I'm trying to write a bash wrapper script that very carefully mimics the value of argv[0]/$0. I'm using exec -a to execute a separate program with the wrapper's argv[0] value. I'm finding that sometimes bash's $0 doesn't give the same value I'd get in a C-program's argv[0]. Here's a simple test program that demonstrates the difference in both C and bash:
int main(int argc, char* argv[0])
{
printf("Argv[0]=%s\n", argv[0]);
return 0;
}
and
#!/bin/bash
echo \$0=$0
When running these programs with the full (absolute or relative) path to the binary, they behave the same:
$ /path/to/printargv
Argv[0]=/path/to/printargv
$ /path/to/printargv.sh
$0=/path/to/printargv.sh
$ to/printargv
Argv[0]=to/printargv
$ to/printargv.sh
$0=to/printargv.sh
But when invoking them as if they are in the path, I get different results:
$ printargv
Arv[0]=printargv
$ printargv.sh
$0=/path/to/printargv.sh
Two questions:
1) Is this intended behavior that can be explained, or is this a bug? 2) What's the "right" way to achieve the goal of carefully mimicking argv[0]?
edit: typos.
$? is the exit status of the most recently-executed command; by convention, 0 means success and anything else indicates failure. That line is testing whether the grep command succeeded. The grep manpage states: The exit status is 0 if selected lines are found, and 1 if not found.
If the $0 special variable is used within a Bash script, it can be used to print its name and if it is used directly within the terminal, it can be used to display the name of the current shell.
By convention, argv[0] is the command with which the program is invoked. argv[1] is the first command-line argument. The last argument from the command line is argv[argc - 1] , and argv[argc] is always NULL.
$1 is the first positional parameter passed to the shell. The general format can be written as ${var#patt} too, where patt is matched (shortest match from start) in $var and deleted. Example: var="first=middle=last" echo "${var#*=}" Output: middle=last.
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 theargv
argument ofexecve()
, starting atargv[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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With