使用 Bash 将给定当前目录的绝对路径转换为相对路径

Posted

技术标签:

【中文标题】使用 Bash 将给定当前目录的绝对路径转换为相对路径【英文标题】:Convert absolute path into relative path given a current directory using Bash 【发布时间】:2011-02-03 14:40:41 【问题描述】:

例子:

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

# Magic

relative="../../bar"

我如何创造魔法(希望代码不要太复杂...)?

【问题讨论】:

例如(我现在的情况)为 gcc 提供相对路径,这样即使源路径发生变化,它也可以生成可用的相对调试信息。 在 U&L 上提出了一个类似的问题:unix.stackexchange.com/questions/100918/…。其中一个答案 (@Gilles) 提到了一个工具,symlinks,它可以轻松解决这个问题。 简单:realpath --relative-to=$absolute $current. 【参考方案1】:

我使用的是默认没有realpath命令的macOS,所以我做了一个pure bash函数来计算它。

#!/bin/bash

##
# print a relative path from "source folder" to "target file"
#
# params:
#  $1 - target file, can be a relative path or an absolute path.
#  $2 - source folder, can be a relative path or an absolute path.
#
# test:
#  $ mkdir -p ~/A/B/C/D; touch ~/A/B/C/D/testfile.txt; touch ~/A/B/testfile.txt
#
#  $ getRelativePath ~/A/B/C/D/testfile.txt  ~/A/B
#  $ C/D/testfile.txt
#  
#  $ getRelativePath ~/A/B/testfile.txt  ~/A/B/C
#  $ ../testfile.txt
#
#  $ getRelativePath ~/A/B/testfile.txt  /
#  $ home/bunnier/A/B/testfile.txt 
#
function getRelativePath()
    local targetFilename=$(basename $1)
    local targetFolder=$(cd $(dirname $1);pwd) # absolute target folder path
    local currentFolder=$(cd $2;pwd) # absulute source folder
    local result=.

    while [ "$currentFolder" != "$targetFolder" ];do
      if [[ "$targetFolder" =~ "$currentFolder"* ]];then
          pointSegment=$targetFolder#$currentFolder
          result=$result/$pointSegment#/
          break
      fi  
      result="$result"/..
      currentFolder=$(dirname $currentFolder)
    done

    result=$result/$targetFilename
    echo $result#./

【讨论】:

【参考方案2】:

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

【讨论】:

【参考方案3】:

假设你已经安装了: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 和其他一些想法中得到了答案

注意:这要求两个路径都是现有文件夹。文件将工作。

【讨论】:

理想答案:与 /bin/sh 一起使用,不需要 readlink、python、perl -> 非常适合轻型/嵌入式系统或 windows bash 控制台 不幸的是,这需要路径存在,这并不总是需要的。 虔诚的回答。 cd-pwd 的东西是为了解决我猜的链接?打高尔夫球不错!【参考方案4】:

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

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

例如:

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

【讨论】:

在Ubuntu 14.04上这个包已经过时了,没有--relative-to选项,很可惜。 在 Ubuntu 16.04 上运行良好 如果您想要相对于当前工作目录的路径,$ realpath --relative-to="$PWD" "$file" 很有用。 这适用于/usr/bin/nmap/-path 中的内容,但不适用于/usr/bin/nmap:从nmap/tmp/testing 只有../../,而不是../ 的3 次。但是它可以工作,因为在 rootfs 上执行 ../ 作为@PatrickB。暗示,--relative-to=… 需要一个目录并且不检查。这意味着如果您请求相对于文件的路径,您最终会得到一个额外的“../”(就像这个例子一样,因为/usr/bin 很少或从不包含目录,而nmap 通常是二进制文件) 【参考方案5】:

这是我的版本。它基于answer by @Offirmo。我使它与 Dash 兼容并修复了以下测试用例失败:

./compute-relative.sh "/a/b/c/de/f/g" "/a/b/c/def/g/" --> "../..f/g/"

现在:

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

【讨论】:

【参考方案6】:

这个答案没有解决问题的 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"

【讨论】:

请注意,对于您提供的测试用例,我相信我最近添加的 python 答案(relpath 函数)的行为与 file-relative-name 相同。【参考方案7】:

它自 2001 年以来内置于 Perl,因此它几乎适用于您能想象到的所有系统,甚至是 VMS。

perl -e 'use File::Spec; print File::Spec->abs2rel(@ARGV) . "\n"' FILE BASE

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

所以对于你的例子:

perl -e 'use File::Spec; print File::Spec->abs2rel(@ARGV) . "\n"' $absolute $current

...可以正常工作。

【讨论】:

say 在 perl 中不能用作日志,但在这里可以有效地使用。 perl -MFile::Spec -E 'say File::Spec->abs2rel(@ARGV)' +1 但另见 this similar answer 较旧(2012 年 2 月)。另请阅读来自William Pursell 的相关 cmets。我的版本是两个命令行:perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target"perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target" "$origin"。第一个单行perl 脚本使用一个参数(原点是当前工作目录)。第二个单行 perl 脚本使用两个参数。 这应该是公认的答案。 perl 几乎随处可见,但答案仍然是单行的。【参考方案8】:

此脚本仅适用于路径名。它不需要任何文件存在。如果传递的路径不是绝对的,则行为有点不寻常,但如果两条路径都是相对的,它应该会按预期工作。

我只在 OS X 上测试过,所以它可能不便携。

#!/bin/bash
set -e
declare SCRIPT_NAME="$(basename $0)"
function usage 
    echo "Usage: $SCRIPT_NAME <base path> <target file>"
    echo "       Outputs <target file> relative to <base path>"
    exit 1


if [ $# -lt 2 ]; then usage; fi

declare base=$1
declare target=$2
declare -a base_part=()
declare -a target_part=()

#Split path elements & canonicalize
OFS="$IFS"; IFS='/'
bpl=0;
for bp in $base; do
    case "$bp" in
        ".");;
        "..") let "bpl=$bpl-1" ;;
        *) base_part[$bpl]="$bp" ; let "bpl=$bpl+1";;
    esac
done
tpl=0;
for tp in $target; do
    case "$tp" in
        ".");;
        "..") let "tpl=$tpl-1" ;;
        *) target_part[$tpl]="$tp" ; let "tpl=$tpl+1";;
    esac
done
IFS="$OFS"

#Count common prefix
common=0
for (( i=0 ; i<$bpl ; i++ )); do
    if [ "$base_part[$i]" = "$target_part[$common]" ] ; then
        let "common=$common+1"
    else
        break
    fi
done

#Compute number of directories up
let "updir=$bpl-$common" || updir=0 #if the expression is zero, 'let' fails

#trivial case (after canonical decomposition)
if [ $updir -eq 0 ]; then
    echo .
    exit
fi

#Print updirs
for (( i=0 ; i<$updir ; i++ )); do
    echo -n ../
done

#Print remaining path
for (( i=$common ; i<$tpl ; i++ )); do
    if [ $i -ne $common ]; then
        echo -n "/"
    fi
    if [ "" != "$target_part[$i]" ] ; then
        echo -n "$target_part[$i]"
    fi
done
#One last newline
echo

【讨论】:

另外,代码有点复制和粘贴,但我需要这个很快。 很好......正是我所需要的。而且你已经包含了一个比我见过的大多数其他人更好的规范化例程(通常依赖于正则表达式替换)。【参考方案9】:

这里没有很多答案适合日常使用。由于在纯 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 中的工作原理都是一样的 不要求文件或目录存在。 文件名可能包含更广泛的特殊字符。 例如,如果文件名包含许多其他解决方案将不起作用 空格或其他特殊字符。 这是一个不会使脚本混乱的单行函数。

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

【讨论】:

这似乎是迄今为止最可靠的方法。【参考方案10】:

这是对来自 @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"

【讨论】:

集成在offirmo shell lib github.com/Offirmo/offirmo-shell-lib,函数«OSL_FILE_find_relative_path»(文件«osl_lib_file.sh») +1。通过将 source=$1; target=$2 替换为 source=$(realpath $1); target=$(realpath $2),可以轻松地处理任何路径(不仅仅是以 / 开头的绝对路径) @Josh 确实如此,前提是 dirs 确实存在......这对于单元测试来说是不方便的 ;) 但在实际使用中是的,建议使用 realpath,或者 source=$(readlink -f $1) 等,如果 realpath不可用(非标准) 我这样定义$source$target:` if [[ -e $1 ]];然后 source=$(readlink -f $1);否则来源=$1;如果 [[ -e $2 ]];然后目标=$(readlink -f $2);否则目标=$2; fi` 这样,该函数可以处理真实/现有的相对路径以及虚构的目录。 @NathanS.Watson-Haigh 更好的是,我最近发现readlink 有一个-m 选项可以做到这一点;)【参考方案11】:
$ python -c "import os.path; print os.path.relpath('/foo/bar', '/foo/baz/foo')"

给予:

../../bar

【讨论】:

它有效,它使替代方案看起来很荒谬。这对我来说是个奖励xD +1。好吧,你被骗了……但这太好了,不能用! relpath() python -c "import os.path; print os.path.relpath('$1','$2:-$PWD')" ; 遗憾的是,这并不是普遍可用的:os.path.relpath 是 Python 2.6 中的新功能。 @ChenLevy:Python 2.6 于 2008 年发布。很难相信它在 2012 年还没有普及。 python -c 'import os, sys; print(os.path.relpath(*sys.argv[1:]))' 工作最自然、最可靠。【参考方案12】:

我需要这样的东西,但它也解决了符号链接。我发现 pwd 为此目的有一个 -P 标志。附加了我的脚本片段。它在 shell 脚本的一个函数中,因此是 $1 和 $2。结果值,即从 START_ABS 到 END_ABS 的相对路径,位于 UPDIRS 变量中。脚本 cd 进入每个参数目录以执行 pwd -P,这也意味着处理相对路径参数。干杯,吉姆

SAVE_DIR="$PWD"
cd "$1"
START_ABS=`pwd -P`
cd "$SAVE_DIR"
cd "$2"
END_ABS=`pwd -P`

START_WORK="$START_ABS"
UPDIRS=""

while test -n "$START_WORK" -a "$END_ABS/#$START_WORK" '==' "$END_ABS";
do
    START_WORK=`dirname "$START_WORK"`"/"
    UPDIRS=$UPDIRS"../"
done
UPDIRS="$UPDIRS$END_ABS/#$START_WORK"
cd "$SAVE_DIR"

【讨论】:

【参考方案13】:

对kasku's 和Pini's 的回答略有改进,可以更好地处理空格并允许传递相对路径:

#!/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

【讨论】:

【参考方案14】:
#!/bin/sh

# Return relative path from canonical absolute dir path $1 to canonical
# absolute dir path $2 ($1 and/or $2 may end with one or no "/").
# Does only need POSIX shell builtins (no external command)
relPath () 
    local common path up
    common=$1%/ path=$2%//
    while test "$path#"$common"/" = "$path"; do
        common=$common%/* up=../$up
    done
    path=$up$path#"$common"/; path=$path%/; printf %s "$path:-."


# Return relative path from dir $1 to dir $2 (Does not impose any
# restrictions on $1 and $2 but requires GNU Core Utility "readlink"
# HINT: busybox's "readlink" does not support option '-m', only '-f'
#       which requires that all but the last path component must exist)
relpath ()  relPath "$(readlink -m "$1")" "$(readlink -m "$2")"; 

以上 shell 脚本的灵感来自 pini's(谢谢!)。它触发了一个错误 在 Stack Overflow 的语法高亮模块中(至少在我的预览中) 框架)。所以如果高亮不正确请忽略。

一些注意事项:

在不显着增加代码的情况下删除了错误并改进了代码 长度和复杂性 将功能放入函数中以方便使用 保持函数与 POSIX 兼容,以便它们(应该)与所有 POSIX 兼容 shells(在 Ubuntu Linux 12.04 中使用 dash、bash 和 zsh 测试) 仅使用局部变量来避免破坏全局变量和 污染全局命名空间 两个目录路径都不需要存在(我的应用程序的要求) 路径名可能包含空格、特殊字符、控制字符、 反斜杠、制表符、'、"、?、*、[、] 等。 核心函数“relPath”仅使用 POSIX shell 内置函数,但需要 规范的绝对目录路径作为参数 扩展函数“relpath”可以处理任意目录路径(也 相对的,非规范的),但需要外部 GNU 核心实用程序“readlink” 避免使用内置“echo”并使用内置“printf”,原因有两个: 由于内置“echo”它的历史实现相互冲突 在不同的 shell 中表现不同 -> POSIX recommends that printf is preferred over echo. 某些 POSIX shell 的内置“回显”将interpret some backslash sequences 并因此损坏包含此类序列的路径名 为避免不必要的转换,在返回时使用路径名 并被 shell 和 OS 实用程序(例如 cd、ln、ls、find、mkdir; 与 python 的“os.path.relpath”不同,它将解释一些反斜杠 序列)

除了提到的反斜杠序列函数“relPath”的最后一行 输出与 python 兼容的路径名:

path=$up$path#"$common"/; path=$path%/; printf %s "$path:-."

最后一行可以用行替换(和简化)

printf %s "$up$path#"$common"/"

我更喜欢后者,因为

    文件名可以直接附加到relPath获取的dir路径中,例如:

    ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
    

    使用此方法创建的同一目录中的符号链接没有 丑陋的"./" 前缀在文件名中。

如果您发现错误,请联系 linuxball (at) gmail.com,我会尝试 修复它。 添加了回归测试套件(也兼容 POSIX shell)

回归测试的代码清单(只需将其附加到 shell 脚本):

############################################################################
# If called with 2 arguments assume they are dir paths and print rel. path #
############################################################################

test "$#" = 2 && 
    printf '%s\n' "Rel. path from '$1' to '$2' is '$(relpath "$1" "$2")'."
    exit 0


#######################################################
# If NOT called with 2 arguments run regression tests #
#######################################################

format="\t%-19s %-22s %-27s %-8s %-8s %-8s\n"
printf \
"\n\n*** Testing own and python's function with canonical absolute dirs\n\n"
printf "$format\n" \
    "From Directory" "To Directory" "Rel. Path" "relPath" "relpath" "python"
IFS=
while read -r p; do
    eval set -- $p
    case $1 in '#'*|'') continue;; esac # Skip comments and empty lines
    # q stores quoting character, use " if ' is used in path name
    q="'"; case $1$2 in *"'"*) q='"';; esac
    rPOk=passed rP=$(relPath "$1" "$2"); test "$rP" = "$3" || rPOk=$rP
    rpOk=passed rp=$(relpath "$1" "$2"); test "$rp" = "$3" || rpOk=$rp
    RPOk=passed
    RP=$(python -c "import os.path; print os.path.relpath($q$2$q, $q$1$q)")
    test "$RP" = "$3" || RPOk=$RP
    printf \
    "$format" "$q$1$q" "$q$2$q" "$q$3$q" "$q$rPOk$q" "$q$rpOk$q" "$q$RPOk$q"
done <<-"EOF"
    # From directory    To directory           Expected relative path

    '/'                 '/'                    '.'
    '/usr'              '/'                    '..'
    '/usr/'             '/'                    '..'
    '/'                 '/usr'                 'usr'
    '/'                 '/usr/'                'usr'
    '/usr'              '/usr'                 '.'
    '/usr/'             '/usr'                 '.'
    '/usr'              '/usr/'                '.'
    '/usr/'             '/usr/'                '.'
    '/u'                '/usr'                 '../usr'
    '/usr'              '/u'                   '../u'
    "/u'/dir"           "/u'/dir"              "."
    "/u'"               "/u'/dir"              "dir"
    "/u'/dir"           "/u'"                  ".."
    "/"                 "/u'/dir"              "u'/dir"
    "/u'/dir"           "/"                    "../.."
    "/u'"               "/u'"                  "."
    "/"                 "/u'"                  "u'"
    "/u'"               "/"                    ".."
    '/u"/dir'           '/u"/dir'              '.'
    '/u"'               '/u"/dir'              'dir'
    '/u"/dir'           '/u"'                  '..'
    '/'                 '/u"/dir'              'u"/dir'
    '/u"/dir'           '/'                    '../..'
    '/u"'               '/u"'                  '.'
    '/'                 '/u"'                  'u"'
    '/u"'               '/'                    '..'
    '/u /dir'           '/u /dir'              '.'
    '/u '               '/u /dir'              'dir'
    '/u /dir'           '/u '                  '..'
    '/'                 '/u /dir'              'u /dir'
    '/u /dir'           '/'                    '../..'
    '/u '               '/u '                  '.'
    '/'                 '/u '                  'u '
    '/u '               '/'                    '..'
    '/u\n/dir'          '/u\n/dir'             '.'
    '/u\n'              '/u\n/dir'             'dir'
    '/u\n/dir'          '/u\n'                 '..'
    '/'                 '/u\n/dir'             'u\n/dir'
    '/u\n/dir'          '/'                    '../..'
    '/u\n'              '/u\n'                 '.'
    '/'                 '/u\n'                 'u\n'
    '/u\n'              '/'                    '..'

    '/    a   b/å/⮀*/!' '/    a   b/å/⮀/xäå/?' '../../⮀/xäå/?'
    '/'                 '/A'                   'A'
    '/A'                '/'                    '..'
    '/  & /  !/*/\\/E'  '/'                    '../../../../..'
    '/'                 '/  & /  !/*/\\/E'     '  & /  !/*/\\/E'
    '/  & /  !/*/\\/E'  '/  & /  !/?/\\/E/F'   '../../../?/\\/E/F'
    '/X/Y'              '/  & /  !/C/\\/E/F'   '../../  & /  !/C/\\/E/F'
    '/  & /  !/C'       '/A'                   '../../../A'
    '/A /  !/C'         '/A /B'                '../../B'
    '/Â/  !/C'          '/Â/  !/C'             '.'
    '/  & /B / C'       '/  & /B / C/D'        'D'
    '/  & /  !/C'       '/  & /  !/C/\\/Ê'     '\\/Ê'
    '/Å/  !/C'          '/Å/  !/D'             '../D'
    '/.A /*B/C'         '/.A /*B/\\/E'         '../\\/E'
    '/  & /  !/C'       '/  & /D'              '../../D'
    '/  & /  !/C'       '/  & /\\/E'           '../../\\/E'
    '/  & /  !/C'       '/\\/E/F'              '../../../\\/E/F'
    '/home/p1/p2'       '/home/p1/p3'          '../p3'
    '/home/p1/p2'       '/home/p4/p5'          '../../p4/p5'
    '/home/p1/p2'       '/work/p6/p7'          '../../../work/p6/p7'
    '/home/p1'          '/work/p1/p2/p3/p4'    '../../work/p1/p2/p3/p4'
    '/home'             '/work/p2/p3'          '../work/p2/p3'
    '/'                 '/work/p2/p3/p4'       'work/p2/p3/p4'
    '/home/p1/p2'       '/home/p1/p2/p3/p4'    'p3/p4'
    '/home/p1/p2'       '/home/p1/p2/p3'       'p3'
    '/home/p1/p2'       '/home/p1/p2'          '.'
    '/home/p1/p2'       '/home/p1'             '..'
    '/home/p1/p2'       '/home'                '../..'
    '/home/p1/p2'       '/'                    '../../..'
    '/home/p1/p2'       '/work'                '../../../work'
    '/home/p1/p2'       '/work/p1'             '../../../work/p1'
    '/home/p1/p2'       '/work/p1/p2'          '../../../work/p1/p2'
    '/home/p1/p2'       '/work/p1/p2/p3'       '../../../work/p1/p2/p3'
    '/home/p1/p2'       '/work/p1/p2/p3/p4'    '../../../work/p1/p2/p3/p4'

    '/-'                '/-'                   '.'
    '/?'                '/?'                   '.'
    '/??'               '/??'                  '.'
    '/???'              '/???'                 '.'
    '/?*'               '/?*'                  '.'
    '/*'                '/*'                   '.'
    '/*'                '/**'                  '../**'
    '/*'                '/***'                 '../***'
    '/*.*'              '/*.**'                '../*.**'
    '/*.???'            '/*.??'                '../*.??'
    '/[]'               '/[]'                  '.'
    '/[a-z]*'           '/[0-9]*'              '../[0-9]*'
EOF


format="\t%-19s %-22s %-27s %-8s %-8s\n"
printf "\n\n*** Testing own and python's function with arbitrary dirs\n\n"
printf "$format\n" \
    "From Directory" "To Directory" "Rel. Path" "relpath" "python"
IFS=
while read -r p; do
    eval set -- $p
    case $1 in '#'*|'') continue;; esac # Skip comments and empty lines
    # q stores quoting character, use " if ' is used in path name
    q="'"; case $1$2 in *"'"*) q='"';; esac
    rpOk=passed rp=$(relpath "$1" "$2"); test "$rp" = "$3" || rpOk=$rp
    RPOk=passed
    RP=$(python -c "import os.path; print os.path.relpath($q$2$q, $q$1$q)")
    test "$RP" = "$3" || RPOk=$RP
    printf "$format" "$q$1$q" "$q$2$q" "$q$3$q" "$q$rpOk$q" "$q$RPOk$q"
done <<-"EOF"
    # From directory    To directory           Expected relative path

    'usr/p1/..//./p4'   'p3/../p1/p6/.././/p2' '../../p1/p2'
    './home/../../work' '..//././../dir///'    '../../dir'

    'home/p1/p2'        'home/p1/p3'           '../p3'
    'home/p1/p2'        'home/p4/p5'           '../../p4/p5'
    'home/p1/p2'        'work/p6/p7'           '../../../work/p6/p7'
    'home/p1'           'work/p1/p2/p3/p4'     '../../work/p1/p2/p3/p4'
    'home'              'work/p2/p3'           '../work/p2/p3'
    '.'                 'work/p2/p3'           'work/p2/p3'
    'home/p1/p2'        'home/p1/p2/p3/p4'     'p3/p4'
    'home/p1/p2'        'home/p1/p2/p3'        'p3'
    'home/p1/p2'        'home/p1/p2'           '.'
    'home/p1/p2'        'home/p1'              '..'
    'home/p1/p2'        'home'                 '../..'
    'home/p1/p2'        '.'                    '../../..'
    'home/p1/p2'        'work'                 '../../../work'
    'home/p1/p2'        'work/p1'              '../../../work/p1'
    'home/p1/p2'        'work/p1/p2'           '../../../work/p1/p2'
    'home/p1/p2'        'work/p1/p2/p3'        '../../../work/p1/p2/p3'
    'home/p1/p2'        'work/p1/p2/p3/p4'     '../../../work/p1/p2/p3/p4'
EOF

【讨论】:

【参考方案15】:

Python 的 os.path.relpath 作为 shell 函数

这个relpath 练习的目标是模仿xni 提出的Python 2.7 的os.path.relpath 函数(可从Python 2.6 版获得,但只能在2.7 中正常工作)。因此,某些结果可能与其他答案中提供的函数不同。

(我没有在路径中使用换行符进行测试,仅仅是因为它破坏了基于从 ZSH 调用 python -c 的验证。通过一些努力当然可以。)

关于 Bash 中的“魔法”,我很久以前就放弃了在 Bash 中寻找魔法,但后来我在 ZSH 中找到了我需要的所有魔法,然后还有一些。

因此,我提出了两种实现方式。

第一个实现旨在完全POSIX 兼容。我已经在 Debian 6.0.6 “Squeeze” 上使用 /bin/dash 对其进行了测试。它还可以与 OS X 10.8.3 上的 /bin/sh 完美配合,这实际上是伪装成 POSIX shell 的 Bash 版本 3.2。

第二个实现是一个 ZSH shell 函数,它对路径中的多个斜杠和其他麻烦具有鲁棒性。如果您有可用的 ZSH,这是推荐的版本,即使您是从另一个 shell 以下面提供的脚本形式(即使用 #!/usr/bin/env zsh 的 shebang)调用它。

最后,我编写了一个 ZSH 脚本,根据其他答案中提供的测试用例,验证 $PATH 中的 relpath 命令的输出。我在这里和那里添加了一些空格、制表符和标点符号(例如! ? *),为这些测试添加了一些趣味,还使用vim-powerline 中的奇异UTF-8 字符进行了另一个测试。

POSIX shell 函数

首先,符合 POSIX 的 shell 函数。它适用于各种路径,但不能清除多个斜杠或解析符号链接。

#!/bin/sh
relpath () 
    [ $# -ge 1 ] && [ $# -le 2 ] || return 1
    current="$2:+"$1""
    target="$2:-"$1""
    [ "$target" != . ] || target=/
    target="/$target##/"
    [ "$current" != . ] || current=/
    current="$current:="/""
    current="/$current##/"
    appendix="$target##/"
    relative=''
    while appendix="$target#"$current"/"
        [ "$current" != '/' ] && [ "$appendix" = "$target" ]; do
        if [ "$current" = "$appendix" ]; then
            relative="$relative:-."
            echo "$relative#/"
            return 0
        fi
        current="$current%/*"
        relative="$relative$relative:+/.."
    done
    relative="$relative$relative:+$appendix:+/$appendix#/"
    echo "$relative"

relpath "$@"

ZSH shell 函数

现在,更强大的zsh 版本。如果您希望它将参数解析为真实路径 à la realpath -f(在 Linux coreutils 包中可用),请将第 3 行和第 4 行的 :a 替换为 :A

要在 zsh 中使用它,请删除第一行和最后一行并将其放在您的 $FPATH 变量中的目录中。

#!/usr/bin/env zsh
relpath () 
    [[ $# -ge 1 ]] && [[ $# -le 2 ]] || return 1
    local target=$$2:-$1:a # replace `:a' by `:A` to resolve symlinks
    local current=$$$2:+$1:-$PWD:a # replace `:a' by `:A` to resolve symlinks
    local appendix=$target#/
    local relative=''
    while appendix=$target#$current/
        [[ $current != '/' ]] && [[ $appendix = $target ]]; do
        if [[ $current = $appendix ]]; then
            relative=$relative:-.
            print $relative#/
            return 0
        fi
        current=$current%/*
        relative="$relative$relative:+/.."
    done
    relative+=$relative:+$appendix:+/$appendix#/
    print $relative

relpath "$@"

测试脚本

最后是测试脚本。它接受一个选项,即-v 来启用详细输出。

#!/usr/bin/env zsh
set -eu
VERBOSE=false
script_name=$(basename $0)

usage () 
    print "\n    Usage: $script_name SRC_PATH DESTINATION_PATH\n" >&2
    exit $1:=1

vrb ()  $VERBOSE && print -P $(%)@ || return 0; 

relpath_check () 
    [[ $# -ge 1 ]] && [[ $# -le 2 ]] || return 1
    target=$$2:-$1
    prefix=$$$2:+$1:-$PWD
    result=$(relpath $prefix $target)
    # Compare with python's os.path.relpath function
    py_result=$(python -c "import os.path; print os.path.relpath('$target', '$prefix')")
    col='%Fgreen'
    if [[ $result != $py_result ]] && col='%Fred' || $VERBOSE; then
        print -P "$colSource: '$prefix'\nDestination: '$target'%f"
        print -P "$colrelpath: $(qq)result%f"
        print -P "$colpython:  $(qq)py_result%f\n"
    fi


run_checks () 
    print "Running checks..."

    relpath_check '/    a   b/å/⮀*/!' '/    a   b/å/⮀/xäå/?'

    relpath_check '/'  '/A'
    relpath_check '/A'  '/'
    relpath_check '/  & /  !/*/\\/E' '/'
    relpath_check '/' '/  & /  !/*/\\/E'
    relpath_check '/  & /  !/*/\\/E' '/  & /  !/?/\\/E/F'
    relpath_check '/X/Y' '/  & /  !/C/\\/E/F'
    relpath_check '/  & /  !/C' '/A'
    relpath_check '/A /  !/C' '/A /B'
    relpath_check '/Â/  !/C' '/Â/  !/C'
    relpath_check '/  & /B / C' '/  & /B / C/D'
    relpath_check '/  & /  !/C' '/  & /  !/C/\\/Ê'
    relpath_check '/Å/  !/C' '/Å/  !/D'
    relpath_check '/.A /*B/C' '/.A /*B/\\/E'
    relpath_check '/  & /  !/C' '/  & /D'
    relpath_check '/  & /  !/C' '/  & /\\/E'
    relpath_check '/  & /  !/C' '/\\/E/F'

    relpath_check /home/part1/part2 /home/part1/part3
    relpath_check /home/part1/part2 /home/part4/part5
    relpath_check /home/part1/part2 /work/part6/part7
    relpath_check /home/part1       /work/part1/part2/part3/part4
    relpath_check /home             /work/part2/part3
    relpath_check /                 /work/part2/part3/part4
    relpath_check /home/part1/part2 /home/part1/part2/part3/part4
    relpath_check /home/part1/part2 /home/part1/part2/part3
    relpath_check /home/part1/part2 /home/part1/part2
    relpath_check /home/part1/part2 /home/part1
    relpath_check /home/part1/part2 /home
    relpath_check /home/part1/part2 /
    relpath_check /home/part1/part2 /work
    relpath_check /home/part1/part2 /work/part1
    relpath_check /home/part1/part2 /work/part1/part2
    relpath_check /home/part1/part2 /work/part1/part2/part3
    relpath_check /home/part1/part2 /work/part1/part2/part3/part4 
    relpath_check home/part1/part2 home/part1/part3
    relpath_check home/part1/part2 home/part4/part5
    relpath_check home/part1/part2 work/part6/part7
    relpath_check home/part1       work/part1/part2/part3/part4
    relpath_check home             work/part2/part3
    relpath_check .                work/part2/part3
    relpath_check home/part1/part2 home/part1/part2/part3/part4
    relpath_check home/part1/part2 home/part1/part2/part3
    relpath_check home/part1/part2 home/part1/part2
    relpath_check home/part1/part2 home/part1
    relpath_check home/part1/part2 home
    relpath_check home/part1/part2 .
    relpath_check home/part1/part2 work
    relpath_check home/part1/part2 work/part1
    relpath_check home/part1/part2 work/part1/part2
    relpath_check home/part1/part2 work/part1/part2/part3
    relpath_check home/part1/part2 work/part1/part2/part3/part4

    print "Done with checks."

if [[ $# -gt 0 ]] && [[ $1 = "-v" ]]; then
    VERBOSE=true
    shift
fi
if [[ $# -eq 0 ]]; then
    run_checks
else
    VERBOSE=true
    relpath_check "$@"
fi

【讨论】:

恐怕第一条路径以/结尾时不起作用。【参考方案16】:

我只会使用 Perl 来完成这个不那么简单的任务:

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

# Perl is magic
relative=$(perl -MFile::Spec -e 'print File::Spec->abs2rel("'$absolute'","'$current'")')

【讨论】:

+1,但建议:perl -MFile::Spec -e "print File::Spec-&gt;abs2rel('$absolute','$current')" 以便引用绝对值和当前值。 我喜欢relative=$(perl -MFile::Spec -e 'print File::Spec-&gt;abs2rel(@ARGV)' "$absolute" "$current")。这确保了值本身不能包含 perl 代码!【参考方案17】:

猜猜这个也可以解决问题...(带有内置测试):)

好的,预计会有一些开销,但我们在这里使用 Bourne shell! ;)

#!/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

【讨论】:

【参考方案18】:

我的解决方案:

computeRelativePath() 


    Source=$(readlink -f $1)
    Target=$(readlink -f $2)

    local OLDIFS=$IFS
    IFS="/"

    local SourceDirectoryArray=($Source)
    local TargetDirectoryArray=($Target)

    local SourceArrayLength=$(echo $SourceDirectoryArray[@] | wc -w)
    local TargetArrayLength=$(echo $TargetDirectoryArray[@] | wc -w)

    local Length
    test $SourceArrayLength -gt $TargetArrayLength && Length=$SourceArrayLength || Length=$TargetArrayLength


    local Result=""
    local AppendToEnd=""

    IFS=$OLDIFS

    local i

    for ((i = 0; i <= $Length + 1 ; i++ ))
    do
            if [ "$SourceDirectoryArray[$i]" = "$TargetDirectoryArray[$i]" ]
            then
                continue    
            elif [ "$SourceDirectoryArray[$i]" != "" ] && [ "$TargetDirectoryArray[$i]" != "" ] 
            then
                AppendToEnd="$AppendToEnd$TargetDirectoryArray[$i]/"
                Result="$Result../"               

            elif [ "$SourceDirectoryArray[$i]" = "" ]
            then
                Result="$Result$TargetDirectoryArray[$i]/"
            else
                Result="$Result../"
            fi
    done

    Result="$Result$AppendToEnd"

    echo $Result


【讨论】:

这是非常便携的:)【参考方案19】:

这是一个无需调用其他程序即可执行此操作的 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

【讨论】:

由于使用了 $param/pattern/subst 构造,这不是真正可移植的,它不是 POSIX(截至 2011 年)。 引用的源 ynform.org/w/Pub/Relpath 指向一个完全乱码的 wiki 页面,其中多次包含脚本内容,其中夹杂着 vi 波浪线、有关未找到命令的错误消息等等。对于研究原著的人来说完全没用。【参考方案20】:

我把你的问题当作一个挑战,用“便携式”shell代码编写这个,即

考虑到 POSIX shell 没有诸如数组之类的 bashism 避免像瘟疫一样调用外部。脚本中没有一个分叉!这使得它的速度非常快,尤其是在具有大量分叉开销的系统上,例如 cygwin。 必须处理路径名中的全局字符(*、?、[、])

它可以在任何符合 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 :

【讨论】:

【参考方案21】:

此脚本仅对绝对路径或没有... 的相对路径的输入给出正确结果:

#!/bin/bash

# usage: relpath from to

if [[ "$1" == "$2" ]]
then
    echo "."
    exit
fi

IFS="/"

current=($1)
absolute=($2)

abssize=$#absolute[@]
cursize=$#current[@]

while [[ $absolute[level] == $current[level] ]]
do
    (( level++ ))
    if (( level > abssize || level > cursize ))
    then
        break
    fi
done

for ((i = level; i < cursize; i++))
do
    if ((i > level))
    then
        newpath=$newpath"/"
    fi
    newpath=$newpath".."
done

for ((i = level; i < abssize; i++))
do
    if [[ -n $newpath ]]
    then
        newpath=$newpath"/"
    fi
    newpath=$newpath$absolute[i]
done

echo "$newpath"

【讨论】:

这似乎有效。如果目录确实存在,则在输入上使用 $(readlink -f $1) 和 $(readlink -f $2) 可以解决“。”的问题。或“..”出现在输入中。如果目录实际上不存在,这可能会导致一些麻烦。【参考方案22】:
#!/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/

【讨论】:

精彩的脚本——短而干净。我应用了一个编辑(等待同行评审):common_part=$source/common_part=$(dirname $common_part)/echo $back$target#$common_part 由于目录名称开头的不适当匹配,现有脚本将失败比较时,例如:“/foo/bar/baz”与“/foo/barsucks/bonk”。将斜杠移入 var 并移出最终 eval 可纠正该错误。 这个脚本根本不起作用。一个简单的“一个目录下”测试失败。 jcwenger 的编辑效果更好一些,但往往会添加一个额外的“../”。 如果参数上有尾随的“/”,在某些情况下对我来说会失败;例如,如果 $1="$HOME/" 和 $2="$HOME/temp",则返回“/home/user/temp/”,但如果 $1=$HOME,则正确返回相对路径“temp”。因此 source=$1 和 target=$2 都可以使用 sed (或使用 bash 变量替换,但这可能是不必要的不​​透明)“清理”,例如 => source=$(echo "$1" | sed 's/ \/*$//') 小改进:不要直接将源/目标设置为 $1 和 $2,而是执行:source=$(cd $1; pwd) target=$(cd $2; pwd)。这样它处理带有 .和..正确。 尽管是投票最多的答案,但这个答案有很多限制,因此发布了许多其他答案。请参阅其他答案,尤其是显示测试用例的答案。请为这条评论点赞!【参考方案23】:

test.sh:

#!/bin/bash                                                                 

cd /home/ubuntu
touch blah
TEST=/home/ubuntu/.//blah
echo TEST=$TEST
TMP=$(readlink -e "$TEST")
echo TMP=$TMP
REL=$TMP#$(pwd)/
echo REL=$REL

测试:

$ ./test.sh 
TEST=/home/ubuntu/.//blah
TMP=/home/ubuntu/blah
REL=blah

【讨论】:

+1 表示紧凑和 bash-ness。但是,您也应该在$(pwd) 上致电readlink 相对并不意味着文件必须放在同一目录中。 虽然最初的问题没有提供很多测试用例,但是这个脚本对于简单的测试失败了,比如找到从 /home/user1 到 /home/user2 的相对路径(正确答案:../user2 )。 pini/jcwenger 的脚本适用于这种情况。【参考方案24】:

遗憾的是,Mark Rushakoff 的答案(现已删除 - 它引用了来自 here 的代码)在适应以下情况时似乎无法正常工作:

source=/home/part2/part3/part4
target=/work/proj1/proj2

评论中概述的想法可以改进,使其在大多数情况下都能正常工作。我将假设脚本接受一个源参数(你在哪里)和一个目标参数(你想去哪里),并且要么都是绝对路径名,要么都是相对的。如果一个是绝对的,另一个是相对的,最简单的方法是在相对名称前面加上当前工作目录 - 但下面的代码没有这样做。


小心

下面的代码接近于正常工作,但并不完全正确。

    Dennis Williamson 的 cmets 解决了这个问题。 还有一个问题是,这种纯粹的路径名文本处理可能会被奇怪的符号链接严重搞砸。 代码不处理路径中的杂散“点”,例如“xyz/./pqr”。 代码不处理路径中的杂散“双点”,如“xyz/../pqr”。 简单地说:代码不会从路径中删除前导“./”。

Dennis 的代码更好,因为它修复了 1 和 5 - 但有相同的问题 2、3、4。 因此,请使用丹尼斯的代码(并在此之前对其进行投票)。

(注意:POSIX 提供了一个系统调用realpath() 来解析路径名,以便其中没有符号链接。将其应用于输入名称,然后使用丹尼斯的代码每次都会给出正确的答案。这很简单编写包装 realpath() 的 C 代码 - 我已经完成了 - 但我不知道这样做的标准实用程序。)


为此,我发现 Perl 比 shell 更易于使用,尽管 bash 对数组有很好的支持并且可能也可以做到这一点 - 为读者练习。因此,给定两个兼容的名称,将它们分别拆分为组件:

将相对路径设置为空。 虽然组件相同,但请跳至下一个。 当对应的组件不同或一条路径没有更多组件时: 如果没有剩余的源组件且相对路径为空,则添加“.”从头开始。 对于每个剩余的源组件,在相对路径前加上“../”前缀。 如果没有剩余目标组件且相对路径为空,则添加“.”从头开始。 对于每个剩余的目标组件,将组件添加到路径末尾的斜杠之后。

因此:

#!/bin/perl -w

use strict;

# Should fettle the arguments if one is absolute and one relative:
# Oops - missing functionality!

# Split!
my(@source) = split '/', $ARGV[0];
my(@target) = split '/', $ARGV[1];

my $count = scalar(@source);
   $count = scalar(@target) if (scalar(@target) < $count);
my $relpath = "";

my $i;
for ($i = 0; $i < $count; $i++)

    last if $source[$i] ne $target[$i];


$relpath = "." if ($i >= scalar(@source) && $relpath eq "");
for (my $s = $i; $s < scalar(@source); $s++)

    $relpath = "../$relpath";

$relpath = "." if ($i >= scalar(@target) && $relpath eq "");
for (my $t = $i; $t < scalar(@target); $t++)

    $relpath .= "/$target[$t]";


# Clean up result (remove double slash, trailing slash, trailing slash-dot).
$relpath =~ s%//%/%;
$relpath =~ s%/$%%;
$relpath =~ s%/\.$%%;

print "source  = $ARGV[0]\n";
print "target  = $ARGV[1]\n";
print "relpath = $relpath\n";

测试脚本(方括号包含一个空格和一个制表符):

sed 's/#.*//;/^[    ]*$/d' <<! |

/home/part1/part2 /home/part1/part3
/home/part1/part2 /home/part4/part5
/home/part1/part2 /work/part6/part7
/home/part1       /work/part1/part2/part3/part4
/home             /work/part2/part3
/                 /work/part2/part3/part4

/home/part1/part2 /home/part1/part2/part3/part4
/home/part1/part2 /home/part1/part2/part3
/home/part1/part2 /home/part1/part2
/home/part1/part2 /home/part1
/home/part1/part2 /home
/home/part1/part2 /

/home/part1/part2 /work
/home/part1/part2 /work/part1
/home/part1/part2 /work/part1/part2
/home/part1/part2 /work/part1/part2/part3
/home/part1/part2 /work/part1/part2/part3/part4

home/part1/part2 home/part1/part3
home/part1/part2 home/part4/part5
home/part1/part2 work/part6/part7
home/part1       work/part1/part2/part3/part4
home             work/part2/part3
.                work/part2/part3

home/part1/part2 home/part1/part2/part3/part4
home/part1/part2 home/part1/part2/part3
home/part1/part2 home/part1/part2
home/part1/part2 home/part1
home/part1/part2 home
home/part1/part2 .

home/part1/part2 work
home/part1/part2 work/part1
home/part1/part2 work/part1/part2
home/part1/part2 work/part1/part2/part3
home/part1/part2 work/part1/part2/part3/part4

!

while read source target
do
    perl relpath.pl $source $target
    echo
done

测试脚本的输出:

source  = /home/part1/part2
target  = /home/part1/part3
relpath = ../part3

source  = /home/part1/part2
target  = /home/part4/part5
relpath = ../../part4/part5

source  = /home/part1/part2
target  = /work/part6/part7
relpath = ../../../work/part6/part7

source  = /home/part1
target  = /work/part1/part2/part3/part4
relpath = ../../work/part1/part2/part3/part4

source  = /home
target  = /work/part2/part3
relpath = ../work/part2/part3

source  = /
target  = /work/part2/part3/part4
relpath = ./work/part2/part3/part4

source  = /home/part1/part2
target  = /home/part1/part2/part3/part4
relpath = ./part3/part4

source  = /home/part1/part2
target  = /home/part1/part2/part3
relpath = ./part3

source  = /home/part1/part2
target  = /home/part1/part2
relpath = .

source  = /home/part1/part2
target  = /home/part1
relpath = ..

source  = /home/part1/part2
target  = /home
relpath = ../..

source  = /home/part1/part2
target  = /
relpath = ../../../..

source  = /home/part1/part2
target  = /work
relpath = ../../../work

source  = /home/part1/part2
target  = /work/part1
relpath = ../../../work/part1

source  = /home/part1/part2
target  = /work/part1/part2
relpath = ../../../work/part1/part2

source  = /home/part1/part2
target  = /work/part1/part2/part3
relpath = ../../../work/part1/part2/part3

source  = /home/part1/part2
target  = /work/part1/part2/part3/part4
relpath = ../../../work/part1/part2/part3/part4

source  = home/part1/part2
target  = home/part1/part3
relpath = ../part3

source  = home/part1/part2
target  = home/part4/part5
relpath = ../../part4/part5

source  = home/part1/part2
target  = work/part6/part7
relpath = ../../../work/part6/part7

source  = home/part1
target  = work/part1/part2/part3/part4
relpath = ../../work/part1/part2/part3/part4

source  = home
target  = work/part2/part3
relpath = ../work/part2/part3

source  = .
target  = work/part2/part3
relpath = ../work/part2/part3

source  = home/part1/part2
target  = home/part1/part2/part3/part4
relpath = ./part3/part4

source  = home/part1/part2
target  = home/part1/part2/part3
relpath = ./part3

source  = home/part1/part2
target  = home/part1/part2
relpath = .

source  = home/part1/part2
target  = home/part1
relpath = ..

source  = home/part1/part2
target  = home
relpath = ../..

source  = home/part1/part2
target  = .
relpath = ../../..

source  = home/part1/part2
target  = work
relpath = ../../../work

source  = home/part1/part2
target  = work/part1
relpath = ../../../work/part1

source  = home/part1/part2
target  = work/part1/part2
relpath = ../../../work/part1/part2

source  = home/part1/part2
target  = work/part1/part2/part3
relpath = ../../../work/part1/part2/part3

source  = home/part1/part2
target  = work/part1/part2/part3/part4
relpath = ../../../work/part1/part2/part3/part4

面对奇怪的输入,这个 Perl 脚本在 Unix 上运行得相当彻底(它没有考虑 Windows 路径名的所有复杂性)。它使用模块Cwd 及其函数realpath 来解析存在的名称的真实路径,并对不存在的路径进行文本分析。除一种情况外,在所有情况下,它都会产生与 Dennis 的脚本相同的输出。异常情况是:

source   = home/part1/part2
target   = .
relpath1 = ../../..
relpath2 = ../../../.

这两个结果是等价的——只是不相同。 (输出来自测试脚本的轻微修改版本 - 下面的 Perl 脚本只是打印答案,而不是上面脚本中的输入和答案。)现在:我应该消除无效的答案?也许……

#!/bin/perl -w
# Based loosely on code from: http://unix.derkeiler.com/Newsgroups/comp.unix.shell/2005-10/1256.html
# Via: http://***.com/questions/2564634

use strict;

die "Usage: $0 from to\n" if scalar @ARGV != 2;

use Cwd qw(realpath getcwd);

my $pwd;
my $verbose = 0;

# Fettle filename so it is absolute.
# Deals with '//', '/./' and '/../' notations, plus symlinks.
# The realpath() function does the hard work if the path exists.
# For non-existent paths, the code does a purely textual hack.
sub resolve

    my($name) = @_;
    my($path) = realpath($name);
    if (!defined $path)
    
        # Path does not exist - do the best we can with lexical analysis
        # Assume Unix - not dealing with Windows.
        $path = $name;
        if ($name !~ m%^/%)
        
            $pwd = getcwd if !defined $pwd;
            $path = "$pwd/$path";
        
        $path =~ s%//+%/%g;     # Not UNC paths.
        $path =~ s%/$%%;        # No trailing /
        $path =~ s%/\./%/%g;    # No embedded /./
        # Try to eliminate /../abc/
        $path =~ s%/\.\./(?:[^/]+)(/|$)%$1%g;
        $path =~ s%/\.$%%;      # No trailing /.
        $path =~ s%^\./%%;      # No leading ./
        # What happens with . and / as inputs?
    
    return($path);


sub print_result

    my($source, $target, $relpath) = @_;
    if ($verbose)
    
        print "source  = $ARGV[0]\n";
        print "target  = $ARGV[1]\n";
        print "relpath = $relpath\n";
    
    else
    
        print "$relpath\n";
    
    exit 0;


my($source) = resolve($ARGV[0]);
my($target) = resolve($ARGV[1]);
print_result($source, $target, ".") if ($source eq $target);

# Split!
my(@source) = split '/', $source;
my(@target) = split '/', $target;

my $count = scalar(@source);
   $count = scalar(@target) if (scalar(@target) < $count);
my $relpath = "";
my $i;

# Both paths are absolute; Perl splits an empty field 0.
for ($i = 1; $i < $count; $i++)

    last if $source[$i] ne $target[$i];


for (my $s = $i; $s < scalar(@source); $s++)

    $relpath = "$relpath/" if ($s > $i);
    $relpath = "$relpath..";

for (my $t = $i; $t < scalar(@target); $t++)

    $relpath = "$relpath/" if ($relpath ne "");
    $relpath = "$relpath$target[$t]";


print_result($source, $target, $relpath);

【讨论】:

您的/home/part1/part2/ 有一个太多../。否则,我的脚本与您的输出相匹配,除了我的脚本在目标为 . 的末尾添加了一个不必要的 . 并且我不在下降而不上升的开头使用 ./ @Dennis:我花了很多时间对结果进行了斗鸡眼——有时我可以看到这个问题,有时我又找不到它。删除前导 './' 是另一个微不足道的步骤。您对“没有嵌入”的评论。或 ..' 也是相关的。正确地完成这项工作实际上非常困难 - 如果任何名称实际上是符号链接,则更是如此;我们都在做纯文本分析。 @Dennis:当然,除非你有 Newcastle Connection 网络,否则试图超越 root 是徒劳的,所以 ../../../.. 和 ../../..是等价的。然而,那是纯粹的逃避现实;你的批评是正确的。 (Newcastle Connection 允许您配置和使用符号 /../host/path/on/remote/machine 来访问不同的主机 - 一个简洁的方案。我相信它支持 /../../network/host/ path/on/remote/network/and/host 也是。它在***上。) 因此,我们现在有了 UNC 的双斜线。 “readlink”实用程序(至少是 GNU 版本)可以执行与 realpath() 等效的操作,如果您将“-f”选项传递给它。例如,在我的系统上,readlink /usr/bin/vi 提供 /etc/alternatives/vi,但这是另一个符号链接 - 而 readlink -f /usr/bin/vi 提供 /usr/bin/vim.basic,这是所有符号链接的最终目的地...

以上是关于使用 Bash 将给定当前目录的绝对路径转换为相对路径的主要内容,如果未能解决你的问题,请参考以下文章

引用外部文件时将绝对目录转换为相对路径

java 怎么把文件的绝对路径转换成相对路径

laravel怎么获取到public路径

node rename绝对路径

使用 JavaScript 将相对路径转换为绝对路径

将相对路径转换为绝对路径?