Converti percorso assoluto in percorso relativo dato una directory corrente usando Bash


261

Esempio:

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

# Magic

relative="../../bar"

Come faccio a creare la magia (si spera non un codice troppo complicato ...)?


7
Ad esempio (il mio caso in questo momento) per dare il percorso relativo di gcc in modo che possa generare informazioni relative al debug utilizzabili anche se cambia il percorso di origine.
Offirmo,

Una domanda simile a questa è stata posta su U&L: unix.stackexchange.com/questions/100918/… . Una delle risposte (@Gilles) menziona uno strumento, i collegamenti simbolici , che possono facilitare il lavoro di questo problema.
slm

25
Semplice: realpath --relative-to=$absolute $current.
Kenorb,

Risposte:


229

Usare realpath da GNU coreutils 8.23 ​​è il più semplice, penso:

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

Per esempio:

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

7
È un peccato che il pacchetto sia obsoleto su Ubuntu 14.04 e non abbia l'opzione --relative-to.
kzh

3
Funziona bene su Ubuntu 16.04
cayhorstmann,

7
$ realpath --relative-to="${PWD}" "$file"è utile se si desidera i percorsi relativi alla directory di lavoro corrente.
dcoles

1
Questo è corretto per le cose all'interno del /usr/bin/nmap/percorso ma non per /usr/bin/nmap: da nmapa /tmp/testingè solo ../../e non 3 volte ../. Funziona comunque, perché farlo ..su rootfs lo è /.
Patrick B.

6
Come @PatrickB. sottinteso, si --relative-to=…aspetta una directory e NON controlla. Ciò significa che si finisce con un "../" extra se si richiede un percorso relativo a un file (come sembra fare questo esempio, perché /usr/binraramente o mai contiene directory ed nmapè normalmente binario)
IBBoard

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

dà:

../../bar

11
Funziona e rende le alternative ridicole. Questo è un bonus per me xD
hasvn

31
+1. Ok, hai tradito ... ma è troppo bello per non essere usato! relpath(){ python -c "import os.path; print os.path.relpath('$1','${2:-$PWD}')" ; }
MestreLion,

4
Purtroppo, questo non è disponibile universalmente: os.path.relpath è nuovo in Python 2.6.
Chen Levy,

15
@ChenLevy: Python 2.6 è stato rilasciato nel 2008. Difficile credere che non fosse universalmente disponibile nel 2012.
MestreLion,

11
python -c 'import os, sys; print(os.path.relpath(*sys.argv[1:]))'funziona in modo più naturale e affidabile.
musiphil,

31

Questo è un miglioramento corretto e completamente funzionale della soluzione attualmente più votata da @pini (che purtroppo gestisce solo pochi casi)

Promemoria: '-z' verifica se la stringa è di lunghezza zero (= vuota) e '-n' verifica se la stringa non è vuota.

# 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

Casi test :

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
Integrato nella shell offirmo lib github.com/Offirmo/offirmo-shell-lib , funzione «OSL_FILE_find_relative_path» (file «osl_lib_file.sh»)
Offirmo

1
+1. Può essere facilmente creato per gestire qualsiasi percorso (non solo percorsi assoluti che iniziano con /) sostituendolo source=$1; target=$2consource=$(realpath $1); target=$(realpath $2)
Josh Kelley,

2
@Josh davvero, a condizione che i dirs esistano effettivamente ... il che era poco comodo per i test unitari;) Ma in uso reale si, realpathè raccomandato, source=$(readlink -f $1)ecc. Se realpath non è disponibile (non standard)
Offirmo

Ho definito $sourcee $targetcosì: `if [[-e $ 1]]; quindi source = $ (readlink -f $ 1); else source = $ 1; fi if [[-e $ 2]]; quindi target = $ (readlink -f $ 2); altro target = $ 2; fi` In questo modo, la funzione potrebbe ostacolare percorsi relativi reali / esistenti e directory fittizie.
Nathan S. Watson-Haigh,

1
@ NathanS.Watson-Haigh Ancora meglio, ho scoperto di recente che readlinkha -mun'opzione che fa proprio questo;)
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/}

Sceneggiatura meravigliosa: breve e pulita. Ho applicato una modifica (Waiting on peer review): common_part = $ source / common_part = $ (dirname $ common_part) / echo $ {back} $ {target # $ common_part} Lo script esistente fallirebbe a causa di una corrispondenza inappropriata all'inizio del nome della directory quando si confronta, ad esempio: "/ foo / bar / baz" a "/ foo / barsucks / bonk". Spostare la barra nel var e fuori dalla valutazione finale corregge quel bug.
jcwenger,

3
Questo script semplicemente non funziona. Non ha superato un semplice test "una directory in giù". Le modifiche di jcwenger funzionano un po 'meglio ma tendono ad aggiungere un ulteriore "../".
Dr. Person Person II

1
in alcuni casi non riesce se un "/" finale è sull'argomento; ad esempio, se $ 1 = "$ HOME /" e $ 2 = "$ HOME / temp", restituisce "/ home / user / temp /", ma se $ 1 = $ HOME restituisce correttamente il relativo percorso "temp". Sia source = $ 1 che target = $ 2 potrebbero quindi essere "ripuliti" usando sed (o usando la sostituzione della variabile bash, ma ciò può essere inutilmente opaco) come => source = $ (echo "$ {1}" | sed 's / \ / * $ // ')
michael

1
Miglioramento minore: invece di impostare sorgente / target direttamente su $ 1 e $ 2, fare: source = $ (cd $ 1; pwd) target = $ (cd $ 2; pwd). In questo modo gestisce i percorsi con. e ... correttamente.
Joseph Garvin,

4
Nonostante sia la risposta più votata, questa risposta presenta molte limitazioni, quindi vengono pubblicate molte altre risposte. Vedi invece le altre risposte, in particolare quella che mostra i casi di test. E per favore vota questo commento!
Offirmo,

25

È integrato in Perl dal 2001, quindi funziona su quasi tutti i sistemi che puoi immaginare, anche su VMS .

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

Inoltre, la soluzione è facile da capire.

Quindi, per il tuo esempio:

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

... funzionerebbe benissimo.


3
saynon è disponibile in perl come log, ma potrebbe essere utilizzato in modo efficace qui. perl -MFile::Spec -E 'say File::Spec->abs2rel(@ARGV)'
William Pursell,

+1 ma vedi anche questa risposta simile che è più vecchia (febbraio 2012). Leggi anche i commenti pertinenti di William Pursell . La mia versione sono due righe di comando: perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target"e perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target" "$origin". Il primo script perl a una riga utilizza un argomento (l'origine è la directory di lavoro corrente). Il secondo script perl a una riga utilizza due argomenti.
olibre,

3
Questa dovrebbe essere la risposta accettata. perlpuò essere trovato quasi ovunque, anche se la risposta è ancora una linea.
Dmitry Ginzburg,

19

Presumendo che tu abbia installato: bash, pwd, dirname, echo; allora 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/}

Ho cercato la risposta di Pini e alcune altre idee

Nota : ciò richiede che entrambi i percorsi siano cartelle esistenti. I file non funzioneranno.


2
risposta ideale: funziona con / bin / sh, non richiede readlink, python, perl -> ottimo per sistemi light / embedded o windows bash console
Francois,

2
Sfortunatamente, ciò richiede l'esistenza del percorso, che non è sempre desiderato.
drwatsoncode

Risposta divina. La roba cd-pwd è per risolvere i collegamenti immagino? Bel golf!
Teck-maniaco del

15

Python os.path.relpath una funzione shell

L'obiettivo di questo relpathesercizio è imitare la os.path.relpathfunzione di Python 2.7 (disponibile dalla versione 2.6 di Python ma funziona correttamente solo in 2.7), come proposto da xni . Di conseguenza, alcuni dei risultati potrebbero differire dalle funzioni fornite in altre risposte.

(Non ho testato i newline nei percorsi semplicemente perché interrompe la convalida basata sulla chiamata python -cda ZSH. Sarebbe certamente possibile con un certo sforzo.)

Per quanto riguarda la "magia" in Bash, ho smesso di cercare la magia in Bash molto tempo fa, ma da allora ho trovato tutta la magia di cui ho bisogno, e poi alcuni, in ZSH.

Di conseguenza, propongo due implementazioni.

La prima implementazione mira ad essere pienamente conforme a POSIX . L'ho provato con /bin/dashDebian 6.0.6 "Squeeze". Funziona perfettamente anche con/bin/sh OS X 10.8.3, che in realtà è la versione 3.2 di Bash che finge di essere una shell POSIX.

La seconda implementazione è una funzione shell ZSH che è robusta contro più barre e altri fastidi nei percorsi. Se hai ZSH disponibile, questa è la versione consigliata, anche se la stai chiamando nel modulo di script presentato di seguito (cioè con un shebang di #!/usr/bin/env zsh) da un'altra shell.

Infine, ho scritto uno script ZSH che verifica l'output del relpathcomando trovato in $PATHdati i casi di test forniti in altre risposte. Ho aggiunto un po 'di pepe a quei test aggiungendo spazi, tabulazioni e punteggiatura come ! ? *qua e là e ho anche lanciato un altro test con caratteri UTF-8 esotici trovati in vim-powerline .

Funzione shell POSIX

Innanzitutto, la funzione shell conforme a POSIX. Funziona con una varietà di percorsi, ma non pulisce più barre o risolve collegamenti simbolici.

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

Funzione shell ZSH

Ora, la zshversione più robusta . Se desideri che risolva gli argomenti in percorsi reali alla realpath -f(disponibili nel coreutilspacchetto Linux ), sostituisci le :arighe 3 e 4 con:A .

Per usarlo in zsh, rimuovi la prima e l'ultima riga e inseriscile in una directory nella tua $FPATHvariabile.

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

Test script

Infine, lo script di test. Accetta un'opzione, vale -va dire abilitare un output dettagliato.

#!/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
Non funziona quando il primo percorso finisce, /temo.
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")"; }

Lo script Above shell è stato ispirato da Pini (grazie!). Attiva un bug nel modulo di evidenziazione della sintassi di Stack Overflow (almeno nel mio riquadro di anteprima). Quindi ignora se l'evidenziazione non è corretta.

Alcune note:

  • Rimossi errori e codice migliorato senza aumentare significativamente la lunghezza e la complessità del codice
  • Metti la funzionalità in funzioni per facilità d'uso
  • Mantenuto le funzioni compatibili POSIX in modo che (dovrebbero) funzionare con tutte le shell POSIX (testato con dash, bash e zsh in Ubuntu Linux 12.04)
  • Utilizzato le variabili locali solo per evitare di ostruire le variabili globali e di inquinare lo spazio dei nomi globale
  • Entrambi i percorsi di directory NON DEVONO esistere (requisito per la mia applicazione)
  • I nomi dei percorsi possono contenere spazi, caratteri speciali, caratteri di controllo, barre rovesciate, schede, ', ",?, *, [,], Ecc.
  • La funzione principale "relPath" utilizza solo i builtin della shell POSIX ma richiede come parametri percorsi di directory assoluti canonici
  • La funzione estesa "relpath" può gestire percorsi di directory arbitrari (anche relativi, non canonici) ma richiede un'utilità di base GNU esterna "readlink"
  • Evitato il builtin "echo" e il builtin "printf" invece per due motivi:
  • Per evitare conversioni non necessarie, i percorsi vengono utilizzati man mano che vengono restituiti e previsti dalle utilità della shell e del sistema operativo (ad es. Cd, ln, ls, find, mkdir; a differenza di "os.path.relpath" di Python che interpreterà alcune sequenze di barre rovesciate)
  • Ad eccezione delle citate sequenze di barre rovesciate, l'ultima riga della funzione "relPath" genera nomi di percorso compatibili con Python:

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

    L'ultima riga può essere sostituita (e semplificata) per riga

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

    Preferisco quest'ultima perché

    1. I nomi dei file possono essere aggiunti direttamente ai percorsi dir ottenuti da relPath, ad esempio:

      ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
    2. I collegamenti simbolici nella stessa directory creata con questo metodo non hanno il brutto "./"anteposto al nome del file.

  • Se trovi un errore, contatta linuxball (at) gmail.com e proverò a risolverlo.
  • Aggiunta suite di test di regressione (compatibile anche con shell POSIX)

Elenco dei codici per i test di regressione (basta aggiungerlo allo script della 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

Non molte risposte qui sono pratiche per l'uso quotidiano. Dal momento che è molto difficile farlo correttamente in bash puro, suggerisco la seguente soluzione affidabile (simile a un suggerimento sepolto in un commento):

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

Quindi, è possibile ottenere il percorso relativo in base alla directory corrente:

echo $(relpath somepath)

oppure puoi specificare che il percorso sia relativo a una determinata directory:

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

L'unico svantaggio è che richiede Python, ma:

  • Funziona in modo identico in qualsiasi pitone> = 2.6
  • Non richiede l'esistenza di file o directory.
  • I nomi dei file possono contenere una gamma più ampia di caratteri speciali. Ad esempio, molte altre soluzioni non funzionano se i nomi dei file contengono spazi o altri caratteri speciali.
  • È una funzione a riga singola che non ingombra gli script.

Si noti che le soluzioni che includono basenameo dirnamepotrebbero non essere necessariamente migliori, in quanto richiedono l' coreutilsinstallazione. Se qualcuno ha una bashsoluzione pura , affidabile e semplice (piuttosto che una curiosità contorta), sarei sorpreso.


Questo sembra di gran lunga l'approccio più solido.
dimo414,

7

Questo script fornisce risultati corretti solo per input che sono percorsi assoluti o percorsi relativi senza .o ..:

#!/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
Questo sembra funzionare. Se le directory effettivamente esistono, l'uso di $ (readlink -f $ 1) e $ (readlink -f $ 2) sugli input può risolvere il problema dove "." o ".." appare negli ingressi. Ciò può causare alcuni problemi se le directory non esistono realmente.
Dr. Person Person II,

7

Vorrei solo usare Perl per questo compito non così banale:

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

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

1
+1, ma raccomanderei: in perl -MFile::Spec -e "print File::Spec->abs2rel('$absolute','$current')"modo che siano quotati assoluti e attuali.
William Pursell,

Mi piace relative=$(perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$absolute" "$current"). Questo assicura che i valori non possano essi stessi contenere codice perl!
Erik Aronesty,

6

Un leggero miglioramento delle risposte di Kasku e Pini , che gioca meglio con gli spazi e permette di percorrere percorsi relativi:

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

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

+1 per compattezza e timidezza. Si dovrebbe, tuttavia, chiamata anche readlinksu $(pwd).
DevSolar,

2
Relativo non significa che il file deve essere inserito nella stessa directory.
Greenoldman,

sebbene la domanda originale non fornisca molti test, questo script fallisce per semplici test come trovare il percorso relativo da / home / user1 a / home / user2 (risposta corretta: ../user2). La sceneggiatura di pini / jcwenger funziona per questo caso.
michael

4

Ancora un'altra soluzione, pure bash+ GNU readlinkper un facile utilizzo nel seguente contesto:

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

Modifica: assicurati che "$ B" non sia esistente o che non ci sia softlink in quel caso, altrimenti relpathsegue questo link che non è quello che vuoi!

Questo funziona in quasi tutti gli attuali Linux. Se readlink -mnon funziona al tuo fianco, prova readlink -finvece. Vedi anche https://gist.github.com/hilbix/1ec361d00a8178ae8ea0 per eventuali aggiornamenti:

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

Appunti:

  • È stato fatto attenzione che sia sicuro contro l'espansione indesiderata di meta caratteri shell, nel caso in cui i nomi dei file contengano *o? .
  • L'output è pensato per essere utilizzabile come primo argomento per ln -s:
    • relpath / /.e non la stringa vuota
    • relpath a aa, anche se acapita di essere una directory
  • I casi più comuni sono stati testati per fornire anche risultati ragionevoli.
  • Questa soluzione utilizza la corrispondenza del prefisso di stringa, quindi readlink è necessaria per canonicalizzare i percorsi.
  • Grazie a readlink -m esso funziona anche per percorsi non ancora esistenti.

Sui vecchi sistemi, dove readlink -mnon è disponibile, readlink -ffallisce se il file non esiste. Quindi probabilmente hai bisogno di qualche soluzione alternativa come questa (non testata!):

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

Questo non è proprio corretto nel caso $1includa .o.. per percorsi inesistenti (come in/doesnotexist/./a ), ma dovrebbe coprire la maggior parte dei casi.

(Sostituire readlink -m -- sopra conreadlink_missing .)

Modifica a causa del downvote segue

Ecco un test, che questa funzione, in effetti, è corretta:

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"

Perplesso? Bene, questi sono i risultati corretti ! Anche se pensi che non si adatti alla domanda, ecco la prova che è corretto:

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

Senza alcun dubbio, ../barè il percorso relativo esatto e solo corretto della pagina barvista dalla paginamoo . Tutto il resto sarebbe chiaramente sbagliato.

È banale adottare l'output alla domanda che apparentemente assume, ovvero currentuna directory:

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

Questo ritorna esattamente, ciò che è stato chiesto.

E prima di alzare un sopracciglio, ecco una variante un po 'più complessa di relpath(individuare la piccola differenza), che dovrebbe funzionare anche per la sintassi URL (quindi un trailing /sopravvive, grazie ad alcuni 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:-.}"
}

Ed ecco i controlli solo per chiarire: funziona davvero come detto.

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

Ed ecco come questo può essere usato per dare il risultato desiderato dalla domanda:

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

Se trovi qualcosa che non funziona, faccelo sapere nei commenti qui sotto. Grazie.

PS:

Perché sono gli argomenti di relpath "invertiti" sono in contrasto con tutte le altre risposte qui?

Se cambi

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

per

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

quindi puoi lasciare il secondo parametro, in modo tale che BASE sia la directory / URL corrente / qualunque cosa. Questo è solo il principio Unix, come al solito.

Se non ti piace, torna a Windows. Grazie.


3

Purtroppo, la risposta di Mark Rushakoff (ora eliminata - fa riferimento al codice da qui ) non sembra funzionare correttamente se adattata a:

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

Il pensiero delineato nel commento può essere perfezionato per farlo funzionare correttamente nella maggior parte dei casi. Sto per presumere che lo script prende un argomento di origine (dove ti trovi) e un argomento di destinazione (dove vuoi arrivare), e che entrambi sono nomi di percorso assoluti o entrambi sono relativi. Se uno è assoluto e l'altro relativo, la cosa più semplice è anteporre il nome relativo alla directory di lavoro corrente, ma il codice seguente non lo fa.


diffidare

Il codice seguente funziona quasi correttamente, ma non è del tutto corretto.

  1. C'è il problema affrontato nei commenti di Dennis Williamson.
  2. C'è anche un problema che questa elaborazione puramente testuale di nomi di percorsi e tu puoi essere seriamente incasinata da strani collegamenti simbolici.
  3. Il codice non gestisce i 'punti' vaganti in percorsi come ' xyz/./pqr'.
  4. Il codice non gestisce i "punti doppi" vaganti in percorsi come " xyz/../pqr".
  5. In sostanza: il codice non rimuove i principali " ./" dai percorsi.

Il codice di Dennis è migliore perché corregge 1 e 5, ma ha gli stessi problemi 2, 3, 4. Usa il codice di Dennis (e votalo prima di questo) per questo.

(NB: POSIX fornisce una chiamata di sistema realpath()che risolve i nomi dei percorsi in modo che non ci siano collegamenti simbolici in essi. Applicarlo ai nomi di input e quindi utilizzare il codice di Dennis darebbe la risposta corretta ogni volta. È banale scrivere il codice C che avvolge realpath()- l'ho fatto - ma non conosco un'utilità standard che lo fa.)


Per questo, trovo Perl più facile da usare rispetto alla shell, sebbene bash abbia un supporto decente per gli array e probabilmente potrebbe farlo anche questo - esercizio per il lettore. Quindi, dati due nomi compatibili, dividerli ciascuno in componenti:

  • Impostare il percorso relativo su vuoto.
  • Mentre i componenti sono uguali, passare al successivo.
  • Quando i componenti corrispondenti sono diversi o non ci sono più componenti per un percorso:
  • Se non sono presenti componenti di origine rimanenti e il percorso relativo è vuoto, aggiungere "." all'inizio.
  • Per ogni componente di origine rimanente, aggiungere il prefisso relativo al percorso con "../".
  • Se non ci sono componenti target rimanenti e il percorso relativo è vuoto, aggiungere "." all'inizio.
  • Per ogni componente di destinazione rimanente, aggiungere il componente alla fine del percorso dopo una barra.

Così:

#!/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 di test (le parentesi quadre contengono uno spazio e una scheda):

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

Output dallo script di test:

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

Questo script Perl funziona in modo abbastanza completo su Unix (non tiene conto di tutte le complessità dei nomi dei percorsi di Windows) di fronte a strani input. Utilizza il modulo Cwde la sua funzione realpathper risolvere il percorso reale dei nomi esistenti e fa un'analisi testuale per i percorsi che non esistono. In tutti i casi tranne uno, produce lo stesso output della sceneggiatura di Dennis. Il caso deviante è:

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

I due risultati sono equivalenti, ma non identici. (L'output proviene da una versione leggermente modificata dello script di test: lo script Perl di seguito stampa semplicemente la risposta, anziché gli input e la risposta come nello script sopra.) Ora: dovrei eliminare la risposta non funzionante? Può essere...

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

Il vostro /home/part1/part2a /ha uno di troppo ../. Altrimenti, il mio script corrisponde al tuo output tranne il mio aggiunge un inutile .alla fine di quello in cui si trova la destinazione .e non uso un ./all'inizio di quelli che scendono senza salire.
In pausa fino a nuovo avviso.

@Dennis: ho passato del tempo a sbirciare i risultati - a volte potevo vedere quel problema, a volte non riuscivo a ritrovarlo. Rimuovere un './' iniziale è un altro passo banale. Il tuo commento su "no incorporato. o .. 'è anche pertinente. In realtà è sorprendentemente difficile svolgere correttamente il lavoro - doppiamente se uno qualsiasi dei nomi è in realtà un collegamento simbolico; stiamo entrambi facendo analisi puramente testuali.
Jonathan Leffler,

@Dennis: Certo, a meno che tu non abbia una rete Newcastle Connection, cercare di andare al di sopra di root è inutile, quindi ../../../ .. e ../../ .. sono equivalenti. Tuttavia, questa è pura evasione; la tua critica è corretta. (Newcastle Connection ti ha permesso di configurare e utilizzare la notazione /../host/path/on/remote/machine per arrivare a un host diverso - uno schema accurato. Credo che sia supportato /../../network/host/ percorso / su / remoto / rete / e / host. È su Wikipedia.)
Jonathan Leffler

Quindi, invece, ora abbiamo la doppia barra dell'UNC.
In pausa fino a nuovo avviso.

1
L'utility "readlink" (almeno la versione GNU) può fare l'equivalente di realpath (), se gli passi l'opzione "-f". Ad esempio, sul mio sistema, readlink /usr/bin/vi/etc/alternatives/vi, ma questo è un altro collegamento simbolico - mentre readlink -f /usr/bin/vi/usr/bin/vim.basic, che è la destinazione finale di tutti i
collegamenti

3

Ho preso la tua domanda come una sfida per scrivere questo nel codice della shell "portatile", cioè

  • con una shell POSIX in mente
  • nessun basismo come array
  • evitare di chiamare gli esterni come la peste. Non c'è un singolo fork nella sceneggiatura! Ciò lo rende straordinariamente veloce, specialmente su sistemi con un significativo overhead della forcella, come Cygwin.
  • Deve occuparsi dei personaggi glob nei nomi dei percorsi (*,?, [,])

Funziona su qualsiasi shell POSIX conforme (zsh, bash, ksh, ash, busybox, ...). Contiene anche una suite di test per verificarne il funzionamento. La canonizzazione dei nomi dei percorsi è lasciata come un esercizio. :-)

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

La mia soluzione:

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

}

Questo è estremamente portatile :)
Anonimo

2

Ecco la mia versione Si basa sulla risposta di @Offirmo . L'ho reso compatibile con Dash e risolto il seguente errore testcase:

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

Adesso:

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

Vedi il codice:

# 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

Suppongo che anche questo debba fare il trucco ... (viene fornito con i test integrati) :)

OK, ci si aspetta un po 'di sovraccarico, ma stiamo facendo Bourne shell qui! ;)

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

Questo script funziona solo sui nomi dei percorsi. Non richiede l'esistenza di alcun file. Se i percorsi passati non sono assoluti, il comportamento è un po 'insolito, ma dovrebbe funzionare come previsto se entrambi i percorsi sono relativi.

L'ho testato solo su OS X, quindi potrebbe non essere portatile.

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

Inoltre, il codice è un po 'copia e incolla, ma ne avevo bisogno piuttosto rapidamente.
juancn,

Bello ... proprio quello di cui avevo bisogno. E hai incluso una routine canonicalizzante che è migliore della maggior parte degli altri che ho visto (che di solito si basano su sostituzioni regex).
drwatsoncode

0

Questa risposta non riguarda la parte Bash della domanda, ma perché ho cercato di usare le risposte in questa domanda per implementare questa funzionalità in Emacs , la lancerò lì.

Emacs ha effettivamente una funzione pronta per questo:

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"

Nota che credo che la risposta di Python che ho aggiunto di recente (la relpathfunzione) si comporti in modo identico file-relative-nameper i casi di test che hai fornito.
Gary Wisniewski,

-1

Ecco uno script di shell che lo fa senza chiamare altri programmi:

#! /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
Questo non è realmente portatile a causa dell'uso del costrutto $ {param / pattern / subst}, che non è POSIX (a partire dal 2011).
Jens,

La fonte di riferimento ynform.org/w/Pub/Relpath punta a una pagina wiki completamente confusa che contiene il contenuto dello script più volte, mescolata a righe viola, messaggi di errore su comandi non trovati e quant'altro. Assolutamente inutile per qualcuno che ricerca l'originale.
Jens,

-1

Avevo bisogno di qualcosa del genere ma che risolveva anche i collegamenti simbolici. Ho scoperto che pwd ha un flag -P a tale scopo. Viene aggiunto un frammento della mia sceneggiatura. È all'interno di una funzione in uno script di shell, quindi $ 1 e $ 2. Il valore del risultato, che è il percorso relativo da START_ABS a END_ABS, è nella variabile UPDIRS. Lo script cd si trova in ogni directory dei parametri per eseguire pwd -P e questo significa anche che vengono gestiti i parametri relativi al percorso. Saluti, 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"
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.