Est-ce un bug dans bash? `return` ne quitte pas la fonction s'il est appelé depuis un tube

16

J'ai eu des problèmes étranges avec bash ces derniers temps. En essayant de simplifier mon script, j'ai trouvé ce petit morceau de code:

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

return aurait dû quitter la fonction sans imprimer $? , n'est-ce pas? Eh bien, j'ai vérifié si je pouvais revenir d'un tuyau seul:

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

La même chose se produit sans whileboucle:

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

Y a-t-il quelque chose qui me manque ici? Une recherche Google n'a rien apporté à ce sujet! Ma version bash est la version 4.2.37 (1) sur Debian Wheezy.

Teresa e Junior
la source
Quelque chose ne va pas avec les paramètres que j'ai suggérés dans ma réponse qui permettent à votre script de se comporter de la manière intuitive que vous attendiez?
jlliagre
@jlliagre C'est un script assez complexe sur des milliers de lignes. Dans le souci de casser quelque chose d'autre, je préfère simplement éviter d'exécuter un tube dans la fonction, je l'ai donc remplacé par une substitution de processus. Merci!
Teresa e Junior
Pourquoi ne pas supprimer les deux premiers exemples, si le whilen'est pas nécessaire pour la reproduction? Cela détourne l'attention du sujet.
Courses de légèreté avec Monica
@LightnessRacesinOrbit Une whileboucle est une utilisation très courante pour un tuyau avec return. Le deuxième exemple est plus direct, mais c'est quelque chose que je ne pense pas que quiconque utiliserait jamais ...
Teresa e Junior
1
Malheureusement, ma bonne réponse a été supprimée ... Vous êtes dans une zone grise car vous faites quelque chose qui n'est pas spécifié. Le comportement dépend de la façon dont le shell interprète les canaux et cela est même différent entre le Bourne Shell et le Korn Shell même si ksh a été dérivé de sources sh. Dans le Bourne Shell, la boucle while est dans un sous-shell donc vous voyez l'écho comme avec bash, Dans ksh la boucle while est le processus de premier plan et donc ksh n'appelle pas echo avec votre exemple.
schily

Réponses:

10

En relation: /programming//a/7804208/4937930

Ce n'est pas un bug que vous ne pouvez pas quitter un script ou retourner d'une fonction par exitou returnen sous-shell. Ils sont exécutés dans un autre processus et n'affectent pas le processus principal.

En plus de cela, je suppose que vous voyez des comportements non documentés de bash sur (probablement) des spécifications non définies. Dans une fonction, aucune erreur n'est affirmée returnau niveau supérieur des commandes de sous-shell et elle se comporte simplement comme exit.

À mon humble avis, c'est un bogue bash pour le comportement incohérent de returnselon que l'instruction principale est dans une fonction ou non.

#!/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 $?

Production:

$ 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
la source
L'absence de verbosité d'erreur peut être non documentée. Mais le fait que returncela ne fonctionne pas à partir d'une séquence de commandes de niveau supérieur dans un sous-shell, et en particulier ne quitte pas le sous-shell, est ce que les documents existants m'ont déjà fait attendre. L'OP pourrait utiliser exit 1 || return 1où ils essaient d'utiliser return, et devrait ensuite obtenir le comportement attendu. EDIT: La réponse de @ herbert indique que le niveau supérieur returndans le sous-shell fonctionne comme exit(mais uniquement à partir du sous-shell).
dubiousjim
1
@dubiousjim Mise à jour de mon script. Je veux dire returndans une simple séquence de sous-shell devrait être affirmé comme une erreur d'exécution dans tous les cas , mais en réalité ce n'est pas quand cela se produit dans une fonction. Ce problème a également été discuté dans gnu.bash.bug , mais il n'y a pas de conclusion.
yaegashi
1
Votre réponse n'est pas correcte car il n'est pas spécifié si la boucle while est dans un sous-shell ou s'il s'agit du processus de premier plan. Quelle que soit la mise en œuvre du shell réel, la returndéclaration a une fonction et est donc légale. Le comportement résultant n'est cependant pas spécifié.
schily
Vous ne devriez pas écrire qu'il s'agit d'un comportement non documenté alors que les composants de tuyau sont dans un sous-shell sont documentés dans la page de manuel bash. Vous ne devriez pas écrire le comportement est probablement basé sur des spécifications non définies tandis que POSIX spécifie les comportements autorisés. Vous ne devriez pas soupçonner un bogue bash pendant que bash suit le standard POSIX en autorisant le retour dans une fonction mais pas à l'extérieur.
jlliagre
17

Ce n'est pas un bug bashmais son comportement documenté :

Chaque commande d'un pipeline est exécutée dans son propre sous-shell

L' returninstruction est valide étant à l'intérieur d'une définition de fonction mais étant également dans un sous-shell, elle n'affecte pas son shell parent donc l'instruction suivante,, echoest exécutée malgré tout. Il s'agit néanmoins d'une construction shell non portable car le standard POSIX permet aux commandes composant un pipeline d'être exécutées soit dans un sous-shell (par défaut) soit dans le top (une extension autorisée).

De plus, chaque commande d'un pipeline multi-commandes se trouve dans un environnement de sous-shell; cependant, en tant qu'extension, une ou toutes les commandes d'un pipeline peuvent être exécutées dans l'environnement actuel. Toutes les autres commandes doivent être exécutées dans l'environnement shell actuel.

Avec un peu de chance, vous pouvez dire bashde vous comporter comme vous le souhaitez avec quelques options:

$ 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
la source
1
Puisque returnne quittera pas la fonction, cela n'aurait-il pas plus de sens si le shell venait d'être imprimé bash: return: can only `return' from a function or sourced script, au lieu de donner à l'utilisateur un faux sens que la fonction aurait pu être retournée?
Teresa e Junior
2
Je ne vois nulle part dans la documentation que le retour à l' intérieur du sous-shell est valide. Je parie que cette fonctionnalité a été copiée à partir de ksh, la déclaration de retour en dehors de la fonction ou du script source se comporte comme exit . Je ne suis pas sûr du shell Bourne d'origine.
cuonglm
1
@jlliagre: Peut-être que Teresa est confuse quant à la terminologie de ce qu'elle demande, mais je ne vois pas pourquoi il serait "délicat" pour bash d'émettre un diagnostic si vous exécutez un returndepuis un sous-shell. Après tout, il sait que c'est dans un sous-shell, comme en témoigne la $BASH_SUBSHELLvariable. Le plus gros problème est que cela pourrait conduire à des faux positifs; un utilisateur qui comprend le fonctionnement des sous-coquilles pourrait avoir écrit des scripts qui utilisent returnà la place de exitmettre fin à une sous-coquille. (Et, bien sûr, il existe des cas valides où l'on peut vouloir définir des variables ou faire un cden sous-shell.)
Scott
1
@Scott Je pense que je comprends bien la situation. Un tube crée un sous-shell et returnrevient du sous-shell au lieu d'échouer, car il se trouve dans une fonction réelle. Le problème est le suivant help return: à la Causes a function or sourced script to exit with the return value specified by N.lecture de la documentation, tout utilisateur s'attendrait à ce qu'il échoue ou affiche au moins un avertissement, mais ne se comporte jamais comme exit.
Teresa e Junior
1
Il me semble que quiconque attend un return dans un sous-shell dans une fonction pour revenir de la fonction (dans le processus shell principal) ne comprend pas très bien les sous-shell. Inversement, je m'attendrais à ce qu'un lecteur qui comprend les sous-coquilles s'attende à ce return qu'une sous-coquille dans une fonction termine la sous-coquille, tout comme le exitferait.
Scott
6

Selon la documentation POSIX, l' utilisation en returndehors de la fonction ou du script source n'est pas spécifiée . Cela dépend donc de votre shell à gérer.

Le shell SystemV signalera une erreur, tandis que dans ksh, en returndehors de la fonction ou du script source se comportera comme exit. La plupart des autres shells POSIX et l' os de schily se comportent également comme ça:

$ 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

kshet zshn'a pas été généré car la dernière partie du tuyau dans ces shells a été exécutée dans le shell actuel au lieu de sous-shell. L'instruction return a affecté l'environnement shell actuel qui a appelé la fonction, provoquant le retour immédiat de la fonction sans rien imprimer.

Dans une session interactive, bashsignalez uniquement l'erreur mais n'avez pas terminé le shell, schily's oshsignalé l'erreur et terminé le 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

( zshdans la session interactive et la sortie est le terminal ne s'est pas terminé bash, yashet a schily's oshsignalé l'erreur mais n'a pas terminé le shell)

cuonglm
la source
1
On peut dire qu'il returnest utilisé à l' intérieur d' une fonction ici.
jlliagre
1
@jlliagre: Je ne suis pas sûr de ce que vous voulez dire, a returnété utilisé à l' intérieur de la fonction sous-shell à l' intérieur , sauf et . kshzsh
cuonglm
2
Je veux dire être à l'intérieur d'un sous-shell qui est lui-même à l'intérieur d'une fonction ne signifie pas nécessairement être à l'extérieur de cette fonction, c'est-à-dire que rien dans les composants standard du pipeline d'états ne doit être considéré comme étant à l'extérieur de la fonction où ils se trouvent. Cela mériterait d'être clarifié par le Groupe ouvert.
jlliagre
3
Je pense que non. C'est en dehors de la fonction. Le shell qui a appelé la fonction et le sous-shell qui a exécuté return sont différents.
cuonglm
Je comprends votre raisonnement qui explique à juste titre le problème, mon point est basé sur la grammaire du shell décrite dans la norme POSIX, le pipeline fait partie de la liste composée qui fait partie de la commande composée qui est le corps de la fonction. Nulle part il n'est indiqué que les composants du pipeline doivent être considérés en dehors de la fonction. Tout comme si je suis dans une voiture et que cette voiture est garée dans un garage, je peux supposer que je suis aussi dans ce garage ;-)
jlliagre
4

Je pense que vous avez obtenu le comportement attendu, en bash, chaque commande dans un pipeline est exécutée en sous-shell. Vous pouvez vous convaincre en essayant de modifier une variable globale de votre fonction:

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

Soit dit en passant, le retour fonctionne mais il revient du sous-shell. Encore une fois, vous pouvez vérifier que:

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

Sortira les éléments suivants:

1
This should not be printed.

Donc, l'instruction de retour a correctement quitté le sous-shell

.

Herbert
la source
2
Par conséquent, pour quitter la fonction, utilisez foo(){ : | return 1 || return 2; echo$?; echo "This should not be printed.";}; foo; echo $?et vous obtiendrez un résultat de 2. Mais pour plus de clarté , je ferais le return 1soit exit 1.
dubiousjim
Soit dit en passant, y a-t-il une justification pour le fait que tous les membres d'un pipeline (pas tous sauf un) sont exécutés en sous-coquilles?
Incnis Mrsi
@IncnisMrsi: Voir la réponse de jlliagre .
Scott
1

La réponse plus générale est que bash et certains autres shells placent normalement tous les éléments d'un pipeline dans des processus séparés. Ceci est raisonnable lorsque la ligne de commande est

programme 1 | programme 2 | programme 3

puisque les programmes sont normalement exécutés dans des processus séparés de toute façon (sauf si vous dites ). Mais cela peut surprendreexec program

commande 1 | commande 2 | commande 3

où certaines ou toutes les commandes sont des commandes intégrées. Des exemples triviaux comprennent:

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

Un exemple un peu plus réaliste est

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

où l'ensemble while... do... la doneboucle est mis dans un sous - processus, et ainsi ses modifications tne sont pas visibles sur la coque principale après la fin de la boucle. Et c'est exactement ce que vous faites: canaliser dans une whileboucle, faire fonctionner la boucle en tant que sous-shell, puis essayer de revenir du sous-shell.

Scott
la source