Як визначити, чи використовується сценарій


217

У мене є сценарій, в якому я не хочу, щоб він дзвонив, exitякщо його отримують.

Я подумав перевірити, чи немає, $0 == bashале це проблеми, якщо скрипт отриманий з іншого сценарію, або якщо користувач передає його з іншої оболонки, як ksh.

Чи є надійний спосіб виявити, якщо сценарій розміщується?


2
У мене був певний час назад і я вирішив його, уникаючи "виходу" у всіх випадках; "kill -INT $$" завершує сценарій безпечно в будь-якому випадку.
JESii

1
Ви помітили цю відповідь ? Він видається на 5 років пізніше від прийнятого, але у нього є "батареї включені".
raratiru

Відповіді:


73

Це здається портативним між Башем і Корном:

[[ $_ != $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
Підсумовуючи: Хоча цей підхід зазвичай працює, він не є надійним ; вона виходить з ладу у наступних двох сценаріях: (a) bash script(виклик через виконуваний оболонку, який це рішення неправильно повідомляє як джерело ), і (b) (набагато рідше) echo bash; . script(якщо $_трапляється відповідати оболонці, що забезпечує сценарій, це рішення неправильно повідомляє про це як подоболочка ). Лише спеціальні спеціальні змінні (наприклад, $BASH_SOURCE) дозволяють надійні рішення (звідси випливає, що не існує надійного рішення, сумісного з POSIX). Це є можливим, хоча і громіздким, щоб виробити надійний тест кросу-оболонку.
mklement0

170

Якщо ваша версія 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, що є умовою, заданою ОП.
Призупинено до подальшого повідомлення.

2
Чи є причина використовувати ${BASH_SOURCE[0]}замість просто $BASH_SOURCE? І ${0}проти $0?
hraban

4
BASH_SOURCEє змінною масиву (див. посібник ), що містить стек стеження джерел, де ${BASH_SOURCE[0]}є останнім. Дужки використовуються тут, щоб повідомити bash, що є частиною назви змінної. Вони не потрібні $0в цьому випадку, але вони також не шкодять. ;)
Конрад

4
@Konrad, а якщо розгорнути $array, ви отримаєте ${array[0]}за замовчуванням. Отже, знову ж таки, чи є причина [...]?
Чарльз Даффі

133

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

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

  • Використовуючи лише функції POSIX (наприклад, у 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

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

  • Баш дозволяє 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" ]

Давайте спробуємо (на льоту, тому що цей баш міг ;-):

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

Важче: зараз ...

Як я не використовую дуже багато, після прочитання на сторінці man, є мої спроби:

#!/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, але це працює лише для одного рівня субсидій ...

Вибачте, я не зміг знайти надійний спосіб зробити це під .


[ "$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. Обидва (!) Повертаються mainза ${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Якщо wihout повернене значення 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запускається безпосередньо з командного рядка, це схоже на попередній випадок, коли сценарій зчитується з stdin. ( BASH_SOURCEпорожній, хоча $0зазвичай це /bin/bashякщо не змушений до чогось зовсім іншого.)

  • Якщо mainне викликається, він повертається true( $?=0).

  • Це не покладається на несподівану поведінку (раніше я писав незадокументовану, але я не знайшов жодної документації, яку ви не можете unsetні змінити BASH_SOURCE):

    • BASH_SOURCE- це резервний масив bash . Але BASH_SOURCE=".$0"якщо це змінити, це відкрило б дуже небезпечну банку глистів, тому я сподіваюся, що це не повинно мати ніякого ефекту (за винятком, мабуть, деякого негарного попередження з'являється в якійсь майбутній версії bash).
    • Немає жодної документації, яка BASH_SOURCEпрацює поза функціями. Однак навпаки (що це працює лише у функціях) не документально підтверджено. Спостереження полягає в тому, що він працює (протестовано з bashv4.3 та v4.4, на жаль, у мене вже немає bashv3.x) і що дуже багато сценаріїв порушиться, якщо $BASH_SOURCEперестане працювати, як спостерігалося. Отже, я сподіваюся, що це також BASH_SOURCEзалишається таким, як і для майбутніх версій bash.
    • На противагу (приємно знайти, BTW!) Врахуйте ( return 0 ), що дає, 0якщо джерело, а 1якщо не джерело. Це стає трохи несподіваним не тільки для мене , і (згідно з прочитаними там) POSIX говорить, що returnв нижній частині - невизначена поведінка (а 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. Приємно. Шкода, що ваша відповідь матиме важке життя, враховуючи, що за інші відповіді вже були роки, набрані голосів.
hagello

дякую за цю відповідь Я оцінив більш тривалий, «менш читабельний» тест із твердженням IF, оскільки приємно обробляти обидві ситуації, принаймні приводити до безмовного відмови. У моєму випадку мені потрібно отримати сценарій або іншим чином повідомити користувача про свою помилку в не використанні джерела.
Тім Річардсон,

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

@Tino: Як убік: Хоча . <your-script> (джерело) працює в принципі з усіх оболонок, схожих на POSIX, це має сенс лише в тому випадку, якщо сценарій був чітко написаний лише для використання функцій POSIX, щоб запобігти порушенню виконання функцій, характерних для однієї оболонки. в інших оболонках; використання лінії Шебанг Баша (а не #!/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


Через SHELL :

$ 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 та ін., Який не є безпечним через те, що це щось ВІДМІННО ), слід:

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

Обидва варіанти не можна автоматично масштабувати, але це безпечніший спосіб.



Наприклад:

коли ви надсилаєте скрипт через сеанс 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.
Тіно

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.