10

I want to grep the current script so I can print out help and version information from the comments section at the top.

I was thinking of something like this:

grep '^#h ' -- "$0" | sed -e 's/#h //'

But then I wondered what would happen if the script was located in a directory that was in PATH and called without explicitly specifying the directory.

I searched for an explanation of the special variables and found the following descriptions of $0:

  • name of the current shell or program

  • filename of the current script

  • name of the script itself

  • command as it was run

None of these make it clear whether or not the value of $0 would include the directory if the script was invoked without it. The last one actually implies to me that it wouldn't.

Testing on My System (Bash 4.1)

I created an executable file in /usr/local/bin called scriptname with one line echo $0 and invoked it from different locations.

These are my results:

> cd /usr/local/bin/test
> ../scriptname
../scriptname

> cd /usr/local/bin
> ./scriptname
./scriptname

> cd /usr/local
> bin/scriptname
bin/scriptname

> cd /tmp
> /usr/local/bin/scriptname
/usr/local/bin/scriptname

> scriptname
/usr/local/bin/scriptname

In these tests, the value of $0 is always exactly how the script was invoked, except if it is invoked without any path component. In that case, the value of $0 is the absolute path. So that looks like it would be safe to pass to another command.

But then I came across a comment on Stack Overflow that confused me. The answer suggests using $(dirname $0) to get the directory of the current script. The comment (upvoted 7 times) says "that will not work if the script is in your path".

Questions

  • Is that comment correct?
  • Is the behavior different on other systems?
  • Are there situations where $0 would not include the directory?
  • People have answered about situations where $0 is something other than the script, which does answer the question title. However, I'm also interested in situations where $0 is the script itself, but does not include the directory. In particular, I'm trying to understand the comment made on the SO answer. – toxalot Mar 16 '14 at 11:21
17

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

  • Stéphane, I don't understand your use of "-" in the above examples. In my experience, exec("the-script", ["the-script", "its", "args"]) becomes exec("/the/interpreter", ["/the/interpreter", "the-script", "its", "args"]), with of course the possibility of an interpreter option. – jrw32982 Aug 3 '16 at 4:28
  • @jrw32982, #! /bin/sh - is the "always use cmd -- something if you can't guarantee that something won't start with -" good practice adage here applied to /bin/sh (where - as the end-of-option marker is more portable than --) with something being the path/name of the script. If you don't use that for setuid scripts (on systems that support them but not with the /dev/fd/x method mentioned in the answer), then one can get a root shell by creating a symlink to your script called -i or -s for instance. – Stéphane Chazelas Aug 3 '16 at 9:00
  • Thanks, Stéphane. I had missed the trailing single hyphen in your example shebang line. I had to hunt for where it's documented that a single hyphen is equivalent to a double hyphen pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html. – jrw32982 Aug 3 '16 at 20:24
  • It's too easy to forget the trailing single/double hyphen in the shebang line for a setuid script; somehow the system should take care of it for you. Disallow setuid scripts altogether or somehow /bin/sh should disable its own option processing if it can detect that it's running setuid. I don't see how using /dev/fd/x by itself fixes that. You still need the single/double hyphen, I think. – jrw32982 Aug 3 '16 at 20:26
  • @jrw32982, /dev/fd/x starts with /, not -. The primary goal is to remove the race condition between the two execve()s though (in between the execve("the-script") which elevates privileges and the subsequent execve("interpreter", "thescript") where interpreter opens thescript later (which may very well have been replaces with a symlink to something else in the meantime). Systems that implement suid scripts properly do a execve("interpreter", "/dev/fd/n") instead where n has been opened as part of the first execve(). – Stéphane Chazelas Aug 4 '16 at 6:41
6

Here are two situations where the directory would not be included:

> bash scriptname
scriptname

> bash <scriptname
bash

In both cases, the current directory would have to be the directory where scriptname was located.

In the first case, the value of $0 could still be passed to grep since it assumes the FILE argument is relative to the current directory.

In the second case, if the help and version information is only printed in response to a specific command line option, it shouldn't be a problem. I'm not sure why anyone would invoke a script that way to print the help or version information.

Caveats

  • If the script changes the current directory, you wouldn't want to use relative paths.

  • If the script is sourced, the value of $0 will usually be the caller script rather than the sourced script.

3

An arbitrary argument zero can be specified when using the -c option to most (all?) shells. Eg:

sh -c 'echo $0' argv0

From man bash (chosen purely because this has a better description than my man sh - usage is the same regardless):

-c

If the -c option is present, then commands are read from the first non-option argument command_string. If there are arguments after the command_string, they are assigned to the positional parameters, starting with $0.

  • I think this works because -c 'command' are operands, and argv0 is its first nonoperand commandline argument. – mikeserv Mar 16 '14 at 11:10
  • @mike correct, I have updated with a man snippet. – Graeme Mar 16 '14 at 11:19
3

NOTE: Other's have already explained the mechanics of $0 so I'll skip all that.

I generally side step this entire issue and simply use the command readlink -f $0. This will always give you back the full path of whatever you give to it as an argument.

Examples

Say I'm here to start with:

$ pwd
/home/saml/tst/119929/adir

Make a directory + file:

$ mkdir adir
$ touch afile
$ cd adir/

Now start showing off readlink:

$ readlink -f ../adir
/home/saml/tst/119929/adir

$ readlink -f ../
/home/saml/tst/119929

$ readlink -f ../afile 
/home/saml/tst/119929/afile

$ readlink -f .
/home/saml/tst/119929/adir

Additional trickery

Now with a consistent result being returned when we interrogate $0 via readlink we can use a simply dirname $(readlink -f $0) to get the absolute path to the script -or- basename $(readlink -f $0) to get the actual name of the script.

0

My man page says:

$0 : expands to the name of shell or shell script.

It seems this translates to argv[0] of the current shell - or the first nonoperand commandline argument the currently interpreting shell is fed when invoked. I previously stated that sh ./somescript would path its $0 $ENV variable to sh but this was incorrect because the shell is a new process of its own and invoked with a new $ENV.

In this way sh ./somescript.sh differs from . ./somescript.sh which runs in the current environment and $0 is already set.

You can check this by comparing $0 to /proc/$$/status.

echo 'script="/proc/$$/status"
    echo $0
    cat "$script"' \
    > ./script.sh
sh ./script.sh ; . ./script.sh

Thanks for the correction, @toxalot. I learned something.

  • On my system, if it's sourced (. ./myscript.sh or source ./myscript.sh), then $0 is the shell. But if it's passed as an argument to the shell (sh ./myscript.sh), then $0 is the path to the script. Of course, sh on my system is Bash. So I don't know if that makes a difference or not. – toxalot Mar 16 '14 at 10:32
  • Does it have a hashbang? I think that the difference matters - and I can add that - without it sh should not exec it and instead source it, but with it, it should exec it. – mikeserv Mar 16 '14 at 10:34
  • With or without hashbang, I get same results. With or without being executable, I get same results. – toxalot Mar 16 '14 at 10:37
  • Me too! I think it's because it's a shell builtin. I'm checking... – mikeserv Mar 16 '14 at 10:38
  • Bang lines are interpreted by the kernel. If you exec ./somescript with the bangline #!/bin/sh, it would be equivalent to running /bin/sh ./somescript. Otherwise they make no difference to the shell. – Graeme Mar 16 '14 at 11:36
0

I want to grep the current script so I can print out help and version information from the comments section at the top.

While $0 contains the script name it may contain a prefixed path based on the way the script is called, I have always used ${0##*/} to print the script name in the help output which removes any leading path from $0.

Taken from Advanced Bash Scripting guide - Section 10.2 parameter substitution

${var#Pattern}

Remove from $var the shortest part of $Pattern that matches the front end of $var.

${var##Pattern}

Remove from $var the longest part of $Pattern that matches the front end of $var.

So the longest part of $0 that matches */ will be the entire path prefix, returning only the script name.

  • Yes, I do that too for the name of the script that is used within the help message. But I'm talking about grepping the script so I need to have at least a relative path. I want to put the help message at the top in the comments and not have to repeat the help message later when printing it. – toxalot Mar 16 '14 at 13:32
0

For something similar I use:

rPath="$(dirname $(realpath $0))"
echo $rPath 

rPath=$(dirname $(readlink -e -- "$0"))
echo $rPath 

rPath always has the same value.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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