ตัวอย่าง:
absolute="/foo/bar"
current="/foo/baz/foo"
# Magic
relative="../../bar"
ฉันจะสร้างเวทย์มนตร์ได้อย่างไร (หวังว่ารหัสไม่ซับซ้อนเกินไป ... )
realpath --relative-to=$absolute $current
ง่าย:
ตัวอย่าง:
absolute="/foo/bar"
current="/foo/baz/foo"
# Magic
relative="../../bar"
ฉันจะสร้างเวทย์มนตร์ได้อย่างไร (หวังว่ารหัสไม่ซับซ้อนเกินไป ... )
realpath --relative-to=$absolute $current
ง่าย:
คำตอบ:
การใช้ realpath จาก GNU coreutils 8.23 นั้นง่ายที่สุดฉันคิดว่า:
$ realpath --relative-to="$file1" "$file2"
ตัวอย่างเช่น:
$ realpath --relative-to=/usr/bin/nmap /tmp/testing
../../../tmp/testing
$ realpath --relative-to="${PWD}" "$file"
มีประโยชน์ถ้าคุณต้องการพา ธ ที่สัมพันธ์กับไดเร็กทอรีการทำงานปัจจุบัน
/usr/bin/nmap/
-path แต่ไม่ใช่สำหรับ/usr/bin/nmap
: จากnmap
ไป/tmp/testing
มันก็เป็นเพียง../../
และไม่ได้ 3 ../
ครั้ง มันทำงานอย่างไรเพราะทำ..
ใน rootfs /
คือ
--relative-to=…
คาดว่าจะมีไดเร็กทอรีและไม่ตรวจสอบ นั่นหมายความว่าคุณจะจบลงด้วยเป็นพิเศษ "../" ถ้าคุณขอเส้นทางเทียบกับไฟล์ (เป็นตัวอย่างนี้ดูเหมือนจะทำเพราะ/usr/bin
ไม่ค่อยหรือไม่เคยมีไดเรกทอรีและnmap
เป็นปกติไบนารี)
$ python -c "import os.path; print os.path.relpath('/foo/bar', '/foo/baz/foo')"
ให้:
../../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:]))'
ทำงานได้อย่างเป็นธรรมชาติและน่าเชื่อถือที่สุด
นี่คือการปรับปรุงที่ถูกต้องและทำงานได้อย่างสมบูรณ์ของโซลูชันที่ได้รับการจัดอันดับที่ดีที่สุดในปัจจุบันจาก @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"
source=$1; target=$2
ด้วยsource=$(realpath $1); target=$(realpath $2)
realpath
แนะนำหรือsource=$(readlink -f $1)
อื่น ๆ หาก realpath ไม่สามารถใช้งานได้ (ไม่ได้มาตรฐาน)
$source
และเป็น$target
เช่นนี้: `ถ้า [[-e $ 1]]; แหล่งที่มา = $ (readlink -f $ 1); แหล่งอื่น = $ 1; fi ถ้า [[-e $ 2]]; จากนั้นเป้าหมาย = $ (readlink -f $ 2); เป้าหมายอื่น = $ 2; fi` ด้วยวิธีนี้ฟังก์ชั่นสามารถจัดการเส้นทางสัมพัทธ์จริง / ที่มีอยู่รวมทั้งไดเรกทอรีที่สมมติขึ้น
readlink
มี-m
ตัวเลือกซึ่งก็ไม่ว่า;)
#!/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/}
มันถูกสร้างขึ้นในPerlตั้งแต่ปี 2001 จึงทำงานในเกือบทุกระบบที่คุณสามารถจินตนาการแม้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)'
perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target"
สคริปต์perlperl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target" "$origin"
หนึ่งบรรทัดแรกใช้อาร์กิวเมนต์หนึ่งตัว (ต้นกำเนิดคือไดเรกทอรีทำงานปัจจุบัน) สคริปต์perlหนึ่งบรรทัดที่สองใช้สองอาร์กิวเมนต์
perl
สามารถพบได้เกือบทุกที่แม้ว่าคำตอบจะยังคงเป็นหนึ่งซับ
สมมติว่าคุณได้ติดตั้ง: bash, pwd, dirname, echo; จากนั้นก็คือ
#!/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และแนวคิดอื่น ๆ
หมายเหตุ : ต้องใช้ทั้งสองเส้นทางในการเป็นโฟลเดอร์ที่มีอยู่ ไฟล์จะไม่ทำงาน
os.path.relpath
เป็นฟังก์ชันเชลล์เป้าหมายของการนี้relpath
การออกกำลังกายคือการเลียนแบบงูหลาม 2.7 ของos.path.relpath
ฟังก์ชั่น (มีงูหลามรุ่น 2.6 แต่ทำงานอย่างถูกต้องใน 2.7) ที่เสนอโดยxni ดังนั้นผลลัพธ์บางอย่างอาจแตกต่างจากฟังก์ชั่นที่ให้ไว้ในคำตอบอื่น ๆ
(ฉันไม่ได้ทดสอบกับบรรทัดใหม่ในเส้นทางเพียงเพราะมันทำลายการตรวจสอบตามการโทรpython -c
จาก ZSH มันจะเป็นไปได้ด้วยความพยายามบางอย่าง)
เกี่ยวกับ“ เวทมนต์” ในแบชฉันได้เลิกค้นหาเวทมนตร์ในแบชมานานแล้ว แต่หลังจากนั้นฉันก็ได้พบเวทมนตร์ทั้งหมดที่ฉันต้องการจากนั้นบางส่วนใน ZSH
ดังนั้นฉันเสนอการใช้งานสองอย่าง
จุดมุ่งหมายดำเนินการครั้งแรกที่จะได้รับอย่างเต็มที่POSIX สอดคล้อง ฉันได้ทำการทดสอบกับ /bin/dash
Debian 6.0.6“ Squeeze” แล้ว มันยังทำงานได้อย่างสมบูรณ์แบบกับ/bin/sh
OS X 10.8.3 ซึ่งจริงๆแล้ว Bash เวอร์ชั่น 3.2 ทำท่าว่าจะเป็น POSIX เชลล์
การใช้งานที่สองคือฟังก์ชั่นเชลล์ ZSH ที่ทนทานต่อหลายสแลชและสิ่งรบกวนอื่น ๆ ในเส้นทาง หากคุณมี ZSH อยู่นี่เป็นเวอร์ชั่นที่แนะนำแม้ว่าคุณจะเรียกมันในรูปแบบสคริปต์ที่แสดงด้านล่าง (เช่นมี Shebang ของ#!/usr/bin/env zsh
) จากเชลล์อื่น
ในที่สุดฉันได้เขียนสคริปต์ ZSH ที่ตรวจสอบผลลัพธ์ของrelpath
คำสั่งที่พบใน$PATH
กรณีทดสอบให้ในคำตอบอื่น ๆ ฉันเพิ่มเครื่องเทศบางอย่างในการทดสอบเหล่านั้นโดยการเพิ่มช่องว่างบางแท็บและเครื่องหมายวรรคตอนเช่น! ? *
นี่และมีและยังโยนยังทดสอบอีกด้วยที่แปลกใหม่ UTF-8 ตัวอักษรที่พบในกลุ่ม-สายไฟฟ้า
ก่อนอื่นฟังก์ชั่นเชลล์ที่สอดคล้องกับ POSIX มันใช้งานได้กับหลากหลายพา ธ แต่ไม่ได้ลบเครื่องหมายทับหลาย ๆ อันหรือแก้ไข symlink
#!/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
รุ่นที่แข็งแกร่งกว่านี้ หากคุณต้องการที่จะแก้ไขข้อโต้แย้งเส้นทางจริงàลาrealpath -f
(ที่มีอยู่ในลินุกซ์coreutils
แพคเกจ) แทนที่:a
บนเส้นที่ 3 และ 4 :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
เปิดใช้งานเอาต์พุต verbose
#!/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
/
กลัว
#!/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")"; }
เชลล์สคริปต์ข้างต้นได้รับแรงบันดาลใจจากpini (ขอบคุณ!) มันก่อให้เกิดข้อผิดพลาดในโมดูลการเน้นไวยากรณ์ของ Stack Overflow (อย่างน้อยในกรอบตัวอย่างของฉัน) ดังนั้นโปรดเพิกเฉยหากการเน้นไม่ถูกต้อง
หมายเหตุบางส่วน:
ยกเว้นเครื่องหมายแบ็กสแลชที่กล่าวถึงบรรทัดสุดท้ายของฟังก์ชัน "relPath" เอาต์พุตชื่อพา ธ ที่เข้ากันได้กับไพ ธ อน:
path=$up${path#"$common"/}; path=${path%/}; printf %s "${path:-.}"
บรรทัดสุดท้ายสามารถถูกแทนที่ (และประยุกต์) โดยบรรทัด
printf %s "$up${path#"$common"/}"
ฉันชอบอันหลังเพราะ
ชื่อไฟล์สามารถต่อท้ายโดยตรงกับเส้นทาง dir ที่ได้รับจาก relPath เช่น:
ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
ลิงก์สัญลักษณ์ใน dir เดียวกันที่สร้างขึ้นด้วยวิธีนี้ไม่มีส่วนเติมเต็ม"./"
ของชื่อไฟล์ที่น่าเกลียด
รายการรหัสสำหรับการทดสอบการถดถอย (เพียงต่อท้ายเชลล์สคริปต์):
############################################################################
# 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
คำตอบที่นี่ไม่ค่อยมีประโยชน์สำหรับการใช้งานทุกวัน เนื่องจากเป็นการยากมากที่จะทำสิ่งนี้อย่างถูกต้องใน 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
ข้อเสียอย่างหนึ่งคือต้องใช้หลาม แต่:
โปรดทราบว่าโซลูชันที่มีbasename
หรือdirname
อาจไม่จำเป็นต้องดีกว่าเนื่องจากจำเป็นต้องcoreutils
ติดตั้ง หากใครบางคนมีbash
ทางออกที่บริสุทธิ์ที่น่าเชื่อถือและเรียบง่าย (แทนที่จะอยากรู้อยากเห็นที่ซับซ้อน) ฉันจะแปลกใจ
สคริปต์นี้ให้ผลลัพธ์ที่ถูกต้องเฉพาะกับอินพุตที่เป็นพา ธ สัมบูรณ์หรือพา ธ สัมพัทธ์โดยไม่มี.
หรือ..
:
#!/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"
ฉันจะใช้ Perl สำหรับงานที่ไม่น่าสนใจนี้:
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')"
ดังนั้นจะมีการเสนอราคาแบบสมบูรณ์และปัจจุบัน
relative=$(perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$absolute" "$current")
ผมชอบ สิ่งนี้ทำให้มั่นใจได้ว่าค่าเหล่านั้นไม่สามารถมีรหัส Perl ได้!
การปรับปรุงเล็กน้อยในคำตอบของ kaskuและPiniซึ่งเล่นดีกว่าด้วยช่องว่างและอนุญาตให้ผ่านเส้นทางสัมพัทธ์:
#!/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
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
readlink
$(pwd)
วิธีการแก้ปัญหาอื่นบริสุทธิ์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:-.}"
}
หมายเหตุ:
*
?
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
เป็นที่แน่นอนและถูกต้องเพียง แต่ทางญาติของหน้าเห็นได้จากหน้าเว็บbar
moo
ทุกอย่างอื่นจะผิดธรรมดา
มันเป็นเรื่องไม่สำคัญที่จะนำเอาท์พุทกับคำถามที่เห็นได้ชัดว่าcurrent
เป็นไดเรกทอรี:
absolute="/foo/bar"
current="/foo/baz/foo"
relative="../$(relpath "$absolute" "$current")"
สิ่งนี้จะคืนสิ่งที่ถูกถาม
และก่อนที่คุณยกคิ้วที่นี่เป็นตัวแปรที่มีความซับซ้อนมากขึ้นอีกนิดของrelpath
(จุดแตกต่างเล็ก ๆ ) ซึ่งควรจะทำงานสำหรับ URL ของไวยากรณ์มากเกินไป (เพื่อลาก/
รอดขอบคุณบาง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:-.}"
}
และนี่คือการตรวจสอบเพื่อให้ชัดเจน: ใช้งานได้จริงตามที่บอก
check()
{
res="$(relpathurl "$2" "$1")"
[ ".$res" = ".$3" ] && return
printf ':WRONG: %-10q %-10q gives %q\nCORRECT %-10q %-10q gives %q\n' "$1" "$2" "$res" "$@"
}
# TARGET SOURCE RESULT
check "/A/B/C" "/A" ".."
check "/A/B/C" "/A.x" "../../A.x"
check "/A/B/C" "/A/B" "."
check "/A/B/C" "/A/B/C" "C"
check "/A/B/C" "/A/B/C/D" "C/D"
check "/A/B/C" "/A/B/C/D/E" "C/D/E"
check "/A/B/C" "/A/B/D" "D"
check "/A/B/C" "/A/B/D/E" "D/E"
check "/A/B/C" "/A/D" "../D"
check "/A/B/C" "/A/D/E" "../D/E"
check "/A/B/C" "/D/E/F" "../../D/E/F"
check "/foo/baz/moo" "/foo/bar" "../bar"
check "http://example.com/foo/baz/moo" "http://example.com/foo/bar" "../bar"
check "http://example.com/foo/baz/moo/" "http://example.com/foo/bar" "../../bar"
check "http://example.com/foo/baz/moo" "http://example.com/foo/bar/" "../bar/"
check "http://example.com/foo/baz/moo/" "http://example.com/foo/bar/" "../../bar/"
และนี่คือวิธีที่สิ่งนี้สามารถนำมาใช้เพื่อให้ผลลัพธ์ที่ต้องการจากคำถาม:
absolute="/foo/bar"
current="/foo/baz/foo"
relative="$(relpathurl "$absolute" "$current/")"
echo "$relative"
หากคุณพบสิ่งที่ใช้งานไม่ได้โปรดแจ้งให้เราทราบในความคิดเห็นด้านล่าง ขอบคุณ
PS:
ทำไมข้อโต้แย้งของrelpath
"กลับรายการ" ตรงกันข้ามกับคำตอบอื่น ๆ ทั้งหมดที่นี่?
หากคุณเปลี่ยน
Y="$(readlink -m -- "$2")" || return
ถึง
Y="$(readlink -m -- "${2:-"$PWD"}")" || return
จากนั้นคุณสามารถปล่อยพารามิเตอร์ที่ 2 ออกไปเช่นว่า BASE เป็นไดเรกทอรี / URL ปัจจุบัน / อะไรก็ตาม นั่นเป็นเพียงหลักการ Unix ตามปกติ
หากคุณไม่ชอบโปรดกลับไปที่ Windows ขอบคุณ
น่าเศร้าที่คำตอบของ Mark Rushakoff (ตอนนี้ถูกลบ - มันอ้างถึงรหัสจากที่นี่ ) ดูเหมือนจะไม่ทำงานอย่างถูกต้องเมื่อปรับให้เข้ากับ:
source=/home/part2/part3/part4
target=/work/proj1/proj2
ความคิดที่อธิบายไว้ในคำอธิบายสามารถขัดเกลาเพื่อให้มันทำงานได้อย่างถูกต้องในกรณีส่วนใหญ่ ฉันกำลังจะสมมติว่าสคริปต์ใช้อาร์กิวเมนต์แหล่งที่มา (ที่คุณอยู่) และอาร์กิวเมนต์เป้าหมาย (ที่คุณต้องการไป) และที่ทั้งคู่เป็นชื่อพา ธ สัมบูรณ์หรือทั้งสองเป็นญาติ ถ้าสิ่งหนึ่งที่แน่นอนและสิ่งที่เกี่ยวข้องอื่น ๆ สิ่งที่ง่ายที่สุดคือการนำหน้าชื่อที่สัมพันธ์กันกับไดเรกทอรีการทำงานปัจจุบัน - แต่รหัสด้านล่างไม่ได้ทำเช่นนั้น
รหัสด้านล่างใกล้กับการทำงานอย่างถูกต้อง แต่ไม่ถูกต้อง
xyz/./pqr
'xyz/../pqr
'./
จากเส้นทางรหัสของเดนนิสนั้นดีกว่าเพราะมันแก้ไข 1 และ 5 - แต่มีปัญหาเดียวกัน 2, 3, 4 ใช้รหัสของเดนนิส (และลงคะแนนล่วงหน้าก่อนนี้) เพราะสิ่งนั้น
(NB: POSIX จัดให้มีการเรียกระบบrealpath()
ที่แก้ไขชื่อพา ธ เพื่อให้ไม่มี symlink เหลือไว้ในนั้นการใช้สิ่งนั้นกับชื่ออินพุตจากนั้นการใช้รหัสของเดนนิสจะให้คำตอบที่ถูกต้องในแต่ละครั้ง wraps realpath()
- ฉันทำไปแล้ว - แต่ฉันไม่รู้อรรถประโยชน์มาตรฐานที่ทำเช่นนั้น)
สำหรับสิ่งนี้ฉันพบว่า Perl ใช้งานง่ายกว่า shell แต่ bash มีการสนับสนุนอาร์เรย์ที่เหมาะสมและอาจทำเช่นนี้ได้เช่นกัน - แบบฝึกหัดสำหรับผู้อ่าน ดังนั้นให้สองชื่อที่เข้ากันได้แยกออกเป็นส่วน ๆ
ดังนั้น:
#!/bin/perl -w
use strict;
# Should fettle the arguments if one is absolute and one relative:
# Oops - missing functionality!
# Split!
my(@source) = split '/', $ARGV[0];
my(@target) = split '/', $ARGV[1];
my $count = scalar(@source);
$count = scalar(@target) if (scalar(@target) < $count);
my $relpath = "";
my $i;
for ($i = 0; $i < $count; $i++)
{
last if $source[$i] ne $target[$i];
}
$relpath = "." if ($i >= scalar(@source) && $relpath eq "");
for (my $s = $i; $s < scalar(@source); $s++)
{
$relpath = "../$relpath";
}
$relpath = "." if ($i >= scalar(@target) && $relpath eq "");
for (my $t = $i; $t < scalar(@target); $t++)
{
$relpath .= "/$target[$t]";
}
# Clean up result (remove double slash, trailing slash, trailing slash-dot).
$relpath =~ s%//%/%;
$relpath =~ s%/$%%;
$relpath =~ s%/\.$%%;
print "source = $ARGV[0]\n";
print "target = $ARGV[1]\n";
print "relpath = $relpath\n";
สคริปต์ทดสอบ (เครื่องหมายวงเล็บเหลี่ยมมีช่องว่างและแท็บ):
sed 's/#.*//;/^[ ]*$/d' <<! |
/home/part1/part2 /home/part1/part3
/home/part1/part2 /home/part4/part5
/home/part1/part2 /work/part6/part7
/home/part1 /work/part1/part2/part3/part4
/home /work/part2/part3
/ /work/part2/part3/part4
/home/part1/part2 /home/part1/part2/part3/part4
/home/part1/part2 /home/part1/part2/part3
/home/part1/part2 /home/part1/part2
/home/part1/part2 /home/part1
/home/part1/part2 /home
/home/part1/part2 /
/home/part1/part2 /work
/home/part1/part2 /work/part1
/home/part1/part2 /work/part1/part2
/home/part1/part2 /work/part1/part2/part3
/home/part1/part2 /work/part1/part2/part3/part4
home/part1/part2 home/part1/part3
home/part1/part2 home/part4/part5
home/part1/part2 work/part6/part7
home/part1 work/part1/part2/part3/part4
home work/part2/part3
. work/part2/part3
home/part1/part2 home/part1/part2/part3/part4
home/part1/part2 home/part1/part2/part3
home/part1/part2 home/part1/part2
home/part1/part2 home/part1
home/part1/part2 home
home/part1/part2 .
home/part1/part2 work
home/part1/part2 work/part1
home/part1/part2 work/part1/part2
home/part1/part2 work/part1/part2/part3
home/part1/part2 work/part1/part2/part3/part4
!
while read source target
do
perl relpath.pl $source $target
echo
done
เอาต์พุตจากสคริปต์ทดสอบ:
source = /home/part1/part2
target = /home/part1/part3
relpath = ../part3
source = /home/part1/part2
target = /home/part4/part5
relpath = ../../part4/part5
source = /home/part1/part2
target = /work/part6/part7
relpath = ../../../work/part6/part7
source = /home/part1
target = /work/part1/part2/part3/part4
relpath = ../../work/part1/part2/part3/part4
source = /home
target = /work/part2/part3
relpath = ../work/part2/part3
source = /
target = /work/part2/part3/part4
relpath = ./work/part2/part3/part4
source = /home/part1/part2
target = /home/part1/part2/part3/part4
relpath = ./part3/part4
source = /home/part1/part2
target = /home/part1/part2/part3
relpath = ./part3
source = /home/part1/part2
target = /home/part1/part2
relpath = .
source = /home/part1/part2
target = /home/part1
relpath = ..
source = /home/part1/part2
target = /home
relpath = ../..
source = /home/part1/part2
target = /
relpath = ../../../..
source = /home/part1/part2
target = /work
relpath = ../../../work
source = /home/part1/part2
target = /work/part1
relpath = ../../../work/part1
source = /home/part1/part2
target = /work/part1/part2
relpath = ../../../work/part1/part2
source = /home/part1/part2
target = /work/part1/part2/part3
relpath = ../../../work/part1/part2/part3
source = /home/part1/part2
target = /work/part1/part2/part3/part4
relpath = ../../../work/part1/part2/part3/part4
source = home/part1/part2
target = home/part1/part3
relpath = ../part3
source = home/part1/part2
target = home/part4/part5
relpath = ../../part4/part5
source = home/part1/part2
target = work/part6/part7
relpath = ../../../work/part6/part7
source = home/part1
target = work/part1/part2/part3/part4
relpath = ../../work/part1/part2/part3/part4
source = home
target = work/part2/part3
relpath = ../work/part2/part3
source = .
target = work/part2/part3
relpath = ../work/part2/part3
source = home/part1/part2
target = home/part1/part2/part3/part4
relpath = ./part3/part4
source = home/part1/part2
target = home/part1/part2/part3
relpath = ./part3
source = home/part1/part2
target = home/part1/part2
relpath = .
source = home/part1/part2
target = home/part1
relpath = ..
source = home/part1/part2
target = home
relpath = ../..
source = home/part1/part2
target = .
relpath = ../../..
source = home/part1/part2
target = work
relpath = ../../../work
source = home/part1/part2
target = work/part1
relpath = ../../../work/part1
source = home/part1/part2
target = work/part1/part2
relpath = ../../../work/part1/part2
source = home/part1/part2
target = work/part1/part2/part3
relpath = ../../../work/part1/part2/part3
source = home/part1/part2
target = work/part1/part2/part3/part4
relpath = ../../../work/part1/part2/part3/part4
สคริปต์ Perl นี้ทำงานได้อย่างทั่วถึงใน Unix (ไม่ได้คำนึงถึงความซับซ้อนทั้งหมดของชื่อพา ธ Windows) ในหน้าของอินพุตแปลก ๆ มันใช้โมดูลCwd
และฟังก์ชั่นrealpath
ในการแก้ไขเส้นทางที่แท้จริงของชื่อที่มีอยู่และทำการวิเคราะห์ข้อความสำหรับเส้นทางที่ไม่มีอยู่ ในทุกกรณียกเว้นหนึ่งรายการจะสร้างเอาต์พุตเดียวกันกับสคริปต์ของ Dennis กรณีเบี่ยงเบนคือ:
source = home/part1/part2
target = .
relpath1 = ../../..
relpath2 = ../../../.
ผลลัพธ์ทั้งสองนั้นเทียบเท่า - ไม่เหมือนกัน (ผลลัพธ์มาจากสคริปต์ทดสอบรุ่นที่ได้รับการแก้ไขอย่างอ่อนโยน - สคริปต์ Perl ด้านล่างจะพิมพ์คำตอบแทนที่จะป้อนข้อมูลและคำตอบตามสคริปต์ด้านบน) ตอนนี้: ฉันควรกำจัดคำตอบที่ไม่ทำงานหรือไม่ อาจจะ...
#!/bin/perl -w
# Based loosely on code from: http://unix.derkeiler.com/Newsgroups/comp.unix.shell/2005-10/1256.html
# Via: http://stackoverflow.com/questions/2564634
use strict;
die "Usage: $0 from to\n" if scalar @ARGV != 2;
use Cwd qw(realpath getcwd);
my $pwd;
my $verbose = 0;
# Fettle filename so it is absolute.
# Deals with '//', '/./' and '/../' notations, plus symlinks.
# The realpath() function does the hard work if the path exists.
# For non-existent paths, the code does a purely textual hack.
sub resolve
{
my($name) = @_;
my($path) = realpath($name);
if (!defined $path)
{
# Path does not exist - do the best we can with lexical analysis
# Assume Unix - not dealing with Windows.
$path = $name;
if ($name !~ m%^/%)
{
$pwd = getcwd if !defined $pwd;
$path = "$pwd/$path";
}
$path =~ s%//+%/%g; # Not UNC paths.
$path =~ s%/$%%; # No trailing /
$path =~ s%/\./%/%g; # No embedded /./
# Try to eliminate /../abc/
$path =~ s%/\.\./(?:[^/]+)(/|$)%$1%g;
$path =~ s%/\.$%%; # No trailing /.
$path =~ s%^\./%%; # No leading ./
# What happens with . and / as inputs?
}
return($path);
}
sub print_result
{
my($source, $target, $relpath) = @_;
if ($verbose)
{
print "source = $ARGV[0]\n";
print "target = $ARGV[1]\n";
print "relpath = $relpath\n";
}
else
{
print "$relpath\n";
}
exit 0;
}
my($source) = resolve($ARGV[0]);
my($target) = resolve($ARGV[1]);
print_result($source, $target, ".") if ($source eq $target);
# Split!
my(@source) = split '/', $source;
my(@target) = split '/', $target;
my $count = scalar(@source);
$count = scalar(@target) if (scalar(@target) < $count);
my $relpath = "";
my $i;
# Both paths are absolute; Perl splits an empty field 0.
for ($i = 1; $i < $count; $i++)
{
last if $source[$i] ne $target[$i];
}
for (my $s = $i; $s < scalar(@source); $s++)
{
$relpath = "$relpath/" if ($s > $i);
$relpath = "$relpath..";
}
for (my $t = $i; $t < scalar(@target); $t++)
{
$relpath = "$relpath/" if ($relpath ne "");
$relpath = "$relpath$target[$t]";
}
print_result($source, $target, $relpath);
/home/part1/part2
จะมีหนึ่งมากเกินไป/
../
มิฉะนั้นสคริปต์ของฉันจะตรงกับเอาต์พุตของคุณยกเว้นของฉันจะเพิ่มสิ่งที่ไม่จำเป็น.
ในตอนท้ายของที่ปลายทาง.
และฉันจะไม่ใช้ a ./
ที่จุดเริ่มต้นของสิ่งที่ลงมาโดยไม่ขึ้น
readlink /usr/bin/vi
ให้/etc/alternatives/vi
แต่เป็น symlink อื่น - ในขณะที่readlink -f /usr/bin/vi
ให้/usr/bin/vim.basic
ซึ่งเป็นปลายทางที่ดีที่สุดของsymlink ทั้งหมด ...
ฉันใช้คำถามของคุณเป็นความท้าทายที่จะเขียนในรหัสเปลือก "พกพา" เช่น
มันทำงานบนเปลือกใด ๆ ที่สอดคล้อง POSIX (zsh, bash, ksh, เถ้า, 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 :
โซลูชันของฉัน:
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
}
นี่คือรุ่นของฉัน มันขึ้นอยู่กับคำตอบโดย@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
}
เดาสิว่าคนนี้จะทำเคล็ดลับด้วย ...
ตกลงค่าใช้จ่ายคาดหวัง แต่เรากำลังทำเชลล์เป้าหมายที่นี่! ;)
#!/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
สคริปต์นี้ใช้ได้กับชื่อพา ธ เท่านั้น ไม่จำเป็นต้องมีไฟล์ใด ๆ อยู่ หากเส้นทางที่ผ่านมาไม่สมบูรณ์พฤติกรรมจะผิดปกติเล็กน้อย แต่ควรทำงานตามที่คาดไว้หากเส้นทางทั้งสองสัมพันธ์กัน
ฉันทดสอบเฉพาะบน 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
คำตอบนี้ไม่ได้อยู่ส่วน 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"
relpath
ฟังก์ชั่น) ทำงานเหมือนกันfile-relative-name
สำหรับกรณีทดสอบที่คุณให้ไว้
นี่คือเชลล์สคริปต์ที่ทำงานได้โดยไม่ต้องเรียกโปรแกรมอื่น ๆ :
#! /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
ฉันต้องการสิ่งนี้ แต่แก้ไขลิงก์สัญลักษณ์ด้วย ฉันค้นพบว่า pwd มีแฟล็ก -P สำหรับจุดประสงค์นั้น ส่วนของสคริปต์ของฉันถูกต่อท้าย มันอยู่ในฟังก์ชันในเชลล์สคริปต์ดังนั้น $ 1 และ $ 2 ค่าผลลัพธ์ซึ่งเป็นเส้นทางสัมพัทธ์จาก START_ABS ถึง END_ABS อยู่ในตัวแปร UPDIRS สคริปต์ของซีดีลงในแต่ละไดเรกทอรีพารามิเตอร์เพื่อดำเนินการ 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"