Convert absolute path into relative path given a current directory using Bash
Asked Answered
F

25

369

Example:

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

# Magic

relative="../../bar"

How do I create the magic (hopefully not too complicated code...)?

Frankforter answered 2/4, 2010 at 1:42 Comment(3)
For example (my case right now) for giving gcc relative path so it can generate relative debug infos usable even if source path change.Amalita
A question similar to this one was asked on U&L: unix.stackexchange.com/questions/100918/…. One of the answers (@Gilles) mentions a tool, symlinks, which can make easy work of this problem.Subsequence
I'm surprised there isn't a tool (or option) like realpath for this.Treacherous
S
372

Using realpath from GNU coreutils 8.23 is the simplest, I think:

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

For example:

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

The -s flag ensures that symlinks are not expanded.

Spicebush answered 15/2, 2015 at 4:59 Comment(11)
Nice, pity it's not installed by default on UbuntuNibelung
It's a pity that the package is outdated on Ubuntu 14.04 and does not have the --relative-to option.Composure
Works fine on Ubuntu 16.04Yearbook
$ realpath --relative-to="${PWD}" "$file" is useful if you want the paths relative to the current working directory.Fallon
This is correct for things inside the /usr/bin/nmap/-path but not for /usr/bin/nmap: from nmap to /tmp/testing it is only ../../ and not 3 times ../. It works however, because doing .. on the rootfs is /.Brotherinlaw
As @PatrickB. implied, --relative-to=… expects a directory and DOES NOT check. That means that you end up with an extra "../" if you request a path relative to a file (as this example appears to do, because /usr/bin rarely or never contains directories and nmap is normally a binary)Kirksey
realpath is not present on Centos 6Eusporangiate
Realpath 8.22 (in Amazon Linux 2018.03) has the --relative-to already. Not sure when it was really introduced...Linger
For macOS: 1. brew install coreutils 2. Use grealpath instead.Falkner
This and touch --reference are a godsend.Dust
Note that this also expands symlinks. To get the relative path of a symlink itself, use realpath -s --relative-to...Riti
G
190
$ python -c "import os.path; print os.path.relpath('/foo/bar', '/foo/baz/foo')"

gives:

../../bar
Gerdy answered 5/9, 2011 at 7:50 Comment(15)
It works, and it makes the alternatives look ridiculous. That's a bonus for me xDMetopic
+1. Ok, you cheated... but this is too good not to be used! relpath(){ python -c "import os.path; print os.path.relpath('$1','${2:-$PWD}')" ; } Altocumulus
Sadly, this is not available universally: os.path.relpath is new in Python 2.6.Ingrowth
@ChenLevy: Python 2.6 was released in 2008. Hard to believe it wasn't universally available in 2012.Altocumulus
Not sure if MestreLion's solution works if $1 has a single quote in it. Perhaps something like: relpath() { python -c 'import sys, os.path; print os.path.relpath(sys.argv[1], sys.argv[2])' "$1" "${2:-$PWD}"; }Galcha
Here's a simple version that works with all strings (including those containing quotes): echo "$1" | python -c "import os, sys; print(os.path.relpath(sys.stdin.read(), os.getcwd()))"Castor
python -c 'import os, sys; print(os.path.relpath(*sys.argv[1:]))' works most naturally and reliably.Emmetropia
@MestreLion, define universally available. Many of us are still stuck with admins on AIX 6 or like systems where requests for python might be scoffed at.Propitiatory
@BrettRyan: using Python 2.5 in 2016 is actually a security risk, as it has been EOL'ed a long time ago. Currently only 2.7 onwards get security fixes.Altocumulus
Totally agree for public facing services. But when you're inside a corporate network not connected to the internet these risks are seen less important to the admins.Propitiatory
Ruby version: ruby -e "require 'pathname'; puts Pathname.new('/foo/bar').relative_path_from Pathname.new('/foo/baz/foo')" No Python-vs-Ruby debate intended, I just like to keep my own scripts in languages I'm more confident in.Malt
OP states: "...using Bash"Combination
You come to this question expecting an answer that uses a command like basename. Once you look through the incredible gymnastics that must be performed to keep this "purely bash", you decide a one-liner in python is totally acceptable.Acknowledgment
It works in macOS too, so I will use this solution. Also calling print with brackets makes it useable with python3 as well python3 -c "import os.path; print(os.path.relpath('/foo/bar', '/foo/baz/foo'))"Lectureship
this works great! I have been using pathlib.Path lately, and now I see there is no equivalent solution in pathlib docs.python.org/3/library/pathlib.html#id6Quiroz
A
32

This is a corrected, fully functional improvement of the currently best rated solution from @pini (which sadly handle only a few cases)

Reminder : -z test if the string is zero-length (=empty) and -n test if the string is not empty.

# 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

Test cases :

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"
Amalita answered 19/9, 2012 at 15:58 Comment(10)
Integrated in offirmo shell lib github.com/Offirmo/offirmo-shell-lib, function «OSL_FILE_find_relative_path» (file «osl_lib_file.sh»)Amalita
+1. It can easily be made to handle any paths (not just absolute paths starting with /) by replacing source=$1; target=$2 with source=$(realpath $1); target=$(realpath $2)Quarterstaff
@Josh indeed, provided that the dirs actually exists... which was unconvenient for the unit tests ;) But in real use yes, realpath is recommended, or source=$(readlink -f $1) etc. if realpath is not available (not standard)Amalita
I defined $source and $target like this: ` if [[ -e $1 ]]; then source=$(readlink -f $1); else source=$1; fi if [[ -e $2 ]]; then target=$(readlink -f $2); else target=$2; fi` That way, the function could heandle real/existsing relative paths as well as fictional directories.Spotless
@NathanS.Watson-Haigh Even better, I discovered recently readlink has a -m option which just does that ;)Amalita
See my answer below which fixes compute-relative.sh "/a/b/c/de/f/g" "/a/b/c/def/g/" --> "../..f/g/" and makes it Dash compatible.Carbonate
Note, I don't believe the above solution allows filenames that contain some special characters, for example, the following doesn't appear to work when there is a space in the filename: compute_relative.sh "/a /b/c" "/a".Doublepark
To make @Offirmo's version work in the face of paths with spaces, change to: common_part=$(dirname "$common_part")Carbonate
compute-relative.sh a/b/c a/b/ccc will result in cc - However this can be workarounded by compute-relative.sh a/b/c/ a/b/ccc/Fabria
This really needs a lot more test cases. The "${target#$common_part}" expansion looked suspicious and indeed compute-relative.sh "/A/B/C" "/AA" prints nonsense: ../..A.Homemade
K
32

It is built in to Perl since 2001, so it works on nearly every system you can imagine, even VMS.

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

Also, the solution is easy to understand.

So for your example:

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

...would work fine.

Kincardine answered 14/6, 2013 at 14:15 Comment(4)
say has not been available in perl for as log, but it could be used effectively here. perl -MFile::Spec -E 'say File::Spec->abs2rel(@ARGV)'Hartfield
+1 but see also this similar answer which is older (Feb 2012). Read also the pertinent comments from William Pursell. My version are two command lines: perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target" and perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$target" "$origin". The first one-line perl script uses one argument (origin is current working directory). The second one-line perl script uses two arguments.Remonaremonetize
That should be the accepted answer. perl can be found almost everywhere, though the answer is still one-liner.Jud
@WilliamPursell The same can be achieved with the -l command line switch, I edited that into the example.Resupine
P
29
#!/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/}
Peers answered 17/2, 2011 at 10:40 Comment(8)
Wonderful script -- short and clean. I applied an edit (Waiting on peer review): common_part=$source/ common_part=$(dirname $common_part)/ echo ${back}${target#$common_part} Existing script would fail due to inappropriate match on start of directory name when comparing, for example: "/foo/bar/baz" to "/foo/barsucks/bonk". Moving the slash into the var and out of the final eval corrects that bug.Tactful
This script simply doesn't work. Fails one a simple "one directory down" tests. The edits by jcwenger work a little better but tend to add an extra "../".Matteroffact
After spending 30 minutes trying to port the script below (by Dennis Williamson) to zsh, this script does the job perfectly for my simple use case, and it also works in zsh without modification.Cryotherapy
it fails for me in some cases if a trailing "/" is on the argument; e.g., if $1="$HOME/" and $2="$HOME/temp", it returns "/home/user/temp/", but if $1=$HOME then it properly returns the relative path "temp". Both source=$1 and target=$2 could therefore be "cleansed" using sed (or using bash variable substitution, but that can be unnecessarily opaque) such as => source=$(echo "${1}" | sed 's/\/*$//')Chloromycetin
Minor improvement: Instead of setting source/target directly to $1 and $2, do: source=$(cd $1; pwd) target=$(cd $2; pwd). This way it handles paths with . and .. correctly.Idette
Actually, source=$(cd $1 && pwd) is better, then you get a nice error when they don't exist and you can set -e to make sure the script fails.Idette
Despite being the top-voted answer, this answer has lot of limitations, hence so many other answers being posted. See the other answers instead, especially the one displaying test cases. And please upvote this comment !Amalita
Wish I could undo my upvote :-( The similar answer by @linuxball is much better — that one works in lots of situations, and is already converted to a function. This one failed in all sorts of ways (wrong results, infinite loops, etc.) as soon as I actually started testing it.Venus
G
22

Presuming that you have installed: bash, pwd, dirname, echo; then relpath is

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

I've golfed the answer from pini and a few other ideas

Note: This requires both paths to be existing folders. Files will not work.

Gamez answered 20/7, 2014 at 8:32 Comment(3)
ideal answer: works with /bin/sh, does not require readlink, python, perl -> great for light/embedded systems or windows bash consoleDame
Unfortunately, this requires that the path exist, which is not always desired.Tina
Godly answer. The cd-pwd stuff is for resolving links I guess? Nice golfing!Peripeteia
D
17

Python's os.path.relpath as a shell function

The goal of this relpath exercise is to mimic Python 2.7's os.path.relpath function (available from Python version 2.6 but only working properly in 2.7), as proposed by xni. As a consequence, some of the results may differ from functions provided in other answers.

(I have not tested with newlines in paths simply because it breaks the validation based on calling python -c from ZSH. It would certainly be possible with some effort.)

Regarding “magic” in Bash, I have given up looking for magic in Bash long ago, but I have since found all the magic I need, and then some, in ZSH.

Consequently, I propose two implementations.

The first implementation aims to be fully POSIX-compliant. I have tested it with /bin/dash on Debian 6.0.6 “Squeeze”. It also works perfectly with /bin/sh on OS X 10.8.3, which is actually Bash version 3.2 pretending to be a POSIX shell.

The second implementation is a ZSH shell function that is robust against multiple slashes and other nuisances in paths. If you have ZSH available, this is the recommended version, even if you are calling it in the script form presented below (i.e. with a shebang of #!/usr/bin/env zsh) from another shell.

Finally, I have written a ZSH script that verifies the output of the relpath command found in $PATH given the test cases provided in other answers. I added some spice to those tests by adding some spaces, tabs, and punctuation such as ! ? * here and there and also threw in yet another test with exotic UTF-8 characters found in vim-powerline.

POSIX shell function

First, the POSIX-compliant shell function. It works with a variety of paths, but does not clean multiple slashes or resolve symlinks.

#!/bin/sh
relpath () {
    [ $# -ge 1 ] && [ $# -le 2 ] || return 1
    current="${2:+"$1"}"
    target="${2:-"$1"}"
    [ "$target" != . ] || target=/
    target="/${target##/}"
    [ "$current" != . ] || current=/
    current="${current:="/"}"
    current="/${current##/}"
    appendix="${target##/}"
    relative=''
    while appendix="${target#"$current"/}"
        [ "$current" != '/' ] && [ "$appendix" = "$target" ]; do
        if [ "$current" = "$appendix" ]; then
            relative="${relative:-.}"
            echo "${relative#/}"
            return 0
        fi
        current="${current%/*}"
        relative="$relative${relative:+/}.."
    done
    relative="$relative${relative:+${appendix:+/}}${appendix#/}"
    echo "$relative"
}
relpath "$@"

ZSH shell function

Now, the more robust zsh version. If you would like it to resolve the arguments to real paths à la realpath -f (available in the Linux coreutils package), replace the :a on lines 3 and 4 with :A.

To use this in zsh, remove the first and last line and put it in a directory that is in your $FPATH variable.

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

Finally, the test script. It accepts one option, namely -v to enable verbose output.

#!/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
Delois answered 16/2, 2013 at 19:12 Comment(3)
Doesn't work when the first path ends in / I'm afraid.Attu
@Attu the Zsh version seems to handle it fine.Hadlee
@Hadlee I was referring to the POSIX one (using Bash).Attu
B
13
#!/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")"; }

Above shell script was inspired by pini's (Thanks!). It triggers a bug in the syntax highlighting module of Stack Overflow (at least in my preview frame). So please ignore if highlighting is incorrect.

Some notes:

  • Removed errors and improved code without significantly increasing code length and complexity
  • Put functionality into functions for easiness of use
  • Kept functions POSIX compatible so that they (should) work with all POSIX shells (tested with dash, bash, and zsh in Ubuntu Linux 12.04)
  • Used local variables only to avoid clobbering global variables and polluting the global name space
  • Both directory paths DO NOT need to exist (requirement for my application)
  • Pathnames may contain spaces, special characters, control characters, backslashes, tabs, ', ", ?, *, [, ], etc.
  • Core function "relPath" uses POSIX shell builtins only but requires canonical absolute directory paths as parameters
  • Extended function "relpath" can handle arbitrary directory paths (also relative, non-canonical) but requires external GNU core utility "readlink"
  • Avoided builtin "echo" and used builtin "printf" instead for two reasons:
  • To avoid unnecessary conversions, pathnames are used as they are returned and expected by shell and OS utilities (e.g. cd, ln, ls, find, mkdir; unlike python's "os.path.relpath" which will interpret some backslash sequences)
  • Except for the mentioned backslash sequences the last line of function "relPath" outputs pathnames compatible to python:

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

    Last line can be replaced (and simplified) by line

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

    I prefer the latter because

    1. Filenames can be directly appended to dir paths obtained by relPath, e.g.:

      ln -s "$(relpath "<fromDir>" "<toDir>")<file>" "<fromDir>"
      
    2. Symbolic links in the same dir created with this method do not have the ugly "./" prepended to the filename.

  • If you find an error please contact linuxball (at) gmail.com and I'll try to fix it.
  • Added regression test suite (also POSIX shell compatible)

Code listing for regression tests (simply append it to the shell script):

############################################################################
# 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
Bipartite answered 19/9, 2013 at 15:24 Comment(0)
D
11

Not a lot of the answers here are practical for every day use. Since it is very difficult to do this properly in pure bash, I suggest the following, reliable solution (similar to one suggestion buried in a comment):

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

Then, you can get the relative path based upon the current directory:

echo $(relpath somepath)

or you can specify that the path be relative to a given directory:

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

The one disadvantage is this requires python, but:

  • It works identically in any python >= 2.6
  • It does not require that the files or directories exist.
  • Filenames may contain a wider range of special characters. For example, many other solutions do not work if filenames contain spaces or other special characters.
  • It is a one-line function that doesn't clutter scripts.

Note that solutions which include basename or dirname may not necessarily be better, as they require that coreutils be installed. If somebody has a pure bash solution that is reliable and simple (rather than a convoluted curiosity), I'd be surprised.

Doublepark answered 6/7, 2015 at 1:9 Comment(1)
This seems like by far the most robust approach.Homemade
B
8

This script gives correct results only for inputs that are absolute paths or relative paths without . or ..:

#!/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"
Birr answered 2/4, 2010 at 5:17 Comment(1)
This appears to work. If the directories actually exist, use of $(readlink -f $1) and $(readlink -f $2) on the inputs can fix the problem where "." or ".." appears in the inputs. This can cause some trouble if the directories don't actually exist.Matteroffact
M
7

I would just use Perl for this not-so-trivial task:

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

# Perl is magic
relative=$(perl -MFile::Spec -e 'print File::Spec->abs2rel("'$absolute'","'$current'")')
Magi answered 12/2, 2012 at 17:8 Comment(2)
+1, but would recommend: perl -MFile::Spec -e "print File::Spec->abs2rel('$absolute','$current')" so that absolute and current are quoted.Hartfield
I like relative=$(perl -MFile::Spec -e 'print File::Spec->abs2rel(@ARGV)' "$absolute" "$current"). This ensures the values cannot, themselves, contain perl code!Kincardine
B
7

Yet another solution, pure bash + GNU readlink for easy use in following context:

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

Edit: Make sure that "$B" is either not existing or no softlink in that case, else relpath follows this link which is not what you want!

This works in nearly all current Linux. If readlink -m does not work at your side, try readlink -f instead. See also https://gist.github.com/hilbix/1ec361d00a8178ae8ea0 for possible updates:

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

Notes:

  • Care was taken that it is safe against unwanted shell meta character expansion, in case filenames contain * or ?.
  • The output is meant to be usable as the first argument to ln -s:
    • relpath / / gives . and not the empty string
    • relpath a a gives a, even if a happens to be a directory
  • Most common cases were tested to give reasonable results, too.
  • This solution uses string prefix matching, hence readlink is required to canonicalize paths.
  • Thanks to readlink -m it works for not yet existing paths, too.

On old systems, where readlink -m is not available, readlink -f fails if the file does not exist. So you probably need some workaround like this (untested!):

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

This is not really quite correct in case $1 includes . or .. for nonexisting paths (like in /doesnotexist/./a), but it should cover most cases.

(Replace readlink -m -- above by readlink_missing.)

Edit because of the downvote follows

Here is a test, that this function, indeed, is correct:

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"

Puzzled? Well, these are the correct results! Even if you think it does not fit the question, here is the proof this is correct:

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

Without any doubt, ../bar is the exact and only correct relative path of the page bar seen from the page moo. Everything else would be plain wrong.

It is trivial to adopt the output to the question which apparently assumes, that current is a directory:

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

This returns exactly, what was asked for.

And before you raise an eyebrow, here is a bit more complex variant of relpath (spot the small difference), which should work for URL-Syntax, too (so a trailing / survives, thanks to some 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:-.}"
}

And here are the checks just to make clear: It really works as told.

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

And here is how this can be used to give the wanted result from the question:

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

If you find something which does not work, please let me know in the comments below. Thanks.

PS:

Why are the arguments of relpath "reversed" in contrast to all the other answers here?

If you change

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

to

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

then you can leave the 2nd parameter away, such that the BASE is the current directory/URL/whatever. That's only the Unix principle, as usual.

Bedrabble answered 27/11, 2014 at 16:58 Comment(0)
P
6

A slight improvement on kasku's and Pini's answers, which plays nicer with spaces and allows passing relative paths:

#!/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
Pall answered 2/4, 2010 at 1:42 Comment(0)
P
4

Sadly, Mark Rushakoff's answer (now deleted - it referenced the code from here) does not seem to work correctly when adapted to:

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

The thinking outlined in the commentary can be refined to make it work correctly for most cases. I'm about to assume that the script takes a source argument (where you are) and a target argument (where you want to get to), and that either both are absolute pathnames or both are relative. If one is absolute and the other relative, the easiest thing is to prefix the relative name with the current working directory - but the code below does not do that.


Beware

The code below is close to working correctly, but is not quite right.

  1. There is the problem addressed in the comments from Dennis Williamson.
  2. There is also a problem that this purely textual processing of pathnames and you can be seriously messed up by weird symlinks.
  3. The code does not handle stray 'dots' in paths like 'xyz/./pqr'.
  4. The code does not handle stray 'double dots' in paths like 'xyz/../pqr'.
  5. Trivially: the code does not remove leading './' from paths.

Dennis's code is better because it fixes 1 and 5 - but has the same issues 2, 3, 4. Use Dennis's code (and up-vote it ahead of this) because of that.

(NB: POSIX provides a system call realpath() that resolves pathnames so that there are no symlinks left in them. Applying that to the input names, and then using Dennis's code would give the correct answer each time. It is trivial to write the C code that wraps realpath() - I've done it - but I don't know of a standard utility that does so.)


For this, I find Perl easier to use than shell, though bash has decent support for arrays and could probably do this too - exercise for the reader. So, given two compatible names, split them each into components:

  • Set the relative path to empty.
  • While the components are the same, skip to the next.
  • When corresponding components are different or there are no more components for one path:
  • If there are no remaining source components and the relative path is empty, add "." to the start.
  • For each remaining source component, prefix the relative path with "../".
  • If there are no remaining target components and the relative path is empty, add "." to the start.
  • For each remaining target component, add the component to the end of the path after a slash.

Thus:

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

Test script (the square brackets contain a blank and a tab):

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 from the test script:

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

This Perl script works fairly thoroughly on Unix (it does not take into account all the complexities of Windows path names) in the face of weird inputs. It uses the module Cwd and its function realpath to resolve the real path of names that exist, and does a textual analysis for paths that don't exist. In all cases except one, it produces the same output as Dennis's script. The deviant case is:

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

The two results are equivalent - just not identical. (The output is from a mildly modified version of the test script - the Perl script below simply prints the answer, rather than the inputs and the answer as in the script above.) Now: should I eliminate the non-working answer? Maybe...

#!/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);
Pedestrianize answered 2/4, 2010 at 4:50 Comment(6)
Your /home/part1/part2 to / has one too many ../. Otherwise, my script matches your output except mine adds an unnecessary . at the end of the one where the destination is . and I don't use a ./ at the beginning of ones that descend without going up.Birr
@Dennis: I spent time going cross-eyed over the results - sometimes I could see that problem, and sometimes I couldn't find it again. Removing a leading './' is of another trivial step. Your comment about 'no embedded . or ..' is also pertinent. It is actually surprisingly difficult to do the job properly - doubly so if any of the names is actually a symlink; we're both doing purely textual analysis.Pedestrianize
@Dennis: Of course, unless you have Newcastle Connection networking, trying to get above root is futile, so ../../../.. and ../../.. are equivalent. However, that is pure escapism; your criticism is correct. (Newcastle Connection allowed you to configure and use the notation /../host/path/on/remote/machine to get to a different host - a neat scheme. I believe it supported /../../network/host/path/on/remote/network/and/host too. It's on Wikipedia.)Pedestrianize
So instead, we now have the double slash of UNC.Birr
The "readlink" utility (at least the GNU version) can do the equivalent of realpath(), if you pass it the "-f" option. For example, on my system, readlink /usr/bin/vi gives /etc/alternatives/vi, but that's another symlink - whereas readlink -f /usr/bin/vi gives /usr/bin/vim.basic, which is the ultimate destination of all the symlinks...Glaudia
note you could insert a tab literal with $'\t' and that won't get mangled by SOGiltzow
L
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

Testing:

$ ./test.sh 
TEST=/home/ubuntu/.//blah
TMP=/home/ubuntu/blah
REL=blah
Liberalize answered 11/6, 2010 at 17:7 Comment(3)
+1 for compactness and bash-ness. You should, however, also call readlink on $(pwd).Loyceloyd
Relative does not mean file has to be placed in the same directory.Incontrovertible
although the original question doesn't provide a lot of testcases, this script fails for simple tests like finding the relative path from /home/user1 to /home/user2 (correct answer: ../user2). The script by pini/jcwenger works for this case.Chloromycetin
C
3

I took your question as a challenge to write this in "portable" shell code, i.e.

  • with a POSIX shell in mind
  • no bashisms such as arrays
  • avoid calling externals like the plague. There's not a single fork in the script! That makes it blazingly fast, especially on systems with significant fork overhead, like cygwin.
  • Must deal with glob characters in pathnames (*, ?, [, ])

It runs on any POSIX conformant shell (zsh, bash, ksh, ash, busybox, ...). It even contains a testsuite to verify its operation. Canonicalization of pathnames is left as an exercise. :-)

#!/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 :
Churchwoman answered 11/8, 2011 at 9:16 Comment(1)
This is impressive!Hadlee
C
2

This script works only on the path names. It does not require any of the files to exist. If the paths passed are not absolute, the behavior is a bit unusual, but it should work as expected if both paths are relative.

I only tested it on OS X, so it might not be portable.

#!/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
Cointreau answered 6/12, 2010 at 20:23 Comment(2)
Also, the code is a bit copy & paste-ish, but I needed this rather quickly.Cointreau
Nice...just what I needed. And you've included a canonicalizing routine that is better than most others i've seen (which usually rely on regex replacements).Tina
M
2

My Solution:

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

}
Mond answered 25/10, 2011 at 16:37 Comment(1)
This is Extrememly Portable :)Mond
C
2

Here is my version. It's based on the answer by @Offirmo. I made it Dash-compatible and fixed the following testcase failure:

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

Now:

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

See the code:

# 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
}
Carbonate answered 11/6, 2015 at 11:3 Comment(0)
L
2

I am using macOS that did not have realpath command by default, so I made a pure bash function to caculate it.

#!/bin/bash

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

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

    result=$result/$targetFilename
    echo ${result#./}
}
Lelahleland answered 8/5, 2021 at 15:22 Comment(0)
P
1

Guess this one shall do the trick too... (comes with built-in tests) :)

OK, some overhead expected, but we're doing Bourne shell here! ;)

#!/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
Perrine answered 23/12, 2011 at 23:5 Comment(0)
L
0

This answer does not address the Bash part of the question, but because I tried to use the answers in this question to implement this functionality in Emacs I'll throw it out there.

Emacs actually has a function for this out of the box:

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"
Leventhal answered 16/6, 2015 at 11:59 Comment(1)
Note that I believe the python answer I've added recently (the relpath function) behaves identically to file-relative-name for the test cases you've provided.Doublepark
P
0

in bash:

realDir=''
cd $(dirname $0) || exit
realDir=$(pwd)
cd -
echo $realDir
Pyromorphite answered 18/11, 2022 at 8:17 Comment(0)
A
-1

Here's a shell script that does it without calling other programs:

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

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

Andradite answered 3/5, 2010 at 12:41 Comment(2)
This is not really portable due to the use of the ${param/pattern/subst} construct, which is not POSIX (as of 2011).Churchwoman
The referenced source ynform.org/w/Pub/Relpath points to a completely garbled wiki page containing the script contents several times, intermingled with vi tilde lines, error messages about commands not found and whatnot. Utterly useless for someone researching the original.Churchwoman
C
-1

I needed something like this but which resolved symbolic links too. I discovered that pwd has a -P flag for that purpose. A fragment of my script is appended. It's within a function in a shell script, hence the $1 and $2. The result value, which is the relative path from START_ABS to END_ABS, is in the UPDIRS variable. The script cd's into each parameter directory in order to execute the pwd -P and this also means that relative path parameters are handled. 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"
Colubrid answered 29/5, 2014 at 12:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.