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
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:
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 IFS
variável em branco e printf
a 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, jobs
isso pode causar confusão, pois esse também é o nome de um comando shell comum.
echo
para printf %s
, para que seu script funcionasse mesmo com entradas não domesticadas.
/tmp
diretó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 /tmp
apenas leitura (e não pode ser alterado por você), ficará feliz com a possibilidade de usar uma solução alternativa, por exemplo, com o printf
cano.
printf "%s\n" "$var" | while IFS= read -r line
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 $foo
literalmente, enquanto que echo "$foo"
pode ser interpretado $foo
como uma opção para o comando echo se começar com a -
e pode expandir as seqüências de barra invertida $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; 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 $n
loop) 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 IFS
de 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 -f
desativa 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 for
loop 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 IFS
configurações.
local IFS=something
. Não afetará o valor do escopo global. IIRC, unset IFS
você não volta ao padrão (e certamente não funciona se não fosse o padrão anteriormente).
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=
while read
loop 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 | while
loop não em um subnível, como ksh sempre teve.)
Nas 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
Isenção de responsabilidade: exemplo horrível, mas você pode criar um comando melhor do que você mesmo
readarray
uma função e chame a função algumas vezes.
jobs="$(jobs)"
while IFS= read
do
echo $REPLY
done <<< "$jobs"
Referências:
-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)
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"
while IFS= read
.... Se você quer evitar \ interpretação, então useread -r