【问题标题】:How to get script directory in POSIX sh?如何在 POSIX sh 中获取脚本目录?
【发布时间】:2015-07-02 03:29:57
【问题描述】:

我的 bash 脚本中有以下代码。现在我想在 POSIX sh 中使用它。如何转换?

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" > /dev/null && pwd )"

【问题讨论】:

  • 问题不在于数组。那里的问题是 BASH_SOURCE 这是一个 bash 主义。 this question 中还有其他答案,还有 mywiki.wooledge.org/BashFAQ/028
  • 您的问题不在于数组,而在于BASH_SOURCE 变量,显然,该变量仅在bash 中可用。
  • 试试DIR=$( cd -P -- "$(dirname -- "$(command -v -- "$0")")" && pwd -P ),我在http://stackoverflow.com/questions/760110/can-i-get-the-absolute-path-to-the-current-script-in-kornshell找到的。
  • 我想知道command 的用法应该在那里做什么。对于不在PATH 中的任何内容,这似乎都是一个错误。
  • @EtanReisner:command -v 只要它的参数是一个可执行文件路径(而不是仅仅文件名,在这种情况下匹配将被限制为$PATH)。然而,它只是与路径原样相呼应,虽然它似乎没有害处,但我不知道它为什么会在那里。

标签: bash posix sh


【解决方案1】:

$BASH_SOURCE 的 POSIX-shell (sh) 对应物是 $0背景信息见底部

警告:关键的区别在于如果您的脚本是来源(使用. 加载到当前 shell ),下面的 sn-ps 将无法正常工作。 下面有进一步的解释

注意我在下面的sn-ps中把DIR改成了dir,因为是better not to use all-uppercase variable names,以免和环境变量和特殊的shell变量发生冲突。
CDPATH= 前缀取代了原始命令中的 > /dev/null$CDPATH 设置为空字符串,以确保 cd 永远不会回显任何内容。

在最简单的情况下,这样做(OP 的命令的等效项):

dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)

如果您还想resulting 目录路径解析为最终目标,以防目录和/或其组件是符号链接,将-P 添加到pwd 命令:

dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)

警告:这与查找脚本自己的真实原始目录相同:
假设您的脚本foo 符号链接到$PATH 中的/usr/local/bin/foo,但它的真实路径是/foodir/bin/foo
以上仍会报告/usr/local/bin,因为符号链接解析(-P)应用于目录/usr/local/bin,而不是脚本本身。

要找到脚本自己的真实来源目录,您必须检查 脚本的 路径以查看它是否是 符号链接 em> ,如果是这样,按照符号链接(链)指向最终目标文件,然后从 target 文件的规范路径中提取目录路径。

GNU 的 readlink -f(更好的是:readlink -e)可以为您做到这一点,但 readlink 不是 POSIX 实用程序。
虽然包括 macOS 在内的 BSD 平台也有一个 readlink 实用程序, 在 macOS 上它不支持 -f 的功能。也就是说,为了显示任务变得多么简单如果readlink -f 可用
dir=$(dirname "$(readlink -f -- "$0")")

事实上,没有 POSIX 实用程序可用于解析文件 符号链接。 有一些方法可以解决这个问题,但它们很麻烦并且不完全健壮:

以下 POSIX 兼容的 shell 函数实现了 GNU 的 readlink -e 所做的,并且是一个相当稳健的解决方案仅在两种罕见的极端情况下失败

  • 嵌入换行符的路径(非常罕见)
  • 包含文字字符串 -> 的文件名(也很少见)

使用这个函数,命名为rreadlink,定义后,以下内容确定了脚本真正的原始目录路径

dir=$(dirname -- "$(rreadlink "$0")")

注意:如果您愿意假设存在(非 POSIX)readlink 实用程序 - 涵盖 macOS、FreeBSD 和 Linux - 可以使用类似但更简单的解决方案在this answer 中找到相关问题。

rreadlink() 源代码 - 在脚本中调用它

rreadlink() ( # Execute the function in a *subshell* to localize variables and the effect of `cd`.

  target=$1 fname= targetDir= CDPATH=

  # Try to make the execution environment as predictable as possible:
  # All commands below are invoked via `command`, so we must make sure that `command`
  # itself is not redefined as an alias or shell function.
  # (Note that command is too inconsistent across shells, so we don't use it.)
  # `command` is a *builtin* in bash, dash, ksh, zsh, and some platforms do not even have
  # an external utility version of it (e.g, Ubuntu).
  # `command` bypasses aliases and shell functions and also finds builtins 
  # in bash, dash, and ksh. In zsh, option POSIX_BUILTINS must be turned on for that
  # to happen.
  { \unalias command; \unset -f command; } >/dev/null 2>&1
  [ -n "$ZSH_VERSION" ] && options[POSIX_BUILTINS]=on # make zsh find *builtins* with `command` too.

  while :; do # Resolve potential symlinks until the ultimate target is found.
      [ -L "$target" ] || [ -e "$target" ] || { command printf '%s\n' "ERROR: '$target' does not exist." >&2; return 1; }
      command cd "$(command dirname -- "$target")" # Change to target dir; necessary for correct resolution of target path.
      fname=$(command basename -- "$target") # Extract filename.
      [ "$fname" = '/' ] && fname='' # !! curiously, `basename /` returns '/'
      if [ -L "$fname" ]; then
        # Extract [next] target path, which may be defined
        # *relative* to the symlink's own directory.
        # Note: We parse `ls -l` output to find the symlink target
        #       which is the only POSIX-compliant, albeit somewhat fragile, way.
        target=$(command ls -l "$fname")
        target=${target#* -> }
        continue # Resolve [next] symlink target.
      fi
      break # Ultimate target reached.
  done
  targetDir=$(command pwd -P) # Get canonical dir. path
  # Output the ultimate target's canonical path.
  # Note that we manually resolve paths ending in /. and /.. to make sure we have a normalized path.
  if [ "$fname" = '.' ]; then
    command printf '%s\n' "${targetDir%/}"
  elif  [ "$fname" = '..' ]; then
    # Caveat: something like /var/.. will resolve to /private (assuming /var@ -> /private/var), i.e. the '..' is applied
    # AFTER canonicalization.
    command printf '%s\n' "$(command dirname -- "${targetDir}")"
  else
    command printf '%s\n' "${targetDir%/}/$fname"
  fi
)

为了稳健和可预测,该函数使用 command 来确保仅调用 shell 内置函数或外部实用程序(忽略别名和函数形式的重载)。
它已在以下 shell 的最新版本中进行了测试:bashdashkshzsh


如何处理来源调用:

tl;dr

仅使用 POSIX 功能

  • 无法来源调用中确定脚本的路径zsh 除外,其中,但是,通常不充当sh)。
  • 可以检测是否只有当您的脚本直接由 shell 获取时,您的脚本才被获取(例如在shell 配置文件/初始化文件;可能通过 源),通过将 $0 与 shell 可执行文件名称/路径进行比较(zsh 除外,其中,如前所述,$0 是真正的当前脚本的路径)。相比之下(zsh 除外),源自另一个脚本 的脚本本身被直接调用,在@ 中包含那个 脚本的路径987654372@.
  • 为了解决这些问题,bashkshzsh 具有非标准 功能确实 允许确定实际的脚本路径,甚至在源场景中,还检测脚本是否被源;比如在bash中,$BASH_SOURCE总是包含了运行脚本的路径,是否被源码,[[ $0 != "$BASH_SOURCE" ]]可以用来测试脚本是否被源码。李>

为了说明为什么不能这样做,我们来分析来自Walter A's answer的命令:

    # NOT recommended - see discussion below.
    DIR=$( cd -P -- "$(dirname -- "$(command -v -- "$0")")" && pwd -P )
  • (两个旁白:
    • 使用 -P 两次是多余的 - 与 pwd 一起使用就足够了。
    • 如果恰好设置了 $CDPATH,则该命令缺少 cd 的潜在标准输出输出的静默。)
  • command -v -- "$0"
    • command -v -- "$0" 旨在涵盖另一种情况:如果脚本是从 交互式 shell 获取的,$0 通常包含文件名 的 shell 可执行文件 (sh),在这种情况下,dirname 将简单地返回 .(因为当给定一个没有 path 组件的参数时,dirname 总是这样做)。 command -v -- "$0" 然后通过 $PATH 查找 (/bin/sh) 返回该 shell 的绝对路径。但是请注意,某些平台(例如 OSX)上的 login shell 在$0 (-sh) 中的文件名前缀为-,在这种情况下command -v -- "$0" 不起作用符合预期(返回一个空字符串)。
    • 相反,command -v -- "$0" 可能在两个来源场景中出现异常行为,其中 shell 可执行文件 sh直接调用,以脚本作为参数:
      • 如果脚本本身是不可可执行的:command -v -- "$0" 可能会返回一个空字符串,这取决于在给定系统上充当sh 的特定shell:@ 987654402@、kshzsh 返回一个空字符串;只有dash 回显$0
        POSIX spec. for command 没有明确说明command -v 在应用于文件系统路径时是否应该只返回可执行文件 - 这就是 bashkshzsh 所做的 - 但您可以争辩说,这是由 command 的目的所暗示的;奇怪的是,dash,通常是最符合 POSIX 的公民,在这里偏离了标准。相比之下,ksh 是这里唯一的模型公民,因为它是唯一一个仅报告可执行文件并且使用绝对(尽管未标准化)路径报告它们的模型公民,按照规范的要求。
      • 如果脚本可执行的,但不在$PATH 中,并且调用使用它的仅仅是文件名(例如,sh myScript),command -v -- "$0" 将也返回 空字符串dash 除外。
    • 鉴于脚本的目录无法在脚本被获取时确定 - 因为$0 不包含该信息(zsh 除外,这通常不会充当sh) - 这个问题没有好的解决方案。
      • 在这种情况下返回 shell 可执行文件的 目录路径的用处有限 - 毕竟,它不是 脚本的 目录 - 除非以后在测试 来确定是否脚本正在被获取。
        • 更可靠的方法是直接测试$0[ "$0" = "sh" ] || [ "$0" = "-sh" ] || [ "$0" = "/bin/sh" ]
      • 但是,如果脚本是从另一个脚本(它本身直接调用的)获取的,那么即使这样也不起作用,因为$0只包含采购脚本的路径。
    • 鉴于 command -v -- "$0" 在来源场景中的有限用处以及它破坏了两个来源场景的事实,我的投票是不使用它,这就离开了我们与:
      • 涵盖所有来源的场景
      • sourced 调用中,您无法确定脚本的路径,并且充其量,在有限的情况下,您可以检测是否采购正在发生:
        • 当由 shell直接获取时(例如从 shell 配置文件/初始化文件),$dir 最终会包含 .,如果 shell 可执行文件被调用为仅仅是一个文件名 (将dirname 应用于单纯的文件名总是 返回.),否则返回shell 可执行文件的目录路径。 . 无法可靠地区分来自当前目录的非源调用。
        • 当来自另一个脚本(它本身没有被来源)时,$0 包含 那个脚本的路径,并且被来源的脚本无法判断是否就是这样。

背景资料:

POSIX 定义了$0 相对于shell 脚本 here行为。

基本上,$0 应该反映脚本文件的路径指定,这意味着:

  • 不要依赖包含绝对路径的$0
  • $0 包含绝对路径,仅当:

    • 明确指定绝对路径;例如。:
      • ~/bin/myScript(假设脚本本身是可执行的)
      • sh ~/bin/myScript
    • 您通过仅文件名调用一个可执行脚本,这要求它在$PATH中既是可执行的;系统在后台将myScript转换成绝对路径然后执行;例如。:
      • myScript # executes /home/jdoe/bin/myScript, for instance
  • 在所有其他情况下,$0 将反映脚本路径指定

    • 当使用脚本显式调用sh 时,这可以是仅仅是文件名(例如,sh myScript)或相对路径(例如,sh ./myScript )
    • 直接调用可执行脚本时,这可以是相对路径(例如,./myScript - 请注意仅仅是文件名只能在$PATH中找到脚本)。

在实践中,bashdashkshzsh 都表现出这种行为。

相比之下,当采购脚本(使用特殊的内置实用程序.(“点”) ),因此您不能依赖它,并且在实践中,shell 的行为会有所不同。

  • 因此,您不能在获取脚本时盲目使用$0 并期望标准化行为。
    • 实际上,bashdashksh 在采购脚本时保留$0 未触及,这意味着$0 包含调用者的 @ 987654459@ 值,或者更准确地说,是调用链中最近调用者的$0 值,该调用者本身尚未被获取;因此,$0 可以指向 shell 的可执行文件或指向当前脚本的 another(直接调用)脚本的路径。
    • 相比之下,zsh,作为唯一的反对者,实际上确实$0 中报告当前脚本的路径。相反,$0 不会提供有关脚本是否来自源的指示。
    • 简而言之:仅使用 POSIX 功能,您既不能可靠地判断手头的脚本是否是来源,也不能可靠地判断手头的脚本的路径是什么,也不能确定 $0 与当前脚本路径的关系是。
  • 如果您确实需要处理这种情况,您必须识别手头的特定外壳并访问其特定非标准功能
    • bashkshzsh 都提供了他们自己的方法来获取运行脚本的路径,即使它正在被获取。

为了完整起见:$0其他上下文中的值

  • 在外壳函数中,POSIX 要求$0 保持不变;因此,无论它在函数外部具有什么值,它也将在内部具有。
    • 在实践中,bashdashksh 的行为确实如此。
    • 同样,zsh 是唯一的反对者,并报告了函数的名称。
  • 在启动时通过-c 选项 接受命令字符串的shell 中,设置$0 的是第一个操作数(非选项参数);例如。:
    • sh -c 'echo \$0: $0 \$1: $1' foo one # -> '$0: foo $1: one'
    • bashdashkshzsh 都是这种行为。
  • 否则,在执行脚本文件的shell中,$0是shell的的第一个参数的值>父进程已通过 - 通常是shell的名称或路径(例如sh/bin/sh);这包括:
    • 交互式外壳
      • 警告:某些平台,尤其是 OSX,总是在创建交互式 shell 时创建 login shell,并在放置前将 - 添加到 shell 名称前在$0 中,以便向 shell 发出信号,表明它是一个 _login shell;因此,默认情况下,$0 在 OSX 上的交互式 shell 中报告 -bash,而不是 bash
    • stdin 读取 命令的 shell
      • 这也适用于通过标准输入将脚本文件传送到 shell(例如,sh < myScript
    • bashdashkshzsh 都是这种行为。

【讨论】:

  • @Sandburg:我已经添加了一个链接到你提到的这个答案的答案,但需要注意的是它依赖于非 POSIX readlink 实用程序;鉴于 macOS、FreeBSD 和 Linux 都具有此实用程序,它是一种实用且更简单的替代方案,可能适用于许多人。
  • 感谢 rreadlink() 函数,非常有帮助!此行:command cd "$(command dirname -- "$target")" 位于 while 循环内,在 macOS 上发出一些意外输出。我通过将输出重定向到 /dev/null 来解决我的情况:command cd "$(command dirname -- "$target")" >/dev/null 2>&1
  • 令人难以置信的 rreadlink() 脚本,我在我正在使用的脚本中链接到这个答案。您为全球 POSIX 社区提供了出色的服务。
  • 我很高兴听到这个消息,@cosmicexplorer;非常感谢您的反馈。
  • @mklement0 -- 抱歉延迟回复。我只是试图重现我在 bash 3.x 和 5.x(在 macOS 上)以及相对最新版本的 dash、ksh 和 zsh shell 上遇到的问题,但没有成功。顺便说一句,我确实记得遇到了 ksh 的另一个问题,但事实证明这是 ksh 实现的问题,而不是您的脚本。无论如何,感谢您提供此脚本,它对于构建必须获取并需要知道其绝对位置的 POSIX 兼容的 shell 脚本非常有价值
【解决方案2】:

@City 回应说

DIR=$( cd -P -- "$(dirname -- "$(command -v -- "$0")")" && pwd -P )

有效。我也用过。
我在https://stackoverflow.com/questions/760110/can-i-get-the-absolute-path-to-the-cu‌​rrent-script-in-kornshell找到了这个命令。

【讨论】:

  • 没有害处,但通常在 cmets 中引用了其他堆栈帖子,而不是作为答案发布。提供链接的 SO 格式是 [Link Title](url)。 (您可以通过用双星号包围链接标题来包含普通粗体)例如Can I get the absolute path to the current script in KornShell?
  • @David 感谢您的格式,我确实先将其作为评论发布。我的回复说我的评论有效,所以我想提供一个结束这个问题的可能性。其他人可能会认为曼城现在仍在寻找答案。你能告诉我应该怎么做吗?
  • 即使评论有效,也应该在评论中留下作为参考。为什么?因为答案已经在 SO 上,而对 SO 的一个巨大头痛是在不同的问题下分散在 SO 上的相同答案的不同版本可以忽略不计。目标是将所有相关答案放在一个问题下,以便所有人都能轻松找到/引用它们。 (我们永远不会100%到达那里,但是当您在另一个问题下找到答案时,请不要再重复它)
  • @DavidC.Rankin:请注意,这个问题是关于 POSIX shell (sh),而这里链接的问题是关于 ksh,问题在顶部的 cmets 中链接的是 bash 问题;虽然这些问题的 一些 答案恰好包含也符合 POSIX 的解决方案,但并未就此进行讨论,即使这个问题密切相关,但它是不同的,具有普遍意义,并且在这里值得拥有自己的规范答案。也许 Walter 可以复制带有 ksh 标记的解决方案并解释为什么它可以与 所有 POSIX 兼容的 shell 一起使用。
【解决方案3】:
if      OLDPWD=/dev/fd/0 \
        cd - && ls -lLidFH ?
then    cd . <8
fi      </proc/self/fd 8<. 9<$0

那里。这应该使您能够通过一些魔术链接作为文件描述符来更改 directpry。

设置$OLDPWD pre-cd 导出一个更改目录持续时间的值(注意:cd 可能对hash 表有残余影响,但唯一的sh我知道实际上男性对这些有任何好处的是 kevin ahlmquists - 并且因为 herbert xu - dash,也许还有一些 bsd 的东西,但我知道什么?) 但不会继承任何 @ 987654329@ 由于更改而导出。

因此,$OLDPWD 实际上并没有改变,如果它有任何价值,那么它仍然保持原样。 $PWD 作为第一个 cd 的结果而更改,值变为 /dev/fd/0 指向 /proc/self/fd,其中我们进程的文件描述符列表应该在 . 中,以包括任何 @987654336 @ 在 ./2 上。

所以我们发送ls ... ? 并查看我们可以获得的所有精彩信息,然后我们就从哪里来。

耶!

【讨论】:

  • 问题是关于 POSIX 兼容的解决方案,因此您不能依赖 Linux 文件系统 (/proc/self)。除非你运行源脚本——无论如何你不能依赖$0——你不必担心OLDPWD(这不是一个POSIX强制的shell变量开始)。当我在 Ubuntu 18.04 上运行它时,我得到文字 /dev/fd/0,然后是进程的文件描述符列表(加上关于描述符 3 的错误消息) - 我没有看到脚本的(符号链接解析)目录路径,这就是问题所在。
  • @mklement0:哟。
  • @mklement0:哟。
猜你喜欢
  • 2022-06-16
  • 1970-01-01
  • 1970-01-01
  • 2022-12-04
  • 2011-01-07
  • 1970-01-01
  • 2010-09-08
相关资源
最近更新 更多