Przekształć ścieżkę bezwzględną na ścieżkę względną, biorąc pod uwagę bieżący katalog, używając Bash

261

Przykład:

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

# Magic

relative="../../bar"

Jak stworzyć magię (mam nadzieję, że niezbyt skomplikowany kod ...)?

Paul Tarjan
źródło
7
Na przykład (mój przypadek w tej chwili) za podanie względnej ścieżki gcc, aby mogła wygenerować względne informacje debugowania przydatne nawet w przypadku zmiany ścieżki źródłowej.
Offirmo,
Pytanie podobne do tego zadano na stronie U&L: unix.stackexchange.com/questions/100918/… . Jedna z odpowiedzi (@Gilles) wspomina o narzędziu, dowiązaniach symbolicznych , które mogą ułatwić pracę nad tym problemem.
slm
25
Prosta realpath --relative-to=$absolute $current.
kenorb

Odpowiedzi:

228

Myślę, że użycie realpath z GNU coreutils 8.23 ​​jest najprostsze:

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

Na przykład:

$ realpath --relative-to=/usr/bin/nmap /tmp/testing
../../../tmp/testing
moduł0
źródło
7
Szkoda, że ​​pakiet jest nieaktualny w Ubuntu 14.04 i nie ma opcji --relative-to.
kzh
3
Działa dobrze na Ubuntu 16.04
cayhorstmann
7
$ realpath --relative-to="${PWD}" "$file"przydaje się, jeśli chcesz ścieżki względem bieżącego katalogu roboczego.
dcoles
1
Jest to poprawne dla rzeczy wewnątrz /usr/bin/nmap/ścieżki, ale nie dla /usr/bin/nmap: od nmapdo /tmp/testingjest tylko, ../../a nie 3 razy ../. Działa jednak, ponieważ działa ..na rootfach /.
Patrick B.
6
Jako @PatrickB. sugeruje, --relative-to=…oczekuje katalogu i NIE sprawdza. Oznacza to, że otrzymujesz dodatkowe „../”, jeśli poprosisz o ścieżkę względem pliku (jak wydaje się w tym przykładzie, ponieważ /usr/binrzadko lub nigdy nie zawiera katalogów i nmapzwykle jest
plikiem
162
$ python -c "import os.path; print os.path.relpath('/foo/bar', '/foo/baz/foo')"

daje:

../../bar
xni
źródło
11
Działa i sprawia, że ​​alternatywy wyglądają absurdalnie. To dla mnie bonus xD
hasvn
31
+1. Ok, oszukiwałeś ... ale to jest zbyt piękne, by nie używać! relpath(){ python -c "import os.path; print os.path.relpath('$1','${2:-$PWD}')" ; }
MestreLion
4
Niestety nie jest to powszechnie dostępne: os.path.relpath jest nowością w Pythonie 2.6.
Chen Levy
15
@ChenLevy: Python 2.6 został wydany w 2008 roku. Trudno uwierzyć, że nie był powszechnie dostępny w 2012 roku.
MestreLion
11
python -c 'import os, sys; print(os.path.relpath(*sys.argv[1:]))'działa najbardziej naturalnie i niezawodnie.
musiphil
31

Jest to poprawione, w pełni funkcjonalne ulepszenie obecnie najlepiej ocenianego rozwiązania @pini (które niestety obsługuje tylko kilka przypadków)

Przypomnienie: test „-z”, jeśli łańcuch ma zerową długość (= pusty) i test „-n”, jeśli łańcuch nie jest pusty.

# 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

Przypadki testowe :

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"
Offirmo
źródło
1
Zintegrowana z biblioteką shell offirmo lib github.com/Offirmo/offirmo-shell-lib , funkcja «OSL_FILE_find_relative_path» (plik «osl_lib_file.sh»)
Offirmo
1
+1. Można go łatwo obsługiwać dowolne ścieżki (nie tylko ścieżki bezwzględne zaczynające się od /), zastępując source=$1; target=$2jesource=$(realpath $1); target=$(realpath $2)
Josh Kelley,
2
@Josh rzeczywiście, pod warunkiem, że katalog rzeczywiście istnieje ... co było niewygodne dla testów jednostkowych;) Ale w prawdziwym użyciu tak, realpathjest zalecane, source=$(readlink -f $1)itp., Jeśli realpath nie jest dostępny (niestandardowy)
Offirmo
Zdefiniowałem $sourcei $targetpodoba mi się to: `if [[-e $ 1]]; następnie source = $ (readlink -f $ 1); w innym przypadku źródło = 1 USD; fi if [[-e $ 2]]; następnie cel = $ (readlink -f $ 2); w przeciwnym razie cel = 2 USD; fi` W ten sposób funkcja może obsługiwać rzeczywiste / istniejące ścieżki względne, a także katalogi fikcyjne.
Nathan S. Watson-Haigh
1
@ NathanS.Watson-Haigh Jeszcze lepiej, niedawno odkryłem, że readlinkma -mopcję, która właśnie to robi;)
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/}
pini
źródło
Cudowny scenariusz - krótki i czysty. Zastosowałem edycję (Oczekiwanie na ocenę): common_part = $ source / common_part = $ (nazwa katalogu $ common_part) / echo $ {back} $ {target # $ common_part} Istniejący skrypt nie powiedzie się z powodu niewłaściwego dopasowania na początku nazwy katalogu podczas porównywania, na przykład: „/ foo / bar / baz” do „/ foo / Barsucks / bonk”. Przesunięcie ukośnika do var i poza ostateczną wersję poprawia ten błąd.
jcwenger,
3
Ten skrypt po prostu nie działa. Nie powiedzie się prosty test „jeden katalog w dół”. Edycje autorstwa jcwenger działają nieco lepiej, ale zwykle dodają dodatkowe „../”.
Dr. Person Person II
1
w niektórych przypadkach nie udaje mi się, jeśli argumentem końcowym jest „/”; np. jeśli $ 1 = „$ HOME /” i $ 2 = „$ HOME / temp”, zwraca „/ home / user / temp /”, ale jeśli $ 1 = $ HOME, to poprawnie zwraca ścieżkę względną „temp”. Zarówno source = 1 $, jak i target = 2 $ można zatem „oczyścić” za pomocą sed (lub stosując podstawienie zmiennych bash, ale może to być niepotrzebnie nieprzejrzyste), takie jak => source = $ (echo "$ {1}" | sed 's / \ / * $ // ')
Michael
1
Drobne poprawki: Zamiast ustawiać źródło / cel bezpośrednio na 1 $ i 2 $, wykonaj: source = $ (cd $ 1; pwd) target = $ (cd $ 2; pwd). W ten sposób obsługuje ścieżki. i .. poprawnie.
Joseph Garvin
4
Pomimo tego, że jest najczęściej wybieraną odpowiedzią, ta odpowiedź ma wiele ograniczeń, dlatego opublikowano tak wiele innych odpowiedzi. Zamiast tego zobacz inne odpowiedzi, szczególnie te zawierające przypadki testowe. I proszę głosować za tym komentarzem!
Offirmo
25

Jest wbudowany w Perla od 2001 roku, więc działa na prawie każdym systemie, jaki możesz sobie wyobrazić, nawet na VMS .

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

Ponadto rozwiązanie jest łatwe do zrozumienia.

Na przykład:

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

... działałoby dobrze.

Erik Aronesty
źródło
3
saynie był dostępny w perlu jako dziennik, ale można go tutaj skutecznie wykorzystać. perl -MFile::Spec -E 'say File::Spec->abs2rel(@ARGV)'
William Pursell
+1, ale zobacz także podobną odpowiedź, która jest starsza (luty 2012). Przeczytaj także stosowne komentarze Williama Pursella . Moja wersja to dwie linie poleceń: perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target"i perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target" "$origin". Pierwszy jednowierszowy skrypt perla używa jednego argumentu (origin jest bieżącym katalogiem roboczym). Drugi jednowierszowy skrypt perla używa dwóch argumentów.
olibre
3
To powinna być zaakceptowana odpowiedź. perlmożna znaleźć prawie wszędzie, choć odpowiedź jest nadal jednokierunkowa.
Dmitry Ginzburg,
19

Zakładając, że zainstalowałeś: bash, pwd, dirname, echo; wtedy relpath jest

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

Grałem w golfa w odpowiedzi od Pini i kilku innych pomysłów

Uwaga : Wymaga to, aby obie ścieżki były istniejącymi folderami. Pliki nie będą działać.

Alexx Roche
źródło
2
idealna odpowiedź: działa z / bin / sh, nie wymaga readlink, python, perl -> doskonale nadaje się do systemów light / embedded lub konsoli bash Windows
Francois
2
Niestety wymaga to ścieżki, która nie zawsze jest pożądana.
drwatsoncode
Boża odpowiedź. Chyba cd-pwd służy do rozwiązywania linków? Miłego golfa!
Teck-freak
15

Python os.path.relpathjako funkcja powłoki

Celem tego relpathćwiczenia jest naśladowanie funkcji Pythona 2.7 os.path.relpath(dostępnej w Pythonie w wersji 2.6, ale działającej poprawnie tylko w 2.7), zgodnie z propozycją xni . W rezultacie niektóre wyniki mogą różnić się od funkcji przedstawionych w innych odpowiedziach.

(Nie testowałem z nowymi liniami w ścieżkach tylko dlatego, że psuje to sprawdzanie poprawności w oparciu o wywołanie python -cz ZSH. Z pewnością byłoby to możliwe z pewnym wysiłkiem.)

Jeśli chodzi o „magię” w Bash, dawno temu zrezygnowałem z poszukiwania magii w Bash, ale od tego czasu znalazłem całą magię, której potrzebuję, a potem trochę w ZSH.

W związku z tym proponuję dwa wdrożenia.

Pierwsza implementacja ma być w pełni zgodna z POSIX . Przetestowałem to /bin/dashna Debian 6.0.6 „Squeeze”. Działa również doskonale z/bin/sh w systemie OS X 10.8.3, który w rzeczywistości jest wersją Bash 3.2 udającą powłokę POSIX.

Druga implementacja to funkcja powłoki ZSH, która jest odporna na wiele cięć i innych niedogodności na ścieżkach. Jeśli masz dostęp do ZSH, jest to zalecana wersja, nawet jeśli wywołujesz go w formie skryptu przedstawionej poniżej (tj. Z shebangiem #!/usr/bin/env zsh) z innej powłoki.

Na koniec napisałem skrypt ZSH, który weryfikuje dane wyjściowe relpathpolecenia znalezionego w $PATHdanych przypadkach testowych podanych w innych odpowiedziach. Dodałem trochę pikanterii do tych testów, dodając spacje, tabulacje i znaki interpunkcyjne, takie jak ! ? *tu i tam, a także rzuciłem kolejny test z egzotycznymi znakami UTF-8 znalezionymi w vim-powerline .

Funkcja powłoki POSIX

Po pierwsze, funkcja powłoki zgodna z POSIX. Działa z różnymi ścieżkami, ale nie czyści wielu ukośników ani nie rozpoznaje dowiązań symbolicznych.

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

Funkcja powłoki ZSH

Teraz bardziej niezawodna zshwersja. Jeśli chcesz, aby rozwiązał argumenty na prawdziwych ścieżkach à la realpath -f(dostępnych w coreutilspakiecie Linux ), zamień :awiersze 3 i 4 na :A.

Aby użyć tego w zsh, usuń pierwszy i ostatni wiersz i umieść go w katalogu, który znajduje się w twojej $FPATHzmiennej.

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

Skrypt testowy

Na koniec skrypt testowy. Akceptuje jedną opcję, a mianowicie -vwłączenie pełnego wyjścia.

#!/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
simonair
źródło
2
/Obawiam się, że nie działa, gdy kończy się pierwsza ścieżka .
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")"; }

Powyższy skrypt powłoki został zainspirowany pini's (Dzięki!). Wywołuje błąd w module podświetlania składni Przepełnienia stosu (przynajmniej w mojej ramce podglądu). Dlatego zignoruj, jeśli wyróżnianie jest nieprawidłowe.

Niektóre uwagi:

  • Usunięto błędy i poprawiono kod bez znacznego zwiększenia długości i złożoności kodu
  • Umieść funkcjonalność w funkcjach dla łatwości użytkowania
  • Utrzymywał funkcje zgodne z POSIX, aby (powinny) działać ze wszystkimi powłokami POSIX (testowane z dash, bash i zsh w Ubuntu Linux 12.04)
  • Używano zmiennych lokalnych tylko w celu uniknięcia blokowania zmiennych globalnych i zanieczyszczania globalnej przestrzeni nazw
  • Obie ścieżki katalogu NIE muszą istnieć (wymaganie dla mojej aplikacji)
  • Ścieżki mogą zawierać spacje, znaki specjalne, znaki kontrolne, ukośniki odwrotne, tabulatory, „,”,?, *, [,] Itd.
  • Funkcja podstawowa „relPath” wykorzystuje tylko wbudowane powłoki POSIX, ale wymaga kanonicznych bezwzględnych ścieżek katalogów jako parametrów
  • Rozszerzona funkcja „relpath” może obsługiwać dowolne ścieżki katalogów (także względne, niekanoniczne), ale wymaga zewnętrznego narzędzia GNU „readlink”
  • Unikano wbudowanego „echa” i zamiast tego używano wbudowanego „printf” z dwóch powodów:
  • Aby uniknąć niepotrzebnych konwersji, nazwy ścieżek są używane, ponieważ są zwracane i oczekiwane przez narzędzia powłoki i systemu operacyjnego (np. Cd, ln, ls, find, mkdir; w przeciwieństwie do pythona „os.path.relpath”, który zinterpretuje niektóre sekwencje odwrotnego ukośnika)
  • Poza wymienionymi sekwencjami odwrotnego ukośnika, ostatni wiersz funkcji „relPath” wypisuje ścieżki zgodne z pythonem:

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

    Ostatni wiersz można zastąpić (i uprościć) wierszem

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

    Wolę to drugie, ponieważ

    1. Nazwy plików mogą być bezpośrednio dołączane do ścieżek katalogu uzyskanych przez relPath, np .:

      ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
    2. Dowiązania symboliczne w tym samym katalogu utworzonym za pomocą tej metody nie mają brzydkiego "./"tekstu przed nazwą pliku.

  • Jeśli znajdziesz błąd, skontaktuj się z linuxball (at) gmail.com, a ja postaram się go naprawić.
  • Dodano zestaw testów regresji (kompatybilny również z powłoką POSIX)

Lista kodów dla testów regresji (wystarczy dołączyć ją do skryptu powłoki):

############################################################################
# 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
linuxball
źródło
9

Niewiele odpowiedzi tutaj jest praktycznych na co dzień. Ponieważ bardzo trudno jest to zrobić poprawnie w czystym bashu, proponuję następujące, niezawodne rozwiązanie (podobne do jednej sugestii ukrytej w komentarzu):

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

Następnie możesz uzyskać ścieżkę względną na podstawie bieżącego katalogu:

echo $(relpath somepath)

lub możesz określić, aby ścieżka była względna do danego katalogu:

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

Jedyną wadą jest to, że wymaga Pythona, ale:

  • Działa identycznie w każdym pythonie> = 2.6
  • Nie wymaga istnienia plików ani katalogów.
  • Nazwy plików mogą zawierać szerszy zakres znaków specjalnych. Na przykład wiele innych rozwiązań nie działa, jeśli nazwy plików zawierają spacje lub inne znaki specjalne.
  • Jest to funkcja jednowierszowa, która nie zaśmieca skryptów.

Należy pamiętać, że rozwiązania, które zawierają basenamelub dirnameniekoniecznie muszą być lepsze, ponieważ wymagają coreutilszainstalowania. Jeśli ktoś ma czyste bashrozwiązanie, które jest niezawodne i proste (zamiast zawiłej ciekawości), byłbym zaskoczony.

Gary Wiśniewski
źródło
Wydaje się, że to zdecydowanie najbardziej niezawodne podejście.
dimo414
7

Ten skrypt daje prawidłowe wyniki tylko dla danych wejściowych, które są ścieżkami bezwzględnymi lub ścieżkami względnymi bez .lub ..:

#!/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"
Wstrzymano do odwołania.
źródło
1
To wydaje się działać. Jeśli katalogi faktycznie istnieją, użycie $ (readlink -f $ 1) i $ (readlink -f $ 2) na wejściach może rozwiązać problem, w którym „.” lub na wejściu pojawi się „..”. Może to powodować problemy, jeśli katalogi tak naprawdę nie istnieją.
Dr Person Person II
7

Chciałbym użyć Perla do tego nie tak trywialnego zadania:

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

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

źródło
1
+1, ale poleciłbym: perl -MFile::Spec -e "print File::Spec->abs2rel('$absolute','$current')"aby cytować wartość absolutną i bieżącą.
William Pursell
Lubię relative=$(perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$absolute" "$current"). To gwarantuje, że same wartości nie mogą zawierać kodu perla!
Erik Aronesty,
6

Nieznaczna poprawa w odpowiedziach kasku i Pini , która lepiej gra ze spacjami i pozwala na przejście względnych ścieżek:

#!/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
sinelaw
źródło
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

Testowanie:

$ ./test.sh 
TEST=/home/ubuntu/.//blah
TMP=/home/ubuntu/blah
REL=blah
Steve
źródło
+1 za zwartość i zuchwałość. Należy jednak również zadzwonić readlinkna $(pwd).
DevSolar
2
Względne nie oznacza, że ​​plik musi być umieszczony w tym samym katalogu.
greenoldman
chociaż pierwotne pytanie nie zawiera wielu przypadków testowych, ten skrypt kończy się niepowodzeniem w przypadku prostych testów, takich jak znalezienie ścieżki względnej od / home / user1 do / home / user2 (poprawna odpowiedź: ../user2). Skrypt pini / jcwenger działa w tym przypadku.
Michael
4

Kolejne rozwiązanie, czysta bash+ GNU readlinkdo łatwego użycia w następującym kontekście:

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

Edycja: Upewnij się, że „$ B” albo nie istnieje, albo nie ma w nim softlink, w przeciwnym razie podążaj relpathza tym linkiem, który nie jest tym, czego chcesz!

Działa to w prawie wszystkich obecnych systemach Linux. Jeśli readlink -mnie działa po twojej stronie, spróbuj readlink -fzamiast tego. Zobacz także https://gist.github.com/hilbix/1ec361d00a8178ae8ea0, aby uzyskać możliwe aktualizacje:

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

Uwagi:

  • Zadbano o to, aby był bezpieczny przed niepożądanym rozszerzaniem się meta znaków powłoki, w przypadku gdy nazwy plików zawierają *lub? .
  • Wynik ma być użyteczny jako pierwszy argument do ln -s:
    • relpath / /daje, .a nie pusty ciąg
    • relpath a adaje a, nawet jeśli ajest to katalog
  • Przebadano również najczęstsze przypadki, aby uzyskać rozsądne wyniki.
  • W tym rozwiązaniu stosuje się dopasowywanie prefiksów ciągów readlink jest wymagane do kanonizacji ścieżek.
  • Dzięki readlink -mtemu działa również dla jeszcze nieistniejących ścieżek.

W starych systemach, gdzie readlink -mnie jest dostępny, readlink -fkończy się niepowodzeniem, jeśli plik nie istnieje. Prawdopodobnie potrzebujesz takiego obejścia (niesprawdzone!):

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

Nie jest to całkiem poprawne w przypadku $1dołączania .lub ..nieistniejących ścieżek (jak w /doesnotexist/./a), ale powinno obejmować większość przypadków.

(Zamień readlink -m --powyżej na readlink_missing.)

Edytuj ze względu na poniższą opinię

Oto test, czy ta funkcja rzeczywiście jest poprawna:

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"

Zdziwiony? To są prawidłowe wyniki ! Nawet jeśli uważasz, że to nie pasuje do pytania, oto dowód, że jest to poprawne:

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

Bez wątpienia ../barjest to dokładna i jedyna poprawna ścieżka względna strony barwidziana ze stronymoo . Wszystko inne byłoby po prostu złe.

Trywialne jest przyjęcie wyniku do pytania, które najwyraźniej zakłada, że currentjest to katalog:

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

To zwraca dokładnie to, o co prosiliśmy.

A zanim podniesiesz brew, oto nieco bardziej złożony wariant relpath(zauważ niewielką różnicę), który powinien również działać w przypadku składni URL (tak, że końcowy /zachowuje się dzięki niektórym bash-magicznym):

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

A oto kontrole, aby wyjaśnić: To naprawdę działa zgodnie z instrukcją.

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

A oto, jak można to wykorzystać, aby uzyskać pożądany wynik pytania:

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

Jeśli znajdziesz coś, co nie działa, daj mi znać w komentarzach poniżej. Dzięki.

PS:

Dlaczego argumenty relpath „odwróconego” w przeciwieństwie do wszystkich innych odpowiedzi tutaj?

Jeśli się zmienisz

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

do

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

wtedy możesz zostawić drugi parametr z dala, tak że BAZĄ jest bieżącym katalogiem / URL / czymkolwiek. To tylko zasada uniksowa, jak zwykle.

Jeśli Ci się nie podoba, wróć do systemu Windows. Dzięki.

Tino
źródło
3

Niestety, odpowiedź Marka Rushakoffa (teraz usunięta - odwoływała się do kodu stąd ) nie wydaje się działać poprawnie, gdy jest przystosowana do:

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

Sposób myślenia opisany w komentarzu można dopracować, aby działał poprawnie w większości przypadków. Mam zamiar założyć, że skrypt pobiera argument źródłowy (gdzie jesteś) i argument docelowy (gdzie chcesz się dostać) i że oba są absolutnymi ścieżkami lub oba są względne. Jeśli jeden jest bezwzględny, a drugi względny, najłatwiej jest poprzedzić względną nazwę bieżącym katalogiem roboczym - ale poniższy kod tego nie robi.


Strzec się

Poniższy kod jest bliski prawidłowego działania, ale nie jest do końca poprawny.

  1. Problem został rozwiązany w komentarzach Dennisa Williamsona.
  2. Istnieje również problem polegający na tym, że to czysto tekstowe przetwarzanie nazw ścieżek może być poważnie zepsute przez dziwne dowiązania symboliczne.
  3. Kod nie obsługuje zbłąkanych „kropek” w ścieżkach takich jak „ xyz/./pqr”.
  4. Kod nie obsługuje bezpańskich „podwójnych kropek” w ścieżkach takich jak „xyz/../pqr ”.
  5. Trywialnie: kod nie usuwa wiodących ./ścieżek.

Kod Dennisa jest lepszy, ponieważ naprawia 1 i 5 - ale ma te same problemy 2, 3, 4. Z tego powodu użyj kodu Dennisa (i głosuj wcześniej).

(Uwaga: POSIX udostępnia wywołanie systemowe, realpath()które rozwiązuje nazwy ścieżek, dzięki czemu nie ma w nich żadnych dowiązań symbolicznych. Zastosowanie tego do nazw wejściowych, a następnie użycie kodu Dennisa dałoby poprawną odpowiedź za każdym razem. otacza realpath()- już to zrobiłem - ale nie znam standardowego narzędzia, które to robi.)


W tym celu uważam, że Perl jest łatwiejszy w użyciu niż powłoka, chociaż bash ma przyzwoitą obsługę tablic i prawdopodobnie mógłby to zrobić - ćwiczenie dla czytelnika. Tak więc, mając dwie kompatybilne nazwy, podziel je na części:

  • Ustaw ścieżkę względną na pustą.
  • Chociaż komponenty są takie same, przejdź do następnego.
  • Gdy odpowiadające komponenty są różne lub nie ma już komponentów dla jednej ścieżki:
  • Jeśli nie ma już żadnych składników źródłowych, a ścieżka względna jest pusta, dodaj „.” na początek.
  • Dla każdego pozostałego komponentu źródłowego poprzedź ścieżkę względną „../”.
  • Jeśli nie ma pozostałych komponentów docelowych, a ścieżka względna jest pusta, dodaj „.” na początek.
  • Dla każdego pozostałego komponentu docelowego dodaj komponent na końcu ścieżki po ukośniku.

A zatem:

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

Skrypt testowy (nawiasy kwadratowe zawierają spację i tabulator):

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

Dane wyjściowe ze skryptu testowego:

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

Ten skrypt Perla działa dość dokładnie na Uniksie (nie uwzględnia wszystkich zawiłości nazw ścieżek Windows) w obliczu dziwnych danych wejściowych. Używa modułu Cwdi jego funkcji realpathdo rozpoznania prawdziwej ścieżki nazw, które istnieją, i dokonuje analizy tekstowej ścieżek, które nie istnieją. We wszystkich przypadkach oprócz jednego generuje to samo wyjście, co skrypt Dennisa. Odmienny przypadek to:

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

Dwa wyniki są równoważne - po prostu nie identyczne. (Dane wyjściowe pochodzą z delikatnie zmodyfikowanej wersji skryptu testowego - poniższy skrypt Perla po prostu drukuje odpowiedź, a nie dane wejściowe i odpowiedź jak w powyższym skrypcie.) Teraz: czy powinienem wyeliminować niedziałającą odpowiedź? Może...

#!/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);
Jonathan Leffler
źródło
Twój /home/part1/part2na /ma zbyt wielu ../. W przeciwnym razie mój skrypt pasuje do twoich wyników, z wyjątkiem tego, że mój dodaje niepotrzebne .na końcu tego, gdzie jest miejsce docelowe .i nie używam ./na początku tych, które schodzą bez wchodzenia w górę.
Wstrzymano do odwołania.
@Dennis: Spędziłem czas patrząc na wyniki - czasami widziałem ten problem, a czasem nie mogłem go znaleźć ponownie. Usunięcie wiodącego „./” to kolejny trywialny krok. Twój komentarz na temat „nie osadzono. lub… ”jest również istotne. Właściwie zaskakująco trudno jest właściwie wykonać tę pracę - podwójnie, więc jeśli którekolwiek z tych nazw jest w rzeczywistości dowiązaniem symbolicznym; oboje przeprowadzamy analizę czysto tekstową.
Jonathan Leffler,
@Dennis: Oczywiście, chyba że masz sieć Newcastle Connection, próba uzyskania dostępu do roota jest daremna, więc ../../../ .. i ../../ .. są równoważne. Jest to jednak czysty eskapizm; twoja krytyka jest słuszna. (Połączenie z Newcastle pozwoliło ci skonfigurować i używać notacji /../host/path/on/remote/machine, aby dostać się do innego hosta - porządny schemat. Wierzę, że to obsługiwane /../../network/host/ ścieżka / on / remote / network / i / host też. Jest na Wikipedii.)
Jonathan Leffler
Zamiast tego mamy teraz podwójne cięcie UNC.
Wstrzymano do odwołania.
1
Narzędzie „readlink” (przynajmniej wersja GNU) może wykonać odpowiednik realpath (), jeśli przekażesz mu opcję „-f”. Na przykład w moim systemie readlink /usr/bin/vidaje /etc/alternatives/vi, ale to kolejne dowiązanie symboliczne - podczas gdy readlink -f /usr/bin/vidaje /usr/bin/vim.basic, które jest ostatecznym celem wszystkich dowiązań symbolicznych ...
psmears
3

Podjąłem twoje pytanie jako wyzwanie, aby napisać to w „przenośnym” kodzie powłoki, tj

  • z myślą o powłoce POSIX
  • bez bachizmów takich jak tablice
  • unikaj dzwonienia na zewnątrz jak zarazy. W skrypcie nie ma ani jednego widelca! To sprawia, że ​​jest niesamowicie szybki, szczególnie w systemach ze znacznym obciążeniem wideł, takich jak cygwin.
  • Musi radzić sobie ze znakami glob w ścieżkach (*,?, [,])

Działa na dowolnej powłoce zgodnej z POSIX (zsh, bash, ksh, ash, busybox, ...). Zawiera nawet testsuite, aby zweryfikować jego działanie. Kanonizacja ścieżek pozostawia się jako ćwiczenie. :-)

#!/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 :
Jens
źródło
2

Moje rozwiązanie:

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

}
Anonimowy
źródło
To jest wyjątkowo przenośne :)
Anonimowy
2

Oto moja wersja. Jest on oparty na odpowiedź przez @Offirmo . Sprawiłem, że jest kompatybilny z Dash i naprawiłem następującą awarię testową:

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

Teraz:

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

Zobacz kod:

# 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
}
Ray Donnelly
źródło
1

Zgadnij, ten też powinien załatwić sprawę ... (zawiera wbudowane testy) :)

OK, spodziewaliśmy się trochę narzutu, ale robimy tutaj powłokę Bourne'a! ;)

#!/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
D4rk1B0t
źródło
1

Ten skrypt działa tylko na nazwach ścieżek. Nie wymaga żadnych plików. Jeśli przekazane ścieżki nie są bezwzględne, zachowanie jest nieco niezwykłe, ale powinno działać zgodnie z oczekiwaniami, jeśli obie ścieżki są względne.

Testowałem go tylko na systemie OS X, więc może nie być przenośny.

#!/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
juancn
źródło
Ponadto kod jest nieco kopiowany i wklejany, ale potrzebowałem tego dość szybko.
juancn
Fajnie ... właśnie tego potrzebowałem. Dołączyłeś procedurę kanonizacji, która jest lepsza niż większość innych, które widziałem (które zwykle polegają na zamianach wyrażeń regularnych).
drwatsoncode
0

Ta odpowiedź nie dotyczy części Bash pytania, ale ponieważ próbowałem użyć odpowiedzi w tym pytaniu, aby zaimplementować tę funkcjonalność w Emacsie , wyrzucę ją tam.

Emacs ma w tym celu funkcję natychmiast po wyjęciu z pudełka:

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"
fakedrake
źródło
Zauważ, że uważam, że odpowiedź na python, którą ostatnio dodałem ( relpathfunkcja) zachowuje się identycznie file-relative-namedla podanych przypadków testowych.
Gary Wiśniewski
-1

Oto skrypt powłoki, który robi to bez wywoływania innych programów:

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

źródło: http://www.ynform.org/w/Pub/Relpath

Biedny Yorick
źródło
1
Nie jest to tak naprawdę przenośne ze względu na użycie konstrukcji $ {param / pattern / subst}, która nie jest POSIX (od 2011 r.).
Jens,
Przywoływane źródło ynform.org/w/Pub/Relpath wskazuje na całkowicie zniekształconą stronę wiki zawierającą treść skryptu kilka razy, przeplataną liniami vi tyldy, komunikatami o błędach dotyczących nieznalezionych poleceń i tym podobne. Całkowicie bezużyteczne dla kogoś, kto bada oryginał.
Jens
-1

Potrzebowałem czegoś takiego, ale to również rozwiązało dowiązania symboliczne. Odkryłem, że pwd ma w tym celu flagę -P. Dołączono fragment mojego skryptu. Jest w funkcji w skrypcie powłoki, stąd 1 $ i 2 $. Wartość wynikowa, która jest ścieżką względną od START_ABS do END_ABS, znajduje się w zmiennej UPDIRS. Skrypty cd do każdego katalogu parametrów w celu wykonania pwd -P, co oznacza również, że obsługiwane są względne parametry ścieżki. Na zdrowie, 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"
sjjh
źródło