web-dev-qa-db-fra.com

Quel est le problème avec “echo $ (stuff)” ou “echo` stuff` ”?

J'ai utilisé l'un des suivants

echo $(stuff)
echo `stuff`

(où stuff est, par exemple, pwd ou date ou quelque chose de plus compliqué).

Ensuite, on m'a dit que cette syntaxe était fausse, une mauvaise pratique, non élégante, excessive, redondante, excessivement compliquée, une programmation culte du fret, noobish, naïf, etc.

Mais la commande fonctionne , alors qu'est-ce qui ne va pas?

32
Kamil Maciorowski

tl; dr

Un seul stuff pourrait très probablement travailler pour vous.



Réponse complète

Ce qui se produit

Lorsque vous exécutez foo $(stuff), voici ce qui se passe:

  1. stuff s'exécute;
  2. sa sortie (stdout), au lieu d'être imprimée, remplace $(stuff) dans l'appel de foo;
  3. puis foo s'exécute, ses arguments de ligne de commande dépendent évidemment de ce que stuff a renvoyé.

Ce mécanisme $(…) est appelé "substitution de commande". Dans votre cas, la commande principale est echo, qui affiche essentiellement ses arguments de ligne de commande sur stdout. Ainsi, tout ce que stuff essaie d'imprimer sur la sortie standard est capturé, transmis à echo et imprimé sur la sortie standard par echo.

Si vous souhaitez que la sortie de stuff soit imprimée sur stdout, exécutez simplement l'unique stuff.

La syntaxe `…` a le même objectif que $(…) (sous le même nom: "substitution de commande"). Cependant, il existe peu de différences, vous ne pouvez donc pas les échanger aveuglément. Voir this FAQ et cette question .


Devrais-je éviter echo $(stuff) quoi qu'il en soit?

Vous voudrez peut-être utiliser echo $(stuff) si vous savez ce que vous faites. Pour la même raison, évitez echo $(stuff) si vous ne savez pas vraiment ce que vous faites.

Le point stuff et echo $(stuff) ne sont pas exactement équivalents. Ce dernier signifie l'appel de l'opérateur split + glob sur la sortie de stuff avec la valeur par défaut de $IFS. La double citation de la substitution de commande empêche cela. La simple citation de la substitution de commande ne la remplace plus.

Pour observer cela quand il s'agit de scinder, exécutez ces commandes:

echo "a       b"
echo $(echo "a       b")
echo "$(echo "a       b")"  # the Shell is smart enough to identify the inner and outer quotes
echo '$(echo "a       b")'

Et pour globbing:

echo "/*"
echo $(echo "/*")
echo "$(echo "/*")"  # the Shell is smart enough to identify the inner and outer quotes
echo '$(echo "/*")'

Comme vous pouvez le constater, echo "$(stuff)" est équivalent (-ish *) à stuff. Vous pouvez l'utiliser, mais à quoi sert-il de compliquer les choses de cette façon?

Par contre, si vous voulez la sortie de stuff subisse une scission + une agrandissement, alors vous pouvez trouver echo $(stuff) utile. Ce doit être votre décision consciente cependant.

Il existe des commandes générant des sorties qui doivent être évaluées (ce qui inclut le fractionnement, la suppression, etc.) et exécutées par le shell. eval "$(stuff)" est donc une possibilité (voir cette réponse ). Je n'ai jamais vu une commande dont la sortie a besoin de subir une scission supplémentaire + une agrégation avant d'être print . Utiliser délibérément echo $(stuff) semble très rare.


Qu'en est-il de var=$(stuff); echo "$var"?

Bon point. Cet extrait:

var=$(stuff)
echo "$var"

doit être équivalent à echo "$(stuff)" équivalent (-ish *) à stuff. Si c'est tout le code, lancez plutôt stuff.

Si, toutefois, vous devez utiliser la sortie de stuff plusieurs fois, cette approche

var=$(stuff)
foo "$var"
bar "$var"

est généralement mieux que

foo "$(stuff)"
bar "$(stuff)"

Même si foo est echo et que vous obtenez echo "$var" dans votre code, il peut être préférable de le conserver de cette façon. Choses à considérer:

  1. Avec var=$(stuff)stuff s'exécute une fois; même si la commande est rapide, éviter de calculer deux fois la même sortie est la bonne chose. Ou peut-être que stuff a des effets autres que l'écriture sur stdout (par exemple créer un fichier temporaire, démarrer un service, démarrer une machine virtuelle, notifier un serveur distant), afin que vous ne vouliez pas l'exécuter plusieurs fois.
  2. Si stuff génère une sortie temporelle ou quelque peu aléatoire, vous pouvez obtenir des résultats incohérents de foo "$(stuff)" et bar "$(stuff)". Après var=$(stuff), la valeur de $var est corrigée et vous pouvez être sûr que foo "$var" et bar "$var" auront un argument de ligne de commande identique.

Dans certains cas, au lieu de foo "$var", vous voudrez peut-être (besoin) d’utiliser foo $var, en particulier si stuff génère multiple arguments pour foo (une variable de tableau peut être préférable si votre Shell le prend en charge). Encore une fois, sachez ce que vous faites. En ce qui concerne echo, la différence entre echo $var et echo "$var" est la même qu'entre echo $(stuff) et echo "$(stuff)".


* Équivalent ( -ish )?

J'ai dit que echo "$(stuff)" est équivalent (-ish) à stuff. Il y a au moins deux problèmes qui ne le rendent pas exactement équivalent:

  1. $(stuff) exécute stuff dans un sous-shell, il est donc préférable de dire que echo "$(stuff)" est équivalent (-ish) à (stuff). Les commandes qui affectent le shell dans lequel elles s'exécutent, si elles se trouvent dans un sous-shell, n'affectent pas le shell principal.

    Dans cet exemple, stuff est a=1; echo "$a":

    a=0
    echo "$(a=1; echo "$a")"      # echo "$(stuff)"
    echo "$a"
    

    Comparez avec

    a=0
    a=1; echo "$a"                # stuff
    echo "$a"
    

    et avec

    a=0
    (a=1; echo "$a")              # (stuff)
    echo "$a"
    

    Un autre exemple, commencez avec stuff étant cd /; pwd:

    cd /bin
    echo "$(cd /; pwd)"           # echo "$(stuff)"
    pwd
    

    et testez les versions stuff et (stuff).

  2. echo n'est pas un bon outil pour afficher des données non contrôlées . Ce echo "$var" dont nous parlions aurait dû être printf '%s\n' "$var". Mais puisque la question mentionne echo et que la solution la plus probable est de ne pas utiliser echo en premier lieu, j'ai décidé de ne pas introduire printf jusqu'à présent.

  3. stuff ou (stuff) entrelacera les sorties stdout et stderr, alors que echo $(stuff) imprimera toutes les sorties stderr de stuff (qui s'exécute en premier), puis uniquement la sortie de stdout digérée par echo (qui s'exécute en dernier).

  4. $(…) supprime toute nouvelle ligne de la fin, puis echo le rajoute. Donc, echo "$(printf %s 'a')" | xxd donne un résultat différent de printf %s 'a' | xxd.

  5. Certaines commandes (ls par exemple) fonctionnent différemment selon que la sortie standard est une console ou non; alors ls | cat n'a pas le même ls. De même, echo $(ls) fonctionnera différemment de ls.

    En mettant ls de côté, dans un cas général if vous devez forcer cet autre comportement, alors stuff | cat est préférable à echo $(ls) ou echo "$(ls)" car il ne déclenche pas tous les autres problèmes mentionnés ici.

  6. Statut de sortie éventuellement différent (mentionné pour l'intégralité de cette réponse du wiki; pour plus de détails, voir une autre réponse qui mérite un crédit).

63
Kamil Maciorowski

Autre différence: le code de sortie du sous-shell est perdu. Le code de sortie de echo est donc récupéré.

> stuff() { return 1
}
> stuff; echo $?
1
> echo $(stuff); echo $?
0
5
WaffleSouffle