programing

Bash를 사용하여 절대 경로를 현재 디렉터리가 지정된 상대 경로로 변환

linuxpc 2023. 5. 11. 21:09
반응형

Bash를 사용하여 절대 경로를 현재 디렉터리가 지정된 상대 경로로 변환

예:

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

# Magic

relative="../../bar"

어떻게 마법을 만들 수 있을까요? (너무 복잡하지 않았으면 좋겠는데...)

용사를 합니다.realpathGNU coreutils 8.23이 가장 단순하다고 생각합니다.

$ 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하고 "0-"(=")인지 합니다.-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"

2001년부터 Perl에 내장되어 있으므로 VMS를 비롯한 거의 모든 시스템에서 작동합니다.

perl -le 'use File::Spec; print File::Spec->abs2rel(@ARGV)' FILE BASE

또한, 그 해결책은 이해하기 쉽습니다.

예를 들어, 다음과 같습니다.

perl -le 'use File::Spec; print File::Spec->abs2rel(@ARGV)' $absolute $current

...괜찮을 겁니다.

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

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

참고: 이 경우 두 경로가 모두 기존 폴더여야 합니다.파일이 작동하지 않습니다.

파이의os.path.relpath

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

는 단순히 호출에 에 새 을 사용하여 .python -cZSH입니다. 의 노력이 어느 정도의 노력이 있다면 분명 가능할 것입니다.)

바시의 "마법"과 관련하여, 저는 오래 전에 바시에서 마법을 찾는 것을 포기했지만, 그 이후로 ZSH에서 제가 필요로 하는 모든 마법을 찾았습니다.

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

첫 번째 구현은 POSIX와 완전히 호환되는 것을 목표로 합니다.로 테스트했습니다./bin/dash데비안 6.0.6 "스퀴즈"에서.또한 완벽하게 작동합니다./bin/shOS X 10.8.3에서 POSIX 셸로 가장하는 실제 Bash 버전 3.2입니다.

두 번째 구현은 경로의 여러 슬래시 및 기타 방해 요소에 대해 강력한 ZSH 셸 기능입니다.이 ZSH를 수 있다면,이 아래에 에도, 버전입니다. ( ZSH의 ).#!/usr/bin/env zsh에서. 다른 껍질에서.

저는 ZSH의 했습니다.relpath은 서에찾위에서 찾을 수 .$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판본실제 경로에 대한 인수를 해결하려면 다음과 같이 하십시오.realpath -f)coreutils를 합니다.:a과 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
#!/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")"; }

위의 셸 스크립트는 피니(Thanks!)에서 영감을 받았습니다.적어도 내 미리보기 프레임에서 스택 오버플로의 구문 강조 모듈에 버그를 트리거합니다.따라서 강조 표시가 잘못되었다면 무시해주시기 바랍니다.

참고 사항:

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

    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. 이메의로만동든일한dir심링볼없다습링니가 ."./"파일 이름 앞에 추가합니다.

  • 오류가 발견되면 리눅스볼(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

여기서는 일상적인 사용에 실용적인 답변이 많지 않습니다.순수한 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(복잡한 호기심보다는) 신뢰할 수 있고 간단한 해결책, 나는 놀랄 것입니다.

는 절대 또는 합니다..또는..:

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

나는 이 그리 간단하지 않은 작업을 위해 Perl을 사용할 것입니다.

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

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

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

하지만 또 다른 해결책은, 순수합니다.bash GNUreadlink다음과 같은 상황에서 쉽게 사용할 수 있습니다.

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.)

다음은 반대표 때문에 편집합니다.

다음은 이 기능이 실제로 정확하다는 테스트입니다.

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-Syntax에도 적용됩니다)./일부 덕분에 살아남습니다.bashfilename):

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

만약 당신이 작동하지 않는 것을 발견하면 아래 댓글로 알려주시기 바랍니다.감사해요.

PS:

의주은의 은?relpath여기 있는 다른 모든 대답들과 대조적으로 "순수"?

변경할 경우

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

로.

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

그런 다음 두 번째 매개 변수를 남겨 BASE가 현재 디렉터리/URL/what이 되도록 할 수 있습니다.그것은 단지 유닉스의 원칙일 뿐입니다. 평소처럼.

안타깝게도 마크 러샤코프의 답변(지금은 삭제되었습니다. 여기서 코드를 참조했습니다.)은 다음과 같이 적용될 때 제대로 작동하지 않는 것 같습니다.

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: 호출을 제공합니다.)realpath()경로 이름을 확인하여 심볼릭 링크가 남지 않도록 합니다.입력 이름에 이를 적용한 다음 Dennis 코드를 사용하면 매번 정답을 얻을 수 있습니다.C 코드를 랩으로 작성하는 것은 사소한 일입니다.realpath()저는 해봤지만, 그렇게 하는 표준 유틸리티는 잘 모릅니다.)


이를 위해, 저는 셸보다 펄을 사용하는 것이 쉽다고 생각하지만, 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 그 은 리고그기 능그기 능▁and입니다.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);

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 쉘을 염두에 두고
  • 배열과 같은 바시즘 없음
  • 페스트와 같은 외부를 부르는 것을 피하세요.대본에 포크가 하나도 없어요!따라서 특히 Cygwin과 같이 포크 오버헤드가 큰 시스템에서 매우 빠릅니다.
  • 경로 이름의 글로벌 문자(*, ?, [, ])를 처리해야 합니다.

모든 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

}

이 스크립트는 경로 이름에서만 작동합니다.파일이 존재할 필요는 없습니다.전달된 경로가 절대 경로가 아닌 경우 동작이 약간 비정상적이지만 두 경로가 모두 상대적인 경우 예상대로 작동해야 합니다.

저는 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

여기 제 버전이 있습니다.@Offirmo의 을 바탕으로 한 것입니다.Dash 호환으로 만들고 다음 테스트 케이스 오류를 해결했습니다.

./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
}

는 macOS가 없었던 .realpath하여 기적으실명니다습행했령을 만들었습니다.pure bash계산하는 함수입니다.

#!/bin/bash

##
# print a relative path from "source folder" to "target file"
#
# params:
#  $1 - target file, can be a relative path or an absolute path.
#  $2 - source folder, can be a relative path or an absolute path.
#
# test:
#  $ mkdir -p ~/A/B/C/D; touch ~/A/B/C/D/testfile.txt; touch ~/A/B/testfile.txt
#
#  $ getRelativePath ~/A/B/C/D/testfile.txt  ~/A/B
#  $ C/D/testfile.txt
#  
#  $ getRelativePath ~/A/B/testfile.txt  ~/A/B/C
#  $ ../testfile.txt
#
#  $ getRelativePath ~/A/B/testfile.txt  /
#  $ home/bunnier/A/B/testfile.txt 
#
function getRelativePath(){
    local targetFilename=$(basename $1)
    local targetFolder=$(cd $(dirname $1);pwd) # absolute target folder path
    local currentFolder=$(cd $2;pwd) # absulute source folder
    local result=.

    while [ "$currentFolder" != "$targetFolder" ];do
      if [[ "$targetFolder" =~ "$currentFolder"* ]];then
          pointSegment=${targetFolder#$currentFolder}
          result=$result/${pointSegment#/}
          break
      fi  
      result="$result"/..
      currentFolder=$(dirname $currentFolder)
    done

    result=$result/$targetFilename
    echo ${result#./}
}

이 사람도 그 재주를 부릴 것 같아요(내장 테스트 포함) :)

좋아요, 약간의 간접비가 예상되지만, 우리는 여기서 본 셸을 하고 있어요! ;)

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

이 답변은 질문의 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"

bash:

realDir=''
cd $(dirname $0) || exit
realDir=$(pwd)
cd -
echo $realDir

다음은 다른 프로그램을 호출하지 않고 이를 수행하는 셸 스크립트입니다.

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

반응형