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


261

Ejemplo:

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

# Magic

relative="../../bar"

¿Cómo creo la magia (espero que no sea un código demasiado complicado ...)?


77
Por ejemplo (mi caso en este momento) para dar la ruta relativa de gcc para que pueda generar información relativa a la depuración utilizable incluso si la ruta de origen cambia.
Offirmo

Se hizo una pregunta similar a esta en U&L: unix.stackexchange.com/questions/100918/… . Una de las respuestas (@Gilles) menciona una herramienta, enlaces simbólicos , que puede facilitar el trabajo de este problema.
slm

25
Simple: realpath --relative-to=$absolute $current.
kenorb

Respuestas:


229

Usar 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

77
Es una pena que el paquete esté desactualizado en Ubuntu 14.04 y no tenga la opción --relative-to.
kzh

3
Funciona bien en Ubuntu 16.04
cayhorstmann

77
$ realpath --relative-to="${PWD}" "$file"es útil si desea las rutas relativas al directorio de trabajo actual.
dcoles

1
Esto es correcto para las cosas dentro de /usr/bin/nmap/-path pero no para /usr/bin/nmap: de nmapa /tmp/testinges solo ../../y no 3 veces ../. Sin embargo, funciona porque hacerlo ..en rootfs es /.
Patrick B.

66
Como @PatrickB. implícito, --relative-to=…espera un directorio y NO lo verifica. Eso significa que terminará con un "../" adicional si solicita una ruta relativa a un archivo (como parece hacer este ejemplo, porque /usr/binrara vez o nunca contiene directorios y nmapnormalmente es un binario)
IBBoard

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

da:

../../bar

11
Funciona y hace que las alternativas parezcan ridículas. Eso es una ventaja para mí xD
hasvn

31
+1. Ok, hiciste trampa ... ¡pero esto es demasiado bueno para no ser usado! relpath(){ python -c "import os.path; print os.path.relpath('$1','${2:-$PWD}')" ; }
MestreLion

44
Lamentablemente, esto no está disponible universalmente: os.path.relpath es nuevo en Python 2.6.
Chen Levy el

15
@ChenLevy: Python 2.6 fue lanzado en 2008. Es difícil de creer que no estaba universalmente disponible en 2012.
MestreLion

11
python -c 'import os, sys; print(os.path.relpath(*sys.argv[1:]))'funciona de forma más natural y confiable.
musiphil

31

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

Recordatorio: '-z' prueba si la cadena es de longitud cero (= vacía) 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"

1
Integrado en offirmo shell lib github.com/Offirmo/offirmo-shell-lib , función «OSL_FILE_find_relative_path» (archivo «osl_lib_file.sh»)
Offirmo

1
+1. Puede hacerse fácilmente para manejar cualquier ruta (no solo rutas absolutas que comienzan con /) reemplazando source=$1; target=$2consource=$(realpath $1); target=$(realpath $2)
Josh Kelley

2
@Josh de hecho, siempre que los directorios realmente existan ... lo que no era conveniente para las pruebas unitarias;) Pero en uso real sí, realpathse recomienda, source=$(readlink -f $1)etc., si realpath no está disponible (no es estándar)
Offirmo

Definí $sourcey me $targetgusta esto: `if [[-e $ 1]]; entonces fuente = $ (readlink -f $ 1); otra fuente = $ 1; fi si [[-e $ 2]]; entonces target = $ (readlink -f $ 2); más objetivo = $ 2; fi` De esa manera, la función podría manejar rutas relativas reales / existentes así como directorios ficticios.
Nathan S. Watson-Haigh

1
@ NathanS.Watson-Haigh Aún mejor, descubrí que recientemente readlinktiene una -mopción que simplemente hace eso;)
Offirmo

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

Guión maravilloso: corto y limpio. Apliqué una edición (en espera de la revisión por pares): common_part = $ source / common_part = $ (dirname $ common_part) / echo $ {back} $ {target # $ common_part} El script existente podría fallar debido a una coincidencia inapropiada al inicio del nombre del directorio al comparar, por ejemplo: "/ foo / bar / baz" con "/ foo / barsucks / bonk". Mover la barra hacia la var y fuera de la evaluación final corrige ese error.
jcwenger

3
Este script simplemente no funciona. Falla una de las pruebas simples de "un directorio inactivo". Las ediciones de jcwenger funcionan un poco mejor, pero tienden a agregar un "../" extra.
Dr. Person Person II

1
me falla en algunos casos si hay un "/" final en el argumento; por ejemplo, si $ 1 = "$ HOME /" y $ 2 = "$ HOME / temp", devuelve "/ home / user / temp /", pero si $ 1 = $ HOME entonces devuelve correctamente la ruta relativa "temp". Por lo tanto, source = $ 1 y target = $ 2 podrían "limpiarse" usando sed (o usando la sustitución de variables bash, pero eso puede ser innecesariamente opaco) como => source = $ (echo "$ {1}" | sed 's / \ / * $ // ')
michael

1
Mejora menor: en lugar de establecer el origen / destino directamente en $ 1 y $ 2, haga: source = $ (cd $ 1; pwd) target = $ (cd $ 2; pwd). De esta manera maneja los caminos con. y ... correctamente.
Joseph Garvin

44
A pesar de ser la respuesta más votada, esta respuesta tiene muchas limitaciones, por lo tanto, se publican muchas otras respuestas. Vea las otras respuestas, especialmente la que muestra casos de prueba. ¡Y por favor vota este comentario!
Offirmo

25

Está integrado 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 su ejemplo:

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

... funcionaría bien.


3
sayno ha estado disponible en perl como registro, pero podría usarse de manera efectiva aquí. perl -MFile::Spec -E 'say File::Spec->abs2rel(@ARGV)'
William Pursell

+1 pero vea también esta respuesta similar que es más antigua (febrero de 2012). Lea también los comentarios pertinentes de William Pursell . Mi versión son dos líneas de comando: perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target"y perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target" "$origin". El primer script perl de una línea usa un argumento (el origen es el directorio de trabajo actual). El segundo script perl de una línea usa dos argumentos.
olibre 01 de

3
Esa debería ser la respuesta aceptada. perlse puede encontrar en casi todas partes, aunque la respuesta sigue siendo de una sola línea.
Dmitry Ginzburg

19

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

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

He jugado la respuesta de pini y algunas otras ideas

Nota : Esto requiere que ambas rutas sean carpetas existentes. Los archivos no funcionarán.


2
respuesta ideal: funciona con / bin / sh, no requiere readlink, python, perl -> ideal para sistemas ligeros / incrustados o consola bash de windows
Francois

2
Desafortunadamente, esto requiere que exista el camino, que no siempre se desea.
drwatsoncode

Respuesta piadosa ¿El material cd-pwd es para resolver enlaces, supongo? Buen golf!
Teck-freak

15

Python os.path.relpathcomo una función de shell

El objetivo de este relpathejercicio es imitar la os.path.relpathfunción de Python 2.7 (disponible desde Python versión 2.6 pero solo funciona correctamente en 2.7), según lo propuesto por xni . Como consecuencia, algunos de los resultados pueden diferir de las funciones proporcionadas en otras respuestas.

(No he probado con nuevas líneas en las rutas simplemente porque rompe la validación basada en llamadas python -cdesde ZSH. Ciertamente sería posible con un poco de esfuerzo).

Con respecto a la "magia" en Bash, he dejado de buscar magia en Bash hace mucho tiempo, 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 tiene como objetivo ser totalmente compatible con POSIX . Lo he probado con /bin/dashDebian 6.0.6 "Squeeze". También funciona perfectamente con/bin/sh OS X 10.8.3, que en realidad es la versión 3.2 de Bash que finge 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 presentado 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 relpathcomando encontrado en $PATHlos casos de prueba proporcionados en otras respuestas. Agregué algo de sabor a esas pruebas agregando algunos espacios, pestañas y signos de puntuación, como ! ? *aquí y allá, y también agregué otra prueba con exóticos personajes UTF-8 encontrados en vim-powerline .

Función de shell POSIX

Primero, la función de shell compatible con POSIX. Funciona con una variedad de rutas, pero no limpia múltiples barras 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 zshversión más robusta . Si desea que resuelva los argumentos de las rutas reales à la realpath -f(disponibles en el coreutilspaquete Linux ), reemplace las :alíneas 3 y 4 con :A.

Para usar esto en zsh, elimine la primera y la última línea y colóquelo en un directorio que esté en su $FPATHvariable.

#!/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 guión de prueba. Acepta una opción, a saber, -vhabilitar 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

2
No funciona cuando termina el primer camino /, me temo.
Noldorin

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

La secuencia de comandos anterior se inspiró en pini's (¡Gracias!). Activa un error en el módulo de resaltado de sintaxis de Stack Overflow (al menos en mi marco de vista previa). Por lo tanto, ignore si el resaltado es incorrecto.

Algunas notas:

  • Se eliminaron los errores y se mejoró el código sin aumentar significativamente la longitud y la complejidad del código
  • Ponga la funcionalidad en funciones para facilitar su uso
  • Funciones mantenidas compatibles con POSIX para que (deberían) funcionar con todos los shells POSIX (probado con dash, bash y zsh en Ubuntu Linux 12.04)
  • Se utilizaron variables locales solo para evitar alterar variables globales y contaminar el espacio de nombre global
  • NO es necesario que existan ambas rutas de directorio (requisito para mi aplicación)
  • Los nombres de ruta pueden contener espacios, caracteres especiales, caracteres de control, barras invertidas, pestañas, ', ",?, *, [,], Etc.
  • La función principal "relPath" usa solo las construcciones de shell POSIX pero requiere rutas de directorio absolutas canónicas como parámetros
  • La función extendida "relpath" puede manejar rutas de directorio arbitrarias (también relativas, no canónicas) pero requiere la utilidad central externa GNU "readlink"
  • Se evitó el "eco" integrado y se utilizó el "printf" integrado en su lugar por dos razones:
  • Para evitar conversiones innecesarias, los nombres de ruta se usan cuando son devueltos y esperados por las utilidades del shell y del sistema operativo (por ejemplo, cd, ln, ls, find, mkdir; a diferencia de "os.path.relpath" de Python, que interpretará algunas secuencias de barra invertida)
  • Excepto por las secuencias de barra invertida mencionadas, 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 se puede reemplazar (y simplificar) por línea

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

    Prefiero el último porque

    1. Los nombres de archivo se pueden agregar directamente a las rutas de directorio obtenidas por relPath, por ejemplo:

      ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
    2. Los enlaces simbólicos en el mismo directorio creado con este método no tienen lo feo "./"antepuesto al nombre del archivo.

  • Si encuentra un error, comuníquese con linuxball (at) gmail.com e intentaré solucionarlo.
  • Conjunto de pruebas 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

9

No muchas de las respuestas aquí son prácticas para el uso diario. Como es muy difícil hacer esto correctamente en puro bash, sugiero la siguiente solución confiable (similar a una sugerencia oculta 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 dado:

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

La única desventaja es que requiere python, pero:

  • Funciona de manera idéntica en cualquier python> = 2.6
  • No requiere que existan los archivos o directorios.
  • 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 los scripts.

Tenga en cuenta que las soluciones que incluyen basenameo dirnamepueden no ser necesariamente mejores, ya que requieren que coreutilsse instalen. Si alguien tiene una bashsolución pura que sea confiable y simple (en lugar de una intrincada curiosidad), me sorprendería.


Este parece, con mucho, el enfoque más sólido.
dimo414

7

Este script proporciona resultados correctos solo para entradas que son rutas absolutas o rutas relativas sin .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"

1
Esto parece funcionar. Si los directorios realmente existen, el uso de $ (readlink -f $ 1) y $ (readlink -f $ 2) en las entradas puede solucionar el problema donde "." o ".." aparece en las entradas. Esto puede causar algunos problemas si los directorios no existen realmente.
Dr. Person Person II

7

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

1
+1, pero recomendaría: perl -MFile::Spec -e "print File::Spec->abs2rel('$absolute','$current')"para que se cite absoluto y actual.
William Pursell

Me gusta relative=$(perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$absolute" "$current"). ¡Esto asegura que los valores no pueden, por sí mismos, contener código perl!
Erik Aronesty

6

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

4

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

+1 para compacidad y timidez. Debería, sin embargo, también llamada readlinken $(pwd).
DevSolar

2
Relativo no significa que el archivo deba colocarse en el mismo directorio.
Greenoldman

Aunque la pregunta original no proporciona muchos casos de prueba, este script falla para pruebas simples como encontrar la ruta relativa de / home / user1 a / home / user2 (respuesta correcta: ../user2). El script de pini / jcwenger funciona para este caso.
michael

4

Otra solución más, pure bash+ GNU readlinkpara un uso fácil en el siguiente contexto:

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

Editar: Asegúrese de que "$ B" no exista o no tenga un enlace suave en ese caso, de lo contrario, relpathsiga este enlace que no es lo que desea.

Esto funciona en casi todos los Linux actuales. Si readlink -mno funciona a su lado, intente en su readlink -flugar. Consulte también https://gist.github.com/hilbix/1ec361d00a8178ae8ea0 para posibles actualizaciones:

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

  • Se tuvo cuidado de que sea seguro contra la expansión no deseada de metacaracteres de shell, en caso de que los nombres de archivo contengan *o ?.
  • El resultado está destinado a ser utilizable como primer argumento para ln -s:
    • relpath / /da .y no la cadena vacía
    • relpath a ada a, incluso si aresulta ser un directorio
  • Los casos más comunes fueron probados para dar resultados razonables, también.
  • Esta solución utiliza coincidencia de prefijo de cadena, por lo tanto readlink lo es necesaria para canonizar las rutas.
  • Gracias a readlink -mesto también funciona para rutas aún no existentes.

En sistemas antiguos, donde readlink -mno está disponible, readlink -ffalla si el archivo no existe. Así que probablemente necesites alguna solución como esta (¡no probado!):

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

Esto no es realmente correcto en caso de que $1incluya .o ..para rutas no existentes (como en /doesnotexist/./a), pero debería cubrir la mayoría de los casos.

(Reemplace readlink -m --arriba por readlink_missing.)

Editar debido al downvote sigue

Aquí hay una prueba de que esta función es correcta:

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" "/A.x"         "../../A.x"
check "/A/B/C" "/A/B"         "."
check "/A/B/C" "/A/B/C"       "C"
check "/A/B/C" "/A/B/C/D"     "C/D"
check "/A/B/C" "/A/B/C/D/E"   "C/D/E"
check "/A/B/C" "/A/B/D"       "D"
check "/A/B/C" "/A/B/D/E"     "D/E"
check "/A/B/C" "/A/D"         "../D"
check "/A/B/C" "/A/D/E"       "../D/E"
check "/A/B/C" "/D/E/F"       "../../D/E/F"

check "/foo/baz/moo" "/foo/bar" "../bar"

¿Perplejo? Bueno, estos son los resultados correctos ! Incluso si cree que no se ajusta a la pregunta, aquí está la prueba de que esto es correcto:

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

Sin ninguna duda, ../bares la ruta relativa exacta y única correcta de la página barvista desde la páginamoo . Todo lo demás estaría completamente equivocado.

Es trivial adoptar el resultado a la pregunta que aparentemente supone, que currentes un directorio:

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

Esto devuelve exactamente lo que se solicitó.

Y antes de levantar una ceja, aquí hay una variante un poco más compleja de relpath(detectar la pequeña diferencia), que también debería funcionar para la sintaxis de URL (por lo que un seguimiento /sobrevive, gracias a algo de bashmagia):

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

Y aquí están las comprobaciones solo para dejar en claro: Realmente funciona como se dijo.

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" "/A.x"         "../../A.x"
check "/A/B/C" "/A/B"         "."
check "/A/B/C" "/A/B/C"       "C"
check "/A/B/C" "/A/B/C/D"     "C/D"
check "/A/B/C" "/A/B/C/D/E"   "C/D/E"
check "/A/B/C" "/A/B/D"       "D"
check "/A/B/C" "/A/B/D/E"     "D/E"
check "/A/B/C" "/A/D"         "../D"
check "/A/B/C" "/A/D/E"       "../D/E"
check "/A/B/C" "/D/E/F"       "../../D/E/F"

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

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

Y así es como se puede usar para dar el resultado deseado de la pregunta:

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

Si encuentra algo que no funciona, hágamelo saber en los comentarios a continuación. Gracias.

PD:

¿Por qué son los argumentos de relpath "invertido" en contraste con todas las otras respuestas aquí?

Si cambias

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

a

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

entonces puede dejar el segundo parámetro ausente, de modo que BASE sea el directorio / URL actual / lo que sea. Ese es solo el principio de Unix, como de costumbre.

Si no le gusta, vuelva a Windows. Gracias.


3

Lamentablemente, la respuesta de Mark Rushakoff (ahora eliminada; hace referencia al código desde aquí ) no parece funcionar correctamente cuando se adapta a:

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

El pensamiento esbozado en el comentario se puede refinar para que funcione correctamente en la mayoría de los casos. Estoy a punto de suponer que el script toma un argumento fuente (donde se encuentra) y un argumento objetivo (donde 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 siguiente código 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 ruta y usted puede estar seriamente desordenado por enlaces simbólicos extraños.
  3. El código no maneja 'puntos' extraviados en rutas como 'xyz/./pqr '.
  4. El código no maneja 'puntos dobles' extraviados en rutas como ' xyz/../pqr'.
  5. Trivialmente: el código no elimina los principales ' ./' de las rutas.

El código de Dennis es mejor porque repara 1 y 5, pero tiene los mismos problemas 2, 3, 4. Utilice el código de Dennis (y vote por encima de esto) debido a eso.

(NB: POSIX proporciona una llamada al sistema realpath()que resuelve los nombres de ruta 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 código C que Wraps realpath(), lo he hecho, pero no conozco una utilidad estándar que lo haga).


Para esto, encuentro que Perl es más fácil de usar que shell, aunque bash tiene un soporte decente para las matrices y probablemente también podría hacer esto: ejercicio para el lector. Entonces, dados dos nombres compatibles, divídalos en componentes:

  • Establecer la ruta relativa para vaciar.
  • Si bien los componentes son los mismos, pase al siguiente.
  • Cuando los componentes correspondientes son diferentes o no hay más componentes para una ruta:
  • Si no quedan componentes de origen restantes y la ruta relativa está vacía, agregue "." al principio
  • Para cada componente fuente restante, prefije la ruta relativa con "../".
  • Si no quedan componentes de destino restantes y la ruta relativa está vacía, agregue "." al principio
  • Para cada componente de destino restante, agregue el componente al final de la ruta después de una barra inclinada.

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

Script de prueba (los corchetes contienen un espacio en blanco y una pestaña):

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

Salida del script de prueba:

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

Este script de Perl funciona bastante bien en Unix (no tiene en cuenta todas las complejidades de los nombres de ruta de Windows) frente a entradas extrañas. Utiliza el módulo Cwdy su función realpathpara resolver la ruta real de los nombres que existen, y realiza un análisis textual para las rutas que no existen. En todos los casos, excepto uno, produce el mismo resultado que el script de Dennis. El caso desviado es:

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

Los dos resultados son equivalentes, pero no idénticos. (El resultado es de una versión ligeramente modificada del script de prueba: el script de Perl a continuación simplemente imprime la respuesta, en lugar de las entradas y la respuesta como en el script anterior.) Ahora: ¿debería eliminar la respuesta que no funciona? 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);

Tu /home/part1/part2to /tiene uno demasiado ../. De lo contrario, mi secuencia de comandos coincide con su salida, excepto que la mía agrega una innecesaria .al final de donde está el destino .y no uso una ./al comienzo de las que descienden sin subir.
Pausado hasta nuevo aviso.

@Dennis: pasé el tiempo con los ojos cruzados sobre los resultados, a veces podía ver ese problema, y ​​a veces no podía encontrarlo nuevamente. Eliminar un './' inicial es otro paso trivial. Tu comentario sobre 'no incrustado. o ... 'también es pertinente. En realidad, es sorprendentemente difícil hacer el trabajo correctamente, doblemente si alguno de los nombres es realmente un enlace simbólico; los dos estamos haciendo un análisis puramente textual.
Jonathan Leffler

@Dennis: Por supuesto, a menos que tenga una red de Newcastle Connection, tratar de superar la raíz es inútil, por lo tanto ../../../ .. y ../../ .. son equivalentes. Sin embargo, eso es puro escapismo; Tu crítica es correcta. (Newcastle Connection le permitió configurar y usar la notación /../host/path/on/remote/machine para llegar a un host diferente, un esquema ordenado. Creo que es compatible con /../../network/host/ path / on / remote / network / and / host también. Está en Wikipedia.)
Jonathan Leffler

Entonces, en cambio, ahora tenemos el doble corte de UNC.
Pausado hasta nuevo aviso.

1
La utilidad "readlink" (al menos la versión GNU) puede hacer el equivalente de realpath (), si le pasa la opción "-f". Por ejemplo, en mi sistema, readlink /usr/bin/vida /etc/alternatives/vi, pero ese es otro enlace simbólico - mientras que readlink -f /usr/bin/vida /usr/bin/vim.basic, que es el destino final de todos los enlaces simbólicos ...
psmears

3

Tomé su pregunta como un desafío para escribir esto en código de shell "portátil", es decir

  • con un shell POSIX en mente
  • sin bashismos como matrices
  • evite llamar a externos como la peste. ¡No hay un solo tenedor en el guión! Eso lo hace increíblemente rápido, especialmente en sistemas con una sobrecarga de horquilla significativa, como cygwin.
  • Debe tratar con caracteres globales en nombres de ruta (*,?, [,])

Se ejecuta en cualquier shell POSIX conforme (zsh, bash, ksh, ash, busybox, ...). Incluso contiene un traje de prueba para verificar su funcionamiento. La canonización de los nombres de ruta se deja como un ejercicio. :-)

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

2

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

}

Esto es extremadamente portátil :)
Anónimo

2

Aquí está mi versión. Se basa en la respuesta de @Offirmo . Lo hice compatible con Dash y solucioné el siguiente error de caso de prueba:

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

Ver el codigo:

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

1

Supongo que este también hará el truco ... (viene con pruebas integradas) :)

OK, se esperan algunos gastos generales, ¡pero estamos haciendo Bourne shell aquí! ;)

#!/bin/sh

#
# Finding the relative path to a certain file ($2), given the absolute path ($1)
# (available here too http://pastebin.com/tWWqA8aB)
#
relpath () {
  local  FROM="$1"
  local    TO="`dirname  $2`"
  local  FILE="`basename $2`"
  local  DEBUG="$3"

  local FROMREL=""
  local FROMUP="$FROM"
  while [ "$FROMUP" != "/" ]; do
    local TOUP="$TO"
    local TOREL=""
    while [ "$TOUP" != "/" ]; do
      [ -z "$DEBUG" ] || echo 1>&2 "$DEBUG$FROMUP =?= $TOUP"
      if [ "$FROMUP" = "$TOUP" ]; then
        echo "${FROMREL:-.}/$TOREL${TOREL:+/}$FILE"
        return 0
      fi
      TOREL="`basename $TOUP`${TOREL:+/}$TOREL"
      TOUP="`dirname $TOUP`"
    done
    FROMREL="..${FROMREL:+/}$FROMREL"
    FROMUP="`dirname $FROMUP`"
  done
  echo "${FROMREL:-.}${TOREL:+/}$TOREL/$FILE"
  return 0
}

relpathshow () {
  echo " - target $2"
  echo "   from   $1"
  echo "   ------"
  echo "   => `relpath $1 $2 '      '`"
  echo ""
}

# If given 2 arguments, do as said...
if [ -n "$2" ]; then
  relpath $1 $2

# If only one given, then assume current directory
elif [ -n "$1" ]; then
  relpath `pwd` $1

# Otherwise perform a set of built-in tests to confirm the validity of the method! ;)
else

  relpathshow /usr/share/emacs22/site-lisp/emacs-goodies-el \
              /usr/share/emacs22/site-lisp/emacs-goodies-el/filladapt.el

  relpathshow /usr/share/emacs23/site-lisp/emacs-goodies-el \
              /usr/share/emacs22/site-lisp/emacs-goodies-el/filladapt.el

  relpathshow /usr/bin \
              /usr/share/emacs22/site-lisp/emacs-goodies-el/filladapt.el

  relpathshow /usr/bin \
              /usr/share/emacs22/site-lisp/emacs-goodies-el/filladapt.el

  relpathshow /usr/bin/share/emacs22/site-lisp/emacs-goodies-el \
              /etc/motd

  relpathshow / \
              /initrd.img
fi

1

Este script solo funciona en los nombres de ruta. No requiere que exista ninguno de los archivos. Si las rutas pasadas no son absolutas, el comportamiento es un poco inusual, pero debería funcionar como se espera si ambas rutas son relativas.

Solo lo probé en OS X, por lo que podría no ser portátil.

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

Además, el código es un poco copiar y pegar, pero lo necesitaba bastante rápido.
juancn

Bien ... justo lo que necesitaba. Y ha incluido una rutina de canonización que es mejor que la mayoría de las otras que he visto (que generalmente se basan en reemplazos de expresiones regulares).
drwatsoncode

0

Esta respuesta no aborda la parte Bash de la pregunta, pero debido a que intenté usar las respuestas en esta pregunta para implementar esta funcionalidad en Emacs , la arrojaré allí.

Emacs realmente tiene una función para esto fuera de la caja:

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"

Tenga en cuenta que creo que la respuesta de Python que agregué recientemente (la relpathfunción) se comporta de manera idéntica file-relative-namepara los casos de prueba que ha proporcionado.
Gary Wisniewski

-1

Aquí hay un script de shell que lo hace sin llamar a otros programas:

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

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


1
Esto no es realmente portátil debido al uso de la construcción $ {param / pattern / subst}, que no es POSIX (a partir de 2011).
Jens

La fuente referenciada ynform.org/w/Pub/Relpath apunta a una página wiki completamente ilegible que contiene el contenido del script varias veces, entremezcladas con líneas de tilde vi, mensajes de error sobre comandos no encontrados y otras cosas. Totalmente inútil para alguien que investiga el original.
Jens

-1

Necesitaba algo como esto, pero también resolvió los enlaces simbólicos. Descubrí que pwd tiene una bandera -P para ese propósito. Se adjunta un fragmento de mi script. Está dentro de una función en un script de shell, de ahí los $ 1 y $ 2. El valor del resultado, que es la ruta relativa de START_ABS a END_ABS, está en la variable UPDIRS. Los cd del script en cada directorio de parámetros para ejecutar el pwd -P y esto también significa que se manejan los parámetros de ruta relativa. Saludos, 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"
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.