$ 0是否总是包含脚本的路径?


11

我想grep当前脚本,以便可以从顶部的注释部分打印出帮助和版本信息。

我在想这样的事情:

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

但是后来我想知道,如果脚本位于PATH中的目录中并且在未显式指定目录的情况下调用该脚本,将会发生什么情况。

我搜索了有关特殊变量的说明,并找到了以下说明$0

  • 当前shell或程序的名称

  • 当前脚本的文件名

  • 脚本本身的名称

  • 运行命令

$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绝对路径。这样看来,传递给另一个命令将是安全的。

但是后来我遇到了关于Stack Overflow的评论,这使我感到困惑。答案建议使用$(dirname $0)来获取当前脚本的目录。评论(已被推荐7次)说:“如果脚本在您的路径中,则该脚本将不起作用”。

问题

  • 该评论正确吗?
  • 其他系统上的行为是否不同?
  • 是否存在$0不包含目录的情况?

人们回答了有关$0脚本以外的其他问题的情况,脚本确实回答了问题标题。但是,我也对$0脚本本身但不包含目录的情况感兴趣。特别是,我试图理解对SO答案的评论。
toxalot

Answers:


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

您的shell将执行以下操作:

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

#! /bin/sh -例如,如果脚本中包含“ she-bang”,系统会将其转换为:

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

(如果它不包含she-bang,或者更一般而言,如果系统返回ENOEXEC错误,则它是您的shell,它将执行相同的操作)

在某些系统上,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

你的shell将查找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其中查找脚本(这实际上是bug和setuid脚本的安全漏洞),因此,除非查找实际上是在当前目录中找到的,否则$0实际上并不包含该脚本的路径。$PATHthe-script

如果可读的话,较新的AT&T ksh会尝试the-script在当前目录中进行解释。如果没有它会查找一个可读和可执行 the-script$PATH

bash,它检查是否the-script是在当前目录(并且不是一个破碎符号链接),并且如果没有,查找为一个可读(不一定可执行文件)the-script$PATH

zshsh仿真中,bash除了the-script在当前目录中的符号链接损坏之外,它不会搜索the-scriptin $PATH而是会报告错误。

所有其他类似Bourne外壳看起来不the-script起来$PATH

无论如何,对于所有这些shell,如果您发现其中$0不包含/且不可读,则可能已在中进行了查找$PATH。然后,由于其中的文件$PATH可能是可执行文件,因此使用它command -v -- "$0"来查找路径可能是一种安全的近似方法(尽管如果$0碰巧也是shell内置名称或关键字的名称(在大多数shell中),则该方法将无效)。

因此,如果您真的想涵盖这种情况,可以编写:

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 shell的某些旧实现)之外,$0将为blah。同样,在这些shell中很难到达脚本的路径。

要么:

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

但是我再次认为这样做是不值得的。


斯特凡(Stéphane),我不理解您"-"在以上示例中的使用。以我的经验,exec("the-script", ["the-script", "its", "args"])成为exec("/the/interpreter", ["/the/interpreter", "the-script", "its", "args"]),当然可以选择口译员。
jrw32982在2013年

@ jrw32982,#! /bin/sh -“始终使用,cmd -- something如果您不能保证something不会以它开头-”,此处的优良作法格言适用于该选项/bin/sh-因为选项结束标记比更具移植性--),something它是脚本。如果你不使用对setuid脚本(在支持他们,但不能与系统的/ dev / FD在答复中提到/ X的方法),那么可以通过创建一个符号链接到你的脚本调用得到一个root shell -i-s为实例。
斯特凡Chazelas

谢谢,史黛芬。我在您的示例shebang行中错过了结尾的单个连字符。我不得不寻找一个连字符等同于双连字符的地方pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html
jrw32982在2013年

对于setuid脚本,忘记在shebang行中尾部的单/双连字符太容易了;系统应该以某种方式为您照顾它。完全禁止setuid脚本,或者/bin/sh如果它可以检测到正在运行setuid,则应以某种方式禁用其自身的选项处理。我没有看到如何单独使用/ dev / fd / x来解决此问题。我认为您仍然需要单/双连字符。
jrw32982在2013年

@ jrw32982,/dev/fd/x以开头/,而不是-。主要目标是execve()尽管要消除两者之间的竞争条件(在两者之间execve("the-script")会提升特权,而随后会execve("interpreter", "thescript")在随后的位置之间interpreter打开脚本(这很可能已被与此之前的其他符号链接所取代))。 SUID脚本正确地做execve("interpreter", "/dev/fd/n"),而不是其中n已经打开的第一execve的(部分)。
斯特凡Chazelas

6

在两种情况下,将包含目录:

> bash scriptname
scriptname

> bash <scriptname
bash

在这两种情况下,当前目录都必须是脚本名所在的目录。

在第一种情况下,$0仍然可以将的值传递给grep它,因为它假定FILE参数是相对于当前目录的。

在第二种情况下,如果仅根据特定的命令行选项打印帮助和版本信息,则应该没有问题。我不确定为什么有人会调用脚本来打印帮助或版本信息。

注意事项

  • 如果脚本更改了当前目录,则您不想使用相对路径。

  • 如果脚本是源脚本,$0通常的值将是调用方脚本而不是源脚本。


3

使用-c大多数(所有?)shell 的选项时,可以指定任意参数零。例如:

sh -c 'echo $0' argv0

From man bash(纯粹是因为它比我的描述更好,所以选择它man sh-用法相同):

-C

如果存在-c选项,那么将从第一个非选项参数command_string中读取命令。如果command_string后面有参数,则将它们分配给位置参数,从$ 0开始。


我认为这是可行的,因为-c'command'是操作数,并且argv0是它的第一个nonoperand命令行参数。
mikeserv

@mike正确,我已更新了一个男人的摘要。
Graeme 2014年

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:扩展到nameshellshell script

似乎这转换为argv[0]当前shell或调用时提供当前解释性shell 的第一个nonoperand命令行参数。我之前曾说过,sh ./somescript可以将其变量传递给,但这是不正确的,因为本身是一个新进程,并使用new调用$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.shsource ./myscript.sh),那么$0它就是外壳。但是,如果将其作为参数传递给shell(sh ./myscript.sh),则$0该路径为脚本的路径。当然,sh在我的系统上是Bash。因此,我不知道这是否有所作为。
toxalot

它有哈希吗?我认为区别很重要-我可以补充一点-没有它,应该不应该exec而是取而代之exec
mikeserv

使用或不使用hashbang,我都会得到相同的结果。无论执行与否,我都会得到相同的结果。
toxalot

我也是!我认为这是因为它是内置的shell。我正在检查...
mikeserv

帮行由内核解释。如果您./somescript使用bangline执行#!/bin/sh,则相当于运行/bin/sh ./somescript。否则,它们对外壳没有影响。
Graeme 2014年

0

我想grep当前脚本,以便可以从顶部的注释部分打印出帮助和版本信息。

虽然$0包含脚本名称,但根据调用脚本的方式,它可能包含前缀路径,但我一直习惯${0##*/}在帮助输出中打印脚本名称,该输出从中删除任何前导路径$0

摘自Advanced Bash脚本指南-第10.2节参数替换

${var#Pattern}

从与的前端匹配$var的最短部分中删除。$Pattern$var

${var##Pattern}

从与的前端匹配$var的最长部分中删除。$Pattern$var

因此$0,匹配的最长部分*/将是整个路径前缀,仅返回脚本名称。


是的,我做到这一点对于所使用的脚本的名称的帮助信息。但是我说的是grepping脚本,因此我至少需要一个相对路径。我想将帮助消息放在注释的顶部,而以后在打印时不必重复该帮助消息。
toxalot

0

对于类似的东西,我使用:

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

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

rPath 始终具有相同的值。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.