Come rilevare se uno script viene fornito


220

Ho una sceneggiatura in cui non voglio che venga chiamata exitse proviene da una fonte.

Ho pensato di verificare se, $0 == bashma questo ha problemi se lo script proviene da un altro script o se l'utente lo utilizza da una shell diversa come ksh.

Esiste un modo affidabile per rilevare se uno script proviene?


2
Ho avuto un problema simile qualche tempo fa e l'ho risolto evitando "l'uscita" in tutti i casi; "kill -INT $$" termina lo script in modo sicuro in entrambi i casi.
GESii

1
Hai notato questa risposta ? Viene dato 5 anni dopo dall'accettazione, ma ha "batterie incluse".
raratiru,

Risposte:


74

Questo sembra essere portatile tra Bash e Korn:

[[ $_ != $0 ]] && echo "Script is being sourced" || echo "Script is a subshell"

Una riga simile a questa o un'assegnazione simile pathname="$_"(con un test e un'azione successivi) deve trovarsi sulla prima riga dello script o sulla riga dopo lo shebang (che, se usato, dovrebbe essere per ksh affinché funzioni sotto il maggior numero di circostanze).


10
Sfortunatamente non è garantito che funzioni. Se l'utente ha impostato BASH_ENV, $_nella parte superiore dello script sarà l'ultimo comando eseguito da BASH_ENV.
Mikel,

30
Questo non funzionerà anche se usi bash per eseguire lo script, ad esempio $ bash script.sh, allora $ _ sarebbe / bin / bash invece di ./script.sh, che è il caso che ti aspetti, quando lo script viene invocato in questo modo: $ ./script.sh In ogni caso il rilevamento con $_è un problema.
Wirawan Purwanto,

2
Potrebbero essere inclusi test aggiuntivi per verificare i metodi di invocazione.
In pausa fino a nuovo avviso.

8
Sfortunatamente, è sbagliato! vedi la mia risposta
F. Hauri,

8
Riassumendo: sebbene questo approccio di solito funzioni, non è solido ; fallisce nei seguenti 2 scenari: (a) bash script(invocazione tramite eseguibile della shell, che questa soluzione riporta erroneamente come di provenienza ), e (b) (molto meno probabile) echo bash; . script(se $_accade che corrisponda alla shell che fornisce lo script, questa soluzione riporta erroneamente come una subshell ). Solo le variabili speciali specifiche della shell (ad es. $BASH_SOURCE) Consentono solide soluzioni (ne consegue che non esiste una solida soluzione conforme a POSIX). Si è possibile, anche se ingombrante, per realizzare una robusta di cross-shell.
mklement0

171

Se la tua versione di Bash conosce la variabile di array BASH_SOURCE, prova qualcosa del tipo:

# man bash | less -p BASH_SOURCE
#[[ ${BASH_VERSINFO[0]} -le 2 ]] && echo 'No BASH_SOURCE array variable' && exit 1

[[ "${BASH_SOURCE[0]}" != "${0}" ]] && echo "script ${BASH_SOURCE[0]} is being sourced ..."

11
Questo è forse il modo più pulito in quanto $ BASH_SOURCE è destinato esattamente a tale scopo.
con-f-use

4
Si noti che questo non funzionerà con ksh che è una condizione specificata dall'OP.
In pausa fino a nuovo avviso.

2
C'è un motivo per usare ${BASH_SOURCE[0]}invece di solo $BASH_SOURCE? E ${0}vs $0?
hraban,

4
BASH_SOURCEè una variabile di array (vedi manuale ) che contiene una traccia di stack di origini, dove ${BASH_SOURCE[0]}è l'ultima. Le parentesi graffe vengono utilizzate qui per dire a bash cosa fa parte del nome della variabile. In $0questo caso non sono necessari , ma non fanno neanche male. ;)
Konrad,

4
@Konrad, e se si espande $array, si ottiene ${array[0]}per impostazione predefinita. Quindi, di nuovo, c'è un motivo [...]?
Charles Duffy,

134

Soluzioni robuste per bash, ksh,zsh , compreso un cross-shell uno, più un ragionevolmente solida soluzione POSIX :

  • I numeri di versione indicati sono quelli su cui è stata verificata la funzionalità - probabilmente anche queste soluzioni funzionano su versioni molto precedenti - feedback positivo .

  • Utilizzando solo le funzionalità POSIX (come in dash, che agisce come /bin/shsu Ubuntu), non esiste un modo affidabile per determinare se viene fornito uno script - vedi sotto per la migliore approssimazione .

Seguono una riga : spiegazione di seguito; la versione cross-shell è complessa, ma dovrebbe funzionare in modo robusto:

  • bash (verificato il 3.57 e il 4.4.19)

    (return 0 2>/dev/null) && sourced=1 || sourced=0
  • ksh (verificato su 93u +)

    [[ $(cd "$(dirname -- "$0")" && 
       printf '%s' "${PWD%/}/")$(basename -- "$0") != "${.sh.file}" ]] &&
         sourced=1 || sourced=0
    
  • zsh (verificato su 5.0.5) - assicurati di chiamarlo al di fuori di una funzione

    [[ $ZSH_EVAL_CONTEXT =~ :file$ ]] && sourced=1 || sourced=0
  • cross-shell (bash, ksh, zsh)

    ([[ -n $ZSH_EVAL_CONTEXT && $ZSH_EVAL_CONTEXT =~ :file$ ]] || 
     [[ -n $KSH_VERSION && $(cd "$(dirname -- "$0")" &&
        printf '%s' "${PWD%/}/")$(basename -- "$0") != "${.sh.file}" ]] || 
     [[ -n $BASH_VERSION ]] && (return 0 2>/dev/null)) && sourced=1 || sourced=0
    
  • Conforme a POSIX ; non un liner (singola tubazione) per motivi tecnici e non completamente robusto (vedi in basso):

    sourced=0
    if [ -n "$ZSH_EVAL_CONTEXT" ]; then 
      case $ZSH_EVAL_CONTEXT in *:file) sourced=1;; esac
    elif [ -n "$KSH_VERSION" ]; then
      [ "$(cd $(dirname -- $0) && pwd -P)/$(basename -- $0)" != "$(cd $(dirname -- ${.sh.file}) && pwd -P)/$(basename -- ${.sh.file})" ] && sourced=1
    elif [ -n "$BASH_VERSION" ]; then
      (return 0 2>/dev/null) && sourced=1 
    else # All other shells: examine $0 for known shell binary filenames
      # Detects `sh` and `dash`; add additional shell filenames as needed.
      case ${0##*/} in sh|dash) sourced=1;; esac
    fi
    

Spiegazione:


bash

(return 0 2>/dev/null) && sourced=1 || sourced=0

Nota: la tecnica è stata adattata dalla risposta dell'utente5754163 , in quanto si è rivelata più solida della soluzione originale, [[ $0 != "$BASH_SOURCE" ]] && sourced=1 || sourced=0[1]

  • Bash consente returnistruzioni solo dalle funzioni e, nell'ambito di uno script di livello superiore, solo se lo script è di provenienza .

    • Se returnviene utilizzato nell'ambito di livello superiore di uno script non di origine , viene emesso un messaggio di errore e il codice di uscita è impostato su 1.
  • (return 0 2>/dev/null)viene eseguito returnin una subshell e sopprime il messaggio di errore; successivamente il codice di uscita indica se lo script è stato fornito ( 0) o meno ( 1), che viene utilizzato con gli operatori &&e ||per impostare la sourcedvariabile di conseguenza.

    • È necessario l'uso di una subshell, poiché l'esecuzione returnnell'ambito di primo livello di uno script di origine uscirebbe dallo script.
    • Punta del cappello a @Haozhun , che ha reso il comando più robusto usando esplicitamente 0come returnoperando; egli osserva: per bash aiuto di return [N]: "Se N viene omesso, lo stato di ritorno è quello dell'ultimo comando." Di conseguenza, la versione precedente [che utilizzava solo return, senza un operando] produce un risultato errato se l'ultimo comando sulla shell dell'utente ha un valore di ritorno diverso da zero.

ksh

[[ \
   $(cd "$(dirname -- "$0")" && printf '%s' "${PWD%/}/")$(basename -- "$0") != \
   "${.sh.file}" \
]] && 
sourced=1 || sourced=0

La variabile speciale ${.sh.file}è in qualche modo analoga a $BASH_SOURCE; nota che ${.sh.file}provoca un errore di sintassi in bash, zsh e dash, quindi assicurati di eseguirlo in modo condizionale negli script multi-shell.

A differenza di bash, $0e ${.sh.file}NON è garantito che siano esattamente identici nel caso non di provenienza, poiché $0può essere un percorso relativo , mentre ${.sh.file}è sempre un percorso completo , quindi $0deve essere risolto in un percorso completo prima del confronto.


zsh

[[ $ZSH_EVAL_CONTEXT =~ :file$ ]] && sourced=1 || sourced=0

$ZSH_EVAL_CONTEXTcontiene informazioni sul contesto di valutazione. Chiamalo al di fuori di una funzione. All'interno di uno script di provenienza [ambito di primo livello], $ZSH_EVAL_CONTEXT termina con :file.

Avvertenza: all'interno di una sostituzione di comando, zsh aggiunge :cmdsubst, quindi prova $ZSH_EVAL_CONTEXTper :file:cmdsubst$lì.


Utilizzo solo delle funzioni POSIX

Se sei disposto a fare alcune ipotesi, puoi fare un'ipotesi ragionevole, ma non a prova di errore , se il tuo script è di provenienza, in base alla conoscenza dei nomi di file binari delle shell che potrebbero eseguire il tuo script .
In particolare, ciò significa che questo approccio fallisce se il tuo script viene fornito da un altro script .

La sezione "Come gestire le invocazioni di provenienza" in questa mia risposta discute i casi limite che non possono essere gestiti con le funzionalità POSIX solo in dettaglio.

Ciò si basa sul comportamento standard di $0, che zsh, ad esempio, non presenta.

Pertanto, l'approccio più sicuro è quello di combinare i metodi robusti e specifici per shell sopra con una soluzione di fallback per tutte le rimanenti shell.

Punta del cappello a Stéphane Desneux e la sua risposta per l'ispirazione (trasformando la mia espressione di dichiarazione cross-shell in un'istruzione shcompatibile ife aggiungendo un gestore per altre shell).

sourced=0
if [ -n "$ZSH_EVAL_CONTEXT" ]; then 
  case $ZSH_EVAL_CONTEXT in *:file) sourced=1;; esac
elif [ -n "$KSH_VERSION" ]; then
  [ "$(cd $(dirname -- $0) && pwd -P)/$(basename -- $0)" != "$(cd $(dirname -- ${.sh.file}) && pwd -P)/$(basename -- ${.sh.file})" ] && sourced=1
elif [ -n "$BASH_VERSION" ]; then
  (return 0 2>/dev/null) && sourced=1 
else # All other shells: examine $0 for known shell binary filenames
  # Detects `sh` and `dash`; add additional shell filenames as needed.
  case ${0##*/} in sh|dash) sourced=1;; esac
fi

[1] user1902689 ha scoperto che [[ $0 != "$BASH_SOURCE" ]]produce un falso positivo quando si esegue uno script situato nel$PATH passaggio del suo semplice nome file al filebash binario; ad esempio, bash my-scriptperché $0allora è giusto my-script, mentre $BASH_SOURCEè il percorso completo . Mentre normalmente non useresti questa tecnica per invocare script nel $PATH- li invocheresti direttamente ( my-script) - è utile quando combinato con -xper il debug .


1
complimenti per una risposta così completa.
DrUseful,

75

Dopo aver letto la risposta di DennisWilliamson, ci sono alcuni problemi, vedi sotto:

Come questa domanda rappresenta e , c'è un'altra parte in questa risposta riguardante ... vedi sotto.

Semplice modo

[ "$0" = "$BASH_SOURCE" ]

Proviamo (al volo perché quel bash potrebbe ;-):

source <(echo $'#!/bin/bash
           [ "$0" = "$BASH_SOURCE" ] && v=own || v=sourced;
           echo "process $$ is $v ($0, $BASH_SOURCE)" ')
process 29301 is sourced (bash, /dev/fd/63)

bash <(echo $'#!/bin/bash
           [ "$0" = "$BASH_SOURCE" ] && v=own || v=sourced;
           echo "process $$ is $v ($0, $BASH_SOURCE)" ')
process 16229 is own (/dev/fd/63, /dev/fd/63)

Uso sourceinvece off .per leggibilità (come .è un alias a source):

. <(echo $'#!/bin/bash
           [ "$0" = "$BASH_SOURCE" ] && v=own || v=sourced;
           echo "process $$ is $v ($0, $BASH_SOURCE)" ')
process 29301 is sourced (bash, /dev/fd/63)

Si noti che il numero di processo non cambia mentre il processo rimane di provenienza :

echo $$
29301

Perché non usare il $_ == $0confronto

Per garantire molti casi, inizio a scrivere un vero script:

#!/bin/bash

# As $_ could be used only once, uncomment one of two following lines

#printf '_="%s", 0="%s" and BASH_SOURCE="%s"\n' "$_" "$0" "$BASH_SOURCE"
[[ "$_" != "$0" ]] && DW_PURPOSE=sourced || DW_PURPOSE=subshell

[ "$0" = "$BASH_SOURCE" ] && BASH_KIND_ENV=own || BASH_KIND_ENV=sourced;
echo "proc: $$[ppid:$PPID] is $BASH_KIND_ENV (DW purpose: $DW_PURPOSE)"

Copia questo in un file chiamato testscript:

cat >testscript   
chmod +x testscript

Ora potremmo testare:

./testscript 
proc: 25758[ppid:24890] is own (DW purpose: subshell)

Va bene.

. ./testscript 
proc: 24890[ppid:24885] is sourced (DW purpose: sourced)

source ./testscript 
proc: 24890[ppid:24885] is sourced (DW purpose: sourced)

Va bene.

Ma, per testare uno script prima di aggiungere -xflag:

bash ./testscript 
proc: 25776[ppid:24890] is own (DW purpose: sourced)

O per usare variabili predefinite:

env PATH=/tmp/bintemp:$PATH ./testscript 
proc: 25948[ppid:24890] is own (DW purpose: sourced)

env SOMETHING=PREDEFINED ./testscript 
proc: 25972[ppid:24890] is own (DW purpose: sourced)

Questo non funzionerà più.

Lo spostamento dei commenti dalla 5a alla 6a risposta darebbe una risposta più leggibile:

./testscript 
_="./testscript", 0="./testscript" and BASH_SOURCE="./testscript"
proc: 26256[ppid:24890] is own

. testscript 
_="_filedir", 0="bash" and BASH_SOURCE="testscript"
proc: 24890[ppid:24885] is sourced

source testscript 
_="_filedir", 0="bash" and BASH_SOURCE="testscript"
proc: 24890[ppid:24885] is sourced

bash testscript 
_="/bin/bash", 0="testscript" and BASH_SOURCE="testscript"
proc: 26317[ppid:24890] is own

env FILE=/dev/null ./testscript 
_="/usr/bin/env", 0="./testscript" and BASH_SOURCE="./testscript"
proc: 26336[ppid:24890] is own

Più forte: adesso...

Come non uso molto, dopo qualche lettura sulla pagina man, ci sono i miei tentativi:

#!/bin/ksh

set >/tmp/ksh-$$.log

Copia questo in un testfile.ksh:

cat >testfile.ksh
chmod +x testfile.ksh

Di eseguirlo due volte:

./testfile.ksh
. ./testfile.ksh

ls -l /tmp/ksh-*.log
-rw-r--r-- 1 user user   2183 avr 11 13:48 /tmp/ksh-9725.log
-rw-r--r-- 1 user user   2140 avr 11 13:48 /tmp/ksh-9781.log

echo $$
9725

e vedi:

diff /tmp/ksh-{9725,9781}.log | grep ^\> # OWN SUBSHELL:
> HISTCMD=0
> PPID=9725
> RANDOM=1626
> SECONDS=0.001
>   lineno=0
> SHLVL=3

diff /tmp/ksh-{9725,9781}.log | grep ^\< # SOURCED:
< COLUMNS=152
< HISTCMD=117
< LINES=47
< PPID=9163
< PS1='$ '
< RANDOM=29667
< SECONDS=23.652
<   level=1
<   lineno=1
< SHLVL=2

C'è una variabile ereditata in una corsa di provenienza , ma nulla di veramente correlato ...

Puoi anche verificare che $SECONDSsia vicino 0.000, ma questo garantisce solo casi di provenienza manuale ...

Potresti anche provare a verificare cos'è il genitore:

Metti questo nel tuo testfile.ksh:

ps $PPID

Di:

./testfile.ksh
  PID TTY      STAT   TIME COMMAND
32320 pts/4    Ss     0:00 -ksh

. ./testfile.ksh
  PID TTY      STAT   TIME COMMAND
32319 ?        S      0:00 sshd: user@pts/4

o ps ho cmd $PPID, ma questo funziona solo per un livello di sottosessioni ...

Mi dispiace, non sono riuscito a trovare un modo affidabile per farlo, sotto .


[ "$0" = "$BASH_SOURCE" ] || [ -z "$BASH_SOURCE" ]per gli script letti tramite pipe ( cat script | bash).
Hakre,

2
Nota che .non è un alias per source, in realtà è il contrario. source somescript.shè un Bash-ism e non è portatile, . somescript.shè POSIX e IIRC portatile.
dragon788,

32

La BASH_SOURCE[]risposta (bash-3.0 e successive) sembra più semplice, anche se BASH_SOURCE[]è non è documentato al lavoro al di fuori corpo di una funzione (che attualmente succede a lavoro, in disaccordo con la pagina man).

Il modo più robusto, come suggerito da Wirawan Purwanto, è controllare FUNCNAME[1] all'interno di una funzione :

function mycheck() { declare -p FUNCNAME; }
mycheck

Poi:

$ bash sourcetest.sh
declare -a FUNCNAME='([0]="mycheck" [1]="main")'
$ . sourcetest.sh
declare -a FUNCNAME='([0]="mycheck" [1]="source")'

Ciò equivale a controllare l'output di caller, i valori maine sourcedistinguere il contesto del chiamante. L'utilizzo FUNCNAME[]consente di salvare l'acquisizione e l'analisi callerdell'output. Tuttavia, è necessario conoscere o calcolare la profondità della chiamata locale per essere corretti. Casi come uno script proveniente da un'altra funzione o script causeranno una maggiore profondità dell'array (stack). ( FUNCNAMEè una speciale variabile di array bash, dovrebbe avere indici contigui corrispondenti allo stack di chiamate, purché non sia mai unset.)

function issourced() {
    [[ ${FUNCNAME[@]: -1} == "source" ]]
}

(In bash-4.2 e versioni successive è possibile utilizzare ${FUNCNAME[-1]}invece il modulo più semplice per l'ultimo elemento dell'array. Migliorato e semplificato grazie al commento di Dennis Williamson di seguito.)

Tuttavia, il tuo problema, come affermato, è " Ho uno script in cui non voglio che si chiami 'exit' se viene fornito ". Il bashlinguaggio comune per questa situazione è:

return 2>/dev/null || exit

Se lo script viene originato return, termina lo script originato e torna al chiamante.

Se lo script viene eseguito, returnrestituirà un errore (reindirizzato) e exitterminerà lo script normalmente. Entrambi returne exitpossono prendere un codice di uscita, se necessario.

Purtroppo, questo non funziona ksh(almeno non nella versione derivata AT&T che ho qui), considera returnequivalente a exitse invocato al di fuori di una funzione o di uno script di origine.

Aggiornato : quello che puoi fare nelle versioni contemporanee di kshè controllare la variabile speciale .sh.levelche è impostata sulla profondità della chiamata di funzione. Per uno script invocato questo inizialmente non sarà impostato, per uno script di origine dot sarà impostato su 1.

function issourced {
    [[ ${.sh.level} -eq 2 ]]
}

issourced && echo this script is sourced

Questo non è abbastanza robusto come la versione bash, è necessario invocare issourced()il file che si sta testando al livello più alto o con una profondità di funzione nota.

(Potresti anche essere interessato a questo codice su github che utilizza una kshfunzione di disciplina e alcuni stratagemmi di debug trap per emulare l' FUNCNAMEarray bash .)

La risposta canonica qui: http://mywiki.wooledge.org/BashFAQ/109 offre anche un $-altro indicatore (sebbene imperfetto) dello stato della shell.


Appunti:

  • è possibile creare funzioni bash denominate "main" e "source" ( sovrascrivendo l'integrato ), questi nomi possono apparire in FUNCNAME[]ma fintanto che viene testato solo l'ultimo elemento in quell'array non c'è ambiguità.
  • Non ho una buona risposta per pdksh. La cosa più vicina che posso trovare si applica solo a pdksh, in cui ogni provenienza di uno script apre un nuovo descrittore di file (a partire da 10 per lo script originale). Quasi certamente non qualcosa su cui vuoi fare affidamento ...

Che ne dici ${FUNCNAME[(( ${#FUNCNAME[@]} - 1 ))]}di mettere l'ultimo (in basso) oggetto nello stack? Quindi il test contro "main" (negare per OP) è stato il più affidabile per me.
Adrian Günter,

Se ho un PROMPT_COMMANDset, quello che appare come l'ultimo indice FUNCNAMEdell'array se corro source sourcetest.sh. Invertendo il controllo (alla ricerca di maincome l'ultimo indice) sembra più robusta: is_main() { [[ ${FUNCNAME[@]: -1} == "main" ]]; }.
dimo414,

1
La man-page afferma che FUNCNAMEè disponibile solo nelle funzioni. Secondo i miei test con declare -p FUNCNAME, bashsi comporta diversamente. v4.3 fornisce un errore al di fuori delle funzioni, mentre v4.4 fornisce declare -a FUNCNAME. Entrambi (!) Ritornano mainper ${FUNCNAME[0]}nello script principale (se eseguito) mentre $FUNCNAMEnon danno nulla. E: Ci sono così tanti script "ab" che usano $BASH_SOURCEfunzioni esterne, che dubito che questo possa o sarà cambiato.
Tino,

24

Nota del redattore: la soluzione di questa risposta funziona in modo robusto, ma è bash-solo. Può essere semplificato
(return 2>/dev/null).

TL; DR

Prova a eseguire returnun'istruzione. Se lo script non proviene, ciò genererà un errore. È possibile rilevare tale errore e procedere come necessario.

Mettilo in un file e chiamalo, ad esempio, test.sh:

#!/usr/bin/env sh

# Try to execute a `return` statement,
# but do it in a sub-shell and catch the results.
# If this script isn't sourced, that will raise an error.
$(return >/dev/null 2>&1)

# What exit code did that give?
if [ "$?" -eq "0" ]
then
    echo "This script is sourced."
else
    echo "This script is not sourced."
fi

Eseguilo direttamente:

shell-prompt> sh test.sh
output: This script is not sourced.

Fonte:

shell-prompt> source test.sh
output: This script is sourced.

Per me, questo funziona in zsh e bash.

Spiegazione

L' returnistruzione genererà un errore se si tenta di eseguirla al di fuori di una funzione o se lo script non è di provenienza. Prova questo da un prompt della shell:

shell-prompt> return
output: ...can only `return` from a function or sourced script

Non è necessario visualizzare quel messaggio di errore, quindi è possibile reindirizzare l'output su dev / null:

shell-prompt> return >/dev/null 2>&1

Ora controlla il codice di uscita. 0 significa OK (non si sono verificati errori), 1 significa che si è verificato un errore:

shell-prompt> echo $?
output: 1

Si desidera anche eseguire l' returnistruzione all'interno di una sotto-shell. Quando returnviene eseguita la dichiarazione. . . bene . . . ritorna. Se lo esegui in una sub-shell, tornerà fuori da quella sub-shell, anziché tornare fuori dallo script. Per eseguire nella sotto-shell, avvolgerlo in $(...):

shell-prompt> $(return >/dev/null 2>$1)

Ora puoi vedere il codice di uscita della sotto-shell, che dovrebbe essere 1, perché all'interno della sotto-shell è stato generato un errore:

shell-prompt> echo $?
output: 1

Ciò fallisce per me in 0.5.8-2.1ubuntu2 $ readlink $(which sh) dash $ . test.sh This script is sourced. $ ./test.sh This script is sourced.
Phil Rutschman,

3
POSIX non specifica cosa returndovrebbe fare al massimo livello ( pubs.opengroup.org/onlinepubs/9699919799/utilities/… ). La dashshell considera un returnlivello superiore come exit. Altre shell gradiscono basho zshnon consentono returnal livello superiore, che è la caratteristica che una tecnica come questa sfrutta.
user5754163,

Funziona in sh se si rimuove $prima la subshell. Cioè, usa (return >/dev/null 2>&1)invece di $(return >/dev/null 2>&1)- ma poi smette di funzionare in bash.
Eponimo

@Eponimo: poiché dash, dove questa soluzione non funziona, agisce come shsu Ubuntu, ad esempio, questa soluzione generalmente non funziona sh. La soluzione funziona per me in Bash 3.2.57 e 4.4.5 - con o senza il $prima (...)(anche se non c'è mai una buona ragione per $).
mklement0

2
returning senza un valore di ritorno di explocit si interrompe quando si inseriscono sourcegli script subito dopo un comando errato. Ha proposto la modifica del miglioramento.
DimG

12

FWIW, dopo aver letto tutte le altre risposte, ho trovato la seguente soluzione per me:

Aggiornamento: In realtà, qualcuno ha individuato un errore corretto in un'altra risposta che ha colpito anche il mio. Penso che anche qui l'aggiornamento sia un miglioramento (vedi le modifiche se sei curioso).

Funziona con tutti gli script, che iniziano con#!/bin/bash ma potrebbero essere forniti anche da shell diverse per apprendere alcune informazioni (come le impostazioni) che sono mantenute al di fuori della mainfunzione.

Secondo i commenti qui sotto, questa risposta qui apparentemente non funziona per tutte le bashvarianti. Inoltre, non per i sistemi su cui /bin/shsi basa bash. IE non riesce per bashv3.x su MacOS. (Attualmente non so come risolverlo.)

#!/bin/bash

# Function definitions (API) and shell variables (constants) go here
# (This is what might be interesting for other shells, too.)

# this main() function is only meant to be meaningful for bash
main()
{
# The script's execution part goes here
}

BASH_SOURCE=".$0" # cannot be changed in bash
test ".$0" != ".$BASH_SOURCE" || main "$@"

Invece delle ultime 2 righe puoi usare il seguente codice (a mio avviso meno leggibile) per non impostare BASH_SOURCEin altre shell e consentire set -edi lavorare in main:

if ( BASH_SOURCE=".$0" && exec test ".$0" != ".$BASH_SOURCE" ); then :; else main "$@"; fi

Questa ricetta di script ha le seguenti proprietà:

  • Se eseguito in bashmodo normale, mainviene chiamato. Si noti che ciò non include una chiamata come bash -x script(dove scriptnon contiene un percorso), vedere di seguito.

  • Se proveniente da bash, mainviene chiamato solo se lo script chiamante ha lo stesso nome. (Ad esempio, se si fonte da solo o attraverso bash -c 'someotherscript "$@"' main-script args..dove main-scriptdeve essere, ciò che testvede come $BASH_SOURCE).

  • Se proveniente / eseguito / letto / evaled da una shell diversa da bash, mainnon viene chiamato ( BASH_SOURCEè sempre diverso da $0).

  • mainnon viene chiamato se bashlegge lo script da stdin, a meno che tu non abbia impostato $0la stringa vuota in questo modo:( exec -a '' /bin/bash ) <script

  • Se valutato da bashcon eval ( eval "`cat script`" tutte le virgolette sono importanti! ) Da qualche altro script, questo chiama main. Se evalviene eseguito direttamente dalla riga di comando, è simile al caso precedente, in cui lo script viene letto da stdin. ( BASH_SOURCEè vuoto, mentre di $0solito è /bin/bashse non costretto a qualcosa di completamente diverso.)

  • Se mainnon viene chiamato, restituisce true( $?=0).

  • Questo non si basa su comportamenti imprevisti (in precedenza ho scritto senza documenti, ma non ho trovato documentazione che non è possibile unsetné modificare BASH_SOURCE):

    • BASH_SOURCEè un array riservato bash . Ma permettere BASH_SOURCE=".$0"di cambiarlo aprirebbe una lattina molto pericolosa di worm, quindi la mia aspettativa è che questo non debba avere alcun effetto (tranne, forse, qualche brutto avvertimento apparirà in qualche versione futura di bash).
    • Non esiste documentazione che BASH_SOURCEfunzioni al di fuori delle funzioni. Tuttavia, l'opposto (che funziona solo nelle funzioni) non è né documentato. L'osservazione è che funziona (testato con bashv4.3 e v4.4, sfortunatamente non ho più bashv3.x) e che troppi script si rompono se $BASH_SOURCEsmette di funzionare come osservato. Quindi la mia aspettativa è che BASH_SOURCErimanga così anche per le versioni future di bash.
    • Al contrario (bella scoperta, BTW!) Considera ( return 0 ), che dà 0se di provenienza e 1se non di provenienza. Questo è un po 'inaspettato non solo per me , e (secondo le letture lì) POSIX dice che returnda subshell è un comportamento indefinito (e che returnqui è chiaramente da una subshell). Forse questa funzione alla fine ottiene un uso abbastanza diffuso da non poter più essere modificata, ma AFAICS ha una probabilità molto più alta che qualche futura bashversione accidentale cambi il comportamento di ritorno in quel caso.
  • Purtroppo bash -x script 1 2 3non funziona main. (Confronta script 1 2 3dove scriptnon ha percorso). Di seguito può essere utilizzato come soluzione alternativa:

    • bash -x "`which script`" 1 2 3
    • bash -xc '. script' "`which script`" 1 2 3
    • Quello bash script 1 2 3che non funziona mainpuò essere considerato una caratteristica.
  • Si noti che ( exec -a none script )chiama main( bashnon passa è $0allo script, per questo è necessario utilizzare -ccome mostrato nell'ultimo punto).

Pertanto, ad eccezione di alcuni casi angolari, mainviene chiamato solo quando lo script viene eseguito nel solito modo. Normalmente questo è quello che vuoi, soprattutto perché manca di codice complesso difficile da capire.

Nota che è molto simile al codice Python:

if __name__ == '__main__': main()

Ciò impedisce anche di chiamare main, ad eccezione di alcuni casi angolari, in quanto è possibile importare / caricare lo script e applicarlo__name__='__main__'

Perché penso che questo sia un buon modo generale per risolvere la sfida

Se hai qualcosa, che può essere fornito da più shell, deve essere compatibile. Tuttavia (leggi le altre risposte), poiché non esiste un modo portatile (facile da implementare) per rilevare l' sourceing, è necessario modificare le regole .

Applicando che lo script debba essere eseguito da /bin/bash, lo fai esattamente.

Questo risolve tutti i casi, ma in seguito nel qual caso lo script non può essere eseguito direttamente:

  • /bin/bash non è installato o non funziona (ad es. in un ambiente di boot)
  • Se lo installi a una shell come in curl https://example.com/script | $SHELL
  • (Nota: questo è vero solo se il tuo bashè abbastanza recente. Si dice che questa ricetta fallisca per alcune varianti. Quindi assicurati di controllare che funzioni per il tuo caso.)

Tuttavia non riesco a pensare a nessuna vera ragione per cui ne hai bisogno e anche alla possibilità di procurarti lo stesso script esattamente in parallelo! Di solito è possibile avvolgerlo per eseguire mainmanualmente. Come quello:

  • $SHELL -c '. script && main'
  • { curl https://example.com/script && echo && echo main; } | $SHELL
  • $SHELL -c 'eval "`curl https://example.com/script`" && main'
  • echo 'eval "`curl https://example.com/script`" && main' | $SHELL

Appunti

  • Questa risposta non sarebbe stata possibile senza l'aiuto di tutte le altre risposte! Anche quelli sbagliati - che inizialmente mi hanno fatto pubblicare questo.

  • Aggiornamento: modificato a causa delle nuove scoperte trovate in https://stackoverflow.com/a/28776166/490291


Testato per ksh e bash-4.3. Bello. È un peccato che la tua risposta avrà una vita difficile dato che le altre risposte hanno già avuto anni accumulando voti positivi.
hagello,

grazie per questa risposta. Ho apprezzato il test più lungo, "meno leggibile" con l'istruzione IF poiché è bello gestire entrambe le situazioni per almeno dare un errore non silenzioso. Nel mio caso ho bisogno di uno script per essere reperito o altrimenti informare l'utente del loro errore nel non usare il sorgente.
Tim Richardson,

@Tino: Per quanto riguarda "potrebbe essere fornito anche da diverse shell": Su macOS, dove si /bin/shtrova effettivamente bashin modalità POSIX, l'assegnazione a BASH_SOURCE interruzioni dello script. In altre shell ( dash, ksh, zsh), invocando lo script passando come argomento di file direttamente all'eseguibile shell malfunzionamenti (ad esempio, zsh <your-script>renderà il vostro scritto erroneamente pensare che è di provenienza ). (Hai già detto che il piping del codice non funziona correttamente, in tutte le shell.)
mklement0

@Tino: A parte: sebbene . <your-script>(il sourcing) funzioni da tutte le shell simili a POSIX in linea di principio, ha senso solo se lo script è stato scritto esplicitamente per utilizzare solo le funzionalità POSIX, in modo da impedire che funzioni specifiche di una shell interrompano l'esecuzione in altre conchiglie; l'uso di una linea Bash shebang (piuttosto che #!/bin/sh) è quindi fonte di confusione, almeno senza un commento evidente. Viceversa, se lo script deve essere eseguito solo da Bash (anche solo per non considerare quali funzionalità potrebbero non essere portatili), è meglio rifiutare l' esecuzione in shell non Bash.
mklement0

1
@ mklement0 Grazie ancora, aggiunta una nota che c'è un problema. Per altri lettori: se fornito con bash v3.x non dovrebbe essere eseguito main, ma in questo caso lo fa! E quando proviene da /bin/sh, che è bash --posix, lo stesso accade in questo caso, e anche questo è chiaramente sbagliato.
Tino,

6

Funziona più avanti nello script e non dipende dalla variabile _:

## Check to make sure it is not sourced:
Prog=myscript.sh
if [ $(basename $0) = $Prog ]; then
   exit 1  # not sourced
fi

o

[ $(basename $0) = $Prog ] && exit

1
Penso che questa risposta sia una delle poche POSIX compatibili qui. Con gli ovvi svantaggi è che devi conoscere il nome file e non funziona se entrambi gli script hanno lo stesso nome file.
JepZ

5

Darò una risposta specifica per BASH. Korn shell, scusa. Supponiamo che il nome del tuo script sia include2.sh; quindi crea una funzione all'interno del include2.shchiamato am_I_sourced. Ecco la mia versione demo di include2.sh:

am_I_sourced()
{
  if [ "${FUNCNAME[1]}" = source ]; then
    if [ "$1" = -v ]; then
      echo "I am being sourced, this filename is ${BASH_SOURCE[0]} and my caller script/shell name was $0"
    fi
    return 0
  else
    if [ "$1" = -v ]; then
      echo "I am not being sourced, my script/shell name was $0"
    fi
    return 1
  fi
}

if am_I_sourced -v; then
  echo "Do something with sourced script"
else
  echo "Do something with executed script"
fi

Ora prova a eseguirlo in molti modi:

~/toys/bash $ chmod a+x include2.sh

~/toys/bash $ ./include2.sh 
I am not being sourced, my script/shell name was ./include2.sh
Do something with executed script

~/toys/bash $ bash ./include2.sh 
I am not being sourced, my script/shell name was ./include2.sh
Do something with executed script

~/toys/bash $ . include2.sh
I am being sourced, this filename is include2.sh and my caller script/shell name was bash
Do something with sourced script

Quindi funziona senza eccezioni e non utilizza le $_cose fragili . Questo trucco utilizza la funzione di introspezione di BASH, ovvero variabili integrate FUNCNAMEe BASH_SOURCE; vedere la loro documentazione nella pagina del manuale di bash.

Solo due avvertimenti:

1) la chiamata a am_I_called deve avvenire nello script di provenienza, ma non all'interno di alcuna funzione, per non ${FUNCNAME[1]}restituire qualcos'altro. Sì ... avresti potuto controllare ${FUNCNAME[2]}- ma hai solo reso la tua vita più difficile.

2) La funzione am_I_called deve risiedere nello script di origine se si desidera scoprire quale sia il nome del file da includere.


1
Chiarimento: questa funzione richiede BASH versione 3+ per funzionare. In BASH 2, FUNCNAME è una variabile scalare anziché un array. Inoltre BASH 2 non ha una variabile array BASH_SOURCE.
Wirawan Purwanto,

4

Vorrei suggerire una piccola correzione alla risposta molto utile di Dennis , per renderla leggermente più portatile, spero:

[ "$_" != "$0" ] && echo "Script is being sourced" || echo "Script is a subshell"

perché [[non è riconosciuto dal (alquanto anale ritentiva IMHO) Debian POSIX compatibile shell, dash. Inoltre, potrebbe essere necessario che le virgolette proteggano da nomi di file contenenti spazi, sempre in detta shell.


2

$_è piuttosto fragile. Devi controllarlo come la prima cosa che fai nella sceneggiatura. E anche in questo caso, non è garantito che contenga il nome della shell (se di provenienza) o il nome dello script (se eseguito).

Ad esempio, se l'utente ha impostato BASH_ENV, quindi nella parte superiore di uno script, $_contiene il nome dell'ultimo comando eseguito nello BASH_ENVscript.

Il modo migliore che ho trovato è usare in $0questo modo:

name="myscript.sh"

main()
{
    echo "Script was executed, running main..."
}

case "$0" in *$name)
    main "$@"
    ;;
esac

Sfortunatamente, in questo modo in zsh non funziona così a causa functionargzerodell'opzione che fa più di quanto suggerisce il suo nome e che è attivo di default.

Per ovviare a questo, ho inserito il unsetopt functionargzeromio .zshenv.


1

Ho seguito l' espressione compatta di mklement0 .

È pulito, ma ho notato che può fallire nel caso di ksh quando viene invocato come questo:

/bin/ksh -c ./myscript.sh

(pensa che sia di provenienza e non perché esegua una subshell) Ma l'espressione funzionerà per rilevare questo:

/bin/ksh ./myscript.sh

Inoltre, anche se l'espressione è compatta, la sintassi non è compatibile con tutte le shell.

Quindi ho concluso con il seguente codice, che funziona per bash, zsh, dash e ksh

SOURCED=0
if [ -n "$ZSH_EVAL_CONTEXT" ]; then 
    [[ $ZSH_EVAL_CONTEXT =~ :file$ ]] && SOURCED=1
elif [ -n "$KSH_VERSION" ]; then
    [[ "$(cd $(dirname -- $0) && pwd -P)/$(basename -- $0)" != "$(cd $(dirname -- ${.sh.file}) && pwd -P)/$(basename -- ${.sh.file})" ]] && SOURCED=1
elif [ -n "$BASH_VERSION" ]; then
    [[ $0 != "$BASH_SOURCE" ]] && SOURCED=1
elif grep -q dash /proc/$$/cmdline; then
    case $0 in *dash*) SOURCED=1 ;; esac
fi

Sentiti libero di aggiungere il supporto di conchiglie esotiche :)


In ksh 93+u, ksh ./myscript.shfunziona bene per me (con la mia dichiarazione) - quale versione stai usando?
mklement0

Temo che non ci sia modo di determinare in modo affidabile se uno script proviene solo utilizzando le funzionalità POSIX: il tuo tentativo assume Linux ( /proc/$$/cmdline) e si concentra dashsolo su (che agisce anche come shsu Ubuntu, ad esempio). Se sei disposto a formulare alcune ipotesi, puoi esaminare $0un test ragionevole, ma incompleto, portatile.
mklement0

++ per l'approccio di base, però - mi sono preso la libertà di adattarlo per quella che penso sia la migliore approssimazione portatile di supporto sh/ dashpure, in un addendum alla mia risposta.
mklement0

0

Non penso che ci sia un modo portatile per farlo sia in ksh che in bash. In bash potresti rilevarlo usando l' calleroutput, ma non credo che esista in ksh equivalente.


$0lavori in bash, ksh93e pdksh. Non devo ksh88testare.
Mikel,

0

Avevo bisogno di un one-liner che funzioni su [mac, linux] con bash.version> = 3 e nessuna di queste risposte è adatta al conto.

[[ ${BASH_SOURCE[0]} = $0 ]] && main "$@"

1
La bashsoluzione funziona bene (è possibile semplificare $BASH_SOURCE), ma la kshsoluzione non è solida: se il tuo script viene fornito da un altro script , otterrai un falso positivo.
mklement0,

0

Dritto al punto: devi valutare se la variabile "$ 0" è uguale al nome della tua Shell.


Come questo:

#!/bin/bash

echo "First Parameter: $0"
echo
if [[ "$0" == "bash" ]] ; then
    echo "The script was sourced."
else
    echo "The script WAS NOT sourced."
fi


Tramite SHELL :

$ bash check_source.sh 
First Parameter: check_source.sh

The script WAS NOT sourced.

Via SOURCE :

$ source check_source.sh
First Parameter: bash

The script was sourced.



È abbastanza difficile avere un modo portatile al 100% di rilevare se uno script è stato fornito o meno.

Per quanto riguarda la mia esperienza (7 anni con Shellscripting) , l'unico modo sicuro (non fare affidamento su variabili di ambiente con PID e così via, che non è sicuro a causa del fatto che è qualcosa di VARIABILE ), dovresti:

  • estendi le possibilità dal tuo if
  • usando switch / case, se vuoi.

Entrambe le opzioni non possono essere ridimensionate automaticamente, ma è il modo più sicuro.



Per esempio:

quando si genera uno script tramite una sessione SSH, il valore restituito dalla variabile "$ 0" (quando si utilizza l' origine ) è -bash .

#!/bin/bash

echo "First Parameter: $0"
echo
if [[ "$0" == "bash" || "$0" == "-bash" ]] ; then
    echo "The script was sourced."
else
    echo "The script WAS NOT sourced."
fi

O

#!/bin/bash

echo "First Parameter: $0"
echo
if [[ "$0" == "bash" ]] ; then
    echo "The script was sourced."
elif [[ "$0" == "-bash" ]] ; then
    echo "The script was sourced via SSH session."
else
    echo "The script WAS NOT sourced."
fi

2
Sottovalutato, poiché questo è chiaramente sbagliato: /bin/bash -c '. ./check_source.sh'The script WAS NOT sourced.. Stesso bug: ln -s /bin/bash pumuckl; ./pumuckl -c '. ./check_source.sh'->The script WAS NOT sourced.
Tino

2
Il tuo downvote ha cambiato l'intero scenario e ha dato un grande contributo, Tino. Grazie!
Ivanleoncz,

0

Ho finito con il controllo [[ $_ == "$(type -p "$0")" ]]

if [[ $_ == "$(type -p "$0")" ]]; then
    echo I am invoked from a sub shell
else
    echo I am invoked from a source command
fi

Quando si usa curl ... | bash -s -- ARGSper eseguire lo script remoto al volo, $ 0 sarà solo al bashposto del normale /bin/bashquando si esegue il file di script effettivo, quindi uso type -p "$0"per mostrare il percorso completo di bash.

test:

curl -sSL https://github.com/jjqq2013/bash-scripts/raw/master/common/relpath | bash -s -- /a/b/c/d/e /a/b/CC/DD/EE

source <(curl -sSL https://github.com/jjqq2013/bash-scripts/raw/master/common/relpath)
relpath /a/b/c/d/e /a/b/CC/DD/EE

wget https://github.com/jjqq2013/bash-scripts/raw/master/common/relpath
chmod +x relpath
./relpath /a/b/c/d/e /a/b/CC/DD/EE

0

Questo è uno spin-off di alcune altre risposte, per quanto riguarda il supporto cross shell "universale". Questo è certamente molto simile a https://stackoverflow.com/a/2942183/3220983 in particolare, anche se leggermente diverso. Il punto debole di questo è che uno script client deve rispettare il modo in cui usarlo (ovvero esportando prima una variabile). Il punto di forza è che questo è semplice e dovrebbe funzionare "ovunque". Ecco un modello per il tuo piacere di tagliare e incollare:

# NOTE: This script may be used as a standalone executable, or callable library.
# To source this script, add the following *prior* to including it:
# export ENTRY_POINT="$0"

main()
{
    echo "Running in direct executable context!"
}

if [ -z "${ENTRY_POINT}" ]; then main "$@"; fi

Nota: uso exportsolo per essere sicuro che questo meccanismo possa essere esteso in sottoprocessi.

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.