Como é determinado o status de retorno de uma atribuição de variável?

10

Eu vi construções em scripts como este:

if somevar="$(somecommand 2>/dev/null)"; then
...
fi

Isso está documentado em algum lugar? Como é determinado o status de retorno de uma variável e como isso se relaciona à substituição de comandos? (Por exemplo, obteria o mesmo resultado com if echo "$(somecommand 2>/dev/null)"; then?)

Curinga
fonte

Respostas:

12

Está documentado (para POSIX) na Seção 2.9.1 Comandos simples das especificações básicas do Open Group. Há uma parede de texto lá; Dirijo sua atenção ao último parágrafo:

Se houver um nome de comando, a execução continuará como descrito em Pesquisa e Execução de Comando . Se não houver um nome de comando, mas o comando contiver uma substituição de comando, o comando será concluído com o status de saída da última substituição de comando executada. Caso contrário, o comando será concluído com um status de saída zero.

Então, por exemplo,

   Command                                         Exit Status
$ FOO=BAR                                   0 (but see also the note from icarus, below)
$ FOO=$(bar)                                Exit status from "bar"
$ FOO=$(bar) baz                            Exit status from "baz"
$ foo $(bar)                                Exit status from "foo"

É assim que o bash também funciona. Mas veja também a seção “não tão simples” no final.

phk , em sua pergunta Atribuições são como comandos com status de saída, exceto quando há substituição de comando? , sugere

… Parece que uma atribuição em si conta como um comando… com um valor de saída zero, mas que se aplica antes do lado direito da atribuição (por exemplo, uma chamada de substituição de comando…)

Essa não é uma maneira terrível de encarar. Um esquema em bruto para a determinação do estado de retorno de um comando simples (uma que não contém ;, &, |, &&ou ||) é:

  • Digitalize a linha da esquerda para a direita até chegar ao final ou a uma palavra de comando (geralmente o nome de um programa).
  • Se você vir uma atribuição de variável, o status de retorno da linha poderá ser 0.
  • Se você vir uma substituição de comando - ou seja, $(…)- obtenha o status de saída desse comando.
  • Se você alcançar um comando real (não em uma substituição de comando), obtenha o status de saída desse comando.
  • O status de retorno da linha é o último número que você encontrou.
    Substituições de comando como argumentos para o comando, por exemplo, foo $(bar)não contam; você obtém o status de saída foo. Parafraseando a notação do phk , o comportamento aqui é

    temporary_variable  = EXECUTE( "bar" )
    overall_exit_status = EXECUTE( "foo", temporary_variable )

Mas isso é uma pequena simplificação excessiva. O status geral de retorno de

A = $ ( cmd 1 ) B = $ ( cmd 2 ) C = $ ( cmd 3 ) D = $ ( cmd 4 ) E = mc 2
é o status de saída de . A atribuição que ocorre após a atribuição não define o status geral da saída como 0.cmd4E=D=

icarus , em sua resposta à pergunta do phk , levanta um ponto importante: as variáveis ​​podem ser definidas como somente leitura. O terceiro ao último parágrafo da Seção 2.9.1 da norma POSIX diz:

Se alguma das atribuições de variáveis ​​tentar atribuir um valor a uma variável para a qual o atributo readonly está definido no ambiente shell atual (independentemente de a atribuição ser feita nesse ambiente), ocorrerá um erro de atribuição de variável. Consulte Consequências dos erros do shell para obter as consequências desses erros.

então se você diz

readonly A
C=Garfield A=Felix T=Tigger

o status de retorno é 1. Não importa se as cordas Garfield, Felixe / ou Tigger são substituídos com substituição de comando (s) - mas veja notas abaixo.

A Seção 2.8.1 Consequências dos erros do shell possui outro monte de texto e uma tabela e termina com

Em todos os casos mostrados na tabela em que é necessário que um shell interativo não saia, o shell não deve executar nenhum processamento adicional do comando no qual o erro ocorreu.

Alguns dos detalhes fazem sentido; alguns não:

  • A A=atribuição às vezes aborta a linha de comando, como a última frase parece especificar. No exemplo acima, Cestá definido como Garfield, mas Tnão está definido (e, é claro, também não  A).
  • Da mesma forma, executa , mas não . Mas, nas minhas versões do bash (que incluem 4.1.X e 4.3.X), ele é executado . (Aliás, isso impacta ainda mais a interpretação do phk de que o valor de saída da atribuição se aplica antes do lado direito da atribuição.)C=$(cmd1) A=$(cmd2) T=$(cmd3)cmd1cmd3
    cmd2

Mas aqui está uma surpresa:

Nas minhas versões do bash,

somente leitura A
C = algo A = algo T = algo  cmd 0

não executar. Em particular,cmd0

C = $ ( cmd 1 ) A = $ ( cmd 2 ) T = $ ( cmd 3 )    cmd 0
executa e , mas não . (Observe que esse é o oposto de seu comportamento quando não há comando.) E ele se estabelece (e também ) no ambiente de . Gostaria de saber se isso é um bug no bash.cmd1cmd3cmd2TCcmd0


Não tão simples:

O primeiro parágrafo desta resposta se refere a "comandos simples".  A especificação diz,

Um "comando simples" é uma sequência de atribuições e redirecionamentos de variáveis ​​opcionais, em qualquer sequência, opcionalmente seguidos por palavras e redirecionamentos, finalizados por um operador de controle.

Estas são instruções como as do meu primeiro bloco de exemplo:

$ FOO=BAR
$ FOO=$(bar)
$ FOO=$(bar) baz
$ foo $(bar)

os três primeiros incluem atribuições de variáveis ​​e os três últimos incluem substituições de comandos.

Mas algumas atribuições de variáveis ​​não são tão simples.  o bash (1) diz:

Instruções de atribuição também pode aparecer como argumentos para o alias, declare, typeset, export, readonly, e localbuiltin comandos ( declaração de comandos).

Para export, a especificação POSIX diz,

ESTADO DE SAÍDA

    0 0
      Todos os operandos de nomes foram exportados com sucesso.
    > 0
      Pelo menos um nome não pôde ser exportado ou a -popção foi especificada e ocorreu um erro.

E o POSIX não suporta local, mas o bash (1) diz:

É um erro usar localquando não estiver em uma função. O status de retorno é 0, a menos que localseja usado fora de uma função, um nome inválido seja fornecido ou nome seja uma variável somente leitura.

Lendo nas entrelinhas, podemos ver que comandos de declaração como

export FOO=$(bar)

e

local FOO=$(bar)

são mais como

foo $(bar)

na medida em que ignorar o status de saída de bar e dar-lhe um status de saída baseado no comando principal ( export, localou foo). Então, nós temos esquisitices como

   Command                                           Exit Status
$ FOO=$(bar)                                    Exit status from "bar"
                                                  (unless FOO is readonly)
$ export FOO=$(bar)                             0 (unless FOO is readonly,
                                                  or other error from “export”)
$ local FOO=$(bar)                              0 (unless FOO is readonly,
                                                  statement is not in a function,
                                                  or other error from “local”)

que podemos demonstrar com

$ export FRIDAY=$(date -d tomorrow)
$ echo "FRIDAY   = $FRIDAY, status = $?"
FRIDAY   = Fri, May 04, 2018  8:58:30 PM, status = 0
$ export SATURDAY=$(date -d "day after tomorrow")
date: invalid date ‘day after tomorrow’
$ echo "SATURDAY = $SATURDAY, status = $?"
SATURDAY = , status = 0

e

myfunc() {
    local x=$(echo "Foo"; true);  echo "x = $x -> $?"
    local y=$(echo "Bar"; false); echo "y = $y -> $?"
    echo -n "BUT! "
    local z; z=$(echo "Baz"; false); echo "z = $z -> $?"
}

$ myfunc
x = Foo -> 0
y = Bar -> 0
BUT! z = Baz -> 1

Felizmente, o ShellCheck pega o erro e gera SC2155 , que aconselha que

export foo="$(mycmd)"

deve ser alterado para

foo=$(mycmd)
export foo

e

local foo="$(mycmd)"

deve ser alterado para

local foo
foo=$(mycmd)
G-Man diz que 'restabelece Monica'
fonte
1
Ótimo, obrigado! Para pontos de bônus, você sabe como localisso se relaciona? Por exemplo local foo=$(bar)?
Curinga
1
Para o segundo exemplo (apenas FOO=$(bar)), vale a pena notar que, teoricamente, tanto o status de saída quanto a atribuição podem desempenhar um papel, consulte unix.stackexchange.com/a/341013/117599
phk
1
@Wildcard: Estou feliz que você gostou. Acabei de atualizar novamente; grande parte da versão que você acabou de ler estava errada. Enquanto você estiver aqui, o que você acha? Isso é um bug no bash, que A=foo cmdé executado cmdmesmo que Aseja somente leitura?
G-Man Diz 'Reinstate Monica'
1
@phk: (1) Teoria interessante, mas não sei ao certo como isso faz sentido. Dê uma outra olhada no exemplo antes da minha "surpresa". Se Afor somente leitura, o comando será C=value₁ A=value₂ T=value₃definido, Cmas não T(e, Aé claro, não será definido) - o shell finalizou o processamento da linha de comando, ignorando T=value₃, porque A=value₂é um erro. (2) Obrigado pelo link para a questão Stack Overflow - eu postei alguns comentários sobre ele.
G-Man diz 'Reinstate Monica'
1
@Wildcard "Para pontos de bônus, você sabe como os laços locais estão envolvidos nisso?". Sim ... local=$(false)tem valor de saída 0porque (a partir da página man): It is an error to use local when not within a function. The return status is 0 unless local is used outside a function, an invalid name is supplied, or name is a readonly variable.. Isso não é suficiente para o gênio que o projetou dessa maneira.
David Tonhofer
2

Está documentado em Bash ( LESS=+/'^SIMPLE COMMAND EXPANSION' bash):

Se houver um nome de comando deixado após a expansão .... Caso contrário, o comando será encerrado. ... Se não houve substituições de comando, o comando sai com um status zero.

Em outras palavras (minhas palavras):

Se não houver nenhum nome de comando após a expansão e nenhuma substituição de comando foi executada, a linha de comandos será encerrada com um status zero.

phk
fonte