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.
- Existe el problema abordado en los comentarios de Dennis Williamson.
- 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.
- El código no maneja los "puntos" extraviados en rutas como "
xyz/./pqr
". - El código no maneja los "puntos dobles" extraviados en rutas como "
xyz/../pqr
". - 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
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
givesa
, even ifa
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:
- Debido a las implementaciones históricas conflictivas del "eco" incorporado, se comporta de manera diferente en diferentes shells -> POSIX recomienda que se prefiera printf sobre echo .
- El "eco" integrado de algunas shells POSIX interpretará algunas secuencias de barra invertida y, por lo tanto, corromperá las rutas que contengan dichas secuencias
- 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
Los nombres de archivo se pueden adjuntar directamente a rutas de directorios obtenidas por relPath, por ejemplo:
ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
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