@Martin Konecny's answer 提供了正确的答案,但是 - 正如他所提到的 - 它仅在不通过驻留在 不同目录 中的 符号链接 调用实际脚本时才有效。
这个答案涵盖了这种情况:当脚本通过 symlink 甚至是 符号链接链 调用时也可以工作的解决方案:
Linux / GNU readlink 解决方案:
如果您的脚本只需要在 Linux 上运行,或者您知道 GNU readlink 在 $PATH 中,请使用 readlink -f,它可以方便地解决指向其最终目标的符号链接:
scriptDir=$(dirname -- "$(readlink -f -- "$BASH_SOURCE")")
请注意,GNU readlink 有 3 个相关选项用于将符号链接解析为其最终目标的完整路径:-f (--canonicalize)、-e (--canonicalize-existing) 和 -m ( --canonicalize-missing) - 见man readlink。
由于此场景中存在定义的目标,因此可以使用 3 个选项中的任何一个;我在这里选择了-f,因为它是最知名的。
多(类 Unix)平台解决方案(包括具有 POSIX-only 实用程序集的平台):
如果您的脚本必须在满足以下条件的任何平台上运行:
以下解决方案,灵感来自https://stackoverflow.com/a/1116890/45375,
定义 helper shell 函数 rreadlink(),它将给定的符号链接解析为循环中的最终目标 - 此函数实际上是 GNU readlink 的 POSIX 兼容实现987654348@ 选项,它类似于-f 选项,只是最终目标必须存在。
注意:该函数是 bash 函数,仅在仅使用带有 POSIX 兼容选项的 POSIX 实用程序的意义上才兼容 POSIX。对于使用符合 POSIX 的 shell 代码(对于 /bin/sh)编写的此函数的自身版本,请参阅 here。
如果readlink 可用,则使用它(不带选项)- 在大多数现代平台上都是如此。
否则,ls -l 的输出将被解析,这是确定符号链接目标的唯一符合 POSIX 标准的方法。
警告:如果文件名或path 包含文字子字符串 -> - 然而,这不太可能。
(请注意,缺少readlink 的平台可能仍会提供其他非POSIX 方法来解析符号链接;例如,@Charles Duffy 提到了HP-UX 的find 实用程序支持%l 格式字符。其-printf 主要; 为简洁起见,该函数不会尝试检测此类情况。)
以下功能的可安装实用程序(脚本)形式(带有附加功能)可以在 rreadlink in the npm registry 中找到;在 Linux 和 macOS 上,使用[sudo] npm install -g rreadlink 安装它;在其他平台上(假设他们有bash),请关注manual installation instructions。
如果参数是符号链接,则返回最终目标的规范路径;否则,返回参数自己的规范路径。
#!/usr/bin/env bash
# Helper function.
rreadlink() ( # execute function in a *subshell* to localize the effect of `cd`, ...
local target=$1 fname targetDir readlinkexe=$(command -v readlink) CDPATH=
# Since we'll be using `command` below for a predictable execution
# environment, we make sure that it has its original meaning.
{ \unalias command; \unset -f command; } &>/dev/null
while :; do # Resolve potential symlinks until the ultimate target is found.
[[ -L $target || -e $target ]] || { command printf '%s\n' "$FUNCNAME: 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 is defined
# relative to the symlink's own directory.
if [[ -n $readlinkexe ]]; then # Use `readlink`.
target=$("$readlinkexe" -- "$fname")
else # `readlink` utility not available.
# Parse `ls -l` output, which, unfortunately, is the only POSIX-compliant
# way to determine a symlink's target. Hypothetically, this can break with
# filenames containig literal ' -> ' and embedded newlines.
target=$(command ls -l -- "$fname")
target=${target#* -> }
fi
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
)
# Determine ultimate script dir. using the helper function.
# Note that the helper function returns a canonical path.
scriptDir=$(dirname -- "$(rreadlink "$BASH_SOURCE")")