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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/2564634/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-09 19:08:00  来源:igfitidea点击:

Convert absolute path into relative path given a current directory using Bash

bashshellpathrelative-pathabsolute-path

提问by Paul Tarjan

Example:

例子:

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

# Magic

relative="../../bar"

How do I create the magic (hopefully not too complicated code...)?

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

回答by modulus0

Using realpath from GNU coreutils 8.23 is the simplest, I think:

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

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

For example:

例如:

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

回答by xni

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

gives:

给出:

../../bar

回答by Offirmo

This is a corrected, fully functional improvement of the currently best rated solution from @pini (which sadly handle only a few cases)

这是对@pini 目前评价最高的解决方案的更正、功能齐全的改进(遗憾的是它只处理少数情况)

Reminder : '-z' test if the string is zero-length (=empty) and '-n' test if the string is notempty.

提醒:“-z”测试如果字符串是零长度(=空)和“-n”测试如果字符串是空。

# both  and  are absolute paths beginning with /
# returns relative path to /$target from /$source
source=
target=

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

Test cases :

测试用例 :

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"

回答by pini

#!/bin/bash
# both  and  are absolute paths
# returns  relative to 

source=
target=

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

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

回答by Erik Aronesty

It is built in to Perlsince 2001, so it works on nearly every system you can imagine, even VMS.

它自 2001 年就内置于Perl 中,因此它几乎适用于您可以想象的所有系统,甚至VMS

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

Also, the solution is easy to understand.

此外,该解决方案很容易理解。

So for your example:

所以对于你的例子:

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

...would work fine.

...会工作得很好。

回答by Alexx Roche

Presuming that you have installed: bash, pwd, dirname, echo; then relpath is

假设你已经安装了:bash、pwd、dirname、echo;那么 relpath 是

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

I've golfed the answer from piniand a few other ideas

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

Note: This requires both paths to be existing folders. Files will notwork.

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

回答by simonair

Python's os.path.relpathas a shell function

Pythonos.path.relpath作为 shell 函数

The goal of this relpathexercise is to mimic Python 2.7's os.path.relpathfunction (available from Python version 2.6 but only working properly in 2.7), as proposed by xni. As a consequence, some of the results may differ from functions provided in other answers.

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

(I have not tested with newlines in paths simply because it breaks the validation based on calling python -cfrom ZSH. It would certainly be possible with some effort.)

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

Regarding “magic” in Bash, I have given up looking for magic in Bash long ago, but I have since found all the magic I need, and then some, in ZSH.

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

Consequently, I propose two implementations.

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

The first implementation aims to be fully POSIX-compliant. I have tested it with /bin/dashon Debian 6.0.6 “Squeeze”. It also works perfectly with /bin/shon OS X 10.8.3, which is actually Bash version 3.2 pretending to be a POSIX shell.

第一个实现旨在完全符合 POSIX。我已经/bin/dash在 Debian 6.0.6 “Squeeze”上对其进行了测试 。它也适用/bin/sh于 OS X 10.8.3,它实际上是伪装成 POSIX shell 的 Bash 3.2 版。

The second implementation is a ZSH shell function that is robust against multiple slashes and other nuisances in paths. If you have ZSH available, this is the recommended version, even if you are calling it in the script form presented below (i.e. with a shebang of #!/usr/bin/env zsh) from another shell.

第二个实现是一个 ZSH shell 函数,它对路径中的多个斜杠和其他麻烦有很强的抵抗力。如果您有可用的 ZSH,这是推荐的版本,即使您是#!/usr/bin/env zsh从另一个 shell以下面显示的脚本形式(即使用 的 shebang )调用它也是如此。

Finally, I have written a ZSH script that verifies the output of the relpathcommand found in $PATHgiven the test cases provided in other answers. I added some spice to those tests by adding some spaces, tabs, and punctuation such as ! ? *here and there and also threw in yet another test with exotic UTF-8 characters found in vim-powerline.

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

POSIXshell function

POSIX外壳函数

First, the POSIX-compliant shell function. It works with a variety of paths, but does not clean multiple slashes or resolve symlinks.

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

#!/bin/sh
relpath () {
    [ $# -ge 1 ] && [ $# -le 2 ] || return 1
    current="${2:+""}"
    target="${2:-""}"
    [ "$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 function

ZSH 外壳函数

Now, the more robust zshversion. If you would like it to resolve the arguments to real paths à la realpath -f(available in the Linux coreutilspackage), replace the :aon lines 3 and 4 with :A.

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

To use this in zsh, remove the first and last line and put it in a directory that is in your $FPATHvariable.

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

#!/usr/bin/env zsh
relpath () {
    [[ $# -ge 1 ]] && [[ $# -le 2 ]] || return 1
    local target=${${2:-}:a} # replace `:a' by `:A` to resolve symlinks
    local current=${${${2:+}:-$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 "$@"

Test script

测试脚本

Finally, the test script. It accepts one option, namely -vto enable verbose output.

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

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

# Return relative path from canonical absolute dir path  to canonical
# absolute dir path  ( and/or  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  to dir  (Does not impose any
# restrictions on  and  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 "")" "$(readlink -m "")"; }
) 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:-}} prefix=${${${2:+}:-$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 ]] && [[ = "-v" ]]; then VERBOSE=true shift fi if [[ $# -eq 0 ]]; then run_checks else VERBOSE=true relpath_check "$@" fi

回答by linuxball

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

Above shell script was inspired by pini's(Thanks!). It triggers a bug in the syntax highlighting module of Stack Overflow (at least in my preview frame). So please ignore if highlighting is incorrect.

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

Some notes:

一些注意事项:

  • Removed errors and improved code without significantly increasing code length and complexity
  • Put functionality into functions for easiness of use
  • Kept functions POSIX compatible so that they (should) work with all POSIX shells (tested with dash, bash, and zsh in Ubuntu Linux 12.04)
  • Used local variables only to avoid clobbering global variables and polluting the global name space
  • Both directory paths DO NOT need to exist (requirement for my application)
  • Pathnames may contain spaces, special characters, control characters, backslashes, tabs, ', ", ?, *, [, ], etc.
  • Core function "relPath" uses POSIX shell builtins only but requires canonical absolute directory paths as parameters
  • Extended function "relpath" can handle arbitrary directory paths (also relative, non-canonical) but requires external GNU core utility "readlink"
  • Avoided builtin "echo" and used builtin "printf" instead for two reasons:
  • To avoid unnecessary conversions, pathnames are used as they are returned and expected by shell and OS utilities (e.g. cd, ln, ls, find, mkdir; unlike python's "os.path.relpath" which will interpret some backslash sequences)
  • Except for the mentioned backslash sequences the last line of function "relPath" outputs pathnames compatible to python:

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

    Last line can be replaced (and simplified) by line

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

    I prefer the latter because

    1. Filenames can be directly appended to dir paths obtained by relPath, e.g.:

      path=$up${path#"$common"/}; path=${path%/}; printf %s "${path:-.}"
      
    2. Symbolic links in the same dir created with this method do not have the ugly "./"prepended to the filename.

  • If you find an error please contact linuxball (at) gmail.com and I'll try to fix it.
  • Added regression test suite (also POSIX shell compatible)
  • 在不显着增加代码长度和复杂性的情况下消除错误并改进代码
  • 将功能放入函数中以方便使用
  • 保持函数 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兼容的路径名:

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

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

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

    我更喜欢后者,因为

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

      ############################################################################
      # If called with 2 arguments assume they are dir paths and print rel. path #
      ############################################################################
      
      test "$#" = 2 && {
          printf '%s\n' "Rel. path from '' to '' is '$(relpath "" "")'."
          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  in '#'*|'') continue;; esac # Skip comments and empty lines
          # q stores quoting character, use " if ' is used in path name
          q="'"; case  in *"'"*) q='"';; esac
          rPOk=passed rP=$(relPath "" ""); test "$rP" = "" || rPOk=$rP
          rpOk=passed rp=$(relpath "" ""); test "$rp" = "" || rpOk=$rp
          RPOk=passed
          RP=$(python -c "import os.path; print os.path.relpath($q$q, $q$q)")
          test "$RP" = "" || RPOk=$RP
          printf \
          "$format" "$q$q" "$q$q" "$q$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  in '#'*|'') continue;; esac # Skip comments and empty lines
          # q stores quoting character, use " if ' is used in path name
          q="'"; case  in *"'"*) q='"';; esac
          rpOk=passed rp=$(relpath "" ""); test "$rp" = "" || rpOk=$rp
          RPOk=passed
          RP=$(python -c "import os.path; print os.path.relpath($q$q, $q$q)")
          test "$RP" = "" || RPOk=$RP
          printf "$format" "$q$q" "$q$q" "$q$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
      
    2. 使用此方法创建的同一目录中的符号链接不会"./"在文件名前加上丑陋的内容。

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

Code listing for regression tests (simply append it to the shell script):

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

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

回答by Gary Wisniewski

Not a lot of the answers here are practical for every day use. Since it is very difficult to do this properly in pure bash, I suggest the following, reliable solution (similar to one suggestion buried in a comment):

这里没有很多答案适用于日常使用。由于在纯 bash 中很难正确执行此操作,因此我建议采用以下可靠的解决方案(类似于隐藏在评论中的一个建议):

echo $(relpath somepath)

Then, you can get the relative path based upon the current directory:

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

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

or you can specify that the path be relative to a given directory:

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

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

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

The one disadvantage is this requires python, but:

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

  • It works identically in any python >= 2.6
  • It does not require that the files or directories exist.
  • Filenames may contain a wider range of special characters. For example, many other solutions do not work if filenames contain spaces or other special characters.
  • It is a one-line function that doesn't clutter scripts.
  • 它在任何 python >= 2.6 中的工作方式相同
  • 它不要求文件或目录存在。
  • 文件名可能包含更广泛的特殊字符。例如,如果文件名包含空格或其他特殊字符,许多其他解决方案将不起作用。
  • 它是一个单行函数,不会使脚本混乱。

Note that solutions which include basenameor dirnamemay not necessarily be better, as they require that coreutilsbe installed. If somebody has a pure bashsolution that is reliable and simple (rather than a convoluted curiosity), I'd be surprised.

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

回答by Gary Wisniewski

I would just use Perl for this not-so-trivial task:

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

##代码##