Como faço para que o bash saia com falha de backtick de maneira semelhante ao pipefail?

55

Por isso, gosto de proteger meus scripts bash sempre que possível (e quando não consigo delegar para uma linguagem como Python / Ruby) para garantir que os erros não ocorram.

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

set -e
set -u
set -o pipefail

E origine-o em outros scripts. No entanto, enquanto o pipefail pegaria:

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 sem saída padrão. Em um cano, teria falhado, mas aqui o erro não foi detectado. Quando esse é realmente um cálculo armazenado em uma variável para mais tarde, e o valor é definido como em branco, isso pode causar problemas posteriores.

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

Danny Staple
fonte

Respostas:

51

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

Quando esta opção está ativada, se um comando simples falhar por algum dos motivos listados em Consequências de erros do shell ou retornar um valor de status de saída> 0 e não for [um comando condicional ou negado], o shell deverá sair imediatamente.

Há uma ambiguidade quanto ao que acontece quando esse comando ocorre em um subshell . Do ponto de vista prático, tudo o que o subshell pode fazer é sair e retornar um status diferente de zero ao shell pai. A saída do shell pai, por sua vez, depende se esse status diferente de zero se traduz em um comando simples que falha no shell pai.

Um desses casos problemáticos é o que você encontrou: um status de retorno diferente de zero de uma 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 consideração o status de saída é usar a substituição de comando em uma atribuição simples : o status de saída da atribuição é o status de saída da última substituição de comando nas atribuições .

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

a=$(false)$(echo foo)

Outro caso para assistir é subshells 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. De fato, todas as conchas que conheço fazem o pai retornar neste momento. Embora isso seja útil em muitos casos, como (cd /some/dir && somecommand)onde os parênteses são usados ​​para manter uma operação, como um diretório atual, altere o local, ela viola a especificação se set -eestiver desativada no subshell ou se o subshell retornar um status diferente de zero de uma maneira que não o encerraria, como usar !um comando true. Por exemplo, todos os ash, bash, pdksh, ksh93 e zsh saem sem exibir fooos seguintes exemplos:

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 -eestava em vigor!

Um terceiro caso problemático são os elementos de um pipeline não trivial . Na prática, todos os shells ignoram falhas dos elementos do pipeline que não sejam o último e exibem um dos dois comportamentos em relação ao último elemento do pipeline:

  • O ATT ksh e zsh, que executam o último elemento do pipeline no shell pai, fazem negócios da maneira usual: se um comando simples falhar no último elemento do pipeline, o shell executando esse comando, que passa a ser o shell pai, saídas.
  • Outras shells aproximam o comportamento saindo se o último elemento do pipeline retornar um status diferente de zero.

Como antes, desligar set -eou 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 encerre o shell; shells diferentes de ATT ksh e zsh sairão.

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

Observe que, como uma complicação adicional, o bash é desativado set -eem subshells, a menos que esteja no modo POSIX ( set -o posixou POSIXLY_CORRECTno ambiente quando o bash é iniciado).

Tudo isso mostra que a especificação POSIX, infelizmente, faz um mau trabalho ao especificar a -eopção. Felizmente, as conchas existentes são principalmente consistentes em seu comportamento.

Gilles 'SO- parar de ser mau'
fonte
Obrigado por isso. Uma experiência que tive foi que alguns erros capturados em uma versão posterior do bash foram ignorados em versões anteriores com set -e. Minha intenção aqui é proteger os scripts na medida em que qualquer condição de retorno / falha de erro não tratado faça com que um script seja encerrado. Isso ocorre em um sistema legado que é conhecido por produzir arquivos de saída de lixo e um código de saída feliz "0" após meia hora de caos com o ambiente errado - a menos que você estivesse assistindo a saída como um falcão (e nem todos os erros estão no stderr, alguns estão no stdout e algumas partes são / dev / null'd), você simplesmente não sabe.
Danny Staple
"Por exemplo, todas as ash, bash, pdksh, ksh93 e zsh saem sem exibir foo nos seguintes exemplos": a cinza do BusyBox não se comporta dessa maneira, exibe a saída conforme a especificação. (Testado com o BusyBox 1.19.4.) Nem sai com set -e; (cd /nonexisting).
precisa saber é o seguinte
27

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

assim

ABC=`exit 1`
echo $?

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

echo `exit 1`
echo $?

Produzirá 0após uma linha em branco. O código de retorno do eco (ou outro comando 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 deixa um pouco longe.

Danny Staple
fonte
23

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

No entanto, um caso extremo ainda pode confundir você com falsos negativos (ou seja, o comando falha, mas o erro não aparece), localdeclaração de variável:

local myvar=$(subcommand)vai sempre voltar 0!

bash(1) aponta 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
mogsie
fonte
obrigado, a inconsistência entre VAR=...e local VAR=...realmente me deixou desconcertada!
21818 Jonny
13

Como outros já disseram, localsempre 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
}

Resultado:

$ testcase
False returned false!
$ 
Vai
fonte
4

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

set -e
x=$(set -e; false; true)
echo "this will never be shown"
errar
fonte
1

Ponto interessante!

Eu nunca tropecei nisso, porque não sou amigo set -e(prefiro trap ... ERR), mas já testei isso: trap ... ERRtambém não pego erros dentro $(...)(ou nos backticks antiquados).

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

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

 ls -l ghost_under_bed | read name

Isso lança ERRe com -eo shell será encerrado. Único problema: isso funciona apenas para comandos com uma linha de saída (ou você passa por algo que une linhas).

ktf
fonte
11
Não tenho certeza sobre o truque de leitura, ao tentar fazer com que a variável não seja vinculada. Eu acho que isso pode ser porque o outro lado do pipe é efetivamente um subshell, o nome não estará disponível.
Danny Staple