web-dev-qa-db-fra.com

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

Exemple:

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

# Magic

relative="../../bar"

Comment créer la magie (espérons pas un code trop compliqué ...)?

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

donne:

../../bar
147
xni

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

Ceci est 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: "-z" test si la chaîne est nulle (= vide) et "-n" si la chaîne 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"
30
Offirmo
#!/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/}
24
pini

Il est intégré à Perl depuis 2001, de sorte qu'il fonctionne sur presque tous les systèmes imaginables, même VMS .

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

En outre, la solution est facile à comprendre.

Donc, pour votre exemple:

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

... fonctionnerait bien.

19
Erik Aronesty

os.path.relpath de Python en tant que fonction shell

Le but de cet exercice relpath est de reproduire la fonction os.path.relpath de Python 2.7 (disponible à partir de la version 2.6 de Python mais ne fonctionnant correctement que dans la version 2.7), comme proposé par xni . En conséquence, certains résultats peuvent différer des fonctions fournies dans d’autres réponses.

(Je n'ai pas testé les nouvelles lignes dans les chemins simplement parce que la validation basée sur l'appel de python -c à partir de ZSH est interrompue. Cela serait certainement possible avec quelques efforts.)

En ce qui concerne la «magie» dans Bash, j’ai abandonné la recherche de la magie dans Bash depuis longtemps, mais j’ai depuis trouvé toute la magie dont j’avais besoin, et même plus, dans ZSH.

En conséquence, je propose deux implémentations.

La première mise en œuvre vise à être entièrement conforme à POSIX. Je l’ai testé avec /bin/dash sur Debian 6.0.6 «Squeeze». Cela fonctionne aussi parfaitement avec /bin/sh sur OS X 10.8.3, qui est en fait Bash version 3.2 prétendant être un shell POSIX.

La deuxième implémentation est une fonction ZSH Shell qui résiste à de nombreuses barres obliques et autres nuisances dans les chemins. Si vous disposez de ZSH, il s'agit de la version recommandée, même si vous l'appelez sous la forme de script présentée 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 commande relpath trouvée dans $PATH en fonction des scénarios 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 tels que ! ? * ici et là, et j'ai également ajouté un autre test avec des caractères exotiques UTF-8 trouvés dans vim-powerline .

POSIX Fonction shell

Tout d’abord, la fonction Shell compatible POSIX. Il fonctionne avec une variété de chemins, mais ne nettoie pas plusieurs barres obliques ni ne résout 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 ZSH Shell

Maintenant, la version zsh plus robuste. Si vous souhaitez résoudre les arguments en chemins réels à la realpath -f (disponible dans le paquetage Linux coreutils), remplacez le :a sur les lignes 3 et 4 par :A.

Pour utiliser ceci dans zsh, supprimez la première et la dernière ligne et placez-la dans un répertoire qui se trouve dans votre variable $FPATH.

#!/usr/bin/env zsh
relpath () {
    [[ $# -ge 1 ]] && [[ $# -le 2 ]] || return 1
    local target=${${2:-$1}:a} # replace `:a' by `:A` to resolve symlinks
    local current=${${${2:+$1}:-$PWD}:a} # replace `:a' by `:A` to resolve symlinks
    local appendix=${target#/}
    local relative=''
    while appendix=${target#$current/}
        [[ $current != '/' ]] && [[ $appendix = $target ]]; do
        if [[ $current = $appendix ]]; then
            relative=${relative:-.}
            print ${relative#/}
            return 0
        fi
        current=${current%/*}
        relative="$relative${relative:+/}.."
    done
    relative+=${relative:+${appendix:+/}}${appendix#/}
    print $relative
}
relpath "$@"

Script de test

Enfin, le script de test. Il accepte une option, à savoir -v pour activer 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
15
simonair
#!/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 ci-dessus a été inspiré par pini's (Merci!). Cela déclenche un bug.__ dans le module de coloration syntaxique de Stack Overflow (au moins dans mon aperçu Frame). Alors ignorez si la mise en évidence est incorrecte.

Quelques notes: 

  • Suppression des erreurs et amélioration du code sans augmentation significative du code Longueur et complexité
  • Mettez la fonctionnalité dans les fonctions pour la facilité d'utilisation 
  • Fonctions conservées compatibles POSIX afin qu'elles fonctionnent (avec) avec tous les shells POSIX (Testés avec dash, bash et zsh sous Ubuntu Linux 12.04)
  • N'utilisez les variables locales que pour éviter de surcharger les variables globales et En polluant l'espace de noms global
  • Les deux chemins de répertoire NE doivent PAS exister (condition requise pour mon application)
  • Les noms de chemin 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 fonctions intégrées du shell POSIX, mais requiert les chemins d'accès aux répertoires absolus canoniques en tant que paramètres.
  • La fonction étendue "relpath" peut gérer des chemins de répertoire arbitraires (également Relatifs, non canoniques) mais nécessite un utilitaire externe GNU core "readlink"
  • Évite "echo" et utilise "printf" à la place pour deux raisons:
  • Pour éviter les conversions inutiles, les noms de chemin sont utilisés car ils sont retournés .__ et attendus par les utilitaires Shell et OS (par exemple, cd, ln, ls, find, mkdir; Contrairement à "os.path.relpath" de python qui en interprétera backslash séquences)
  • À l'exception des séquences de barre oblique inverse mentionnées, la dernière ligne de la fonction "relPath" Génère des chemins compatibles avec python:

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

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

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

    Je préfère ce dernier parce que

    1. Les noms de fichiers peuvent être directement ajoutés aux chemins de répertoires 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 ne comportent pas Le laid "./" ajouté au nom de fichier.

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

Liste de code pour les tests de régression (il suffit de l'ajouter 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
11
linuxball

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

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

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

11
Alexx Roche

Je voudrais juste utiliser Perl pour cette tâche pas si triviale:

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

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

Une légère amélioration sur kasku et Pini answers, qui joue plus agréablement 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
6
sinelaw

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

Peu de réponses ici sont pratiques pour une utilisation quotidienne. Comme il est très difficile de le faire correctement en Bash pur, je suggère la solution fiable suivante (similaire à une suggestion enfouie 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:

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

Notez que les solutions incluant basename ou dirname peuvent ne pas être nécessairement meilleures, car elles nécessitent l'installation de coreutils. Si quelqu'un a une solution bash pure, fiable et simple (plutôt qu'une curiosité alambiquée), je serais surpris.

4
Gary Wisniewski

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

J’ai pris votre question comme un défi de l’écrire dans du code Shell "portable", c’est-à-dire.

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

Il fonctionne sur n’importe quel shell conforme à POSIX (zsh, bash, ksh, ash, busybox, ...). Il contient même une suite de tests pour vérifier son fonctionnement. La canonisation des noms de chemins est laissée à titre d'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 :
3
Jens

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

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

La pensée exposée dans le commentaire peut être affinée pour fonctionner correctement dans la plupart des cas. Je suis sur le point de supposer que le script prend un argument source (où vous en êtes) et un argument cible (où vous voulez accéder à), et que les deux sont des noms de chemin absolus ou que 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 en cours - mais le code ci-dessous ne le fait pas.


Il faut se méfier

Le code ci-dessous est proche 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, c'est que ce traitement purement textuel des noms de chemin et que vous pouvez être sérieusement dérangé par des liens symboliques étranges.
  3. Le code ne gère pas les "points" parasites dans les chemins tels que "xyz/./pqr".
  4. Le code ne gère pas les "doubles points" parasites dans les chemins tels que "xyz/../pqr".
  5. Trivial: le code ne supprime pas les './' 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 en avance sur celui-ci) à cause de cela.

(NB: POSIX fournit un appel système realpath() qui résout les noms de chemins afin qu’ils ne contiennent plus de liens symboliques. Appliquer cela aux noms des entrées, puis utiliser le code de Dennis donnerait la bonne réponse à chaque fois. L’écriture du code C est simple cela enveloppe realpath() - je l'ai fait - mais je ne connais pas d'utilitaire standard qui le fasse.)


Pour ce faire, je trouve Perl plus facile à utiliser que Shell, bien que bash bénéficie d’un soutien décent pour les tableaux et puisse probablement le faire aussi - un exercice pour le lecteur. Donc, étant donné deux noms compatibles, divisez-les chacun en composants:

  • Définissez le chemin relatif sur vide.
  • Bien que les composants soient identiques, 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.

Ainsi:

#!/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 chemins Windows) face à des entrées étranges. Il utilise le module Cwd et sa fonction realpath pour résoudre le chemin réel des noms existants et effectue une analyse textuelle pour les chemins inexistants. Dans tous les cas sauf un, il produit le même résultat 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 affiche 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);
3
Jonathan Leffler

Voici ma version. Il est basé sur le answer by @Offirmo . Je l'ai rendu compatible avec Dash et corrigé l'échec suivant du test:

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

À présent:

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
}
2
Ray Donnelly

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

}
2
Anonymous

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

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

Édition: Assurez-vous que "$ B" n’existe pas ou n’existe pas de lien symbolique dans ce cas, sinon relpath suit ce lien, ce qui n’est pas ce que vous voulez!

Cela fonctionne dans presque tous les Linux actuels. Si readlink -m ne fonctionne pas à vos côtés, essayez plutôt readlink -f. Voir aussi https://Gist.github.com/hilbix/1ec361d00a8178ae8ea0 pour les éventuelles mises à jour:

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

  • Nous avons pris soin de nous en assurer contre toute expansion indésirable des méta-caractères de Shell, au cas où les noms de fichiers contiennent * ou ?.
  • La sortie est censée être utilisable comme premier argument de ln -s: .____.
    • relpath / / donne . et pas la chaîne vide
    • relpath a a donne a, même si a est un répertoire
  • La plupart des cas courants ont également été testés pour donner des résultats raisonnables.
  • Cette solution utilise la correspondance du préfixe de chaîne. readlink est donc nécessaire pour canoniser les chemins.
  • Grâce à readlink -m, cela fonctionne également pour les chemins non existants.

Sur les anciens systèmes, où readlink -m n'est pas disponible, readlink -f échoue si le fichier n'existe pas. Donc, vous avez 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 si $1 inclut . ou .. pour des chemins inexistants (comme dans /doesnotexist/./a), mais cela devrait couvrir la plupart des cas.

(Remplacez readlink -m -- par readlink_missing.)

Modifier à cause du vote négatif qui 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 ceci est correct:

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

Sans aucun doute, ../bar est le chemin relatif exact et unique correct de la page bar vue depuis la page moo. Tout le reste serait tout simplement faux.

Il est trivial d’adopter le résultat de la question qui suppose apparemment que current est un répertoire:

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

Cela retourne exactement ce qui a été demandé.

Et avant de lever les sourcils, voici une variante un peu plus complexe de relpath (remarquez la petite différence), qui devrait également fonctionner pour URL-Syntax (si un / suivi survit, grâce à une certaine bash-magie):

# 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 vérifications pour préciser: Cela fonctionne vraiment comme prévu.

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 sont-ils "inversés" par opposition à 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 absent, tel que la BASE soit le répertoire/URL actuel/peu importe. Ce n'est que le principe Unix, comme d'habitude.

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

1
Tino

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 ici Bourne Shell! ;) 

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

Ce script ne fonctionne que sur les noms de chemins. Il ne nécessite aucun des fichiers 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 l'ai seulement testé sur OS X, de sorte qu'il 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
1
juancn

Cette réponse ne traite pas de 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.

Emacs a en fait une fonction pour cela:

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"
0
fakedrake