Por que (sair 1) não sai do script?

43

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

Um exemplo de script com o mesmo erro é:

#!/bin/bash

function bla() {
    return 1
}

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

echo '2'

Eu presumo ver a saída:

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

Mas, na verdade, vejo:

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

O encadeamento do comando () de alguma forma cria um escopo? O que é exit saindo, se não o script?

    
por Minix 10.12.2014 / 14:33

4 respostas

80

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

Do manual do bash:

(list) list is executed in a subshell environment. Variable assignments and builtin commands that affect the shell's environment do not remain in effect after the command completes. The return status is the exit status of list.

{ list; } list is simply executed in the current shell environment. list must be terminated with a newline or semicolon. This is known as a group command. The return status is the exit status of list. Note that unlike the metacharacters ( and ), { and } are reserved words and must occur where a reserved word is permitted to be recognized. Since they do not cause a word break, they must be separated from list by whitespace or another shell metacharacter.

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

echo $(exit)
cat <(exit)

Embora seja óbvio que as subshells estão envolvidas quando os comandos são colocados explicitamente dentro de () , 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 (após man bash )

    If a command is terminated by the control operator &, the shell executes the command in the background in a subshell. The shell does not wait for the command to finish, and the return status is 0.

  • o pipeline

    exit | echo foo
    

    ainda sai apenas da subcamada.

    No entanto, diferentes cartuchos se comportam de maneira diferente a esse respeito. Por exemplo, bash coloca todos os componentes do pipeline em subshells separados (a menos que você use a opção lastpipe em invocações onde o controle de job não está habilitado), mas AT & T ksh e zsh executam a última parte dentro do shell atual (ambos os comportamentos são permitidos pelo POSIX). Assim

    exit | exit | exit
    

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

  • coproc exit também executa exit em uma subshell.

por 10.12.2014 / 14:38
13

Executar o exit em uma subshell é uma armadilha:

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

O script imprime 42, sai do subshell com o código de retorno 1 e continua com o script. Mesmo a substituição da chamada por echo $(CALC) || exit 1 nã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 é frustrar o efeito de exit agrupando-o em local construído como no script a seguir. Eu 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.log para hoje. A data é inserida por um usuário que pode deixar de fornecer um valor razoável. Portanto, na minha função fname eu verifico o valor de retorno de date para 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 com ./s.sh "Thu Dec 11 20:45:49 CET 2014" , o arquivo 20141211.log será criado. Se, no entanto, o usuário digitar ./s.sh "Thu hec 11 20:45:49 CET 2014" , o script gerará:

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 1 no final da linha local … nunca é acionado porque a diretiva local sempre retorna 0 . Isso ocorre porque local é executado após $(fname) e, portanto, sobrescreve seu código de retorno. E por causa disso, o script continua e invoca touch com um parâmetro vazio. Este exemplo é simples, mas o comportamento do bash pode ser bastante confuso em um aplicativo real. Eu sei, verdadeiros programadores não usam os locais.☺

Para deixar claro: sem o local , o script é abortado como 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 de local na página man 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 nome seja uma variável somente leitura". / p>

Apesar de não ser um bug, sinto que o comportamento do bash é contra-intuitivo. Estou ciente da sequência de execução, local não deve mascarar uma atribuição quebrada, no entanto.

Minha resposta inicial continha algumas imprecisões. Depois de uma discussão reveladora e aprofundada com o mikeserv (obrigado por isso) eu fui para corrigi-los.

    
por 10.12.2014 / 22:05
1

A solução atual:

#!/bin/bash

function bla() {
    return 1
}

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

echo '2'

O agrupamento de erros só será executado se bla retornar um status de erro e exit não estiver em uma sub-shell, portanto, todo o script será interrompido.

    
por 19.07.2017 / 02:06
0

Os parênteses iniciam um subshell e a saída só sai dessa sub-linha.

Você pode ler o código de saída com $? e adicionar isso ao seu script para sair do script se a subcamada foi encerrada:

#!/bin/bash

function bla() {
    return 1
}

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

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

echo '2'
    
por 11.08.2016 / 21:59