Como posso ler linha por linha de uma variável no bash?


49

Eu tenho uma variável que contém saída multilinha de um comando. Qual é a maneira mais eficiente de ler a linha de saída por linha da variável?

Por exemplo:

jobs="$(jobs)"
if [ "$jobs" ]; then
    # read lines from $jobs
fi

Respostas:


65

Você pode usar um loop while com substituição de processo:

while read -r line
do
    echo "$line"
done < <(jobs)

Uma maneira ideal de ler uma variável de múltiplas linhas é definir uma IFSvariável em branco e printfa variável com uma nova linha à direita:

# Printf '%s\n' "$var" is necessary because printf '%s' "$var" on a
# variable that doesn't end with a newline then the while loop will
# completely miss the last line of the variable.
while IFS= read -r line
do
   echo "$line"
done < <(printf '%s\n' "$var")

Nota: Conforme o shellcheck sc2031 , o uso da subestação de processo é preferível a um tubo para evitar [sutilmente] criar um subshell.

Além disso, saiba que, ao nomear a variável, jobsisso pode causar confusão, pois esse também é o nome de um comando shell comum.


3
Se você quiser manter todos os seus espaços em branco, em seguida, usar while IFS= read.... Se você quer evitar \ interpretação, então useread -r
Peter.O

Corrigi os pontos que fred.bear mencionou e também mudei echopara printf %s, para que seu script funcionasse mesmo com entradas não domesticadas.
Gilles 'SO- stop being evil'

Para ler a partir de uma variável de várias linhas, é preferível usar um herestring do que enviar a partir de printf (consulte a resposta de 10b0).
ata

1
@ata Embora eu tenha ouvido isso "preferível" com bastante frequência, deve-se notar que uma herestring sempre exige que o /tmpdiretório seja gravável, pois depende de ser capaz de criar um arquivo de trabalho temporário. Se você se encontrar em um sistema restrito com /tmpapenas leitura (e não pode ser alterado por você), ficará feliz com a possibilidade de usar uma solução alternativa, por exemplo, com o printfcano.
Syntaxerror 4/12/14

1
No segundo exemplo, se a variável de várias linhas não contiver uma nova linha à direita, você perderá o último elemento. Altere para:printf "%s\n" "$var" | while IFS= read -r line
David H. Bennett

26

Para processar a saída de uma linha de comando por linha ( explicação ):

jobs |
while IFS= read -r line; do
  process "$line"
done

Se você já possui os dados em uma variável:

printf %s "$foo" | 

printf %s "$foo"é quase idêntico a echo "$foo", mas é impresso $fooliteralmente, enquanto que echo "$foo"pode ser interpretado $foocomo uma opção para o comando echo se começar com a -e pode expandir as seqüências de barra invertida $fooem alguns shells.

Observe que, em alguns shells (ash, bash, pdksh, mas não ksh ou zsh), o lado direito de um pipeline é executado em um processo separado; portanto, qualquer variável que você definir no loop é perdida. Por exemplo, o seguinte script de contagem de linhas imprime 0 nesses shells:

n=0
printf %s "$foo" |
while IFS= read -r line; do
  n=$(($n + 1))
done
echo $n

Uma solução alternativa é colocar o restante do script (ou pelo menos a parte que precisa do valor do $nloop) em uma lista de comandos:

n=0
printf %s "$foo" | {
  while IFS= read -r line; do
    n=$(($n + 1))
  done
  echo $n
}

Se agir nas linhas não vazias for bom o suficiente e a entrada não for grande, você poderá usar a divisão de palavras:

IFS='
'
set -f
for line in $(jobs); do
  # process line
done
set +f
unset IFS

Explicação: a configuração IFSde uma única nova linha faz com que a divisão de palavras ocorra somente em novas linhas (em oposição a qualquer caractere de espaço em branco na configuração padrão). set -fdesativa o globbing (ou seja, expansão de curinga), que de outra forma ocorreria com o resultado de uma substituição de comando $(jobs)ou de uma variável substitutino $foo. O forloop atua em todas as partes de $(jobs), que são todas as linhas não vazias na saída do comando. Por fim, restaure as globbing e as IFSconfigurações.


Ocorreu um problema ao definir o IFS e desabilitar o IFS. Eu acho que a coisa certa a fazer é armazenar o valor antigo do IFS e definir o IFS novamente para esse valor antigo. Eu não sou um especialista no bash, mas na minha experiência, isso leva você de volta ao comportamento original.
Bjorn Roche

1
@BjornRoche: dentro de uma função, use local IFS=something. Não afetará o valor do escopo global. IIRC, unset IFSvocê não volta ao padrão (e certamente não funciona se não fosse o padrão anteriormente).
Peter Cordes

14

Problema: se você usar o loop while, ele será executado em subshell e todas as variáveis ​​serão perdidas. Solução: use para loop

# change delimiter (IFS) to new line.
IFS_BAK=$IFS
IFS=$'\n'

for line in $variableWithSeveralLines; do
 echo "$line"

 # return IFS back if you need to split new line by spaces:
 IFS=$IFS_BAK
 IFS_BAK=
 lineConvertedToArraySplittedBySpaces=( $line )
 echo "{lineConvertedToArraySplittedBySpaces[0]}"
 # return IFS back to newline for "for" loop
 IFS_BAK=$IFS
 IFS=$'\n'

done 

# return delimiter to previous value
IFS=$IFS_BAK
IFS_BAK=

MUITO OBRIGADO!! Todas as soluções acima falharam para mim.
hax0r_n_code

canalizar para um while readloop no bash significa que o loop while está em um subshell, portanto, as variáveis ​​não são globais. while read;do ;done <<< "$var"torna o corpo do loop não um subconjunto. (Bash recentes tem a opção de colocar o corpo de um cmd | whileloop não em um subnível, como ksh sempre teve.)
Peter Cordes

Veja também este post relacionado .
Curinga

10

Nas versões recentes do bash, use mapfileou readarraypara ler com eficiência a saída do comando em matrizes

$ readarray test < <(ls -ltrR)
$ echo ${#test[@]}
6305

Isenção de responsabilidade: exemplo horrível, mas você pode criar um comando melhor do que você mesmo


É uma boa maneira, mas ninhadas / var / tmp com arquivos temporários no meu sistema. +1 de qualquer maneira
Eugene Yarmash 22/03

@eugene: isso é engraçado. Em que sistema (distro / OS) está instalado?
sehe

É o FreeBSD 8. Como se reproduzir: coloque readarrayuma função e chame a função algumas vezes.
Eugene Yarmash 23/03

Bom, @sehe. +1
Teresa e Junior

7
jobs="$(jobs)"
while IFS= read
do
    echo $REPLY
done <<< "$jobs"

Referências:


3
-ré uma boa ideia também; Ela impede que \` interpretation... (it is in your links, but its probably worth mentioning, just to round out your IFS = `(que é essencial para evitar a perda de espaço em branco)
Peter.O

-1

Você pode usar <<< para simplesmente ler a variável que contém os dados separados por nova linha:

while read -r line
do 
  echo "A line of input: $line"
done <<<"$lines"

1
Bem-vindo ao Unix e Linux! Isso essencialmente duplica uma resposta de quatro anos atrás. Não poste uma resposta, a menos que você tenha algo novo para contribuir.
G-Man diz 'Reinstate Monica'
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.