Isso é um bug no bash? `return` não sai da função se chamado de um pipe

16

Ultimamente tenho tido problemas estranhos com o bash. Ao tentar simplificar meu script, vim com esse pequeno pedaço de código:

$ o(){ echo | while read -r; do return 0; done; echo $?;}; o
0
$ o(){ echo | while read -r; do return 1; done; echo $?;}; o
1

returndeveria ter saído da função sem imprimir $?, não deveria? Bem, então verifiquei se posso retornar de um cano sozinho:

$ echo | while read -r; do return 1; done
bash: return: can only `return' from a function or sourced script

O mesmo acontece sem um whileloop:

$ foo(){ : | return 1; echo "This should not be printed.";}
$ foo
This should not be printed.

Há algo que estou perdendo aqui? Uma pesquisa no Google não trouxe nada sobre isso! Minha versão do bash é a versão 4.2.37 (1) no Debian Wheezy.

Teresa e Junior
fonte
Alguma coisa errada com as configurações sugeridas na minha resposta que permitem que seu script se comporte da maneira intuitiva que você esperava?
Jlliagre
@ jlliagre É um script bastante complexo nas milhares de linhas. Com a preocupação de quebrar outra coisa, prefiro evitar executar um pipe dentro da função, então substituí-o por uma substituição de processo. Obrigado!
Teresa e Junior
Por que não remover os dois primeiros exemplos, se whilenão for necessário para a reprodução? Isso distrai do ponto.
Lightness Races com Monica
@LightnessRacesinOrbit Um whileloop é um uso muito comum para um pipe return. O segundo exemplo é mais direto ao ponto, mas é algo que eu não acredito que alguém jamais usaria ...
Teresa e Junior
11
Infelizmente, minha resposta correta foi excluída ... Você está em uma zona cinza enquanto faz algo não especificado. O comportamento depende de como o shell interpreta os pipes e isso é ainda diferente entre o Bourne Shell e o Korn Shell, mesmo que o ksh tenha sido derivado de fontes sh. No Bourne Shell, o loop while está em um subshell, portanto, você vê o eco como no bash. No ksh, o loop while é o processo em primeiro plano e, portanto, o ksh não chama eco no seu exemplo.
schily

Respostas:

10

Relacionado: /programming//a/7804208/4937930

Não é um bug que você não possa sair de um script ou retornar de uma função por exitou returnem subshells. Eles são executados em outro processo e não afetam o processo principal.

Além disso, suponho que você esteja vendo comportamentos não documentados de bash em (provavelmente) especificações indefinidas. Em uma função, nenhum erro é declarado returnno nível superior dos comandos de subshell e apenas se comporta como exit.

IMHO é um bug de bash para o comportamento inconsistente de returndepender se a declaração principal está em uma função ou não.

#!/bin/bash

o() {
    # Runtime error, but no errors are asserted,
    # each $? is set to the return code.
    echo | return 10
    echo $?
    (return 11)
    echo $?

    # Valid, each $? is set to the exit code.
    echo | exit 12
    echo $?
    (exit 13)
    echo $?
}
o

# Runtime errors are asserted, each $? is set to 1.
echo | return 20
echo $?
(return 21)
echo $?

# Valid, each $? is set to the exit code.
echo | exit 22
echo $?
(exit 23)
echo $?

Resultado:

$ bash script.sh 
10
11
12
13
script.sh: line 20: return: can only `return' from a function or sourced script
1
script.sh: line 22: return: can only `return' from a function or sourced script
1
22
23
yaegashi
fonte
A falta de verbosidade do erro pode ser indocumentada. Mas o fato de que returnnão funciona a partir de uma sequência de comandos de nível superior em um subshell, e em particular não sai do subshell, é o que os documentos existentes já me fizeram esperar. O OP pode usar exit 1 || return 1onde eles estão tentando usar returne, em seguida, deve obter o comportamento esperado. EDIT: A resposta de @ herbert indica que o nível superior returnno subshell está funcionando como exit(mas somente no subshell).
dubiousjim
11
@dubiousjim Atualizei meu script. Quero dizer que returnem uma sequência simples de subshell deve ser declarada como um erro de tempo de execução em qualquer caso , mas na verdade não é quando ocorre em uma função. Esse problema também foi discutido no gnu.bash.bug , mas não há conclusão.
Yaegashi
11
Sua resposta não está correta, pois não é especificado se o loop while está em uma subshell ou é o processo em primeiro plano. Independentemente da implementação do shell real, a returninstrução está em uma função e, portanto, é legal. O comportamento resultante, no entanto, não é especificado.
schily
Você não deve escrever que é um comportamento não documentado, enquanto os componentes de pipe estão em uma subcama estão documentados na página de manual do bash. Você não deve escrever que o comportamento provavelmente se baseia em especificações indefinidas, enquanto o POSIX especifica os comportamentos permitidos. Você não deve suspeitar de um erro do bash enquanto o bash segue o padrão POSIX, permitindo o retorno em uma função, mas não fora.
Jlliagre 23/09/2015
17

Não é um bug, bashmas seu comportamento documentado :

Cada comando em um pipeline é executado em seu próprio subshell

A returninstrução é válida estando dentro de uma definição de função, mas estando em um subshell, ela não afeta seu shell pai, portanto a próxima instrução echoé executada independentemente. No entanto, é uma construção de shell não portátil, pois o padrão POSIX permite que os comandos que compõem um pipeline sejam executados em uma subshell (o padrão) ou na parte superior (uma extensão permitida).

Além disso, cada comando de um pipeline de comandos múltiplos está em um ambiente de subcasca; como uma extensão, no entanto, qualquer um ou todos os comandos em um pipeline podem ser executados no ambiente atual. Todos os outros comandos devem ser executados no ambiente atual do shell.

Felizmente, você pode dizer bashpara se comportar da maneira que espera com algumas opções:

$ set +m # disable job control
$ shopt -s lastpipe # do not run the last command of a pipeline a subshell 
$ o(){ echo | while read -r; do return 0; done; echo $?;}
$ o
$          <- nothing is printed here
jlliagre
fonte
11
Como returnnão encerra a função, não faria mais sentido se o shell fosse impresso bash: return: can only `return' from a function or sourced script, em vez de dar ao usuário uma falsa sensação de que a função pode ter retornado?
Teresa e Junior
2
Não vejo em nenhum lugar na documentação que o retorno dentro do subshell seja válido. Aposto que esse recurso foi copiado do ksh, a instrução de retorno fora da função ou o script de origem se comportam como exit . Não tenho certeza sobre o shell Bourne original.
precisa saber é
11
@jlliagre: Talvez Teresa esteja confusa com a terminologia do que está pedindo, mas não vejo por que seria "complicado" para o bash emitir um diagnóstico se você executar um returnde um subshell. Afinal, ele sabe que está em um subshell, como evidenciado pela $BASH_SUBSHELLvariável. O maior problema é que isso pode levar a falsos positivos; um usuário que entenda como os subshells funcionam pode ter scripts escritos que são usados returnpara substituir exitum subshell. (E, claro, há casos válidos onde se pode querer variáveis fixos ou fazer um cdem um subnível.)
Scott
11
@ Scott Acho que entendo bem a situação. Um canal cria um subshell e returnestá retornando do subshell em vez de falhar, pois está dentro de uma função real. O problema é que help returnafirma especificamente: Causes a function or sourced script to exit with the return value specified by N.Ao ler a documentação, qualquer usuário esperaria que ao menos falhasse ou imprimisse um aviso, mas nunca se comportasse dessa maneira exit.
Teresa e Junior
11
Parece-me que quem espera que um return em um subshell em uma função retorne da função (no processo principal do shell) não entende muito bem os subshells. Por outro lado, eu esperaria que um leitor que entenda subconjuntos espere return em um subconjunto em uma função finalizá-lo, exatamente como exitfaria.
Scott
6

De acordo com a documentação do POSIX, o uso de returnfora da função ou do script de origem não é especificado . Portanto, depende do seu shell para lidar.

O shell SystemV relatará erro, enquanto estiver dentro ksh, returnfora da função ou do script de origem se comportar como exit. A maioria das outras conchas POSIX e osh de schily também se comportam assim:

$ for s in /bin/*sh /opt/schily/bin/osh; do
  printf '<%s>\n' $s
  $s -c '
    o(){ echo | while read l; do return 0; done; echo $?;}; o
  '
done
</bin/bash>
0
</bin/dash>
0
</bin/ksh>
</bin/lksh>
0
</bin/mksh>
0
</bin/pdksh>
0
</bin/posh>
0
</bin/sh>
0
</bin/yash>
0
</bin/zsh>
</opt/schily/bin/osh>
0

kshe zshnão produziu porque a última parte do pipe nessas conchas foi executada no shell atual em vez do subshell. A instrução de retorno afetou o ambiente de shell atual que chamou a função, faz com que a função retorne imediatamente sem imprimir nada.

Na sessão interativa, bashapenas reporte o erro, mas não encerrou o shell, schily's oshrelatou o erro e encerrou o shell:

$ for s in /bin/*sh; do printf '<%s>\n' $s; $s -ci 'return 1; echo 1'; done
</bin/bash>
bash: return: can only `return' from a function or sourced script
1
</bin/dash>
</bin/ksh>
</bin/lksh>
</bin/mksh>
</bin/pdksh>
</bin/posh>
</bin/sh>
</bin/yash>
</bin/zsh>
</opt/schily/bin/osh>
$ cannot return when not in function

( zshNa sessão interativa e saída é fazer terminal não encerrado, bash, yashe schily's oshrelatou o erro, mas não encerrar o shell)

cuonglm
fonte
11
Pode-se argumentar que returné usado dentro de uma função aqui.
Jlliagre
11
@ jlliagre: Não sei ao certo o que você quer dizer, returnfoi usado dentro da função interna do subshell , exceto e . kshzsh
cuonglm
2
Quero dizer, estar dentro de um subshell que está dentro de uma função não significa necessariamente estar fora dessa função, ou seja, nada nos componentes do pipeline de estados padrão deve ser considerado fora da função em que eles estão localizados. Isso mereceria ser esclarecido pelo Open Group.
Jlliagre 19/09/2015
3
Eu acho que não. Isso está fora da função. O shell que chamou a função e o subshell que executou return são diferentes.
precisa saber é
Eu entendo o seu raciocínio que explica corretamente a questão, meu argumento é de acordo com a gramática de shell descrita no padrão POSIX, o pipeline faz parte da lista composta, que faz parte do comando composto, que é o corpo da função. Em nenhum lugar é indicado que os componentes do pipeline devem ser considerados fora da função. Assim como se eu estou em um carro e esse carro está estacionado em uma garagem, posso supor que eu sou naquela garagem também ;-)
jlliagre
4

Eu acho que você obteve o comportamento esperado, no bash, cada comando em um pipeline é executado em um subshell. Você pode se convencer tentando modificar uma variável global de sua função:

foo(){ x=42; : | x=3; echo "x==$x";}

A propósito, o retorno está funcionando, mas retorna do subshell. Mais uma vez, você pode verificar se:

foo(){ : | return 1; echo$?; echo "This should not be printed.";}

Produzirá o seguinte:

1
This should not be printed.

Então a instrução return saiu corretamente do subshell

.

Herbert
fonte
2
Portanto, para sair da função, use foo(){ : | return 1 || return 2; echo$?; echo "This should not be printed.";}; foo; echo $?e você obterá um resultado 2. Mas para a clareza que eu faria o return 1seja exit 1.
precisa
A propósito, há alguma justificativa para o fato de que todos os membros de um pipeline (não todos, exceto um) são executados em subcascas?
Incnis MRSI
@IncnisMrsi: Veja a resposta de jlliagre .
Scott
1

A resposta mais geral é que o bash e algumas outras conchas normalmente colocam todos os elementos de um pipeline em processos separados. Isso é razoável quando a linha de comando é

programa 1 | programa 2 | programa 3

já que os programas normalmente são executados em processos separados de qualquer maneira (a menos que você diga ). Mas pode ser uma surpresa paraexec program

comando 1 | comando 2 | comando 3

onde alguns ou todos os comandos são comandos internos. Exemplos triviais incluem:

$ a=0
$ echo | a=1
$ echo "$a"
0
$ cd /
$ echo | cd /tmp
$ pwd
/

Um exemplo um pouco mais realista é

$ t=0
$ ps | while read pid rest_of_line
> do
>     : $((t+=pid))
> done
$ echo "$t"
0

onde o todo while... do... donelaço é colocado em um subprocesso, e assim suas alterações tnão são visíveis para o shell principal após as extremidades do laço. E é exatamente isso que você está fazendo - canalizando em um whileloop, fazendo com que o loop seja executado como um subshell e, em seguida, tentando retornar do subshell.

Scott
fonte