web-dev-qa-db-de.com

Konvertieren Sie den absoluten Pfad in einen relativen Pfad, wenn Sie ein aktuelles Verzeichnis mit Bash verwenden

Beispiel:

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

# Magic

relative="../../bar"

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

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

gibt:

../../bar
147
xni

Die Verwendung von realpath aus GNU coreutils 8.23 ​​ist am einfachsten, denke ich:

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

Zum Beispiel:

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

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' Test, ob die Zeichenfolge null ist (= leer) und '-n' Test, 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"
30
Offirmo
#!/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/}
24
pini

Es ist seit 2001 in Perl integriert, sodass es auf fast jedem System funktioniert, das Sie sich vorstellen können, sogar VMS .

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

Die Lösung ist auch leicht zu verstehen.

Also für dein Beispiel:

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

... würde gut funktionieren.

19
Erik Aronesty

Pythons os.path.relpath als Shell-Funktion

Das Ziel dieser relpath-Übung ist es, die os.path.relpath-Funktion von Python 2.7 zu imitieren (verfügbar ab Python Version 2.6, funktioniert jedoch nur in 2.7 ordnungsgemäß), wie von xni vorgeschlagen. Infolgedessen können sich einige Ergebnisse von den in anderen Antworten bereitgestellten Funktionen unterscheiden.

(Ich habe mit Zeilenumbrüchen in Pfaden nicht getestet, nur weil sie die Validierung aufgrund des Aufrufs von python -c von ZSH bricht. Dies wäre sicherlich mit etwas Aufwand möglich.)

Bezüglich der „Magie“ in Bash habe ich vor langer Zeit aufgehört, nach Magie in Bash zu suchen, aber seitdem habe ich alle Magie gefunden, die ich brauche, und noch einige andere in ZSH.

Ich schlage daher zwei Implementierungen vor.

Die erste Implementierung zielt darauf ab, vollständig POSIX-konform zu sein. Ich habe es mit /bin/dash auf Debian 6.0.6 "Squeeze" getestet. Es funktioniert auch perfekt mit /bin/sh unter OS X 10.8.3, der eigentlich Bash-Version 3.2 ist, die vorgibt, eine POSIX-Shell zu sein.

Die zweite Implementierung ist eine ZSH-Shell-Funktion, die robust gegenüber mehrfachen Schrägstrichen und anderen Belästigungen in Pfaden ist. Wenn Sie über ZSH verfügen, ist dies die empfohlene Version, auch wenn Sie es in dem unten dargestellten Skriptformular (d. H. Mit einem Shebang von #!/usr/bin/env zsh) von einer anderen Shell aus aufrufen.

Schließlich habe ich ein ZSH-Skript geschrieben, das die Ausgabe des in $PATH gefundenen relpath-Befehls anhand der in anderen Antworten bereitgestellten Testfälle überprüft. Ich habe diesen Tests etwas Würze hinzugefügt, indem ich hier und dort einige Leerzeichen, Tabulatoren und Interpunktionszeichen wie ! ? * eingefügt habe, und habe noch einen weiteren Test mit exotischen UTF-8-Zeichen in vim-powerline hinzugefügt.

POSIX Shell-Funktion

Zunächst die POSIX-kompatible Shell-Funktion. Es funktioniert mit einer Vielzahl von Pfaden, reinigt jedoch nicht mehrere Schrägstriche oder Symbollinks.

#!/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 "[email protected]"

ZSH-Shell-Funktion

Nun die robustere Version zsh. Wenn Sie möchten, dass die Argumente in echte Pfade à la realpath -f (im Linux-Paket coreutils verfügbar) aufgelöst werden, ersetzen Sie :a in Zeile 3 und 4 durch :A.

Um dies in zsh zu verwenden, entfernen Sie die erste und letzte Zeile und platzieren Sie sie in einem Verzeichnis, das sich in Ihrer $FPATH-Variablen 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 "[email protected]"

Skript testen

Zum Schluss noch das Testskript. Es akzeptiert eine Option, nämlich -v, um die 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 "[email protected]"
fi
15
simonair
#!/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 pini's (Danke!) Inspiriert. Es löst einen Fehler aus im Syntaxhervorhebungsmodul von Stack Overflow (zumindest in meinem Vorschaubild ). Also bitte ignorieren, wenn die Hervorhebung falsch ist.

Einige Notizen: 

  • Fehler behoben und Code verbessert, ohne die Länge und Komplexität des Codes erheblich zu erhöhen
  • Setzen Sie Funktionen in Funktionen, um die Verwendung zu erleichtern 
  • Funktionen, die POSIX-kompatibel sind, damit sie mit allen POSIX-Shells funktionieren sollten (getestet mit Bash, Bash und Zsh in Ubuntu Linux 12.04).
  • Nur lokale Variablen verwendet, um zu vermeiden, dass globale Variablen verschmutzt werden und Den globalen Namensraum verschmutzt
  • Beide Verzeichnispfade müssen NICHT existieren (Voraussetzung für meine Anwendung)
  • Pfadnamen können Leerzeichen, Sonderzeichen, Steuerzeichen, Backslashes, Tabulatoren, ', ",?, *, [] Usw. enthalten.
  • Die Kernfunktion "relPath" verwendet nur POSIX-Shell-Builtins, erfordert jedoch die kanalischen absoluten Verzeichnispfade Als Parameter
  • Die erweiterte Funktion "relpath" kann mit beliebigen Verzeichnispfaden umgehen (auch Relativ, nicht kanonisch), erfordert jedoch das externe Dienstprogramm GNU "readlink".
  • Das eingebaute "Echo" wurde vermieden und stattdessen das integrierte "Printf" verwendet, aus zwei Gründen:
    • Aufgrund widersprüchlicher historischer Implementierungen des eingebauten "Echo" verhält sich.... In verschiedenen Shells unterschiedlich -> POSIX empfiehlt, dass printf echo vorgezogen wird.
    • Ein eingebautes "Echo" einiger POSIX-Shells wird einige Backslash -Sequenzen interpretieren und somit Pfadnamen beschädigen, die solche Sequenzen enthalten 
  • Um unnötige Konvertierungen zu vermeiden, werden Pfadnamen verwendet, wenn sie zurückgegeben werden Und von Shell- und OS-Dienstprogrammen erwartet werden (z. B. cd, ln, ls, find, mkdir; Im Gegensatz zu "os.path.relpath" von Python, die einige interpretieren Backslash Sequenzen)
  • Mit Ausnahme der erwähnten Backslash-Sequenzen gibt die letzte Zeile der Funktion "relPath" Pfadnamen aus, die mit Python kompatibel sind:

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

    Letzte Zeile kann durch Zeile ersetzt (und vereinfacht) werden

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

    Ich ziehe letzteres vor, weil

    1. Dateinamen können direkt an die durch relPath erhaltenen Pfade angehängt werden, z.

      ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
      
    2. Symbolische Links in demselben Verzeichnis, die mit dieser Methode erstellt wurden, haben nicht den "./" Dem Dateinamen vorangestellt.

  • Wenn Sie einen Fehler finden, wenden Sie sich bitte an linuxball (at) gmail.com. Ich versuche es, um den Fehler zu beheben.
  • Regressionstest-Suite 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
11
linuxball

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

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

Ich habe die Antwort von pini und ein paar anderen Ideen golfen

11
Alexx Roche

Ich würde einfach Perl 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'")')
6
user1205347

Eine leichte Verbesserung der kasku und Pini's - Antworten, die besser mit Leerzeichen spielt und relative Pfade zulässt:

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

Dieses Skript liefert nur dann korrekte Ergebnisse 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"
6

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

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

Dann 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 in jedem Python> = 2.6 identisch
  • Es ist nicht erforderlich, dass die Dateien oder Verzeichnisse vorhanden sind.
  • Dateinamen können eine breitere Palette von Sonderzeichen enthalten. Beispielsweise funktionieren viele andere Lösungen nicht, wenn Dateinamen .__ enthalten. Leerzeichen oder andere Sonderzeichen.
  • Es ist eine einzeilige Funktion, die Skripts nicht stört.

Beachten Sie, dass Lösungen, die basename oder dirname enthalten, nicht unbedingt besser sind, da sie die Installation von coreutils erfordern. Wenn jemand eine reine bash-Lösung hat, die zuverlässig und einfach ist (und keine verworrene Neugier), wäre ich überrascht.

4
Gary Wisniewski

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
3
Steve

Ich habe Ihre Frage als Herausforderung angenommen, dies in "tragbarem" Shell-Code zu schreiben, d. H.

  • mit einer POSIX-Shell im Auge
  • keine basismen wie Arrays
  • vermeiden Sie es, Externe wie die Pest anzurufen. Es gibt keine einzige Gabel im Skript! Das macht es besonders schnell 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 übrig. :-)

#!/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 :
3
Jens

Leider scheint Mark Rushakoffs Antwort (jetzt gelöscht - er hat den Code von hier referenziert) anscheinend nicht richtig zu funktionieren, wenn er angepasst wird an:

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

Das im Kommentar beschriebene 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 möchten) verwendet, und dass 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. Dies geschieht jedoch nicht mit dem folgenden Code.


In acht nehmen

Der folgende Code steht kurz davor, richtig zu funktionieren, ist aber nicht ganz richtig.

  1. In den Kommentaren von Dennis Williamson wird das Problem angesprochen.
  2. Es besteht auch das Problem, dass diese rein textliche Verarbeitung von Pfadnamen und Sie durch ernsthafte Symlinks ernsthaft in Unordnung geraten können.
  3. Der Code behandelt keine verirrten "Punkte" in Pfaden wie "xyz/./pqr".
  4. Der Code behandelt keine "doppelten Punkte" in Pfaden wie "xyz/../pqr".
  5. Trivial: Der Code entfernt nicht das führende './' aus den Pfaden.

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

(Hinweis: POSIX stellt einen Systemaufruf realpath() bereit, mit dem Pfadnamen aufgelöst werden, sodass in ihnen keine symbolischen Links vorhanden sind. Wenn Sie das auf die Eingangsnamen anwenden und dann Dennis-Code verwenden, würde dies jedes Mal die richtige Antwort geben. Es ist einfach, den C-Code zu schreiben das wraps realpath() - ich habe es geschafft - aber ich kenne kein Standardprogramm, das dies tut.)


Ich finde, dass Perl einfacher zu benutzen ist als Shell, obwohl bash ordentliche Unterstützung für Arrays bietet und dies wahrscheinlich auch tun könnte - Übung für den Leser. Wenn Sie zwei kompatible Namen haben, teilen Sie diese in Komponenten auf:

  • Setzen Sie den relativen Pfad auf leer.
  • Wenn die Komponenten gleich sind, fahren Sie mit den nächsten fort.
  • Wenn entsprechende Komponenten unterschiedlich sind oder für einen Pfad keine weiteren Komponenten vorhanden sind:
  • Wenn keine Quellkomponenten vorhanden sind und der relative Pfad leer ist, fügen Sie "." zum Anfang.
  • Stellen Sie für jede verbleibende Quellkomponente "../" den relativen Pfad voran.
  • Wenn keine Zielkomponenten 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.

Somit:

#!/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 gut (es berücksichtigt nicht die Komplexität der Windows-Pfadnamen) angesichts merkwürdiger Eingaben. Es verwendet das Modul Cwd und 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 er dieselbe 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 untenstehende Perl-Skript gibt einfach die Antwort aus und nicht die Eingaben und die Antwort wie im obigen Skript.) Nun: Soll ich die nicht funktionierende Antwort beseitigen? Könnte sein...

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

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
}
2
Ray Donnelly

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

}
2
Anonymous

Eine weitere Lösung, pure bash + GNU readlink, zur einfachen Verwendung in folgendem Kontext:

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

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

Dies funktioniert in fast allen aktuellen Linux. Wenn readlink -m nicht an Ihrer Seite funktioniert, versuchen Sie stattdessen readlink -f. Siehe auch https://Gist.github.com/hilbix/1ec361d00a8178ae8ea0 für mögliche Aktualisierungen:

: 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 ln -s: .__ verwendet werden.
    • relpath / / gibt . und nicht die leere Zeichenfolge an
    • relpath a a gibt a, auch wenn a zufällig ein Verzeichnis ist
  • Die meisten Fälle wurden getestet, um auch vernünftige Ergebnisse zu erzielen.
  • Diese Lösung verwendet einen String-Präfix-Abgleich. Daher ist readlink erforderlich, um Pfade zu kanonisieren.
  • Dank readlink -m funktioniert es auch für noch nicht vorhandene Pfade.

Auf alten Systemen, auf denen readlink -m nicht verfügbar ist, schlägt readlink -f fehl, wenn die Datei nicht vorhanden ist. Daher benötigen Sie 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, falls $1. oder .. für nicht vorhandene Pfade enthält (wie in /doesnotexist/./a), aber es sollte die meisten Fälle abdecken.

(Ersetzen Sie readlink -m -- durch readlink_missing.)

Bearbeiten wegen des Downvotes folgt

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" "[email protected]"
}

#     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 denken, dass die Frage nicht passt, hier ist der Beweis, dass dies richtig ist:

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

Ohne Zweifel ist ../bar der genaue und einzige korrekte relative Pfad der Seite bar, die von der Seite moo aus gesehen wird. Alles andere wäre eindeutig falsch.

Es ist trivial, die Ausgabe auf die Frage anzuwenden, die anscheinend davon ausgeht, dass current ein Verzeichnis ist:

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 (ein nachfolgender / überlebt dank einiger bash- magic):

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

Und hier sind die Kontrollen nur 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" "[email protected]"
}

#     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 man das gewünschte Ergebnis aus der Frage ziehen:

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

Wenn Sie etwas finden, das nicht funktioniert, teilen Sie mir dies bitte in den Kommentaren mit. Vielen Dank.

PS:

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

Wenn Sie sich ändern

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

zu

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

dann können Sie den 2. Parameter weglassen, so dass BASE das aktuelle Verzeichnis/URL/was auch immer ist. Das ist wie immer nur das Unix-Prinzip.

Wenn Sie das nicht mögen, gehen Sie bitte zurück zu Windows. Vielen Dank.

1
Tino

Schätze mal, das soll auch der Trick sein ...

OK, ein bisschen Overhead erwartet, aber wir machen hier eine 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
D4rk1B0t

Dieses Skript funktioniert nur für die Pfadnamen. Es muss keine der Dateien vorhanden sein. Wenn die übergebenen Pfade nicht absolut sind, ist das Verhalten etwas ungewöhnlich, sollte jedoch wie erwartet funktionieren, wenn beide Pfade relativ sind.

Ich habe es nur unter OS X getestet, daher ist es möglicherweise nicht portierbar.

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

Diese Antwort geht nicht auf den Bash-Teil der Frage ein, sondern weil ich versucht habe, die Antworten in dieser Frage zu verwenden, um diese Funktionalität in Emacs zu implementieren.

Emacs hat dafür eine 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"
0
fakedrake