Konvertieren Sie den absoluten Pfad mit Bash in einen relativen Pfad in einem aktuellen Verzeichnis


261

Beispiel:

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

# Magic

relative="../../bar"

Wie erstelle ich die Magie (hoffentlich nicht zu komplizierter Code ...)?


7
Zum Beispiel (mein aktueller Fall) für die Angabe des relativen Pfads von gcc, damit relative Debug-Informationen generiert werden können, die auch dann verwendet werden können, wenn sich der Quellpfad ändert.
Offirmo

Eine ähnliche Frage wurde auf U & L gestellt: unix.stackexchange.com/questions/100918/… . Eine der Antworten (@Gilles) erwähnt ein Tool, Symlinks , mit dem sich dieses Problem leicht lösen lässt.
Slm

25
Einfach : realpath --relative-to=$absolute $current.
Kenorb

Antworten:


228

Die Verwendung von realpath aus GNU coreutils 8.23 ​​ist meiner Meinung nach am einfachsten:

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

Beispielsweise:

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

7
Schade, dass das Paket unter Ubuntu 14.04 veraltet ist und nicht die Option --relative-to enthält.
kzh

3
Funktioniert gut unter Ubuntu 16.04
Cayhorstmann

7
$ realpath --relative-to="${PWD}" "$file"ist nützlich, wenn Sie die Pfade relativ zum aktuellen Arbeitsverzeichnis möchten.
dcoles

1
Dies ist korrekt für Dinge innerhalb des /usr/bin/nmap/Pfades, aber nicht für /usr/bin/nmap: von nmapbis ist /tmp/testinges nur ../../und nicht dreimal ../. Es funktioniert jedoch, weil ..auf dem rootfs zu tun ist /.
Patrick B.

6
Als @PatrickB. impliziert, --relative-to=…erwartet ein Verzeichnis und prüft NICHT. Das bedeutet, dass Sie ein zusätzliches "../" erhalten, wenn Sie einen Pfad relativ zu einer Datei anfordern (wie dieses Beispiel zu tun scheint, da es /usr/binselten oder nie Verzeichnisse enthält und nmapnormalerweise eine Binärdatei ist)
IBBoard

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

gibt:

../../bar

11
Es funktioniert und macht die Alternativen lächerlich. Das ist ein Bonus für mich xD
hasvn

31
+1. Ok, du hast geschummelt ... aber das ist zu gut, um nicht benutzt zu werden! relpath(){ python -c "import os.path; print os.path.relpath('$1','${2:-$PWD}')" ; }
MestreLion

4
Leider ist dies nicht allgemein verfügbar: os.path.relpath ist neu in Python 2.6.
Chen Levy

15
@ChenLevy: Python 2.6 wurde im Jahr 2008 veröffentlicht. Kaum zu glauben, dass es im Jahr 2012 nicht allgemein verfügbar war.
MestreLion

11
python -c 'import os, sys; print(os.path.relpath(*sys.argv[1:]))'funktioniert am natürlichsten und zuverlässigsten.
Musiphil

31

Dies ist eine korrigierte, voll funktionsfähige Verbesserung der derzeit am besten bewerteten Lösung von @pini (die leider nur wenige Fälle behandelt).

Erinnerung: '-z' testet, ob die Zeichenfolge die Länge Null hat (= leer) und '-n' testet, ob die Zeichenfolge nicht leer ist.

# 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

Testfälle:

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
Integriert in die Offirmo-Shell lib github.com/Offirmo/offirmo-shell-lib , Funktion «OSL_FILE_find_relative_path» (Datei «osl_lib_file.sh»)
Offirmo

1
+1. Es kann leicht gemacht werden, um alle Pfade (nicht nur absolute Pfade, die mit / beginnen) zu handhaben, indem source=$1; target=$2durchsource=$(realpath $1); target=$(realpath $2)
Josh Kelley

2
@ Josh in der Tat, vorausgesetzt, die Verzeichnisse existieren tatsächlich ... was für die Unit-Tests unpraktisch war;) Aber im realen Gebrauch ja, realpathwird empfohlen, oder source=$(readlink -f $1)etc., wenn realpath nicht verfügbar ist (nicht Standard)
Offirmo

Ich definierte $sourceund $targetmag dies: `if [[-e $ 1]]; dann source = $ (readlink -f $ 1); sonst Quelle = $ 1; fi if [[-e $ 2]]; dann target = $ (readlink -f $ 2); sonst Ziel = $ 2; Auf diese Weise könnte die Funktion sowohl reale / existierende relative Pfade als auch fiktive Verzeichnisse verarbeiten.
Nathan S. Watson-Haigh

1
@ NathanS.Watson-Haigh Noch besser, ich entdeckte vor kurzem readlinkhat eine -mOption, die genau das tut;)
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/}

Wunderbares Drehbuch - kurz und sauber. Ich habe eine Bearbeitung angewendet (Warten auf Peer Review): common_part = $ source / common_part = $ (dirname $ common_part) / echo $ {back} $ {target # $ common_part} Das vorhandene Skript würde aufgrund einer unangemessenen Übereinstimmung beim Start des Verzeichnisnamens fehlschlagen beim Vergleich zum Beispiel: "/ foo / bar / baz" mit "/ foo / barsucks / bonk". Durch Verschieben des Schrägstrichs in die Variable und aus der endgültigen Bewertung wird dieser Fehler behoben.
JCwenger

3
Dieses Skript funktioniert einfach nicht. Schlägt ein einfacher "One Directory Down" -Test fehl. Die Bearbeitungen von jcwenger funktionieren etwas besser, fügen aber tendenziell ein zusätzliches "../" hinzu.
Dr. Person Person II

1
es schlägt für mich in einigen Fällen fehl, wenn ein abschließendes "/" auf dem Argument steht; Wenn beispielsweise $ 1 = "$ HOME /" und $ 2 = "$ HOME / temp", wird "/ home / user / temp /" zurückgegeben. Wenn jedoch $ 1 = $ HOME, wird der relative Pfad "temp" ordnungsgemäß zurückgegeben. Sowohl source = $ 1 als auch target = $ 2 könnten daher mit sed (oder durch Bash-Variablensubstitution, aber das kann unnötig undurchsichtig sein) "bereinigt" werden, z. B. => source = $ (echo "$ {1}" | sed 's / \ / * $ // ')
Michael

1
Kleinere Verbesserung: Anstatt Quelle / Ziel direkt auf $ 1 und $ 2 zu setzen, gehen Sie wie folgt vor: Quelle = $ (cd $ 1; pwd) Ziel = $ (cd $ 2; pwd). Auf diese Weise werden Pfade mit behandelt. und .. richtig.
Joseph Garvin

4
Obwohl diese Antwort die am besten gewählte Antwort ist, weist sie viele Einschränkungen auf, weshalb so viele andere Antworten veröffentlicht werden. Sehen Sie sich stattdessen die anderen Antworten an, insbesondere die mit Testfällen. Und bitte stimmen Sie diesem Kommentar zu!
Offirmo

25

Es ist seit 2001 in Perl integriert und funktioniert daher auf nahezu jedem erdenklichen System, sogar auf VMS .

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

Auch die Lösung ist leicht zu verstehen.

Also für Ihr Beispiel:

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

... würde gut funktionieren.


3
saywar in perl nicht als Protokoll verfügbar, könnte aber hier effektiv verwendet werden. perl -MFile::Spec -E 'say File::Spec->abs2rel(@ARGV)'
William Pursell

+1 aber siehe auch diese ähnliche Antwort, die älter ist (Feb 2012). Lesen Sie auch die einschlägigen Kommentare von William Pursell . Meine Version besteht aus zwei Befehlszeilen: perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target"und perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target" "$origin". Das erste einzeilige Perl- Skript verwendet ein Argument (Ursprung ist das aktuelle Arbeitsverzeichnis). Das zweite einzeilige Perl- Skript verwendet zwei Argumente.
Olibre

3
Das sollte die akzeptierte Antwort sein. perlkann fast überall gefunden werden, obwohl die Antwort immer noch einzeilig ist.
Dmitry Ginzburg

19

Vorausgesetzt, Sie haben Folgendes installiert: bash, pwd, dirname, echo; dann ist relpath

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

Ich habe die Antwort von golfed Pini und ein paar anderen Ideen

Hinweis : Dies erfordert, dass beide Pfade vorhandene Ordner sind. Dateien funktionieren nicht .


2
Ideale Antwort: Funktioniert mit / bin / sh, benötigt keinen Readlink, Python, Perl -> ideal für Light / Embedded-Systeme oder Windows Bash Console
Francois

2
Leider setzt dies voraus, dass der Pfad vorhanden ist, was nicht immer erwünscht ist.
Drwatsoncode

Göttliche Antwort. Das cd-pwd-Zeug dient zum Auflösen von Links, denke ich? Schönes Golfen!
Teck-Freak

15

Python os.path.relpathals Shell-Funktion

Das Ziel dieser relpathÜbung ist es os.path.relpath, die von xni vorgeschlagene Funktion von Python 2.7 nachzuahmen (verfügbar ab Python Version 2.6, funktioniert aber nur in 2.7 ordnungsgemäß) . Infolgedessen können einige der Ergebnisse von den in anderen Antworten angegebenen Funktionen abweichen.

(Ich habe nicht mit Zeilenumbrüchen in Pfaden getestet, nur weil dadurch die Validierung aufgrund eines Aufrufs python -cvon ZSH unterbrochen wird. Dies wäre sicherlich mit einigem Aufwand möglich.)

In Bezug auf „Magie“ in Bash habe ich es längst aufgegeben, in Bash nach Magie zu suchen, aber seitdem habe ich alle Magie gefunden, die ich brauche, und noch einige mehr in ZSH.

Folglich schlage ich zwei Implementierungen vor.

Die erste Implementierung soll vollständig POSIX-konform sein . Ich habe es mit /bin/dashDebian 6.0.6 "Squeeze" getestet . Es funktioniert auch perfekt mit /bin/shOS X 10.8.3, bei dem es sich tatsächlich um Bash Version 3.2 handelt, die vorgibt, eine POSIX-Shell zu sein.

Die zweite Implementierung ist eine ZSH-Shell-Funktion, die robust gegen mehrere Schrägstriche und andere Belästigungen in Pfaden ist. Wenn Sie ZSH zur Verfügung haben, ist dies die empfohlene Version, auch wenn Sie sie in der unten dargestellten Skriptform (dh mit einem Shebang von #!/usr/bin/env zsh) aus einer anderen Shell aufrufen .

Schließlich habe ich ein ZSH-Skript geschrieben, das die Ausgabe des relpathBefehls überprüft, der in $PATHden in anderen Antworten angegebenen Testfällen gefunden wurde . Ich habe diesen Tests etwas Würze hinzugefügt, indem ich Leerzeichen, Tabulatoren und Interpunktion wie ! ? *hier und da hinzugefügt habe, und auch einen weiteren Test mit exotischen UTF-8-Zeichen in vim-powerline durchgeführt .

POSIX- Shell-Funktion

Erstens die POSIX-kompatible Shell-Funktion. Es funktioniert mit einer Vielzahl von Pfaden, bereinigt jedoch nicht mehrere Schrägstriche und löst keine Symlinks auf.

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

ZSH-Shell-Funktion

Nun die robustere zshVersion. Wenn Sie möchten, dass die Argumente in reale Pfade à la aufgelöst werden realpath -f(im Linux- coreutilsPaket verfügbar ), ersetzen Sie die :aZeilen 3 und 4 durch :A.

Um dies in zsh zu verwenden, entfernen Sie die erste und letzte Zeile und legen Sie sie in einem Verzeichnis ab, das sich in Ihrer $FPATHVariablen befindet.

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

Testskript

Zum Schluss das Testskript. Es wird eine Option akzeptiert, nämlich -vdie ausführliche Ausgabe zu aktivieren.

#!/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
Funktioniert nicht, wenn der erste Weg leider endet /.
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")"; }

Das obige Shell-Skript wurde von Pinis inspiriert (Danke!). Es löst einen Fehler im Syntaxhervorhebungsmodul von Stack Overflow aus (zumindest in meinem Vorschaurahmen). Bitte ignorieren Sie, wenn die Hervorhebung falsch ist.

Einige Notizen:

  • Fehler behoben und Code verbessert, ohne die Codelänge und -komplexität wesentlich zu erhöhen
  • Setzen Sie Funktionen in Funktionen ein, um die Verwendung zu vereinfachen
  • Behaltene Funktionen POSIX-kompatibel, so dass sie mit allen POSIX-Shells funktionieren (sollten) (getestet mit Dash, Bash und Zsh in Ubuntu Linux 12.04)
  • Verwendet nur lokale Variablen, um zu vermeiden, dass globale Variablen überlastet werden und der globale Namensraum verschmutzt wird
  • Beide Verzeichnispfade müssen NICHT vorhanden sein (Voraussetzung für meine Anwendung)
  • Pfadnamen können Leerzeichen, Sonderzeichen, Steuerzeichen, Backslashes, Tabulatoren, ', ",?, *, [,] Usw. enthalten.
  • Die Kernfunktion "relPath" verwendet nur integrierte POSIX-Shell, benötigt jedoch kanonische absolute Verzeichnispfade als Parameter
  • Die erweiterte Funktion "relpath" kann beliebige Verzeichnispfade (auch relativ, nicht kanonisch) verarbeiten, erfordert jedoch das externe GNU-Kerndienstprogramm "readlink".
  • Aus zwei Gründen wurde das eingebaute "Echo" vermieden und stattdessen das eingebaute "printf" verwendet:
  • Um unnötige Konvertierungen zu vermeiden, werden Pfadnamen verwendet, wenn sie von Shell- und Betriebssystem-Dienstprogrammen zurückgegeben und erwartet werden (z. B. cd, ln, ls, find, mkdir; im Gegensatz zu Pythons "os.path.relpath", das einige Backslash-Sequenzen interpretiert).
  • Mit Ausnahme der genannten Backslash-Sequenzen gibt die letzte Funktionszeile "relPath" Pfadnamen aus, die mit Python kompatibel sind:

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

    Die letzte Zeile kann durch eine Zeile ersetzt (und vereinfacht) werden

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

    Ich bevorzuge letzteres, weil

    1. Dateinamen können direkt an von relPath erhaltene Verzeichnispfade angehängt werden, z.

      ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
    2. Bei symbolischen Links im selben Verzeichnis, die mit dieser Methode erstellt wurden "./", wird dem Dateinamen nicht das Hässliche vorangestellt.

  • Wenn Sie einen Fehler finden, wenden Sie sich bitte an linuxball (at) gmail.com und ich werde versuchen, ihn zu beheben.
  • Regressionstestsuite hinzugefügt (auch POSIX-Shell-kompatibel)

Codeauflistung für Regressionstests (einfach an das Shell-Skript anhängen):

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

Nicht viele der Antworten hier sind für den täglichen Gebrauch praktisch. Da es sehr schwierig ist, dies in reiner Bash richtig zu machen, schlage ich die folgende zuverlässige Lösung vor (ähnlich einem in einem Kommentar vergrabenen Vorschlag):

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

Anschließend können Sie den relativen Pfad basierend auf dem aktuellen Verzeichnis abrufen:

echo $(relpath somepath)

oder Sie können angeben, dass der Pfad relativ zu einem bestimmten Verzeichnis ist:

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

Der einzige Nachteil ist, dass dies Python erfordert, aber:

  • Es funktioniert identisch in jedem Python> = 2.6
  • Es ist nicht erforderlich, dass die Dateien oder Verzeichnisse vorhanden sind.
  • Dateinamen können eine größere Auswahl an Sonderzeichen enthalten. Beispielsweise funktionieren viele andere Lösungen nicht, wenn Dateinamen Leerzeichen oder andere Sonderzeichen enthalten.
  • Es ist eine einzeilige Funktion, die Skripte nicht überladen.

Beachten Sie, dass Lösungen, die enthalten sind basenameoder dirnamenicht unbedingt besser sein müssen, da sie eine coreutilsInstallation erfordern . Wenn jemand eine reine bashLösung hat, die zuverlässig und einfach ist (und keine verworrene Neugier), wäre ich überrascht.


Dies scheint bei weitem der robusteste Ansatz zu sein.
dimo414

7

Dieses Skript liefert korrekte Ergebnisse nur für Eingaben, die absolute Pfade oder relative Pfade ohne .oder sind ..:

#!/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
Dies scheint zu funktionieren. Wenn die Verzeichnisse tatsächlich vorhanden sind, kann die Verwendung von $ (readlink -f $ 1) und $ (readlink -f $ 2) für die Eingaben das Problem beheben, bei dem "." oder ".." erscheint in den Eingängen. Dies kann zu Problemen führen, wenn die Verzeichnisse nicht vorhanden sind.
Dr. Person Person II

7

Ich würde Perl nur für diese nicht so triviale Aufgabe verwenden:

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

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

1
+1, würde aber empfehlen: perl -MFile::Spec -e "print File::Spec->abs2rel('$absolute','$current')"damit absolut und aktuell angegeben werden.
William Pursell

Ich mag relative=$(perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$absolute" "$current"). Dies stellt sicher, dass die Werte selbst keinen Perl-Code enthalten können!
Erik Aronesty

6

Eine leichte Verbesserung der Antworten von Kasku und Pini , die besser mit Leerzeichen spielt und das Übergeben relativer Pfade ermöglicht:

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

Testen:

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

+1 für Kompaktheit und Schüchternheit. Sie sollten jedoch fordern auch readlinkauf $(pwd).
DevSolar

2
Relativ bedeutet nicht, dass die Datei im selben Verzeichnis abgelegt werden muss.
Greenoldman

Obwohl die ursprüngliche Frage nicht viele Testfälle enthält, schlägt dieses Skript für einfache Tests wie das Ermitteln des relativen Pfads von / home / user1 zu / home / user2 fehl (richtige Antwort: ../user2). Das Skript von pini / jcwenger funktioniert für diesen Fall.
Michael

4

Noch eine andere Lösung, pure bash+ GNU readlinkfür die einfache Verwendung in folgendem Kontext:

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

Bearbeiten: Stellen Sie sicher, dass "$ B" entweder nicht vorhanden ist oder in diesem Fall kein Softlink vorhanden ist. Andernfalls relpathfolgt dieser Link, der nicht Ihren Wünschen entspricht !

Dies funktioniert unter fast allen aktuellen Linux. Wenn readlink -mes an Ihrer Seite nicht funktioniert, versuchen Sie es readlink -fstattdessen. Siehe auch https://gist.github.com/hilbix/1ec361d00a8178ae8ea0 für mögliche Updates:

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

Anmerkungen:

  • Es wurde darauf geachtet, dass es gegen unerwünschte Shell-Metazeichenerweiterungen sicher ist, falls Dateinamen *oder enthalten ?.
  • Die Ausgabe soll als erstes Argument für Folgendes verwendet werden können ln -s:
    • relpath / /gibt .und nicht die leere Zeichenfolge
    • relpath a agibt a, auch wenn azufällig ein Verzeichnis ist
  • Die meisten Fälle wurden getestet, um auch vernünftige Ergebnisse zu erzielen.
  • Diese Lösung verwendet daher den String-Präfix-Abgleich readlink erforderlich, um Pfade zu kanonisieren.
  • Dank readlink -mdessen funktioniert es auch für noch nicht vorhandene Pfade.

Auf alten Systemen, auf denen readlink -mkeine verfügbar ist, readlink -fschlägt dies fehl, wenn die Datei nicht vorhanden ist. Sie benötigen also wahrscheinlich eine Problemumgehung wie diese (ungetestet!):

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

Dies ist nicht wirklich richtig, wenn $1Includes .oder ..nicht vorhandene Pfade enthalten sind (wie in/doesnotexist/./a ) enthalten sind, sollte aber die meisten Fälle abdecken.

(Ersetzen Sie readlink -m --oben durchreadlink_missing .)

Bearbeiten wegen der folgenden Downvote

Hier ist ein Test, dass diese Funktion tatsächlich korrekt ist:

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"

Verwirrt? Nun, das sind die richtigen Ergebnisse ! Auch wenn Sie der Meinung sind, dass es nicht zur Frage passt, ist hier der Beweis, dass dies richtig ist:

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

Ohne Zweifel ../barist der genaue und einzige korrekte relative Pfad der Seite barvon der Seite aus zu sehenmoo . Alles andere wäre einfach falsch.

Es ist trivial, die Ausgabe auf die Frage zu übernehmen, die anscheinend davon ausgeht, dass currentes sich um ein Verzeichnis handelt:

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

Dies gibt genau das zurück, wonach gefragt wurde.

Und bevor Sie eine Augenbraue hochziehen, ist hier eine etwas komplexere Variante von relpath(erkennen Sie den kleinen Unterschied), die auch für die URL-Syntax funktionieren sollte (so dass ein Trailing /dank einiger bashMagier überlebt ):

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

Und hier sind die Überprüfungen, um klar zu machen: Es funktioniert wirklich wie gesagt.

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

Und so kann dies verwendet werden, um das gewünschte Ergebnis aus der Frage zu erhalten:

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

Wenn Sie etwas finden, das nicht funktioniert, lassen Sie es mich bitte in den Kommentaren unten wissen. Vielen Dank.

PS:

Warum sind die Argumente von relpath "umgekehrt" im Gegensatz zu allen anderen Antworten hier?

Wenn Sie sich ändern

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

zu

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

Dann können Sie den zweiten Parameter weglassen, sodass die BASE das aktuelle Verzeichnis / URL / was auch immer ist. Das ist wie immer nur das Unix-Prinzip.

Wenn Ihnen das nicht gefällt, kehren Sie bitte zu Windows zurück. Vielen Dank.


3

Leider scheint Mark Rushakoffs Antwort (jetzt gelöscht - sie verweist auf den Code von hier ) nicht richtig zu funktionieren, wenn sie angepasst wird an:

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

Das im Kommentar skizzierte Denken kann verfeinert werden, damit es in den meisten Fällen richtig funktioniert. Ich gehe davon aus, dass das Skript ein Quellargument (wo Sie sich befinden) und ein Zielargument (wo Sie hin wollen) verwendet und dass entweder beide absolute Pfadnamen oder beide relativ sind. Wenn einer absolut und der andere relativ ist, ist es am einfachsten, dem relativen Namen das aktuelle Arbeitsverzeichnis voranzustellen - der folgende Code tut dies jedoch nicht.


In acht nehmen

Der folgende Code funktioniert fast korrekt, ist aber nicht ganz richtig.

  1. In den Kommentaren von Dennis Williamson wird das Problem angesprochen.
  2. Es gibt auch ein Problem, dass diese rein textuelle Verarbeitung von Pfadnamen und Sie durch seltsame Symlinks ernsthaft durcheinander gebracht werden können.
  3. Der Code behandelt keine streunenden 'Punkte' in Pfaden wie ' xyz/./pqr'.
  4. Der Code behandelt keine streunenden 'Doppelpunkte' in Pfaden wie 'xyz/../pqr '.
  5. Trivial: Der Code entfernt keine führenden ' ./' aus Pfaden.

Dennis 'Code ist besser, weil er 1 und 5 behebt - hat aber die gleichen Probleme 2, 3, 4. Verwenden Sie deshalb Dennis' Code (und stimmen Sie ihn vorher ab).

(Hinweis: POSIX bietet einen Systemaufruf realpath(), mit dem Pfadnamen aufgelöst werden, sodass keine Symlinks mehr vorhanden sind. Wenn Sie diese auf die Eingabenamen anwenden und dann Dennis 'Code verwenden, erhalten Sie jedes Mal die richtige Antwort. Es ist trivial, den C-Code zu schreiben Wraps realpath()- ich habe es geschafft - aber ich kenne kein Standarddienstprogramm, das dies tut.)


Aus diesem Grund finde ich Perl einfacher zu verwenden als Shell, obwohl bash Arrays anständig unterstützt und dies wahrscheinlich auch tun könnte - Übung für den Leser. Teilen Sie sie also bei zwei kompatiblen Namen jeweils in Komponenten auf:

  • Setzen Sie den relativen Pfad auf leer.
  • Fahren Sie mit den nächsten fort, während die Komponenten identisch sind.
  • Wenn entsprechende Komponenten unterschiedlich sind oder keine Komponenten mehr für einen Pfad vorhanden sind:
  • Wenn keine verbleibenden Quellkomponenten vorhanden sind und der relative Pfad leer ist, fügen Sie "." zum Anfang.
  • Stellen Sie dem relativen Pfad für jede verbleibende Quellkomponente "../" voran.
  • Wenn keine Zielkomponenten mehr vorhanden sind und der relative Pfad leer ist, fügen Sie "." zum Anfang.
  • Fügen Sie für jede verbleibende Zielkomponente die Komponente nach einem Schrägstrich am Ende des Pfads hinzu.

So:

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

Testskript (die eckigen Klammern enthalten ein Leerzeichen und eine Registerkarte):

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

Ausgabe aus dem Testskript:

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

Dieses Perl-Skript funktioniert unter Unix ziemlich gründlich (es berücksichtigt nicht alle Komplexitäten von Windows-Pfadnamen) angesichts seltsamer Eingaben. Es verwendet das Modul Cwdund seine Funktion realpath, um den tatsächlichen Pfad der vorhandenen Namen aufzulösen, und führt eine Textanalyse für nicht vorhandene Pfade durch. In allen Fällen außer einem erzeugt es die gleiche Ausgabe wie Dennis 'Skript. Der abweichende Fall ist:

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

Die beiden Ergebnisse sind gleichwertig - nur nicht identisch. (Die Ausgabe stammt aus einer leicht modifizierten Version des Testskripts. Das folgende Perl-Skript druckt einfach die Antwort und nicht die Eingaben und die Antwort wie im obigen Skript.) Nun: Sollte ich die nicht funktionierende Antwort entfernen? Vielleicht...

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

Sie /home/part1/part2zu /hat man zu viele ../. Andernfalls stimmt mein Skript mit Ihrer Ausgabe überein, außer dass mein Skript .am Ende des Ziels, in dem sich das Ziel befindet, ein unnötiges hinzufügt, .und ich verwende ./am Anfang kein Skript , das absteigt, ohne nach oben zu gehen.
Bis auf weiteres angehalten.

@Dennis: Ich habe einige Zeit damit verbracht, die Ergebnisse zu überprüfen - manchmal konnte ich dieses Problem erkennen und manchmal konnte ich es nicht wiederfinden. Das Entfernen eines führenden './' ist ein weiterer trivialer Schritt. Ihr Kommentar zu 'no embedded. oder .. 'ist auch relevant. Es ist tatsächlich überraschend schwierig, die Arbeit richtig zu machen - doppelt, wenn einer der Namen tatsächlich ein Symlink ist; Wir machen beide eine reine Textanalyse.
Jonathan Leffler

@Dennis: Wenn Sie nicht über ein Newcastle Connection-Netzwerk verfügen, ist es natürlich vergeblich, über Root zu gehen. Daher sind ../../../ .. und ../../ .. gleichwertig. Das ist jedoch reine Flucht; Ihre Kritik ist richtig. (Mit Newcastle Connection konnten Sie die Notation /../host/path/on/remote/machine konfigurieren und verwenden, um zu einem anderen Host zu gelangen - ein ordentliches Schema. Ich glaube, es unterstützt /../../network/host/ Pfad / auf / Remote / Netzwerk / und / Host auch. Es ist auf Wikipedia.)
Jonathan Leffler

Stattdessen haben wir jetzt den doppelten Schrägstrich von UNC.
Bis auf weiteres angehalten.

1
Das Dienstprogramm "readlink" (zumindest die GNU-Version) kann das Äquivalent von realpath () ausführen, wenn Sie die Option "-f" übergeben. Zum Beispiel auf meinem System readlink /usr/bin/vigibt /etc/alternatives/vi, aber das ist ein weiterer Symlink - während readlink -f /usr/bin/vigibt /usr/bin/vim.basic, was das ultimative Ziel aller Symlinks ist ...
psmears

3

Ich nahm Ihre Frage als Herausforderung, dies in "tragbaren" Shell-Code zu schreiben, dh

  • mit Blick auf eine POSIX-Shell
  • Keine Bashismen wie Arrays
  • Vermeiden Sie es, externe Personen wie die Pest anzurufen. Es gibt keine einzige Gabelung im Skript! Das macht es unglaublich schnell, insbesondere auf Systemen mit erheblichem Gabelaufwand wie Cygwin.
  • Muss mit Glob-Zeichen in Pfadnamen umgehen (*,?, [,])

Es läuft auf jeder POSIX-konformen Shell (zsh, bash, ksh, ash, Busybox, ...). Es enthält sogar eine Testsuite, um den Betrieb zu überprüfen. Die Kanonisierung von Pfadnamen bleibt als Übung. :-)

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

Meine Lösung:

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

}

Dies ist extrem tragbar :)
Anonym

2

Hier ist meine Version. Es basiert auf der Antwort von @Offirmo . Ich habe es Dash-kompatibel gemacht und den folgenden Testfallfehler behoben:

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

Jetzt:

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

Siehe den Code:

# 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

Ich denke, dieser soll auch den Trick machen ... (kommt mit eingebauten Tests) :)

OK, etwas Overhead erwartet, aber wir machen hier Bourne Shell! ;)

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

Dieses Skript funktioniert nur mit den Pfadnamen. Es ist nicht erforderlich, dass eine der Dateien vorhanden ist. Wenn die übergebenen Pfade nicht absolut sind, ist das Verhalten etwas ungewöhnlich, aber es sollte wie erwartet funktionieren, wenn beide Pfade relativ sind.

Ich habe es nur unter OS X getestet, daher ist es möglicherweise nicht 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

Auch der Code ist ein bisschen kopieren und einfügen, aber ich brauchte das ziemlich schnell.
Juancn

Schön ... genau das, was ich brauchte. Und Sie haben eine kanonisierende Routine aufgenommen, die besser ist als die meisten anderen, die ich gesehen habe (die normalerweise auf Regex-Ersetzungen beruhen).
Drwatsoncode

0

Diese Antwort behandelt nicht den Bash-Teil der Frage, aber da ich versucht habe, die Antworten in dieser Frage zu verwenden, um diese Funktionalität in Emacs zu implementieren, werde ich sie dort rauswerfen.

Emacs hat tatsächlich eine sofort einsatzbereite Funktion:

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"

Beachten Sie, dass sich die Python-Antwort, die ich kürzlich hinzugefügt habe (die relpathFunktion), file-relative-namefür die von Ihnen bereitgestellten Testfälle identisch verhält .
Gary Wisniewski

-1

Hier ist ein Shell-Skript, das dies tut, ohne andere Programme aufzurufen:

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

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


1
Dies ist aufgrund der Verwendung des Konstrukts $ {param / pattern / subst}, das nicht POSIX ist (Stand 2011), nicht wirklich portabel.
Jens

Die referenzierte Quelle ynform.org/w/Pub/Relpath verweist auf eine vollständig verstümmelte Wiki-Seite, die mehrmals den Skriptinhalt enthält, vermischt mit vi-Tilde-Zeilen, Fehlermeldungen zu nicht gefundenen Befehlen und so weiter. Völlig nutzlos für jemanden, der das Original recherchiert.
Jens

-1

Ich brauchte so etwas, aber das löste auch symbolische Links auf. Ich habe festgestellt, dass pwd zu diesem Zweck ein -P-Flag hat. Ein Fragment meines Skripts ist angehängt. Es befindet sich innerhalb einer Funktion in einem Shell-Skript, daher die $ 1 und $ 2. Der Ergebniswert, der der relative Pfad von START_ABS zu END_ABS ist, befindet sich in der Variablen UPDIRS. Die Skript-CDs befinden sich in jedem Parameterverzeichnis, um das pwd -P auszuführen, und dies bedeutet auch, dass relative Pfadparameter behandelt werden. Prost, 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"
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.