Podziel ciąg na tablicę w Bash


640

W skrypcie Bash chciałbym podzielić linię na części i przechowywać je w tablicy.

Linia:

Paris, France, Europe

Chciałbym mieć je w tablicy takiej jak ta:

array[0] = Paris
array[1] = France
array[2] = Europe

Chciałbym użyć prostego kodu, szybkość polecenia nie ma znaczenia. Jak mogę to zrobić?


22
Jest to hit Google nr 1, ale odpowiedź budzi kontrowersje, ponieważ pytanie niestety dotyczy pytania o ograniczanie , (przecinek-spacja), a nie o pojedynczy znak, taki jak przecinek. Jeśli interesuje Cię tylko ta ostatnia, odpowiedzi tutaj są łatwiejsze do naśladowania: stackoverflow.com/questions/918886/...
antak

Jeśli chcesz wyciszyć łańcuch znaków i nie przejmuj się tym, że jest to tablica, cutwarto o tym pamiętać. Separator jest definiowalny en.wikibooks.org/wiki/Cut Można również wyodrębnić dane ze struktury rekordów o stałej szerokości. en.wikipedia.org/wiki/Cut_(Unix) computerhope.com/unix/ucut.htm
JGFMK

Odpowiedzi:


1088
IFS=', ' read -r -a array <<< "$string"

Należy zauważyć, że znaki $IFSsą traktowane indywidualnie jako separatory tak, że w tym przypadku, pola mogą być oddzielone albo przecinkiem lub przestrzeni, a nie z dwóch sekwencji znaków. Co ciekawe, puste pola nie są tworzone, gdy na wejściu pojawia się przecinek, ponieważ spacja jest traktowana specjalnie.

Aby uzyskać dostęp do pojedynczego elementu:

echo "${array[0]}"

Aby iterować po elementach:

for element in "${array[@]}"
do
    echo "$element"
done

Aby uzyskać zarówno indeks, jak i wartość:

for index in "${!array[@]}"
do
    echo "$index ${array[index]}"
done

Ostatni przykład jest przydatny, ponieważ tablice Bash są rzadkie. Innymi słowy, możesz usunąć element lub dodać element, a wtedy indeksy nie są ciągłe.

unset "array[1]"
array[42]=Earth

Aby uzyskać liczbę elementów w tablicy:

echo "${#array[@]}"

Jak wspomniano powyżej, tablice mogą być rzadkie, więc nie należy używać długości, aby uzyskać ostatni element. Oto, jak możesz to zrobić w Bash 4.2 i nowszych wersjach:

echo "${array[-1]}"

w dowolnej wersji Basha (skądś po wersji 2.05b):

echo "${array[@]: -1:1}"

Większe ujemne przesunięcia należy wybierać dalej od końca tablicy. Zwróć uwagę na spację przed znakiem minus w starszej formie. Jest wymagane.


15
Po prostu użyj IFS=', ', wtedy nie musisz usuwać spacji osobno. Test:IFS=', ' read -a array <<< "Paris, France, Europe"; echo "${array[@]}"
10b0

4
@ l0b0: Dzięki. Nie wiem o czym myślałem. Nawiasem mówiąc, lubię używać declare -p arraywyników testowych.
Wstrzymano do odwołania.

1
To nie wydaje się szanować cytatów. Na przykład France, Europe, "Congo, The Democratic Republic of the"podzieli się to po Kongu.
Israel Dov

2
@YisraelDov: Bash nie może sam poradzić sobie z CSV. Nie potrafi odróżnić przecinków w cudzysłowie od tych poza nimi. Musisz użyć narzędzia, które rozumie CSV, takiego jak lib w języku wyższego poziomu, na przykład moduł csv w Pythonie.
Wstrzymano do odwołania.

5
str="Paris, France, Europe, Los Angeles"; IFS=', ' read -r -a array <<< "$str"podzieli się array=([0]="Paris" [1]="France" [2]="Europe" [3]="Los" [4]="Angeles")na notatkę. Działa to zatem tylko z polami bez spacji, ponieważ IFS=', 'jest to zestaw pojedynczych znaków - nie ogranicznik łańcucha.
dawg

332

Wszystkie odpowiedzi na to pytanie są błędne w taki czy inny sposób.


Zła odpowiedź nr 1

IFS=', ' read -r -a array <<< "$string"

1: To niewłaściwe użycie $IFS. Wartość $IFSzmiennej nie jest traktowana jako pojedynczy separator ciągów o zmiennej długości , a raczej jako zestaw separatorów ciągów o jednym znaku , w którym każde pole readoddzielające się od linii wejściowej może być zakończone dowolnym znakiem w zestawie (przecinek lub spacja, w tym przykładzie).

W rzeczywistości, dla prawdziwych kijów tam, pełne znaczenie $IFSjest nieco bardziej zaangażowane. Z podręcznika bash :

Powłoka traktuje każdy znak IFS jako ogranicznik i dzieli wyniki innych rozszerzeń na słowa, używając tych znaków jako terminatorów pól. Jeśli IFS jest rozbrojony lub jego wartość to dokładnie <spacja><tab> <nowa linia> , wartość domyślna to sekwencje <spacja> , <karta> i <nowa linia> na początku i na końcu wyników poprzednich rozszerzeń są ignorowane, a każda sekwencja znaków IFS nie na początku ani na końcu służy do rozgraniczenia słów. Jeśli IFS ma wartość inną niż domyślna, to sekwencje białych znaków <space> , <tab> i <są ignorowane na początku i na końcu słowa, o ile biały znak ma wartość IFS ( biały znak IFS ). Każdy znak w IFS, który nie jest białą spacją IFS , wraz z dowolnymi sąsiadującymi znakami białych spacji IFS , ogranicza pole. Sekwencja białych znaków IFS jest również traktowana jako separator. Jeśli wartość IFS wynosi null, nie występuje dzielenie słów.

Zasadniczo w przypadku wartości domyślnych niepustych wartości $IFSpola można oddzielić (1) sekwencją jednego lub więcej znaków, które wszystkie pochodzą ze zbioru „znaków białych znaków IFS” (to znaczy, którekolwiek z <spacji> , <tab> i <newline> („nowa linia”, co oznacza przesunięcie wiersza (LF) ) są obecne w dowolnym miejscu $IFS) lub (2) dowolny inny niż „znak białych znaków IFS”, który jest obecny $IFSwraz z otaczającymi go „znakami białych znaków IFS” w linii wejściowej.

W przypadku OP możliwe jest, że drugi tryb separacji, który opisałem w poprzednim akapicie, jest dokładnie tym, czego chce dla swojego ciągu wejściowego, ale możemy być całkiem pewni, że pierwszy tryb separacji, który opisałem, nie jest wcale poprawny. Na przykład, co jeśli jego ciąg wejściowy był 'Los Angeles, United States, North America'?

IFS=', ' read -ra a <<<'Los Angeles, United States, North America'; declare -p a;
## declare -a a=([0]="Los" [1]="Angeles" [2]="United" [3]="States" [4]="North" [5]="America")

2: Nawet jeśli użyjesz tego rozwiązania z separatorem jednoznakowym (takim jak przecinek sam w sobie, to znaczy bez następnej spacji lub innego bagażu), jeśli wartość $stringzmiennej zawiera jakieś LF, to readbędzie zatrzymać przetwarzanie, gdy napotka pierwszy LF. readWbudowane przetwarza tylko jedną linię za wezwaniem. Jest to prawdą, nawet jeśli przesyłasz dane wejściowe lub przekierowujesz tylko do readinstrukcji, tak jak robimy w tym przykładzie z mechanizmem łańcuchowym , a zatem nieprzetworzone dane wejściowe są gwarantowane, że zostaną utracone. Kod, który zasila readwbudowane narzędzie, nie ma wiedzy o przepływie danych w ramach zawierającej go struktury poleceń.

Można argumentować, że raczej nie spowoduje to problemu, ale jest to subtelne zagrożenie, którego należy unikać, jeśli to możliwe. Jest to spowodowane faktem, że readwbudowane narzędzie faktycznie dzieli dwa poziomy podziału danych wejściowych: najpierw na linie, a następnie na pola. Ponieważ OP chce tylko jednego poziomu podziału, to użycie readwbudowanej funkcji nie jest właściwe i należy tego unikać.

3: Nieoczywistym potencjalnym problemem związanym z tym rozwiązaniem jest to, że readzawsze upuszcza końcowe pole, jeśli jest puste, chociaż w przeciwnym razie zachowuje puste pola. Oto demo:

string=', , a, , b, c, , , '; IFS=', ' read -ra a <<<"$string"; declare -p a;
## declare -a a=([0]="" [1]="" [2]="a" [3]="" [4]="b" [5]="c" [6]="" [7]="")

Może OP nie przejmowałby się tym, ale nadal jest to ograniczenie, o którym warto wiedzieć. Zmniejsza solidność i ogólność rozwiązania.

Ten problem można rozwiązać, dodając atrapę ogranicznika końcowego do ciągu wejściowego tuż przed jego podaniem read, co pokażę później.


Zła odpowiedź # 2

string="1:2:3:4:5"
set -f                     # avoid globbing (expansion of *).
array=(${string//:/ })

Podobny pomysł:

t="one,two,three"
a=($(echo $t | tr ',' "\n"))

(Uwaga: dodałem brakujące nawiasy wokół podstawiania poleceń, które, jak się zdaje, zostało pominięte przez odpowiadającego).

Podobny pomysł:

string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)

Rozwiązania te wykorzystują dzielenie słów w przypisaniu tablicowym, aby podzielić ciąg na pola. Co zabawne, podobnie jak readw ogólnym dzieleniu słów również używana jest $IFSspecjalna zmienna, chociaż w tym przypadku sugeruje się, że jest ustawiona domyślna wartość <space><tab> <newline> , a zatem dowolna sekwencja jednego lub więcej IFS znaki (które teraz są teraz białymi znakami) są uznawane za separatory pól.

Rozwiązuje to problem popełnienia dwóch poziomów podziału read, ponieważ samo dzielenie słów stanowi tylko jeden poziom podziału. Ale tak jak poprzednio, problem polega na tym, że poszczególne pola w ciągu wejściowym mogą już zawierać $IFSznaki, a zatem byłyby niepoprawnie podzielone podczas operacji dzielenia słów. Zdarza się, że nie dzieje się tak w przypadku żadnego z przykładowych ciągów wejściowych dostarczonych przez tych użytkowników (jak wygodne ...), ale oczywiście nie zmienia to faktu, że każda podstawa kodu, która użyłaby tego idiomu, naraziłaby na ryzyko wysadzenie w powietrze, jeśli to założenie zostanie kiedykolwiek naruszone w pewnym momencie. Jeszcze raz rozważ mój kontrprzykład 'Los Angeles, United States, North America'(lub 'Los Angeles:United States:North America').

Również podział na słowa jest zwykle następnie rozszerzenia nazwy pliku ( vel rozszerzalności ścieżce, aka masek), który jeśli odbywa się, że potencjalnie uszkodzone słowa zawierające znaki *, ?lub [następnie ](a jeśli extglobjest ustawiony w nawiasach fragmenty poprzedzony ?, *, +, @, lub !) poprzez dopasowanie ich do obiektów systemu plików i odpowiednie rozwinięcie słów („globs”). Pierwszy z tych trzech odpowiadających sprytnie podciął ten problem, uruchamiając set -fwcześniej, aby wyłączyć globowanie. Technicznie działa to (choć prawdopodobnie należy dodaćset +f później do ponownego włączenia globowania dla kolejnego kodu, który może od niego zależeć), ale niepożądane jest wprowadzanie bałaganu w globalnych ustawieniach powłoki w celu zhakowania podstawowej operacji parsowania łańcucha na tablicę w kodzie lokalnym.

Innym problemem związanym z tą odpowiedzią jest to, że wszystkie puste pola zostaną utracone. Może to stanowić problem, w zależności od aplikacji.

Uwaga: jeśli zamierzasz skorzystać z tego rozwiązania, lepiej użyć ${string//:/ }formy rozszerzania parametrów w formie „podstawienia wzorca” , zamiast kłopotać się z wywołaniem podstawienia polecenia (które powoduje rozwarcie powłoki), uruchomieniem potoku i uruchamianie zewnętrznego pliku wykonywalnego ( trlub sed), ponieważ interpretacja parametrów jest wyłącznie operacją wewnętrzną powłoki. (Ponadto w przypadku rozwiązań tri sedzmienna wejściowa powinna być podwójnie cytowana w podstawieniu polecenia; w przeciwnym razie podział słowa zadziałałby w echopoleceniu i potencjalnie bałaganiłby wartości pól. Również $(...)forma podstawienia polecenia jest lepsza niż stara`...` formularza, ponieważ upraszcza zagnieżdżanie podstawień poleceń i pozwala na lepsze wyróżnianie składni przez edytory tekstu).


Zła odpowiedź # 3

str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

Ta odpowiedź jest prawie taka sama jak # 2 . Różnica polega na tym, że odpowiadający przyjął założenie, że pola są rozdzielone dwoma znakami, z których jeden jest reprezentowany domyślnie $IFS, a drugi nie. Rozwiązał ten dość szczególny przypadek, usuwając znak nie reprezentowany przez IFS, używając rozszerzenia podstawiania wzorca, a następnie stosując dzielenie słów, aby podzielić pola na pozostałym znaku ograniczającym reprezentowanym przez IFS.

To nie jest bardzo ogólne rozwiązanie. Co więcej, można argumentować, że przecinek jest tak naprawdę „głównym” znakiem ograniczającym tutaj i że usuwanie go, a następnie zależnie od znaku spacji do dzielenia pól jest po prostu niewłaściwe. Po raz kolejny, że mój kontrprzykład: 'Los Angeles, United States, North America'.

Również ponownie rozszerzenie nazwy pliku może uszkodzić rozwinięte słowa, ale można temu zapobiec, tymczasowo wyłączając globowanie dla przypisania za pomocą, set -fa następnie set +f.

Ponownie wszystkie puste pola zostaną utracone, co może, ale nie musi stanowić problemu, w zależności od aplikacji.


Zła odpowiedź # 4

string='first line
second line
third line'

oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"

Jest to podobne do # 2 i # 3 , ponieważ wykorzystuje dzielenie słów, aby wykonać zadanie, tylko teraz kod jawnie ustawia się tak, $IFSaby zawierał tylko jednoznakowy separator pola obecny w ciągu wejściowym. Należy powtórzyć, że nie może to działać w przypadku ograniczników wieloznakowych, takich jak ogranicznik przecinka w PO. Ale w przypadku ogranicznika jednoznakowego, takiego jak LF zastosowanego w tym przykładzie, w rzeczywistości jest on prawie idealny. Pola nie mogą zostać przypadkowo podzielone na środku, jak widzieliśmy przy poprzednich błędnych odpowiedziach, i istnieje tylko jeden poziom podziału, zgodnie z wymaganiami.

Jednym z problemów jest to, że rozszerzenie nazwy pliku uszkodzi słowa, których to dotyczy, jak opisano wcześniej, chociaż można to rozwiązać, umieszczając krytyczne zdanie w set -fi set +f.

Innym potencjalnym problemem jest to, że ponieważ LF kwalifikuje się jako „znak białych znaków IFS”, jak zdefiniowano wcześniej, wszystkie puste pola zostaną utracone, tak jak w punktach 2 i 3 . Oczywiście nie stanowiłoby to problemu, gdyby separator był innym niż „znakiem białych znaków IFS”, a w zależności od aplikacji i tak może nie mieć znaczenia, ale narusza ogólność rozwiązania.

Tak więc, podsumowując, zakładając, że masz jeden ogranicznik-znakowy, i jest to albo nie- „IFS biały znak” lub nie dbają o pustych pól, i owinąć krytyczne oświadczenie set -fi set +f, to rozwiązanie działa , ale poza tym nie.

(Również ze względów informacyjnych przypisanie LF do zmiennej w bash można łatwiej wykonać za pomocą $'...'składni, np IFS=$'\n';.)


Zła odpowiedź # 5

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

Podobny pomysł:

IFS=', ' eval 'array=($string)'

To rozwiązanie jest w rzeczywistości skrzyżowaniem między nr 1 (w tym, że ustawia się $IFSna przecinek) i # 2-4 (w tym, że wykorzystuje dzielenie słów, aby podzielić ciąg na pola). Z tego powodu cierpi na większość problemów, które dotyczą wszystkich powyższych błędnych odpowiedzi, podobnie jak najgorszy ze wszystkich światów.

Również w odniesieniu do drugiego wariantu może się wydawać, że evalwywołanie jest całkowicie niepotrzebne, ponieważ jego argument jest dosłownym ciągiem cudzysłowu i dlatego jest statycznie znany. Ale w rzeczywistości korzystanie z evaltego sposobu jest bardzo nieoczywiste . Zwykle po uruchomieniu prostego polecenia, które składa się tylko z przypisania zmiennej , co oznacza, że ​​nie następuje po nim rzeczywiste słowo polecenia, przypisanie działa w środowisku powłoki:

IFS=', '; ## changes $IFS in the shell environment

Jest to prawdą, nawet jeśli proste polecenie obejmuje wiele przypisań zmiennych; ponownie, dopóki nie ma słowa polecenia, wszystkie przypisania zmiennych wpływają na środowisko powłoki:

IFS=', ' array=($countries); ## changes both $IFS and $array in the shell environment

Ale jeśli przypisanie zmiennej jest dołączone do nazwy polecenia (chciałbym to nazwać „przypisaniem prefiksu”), to nie wpływa to na środowisko powłoki, a jedynie wpływa na środowisko wykonanego polecenia, niezależnie od tego, czy jest ono wbudowane lub zewnętrzny:

IFS=', ' :; ## : is a builtin command, the $IFS assignment does not outlive it
IFS=', ' env; ## env is an external command, the $IFS assignment does not outlive it

Odpowiedni cytat z podręcznika bash :

Jeśli nie pojawi się nazwa polecenia, przypisania zmiennych wpływają na bieżące środowisko powłoki. W przeciwnym razie zmienne są dodawane do środowiska wykonanego polecenia i nie wpływają na bieżące środowisko powłoki.

Możliwe jest wykorzystanie tej funkcji przypisywania zmiennych do zmiany $IFStylko tymczasowo, co pozwala nam uniknąć całego zapisu i przywracania gambitu takiego jak ten, który jest wykonywany ze $OIFSzmienną w pierwszym wariancie. Ale wyzwanie, przed którym stoimy, polega na tym, że polecenie, które musimy wykonać, samo w sobie jest jedynie zmiennym przypisaniem, a zatem nie wymagałoby słowa polecenia, aby uczynić $IFSprzypisanie tymczasowym. Możesz pomyśleć, dlaczego nie po prostu dodać do polecenia słowa zakazu, : builtinaby $IFStymczasowo przypisać to zadanie? To nie działa, ponieważ spowodowałoby to również $arraytymczasowe przypisanie:

IFS=', ' array=($countries) :; ## fails; new $array value never escapes the : command

Tak więc, jesteśmy skutecznie w impasie, trochę w pułapce 22. Ale kiedy evaluruchamia swój kod, uruchamia go w środowisku powłoki, tak jakby to był normalny, statyczny kod źródłowy, i dlatego możemy uruchomić $arrayprzypisanie w evalargumencie, aby zadziałało w środowisku powłoki, podczas gdy $IFSprzypisanie prefiksu to jest poprzedzony evalpoleceniem, nie przeżyje evalpolecenia. To jest właśnie sztuczka stosowana w drugim wariancie tego rozwiązania:

IFS=', ' eval 'array=($string)'; ## $IFS does not outlive the eval command, but $array does

Tak więc, jak widać, jest to naprawdę sprytna sztuczka i realizuje dokładnie to, co jest wymagane (przynajmniej w odniesieniu do realizacji przypisania) w dość nieoczywisty sposób. W rzeczywistości nie jestem przeciwny tej sztuczce, pomimo zaangażowania eval; po prostu ostrożnie, aby zacytować ciąg argumentu, aby uchronić się przed zagrożeniami bezpieczeństwa.

Ale znowu, z powodu aglomeracji problemów „najgorszego ze wszystkich światów”, nadal jest to zła odpowiedź na wymagania PO.


Zła odpowiedź # 6

IFS=', '; array=(Paris, France, Europe)

IFS=' ';declare -a array=(Paris France Europe)

Co? OP ma zmienną łańcuchową, którą należy przeanalizować w tablicy. Ta „odpowiedź” zaczyna się od pełnej treści ciągu wejściowego wklejonego do literału tablicowego. Myślę, że to jeden ze sposobów, aby to zrobić.

Wygląda na to, że odpowiadający mógł założyć, że $IFSzmienna wpływa na wszystkie analizy bash we wszystkich kontekstach, co nie jest prawdą. Z podręcznika bash:

IFS     Wewnętrzny separator pól używany do dzielenia słów po rozwinięciu i dzielenia linii na słowa za pomocą wbudowanego polecenia odczytu . Wartość domyślna to <space><tab> <newline> .

Tak więc $IFSspecjalna zmienna jest faktycznie używana tylko w dwóch kontekstach: (1) dzielenie słów, które jest wykonywane po rozwinięciu (to znaczy nie podczas analizowania kodu źródłowego bash) i (2) do dzielenia linii wejściowych na słowa przez readwbudowane.

Pozwól mi spróbować to wyjaśnić. Myślę, że dobrze byłoby rozróżnić parsowanie i wykonanie . Bash musi najpierw przeanalizować kod źródłowy, co oczywiście jest zdarzeniem analizującym , a następnie wykonuje kod, czyli wtedy, gdy pojawia się rozszerzenie. Rozszerzenie jest naprawdę zdarzeniem wykonawczym . Ponadto mam problem z opisem $IFSzmiennej, którą właśnie cytowałem powyżej; zamiast powiedzieć, że dzielenie słów odbywa się po rozwinięciu , powiedziałbym, że dzielenie słów odbywa się podczas rozwijania, a może nawet bardziej precyzyjnie, dzielenie słów jest częściąproces ekspansji. Wyrażenie „dzielenie słów” odnosi się tylko do tego etapu ekspansji; nigdy nie należy go używać do odwoływania się do parsowania kodu źródłowego bash, chociaż niestety dokumenty wydają się często rzucać na słowa „split” i „words”. Oto odpowiedni fragment linux.die.net wersji podręcznika bash:

Rozwinięcie jest wykonywane w wierszu polecenia po podzieleniu go na słowa. Istnieje siedem rodzajów ekspansji wykonywane: interpretacja nawiasów , tyldy , parametrów i zmiennych ekspansji , podstawiania poleceń , interpretacji wyrażeń arytmetycznych , podziałowi na słowa i rozwijania nazw plików .

Kolejność rozszerzeń jest następująca: nawias klamrowy; interpretacja tyldy, interpretacja parametrów i zmiennych, interpretacja arytmetyczna i podstawianie poleceń (wykonywane od lewej do prawej); dzielenie słów; i rozszerzenie nazwy ścieżki.

Można argumentować, że wersja podręcznika GNU jest nieco lepsza, ponieważ w pierwszym zdaniu rozdziału o rozszerzeniu wybiera słowo „tokeny” zamiast „słowa”:

Rozbudowa jest wykonywana w wierszu poleceń po podzieleniu na tokeny.

Ważne jest to, $IFSże nie zmienia sposobu, w jaki bash analizuje kod źródłowy. Analiza kodu źródłowego bash jest w rzeczywistości bardzo złożonym procesem, który obejmuje rozpoznawanie różnych elementów gramatyki powłoki, takich jak sekwencje poleceń, listy poleceń, potoki, rozszerzenia parametrów, podstawienia arytmetyczne i podstawienia poleceń. W większości przypadków proces analizowania bash nie może zostać zmieniony przez działania na poziomie użytkownika, takie jak przypisania zmiennych (w rzeczywistości istnieją pewne niewielkie wyjątki od tej reguły; na przykład zobacz różne compatxxustawienia powłoki, które mogą zmienić niektóre aspekty przetwarzania podczas pracy). Początkowe „słowa” / „tokeny”, które wynikają z tego złożonego procesu analizy, są następnie rozwijane zgodnie z ogólnym procesem „ekspansji”, w podziale na powyższe fragmenty dokumentacji, w których dzielenie słów rozwiniętego (rozwijanego?) Tekstu na dalszy słowa to po prostu jeden z kroków tego procesu. Dzielenie wyrazów dotyczy tylko tekstu wypluwanego z poprzedniego kroku rozwijania; nie wpływa na dosłowny tekst, który został przeanalizowany bezpośrednio ze źródłowego strumienia bocznego.


Zła odpowiedź # 7

string='first line
        second line
        third line'

while read -r line; do lines+=("$line"); done <<<"$string"

To jedno z najlepszych rozwiązań. Zauważ, że wróciliśmy do używania read. Czy nie mówiłem wcześniej, że readjest to niewłaściwe, ponieważ wykonuje dwa poziomy podziału, gdy tylko potrzebujemy jednego? Sztuczka polega na tym, że możesz wywoływać readw taki sposób, że skutecznie wykonuje tylko jeden poziom podziału, w szczególności poprzez rozdzielenie tylko jednego pola na wywołanie, co wymaga kosztu konieczności wielokrotnego wywoływania go w pętli. To trochę sztuczka, ale działa.

Ale są problemy. Po pierwsze: podanie co najmniej jednego argumentu NAZWAread powoduje automatyczne ignorowanie początkowych i końcowych białych znaków w każdym polu oddzielonym od ciągu wejściowego. Dzieje się tak niezależnie od tego, czy $IFSjest ustawiona na wartość domyślną, czy nie, jak opisano wcześniej w tym poście. Teraz OP może nie przejmować się tym ze względu na swój konkretny przypadek użycia, aw rzeczywistości może być pożądaną cechą zachowania podczas analizowania. Ale nie każdy, kto chce parsować ciąg znaków w pola, będzie tego chciał. Istnieje jednak rozwiązanie: nieco nieoczywistym zastosowaniem readjest przekazywanie zerowych argumentów NAME . W takim przypadku readzapisze całą linię wejściową otrzymaną ze strumienia wejściowego w zmiennej o nazwie $REPLYi, jako bonus, nie będzieusuń początkowe i końcowe białe spacje z wartości. Jest to bardzo solidne użycie, z readktórego często korzystałem w swojej karierze programistycznej. Oto demonstracja różnicy w zachowaniu:

string=$'  a  b  \n  c  d  \n  e  f  '; ## input string

a=(); while read -r line; do a+=("$line"); done <<<"$string"; declare -p a;
## declare -a a=([0]="a  b" [1]="c  d" [2]="e  f") ## read trimmed surrounding whitespace

a=(); while read -r; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="  a  b  " [1]="  c  d  " [2]="  e  f  ") ## no trimming

Drugi problem z tym rozwiązaniem polega na tym, że tak naprawdę nie zajmuje się przypadkiem niestandardowego separatora pól, takiego jak przecinek PO. Tak jak poprzednio, separatory wieloznakowe nie są obsługiwane, co jest niefortunnym ograniczeniem tego rozwiązania. Możemy spróbować przynajmniej podzielić przecinek, określając separator dla -dopcji, ale spójrz, co się stanie:

string='Paris, France, Europe';
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France")

Jak można się było spodziewać, nieznane otaczające białe spacje zostały wciągnięte do wartości pola, a zatem należałoby to później skorygować poprzez operacje przycinania (można to również zrobić bezpośrednio w pętli while). Ale jest jeszcze jeden oczywisty błąd: brakuje Europy! Co się z tym stało? Odpowiedź jest taka, że readzwraca nieudany kod powrotu, jeśli trafi na koniec pliku (w tym przypadku możemy go nazwać końcem ciągu) bez napotkania końcowego terminatora pola na polu końcowym. Powoduje to przedwczesne zerwanie pętli while i tracimy końcowe pole.

Technicznie ten sam błąd dotyczył również poprzednich przykładów; różnica polega na tym, że jako separator pól przyjęto LF, co jest wartością domyślną, gdy nie podasz -dopcji, a mechanizm <<<(„ciąg tutaj”) automatycznie dołącza LF do łańcucha tuż przed przekazaniem go jako wejście do polecenia. Dlatego w takich przypadkach przypadkowo rozwiązaliśmy problem upuszczenia pola końcowego, nieświadomie dołączając dodatkowy wejściowy terminator do wejścia. Nazwijmy to rozwiązanie „obojętnym terminatorem”. Możemy ręcznie zastosować rozwiązanie fikcyjne-terminator do dowolnego niestandardowego separatora, sami łącząc go z ciągiem wejściowym podczas tworzenia go w ciągu tutaj:

a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,"; declare -p a;
declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")

Tam problem rozwiązany. Innym rozwiązaniem jest przerwanie pętli while tylko wtedy, gdy oba (1) readzwróciły błąd, a (2) $REPLYjest pusty, co oznacza, że readnie był w stanie odczytać żadnych znaków przed trafieniem na koniec pliku. Próbny:

a=(); while read -rd,|| [[ -n "$REPLY" ]]; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')

Podejście to ujawnia również tajny LF, który jest automatycznie dołączany do ciągu przez <<<operator przekierowania. Można go oczywiście usunąć osobno poprzez jawną operację przycinania, jak opisano przed chwilą, ale oczywiście ręczne podejście manekina-terminatora rozwiązuje to bezpośrednio, więc możemy po prostu to zrobić. Ręczne rozwiązanie manekina-terminatora jest w rzeczywistości całkiem wygodne, ponieważ rozwiązuje oba te dwa problemy (problem upuszczonego pola końcowego i dołączony problem LF) za jednym razem.

Ogólnie rzecz biorąc, jest to dość potężne rozwiązanie. Jedyną słabością pozostaje brak wsparcia dla ograniczników wieloznakowych, którym zajmę się później.


Zła odpowiedź # 8

string='first line
        second line
        third line'

readarray -t lines <<<"$string"

(Tak naprawdę pochodzi z tego samego posta co nr 7 ; odpowiadający podał dwa rozwiązania w tym samym poście).

readarrayWbudowane, które jest synonimem mapfile, jest idealny. Jest to wbudowane polecenie, które analizuje strumień bajtów w zmienną tablicową w jednym ujęciu; bez bałaganu z pętlami, warunkami, zamianami lub czymkolwiek innym. I nie skrycie ukrywa żadnych białych znaków w ciągu wejściowym. I (jeśli -Onie podano) wygodnie usuwa tablicę docelową przed przypisaniem do niej. Ale wciąż nie jest idealny, stąd moja krytyka jako „złej odpowiedzi”.

Po pierwsze, aby to usunąć, zauważ, że podobnie jak zachowanie readpodczas parsowania pola, readarrayupuszcza końcowe pole, jeśli jest puste. Ponownie, prawdopodobnie nie dotyczy to PO, ale może dotyczyć niektórych przypadków użycia. Wrócę do tego za chwilę.

Po drugie, jak poprzednio, nie obsługuje ograniczników wieloznakowych. Zaraz to naprawię.

Po trzecie, zapisane rozwiązanie nie analizuje ciągu wejściowego OP, a w rzeczywistości nie można go użyć jako takiego do przeanalizowania. Zaraz też to rozwinę.

Z powyższych powodów nadal uważam to za „złą odpowiedź” na pytanie PO. Poniżej podam odpowiedź, którą uważam za właściwą.


Poprawna odpowiedź

Oto naiwna próba sprawienia, by numer 8 działał, po prostu określając -dopcję:

string='Paris, France, Europe';
readarray -td, a <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')

Widzimy, że wynik jest identyczny z wynikiem uzyskanym z podwójnego warunkowego podejścia readrozwiązania zapętlającego omówionego w punkcie 7 . Prawie możemy to rozwiązać za pomocą sztuczki manekina-terminatora:

readarray -td, a <<<"$string,"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe" [3]=$'\n')

Problem polega na tym, że readarrayzachowano końcowe pole, ponieważ <<<operator przekierowania dołączył LF do ciągu wejściowego, a zatem końcowe pole nie było puste (w przeciwnym razie zostałoby upuszczone). Możemy sobie z tym poradzić poprzez jawne rozbrojenie końcowego elementu tablicy po fakcie:

readarray -td, a <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")

Jedynymi dwoma pozostałymi problemami, które są rzeczywiście powiązane, są (1) obce białe znaki, które należy usunąć, oraz (2) brak wsparcia dla ograniczników wieloznakowych.

Oczywiście można później przyciąć biały znak (na przykład zobacz Jak przyciąć biały znak ze zmiennej Bash? ). Ale jeśli uda nam się zhakować ogranicznik wieloznakowy, rozwiąże to oba problemy za jednym razem.

Niestety nie ma bezpośredniego sposobu na uruchomienie ogranicznika wieloznakowego. Najlepszym rozwiązaniem, jakie wymyśliłem, jest wstępne przetworzenie ciągu wejściowego w celu zastąpienia separatora wieloznakowego separatorem jednoznakowym, który gwarantuje, że nie koliduje z zawartością ciągu wejściowego. Jedyny znak, który ma tę gwarancję, to bajt NUL . Wynika to z faktu, że w bash (nawiasem mówiąc nie w Zsh) zmienne nie mogą zawierać bajtu NUL. Ten etap przetwarzania wstępnego można wykonać bezpośrednio w procesie podstawiania. Oto jak to zrobić za pomocą awk :

readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; }' <<<"$string, "); unset 'a[-1]';
declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")

Tam w końcu! To rozwiązanie nie będzie błędnie dzielić pól na środku, nie przedwcześnie wycinać, nie upuszczać pustych pól, nie uszkadzać się przy rozszerzeniach nazw plików, nie będzie automatycznie usuwać początkowych i końcowych białych znaków, nie pozostawi zagubionego LF na końcu, nie wymaga pętli i nie zadowala się ogranicznikiem jednoznakowym.


Rozwiązanie do przycinania

Na koniec chciałem zademonstrować własne dość skomplikowane rozwiązanie do przycinania przy użyciu niejasnej -C callbackopcji readarray. Niestety skończyło mi się miejsce na drakońskim limicie 30 000 znaków Stack Overflow, więc nie będę w stanie tego wyjaśnić. Zostawię to jako ćwiczenie dla czytelnika.

function mfcb { local val="$4"; "$1"; eval "$2[$3]=\$val;"; };
function val_ltrim { if [[ "$val" =~ ^[[:space:]]+ ]]; then val="${val:${#BASH_REMATCH[0]}}"; fi; };
function val_rtrim { if [[ "$val" =~ [[:space:]]+$ ]]; then val="${val:0:${#val}-${#BASH_REMATCH[0]}}"; fi; };
function val_trim { val_ltrim; val_rtrim; };
readarray -c1 -C 'mfcb val_trim a' -td, <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")

8
Pomocne może być również odnotowanie (choć zrozumiałe, że nie było na to miejsca), że -dopcja readarraypierwszego pojawienia się w Bash 4.4.
fbicknel

2
Świetna odpowiedź (+1). Jeśli zmienisz awk na awk '{ gsub(/,[ ]+|$/,"\0"); print }'i wyeliminujesz konkatenację finału ", " , nie musisz przechodzić przez gimnastykę po wyeliminowaniu ostatecznego rekordu. Więc: readarray -td '' a < <(awk '{ gsub(/,[ ]+/,"\0"); print; }' <<<"$string")na Bash, który obsługuje readarray. Zauważ, że twoja metoda to Bash 4.4+. Myślę, że z powodu -dinreadarray
wit

3
@datUser To niefortunne. Twoja wersja bash musi być za stara readarray. W takim przypadku możesz użyć wbudowanego drugiego najlepszego rozwiązania read. Mam na myśli to: a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,";(z awkpodstawieniem, jeśli potrzebujesz obsługi ogranicznika wieloznakowego). Daj mi znać, jeśli napotkasz jakieś problemy; Jestem prawie pewien, że to rozwiązanie powinno działać na dość starych wersjach basha, powracając do wersji 2-coś, wydanej jak dwie dekady temu.
bgoldst

1
Wow, co za wspaniała odpowiedź! Hee hee, moja odpowiedź: porzuciłem skrypt bash i odpaliłem pytona!
artfulrobot

1
@datUser bash na OSX wciąż jest zablokowany na poziomie 3.2 (wydany ok. 2007); Użyłem basha znalezionego w Homebrew, aby uzyskać wersje bash 4.X na OS X
JDS 14'18

222

Oto sposób bez ustawiania IFS:

string="1:2:3:4:5"
set -f                      # avoid globbing (expansion of *).
array=(${string//:/ })
for i in "${!array[@]}"
do
    echo "$i=>${array[i]}"
done

Pomysł polega na zamianie łańcucha:

${string//substring/replacement}

aby zastąpić wszystkie dopasowania podłańcucha $ spacją, a następnie za pomocą podstawionego łańcucha zainicjować tablicę:

(element1 element2 ... elementN)

Uwaga: ta odpowiedź korzysta z operatora split + glob . Dlatego, aby zapobiec ekspansji niektórych znaków (np. *), Dobrym pomysłem jest wstrzymanie globowania dla tego skryptu.


1
Użyłem tego podejścia ... dopóki nie natknąłem się na długi sznurek do podziału. 100% procesora przez ponad minutę (potem go zabiłem). Szkoda, ponieważ ta metoda pozwala na podział na ciąg znaków, a nie na niektóre znaki w IFS.
Werner Lehmann

100% czasu procesora przez ponad minutę wydaje mi się, że coś jest nie tak. Jak długi był ten ciąg, czy ma on rozmiar MB lub GB? Myślę, że normalnie, jeśli będziesz potrzebować tylko małego podziału łańcucha, chcesz pozostać w Bash, ale jeśli jest to ogromny plik, wykonałbym coś takiego jak Perl, aby to zrobić.

12
OSTRZEŻENIE: Właśnie natrafiłem na problem z tym podejściem. Jeśli masz element o nazwie *, otrzymasz również wszystkie elementy swojego cwd. zatem string = "1: 2: 3: 4: *" da pewne nieoczekiwane i potencjalnie niebezpieczne wyniki w zależności od twojej implementacji. Nie otrzymałem tego samego błędu z (IFS = ',' read -a array <<< "$ string") i ten wydaje się bezpieczny w użyciu.
Dieter Gribnitz

4
cytowanie ${string//:/ }zapobiega rozszerzeniu powłoki
Andrew White

1
Musiałem użyć następujących elementów w OSX: array=(${string//:/ })
Mark Thomson

95
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"

Drukuje trzy


8
Właściwie wolę to podejście. Prosty.
shrimpwagon,

4
Skopiowałem i wkleiłem to i to nie działało z echo, ale działało, gdy użyłem go w pętli for.
Ben

2
To nie działa, jak stwierdzono. @ Jmoney38 lub shrimpwagon, jeśli możesz wkleić to w terminalu i uzyskać pożądane wyjście, wklej wynik tutaj.
abalter

2
@abalter Działa dla mnie z a=($(echo $t | tr ',' "\n")). Ten sam wynik z a=($(echo $t | tr ',' ' ')).
liść

@procrastinator prostu próbowałem go VERSION="16.04.2 LTS (Xenial Xerus)"w bashmuszli, a ostatni echowłaśnie drukuje pusty wiersz. Jakiej wersji systemu Linux i jakiej powłoki używasz? Niestety nie można wyświetlić sesji terminalu w komentarzu.
abalter

29

Czasami zdarzało mi się, że metoda opisana w zaakceptowanej odpowiedzi nie działała, szczególnie jeśli separator jest znakiem powrotu karetki.
W tych przypadkach rozwiązałem w ten sposób:

string='first line
second line
third line'

oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"

for line in "${lines[@]}"
    do
        echo "--> $line"
done

2
+1 To całkowicie działało dla mnie. Musiałem umieścić wiele tablic, podzielonych znakiem nowej linii, w tablicy i read -a arr <<< "$strings"nie działałem IFS=$'\n'.
Stefan van den Akker


To nie do końca odpowiada oryginalne pytanie.
Mike

29

Akceptowana odpowiedź działa dla wartości w jednym wierszu.
Jeśli zmienna ma kilka wierszy:

string='first line
        second line
        third line'

Potrzebujemy zupełnie innego polecenia, aby uzyskać wszystkie wiersze:

while read -r line; do lines+=("$line"); done <<<"$string"

Lub o wiele prostsza tablica bash :

readarray -t lines <<<"$string"

Drukowanie wszystkich linii jest bardzo łatwe dzięki funkcji printf:

printf ">[%s]\n" "${lines[@]}"

>[first line]
>[        second line]
>[        third line]

2
Chociaż nie każde rozwiązanie działa w każdej sytuacji, twoja wzmianka o tablicy ... zastąpiła moje ostatnie dwie godziny 5 minutami ... masz mój głos
Angry 84


6

Kluczem do podziału łańcucha na tablicę jest wieloznakowy ogranicznik ", ". Każde rozwiązanie stosowane IFSdo ograniczników wieloznakowych jest z natury złe, ponieważ IFS jest zbiorem tych znaków, a nie ciągiem znaków.

Jeśli przypiszesz, IFS=", "łańcuch zostanie przerwany na EITHER ","OR " "lub dowolnej ich kombinacji, która nie jest dokładną reprezentacją dwóch znaków ogranicznika ", ".

Możesz użyć awklub, sedaby podzielić ciąg, z podstawieniem procesu:

#!/bin/bash

str="Paris, France, Europe"
array=()
while read -r -d $'\0' each; do   # use a NUL terminated field separator 
    array+=("$each")
done < <(printf "%s" "$str" | awk '{ gsub(/,[ ]+|$/,"\0"); print }')
declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output

Bardziej efektywne jest użycie wyrażenia regularnego bezpośrednio w Bash:

#!/bin/bash

str="Paris, France, Europe"

array=()
while [[ $str =~ ([^,]+)(,[ ]+|$) ]]; do
    array+=("${BASH_REMATCH[1]}")   # capture the field
    i=${#BASH_REMATCH}              # length of field + delimiter
    str=${str:i}                    # advance the string by that length
done                                # the loop deletes $str, so make a copy if needed

declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output...

W drugiej formie nie ma podpowłoki i będzie ona z natury szybsza.


Edytuj przez bgoldst: Oto kilka testów porównujących moje readarrayrozwiązanie z rozwiązaniem regex dawga , a także załączyłem rozwiązanie tego, co readdo cholery (uwaga: nieco zmodyfikowałem rozwiązanie regex dla większej harmonii z moim rozwiązaniem) (zobacz także moje komentarze poniżej Poczta):

## competitors
function c_readarray { readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); unset 'a[-1]'; };
function c_read { a=(); local REPLY=''; while read -r -d ''; do a+=("$REPLY"); done < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); };
function c_regex { a=(); local s="$1, "; while [[ $s =~ ([^,]+),\  ]]; do a+=("${BASH_REMATCH[1]}"); s=${s:${#BASH_REMATCH}}; done; };

## helper functions
function rep {
    local -i i=-1;
    for ((i = 0; i<$1; ++i)); do
        printf %s "$2";
    done;
}; ## end rep()

function testAll {
    local funcs=();
    local args=();
    local func='';
    local -i rc=-1;
    while [[ "$1" != ':' ]]; do
        func="$1";
        if [[ ! "$func" =~ ^[_a-zA-Z][_a-zA-Z0-9]*$ ]]; then
            echo "bad function name: $func" >&2;
            return 2;
        fi;
        funcs+=("$func");
        shift;
    done;
    shift;
    args=("$@");
    for func in "${funcs[@]}"; do
        echo -n "$func ";
        { time $func "${args[@]}" >/dev/null 2>&1; } 2>&1| tr '\n' '/';
        rc=${PIPESTATUS[0]}; if [[ $rc -ne 0 ]]; then echo "[$rc]"; else echo; fi;
    done| column -ts/;
}; ## end testAll()

function makeStringToSplit {
    local -i n=$1; ## number of fields
    if [[ $n -lt 0 ]]; then echo "bad field count: $n" >&2; return 2; fi;
    if [[ $n -eq 0 ]]; then
        echo;
    elif [[ $n -eq 1 ]]; then
        echo 'first field';
    elif [[ "$n" -eq 2 ]]; then
        echo 'first field, last field';
    else
        echo "first field, $(rep $[$1-2] 'mid field, ')last field";
    fi;
}; ## end makeStringToSplit()

function testAll_splitIntoArray {
    local -i n=$1; ## number of fields in input string
    local s='';
    echo "===== $n field$(if [[ $n -ne 1 ]]; then echo 's'; fi;) =====";
    s="$(makeStringToSplit "$n")";
    testAll c_readarray c_read c_regex : "$s";
}; ## end testAll_splitIntoArray()

## results
testAll_splitIntoArray 1;
## ===== 1 field =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.000s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 10;
## ===== 10 fields =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.001s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 100;
## ===== 100 fields =====
## c_readarray   real  0m0.069s   user 0m0.000s   sys  0m0.062s
## c_read        real  0m0.065s   user 0m0.000s   sys  0m0.046s
## c_regex       real  0m0.005s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 1000;
## ===== 1000 fields =====
## c_readarray   real  0m0.084s   user 0m0.031s   sys  0m0.077s
## c_read        real  0m0.092s   user 0m0.031s   sys  0m0.046s
## c_regex       real  0m0.125s   user 0m0.125s   sys  0m0.000s
##
testAll_splitIntoArray 10000;
## ===== 10000 fields =====
## c_readarray   real  0m0.209s   user 0m0.093s   sys  0m0.108s
## c_read        real  0m0.333s   user 0m0.234s   sys  0m0.109s
## c_regex       real  0m9.095s   user 0m9.078s   sys  0m0.000s
##
testAll_splitIntoArray 100000;
## ===== 100000 fields =====
## c_readarray   real  0m1.460s   user 0m0.326s   sys  0m1.124s
## c_read        real  0m2.780s   user 0m1.686s   sys  0m1.092s
## c_regex       real  17m38.208s   user 15m16.359s   sys  2m19.375s
##

Bardzo fajne rozwiązanie! Nigdy nie myślałem o użyciu pętli do dopasowania wyrażenia regularnego, sprytne użycie $BASH_REMATCH. Działa i rzeczywiście unika tworzenia podpowłok. +1 ode mnie Jednak w wyniku krytyki sam regex jest trochę nieidealny, ponieważ wydaje się, że musiałeś zduplikować część tokena separatora (szczególnie przecinek), aby obejść brak wsparcia dla niechcianych multiplikatorów (także lookarounds) w ERE („rozszerzony” smak regularny wbudowany w bash). To sprawia, że ​​jest trochę mniej ogólny i solidny.
bgoldst

Po drugie, przeprowadziłem testy porównawcze i chociaż wydajność jest lepsza niż w przypadku innych rozwiązań dla małych łańcuchów, pogarsza się wykładniczo z powodu powtarzającej się przebudowy łańcucha, który staje się katastrofalny dla bardzo dużych łańcuchów. Zobacz moją edycję swojej odpowiedzi.
bgoldst

@bgoldst: Co za fajny test! W obronie wyrażenia regularnego, dla 10 lub 100 tysięcy pól (co regex dzieli), prawdopodobnie istniałaby jakaś forma zapisu (jak \nrozdzielone linie tekstu) zawierająca te pola, więc katastrofalne spowolnienie prawdopodobnie nie nastąpiłoby. Jeśli masz ciąg z 100 000 pól - może Bash nie jest idealny ;-) Dzięki za test. Nauczyłem się czegoś lub dwóch.
dawg

4

Rozwiązanie wieloznakowego separatora Pure Bash.

Jak zauważyli inni w tym wątku, pytanie OP podało przykład ciągu rozdzielanego przecinkami, który ma zostać przeanalizowany w tablicy, ale nie wskazało, czy był zainteresowany tylko ogranicznikami przecinkowymi, ogranicznikami jednoznakowymi lub znakami wieloznakowymi ograniczniki.

Ponieważ Google ma tendencję do umieszczania tej odpowiedzi na górze lub w pobliżu wyników wyszukiwania, chciałem zapewnić czytelnikom silną odpowiedź na pytanie o ograniczniki wielu znaków, ponieważ jest to również wspomniane w co najmniej jednej odpowiedzi.

Jeśli szukasz rozwiązania problemu z ogranicznikiem wieloznakowym, sugeruję przejrzenie postu Mallikarjun M. , w szczególności odpowiedzi od gniourf_gniourf, który zapewnia to eleganckie czyste rozwiązanie BASH za pomocą rozszerzenia parametrów:

#!/bin/bash
str="LearnABCtoABCSplitABCaABCString"
delimiter=ABC
s=$str$delimiter
array=();
while [[ $s ]]; do
    array+=( "${s%%"$delimiter"*}" );
    s=${s#*"$delimiter"};
done;
declare -p array

Link do cytowanego komentarza / odnośnika

Link do cytowanego pytania: Jak rozdzielić ciąg na ograniczniku wieloznakowym w bash?


1
Zobacz mój komentarz dotyczący podobnego, ale ulepszonego podejścia.
xebeche

3

Działa to dla mnie w OSX:

string="1 2 3 4 5"
declare -a array=($string)

Jeśli ciąg ma inny separator, po prostu 1. zastąp je spacją:

string="1,2,3,4,5"
delimiter=","
declare -a array=($(echo $string | tr "$delimiter" " "))

Prosty :-)


Działa zarówno dla Bash, jak i Zsh, co jest plusem!
Elijah W. Gagne

2

Innym sposobem na zrobienie tego bez modyfikacji IFS:

read -r -a myarray <<< "${string//, /$IFS}"

Zamiast zmieniać IFS tak, aby pasował do pożądanego separatora, możemy zastąpić wszystkie wystąpienia pożądanego separatora ", "zawartością $IFSvia "${string//, /$IFS}".

Może jednak będzie to bardzo powolne w przypadku bardzo dużych ciągów?

Jest to oparte na odpowiedzi Dennisa Williamsona.


2

Natrafiłem na ten post, gdy analizowałem dane wejściowe takie jak: słowo1, słowo2, ...

żadne z powyższych nie pomogło mi. rozwiązał to za pomocą awk. Jeśli to pomaga komuś:

STRING="value1,value2,value3"
array=`echo $STRING | awk -F ',' '{ s = $1; for (i = 2; i <= NF; i++) s = s "\n"$i; print s; }'`
for word in ${array}
do
        echo "This is the word $word"
done

1

Spróbuj tego

IFS=', '; array=(Paris, France, Europe)
for item in ${array[@]}; do echo $item; done

To proste. Jeśli chcesz, możesz również dodać deklarację (a także usunąć przecinki):

IFS=' ';declare -a array=(Paris France Europe)

IFS został dodany w celu cofnięcia powyższego, ale działa bez niego w nowej instancji bash


1

Możemy użyć polecenia tr, aby podzielić ciąg znaków na obiekt tablicy. Działa zarówno w systemie MacOS, jak i Linux

  #!/usr/bin/env bash
  currentVersion="1.0.0.140"
  arrayData=($(echo $currentVersion | tr "." "\n"))
  len=${#arrayData[@]}
  for (( i=0; i<=$((len-1)); i++ )); do 
       echo "index $i - value ${arrayData[$i]}"
  done

Inną opcją jest użycie polecenia IFS

IFS='.' read -ra arrayData <<< "$currentVersion"
#It is the same as tr
arrayData=($(echo $currentVersion | tr "." "\n"))

#Print the split string
for i in "${arrayData[@]}"
do
    echo $i
done

0

Użyj tego:

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

#${array[1]} == Paris
#${array[2]} == France
#${array[3]} == Europe

3
Źle: podlega podziałowi słów i rozwijaniu nazw ścieżek. Proszę nie ożywić starych pytań z dobrymi odpowiedziami, aby dać złe odpowiedzi.
gniourf_gniourf

2
To może być zła odpowiedź, ale nadal jest poprawna. Osoby zgłaszające / recenzenci: w przypadku niepoprawnych odpowiedzi, takich jak ta, głosuj, nie usuwaj!
Scott Weldon,

2
@gniourf_gniourf Czy możesz wyjaśnić, dlaczego jest to zła odpowiedź? Naprawdę nie rozumiem, kiedy zawodzi.
George Sovetov

3
@GeorgeSovetov: Jak powiedziałem, podlega podziałowi słów i rozszerzaniu nazw ścieżek. Bardziej ogólnie, rozszczepienie ciąg w szyku, jak array=( $string )to (niestety bardzo powszechne) antywzorzec projektowy: podział występuje słowo: string='Prague, Czech Republic, Europe'; Występuje rozwinięcie string='foo[abcd],bar[efgh]'nazwy ścieżki: zakończy się niepowodzeniem, jeśli masz plik o nazwie np. foodLub barfw katalogu. Jedynym prawidłowym zastosowaniem takiej konstrukcji jest stringglob.
gniourf_gniourf

0

AKTUALIZACJA: Nie rób tego z powodu problemów z eval.

Z nieco mniejszą ceremonią:

IFS=', ' eval 'array=($string)'

na przykład

string="foo, bar,baz"
IFS=', ' eval 'array=($string)'
echo ${array[1]} # -> bar

4
eval jest zły! nie rób tego.
caesarsol

1
Pfft. Nie. Jeśli piszesz skrypty wystarczająco duże, aby to miało znaczenie, robisz to źle. W kodzie aplikacji eval jest złem. W skryptach powłoki jest to powszechne, konieczne i nieistotne.
user1009908,

2
wstaw $zmienną, a zobaczysz ... Piszę wiele skryptów i nigdy nie musiałem używać jednegoeval
caesarsol,

2
Masz rację, jest to użyteczne tylko wtedy, gdy wiadomo, że dane wejściowe są czyste. Niezbyt solidne rozwiązanie.
user1009908,

Jedyny raz, kiedy musiałem użyć eval, to aplikacja, która sama wygenerowałaby własny kod / moduły ... I to nigdy nie miało żadnej formy wprowadzania danych przez użytkownika ...
Angry 84

0

Oto mój hack!

Dzielenie ciągów przez ciągi jest dość nudną rzeczą przy użyciu bash. Co się dzieje, mamy ograniczone podejścia, które działają tylko w kilku przypadkach (podzielone przez „;”, „/”, „.” Itd.) Lub mamy różne efekty uboczne w wynikach.

Poniższe podejście wymagało szeregu manewrów, ale wierzę, że zadziała w przypadku większości naszych potrzeb!

#!/bin/bash

# --------------------------------------
# SPLIT FUNCTION
# ----------------

F_SPLIT_R=()
f_split() {
    : 'It does a "split" into a given string and returns an array.

    Args:
        TARGET_P (str): Target string to "split".
        DELIMITER_P (Optional[str]): Delimiter used to "split". If not 
    informed the split will be done by spaces.

    Returns:
        F_SPLIT_R (array): Array with the provided string separated by the 
    informed delimiter.
    '

    F_SPLIT_R=()
    TARGET_P=$1
    DELIMITER_P=$2
    if [ -z "$DELIMITER_P" ] ; then
        DELIMITER_P=" "
    fi

    REMOVE_N=1
    if [ "$DELIMITER_P" == "\n" ] ; then
        REMOVE_N=0
    fi

    # NOTE: This was the only parameter that has been a problem so far! 
    # By Questor
    # [Ref.: https://unix.stackexchange.com/a/390732/61742]
    if [ "$DELIMITER_P" == "./" ] ; then
        DELIMITER_P="[.]/"
    fi

    if [ ${REMOVE_N} -eq 1 ] ; then

        # NOTE: Due to bash limitations we have some problems getting the 
        # output of a split by awk inside an array and so we need to use 
        # "line break" (\n) to succeed. Seen this, we remove the line breaks 
        # momentarily afterwards we reintegrate them. The problem is that if 
        # there is a line break in the "string" informed, this line break will 
        # be lost, that is, it is erroneously removed in the output! 
        # By Questor
        TARGET_P=$(awk 'BEGIN {RS="dn"} {gsub("\n", "3F2C417D448C46918289218B7337FCAF"); printf $0}' <<< "${TARGET_P}")

    fi

    # NOTE: The replace of "\n" by "3F2C417D448C46918289218B7337FCAF" results 
    # in more occurrences of "3F2C417D448C46918289218B7337FCAF" than the 
    # amount of "\n" that there was originally in the string (one more 
    # occurrence at the end of the string)! We can not explain the reason for 
    # this side effect. The line below corrects this problem! By Questor
    TARGET_P=${TARGET_P%????????????????????????????????}

    SPLIT_NOW=$(awk -F"$DELIMITER_P" '{for(i=1; i<=NF; i++){printf "%s\n", $i}}' <<< "${TARGET_P}")

    while IFS= read -r LINE_NOW ; do
        if [ ${REMOVE_N} -eq 1 ] ; then

            # NOTE: We use "'" to prevent blank lines with no other characters 
            # in the sequence being erroneously removed! We do not know the 
            # reason for this side effect! By Questor
            LN_NOW_WITH_N=$(awk 'BEGIN {RS="dn"} {gsub("3F2C417D448C46918289218B7337FCAF", "\n"); printf $0}' <<< "'${LINE_NOW}'")

            # NOTE: We use the commands below to revert the intervention made 
            # immediately above! By Questor
            LN_NOW_WITH_N=${LN_NOW_WITH_N%?}
            LN_NOW_WITH_N=${LN_NOW_WITH_N#?}

            F_SPLIT_R+=("$LN_NOW_WITH_N")
        else
            F_SPLIT_R+=("$LINE_NOW")
        fi
    done <<< "$SPLIT_NOW"
}

# --------------------------------------
# HOW TO USE
# ----------------

STRING_TO_SPLIT="
 * How do I list all databases and tables using psql?

\"
sudo -u postgres /usr/pgsql-9.4/bin/psql -c \"\l\"
sudo -u postgres /usr/pgsql-9.4/bin/psql <DB_NAME> -c \"\dt\"
\"

\"
\list or \l: list all databases
\dt: list all tables in the current database
\"

[Ref.: /dba/1285/how-do-i-list-all-databases-and-tables-using-psql]


"

f_split "$STRING_TO_SPLIT" "bin/psql -c"

# --------------------------------------
# OUTPUT AND TEST
# ----------------

ARR_LENGTH=${#F_SPLIT_R[*]}
for (( i=0; i<=$(( $ARR_LENGTH -1 )); i++ )) ; do
    echo " > -----------------------------------------"
    echo "${F_SPLIT_R[$i]}"
    echo " < -----------------------------------------"
done

if [ "$STRING_TO_SPLIT" == "${F_SPLIT_R[0]}bin/psql -c${F_SPLIT_R[1]}" ] ; then
    echo " > -----------------------------------------"
    echo "The strings are the same!"
    echo " < -----------------------------------------"
fi

0

Dla elementów wielowarstwowych, dlaczego nie coś takiego

$ array=($(echo -e $'a a\nb b' | tr ' ' '§')) && array=("${array[@]//§/ }") && echo "${array[@]/%/ INTERELEMENT}"

a a INTERELEMENT b b INTERELEMENT

-1

Innym sposobem byłoby:

string="Paris, France, Europe"
IFS=', ' arr=(${string})

Teraz twoje elementy są przechowywane w tablicy „arr”. Aby iterować po elementach:

for i in ${arr[@]}; do echo $i; done

1
Omawiam ten pomysł w mojej odpowiedzi ; patrz Błędna odpowiedź # 5 (możesz być szczególnie zainteresowany moją dyskusją na temat evalsztuczki). Twoje rozwiązanie pozostawia $IFSpo fakcie wartość przecinka.
bgoldst

-1

Ponieważ istnieje wiele sposobów rozwiązania tego problemu, zacznijmy od zdefiniowania tego, co chcemy zobaczyć w naszym rozwiązaniu.

  1. Bash zapewnia wbudowane readarraydo tego celu. Wykorzystajmy to.
  2. Unikaj brzydkich i niepotrzebnych sztuczek, takich jak zmiana IFS, zapętlenie, użycie evallub dodanie dodatkowego elementu, a następnie usunięcie go.
  3. Znajdź proste, czytelne podejście, które można łatwo dostosować do podobnych problemów.

readarrayKomenda jest najłatwiejszy w obsłudze z nowymi liniami jak ogranicznik. W przypadku innych ograniczników może dodać dodatkowy element do tablicy. Najczystsze podejście polega na tym, aby najpierw dostosować nasze dane wejściowe do formy, która działa dobrze readarrayprzed przekazaniem.

Dane wejściowe w tym przykładzie nie mają ogranicznika wieloznakowego. Jeśli zastosujemy trochę zdrowego rozsądku, najlepiej będzie to rozumieć jako wejście oddzielone przecinkami, dla których każdy element może wymagać przycięcia. Moim rozwiązaniem jest podzielenie danych wejściowych przecinkiem na wiele linii, przycięcie każdego elementu i przekazanie go wszystkim readarray.

string='  Paris,France  ,   All of Europe  '
readarray -t foo < <(tr ',' '\n' <<< "$string" |sed 's/^ *//' |sed 's/ *$//')
declare -p foo

# declare -a foo='([0]="Paris" [1]="France" [2]="All of Europe")'

-2

Innym podejściem może być:

str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

Po tym „arr” jest tablicą z czterema ciągami. Nie wymaga to zajmowania się IFS, czytaniem ani żadnymi innymi specjalnymi rzeczami, dlatego jest o wiele prostsze i bezpośrednie.


Taki sam (niestety często występujący) antypater, jak inne odpowiedzi: podlega podziałowi słów i rozszerzaniu nazw plików.
gniourf_gniourf
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.