Convertissez le chemin absolu en chemin relatif à partir D'un répertoire courant en utilisant Bash
exemple:
absolute="/foo/bar"
current="/foo/baz/foo"
# Magic
relative="../../bar"
comment créer le code magique (avec un peu de chance, pas trop compliqué...)?
23 réponses
$ python -c "import os.path; print os.path.relpath('/foo/bar', '/foo/baz/foo')"
donne:
../../bar
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
il s'agit d'une amélioration corrigée, entièrement fonctionnelle de la solution actuellement mieux notée de @pini (qui malheureusement ne traitent que quelques cas)
rappel: le test ' - z 'si la chaîne est de longueur zéro (=vide) et le test'-n ' si la chaîne est et non vide.
# both and are absolute paths beginning with /
# returns relative path to /$target from /$source
source=
target=
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 D'essai:
compute_relative.sh "/A/B/C" "/A" --> "../.."
compute_relative.sh "/A/B/C" "/A/B" --> ".."
compute_relative.sh "/A/B/C" "/A/B/C" --> ""
compute_relative.sh "/A/B/C" "/A/B/C/D" --> "D"
compute_relative.sh "/A/B/C" "/A/B/C/D/E" --> "D/E"
compute_relative.sh "/A/B/C" "/A/B/D" --> "../D"
compute_relative.sh "/A/B/C" "/A/B/D/E" --> "../D/E"
compute_relative.sh "/A/B/C" "/A/D" --> "../../D"
compute_relative.sh "/A/B/C" "/A/D/E" --> "../../D/E"
compute_relative.sh "/A/B/C" "/D/E/F" --> "../../../D/E/F"
#!/bin/bash
# both and are absolute paths
# returns relative to
source=
target=
common_part=$source
back=
while [ "${target#$common_part}" = "${target}" ]; do
common_part=$(dirname $common_part)
back="../${back}"
done
echo ${back}${target#$common_part/}
il est intégré à Perl depuis 2001, donc il fonctionne 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
Aussi, la solution est facile à comprendre.
donc pour votre exemple:
perl -e 'use File::Spec; print File::Spec->abs2rel(@ARGV) . "\n"' $absolute $current
...marcherait bien.
Python os.path.relpath
comme une fonction shell
le but de cet exercice relpath
est d'imiter la fonction os.path.relpath
de Python 2.7 (disponible à partir de la version 2.6 de Python mais ne fonctionne correctement qu'en 2.7), comme proposé par xni . En conséquence, certains résultats peuvent différer de fonctions prévues dans d'autres réponses.
(Je n'ai pas testé avec newlines dans paths simplement parce qu'il casse la validation basé sur l'appel python -c
de ZSH. Ce serait certainement possible avec un certain effort.)
en ce qui concerne la" magie " à Bash, j'ai abandonné la recherche de la magie à Bash il y a longtemps, mais j'ai depuis trouvé toute la magie dont j'ai besoin, et puis certains, en ZSH.
par conséquent, je propose deux implémentations.
la première mise en œuvre vise à être entièrement conforme POSIX . Je l'ai testé avec /bin/dash
Debian 6.0.6 "Squeeze". Il fonctionne également 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 seconde implémentation est une fonction shell ZSH robuste contre les coupures multiples 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 script présenté ci-dessous (i.e. 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
étant donné les cas de test fournis dans d'autres réponses. J'ai ajouté un peu de piquant à ces tests en ajoutant des espaces, des onglets et des signes de ponctuation comme ! ? *
ici et là et j'ai aussi 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 conforme à POSIX. Il fonctionne avec une variété de chemins, mais ne nettoie pas plusieurs slashes ou résoudre les liens symboliques.
#!/bin/sh
relpath () {
[ $# -ge 1 ] && [ $# -le 2 ] || return 1
current="${2:+""}"
target="${2:-""}"
[ "$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 "$@"
ZSH fonction shell
maintenant, la version plus robuste zsh
. Si vous souhaitez résoudre les arguments de Real paths à la realpath -f
(disponible dans le paquet Linux coreutils
), remplacez le :a
sur les lignes 3 et 4 par :A
.
pour utiliser ceci en zsh, supprimez la première et la dernière ligne et mettez-la dans un répertoire qui est dans votre variable $FPATH
.
#!/usr/bin/env zsh
relpath () {
[[ $# -ge 1 ]] && [[ $# -le 2 ]] || return 1
local target=${${2:-}:a} # replace `:a' by `:A` to resolve symlinks
local current=${${${2:+}:-$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 verbeuse.
#!/usr/bin/env zsh
set -eu
VERBOSE=false
script_name=$(basename "151920920")
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:-}}
prefix=${${${2:+}:-$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 ]] && [[ = "-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 to canonical
# absolute dir path ( and/or 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 to dir (Does not impose any
# restrictions on and 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 "")" "$(readlink -m "")"; }
ci-dessus shell script a été inspiré par pini (Merci!). Il déclenche un bug dans le module de mise en surbrillance syntaxique de Stack Overflow (au moins dans mon preview cadre.) Veuillez donc ignorer 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é
- mettre la fonctionnalité dans les fonctions pour facilité d'utilisation
- maintenaient les fonctions POSIX compatibles de sorte qu'elles (devraient) fonctionner avec toutes les fonctions POSIX shells (testé avec dash, bash, et zsh dans Ubuntu Linux 12.04)
- a utilisé des variables locales uniquement pour éviter de faire claquer des variables globales et polluer l'Espace nom mondial
- les deux chemins de répertoire N'ont pas besoin d'exister (exigence pour ma demande)
- les noms de chemin peuvent contenir des espaces, des caractères spéciaux, le contrôle caractère, les barres obliques inverses, les onglets, ', ", ?, *, [,], etc.
- Core function "relPath" utilise les builtins de shell POSIX seulement mais nécessite canonique absolu chemins de répertoire en tant que paramètres
- fonction étendue "relpath" peut gérer des chemins de répertoires arbitraires (aussi relatif, non canonique) mais nécessite un utilitaire externe GNU core "readlink "
- a évité "echo" et utilisé "printf" à la place pour deux raisons:
- en raison de la mise en œuvre historique conflictuelle de l'architecture "echo" it se comporte différemment dans des shells différents - > POSIX recommande que printf est préférable à echo .
- Builtin "écho" de certains shells POSIX interprete some backslash séquences et donc noms de chemins corrompus contenant de telles séquences
- pour éviter les conversions inutiles, les noms de chemins sont utilisés comme ils sont retournés et attendu par les utilitaires shell et OS (par exemple cd, ln, ls, find, mkdir); contrairement au " os " de python.chemin.relpath " qui interprétera un certain backslash sequences)
-
à L'exception des séquences de backslash mentionnées la dernière ligne de fonction "relPath" sorties pathnames 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 ce dernier parce que
-
les noms de fichiers peuvent être directement annexés à des chemins dir obtenus par relPath, par exemple:
ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
-
les liens symboliques dans le même dir créés avec cette méthode n'ont pas le laid
"./"
est ajouté au nom du fichier.
-
- si vous trouvez une erreur, veuillez contacter linuxball (at) gmail.com et j'essaierai remédier.
- Added regression test suite (also POSIX shell compatible) ""
exemple de Code pour les tests de régression (il suffit d'ajouter le script shell):
############################################################################
# If called with 2 arguments assume they are dir paths and print rel. path #
############################################################################
test "$#" = 2 && {
printf '%s\n' "Rel. path from '' to '' is '$(relpath "" "")'."
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 in '#'*|'') continue;; esac # Skip comments and empty lines
# q stores quoting character, use " if ' is used in path name
q="'"; case in *"'"*) q='"';; esac
rPOk=passed rP=$(relPath "" ""); test "$rP" = "" || rPOk=$rP
rpOk=passed rp=$(relpath "" ""); test "$rp" = "" || rpOk=$rp
RPOk=passed
RP=$(python -c "import os.path; print os.path.relpath($q$q, $q$q)")
test "$RP" = "" || RPOk=$RP
printf \
"$format" "$q$q" "$q$q" "$q$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 in '#'*|'') continue;; esac # Skip comments and empty lines
# q stores quoting character, use " if ' is used in path name
q="'"; case in *"'"*) q='"';; esac
rpOk=passed rp=$(relpath "" ""); test "$rp" = "" || rpOk=$rp
RPOk=passed
RP=$(python -c "import os.path; print os.path.relpath($q$q, $q$q)")
test "$RP" = "" || RPOk=$RP
printf "$format" "$q$q" "$q$q" "$q$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
présumant que vous avez installé: bash, pwd, dirname, echo; puis relpath est
#!/bin/bash
s=$(cd ${1%%/};pwd); d=$(cd ;pwd); while [ "${d#$s/}" == "${d}" ]
do s=$(dirname $s);b="../${b}"; done; echo ${b}${d#$s/}
j'ai ont joué au golf la réponse de pini et de quelques autres idées
ce script donne des résultats corrects seulement pour les entrées qui sont des chemins absolus ou des chemins relatifs sans .
ou ..
:
#!/bin/bash
# usage: relpath from to
if [[ "" == "" ]]
then
echo "."
exit
fi
IFS="/"
current=()
absolute=()
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"
une légère amélioration sur kasku et Pini réponses, qui joue plus agréable avec les espaces et permet de passer des chemins relatifs:
#!/bin/bash
# both and are paths
# returns relative to
absolute=`readlink -f ""`
current=`readlink -f ""`
# 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
peu de réponses ici sont pratiques pour une utilisation quotidienne. Comme il est très difficile de le faire correctement en pur bash, je suggère la solution suivante, fiable (semblable à 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 basé sur le répertoire courant:
echo $(relpath somepath)
ou vous pouvez spécifier que le chemin est relatif à un répertoire donné:
echo $(relpath somepath /etc) # relative to /etc
le un inconvénient est que cela nécessite python, mais:
- il fonctionne de manière identique dans n'importe quel python > = 2.6
- il n'est pas nécessaire que les fichiers ou répertoires existent. Les noms de fichiers
- peuvent contenir un plus grand nombre de caractères spéciaux. Par exemple, beaucoup d'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.
noter que les solutions qui comprennent basename
ou dirname
ne sont pas nécessairement meilleures, car elles exigent que coreutils
soit installé. Si quelqu'un a une solution pure bash
qui est fiable et simple (plutôt qu'une curiosité alambiquée), je serais surpris.
malheureusement, la réponse de Mark Rushakoff (maintenant supprimé-il Référencé le code de ici ) ne semble pas fonctionner correctement lorsqu'adapté à:
source=/home/part2/part3/part4
target=/work/proj1/proj2
la pensée décrite dans le commentaire peut être affinée pour qu'elle fonctionne 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 aller), et que soit les deux sont des noms de chemin absolus, soit les deux sont Relatif. Si l'un est absolu et l'autre relatif, la chose la plus facile est de préfixer le nom relatif avec le répertoire de travail courant - mais le code ci-dessous ne fait pas cela.
attention
le code ci-dessous est proche de fonctionner correctement, mais n'est pas tout à fait correct.
- le problème est abordé dans les commentaires de Dennis Williamson.
- Il ya aussi un problème que ce traitement purement textuel de noms de chemins et vous pouvez être sérieusement perturbé par des liens symboliques bizarres.
- le code ne gère pas les 'points' errants dans les chemins comme '
xyz/./pqr
". - le code ne gère pas les 'doubles points' errants dans les chemins comme '
xyz/../pqr
". - trivialement: le code ne supprime pas la mention
./
" des chemins.
le code de Dennis est meilleur car il fixe 1 et 5, mais a la même questions 2, 3, 4. Utilisez le code de Dennis (et votez-le avant cela) pour cette raison.
(NB: POSIX fournit un appel système realpath()
qui résout les noms de chemin 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 enveloppe 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. Ainsi, étant donné deux noms compatibles, les diviser chacun en composants:
- définit le chemin relatif à vide.
- Alors que les composants sont les mêmes, passez à la suivante.
- Lorsque les composants correspondants sont différents ou il n'y a plus de composants pour un chemin:
- s'il ne reste aucun composant source et que le chemin relatif est vide, ajouter"."au début.
- pour chaque composant source restant, préfixer le chemin relatif avec"../".
- s'il ne reste aucun composant cible et que le chemin relatif est vide, ajouter"."au début.
- Pour chaque composant cible, ajouter le composant à la fin de le chemin après un slash.
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 vide 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 tient pas compte de toutes les complexités des noms de chemins Windows) face à des entrées bizarres. Il utilise le module Cwd
et sa fonction realpath
pour résoudre le chemin réel 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 est 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 le travail non-réponse? Peut-être...
#!/bin/perl -w
# Based loosely on code from: http://unix.derkeiler.com/Newsgroups/comp.unix.shell/2005-10/1256.html
# Via: /q/convert-absolute-path-into-relative-path-given-a-current-directory-using-bash-20104/"Usage: "151950920" 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%/\.\./(?:[^/]+)(/|$)%%g;
$path =~ s%/\.$%%; # No trailing /.
$path =~ s%^\./%%; # No leading ./
# What happens with . and / as inputs?
}
return($path);
}
sub print_result
{
my($source, $target, $relpath) = @_;
if ($verbose)
{
print "source = $ARGV[0]\n";
print "target = $ARGV[1]\n";
print "relpath = $relpath\n";
}
else
{
print "$relpath\n";
}
exit 0;
}
my($source) = resolve($ARGV[0]);
my($target) = resolve($ARGV[1]);
print_result($source, $target, ".") if ($source eq $target);
# Split!
my(@source) = split '/', $source;
my(@target) = split '/', $target;
my $count = scalar(@source);
$count = scalar(@target) if (scalar(@target) < $count);
my $relpath = "";
my $i;
# Both paths are absolute; Perl splits an empty field 0.
for ($i = 1; $i < $count; $i++)
{
last if $source[$i] ne $target[$i];
}
for (my $s = $i; $s < scalar(@source); $s++)
{
$relpath = "$relpath/" if ($s > $i);
$relpath = "$relpath..";
}
for (my $t = $i; $t < scalar(@target); $t++)
{
$relpath = "$relpath/" if ($relpath ne "");
$relpath = "$relpath$target[$t]";
}
print_result($source, $target, $relpath);
test.sh:
#!/bin/bash
cd /home/ubuntu
touch blah
TEST=/home/ubuntu/.//blah
echo TEST=$TEST
TMP=$(readlink -e "$TEST")
echo TMP=$TMP
REL=${TMP#$(pwd)/}
echo REL=$REL
test:
$ ./test.sh
TEST=/home/ubuntu/.//blah
TMP=/home/ubuntu/blah
REL=blah
j'ai pris votre question comme un défi pour écrire ceci en code shell "portable", i.e.
- avec un shell POSIX dans l'esprit
- pas de bashisms comme des tableaux
- évitez d'appeler les externes comme la peste. Il n'y a pas une seule fourchette dans le script! Cela le rend incroyablement rapide, surtout sur les systèmes avec fourche importante au-dessus, comme cygwin.
- Doivent composer avec glob caractères dans les noms de fichiers (*, ?, [,])
il fonctionne sur n'importe quelle coque conforme POSIX (zsh, bash, ksh, ash, busybox, ...). Il contient même une suite de tests pour vérifier son fonctionnement. La canonisation de pathnames est laissée comme un 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 ; do
result="$result../"
done
result="${result%/}"
IFS="$OLDIFS"
}
# Call with FROM TO args.
relativepath () {
case "" in
(*//*|*/./*|*/../*|*?/|*/.|*/..)
printf '%s\n' "'' not canonical"; exit 1;;
(/*)
from="${1#?}";;
(*)
printf '%s\n' "'' not absolute"; exit 1;;
esac
case "" in
(*//*|*/./*|*/../*|*?/|*/.|*/..)
printf '%s\n' "'' not canonical"; exit 1;;
(/*)
to="${2#?}";;
(*)
printf '%s\n' "'' 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 :
Ma Solution:
computeRelativePath()
{
Source=$(readlink -f )
Target=$(readlink -f )
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
}
voici ma version. Il est basé sur la réponse par @Offirmo . Je l'ai rendu compatible avec le tableau de bord et j'ai corrigé la défaillance suivante du testcase:
./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 and are absolute paths beginning with /
# returns relative path to /$target from /$source
CT_FindRelativePath()
{
local insource=
local intarget=
# 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("151900920", 1, length("151900920")-1); print string; }' ) ;;
esac
fi
echo $result
return 0
}
je suppose que celui-ci fera l'affaire aussi... (livré avec des tests intégrés) :)
OK, un peu de frais généraux attendus, mais nous faisons Bourne shell ici! ;)
#!/bin/sh
#
# Finding the relative path to a certain file (), given the absolute path ()
# (available here too http://pastebin.com/tWWqA8aB)
#
relpath () {
local FROM=""
local TO="`dirname `"
local FILE="`basename `"
local DEBUG=""
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 "
echo " from "
echo " ------"
echo " => `relpath ' '`"
echo ""
}
# If given 2 arguments, do as said...
if [ -n "" ]; then
relpath
# If only one given, then assume current directory
elif [ -n "" ]; then
relpath `pwd`
# 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
ce script ne fonctionne que sur les noms de chemins. Il ne nécessite pas de tous les fichiers pour exister. Si les chemins d'accès transmis 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 il se peut qu'il ne soit pas portable.
#!/bin/bash
set -e
declare SCRIPT_NAME="$(basename "151900920")"
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=
declare target=
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
cette réponse ne répond pas à la partie Bash de la question, mais parce que j'ai essayé d'utiliser les réponses dans cette question pour mettre en œuvre cette fonctionnalité dans Emacs je vais le jeter là-bas.
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"
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" "")
path2=$("$creadlink" "")
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 ! "" = ""; then
printf 'failed!\nresult:\nX%sX\nexpected:\nX%sX\n\n' "$@"
fi
}
#relpath_test
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 A-P pour ça. Un fragment de mon script est ajouté. C'est dans une fonction dans un script shell, donc les $1 et $2. La valeur de résultat, qui est le chemin relatif de START_ABS à END_ABS, est dans la variable UPDIRS. Le cd de script se trouve dans chaque répertoire de paramètres afin d'exécuter le pwd-P et cela signifie également que les paramètres relatifs de chemin sont manipulés. Santé, Jim
SAVE_DIR="$PWD"
cd ""
START_ABS=`pwd -P`
cd "$SAVE_DIR"
cd ""
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"
encore une autre solution, pure bash
+ GNU readlink
pour une utilisation facile dans le contexte suivant:
ln -s "$(relpath "$A" "$B")" "$B"
Edit: assurez-vous que "$B" soit n'existe pas ou n'softlink dans ce cas, sinon
relpath
suit ce lien, 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 readlink -f
à la place. Voir aussi 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 -- "")" || return
Y="$(readlink -m -- "")" || return
X="${X%/}/"
A=""
while Y="${Y%/*}"
[ ".${X#"$Y"/}" = ".$X" ]
do
A="../$A"
done
X="$A${X#"$Y"/}"
X="${X%/}"
echo "${X:-.}"
}
Notes:
- on a pris soin qu'il ne soit pas dangereux contre l'extension de méta-caractères non désirée de l'interpréteur de commandes, dans le cas où les noms de fichiers contiennent
*
ou?
. - la sortie est destinée à être utilisable comme premier argument à
ln -s
:-
relpath / /
donne.
et non la chaîne vide -
relpath a a
donnea
, même sia
se trouve être un répertoire
-
- la plupart des cas communs ont été testés pour donner des résultats raisonnables, aussi.
- cette solution utilise le préfixe de chaîne correspondant, donc
readlink
est nécessaire pour canoniser les chemins. - grâce à
readlink -m
il fonctionne pour les chemins non encore existants, aussi.
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 de quelques contournements comme celui-ci (non testé!):
readlink_missing()
{
readlink -m -- "" && return
readlink -f -- "" && return
[ -e . ] && echo "$(readlink_missing "$(dirname "")")/$(basename "")"
}
ce n'est pas vraiment tout à fait correct dans le cas comprend
.
ou ..
pour les chemins non existants (comme dans /doesnotexist/./a
), mais il devrait couvrir la plupart des cas.
(remplacer readlink -m --
ci-dessus par readlink_missing
.)
modifier en raison de la note descendante suit
voici un test, que cette fonction, en effet, est correcte:
check()
{
res="$(relpath "" "")"
[ ".$res" = "." ] && return
printf ':WRONG: %-10q %-10q gives %q\nCORRECT %-10q %-10q gives %q\n' "" "" "$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 qu'il 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, ../bar
est la trajectoire relative exacte et la seule correcte de la la page bar
vu à partir de la page moo
. Tout le reste serait simplement faux.
il est trivial d'adopter la sortie à la question qui suppose apparemment ,que current
est un répertoire:
absolute="/foo/bar"
current="/foo/baz/foo"
relative="../$(relpath "$absolute" "$current")"
cela revient exactement, ce qui a été demandé.
et avant de lever un sourcil, voici une variante un peu plus complexe de relpath
(spot la petite différence), qui devrait fonctionner pour la syntaxe URL-, aussi (ainsi, un /
survit, grâce à quelque 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 -- "")" || return
Y="$(readlink -m -- "")" || 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 juste pour faire clair: Il fonctionne vraiment comme dit.
check()
{
res="$(relpathurl "" "")"
[ ".$res" = "." ] && return
printf ':WRONG: %-10q %-10q gives %q\nCORRECT %-10q %-10q gives %q\n' "" "" "$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 ceci peut être utilisé pour donner le résultat voulu 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, merci de me le faire savoir dans les commentaires ci-dessous. Grâce.
PS:
Pourquoi les arguments de relpath
"inversé", contrairement à toutes les autres réponses ici?
si vous changez
Y="$(readlink -m -- "")" || return
à
Y="$(readlink -m -- "${2:-"$PWD"}")" || return
alors vous pouvez laisser le 2e paramètre loin, de sorte que la BASE est le répertoire courant/URL/n'importe quoi. Ce n'est que le principe Unix, comme d'habitude.
si vous n'aimez pas ça, retournez à Windows. Grâce.