Por que (saída 1) não sai do script?

48

Eu tenho um script que não sai quando eu quero.

Um script de exemplo com o mesmo erro é:

#!/bin/bash

function bla() {
    return 1
}

bla || ( echo '1' ; exit 1 )

echo '2'

Eu assumiria ver a saída:

:~$ ./test.sh
1
:~$

Mas eu realmente vejo:

:~$ ./test.sh
1
2
:~$

O ()encadeamento de comando de alguma forma cria um escopo? O que está exitsaindo, se não o script?

Minix
fonte
5
Isso exige uma resposta de uma única palavra: subshell
Joshua
Você pode usar o seguinte comando
Wildcard

Respostas:

87

()executa comandos no subshell; portanto, exitvocê está saindo do subshell e retornando ao shell pai. Use chaves {}se desejar executar comandos no shell atual.

No manual do bash:

(lista) é executada em um ambiente de subcasca. As atribuições variáveis ​​e os comandos internos que afetam o ambiente do shell não permanecem em vigor após a conclusão do comando. O status de retorno é o status de saída da lista.

{ Lista; } lista é simplesmente executada no ambiente atual do shell. A lista deve ser finalizada com uma nova linha ou ponto e vírgula. Isso é conhecido como comando de grupo. O status de retorno é o status de saída da lista. Observe que, diferentemente dos metacaracteres (e), {e} são palavras reservadas e devem ocorrer onde uma palavra reservada pode ser reconhecida. Como eles não causam uma quebra de palavra, eles devem ser separados da lista por espaços em branco ou outro metacaractere do shell.

Vale ressaltar que a sintaxe do shell é bastante consistente e o subshell também participa de outras ()construções, como substituição de comando (também com a `..`sintaxe de estilo antigo ) ou substituição de processo, para que o seguinte também não saia do shell atual:

echo $(exit)
cat <(exit)

Embora possa ser óbvio que os subshells estão envolvidos quando os comandos são explicitamente colocados no interior (), o fato menos visível é que eles também são gerados nessas outras estruturas:

  • comando iniciado em segundo plano

    exit &

    não sai do shell atual porque (depois man bash)

    Se um comando é finalizado pelo operador de controle &, o shell executa o comando em segundo plano em uma subshell. O shell não espera o comando terminar e o status de retorno é 0.

  • o gasoduto

    exit | echo foo

    ainda sai apenas do subshell.

    No entanto, conchas diferentes se comportam de maneira diferente a esse respeito. Por exemplo, bashcoloca todos os componentes do pipeline em subshells separados (a menos que você use a lastpipeopção em invocações onde o controle de tarefas não está ativado), mas a AT&T kshe zshexecuta a última parte dentro do shell atual (ambos os comportamentos são permitidos pelo POSIX). portanto

    exit | exit | exit

    basicamente não faz nada no bash, mas sai do zsh por causa do último exit .

  • coproc exittambém é executado exitem um subshell.

jimmij
fonte
5
Ah Agora, para encontrar todos os lugares, onde meu antecessor usava o aparelho errado. Obrigado pela compreensão.
Minix
10
Observe cuidadosamente o espaçamento na página de manual: {e }não são sintaxes, são palavras reservadas e devem estar entre espaços, e a lista deve terminar com um terminador de comando (ponto-e-vírgula, nova linha, e comercial)
glenn jackman
Por interesse, isso tende a ser outro processo ou apenas um ambiente separado em uma pilha interna? Eu uso muito () para isolar chdirs e deve ter tido sorte com meu uso de $$ etc, se for o primeiro.
Dan Sheppard
5
@DanSheppard É outro processo, mas (echo $$)imprime o ID do shell pai porque $$é expandido antes mesmo da criação do subshell. Na verdade, a impressão do ID do processo do subshell pode ser complicada, consulte stackoverflow.com/questions/9119885/…
jimmij
@jimmij, como pode ser $$expandido antes da criação do subshell, e ainda $BASHPIDmostra o valor correto para um subshell?
Curinga
13

A execução de exitum subshell é uma armadilha:

#!/bin/bash
function calc { echo 42; exit 1; }
echo $(calc)

O script imprime 42, sai do subshell com código de retorno 1e continua com o script. Mesmo substituindo a chamada por echo $(CALC) || exit 1não ajuda, porque o código de retorno de echoé 0, independentemente do código de retorno de calc. E calcé executado antes de echo.

Ainda mais intrigante é impedir o efeito exit, envolvendo-o no interior, localcomo no script a seguir. Tropecei no problema quando escrevi uma função para verificar um valor de entrada. Exemplo:

Eu quero criar um arquivo chamado "year month day.log", ou seja, 20141211.loghoje. A data é inserida por um usuário que pode não fornecer um valor razoável. Portanto, na minha função fname, verifico o valor de retorno de datepara verificar a validade da entrada do usuário:

#!/bin/bash

doit ()
    {
    local FNAME=$(fname "$1") || exit 1
    touch "${FNAME}"
    }

fname ()
    {
    date +"%Y%m%d.log" -d"$1" 2>/dev/null
    if [ "$?" != 0 ] ; then
        echo "fname reports \"Illegal Date\"" >&2
        exit 1
    fi
    }

doit "$1"

Parece bom. Deixe o script ser nomeado s.sh. Se o usuário chamar o script ./s.sh "Thu Dec 11 20:45:49 CET 2014", o arquivo 20141211.logserá criado. Se, no entanto, o usuário digitar ./s.sh "Thu hec 11 20:45:49 CET 2014", o script exibirá:

fname reports "Illegal Date"
touch: cannot touch ‘’: No such file or directory

A linha fname…diz que os dados de entrada incorretos foram detectados no subshell. Mas o exit 1final da local …linha nunca é acionado porque a localdiretiva sempre retorna 0. Isso ocorre porque localé executado depois $(fname) e, portanto, substitui seu código de retorno. E por isso, o script continua e invoca touchcom um parâmetro vazio. Este exemplo é simples, mas o comportamento do bash pode ser bastante confuso em um aplicativo real. Eu sei, programadores reais não usam locais.☺

Para deixar claro: sem o local, o script é interrompido conforme o esperado quando uma data inválida é inserida.

A correção é dividir a linha como

local FNAME
FNAME=$(fname "$1") || exit 1

O comportamento estranho está de acordo com a documentação da localpágina de manual do bash: "O status de retorno é 0, a menos que local seja usado fora de uma função, um nome inválido seja fornecido ou o nome seja uma variável somente leitura".

Embora não seja um bug, sinto que o comportamento do bash é contra-intuitivo. Estou ciente da sequência de execução local, no entanto, não devo mascarar uma tarefa quebrada.

Minha resposta inicial continha algumas imprecisões. Após uma discussão reveladora e profunda com mikeserv (obrigado por isso), fui corrigi-los.

hermannk
fonte
@ MikeServ: Adicionei um exemplo para mostrar a relevância.
hermannk
@ MikeServ: Sim, você está certo. Terser ainda. Mas a armadilha ainda está lá.
hermannk
@ MikeServ: Desculpe, meu exemplo foi quebrado. Eu tinha esquecido o teste doit().
hermannk
2

A solução real:

#!/bin/bash

function bla() {
    return 1
}

bla || { echo '1'; exit 1; }

echo '2'

O agrupamento de erros será executado apenas se blaretornar um status de erro e exitnão estiver em uma subshell; portanto, o script inteiro será interrompido.

Walf
fonte
1

Os colchetes iniciam um subshell e a saída sai apenas desse subshell.

Você pode ler o código de saída $?e adicioná-lo ao seu script para sair do script se o subshell foi encerrado:

#!/bin/bash

function bla() {
    return 1
}

bla || ( echo '1' ; exit 1 )

exitcode=$?
if [ $exitcode != 0 ]; then exit $exitcode; fi

echo '2'
rubo77
fonte