7
votes

Commande shell sans opération qui préserve $?

En utilisant uniquement les fonctionnalités du shell POSIX, existe-t-il une "commande simple" qui ne fait rien et ne change pas la valeur de $? Les gens décrivent généralement : comme une commande no-op pour le shell, mais cela définit toujours $? à zéro, donc ce n'est pas ce que je veux.

Ceci est nécessaire à un programme qui génère des scripts shell. À plusieurs endroits, il doit émettre un bloc if-then-else

if CONDITION
then
    TRUE-COMMANDS
else
    FALSE-COMMANDS
fi

mais, en raison de complications supplémentaires que je préfère ne pas essayer d'expliquer maintenant, il ne peut pas dire de manière fiable si TRUE-COMMANDS et FALSE-COMMANDS sont vides. Une clause then- or else vide sera une erreur de syntaxe shell.

: Peut être mis au début de then -clause pour prendre soin de TRUE-COMMANDS étant vide, mais cela ne fonctionnera pas pour FALSE-COMMANDS parce qu'il aplatit $? et les FALSE-COMMANDS pourraient vouloir regarder la valeur $? a été défini par la condition. Pour la même raison, : ne peut pas être mis après FALSE-COMMANDS â € » le code après if-then-else besoin de puissance pour voir $? à partir de la dernière opération dans le if-then-else.

Des points bonus si vous pouvez éviter:

  • Forking: (exit $?) Fait le travail, mais il y a tellement de ces blocs conditionnels dans un script généré que cela produit un ralentissement mesurable.

  • Fonctions: donné nop () { return $? } alors nop fait le travail, mais en raison de plus de complications que je préfère ne pas entrer, il n'est pas pratique de définir nop suffisamment tôt pour tous les endroits qui en auraient besoin.


10 commentaires

Je pense que CONDITION fixera $ ?; vous ne pouvez donc pas évaluer votre if sans le définir.


@mevets Oui, c'est exactement le problème. status=$? au début de FALSE-COMMANDS faut définir l' status sur l'état de sortie de CONDITION , pas sur zéro. Si je mets un : immédiatement après else cela n'arrivera pas.


sinon vous pourriez mettre un while CONDITION; do : ; done comme votre instruction nulle pour la clause else, puisque CONDITION ne peut pas être vraie, elle ne s'exécuterait jamais.


@mevets CONDITION peut avoir des effets secondaires, donc ce n'est pas bon.


function r() { return $1; } alors x=$?; if CONDITION; then r $x; else ls; fi


Comment ne pas savoir si FALSE-COMMANDS est vide si c'est vous qui le générez ? Cela ressemble à un problème XY; vous devriez demander comment savoir si la liste de commandes est vide ou non.


@chepner Lisez l'intégralité de git.savannah.gnu.org/cgit/autoconf.git/tree/lib/m4sugar/… et ... / m4sh.m4, en accordant une attention particulière aux commentaires ci-dessus AS_IF, m4_expand et m4_provide, et alors si vous pouvez penser à quelque chose, je suis toutes les oreilles.


Veuillez lire stackoverflow.com/help/minimal-reproducible-example .


@chepner La question que j'ai posée peut répondre telle quelle. Je ne vais pas faire plus d'efforts dans une approche alternative que j'avais déjà décidé n'était pas faisable avant de poser la question.


Il n'y a aucune commande qui ne modifie pas $? .


3 Réponses :


2
votes

Il n'est certainement pas possible dans un shell compatible POSIX d'écrire une commande qui ne touche pas $? . En lisant la norme IEEE 1003.1-2017 vol. 3 chapitre 2 «Shell Command Language», nous apprenons que:

  • §2.8.2 indique que «[chaque] commande a un statut de sortie» (donc aucune commande n'a pas de statut de sortie).
  • §2.5.2 «Paramètres spéciaux» dit que $? «[e] xpands à l'état de sortie décimal du pipeline le plus récent (voir Section 2.9.2)».
  • §2.9.2 «Pipelines» indique qu'un pipeline est une séquence d'une ou plusieurs commandes, éventuellement précédée (dans son ensemble) par ! , avec des commandes jointes par | . À propos de l'état de sortie, il dit

    Si le pipeline ne commence pas par le ! réservé, l'état de sortie doit être l'état de sortie de la dernière commande spécifiée dans le pipeline. Sinon, l'état de sortie doit être le NON logique de l'état de sortie de la dernière commande.

  • §2.9 «Shell Commands» définit une «commande» et dit que «[u] nsauf indication contraire, le statut de sortie d'une commande sera celui de la dernière commande simple exécutée par la commande». ™. Une «commande» peut être l'une des suivantes:
    • Une commande simple (§2.9.1), qui est simplement un programme externe, un shell intégré ou une affectation de variable (sans nom de commande). Le premier retournera bien sûr l'état de sortie de la commande exécutée. À propos de ce dernier, la spécification dit:

      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 nul.

    • Un pipeline, décrit par le §2.9.2 mentionné ci-dessus.
    • Une liste composée (§2.9.3), qui est une séquence d'une ou plusieurs {séquences d'un ou plusieurs {séquences d'un ou plusieurs pipelines (voir ci-dessus), jointes par && ou || }, chacun terminé par ; ou & (avec un final ; facultatif)}, joint par des caractères de nouvelle ligne. Une liste composée renvoie l'état de sortie du dernier pipeline exécuté de manière synchrone; Les pipelines exécutés de manière asynchrone (ceux terminés par & ) définissent l'état de sortie sur zéro. Aucune des séquences ne peut être vide, ce qui garantit qu'au moins un pipeline sera exécuté.
    • Une commande composée (§2.9.4), qui est soit:
      • Un sous-shell ou une liste composée accolée (§2.9.4.1), qui renvoie l'état de sortie de la liste composée sous-jacente (voir ci-dessus).
      • Une construction conditionnelle ( case , §2.9.4.3 ou if , §2.9.4.4) ou une boucle ( for , §2.9.4.2; while , §2.9.4.5; until , §2.9.4.6). Si le corps de cette construction n'est pas exécuté, elle renvoie un statut de sortie nul.
    • Une définition de fonction (§2.9.5), qui renvoie un état de sortie de zéro si la définition est acceptée, et un état différent de zéro dans le cas contraire.
  • Et enfin, §2.9.4.4 «La construction conditionnelle if» définit la condition et le corps de la construction if comme étant une liste composée, décrite par le §2.9.3 mentionné ci-dessus. Cela signifie que le corps d'une construction if contiendra toujours au moins une commande qui écrasera $? . La spécification ne laisse même aucune marge de manœuvre pour le «comportement défini par la mise en œuvre».

Tout ce qui précède signifie que la seule façon d'écrire une commande no-op qui préserve $? est en lisant la valeur en $? et le renvoyer. Il y a trois constructions dans le shell POSIX qui en sont capables, dont deux sont déjà mentionnées dans le corps de la question: une exit d'un sous-shell ou une invocation de fonction.

Le troisième est . : la déclaration d'approvisionnement en shell mentionnée dans la réponse de @ Jens. En recherchant un script contenant juste un return "$?" commande, la valeur de $? peuvent être préservés. Cependant, cela vous oblige à faire en sorte qu'un script approprié soit trouvé à un emplacement connu, ce qui, je suppose, est tout aussi gênant que de s'assurer qu'une fonction no-op a été définie suffisamment tôt dans le fichier (sinon plus).

Si vous pliez un peu l'exigence stricte de POSIX, même cela peut être surmonté:

. /dev/stdin <<EOF
return "$?"
EOF

/dev/stdin n'est pas une fonctionnalité POSIX, mais est largement disponible; il est explicitement répertorié dans IEEE Std 1003.1-2017 vol. 1 §2.1.1 comme extension. L'extrait ci-dessus a été testé pour fonctionner dans bash 5.0.8, dash 0.5.11 et busybox sh 1.30.1 sous Linux (avec une configuration /dev appropriée et ainsi de suite).


1 commentaires

Après réflexion, je vous donne la prime parce que j'apprécie votre fouille dans POSIX pour confirmer que je n'avais manqué aucune possibilité, mais j'accepte la réponse de kvantour qui m'a quand même donné une nouvelle possibilité.



1
votes

Le plus simple serait d'utiliser une simple mission. Au lieu d'utiliser : , do _rc=$? .

if condition; then
   [ list-true ]     # optional 
   _rc=$?
else
   [ list-false ]    # optional
   _rc=$?
fi
( exit $_rc )        # optional
list-post-if

En utilisant cette variable _rc , vous avez stocké l'état de sortie de la dernière commande exécutée, qu'il s'agisse d'une condition ou de la dernière commande dans list-true ou list-false .

  • Les arguments en faveur de cette méthode sont la faible surcharge d'une affectation.
  • L'argument contre est le besoin d'au moins réécrire list-post-if pour utiliser _rc au lieu de $? .
  • Si ce dernier n'est pas possible, ou trop fastidieux, vous pourriez envisager d'ajouter un (exit $_rc) juste après l'instruction conditionnelle. Ceci, cependant, nécessite un sous-shell, mais ce n'est qu'un seul.


2 commentaires

Après réflexion, j'ai donné à user3840170 la prime parce que j'apprécie leur fouille dans POSIX pour confirmer que je n'avais manqué aucune possibilité, mais j'accepte cette réponse qui m'a quand même donné une nouvelle possibilité.


@zwol Merci, très apprécié et bonne décision de donner à user3840170 la prime.



1
votes

Ne laissez personne vous dire qu'il n'y a que des fonctions et des sous-shells.

Pouvez-vous créer ou distribuer un autre petit fichier? Si tel est le cas, vous pouvez créer un fichier avec juste

cat > keepstatus.c <<EOF
#include <stdlib.h>
int main(int argc, char **argv) {
    return argv[1] ? atoi(argv[1]) : 0;
}
EOF
$CC -o keepstatus keepstatus.c

puis source-le comme une commande "vide" gardant l'état de sortie:

$ echo 'return $?' > keepstatus
$ ls foobar
ls: fooobar: No such file or directory
$ . ./keepstatus
$ echo $?
2
$ sleep 100
^C
$ . ./keepstatus
$ echo $?
130

Ne bifche pas, n'utilise pas de fonctions, pas de variable supplémentaire, garde le statut et est aussi POSIXly que possible.

Il y a même une quatrième façon dont je peux voir, lorsque je sacrifie les points bonus pour avoir bifurqué et supposé, puisque vous êtes dans le territoire autoconf m4, que trouver et utiliser un compilateur hôte est un jeu d'enfant.

return $?

Ensuite, utilisez /path/to/keepstatus $? .


9 commentaires

Je pense que cela fonctionne bien, mais la lecture d'un fichier externe ne créerait-elle pas plus de frais généraux que d'appeler un sous-shell? J'ai essayé quelque chose de similaire en utilisant un heredoc, mais cela ne fonctionne pas du tout. Néanmoins, vote favorable.


@kvantour Je dirais que c'est moins de frais généraux, puisque les sous-shells doivent faire une certaine comptabilité, tandis que l'approvisionnement est ouvert / lu / exécuté / fermé. Mais je n'ai jamais implémenté de shell donc tout cela pourrait être de la fantaisie :-)


Faire en sorte qu'un fichier séparé existe dans un emplacement connu est probablement encore plus problématique que de s'assurer qu'une fonction est définie suffisamment tôt dans le même script, ce que le demandeur a mentionné comme n'étant pas particulièrement faisable. Ce n'est donc pas vraiment une solution.


@ user3840170 Comment le savez-vous? L'utilisateur crée du code shell. Un seul return $? est exactement un tout petit peu de cela. Oh, et j'attends toujours une réponse pourquoi vous avez volé ma solution et l'avez plagiée dans votre réponse. Qu'est devenu le crédit là où le crédit est dû?


Je ne le fais pas - mais le demandeur voulait une solution dans le shell POSIX, donc l'hypothèse du pire des cas est qu'il devrait pouvoir fonctionner tel quel sur un système POSIX arbitraire, où de telles choses n'ont pas été pré-arrangées, surtout depuis il a été mentionné que la définition d'une fonction de non-opération suffisamment tôt ne serait pas particulièrement pratique. Je suppose donc que pré-organiser d'autres choses serait tout aussi gênant.


Le moyen le plus simple pour le script généré de s'assurer qu'un tel return $? one-liner est disponible serait de le créer dans /tmp tôt dans le script généré lui-même - mais alors, vous pourriez aussi bien y mettre une définition d'une fonction no-op et vous passer complètement du script one-liner.


De plus, vous avez votre crédit, que voulez-vous de plus? J'ai également ajouté le bit /dev/stdin , donc ce n'est pas comme si je n'y avais rien ajouté.


@ user3840170 Je trouve votre attitude à deviner quelle est une solution assez grossière. C'est entièrement à zwol de décider. Ma solution répond à toutes ses exigences et ne viole aucun de ses no-gos. J'ai même dû vous corriger sur le POSIXness de . (point).


Il serait un peu plus facile pour moi de préorganiser l'existence de ce script d'aide que de préorganiser une fonction shell. Cependant, je devrais également faire en sorte que l'assistant soit nettoyé par la suite, et le piège 0 est déjà utilisé pour quelque chose sans rapport, donc dans l'ensemble, cette approche n'est pas plus facile que d'utiliser une fonction.