Converter caminho absoluto em caminho relativo, dado um diretório atual usando o Bash


261

Exemplo:

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

# Magic

relative="../../bar"

Como crio a mágica (espero que não seja um código muito complicado ...)?


7
Por exemplo (agora, no meu caso), para fornecer um caminho relativo ao gcc para que ele possa gerar informações de depuração relativas úteis, mesmo que o caminho da fonte seja alterado.
Offirmo

Uma pergunta semelhante a esta foi feita em U&L: unix.stackexchange.com/questions/100918/… . Uma das respostas (@Gilles) menciona uma ferramenta, links simbólicos , que podem facilitar o trabalho desse problema.
Slm

25
Simples: realpath --relative-to=$absolute $current.
Kenorb # 4/15

Respostas:


229

Usar o caminho real do GNU coreutils 8.23 ​​é o mais simples, eu acho:

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

Por exemplo:

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

7
É uma pena que o pacote esteja desatualizado no Ubuntu 14.04 e não tenha a opção --relative-to.
Kzh

3
Funciona bem no Ubuntu 16.04
cayhorstmann

7
$ realpath --relative-to="${PWD}" "$file"é útil se você deseja os caminhos relativos ao diretório de trabalho atual.
Dcoles

1
Isso é correto para coisas dentro do /usr/bin/nmap/caminho-mas não para /usr/bin/nmap: de nmappara /tmp/testingele é apenas ../../e não três vezes ../. Funciona no entanto, porque fazer ..no rootfs é /.
Patrick B.

6
Como @PatrickB. implícita, --relative-to=…espera um diretório e NÃO verifica. Isso significa que você acaba com um "../" extra se você solicitar um caminho relativo a um arquivo (como este exemplo parece fazer, porque /usr/binraramente ou nunca contém diretórios e nmapnormalmente é um binário)
IBBoard

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

dá:

../../bar

11
Funciona e faz com que as alternativas pareçam ridículas. Isso é um bônus para mim xD
hasvn

31
+1. Ok, você trapaceou ... mas isso é bom demais para não ser usado! relpath(){ python -c "import os.path; print os.path.relpath('$1','${2:-$PWD}')" ; }
precisa saber é o seguinte

4
Infelizmente, isso não está disponível universalmente: os.path.relpath é novo no Python 2.6.
Chen Levy

15
@ChenLevy: Python 2.6 foi lançado em 2008. É difícil de acreditar que não era universalmente disponível em 2012.
MestreLion

11
python -c 'import os, sys; print(os.path.relpath(*sys.argv[1:]))'funciona de maneira mais natural e confiável.
Musiphil

31

Esse é um aprimoramento corrigido e totalmente funcional da solução atualmente melhor classificada do @pini (que infelizmente lida com apenas alguns casos)

Lembrete: teste '-z' se a sequência tiver comprimento zero (= vazio) e teste '-n' se a sequência não estiver vazia.

# 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

Casos de teste :

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
Integrado no shell offirmo lib github.com/Offirmo/offirmo-shell-lib , função «OSL_FILE_find_relative_path» (arquivo «osl_lib_file.sh»)
Offirmo

1
+1. Ele pode ser facilmente feito para lidar com todos os caminhos (não apenas absolutos caminhos começando com /), substituindo source=$1; target=$2comsource=$(realpath $1); target=$(realpath $2)
Josh Kelley

2
@ Josh, de fato, desde que os diretórios realmente existe ... que foi unconvenient para os testes de unidade;) Mas em uso real, sim, realpathé recomendado, ou source=$(readlink -f $1)etc., se realpath não está disponível (não standard)
Offirmo

Eu defini $sourcee $targetassim: `if [[-e $ 1]]; então source = $ (readlink -f $ 1); outra fonte = $ 1; fi se [[-e $ 2]]; então target = $ (readlink -f $ 2); else target = $ 2; Dessa maneira, a função poderia agrupar caminhos relativos reais / existentes, além de diretórios fictícios.
Nathan S. Watson-Haigh

1
@ NathanS.Watson-Haigh Mesmo melhor, eu descobri recentemente readlinktem uma -mopção que só faz isso;)
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/}

Script maravilhoso - curto e limpo. Apliquei uma edição (Aguardando revisão por pares): common_part = $ source / common_part = $ (dirname $ common_part) / eco $ {back} $ {target # $ common_part} O script existente falharia devido a uma correspondência inadequada no início do nome do diretório ao comparar, por exemplo: "/ foo / bar / baz" a "/ foo / barsucks / bonk". Mover a barra para a var e sair da avaliação final corrige esse bug.
jcwenger

3
Este script simplesmente não funciona. Falha em um simples "um diretório para baixo" testes. As edições do jcwenger funcionam um pouco melhor, mas tendem a adicionar um "../" extra.
Person Person II

1
em alguns casos, falha para mim se um "/" à direita estiver no argumento; por exemplo, se $ 1 = "$ HOME /" e $ 2 = "$ HOME / temp", ele retornará "/ home / user / temp /", mas se $ 1 = $ HOME, retornará corretamente o caminho relativo "temp". Tanto source = $ 1 quanto target = $ 2 podem, portanto, ser "limpos" usando sed (ou usando substituição de variáveis ​​bash, mas isso pode ser desnecessariamente opaco) como => source = $ (echo "$ {1}" | sed / \ / * $ // ')
michael

1
Pequena melhoria: em vez de definir origem / destino diretamente para $ 1 e $ 2, faça: source = $ (cd $ 1; pwd) target = $ (cd $ 2; pwd). Dessa forma, ele lida com caminhos. e .. corretamente.
31711 Joseph Garvin

4
Apesar de ser a resposta mais votada, essa resposta tem muitas limitações, portanto, muitas outras respostas estão sendo postadas. Veja as outras respostas, especialmente a que exibe casos de teste. E por favor, vote este comentário!
Offirmo 01/10/12

25

Ele é incorporado ao Perl desde 2001, portanto funciona em quase todos os sistemas que você pode imaginar, até no VMS .

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

Além disso, a solução é fácil de entender.

Então, para o seu exemplo:

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

... funcionaria bem.


3
saynão estava disponível em perl como log, mas poderia ser usado efetivamente aqui. perl -MFile::Spec -E 'say File::Spec->abs2rel(@ARGV)'
precisa saber é o seguinte

+1, mas veja também essa resposta semelhante, que é mais antiga (fevereiro de 2012). Leia também os comentários pertinentes de William Pursell . Minha versão são duas linhas de comando: perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target"e perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target" "$origin". O primeiro script perl de uma linha usa um argumento (origem é o diretório de trabalho atual). O segundo script perl de uma linha usa dois argumentos.
olibre

3
Essa deve ser a resposta aceita. perlpode ser encontrado em quase todos os lugares, embora a resposta ainda seja uma linha.
Dmitry Ginzburg

19

Presumindo que você tenha instalado: bash, pwd, dirname, echo; então 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/}

Eu joguei a resposta de pini e algumas outras idéias

Nota : Isso requer que os dois caminhos sejam pastas existentes. Arquivos não funcionarão.


2
resposta ideal: trabalhos com / bin / sh, não requer readlink, Python, Perl -> ótimo para sistemas de luz / incorporado ou janelas bater consola
François

2
Infelizmente, isso requer que o caminho exista, o que nem sempre é desejado.
Drwatsoncode 10/10

Resposta piedosa. O material cd-pwd é para resolver links, eu acho? Bom golfe!
Teck-freak

15

Python os.path.relpathcomo uma função shell

O objetivo deste relpathexercício é imitar a os.path.relpathfunção do Python 2.7 (disponível no Python versão 2.6, mas funcionando apenas corretamente no 2.7), conforme proposto por xni . Como conseqüência, alguns dos resultados podem diferir das funções fornecidas em outras respostas.

(Eu não testei com novas linhas nos caminhos simplesmente porque interrompe a validação com base em chamadas python -cdo ZSH. Certamente seria possível com algum esforço.)

Em relação à “mágica” no Bash, eu desisti de procurar magia no Bash há muito tempo, mas desde então encontrei toda a magia que preciso e, em seguida, algumas no ZSH.

Consequentemente, proponho duas implementações.

A primeira implementação visa ser totalmente compatível com POSIX . Eu testei com o /bin/dashDebian 6.0.6 "Squeeze". Também funciona perfeitamente /bin/shno OS X 10.8.3, que na verdade é o Bash versão 3.2, fingindo ser um shell POSIX.

A segunda implementação é uma função de shell ZSH que é robusta contra múltiplas barras e outros incômodos nos caminhos. Se você tiver o ZSH disponível, esta é a versão recomendada, mesmo se você a estiver chamando no formulário de script apresentado abaixo (ou seja, com um shebang de #!/usr/bin/env zsh) de outro shell.

Por fim, escrevi um script ZSH que verifica a saída do relpathcomando encontrado em $PATHdeterminados casos de teste fornecidos em outras respostas. Adicionei um pouco de tempero a esses testes adicionando alguns espaços, tabulações e pontuação, como ! ? *aqui e ali, e também realizei outro teste com caracteres UTF-8 exóticos encontrados no vim-powerline .

Função shell POSIX

Primeiro, a função de shell compatível com POSIX. Ele funciona com vários caminhos, mas não limpa várias barras nem resolve links simbólicos.

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

Função shell ZSH

Agora, a zshversão mais robusta . Se você deseja resolver os argumentos para os caminhos reais à la realpath -f(disponíveis no coreutilspacote Linux ), substitua as :alinhas 3 e 4 por :A.

Para usar isso no zsh, remova a primeira e a última linha e coloque-a em um diretório que esteja na sua $FPATHvariável.

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

Script de teste

Finalmente, o script de teste. Ele aceita uma opção, a saber, -vativar a saída detalhada.

#!/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
Não funciona quando o primeiro caminho termina, /receio.
Noldorin

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

O script shell acima foi inspirado em pini's (obrigado!). Ele dispara um bug no módulo de realce da sintaxe do Stack Overflow (pelo menos no meu quadro de visualização). Portanto, ignore se o realce está incorreto.

Algumas notas:

  • Erros removidos e código aprimorado sem aumentar significativamente o comprimento e a complexidade do código
  • Coloque a funcionalidade em funções para facilitar o uso
  • Funções mantidas compatíveis com POSIX, para que elas funcionem com todos os shells POSIX (testados com dash, bash e zsh no Ubuntu Linux 12.04)
  • Utilizou variáveis ​​locais apenas para evitar sobrecarregar variáveis ​​globais e poluir o espaço para nome global
  • Ambos os caminhos de diretório NÃO precisam existir (requisito para meu aplicativo)
  • Os nomes de caminhos podem conter espaços, caracteres especiais, caracteres de controle, barras invertidas, guias, ', ",?, *, [,] Etc.
  • A função principal "relPath" usa apenas os recursos internos do shell POSIX, mas requer caminhos de diretório absolutos canônicos como parâmetros
  • A função estendida "relpath" pode manipular caminhos de diretório arbitrários (também relativos, não canônicos), mas requer o utilitário principal externo do GNU "readlink"
  • Evitou "eco" interno e usou "printf" interno por dois motivos:
  • Para evitar conversões desnecessárias, os nomes de caminho são usados ​​conforme retornados e esperados pelos utilitários shell e OS (por exemplo, cd, ln, ls, find, mkdir; ao contrário do "os.path.relpath" do python, que interpretará algumas seqüências de barra invertida)
  • Exceto pelas seqüências de barra invertida mencionadas, a última linha da função "relPath" gera nomes de caminho compatíveis com python:

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

    A última linha pode ser substituída (e simplificada) por linha

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

    Eu prefiro o último porque

    1. Os nomes de arquivos podem ser anexados diretamente aos caminhos de diretório obtidos pelo relPath, por exemplo:

      ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
    2. Links simbólicos no mesmo diretório criado com este método não têm o feio "./"anexado ao nome do arquivo.

  • Se você encontrar um erro, entre em contato com linuxball (at) gmail.com e tentarei corrigi-lo.
  • Adicionado conjunto de testes de regressão (também compatível com shell POSIX)

Listagem de código para testes de regressão (basta anexá-lo ao script de shell):

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

Poucas respostas aqui são práticas para o uso diário. Como é muito difícil fazer isso corretamente no bash puro, sugiro a seguinte solução confiável (semelhante a uma sugestão oculta em um comentário):

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

Em seguida, você pode obter o caminho relativo com base no diretório atual:

echo $(relpath somepath)

ou você pode especificar que o caminho seja relativo a um determinado diretório:

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

A única desvantagem é que isso requer python, mas:

  • Funciona de forma idêntica em qualquer python> = 2.6
  • Não requer que os arquivos ou diretórios existam.
  • Os nomes de arquivos podem conter uma variedade maior de caracteres especiais. Por exemplo, muitas outras soluções não funcionam se os nomes de arquivos contiverem espaços ou outros caracteres especiais.
  • É uma função de uma linha que não confunde scripts.

Observe que as soluções que incluem basenameou dirnamepodem não ser necessariamente melhores, pois exigem a coreutilsinstalação. Se alguém tiver uma bashsolução pura que seja confiável e simples (em vez de uma curiosidade complicada), eu ficaria surpreso.


Esta parece ser de longe a abordagem mais robusta.
dimo414 25/04

7

Este script fornece resultados corretos apenas para entradas que são caminhos absolutos ou caminhos relativos sem .ou ..:

#!/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
Isso parece funcionar. Se os diretórios realmente existirem, o uso de $ (readlink -f $ 1) e $ (readlink -f $ 2) nas entradas pode corrigir o problema em que "." ou ".." aparece nas entradas. Isso pode causar alguns problemas se os diretórios não existirem realmente.
Dr. Person Person II

7

Eu apenas usaria Perl para esta tarefa não tão trivial:

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

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

1
+1, mas recomendaria: perl -MFile::Spec -e "print File::Spec->abs2rel('$absolute','$current')"para que absoluto e atual sejam citados.
precisa saber é o seguinte

Eu gosto relative=$(perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$absolute" "$current"). Isso garante que os valores não possam, eles próprios, conter código perl!
Erik Aronesty 9/09/15

6

Uma ligeira melhoria nas respostas de kasku e Pini , que são mais agradáveis ​​com os espaços e permitem a passagem de caminhos relativos:

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

Teste:

$ ./test.sh 
TEST=/home/ubuntu/.//blah
TMP=/home/ubuntu/blah
REL=blah

+1 para compacidade e bash-ness. Você deve, no entanto, também chamada readlinkno $(pwd).
precisa saber é o seguinte

2
Relativo não significa que o arquivo deve ser colocado no mesmo diretório.
greenoldman

embora a pergunta original não forneça muitos casos de teste, esse script falha em testes simples, como encontrar o caminho relativo de / home / user1 para / home / user2 (resposta correta: ../user2). O script de pini / jcwenger funciona para este caso.
28612 Michael

4

Mais uma solução, pura bash+ GNU, readlinkpara fácil utilização no seguinte contexto:

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

Editar: verifique se "$ B" não existe ou não possui softlink nesse caso; caso contrário, relpathsiga este link que não é o que você deseja!

Isso funciona em quase todo o Linux atual. Se readlink -mnão funcionar ao seu lado, tente readlink -f. Consulte também https://gist.github.com/hilbix/1ec361d00a8178ae8ea0 para obter possíveis atualizações:

: 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:-.}"
}

Notas:

  • Foi tomado cuidado para que ele seja seguro contra a expansão indesejada de metacaracteres do shell, caso os nomes de arquivos contenham *ou ?.
  • A saída deve ser usada como o primeiro argumento para ln -s:
    • relpath / /.e não a string vazia
    • relpath a aa, mesmo que aseja um diretório
  • Os casos mais comuns foram testados para fornecer resultados razoáveis ​​também.
  • Esta solução usa correspondência de prefixo de string, portanto, readlinké necessária para canonizar caminhos.
  • Graças a readlink -mele, também funciona para caminhos ainda não existentes.

Nos sistemas antigos, onde readlink -mnão está disponível, readlink -ffalhará se o arquivo não existir. Portanto, você provavelmente precisará de uma solução alternativa como esta (não testada!):

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

Isso não é realmente muito correto no caso de $1incluir .ou ..para caminhos inexistentes (como em /doesnotexist/./a), mas deve cobrir a maioria dos casos.

(Substitua readlink -m --acima por readlink_missing.)

Editar por causa do voto negativo a seguir

Aqui está um teste, de que essa função, de fato, está correta:

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"

Intrigado? Bem, estes são os resultados corretos ! Mesmo se você acha que isso não se encaixa na pergunta, aqui está a prova de que isso está correto:

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

Sem dúvida, ../baré o caminho relativo exato e único correto da página barvisto a partir da página moo. Tudo o resto estaria completamente errado.

É trivial adotar a saída para a pergunta que aparentemente assume, que currenté um diretório:

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

Isso retorna exatamente o que foi solicitado.

E antes que você levante uma sobrancelha, aqui está uma variante um pouco mais complexa de relpath(identifique a pequena diferença), que também deve funcionar para a sintaxe de URL (para que uma trilha /sobreviva, graças a alguns 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:-.}"
}

E aqui estão as verificações apenas para deixar claro: ele realmente funciona como dito.

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

E aqui está como isso pode ser usado para fornecer o resultado desejado da pergunta:

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

Se você encontrar algo que não funciona, entre em contato nos comentários abaixo. Obrigado.

PS:

Por que os argumentos de relpath"invertido" contrastam com todas as outras respostas aqui?

Se você mudar

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

para

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

então você pode deixar o segundo parâmetro ausente, de modo que o BASE seja o diretório / URL atual / o que for. Esse é apenas o princípio Unix, como de costume.

Se você não gostar disso, volte ao Windows. Obrigado.


3

Infelizmente, a resposta de Mark Rushakoff (agora excluída - referenciou o código daqui ) não parece funcionar corretamente quando adaptada para:

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

O pensamento descrito no comentário pode ser refinado para fazê-lo funcionar corretamente na maioria dos casos. Estou prestes a assumir que o script usa um argumento de origem (onde você está) e um argumento de destino (onde você deseja chegar), e que ambos são nomes de caminho absolutos ou ambos são relativos. Se um for absoluto e o outro relativo, o mais fácil é prefixar o nome relativo no diretório de trabalho atual - mas o código abaixo não faz isso.


Cuidado

O código abaixo está quase funcionando corretamente, mas não está certo.

  1. Há o problema abordado nos comentários de Dennis Williamson.
  2. Há também um problema que esse processamento puramente textual de nomes de caminho e você pode ser seriamente confuso com links simbólicos estranhos.
  3. O código não manipula 'pontos' dispersos em caminhos como ' xyz/./pqr'.
  4. O código não manipula 'pontos duplos' perdidos em caminhos como ' xyz/../pqr'.
  5. Trivialmente: o código não remove os principais ' ./' dos caminhos.

O código de Dennis é melhor porque corrige 1 e 5 - mas tem os mesmos problemas 2, 3, 4. Use o código de Dennis (e faça uma votação antecipada antes disso) por causa disso.

(NB: POSIX fornece uma chamada do sistema realpath()que resolve nomes de caminho para que não haja links simbólicos neles. Aplicar isso aos nomes de entrada e usar o código de Dennis daria a resposta correta a cada vez. É trivial escrever o código C que wraps realpath()- eu já fiz - mas não conheço um utilitário padrão que faça isso.)


Por isso, considero o Perl mais fácil de usar do que o shell, embora o bash tenha um suporte decente para matrizes e provavelmente também possa fazer isso - exercício para o leitor. Portanto, dados dois nomes compatíveis, divida-os em componentes:

  • Defina o caminho relativo como vazio.
  • Enquanto os componentes são os mesmos, pule para o próximo.
  • Quando os componentes correspondentes são diferentes ou não há mais componentes para um caminho:
  • Se não houver componentes de origem restantes e o caminho relativo estiver vazio, adicione "." para o começo.
  • Para cada componente de origem restante, prefixe o caminho relativo com "../".
  • Se não houver componentes de destino restantes e o caminho relativo estiver vazio, adicione "." para o começo.
  • Para cada componente de destino restante, adicione o componente ao final do caminho após uma barra.

Portanto:

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

Script de teste (os colchetes contêm um espaço em branco e uma guia):

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

Saída do script de teste:

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

Esse script Perl funciona bastante bem no Unix (não leva em consideração todas as complexidades dos nomes de caminho do Windows) diante de entradas estranhas. Ele usa o módulo Cwde sua função realpathpara resolver o caminho real dos nomes que existem e faz uma análise textual dos caminhos que não existem. Em todos os casos, exceto um, ele produz a mesma saída que o script de Dennis. O caso desviante é:

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

Os dois resultados são equivalentes - apenas não idênticos. (A saída é de uma versão levemente modificada do script de teste - o script Perl abaixo simplesmente imprime a resposta, em vez das entradas e a resposta como no script acima.) Agora: devo eliminar a resposta que não está funcionando? Talvez...

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

Seu /home/part1/part2para /tem um demais ../. Caso contrário, meu script corresponde à sua saída, exceto a minha adiciona um desnecessário .no final daquele em que o destino está .e eu não uso um ./no início dos que descem sem subir.
Pausado até novo aviso.

@Dennis: Passei um tempo olhando os resultados - às vezes eu pude ver esse problema e às vezes não consegui encontrá-lo novamente. Remover um './' inicial é outro passo trivial. O seu comentário sobre 'não incorporado. ou .. 'também é pertinente. É realmente surpreendentemente difícil fazer o trabalho corretamente - duplamente, se algum dos nomes é realmente um link simbólico; nós dois estamos fazendo uma análise puramente textual.
Jonathan Leffler

@Dennis: Obviamente, a menos que você tenha uma rede Newcastle Connection, tentar ficar acima da raiz é inútil, então ../../../ .. e ../../ .. são equivalentes. No entanto, isso é puro escapismo; sua crítica está correta. (O Newcastle Connection permitiu configurar e usar a notação /../host/path/on/remote/machine para chegar a um host diferente - um esquema interessante. Acredito que ele suportava /../../network/host/ path / on / remoto / network / e / host também está na Wikipedia)..
Jonathan Leffler

Portanto, agora temos a barra dupla de UNC.
Pausado até novo aviso.

1
O utilitário "readlink" (pelo menos a versão GNU) pode fazer o equivalente a realpath (), se você passar a opção "-f". Por exemplo, no meu sistema, readlink /usr/bin/vi/etc/alternatives/vi, mas isso é outra ligação simbólica - ao passo que readlink -f /usr/bin/vi/usr/bin/vim.basic, que é o destino final de todos os links simbólicos ...
psmears

3

Tomei sua pergunta como um desafio para escrever isso em código shell "portátil", ou seja,

  • com um shell POSIX em mente
  • sem basismos como matrizes
  • evite ligar para pessoas externas como a praga. Não há um único garfo no script! Isso o torna incrivelmente rápido, especialmente em sistemas com sobrecarga significativa do garfo, como o cygwin.
  • Deve lidar com caracteres glob nos nomes de caminho (*,?, [,])

É executado em qualquer shell compatível com POSIX (zsh, bash, ksh, ash, busybox, ...). Ele ainda contém um conjunto de testes para verificar seu funcionamento. A canonização dos nomes de caminho é deixada como um exercício. :-)

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

Minha solução:

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

}

Isto é extremamente portátil :)
Anônimo

2

Aqui está a minha versão. É baseado na resposta de @Offirmo . Tornei-o compatível com o Dash e corrigi a seguinte falha do testcase:

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

Agora:

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

Veja o código:

# 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

Acho que esse também deve fazer o truque ... (vem com testes internos) :)

OK, espera-se alguma sobrecarga, mas estamos fazendo o Bourne aqui! ;)

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

Este script funciona apenas nos nomes dos caminhos. Ele não requer que nenhum dos arquivos exista. Se os caminhos transmitidos não forem absolutos, o comportamento é um pouco incomum, mas deve funcionar como esperado se os dois caminhos forem relativos.

Eu o testei apenas no OS X, portanto, pode não ser portátil.

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

Além disso, o código é um pouco copiado e colado, mas eu precisava disso rapidamente.
juancn

Bom ... exatamente o que eu precisava. E você incluiu uma rotina de canonização que é melhor do que a maioria das outras que eu já vi (que geralmente dependem de substituições de regex).
Drwatsoncode 10/10

0

Esta resposta não aborda a parte Bash da pergunta, mas como tentei usar as respostas nesta pergunta para implementar essa funcionalidade no Emacs, eu a divulgarei.

O Emacs realmente tem uma função para isso imediatamente:

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"

Observe que acredito que a resposta python que adicionei recentemente (a relpathfunção) se comporta de forma idêntica file-relative-namenos casos de teste que você forneceu.
Gary Wisniewski

-1

Aqui está um script de shell que faz isso sem chamar outros programas:

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

fonte: http://www.ynform.org/w/Pub/Relpath


1
Isso não é realmente portátil devido ao uso da construção $ {param / pattern / subst}, que não é POSIX (a partir de 2011).
Jens

A fonte referenciada ynform.org/w/Pub/Relpath aponta para uma página wiki completamente distorcida que contém o conteúdo do script várias vezes, misturado com linhas de vi til, mensagens de erro sobre comandos não encontrados e outros enfeites. Totalmente inútil para alguém pesquisando o original.
Jens

-1

Eu precisava de algo assim, mas que resolvesse links simbólicos também. Descobri que o pwd tem um sinalizador -P para esse fim. Um fragmento do meu script é anexado. Está dentro de uma função em um script de shell, daí $ 1 e $ 2. O valor do resultado, que é o caminho relativo de START_ABS para END_ABS, está na variável UPDIRS. O script cd está em cada diretório de parâmetros para executar o pwd -P e isso também significa que os parâmetros do caminho relativo são tratados. Cheers, Jim

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"
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.