例子:

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

# Magic

relative="../../bar"

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


当前回答

这里的答案并不是每天都能用的。由于在纯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解决方案(而不是令人费解的好奇心),我会感到惊讶。

其他回答

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

使用GNU coreutils 8.23中的realpath是最简单的,我认为:

$ realpath --relative-to="$file1" "$file2"

例如:

$ realpath --relative-to=/usr/bin/nmap /tmp/testing
../../../tmp/testing

下面是一个shell脚本,它可以在不调用其他程序的情况下完成:

#! /bin/env bash 

#bash script to find the relative path between two directories

mydir=${0%/}
mydir=${0%/*}
creadlink="$mydir/creadlink"

shopt -s extglob

relpath_ () {
        path1=$("$creadlink" "$1")
        path2=$("$creadlink" "$2")
        orig1=$path1
        path1=${path1%/}/
        path2=${path2%/}/

        while :; do
                if test ! "$path1"; then
                        break
                fi
                part1=${path2#$path1}
                if test "${part1#/}" = "$part1"; then
                        path1=${path1%/*}
                        continue
                fi
                if test "${path2#$path1}" = "$path2"; then
                        path1=${path1%/*}
                        continue
                fi
                break
        done
        part1=$path1
        path1=${orig1#$part1}
        depth=${path1//+([^\/])/..}
        path1=${path2#$path1}
        path1=${depth}${path2#$part1}
        path1=${path1##+(\/)}
        path1=${path1%/}
        if test ! "$path1"; then
                path1=.
        fi
        printf "$path1"

}

relpath_test () {
        res=$(relpath_ /path1/to/dir1 /path1/to/dir2 )
        expected='../dir2'
        test_results "$res" "$expected"

        res=$(relpath_ / /path1/to/dir2 )
        expected='path1/to/dir2'
        test_results "$res" "$expected"

        res=$(relpath_ /path1/to/dir2 / )
        expected='../../..'
        test_results "$res" "$expected"

        res=$(relpath_ / / )
        expected='.'
        test_results "$res" "$expected"

        res=$(relpath_ /path/to/dir2/dir3 /path/to/dir1/dir4/dir4a )
        expected='../../dir1/dir4/dir4a'
        test_results "$res" "$expected"

        res=$(relpath_ /path/to/dir1/dir4/dir4a /path/to/dir2/dir3 )
        expected='../../../dir2/dir3'
        test_results "$res" "$expected"

        #res=$(relpath_ . /path/to/dir2/dir3 )
        #expected='../../../dir2/dir3'
        #test_results "$res" "$expected"
}

test_results () {
        if test ! "$1" = "$2"; then
                printf 'failed!\nresult:\nX%sX\nexpected:\nX%sX\n\n' "$@"
        fi
}

#relpath_test

来源:http://www.ynform.org/w/Pub/Relpath

这个答案并没有解决问题的Bash部分,但是因为我试图使用这个问题中的答案在Emacs中实现这个功能,所以我就把它扔到那里。

Emacs实际上有一个开箱即用的函数:

ELISP> (file-relative-name "/a/b/c" "/a/b/c")
"."
ELISP> (file-relative-name "/a/b/c" "/a/b")
"c"
ELISP> (file-relative-name "/a/b/c" "/c/b")
"../../a/b/c"

自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

...会很好。