แปลงพา ธ สัมบูรณ์เป็นพา ธ สัมพัทธ์ที่กำหนดไดเรกทอรีปัจจุบันโดยใช้ Bash


261

ตัวอย่าง:

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

# Magic

relative="../../bar"

ฉันจะสร้างเวทย์มนตร์ได้อย่างไร (หวังว่ารหัสไม่ซับซ้อนเกินไป ... )


7
ตัวอย่างเช่น (กรณีของฉันตอนนี้) สำหรับการให้เส้นทางสัมพัทธ์ gcc เพื่อให้มันสามารถสร้าง infos สัมพัทธ์ดีบักที่ใช้งานได้แม้ว่าการเปลี่ยนแปลงเส้นทางที่มา
Offirmo

คำถามที่คล้ายกับหน้านี้ถูกถามเกี่ยวกับ U & L: unix.stackexchange.com/questions/100918/... หนึ่งในคำตอบ (@Gilles) กล่าวถึงเครื่องมือsymlinksซึ่งสามารถทำให้การทำงานง่ายของปัญหานี้
slm

25
realpath --relative-to=$absolute $currentง่าย:
kenorb

คำตอบ:


228

การใช้ realpath จาก GNU coreutils 8.23 ​​นั้นง่ายที่สุดฉันคิดว่า:

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

ตัวอย่างเช่น:

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

7
น่าเสียดายที่แพ็คเกจล้าสมัยใน Ubuntu 14.04 และไม่มีตัวเลือก - สัมพันธ์กับตัวเลือก
kzh

3
ทำงานได้ดีบน Ubuntu 16.04
cayhorstmann

7
$ realpath --relative-to="${PWD}" "$file"มีประโยชน์ถ้าคุณต้องการพา ธ ที่สัมพันธ์กับไดเร็กทอรีการทำงานปัจจุบัน
dcoles

1
นี่คือสิ่งที่ถูกต้องสำหรับภายใน/usr/bin/nmap/-path แต่ไม่ใช่สำหรับ/usr/bin/nmap: จากnmapไป/tmp/testingมันก็เป็นเพียง../../และไม่ได้ 3 ../ครั้ง มันทำงานอย่างไรเพราะทำ..ใน rootfs /คือ
Patrick B.

6
เป็น @PatrickB โดยนัย--relative-to=…คาดว่าจะมีไดเร็กทอรีและไม่ตรวจสอบ นั่นหมายความว่าคุณจะจบลงด้วยเป็นพิเศษ "../" ถ้าคุณขอเส้นทางเทียบกับไฟล์ (เป็นตัวอย่างนี้ดูเหมือนจะทำเพราะ/usr/binไม่ค่อยหรือไม่เคยมีไดเรกทอรีและnmapเป็นปกติไบนารี)
IBBoard

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

ให้:

../../bar

11
มันใช้งานได้และมันทำให้ทางเลือกดูไร้สาระ นั่นเป็นโบนัสสำหรับฉัน xD
hasvn

31
+1 ตกลงคุณโกง ... แต่นี่มันดีเกินไปที่จะไม่ใช้! relpath(){ python -c "import os.path; print os.path.relpath('$1','${2:-$PWD}')" ; }
MestreLion

4
น่าเศร้าที่นี่ไม่สามารถใช้ได้ทั่วไป: os.path.relpath เป็นของใหม่ใน Python 2.6
เฉินเลวี่

15
@ChenLevy: Python 2.6 เปิดตัวในปี 2008 ยากที่จะเชื่อว่าไม่มีในระดับสากลในปี 2012
MestreLion

11
python -c 'import os, sys; print(os.path.relpath(*sys.argv[1:]))'ทำงานได้อย่างเป็นธรรมชาติและน่าเชื่อถือที่สุด
musiphil

31

นี่คือการปรับปรุงที่ถูกต้องและทำงานได้อย่างสมบูรณ์ของโซลูชันที่ได้รับการจัดอันดับที่ดีที่สุดในปัจจุบันจาก @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"

1
รวมอยู่ใน offirmo shell lib github.com/Offirmo/offirmo-shell-lib , ฟังก์ชั่น« OSL_FILE_find_relative_path » (ไฟล์« osl_lib_file.sh »)
Offirmo

1
+1 มันสามารถทำได้อย่างง่ายดายเพื่อจัดการเส้นทางใด ๆ (ไม่เพียง แต่เส้นทางที่แน่นอนเริ่มต้นด้วย /) โดยแทนที่source=$1; target=$2ด้วยsource=$(realpath $1); target=$(realpath $2)
Josh Kelley

2
@ Josh แน่นอนโดยมีเงื่อนไขว่า dirs มีอยู่จริง ... ซึ่งไม่สะดวกสำหรับการทดสอบหน่วย;) แต่ในการใช้งานจริงใช่realpathแนะนำหรือsource=$(readlink -f $1)อื่น ๆ หาก realpath ไม่สามารถใช้งานได้ (ไม่ได้มาตรฐาน)
Offirmo

ฉันกำหนด$sourceและเป็น$targetเช่นนี้: `ถ้า [[-e $ 1]]; แหล่งที่มา = $ (readlink -f $ 1); แหล่งอื่น = $ 1; fi ถ้า [[-e $ 2]]; จากนั้นเป้าหมาย = $ (readlink -f $ 2); เป้าหมายอื่น = $ 2; fi` ด้วยวิธีนี้ฟังก์ชั่นสามารถจัดการเส้นทางสัมพัทธ์จริง / ที่มีอยู่รวมทั้งไดเรกทอรีที่สมมติขึ้น
Nathan S. Watson-Haigh

1
@ NathanS.Watson-Haigh ดียิ่งขึ้นเมื่อเร็ว ๆ นี้ผมค้นพบreadlinkมี-mตัวเลือกซึ่งก็ไม่ว่า;)
Offirmo

26
#!/bin/bash
# both $1 and $2 are absolute paths
# returns $2 relative to $1

source=$1
target=$2

common_part=$source
back=
while [ "${target#$common_part}" = "${target}" ]; do
  common_part=$(dirname $common_part)
  back="../${back}"
done

echo ${back}${target#$common_part/}

สคริปต์ที่ยอดเยี่ยม - สั้นและสะอาด ฉันใช้การแก้ไข (กำลังรอการตรวจทานจากเพื่อน): common_part = $ source / common_part = $ (dirname $ common_part) / echo $ {ย้อนกลับ} $ {target # $ common_part} สคริปต์ที่มีอยู่จะล้มเหลวเนื่องจากการจับคู่ที่ไม่เหมาะสมกับชื่อไดเรกทอรี เมื่อทำการเปรียบเทียบเช่น: "/ foo / bar / baz" ถึง "/ foo / barsucks / bonk" การย้ายสแลชไปยัง var และ out ของ eval สุดท้ายจะแก้ไขข้อผิดพลาดนั้น
jcwenger

3
สคริปต์นี้ใช้งานไม่ได้ ไม่ทำการทดสอบอย่างง่าย "หนึ่งไดเรกทอรีลง" การแก้ไขโดย jcwenger ทำงานได้ดีขึ้นเล็กน้อย แต่มีแนวโน้มที่จะเพิ่ม "../" พิเศษ
Dr. Person Person II II

1
มันล้มเหลวสำหรับฉันในบางกรณีหากมีการต่อท้าย "/" ในการโต้แย้ง; เช่นถ้า $ 1 = "$ HOME /" และ $ 2 = "$ HOME / temp" ก็จะส่งกลับ "/ home / user / temp /" แต่ถ้า $ 1 = $ HOME ก็จะส่งกลับเส้นทางญาติ "temp" อย่างถูกต้อง ทั้ง source = $ 1 และ target = $ 2 อาจเป็น "cleaned" โดยใช้ sed (หรือใช้การแทนที่ตัวแปร bash แต่อาจเป็นทึบแสงโดยไม่จำเป็น) เช่น => source = $ (echo "$ {1}" | sed 's / \ / * $ // ')
michael

1
การปรับปรุงเล็กน้อย: แทนที่จะตั้งค่าต้นทาง / เป้าหมายโดยตรงเป็น $ 1 และ $ 2 ทำ: source = $ (cd $ 1; pwd) target = $ (cd $ 2; pwd) วิธีนี้จะจัดการเส้นทางด้วย และ .. ถูกต้อง
Joseph Garvin

4
แม้จะเป็นคำตอบที่ได้รับคะแนนสูงสุดคำตอบนี้มีข้อ จำกัด มากมายดังนั้นจึงมีคำตอบอื่น ๆ อีกมากมายที่โพสต์ ดูคำตอบอื่น ๆ แทนโดยเฉพาะที่แสดงกรณีทดสอบ และโปรดโหวตความคิดเห็นนี้!
Offirmo

25

มันถูกสร้างขึ้นใน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

... จะทำงานได้ดี


3
sayไม่ได้มีให้ในภาษา Perl สำหรับบันทึก แต่สามารถใช้ได้อย่างมีประสิทธิภาพที่นี่ perl -MFile::Spec -E 'say File::Spec->abs2rel(@ARGV)'
วิลเลียม Pursell

+1 แต่โปรดดูคำตอบที่คล้ายกันนี้ซึ่งเก่ากว่า (ก.พ. 2555) อ่านยังแสดงความคิดเห็นที่เกี่ยวข้องจากวิลเลียม Pursell รุ่นของฉันมีสองบรรทัดคำสั่งและperl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target" สคริปต์perlperl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target" "$origin"หนึ่งบรรทัดแรกใช้อาร์กิวเมนต์หนึ่งตัว (ต้นกำเนิดคือไดเรกทอรีทำงานปัจจุบัน) สคริปต์perlหนึ่งบรรทัดที่สองใช้สองอาร์กิวเมนต์
olibre

3
นั่นควรเป็นคำตอบที่ยอมรับได้ perlสามารถพบได้เกือบทุกที่แม้ว่าคำตอบจะยังคงเป็นหนึ่งซับ
Dmitry Ginzburg

19

สมมติว่าคุณได้ติดตั้ง: 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และแนวคิดอื่น ๆ

หมายเหตุ : ต้องใช้ทั้งสองเส้นทางในการเป็นโฟลเดอร์ที่มีอยู่ ไฟล์จะไม่ทำงาน


2
คำตอบที่เหมาะ: ทำงานร่วมกับ / bin / sh, ไม่จำเป็นต้องใช้ readlink, python, perl -> ยอดเยี่ยมสำหรับระบบไฟ / ฝังตัวหรือคอนโซลทุบตี windows
Francois

2
น่าเสียดายที่สิ่งนี้ต้องการเส้นทางที่มีอยู่ซึ่งไม่ต้องการเสมอไป
drwatsoncode

คำตอบจากพระเจ้า สิ่งที่ cd-pwd ใช้สำหรับแก้ไขลิงก์ที่ฉันเดา? เล่นกอล์ฟได้ดี!
Teck-freak

15

Python os.path.relpathเป็นฟังก์ชันเชลล์

เป้าหมายของการนี้relpathการออกกำลังกายคือการเลียนแบบงูหลาม 2.7 ของos.path.relpathฟังก์ชั่น (มีงูหลามรุ่น 2.6 แต่ทำงานอย่างถูกต้องใน 2.7) ที่เสนอโดยxni ดังนั้นผลลัพธ์บางอย่างอาจแตกต่างจากฟังก์ชั่นที่ให้ไว้ในคำตอบอื่น ๆ

(ฉันไม่ได้ทดสอบกับบรรทัดใหม่ในเส้นทางเพียงเพราะมันทำลายการตรวจสอบตามการโทรpython -cจาก ZSH มันจะเป็นไปได้ด้วยความพยายามบางอย่าง)

เกี่ยวกับ“ เวทมนต์” ในแบชฉันได้เลิกค้นหาเวทมนตร์ในแบชมานานแล้ว แต่หลังจากนั้นฉันก็ได้พบเวทมนตร์ทั้งหมดที่ฉันต้องการจากนั้นบางส่วนใน ZSH

ดังนั้นฉันเสนอการใช้งานสองอย่าง

จุดมุ่งหมายดำเนินการครั้งแรกที่จะได้รับอย่างเต็มที่POSIX สอดคล้อง ฉันได้ทำการทดสอบกับ /bin/dashDebian 6.0.6“ Squeeze” แล้ว มันยังทำงานได้อย่างสมบูรณ์แบบกับ/bin/shOS X 10.8.3 ซึ่งจริงๆแล้ว Bash เวอร์ชั่น 3.2 ทำท่าว่าจะเป็น POSIX เชลล์

การใช้งานที่สองคือฟังก์ชั่นเชลล์ ZSH ที่ทนทานต่อหลายสแลชและสิ่งรบกวนอื่น ๆ ในเส้นทาง หากคุณมี ZSH อยู่นี่เป็นเวอร์ชั่นที่แนะนำแม้ว่าคุณจะเรียกมันในรูปแบบสคริปต์ที่แสดงด้านล่าง (เช่นมี Shebang ของ#!/usr/bin/env zsh) จากเชลล์อื่น

ในที่สุดฉันได้เขียนสคริปต์ ZSH ที่ตรวจสอบผลลัพธ์ของrelpathคำสั่งที่พบใน$PATHกรณีทดสอบให้ในคำตอบอื่น ๆ ฉันเพิ่มเครื่องเทศบางอย่างในการทดสอบเหล่านั้นโดยการเพิ่มช่องว่างบางแท็บและเครื่องหมายวรรคตอนเช่น! ? *นี่และมีและยังโยนยังทดสอบอีกด้วยที่แปลกใหม่ UTF-8 ตัวอักษรที่พบในกลุ่ม-สายไฟฟ้า

ฟังก์ชันเชลล์POSIX

ก่อนอื่นฟังก์ชั่นเชลล์ที่สอดคล้องกับ 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

ตอนนี้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

2
ไม่ทำงานเมื่อเส้นทางแรกจบลงด้วยความ/กลัว
Noldorin

12
#!/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 (อย่างน้อยในกรอบตัวอย่างของฉัน) ดังนั้นโปรดเพิกเฉยหากการเน้นไม่ถูกต้อง

หมายเหตุบางส่วน:

  • ลบข้อผิดพลาดและรหัสที่ปรับปรุงแล้วโดยไม่เพิ่มความยาวและความซับซ้อนของรหัสอย่างมีนัยสำคัญ
  • ใส่ฟังก์ชั่นลงในฟังก์ชั่นเพื่อความสะดวกในการใช้งาน
  • เก็บฟังก์ชั่น POSIX ได้เพื่อให้พวกเขา (ควร) ทำงานกับเชลล์ POSIX ทั้งหมด (ทดสอบด้วยเส้นประทุบตีและ zsh ใน Ubuntu Linux 12.04)
  • ใช้ตัวแปรโลคัลเท่านั้นเพื่อหลีกเลี่ยงการปิดกั้นตัวแปรโกลบอลและสร้างมลภาวะพื้นที่ชื่อโกลบอล
  • เส้นทางไดเรกทอรีทั้งสองไม่จำเป็นต้องมีอยู่ (ข้อกำหนดสำหรับใบสมัครของฉัน)
  • ชื่อพา ธ อาจมีช่องว่างอักขระพิเศษอักขระควบคุมแบ็กสแลชแท็บ ', ",?, *, [,], เป็นต้น
  • ฟังก์ชั่นหลัก "relPath" ใช้ POSIX เชลล์บิวด์อินเท่านั้น
  • ฟังก์ชั่นเพิ่มเติม "relpath" สามารถจัดการเส้นทางไดเรกทอรีโดยพลการ (เช่นญาติไม่ใช่แบบบัญญัติ) แต่ต้องใช้ยูทิลิตี้หลักของ GNU ภายนอก "readlink"
  • หลีกเลี่ยง builtin "echo" และใช้ builtin "printf" แทนด้วยเหตุผลสองประการ:
    • เนื่องจากความขัดแย้งทางประวัติศาสตร์ของการใช้งานในตัว "ก้อง" มันจะทำงานแตกต่างกันในเปลือกหอยที่แตกต่างกัน -> POSIX แนะนำ printf ที่เป็นที่ต้องการมากกว่าก้อง
    • บิวด์อิน "echo" ของเชลล์ POSIX บางตัวจะตีความลำดับแบ็กสแลชบางส่วนและทำให้ชื่อพา ธ เสียหายที่มีลำดับดังกล่าว
  • เพื่อหลีกเลี่ยงการแปลงที่ไม่จำเป็นชื่อพา ธ จะถูกใช้เมื่อส่งคืนและคาดหวังโดยยูทิลิตี้เชลล์และ OS (เช่น cd, ln, ls, find, mkdir; ต่างจาก python "os.path.relpath" ซึ่งจะตีความลำดับแบ็กสแลชบางส่วน)
  • ยกเว้นเครื่องหมายแบ็กสแลชที่กล่าวถึงบรรทัดสุดท้ายของฟังก์ชัน "relPath" เอาต์พุตชื่อพา ธ ที่เข้ากันได้กับไพ ธ อน:

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

    บรรทัดสุดท้ายสามารถถูกแทนที่ (และประยุกต์) โดยบรรทัด

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

    ฉันชอบอันหลังเพราะ

    1. ชื่อไฟล์สามารถต่อท้ายโดยตรงกับเส้นทาง dir ที่ได้รับจาก relPath เช่น:

      ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
    2. ลิงก์สัญลักษณ์ใน dir เดียวกันที่สร้างขึ้นด้วยวิธีนี้ไม่มีส่วนเติมเต็ม"./"ของชื่อไฟล์ที่น่าเกลียด

  • หากคุณพบข้อผิดพลาดโปรดติดต่อ linuxball (at) gmail.com และฉันจะพยายามแก้ไข
  • เพิ่มชุดทดสอบการถดถอย (รองรับ POSIX เชลล์ด้วย)

รายการรหัสสำหรับการทดสอบการถดถอย (เพียงต่อท้ายเชลล์สคริปต์):

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

9

คำตอบที่นี่ไม่ค่อยมีประโยชน์สำหรับการใช้งานทุกวัน เนื่องจากเป็นการยากมากที่จะทำสิ่งนี้อย่างถูกต้องใน bash บริสุทธิ์ฉันขอแนะนำวิธีแก้ปัญหาต่อไปนี้ที่เชื่อถือได้ (คล้ายกับข้อเสนอแนะเดียวที่ฝังอยู่ในความคิดเห็น):

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

จากนั้นคุณจะได้รับเส้นทางสัมพัทธ์ตามไดเรกทอรีปัจจุบัน:

echo $(relpath somepath)

หรือคุณสามารถระบุว่าพา ธ นั้นสัมพันธ์กับไดเรกทอรีที่กำหนด:

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

ข้อเสียอย่างหนึ่งคือต้องใช้หลาม แต่:

  • มันทำงานเหมือนกันในหลาม> = 2.6
  • ไม่ต้องการให้มีไฟล์หรือไดเรกทอรีอยู่
  • ชื่อไฟล์อาจมีอักขระพิเศษหลากหลายประเภท ตัวอย่างเช่นโซลูชันอื่น ๆ ไม่สามารถใช้งานได้หากชื่อไฟล์มีช่องว่างหรืออักขระพิเศษอื่น ๆ
  • มันเป็นฟังก์ชั่นหนึ่งบรรทัดที่ไม่ถ่วง

โปรดทราบว่าโซลูชันที่มีbasenameหรือdirnameอาจไม่จำเป็นต้องดีกว่าเนื่องจากจำเป็นต้องcoreutilsติดตั้ง หากใครบางคนมีbashทางออกที่บริสุทธิ์ที่น่าเชื่อถือและเรียบง่าย (แทนที่จะอยากรู้อยากเห็นที่ซับซ้อน) ฉันจะแปลกใจ


ดูเหมือนว่าจะเป็นวิธีที่แข็งแกร่งที่สุด
dimo414

7

สคริปต์นี้ให้ผลลัพธ์ที่ถูกต้องเฉพาะกับอินพุตที่เป็นพา ธ สัมบูรณ์หรือพา ธ สัมพัทธ์โดยไม่มี.หรือ..:

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

1
ดูเหมือนว่าจะใช้งานได้ หากไดเรกทอรีมีอยู่จริงให้ใช้ $ (readlink -f $ 1) และ $ (readlink -f $ 2) บนอินพุตสามารถแก้ไขปัญหาที่ "." หรือ ".. " ปรากฏในอินพุต สิ่งนี้อาจทำให้เกิดปัญหาหากไดเรกทอรีไม่มีอยู่จริง
Dr. Person Person II II

7

ฉันจะใช้ Perl สำหรับงานที่ไม่น่าสนใจนี้:

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

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

1
+1 แต่อยากจะแนะนำ: perl -MFile::Spec -e "print File::Spec->abs2rel('$absolute','$current')"ดังนั้นจะมีการเสนอราคาแบบสมบูรณ์และปัจจุบัน
วิลเลียม Pursell

relative=$(perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$absolute" "$current")ผมชอบ สิ่งนี้ทำให้มั่นใจได้ว่าค่าเหล่านั้นไม่สามารถมีรหัส Perl ได้!
Erik Aronesty

6

การปรับปรุงเล็กน้อยในคำตอบของ 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

4

test.sh:

#!/bin/bash                                                                 

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

การทดสอบ:

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

+1 เพื่อความกะทัดรัดและ bash-ness คุณควร แต่ยังเรียกร้องบนreadlink $(pwd)
DevSolar

2
ความสัมพันธ์ไม่ได้หมายความว่าไฟล์จะต้องอยู่ในไดเรกทอรีเดียวกัน
greenoldman

แม้ว่าคำถามดั้งเดิมจะไม่มีการทดสอบจำนวนมาก แต่สคริปต์นี้ล้มเหลวสำหรับการทดสอบอย่างง่ายเช่นการค้นหาเส้นทางสัมพัทธ์จาก / home / user1 ถึง / home / user2 (คำตอบที่ถูกต้อง: ../user2) สคริปต์โดย pini / jcwenger ใช้ได้กับกรณีนี้
ไมเคิล

4

วิธีการแก้ปัญหาอื่นบริสุทธิ์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)

แก้ไขเนื่องจาก downvote เป็นดังนี้

นี่คือการทดสอบว่าฟังก์ชั่นนี้ถูกต้องจริง:

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 ขอบคุณ


3

น่าเศร้าที่คำตอบของ Mark Rushakoff (ตอนนี้ถูกลบ - มันอ้างถึงรหัสจากที่นี่ ) ดูเหมือนจะไม่ทำงานอย่างถูกต้องเมื่อปรับให้เข้ากับ:

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

ความคิดที่อธิบายไว้ในคำอธิบายสามารถขัดเกลาเพื่อให้มันทำงานได้อย่างถูกต้องในกรณีส่วนใหญ่ ฉันกำลังจะสมมติว่าสคริปต์ใช้อาร์กิวเมนต์แหล่งที่มา (ที่คุณอยู่) และอาร์กิวเมนต์เป้าหมาย (ที่คุณต้องการไป) และที่ทั้งคู่เป็นชื่อพา ธ สัมบูรณ์หรือทั้งสองเป็นญาติ ถ้าสิ่งหนึ่งที่แน่นอนและสิ่งที่เกี่ยวข้องอื่น ๆ สิ่งที่ง่ายที่สุดคือการนำหน้าชื่อที่สัมพันธ์กันกับไดเรกทอรีการทำงานปัจจุบัน - แต่รหัสด้านล่างไม่ได้ทำเช่นนั้น


ระวัง

รหัสด้านล่างใกล้กับการทำงานอย่างถูกต้อง แต่ไม่ถูกต้อง

  1. มีปัญหาที่ระบุไว้ในความคิดเห็นจาก Dennis Williamson
  2. นอกจากนี้ยังมีปัญหาที่การประมวลผลชื่อพา ธ แบบข้อความล้วน ๆ และคุณอาจสับสนได้โดย symlinks แปลก ๆ
  3. รหัสไม่จัดการ 'จุด' หลงทางในเส้นทางเช่น ' xyz/./pqr'
  4. รหัสไม่ได้จัดการ 'จุดสองจุดที่ผิดปกติในเส้นทางเช่น' xyz/../pqr'
  5. เล็กน้อย: รหัสไม่ได้ลบ '' นำหน้า./จากเส้นทาง

รหัสของเดนนิสนั้นดีกว่าเพราะมันแก้ไข 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 ./ที่จุดเริ่มต้นของสิ่งที่ลงมาโดยไม่ขึ้น
หยุดชั่วคราวจนกว่าจะมีการแจ้งให้ทราบต่อไป

@Dennis: ฉันใช้เวลาในการมองข้ามผลลัพธ์ - บางครั้งฉันก็เห็นปัญหานั้นและบางครั้งฉันก็หาไม่เจออีก การลบ './' นำหน้าเป็นอีกขั้นตอนหนึ่งที่ไม่สำคัญ ความคิดเห็นของคุณเกี่ยวกับ 'ไม่ฝังตัว หรือ .. ก็เกี่ยวข้องกันเช่นกัน จริงๆแล้วมันเป็นเรื่องยากอย่างน่าประหลาดใจที่จะทำงานอย่างถูกต้อง - สองครั้งดังนั้นหากชื่อใด ๆ ที่เป็น symlink; เราทั้งคู่ทำการวิเคราะห์ด้วยข้อความล้วนๆ
Jonathan Leffler

@Dennis: แน่นอนว่าถ้าคุณไม่มีเครือข่าย Newcastle Connection การพยายามที่จะหยั่งรากได้ก็จะไร้ประโยชน์ดังนั้น .. /../../ .. และ ../../ .. จะเทียบเท่ากัน อย่างไรก็ตามนั่นคือการหนีที่บริสุทธิ์ คำติชมของคุณถูกต้อง (Newcastle Connection อนุญาตให้คุณกำหนดค่าและใช้เครื่องหมาย /../host/path/on/remote/machine เพื่อไปยังโฮสต์อื่น - แบบแผนฉันเชื่อว่ามันรองรับ /../../network/host/ เส้นทาง / บน / ระยะไกล / เครือข่าย / และ / โฮสต์ด้วยอยู่ใน Wikipedia.)
Jonathan Leffler

ดังนั้นตอนนี้เรามีเครื่องหมายทับสองอันของ UNC
หยุดชั่วคราวจนกว่าจะมีการแจ้งให้ทราบต่อไป

1
ยูทิลิตี้ "readlink" (อย่างน้อยรุ่น GNU) สามารถทำเทียบเท่า realpath () หากคุณผ่านตัวเลือก "-f" ตัวอย่างเช่นในระบบของฉันreadlink /usr/bin/viให้/etc/alternatives/viแต่เป็น symlink อื่น - ในขณะที่readlink -f /usr/bin/viให้/usr/bin/vim.basicซึ่งเป็นปลายทางที่ดีที่สุดของsymlink ทั้งหมด ...
psmears

3

ฉันใช้คำถามของคุณเป็นความท้าทายที่จะเขียนในรหัสเปลือก "พกพา" เช่น

  • โดยคำนึงถึงเชลล์ POSIX
  • ไม่มีการทุบตีเช่นอาร์เรย์
  • หลีกเลี่ยงการโทรจากภายนอกเช่นโรคระบาด ไม่มีทางแยกเดียวในสคริปต์! ทำให้รวดเร็วอย่างเห็นได้ชัดโดยเฉพาะอย่างยิ่งในระบบที่มีค่าใช้จ่ายทางแยกที่สำคัญเช่น cygwin
  • ต้องจัดการกับตัวอักษรแบบกลมในชื่อพา ธ (*,?, [,])

มันทำงานบนเปลือกใด ๆ ที่สอดคล้อง 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 :

2

โซลูชันของฉัน:

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

}

นี่คือ Portable Extrememly :)
ไม่ระบุตัวตน

2

นี่คือรุ่นของฉัน มันขึ้นอยู่กับคำตอบโดย@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
}

1

เดาสิว่าคนนี้จะทำเคล็ดลับด้วย ...

ตกลงค่าใช้จ่ายคาดหวัง แต่เรากำลังทำเชลล์เป้าหมายที่นี่! ;)

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

1

สคริปต์นี้ใช้ได้กับชื่อพา ธ เท่านั้น ไม่จำเป็นต้องมีไฟล์ใด ๆ อยู่ หากเส้นทางที่ผ่านมาไม่สมบูรณ์พฤติกรรมจะผิดปกติเล็กน้อย แต่ควรทำงานตามที่คาดไว้หากเส้นทางทั้งสองสัมพันธ์กัน

ฉันทดสอบเฉพาะบน 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

นอกจากนี้โค้ดยังเป็นสำเนา & paste-ish เล็กน้อย แต่ฉันต้องการมันค่อนข้างเร็ว
juancn

ดี ... แค่สิ่งที่ฉันต้องการ และคุณได้รวมกิจวัตรประจำวันที่ดีกว่าคนอื่น ๆ ที่ฉันเคยเห็น (ซึ่งมักจะพึ่งพาการเปลี่ยน regex)
drwatsoncode

0

คำตอบนี้ไม่ได้อยู่ส่วน Bash ของคำถาม แต่เพราะฉันพยายามใช้คำตอบในคำถามนี้เพื่อใช้ฟังก์ชั่นนี้ในEmacsฉันจะทิ้งมันไว้ที่นั่น

Emacs มีฟังก์ชั่นนี้นอกกรอบ:

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

โปรดทราบว่าฉันเชื่อว่าคำตอบของหลามที่ฉันได้เพิ่มเมื่อเร็ว ๆ นี้ ( relpathฟังก์ชั่น) ทำงานเหมือนกันfile-relative-nameสำหรับกรณีทดสอบที่คุณให้ไว้
Gary Wisniewski

-1

นี่คือเชลล์สคริปต์ที่ทำงานได้โดยไม่ต้องเรียกโปรแกรมอื่น ๆ :

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


1
สิ่งนี้ไม่สามารถเคลื่อนย้ายได้จริงเนื่องจากการใช้โครงสร้าง $ {param / pattern / subst} ซึ่งไม่ใช่ POSIX (ตั้งแต่ปี 2011)
Jens

แหล่งอ้างอิงynform.org/w/Pub/Relpathชี้ไปที่หน้าวิกิพีเดียที่อ่านไม่ออกซึ่งมีเนื้อหาของสคริปต์หลายครั้งรวมกับบรรทัด vi tilde ข้อความแสดงข้อผิดพลาดเกี่ยวกับคำสั่งที่ไม่พบและอะไรก็ตาม ไร้ประโยชน์อย่างมากสำหรับใครบางคนค้นคว้าต้นฉบับ
Jens

-1

ฉันต้องการสิ่งนี้ แต่แก้ไขลิงก์สัญลักษณ์ด้วย ฉันค้นพบว่า 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"
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.