Thí dụ:
absolute="/foo/bar"
current="/foo/baz/foo"
# Magic
relative="../../bar"
Làm cách nào để tạo ra phép thuật (hy vọng mã không quá phức tạp ...)?
realpath --relative-to=$absolute $current
.
Thí dụ:
absolute="/foo/bar"
current="/foo/baz/foo"
# Magic
relative="../../bar"
Làm cách nào để tạo ra phép thuật (hy vọng mã không quá phức tạp ...)?
realpath --relative-to=$absolute $current
.
Câu trả lời:
Sử dụng realpath từ GNU coreutils 8.23 là đơn giản nhất, tôi nghĩ:
$ realpath --relative-to="$file1" "$file2"
Ví dụ:
$ realpath --relative-to=/usr/bin/nmap /tmp/testing
../../../tmp/testing
$ realpath --relative-to="${PWD}" "$file"
là hữu ích nếu bạn muốn các đường dẫn liên quan đến thư mục làm việc hiện tại.
/usr/bin/nmap/
-path nhưng không phải cho /usr/bin/nmap
: từ nmap
đến /tmp/testing
nó chỉ ../../
và không 3 lần ../
. Nó hoạt động tuy nhiên, bởi vì làm ..
trên rootfs là /
.
--relative-to=…
mong đợi một thư mục và KHÔNG kiểm tra. Điều đó có nghĩa rằng bạn kết thúc với một thêm "../" nếu bạn yêu cầu một đường dẫn tương đối đến một tập tin (như ví dụ này dường như làm, bởi vì /usr/bin
hiếm khi hoặc không bao giờ chứa các thư mục và nmap
thường là một nhị phân)
$ python -c "import os.path; print os.path.relpath('/foo/bar', '/foo/baz/foo')"
cho:
../../bar
relpath(){ python -c "import os.path; print os.path.relpath('$1','${2:-$PWD}')" ; }
python -c 'import os, sys; print(os.path.relpath(*sys.argv[1:]))'
hoạt động tự nhiên và đáng tin cậy nhất.
Đây là một cải tiến đầy đủ chức năng của giải pháp được đánh giá tốt nhất hiện tại từ @pini (đáng buồn chỉ xử lý một vài trường hợp)
Nhắc nhở: '-z' kiểm tra nếu chuỗi có độ dài bằng không (= trống) và kiểm tra '-n' nếu chuỗi không trống.
# 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
Các trường hợp thử nghiệm:
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"
source=$1; target=$2
bằngsource=$(realpath $1); target=$(realpath $2)
realpath
được khuyến nghị, hoặc source=$(readlink -f $1)
vv nếu không có sẵn (không chuẩn)
$source
và $target
như thế này: `if [[-e $ 1]]; sau đó nguồn = $ (readlink -f $ 1); nguồn khác = $ 1; fi nếu [[-e $ 2]]; sau đó đích = $ (readlink -f $ 2); mục tiêu khác = $ 2; Theo cách đó, hàm có thể làm tăng các đường dẫn thực / tồn tại cũng như các thư mục hư cấu.
readlink
có một -m
tùy chọn chỉ thực hiện điều đó;)
#!/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/}
Nó được tích hợp vào Perl từ năm 2001, vì vậy nó hoạt động trên hầu hết mọi hệ thống mà bạn có thể tưởng tượng, thậm chí là VMS .
perl -e 'use File::Spec; print File::Spec->abs2rel(@ARGV) . "\n"' FILE BASE
Ngoài ra, giải pháp rất dễ hiểu.
Vì vậy, ví dụ của bạn:
perl -e 'use File::Spec; print File::Spec->abs2rel(@ARGV) . "\n"' $absolute $current
... sẽ hoạt động tốt.
say
đã không có sẵn trong perl cho dưới dạng nhật ký, nhưng nó có thể được sử dụng hiệu quả ở đây. perl -MFile::Spec -E 'say File::Spec->abs2rel(@ARGV)'
perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target"
và perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target" "$origin"
. Tập lệnh perl một dòng đầu tiên sử dụng một đối số (gốc là thư mục làm việc hiện tại). Kịch bản perl một dòng thứ hai sử dụng hai đối số.
perl
có thể được tìm thấy ở hầu hết mọi nơi, mặc dù câu trả lời vẫn là một lớp lót.
Giả sử bạn đã cài đặt: bash, pwd, dirname, echo; sau đó relpath là
#!/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/}
Tôi đã đánh golf câu trả lời từ pini và một vài ý tưởng khác
Lưu ý : Điều này đòi hỏi cả hai đường dẫn là các thư mục hiện có. Các tập tin sẽ không hoạt động.
os.path.relpath
là một hàm shellMục tiêu của relpath
bài tập này là bắt chước os.path.relpath
chức năng của Python 2.7 (có sẵn từ phiên bản Python 2.6 nhưng chỉ hoạt động đúng trong 2.7), như đề xuất của xni . Do đó, một số kết quả có thể khác với các chức năng được cung cấp trong các câu trả lời khác.
(Tôi chưa thử nghiệm các dòng mới trong các đường dẫn đơn giản vì nó phá vỡ xác nhận dựa trên cuộc gọi python -c
từ ZSH. Chắc chắn sẽ có thể với một số nỗ lực.)
Về ma thuật của người Hồi giáo ở Bash, tôi đã từ bỏ việc tìm kiếm ma thuật ở Bash từ lâu, nhưng tôi đã tìm thấy tất cả ma thuật tôi cần, và sau đó là một số, trong ZSH.
Do đó, tôi đề xuất hai triển khai.
Việc thực hiện đầu tiên nhằm hoàn toàn tuân thủ POSIX . Tôi đã thử nghiệm nó với /bin/dash
Debian 6.0.6. Nó cũng hoạt động hoàn hảo với /bin/sh
trên OS X 10.8.3, thực tế là phiên bản Bash 3.2 giả vờ là vỏ POSIX.
Việc thực hiện thứ hai là một hàm shell ZSH mạnh mẽ chống lại nhiều dấu gạch chéo và các phiền toái khác trong các đường dẫn. Nếu bạn có sẵn ZSH, đây là phiên bản được đề xuất, ngay cả khi bạn đang gọi nó ở dạng tập lệnh được trình bày bên dưới (nghĩa là với một shebang #!/usr/bin/env zsh
) từ một shell khác.
Cuối cùng, tôi đã viết một tập lệnh ZSH để xác minh đầu ra của relpath
lệnh được tìm thấy trong $PATH
các trường hợp thử nghiệm được cung cấp trong các câu trả lời khác. Tôi đã thêm một số gia vị vào các thử nghiệm đó bằng cách thêm một số khoảng trắng, tab và dấu câu như ! ? *
ở đây và ở đó và cũng đã ném vào một thử nghiệm khác với các ký tự UTF-8 kỳ lạ được tìm thấy trong vim-powerline .
Đầu tiên, chức năng shell tuân thủ POSIX. Nó hoạt động với nhiều đường dẫn khác nhau, nhưng không làm sạch nhiều dấu gạch chéo hoặc giải quyết các liên kết tượng trưng.
#!/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 "$@"
Bây giờ, zsh
phiên bản mạnh mẽ hơn . Nếu bạn muốn nó giải quyết các đối số thành các đường dẫn thực sự realpath -f
(có sẵn trong coreutils
gói Linux ), hãy thay thế các :a
dòng 3 và 4 bằng :A
.
Để sử dụng điều này trong zsh, loại bỏ dòng đầu tiên và cuối cùng và đặt nó trong một thư mục nằm trong $FPATH
biến của bạn .
#!/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 "$@"
Cuối cùng là kịch bản thử nghiệm. Nó chấp nhận một tùy chọn, cụ thể là -v
để cho phép đầu ra dài dòng.
#!/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
/
tôi sợ.
#!/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")"; }
Kịch bản shell trên được lấy cảm hứng từ pini (Cảm ơn!). Nó gây ra lỗi trong mô-đun tô sáng cú pháp của Stack Overflow (ít nhất là trong khung xem trước của tôi). Vì vậy, xin vui lòng bỏ qua nếu đánh dấu là không chính xác.
Một số lưu ý:
Ngoại trừ các chuỗi dấu gạch chéo ngược được đề cập, dòng cuối cùng của hàm "relPath" xuất ra các tên đường dẫn tương thích với python:
path=$up${path#"$common"/}; path=${path%/}; printf %s "${path:-.}"
Dòng cuối cùng có thể được thay thế (và đơn giản hóa) bằng dòng
printf %s "$up${path#"$common"/}"
Tôi thích cái sau bởi vì
Tên tệp có thể được thêm trực tiếp vào đường dẫn dir thu được bởi relPath, ví dụ:
ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
Các liên kết tượng trưng trong cùng một thư mục được tạo bằng phương thức này không có tên xấu được "./"
thêm vào tên tệp.
Danh sách mã cho các bài kiểm tra hồi quy (chỉ cần thêm nó vào tập lệnh 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
Không có nhiều câu trả lời ở đây là thiết thực cho việc sử dụng hàng ngày. Vì rất khó để thực hiện điều này đúng cách trong bash thuần túy, tôi đề xuất giải pháp đáng tin cậy sau đây (tương tự như một đề xuất được chôn trong một nhận xét):
function relpath() {
python -c "import os,sys;print(os.path.relpath(*(sys.argv[1:])))" "$@";
}
Sau đó, bạn có thể nhận được đường dẫn tương đối dựa trên thư mục hiện tại:
echo $(relpath somepath)
hoặc bạn có thể chỉ định rằng đường dẫn có liên quan đến một thư mục đã cho:
echo $(relpath somepath /etc) # relative to /etc
Một nhược điểm là điều này đòi hỏi python, nhưng:
Lưu ý rằng các giải pháp bao gồm basename
hoặc dirname
có thể không nhất thiết phải tốt hơn, vì chúng yêu cầu phải coreutils
được cài đặt. Nếu ai đó có một bash
giải pháp thuần túy đáng tin cậy và đơn giản (thay vì tò mò phức tạp), tôi sẽ ngạc nhiên.
Tập lệnh này chỉ cung cấp kết quả chính xác cho các đầu vào là đường dẫn tuyệt đối hoặc đường dẫn tương đối không có .
hoặc ..
:
#!/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"
Tôi sẽ chỉ sử dụng Perl cho nhiệm vụ không hề nhỏ này:
absolute="/foo/bar"
current="/foo/baz/foo"
# Perl is magic
relative=$(perl -MFile::Spec -e 'print File::Spec->abs2rel("'$absolute'","'$current'")')
perl -MFile::Spec -e "print File::Spec->abs2rel('$absolute','$current')"
sao cho tuyệt đối và hiện tại được trích dẫn.
relative=$(perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$absolute" "$current")
. Điều này đảm bảo các giá trị không thể, chính chúng, chứa mã perl!
Một cải tiến nhỏ về câu trả lời của kasku và Pini , chơi đẹp hơn với không gian và cho phép vượt qua các đường dẫn tương đối:
#!/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
kiểm tra
#!/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
Kiểm tra:
$ ./test.sh
TEST=/home/ubuntu/.//blah
TMP=/home/ubuntu/blah
REL=blah
readlink
vào $(pwd)
.
Một giải pháp khác, thuần bash
+ GNU readlink
để dễ sử dụng trong ngữ cảnh sau:
ln -s "$(relpath "$A" "$B")" "$B"
Chỉnh sửa: Đảm bảo rằng "$ B" không tồn tại hoặc không có
relpath
liên kết mềm trong trường hợp đó, nếu không thì theo liên kết này không phải là điều bạn muốn!
Điều này hoạt động trong gần như tất cả các Linux hiện tại. Nếu readlink -m
không làm việc ở bên cạnh bạn, hãy thử readlink -f
thay thế. Xem thêm https://gist.github.com/hilbix/1ec361d00a8178ae8ea0 để biết các cập nhật có thể:
: 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:-.}"
}
Ghi chú:
*
hoặc ?
.ln -s
:
relpath / /
cho .
và không phải chuỗi rỗngrelpath a a
cho a
, ngay cả khi a
tình cờ là một thư mụcreadlink
được yêu cầu để chuẩn hóa các đường dẫn.readlink -m
nó hoạt động cho các đường dẫn chưa có, quá.Trên các hệ thống cũ, nơi readlink -m
không có sẵn, readlink -f
sẽ thất bại nếu tệp không tồn tại. Vì vậy, bạn có thể cần một số cách giải quyết như thế này (chưa được kiểm tra!):
readlink_missing()
{
readlink -m -- "$1" && return
readlink -f -- "$1" && return
[ -e . ] && echo "$(readlink_missing "$(dirname "$1")")/$(basename "$1")"
}
Điều này không thực sự hoàn toàn chính xác trong trường hợp $1
bao gồm .
hoặc ..
cho các đường dẫn chưa có (như trong /doesnotexist/./a
), nhưng nó sẽ bao gồm hầu hết các trường hợp.
(Thay thế readlink -m --
ở trên bằngreadlink_missing
.)
Đây là một bài kiểm tra, rằng chức năng này, thực sự, là chính xác:
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"
Bối rối? Vâng, đây là những kết quả chính xác ! Ngay cả khi bạn nghĩ rằng nó không phù hợp với câu hỏi, đây là bằng chứng điều này là chính xác:
check "http://example.com/foo/baz/moo" "http://example.com/foo/bar" "../bar"
Không còn nghi ngờ gì nữa, ../bar
là đường dẫn tương đối chính xác và duy nhất của trang bar
nhìn từ trangmoo
. Mọi thứ khác sẽ hoàn toàn sai.
Thật là tầm thường khi chấp nhận đầu ra cho câu hỏi mà dường như giả định, đó current
là một thư mục:
absolute="/foo/bar"
current="/foo/baz/foo"
relative="../$(relpath "$absolute" "$current")"
Điều này trả về chính xác, những gì đã được yêu cầu.
Và trước khi bạn nhướn mày, đây là một biến thể phức tạp hơn một chút relpath
(phát hiện ra sự khác biệt nhỏ), cũng sẽ hoạt động với URL - Cú pháp (vì vậy, một dấu vết /
còn tồn tại, nhờ một số bash
-magic):
# Calculate relative PATH to the given DEST from the given BASE
# In the URL case, both URLs must be absolute and have the same Scheme.
# The `SCHEME:` must not be present in the FS either.
# This way this routine works for file paths an
: relpathurl DEST BASE
relpathurl()
{
local X Y A
# We can create dangling softlinks
X="$(readlink -m -- "$1")" || return
Y="$(readlink -m -- "$2")" || return
X="${X%/}/${1#"${1%/}"}"
Y="${Y%/}${2#"${2%/}"}"
A=""
while Y="${Y%/*}"
[ ".${X#"$Y"/}" = ".$X" ]
do
A="../$A"
done
X="$A${X#"$Y"/}"
X="${X%/}"
echo "${X:-.}"
}
Và đây là những kiểm tra chỉ để làm rõ: Nó thực sự hoạt động như đã nói.
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/"
Và đây là cách nó có thể được sử dụng để đưa ra kết quả mong muốn từ câu hỏi:
absolute="/foo/bar"
current="/foo/baz/foo"
relative="$(relpathurl "$absolute" "$current/")"
echo "$relative"
Nếu bạn tìm thấy một cái gì đó không hoạt động, xin vui lòng cho tôi biết trong các ý kiến dưới đây. Cảm ơn.
Tái bút
Tại sao các đối số của relpath
"đảo ngược" trái ngược với tất cả các câu trả lời khác ở đây?
Nếu bạn thay đổi
Y="$(readlink -m -- "$2")" || return
đến
Y="$(readlink -m -- "${2:-"$PWD"}")" || return
sau đó bạn có thể bỏ tham số thứ 2 đi, sao cho BASE là thư mục hiện tại / URL / bất cứ thứ gì. Đó chỉ là nguyên tắc Unix, như thường lệ.
Nếu bạn không thích điều đó, vui lòng quay lại Windows. Cảm ơn.
Đáng buồn thay, câu trả lời của Mark Rushakoff (hiện đã bị xóa - nó đã tham chiếu mã từ đây ) dường như không hoạt động chính xác khi thích nghi với:
source=/home/part2/part3/part4
target=/work/proj1/proj2
Suy nghĩ được nêu trong các bài bình luận có thể được tinh chỉnh để làm cho nó hoạt động chính xác cho hầu hết các trường hợp. Tôi sắp giả định rằng tập lệnh lấy một đối số nguồn (nơi bạn đang ở) và đối số đích (nơi bạn muốn đến) và cả hai đều là tên đường dẫn tuyệt đối hoặc cả hai đều là tương đối. Nếu một là tuyệt đối và tương đối khác, điều dễ nhất là tiền tố tên tương đối với thư mục làm việc hiện tại - nhưng mã dưới đây không làm điều đó.
Mã dưới đây gần hoạt động chính xác, nhưng không hoàn toàn đúng.
xyz/./pqr
'.xyz/../pqr
'../
' khỏi các đường dẫn.Mã của Dennis tốt hơn vì nó sửa được 1 và 5 - nhưng có cùng các vấn đề 2, 3, 4. Sử dụng mã của Dennis (và bỏ phiếu trước mã này) vì điều đó.
(NB: POSIX cung cấp một cuộc gọi hệ thống realpath()
giải quyết các tên đường dẫn để không còn các liên kết tượng trưng nào trong chúng. Áp dụng điều đó cho các tên đầu vào, và sau đó sử dụng mã của Dennis sẽ đưa ra câu trả lời chính xác mỗi lần. kết thúc tốt đẹp realpath()
- tôi đã thực hiện nó - nhưng tôi không biết về một tiện ích tiêu chuẩn làm như vậy.)
Đối với điều này, tôi thấy Perl dễ sử dụng hơn shell, mặc dù bash có hỗ trợ tốt cho mảng và có lẽ cũng có thể làm điều này - tập thể dục cho người đọc. Vì vậy, đưa ra hai tên tương thích, chia chúng thành các thành phần:
Như vậy:
#!/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";
Tập lệnh kiểm tra (dấu ngoặc vuông chứa một khoảng trống và một 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
Đầu ra từ tập lệnh thử nghiệm:
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
Kịch bản Perl này hoạt động khá kỹ lưỡng trên Unix (nó không tính đến tất cả sự phức tạp của tên đường dẫn Windows) khi đối mặt với các đầu vào kỳ lạ. Nó sử dụng mô-đun Cwd
và chức năng của nó realpath
để giải quyết đường dẫn thực sự của các tên tồn tại và thực hiện phân tích văn bản cho các đường dẫn không tồn tại. Trong mọi trường hợp ngoại trừ một, nó tạo ra cùng một đầu ra như tập lệnh của Dennis. Trường hợp lệch lạc là:
source = home/part1/part2
target = .
relpath1 = ../../..
relpath2 = ../../../.
Hai kết quả tương đương - chỉ không giống nhau. (Đầu ra là từ một phiên bản sửa đổi nhẹ của tập lệnh thử nghiệm - tập lệnh Perl bên dưới chỉ đơn giản là in câu trả lời, thay vì đầu vào và câu trả lời như trong tập lệnh ở trên.) Bây giờ: tôi có nên loại bỏ câu trả lời không hoạt động không? Có lẽ...
#!/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);
/home/part1/part2
để /
có một quá nhiều ../
. Mặt khác, tập lệnh của tôi khớp với đầu ra của bạn ngoại trừ phần bổ sung của tôi không cần thiết .
ở phần cuối của điểm đến .
và tôi không sử dụng ./
phần đầu của phần xuất phát mà không đi lên.
readlink /usr/bin/vi
cho /etc/alternatives/vi
, nhưng đó là liên kết tượng trưng khác - trong khi readlink -f /usr/bin/vi
cho phép /usr/bin/vim.basic
, đó là đích đến cuối cùng của tất cả các liên kết tượng trưng ...
Tôi lấy câu hỏi của bạn làm thử thách để viết mã này trong mã shell "xách tay", tức là
Nó chạy trên mọi vỏ phù hợp POSIX (zsh, bash, ksh, ash, busybox, ...). Nó thậm chí có chứa một testsuite để xác minh hoạt động của nó. Canonicalization của tên đường được để lại như một bài tập. :-)
#!/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 :
Giải pháp của tôi:
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
}
Đây là phiên bản của tôi. Nó dựa trên câu trả lời của @ Offerirmo . Tôi đã làm cho nó tương thích với Dash và sửa lỗi testcase sau:
./compute-relative.sh "/a/b/c/de/f/g" "/a/b/c/def/g/"
-> "../..f/g/"
Hiện nay:
CT_FindRelativePath "/a/b/c/de/f/g" "/a/b/c/def/g/"
-> "../../../def/g/"
Xem mã:
# 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
}
Đoán cái này cũng sẽ làm được điều đó ... (đi kèm với các bài kiểm tra tích hợp) :)
OK, một số chi phí dự kiến, nhưng chúng tôi đang thực hiện vỏ Bourne ở đây! ;)
#!/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
Kịch bản này chỉ hoạt động trên tên đường dẫn. Nó không yêu cầu bất kỳ tập tin nào tồn tại. Nếu các đường dẫn đi qua không tuyệt đối, hành vi hơi bất thường, nhưng nó sẽ hoạt động như mong đợi nếu cả hai đường dẫn đều tương đối.
Tôi chỉ thử nghiệm nó trên OS X, vì vậy nó có thể không di động được.
#!/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
Câu trả lời này không giải quyết phần Bash của câu hỏi, nhưng vì tôi đã cố gắng sử dụng các câu trả lời trong câu hỏi này để thực hiện chức năng này trong Emacs nên tôi sẽ ném nó ra khỏi đó.
Emacs thực sự có một chức năng cho điều này ra khỏi hộp:
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"
relpath
chức năng) hoạt động giống hệt file-relative-name
với các trường hợp thử nghiệm mà bạn đã cung cấp.
Đây là một kịch bản shell thực hiện nó mà không cần gọi các chương trình khác:
#! /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
Tôi cần một cái gì đó như thế này nhưng cũng giải quyết các liên kết tượng trưng. Tôi phát hiện ra rằng pwd có cờ -P cho mục đích đó. Một đoạn trong kịch bản của tôi được nối thêm. Nó nằm trong một hàm trong tập lệnh shell, do đó là $ 1 và $ 2. Giá trị kết quả, là đường dẫn tương đối từ START_ABS đến END_ABS, nằm trong biến UPDIRS. Tập lệnh cd vào từng thư mục tham số để thực thi pwd -P và điều này cũng có nghĩa là các tham số đường dẫn tương đối được xử lý. Chúc mừng, 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"