Convierta ruta absoluta en ruta relativa dado un directorio actual usando Bash

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 -c "import os.path; print os.path.relpath('/foo/bar', '/foo/baz/foo')" 

da:

 ../../bar 

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 

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

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.

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

El objective 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, tabs 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 '/ ab/å/⮀*/!' '/ ab/å/⮀/xäå/?' relpath_check '/' '/A' relpath_check '/A' '/' relpath_check '/ & / !/*/\\/E' '/' relpath_check '/' '/ & / !/*/\\/E' relpath_check '/ & / !/*/\\/E' '/ & / !/?/\\/E/F' relpath_check '/X/Y' '/ & / !/C/\\/E/F' relpath_check '/ & / !/C' '/A' relpath_check '/A / !/C' '/A /B' relpath_check '/Â/ !/C' '/Â/ !/C' relpath_check '/ & /B / C' '/ & /B / C/D' relpath_check '/ & / !/C' '/ & / !/C/\\/Ê' relpath_check '/Å/ !/C' '/Å/ !/D' relpath_check '/.A /*B/C' '/.A /*B/\\/E' relpath_check '/ & / !/C' '/ & /D' relpath_check '/ & / !/C' '/ & /\\/E' relpath_check '/ & / !/C' '/\\/E/F' relpath_check /home/part1/part2 /home/part1/part3 relpath_check /home/part1/part2 /home/part4/part5 relpath_check /home/part1/part2 /work/part6/part7 relpath_check /home/part1 /work/part1/part2/part3/part4 relpath_check /home /work/part2/part3 relpath_check / /work/part2/part3/part4 relpath_check /home/part1/part2 /home/part1/part2/part3/part4 relpath_check /home/part1/part2 /home/part1/part2/part3 relpath_check /home/part1/part2 /home/part1/part2 relpath_check /home/part1/part2 /home/part1 relpath_check /home/part1/part2 /home relpath_check /home/part1/part2 / relpath_check /home/part1/part2 /work relpath_check /home/part1/part2 /work/part1 relpath_check /home/part1/part2 /work/part1/part2 relpath_check /home/part1/part2 /work/part1/part2/part3 relpath_check /home/part1/part2 /work/part1/part2/part3/part4 relpath_check home/part1/part2 home/part1/part3 relpath_check home/part1/part2 home/part4/part5 relpath_check home/part1/part2 work/part6/part7 relpath_check home/part1 work/part1/part2/part3/part4 relpath_check home work/part2/part3 relpath_check . work/part2/part3 relpath_check home/part1/part2 home/part1/part2/part3/part4 relpath_check home/part1/part2 home/part1/part2/part3 relpath_check home/part1/part2 home/part1/part2 relpath_check home/part1/part2 home/part1 relpath_check home/part1/part2 home relpath_check home/part1/part2 . relpath_check home/part1/part2 work relpath_check home/part1/part2 work/part1 relpath_check home/part1/part2 work/part1/part2 relpath_check home/part1/part2 work/part1/part2/part3 relpath_check home/part1/part2 work/part1/part2/part3/part4 print "Done with checks." } if [[ $# -gt 0 ]] && [[ $1 = "-v" ]]; then VERBOSE=true shift fi if [[ $# -eq 0 ]]; then run_checks else VERBOSE=true relpath_check "$@" fi 
 #!/bin/sh # Return relative path from canonical absolute dir path $1 to canonical # absolute dir path $2 ($1 and/or $2 may end with one or no "/"). # Does only need POSIX shell builtins (no external command) relPath () { local common path up common=${1%/} path=${2%/}/ while test "${path#"$common"/}" = "$path"; do common=${common%/*} up=../$up done path=$up${path#"$common"/}; path=${path%/}; printf %s "${path:-.}" } # Return relative path from dir $1 to dir $2 (Does not impose any # restrictions on $1 and $2 but requires GNU Core Utility "readlink" # HINT: busybox's "readlink" does not support option '-m', only '-f' # which requires that all but the last path component must exist) relpath () { relPath "$(readlink -m "$1")" "$(readlink -m "$2")"; } 

El script de shell anterior se inspiró en pini (¡Gracias!). Activa un error en el módulo de resaltado de syntax de Stack Overflow (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 boost 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, tabs, ‘, “,?, *, [,], 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

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

       ln -s "$(relpath "" "")" "" 
    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 -rp; do eval set -- $p case $1 in '#'*|'') continue;; esac # Skip comments and empty lines # q stores quoting character, use " if ' is used in path name q="'"; case $1$2 in *"'"*) q='"';; esac rPOk=passed rP=$(relPath "$1" "$2"); test "$rP" = "$3" || rPOk=$rP rpOk=passed rp=$(relpath "$1" "$2"); test "$rp" = "$3" || rpOk=$rp RPOk=passed RP=$(python -c "import os.path; print os.path.relpath($q$2$q, $q$1$q)") test "$RP" = "$3" || RPOk=$RP printf \ "$format" "$q$1$q" "$q$2$q" "$q$3$q" "$q$rPOk$q" "$q$rpOk$q" "$q$RPOk$q" done <<-"EOF" # From directory To directory Expected relative path '/' '/' '.' '/usr' '/' '..' '/usr/' '/' '..' '/' '/usr' 'usr' '/' '/usr/' 'usr' '/usr' '/usr' '.' '/usr/' '/usr' '.' '/usr' '/usr/' '.' '/usr/' '/usr/' '.' '/u' '/usr' '../usr' '/usr' '/u' '../u' "/u'/dir" "/u'/dir" "." "/u'" "/u'/dir" "dir" "/u'/dir" "/u'" ".." "/" "/u'/dir" "u'/dir" "/u'/dir" "/" "../.." "/u'" "/u'" "." "/" "/u'" "u'" "/u'" "/" ".." '/u"/dir' '/u"/dir' '.' '/u"' '/u"/dir' 'dir' '/u"/dir' '/u"' '..' '/' '/u"/dir' 'u"/dir' '/u"/dir' '/' '../..' '/u"' '/u"' '.' '/' '/u"' 'u"' '/u"' '/' '..' '/u /dir' '/u /dir' '.' '/u ' '/u /dir' 'dir' '/u /dir' '/u ' '..' '/' '/u /dir' 'u /dir' '/u /dir' '/' '../..' '/u ' '/u ' '.' '/' '/u ' 'u ' '/u ' '/' '..' '/u\n/dir' '/u\n/dir' '.' '/u\n' '/u\n/dir' 'dir' '/u\n/dir' '/u\n' '..' '/' '/u\n/dir' 'u\n/dir' '/u\n/dir' '/' '../..' '/u\n' '/u\n' '.' '/' '/u\n' 'u\n' '/u\n' '/' '..' '/ ab/å/⮀*/!' '/ ab/å/⮀/xäå/?' '../../⮀/xäå/?' '/' '/A' 'A' '/A' '/' '..' '/ & / !/*/\\/E' '/' '../../../../..' '/' '/ & / !/*/\\/E' ' & / !/*/\\/E' '/ & / !/*/\\/E' '/ & / !/?/\\/E/F' '../../../?/\\/E/F' '/X/Y' '/ & / !/C/\\/E/F' '../../ & / !/C/\\/E/F' '/ & / !/C' '/A' '../../../A' '/A / !/C' '/A /B' '../../B' '/Â/ !/C' '/Â/ !/C' '.' '/ & /B / C' '/ & /B / C/D' 'D' '/ & / !/C' '/ & / !/C/\\/Ê' '\\/Ê' '/Å/ !/C' '/Å/ !/D' '../D' '/.A /*B/C' '/.A /*B/\\/E' '../\\/E' '/ & / !/C' '/ & /D' '../../D' '/ & / !/C' '/ & /\\/E' '../../\\/E' '/ & / !/C' '/\\/E/F' '../../../\\/E/F' '/home/p1/p2' '/home/p1/p3' '../p3' '/home/p1/p2' '/home/p4/p5' '../../p4/p5' '/home/p1/p2' '/work/p6/p7' '../../../work/p6/p7' '/home/p1' '/work/p1/p2/p3/p4' '../../work/p1/p2/p3/p4' '/home' '/work/p2/p3' '../work/p2/p3' '/' '/work/p2/p3/p4' 'work/p2/p3/p4' '/home/p1/p2' '/home/p1/p2/p3/p4' 'p3/p4' '/home/p1/p2' '/home/p1/p2/p3' 'p3' '/home/p1/p2' '/home/p1/p2' '.' '/home/p1/p2' '/home/p1' '..' '/home/p1/p2' '/home' '../..' '/home/p1/p2' '/' '../../..' '/home/p1/p2' '/work' '../../../work' '/home/p1/p2' '/work/p1' '../../../work/p1' '/home/p1/p2' '/work/p1/p2' '../../../work/p1/p2' '/home/p1/p2' '/work/p1/p2/p3' '../../../work/p1/p2/p3' '/home/p1/p2' '/work/p1/p2/p3/p4' '../../../work/p1/p2/p3/p4' '/-' '/-' '.' '/?' '/?' '.' '/??' '/??' '.' '/???' '/???' '.' '/?*' '/?*' '.' '/*' '/*' '.' '/*' '/**' '../**' '/*' '/***' '../***' '/*.*' '/*.**' '../*.**' '/*.???' '/*.??' '../*.??' '/[]' '/[]' '.' '/[az]*' '/[0-9]*' '../[0-9]*' EOF format="\t%-19s %-22s %-27s %-8s %-8s\n" printf "\n\n*** Testing own and python's function with arbitrary dirs\n\n" printf "$format\n" \ "From Directory" "To Directory" "Rel. Path" "relpath" "python" IFS= while read -rp; do eval set -- $p case $1 in '#'*|'') continue;; esac # Skip comments and empty lines # q stores quoting character, use " if ' is used in path name q="'"; case $1$2 in *"'"*) q='"';; esac rpOk=passed rp=$(relpath "$1" "$2"); test "$rp" = "$3" || rpOk=$rp RPOk=passed RP=$(python -c "import os.path; print os.path.relpath($q$2$q, $q$1$q)") test "$RP" = "$3" || RPOk=$RP printf "$format" "$q$1$q" "$q$2$q" "$q$3$q" "$q$rpOk$q" "$q$RPOk$q" done <<-"EOF" # From directory To directory Expected relative path 'usr/p1/..//./p4' 'p3/../p1/p6/.././/p2' '../../p1/p2' './home/../../work' '..//././../dir///' '../../dir' 'home/p1/p2' 'home/p1/p3' '../p3' 'home/p1/p2' 'home/p4/p5' '../../p4/p5' 'home/p1/p2' 'work/p6/p7' '../../../work/p6/p7' 'home/p1' 'work/p1/p2/p3/p4' '../../work/p1/p2/p3/p4' 'home' 'work/p2/p3' '../work/p2/p3' '.' 'work/p2/p3' 'work/p2/p3' 'home/p1/p2' 'home/p1/p2/p3/p4' 'p3/p4' 'home/p1/p2' 'home/p1/p2/p3' 'p3' 'home/p1/p2' 'home/p1/p2' '.' 'home/p1/p2' 'home/p1' '..' 'home/p1/p2' 'home' '../..' 'home/p1/p2' '.' '../../..' 'home/p1/p2' 'work' '../../../work' 'home/p1/p2' 'work/p1' '../../../work/p1' 'home/p1/p2' 'work/p1/p2' '../../../work/p1/p2' 'home/p1/p2' 'work/p1/p2/p3' '../../../work/p1/p2/p3' 'home/p1/p2' 'work/p1/p2/p3/p4' '../../../work/p1/p2/p3/p4' EOF 

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

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" 

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'")') 

Una ligera mejora en las respuestas de kasku y Pini , 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 

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.

Lamentablemente, la respuesta de Mark Rushakoff (ahora borrada – hace referencia al código de aquí ) 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 es del todo correcto.

  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.

El código de Dennis es mejor porque arregla 1 y 5, pero tiene los mismos problemas 2, 3, 4. Use el código de Dennis (y vuélvala a votar antes) debido a eso.

(NB: POSIX proporciona una llamada al sistema realpath() que resuelve los nombres de las rutas para que no queden enlaces simbólicos en ellos. Aplicar eso a los nombres de entrada, y luego usar el código de Dennis daría la respuesta correcta cada vez. Es trivial escribir el El código C que envuelve a realpath() : lo he hecho, pero no sé de una utilidad estándar que lo haga.


Para esto, creo que Perl es más fácil de usar que el intérprete de comandos, aunque bash tiene un soporte decente para las matrices y probablemente también pueda hacer esto: ejercítelo para el lector. Entonces, dados dos nombres compatibles, divídalos en componentes:

  • Establezca la ruta relativa en vacío.
  • Si bien los componentes son iguales, salte a la siguiente.
  • Cuando los componentes correspondientes son diferentes o no hay más componentes para una ruta:
  • Si no hay componentes fuente restantes y la ruta relativa está vacía, agregue “.” al comienzo
  • Para cada componente fuente restante, prefija la ruta relativa con “../”.
  • Si no hay componentes de destino restantes y la ruta relativa está vacía, agregue “.” al comienzo
  • Para cada componente de destino restante, agregue el componente al final de la ruta después de una barra diagonal.

Así:

 #!/bin/perl -w use strict; # Should fettle the arguments if one is absolute and one relative: # Oops - missing functionality! # Split! my(@source) = split '/', $ARGV[0]; my(@target) = split '/', $ARGV[1]; my $count = scalar(@source); $count = scalar(@target) if (scalar(@target) < $count); my $relpath = ""; my $i; for ($i = 0; $i < $count; $i++) { last if $source[$i] ne $target[$i]; } $relpath = "." if ($i >= scalar(@source) && $relpath eq ""); for (my $s = $i; $s < scalar(@source); $s++) { $relpath = "../$relpath"; } $relpath = "." if ($i >= scalar(@target) && $relpath eq ""); for (my $t = $i; $t < scalar(@target); $t++) { $relpath .= "/$target[$t]"; } # Clean up result (remove double slash, trailing slash, trailing slash-dot). $relpath =~ s%//%/%; $relpath =~ s%/$%%; $relpath =~ s%/\.$%%; print "source = $ARGV[0]\n"; print "target = $ARGV[1]\n"; print "relpath = $relpath\n"; 

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

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

Output from the test script:

 source = /home/part1/part2 target = /home/part1/part3 relpath = ../part3 source = /home/part1/part2 target = /home/part4/part5 relpath = ../../part4/part5 source = /home/part1/part2 target = /work/part6/part7 relpath = ../../../work/part6/part7 source = /home/part1 target = /work/part1/part2/part3/part4 relpath = ../../work/part1/part2/part3/part4 source = /home target = /work/part2/part3 relpath = ../work/part2/part3 source = / target = /work/part2/part3/part4 relpath = ./work/part2/part3/part4 source = /home/part1/part2 target = /home/part1/part2/part3/part4 relpath = ./part3/part4 source = /home/part1/part2 target = /home/part1/part2/part3 relpath = ./part3 source = /home/part1/part2 target = /home/part1/part2 relpath = . source = /home/part1/part2 target = /home/part1 relpath = .. source = /home/part1/part2 target = /home relpath = ../.. source = /home/part1/part2 target = / relpath = ../../../.. source = /home/part1/part2 target = /work relpath = ../../../work source = /home/part1/part2 target = /work/part1 relpath = ../../../work/part1 source = /home/part1/part2 target = /work/part1/part2 relpath = ../../../work/part1/part2 source = /home/part1/part2 target = /work/part1/part2/part3 relpath = ../../../work/part1/part2/part3 source = /home/part1/part2 target = /work/part1/part2/part3/part4 relpath = ../../../work/part1/part2/part3/part4 source = home/part1/part2 target = home/part1/part3 relpath = ../part3 source = home/part1/part2 target = home/part4/part5 relpath = ../../part4/part5 source = home/part1/part2 target = work/part6/part7 relpath = ../../../work/part6/part7 source = home/part1 target = work/part1/part2/part3/part4 relpath = ../../work/part1/part2/part3/part4 source = home target = work/part2/part3 relpath = ../work/part2/part3 source = . target = work/part2/part3 relpath = ../work/part2/part3 source = home/part1/part2 target = home/part1/part2/part3/part4 relpath = ./part3/part4 source = home/part1/part2 target = home/part1/part2/part3 relpath = ./part3 source = home/part1/part2 target = home/part1/part2 relpath = . source = home/part1/part2 target = home/part1 relpath = .. source = home/part1/part2 target = home relpath = ../.. source = home/part1/part2 target = . relpath = ../../.. source = home/part1/part2 target = work relpath = ../../../work source = home/part1/part2 target = work/part1 relpath = ../../../work/part1 source = home/part1/part2 target = work/part1/part2 relpath = ../../../work/part1/part2 source = home/part1/part2 target = work/part1/part2/part3 relpath = ../../../work/part1/part2/part3 source = home/part1/part2 target = work/part1/part2/part3/part4 relpath = ../../../work/part1/part2/part3/part4 

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

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

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

 #!/bin/perl -w # Based loosely on code from: http://unix.derkeiler.com/Newsgroups/comp.unix.shell/2005-10/1256.html # Via: http://stackoverflow.com/questions/2564634 use strict; die "Usage: $0 from to\n" if scalar @ARGV != 2; use Cwd qw(realpath getcwd); my $pwd; my $verbose = 0; # Fettle filename so it is absolute. # Deals with '//', '/./' and '/../' notations, plus symlinks. # The realpath() function does the hard work if the path exists. # For non-existent paths, the code does a purely textual hack. sub resolve { my($name) = @_; my($path) = realpath($name); if (!defined $path) { # Path does not exist - do the best we can with lexical analysis # Assume Unix - not dealing with Windows. $path = $name; if ($name !~ m%^/%) { $pwd = getcwd if !defined $pwd; $path = "$pwd/$path"; } $path =~ s%//+%/%g; # Not UNC paths. $path =~ s%/$%%; # No trailing / $path =~ s%/\./%/%g; # No embedded /./ # Try to eliminate /../abc/ $path =~ s%/\.\./(?:[^/]+)(/|$)%$1%g; $path =~ s%/\.$%%; # No trailing /. $path =~ s%^\./%%; # No leading ./ # What happens with . and / as inputs? } return($path); } sub print_result { my($source, $target, $relpath) = @_; if ($verbose) { print "source = $ARGV[0]\n"; print "target = $ARGV[1]\n"; print "relpath = $relpath\n"; } else { print "$relpath\n"; } exit 0; } my($source) = resolve($ARGV[0]); my($target) = resolve($ARGV[1]); print_result($source, $target, ".") if ($source eq $target); # Split! my(@source) = split '/', $source; my(@target) = split '/', $target; my $count = scalar(@source); $count = scalar(@target) if (scalar(@target) < $count); my $relpath = ""; my $i; # Both paths are absolute; Perl splits an empty field 0. for ($i = 1; $i < $count; $i++) { last if $source[$i] ne $target[$i]; } for (my $s = $i; $s < scalar(@source); $s++) { $relpath = "$relpath/" if ($s > $i); $relpath = "$relpath.."; } for (my $t = $i; $t < scalar(@target); $t++) { $relpath = "$relpath/" if ($relpath ne ""); $relpath = "$relpath$target[$t]"; } print_result($source, $target, $relpath); 

test.sh:

 #!/bin/bash cd /home/ubuntu touch blah TEST=/home/ubuntu/.//blah echo TEST=$TEST TMP=$(readlink -e "$TEST") echo TMP=$TMP REL=${TMP#$(pwd)/} echo REL=$REL 

Pruebas:

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

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

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

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

 #!/bin/sh # Find common parent directory path for a pair of paths. # Call with two pathnames as args, eg # commondirpart foo/bar foo/baz/bat -> result="foo/" # The result is either empty or ends with "/". commondirpart () { result="" while test ${#1} -gt 0 -a ${#2} -gt 0; do if test "${1%${1#?}}" != "${2%${2#?}}"; then # First characters the same? break # No, we're done comparing. fi result="$result${1%${1#?}}" # Yes, append to result. set -- "${1#?}" "${2#?}" # Chop first char off both strings. done case "$result" in (""|*/) ;; (*) result="${result%/*}/";; esac } # Turn foo/bar/baz into ../../.. # dir2dotdot () { OLDIFS="$IFS" IFS="/" result="" for dir in $1; do result="$result../" done result="${result%/}" IFS="$OLDIFS" } # Call with FROM TO args. relativepath () { case "$1" in (*//*|*/./*|*/../*|*?/|*/.|*/..) printf '%s\n' "'$1' not canonical"; exit 1;; (/*) from="${1#?}";; (*) printf '%s\n' "'$1' not absolute"; exit 1;; esac case "$2" in (*//*|*/./*|*/../*|*?/|*/.|*/..) printf '%s\n' "'$2' not canonical"; exit 1;; (/*) to="${2#?}";; (*) printf '%s\n' "'$2' not absolute"; exit 1;; esac case "$to" in ("$from") # Identical directories. result=".";; ("$from"/*) # From /x to /x/foo/bar -> foo/bar result="${to##$from/}";; ("") # From /foo/bar to / -> ../.. dir2dotdot "$from";; (*) case "$from" in ("$to"/*) # From /x/foo/bar to /x -> ../.. dir2dotdot "${from##$to/}";; (*) # Everything else. commondirpart "$from" "$to" common="$result" dir2dotdot "${from#$common}" result="$result/${to#$common}" esac ;; esac } set -f # noglob set -x cat < 

Mi solución:

 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 } 

Aquí está mi versión. 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 } 

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

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

 #!/bin/sh # # Finding the relative path to a certain file ($2), given the absolute path ($1) # (available here too http://pastebin.com/tWWqA8aB) # relpath () { local FROM="$1" local TO="`dirname $2`" local FILE="`basename $2`" local DEBUG="$3" local FROMREL="" local FROMUP="$FROM" while [ "$FROMUP" != "/" ]; do local TOUP="$TO" local TOREL="" while [ "$TOUP" != "/" ]; do [ -z "$DEBUG" ] || echo 1>&2 "$DEBUG$FROMUP =?= $TOUP" if [ "$FROMUP" = "$TOUP" ]; then echo "${FROMREL:-.}/$TOREL${TOREL:+/}$FILE" return 0 fi TOREL="`basename $TOUP`${TOREL:+/}$TOREL" TOUP="`dirname $TOUP`" done FROMREL="..${FROMREL:+/}$FROMREL" FROMUP="`dirname $FROMUP`" done echo "${FROMREL:-.}${TOREL:+/}$TOREL/$FILE" return 0 } relpathshow () { echo " - target $2" echo " from $1" echo " ------" echo " => `relpath $1 $2 ' '`" echo "" } # If given 2 arguments, do as said... if [ -n "$2" ]; then relpath $1 $2 # If only one given, then assume current directory elif [ -n "$1" ]; then relpath `pwd` $1 # Otherwise perform a set of built-in tests to confirm the validity of the method! ;) else relpathshow /usr/share/emacs22/site-lisp/emacs-goodies-el \ /usr/share/emacs22/site-lisp/emacs-goodies-el/filladapt.el relpathshow /usr/share/emacs23/site-lisp/emacs-goodies-el \ /usr/share/emacs22/site-lisp/emacs-goodies-el/filladapt.el relpathshow /usr/bin \ /usr/share/emacs22/site-lisp/emacs-goodies-el/filladapt.el relpathshow /usr/bin \ /usr/share/emacs22/site-lisp/emacs-goodies-el/filladapt.el relpathshow /usr/bin/share/emacs22/site-lisp/emacs-goodies-el \ /etc/motd relpathshow / \ /initrd.img fi 

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

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

 #!/bin/bash set -e declare SCRIPT_NAME="$(basename $0)" function usage { echo "Usage: $SCRIPT_NAME  " echo " Outputs  relative to " exit 1 } if [ $# -lt 2 ]; then usage; fi declare base=$1 declare target=$2 declare -a base_part=() declare -a target_part=() #Split path elements & canonicalize OFS="$IFS"; IFS='/' bpl=0; for bp in $base; do case "$bp" in ".");; "..") let "bpl=$bpl-1" ;; *) base_part[${bpl}]="$bp" ; let "bpl=$bpl+1";; esac done tpl=0; for tp in $target; do case "$tp" in ".");; "..") let "tpl=$tpl-1" ;; *) target_part[${tpl}]="$tp" ; let "tpl=$tpl+1";; esac done IFS="$OFS" #Count common prefix common=0 for (( i=0 ; i<$bpl ; i++ )); do if [ "${base_part[$i]}" = "${target_part[$common]}" ] ; then let "common=$common+1" else break fi done #Compute number of directories up let "updir=$bpl-$common" || updir=0 #if the expression is zero, 'let' fails #trivial case (after canonical decomposition) if [ $updir -eq 0 ]; then echo . exit fi #Print updirs for (( i=0 ; i<$updir ; i++ )); do echo -n ../ done #Print remaining path for (( i=$common ; i<$tpl ; i++ )); do if [ $i -ne $common ]; then echo -n "/" fi if [ "" != "${target_part[$i]}" ] ; then echo -n "${target_part[$i]}" fi done #One last newline echo 

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

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

 ELISP> (file-relative-name "/a/b/c" "/a/b/c") "." ELISP> (file-relative-name "/a/b/c" "/a/b") "c" ELISP> (file-relative-name "/a/b/c" "/c/b") "../../a/b/c" 

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

 #! /bin/env bash #bash script to find the relative path between two directories mydir=${0%/} mydir=${0%/*} creadlink="$mydir/creadlink" shopt -s extglob relpath_ () { path1=$("$creadlink" "$1") path2=$("$creadlink" "$2") orig1=$path1 path1=${path1%/}/ path2=${path2%/}/ while :; do if test ! "$path1"; then break fi part1=${path2#$path1} if test "${part1#/}" = "$part1"; then path1=${path1%/*} continue fi if test "${path2#$path1}" = "$path2"; then path1=${path1%/*} continue fi break done part1=$path1 path1=${orig1#$part1} depth=${path1//+([^\/])/..} path1=${path2#$path1} path1=${depth}${path2#$part1} path1=${path1##+(\/)} path1=${path1%/} if test ! "$path1"; then path1=. fi printf "$path1" } relpath_test () { res=$(relpath_ /path1/to/dir1 /path1/to/dir2 ) expected='../dir2' test_results "$res" "$expected" res=$(relpath_ / /path1/to/dir2 ) expected='path1/to/dir2' test_results "$res" "$expected" res=$(relpath_ /path1/to/dir2 / ) expected='../../..' test_results "$res" "$expected" res=$(relpath_ / / ) expected='.' test_results "$res" "$expected" res=$(relpath_ /path/to/dir2/dir3 /path/to/dir1/dir4/dir4a ) expected='../../dir1/dir4/dir4a' test_results "$res" "$expected" res=$(relpath_ /path/to/dir1/dir4/dir4a /path/to/dir2/dir3 ) expected='../../../dir2/dir3' test_results "$res" "$expected" #res=$(relpath_ . /path/to/dir2/dir3 ) #expected='../../../dir2/dir3' #test_results "$res" "$expected" } test_results () { if test ! "$1" = "$2"; then printf 'failed!\nresult:\nX%sX\nexpected:\nX%sX\n\n' "$@" fi } #relpath_test 

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

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

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

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

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

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

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

 : relpath AB # Calculate relative path from A to B, returns true on success # Example: ln -s "$(relpath "$A" "$B")" "$B" relpath() { local XYA # We can create dangling softlinks X="$(readlink -m -- "$1")" || return Y="$(readlink -m -- "$2")" || return X="${X%/}/" A="" while Y="${Y%/*}" [ ".${X#"$Y"/}" = ".$X" ] do A="../$A" done X="$A${X#"$Y"/}" X="${X%/}" echo "${X:-.}" } 

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

Edit because of the downvote follows

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

 check() { res="$(relpath "$2" "$1")" [ ".$res" = ".$3" ] && return printf ':WRONG: %-10q %-10q gives %q\nCORRECT %-10q %-10q gives %q\n' "$1" "$2" "$res" "$@" } # TARGET SOURCE RESULT check "/A/B/C" "/A" ".." check "/A/B/C" "/Ax" "../../Ax" check "/A/B/C" "/A/B" "." check "/A/B/C" "/A/B/C" "C" check "/A/B/C" "/A/B/C/D" "C/D" check "/A/B/C" "/A/B/C/D/E" "C/D/E" check "/A/B/C" "/A/B/D" "D" check "/A/B/C" "/A/B/D/E" "D/E" check "/A/B/C" "/A/D" "../D" check "/A/B/C" "/A/D/E" "../D/E" check "/A/B/C" "/D/E/F" "../../D/E/F" check "/foo/baz/moo" "/foo/bar" "../bar" 

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

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

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

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

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

This returns exactly, what was asked for.

And before you raise an eyebrow, here is a bit more complex variant of relpath (spot the small difference), which should work for URL-Syntax, too (so a trailing / survives, thanks to some bash -magic):

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

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

 check() { res="$(relpathurl "$2" "$1")" [ ".$res" = ".$3" ] && return printf ':WRONG: %-10q %-10q gives %q\nCORRECT %-10q %-10q gives %q\n' "$1" "$2" "$res" "$@" } # TARGET SOURCE RESULT check "/A/B/C" "/A" ".." check "/A/B/C" "/Ax" "../../Ax" check "/A/B/C" "/A/B" "." check "/A/B/C" "/A/B/C" "C" check "/A/B/C" "/A/B/C/D" "C/D" check "/A/B/C" "/A/B/C/D/E" "C/D/E" check "/A/B/C" "/A/B/D" "D" check "/A/B/C" "/A/B/D/E" "D/E" check "/A/B/C" "/A/D" "../D" check "/A/B/C" "/A/D/E" "../D/E" check "/A/B/C" "/D/E/F" "../../D/E/F" check "/foo/baz/moo" "/foo/bar" "../bar" check "http://example.com/foo/baz/moo" "http://example.com/foo/bar" "../bar" check "http://example.com/foo/baz/moo/" "http://example.com/foo/bar" "../../bar" check "http://example.com/foo/baz/moo" "http://example.com/foo/bar/" "../bar/" check "http://example.com/foo/baz/moo/" "http://example.com/foo/bar/" "../../bar/" 

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

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

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

PD:

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

If you change

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

a

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

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

If you dislike that, please go back to Windows. Gracias.