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ść $IFS
zmiennej 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 read
oddzielają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 $IFS
jest 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 $IFS
pola 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 $IFS
wraz 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ść $string
zmiennej zawiera jakieś LF, to read
będzie zatrzymać przetwarzanie, gdy napotka pierwszy LF. read
Wbudowane przetwarza tylko jedną linię za wezwaniem. Jest to prawdą, nawet jeśli przesyłasz dane wejściowe lub przekierowujesz tylko do read
instrukcji, 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 read
wbudowane 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 read
wbudowane 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 read
wbudowanej funkcji nie jest właściwe i należy tego unikać.
3: Nieoczywistym potencjalnym problemem związanym z tym rozwiązaniem jest to, że read
zawsze 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 read
w ogólnym dzieleniu słów również używana jest $IFS
specjalna 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ć $IFS
znaki, 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 extglob
jest 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 -f
wcześ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 ( tr
lub sed
), ponieważ interpretacja parametrów jest wyłącznie operacją wewnętrzną powłoki. (Ponadto w przypadku rozwiązań tr
i sed
zmienna wejściowa powinna być podwójnie cytowana w podstawieniu polecenia; w przeciwnym razie podział słowa zadziałałby w echo
poleceniu 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 -f
a 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, $IFS
aby 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 -f
i 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 -f
i 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ę $IFS
na 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 eval
wywoł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 eval
tego 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 $IFS
tylko tymczasowo, co pozwala nam uniknąć całego zapisu i przywracania gambitu takiego jak ten, który jest wykonywany ze $OIFS
zmienną 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ć $IFS
przypisanie tymczasowym. Możesz pomyśleć, dlaczego nie po prostu dodać do polecenia słowa zakazu, : builtin
aby $IFS
tymczasowo przypisać to zadanie? To nie działa, ponieważ spowodowałoby to również $array
tymczasowe 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 eval
uruchamia swój kod, uruchamia go w środowisku powłoki, tak jakby to był normalny, statyczny kod źródłowy, i dlatego możemy uruchomić $array
przypisanie w eval
argumencie, aby zadziałało w środowisku powłoki, podczas gdy $IFS
przypisanie prefiksu to jest poprzedzony eval
poleceniem, nie przeżyje eval
polecenia. 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 $IFS
zmienna 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 $IFS
specjalna 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 read
wbudowane.
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 $IFS
zmiennej, 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 compatxx
ustawienia 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 read
jest to niewłaściwe, ponieważ wykonuje dwa poziomy podziału, gdy tylko potrzebujemy jednego? Sztuczka polega na tym, że możesz wywoływać read
w 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 $IFS
jest 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 read
jest przekazywanie zerowych argumentów NAME . W takim przypadku read
zapisze całą linię wejściową otrzymaną ze strumienia wejściowego w zmiennej o nazwie $REPLY
i, jako bonus, nie będzieusuń początkowe i końcowe białe spacje z wartości. Jest to bardzo solidne użycie, z read
któ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 -d
opcji, 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 read
zwraca 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 -d
opcji, 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) read
zwróciły błąd, a (2) $REPLY
jest pusty, co oznacza, że read
nie 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).
readarray
Wbudowane, 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 -O
nie 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 read
podczas parsowania pola, readarray
upuszcza 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 -d
opcję:
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 read
rozwią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 readarray
zachowano 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 callback
opcji 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")
,
(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/...