Как определить, что сценарий получен


220

У меня есть сценарий, в котором я не хочу, чтобы он вызывался, exitесли он получен.

Я подумал о том, чтобы проверить, $0 == bashесть ли проблемы, если сценарий получен из другого сценария или если пользователь получает его из другой оболочки, например ksh.

Есть ли надежный способ определить, что сценарий был получен?


2
Некоторое время назад у меня была похожая проблема, и я решил ее, избегая «выхода» во всех случаях; "kill -INT $$" завершает сценарий безопасно в любом случае.
JESii

1
Вы заметили этот ответ ? Это дано 5 лет спустя от принятого, но у него есть "включенные батареи".
raratiru

Ответы:


73

Это похоже на переносимость между Bash и Korn:

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

Строка, аналогичная этой, или подобное присвоение pathname="$_"(с более поздним тестом и действием) должна находиться в первой строке скрипта или в строке после шебанга (который, если используется, должен быть для ksh, чтобы он работал в большинство обстоятельств).


10
К сожалению, это не гарантирует работу. Если пользователь установил BASH_ENV, $_в верхней части скрипта будет последняя команда, запущенная из BASH_ENV.
Микель

30
Это также не будет работать, если вы используете bash для выполнения скрипта, например, $ bash script.sh, тогда $ _ будет / bin / bash вместо ./script.sh, как вы и ожидаете, когда скрипт вызывается таким образом: $ ./script.sh В любом случае обнаружение с помощью $_является проблемой.
Wirawan Purwanto

2
Дополнительные тесты могут быть включены для проверки этих методов вызова.
Приостановлено до дальнейшего уведомления.

8
К сожалению, это неправильно! смотри мой ответ
Ф. Хаури

8
Подводя итог: хотя этот подход обычно работает, он не является надежным ; он завершается ошибкой в ​​следующих 2 сценариях: (a) bash script(вызов через исполняемый файл оболочки, о котором это решение неверно сообщает как источник ), и (b) (гораздо менее вероятно) echo bash; . script(если он $_совпадает с тем, что сценарий содержит оболочку, это решение представляет его как подоболочка ). Только специфичные для оболочки специальные переменные (например, $BASH_SOURCE) допускают надежные решения (из этого следует, что не существует надежного POSIX-совместимого решения). Это является возможным, хотя и громоздким, чтобы выработать надежный тест кросса-оболочку.
mklement0

171

Если ваша версия Bash знает о переменной массива BASH_SOURCE, попробуйте что-то вроде:

# 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
Возможно, это самый чистый способ, так как $ BASH_SOURCE предназначен именно для этой цели.
con-f-use

4
Обратите внимание, что это не будет работать в ksh, что является условием, указанным OP.
Приостановлено до дальнейшего уведомления.

2
Есть ли причина использовать ${BASH_SOURCE[0]}вместо просто $BASH_SOURCE? А ${0}против $0?
hraban

4
BASH_SOURCEпеременная массива (см. руководство ), которая содержит трассировку стека источников, где ${BASH_SOURCE[0]}находится самая последняя. Здесь используются скобки, чтобы сообщить bash, что является частью имени переменной. Они не нужны $0в этом случае, но они также не причиняют вреда. ;)
Конрад

4
@ Конрад, и если вы развернетесь, $arrayвы получите ${array[0]}по умолчанию. Итак, опять же, есть ли причина [...]?
Чарльз Даффи

134

Надежные решения для bash, ksh,zsh , в том числе кросс-оболочка один, плюс достаточно надежный POSIX-совместимые решения :

  • Приведенные номера версий - это те, на которых была проверена функциональность - вероятно, эти решения работают и на гораздо более ранних версиях - обратная связь приветствуется .

  • Используя только функции POSIX (например, in dash, который действует как /bin/shв Ubuntu), не существует надежного способа определить, идет ли сценарий из источника - для получения наилучшего приближения см. Ниже .

Последующие строки - пояснение ниже; версия с несколькими оболочками сложна, но она должна работать надежно:

  • bash (проверено в 3.57 и 4.4.19)

    (return 0 2>/dev/null) && sourced=1 || sourced=0
  • ksh (проверено на 93u +)

    [[ $(cd "$(dirname -- "$0")" && 
       printf '%s' "${PWD%/}/")$(basename -- "$0") != "${.sh.file}" ]] &&
         sourced=1 || sourced=0
  • zsh (проверено на 5.0.5) - обязательно вызовите это вне функции

    [[ $ZSH_EVAL_CONTEXT =~ :file$ ]] && sourced=1 || sourced=0
  • поперечная оболочка (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
  • POSIX-совместимый ; не однострочный (однотрубный) по техническим причинам и не полностью устойчивый (см. внизу):

    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

Объяснение:


удар

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

Примечание: метод был адаптирован из ответа пользователя user5754163 , поскольку он оказался более надежным, чем исходное решение, [[ $0 != "$BASH_SOURCE" ]] && sourced=1 || sourced=0[1]

  • Bash допускает returnоператоры только из функций и, в области видимости верхнего уровня скрипта, только если источник скрипта получен .

    • Если returnиспользуется в области верхнего уровня сценария без источников , выдается сообщение об ошибке и устанавливается код выхода 1.
  • (return 0 2>/dev/null)выполняется returnв подоболочке и подавляет сообщение об ошибке; затем код завершения указывает на то, был ли найденный скрипт ( 0) или нет ( 1), который используется с &&и ||операторов , чтобы установить sourcedпеременную , соответственно.

    • Использование подоболочки необходимо, потому что выполнение returnв области верхнего уровня сценария с исходным кодом приведет к его завершению.
    • Наконечник шляпы @Haozhun , который сделал команду более устойчивой, явно используя 0в качестве returnоперанда; он отмечает: для каждой подсказки return [N]: «Если N опущено, возвращается статус последней команды». В результате более ранняя версия [которая использовалась просто return, без операнда] дает неверный результат, если последняя команда в оболочке пользователя имеет ненулевое возвращаемое значение.

КШ

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

Специальная переменная ${.sh.file}чем-то аналогична $BASH_SOURCE; обратите внимание, что это ${.sh.file}вызывает синтаксическую ошибку в bash, zsh и dash, поэтому обязательно выполняйте ее условно в сценариях с несколькими оболочками.

В отличие от bash, $0и ${.sh.file}НЕ гарантируется, что они будут точно идентичными в случае без источников, поскольку это $0может быть относительный путь, хотя ${.sh.file}он всегда является полным путем, поэтому $0перед сравнением должен быть разрешен полный путь.


ЗШ

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

$ZSH_EVAL_CONTEXTсодержит информацию о контексте оценки - вызывайте ее вне функции. Внутри исходного сценария [область верхнего уровня] $ZSH_EVAL_CONTEXT заканчивается на :file.

Оговорка: Внутри команды замены, ЗШ Присоединяет :cmdsubst, поэтому тест $ZSH_EVAL_CONTEXTна :file:cmdsubst$там.


Использование только функций POSIX

Если вы готовы сделать определенные предположения, вы можете сделать разумное, но не надежное предположение о том, что ваш сценарий получен, основываясь на знании двоичных имен файлов оболочек, которые могут выполнять ваш сценарий .
В частности, это означает, что этот подход не работает, если ваш сценарий получен из другого сценария .

Раздел «Как обрабатывать вызовы из источника» в этом моем ответе обсуждает крайние случаи, которые не могут быть обработаны только с помощью функций POSIX.

Это зависит от стандартного поведения $0, которое zsh, например, не демонстрирует.

Таким образом, самый безопасный подход состоит в том, чтобы объединить надежные, специфичные для оболочки методы, описанные выше, с запасным решением для всех оставшихся оболочек.

Наконечник шляпы Стефану Десне и его ответу за вдохновение (преобразование моего выражения оператора через оболочку в sh-совместимое ifвыражение и добавление обработчика для других оболочек).

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 обнаружил, что [[ $0 != "$BASH_SOURCE" ]]дает ложное срабатывание, когда вы выполняете скрипт, расположенный в,$PATH путем передачи его простого имени файла в bashдвоичный файл; Например, bash my-scriptпотому что $0тогда справедливо my-script, тогда как $BASH_SOURCEэто полный путь . В то время как вы обычно не использовать этот метод для вызова скриптов в $PATH- вы бы просто ссылаться на них напрямую ( my-script) - это является полезным в сочетании с -xдля отладки .


1
слава за такой исчерпывающий ответ.
DrUseful

75

После прочтения ответа @ DennisWilliamson, есть некоторые проблемы, см. Ниже:

Как этот вопрос означает и есть другая часть в этом ответе, касающаяся ... увидеть ниже.

просто путь

[ "$0" = "$BASH_SOURCE" ]

Давайте попробуем (на лету, потому что bash может ;-):

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)

sourceВместо этого я использую .для удобства чтения (как .псевдоним 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)

Обратите внимание, что номер процесса не меняется, пока процесс остается в источнике :

echo $$
29301

Почему бы не использовать $_ == $0сравнение

Для обеспечения многих случаев я начинаю писать настоящий скрипт:

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

Скопируйте это в файл с именем testscript:

cat >testscript   
chmod +x testscript

Теперь мы можем проверить:

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

Все нормально.

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

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

Все нормально.

Но для тестирования скрипта перед добавлением -xфлага:

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

Или использовать предопределенные переменные:

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)

Это больше не будет работать.

Перемещение комментария с 5-й строки на 6-ю даст более читаемый ответ:

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

Сильнее: сейчас...

Как я не пользуюсь много, после некоторого прочтения на странице руководства, есть мои попытки:

#!/bin/ksh

set >/tmp/ksh-$$.log

Скопируйте это в testfile.ksh:

cat >testfile.ksh
chmod +x testfile.ksh

Чем запустить его два раза:

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

и увидеть:

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

Существует некоторая переменная, унаследованная в исходном цикле, но ничего действительно не связанная ...

Вы могли бы даже проверить, что $SECONDSблизко 0.000, но это гарантирует только ручные источники ...

Вы даже можете попытаться проверить, кто является родителем:

Поместите это в свой testfile.ksh:

ps $PPID

чем:

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

или ps ho cmd $PPID, но эта работа только для одного уровня subsessions ...

Извините, я не мог найти надежный способ сделать это, под ,


[ "$0" = "$BASH_SOURCE" ] || [ -z "$BASH_SOURCE" ]для скриптов читайте через pipe ( cat script | bash).
Хакре

2
Обратите внимание, что .это не псевдоним source, а наоборот. source somescript.shэто Bash-ism и не переносимый, . somescript.shэто POSIX и переносимый IIRC.
dragon788

32

BASH_SOURCE[]Ответ (Баш-3,0 и выше) кажется простым, хотя BASH_SOURCE[]это не документирован для работы вне тела функции (это в настоящее время происходит на работу, в несогласии со страницей человека).

Самый надежный способ, как предлагает Wirawan Purwanto, - это проверка FUNCNAME[1] внутри функции :

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

Затем:

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

Это эквивалентно проверке вывода callerзначений mainи sourceразличению контекста вызывающего. Использование FUNCNAME[]экономит вам захват и анализ callerвыходных данных. Вы должны знать или рассчитать локальную глубину вызова, чтобы быть правильной, хотя. Случаи, подобные сценарию, полученному из другой функции или сценария, приведут к тому, что массив (стек) будет глубже. ( FUNCNAMEэто специальная переменная массива bash, она должна иметь смежные индексы, соответствующие стеку вызовов, если это не так unset).

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

(В bash-4.2 и позже вы можете использовать более простую форму ${FUNCNAME[-1]}вместо последнего элемента в массиве. Улучшено и упрощено благодаря комментарию Денниса Уильямсона ниже.)

Тем не менее, ваша проблема как заявлено: « У меня есть скрипт, в котором я не хочу, чтобы он вызывал« выход », если он получен ». Общая bashидиома для этой ситуации:

return 2>/dev/null || exit

Если сценарий получен, он returnпрервет исходный сценарий и вернется к вызывающей стороне.

Если сценарий выполняется, он returnвернет ошибку (перенаправлено) и exitзавершит работу сценария в обычном режиме. Оба returnи exitмогут взять код выхода, если требуется.

К сожалению, это не работает в ksh(по крайней мере, не в производной версии AT & T, которую я имею здесь), оно рассматривается returnкак эквивалент, exitесли вызывается вне функции или скрипта с точечным источником.

Обновлено : в современных версиях вы можетеksh проверить специальную переменную, для .sh.levelкоторой задана глубина вызова функции. Для вызванного скрипта это будет изначально не установлено, для скрипта с точечным источником будет установлено значение 1.

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

issourced && echo this script is sourced

Это не так надежно, как версия bash, вы должны вызывать issourced()в файле, который вы тестируете, на верхнем уровне или с известной глубиной функции.

(Вас также может заинтересовать этот код на github, который использует функцию kshдисциплины и некоторые хитрости для отладки ловушек для эмуляции FUNCNAMEмассива bash .)

Канонический ответ здесь: http://mywiki.wooledge.org/BashFAQ/109 также предлагает в $-качестве еще одного (хотя и несовершенного) индикатора состояния оболочки.


Ноты:

  • Можно создать функции bash с именами «main» и «source» ( переопределяя встроенные функции ), в которых эти имена могут появляться, FUNCNAME[]но пока проверяется только последний элемент в этом массиве, нет никакой неоднозначности.
  • У меня нет хорошего ответа для pdksh. Самое близкое, что я могу найти, относится только к тому pdksh, где каждый источник скрипта открывает новый дескриптор файла (начиная с 10 для оригинального скрипта). Почти наверняка не то, на что вы хотите положиться ...

Как насчет ${FUNCNAME[(( ${#FUNCNAME[@]} - 1 ))]}получения последнего (нижнего) элемента в стеке? Тогда тестирование на «основной» (отрицание для ОП) было для меня самым надежным.
Адриан Гюнтер

Если у меня есть PROMPT_COMMANDнабор, он отображается как последний индекс FUNCNAMEмассива, если я запускаю source sourcetest.sh. Обращая чек (ищем в mainкачестве последнего индекса) кажется более надежным: is_main() { [[ ${FUNCNAME[@]: -1} == "main" ]]; }.
dimo414

1
На странице руководства указано, что FUNCNAMEэто доступно только в функциях. Согласно моим тестам declare -p FUNCNAME, bashведет себя по-разному. v4.3 выдает ошибку за пределами функций, а v4.4 выдает declare -a FUNCNAME. Оба (!) Возвращают mainfor ${FUNCNAME[0]}в основной скрипт (если он выполняется), пока $FUNCNAMEничего не дает. И: Есть так много сценариев "ab", использующих $BASH_SOURCEвнешние функции, что я сомневаюсь, что это может или будет изменено.
Тино

24

Примечание редактора: решение этого ответа работает надежно, но - только bash. Это может быть упрощено до
(return 2>/dev/null).

TL; DR

Попробуйте выполнить returnзаявление. Если сценарий не получен, это вызовет ошибку. Вы можете поймать эту ошибку и продолжить, как вам нужно.

Поместите это в файл и назовите его, скажем, 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

Выполните это напрямую:

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

Источник это:

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

Для меня это работает в Zsh и Bash.

объяснение

returnЗаявление вызовет ошибку , если вы пытаетесь выполнить его вне функции или если сценарий не получен. Попробуйте это из командной строки:

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

Вам не нужно видеть это сообщение об ошибке, поэтому вы можете перенаправить вывод в dev / null:

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

Теперь проверьте код выхода. 0 означает, что все в порядке (ошибок не было), 1 означает, что произошла ошибка:

shell-prompt> echo $?
output: 1

Вы также хотите выполнить returnинструкцию внутри вложенной оболочки. Когда returnоператор запускает его. , , хорошо . , , возвращается. Если вы выполните его в под-оболочке, он вернется из этой под-оболочки, а не из вашего скрипта. Чтобы выполнить в под-оболочке, оберните его $(...):

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

Теперь вы можете увидеть код завершения вложенной оболочки, который должен быть 1, потому что внутри вложенной оболочки возникла ошибка:

shell-prompt> echo $?
output: 1

Это не получается для меня в 0.5.8-2.1ubuntu2 $ readlink $(which sh) dash $ . test.sh This script is sourced. $ ./test.sh This script is sourced.
Фил Рутшман

3
POSIX не определяет, что returnделать на верхнем уровне ( pubs.opengroup.org/onlinepubs/9699919799/utilities/… ). В dashоболочках лечат returnна верхнем уровне exit. Другие оболочки любят bashили zshне допускают returnна верхнем уровне, что является особенностью подобной техники.
user5754163

Это работает в sh, если вы удалите $перед оболочкой. То есть используйте (return >/dev/null 2>&1)вместо $(return >/dev/null 2>&1)- но тогда он перестает работать в bash.
Одноименный

@Eponymous: Так dash, где это решение не работает, действует как shна Ubuntu, например, это решение не вообще работать с sh. Решение работает для меня в Bash 3.2.57 и 4.4.5 - с или без $ранее (...)(хотя для этого никогда нет веских причин $).
mklement0

2
returnПри отсутствии скрипта возвращаемое значение explcit обрывается при sourceсоздании скриптов. Предложено улучшение редактирования.
DimG

12

FWIW, прочитав все остальные ответы, я нашел следующее решение для меня:

Обновление: На самом деле, кто-то заметил исправленную с тех пор ошибку в другом ответе, который также затронул мой. Я думаю, что обновление здесь также является улучшением (см. Изменения, если вам интересно).

Это работает для всех сценариев, которые начинаются с,#!/bin/bash но могут быть получены из различных оболочек, а также для изучения некоторой информации (например, настроек), которая хранится вне mainфункции.

Согласно комментариям ниже, этот ответ здесь, очевидно, не работает для всех bashвариантов. Также не для систем, на которых /bin/shосновано bash. IE это не удается для bashv3.x на MacOS. (В настоящее время я не знаю, как решить эту проблему.)

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

Вместо последних двух строк вы можете использовать следующий (на мой взгляд, менее читаемый) код, чтобы не устанавливать его BASH_SOURCEв других оболочках и разрешать set -eработать в main:

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

Этот скрипт-рецепт имеет следующие свойства:

  • Если выполняется bashобычным способом, mainназывается. Обратите внимание, что это не включает в себя вызов, как bash -x script(где scriptне содержит путь), см. Ниже.

  • Если источник bash, mainвызывается, только если вызывающий скрипт имеет одно и то же имя. (Например, если это источник или сам, bash -c 'someotherscript "$@"' main-script args..где main-scriptдолжен быть, то, что testвидит как $BASH_SOURCE).

  • Если источник / выполнен / прочитан / evalотредактирован оболочкой, отличной от bash, mainне BASH_SOURCEвызывается ( всегда отличается от $0).

  • mainне вызывается, если bashчитает сценарий из stdin, если вы не установили $0пустую строку, например:( exec -a '' /bin/bash ) <script

  • Если это выполняется bashс помощью eval ( eval "`cat script`" все кавычки важны! ) Из другого скрипта, это вызывает main. Если evalон запускается непосредственно из командной строки, это аналогично предыдущему случаю, когда скрипт читается из стандартного ввода. ( BASH_SOURCEпусто, хотя $0обычно /bin/bashесли не принуждено к чему-то совершенно другому.)

  • Если mainне вызывается, он возвращает true( $?=0).

  • Это не зависит от неожиданного поведения (ранее я писал недокументированное, но я не нашел документации, которую вы не можете unsetни изменить, ни изменить BASH_SOURCE):

    • BASH_SOURCEявляется Баш зарезервирован массив . Но разрешение BASH_SOURCE=".$0"изменить его открыло бы очень опасную банку червей, поэтому я ожидаю, что это не должно иметь никакого эффекта (за исключением, возможно, какого-то ужасного предупреждения, которое появится в какой-то будущей версии bash).
    • Нет документации, которая бы BASH_SOURCEработала вне функций. Однако обратное (то, что он работает только в функциях) не задокументировано. Наблюдение состоит в том, что это работает (протестировано с bashv4.3 и v4.4, к сожалению, у меня больше нет bashv3.x), и что слишком много сценариев сломалось бы, если $BASH_SOURCEперестало работать, как наблюдалось. Следовательно, я ожидаю, что она BASH_SOURCEостанется такой же и для будущих версий bash.
    • В отличие от (хорошая находка, кстати!) Рассмотрим ( return 0 ), что дает, 0если получены и 1если не получены. Это приходит немного неожиданно не только для меня , и (согласно показаниям там) POSIX говорит, что returnот subshell есть неопределенное поведение (а returnздесь явно от подоболочек). Возможно, эта функция в конечном итоге получит достаточно широкое использование, так что ее уже нельзя будет изменить, но AFAICS имеет гораздо более высокий шанс того, что какая-то будущая bashверсия случайно изменит поведение возврата в этом случае.
  • К сожалению bash -x script 1 2 3, не работает main. (Сравните, script 1 2 3где scriptнет пути). Следующее может быть использовано в качестве обходного пути:

    • bash -x "`which script`" 1 2 3
    • bash -xc '. script' "`which script`" 1 2 3
    • То, bash script 1 2 3что не работает, mainможно считать особенностью.
  • Обратите внимание, что ( exec -a none script )вызовы main( bashне передаются $0в сценарий, для этого вам нужно использовать, -cкак показано в последнем пункте).

Таким образом, за исключением некоторых некоторых угловых случаев, mainвызывается только тогда, когда скрипт выполняется обычным способом. Обычно это то, что вы хотите, особенно потому , что ему не хватает сложного, трудного для понимания кода.

Обратите внимание, что это очень похоже на код Python:

if __name__ == '__main__': main()

Что также предотвращает вызов main, за исключением некоторых угловых случаев, поскольку вы можете импортировать / загружать скрипт и применять его__name__='__main__'

Почему я думаю, что это хороший общий способ решения проблемы

Если у вас есть что-то, что может быть получено из нескольких оболочек, это должно быть совместимо. Однако (прочитайте другие ответы), поскольку не существует (легко реализуемого) переносимого способа обнаружения данных source, вам следует изменить правила .

Обеспечивая выполнение сценария /bin/bash, вы точно делаете это.

Это решает все случаи, кроме следующих: в этом случае скрипт не может работать напрямую:

  • /bin/bash не установлен или не работает (т.е. в среде загрузки)
  • Если вы трубите его в оболочку, как в curl https://example.com/script | $SHELL
  • (Примечание. Это верно только в том случае, если ваш bashпродукт достаточно свежий. Сообщается, что этот рецепт не подходит для некоторых вариантов. Поэтому убедитесь, что он работает для вашего случая.)

Однако я не могу думать ни о какой реальной причине, где вам это нужно, а также о возможности параллельно создавать один и тот же сценарий! Обычно вы можете обернуть его, чтобы выполнить mainвручную. Как это:

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

Ноты

  • Этот ответ не был бы возможен без помощи всех других ответов! Даже неправильные - которые изначально заставили меня опубликовать это.

  • Обновление: отредактировано из-за новых открытий, найденных в https://stackoverflow.com/a/28776166/490291


Проверено на ksh и bash-4.3. Ницца. Жаль, что у вашего ответа будет тяжелая жизнь, учитывая, что другие ответы уже годами набирали голоса.
Гагелло

спасибо за этот ответ. Я оценил более длинный, «менее читаемый» тест с оператором IF, так как приятно обрабатывать обе ситуации, чтобы, по крайней мере, дать не тихий сбой. В моем случае мне нужен сценарий, чтобы получить исходный код или иным образом проинформировать пользователя об ошибке в том, что он не использовал источник.
Тим Ричардсон

@Tino: Что касается «могут быть получены с помощью различных оболочек , как хорошо»: В MacOS, где /bin/shэффективно находится bashв режиме POSIX, сопоставляя BASH_SOURCE брейков сценарий. В других оболочках ( dash, ksh, zsh), ссылаясь на свой скрипт, передав его в качестве файлового аргумента непосредственно к оболочке исполняемым неисправностям (например, zsh <your-script>сделает ваш сценарий ошибочно думать , что это источники ). (Вы уже отметить , что обжигающе неисправность , код во всех оболочках.)
mklement0

@Tino: В качестве отступления: хотя . <your-script>(sourcing) в принципе работает из всех POSIX-подобных оболочек, имеет смысл только, если сценарий явно написан для использования только функций POSIX, чтобы предотвратить прерывание выполнения функциями, специфичными для одной оболочки. в других снарядах; поэтому использование линии Bash Shebang (а не #!/bin/sh) сбивает с толку - по крайней мере, без заметного комментария. И наоборот, если ваш сценарий предназначен для запуска только из Bash (даже если только из-за того, что он не учитывает, какие функции могут быть не переносимыми), лучше отказаться от выполнения в оболочках, отличных от Bash.
mklement0

1
@ mklement0 Еще раз спасибо, добавил примечание, что есть проблема. Для других читателей: При использовании bash v3.x он не должен выполняться main, но он делает это в этом случае! И когда источником является то /bin/sh, что bash --posixто же самое происходит в этом случае, и это также явно неправильно.
Тино

6

Это работает позже в скрипте и не зависит от переменной _:

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

или

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

1
Я думаю, что этот ответ является одним из немногих POSIX-совместимых здесь. С очевидным недостатком является то, что вы должны знать имя файла, и это не работает, если оба сценария имеют одинаковое имя файла.
JepZ

5

Я дам BASH-специфичный ответ. Корн скорлупа, извините. Предположим, что ваш скрипт называется include2.sh; затем сделать функцию внутриinclude2.sh называется am_I_sourced. Вот моя демо-версия 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

Теперь попробуйте выполнить это многими способами:

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

Так что это работает без исключения и не использует хрупкие $_вещи. Этот трюк использует средство самоанализа BASH, то есть встроенные переменные FUNCNAMEи BASH_SOURCE; см. их документацию на странице руководства bash.

Только два предостережения:

1) вызов am_I_called должен выполняться в исходном скрипте, но не внутри какой-либо функции, чтобы не ${FUNCNAME[1]}возвращалось что-то еще. Да ... ты мог бы проверить${FUNCNAME[2]} - но ты просто усложнил свою жизнь.

2) функция am_I_called должна находиться в исходном скрипте, если вы хотите узнать, какое имя включаемого файла.


1
Пояснение: эта функция требует BASH версии 3+ для работы. В BASH 2 FUNCNAME является скалярной переменной, а не массивом. Также BASH 2 не имеет переменной массива BASH_SOURCE.
Wirawan Purwanto

4

Я хотел бы предложить небольшую поправку к очень полезному ответу Денниса , чтобы сделать его немного более портативным, я надеюсь:

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

потому что [[не распознается (несколько анального удерживающий ИМХО) Debian POSIX совместимой оболочкой, dash. Кроме того, могут потребоваться кавычки для защиты от имен файлов, содержащих пробелы, опять же в указанной оболочке.


2

$_довольно хрупкий Вы должны проверить это как первое, что вы делаете в сценарии. И даже в этом случае не обязательно будет содержать имя вашей оболочки (если оно получено) или имя скрипта (если оно выполнено).

Например, если пользователь установил BASH_ENV, то в верхней части скрипта $_содержится имя последней команды, выполненной вBASH_ENV скрипте.

Лучший способ, который я нашел, это использовать $0так:

name="myscript.sh"

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

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

К сожалению, этот способ не работает из коробки в zsh из-за того, что functionargzeroопция делает больше, чем предполагает ее имя, и включена по умолчанию.

Чтобы обойти это, я вставил unsetopt functionargzeroсвой .zshenv.


1

Я следовал за компактным выражением mklement0 .

Это здорово, но я заметил, что он может потерпеть неудачу в случае ksh, когда вызывается так:

/bin/ksh -c ./myscript.sh

(он думает, что это источник, и это не потому, что он выполняет подоболочку) Но выражение будет работать, чтобы обнаружить это:

/bin/ksh ./myscript.sh

Кроме того, даже если выражение компактно, синтаксис не совместим со всеми оболочками.

Итак, я закончил со следующим кодом, который работает для bash, zsh, dash и 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

Не стесняйтесь добавлять поддержку экзотических оболочек :)


В ksh 93+u, ksh ./myscript.shпрекрасно работает для меня (с моим заявлением) - какую версию вы используете?
mklement0

Я боюсь, что нет никакого способа надежно определить, был ли источник написан с использованием только POSIX-функций: ваша попытка предполагает использование Linux ( /proc/$$/cmdline) и фокусируется dashтолько на ней (которая также действует, как sh, например, в Ubuntu). Если вы готовы сделать определенные предположения, вы можете проверить $0приемлемый, но неполный тест, который является переносимым.
mklement0

++ для базового подхода, хотя - я позволил себе адаптировать его для того, что я считаю лучшим переносимым приближением поддержки sh/, dashтакже, в добавлении к моему ответу.
mklement0

0

Я не думаю, что есть какой-либо переносимый способ сделать это как в ksh, так и в bash. В bash вы можете обнаружить это, используя callerвыходные данные, но я не думаю, что в ksh существует эквивалент.


$0работает bash, ksh93и pdksh. Мне не нужно ksh88проверять.
Микель

0

Мне нужна была одна строка, которая работает на [mac, linux] с bash.version> = 3, и ни один из этих ответов не отвечал всем требованиям.

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

1
bashРешение работает хорошо (можно упростить $BASH_SOURCE), но kshрешение не является надежным: если ваш скрипт быть получен по другому сценарию , вы получите ложную положительный результат .
mklement0

0

Прямо к делу: вы должны оценить, равна ли переменная «$ 0» названию вашей оболочки.


Как это:

#!/bin/bash

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


Через ОБОЛОЧКУ :

$ bash check_source.sh 
First Parameter: check_source.sh

The script WAS NOT sourced.

Через ИСТОЧНИК :

$ source check_source.sh
First Parameter: bash

The script was sourced.



Довольно сложно иметь 100% -ый портативный способ определения, был ли скрипт создан или нет.

Что касается моего опыта (7 лет с Shellscripting) , единственным безопасным способом (не полагаясь на переменные окружения с PID и т. Д., Который небезопасен из-за того, что это что-то переменное ), вы должны:

  • расширить возможности вашего if
  • используя переключатель / чехол, если хотите.

Оба параметра не могут быть автоматически масштабированы, но это более безопасный способ.



Например:

при создании сценария через сеанс SSH значением, возвращаемым переменной «$ 0» (при использовании источника ), является -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

ИЛИ

#!/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
Понижено, поскольку это совершенно неправильно: /bin/bash -c '. ./check_source.sh'дает The script WAS NOT sourced.. Та же ошибка: ln -s /bin/bash pumuckl; ./pumuckl -c '. ./check_source.sh'->The script WAS NOT sourced.
Tino

2
Ваше отрицательное мнение изменило весь сценарий и внесло большой вклад, Тино. Спасибо!
ivanleoncz

0

Я закончил с проверкой [[ $_ == "$(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

При использовании curl ... | bash -s -- ARGSдля запуска удаленного сценария «на лету», $ 0 будет просто bashвместо обычного /bin/bashпри запуске реального файла сценария, поэтому я использую, type -p "$0"чтобы показать полный путь bash.

тест:

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

Это ответ на некоторые другие ответы, касающиеся «универсальной» поддержки кросс-оболочек. По общему признанию это очень похоже на https://stackoverflow.com/a/2942183/3220983 в частности, хотя немного отличается. Недостатком этого является то, что клиентский скрипт должен учитывать, как его использовать (т.е. сначала экспортировать переменную). Сила в том, что это просто и должно работать "где угодно". Вот шаблон для вашего удовольствия:

# 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

Примечание: я использую exportтолько для того, чтобы этот механизм можно было распространить на подпроцессы.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.