Перетворіть абсолютний шлях у відносний шлях заданого поточного каталогу за допомогою 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.
kenorb

Відповіді:


228

Використання realpath від GNU coreutils 8.23, я думаю:

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

Наприклад:

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

7
Шкода, що пакет застарів на Ubuntu 14.04 і не має опції - відносно до.
кж

3
Прекрасно працює на Ubuntu 16.04
cayhorstmann

7
$ realpath --relative-to="${PWD}" "$file"корисно, якщо ви хочете, щоб шляхи були відносно поточного робочого каталогу.
dcoles

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

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 shell 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)
Джош Келлі

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

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

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 = $ (dirname $ common_part) / echo $ {назад} $ {target # $ common_part} Існуючий сценарій не вдасться через невідповідну відповідність початку запуску імені каталогу при порівнянні, наприклад: "/ foo / bar / baz" to "/ foo / barsucks / bonk". Переміщення косої риски у вар і поза остаточним eval виправляє помилку.
jcwenger

3
Цей сценарій просто не працює. Не вдається простий тест "один каталог вниз". Зміни jcwenger працюють трохи краще, але, як правило, додають додаткових "../".
Доктор Персона Особа II

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

1
Незначне вдосконалення: замість встановлення джерела / цілі безпосередньо на $ 1 і $ 2, зробіть: source = $ (cd $ 1; 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 використовує один аргумент (джерелом є поточна робоча директорія). Другий однорядковий сценарій perl використовує два аргументи.
олібре

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

Я golfed відповідь від Піні і кілька інших ідей

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


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

2
На жаль, це вимагає існування шляху, який не завжди бажаний.
drwatsoncode

Божественна відповідь. Мабуть, CD-pwd призначений для вирішення посилань? Приємного гольфу!
Teck-freak

15

Python's os.path.relpath є функцією оболонки

Мета цієї relpathвправи - імітувати os.path.relpathфункцію Python 2.7 (доступна з версії Python 2.6, але працює належним чином у 2.7), як це запропонував xni . Як наслідок, деякі результати можуть відрізнятися від функцій, наданих в інших відповідях.

(Я не перевіряв нові рядки в шляхах просто тому, що він порушує перевірку на основі виклику python -cз ZSH. Це, безумовно, було б можливо з певними зусиллями.)

Щодо "магії" в Баші, я давно відмовився шукати магію в Баші, але з тих пір знайшов всю магію, яка мені потрібна, а потім і якусь, в ZSH.

Отже, я пропоную дві реалізації.

Перша реалізація має на меті бути повністю сумісною з POSIX . Я перевірив це /bin/dashна Debian 6.0.6 “Stqueeze”. Він також чудово працює з /bin/shОС X 10.8.3, що фактично є версією 3.2 Bash, яка претендує на оболонку 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версія. Якщо ви хочете, щоб він вирішив аргументи на реальні шляхи à la 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 (тестували тире, bash та zsh в Ubuntu Linux 12.04)
  • Використовуються лише локальні змінні, щоб уникнути зменшення глобальних змінних та забруднення глобального простору імен
  • Обидва шляхи до каталогу НЕ потребують існування (вимога для моєї програми)
  • Імена шляхів можуть містити пробіли, спеціальні символи, контрольні символи, зворотні косої риски, вкладки ",",?, *, [,] Тощо.
  • Основна функція "relPath" використовує лише вбудовані оболонки POSIX, але вимагає абсолютних канонічних контурів каталогів як параметрів
  • Розширена функція "relpath" може обробляти довільні шляхи до каталогу (також відносні, неканонічні), але вимагає зовнішньої утиліти GNU "readlink"
  • Уникайте вбудованого "відлуння" і використовуйте вбудований "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

Один з недоліків - це потрібен пітон, але:

  • Він працює однаково в будь-якому пітоні> = 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
Відносно не означає, що файл повинен бути розміщений в одному каталозі.
greenoldman

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

4

Ще одне рішення, чисто 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.)

Редагувати через зменшення голосування слід

Ось тест, чи справді ця функція є правильною:

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

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

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

Спантеличений? Ну, це правильні результати ! Навіть якщо ви вважаєте, що це питання не відповідає, ось такий доказ це правильно:

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

Без жодного сумніву, ../barце точний і єдино правильний відносний шлях сторінки, що barбачиться зі сторінки moo. Все інше було б явно неправильним.

Тривіально прийняти висновок до питання, яке, мабуть, передбачає, що currentце каталог:

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

Це точно повертає те, про що вимагали.

І перед тим як підняти брову, ось трохи складніший варіант relpath(відзначте невелику різницю), який також повинен працювати для URL-синтаксису (так що трелінг /виживає, завдяки деяким bash-магічним):

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

І ось перевірки просто для того, щоб зрозуміти: це дійсно працює, як сказано.

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

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

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

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

Ось як це можна використати для отримання бажаного результату від питання:

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

Якщо ви знайдете щось, що не працює, повідомте мене про це у коментарях нижче. Дякую.

PS:

Чому аргументи relpath"зворотні" на відміну від усіх інших відповідей тут?

Якщо ти змінишся

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

до

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

тоді ви можете залишити другий параметр подалі, таким чином, що БАЗА є поточним каталогом / 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()який вирішує імена шляхів, щоб у них не залишилося жодних посилань. Застосовуючи це до імен вводу, а потім використовуючи код Денніса, кожен раз даватиме правильну відповідь. Тривіально писати код С, обгортання realpath()- я це зробив - але я не знаю стандартної утиліти, яка це робить.)


Для цього мені здається, що Perl простіший у використанні, ніж оболонка, хоча 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до /має занадто багато ../. В іншому випадку мій сценарій відповідає вашому результату, крім мого, додає непотрібне .в кінці того, де є місце призначення, .і я не використовую ./на початку ті, що спускаються без підйому вгору.
Призупинено до подальшого повідомлення.

@Денніс: Я витрачав час, переглядаючи результати, пильнуючи очима - іноді я міг бачити цю проблему, а іноді не міг її знову знайти. Видалення ведучого "./" - це ще один тривіальний крок. Ваш коментар про "немає вбудованих". або .. 'також доречно. Насправді напрочуд складно виконати роботу належним чином - удвічі, якщо будь-яке з назв насправді є символьним посиланням; ми обидва робимо чисто текстовий аналіз.
Джонатан Леффлер

@ Денніс: Звичайно, якщо у вас немає мережі Newcastle Connection, намагатися вийти вище коріння марно, тому ../../../ .. і ../../ .. рівноцінні. Однак це чистий ескапізм; Ваша критика правильна. (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

}

Це Extrememly Portable :)
Анонім

2

Ось моя версія. Він заснований на відповідь по @Offirmo . Я зробив це, сумісний з тире, і виправив наступний збій тестових шаф:

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

Зараз:

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

Дивіться код:

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

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

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

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

    local common_part=$source # for now

    local result=""

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

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

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

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

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

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

    echo $result

    return 0
}

1

Здогадайтесь, цей теж зробить трюк ... (поставляється із вбудованими тестами) :)

Добре, очікується деякий наклад, але ми тут робимо оболонку Борна! ;)

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

Ця відповідь не стосується частини запитання Баша, але оскільки я намагався використовувати відповіді в цьому питанні для реалізації цієї функціональності в 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для тестових випадків, які ви надали.
Gary Wisniewski

-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 вказує на повністю зашиті вікі-сторінки, що містять вміст скрипту кілька разів, переплітаючись з рядками vi tilde, повідомленнями про помилки щодо не знайдених команд та нічого. Цілком марно для когось, хто досліджує оригінал.
Єнс

-1

Мені було потрібно щось подібне, але це також вирішило символічні посилання. Я виявив, що у 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"
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.