En quoi la redirection de fichiers bash vers la norme diffère-t-elle du shell (`sh`) sous Linux?

9

J'ai écrit un script qui passe des utilisateurs lors de l' exécution, et exécuté à l'aide de la redirection de fichier dans la norme. Donc , user-switch.shest ...

#!/bin/bash

whoami
sudo su -l root
whoami

Et l'exécuter avec bashme donne le comportement que j'attends

$ bash < user-switch.sh
vagrant
root

Cependant, si j'exécute le script avec sh, j'obtiens une sortie différente

$ sh < user-switch.sh 
vagrant
vagrant

Pourquoi bash < user-switch.shdonner une sortie différente de celle sh < user-switch.sh?

Remarques:

  • se produit sur deux boîtes différentes exécutant Debian Jessie
popedotninja
la source
1
Pas une réponse, mais concernant sudo su: unix.stackexchange.com/questions/218169/…
Kusalananda
1
est / bin / sh = dash sur Debian? Je ne peux pas faire de repro sur RHEL où / bin / sh = bash
Jeff Schaller
1
/bin/shsur (ma copie de) Debian est Dash, oui. Je ne savais même pas ce que c'était jusqu'à maintenant. Je suis resté avec bash jusqu'à présent.
popedotninja
1
Le comportement bash n'est pas non plus garanti par une sémantique documentée.
Charles Duffy
Quelqu'un m'a fait remarquer aujourd'hui que le script que j'exécutais violait la sémantique de l'utilisation de bash. Il y a eu quelques bonnes réponses à cette question, mais il convient de souligner que l'on ne devrait probablement pas changer d'utilisateur en milieu de script. J'ai essayé user-switch.shà l' origine dans un travail Jenkins et le script a été exécuté. L'exécution du même script en dehors de Jenkins a produit des résultats différents, et j'ai eu recours à la redirection de fichiers pour obtenir le comportement que j'ai vu dans Jenkins.
popedotninja

Réponses:

12

Un script similaire, sans sudo, mais des résultats similaires:

$ cat script.sh
#!/bin/bash
sed -e 's/^/--/'
whoami

$ bash < script.sh
--whoami

$ dash < script.sh
itvirta

Avec bash, le reste du script va en entrée à sed, avec dash, le shell l'interprète.

En cours straced' exécution sur: dashlit un bloc du script (huit Ko ici, plus que suffisant pour contenir l'intégralité du script), puis apparaît sed:

read(0, "#!/bin/bash\nsed -e 's/^/--/'\nwho"..., 8192) = 36
stat("/bin/sed", {st_mode=S_IFREG|0755, st_size=73416, ...}) = 0
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|...

Ce qui signifie que le descripteur de fichier est à la fin du fichier et sedne verra aucune entrée. La partie restante étant tamponnée à l'intérieur dash. (Si le script était plus long que la taille de bloc de 8 Ko, la partie restante serait lue par sed.)

Bash, par contre, cherche à revenir à la fin de la dernière commande:

read(0, "#!/bin/bash\nsed -e 's/^/--/'\nwho"..., 36) = 36
stat("/bin/sed", {st_mode=S_IFREG|0755, st_size=73416, ...}) = 0
...
lseek(0, -7, SEEK_CUR)                  = 29
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|...

Si l'entrée provient d'un tuyau, comme ici:

$ cat script.sh | bash

le rembobinage ne peut pas être fait, car les tuyaux et les prises ne sont pas recherchés. Dans ce cas, Bash revient à lire l'entrée un caractère à la fois pour éviter les surcharges. ( fd_to_buffered_stream()ininput.c ) Faire un appel système complet pour chaque octet n'est pas très efficace en principe. Dans la pratique, je ne pense pas que les lectures seront un gros frais généraux par exemple par rapport au fait que la plupart des choses que le shell implique impliquent de nouveaux processus entiers.

Une situation similaire est la suivante:

echo -e 'foo\nbar\ndoo' | bash -c 'read a; head -1'

Le sous-shell doit s'assurer de readne lire que la première nouvelle ligne, de sorte que headla ligne suivante soit visible. (Cela fonctionne dashaussi avec .)


En d'autres termes, Bash va à des longueurs supplémentaires pour prendre en charge la lecture de la même source pour le script lui-même et pour les commandes exécutées à partir de celui-ci. dashnon. Le zsh, et ksh93empaqueté dans Debian vont avec Bash à ce sujet.

ilkkachu
la source
1
Franchement, je suis un peu surpris que ça marche.
ilkkachu
merci pour la bonne réponse! J'exécuterai les deux commandes en utilisant straceplus tard et comparerai les sorties. Je n'avais pas pensé à utiliser cette approche.
popedotninja
2
Oui, ma réaction à la lecture de la question était surprenant que cela fait le travail avec bash - je l' avais remisée comme quelque chose qui serait certainement pas le travail. Je suppose que je n'avais qu'à moitié raison :)
hobbs
12

Le shell lit le script à partir de l'entrée standard. Dans le script, vous exécutez une commande qui veut également lire l'entrée standard. Quelle entrée va aller où? Vous ne pouvez pas le dire de manière fiable .

La façon dont les shells fonctionnent est qu'ils lisent un morceau de code source, l'analysent, et s'ils trouvent une commande complète, exécutez la commande, puis passez au reste du morceau et au reste du fichier. Si le morceau ne contient pas de commande complète (avec un caractère de fin à la fin - je pense que tous les shells sont lus jusqu'à la fin d'une ligne), le shell lit un autre morceau, et ainsi de suite.

Si une commande du script essaie de lire à partir du même descripteur de fichier que le shell lit le script, alors la commande trouvera tout ce qui vient après le dernier morceau lu. Cet emplacement est imprévisible: il dépend de la taille de bloc choisie par le shell, et cela peut dépendre non seulement du shell et de sa version mais de la configuration de la machine, de la mémoire disponible, etc.

Bash cherche à la fin du code source d'une commande dans le script avant d'exécuter la commande. Ce n'est pas quelque chose sur lequel vous pouvez compter, non seulement parce que d'autres shells ne le font pas, mais aussi parce que cela ne fonctionne que si le shell lit à partir d'un fichier normal. Si le shell lit un tube (par exemple ssh remote-host.example.com <local-script-file.sh), les données lues sont lues et ne peuvent pas être non lues.

Si vous souhaitez transmettre une entrée à une commande dans le script, vous devez le faire explicitement, généralement avec un document ici . (Un document ici est généralement le plus pratique pour une entrée multiligne, mais n'importe quelle méthode fera l'affaire.) Le code que vous avez écrit ne fonctionne que dans quelques shells, uniquement si le script est passé en entrée au shell à partir d'un fichier normal; si vous vous attendiez à ce que le second whoamisoit passé en entrée à sudo …, détrompez-vous, en gardant à l'esprit que la plupart du temps le script n'est pas passé à l'entrée standard du shell.

#!/bin/bash
whoami
sudo su -l root <<'EOF'
whoami
EOF

Notez que cette décennie, vous pouvez utiliser sudo -i root. La course sudo suest un hack du passé.

Gilles 'SO- arrête d'être méchant'
la source