例:
absolute="/foo/bar"
current="/foo/baz/foo"
# Magic
relative="../../bar"
魔法を作成するにはどうすればよいですか (あまり複雑なコードではないことを願っています...)。
例:
absolute="/foo/bar"
current="/foo/baz/foo"
# Magic
relative="../../bar"
魔法を作成するにはどうすればよいですか (あまり複雑なコードではないことを願っています...)。
GNU coreutils 8.23 の realpath を使用するのが最も簡単だと思います。
$ realpath --relative-to="$file1" "$file2"
例えば:
$ realpath --relative-to=/usr/bin/nmap /tmp/testing
../../../tmp/testing
$ python -c "import os.path; print os.path.relpath('/foo/bar', '/foo/baz/foo')"
与えます:
../../bar
これは、@pini からの現在最も評価の高いソリューションの修正された、完全に機能的な改善です (悲しいことに、少数のケースしか処理しません)。
注意: '-z' は文字列の長さがゼロ (=空) かどうかをテストし、'-n' は文字列が空でないかどうかをテストします。
# 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
テストケース:
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 $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/}
bash、pwd、dirname、echo がインストールされていると仮定します。次に、relpath は
#!/bin/bash
s=$(cd ${1%%/};pwd); d=$(cd $2;pwd); b=; while [ "${d#$s/}" == "${d}" ]
do s=$(dirname $s);b="../${b}"; done; echo ${b}${d#$s/}
私はpiniからの答えと他のいくつかのアイデアをゴルフしました
注: これには、両方のパスが既存のフォルダーである必要があります。ファイルは機能しません。
os.path.relpath
as a shell 関数このrelpath
演習の目標は、 xnios.path.relpath
によって提案されているように、Python 2.7 の機能 (Python バージョン 2.6 から利用可能ですが、2.7 でのみ適切に動作する) を模倣することです。結果として、一部の結果は、他の回答で提供される機能と異なる場合があります。
(ZSH からの呼び出しに基づく検証を破るという理由だけで、パスに改行を使用してテストしていませんpython -c
。多少の努力で確かに可能です。)
Bash の「魔法」に関しては、私はずっと前に Bash で魔法を探すことをあきらめていましたが、その後、必要なすべての魔法を ZSH で見つけ、いくつかは ZSH で見つけました。
したがって、2 つの実装を提案します。
最初の実装は、完全にPOSIX に準拠することを目的としています。/bin/dash
Debian 6.0.6「Squeeze」でテストしまし た。また、OS X 10.8.3 でも完全に動作し/bin/sh
ます。OS X 10.8.3 は、実際には POSIX シェルを装った Bash バージョン 3.2 です。
2 番目の実装は、パス内の複数のスラッシュやその他の迷惑行為に対して堅牢な ZSH シェル関数です。#!/usr/bin/env zsh
ZSH を使用できる場合は、別のシェルから以下に示すスクリプト形式 (つまり、シバン) で呼び出している場合でも、これが推奨されるバージョンです。
最後に、他の回答で提供されているテストケースrelpath
を考慮して、見つかったコマンドの出力を検証する ZSH スクリプトを作成しました。$PATH
いくつかのスペース、タブ、句読点 (あちこちなど) を追加してこれらのテストにスパイスを加え、さらにvim-powerline! ? *
に見られるエキゾチックな UTF-8 文字を使用した別のテストを投入しました。
まず、POSIX 準拠のシェル関数。さまざまなパスで動作しますが、複数のスラッシュを消去したり、シンボリック リンクを解決したりしません。
#!/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 "$@"
さて、より堅牢なzsh
バージョン。realpath -f
引数を実際のパス(Linuxcoreutils
パッケージで利用可能)に解決したい場合は、 :a
3 行目と 4 行目の をに置き換え:A
ます。
これを zsh で使用するには、最初と最後の行を削除して、$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 "$@"
最後にテストスクリプトです。-v
1 つのオプション、つまり冗長出力を有効にするオプションを受け入れます。
#!/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")"; }
上記のシェル スクリプトは、 pini の(Thanks!)に触発されました。Stack Overflow の構文強調表示モジュールでバグが発生します (少なくとも私のプレビュー フレームでは)。そのため、強調表示が正しくない場合は無視してください。
いくつかのメモ:
上記のバックスラッシュ シーケンスを除いて、関数 "relPath" の最後の行は、python と互換性のあるパス名を出力します。
path=$up${path#"$common"/}; path=${path%/}; printf %s "${path:-.}"
最後の行は次の行に置き換える (および簡略化する) ことができます
printf %s "$up${path#"$common"/}"
私は後者を好むので
ファイル名は、relPath によって取得されたディレクトリ パスに直接追加できます。たとえば、次のようになります。
ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
"./"
この方法で作成された同じディレクトリ内のシンボリック リンクには、ファイル名の先頭に醜い が付加されません。
リグレッション テストのコード リスト (シェル スクリプトに追加するだけ):
############################################################################
# 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
このそれほど自明ではないタスクには、Perl を使用します。
absolute="/foo/bar"
current="/foo/baz/foo"
# Perl is magic
relative=$(perl -MFile::Spec -e 'print File::Spec->abs2rel("'$absolute'","'$current'")')
.
このスクリプトは、絶対パスまたはorなしの相対パスである入力に対してのみ正しい結果を返します..
。
#!/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"
kaskuとPini の回答がわずかに改善されました。これにより、スペースでより適切に再生され、相対パスを渡すことができます。
#!/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
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.sh
TEST=/home/ubuntu/.//blah
TMP=/home/ubuntu/blah
REL=blah
私はあなたの質問を「ポータブル」シェルコードでこれを書くための挑戦として受け止めました。
POSIX準拠のシェル(zsh、bash、ksh、ash、busyboxなど)で実行されます。動作を確認するためのテストスイートも含まれています。パス名の正規化は演習として残されています。:-)
#!/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 :
悲しいことに、Mark Rushakoff の回答 (現在は削除されています - コードはhereから参照されています) は、以下に適応すると正しく機能しないようです:
source=/home/part2/part3/part4
target=/work/proj1/proj2
解説で概説されている考え方は、ほとんどの場合に正しく機能するように改良することができます。スクリプトがソース引数 (現在の場所) とターゲット引数 (取得したい場所) を受け取り、両方が絶対パス名であるか、両方が相対パス名であると想定しようとしています。1 つが絶対でもう 1 つが相対である場合、最も簡単な方法は相対名の前に現在の作業ディレクトリを付けることですが、以下のコードではそれを行いません。
以下のコードはほぼ正しく動作していますが、完全には正しくありません。
xyz/./pqr
。xyz/../pqr
。./
自明なことですが、コードは先頭の ' ' をパスから削除しません。Dennis のコードは 1 と 5 を修正するため、より優れていますが、2、3、4 も同じ問題があります。そのため、Dennis のコードを使用してください (そして、これより前に賛成票を投じてください)。
(注: POSIX はrealpath()
パス名を解決するシステム コールを提供しているため、パス名にシンボリック リンクが残らないようになっています。それを入力名に適用し、Dennis のコードを使用すると、毎回正しい答えが得られます。C コードを書くのは簡単です。ラップrealpath()
- 私はそれをしました - しかし、私はそうする標準的なユーティリティを知りません.)
これについては、シェルよりも Perl の方が使いやすいと思いますが、bash は配列を適切にサポートしており、おそらくこれも行うことができます - 読者の演習です。したがって、2 つの互換性のある名前が与えられた場合、それぞれをコンポーネントに分割します。
したがって:
#!/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";
テスト スクリプト (角括弧には空白とタブが含まれます):
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
テスト スクリプトからの出力:
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
この Perl スクリプトは Unix 上でかなり完全に機能します (Windows のパス名のすべての複雑さは考慮されていません)。モジュールCwd
とその関数realpath
を使用して、存在する名前の実際のパスを解決し、存在しないパスのテキスト分析を行います。1 つを除くすべてのケースで、Dennis のスクリプトと同じ出力が生成されます。逸脱したケースは次のとおりです。
source = home/part1/part2
target = .
relpath1 = ../../..
relpath2 = ../../../.
2 つの結果は同等ですが、同一ではありません。(出力は、テスト スクリプトのわずかに変更されたバージョンからのものです。以下の Perl スクリプトは、上記のスクリプトのように入力と回答ではなく、単純に回答を出力します。) さて: 機能しない回答を削除する必要がありますか? 多分...
#!/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);
私の解決策:
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
}
これもトリックを行うと思います...(組み込みテストが付属しています):)
OK、ある程度のオーバーヘッドが予想されますが、ここではBourneシェルを実行しています。;)
#!/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
このスクリプトは、パス名に対してのみ機能します。ファイルが存在する必要はありません。渡されたパスが絶対パスでない場合、動作は少し異常ですが、両方のパスが相対パスであれば期待どおりに動作するはずです。
OS X でのみテストしたので、移植性がない可能性があります。
#!/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
この回答は質問の Bash 部分には対応していませんが、この質問の回答を使用してEmacsにこの機能を実装しようとしたため、そこに投げます。
Emacsには、実際にこれをすぐに使用できる機能があります。
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"
これは、他のプログラムを呼び出さずに実行するシェル スクリプトです。
#! /bin/env bash
#bash script to find the relative path between two directories
mydir=${0%/}
mydir=${0%/*}
creadlink="$mydir/creadlink"
shopt -s extglob
relpath_ () {
path1=$("$creadlink" "$1")
path2=$("$creadlink" "$2")
orig1=$path1
path1=${path1%/}/
path2=${path2%/}/
while :; do
if test ! "$path1"; then
break
fi
part1=${path2#$path1}
if test "${part1#/}" = "$part1"; then
path1=${path1%/*}
continue
fi
if test "${path2#$path1}" = "$path2"; then
path1=${path1%/*}
continue
fi
break
done
part1=$path1
path1=${orig1#$part1}
depth=${path1//+([^\/])/..}
path1=${path2#$path1}
path1=${depth}${path2#$part1}
path1=${path1##+(\/)}
path1=${path1%/}
if test ! "$path1"; then
path1=.
fi
printf "$path1"
}
relpath_test () {
res=$(relpath_ /path1/to/dir1 /path1/to/dir2 )
expected='../dir2'
test_results "$res" "$expected"
res=$(relpath_ / /path1/to/dir2 )
expected='path1/to/dir2'
test_results "$res" "$expected"
res=$(relpath_ /path1/to/dir2 / )
expected='../../..'
test_results "$res" "$expected"
res=$(relpath_ / / )
expected='.'
test_results "$res" "$expected"
res=$(relpath_ /path/to/dir2/dir3 /path/to/dir1/dir4/dir4a )
expected='../../dir1/dir4/dir4a'
test_results "$res" "$expected"
res=$(relpath_ /path/to/dir1/dir4/dir4a /path/to/dir2/dir3 )
expected='../../../dir2/dir3'
test_results "$res" "$expected"
#res=$(relpath_ . /path/to/dir2/dir3 )
#expected='../../../dir2/dir3'
#test_results "$res" "$expected"
}
test_results () {
if test ! "$1" = "$2"; then
printf 'failed!\nresult:\nX%sX\nexpected:\nX%sX\n\n' "$@"
fi
}
#relpath_test