Exemple :
absolute="/foo/bar"
current="/foo/baz/foo"
# Magic
relative="../../bar"
Comment créer la magie (en espérant que le code ne soit pas trop compliqué...) ?
Exemple :
absolute="/foo/bar"
current="/foo/baz/foo"
# Magic
relative="../../bar"
Comment créer la magie (en espérant que le code ne soit pas trop compliqué...) ?
+1. Ok, tu as triché... mais c'est trop bien pour ne pas être utilisé ! relpath(){ python -c "import os.path; print os.path.relpath('$1','${2:-$PWD}')" ; }
Malheureusement, ceci n'est pas disponible universellement : os.path.relpath est nouveau dans Python 2.6.
Il s'agit d'une amélioration corrigée et pleinement fonctionnelle de la solution actuellement la mieux notée de @pini (qui ne traite malheureusement que quelques cas).
Rappel : '-z' teste si la chaîne de caractères est de longueur nulle (=vide) et '-n' teste si la chaîne de caractères est no 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"
Intégré dans la librairie shell offirmo github.com/Offirmo/offirmo-shell-lib , fonction "OSL_FILE_find_relative_path" (fichier "osl_lib_file.sh")
+1. Il est facile de le rendre capable de gérer n'importe quel chemin (pas seulement les chemins absolus commençant par /) en remplaçant source=$1; target=$2
con source=$(realpath $1); target=$(realpath $2)
@Josh en effet, à condition que le répertoire existe réellement... ce qui n'était pas pratique pour les tests unitaires ;) Mais en utilisation réelle, oui, realpath
est recommandé, ou source=$(readlink -f $1)
etc. si realpath n'est pas disponible (non standard)
Merveilleux script -- court et propre. J'ai appliqué une modification (En attente d'un examen par les pairs) : common_part=$source/ common_part=$(dirname $common_part)/ echo ${back}${target#$common_part} Le script existant échouait en raison d'une correspondance inappropriée sur le début du nom du répertoire lors de la comparaison, par exemple : "/foo/bar/baz" à "/foo/barsucks/bonk". Le déplacement du slash dans le var et hors de l'eval final corrige ce bug.
Ce script ne fonctionne tout simplement pas. Il échoue à un simple test "un répertoire en moins". Les modifications apportées par jcwenger fonctionnent un peu mieux mais ont tendance à ajouter un "../" supplémentaire.
Après avoir passé 30 minutes à essayer de porter le script ci-dessous (par Dennis Williamson) vers zsh, ce script fait parfaitement le travail pour mon cas d'utilisation simple, et il fonctionne également dans zsh sans modification.
os.path.relpath
comme une fonction shellL'objectif de cette relpath
est d'imiter la méthode de Python 2.7. os.path.relpath
(disponible à partir de la version 2.6 de Python mais ne fonctionnant correctement qu'en 2.7), comme proposé par xni . Par conséquent, certains des résultats peuvent différer des fonctions fournies dans d'autres réponses.
(Je n'ai pas testé l'utilisation de retours à la ligne dans les chemins d'accès, simplement parce que cela rompt la validation basée sur l'appel de l'option python -c
de ZSH. Ce serait certainement possible avec quelques efforts).
En ce qui concerne la "magie" dans Bash, j'ai renoncé depuis longtemps à chercher de la magie dans Bash, mais j'ai depuis trouvé toute la magie dont j'ai besoin, et même plus, dans ZSH.
Par conséquent, je propose deux mises en œuvre.
La première mise en œuvre vise à être pleinement Conforme à POSIX . Je l'ai testé avec /bin/dash
sur Debian 6.0.6 "Squeeze". Il fonctionne aussi parfaitement avec /bin/sh
sur OS X 10.8.3, qui est en fait la version 3.2 de Bash se faisant passer pour un shell POSIX.
La deuxième implémentation est une fonction shell ZSH qui est robuste contre les slashs multiples et autres nuisances dans les chemins. Si vous disposez de ZSH, c'est la version recommandée, même si vous l'appelez sous la forme 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 l'application relpath
qui se trouve dans $PATH
étant donné les cas de test fournis dans les autres réponses. J'ai ajouté un peu de piment à ces tests en ajoutant des espaces, des tabulations et de la ponctuation, comme par exemple ! ? *
ici et là, et j'ai également ajouté un autre test avec des caractères UTF-8 exotiques trouvés dans le fichier vim-powerline .
Tout d'abord, la fonction shell conforme à la norme POSIX. Elle fonctionne avec une variété de chemins, mais ne nettoie pas les slashs multiples 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 "$@"
Maintenant, le plus robuste zsh
version. Si vous souhaitez qu'il résolve les arguments pour les chemins réels à la realpath -f
(disponible dans la version Linux coreutils
), remplacez le :a
aux lignes 3 et 4 avec :A
.
Pour l'utiliser dans zsh, supprimez la première et la dernière ligne et mettez-le dans un répertoire qui se trouve dans votre répertoire $FPATH
variable.
#!/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 "$@"
Enfin, le test script. Il accepte une option, à savoir -v
pour activer la sortie verbeuse.
#!/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
#!/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 shell ci-dessus script 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 cadre de prévisualisation). aperçu). Veuillez donc ignorer si la coloration est incorrecte.
Quelques notes :
Suppression des erreurs et amélioration du code sans augmentation significative du code longueur et la complexité du code
Mettre la fonctionnalité dans les fonctions pour faciliter l'utilisation
Maintien de la compatibilité POSIX des fonctions afin qu'elles fonctionnent (devraient fonctionner) avec tous les systèmes POSIX. (testé avec dash, bash et zsh sous Ubuntu Linux 12.04).
Utiliser uniquement les variables locales pour éviter de bloquer les variables globales et les de polluer l'espace de noms global
Les deux chemins de répertoire NE DOIVENT PAS exister (exigence pour mon application).
Les noms de chemin peuvent contenir des espaces, des caractères spéciaux, des caractères de contrôle, des antislashes, des tabulations, ', ", ?, *, [, ], etc.
La fonction de base "relPath" utilise uniquement les modules POSIX du shell mais requiert 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 canonique) mais nécessite l'utilitaire externe "readlink" du noyau GNU.
J'ai évité le buildin "echo" et utilisé le buildin "printf" à la place pour deux raisons :
Pour éviter les conversions inutiles, les noms de chemin sont utilisés tels qu'ils sont retournés. et attendus par les utilitaires de l'interpréteur de commandes et du système d'exploitation (par exemple cd, ln, ls, find, mkdir) ; contrairement à "os.path.relpath" de python qui interprète certaines séquences backslash )
À l'exception des séquences de barres obliques inversées mentionnées, la dernière ligne de la fonction "relPath" permet d'obtenir produit 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 la ligne
printf %s "$up${path#"$common"/}"
Je préfère cette dernière parce que
Les noms de fichiers peuvent être directement ajoutés aux chemins de répertoire obtenus par relPath, par exemple :
ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
Les liens symboliques dans le même répertoire créés avec cette méthode n'ont pas de la laideur "./"
ajouté au début du nom du fichier.
Si vous trouvez une erreur, veuillez contacter linuxball (at) gmail.com et j'essaierai de la corriger. 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 (il suffit de l'ajouter au shell script) :
############################################################################
# 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
Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.
8 votes
Par exemple (mon cas actuel) pour donner à gcc un chemin relatif afin qu'il puisse générer des informations de débogage relatives utilisables même si le chemin source change.
2 votes
Une question similaire à celle-ci a été posée sur U&L : unix.stackexchange.com/questions/100918/ . Une des réponses (@Gilles) mentionne un outil, liens symétriques qui permet de résoudre facilement ce problème.
45 votes
Simple :
realpath --relative-to=$absolute $current
.