Como eu divido uma string em um delimitador no Bash?


2043

Eu tenho essa string armazenada em uma variável:

IN="bla@some.com;john@home.com"

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

ADDR1="bla@some.com"
ADDR2="john@home.com"

Eu não necessariamente precisa do ADDR1e ADDR2variáveis. Se eles são elementos de uma matriz, é ainda melhor.


Após sugestões das respostas abaixo, acabei com o seguinte:

#!/usr/bin/env bash

IN="bla@some.com;john@home.com"

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

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

Resultado:

> [bla@some.com]
> [john@home.com]

Havia uma solução envolvendo a configuração Internal_field_separator (IFS) como ;. Não sei o que aconteceu com essa resposta, como você redefineIFS padrão?

RE: IFSsolução, eu tentei isso e funciona, eu mantenho o antigo IFSe depois restauro:

IN="bla@some.com;john@home.com"

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

IFS=$OIFS

BTW, quando eu tentei

mails2=($IN)

Eu só consegui a primeira string ao imprimi-la em loop, sem colchetes ao redor $INdela.


14
Com relação ao seu "Edit2": Você pode simplesmente "desabilitar o IFS" e ele retornará ao estado padrão. Não é necessário salvá-lo e restaurá-lo explicitamente, a menos que você tenha algum motivo para esperar que ele já tenha sido definido como um valor não padrão. Além disso, se você estiver fazendo isso dentro de uma função (e, se não estiver, por que não?), Poderá definir o IFS como uma variável local e ele retornará ao seu valor anterior assim que você sair da função.
Brooks Moses

19
@BrooksMoses: (a) +1 para usar sempre local IFS=...que possível; (b) -1 para unset IFS, isso não redefine exatamente o IFS para seu valor padrão, embora eu acredite que um IFS não configurado se comporte da mesma forma que o valor padrão do IFS ($ '\ t \ n'), no entanto, parece uma má prática assumindo cegamente que seu código nunca será chamado com o IFS definido como um valor personalizado; (c) outra idéia é invocar um subshell: (IFS=$custom; ...)quando o subshell sair, o IFS retornará ao que era originalmente.
dubiousjim

Eu só quero dar uma olhada rápida nos caminhos para decidir onde lançar um executável, por isso resolvi correr ruby -e "puts ENV.fetch('PATH').split(':')". Se você deseja permanecer puro, o bash não ajudará, mas é mais fácil usar qualquer linguagem de script que tenha uma divisão interna.
Nicooga

4
for x in $(IFS=';';echo $IN); do echo "> [$x]"; done
usar o seguinte comando

2
Para salvá-lo como uma matriz, tive que colocar outro conjunto de parênteses e alterá-lo \npor apenas um espaço. Então a linha final é mails=($(echo $IN | tr ";" " ")). Então agora eu posso verificar os elementos do mailsusando a notação de matriz mails[index]ou apenas iteração em um loop
afranques

Respostas:


1236

Você pode definir a variável separador interno de campo (IFS) e, em seguida, deixá-la analisar em uma matriz. Quando isso acontece em um comando, a atribuição a IFSocorre apenas no ambiente desse comando (a read). Em seguida, analisa a entrada de acordo com o IFSvalor da variável em uma matriz, sobre a qual podemos iterar.

IFS=';' read -ra ADDR <<< "$IN"
for i in "${ADDR[@]}"; do
    # process "$i"
done

Ele analisará uma linha de itens separados por ;, empurrando-a para uma matriz. Material para processar todo $IN, cada vez que uma linha de entrada é separada por ;:

 while IFS=';' read -ra ADDR; do
      for i in "${ADDR[@]}"; do
          # process "$i"
      done
 done <<< "$IN"

22
Esta é provavelmente a melhor maneira. Por quanto tempo o IFS persistirá em seu valor atual, ele pode atrapalhar meu código sendo definido quando não deveria ser e como posso redefini-lo quando terminar?
28711 Chris Lutz

7
agora, após a correção aplicada, somente dentro do período de duração do comando de leitura :)
Johannes Schaub - litb

14
Você pode ler tudo de uma vez sem usar um loop while: read -r -d '' -a addr <<< "$ in" # O -d '' é a chave aqui, ele diz ao read para não parar na primeira nova linha ( qual é o padrão -d), mas continua até EOF ou um byte NULL (que ocorre apenas em dados binários).
28119 lhunath

55
@LucaBorrione A configuração IFSna mesma linha do readsem ponto-e-vírgula ou outro separador, em oposição a um comando separado, faz o escopo para esse comando - para que seja sempre "restaurado"; você não precisa fazer nada manualmente.
Charles Duffy

5
@imagineerThis Há um erro que envolve herestrings e alterações locais no IFS que precisa $INser citado. O bug foi corrigido no bash4.3.
Chepner #

973

Retirado da matriz dividida do script do shell Bash :

IN="bla@some.com;john@home.com"
arrIN=(${IN//;/ })

Explicação:

Essa construção substitui todas as ocorrências de ';'(a inicial //significa substituição global) na cadeia de caracteres INpor' ' (um único espaço) e interpreta a cadeia delimitada por espaço como uma matriz (é o que os parênteses circundantes fazem).

A sintaxe usada dentro das chaves para substituir cada ';'caractere por um ' 'caractere é denominada Expansão de parâmetro .

Existem algumas dicas comuns:

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

84
Eu só quero acrescentar: este é o mais simples de todos, você pode acessar os elementos de matriz com $ {Arrin [1]} (a partir de zeros, é claro)
Oz123

26
Encontrou: a técnica de modificar uma variável dentro de $ {} é conhecida como 'expansão de parâmetro'.
KomodoDave

23
Não, eu não acho que isso funcione quando também há espaços presentes ... está convertendo o ',' para '' e construindo um array separado por espaço.
Ethan

12
Muito conciso, mas existem advertências para uso geral : o shell aplica divisão e expansão de palavras à string, o que pode ser indesejável; apenas tente. IN="bla@some.com;john@home.com;*;broken apart". Em resumo: essa abordagem será interrompida se seus tokens contiverem espaços incorporados e / ou caracteres. como *esse, faz com que um token corresponda aos nomes dos arquivos na pasta atual.
usar o seguinte comando

53
Essa é uma péssima abordagem por outros motivos: por exemplo, se a sua string contiver ;*;, ela *será expandida para uma lista de nomes de arquivos no diretório atual. -1
Charles Duffy

249

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ê pode usar esse tipo de loop para inicializar uma matriz, mas provavelmente existe uma maneira mais fácil de fazer isso. Espero que isso ajude, no entanto.


Você deveria ter mantido a resposta do IFS. Ele me ensinou algo que eu não sabia, e definitivamente criou uma matriz, enquanto isso apenas faz um substituto barato.
31711 Chris Lutz

Eu vejo. Sim, eu acho que, fazendo essas experiências tolas, vou aprender coisas novas toda vez que estou tentando responder. Eu editei o material com base no feedback IRC #bash e recuperados :)
Johannes Schaub - litb

33
-1, obviamente, você não está ciente de separar palavras, porque está introduzindo dois erros no seu código. uma é quando você não cita $ IN e a outra é quando você finge que uma nova linha é o único delimitador usado na separação de palavras. Você está repetindo todas as PALAVRAS em IN, nem todas as linhas, e DEFINATAMENTE nem todos os elementos delimitados por ponto-e-vírgula, embora pareça ter o efeito colateral de parecer que funciona.
Lhunath 28/05/09

3
Você pode alterá-lo para ecoar "$ IN" | tr ';' '\ n' | enquanto lê -r ADDY; faça # processo "$ ADDY"; feito para torná-lo sortudo, acho :) Observe que isso vai bifurcar e você não pode alterar variáveis ​​externas de dentro do loop (é por isso que usei a sintaxe <<< "$ IN") e então
Johannes Schaub - litb

8
Para resumir o debate nos comentários: Advertências para uso geral : o shell aplica divisão e expansão de palavras à string, o que pode ser indesejável; apenas tente. IN="bla@some.com;john@home.com;*;broken apart". Em resumo: essa abordagem será interrompida se seus tokens contiverem espaços incorporados e / ou caracteres. como *esse, faz com que um token corresponda aos nomes dos arquivos na pasta atual.
usar o seguinte comando

202

Resposta compatível

Existem várias maneiras diferentes de fazer isso em .

No entanto, é importante notar que existembash muitos recursos especiais (os chamados basismos ) que não funcionarão em nenhum outro.

Em particular, matrizes , matrizes associativas e substituição padrão , que são utilizados nas soluções neste post, bem como outros no segmento, são bashisms e não podem trabalhar sob outras conchas que muitas pessoas usam.

Por exemplo: no meu Debian GNU / Linux , existe um padrão shell chamado; Conheço muitas pessoas que gostam de usar outro shell chamado; e também há uma ferramenta especial chamada com seu próprio interpretador de shell ()

Sequência solicitada

A string a ser dividida na pergunta acima é:

IN="bla@some.com;john@home.com"

Usarei uma versão modificada dessa cadeia para garantir que minha solução seja robusta para cadeias que contenham espaço em branco, o que poderia interromper outras soluções:

IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"

Dividir string com base no delimitador em (versão> = 4.2)

Em termos puros bash , podemos criar uma matriz com elementos divididos por um valor temporário para o IFS (o separador de campos de entrada ). O IFS, entre outras coisas, informa bashquais caracteres deve ser tratado como um delimitador entre os elementos ao definir uma matriz:

IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"

# save original IFS value so we can restore it later
oIFS="$IFS"
IFS=";"
declare -a fields=($IN)
IFS="$oIFS"
unset oIFS

Em versões mais recentes do bash, prefixando um comando com uma definição IFS altera as IFS para esse comando única e redefine para o valor anterior imediatamente depois. Isso significa que podemos fazer o acima em apenas uma linha:

IFS=\; read -a fields <<<"$IN"
# after this command, the IFS resets back to its previous value (here, the default):
set | grep ^IFS=
# IFS=$' \t\n'

Podemos ver que a string INfoi armazenada em uma matriz chamada fields, dividida em ponto e vírgula:

set | grep ^fields=\\\|^IN=
# fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name <fulnam@other.org>")
# IN='bla@some.com;john@home.com;Full Name <fulnam@other.org>'

(Também podemos exibir o conteúdo dessas variáveis ​​usando declare -p:)

declare -p IN fields
# declare -- IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
# declare -a fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name <fulnam@other.org>")

Observe que readé a maneira mais rápida de fazer a divisão, porque não há garfos ou recursos externos chamados.

Depois que a matriz é definida, você pode usar um loop simples para processar cada campo (ou melhor, cada elemento da matriz que você definiu agora):

# `"${fields[@]}"` expands to return every element of `fields` array as a separate argument
for x in "${fields[@]}" ;do
    echo "> [$x]"
    done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

Ou você pode soltar cada campo da matriz após o processamento usando uma abordagem de deslocamento , que eu gosto:

while [ "$fields" ] ;do
    echo "> [$fields]"
    # slice the array 
    fields=("${fields[@]:1}")
    done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

E se você quer apenas uma impressão simples da matriz, não precisa nem fazer um loop sobre ela:

printf "> [%s]\n" "${fields[@]}"
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

Atualização: recente > = 4.4

Nas versões mais recentes do bash, você também pode jogar com o comando mapfile:

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

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

Se você não quiser incluir campos vazios, faça o seguinte:

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

Com mapfile, você também pode pular a declaração de uma matriz e implicitamente "fazer um loop" sobre os elementos delimitados, chamando uma função em cada:

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: a \0sequência no final da sequência de formatação é inútil se você não se importa com campos vazios no final da sequência ou eles não estiverem presentes.)

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

# Seq:      0: Sending mail to 'bla@some.com', done.
# Seq:      1: Sending mail to 'john@home.com', done.
# Seq:      2: Sending mail to 'Full Name <fulnam@other.org>', done.

Ou você pode usar <<<e, no corpo da função, incluir algum processamento para descartar a nova linha que ele adiciona:

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

# Renders the same output:
# Seq:      0: Sending mail to 'bla@some.com', done.
# Seq:      1: Sending mail to 'john@home.com', done.
# Seq:      2: Sending mail to 'Full Name <fulnam@other.org>', done.

Dividir string com base no delimitador em

Se você não pode usar bash, ou se deseja escrever algo que possa ser usado em muitas conchas diferentes, geralmente não pode usar basismos - e isso inclui as matrizes que usamos nas soluções acima.

No entanto, não precisamos usar matrizes para fazer um loop sobre os "elementos" de uma string. Há uma sintaxe usada em muitos shells para excluir substrings de uma string da primeira ou da última ocorrência de um padrão. Observe que *é um curinga que representa zero ou mais caracteres:

(A falta dessa abordagem em qualquer solução postada até agora é o principal motivo pelo qual estou escrevendo esta resposta;)

${var#*SubStr}  # drops substring from start of string up to first occurrence of `SubStr`
${var##*SubStr} # drops substring from start of string up to last occurrence of `SubStr`
${var%SubStr*}  # drops substring from last occurrence of `SubStr` to end of string
${var%%SubStr*} # drops substring from first occurrence of `SubStr` to end of string

Conforme explicado por Score_Under :

#e %exclua a substring correspondente mais curta possível do início e do final da string, respectivamente, e

##e %%exclua a substring correspondente mais longa possível.

Usando a sintaxe acima, podemos criar uma abordagem na qual extraímos "elementos" de substring da string excluindo as substrings até ou após o delimitador.

O código de bloqueio abaixo funciona bem em (incluindo Mac OS's bash ),, e é :

IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
while [ "$IN" ] ;do
    # extract the substring from start of string up to delimiter.
    # this is the first "element" of the string.
    iter=${IN%%;*}
    echo "> [$iter]"
    # if there's only one element left, set `IN` to an empty string.
    # this causes us to exit this `while` loop.
    # else, we delete the first "element" of the string from IN, and move onto the next.
    [ "$IN" = "$iter" ] && \
        IN='' || \
        IN="${IN#*;}"
  done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

Diverta-se!


15
Os #, ##, %, e %%substituições têm o que é IMO uma explicação mais fácil de lembrar (para o quanto eles delete): #e %apagar a menor cadeia correspondente possível, e ##e %%eliminar o mais longo possível.
Score_Under

1
A IFS=\; read -a fields <<<"$var"falha nas novas linhas e adiciona uma nova linha à direita. A outra solução remove um campo vazio à direita.
Isaac

O delimitador de shell é a resposta mais elegante, ponto final.
Eric Chen

A última alternativa poderia ser usada com uma lista de separadores de campos definidos em outro lugar? Por exemplo, pretendo usar isso como um script de shell e passar uma lista de separadores de campos como um parâmetro posicional.
Sancho.s ReinstateMonicaCellio

Sim, em um loop:for sep in "#" "ł" "@" ; do ... var="${var#*$sep}" ...
F. Hauri

184

Vi algumas respostas referenciando o cutcomando, mas todas foram excluídas. É 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 este exemplo específico em uma matriz de scripts bash, tré provavelmente mais eficiente, mas cutpode ser usado e é mais eficaz se você deseja extrair campos específicos do meio.

Exemplo:

$ echo "bla@some.com;john@home.com" | cut -d ";" -f 1
bla@some.com
$ echo "bla@some.com;john@home.com" | cut -d ";" -f 2
john@home.com

Obviamente, você pode colocar isso em um loop e iterar o parâmetro -f para extrair cada campo independentemente.

Isso se torna 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 poder acessar catesse arquivo e selecionar um campo específico para processamento adicional.


6
Parabéns pelo uso cut, é a ferramenta certa para o trabalho! Muito limpo do que qualquer um desses hacks de shell.
precisa saber é o seguinte

4
Essa abordagem só funcionará se você souber o número de elementos com antecedência; você precisaria programar um pouco mais de lógica. Também executa uma ferramenta externa para cada elemento.
precisa saber é

Exatamente o que eu estava procurando, tentando evitar uma string vazia em um csv. Agora eu também posso apontar o valor exato da 'coluna'. Trabalhe com o IFS já usado em um loop. Melhor do que o esperado para a minha situação.
Louis Loudog Trottier

Muito útil para puxar IDs e PIDs também ie
Milos Grujic

Vale a pena rolar esta resposta para mais de meia página :)
Gucu112

124

Isso funcionou para mim:

string="1;2"
echo $string | cut -d';' -f1 # output is 1
echo $string | cut -d';' -f2 # output is 2

1
Embora ele funcione apenas com um delimitador de caractere único, é isso que o OP estava procurando (registros delimitados por ponto e vírgula).
GuyPaddock

Respondida cerca de quatro anos atrás por @Ashok , e também, mais de um ano atrás por @DougW , que sua resposta, com ainda mais informações. Por favor, publique uma solução diferente da dos outros.
MAChitgarha 03/04

90

Que tal essa abordagem:

IN="bla@some.com;john@home.com" 
set -- "$IN" 
IFS=";"; declare -a Array=($*) 
echo "${Array[@]}" 
echo "${Array[0]}" 
echo "${Array[1]}" 

Fonte


7
+1 ... mas eu não nomearia a variável "Array" ... pet peev, eu acho. Boa solução.
Yzmir Ramirez 5/09/11

14
+1 ... mas o "conjunto" e declara -a são desnecessários. Você poderia muito bem ter usado apenasIFS";" && Array=($IN)
ata

+1 Apenas uma observação: não seria recomendável manter o IFS antigo e depois restaurá-lo? (como mostrado por stefanB em seus Edit3) pessoas desembarque aqui (às vezes apenas copiando e colando uma solução) pode não pensar sobre isso
Luca Borrione

6
-1: Primeiro, @ata está certo em que a maioria dos comandos não faz nada. Segundo, ele usa a divisão de palavras para formar a matriz e não faz nada para inibir a expansão glob ao fazê-lo (por isso, se você tiver caracteres glob em qualquer um dos elementos da matriz, esses elementos serão substituídos por nomes de arquivos correspondentes).
Charles Duffy

1
Sugerir ao uso $'...': IN=$'bla@some.com;john@home.com;bet <d@\ns* kl.com>'. Em seguida echo "${Array[2]}", imprimirá uma string com nova linha. set -- "$IN"também é necessário neste caso. Sim, para impedir a expansão glob, a solução deve incluir set -f.
John_West

79

Acho que o AWK é o comando melhor e eficiente para resolver seu problema. O AWK é incluído por padrão em quase todas as distribuições Linux.

echo "bla@some.com;john@home.com" | awk -F';' '{print $1,$2}'

darei

bla@some.com john@home.com

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


3
Ou ainda mais simples: echo "bla@some.com; john@home.com" | awk 'BEGIN {RS = ";"} {print}'
Jaro

@ Jaro Isso funcionou perfeitamente para mim quando eu tinha uma string com vírgulas e precisava reformatá-la em linhas. Obrigado.
Aquarelle

Funcionou neste cenário -> "echo" $ SPLIT_0 "| awk -F 'inode =' '{print $ 1}'"! Tive problemas ao tentar usar atrings ("inode =") em vez de caracteres (";"). $ 1, $ 2, $ 3, $ 4 são definidos como posições em uma matriz! Se existe uma maneira de definir uma matriz ... melhor! Obrigado!
Eduardo Lucio

@EduardoLucio, o que eu estou pensando é talvez você possa primeira substituir o seu delimitador inode=em ;por exemplo sed -i 's/inode\=/\;/g' your_file_to_process, em seguida, definir -F';'quando se aplicam awk, esperança de que pode ajudá-lo.
Tong

66
echo "bla@some.com;john@home.com" | sed -e 's/;/\n/g'
bla@some.com
john@home.com

4
-1 e se a string contiver espaços? por exemplo, IN="this is first line; this is second line" arrIN=( $( echo "$IN" | sed -e 's/;/\n/g' ) )irão produzir uma matriz de 8 elementos, neste caso, (um para cada elemento de espaço palavra separados), em vez de duas (uma para cada elemento de linha cólon semi separados)
Luca Borrione

3
@Luca Não, o script sed cria exatamente duas linhas. O que cria as várias entradas para você é quando você colocá-lo em uma matriz bash (que divide no espaço branco por padrão)
Lothar

Esse é exatamente o ponto: o OP precisa armazenar entradas em uma matriz para fazer um loop sobre ela, como você pode ver nas edições dele. Acho que sua (boa) resposta não foi mencionada para usar arrIN=( $( echo "$IN" | sed -e 's/;/\n/g' ) )para conseguir isso, e conselhos para mudar o IFS IFS=$'\n'para aqueles que pousarem aqui no futuro e precisam dividir uma sequência contendo espaços. (e para restaurá-lo depois). :)
Luca Borrione

1
@Luca Bom ponto. No entanto, a atribuição da matriz não estava na pergunta inicial quando escrevi essa resposta.
lothar 4/12

65

Isso também funciona:

IN="bla@some.com;john@home.com"
echo ADD1=`echo $IN | cut -d \; -f 1`
echo ADD2=`echo $IN | cut -d \; -f 2`

Cuidado, esta solução nem sempre está correta. Caso você passe "bla@some.com" apenas, ele será atribuído a ambos ADD1 e ADD2.


1
Você pode usar -s para evitar o problema mencionado: superuser.com/questions/896800/… "-f, --fields = LIST seleciona apenas esses campos; também imprime qualquer linha que não contenha caracteres delimitadores, a menos que a opção -s seja especificado "
fersarr 3/03/16

34

Uma visão diferente da resposta de Darron , é assim que eu faço:

IN="bla@some.com;john@home.com"
read ADDR1 ADDR2 <<<$(IFS=";"; echo $IN)

Eu acho que sim! Executar os comandos acima e depois "echo $ ADDR1 ... $ ADDR2" e eu recebo "bla@some.com ... john@home.com" output
nickjb

1
Isso funcionou MUITO bem para mim ... Eu usei para iterar sobre uma matriz de strings que continham dados separados por vírgula DB, SERVER, PORT para usar o mysqldump.
Nick

5
Diagnóstico: a IFS=";"atribuição existe apenas no $(...; echo $IN)subshell; é por isso que alguns leitores (inclusive eu) inicialmente pensam que não vai funcionar. Eu assumi que todo o $ IN estava sendo absorvido pelo ADDR1. Mas o nickjb está correto; isso funciona. O motivo é que o echo $INcomando analisa seus argumentos usando o valor atual de $ IFS, mas os ecoa para stdout usando um delimitador de espaço, independentemente da configuração de $ IFS. Portanto, o efeito líquido é como se alguém tivesse chamado read ADDR1 ADDR2 <<< "bla@some.com john@home.com"(observe que a entrada não é separada por espaço; é separada).
Dubiousjim

1
Isso falha em espaços e novas linhas e também expande curingas *no echo $INcom uma expansão de variável não citada.
Isaac

Eu realmente gosto desta solução. Uma descrição do porquê funciona seria muito útil e a tornaria uma resposta geral melhor.
Michael Gaskill

32

No Bash, à prova de balas, isso 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 -dopção de read(delimitador) com um delimitador vazio, para que readseja forçado a ler tudo o que é alimentado. E nós alimentamos readexatamente o conteúdo da variável in, sem nova linha à direita graças a printf. Observe que também estamos colocando o delimitador printfpara garantir que a sequência passada readtenha um delimitador à direita. Sem ele, readapararia possíveis campos vazios à direita:

$ 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 built-in mapfile(aka readarray) suporta a -dopção de especificar um delimitador. Portanto, outra maneira canônica é:

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

5
Achei a solução rara nessa lista que funciona corretamente com \nespaços e *simultaneamente. Além disso, sem loops; A variável array pode ser acessada no shell após a execução (ao contrário da resposta mais votada). Observe in=$'...'que ele não funciona com aspas duplas. Eu acho que precisa de mais votos.
John_West

28

Que tal esse liner único, se você não estiver usando matrizes:

IFS=';' read ADDR1 ADDR2 <<<$IN

Considere usar read -r ...para garantir que, por exemplo, os dois caracteres "\ t" na entrada acabem com os mesmos dois caracteres em suas variáveis ​​(em vez de um único caractere de tabulação).
dubiousjim

-1 Isso não está funcionando aqui (ubuntu 12.04). Adicionando echo "ADDR1 $ADDR1"\n echo "ADDR2 $ADDR2"à sua saída vontade trecho ADDR1 bla@some.com john@home.com\nADDR2(\ n é de nova linha)
Luca Borrione

Provavelmente, IFSisso ocorreu devido a um erro envolvendo as cordas aqui corrigidas no bash4.3. A citação $INdeve corrigi-lo. (Em teoria, $INnão está sujeito a divisão de palavras ou globbing após a expansão, o que significa que as aspas devem ser desnecessárias. Mesmo em 4.3, no entanto, há pelo menos um bug restante - relatado e programado para ser corrigido -, portanto, a citação continua sendo uma boa opção. idéia).
chepner

Isso interrompe se $ in contiver novas linhas, mesmo que $ IN esteja entre aspas. E adiciona uma nova linha à direita.
Isaac

Um problema com isso e muitas outras soluções também é que pressupõe que EXATAMENTE DOIS elementos em $ IN - OU que você deseja que o segundo e os itens subsequentes sejam esmagados no ADDR2. Eu entendo que isso atende à pergunta, mas é uma bomba-relógio.
Steven the Facilly Fun

23

Sem configurar o IFS

Se você tiver apenas dois pontos, poderá fazer isso:

a="foo:bar"
b=${a%:*}
c=${a##*:}

você vai ter:

b = foo
c = bar

20

Aqui está um 3-liner limpo:

in="foo@bar;bizz@buzz;fizz@buzz;buzz@woof"
IFS=';' list=($in)
for item in "${list[@]}"; do echo $item; done

onde IFSdelimita as palavras com base no separador e ()é usado para criar uma matriz . Em seguida, [@]é usado para retornar cada item como uma palavra separada.

Se você tiver algum código depois disso, também precisará restaurar $IFS, por exemplo unset IFS.


5
O uso de $inaspas permite expandir caracteres curinga.
Isaac

10

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 fornecidas, esta possui as seguintes vantagens:

  • IFSnão é substituída: devido ao escopo dinâmico de variáveis ​​locais, a substituição de IFSum loop faz com que o novo valor vaze nas chamadas de função realizadas de dentro do loop.

  • Matrizes não são usadas: a leitura de uma string em uma matriz usando readrequer o sinalizador -ano Bash e -Ano zsh.

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

#!/usr/bin/env bash

split() {
    # ...
}

split "$@"

Não parece trabalho com delimitadores mais de 1 personagem: split = $ (split "$ content" "file: //")
madprops

Verdadeiro - de help read:-d delim continue until the first character of DELIM is read, rather than newline
Halle Knast

8

você pode aplicar o awk a muitas situações

echo "bla@some.com;john@home.com"|awk -F';' '{printf "%s\n%s\n", $1, $2}'

Também você pode usar isso

echo "bla@some.com;john@home.com"|awk -F';' '{print $1,$2}' OFS="\n"

7

Existe uma maneira simples e inteligente como esta:

echo "add:sfff" | xargs -d: -i  echo {}

Mas você deve usar o gnu xargs, o BSD xargs não pode suportar -d delim. Se você usa o Apple Mac como eu. Você pode instalar o gnu xargs:

brew install findutils

então

echo "add:sfff" | gxargs -d: -i  echo {}

4

Esta é a maneira mais simples de fazer isso.

spo='one;two;three'
OIFS=$IFS
IFS=';'
spo_array=($spo)
IFS=$OIFS
echo ${spo_array[*]}

4

Existem algumas respostas legais aqui (errator esp.), Mas para algo análogo se dividir em outros idiomas - que é o que eu entendi a pergunta original -, decidi sobre isso:

IN="bla@some.com;john@home.com"
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 com que se preocupar, o que resolveu o meu problema, mas pode não resolver o seu. Vá com a $IFS(s) solução (s) nesse caso.


Não funciona quando INcontém mais de dois endereços de email. Por favor, referir-se a mesma idéia (mas fixo) na resposta de Palindrom
olibre

Melhor uso ${IN//;/ }(barra dupla) para que ele também funcione com mais de dois valores. Cuidado que qualquer curinga ( *?[) será expandido. E um campo vazio à direita será descartado.
Isaac

3
IN="bla@some.com;john@home.com"
IFS=';'
read -a IN_arr <<< "${IN}"
for entry in "${IN_arr[@]}"
do
    echo $entry
done

Resultado

bla@some.com
john@home.com

Sistema: Ubuntu 12.04.1


O IFS não está sendo definido no contexto específico readdaqui e, portanto, pode perturbar o restante do código, se houver.
codeforester

2

Se não houver espaço, por que não isso?

IN="bla@some.com;john@home.com"
arr=(`echo $IN | tr ';' ' '`)

echo ${arr[0]}
echo ${arr[1]}

2

Use o setbuilt-in para carregar a $@matriz:

IN="bla@some.com;john@home.com"
IFS=';'; set $IN; IFS=$' \t\n'

Então, deixe a festa começar:

echo $#
for a; do echo $a; done
ADDR1=$1 ADDR2=$2

Melhor uso set -- $INpara evitar alguns problemas com "$ IN" começando com o hífen. Ainda assim, a expansão não cotada de $INexpandirá curingas ( *?[).
Isaac

2

Duas alternativas de bourne-ish em que nenhuma delas exige matrizes de bash:

Caso 1 : Mantenha-o agradável e simples: use uma Nova Linha como separador de registros ... por exemplo.

IN="bla@some.com
john@home.com"

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

Nota: neste primeiro caso, nenhum subprocesso é bifurcado para ajudar na manipulação da lista.

Idéia: Talvez valha a pena usar NL extensivamente internamente e apenas converter em um RS diferente ao gerar o resultado final externamente .

Caso 2 : Usando um ";" como um separador de registros ... por exemplo.

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

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

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

IN="bla@some.com;john@home.com"
IN="$(conv_IRS ";" <<< "$IN")"

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

Nos dois 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-)}


2

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

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

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

Teste

$ IN="bla@some.com;john@home.com"
$ awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "$IN"
> [bla@some.com]
> [john@home.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]

2

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.


1
Falha se qualquer parte do $ PATH contiver espaços (ou novas linhas). Também expande curingas (asterisco *, ponto de interrogação? E chaves […]).
Isaac

2
IN='bla@some.com;john@home.com;Charlie Brown <cbrown@acme.com;!"#$%&/()[]{}*? 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

Resultado:

bla@some.com
john@home.com
Charlie Brown <cbrown@acme.com
!"#$%&/()[]{}*? are no problem
simple is beautiful :-)

Explicação: A atribuição simples usando parênteses () 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 lida com itens individuais nessa matriz, como de costume. Observe que a lista fornecida para a variável IN deve ser "difícil" entre aspas, ou seja, com marcações simples.

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


!"#$%&/()[]{}*? are no problembem ... não exatamente: []*?são personagens globais. Então, que tal criar este diretório e arquivo: `mkdir '!" # $% &'; Touch '! "# $% & / () [] {} Te pegou hahahaha - não há problema' e executando seu comando? simples pode ser bonito, mas quando está quebrado, está quebrado.
gniourf_gniourf

@gniourf_gniourf A string é armazenada em uma variável. Por favor, veja a pergunta original.
ajaaskel

1
@ajaaskel você não entendeu completamente o meu comentário. Vá em um diretório do zero e emitir estes comandos: mkdir '!"#$%&'; touch '!"#$%&/()[]{} got you hahahaha - are no problem'. Eles apenas criarão um diretório e um arquivo, com nomes estranhos, devo admitir. Em seguida, execute os comandos com o exato INque você deu: IN='bla@some.com;john@home.com;Charlie Brown <cbrown@acme.com;!"#$%&/()[]{}*? are no problem;simple is beautiful :-)'. Você verá que não obterá o resultado esperado. Porque você está usando um método sujeito a expansões de nome de caminho para dividir sua string.
gniourf_gniourf

Isso é para demonstrar que os personagens *, ?, [...]e até mesmo, se extglobfor definida, !(...), @(...), ?(...), +(...) são problemas com este método!
gniourf_gniourf

1
@gniourf_gniourf Obrigado por comentários detalhados sobre globbing. Eu ajustei o código para ficar sem brilho. Meu argumento, porém, era apenas mostrar que uma tarefa bastante simples pode fazer o trabalho de divisão.
ajaaskel

1

Ok pessoal!

Aqui está a minha resposta!

DELIMITER_VAL='='

read -d '' F_ABOUT_DISTRO_R <<"EOF"
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION="Ubuntu 14.04.4 LTS"
NAME="Ubuntu"
VERSION="14.04.4 LTS, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 14.04.4 LTS"
VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
EOF

SPLIT_NOW=$(awk -F$DELIMITER_VAL '{for(i=1;i<=NF;i++){printf "%s\n", $i}}' <<<"${F_ABOUT_DISTRO_R}")
while read -r line; do
   SPLIT+=("$line")
done <<< "$SPLIT_NOW"
for i in "${SPLIT[@]}"; do
    echo "$i"
done

Por que essa abordagem é "a melhor" para mim?

Por duas razões:

  1. Você não precisa escapar do delimitador;
  2. Você não terá problemas com espaços em branco . O valor será separado corretamente na matriz!

[]


FYI, /etc/os-releasee /etc/lsb-releasedevem ser adquiridos e não analisados. Portanto, seu método está realmente errado. Além disso, você não está respondendo bem à pergunta sobre o spiltting de uma string em um delimitador.
precisa saber é o seguinte

0

Uma linha para dividir uma sequência separada por ';' em uma matriz é:

IN="bla@some.com;john@home.com"
ADDRS=( $(IFS=";" echo "$IN") )
echo ${ADDRS[0]}
echo ${ADDRS[1]}

Isso define apenas o IFS em um subshell, para que você não precise se preocupar em salvar e restaurar seu valor.


-1 isso não funciona aqui (ubuntu 12.04). ele imprime apenas o primeiro eco com todo o valor $ IN, enquanto o segundo está vazio. você pode vê-lo se colocar o eco "0:" $ {ADDRS [0]} \ n eco "1:" $ {ADDRS [1]} o resultado é 0: bla@some.com;john@home.com\n 1:(\ n é uma nova linha)
Luca Borrione

1
consulte a resposta das nickjb em uma alternativa de trabalho para essa idéia stackoverflow.com/a/6583589/1032370
Luca Borrione

1
-1, 1. O IFS não está sendo definido nesse subshell (está sendo passado para o ambiente de "echo", que é um builtin, para que nada esteja acontecendo de qualquer maneira). 2. $INé citado para que não esteja sujeito à divisão do IFS. 3. A substituição do processo é dividida por espaços em branco, mas isso pode corromper os dados originais.
28/04

0

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

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

Saídas

> [bla@so me.com]
> [*]
> [john@home.com]

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

IN=";bla@so me.com;*;john@home.com;"
> []
> [bla@so me.com]
> [*]
> [john@home.com]
> []

Basicamente, ele remove todos os personagens que não ;fazer delims, por exemplo. ;;;. Em seguida, ele faz um forloop de 1para number-of-delimitersconforme contado ${#delims}. O passo final é obter a $iparte th com segurança cut.

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.