$ 0 inclura-t-il toujours le chemin du script?

11

Je veux grep le script actuel afin que je puisse imprimer les informations d'aide et de version à partir de la section des commentaires en haut.

Je pensais à quelque chose comme ça:

grep '^#h ' -- "$0" | sed -e 's/#h //'

Mais ensuite je me suis demandé ce qui se passerait si le script se trouvait dans un répertoire qui était dans PATH et appelé sans spécifier explicitement le répertoire.

J'ai cherché une explication des variables spéciales et j'ai trouvé les descriptions suivantes de $0:

  • nom du shell ou du programme en cours

  • nom de fichier du script actuel

  • nom du script lui-même

  • commande telle qu'elle a été exécutée

Aucun de ceux-ci ne précise si la valeur de $0inclurait ou non le répertoire si le script était invoqué sans lui. Le dernier m'implique en fait que non.

Test sur mon système (Bash 4.1)

J'ai créé un fichier exécutable dans / usr / local / bin appelé scriptname avec une ligne echo $0et l' ai invoqué à partir de différents emplacements.

Voici mes résultats:

> cd /usr/local/bin/test
> ../scriptname
../scriptname

> cd /usr/local/bin
> ./scriptname
./scriptname

> cd /usr/local
> bin/scriptname
bin/scriptname

> cd /tmp
> /usr/local/bin/scriptname
/usr/local/bin/scriptname

> scriptname
/usr/local/bin/scriptname

Dans ces tests, la valeur de $0est toujours exactement la façon dont le script a été appelé, sauf s'il est appelé sans aucun composant de chemin. Dans ce cas, la valeur de $0est le chemin absolu . Il semble donc qu'il serait prudent de passer à une autre commande.

Mais je suis tombé sur un commentaire sur Stack Overflow qui m'a dérouté. La réponse suggère d'utiliser $(dirname $0)pour obtenir le répertoire du script actuel. Le commentaire (voté 7 fois) dit "cela ne fonctionnera pas si le script est sur votre chemin".

Des questions

  • Ce commentaire est-il correct?
  • Le comportement est-il différent sur d'autres systèmes?
  • Y a-t-il des situations où $0le répertoire ne serait pas inclus?
toxalote
la source
Les gens ont répondu à propos de situations où $0il y a autre chose que le script, qui répond au titre de la question. Cependant, je suis également intéressé par les situations où se $0trouve le script lui-même, mais n'inclut pas le répertoire. En particulier, j'essaie de comprendre le commentaire fait sur la réponse SO.
toxalot

Réponses:

17

Dans les cas les plus courants, $0contiendra un chemin, absolu ou relatif au script, donc

script_path=$(readlink -e -- "$0")

(en supposant qu'il existe une readlinkcommande et qu'elle prend en charge -e) est généralement un assez bon moyen d'obtenir le chemin absolu canonique du script.

$0 est attribué à partir de l'argument spécifiant le script tel qu'il a été transmis à l'interpréteur.

Par exemple, dans:

the-shell -shell-options the/script its args

$0obtient the/script.

Lorsque vous exécutez:

the/script its args

Votre shell fera:

exec("the/script", ["the/script", "its", "args"])

Si le script contient un #! /bin/sh -she-bang par exemple, le système le transformera en:

exec("/bin/sh", ["/bin/sh" or "the/script", "-", "the/script", "its", "args"])

(si elle ne contient pas de coup sec, ou plus généralement si le système retourne une erreur ENOEXEC, alors c'est votre shell qui fera la même chose)

Il existe une exception pour les scripts setuid / setgid sur certains systèmes, où le système ouvrira le script sur certains fd xet s'exécutera à la place:

exec("/bin/sh", ["/bin/sh" or "the/script", "-", "/dev/fd/x", "its", "args"])

pour éviter les conditions de course (auquel cas $0contiendra /dev/fd/x).

Maintenant, vous pouvez dire que /dev/fd/x c'est un chemin vers ce script. Notez cependant que si vous lisez $0, vous romprez le script lorsque vous consommerez l'entrée.

Maintenant, il y a une différence si le nom de la commande de script invoqué ne contient pas de barre oblique. Dans:

the-script its args

Votre shell va chercher the-scriptdans $PATH. $PATHpeut contenir des chemins absolus ou relatifs (y compris la chaîne vide) vers certains répertoires. Par exemple, si $PATHcontient /bin:/usr/bin:et the-scriptse trouve dans le répertoire courant, le shell fera:

exec("the-script", ["the-script", "its", "args"])

qui deviendra:

exec("/bin/sh", ["/bin/sh" or "the-script", "-", "the-script", "its", "args"]

Ou s'il se trouve dans /usr/bin:

exec("/usr/bin/the-script", ["the-script", "its", "args"])
exec("/bin/sh", ["/bin/sh" or "the-script" or "/usr/bin/the-script",
     "-", "/usr/bin/the-script", "its", "args")

Dans tous les cas ci-dessus, à l'exception du cas du coin setuid, $0contiendra un chemin (absolu ou relatif) vers le script.

Maintenant, un script peut également être appelé comme:

the-interpreter the-script its args

Lorsque, the-scriptcomme ci-dessus, ne contient pas de barres obliques, le comportement varie légèrement d'un shell à l'autre.

Les anciennes kshimplémentations AT&T recherchaient en fait le script sans condition $PATH(ce qui était en fait un bogue et une faille de sécurité pour les scripts setuid), donc $0ne contenaient en fait pas de chemin vers le script à moins que la $PATHrecherche ne se trouve réellement the-scriptdans le répertoire actuel.

Un AT&T plus récent kshessaierait d'interpréter the-scriptdans le répertoire courant s'il est lisible. Sinon, il rechercherait un fichier lisible et exécutablethe-script dans $PATH.

Pour bash, il vérifie s'il se the-scripttrouve dans le répertoire courant (et n'est pas un lien symbolique cassé) et sinon, recherche un fichier lisible (pas nécessairement exécutable) the-scriptdans $PATH.

zshen shémulation ferait comme , bashsauf que si the-scriptest un lien symbolique cassé dans le répertoire courant, il ne serait pas la recherche d'un the-scriptdans $PATHet serait plutôt une erreur.

Tous les autres Bourne comme des obus ne regardent pas the-scriptdans $PATH.

Pour tous ces shells de toute façon, si vous trouvez qu'il $0ne contient pas de /et n'est pas lisible, alors il a probablement été recherché $PATH. Ensuite, comme les fichiers $PATHsont susceptibles d'être exécutables, c'est probablement une approximation sûre à utiliser command -v -- "$0"pour trouver son chemin (bien que cela ne fonctionnerait pas s'il $0se trouve que c'est aussi le nom d'un shell intégré ou d'un mot clé (dans la plupart des shells)).

Donc, si vous voulez vraiment couvrir ce cas, vous pouvez l'écrire:

progname=$0
[ -r "$progname" ] || progname=$(
    IFS=:; set -f
    for i in ${PATH-$(getconf PATH)}""; do
      case $i in
        "") p=$progname;;
        */) p=$i$progname;;
        *) p=$i/$progname
      esac
      [ -r "$p" ] && exec printf '%s\n' "$p"
    done
    exit 1
  ) && progname=$(readlink -e -- "$progname") ||
  progname=unknown

(le ""annexé à $PATHest de conserver un élément vide de fin avec des coques dont $IFSle délimiteur au lieu de séparateur agit ).

Maintenant, il existe des moyens plus ésotériques d'invoquer un script. On pourrait faire:

the-shell < the-script

Ou:

cat the-script | the-shell

Dans ce cas, $0sera le premier argument ( argv[0]) que l' interprète a reçu (ci the-shell- dessus , mais cela peut être n'importe quoi, mais généralement le nom de base ou un chemin d'accès à cet interprète).

Détecter que vous vous trouvez dans cette situation en fonction de la valeur de $0n'est pas fiable. Vous pouvez regarder la sortie de ps -o args= -p "$$"pour obtenir un indice. Dans le cas du tuyau, il n'y a aucun moyen réel de revenir à un chemin d'accès au script.

On pourrait aussi faire:

the-shell -c '. the-script' blah blih

Ensuite, sauf dans zsh(et une ancienne implémentation du shell Bourne), ce $0serait blah. Encore une fois, difficile d'accéder au chemin du script dans ces coquilles.

Ou:

the-shell -c "$(cat the-script)" blah blih

etc.

Pour vous assurer que vous avez le droit $progname, vous pouvez rechercher une chaîne spécifique comme:

progname=$0
[ -r "$progname" ] || progname=$(
    IFS=:; set -f
    for i in ${PATH-$(getconf PATH)}:; do
      case $i in
        "") p=$progname;;
        */) p=$i$progname;;
        *) p=$i/$progname
      esac
      [ -r "$p" ] && exec printf '%s\n' "$p"
    done
    exit 1
  ) && progname=$(readlink -e -- "$progname") ||
  progname=unknown

[ -f "$progname" ] && grep -q 7YQLVVD3UIUDTA32LSE8U9UOHH < "$progname" ||
  progname=unknown

Mais encore une fois, je ne pense pas que cela en vaille la peine.

Stéphane Chazelas
la source
Stéphane, je ne comprends pas votre utilisation "-"dans les exemples ci-dessus. D'après mon expérience, exec("the-script", ["the-script", "its", "args"])devient exec("/the/interpreter", ["/the/interpreter", "the-script", "its", "args"]), avec bien sûr la possibilité d'une option interprète.
jrw32982 prend en charge Monica
@ jrw32982, #! /bin/sh -est le "toujours utiliser cmd -- somethingsi vous ne pouvez pas garantir que somethingcela ne commencera pas par -" l' adage de bonne pratique appliqué ici /bin/sh(où -le marqueur de fin d'option est plus portable que --) avec somethingétant le chemin / nom du scénario. Si vous ne l'utilisez pas pour les scripts setuid (sur les systèmes qui les prennent en charge mais pas avec la méthode / dev / fd / x mentionnée dans la réponse), alors on peut obtenir un shell racine en créant un lien symbolique vers votre script appelé -iou -spour exemple.
Stéphane Chazelas du
Merci, Stéphane. J'avais manqué le trait d'union de fin dans votre exemple de ligne de shebang. J'ai dû chercher où il est documenté qu'un seul trait d'union équivaut à un double trait d'union pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html .
jrw32982 prend en charge Monica
Il est trop facile d'oublier le tiret simple / double de fin dans la ligne shebang pour un script setuid; d'une manière ou d'une autre, le système devrait s'en occuper pour vous. Interdire complètement les scripts setuid ou d'une manière ou d'une autre /bin/shdevrait désactiver son propre traitement d'options s'il peut détecter qu'il exécute setuid. Je ne vois pas comment l'utilisation de / dev / fd / x en soi résout ce problème. Vous avez toujours besoin du tiret simple / double, je pense.
jrw32982 prend en charge Monica
@ jrw32982, /dev/fd/xcommence par /, non -. Le but principal est de supprimer la condition de concurrence entre les deux execve()si (entre le execve("the-script")qui élève les privilèges et le suivant execve("interpreter", "thescript")interpreterouvre le script plus tard (qui peut très bien avoir été remplacé par un lien symbolique vers quelque chose d'autre entre-temps). les scripts suid font correctement une execve("interpreter", "/dev/fd/n")place où n a été ouvert dans le cadre du premier execve ().
Stéphane Chazelas
6

Voici deux situations où le répertoire ne serait pas inclus:

> bash scriptname
scriptname

> bash <scriptname
bash

Dans les deux cas, le répertoire actuel devrait être le répertoire où se trouvait le nom du script .

Dans le premier cas, la valeur de $0pourrait toujours être transmise grepcar elle suppose que l'argument FILE est relatif au répertoire en cours.

Dans le second cas, si les informations d'aide et de version ne sont imprimées qu'en réponse à une option de ligne de commande spécifique, cela ne devrait pas poser de problème. Je ne sais pas pourquoi quelqu'un invoquerait un script de cette façon pour imprimer l'aide ou la version.

Avertissements

  • Si le script modifie le répertoire actuel, vous ne voudriez pas utiliser de chemins relatifs.

  • Si le script provient, la valeur de $0sera généralement le script appelant plutôt que le script source.

toxalote
la source
3

Un argument arbitraire zéro peut être spécifié lors de l'utilisation de l' -coption pour la plupart (tous?) Des shells. Par exemple:

sh -c 'echo $0' argv0

De man bash(choisi uniquement parce que cela a une meilleure description que mon man sh- l'utilisation est la même indépendamment):

-c

Si l'option -c est présente, les commandes sont lues à partir du premier argument non-option chaîne_commande. S'il y a des arguments après la chaîne de commande, ils sont affectés aux paramètres positionnels, en commençant par $ 0.

Graeme
la source
Je pense que cela fonctionne parce que -c 'commande' sont des opérandes, et argv0c'est son premier argument de ligne de commande non opérande .
mikeserv
@mike correct, j'ai mis à jour avec un extrait d'homme.
Graeme
3

REMARQUE: d' autres ont déjà expliqué la mécanique de $0donc je vais sauter tout cela.

En général, je contourne ce problème et j'utilise simplement la commande readlink -f $0. Cela vous donnera toujours le chemin complet de tout ce que vous lui donnez comme argument.

Exemples

Disons que je suis ici pour commencer:

$ pwd
/home/saml/tst/119929/adir

Créez un répertoire + fichier:

$ mkdir adir
$ touch afile
$ cd adir/

Maintenant, commencez à vous montrer readlink:

$ readlink -f ../adir
/home/saml/tst/119929/adir

$ readlink -f ../
/home/saml/tst/119929

$ readlink -f ../afile 
/home/saml/tst/119929/afile

$ readlink -f .
/home/saml/tst/119929/adir

Ruse supplémentaire

Maintenant, avec un résultat cohérent renvoyé lorsque nous interrogeons $0via, readlinknous pouvons utiliser simplement dirname $(readlink -f $0)pour obtenir le chemin absolu vers le script -ou- basename $(readlink -f $0)pour obtenir le nom réel du script.

slm
la source
0

Ma manpage dit:

$0: Se développe dans le namede shellou shell script.

Il semble que cela se traduise par argv[0]le shell actuel - ou le premier argument de ligne de commande non opérande auquel le shell en cours d'interprétation est alimenté lorsqu'il est appelé. J'ai déjà indiqué que sh ./somescriptcela acheminerait sa variable vers mais c'était incorrect car c'est un nouveau processus qui lui est propre et invoqué avec un nouveau .$0 $ENV shshell$ENV

De cette façon, sh ./somescript.shdiffère de celui . ./somescript.shqui s'exécute dans l' environnement actuel et $0est déjà défini.

Vous pouvez vérifier cela en comparant $0à /proc/$$/status.

echo 'script="/proc/$$/status"
    echo $0
    cat "$script"' \
    > ./script.sh
sh ./script.sh ; . ./script.sh

Merci pour la correction, @toxalot. J'ai appris quelque chose.

mikeserv
la source
Sur mon système, s'il est originaire ( . ./myscript.shou source ./myscript.sh), $0c'est le shell. Mais s'il est passé en argument au shell ( sh ./myscript.sh), alors $0c'est le chemin du script. Bien sûr, shsur mon système est Bash. Je ne sais donc pas si cela fait une différence ou non.
toxalot
At-il un hashbang? Je pense que la différence est importante - et je peux ajouter que - sans elle, execelle ne devrait pas et au lieu de cela, mais avec elle, elle devrait exec.
mikeserv
Avec ou sans hashbang, j'obtiens les mêmes résultats. Avec ou sans être exécutable, j'obtiens les mêmes résultats.
toxalot
Moi aussi! Je pense que c'est parce que c'est un shell intégré. Je vérifie ...
mikeserv
Les lignes de coup sont interprétées par le noyau. Si vous ./somescriptexécutez avec la bangline #!/bin/sh, cela équivaudrait à courir /bin/sh ./somescript. Sinon, ils ne font aucune différence pour le shell.
Graeme
0

Je veux grep le script actuel afin que je puisse imprimer les informations d'aide et de version à partir de la section des commentaires en haut.

Bien qu'il $0contienne le nom du script, il peut contenir un chemin préfixé basé sur la façon dont le script est appelé, j'ai toujours utilisé ${0##*/}pour imprimer le nom du script dans la sortie de l'aide qui supprime tout chemin principal $0.

Tiré du guide Advanced Bash Scripting - Section 10.2 Substitution de paramètres

${var#Pattern}

Retirez de $varla partie la plus courte $Patternqui correspond à l'extrémité avant de $var.

${var##Pattern}

Retirez de $varla partie la plus longue $Patternqui correspond à l'extrémité avant de $var.

Ainsi, la partie la plus longue de $0ces correspondances */sera le préfixe de chemin entier, ne renvoyant que le nom du script.

sambler
la source
Oui, je le fais aussi pour le nom du script utilisé dans le message d'aide. Mais je parle d' accueillir le script, j'ai donc besoin d'avoir au moins un chemin relatif. Je veux mettre le message d'aide en haut dans les commentaires et ne pas avoir à répéter le message d'aide plus tard lors de l'impression.
toxalot
0

Pour quelque chose de similaire, j'utilise:

rPath="$(dirname $(realpath $0))"
echo $rPath 

rPath=$(dirname $(readlink -e -- "$0"))
echo $rPath 

rPath a toujours la même valeur.

Anonimous
la source