Todas as respostas a esta pergunta estão erradas de uma maneira ou de outra.
Resposta errada # 1
IFS=', ' read -r -a array <<< "$string"
1: Este é um mau uso de $IFS
. O valor da $IFS
variável não é usado como um único separador de cadeia de comprimento variável , mas como um conjunto de separadores de cadeia de caracteres únicos , em que cada campo que se read
separa da linha de entrada pode ser finalizado por qualquer caractere do conjunto (vírgula ou espaço, neste exemplo).
Na verdade, para os verdadeiros defensores, o significado completo de $IFS
é um pouco mais envolvido. No manual do bash :
O shell trata cada caractere do IFS como um delimitador e divide os resultados das outras expansões em palavras usando esses caracteres como terminadores de campo. Se o IFS não estiver definido ou seu valor for exatamente <espaço> <tabela> <nova linha> , o padrão e as sequências de <espaço> , <tabela> e <linha> no início e no final dos resultados das expansões anteriores são ignorados e qualquer sequência de caracteres IFS que não esteja no início ou no final serve para delimitar palavras. Se o IFS tiver um valor diferente do padrão, as seqüências dos caracteres de espaço em branco <espaço> , <guia> e <são ignorados no início e no final da palavra, desde que o caractere de espaço em branco esteja no valor de IFS (um caractere de espaço em branco do IFS ). Qualquer caractere no IFS que não seja espaço em branco do IFS , juntamente com qualquer caractere de espaço em branco do 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 sequência de um ou mais caracteres que são todos do conjunto de "caracteres de espaço em branco do IFS" (ou seja, o que for <espaço> , <tab> e <newline> ("nova linha", significando avanço de linha (LF) ) estão presentes em qualquer local $IFS
) ou (2) qualquer "caractere de espaço em branco do IFS" que esteja presente $IFS
junto com os "caracteres de espaço em branco do IFS" na linha de entrada.
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 sequência de entrada, mas podemos ter certeza de que o primeiro modo de separação que descrevi não está correto. Por exemplo, e se sua string de entrada 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: Mesmo se você usasse esta solução com um separador de caractere único (como uma vírgula por si só, ou seja, sem espaço a seguir ou outra bagagem), se o valor da $string
variável contiver LFs, read
será necessário interrompa o processamento quando encontrar o primeiro LF. O read
builtin processa apenas uma linha por chamada. Isso é verdade mesmo se você estiver canalizando ou redirecionando a entrada apenas para a read
instrução, como estamos fazendo neste exemplo com o mecanismo aqui-string e, portanto, a entrada não processada é garantida como perdida. O código que alimenta o read
builtin não tem conhecimento do fluxo de dados em sua estrutura de comando que o contém.
Você pode 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 o read
interno realmente faz dois níveis de divisão de entrada: primeiro em linhas e depois em campos. Como o OP deseja apenas um nível de divisão, esse uso do read
built-in não é apropriado, e devemos evitá-lo.
3: Um problema potencial não óbvio com esta solução é que read
sempre descarta o campo à direita se estiver vazio, embora, de outra forma, preserve os campos vazios. Aqui está uma demonstração:
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]="")
Talvez o OP não se importe com isso, mas ainda é uma limitação que vale a pena conhecer. Reduz a robustez e a generalidade da solução.
Esse problema pode ser resolvido anexando um delimitador à direita da cadeia de entrada antes de alimentá-lo read
, como demonstrarei mais adiante.
Resposta errada # 2
string="1:2:3:4:5"
set -f # avoid globbing (expansion of *).
array=(${string//:/ })
Idéia semelhante:
t="one,two,three"
a=($(echo $t | tr ',' "\n"))
(Nota: eu adicionei os parênteses ausentes em torno da substituição de comando que o atendedor parece ter omitido.)
Idéia semelhante:
string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)
Essas soluções utilizam a divisão de palavras em uma atribuição de matriz para dividir a sequência em campos. Curiosamente, assim como read
a divisão geral de palavras também usa a $IFS
variável especial, embora, neste caso, esteja implícito que ela esteja configurada com o valor padrão de <space><tab> <newline> e, portanto, qualquer sequência de um ou mais IFS caracteres (que são todos os caracteres de espaço em branco agora) é considerado um delimitador de campo.
Isso resolve o problema de dois níveis de divisão cometidos por read
, uma vez que a divisão de palavras por si só constitui apenas um nível de divisão. Mas, exatamente como antes, o problema aqui é que os campos individuais na sequência de entrada já podem conter $IFS
caracteres e, portanto, seriam divididos incorretamente durante a operação de divisão de palavras. Isso não é o caso de nenhuma das seqüências de entrada de amostra fornecidas por esses respondentes (que conveniente ...), mas é claro que isso não muda o fato de que qualquer base de código que usasse esse idioma correria o risco de explodindo se essa suposição fosse violada em algum momento abaixo da linha. Mais uma vez, considere meu contra-exemplo de 'Los Angeles, United States, North America'
(ou 'Los Angeles:United States:North America'
).
Além disso, a palavra de divisão é normalmente seguido por expansão nome de arquivo ( aka expansão de nome aka englobamento), que, se feito, seria palavras potencialmente corruptos contendo os caracteres *
, ?
ou [
seguido por ]
(e, se extglob
estiver definido, fragmentos entre parênteses precedida por ?
, *
, +
, @
, ou !
) combinando-os com objetos do sistema de arquivos e expandindo as palavras ("globs") de acordo. O primeiro desses três respondedores inteligentemente resolveu esse problema, executando set -f
antecipadamente para desativar o globbing. Tecnicamente, isso funciona (embora você provavelmente deva adicionarset +f
depois, para reativar o globbing do código subsequente 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 de string para 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ê usar esta solução, é melhor usar a forma ${string//:/ }
"substituição de padrão" da expansão de parâmetros , em vez de invocar uma substituição de comando (que bifurca o shell), iniciar um pipeline e executando um executável externo ( tr
ou sed
), pois a expansão de parâmetros é puramente uma operação interna do shell. (Além disso, para os tr
e sed
soluções, a variável de entrada deve ser duas vezes citado no interior da substituição de comando, caso contrário repartição de palavras levaria efeito no echo
comando e, potencialmente, suje os valores de campo Além disso, a. $(...)
Forma de substituição de comando é preferível para o velho`...`
forma, pois simplifica o aninhamento de substituições de comando e permite um melhor destaque da sintaxe pelos 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 a mesma que a nº 2 . A diferença é que o atendedor assumiu que os campos são delimitados por dois caracteres, um dos quais sendo representado no padrão $IFS
e o outro não. Ele resolveu esse caso bastante específico removendo o caractere não representado pelo 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 representado pelo IFS sobrevivente.
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 removê-lo e depois dependendo do caractere de espaço para a divisão do campo está simplesmente errado. Mais uma vez, considere minhas contra-exemplo: 'Los Angeles, United States, North America'
.
Além disso, novamente, a expansão do nome do arquivo pode corromper as palavras expandidas, mas isso pode ser evitado desativando temporariamente o globbing para a atribuição com set -f
e depois 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 aos itens 2 e 3, na medida em que utiliza a divisão de palavras para concluir o trabalho, mas agora o código define explicitamente $IFS
para conter apenas o delimitador de campo de caractere único presente na cadeia de entrada. Deve-se repetir que isso não pode funcionar para delimitadores de campo com vários caracteres, 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 quase chega a ser perfeito. Os campos não podem ser divididos acidentalmente 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 como descrito anteriormente, embora mais uma vez isso possa ser resolvido envolvendo a instrução crítica em set -f
e set +f
.
Outro problema em potencial é que, como o LF se qualifica como um "caractere de espaço em branco do IFS", conforme definido anteriormente, todos os campos vazios serão perdidos, assim como nos itens 2 e 3 . Obviamente, isso não seria um problema se o delimitador não fosse um "caractere de espaço em branco do IFS" e, dependendo do aplicativo, isso pode não ter importância, mas vicia a generalidade da solução.
Então, para resumir, supondo que você tenha um delimitador de um caractere e ele seja um "caractere de espaço em branco do IFS" ou não se importe com campos vazios, envolva a instrução crítica em set -f
e set +f
, então, esta solução funcionará , mas caso contrário não.
(Além disso, para fins de informação, a atribuição de um LF a uma variável no bash pode ser feita mais facilmente com a $'...'
sintaxe, por exemplo IFS=$'\n';
.)
Resposta errada # 5
countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"
Idéia semelhante:
IFS=', ' eval 'array=($string)'
Essa solução é efetivamente um cruzamento entre o número 1 (na $IFS
definição de espaço entre vírgulas) e o número 2-4 (na medida em que utiliza a divisão de palavras para dividir a sequência em campos). Por causa disso, ele sofre com a maioria dos problemas que afligem todas as respostas erradas acima, como o pior de todos os mundos.
Além disso, em relação à segunda variante, pode parecer que a eval
chamada é completamente desnecessária, pois seu argumento é uma literal de string com aspas simples e, portanto, é estaticamente conhecida. Mas, na verdade, há um benefício muito óbvio em usar eval
dessa maneira. Normalmente, quando você executar um comando simples que consiste em uma atribuição de variável única , ou seja, sem uma palavra de comando real que se lhe segue, a atribuição tem efeito no ambiente shell:
IFS=', '; ## changes $IFS in the shell environment
Isso é verdade mesmo que o comando simples envolva várias atribuições de variáveis; novamente, desde que não haja uma palavra de comando, todas as atribuições de variáveis afetam o ambiente do shell:
IFS=', ' array=($countries); ## changes both $IFS and $array in the shell environment
Porém, se a atribuição de variável estiver anexada a um nome de comando (eu gosto de chamar isso de "atribuição de prefixo"), ela não afetará o ambiente do shell e, em vez disso, afetará 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ções relevantes do manual do bash :
Se nenhum nome de comando resultar, as designações de variáveis afetam o ambiente atual do shell. Caso contrário, as variáveis são adicionadas ao ambiente do comando executado e não afetam o ambiente atual do shell.
É possível explorar esse recurso de atribuição de variáveis para alterar $IFS
apenas temporariamente, o que nos permite evitar todo o lance de salvar e restaurar como o que está sendo feito com a $OIFS
variável na primeira variante. Mas o desafio que enfrentamos aqui é que o comando que precisamos executar é em si uma mera atribuição de variáveis e, portanto, não envolveria uma palavra de comando para tornar a $IFS
atribuição temporária. Você pode pensar: por que não adicionar uma palavra de comando no-op à declaração como a : builtin
para tornar a $IFS
tarefa temporária? Isso não funciona porque tornaria a $array
atribuição temporária também:
IFS=', ' array=($countries) :; ## fails; new $array value never escapes the : command
Então, estamos efetivamente em um impasse, um pouco complicado. Mas, quando eval
executa seu código, ele é executado no ambiente do shell, como se fosse um código-fonte estático normal, e, portanto, podemos executar a $array
atribuição dentro do eval
argumento para que ela entre em vigor no ambiente do shell, enquanto a $IFS
atribuição de prefixo que O prefixo do eval
comando não sobreviverá ao eval
comando. 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 realiza exatamente o que é necessário (pelo menos no que diz respeito à efetivação da atribuição) de uma maneira bastante óbvia. Na verdade, não sou contra esse truque em geral, apesar do envolvimento de eval
; apenas tenha cuidado entre aspas simples a sequência de argumentos para se proteger contra ameaças à segurança.
Mais uma vez, devido à aglomeração de problemas "pior de todos os mundos", essa ainda é uma resposta errada ao requisito 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 cadeia que precisa ser analisada em uma matriz. Essa "resposta" começa com o conteúdo literal da sequência de entrada colada em um literal de matriz. Eu acho que é uma maneira de fazer isso.
Parece que o respondente pode ter assumido que a $IFS
variável afeta toda a análise do bash em todos os contextos, o que não é verdade. No manual do bash:
IFS O Separador de Campo Interno usado para dividir palavras após a expansão e para dividir linhas em palavras com o comando read builtin. O valor padrão é <space><tab> <newline> .
Portanto, a $IFS
variável especial é realmente usada apenas em dois contextos: (1) divisão de palavras que é executada após a expansão (ou seja, não ao analisar o código-fonte do bash) e (2) para dividir as linhas de entrada em palavras pelo read
built-in.
Deixe-me tentar deixar isso mais claro. Eu acho que pode ser 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, quando ocorre a expansão na imagem. A expansão é realmente um evento de execução . Além disso, discordo da descrição da $IFS
variável que acabei de citar acima; em vez de dizer que a divisão de palavras é realizada após a expansão , eu diria que a divisão de palavras é realizada durante a expansão ou, talvez ainda mais precisamente, a divisão de palavras faz parte deo processo de expansão. A frase "divisão de palavras" refere-se apenas a esta 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 usar as palavras "split" e "words" muito. Aqui está um trecho relevante da versão linux.die.net do manual do bash:
A expansão é realizada na linha de comando após ter sido dividida em palavras. Existem tipos sete da expansão realizada: expansão cinta , tilde expansão , parâmetro e expansão de variáveis , substituição de comandos , expansão aritmética , dividindo palavra , e expansão de nome .
A ordem das expansões é: expansão de chaves; expansão de til, expansão de parâmetro e variável, expansão aritmética e substituição de comando (feita da esquerda para a direita); divisão de palavras; e expansão do nome do caminho.
Você poderia argumentar que a versão GNU do manual é um pouco melhor, pois ela opta pela palavra "tokens" em vez de "words" na primeira frase da seção Expansão:
A expansão é realizada na linha de comando após ter sido dividida em tokens.
O ponto importante é $IFS
que não altera a maneira como o bash analisa o código-fonte. A análise do código-fonte do bash é, na verdade, um processo muito complexo que envolve o reconhecimento dos vários elementos da gramática do 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 maioria das vezes, o processo de análise do bash não pode ser alterado por ações no nível do usuário, como atribuições de variáveis (na verdade, existem algumas pequenas exceções a esta regra; por exemplo, consulte as várias compatxx
configurações de shell, que pode alterar certos aspectos do comportamento de análise on-the-fly). As "palavras" / "tokens" upstream resultantes desse complexo processo de análise são expandidas de acordo com o processo geral de "expansão", conforme detalhado nos trechos da documentação acima, onde a divisão da palavra do texto expandido (expansível?) Para o downstream palavras é simplesmente uma etapa desse processo. A divisão de palavras apenas toca no texto que foi cuspido em uma etapa de expansão anterior; isso não afeta o texto literal que foi analisado diretamente da fonte pelo testream.
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 voltamos a usar read
. Eu não disse anteriormente que isso read
é inapropriado porque realiza dois níveis de divisão, quando precisamos apenas de um? O truque aqui é que você pode chamar de read
maneira que efetivamente apenas faça um nível de divisão, especificamente dividindo apenas um campo por invocação, o que exige o custo de ter que chamá-lo repetidamente em um loop. É um truque, mas funciona.
Mas há problemas. Primeiro: quando você fornece pelo menos um argumento NAMEread
, ele ignora automaticamente os espaços em branco à esquerda e à direita em cada campo que é separado da sequência de entrada. Isso ocorre se $IFS
o valor padrão é definido ou não, conforme descrito anteriormente nesta postagem. Agora, o OP pode não se importar com isso para seu caso de uso específico e, de fato, pode ser um recurso desejável do comportamento de análise. Mas nem todo mundo que deseja analisar uma seqüência de caracteres em campos deseja isso. Existe uma solução, no entanto: Um uso não óbvio de read
é passar zero argumentos NAME . Nesse caso, read
armazenará toda a linha de entrada obtida do fluxo de entrada em uma variável denominada $REPLY
e, como bônus, ela nãotira o espaço em branco à esquerda e à esquerda do valor. Esse é um uso muito robusto, do read
qual tenho explorado frequentemente em minha carreira de programação de shell. Aqui está uma demonstração da diferença de 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
O segundo problema com esta solução é que na verdade não trata o caso de um separador de campo personalizado, como o espaço de vírgula do OP. Como antes, os separadores de vários caracteres não são suportados, o que é uma limitação infeliz dessa solução. Poderíamos tentar pelo menos dividir por vírgula especificando o separador para a -d
opção, mas veja o que acontece:
string='Paris, France, Europe';
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France")
Previsivelmente, o espaço em branco ao redor não contabilizado foi atraído para os valores de 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 final de string) sem encontrar um terminador de campo final no campo final. Isso faz com que o loop while pare prematuramente e perdemos o campo final.
Tecnicamente, esse mesmo erro também afetou os exemplos anteriores; a diferença é que o separador de campo foi considerado LF, que é o padrão quando você não especifica a -d
opção, e o <<<
mecanismo ("aqui-string") anexa automaticamente um LF à string imediatamente antes de alimentá-lo como entrada para o comando. Portanto, nesses casos, resolvemos acidentalmente o problema de um campo final descartado anexando inadvertidamente um terminador fictício adicional à entrada. Vamos chamar essa solução de solução "dummy-terminator". Podemos aplicar a solução dummy-terminator manualmente para qualquer delimitador personalizado, concatenando-a na cadeia de entrada quando instanciamos na cadeia here:
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,"; declare -p a;
declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")
Lá, problema resolvido. Outra solução é interromper o loop while apenas se ambos (1) read
retornarem falha e (2) $REPLY
estiverem vazios, o que significa que read
não foi possível ler nenhum caractere antes de atingir o final do arquivo. 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')
Essa abordagem também revela o LF secreto que é automaticamente anexado à string here pelo <<<
operador de redirecionamento. É claro que ele poderia ser retirado separadamente por meio de uma operação explícita de corte, conforme descrito há pouco, mas obviamente a abordagem manual de terminação fictícia resolve isso diretamente, para que possamos continuar com isso. A solução manual de terminação fictícia é realmente bastante conveniente, pois resolve esses dois problemas (o problema do campo final descartado e o problema de LF anexado) de uma só vez.
Portanto, no geral, esta é uma solução bastante poderosa. A única fraqueza restante é a falta de suporte para delimitadores de vários caracteres, que abordarei mais adiante.
Resposta errada # 8
string='first line
second line
third line'
readarray -t lines <<<"$string"
(Na verdade, é da mesma postagem que o nº 7 ; o atendedor forneceu duas soluções na mesma postagem.)
O readarray
builtin, que é um sinônimo mapfile
, é ideal. É um comando interno que analisa um bytestream em uma variável de matriz de uma só vez; sem mexer com loops, condicionais, substituições ou qualquer outra coisa. E não tira clandestinamente nenhum espaço em branco da string de entrada. E (se -O
não for fornecido), limpa convenientemente a matriz de destino antes de atribuir a ela. Mas ainda não é perfeito, daí a minha crítica a ela como uma "resposta errada".
Primeiro, apenas para tirar isso do caminho, observe que, assim como o comportamento de read
fazer uma análise de campo, 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. Voltarei a isso daqui a pouco.
Segundo, como antes, ele não suporta delimitadores de vários caracteres. Vou dar uma correção para isso em um momento também.
Terceiro, a solução escrita não analisa a cadeia de entrada do OP e, de fato, não pode ser usada como está para analisá-la. 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 a resposta certa.
Resposta correta
Aqui está uma tentativa ingênua de fazer o # 8 funcionar apenas especificando a -d
opção:
string='Paris, France, Europe';
readarray -td, a <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')
Vemos que o resultado é idêntico ao resultado obtido pela abordagem de dupla condicional da read
solução de loop discutida no item 7 . Quase podemos resolver isso com o truque manual do terminador fictício:
readarray -td, a <<<"$string,"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe" [3]=$'\n')
O problema aqui é que readarray
preservou o campo à direita, pois o <<<
operador de redirecionamento anexou o LF à sequência de entrada e, portanto, o campo à direita não estava vazio (caso contrário, teria sido descartado). Podemos resolver isso desabilitando explicitamente o elemento final da matriz após o fato:
readarray -td, a <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")
Os únicos dois problemas que permanecem, que estão realmente relacionados, são (1) o espaço em branco estranho que precisa ser aparado e (2) a falta de suporte para delimitadores de vários caracteres.
É claro que o espaço em branco pode ser aparado posteriormente (por exemplo, consulte Como aparar o espaço em branco de uma variável Bash? ). Mas se pudermos hackear um delimitador de vários caracteres, isso resolveria os dois problemas de uma só vez.
Infelizmente, não há uma maneira direta de fazer funcionar um delimitador de vários caracteres. A melhor solução que eu pensei é pré-processar a sequência de entrada para substituir o delimitador de vários caracteres por um delimitador de um caractere que garantirá não colidir com o conteúdo da sequência de entrada. O único caractere que tem essa garantia é o byte NUL . Isso ocorre porque, no bash (embora não no zsh, aliás), as variáveis não podem conter o byte NUL. Esta etapa de pré-processamento pode ser realizada 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 -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")
Lá finalmente! Esta solução não dividirá erroneamente os campos no meio, não cortará prematuramente, não eliminará campos vazios, não se danificará nas expansões de nomes de arquivos, não removerá automaticamente os espaços em branco à esquerda e à direita, não deixará um LF clandestino no final, não requer loops e não aceita um delimitador de caractere único.
Solução de aparar
Por fim, queria demonstrar minha própria solução de aparar bastante complexa usando a -C callback
opção obscura de readarray
. Infelizmente, fiquei sem espaço contra o draconiano limite de 30.000 caracteres do Stack Overflow, por isso não poderei explicar. 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 -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")
,
(vírgula-espaço) e não um único caractere , como vírgula. Se você está interessado apenas neste último, é fácil seguir as respostas aqui: stackoverflow.com/questions/918886/…