例子:
absolute="/foo/bar"
current="/foo/baz/foo"
# Magic
relative="../../bar"
我如何创造魔法(希望不是太复杂的代码…)?
例子:
absolute="/foo/bar"
current="/foo/baz/foo"
# Magic
relative="../../bar"
我如何创造魔法(希望不是太复杂的代码…)?
当前回答
这是我的版本。这是基于@Offirmo的回答。我使它与dash兼容,并修复了以下测试用例失败:
sh - compute-relative。“a / b / c - de - f / g " / a / b / c / def / g /" --> "../.. f - g - "
Now:
CT_FindRelativePath”a / b / c - de - f / g " / a / b / c / def / g /" --> "../../../ def - g”
查看代码:
# both $1 and $2 are absolute paths beginning with /
# returns relative path to $2/$target from $1/$source
CT_FindRelativePath()
{
local insource=$1
local intarget=$2
# Ensure both source and target end with /
# This simplifies the inner loop.
#echo "insource : \"$insource\""
#echo "intarget : \"$intarget\""
case "$insource" in
*/) ;;
*) source="$insource"/ ;;
esac
case "$intarget" in
*/) ;;
*) target="$intarget"/ ;;
esac
#echo "source : \"$source\""
#echo "target : \"$target\""
local common_part=$source # for now
local result=""
#echo "common_part is now : \"$common_part\""
#echo "result is now : \"$result\""
#echo "target#common_part : \"${target#$common_part}\""
while [ "${target#$common_part}" = "${target}" -a "${common_part}" != "//" ]; do
# no match, means that candidate common part is not correct
# go up one level (reduce common part)
common_part=$(dirname "$common_part")/
# and record that we went back
if [ -z "${result}" ]; then
result="../"
else
result="../$result"
fi
#echo "(w) common_part is now : \"$common_part\""
#echo "(w) result is now : \"$result\""
#echo "(w) target#common_part : \"${target#$common_part}\""
done
#echo "(f) common_part is : \"$common_part\""
if [ "${common_part}" = "//" ]; then
# special case for root (no common path)
common_part="/"
fi
# since we now have identified the common part,
# compute the non-common part
forward_part="${target#$common_part}"
#echo "forward_part = \"$forward_part\""
if [ -n "${result}" -a -n "${forward_part}" ]; then
#echo "(simple concat)"
result="$result$forward_part"
elif [ -n "${forward_part}" ]; then
result="$forward_part"
fi
#echo "result = \"$result\""
# if a / was added to target and result ends in / then remove it now.
if [ "$intarget" != "$target" ]; then
case "$result" in
*/) result=$(echo "$result" | awk '{ string=substr($0, 1, length($0)-1); print string; }' ) ;;
esac
fi
echo $result
return 0
}
其他回答
kasku和Pini的答案略有改进,空格更好,允许传递相对路径:
#!/bin/bash
# both $1 and $2 are paths
# returns $2 relative to $1
absolute=`readlink -f "$2"`
current=`readlink -f "$1"`
# Perl is magic
# Quoting horror.... spaces cause problems, that's why we need the extra " in here:
relative=$(perl -MFile::Spec -e "print File::Spec->abs2rel(q($absolute),q($current))")
echo $relative
这是对@pini目前评分最高的解决方案(遗憾的是,它只处理少数情况)的更正,全功能改进
提醒:'-z'测试如果字符串是零长度(=空),'-n'测试如果字符串不是空。
# both $1 and $2 are absolute paths beginning with /
# returns relative path to $2/$target from $1/$source
source=$1
target=$2
common_part=$source # for now
result="" # for now
while [[ "${target#$common_part}" == "${target}" ]]; do
# no match, means that candidate common part is not correct
# go up one level (reduce common part)
common_part="$(dirname $common_part)"
# and record that we went back, with correct / handling
if [[ -z $result ]]; then
result=".."
else
result="../$result"
fi
done
if [[ $common_part == "/" ]]; then
# special case for root (no common path)
result="$result/"
fi
# since we now have identified the common part,
# compute the non-common part
forward_part="${target#$common_part}"
# and now stick all parts together
if [[ -n $result ]] && [[ -n $forward_part ]]; then
result="$result$forward_part"
elif [[ -n $forward_part ]]; then
# extra slash removal
result="${forward_part:1}"
fi
echo $result
测试用例:
compute_relative.sh "/A/B/C" "/A" --> "../.."
compute_relative.sh "/A/B/C" "/A/B" --> ".."
compute_relative.sh "/A/B/C" "/A/B/C" --> ""
compute_relative.sh "/A/B/C" "/A/B/C/D" --> "D"
compute_relative.sh "/A/B/C" "/A/B/C/D/E" --> "D/E"
compute_relative.sh "/A/B/C" "/A/B/D" --> "../D"
compute_relative.sh "/A/B/C" "/A/B/D/E" --> "../D/E"
compute_relative.sh "/A/B/C" "/A/D" --> "../../D"
compute_relative.sh "/A/B/C" "/A/D/E" --> "../../D/E"
compute_relative.sh "/A/B/C" "/D/E/F" --> "../../../D/E/F"
我把你的问题作为一个挑战,用“可移植的”shell代码来编写它,即。
考虑到POSIX外壳 没有数组之类的bashisms 避免像打瘟疫一样打外部电话。脚本中没有一个分叉!这使得它非常快,特别是在有显著分叉开销的系统上,比如cygwin。 必须处理路径名中的glob字符(*,?,[,])
它运行在任何POSIX兼容shell (zsh, bash, ksh, ash, busybox,…)上。它甚至包含一个测试套件来验证其操作。路径名的规范化留作练习。: -)
#!/bin/sh
# Find common parent directory path for a pair of paths.
# Call with two pathnames as args, e.g.
# commondirpart foo/bar foo/baz/bat -> result="foo/"
# The result is either empty or ends with "/".
commondirpart () {
result=""
while test ${#1} -gt 0 -a ${#2} -gt 0; do
if test "${1%${1#?}}" != "${2%${2#?}}"; then # First characters the same?
break # No, we're done comparing.
fi
result="$result${1%${1#?}}" # Yes, append to result.
set -- "${1#?}" "${2#?}" # Chop first char off both strings.
done
case "$result" in
(""|*/) ;;
(*) result="${result%/*}/";;
esac
}
# Turn foo/bar/baz into ../../..
#
dir2dotdot () {
OLDIFS="$IFS" IFS="/" result=""
for dir in $1; do
result="$result../"
done
result="${result%/}"
IFS="$OLDIFS"
}
# Call with FROM TO args.
relativepath () {
case "$1" in
(*//*|*/./*|*/../*|*?/|*/.|*/..)
printf '%s\n' "'$1' not canonical"; exit 1;;
(/*)
from="${1#?}";;
(*)
printf '%s\n' "'$1' not absolute"; exit 1;;
esac
case "$2" in
(*//*|*/./*|*/../*|*?/|*/.|*/..)
printf '%s\n' "'$2' not canonical"; exit 1;;
(/*)
to="${2#?}";;
(*)
printf '%s\n' "'$2' not absolute"; exit 1;;
esac
case "$to" in
("$from") # Identical directories.
result=".";;
("$from"/*) # From /x to /x/foo/bar -> foo/bar
result="${to##$from/}";;
("") # From /foo/bar to / -> ../..
dir2dotdot "$from";;
(*)
case "$from" in
("$to"/*) # From /x/foo/bar to /x -> ../..
dir2dotdot "${from##$to/}";;
(*) # Everything else.
commondirpart "$from" "$to"
common="$result"
dir2dotdot "${from#$common}"
result="$result/${to#$common}"
esac
;;
esac
}
set -f # noglob
set -x
cat <<EOF |
/ / .
/- /- .
/? /? .
/?? /?? .
/??? /??? .
/?* /?* .
/* /* .
/* /** ../**
/* /*** ../***
/*.* /*.** ../*.**
/*.??? /*.?? ../*.??
/[] /[] .
/[a-z]* /[0-9]* ../[0-9]*
/foo /foo .
/foo / ..
/foo/bar / ../..
/foo/bar /foo ..
/foo/bar /foo/baz ../baz
/foo/bar /bar/foo ../../bar/foo
/foo/bar/baz /gnarf/blurfl/blubb ../../../gnarf/blurfl/blubb
/foo/bar/baz /gnarf ../../../gnarf
/foo/bar/baz /foo/baz ../../baz
/foo. /bar. ../bar.
EOF
while read FROM TO VIA; do
relativepath "$FROM" "$TO"
printf '%s\n' "FROM: $FROM" "TO: $TO" "VIA: $result"
if test "$result" != "$VIA"; then
printf '%s\n' "OOOPS! Expected '$VIA' but got '$result'"
fi
done
# vi: set tabstop=3 shiftwidth=3 expandtab fileformat=unix :
另一个解决方案,纯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:-.}"
}
注:
Care was taken that it is safe against unwanted shell meta character expansion, in case filenames contain * or ?. The output is meant to be usable as the first argument to ln -s: relpath / / gives . and not the empty string relpath a a gives a, even if a happens to be a directory Most common cases were tested to give reasonable results, too. This solution uses string prefix matching, hence readlink is required to canonicalize paths. Thanks to readlink -m it works for not yet existing paths, too.
在旧系统上,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_missing替换上面的readlink -m——)
编辑,因为下面是反对票
下面是一个测试,这个函数确实是正确的:
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中看到的页面栏的准确且唯一正确的相对路径。其他一切都是完全错误的。
采用问题的输出很简单,显然假设current是一个目录:
absolute="/foo/bar"
current="/foo/baz/foo"
relative="../$(relpath "$absolute" "$current")"
这将返回所请求的内容。
在你感到惊讶之前,这里有一个稍微复杂一点的relpath变体(注意细微的区别),它也应该适用于url语法(因此,由于一些bash魔法,末尾/幸存下来):
# 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"
如果你发现什么东西不起作用,请在下面的评论中告诉我。谢谢。
PS:
为什么relpath的论点与这里的所有其他答案相反?
如果你改变
Y="$(readlink -m -- "$2")" || return
to
Y="$(readlink -m -- "${2:-"$PWD"}")" || return
然后你可以去掉第二个参数,这样BASE就是当前目录/URL/任何东西。这只是Unix的原则。
假设您已经安装了:bash、pwd、dirname、echo;relpath是
#!/bin/bash
s=$(cd ${1%%/};pwd); d=$(cd $2;pwd); b=; while [ "${d#$s/}" == "${d}" ]
do s=$(dirname $s);b="../${b}"; done; echo ${b}${d#$s/}
我从pini和其他一些想法中得到了答案
注意:这要求两个路径都是现有文件夹。文件将无法工作。