Enthält $ 0 immer den Pfad zum Skript?

11

Ich möchte das aktuelle Skript durchsuchen, damit ich Hilfe- und Versionsinformationen aus dem Kommentarbereich oben ausdrucken kann.

Ich dachte an so etwas:

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

Aber dann habe ich mich gefragt, was passieren würde, wenn sich das Skript in einem Verzeichnis befindet, das sich in PATH befindet und ohne explizite Angabe des Verzeichnisses aufgerufen wird.

Ich suchte nach einer Erklärung der speziellen Variablen und fand die folgenden Beschreibungen von $0:

  • Name der aktuellen Shell oder des aktuellen Programms

  • Dateiname des aktuellen Skripts

  • Name des Skripts selbst

  • Befehl, wie es ausgeführt wurde

Keines davon macht deutlich, ob der Wert von $0das Verzeichnis enthalten würde oder nicht, wenn das Skript ohne es aufgerufen würde. Der letzte impliziert mir tatsächlich, dass dies nicht der Fall ist.

Testen auf meinem System (Bash 4.1)

Ich habe eine ausführbare Datei in / usr / local / bin mit dem Namen scriptname mit einer Zeile erstellt echo $0und sie von verschiedenen Speicherorten aus aufgerufen.

Das sind meine Ergebnisse:

> 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

In diesen Tests ist der Wert von $0immer genau so, wie das Skript aufgerufen wurde, außer wenn es ohne Pfadkomponente aufgerufen wird. In diesem Fall ist der Wert von $0der absolute Pfad . Es sieht also so aus, als wäre es sicher, an einen anderen Befehl weiterzugeben.

Aber dann stieß ich auf einen Kommentar zu Stack Overflow , der mich verwirrte. In der Antwort wird vorgeschlagen $(dirname $0), das Verzeichnis des aktuellen Skripts abzurufen. Der Kommentar (7-mal positiv bewertet) besagt, dass "das nicht funktioniert, wenn sich das Skript in Ihrem Pfad befindet".

Fragen

  • Ist dieser Kommentar richtig?
  • Ist das Verhalten auf anderen Systemen anders?
  • Gibt es Situationen, in denen $0das Verzeichnis nicht enthalten wäre?
Toxalot
quelle
Die Leute haben auf Situationen geantwortet, in denen $0etwas anderes als das Skript ist, das den Fragentitel beantwortet. Ich interessiere mich jedoch auch für Situationen, in denen $0sich das Skript selbst befindet, das Verzeichnis jedoch nicht enthält. Insbesondere versuche ich, den Kommentar zur SO-Antwort zu verstehen.
Toxalot

Antworten:

17

In den meisten Fällen $0wird ein Pfad enthalten, absolut oder relativ zum Skript, also

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

(vorausgesetzt, es gibt einen readlinkBefehl, der unterstützt wird -e) ist im Allgemeinen ein guter Weg, um den kanonischen absoluten Pfad zum Skript zu erhalten.

$0 wird aus dem Argument zugewiesen, das das an den Interpreter übergebene Skript angibt.

Zum Beispiel in:

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

$0bekommt the/script.

Wenn Sie laufen:

the/script its args

Ihre Shell wird Folgendes tun:

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

Wenn das Skript #! /bin/sh -zum Beispiel einen Knall enthält , wandelt das System diesen um in:

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

(Wenn es keinen She-Bang enthält oder allgemeiner, wenn das System einen ENOEXEC-Fehler zurückgibt, dann ist es Ihre Shell, die dasselbe tut)

Es gibt eine Ausnahme für setuid / setgid-Skripte auf einigen Systemen, bei denen das System das Skript auf einigen Systemen öffnet fd xund stattdessen ausführt:

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

um Rennbedingungen zu vermeiden (in diesem Fall $0wird enthalten /dev/fd/x).

Nun kann man argumentieren , dass /dev/fd/x ist ein Pfad zu diesem Skript. Beachten Sie jedoch, dass Sie beim Lesen $0das Skript unterbrechen, wenn Sie die Eingabe verwenden.

Jetzt gibt es einen Unterschied, wenn der aufgerufene Skriptbefehlsname keinen Schrägstrich enthält. Im:

the-script its args

Shell wird aufblicken the-scriptin $PATH. $PATHkann absolute oder relative Pfade (einschließlich der leeren Zeichenfolge) zu einigen Verzeichnissen enthalten. Wenn beispielsweise das aktuelle Verzeichnis $PATHenthält /bin:/usr/bin:und the-scriptgefunden wird, führt die Shell Folgendes aus:

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

was wird werden:

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

Oder wenn es gefunden wird in /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")

In allen oben genannten Fällen mit Ausnahme des Falles setuid Corner $0wird ein Pfad (absolut oder relativ) zum Skript enthalten.

Ein Skript kann jetzt auch wie folgt aufgerufen werden:

the-interpreter the-script its args

Wenn the-scriptwie oben keine Schrägstriche enthalten sind, variiert das Verhalten geringfügig von Shell zu Shell.

Alte AT & T- kshImplementierungen haben das Skript tatsächlich bedingungslos nachgeschlagen $PATH(was eigentlich ein Fehler und eine Sicherheitslücke für Setuid-Skripte war) und $0enthielten daher keinen Pfad zum Skript, es sei denn, die $PATHSuche wurde tatsächlich the-scriptim aktuellen Verzeichnis gefunden.

Neuere AT & T kshwürden versuchen, the-scriptim aktuellen Verzeichnis zu interpretieren, wenn es lesbar ist. Wenn nicht, würde es nach einer lesbaren und ausführbaren Datei the-script in suchen$PATH .

Denn bashes prüft, ob the-scriptes sich im aktuellen Verzeichnis befindet (und kein defekter Symlink ist), und wenn nicht, sucht es nach einem lesbaren (nicht unbedingt ausführbaren) the-scriptin $PATH.

zshin shEmulation würde gerne tun, bashaußer dass, wenn the-scriptein defekter Symlink im aktuellen Verzeichnis ist, es nicht nach einem the-scriptIn suchen $PATHwürde und stattdessen einen Fehler melden würde.

Alle anderen Bourne-ähnlichen Muscheln sehen nicht the-scriptauf $PATH.

Wenn Sie bei all diesen Shells feststellen, dass $0sie kein a enthalten /und nicht lesbar sind, wurde sie wahrscheinlich nachgeschlagen $PATH. Da Dateien in $PATHwahrscheinlich ausführbar sind, ist es wahrscheinlich eine sichere Annäherung, command -v -- "$0"um den Pfad zu finden (obwohl dies nicht funktionieren würde, wenn $0zufällig auch der Name einer eingebauten Shell oder eines Schlüsselworts (in den meisten Shells) verwendet wird).

Wenn Sie diesen Fall wirklich abdecken möchten, können Sie ihn schreiben:

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

(Das ""angehängte $PATHElement besteht darin, ein nachfolgendes leeres Element mit Schalen $IFSbeizubehalten, die als Trennzeichen anstelle des Trennzeichens fungieren .)

Jetzt gibt es esoterischere Möglichkeiten, ein Skript aufzurufen. Man könnte tun:

the-shell < the-script

Oder:

cat the-script | the-shell

In diesem Fall ist $0dies das erste Argument ( argv[0]), das der Interpreter erhalten hat (oben the-shell, aber das kann alles sein, obwohl im Allgemeinen entweder der Basisname oder ein Pfad zu diesem Interpreter).

Es $0ist nicht zuverlässig zu erkennen, dass Sie sich in dieser Situation befinden, basierend auf dem Wert von . Sie können sich die Ausgabe von ansehen ps -o args= -p "$$", um einen Hinweis zu erhalten. Im Pipe-Fall gibt es keine echte Möglichkeit, zu einem Pfad zum Skript zurückzukehren.

Man könnte auch tun:

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

Dann, es sei denn zsh(und einige alte Implementierung der Bourne - Shell), $0wäre blah. Auch hier ist es schwierig, in diesen Shells zum Pfad des Skripts zu gelangen.

Oder:

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

etc.

Um sicherzustellen, dass Sie das Recht haben $progname, können Sie nach einer bestimmten Zeichenfolge suchen:

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

Aber ich denke auch nicht, dass es die Mühe wert ist.

Stéphane Chazelas
quelle
Stéphane, ich verstehe Ihre Verwendung "-"in den obigen Beispielen nicht. Nach meiner Erfahrung exec("the-script", ["the-script", "its", "args"])wird exec("/the/interpreter", ["/the/interpreter", "the-script", "its", "args"])natürlich mit der Möglichkeit eine Dolmetscheroption.
jrw32982 unterstützt Monica
@ jrw32982, #! /bin/sh -ist das Sprichwort "Immer verwenden, cmd -- somethingwenn Sie nicht garantieren können, dass somethinges nicht beginnt -", das hier angewendet wird /bin/sh(wobei -der Marker für das Ende der Option portabler ist als --), wobei somethinges der Pfad / Name des ist Skript. Wenn Sie das nicht für setuid Skripte verwenden (auf Systeme , die sie unterstützen , aber nicht mit dem / dev / fd / x - Methode in der Antwort erwähnt), dann kann man ein Root - Shell erhalten , indem Sie einen symbolischen Link zu Ihrem Skript erstellen genannt -ioder -sfür Beispiel.
Stéphane Chazelas
Danke, Stéphane. Ich hatte den nachfolgenden Bindestrich in Ihrer Beispiel-Shebang-Zeile verpasst. Ich musste suchen, wo dokumentiert ist, dass ein einzelner Bindestrich einem doppelten Bindestrich entspricht pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html .
jrw32982 unterstützt Monica
Es ist zu leicht, den nachfolgenden einfachen / doppelten Bindestrich in der Shebang-Zeile für ein Setuid-Skript zu vergessen. Irgendwie sollte sich das System für Sie darum kümmern. Deaktivieren Sie Setuid-Skripte insgesamt oder /bin/shsollten Sie die eigene Optionsverarbeitung deaktivieren, wenn festgestellt werden kann, dass Setuid ausgeführt wird. Ich sehe nicht, wie die Verwendung von / dev / fd / x allein das behebt. Sie brauchen immer noch den einfachen / doppelten Bindestrich, denke ich.
jrw32982 unterstützt Monica
@ jrw32982, /dev/fd/xbeginnt mit /, nicht -. Das primäre Ziel ist es jedoch, die Race-Bedingung zwischen den beiden execve()s zu entfernen (zwischen dem, execve("the-script")was die Berechtigungen erhöht, und dem nachfolgenden, execve("interpreter", "thescript")wo interpreterdas Skript später geöffnet wird (was in der Zwischenzeit möglicherweise durch einen Symlink zu etwas anderem ersetzt wurde). Systeme, die implementiert werden suid-Skripte machen execve("interpreter", "/dev/fd/n")stattdessen ein, wo n als Teil des ersten execve () geöffnet wurde.
Stéphane Chazelas
6

Hier sind zwei Situationen, in denen das Verzeichnis nicht enthalten wäre:

> bash scriptname
scriptname

> bash <scriptname
bash

In beiden Fällen müsste das aktuelle Verzeichnis das Verzeichnis sein, in dem sich der Skriptname befindet.

Im ersten Fall könnte der Wert von $0weiterhin übergeben werden, grepda davon ausgegangen wird, dass das Argument FILE relativ zum aktuellen Verzeichnis ist.

Im zweiten Fall sollte es kein Problem sein, wenn die Hilfe- und Versionsinformationen nur als Antwort auf eine bestimmte Befehlszeilenoption gedruckt werden. Ich bin mir nicht sicher, warum jemand auf diese Weise ein Skript aufrufen würde, um die Hilfe- oder Versionsinformationen zu drucken.

Vorsichtsmaßnahmen

  • Wenn das Skript das aktuelle Verzeichnis ändert, möchten Sie keine relativen Pfade verwenden.

  • Wenn das Skript bezogen wird, ist der Wert von $0normalerweise das Aufruferskript und nicht das bezogene Skript.

Toxalot
quelle
3

Ein beliebiges Argument Null kann angegeben werden, wenn die -cOption für die meisten (alle?) Shells verwendet wird. Z.B:

sh -c 'echo $0' argv0

Von man bash(nur gewählt, weil dies eine bessere Beschreibung als meine hat man sh- die Verwendung ist unabhängig davon gleich):

-c

Wenn die Option -c vorhanden ist, werden Befehle aus dem ersten Nicht-Optionsargument command_string gelesen. Wenn nach dem Befehlsstring Argumente vorhanden sind, werden diese den Positionsparametern zugewiesen, beginnend mit $ 0.

Graeme
quelle
Ich denke, das funktioniert, weil -c 'Befehl' Operanden sind und argv0das erste Nichtoperanden -Befehlszeilenargument ist.
Mikeserv
@mike richtig, ich habe mit einem Mann Snippet aktualisiert.
Graeme
3

HINWEIS: Andere haben die Mechanik bereits erklärt, $0daher überspringe ich das alles.

Im Allgemeinen gehe ich dieses ganze Problem zur Seite und benutze einfach den Befehl readlink -f $0. Dies gibt Ihnen immer den vollständigen Weg zurück, was Sie ihm als Argument geben.

Beispiele

Angenommen, ich bin hier, um zu beginnen mit:

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

Erstellen Sie ein Verzeichnis + Datei:

$ mkdir adir
$ touch afile
$ cd adir/

Jetzt fang an anzugeben 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

Zusätzlicher Trick

Jetzt, da ein konsistentes Ergebnis zurückgegeben wird, wenn wir $0über abfragen readlink, können wir einfach verwenden dirname $(readlink -f $0), um den absoluten Pfad zum Skript -oder- basename $(readlink -f $0)zu erhalten, um den tatsächlichen Namen des Skripts zu erhalten.

slm
quelle
0

Meine manSeite sagt:

$0: erweitert sich auf die namevon shelloder shell script.

Es scheint, dass dies auf argv[0]die aktuelle Shell übersetzt wird - oder auf das erste Nichtoperanden- Befehlszeilenargument, das der aktuell interpretierenden Shell beim Aufrufen zugeführt wird. Ich habe zuvor angegeben, dass sh ./somescriptder Pfad zu seiner Variablen führen würde , aber dies war falsch, da dies ein neuer Prozess für sich ist und mit einem neuen aufgerufen wird .$0 $ENV shshell$ENV

Auf diese Weise sh ./somescript.shunterscheidet sich von dem, . ./somescript.shwas in der aktuellen Umgebung läuft und $0bereits eingestellt ist.

Sie können dies durch einen Vergleich $0zu /proc/$$/status.

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

Danke für die Korrektur, @toxalot. Ich habe etwas gelernt

mikeserv
quelle
Wenn es auf meinem System bezogen wird ( . ./myscript.shoder source ./myscript.sh), dann $0ist es die Shell. Wenn es jedoch als Argument an shell ( sh ./myscript.sh) übergeben wird, $0ist dies der Pfad zum Skript. Natürlich ist shauf meinem System Bash. Ich weiß also nicht, ob das einen Unterschied macht oder nicht.
Toxalot
Hat es einen Hashbang? Ich denke, dass der Unterschied wichtig ist - und ich kann das hinzufügen - ohne ihn sollte er es nicht exectun und stattdessen beschaffen, aber damit sollte execer es tun .
Mikeserv
Mit oder ohne Hashbang erhalte ich die gleichen Ergebnisse. Mit oder ohne ausführbar zu sein, erhalte ich die gleichen Ergebnisse.
Toxalot
Ich auch! Ich denke, das liegt daran, dass es sich um eine eingebaute Shell handelt. Ich überprüfe ...
mikeserv
Bang Lines werden vom Kernel interpretiert. Wenn Sie ./somescriptmit der Bangline ausführen #!/bin/sh, entspricht dies dem Ausführen /bin/sh ./somescript. Ansonsten machen sie keinen Unterschied für die Shell.
Graeme
0

Ich möchte das aktuelle Skript durchsuchen, damit ich Hilfe- und Versionsinformationen aus dem Kommentarbereich oben ausdrucken kann.

Während $0des Skriptnamen enthält , kann es einen Präfix Pfad auf dem Weg basierend enthält das Skript aufgerufen wird, habe ich immer verwenden ${0##*/}den Namen des Skripts in der Hilfe - Ausgabe zu drucken , die von jedem führenden Pfad entfernt $0.

Entnommen aus dem Advanced Bash Scripting-Handbuch - Abschnitt 10.2 Parameterersetzung

${var#Pattern}

Entfernen Sie vom $varkürzesten Teil $Pattern, der zum vorderen Ende von passt $var.

${var##Pattern}

Entfernen Sie vom $varlängsten Teil $Pattern, der zum vorderen Ende von passt $var.

Der längste Teil $0dieser Übereinstimmungen */ist also das gesamte Pfadpräfix, wobei nur der Skriptname zurückgegeben wird.

Sambler
quelle
Ja, das mache ich auch für den Namen des Skripts, das in der Hilfemeldung verwendet wird. Aber ich spreche über das Greppen des Skripts, also muss ich mindestens einen relativen Pfad haben. Ich möchte die Hilfemeldung in den Kommentaren ganz oben platzieren und muss die Hilfemeldung später beim Drucken nicht wiederholen.
Toxalot
0

Für etwas ähnliches benutze ich:

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

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

rPath hat immer den gleichen Wert.

Anonym
quelle