Ubah jalur absolut menjadi jalur relatif yang diberikan direktori saat ini menggunakan Bash


261

Contoh:

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

# Magic

relative="../../bar"

Bagaimana cara membuat keajaiban (semoga kode tidak terlalu rumit ...)?


7
Misalnya (kasus saya sekarang) untuk memberikan jalur relatif gcc sehingga dapat menghasilkan informasi debug relatif yang dapat digunakan bahkan jika perubahan jalur sumber.
Offirmo

Pertanyaan yang mirip dengan yang ini ditanyakan pada U&L: unix.stackexchange.com/questions/100918/… . Salah satu jawaban (@Gilles) menyebutkan alat, symlink , yang dapat memudahkan pekerjaan mengatasi masalah ini.
slm

25
Sederhana: realpath --relative-to=$absolute $current.
kenorb

Jawaban:


229

Menggunakan realpath dari GNU coreutils 8.23 ​​adalah yang paling sederhana, saya pikir:

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

Sebagai contoh:

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

7
Sangat disayangkan bahwa paket ini sudah usang pada Ubuntu 14.04 dan tidak memiliki opsi --relative-to.
kzh

3
Berfungsi dengan baik di Ubuntu 16.04
cayhorstmann

7
$ realpath --relative-to="${PWD}" "$file"berguna jika Anda ingin jalur relatif ke direktori kerja saat ini.
dcoles

1
Ini benar untuk hal-hal di dalam /usr/bin/nmap/-path tetapi tidak untuk /usr/bin/nmap: dari nmapke /tmp/testingitu hanya ../../dan tidak 3 kali ../. Namun itu berhasil, karena melakukan ..di rootfs adalah /.
Patrick B.

6
Sebagai @ PatrickB. tersirat, --relative-to=…mengharapkan direktori dan TIDAK MEMERIKSA. Itu berarti bahwa Anda berakhir dengan "../" tambahan jika Anda meminta path relatif ke file (seperti contoh ini tampaknya dilakukan, karena /usr/binjarang atau tidak pernah berisi direktori dan nmapbiasanya biner)
IBBoard

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

memberi:

../../bar

11
Ini bekerja, dan itu membuat alternatif terlihat konyol. Itu bonus bagi saya xD
hasvn

31
+1. Ok, kamu curang ... tapi ini terlalu bagus untuk tidak digunakan! relpath(){ python -c "import os.path; print os.path.relpath('$1','${2:-$PWD}')" ; }
MestreLion

4
Sayangnya, ini tidak tersedia secara universal: os.path.relpath baru dalam Python 2.6.
Chen Levy

15
@ChenLevy: Python 2.6 dirilis pada tahun 2008. Sulit untuk percaya itu tidak tersedia secara universal pada tahun 2012.
MestreLion

11
python -c 'import os, sys; print(os.path.relpath(*sys.argv[1:]))'bekerja paling alami dan andal.
musiphil

31

Ini adalah perbaikan yang diperbaiki dan berfungsi penuh dari solusi berperingkat terbaik saat ini dari @pini (yang sayangnya hanya menangani beberapa kasus)

Pengingat: tes '-z' jika string adalah panjang nol (= kosong) dan tes '-n' jika string tidak kosong.

# 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

Kasus uji:

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
Terintegrasi dalam shell offirmo lib github.com/Offirmo/offirmo-shell-lib , berfungsi «OSL_FILE_find_relative_path» (file «osl_lib_file.sh»)
Offirmo

1
+1. Dapat dengan mudah dibuat untuk menangani jalur apa pun (bukan hanya jalur absolut dimulai dengan /) dengan mengganti source=$1; target=$2dengansource=$(realpath $1); target=$(realpath $2)
Josh Kelley

2
@Josh memang, asalkan dir itu benar-benar ada ... yang tidak nyaman untuk tes unit;) Tetapi dalam penggunaan nyata ya, realpathdianjurkan, atau source=$(readlink -f $1)dll. Jika jalan setapak tidak tersedia (bukan standar)
Offirmo

Saya mendefinisikan $sourcedan $targetmenyukai ini: `if [[-e $ 1]]; lalu source = $ (readlink -f $ 1); sumber lain = $ 1; fi jika [[-e $ 2]]; lalu target = $ (readlink -f $ 2); selain itu target = $ 2; Dengan cara itu, fungsi tersebut dapat menangani jalur relatif yang ada / nyata serta direktori fiksi.
Nathan S. Watson-Haigh

1
@ NathanS.Watson-Haigh Bahkan lebih baik, saya menemukan baru-baru ini readlinkmemiliki -mopsi yang hanya melakukan itu;)
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/}

Script yang luar biasa - pendek dan bersih. Saya menerapkan suntingan (Menunggu peer review): common_part = $ source / common_part = $ (dirname $ common_part) / echo $ {back} $ {target # $ common_part} Skrip yang ada akan gagal karena kecocokan yang tidak sesuai pada awal nama direktori ketika membandingkan, misalnya: "/ foo / bar / baz" ke "/ foo / barsucks / bonk". Memindahkan slash ke var dan keluar dari eval akhir mengoreksi bug itu.
jcwenger

3
Script ini tidak berfungsi. Gagal satu tes sederhana "satu direktori turun". Hasil edit oleh jcwenger bekerja sedikit lebih baik tetapi cenderung menambahkan "../" tambahan.
Dr. Person Person II

1
dalam beberapa kasus gagal bagi saya jika "/" tertinggal di argumen; misal, jika $ 1 = "$ HOME /" dan $ 2 = "$ HOME / temp", ia mengembalikan "/ home / user / temp /", tetapi jika $ 1 = $ HOME maka ia dengan benar mengembalikan jalur relatif "temp". Karena itu kedua sumber = $ 1 dan target = $ 2 dapat "dibersihkan" menggunakan sed (atau menggunakan substitusi variabel bash, tetapi itu bisa menjadi tidak jelas) seperti => sumber = $ (echo "$ {1}" | sed 's / \ / * $ // ')
michael

1
Perbaikan kecil: Alih-alih menetapkan sumber / target langsung ke $ 1 dan $ 2, lakukan: sumber = $ (cd $ 1; pwd) target = $ (cd $ 2; pwd). Dengan cara ini menangani jalur dengan. dan .. dengan benar.
Joseph Garvin

4
Meskipun merupakan jawaban terpilih, jawaban ini memiliki banyak keterbatasan, karenanya banyak jawaban lain yang diposting. Lihat jawaban lain sebagai gantinya, terutama yang menampilkan kasus uji. Dan tolong beri komentar pada komentar ini!
Offirmo

25

Ini dibangun untuk Perl sejak tahun 2001, sehingga bekerja pada hampir setiap sistem yang dapat Anda bayangkan, bahkan VMS .

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

Juga, solusinya mudah dimengerti.

Jadi untuk contoh Anda:

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

... akan bekerja dengan baik.


3
saybelum tersedia dalam perl untuk log, tetapi dapat digunakan secara efektif di sini. perl -MFile::Spec -E 'say File::Spec->abs2rel(@ARGV)'
William Pursell

+1 tetapi lihat juga jawaban serupa ini yang lebih lama (Feb 2012). Baca juga komentar terkait dari William Pursell . Versi saya adalah dua baris perintah: perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target"dan perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target" "$origin". Skrip perl satu baris pertama menggunakan satu argumen (asal adalah direktori kerja saat ini). Skrip perl satu baris kedua menggunakan dua argumen.
olibre

3
Itu harus menjadi jawaban yang diterima. perldapat ditemukan hampir di mana-mana, meskipun jawabannya masih satu baris.
Dmitry Ginzburg

19

Menganggap bahwa Anda telah menginstal: bash, pwd, dirname, echo; lalu relpath adalah

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

Saya telah mendapatkan jawaban dari pini dan beberapa ide lainnya

Catatan : Ini membutuhkan kedua jalur untuk menjadi folder yang ada. File tidak akan berfungsi.


2
jawaban ideal: bekerja dengan / bin / sh, tidak memerlukan readlink, python, perl -> bagus untuk sistem yang ringan / tertanam atau konsol windows bash
Francois

2
Sayangnya, ini mensyaratkan bahwa jalan itu ada, yang tidak selalu diinginkan.
drwatsoncode

Jawaban yang saleh. Hal-hal cd-pwd adalah untuk menyelesaikan tautan, saya kira? Golf yang bagus!
Teck-freak

15

Python os.path.relpathsebagai fungsi shell

Tujuan dari relpathlatihan ini adalah untuk meniru os.path.relpathfungsi Python 2.7 (tersedia dari Python versi 2.6 tetapi hanya berfungsi dengan baik di 2.7), seperti yang diusulkan oleh xni . Akibatnya, beberapa hasil mungkin berbeda dari fungsi yang disediakan dalam jawaban lain.

(Saya belum menguji dengan jalur baru di jalur hanya karena itu memecah validasi berdasarkan panggilan python -cdari ZSH. Itu pasti akan mungkin dengan beberapa upaya.)

Mengenai "sihir" di Bash, saya sudah menyerah mencari sihir di Bash sejak lama, tetapi sejak itu saya menemukan semua sihir yang saya butuhkan, dan kemudian beberapa, di ZSH.

Akibatnya, saya mengusulkan dua implementasi.

Implementasi pertama bertujuan untuk sepenuhnya mematuhi POSIX . Saya telah mengujinya dengan /bin/dashdi Debian 6.0.6 "Squeeze". Ini juga berfungsi dengan baik /bin/shpada OS X 10.8.3, yang sebenarnya adalah Bash versi 3.2 yang berpura-pura menjadi shell POSIX.

Implementasi kedua adalah fungsi shell ZSH yang kuat terhadap beberapa garis miring dan gangguan lainnya di jalur. Jika Anda memiliki ZSH tersedia, ini adalah versi yang disarankan, bahkan jika Anda memanggilnya dalam bentuk skrip yang disajikan di bawah ini (yaitu dengan shebang of #!/usr/bin/env zsh) dari shell lain.

Akhirnya, saya telah menulis skrip ZSH yang memverifikasi output dari relpathperintah yang ditemukan dalam $PATHmemberikan test case yang disediakan dalam jawaban lain. Saya menambahkan beberapa bumbu pada tes tersebut dengan menambahkan beberapa spasi, tab, dan tanda baca seperti ! ? *di sana-sini dan juga melemparkan tes lain dengan karakter UTF-8 eksotis yang ditemukan di vim-powerline .

Fungsi shell POSIX

Pertama, fungsi shell yang sesuai dengan POSIX. Ini bekerja dengan berbagai jalur, tetapi tidak membersihkan beberapa garis miring atau menyelesaikan 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 "$@"

Fungsi shell ZSH

Sekarang, zshversi yang lebih kuat . Jika Anda ingin menyelesaikan argumen ke jalur nyata à la realpath -f(tersedia dalam coreutilspaket Linux ), ganti :abaris on 3 dan 4 dengan :A.

Untuk menggunakan ini di zsh, hapus baris pertama dan terakhir dan letakkan di direktori yang ada di $FPATHvariabel Anda .

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

Skrip uji

Akhirnya, skrip uji. Ia menerima satu opsi, yaitu -vuntuk mengaktifkan keluaran 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
Tidak bekerja ketika jalan pertama berakhir /aku takut.
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")"; }

Skrip shell di atas terinspirasi oleh pini (Terima kasih!) Ini memicu bug dalam modul penyorotan sintaks Stack Overflow (setidaknya dalam bingkai pratinjau saya). Jadi tolong abaikan jika penyorotan salah.

Beberapa catatan:

  • Kesalahan yang dihapus dan peningkatan kode tanpa secara signifikan meningkatkan panjang dan kompleksitas kode
  • Masukkan fungsionalitas ke dalam fungsi untuk kemudahan penggunaan
  • Tetap fungsi POSIX yang kompatibel sehingga mereka (harus) bekerja dengan semua shell POSIX (diuji dengan dash, bash, dan zsh di Ubuntu Linux 12.04)
  • Digunakan variabel lokal hanya untuk menghindari clobbering variabel global dan mencemari ruang nama global
  • Kedua jalur direktori TIDAK perlu ada (persyaratan untuk aplikasi saya)
  • Nama path dapat berisi spasi, karakter khusus, karakter kontrol, garis miring terbalik, tab, ', ",?, *, [,], Dll.
  • Fungsi inti "relPath" hanya menggunakan built-in POSIX shell tetapi membutuhkan jalur direktori absolut kanonik sebagai parameter
  • Fungsi tambahan "relpath" dapat menangani jalur direktori arbitrer (juga relatif, non-kanonik) tetapi membutuhkan utilitas inti GNU eksternal "readlink"
  • Hindari builtin "echo" dan digunakan builtin "printf" sebagai gantinya karena dua alasan:
  • Untuk menghindari konversi yang tidak perlu, nama path digunakan karena dikembalikan dan diharapkan oleh utilitas shell dan OS (misalnya cd, ln, ls, find, mkdir; tidak seperti "os.path.relpath" python, yang akan menafsirkan beberapa urutan backslash)
  • Kecuali untuk urutan backslash yang disebutkan, baris terakhir dari fungsi "relPath" menghasilkan pathnames yang kompatibel dengan python:

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

    Baris terakhir dapat diganti (dan disederhanakan) dengan baris

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

    Saya lebih suka yang terakhir karena

    1. Nama file dapat ditambahkan secara langsung ke jalur yang diperoleh oleh relPath, misalnya:

      ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
    2. Tautan simbolik dalam direktori yang sama yang dibuat dengan metode ini tidak memiliki yang jelek "./"untuk nama file.

  • Jika Anda menemukan kesalahan, silakan hubungi linuxball (at) gmail.com dan saya akan mencoba memperbaikinya.
  • Ditambahkan suite uji regresi (juga kompatibel dengan shell POSIX)

Daftar kode untuk uji regresi (cukup tambahkan ke skrip 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

Tidak banyak jawaban di sini yang praktis untuk digunakan setiap hari. Karena sangat sulit untuk melakukan ini dengan benar dalam bash murni, saya menyarankan solusi yang andal berikut ini (mirip dengan satu saran yang terkubur dalam komentar):

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

Kemudian, Anda bisa mendapatkan jalur relatif berdasarkan direktori saat ini:

echo $(relpath somepath)

atau Anda dapat menentukan bahwa path relatif ke direktori yang diberikan:

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

Kerugiannya adalah ini membutuhkan python, tetapi:

  • Ia bekerja secara identik di sembarang python> = 2.6
  • Itu tidak mengharuskan file atau direktori ada.
  • Nama file dapat berisi rentang karakter khusus yang lebih luas. Misalnya, banyak solusi lain tidak berfungsi jika nama file mengandung spasi atau karakter khusus lainnya.
  • Ini adalah fungsi satu baris yang tidak mengacaukan skrip.

Perhatikan bahwa solusi yang memasukkan basenameatau dirnamemungkin tidak selalu lebih baik, karena mereka mengharuskan coreutilsuntuk diinstal. Jika seseorang memiliki bashsolusi murni yang dapat diandalkan dan sederhana (daripada rasa ingin tahu yang berbelit-belit), saya akan terkejut.


Ini sepertinya pendekatan yang paling kuat.
dimo414

7

Skrip ini memberikan hasil yang benar hanya untuk input yang jalur absolut atau jalur relatif tanpa .atau ..:

#!/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
Ini tampaknya berhasil. Jika direktori benar-benar ada, gunakan $ (readlink -f $ 1) dan $ (readlink -f $ 2) pada input dapat memperbaiki masalah di mana "." atau ".." muncul di input. Ini dapat menyebabkan beberapa masalah jika direktori tidak benar-benar ada.
Dr. Person Person II

7

Saya hanya akan menggunakan Perl untuk tugas yang tidak terlalu sepele ini:

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

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

1
+1, tetapi akan merekomendasikan: perl -MFile::Spec -e "print File::Spec->abs2rel('$absolute','$current')"agar absolut dan terkini dikutip.
William Pursell

Saya suka relative=$(perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$absolute" "$current"). Ini memastikan nilai tidak bisa, sendiri, berisi kode perl!
Erik Aronesty

6

Sedikit perbaikan pada jawaban kasku dan Pini , yang memainkan lebih baik dengan spasi dan memungkinkan melewati jalur relatif:

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

Pengujian:

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

+1 untuk kekompakan dan bash-ness. Anda harus, bagaimanapun, juga panggilan readlinkdi $(pwd).
DevSolar

2
Relatif tidak berarti file harus ditempatkan di direktori yang sama.
greenoldman

walaupun pertanyaan awal tidak memberikan banyak testcases, skrip ini gagal untuk tes sederhana seperti menemukan jalur relatif dari / home / user1 ke / home / user2 (jawaban yang benar: ../user2). Script oleh pini / jcwenger berfungsi untuk kasus ini.
michael

4

Namun solusi lain, pure bash+ GNU readlinkagar mudah digunakan dalam konteks berikut:

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

Sunting: Pastikan "$ B" tidak ada atau tidak ada softlink dalam hal itu, relpathikuti tautan ini yang bukan yang Anda inginkan!

Ini berfungsi di hampir semua Linux saat ini. Jika readlink -mtidak berhasil di sisi Anda, coba readlink -fsaja. Lihat juga https://gist.github.com/hilbix/1ec361d00a8178ae8ea0 untuk kemungkinan pembaruan:

: 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:-.}"
}

Catatan:

  • Kami berhati-hati agar aman terhadap ekspansi karakter meta shell yang tidak diinginkan, seandainya nama file mengandung *atau ?.
  • Output dimaksudkan agar dapat digunakan sebagai argumen pertama untuk ln -s:
    • relpath / /memberi .dan bukan string kosong
    • relpath a amemberi a, bahkan jika akebetulan direktori
  • Kebanyakan kasus umum diuji untuk memberikan hasil yang masuk akal juga.
  • Solusi ini menggunakan pencocokan awalan string, oleh karena readlinkitu diperlukan untuk mengkanonik jalur.
  • Berkat readlink -mitu berfungsi untuk jalur yang belum ada juga.

Pada sistem lama, di mana readlink -mtidak tersedia, readlink -fgagal jika file tidak ada. Jadi Anda mungkin perlu beberapa solusi seperti ini (belum diuji!):

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

Ini tidak benar-benar benar jika $1mencakup .atau ..untuk jalur yang tidak ada (seperti dalam /doesnotexist/./a), tetapi harus mencakup sebagian besar kasus.

(Ganti di readlink -m --atas dengan readlink_missing.)

Edit karena downvote berikut

Berikut ini adalah tes, bahwa fungsi ini memang benar:

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"

Bingung? Ya, inilah hasil yang benar ! Bahkan jika Anda berpikir itu tidak sesuai dengan pertanyaan, inilah buktinya ini benar:

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

Tanpa keraguan, ../baradalah jalur relatif yang tepat dan satu-satunya yang benar dari halaman bardilihat dari halaman moo. Segala sesuatu yang lain akan salah.

Ini sepele untuk mengadopsi output ke pertanyaan yang tampaknya mengasumsikan, yaitu currentdirektori:

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

Ini mengembalikan persis, apa yang diminta.

Dan sebelum Anda mengangkat alis, inilah varian yang sedikit lebih kompleks dari relpath(tempat perbedaan kecil), yang seharusnya juga berfungsi untuk URL-Sintaks (sehingga trailing dapat /bertahan, berkat beberapa- bashmagik):

# 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:-.}"
}

Dan berikut ini cek untuk memperjelas: Ini benar-benar berfungsi seperti yang diperintahkan.

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

Dan di sini adalah bagaimana ini dapat digunakan untuk memberikan hasil yang diinginkan dari pertanyaan:

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

Jika Anda menemukan sesuatu yang tidak berfungsi, beri tahu saya di komentar di bawah. Terima kasih.

PS:

Mengapa argumen relpath"terbalik" berbeda dengan semua jawaban lain di sini?

Jika Anda berubah

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

untuk

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

maka Anda dapat meninggalkan parameter ke-2, sehingga BASE adalah direktori saat ini / URL / apa pun. Itu hanya prinsip Unix, seperti biasa.

Jika Anda tidak suka itu, silakan kembali ke Windows. Terima kasih.


3

Sayangnya, jawaban Mark Rushakoff (sekarang dihapus - itu merujuk kode dari sini ) tampaknya tidak berfungsi dengan benar ketika diadaptasi ke:

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

Pemikiran yang diuraikan dalam komentar dapat disempurnakan untuk membuatnya bekerja dengan benar untuk sebagian besar kasus. Saya akan berasumsi bahwa skrip mengambil argumen sumber (di mana Anda berada) dan argumen target (di mana Anda ingin pergi), dan bahwa keduanya adalah nama path absolut atau keduanya relatif. Jika satu mutlak dan relatif lain, hal yang paling mudah adalah dengan awalan nama relatif dengan direktori kerja saat ini - tetapi kode di bawah ini tidak melakukan itu.


Awas

Kode di bawah ini hampir berfungsi dengan benar, tetapi tidak sepenuhnya benar.

  1. Ada masalah yang dibahas dalam komentar dari Dennis Williamson.
  2. Ada juga masalah bahwa pemrosesan pathnames murni ini tekstual dan Anda dapat secara serius dikacaukan oleh symlink aneh.
  3. Kode tidak menangani 'titik' tersesat di jalur seperti ' xyz/./pqr'.
  4. Kode tidak menangani 'titik ganda' yang tersesat di jalur seperti ' xyz/../pqr'.
  5. Sepele: kode tidak menghapus awalan ' ./' dari jalur.

Kode Dennis lebih baik karena memperbaiki 1 dan 5 - tetapi memiliki masalah yang sama 2, 3, 4. Gunakan kode Dennis (dan pilih di depan ini) karena itu.

(NB: POSIX menyediakan panggilan sistem realpath()yang menyelesaikan nama path sehingga tidak ada symlink yang tersisa di dalamnya. Menerapkannya ke nama input, dan kemudian menggunakan kode Dennis akan memberikan jawaban yang benar setiap kali. Ini sepele untuk menulis kode C yang membungkus realpath()- Saya sudah melakukannya - tapi saya tidak tahu utilitas standar yang melakukannya.)


Untuk ini, saya menemukan Perl lebih mudah digunakan daripada shell, meskipun bash memiliki dukungan yang layak untuk array dan mungkin bisa melakukan ini juga - latihan untuk pembaca. Jadi, diberi dua nama yang kompatibel, pisahkan masing-masing menjadi komponen:

  • Tetapkan jalur relatif ke kosong.
  • Sementara komponennya sama, lewati ke yang berikutnya.
  • Ketika komponen yang sesuai berbeda atau tidak ada lagi komponen untuk satu jalur:
  • Jika tidak ada komponen sumber yang tersisa dan jalur relatif kosong, tambahkan "." ke awal.
  • Untuk setiap komponen sumber yang tersisa, awali jalur relatif dengan "../".
  • Jika tidak ada komponen target yang tersisa dan jalur relatif kosong, tambahkan "." ke awal.
  • Untuk setiap komponen target yang tersisa, tambahkan komponen ke ujung jalan setelah garis miring.

Jadi:

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

Skrip uji (tanda kurung berisi kosong dan 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

Output dari skrip uji:

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

Script Perl ini bekerja cukup menyeluruh pada Unix (tidak memperhitungkan semua kompleksitas nama jalur Windows) dalam menghadapi input aneh. Ini menggunakan modul Cwddan fungsinya realpathuntuk menyelesaikan jalur nama sebenarnya yang ada, dan melakukan analisis tekstual untuk jalur yang tidak ada. Dalam semua kasus kecuali satu, itu menghasilkan output yang sama dengan skrip Dennis. Kasus yang menyimpang adalah:

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

Kedua hasil tersebut setara - hanya saja tidak identik. (Outputnya berasal dari versi skrip tes yang sedikit dimodifikasi - skrip Perl di bawah ini hanya mencetak jawaban, bukan input dan jawaban seperti pada skrip di atas.) Sekarang: haruskah saya menghilangkan jawaban yang tidak berfungsi? Mungkin...

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

Anda /home/part1/part2untuk /memiliki salah satu terlalu banyak ../. Kalau tidak, skrip saya cocok dengan output Anda, kecuali skrip saya menambahkan yang tidak perlu .di akhir tempat tujuan .dan saya tidak menggunakan ./di awal yang turun tanpa naik.
Dijeda sampai pemberitahuan lebih lanjut.

@ Dennis: Saya menghabiskan waktu dengan juling atas hasilnya - kadang-kadang saya bisa melihat masalah itu, dan kadang-kadang saya tidak bisa menemukannya lagi. Menghapus awalan './' adalah langkah sepele lainnya. Komentar Anda tentang 'tidak disematkan. atau .. 'juga relevan. Sebenarnya sangat sulit untuk melakukan pekerjaan dengan benar - dua kali lipat jika salah satu dari nama-nama tersebut sebenarnya adalah symlink; kami berdua melakukan analisis tekstual murni.
Jonathan Leffler

@ Dennis: Tentu saja, kecuali Anda memiliki jaringan Newcastle Connection, mencoba untuk mendapatkan root di atas adalah sia-sia, jadi ../../../ .. dan ../../ .. adalah setara. Namun, itu adalah pelarian murni; kritik Anda benar. (Newcastle Connection memungkinkan Anda untuk mengonfigurasi dan menggunakan notasi /../host/path/on/remote/machine untuk mencapai host yang berbeda - skema yang rapi. Saya yakin itu mendukung /../../network/host/ path / on / remote / network / dan / host juga. Ini di Wikipedia.)
Jonathan Leffler

Jadi sebagai gantinya, kami sekarang memiliki tebasan ganda UNC.
Dijeda sampai pemberitahuan lebih lanjut.

1
Utilitas "readlink" (setidaknya versi GNU) dapat melakukan hal yang sama dengan realpath (), jika Anda memberikannya opsi "-f". Sebagai contoh, pada sistem saya, readlink /usr/bin/vimemberi /etc/alternatives/vi, tapi itu symlink lain - sedangkan readlink -f /usr/bin/vimemberi /usr/bin/vim.basic, yang merupakan tujuan akhir dari semua symlink ...
psmears

3

Saya menganggap pertanyaan Anda sebagai tantangan untuk menulis ini dalam kode shell "portabel", yaitu

  • dengan shell POSIX dalam pikiran
  • tidak ada bashism seperti array
  • hindari memanggil pihak luar seperti wabah. Tidak ada garpu tunggal dalam skrip! Itu membuatnya sangat cepat, terutama pada sistem dengan overhead garpu yang signifikan, seperti cygwin.
  • Harus berurusan dengan karakter glob di nama path (*,?, [,])

Ini berjalan pada semua shell POSIX (zsh, bash, ksh, ash, busybox, ...). Itu bahkan berisi testuite untuk memverifikasi operasinya. Kanonalisasi nama path dibiarkan sebagai latihan. :-)

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

Solusi Saya:

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

}

Ini Extrememly Portable :)
Anonim

2

Ini versi saya. Ini berdasarkan jawaban oleh @Offirmo . Saya membuatnya kompatibel dengan Dash dan memperbaiki kegagalan testcase berikut:

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

Sekarang:

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

Lihat kode:

# 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

Tebak yang satu ini akan melakukan triknya juga ... (dilengkapi dengan tes bawaan) :)

OK, beberapa overhead yang diharapkan, tapi kami melakukan Bourne shell di sini! ;)

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

Script ini hanya berfungsi pada nama path. Tidak memerlukan file apa pun untuk eksis. Jika jalur yang dilewati tidak absolut, perilaku ini agak tidak biasa, tetapi harus bekerja seperti yang diharapkan jika kedua jalur tersebut relatif.

Saya hanya mengujinya pada OS X, jadi mungkin tidak portabel.

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

Juga, kodenya sedikit copy & paste-ish, tapi saya butuh ini agak cepat.
juancn

Bagus ... hanya apa yang saya butuhkan. Dan Anda telah memasukkan rutin kanonikisasi yang lebih baik daripada kebanyakan orang lain yang pernah saya lihat (yang biasanya bergantung pada penggantian regex).
drwatsoncode

0

Jawaban ini tidak membahas bagian Bash dari pertanyaan, tetapi karena saya mencoba menggunakan jawaban dalam pertanyaan ini untuk mengimplementasikan fungsi ini di Emacs saya akan membuangnya di sana.

Emacs sebenarnya memiliki fungsi untuk ini di luar kotak:

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"

Perhatikan bahwa saya percaya jawaban python yang saya tambahkan baru-baru ini ( relpathfungsi) berperilaku identik dengan file-relative-nameuntuk kasus uji yang Anda berikan.
Gary Wisniewski

-1

Berikut skrip shell yang melakukannya tanpa memanggil program lain:

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

sumber: http://www.ynform.org/w/Pub/Relpath


1
Ini tidak benar-benar portabel karena penggunaan konstruk $ {param / pattern / subst}, yang bukan POSIX (per 2011).
Jens

Sumber yang direferensikan ynform.org/w/Pub/Relpath menunjuk ke halaman wiki yang benar-benar kacau berisi isi skrip beberapa kali, bercampur dengan garis vi tilde, pesan kesalahan tentang perintah yang tidak ditemukan dan yang lainnya. Sama sekali tidak berguna bagi seseorang yang meneliti yang asli.
Jens

-1

Saya membutuhkan sesuatu seperti ini tetapi yang memutuskan tautan simbolik juga. Saya menemukan bahwa pwd memiliki flag -P untuk tujuan itu. Sepotong skrip saya ditambahkan. Ini dalam fungsi dalam skrip shell, maka $ 1 dan $ 2. Nilai hasil, yang merupakan jalur relatif dari START_ABS ke END_ABS, ada dalam variabel UPDIRS. Script cd masuk ke setiap direktori parameter untuk mengeksekusi pwd -P dan ini juga berarti bahwa parameter path relatif ditangani. Cheers, 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"
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.