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
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 queset +f
estava ativo no início e, portanto, restaura essa configuração no final. No entanto, essa suposição pode estar errada. E seset -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. ParaIFS
, é ‘ irritantemente confuso se você não ‘ não tiverlocal
e você deseja apoiar o caso não definido; se você quiser restaurá-lo, recomendo aplicar a invariante queIFS
permanece definida. - Usar
local
faria na verdade, é a melhor solução, porquelocal -
torna as opções de shell locais elocal IFS
tornaIFS
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 queIFS
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 loopcmd | 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 seIFS
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 textoc3
) 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 paraProcessLine
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.
while IFS= read
…. Se você deseja evitar \ interpretação, então useread -r
echo
paraprintf %s
, de modo que seu script funcionaria mesmo com entrada não domesticada./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 oprintf
pipe.printf "%s\n" "$var" | while IFS= read -r line