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



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

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

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

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

请注意,我在下面的代码片段中已将DIR更改为dir,因为最好不要使用全大写变量名称,以避免与环境变量和特殊 shell 变量发生冲突.
CDPATH=前缀取代了原始命令中的> /dev/null$CDPATH设置为空字符串,以确保cd永远不会回显任何内容。

在最简单的情况下,这将完成(相当于 OP 的命令):

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

如果还想将生成的目录路径解析为其最终目标,以防目录和/或其组件是符号链接,请将-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,而不是剧本本身。

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

GNU的readlink -f(更好的:readlink -e)可以为你做到这一点,但readlink不是POSIX实用程序.
虽然BSD平台,包括macOS,也有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 - 可以在相关问题的答案中找到类似但更简单的解决方案。

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 '%sn' "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 '%sn' "${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 '%sn' "$(command dirname -- "${targetDir}")"
else
command printf '%sn' "${targetDir%/}/$fname"
fi
)

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


如何处理调用:

tl;博士

仅使用POSIX 功能

  • 您无法在调用确定脚本的路径(zsh除外,但是,它通常不充当sh)。

  • 只有当你的脚本直接由 shell 获取时(例如在 shell 配置文件/初始化文件中;可能通过源),你才能检测你的脚本是否被获取,方法是将$0与 shell 可执行文件名称/路径进行比较(zsh除外,如前所述,其中$0确实是当前脚本的路径)。相比之下(zsh除外),一个脚本来源于另一个脚本,而另一个脚本本身被直接调用,包含脚本在$0中的路径。

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

    • 此答案在确定给定脚本是否正在获取的上下文中间接显示了这些技术。

为了说明为什么不能做到这一点,让我们分析一下 Walter A 答案中的命令:

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

>背景信息:POSIX在此处定义了$0相对于 shell脚本的行为

本质上,$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都表现出这种行为。

相比之下,POSIX 在获取脚本时不强制要求$0的值(使用特殊的内置实用程序.("点")),因此您不能依赖它,并且在实践中,行为因 shell 而异。

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

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

在 shell 函数内部,
  • 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);这包括: 交互式 shell 警告:一些平台,特别是 OSX,在创建交互式 shell
      • 时总是创建登录shell,并在 shell 名称前面加上-,然后再将其放入$0,以便向 shell 发出信号,表明它是一个_login shell;因此,默认情况下,$0在 OSX 上的交互式 shell 中报告-bash,而不是bash
    • stdin读取命令的 shell 这
      • 也适用于通过 stdin 将脚本文件管道传输到 shell(例如,sh < myScript)
    • bashdashkshzsh都是这样表现的。

@City回应说

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

工程。我也用过。
我在 rrent-script-in-kornshell 中找到 https://stackoverflow.com/questions/760110/can-i-get-the-absolute-path-to-the-cu 命令。

if      OLDPWD=/dev/fd/0 
cd - && ls -lLidFH ?
then    cd . <8
fi      </proc/self/fd 8<. 9<$0

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

$OLDPWDpre-cd设置导出一个更改目录持续时间的值(注意:cd可能会对hash表产生残余影响,但我知道唯一一个实际上男性很好地使用这些表的sh是凯文·阿尔姆奎斯特 - 自从赫伯特·徐 -dash,也许还有一些bsd的东西, 但我知道什么?但不因更改而结转任何cd出口。

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

所以我们做了一个ls ... ?,看看我们能得到的所有精彩信息,然后我们去我们来的地方。

耶!

相关内容

  • 没有找到相关文章

最新更新