Getting relative links between two paths

Say I have two paths: <source_path> and <target_path>. I would like my shell (zsh) to automatically find out if there is a way to represent <target_path> from <source_path> as a relative path.

E.g. Let’s assume

  • <source_path> is /foo/bar/something
  • <target_path> is /foo/hello/world

The result would be ../../hello/world

Why I need this:

I need like to create a symbolic link from <source_path> to <target_path> using a relative symbolic link whenever possible, since otherwise our samba server does not show the file properly when I access these files on the network from Windows (I am not the sys admin, and don’t have control over this setting)

Assuming that <target_path> and <source_path> are absolute paths, the following creates a symbolic link pointing to an absolute path.

ln -s <target_path> <source_path>

so it does not work for my needs. I need to do this for hundreds of files, so I can’t just manually fix it.

Any shell built-ins that take care of this?

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

Try using realpath command (part of GNU coreutils; >=8.23), e.g.:

realpath --relative-to=/foo/bar/something /foo/hello/world

If you’re using macOS, install GNU version via: brew install coreutils and use grealpath.

Note that both paths need to exist for the command to be successful. If you need the relative path anyway even if one of them does not exist then add the -m switch.

For more examples, see Convert absolute path into relative path given a current directory.

Method 2

You could use the symlinks command to convert absolute paths to relative:

/tmp$ mkdir -p 1/{a,b,c} 2
/tmp$ cd 2
/tmp/2$ ln -s /tmp/1/* .
/tmp/2$ ls -l
total 0
lrwxrwxrwx 1 stephane stephane 8 Jul 31 16:32 a -> /tmp/1/a/
lrwxrwxrwx 1 stephane stephane 8 Jul 31 16:32 b -> /tmp/1/b/
lrwxrwxrwx 1 stephane stephane 8 Jul 31 16:32 c -> /tmp/1/c/

We’ve got absolute links, let’s convert them to relative:

/tmp/2$ symlinks -cr .
absolute: /tmp/2/a -> /tmp/1/a
changed:  /tmp/2/a -> ../1/a
absolute: /tmp/2/b -> /tmp/1/b
changed:  /tmp/2/b -> ../1/b
absolute: /tmp/2/c -> /tmp/1/c
changed:  /tmp/2/c -> ../1/c
/tmp/2$ ls -l
total 0
lrwxrwxrwx 1 stephane stephane 6 Jul 31 16:32 a -> ../1/a/
lrwxrwxrwx 1 stephane stephane 6 Jul 31 16:32 b -> ../1/b/
lrwxrwxrwx 1 stephane stephane 6 Jul 31 16:32 c -> ../1/c/

References

Method 3

I think this python solution (taken from this SO answer) is also worth mentioning. Add this to your ~/.zshrc:

relpath() python -c 'import os.path, sys;
  print os.path.relpath(sys.argv[1],sys.argv[2])' "$1" "${2-$PWD}"

You can then do, for example:

$ relpath /usr/local/share/doc/emacs /usr/local/share/fonts
../doc/emacs

Method 4

There are not any shell builtins to take care of this. I found a solution on
Stack Overflow
though. I have
copied the solution here with slight modifications and marked this answer as
community wiki.

relpath() {
    # both $1 and $2 are absolute paths beginning with /
    # $1 must be a canonical path; that is none of its directory
    # components may be ".", ".." or a symbolic link
    #
    # returns relative path to $2/$target from $1/$source
    source=$1
    target=$2

    common_part=$source
    result=

    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#?}
    fi

    printf '%sn' "$result"
}

You can use this function like so:

source=/foo/bar/something
target=/foo/hello/world
ln -s "$(relpath "$source" "$target")" "$source"

Method 5

# Prints out the relative path between to absolute paths. Trivial.
#
# Parameters:
# $1 = first path
# $2 = second path
#
# Output: the relative path between 1st and 2nd paths
relpath() {
    local pos="${1%%/}" ref="${2%%/}" down=''

    while :; do
        test "$pos" = '/' && break
        case "$ref" in $pos/*) break;; esac
        down="../$down"
        pos=${pos%/*}
    done

    echo "$down${ref##$pos/}"
}

This is the simplest and most beautiful solution to the problem.

Also it should work on all bourne-like shells.

And the wisdom here is: there is absolutely no need for external utilities or even complicated conditions. A couple prefix/suffix substitutions and a while loop is all you need to get just about anything done on bourne-like shells.

Also available via PasteBin: http://pastebin.com/CtTfvime

Method 6

I had to write a bash script to do this reliably (and of course without verifying file existence).

Usage

relpath <symlink path> <source path>

Returns a relative source path.

example:

#/a/b/c/d/e   ->  /a/b/CC/DD/EE       ->  ../../CC/DD/EE
relpath /a/b/c/d/e   /a/b/CC/DD/EE

#./a/b/c/d/e   ->  ./a/b/CC/DD/EE       ->  ../../CC/DD/EE
relpath ./a/b/c/d/e  ./a/b/CC/DD/EE

both get
../../CC/DD/EE


To have a quick test you can run

curl -sL https://github.com/jjqq2013/bash-scripts/raw/master/common/relpath | bash -s -- --test

result: a list of test

/a             ->  /                 ->  .
/a/b           ->  /                 ->  ..
/a/b           ->  /a                ->  .
/a/b/c         ->  /a                ->  ..
/a/b/c         ->  /a/b              ->  .
/a/b/c         ->  /a/b/CC           ->  CC
/a/b/c/d/e     ->  /a                ->  ../../..
/a/b/c/d/e     ->  /a/b              ->  ../..
/a/b/c/d/e     ->  /a/b/CC           ->  ../../CC
/a/b/c/d/e     ->  /a/b/CC/DD/EE     ->  ../../CC/DD/EE
./a            ->  ./                ->  .
./a/b          ->  ./                ->  ..
./a/b          ->  ./a               ->  .
./a/b/c        ->  ./a               ->  ..
./a/b/c        ->  ./a/b             ->  .
./a/b/c        ->  ./a/b/CC          ->  CC
./a/b/c/d/e    ->  ./a               ->  ../../..
./a/b/c/d/e    ->  ./a/b/CC          ->  ../../CC
./a/b/c/d/e    ->  ./a/b/CC/DD/EE    ->  ../../CC/DD/EE
/a             ->  /x                ->  x
/a/b           ->  /x                ->  ../x
/a/b/c         ->  /x                ->  ../../x
/a             ->  /x/y              ->  x/y
/a/b           ->  /x/y              ->  ../x/y
/a/b/c         ->  /x/y              ->  ../../x/y
/x             ->  /a                ->  a
/x             ->  /a/b              ->  a/b
/x             ->  /a/b/c            ->  a/b/c
/x/y           ->  /a                ->  ../a
/x/y           ->  /a/b              ->  ../a/b
/x/y           ->  /a/b/c            ->  ../a/b/c
./a a/b/c/d/e  ->  ./a a/b/CC/DD/EE  ->  ../../CC/DD/EE
/x x/y y       ->  /a a/b b/c c      ->  ../a a/b b/c c

BTW, if you want to use this script without saving it and you trust this script, then you have two ways to to this with bash trick.

Import relpath as a bash function by following command. (Require bash 4+)

source <(curl -sSL https://github.com/jjqq2013/bash-scripts/raw/master/common/relpath)

or call the script on-the-fly like with curl bash combo.

curl -sSL https://github.com/jjqq2013/bash-scripts/raw/master/common/relpath | bash -s -- /a/b/c/d/e /a/b/CC/DD/EE


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x