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


261

例:

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

# Magic

relative="../../bar"

如何创建魔术(希望不太复杂的代码...)?


7
例如(我现在的情况)用于提供gcc相对路径,以便即使源路径更改,它也可以生成可用的相对调试信息。
Offirmo 2012年

在U&L上提出了与此问题类似的问题:unix.stackexchange.com/questions/100918/…。答案之一(@Gilles)提到了一个工具symlinks,它可以使此问题更容易解决。
slm

25
简单:realpath --relative-to=$absolute $current
kenorb

Answers:


228

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

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

例如:

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

7
遗憾的是,该软件包在Ubuntu 14.04上已经过时,并且没有--relative-to选项。
kzh

3
在Ubuntu 16.04上运行良好
cayhorstmann

7
$ realpath --relative-to="${PWD}" "$file"如果您想要相对于当前工作目录的路径,则很有用。
dcoles

1
这对于/usr/bin/nmap/-path 内部的内容是正确的,但对于/usr/bin/nmap以下情况却不正确:从nmap/tmp/testing它只是../../3次而不是3倍../。但是它可以工作,因为..在rootfs上执行/
Patrick B.

6
作为@PatrickB。暗示,--relative-to=…期望目录并且不检查。这意味着,如果您请求相对于文件的路径(如本例所示,因为/usr/bin很少或从不包含目录,nmap并且通常是二进制文件),则最终会得到一个额外的“ ../”
IBBoard,

162
$ python -c "import os.path; print os.path.relpath('/foo/bar', '/foo/baz/foo')"

给出:

../../bar

11
它有效,并且使替代方案看起来很荒谬。这对我来说是xD的好处
hasvn 2012年

31
+1。好吧,你被骗了...但这太好了,不能使用!relpath(){ python -c "import os.path; print os.path.relpath('$1','${2:-$PWD}')" ; }
MestreLion'4

4
遗憾的是,这并不是普遍可用的:os.path.relpath是Python 2.6中的新增功能。
陈征费2012年

15
@ChenLevy:Python 2.6于2008年发布。令人难以置信的是,它在2012
。– MestreLion 2013年

11
python -c 'import os, sys; print(os.path.relpath(*sys.argv[1:]))'最自然,最可靠地工作。
musiphil

31

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

1
集成在offirmo shell lib github.com/Offirmo/offirmo-shell-lib中,功能«OSL_FILE_find_relative_path»(文件«osl_lib_file.sh»)
Offirmo 2012年

1
+1。通过替换source=$1; target=$2source=$(realpath $1); target=$(realpath $2)
Josh Kelley

2
确实是@Josh,条件是dirs实际上存在...对于单元测试而言不方便;)但在实际使用中,realpath建议使用,source=$(readlink -f $1)如果没有realpath(不标准),则建议这样做,等等
Offirmo 2013年

我定义$source$target像这样:`if [[--e $ 1]]; 然后source = $(readlink -f $ 1); else source = $ 1; fi if [[-e $ 2]]; 然后target = $(readlink -f $ 2); 否则target = $ 2; 这样,该函数可以合并实际/现有的相对路径以及虚构目录。
内森·沃森

1
@ NathanS.Watson-Haigh更好的是,我发现最近readlink有一个-m选项可以做到这一点;)
Offirmo 2014年

26
#!/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 2011年

3
该脚本根本不起作用。无法通过简单的“向下一个目录”测试。jcwenger的编辑效果更好,但往往会添加一个额外的“ ../”。
Person Person II博士

1
在某些情况下,如果在参数上以“ /”结尾,则对我而言是失败的;例如,如果$ 1 =“ $ HOME /”和$ 2 =“ $ HOME / temp”,则返回“ / home / user / temp /”,但是,如果$ 1 = $ HOME,则它将正确返回相对路径“ temp”。因此,可以使用sed(或使用bash变量替换,但是不必要不必要的不​​透明)对source = $ 1和target = $ 2进行“清理”,例如=> source = $(echo“ $ {1}” | sed的/ \ / * $ //')
迈克尔

1
较小的改进:不必直接将source / target设置为$ 1和$ 2,而是:source = $(cd $ 1; pwd)target = $(cd $ 2; pwd)。这样,它可以使用处理路径。和..正确。
约瑟夫·加文2012年

4
尽管答案是最受好评的,但该答案仍有很多局限性,因此发布了许多其他答案。请改为查看其他答案,尤其是显示测试用例的答案。并请对此评论投票!
Offirmo 2012年

25

它自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

...会很好。


3
say尚未在perl中用作日志,但可以在此处有效使用。 perl -MFile::Spec -E 'say File::Spec->abs2rel(@ARGV)'
威廉·珀塞尔

+1,但也可以看到这个类似的答案答案早一些(2012年2月)。另请阅读William Pursell的相关评论。我的版本是两个命令行:perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target"perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target" "$origin"。第一个单行的perl脚本使用一个参数(起源是当前工作目录)。第二个单行的perl脚本使用两个参数。
olibre 2014年

3
那应该是公认的答案。perl尽管答案仍然是单一的,但几乎可以在任何地方找到它。
德米特里·金茨堡

19

假设您已安装: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和其他一些想法的答案

注意:这要求两个路径都必须是现有文件夹。文件将无法正常工作。


2
理想的答案:与/ bin / sh一起使用,不需要readlink,python,perl->非常适合轻型/嵌入式系统或Windows bash控制台
Francois

2
不幸的是,这要求存在路径,但这并不总是需要的。
drwatsoncode 2015年

敬虔的回答。cd-pwd内容用于解析我猜的链接?高尔夫不错!
泰克怪胎

15

Python的 os.path.relpath作为外壳函数

relpath练习的目的是模仿xnios.path.relpath提出的Python 2.7的功能(可从python版本2.6获得,但只能在2.7中正常工作)。因此,某些结果可能与其他答案中提供的功能有所不同。

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

关于Bash中的“魔术”,我很早以前就放弃了在Bash中寻找魔术,但是自那以后,我在ZSH中找到了我需要的所有魔术,然后找到了一些。

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

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

第二种实现是ZSH Shell函数,它对路径中的多个斜杠和其他有害内容具有鲁棒性。如果您有可用的ZSH,则推荐使用此版本,即使您以下面显示的脚本形式调用它(例如,#!/usr/bin/env zsh从另一个shell中)。

最后,我编写了一个ZSH脚本,该脚本可以验证在其他答案提供的测试用例中relpath找到的命令的输出$PATH。我通过! ? *在此处和此处添加一些空格,制表符和标点符号为这些测试添加了一些趣味,并且还使用了在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软件包中可用),请将第:a3行和第4行替换为: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='%F{green}'
    if [[ $result != $py_result ]] && col='%F{red}' || $VERBOSE; then
        print -P "${col}Source: '$prefix'\nDestination: '$target'%f"
        print -P "${col}relpath: ${(qq)result}%f"
        print -P "${col}python:  ${(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

2
/恐怕第一个路径结束时不起作用。
Noldorin'2

12
#!/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的启发(谢谢!)。它会触发Stack Overflow的语法突出显示模块中的错误(至少在我的预览框中)。因此,如果突出显示不正确,请忽略。

一些注意事项:

  • 在不显着增加代码长度和复杂性的情况下消除了错误并改进了代码
  • 将功能放入功能中以便于使用
  • 保留与POSIX兼容的功能,以便它们(应该)与所有POSIX Shell一起使用(在Ubuntu Linux 12.04中经过dash,bash和zsh测试)
  • 仅使用局部变量是为了避免破坏全局变量并污染全局名称空间
  • 两个目录路径都不需要存在(我的应用程序的要求)
  • 路径名可以包含空格,特殊字符,控制字符,反斜杠,制表符,“,”,?,*,[,]等。
  • 核心功能“ relPath”仅使用POSIX shell内置函数,但需要规范的绝对目录路径作为参数
  • 扩展功能“ relpath”可以处理任意目录路径(也可以是相对的,非规范的),但需要外部GNU核心实用程序“ readlink”
  • 避免使用内置的“ echo”,而是使用内置的“ printf”,原因有两个:
  • 为了避免不必要的转换,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"/}"

    我喜欢后者,因为

    1. 文件名可以直接附加到relPath获得的目录路径中,例如:

      ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
    2. 用此方法创建的同一目录中的符号链接没有丑陋"./"的文件名前缀。

  • 如果发现错误,请联系gmail.com上的linuxball,我将尝试修复它。
  • 添加了回归测试套件(也兼容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

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可靠且简单的纯解决方案(而不是好奇心高涨),我会感到惊讶。


到目前为止,这似乎是最可靠的方法。
dimo414

7

该脚本仅对绝对路径或相对路径不带.或的输入给出正确的结果..

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

1
这似乎起作用。如果目录确实存在,则在输入中使用$(readlink -f $ 1)和$(readlink -f $ 2)可以解决问题“。”。或“ ..”出现在输入中。如果目录实际上不存在,这可能会引起一些麻烦。
Person Person II博士

7

我只是将Perl用于这项不那么琐碎的任务:

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

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

1
+1,但建议:perl -MFile::Spec -e "print File::Spec->abs2rel('$absolute','$current')"这样就可以引用绝对值和当前值。
威廉·珀塞尔

我喜欢relative=$(perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$absolute" "$current")。这样可以确保值本身不能包含perl代码!
Erik Aronesty

6

kaskuPini的答案略有改进,它与空格配合使用更好,并允许传递相对路径:

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

4

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表示紧凑和重击。你应该,但是,也叫readlink$(pwd)
DevSolar 2010年

2
相对并不意味着文件必须放置在同一目录中。
greenoldman 2011年

尽管最初的问题并没有提供很多测试用例,但是该脚本对于简单的测试失败,例如查找从/ home / user1到/ home / user2的相对路径(正确答案:../ user2)。pini / jcwenger编写的脚本适用于这种情况。
迈克尔

4

另一个解决方案,pure 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:-.}"
}

笔记:

  • 注意,如果文件名包含*或,可以防止不必要的外壳元字符扩展?
  • 输出应可用作以下内容的第一个参数ln -s
    • relpath / /给出.而不是空字符串
    • relpath a aa,即使a碰巧是一个目录
  • 大多数常见的案例也经过测试以得出合理的结果。
  • 此解决方案使用字符串前缀匹配,因此readlink需要规范化路径。
  • 由于readlink -m它还可以用于尚不存在的路径。

在旧系统上,如果文件不存在readlink -mreadlink -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"

毫无疑问,从../barpage bar看到的是页面的确切且唯一正确的相对路径moo。其他一切都是错误的。

对于显然假定的问题采用输出是很简单的,这current是一个目录:

absolute="/foo/bar"
current="/foo/baz/foo"
relative="../$(relpath "$absolute" "$current")"

这恰好返回了要求的结果。

而且,在引起您的注意之前,这是(有点relpath小区别)的更复杂的变体,它也应该适用于URL语法(因此/,由于一些bash-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"

如果您发现无法解决的问题,请在下面的评论中告诉我。谢谢。

PS:

为什么relpath“相反” 的论证与这里的所有其他答案相反?

如果你改变

Y="$(readlink -m -- "$2")" || return

Y="$(readlink -m -- "${2:-"$PWD"}")" || return

那么您可以不使用第二个参数,从而使BASE是当前目录/ URL /任何内容。像往常一样,那只是Unix原则。

如果您不满意,请返回Windows。谢谢。


3

可悲的是,Mark Rushakoff的答案(现已删除-它从此处引用了代码)在适应以下情况时似乎无法正常工作:

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

评论中概述的思想可以完善,以使其在大多数情况下都能正常工作。我将假设脚本采用源参数(您在哪里)和目标参数(您想到达哪里),并且两者都是绝对路径名或都是相对路径名。如果一个是绝对的,另一个是相对的,则最简单的方法是在当前工作目录的前面加上相对名称-但是下面的代码不能做到这一点。


谨防

下面的代码几乎可以正常工作,但是不太正确。

  1. Dennis Williamson的评论中解决了这个问题。
  2. 还有一个问题是,对路径名的这种纯文本处理会导致您被奇怪的符号链接严重弄乱。
  3. 该代码无法处理“”等路径中的杂散“点xyz/./pqr”。
  4. 该代码无法处理“xyz/../pqr ”。
  5. 琐碎:代码不会./从路径中删除前导“ ”。

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

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


为此,我发现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://stackoverflow.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:我花了很多时间对结果cross目结舌-有时我可以看到这个问题,有时又找不到了。删除前导“ ./”是另一个不重要的步骤。您对“没有嵌入式”的评论。或..'也与此有关。正确地完成这项工作实际上是令人惊讶的困难-双重,如果任何一个名称实际上是一个符号链接,那么就加倍了。我们都在做纯文本分析。
乔纳森·莱夫勒

@Dennis:当然,除非您具有Newcastle Connection网络,否则尝试超越根目录是徒劳的,因此../../../ ..和../../ ..是等效的。但是,这纯粹是逃避现实。您的批评是正确的。(Newcastle Connection允许您配置和使用符号/../host/path/on/remote/machine转到其他主机-整洁的方案。我相信它支持/../../network/host/路径/上/远程/网络/和/主机。(它在Wikipedia上。)
Jonathan Leffler 2010年

因此,现在我们有了UNC的双斜线。
暂停,直到另行通知。

1
如果将“ -f”选项传递给它,则“ readlink”实用程序(至少是GNU版本)可以执行与realpath()等效的操作。例如,我的系统上,readlink /usr/bin/vi给人/etc/alternatives/vi,但这是另一个符号链接-而readlink -f /usr/bin/vi给出/usr/bin/vim.basic,这是所有的符号链接的最终目的地...
psmears

3

我以您的问题为挑战,以“便携式” shell代码编写此代码,即

  • 考虑到POSIX外壳
  • 没有像数组这样的bashisms
  • 避免像瘟疫一样叫外部人员。脚本中没有一个分支!这使其运行起来非常快,尤其是在像cygwin这样具有大量fork开销的系统上。
  • 必须处理路径名中的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 :

2

我的解决方案:

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

}

这是非常便携式的:)
匿名

2

这是我的版本。它基于的答案@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
}

1

猜猜这也可以解决问题……(内置测试附带):)

好的,预计会有一些开销,但是我们在这里做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

1

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

我只在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

另外,代码有点复制和粘贴,但是我很快就需要它。
juancn 2010年

很好...正是我所需要的。而且您已经包括了一个规范化例程,该例程比我见过的大多数其他例程都要好(通常依赖于正则表达式替换)。
drwatsoncode 2015年

0

这个答案不能解决问题的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您提供的测试用例的行为相同。
加里·维斯涅夫斯基

-1

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


1
由于使用$ {param / pattern / subst}构造(不是POSIX(截至2011年)),因此这并不是真正可移植的。
詹斯

引用的源ynform.org/w/Pub/Relpath指向一个完全乱码的Wiki页面,该页面多次包含脚本内容,并与Vitilde行混合在一起,显示关于未找到命令的错误消息以及诸如此类。对于研究原始版本的人完全没有用。
詹斯

-1

我需要这样的东西,但它也解决了符号链接。我发现pwd为此有一个-P标志。我的脚本的一个片段被追加。它在外壳程序脚本的函数中,因此在$ 1和$ 2中。结果值是UPDIRS变量,它是从START_ABS到END_ABS的相对路径。脚本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"
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.