Dividir string em um array no Bash

Em um script Bash eu gostaria de dividir uma linha em pedaços e armazená-los em uma matriz.

A linha:

Paris, France, Europe 

Eu gostaria de tê-los em uma matriz como esta:

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

Eu gostaria de usar um código simples, a velocidade do comando não importa. Como eu posso fazer isso?

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

Observe que os caracteres em $IFS são tratados individualmente como separadores, de modo que, nesse caso, os campos podem ser separados por uma vírgula ou por um espaço, e não pela seqüência dos dois caracteres. Curiosamente, os campos vazios não são criados quando o espaço de vírgula aparece na input porque o espaço é tratado especialmente.

Para acessar um elemento individual:

 echo "${array[0]}" 

Para iterar sobre os elementos:

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

Para obter o índice e o valor:

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

O último exemplo é útil porque os arrays Bash são esparsos. Em outras palavras, você pode excluir um elemento ou adicionar um elemento e, em seguida, os índices não são contíguos.

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

Para obter o número de elementos em uma matriz:

 echo "${#array[@]}" 

Como mencionado acima, os arrays podem ser escassos, portanto você não deve usar o comprimento para obter o último elemento. Veja como você pode no Bash 4.2 e posterior:

 echo "${array[-1]}" 

em qualquer versão do Bash (de algum lugar após 2.05b):

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

Deslocamentos negativos maiores selecionam mais longe do final da matriz. Observe o espaço antes do sinal de menos no formulário antigo. É necessário.

Aqui está uma maneira sem definir o 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 

A ideia é usar a substituição de string:

 ${string//substring/replacement} 

para replace todas as correspondências de $ substring por espaços em branco e depois usar a string substituída para inicializar uma matriz:

 (element1 element2 ... elementN) 

Nota: esta resposta faz uso do operador split + glob . Portanto, para evitar a expansão de alguns caracteres (como * ), é recomendável pausar a globalização desse script.

Todas as respostas a essa pergunta estão erradas de uma forma ou de outra.


Resposta errada # 1

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

1: Este é um uso incorreto do $IFS . O valor da variável $IFS não é considerado como um único separador de string de tamanho variável , mas é tomado como um conjunto de separadores de string de caractere único , em que cada campo que read divisões da linha de input pode ser terminado por qualquer caractere no conjunto (vírgula ou espaço, neste exemplo).

Na verdade, para os verdadeiros defensores, o significado completo do $IFS é um pouco mais complicado. Do manual bash :

O shell trata cada caractere do IFS como um delimitador e divide os resultados de outras expansões em palavras usando esses caracteres como terminadores de campo. Se o IFS não estiver definido ou seu valor for exatamente , o padrão, as seqüências de , e no início e no final dos resultados das expansões anteriores são ignorados e qualquer seqüência de caracteres IFS não no início ou no final serve para delimitar as palavras. Se o IFS tiver um valor diferente do padrão, as sequências dos caracteres de espaço em branco , e serão ignoradas no início e no final da palavra, contanto que o caractere de espaço em branco esteja no valor de IFS (um caractere de espaço em branco IFS ). Qualquer caractere no IFS que não seja espaço em branco do IFS , juntamente com qualquer caractere de espaço em branco IFS adjacente, delimita um campo. Uma sequência de caracteres de espaço em branco do IFS também é tratada como um delimitador. Se o valor do IFS for nulo, nenhuma divisão de palavras ocorrerá.

Basicamente, para valores não nulos não-padrão de $IFS , os campos podem ser separados com (1) uma seqüência de um ou mais caracteres que são todos do conjunto de "caracteres de espaço em branco IFS" (isto é, qualquer um dos , e ("newline" que significa feed de linha (LF) ) estão presentes em qualquer lugar no $IFS ), ou (2) qualquer caractere de espaço em branco não IFS presente em $IFS junto com qualquer Os caracteres de espaço em branco do IFS "o cercam na linha de input.

Para o OP, é possível que o segundo modo de separação que descrevi no parágrafo anterior seja exatamente o que ele deseja para sua string de input, mas podemos ter certeza de que o primeiro modo de separação que descrevi não está correto. Por exemplo, e se a string de input dele fosse 'Los Angeles, United States, North America' ?

 IFS=', ' read -ra a <<<'Los Angeles, United States, North America'; declare -pa; ## declare -aa=([0]="Los" [1]="Angeles" [2]="United" [3]="States" [4]="North" [5]="America") 

2: Mesmo se você usasse essa solução com um separador de caractere único (como uma vírgula por si só, ou seja, sem espaço ou outra bagagem), se o valor da variável $string contiver quaisquer LFs, então read interromperá o processamento assim que encontrar o primeiro LF. A read incorporada processa apenas uma linha por chamada. Isso é verdadeiro mesmo se você estiver canalizando ou redirecionando a input apenas para a instrução de read , como estamos fazendo neste exemplo com o mecanismo here-string e, portanto, é garantido que a input não processada será perdida. O código que alimenta a read incorporada não tem conhecimento do stream de dados dentro de sua estrutura de comando.

Você poderia argumentar que é improvável que isso cause um problema, mas ainda assim, é um risco sutil que deve ser evitado, se possível. Isso é causado pelo fato de que a read incorporada realmente faz dois níveis de divisão de input: primeiro em linhas e depois em campos. Como o OP só quer um nível de divisão, esse uso da read interna não é apropriado, e devemos evitá-lo.

3: Um possível problema não óbvio com essa solução é que a read sempre deixa o campo à direita se estiver vazio, embora preserve os campos vazios de outra forma. Aqui está uma demonstração:

 string=', , a, , b, c, , , '; IFS=', ' read -ra a <<<"$string"; declare -pa; ## declare -aa=([0]="" [1]="" [2]="a" [3]="" [4]="b" [5]="c" [6]="" [7]="") 

Talvez o OP não se importe com isso, mas ainda é uma limitação que vale a pena conhecer. Reduz a robustez e generalidade da solução.

Esse problema pode ser resolvido adicionando um delimitador à direita da input antes de alimentá-lo para read , como demonstrarei mais adiante.


Resposta errada # 2

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

Ideia semelhante:

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

(Nota: eu adicionei os parênteses em falta em torno da substituição de comando que o respondente parece ter omitido.)

Ideia semelhante:

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

Essas soluções aproveitam a divisão de palavras em uma atribuição de matriz para dividir a sequência em campos. Curiosamente, assim como a read , a divisão geral de palavras também usa a variável especial $IFS , embora neste caso esteja implícito que ela esteja configurada com seu valor padrão de e, portanto, qualquer sequência de um ou mais caracteres IFS (que são todos os caracteres de espaço em branco agora) são considerados um delimitador de campo.

Isso resolve o problema de dois níveis de divisão cometidos por read , pois a divisão de palavras por si só constitui apenas um nível de divisão. Mas, assim como antes, o problema aqui é que os campos individuais na cadeia de input já podem conter caracteres $IFS e, portanto, seriam divididos indevidamente durante a operação de divisão de palavras. Este não é o caso de nenhuma das strings de input de amostra fornecidas por esses respondentes (quão conveniente ...), mas é claro que isso não muda o fato de que qualquer código base que usasse esse idioma correria o risco de explodindo se essa suposição já foi violada em algum ponto abaixo da linha. Mais uma vez, considere o meu contra-exemplo de 'Los Angeles, United States, North America' (ou 'Los Angeles:United States:North America' ).

Além disso, a divisão de palavras normalmente é seguida por expansão de nome de arquivo ( também conhecida como expansão de nome de caminho aka globbing), o que, se feito, poderia corromper as palavras contendo os caracteres * ? , ou [ seguido por ] (e, se extglob for definido, fragments entre parênteses precedidos por ? * , + , @ ou ! ) combinando-os com objects do sistema de arquivos e expandindo as palavras ("globs") de acordo. O primeiro desses três respondedores diminuiu inteligentemente esse problema executando set -f antecipadamente para desativar o globbing. Tecnicamente isso funciona (embora você provavelmente deva adicionar set +f posteriormente para reativar o globbing para código subseqüente que pode depender dele), mas é indesejável ter que mexer com as configurações globais do shell para hackear uma operação básica de análise string-to-array no código local.

Outro problema com esta resposta é que todos os campos vazios serão perdidos. Isso pode ou não ser um problema, dependendo do aplicativo.

Nota: Se você for usar essa solução, é melhor usar o formulário ${string//:/ } "pattern conversion" de expansão de parâmetro , em vez de dar o trabalho de chamar uma substituição de comando (que bifurca o shell ), iniciando um pipeline e executando um executável externo ( tr ou sed ), já que a expansão de parâmetros é puramente uma operação interna do shell. (Além disso, para as soluções tr e sed , a variável de input deve ter aspas duplas dentro da substituição do comando; caso contrário, a divisão de palavras entraria em vigor no comando echo e potencialmente atrapalharia os valores do campo. Além disso, o $(...) A forma de substituição de comando é preferível à forma antiga `...` , uma vez que simplifica o aninhamento de substituições de comandos e permite melhor realce de syntax por editores de texto.


Resposta errada # 3

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

Esta resposta é quase igual a # 2 . A diferença é que o respondente assumiu que os campos são delimitados por dois caracteres, sendo um deles representado no padrão $IFS e o outro não. Ele resolveu esse caso bastante específico removendo o caractere não representado por IFS usando uma expansão de substituição de padrão e, em seguida, usando a divisão de palavras para dividir os campos no caractere delimitador sobrevivente representado por IFS.

Esta não é uma solução muito genérica. Além disso, pode-se argumentar que a vírgula é realmente o caractere delimitador "primário" aqui, e que despojá-lo e depois depender do caractere de espaço para divisão de campo é simplesmente errado. Mais uma vez, considere o meu contra-exemplo: 'Los Angeles, United States, North America' .

Além disso, novamente, a expansão de nome de arquivo pode corromper as palavras expandidas, mas isso pode ser evitado desativando temporariamente a globbing para a atribuição com set -f e, em seguida, set +f .

Além disso, novamente, todos os campos vazios serão perdidos, o que pode ou não ser um problema, dependendo do aplicativo.


Resposta errada # 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" 

Isso é semelhante a # 2 e # 3 , pois usa a divisão de palavras para realizar o trabalho, somente agora o código define explicitamente o $IFS para conter apenas o delimitador de campo de caractere único presente na cadeia de input. Deve ser repetido que isso não pode funcionar para delimitadores de campos multicaracteres, como o delimitador de espaço de vírgula do OP. Mas para um delimitador de caractere único como o LF usado neste exemplo, ele realmente chega perto de ser perfeito. Os campos não podem ser involuntariamente divididos no meio, como vimos com respostas erradas anteriores, e há apenas um nível de divisão, conforme necessário.

Um problema é que a expansão do nome de arquivo corromperá as palavras afetadas conforme descrito anteriormente, embora, mais uma vez, isso possa ser resolvido envolvendo a declaração crítica em set -f e set +f .

Outro problema em potencial é que, como o LF se qualifica como "caractere de espaço em branco do IFS", conforme definido anteriormente, todos os campos vazios serão perdidos, assim como em # 2 e # 3 . Obviamente, isso não será um problema se o delimitador for um "caractere de espaço em branco IFS" diferente e, dependendo da aplicação, pode não importar de qualquer maneira, mas ele viciará a generalidade da solução.

Então, para resumir, supondo que você tenha um delimitador de um caractere e seja um não "caractere de espaço em branco IFS" ou não se preocupe com campos vazios, e set -f declaração crítica em set -f e set +f , então esta solução funciona, mas caso contrário não.

(Além disso, para fins informativos, atribuir um LF a uma variável no bash pode ser feito mais facilmente com a syntax $'...' , por exemplo, IFS=$'\n'; )


Resposta errada # 5

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

Ideia semelhante:

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

Esta solução é efetivamente um cruzamento entre # 1 (em que define $IFS para espaço de vírgula) e # 2-4 (em que usa a divisão de palavras para dividir a cadeia em campos). Por causa disso, ele sofre com a maioria dos problemas que afligem todas as respostas erradas acima, mais ou menos como o pior dos mundos.

Além disso, em relação à segunda variante, pode parecer que a chamada eval é completamente desnecessária, uma vez que seu argumento é um literal de cadeia de aspas simples e, portanto, é estaticamente conhecido. Mas, na verdade, há um benefício não muito óbvio em usar o eval dessa maneira. Normalmente, quando você executa um comando simples que consiste apenas em uma atribuição de variável, ou seja, sem uma palavra de comando real que a segue, a atribuição entra em vigor no ambiente do shell:

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

Isso é verdade mesmo se o comando simples envolver várias atribuições de variables; novamente, desde que não exista uma palavra de comando, todas as atribuições de variables ​​afetam o ambiente do shell:

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

Mas, se a atribuição da variável estiver anexada a um nome de comando (eu gosto de chamar isso de "atribuição de prefixo"), ela não afeta o ambiente do shell e afeta apenas o ambiente do comando executado, independentemente de ser um ou externo:

 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 

Citação relevante do manual da bash :

Se nenhum nome de comando resultar, as atribuições de variables ​​afetarão o ambiente de shell atual. Caso contrário, as variables ​​serão adicionadas ao ambiente do comando executado e não afetarão o ambiente atual do shell.

É possível explorar este recurso de atribuição de variables ​​para alterar $IFS apenas temporariamente, o que nos permite evitar todo o gambito de salvar e restaurar como o que está sendo feito com a variável $OIFS na primeira variante. Mas o desafio que enfrentamos aqui é que o comando que precisamos executar é em si uma mera atribuição de variável e, portanto, não envolveria uma palavra de comando para tornar temporária a atribuição $IFS . Você pode pensar por si mesmo, bem, por que não apenas adicionar uma palavra de comando no-op à declaração como : builtin para tornar a atribuição $IFS temporária? Isso não funciona porque isso tornaria a atribuição $array temporária também:

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

Então, estamos efetivamente em um impasse, um pouco difícil. Mas, quando eval executa seu código, ele o executa no ambiente shell, como se fosse um código fonte estático normal e, portanto, podemos executar a atribuição $array dentro do argumento eval para que ele tenha efeito no ambiente shell, enquanto a atribuição de prefixo $IFS que é prefixada ao comando eval não sobreviverá ao comando eval . Este é exatamente o truque que está sendo usado na segunda variante desta solução:

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

Então, como você pode ver, é realmente um truque inteligente e consegue exatamente o que é necessário (pelo menos no que diz respeito à efetivação da atribuição) de uma maneira não óbvia. Na verdade, não sou contra esse truque, apesar do envolvimento de eval ; apenas tenha cuidado para citar a string de argumento para proteger contra ameaças de segurança.

Mas, novamente, por causa da aglomeração de problemas do "pior de todos os mundos", essa ainda é uma resposta errada à exigência do OP.


Resposta errada # 6

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

Hum ... o que? O OP tem uma variável de string que precisa ser analisada em um array. Essa "resposta" começa com o conteúdo textual da cadeia de input colada em um literal de matriz. Eu acho que é uma maneira de fazer isso.

Parece que o respondente pode ter assumido que a variável $IFS afeta todos os bash parsing em todos os contextos, o que não é verdade. Do manual bash:

IFS O separador de campo interno que é usado para a divisão de palavras após a expansão e para dividir as linhas em palavras com o comando read builtin. O valor padrão é .

Portanto, a variável especial $IFS é, na verdade, usada apenas em dois contextos: (1) divisão de palavras executada após a expansão (ou seja, não ao analisar o código-fonte bash) e (2) para dividir linhas de input em palavras pela read incorporada.

Deixe-me tentar deixar isso mais claro. Acho que seria bom fazer uma distinção entre análise e execução . O Bash deve primeiro analisar o código-fonte, que obviamente é um evento de análise , e depois executa o código, que é quando a expansão entra em cena. Expansão é realmente um evento de execução . Além disso, discordo da descrição da variável $IFS que acabei de citar acima; em vez de dizer que a divisão de palavras é executada após a expansão , eu diria que a divisão de palavras é executada durante a expansão ou, talvez ainda mais precisamente, a divisão de palavras faz parte do processo de expansão. A frase "divisão de palavras" refere-se apenas a essa etapa de expansão; ele nunca deve ser usado para se referir à análise do código-fonte do bash, embora infelizmente os documentos pareçam lançar muito em torno das palavras "split" e "words". Aqui está um trecho relevante da versão linux.die.net do manual do bash:

A expansão é executada na linha de comando depois de ter sido dividida em palavras. Há sete tipos de expansão executados: expansão de chave , expansão de til , expansão de parâmetro e variável , substituição de comando , expansão aritmética , divisão de palavras e expansão de nome de caminho .

A ordem das expansões é: expansão de brace; expansão do til, expansão de parâmetros e variables, expansão aritmética e substituição de comando (feito da esquerda para a direita); divisão de palavras; e expansão do caminho.

Você poderia argumentar que a versão GNU do manual é um pouco melhor, pois opta pela palavra "tokens" em vez de "words" na primeira sentença da seção Expansion:

A expansão é executada na linha de comando depois de ter sido dividida em tokens.

O ponto importante é que o $IFS não altera a forma como o bash analisa o código-fonte. A análise do código-fonte bash é, na verdade, um processo muito complexo que envolve o reconhecimento de vários elementos da gramática de shell, como seqüências de comandos, listas de comandos, pipelines, expansões de parâmetros, substituições aritméticas e substituições de comandos. Na maior parte, o processo de análise bash não pode ser alterado por ações no nível do usuário, como atribuições de variables ​​(na verdade, há algumas pequenas exceções a essa regra; por exemplo, consulte as várias configurações de shell compatxx , que podem alterar certos aspectos do comportamento de análise no vôo). As "palavras" / "tokens" upstream que resultam desse complexo processo de análise são então expandidas de acordo com o processo geral de "expansão", como descrito nos trechos da documentação acima, onde a divisão de palavras do texto expandido (expandindo?) Para jusante palavras é simplesmente um passo desse processo. A divisão de palavras só toca o texto que foi cuspido de uma etapa de expansão anterior; Ele não afeta o texto literal que foi analisado diretamente do bytestream de origem.


Resposta errada # 7

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

Esta é uma das melhores soluções. Observe que estamos de volta ao uso de read . Eu não disse antes que a read é inadequada porque realiza dois níveis de divisão, quando precisamos apenas de um? O truque aqui é que você pode chamar read de tal forma que ele efetivamente faça apenas um nível de divisão, especificamente dividindo apenas um campo por chamada, o que requer o custo de ter que chamá-lo repetidamente em um loop. É um pouco de truque, mas funciona.

Mas existem problemas. Primeiro: quando você fornece pelo menos um argumento NAME para read , ele automaticamente ignora os espaços em branco inicial e final em cada campo que é separado da string de input. Isso ocorre se $IFS está configurado para seu valor padrão ou não, conforme descrito anteriormente neste post. Agora, o OP pode não se importar com isso para seu caso de uso específico e, de fato, pode ser uma característica desejável do comportamento de análise. Mas nem todo mundo que quer analisar uma string em campos vai querer isso. Há uma solução, no entanto: Um uso não óbvio de read é para passar zero argumentos NAME . Nesse caso, read armazenará toda a linha de input que recebe do stream de input em uma variável denominada $REPLY e, como um bônus, não tira o espaço em branco inicial e final do valor. Este é um uso muito robusto de read que eu tenho explorado com frequência na minha carreira de programação shell. Aqui está uma demonstração da diferença de comportamento:

 string=$' ab \ncd \nef '; ## input string a=(); while read -r line; do a+=("$line"); done <<<"$string"; declare -pa; ## declare -aa=([0]="ab" [1]="cd" [2]="ef") ## read trimmed surrounding whitespace a=(); while read -r; do a+=("$REPLY"); done <<<"$string"; declare -pa; ## declare -aa=([0]=" ab " [1]=" cd " [2]=" ef ") ## no trimming 

O segundo problema com essa solução é que ela não aborda o caso de um separador de campo personalizado, como o espaço de vírgula do OP. Como antes, os separadores multicharacteres não são suportados, o que é uma limitação infeliz desta solução. Poderíamos pelo menos dividir em vírgula, especificando o separador para a opção -d , mas veja o que acontece:

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

Previsivelmente, o espaço em branco circundante não contabilizado foi puxado para os valores do campo e, portanto, isso teria que ser corrigido posteriormente por meio de operações de corte (isso também poderia ser feito diretamente no loop while). Mas há outro erro óbvio: a Europa está faltando! O que aconteceu com isso? A resposta é que read retorna um código de retorno com falha se atingir o final do arquivo (neste caso, podemos chamá-lo de fim de cadeia) sem encontrar um finalizador de campo final no campo final. Isso faz com que o loop while se quebre prematuramente e perdemos o campo final.

Tecnicamente, esse mesmo erro afligiu os exemplos anteriores também; a diferença é que o separador de campos foi considerado como LF, que é o padrão quando você não especifica a opção -d , e o mecanismo <<< ("here-string") anexa automaticamente um LF à string apenas antes de alimentá-lo como input para o comando. Assim, nesses casos, acidentalmente resolvemos o problema de um campo final perdido, involuntariamente anexando um terminador fictício adicional à input. Vamos chamar essa solução de "terminador fictício". Nós podemos aplicar manualmente a solução dummy-terminator para qualquer delimitador customizado, concatenando-a contra a string de input quando a instanciamos na string here:

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

Lá, problema resolvido. Outra solução é apenas quebrar o while-loop se ambos (1) read falha retornada e (2) $REPLY estiver vazio, significando que a read não foi capaz de ler nenhum caractere antes de atingir o fim do arquivo. Demonstração:

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

Essa abordagem também revela o LF secreto que é automaticamente anexado à string here pelo operador de redirecionamento <<< . Claro que poderia ser separado separadamente por meio de uma operação de corte explícita, como descrito há pouco, mas obviamente a abordagem manual do terminador-manequim resolve isso diretamente, então poderíamos ir com isso. A solução manual de terminação simulada é realmente bastante conveniente na medida em que resolve ambos os dois problemas (o problema do campo final-eliminado e o problema da LF acrescentado) de uma só vez.

Então, no geral, esta é uma solução bastante poderosa. Só resta fraqueza é a falta de suporte para delimitadores multiparacteres, que abordarei mais adiante.


Resposta errada # 8

 string='first line second line third line' readarray -t lines <<<"$string" 

(Isto é, na verdade, da mesma postagem como # 7 ; o respondente forneceu duas soluções no mesmo post.)

O readarray builtin, que é um sinônimo de mapfile , é ideal. É um comando interno que analisa um bytestream em uma variável de matriz em um único disparo; sem mexer com loops, condicionais, substituições ou qualquer outra coisa. E não tira sub-repticiamente nenhum espaço em branco da string de input. E (se -O não é dado) convenientemente limpa o array de destino antes de atribuir a ele. Mas ainda não é perfeito, daí a minha crítica a isso como uma "resposta errada".

Primeiro, apenas para tirar isso do caminho, observe que, assim como o comportamento da read ao fazer a análise de campo, o readarray descarta o campo à direita se estiver vazio. Novamente, isso provavelmente não é uma preocupação para o OP, mas pode ser para alguns casos de uso. Eu voltarei a isso em um momento.

Segundo, como antes, ele não suporta delimitadores multiparacteres. Eu vou dar uma correção para isso em um momento também.

Terceiro, a solução como escrita não analisa a cadeia de input do OP e, de fato, ela não pode ser usada como está para analisá-la. Eu vou expandir isso momentaneamente também.

Pelas razões acima, ainda considero que esta é uma "resposta errada" à pergunta do OP. Abaixo, darei o que considero ser a resposta certa.


Resposta correta

Aqui está uma tentativa ingênua de fazer o # 8 funcionar simplesmente especificando a opção -d :

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

Vemos que o resultado é idêntico ao resultado obtido da abordagem de dupla condicional da solução de read loop discutida no item 7 . Podemos quase resolver isso com o truque manual dummy-terminator:

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

O problema aqui é que readarray preservou o campo à direita, já que o operador de redirecionamento <<< anexou o LF à string de input e, portanto, o campo à direita não estava vazio (caso contrário, ele teria sido eliminado). Podemos cuidar disso desabilitando explicitamente o elemento da matriz final após o fato:

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

Os únicos dois problemas que permanecem, que são realmente relacionados, são (1) o espaço em branco externo que precisa ser aparado, e (2) a falta de suporte para delimitadores multiparacterísticos.

O espaço em branco poderia, é claro, ser aparado posteriormente (por exemplo, veja Como aparar espaços em branco a partir de uma variável Bash? ). Mas, se conseguirmos hackear um delimitador multicharacter, isso resolveria os dois problemas em um único disparo.

Infelizmente, não há nenhuma maneira direta de fazer funcionar um delimitador multicharacter. A melhor solução que pensei é pré-processar a cadeia de input para replace o delimitador multicharacter com um delimitador de caractere único que será garantido para não colidir com o conteúdo da cadeia de input. O único caractere que tem essa garantia é o byte NUL . Isso ocorre porque, no bash (embora não no zsh, incidentalmente), as variables ​​não podem conter o byte NUL. Essa etapa de pré-processamento pode ser feita em linha em uma substituição de processo. Veja como fazer isso usando o awk :

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

Lá finalmente! Esta solução não irá erroneamente dividir os campos no meio, não irá cortar prematuramente, não irá deixar campos vazios, não irá se corromper nas expansões de nomes de arquivos, não irá automaticamente remover espaços em branco iniciais e finais, não deixará um LF clandestino no final, não requer loops e não se conforma com um delimitador de caractere único.


Solução de recorte

Por último, eu queria demonstrar minha própria solução de aparamento bastante intrincada usando a opção obscura de -C callback de -C callback do readarray . Infelizmente, fiquei sem espaço com o limite draconiano de postagens de 30.000 caracteres do Stack Overflow, então não poderei explicá-lo. Vou deixar isso como um exercício para o leitor.

 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 -pa; ## declare -aa=([0]="Paris" [1]="France" [2]="Europe") 
 t="one,two,three" a=($(echo "$t" | tr ',' '\n')) echo "${a[2]}" 

Prints three

Sometimes it happened to me that the method described in the accepted answer didn’t work, especially if the separator is a carriage return.
In those cases I solved in this way:

 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 

The accepted answer works for values in one line.
If the variable has several lines:

 string='first line second line third line' 

We need a very different command to get all lines:

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

Or the much simpler bash readarray :

 readarray -t lines <<<"$string" 

Printing all lines is very easy taking advantage of a printf feature:

 printf ">[%s]\n" "${lines[@]}" >[first line] >[ second line] >[ third line] 

This is similar to the approach by Jmoney38, but using sed:

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

Prints 1

The key to splitting your string into an array is the multi character delimiter of ", " . Any solution using IFS for multi character delimiters is inherently wrong since IFS is a set of those characters, not a string.

If you assign IFS=", " then the string will break on EITHER "," OR " " or any combination of them which is not an accurate representation of the two character delimiter of ", " .

You can use awk or sed to split the string, with process substitution:

 #!/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 

It is more efficient to use a regex you directly 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... 

With the second form, there is no sub shell and it will be inherently faster.


Edit by bgoldst: Here are some benchmarks comparing my readarray solution to dawg's regex solution, and I also included the read solution for the heck of it (note: I slightly modified the regex solution for greater harmony with my solution) (also see my comments below the post):

 ## 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 -ii=-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 -in=$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 -in=$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 ## 

Tente isso

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

It’s simple. If you want, you can also add a declare (and also remove the commas):

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

The IFS is added to undo the above but it works without it in a fresh bash instance

Usa isto:

 countries='Paris, France, Europe' OIFS="$IFS" IFS=', ' array=($countries) IFS="$OIFS" #${array[1]} == Paris #${array[2]} == France #${array[3]} == Europe 

Here’s my hack!

Splitting strings by strings is a pretty boring thing to do using bash. What happens is that we have limited approaches that only work in a few cases (split by “;”, “/”, “.” and so on) or we have a variety of side effects in the outputs.

The approach below has required a number of maneuvers, but I believe it will work for most of our needs!

 #!/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  -c \"\dt\" \" \" \list or \l: list all databases \dt: list all tables in the current database \" [Ref.: https://dba.stackexchange.com/questions/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 

Another way to do it without modifying IFS:

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

Rather than changing IFS to match our desired delimiter, we can replace all occurrences of our desired delimiter ", " with contents of $IFS via "${string//, /$IFS}" .

Maybe this will be slow for very large strings though?

This is based on Dennis Williamson's answer.

Another approach can be:

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

After this ‘arr’ is an array with four strings. This doesn’t require dealing IFS or read or any other special stuff hence much simpler and direct.

UPDATE: Don’t do this, due to problems with eval.

With slightly less ceremony:

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

por exemplo

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

Outra maneira seria:

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

Now your elements are stored in “arr” array. To iterate through the elements:

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