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

Pau*_*jan 236 bash shell path relative-path absolute-path

例:

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

# Magic

relative="../../bar"
Run Code Online (Sandbox Code Playgroud)

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

mod*_*us0 193

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

$ realpath --relative-to="$file1" "$file2"
Run Code Online (Sandbox Code Playgroud)

例如:

$ realpath --relative-to=/usr/bin/nmap /tmp/testing
../../../tmp/testing
Run Code Online (Sandbox Code Playgroud)

  • 遗憾的是,该软件包已在Ubuntu 14.04上过时,并且没有--relative-to选项. (7认同)
  • `$ realpath --relative-to ="$ {PWD}""$ file"`如果你想要相对于当前工作目录的路径是很有用的. (6认同)
  • 作为@PatrickB.暗示,`--relative-to = ...`需要一个目录,不要检查.这意味着如果你请求一个相对于文件的路径,你最终得到一个额外的"../"(因为这个例子似乎是这样做的,因为`/ usr/bin`很少或从不包含目录而``nmap`通常是二进制) (6认同)
  • 适用于Ubuntu 16.04 (2认同)

小智 158

$ python -c "import os.path; print os.path.relpath('/foo/bar', '/foo/baz/foo')"
Run Code Online (Sandbox Code Playgroud)

得到:

../../bar
Run Code Online (Sandbox Code Playgroud)

  • +1.好吧,你作弊......但这太好了不能用!`relpath(){python -c"import os.path; print os.path.relpath('$ 1','$ {2: - $ PWD}')"; } (30认同)
  • @ChenLevy:Python 2.6于2008年发布.很难相信它在2012年没有普及. (14认同)
  • 它有效,它使替代品看起来很荒谬.这对我来说是个额外的奖励 (10认同)
  • `python -c'import os,sys; print(os.path.relpath(*sys.argv [1:]))'`最自然,最可靠. (10认同)
  • 遗憾的是,这并不普遍:os.path.relpath是Python 2.6中的新功能. (4认同)
  • 如果 $1 中有单引号,则不确定 MestreLion 的解决方案是否有效。也许是这样的: relpath() { python -c 'import sys, os.path; 打印 os.path.relpath(sys.argv[1], sys.argv[2])' "$1" "${2:-$PWD}"; } (2认同)
  • 您来到这个问题时希望得到一个使用“basename”之类的命令的答案。一旦您浏览了为保持这种“纯粹的打击”而必须执行的令人难以置信的体操,您就会认为 Python 中的单线是完全可以接受的。 (2认同)

Off*_*rmo 30

这是对@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
Run Code Online (Sandbox Code Playgroud)

测试用例 :

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"
Run Code Online (Sandbox Code Playgroud)

  • +1。通过替换 `source=$1; 可以轻松地处理任何路径(不仅仅是以 / 开头的绝对路径)。target=$2` 和 `source=$(realpath $1); 目标=$(实际路径$2)` (2认同)
  • @Josh确实,只要dirs实际存在......这对于单元测试是不方便的;)但在实际使用中是的,建议使用`realpath`,或者如果是realpath则使用`source = $(readlink -f $ 1)`等.不可用(非标准) (2认同)

小智 25

#!/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/}
Run Code Online (Sandbox Code Playgroud)

  • 这个脚本根本不起作用.失败一个简单的"一个目录下来"测试.jcwenger的编辑效果稍好一些,但往往会增加额外的"../". (3认同)
  • 尽管这是最受欢迎的答案,但这个答案有很多局限性,因此发布了许多其他答案.请改为查看其他答案,尤其是显示测试用例的答案.请注意这个评论! (3认同)

Eri*_*sty 23

它自2001年开始内置于Perl中,因此几乎适用于您可以想象的每个系统,甚至是VMS.

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

此外,该解决方案易于理解.

所以对于你的例子:

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

......会很好的.

  • 加上perl比python更好. (7认同)
  • "说"在perl中没有作为日志提供,但它可以在这里有效地使用.`perl -MFile :: Spec -E'说File :: Spec-> abs2rel(@ARGV)'` (3认同)
  • 那应该是公认的答案.`perl`几乎可以在任何地方找到,虽然答案仍然是单行. (3认同)

小智 15

Python os.path.relpath作为shell函数

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

(我没有在路径中使用换行符进行测试,因为它违反了基于python -cZSH 调用的验证.通过一些努力肯定是可能的.)

关于Bash中的"魔法",我已经放弃了很久以前在Bash中寻找魔法,但我已经找到了我需要的所有魔法,然后是ZSH.

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

第一个实现旨在完全符合POSIX标准.我/bin/dash在Debian 6.0.6"Squeeze"上测试过它 .它也适用/bin/sh于OS X 10.8.3,它实际上是Bash版本3.2,假装是POSIX shell.

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

最后,我编写了一个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 "$@"
Run Code Online (Sandbox Code Playgroud)

ZSH shell功能

现在,更强大的zsh版本.如果您希望它解析实际路径的参数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 "$@"
Run Code Online (Sandbox Code Playgroud)

测试脚本

最后,测试脚本.它接受一个选项,即-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
Run Code Online (Sandbox Code Playgroud)

  • 恐怕当第一条路径以`/` 结尾时不起作用。 (2认同)

Ale*_*che 15

假设你已经安装了:bash,pwd,dirname,echo; 然后重新路径是

#!/bin/bash
s=$(cd ${1%%/};pwd); d=$(cd $2;pwd); while [ "${d#$s/}" == "${d}" ]
do s=$(dirname $s);b="../${b}"; done; echo ${b}${d#$s/}
Run Code Online (Sandbox Code Playgroud)

我从pini和其他一些想法中得到了答案

  • 理想答案:使用/ bin/sh,不需要readlink,python,perl - >非常适合光/嵌入式系统或windows bash控制台 (2认同)
  • 不幸的是,这需要存在路径,这并不总是需要的. (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")"; }
Run Code Online (Sandbox Code Playgroud)

以上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:-.}"
    
    Run Code Online (Sandbox Code Playgroud)

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

    printf %s "$up${path#"$common"/}"
    
    Run Code Online (Sandbox Code Playgroud)

    我更喜欢后者,因为

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

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

  • 如果您发现错误,请联系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
Run Code Online (Sandbox Code Playgroud)


小智 7

我只是将Perl用于这个不那么重要的任务:

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

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


Pau*_*ce. 6

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

#!/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"
Run Code Online (Sandbox Code Playgroud)


sin*_*law 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
Run Code Online (Sandbox Code Playgroud)


Gar*_*ski 6

这里没有很多答案对于每天使用都很实用。由于以纯bash正确地执行此操作非常困难,因此我建议使用以下可靠的解决方案(类似于埋在评论中的一项建议):

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

然后,您可以基于当前目录获取相对路径:

echo $(relpath somepath)
Run Code Online (Sandbox Code Playgroud)

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

echo $(relpath somepath /etc)  # relative to /etc
Run Code Online (Sandbox Code Playgroud)

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

  • 它在任何python> = 2.6中均相同
  • 它不需要文件或目录。
  • 文件名可能包含更多的特殊字符。例如,如果文件名包含空格或其他特殊字符,则许多其他解决方案将不起作用。
  • 这是一个单行函数,不会使脚本混乱。

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


Tin*_*ino 5

另一个解决方案,纯bash+ GNU readlink,可在以下上下文中轻松使用:

ln -s "$(relpath "$A" "$B")" "$B"
Run Code Online (Sandbox Code Playgroud)

编辑:在这种情况下,请确保“$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:-.}"
}
Run Code Online (Sandbox Code Playgroud)

笔记:

  • 注意防止不需要的 shell 元字符扩展是安全的,以防文件名包含*?
  • 输出旨在用作第一个参数ln -s
    • relpath / /给出.而不是空字符串
    • relpath a aa,即使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")"
}
Run Code Online (Sandbox Code Playgroud)

对于$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"
Run Code Online (Sandbox Code Playgroud)

困惑?好吧,这些是正确的结果!即使您认为它不适合问题,这里也证明这是正确的:

check "http://example.com/foo/baz/moo" "http://example.com/foo/bar" "../bar"
Run Code Online (Sandbox Code Playgroud)

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

将输出用于显然假设的问题,即current目录是微不足道的:

absolute="/foo/bar"
current="/foo/baz/foo"
relative="../$(relpath "$absolute" "$current")"
Run Code Online (Sandbox Code Playgroud)

这正是所要求的返回。

在你皱眉之前,这里有一个更复杂的变体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:-.}"
}
Run Code Online (Sandbox Code Playgroud)

这里有一些检查只是为了说明:它真的像告诉的那样有效。

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/"
Run Code Online (Sandbox Code Playgroud)

这是如何使用它从问题中给出想要的结果:

absolute="/foo/bar"
current="/foo/baz/foo"
relative="$(relpathurl "$absolute" "$current/")"
echo "$relative"
Run Code Online (Sandbox Code Playgroud)

如果您发现某些内容不起作用,请在下面的评论中告诉我。谢谢。

PS:

为什么relpath“颠倒”的论点与这里的所有其他答案形成对比?

如果你改变

Y="$(readlink -m -- "$2")" || return
Run Code Online (Sandbox Code Playgroud)

Y="$(readlink -m -- "${2:-"$PWD"}")" || return
Run Code Online (Sandbox Code Playgroud)

然后你可以离开第二个参数,这样 BASE 就是当前目录/URL/任何东西。像往常一样,这只是 Unix 的原则。


Jon*_*ler 4

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

source=/home/part2/part3/part4
target=/work/proj1/proj2
Run Code Online (Sandbox Code Playgroud)

可以对评论中概述的想法进行改进,使其在大多数情况下都能正确工作。我将假设该脚本采用源参数(您所在的位置)和目标参数(您想要到达的位置),并且两者都是绝对路径名或都是相对路径名。如果一个是绝对名称,另一个是相对名称,最简单的方法就是在相对名称前加上当前工作目录的前缀 - 但下面的代码不会这样做。


谨防

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

  1. 丹尼斯·威廉姆森的评论中解决了这个问题。
  2. 还有一个问题是,这种纯粹的路径名文本处理可能会被奇怪的符号链接严重搞乱。
  3. 该代码不处理“ ”等路径中的杂散“点” xyz/./pqr
  4. 该代码不处理“ ”等路径中的杂散“双点” xyz/../pqr
  5. 简单地说:代码不会./从路径中删除前导“”。

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

(注意: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";
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

测试脚本的输出:

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
Run Code Online (Sandbox Code Playgroud)

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

source   = home/part1/part2
target   = .
relpath1 = ../../..
relpath2 = ../../../.
Run Code Online (Sandbox Code Playgroud)

这两个结果是等效的 - 只是不完全相同。(输出来自测试脚本的轻微修改版本 - 下面的 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);
Run Code Online (Sandbox Code Playgroud)

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