Dividi la stringa in un array in Bash


641

In uno script Bash vorrei dividere una riga in pezzi e memorizzarli in un array.

La linea:

Paris, France, Europe

Vorrei averli in un array come questo:

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

Vorrei usare un codice semplice, la velocità del comando non ha importanza. Come posso farlo?


22
Questo è il numero 1 di Google, ma c'è una controversia nella risposta perché la domanda purtroppo chiede delimitazione su , (spazio-virgola) e non un singolo carattere come la virgola. Se siete interessati solo a quest'ultimo, le risposte qui sono più facili da seguire: stackoverflow.com/questions/918886/...
antak

Se vuoi munge una stringa e non ti importa di averla come matrice, cutè anche un utile comando bash da tenere a mente. Il separatore è definibile en.wikibooks.org/wiki/Cut Puoi anche estrarre i dati da una struttura di record a larghezza fissa. en.wikipedia.org/wiki/Cut_(Unix) computerhope.com/unix/ucut.htm
JGFMK

Risposte:


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

Si noti che i caratteri $IFSvengono trattati singolarmente come separatori in modo che in questo caso campi possono essere separati da uno virgola o uno spazio piuttosto che la sequenza dei due caratteri. È interessante notare che i campi vuoti non vengono creati quando lo spazio virgola appare nell'input perché lo spazio viene trattato in modo speciale.

Per accedere a un singolo elemento:

echo "${array[0]}"

Per scorrere gli elementi:

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

Per ottenere sia l'indice che il valore:

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

L'ultimo esempio è utile perché gli array Bash sono sparsi. In altre parole, è possibile eliminare un elemento o aggiungere un elemento e quindi gli indici non sono contigui.

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

Per ottenere il numero di elementi in un array:

echo "${#array[@]}"

Come accennato in precedenza, le matrici possono essere sparse, quindi non dovresti usare la lunghezza per ottenere l'ultimo elemento. Ecco come è possibile in Bash 4.2 e versioni successive:

echo "${array[-1]}"

in qualsiasi versione di Bash (da qualche parte dopo 2.05b):

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

Offset negativi più grandi selezionano più lontano dalla fine dell'array. Nota lo spazio prima del segno meno nel modulo precedente. È richiesto.


15
Basta usare IFS=', ', quindi non è necessario rimuovere gli spazi separatamente. Test:IFS=', ' read -a array <<< "Paris, France, Europe"; echo "${array[@]}"
l0b0

4
@ l0b0: grazie. Non so cosa stavo pensando. A declare -p arrayproposito, mi piace usare per l'output di test.
In pausa fino a nuovo avviso.

1
Questo non sembra rispettare le virgolette. Ad esempio France, Europe, "Congo, The Democratic Republic of the"questo si dividerà dopo il congo.
Yisrael Dov

2
@YisraelDov: Bash non ha modo di gestire CSV da solo. Non è in grado di distinguere tra virgole all'interno delle virgolette e quelle esterne. Dovrai usare uno strumento che capisca CSV come una lib in un linguaggio di livello superiore, ad esempio il modulo CSV in Python.
In pausa fino a nuovo avviso.

5
str="Paris, France, Europe, Los Angeles"; IFS=', ' read -r -a array <<< "$str"verrà suddiviso in array=([0]="Paris" [1]="France" [2]="Europe" [3]="Los" [4]="Angeles")una nota. Quindi funziona solo con i campi senza spazi poiché IFS=', 'è un insieme di singoli caratteri, non un delimitatore di stringa.
Dawg,

333

Tutte le risposte a questa domanda sono sbagliate in un modo o nell'altro.


Risposta errata n. 1

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

1: Questo è un uso improprio di $IFS. Il valore della $IFSvariabile non viene considerato come un singolo separatore di stringhe di lunghezza variabile , ma piuttosto come un insieme di separatori di stringhe a carattere singolo , in cui ogni campo che si readdivide dalla riga di input può essere terminato da qualsiasi carattere nel set (virgola o spazio, in questo esempio).

In realtà, per i veri pignoli là fuori, il significato completo di $IFSè leggermente più coinvolto. Dal manuale di bash :

La shell tratta ogni carattere di IFS come un delimitatore e divide i risultati delle altre espansioni in parole usando questi caratteri come terminatori di campo. Se IFS non è impostato o il suo valore è esattamente <spazio><tab> <nuova> , l'impostazione predefinita, quindi le sequenze di <spazio> , <tab> e <nuova> all'inizio e alla fine dei risultati delle espansioni precedenti vengono ignorati e qualsiasi sequenza di caratteri IFS non all'inizio o alla fine serve a delimitare le parole. Se IFS ha un valore diverso da quello predefinito, le sequenze dei caratteri degli spazi bianchi <spazio> , <tab> e <vengono ignorati all'inizio e alla fine della parola, purché il carattere di spazio bianco sia nel valore di IFS (un carattere di spazio bianco IFS ). Qualsiasi carattere in IFS che non sia uno spazio bianco IFS , insieme a qualsiasi carattere di spazio bianco IFS adiacente , delimita un campo. Una sequenza di caratteri spazi bianchi IFS viene anche trattata come delimitatore. Se il valore di IFS è nullo, non si verifica la divisione di parole.

Fondamentalmente, per valori non nulli non predefiniti di $IFS, i campi possono essere separati con (1) una sequenza di uno o più caratteri che appartengono tutti all'insieme di "caratteri spazi bianchi IFS" (ovvero, qualunque sia <space> , <tab> e <newline> ("newline" che significa avanzamento riga (LF) ) sono presenti ovunque in $IFS), o (2) qualsiasi "carattere di spazio bianco IFS" non presente che sia presente $IFScon qualunque "carattere di spazio bianco IFS" lo circonda nella riga di input.

Per l'OP, è possibile che la seconda modalità di separazione che ho descritto nel paragrafo precedente sia esattamente ciò che desidera per la sua stringa di input, ma possiamo essere abbastanza sicuri che la prima modalità di separazione che ho descritto non sia affatto corretta. Ad esempio, se la sua stringa di input fosse '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: Anche se si dovesse utilizzare questa soluzione con un separatore singolo carattere (come una virgola per sé, cioè senza spazio seguente o altri bagagli), se il valore della $stringvariabile succede a contenere LF, allora readsarà interrompere l'elaborazione una volta che incontra il primo LF. L' readintegrato elabora solo una riga per invocazione. Questo è vero anche se si sta tubazioni o il reindirizzamento di ingresso solo per la readdichiarazione, come stiamo facendo in questo esempio con la stringa here meccanismo, e l'ingresso in tal modo non trasformati è garantito da perdere. Il codice che alimenta il readbuiltin non ha conoscenza del flusso di dati all'interno della sua struttura di comando contenente.

Si potrebbe sostenere che è improbabile che ciò causi un problema, ma è comunque un rischio sottile che dovrebbe essere evitato, se possibile. È causato dal fatto che l' readintegrato esegue effettivamente due livelli di suddivisione dell'input: prima in linee, poi in campi. Poiché l'OP vuole solo un livello di suddivisione, questo uso del readbuiltin non è appropriato e dovremmo evitarlo.

3: Un potenziale problema non ovvio con questa soluzione è che readelimina sempre il campo finale se è vuoto, sebbene conservi altrimenti i campi vuoti. Ecco una 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]="")

Forse il PO non se ne preoccuperebbe, ma è comunque una limitazione che vale la pena conoscere. Riduce la robustezza e la generalità della soluzione.

Questo problema può essere risolto aggiungendo un delimitatore finale fittizio alla stringa di input appena prima di alimentarla read, come dimostrerò più avanti.


Risposta errata n. 2

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

Idea simile:

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

(Nota: ho aggiunto le parentesi mancanti attorno alla sostituzione del comando che il risponditore sembra aver omesso.)

Idea simile:

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

Queste soluzioni sfruttano la suddivisione delle parole in un'assegnazione di array per dividere la stringa in campi. Stranamente, proprio come la readsuddivisione generale delle parole usa anche la $IFSvariabile speciale, sebbene in questo caso sia implicito che sia impostato sul suo valore predefinito <space><tab> <newline> , e quindi qualsiasi sequenza di uno o più IFS i caratteri (che ora sono tutti caratteri di spazi bianchi) è considerato un delimitatore di campo.

Questo risolve il problema di due livelli di scissione commessi da read, poiché la scissione di parole da sola costituisce solo un livello di scissione. Ma proprio come prima, il problema qui è che i singoli campi nella stringa di input possono già contenere $IFScaratteri e quindi verrebbero suddivisi in modo errato durante l'operazione di divisione delle parole. Questo non è il caso di nessuna delle stringhe di input di esempio fornite da questi risponditori (quanto conveniente ...), ma ovviamente ciò non cambia il fatto che qualsiasi base di codice che ha usato questo idioma rischierebbe quindi di esplodere se questa ipotesi fosse mai stata violata ad un certo punto lungo la linea. Ancora una volta, considera il mio controesempio di 'Los Angeles, United States, North America'(o 'Los Angeles:United States:North America').

Inoltre, suddivisione in parole è normalmente seguito da espansione dei nomi ( alias espansione di percorso alias globbing), che, se fatto, sarebbero parole potenzialmente danneggiare contenenti i caratteri *, ?o [seguita da ](e, se extglobè impostato, frammenti parentesi preceduto da ?, *, +, @, oppure !) abbinandoli agli oggetti del file system ed espandendo le parole ("globs") di conseguenza. Il primo di questi tre risponditori ha abilmente risolto il problema eseguendo in set -fanticipo per disabilitare il globbing. Tecnicamente funziona (anche se probabilmente dovresti aggiungereset +f successivamente riattivare il globbing per il codice successivo che può dipendere da esso), ma è indesiderabile dover fare confusione con le impostazioni globali della shell per hackerare un'operazione di analisi string-to-array di base nel codice locale.

Un altro problema con questa risposta è che tutti i campi vuoti andranno persi. Questo può o meno essere un problema, a seconda dell'applicazione.

Nota: se si intende utilizzare questa soluzione, è meglio utilizzare la ${string//:/ }forma di " espansione dei parametri" dell'espansione dei parametri , piuttosto che preoccuparsi di invocare una sostituzione dei comandi (che crea la shell), avviare una pipeline e eseguire un eseguibile esterno ( tro sed), poiché l'espansione dei parametri è puramente un'operazione interna alla shell. (Inoltre, per le soluzioni tre sed, la variabile di input dovrebbe essere racchiusa tra virgolette all'interno della sostituzione del comando; altrimenti la suddivisione delle parole avrebbe effetto nel echocomando e potenzialmente confonderebbe i valori del campo. Inoltre, la $(...)forma di sostituzione del comando è preferibile alla vecchia`...` form poiché semplifica l'annidamento delle sostituzioni di comandi e consente una migliore evidenziazione della sintassi da parte degli editor di testo.)


Risposta errata n. 3

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

Questa risposta è quasi la stessa di # 2 . La differenza è che il risponditore ha ipotizzato che i campi siano delimitati da due caratteri, uno dei quali rappresentato nell'impostazione predefinita $IFSe l'altro no. Ha risolto questo caso piuttosto specifico rimuovendo il carattere non rappresentato dall'IFS usando un'espansione di sostituzione del modello e quindi usando la suddivisione delle parole per dividere i campi sul carattere delimitatore rappresentato dall'IFS sopravvissuto.

Questa non è una soluzione molto generica. Inoltre, si può sostenere che la virgola è davvero il carattere delimitatore "primario" qui, e che rimuoverlo e quindi a seconda del carattere dello spazio per la divisione del campo è semplicemente sbagliato. Ancora una volta, prendere in considerazione le mie controesempio: 'Los Angeles, United States, North America'.

Inoltre, ancora una volta, l'espansione del nome file potrebbe corrompere le parole espanse, ma ciò può essere prevenuto disabilitando temporaneamente il globbing per il compito con set -fe quindi set +f.

Inoltre, tutti i campi vuoti andranno persi, il che potrebbe essere un problema a seconda dell'applicazione.


Risposta errata n. 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"

Questo è simile a # 2 e # 3 in quanto utilizza la suddivisione delle parole per eseguire il lavoro, solo ora il codice imposta esplicitamente $IFSper contenere solo il delimitatore di campo a carattere singolo presente nella stringa di input. Va ripetuto che ciò non può funzionare per i delimitatori di campi multicaratteri come il delimitatore di spazio virgola del PO. Ma per un delimitatore a carattere singolo come l'LF usato in questo esempio, in realtà si avvicina all'essere perfetto. I campi non possono essere divisi involontariamente nel mezzo, come abbiamo visto con precedenti risposte errate, e c'è solo un livello di divisione, come richiesto.

Un problema è che l'espansione del nome file corromperà le parole interessate come descritto in precedenza, anche se ancora una volta questo può essere risolto racchiudendo l'istruzione critica in set -fe set +f.

Un altro potenziale problema è che, poiché LF si qualifica come "carattere di spazi bianchi IFS" come definito in precedenza, tutti i campi vuoti andranno persi, proprio come in # 2 e # 3 . Questo ovviamente non sarebbe un problema se il delimitatore fosse un non "carattere di spazi bianchi IFS" e, a seconda dell'applicazione, potrebbe non importare comunque, ma vizierebbe la generalità della soluzione.

Quindi, per riassumere, supponendo che tu abbia un delimitatore di un carattere e che non sia un "carattere di spazi bianchi IFS" o che non ti interessi dei campi vuoti, e avvolgi l'istruzione critica in , set -fe set +fquindi questa soluzione funziona , ma per il resto no.

(Inoltre, per motivi di informazione, l'assegnazione di un LF a una variabile in bash può essere eseguita più facilmente con la $'...'sintassi, ad es IFS=$'\n';.)


Risposta errata n. 5

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

Idea simile:

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

Questa soluzione è effettivamente un incrocio tra # 1 (in quanto imposta $IFSsu spazio-virgola) e # 2-4 (in quanto utilizza la suddivisione in parole per dividere la stringa in campi). Per questo motivo, soffre della maggior parte dei problemi che affliggono tutte le risposte sbagliate sopra, un po 'come il peggiore di tutti i mondi.

Inoltre, per quanto riguarda la seconda variante, può sembrare che la evalchiamata sia completamente inutile, poiché il suo argomento è una stringa a virgoletta singola letterale e quindi è staticamente noto. Ma in realtà c'è un vantaggio molto ovvio nell'utilizzare evalin questo modo. Normalmente, quando si esegue un comando semplice che consiste solo in un'assegnazione variabile , ovvero senza una parola di comando effettiva che lo segue, l'assegnazione ha effetto nell'ambiente shell:

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

Questo è vero anche se il semplice comando prevede più assegnazioni di variabili; di nuovo, fintanto che non ci sono parole di comando, tutte le assegnazioni di variabili influiscono sull'ambiente shell:

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

Tuttavia, se l'assegnazione della variabile è associata a un nome di comando (mi piace chiamarla "assegnazione di prefisso"), ciò non influisce sull'ambiente shell, ma riguarda solo l'ambiente del comando eseguito, indipendentemente dal fatto che sia un builtin o esterno:

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

Citazione pertinente dal manuale di bash :

Se non risulta alcun nome di comando, le assegnazioni delle variabili influiscono sull'ambiente di shell corrente. Altrimenti, le variabili vengono aggiunte all'ambiente del comando eseguito e non influiscono sull'ambiente di shell corrente.

È possibile sfruttare questa funzione di assegnazione delle variabili per modificare $IFSsolo temporaneamente, il che ci consente di evitare l'intero gambit di salvataggio e ripristino come quello che viene fatto con la $OIFSvariabile nella prima variante. Ma la sfida che affrontiamo qui è che il comando che dobbiamo eseguire è esso stesso un semplice compito variabile, e quindi non implicherebbe una parola di comando per rendere il $IFScompito temporaneo. Potresti pensare a te stesso, beh, perché non aggiungere semplicemente una parola di comando no-op all'istruzione come la : builtinper rendere $IFStemporanea l' assegnazione? Questo non funziona perché renderebbe $arraytemporanea anche l' assegnazione:

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

Quindi, siamo effettivamente in un vicolo cieco, un po 'un catch-22. Ma, quando evalesegue il suo codice, lo esegue nell'ambiente shell, come se fosse un normale codice sorgente statico, e quindi possiamo eseguire l' $arrayassegnazione all'interno deleval dell'argomento affinché abbia effetto nell'ambiente shell, mentre l' $IFSassegnazione prefisso che è preceduto dal evalcomando non sopravviverà al evalcomando. Questo è esattamente il trucco che viene utilizzato nella seconda variante di questa soluzione:

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

Quindi, come puoi vedere, in realtà è un trucco abbastanza intelligente e realizza esattamente ciò che è richiesto (almeno per quanto riguarda l'esecuzione dell'incarico) in un modo piuttosto non ovvio. In realtà non sono contro questo trucco in generale, nonostante il coinvolgimento di eval; fai solo attenzione a virgolette singole per la stringa dell'argomento per proteggerti dalle minacce alla sicurezza.

Ma ancora una volta, a causa dell'agglomerazione di problemi "peggiore di tutti i mondi", questa è ancora una risposta errata ai requisiti del PO.


Risposta errata n. 6

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

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

Ehm, cosa? L'OP ha una variabile stringa che deve essere analizzata in un array. Questa "risposta" inizia con il contenuto integrale della stringa di input incollata in un array letterale. Immagino che sia un modo per farlo.

Sembra che il risponditore possa aver supposto che la $IFSvariabile influisca su tutto l'analisi bash in tutti i contesti, il che non è vero. Dal manuale di bash:

IFS     Il separatore di campo interno utilizzato per la divisione delle parole dopo l'espansione e per dividere le linee in parole con il comando incorporato read . Il valore predefinito è <spazio><tab> <nuova> .

Quindi la $IFSvariabile speciale viene effettivamente utilizzata solo in due contesti: (1) suddivisione delle parole che viene eseguita dopo l'espansione (che significa non quando si analizza il codice sorgente di bash) e (2) per dividere le linee di input in parole dall'integrato read.

Vorrei provare a renderlo più chiaro. Penso che potrebbe essere utile fare una distinzione tra analisi ed esecuzione . Bash deve prima analizzare il codice sorgente, che ovviamente è un evento di analisi , e successivamente esegue il codice, che è quando l'espansione entra in scena. L'espansione è davvero un evento di esecuzione . Inoltre, metto in discussione la descrizione della $IFSvariabile che ho appena citato sopra; piuttosto che dire che la divisione delle parole viene eseguita dopo l'espansione , direi che la divisione delle parole viene eseguita durante l' espansione o, forse ancora più precisamente, la divisione delle parole è parte diil processo di espansione. La frase "frazionamento di parole" si riferisce solo a questo passaggio di espansione; non dovrebbe mai essere usato per riferirsi all'analisi del codice sorgente di bash, anche se sfortunatamente i documenti sembrano gettare molte parole "split" e "words". Ecco un estratto rilevante dalla versione linux.die.net del manuale di bash:

L'espansione viene eseguita sulla riga di comando dopo che è stata suddivisa in parole. Ci sono sette tipi di espansione eseguita: espansione delle parentesi graffe , tilde espansione , espansione di parametro e variabile , sostituzione di comando , espansione aritmetica , suddivisione in parole , e espansione di percorso .

L'ordine delle espansioni è: espansione delle parentesi graffe; espansione della tilde, espansione di parametri e variabili, espansione aritmetica e sostituzione dei comandi (da sinistra a destra); divisione delle parole; e l'espansione del nome percorso.

Potresti argomentare la versione GNU del manuale funzioni leggermente meglio, poiché opta per la parola "token" anziché per "parole" nella prima frase della sezione Espansione:

L'espansione viene eseguita sulla riga di comando dopo che è stata suddivisa in token.

Il punto importante è $IFSche non cambia il modo in cui bash analizza il codice sorgente. L'analisi del codice sorgente di bash è in realtà un processo molto complesso che comporta il riconoscimento dei vari elementi della grammatica della shell, come sequenze di comandi, elenchi di comandi, pipeline, espansioni di parametri, sostituzioni aritmetiche e sostituzioni di comandi. Per la maggior parte, il processo di analisi bash non può essere modificato da azioni a livello di utente come assegnazioni di variabili (in realtà, ci sono alcune eccezioni minori a questa regola; ad esempio, vedere le variecompatxx impostazioni della shell, che può modificare alcuni aspetti del comportamento di analisi al volo). Le "parole" / "token" a monte risultanti da questo complesso processo di analisi vengono quindi espanse secondo il processo generale di "espansione" come suddiviso negli estratti della documentazione sopra, in cui la suddivisione delle parole del testo espanso (in espansione?) In downstream le parole sono semplicemente un passo di quel processo. La suddivisione in parole tocca solo il testo che è stato sputato da una precedente fase di espansione; non influisce sul testo letterale che è stato analizzato direttamente dalla fonte bytestream.


Risposta errata n. 7

string='first line
        second line
        third line'

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

Questa è una delle migliori soluzioni. Si noti che siamo tornati a utilizzare read. Non ho detto prima che readè inappropriato perché esegue due livelli di divisione, quando ne abbiamo solo bisogno? Il trucco qui è che puoi chiamare readin modo tale da fare effettivamente un solo livello di divisione, in particolare dividendo un solo campo per invocazione, il che richiede il costo di doverlo chiamare ripetutamente in un ciclo. È un po 'un gioco di prestigio, ma funziona.

Ma ci sono problemi. Primo: quando si fornisce almeno un argomento NAME a read, ignora automaticamente gli spazi bianchi iniziali e finali in ogni campo che è separato dalla stringa di input. Ciò si verifica indipendentemente dal fatto che $IFSsia impostato sul valore predefinito, come descritto in precedenza in questo post. Ora, l'OP potrebbe non preoccuparsene per il suo caso d'uso specifico, e in effetti potrebbe essere una caratteristica desiderabile del comportamento di analisi. Ma non tutti quelli che vogliono analizzare una stringa in campi lo vorranno. C'è una soluzione, tuttavia: un uso un po 'non ovvio di readè passare zero argomenti NAME . In questo caso, readmemorizzerà l'intera riga di input che ottiene dal flusso di input in una variabile denominata $REPLYe, come bonus, nonrimuovere lo spazio bianco iniziale e finale dal valore. Questo è un uso molto robusto readche ho sfruttato frequentemente nella mia carriera di programmatore di shell. Ecco una dimostrazione della differenza di comportamento:

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

Il secondo problema con questa soluzione è che in realtà non risolve il caso di un separatore di campo personalizzato, come lo spazio virgola dell'OP. Come in precedenza, i separatori multi-carattere non sono supportati, il che è una sfortunata limitazione di questa soluzione. Potremmo provare a dividere almeno sulla virgola specificando il separatore per l' -dopzione, ma guarda cosa succede:

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

Com'era prevedibile, lo spazio bianco circostante non contabilizzato è stato inserito nei valori di campo, e quindi questo dovrebbe essere corretto successivamente attraverso operazioni di taglio (questo potrebbe anche essere fatto direttamente nel ciclo while). Ma c'è un altro ovvio errore: manca l'Europa! Cosa gli è successo? La risposta è che readrestituisce un codice di ritorno non riuscito se colpisce end-of-file (in questo caso possiamo chiamarlo end-of-string) senza incontrare un terminatore di campo finale sul campo finale. Questo fa sì che il ciclo while si interrompa prematuramente e perdiamo il campo finale.

Tecnicamente questo stesso errore ha colpito anche gli esempi precedenti; la differenza è che il separatore di campo è stato considerato LF, che è l'impostazione predefinita quando non si specifica l' -dopzione, e il <<<meccanismo ("qui-stringa") aggiunge automaticamente un LF alla stringa appena prima che venga alimentato come input al comando. Quindi, in quei casi, abbiamo risolto accidentalmente il problema di un campo finale abbandonato aggiungendo involontariamente un terminatore fittizio aggiuntivo all'input. Chiamiamo questa soluzione la soluzione "dummy-terminator". Possiamo applicare manualmente la soluzione dummy-terminator per qualsiasi delimitatore personalizzato concatenando noi stessi contro la stringa di input quando lo istanziamo nella stringa here:

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

Lì, problema risolto. Un'altra soluzione è quella di interrompere il ciclo while se entrambi (1) hanno readrestituito un errore e (2) $REPLYsono vuoti, il che significa che readnon è stato in grado di leggere alcun carattere prima di colpire la fine del file. demo:

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

Questo approccio rivela anche il LF segreto che viene automaticamente aggiunto alla stringa qui <<<dall'operatore di reindirizzamento. Ovviamente potrebbe essere rimosso separatamente attraverso un'operazione di taglio esplicito come descritto un momento fa, ma ovviamente l'approccio manuale fittizio-terminatore lo risolve direttamente, quindi potremmo semplicemente andare con quello. La soluzione manuale dummy-terminator è in realtà abbastanza conveniente in quanto risolve entrambi questi due problemi (il problema del campo finale abbandonato e il problema LF aggiunto) in una volta sola.

Quindi, nel complesso, questa è una soluzione abbastanza potente. L'unica debolezza che rimane è la mancanza di supporto per i delimitatori multi-carattere, che affronterò più avanti.


Risposta errata n. 8

string='first line
        second line
        third line'

readarray -t lines <<<"$string"

(Questo è in realtà dallo stesso post di # 7 ; il risponditore ha fornito due soluzioni nello stesso post.)

L' readarrayincasso, che è sinonimo di mapfile, è l'ideale. È un comando incorporato che analizza un bytestream in una variabile di array in un colpo solo; nessun pasticcio con loop, condizionali, sostituzioni o altro. E non elimina di nascosto alcuno spazio bianco dalla stringa di input. E (se -Onon viene fornito) cancella convenientemente l'array di destinazione prima di assegnarlo. Ma non è ancora perfetto, quindi la mia critica è una "risposta sbagliata".

Innanzitutto, solo per toglierlo di mezzo, nota che, proprio come il comportamento di readquando si esegue l'analisi del campo, readarrayrilascia il campo finale se è vuoto. Ancora una volta, questo non è probabilmente un problema per il PO, ma potrebbe esserlo per alcuni casi d'uso. Tornerò su questo tra un momento.

In secondo luogo, come prima, non supporta i delimitatori multi-carattere. Ti darò una soluzione anche per un momento.

In terzo luogo, la soluzione scritta non analizza la stringa di input dell'OP e, di fatto, non può essere utilizzata così com'è per analizzarla. Espanderò anche questo momentaneamente.

Per le ragioni sopra esposte, considero ancora questa una "risposta sbagliata" alla domanda del PO. Di seguito darò quella che considero la risposta giusta.


Risposta esatta

Ecco un tentativo ingenuo di far funzionare # 8 semplicemente specificando l' -dopzione:

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

Vediamo che il risultato è identico al risultato ottenuto dall'approccio a doppia condizionale della readsoluzione di looping discussa nel n . 7 . Possiamo quasi risolverlo con il trucco manuale del finto terminale:

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

Il problema qui è che ha readarrayconservato il campo finale, poiché l' <<<operatore di reindirizzamento ha aggiunto l'LF alla stringa di input e quindi il campo finale non era vuoto (altrimenti sarebbe stato eliminato). Possiamo occuparcene eliminando esplicitamente l'elemento array finale dopo il fatto:

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

Gli unici due problemi che rimangono, che sono effettivamente correlati, sono (1) lo spazio bianco estraneo che deve essere tagliato, e (2) la mancanza di supporto per i delimitatori multi-carattere.

Lo spazio bianco potrebbe ovviamente essere ritagliato in seguito (ad esempio, vedi Come tagliare lo spazio bianco da una variabile Bash? ). Ma se riusciamo a hackerare un delimitatore multicharacter, ciò risolverebbe entrambi i problemi in un colpo solo.

Sfortunatamente, non esiste un modo diretto per far funzionare un delimitatore multicharacter. La migliore soluzione a cui ho pensato è di preelaborare la stringa di input per sostituire il delimitatore multicharacter con un delimitatore a carattere singolo che garantirà di non scontrarsi con il contenuto della stringa di input. L'unico carattere che ha questa garanzia è il byte NUL . Questo perché, in bash (anche se non in zsh, per inciso), le variabili non possono contenere il byte NUL. Questa fase di preelaborazione può essere eseguita in linea in una sostituzione di processo. Ecco come farlo usando awk :

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

Ecco, finalmente! Questa soluzione non dividerà erroneamente i campi nel mezzo, non taglierà prematuramente, non lascerà cadere i campi vuoti, non si corromperà sulle espansioni del nome file, non rimuoverà automaticamente gli spazi bianchi iniziali e finali, non lascerà un LF clandestino alla fine, non richiede loop e non si accontenta di un delimitatore a carattere singolo.


Soluzione di taglio

Infine, volevo dimostrare la mia soluzione di rifilatura abbastanza complessa usando l' -C callbackopzione oscura di readarray. Sfortunatamente, ho esaurito lo spazio contro il limite draconiano di posta di 30.000 caratteri di Stack Overflow, quindi non sarò in grado di spiegarlo. Lo lascerò come esercizio per il lettore.

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

8
Potrebbe anche essere utile notare (anche se comprensibilmente non c'era spazio per farlo) che l' -dopzione per readarrayprima appare in Bash 4.4.
fbicknel,

2
Ottima risposta (+1). Se cambi il tuo awk in awk '{ gsub(/,[ ]+|$/,"\0"); print }'ed elimini quella concatenazione della finale ", " , non devi passare attraverso la ginnastica per eliminare il record finale. Quindi: readarray -td '' a < <(awk '{ gsub(/,[ ]+/,"\0"); print; }' <<<"$string")su Bash che supporta readarray. Nota il metodo è Bash 4.4+ penso a causa della -dinreadarray
Dawg

3
@datUser È un peccato. La tua versione di bash deve essere troppo vecchia per readarray. In questo caso, è possibile utilizzare la seconda migliore soluzione integrata read. Mi riferisco a questo: a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,";(con la awksostituzione se hai bisogno del supporto del delimitatore multi-carattere). Fammi sapere se riscontri problemi; Sono abbastanza sicuro che questa soluzione dovrebbe funzionare su versioni piuttosto vecchie di bash, torna alla versione 2-qualcosa, rilasciata come due decenni fa.
1818

1
Caspita, che risposta brillante! Hee hee, la mia risposta: ho abbandonato la sceneggiatura di bash e acceso il pitone!
artfulrobot

1
@datUser bash su OSX è ancora bloccato a 3.2 (rilasciato intorno al 2007); Ho usato il bash trovato in Homebrew per ottenere versioni bash 4.X su OS X
JDS

222

Ecco un modo senza impostare IFS:

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

L'idea sta usando la sostituzione della stringa:

${string//substring/replacement}

per sostituire tutte le corrispondenze di $ sottostringa con spazi bianchi e quindi utilizzare la stringa sostituita per inizializzare un array:

(element1 element2 ... elementN)

Nota: questa risposta utilizza l' operatore split + glob . Pertanto, per impedire l'espansione di alcuni personaggi (come *) è una buona idea mettere in pausa questo gioco.


1
Ho usato questo approccio ... fino a quando mi sono imbattuto in una lunga corda da dividere. CPU al 100% per più di un minuto (poi l'ho ucciso). È un peccato perché questo metodo consente di dividere per una stringa, non un carattere in IFS.
Werner Lehmann,

Il 100% del tempo di CPU per uno di un minuto mi sembra che ci sia qualcosa di sbagliato da qualche parte. Quanto è durata quella stringa, ha dimensioni MB o GB? Penso, normalmente, se hai solo bisogno di una piccola suddivisione in stringhe, vuoi rimanere all'interno di Bash, ma se è un file enorme, eseguirò qualcosa come Perl per farlo.

12
ATTENZIONE: ho appena riscontrato un problema con questo approccio. Se hai un elemento chiamato * otterrai anche tutti gli elementi del tuo CWD. pertanto string = "1: 2: 3: 4: *" darà alcuni risultati imprevisti e potenzialmente pericolosi a seconda dell'implementazione. Non ho riscontrato lo stesso errore con (IFS = ',' read -a array <<< "$ string") e questo sembra sicuro da usare.
Dieter Gribnitz,

4
la quotazione ${string//:/ }impedisce l'espansione della shell
Andrew White,

1
Ho dovuto usare quanto segue su OSX: array=(${string//:/ })
Mark Thomson il

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

Stampa tre


8
In realtà preferisco questo approccio. Semplice.
Shrimpwagon

4
Ho copiato e incollato questo e non ha funzionato con l'eco, ma ha funzionato quando l'ho usato in un ciclo for.
Ben

2
Questo non funziona come indicato. @ Jmoney38 o Shrimpwagon se puoi incollarlo in un terminale e ottenere l'output desiderato, incolla qui il risultato.
abalter,

2
@abalter Funziona per me con a=($(echo $t | tr ',' "\n")). Stesso risultato con a=($(echo $t | tr ',' ' ')).
foglia

@procrastinator L'ho appena provato VERSION="16.04.2 LTS (Xenial Xerus)"in una bashshell e l'ultimo echostampa solo una riga vuota. Quale versione di Linux e quale shell stai usando? Sfortunatamente, non è possibile visualizzare la sessione terminale in un commento.
abalter,

29

A volte mi è capitato che il metodo descritto nella risposta accettata non funzionasse, specialmente se il separatore è un ritorno a capo.
In quei casi ho risolto in questo modo:

string='first line
second line
third line'

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

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

2
+1 Questo ha funzionato completamente per me. Avevo bisogno di mettere più stringhe, divise per una nuova riga, in un array, e read -a arr <<< "$strings"non funzionava IFS=$'\n'.
Stefan van den Akker,


Questo non risponde del tutto alla domanda originale.
Mike,

29

La risposta accettata funziona per i valori in una riga.
Se la variabile ha più righe:

string='first line
        second line
        third line'

Abbiamo bisogno di un comando molto diverso per ottenere tutte le linee:

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

O il readarray bash molto più semplice :

readarray -t lines <<<"$string"

Stampare tutte le linee è molto semplice sfruttando una funzione printf:

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

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

2
Mentre non tutte le soluzioni funzionano per ogni situazione, la tua menzione di readarray ... ha sostituito le mie ultime due ore con 5 minuti ... hai ottenuto il mio voto
Angry 84


6

La chiave per dividere la stringa in un array è il delimitatore multi carattere di ", ". Qualsiasi soluzione che utilizzaIFS per i delimitatori a più caratteri è intrinsecamente errata poiché IFS è un insieme di quei caratteri, non una stringa.

Se si assegna, IFS=", "la stringa si interromperà su ","OGNI O su " "qualsiasi combinazione di essi che non è una rappresentazione accurata del delimitatore di due caratteri di", " .

È possibile utilizzare awko sedper dividere la stringa, con la sostituzione del processo:

#!/bin/bash

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

È più efficiente usare un regex direttamente in Bash:

#!/bin/bash

str="Paris, France, Europe"

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

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

Con il secondo modulo, non esiste una sub shell e sarà intrinsecamente più veloce.


Modifica di bgoldst: ecco alcuni parametri di riferimento che confrontano la mia readarraysoluzione con la soluzione regex di dawg e ho anche incluso la readsoluzione per il diavolo di essa (nota: ho leggermente modificato la soluzione regex per una maggiore armonia con la mia soluzione) (vedi anche i miei commenti sotto il inviare):

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

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

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

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

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

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

Soluzione molto interessante! Non ho mai pensato di usare un loop su una partita regex, uso intelligente di $BASH_REMATCH. Funziona e in effetti evita la generazione di subshells. +1 da me. Tuttavia, a titolo di critica, la regex stessa è un po 'non ideale, in quanto sembra che tu sia stato costretto a duplicare parte del token delimitatore (in particolare la virgola) in modo da aggirare la mancanza di supporto per moltiplicatori non avidi (anche lookaround) in ERE (sapore regex "esteso" incorporato in bash). Questo lo rende un po 'meno generico e robusto.
Il

In secondo luogo, ho eseguito alcuni benchmarking e, sebbene le prestazioni siano migliori rispetto alle altre soluzioni per stringhe di piccole dimensioni, peggiorano in modo esponenziale a causa della ripetuta ricostruzione delle stringhe, diventando catastrofiche per stringhe molto grandi. Vedi la mia modifica alla tua risposta.
Il

@bgoldst: che bel punto di riferimento! A difesa della regex, per 10 o 100 di migliaia di campi (ciò che la regex sta dividendo) ci sarebbe probabilmente una qualche forma di registrazione (come \nrighe di testo delimitate) che comprende quei campi in modo che il catastrofico rallentamento non si verifichi. Se hai una stringa con 100.000 campi, forse Bash non è l'ideale ;-) Grazie per il benchmark. Ho imparato una cosa o due.
Dawg,

4

Soluzione di delimitatore multi-carattere puro bash.

Come altri hanno sottolineato in questo thread, la domanda del PO ha fornito un esempio di stringa delimitata da virgole da analizzare in un array, ma non ha indicato se era interessato solo a delimitatori di virgola, delimitatori a singolo carattere o multi-carattere Delimitatori.

Dal momento che Google tende a classificare questa risposta in corrispondenza o in cima ai risultati di ricerca, ho voluto fornire ai lettori una risposta forte alla domanda di delimitatori di più caratteri, poiché anche questa è menzionata in almeno una risposta.

Se stai cercando una soluzione a un problema del delimitatore a più caratteri, ti suggerisco di rivedere il post di Mallikarjun M , in particolare la risposta di gniourf_gniourf che fornisce questa elegante soluzione BASH pura usando l'espansione dei parametri:

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

Collegamento a commento citato / post di riferimento

Link alla domanda citata: come dividere una stringa su un delimitatore multi-carattere in bash?


1
Vedi il mio commento per un approccio simile ma migliorato.
xebeche,

3

Questo funziona per me su OSX:

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

Se la tua stringa ha delimitatore diverso, sostituisci solo quelli con spazio:

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

Semplice :-)


Funziona sia per Bash che per Zsh, il che è un vantaggio!
Elija W. Gagne,

2

Un altro modo per farlo senza modificare IFS:

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

Invece di modificare l'IFS in modo che corrisponda al delimitatore desiderato, possiamo sostituire tutte le occorrenze del delimitatore desiderato ", "con i contenuti di $IFSvia "${string//, /$IFS}".

Forse questo sarà lento per stringhe molto grandi?

Questo si basa sulla risposta di Dennis Williamson.


2

Mi sono imbattuto in questo post quando ho cercato di analizzare un input come: word1, word2, ...

nessuna delle precedenti mi ha aiutato. risolto usando awk. Se aiuta qualcuno:

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

1

Prova questo

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

È semplice. Se lo desideri, puoi anche aggiungere una dichiarazione (e rimuovere anche le virgole):

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

L'IFS viene aggiunto per annullare quanto sopra ma funziona senza di esso in una nuova istanza bash


1

Possiamo usare il comando tr per dividere la stringa nell'oggetto array. Funziona sia su MacOS che su Linux

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

Un'altra opzione utilizza il comando IFS

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

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

0

Usa questo:

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

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

3
Cattivo: soggetto a suddivisione delle parole e all'espansione del percorso. Per favore, non ravvivare vecchie domande con buone risposte per dare cattive risposte.
gniourf_gniourf,

2
Questa potrebbe essere una cattiva risposta, ma è comunque una risposta valida. Flaggers / recensori: per risposte errate come questa, downvote, non cancellare!
Scott Weldon,

2
@gniourf_gniourf Potresti spiegare perché è una cattiva risposta? Davvero non capisco quando fallisce.
George Sovetov,

3
@GeorgeSovetov: Come ho già detto, è soggetto alla divisione delle parole e all'espansione del percorso. Più in generale, scissione una stringa in una matrice come array=( $string )è (purtroppo molto comune) antipattern: suddivisione delle parole si verifica: string='Prague, Czech Republic, Europe'; Si verifica l'espansione del nome percorso: string='foo[abcd],bar[efgh]'fallirà se hai un file chiamato, ad esempio, foodo barfnella tua directory. L'unico uso valido di tale costrutto è quando stringè un glob.
gniourf_gniourf,

0

AGGIORNAMENTO: non farlo, a causa di problemi con eval.

Con un po 'meno cerimonia:

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

per esempio

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

4
eval è malvagio! non farlo.
Caesarsol,

1
Pfft. No. Se stai scrivendo degli script abbastanza grandi da essere importanti, stai sbagliando. Nel codice dell'applicazione, eval è malvagio. Nella shell scripting è comune, necessario e insignificante.
user1009908

2
metti un $nella tua variabile e vedrai ... scrivo molti script e non ho mai dovuto usare un singoloeval
caesarsol il

2
Hai ragione, questo è utilizzabile solo quando l'ingresso è noto per essere pulito. Non è una soluzione solida.
user1009908

L'unica volta che ho mai dovuto usare eval è stato per un'applicazione che avrebbe generato da sé il proprio codice / moduli ... E questo non ha mai avuto alcuna forma di input dell'utente ...
Angry 84

0

Ecco il mio trucco!

Dividere le stringhe per stringhe è una cosa piuttosto noiosa da fare usando bash. Quello che succede è che abbiamo approcci limitati che funzionano solo in alcuni casi (divisi per ";", "/", "." E così via) o che abbiamo una varietà di effetti collaterali negli output.

L'approccio che segue ha richiesto una serie di manovre, ma credo che funzionerà per la maggior parte delle nostre esigenze!

#!/bin/bash

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

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

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

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

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

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

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

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

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

    fi

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

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

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

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

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

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

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

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

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

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

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


"

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

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

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

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

0

Per elementi multilinea, perché non qualcosa del genere

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

a a INTERELEMENT b b INTERELEMENT

-1

Un altro modo sarebbe:

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

Ora i tuoi elementi sono memorizzati nell'array "arr". Per scorrere gli elementi:

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

1
Copro questa idea nella mia risposta ; vedi Risposta errata n. 5 (potresti essere particolarmente interessato alla mia discussione sul evaltrucco). La soluzione non viene $IFSimpostata sul valore spazio-virgola dopo il fatto.
17-17

-1

Dal momento che ci sono molti modi per risolvere questo, iniziamo definendo ciò che vogliamo vedere nella nostra soluzione.

  1. Bash fornisce un built-in readarray per questo scopo. Usiamolo.
  2. Evita brutti e inutili trucchi come cambiare IFS, fare il loop, usareeval o aggiungere un elemento aggiuntivo e rimuoverlo.
  3. Trova un approccio semplice e leggibile che possa essere facilmente adattato a problemi simili.

Il readarraycomando è più semplice da utilizzare con le nuove righe come delimitatore. Con altri delimitatori può aggiungere un ulteriore elemento alla matrice. L'approccio più pulito è innanzitutto adattare il nostro contributo in una forma che funzioni benereadarray funzioni prima di trasmetterlo.

L'input in questo esempio non ha un delimitatore multicharacter. Se applichiamo un po 'di buon senso, viene meglio compreso come input separato da virgola per il quale potrebbe essere necessario tagliare ogni elemento. La mia soluzione è quella di dividere l'input per virgola in più righe, tagliare ogni elemento e passare tutto a readarray.

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

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

-2

Un altro approccio può essere:

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

Dopo questo 'arr' è un array con quattro stringhe. Ciò non richiede la negoziazione di IFS o la lettura o qualsiasi altra cosa speciale, quindi molto più semplice e diretta.


Stesso antipattern (tristemente comune) delle altre risposte: soggetto a suddivisione delle parole ed espansione del nome file.
gniourf_gniourf,
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.