Bash: iterando sobre linhas em uma variável

225

Como alguém itera corretamente sobre linhas no bash em uma variável ou a partir da saída de um comando? Simplesmente definir a variável IFS como uma nova linha funciona para a saída de um comando, mas não ao processar uma variável que contém novas linhas.

Por exemplo

#!/bin/bash

list="One\ntwo\nthree\nfour"

#Print the list with echo
echo -e "echo: \n$list"

#Set the field separator to new line
IFS=$'\n'

#Try to iterate over each line
echo "For loop:"
for item in $list
do
        echo "Item: $item"
done

#Output the variable to a file
echo -e $list > list.txt

#Try to iterate over each line from the cat command
echo "For loop over command output:"
for item in `cat list.txt`
do
        echo "Item: $item"
done

Isso fornece a saída:

echo: 
One
two
three
four
For loop:
Item: One\ntwo\nthree\nfour
For loop over command output:
Item: One
Item: two
Item: three
Item: four

Como você pode ver, ecoar a variável ou iterar sobre o catcomando imprime cada uma das linhas uma por uma corretamente. No entanto, o primeiro loop for imprime todos os itens em uma única linha. Alguma ideia?

Alex Spurling
fonte
Apenas um comentário para todas as respostas: Eu tive que fazer $ (echo "$ line" | sed -e 's / ^ [[: space:]] * //') para aparar o caractere da nova linha.
servermanfail 04/09

Respostas:

290

Com o bash, se você deseja incorporar novas linhas em uma sequência, coloque a sequência com $'':

$ list="One\ntwo\nthree\nfour"
$ echo "$list"
One\ntwo\nthree\nfour
$ list=$'One\ntwo\nthree\nfour'
$ echo "$list"
One
two
three
four

E se você já possui uma string em uma variável, pode lê-la linha por linha com:

while read -r line; do
    echo "... $line ..."
done <<< "$list"
Glenn Jackman
fonte
30
Apenas uma nota que done <<< "$list"é crucial
Jason Axelson
21
O motivo done <<< "$list"é crucial, porque isso passará "$ list" como entrada pararead
wisbucky
22
Eu preciso enfatizar o seguinte: as citações duplas $listsão muito cruciais.
André Chalella 10/06
8
echo "$list" | while ...pode parecer mais claro do que... done <<< "$line"
kyb 14/05
5
Existem desvantagens nessa abordagem, pois o loop while será executado em um subshell: quaisquer variáveis ​​atribuídas no loop não persistirão.
glenn jackman
66

Você pode usar while+ read:

some_command | while read line ; do
   echo === $line ===
done

Btw. a -eopção para echonão é padrão. Use em printfvez disso, se você quiser portabilidade.

maxelost
fonte
12
Observe que, se você usar essa sintaxe, as variáveis ​​atribuídas dentro do loop não serão mantidas após o loop. Curiosamente, a <<<versão sugerida por Glenn Jackman não trabalhar com atribuição de variável.
Sparhawk #
7
@ Sparhawk Sim, é porque o pipe inicia um subshell executando a whilepeça. A <<<versão não (em novas versões do bash, pelo menos).
precisa saber é o seguinte
26
#!/bin/sh

items="
one two three four
hello world
this should work just fine
"

IFS='
'
count=0
for item in $items
do
  count=$((count+1))
  echo $count $item
done
ninguém importante
fonte
2
Isso gera todos os itens, não linhas.
Tomasz Posłuszny 13/08/2015
4
@ TomaszPosłuszny Não, isso gera 3 (a contagem é 3) linhas na minha máquina. Observe a configuração do IFS. Você também pode usar IFS=$'\n'para o mesmo efeito.
Jmiserez # 10/16
11

Aqui está uma maneira engraçada de fazer seu loop for:

for item in ${list//\\n/
}
do
   echo "Item: $item"
done

Um pouco mais sensato / legível seria:

cr='
'
for item in ${list//\\n/$cr}
do
   echo "Item: $item"
done

Mas isso é muito complexo, você só precisa de um espaço lá:

for item in ${list//\\n/ }
do
   echo "Item: $item"
done

Sua $linevariável não contém novas linhas. Ele contém instâncias de \seguido por n. Você pode ver isso claramente com:

$ cat t.sh
#! /bin/bash
list="One\ntwo\nthree\nfour"
echo $list | hexdump -C

$ ./t.sh
00000000  4f 6e 65 5c 6e 74 77 6f  5c 6e 74 68 72 65 65 5c  |One\ntwo\nthree\|
00000010  6e 66 6f 75 72 0a                                 |nfour.|
00000016

A substituição está substituindo aqueles por espaços, o que é suficiente para trabalhar em loops:

$ cat t.sh
#! /bin/bash
list="One\ntwo\nthree\nfour"
echo ${list//\\n/ } | hexdump -C

$ ./t.sh 
00000000  4f 6e 65 20 74 77 6f 20  74 68 72 65 65 20 66 6f  |One two three fo|
00000010  75 72 0a                                          |ur.|
00000013

Demo:

$ cat t.sh
#! /bin/bash
list="One\ntwo\nthree\nfour"
echo ${list//\\n/ } | hexdump -C
for item in ${list//\\n/ } ; do
    echo $item
done

$ ./t.sh 
00000000  4f 6e 65 20 74 77 6f 20  74 68 72 65 65 20 66 6f  |One two three fo|
00000010  75 72 0a                                          |ur.|
00000013
One
two
three
four
Esteira
fonte
Interessante, você pode explicar o que está acontecendo aqui? Parece que você está substituindo \ n por uma nova linha ... Qual é a diferença entre a string original e a nova?
Alex Spurling
@ Alex: atualizei a minha resposta - com uma versão mais simples também :-) #
Mat
Eu tentei isso e não parece funcionar se você tiver espaços em sua entrada. No exemplo acima, temos "um \ dois \ três" e isso funciona, mas falha se tivermos "entrada um \ dois" dois \ três "", pois também adiciona uma nova linha para o espaço.
John Rocha
2
Apenas uma nota que, em vez de definir, crvocê pode usar $'\n'.
precisa saber é o seguinte
1

Você também pode primeiro converter a variável em uma matriz e iterar sobre isso.

lines="abc
def
ghi"

declare -a theArray

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

for line in "${theArray[@]}"
do
    echo "$line"
    #Do something complex here that would break your read loop
done

Isso é útil apenas se você não quiser mexer com o IFSe também tiver problemas com o readcomando, pois pode acontecer, se você chamar outro script dentro do loop para que esse script possa esvaziar seu buffer de leitura antes de retornar, como aconteceu comigo .

Torge
fonte