Будет ли $ 0 всегда включать путь к сценарию?


11

Я хочу использовать текущий скрипт, чтобы распечатать справку и информацию о версии из раздела комментариев вверху.

Я думал о чем-то вроде этого:

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

Но потом мне стало интересно, что произойдет, если скрипт находится в каталоге, который находится в PATH и вызывается без явного указания каталога.

Я искал объяснение специальных переменных и нашел следующие описания $0:

  • имя текущей оболочки или программы

  • имя файла текущего скрипта

  • название самого скрипта

  • команда как она была запущена

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

Тестирование в моей системе (Bash 4.1)

Я создал исполняемый файл в / usr / local / bin с именем scriptname с одной строкой echo $0и вызывал его из разных мест.

Вот мои результаты:

> cd /usr/local/bin/test
> ../scriptname
../scriptname

> cd /usr/local/bin
> ./scriptname
./scriptname

> cd /usr/local
> bin/scriptname
bin/scriptname

> cd /tmp
> /usr/local/bin/scriptname
/usr/local/bin/scriptname

> scriptname
/usr/local/bin/scriptname

В этих тестах значение $0всегда точно соответствует тому, как был вызван скрипт, за исключением случаев, когда он вызывается без какого-либо компонента пути. В этом случае значение $0является абсолютным путем . Так что похоже, что было бы безопасно перейти к другой команде.

Но потом я наткнулся на комментарий о переполнении стека, который смутил меня. Ответ предлагает использовать, $(dirname $0)чтобы получить каталог текущего скрипта. В комментарии (7 раз) проголосовало "это не сработает, если скрипт находится на вашем пути".

Вопросов

  • Это комментарий правильный?
  • Отличается ли поведение в других системах?
  • Существуют ли ситуации, когда $0бы не включать каталог?

Люди отвечали о ситуациях, когда $0что-то отличалось от сценария, который действительно отвечал на заголовок вопроса. Однако меня также интересуют ситуации, в которых $0находится сам скрипт, но не включается каталог. В частности, я пытаюсь понять комментарий к такому ответу.
Токсалот

Ответы:


17

В наиболее распространенных случаях $0будет содержать путь, абсолютный или относительный к сценарию, поэтому

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

(при условии, что readlinkкоманда есть и она поддерживается -e), как правило, является достаточно хорошим способом получения канонического абсолютного пути к сценарию.

$0 присваивается из аргумента, определяющего скрипт как переданный интерпретатору.

Например, в:

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

$0получает the/script.

Когда вы бежите:

the/script its args

Ваша оболочка сделает:

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

Если в сценарии, например, есть #! /bin/sh -она, система преобразует это в:

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

(если он не содержит ничего, или, в более общем случае, если система возвращает ошибку ENOEXEC, то ваша оболочка будет делать то же самое)

Существует исключение для сценариев setuid / setgid на некоторых системах, где система открывает сценарий на некоторых fd xи запускает вместо этого:

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

чтобы избежать условий гонки (в этом случае $0будет содержать /dev/fd/x).

Теперь, вы можете утверждать , что /dev/fd/x это путь к этому сценарию. Тем не менее, обратите внимание, что если вы читаете из $0, вы нарушите сценарий при использовании ввода.

Теперь есть разница, если имя команды сценария, которое вызывается, не содержит косой черты. В:

the-script its args

Ваша оболочка будет искать the-scriptв $PATH. $PATHможет содержать абсолютные или относительные (включая пустую строку) пути к некоторым каталогам. Например, если $PATHсодержится /bin:/usr/bin:и the-scriptнаходится в текущем каталоге, оболочка выполнит:

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

который станет:

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

Или если он найден в /usr/bin:

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

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

Теперь скрипт также можно назвать так:

the-interpreter the-script its args

Если, the-scriptкак указано выше, символы косой черты не содержат, поведение немного меняется от оболочки к оболочке.

Старые реализации AT & T kshфактически безоговорочно искали сценарий $PATH(который на самом деле являлся ошибкой и дырой в безопасности для сценариев setuid), поэтому $0фактически не содержали пути к сценарию, если только $PATHпоиск не обнаружился the-scriptв текущем каталоге.

Более новые AT & T kshбудут пытаться интерпретировать the-scriptв текущем каталоге, если он читается. Если нет, то он будет искать для чтения и выполнения the-script в $PATH.

Для bash, он проверяет, the-scriptнаходится ли в текущем каталоге (и не является ли это неработающей символической ссылкой), а если нет, ищет читаемый (не обязательно исполняемый) файл the-scriptв $PATH.

zshв shэмуляции будет так, bashза исключением того, что, если the-scriptв текущем каталоге есть неработающая символическая ссылка, он не будет искать the-scriptв $PATHи вместо этого будет сообщать об ошибке.

Все остальные борновоподобные снаряды не заглядывают the-scriptвнутрь $PATH.

В любом случае, для всех этих оболочек, если вы обнаружите, что $0они не содержат /и не читаются, то, вероятно, они были найдены $PATH. Тогда, поскольку файлы в $PATH, вероятно, будут исполняемыми, это, вероятно, безопасное приближение, чтобы использовать его, command -v -- "$0"чтобы найти его путь (хотя это не сработало бы, если $0бы также было имя встроенной оболочки или ключевое слово (в большинстве оболочек)).

Так что, если вы действительно хотите покрыть это дело, вы можете написать это:

progname=$0
[ -r "$progname" ] || progname=$(
    IFS=:; set -f
    for i in ${PATH-$(getconf PATH)}""; do
      case $i in
        "") p=$progname;;
        */) p=$i$progname;;
        *) p=$i/$progname
      esac
      [ -r "$p" ] && exec printf '%s\n' "$p"
    done
    exit 1
  ) && progname=$(readlink -e -- "$progname") ||
  progname=unknown

( ""добавлено, чтобы $PATHсохранить конечный пустой элемент с оболочками, которые $IFSдействуют как разделитель вместо разделителя ).

Теперь есть более эзотерические способы вызова скрипта. Можно сделать:

the-shell < the-script

Или:

cat the-script | the-shell

В этом случае $0будет первый аргумент ( argv[0]), полученный интерпретатором (выше the-shell, но это может быть что угодно, хотя обычно это либо базовое имя, либо один путь к этому интерпретатору).

Обнаружение того, что вы находитесь в такой ситуации на основе значения, $0не является надежным. Вы можете посмотреть на вывод, ps -o args= -p "$$"чтобы получить подсказку. В случае с конвейером, нет никакого реального способа вернуться к пути к сценарию.

Можно также сделать:

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

Тогда, за исключением zsh(и некоторых старых реализаций оболочки Bourne), $0будет blah. Опять же, сложно найти путь к сценарию в этих оболочках.

Или:

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

и т.п.

Чтобы убедиться, что у вас есть права $progname, вы можете найти в ней определенную строку, например:

progname=$0
[ -r "$progname" ] || progname=$(
    IFS=:; set -f
    for i in ${PATH-$(getconf PATH)}:; do
      case $i in
        "") p=$progname;;
        */) p=$i$progname;;
        *) p=$i/$progname
      esac
      [ -r "$p" ] && exec printf '%s\n' "$p"
    done
    exit 1
  ) && progname=$(readlink -e -- "$progname") ||
  progname=unknown

[ -f "$progname" ] && grep -q 7YQLVVD3UIUDTA32LSE8U9UOHH < "$progname" ||
  progname=unknown

Но опять же я не думаю, что это стоит усилий.


Стефан, я не понимаю твоего использования "-"в приведенных выше примерах. По моему опыту, exec("the-script", ["the-script", "its", "args"])становится exec("/the/interpreter", ["/the/interpreter", "the-script", "its", "args"]), конечно же, возможность выбора переводчика.
jrw32982 поддерживает Монику

@ jrw32982, #! /bin/sh -это «всегда используйте, cmd -- somethingесли вы не можете гарантировать, что somethingне начнете с -», здесь применена пословица передовой практики /bin/sh(где -маркер конца опции более переносим, ​​чем --) с somethingуказанием пути / имени скрипт. Если вы не используете , что для Setuid сценариев (на системах , которые поддерживают их , но не с / DEV / FD упоминается в ответе / х метод), то можно получить корневую оболочку, создавая символьную ссылку на свой сценарий под названием -iили -sдля пример.
Стефан Шазелас

Спасибо, Стефан. Я пропустил конечный одиночный дефис в вашем примере строки Шебанга. Мне пришлось искать там, где задокументировано, что один дефис эквивалентен двойному дефису pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html .
jrw32982 поддерживает Монику

Слишком легко забыть конечный одиночный / двойной дефис в строке shebang для сценария setuid; каким-то образом система должна позаботиться об этом за вас. Запретить скрипты setuid в целом или каким-то образом /bin/shследует отключить обработку собственного параметра, если он может обнаружить, что работает setuid. Я не вижу, как использование / dev / fd / x само по себе исправляет это. Вам все еще нужен одиночный / двойной дефис, я думаю.
jrw32982 поддерживает Монику

@ jrw32982, /dev/fd/xначинается с /, а не -. Основная цель состоит в том, чтобы удалить условие гонки между двумя символами execve()(между ними, execve("the-script")которые повышают привилегии, и последующими, execve("interpreter", "thescript")где interpreterпозже открывается сценарий (который вполне может быть заменен символической ссылкой на что-то еще в то же время). Сценарии suid правильно делают execve("interpreter", "/dev/fd/n")вместо этого, где n был открыт как часть первого execve ().
Стефан Шазелас

6

Вот две ситуации, когда каталог не будет включен:

> bash scriptname
scriptname

> bash <scriptname
bash

В обоих случаях текущий каталог должен быть каталогом, в котором находится имя скрипта .

В первом случае значение $0еще можно передать, grepпоскольку предполагается, что аргумент FILE относится к текущему каталогу.

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

Предостережения

  • Если скрипт изменяет текущий каталог, вы не захотите использовать относительные пути.

  • Если сценарий получен, значением $0обычно будет сценарий вызывающего, а не исходный сценарий.


3

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

sh -c 'echo $0' argv0

From man bash(выбран просто потому, что это описание лучше, чем my man sh- использование одно и то же):

Если присутствует опция -c, то команды читаются из первого неопционального аргумента command_string. Если после command_string есть аргументы, они присваиваются позиционным параметрам, начиная с $ 0.


Я думаю, что это работает, потому что -c 'command' являются операндами, и argv0это первый неоперандный аргумент командной строки.
mikeserv

@mike правильно, я обновил фрагмент человека.
Грэм

3

ПРИМЕЧАНИЕ: другие уже объяснили механику, $0поэтому я пропущу все это.

Я обычно делаю шаг в сторону всей этой проблемы и просто использую команду readlink -f $0. Это всегда вернет вам полный путь того, что вы дадите ему в качестве аргумента.

Примеры

Скажи, что я здесь, чтобы начать с:

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

Создайте каталог + файл:

$ mkdir adir
$ touch afile
$ cd adir/

Теперь начните хвастаться readlink:

$ readlink -f ../adir
/home/saml/tst/119929/adir

$ readlink -f ../
/home/saml/tst/119929

$ readlink -f ../afile 
/home/saml/tst/119929/afile

$ readlink -f .
/home/saml/tst/119929/adir

Дополнительный обман

Теперь с последовательным результатом возвращается , когда мы опрашивать с $0помощью readlinkмы можем использовать просто dirname $(readlink -f $0)получить абсолютный путь к сценарию -или- , basename $(readlink -f $0)чтобы получить реальное имя сценария.


0

Моя manстраница говорит:

$0: Расширяется до nameиз shellили shell script.

Похоже, это argv[0]соответствует текущей оболочке или первому неоперандному аргументу командной строки, который интерпретируется в данный момент интерпретируемой оболочкой при вызове. Я ранее заявлял , что sh ./somescriptпуть будет его переменной к , но это неправильно , потому что это новый процесс самостоятельно и вызывается с новым .$0 $ENV shshell$ENV

Этим sh ./somescript.shотличается от того, . ./somescript.shкоторый работает в текущей среде и $0уже настроен.

Вы можете проверить это, сравнив $0с /proc/$$/status.

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

Спасибо за исправление, @toxalot. Я кое-что узнал.


В моей системе, если это источник ( . ./myscript.shили source ./myscript.sh), то $0это оболочка. Но если он передан в качестве аргумента в shell ( sh ./myscript.sh), то $0это путь к скрипту. Конечно, shна моей системе стоит Bash. Так что я не знаю, имеет ли это значение или нет.
Токсалот

У этого есть hashbang? Я думаю, что разница имеет значение - и я могу добавить, что - без этого она не должна, execа вместо этого должна исходить , но с этим она должна exec.
mikeserv

С или без hashbang, я получаю одинаковые результаты. С или без того, чтобы быть исполняемым, я получаю одинаковые результаты.
Токсалот

Я тоже! Я думаю, это потому что это встроенная оболочка. Я проверяю ...
mikeserv

Линии взрыва интерпретируются ядром. Если вы ./somescriptвыполните команду bangline #!/bin/sh, это будет эквивалентно запуску /bin/sh ./somescript. В противном случае они не имеют никакого значения для оболочки.
Грэм

0

Я хочу использовать текущий скрипт, чтобы распечатать справку и информацию о версии из раздела комментариев вверху.

Несмотря $0на то, что он содержит имя сценария, он может содержать префиксный путь в зависимости от способа вызова сценария, я всегда использовал его ${0##*/}для вывода имени сценария в выводе справки, в котором удаляется любой начальный путь $0.

Взято из руководства Advanced Bash Scripting - Раздел 10.2 Замена параметров

${var#Pattern}

Удалить из $varсамой короткой части, $Patternкоторая соответствует переднему концу $var.

${var##Pattern}

Удалите из $varсамой длинной части, $Patternкоторая соответствует переднему концу $var.

Поэтому самой длинной частью $0этого совпадения */будет префикс полного пути, возвращающий только имя скрипта.


Да, я делаю это тоже для имени скрипта, который используется в сообщении помощи. Но я говорю о разборе сценария, поэтому мне нужен хотя бы относительный путь. Я хочу поместить справочное сообщение вверху в комментариях, и мне не нужно повторять его позже при печати.
toxalot

0

Для чего-то похожего я использую:

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

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

rPath всегда имеет одинаковое значение.

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