如何在POSIX sh 中获取脚本目录?

How to get script directory in POSIX sh?

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

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

@City 回应说

DIR=$( cd -P -- "$(dirname -- "$(command -v -- "[=10=]")")" && pwd -P )

有效。我也用过
我在 .

找到了命令

$BASH_SOURCE 的 POSIX-shell (sh) 对应项是 [=17=]背景信息见底部

注意事项:关键区别在于如果您的脚本是来源(加载到current shell with .),下面的 片段将 not 正常工作。 下面进一步解释

请注意,我已将下面代码段中的 DIR 更改为 dir,因为它是 better not to use all-uppercase variable names 以避免与环境变量和特殊 shell变量。
CDPATH= 前缀取代了原始命令中的 > /dev/null$CDPATH 设置为空字符串,以确保 cd 从不 echoes随便什么。

在最简单的情况下,这样做(相当于 OP 的命令):

dir=$(CDPATH= cd -- "$(dirname -- "[=10=]")" && pwd)

如果您还想结果目录路径解析为其最终目标,以防目录and/or其组件是symlinks,在pwd命令中添加-P

dir=$(CDPATH= cd -- "$(dirname -- "[=11=]")" && pwd -P)

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

要找到脚本自己的真实目录 ,您必须检查 脚本的 的路径查看它是否是 symlink,如果是,则遵循指向最终目标文件的(链)符号链接,然后从 target 文件的规范路径。

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

事实上,有 no POSIX 工具来解析 file 符号链接. 有一些方法可以解决这个问题,但它们很麻烦而且不够稳健:

下面的POSIX-compliantshell函数实现了GNU的readlink -e所做的并且是相当稳健的解决方案仅在两种罕见的边缘情况下失败:

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

有了这个函数,命名为rreadlink,定义,下面确定脚本的真实目录源路径

dir=$(dirname -- "$(rreadlink "[=12=]")")

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

rreadlink() 源代码 - 在脚本中调用之前放置 :

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

  target= 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


如何处理 sourced 调用:

tl;dr:

仅使用POSIX功能:

  • 无法确定sourced[中的脚本路径 =494=] 调用zsh 除外,但通常不作为 sh)。

  • 可以检测您的脚本是否仅当您的脚本被直接获取时[=] =494=] 通过 shell(例如在 shell profile/initialization 文件中;可能通过 来源) ,通过将 [=17=] 与 shell 可执行文件 name/path 进行比较(zsh 除外,其中 [=17=] 确实是当前脚本的路径)。相比之下(zsh 除外),源自 另一个脚本 的脚本本身 直接调用 ,包含 脚本在 [=17=].

    中的路径
  • 为了解决这些问题,bashkshzsh非标准 features do 允许确定实际的脚本路径,即使在源场景中也可以检测脚本是否正在源;例如,在 bash$BASH_SOURCE 中总是 包含 运行 脚本的路径,无论它是否被源化,并且 [[ [=63=] != "$BASH_SOURCE" ]] 可以用于测试脚本是否被获取。

    • This answer 在确定给定脚本是否为来源的上下文中间接展示了这些技术。

为了说明为什么不能这样做,让我们分析一下来自的命令:

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

背景资料:

POSIX 定义了 [=17=] 相对于 shell scripts[= 的 行为494=]here.

本质上,[=17=]应该按照指定反映脚本文件的路径,这意味着:

  • 不要依赖包含绝对路径.

    [=17=]
  • [=17=] 包含 绝对 路径 仅当:

  • 显式指定一个绝对路径;例如:

    • ~/bin/myScript(假设脚本本身是可执行的)
    • sh ~/bin/myScript
  • 您通过 仅文件名 调用可执行脚本,这要求它在 中都是可执行的 $PATH;在后台,系统将 myScript 转换为绝对路径,然后执行;例如:

    • myScript # executes /home/jdoe/bin/myScript, for instance
  • 在所有其他情况下,[=17=] 将按指定反映脚本 路径 :

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

实际上,bashdashkshzsh 都表现出这种行为。

相比之下,POSIX 在 采购 脚本 时不强制要求 [=17=] 的值(使用特殊的 built-in 实用程序 . ("dot")),所以 你不能依赖它 ,并且在实践中, 行为shells.

不同
  • 因此,您不能在获取脚本资源时盲目地使用 [=17=] 并期待标准化行为。
    • 实际上,bashdashksh 在采购脚本时保留 [=17=] 不变 ,这意味着 [ =17=] 包含 caller 的 [=17=] 值,或者更准确地说,是调用链中最近调用者的 [=17=] 值自己采购;因此,[=17=] 可能指向 shell 的可执行文件或 另一个 (直接调用)脚本的路径,该脚本是当前脚本的来源。
    • 相比之下,zsh作为唯一的异议者,实际上确实报告了当前脚本在[=17中的路径=].相反,[=17=] 将不提供有关脚本是否正在来源的指示。
    • 简而言之:仅使用 POSIX 功能,您既不能可靠地判断手头的脚本是否是来源,也不能判断手头的脚本路径是什么,也不能判断它们之间的关系[=17=]当前脚本的路径是.
  • 如果您确实需要处理这种情况,您必须确定手头的具体 shell 并访问其 具体 non-standard功能
    • bashkshzsh 都提供了自己的 方法来获取 运行 脚本的路径,甚至采购时。

为了完整起见:在其他上下文中[=17=]的值:

  • 在shell函数中,POSIX强制[=17=]保持不变;因此,无论它在外部函数中具有什么值,它也会在内部中具有任何值。
    • 在实践中,bashdashksh 确实如此。
    • 同样,zsh 是唯一的反对者并报告了 函数的 名称。
  • 在启动时通过-c选项接受命令字符串的shell中,它是第一个操作数(non-option 参数)设置 [=17=];例如。:
    • sh -c 'echo $0: [=161=] $1: ' foo one # -> '[=161=]: foo : one'
    • bashdashkshzsh 都是这样。
  • 否则,在shell执行脚本文件[=17=] 是 shell 的 父进程 传递的第一个参数的值 - 通常,这是 shell的名称或路径(例如sh,或/bin/sh);这包括:
    • 互动shell
      • 注意事项:某些平台,尤其是 OSX,在创建交互式 login shell 时总是创建 [= =755=]s,并且 在 shell 名称之前添加 -,然后将其放入 [=17=],以便向 [=755] 发出信号=] 这是一个 _login shell;因此,默认情况下,[=17=] 在 OSX.
      • 上的交互式 shell 中报告 -bash,而不是 bash
    • a shell 从 stdin 读取 命令
      • 这也适用于通过标准输入(例如,sh < myScript)将脚本文件通过管道传输到 shell
    • bashdashkshzsh 都是这样。
if      OLDPWD=/dev/fd/0 \
        cd - && ls -lLidFH ?
then    cd . <8
fi      </proc/self/fd 8<. 9<[=10=]

那里。这应该使您能够通过一些神奇的链接将 directpry 更改为文件描述符。

设置 $OLDPWD pre-cd 在一次更改目录 期间导出值(注意:cd 会对 hash 表,但我知道唯一 sh 实际上男性可以很好地使用这些表的是 kevin ahlmquists - 自从 herbert xu - dash,也许还有一些 bsd 东西,但我知道什么?) 但不会因更改而结转任何 cd 导出。

因此,$OLDPWD 实际上并没有改变,即使它有任何价值,它仍然保持原样。 $PWD 由于第一个 cd 而改变,值变成 /dev/fd/0 指向 /proc/self/fd,我们进程的文件描述符列表应该在 .,包括 [=25=]./2.

上的任何内容

所以我们做了一个 ls ... ? 并查看我们可以获得的所有精彩信息,然后我们去哪里。

耶!