Czy 0 USD zawsze będzie zawierać ścieżkę do skryptu?

11

Chcę zastąpić bieżący skrypt, aby móc wydrukować pomoc i informacje o wersji z sekcji komentarzy u góry.

Myślałem o czymś takim:

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

Ale potem zastanawiałem się, co by się stało, gdyby skrypt został umieszczony w katalogu, który był w PATH i wywołany bez wyraźnego określenia katalogu.

Szukałem wyjaśnienia zmiennych specjalnych i znalazłem następujące opisy $0:

  • nazwa bieżącej powłoki lub programu

  • nazwa pliku bieżącego skryptu

  • nazwa samego skryptu

  • polecenie, jak zostało uruchomione

Żadne z nich nie wyjaśnia, czy wartość $0zawiera katalog, jeśli skrypt zostałby wywołany bez niego. Ten ostatni sugeruje mi, że nie.

Testowanie w moim systemie (Bash 4.1)

Utworzyłem plik wykonywalny w / usr / local / bin o nazwie scriptname z jedną linią echo $0i wywołałem go z różnych lokalizacji.

Oto moje wyniki:

> 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

W tych testach wartość parametru $0jest zawsze dokładnie taka, jak skrypt został wywołany, z wyjątkiem sytuacji, gdy jest on wywoływany bez komponentu ścieżki. W takim przypadku wartością $0jest ścieżka bezwzględna . Wygląda na to, że przejście do innego polecenia byłoby bezpieczne.

Ale potem natknąłem się na komentarz na temat przepełnienia stosu, który mnie zdezorientował. Odpowiedź sugeruje użycie, $(dirname $0)aby uzyskać katalog bieżącego skryptu. W komentarzu (ocenianym 7 razy) napisano: „to nie zadziała, jeśli skrypt jest na twojej drodze”.

pytania

  • Czy ten komentarz jest poprawny?
  • Czy zachowanie jest inne w innych systemach?
  • Czy są sytuacje, w których $0katalog nie zawierałby?
toksalot
źródło
Ludzie odpowiadali na sytuacje, w których $0jest coś innego niż skrypt, który odpowiada na tytuł pytania. Jednak interesują mnie również sytuacje, w których $0sam skrypt jest, ale nie zawiera katalogu. W szczególności staram się zrozumieć komentarz podany w odpowiedzi na SO.
toxalot

Odpowiedzi:

17

W najczęstszych przypadkach $0będzie zawierać ścieżkę bezwzględną lub względną do skryptu, więc

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

(zakładając, że istnieje readlinkpolecenie i obsługuje -e) ogólnie jest wystarczającym sposobem na uzyskanie kanonicznej bezwzględnej ścieżki do skryptu.

$0 jest przypisywany z argumentu określającego skrypt przekazany interpreterowi.

Na przykład w:

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

$0dostaje the/script.

Kiedy biegniesz:

the/script its args

Twoja powłoka wykona:

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

Jeśli skrypt zawiera na przykład #! /bin/sh -she-bang, system przekształci go w:

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

(jeśli nie zawiera she-bang, lub bardziej ogólnie, jeśli system zwraca błąd ENOEXEC, to jego powłoka zrobi to samo)

Istnieje wyjątek dla skryptów setuid / setgid w niektórych systemach, gdzie system otworzy skrypt na niektórych fd xi uruchomi zamiast niego:

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

aby uniknąć warunków wyścigu (w takim przypadku $0będzie to zawierać /dev/fd/x).

Teraz możesz argumentować, że /dev/fd/x jest to ścieżka do tego skryptu. Pamiętaj jednak, że jeśli czytasz z $0, złamiesz skrypt, gdy zużyjesz dane wejściowe.

Istnieje różnica, jeśli wywołana nazwa polecenia skryptu nie zawiera ukośnika. W:

the-script its args

Twoja powłoka będzie patrzeć the-scriptw $PATH. $PATHmoże zawierać ścieżki bezwzględne lub względne (w tym pusty ciąg) do niektórych katalogów. Na przykład, jeśli $PATHzawiera /bin:/usr/bin:i the-scriptzostanie znaleziony w bieżącym katalogu, powłoka wykona:

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

który stanie się:

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

Lub jeśli zostanie znaleziony w /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")

We wszystkich powyższych przypadkach, z wyjątkiem przypadków narożników setuid, $0będzie zawierać ścieżkę (bezwzględną lub względną) do skryptu.

Teraz skrypt można również wywołać jako:

the-interpreter the-script its args

Gdy the-scriptjak wyżej nie zawiera znaków ukośnika, zachowanie różni się nieznacznie w zależności od powłoki.

Stare kshimplementacje AT&T faktycznie szukały skryptu bezwarunkowo w $PATH(co było w rzeczywistości błędem i luką w zabezpieczeniach dla skryptów setuid), więc tak $0naprawdę nie zawierało ścieżki do skryptu, chyba że $PATHwyszukiwanie rzeczywiście zdarzyło się znaleźć the-scriptw bieżącym katalogu.

Nowsze AT&T kshspróbuje zinterpretować the-scriptw bieżącym katalogu, jeśli jest to możliwe do odczytania. Jeśli nie, to szukałby czytelnego i wykonywalnego the-script w $PATH.

Sprawdza bowiem bash, czy the-scriptznajduje się w bieżącym katalogu (i nie jest zepsutym dowiązaniem symbolicznym), a jeśli nie, poszukaj możliwego do odczytu (niekoniecznie wykonywalnego) the-scriptw $PATH.

zshw shemulacji zrobiłoby to tak, bashże jeśli the-scriptjest zepsute dowiązanie symboliczne w bieżącym katalogu, nie szukałoby the-scriptin $PATHi zamiast tego zgłosiłoby błąd.

Wszystkie inne muszle podobne do Bourne'a nie patrzą w the-scriptgórę $PATH.

W każdym razie, jeśli okaże się, że $0nie zawiera /i nie jest czytelny, prawdopodobnie został sprawdzony $PATH. W związku z tym, ponieważ pliki w plikach $PATHprawdopodobnie są wykonywalne, prawdopodobnie jest to bezpieczne przybliżenie do command -v -- "$0"znalezienia ich ścieżki (chociaż nie zadziałałoby, jeśli $0tak się składa, że ​​jest to nazwa wbudowanej powłoki lub słowa kluczowego (w większości powłok)).

Więc jeśli naprawdę chcesz ukryć sprawę, możesz ją napisać:

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

( ""dołączono do $PATHzachowania końcowego pustego elementu za pomocą powłok, które $IFSdziałają jak separator zamiast separatora ).

Teraz są bardziej ezoteryczne sposoby na wywołanie skryptu. Można zrobić:

the-shell < the-script

Lub:

cat the-script | the-shell

W takim przypadku $0będzie to pierwszy argument ( argv[0]) otrzymany przez interpretera (powyżej the-shell, ale może to być cokolwiek, ale ogólnie albo basename, albo jedna ścieżka do tego interpretera).

Wykrywanie, że jesteś w takiej sytuacji na podstawie wartości, $0nie jest wiarygodne. Możesz spojrzeć na wynik, ps -o args= -p "$$"aby uzyskać wskazówkę. W przypadku potoku nie ma prawdziwego sposobu na powrót do ścieżki do skryptu.

Można również zrobić:

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

Wtedy, z wyjątkiem zsh(i niektórych starych implementacji powłoki Bourne'a), $0byłoby blah. Ponownie trudno jest znaleźć ścieżkę skryptu w tych powłokach.

Lub:

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

itp.

Aby upewnić się, że masz do tego prawo $progname, możesz wyszukać w nim określony ciąg znaków, np .:

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

Ale znowu nie sądzę, żeby było warto.

Stéphane Chazelas
źródło
Stéphane, nie rozumiem twojego użycia "-"w powyższych przykładach. Z mojego doświadczenia exec("the-script", ["the-script", "its", "args"])wynika exec("/the/interpreter", ["/the/interpreter", "the-script", "its", "args"]), że oczywiście istnieje możliwość skorzystania z opcji tłumacza.
jrw32982 obsługuje Monikę
@ jrw32982, #! /bin/sh -to „zawsze używaj, cmd -- somethingjeśli nie możesz zagwarantować, że somethingnie zacznie się od - tutaj zastosowano dobre objaśnienie /bin/sh(gdzie -znacznik końca opcji jest bardziej przenośny niż --), somethingbędąc ścieżką / nazwą scenariusz. Jeśli nie użyć dla skryptów setuid (w systemach, które je obsługują, ale nie z katalogu / dev / fd / x metoda wspomniano w odpowiedzi), a następnie można uzyskać powłokę roota poprzez utworzenie dowiązania do skryptu o nazwie -ilub -sza instancja.
Stéphane Chazelas
Dzięki, Stéphane. Brakowało mi pojedynczego łącznika w twojej przykładowej linii shebang. Musiałem szukać tam, gdzie udokumentowano, że pojedynczy myślnik jest równoważny podwójnemu myślnikowi pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html .
jrw32982 obsługuje Monikę
Zbyt łatwo jest zapomnieć o łączniku pojedynczym / podwójnym w linii shebang dla skryptu setuid; jakoś system powinien się tym zająć. Zablokuj skrypty setuid całkowicie lub w jakiś sposób /bin/shpowinien wyłączyć własne przetwarzanie opcji, jeśli może wykryć, że działa setuid. Nie widzę, jak samo użycie / dev / fd / x to naprawia. Myślę, że nadal potrzebujesz pojedynczego / podwójnego łącznika.
jrw32982 obsługuje Monikę
@ jrw32982, /dev/fd/xzaczyna się od /, nie -. Podstawowym celem jest jednak usunięcie warunków wyścigu między tymi dwoma execve()(pomiędzy, execve("the-script")które podnoszą przywileje, a później execve("interpreter", "thescript")gdzieinterpreter skrypt otwiera się później (co równie dobrze może zostać zastąpione przez dowiązanie symboliczne do czegoś innego w międzyczasie). skrypty suid poprawnie wykonują execve("interpreter", "/dev/fd/n")zamiast tego, gdzie n zostało otwarte jako część pierwszego execve ().
Stéphane Chazelas
6

Oto dwie sytuacje, w których katalog nie zostałby uwzględniony:

> bash scriptname
scriptname

> bash <scriptname
bash

W obu przypadkach katalogiem bieżącym musiałby być katalog, w którym zlokalizowana była nazwa skryptu .

W pierwszym przypadku wartość parametru $0nadal może zostać przekazana, grepponieważ zakłada, że ​​argument PLIK jest względem bieżącego katalogu.

W drugim przypadku, jeśli informacje pomocy i wersji są drukowane tylko w odpowiedzi na konkretną opcję wiersza poleceń, nie powinno to stanowić problemu. Nie jestem pewien, dlaczego ktokolwiek miałby wywoływać skrypt w ten sposób, aby wydrukować pomoc lub informacje o wersji.

Ostrzeżenia

  • Jeśli skrypt zmieni bieżący katalog, nie będziesz chciał używać ścieżek względnych.

  • Jeśli skrypt jest pozyskiwany, wartość $0 zwykle będzie skrypt wywołujący, a nie skrypt źródłowy.

toksalot
źródło
3

Dowolny argument zero może być określony przy użyciu -copcji dla większości (wszystkich?) Powłok. Na przykład:

sh -c 'echo $0' argv0

Od man bash(wybrany wyłącznie dlatego, że ma lepszy opis niż mój man sh- użycie jest takie samo niezależnie):

-do

Jeśli opcja -c jest obecna, polecenia są odczytywane z pierwszego nie-opcji argumentu łańcuch_w poleceniu. Jeśli po łańcuchu komend występują argumenty, są one przypisywane do parametrów pozycyjnych, zaczynając od $ 0.

Graeme
źródło
Myślę, że to działa, ponieważ -c „polecenie” to operandy i argv0jest to pierwszy argument wiersza polecenia nieoperand.
mikeserv
@mike poprawne, zaktualizowałem fragment kodu człowieka.
Graeme
3

UWAGA: Inni już wyjaśnili mechanikę, $0więc pominę wszystko.

Generalnie wykonuję krok po kroku cały problem i po prostu używam polecenia readlink -f $0 . To zawsze da ci pełną ścieżkę tego, co podasz jako argument.

Przykłady

Powiedz, że jestem tu na początek:

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

Utwórz katalog + plik:

$ mkdir adir
$ touch afile
$ cd adir/

Teraz zacznij się popisywać 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

Dodatkowe oszustwo

Teraz, gdy zwracamy się $0za pośrednictwem spójnego wyniku readlink, możemy użyć po prostu, dirname $(readlink -f $0)aby uzyskać bezwzględną ścieżkę do skryptu lub basename $(readlink -f $0)uzyskać rzeczywistą nazwę skryptu.

slm
źródło
0

Moja manstrona mówi:

$0: Rozwija się do namez shelllub shell script.

Wygląda na to, że przekłada się to argv[0]na bieżącą powłokę - lub na pierwszy argument nieoperandolowy wiersza poleceń, który aktualnie interpretowana powłoka jest podawana po wywołaniu. I wcześniej stwierdził, że sh ./somescriptbyłoby jej ścieżce zmiennej do ale to było błędne, ponieważ jest to nowy proces z własnej i wywołany z nowym .$0 $ENV shshell$ENV

W ten sposób sh ./somescript.shróżni się od tego, . ./somescript.shktóry działa w bieżącym środowisku i $0jest już ustawiony.

Możesz to sprawdzić, porównując $0z /proc/$$/status.

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

Dzięki za korektę, @toxalot. Nauczyłem się czegoś.

mikeserv
źródło
W moim systemie, jeśli jest to źródło ( . ./myscript.shlub source ./myscript.sh), to $0jest to powłoka. Ale jeśli jest przekazywany jako argument do powłoki ( sh ./myscript.sh), to $0jest ścieżka do skryptu. Oczywiście shw moim systemie jest Bash. Więc nie wiem, czy to robi różnicę, czy nie.
toxalot
Czy to hashbang? Myślę, że różnica ma znaczenie - i mogę to dodać - bez niej nie powinna execi zamiast tego powinna ją pozyskiwać, ale z nią powinna exec.
mikeserv
Z haszyszem lub bez niego uzyskuję te same wyniki. Z wykonalnością lub bez niej uzyskuję te same wyniki.
toxalot
Ja też! Myślę, że to dlatego, że jest to wbudowana powłoka. Sprawdzam ...
Mikeserv
Linie Bang są interpretowane przez jądro. Jeśli wykonasz ./somescriptz bangline #!/bin/sh, byłoby to równoważne z uruchomieniem /bin/sh ./somescript. W przeciwnym razie nie mają znaczenia dla powłoki.
Graeme
0

Chcę zastąpić bieżący skrypt, aby móc wydrukować pomoc i informacje o wersji z sekcji komentarzy u góry.

Chociaż $0zawiera nazwę skryptu, może zawierać prefiksową ścieżkę na podstawie sposobu, w jaki skrypt jest wywoływany, zawsze ${0##*/}drukowałem nazwę skryptu na wyjściu pomocy, która usuwa wszelką wiodącą ścieżkę $0.

Zaczerpnięte z przewodnika Advanced Bash Scripting - Zmiana parametrów w rozdziale 10.2

${var#Pattern}

Usuń z $varnajkrótszej części $Patternpasującej do przedniej części $var.

${var##Pattern}

Usuń z $varnajdłuższej części $Patternpasującej do przedniej części $var.

Najdłuższą częścią $0tych dopasowań */będzie cały prefiks ścieżki, zwracający tylko nazwę skryptu.

sambler
źródło
Tak, robię to również dla nazwy skryptu używanego w komunikacie pomocy. Ale mówię o pomijaniu skryptu, więc muszę mieć przynajmniej ścieżkę względną. Chcę umieścić komunikat pomocy u góry w komentarzach i nie muszę powtarzać tego komunikatu później podczas drukowania.
toxalot
0

Do czegoś podobnego używam:

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

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

rPath zawsze ma tę samą wartość.

Anonimowy
źródło