Konvertiere den absoluten Pfad in einen relativen Pfad unter Verwendung eines aktuellen Verzeichnisses unter Verwendung von Bash

Beispiel:

absolute="/foo/bar" current="/foo/baz/foo" # Magic relative="../../bar" 

Wie erstelle ich den magischen (hoffentlich nicht zu komplizierten Code …)?

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

gibt:

 ../../bar 

Die Verwendung von Realpath von GNU Coreutils 8.23 ​​ist die einfachste, denke ich:

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

Beispielsweise:

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

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, wenn der String Null-Länge (= leer) und ‘-n’ Test, wenn der String 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" 
 #!/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/} 

Es ist seit 2001 in Perl integriert und funktioniert auf fast allen Systemen, die Sie sich vorstellen können, sogar VMS .

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

Außerdem ist die Lösung 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.

Pythons os.path.relpath als Shell-function

Das Ziel dieser relpath Übung ist es, die os.path.relpath function von Python 2.7 os.path.relpath (verfügbar ab Python Version 2.6, funktioniert aber nur in 2.7), wie von xni vorgeschlagen. Daher können einige der Ergebnisse von den functionen anderer Antworten abweichen.

(Ich habe nicht mit Newlines in Pfaden getestet, nur weil es die validation aufgrund des Aufrufs von python -c von ZSH bricht. Es wäre sicherlich mit einiger Mühe möglich.)

Bezüglich “Magie” in Bash habe ich vor langer Zeit aufgegeben, in Bash nach Magie zu suchen, aber seitdem habe ich all die Magie gefunden, die ich brauche, und dann einige in ZSH.

Folglich schlage ich zwei Implementierungen vor.

Die erste Implementierung soll vollständig POSIX-konform sein . Ich habe es mit Debian 6.0.6 “Squeeze” mit /bin/dash getestet. Es funktioniert auch perfekt mit /bin/sh unter OS X 10.8.3, was eigentlich Bash Version 3.2 ist, die sich als POSIX-Shell ausgibt.

Die zweite Implementierung ist eine ZSH-Shell-function, die robust gegen mehrere Schrägstriche und andere Störungen in Pfaden ist. Wenn Sie ZSH verfügbar haben, ist dies die empfohlene Version, auch wenn Sie sie in der unten angegebenen Form (zB mit einem Shebang von #!/usr/bin/env zsh ) von einer anderen Shell aufrufen.

Schließlich habe ich ein ZSH-Skript geschrieben, das die Ausgabe des relpath Befehls in $PATH überprüft, vorausgesetzt, die Testfälle sind in anderen Antworten enthalten. Ich habe diesen Tests etwas Würze hinzugefügt, indem ich einige Leerzeichen, Tabulatoren und Satzzeichen wie zB ! ? * ! ? * ! ? * Hier und da und warf noch einen weiteren Test mit exotischen UTF-8-Zeichen in vim-Powerline gefunden .

POSIX- Shell-function

Zuerst die POSIX-konforme Shell-function. Es funktioniert mit einer Vielzahl von Pfaden, bereinigt jedoch nicht mehrere Schrägstriche oder triggers 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-function

Jetzt, die robustere zsh Version. Wenn Sie möchten, dass die Argumente zu echten Pfaden à la realpath -f (verfügbar im Linux- coreutils Paket) aufgetriggers werden, ersetzen Sie das :a in den Zeilen 3 und 4 durch :A

Um dies in zsh zu verwenden, entfernen Sie die erste und letzte Zeile und $FPATH sie in ein Verzeichnis ein, 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 "$@" 

Testskript

Zum Schluss das Testskript. Es akzeptiert eine Option, nämlich -v , um eine ausführliche Ausgabe zu ermöglichen.

 #!/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 '/ ab/å/⮀*/!' '/ ab/å/⮀/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 
 #!/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 inspiriert (Danke!). Es triggers einen Fehler im Syntax-Highlighting-Modul von Stack Overflow aus (zumindest in meinem Vorschau-Frame). Also bitte ignorieren, wenn die Hervorhebung falsch ist.

Einige Notizen:

  • Fehler behoben und Code verbessert, ohne die Codelänge und -komplexität signifikant zu erhöhen
  • Fügen Sie functionalität in functionen für die Benutzerfreundlichkeit
  • Keept-functionen POSIX-kompatibel, so dass sie mit allen POSIX-Shells funktionieren sollen (getestet mit Dash, Bash und Zsh in Ubuntu Linux 12.04)
  • Verwendete lokale Variablen nur, um überschneidende globale Variablen zu vermeiden und den globalen Namensraum zu verschmutzen
  • Beide Verzeichnispfade müssen NICHT existieren (Voraussetzung für meine Anwendung)
  • Pfadnamen können Leerzeichen, Sonderzeichen, Steuerzeichen, umgekehrte Schrägstriche, Tabulatoren, ‘,’,?, *, [,] Usw. enthalten.
  • Die Core-function “relPath” verwendet nur POSIX-Shell-Builtins, benötigt aber kanonische absolute Verzeichnispfade als Parameter
  • Die erweiterte function “relpath” kann beliebige Verzeichnispfade behandeln (auch relativ, nicht-kanonisch), benötigt aber das externe GNU-coreprogramm “readlink”
  • Vermiedenes eingebautes “Echo” und stattdessen verwendetes “printf” aus zwei Gründen:
    • Aufgrund widersprüchlicher historischer Implementierungen von eingebautem “echo” verhält es sich in verschiedenen Shells unterschiedlich -> POSIX empfiehlt, dass printf dem Echo vorgezogen wird .
    • Ein eingebautes “Echo” von einigen POSIX-Shells interpretiert einige Backslash-Sequenzen und beschädigt somit Pfadnamen, die solche Sequenzen enthalten
  • Um unnötige Konvertierungen zu vermeiden, werden Pfadnamen verwendet, da sie von Shell- und OS-Dienstprogrammen zurückgegeben und erwartet werden (zB cd, ln, ls, find, mkdir; im Gegensatz zu “os.path.relpath” von Python, das einige Backslash-Sequenzen interpretiert)
  • Bis auf die erwähnten Backslash-Sequenzen gibt die letzte Zeile der function “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 Verzeichnispfade angehängt werden, die von relPath erhalten wurden, zB:

       ln -s "$(relpath "" "")" "" 
    2. Symbolische Links im selben Verzeichnis, die mit dieser Methode erstellt wurden, haben nicht das hässliche "./" vor dem Dateinamen.

  • Wenn Sie einen Fehler finden, kontaktieren Sie bitte linuxball (at) gmail.com und ich werde versuchen, es zu beheben.
  • Regressionstest-Suite hinzugefügt (auch POSIX-Shell-kompatibel)

Code-Auflistung 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 -rp; 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' '/' '..' '/ ab/å/⮀*/!' '/ ab/å/⮀/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' '/-' '/-' '.' '/?' '/?' '.' '/??' '/??' '.' '/???' '/???' '.' '/?*' '/?*' '.' '/*' '/*' '.' '/*' '/**' '../**' '/*' '/***' '../***' '/*.*' '/*.**' '../*.**' '/*.???' '/*.??' '../*.??' '/[]' '/[]' '.' '/[az]*' '/[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 -rp; 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 

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 golfed

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

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

Ich würde nur 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'")') 

Eine leichte Verbesserung gegenüber kaskus und Pinis Antworten, die mit Räumen schöner spielen und relative Pfade erlauben:

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

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

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

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 sein soll:

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

Der einzige Nachteil ist, dass Python benötigt, aber:

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

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

Leider scheint die Antwort von Mark Ruschakoff (jetzt gelöscht – sie referenzierte den Code von hier ) nicht richtig zu funktionieren, wenn sie angepasst wurde 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 korrekt funktioniert. Ich nehme an, dass das Skript ein Quellargument (wo Sie sind) und ein Zielargument (wo Sie hinkommen wollen) nimmt, und dass entweder beide absolute Pfadnamen sind oder beide relativ sind. Wenn eins absolut und das andere relativ ist, ist es am einfachsten, den relativen Namen dem aktuellen Arbeitsverzeichnis voranzuzählen – aber der folgende Code macht das nicht.


In acht nehmen

Der folgende Code ist in der Nähe, funktioniert aber nicht ganz richtig.

  1. Da ist das Problem in den Kommentaren von Dennis Williamson angesprochen.
  2. Es gibt auch ein Problem, dass diese rein textuelle Verarbeitung von Pfadnamen und Sie durch merkwürdige Symlinks ernsthaft durcheinander gebracht werden können.
  3. Der Code behandelt nicht streunende ‘Punkte’ in Pfaden wie ‘ xyz/./pqr ‘.
  4. Der Code behandelt keine “Doppelpunkte” in Pfaden wie ” xyz/../pqr “.
  5. Trivial: Der Code entfernt führende ‘ ./ ‘ nicht aus Pfaden.

Dennis Code ist besser, weil er 1 und 5 behebt – aber hat die gleichen Probleme 2, 3, 4. Verwenden Sie Dennis Code (und up-stimme es davor) aus diesem Grund.

(NB: POSIX stellt einen Systemaufruf realpath() , der Pfadnamen auftriggers, so dass keine symbolischen Links in ihnen übrig bleiben. Das Anwenden auf die Eingangsnamen und die Verwendung von Dennis ‘Code würden jedes Mal die richtige Antwort liefern. Es ist trivial die C-Code, der realpath() – Ich habe es getan – aber ich kenne kein Standard-Dienstprogramm, das dies tut.)


Dafür finde ich Perl einfacher zu benutzen als Shell, obwohl bash ordentliche Unterstützung für Arrays bietet und dies wahrscheinlich auch tun könnte – Übung für den Leser. Also, geben Sie zwei kompatible Namen, teilen Sie sie jeweils in Komponenten:

  • Legen Sie den relativen Pfad leer fest.
  • Während die Komponenten identisch sind, fahren Sie mit dem nächsten fort.
  • Wenn sich entsprechende Komponenten unterscheiden oder für einen Pfad keine Komponenten mehr vorhanden sind:
  • Wenn keine verbleibenden Quellkomponenten vorhanden sind und der relative Pfad leer ist, fügen Sie “.” zum Start.
  • Weisen Sie für jede verbleibende Quellenkomponente den relativen Pfad mit “../” voran.
  • Wenn keine verbleibenden Zielkomponenten vorhanden sind und der relative Pfad leer ist, fügen Sie “.” zum Start.
  • 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"; 

Test script (the square brackets contain a blank and a tab):

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

Output from the test script:

 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 

This Perl script works fairly thoroughly on Unix (it does not take into account all the complexities of Windows path names) in the face of weird inputs. It uses the module Cwd and its function realpath to resolve the real path of names that exist, and does a textual analysis for paths that don't exist. In all cases except one, it produces the same output as Dennis's script. The deviant case is:

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

The two results are equivalent - just not identical. (The output is from a mildly modified version of the test script - the Perl script below simply prints the answer, rather than the inputs and the answer as in the script above.) Now: should I eliminate the non-working answer? Maybe...

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

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 

I took your question as a challenge to write this in “portable” shell code, ie

  • with a POSIX shell in mind
  • no bashisms such as arrays
  • avoid calling externals like the plague. There’s not a single fork in the script! That makes it blazingly fast, especially on systems with significant fork overhead, like cygwin.
  • Must deal with glob characters in pathnames (*, ?, [, ])

It runs on any POSIX conformant shell (zsh, bash, ksh, ash, busybox, …). It even contains a testsuite to verify its operation. Canonicalization of pathnames is left as an exercise. 🙂

 #!/bin/sh # Find common parent directory path for a pair of paths. # Call with two pathnames as args, eg # 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 <  

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 } 

Hier ist meine Version. It’s based on the answer by @Offirmo . I made it Dash-compatible and fixed the following testcase failure:

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

See the 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 } 

Guess this one shall do the trick too… (comes with built-in tests) 🙂

OK, some overhead expected, but we’re doing Bourne shell here! 😉

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

This script works only on the path names. It does not require any of the files to exist. If the paths passed are not absolute, the behavior is a bit unusual, but it should work as expected if both paths are relative.

I only tested it on OS X, so it might not be portable.

 #!/bin/bash set -e declare SCRIPT_NAME="$(basename $0)" function usage { echo "Usage: $SCRIPT_NAME  " echo " Outputs  relative to " 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 

This answer does not address the Bash part of the question, but because I tried to use the answers in this question to implement this functionality in Emacs I’ll throw it out there.

Emacs actually has a function for this out of the box:

 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" 

Here’s a shell script that does it without calling other programs:

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

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

I needed something like this but which resolved symbolic links too. I discovered that pwd has a -P flag for that purpose. A fragment of my script is appended. It’s within a function in a shell script, hence the $1 and $2. The result value, which is the relative path from START_ABS to END_ABS, is in the UPDIRS variable. The script cd’s into each parameter directory in order to execute the pwd -P and this also means that relative path parameters are handled. Cheers, Jim

 SAVE_DIR="$PWD" cd "$1" START_ABS=`pwd -P` cd "$SAVE_DIR" cd "$2" END_ABS=`pwd -P` START_WORK="$START_ABS" UPDIRS="" while test -n "${START_WORK}" -a "${END_ABS/#${START_WORK}}" '==' "$END_ABS"; do START_WORK=`dirname "$START_WORK"`"/" UPDIRS=${UPDIRS}"../" done UPDIRS="$UPDIRS${END_ABS/#${START_WORK}}" cd "$SAVE_DIR" 

Yet another solution, pure bash + GNU readlink for easy use in following context:

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

Edit: Make sure that “$B” is either not existing or no softlink in that case, else relpath follows this link which is not what you want!

This works in nearly all current Linux. If readlink -m does not work at your side, try readlink -f instead. See also https://gist.github.com/hilbix/1ec361d00a8178ae8ea0 for possible updates:

 : relpath AB # Calculate relative path from A to B, returns true on success # Example: ln -s "$(relpath "$A" "$B")" "$B" relpath() { local XYA # 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:

  • Care was taken that it is safe against unwanted shell meta character expansion, in case filenames contain * or ? .
  • The output is meant to be usable as the first argument to ln -s :
    • relpath / / gives . and not the empty string
    • relpath aa gives a , even if a happens to be a directory
  • Most common cases were tested to give reasonable results, too.
  • This solution uses string prefix matching, hence readlink is required to canonicalize paths.
  • Thanks to readlink -m it works for not yet existing paths, too.

On old systems, where readlink -m is not available, readlink -f fails if the file does not exist. So you probably need some workaround like this (untested!):

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

This is not really quite correct in case $1 includes . or .. for nonexisting paths (like in /doesnotexist/./a ), but it should cover most cases.

(Replace readlink -m -- above by readlink_missing .)

Edit because of the downvote follows

Here is a test, that this function, indeed, is correct:

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

Puzzled? Well, these are the correct results ! Even if you think it does not fit the question, here is the proof this is correct:

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

Without any doubt, ../bar is the exact and only correct relative path of the page bar seen from the page moo . Everything else would be plain wrong.

It is trivial to adopt the output to the question which apparently assumes, that current is a directory:

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

This returns exactly, what was asked for.

And before you raise an eyebrow, here is a bit more complex variant of relpath (spot the small difference), which should work for URL-Syntax, too (so a trailing / survives, thanks to some 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 XYA # 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:-.}" } 

And here are the checks just to make clear: It really works as told.

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

And here is how this can be used to give the wanted result from the question:

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

If you find something which does not work, please let me know in the comments below. Vielen Dank.

PS:

Why are the arguments of relpath “reversed” in contrast to all the other answers here?

If you change

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

zu

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

then you can leave the 2nd parameter away, such that the BASE is the current directory/URL/whatever. That’s only the Unix principle, as usual.

If you dislike that, please go back to Windows. Vielen Dank.