IT story

Bash를 사용하여 현재 디렉토리에서 절대 경로를 상대 경로로 변환

hot-time 2020. 4. 14. 19:14
반응형

Bash를 사용하여 현재 디렉토리에서 절대 경로를 상대 경로로 변환


예:

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의 현재 최고 등급의 솔루션을 수정하여 완전히 기능적으로 개선 한 것입니다 (슬프게도 소수의 경우 만 처리).

알림 : 문자열이 길이가 0 인 경우 '-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/}

2001 년 부터 Perl에 내장되어 있으므로 VMS 까지 상상할 수있는 거의 모든 시스템에서 작동합니다 .

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

또한 솔루션은 이해하기 쉽습니다.

예를 들어,

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

... 잘 작동합니다.


os.path.relpath쉘 함수로서의 파이썬

relpath연습 의 목표는 xni가os.path.relpath 제안한대로 Python 2.7의 기능 (Python 버전 2.6에서 사용 가능하지만 2.7에서만 제대로 작동 함)을 모방하는 것 입니다. 결과적으로 일부 결과는 다른 답변에서 제공되는 기능과 다를 수 있습니다.

( python -cZSH의 호출 기반으로 유효성 검사를 중단하기 때문에 단순히 경로에서 줄 바꿈으로 테스트하지 않았습니다 . 확실히 노력하면 가능합니다.)

Bash의 "매직"과 관련하여 나는 오래 전에 Bash에서 마법을 찾는 것을 포기했지만 이후 ZSH에서 필요한 모든 마법을 찾았습니다.

결과적으로 두 가지 구현을 제안합니다.

첫 번째 구현은 POSIX를 완전히 준수하는 것 입니다. /bin/dashDebian 6.0.6“Squeeze”에서 테스트했습니다 . 또한 /bin/shOS X 10.8.3 에서도 완벽하게 작동합니다. 실제로 Bash 버전 3.2는 POSIX 쉘인 것처럼 보입니다.

두 번째 구현은 경로의 여러 슬래시 및 기타 방해 요소에 대비 한 ZSH 셸 함수입니다. ZSH를 사용할 수있는 경우 #!/usr/bin/env zsh다른 쉘에서 아래에 제시된 스크립트 형식 (예 : shebang ) 으로 호출하더라도 권장 버전 입니다.

마지막으로, 다른 답변에서 제공된 테스트 사례 relpath에서 찾은 명령 의 출력을 확인하는 ZSH 스크립트를 작성했습니다 $PATH. ! ? *여기와 거기에 공백과 탭, 문장 부호를 추가하여 테스트에 향신료를 더했으며 vim-powerline 에서 이국적인 UTF-8 문자를 사용하여 또 다른 테스트를 던졌습니다 .

POSIX 쉘 기능

먼저 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 쉘 기능

이제 더 강력한 zsh버전입니다. 실제 경로 (la)에 대한 인수를 해결하려면 realpath -f(Linux coreutils패키지 에서 사용 가능 ) :a3 행과 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상세 출력을 가능하게합니다.

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

설치된 것으로 가정 : bash, pwd, dirname, echo; relpath는

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

나는 pini 와 다른 몇 가지 아이디어 에서 답을 얻었습니다.


#!/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 's (Thanks!) 에서 영감을 받았습니다 . 스택 오버플로의 구문 강조 모듈 (적어도 내 미리보기 프레임)에서 버그가 발생합니다. 강조 표시가 잘못된 경우 무시하십시오.

몇 가지 참고 사항 :

  • 코드 길이와 복잡성을 크게 증가시키지 않으면 서 오류 제거 및 코드 개선
  • 사용 편의성을 위해 기능을 기능에 추가
  • Kept는 POSIX와 호환되므로 모든 POSIX 셸에서 작동해야합니다 (Ubuntu Linux 12.04에서 대시, bash 및 zsh로 테스트).
  • 전역 변수를 클로버하고 전역 네임 스페이스를 오염시키지 않도록 로컬 변수 만 사용
  • 두 디렉토리 경로 모두 존재할 필요는 없습니다 (응용 프로그램 요구 사항).
  • 경로 이름에는 공백, 특수 문자, 제어 문자, 백 슬래시, 탭, ', ",?, *, [,] 등이 포함될 수 있습니다.
  • 핵심 기능 "relPath"는 POSIX 셸 내장만을 사용하지만 표준 절대 디렉토리 경로를 매개 변수로 필요로합니다.
  • 확장 함수 "relpath"는 임의의 디렉토리 경로 (상대적이 아닌 비정규)를 처리 할 수 ​​있지만 외부 GNU 핵심 유틸리티 인 "readlink"가 필요합니다.
  • 내장 "echo"를 피하고 대신 두 가지 이유로 내장 "printf"를 사용했습니다.
  • 불필요한 변환을 피하기 위해 경로 이름은 쉘 및 OS 유틸리티에 의해 리턴되고 예상되는대로 사용됩니다 (예 : cd, ln, ls, find, mkdir; 일부 백 슬래시 시퀀스를 해석하는 Python의 "os.path.relpath"와 달리)
  • 언급 된 백 슬래시 시퀀스를 제외하고 함수 "relPath"의 마지막 행은 파이썬과 호환되는 경로 이름을 출력합니다.

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

    마지막 줄은 한 줄씩 바꾸고 단순화 할 수 있습니다

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

    나는 후자를 선호하기 때문에

    1. 파일 이름은 relPath에 의해 얻어진 dir 경로에 직접 추가 될 수 있습니다 :

      ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
      
    2. 이 방법으로 만든 동일한 디렉토리의 심볼릭 링크 "./"에는 파일 이름 앞에 못생긴 것이 없습니다 .

  • 당신이 오류를 발견하면 linuxball () gmail.com에 연락하여 수정하려고합니다.
  • 회귀 테스트 스위트 추가 (POSIX 셸 호환 가능)

회귀 테스트를위한 코드 목록 (쉘 스크립트에 간단히 추가) :

############################################################################
# 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'")')

이 스크립트는 또는없이 .또는 절대 경로 또는 상대 경로 인 입력에 대해서만 올바른 결과를 제공합니다 ...

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

kaskuPini의 답변이 약간 개선 되어 공백으로 더 잘 재생되고 상대 경로를 통과 할 수 있습니다.

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

여기에 많은 대답이 매일 사용하기에 실용적이지는 않습니다. 순수한 bash 에서이 작업을 올바르게 수행하는 것은 매우 어렵 기 때문에 다음과 같은 신뢰할 수있는 솔루션을 제안합니다 (의견에 묻힌 하나의 제안과 유사).

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

그런 다음 현재 디렉토리를 기반으로 상대 경로를 얻을 수 있습니다.

echo $(relpath somepath)

또는 주어진 디렉토리에 상대적인 경로를 지정할 수 있습니다 :

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

한 가지 단점은 파이썬이 필요하지만 다음과 같습니다.

  • 파이썬> = 2.6에서 동일하게 작동합니다.
  • 파일이나 디렉토리가 존재하지 않아도됩니다.
  • 파일 이름에는 더 넓은 범위의 특수 문자가 포함될 수 있습니다. 예를 들어 파일 이름에 공백이나 다른 특수 문자가 포함되어 있으면 다른 많은 솔루션이 작동하지 않습니다.
  • 스크립트를 어지럽히 지 않는 한 줄 함수입니다.

설치가 필요하기 때문에 반드시 포함 basename되거나 dirname더 나은 솔루션 은 아닐 수 있습니다 coreutils. 누군가가 bash(심지어 호기심이 아닌) 신뢰할 수 있고 간단한 순수한 솔루션을 가지고 있다면 놀랄 것입니다.


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

슬프게도 Mark Rushakoff의 답변 (현재 삭제되었습니다- 여기 에서 코드를 참조했습니다 )은 다음에 적응했을 때 올바르게 작동하지 않는 것 같습니다.

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

해설에 요약 된 생각은 대부분의 경우 올바르게 작동하도록 다듬을 수 있습니다. 스크립트가 소스 인수 (현재 위치)와 대상 인수 (가려는 위치)를 취하고 둘 다 절대 경로 이름이거나 둘 다 상대적이라고 가정합니다. 하나가 절대적이고 다른 것이 상대라면, 가장 쉬운 것은 상대 이름 앞에 현재 작업 디렉토리를 붙이는 것입니다. 그러나 아래 코드는 그렇게하지 않습니다.


조심

아래 코드는 올바르게 작동하지만 거의 맞지 않습니다.

  1. Dennis Williamson의 의견에 언급 된 문제가 있습니다.
  2. 경로 이름을 순전히 텍스트로 처리하면 이상한 심볼릭 링크가 심각하게 망칠 수 있다는 문제도 있습니다.
  3. 이 코드는 ' xyz/./pqr' 와 같은 경로에서 길 잃은 '점'을 처리하지 않습니다 .
  4. 이 코드는 ' xyz/../pqr' 와 같은 경로에서 길잃은 '더블 도트'를 처리하지 않습니다 .
  5. 분명히 : 코드는 ./경로에서 선행 ' '을 제거하지 않습니다 .

Dennis의 코드는 1과 5를 수정하기 때문에 더 낫지 만 2, 3, 4와 같은 문제가 있습니다. Dennis의 코드를 사용하여 미리 투표하십시오.

(NB : POSIX는 realpath()경로 이름을 확인하여 심볼릭 링크가 남아 있지 않은 시스템 호출 제공 합니다. 입력 이름에 적용한 다음 Dennis의 코드를 사용하면 매번 정답이 제공됩니다. C 코드를 작성하는 것은 쉽지 않습니다. 랩 realpath()-나는 그것을했다-하지만 그렇게하는 표준 유틸리티를 모른다.)


이를 위해 bash는 쉘보다 사용하기가 쉽지만 bash는 배열을 적절하게 지원하고 독자도 연습 할 수 있습니다. 따라서 두 개의 호환 가능한 이름이 주어지면 각각을 구성 요소로 나눕니다.

  • 상대 경로를 비워 두십시오.
  • 구성 요소는 동일하지만 다음으로 건너 뜁니다.
  • 해당 구성 요소가 다르거 나 한 경로에 대한 구성 요소가 더 이상없는 경우 :
  • 남아있는 소스 구성 요소가없고 상대 경로가 비어 있으면 "."를 추가하십시오. 처음에.
  • 나머지 소스 구성 요소마다 상대 경로 앞에 "../"를 붙입니다.
  • 남아있는 대상 구성 요소가없고 상대 경로가 비어 있으면 "."를 추가하십시오. 처음에.
  • 나머지 대상 구성 요소마다 슬래시 후 경로 끝에 구성 요소를 추가하십시오.

그러므로:

#!/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사용하여 존재하는 실제 이름 경로를 확인하고 존재하지 않는 경로에 대한 텍스트 분석을 수행합니다. 하나를 제외한 모든 경우에 Dennis의 스크립트와 동일한 출력을 생성합니다. 이례적인 경우는 다음과 같습니다.

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

두 결과는 동일하지만 동일하지 않습니다. (출력은 약간 수정 된 테스트 스크립트 버전에서 나온 것입니다. 아래 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);

나는 "휴대용"쉘 코드로 이것을 작성하기위한 도전으로 당신의 질문을했습니다.

  • POSIX 쉘을 염두에두고
  • 배열과 같은 bashisms 없음
  • 전염병과 같은 외부인을 부르지 마십시오. 스크립트에는 단일 포크가 없습니다! 이는 특히 cygwin과 같이 포크 오버 헤드가 큰 시스템에서 엄청나게 빠릅니다.
  • 경로 이름 (*,?, [,])에서 glob 문자를 처리해야합니다.

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 :

내 솔루션 :

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

}

여기 내 버전이 있습니다. @Offirmo답변기반으로합니다 . 대시 호환으로 만들고 다음 테스트 케이스 실패를 수정했습니다.

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

지금:

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

코드를 참조하십시오 :

# both $1 and $2 are absolute paths beginning with /
# returns relative path to $2/$target from $1/$source
CT_FindRelativePath()
{
    local insource=$1
    local intarget=$2

    # Ensure both source and target end with /
    # This simplifies the inner loop.
    #echo "insource : \"$insource\""
    #echo "intarget : \"$intarget\""
    case "$insource" in
        */) ;;
        *) source="$insource"/ ;;
    esac

    case "$intarget" in
        */) ;;
        *) target="$intarget"/ ;;
    esac

    #echo "source : \"$source\""
    #echo "target : \"$target\""

    local common_part=$source # for now

    local result=""

    #echo "common_part is now : \"$common_part\""
    #echo "result is now      : \"$result\""
    #echo "target#common_part : \"${target#$common_part}\""
    while [ "${target#$common_part}" = "${target}" -a "${common_part}" != "//" ]; do
        # no match, means that candidate common part is not correct
        # go up one level (reduce common part)
        common_part=$(dirname "$common_part")/
        # and record that we went back
        if [ -z "${result}" ]; then
            result="../"
        else
            result="../$result"
        fi
        #echo "(w) common_part is now : \"$common_part\""
        #echo "(w) result is now      : \"$result\""
        #echo "(w) target#common_part : \"${target#$common_part}\""
    done

    #echo "(f) common_part is     : \"$common_part\""

    if [ "${common_part}" = "//" ]; then
        # special case for root (no common path)
        common_part="/"
    fi

    # since we now have identified the common part,
    # compute the non-common part
    forward_part="${target#$common_part}"
    #echo "forward_part = \"$forward_part\""

    if [ -n "${result}" -a -n "${forward_part}" ]; then
        #echo "(simple concat)"
        result="$result$forward_part"
    elif [ -n "${forward_part}" ]; then
        result="$forward_part"
    fi
    #echo "result = \"$result\""

    # if a / was added to target and result ends in / then remove it now.
    if [ "$intarget" != "$target" ]; then
        case "$result" in
            */) result=$(echo "$result" | awk '{ string=substr($0, 1, length($0)-1); print string; }' ) ;;
        esac
    fi

    echo $result

    return 0
}

다음과 같은 맥락에서 쉽게 사용할 수있는 또 다른 솔루션 인 순수 bash+ GNU readlink:

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

편집 :이 경우 "$ B"가 존재하지 않거나 소프트 링크가 없는지 확인하십시오. 그렇지 않으면 relpath원하는 링크가 아닙니다.

이것은 거의 모든 현재 Linux에서 작동합니다. 경우 readlink -m귀하의 측면에서 작업을하지 않는 시도 readlink -f대신에. 가능한 업데이트 https://gist.github.com/hilbix/1ec361d00a8178ae8ea0참조하십시오 .

: relpath A B
# Calculate relative path from A to B, returns true on success
# Example: ln -s "$(relpath "$A" "$B")" "$B"
relpath()
{
local X Y A
# We can create dangling softlinks
X="$(readlink -m -- "$1")" || return
Y="$(readlink -m -- "$2")" || return
X="${X%/}/"
A=""
while   Y="${Y%/*}"
        [ ".${X#"$Y"/}" = ".$X" ]
do
        A="../$A"
done
X="$A${X#"$Y"/}"
X="${X%/}"
echo "${X:-.}"
}

노트:

  • 파일 이름에 *또는이 포함 된 경우, 원치 않는 셸 메타 문자 확장에 대해 안전합니다 ?.
  • 출력은 다음에 대한 첫 번째 인수로 사용할 수 있습니다 ln -s.
    • relpath / /제공 .빈 문자열을하지
    • relpath a a디렉토리 인 a경우에도 제공a
  • 가장 일반적인 경우도 합리적인 결과를 얻기 위해 테스트되었습니다.
  • 이 솔루션은 문자열 접두사 일치를 사용하므로 readlink경로를 정규화해야합니다.
  • 덕분에 readlink -m아직 기존 경로가 작동하지 않습니다.

readlink -m사용할 수없는 이전 시스템에서는 readlink -f파일이 없으면 실패합니다. 따라서 다음과 같은 해결 방법이 필요할 것입니다.

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

이것은 $1포함 .하거나 ..존재하지 않는 경로 (와 같이 /doesnotexist/./a)의 경우 실제로는 정확 하지 않지만 대부분의 경우를 다루어야합니다.

(교체 readlink -m --이상 readlink_missing).

downvote 때문에 편집

다음은이 기능이 실제로 올바른 테스트입니다.

check()
{
res="$(relpath "$2" "$1")"
[ ".$res" = ".$3" ] && return
printf ':WRONG: %-10q %-10q gives %q\nCORRECT %-10q %-10q gives %q\n' "$1" "$2" "$res" "$@"
}

#     TARGET   SOURCE         RESULT
check "/A/B/C" "/A"           ".."
check "/A/B/C" "/A.x"         "../../A.x"
check "/A/B/C" "/A/B"         "."
check "/A/B/C" "/A/B/C"       "C"
check "/A/B/C" "/A/B/C/D"     "C/D"
check "/A/B/C" "/A/B/C/D/E"   "C/D/E"
check "/A/B/C" "/A/B/D"       "D"
check "/A/B/C" "/A/B/D/E"     "D/E"
check "/A/B/C" "/A/D"         "../D"
check "/A/B/C" "/A/D/E"       "../D/E"
check "/A/B/C" "/D/E/F"       "../../D/E/F"

check "/foo/baz/moo" "/foo/bar" "../bar"

어찌할 바를 모르는? 글쎄, 이것들은 올바른 결과입니다 ! 질문에 맞지 않는다고 생각하더라도 이것이 올바른 증거입니다.

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

의심의 여지없이, ../bar페이지에서 bar페이지의 정확하고 정확한 상대 경로입니다 moo. 다른 모든 것은 잘못 될 것입니다.

명백하게 가정하는 질문, 즉 current디렉토리에 출력을 채택하는 것은 사소한 일입니다 .

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

요청한 내용을 정확하게 반환합니다.

그리고 눈썹을 높이기 전에 약간 더 복잡한 변형 relpath(작은 차이를 발견하십시오)은 URL 구문에서도 작동합니다 (따라서- 마법 /덕분에 후행이 유지 bash됩니다).

# Calculate relative PATH to the given DEST from the given BASE
# In the URL case, both URLs must be absolute and have the same Scheme.
# The `SCHEME:` must not be present in the FS either.
# This way this routine works for file paths an
: relpathurl DEST BASE
relpathurl()
{
local X Y A
# We can create dangling softlinks
X="$(readlink -m -- "$1")" || return
Y="$(readlink -m -- "$2")" || return
X="${X%/}/${1#"${1%/}"}"
Y="${Y%/}${2#"${2%/}"}"
A=""
while   Y="${Y%/*}"
        [ ".${X#"$Y"/}" = ".$X" ]
do
        A="../$A"
done
X="$A${X#"$Y"/}"
X="${X%/}"
echo "${X:-.}"
}

그리고 다음은 명확하게하기위한 점검 사항입니다. 실제로 말한대로 작동합니다.

check()
{
res="$(relpathurl "$2" "$1")"
[ ".$res" = ".$3" ] && return
printf ':WRONG: %-10q %-10q gives %q\nCORRECT %-10q %-10q gives %q\n' "$1" "$2" "$res" "$@"
}

#     TARGET   SOURCE         RESULT
check "/A/B/C" "/A"           ".."
check "/A/B/C" "/A.x"         "../../A.x"
check "/A/B/C" "/A/B"         "."
check "/A/B/C" "/A/B/C"       "C"
check "/A/B/C" "/A/B/C/D"     "C/D"
check "/A/B/C" "/A/B/C/D/E"   "C/D/E"
check "/A/B/C" "/A/B/D"       "D"
check "/A/B/C" "/A/B/D/E"     "D/E"
check "/A/B/C" "/A/D"         "../D"
check "/A/B/C" "/A/D/E"       "../D/E"
check "/A/B/C" "/D/E/F"       "../../D/E/F"

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

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

그리고 이것이 질문에서 원하는 결과를 얻는 데 어떻게 사용될 수 있습니까?

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

작동하지 않는 것을 찾으면 아래 의견에 알려주십시오. 감사.

추신:

relpath여기에있는 다른 모든 대답과 대조적으로 "역전 된" 주장은 왜 그렇 습니까?

변경하면

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

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

그런 다음 BASE가 현재 디렉토리 / URL / 무엇이든지 두 번째 매개 변수를 남겨 둘 수 있습니다. 그것은 평소와 같이 유닉스 원칙입니다.

마음에 들지 않으면 Windows로 돌아갑니다. 감사.


이것도 트릭을 할 것 같아요 ... (내장 테스트가 제공됩니다) :)

좋아, 약간의 오버 헤드가 예상되었지만 우리는 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 에서이 기능을 구현하기 위해이 질문에 대한 답변을 사용하려고했기 때문에 그것을 버릴 것입니다.

이맥스는 실제로 다음과 같은 기능을 가지고 있습니다.

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

출처 : http://www.ynform.org/w/Pub/Relpath


나는 이와 같은 것이 필요했지만 상징적 인 링크도 해결했습니다. pwd에 해당 용도로 -P 플래그가 있음을 발견했습니다. 내 스크립트 조각이 추가되었습니다. 쉘 스크립트의 함수 내에 있으므로 $ 1과 $ 2입니다. START_ABS에서 END_ABS까지의 상대 경로 인 결과 값은 UPDIRS 변수에 있습니다. 스크립트 cd는 pwd -P를 실행하기 위해 각 매개 변수 디렉토리에 있으며 이는 상대 경로 매개 변수가 처리됨을 의미합니다. 건배, 짐

SAVE_DIR="$PWD"
cd "$1"
START_ABS=`pwd -P`
cd "$SAVE_DIR"
cd "$2"
END_ABS=`pwd -P`

START_WORK="$START_ABS"
UPDIRS=""

while test -n "${START_WORK}" -a "${END_ABS/#${START_WORK}}" '==' "$END_ABS";
do
    START_WORK=`dirname "$START_WORK"`"/"
    UPDIRS=${UPDIRS}"../"
done
UPDIRS="$UPDIRS${END_ABS/#${START_WORK}}"
cd "$SAVE_DIR"

참고 URL : https://stackoverflow.com/questions/2564634/convert-absolute-path-into-relative-path-given-a-current-directory-using-bash

반응형