#! / bin / sh vs #! / bin / bash pour une portabilité maximale

36

Je travaille habituellement avec des serveurs Ubuntu LTS qui , de ce que je comprends symlink /bin/shà /bin/dash. Beaucoup d'autres distributions que symlink /bin/shto /bin/bash.

À partir de là, je comprends que si un script utilise #!/bin/shpar dessus, il ne fonctionnera peut-être pas de la même manière sur tous les serveurs?

Existe-t-il une pratique suggérée sur le shell à utiliser pour les scripts lorsque l’on veut une portabilité maximale de ces scripts entre les serveurs?

cherouvim
la source
Il existe de légères différences entre les différentes coquilles. Si la portabilité est le plus important pour vous, utilisez #!/bin/shet n'utilisez rien d'autre que ce que le shell d'origine a fourni.
Thorbjørn Ravn Andersen

Réponses:

65

Il existe environ quatre niveaux de portabilité pour les scripts shell (en ce qui concerne la ligne shebang):

  1. Plus portable: utilisez un #!/bin/shshebang et utilisez uniquement la syntaxe de base du shell spécifiée dans la norme POSIX . Cela devrait fonctionner à peu près sur n'importe quel système POSIX / unix / linux. (Eh bien, à l'exception de Solaris 10 et des versions antérieures, qui contenaient le véritable shell Bourne, précédant POSIX si non conforme, comme /bin/sh.)

  2. Deuxième plus portable: utilisez une ligne #!/bin/bash(ou #!/usr/bin/env bash) shebang et respectez les fonctionnalités de bash v3. Cela fonctionnera sur tous les systèmes dotés de bash (à l'emplacement prévu).

  3. Troisième plus portable: utilisez une ligne #!/bin/bash(ou #!/usr/bin/env bash) shebang, et utilisez les fonctionnalités de bash v4. Cela échouera sur tous les systèmes dotés de bash v3 (par exemple, macOS, qui doit l’utiliser pour des raisons de licence).

  4. Le moins portable: utilisez un #!/bin/shshebang et des extensions bash à la syntaxe du shell POSIX. Cela échouera sur tout système ayant autre chose que bash pour / bin / sh (comme les versions récentes d'Ubuntu). Ne fais jamais ça; ce n'est pas simplement un problème de compatibilité, c'est tout simplement faux. Malheureusement, c'est une erreur commise par beaucoup de gens.

Ma recommandation: utilisez le plus prudent des trois premiers qui fournit toutes les fonctionnalités du shell dont vous avez besoin pour le script. Pour une portabilité maximale, utilisez l'option n ° 1, mais d'après mon expérience, certaines fonctionnalités de bash (telles que les tableaux) sont suffisamment utiles pour que je passe à la n ° 2.

La pire chose que vous puissiez faire est la n ° 4, en utilisant le mauvais shebang. Si vous n'êtes pas sûr des fonctionnalités POSIX de base et des extensions bash, vous pouvez soit vous en tenir à un bash shebang (option n ° 2), soit tester le script de manière approfondie avec un shell très basique (comme dash sur vos serveurs Ubuntu LTS). Le wiki Ubuntu a une bonne liste de bashismes à surveiller .

Il y a quelques très bonnes informations sur l'histoire et les différences entre les shells dans la question Unix et Linux "Qu'est-ce que ça veut dire être compatible sh?" et la question Stackoverflow "Différence entre sh et bash" .

Sachez également que le shell n’est pas la seule chose qui diffère d’un système à l’autre; si vous êtes habitué à Linux, vous êtes habitué aux commandes GNU, qui ont beaucoup d'extensions non standard que vous pourriez ne pas trouver sur d'autres systèmes Unix (par exemple, bsd, macOS). Malheureusement, il n'y a pas de règle simple ici, il vous suffit de connaître la plage de variation des commandes que vous utilisez.

L' une des commandes les plus méchantes en termes de portabilité est l' un des plus élémentaires: echo. Chaque fois que vous l'utilisez avec des options (par exemple, echo -nou echo -e), ou avec des échappements (barres obliques inverses) dans la chaîne à imprimer, différentes versions feront des choses différentes. Chaque fois que vous souhaitez imprimer une chaîne sans saut de ligne après celle-ci, ou avec des échappements dans la chaîne, utilisez-la à la printfplace (et découvrez comment cela fonctionne - c'est plus compliqué que ce echon'est). La pscommande est aussi un désordre .

Les extensions récentes / GNUish de la syntaxe des options de commande constituent un autre point à surveiller: le format de commande ancien (standard) indique que la commande est suivie d'options (avec un tiret unique, chaque option étant une lettre), suivies de arguments de commande. Les variantes récentes (et souvent non portables) incluent des options longues (généralement introduites avec --), permettant aux options de suivre les arguments et --permettant de séparer les options des arguments.

Gordon Davisson
la source
12
La quatrième option est simplement une mauvaise idée. S'il-vous-plait, ne l'utilisez pas.
pabouk
3
@pabouk Je suis tout à fait d'accord, j'ai donc modifié ma réponse pour que cela soit plus clair.
Gordon Davisson
1
Votre première déclaration est légèrement trompeuse. La norme POSIX ne spécifie rien à propos du shebang en dehors de dire que l'utiliser conduit à un comportement non spécifié. De plus, POSIX ne spécifie pas où le shell posix doit être situé, seulement son nom ( sh), il /bin/shn’est donc pas garanti que ce soit le bon chemin. Le plus portable est alors de ne spécifier aucun shebang, ou d'adapter le shebang au système d'exploitation utilisé.
jlliagre
2
Je suis tombé dans le numéro 4 avec un de mes scripts très récemment et je ne pouvais pas comprendre pourquoi cela ne fonctionnait pas; après tout, les mêmes commandes ont fonctionné de manière solide et ont fait exactement ce que je voulais qu'elles fassent, lorsque je les ai essayées directement dans la coque. Dès que je l' ai changé #!/bin/shà #!/bin/bashbien, le script a parfaitement fonctionné. (Pour ma défense, ce script a évolué avec le temps, passant de celui qui nécessitait uniquement un sh-isme à un comportement qui s'appuyait sur un comportement semblable à celui de bash.)
CVn
2
@ MichaelKjörling Le (vrai) Bourne n'est presque jamais intégré aux distributions Linux et n'est pas compatible POSIX de toute façon. Le shell standard POSIX a été créé à partir d'un kshcomportement, pas de Bourne. La plupart des distributions Linux suivant la FHS ont /bin/shun lien symbolique avec le shell qu'elles sélectionnent pour assurer la compatibilité POSIX, généralement bashou dash.
jlliagre
5

Dans le ./configurescript qui prépare le langage TXR à la construction, j’ai écrit le prologue suivant pour une meilleure portabilité. Le script s’amorce même s’il #!/bin/shest un ancien Bourne Shell non conforme à POSIX. (Je construis chaque version sur une machine virtuelle Solaris 10).

#!/bin/sh

# use your own variable name instead of txr_shell;
# adjust to taste: search for your favorite shells

if test x$txr_shell = x ; then
  for shell in /bin/bash /usr/bin/bash /usr/xpg4/bin/sh ; do
    if test -x $shell ; then
       txr_shell=$shell
       break
    fi
  done
  if test x$txr_shell = x ; then
    echo "No known POSIX shell found: falling back on /bin/sh, which may not work"
    txr_shell=/bin/sh
  fi
  export txr_shell
  exec $txr_shell $0 ${@+"$@"}
fi

# rest of the script here, executing in upgraded shell

L'idée ici est que nous trouvions un meilleur shell que celui sous lequel nous fonctionnons et que nous ré-exécutions le script à l'aide de ce shell. La txr_shellvariable d'environnement est définie de sorte que le script ré-exécuté sache qu'il s'agit de l'instance récursive ré-exécutée.

(Dans mon script, la txr_shellvariable est également utilisé par la suite, pour exactement deux buts: d'une part , il est imprimé dans le cadre d'un message d' information dans la sortie du script En second lieu , il est installé comme. SHELLVariable dans le Makefile, de sorte que makeutilisera cette shell aussi pour exécuter des recettes.)

Sur un système où /bin/shest un tiret, vous pouvez voir que la logique ci-dessus trouvera /bin/bashet ré-exécutera le script avec cela.

Sur une boîte Solaris 10, le /usr/xpg4/bin/shsera activé si aucun Bash n’est trouvé.

Le prologue est écrit dans un dialecte de shell conservateur, utilisant testpour les tests d’existence de fichier et le ${@+"$@"}truc pour développer les arguments s’adressant à de vieux shell cassés (ce qui serait tout simplement le "$@"cas si nous étions dans un shell conforme à POSIX).

Kaz
la source
On n'aurait pas besoin de xpiratage informatique si on utilisait des citations appropriées, car les situations qui l' exigeaient englobaient des invocations de test maintenant obsolètes, -aou -ocombinant plusieurs tests.
Charles Duffy
@ CharlesDuffy Indeed; celui test x$whateverque je suis en train de commettre ressemble à un oignon dans le vernis. Si nous ne pouvons pas faire confiance à l'ancien shell cassé pour citer, alors la dernière ${@+"$@"}tentative est vaine.
Kaz
2

Toutes les variantes du langage Bourne shell sont objectivement terribles par rapport aux langages de script modernes tels que Perl, Python, Ruby, node.js et même (sans doute) Tcl. Si vous devez faire quelque chose, même un peu compliqué, vous serez plus heureux à long terme si vous utilisez l'un des éléments ci-dessus au lieu d'un script shell.

Le seul et unique avantage du langage shell sur ces nouveaux langages réside dans le fait qu’il est garanti que quelque chose qui s’appelle lui /bin/sh- même existe sur tout ce qui est censé être Unix. Cependant, il se peut que quelque chose ne soit même pas conforme à POSIX; la plupart des Unixes propriétaires hérités ont gelé le langage mis en œuvre par /bin/shet les utilitaires dans le PATH par défaut avant les modifications demandées par Unix95 (oui, Unix95, il y a vingt ans). Il peut y avoir un ensemble d'Unix95, ou même de POSIX.1-2001 si vous êtes chanceux, des outils dans un répertoire ne se trouvant pas sur le chemin PATH par défaut (par exemple /usr/xpg4/bin), mais leur existence n'est pas garantie.

Cependant, les bases de Perl sont plus susceptibles d'être présentes sur une installation Unix choisie arbitrairement que Bash. (Par "les bases de Perl", je veux dire /usr/bin/perlexiste et est une version peut-être assez ancienne de Perl 5, et si vous avez de la chance, les modules fournis avec cette version de l'interpréteur sont également disponibles.)

Par conséquent:

Si vous écrivez quelque chose qui doit fonctionner partout et qui est censé être Unix (comme un script "configure"), vous devez utiliser #! /bin/sh, et vous ne devez utiliser aucune extension. De nos jours, j'écrirais un shell conforme à POSIX.1-2001 dans ce cas, mais je serais prêt à corriger les erreurs POSIXisms si quelqu'un demandait de l'aide pour le fer rouillé.

Mais si vous n'écrivez pas quelque chose qui doit fonctionner partout, alors au moment où vous serez tenté d'utiliser un quelconque Bashisme, vous devriez arrêter et réécrire le tout dans un meilleur langage de script. Votre futur moi vous en sera reconnaissant.

(Alors, quand est- il approprié d'utiliser des extensions Bash? Au premier ordre: jamais. Au second ordre: uniquement pour étendre l'environnement interactif Bash - par exemple pour fournir des commandes intelligentes pour compléter les onglets et fantaisie.)

zwol
la source
L'autre jour, j'ai dû écrire un script qui faisait quelque chose comme find ... -print0 |while IFS="" read -rd "" file; do ...; done |tar c --null -T -. Il ne serait pas possible de le faire fonctionner correctement sans les bashismes ( read -d "") et les extensions GNU ( find -print0, tar --null). Réimplémenter cette ligne dans Perl serait beaucoup plus long et plus maladroit. C'est le genre de situation lorsque vous utilisez des bashismes et d'autres extensions non-POSIX, c'est la bonne chose à faire.
Michu
@ michau Cela peut ne plus être considéré comme une ligne, en fonction de ce qui se passe dans ces ellipses, mais je pense que vous ne prenez pas la "réécriture de la chose dans un meilleur langage de script" aussi littéralement que je le pensais. Mon perl -MFile::Find -e 'our @tarcmd = ("tar", "c"); find(sub { return unless ...; ...; push @tarcmd, $File::Find::name }, "."); exec @tarcmdstyle ressemble à quelque chose comme: Notez que non seulement vous n’avez besoin d’aucun basisme, vous n’avez besoin d’aucune extension GNU tar. (Si la longueur de la ligne de commande vous préoccupe ou si vous souhaitez que l'analyse et les tar --null -T
fichiers tar soient
Le problème est que findplus de 50 000 noms de fichiers ont été renvoyés dans mon scénario. Votre script atteindra donc probablement la limite de longueur d'argument. Une implémentation correcte qui n'utilise pas d'extensions non-POSIX devrait générer la sortie tar de manière incrémentielle, ou peut-être utiliser Archive :: Tar :: Stream dans le code Perl. Bien sûr, cela peut être fait, mais dans ce cas, le script Bash est beaucoup plus rapide à écrire et a moins de passe-partout, et donc moins de place pour les bugs.
Michaël
BTW, je pourrais utiliser Perl au lieu de Bash de façon rapide et concise: find ... -print0 |perl -0ne '... print ...' |tar c --null -T -. Mais les questions qui se posent sont les suivantes: 1) j’utilise find -print0et de tar --nulltoute façon, alors quel est le point d’éviter le bashisme read -d "", qui est exactement le même genre de chose (une extension GNU pour gérer le séparateur nul qui, contrairement à Perl, peut devenir un jour POSIX )? 2) Perl est-il vraiment plus répandu que Bash? J'ai utilisé des images Docker récemment, et beaucoup d'entre elles ont Bash mais pas de Perl. Il est plus probable que de nouveaux scripts soient exécutés ici que sur les anciens Unices.
Michaël