32

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?

2
  • You can use symlinks to convert absolute links to relative. Jul 31 '13 at 15:12
  • @StephaneChazelas how? Could you post an answer explaining?
    – terdon
    Jul 31 '13 at 15:14
38

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.

6
  • I get realpath: unrecognized option '--relative-to=.' :( realpath version 1.19
    – phuclv
    Feb 8 '18 at 11:26
  • @LưuVĩnhPhúc You need to use GNU/Linux version of realpath. If you're on macOS, install via: brew install coreutils.
    – kenorb
    Feb 8 '18 at 11:27
  • no, I'm on Ubuntu 14.04 LTS
    – phuclv
    Feb 8 '18 at 14:56
  • @LưuVĩnhPhúc I'm on realpath (GNU coreutils) 8.26 where the parameter is present. See: gnu.org/software/coreutils/realpath Double check that you're not using an alias (e.g. run as \realpath), and how you can install the version from the coreutils.
    – kenorb
    Feb 8 '18 at 14:57
  • 1
    Seems realpath on Ubuntu 14.04 is outdated indeed, and the only way is rebuild coreutil manually
    – phuclv
    Feb 9 '18 at 3:32
11

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

2
  • 1
    Thanks Stephane. This solves the root problem, so I have accepted. That said, it would be great to have something (e.g. a zsh function) that takes two paths (source and target) and outputs the relative path from source to target (no symbolic links here). That would solve the question asked in the title as well. Jul 31 '13 at 17:23
  • @user815423426, terdon has answered that part Jul 31 '13 at 17:33
8

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

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 '%s\n' "$result"
}

You can use this function like so:

source=/foo/bar/something
target=/foo/hello/world
ln -s "$(relpath "$source" "$target")" "$source"
3
# 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

2
  • Thanks for your solution. I found that to make it work in a Makefile I needed to add a pair of double quotes to the line pos=${pos%/*} (and, of course, semicolons and backslashes at the end of everyline). Jul 26 '17 at 8:15
  • Idea is not bad but, in current version plenty of cases don't work: relpath /tmp /tmp, relpath /tmp /tmp/a, relpath /tmp/a /. In add, you forget to mention that all paths must be absolute AND canonicalized (i.e. /tmp/a/.. does not work) Apr 24 '18 at 8:55
0

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.