Convertir le chemin absolu en chemin relatif étant donné un répertoire courant à l'aide de Bash


261

Exemple:

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

# Magic

relative="../../bar"

Comment créer la magie (si tout va bien pas du code trop compliqué ...)?


7
Par exemple (mon cas en ce moment) pour donner un chemin relatif à gcc afin qu'il puisse générer des informations de débogage relatives utilisables même si le chemin source change.
Offirmo

Une question similaire à celle-ci a été posée sur U&L: unix.stackexchange.com/questions/100918/… . L'une des réponses (@Gilles) mentionne un outil, les liens symboliques , qui peut faciliter la résolution de ce problème.
slm

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

Réponses:


229

Utiliser realpath de GNU coreutils 8.23 ​​est le plus simple, je pense:

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

Par exemple:

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

7
C'est dommage que le paquet soit obsolète sur Ubuntu 14.04 et ne dispose pas de l'option --relative-to.
kzh

3
Fonctionne bien sur Ubuntu 16.04
cayhorstmann

7
$ realpath --relative-to="${PWD}" "$file"est utile si vous voulez les chemins relatifs au répertoire de travail courant.
dcoles

1
C'est correct pour les choses à l'intérieur du /usr/bin/nmap/chemin mais pas pour /usr/bin/nmap: de nmapà /tmp/testingc'est seulement ../../et pas 3 fois ../. Cela fonctionne cependant, car faire ..sur les rootfs l'est /.
Patrick B.

6
Comme @PatrickB. implicite, --relative-to=…attend un répertoire et NE PAS vérifier. Cela signifie que vous vous retrouvez avec un "../" supplémentaire si vous demandez un chemin par rapport à un fichier (comme cet exemple semble le faire, car il /usr/bincontient rarement ou jamais de répertoires et nmapest normalement un binaire)
IBBoard

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

donne:

../../bar

11
Cela fonctionne, et cela rend les alternatives ridicules. C'est un bonus pour moi xD
hasvn

31
+1. Ok, tu as triché ... mais c'est trop beau pour ne pas être utilisé! relpath(){ python -c "import os.path; print os.path.relpath('$1','${2:-$PWD}')" ; }
MestreLion

4
Malheureusement, ce n'est pas disponible universellement: os.path.relpath est nouveau dans Python 2.6.
Chen Levy

15
@ChenLevy: Python 2.6 est sorti en 2008. Difficile de croire qu'il n'était pas universellement disponible en 2012.
MestreLion

11
python -c 'import os, sys; print(os.path.relpath(*sys.argv[1:]))'fonctionne de manière plus naturelle et fiable.
musiphil

31

Il s'agit d'une amélioration corrigée et entièrement fonctionnelle de la solution actuellement la mieux notée de @pini (qui ne gère malheureusement que quelques cas)

Rappel: test '-z' si la chaîne est de longueur nulle (= vide) et test '-n' si la chaîne n'est pas vide.

# 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

Cas de test:

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
Intégré dans le shell offirmo lib github.com/Offirmo/offirmo-shell-lib , fonction «OSL_FILE_find_relative_path» (fichier «osl_lib_file.sh»)
Offirmo

1
+1. Il peut facilement être fait pour gérer tous les chemins (pas seulement les chemins absolus commençant par /) en remplaçant source=$1; target=$2parsource=$(realpath $1); target=$(realpath $2)
Josh Kelley

2
@Josh en effet, à condition que les répertoires existent réellement ... ce qui n'était pas pratique pour les tests unitaires;) Mais en utilisation réelle oui, realpathest recommandé, source=$(readlink -f $1)etc. si realpath n'est pas disponible (non standard)
Offirmo

J'ai défini $sourceet $targetcomme ceci: `if [[-e $ 1]]; alors source = $ (readlink -f $ 1); else source = $ 1; fi si [[-e $ 2]]; alors target = $ (readlink -f $ 2); sinon cible = 2 $; fi` De cette façon, la fonction pourrait gérer des chemins relatifs réels / existants ainsi que des répertoires fictifs.
Nathan S. Watson-Haigh

1
@ NathanS.Watson-Haigh Encore mieux, j'ai découvert récemment readlinkune -moption qui fait juste ça;)
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/}

Magnifique script - court et propre. J'ai appliqué une modification (en attente d'examen par les pairs): common_part = $ source / common_part = $ (dirname $ common_part) / echo $ {back} $ {target # $ common_part} Le script existant échouerait en raison d'une correspondance inappropriée au début du nom du répertoire lors de la comparaison, par exemple: "/ foo / bar / baz" à "/ foo / barsucks / bonk". Déplacer la barre oblique dans la var et hors de l'évaluation finale corrige ce bogue.
jcwenger

3
Ce script ne fonctionne tout simplement pas. Échoue un test simple "un répertoire vers le bas". Les modifications de jcwenger fonctionnent un peu mieux mais ont tendance à ajouter un "../" supplémentaire.
Dr. Person Person II

1
il échoue pour moi dans certains cas si un "/" de fin est sur l'argument; Par exemple, si $ 1 = "$ HOME /" et $ 2 = "$ HOME / temp", il retourne "/ home / user / temp /", mais si $ 1 = $ HOME alors il renvoie correctement le chemin relatif "temp". Source = $ 1 et target = $ 2 pourraient donc être "nettoyés" en utilisant sed (ou en utilisant la substitution de variables bash, mais qui peuvent être inutilement opaques) tels que => source = $ (echo "$ {1}" | sed 's / \ / * $ // ')
michael

1
Amélioration mineure: au lieu de définir directement source / cible sur 1 $ et 2 $, faites: source = $ (cd $ 1; pwd) cible = $ (cd $ 2; pwd). De cette façon, il gère les chemins avec. et .. correctement.
Joseph Garvin

4
Bien qu'elle soit la réponse la plus votée, cette réponse a beaucoup de limites, d'où de nombreuses autres réponses publiées. Voir les autres réponses à la place, en particulier celle affichant les cas de test. Et veuillez voter pour ce commentaire!
Offirmo

25

Il est intégré à Perl depuis 2001, il fonctionne donc sur presque tous les systèmes que vous pouvez imaginer, même VMS .

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

De plus, la solution est facile à comprendre.

Donc pour votre exemple:

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

... fonctionnerait bien.


3
sayn'a pas été disponible en perl pour le journal, mais il pourrait être utilisé efficacement ici. perl -MFile::Spec -E 'say File::Spec->abs2rel(@ARGV)'
William Pursell

+1 mais voir aussi cette réponse similaire qui est plus ancienne (février 2012). Lisez également les commentaires pertinents de William Pursell . Ma version comporte deux lignes de commande: perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target"et perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target" "$origin". Le premier script perl d' une ligne utilise un argument (l'origine est le répertoire de travail actuel). Le deuxième script perl d' une ligne utilise deux arguments.
olibre

3
Cela devrait être la réponse acceptée. perlpeut être trouvé presque partout, bien que la réponse soit toujours à sens unique.
Dmitry Ginzburg

19

En supposant que vous avez installé: bash, pwd, dirname, echo; alors relpath est

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

J'ai joué la réponse de pini et quelques autres idées

Remarque : Cela nécessite que les deux chemins d'accès soient des dossiers existants. Les fichiers ne fonctionneront pas .


2
réponse idéale: fonctionne avec / bin / sh, ne nécessite pas de lien de lecture, python, perl -> idéal pour les systèmes légers / embarqués ou la console bash de windows
François

2
Malheureusement, cela nécessite que le chemin existe, ce qui n'est pas toujours souhaité.
drwatsoncode

Réponse divine. Le truc cd-pwd est pour résoudre les liens je suppose? Bon golf!
Teck-freak

15

Python os.path.relpath tant que fonction shell

Le but de cet relpathexercice est d'imiter la os.path.relpathfonction de Python 2.7 (disponible à partir de Python version 2.6 mais ne fonctionnant correctement qu'en 2.7), comme proposé par xni . Par conséquent, certains résultats peuvent différer des fonctions fournies dans d'autres réponses.

(Je n'ai pas testé avec des sauts de ligne dans les chemins simplement parce que cela rompt la validation basée sur l'appel python -cdepuis ZSH. Ce serait certainement possible avec un certain effort.)

En ce qui concerne la «magie» dans Bash, j'ai renoncé à chercher de la magie dans Bash il y a longtemps, mais j'ai depuis trouvé toute la magie dont j'ai besoin, puis certaines, dans ZSH.

Par conséquent, je propose deux implémentations.

La première implémentation vise à être entièrement compatible POSIX . Je l'ai testé avec /bin/dashsur Debian 6.0.6 «Squeeze». Il fonctionne également parfaitement avec /bin/shOS X 10.8.3, qui est en fait la version Bash 3.2 se faisant passer pour un shell POSIX.

La deuxième implémentation est une fonction shell ZSH qui est robuste contre plusieurs barres obliques et autres nuisances dans les chemins. Si vous avez ZSH disponible, c'est la version recommandée, même si vous l'appelez dans le formulaire de script présenté ci-dessous (c'est-à-dire avec un shebang de #!/usr/bin/env zsh) à partir d'un autre shell.

Enfin, j'ai écrit un script ZSH qui vérifie la sortie de la relpathcommande trouvée dans $PATHles cas de test fournis dans d'autres réponses. J'ai ajouté du piquant à ces tests en ajoutant des espaces, des tabulations et des signes de ponctuation comme ! ? *ici et là et j'ai également ajouté un autre test avec des caractères UTF-8 exotiques trouvés dans vim-powerline .

Fonction shell POSIX

Tout d'abord, la fonction shell compatible POSIX. Il fonctionne avec une variété de chemins, mais ne nettoie pas plusieurs barres obliques et ne résout pas les liens symboliques.

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

Fonction shell ZSH

Maintenant, la zshversion la plus robuste . Si vous souhaitez qu'il résout les arguments en chemins réels à la realpath -f(disponibles dans le coreutilspaquet Linux ), remplacez les :alignes 3 et 4 par :A.

Pour l'utiliser dans zsh, supprimez la première et la dernière ligne et placez-les dans un répertoire qui se trouve dans votre $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 test

Enfin, le script de test. Il accepte une option, à savoir -vactiver la sortie détaillée.

#!/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
Ça ne marche pas quand le premier chemin se termine, /j'ai peur.
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")"; }

Le script shell ci-dessus a été inspiré par pini (Merci!). Il déclenche un bogue dans le module de coloration syntaxique de Stack Overflow (au moins dans mon cadre d'aperçu). Veuillez donc ignorer si la mise en surbrillance est incorrecte.

Quelques notes:

  • Suppression des erreurs et amélioration du code sans augmenter considérablement la longueur et la complexité du code
  • Mettez des fonctionnalités dans des fonctions pour une facilité d'utilisation
  • Fonctions maintenues compatibles POSIX afin qu'elles (devraient) fonctionner avec tous les shells POSIX (testées avec dash, bash et zsh dans Ubuntu Linux 12.04)
  • Utilisé des variables locales uniquement pour éviter d'altérer les variables globales et de polluer l'espace de nom global
  • Les deux chemins de répertoire n'ont PAS besoin d'exister (exigence pour mon application)
  • Les chemins d'accès peuvent contenir des espaces, des caractères spéciaux, des caractères de contrôle, des barres obliques inverses, des tabulations, ', ",?, *, [,], Etc.
  • La fonction principale "relPath" utilise uniquement les commandes internes du shell POSIX mais nécessite des chemins de répertoire absolus canoniques comme paramètres
  • La fonction étendue "relpath" peut gérer des chemins de répertoire arbitraires (également relatifs, non canoniques) mais nécessite un utilitaire de base GNU externe "readlink"
  • "Echo" intégré évité et "printf" intégré à la place pour deux raisons:
  • Pour éviter les conversions inutiles, les chemins d'accès sont utilisés tels qu'ils sont renvoyés et attendus par les utilitaires shell et OS (par exemple cd, ln, ls, find, mkdir; contrairement au "os.path.relpath" de python qui interprétera certaines séquences de barre oblique inverse)
  • À l'exception des séquences de barre oblique inverse mentionnées, la dernière ligne de la fonction "relPath" affiche des noms de chemin compatibles avec python:

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

    La dernière ligne peut être remplacée (et simplifiée) par une ligne

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

    Je préfère ce dernier car

    1. Les noms de fichiers peuvent être directement ajoutés aux chemins dir obtenus par relPath, par exemple:

      ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
    2. Les liens symboliques dans le même répertoire créé avec cette méthode n'ont pas le laid "./"ajouté au nom de fichier.

  • Si vous trouvez une erreur, veuillez contacter linuxball (at) gmail.com et je vais essayer de la corriger.
  • Ajout d'une suite de tests de régression (également compatible avec le shell POSIX)

Liste de code pour les tests de régression (ajoutez-la simplement au script 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

Pas beaucoup de réponses ici sont pratiques pour une utilisation quotidienne. Puisqu'il est très difficile de le faire correctement en pure bash, je suggère la solution fiable suivante (similaire à une suggestion enterrée dans un commentaire):

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

Ensuite, vous pouvez obtenir le chemin relatif en fonction du répertoire actuel:

echo $(relpath somepath)

ou vous pouvez spécifier que le chemin soit relatif à un répertoire donné:

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

Le seul inconvénient est que cela nécessite python, mais:

  • Il fonctionne de manière identique dans n'importe quel python> = 2.6
  • Il ne nécessite pas que les fichiers ou répertoires existent.
  • Les noms de fichiers peuvent contenir une plus large gamme de caractères spéciaux. Par exemple, de nombreuses autres solutions ne fonctionnent pas si les noms de fichiers contiennent des espaces ou d'autres caractères spéciaux.
  • Il s'agit d'une fonction d'une ligne qui n'encombre pas les scripts.

Notez que les solutions qui incluent basenameou dirnamene sont pas nécessairement meilleures, car elles nécessitent d' coreutilsêtre installées. Si quelqu'un a une bashsolution pure , fiable et simple (plutôt qu'une curiosité alambiquée), je serais surpris.


Cela semble de loin l'approche la plus robuste.
dimo414

7

Ce script donne des résultats corrects uniquement pour les entrées qui sont des chemins absolus ou des chemins relatifs sans .ou ..:

#!/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
Cela semble fonctionner. Si les répertoires existent réellement, l'utilisation de $ (readlink -f $ 1) et $ (readlink -f $ 2) sur les entrées peut résoudre le problème où "." ou ".." apparaît dans les entrées. Cela peut causer des problèmes si les répertoires n'existent pas réellement.
Dr. Person Person II

7

J'utiliserais simplement Perl pour cette tâche pas si banale:

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

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

1
+1, mais je le recommande: perl -MFile::Spec -e "print File::Spec->abs2rel('$absolute','$current')"pour que l'absolu et le courant soient cités.
William Pursell

J'aime relative=$(perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$absolute" "$current"). Cela garantit que les valeurs ne peuvent pas elles-mêmes contenir du code perl!
Erik Aronesty

6

Une légère amélioration sur les réponses de Kasku et Pini , qui joue mieux avec les espaces et permet de passer des chemins relatifs:

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

Essai:

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

+1 pour la compacité et la timidité. Vous devriez cependant également appeler readlinksur $(pwd).
DevSolar

2
Relatif ne signifie pas que le fichier doit être placé dans le même répertoire.
greenoldman

bien que la question d'origine ne fournisse pas beaucoup de cas de test, ce script échoue pour des tests simples comme trouver le chemin relatif de / home / user1 vers / home / user2 (bonne réponse: ../user2). Le script de pini / jcwenger fonctionne pour ce cas.
michael

4

Encore une autre solution, pure bash+ GNU readlinkpour une utilisation facile dans le contexte suivant:

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

Edit: Assurez-vous que "$ B" n'existe pas ou pas de lien logiciel dans ce cas, sinon relpathsuit ce lien qui n'est pas ce que vous voulez!

Cela fonctionne dans presque tous les Linux actuels. Si readlink -mcela ne fonctionne pas à vos côtés, essayez readlink -fplutôt. Voir également https://gist.github.com/hilbix/1ec361d00a8178ae8ea0 pour les mises à jour possibles:

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

Remarques:

  • Le soin a été pris qu'il est sûr contre l'expansion indésirable du méta-caractère du shell, au cas où les noms de fichiers contiennent *ou ?.
  • La sortie est censée être utilisable comme premier argument pour ln -s:
    • relpath / /donne .et non la chaîne vide
    • relpath a adonne a, même s'il ase trouve être un répertoire
  • La plupart des cas courants ont également été testés pour donner des résultats raisonnables.
  • Cette solution utilise une correspondance de préfixe de chaîne, elle readlinkest donc requise pour canoniser les chemins.
  • Grâce à readlink -mcela, cela fonctionne aussi pour les chemins pas encore existants.

Sur les anciens systèmes, où il readlink -mn'est pas disponible, readlink -féchoue si le fichier n'existe pas. Vous avez donc probablement besoin d'une solution de contournement comme celle-ci (non testée!):

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

Ce n'est pas vraiment tout à fait correct dans le cas où $1inclut .ou ..pour les chemins inexistants (comme dans /doesnotexist/./a), mais cela devrait couvrir la plupart des cas.

(Remplacer readlink -m --ci-dessus par readlink_missing.)

Modifier à cause du downvote suit

Voici un test, que cette fonction, en effet, est correcte:

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"

Perplexe? Eh bien, ce sont les bons résultats ! Même si vous pensez que cela ne correspond pas à la question, voici la preuve que c'est correct:

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

Sans aucun doute, ../barc'est le chemin relatif exact et seul correct de la page barvu depuis la page moo. Tout le reste serait tout simplement faux.

Il est trivial d'adopter la sortie à la question qui suppose apparemment, c'est currentun répertoire:

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

Cela revient exactement à ce qui était demandé.

Et avant de lever un sourcil, voici une variante un peu plus complexe de relpath(repérer la petite différence), qui devrait également fonctionner pour la syntaxe URL (donc une traînée /survit, grâce à quelques bash-magiques):

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

Et voici les contrôles pour être clair: cela fonctionne vraiment comme indiqué.

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

Et voici comment cela peut être utilisé pour donner le résultat souhaité de la question:

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

Si vous trouvez quelque chose qui ne fonctionne pas, faites-le moi savoir dans les commentaires ci-dessous. Merci.

PS:

Pourquoi les arguments de relpath"renversé" contrastent-ils avec toutes les autres réponses ici?

Si vous changez

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

à

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

alors vous pouvez laisser le 2ème paramètre à l'écart, de telle sorte que la BASE soit le répertoire / URL / quoi que ce soit. Ce n'est que le principe Unix, comme d'habitude.

Si vous n'aimez pas cela, veuillez revenir à Windows. Merci.


3

Malheureusement, la réponse de Mark Rushakoff (maintenant supprimée - elle faisait référence au code d' ici ) ne semble pas fonctionner correctement lorsqu'elle est adaptée à:

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

La pensée décrite dans le commentaire peut être affinée pour la faire fonctionner correctement dans la plupart des cas. Je suis sur le point de supposer que le script prend un argument source (où vous êtes) et un argument cible (où vous voulez vous rendre), et que les deux sont des chemins absolus ou les deux sont relatifs. Si l'un est absolu et l'autre relatif, le plus simple est de préfixer le nom relatif avec le répertoire de travail actuel - mais le code ci-dessous ne le fait pas.


Il faut se méfier

Le code ci-dessous est sur le point de fonctionner correctement, mais n'est pas tout à fait correct.

  1. Il y a le problème abordé dans les commentaires de Dennis Williamson.
  2. Il y a aussi un problème que ce traitement purement textuel des chemins d'accès et vous pouvez être sérieusement gâché par des liens symboliques étranges.
  3. Le code ne gère pas les «points» parasites dans les chemins comme « xyz/./pqr».
  4. Le code ne gère pas les «doubles points» parasites dans des chemins comme « xyz/../pqr».
  5. Anecdotique: le code ne supprime pas le début ' ./' des chemins.

Le code de Dennis est meilleur car il corrige 1 et 5 - mais a les mêmes problèmes 2, 3, 4. Utilisez le code de Dennis (et votez-le plus tôt) à cause de cela.

(NB: POSIX fournit un appel système realpath()qui résout les chemins d'accès afin qu'il n'y ait plus de liens symboliques. Appliquer cela aux noms d'entrée, puis utiliser le code de Dennis donnerait la bonne réponse à chaque fois. Il est trivial d'écrire le code C qui wraps realpath()- je l'ai fait - mais je ne connais pas d'utilitaire standard qui le fasse.)


Pour cela, je trouve Perl plus facile à utiliser que shell, bien que bash ait un support décent pour les tableaux et pourrait probablement le faire aussi - exercice pour le lecteur. Donc, étant donné deux noms compatibles, divisez-les chacun en composants:

  • Définissez le chemin relatif sur vide.
  • Alors que les composants sont les mêmes, passez au suivant.
  • Lorsque les composants correspondants sont différents ou qu'il n'y a plus de composants pour un chemin:
  • S'il ne reste aucun composant source et que le chemin relatif est vide, ajoutez "." au début.
  • Pour chaque composant source restant, préfixez le chemin relatif avec "../".
  • S'il ne reste aucun composant cible et que le chemin relatif est vide, ajoutez "." au début.
  • Pour chaque composant cible restant, ajoutez le composant à la fin du chemin après une barre oblique.

Donc:

#!/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 test (les crochets contiennent un blanc et un onglet):

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

Sortie du script de test:

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

Ce script Perl fonctionne assez bien sur Unix (il ne prend pas en compte toutes les complexités des noms de chemin Windows) face à des entrées étranges. Il utilise le module Cwdet sa fonction realpathpour résoudre le vrai chemin des noms qui existent et fait une analyse textuelle pour les chemins qui n'existent pas. Dans tous les cas sauf un, il produit la même sortie que le script de Dennis. Le cas déviant est:

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

Les deux résultats sont équivalents - mais pas identiques. (La sortie provient d'une version légèrement modifiée du script de test - le script Perl ci-dessous imprime simplement la réponse, plutôt que les entrées et la réponse comme dans le script ci-dessus.) Maintenant: dois-je éliminer la réponse qui ne fonctionne pas? Peut être...

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

Votre /home/part1/part2à /a un trop grand nombre ../. Sinon, mon script correspond à votre sortie, sauf que le mien ajoute un inutile .à la fin de celui où se trouve la destination .et je n'utilise pas un ./au début de ceux qui descendent sans monter.
pause jusqu'à nouvel ordre.

@ Dennis: J'ai passé du temps à regarder les résultats en travers - parfois je pouvais voir ce problème, et parfois je ne pouvais pas le retrouver. La suppression d'un «./» de tête est une autre étape triviale. Votre commentaire sur 'no embedded. ou .. 'est également pertinent. Il est en fait étonnamment difficile de faire le travail correctement - d'autant plus si l'un des noms est en fait un lien symbolique; nous faisons tous deux une analyse purement textuelle.
Jonathan Leffler

@Dennis: Bien sûr, à moins que vous n'ayez un réseau Newcastle Connection, essayer de dépasser la racine est inutile, donc ../../../ .. et ../../ .. sont équivalents. Cependant, c'est de l'évasion pure; votre critique est correcte. (Newcastle Connection vous a permis de configurer et d'utiliser la notation /../host/path/on/remote/machine pour accéder à un hôte différent - un schéma soigné. Je pense qu'il prend en charge /../../network/host/ path / on / remote / network / and / host too. It's on Wikipedia.)
Jonathan Leffler

Au lieu de cela, nous avons maintenant la double barre oblique de l'UNC.
pause jusqu'à nouvel ordre.

1
L'utilitaire "readlink" (au moins la version GNU) peut faire l'équivalent de realpath (), si vous lui passez l'option "-f". Par exemple, sur mon système, readlink /usr/bin/vidonne /etc/alternatives/vi, mais c'est un autre lien symbolique - alors que readlink -f /usr/bin/vidonne /usr/bin/vim.basic, qui est la destination ultime de tous les liens symboliques ...
psmears

3

J'ai pris votre question comme un défi pour l'écrire en code shell "portable", c'est-à-dire

  • avec un shell POSIX en tête
  • pas de bashismes tels que des tableaux
  • évitez d'appeler des externes comme la peste. Il n'y a pas une seule fourchette dans le script! Cela le rend incroyablement rapide, en particulier sur les systèmes avec des frais généraux importants, comme cygwin.
  • Doit traiter les caractères glob dans les noms de chemin (*,?, [,])

Il s'exécute sur tout shell conforme POSIX (zsh, bash, ksh, ash, busybox, ...). Il contient même une suite de tests pour vérifier son fonctionnement. La canonisation des chemins d'accès est laissée en exercice. :-)

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

Ma solution:

computeRelativePath() 
{

    Source=$(readlink -f ${1})
    Target=$(readlink -f ${2})

    local OLDIFS=$IFS
    IFS="/"

    local SourceDirectoryArray=($Source)
    local TargetDirectoryArray=($Target)

    local SourceArrayLength=$(echo ${SourceDirectoryArray[@]} | wc -w)
    local TargetArrayLength=$(echo ${TargetDirectoryArray[@]} | wc -w)

    local Length
    test $SourceArrayLength -gt $TargetArrayLength && Length=$SourceArrayLength || Length=$TargetArrayLength


    local Result=""
    local AppendToEnd=""

    IFS=$OLDIFS

    local i

    for ((i = 0; i <= $Length + 1 ; i++ ))
    do
            if [ "${SourceDirectoryArray[$i]}" = "${TargetDirectoryArray[$i]}" ]
            then
                continue    
            elif [ "${SourceDirectoryArray[$i]}" != "" ] && [ "${TargetDirectoryArray[$i]}" != "" ] 
            then
                AppendToEnd="${AppendToEnd}${TargetDirectoryArray[${i}]}/"
                Result="${Result}../"               

            elif [ "${SourceDirectoryArray[$i]}" = "" ]
            then
                Result="${Result}${TargetDirectoryArray[${i}]}/"
            else
                Result="${Result}../"
            fi
    done

    Result="${Result}${AppendToEnd}"

    echo $Result

}

C'est extrêmement portable :)
Anonyme

2

Voici ma version. Il est basé sur la réponse de @Offirmo . Je l'ai rendu compatible avec Dash et corrigé l'échec du testcase suivant:

./compute-relative.sh "/a/b/c/de/f/g" "/a/b/c/def/g/" -> "../..f/g/"

Maintenant:

CT_FindRelativePath "/a/b/c/de/f/g" "/a/b/c/def/g/" -> "../../../def/g/"

Voir le 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
}

1

Je suppose que celui-ci fera aussi l'affaire ... (livré avec des tests intégrés) :)

OK, des frais généraux sont attendus, mais nous faisons du Bourne shell ici! ;)

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

Ce script ne fonctionne que sur les noms de chemin. Il ne nécessite aucun fichier pour exister. Si les chemins passés ne sont pas absolus, le comportement est un peu inhabituel, mais il devrait fonctionner comme prévu si les deux chemins sont relatifs.

Je ne l'ai testé que sur OS X, donc ce n'est peut-être pas portable.

#!/bin/bash
set -e
declare SCRIPT_NAME="$(basename $0)"
function usage {
    echo "Usage: $SCRIPT_NAME <base path> <target file>"
    echo "       Outputs <target file> relative to <base path>"
    exit 1
}

if [ $# -lt 2 ]; then usage; fi

declare base=$1
declare target=$2
declare -a base_part=()
declare -a target_part=()

#Split path elements & canonicalize
OFS="$IFS"; IFS='/'
bpl=0;
for bp in $base; do
    case "$bp" in
        ".");;
        "..") let "bpl=$bpl-1" ;;
        *) base_part[${bpl}]="$bp" ; let "bpl=$bpl+1";;
    esac
done
tpl=0;
for tp in $target; do
    case "$tp" in
        ".");;
        "..") let "tpl=$tpl-1" ;;
        *) target_part[${tpl}]="$tp" ; let "tpl=$tpl+1";;
    esac
done
IFS="$OFS"

#Count common prefix
common=0
for (( i=0 ; i<$bpl ; i++ )); do
    if [ "${base_part[$i]}" = "${target_part[$common]}" ] ; then
        let "common=$common+1"
    else
        break
    fi
done

#Compute number of directories up
let "updir=$bpl-$common" || updir=0 #if the expression is zero, 'let' fails

#trivial case (after canonical decomposition)
if [ $updir -eq 0 ]; then
    echo .
    exit
fi

#Print updirs
for (( i=0 ; i<$updir ; i++ )); do
    echo -n ../
done

#Print remaining path
for (( i=$common ; i<$tpl ; i++ )); do
    if [ $i -ne $common ]; then
        echo -n "/"
    fi
    if [ "" != "${target_part[$i]}" ] ; then
        echo -n "${target_part[$i]}"
    fi
done
#One last newline
echo

De plus, le code est un peu copier-coller, mais j'en avais besoin assez rapidement.
juancn

Bien ... juste ce dont j'avais besoin. Et vous avez inclus une routine de canonisation qui est meilleure que la plupart des autres que j'ai vues (qui reposent généralement sur des remplacements d'expressions régulières).
drwatsoncode

0

Cette réponse ne répond pas à la partie Bash de la question, mais parce que j'ai essayé d'utiliser les réponses de cette question pour implémenter cette fonctionnalité dans Emacs je vais la jeter ici.

Emacs a en fait une fonction prête à l'emploi:

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"

Notez que je crois que la réponse python que j'ai ajoutée récemment (la relpathfonction) se comporte de la même manière file-relative-namepour les cas de test que vous avez fournis.
Gary Wisniewski

-1

Voici un script shell qui le fait sans appeler d'autres programmes:

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


1
Ce n'est pas vraiment portable à cause de l'utilisation de la construction $ {param / pattern / subst}, qui n'est pas POSIX (à partir de 2011).
Jens

La source référencée ynform.org/w/Pub/Relpath pointe vers une page wiki complètement tronquée contenant le contenu du script plusieurs fois, entremêlée de lignes de tilde vi, de messages d'erreur sur les commandes introuvables et ainsi de suite. Tout à fait inutile pour quelqu'un recherchant l'original.
Jens

-1

J'avais besoin de quelque chose comme ça mais qui résolvait aussi les liens symboliques. J'ai découvert que pwd a un drapeau -P à cet effet. Un fragment de mon script est joint. C'est dans une fonction dans un script shell, d'où les $ 1 et $ 2. La valeur de résultat, qui est le chemin relatif de START_ABS à END_ABS, se trouve dans la variable UPDIRS. Les cd de script se trouvent dans chaque répertoire de paramètres afin d'exécuter le pwd -P et cela signifie également que les paramètres de chemin relatifs sont traités. À la vôtre, 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"
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.