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

例子:

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


# Magic


relative="../../bar"

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

170974 次浏览

可悲的是,Mark Rushakoff的答案(现已删除-它引用了在这里中的代码)在适应时似乎不能正确工作:

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

注释中概述的思想可以经过改进,使其在大多数情况下正确工作。我假设脚本有一个源参数(您所在的位置)和一个目标参数(您想要到达的位置),这两个参数要么是绝对路径名,要么是相对路径名。如果一个是绝对的,另一个是相对的,最简单的方法是用当前工作目录作为相对名称的前缀——但是下面的代码没有这样做。


当心

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

  1. 丹尼斯·威廉姆森(Dennis Williamson)的评论中提到了一个问题。
  2. 还有一个问题,这种纯文本的路径名处理,你可能会被奇怪的符号链接搞得一团糟。
  3. 该代码不处理` xyz/./pqr `等路径中的流浪`点`。
  4. 该代码不处理` xyz/../pqr `等路径中的游离'双点'。
  5. 简单地说:该代码不会从路径中删除前导'./'。
Dennis的代码更好,因为它修复了1和5,但也有相同的问题2,3,4。 因此,使用Dennis的代码(并在此之前为其投票)

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


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

  • 设置相对路径为空。
  • 在组件相同的情况下,请跳到下一个。
  • 当对应的分量不同或一条路径没有更多分量时:
  • 如果没有剩余的源组件,且相对路径为空,则在开头添加“。”。
  • 对于每个剩余的源组件,在相对路径前加上“../”。
  • 如果没有剩余的目标组件,且相对路径为空,则在开头添加“。”。
  • 对于每个剩余的目标组件,将该组件添加到路径末尾的斜杠后面。

因此:

#!/bin/perl -w


use strict;


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


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


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


my $i;
for ($i = 0; $i < $count; $i++)
{
last if $source[$i] ne $target[$i];
}


$relpath = "." if ($i >= scalar(@source) && $relpath eq "");
for (my $s = $i; $s < scalar(@source); $s++)
{
$relpath = "../$relpath";
}
$relpath = "." if ($i >= scalar(@target) && $relpath eq "");
for (my $t = $i; $t < scalar(@target); $t++)
{
$relpath .= "/$target[$t]";
}


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


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

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

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


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


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


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


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


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


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


!


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

测试脚本的输出:

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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

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

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

这两个结果是等价的,只是不完全相同。(输出来自测试脚本的一个轻微修改版本-下面的Perl脚本只是打印答案,而不是像上面的脚本那样打印输入和答案。

#!/bin/perl -w
# Based loosely on code from: http://unix.derkeiler.com/Newsgroups/comp.unix.shell/2005-10/1256.html
# Via: http://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);

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

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

下面是一个shell脚本,它可以在不调用其他程序的情况下完成:

#! /bin/env bash


#bash script to find the relative path between two directories


mydir=${0%/}
mydir=${0%/*}
creadlink="$mydir/creadlink"


shopt -s extglob


relpath_ () {
path1=$("$creadlink" "$1")
path2=$("$creadlink" "$2")
orig1=$path1
path1=${path1%/}/
path2=${path2%/}/


while :; do
if test ! "$path1"; then
break
fi
part1=${path2#$path1}
if test "${part1#/}" = "$part1"; then
path1=${path1%/*}
continue
fi
if test "${path2#$path1}" = "$path2"; then
path1=${path1%/*}
continue
fi
break
done
part1=$path1
path1=${orig1#$part1}
depth=${path1//+([^\/])/..}
path1=${path2#$path1}
path1=${depth}${path2#$part1}
path1=${path1##+(\/)}
path1=${path1%/}
if test ! "$path1"; then
path1=.
fi
printf "$path1"


}


relpath_test () {
res=$(relpath_ /path1/to/dir1 /path1/to/dir2 )
expected='../dir2'
test_results "$res" "$expected"


res=$(relpath_ / /path1/to/dir2 )
expected='path1/to/dir2'
test_results "$res" "$expected"


res=$(relpath_ /path1/to/dir2 / )
expected='../../..'
test_results "$res" "$expected"


res=$(relpath_ / / )
expected='.'
test_results "$res" "$expected"


res=$(relpath_ /path/to/dir2/dir3 /path/to/dir1/dir4/dir4a )
expected='../../dir1/dir4/dir4a'
test_results "$res" "$expected"


res=$(relpath_ /path/to/dir1/dir4/dir4a /path/to/dir2/dir3 )
expected='../../../dir2/dir3'
test_results "$res" "$expected"


#res=$(relpath_ . /path/to/dir2/dir3 )
#expected='../../../dir2/dir3'
#test_results "$res" "$expected"
}


test_results () {
if test ! "$1" = "$2"; then
printf 'failed!\nresult:\nX%sX\nexpected:\nX%sX\n\n' "$@"
fi
}


#relpath_test

来源:http://www.ynform.org/w/Pub/Relpath

test.sh:

#!/bin/bash


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

测试:

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

这个脚本只对路径名有效。它不需要任何文件存在。如果传递的路径不是绝对的,那么行为就有点不寻常,但是如果两条路径都是相对的,那么应该能正常工作。

我只在OS X上测试了它,所以它可能不是可移植的。

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


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


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


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


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


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


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


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


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

我把你的问题作为一个挑战,用“可移植的”shell代码来编写它,即。

  • 考虑到POSIX外壳
  • 没有数组之类的bashisms
  • 避免像打瘟疫一样打外部电话。脚本中没有一个分叉!这使得它非常快,特别是在有显著分叉开销的系统上,比如cygwin。
  • 必须处理路径名中的glob字符(*,?,[,])

它运行在任何POSIX兼容shell (zsh, bash, ksh, ash, busybox,…)上。它甚至包含一个测试套件来验证其操作。路径名的规范化留作练习。: -)

#!/bin/sh


# Find common parent directory path for a pair of paths.
# Call with two pathnames as args, e.g.
# commondirpart foo/bar foo/baz/bat -> result="foo/"
# The result is either empty or ends with "/".
commondirpart () {
result=""
while test ${#1} -gt 0 -a ${#2} -gt 0; do
if test "${1%${1#?}}" != "${2%${2#?}}"; then   # First characters the same?
break                                       # No, we're done comparing.
fi
result="$result${1%${1#?}}"                    # Yes, append to result.
set -- "${1#?}" "${2#?}"                       # Chop first char off both strings.
done
case "$result" in
(""|*/) ;;
(*)     result="${result%/*}/";;
esac
}


# Turn foo/bar/baz into ../../..
#
dir2dotdot () {
OLDIFS="$IFS" IFS="/" result=""
for dir in $1; do
result="$result../"
done
result="${result%/}"
IFS="$OLDIFS"
}


# Call with FROM TO args.
relativepath () {
case "$1" in
(*//*|*/./*|*/../*|*?/|*/.|*/..)
printf '%s\n' "'$1' not canonical"; exit 1;;
(/*)
from="${1#?}";;
(*)
printf '%s\n' "'$1' not absolute"; exit 1;;
esac
case "$2" in
(*//*|*/./*|*/../*|*?/|*/.|*/..)
printf '%s\n' "'$2' not canonical"; exit 1;;
(/*)
to="${2#?}";;
(*)
printf '%s\n' "'$2' not absolute"; exit 1;;
esac


case "$to" in
("$from")   # Identical directories.
result=".";;
("$from"/*) # From /x to /x/foo/bar -> foo/bar
result="${to##$from/}";;
("")        # From /foo/bar to / -> ../..
dir2dotdot "$from";;
(*)
case "$from" in
("$to"/*)       # From /x/foo/bar to /x -> ../..
dir2dotdot "${from##$to/}";;
(*)             # Everything else.
commondirpart "$from" "$to"
common="$result"
dir2dotdot "${from#$common}"
result="$result/${to#$common}"
esac
;;
esac
}


set -f # noglob


set -x
cat <<EOF |
/ / .
/- /- .
/? /? .
/?? /?? .
/??? /??? .
/?* /?* .
/* /* .
/* /** ../**
/* /*** ../***
/*.* /*.** ../*.**
/*.??? /*.?? ../*.??
/[] /[] .
/[a-z]* /[0-9]* ../[0-9]*
/foo /foo .
/foo / ..
/foo/bar / ../..
/foo/bar /foo ..
/foo/bar /foo/baz ../baz
/foo/bar /bar/foo  ../../bar/foo
/foo/bar/baz /gnarf/blurfl/blubb ../../../gnarf/blurfl/blubb
/foo/bar/baz /gnarf ../../../gnarf
/foo/bar/baz /foo/baz ../../baz
/foo. /bar. ../bar.
EOF
while read FROM TO VIA; do
relativepath "$FROM" "$TO"
printf '%s\n' "FROM: $FROM" "TO:   $TO" "VIA:  $result"
if test "$result" != "$VIA"; then
printf '%s\n' "OOOPS! Expected '$VIA' but got '$result'"
fi
done


# vi: set tabstop=3 shiftwidth=3 expandtab fileformat=unix :
$ python -c "import os.path; print os.path.relpath('/foo/bar', '/foo/baz/foo')"

给:

../../bar

我的解决方案:

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


}

我猜这个也可以…(自带内置测试):)

好吧,预计会有一些开销,但我们在这里做的是伯恩壳!;)

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

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

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


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

kasku的扎的答案略有改进,它更好地处理了空格,并允许传递相对路径:

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

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

提醒:'-z'测试如果字符串长度为零(=空),'-n'测试如果字符串为空。

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


common_part=$source # for now
result="" # for now


while [[ "${target#$common_part}" == "${target}" ]]; do
# no match, means that candidate common part is not correct
# go up one level (reduce common part)
common_part="$(dirname $common_part)"
# and record that we went back, with correct / handling
if [[ -z $result ]]; then
result=".."
else
result="../$result"
fi
done


if [[ $common_part == "/" ]]; then
# special case for root (no common path)
result="$result/"
fi


# since we now have identified the common part,
# compute the non-common part
forward_part="${target#$common_part}"


# and now stick all parts together
if [[ -n $result ]] && [[ -n $forward_part ]]; then
result="$result$forward_part"
elif [[ -n $forward_part ]]; then
# extra slash removal
result="${forward_part:1}"
fi


echo $result

测试用例:

compute_relative.sh "/A/B/C" "/A"           -->  "../.."
compute_relative.sh "/A/B/C" "/A/B"         -->  ".."
compute_relative.sh "/A/B/C" "/A/B/C"       -->  ""
compute_relative.sh "/A/B/C" "/A/B/C/D"     -->  "D"
compute_relative.sh "/A/B/C" "/A/B/C/D/E"   -->  "D/E"
compute_relative.sh "/A/B/C" "/A/B/D"       -->  "../D"
compute_relative.sh "/A/B/C" "/A/B/D/E"     -->  "../D/E"
compute_relative.sh "/A/B/C" "/A/D"         -->  "../../D"
compute_relative.sh "/A/B/C" "/A/D/E"       -->  "../../D/E"
compute_relative.sh "/A/B/C" "/D/E/F"       -->  "../../../D/E/F"

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

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

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

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

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

第一个实现的目标是完全posix兼容。我已经在Debian 6.0.6“挤压”上用/bin/dash测试了它。它在OS X 10.8.3上也可以完美地与/bin/sh一起工作,这实际上是伪装成POSIX shell的Bash版本3.2。

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

最后,我写了一个ZSH脚本,根据其他答案中提供的测试用例,验证在$PATH中找到的relpath命令的输出。我通过在这些测试中添加一些空格、制表符和标点符号,比如! ? *,还添加了另一个测试,其中包含vim-powerline中发现的奇异UTF-8字符。

POSIX shell函数

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

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

ZSH壳函数

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

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

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

测试脚本

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

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


usage () {
print "\n    Usage: $script_name SRC_PATH DESTINATION_PATH\n" >&2
exit ${1:=1}
}
vrb () { $VERBOSE && print -P ${(%)@} || return 0; }


relpath_check () {
[[ $# -ge 1 ]] && [[ $# -le 2 ]] || return 1
target=${${2:-$1}}
prefix=${${${2:+$1}:-$PWD}}
result=$(relpath $prefix $target)
# Compare with python's os.path.relpath function
py_result=$(python -c "import os.path; print os.path.relpath('$target', '$prefix')")
col='%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

自2001年以来,它被内置在Perl中,所以它几乎可以在你能想象到的所有系统上工作,甚至虚拟机

perl -le 'use File::Spec; print File::Spec->abs2rel(@ARGV)' FILE BASE

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

举个例子:

perl -le 'use File::Spec; print File::Spec->abs2rel(@ARGV)' $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脚本的灵感来自扎的(谢谢!)。它会触发一个错误 在Stack Overflow的语法高亮显示模块中(至少在我的预览中是这样) 帧)。如果高亮显示不正确,请忽略

一些注意事项:

  • 在没有显著增加代码的情况下删除错误并改进代码 长度和复杂度
  • 为了便于使用,将功能放入函数中
  • 保持函数与POSIX兼容,以便它们(应该)与所有POSIX兼容 shell(在Ubuntu Linux 12.04中使用dash, bash和zsh测试)
  • 仅使用局部变量以避免打击全局变量和 污染全局名称空间
  • 两个目录路径都不需要存在(我的应用程序的要求)
  • 路径名可以包含空格、特殊字符、控制字符、 反斜杠,制表符,',",?,*,[,]等
  • 核心函数"relPath"仅使用POSIX shell内置程序,但需要 作为参数的规范绝对目录路径
  • 扩展函数"relpath"可以处理任意目录路径 相对的,非规范的),但需要外部GNU核心实用程序"readlink"
  • 避免内置的“echo”,而是使用内置的“printf”,原因有两个:
    • 由于冲突的历史实现内置的“回声”它 在不同的shell中有不同的行为-> POSIX建议printf 优先于echo.
    • 一些POSIX shell内置的“echo”将解释一些反斜杠 序列,从而破坏包含这种序列
    • 的路径名 李< / ul > < / >
    • 为了避免不必要的转换,路径名将在返回时使用 shell和OS实用程序(例如cd, ln, ls, find, mkdir; 不像python的“os.path”。Relpath”,它将解释一些反斜杠 李序列)< / >
    • 除了前面提到的反斜杠序列,函数"relPath"的最后一行 输出兼容python的路径名:

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

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

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

      我更喜欢后者,因为

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

        ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
        
      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)

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

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

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

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"

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

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

我已经从和其他一些想法的答案

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

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

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

编辑:确保"$B"在这种情况下,要么不存在,要么没有软链接,否则relpath遵循这个链接,这不是你想要的!

这几乎适用于当前所有的Linux。如果readlink -m在你这边不起作用,试试readlink -f。请参见https://gist.github.com/hilbix/1ec361d00a8178ae8ea0获取可能的更新:

: relpath A B
# Calculate relative path from A to B, returns true on success
# Example: ln -s "$(relpath "$A" "$B")" "$B"
relpath()
{
local X Y A
# We can create dangling softlinks
X="$(readlink -m -- "$1")" || return
Y="$(readlink -m -- "$2")" || return
X="${X%/}/"
A=""
while   Y="${Y%/*}"
[ ".${X#"$Y"/}" = ".$X" ]
do
A="../$A"
done
X="$A${X#"$Y"/}"
X="${X%/}"
echo "${X:-.}"
}

注:

  • 在文件名包含*?的情况下,要注意防止不必要的shell元字符扩展。
  • 输出可以用作ln -s的第一个参数:
    • relpath / /给出了.,而不是空字符串
    • relpath a a给出a,即使a恰好是一个目录
  • 大多数常见的病例也进行了测试,得出了合理的结果。
  • 此解决方案使用字符串前缀匹配,因此需要readlink来规范化路径。
  • 多亏了readlink -m,它也适用于尚未存在的路径。

在旧系统中,readlink -m不可用,如果文件不存在,readlink -f将失败。所以你可能需要一些像这样的解决方法(未经测试!):

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

如果$1包含不存在的路径的...(如/doesnotexist/./a),这就不太正确了,但它应该涵盖大多数情况。

(用readlink_missing替换上面的readlink -m --。)

编辑,因为下面是反对票

下面是一个测试,这个函数确实是正确的:

check()
{
res="$(relpath "$2" "$1")"
[ ".$res" = ".$3" ] && return
printf ':WRONG: %-10q %-10q gives %q\nCORRECT %-10q %-10q gives %q\n' "$1" "$2" "$res" "$@"
}


#     TARGET   SOURCE         RESULT
check "/A/B/C" "/A"           ".."
check "/A/B/C" "/A.x"         "../../A.x"
check "/A/B/C" "/A/B"         "."
check "/A/B/C" "/A/B/C"       "C"
check "/A/B/C" "/A/B/C/D"     "C/D"
check "/A/B/C" "/A/B/C/D/E"   "C/D/E"
check "/A/B/C" "/A/B/D"       "D"
check "/A/B/C" "/A/B/D/E"     "D/E"
check "/A/B/C" "/A/D"         "../D"
check "/A/B/C" "/A/D/E"       "../D/E"
check "/A/B/C" "/D/E/F"       "../../D/E/F"


check "/foo/baz/moo" "/foo/bar" "../bar"

困惑吗?嗯,这些是正确的结果 !即使你认为它不符合问题,以下是正确的证明:

check "http://example.com/foo/baz/moo" "http://example.com/foo/bar" "../bar"

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

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

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

这将返回所请求的内容。

在你感到惊讶之前,这里有一个更复杂一点的relpath变体(注意细微的区别),它也应该适用于url语法(因此,由于一些__abc2魔法,后面的/得以保留):

# Calculate relative PATH to the given DEST from the given BASE
# In the URL case, both URLs must be absolute and have the same Scheme.
# The `SCHEME:` must not be present in the FS either.
# This way this routine works for file paths an
: relpathurl DEST BASE
relpathurl()
{
local X Y A
# We can create dangling softlinks
X="$(readlink -m -- "$1")" || return
Y="$(readlink -m -- "$2")" || return
X="${X%/}/${1#"${1%/}"}"
Y="${Y%/}${2#"${2%/}"}"
A=""
while   Y="${Y%/*}"
[ ".${X#"$Y"/}" = ".$X" ]
do
A="../$A"
done
X="$A${X#"$Y"/}"
X="${X%/}"
echo "${X:-.}"
}

这里有一些检查,只是为了弄清楚:它确实像所说的那样工作。

check()
{
res="$(relpathurl "$2" "$1")"
[ ".$res" = ".$3" ] && return
printf ':WRONG: %-10q %-10q gives %q\nCORRECT %-10q %-10q gives %q\n' "$1" "$2" "$res" "$@"
}


#     TARGET   SOURCE         RESULT
check "/A/B/C" "/A"           ".."
check "/A/B/C" "/A.x"         "../../A.x"
check "/A/B/C" "/A/B"         "."
check "/A/B/C" "/A/B/C"       "C"
check "/A/B/C" "/A/B/C/D"     "C/D"
check "/A/B/C" "/A/B/C/D/E"   "C/D/E"
check "/A/B/C" "/A/B/D"       "D"
check "/A/B/C" "/A/B/D/E"     "D/E"
check "/A/B/C" "/A/D"         "../D"
check "/A/B/C" "/A/D/E"       "../D/E"
check "/A/B/C" "/D/E/F"       "../../D/E/F"


check "/foo/baz/moo" "/foo/bar" "../bar"
check "http://example.com/foo/baz/moo" "http://example.com/foo/bar" "../bar"


check "http://example.com/foo/baz/moo/" "http://example.com/foo/bar" "../../bar"
check "http://example.com/foo/baz/moo"  "http://example.com/foo/bar/" "../bar/"
check "http://example.com/foo/baz/moo/"  "http://example.com/foo/bar/" "../../bar/"

下面是如何用它从问题中得到想要的结果:

absolute="/foo/bar"
current="/foo/baz/foo"
relative="$(relpathurl "$absolute" "$current/")"
echo "$relative"

如果你发现什么东西不起作用,请在下面的评论中告诉我。谢谢。

PS:

为什么relpath的论点是“颠倒的”;和其他答案相比呢?

如果你改变

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

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

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

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

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

例如:

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

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

__abc0——> __abc1

现在:

__abc0——> __abc1

查看代码:

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

这个答案并没有解决问题的Bash部分,但因为我试图使用这个问题中的答案来实现Emacs中的这个功能,所以我将把它扔在那里。

Emacs实际上有一个开箱即用的函数:

ELISP> (file-relative-name "/a/b/c" "/a/b/c")
"."
ELISP> (file-relative-name "/a/b/c" "/a/b")
"c"
ELISP> (file-relative-name "/a/b/c" "/c/b")
"../../a/b/c"

这里的答案并不是每天都能用的。由于在纯bash中很难正确地做到这一点,我建议以下可靠的解决方案(类似于注释中的一个建议):

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

然后,你可以得到基于当前目录的相对路径:

echo $(relpath somepath)

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

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

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

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

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

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

#!/bin/bash


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


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


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

在bash中:

realDir=''
cd $(dirname $0) || exit
realDir=$(pwd)
cd -
echo $realDir