$ 0 includerà sempre il percorso dello script?


11

Voglio grep lo script corrente in modo da poter stampare le informazioni di aiuto e versione dalla sezione commenti in alto.

Stavo pensando a qualcosa del genere:

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

Ma poi mi chiedevo cosa sarebbe successo se lo script si trovasse in una directory che era in PERCORSO e chiamato senza specificare esplicitamente la directory.

Ho cercato una spiegazione delle variabili speciali e ho trovato le seguenti descrizioni di $0:

  • nome della shell o del programma corrente

  • nome file dello script corrente

  • nome della sceneggiatura stessa

  • comando come è stato eseguito

Nessuno di questi chiarisce se il valore di $0includa o meno la directory se lo script fosse invocato senza di essa. L'ultimo in realtà implica per me che non lo farebbe.

Test sul mio sistema (Bash 4.1)

Ho creato un file eseguibile in / usr / local / bin chiamato scriptname con una riga echo $0e lo ho invocato da posizioni diverse.

Questi sono i miei risultati:

> 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 questi test, il valore di $0è sempre esattamente come è stato invocato lo script, tranne se è invocato senza alcun componente del percorso. In tal caso, il valore di $0è il percorso assoluto . Quindi sembra che sarebbe sicuro passare a un altro comando.

Ma poi mi sono imbattuto in un commento su Stack Overflow che mi ha confuso. La risposta suggerisce di usare $(dirname $0)per ottenere la directory dello script corrente. Il commento (aggiornato 7 volte) dice "che non funzionerà se lo script è sul tuo percorso".

Domande

  • È corretto questo commento?
  • Il comportamento è diverso su altri sistemi?
  • Ci sono situazioni in cui $0non includerebbe la directory?

Le persone hanno risposto a situazioni in cui $0c'è qualcosa di diverso dalla sceneggiatura, che risponde al titolo della domanda. Tuttavia, sono anche interessato a situazioni in cui si $0trova lo script stesso, ma non include la directory. In particolare, sto cercando di capire il commento fatto sulla risposta SO.
Toxalot

Risposte:


17

Nei casi più comuni, $0conterrà un percorso, assoluto o relativo allo script, quindi

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

(supponendo che ci sia un readlinkcomando e che supporti -e) generalmente è un modo abbastanza buono per ottenere il percorso assoluto canonico allo script.

$0 viene assegnato dall'argomento specificando lo script come passato all'interprete.

Ad esempio, in:

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

$0ottiene the/script.

Quando corri:

the/script its args

La tua shell farà un:

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

Se lo script contiene un #! /bin/sh -botto per esempio, il sistema lo trasformerà in:

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

(se non contiene un botto o, più in generale, se il sistema restituisce un errore ENOEXEC, allora è la tua shell che farà la stessa cosa)

C'è un'eccezione per gli script setuid / setgid su alcuni sistemi, in cui il sistema aprirà lo script su alcuni fd xed eseguirà invece:

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

per evitare le condizioni di gara (nel qual caso $0conterrà /dev/fd/x).

Ora, potresti obiettare che /dev/fd/x è un percorso per quello script. Si noti tuttavia che se si legge da $0, si romperà lo script mentre si consuma l'input.

Ora, c'è una differenza se il nome del comando di script come invocato non contiene una barra. Nel:

the-script its args

La shell cercherà the-scriptin $PATH. $PATHpuò contenere percorsi assoluti o relativi (inclusa la stringa vuota) in alcune directory. Ad esempio, se $PATHcontiene /bin:/usr/bin:e the-scriptsi trova nella directory corrente, la shell farà un:

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

che diventerà:

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

O se si trova 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 tutti quei casi sopra, tranne il caso d'angolo setuid, $0conterrà un percorso (assoluto o relativo) allo script.

Ora, uno script può anche essere chiamato come:

the-interpreter the-script its args

Quando the-scriptcome sopra non contiene caratteri barra, il comportamento varia leggermente da shell a shell.

Le vecchie kshimplementazioni AT&T stavano effettivamente cercando lo script incondizionatamente $PATH(che in realtà era un bug e una falla di sicurezza per gli script setuid), quindi in $0realtà non conteneva un percorso dello script a meno che la $PATHricerca non fosse effettivamente trovata the-scriptnella directory corrente.

Gli AT&T più recenti kshtenterebbero di interpretare the-scriptnella directory corrente se è leggibile. In caso contrario, cercherebbe un file leggibile ed eseguibilethe-script in $PATH.

Per bash, controlla se si the-scripttrova nella directory corrente (e non è un link simbolico non funzionante) e, in caso contrario, cerca un leggibile (non necessariamente eseguibile) the-scriptin $PATH.

zshin shemulazione farebbe basheccezione se non the-scriptfosse un collegamento simbolico interrotto nella directory corrente, non cercherebbe un the-scriptin $PATHe segnalerebbe invece un errore.

Tutte le altre conchiglie tipo Bourne non guardano the-scriptdentro $PATH.

Per tutte quelle shell comunque, se trovi che $0non contiene un /e non è leggibile, probabilmente è stato cercato in $PATH. Quindi, poiché $PATHè probabile che i file siano eseguibili, è probabilmente un'approssimazione sicura da usare command -v -- "$0"per trovare il suo percorso (anche se non funzionerebbe se si $0verifica anche il nome di una shell incorporata o di una parola chiave (nella maggior parte delle shell)).

Quindi, se vuoi davvero coprire questo caso, potresti scriverlo:

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

(l' ""aggiunta a $PATHè di preservare un elemento vuoto finale con shell le cui $IFSfunzioni sono delimitatori anziché separatori ).

Ora, ci sono modi più esoterici per invocare uno script. Si potrebbe fare:

the-shell < the-script

O:

cat the-script | the-shell

In tal caso, $0sarà il primo argomento ( argv[0]) che l' interprete ha ricevuto (sopra the-shell, ma potrebbe essere qualsiasi cosa, sebbene generalmente sia il nome di base o un percorso per quell'interprete).

Rilevare che ti trovi in ​​quella situazione in base al valore di $0non è affidabile. Potresti guardare l'output di ps -o args= -p "$$"per ottenere un indizio. Nel caso della pipe, non c'è modo reale di tornare a un percorso dello script.

Si potrebbe anche fare:

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

Quindi, tranne in zsh(e qualche vecchia implementazione della shell Bourne), $0sarebbe blah. Ancora una volta, difficile arrivare al percorso dello script in quelle shell.

O:

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

eccetera.

Per assicurarti di avere il diritto $progname, puoi cercare una stringa specifica in esso come:

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

Ma di nuovo non penso che valga la pena.


Stéphane, non capisco il tuo uso "-"negli esempi sopra. Nella mia esperienza, exec("the-script", ["the-script", "its", "args"])diventa exec("/the/interpreter", ["/the/interpreter", "the-script", "its", "args"]), ovviamente, la possibilità di un'opzione di interprete.
jrw32982 supporta Monica

@ jrw32982, #! /bin/sh -è il "usa sempre cmd -- somethingse non puoi garantire che somethingnon inizierà con -" un adagio di buona pratica qui applicato a /bin/sh(dove -il marker di fine opzione è più portabile di --) somethingessendo il percorso / nome del script. Se non lo usi per gli script setuid (sui sistemi che li supportano ma non con il metodo / dev / fd / x menzionato nella risposta), puoi ottenere una shell di root creando un link simbolico al tuo script chiamato -io -sper esempio.
Stéphane Chazelas,

Grazie, Stéphane. Mi mancava il trattino singolo finale nel tuo esempio di Shebang Line. Ho dovuto cercare dove è documentato che un singolo trattino equivale a un doppio trattino pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html .
jrw32982 supporta Monica il

È troppo facile dimenticare il trattino singolo / doppio nella riga shebang per uno script setuid; in qualche modo il sistema dovrebbe occuparsene per te. Non consentire gli script setuid del tutto o in qualche modo /bin/shdovrebbe disabilitare la propria elaborazione delle opzioni se è in grado di rilevare che sta eseguendo setuid. Non vedo come l'uso di / dev / fd / x da solo risolva questo problema. Hai ancora bisogno del trattino singolo / doppio, credo.
jrw32982 supporta Monica il

@ jrw32982, /dev/fd/xinizia con /, no -. L'obiettivo principale è quello di rimuovere le condizioni di gara tra i due execve()(tra i execve("the-script")quali eleva i privilegi e il successivo execve("interpreter", "thescript")dove interpreterapre lo script in seguito (che potrebbe benissimo essere stato sostituito con un link simbolico a qualcos'altro nel frattempo). Sistemi che implementano gli script suid eseguono correttamente execve("interpreter", "/dev/fd/n")invece un punto in cui n è stato aperto come parte del primo execve ().
Stéphane Chazelas

6

Ecco due situazioni in cui la directory non verrebbe inclusa:

> bash scriptname
scriptname

> bash <scriptname
bash

In entrambi i casi, la directory corrente dovrebbe essere la directory in cui si trovava lo scriptname .

Nel primo caso, il valore di $0potrebbe ancora essere passato greppoiché presuppone che l'argomento FILE sia relativo alla directory corrente.

Nel secondo caso, se le informazioni sulla guida e sulla versione vengono stampate solo in risposta a una specifica opzione della riga di comando, non dovrebbe essere un problema. Non sono sicuro del motivo per cui qualcuno invocherebbe uno script in quel modo per stampare la guida o le informazioni sulla versione.

Avvertenze

  • Se lo script modifica la directory corrente, non si desidera utilizzare percorsi relativi.

  • Se lo script proviene, il valore di $0sarà in genere lo script chiamante anziché lo script di provenienza.


3

È possibile specificare un argomento zero arbitrario quando si utilizza l' -copzione per la maggior parte (tutte?) Shell. Per esempio:

sh -c 'echo $0' argv0

Da man bash(scelto puramente perché ha una descrizione migliore del mio man sh- l'utilizzo è lo stesso indipendentemente):

-c

Se l'opzione -c è presente, i comandi vengono letti dal primo argomento non-opzione command_string. Se ci sono argomenti dopo il command_string, vengono assegnati ai parametri posizionali, a partire da $ 0.


Penso che argv0funzioni perché -c 'command' sono operandi ed è il suo primo argomento da riga di comando nonoperando.
Mikeserv,

@mike corretto, ho aggiornato con un frammento di uomo.
Graeme,

3

NOTA: altri hanno già spiegato la meccanica di $0così salterò tutto questo.

Generalmente passo avanti l'intero problema e utilizzo semplicemente il comando readlink -f $0. Questo ti restituirà sempre il percorso completo di qualsiasi cosa tu gli dia come argomento.

Esempi

Di 'che sono qui per iniziare:

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

Crea una directory + file:

$ mkdir adir
$ touch afile
$ cd adir/

Ora inizia a mostrare 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

Inganno aggiuntivo

Ora con un risultato coerente che viene restituito quando interroghiamo $0tramite readlinkpossiamo usare semplicemente un dirname $(readlink -f $0)per ottenere il percorso assoluto dello script -oppure- basename $(readlink -f $0)per ottenere il nome effettivo dello script.


0

La mia manpagina dice:

$0: si espande al namedi shello shell script.

Sembra che ciò si traduca argv[0]nella shell corrente - o nel primo argomento da riga di comando nonoperando che la shell attualmente interpretante viene alimentata quando invocata. Ho già detto che il sh ./somescriptpercorso corretto la sua variabile a ma questo era corretto perché il è un nuovo processo a sé stante e invocato con una nuova .$0 $ENV shshell$ENV

In questo modo sh ./somescript.shdifferisce da quello . ./somescript.shche viene eseguito nell'ambiente corrente ed $0è già impostato.

Puoi verificarlo confrontandolo $0con /proc/$$/status.

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

Grazie per la correzione, @toxalot. Ho imparato qualcosa


Sul mio sistema, se è di provenienza ( . ./myscript.sho source ./myscript.sh), allora $0è la shell. Ma se viene passato come argomento alla shell ( sh ./myscript.sh), allora $0è il percorso dello script. Certo, shsul mio sistema c'è Bash. Quindi non so se questo fa la differenza o no.
Toxalot

Ha un hashbang? Penso che la differenza sia importante - e posso aggiungerla - senza di essa sh non dovrebbe exece invece cercarla, ma con essa dovrebbe execfarlo.
Mikeserv,

Con o senza hashbang, ottengo gli stessi risultati. Con o senza essere eseguibile, ottengo gli stessi risultati.
Toxalot

Anche a me! Penso che sia perché è incorporato in una shell. Sto controllando ...
Mikeserv,

Le linee di scoppio sono interpretate dal kernel. Se si ./somescriptesegue con il bangline #!/bin/sh, sarebbe equivalente a correre /bin/sh ./somescript. Altrimenti non fanno differenza per la shell.
Graeme,

0

Voglio grep lo script corrente in modo da poter stampare le informazioni di aiuto e versione dalla sezione commenti in alto.

Sebbene $0contenga il nome dello script, può contenere un percorso con prefisso basato sul modo in cui viene chiamato lo script, ho sempre usato ${0##*/}per stampare il nome dello script nell'output della guida che rimuove qualsiasi percorso iniziale da $0.

Tratto da Advanced Bash Scripting guide - Sostituzione parametri sezione 10.2

${var#Pattern}

Rimuovi dalla $varparte più corta $Patterncorrispondente alla parte frontale di $var.

${var##Pattern}

Rimuovi dalla $varparte più lunga di $Patternquella corrispondente alla parte frontale di $var.

Quindi la parte più lunga di $0quelle corrispondenze */sarà l'intero prefisso del percorso, restituendo solo il nome dello script.


Sì, lo faccio anche per il nome dello script utilizzato nel messaggio di aiuto. Ma sto parlando del grepping della sceneggiatura, quindi devo avere almeno un percorso relativo. Voglio inserire il messaggio di aiuto in alto nei commenti e non dover ripetere il messaggio di aiuto in seguito durante la stampa.
Toxalot

0

Per qualcosa di simile che uso:

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

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

rPath ha sempre lo stesso valore.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.