Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Carefully mimicking Argv[0] with bash

Tags:

bash

wrapper

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.

like image 695
Jason Hiser Avatar asked May 21 '16 23:05

Jason Hiser


People also ask

What is $? == 0 in shell script?

$? 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.

What is ${ 0 in bash?

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.

What does argv 0 mean?

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.

What does ${ 1 mean in bash script?

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


1 Answers

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.

like image 98
rici Avatar answered Sep 21 '22 06:09

rici