Redireciona STDERR e STDOUT para variáveis diferentes sem arquivos temporários

2
func() {
    echo 'hello'
    echo 'This is an error' >&2
}

a=$(func)
b=???

Gostaria de redirecionar o stderr para a variável b sem criar um arquivo temporário.

 echo $b
 # output should be: "This is an error"

A solução que funciona, mas com um arquivo temporário:

touch temp.txt
exec 3< temp.txt
a=$(func 2> temp.txt);
cat <&3
rm temp.txt

Então, a pergunta é: como eu redireciono o stderr das function func para a variável b sem a necessidade de um arquivo temporário?

    
por smarber 14.03.2018 / 13:09

2 respostas

4

No Linux e com shells que implementam here-documents com arquivos temporários graváveis (como bash ), você pode fazer:

{
  out=$(ls /dev/null /x 2> /dev/fd/3)
  err=$(cat<&3)
} 3<<EOF
EOF

printf '%s=<%s>\n' out "$out" err "$err"

(onde ls /dev/null /x é um exemplo de comando que gera algo em stdout e stderr).

Com zsh , você também pode fazer:

(){ out=$(ls /dev/null /x 2> $1) err=$(<$1);} =(:)

(onde =(cmd) é uma forma de substituição de processo que usa arquivos temporários e (){ code; } args funções anônimas).

Em qualquer caso, você desejaria usar arquivos temporários. Qualquer solução que usasse pipes estaria sujeita a deadlocks no caso de grandes saídas. Você pode ler stdout e stderr através de dois canais separados e usar select() / poll() e algumas leituras em um loop para ler dados, pois eles vêm dos dois canais sem causar travamentos, mas isso seria bastante complicado e AFAIK, somente zsh tem select() suporte embutido e apenas yash uma interface bruta para pipe() (mais sobre isso em Lido / escreva para o mesmo descritor de arquivo com redirecionamento de shell ).

Outra abordagem poderia ser armazenar um dos fluxos na memória temporária em vez de um arquivo temporário. Como ( zsh ou bash sintaxe):

{
  IFS= read -rd '' err
  IFS= read -rd '' out
} < <({ out=$(ls /dev/null /x); } 2>&1; printf '
out= err=
while IFS= read -r line; do
  case $line in
    (out:*) out=$out${line#out:}$'\n';;
    (err:*) err=$err${line#err:}$'\n';;
  esac
done < <(
  { { ls /dev/null /x | grep --label=out --line-buffered -H '^' >&3; } 2>&1 |
     grep --label=err --line-buffered -H '^'; } 3>&1
)
%s' "$out")

(assumindo que o comando não produz qualquer NUL)

Observe que $err incluirá o caractere de nova linha à direita.

Outras abordagens podem ser para decorar a stdout e stderr de forma diferente e remover a decoração ao ler:

{
  out=$(ls /dev/null /x 2> /dev/fd/3)
  err=$(cat<&3)
} 3<<EOF
EOF

printf '%s=<%s>\n' out "$out" err "$err"

Isso pressupõe o% GNUgrep e as linhas são curtas o suficiente. Com linhas maiores que o PIPEBUF (4K no Linux), as linhas da saída dos dois grep s podem acabar sendo mutiladas em partes.

    
por 14.03.2018 / 14:43
1

Bem, capturar o stderr em uma variável e stdout em outra variável sem arquivo temporário não é nada fácil.

Aqui está um exemplo que funciona

func() {
    echo 'hello'
    echo 'This is an error' >&2
}

result=$(
    { stdout=$(func) ; } 2>&1
    echo -e "mysuperuniqueseparator\n"
    echo -e "${stdout}\n"
)
var_out=${result#*mysuperuniqueseparator$'\n'}
var_err=${result%$'\n'mysuperuniqueseparator*}

Eu não estou feliz com isso, porque é uma maneira suja, redirecionar o stderr para stdout e colocar ambos em uma variável com um separador entre eles e, em seguida, dividi-lo em duas partes.

Além disso:

Obviously, this is not robust, because either the standard output or the standard error of the command could contain whatever separator string you employ.

Extraído daqui link

    
por 14.03.2018 / 16:23