Como canalizar o stdout de um comando, dependendo do resultado do código de saída

5

Expandindo de esta pergunta , temos um caso de uso em que queremos canalizar o stdout de um comando, dependendo se o comando foi bem-sucedido ou não.

Começamos com um tubo básico

command | grep -P "foo"

No entanto, notamos que às vezes command não produz nada para stdout, mas tem um código de saída de 0 . Queremos ignorar este caso e aplicar apenas o grep quando o código de saída for 1

Para um exemplo prático, podemos implementar um comando como este:

OUTPUT=$(command) # exit code is 0 or 1
RESULT=$?
if [ $RESULT -eq 0 ]; then
  return $RESULT; # if the exit code is 0 then we simply pass it forward
else
  grep -P "foo" <<< $OUTPUT; # otherwise check if the stdout contains "foo"
fi

mas isso tem um número de desvantagens, ou seja, você tem que escrever um script, o que significa que você não pode simplesmente executá-lo no console. Também parece um tanto amadorístico.

Para uma sintaxe mais concisa, estou imaginando um operador ternário fictício que canaliza se o código de saída for 1 , caso contrário, ele passa o código de saída para frente.

command |?1 grep -P "foo" : $?

Existe uma série de operadores e utilitários que alcançarão esse resultado?

    
por I'll Eat My Hat 25.10.2018 / 21:18

2 respostas

17

Os comandos em um pipeline são executados ao mesmo tempo, esse é o ponto inteiro dos canais e do mecanismo de comunicação entre processos.

Em:

cmd1 | cmd2

cmd1 e cmd2 são iniciados ao mesmo tempo, cmd2 processa os dados que cmd1 grava conforme aparecem.

Se você quisesse que cmd2 fosse iniciado somente se cmd1 tivesse falhado, você teria que iniciar cmd2 após cmd1 ter terminado e reportado seu status de saída, então você não poderia usar um canal, você teria que usar um arquivo temporário que armazena todos os dados que o cmd1 produziu:

 cmd1 > file || cmd2 < file; rm -f file

Ou armazene na memória como no seu exemplo, mas tem vários outros problemas (como $(...) removendo todos os caracteres de nova linha à direita, e a maioria dos shells não consegue lidar com bytes NUL lá, sem mencionar os problemas de dimensionamento de grandes saídas).

No Linux e com shells como zsh ou bash que armazenam aqui-documents e here-strings em arquivos temporários, você poderia fazer:

{ cmd1 > /dev/fd/3 || cmd2 <&3 3<&-; } 3<<< ignored

Para permitir que o shell lide com a criação e limpeza do arquivo temporário.

manualmente, POSIXly:

tmpfile=$(
  echo 'mkstemp(template)' |
    m4 -D template="${TMPDIR:-/tmp}/XXXXXX"
) && [ -n "$tmpfile" ] && (
  rm -f -- "$tmpfile" || exit
  cmd1 >&3 3>&- 4<&- ||
    cmd2 <&4 4<&- 3>&-) 3> "$tmpfile" 4< "$tmpfile"

Alguns sistemas possuem um comando mktemp não padrão (embora com uma interface que varia entre sistemas) que facilita um pouco a criação do arquivo temporário ( tmpfile=$(mktemp) deve ser suficiente com a maioria da implementação, embora alguns não criem o arquivo então você pode precisar ajustar o umask ). O [ -n "$tmpfile" ] não deve ser necessário com m4 de implementações compatíveis, mas pelo menos o GNU m4 não é compatível, pois não retorna um status de saída diferente de zero quando a chamada mkstemp() falha.

Observe também que não há nada que o impeça de executar qualquer código no console . Seu "script" pode ser inserido da mesma forma no prompt de um shell interativo (exceto pela parte return que assume que o código está em uma função), embora seja possível simplificá-lo para:

output=$(cmd) || grep foo <<< "$output"
    
por 25.10.2018 / 21:23
5

Observação: como a pergunta foi marcada com , presumo que você possa use recursos bash.

Dado o seu código de exemplo, parece que o que você quer fazer é:

  • Use o status de saída do comando se for 0,
  • Use o status de saída de grep de outra forma.

Executar grep em um pipeline vazio não custa nada, portanto, uma opção seria canalizá-lo de qualquer maneira e verificar o status de saída do comando, que você pode obter usando o PIPESTATUS array no bash:

$ (echo foo; exit 1) | grep foo
foo
$ echo "${PIPESTATUS[@]}"
1 0  

Aqui, a subshell saiu com 1, grep saiu com 0.

Então você poderia fazer algo como:

(command | grep -P "foo"; exit "$((PIPESTATUS[0] ? $? : 0))")

A expressão pode ser simplificada, mas você tem a ideia.

Há também a opção de simplesmente falsificar a saída:

(command && echo foo) | grep -P foo

Onde echo foo será executado somente se o comando for bem sucedido, fazendo o grep ter sucesso também.

    
por 26.10.2018 / 12:07