substitute - split string to array in bash




Como faço para dividir uma string em um delimitador no Bash? (20)

Eu tenho essa string armazenada em uma variável:

IN="[email protected];[email protected]"

Agora eu gostaria de dividir as cordas por ; delimitador para que eu tenha:

ADDR1="[email protected]"
ADDR2="[email protected]"

Eu não preciso necessariamente das variáveis ADDR2 e ADDR2 . Se eles são elementos de uma matriz que é ainda melhor.

Depois de sugestões das respostas abaixo, acabei com o seguinte, que é o que eu estava depois:

#!/usr/bin/env bash

IN="[email protected];[email protected]"

mails=$(echo $IN | tr ";" "\n")

for addr in $mails
do
    echo "> [$addr]"
done

Saída:

> [[email protected].com]
> [[email protected].com]

Houve uma solução envolvendo configuração Internal_field_separator (IFS) para ; . Não tenho certeza do que aconteceu com essa resposta, como você redefinir o IFS volta ao padrão?

RE: solução IFS , eu tentei isso e funciona, eu mantenho o IFS antigo e restauro:

IN="[email protected];[email protected]"

OIFS=$IFS
IFS=';'
mails2=$IN
for x in $mails2
do
    echo "> [$x]"
done

IFS=$OIFS

BTW, quando eu tentei

mails2=($IN)

Eu só tenho a primeira corda quando imprimi-lo em loop, sem colchetes em torno de $IN funciona.


Resposta compatível

Para essa pergunta, já existe muita maneira diferente de fazer isso no bash . Mas o bash tem muitos recursos especiais , chamados de bashism que funcionam bem, mas que não funcionam em nenhum outro shell .

Em particular, arrays , array associativo e substituição de padrões são bashisms puros e podem não funcionar sob outros shells .

No meu Debian GNU / Linux , existe um shell padrão chamado dash , mas eu conheço muitas pessoas que gostam de usar o ksh .

Finalmente, em uma situação muito pequena, existe uma ferramenta especial chamada busybox com seu próprio interpretador de shell ( ash ).

String solicitada

A amostra da cadeia na pergunta SO é:

IN="[email protected];[email protected]"

Como isso poderia ser útil com espaços em branco e como espaços em branco poderiam modificar o resultado da rotina, eu prefiro usar essa string de amostra:

 IN="[email protected];[email protected];Full Name <[email protected]>"

Cadeia de divisão baseada no delimitador no bash (versão> = 4.2)

Sob o bash puro , podemos usar arrays e IFS :

var="[email protected];[email protected];Full Name <[email protected]>"

oIFS="$IFS"
IFS=";"
declare -a fields=($var)
IFS="$oIFS"
unset oIFS

IFS=\; read -a fields <<<"$IN"

Usando esta sintaxe no bash recente, não altere o $IFS para a sessão atual, mas apenas para o comando atual:

set | grep ^IFS=
IFS=$' \t\n'

Agora a string var é dividida e armazenada em uma matriz ( fields nomeados):

set | grep ^fields=\\\|^var=
fields=([0]="[email protected]" [1]="[email protected]" [2]="Full Name <[email protected]>")
var='[email protected];[email protected];Full Name <[email protected]>'

Poderíamos solicitar conteúdo variável com declare -p :

declare -p IN fields
declare -- IN="[email protected];[email protected];Full Name <[email protected]>"
declare -a fields=([0]="[email protected]" [1]="[email protected]" [2]="Full Name <[email protected]>")

read é a maneira mais rápida de fazer a divisão, porque não há garfos e nenhum recurso externo é chamado.

A partir daí, você pode usar a sintaxe que já conhece para processar cada campo:

for x in "${fields[@]}";do
    echo "> [$x]"
    done
> [[email protected].com]
> [[email protected].com]
> [Full Name <[email protected].org>]

ou soltar cada campo após o processamento (gosto dessa abordagem de mudança ):

while [ "$fields" ] ;do
    echo "> [$fields]"
    fields=("${fields[@]:1}")
    done
> [[email protected].com]
> [[email protected].com]
> [Full Name <[email protected].org>]

ou mesmo para impressão simples (sintaxe mais curta):

printf "> [%s]\n" "${fields[@]}"
> [[email protected].com]
> [[email protected].com]
> [Full Name <[email protected].org>]

Atualização: recente bash > = 4.4

Você poderia jogar com o mapfile :

mapfile -td \; fields < <(printf "%s\0" "$IN")

Esta sintaxe preserva caracteres especiais, novas linhas e campos vazios!

Se você não se importa com campos vazios, você pode:

mapfile -td \; fields <<<"$IN"
fields=("${fields[@]%$'\n'}")   # drop '\n' added by '<<<'

Mas você poderia usar campos através da função:

myPubliMail() {
    printf "Seq: %6d: Sending mail to '%s'..." $1 "$2"
    # mail -s "This is not a spam..." "$2" </path/to/body
    printf "\e[3D, done.\n"
}

mapfile < <(printf "%s\0" "$IN") -td \; -c 1 -C myPubliMail

(Nota: \0 no final da string de formatação são inúteis enquanto você não se importa com campos vazios no final da string)

mapfile < <(echo -n "$IN") -td \; -c 1 -C myPubliMail

Vai renderizar algo como:

Seq:      0: Sending mail to '[email protected]', done.
Seq:      1: Sending mail to '[email protected]', done.
Seq:      2: Sending mail to 'Full Name <[email protected]>', done.

Ou soltar nova linha adicionada pela sintaxe <<< bash em função:

myPubliMail() {
    local seq=$1 dest="${2%$'\n'}"
    printf "Seq: %6d: Sending mail to '%s'..." $seq "$dest"
    # mail -s "This is not a spam..." "$dest" </path/to/body
    printf "\e[3D, done.\n"
}

mapfile <<<"$IN" -td \; -c 1 -C myPubliMail

Irá renderizar a mesma saída:

Seq:      0: Sending mail to '[email protected]', done.
Seq:      1: Sending mail to '[email protected]', done.
Seq:      2: Sending mail to 'Full Name <[email protected]>', done.

Cadeia de divisão baseada no delimitador no shell

Mas se você escrever algo útil em muitos shells, você não deve usar bashisms .

Há uma sintaxe, usada em muitos shells, para dividir uma string na primeira ou na última ocorrência de uma substring:

${var#*SubStr}  # will drop begin of string up to first occur of `SubStr`
${var##*SubStr} # will drop begin of string up to last occur of `SubStr`
${var%SubStr*}  # will drop part of string from last occur of `SubStr` to the end
${var%%SubStr*} # will drop part of string from first occur of `SubStr` to the end

(A falta desta é a principal razão da minha publicação de resposta;)

Como apontado por Score_Under :

# e % excluir a string correspondente mais curta possível e

## e %% deletam o maior tempo possível.

onde # e ## significam a partir da esquerda (começar) da string e

% e %% da direita (fim) da string.

Este pequeno script de exemplo funciona bem sob bash , dash , ksh , busybox e foi testado no bash do Mac-OS também:

var="[email protected];[email protected];Full Name <[email protected]>"
while [ "$var" ] ;do
    iter=${var%%;*}
    echo "> [$iter]"
    [ "$var" = "$iter" ] && \
        var='' || \
        var="${var#*;}"
  done
> [[email protected].com]
> [[email protected].com]
> [Full Name <[email protected].org>]

Diverta-se!


A seguinte função Bash / zsh divide seu primeiro argumento no delimitador fornecido pelo segundo argumento:

split() {
    local string="$1"
    local delimiter="$2"
    if [ -n "$string" ]; then
        local part
        while read -d "$delimiter" part; do
            echo $part
        done <<< "$string"
        echo $part
    fi
}

Por exemplo, o comando

$ split 'a;b;c' ';'

rendimentos

a
b
c

Essa saída pode, por exemplo, ser canalizada para outros comandos. Exemplo:

$ split 'a;b;c' ';' | cat -n
1   a
2   b
3   c

Comparado com as outras soluções dadas, esta tem as seguintes vantagens:

  • IFS não é substituído: devido ao escopo dinâmico de variáveis ​​locais, a substituição do IFS por um loop faz com que o novo valor vaze para as chamadas de função realizadas no loop.

  • Matrizes não são usadas: A leitura de uma string em um array usando read requer a flag -a em Bash e -A em zsh.

Se desejado, a função pode ser colocada em um script da seguinte maneira:

#!/usr/bin/env bash

split() {
    # ...
}

split "[email protected]"


Duas alternativas bourne-ish, em que nenhuma delas requer matrizes bash:

Caso 1 : Mantenha-o agradável e simples: use um NewLine como o Record-Separator ... por exemplo.

IN="[email protected]
[email protected]"

while read i; do
  # process "$i" ... eg.
    echo "[email:$i]"
done <<< "$IN"

Nota: neste primeiro caso, nenhum subprocesso é bifurcado para auxiliar na manipulação de listas.

Idéia: Talvez valha a pena usar o NL extensivamente internamente , e só convertendo para um RS diferente ao gerar o resultado final externamente .

Caso 2 : usando um ";" como um separador de registro ... por exemplo.

NL="
" IRS=";" ORS=";"

conv_IRS() {
  exec tr "$1" "$NL"
}

conv_ORS() {
  exec tr "$NL" "$1"
}

IN="[email protected];[email protected]"
IN="$(conv_IRS ";" <<< "$IN")"

while read i; do
  # process "$i" ... eg.
    echo -n "[email:$i]$ORS"
done <<< "$IN"

Em ambos os casos, uma sub-lista pode ser composta dentro do loop é persistente após a conclusão do loop. Isso é útil ao manipular listas na memória, em vez de armazenar listas em arquivos. {PS Mantenha a calma e continue B-)}


Eu acho que o AWK é o melhor e mais eficiente comando para resolver seu problema. O AWK está incluído no Bash por padrão em quase todas as distribuições Linux.

echo "[email protected];[email protected]" | awk -F';' '{print $1,$2}'

darei

[email protected].com [email protected].com

É claro que você pode armazenar cada endereço de e-mail redefinindo o campo de impressão do awk.


Eu vi algumas respostas referenciando o comando cut , mas elas foram todas deletadas. É um pouco estranho que ninguém tenha elaborado isso, porque acho que é um dos comandos mais úteis para fazer esse tipo de coisa, especialmente para analisar arquivos de log delimitados.

No caso de dividir esse exemplo específico em uma matriz de script bash, o tr provavelmente é mais eficiente, mas o cut pode ser usado e é mais eficaz se você quiser extrair campos específicos do meio.

Exemplo:

$ echo "[email protected];[email protected]" | cut -d ";" -f 1
[email protected].com
$ echo "[email protected];[email protected]" | cut -d ";" -f 2
[email protected].com

Você pode obviamente colocar isso em um loop e iterar o parâmetro -f para puxar cada campo de forma independente.

Isso fica mais útil quando você tem um arquivo de log delimitado com linhas como esta:

2015-04-27|12345|some action|an attribute|meta data

cut é muito útil para ser capaz de cat este arquivo e selecionar um campo específico para processamento adicional.


Extraído da matriz de divisão do script de shell Bash :

IN="[email protected];[email protected]"
arrIN=(${IN//;/ })

Explicação:

Esta construção substitui todas as ocorrências de ';' (a inicial // significa global replace) na string IN com ' ' (um único espaço), então interpreta a string delimitada por espaço como uma matriz (é o que os parênteses circundantes fazem).

A sintaxe usada dentro das chaves para substituir cada ';' caractere com um caractere ' ' é chamado de Expansão de Parâmetros .

Existem algumas dicas comuns:

  1. Se a string original tiver espaços, você precisará usar o IFS :
    • IFS=':'; arrIN=($IN); unset IFS;
  2. Se a string original tiver espaços e o delimitador for uma nova linha, você poderá definir o IFS com:
    • IFS=$'\n'; arrIN=($IN); unset IFS;

Há algumas respostas legais aqui (errator esp.), Mas para algo análogo a dividir em outras linguagens - que é o que eu levei a pergunta original para significar - eu decidi sobre isso:

IN="[email protected];[email protected]"
declare -a a="(${IN/;/ })";

Agora, ${a[0]} , ${a[1]} , etc, são como você esperaria. Use ${#a[*]} para o número de termos. Ou para iterar, claro:

for i in ${a[*]}; do echo $i; done

NOTA IMPORTANTE:

Isso funciona nos casos em que não há espaços para se preocupar, o que resolveu meu problema, mas pode não resolver o seu. Vá com a (s) solução (ões) do $IFS nesse caso.


Isso também funciona:

IN="[email protected];[email protected]"
echo ADD1=`echo $IN | cut -d \; -f 1`
echo ADD2=`echo $IN | cut -d \; -f 2`

Tenha cuidado, esta solução nem sempre está correta. No caso de você passar "[email protected]" apenas, ele será atribuído a ADD1 e ADD2.


No Bash, uma maneira à prova de bala, que funcionará mesmo que sua variável contenha novas linhas:

IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")

Veja:

$ in=$'one;two three;*;there is\na newline\nin this field'
$ IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")
$ declare -p array
declare -a array='([0]="one" [1]="two three" [2]="*" [3]="there is
a newline
in this field")'

O truque para isso funcionar é usar a opção -d de read (delimitador) com um delimitador vazio, para que a read seja forçada a ler tudo o que é alimentado. E nós alimentamos a read com exatamente o conteúdo da variável, sem nenhuma nova linha, graças ao printf . Note que também estamos colocando o delimitador em printf para garantir que a string passada para read tenha um delimitador final. Sem ele, a read aparecia campos vazios em potencial:

$ in='one;two;three;'    # there's an empty field
$ IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")
$ declare -p array
declare -a array='([0]="one" [1]="two" [2]="three" [3]="")'

o campo vazio à direita é preservado.

Atualização para Bash≥4.4

Desde o Bash 4.4, o mapfile (aka readarray ) suporta a opção -d para especificar um delimitador. Daí outro caminho canônico é:

mapfile -d ';' -t array < <(printf '%s;' "$in")


Se você não se importa em processá-los imediatamente, eu gosto de fazer isso:

for i in $(echo $IN | tr ";" "\n")
do
  # process
done

Você poderia usar esse tipo de loop para inicializar uma matriz, mas provavelmente há uma maneira mais fácil de fazer isso. Espero que isso ajude, no entanto.





No shell do Android, a maioria dos métodos propostos simplesmente não funciona:

$ IFS=':' read -ra ADDR <<<"$PATH"                             
/system/bin/sh: can't create temporary file /sqlite_stmt_journals/mksh.EbNoR10629: No such file or directory

O que funciona é:

$ for i in ${PATH//:/ }; do echo $i; done
/sbin
/vendor/bin
/system/sbin
/system/bin
/system/xbin

onde //significa substituição global.


Além das respostas fantásticas que já foram fornecidas, se for apenas uma questão de imprimir os dados que você pode considerar usar awk:

awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "$IN"

Isso define o separador de campo como ;, para que possa percorrer os campos com um forloop e imprimi- los adequadamente.

Teste

$ IN="[email protected];[email protected]"
$ awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "$IN"
> [[email protected].com]
> [[email protected].com]

Com outra entrada:

$ awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "a;b;c   d;e_;f"
> [a]
> [b]
> [c   d]
> [e_]
> [f]

Talvez não seja a solução mais elegante, mas funciona com *e espaços:

IN="[email protected] me.com;*;[email protected]"
for i in `delims=${IN//[^;]}; seq 1 $((${#delims} + 1))`
do
   echo "> [`echo $IN | cut -d';' -f$i`]"
done

Saídas

> [[email protected] me.com]
> [*]
> [[email protected].com]

Outro exemplo (delimitadores no início e no final):

IN=";[email protected] me.com;*;[email protected];"
> []
> [[email protected] me.com]
> [*]
> [[email protected].com]
> []

Basicamente, ele remove todos os personagens que não ;fazer delims, por exemplo.;;; .Em seguida, ele faz forlaço 1da number-of-delimiterscomo contadas por ${#delims}. A etapa final é obter com segurança a $iparte th usando cut.



IN='[email protected];[email protected];Charlie Brown <[email protected];!"#$%&/()[]{}*? are no problem;simple is beautiful :-)'
set -f
oldifs="$IFS"
IFS=';'; arrayIN=($IN)
IFS="$oldifs"
for i in "${arrayIN[@]}"; do
echo "$i"
done
set +f

Saída:

[email protected].com
[email protected].com
Charlie Brown <[email protected].com
!"#$%&/()[]{}*? are no problem
simple is beautiful :-)

Explicação: A atribuição simples usando parêntese () converte a lista separada por ponto-e-vírgula em uma matriz, desde que você tenha o IFS correto ao fazer isso. O loop FOR padrão manipula itens individuais nesse array como de costume. Note que a lista dada para a variável IN deve ser "hard", ou seja, com ticks únicos.

O IFS deve ser salvo e restaurado, pois o Bash não trata uma atribuição da mesma maneira que um comando. Uma alternativa alternativa é envolver a atribuição dentro de uma função e chamar essa função com um IFS modificado. Nesse caso, salvar / restaurar separadamente o IFS não é necessário. Obrigado por "Bize" por apontar isso.







scripting