Cara mendeteksi jika skrip sedang dibuat


217

Saya memiliki skrip di mana saya tidak ingin memanggilnya exitjika bersumber.

Saya berpikir untuk memeriksa apakah $0 == bashtetapi ini memiliki masalah jika skrip tersebut bersumber dari skrip lain, atau jika pengguna sumbernya dari shell yang berbeda suka ksh.

Apakah ada cara yang dapat diandalkan untuk mendeteksi jika skrip sedang bersumber?


2
Saya memiliki masalah yang sama beberapa waktu lalu dan menyelesaikannya dengan menghindari 'keluar' dalam semua kasus; "kill -INT $$" mengakhiri skrip dengan aman.
JESii

1
Apakah Anda memperhatikan jawaban ini ? Ini diberikan 5 tahun kemudian dari yang diterima, tetapi memiliki "baterai termasuk".
raratiru

Jawaban:


72

Ini tampaknya portabel antara Bash dan Korn:

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

Baris yang mirip dengan ini atau tugas seperti pathname="$_"(dengan tes dan tindakan nanti) harus di baris pertama skrip atau di baris setelah shebang (yang, jika digunakan, harus untuk ksh agar dapat bekerja di bawah sebagian besar keadaan).


10
Sayangnya itu tidak dijamin berhasil. Jika pengguna telah menetapkan BASH_ENV, $_di bagian atas skrip akan menjadi perintah terakhir dijalankan BASH_ENV.
Mikel

30
Ini juga tidak akan berfungsi jika Anda menggunakan bash untuk mengeksekusi skrip, misalnya $ bash script.sh maka $ _ akan menjadi / bin / bash alih-alih ./script.sh, yang merupakan kasus yang Anda harapkan, ketika skrip dipanggil dengan cara ini: $ ./script.sh Dalam hal apa pun mendeteksi dengan $_merupakan masalah.
Wirawan Purwanto

2
Tes tambahan dapat dimasukkan untuk memeriksa metode doa tersebut.
Dijeda sampai pemberitahuan lebih lanjut.

8
Sayangnya, itu salah! lihat jawaban saya
F. Hauri

8
Untuk meringkas: Meskipun pendekatan ini biasanya bekerja, itu tidak kuat ; gagal dalam 2 skenario berikut: (a) bash script(doa melalui executable shell, yang solusi ini salah laporkan sebagai sumbernya ), dan (b) (jauh lebih kecil kemungkinannya) echo bash; . script(jika $_kebetulan cocok dengan shell yang mengambil skrip, solusi ini salah melaporkannya sebagai sebuah subkulit ). Hanya variabel khusus spesifik-shell (mis., $BASH_SOURCE) Yang memungkinkan solusi yang tangguh (berarti tidak ada solusi yang sesuai dengan POSIX). Hal ini dimungkinkan, meskipun rumit, untuk kerajinan tes lintas shell kuat.
mklement0

170

Jika versi Bash Anda tahu tentang variabel array BASH_SOURCE, coba sesuatu seperti:

# 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
Itu mungkin cara terbersih karena $ BASH_SOURCE ditujukan untuk tujuan itu.
con-f-use

4
Perhatikan bahwa ini tidak akan bekerja di bawah ksh yang merupakan kondisi yang ditentukan OP.
Dijeda sampai pemberitahuan lebih lanjut.

2
Apakah ada alasan untuk menggunakannya, ${BASH_SOURCE[0]}bukan hanya $BASH_SOURCE? Dan ${0}vs $0?
hraban

4
BASH_SOURCEadalah variabel array (lihat manual ) yang menyimpan jejak tumpukan sumber, di mana ${BASH_SOURCE[0]}yang terbaru. Kawat gigi digunakan di sini untuk memberi tahu bash apa yang merupakan bagian dari nama variabel. Mereka tidak diperlukan $0dalam kasus ini, tetapi mereka juga tidak terluka. ;)
Konrad

4
@ Konrad, dan jika Anda memperluas $array, Anda mendapatkan ${array[0]}secara default. Jadi, sekali lagi, apakah ada alasan [...]?
Charles Duffy

133

Solusi kuat untuk bash, ksh,zsh , termasuk cross-shell satu, ditambah solusi POSIX-compliant cukup kuat :

  • Nomor versi yang diberikan adalah yang di mana fungsionalitasnya diverifikasi - kemungkinan, solusi ini juga bekerja pada versi yang lebih awal, - umpan balik diterima .

  • Menggunakan fitur POSIX saja (seperti pada dash, yang bertindak sebagai /bin/shpada Ubuntu), tidak ada cara yang kuat untuk menentukan apakah skrip sedang bersumber - lihat di bawah untuk perkiraan terbaik .

Ikuti satu kalimat - penjelasan di bawah ini; versi cross-shell rumit, tetapi harus bekerja dengan baik:

  • bash (diverifikasi pada 3.57 dan 4.4.19)

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

    [[ $(cd "$(dirname -- "$0")" && 
       printf '%s' "${PWD%/}/")$(basename -- "$0") != "${.sh.file}" ]] &&
         sourced=1 || sourced=0
  • zsh (diverifikasi pada 5.0.5) - pastikan untuk memanggil ini di luar fungsi

    [[ $ZSH_EVAL_CONTEXT =~ :file$ ]] && sourced=1 || sourced=0
  • lintas-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
  • Sesuai dengan POSIX ; bukan satu-liner (pipa tunggal) karena alasan teknis dan tidak sepenuhnya kuat (lihat bawah):

    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

Penjelasan:


pesta

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

Catatan: Teknik ini diadaptasi dari jawaban pengguna5754163 , karena ternyata lebih kuat dari solusi aslinya, [[ $0 != "$BASH_SOURCE" ]] && sourced=1 || sourced=0[1]

  • Bash memungkinkan returnpernyataan hanya dari fungsi dan, dalam lingkup tingkat atas skrip, hanya jika skrip tersebut bersumber .

    • Jika returndigunakan dalam lingkup tingkat atas dari skrip non-sumber , pesan kesalahan dipancarkan, dan kode keluar diatur ke 1.
  • (return 0 2>/dev/null)dieksekusi returndalam subkulit dan menekan pesan kesalahan; setelah itu kode keluar menunjukkan apakah skrip bersumber ( 0) atau tidak ( 1), yang digunakan dengan &&dan ||operator untuk mengatur sourcedvariabel sesuai.

    • Diperlukan penggunaan subkulit, karena mengeksekusi returndalam lingkup tingkat atas dari skrip bersumber akan keluar dari skrip.
    • Tip dari topi untuk @Haozhun , yang membuat perintah lebih kuat dengan secara eksplisit menggunakan 0sebagai returnoperan; ia mencatat: per bash bantuan return [N]: "Jika N dihilangkan, status pengembalian adalah perintah terakhir." Akibatnya, versi sebelumnya [yang hanya digunakan return, tanpa operan] menghasilkan hasil yang salah jika perintah terakhir pada shell pengguna memiliki nilai balik yang tidak nol.

ksh

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

Variabel khusus ${.sh.file}agak analog dengan $BASH_SOURCE; perhatikan bahwa ${.sh.file}menyebabkan kesalahan sintaks di bash, zsh, dan dash, jadi pastikan untuk menjalankannya secara kondisional dalam skrip multi-shell.

Tidak seperti dalam bash, $0dan ${.sh.file}TIDAK dijamin persis sama dalam kasus non-bersumber, karena $0mungkin merupakan jalur relatif , sementara ${.sh.file}selalu jalur penuh , jadi $0harus diselesaikan ke jalur penuh sebelum membandingkan.


zsh

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

$ZSH_EVAL_CONTEXTberisi informasi tentang konteks evaluasi - sebut ini di luar fungsi. Di dalam skrip bersumber [lingkup tingkat atas], $ZSH_EVAL_CONTEXT diakhiri dengan :file.

Peringatan: Di dalam substitusi perintah, zsh menambahkan :cmdsubst, jadi uji $ZSH_EVAL_CONTEXTuntuk di :file:cmdsubst$sana.


Hanya menggunakan fitur POSIX

Jika Anda ingin membuat asumsi tertentu, Anda dapat membuat dugaan yang masuk akal, tetapi tidak bodoh, apakah skrip Anda dibuat, berdasarkan pada mengetahui nama file biner dari shell yang mungkin mengeksekusi skrip Anda .
Khususnya, ini berarti bahwa pendekatan ini gagal jika skrip Anda bersumber dari skrip lain .

Bagian "Cara menangani doa bersumber" dalam jawaban saya ini membahas kasus tepi yang tidak dapat ditangani dengan fitur POSIX hanya secara rinci.

Ini bergantung pada perilaku standar $0, yang zsh, misalnya, tidak menunjukkan.

Dengan demikian, pendekatan teraman adalah menggabungkan metode yang kuat dan spesifik-shell di atas dengan solusi fallback untuk semua shell yang tersisa.

Ujung topi untuk Stéphane Desneux dan jawabannya untuk inspirasi (mengubah ekspresi pernyataan lintas-shell saya menjadi pernyataan sh-compatible ifdan menambahkan handler untuk shell lain).

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 menemukan bahwa [[ $0 != "$BASH_SOURCE" ]]menghasilkan false positif ketika Anda mengeksekusi skrip yang terletak di$PATH dengan meneruskan nama file yang hanya ke bashbiner; misalnya bash my-script, karena$0 kemudian hanya my-script, sedangkan $BASH_SOURCEadalah path lengkap . Meskipun Anda biasanya tidak akan menggunakan teknik ini untuk menjalankan skrip di $PATH- Anda hanya akan memohonnya secara langsung ( my-script) - akan sangat membantu jika dikombinasikan dengan -xuntuk debugging .


1
pujian untuk jawaban yang begitu komprehensif.
DrUseful

75

Setelah membaca jawaban @ DennisWilliamson, ada beberapa masalah, lihat di bawah:

Seperti pertanyaan ini dan , ada bagian lain dalam jawaban ini mengenai ... Lihat di bawah.

Sederhana cara

[ "$0" = "$BASH_SOURCE" ]

Mari kita coba (dengan cepat karena pesta itu bisa ;-):

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)

sourceSebagai gantinya saya menggunakan off .untuk dibaca (seperti .alias untuk 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)

Perhatikan bahwa nomor proses tidak berubah saat proses tetap bersumber :

echo $$
29301

Mengapa tidak menggunakan $_ == $0perbandingan

Untuk memastikan banyak kasus, saya mulai menulis skrip yang benar :

#!/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)"

Salin ini ke file bernama testscript:

cat >testscript   
chmod +x testscript

Sekarang kita bisa menguji:

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

Tidak apa-apa.

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

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

Tidak apa-apa.

Tapi, untuk menguji skrip sebelum menambahkan -xtanda:

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

Atau untuk menggunakan variabel yang telah ditentukan:

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)

Ini tidak akan berfungsi lagi.

Memindahkan komentar dari baris 5 ke 6 akan memberikan jawaban yang lebih mudah dibaca:

./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

Lebih keras: sekarang...

Karena saya tidak menggunakan banyak, setelah beberapa membaca di halaman manual, ada mencoba saya:

#!/bin/ksh

set >/tmp/ksh-$$.log

Salin ini di testfile.ksh:

cat >testfile.ksh
chmod +x testfile.ksh

Daripada menjalankannya dua kali:

./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

Dan lihat:

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

Ada beberapa variabel yang diwarisi dalam proses yang bersumber , tetapi tidak ada yang benar-benar terkait ...

Anda bahkan dapat memeriksa yang $SECONDSdekat 0.000, tapi itu memastikan hanya kasus yang bersumber secara manual ...

Anda bahkan dapat mencoba memeriksa apakah orang tua itu:

Tempatkan ini di Anda testfile.ksh:

ps $PPID

Dari:

./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

atau ps ho cmd $PPID, tetapi ini hanya berfungsi untuk satu tingkat subsesi ...

Maaf, saya tidak dapat menemukan cara yang dapat diandalkan untuk melakukan itu, di bawah .


[ "$0" = "$BASH_SOURCE" ] || [ -z "$BASH_SOURCE" ]untuk skrip dibaca melalui pipa ( cat script | bash).
hakre

2
Perhatikan bahwa .ini bukan alias untuk source, sebenarnya sebaliknya. source somescript.shadalah Bash-isme dan tidak portabel, . somescript.shPOSIX dan portabel IIRC.
dragon788

32

The BASH_SOURCE[]jawaban (bash-3.0 dan kemudian) tampaknya sederhana, meskipun BASH_SOURCE[]ini tidak didokumentasikan untuk bekerja di luar tubuh fungsi (yang saat ini terjadi pada pekerjaan, dalam perselisihan dengan halaman manual).

Cara yang paling kuat, seperti yang disarankan oleh Wirawan Purwanto, adalah memeriksa FUNCNAME[1] dalam suatu fungsi :

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

Kemudian:

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

Ini setara dengan memeriksa output caller, nilai-nilai maindan sourcemembedakan konteks pemanggil. Menggunakan FUNCNAME[]save yang Anda ambil dan parsing calleroutput. Anda perlu tahu atau menghitung kedalaman panggilan lokal Anda untuk menjadi benar. Kasus-kasus seperti skrip yang bersumber dari dalam fungsi atau skrip lain akan menyebabkan array (tumpukan) menjadi lebih dalam. ( FUNCNAMEadalah variabel array bash khusus, harus memiliki indeks berdekatan yang sesuai dengan panggilan stack, asalkan tidak pernah unset.)

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

(Dalam bash-4.2 dan yang lebih baru Anda bisa menggunakan formulir yang lebih sederhana ${FUNCNAME[-1]}sebagai ganti untuk item terakhir dalam array. Lebih baik dan disederhanakan berkat komentar Dennis Williamson di bawah ini.)

Namun, masalah Anda sebagaimana dinyatakan adalah " Saya memiliki skrip di mana saya tidak ingin itu memanggil 'keluar' jika sedang bersumber ". bashUngkapan umum untuk situasi ini adalah:

return 2>/dev/null || exit

Jika skrip bersumber maka returnakan menghentikan skrip bersumber dan kembali ke pemanggil.

Jika skrip dieksekusi, maka returnakan mengembalikan kesalahan (dialihkan), dan exitakan menghentikan skrip seperti biasa. Keduanya returndan exitdapat mengambil kode keluar, jika diperlukan.

Sayangnya, ini tidak berfungsi ksh(setidaknya tidak dalam versi turunan AT&T yang saya miliki di sini), ini diperlakukan returnsebagai setara dengan exitjika dipanggil di luar fungsi atau skrip sumber-sumber.

Diperbarui : Apa yang dapat Anda lakukan dalam versi kontemporer kshadalah memeriksa variabel khusus .sh.levelyang diatur ke kedalaman panggilan fungsi. Untuk skrip yang dipanggil ini pada awalnya tidak akan disetel, untuk skrip bersumber titik akan disetel ke 1.

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

issourced && echo this script is sourced

Ini tidak sekuat versi bash, Anda harus menjalankan issourced()file yang Anda uji dari tingkat atas atau pada kedalaman fungsi yang diketahui.

(Anda mungkin juga tertarik dengan kode ini di github yang menggunakan kshfungsi disiplin dan beberapa tipuan perangkap debug untuk meniru bash FUNCNAMEarray.)

Jawaban kanonik di sini: http://mywiki.wooledge.org/BashFAQ/109 juga menawarkan $-sebagai indikator lain (meskipun tidak sempurna) dari status shell.


Catatan:

  • dimungkinkan untuk membuat fungsi bash bernama "main" dan "source" ( mengesampingkan builtin ), nama-nama ini dapat muncul FUNCNAME[]tetapi selama hanya item terakhir dalam array yang diuji tidak ada ambiguitas.
  • Saya tidak punya jawaban yang bagus untuk itu pdksh. Hal terdekat yang saya temukan hanya berlaku untuk pdksh, di mana setiap sumber skrip membuka file descriptor baru (dimulai dengan 10 untuk skrip asli). Hampir pasti bukan sesuatu yang ingin Anda andalkan ...

Bagaimana ${FUNCNAME[(( ${#FUNCNAME[@]} - 1 ))]}cara mendapatkan item (bawah) terakhir di tumpukan? Kemudian menguji terhadap "main" (meniadakan untuk OP) adalah yang paling dapat diandalkan untuk saya.
Adrian Günter

Jika saya memiliki satu PROMPT_COMMANDset, yang muncul sebagai indeks terakhir dari FUNCNAMEarray jika saya jalankan source sourcetest.sh. Pembalik cek (mencari mainsebagai indeks terakhir) tampaknya lebih kuat: is_main() { [[ ${FUNCNAME[@]: -1} == "main" ]]; }.
dimo414

1
Halaman manual menyatakan, bahwa FUNCNAMEhanya tersedia dalam fungsi. Menurut tes saya dengan declare -p FUNCNAME, bashberperilaku berbeda. v4.3 memberikan kesalahan di luar fungsi, sedangkan v4.4 memberi declare -a FUNCNAME. Kedua (!) Kembali mainuntuk ${FUNCNAME[0]}di script utama (jika dijalankan) saat $FUNCNAMEmemberikan apa-apa. Dan: Ada begitu banyak skrip di luar sana "ab" menggunakan $BASH_SOURCEfungsi luar, yang saya ragu ini dapat atau akan diubah.
Tino

24

Catatan editor: Solusi jawaban ini bekerja dengan baik, tetapi bashhanya -ya. Itu bisa disederhanakan menjadi
(return 2>/dev/null).

TL; DR

Cobalah untuk menjalankan returnpernyataan. Jika skrip tidak bersumber, itu akan menimbulkan kesalahan. Anda dapat menangkap kesalahan itu dan melanjutkan sesuai kebutuhan.

Masukkan ini ke dalam file dan sebut, katakan, 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

Jalankan secara langsung:

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

Sumbernya:

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

Bagi saya, ini berfungsi dalam zsh dan bash.

Penjelasan

The returnpernyataan akan meningkatkan kesalahan jika Anda mencoba untuk melaksanakannya di luar fungsi atau jika script tidak bersumber. Coba ini dari prompt shell:

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

Anda tidak perlu melihat pesan kesalahan itu, sehingga Anda dapat mengarahkan output ke dev / null:

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

Sekarang periksa kode keluar. 0 berarti OK (tidak ada kesalahan terjadi), 1 berarti terjadi kesalahan:

shell-prompt> echo $?
output: 1

Anda juga ingin menjalankan returnpernyataan di dalam sub-shell. Saat returnpernyataan itu dijalankan. . . yah. . . kembali. Jika Anda menjalankannya dalam sub-shell, itu akan kembali keluar dari sub-shell itu, bukan kembali dari skrip Anda. Untuk mengeksekusi di sub-shell, bungkus dalam $(...):

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

Sekarang, Anda dapat melihat kode keluar dari sub-shell, yang seharusnya 1, karena kesalahan muncul di dalam sub-shell:

shell-prompt> echo $?
output: 1

Ini gagal bagi saya di 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 tidak menentukan apa yang returnharus dilakukan di tingkat atas ( pubs.opengroup.org/onlinepubs/9699919799/utilities/… ). The dashshell memperlakukan seorang returndi tingkat atas sebagai exit. Kerang lain suka bashatau zshtidak memungkinkan returndi tingkat atas, yang merupakan fitur teknik seperti eksploitasi ini.
user5754163

Ini bekerja di sh jika Anda menghapus $sebelum subkulit. Artinya, gunakan (return >/dev/null 2>&1)bukannya $(return >/dev/null 2>&1)- tapi kemudian berhenti bekerja di bash.
Eponim

@Eponymous: Karena dash, di mana solusi ini tidak berfungsi, bertindak seperti shdi Ubuntu, misalnya, solusi ini biasanya tidak berfungsi sh. Solusinya tidak bekerja untuk saya di Bash 3.2.57 dan 4.4.5 - dengan atau tanpa $sebelumnya (...)(meskipun tidak pernah ada alasan bagus untuk itu $).
mklement0

2
returntanpa nilai pengembalian explcit terputus ketika sourceskrip tepat setelah perintah yang keluar buruk. Usulkan pengeditan peningkatan.
DimG

12

FWIW, setelah membaca semua jawaban lain, saya datang dengan solusi berikut untuk saya:

Pembaruan: Sebenarnya, seseorang melihat kesalahan yang telah diperbaiki sejak jawaban lain yang mempengaruhi saya juga. Saya pikir pembaruan di sini juga merupakan peningkatan (lihat suntingan jika Anda penasaran).

Ini berfungsi untuk semua skrip, yang dimulai dengan#!/bin/bash tetapi mungkin bersumber dari shell yang berbeda juga untuk mempelajari beberapa informasi (seperti pengaturan) yang disimpan di luar mainfungsi.

Menurut komentar di bawah, jawaban ini di sini tampaknya tidak bekerja untuk semua bashvarian. Juga tidak untuk sistem, di mana /bin/shberdasarkan bash. Yaitu gagal untuk bashv3.x pada MacOS. (Sekarang saya tidak tahu bagaimana menyelesaikan ini.)

#!/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 "$@"

Alih-alih 2 baris terakhir Anda dapat menggunakan kode berikut (menurut saya kurang dapat dibaca) untuk tidak mengatur BASH_SOURCEshell lain dan memungkinkan set -euntuk bekerja di main:

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

Resep-skrip ini memiliki sifat-sifat berikut:

  • Jika dijalankan dengan bashcara biasa, maindisebut. Harap dicatat bahwa ini tidak termasuk panggilan seperti bash -x script(di mana scripttidak mengandung jalur), lihat di bawah.

  • Jika bersumber oleh bash, mainhanya dipanggil, jika skrip panggilan kebetulan memiliki nama yang sama. (Misalnya, jika sumber itu sendiri atau melalui bash -c 'someotherscript "$@"' main-script args..mana main-scriptharus, apa yang testdilihatnya $BASH_SOURCE).

  • Jika bersumber / dieksekusi / dibaca / evaled oleh shell selain bash, maintidak dipanggil ( BASH_SOURCEselalu berbeda dengan $0).

  • maintidak dipanggil jika bashmembaca skrip dari stdin, kecuali jika Anda mengatur $0agar string kosong seperti ini:( exec -a '' /bin/bash ) <script

  • Jika dievaluasi oleh bashdengan eval ( eval "`cat script`" semua kutipan penting! ) Dari dalam beberapa skrip lain, panggilan ini main. Jika evaldijalankan dari commandline secara langsung, ini mirip dengan kasus sebelumnya, di mana skrip dibaca dari stdin. ( BASH_SOURCEkosong, sedangkan $0biasanya /bin/bashjika tidak dipaksa untuk sesuatu yang sama sekali berbeda.)

  • Jika maintidak dipanggil, ia kembali true( $?=0).

  • Ini tidak bergantung pada perilaku yang tidak terduga (sebelumnya saya menulis tanpa dokumen, tetapi saya tidak menemukan dokumentasi yang tidak dapat Anda unsetubah atau ubah BASH_SOURCE):

    • BASH_SOURCEadalah bash reserved array . Tetapi membiarkannya BASH_SOURCE=".$0"berubah akan membuka kaleng cacing yang sangat berbahaya, jadi harapan saya adalah, bahwa ini tidak akan berpengaruh (kecuali, mungkin, beberapa peringatan buruk muncul di beberapa versi masa depan bash).
    • Tidak ada dokumentasi yang BASH_SOURCEberfungsi di luar fungsi. Namun sebaliknya (hanya berfungsi dalam fungsi) tidak didokumentasikan. Pengamatannya adalah, itu berfungsi (diuji dengan bashv4.3 dan v4.4, sayangnya saya tidak punya bashv3.x lagi) dan skrip yang terlalu banyak akan rusak, jika $BASH_SOURCEberhenti bekerja seperti yang diamati. Karena itu harapan saya adalah, itu BASH_SOURCEtetap seperti untuk versi masa depan bashjuga.
    • Sebaliknya (nice find, BTW!) Pertimbangkan ( return 0 ), yang memberi 0jika bersumber dan 1jika tidak bersumber. Ini datang sedikit tidak terduga tidak hanya untuk saya , dan (menurut bacaan di sana) POSIX mengatakan, bahwa returndari subkulit adalah perilaku yang tidak terdefinisi (dan di returnsini jelas dari subkulit). Mungkin fitur ini pada akhirnya mendapatkan penggunaan yang cukup luas sehingga tidak dapat lagi diubah, tetapi AFAICS ada peluang yang jauh lebih tinggi bahwa beberapa bashversi yang tidak disengaja mengubah perilaku pengembalian dalam kasus tersebut.
  • Sayangnya bash -x script 1 2 3tidak berjalan main. (Bandingkan script 1 2 3mana yang scripttidak memiliki jalur). Berikut ini dapat digunakan sebagai solusi:

    • bash -x "`which script`" 1 2 3
    • bash -xc '. script' "`which script`" 1 2 3
    • Itu bash script 1 2 3tidak berjalan maindapat dianggap sebagai fitur.
  • Perhatikan bahwa ( exec -a none script )panggilan main( bashtidak meneruskannya $0ke skrip, untuk ini Anda harus menggunakan -cseperti yang ditunjukkan pada poin terakhir).

Jadi, kecuali untuk beberapa kasus sudut, mainhanya dipanggil, ketika skrip dieksekusi dengan cara biasa. Biasanya ini adalah apa yang Anda inginkan, terutama karena tidak memiliki kode yang rumit dan sulit dipahami.

Perhatikan bahwa ini sangat mirip dengan kode Python:

if __name__ == '__main__': main()

Yang juga mencegah pemanggilan main, kecuali untuk beberapa kasus sudut, karena Anda dapat mengimpor / memuat skrip dan menerapkannya__name__='__main__'

Mengapa saya pikir ini adalah cara umum yang baik untuk menyelesaikan tantangan

Jika Anda memiliki sesuatu, yang dapat bersumber dari beberapa shell, itu harus kompatibel. Namun (baca jawaban lain), karena tidak ada cara portabel (mudah diterapkan) untuk mendeteksi sourceing, Anda harus mengubah aturan .

Dengan memastikan bahwa skrip harus dijalankan /bin/bash, Anda melakukan ini dengan tepat.

Ini menyelesaikan semua kasus tetapi mengikuti dalam hal ini skrip tidak dapat berjalan secara langsung:

  • /bin/bash tidak diinstal atau disfungsional (i. E. dalam lingkungan boot)
  • Jika Anda menyalurkannya ke shell seperti di curl https://example.com/script | $SHELL
  • (Catatan: Ini hanya benar jika Anda bashcukup baru. Resep ini dilaporkan gagal untuk varian tertentu. Jadi pastikan untuk memeriksa itu berfungsi untuk kasus Anda.)

Namun saya tidak bisa memikirkan alasan sebenarnya di mana Anda membutuhkannya dan juga kemampuan untuk sumber skrip yang persis sama secara paralel! Biasanya Anda bisa membungkusnya dengan mengeksekusi maindengan tangan. Seperti itu:

  • $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

Catatan

  • Jawaban ini tidak akan mungkin terjadi tanpa bantuan semua jawaban lain! Bahkan yang salah - yang awalnya membuat saya memposting ini.

  • Pembaruan: Diedit karena penemuan baru yang ditemukan di https://stackoverflow.com/a/28776166/490291


Diuji untuk ksh dan bash-4.3. Bagus. Sangat disayangkan jawaban Anda akan memiliki kehidupan yang sulit mengingat bahwa jawaban lain sudah bertahun-tahun mengumpulkan suara.
hagello

terima kasih atas jawaban ini. Saya menghargai tes yang lebih lama, 'kurang bisa dibaca' dengan pernyataan IF karena senang menangani kedua situasi untuk setidaknya memberikan kegagalan yang tidak bersuara. Dalam kasus saya, saya memerlukan skrip yang akan bersumber atau memberitahu pengguna kesalahan mereka dalam tidak menggunakan sumber.
Tim Richardson

@Tino: Adapun "mungkin bersumber dari shell yang berbeda juga": Di macOS, di mana /bin/shsecara efektif bashdalam mode POSIX, menetapkan untuk BASH_SOURCE memecah skrip Anda. Dalam kerang lainnya ( dash, ksh, zsh), memohon naskah Anda dengan melewati sebagai argumen berkas langsung ke executable shell malfungsi (misal, zsh <your-script>akan membuat naskah Anda keliru berpikir itu bersumber ). (Anda sudah menyebutkan bahwa perpipaan kerusakan kode, di semua shell.)
mklement0

@Tino: Sebagai tambahan: Sementara . <your-script>(sumber) tidak bekerja dari semua shell seperti POSIX pada prinsipnya, hanya masuk akal jika skrip ditulis secara eksplisit untuk menggunakan fitur POSIX saja, sehingga untuk mencegah fitur-fitur spesifik pada satu shell dari memecah eksekusi di kulit lain; karena itu menggunakan garis Bash shebang (bukan #!/bin/sh) membingungkan - setidaknya tanpa komentar yang mencolok. Sebaliknya, jika skrip Anda dimaksudkan untuk dijalankan dari Bash saja (meskipun hanya karena tidak mempertimbangkan fitur apa yang mungkin tidak portabel), lebih baik menolak eksekusi dalam cangkang non-Bash.
mklement0

1
@ mklement0 Terima kasih lagi, menambahkan catatan bahwa ada masalah. Untuk pembaca lain: Ketika bersumber dari bash v3.x seharusnya tidak dieksekusi main, tetapi ia melakukan ini dalam kasus ini! Dan ketika bersumber dari /bin/sh, yaitu bash --posix, hal yang sama terjadi dalam kasus ini, dan itu juga salah.
Tino

6

Ini berfungsi nanti di dalam skrip dan tidak tergantung pada variabel _:

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

atau

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

1
Saya pikir jawaban ini adalah salah satu dari sedikit yang memenuhi syarat POSIX di sini. Dengan kerugian yang jelas adalah bahwa Anda harus mengetahui nama file dan itu tidak berfungsi jika kedua skrip memiliki nama file yang sama.
JepZ

5

Saya akan memberikan jawaban spesifik BASH. Korn shell, maaf. Misalkan nama skrip Anda adalah include2.sh; kemudian buat fungsi di dalam yang include2.shdipanggil am_I_sourced. Inilah versi demo saya 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

Sekarang coba jalankan dengan banyak cara:

~/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

Jadi ini bekerja tanpa kecuali, dan tidak menggunakan $_barang rapuh . Trik ini menggunakan fasilitas introspeksi BASH, yaitu variabel bawaan FUNCNAMEdan BASH_SOURCE; lihat dokumentasi mereka di halaman manual bash.

Hanya dua peringatan:

1) panggilan ke am_I_called harus dilakukan dalam skrip bersumber, tetapi tidak dalam fungsi apa pun, agar tidak ${FUNCNAME[1]}mengembalikan sesuatu yang lain. Ya ... Anda bisa memeriksa ${FUNCNAME[2]}- tetapi Anda hanya membuat hidup Anda lebih sulit.

2) fungsi am_I_called harus berada di skrip bersumber jika Anda ingin mengetahui apa nama file yang dimasukkan.


1
Klarifikasi: Fitur ini membutuhkan BASH versi 3+ untuk berfungsi. Dalam BASH 2, FUNCNAME adalah variabel skalar dan bukan array. Juga BASH 2 tidak memiliki variabel array BASH_SOURCE.
Wirawan Purwanto

4

Saya ingin menyarankan sedikit koreksi pada jawaban Dennis yang sangat membantu , agar sedikit lebih portabel, saya harap:

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

karena [[tidak dikenali oleh shell yang kompatibel (agak anal retention IMHO) Debian POSIXdash ,. Juga, orang mungkin perlu tanda kutip untuk melindungi terhadap nama file yang mengandung spasi, sekali lagi di shell tersebut.


2

$_cukup rapuh. Anda harus memeriksanya sebagai hal pertama yang Anda lakukan dalam skrip. Dan bahkan kemudian, itu tidak dijamin mengandung nama shell Anda (jika bersumber) atau nama skrip (jika dijalankan).

Misalnya, jika pengguna telah menetapkan BASH_ENV, maka di bagian atas skrip, $_berisi nama perintah terakhir yang dijalankan dalam BASH_ENVskrip.

Cara terbaik yang saya temukan adalah menggunakan $0seperti ini:

name="myscript.sh"

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

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

Sayangnya, cara ini tidak bekerja di luar kotak di zsh karena functionargzeroopsi melakukan lebih dari yang disarankan namanya, dan diaktifkan secara default.

Untuk bekerja di sekitar ini, saya menempatkan unsetopt functionargzerodi saya .zshenv.


1

Saya mengikuti ekspresi kompak mklement0 .

Itu rapi, tetapi saya perhatikan bahwa itu bisa gagal dalam kasus ksh ketika dipanggil sebagai berikut:

/bin/ksh -c ./myscript.sh

(Diperkirakan itu bersumber dan bukan karena mengeksekusi subkulit) Tetapi ekspresi akan berfungsi untuk mendeteksi ini:

/bin/ksh ./myscript.sh

Juga, bahkan jika ekspresi itu kompak, sintaksisnya tidak kompatibel dengan semua shell.

Jadi saya mengakhiri dengan kode berikut, yang berfungsi untuk bash, zsh, dash dan 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

Jangan ragu untuk menambahkan dukungan kerang eksotis :)


Di ksh 93+u, ksh ./myscript.shberfungsi dengan baik untuk saya (dengan pernyataan saya) - versi apa yang Anda gunakan?
mklement0

Saya khawatir tidak ada cara untuk secara andal menentukan apakah sebuah skrip dibuat menggunakan fitur-fitur POSIX saja: upaya Anda mengasumsikan Linux ( /proc/$$/cmdline) dan dashhanya berfokus pada (yang juga bertindak seperti shpada Ubuntu, misalnya). Jika Anda ingin membuat asumsi tertentu, Anda dapat memeriksa $0untuk tes masuk akal - tetapi tidak lengkap - yang portabel.
mklement0

++ untuk pendekatan dasar, meskipun - Saya telah mengambil kebebasan untuk beradaptasi untuk apa yang saya pikir adalah perkiraan portabel terbaik untuk mendukung sh/ dashjuga, sebagai tambahan untuk jawaban saya.
mklement0

0

Saya tidak berpikir ada cara portabel untuk melakukan ini di ksh dan bash. Dalam bash Anda bisa mendeteksinya menggunakan calleroutput, tapi saya tidak berpikir ada yang setara di ksh.


$0bekerja di bash, ksh93, dan pdksh. Saya tidak perlu ksh88menguji.
Mikel

0

Saya membutuhkan one-liner yang bekerja di [mac, linux] dengan bash.version> = 3 dan tidak ada jawaban yang sesuai dengan tagihan.

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

1
The bashsolusi bekerja dengan baik (Anda bisa menyederhanakan untuk $BASH_SOURCE), tetapi kshsolusi tidak kuat: jika naskah Anda sedang bersumber oleh script lain , Anda akan mendapatkan positif palsu.
mklement0

0

Langsung ke titik: Anda harus mengevaluasi apakah variabel "$ 0" sama dengan nama Shell Anda.


Seperti ini:

#!/bin/bash

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


Melalui SHELL :

$ bash check_source.sh 
First Parameter: check_source.sh

The script WAS NOT sourced.

Melalui SUMBER :

$ source check_source.sh
First Parameter: bash

The script was sourced.



Sangat sulit untuk memiliki 100% cara portabel mendeteksi apakah sebuah skrip bersumber atau tidak.

Mengenai pengalaman saya (7 tahun dengan Shellscripting) , satu-satunya cara yang aman (tidak bergantung pada variabel lingkungan dengan PID dan sebagainya, yang tidak aman karena fakta bahwa itu adalah sesuatu BERBAGAI ), Anda harus:

  • memperluas kemungkinan dari Anda jika
  • menggunakan sakelar / kasing, jika Anda mau.

Kedua opsi tidak dapat diskalakan secara otomatis, tetapi ini adalah cara yang lebih aman.



Sebagai contoh:

ketika Anda sumber skrip melalui sesi SSH, nilai yang dikembalikan oleh variabel "$ 0" (saat menggunakan sumber ), adalah -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

ATAU

#!/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
Diturunkan, karena ini jelas salah: /bin/bash -c '. ./check_source.sh'memberi The script WAS NOT sourced.. Bug yang sama: ln -s /bin/bash pumuckl; ./pumuckl -c '. ./check_source.sh'->The script WAS NOT sourced.
Tino

2
Downvote Anda telah mengubah keseluruhan skenario dan memberikan kontribusi besar, Tino. Terima kasih!
ivanleoncz

0

Saya akhirnya memeriksa [[ $_ == "$(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

Saat digunakan curl ... | bash -s -- ARGSuntuk menjalankan skrip jarak jauh saat dalam keadaan on-the-fly, $ 0 hanya akan menjadi bashnormal /bin/bashketika menjalankan file skrip yang sebenarnya, jadi saya menggunakantype -p "$0" untuk menunjukkan path lengkap bash.

uji:

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

Ini adalah spin-off dari beberapa jawaban lain, mengenai dukungan cross shell "universal". Ini diakui sangat mirip dengan https://stackoverflow.com/a/2942183/3220983 khususnya, meskipun sedikit berbeda. Kelemahannya adalah skrip klien harus menghargai cara menggunakannya (yaitu dengan mengekspor variabel terlebih dahulu). Kekuatannya adalah ini sederhana dan harus bekerja "di mana saja". Berikut template untuk kesenangan cut & paste Anda:

# 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

Catatan: Saya menggunakan exportmemastikan mekanisme ini dapat diperluas menjadi sub proses.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.