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.
$0is something other than the script, which does answer the question title. However, I'm also interested in situations where$0is the script itself, but does not include the directory. In particular, I'm trying to understand the comment made on the SO answer.