Carefully mimicking Argv[0] with bash
Asked Answered
T

1

6

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.

Tapir answered 21/5, 2016 at 23:21 Comment(0)
M
2

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.

Moye answered 22/5, 2016 at 0:12 Comment(4)
Thanks for the reply. printargs.sh does indeed have a shebang. I posted it's 2-line source code. This still still leaves the important part of the question: what's the right way to accurately mimic $argv[0] with a bash script?Tapir
@jason, yeah, sorry, i was interrupted. I'll finish the answer in a couple of hours.Moye
@jason: OK, rewrote the answer; hope it helps. I don't think there is a "right way", since argv[0] might be this or it might be that, and ideally a bash script will work with either. So if you're testing, you should test the script with various values for $0. If you're just trying to give it something plausible, my suggestion is to use the full absolute filename.Moye
thanks for the detailed reply. This isn't for testing. I really need to accurately pass argv[0], as an (ELF) executable would get it. Basically, based on what you've said, the documentation, and some experimentation, I'm finding that since bash scripts exec differently, they simply don't have access to the string used when execing an ELF executable. I ended up wrapping the bash script with a tiny executable that passes the arguments, including argv[0] correctly. This does mean that it's impossible for bash script to transparently replace an executable. I find that vexing.Tapir

© 2022 - 2024 — McMap. All rights reserved.