Jak podzielić ciąg na ograniczniku w Bash?


2039

Mam ten ciąg przechowywany w zmiennej:

IN="bla@some.com;john@home.com"

Teraz chciałbym podzielić ciągi znaków według ;ogranicznika, aby:

ADDR1="bla@some.com"
ADDR2="john@home.com"

Niekoniecznie potrzebuję zmiennych ADDR1i ADDR2. Jeśli są to elementy tablicy, które są jeszcze lepsze.


Po sugestiach z poniższych odpowiedzi, skończyłem z następującymi, co było po:

#!/usr/bin/env bash

IN="bla@some.com;john@home.com"

mails=$(echo $IN | tr ";" "\n")

for addr in $mails
do
    echo "> [$addr]"
done

Wynik:

> [bla@some.com]
> [john@home.com]

Było rozwiązanie polegające na ustawieniu Internal_field_separator (IFS) na ;. Nie jestem pewien, co się stało z tą odpowiedzią, w jaki sposób przywracasz IFSustawienia domyślne?

RE: IFSrozwiązanie, próbowałem tego i działa, zachowuję stary, IFSa następnie przywracam:

IN="bla@some.com;john@home.com"

OIFS=$IFS
IFS=';'
mails2=$IN
for x in $mails2
do
    echo "> [$x]"
done

IFS=$OIFS

BTW, kiedy próbowałem

mails2=($IN)

Pierwszy ciąg mam tylko podczas drukowania w pętli, ale bez nawiasów wokół $INniego działa.


14
W odniesieniu do „Edit2”: Możesz po prostu „rozbroić IFS”, a on powróci do stanu domyślnego. Nie ma potrzeby jawnego zapisywania i przywracania, chyba że masz powód, aby oczekiwać, że została już ustawiona wartość inna niż domyślna. Co więcej, jeśli robisz to wewnątrz funkcji (a jeśli nie, to dlaczego?), Możesz ustawić IFS jako zmienną lokalną i powróci on do poprzedniej wartości po wyjściu z funkcji.
Brooks Moses

19
@BrooksMoses: (a) +1 za używanie, local IFS=...gdzie to możliwe; (b) -1 unset IFS, ponieważ nie resetuje to dokładnie IFS do wartości domyślnej, chociaż uważam, że nieuzbrojony IFS zachowuje się tak samo jak domyślna wartość IFS ($ '\ t \ n'), jednak wydaje się, że złym postępowaniem jest zakładaj na ślepo, że twój kod nigdy nie będzie wywoływany z IFS ustawionym na wartość niestandardową; (c) innym pomysłem jest wywołanie podpowłoki: (IFS=$custom; ...)po wyjściu podpowłoki IFS powróci do pierwotnego stanu.
dubiousjim

Chcę tylko rzucić okiem na ścieżki, aby zdecydować, gdzie rzucić plik wykonywalny, więc postanowiłem uciec ruby -e "puts ENV.fetch('PATH').split(':')". Jeśli chcesz pozostać czystym, bash nie pomoże, ale łatwiej jest używać dowolnego języka skryptowego z wbudowanym podziałem.
nicooga,

4
for x in $(IFS=';';echo $IN); do echo "> [$x]"; done
user2037659

2
Aby zapisać go jako tablicę, musiałem umieścić inny zestaw nawiasów i zmienić \ntylko na spację. Więc ostatnia linia jest mails=($(echo $IN | tr ";" " ")). Teraz mogę sprawdzić elementy mailsza pomocą notacji tablicowej mails[index]lub po prostu iteracji w pętli
afranques

Odpowiedzi:


1232

Możesz ustawić zmienną wewnętrznego separatora pól (IFS), a następnie pozwolić jej przeanalizować w tablicy. Kiedy dzieje się tak w poleceniu, wówczas przypisanie do IFSma miejsce tylko w środowisku (pojedynczym) tego polecenia read. Następnie analizuje dane wejściowe zgodnie z IFSwartością zmiennej w tablicy, którą możemy następnie iterować.

IFS=';' read -ra ADDR <<< "$IN"
for i in "${ADDR[@]}"; do
    # process "$i"
done

Spowoduje to przeanalizowanie jednego wiersza elementów oddzielonych przez ;, popychając go do tablicy. Rzeczy do przetwarzania w całości $IN, za każdym razem jeden wiersz danych wejściowych oddzielony ;:

 while IFS=';' read -ra ADDR; do
      for i in "${ADDR[@]}"; do
          # process "$i"
      done
 done <<< "$IN"

22
To chyba najlepszy sposób. Jak długo IFS będzie utrzymywał swoją aktualną wartość, czy może zepsuć mój kod, ustawiając go, kiedy nie powinien, i jak mogę go zresetować, gdy skończę z nim?
Chris Lutz

7
teraz po zastosowaniu poprawki, tylko w czasie trwania polecenia odczytu :)
Johannes Schaub - litb

14
Możesz przeczytać wszystko na raz bez użycia pętli while: czytaj -r -d '' -a addr <<< "$ in" # The -d jest kluczem tutaj, mówi czytaniu, aby nie zatrzymywał się na pierwszej nowej linii ( która jest domyślną wartością -d), ale kontynuacja do EOF lub bajtu NULL (które występują tylko w danych binarnych).
lhunath

55
@LucaBorrione Ustawienie IFSw tym samym wierszu, co readbez średnika lub innego separatora, w przeciwieństwie do oddzielnego polecenia, obejmuje go tym poleceniem - dlatego zawsze jest „przywracane”; nie musisz nic robić ręcznie.
Charles Duffy,

5
@imagineerThis Występuje błąd związany z linkami i lokalnymi zmianami w IFS, który wymaga podania $IN. Błąd został naprawiony w bash4.3.
chepner

971

Zaczerpnięty z podzielonej tablicy skryptów powłoki Bash :

IN="bla@some.com;john@home.com"
arrIN=(${IN//;/ })

Wyjaśnienie:

Konstrukcja ta zastępuje wszystkie wystąpienia ';'(początkowy //środków globalny zastąpić) w ciągu INz ' '(pojedyncza spacja), a następnie interpretuje ciąg oddzielonych znakiem spacji jako tablica (to co otaczające nawiasy zrobić).

Składnia zastosowana w nawiasach klamrowych w celu zastąpienia każdego ';'znaku ' 'znakiem nosi nazwę Rozszerzenie parametru .

Istnieje kilka typowych błędów:

  1. Jeśli oryginalny ciąg ma spacje, musisz użyć IFS :
    • IFS=':'; arrIN=($IN); unset IFS;
  2. Jeśli oryginalny ciąg ma spacje, a separator jest nowym wierszem, możesz ustawić IFS za pomocą:
    • IFS=$'\n'; arrIN=($IN); unset IFS;

84
Chcę tylko dodać: jest to najprostszy ze wszystkich, możesz uzyskać dostęp do elementów tablicy za pomocą $ {arrIN [1]} (oczywiście od zer)
Oz123

26
Znalazłem: technika modyfikowania zmiennej w $ {} jest znana jako „rozszerzenie parametru”.
KomodoDave

22
Nie, nie sądzę, żeby to zadziałało, gdy istnieją również spacje ... konwertuje „,” na „”, a następnie buduje tablicę oddzieloną spacjami.
Ethan,

12
Bardzo zwięzłe, ale istnieją ogólne zastrzeżenia : powłoka stosuje dzielenie wyrazów i rozwinięcia łańcucha, co może być niepożądane; po prostu spróbuj. IN="bla@some.com;john@home.com;*;broken apart". W skrócie: to podejście się przerwie, jeśli twoje tokeny zawierają osadzone spacje i / lub znaki. tak się *dzieje, że token pasuje do nazw plików w bieżącym folderze.
mklement0

53
Jest to złe podejście z innych powodów: Na przykład, jeśli Twój ciąg zawiera ;*;, to *zostanie rozwinięty do listy nazw plików w bieżącym katalogu. -1
Charles Duffy,

249

Jeśli nie masz nic przeciwko natychmiastowemu ich przetworzeniu, lubię to:

for i in $(echo $IN | tr ";" "\n")
do
  # process
done

Możesz użyć tego rodzaju pętli do zainicjowania tablicy, ale prawdopodobnie jest to łatwiejszy sposób. Mam nadzieję, że to pomaga.


Powinieneś był zachować odpowiedź IFS. Nauczyło mnie czegoś, o czym nie wiedziałem, i zdecydowanie stworzyło tablicę, a to po prostu tani zamiennik.
Chris Lutz

Widzę. Tak, robię te głupie eksperymenty, będę się uczyć nowych rzeczy za każdym razem, gdy próbuję na nie odpowiedzieć. Zredagowałem rzeczy na podstawie opinii IRC #bash i nie usunąłem :)
Johannes Schaub - litb

33
-1, oczywiście nie jesteś świadomy podziału słów, ponieważ wprowadza on dwa błędy w kodzie. jednym z nich jest, gdy nie podajesz $ IN, a drugim jest udawanie, że nowy wiersz jest jedynym ogranicznikiem używanym w dzieleniu słów. Iterujesz po każdym SŁOWIE w IN, nie w każdym wierszu, a ZDECYDOWANIE nie w każdym elemencie ograniczonym średnikiem, chociaż może się wydawać, że ma efekt uboczny, jakby wyglądał, jakby działał.
lhunath

3
Możesz zmienić to na echo „$ IN” | tr ';' „\ n” | podczas odczytu -r ADDY; wykonaj # process "$ ADDY"; zrobione, żeby miał szczęście, myślę :) Pamiętaj, że rozwinie się i nie możesz zmienić zewnętrznych zmiennych z pętli (dlatego użyłem składni <<< "$ IN"), a następnie
Johannes Schaub - litb

8
Podsumowując debatę w komentarzach: Ostrzeżenia do użytku ogólnego : powłoka stosuje dzielenie wyrazów i rozwinięcia łańcucha, co może być niepożądane; po prostu spróbuj. IN="bla@some.com;john@home.com;*;broken apart". W skrócie: to podejście się przerwie, jeśli twoje tokeny zawierają osadzone spacje i / lub znaki. tak się *dzieje, że token pasuje do nazw plików w bieżącym folderze.
mklement0

202

Kompatybilna odpowiedź

Można to zrobić na wiele różnych sposobów .

Jednak ważne jest, aby najpierw zauważyć, że bashma wiele specjalnych funkcji (tak zwanych baszizmów ), które nie będą działać w żadnej innej.

W szczególności tablice , tablice asocjacyjne i podstawianie wzorców , które są używane w rozwiązaniach w tym poście, a także inne w wątku, są bashizmami i mogą nie działać pod innymi powłokami, z których korzysta wiele osób.

Na przykład: w moim Debian GNU / Linux jest standardowa powłoka o nazwie; Znam wielu ludzi, którzy lubią używać innej powłoki o nazwie; i istnieje również specjalne narzędzie o nazwie z własnym tłumaczem powłoki ().

Żądany ciąg

Ciąg do podzielenia w powyższym pytaniu to:

IN="bla@some.com;john@home.com"

Użyję zmodyfikowanej wersji tego ciągu, aby upewnić się, że moje rozwiązanie jest odporne na ciągi zawierające białe znaki, które mogłyby uszkodzić inne rozwiązania:

IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"

Podziel ciąg na podstawie separatora w (wersja> = 4.2)

W czystej postaci bash możemy utworzyć tablicę z elementami podzielonymi przez tymczasową wartość dla IFS ( separator pola wejściowego ). IFS wskazuje między innymi, bashktóre znaki należy traktować jako ogranicznik między elementami podczas definiowania tablicy:

IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"

# save original IFS value so we can restore it later
oIFS="$IFS"
IFS=";"
declare -a fields=($IN)
IFS="$oIFS"
unset oIFS

W nowszych wersjach bash, poprzedzając polecenie z definicji zmienia IFS IFS dla tego polecenia tylko i resetuje go do poprzedniej wartości zaraz potem. Oznacza to, że możemy to zrobić w jednym wierszu:

IFS=\; read -a fields <<<"$IN"
# after this command, the IFS resets back to its previous value (here, the default):
set | grep ^IFS=
# IFS=$' \t\n'

Widzimy, że ciąg INzostał zapisany w tablicy o nazwie fieldspodzielonej na średniki:

set | grep ^fields=\\\|^IN=
# fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name <fulnam@other.org>")
# IN='bla@some.com;john@home.com;Full Name <fulnam@other.org>'

(Możemy również wyświetlić zawartość tych zmiennych za pomocą declare -p:)

declare -p IN fields
# declare -- IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
# declare -a fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name <fulnam@other.org>")

Zauważ, że readjest to najszybszy sposób wykonania podziału, ponieważ nie ma wywoływanych widelców ani zasobów zewnętrznych.

Po zdefiniowaniu tablicy możesz użyć prostej pętli do przetworzenia każdego pola (a raczej każdego elementu w tablicy, którą teraz zdefiniowałeś):

# `"${fields[@]}"` expands to return every element of `fields` array as a separate argument
for x in "${fields[@]}" ;do
    echo "> [$x]"
    done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

Lub możesz usunąć każde pole z tablicy po przetworzeniu przy użyciu metody przesunięcia , co lubię:

while [ "$fields" ] ;do
    echo "> [$fields]"
    # slice the array 
    fields=("${fields[@]:1}")
    done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

A jeśli chcesz po prostu wydrukować tablicę, nie musisz jej nawet zapętlać:

printf "> [%s]\n" "${fields[@]}"
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

Aktualizacja: ostatnia > = 4,4

W nowszych wersjach bashmożesz także grać za pomocą polecenia mapfile:

mapfile -td \; fields < <(printf "%s\0" "$IN")

Ta składnia chroni specjalne znaki, znaki nowej linii i puste pola!

Jeśli nie chcesz dołączać pustych pól, możesz wykonać następujące czynności:

mapfile -td \; fields <<<"$IN"
fields=("${fields[@]%$'\n'}")   # drop '\n' added by '<<<'

Za pomocą mapfilemożna również pominąć deklarowanie tablicy i niejawnie „zapętlić” ograniczane elementy, wywołując funkcję na każdym:

myPubliMail() {
    printf "Seq: %6d: Sending mail to '%s'..." $1 "$2"
    # mail -s "This is not a spam..." "$2" </path/to/body
    printf "\e[3D, done.\n"
}

mapfile < <(printf "%s\0" "$IN") -td \; -c 1 -C myPubliMail

(Uwaga: \0na końcu łańcucha formatu jest bezużyteczne, jeśli nie obchodzi Cię puste pole na końcu łańcucha lub nie ma ich).

mapfile < <(echo -n "$IN") -td \; -c 1 -C myPubliMail

# Seq:      0: Sending mail to 'bla@some.com', done.
# Seq:      1: Sending mail to 'john@home.com', done.
# Seq:      2: Sending mail to 'Full Name <fulnam@other.org>', done.

Lub możesz użyć <<<, a w treści funkcji włącz trochę przetwarzania, aby usunąć nowy wiersz, który dodaje:

myPubliMail() {
    local seq=$1 dest="${2%$'\n'}"
    printf "Seq: %6d: Sending mail to '%s'..." $seq "$dest"
    # mail -s "This is not a spam..." "$dest" </path/to/body
    printf "\e[3D, done.\n"
}

mapfile <<<"$IN" -td \; -c 1 -C myPubliMail

# Renders the same output:
# Seq:      0: Sending mail to 'bla@some.com', done.
# Seq:      1: Sending mail to 'john@home.com', done.
# Seq:      2: Sending mail to 'Full Name <fulnam@other.org>', done.

Podziel ciąg na podstawie separatora w

Jeśli nie możesz użyć bashlub chcesz napisać coś, co można wykorzystać w wielu różnych powłokach, często nie możesz użyć bashism - i obejmuje to tablice, których używaliśmy w powyższych rozwiązaniach.

Nie musimy jednak używać tablic do zapętlania „elementów” łańcucha. W wielu powłokach stosowana jest składnia do usuwania podciągów ciągu od pierwszego lub ostatniego wystąpienia wzorca. Pamiętaj, że *jest to symbol wieloznaczny oznaczający zero lub więcej znaków:

(Brak tego podejścia w jakimkolwiek opublikowanym rozwiązaniu jest głównym powodem, dla którego piszę tę odpowiedź;)

${var#*SubStr}  # drops substring from start of string up to first occurrence of `SubStr`
${var##*SubStr} # drops substring from start of string up to last occurrence of `SubStr`
${var%SubStr*}  # drops substring from last occurrence of `SubStr` to end of string
${var%%SubStr*} # drops substring from first occurrence of `SubStr` to end of string

Jak wyjaśniono w Score_Under :

#i %usuń możliwie najkrótsze pasujące podciąg odpowiednio z początku i końca łańcucha, oraz

##i %%usuń możliwie najdłuższy pasujący podciąg.

Korzystając z powyższej składni, możemy stworzyć podejście, w którym wyodrębniamy „elementy” podłańcucha z ciągu, usuwając podciągi do separatora lub po nim.

Poniższy blok kodu działa dobrze w (w tym Mac OS bash),, , i „s :

IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
while [ "$IN" ] ;do
    # extract the substring from start of string up to delimiter.
    # this is the first "element" of the string.
    iter=${IN%%;*}
    echo "> [$iter]"
    # if there's only one element left, set `IN` to an empty string.
    # this causes us to exit this `while` loop.
    # else, we delete the first "element" of the string from IN, and move onto the next.
    [ "$IN" = "$iter" ] && \
        IN='' || \
        IN="${IN#*;}"
  done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

Baw się dobrze!


15
Te #, ##, %i %%substytucje mają co IMO jest łatwiejszy do zapamiętania wyjaśnienie (na ile kasować) #i %usunąć najkrótszy ciąg pasujący, a ##i %%usunąć najdłużej to możliwe.
Score_Under

1
IFS=\; read -a fields <<<"$var"Nie działa na nowej linii i dodanie nowej linii spływu. Drugie rozwiązanie usuwa końcowe puste pole.
Izaak

Ogranicznik powłoki to najbardziej elegancka odpowiedź, kropka.
Eric Chen

Czy można użyć ostatniej alternatywy z listą separatorów pól ustawioną gdzie indziej? Na przykład zamierzam użyć tego jako skryptu powłoki i przekazać listę separatorów pól jako parametr pozycyjny.
sancho.s ReinstateMonicaCellio

Tak, w pętli:for sep in "#" "ł" "@" ; do ... var="${var#*$sep}" ...
F. Hauri

184

Widziałem kilka odpowiedzi dotyczących cutpolecenia, ale wszystkie zostały usunięte. To trochę dziwne, że nikt o tym nie rozwinął, ponieważ uważam, że jest to jedno z bardziej użytecznych poleceń do robienia tego typu rzeczy, szczególnie do analizowania plików dziennika z ogranicznikami.

W przypadku podzielenia tego konkretnego przykładu na tablicę skryptów bash, trjest prawdopodobnie bardziej wydajny, ale cutmożna go użyć i jest bardziej skuteczny, jeśli chcesz wyciągnąć określone pola ze środka.

Przykład:

$ echo "bla@some.com;john@home.com" | cut -d ";" -f 1
bla@some.com
$ echo "bla@some.com;john@home.com" | cut -d ";" -f 2
john@home.com

Możesz oczywiście umieścić to w pętli i iterować parametr -f, aby pobrać każde pole niezależnie.

Staje się to bardziej przydatne, gdy masz plik dziennika z ogranicznikami taki jak ten:

2015-04-27|12345|some action|an attribute|meta data

cutjest bardzo przydatne, aby móc catten plik i wybrać konkretne pole do dalszego przetwarzania.


6
Uznanie za korzystanie cut, to odpowiednie narzędzie do pracy! Znacznie wyczyszczone niż jakikolwiek z tych hakerskich pocisków.
MisterMiyagi,

4
To podejście zadziała tylko wtedy, gdy z góry znasz liczbę elementów; musisz zaprogramować trochę więcej logiki wokół tego. Obsługuje również zewnętrzne narzędzie dla każdego elementu.
uli42

Właśnie chciałem uniknąć pustego łańcucha w pliku CSV. Teraz mogę również wskazać dokładną wartość „kolumny”. Praca z IFS już używanym w pętli. Lepsze niż się spodziewałem w mojej sytuacji.
Louis Loudog Trottier,

Bardzo przydatny do
pobierania

Ta odpowiedź jest warta
przewinięcia w

124

To działało dla mnie:

string="1;2"
echo $string | cut -d';' -f1 # output is 1
echo $string | cut -d';' -f2 # output is 2

1
Chociaż działa tylko z ogranicznikiem pojedynczego znaku, właśnie tego szukał OP (rekordy rozdzielone średnikiem).
GuyPaddock,

Odpowiedzi udzielił około cztery lata temu @Ashok , a także ponad rok temu @DougW , niż Twoja odpowiedź, z jeszcze większą ilością informacji. Proszę zamieścić inne rozwiązanie niż inne ”.
MAChitgarha

90

Co powiesz na to podejście:

IN="bla@some.com;john@home.com" 
set -- "$IN" 
IFS=";"; declare -a Array=($*) 
echo "${Array[@]}" 
echo "${Array[0]}" 
echo "${Array[1]}" 

Źródło


7
+1 ... ale nie nazwałbym zmiennej „Array” ... zgaduję, że jest to zwierzę domowe. Dobre rozwiązanie.
Yzmir Ramirez

14
+1 ... ale „set” i deklaracja -a są niepotrzebne. Można równie dobrze wykorzystane tylkoIFS";" && Array=($IN)
ATA

+1 Tylko uwaga dodatkowa: czy nie warto zachować starego IFS, a następnie go przywrócić? (jak pokazuje stefanB w swoim edit3) ludzie lądujący tutaj (czasem po prostu kopiujący i wklejający rozwiązanie) mogą o tym nie myśleć
Luca Borrione

6
-1: Po pierwsze, @ata ma rację, że większość poleceń w tym trybie nic nie robi. Po drugie, używa dzielenia słów, aby utworzyć tablicę, i nie robi nic, aby zahamować ekspansję globu (więc jeśli masz znaki globu w jednym z elementów tablicy, elementy te zostaną zastąpione pasującymi nazwami plików).
Charles Duffy,

1
Sugeruję użycie $'...': IN=$'bla@some.com;john@home.com;bet <d@\ns* kl.com>'. Następnie echo "${Array[2]}"wydrukuje ciąg z nową linią. set -- "$IN"jest również konieczne w tym przypadku. Tak, aby zapobiec globalnej ekspansji, rozwiązanie powinno obejmować set -f.
John_West

79

Myślę, że AWK to najlepsze i skuteczne polecenie do rozwiązania twojego problemu. AWK jest domyślnie dołączany do prawie każdej dystrybucji Linuksa.

echo "bla@some.com;john@home.com" | awk -F';' '{print $1,$2}'

da

bla@some.com john@home.com

Oczywiście możesz zapisać każdy adres e-mail, zmieniając pole drukowania awk.


3
Lub jeszcze prościej: echo „bla@some.com; jan@home.com” | awk 'BEGIN {RS = ";"} {print}'
Jaro

@Jaro To działało idealnie dla mnie, gdy miałem ciąg z przecinkami i potrzebowałem sformatować go w linie. Dzięki.
Aquarelle

W tym scenariuszu zadziałało -> „echo” $ SPLIT_0 "| awk -F 'inode =' '{print 1 $}! Miałem problemy przy próbie użycia atrings („inode =”) zamiast znaków („;”). 1 $, 2 $, 3 $, 4 $ są ustawione jako pozycje w tablicy! Jeśli istnieje sposób na ustawienie tablicy ... lepiej! Dzięki!
Eduardo Lucio,

@EduardoLucio, myślę o tym, że może najpierw możesz zamienić separator inode=na, ;na przykład sed -i 's/inode\=/\;/g' your_file_to_process, a następnie zdefiniować, -F';'kiedy zastosujesz awk, mam nadzieję, że może ci to pomóc.
Tong,

66
echo "bla@some.com;john@home.com" | sed -e 's/;/\n/g'
bla@some.com
john@home.com

4
-1 co jeśli ciąg zawiera spacje? na przykład IN="this is first line; this is second line" arrIN=( $( echo "$IN" | sed -e 's/;/\n/g' ) )wytworzy tablicę 8 elementów w tym przypadku (element dla każdej oddzielonej przestrzeni słów), a nie 2 (element dla każdej linii oddzielonej średnikiem dwukropka)
Luca Borrione

3
@Luca Nie skrypt sed tworzy dokładnie dwie linie. To, co tworzy dla ciebie wiele wpisów, to umieszczenie go w tablicy bash (która domyślnie dzieli się na białe
znaki

Właśnie o to chodzi: OP musi przechowywać wpisy w tablicy, aby się nad nią zapętlić, jak widać w jego edycji. Myślę, że brakuje twojej (dobrej) odpowiedzi, aby wspomnieć o tym, aby użyć tego arrIN=( $( echo "$IN" | sed -e 's/;/\n/g' ) )do osiągnięcia, i porady, aby zmienić IFS IFS=$'\n'na tych, którzy wylądują tutaj w przyszłości i muszą podzielić ciąg zawierający spacje. (i przywrócić go później). :)
Luca Borrione,

1
@Luca Dobry punkt. Jednak przypisanie tablicy nie było w początkowym pytaniu, kiedy napisałem tę odpowiedź.
lothar

65

Działa to również:

IN="bla@some.com;john@home.com"
echo ADD1=`echo $IN | cut -d \; -f 1`
echo ADD2=`echo $IN | cut -d \; -f 2`

Uważaj, to rozwiązanie nie zawsze jest poprawne. Jeśli przekażesz tylko „bla@some.com”, przypisze to zarówno do ADD1, jak i ADD2.


1
Możesz użyć opcji -s, aby uniknąć wspomnianego problemu: superuser.com/questions/896800/… "-f, --fields = LISTA zaznacz tylko te pola; wydrukuj również dowolny wiersz, który nie zawiera znaku ogranicznika, chyba że opcja -s jest podano”
fersarr

34

Inne podejście do odpowiedzi Darrona , oto jak to robię:

IN="bla@some.com;john@home.com"
read ADDR1 ADDR2 <<<$(IFS=";"; echo $IN)

Myślę, że tak! Uruchom powyższe polecenia, a następnie „echo $ ADDR1 ... $ ADDR2” i otrzymam wynik „bla@some.com ... jan@home.com”
nickjb

1
Działa to NAPRAWDĘ dobrze dla mnie ... Użyłem go do iteracji po tablicy ciągów zawierających dane DB, SERWER, PORT oddzielone przecinkami, aby użyć mysqldump.
Nick

5
Diagnoza: IFS=";"przypisanie istnieje tylko w $(...; echo $IN)podpowłoce; dlatego niektórzy czytelnicy (w tym ja) początkowo myślą, że to nie zadziała. Założyłem, że wszystkie dolary $ IN zostały zalane przez ADDR1. Ale nickjb ma rację; to działa. Powodem jest to, że echo $INpolecenie analizuje argumenty przy użyciu bieżącej wartości $ IFS, ale następnie powtarza je na standardowe wyjście przy użyciu separatora spacji, niezależnie od ustawienia $ IFS. Zatem efekt netto jest taki, jakby ktoś go wywołał read ADDR1 ADDR2 <<< "bla@some.com john@home.com"(zwróć uwagę, że dane wejściowe nie są oddzielone spacją; -separated).
dubiousjim

1
To nie działa na spacjami i znakami nowej linii, a także poszerzyć symboli wieloznacznych *w echo $INz nienotowanego ekspansji zmiennej.
Izaak

Naprawdę podoba mi się to rozwiązanie. Opis, dlaczego działa, byłby bardzo przydatny i sprawiłby, że jest to lepsza ogólna odpowiedź.
Michael Gaskill

32

W Bash: kuloodporny sposób, który zadziała, nawet jeśli twoja zmienna zawiera nowe linie:

IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")

Popatrz:

$ in=$'one;two three;*;there is\na newline\nin this field'
$ IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")
$ declare -p array
declare -a array='([0]="one" [1]="two three" [2]="*" [3]="there is
a newline
in this field")'

Sztuczka, aby to zadziałało, polega na użyciu -dopcji read(separatora) z pustym separatorem, aby readzmuszony był odczytać wszystko, co jest zasilane. I karmimy readsię dokładnie zawartością zmiennej in, dzięki czemu nie ma końca nowej linii printf. Pamiętaj, że umieszczamy również ogranicznik, printfaby upewnić się, że przekazywany ciąg readma ogranicznik końcowy. Bez niego readprzycinałoby potencjalne końcowe pola puste:

$ in='one;two;three;'    # there's an empty field
$ IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")
$ declare -p array
declare -a array='([0]="one" [1]="two" [2]="three" [3]="")'

końcowe puste pole zostaje zachowane.


Aktualizacja dla Bash ≥4.4

Od wersji Bash 4.4 wbudowane mapfile(aka readarray) obsługuje -dopcję określania separatora. Stąd innym kanonicznym sposobem jest:

mapfile -d ';' -t array < <(printf '%s;' "$in")

5
Znalazłem to jako rzadkie rozwiązanie na tej liście, które działa poprawnie z \nspacjami i *jednocześnie. Również brak pętli; zmienna tablicowa jest dostępna w powłoce po wykonaniu (w przeciwieństwie do najwyższej pozytywnej odpowiedzi). Uwaga: in=$'...'nie działa z podwójnymi cudzysłowami. Myślę, że potrzebuje więcej pozytywnych opinii.
John_West

28

Co powiesz na ten jeden liniowiec, jeśli nie używasz tablic:

IFS=';' read ADDR1 ADDR2 <<<$IN

Rozważ użycie, read -r ...aby na przykład upewnić się, że dwa znaki „\ t” na wejściu kończą się tymi samymi dwoma znakami w zmiennych (zamiast pojedynczego znaku tabulacji).
dubiousjim

-1 To nie działa tutaj (ubuntu 12.04). Dodanie echo "ADDR1 $ADDR1"\n echo "ADDR2 $ADDR2"do fragmentu spowoduje wyjście ADDR1 bla@some.com john@home.com\nADDR2(\ n to nowy wiersz)
Luca Borrione

Jest to prawdopodobnie spowodowane błędem obejmującym IFSi tutaj ciągi, które zostało naprawione w bash4.3. Cytowanie $INpowinno to naprawić. (Teoretycznie $INnie podlega podziałowi ani globowaniu po rozwinięciu, co oznacza, że ​​cytaty powinny być niepotrzebne. Jednak nawet w wersji 4.3 pozostała przynajmniej jedna usterka - zgłoszona i zaplanowana do usunięcia - więc cytowanie pozostaje dobre pomysł.)
chepner

Dzieje się tak, jeśli $ in zawiera znaki nowej linii, nawet jeśli podano $ IN. I dodaje końcowy znak nowej linii.
Izaak

Problem z tym i wiele innych rozwiązań polega również na tym, że zakłada, że ​​w $ IN są DOKŁADNIE DWA elementy - LUB, że chcesz, aby drugi i kolejne elementy zostały zmiażdżone w ADDR2. Rozumiem, że to odpowiada pytaniu, ale to bomba zegarowa.
Steven the Easy Amused

21

Bez ustawiania IFS

Jeśli masz tylko jeden dwukropek, możesz to zrobić:

a="foo:bar"
b=${a%:*}
c=${a##*:}

dostaniesz:

b = foo
c = bar

20

Oto czysty 3-liniowy:

in="foo@bar;bizz@buzz;fizz@buzz;buzz@woof"
IFS=';' list=($in)
for item in "${list[@]}"; do echo $item; done

gdzie IFSrozgraniczaj słowa na podstawie separatora i ()służy do tworzenia tablicy . Następnie [@]służy do zwracania każdego elementu jako osobnego słowa.

Jeśli masz później kod, musisz również przywrócić $IFS, np unset IFS.


5
Zastosowanie bez $incudzysłowu pozwala na rozszerzenie symboli wieloznacznych.
Izaak,

10

Następująca funkcja Bash / zsh dzieli swój pierwszy argument na separator podany przez drugi argument:

split() {
    local string="$1"
    local delimiter="$2"
    if [ -n "$string" ]; then
        local part
        while read -d "$delimiter" part; do
            echo $part
        done <<< "$string"
        echo $part
    fi
}

Na przykład polecenie

$ split 'a;b;c' ';'

daje

a
b
c

Dane wyjściowe mogą być na przykład przesyłane potokowo do innych poleceń. Przykład:

$ split 'a;b;c' ';' | cat -n
1   a
2   b
3   c

W porównaniu z innymi podanymi rozwiązaniami, to ma następujące zalety:

  • IFSnie jest zastępowane: Z powodu dynamicznego określania zasięgu nawet zmiennych lokalnych, zastąpienie IFSw pętli powoduje wyciek nowej wartości do wywołań funkcji wykonywanych z poziomu pętli.

  • Tablice nie są używane: wczytywanie ciągu do tablicy przy użyciu readwymaga flagi -aw Bash i -Aw zsh.

W razie potrzeby funkcję można umieścić w skrypcie w następujący sposób:

#!/usr/bin/env bash

split() {
    # ...
}

split "$@"

Nie działa z ogranicznikami dłuższymi niż 1 znak: split = $ (split „$ content” „file: //”)
madprops

Prawda - od help read:-d delim continue until the first character of DELIM is read, rather than newline
Halle Knast

8

możesz zastosować awk w wielu sytuacjach

echo "bla@some.com;john@home.com"|awk -F';' '{printf "%s\n%s\n", $1, $2}'

możesz także tego użyć

echo "bla@some.com;john@home.com"|awk -F';' '{print $1,$2}' OFS="\n"

7

Istnieje prosty i sprytny sposób:

echo "add:sfff" | xargs -d: -i  echo {}

Ale musisz użyć gnu xargs, BSD xargs nie może obsługiwać -d delim. Jeśli używasz Apple Mac jak ja. Możesz zainstalować gnu xargs:

brew install findutils

następnie

echo "add:sfff" | gxargs -d: -i  echo {}

4

To najprostszy sposób na zrobienie tego.

spo='one;two;three'
OIFS=$IFS
IFS=';'
spo_array=($spo)
IFS=$OIFS
echo ${spo_array[*]}

4

Jest tu kilka fajnych odpowiedzi (errator esp.), Ale dla czegoś analogicznego do podzielenia się na inne języki - to właśnie rozumiałem pierwotne pytanie - postanowiłem:

IN="bla@some.com;john@home.com"
declare -a a="(${IN/;/ })";

Teraz ${a[0]}, ${a[1]}itp, to jak można by oczekiwać. Użyj ${#a[*]}dla wielu terminów. Lub w celu iteracji, oczywiście:

for i in ${a[*]}; do echo $i; done

WAŻNA UWAGA:

Działa to w przypadkach, gdy nie ma się o co martwić, co rozwiązało mój problem, ale może nie rozwiązać twojego. $IFSW takim przypadku skorzystaj z rozwiązania (rozwiązań).


Nie działa, jeśli INzawiera więcej niż dwa adresy e-mail. Proszę odnieść się do tego samego pomysłu (ale naprawionego) w odpowiedzi palindromu
olibre

Lepsze użycie ${IN//;/ }(podwójny ukośnik), aby działało również z więcej niż dwiema wartościami. Uwaga: wszelkie symbole wieloznaczne ( *?[) zostaną rozwinięte. I końcowe puste pole zostanie odrzucone.
Izaak

3
IN="bla@some.com;john@home.com"
IFS=';'
read -a IN_arr <<< "${IN}"
for entry in "${IN_arr[@]}"
do
    echo $entry
done

Wynik

bla@some.com
john@home.com

System: Ubuntu 12.04.1


IFS nie ustawia się w konkretnym kontekście readtutaj i dlatego może zdenerwować resztę kodu, jeśli taki istnieje.
codeforester

2

Jeśli nie ma miejsca, dlaczego nie to?

IN="bla@some.com;john@home.com"
arr=(`echo $IN | tr ';' ' '`)

echo ${arr[0]}
echo ${arr[1]}

2

Użyj setwbudowanego, aby załadować $@tablicę:

IN="bla@some.com;john@home.com"
IFS=';'; set $IN; IFS=$' \t\n'

Następnie zacznij przyjęcie:

echo $#
for a; do echo $a; done
ADDR1=$1 ADDR2=$2

Lepsze użycie, set -- $INaby uniknąć problemów z „$ IN” zaczynających się od myślnika. Mimo to niecytowane rozwinięcie $INspowoduje rozwinięcie symboli wieloznacznych ( *?[).
Izaak

2

Dwie alternatywne opcje, w których żadna nie wymaga tablic bash:

Przypadek 1 : Zachowaj prostotę i prostotę: użyj NewLine jako separatora rekordów ... np.

IN="bla@some.com
john@home.com"

while read i; do
  # process "$i" ... eg.
    echo "[email:$i]"
done <<< "$IN"

Uwaga: w tym pierwszym przypadku żaden podproces nie jest rozwidlany, aby pomóc w manipulowaniu listami.

Pomysł: Być może warto intensywnie stosować NL wewnętrznie , a konwersję do innego RS można generować tylko zewnętrznie .

Przypadek 2 : Używanie „;” jako separator rekordów ... np.

NL="
" IRS=";" ORS=";"

conv_IRS() {
  exec tr "$1" "$NL"
}

conv_ORS() {
  exec tr "$NL" "$1"
}

IN="bla@some.com;john@home.com"
IN="$(conv_IRS ";" <<< "$IN")"

while read i; do
  # process "$i" ... eg.
    echo -n "[email:$i]$ORS"
done <<< "$IN"

W obu przypadkach można utworzyć podlistę w pętli, która jest trwała po zakończeniu pętli. Jest to przydatne podczas manipulowania listami w pamięci zamiast przechowywania list w plikach. {ps zachowaj spokój i kontynuuj B-)}


2

Oprócz fantastycznych odpowiedzi, które już zostały udzielone, jeśli chodzi tylko o wydrukowanie danych, które możesz rozważyć przy użyciu awk:

awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "$IN"

Spowoduje to ustawienie separatora pól na ;, aby mógł on zapętlać pola za pomocą forpętli i odpowiednio drukować.

Test

$ IN="bla@some.com;john@home.com"
$ awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "$IN"
> [bla@some.com]
> [john@home.com]

Z innym wejściem:

$ awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "a;b;c   d;e_;f"
> [a]
> [b]
> [c   d]
> [e_]
> [f]

2

W powłoce Androida większość proponowanych metod po prostu nie działa:

$ IFS=':' read -ra ADDR <<<"$PATH"                             
/system/bin/sh: can't create temporary file /sqlite_stmt_journals/mksh.EbNoR10629: No such file or directory

Co działa to:

$ for i in ${PATH//:/ }; do echo $i; done
/sbin
/vendor/bin
/system/sbin
/system/bin
/system/xbin

gdzie //oznacza globalną wymianę.


1
Nie działa, jeśli jakakolwiek część zmiennej $ PATH zawiera spacje (lub znaki nowej linii). Rozwija także symbole wieloznaczne (gwiazdka *, znak zapytania? I nawiasy klamrowe […]).
Izaak

2
IN='bla@some.com;john@home.com;Charlie Brown <cbrown@acme.com;!"#$%&/()[]{}*? are no problem;simple is beautiful :-)'
set -f
oldifs="$IFS"
IFS=';'; arrayIN=($IN)
IFS="$oldifs"
for i in "${arrayIN[@]}"; do
echo "$i"
done
set +f

Wynik:

bla@some.com
john@home.com
Charlie Brown <cbrown@acme.com
!"#$%&/()[]{}*? are no problem
simple is beautiful :-)

Objaśnienie: Proste przypisanie za pomocą nawiasu () przekształca listę oddzieloną średnikami w tablicę, pod warunkiem, że podczas wykonywania tej operacji masz poprawny IFS. Standardowa pętla FOR obsługuje poszczególne elementy w tej tablicy jak zwykle. Zauważ, że lista podana dla zmiennej IN musi być „twarda”, tzn. Z pojedynczymi tikami.

IFS musi zostać zapisany i przywrócony, ponieważ Bash nie traktuje przypisania w taki sam sposób jak polecenia. Alternatywnym obejściem jest zawinięcie przypisania do funkcji i wywołanie tej funkcji za pomocą zmodyfikowanego IFS. W takim przypadku oddzielne zapisywanie / przywracanie IFS nie jest potrzebne. Dzięki za „Bize” za zwrócenie na to uwagi.


!"#$%&/()[]{}*? are no problemcóż ... niezupełnie: []*?są postaciami globalnymi. A co z utworzeniem tego katalogu i pliku: `mkdir '!" # $% &'; Touch '! "# $% & / () [] {} Dostałeś hahahaha - nie ma problemu' i uruchomiłeś polecenie? proste może być piękne, ale kiedy jest zepsute, jest zepsute.
gniourf_gniourf

@gniourf_gniourf Ciąg jest przechowywany w zmiennej. Zobacz oryginalne pytanie.
ajaaskel

1
@ajaaskel, nie rozumiesz w pełni mojego komentarza. Idź w katalogu zarysowania i wydać następujące polecenia: mkdir '!"#$%&'; touch '!"#$%&/()[]{} got you hahahaha - are no problem'. Muszą przyznać, że stworzą tylko katalog i plik o dziwnie wyglądających nazwach. Następnie uruchom komendy z dokładnym INdaliście: IN='bla@some.com;john@home.com;Charlie Brown <cbrown@acme.com;!"#$%&/()[]{}*? are no problem;simple is beautiful :-)'. Zobaczysz, że nie uzyskasz oczekiwanych wyników. Ponieważ używasz metody podlegającej rozszerzeniom nazw ścieżek, aby podzielić ciąg.
gniourf_gniourf

Ma to na celu wykazanie, że znaki *, ?, [...]a nawet, jeśli extglobjest ustawiona, !(...), @(...), ?(...), +(...) problemy z tej metody!
gniourf_gniourf

1
@gniourf_gniourf Dziękujemy za szczegółowe komentarze na temat globowania. Dostosowałem kod, aby wyłączyć globowanie. Chodziło mi jednak o to, aby pokazać, że dość proste zadanie może wykonać zadanie dzielenia.
ajaaskel

1

Okej chłopaki!

Oto moja odpowiedź!

DELIMITER_VAL='='

read -d '' F_ABOUT_DISTRO_R <<"EOF"
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION="Ubuntu 14.04.4 LTS"
NAME="Ubuntu"
VERSION="14.04.4 LTS, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 14.04.4 LTS"
VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
EOF

SPLIT_NOW=$(awk -F$DELIMITER_VAL '{for(i=1;i<=NF;i++){printf "%s\n", $i}}' <<<"${F_ABOUT_DISTRO_R}")
while read -r line; do
   SPLIT+=("$line")
done <<< "$SPLIT_NOW"
for i in "${SPLIT[@]}"; do
    echo "$i"
done

Dlaczego takie podejście jest dla mnie „najlepsze”?

Z dwóch powodów:

  1. Nie musisz uciekać z ogranicznika;
  2. Nie będziesz mieć problemu z pustymi spacjami . Wartość zostanie poprawnie rozdzielona w tablicy!

[]


FYI /etc/os-releasei /etc/lsb-releasemają być pozyskiwane, a nie analizowane. Więc twoja metoda jest naprawdę zła. Co więcej, nie do końca odpowiadasz na pytanie o spilt string w ograniczniku.
gniourf_gniourf

0

Jednowarstwowy do dzielenia łańcucha rozdzielonego znakiem „;” w tablicę jest:

IN="bla@some.com;john@home.com"
ADDRS=( $(IFS=";" echo "$IN") )
echo ${ADDRS[0]}
echo ${ADDRS[1]}

To tylko ustawia IFS w podpowłoce, więc nie musisz się martwić o zapisywanie i przywracanie jego wartości.


-1 to nie działa tutaj (ubuntu 12.04). wypisuje tylko pierwsze echo ze wszystkimi wartościami $ IN, podczas gdy drugie jest puste. możesz to zobaczyć, jeśli wstawisz echo "0:" $ {ADDRS [0]} \ n echo "1:" $ {ADDRS [1]} wyjście to 0: bla@some.com;john@home.com\n 1:(\ n to nowa linia)
Luca Borrione

1
zapoznaj się z odpowiedzią nickjb na działającą alternatywą dla tego pomysłu stackoverflow.com/a/6583589/1032370
Luca Borrione

1
-1, 1. IFS nie jest ustawiony w tej podpowłoce (jest przekazywany do środowiska „echo”, które jest wbudowane, więc i tak nic się nie dzieje). 2. $INjest cytowany, więc nie podlega podziałowi IFS. 3. Podstawienie procesu jest podzielone na białe znaki, ale może to uszkodzić oryginalne dane.
Score_Under

0

Może nie jest to najbardziej eleganckie rozwiązanie, ale działa z *i spacjami:

IN="bla@so me.com;*;john@home.com"
for i in `delims=${IN//[^;]}; seq 1 $((${#delims} + 1))`
do
   echo "> [`echo $IN | cut -d';' -f$i`]"
done

Wyjścia

> [bla@so me.com]
> [*]
> [john@home.com]

Inny przykład (ograniczniki na początku i na końcu):

IN=";bla@so me.com;*;john@home.com;"
> []
> [bla@so me.com]
> [*]
> [john@home.com]
> []

Zasadniczo usuwa każdą postać inną niż ;tworzenie delimsnp. ;;;. Następnie wykonuje forpętlę od 1do, number-of-delimiterszgodnie z obliczeniami ${#delims}. Ostatnim krokiem jest bezpieczne zdobycie $itej części cut.

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.