Para o benefício do leitor, esta receita aqui
- pode ser reutilizado como oneliner para capturar stderr em uma variável
- ainda dá acesso ao código de retorno do comando
- Sacrifica um descritor de arquivo temporário 3 (que pode ser alterado por você, é claro)
- E não expõe esses descritores de arquivo temporários ao comando interno
Se você quiser pegar stderr
de algum command
em var
que você pode fazer
{ var="$( { command; } 2>&1 1>&3 3>&- )"; } 3>&1;
Depois você tem tudo:
echo "command gives $? and stderr '$var'";
Se command
é simples (não algo parecido a | b
), você pode deixar o interior {}
fora:
{ var="$(command 2>&1 1>&3 3>&-)"; } 3>&1;
Embrulhado em uma bash
função reutilizável fácil (provavelmente precisa da versão 3 e posterior local -n
):
: catch-stderr var cmd [args..]
catch-stderr() { local -n v="$1"; shift && { v="$("$@" 2>&1 1>&3 3>&-)"; } 3>&1; }
Explicado:
local -n
aliases "$ 1" (que é a variável para catch-stderr
)
3>&1
usa o descritor de arquivo 3 para salvar pontos stdout
{ command; }
(ou "$ @") executa o comando na saída capturando $(..)
- Observe que a ordem exata é importante aqui (fazer da maneira errada embaralha os descritores de arquivo incorretamente):
2>&1
redireciona stderr
para a saída capturando$(..)
1>&3
redireciona para stdout
fora da captura de saída de $(..)
volta para o "externo" stdout
que foi salvo no descritor de arquivo 3. Observe que stderr
ainda se refere ao local onde o FD 1 apontou antes: Para a captura de saída$(..)
3>&-
em seguida, fecha o descritor de arquivo 3, pois ele não é mais necessário, de modo que de command
repente não aparece nenhum descritor de arquivo aberto desconhecido. Observe que o shell externo ainda tem o FD 3 aberto, mas command
não o verá.
- O último é importante, porque alguns programas como o
lvm
queixam-se de descritores de arquivos inesperados. E lvm
reclama stderr
- exatamente o que vamos capturar!
Você pode capturar qualquer outro descritor de arquivo com esta receita, se você se adaptar adequadamente. Exceto o descritor de arquivo 1, é claro (aqui a lógica de redirecionamento estaria errada, mas para o descritor de arquivo 1 você pode simplesmente usar var=$(command)
como de costume).
Observe que isso sacrifica o descritor de arquivo 3. Se você precisar desse descritor de arquivo, sinta-se à vontade para alterar o número. Mas esteja ciente de que algumas conchas (da década de 1980) podem entender 99>&1
como argumento 9
seguido por 9>&1
(isso não é problema para bash
).
Observe também que não é particularmente fácil tornar esse FD 3 configurável por meio de uma variável. Isso torna as coisas muito ilegíveis:
: catch-var-from-fd-by-fd variable fd-to-catch fd-to-sacrifice command [args..]
catch-var-from-fd-by-fd()
{
local -n v="$1";
local fd1="$2" fd2="$3";
shift 3 || return;
eval exec "$fd2>&1";
v="$(eval '"$@"' "$fd1>&1" "1>&$fd2" "$fd2>&-")";
eval exec "$fd2>&-";
}
Nota de segurança: Os três primeiros argumentos a catch-var-from-fd-by-fd
não devem ser retirados de terceiros. Sempre dê explicitamente de maneira "estática".
Então não-não-não catch-var-from-fd-by-fd $var $fda $fdb $command
, nunca faça isso!
Se você passar um nome de variável variável, faça pelo menos o seguinte:
local -n var="$var"; catch-var-from-fd-by-fd var 3 5 $command
Isso ainda não o protegerá de todas as explorações, mas pelo menos ajuda a detectar e evitar erros de script comuns.
Notas:
catch-var-from-fd-by-fd var 2 3 cmd..
é o mesmo que catch-stderr var cmd..
shift || return
é apenas uma maneira de evitar erros feios, caso você esqueça de fornecer o número correto de argumentos. Talvez encerrar o shell seja outra maneira (mas isso dificulta o teste a partir da linha de comando).
- A rotina foi escrita de tal forma que é mais fácil de entender. Pode-se reescrever a função de forma que ela não precise
exec
, mas então fica realmente feia.
- Essa rotina pode ser reescrita para
bash
outras que não sejam necessárias local -n
. No entanto, você não pode usar variáveis locais e isso fica extremamente feio!
- Observe também que os
eval
s são usados de maneira segura. Geralmente eval
é considerado perigoso. No entanto, neste caso, não é mais mau do que usar "$@"
(para executar comandos arbitrários). No entanto, certifique-se de usar a citação exata e correta, como mostrado aqui (caso contrário, isso se torna muito, muito perigoso ).
ERROR=$(./useless.sh | sed 's/Output/Useless/' 2>&1 1>/dev/ttyX)