Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

What is a portable 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?

share|improve this question

6 Answers 6

up vote 10 down vote accepted

With zsh, it's just:

mypath=$0:A

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.

share|improve this answer

Ive 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")")
share|improve this answer
    
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
    
Where it's not equivalent to GNU's readlink -f is when the script itself is a symlink. –  Stéphane Chazelas Jun 11 at 10:26

In zsh you can do the following:

mypath=${0:a}

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

mydir=${0:a:h}

Source: zshexpn(1) man page, section HISTORY EXPANSION, subsection Modifiers (or simply info -f zsh -n Modifiers).

share|improve this answer
    
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 at 20:04
    
The equivalent of GNU's readlink -f would rather be $0:A. –  Stéphane Chazelas Jun 11 at 10:23

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

share|improve this answer
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 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 Jun 11 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 Jul 4 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 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 at 8:03

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
share|improve this answer
    
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 at 19:38
    
@moose What OS are you running ? –  jlliagre Aug 31 at 19:07
    
who is moose? what? –  mikeserv Aug 31 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 at 19:17

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' -> ...
share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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