34

readlink -f does not exist on MacOS. The only working solution for Mac OS I managed to find on the net goes like this:

if [[ $(echo $0 | awk '/^\//') == $0 ]]; then
    ABSPATH=$(dirname $0)
else
    ABSPATH=$PWD/$(dirname $0)
fi

Can anyone suggest anything more elegant to this seemingly trivial task?

l0b0
  • 55,365
  • 30
  • 138
  • 223
Ivan Balashov
  • 1,897
  • 1
  • 23
  • 33
  • 1
    See also the closely related question: http://stackoverflow.com/questions/1055671/how-can-i-get-the-behavior-of-gnus-readlink-f-on-a-mac – jhclark Nov 21 '11 at 19:41
  • As of macOS Big Sur (2021), `realpath` command does this and is available via the coreutils Homebrew formula: "brew install coreutils" – Dalmazio Mar 18 '21 at 21:50

11 Answers11

76

Another (also rather ugly) option:

ABSPATH=$(cd "$(dirname "$0")"; pwd -P)

From pwd man page,

-P      Display the physical current working directory (all symbolic links resolved).
Izana
  • 2,537
  • 27
  • 33
Gordon Davisson
  • 118,432
  • 16
  • 123
  • 151
  • 4
    +1, but perhaps ABSPATH=$( cd $(dirname $0); pwd)/$(basename $0) – William Pursell Apr 22 '11 at 18:59
  • 1
    @William Pursell: yes, if you want the path of the script; my understanding (from the example code in the question) was that the question was about the path of its directory. – Gordon Davisson Apr 22 '11 at 19:30
  • If the directory does not exist, this option will produce error. – Li Dong Oct 06 '15 at 06:12
  • 3
    Good solution that comes with caveats: (a) for a symlinked script, the _symlink's_ directory is returned (which may not be a problem); (b) `$0` doesn't reflect the script path if the script is being _sourced_; to fix that, use (Bash-specific) `$BASH_SOURCE` instead; (c) with (unusual) paths that start with `-`, the command will break (easily fixed by using `--` as the 1st argument for both the `cd` and the `dirname` command); finally, it's better not to use all-uppercase shell-variable names in order to avoid conflicts with environment variables and special shell variables. – mklement0 Aug 04 '16 at 01:31
  • @LiDong: Unless someone goes out of their way to redefine the value of `$0`, `dirname -- "$0"` should never fail, but, as stated, `$0` may not refer to the script at hand; using (Bash-specific) `$BASH_SOURCE` instead addresses both issues. – mklement0 Aug 04 '16 at 01:41
  • This doesn't work when the script is invoked as a mac app, because the app can't change directories – Myer May 24 '17 at 19:54
  • 1
    Ugly as it it, it is still the simplest way to get the desired result in a portable way IMO. With the above corrections, this should be (for Bash) `abspath="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")"; pwd)/$(basename -- "${BASH_SOURCE[0]}")"`. – n.caillou Nov 20 '17 at 21:54
  • 1
    Also, you want to use `pwd -P` to resolve all possible link paths on dirname. – Izana Feb 04 '20 at 19:30
  • This doesn't work for multiple levels of links (used by `update-alternatives`). – Cromax Nov 15 '20 at 11:04
10

Get absolute path of shell script

Dug out some old scripts from my .bashrc, and updated the syntax a bit, added a test suite.

Supports

  • source ./script (When called by the . dot operator)
  • Absolute path /path/to/script
  • Relative path like ./script
  • /path/dir1/../dir2/dir3/../script
  • When called from symlink
  • When symlink is nested eg) foo->dir1/dir2/bar bar->./../doe doe->script
  • When caller changes the scripts name

It has been tested and used in real projects with success, however there may be corner cases I am not aware of.
If you were able to find such a situation, please let me know.
(For one, I know that this does not run on the sh shell)

Code

pushd . > /dev/null
SCRIPT_PATH="${BASH_SOURCE[0]}";
  while([ -h "${SCRIPT_PATH}" ]) do 
    cd "`dirname "${SCRIPT_PATH}"`"
    SCRIPT_PATH="$(readlink "`basename "${SCRIPT_PATH}"`")"; 
  done
cd "`dirname "${SCRIPT_PATH}"`" > /dev/null
SCRIPT_PATH="`pwd`";
popd  > /dev/null
echo "srcipt=[${SCRIPT_PATH}]"
echo "pwd   =[`pwd`]"

Known issuse

Script must be on disk somewhere, let it be over a network. If you try to run this script from a PIPE it will not work

wget -o /dev/null -O - http://host.domain/dir/script.sh |bash

Technically speaking, it is undefined.
Practically speaking, there is no sane way to detect this.

Test case used

And the current test case that check that it works.

#!/bin/bash
# setup test enviroment
mkdir -p dir1/dir2
mkdir -p dir3/dir4
ln -s ./dir1/dir2/foo bar
ln -s ./../../dir3/dir4/test.sh dir1/dir2/foo
ln -s ./dir1/dir2/foo2 bar2
ln -s ./../../dir3/dir4/doe dir1/dir2/foo2
cp test.sh ./dir1/dir2/
cp test.sh ./dir3/dir4/
cp test.sh ./dir3/dir4/doe
P="`pwd`"
echo "--- 01"
echo "base  =[${P}]" && ./test.sh
echo "--- 02"
echo "base  =[${P}]" && `pwd`/test.sh
echo "--- 03"
echo "base  =[${P}]" && ./dir1/dir2/../../test.sh
echo "--- 04"
echo "base  =[${P}/dir3/dir4]" && ./bar
echo "--- 05"
echo "base  =[${P}/dir3/dir4]" && ./bar2
echo "--- 06"
echo "base  =[${P}/dir3/dir4]" && `pwd`/bar
echo "--- 07"
echo "base  =[${P}/dir3/dir4]" && `pwd`/bar2
echo "--- 08"
echo "base  =[${P}/dir1/dir2]" && `pwd`/dir3/dir4/../../dir1/dir2/test.sh
echo "--- 09"
echo "base  =[${P}/dir1/dir2]" && ./dir1/dir2/test.sh
echo "--- 10"
echo "base  =[${P}/dir3/dir4]" && ./dir3/dir4/doe
echo "--- 11"
echo "base  =[${P}/dir3/dir4]" && ./dir3/dir4/test.sh
echo "--- 12"
echo "base  =[${P}/dir3/dir4]" && `pwd`/dir3/dir4/doe
echo "--- 13"
echo "base  =[${P}/dir3/dir4]" && `pwd`/dir3/dir4/test.sh
echo "--- 14"
echo "base  =[${P}/dir3/dir4]" && `pwd`/dir1/dir2/../../dir3/dir4/doe
echo "--- 15"
echo "base  =[${P}/dir3/dir4]" && `pwd`/dir1/dir2/../../dir3/dir4/test.sh
echo "--- 16"
echo "base s=[${P}]" && source test.sh
echo "--- 17"
echo "base s=[${P}]" && source `pwd`/test.sh
echo "--- 18"
echo "base s=[${P}/dir1/dir2]" && source ./dir1/dir2/test.sh
echo "--- 19"
echo "base s=[${P}/dir3/dir4]" && source ./dir1/dir2/../../dir3/dir4/test.sh
echo "--- 20"
echo "base s=[${P}/dir3/dir4]" && source `pwd`/dir1/dir2/../../dir3/dir4/test.sh
echo "--- 21"
pushd . >/dev/null
cd ..
echo "base x=[${P}/dir3/dir4]"
./`basename "${P}"`/bar
popd  >/dev/null

PurpleFox aka GreenFox

GreenFox
  • 1,112
  • 10
  • 5
  • Very comprehensive answer. – Samveen Jun 13 '13 at 09:13
  • Well done; one corner case: directories whose name starts with `-` break the script, which you can easily remedy by using `--` as the 1st argument in the various calls. As for not being able to detect when a script is being passed via _stdin_, such as through a _pipeline_: AFAIK, that is the only case in which `$BASH_SOURCE` is _empty_. Find what I believe to be a slightly more robust implementation that is also POSIX-compliant answer (of mine) [here](http://stackoverflow.com/a/24114056/45375). – mklement0 Aug 04 '16 at 02:07
4

Also note that homebrew's (http://brew.sh) coreutils package includes realpath (link created in/opt/local/bin).

$ realpath bin
/Users/nhed/bin
nhed
  • 5,774
  • 3
  • 30
  • 44
4

Using bash I suggest this approach. You first cd to the directory, then you take the current directory using pwd. After that you must return to the old directory to ensure your script does not create side effects to an other script calling it.

cd "$(dirname -- "$0")"
dir="$PWD"
echo "$dir"
cd - > /dev/null

This solution is safe with complex path. You will never have troubles with spaces or special charaters if you put the quotes.

Note: the /dev/null is require or "cd -" print the path its return to.

Lynch
  • 9,174
  • 2
  • 23
  • 34
1

I've found this to be useful for symlinks / dynamic links - works with GNU readlink only though (because of the -f flag):

# detect if GNU readlink is available on OS X
if [ "$(uname)" = "Darwin" ]; then
  which greadlink > /dev/null || {
    printf 'GNU readlink not found\n'
    exit 1
  }
  alias readlink="greadlink"
fi

# create a $dirname variable that contains the file dir
dirname=$(dirname "$(readlink -f "$0")")

# use $dirname to find a relative file
cat "$dirname"/foo/bar.txt
Yoshua Wuyts
  • 3,926
  • 1
  • 20
  • 16
1

If you don't mind using perl:

ABSPATH=$(perl -MCwd=realpath -e "print realpath '$0'")
William Pursell
  • 204,365
  • 48
  • 270
  • 300
1

Can you try something like this inside your script?

echo $(pwd)/"$0"

In my machine it shows:

/home/barun/codes/ns2/link_down/./test.sh

which is the absolute path name of the shell script.

Barun
  • 2,542
  • 33
  • 35
0

If you're using ksh, the ${.sh.file} parameter is set to the absolute pathname of the script. To get the parent directory of the script: ${.sh.file%/*}

0

I use the function below to emulate "readlink -f" for scripts that have to run on both linux and Mac OS X.

#!/bin/bash
# This was re-worked on 2018-10-26 after der@build correctly
# observed that the previous version did not work.

# Works on both linux and Mac OS X.
# The "pwd -P" re-interprets all symlinks.
function read-link() {
    local path=$1
    if [ -d $path ] ; then
        local abspath=$(cd $path; pwd -P)
    else
        local prefix=$(cd $(dirname -- $path) ; pwd -P)
        local suffix=$(basename $path)
        local abspath="$prefix/$suffix"
    fi
    if [ -e $abspath ] ; then
        echo $abspath
    else
        echo 'error: does not exist'
    fi
}

# Example usage.
while (( $# )) ; do
    printf '%-24s - ' "$1"
    read-link $1
    shift
done

This is the output for some common Mac OS X targets:

$ ./example.sh /usr/bin/which /bin/which /etc/racoon ~/Downloads
/usr/bin/which           - /usr/bin/which
/bin/which               - error: does not exist
/etc/racoon              - /private/etc/racoon
/Users/jlinoff/Downloads - /Users/jlinoff/Downloads

The is the output for some linux targets.

$ ./example.sh /usr/bin/which /bin/whichx /etc/init.d ~/Downloads
/usr/bin/which           - /usr/bin/which
/bin/whichx              - error: does not exist
/etc/init.d              - /etc/init.d
/home/jlinoff/Downloads  - /home/jlinoff/Downloads
Joe Linoff
  • 761
  • 9
  • 13
  • What is the problem? I just ran it and got reasonable results for several tests on Mac OSX. I may not be testing the right thing. I also tried it on ubuntu. – Joe Linoff Oct 22 '18 at 21:15
  • der@build:~$ read-link /usr/bin/which /usr/bin/which der@build:~$ readlink /usr/bin/which /bin/which – Der_Meister Oct 23 '18 at 07:35
  • You were correct, the original version did not work. Thank you for taking the time to point that out. I have updated to correct the original problem (i forgot to add the -P option to pwd). – Joe Linoff Oct 26 '18 at 15:02
0

this is what I use, may need a tweak here or there

abspath () 
{ 
    case "${1}" in 
        [./]*)
            local ABSPATH="$(cd ${1%/*}; pwd)/${1##*/}"
            echo "${ABSPATH/\/\///}"
        ;;
        *)
            echo "${PWD}/${1}"
        ;;
    esac
}

This is for any file - and of curse you can just invoke it as abspath ${0}

The first case deals with relative paths by cd-ing to the path and letting pwd figure it out

The second case is for dealing with a local file (where the ${1##/} would not have worked)

This does NOT attempt to undo symlinks!

nhed
  • 5,774
  • 3
  • 30
  • 44
  • Thanks! Do you know if this is any portable? I mean across sh/bash versions? – Ivan Balashov Apr 22 '11 at 18:05
  • @Ivan I do not know, the concept portable but you may need to tweak details. I typically use it on bash under OSX & Linux – nhed Apr 22 '11 at 18:20
  • It would be great if anyone voting down would also leave a note why they think the answer is not productive – nhed Jan 28 '15 at 17:36
0

This works as long as it's not a symlink, and is perhaps marginally less ugly:

ABSPATH=$(dirname $(pwd -P $0)/${0#\.\/})
Magnus
  • 10,736
  • 5
  • 44
  • 57