例子:

absolute="/foo/bar"
current="/foo/baz/foo"

# Magic

relative="../../bar"

我如何创造魔法(希望不是太复杂的代码…)?


当前回答

自2001年以来,它被内置到Perl中,因此它几乎可以在您能想象到的所有系统上工作,甚至VMS。

perl -le 'use File::Spec; print File::Spec->abs2rel(@ARGV)' FILE BASE

而且,解决方案很容易理解。

举个例子:

perl -le 'use File::Spec; print File::Spec->abs2rel(@ARGV)' $absolute $current

...会很好。

其他回答

这里的答案并不是每天都能用的。由于在纯bash中很难正确地做到这一点,我建议以下可靠的解决方案(类似于注释中的一个建议):

function relpath() { 
  python -c "import os,sys;print(os.path.relpath(*(sys.argv[1:])))" "$@";
}

然后,你可以得到基于当前目录的相对路径:

echo $(relpath somepath)

或者你可以指定路径相对于给定的目录:

echo $(relpath somepath /etc)  # relative to /etc

一个缺点是这需要python,但是:

它在任何python >= 2.6中工作相同 它不要求文件或目录存在。 文件名可以包含更广泛的特殊字符。 例如,如果文件名包含 空格或其他特殊字符。 它是一个单行函数,不会使脚本混乱。

注意,包含basename或dirname的解决方案不一定更好,因为它们要求安装coreutils。如果有人有可靠而简单的纯bash解决方案(而不是令人费解的好奇心),我会感到惊讶。

另一个解决方案,纯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的原则。

我猜这个也可以…(自带内置测试):)

好吧,预计会有一些开销,但我们在这里做的是伯恩壳!;)

#!/bin/sh

#
# Finding the relative path to a certain file ($2), given the absolute path ($1)
# (available here too http://pastebin.com/tWWqA8aB)
#
relpath () {
  local  FROM="$1"
  local    TO="`dirname  $2`"
  local  FILE="`basename $2`"
  local  DEBUG="$3"

  local FROMREL=""
  local FROMUP="$FROM"
  while [ "$FROMUP" != "/" ]; do
    local TOUP="$TO"
    local TOREL=""
    while [ "$TOUP" != "/" ]; do
      [ -z "$DEBUG" ] || echo 1>&2 "$DEBUG$FROMUP =?= $TOUP"
      if [ "$FROMUP" = "$TOUP" ]; then
        echo "${FROMREL:-.}/$TOREL${TOREL:+/}$FILE"
        return 0
      fi
      TOREL="`basename $TOUP`${TOREL:+/}$TOREL"
      TOUP="`dirname $TOUP`"
    done
    FROMREL="..${FROMREL:+/}$FROMREL"
    FROMUP="`dirname $FROMUP`"
  done
  echo "${FROMREL:-.}${TOREL:+/}$TOREL/$FILE"
  return 0
}

relpathshow () {
  echo " - target $2"
  echo "   from   $1"
  echo "   ------"
  echo "   => `relpath $1 $2 '      '`"
  echo ""
}

# If given 2 arguments, do as said...
if [ -n "$2" ]; then
  relpath $1 $2

# If only one given, then assume current directory
elif [ -n "$1" ]; then
  relpath `pwd` $1

# Otherwise perform a set of built-in tests to confirm the validity of the method! ;)
else

  relpathshow /usr/share/emacs22/site-lisp/emacs-goodies-el \
              /usr/share/emacs22/site-lisp/emacs-goodies-el/filladapt.el

  relpathshow /usr/share/emacs23/site-lisp/emacs-goodies-el \
              /usr/share/emacs22/site-lisp/emacs-goodies-el/filladapt.el

  relpathshow /usr/bin \
              /usr/share/emacs22/site-lisp/emacs-goodies-el/filladapt.el

  relpathshow /usr/bin \
              /usr/share/emacs22/site-lisp/emacs-goodies-el/filladapt.el

  relpathshow /usr/bin/share/emacs22/site-lisp/emacs-goodies-el \
              /etc/motd

  relpathshow / \
              /initrd.img
fi
#!/bin/bash
# both $1 and $2 are absolute paths
# returns $2 relative to $1

source=$1
target=$2

common_part=$source
back=
while [ "${target#$common_part}" = "${target}" ]; do
  common_part=$(dirname $common_part)
  back="../${back}"
done

echo ${back}${target#$common_part/}

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