Dividir string em uma matriz no Bash


641

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 código simples, a velocidade do comando não importa. Como eu posso fazer isso?


22
Este é o hit número 1 do Google, mas há controvérsia na resposta porque, infelizmente, a pergunta é sobre delimitar em , (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/…
antak

Se você deseja mover uma string e não se importa em tê-la como uma matriz, cuté um comando útil do bash a ter em mente também. O separador é definível en.wikibooks.org/wiki/Cut Você também pode extrair dados de uma estrutura de registro de largura fixa. pt.wikipedia.org/wiki/Cut_(Unix) computerhope.com/unix/ucut.htm
JGFMK

Respostas:


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

Note-se que os caracteres $IFSsão tratados individualmente como separadores de modo que, neste caso, os campos podem ser separados por qualquer uma vírgula ou um espaço em vez da sequência de um dos dois caracteres. Curiosamente, os campos vazios não são criados quando o espaço de vírgula aparece na entrada 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 as matrizes Bash são esparsas. 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, as matrizes podem ser esparsas, 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 o 2.05b):

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

Compensações negativas maiores são selecionadas mais longe do final da matriz. Observe o espaço antes do sinal de menos no formulário antigo. É necessário.


15
Basta usar IFS=', ', então você não precisa remover os espaços separadamente. Teste:IFS=', ' read -a array <<< "Paris, France, Europe"; echo "${array[@]}"
l0b0

4
@ l0b0: Obrigado. Não sei o que estava pensando. Eu gosto de usar declare -p arraypara saída de teste, a propósito.
Pausado até novo aviso.

1
Isso não parece respeitar as aspas. Por exemplo, France, Europe, "Congo, The Democratic Republic of the"isso será dividido após o congo.
Yisrael Dov

2
@YisraelDov: Bash não tem como lidar sozinho com o CSV. Ele não pode dizer a diferença entre vírgulas dentro de aspas e aquelas fora delas. Você precisará usar uma ferramenta que entenda CSV, como uma lib em uma linguagem de nível superior, por exemplo, o módulo csv no Python.
Pausado até novo aviso.

5
str="Paris, France, Europe, Los Angeles"; IFS=', ' read -r -a array <<< "$str"será dividido array=([0]="Paris" [1]="France" [2]="Europe" [3]="Los" [4]="Angeles")como uma nota. Portanto, isso funciona apenas com campos sem espaços, pois IFS=', 'existe um conjunto de caracteres individuais - não um delimitador de string.
Dawg

333

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 $IFSvariá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 readsepara 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 $IFSjunto 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 $stringvariável contiver LFs, readserá necessário interrompa o processamento quando encontrar o primeiro LF. O readbuiltin processa apenas uma linha por chamada. Isso é verdade mesmo se você estiver canalizando ou redirecionando a entrada apenas para a readinstruçã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 readbuiltin 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 readinterno 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 readbuilt-in não é apropriado, e devemos evitá-lo.

3: Um problema potencial não óbvio com esta solução é que readsempre 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 reada divisão geral de palavras também usa a $IFSvariá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 $IFScaracteres 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 extglobestiver 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 -fantecipadamente 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 ( trou sed), pois a expansão de parâmetros é puramente uma operação interna do shell. (Além disso, para os tre sedsoluçõ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 echocomando 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 $IFSe 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 -fe 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 $IFSpara 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 -fe 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 -fe 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 $IFSdefiniçã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 evalchamada é 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 evaldessa 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 $IFSapenas temporariamente, o que nos permite evitar todo o lance de salvar e restaurar como o que está sendo feito com a $OIFSvariá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 $IFSatribuição temporária. Você pode pensar: por que não adicionar uma palavra de comando no-op à declaração como a : builtinpara tornar a $IFStarefa temporária? Isso não funciona porque tornaria a $arrayatribuiçã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 evalexecuta seu código, ele é executado no ambiente do shell, como se fosse um código-fonte estático normal, e, portanto, podemos executar a $arrayatribuição dentro do evalargumento para que ela entre em vigor no ambiente do shell, enquanto a $IFSatribuição de prefixo que O prefixo do evalcomando não sobreviverá ao evalcomando. 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 $IFSvariá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 $IFSvariá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 readbuilt-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 $IFSvariá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 é $IFSque 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 compatxxconfiguraçõ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 readmaneira 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 $IFSo 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, readarmazenará toda a linha de entrada obtida do fluxo de entrada em uma variável denominada $REPLYe, como bônus, ela nãotira o espaço em branco à esquerda e à esquerda do valor. Esse é um uso muito robusto, do readqual 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 -dopçã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 readretorna 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 -dopçã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) readretornarem falha e (2) $REPLYestiverem vazios, o que significa que readnã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 readarraybuiltin, 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 -Onã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 readfazer uma análise de campo, readarraydescarta 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 -dopçã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 readsoluçã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 readarraypreservou 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 callbackopçã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")

8
Também pode ser útil observar (embora seja compreensível que você não tenha espaço para fazê-lo) que a -dopção para readarrayaparecer pela primeira vez no Bash 4.4.
Fbicknel

2
Ótima resposta (+1). Se você alterar seu awk awk '{ gsub(/,[ ]+|$/,"\0"); print }'e eliminar a concatenação da final ", " , não precisará passar pela ginástica para eliminar o registro final. Então: readarray -td '' a < <(awk '{ gsub(/,[ ]+/,"\0"); print; }' <<<"$string")no Bash que suporta readarray. Observe o seu método é Bash 4.4 ou superior Eu acho que por causa da -demreadarray
dawg

3
@datUser Isso é lamentável. Sua versão do bash deve ser muito antiga para readarray. Nesse caso, você pode usar a segunda melhor solução integrada read. Estou me referindo a isso: a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,";(com a awksubstituição, se você precisar de suporte ao delimitador de vários caracteres). Deixe-me saber se você tiver algum problema; Tenho certeza de que essa solução deve funcionar em versões bastante antigas do bash, de volta à versão 2 - lançada há duas décadas.
bgoldst

1
Uau, que resposta brilhante! Hee hee, minha resposta: abandonou o script bash e disparou o python!
Artfulrobot

1
O @datUser bash no OSX ainda está parado na versão 3.2 (lançado em 2007); Eu usei a festa encontrados em Homebrew para obter 4.X versões festança no OS X
JDS

222

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 substituir todas as correspondências de $ substring por espaço em branco e, em seguida, usar a sequência substituída para inicializar uma matriz:

(element1 element2 ... elementN)

Nota: esta resposta faz uso do operador split + glob . Portanto, para impedir a expansão de alguns caracteres (como *), é uma boa ideia fazer uma pausa no globbing deste script.


1
Usei essa abordagem ... até encontrar uma longa sequência para dividir. CPU 100% por mais de um minuto (então eu matei). É uma pena, porque esse método permite dividir por uma sequência, não por algum caractere no IFS.
Werner Lehmann

O tempo de CPU de 100% por um minuto parece-me que deve haver algo errado em algum lugar. Quanto tempo durou essa string, tem MB ou GB? Acho que, normalmente, se você precisar apenas de uma pequena divisão de cadeias, deseja permanecer no Bash, mas se for um arquivo enorme, eu executaria algo como Perl para fazer isso.

12
AVISO: Ocorreu um problema com esta abordagem. Se você possui um elemento chamado *, também receberá todos os elementos do seu cwd. assim, string = "1: 2: 3: 4: *" fornecerá resultados inesperados e possivelmente perigosos, dependendo da sua implementação. Não obteve o mesmo erro com (IFS = ',' read -a array <<< "$ string") e este parece seguro de usar.
Dieter Gribnitz

4
citando ${string//:/ }evita desembolsar expansão
Andrew White

1
Eu tive que usar o seguinte no OSX: array=(${string//:/ })
Mark Thomson

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

Imprime três


8
Na verdade, eu prefiro essa abordagem. Simples.
shrimpwagon

4
Copiei e colei isso e ele não funcionou com eco, mas funcionou quando o usei em um loop for.
Ben

2
Isso não funciona como indicado. @ Jmoney38 ou shrimpwagon, se você puder colar isso em um terminal e obter a saída desejada, cole o resultado aqui.
abalter 30/08/16

2
@abalter Trabalha para mim com a=($(echo $t | tr ',' "\n")). Mesmo resultado com a=($(echo $t | tr ',' ' ')).
folha de

@procrastinator Acabei de experimentá-lo em VERSION="16.04.2 LTS (Xenial Xerus)"uma bashconcha, e o último echoapenas imprime uma linha em branco. Qual versão do Linux e qual shell você está usando? Infelizmente, não é possível exibir a sessão do terminal em um comentário.
abalter

29

Às vezes me ocorreu que o método descrito na resposta aceita não funcionou, especialmente se o separador for um retorno de carro.
Nesses casos, resolvi desta maneira:

string='first line
second line
third line'

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

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

2
+1 Isso funcionou completamente para mim. Eu precisava colocar várias strings, divididas por uma nova linha, em uma matriz e read -a arr <<< "$strings"não funcionou IFS=$'\n'.
Stefan van den Akker


Isso não responde bem à pergunta original.
Mike

29

A resposta aceita funciona para valores em uma linha.
Se a variável tiver várias linhas:

string='first line
        second line
        third line'

Precisamos de um comando muito diferente para obter todas as linhas:

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

Ou o muito mais simples readay do bash :

readarray -t lines <<<"$string"

Imprimir todas as linhas é muito fácil, aproveitando o recurso printf:

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

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

2
Embora nem toda solução funcione para todas as situações, sua menção ao readarray ... substituiu minhas últimas duas horas por 5 minutos ... você obteve meu voto
Irritado


6

A chave para dividir sua string em uma matriz é o delimitador de vários caracteres ", ". Qualquer solução usada IFSpara delimitadores de vários caracteres é inerentemente errada, pois o IFS é um conjunto desses caracteres, não uma sequência.

Se você atribuir IFS=", ", a sequência será interrompida em OU ","OU em " "qualquer combinação deles que não seja uma representação precisa do delimitador de dois caracteres de ", ".

Você pode usar awkou seddividir a sequência, com a substituição do processo:

#!/bin/bash

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

É mais eficiente usar um regex diretamente no 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...

Com o segundo formulário, não há sub shell e ele será inerentemente mais rápido.


Editar por bgoldst: Aqui estão alguns benchmarks que comparam minha readarraysolução à solução regex dawg e também incluí a readsolução para o problema (nota: eu modifiquei levemente a solução regex para maior harmonia com minha solução) (também veja meus comentários abaixo do postar):

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

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

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

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

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

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

Solução muito legal! Eu nunca pensei em usar um loop em uma correspondência regex, uso bacana de $BASH_REMATCH. Funciona e, de fato, evita sub-conchas. +1 de mim. No entanto, a título de crítica, o regex em si é um pouco não ideal, pois parece que você foi forçado a duplicar parte do token delimitador (especificamente a vírgula), para contornar a falta de suporte a multiplicadores não gananciosos (também pesquisas) no ERE (sabor regex "estendido" incorporado no bash). Isso o torna um pouco menos genérico e robusto.
bgoldst

Em segundo lugar, fiz alguns testes comparativos e, embora o desempenho seja melhor que as outras soluções para cadeias pequenas, piora exponencialmente devido à repetida reconstrução de cadeias, tornando-se catastrófico para cadeias muito grandes. Veja minha edição na sua resposta.
bgoldst

@bgoldst: Que referência legal! Em defesa do regex, para 10 ou 100 de milhares de campos (o que o regex está dividindo) provavelmente haveria alguma forma de registro (como \nlinhas de texto delimitadas) compreendendo esses campos, de modo que a desaceleração catastrófica provavelmente não ocorreria. Se você tem uma string com 100.000 campos - talvez o Bash não seja o ideal ;-) Obrigado pela referência. Eu aprendi uma coisa ou duas.
dawg 27/11

4

Solução delimitadora de vários caracteres do bash puro.

Como outros apontaram neste tópico, a pergunta do OP deu um exemplo de uma sequência delimitada por vírgula a ser analisada em uma matriz, mas não indicou se ele / ela estava interessado apenas em delimitadores de vírgula, delimitadores de caractere único ou multi-caractere delimitadores.

Como o Google tende a classificar essa resposta no topo ou perto dos resultados de pesquisa, eu queria fornecer aos leitores uma resposta forte à pergunta dos delimitadores de vários caracteres, pois isso também é mencionado em pelo menos uma resposta.

Se você está procurando uma solução para um problema de delimitador de vários caracteres, sugiro revisar a publicação de Mallikarjun M , em particular a resposta de gniourf_gniourf, que fornece esta solução BASH pura e elegante usando a expansão de parâmetros:

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

Link para o comentário citado / publicação referenciada

Link para a pergunta citada: Como dividir uma string em um delimitador de vários caracteres no bash?


1
Veja meu comentário para uma abordagem semelhante, mas aprimorada.
Xebeche 03/06/19

3

Isso funciona para mim no OSX:

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

Se sua string tiver um delimitador diferente, substitua-o primeiro por um espaço:

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

Simples :-)


Funciona para o Bash e o Zsh, o que é uma vantagem!
Elijah W. Gagne

2

Outra maneira de fazer isso sem modificar o IFS:

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

Em vez de alterar o IFS para corresponder ao delimitador desejado, podemos substituir todas as ocorrências do delimitador desejado ", "pelo conteúdo de $IFSvia "${string//, /$IFS}".

Talvez isso seja lento para cordas muito grandes?

Isso se baseia na resposta de Dennis Williamson.


2

Me deparei com este post ao analisar uma entrada como: word1, word2, ...

nenhuma das opções acima me ajudou. resolveu usando o awk. Se isso ajudar alguém:

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

1

Tente isto

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

É simples. Se desejar, você também pode adicionar uma declaração (e também remover as vírgulas):

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

O IFS é adicionado para desfazer o acima, mas funciona sem ele em uma nova instância do bash


1

Podemos usar o comando tr para dividir a string no objeto da matriz. Funciona em MacOS e Linux

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

Outra opção usa o comando IFS

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

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

0

Usa isto:

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

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

3
Ruim: sujeito a divisão de palavras e expansão de nome de caminho. Por favor, não revive perguntas antigas com boas respostas para dar más respostas.
gniourf_gniourf

2
Esta pode ser uma resposta ruim, mas ainda é uma resposta válida. Sinalizadores / revisores: para respostas incorretas como essa, faça
Scott Weldon

2
@gniourf_gniourf Você poderia explicar por que é uma resposta ruim? Eu realmente não entendo quando falha.
George Sovetov

3
@GeorgeSovetov: Como eu disse, está sujeito a divisão de palavras e expansão de nome de caminho. De modo mais geral, a divisão de uma cadeia numa matriz como array=( $string )é um (infelizmente muito comum) antipattern: palavra cisão ocorre: string='Prague, Czech Republic, Europe'; A expansão do nome do caminho ocorre: string='foo[abcd],bar[efgh]'falhará se você tiver um arquivo chamado, por exemplo, foodou barfem seu diretório. O único uso válido de uma construção desse tipo é quando stringé um glob.
gniourf_gniourf

0

ATUALIZAÇÃO: Não faça isso, devido a problemas com a avaliação.

Com um pouco menos de cerimônia:

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

por exemplo

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

4
eval é mau! não faça isso.
Caesarsol

1
Pfft. Não. Se você está escrevendo scripts grandes o suficiente para isso importar, está fazendo errado. No código do aplicativo, eval é mau. No script de shell, é comum, necessário e inconseqüente.
user1009908

2
colocar um $na sua variável e você vai ver ... eu escrever muitos scripts e eu nunca tive que usar um únicoeval
caesarsol

2
Você está certo, isso só é utilizável quando a entrada é conhecida por estar limpa. Não é uma solução robusta.
user1009908

A única vez que eu tive que usar eval, foi para um aplicativo que seria auto gerar seu próprio código / módulos ... e este nunca teve qualquer forma de entrada do usuário ...
irritado 84

0

Aqui está o meu hack!

Dividir strings por strings é uma coisa bastante chata de se fazer usando o bash. O que acontece é que temos abordagens limitadas que funcionam apenas em alguns casos (divididas por ";", "/", "." E assim por diante) ou que temos vários efeitos colaterais nos resultados.

A abordagem abaixo exigiu várias manobras, mas acredito que funcionará para a maioria das nossas necessidades!

#!/bin/bash

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

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

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

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

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

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

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

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

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

    fi

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

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

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

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

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

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

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

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

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

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

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


"

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

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

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

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

0

Para elementos com várias linhas, por que não algo como

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

a a INTERELEMENT b b INTERELEMENT

-1

Outra maneira seria:

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

Agora seus elementos são armazenados na matriz "arr". Para percorrer os elementos:

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

1
Eu cubro essa idéia na minha resposta ; veja Resposta errada nº 5 (você pode estar especialmente interessado na minha discussão sobre o evaltruque). Sua solução deixa $IFSdefinida como o valor de espaço de vírgula após o fato.
precisa saber é

-1

Como existem muitas maneiras de resolver isso, vamos começar definindo o que queremos ver em nossa solução.

  1. O Bash fornece um built-in readarraypara esse fim. Vamos usá-lo.
  2. Evite truques feios e desnecessários, como alterar IFS, repetir, usar evalou adicionar um elemento extra e removê-lo.
  3. Encontre uma abordagem simples e legível que possa ser facilmente adaptada a problemas semelhantes.

O readarraycomando é mais fácil de usar com novas linhas como delimitador. Com outros delimitadores, ele pode adicionar um elemento extra à matriz. A abordagem mais limpa é primeiro adaptar nossa entrada a um formulário que funcione bem readarrayantes de transmiti-la.

A entrada neste exemplo não possui um delimitador de vários caracteres. Se aplicarmos um pouco de bom senso, é melhor entender como entrada separada por vírgula, para a qual cada elemento pode precisar ser aparado. Minha solução é dividir a entrada por vírgula em várias linhas, aparar cada elemento e passar tudo para readarray.

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

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

-2

Outra abordagem pode ser:

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

Depois disso, 'arr' é uma matriz com quatro strings. Isso não requer lidar com o IFS, ler ou qualquer outro material especial, portanto, muito mais simples e direto.


O mesmo antipadrão (infelizmente comum) das outras respostas: sujeito a divisão de palavras e expansão de nome de arquivo.
gniourf_gniourf
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.