Comment puis-je faire en sorte que bash se termine en cas de défaillance du backtick de la même manière que pipefail?

55

Donc, j'aime durcir mes scripts bash partout où je peux (et quand je ne peux pas déléguer à un langage comme Python / Ruby) pour que les erreurs ne restent pas sans erreur.

Dans cette veine, j'ai un strict.sh, qui contient des choses comme:

set -e
set -u
set -o pipefail

Et source dans d'autres scripts. Cependant, tandis que pipefail prendrait:

false | echo it kept going | true

Il ne va pas ramasser:

echo The output is '`false; echo something else`' 

La sortie serait

La sortie est ''

False renvoie un statut non nul et no-stdout. Dans un tuyau, cela aurait échoué, mais ici l'erreur n'est pas interceptée. Lorsqu'il s'agit en réalité d'un calcul stocké dans une variable pour plus tard et que la valeur est définie sur vide, cela peut alors causer des problèmes ultérieurs.

Alors - y a-t-il un moyen de faire en sorte que bash traite un code de retour non nul dans un backtick comme une raison suffisante pour sortir?

Danny Staple
la source

Réponses:

51

Le langage exact utilisé dans la spécification Single UNIX pour décrire la signification deset -e est:

Lorsque cette option est activée, si une commande simple échoue pour l'une des raisons répertoriées dans Conséquences des erreurs de shell ou renvoie une valeur de statut de sortie> 0, et n'est pas une [commande conditionnelle ou inversée], le shell doit immédiatement quitter.

Il y a une ambiguïté sur ce qui se passe lorsqu'une telle commande est exécutée dans un sous - shell . D'un point de vue pratique, tout ce que le sous-shell peut faire est de quitter et de renvoyer un statut différent de zéro au shell parent. La sortie du shell parent dépend de la transformation de cet état non nul en une simple commande échouant dans le shell parent.

Un de ces problèmes est celui que vous avez rencontré: un état de retour différent de zéro à partir d'une substitution de commande . Étant donné que cet état est ignoré, le shell parent ne se ferme pas. Comme vous l'avez déjà découvert , un moyen de prendre en compte le statut de sortie consiste à utiliser la substitution de commande dans une affectation simple : le statut de sortie de l'affectation est alors le statut de sortie de la dernière substitution de commande dans la ou les assignations .

Notez que cela fonctionnera comme prévu uniquement s'il y a une substitution de commande unique, car seul le statut de la dernière substitution est pris en compte. Par exemple, la commande suivante réussit (à la fois selon le standard et dans chaque implémentation que j'ai vue):

a=$(false)$(echo foo)

Un autre cas à surveiller est sous - couches explicites : (somecommand). Selon l'interprétation ci-dessus, le sous-shell peut renvoyer un statut différent de zéro, mais comme il ne s'agit pas d'une simple commande dans le shell parent, le shell parent doit continuer. En fait, tous les obus que je connais font revenir le parent à ce stade. Bien que cela soit utile dans de nombreux cas, par exemple (cd /some/dir && somecommand)lorsque les parenthèses sont utilisées pour conserver une opération telle qu'un changement d'annuaire en cours, il enfreint la spécification si elle set -eest désactivée dans le sous-shell ou si le sous-shell renvoie un statut différent de zéro de manière à ce que ne le terminerait pas, par exemple en utilisant !une vraie commande. Par exemple, ash, bash, pdksh, ksh93 et ​​zsh se ferment sans afficher fooles exemples suivants:

set -e; (set +e; false); echo "This should be displayed"
set -e; (! true); echo "This should be displayed"

Pourtant, aucune commande simple n'a échoué tant qu'elle set -eétait en vigueur!

Un troisième cas problématique concerne les éléments d'un pipeline non trivial . En pratique, tous les shells ignorent les défaillances des éléments du pipeline autres que le dernier et présentent l'un des deux comportements suivants en ce qui concerne le dernier élément de pipeline:

  • ATT ksh et zsh, qui exécutent le dernier élément du pipeline dans le shell parent, fonctionnent comme d'habitude: si une commande simple échoue dans le dernier élément du pipeline, le shell qui l'exécute, qui se trouve être le shell parent, sorties.
  • D'autres shells approchent le comportement en quittant si le dernier élément du pipeline renvoie un statut différent de zéro.

Comme auparavant, le fait de désactiver set -eou d'utiliser une négation dans le dernier élément du pipeline entraîne le renvoi d'un statut différent de zéro de manière à ne pas terminer le shell; les obus autres que ATT ksh et zsh vont alors sortir.

L' pipefailoption de Bash entraîne la fermeture immédiate d'un pipeline set -esi l'un de ses éléments renvoie un statut différent de zéro.

Notez que, complication supplémentaire, bash est désactivé set -edans les sous-shell, sauf s’il est en mode POSIX ( set -o posixou POSIXLY_CORRECTdans l’environnement au démarrage de bash).

Tout cela montre que la spécification POSIX fait malheureusement un travail médiocre en spécifiant l' -eoption. Heureusement, les obus existants sont généralement cohérents dans leur comportement.

Gilles, arrête de faire le mal
la source
Merci pour cela. Une expérience que j’ai eue est que certaines erreurs détectées dans une version ultérieure de bash ont été ignorées dans les versions précédentes avec set -e. Mon intention ici est de durcir les scripts dans la mesure où toute condition de retour d'erreur / échec non traitée entraînera la fermeture d'un script. Ceci est dans un système hérité qui a été connu pour produire des fichiers de sortie de déchets et un code de sortie heureux "0" après une demi-heure de chaos avec le mauvais env - sauf si vous regardiez la sortie comme un faucon (et pas toutes les erreurs sont sur stderr, certaines sur stdout et certaines parties sont / dev / null'd), vous ne savez tout simplement pas.
Danny Staple
"Par exemple, tous les éléments ash, bash, pdksh, ksh93 et ​​zsh se terminent sans afficher foo dans les exemples suivants": asy ne se comporte pas de la manière suivante: BusyBox ash affiche la sortie selon les spécifications. (Testé avec BusyBox 1.19.4.) Il ne sort pas non plus avec set -e; (cd /nonexisting).
dubiousjim
27

(Répondre à la mienne parce que j'ai trouvé une solution) Une solution consiste à toujours l'affecter à une variable intermédiaire. De cette façon, le code de retour ( $?) est défini.

Alors

ABC=`exit 1`
echo $?

Est-ce que la sortie 1(ou la sortie si elle set -eest présente), cependant:

echo `exit 1`
echo $?

Sortira 0après une ligne vide. Le code retour de l'écho (ou une autre commande exécutée avec la sortie backtick) remplacera le code retour 0.

Je suis toujours ouvert aux solutions qui n'exigent pas la variable intermédiaire, mais cela me permet de faire quelques progrès.

Danny Staple
la source
24

Comme le PO l'a souligné dans sa propre réponse, l'affectation du résultat de la sous-commande à une variable résout le problème. le $?reste indemne.

Cependant, un cas de bord peut toujours vous intercepter avec de faux négatifs (c’est-à-dire que la commande échoue mais que l’erreur ne bouillonne pas), localdéclaration de variable:

local myvar=$(subcommand)sera toujours revenir 0!

bash(1) souligne ceci:

   local [option] [name[=value] ...]
          ... The return status is 0 unless local is used outside a function,
          an invalid name is supplied, or name is a readonly variable.

Voici un cas de test simple:

#!/bin/bash

function test1() {
  data1=$(false) # undeclared variable
  echo 'data1=$(false):' "$?"
  local data2=$(false) # declaring and assigning in one go
  echo 'local data2=$(false):' "$?"
  local data3
  data3=$(false) # assigning a declared variable
  echo 'local data3; data3=$(false):' "$?"
}

test1

Le résultat:

data1=$(false): 1
local data2=$(false): 0
local data3; data3=$(false): 1
mogsie
la source
merci, l'incohérence entre VAR=...et local VAR=...m'a vraiment déconcerté!
Jonny
13

Comme d'autres l'ont déjà dit, localle résultat retournera toujours 0. La solution consiste à déclarer d'abord la variable:

function testcase()
{
    local MYRESULT

    MYRESULT=$(false)
    if (( $? != 0 )); then
        echo "False returned false!"
        return 1
    fi

    return 0
}

Sortie:

$ testcase
False returned false!
$ 
Volonté
la source
4

Pour sortir d'un échec de substitution de commande, vous pouvez définir explicitement -edans un sous-shell, comme ceci:

set -e
x=$(set -e; false; true)
echo "this will never be shown"
errr
la source
1

Point intéressant!

Je ne suis jamais tombé sur ça, parce que je ne suis pas un ami de set -e(mais que je préfère trap ... ERR), mais j'ai déjà testé cela: trap ... ERRne $(...)capturez pas non plus les erreurs internes (ou les backticks à l'ancienne).

Je pense que le problème est (comme si souvent) qu’un sous-shell est appelé ici et -esignifie explicitement le shell actuel .

Seule une autre solution qui m'est venue à l’esprit à ce moment-là serait d’utiliser la lecture

 ls -l ghost_under_bed | read name

Cela jette ERRet avec -ele shell sera terminé. Seul problème: cela ne fonctionne que pour les commandes avec une seule ligne de sortie (ou vous passez au travers de quelque chose qui joint des lignes).

ktf
la source
1
Pas sûr du tour de lecture, essayer de ne pas lier la variable. Je pense que cela est peut-être dû au fait que l’autre côté du tuyau est un sous-shell, le nom ne sera pas disponible.
Danny Staple