Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

tempfile not being given the right name. $BASHPID is changing?

Tags:

bash

I thought that this should be straight forward. Generate a temp file and output it. Apparently, there's some strangeness happening behind the scenes.

function x {
  cat $2 > /tmp/$BASHPID.$$;
  cat /tmp/$BASHPID.$$;  # < FAILS because $BASHPID has changed???
};
echo a>/tmp/junk;
x $$ /tmp/junk &

This works however:

function x {
  local tmp=/tmp/$BASHPID.$$;
  cat $2 > $tmp;
  cat $tmp;  # < WORKS!
};
echo a>/tmp/junk;
x $$ /tmp/junk &

What exactly is $BASHPID? I thought it is basically like $$ except if executed in a subprocess, it will get the subprocesses PID. When executing cat, is it actually getting cat's PID?

like image 883
Adrian Avatar asked Dec 29 '25 16:12

Adrian


2 Answers

This is a particular instance of a general issue with subshells in bash. You don't need $BASHPID to see it.

The bash manual is quite clear that redirections and assignments are expanded after all other words have been expanded:

When a simple command is executed, the shell performs the following expansions, assignments, and redirections, from left to right.

  1. The words that the parser has marked as variable assignments (those preceding the command name) and redirections are saved for later processing.
  2. The words that are not variable assignments or redirections are expanded (see Shell Expansions). If any words remain after expansion, the first word is taken to be the name of the command and the remaining words are the arguments. …

Consequently, the following should not be surprising:

$ unset tmp; a=$tmp eval "echo ${tmp:=foo} \$a"
foo foo

Here, ${tmp:=foo} is expanded before a=$tmp with the result that a=foo is passed into the environment for the evaluation of echo foo $a.

From here, things get murkier. The manual says that redirections are expanded before assignments (actually points 3 and 4; I can't get markdown to co-operate):

  1. Redirections are performed as described above (see Redirections).
  2. The text after the ‘=’ in each variable assignment undergoes tilde expansion, parameter expansion, command substitution, arithmetic expansion, and quote removal before being assigned to the variable.

But as we see here, assignments seem to be expanded first:

$ unset tmp; a=$tmp eval <${tmp:=foo} 'echo :$a:'
::
$ unset tmp; <${tmp:=foo} a=$tmp eval 'echo :$a:'
::

And while the manual is explicit that once bash determines that a command is an external utility:

the shell executes the named program in a separate execution environment (from the next section of the manual.)

it's not explicit how much of the word expansion in assignments and redirects is done in the command's execution environment and how much is done in the original environment. And, indeed, the behaviour is not easily predictable.

From experimentation (with bash 4.2), we can see that:

  1. Assignments are expanded in the parent environment.
  2. Redirections are expanded in the child environment, if there is one.
  3. In the case of builtins, no child is created, and redirections are expanded in the parent:
$ unset tmp; a=${tmp:=foo} cat </dev/null; echo tmp=$tmp
tmp=foo
$ unset tmp; >${tmp:=foo} cat </dev/null; echo tmp=$tmp
tmp=
$ unset tmp; >${tmp:=foo} echo </dev/null; echo tmp=$tmp
tmp=foo

All of the above is, at least, reasonable, although the order of assignments and redirections should, IMHO, be documented more accurately. However, the following -- where the expansion in a stdin redirection is evaluated twice if the file doesn't exist -- is certainly a bug:

$ tmp=0; /bin/echo .. <$((tmp+=2)); echo $tmp
bash: 4: No such file or directory
0

# As expected, with a builtin tmp is altered in the parent environment
$ tmp=0; echo .. <$((tmp+=2)); echo $tmp
bash: 4: No such file or directory
4

Since $BASHPID is always the pid of the process in which $BASHPID is actually expanded, all of these various issues also affect its value. In general, I think, the only safe rule is:

  • Do not modify the environment or depend on $BASHPID anywhere other than:
    • a stand-alone assignment, or
    • a simple parameter expansion in a command or argument word.

As a side note, the best way to generate a name for a temporary file is with the mktemp utility.

like image 143
rici Avatar answered Jan 01 '26 10:01

rici


The reason is that cat isn't a shell builtin. As such, the shell needs to fork and exec.

This causes the BASHPID in the following line:

cat $2 > /tmp/$BASHPID.$$;

to be the PID of the child process. Note that the special variable $$, on the other hand, is expanded before forking. As such, you could think of it as saying:

cat $2 > /tmp/$BASHPID.xxxxx

where xxxxx refers to the shell process executing the function.


A simpler way to observe that would be by saying:

touch $BASHPID; cat $BASHPID > $BASHPID

You'd observe that the filename would be different from the contents of the file. Essentially implying that the BASHPID changed.

On the other hand, saying:

touch $BASHPID; echo $BASHPID > $BASHPID

would result in the file contents being same as it's name.

like image 29
devnull Avatar answered Jan 01 '26 11:01

devnull



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!