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.


7 Answers 7


In zsh you can do the following:


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


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.

  • 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'. May 20, 2014 at 20:04
  • 3
    The equivalent of GNU's readlink -f would rather be $0:A. Jun 11, 2014 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 1, 2021 at 7: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. Jan 20, 2022 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, 2022 at 17:40

With zsh, it's just:




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.

  • 1
    Note: this does not work for your .zshrc: See this post instead. Nov 29, 2016 at 4:24
  • 1
    Explanation for :A from man zshall: Turn a file name into an absolute path: prepends the current directory, if necessary; remove .' path segments; and remove ..' path segments and the segments that immediately precede them. Nov 26, 2020 at 22:30

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")")
  • 1
    i like it! so far, that's pretty portable. works on Solaris, OmniOS, Linux, Mac, and even Cygwin on Windows 2008. May 21, 2013 at 17:18
  • 6
    Where it's not equivalent to GNU's readlink -f is when the script itself is a symlink. Jun 11, 2014 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, 2018 at 18:24
  • This is great! Fyi to others: you can get rid of the script name itself by removing the basename part: canonical=$(cd -P -- "$(dirname -- "$0")" && printf '%s\n' "$(pwd -P)")
    – Jack
    Apr 18 at 17:18

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
  • 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, 2014 at 19:38
  • @moose What OS are you running ?
    – jlliagre
    Aug 31, 2014 at 19:07
  • 2
    who is moose? what?
    – mikeserv
    Aug 31, 2014 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, 2014 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 6, 2015 at 13:20

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

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

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";;
  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;;
  case $1 in
    */.|*/..) set "$(cd "$1" && pwd -P)";;
    */./*|*/../*) set "$(cd "${1%/*}" && pwd -P)/${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 -> "}"
  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. Jun 11, 2014 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. Jun 11, 2014 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). Jul 4, 2014 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, 2014 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, 2014 at 8:03

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 -



wait for it...

cd -P . ; cd . ; cd -



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 \
    . ..      


/tmp/zshtpKRVx (deleted)
ERR: NO_SUCH_PATH: doesntexist

Or possibly...

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

zdlm=\\0 zpath * | cat -A


/home/mikeserv/test/file$               #'link' -> 'file\n'
/home/mikeserv/test/dir/^@$             #'ln' -> './dir'
/home/mikeserv/test/dir/script.sh^@$    #'script3' -> './dir/script.sh'
/home/mikeserv/test/dir/script.sh^@$    #'script4' -> '/tmp/script' -> ...

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

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .