Come divido una stringa su un delimitatore in Bash?


2043

Ho questa stringa memorizzata in una variabile:

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

Ora vorrei dividere le stringhe per ;delimitatore in modo da avere:

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

Non ho necessariamente bisogno delle variabili ADDR1e ADDR2. Se sono elementi di un array, è ancora meglio.


Dopo i suggerimenti delle risposte seguenti, ho finito con il seguente che è quello che stavo cercando:

#!/usr/bin/env bash

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

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

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

Produzione:

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

C'era una soluzione che implicava l'impostazione di Internal_field_separator (IFS) su ;. Non sono sicuro di cosa sia successo con quella risposta, come si ripristina IFSl'impostazione predefinita?

RE: IFSsoluzione, ho provato questo e funziona, mantengo il vecchio IFSe poi lo ripristino:

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

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

IFS=$OIFS

A proposito, quando ho provato

mails2=($IN)

Ho ottenuto la prima stringa solo quando la stampa in loop, senza parentesi attorno $INfunziona.


14
Per quanto riguarda il tuo "Modifica2": puoi semplicemente "deselezionare IFS" e tornerà allo stato predefinito. Non è necessario salvarlo e ripristinarlo esplicitamente a meno che non si abbia qualche motivo per aspettarsi che sia già stato impostato su un valore non predefinito. Inoltre, se lo stai facendo all'interno di una funzione (e, in caso contrario, perché no?), Puoi impostare IFS come variabile locale e tornerà al suo valore precedente una volta chiusa la funzione.
Brooks Moses,

19
@BrooksMoses: (a) +1 per l'utilizzo local IFS=...ove possibile; (b) -1 per unset IFS, questo non ripristina esattamente IFS al suo valore predefinito, anche se credo che un IFS non impostato si comporti come il valore predefinito di IFS ($ '\ t \ n'), tuttavia sembra una cattiva pratica supponendo ciecamente che il tuo codice non verrà mai invocato con IFS impostato su un valore personalizzato; (c) un'altra idea è quella di invocare una subshell: (IFS=$custom; ...)quando la subshell esce, IFS tornerà a quello che era originariamente.
dubiousjim

Voglio solo dare una rapida occhiata ai percorsi per decidere dove lanciare un eseguibile, quindi ho deciso di correre ruby -e "puts ENV.fetch('PATH').split(':')". Se vuoi rimanere puro bash non ti aiuterà, ma è più facile usare qualsiasi linguaggio di script che abbia una divisione integrata.
nicooga,

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

2
Al fine di salvarlo come un array ho dovuto posizionare un altro set di parentesi e cambiare il \nper solo uno spazio. Quindi la linea finale è mails=($(echo $IN | tr ";" " ")). Quindi ora posso controllare gli elementi mailsusando la notazione dell'array mails[index]o semplicemente ripetendo in un ciclo
afranques

Risposte:


1236

È possibile impostare la variabile IFS ( Internal Field Separator ) e quindi lasciarla analizzare in un array. Quando ciò accade in un comando, l'assegnazione IFSavviene solo nell'ambiente di quel singolo comando (a read). Quindi analizza l'input in base al IFSvalore della variabile in un array, che possiamo quindi ripetere.

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

Analizzerà una riga di elementi separati da ;, spingendola in un array. Materiale per l'elaborazione totale $IN, ogni volta che una riga di input è separata da ;:

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

22
Questo è probabilmente il modo migliore. Per quanto tempo persisterà IFS nel suo valore attuale, può rovinare il mio codice impostandolo quando non dovrebbe essere e come posso resettarlo quando ho finito?
Chris Lutz,

7
ora dopo la correzione applicata, solo entro la durata del comando di lettura :)
Johannes Schaub - litb

14
Puoi leggere tutto in una volta senza usare un ciclo while: leggi -r -d '' -a addr <<< "$ in" # La -d '' è la chiave qui, dice a read di non fermarsi alla prima riga ( che è il predefinito -d) ma per continuare fino a EOF o un byte NULL (che si verifica solo nei dati binari).
lhunath,

55
@LucaBorrione L'impostazione IFSsulla stessa riga del readsenza punto e virgola o di un altro separatore, anziché in un comando separato, lo sposta in quel comando - quindi viene sempre "ripristinato"; non devi fare nulla manualmente.
Charles Duffy,

5
@imagineer Questo è un bug che coinvolge herestring e modifiche locali a IFS che richiedono $INdi essere quotate. Il bug è stato corretto in bash4.3.
Chepner,

973

Tratto dalla matrice divisa dello script della shell Bash :

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

Spiegazione:

Questa costruzione sostituisce tutte le occorrenze di ';'(l'iniziale //significa sostituzione globale) nella stringa INcon ' '(un singolo spazio), quindi interpreta la stringa delimitata da spazi come un array (ecco cosa fanno le parentesi circostanti).

La sintassi utilizzata all'interno delle parentesi graffe per sostituire ogni ';'carattere con un ' 'carattere si chiama Espansione dei parametri .

Ci sono alcuni gotcha comuni:

  1. Se la stringa originale ha spazi, dovrai utilizzare IFS :
    • IFS=':'; arrIN=($IN); unset IFS;
  2. Se la stringa originale ha spazi e il delimitatore è una nuova riga, è possibile impostare IFS con:
    • IFS=$'\n'; arrIN=($IN); unset IFS;

84
Voglio solo aggiungere: questo è il più semplice di tutti, puoi accedere agli elementi dell'array con $ {arrIN [1]} (a partire dagli zeri ovviamente)
Oz123

26
Trovato: la tecnica per modificare una variabile all'interno di $ {} è nota come "espansione dei parametri".
KomodoDave,

23
No, non penso che funzioni quando sono presenti anche spazi ... sta convertendo ',' in '' e quindi costruendo un array separato da spazi.
Ethan,

12
Molto conciso, ma ci sono avvertenze per l'uso generale : la shell applica la divisione delle parole e le espansioni alla stringa, che possono essere indesiderate; provalo con. IN="bla@some.com;john@home.com;*;broken apart". In breve: questo approccio si interromperà se i token contengono spazi e / o caratteri incorporati. come *quello accade per far corrispondere un token ai nomi dei file nella cartella corrente.
mklement0

53
Questo è un cattivo approccio per altri motivi: ad esempio, se la stringa contiene ;*;, *verrà espanso in un elenco di nomi di file nella directory corrente. -1
Charles Duffy,

249

Se non ti dispiace elaborarli immediatamente, mi piace fare questo:

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

È possibile utilizzare questo tipo di loop per inizializzare un array, ma probabilmente esiste un modo più semplice per farlo. Spero che questo aiuti, però.


Avresti dovuto mantenere la risposta IFS. Mi ha insegnato qualcosa che non sapevo e sicuramente ha fatto un array, mentre questo fa solo un sostituto economico.
Chris Lutz,

Vedo. Sì, trovo che faccia questi stupidi esperimenti, imparerò nuove cose ogni volta che cerco di rispondere alle cose. Ho modificato le cose in base al feedback IRC #bash e non eliminato :)
Johannes Schaub - litb

33
-1, ovviamente non sei a conoscenza del wordplitting, perché introduce due bug nel tuo codice. uno è quando non citi $ IN e l'altro è quando fai finta che una nuova riga sia l'unico delimitatore usato nel wordplitting. Stai iterando su ogni WORD in IN, non su ogni riga e DEFINATAMENTE non tutti gli elementi delimitati da un punto e virgola, anche se può sembrare che abbia l'effetto collaterale di sembrare che funzioni.
lhunath,

3
È possibile modificarlo in eco "$ IN" | tr ';' '\ n' | durante la lettura -r ADDY; fare # process "$ ADDY"; fatto per renderlo fortunato, penso :) Notate che questo si biforcherà, e non potete cambiare le variabili esterne dall'interno del loop (ecco perché ho usato la sintassi <<< "$ IN") quindi
Johannes Schaub - litb

8
Per riassumere il dibattito nei commenti: Avvertenze per uso generale : la shell applica la divisione delle parole e le espansioni alla stringa, che possono essere indesiderate; provalo con. IN="bla@some.com;john@home.com;*;broken apart". In breve: questo approccio si interromperà se i token contengono spazi e / o caratteri incorporati. come *quello accade per far corrispondere un token ai nomi dei file nella cartella corrente.
mklement0

202

Risposta compatibile

Ci sono molti modi diversi per farlo .

Tuttavia, è importante notare prima che bashha molte caratteristiche speciali (i cosiddetti bashismi ) che non funzioneranno in nessun altro.

In particolare, gli array , array associativi e sostituzione del modello , che vengono utilizzati nelle soluzioni in questo post così come gli altri nel thread, sono bashismi e potrebbe non funzionare con altri gusci che molte persone utilizzano.

Ad esempio: sul mio Debian GNU / Linux , c'è una shell standard chiamata; Conosco molte persone a cui piace usare un'altra shell chiamata; e c'è anche uno strumento speciale chiamato con il suo interprete di shell ().

Stringa richiesta

La stringa da dividere nella domanda precedente è:

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

Userò una versione modificata di questa stringa per garantire che la mia soluzione sia robusta per le stringhe contenenti spazi bianchi, che potrebbero interrompere altre soluzioni:

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

Dividi la stringa in base al delimitatore in (versione> = 4.2)

In puro bash , possiamo creare un array con elementi divisi per un valore temporaneo per IFS (il separatore del campo di input ). L'IFS, tra le altre cose, dice a bashquale carattere deve trattare come delimitatore tra gli elementi quando si definisce un array:

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

In versioni più recenti di bash, anteponendo un comando con una definizione IFS cambia le IFS per quel comando solo e reset al valore precedente subito dopo. Ciò significa che possiamo fare quanto sopra in una sola riga:

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'

Possiamo vedere che la stringa INè stata memorizzata in un array chiamato fields, diviso nei punti e virgola:

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>'

(Possiamo anche visualizzare il contenuto di queste variabili usando 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>")

Si noti che readè il modo più rapido per eseguire la divisione perché non sono presenti fork o risorse esterne chiamate.

Una volta definito l'array, è possibile utilizzare un semplice ciclo per elaborare ciascun campo (o, piuttosto, ogni elemento dell'array che ora è stato definito):

# `"${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>]

Oppure potresti eliminare ogni campo dall'array dopo l'elaborazione usando un approccio di spostamento , che mi piace:

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

E se vuoi solo una semplice stampa dell'array, non hai nemmeno bisogno di passarci sopra:

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

Aggiornamento: recente > = 4.4

Nelle versioni più recenti di bash, puoi anche giocare con il comando mapfile:

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

Questa sintassi conserva caratteri speciali, newline e campi vuoti!

Se non si desidera includere campi vuoti, è possibile effettuare le seguenti operazioni:

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

Con mapfile, puoi anche saltare la dichiarazione di un array e implicitamente "loop" sugli elementi delimitati, chiamando una funzione su ciascuno:

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

(Nota: la \0fine della stringa di formato è inutile se non ti interessano i campi vuoti alla fine della stringa o non sono presenti.)

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.

Oppure potresti usare <<<, e nel corpo della funzione includere alcune elaborazioni per eliminare la nuova riga che aggiunge:

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.

Dividi la stringa in base al delimitatore in

Se non puoi usare bash, o se vuoi scrivere qualcosa che può essere usato in molte diverse shell, spesso non puoi usare i bashismi - e questo include gli array che abbiamo usato nelle soluzioni sopra.

Tuttavia, non è necessario utilizzare le matrici per eseguire il loop su "elementi" di una stringa. Esiste una sintassi utilizzata in molte shell per eliminare sottostringhe di una stringa dalla prima o dall'ultima occorrenza di un modello. Si noti che *è un carattere jolly che sta per zero o più caratteri:

(La mancanza di questo approccio in qualsiasi soluzione pubblicata finora è il motivo principale per cui sto scrivendo questa risposta;)

${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

Come spiegato da Score_Under :

#ed %elimina la sottostringa corrispondente più corta possibile rispettivamente dall'inizio e dalla fine della stringa, e

##ed %%elimina la sottostringa corrispondente più lunga possibile.

Usando la sintassi sopra, possiamo creare un approccio in cui estraiamo "elementi" di sottostringa dalla stringa eliminando le sottostringhe fino o dopo il delimitatore.

Il blocco codice qui sotto funziona bene in (compresi quelli di Mac OS bash),, , e '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>]

Divertiti!


15
I #, ##, %, e %%le sostituzioni hanno quello che è IMO una spiegazione più facile da ricordare (per quanto essi cancellare): #e %cancellare più breve stringa corrispondente possibile, ed ##e %%eliminare il più lungo possibile.
Score_Under

1
Il IFS=\; read -a fields <<<"$var"fallisce su newline e aggiunge un newline finale. L'altra soluzione rimuove un campo vuoto finale.
Isaac,

Il delimitatore di shell è la risposta più elegante, punto.
Eric Chen,

L'ultima alternativa potrebbe essere utilizzata con un elenco di separatori di campo impostato altrove? Ad esempio, intendo usarlo come script di shell e passare un elenco di separatori di campo come parametro posizionale.
sancho.s ReinstateMonicaCellio il

Sì, in un ciclo:for sep in "#" "ł" "@" ; do ... var="${var#*$sep}" ...
F. Hauri,

184

Ho visto un paio di risposte che fanno riferimento al cutcomando, ma sono state tutte cancellate. È un po 'strano che nessuno l'abbia elaborato, perché penso che sia uno dei comandi più utili per fare questo tipo di cose, specialmente per analizzare i file di registro delimitati.

Nel caso di suddividere questo esempio specifico in un array di script bash, trè probabilmente più efficiente, ma cutpuò essere utilizzato ed è più efficace se si desidera estrarre campi specifici dal centro.

Esempio:

$ 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

Puoi ovviamente inserirlo in un ciclo e ripetere il parametro -f per estrarre ogni campo in modo indipendente.

Ciò diventa più utile quando si dispone di un file di registro delimitato con righe come questa:

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

cutè molto utile essere in grado di catquesto file e selezionare un campo particolare per ulteriori elaborazioni.


6
Complimenti per l'uso cut, è lo strumento giusto per il lavoro! Molto più chiaro di qualsiasi di questi hack di shell.
Mister Miyagi,

4
Questo approccio funzionerà solo se si conosce in anticipo il numero di elementi; avresti bisogno di programmare un po 'più di logica attorno ad esso. Esegue anche uno strumento esterno per ogni elemento.
uli42,

Esattamente che stavo cercando di evitare una stringa vuota in un CSV. Ora posso indicare anche il valore esatto della "colonna". Lavora con IFS già utilizzato in un ciclo. Meglio del previsto per la mia situazione.
Louis Loudog Trottier,

Molto utile per estrarre documenti d'identità e PID, ad esempio
Milos Grujic,

Questa risposta vale scorrere più di mezza pagina :)
Gucu112,

124

Questo ha funzionato per me:

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

1
Sebbene funzioni solo con un delimitatore a singolo carattere, è quello che l'OP stava cercando (record delimitati da un punto e virgola).
GuyPaddock,

Risposto circa quattro anni fa da @Ashok e, più di un anno fa da @DougW , della tua risposta, con ancora più informazioni. Si prega di inviare una soluzione diversa rispetto ad altre.
MAChitgarha,

90

Che ne dici di questo approccio:

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

fonte


7
+1 ... ma non nominerei la variabile "Array" ... suppongo. Buona soluzione
Yzmir Ramirez,

14
+1 ... ma il "set" e dichiara -a non sono necessari. Avresti anche potuto usare soloIFS";" && Array=($IN)
ata

+1 Solo una nota a margine: non dovrebbe essere consigliabile conservare il vecchio IFS e ripristinarlo? (come mostrato da stefanB nella sua modifica3) le persone che atterrano qui (a volte solo copiando e incollando una soluzione) potrebbero non pensarci
Luca Borrione,

6
-1: Innanzitutto, @ata ha ragione sul fatto che la maggior parte dei comandi in questo non fa nulla. In secondo luogo, usa la suddivisione delle parole per formare l'array e non fa nulla per inibire l'espansione glob quando lo fa (quindi se hai caratteri glob in uno degli elementi dell'array, quegli elementi vengono sostituiti con nomi di file corrispondenti).
Charles Duffy,

1
Suggerisco di usare $'...': IN=$'bla@some.com;john@home.com;bet <d@\ns* kl.com>'. Quindi echo "${Array[2]}"stamperà una stringa con newline. set -- "$IN"è anche necessario in questo caso. Sì, per impedire l'espansione globale, la soluzione dovrebbe includere set -f.
John_West

79

Penso che AWK sia il comando migliore ed efficiente per risolvere il tuo problema. AWK è incluso per impostazione predefinita in quasi tutte le distribuzioni Linux.

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

darà

bla@some.com john@home.com

Ovviamente puoi memorizzare ogni indirizzo email ridefinendo il campo di stampa awk.


3
O ancora più semplice: echo "bla@some.com; john@home.com" | awk 'BEGIN {RS = ";"} {print}'
Jaro

@Jaro Questo ha funzionato perfettamente per me quando avevo una stringa con virgole e avevo bisogno di riformattarla in righe. Grazie.
Aquarelle,

Ha funzionato in questo scenario -> "echo" $ SPLIT_0 "| awk -F 'inode =' '{print $ 1}'"! Ho avuto problemi durante il tentativo di utilizzare atrings ("inode =") anziché caratteri (";"). $ 1, $ 2, $ 3, $ 4 sono impostati come posizioni in un array! Se c'è un modo di impostare un array ... meglio! Grazie!
Eduardo Lucio,

@EduardoLucio, quello che sto pensando è forse si può sostituire il vostro primo delimitatore inode=in ;per esempio sed -i 's/inode\=/\;/g' your_file_to_process, quindi definire -F';'quando applicare awk, la speranza che ti può aiutare.
Tong

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

4
-1 e se la stringa contiene spazi? per esempio IN="this is first line; this is second line" arrIN=( $( echo "$IN" | sed -e 's/;/\n/g' ) )produrrà una matrice di 8 elementi in questo caso (un elemento per ogni spazio di parole separato), anziché 2 (un elemento per ogni riga di punti e virgola separati)
Luca Borrione

3
@Luca No lo script sed crea esattamente due righe. Ciò che crea le voci multiple per te è quando le metti in un array bash (che si divide in uno spazio bianco per impostazione predefinita)
lothar

Questo è esattamente il punto: l'OP deve archiviare le voci in un array per poterlo scorrere, come puoi vedere nelle sue modifiche. Penso che la tua (buona) risposta non sia menzionata per usarla arrIN=( $( echo "$IN" | sed -e 's/;/\n/g' ) )per raggiungere questo obiettivo e per consigliare di cambiare IFS IFS=$'\n'per coloro che atterreranno qui in futuro e dovranno dividere una stringa contenente spazi. (e per ripristinarlo in seguito). :)
Luca Borrione il

1
@Luca Un buon punto. Tuttavia, l'assegnazione dell'array non era nella domanda iniziale quando ho scritto quella risposta.
lothar,

65

Questo funziona anche:

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

Fai attenzione, questa soluzione non è sempre corretta. Nel caso in cui passi solo "bla@some.com", lo assegnerà sia ad ADD1 che ad ADD2.


1
Puoi usare -s per evitare il problema menzionato: superuser.com/questions/896800/… "-f, --fields = LIST seleziona solo questi campi; stampa anche qualsiasi riga che non contenga alcun carattere delimitatore, a meno che l'opzione -s non sia specificato "
fersarr

34

Un'opinione diversa sulla risposta di Darron , ecco come la faccio:

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

Penso di si! Esegui i comandi sopra e poi "echo $ ADDR1 ... $ ADDR2" e ottengo l'output "bla@some.com ... john@home.com"
nickjb

1
Questo ha funzionato MOLTO bene per me ... L'ho usato per scorrere su una serie di stringhe che contenevano dati separati da virgola DB, SERVER, PORT per usare mysqldump.
Nick,

5
Diagnosi: l' IFS=";"assegnazione esiste solo nella $(...; echo $IN)subshell; questo è il motivo per cui alcuni lettori (incluso me) inizialmente pensano che non funzionerà. Ho pensato che tutto $ IN venisse assorbito da ADDR1. Ma il nickjb è corretto; funziona. Il motivo è che il echo $INcomando analizza i suoi argomenti utilizzando il valore corrente di $ IFS, ma li fa eco a stdout utilizzando un delimitatore di spazio, indipendentemente dall'impostazione di $ IFS. Quindi l'effetto netto è come se uno avesse chiamato read ADDR1 ADDR2 <<< "bla@some.com john@home.com"(nota che l'input è separato dallo spazio, non separato).
dubiousjim

1
Ciò non è riuscito a spazi e ritorni a capo, e anche espandere jolly *in echo $INcon un'espansione variabile non quotato.
Isaac,

Mi piace molto questa soluzione. Una descrizione del perché funziona sarebbe molto utile e renderebbe una risposta globale migliore.
Michael Gaskill,

32

In Bash, un modo a prova di proiettile, che funzionerà anche se la tua variabile contiene nuove righe:

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

Guarda:

$ 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")'

Il trucco per farlo funzionare è usare l' -dopzione di read(delimitatore) con un delimitatore vuoto, in modo che readsia costretto a leggere tutto ciò che viene alimentato. E ci nutriamo readesattamente con il contenuto della variabile in, senza alcuna nuova riga finale grazie a printf. Nota che stiamo anche inserendo il delimitatore printfper assicurarci che la stringa passata readabbia un delimitatore finale. Senza di essa, readtaglierebbero potenziali campi vuoti finali:

$ 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]="")'

il campo vuoto finale viene conservato.


Aggiornamento per Bash≥4.4

Da Bash 4.4, il builtin mapfile(aka readarray) supporta l' -dopzione per specificare un delimitatore. Quindi un altro modo canonico è:

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

5
L'ho trovata come la rara soluzione in quella lista che funziona correttamente con \n, spazi e *contemporaneamente. Inoltre, nessun loop; la variabile array è accessibile nella shell dopo l'esecuzione (contrariamente alla risposta con il voto più alto). Nota, in=$'...'non funziona con virgolette doppie. Penso che abbia bisogno di più voti.
John_West,

28

Che ne dici di questa fodera, se non stai usando le matrici:

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

Considerare l'utilizzo read -r ...per assicurarsi che, ad esempio, i due caratteri "\ t" nell'input finiscano come gli stessi due caratteri nelle variabili (anziché un singolo carattere di tabulazione).
dubiousjim

-1 Questo non funziona qui (Ubuntu 12.04). L'aggiunta echo "ADDR1 $ADDR1"\n echo "ADDR2 $ADDR2"al tuo frammento genererà ADDR1 bla@some.com john@home.com\nADDR2(\ n è newline)
Luca Borrione,

Ciò è probabilmente dovuto a un bug che coinvolge IFSe qui stringhe che sono state corrette in bash4.3. La citazione $INdovrebbe risolverlo. (In teoria, $INnon è soggetto a suddivisione in parole o globbing dopo l'espansione, il che significa che le virgolette non dovrebbero essere necessarie. Anche in 4.3, tuttavia, rimane almeno un bug - segnalato e programmato per essere risolto - quindi la quotazione rimane buona idea.)
chepner,

Ciò si interrompe se $ in contiene newline anche se viene quotato $ IN. E aggiunge una nuova riga finale.
Isaac,

Un problema con questo, e molte altre soluzioni è anche che presuppone che ci siano ESATTAMENTE DUE elementi in $ IN - O che tu sia disposto a far fracassare il secondo e i successivi elementi in ADDR2. Capisco che questo soddisfa la domanda, ma è una bomba a orologeria.
Steven the Easy Amused

23

Senza impostare l'IFS

Se hai solo due punti puoi farlo:

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

otterrete:

b = foo
c = bar

20

Ecco un 3-liner pulito:

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

dove IFSdelimita le parole in base al separatore e ()viene utilizzato per creare un array . Poi[@] viene utilizzato per restituire ogni articolo come una parola separata.

Se dopo hai qualche codice, devi anche ripristinare $IFS, ad es unset IFS.


5
L'uso di un $innon quotato consente di espandere i caratteri jolly.
Isaac,

10

La seguente funzione Bash / zsh divide il suo primo argomento sul delimitatore dato dal secondo argomento:

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
}

Ad esempio, il comando

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

i rendimenti

a
b
c

Questo output può, ad esempio, essere reindirizzato ad altri comandi. Esempio:

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

Rispetto alle altre soluzioni fornite, questa presenta i seguenti vantaggi:

  • IFSnon viene sostituito: a causa dell'ambito dinamico anche delle variabili locali, l'override IFSsu un loop provoca la perdita del nuovo valore nelle chiamate di funzione eseguite dall'interno del loop.

  • Gli array non vengono utilizzati: la lettura di una stringa in un array mediante readrichiede il flag -ain Bash e -Ain zsh.

Se lo si desidera, la funzione può essere inserita in uno script come segue:

#!/usr/bin/env bash

split() {
    # ...
}

split "$@"

Non sembra funzionare con delimitatori più lunghi di 1 carattere: split = $ (split "$ content" "file: //")
madprops

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

8

puoi applicare awk in molte situazioni

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

anche tu puoi usare questo

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

7

Esiste un modo semplice e intelligente come questo:

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

Ma devi usare gnu xargs, BSD xargs non può supportare -d delim. Se usi Apple Mac come me. Puoi installare gnu xargs:

brew install findutils

poi

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

4

Questo è il modo più semplice per farlo.

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

4

Ci sono alcune risposte interessanti qui (errator esp.), Ma per qualcosa di analogo da dividere in altre lingue - che è quello che ho voluto dire con la domanda originale - mi sono deciso su questo:

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

Ora ${a[0]}, ${a[1]}ecc. Sono come ti aspetteresti. Utilizzare ${#a[*]}per numero di termini. O per iterare, ovviamente:

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

NOTA IMPORTANTE:

Questo funziona nei casi in cui non ci sono spazi di cui preoccuparsi, il che ha risolto il mio problema, ma potrebbe non risolvere il tuo. Vai con la $IFSsoluzione (s) in quel caso.


Non funziona quando INcontiene più di due indirizzi e-mail. Si prega di fare riferimento alla stessa idea (ma risolta) alla risposta di palindrom
olibre

Meglio usare ${IN//;/ }(doppia barra) per farlo funzionare anche con più di due valori. Attenzione che qualsiasi carattere jolly ( *?[) verrà espanso. E un campo vuoto finale verrà scartato.
Isaac,

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

Produzione

bla@some.com
john@home.com

Sistema: Ubuntu 12.04.1


IFS non viene impostato nel contesto specifico di readqui e quindi può sconvolgere il resto del codice, se presente.
codeforester

2

Se non c'è spazio, perché non questo?

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

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

2

Utilizzare il setbuilt-in per caricare l' $@array:

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

Quindi, lascia che la festa abbia inizio:

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

Uso migliore set -- $INper evitare alcuni problemi con "$ IN" che inizia con il trattino. Tuttavia, l'espansione non quotata di $INespanderà i caratteri jolly ( *?[).
Isaac,

2

Due alternative bourne-ish in cui nessuno dei due richiede array di bash:

Caso 1 : mantienilo bello e semplice: usa una NewLine come separatore di record ... ad es.

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

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

Nota: in questo primo caso non viene eseguito alcun processo secondario per agevolare la manipolazione dell'elenco.

Idea: forse vale la pena usare la NL ampiamente internamente e convertirsi in una RS diversa quando si genera esternamente il risultato finale .

Caso 2 : utilizzo di un ";" come separatore di record ... ad es.

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"

In entrambi i casi, un sottoelenco che può essere composto all'interno del ciclo è persistente dopo il completamento del ciclo. Ciò è utile quando si manipolano elenchi in memoria, invece di archiviare elenchi in file. {ps mantieni la calma e vai avanti B-)}


2

A parte le fantastiche risposte che sono già state fornite, se si tratta solo di stampare i dati che potresti prendere in considerazione utilizzando awk:

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

Questo imposta il separatore di campo su ;, in modo che possa scorrere i campi con un forciclo e stampare di conseguenza.

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]

Con un altro input:

$ 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

Nella shell di Android, la maggior parte dei metodi proposti non funziona:

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

Ciò che funziona è:

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

dove //significa sostituzione globale.


1
Non riesce se una parte di $ PATH contiene spazi (o nuove righe). Espande anche i caratteri jolly (asterisco *, punto interrogativo? E parentesi graffe […]).
Isaac,

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

Produzione:

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

Spiegazione: L'assegnazione semplice mediante parentesi () converte l'elenco separato da punto e virgola in un array a condizione che si disponga di IFS corretto. Il ciclo FOR standard gestisce i singoli elementi in tale array come al solito. Si noti che l'elenco indicato per la variabile IN deve essere "rigido", vale a dire con singoli tick.

L'IFS deve essere salvato e ripristinato poiché Bash non tratta un'assegnazione come un comando. Una soluzione alternativa è quella di avvolgere l'assegnazione all'interno di una funzione e chiamare quella funzione con un IFS modificato. In tal caso non è necessario il salvataggio / ripristino separato di IFS. Grazie per "Bize" per averlo sottolineato.


!"#$%&/()[]{}*? are no problembene ... non proprio: []*?sono personaggi glob. Quindi, per quanto riguarda la creazione di questa directory e file: `mkdir '!" # $% &'; Touch '! "# $% & / () [] {} Ti hahahahaha - non ci sono problemi' ed esegui il tuo comando? semplice può essere bello, ma quando è rotto, è rotto.
gniourf_gniourf,

@gniourf_gniourf La stringa è memorizzata in una variabile. Si prega di consultare la domanda originale.
ajaaskel,

1
@ajaaskel non hai compreso appieno il mio commento. Andare in una directory zero e questi comandi: mkdir '!"#$%&'; touch '!"#$%&/()[]{} got you hahahaha - are no problem'. Creeranno solo una directory e un file, con nomi dall'aspetto strano, devo ammetterlo. Quindi eseguire i comandi con l'esatto INhai dato: IN='bla@some.com;john@home.com;Charlie Brown <cbrown@acme.com;!"#$%&/()[]{}*? are no problem;simple is beautiful :-)'. Vedrai che non otterrai l'output che ti aspetti. Perché stai usando un metodo soggetto ad espansioni di percorso per dividere la stringa.
gniourf_gniourf,

Questo è quello di dimostrare che i personaggi *, ?, [...]e anche, se extglobè impostato, !(...), @(...), ?(...), +(...) sono problemi con questo metodo!
gniourf_gniourf,

1
@gniourf_gniourf Grazie per commenti dettagliati sul globbing. Ho modificato il codice in modo che si stacchi. Il mio punto era tuttavia solo quello di dimostrare che un compito piuttosto semplice può fare il lavoro di scissione.
ajaaskel,

1

Ok ragazzi!

Ecco la mia risposta!

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

Perché questo approccio è "il migliore" per me?

Per due motivi:

  1. Non è necessario sfuggire al delimitatore;
  2. Non avrai problemi con gli spazi vuoti . Il valore verrà separato correttamente nell'array!

[]'S


Cordiali saluti, /etc/os-releasee /etc/lsb-releasesono pensati per essere di provenienza e non analizzati. Quindi il tuo metodo è davvero sbagliato. Inoltre, non stai ancora rispondendo alla domanda sullo spilting di una stringa su un delimitatore.
gniourf_gniourf

0

Un liner per dividere una stringa separata da ';' in un array è:

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

Questo imposta IFS solo in una subshell, quindi non devi preoccuparti di salvare e ripristinare il suo valore.


-1 questo non funziona qui (Ubuntu 12.04). stampa solo il primo eco con tutto il valore $ IN, mentre il secondo è vuoto. puoi vederlo se metti echo "0:" $ {ADDRS [0]} \ n echo "1:" $ {ADDRS [1]} l'output è 0: bla@some.com;john@home.com\n 1:(\ n è una nuova riga)
Luca Borrione,

1
fare riferimento alla risposta di nickjb all'indirizzo per un'alternativa di lavoro a questa idea stackoverflow.com/a/6583589/1032370
Luca Borrione,

1
-1, 1. IFS non viene impostato in quella subshell (viene passato all'ambiente di "echo", che è incorporato, quindi non succede nulla). 2. $INè quotato, quindi non è soggetto alla suddivisione IFS. 3. La sostituzione del processo è suddivisa per spazi bianchi, ma ciò potrebbe danneggiare i dati originali.
Score_Under

0

Forse non è la soluzione più elegante, ma funziona con *e spazi:

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

Uscite

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

Altro esempio (delimitatori all'inizio e alla fine):

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

Fondamentalmente rimuove ogni personaggio diverso da quello di ;fare delimsad es. ;;;. Quindi passa forda 1a number-of-delimiterscome contato da ${#delims}. Il passo finale è quello di ottenere in sicurezza la $iparte usando cut.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.