Como armazenar erro padrão em uma variável

182

Digamos que eu tenho um script como o seguinte:

useless.sh

echo "This Is Error" 1>&2
echo "This Is Output" 

E eu tenho outro script de shell:

alsoUseless.sh

./useless.sh | sed 's/Output/Useless/'

Eu quero capturar "This Is Error", ou qualquer outro stderr de useless.sh, em uma variável. Vamos chamá-lo de erro.

Observe que estou usando stdout para alguma coisa. Quero continuar usando o stdout, portanto, redirecionar o stderr para o stdout não é útil, neste caso.

Então, basicamente, eu quero fazer

./useless.sh 2> $ERROR | ...

mas isso obviamente não funciona.

Eu também sei que eu poderia fazer

./useless.sh 2> /tmp/Error
ERROR=`cat /tmp/Error`

mas isso é feio e desnecessário.

Infelizmente, se não houver respostas aqui, é isso que terei que fazer.

Espero que exista outro caminho.

Alguém tem alguma ideia melhor?

psycotica0
fonte
4
Para que exatamente deseja usar o stdout? Deseja simplesmente visualizá-lo no console? Ou você está capturando / redirecionando sua saída? Se for apenas para o console, você redireciona o stdout para o console e o stderr para o stdout para capturá-lo:ERROR=$(./useless.sh | sed 's/Output/Useless/' 2>&1 1>/dev/ttyX)
Tim Kersten

Respostas:

91

Seria mais fácil capturar o arquivo de erro assim:

ERROR=$(</tmp/Error)

O shell reconhece isso e não precisa executar ' cat' para obter os dados.

A questão maior é difícil. Eu não acho que exista uma maneira fácil de fazer isso. Você precisaria criar todo o pipeline no sub-shell, enviando sua saída padrão final para um arquivo, para que você possa redirecionar os erros para a saída padrão.

ERROR=$( { ./useless.sh | sed s/Output/Useless/ > outfile; } 2>&1 )

Observe que o ponto e vírgula é necessário (em conchas clássicas - Bourne, Korn - com certeza; provavelmente em Bash também). O ' {}' faz o redirecionamento de E / S sobre os comandos incluídos. Como está escrito, também capturaria erros sed.

AVISO: Código formalmente não testado - use por sua conta e risco.

Jonathan Leffler
fonte
1
Eu esperava que houvesse algum truque realmente louco que eu não conhecia, mas parece que é isso. Obrigado.
psycotica0
9
Se você não precisar da saída padrão, poderá redirecioná-la para, em /dev/nullvez de outfile(Se você é como eu, encontrou essa pergunta pelo Google e não tem os mesmos requisitos do OP)
Mark Eirich,
2
Para uma resposta sem arquivos temporários, veja aqui .
Tom Hale
1
Aqui está uma maneira de fazer isso sem redirecioná-lo para arquivos; brinca com a troca stdoute para stderrfrente e para trás. Mas cuidado , como é dito aqui : no bash, seria melhor não supor que o descritor de arquivo 3 não seja usado " .
Golar Ramblar
69

alsoUseless.sh

Isso permitirá que você canalizar a saída do seu useless.shroteiro através de um comando como sede salvar o stderrem uma variável chamada error. O resultado do tubo é enviado stdoutpara exibição ou a ser canalizado para outro comando.

Ele configura alguns descritores de arquivo extras para gerenciar os redirecionamentos necessários para fazer isso.

#!/bin/bash

exec 3>&1 4>&2 #set up extra file descriptors

error=$( { ./useless.sh | sed 's/Output/Useless/' 2>&4 1>&3; } 2>&1 )

echo "The message is \"${error}.\""

exec 3>&- 4>&- # release the extra file descriptors
Pausado até novo aviso.
fonte
4
É uma boa técnica usar 'exec' para definir e fechar descritores de arquivo. O fechamento não é realmente necessário se o script sair imediatamente depois.
11139 Jonathan Leffler
3
Como eu capturaria ambas stderre stdoutem variáveis?
Gingi
Excelente. Isso me ajuda a implementar uma dry_runfunção que pode escolher com segurança entre repetir seus argumentos e executá-los, independentemente de o comando que está sendo executado a seco estiver sendo canalizado para algum outro arquivo.
Mihai Danila
1
@ t00bs: readnão aceita entrada de um tubo. Você pode usar outras técnicas para alcançar o que está tentando demonstrar.
Pausado até novo aviso.
2
Poderia ser mais simples, com: error = $ (./useless.sh | sed 's / Output / Useless /' 2> & 1 1> & 3)
Jocelyn
64

Redirecionado stderr para stdout, stdout para / dev / null e, em seguida, use os backticks ou $()para capturar o stderr redirecionado:

ERROR=$(./useless.sh 2>&1 >/dev/null)
Chas. Owens
fonte
8
Esta é a razão pela qual incluí o pipe no meu exemplo. Ainda quero a saída padrão e quero que ela faça outras coisas, vá a outros lugares.
Psycotica0 /
Para comandos que enviar a saída apenas para stderr, a maneira simples de capturá-lo é, por exemploPY_VERSION="$(python --version 2>&1)"
John Mark
9

Há muitas duplicatas para esta pergunta, muitas das quais com um cenário de uso um pouco mais simples, no qual você não deseja capturar stderr e stdout e o código de saída, tudo ao mesmo tempo.

if result=$(useless.sh 2>&1); then
    stdout=$result
else
    rc=$?
    stderr=$result
fi

funciona para o cenário comum em que você espera uma saída adequada no caso de êxito ou uma mensagem de diagnóstico no stderr no caso de falha.

Observe que as instruções de controle do shell já examinam $?sob o capô; então qualquer coisa que pareça

cmd
if [ $? -eq 0 ], then ...

é apenas uma maneira desajeitada e uniomática de dizer

if cmd; then ...
triplo
fonte
Isso funcionou para mim: my_service_status = $ (service my_service status 2> & 1) Obrigado !!
JRichardsz #
6
# command receives its input from stdin.
# command sends its output to stdout.
exec 3>&1
stderr="$(command </dev/stdin 2>&1 1>&3)"
exitcode="${?}"
echo "STDERR: $stderr"
exit ${exitcode}
human9
fonte
1
commandé uma má escolha aqui, na medida em que há realmente um builtin com esse nome. Pode torná-lo yourCommandou algo assim, para ser mais explícito.
Charles Duffy
4

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 stderrde algum commandem varque 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 bashfunçã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 -naliases "$ 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>&1redireciona stderrpara a saída capturando$(..)
    • 1>&3redireciona para stdoutfora da captura de saída de $(..)volta para o "externo" stdoutque foi salvo no descritor de arquivo 3. Observe que stderrainda 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 commandrepente não aparece nenhum descritor de arquivo aberto desconhecido. Observe que o shell externo ainda tem o FD 3 aberto, mas commandnão o verá.
    • O último é importante, porque alguns programas como o lvmqueixam-se de descritores de arquivos inesperados. E lvmreclama 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>&1como argumento 9seguido 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-fdnã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 bashoutras 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 evals 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 ).
Tino
fonte
3

Aqui está como eu fiz isso:

#
# $1 - name of the (global) variable where the contents of stderr will be stored
# $2 - command to be executed
#
captureStderr()
{
    local tmpFile=$(mktemp)

    $2 2> $tmpFile

    eval "$1=$(< $tmpFile)"

    rm $tmpFile
}

Exemplo de uso:

captureStderr err "./useless.sh"

echo -$err-

Ele faz usar um arquivo temporário. Mas pelo menos as coisas feias estão envolvidas em uma função.

tfga
fonte
@ShadowWizard Pouca dúvida do meu lado. Em francês, o cólon é geralmente precedido por um espaço. Por engano, aplico esta mesma regra com respostas em inglês . Depois de verificar isso , sei que não vou cometer esse erro novamente.
Stephan
Felicidades @Stephan, isso também foi discutido aqui . :)
Shadow Wizard é Ear For You
1
Existem maneiras mais seguras de fazer isso do que usar eval. Por exemplo, printf -v "$1" '%s' "$(<tmpFile)"não corre o risco de executar código arbitrário se sua TMPDIRvariável tiver sido definida como um valor malicioso (ou se o nome da variável de destino contiver esse valor).
Charles Duffy
1
Da mesma forma, rm -- "$tmpFile"é mais robusto que rm $tmpFile.
Charles Duffy
2

Esse é um problema interessante para o qual eu esperava que houvesse uma solução elegante. Infelizmente, acabo com uma solução semelhante ao Sr. Leffler, mas acrescentarei que você pode chamar de inútil de dentro de uma função Bash para melhorar a legibilidade:

#! / bin / bash

função inútil {
    /tmp/useless.sh | sed / Saída / Inútil / '
}

ERRO = $ (inútil)
echo $ ERROR

Todo outro tipo de redirecionamento de saída deve ser apoiado por um arquivo temporário.


fonte
2

POSIX

STDERR pode ser capturado com alguma mágica de redirecionamento:

$ { error=$( { { ls -ld /XXXX /bin | tr o Z ; } 1>&3 ; } 2>&1); } 3>&1
lrwxrwxrwx 1 rZZt rZZt 7 Aug 22 15:44 /bin -> usr/bin/

$ echo $error
ls: cannot access '/XXXX': No such file or directory

Observe que a tubulação de STDOUT do comando (aqui ls) é feita na parte interna { }. Se você estiver executando um comando simples (por exemplo, não um pipe), poderá remover esses aparelhos internos.

Você não pode canalizar para fora do comando, pois a tubulação cria um subshell dentro bashe zsh, e a atribuição à variável no subshell não estaria disponível para o shell atual.

festança

Em bash, seria melhor não supor que o descritor de arquivo 3 não seja usado:

{ error=$( { { ls -ld /XXXX /bin | tr o Z ; } 1>&$tmp ; } 2>&1); } {tmp}>&1; 
exec {tmp}>&-  # With this syntax the FD stays open

Observe que isso não funciona zsh.


Graças a esta resposta para a idéia geral.

Tom Hale
fonte
você pode explicar essa linha com detalhes? não entendeu 1> & $ tmp; {error = $ ({{ls -ld / XXXX / bin | tr o Z;} 1> & $ tmp;} 2> & 1); } {tmp}> & 1;
Thiago Conrado
1

Este post me ajudou a encontrar uma solução semelhante para meus próprios propósitos:

MESSAGE=`{ echo $ERROR_MESSAGE | format_logs.py --level=ERROR; } 2>&1`

Então, desde que nossa MESSAGE não seja uma string vazia, passamos para outras coisas. Isso nos informará se nosso format_logs.py falhou com algum tipo de exceção python.

palmbardier
fonte
1

Capturar e imprimir stderr

ERROR=$( ./useless.sh 3>&1 1>&2 2>&3 | tee /dev/fd/2 )

Demolir

Você pode usar $()para capturar stdout, mas deseja capturar stderr. Então você troca stdout e stderr. Usando fd 3 como armazenamento temporário no algoritmo de troca padrão.

Se você deseja capturar E imprimir, use teeuma duplicata. Nesse caso, a saída de teeserá capturada em $()vez de ir para o console, mas stderr (of tee) ainda irá para o console, por isso usamos como segunda saída para teeo arquivo especial, /dev/fd/2pois teeespera um caminho de arquivo em vez de um fd número.

NOTA: São muitos redirecionamentos em uma única linha e a ordem é importante. $()está pegando o stdout teeno final do pipeline e o próprio pipeline direciona stdout ./useless.shpara o stdin de teeAFTER depois do qual trocamos stdin e stdout ./useless.sh.

Usando stdout de ./useless.sh

O OP disse que ainda queria usar (e não apenas imprimir) stdout, como ./useless.sh | sed 's/Output/Useless/'.

Não há problema, basta fazê-lo ANTES de trocar stdout e stderr. Eu recomendo movê-lo para uma função ou arquivo (also-useless.sh) e chamar isso no lugar de ./useless.sh na linha acima.

No entanto, se você deseja CAPTURAR stdout AND stderr, acho que você deve recorrer a arquivos temporários, porque ele $()fará apenas um de cada vez e cria um subshell do qual você não pode retornar variáveis.

SensorSmith
fonte
1

Iterando um pouco a resposta de Tom Hale, achei possível agrupar o yoga de redirecionamento em uma função para facilitar a reutilização. Por exemplo:

#!/bin/sh

capture () {
    { captured=$( { { "$@" ; } 1>&3 ; } 2>&1); } 3>&1
}

# Example usage; capturing dialog's output without resorting to temp files
# was what motivated me to search for this particular SO question
capture dialog --menu "Pick one!" 0 0 0 \
        "FOO" "Foo" \
        "BAR" "Bar" \
        "BAZ" "Baz"
choice=$captured

clear; echo $choice

É quase certamente possível simplificar ainda mais isso. Não testei especialmente, mas parece funcionar com o bash e o ksh.

AmareloApple
fonte
0

Se você deseja ignorar o uso de um arquivo temporário, poderá usar a substituição de processo. Ainda não consegui funcionar. Esta foi minha primeira tentativa:

$ .useless.sh 2> >( ERROR=$(<) )
-bash: command substitution: line 42: syntax error near unexpected token `)'
-bash: command substitution: line 42: `<)'

Então eu tentei

$ ./useless.sh 2> >( ERROR=$( cat <() )  )
This Is Output
$ echo $ERROR   # $ERROR is empty

Contudo

$ ./useless.sh 2> >( cat <() > asdf.txt )
This Is Output
$ cat asdf.txt
This Is Error

Portanto, a substituição do processo geralmente está fazendo a coisa certa ... infelizmente, sempre que envolvo o STDIN por dentro >( )com algo na $()tentativa de capturar isso em uma variável, perco o conteúdo de $(). Eu acho que isso ocorre porque $()lança um subprocesso que não tem mais acesso ao descritor de arquivo em / dev / fd que pertence ao processo pai.

A substituição de processos me deu a capacidade de trabalhar com um fluxo de dados que não está mais no STDERR, infelizmente não parece capaz de manipulá-lo da maneira que desejo.

Barton Chittenden
fonte
1
Se você o fizesse ./useless.sh 2> >( ERROR=$( cat <() ); echo "$ERROR" ), veria a saída de ERROR. O problema é que a substituição do processo é executada em um sub-shell, portanto, o valor definido no sub-shell não afeta o shell pai.
Jonathan Leffler 4/04
0
$ b=$( ( a=$( (echo stdout;echo stderr >&2) ) ) 2>&1 )
$ echo "a=>$a b=>$b"
a=>stdout b=>stderr
Mario Wolff
fonte
3
Isto parece uma boa idéia, mas no Mac OSX 10.8.5, imprimea=> b=>stderr
Heath Borders
3
Eu concordo com @HeathBorders; isso não produz a saída mostrada. O problema aqui é que aé avaliado e atribuído em um sub shell, e a atribuição no sub shell não afeta o shell pai. (Testado no Ubuntu 14.04 LTS e no Mac OS X 10.10.1.)
Jonathan Leffler
O mesmo no Windows GitBash. Então, isso não funciona. ( GNU bash, version 4.4.12(1)-release (x86_64-pc-msys))
Kirby
Não funciona em SLE 11.4qualquer um e produz o efeito descrito por @JonathanLeffler
smarber
Embora esse código possa responder à pergunta, fornecer um contexto adicional a respeito de por que e / ou como esse código responde à pergunta melhora seu valor a longo prazo.
β.εηοιτ.βε
0

No zsh:

{ . ./useless.sh > /dev/tty } 2>&1 | read ERROR
$ echo $ERROR
( your message )
Ray Andrews
fonte
0

Para evitar erros em seus comandos:

execute [INVOKING-FUNCTION] [COMMAND]

execute () {
    function="${1}"
    command="${2}"
    error=$(eval "${command}" 2>&1 >"/dev/null")

    if [ ${?} -ne 0 ]; then
        echo "${function}: ${error}"
        exit 1
    fi
}

Inspirado na fabricação Lean:

Alberto Salvia Novella
fonte
A solução idiomática é executar a atribuição dentro do arquivo if. Deixe-me postar uma solução separada.
Tripleee
0

Uma solução simples

{ ERROR=$(./useless.sh 2>&1 1>&$out); } {out}>&1
echo "-"
echo $ERROR

Vai produzir:

This Is Output
-
This Is Error
K - A toxicidade no SO está crescendo.
fonte
0

Melhorando a resposta da YellowApple :

Esta é uma função Bash para capturar stderr em qualquer variável

stderr_capture_example.sh:

#!/usr/bin/env bash

# Capture stderr from a command to a variable while maintaining stdout
# @Args:
# $1: The variable name to store the stderr output
# $2: Vararg command and arguments
# @Return:
# The Command's Returnn-Code or 2 if missing arguments
function capture_stderr {
  [ $# -lt 2 ] && return 2
  local stderr="$1"
  shift
  {
    printf -v "$stderr" '%s' "$({ "$@" 1>&3; } 2>&1)"
  } 3>&1
}

# Testing with a call to erroring ls
LANG=C capture_stderr my_stderr ls "$0" ''

printf '\nmy_stderr contains:\n%s' "$my_stderr"

Teste:

bash stderr_capture_example.sh

Resultado:

 stderr_capture_example.sh

my_stderr contains:
ls: cannot access '': No such file or directory

Esta função pode ser usada para capturar a escolha retornada de um dialogcomando.

Léa Gris
fonte