使用 Bash 将给定当前目录的绝对路径转换为相对路径
Posted
技术标签:
【中文标题】使用 Bash 将给定当前目录的绝对路径转换为相对路径【英文标题】:Convert absolute path into relative path given a current directory using Bash 【发布时间】:2011-02-03 14:40:41 【问题描述】:例子:
absolute="/foo/bar"
current="/foo/baz/foo"
# Magic
relative="../../bar"
我如何创造魔法(希望代码不要太复杂...)?
【问题讨论】:
例如(我现在的情况)为 gcc 提供相对路径,这样即使源路径发生变化,它也可以生成可用的相对调试信息。 在 U&L 上提出了一个类似的问题:unix.stackexchange.com/questions/100918/…。其中一个答案 (@Gilles) 提到了一个工具,symlinks,它可以轻松解决这个问题。 简单:realpath --relative-to=$absolute $current
.
【参考方案1】:
我使用的是默认没有realpath
命令的macOS,所以我做了一个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#./
【讨论】:
【参考方案2】:另一种解决方案,纯 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 -m --
替换为readlink_missing
。)
编辑因为不赞成投票跟随
这是一个测试,这个函数确实是正确的:
check()
res="$(relpath "$2" "$1")"
[ ".$res" = ".$3" ] && return
printf ':WRONG: %-10q %-10q gives %q\nCORRECT %-10q %-10q gives %q\n' "$1" "$2" "$res" "$@"
# TARGET SOURCE RESULT
check "/A/B/C" "/A" ".."
check "/A/B/C" "/A.x" "../../A.x"
check "/A/B/C" "/A/B" "."
check "/A/B/C" "/A/B/C" "C"
check "/A/B/C" "/A/B/C/D" "C/D"
check "/A/B/C" "/A/B/C/D/E" "C/D/E"
check "/A/B/C" "/A/B/D" "D"
check "/A/B/C" "/A/B/D/E" "D/E"
check "/A/B/C" "/A/D" "../D"
check "/A/B/C" "/A/D/E" "../D/E"
check "/A/B/C" "/D/E/F" "../../D/E/F"
check "/foo/baz/moo" "/foo/bar" "../bar"
困惑?嗯,这些是正确的结果!即使您认为它不适合这个问题,也可以证明这是正确的:
check "http://example.com/foo/baz/moo" "http://example.com/foo/bar" "../bar"
毫无疑问,../bar
是从页面moo
看到的页面bar
的准确且唯一正确的相对路径。其他一切都是错误的。
采用显然假设current
是一个目录的问题的输出是微不足道的:
absolute="/foo/bar"
current="/foo/baz/foo"
relative="../$(relpath "$absolute" "$current")"
这将准确返回所要求的内容。
在你挑眉之前,这里有一个更复杂的 relpath
变体(发现细微差别),它也应该适用于 URL 语法(所以尾随 /
仍然存在,感谢一些 @ 987654357@-magic):
# Calculate relative PATH to the given DEST from the given BASE
# In the URL case, both URLs must be absolute and have the same Scheme.
# The `SCHEME:` must not be present in the FS either.
# This way this routine works for file paths an
: relpathurl DEST BASE
relpathurl()
local X Y A
# We can create dangling softlinks
X="$(readlink -m -- "$1")" || return
Y="$(readlink -m -- "$2")" || return
X="$X%//$1#"$1%/""
Y="$Y%/$2#"$2%/""
A=""
while Y="$Y%/*"
[ ".$X#"$Y"/" = ".$X" ]
do
A="../$A"
done
X="$A$X#"$Y"/"
X="$X%/"
echo "$X:-."
这里的检查只是为了说明:它确实如所描述的那样工作。
check()
res="$(relpathurl "$2" "$1")"
[ ".$res" = ".$3" ] && return
printf ':WRONG: %-10q %-10q gives %q\nCORRECT %-10q %-10q gives %q\n' "$1" "$2" "$res" "$@"
# TARGET SOURCE RESULT
check "/A/B/C" "/A" ".."
check "/A/B/C" "/A.x" "../../A.x"
check "/A/B/C" "/A/B" "."
check "/A/B/C" "/A/B/C" "C"
check "/A/B/C" "/A/B/C/D" "C/D"
check "/A/B/C" "/A/B/C/D/E" "C/D/E"
check "/A/B/C" "/A/B/D" "D"
check "/A/B/C" "/A/B/D/E" "D/E"
check "/A/B/C" "/A/D" "../D"
check "/A/B/C" "/A/D/E" "../D/E"
check "/A/B/C" "/D/E/F" "../../D/E/F"
check "/foo/baz/moo" "/foo/bar" "../bar"
check "http://example.com/foo/baz/moo" "http://example.com/foo/bar" "../bar"
check "http://example.com/foo/baz/moo/" "http://example.com/foo/bar" "../../bar"
check "http://example.com/foo/baz/moo" "http://example.com/foo/bar/" "../bar/"
check "http://example.com/foo/baz/moo/" "http://example.com/foo/bar/" "../../bar/"
下面是如何使用它来从问题中给出想要的结果:
absolute="/foo/bar"
current="/foo/baz/foo"
relative="$(relpathurl "$absolute" "$current/")"
echo "$relative"
如果您发现某些东西不起作用,请在下面的 cmets 中告诉我。谢谢。
PS:
为什么relpath
的论点与这里的所有其他答案相反?
如果你改变了
Y="$(readlink -m -- "$2")" || return
到
Y="$(readlink -m -- "$2:-"$PWD"")" || return
然后您可以保留第二个参数,这样 BASE 就是当前目录/URL/whatever。和往常一样,这只是 Unix 原则。
【讨论】:
【参考方案3】:假设你已经安装了:bash、pwd、dirname、echo;那么 relpath 是
#!/bin/bash
s=$(cd $1%%/;pwd); d=$(cd $2;pwd); b=; while [ "$d#$s/" == "$d" ]
do s=$(dirname $s);b="../$b"; done; echo $b$d#$s/
我已经从pini 和其他一些想法中得到了答案
注意:这要求两个路径都是现有文件夹。文件将不工作。
【讨论】:
理想答案:与 /bin/sh 一起使用,不需要 readlink、python、perl -> 非常适合轻型/嵌入式系统或 windows bash 控制台 不幸的是,这需要路径存在,这并不总是需要的。 虔诚的回答。 cd-pwd 的东西是为了解决我猜的链接?打高尔夫球不错!【参考方案4】:我认为使用 GNU coreutils 8.23 中的 realpath 是最简单的:
$ realpath --relative-to="$file1" "$file2"
例如:
$ realpath --relative-to=/usr/bin/nmap /tmp/testing
../../../tmp/testing
【讨论】:
在Ubuntu 14.04上这个包已经过时了,没有--relative-to选项,很可惜。 在 Ubuntu 16.04 上运行良好 如果您想要相对于当前工作目录的路径,$ realpath --relative-to="$PWD" "$file"
很有用。
这适用于/usr/bin/nmap/
-path 中的内容,但不适用于/usr/bin/nmap
:从nmap
到/tmp/testing
只有../../
,而不是../
的3 次。但是它可以工作,因为在 rootfs 上执行 ..
是 /
。
作为@PatrickB。暗示,--relative-to=…
需要一个目录并且不检查。这意味着如果您请求相对于文件的路径,您最终会得到一个额外的“../”(就像这个例子一样,因为/usr/bin
很少或从不包含目录,而nmap
通常是二进制文件)
【参考方案5】:
这是我的版本。它基于answer by @Offirmo。我使它与 Dash 兼容并修复了以下测试用例失败:
./compute-relative.sh "/a/b/c/de/f/g" "/a/b/c/def/g/"
--> "../..f/g/"
现在:
CT_FindRelativePath "/a/b/c/de/f/g" "/a/b/c/def/g/"
--> "../../../def/g/"
查看代码:
# both $1 and $2 are absolute paths beginning with /
# returns relative path to $2/$target from $1/$source
CT_FindRelativePath()
local insource=$1
local intarget=$2
# Ensure both source and target end with /
# This simplifies the inner loop.
#echo "insource : \"$insource\""
#echo "intarget : \"$intarget\""
case "$insource" in
*/) ;;
*) source="$insource"/ ;;
esac
case "$intarget" in
*/) ;;
*) target="$intarget"/ ;;
esac
#echo "source : \"$source\""
#echo "target : \"$target\""
local common_part=$source # for now
local result=""
#echo "common_part is now : \"$common_part\""
#echo "result is now : \"$result\""
#echo "target#common_part : \"$target#$common_part\""
while [ "$target#$common_part" = "$target" -a "$common_part" != "//" ]; do
# no match, means that candidate common part is not correct
# go up one level (reduce common part)
common_part=$(dirname "$common_part")/
# and record that we went back
if [ -z "$result" ]; then
result="../"
else
result="../$result"
fi
#echo "(w) common_part is now : \"$common_part\""
#echo "(w) result is now : \"$result\""
#echo "(w) target#common_part : \"$target#$common_part\""
done
#echo "(f) common_part is : \"$common_part\""
if [ "$common_part" = "//" ]; then
# special case for root (no common path)
common_part="/"
fi
# since we now have identified the common part,
# compute the non-common part
forward_part="$target#$common_part"
#echo "forward_part = \"$forward_part\""
if [ -n "$result" -a -n "$forward_part" ]; then
#echo "(simple concat)"
result="$result$forward_part"
elif [ -n "$forward_part" ]; then
result="$forward_part"
fi
#echo "result = \"$result\""
# if a / was added to target and result ends in / then remove it now.
if [ "$intarget" != "$target" ]; then
case "$result" in
*/) result=$(echo "$result" | awk ' string=substr($0, 1, length($0)-1); print string; ' ) ;;
esac
fi
echo $result
return 0
【讨论】:
【参考方案6】:这个答案没有解决问题的 Bash 部分,但是因为我试图使用这个问题中的答案来实现 Emacs 中的这个功能,所以我会把它扔在那里。
Emacs 实际上有一个开箱即用的功能:
ELISP> (file-relative-name "/a/b/c" "/a/b/c")
"."
ELISP> (file-relative-name "/a/b/c" "/a/b")
"c"
ELISP> (file-relative-name "/a/b/c" "/c/b")
"../../a/b/c"
【讨论】:
请注意,对于您提供的测试用例,我相信我最近添加的 python 答案(relpath
函数)的行为与 file-relative-name
相同。【参考方案7】:
它自 2001 年以来内置于 Perl,因此它几乎适用于您能想象到的所有系统,甚至是 VMS。
perl -e 'use File::Spec; print File::Spec->abs2rel(@ARGV) . "\n"' FILE BASE
而且,解决方案很容易理解。
所以对于你的例子:
perl -e 'use File::Spec; print File::Spec->abs2rel(@ARGV) . "\n"' $absolute $current
...可以正常工作。
【讨论】:
say
在 perl 中不能用作日志,但在这里可以有效地使用。 perl -MFile::Spec -E 'say File::Spec->abs2rel(@ARGV)'
+1 但另见 this similar answer 较旧(2012 年 2 月)。另请阅读来自William Pursell 的相关 cmets。我的版本是两个命令行:perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target"
和 perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target" "$origin"
。第一个单行perl 脚本使用一个参数(原点是当前工作目录)。第二个单行 perl 脚本使用两个参数。
这应该是公认的答案。 perl
几乎随处可见,但答案仍然是单行的。【参考方案8】:
此脚本仅适用于路径名。它不需要任何文件存在。如果传递的路径不是绝对的,则行为有点不寻常,但如果两条路径都是相对的,它应该会按预期工作。
我只在 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
【讨论】:
另外,代码有点复制和粘贴,但我需要这个很快。 很好......正是我所需要的。而且你已经包含了一个比我见过的大多数其他人更好的规范化例程(通常依赖于正则表达式替换)。【参考方案9】:这里没有很多答案适合日常使用。由于在纯 bash 中正确执行此操作非常困难,因此我建议以下可靠的解决方案(类似于隐藏在评论中的一个建议):
function relpath()
python -c "import os,sys;print(os.path.relpath(*(sys.argv[1:])))" "$@";
然后,可以根据当前目录获取相对路径:
echo $(relpath somepath)
或者您可以指定路径是相对于给定目录的:
echo $(relpath somepath /etc) # relative to /etc
一个缺点是这需要python,但是:
它在任何 python >= 2.6 中的工作原理都是一样的 不要求文件或目录存在。 文件名可能包含更广泛的特殊字符。 例如,如果文件名包含许多其他解决方案将不起作用 空格或其他特殊字符。 这是一个不会使脚本混乱的单行函数。请注意,包含basename
或dirname
的解决方案不一定更好,因为它们需要安装coreutils
。如果有人有一个可靠且简单的纯bash
解决方案(而不是令人费解的好奇心),我会感到惊讶。
【讨论】:
这似乎是迄今为止最可靠的方法。【参考方案10】:这是对来自 @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"
【讨论】:
集成在offirmo shell lib github.com/Offirmo/offirmo-shell-lib,函数«OSL_FILE_find_relative_path»(文件«osl_lib_file.sh») +1。通过将source=$1; target=$2
替换为 source=$(realpath $1); target=$(realpath $2)
,可以轻松地处理任何路径(不仅仅是以 / 开头的绝对路径)
@Josh 确实如此,前提是 dirs 确实存在......这对于单元测试来说是不方便的 ;) 但在实际使用中是的,建议使用 realpath
,或者 source=$(readlink -f $1)
等,如果 realpath不可用(非标准)
我这样定义$source
和$target
:` if [[ -e $1 ]];然后 source=$(readlink -f $1);否则来源=$1;如果 [[ -e $2 ]];然后目标=$(readlink -f $2);否则目标=$2; fi` 这样,该函数可以处理真实/现有的相对路径以及虚构的目录。
@NathanS.Watson-Haigh 更好的是,我最近发现readlink
有一个-m
选项可以做到这一点;)【参考方案11】:
$ python -c "import os.path; print os.path.relpath('/foo/bar', '/foo/baz/foo')"
给予:
../../bar
【讨论】:
它有效,它使替代方案看起来很荒谬。这对我来说是个奖励xD +1。好吧,你被骗了……但这太好了,不能用!relpath() python -c "import os.path; print os.path.relpath('$1','$2:-$PWD')" ;
遗憾的是,这并不是普遍可用的:os.path.relpath 是 Python 2.6 中的新功能。
@ChenLevy:Python 2.6 于 2008 年发布。很难相信它在 2012 年还没有普及。
python -c 'import os, sys; print(os.path.relpath(*sys.argv[1:]))'
工作最自然、最可靠。【参考方案12】:
我需要这样的东西,但它也解决了符号链接。我发现 pwd 为此目的有一个 -P 标志。附加了我的脚本片段。它在 shell 脚本的一个函数中,因此是 $1 和 $2。结果值,即从 START_ABS 到 END_ABS 的相对路径,位于 UPDIRS 变量中。脚本 cd 进入每个参数目录以执行 pwd -P,这也意味着处理相对路径参数。干杯,吉姆
SAVE_DIR="$PWD"
cd "$1"
START_ABS=`pwd -P`
cd "$SAVE_DIR"
cd "$2"
END_ABS=`pwd -P`
START_WORK="$START_ABS"
UPDIRS=""
while test -n "$START_WORK" -a "$END_ABS/#$START_WORK" '==' "$END_ABS";
do
START_WORK=`dirname "$START_WORK"`"/"
UPDIRS=$UPDIRS"../"
done
UPDIRS="$UPDIRS$END_ABS/#$START_WORK"
cd "$SAVE_DIR"
【讨论】:
【参考方案13】:对kasku's 和Pini's 的回答略有改进,可以更好地处理空格并允许传递相对路径:
#!/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
【讨论】:
【参考方案14】:#!/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's(谢谢!)。它触发了一个错误 在 Stack Overflow 的语法高亮模块中(至少在我的预览中) 框架)。所以如果高亮不正确请忽略。
一些注意事项:
在不显着增加代码的情况下删除了错误并改进了代码 长度和复杂性 将功能放入函数中以方便使用 保持函数与 POSIX 兼容,以便它们(应该)与所有 POSIX 兼容 shells(在 Ubuntu Linux 12.04 中使用 dash、bash 和 zsh 测试) 仅使用局部变量来避免破坏全局变量和 污染全局命名空间 两个目录路径都不需要存在(我的应用程序的要求) 路径名可能包含空格、特殊字符、控制字符、 反斜杠、制表符、'、"、?、*、[、] 等。 核心函数“relPath”仅使用 POSIX shell 内置函数,但需要 规范的绝对目录路径作为参数 扩展函数“relpath”可以处理任意目录路径(也 相对的,非规范的),但需要外部 GNU 核心实用程序“readlink” 避免使用内置“echo”并使用内置“printf”,原因有两个: 由于内置“echo”它的历史实现相互冲突 在不同的 shell 中表现不同 -> POSIX recommends that printf is preferred over echo. 某些 POSIX shell 的内置“回显”将interpret some backslash sequences 并因此损坏包含此类序列的路径名 为避免不必要的转换,在返回时使用路径名 并被 shell 和 OS 实用程序(例如 cd、ln、ls、find、mkdir; 与 python 的“os.path.relpath”不同,它将解释一些反斜杠 序列)除了提到的反斜杠序列函数“relPath”的最后一行 输出与 python 兼容的路径名:
path=$up$path#"$common"/; path=$path%/; printf %s "$path:-."
最后一行可以用行替换(和简化)
printf %s "$up$path#"$common"/"
我更喜欢后者,因为
文件名可以直接附加到relPath获取的dir路径中,例如:
ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
使用此方法创建的同一目录中的符号链接没有
丑陋的"./"
前缀在文件名中。
回归测试的代码清单(只需将其附加到 shell 脚本):
############################################################################
# If called with 2 arguments assume they are dir paths and print rel. path #
############################################################################
test "$#" = 2 &&
printf '%s\n' "Rel. path from '$1' to '$2' is '$(relpath "$1" "$2")'."
exit 0
#######################################################
# If NOT called with 2 arguments run regression tests #
#######################################################
format="\t%-19s %-22s %-27s %-8s %-8s %-8s\n"
printf \
"\n\n*** Testing own and python's function with canonical absolute dirs\n\n"
printf "$format\n" \
"From Directory" "To Directory" "Rel. Path" "relPath" "relpath" "python"
IFS=
while read -r p; do
eval set -- $p
case $1 in '#'*|'') continue;; esac # Skip comments and empty lines
# q stores quoting character, use " if ' is used in path name
q="'"; case $1$2 in *"'"*) q='"';; esac
rPOk=passed rP=$(relPath "$1" "$2"); test "$rP" = "$3" || rPOk=$rP
rpOk=passed rp=$(relpath "$1" "$2"); test "$rp" = "$3" || rpOk=$rp
RPOk=passed
RP=$(python -c "import os.path; print os.path.relpath($q$2$q, $q$1$q)")
test "$RP" = "$3" || RPOk=$RP
printf \
"$format" "$q$1$q" "$q$2$q" "$q$3$q" "$q$rPOk$q" "$q$rpOk$q" "$q$RPOk$q"
done <<-"EOF"
# From directory To directory Expected relative path
'/' '/' '.'
'/usr' '/' '..'
'/usr/' '/' '..'
'/' '/usr' 'usr'
'/' '/usr/' 'usr'
'/usr' '/usr' '.'
'/usr/' '/usr' '.'
'/usr' '/usr/' '.'
'/usr/' '/usr/' '.'
'/u' '/usr' '../usr'
'/usr' '/u' '../u'
"/u'/dir" "/u'/dir" "."
"/u'" "/u'/dir" "dir"
"/u'/dir" "/u'" ".."
"/" "/u'/dir" "u'/dir"
"/u'/dir" "/" "../.."
"/u'" "/u'" "."
"/" "/u'" "u'"
"/u'" "/" ".."
'/u"/dir' '/u"/dir' '.'
'/u"' '/u"/dir' 'dir'
'/u"/dir' '/u"' '..'
'/' '/u"/dir' 'u"/dir'
'/u"/dir' '/' '../..'
'/u"' '/u"' '.'
'/' '/u"' 'u"'
'/u"' '/' '..'
'/u /dir' '/u /dir' '.'
'/u ' '/u /dir' 'dir'
'/u /dir' '/u ' '..'
'/' '/u /dir' 'u /dir'
'/u /dir' '/' '../..'
'/u ' '/u ' '.'
'/' '/u ' 'u '
'/u ' '/' '..'
'/u\n/dir' '/u\n/dir' '.'
'/u\n' '/u\n/dir' 'dir'
'/u\n/dir' '/u\n' '..'
'/' '/u\n/dir' 'u\n/dir'
'/u\n/dir' '/' '../..'
'/u\n' '/u\n' '.'
'/' '/u\n' 'u\n'
'/u\n' '/' '..'
'/ a b/å/⮀*/!' '/ a b/å/⮀/xäå/?' '../../⮀/xäå/?'
'/' '/A' 'A'
'/A' '/' '..'
'/ & / !/*/\\/E' '/' '../../../../..'
'/' '/ & / !/*/\\/E' ' & / !/*/\\/E'
'/ & / !/*/\\/E' '/ & / !/?/\\/E/F' '../../../?/\\/E/F'
'/X/Y' '/ & / !/C/\\/E/F' '../../ & / !/C/\\/E/F'
'/ & / !/C' '/A' '../../../A'
'/A / !/C' '/A /B' '../../B'
'/Â/ !/C' '/Â/ !/C' '.'
'/ & /B / C' '/ & /B / C/D' 'D'
'/ & / !/C' '/ & / !/C/\\/Ê' '\\/Ê'
'/Å/ !/C' '/Å/ !/D' '../D'
'/.A /*B/C' '/.A /*B/\\/E' '../\\/E'
'/ & / !/C' '/ & /D' '../../D'
'/ & / !/C' '/ & /\\/E' '../../\\/E'
'/ & / !/C' '/\\/E/F' '../../../\\/E/F'
'/home/p1/p2' '/home/p1/p3' '../p3'
'/home/p1/p2' '/home/p4/p5' '../../p4/p5'
'/home/p1/p2' '/work/p6/p7' '../../../work/p6/p7'
'/home/p1' '/work/p1/p2/p3/p4' '../../work/p1/p2/p3/p4'
'/home' '/work/p2/p3' '../work/p2/p3'
'/' '/work/p2/p3/p4' 'work/p2/p3/p4'
'/home/p1/p2' '/home/p1/p2/p3/p4' 'p3/p4'
'/home/p1/p2' '/home/p1/p2/p3' 'p3'
'/home/p1/p2' '/home/p1/p2' '.'
'/home/p1/p2' '/home/p1' '..'
'/home/p1/p2' '/home' '../..'
'/home/p1/p2' '/' '../../..'
'/home/p1/p2' '/work' '../../../work'
'/home/p1/p2' '/work/p1' '../../../work/p1'
'/home/p1/p2' '/work/p1/p2' '../../../work/p1/p2'
'/home/p1/p2' '/work/p1/p2/p3' '../../../work/p1/p2/p3'
'/home/p1/p2' '/work/p1/p2/p3/p4' '../../../work/p1/p2/p3/p4'
'/-' '/-' '.'
'/?' '/?' '.'
'/??' '/??' '.'
'/???' '/???' '.'
'/?*' '/?*' '.'
'/*' '/*' '.'
'/*' '/**' '../**'
'/*' '/***' '../***'
'/*.*' '/*.**' '../*.**'
'/*.???' '/*.??' '../*.??'
'/[]' '/[]' '.'
'/[a-z]*' '/[0-9]*' '../[0-9]*'
EOF
format="\t%-19s %-22s %-27s %-8s %-8s\n"
printf "\n\n*** Testing own and python's function with arbitrary dirs\n\n"
printf "$format\n" \
"From Directory" "To Directory" "Rel. Path" "relpath" "python"
IFS=
while read -r p; do
eval set -- $p
case $1 in '#'*|'') continue;; esac # Skip comments and empty lines
# q stores quoting character, use " if ' is used in path name
q="'"; case $1$2 in *"'"*) q='"';; esac
rpOk=passed rp=$(relpath "$1" "$2"); test "$rp" = "$3" || rpOk=$rp
RPOk=passed
RP=$(python -c "import os.path; print os.path.relpath($q$2$q, $q$1$q)")
test "$RP" = "$3" || RPOk=$RP
printf "$format" "$q$1$q" "$q$2$q" "$q$3$q" "$q$rpOk$q" "$q$RPOk$q"
done <<-"EOF"
# From directory To directory Expected relative path
'usr/p1/..//./p4' 'p3/../p1/p6/.././/p2' '../../p1/p2'
'./home/../../work' '..//././../dir///' '../../dir'
'home/p1/p2' 'home/p1/p3' '../p3'
'home/p1/p2' 'home/p4/p5' '../../p4/p5'
'home/p1/p2' 'work/p6/p7' '../../../work/p6/p7'
'home/p1' 'work/p1/p2/p3/p4' '../../work/p1/p2/p3/p4'
'home' 'work/p2/p3' '../work/p2/p3'
'.' 'work/p2/p3' 'work/p2/p3'
'home/p1/p2' 'home/p1/p2/p3/p4' 'p3/p4'
'home/p1/p2' 'home/p1/p2/p3' 'p3'
'home/p1/p2' 'home/p1/p2' '.'
'home/p1/p2' 'home/p1' '..'
'home/p1/p2' 'home' '../..'
'home/p1/p2' '.' '../../..'
'home/p1/p2' 'work' '../../../work'
'home/p1/p2' 'work/p1' '../../../work/p1'
'home/p1/p2' 'work/p1/p2' '../../../work/p1/p2'
'home/p1/p2' 'work/p1/p2/p3' '../../../work/p1/p2/p3'
'home/p1/p2' 'work/p1/p2/p3/p4' '../../../work/p1/p2/p3/p4'
EOF
【讨论】:
【参考方案15】:Python 的 os.path.relpath
作为 shell 函数
这个relpath
练习的目标是模仿xni 提出的Python 2.7 的os.path.relpath
函数(可从Python 2.6 版获得,但只能在2.7 中正常工作)。因此,某些结果可能与其他答案中提供的函数不同。
(我没有在路径中使用换行符进行测试,仅仅是因为它破坏了基于从 ZSH 调用 python -c
的验证。通过一些努力当然可以。)
关于 Bash 中的“魔法”,我很久以前就放弃了在 Bash 中寻找魔法,但后来我在 ZSH 中找到了我需要的所有魔法,然后还有一些。
因此,我提出了两种实现方式。
第一个实现旨在完全POSIX 兼容。我已经在 Debian 6.0.6 “Squeeze” 上使用 /bin/dash
对其进行了测试。它还可以与 OS X 10.8.3 上的 /bin/sh
完美配合,这实际上是伪装成 POSIX shell 的 Bash 版本 3.2。
第二个实现是一个 ZSH shell 函数,它对路径中的多个斜杠和其他麻烦具有鲁棒性。如果您有可用的 ZSH,这是推荐的版本,即使您是从另一个 shell 以下面提供的脚本形式(即使用 #!/usr/bin/env zsh
的 shebang)调用它。
最后,我编写了一个 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 shell 函数
现在,更强大的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='%Fgreen'
if [[ $result != $py_result ]] && col='%Fred' || $VERBOSE; then
print -P "$colSource: '$prefix'\nDestination: '$target'%f"
print -P "$colrelpath: $(qq)result%f"
print -P "$colpython: $(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
【讨论】:
恐怕第一条路径以/
结尾时不起作用。【参考方案16】:
我只会使用 Perl 来完成这个不那么简单的任务:
absolute="/foo/bar"
current="/foo/baz/foo"
# Perl is magic
relative=$(perl -MFile::Spec -e 'print File::Spec->abs2rel("'$absolute'","'$current'")')
【讨论】:
+1,但建议:perl -MFile::Spec -e "print File::Spec->abs2rel('$absolute','$current')"
以便引用绝对值和当前值。
我喜欢relative=$(perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$absolute" "$current")
。这确保了值本身不能包含 perl 代码!【参考方案17】:
猜猜这个也可以解决问题...(带有内置测试):)
好的,预计会有一些开销,但我们在这里使用 Bourne shell! ;)
#!/bin/sh
#
# Finding the relative path to a certain file ($2), given the absolute path ($1)
# (available here too http://pastebin.com/tWWqA8aB)
#
relpath ()
local FROM="$1"
local TO="`dirname $2`"
local FILE="`basename $2`"
local DEBUG="$3"
local FROMREL=""
local FROMUP="$FROM"
while [ "$FROMUP" != "/" ]; do
local TOUP="$TO"
local TOREL=""
while [ "$TOUP" != "/" ]; do
[ -z "$DEBUG" ] || echo 1>&2 "$DEBUG$FROMUP =?= $TOUP"
if [ "$FROMUP" = "$TOUP" ]; then
echo "$FROMREL:-./$TOREL$TOREL:+/$FILE"
return 0
fi
TOREL="`basename $TOUP`$TOREL:+/$TOREL"
TOUP="`dirname $TOUP`"
done
FROMREL="..$FROMREL:+/$FROMREL"
FROMUP="`dirname $FROMUP`"
done
echo "$FROMREL:-.$TOREL:+/$TOREL/$FILE"
return 0
relpathshow ()
echo " - target $2"
echo " from $1"
echo " ------"
echo " => `relpath $1 $2 ' '`"
echo ""
# If given 2 arguments, do as said...
if [ -n "$2" ]; then
relpath $1 $2
# If only one given, then assume current directory
elif [ -n "$1" ]; then
relpath `pwd` $1
# Otherwise perform a set of built-in tests to confirm the validity of the method! ;)
else
relpathshow /usr/share/emacs22/site-lisp/emacs-goodies-el \
/usr/share/emacs22/site-lisp/emacs-goodies-el/filladapt.el
relpathshow /usr/share/emacs23/site-lisp/emacs-goodies-el \
/usr/share/emacs22/site-lisp/emacs-goodies-el/filladapt.el
relpathshow /usr/bin \
/usr/share/emacs22/site-lisp/emacs-goodies-el/filladapt.el
relpathshow /usr/bin \
/usr/share/emacs22/site-lisp/emacs-goodies-el/filladapt.el
relpathshow /usr/bin/share/emacs22/site-lisp/emacs-goodies-el \
/etc/motd
relpathshow / \
/initrd.img
fi
【讨论】:
【参考方案18】:我的解决方案:
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
【讨论】:
这是非常便携的:)【参考方案19】:这是一个无需调用其他程序即可执行此操作的 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
【讨论】:
由于使用了 $param/pattern/subst 构造,这不是真正可移植的,它不是 POSIX(截至 2011 年)。 引用的源 ynform.org/w/Pub/Relpath 指向一个完全乱码的 wiki 页面,其中多次包含脚本内容,其中夹杂着 vi 波浪线、有关未找到命令的错误消息等等。对于研究原著的人来说完全没用。【参考方案20】:我把你的问题当作一个挑战,用“便携式”shell代码编写这个,即
考虑到 POSIX shell 没有诸如数组之类的 bashism 避免像瘟疫一样调用外部。脚本中没有一个分叉!这使得它的速度非常快,尤其是在具有大量分叉开销的系统上,例如 cygwin。 必须处理路径名中的全局字符(*、?、[、])它可以在任何符合 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 :
【讨论】:
【参考方案21】:此脚本仅对绝对路径或没有.
或..
的相对路径的输入给出正确结果:
#!/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"
【讨论】:
这似乎有效。如果目录确实存在,则在输入上使用 $(readlink -f $1) 和 $(readlink -f $2) 可以解决“。”的问题。或“..”出现在输入中。如果目录实际上不存在,这可能会导致一些麻烦。【参考方案22】:#!/bin/bash
# both $1 and $2 are absolute paths
# returns $2 relative to $1
source=$1
target=$2
common_part=$source
back=
while [ "$target#$common_part" = "$target" ]; do
common_part=$(dirname $common_part)
back="../$back"
done
echo $back$target#$common_part/
【讨论】:
精彩的脚本——短而干净。我应用了一个编辑(等待同行评审):common_part=$source/common_part=$(dirname $common_part)/echo $back$target#$common_part 由于目录名称开头的不适当匹配,现有脚本将失败比较时,例如:“/foo/bar/baz”与“/foo/barsucks/bonk”。将斜杠移入 var 并移出最终 eval 可纠正该错误。 这个脚本根本不起作用。一个简单的“一个目录下”测试失败。 jcwenger 的编辑效果更好一些,但往往会添加一个额外的“../”。 如果参数上有尾随的“/”,在某些情况下对我来说会失败;例如,如果 $1="$HOME/" 和 $2="$HOME/temp",则返回“/home/user/temp/”,但如果 $1=$HOME,则正确返回相对路径“temp”。因此 source=$1 和 target=$2 都可以使用 sed (或使用 bash 变量替换,但这可能是不必要的不透明)“清理”,例如 => source=$(echo "$1" | sed 's/ \/*$//') 小改进:不要直接将源/目标设置为 $1 和 $2,而是执行:source=$(cd $1; pwd) target=$(cd $2; pwd)。这样它处理带有 .和..正确。 尽管是投票最多的答案,但这个答案有很多限制,因此发布了许多其他答案。请参阅其他答案,尤其是显示测试用例的答案。请为这条评论点赞!【参考方案23】:test.sh:
#!/bin/bash
cd /home/ubuntu
touch blah
TEST=/home/ubuntu/.//blah
echo TEST=$TEST
TMP=$(readlink -e "$TEST")
echo TMP=$TMP
REL=$TMP#$(pwd)/
echo REL=$REL
测试:
$ ./test.sh
TEST=/home/ubuntu/.//blah
TMP=/home/ubuntu/blah
REL=blah
【讨论】:
+1 表示紧凑和 bash-ness。但是,您也应该在$(pwd)
上致电readlink
。
相对并不意味着文件必须放在同一目录中。
虽然最初的问题没有提供很多测试用例,但是这个脚本对于简单的测试失败了,比如找到从 /home/user1 到 /home/user2 的相对路径(正确答案:../user2 )。 pini/jcwenger 的脚本适用于这种情况。【参考方案24】:
遗憾的是,Mark Rushakoff 的答案(现已删除 - 它引用了来自 here 的代码)在适应以下情况时似乎无法正常工作:
source=/home/part2/part3/part4
target=/work/proj1/proj2
评论中概述的想法可以改进,使其在大多数情况下都能正常工作。我将假设脚本接受一个源参数(你在哪里)和一个目标参数(你想去哪里),并且要么都是绝对路径名,要么都是相对的。如果一个是绝对的,另一个是相对的,最简单的方法是在相对名称前面加上当前工作目录 - 但下面的代码没有这样做。
小心
下面的代码接近于正常工作,但并不完全正确。
-
Dennis Williamson 的 cmets 解决了这个问题。
还有一个问题是,这种纯粹的路径名文本处理可能会被奇怪的符号链接严重搞砸。
代码不处理路径中的杂散“点”,例如“
xyz/./pqr
”。
代码不处理路径中的杂散“双点”,如“xyz/../pqr
”。
简单地说:代码不会从路径中删除前导“./
”。
Dennis 的代码更好,因为它修复了 1 和 5 - 但有相同的问题 2、3、4。 因此,请使用丹尼斯的代码(并在此之前对其进行投票)。
(注意:POSIX 提供了一个系统调用realpath()
来解析路径名,以便其中没有符号链接。将其应用于输入名称,然后使用丹尼斯的代码每次都会给出正确的答案。这很简单编写包装 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://***.com/questions/2564634
use strict;
die "Usage: $0 from to\n" if scalar @ARGV != 2;
use Cwd qw(realpath getcwd);
my $pwd;
my $verbose = 0;
# Fettle filename so it is absolute.
# Deals with '//', '/./' and '/../' notations, plus symlinks.
# The realpath() function does the hard work if the path exists.
# For non-existent paths, the code does a purely textual hack.
sub resolve
my($name) = @_;
my($path) = realpath($name);
if (!defined $path)
# Path does not exist - do the best we can with lexical analysis
# Assume Unix - not dealing with Windows.
$path = $name;
if ($name !~ m%^/%)
$pwd = getcwd if !defined $pwd;
$path = "$pwd/$path";
$path =~ s%//+%/%g; # Not UNC paths.
$path =~ s%/$%%; # No trailing /
$path =~ s%/\./%/%g; # No embedded /./
# Try to eliminate /../abc/
$path =~ s%/\.\./(?:[^/]+)(/|$)%$1%g;
$path =~ s%/\.$%%; # No trailing /.
$path =~ s%^\./%%; # No leading ./
# What happens with . and / as inputs?
return($path);
sub print_result
my($source, $target, $relpath) = @_;
if ($verbose)
print "source = $ARGV[0]\n";
print "target = $ARGV[1]\n";
print "relpath = $relpath\n";
else
print "$relpath\n";
exit 0;
my($source) = resolve($ARGV[0]);
my($target) = resolve($ARGV[1]);
print_result($source, $target, ".") if ($source eq $target);
# Split!
my(@source) = split '/', $source;
my(@target) = split '/', $target;
my $count = scalar(@source);
$count = scalar(@target) if (scalar(@target) < $count);
my $relpath = "";
my $i;
# Both paths are absolute; Perl splits an empty field 0.
for ($i = 1; $i < $count; $i++)
last if $source[$i] ne $target[$i];
for (my $s = $i; $s < scalar(@source); $s++)
$relpath = "$relpath/" if ($s > $i);
$relpath = "$relpath..";
for (my $t = $i; $t < scalar(@target); $t++)
$relpath = "$relpath/" if ($relpath ne "");
$relpath = "$relpath$target[$t]";
print_result($source, $target, $relpath);
【讨论】:
您的/home/part1/part2
到/
有一个太多../
。否则,我的脚本与您的输出相匹配,除了我的脚本在目标为 .
的末尾添加了一个不必要的 .
并且我不在下降而不上升的开头使用 ./
。
@Dennis:我花了很多时间对结果进行了斗鸡眼——有时我可以看到这个问题,有时我又找不到它。删除前导 './' 是另一个微不足道的步骤。您对“没有嵌入”的评论。或 ..' 也是相关的。正确地完成这项工作实际上非常困难 - 如果任何名称实际上是符号链接,则更是如此;我们都在做纯文本分析。
@Dennis:当然,除非你有 Newcastle Connection 网络,否则试图超越 root 是徒劳的,所以 ../../../.. 和 ../../..是等价的。然而,那是纯粹的逃避现实;你的批评是正确的。 (Newcastle Connection 允许您配置和使用符号 /../host/path/on/remote/machine 来访问不同的主机 - 一个简洁的方案。我相信它支持 /../../network/host/ path/on/remote/network/and/host 也是。它在***上。)
因此,我们现在有了 UNC 的双斜线。
“readlink”实用程序(至少是 GNU 版本)可以执行与 realpath() 等效的操作,如果您将“-f”选项传递给它。例如,在我的系统上,readlink /usr/bin/vi
提供 /etc/alternatives/vi
,但这是另一个符号链接 - 而 readlink -f /usr/bin/vi
提供 /usr/bin/vim.basic
,这是所有符号链接的最终目的地...以上是关于使用 Bash 将给定当前目录的绝对路径转换为相对路径的主要内容,如果未能解决你的问题,请参考以下文章