Chuyển đổi đường dẫn tuyệt đối thành đường dẫn tương đối được cung cấp một thư mục hiện tại bằng Bash


261

Thí dụ:

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

# Magic

relative="../../bar"

Làm cách nào để tạo ra phép thuật (hy vọng mã không quá phức tạp ...)?


7
Ví dụ (trường hợp của tôi ngay bây giờ) để cung cấp đường dẫn tương đối gcc để nó có thể tạo ra các bản sửa lỗi tương đối có thể sử dụng được ngay cả khi đường dẫn nguồn thay đổi.
Offirmo

Một câu hỏi tương tự như câu hỏi này đã được hỏi trên U & L: unix.stackexchange.com/questions/100918/ . Một trong những câu trả lời (@Gilles) đề cập đến một công cụ, liên kết tượng trưng , có thể giúp giải quyết vấn đề này dễ dàng.
slm

25
Đơn giản : realpath --relative-to=$absolute $current.
kenorb

Câu trả lời:


229

Sử dụng realpath từ GNU coreutils 8.23 ​​là đơn giản nhất, tôi nghĩ:

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

Ví dụ:

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

7
Thật đáng tiếc khi gói đã lỗi thời trên Ubuntu 14.04 và không có tùy chọn - liên quan đến.
kzh

3
Hoạt động tốt trên Ubuntu 16.04
cayhorstmann

7
$ realpath --relative-to="${PWD}" "$file"là hữu ích nếu bạn muốn các đường dẫn liên quan đến thư mục làm việc hiện tại.
dcoles

1
Điều này đúng cho những thứ bên trong /usr/bin/nmap/-path nhưng không phải cho /usr/bin/nmap: từ nmapđến /tmp/testingnó chỉ ../../và không 3 lần ../. Nó hoạt động tuy nhiên, bởi vì làm ..trên rootfs là /.
Patrick B.

6
Như @PatrickB. ngụ ý, --relative-to=…mong đợi một thư mục và KHÔNG kiểm tra. Điều đó có nghĩa rằng bạn kết thúc với một thêm "../" nếu bạn yêu cầu một đường dẫn tương đối đến một tập tin (như ví dụ này dường như làm, bởi vì /usr/binhiếm khi hoặc không bao giờ chứa các thư mục và nmapthường là một nhị phân)
IBBoard

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

cho:

../../bar

11
Nó hoạt động, và nó làm cho các lựa chọn thay thế trông vô lý. Đó là phần thưởng cho tôi xD
hasvn

31
+1. Ok, bạn đã lừa dối ... nhưng điều này là quá tốt để không được sử dụng! relpath(){ python -c "import os.path; print os.path.relpath('$1','${2:-$PWD}')" ; }
MestreLion

4
Đáng buồn thay, điều này không có sẵn trên toàn cầu: os.path.relpath là mới trong Python 2.6.
Chen Levy

15
@ChenLevy: Python 2.6 được phát hành vào năm 2008. Khó có thể tin rằng nó không có sẵn trên toàn cầu vào năm 2012.
MestreLion

11
python -c 'import os, sys; print(os.path.relpath(*sys.argv[1:]))'hoạt động tự nhiên và đáng tin cậy nhất.
musiphil

31

Đây là một cải tiến đầy đủ chức năng của giải pháp được đánh giá tốt nhất hiện tại từ @pini (đáng buồn chỉ xử lý một vài trường hợp)

Nhắc nhở: '-z' kiểm tra nếu chuỗi có độ dài bằng không (= trống) và kiểm tra '-n' nếu chuỗi không trống.

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

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

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

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

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

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

echo $result

Các trường hợp thử nghiệm:

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

1
Được tích hợp trong shell offifyo lib github.com/ Offerirmo/offirmo-shell-lib , chức năng «OSL_FILE_find_relative_path» (tệp «osl_lib_file.sh»)
Offifyo

1
+1. Nó có thể dễ dàng được thực hiện để xử lý bất kỳ đường dẫn nào (không chỉ là đường dẫn tuyệt đối bắt đầu bằng /) bằng cách thay thế source=$1; target=$2bằngsource=$(realpath $1); target=$(realpath $2)
Josh Kelley

2
@Josh thực sự, với điều kiện là các thư mục thực sự tồn tại ... không thuận tiện cho các bài kiểm tra đơn vị;) Nhưng trong thực tế sử dụng có, realpathđược khuyến nghị, hoặc source=$(readlink -f $1)vv nếu không có sẵn (không chuẩn)
Offifyo

Tôi đã định nghĩa $source$targetnhư thế này: `if [[-e $ 1]]; sau đó nguồn = $ (readlink -f $ 1); nguồn khác = $ 1; fi nếu [[-e $ 2]]; sau đó đích = $ (readlink -f $ 2); mục tiêu khác = $ 2; Theo cách đó, hàm có thể làm tăng các đường dẫn thực / tồn tại cũng như các thư mục hư cấu.
Nathan S. Watson-Haigh

1
@ NathanS.Watson-Haigh Thậm chí tốt hơn, tôi phát hiện ra gần đây readlinkcó một -mtùy chọn chỉ thực hiện điều đó;)
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/}

Kịch bản tuyệt vời - ngắn gọn và sạch sẽ. Tôi đã áp dụng một chỉnh sửa (Chờ đánh giá ngang hàng): common_part = $ source / common_part = $ (dirname $ common_part) / echo $ {back} $ {target # $ common_part} Tập lệnh hiện tại sẽ không thành công do không phù hợp khi bắt đầu tên thư mục khi so sánh, ví dụ: "/ foo / bar / baz" với "/ foo / barucks / bonk". Di chuyển dấu gạch chéo vào var và ra khỏi eval cuối cùng sửa lỗi đó.
jcwenger

3
Kịch bản này đơn giản là không hoạt động. Thất bại một bài kiểm tra "một thư mục xuống" đơn giản. Các chỉnh sửa của jcwenger hoạt động tốt hơn một chút nhưng có xu hướng thêm "../".
Tiến sĩ Person Person II

1
nó không thành công đối với tôi trong một số trường hợp nếu dấu "/" nằm trong đối số; ví dụ: nếu $ 1 = "$ HOME /" và $ 2 = "$ HOME / temp", nó sẽ trả về "/ home / user / temp /", nhưng nếu $ 1 = $ HOME thì nó trả về đúng đường dẫn "temp". Do đó, cả nguồn = $ 1 và đích = $ 2 đều có thể được "xóa" bằng cách sử dụng sed (hoặc sử dụng thay thế biến bash, nhưng điều đó có thể bị mờ một cách không cần thiết), chẳng hạn như => source = $ (echo "$ {1}" | sed 's / \ / * $ // ')
michael

1
Cải thiện nhỏ: Thay vì đặt trực tiếp nguồn / mục tiêu thành $ 1 và $ 2, hãy thực hiện: source = $ (cd $ 1; pwd) target = $ (cd $ 2; pwd). Bằng cách này, nó xử lý các đường dẫn với. và .. chính xác.
Joseph Garvin

4
Mặc dù là câu trả lời được bình chọn hàng đầu, câu trả lời này có rất nhiều hạn chế, do đó rất nhiều câu trả lời khác được đăng. Thay vào đó, hãy xem các câu trả lời khác, đặc biệt là câu trả lời hiển thị trường hợp kiểm tra. Và xin vui lòng upvote bình luận này!
Offirmo

25

Nó được tích hợp vào Perl từ năm 2001, vì vậy nó hoạt động trên hầu hết mọi hệ thống mà bạn có thể tưởng tượng, thậm chí là VMS .

perl -e 'use File::Spec; print File::Spec->abs2rel(@ARGV) . "\n"' FILE BASE

Ngoài ra, giải pháp rất dễ hiểu.

Vì vậy, ví dụ của bạn:

perl -e 'use File::Spec; print File::Spec->abs2rel(@ARGV) . "\n"' $absolute $current

... sẽ hoạt động tốt.


3
sayđã không có sẵn trong perl cho dưới dạng nhật ký, nhưng nó có thể được sử dụng hiệu quả ở đây. perl -MFile::Spec -E 'say File::Spec->abs2rel(@ARGV)'
William Pursell

+1 nhưng cũng thấy câu trả lời tương tự cũ hơn (tháng 2 năm 2012). Đọc những bình luận thích hợp từ William Pursell . Phiên bản của tôi là hai dòng lệnh: perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target"perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target" "$origin". Tập lệnh perl một dòng đầu tiên sử dụng một đối số (gốc là thư mục làm việc hiện tại). Kịch bản perl một dòng thứ hai sử dụng hai đối số.
olibre

3
Đó nên là câu trả lời được chấp nhận. perlcó thể được tìm thấy ở hầu hết mọi nơi, mặc dù câu trả lời vẫn là một lớp lót.
Dmitry Ginzburg

19

Giả sử bạn đã cài đặt: bash, pwd, dirname, echo; sau đó relpath là

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

Tôi đã đánh golf câu trả lời từ pini và một vài ý tưởng khác

Lưu ý : Điều này đòi hỏi cả hai đường dẫn là các thư mục hiện có. Các tập tin sẽ không hoạt động.


2
câu trả lời lý tưởng: hoạt động với / bin / sh, không yêu cầu readlink, python, perl -> tuyệt vời cho hệ thống ánh sáng / nhúng hoặc bảng điều khiển bash windows
Francois

2
Thật không may, điều này đòi hỏi rằng con đường tồn tại, không phải lúc nào cũng mong muốn.
drwatson

Thần trả lời. Tôi đoán công cụ cd-pwd là để giải quyết các liên kết? Chơi golf tốt
Teck-freak

15

Python os.path.relpathlà một hàm shell

Mục tiêu của relpathbài tập này là bắt chước os.path.relpathchức năng của Python 2.7 (có sẵn từ phiên bản Python 2.6 nhưng chỉ hoạt động đúng trong 2.7), như đề xuất của xni . Do đó, một số kết quả có thể khác với các chức năng được cung cấp trong các câu trả lời khác.

(Tôi chưa thử nghiệm các dòng mới trong các đường dẫn đơn giản vì nó phá vỡ xác nhận dựa trên cuộc gọi python -ctừ ZSH. Chắc chắn sẽ có thể với một số nỗ lực.)

Về ma thuật của người Hồi giáo ở Bash, tôi đã từ bỏ việc tìm kiếm ma thuật ở Bash từ lâu, nhưng tôi đã tìm thấy tất cả ma thuật tôi cần, và sau đó là một số, trong ZSH.

Do đó, tôi đề xuất hai triển khai.

Việc thực hiện đầu tiên nhằm hoàn toàn tuân thủ POSIX . Tôi đã thử nghiệm nó với /bin/dashDebian 6.0.6. Nó cũng hoạt động hoàn hảo với /bin/shtrên OS X 10.8.3, thực tế là phiên bản Bash 3.2 giả vờ là vỏ POSIX.

Việc thực hiện thứ hai là một hàm shell ZSH mạnh mẽ chống lại nhiều dấu gạch chéo và các phiền toái khác trong các đường dẫn. Nếu bạn có sẵn ZSH, đây là phiên bản được đề xuất, ngay cả khi bạn đang gọi nó ở dạng tập lệnh được trình bày bên dưới (nghĩa là với một shebang #!/usr/bin/env zsh) từ một shell khác.

Cuối cùng, tôi đã viết một tập lệnh ZSH để xác minh đầu ra của relpathlệnh được tìm thấy trong $PATHcác trường hợp thử nghiệm được cung cấp trong các câu trả lời khác. Tôi đã thêm một số gia vị vào các thử nghiệm đó bằng cách thêm một số khoảng trắng, tab và dấu câu như ! ? *ở đây và ở đó và cũng đã ném vào một thử nghiệm khác với các ký tự UTF-8 kỳ lạ được tìm thấy trong vim-powerline .

Chức năng vỏ POSIX

Đầu tiên, chức năng shell tuân thủ POSIX. Nó hoạt động với nhiều đường dẫn khác nhau, nhưng không làm sạch nhiều dấu gạch chéo hoặc giải quyết các liên kết tượng trưng.

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

Hàm shell ZSH

Bây giờ, zshphiên bản mạnh mẽ hơn . Nếu bạn muốn nó giải quyết các đối số thành các đường dẫn thực sự realpath -f(có sẵn trong coreutilsgói Linux ), hãy thay thế các :adòng 3 và 4 bằng :A.

Để sử dụng điều này trong zsh, loại bỏ dòng đầu tiên và cuối cùng và đặt nó trong một thư mục nằm trong $FPATHbiến của bạn .

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

Kịch bản kiểm tra

Cuối cùng là kịch bản thử nghiệm. Nó chấp nhận một tùy chọn, cụ thể là -vđể cho phép đầu ra dài dòng.

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

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

relpath_check () {
    [[ $# -ge 1 ]] && [[ $# -le 2 ]] || return 1
    target=${${2:-$1}}
    prefix=${${${2:+$1}:-$PWD}}
    result=$(relpath $prefix $target)
    # Compare with python's os.path.relpath function
    py_result=$(python -c "import os.path; print os.path.relpath('$target', '$prefix')")
    col='%F{green}'
    if [[ $result != $py_result ]] && col='%F{red}' || $VERBOSE; then
        print -P "${col}Source: '$prefix'\nDestination: '$target'%f"
        print -P "${col}relpath: ${(qq)result}%f"
        print -P "${col}python:  ${(qq)py_result}%f\n"
    fi
}

run_checks () {
    print "Running checks..."

    relpath_check '/    a   b/å/⮀*/!' '/    a   b/å/⮀/xäå/?'

    relpath_check '/'  '/A'
    relpath_check '/A'  '/'
    relpath_check '/  & /  !/*/\\/E' '/'
    relpath_check '/' '/  & /  !/*/\\/E'
    relpath_check '/  & /  !/*/\\/E' '/  & /  !/?/\\/E/F'
    relpath_check '/X/Y' '/  & /  !/C/\\/E/F'
    relpath_check '/  & /  !/C' '/A'
    relpath_check '/A /  !/C' '/A /B'
    relpath_check '/Â/  !/C' '/Â/  !/C'
    relpath_check '/  & /B / C' '/  & /B / C/D'
    relpath_check '/  & /  !/C' '/  & /  !/C/\\/Ê'
    relpath_check '/Å/  !/C' '/Å/  !/D'
    relpath_check '/.A /*B/C' '/.A /*B/\\/E'
    relpath_check '/  & /  !/C' '/  & /D'
    relpath_check '/  & /  !/C' '/  & /\\/E'
    relpath_check '/  & /  !/C' '/\\/E/F'

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

    print "Done with checks."
}
if [[ $# -gt 0 ]] && [[ $1 = "-v" ]]; then
    VERBOSE=true
    shift
fi
if [[ $# -eq 0 ]]; then
    run_checks
else
    VERBOSE=true
    relpath_check "$@"
fi

2
Không hoạt động khi con đường đầu tiên kết thúc trong /tôi sợ.
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")"; }

Kịch bản shell trên được lấy cảm hứng từ pini (Cảm ơn!). Nó gây ra lỗi trong mô-đun tô sáng cú pháp của Stack Overflow (ít nhất là trong khung xem trước của tôi). Vì vậy, xin vui lòng bỏ qua nếu đánh dấu là không chính xác.

Một số lưu ý:

  • Đã xóa lỗi và cải thiện mã mà không làm tăng đáng kể độ dài và độ phức tạp của mã
  • Đặt chức năng vào các chức năng để dễ sử dụng
  • Giữ các chức năng tương thích POSIX để chúng (nên) hoạt động với tất cả các vỏ POSIX (được thử nghiệm với dấu gạch ngang, bash và zsh trong Ubuntu Linux 12.04)
  • Chỉ sử dụng các biến cục bộ để tránh ghi đè các biến toàn cục và gây ô nhiễm không gian tên toàn cầu
  • Cả hai đường dẫn thư mục KHÔNG cần tồn tại (yêu cầu cho ứng dụng của tôi)
  • Tên đường dẫn có thể chứa khoảng trắng, ký tự đặc biệt, ký tự điều khiển, dấu gạch chéo ngược, tab, ', ",?, *, [,], V.v.
  • Hàm lõi "relPath" chỉ sử dụng các hàm dựng sẵn POSIX nhưng yêu cầu đường dẫn thư mục tuyệt đối chính tắc làm tham số
  • Hàm mở rộng "relpath" có thể xử lý các đường dẫn thư mục tùy ý (cũng tương đối, không chính tắc) nhưng yêu cầu tiện ích lõi GNU bên ngoài "readlink"
  • Đã tránh dựng sẵn "echo" và sử dụng "printf" dựng sẵn thay vì hai lý do:
  • Để tránh các chuyển đổi không cần thiết, tên đường dẫn được sử dụng khi chúng được trả về và mong đợi bởi các tiện ích shell và OS (ví dụ: cd, ln, ls, find, mkdir; không giống như "os.path.relpath" của python sẽ diễn giải một số chuỗi dấu gạch chéo ngược)
  • Ngoại trừ các chuỗi dấu gạch chéo ngược được đề cập, dòng cuối cùng của hàm "relPath" xuất ra các tên đường dẫn tương thích với python:

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

    Dòng cuối cùng có thể được thay thế (và đơn giản hóa) bằng dòng

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

    Tôi thích cái sau bởi vì

    1. Tên tệp có thể được thêm trực tiếp vào đường dẫn dir thu được bởi relPath, ví dụ:

      ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
    2. Các liên kết tượng trưng trong cùng một thư mục được tạo bằng phương thức này không có tên xấu được "./"thêm vào tên tệp.

  • Nếu bạn tìm thấy một lỗi, xin vui lòng liên hệ với linuxball (at) gmail.com và tôi sẽ cố gắng sửa nó.
  • Đã thêm bộ kiểm tra hồi quy (cũng tương thích với vỏ POSIX)

Danh sách mã cho các bài kiểm tra hồi quy (chỉ cần thêm nó vào tập lệnh shell):

############################################################################
# If called with 2 arguments assume they are dir paths and print rel. path #
############################################################################

test "$#" = 2 && {
    printf '%s\n' "Rel. path from '$1' to '$2' is '$(relpath "$1" "$2")'."
    exit 0
}

#######################################################
# If NOT called with 2 arguments run regression tests #
#######################################################

format="\t%-19s %-22s %-27s %-8s %-8s %-8s\n"
printf \
"\n\n*** Testing own and python's function with canonical absolute dirs\n\n"
printf "$format\n" \
    "From Directory" "To Directory" "Rel. Path" "relPath" "relpath" "python"
IFS=
while read -r p; do
    eval set -- $p
    case $1 in '#'*|'') continue;; esac # Skip comments and empty lines
    # q stores quoting character, use " if ' is used in path name
    q="'"; case $1$2 in *"'"*) q='"';; esac
    rPOk=passed rP=$(relPath "$1" "$2"); test "$rP" = "$3" || rPOk=$rP
    rpOk=passed rp=$(relpath "$1" "$2"); test "$rp" = "$3" || rpOk=$rp
    RPOk=passed
    RP=$(python -c "import os.path; print os.path.relpath($q$2$q, $q$1$q)")
    test "$RP" = "$3" || RPOk=$RP
    printf \
    "$format" "$q$1$q" "$q$2$q" "$q$3$q" "$q$rPOk$q" "$q$rpOk$q" "$q$RPOk$q"
done <<-"EOF"
    # From directory    To directory           Expected relative path

    '/'                 '/'                    '.'
    '/usr'              '/'                    '..'
    '/usr/'             '/'                    '..'
    '/'                 '/usr'                 'usr'
    '/'                 '/usr/'                'usr'
    '/usr'              '/usr'                 '.'
    '/usr/'             '/usr'                 '.'
    '/usr'              '/usr/'                '.'
    '/usr/'             '/usr/'                '.'
    '/u'                '/usr'                 '../usr'
    '/usr'              '/u'                   '../u'
    "/u'/dir"           "/u'/dir"              "."
    "/u'"               "/u'/dir"              "dir"
    "/u'/dir"           "/u'"                  ".."
    "/"                 "/u'/dir"              "u'/dir"
    "/u'/dir"           "/"                    "../.."
    "/u'"               "/u'"                  "."
    "/"                 "/u'"                  "u'"
    "/u'"               "/"                    ".."
    '/u"/dir'           '/u"/dir'              '.'
    '/u"'               '/u"/dir'              'dir'
    '/u"/dir'           '/u"'                  '..'
    '/'                 '/u"/dir'              'u"/dir'
    '/u"/dir'           '/'                    '../..'
    '/u"'               '/u"'                  '.'
    '/'                 '/u"'                  'u"'
    '/u"'               '/'                    '..'
    '/u /dir'           '/u /dir'              '.'
    '/u '               '/u /dir'              'dir'
    '/u /dir'           '/u '                  '..'
    '/'                 '/u /dir'              'u /dir'
    '/u /dir'           '/'                    '../..'
    '/u '               '/u '                  '.'
    '/'                 '/u '                  'u '
    '/u '               '/'                    '..'
    '/u\n/dir'          '/u\n/dir'             '.'
    '/u\n'              '/u\n/dir'             'dir'
    '/u\n/dir'          '/u\n'                 '..'
    '/'                 '/u\n/dir'             'u\n/dir'
    '/u\n/dir'          '/'                    '../..'
    '/u\n'              '/u\n'                 '.'
    '/'                 '/u\n'                 'u\n'
    '/u\n'              '/'                    '..'

    '/    a   b/å/⮀*/!' '/    a   b/å/⮀/xäå/?' '../../⮀/xäå/?'
    '/'                 '/A'                   'A'
    '/A'                '/'                    '..'
    '/  & /  !/*/\\/E'  '/'                    '../../../../..'
    '/'                 '/  & /  !/*/\\/E'     '  & /  !/*/\\/E'
    '/  & /  !/*/\\/E'  '/  & /  !/?/\\/E/F'   '../../../?/\\/E/F'
    '/X/Y'              '/  & /  !/C/\\/E/F'   '../../  & /  !/C/\\/E/F'
    '/  & /  !/C'       '/A'                   '../../../A'
    '/A /  !/C'         '/A /B'                '../../B'
    '/Â/  !/C'          '/Â/  !/C'             '.'
    '/  & /B / C'       '/  & /B / C/D'        'D'
    '/  & /  !/C'       '/  & /  !/C/\\/Ê'     '\\/Ê'
    '/Å/  !/C'          '/Å/  !/D'             '../D'
    '/.A /*B/C'         '/.A /*B/\\/E'         '../\\/E'
    '/  & /  !/C'       '/  & /D'              '../../D'
    '/  & /  !/C'       '/  & /\\/E'           '../../\\/E'
    '/  & /  !/C'       '/\\/E/F'              '../../../\\/E/F'
    '/home/p1/p2'       '/home/p1/p3'          '../p3'
    '/home/p1/p2'       '/home/p4/p5'          '../../p4/p5'
    '/home/p1/p2'       '/work/p6/p7'          '../../../work/p6/p7'
    '/home/p1'          '/work/p1/p2/p3/p4'    '../../work/p1/p2/p3/p4'
    '/home'             '/work/p2/p3'          '../work/p2/p3'
    '/'                 '/work/p2/p3/p4'       'work/p2/p3/p4'
    '/home/p1/p2'       '/home/p1/p2/p3/p4'    'p3/p4'
    '/home/p1/p2'       '/home/p1/p2/p3'       'p3'
    '/home/p1/p2'       '/home/p1/p2'          '.'
    '/home/p1/p2'       '/home/p1'             '..'
    '/home/p1/p2'       '/home'                '../..'
    '/home/p1/p2'       '/'                    '../../..'
    '/home/p1/p2'       '/work'                '../../../work'
    '/home/p1/p2'       '/work/p1'             '../../../work/p1'
    '/home/p1/p2'       '/work/p1/p2'          '../../../work/p1/p2'
    '/home/p1/p2'       '/work/p1/p2/p3'       '../../../work/p1/p2/p3'
    '/home/p1/p2'       '/work/p1/p2/p3/p4'    '../../../work/p1/p2/p3/p4'

    '/-'                '/-'                   '.'
    '/?'                '/?'                   '.'
    '/??'               '/??'                  '.'
    '/???'              '/???'                 '.'
    '/?*'               '/?*'                  '.'
    '/*'                '/*'                   '.'
    '/*'                '/**'                  '../**'
    '/*'                '/***'                 '../***'
    '/*.*'              '/*.**'                '../*.**'
    '/*.???'            '/*.??'                '../*.??'
    '/[]'               '/[]'                  '.'
    '/[a-z]*'           '/[0-9]*'              '../[0-9]*'
EOF


format="\t%-19s %-22s %-27s %-8s %-8s\n"
printf "\n\n*** Testing own and python's function with arbitrary dirs\n\n"
printf "$format\n" \
    "From Directory" "To Directory" "Rel. Path" "relpath" "python"
IFS=
while read -r p; do
    eval set -- $p
    case $1 in '#'*|'') continue;; esac # Skip comments and empty lines
    # q stores quoting character, use " if ' is used in path name
    q="'"; case $1$2 in *"'"*) q='"';; esac
    rpOk=passed rp=$(relpath "$1" "$2"); test "$rp" = "$3" || rpOk=$rp
    RPOk=passed
    RP=$(python -c "import os.path; print os.path.relpath($q$2$q, $q$1$q)")
    test "$RP" = "$3" || RPOk=$RP
    printf "$format" "$q$1$q" "$q$2$q" "$q$3$q" "$q$rpOk$q" "$q$RPOk$q"
done <<-"EOF"
    # From directory    To directory           Expected relative path

    'usr/p1/..//./p4'   'p3/../p1/p6/.././/p2' '../../p1/p2'
    './home/../../work' '..//././../dir///'    '../../dir'

    'home/p1/p2'        'home/p1/p3'           '../p3'
    'home/p1/p2'        'home/p4/p5'           '../../p4/p5'
    'home/p1/p2'        'work/p6/p7'           '../../../work/p6/p7'
    'home/p1'           'work/p1/p2/p3/p4'     '../../work/p1/p2/p3/p4'
    'home'              'work/p2/p3'           '../work/p2/p3'
    '.'                 'work/p2/p3'           'work/p2/p3'
    'home/p1/p2'        'home/p1/p2/p3/p4'     'p3/p4'
    'home/p1/p2'        'home/p1/p2/p3'        'p3'
    'home/p1/p2'        'home/p1/p2'           '.'
    'home/p1/p2'        'home/p1'              '..'
    'home/p1/p2'        'home'                 '../..'
    'home/p1/p2'        '.'                    '../../..'
    'home/p1/p2'        'work'                 '../../../work'
    'home/p1/p2'        'work/p1'              '../../../work/p1'
    'home/p1/p2'        'work/p1/p2'           '../../../work/p1/p2'
    'home/p1/p2'        'work/p1/p2/p3'        '../../../work/p1/p2/p3'
    'home/p1/p2'        'work/p1/p2/p3/p4'     '../../../work/p1/p2/p3/p4'
EOF

9

Không có nhiều câu trả lời ở đây là thiết thực cho việc sử dụng hàng ngày. Vì rất khó để thực hiện điều này đúng cách trong bash thuần túy, tôi đề xuất giải pháp đáng tin cậy sau đây (tương tự như một đề xuất được chôn trong một nhận xét):

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

Sau đó, bạn có thể nhận được đường dẫn tương đối dựa trên thư mục hiện tại:

echo $(relpath somepath)

hoặc bạn có thể chỉ định rằng đường dẫn có liên quan đến một thư mục đã cho:

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

Một nhược điểm là điều này đòi hỏi python, nhưng:

  • Nó hoạt động giống hệt nhau trong mọi python> = 2.6
  • Nó không yêu cầu các tập tin hoặc thư mục tồn tại.
  • Tên tệp có thể chứa một phạm vi rộng hơn của các ký tự đặc biệt. Ví dụ: nhiều giải pháp khác không hoạt động nếu tên tệp chứa khoảng trắng hoặc các ký tự đặc biệt khác.
  • Đây là một hàm một dòng không làm lộn xộn các tập lệnh.

Lưu ý rằng các giải pháp bao gồm basenamehoặc dirnamecó thể không nhất thiết phải tốt hơn, vì chúng yêu cầu phải coreutilsđược cài đặt. Nếu ai đó có một bashgiải pháp thuần túy đáng tin cậy và đơn giản (thay vì tò mò phức tạp), tôi sẽ ngạc nhiên.


Điều này có vẻ như cho đến nay cách tiếp cận mạnh mẽ nhất.
dimo414

7

Tập lệnh này chỉ cung cấp kết quả chính xác cho các đầu vào là đường dẫn tuyệt đối hoặc đường dẫn tương đối không có .hoặc ..:

#!/bin/bash

# usage: relpath from to

if [[ "$1" == "$2" ]]
then
    echo "."
    exit
fi

IFS="/"

current=($1)
absolute=($2)

abssize=${#absolute[@]}
cursize=${#current[@]}

while [[ ${absolute[level]} == ${current[level]} ]]
do
    (( level++ ))
    if (( level > abssize || level > cursize ))
    then
        break
    fi
done

for ((i = level; i < cursize; i++))
do
    if ((i > level))
    then
        newpath=$newpath"/"
    fi
    newpath=$newpath".."
done

for ((i = level; i < abssize; i++))
do
    if [[ -n $newpath ]]
    then
        newpath=$newpath"/"
    fi
    newpath=$newpath${absolute[i]}
done

echo "$newpath"

1
Điều này xuất hiện để làm việc. Nếu các thư mục thực sự tồn tại, sử dụng $ (readlink -f $ 1) và $ (readlink -f $ 2) trên các đầu vào có thể khắc phục sự cố trong đó "." hoặc ".." xuất hiện trong các đầu vào. Điều này có thể gây ra một số rắc rối nếu các thư mục không thực sự tồn tại.
Tiến sĩ Person Person II

7

Tôi sẽ chỉ sử dụng Perl cho nhiệm vụ không hề nhỏ này:

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

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

1
+1, nhưng sẽ khuyến nghị: perl -MFile::Spec -e "print File::Spec->abs2rel('$absolute','$current')"sao cho tuyệt đối và hiện tại được trích dẫn.
William Pursell

Tôi thích relative=$(perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$absolute" "$current"). Điều này đảm bảo các giá trị không thể, chính chúng, chứa mã perl!
Erik Aronesty

6

Một cải tiến nhỏ về câu trả lời của kaskuPini , chơi đẹp hơn với không gian và cho phép vượt qua các đường dẫn tương đối:

#!/bin/bash
# both $1 and $2 are paths
# returns $2 relative to $1
absolute=`readlink -f "$2"`
current=`readlink -f "$1"`
# Perl is magic
# Quoting horror.... spaces cause problems, that's why we need the extra " in here:
relative=$(perl -MFile::Spec -e "print File::Spec->abs2rel(q($absolute),q($current))")

echo $relative

4

kiểm tra

#!/bin/bash                                                                 

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

Kiểm tra:

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

+1 cho sự gọn nhẹ và bash-ness. Bạn nên, tuy nhiên, cũng gọi readlinkvào $(pwd).
DevSolar

2
Tương đối không có nghĩa là tập tin phải được đặt trong cùng một thư mục.
greenoldman

mặc dù câu hỏi ban đầu không cung cấp nhiều câu hỏi thử nghiệm, tập lệnh này không thành công cho các bài kiểm tra đơn giản như tìm đường dẫn tương đối từ / home / user1 đến / home / user2 (câu trả lời đúng: ../user2). Kịch bản của pini / jcwenger hoạt động cho trường hợp này.
michael

4

Một giải pháp khác, thuần bash+ GNU readlinkđể dễ sử dụng trong ngữ cảnh sau:

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

Chỉnh sửa: Đảm bảo rằng "$ B" không tồn tại hoặc không có relpathliên kết mềm trong trường hợp đó, nếu không thì theo liên kết này không phải là điều bạn muốn!

Điều này hoạt động trong gần như tất cả các Linux hiện tại. Nếu readlink -mkhông làm việc ở bên cạnh bạn, hãy thử readlink -fthay thế. Xem thêm https://gist.github.com/hilbix/1ec361d00a8178ae8ea0 để biết các cập nhật có thể:

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

Ghi chú:

  • Cẩn thận rằng nó an toàn trước việc mở rộng ký tự meta shell không mong muốn, trong trường hợp tên tệp chứa *hoặc ?.
  • Đầu ra có nghĩa là có thể sử dụng làm đối số đầu tiên cho ln -s:
    • relpath / /cho .và không phải chuỗi rỗng
    • relpath a acho a, ngay cả khi atình cờ là một thư mục
  • Hầu hết các trường hợp phổ biến đã được thử nghiệm để cho kết quả hợp lý, quá.
  • Giải pháp này sử dụng kết hợp tiền tố chuỗi, do đó readlink được yêu cầu để chuẩn hóa các đường dẫn.
  • Nhờ readlink -mnó hoạt động cho các đường dẫn chưa có, quá.

Trên các hệ thống cũ, nơi readlink -mkhông có sẵn, readlink -fsẽ thất bại nếu tệp không tồn tại. Vì vậy, bạn có thể cần một số cách giải quyết như thế này (chưa được kiểm tra!):

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

Điều này không thực sự hoàn toàn chính xác trong trường hợp $1bao gồm .hoặc ..cho các đường dẫn chưa có (như trong /doesnotexist/./a), nhưng nó sẽ bao gồm hầu hết các trường hợp.

(Thay thế readlink -m --ở trên bằngreadlink_missing .)

Chỉnh sửa vì downvote sau

Đây là một bài kiểm tra, rằng chức năng này, thực sự, là chính xác:

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

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

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

Bối rối? Vâng, đây là những kết quả chính xác ! Ngay cả khi bạn nghĩ rằng nó không phù hợp với câu hỏi, đây là bằng chứng điều này là chính xác:

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

Không còn nghi ngờ gì nữa, ../barlà đường dẫn tương đối chính xác và duy nhất của trang barnhìn từ trangmoo . Mọi thứ khác sẽ hoàn toàn sai.

Thật là tầm thường khi chấp nhận đầu ra cho câu hỏi mà dường như giả định, đó currentlà một thư mục:

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

Điều này trả về chính xác, những gì đã được yêu cầu.

Và trước khi bạn nhướn mày, đây là một biến thể phức tạp hơn một chút relpath(phát hiện ra sự khác biệt nhỏ), cũng sẽ hoạt động với URL - Cú pháp (vì vậy, một dấu vết /còn tồn tại, nhờ một số bash-magic):

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

Và đây là những kiểm tra chỉ để làm rõ: Nó thực sự hoạt động như đã nói.

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

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

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

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

Và đây là cách nó có thể được sử dụng để đưa ra kết quả mong muốn từ câu hỏi:

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

Nếu bạn tìm thấy một cái gì đó không hoạt động, xin vui lòng cho tôi biết trong các ý kiến ​​dưới đây. Cảm ơn.

Tái bút

Tại sao các đối số của relpath "đảo ngược" trái ngược với tất cả các câu trả lời khác ở đây?

Nếu bạn thay đổi

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

đến

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

sau đó bạn có thể bỏ tham số thứ 2 đi, sao cho BASE là thư mục hiện tại / URL / bất cứ thứ gì. Đó chỉ là nguyên tắc Unix, như thường lệ.

Nếu bạn không thích điều đó, vui lòng quay lại Windows. Cảm ơn.


3

Đáng buồn thay, câu trả lời của Mark Rushakoff (hiện đã bị xóa - nó đã tham chiếu mã từ đây ) dường như không hoạt động chính xác khi thích nghi với:

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

Suy nghĩ được nêu trong các bài bình luận có thể được tinh chỉnh để làm cho nó hoạt động chính xác cho hầu hết các trường hợp. Tôi sắp giả định rằng tập lệnh lấy một đối số nguồn (nơi bạn đang ở) và đối số đích (nơi bạn muốn đến) và cả hai đều là tên đường dẫn tuyệt đối hoặc cả hai đều là tương đối. Nếu một là tuyệt đối và tương đối khác, điều dễ nhất là tiền tố tên tương đối với thư mục làm việc hiện tại - nhưng mã dưới đây không làm điều đó.


Coi chừng

Mã dưới đây gần hoạt động chính xác, nhưng không hoàn toàn đúng.

  1. Có vấn đề được giải quyết trong các ý kiến ​​từ Dennis Williamson.
  2. Ngoài ra còn có một vấn đề là việc xử lý văn bản hoàn toàn các tên đường dẫn và bạn có thể bị rối loạn nghiêm trọng bởi các liên kết lạ.
  3. Mã không xử lý 'dấu chấm' đi lạc trong các đường dẫn như 'xyz/./pqr '.
  4. Mã không xử lý 'dấu chấm kép' đi lạc trong các đường dẫn như 'xyz/../pqr '.
  5. Một cách tầm thường: mã không loại bỏ hàng đầu ' ./' khỏi các đường dẫn.

Mã của Dennis tốt hơn vì nó sửa được 1 và 5 - nhưng có cùng các vấn đề 2, 3, 4. Sử dụng mã của Dennis (và bỏ phiếu trước mã này) vì điều đó.

(NB: POSIX cung cấp một cuộc gọi hệ thống realpath()giải quyết các tên đường dẫn để không còn các liên kết tượng trưng nào trong chúng. Áp dụng điều đó cho các tên đầu vào, và sau đó sử dụng mã của Dennis sẽ đưa ra câu trả lời chính xác mỗi lần. kết thúc tốt đẹp realpath()- tôi đã thực hiện nó - nhưng tôi không biết về một tiện ích tiêu chuẩn làm như vậy.)


Đối với điều này, tôi thấy Perl dễ sử dụng hơn shell, mặc dù bash có hỗ trợ tốt cho mảng và có lẽ cũng có thể làm điều này - tập thể dục cho người đọc. Vì vậy, đưa ra hai tên tương thích, chia chúng thành các thành phần:

  • Đặt đường dẫn tương đối thành trống.
  • Trong khi các thành phần là như nhau, bỏ qua tiếp theo.
  • Khi các thành phần tương ứng khác nhau hoặc không còn thành phần nào cho một đường dẫn:
  • Nếu không có thành phần nguồn nào còn lại và đường dẫn tương đối trống, hãy thêm "." để bắt đầu
  • Đối với mỗi thành phần nguồn còn lại, tiền tố đường dẫn tương đối với "../".
  • Nếu không có thành phần đích còn lại và đường dẫn tương đối trống, hãy thêm "." để bắt đầu
  • Đối với mỗi thành phần đích còn lại, thêm thành phần vào cuối đường dẫn sau dấu gạch chéo.

Như vậy:

#!/bin/perl -w

use strict;

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

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

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

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

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

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

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

Tập lệnh kiểm tra (dấu ngoặc vuông chứa một khoảng trống và một tab):

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

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

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

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

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

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

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

!

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

Đầu ra từ tập lệnh thử nghiệm:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Kịch bản Perl này hoạt động khá kỹ lưỡng trên Unix (nó không tính đến tất cả sự phức tạp của tên đường dẫn Windows) khi đối mặt với các đầu vào kỳ lạ. Nó sử dụng mô-đun Cwdvà chức năng của nó realpathđể giải quyết đường dẫn thực sự của các tên tồn tại và thực hiện phân tích văn bản cho các đường dẫn không tồn tại. Trong mọi trường hợp ngoại trừ một, nó tạo ra cùng một đầu ra như tập lệnh của Dennis. Trường hợp lệch lạc là:

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

Hai kết quả tương đương - chỉ không giống nhau. (Đầu ra là từ một phiên bản sửa đổi nhẹ của tập lệnh thử nghiệm - tập lệnh Perl bên dưới chỉ đơn giản là in câu trả lời, thay vì đầu vào và câu trả lời như trong tập lệnh ở trên.) Bây giờ: tôi có nên loại bỏ câu trả lời không hoạt động không? Có lẽ...

#!/bin/perl -w
# Based loosely on code from: http://unix.derkeiler.com/Newsgroups/comp.unix.shell/2005-10/1256.html
# Via: http://stackoverflow.com/questions/2564634

use strict;

die "Usage: $0 from to\n" if scalar @ARGV != 2;

use Cwd qw(realpath getcwd);

my $pwd;
my $verbose = 0;

# Fettle filename so it is absolute.
# Deals with '//', '/./' and '/../' notations, plus symlinks.
# The realpath() function does the hard work if the path exists.
# For non-existent paths, the code does a purely textual hack.
sub resolve
{
    my($name) = @_;
    my($path) = realpath($name);
    if (!defined $path)
    {
        # Path does not exist - do the best we can with lexical analysis
        # Assume Unix - not dealing with Windows.
        $path = $name;
        if ($name !~ m%^/%)
        {
            $pwd = getcwd if !defined $pwd;
            $path = "$pwd/$path";
        }
        $path =~ s%//+%/%g;     # Not UNC paths.
        $path =~ s%/$%%;        # No trailing /
        $path =~ s%/\./%/%g;    # No embedded /./
        # Try to eliminate /../abc/
        $path =~ s%/\.\./(?:[^/]+)(/|$)%$1%g;
        $path =~ s%/\.$%%;      # No trailing /.
        $path =~ s%^\./%%;      # No leading ./
        # What happens with . and / as inputs?
    }
    return($path);
}

sub print_result
{
    my($source, $target, $relpath) = @_;
    if ($verbose)
    {
        print "source  = $ARGV[0]\n";
        print "target  = $ARGV[1]\n";
        print "relpath = $relpath\n";
    }
    else
    {
        print "$relpath\n";
    }
    exit 0;
}

my($source) = resolve($ARGV[0]);
my($target) = resolve($ARGV[1]);
print_result($source, $target, ".") if ($source eq $target);

# Split!
my(@source) = split '/', $source;
my(@target) = split '/', $target;

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

# Both paths are absolute; Perl splits an empty field 0.
for ($i = 1; $i < $count; $i++)
{
    last if $source[$i] ne $target[$i];
}

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

print_result($source, $target, $relpath);

Của bạn /home/part1/part2để /có một quá nhiều ../. Mặt khác, tập lệnh của tôi khớp với đầu ra của bạn ngoại trừ phần bổ sung của tôi không cần thiết .ở phần cuối của điểm đến .và tôi không sử dụng ./phần đầu của phần xuất phát mà không đi lên.
Tạm dừng cho đến khi có thông báo mới.

@Dennis: Tôi đã dành thời gian để xem qua kết quả - đôi khi tôi có thể thấy vấn đề đó và đôi khi tôi không thể tìm thấy nó một lần nữa. Xóa một './' hàng đầu là một bước tầm thường khác. Nhận xét của bạn về 'không nhúng. hoặc .. 'cũng thích hợp. Thật sự rất khó để thực hiện công việc đúng cách - gấp đôi vì vậy nếu bất kỳ tên nào thực sự là một liên kết tượng trưng; cả hai chúng tôi đang làm phân tích văn bản thuần túy.
Jonathan Leffler

@Dennis: Tất nhiên, trừ khi bạn có kết nối mạng Newcastle, cố gắng vượt lên trên root là vô ích, vì vậy ../../../ .. và ../../ .. là tương đương. Tuy nhiên, đó là sự thoát ly thuần túy; lời phê bình của bạn là chính xác. (Kết nối Newcastle cho phép bạn định cấu hình và sử dụng ký hiệu /../host/path/on/remote/machine để đến một máy chủ khác - một sơ đồ gọn gàng. Tôi tin rằng nó hỗ trợ /../../network/host/ đường dẫn / trên / từ xa / mạng / và / máy chủ cũng vậy. Nó trên Wikipedia.)
Jonathan Leffler

Vì vậy, thay vào đó, bây giờ chúng ta có dấu gạch chéo kép của UNC.
Tạm dừng cho đến khi có thông báo mới.

1
Tiện ích "readlink" (ít nhất là phiên bản GNU) có thể thực hiện tương đương với realpath (), nếu bạn chuyển qua tùy chọn "-f". Ví dụ, trên hệ thống của tôi, readlink /usr/bin/vicho /etc/alternatives/vi, nhưng đó là liên kết tượng trưng khác - trong khi readlink -f /usr/bin/vicho phép /usr/bin/vim.basic, đó là đích đến cuối cùng của tất cả các liên kết tượng trưng ...
psmears

3

Tôi lấy câu hỏi của bạn làm thử thách để viết mã này trong mã shell "xách tay", tức là

  • với một vỏ POSIX trong tâm trí
  • không có bashism như mảng
  • tránh gọi bên ngoài như bệnh dịch hạch. Không có một ngã ba nào trong kịch bản! Điều đó làm cho nó cực kỳ nhanh, đặc biệt là trên các hệ thống có chi phí hoạt động đáng kể, như cygwin.
  • Phải đối phó với các ký tự toàn cầu trong tên đường dẫn (*,?, [,])

Nó chạy trên mọi vỏ phù hợp POSIX (zsh, bash, ksh, ash, busybox, ...). Nó thậm chí có chứa một testsuite để xác minh hoạt động của nó. Canonicalization của tên đường được để lại như một bài tập. :-)

#!/bin/sh

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

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

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

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

set -f # noglob

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

# vi: set tabstop=3 shiftwidth=3 expandtab fileformat=unix :

2

Giải pháp của tôi:

computeRelativePath() 
{

    Source=$(readlink -f ${1})
    Target=$(readlink -f ${2})

    local OLDIFS=$IFS
    IFS="/"

    local SourceDirectoryArray=($Source)
    local TargetDirectoryArray=($Target)

    local SourceArrayLength=$(echo ${SourceDirectoryArray[@]} | wc -w)
    local TargetArrayLength=$(echo ${TargetDirectoryArray[@]} | wc -w)

    local Length
    test $SourceArrayLength -gt $TargetArrayLength && Length=$SourceArrayLength || Length=$TargetArrayLength


    local Result=""
    local AppendToEnd=""

    IFS=$OLDIFS

    local i

    for ((i = 0; i <= $Length + 1 ; i++ ))
    do
            if [ "${SourceDirectoryArray[$i]}" = "${TargetDirectoryArray[$i]}" ]
            then
                continue    
            elif [ "${SourceDirectoryArray[$i]}" != "" ] && [ "${TargetDirectoryArray[$i]}" != "" ] 
            then
                AppendToEnd="${AppendToEnd}${TargetDirectoryArray[${i}]}/"
                Result="${Result}../"               

            elif [ "${SourceDirectoryArray[$i]}" = "" ]
            then
                Result="${Result}${TargetDirectoryArray[${i}]}/"
            else
                Result="${Result}../"
            fi
    done

    Result="${Result}${AppendToEnd}"

    echo $Result

}

Đây là Cực kỳ di động :)
Ẩn danh

2

Đây là phiên bản của tôi. Nó dựa trên câu trả lời của @ Offerirmo . Tôi đã làm cho nó tương thích với Dash và sửa lỗi testcase sau:

./compute-relative.sh "/a/b/c/de/f/g" "/a/b/c/def/g/" -> "../..f/g/"

Hiện nay:

CT_FindRelativePath "/a/b/c/de/f/g" "/a/b/c/def/g/" -> "../../../def/g/"

Xem mã:

# both $1 and $2 are absolute paths beginning with /
# returns relative path to $2/$target from $1/$source
CT_FindRelativePath()
{
    local insource=$1
    local intarget=$2

    # Ensure both source and target end with /
    # This simplifies the inner loop.
    #echo "insource : \"$insource\""
    #echo "intarget : \"$intarget\""
    case "$insource" in
        */) ;;
        *) source="$insource"/ ;;
    esac

    case "$intarget" in
        */) ;;
        *) target="$intarget"/ ;;
    esac

    #echo "source : \"$source\""
    #echo "target : \"$target\""

    local common_part=$source # for now

    local result=""

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

    #echo "(f) common_part is     : \"$common_part\""

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

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

    if [ -n "${result}" -a -n "${forward_part}" ]; then
        #echo "(simple concat)"
        result="$result$forward_part"
    elif [ -n "${forward_part}" ]; then
        result="$forward_part"
    fi
    #echo "result = \"$result\""

    # if a / was added to target and result ends in / then remove it now.
    if [ "$intarget" != "$target" ]; then
        case "$result" in
            */) result=$(echo "$result" | awk '{ string=substr($0, 1, length($0)-1); print string; }' ) ;;
        esac
    fi

    echo $result

    return 0
}

1

Đoán cái này cũng sẽ làm được điều đó ... (đi kèm với các bài kiểm tra tích hợp) :)

OK, một số chi phí dự kiến, nhưng chúng tôi đang thực hiện vỏ Bourne ở đây! ;)

#!/bin/sh

#
# Finding the relative path to a certain file ($2), given the absolute path ($1)
# (available here too http://pastebin.com/tWWqA8aB)
#
relpath () {
  local  FROM="$1"
  local    TO="`dirname  $2`"
  local  FILE="`basename $2`"
  local  DEBUG="$3"

  local FROMREL=""
  local FROMUP="$FROM"
  while [ "$FROMUP" != "/" ]; do
    local TOUP="$TO"
    local TOREL=""
    while [ "$TOUP" != "/" ]; do
      [ -z "$DEBUG" ] || echo 1>&2 "$DEBUG$FROMUP =?= $TOUP"
      if [ "$FROMUP" = "$TOUP" ]; then
        echo "${FROMREL:-.}/$TOREL${TOREL:+/}$FILE"
        return 0
      fi
      TOREL="`basename $TOUP`${TOREL:+/}$TOREL"
      TOUP="`dirname $TOUP`"
    done
    FROMREL="..${FROMREL:+/}$FROMREL"
    FROMUP="`dirname $FROMUP`"
  done
  echo "${FROMREL:-.}${TOREL:+/}$TOREL/$FILE"
  return 0
}

relpathshow () {
  echo " - target $2"
  echo "   from   $1"
  echo "   ------"
  echo "   => `relpath $1 $2 '      '`"
  echo ""
}

# If given 2 arguments, do as said...
if [ -n "$2" ]; then
  relpath $1 $2

# If only one given, then assume current directory
elif [ -n "$1" ]; then
  relpath `pwd` $1

# Otherwise perform a set of built-in tests to confirm the validity of the method! ;)
else

  relpathshow /usr/share/emacs22/site-lisp/emacs-goodies-el \
              /usr/share/emacs22/site-lisp/emacs-goodies-el/filladapt.el

  relpathshow /usr/share/emacs23/site-lisp/emacs-goodies-el \
              /usr/share/emacs22/site-lisp/emacs-goodies-el/filladapt.el

  relpathshow /usr/bin \
              /usr/share/emacs22/site-lisp/emacs-goodies-el/filladapt.el

  relpathshow /usr/bin \
              /usr/share/emacs22/site-lisp/emacs-goodies-el/filladapt.el

  relpathshow /usr/bin/share/emacs22/site-lisp/emacs-goodies-el \
              /etc/motd

  relpathshow / \
              /initrd.img
fi

1

Kịch bản này chỉ hoạt động trên tên đường dẫn. Nó không yêu cầu bất kỳ tập tin nào tồn tại. Nếu các đường dẫn đi qua không tuyệt đối, hành vi hơi bất thường, nhưng nó sẽ hoạt động như mong đợi nếu cả hai đường dẫn đều tương đối.

Tôi chỉ thử nghiệm nó trên OS X, vì vậy nó có thể không di động được.

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

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

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

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

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

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

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

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

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

Ngoài ra, mã là một bản sao bit & paste-ish, nhưng tôi cần điều này khá nhanh chóng.
juancn

Đẹp ... đúng là thứ tôi cần. Và bạn đã bao gồm một thói quen chuẩn hóa tốt hơn hầu hết các thói quen khác mà tôi đã thấy (thường dựa vào thay thế regex).
drwatson

0

Câu trả lời này không giải quyết phần Bash của câu hỏi, nhưng vì tôi đã cố gắng sử dụng các câu trả lời trong câu hỏi này để thực hiện chức năng này trong Emacs nên tôi sẽ ném nó ra khỏi đó.

Emacs thực sự có một chức năng cho điều này ra khỏi hộp:

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

Lưu ý rằng tôi tin rằng câu trả lời python tôi đã thêm gần đây ( relpathchức năng) hoạt động giống hệt file-relative-namevới các trường hợp thử nghiệm mà bạn đã cung cấp.
Gary Wisniewski

-1

Đây là một kịch bản shell thực hiện nó mà không cần gọi các chương trình khác:

#! /bin/env bash 

#bash script to find the relative path between two directories

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

shopt -s extglob

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

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

}

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

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

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

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

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

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

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

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

#relpath_test

nguồn: http://www.ynform.org/w/Pub/Relpath


1
Điều này không thực sự di động do sử dụng cấu trúc $ {param / mẫu / trạm}, không phải là POSIX (tính đến năm 2011).
Jens

Nguồn tham chiếu ynform.org/w/Pub/Relpath trỏ đến trang wiki bị cắt xén hoàn toàn có chứa nội dung tập lệnh nhiều lần, xen kẽ với các dòng vi tilde, thông báo lỗi về các lệnh không tìm thấy và không có gì. Hoàn toàn vô dụng cho một người nghiên cứu bản gốc.
Jens

-1

Tôi cần một cái gì đó như thế này nhưng cũng giải quyết các liên kết tượng trưng. Tôi phát hiện ra rằng pwd có cờ -P cho mục đích đó. Một đoạn trong kịch bản của tôi được nối thêm. Nó nằm trong một hàm trong tập lệnh shell, do đó là $ 1 và $ 2. Giá trị kết quả, là đường dẫn tương đối từ START_ABS đến END_ABS, nằm trong biến UPDIRS. Tập lệnh cd vào từng thư mục tham số để thực thi pwd -P và điều này cũng có nghĩa là các tham số đường dẫn tương đối được xử lý. Chúc mừng, Jim

SAVE_DIR="$PWD"
cd "$1"
START_ABS=`pwd -P`
cd "$SAVE_DIR"
cd "$2"
END_ABS=`pwd -P`

START_WORK="$START_ABS"
UPDIRS=""

while test -n "${START_WORK}" -a "${END_ABS/#${START_WORK}}" '==' "$END_ABS";
do
    START_WORK=`dirname "$START_WORK"`"/"
    UPDIRS=${UPDIRS}"../"
done
UPDIRS="$UPDIRS${END_ABS/#${START_WORK}}"
cd "$SAVE_DIR"
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.