Como posso obter o bash para sair da falha de backtick de maneira semelhante ao pipefail?

47

Por isso, gosto de endurecer meus scripts bash sempre que posso (e quando não posso delegar a uma linguagem como Python / Ruby) para garantir que os erros não sejam perdidos.

Nesse sentido, eu tenho um strict.sh, que contém coisas como:

set -e
set -u
set -o pipefail

E fonte em outros scripts. No entanto, enquanto pipefail iria pegar:

false | echo it kept going | true

Não vai pegar:

echo The output is ''false; echo something else'' 

A saída seria

A saída é ''

False retorna status diferente de zero e não-stdout. Em um pipe, ele teria falhado, mas aqui o erro não foi detectado. Quando isso é realmente um cálculo armazenado em uma variável para posterior, e o valor é definido como em branco, isso pode causar problemas posteriores.

Então - existe uma maneira de obter o bash para tratar um código de retorno diferente de zero dentro de um backtick como razão suficiente para sair?

    
por Danny Staple 21.10.2011 / 12:42

6 respostas

43

O idioma exato usado na especificação Single UNIX para descrever o significado de set -e é:

When this option is on, if a simple command fails for any of the reasons listed in Consequences of Shell Errors or returns an exit status value >0, and is not [a conditional or negated command], then the shell shall immediately exit.

Existe uma ambiguidade quanto ao que acontece quando um comando desse tipo ocorre em um subshell . Do ponto de vista prático, tudo o que a subshell pode fazer é sair e retornar um status diferente de zero ao shell pai. Se o shell pai, por sua vez, sairá depende se esse status diferente de zero se traduz em um comando simples com falha no shell pai.

Um desses casos problemáticos é aquele que você encontrou: um status de retorno diferente de zero de um substituição de comando . Como esse status é ignorado, ele não faz com que o shell pai saia. Como você já descobriu , uma maneira de levar em conta o status de saída é usar a substituição de comando em uma simples atribuição : então o status de saída da atribuição é o status de saída da última substituição de comando na (s) atribuição (ões) .

Observe que isso funcionará conforme planejado apenas se houver uma única substituição de comando, pois somente o status da última substituição será levado em consideração. Por exemplo, o seguinte comando é bem-sucedido (ambos de acordo com o padrão e em todas as implementações que já vi):

a=$(false)$(echo foo)

Outro caso a ser observado é subcasas explícitas : (somecommand) . De acordo com a interpretação acima, o subshell pode retornar um status diferente de zero, mas como esse não é um comando simples no shell pai, o shell pai deve continuar. Na verdade, todas as conchas que conheço fazem o pai retornar nesse ponto. Embora isso seja útil em muitos casos, como (cd /some/dir && somecommand) , em que os parênteses são usados para manter uma operação como uma alteração no diretório atual, ela viola a especificação se set -e estiver desativado na subshell ou se a subshell retornar um status diferente de zero de uma maneira que não terminaria, como usar ! em um comando true. Por exemplo, todos os ash, bash, pdksh, ksh93 e zsh saem sem exibir foo nos exemplos a seguir:

set -e; (set +e; false); echo "This should be displayed"
set -e; (! true); echo "This should be displayed"

No entanto, nenhum comando simples falhou enquanto set -e estava em vigor!

Um terceiro caso problemático é um elemento em um pipeline strong>. Na prática, todos os shells ignoram falhas dos elementos do pipeline diferentes do último e exibem um dos dois comportamentos relacionados ao último elemento de pipeline:

  • ATT ksh e zsh, que executam o último elemento do pipeline no shell pai, fazem negócios como de costume: se um comando simples falhar no último elemento do pipeline, o shell executando esse comando, que por acaso é o shell pai, sai.
  • Outras camadas aproximam o comportamento ao sair se o último elemento do pipeline retornar um status diferente de zero.

Como antes, desligar set -e ou usar uma negação no último elemento do pipeline faz com que ele retorne um status diferente de zero de uma maneira que não deve terminar o shell; shells diferentes de ATT ksh e zsh irão sair.

A opção pipefail do Bash faz com que um pipeline saia imediatamente abaixo de set -e se algum de seus elementos retornar um status diferente de zero.

Note que como uma complicação adicional, o bash desativa set -e em subshells, a menos que esteja no modo POSIX ( set -o posix ou tenha POSIXLY_CORRECT no ambiente quando o bash iniciar).

Tudo isso mostra que a especificação POSIX infelizmente faz um trabalho ruim ao especificar a opção -e . Felizmente, os shells existentes são mais consistentes em seu comportamento.

    
por 22.10.2011 / 20:09
22

Como o OP apontou em sua própria resposta, atribuir a saída do subcomando a uma variável resolve o problema; o $? fica ileso.

No entanto, um caso extremo ainda pode confundi-lo com falsos negativos (ou seja, o comando falha, mas o erro não é gerado), declaração de variável local :

local myvar=$(subcommand) irá sempre devolver 0 !

bash(1) indica isso:

   local [option] [name[=value] ...]
          ... The return status is 0 unless local is used outside a function,
          an invalid name is supplied, or name is a readonly variable.

Aqui está um caso de teste simples:

#!/bin/bash

function test1() {
  data1=$(false) # undeclared variable
  echo 'data1=$(false):' "$?"
  local data2=$(false) # declaring and assigning in one go
  echo 'local data2=$(false):' "$?"
  local data3
  data3=$(false) # assigning a declared variable
  echo 'local data3; data3=$(false):' "$?"
}

test1

A saída:

data1=$(false): 1
local data2=$(false): 0
local data3; data3=$(false): 1

Espero que isso ajude outras pessoas que encontrarem a pergunta e não consigam entender por que isso não se aplica ao problema delas!

    
por 27.08.2013 / 10:04
21

(respondendo meu próprio porque eu encontrei uma solução) Uma solução é sempre atribuir isso a uma variável intermediária. Dessa forma, o código de retorno ( $? ) é definido.

Então

ABC='exit 1'
echo $?

A saída 1 (ou, em vez disso, sairá se set -e estiver presente), no entanto:

echo 'exit 1'
echo $?

Produzirá 0 após uma linha em branco. O código de retorno do eco (ou outro comando que tenha sido executado com a saída do backtick) substituirá o código de retorno 0.

Ainda estou aberto a soluções que não exigem a variável intermediária, mas isso me ajuda.

    
por 21.10.2011 / 13:16
10

Como outros já disseram, local sempre retornará 0. A solução é declarar a variável primeiro:

function testcase()
{
    local MYRESULT

    MYRESULT=$(false)
    if (( $? != 0 )); then
        echo "False returned false!"
        return 1
    fi

    return 0
}

Saída:

$ testcase
False returned false!
$ 
    
por 28.07.2014 / 04:39
3

Para sair em uma falha de substituição de comando, você pode definir explicitamente -e em um subshell, assim:

set -e
x=$(set -e; false; true)
echo "this will never be shown"
    
por 24.12.2015 / 14:33
1

Ponto interessante!

Eu nunca tropecei nisso, porque eu não sou amigo de set -e (ao invés disso eu prefiro trap ... ERR ) mas já testei isso: trap ... ERR também não pega erros dentro de $(...) (ou os backticks antiquados).

Eu acho que o problema é (como muitas vezes) que aqui um subshell é chamado e -e explicitamente significa o shell atual .

Apenas outra solução que me veio à mente neste momento seria usar a leitura:

 ls -l ghost_under_bed | read name

Isso lança ERR e, com -e , o shell será finalizado. Único problema: isso funciona apenas para comandos com uma linha de saída (ou você canaliza algo que une linhas).

    
por 21.10.2011 / 14:26