In the most common cases, $0
will contain a path, absolute or relative to the script, so
script_path=$(readlink -e -- "$0")
(assuming there's a readlink
command and it supports -e
) generally is a good enough way to obtain the canonical absolute path to the script.
$0
is assigned from the argument specifying the script as passed to the interpreter.
For example, in:
the-shell -shell-options the/script its args
$0
gets the/script
.
When you run:
the/script its args
Your shell will do a:
exec("the/script", ["the/script", "its", "args"])
If the script contains a #! /bin/sh -
she-bang for instance, the system will transform that to:
exec("/bin/sh", ["/bin/sh" or "the/script", "-", "the/script", "its", "args"])
(if it doesn't contain a she-bang, or more generally if the system returns a ENOEXEC error, then it's your shell that will do the same thing)
There's an exception for setuid/setgid scripts on some systems, where the system will open the script on some fd
x
and run instead:
exec("/bin/sh", ["/bin/sh" or "the/script", "-", "/dev/fd/x", "its", "args"])
to avoid race conditions (in which case $0
will contain /dev/fd/x
).
Now, you may argue that /dev/fd/x
is a path to that script. Note however that if you read from $0
, you'll break the script as you consume the input.
Now, there's a difference if the script command name as invoked doesn't contain a slash. In:
the-script its args
Your shell will look up the-script
in $PATH
. $PATH
may contain absolute or relative (including the empty string) paths to some directories. For instance, if $PATH
contains /bin:/usr/bin:
and the-script
is found in the current directory, the shell will do a:
exec("the-script", ["the-script", "its", "args"])
which will become:
exec("/bin/sh", ["/bin/sh" or "the-script", "-", "the-script", "its", "args"]
Or if it's found in /usr/bin
:
exec("/usr/bin/the-script", ["the-script", "its", "args"])
exec("/bin/sh", ["/bin/sh" or "the-script" or "/usr/bin/the-script",
"-", "/usr/bin/the-script", "its", "args")
In all those cases above except the setuid corner case, $0
will contain a path (absolute or relative) to the script.
Now, a script can also be called as:
the-interpreter the-script its args
When the-script
as above doesn't contain slash characters, the behaviour varies slightly from shell to shell.
Old AT&T ksh
implementations were actually looking up the-script unconditionally in $PATH
(which was actually a bug and a security hole for setuid scripts), so $0
actually did not contain a path to the script unless the $PATH
lookup actually happened to find the-script
in the current directory.
Newer AT&T ksh
would try and interpret the-script
in the current directory if it's readable. If not it would lookup for a readable and executable the-script
in $PATH
.
For bash
, it checks if the-script
is in the current directory (and is not a broken symlink) and if not, lookup for a readable (not necessarily executable) the-script
in $PATH
.
zsh
in sh
emulation would do like bash
except that if the-script
is a broken symlink in the current directory, it would not search for a the-script
in $PATH
and would instead report an error.
All the other Bourne-like shells don't look the-script
up in $PATH
.
For all those shells anyway, if you find that $0
doesn't contain a /
and is not readable, then it probably has been looked up in $PATH
. Then, as files in $PATH
are likely to be executable, it's probably a safe approximation to use command -v -- "$0"
to find its path (though that wouldn't work if $0
happens to also be the name of a shell builtin or keyword (in most shells)).
So if you really want to cover for that case, you could write it:
progname=$0
[ -r "$progname" ] || progname=$(
IFS=:; set -f
for i in ${PATH-$(getconf PATH)}""; do
case $i in
"") p=$progname;;
*/) p=$i$progname;;
*) p=$i/$progname
esac
[ -r "$p" ] && exec printf '%s\n' "$p"
done
exit 1
) && progname=$(readlink -e -- "$progname") ||
progname=unknown
(the ""
appended to $PATH
is to preserve a trailing empty element with shells whose $IFS
acts as delimiter instead of separator).
Now, there are more esoteric ways to invoke a script. One could do:
the-shell < the-script
Or:
cat the-script | the-shell
In that case, $0
will be the first argument (argv[0]
) that the interpreter received (above the-shell
, but that could be anything though generally either the basename or one path to that interpreter).
Detecting that you're in that situation based on the value of $0
is not reliable. You could look at the output of ps -o args= -p "$$"
to get a clue. In the pipe case, there's no real way you can get back to a path to the script.
One could also do:
the-shell -c '. the-script' blah blih
Then, except in zsh
(and some old implementation of the Bourne shell), $0
would be blah
. Again, hard to get to the path of the script in those shells.
Or:
the-shell -c "$(cat the-script)" blah blih
etc.
To make sure you have the right $progname
, you could search for a specific string in it like:
progname=$0
[ -r "$progname" ] || progname=$(
IFS=:; set -f
for i in ${PATH-$(getconf PATH)}:; do
case $i in
"") p=$progname;;
*/) p=$i$progname;;
*) p=$i/$progname
esac
[ -r "$p" ] && exec printf '%s\n' "$p"
done
exit 1
) && progname=$(readlink -e -- "$progname") ||
progname=unknown
[ -f "$progname" ] && grep -q 7YQLVVD3UIUDTA32LSE8U9UOHH < "$progname" ||
progname=unknown
But again I don't think it's worth the effort.
Best Answer
These should work the same, as long as there are no symlinks (in the path expansion or the script itself):
MYDIR="$(dirname "$(realpath "$0")")"
MYDIR="$(dirname "$(which "$0")")"
A two step version of any of the above:
MYSELF="$(realpath "$0")"
MYDIR="${MYSELF%/*}"
If there is a symlink on the way to your script, then
which
will provide an answer not including resolution of that link. Ifrealpath
is not installed by default on your system, you can find it here.[EDIT]: As it seems that
realpath
has no advantage overreadlink -f
suggested by Caleb, it is probably better to use the latter. My timing tests indicate it is actually faster.