另一种解决方案,纯 bash + GNU readlink 以便在以下上下文中轻松使用:
ln -s "$(relpath "$A" "$B")" "$B"
编辑:在这种情况下,请确保“$B”不存在或没有软链接,否则relpath 跟随此链接,这不是您想要的!
这适用于几乎所有当前的 Linux。如果readlink -m 在您身边不起作用,请尝试使用readlink -f。另请参阅https://gist.github.com/hilbix/1ec361d00a8178ae8ea0 了解可能的更新:
: relpath A B
# Calculate relative path from A to B, returns true on success
# Example: ln -s "$(relpath "$A" "$B")" "$B"
relpath()
{
local X Y A
# We can create dangling softlinks
X="$(readlink -m -- "$1")" || return
Y="$(readlink -m -- "$2")" || return
X="${X%/}/"
A=""
while Y="${Y%/*}"
[ ".${X#"$Y"/}" = ".$X" ]
do
A="../$A"
done
X="$A${X#"$Y"/}"
X="${X%/}"
echo "${X:-.}"
}
注意事项:
- 注意防止不必要的 shell 元字符扩展,以防文件名包含
* 或 ?。
- 输出可用作
ln -s 的第一个参数:
-
relpath / / 给出 . 而不是空字符串
-
relpath a a 给出a,即使a 恰好是一个目录
- 也对大多数常见案例进行了测试以得出合理的结果。
- 此解决方案使用字符串前缀匹配,因此需要
readlink 来规范化路径。
- 感谢
readlink -m,它也适用于尚不存在的路径。
在旧系统上,readlink -m 不可用,如果文件不存在,readlink -f 将失败。所以你可能需要一些这样的解决方法(未经测试!):
readlink_missing()
{
readlink -m -- "$1" && return
readlink -f -- "$1" && return
[ -e . ] && echo "$(readlink_missing "$(dirname "$1")")/$(basename "$1")"
}
如果$1 包含不存在路径的. 或..(如/doesnotexist/./a),这并不完全正确,但它应该涵盖大多数情况。
(将上面的readlink -m --替换为readlink_missing。)
编辑因为不赞成投票跟随
这是一个测试,这个函数确实是正确的:
check()
{
res="$(relpath "$2" "$1")"
[ ".$res" = ".$3" ] && return
printf ':WRONG: %-10q %-10q gives %q\nCORRECT %-10q %-10q gives %q\n' "$1" "$2" "$res" "$@"
}
# TARGET SOURCE RESULT
check "/A/B/C" "/A" ".."
check "/A/B/C" "/A.x" "../../A.x"
check "/A/B/C" "/A/B" "."
check "/A/B/C" "/A/B/C" "C"
check "/A/B/C" "/A/B/C/D" "C/D"
check "/A/B/C" "/A/B/C/D/E" "C/D/E"
check "/A/B/C" "/A/B/D" "D"
check "/A/B/C" "/A/B/D/E" "D/E"
check "/A/B/C" "/A/D" "../D"
check "/A/B/C" "/A/D/E" "../D/E"
check "/A/B/C" "/D/E/F" "../../D/E/F"
check "/foo/baz/moo" "/foo/bar" "../bar"
困惑?嗯,这些是正确的结果!即使您认为它不适合这个问题,也可以证明这是正确的:
check "http://example.com/foo/baz/moo" "http://example.com/foo/bar" "../bar"
毫无疑问,../bar 是从页面moo 看到的页面bar 的准确且唯一正确的相对路径。其他一切都是错误的。
采用显然假设current 是一个目录的问题的输出是微不足道的:
absolute="/foo/bar"
current="/foo/baz/foo"
relative="../$(relpath "$absolute" "$current")"
这将准确返回所要求的内容。
在你挑眉之前,这里有一个更复杂的 relpath 变体(发现细微差别),它也应该适用于 URL 语法(所以尾随 / 仍然存在,感谢一些 @ 987654357@-magic):
# Calculate relative PATH to the given DEST from the given BASE
# In the URL case, both URLs must be absolute and have the same Scheme.
# The `SCHEME:` must not be present in the FS either.
# This way this routine works for file paths an
: relpathurl DEST BASE
relpathurl()
{
local X Y A
# We can create dangling softlinks
X="$(readlink -m -- "$1")" || return
Y="$(readlink -m -- "$2")" || return
X="${X%/}/${1#"${1%/}"}"
Y="${Y%/}${2#"${2%/}"}"
A=""
while Y="${Y%/*}"
[ ".${X#"$Y"/}" = ".$X" ]
do
A="../$A"
done
X="$A${X#"$Y"/}"
X="${X%/}"
echo "${X:-.}"
}
这里的检查只是为了说明:它确实如所描述的那样工作。
check()
{
res="$(relpathurl "$2" "$1")"
[ ".$res" = ".$3" ] && return
printf ':WRONG: %-10q %-10q gives %q\nCORRECT %-10q %-10q gives %q\n' "$1" "$2" "$res" "$@"
}
# TARGET SOURCE RESULT
check "/A/B/C" "/A" ".."
check "/A/B/C" "/A.x" "../../A.x"
check "/A/B/C" "/A/B" "."
check "/A/B/C" "/A/B/C" "C"
check "/A/B/C" "/A/B/C/D" "C/D"
check "/A/B/C" "/A/B/C/D/E" "C/D/E"
check "/A/B/C" "/A/B/D" "D"
check "/A/B/C" "/A/B/D/E" "D/E"
check "/A/B/C" "/A/D" "../D"
check "/A/B/C" "/A/D/E" "../D/E"
check "/A/B/C" "/D/E/F" "../../D/E/F"
check "/foo/baz/moo" "/foo/bar" "../bar"
check "http://example.com/foo/baz/moo" "http://example.com/foo/bar" "../bar"
check "http://example.com/foo/baz/moo/" "http://example.com/foo/bar" "../../bar"
check "http://example.com/foo/baz/moo" "http://example.com/foo/bar/" "../bar/"
check "http://example.com/foo/baz/moo/" "http://example.com/foo/bar/" "../../bar/"
下面是如何使用它来从问题中给出想要的结果:
absolute="/foo/bar"
current="/foo/baz/foo"
relative="$(relpathurl "$absolute" "$current/")"
echo "$relative"
如果您发现某些东西不起作用,请在下面的 cmets 中告诉我。谢谢。
PS:
为什么relpath 的论点与这里的所有其他答案相反?
如果你改变了
Y="$(readlink -m -- "$2")" || return
到
Y="$(readlink -m -- "${2:-"$PWD"}")" || return
然后您可以保留第二个参数,这样 BASE 就是当前目录/URL/whatever。和往常一样,这只是 Unix 原则。