Конвертировать абсолютный путь в относительный путь, используя текущий каталог, используя Bash


261

Пример:

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

# Magic

relative="../../bar"

Как мне создать магию (надеюсь, не слишком сложный код ...)?


7
Например (мой случай прямо сейчас) для предоставления относительного пути gcc, чтобы он мог генерировать относительную информацию отладки, пригодную для использования, даже если исходный путь изменился.
Offirmo

Вопрос, похожий на этот, был задан в U & L: unix.stackexchange.com/questions/100918/… . В одном из ответов (@Gilles) упоминается инструмент, символические ссылки , которые могут облегчить решение этой проблемы.
Slm

25
Простой: realpath --relative-to=$absolute $current.
Кенорб

Ответы:


229

Я думаю, что использование realpath из GNU coreutils 8.23 ​​является самым простым:

$ realpath --relative-to="$file1" "$file2"

Например:

$ realpath --relative-to=/usr/bin/nmap /tmp/testing
../../../tmp/testing

7
Жаль, что пакет устарел в Ubuntu 14.04 и не имеет опции --relative-to.
кж


7
$ realpath --relative-to="${PWD}" "$file"полезно, если вы хотите пути относительно текущего рабочего каталога.
17

1
Это верно для вещей внутри /usr/bin/nmap/-path, но не для /usr/bin/nmap: от nmapк /tmp/testingтолько ../../и не 3 раза ../. Это работает, однако, потому что делать ..на rootfs есть /.
Патрик Б.

6
Как @PatrickB. подразумевается, --relative-to=…ожидает каталог и не проверяет. Это означает, что вы получите дополнительный «../», если вы запрашиваете путь относительно файла (как, кажется, делает этот пример, потому что /usr/binредко или никогда не содержит каталогов и nmapобычно является двоичным)
IBBoard

162
$ python -c "import os.path; print os.path.relpath('/foo/bar', '/foo/baz/foo')"

дает:

../../bar

11
Это работает, и это делает альтернативы выглядеть нелепо. Это бонус для меня xD
hasvn

31
+1. Хорошо, вы обманули ... но это слишком хорошо, чтобы не использовать! relpath(){ python -c "import os.path; print os.path.relpath('$1','${2:-$PWD}')" ; }
MestreLion

4
К сожалению, это не доступно универсально: os.path.relpath является новым в Python 2.6.
Чен Леви

15
@ChenLevy: Python 2.6 был выпущен в 2008 году. Трудно поверить, что он не был общедоступным в 2012 году.
MestreLion

11
python -c 'import os, sys; print(os.path.relpath(*sys.argv[1:]))'работает максимально естественно и надежно.
Musiphil

31

Это исправленное, полностью функциональное улучшение решения @pini, которое в настоящее время имеет наивысшую оценку (к сожалению, оно работает только в нескольких случаях)

Напоминание: проверка «-z», если строка имеет нулевую длину (= пусто), и проверка «-n», если строка не пустая.

# both $1 and $2 are absolute paths beginning with /
# returns relative path to $2/$target from $1/$source
source=$1
target=$2

common_part=$source # for now
result="" # for now

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

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

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

# and now stick all parts together
if [[ -n $result ]] && [[ -n $forward_part ]]; then
    result="$result$forward_part"
elif [[ -n $forward_part ]]; then
    # extra slash removal
    result="${forward_part:1}"
fi

echo $result

Тестовые случаи:

compute_relative.sh "/A/B/C" "/A"           -->  "../.."
compute_relative.sh "/A/B/C" "/A/B"         -->  ".."
compute_relative.sh "/A/B/C" "/A/B/C"       -->  ""
compute_relative.sh "/A/B/C" "/A/B/C/D"     -->  "D"
compute_relative.sh "/A/B/C" "/A/B/C/D/E"   -->  "D/E"
compute_relative.sh "/A/B/C" "/A/B/D"       -->  "../D"
compute_relative.sh "/A/B/C" "/A/B/D/E"     -->  "../D/E"
compute_relative.sh "/A/B/C" "/A/D"         -->  "../../D"
compute_relative.sh "/A/B/C" "/A/D/E"       -->  "../../D/E"
compute_relative.sh "/A/B/C" "/D/E/F"       -->  "../../../D/E/F"

1
Интегрирована в оболочку offirmo lib github.com/Offirmo/offirmo-shell-lib , функция «OSL_FILE_find_relative_path» (файл «osl_lib_file.sh»)
Offirmo

1
+1. Это может быть легко сделано для обработки любых путей (не только абсолютных путей, начинающихся с /) путем заменыsource=$1; target=$2 наsource=$(realpath $1); target=$(realpath $2)
Josh Kelley

2
@ Джош, действительно, при условии, что каталоги действительно существуют ... что было неудобно для модульных тестов;) Но при реальном использовании да, realpathрекомендуется, и source=$(readlink -f $1)т. Д., Если realpath недоступен (не стандарт)
Offirmo

Я определил $sourceи $targetвот так: `if [[-e $ 1]]; затем source = $ (readlink -f $ 1); иначе источник = 1 $; fi if [[-e $ 2]]; затем target = $ (readlink -f $ 2); иначе цель = 2 $; Таким образом, функция может обрабатывать реальные / существующие относительные пути, а также вымышленные каталоги.
Натан Уотсон-Хей

1
@ NathanS.Watson-Haigh Еще лучше, я недавно обнаружил, что readlinkесть -mопция, которая просто делает это;)
Offirmo

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

Прекрасный сценарий - короткий и чистый. Я применил редактирование (Ожидание экспертной оценки): common_part = $ source / common_part = $ (имя_директора $ common_part) / echo $ {target} $ {target # $ common_part} Существующий сценарий потерпит неудачу из-за неправильного соответствия в начале имени каталога при сравнении, например: «/ foo / bar / baz» с «/ foo / barsucks / bonk». Перемещение косой черты в var и из последнего eval исправляет эту ошибку.
jcwenger

3
Этот скрипт просто не работает. Сбой одного простого теста "один каталог вниз". Редактирование jcwenger работает немного лучше, но имеет тенденцию добавлять дополнительные "../".
Доктор Лицо Человек II

1
в некоторых случаях у меня ничего не получается, если в аргументе идет завершающий символ "/"; например, если $ 1 = "$ HOME /" и $ 2 = "$ HOME / temp", он возвращает "/ home / user / temp /", но если $ 1 = $ HOME, тогда он правильно возвращает относительный путь "temp". Поэтому source = $ 1 и target = $ 2 могут быть «очищены» с помощью sed (или с помощью подстановки переменных bash, но это может быть излишне непрозрачным), например => source = $ (echo "$ {1}" | sed 's / \ / * $ // ')
Майкл

1
Незначительное улучшение: вместо того, чтобы устанавливать источник / цель непосредственно на $ 1 и $ 2, сделайте: source: $ = ($ cd $; pwd) target = $ (cd $ 2; pwd). Таким образом, он обрабатывает пути с. и .. правильно.
Джозеф Гарвин

4
Несмотря на то, что этот ответ получил наибольшее количество голосов, у этого ответа есть много ограничений, поэтому многие другие ответы публикуются. Вместо этого посмотрите другие ответы, особенно те, которые показывают тестовые случаи. И пожалуйста, проголосуйте за этот комментарий!
Offirmo

25

Он встроен в Perl с 2001 года, поэтому работает практически на всех системах, которые вы можете себе представить, даже на 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

... будет работать нормально.


3
sayне был доступен в Perl для журнала, но он мог бы эффективно использоваться здесь. perl -MFile::Spec -E 'say File::Spec->abs2rel(@ARGV)'
Уильям Перселл

+1, но смотрите также этот аналогичный ответ, который старше (февраль 2012). Читайте также соответствующие комментарии от Уильяма Перселла . Моя версия состоит из двух командных строк: perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target"и perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target" "$origin". Первый однострочный perl- скрипт использует один аргумент (origin - текущий рабочий каталог). Второй однострочный Perl- скрипт использует два аргумента.
olibre

3
Это должен быть принятый ответ. perlможно найти почти везде, хотя ответ по-прежнему однострочно.
Дмитрий Гинзбург

19

Предполагается, что вы установили: 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/}

Я играл в гольф ответ от Пини и несколько других идей

Примечание . Для этого необходимо, чтобы оба пути были существующими папками. Файлы не будут работать.


2
идеальный ответ: работает с / bin / sh, не требует readlink, python, perl -> отлично подходит для легких / встроенных систем или консоли Windows bash
Francois

2
К сожалению, это требует, чтобы путь существовал, что не всегда желательно.
drwatsoncode

Божественный ответ. Полагаю, что cd-pwd предназначен для разрешения ссылок? Приятно играть в гольф!
Teck-урод

15

Питона os.path.relpath как функция оболочки

Цель этого relpathупражнения состоит в том, чтобы имитировать os.path.relpathфункцию Python 2.7 (доступна из Python версии 2.6, но работает должным образом только в 2.7), как предложено xni . Как следствие, некоторые результаты могут отличаться от функций, представленных в других ответах.

(Я не тестировал с символами новой строки в путях просто потому, что это нарушает валидацию, основанную на вызовах python -cиз ZSH. Это, безусловно, было бы возможно с некоторыми усилиями.)

Что касается «магии» в Bash, я давно перестал искать магию в Bash, но с тех пор я нашел всю магию, которая мне нужна, а затем и некоторую, в ZSH.

Следовательно, я предлагаю две реализации.

Первая реализация стремится быть полностью POSIX-совместимой . Я проверил это /bin/dashна Debian 6.0.6 «Squeeze». Он также отлично работает с/bin/sh OS X 10.8.3, которая на самом деле является версией Bash 3.2, претендующей на роль оболочки POSIX.

Вторая реализация - это функция оболочки ZSH, устойчивая к множественным слешам и другим неприятностям в путях. Если у вас есть доступный ZSH, это рекомендуемая версия, даже если вы вызываете ее в форме скрипта, представленной ниже (то есть с шаблоном #!/usr/bin/env zsh) из другой оболочки.

Наконец, я написал сценарий ZSH, который проверяет вывод relpathкоманды, найденной в $PATHзаданных тестах, приведенных в других ответах. Я добавил в эти тесты некоторую изюминку, добавив несколько пробелов, табуляций и знаков препинания, таких как ! ? *здесь и там, а также добавил еще один тест с экзотическими символами UTF-8, найденными в vim-powerline .

Функция оболочки 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пакете Linux ), замените :aстроки 3 и 4 на :A.

Чтобы использовать это в zsh, удалите первую и последнюю строку и поместите его в каталог, который находится в вашей $FPATHпеременной.

#!/usr/bin/env zsh
relpath () {
    [[ $# -ge 1 ]] && [[ $# -le 2 ]] || return 1
    local target=${${2:-$1}:a} # replace `:a' by `:A` to resolve symlinks
    local current=${${${2:+$1}:-$PWD}:a} # replace `:a' by `:A` to resolve symlinks
    local appendix=${target#/}
    local relative=''
    while appendix=${target#$current/}
        [[ $current != '/' ]] && [[ $appendix = $target ]]; do
        if [[ $current = $appendix ]]; then
            relative=${relative:-.}
            print ${relative#/}
            return 0
        fi
        current=${current%/*}
        relative="$relative${relative:+/}.."
    done
    relative+=${relative:+${appendix:+/}}${appendix#/}
    print $relative
}
relpath "$@"

Тестовый скрипт

Наконец, тестовый скрипт. Он принимает одну опцию, а именно -vвключить подробный вывод.

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

2
/Я не боюсь, что первый путь заканчивается .
Нолдорин

12
#!/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")"; }

Над сценарием оболочки был вдохновлен Пини (Спасибо!). Это вызывает ошибку в модуле подсветки синтаксиса переполнения стека (по крайней мере, в моем окне предварительного просмотра). Поэтому, пожалуйста, игнорируйте, если подсветка неверна.

Некоторые заметки:

  • Удалены ошибки и улучшен код без значительного увеличения длины и сложности кода.
  • Внесите функциональность в функции для простоты использования
  • Поддерживает функции POSIX-совместимые, чтобы они (должны) работать со всеми оболочками POSIX (протестировано с dash, bash и zsh в Ubuntu Linux 12.04)
  • Используются только локальные переменные, чтобы избежать засорения глобальных переменных и загрязнения глобального пространства имен
  • Оба пути к каталогам НЕ должны существовать (требование для моего приложения)
  • Имена путей могут содержать пробелы, специальные символы, управляющие символы, обратную косую черту, символы табуляции, ', ",?, *, [,] И т. Д.
  • Основная функция «relPath» использует только встроенные функции оболочки POSIX, но требует канонических абсолютных путей к каталогам в качестве параметров.
  • Расширенная функция "relpath" может обрабатывать произвольные пути к каталогам (также относительные, неканонические), но требует использования внешней утилиты ядра GNU "readlink"
  • Избегал встроенного «echo» и использовал вместо него встроенный «printf» по двум причинам:
  • Чтобы избежать ненужных преобразований, имена путей используются по мере того, как они возвращаются и ожидаются утилитами оболочки и ОС (например, cd, ln, ls, find, mkdir; в отличие от «os.path.relpath» Python, который будет интерпретировать некоторые последовательности обратной косой черты)
  • За исключением упомянутых последовательностей обратной косой черты, последняя строка функции «relPath» выводит имена путей, совместимые с python:

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

    Последняя строка может быть заменена (и упрощена) на строку

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

    Я предпочитаю последнее, потому что

    1. Имена файлов могут быть непосредственно добавлены в пути dir, полученные relPath, например:

      ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
    2. Символические ссылки в том же каталоге, созданные с помощью этого метода, не имеют отвратительного "./"префикса к имени файла.

  • Если вы обнаружите ошибку, пожалуйста, свяжитесь с linuxball (at) 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

9

Не многие ответы здесь являются практичными для повседневного использования. Поскольку это очень трудно сделать правильно в чистом виде, я предлагаю следующее, надежное решение (аналогично одному предложению, скрытому в комментарии):

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

Затем вы можете получить относительный путь на основе текущего каталога:

echo $(relpath somepath)

или вы можете указать, что путь должен быть относительно данного каталога:

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

Недостатком является то, что это требует Python, но:

  • Работает одинаково в любом питоне> = 2.6
  • Это не требует, чтобы файлы или каталоги существовали.
  • Имена файлов могут содержать более широкий диапазон специальных символов. Например, многие другие решения не работают, если имена файлов содержат пробелы или другие специальные символы.
  • Это однострочная функция, которая не загромождает сценарии.

Обратите внимание, что решения, которые включают basenameили dirnameмогут не обязательно быть лучше, так как требуют coreutilsих установки. Если у кого-то есть чистое bashрешение, которое является надежным и простым (а не извилистым любопытством), я был бы удивлен.


Это кажется самым надежным подходом.
dimo414

7

Этот скрипт дает правильные результаты только для входных данных, которые являются абсолютными или относительными путями без .или ..:

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

1
Это похоже на работу. Если каталоги действительно существуют, использование $ (readlink -f $ 1) и $ (readlink -f $ 2) на входах может решить проблему, где "." или ".." появляется на входах. Это может вызвать некоторые проблемы, если каталоги на самом деле не существуют.
Доктор Лицо Человек II

7

Я бы просто использовал Perl для этой не очень тривиальной задачи:

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

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

1
+1, но рекомендую: perl -MFile::Spec -e "print File::Spec->abs2rel('$absolute','$current')"так что абсолютные и текущие котируются.
Уильям Перселл

Мне нравится relative=$(perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$absolute" "$current"). Это гарантирует, что значения сами по себе не могут содержать код perl!
Эрик Аронести

6

Небольшое улучшение ответов Каску и Пини , которое лучше подходит для пробелов и позволяет проходить относительные пути:

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

4

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

+1 за компактность и башку. Вы должны, однако, также вызвать readlinkна $(pwd).
DevSolar

2
Относительный не означает, что файл должен быть помещен в тот же каталог.
Гринольдман

хотя исходный вопрос не содержит большого количества тестовых случаев, этот скрипт не подходит для простых тестов, таких как поиск относительного пути от / home / user1 к / home / user2 (правильный ответ: ../user2). Сценарий pini / jcwenger работает для этого случая.
Майкл

4

Еще одно решение, pure 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-magic):

# 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 / что угодно. Это только принцип Unix, как обычно.

Если вам это не нравится, пожалуйста, вернитесь в Windows. Спасибо.


3

К сожалению, ответ Марка Рушакова (теперь удаленный - он ссылался на код отсюда ), кажется, не работает правильно, когда адаптирован к:

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

Мышление, изложенное в комментарии, может быть улучшено, чтобы оно работало правильно в большинстве случаев. Я собираюсь предположить, что скрипт принимает исходный аргумент (где вы находитесь) и целевой аргумент (где вы хотите попасть), и что либо оба являются абсолютными путями, либо оба являются относительными. Если одно является абсолютным, а другое относительным, проще всего сделать префикс относительного имени с текущим рабочим каталогом - но приведенный ниже код этого не делает.


берегись

Код ниже близок к правильной работе, но не совсем правильно.

  1. Эта проблема решена в комментариях от Денниса Уильямсона.
  2. Есть также проблема в том, что это чисто текстовая обработка имен путей, и вы можете быть серьезно испорчены странными символическими ссылками.
  3. Код не обрабатывает случайные «точки» в путях типа « xyz/./pqr».
  4. Код не обрабатывает случайные «двойные точки» в путях типа « xyz/../pqr».
  5. Тривиально: код не удаляет ведущие ' ./' из путей.

Код Денниса лучше, потому что он исправляет 1 и 5 - но имеет те же проблемы 2, 3, 4. Из-за этого используйте код Денниса (и проголосуйте заранее).

(Примечание: POSIX предоставляет системный вызов, realpath()который разрешает имена путей, так что в них не остается символических ссылок. Применение этого к входным именам, а затем использование кода Денниса даст правильный ответ каждый раз. Это тривиально, чтобы написать код C, который обертывания realpath()- я сделал это - но я не знаю стандартной утилиты, которая делает это.)


Для этого я считаю, что Perl проще в использовании, чем shell, хотя 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для определения реального пути имен, которые существуют, и выполняет текстовый анализ для путей, которые не существуют. Во всех случаях, кроме одного, он выдает тот же результат, что и сценарий Денниса. Девиантный случай:

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

Ваш /home/part1/part2к /имеет слишком много ../. В противном случае мой сценарий совпадает с вашим выводом, за исключением того, что мой добавляет ненужный .в конце того, где находится пункт назначения, .и я не использую ./в начале те, которые спускаются, не повышаясь.
Приостановлено до дальнейшего уведомления.

@Dennis: я проводил время, наблюдая за результатами - иногда я мог видеть эту проблему, а иногда я не мог найти ее снова. Удаление ведущего «./» является еще одним тривиальным шагом. Ваш комментарий о «нет вложенных. или .. 'также уместно. На самом деле на удивление трудно выполнить работу должным образом - вдвойне, если какое-либо из имен действительно является символической ссылкой; мы оба делаем чисто текстовый анализ.
Джонатан Леффлер

@Dennis: Конечно, если у вас нет сети Newcastle Connection, пытаться выйти выше уровня root бесполезно, поэтому ../../../ .. и ../../ .. эквивалентны. Однако это чистый эскапизм; Ваша критика верна. (Newcastle Connection позволил вам настроить и использовать нотацию /../host/path/on/remote/machine для доступа к другому хосту - аккуратная схема. Я считаю, что он поддерживается /../../network/host/ путь / на / удаленный / сеть / и / хост тоже. Это в Википедии.)
Джонатан Леффлер

Таким образом, вместо этого у нас теперь есть двойной слэш UNC.
Приостановлено до дальнейшего уведомления.

1
Утилита «readlink» (по крайней мере, версия GNU) может сделать эквивалент realpath (), если вы передадите ей опцию «-f». Например, в моей системе, readlink /usr/bin/viдает /etc/alternatives/vi, но это еще одна символическая ссылка - в то время как readlink -f /usr/bin/viдает /usr/bin/vim.basic, который является конечным пунктом назначения всех символических ссылок ...
psmears

3

Я принял ваш вопрос как задачу написать это в «переносимом» коде оболочки, т.е.

  • имея в виду оболочку 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 :

2

Мое решение:

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

}

Это чрезвычайно портативно :)
Аноним

2

Вот моя версия. Он основан на ответ по @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
}

1

Угадай, этот тоже сделает свое дело ... (поставляется со встроенными тестами) :)

ОК, ожидаются некоторые накладные расходы, но мы здесь работаем над оболочкой 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

1

Этот скрипт работает только с путями. Для этого не требуется никаких файлов. Если пройденные пути не являются абсолютными, поведение будет немного необычным, но оно должно работать так, как ожидается, если оба пути являются относительными.

Я только протестировал его на 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

Кроме того, код немного скопировать и вставить, но мне это нужно было довольно быстро.
Juancn

Хорошо ... как раз то, что мне было нужно. И вы включили канонизирующую процедуру, которая лучше, чем большинство других, которые я видел (которые обычно полагаются на замены регулярных выражений).
drwatsoncode

0

Этот ответ не касается 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"

Обратите внимание, что я полагаю, что ответ Python, который я недавно добавил ( relpathфункция), ведет себя идентично file-relative-nameдля предоставленных вами тестов.
Гари Вишневски,

-1

Вот скрипт оболочки, который делает это без вызова других программ:

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


1
Это не очень переносимо из-за использования конструкции $ {param / pattern / subst}, которая не является POSIX (по состоянию на 2011 год).
Дженс

Ссылочный источник ynform.org/w/Pub/Relpath указывает на полностью искаженную вики-страницу, содержащую несколько раз содержимое скрипта, смешанное со строками тильды, сообщениями об ошибках, не найденных командах, и еще много чего. Совершенно бесполезно для тех, кто исследует оригинал.
Дженс

-1

Мне нужно было что-то подобное, но также разрешало символические ссылки. Я обнаружил, что для этой цели у pwd есть флаг -P. Фрагмент моего сценария прилагается. Это внутри функции в сценарии оболочки, следовательно, $ 1 и $ 2. Значение результата, которое является относительным путем от START_ABS до END_ABS, находится в переменной UPDIRS. Компакт-диск скрипта входит в каждый каталог параметров, чтобы выполнить 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"
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.