What you're seeing here is the documented behaviour of bash
and execve
(at least, it is documented on Linux and FreeBSD; I presume that other systems have similar documentation), and reflects the different ways that argv[0]
is constructed.
Bash (like any other shell) constructs argv
from the provided command line, after having performed the various expansions, resplit words as necessary, and so on. The end result is that when you type
printargv
argv
is constructed as { "printargv", NULL }
and when you type
to/printargv
argv
is constructed as { "to/printargv", NULL }
. So no surprises there.
(In both cases, had there been command line arguments, they would have appeared in argv
starting at position 1.)
But the execution path diverges at that point. When the first word in the command line includes a /, then it is considered to be a filename, either relative or absolute. The shell does no further processing; it simply calls execve
with the provided filename as its filename
argument and the argv
array constructed previously as its argv
argument. In this case, argv[0]
precisely corresponds to the filename
But when the command has no slashes:
printargv
the shell does a lot more work:
First, it checks to see if the name is a user-defined shell function. If so, it executes it, with $1...$n
taken from the argv
array already constructed. ($0
continues to be argv[0]
from the script invocation, though.)
Then, it checks to see if the name is a built-in bash command. If so, it executes it. How built-ins interact with command-line arguments is out of scope for this answer, and is not really user-visible.
Finally, it attempts to find the external utility corresponding with the command, by searching through the components of $PATH
and looking for an executable file. If it finds one, it calls execve
, giving it the path that it found as the filename
argument, but still using the argv
array consisting of the words from the command. So in this case, filename
and argv[0]
do not correspond.
So, in both cases, the shell ends up calling execve
, providing a filepath (possibly relative) as the filename
argument and the word-split command as the argv
argument.
If the indicated file is an executable image, there is nothing more to say, really. The image is loaded into memory, and its main
is called with the provided argv
vector. argv[0]
will be a single word or a relative or absolute path, depending only on what was originally typed.
But if the indicated file is a script, the loader will produce an error and execve
will check to see if the file starts with a shebang (#!
). (Since Posix 2008, execve
will also attempt to run the file as a script using the system shell, as though it had #!/bin/sh
as a shebang line.)
Here's the documentation for execve
on Linux:
An interpreter script is a text file that has execute permission enabled and whose first line is of the form:
#! interpreter [optional-arg]
The interpreter must be a valid pathname for an executable file. If the filename argument of execve() specifies an interpreter script, then interpreter will be invoked with the following arguments:
interpreter [optional-arg] filename arg...
where arg...
is the series of words pointed to by the argv
argument of execve()
, starting at argv[1]
.
Note that in the above, the filename
argument is the filename
argument to execve
. Given the shebang line #!/bin/bash
we now have either
/bin/bash to/printargv # If the original invocation was to/printargv
or
/bin/bash /path/to/printargv # If the original invocation was printargv
Note that argv[0]
has effectively disappeared.
bash
then runs the script in the file. Prior to executing the script, it sets $0
to the filename argument it was given, in our example either to/printargv
or /path/to/printargv
, and sets $1...$n
to the remaining arguments, which were copied from the command-line arguments in the original command line.
In summary, if you invoke the command using a filename with no slashes:
If the filename contains an executable image, it will see argv[0]
as the command name as typed.
If the filename contains a bash script with a shebang line, the script will see $0
as the actual path to the script file.
If you invoke the command using a filename with slashes, in both cases it will see argv[0] as the filename as typed (which might be relative, but will obviously always have a slash).
On the other hand, if you invoke a script by invoking the shell interpreter explicitly (bash printargv
), the script will see $0
as the filename as typed, which not only might be relative but also might not have a slash.
All that means that you can only "carefully mimic argv[0]" if you know what form of invoking the script you wish to mimic. (It also means that the script should never rely on the value of argv[0]
, but that's a different topic.)
If you are doing this for unit testing, you should provide an option to specify what value to provide as argv[0]. Many shell scripts which attempt to analyze $0
assume that it is a filepath. They shouldn't do that, since it might not be, but there it is. If you want to smoke those utilities out, you'll want to supply some garbage value as $0
. Otherwise, your best bet as a default is to provide a path to the scriptfile.