Arguments multiples dans shebang

33

Je me demande s'il existe un moyen général de passer plusieurs options à un exécutable via la ligne shebang ( #!).

J'utilise NixOS, et la première partie du shebang dans n'importe quel script que j'écris est généralement /usr/bin/env. Le problème que je rencontre alors est que tout ce qui vient après est interprété comme un seul fichier ou répertoire par le système.

Supposons, par exemple, que je veuille écrire un script à exécuter bashen mode posix. La manière naïve d'écrire le shebang serait:

#!/usr/bin/env bash --posix

mais essayer d'exécuter le script résultant produit l'erreur suivante:

/usr/bin/env: ‘bash --posix’: No such file or directory

Je connais ce post , mais je me demandais s'il y avait une solution plus générale et plus propre.


EDIT : Je sais que pour les scripts Guile , il existe un moyen d'atteindre ce que je veux, documenté dans la section 4.3.4 du manuel:

 #!/usr/bin/env sh
 exec guile -l fact -e '(@ (fac) main)' -s "$0" "$@"
 !#

L'astuce, ici, est que la deuxième ligne (commençant par exec) est interprétée comme du code par shmais, étant dans le bloc #!... !#, comme un commentaire, et donc ignorée, par l'interpréteur Guile.

Ne serait-il pas possible de généraliser cette méthode à n'importe quel interprète?


Second EDIT : Après avoir joué un peu, il semble que, pour les interprètes qui peuvent lire leur entrée stdin, la méthode suivante fonctionnerait:

#!/usr/bin/env sh
sed '1,2d' "$0" | bash --verbose --posix /dev/stdin; exit;

Ce n'est probablement pas optimal, cependant, car le shprocessus se poursuit jusqu'à ce que l'interprète ait terminé son travail. Toute rétroaction ou suggestion serait appréciée.

Rastapopoulos
la source

Réponses:

27

Il n'y a pas de solution générale, du moins pas si vous devez prendre en charge Linux, car le noyau Linux traite tout ce qui suit le premier «mot» de la ligne shebang comme un seul argument .

Je ne sais pas quelles sont les contraintes de NixOS, mais en général, j'écrirais simplement votre shebang comme

#!/bin/bash --posix

ou, si possible, définissez les options dans le script :

set -o posix

Alternativement, vous pouvez faire redémarrer le script lui-même avec l'invocation de shell appropriée:

#!/bin/sh -

if [ "$1" != "--really" ]; then exec bash --posix -- "$0" --really "$@"; fi

shift

# Processing continues

Cette approche peut être généralisée à d'autres langues, à condition que vous trouviez un moyen pour les deux premières lignes (qui sont interprétées par le shell) d'être ignorées par la langue cible.

GNU coreutils' envfournit une solution de contournement depuis la version 8.30, voir la réponse de unode pour plus de détails. (Ceci est disponible dans Debian 10 et versions ultérieures, RHEL 8 et versions ultérieures, Ubuntu 19.04 et versions ultérieures, etc.)

Stephen Kitt
la source
18

Bien que pas exactement portable, à partir de coreutils 8.30 et selon sa documentation, vous pourrez utiliser:

#!/usr/bin/env -S command arg1 arg2 ...

Donc donné:

$ cat test.sh
#!/usr/bin/env -S showargs here 'is another' long arg -e "this and that " too

tu auras:

% ./test.sh 
$0 is '/usr/local/bin/showargs'
$1 is 'here'
$2 is 'is another'
$3 is 'long'
$4 is 'arg'
$5 is '-e'
$6 is 'this and that '
$7 is 'too'
$8 is './test.sh'

et si vous êtes curieux, showargsc'est:

#!/usr/bin/env sh
echo "\$0 is '$0'"

i=1
for arg in "$@"; do
    echo "\$$i is '$arg'"
    i=$((i+1))
done
unode
la source
C'est très bon à savoir pour référence future.
John McGehee
Cette option a été copiée depuis FreeBSD envoù a -Sété ajoutée en 2005. Voir lists.gnu.org/r/coreutils/2018-04/msg00011.html
Stéphane Chazelas
Fonctionne un régal sur Fedora 29
Eric
@unode quelques améliorations de showargs: pastebin.com/q9m6xr8H et pastebin.com/gS8AQ5WA (one-liner)
Eric
Pour info: à partir de coreutils 8.31, envinclut la sienne showargs: l'option -v par exemple#!/usr/bin/env -vS --option1 --option2 ...
chocolateboy
9

La norme POSIX est très concise pour décrire #!:

De la section justification de la documentation de la exec()famille des interfaces système :

Certaines implémentations historiques gèrent les scripts shell en reconnaissant les deux premiers octets du fichier comme chaîne de caractères #!et en utilisant le reste de la première ligne du fichier comme nom de l'interpréteur de commandes à exécuter.

Dans la section Introduction au shell :

Le shell lit son entrée depuis un fichier (voir sh), depuis l' -coption ou depuis les fonctions system()et popen()définies dans le volume System Interfaces de POSIX.1-2008. Si la première ligne d'un fichier de commandes shell commence par les caractères #!, les résultats ne sont pas spécifiés .

Cela signifie essentiellement que toute implémentation (l'Unix que vous utilisez) est libre de faire les spécificités de l'analyse de la ligne shebang comme elle le souhaite.

Certains Unices, comme macOS (ne peut pas tester ATM), diviseront les arguments donnés à l'interpréteur sur la ligne shebang en arguments séparés, tandis que Linux et la plupart des autres Unices donneront les arguments comme une seule option à l'interpréteur.

Il n'est donc pas judicieux de compter sur la ligne de shebang pour pouvoir prendre plus d'un seul argument.

Voir également la section Portabilité de l'article de Shebang sur Wikipédia .


Une solution simple, généralisable à tout utilitaire ou langage, consiste à créer un script wrapper qui exécute le script réel avec les arguments de ligne de commande appropriés:

#!/bin/sh
exec /bin/bash --posix /some/path/realscript "$@"

Je ne pense pas que je voudrais essayer personnellement de le faire re-exécuter lui - même comme cela se sent un peu fragile.

Kusalananda
la source
7

Le shebang est décrit dans la page de manuel execve(2) comme suit:

#! interpreter [optional-arg]

Deux espaces sont acceptés dans cette syntaxe:

  1. Un espace avant le chemin de l' interpréteur , mais cet espace est facultatif.
  2. Un espace séparant le chemin de l' interpréteur et son argument facultatif.

Notez que je n'ai pas utilisé le pluriel en parlant d'un argument facultatif, la syntaxe ci-dessus non plus [optional-arg ...], car vous pouvez fournir au plus un seul argument .

En ce qui concerne les scripts shell, vous pouvez utiliser la setcommande intégrée près du début de votre script qui permettra de définir les paramètres des interprètes, fournissant le même résultat que si vous utilisiez des arguments de ligne de commande.

Dans ton cas:

set -o posix

À partir d'une invite Bash, vérifiez la sortie de help setpour obtenir toutes les options disponibles.

WhiteWinterWolf
la source
1
Vous êtes autorisé à avoir plus de deux espaces, ils sont simplement considérés comme faisant partie de l'argument facultatif.
Stephen Kitt
@StephenKitt: En effet, l'espace blanc ici doit être considéré plus comme une catégorie que le caractère d'espace réel. Je suppose que d'autres espaces blancs tels que les tabulations devraient également être largement acceptés.
WhiteWinterWolf
3

Sous Linux, le shebang n'est pas très flexible; selon plusieurs réponses (réponse de Stephen Kitt et Jörg W Mittag ), il n'y a pas de moyen désigné pour passer plusieurs arguments dans une ligne de shebang.

Je ne sais pas si cela sera utile à personne, mais j'ai écrit un court script pour implémenter la fonctionnalité manquante. Voir https://gist.github.com/loxaxs/7cbe84aed1c38cf18f70d8427bed1efa .

Il est également possible d'écrire des solutions de contournement intégrées. Ci-dessous, je présente quatre solutions de contournement indépendantes de la langue appliquées au même script de test et le résultat de chaque impression. Je suppose que le script est exécutable et est dedans /tmp/shebang.


Envelopper votre script dans un hérédoc bash à l'intérieur de la substitution de processus

Pour autant que je sache, c'est la façon la plus fiable de le faire indépendamment du langage. Il permet de passer des arguments et préserve stdin. L'inconvénient est que l'interpréteur ne connaît pas l'emplacement (réel) du fichier qu'il lit.

#!/bin/bash
exec python3 -O <(cat << 'EOWRAPPER'
print("PYTHON_SCRIPT_BEGINNING")

from sys import argv
try:
    print("input() 0 ::", input())
    print("input() 1 ::", input())
except EOFError:
    print("input() caused EOFError")
print("argv[0]   ::", argv[0])
print("argv[1:]  ::", argv[1:])
print("__debug__ ::", __debug__)
# The -O option changes __debug__ to False

print("PYTHON_SCRIPT_END")
EOWRAPPER
) "$@"

echo -e 'aa\nbb' | /tmp/shebang 'arg1' 'arg2 contains spaces' 'arg3\ uses\ \\escapes\\'Impressions d' appel :

PYTHON_SCRIPT_BEGINNING
input() 0 :: aa
input() 1 :: bb
argv[0]   :: /dev/fd/62
argv[1:]  :: ['arg1', 'arg2 contains spaces', 'arg3\\ uses\\ \\\\escapes\\\\']
__debug__ :: False
PYTHON_SCRIPT_END

Notez que la substitution de processus produit un fichier spécial. Cela peut ne pas convenir à tous les exécutables. Par exemple, se #!/usr/bin/lessplaint:/dev/fd/63 is not a regular file (use -f to see it)

Je ne sais pas s'il est possible d'avoir heredoc inside substitution de processus dans le tiret.


Envelopper votre script dans un hérédoc simple

Plus court et plus simple, mais vous ne pourrez pas accéder à stdinpartir de votre script et cela nécessite que l'interpréteur puisse lire et exécuter un script à partir de stdin.

#!/bin/sh
exec python3 - "$@" << 'EOWRAPPER'
print("PYTHON_SCRIPT_BEGINNING")

from sys import argv

try:
    print("input() 0 ::", input())
    print("input() 1 ::", input())
except EOFError:
    print("input() caused EOFError")
print("argv[0]   ::", argv[0])
print("argv[1:]  ::", argv[1:])
print("__debug__ ::", __debug__)
# The -O option changes __debug__ to False

print("PYTHON_SCRIPT_END")
EOWRAPPER

echo -e 'aa\nbb' | /tmp/shebang 'arg1' 'arg2 contains spaces' 'arg3\ uses\ \\escapes\\'Impressions d' appel :

PYTHON_SCRIPT_BEGINNING
input() caused EOFError
argv[0]   :: -
argv[1:]  :: ['arg1', 'arg2 contains spaces', 'arg3\\ uses\\ \\\\escapes\\\\']
__debug__ :: True
PYTHON_SCRIPT_END

Utiliser un system()appel awk mais sans arguments

Transmet correctement le nom du fichier exécuté, mais votre script ne recevra pas les arguments que vous lui donnez. Notez que awk est la seule langue que je connaisse dont l'interprète est installé sur linux par défaut et lit ses instructions depuis la ligne de commande par défaut.

#!/usr/bin/gawk BEGIN {system("python3 -O " ARGV[1])}
print("PYTHON_SCRIPT_BEGINNING")

from sys import argv

print("input() 0 ::", input())
print("input() 1 ::", input())
print("argv[0]   ::", argv[0])
print("argv[1:]  ::", argv[1:])
print("__debug__ ::", __debug__)
# The -O option changes __debug__ to False

print("PYTHON_SCRIPT_END")

echo -e 'aa\nbb' | /tmp/shebang 'arg1' 'arg2 contains spaces' 'arg3\ uses\ \\escapes\\'Impressions d' appel :

PYTHON_SCRIPT_BEGINNING
input() 0 :: aa
input() 1 :: bb
argv[0]   :: /tmp/shebang
argv[1:]  :: []
__debug__ :: False
PYTHON_SCRIPT_END

Utilisez l' system()appel awk 4.1+ , à condition que vos arguments ne contiennent pas d'espaces

Bien, mais seulement si vous êtes sûr que votre script ne sera pas appelé avec des arguments contenant des espaces. Comme vous pouvez le voir, vos arguments contenant des espaces seraient divisés, à moins que les espaces ne soient échappés.

#!/usr/bin/gawk @include "join"; BEGIN {system("python3 -O " join(ARGV, 1, ARGC, " "))}
print("PYTHON_SCRIPT_BEGINNING")

from sys import argv

print("input() 0 ::", input())
print("input() 1 ::", input())
print("argv[0]   ::", argv[0])
print("argv[1:]  ::", argv[1:])
print("__debug__ ::", __debug__)
# The -O option changes __debug__ to False

print("PYTHON_SCRIPT_END")

echo -e 'aa\nbb' | /tmp/shebang 'arg1' 'arg2 contains spaces' 'arg3\ uses\ \\escapes\\'Impressions d' appel :

PYTHON_SCRIPT_BEGINNING
input() 0 :: aa
input() 1 :: bb
argv[0]   :: /tmp/shebang
argv[1:]  :: ['arg1', 'arg2', 'contains', 'spaces', 'arg3 uses \\escapes\\']
__debug__ :: False
PYTHON_SCRIPT_END

Pour les versions awk inférieures à 4.1, vous devrez utiliser la concaténation de chaînes à l'intérieur d'une boucle for, voir l'exemple de fonction https://www.gnu.org/software/gawk/manual/html_node/Join-Function.html .

loxaxs
la source
1
Citez le terminateur de document ici pour inhiber $variableou `command`remplacer:exec python3 -O <(cat <<'EOWRAPPER'
John McGehee
2

Une astuce à utiliser LD_LIBRARY_PATHavec python sur la ligne #!(shebang) qui ne dépend pas d'autre chose que du shell et qui fonctionne comme un régal:

#!/bin/sh
'''' 2>/dev/null; exec /usr/bin/env LD_LIBRARY_PATH=. python -x "$0" "$@" #'''

__doc__ = 'A great module docstring'

Comme expliqué ailleurs dans cette page, certains shells comme shpeuvent prendre un script sur leur entrée standard.

Le script que nous donnons shessaie d'exécuter la commande ''''qui est simplifiée en ''(la chaîne vide) par shet bien sûr il ne parvient pas à l'exécuter car il n'y a pas de ''commande, donc il sort normalement line 2: command not foundsur le descripteur d'erreur standard mais nous redirigeons ce message en utilisant 2>/dev/nullle trou noir le plus proche car ce serait désordonné et déroutant pour l'utilisateur de le laisser shafficher.

Nous passons ensuite à la commande qui nous intéresse: execqui remplace le processus shell actuel par ce qui suit, dans notre cas: /usr/bin/env pythonavec les paramètres adéquats:

  • "$0" pour faire savoir à python quel script il doit ouvrir et interpréter, et aussi définir sys.argv[0]
  • "$@"pour définir python sys.argv[1:]aux arguments passés sur la ligne de commande de script.

Et nous demandons également envde définir la LD_LIBRARY_PATHvariable d'environnement, qui est le seul point du hack.

La commande shell se termine au commentaire commençant par #afin que le shell ignore les guillemets de fin '''.

shest ensuite remplacé par une nouvelle instance brillante de l'interpréteur python qui ouvre et lit le script source python donné comme premier argument (le "$0").

Python ouvre le fichier et saute la 1ère ligne de la source grâce à l' -xargument. Remarque: cela fonctionne également sans -xcar pour Python un shebang n'est qu'un commentaire .

Python interprète ensuite la 2e ligne comme la docstring pour le fichier de module actuel, donc si vous avez besoin d'une docstring de module valide, définissez simplement la __doc__première chose dans votre programme python comme dans l'exemple ci-dessus.

Eric
la source
Étant donné qu'une chaîne vide est… euh… vide, vous devriez être en mesure de supprimer votre commande singkey business: ''''exec ...devrait faire le travail. Notez qu'il n'y a pas d'espace avant exec ou cela fera chercher la commande vide. Vous voulez épisser le vide sur le premier argument pour qu'il en $0soit ainsi exec.
Caleb
1

J'ai trouvé une solution plutôt stupide en cherchant un exécutable qui exclut un script comme argument unique:

#!/usr/bin/awk BEGIN{system("bash --posix "ARGV[1])}
jusqu'à
la source