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

例:

absolute="/foo/bar" current="/foo/baz/foo" # Magic relative="../../bar" 

如何创build魔术(希望不要太复杂的代码…)?

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

得到:

 ../../bar 

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

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

例如:

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

这是对@pini目前最好的解决scheme进行了改进,function完善(可悲的是只能处理less数几个案例)

提醒:'-z'testingstring是否为零(=空),'-n'testingstring是否为空。

 # 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 

testing用例:

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

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

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

(我还没有在path中用换行符testing,因为它打破了基于从ZSH调用python -c的validation,这肯定是可能的。

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

因此,我提出了两个实现。

第一个实现的目标是完全符合POSIX标准 。 我在Debian 6.0.6“Squeeze”上用/bin/dashtesting过。 它也适用于OS X 10.8.3上的/bin/sh ,它实际上是Bash 3.2假装的POSIX shell。

第二个实现是一个ZSH shell函数,可以抵抗多个斜杠和其他path中的麻烦。 如果你有ZSH可用,这是推荐的版本,即使你是从另一个shell下面提供的脚本forms(即用#!/usr/bin/env zsh的shebang)来调用它。

最后,我写了一个ZSH脚本来validation在$PATHfind的relpath命令的输出,给出了其他答案中提供的testing用例。 我通过添加一些空格,制表符和标点符号来为这些testing添加一些香料! ? * ! ? * ! ? *在这里和那里,还扔在vim-powerline中发现异国情调的UTF-8字符的又一个testing。

POSIX shell函数

首先,符合POSIX标准的shell函数。 它适用于各种path,但不会清除多个斜杠或parsing符号链接。

 #!/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版本。 如果您希望将参数parsing为realpath -f (在Linux coreutils包中提供),请将第3行和第4行中的:areplace为:A

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

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

testing脚本

最后是testing脚本。 它接受一个选项,即-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 '/ ab/å/⮀*/!' '/ ab/å/⮀/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 

它是自2001年以来内置于Perl的 ,所以它几乎可以在你所能想象的所有系统上工作,甚至是VMS 。

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

此外,解决scheme很容易理解。

所以对于你的例子:

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

…会正常工作。

 #!/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的语法高亮模块中的错误(至less在我的预览框中)。 所以如果突出显示不正确,请忽略。

一些说明:

  • 删除错误和改进代码,而不会显着增加代码长度和复杂性
  • 将function放入function以便于使用
  • 保持与POSIX兼容的function,以便它们(应该)能够与所有POSIX shell一起工作(在Ubuntu Linux 12.04中使用dash,bash和zsh进行testing)
  • 只使用局部variables来避免全局variables的破坏,并污染全局名称空间
  • 两个目录path不需要存在(需要我的应用程序)
  • path名可以包含空格,特殊字符,控制字符,反斜杠,制表符,',',?,*,[,]等
  • 核心函数“relPath”仅使用POSIX shell builtins,但要求规范的绝对目录path作为参数
  • 扩展函数“relpath”可以处理任意的目录path(也是相对的,非规范的),但是需要外部的GNU核心工具“readlink”
  • 避免了内build的“回声”,并使用内置的“printf”,原因有两个:
    • 由于内build“echo”的历史实现相互冲突,它在不同的shell中performance不同 – > POSIXbuild议printf优先于echo 。
    • 一些POSIX shell的内build“回声”会解释一些反斜杠序列 ,从而破坏包含这些序列的path名
  • 为了避免不必要的转换,path名被使用,因为它们被shell和OS实用程序返回和期望(例如,cd,ln,ls,find,mkdir;不像Python的“os.path.relpath”,它将解释一些反斜杠序列)
  • 除了提到的反斜杠序列,函数“relPath”的最后一行输出与python兼容的path名:

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

    最后一行可以逐行replace(并简化)

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

    我更喜欢后者

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

       ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>" 
    2. 与此方法创build的同一个dir中的符号链接没有在文件名前加上丑陋的"./"

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

回归testing的代码清单(简单地将它附加到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 -rp; 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' '/' '..' '/ ab/å/⮀*/!' '/ ab/å/⮀/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' '/-' '/-' '.' '/?' '/?' '.' '/??' '/??' '.' '/???' '/???' '.' '/?*' '/?*' '.' '/*' '/*' '.' '/*' '/**' '../**' '/*' '/***' '../***' '/*.*' '/*.**' '../*.**' '/*.???' '/*.??' '../*.??' '/[]' '/[]' '.' '/[az]*' '/[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 -rp; 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 

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

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

我已经打高尔夫pini和其他一些想法的答案

我只是用Perl来做这个不那么简单的任务:

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

kasku和Pini的答案有轻微的改进, 这个答案在空间上更好,并允许通过相对path:

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

此脚本仅为绝对path或不具有相对path的input提供了正确的结果...

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

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 

testing:

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

这里没有很多的答案是每天使用的实用。 由于在纯粹的bash中很难做到这一点,我提出以下可靠的解决scheme(类似于评论中的一个build议):

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

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

 echo $(relpath somepath) 

或者你可以指定path是相对于给定的目录:

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

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

  • 它在任何python> = 2.6中工作相同
  • 它不要求文件或目录存在。
  • 文件名可能包含更广泛的特殊字符。 例如,如果文件名包含空格或其他特殊字符,许多其他解决scheme不起作用。
  • 这是一个单线函数,不会混乱脚本。

请注意,包含basenamedirname解决scheme可能不一定更好,因为它们需要安装coreutils 。 如果有人有一个纯粹的bash解决scheme是可靠和简单的(而不是一个错综复杂的好奇),我会感到惊讶。

可悲的是,Mark Rushakoff的回答(现在删除 – 它引用了这里的代码)似乎并没有正确工作,当适应:

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

在评论中概述的思想可以被改进,以使其在大多数情况下正确工作。 我将假定脚本需要一个源参数(你在哪里)和一个目标参数(你想去的地方),而且两者都是绝对path名,或者两者都是相对的。 如果其中一个是绝对的,另一个是相对的,那么最简单的办法就是将相对名称加上当前工作目录 – 但是下面的代码并没有这样做。


谨防

下面的代码接近正常工作,但不是很正确。

  1. 丹尼斯·威廉姆森(Dennis Williamson)的评论中提到了这个问题。
  2. 还有一个问题,这个纯粹的文本处理path名,你可以认真搞砸了奇怪的符号链接。
  3. 该代码不会处理像“ xyz/./pqr ”这样的path中的杂散“点”。
  4. 代码不会像“ xyz/../pqr ”那样处理path中的“双点”。
  5. 平凡:代码不会从path中删除前导“ ./ ”。

丹尼斯的代码更好,因为它修复了1和5 – 但是具有相同的问题2,3,4。因此,使用Dennis的代码(并且提前投票)。

(注意:POSIX提供了一个系统调用realpath()来parsingpath名,这样就不会留下任何符号链接,将它应用到input名称,然后使用Dennis的代码,每次都会给出正确的答案。包装realpath() C代码 – 我已经做到了 – 但我不知道这样做的标准实用程序。)


为此,我发现Perl比shell更容易使用,尽pipebash对arrays有很好的支持,也可能也是这样做的 – 对读者来说。 所以,给定两个兼容的名称,将它们分成组件:

  • 将相对path设置为空。
  • 虽然组件是相同的,跳到下一个。
  • When corresponding components are different or there are no more components for one path:
  • If there are no remaining source components and the relative path is empty, add "." to the start.
  • For each remaining source component, prefix the relative path with "../".
  • If there are no remaining target components and the relative path is empty, add "." to the start.
  • For each remaining target component, add the component to the end of the path after a slash.

从而:

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

Test script (the square brackets contain a blank and a tab):

 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 

Output from the test script:

 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 

This Perl script works fairly thoroughly on Unix (it does not take into account all the complexities of Windows path names) in the face of weird inputs. It uses the module Cwd and its function realpath to resolve the real path of names that exist, and does a textual analysis for paths that don't exist. In all cases except one, it produces the same output as Dennis's script. The deviant case is:

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

The two results are equivalent – just not identical. (The output is from a mildly modified version of the test script – the Perl script below simply prints the answer, rather than the inputs and the answer as in the script above.) Now: should I eliminate the non-working answer? Maybe…

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

I took your question as a challenge to write this in "portable" shell code, ie

  • with a POSIX shell in mind
  • no bashisms such as arrays
  • avoid calling externals like the plague. There's not a single fork in the script! That makes it blazingly fast, especially on systems with significant fork overhead, like cygwin.
  • Must deal with glob characters in pathnames (*, ?, [, ])

It runs on any POSIX conformant shell (zsh, bash, ksh, ash, busybox, …). It even contains a testsuite to verify its operation. Canonicalization of pathnames is left as an exercise. 🙂

 #!/bin/sh # Find common parent directory path for a pair of paths. # Call with two pathnames as args, eg # 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 | / / . /- /- . /? /? . /?? /?? . /??? /??? . /?* /?* . /* /* . /* /** ../** /* /*** ../*** /*.* /*.** ../*.** /*.??? /*.?? ../*.?? /[] /[] . /[az]* /[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 : 

我的解决scheme

 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 } 

Guess this one shall do the trick too… (comes with built-in tests) 🙂

OK, some overhead expected, but we're doing Bourne shell here! ;)

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

This script works only on the path names. It does not require any of the files to exist. If the paths passed are not absolute, the behavior is a bit unusual, but it should work as expected if both paths are relative.

I only tested it on OS X, so it might not be portable.

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

这是我的版本。 It's based on the answer by @Offirmo . I made it Dash-compatible and fixed the following testcase failure:

./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 } 

Here's a shell script that does it without calling other programs:

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

source: http://www.ynform.org/w/Pub/Relpath

I needed something like this but which resolved symbolic links too. I discovered that pwd has a -P flag for that purpose. A fragment of my script is appended. It's within a function in a shell script, hence the $1 and $2. The result value, which is the relative path from START_ABS to END_ABS, is in the UPDIRS variable. The script cd's into each parameter directory in order to execute the pwd -P and this also means that relative path parameters are handled. Cheers, Jim

 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" 

Yet another solution, pure bash + GNU readlink for easy use in following context:

 ln -s "$(relpath "$A" "$B")" "$B" 

Edit: Make sure that "$B" is either not existing or no softlink in that case, else relpath follows this link which is not what you want!

This works in nearly all current Linux. If readlink -m does not work at your side, try readlink -f instead. See also https://gist.github.com/hilbix/1ec361d00a8178ae8ea0 for possible updates:

 : relpath AB # Calculate relative path from A to B, returns true on success # Example: ln -s "$(relpath "$A" "$B")" "$B" relpath() { local XYA # 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:-.}" } 

笔记:

  • Care was taken that it is safe against unwanted shell meta character expansion, in case filenames contain * or ?
  • The output is meant to be usable as the first argument to ln -s :
    • relpath / / gives . and not the empty string
    • relpath aa gives a , even if a happens to be a directory
  • Most common cases were tested to give reasonable results, too.
  • This solution uses string prefix matching, hence readlink is required to canonicalize paths.
  • Thanks to readlink -m it works for not yet existing paths, too.

On old systems, where readlink -m is not available, readlink -f fails if the file does not exist. So you probably need some workaround like this (untested!):

 readlink_missing() { readlink -m -- "$1" && return readlink -f -- "$1" && return [ -e . ] && echo "$(readlink_missing "$(dirname "$1")")/$(basename "$1")" } 

This is not really quite correct in case $1 includes . or .. for nonexisting paths (like in /doesnotexist/./a ), but it should cover most cases.

(Replace readlink -m -- above by readlink_missing .)

This answer does not address the Bash part of the question, but because I tried to use the answers in this question to implement this functionality in Emacs I'll throw it out there.

Emacs actually has a function for this out of the box:

 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"