3

From bash manual

_

At shell startup, set to the absolute pathname used to invoke the shell or shell script being executed as passed in the environment or argument list. Subsequently, expands to the last argument to the previous command, after expansion. Also set to the full pathname used to invoke each command executed and placed in the environment exported to that command. When checking mail, this parameter holds the name of the mail file.

Regarding the sentence in bold, Stephane said that

bash, like a few other shells will pass a _ environment variable to commands it executes that contains the path that bash used as the first argument to the execve() system calls.

$ env | grep '^_'
_=/usr/bin/env

How can I get the environment variable _'s value for an arbitrary command instead of env, to verify that its value is the pathname of the executable or script for the command?

Thanks.

Tim
  • 101,790
  • If I'm understanding you correctly, that arbitrary command would need to print out the value of that environment variable, if it's able. Something like perl can do it, but something like cat cannot. Do you want to explore OS options for displaying an arbitrary processes' environment variables? – Jeff Schaller Jun 30 '16 at 00:31
  • yes and yes.... – Tim Jun 30 '16 at 12:08

3 Answers3

7

The $_ value is visible to the child process. How you read it in the child is up to the language you use.

e.g. in C

#include <stdlib.h>
#include <stdio.h>

main()
{
  char *x=getenv("_");
  printf("%s\n",x);
}

However a program can not rely on $_ being set because other shells may do different things.

e.g.

$ env -i /bin/bash -c ./a.out
./a.out

$ env -i /bin/ksh -c ./a.out 
*31801*./a.out

$ env -i /bin/sh -c ./a.out 
Segmentation fault (core dumped)

/bin/sh (which is "dash" on this Ubuntu system) doesn't set $_ at all and so this simple program crashes. Oops :-)

Your calling program can't see this value of $_ at all. Indeed, in the calling shell $_ is set to the last parameter of the previous line

$ ls /tmp > /dev/null
$ echo $_
/tmp

$ echo hello
hello
$ echo $_
hello
1

If you're running on a Linux-based system, you can examine a process's environment by reading the /proc/[pid]/environ pseudo-file -- but only if the process is still running and you have permission.

Its contents are similar to the output of env or printenv except that variables are separated by null characters rather than newlines.

man proc and search for environ for more information.

For example:

$ /bin/sleep 60 &
[1] 25909
$ tr '\0' '\n' < /proc/$!/environ | grep '^_='
_=/bin/sleep
$ 

where $! expands to the PID of the job most recently placed into the background.

That's going to be more difficult for programs that finish quickly.

It's possible for this to produce a false positive if you have an environment variable whose value contains "\n_=", since it doesn't distinguish between newlines that are part of the environment and newlines translated from null characters. That's not likely to be an issue, but if it is you'll need to something a bit different:

perl -nE 'undef $/; say grep /^_=/, split /\0/, scalar <>' /proc/$!/environ
1

That is not possible (in the running shell).

To print an environment variable value you need a command, either use printenv:

$ printenv "LANG"
LANG=en_US.UTF-8

Which will report the value of the variable as exported to the command's environment. Or, you will use echo (or printf, or print) the value of the variable in the present running shell:

$ echo "$LANG"
LANG=en_US.UTF-8

In both cases, a command has been executed and the value of the variable $_ will been set to the last argument of the command executed (if there is any command executed).

$ ls
$ echo ".$_."
.--color=auto.
$ echo ".$_."
..--color=auto..
$ echo ".$_."
...--color=auto...

$ bash -c 'echo $_'
/bin/bash

$ bash -c 'ls -l >>/dev/null; echo $_'
-l

Therefore: any command executed will set $_ for the child shell in which the command was executed and printed as the value of $_ by the same shell.

Some shells do not use or set a temporal $_:

$ dash -c 'ls -l >/dev/null; echo ".$_."'
./bin/dash.

But that will only reveal the value of the path to dash. ..