Comment le statut de retour d'une affectation de variable est-il déterminé?

10

J'ai vu des constructions dans des scripts comme celui-ci:

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

Est-ce documenté quelque part? Comment le statut de retour d'une variable est-il déterminé et comment est-il lié à la substitution de commandes? (Par exemple, pourrais-je obtenir le même résultat avec if echo "$(somecommand 2>/dev/null)"; then?)

Caractère générique
la source

Réponses:

12

Il est documenté (pour POSIX) dans la section 2.9.1 Commandes simples des spécifications de base du groupe ouvert. Il y a un mur de texte là-bas; J'attire votre attention sur le dernier paragraphe:

S'il existe un nom de commande, l'exécution doit continuer comme décrit dans Recherche et exécution de commandes . S'il n'y a pas de nom de commande, mais que la commande contenait une substitution de commande, la commande doit se terminer avec l'état de sortie de la dernière substitution de commande effectuée. Sinon, la commande doit se terminer avec un état de sortie zéro.

Ainsi, par exemple,

   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"

C'est aussi ainsi que bash fonctionne. Mais voir aussi la section «pas si simple» à la fin.

phk , dans sa question Les affectations sont comme des commandes avec un statut de sortie sauf quand il y a substitution de commande? , suggère

… Il semble que l'affectation elle-même compte comme une commande… avec une valeur de sortie nulle, mais qui s'applique avant le côté droit de l'affectation (par exemple, un appel de substitution de commande…)

Ce n'est pas une façon terrible de voir les choses. Un schéma brut pour la détermination du statut de retour d'une commande simple (ne contenant pas une ;, &, |, &&ou ||) est:

  • Parcourez la ligne de gauche à droite jusqu'à la fin ou un mot de commande (généralement un nom de programme).
  • Si vous voyez une affectation de variable, le statut de retour de la ligne pourrait être 0.
  • Si vous voyez une substitution de commande, c'est-à-dire, $(…)prenez le statut de sortie de cette commande.
  • Si vous atteignez une commande réelle (pas dans une substitution de commande), prenez le statut de sortie de cette commande.
  • Le statut de retour de la ligne est le dernier numéro que vous avez rencontré.
    Les substitutions de commandes comme arguments de la commande, par exemple, foo $(bar)ne comptent pas; vous obtenez le statut de sortie de foo. Pour paraphraser la notation de phk , le comportement ici est

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

Mais c'est une légère simplification excessive. Le statut de retour global de

A = $ ( cmd 1 ) B = $ ( cmd 2 ) C = $ ( cmd 3 ) D = $ ( cmd 4 ) E = mc 2
est l'état de sortie de . L' affectation qui se produit après l' affectation ne définit pas l'état de sortie global sur 0.cmd4E=D=

icarus , dans sa réponse à la question de phk , soulève un point important: les variables peuvent être définies en lecture seule. L'avant-dernier paragraphe de la section 2.9.1 de la norme POSIX dit:

Si l'une des affectations de variables tente d'affecter une valeur à une variable pour laquelle l' attribut readonly est défini dans l'environnement shell actuel (indépendamment du fait que l'affectation soit effectuée dans cet environnement), une erreur d'affectation de variable se produit. Voir Conséquences des erreurs Shell pour les conséquences de ces erreurs.

donc si tu dis

readonly A
C=Garfield A=Felix T=Tigger

l'état de retour est 1. Peu importe si les chaînes Garfield, Felixet / ou Tigger sont remplacés par substitution de commande (s) - mais voir les notes ci - dessous.

Section 2.8.1 Conséquences des erreurs de shell a un autre groupe de texte et un tableau et se termine par

Dans tous les cas indiqués dans le tableau où un shell interactif ne doit pas quitter, le shell ne doit effectuer aucun autre traitement de la commande dans laquelle l'erreur s'est produite.

Certains détails ont du sens; certains ne le font pas:

  • L' A=affectation abandonne parfois la ligne de commande, comme cette dernière phrase semble le spécifier. Dans l'exemple ci-dessus, Cest défini sur Garfield, mais Tn'est pas défini (et, bien sûr, aucun ne l'est  A).
  • De même, s'exécute mais pas . Mais, dans mes versions de bash (qui comprennent 4.1.X et 4.3.X), il n'exécute . (Soit dit en passant, cela entrave encore l'interprétation de phk selon laquelle la valeur de sortie de l'affectation s'applique avant le côté droit de l'affectation.)C=$(cmd1) A=$(cmd2) T=$(cmd3)cmd1cmd3
    cmd2

Mais voici une surprise:

Dans mes versions de bash,

en lecture seule A
C = quelque chose A = quelque chose T = quelque chose  cmd 0

n'exécute. En particulier,cmd0

C = $ ( cmd 1 ) A = $ ( cmd 2 ) T = $ ( cmd 3 )    cmd 0
s'exécute et , mais pas . (Notez que c'est l'opposé de son comportement quand il n'y a pas de commande.) Et il se positionne (ainsi que ) dans l'environnement de . Je me demande si c'est un bug dans bash.cmd1cmd3cmd2TCcmd0


Pas si simple:

Le premier paragraphe de cette réponse fait référence aux «commandes simples».  La spécification dit,

Une «commande simple» est une séquence d'affectations et de redirections de variables facultatives, dans n'importe quelle séquence, éventuellement suivie de mots et de redirections, terminée par un opérateur de contrôle.

Ce sont des déclarations comme celles de mon premier bloc d'exemple:

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

les trois premiers incluent des affectations de variables et les trois derniers incluent des substitutions de commandes.

Mais certaines affectations de variables ne sont pas aussi simples.  bash (1) dit,

Déclarations d'affectation peuvent également apparaître comme arguments à la alias, declare, typeset, export, readonlyet localbuiltin commandes ( déclaration des commandes).

Pour export, la spécification POSIX dit,

ÉTAT DE SORTIE

    0
      Tous les opérandes de nom ont été exportés avec succès.
    > 0
      Au moins un nom n'a pas pu être exporté ou l' -poption a été spécifiée et une erreur s'est produite.

Et POSIX ne prend pas en charge local, mais bash (1) dit,

C'est une erreur à utiliser localen dehors d'une fonction. L'état de retour est 0 sauf s'il localest utilisé en dehors d'une fonction, si un nom non valide est fourni ou si le nom est une variable en lecture seule.

En lisant entre les lignes, nous pouvons voir que les commandes de déclaration comme

export FOO=$(bar)

et

local FOO=$(bar)

sont plus comme

foo $(bar)

dans la mesure où ils ignorent l'état de sortie bar et vous donner un statut de sortie en fonction de la commande principale ( export, localou foo). Nous avons donc une étrangeté comme

   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 nous pouvons démontrer avec

$ 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

et

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

Heureusement, ShellCheck détecte l'erreur et déclenche SC2155 , qui indique que

export foo="$(mycmd)"

devrait être changé en

foo=$(mycmd)
export foo

et

local foo="$(mycmd)"

devrait être changé en

local foo
foo=$(mycmd)
G-Man dit «Réintègre Monica»
la source
1
Super merci! Pour les points bonus, savez-vous comment les localliens avec cela? Par exemple local foo=$(bar)?
Wildcard
1
Pour le deuxième exemple (juste FOO=$(bar)), il convient de noter que théoriquement le statut de sortie et l'affectation peuvent jouer un rôle, voir unix.stackexchange.com/a/341013/117599
phk
1
@Wildcard: Je suis content que vous l'aimiez. Je viens de le mettre à jour à nouveau; une grande partie de la version que vous venez de lire était fausse. Tant que vous êtes ici, qu'en pensez-vous? Est-ce un bug dans bash, qui A=foo cmds'exécute cmdmême s'il Aest en lecture seule?
G-Man dit `` Réintègre Monica '' le
1
@phk: (1) Théorie intéressante, mais je ne sais pas en quoi cela a du sens. Jetez un autre coup d'œil à l'exemple juste avant ma rubrique «surprise». Si Aest en lecture seule, la commande C=value₁ A=value₂ T=value₃définit Cmais pas T(et, bien sûr, An'est pas définie) - le shell a terminé le traitement de la ligne de commande, en ignorant T=value₃, car il A=value₂s'agit d'une erreur. (2) Merci pour le lien vers la question Stack Overflow - j'ai posté des commentaires à ce sujet.
G-Man dit `` Réintègre Monica '' le
1
@Wildcard "Pour les points bonus, savez-vous comment les liens locaux s'y rattachent?". Oui ... local=$(false)a une valeur de sortie , 0car ( à partir de la page de manuel): 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.. Ce n'est pas assez de trollface sur le monde pour le génie qui l'a conçu de cette façon.
David Tonhofer
2

Il est documenté dans Bash ( LESS=+/'^SIMPLE COMMAND EXPANSION' bash):

S'il reste un nom de commande après l'extension .... Sinon, la commande se ferme. ... S'il n'y a pas eu de substitution de commande, la commande se termine avec un état de zéro.

En d'autres termes (mes mots):

S'il ne reste aucun nom de commande après l'expansion et qu'aucune substitution de commande n'a été exécutée, la ligne de commande se ferme avec un état de zéro.

phk
la source