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 $IFS
variabile 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 read
divide 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 $IFS
con 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 $string
variabile succede a contenere LF, allora read
sarà interrompere l'elaborazione una volta che incontra il primo LF. L' read
integrato elabora solo una riga per invocazione. Questo è vero anche se si sta tubazioni o il reindirizzamento di ingresso solo per la read
dichiarazione, 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 read
builtin 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' read
integrato 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 read
builtin non è appropriato e dovremmo evitarlo.
3: Un potenziale problema non ovvio con questa soluzione è che read
elimina 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 read
suddivisione generale delle parole usa anche la $IFS
variabile 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 $IFS
caratteri 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 -f
anticipo 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 ( tr
o sed
), poiché l'espansione dei parametri è puramente un'operazione interna alla shell. (Inoltre, per le soluzioni tr
e sed
, la variabile di input dovrebbe essere racchiusa tra virgolette all'interno della sostituzione del comando; altrimenti la suddivisione delle parole avrebbe effetto nel echo
comando 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 $IFS
e 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 -f
e 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 $IFS
per 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 -f
e 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 -f
e set +f
quindi 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 $IFS
su 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 eval
chiamata 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 eval
in 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 $IFS
solo temporaneamente, il che ci consente di evitare l'intero gambit di salvataggio e ripristino come quello che viene fatto con la $OIFS
variabile 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 $IFS
compito temporaneo. Potresti pensare a te stesso, beh, perché non aggiungere semplicemente una parola di comando no-op all'istruzione come la : builtin
per rendere $IFS
temporanea l' assegnazione? Questo non funziona perché renderebbe $array
temporanea 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 eval
esegue il suo codice, lo esegue nell'ambiente shell, come se fosse un normale codice sorgente statico, e quindi possiamo eseguire l' $array
assegnazione all'interno deleval
dell'argomento affinché abbia effetto nell'ambiente shell, mentre l' $IFS
assegnazione prefisso che è preceduto dal eval
comando non sopravviverà al eval
comando. 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 $IFS
variabile 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 $IFS
variabile 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 $IFS
variabile 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 è $IFS
che 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 read
in 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 $IFS
sia 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, read
memorizzerà l'intera riga di input che ottiene dal flusso di input in una variabile denominata $REPLY
e, come bonus, nonrimuovere lo spazio bianco iniziale e finale dal valore. Questo è un uso molto robusto read
che 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' -d
opzione, 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 read
restituisce 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' -d
opzione, 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 read
restituito un errore e (2) $REPLY
sono vuoti, il che significa che read
non è 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' readarray
incasso, 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 -O
non 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 read
quando si esegue l'analisi del campo, readarray
rilascia 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' -d
opzione:
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 read
soluzione 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 readarray
conservato 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 callback
opzione 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")
,
(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/...