59

What is a portable1 way for a (zsh) script to determine its absolute path?

On Linux I use something like

mypath=$(readlink -f $0)

...but this is not portable. (E.g., readlink on darwin does not recognize the -f flag, nor has any equivalent.) (Also, using readlink for this is, admittedly, a pretty obscure-looking hack.)

What's a more portable way?


1 Portable across OSs in the Unix family, that is.

kjo
  • 15,339
  • 25
  • 73
  • 114

7 Answers7

71

In zsh you can do the following:

mypath=${0:a}

Or, to get the directory in which the script resides:

mydir=${0:a:h}

See the Zsh documentation on history expansion modifiers, visible locally in man zshexpn or with info -f zsh -n Modifiers if the Info documentation is installed.

Stephen Kitt
  • 434,908
mrmenken
  • 819
  • 2
    Sweet! I've been looking for somethink like this for ages and read the whole bunch of the zsh manpages searching for it, but it would never have occurred to me to look under 'History expansion'. – Vucar Timnärakrul May 20 '14 at 20:04
  • 3
    The equivalent of GNU's readlink -f would rather be $0:A. – Stéphane Chazelas Jun 11 '14 at 10:23
  • 3
    :A: Turn a file name into an absolute path as the :a modifier does, and then pass the result through the realpath(3) library function to resolve symbolic links. doc via info -f zsh -n Modifiers – Weekend Sep 01 '21 at 07:02
  • 1
    Is the OP @kjo, asking for a "portable way to do [something], that only works in zsh"? Or is that how it is being answered? The above isn't portable to any other shell, besides, I am assuming, zsh (not a fan, don't know). Maybe a portable way of determining abspath in zsh AND other shells would be a better use of time and answer for future look ups. – christian elsee Jan 20 '22 at 21:25
  • 1
    Thanks! This is super helpful. Another cool tip for anyone who is reading, if you want to get the parent directory of the directory in which the script resides, you can do ${0:a:h:h}. Each additional :h you add will go up one additional directory. – scorgn May 15 '22 at 17:40
  • This gives an incorrect result when the script is being run as ./script and uses cd to change the directory before expanding ${0:a:h}: https://pastebin.com/3xHsMGYB – Martin von Wittich Oct 24 '23 at 12:09
  • this is the first thing that's bubbled to my attention where Bash has a shortcoming which Zsh lacks. rad ! – orion elenzil Feb 09 '24 at 22:19
44

With zsh, it's just:

mypath=$0:A

Or

mypath=$0:P

In newer versions (see the manual for the details on how they differ).

Now for other shells, though realpath() and readlink() are standard functions (the latter being a system call), realpath and readlink are not standard command, though some systems have one or the other or both with various behaviour and feature set.

As often, for portability, you may want to resort to perl:

abs_path() {
  perl -MCwd -le '
    for (@ARGV) {
      if ($p = Cwd::abs_path $_) {
        print $p;
      } else {
        warn "abs_path: $_: $!\n";
        $ret = 1;
      }
    }
    exit $ret' "$@"
}

That would behave more like GNU's readlink -f than realpath() (GNU readlink -e) in that it will not complain if the file doesn't exist as long as its dirname does.

18

I've been using this for several years now:

# The absolute, canonical ( no ".." ) path to this script
canonical=$(cd -P -- "$(dirname -- "$0")" && printf '%s\n' "$(pwd -P)/$(basename -- "$0")")
Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
user17591
  • 1,098
  • 1
    i like it! so far, that's pretty portable. works on Solaris, OmniOS, Linux, Mac, and even Cygwin on Windows 2008. – Tim Kennedy May 21 '13 at 17:18
  • 6
    Where it's not equivalent to GNU's readlink -f is when the script itself is a symlink. – Stéphane Chazelas Jun 11 '14 at 10:26
  • 2
    On Ubuntu 16.04 with zsh, if I execute this either directly or in the subshell (as suggested) while in my home dir (/home/ville), it prints out /home/ville/zsh. – Ville Feb 21 '18 at 18:24
11

This syntax should be portable to any Bourne shell style interpreter (tested with bash, ksh88, ksh93, zsh, mksh, dash and busybox sh):

mypath=$(exec 2>/dev/null;cd -- $(dirname "$0"); unset PWD; /usr/bin/pwd || /bin/pwd || pwd)
echo mypath=$mypath

This version adds compatibility to the legacy AT&T Bourne shell (non POSIX):

mypath=`dirname "$0"`
mypath=`exec 2>/dev/null;(cd -- "$mypath") && cd -- "$mypath"|| cd "$mypath"; unset PWD; /usr/bin/pwd || /bin/pwd || pwd`
echo mypath=$mypath
jlliagre
  • 61,204
  • thanks. though, I think unsetting $PWD might be overkill - you can just set it to absolute current like cd -P .. I doubt that would work in bourneshell - but it should work in all of the ones you tested for the first. does for me anyway. – mikeserv Aug 30 '14 at 19:38
  • @moose What OS are you running ? – jlliagre Aug 31 '14 at 19:07
  • 2
    who is moose? what? – mikeserv Aug 31 '14 at 19:12
  • @mikeserv moose is a passerby who posted a comment about some issue with zsh and dirname but quickly withdraw hir/her comment ... – jlliagre Aug 31 '14 at 19:17
  • Your Bourne Shell script will not work with a Bourne Shell as the Bourne Shell does not use getopt() for cd(1). – schily Sep 06 '15 at 13:20
  • @schily Can you elaborate a little further? Did you test my script under a legacy bourne shell and see it failing? – jlliagre Sep 06 '15 at 14:04
  • The Bourne Shell ignores all superfluous arguments and tries to chdir() to the first argument (which is -- in your example). If you like to check, get the portable Bourne Shell from http://sourceforge.net/projects/schilytools/files/ and check the binary osh that has the original SVr4 feature set. The binary shor boshis a newer maintained variant. – schily Sep 06 '15 at 15:22
  • @schily I'm aware of that. Look to my script logic closer, if the shell doesn't handle --, it falls back to legacy usage. – jlliagre Sep 06 '15 at 18:05
  • OK, after scrolling to the side I see this. BTW: The Bourne Shell does not support pwd -L/-P, but since 1989 it always returns the real path name as it always checks for symlinks. – schily Sep 06 '15 at 18:20
  • On zsh 5.8.1 (x86_64-apple-darwin22.0) on macOS 13.2 this solution only returns the directory containing the shell script, not the file name of the shell script – HairOfTheDog Jan 27 '23 at 19:03
5

Assuming you really meant the absolute path, i.e. a path from the root directory:

case $0 in
  /*) mypath=$0;;
  *) mypath=$PWD/$0;;
esac

This works in any Bourne-style shell, by the way.

If you meant a path with all symbolic links resolved, that's a different matter. readlink -f works on Linux (excluding some stripped-down BusyBox systems), FreeBSD, NetBSD, OpenBSD and Cygwin, but not on OS/X, AIX, HP/UX or Solaris. If you have readlink, you can call it in a loop:

realpath () {
  [ -e "$1" ] || return
  case $1 in
    /*) :;;
    *) set "$PWD/$1";;
  esac
  while [ -L "$1" ]; do
    set "${1%/*}" "$(readlink "$1")"
    case $2 in
      /*) set "$2";;
      *) if [ -z "$1" ]; then set "/$2"; else set "$(cd "$1" && pwd -P)/$2"; fi;;
    esac
  done
  case $1 in
    */.|*/..) set "$(cd "$1" && pwd -P)";;
    */./*|*/../*) set "$(cd "${1%/*}" && pwd -P)/${1##*/}"
  esac
  realpath=$1
}

If you don't have readlink, you can approximate it with ls -n, but this only works if ls doesn't mangle any non-printable character in the file name.

poor_mans_readlink () {
  if [ -L "$1" ]; then
    set -- "$1" "$(LC_ALL=C command ls -n -- "$2"; echo z)"
    set -- "${2%??}"
    set -- "${2#*"$1 -> "}"
  fi
  printf '%s\n' "$1"
}

(The extra z is in case the link target ends in a newline, which command substitution would otherwise eat up. The realpath function doesn't handle that case for directory names, by the way.)

  • 1
    Do you know of any ls implementation that mangles non-printable characters when the output doesn't go to a terminal. – Stéphane Chazelas Jun 11 '14 at 10:09
  • 1
    @StéphaneChazelas touch Stéphane; LC_ALL=C busybox ls Stéphane | catSt??phane (that's if the name is in UTF-8, latin1 gives you a single ?). I think I've seen that on older commercial Unices too. – Gilles 'SO- stop being evil' Jun 11 '14 at 18:11
  • @StéphaneChazelas I've fixed several bugs but not tested extensively. Let me know if it still fails in some cases (other than lack of execution permissions in some directories, I'm not going to deal with that edge case). – Gilles 'SO- stop being evil' Jul 04 '14 at 15:45
  • @Gilles - what busybox is this? according to git busybox ls hasn't had a code change since 2011. My busybox ls - circa 2013 - does not do this thing. This one - circa 2012 - does. This might explain why. Have you built your busybox with Unicode support - to include wchar support? You might want to give it a go, else to check the build options in the mkinitcpio busybox package. – mikeserv Aug 27 '14 at 18:07
  • Gilles - I believe I initially misjudged this answer - or at least a part of it. While I firmly believe your mangling filenames mantra to be utter fallacy, definitely your poor_mans_readlink is very well done. If you will do me the kindness of making an edit - any edit will do - and pinging me afterward, I should like to reverse my vote on this. – mikeserv Aug 30 '14 at 08:03
3

Provided you have execute permissions on the current directory - or on the directory from which you executed your shell script - if you want an absolute path to a directory all you need is cd.

Step 10 of cd's spec

If the -P option is in effect, the $PWD environment variable shall be set to the string that would be output by pwd -P. If there is insufficient permission on the new directory, or on any parent of that directory, to determine the current working directory, the value of the $PWD environment variable is unspecified.

And on pwd -P

The pathname written to standard output shall not contain any components that refer to files of type symbolic link. If there are multiple pathnames that the pwd utility could write to standard output, one beginning with a single /slash character and one or more beginning with two /slash characters, then it shall write the pathname beginning with a single /slash character. The pathname shall not contain any unnecessary /slash characters after the leading one or two /slash characters.

It's because cd -P has to set the current working directory to what pwd -P should otherwise print and that cd - has to print the $OLDPWD that the following works:

mkdir ./dir
ln -s ./dir ./ln
cd ./ln ; cd . ; cd -

OUTPUT

/home/mikeserv/test/ln

wait for it...

cd -P . ; cd . ; cd -

OUTPUT

/home/mikeserv/test/dir

And when I print with cd - I'm printing $OLDPWD. cd sets $PWD as soon as I cd -P . $PWD is now an absolute path to / - so I don't need any other variables. And actually, I shouldn't even need the trailing . but there is a specified behavior of resetting $PWD to $HOME in an interactive shell when cd is unadorned. So it's just a good habit to develop.

So just doing the above on the path in ${0%/*} should be more than enough to verify $0's path, but in the case that $0 is itself a soft-link, you probably cannot change directory into it, unfortunately.

Here is a function that will handle that:

zpath() { cd -P . || return
    _out() { printf "%s$_zdlm\n" "$PWD/${1##*/}"; }
    _cd()  { cd -P "$1" ; } >/dev/null 2>&1
    while [ $# -gt 0 ] && _cd .
    do  if     _cd "$1" 
        then   _out
        elif ! [ -L "$1" ] && [ -e "$1" ] 
        then   _cd "${1%/*}"; _out "$1"
        elif   [ -L "$1" ]
        then   ( while set -- "${1%?/}"; _cd "${1%/*}"; [ -L "${1##*/}" ]
                 do    set " $1" "$(_cd -; ls -nd -- "$1"; echo /)"
                       set -- "${2#*"$1" -> }"
                 done; _out "$1" 
    );  else   ( PS4=ERR:\ NO_SUCH_PATH; set -x; : "$1" )
    fi; _cd -; shift; done
    unset -f _out _cd; unset -v _zdlm                                    
}

It strives to do as much as it might in the current shell - without invoking a subshell - though there are subshells invoked for errors and soft links which do not point to directories. It depends on a POSIX-compatible shell and a POSIX-compatible ls as well as a clean _function() namespace. It will still function just fine without the latter, though it may overwrite then unset some current shell functions in that case. In general all of these dependencies should be pretty reliably available on a Unix machine.

Called with or without arguments the first thing it does is reset $PWD to its canonical value - it resolves any links therein to their targets as necessary. Called without arguments and that's about it; but called with them and it will resolve and canonicalize the path for each or else print a message to stderr why not.

Because it mostly operates in the current shell it should be able to handle an argument list of any length. It also looks for the $_zdlm variable (which it also unsets when it is through) and prints its C-escaped value immediately to the right of each of its arguments, each of which is always followed also by a single \newline character.

It does a lot of directory changing, but, other than setting it to its canonical value, it does not affect $PWD, though $OLDPWD cannot by any means be counted upon when it is through.

It tries to quit each of its arguments as soon as it might. It first tries to cd into $1. If it can it prints the argument's canonical path to stdout. If it cannot it checks that $1 exists and is not a soft link. If true, it prints.

In this way it handles any file type argument which the shell has permissions to address unless $1 is a symbolic link that does not point to a directory. In that case it calls while loop in a subshell.

It calls ls to read the link. The current directory must be changed to its initial value first in order to reliably handle any referent paths and so, in the command substitution subshell the function does:

cd -...ls...echo /

It strips from the left of ls's output as little as it must to fully contain the link's name and the string ->. While I at first tried to avoid do this with shift and $IFS it turns out this is the most reliable method as near as I can figure. This is the same thing Gilles's poor_mans_readlink does - and it is well done.

It will repeat this process in a loop until the filename returned from ls is definitely not a soft link. At that point it canonicalizes that path as before with cd then prints.

Example usage:

zpath \
    /tmp/script \   #symlink to $HOME/test/dir/script.sh
    ln \            #symlink to ./dir/
    ln/nl \         #symlink to ../..
    /dev/fd/0 \     #currently a here-document like : dash <<\HD
    /dev/fd/1 \     #(zlink) | dash
    file \          #regular file                                             
    doesntexist \   #doesnt exist
    /dev/disk/by-path/pci-0000:00:16.2-usb-0:3:1.0-scsi-0:0:0:0 \
    /dev/./././././././null \
    . ..      

OUTPUT

/home/mikeserv/test/dir/script.sh
/home/mikeserv/test/dir/
/home/mikeserv/test/
/tmp/zshtpKRVx (deleted)
/proc/17420/fd/pipe:[1782312]
/home/mikeserv/test/file
ERR: NO_SUCH_PATH: doesntexist
/dev/sdd
/dev/null
/home/mikeserv/test/
/home/mikeserv/

Or possibly...

ls
dir/  file  file?  folder/  link@  ln@  script*  script3@  script4@

zdlm=\\0 zpath * | cat -A

OUTPUT

/home/mikeserv/test/dir/^@$               
/home/mikeserv/test/file^@$
/home/mikeserv/test/file$
^@$
/home/mikeserv/test/folder/^@$
/home/mikeserv/test/file$               #'link' -> 'file\n'
^@$
/home/mikeserv/test/dir/^@$             #'ln' -> './dir'
/home/mikeserv/test/script^@$
/home/mikeserv/test/dir/script.sh^@$    #'script3' -> './dir/script.sh'
/home/mikeserv/test/dir/script.sh^@$    #'script4' -> '/tmp/script' -> ...
mikeserv
  • 58,310
0

what about a sympathetic one-liner when theres python available to prevent from redefining an algorithm?

function readlink { python -c "import os.path; print os.path.realpath('$1')"; }

same as https://stackoverflow.com/a/7305217