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

Tenho uma variável que contém a saída de várias linhas de um comando. Qual é a maneira mais eficiente de ler a saída linha por linha da variável?

Por exemplo:

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

Resposta

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 multilinha é definir uma IFS variável em branco e printf a variável com uma nova linha final:

# 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") 

Observação: de acordo com shellcheck sc2031 , o uso de subposição de processo é preferível a um tubo para evitar [sutilmente] criando um subshell.

Além disso, lembre-se de que nomear a variável jobs pode causar confusão, pois esse também é o nome de um comando de shell comum.

Comentários

  • Se você quiser manter todos os seus espaços em branco, use while IFS= read …. Se você deseja evitar \ interpretação, então use read -r
  • Eu ‘ fixei os pontos fred.bear mencionados, bem como alterei echo para printf %s, de modo que seu script funcionaria mesmo com entrada não domesticada.
  • Para ler de uma variável multilinha, uma cadeia de caracteres é preferível a piping de printf (consulte l0b0 ‘ s resposta).
  • @ata Embora eu ‘ tenha ouvido isso ” preferível ” com frequência suficiente, deve-se observar que uma cadeia de caracteres sempre exige que o diretório /tmp 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 /tmp sendo somente leitura (e não alterável por você), você ficará feliz com a possibilidade de usar uma solução alternativa, por exemplo. g. com o printf pipe.
  • No segundo exemplo, se a variável multilinha ‘ não contém um à direita da nova linha você perderá o último elemento. Altere para: printf "%s\n" "$var" | while IFS= read -r line

Resposta

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

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

Se você tiver o dados já em uma variável:

printf %s "$foo" | … 

printf %s "$foo" é quase idêntico a echo "$foo", mas imprime $foo literalmente, enquanto echo "$foo" pode interpretar $foo como uma opção ao comando echo se ele começar com - e pode expandir as sequências de barra invertida em $foo em 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, de modo que qualquer variável definida no loop é perdida. Por exemplo, o seguinte script de contagem de linha imprime 0 nestes 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 de $n do loop) em uma lista de comandos:

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

Se atuar nas linhas não vazias é bom o suficiente e a entrada não é grande, você pode usar a divisão de palavras:

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

Explicação: configuração IFS para uma única nova linha faz com que a divisão de palavras ocorra apenas nas novas linhas (ao contrário de qualquer caractere de espaço em branco na configuração padrão). set -f desativa o globbing (ou seja, expansão de curinga), que de outra forma aconteceria com o resultado de uma substituição de comando $(jobs) ou uma substituição de variável $foo. O loop for 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 configurações globais e IFS para valores equivalentes aos padrões.

Comentários

  • Tive problemas com a configuração do IFS e com a desativação do IFS. Acho que a coisa certa a fazer é armazenar o valor antigo do IFS e definir o IFS de volta para esse valor antigo. Eu ‘ não sou um especialista em bash, mas na minha experiência, isso leva você de volta ao comportamento original.
  • @BjornRoche: dentro de uma função, use local IFS=something. Ele não ‘ afetará o valor do escopo global. IIRC, unset IFS não ‘ não leva você de volta ao padrão (e certamente não ‘ funcionaria se não fosse ‘ o padrão de antemão).
  • Estou me perguntando se usar set no caminho mostrado no último exemplo está correto.O fragmento de código assume que set +f estava ativo no início e, portanto, restaura essa configuração no final. No entanto, essa suposição pode estar errada. E se set -f estivesse ativo no início?
  • @Binarus Eu só restauro as configurações equivalentes aos padrões. Na verdade, se você deseja restaurar as configurações originais, é necessário trabalhar mais. Para set -f, salve o $- original. Para IFS, é ‘ irritantemente confuso se você não ‘ não tiver local e você deseja apoiar o caso não definido; se você quiser restaurá-lo, recomendo aplicar a invariante que IFS permanece definida.
  • Usar local faria na verdade, é a melhor solução, porque local - torna as opções de shell locais e local IFS torna IFS local. Infelizmente, local só é válido dentro de funções, o que torna necessária a reestruturação do código. Sua sugestão de introduzir a política de que IFS está sempre definida também parece muito razoável e resolve a maior parte do problema. Obrigado!

Resposta

Problema: se você usar o loop while, ele será executado em subshell e todas as variáveis serão perdido. Solução: use for 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= 

Comentários

  • MUITO OBRIGADO !! Todas as soluções acima falharam para mim.
  • encadeando em um while read loop em bash significa que o loop while está em um subshell, então as variáveis não são ‘ t global. while read;do ;done <<< "$var" torna o corpo do loop não um subshell. (O bash recente tem a opção de colocar o corpo de um loop cmd | while em um subshell, como o ksh sempre teve.)
  • Consulte também esta postagem relacionada .
  • Em situações semelhantes, achei surpreendentemente difícil tratar IFS corretamente. Esta solução também tem um problema: e se IFS não for definido no início (ou seja, for indefinido)? Ele será definido em todos os casos após esse trecho de código; isto não ‘ não parece estar correto.

Resposta

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

Referências:

Comentários

  • -r também é uma boa ideia; Impede \` 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ços em branco)
  • Apenas esta solução funcionou para mim. Obrigado brah.
  • Não ‘ esta solução sofre do mesmo problema que é mencionado nos comentários para @dogbane ‘ s resposta? E se a última linha da variável não terminar com um caractere de nova linha?
  • Esta resposta fornece a maneira mais limpa de alimentar o conteúdo de uma variável para o while read construir.

Resposta

Em versões recentes do bash, use mapfile ou readarray para ler com eficiência a saída do comando em matrizes

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

Aviso de isenção: exemplo horrível, mas você provavelmente pode vir com um comando melhor para usar do que você mesmo

Comentários

  • É ‘ de uma maneira agradável, mas cria / var / tmp com arquivos temporários no meu sistema. +1 de qualquer maneira
  • @eugene: isso ‘ é engraçado. Em que sistema (distro / OS) está isso?
  • É ‘ s FreeBSD 8. Como reproduzir: put readarray em uma função e chame a função algumas vezes.
  • Muito bem, @sehe. +1

Resposta

Os padrões comuns para resolver esse problema foram fornecidos nas outras respostas.

No entanto, gostaria de adicionar minha abordagem, embora não tenha certeza de quão eficiente é. Mas é (pelo menos para mim) bastante compreensível, não altera a variável original (todas as soluções que usam read deve ter a variável em questão com uma nova linha final e, portanto, adicioná-la, o que altera a variável), não cria subshells (o que todas as soluções baseadas em pipe fazem), não usa aqui -strings (que têm seus próprios problemas) e não usa substituição de processo (nada contra isso, mas às vezes é um pouco difícil de entender).

Na verdade, não entendo por que bash” REs integrados são usados tão raramente.Talvez eles não sejam portáteis, mas como o OP usou a tag bash, isso não me “impedirá 🙂

#!/bin/bash function ProcessLine() { printf "%s" "$1" } function ProcessText1() { local Text=$1 local Pattern=$"^([^\n]*\n)(.*)$" while [[ "$Text" =~ $Pattern ]]; do ProcessLine "${BASH_REMATCH[1]}" Text="${BASH_REMATCH[2]}" done ProcessLine "$Text" } function ProcessText2() { local Text=$1 local Pattern=$"^([^\n]*\n)(.*)$" while [[ "$Text" =~ $Pattern ]]; do ProcessLine "${BASH_REMATCH[1]}" Text="${BASH_REMATCH[2]}" done } function ProcessText3() { local Text=$1 local Pattern=$"^([^\n]*\n?)(.*)$" while [[ ("$Text" != "") && ("$Text" =~ $Pattern) ]]; do ProcessLine "${BASH_REMATCH[1]}" Text="${BASH_REMATCH[2]}" done } MyVar1=$"a1\nb1\nc1\n" MyVar2=$"a2\n\nb2\nc2" MyVar3=$"a3\nb3\nc3" ProcessText1 "$MyVar1" ProcessText1 "$MyVar2" ProcessText1 "$MyVar3" 

Resultado:

root@cerberus:~/scripts# ./test4 a1 b1 c1 a2 b2 c2a3 b3 c3root@cerberus:~/scripts# 

Algumas notas:

O comportamento depende de qual variante de ProcessText que você usa. No exemplo acima, usei ProcessText1.

Observe que

  • ProcessText1 mantém caracteres de nova linha no final das linhas
  • ProcessText1 processa a última linha da variável (que contém o texto c3) embora essa linha não contenha um caractere de nova linha à direita. Devido à falta de uma nova linha à direita, o prompt de comando após a execução do script é anexado ao último linha da variável sem ser separada da saída.
  • ProcessText1 sempre considera a parte entre a última nova linha na variável e o final da variável como uma linha , mesmo se estiver vazio; é claro, essa linha, vazia ou não, não tem um caractere de nova linha à direita. Ou seja, mesmo se o último caractere na variável for uma nova linha, ProcessText1 tratará a parte vazia (string nula) entre a última nova linha e o final da variável como um (ainda vazio) linha e irá passá-lo para o processamento de linha. Você pode evitar facilmente esse comportamento envolvendo a segunda chamada para ProcessLine em uma condição apropriada de verificar se está vazio; no entanto, acho que é mais lógico deixar como está.

ProcessText1 precisa chamar ProcessLine em dois lugares, o que pode ser desconfortável se você quiser colocar um bloco de código que processa diretamente a linha, em vez de chamar uma função que processa a linha; você teria que repetir o código que está sujeito a erros.

Em contraste, ProcessText3 processa a linha ou chama a função respectiva apenas em um lugar, tornando a substituição a chamada de função por um bloco de código um acéfalo. Isso tem o custo de duas while condições em vez de uma. Além das diferenças de implementação, ProcessText3 se comporta exatamente da mesma forma que ProcessText1, exceto que não considera a parte entre o último caractere de nova linha em a variável e o final da variável como uma linha se essa parte estiver vazia. Ou seja, ProcessText3 não entrará em processamento de linha após o último caractere de nova linha da variável se esse caractere de nova linha for o último caractere na variável.

ProcessText2 funciona como ProcessText1, exceto que as linhas devem ter um caractere de nova linha à direita. Ou seja, a parte entre o último caractere de nova linha na variável e o final da variável não é considerada uma linha e é silenciosamente jogada fora. Conseqüentemente, se a variável não contém nenhum caractere de nova linha, nenhum processamento de linha acontece.

Gosto dessa abordagem mais do que das outras soluções mostradas acima, mas provavelmente esqueci algo (não tenho muita experiência em bash programação e não estar muito interessado em outros shells).

Resposta

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

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

Comentários

  • Bem-vindo ao Unix & Linux! Isso basicamente duplica uma resposta de quatro anos atrás. Não poste uma resposta a menos que você tenha algo novo para contribuir.

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *