unidad una siguientes rutas ruta relativas relativa regresar raiz las ejercicios directorio cuál comandos cambiar anterior absolutas bash shell path relative-path absolute-path

bash - una - rutas absolutas y relativas linux



Convierta ruta absoluta en ruta relativa dado un directorio actual usando Bash (23)

Ejemplo:

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

¿Cómo creo la magia (es de esperar que el código no sea demasiado complicado ...)?


Python''s os.path.relpath como una función de shell

El objetivo de este ejercicio relpath es imitar la función os.path.relpath Python 2.7 (disponible desde Python versión 2.6 pero solo funciona correctamente en 2.7), como lo propuso xni . Como consecuencia, algunos de los resultados pueden diferir de las funciones provistas en otras respuestas.

(No he probado con líneas nuevas en rutas simplemente porque rompe la validación basada en llamar python -c desde ZSH. Sin duda sería posible con un poco de esfuerzo).

En cuanto a la "magia" en Bash, hace mucho que dejé de buscar magia en Bash, pero desde entonces he encontrado toda la magia que necesito, y algo más, en ZSH.

En consecuencia, propongo dos implementaciones.

La primera implementación pretende ser totalmente compatible con POSIX . Lo he probado con /bin/dash en Debian 6.0.6 "Squeeze". También funciona perfectamente con /bin/sh en OS X 10.8.3, que en realidad es la versión 3.2 de Bash que pretende ser un shell POSIX.

La segunda implementación es una función de shell ZSH que es robusta contra múltiples barras y otras molestias en las rutas. Si tiene ZSH disponible, esta es la versión recomendada, incluso si la está llamando en el formulario de script que se presenta a continuación (es decir, con un shebang de #!/usr/bin/env zsh ) desde otro shell.

Finalmente, he escrito un script ZSH que verifica el resultado del comando relpath encontrado en $PATH dados los casos de prueba provistos en otras respuestas. ¡Agregué un poco de sabor a esas pruebas al agregar espacios, pestañas y signos de puntuación como ! ? * ! ? * ! ? * aquí y allá y también arrojó una prueba más con exóticos caracteres UTF-8 que se encuentran en vim-powerline .

Función de shell POSIX

Primero, la función de shell que cumple con POSIX. Funciona con una variedad de rutas, pero no limpia barras múltiples ni resuelve enlaces simbólicos.

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

Función de shell ZSH

Ahora, la versión más robusta de zsh . Si desea resolver los argumentos a rutas reales à la realpath -f (disponible en el paquete Linux coreutils ), reemplace :a en las líneas 3 y 4 con :A

Para usar esto en zsh, elimine la primera y la última línea y $FPATH en un directorio que esté en su variable $FPATH .

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

Script de prueba

Finalmente, el script de prueba. Acepta una opción, a saber, -v para habilitar la salida detallada.

#!/usr/bin/env zsh set -eu VERBOSE=false script_name=$(basename $0) usage () { print "/n Usage: $script_name SRC_PATH DESTINATION_PATH/n" >&2 exit ${1:=1} } vrb () { $VERBOSE && print -P ${(%)@} || return 0; } relpath_check () { [[ $# -ge 1 ]] && [[ $# -le 2 ]] || return 1 target=${${2:-$1}} prefix=${${${2:+$1}:-$PWD}} result=$(relpath $prefix $target) # Compare with python''s os.path.relpath function py_result=$(python -c "import os.path; print os.path.relpath(''$target'', ''$prefix'')") col=''%F{green}'' if [[ $result != $py_result ]] && col=''%F{red}'' || $VERBOSE; then print -P "${col}Source: ''$prefix''/nDestination: ''$target''%f" print -P "${col}relpath: ${(qq)result}%f" print -P "${col}python: ${(qq)py_result}%f/n" fi } run_checks () { print "Running checks..." relpath_check ''/ a b/å/⮀*/!'' ''/ a b/å/⮀/xäå/?'' relpath_check ''/'' ''/A'' relpath_check ''/A'' ''/'' relpath_check ''/ & / !/*////E'' ''/'' relpath_check ''/'' ''/ & / !/*////E'' relpath_check ''/ & / !/*////E'' ''/ & / !/?////E/F'' relpath_check ''/X/Y'' ''/ & / !/C////E/F'' relpath_check ''/ & / !/C'' ''/A'' relpath_check ''/A / !/C'' ''/A /B'' relpath_check ''/Â/ !/C'' ''/Â/ !/C'' relpath_check ''/ & /B / C'' ''/ & /B / C/D'' relpath_check ''/ & / !/C'' ''/ & / !/C////Ê'' relpath_check ''/Å/ !/C'' ''/Å/ !/D'' relpath_check ''/.A /*B/C'' ''/.A /*B////E'' relpath_check ''/ & / !/C'' ''/ & /D'' relpath_check ''/ & / !/C'' ''/ & ////E'' relpath_check ''/ & / !/C'' ''////E/F'' relpath_check /home/part1/part2 /home/part1/part3 relpath_check /home/part1/part2 /home/part4/part5 relpath_check /home/part1/part2 /work/part6/part7 relpath_check /home/part1 /work/part1/part2/part3/part4 relpath_check /home /work/part2/part3 relpath_check / /work/part2/part3/part4 relpath_check /home/part1/part2 /home/part1/part2/part3/part4 relpath_check /home/part1/part2 /home/part1/part2/part3 relpath_check /home/part1/part2 /home/part1/part2 relpath_check /home/part1/part2 /home/part1 relpath_check /home/part1/part2 /home relpath_check /home/part1/part2 / relpath_check /home/part1/part2 /work relpath_check /home/part1/part2 /work/part1 relpath_check /home/part1/part2 /work/part1/part2 relpath_check /home/part1/part2 /work/part1/part2/part3 relpath_check /home/part1/part2 /work/part1/part2/part3/part4 relpath_check home/part1/part2 home/part1/part3 relpath_check home/part1/part2 home/part4/part5 relpath_check home/part1/part2 work/part6/part7 relpath_check home/part1 work/part1/part2/part3/part4 relpath_check home work/part2/part3 relpath_check . work/part2/part3 relpath_check home/part1/part2 home/part1/part2/part3/part4 relpath_check home/part1/part2 home/part1/part2/part3 relpath_check home/part1/part2 home/part1/part2 relpath_check home/part1/part2 home/part1 relpath_check home/part1/part2 home relpath_check home/part1/part2 . relpath_check home/part1/part2 work relpath_check home/part1/part2 work/part1 relpath_check home/part1/part2 work/part1/part2 relpath_check home/part1/part2 work/part1/part2/part3 relpath_check home/part1/part2 work/part1/part2/part3/part4 print "Done with checks." } if [[ $# -gt 0 ]] && [[ $1 = "-v" ]]; then VERBOSE=true shift fi if [[ $# -eq 0 ]]; then run_checks else VERBOSE=true relpath_check "$@" fi


Está construido en Perl desde 2001, por lo que funciona en casi todos los sistemas que puedas imaginar, incluso VMS .

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

Además, la solución es fácil de entender.

Entonces para tu ejemplo:

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

... funcionaría bien.


Esta es una mejora corregida y completamente funcional de la mejor solución actualmente clasificada de @pini (que lamentablemente maneja solo unos pocos casos)

Recordatorio: ''-z'' prueba si la cadena es de longitud cero (= vacío) y ''-n'' prueba si la cadena no está vacía.

# 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

Casos de prueba :

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"


Este script proporciona resultados correctos solo para las entradas que son rutas absolutas o rutas relativas sin ellas . o .. :

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


Lamentablemente, la respuesta de Mark Rushakoff (ahora borrada - hace referencia al código de here ) no parece funcionar correctamente cuando se adapta a:

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

El pensamiento descrito en el comentario se puede refinar para que funcione correctamente en la mayoría de los casos. Estoy a punto de suponer que la secuencia de comandos toma un argumento de origen (donde se encuentra) y un argumento de destino (al que desea llegar), y que ambos son nombres de ruta absolutos o ambos son relativos. Si uno es absoluto y el otro relativo, lo más fácil es prefijar el nombre relativo con el directorio de trabajo actual, pero el código siguiente no lo hace.

Tener cuidado

El siguiente código está cerca de funcionar correctamente, pero no está del todo bien.

  1. Existe el problema abordado en los comentarios de Dennis Williamson.
  2. También existe el problema de que este procesamiento puramente textual de nombres de rutas y usted puede ser seriamente dañado por enlaces simbólicos extraños.
  3. El código no maneja los "puntos" extraviados en rutas como " xyz/./pqr ".
  4. El código no maneja los "puntos dobles" extraviados en rutas como " xyz/../pqr ".
  5. Trivialmente: el código no elimina el '' ./ '' principal de las rutas.

Dennis''s code is better because it fixes 1 and 5 - but has the same issues 2, 3, 4. Use Dennis''s code (and up-vote it ahead of this) because of that.

(NB: POSIX provides a system call realpath() that resolves pathnames so that there are no symlinks left in them. Applying that to the input names, and then using Dennis''s code would give the correct answer each time. It is trivial to write the C code that wraps realpath() - I''ve done it - but I don''t know of a standard utility that does so.)

For this, I find Perl easier to use than shell, though bash has decent support for arrays and could probably do this too - exercise for the reader. So, given two compatible names, split them each into components:

  • Set the relative path to empty.
  • While the components are the same, skip to the next.
  • When corresponding components are different or there are no more components for one path:
  • If there are no remaining source components and the relative path is empty, add "." to the start.
  • For each remaining source component, prefix the relative path with "../".
  • If there are no remaining target components and the relative path is empty, add "." to the start.
  • For each remaining target component, add the component to the end of the path after a slash.

Thus:

#!/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'' <<! | /home/part1/part2 /home/part1/part3 /home/part1/part2 /home/part4/part5 /home/part1/part2 /work/part6/part7 /home/part1 /work/part1/part2/part3/part4 /home /work/part2/part3 / /work/part2/part3/part4 /home/part1/part2 /home/part1/part2/part3/part4 /home/part1/part2 /home/part1/part2/part3 /home/part1/part2 /home/part1/part2 /home/part1/part2 /home/part1 /home/part1/part2 /home /home/part1/part2 / /home/part1/part2 /work /home/part1/part2 /work/part1 /home/part1/part2 /work/part1/part2 /home/part1/part2 /work/part1/part2/part3 /home/part1/part2 /work/part1/part2/part3/part4 home/part1/part2 home/part1/part3 home/part1/part2 home/part4/part5 home/part1/part2 work/part6/part7 home/part1 work/part1/part2/part3/part4 home work/part2/part3 . work/part2/part3 home/part1/part2 home/part1/part2/part3/part4 home/part1/part2 home/part1/part2/part3 home/part1/part2 home/part1/part2 home/part1/part2 home/part1 home/part1/part2 home home/part1/part2 . home/part1/part2 work home/part1/part2 work/part1 home/part1/part2 work/part1/part2 home/part1/part2 work/part1/part2/part3 home/part1/part2 work/part1/part2/part3/part4 ! while read source target do perl relpath.pl $source $target echo done

Output 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://.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);


No muchas de las respuestas aquí son prácticas para el uso diario. Dado que es muy difícil hacer esto correctamente en pure bash, sugiero la siguiente solución confiable (similar a una sugerencia enterrada en un comentario):

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

Luego, puede obtener la ruta relativa basada en el directorio actual:

echo $(relpath somepath)

o puede especificar que la ruta sea relativa a un directorio determinado:

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

La única desventaja es que esto requiere Python, pero:

  • Funciona de forma idéntica en cualquier python> = 2.6
  • No requiere que los archivos o directorios existan.
  • Los nombres de archivo pueden contener una gama más amplia de caracteres especiales. Por ejemplo, muchas otras soluciones no funcionan si los nombres de archivo contienen espacios u otros caracteres especiales.
  • Es una función de una línea que no satura las secuencias de comandos.

Tenga en cuenta que las soluciones que incluyen basename o dirname pueden no ser necesariamente mejores, ya que requieren la instalación de coreutils . Si alguien tiene una solución de bash pura que es confiable y simple (en lugar de una curiosidad intrincada), me sorprendería.


Solo usaría Perl para esta tarea no tan trivial:

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


Suponiendo que tiene instalado: bash, pwd, dirname, echo; entonces relpath es

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

He jugado golf la respuesta de pini y algunas otras ideas


Una ligera mejora en las kasku''s y Pini''s , que juega mejor con los espacios y permite pasar caminos relativos:

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


Usando realpath de GNU coreutils 8.23 ​​es el más simple, creo:

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

Por ejemplo:

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


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

Pruebas:

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


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


Here is my 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/"

Ahora:

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 }


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"


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, 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 :


My Solution:

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 }


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"


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


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

Notas:

  • 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 .)


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

El script de shell anterior se inspiró en share (¡Gracias!). Activa un error en el módulo de resaltado de sintaxis de (al menos en mi marco de vista previa). Por lo tanto, ignore si resaltar es incorrecto.

Algunas notas:

  • Se eliminaron los errores y el código mejorado sin aumentar significativamente la longitud y complejidad del código
  • Ponga funcionalidad en funciones para facilidad de uso
  • Las funciones mantenidas POSIX son compatibles para que (deberían) funcionar con todas las shells POSIX (probadas con dash, bash y zsh en Ubuntu Linux 12.04)
  • Se utilizaron variables locales solo para evitar las variables globales y la contaminación del espacio de nombre global
  • Ambas rutas de directorio NO DEBEN existir (requisito para mi aplicación)
  • Los nombres de rutas pueden contener espacios, caracteres especiales, caracteres de control, barras invertidas, pestañas, '', ",?, *, [,], Etc.
  • La función principal "relPath" usa solo matrices de shell POSIX pero requiere rutas de directorio absolutas canónicas como parámetros
  • La función extendida "relpath" puede manejar rutas de acceso de directorio arbitrarias (también relativas, no canónicas) pero requiere la utilidad de núcleo GNU externa "readlink"
  • Se evitó el "echo" incorporado y se usó el "printf" integrado por dos razones:
  • Para evitar conversiones innecesarias, los nombres de ruta se utilizan a medida que son devueltos y esperados por las utilidades de shell y OS (por ejemplo, cd, ln, ls, find, mkdir; a diferencia de "os.path.relpath" de python que interpretará algunas secuencias de barra invertida)
  • A excepción de las mencionadas secuencias de barra invertida, la última línea de función "relPath" genera nombres de ruta compatibles con python:

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

    La última línea puede ser reemplazada (y simplificada) por línea

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

    Prefiero esto último porque

    1. Los nombres de archivo se pueden adjuntar directamente a rutas de directorios obtenidas por relPath, por ejemplo:

      ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"

    2. Los enlaces simbólicos en el mismo directorio creado con este método no tienen el "./" feo antes del nombre del archivo.

  • Si encuentra un error, póngase en contacto con linuxball (at) gmail.com e intentaré solucionarlo.
  • Conjunto de prueba de regresión agregado (también compatible con shell POSIX)

Listado de código para pruebas de regresión (simplemente añádalo al script de shell):

############################################################################ # If called with 2 arguments assume they are dir paths and print rel. path # ############################################################################ test "$#" = 2 && { printf ''%s/n'' "Rel. path from ''$1'' to ''$2'' is ''$(relpath "$1" "$2")''." exit 0 } ####################################################### # If NOT called with 2 arguments run regression tests # ####################################################### format="/t%-19s %-22s %-27s %-8s %-8s %-8s/n" printf / "/n/n*** Testing own and python''s function with canonical absolute dirs/n/n" printf "$format/n" / "From Directory" "To Directory" "Rel. Path" "relPath" "relpath" "python" IFS= while read -r p; do eval set -- $p case $1 in ''#''*|'''') continue;; esac # Skip comments and empty lines # q stores quoting character, use " if '' is used in path name q="''"; case $1$2 in *"''"*) q=''"'';; esac rPOk=passed rP=$(relPath "$1" "$2"); test "$rP" = "$3" || rPOk=$rP rpOk=passed rp=$(relpath "$1" "$2"); test "$rp" = "$3" || rpOk=$rp RPOk=passed RP=$(python -c "import os.path; print os.path.relpath($q$2$q, $q$1$q)") test "$RP" = "$3" || RPOk=$RP printf / "$format" "$q$1$q" "$q$2$q" "$q$3$q" "$q$rPOk$q" "$q$rpOk$q" "$q$RPOk$q" done <<-"EOF" # From directory To directory Expected relative path ''/'' ''/'' ''.'' ''/usr'' ''/'' ''..'' ''/usr/'' ''/'' ''..'' ''/'' ''/usr'' ''usr'' ''/'' ''/usr/'' ''usr'' ''/usr'' ''/usr'' ''.'' ''/usr/'' ''/usr'' ''.'' ''/usr'' ''/usr/'' ''.'' ''/usr/'' ''/usr/'' ''.'' ''/u'' ''/usr'' ''../usr'' ''/usr'' ''/u'' ''../u'' "/u''/dir" "/u''/dir" "." "/u''" "/u''/dir" "dir" "/u''/dir" "/u''" ".." "/" "/u''/dir" "u''/dir" "/u''/dir" "/" "../.." "/u''" "/u''" "." "/" "/u''" "u''" "/u''" "/" ".." ''/u"/dir'' ''/u"/dir'' ''.'' ''/u"'' ''/u"/dir'' ''dir'' ''/u"/dir'' ''/u"'' ''..'' ''/'' ''/u"/dir'' ''u"/dir'' ''/u"/dir'' ''/'' ''../..'' ''/u"'' ''/u"'' ''.'' ''/'' ''/u"'' ''u"'' ''/u"'' ''/'' ''..'' ''/u /dir'' ''/u /dir'' ''.'' ''/u '' ''/u /dir'' ''dir'' ''/u /dir'' ''/u '' ''..'' ''/'' ''/u /dir'' ''u /dir'' ''/u /dir'' ''/'' ''../..'' ''/u '' ''/u '' ''.'' ''/'' ''/u '' ''u '' ''/u '' ''/'' ''..'' ''/u/n/dir'' ''/u/n/dir'' ''.'' ''/u/n'' ''/u/n/dir'' ''dir'' ''/u/n/dir'' ''/u/n'' ''..'' ''/'' ''/u/n/dir'' ''u/n/dir'' ''/u/n/dir'' ''/'' ''../..'' ''/u/n'' ''/u/n'' ''.'' ''/'' ''/u/n'' ''u/n'' ''/u/n'' ''/'' ''..'' ''/ a b/å/⮀*/!'' ''/ a b/å/⮀/xäå/?'' ''../../⮀/xäå/?'' ''/'' ''/A'' ''A'' ''/A'' ''/'' ''..'' ''/ & / !/*////E'' ''/'' ''../../../../..'' ''/'' ''/ & / !/*////E'' '' & / !/*////E'' ''/ & / !/*////E'' ''/ & / !/?////E/F'' ''../../../?////E/F'' ''/X/Y'' ''/ & / !/C////E/F'' ''../../ & / !/C////E/F'' ''/ & / !/C'' ''/A'' ''../../../A'' ''/A / !/C'' ''/A /B'' ''../../B'' ''/Â/ !/C'' ''/Â/ !/C'' ''.'' ''/ & /B / C'' ''/ & /B / C/D'' ''D'' ''/ & / !/C'' ''/ & / !/C////Ê'' ''///Ê'' ''/Å/ !/C'' ''/Å/ !/D'' ''../D'' ''/.A /*B/C'' ''/.A /*B////E'' ''..////E'' ''/ & / !/C'' ''/ & /D'' ''../../D'' ''/ & / !/C'' ''/ & ////E'' ''../..////E'' ''/ & / !/C'' ''////E/F'' ''../../..////E/F'' ''/home/p1/p2'' ''/home/p1/p3'' ''../p3'' ''/home/p1/p2'' ''/home/p4/p5'' ''../../p4/p5'' ''/home/p1/p2'' ''/work/p6/p7'' ''../../../work/p6/p7'' ''/home/p1'' ''/work/p1/p2/p3/p4'' ''../../work/p1/p2/p3/p4'' ''/home'' ''/work/p2/p3'' ''../work/p2/p3'' ''/'' ''/work/p2/p3/p4'' ''work/p2/p3/p4'' ''/home/p1/p2'' ''/home/p1/p2/p3/p4'' ''p3/p4'' ''/home/p1/p2'' ''/home/p1/p2/p3'' ''p3'' ''/home/p1/p2'' ''/home/p1/p2'' ''.'' ''/home/p1/p2'' ''/home/p1'' ''..'' ''/home/p1/p2'' ''/home'' ''../..'' ''/home/p1/p2'' ''/'' ''../../..'' ''/home/p1/p2'' ''/work'' ''../../../work'' ''/home/p1/p2'' ''/work/p1'' ''../../../work/p1'' ''/home/p1/p2'' ''/work/p1/p2'' ''../../../work/p1/p2'' ''/home/p1/p2'' ''/work/p1/p2/p3'' ''../../../work/p1/p2/p3'' ''/home/p1/p2'' ''/work/p1/p2/p3/p4'' ''../../../work/p1/p2/p3/p4'' ''/-'' ''/-'' ''.'' ''/?'' ''/?'' ''.'' ''/??'' ''/??'' ''.'' ''/???'' ''/???'' ''.'' ''/?*'' ''/?*'' ''.'' ''/*'' ''/*'' ''.'' ''/*'' ''/**'' ''../**'' ''/*'' ''/***'' ''../***'' ''/*.*'' ''/*.**'' ''../*.**'' ''/*.???'' ''/*.??'' ''../*.??'' ''/[]'' ''/[]'' ''.'' ''/[a-z]*'' ''/[0-9]*'' ''../[0-9]*'' EOF format="/t%-19s %-22s %-27s %-8s %-8s/n" printf "/n/n*** Testing own and python''s function with arbitrary dirs/n/n" printf "$format/n" / "From Directory" "To Directory" "Rel. Path" "relpath" "python" IFS= while read -r p; do eval set -- $p case $1 in ''#''*|'''') continue;; esac # Skip comments and empty lines # q stores quoting character, use " if '' is used in path name q="''"; case $1$2 in *"''"*) q=''"'';; esac rpOk=passed rp=$(relpath "$1" "$2"); test "$rp" = "$3" || rpOk=$rp RPOk=passed RP=$(python -c "import os.path; print os.path.relpath($q$2$q, $q$1$q)") test "$RP" = "$3" || RPOk=$RP printf "$format" "$q$1$q" "$q$2$q" "$q$3$q" "$q$rpOk$q" "$q$RPOk$q" done <<-"EOF" # From directory To directory Expected relative path ''usr/p1/..//./p4'' ''p3/../p1/p6/.././/p2'' ''../../p1/p2'' ''./home/../../work'' ''..//././../dir///'' ''../../dir'' ''home/p1/p2'' ''home/p1/p3'' ''../p3'' ''home/p1/p2'' ''home/p4/p5'' ''../../p4/p5'' ''home/p1/p2'' ''work/p6/p7'' ''../../../work/p6/p7'' ''home/p1'' ''work/p1/p2/p3/p4'' ''../../work/p1/p2/p3/p4'' ''home'' ''work/p2/p3'' ''../work/p2/p3'' ''.'' ''work/p2/p3'' ''work/p2/p3'' ''home/p1/p2'' ''home/p1/p2/p3/p4'' ''p3/p4'' ''home/p1/p2'' ''home/p1/p2/p3'' ''p3'' ''home/p1/p2'' ''home/p1/p2'' ''.'' ''home/p1/p2'' ''home/p1'' ''..'' ''home/p1/p2'' ''home'' ''../..'' ''home/p1/p2'' ''.'' ''../../..'' ''home/p1/p2'' ''work'' ''../../../work'' ''home/p1/p2'' ''work/p1'' ''../../../work/p1'' ''home/p1/p2'' ''work/p1/p2'' ''../../../work/p1/p2'' ''home/p1/p2'' ''work/p1/p2/p3'' ''../../../work/p1/p2/p3'' ''home/p1/p2'' ''work/p1/p2/p3/p4'' ''../../../work/p1/p2/p3/p4'' EOF


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


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

da:

../../bar