ffledgling ffledgling - 1 month ago 4x
Bash Question

Odd behaviour in bash (and possibly other shells?)

When I do:

/bin/bash -c 'cat /proc/$$/cmdline'

The output I get is:


Whereas the output I expected was:

/bin/bash -c 'cat /proc/$$/cmdline'

On the other hand when I do:

/bin/bash -c 'echo $$; cat /proc/$$/cmdline'

I get the expected output, which is:

/bin/bash-cecho $$; cat /proc/$$/cmdline

It seems like $$ is cat's pid rather than bash/sh's pid.

Why is this?

Does the shell do some kind of parsing and
style replace? If so, how does it know cat's PID before it even does the replace?


In order to understand this behaviour, one has to figure how bash executes commands passed to it on the command line. The key point is that if the command is simple enough, there's no fork (or clone or anything like that).

$ strace -f -e clone,execve /bin/bash -c 'cat /proc/$$/cmdline'
execve("/bin/bash", ["/bin/bash", "-c", "cat /proc/$$/cmdline"], [/* 80 vars */]) = 0
execve("/bin/cat", ["cat", "/proc/2942/cmdline"], [/* 80 vars */]) = 0
cat/proc/2942/cmdline+++ exited with 0 +++

OTOH if the command is more complicated, bash forks:

$ strace -f -e clone,execve /bin/bash -c 'echo $$; cat /proc/$$/cmdline'
execve("/bin/bash", ["/bin/bash", "-c", "echo $$; cat /proc/$$/cmdline"], [/* 80 vars */]) = 0
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7ff64e6779d0) = 2934
Process 2934 attached
[pid  2934] execve("/bin/cat", ["cat", "/proc/2933/cmdline"], [/* 80 vars */]) = 0
/bin/bash-cecho $$; cat /proc/$$/cmdline[pid  2934] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2934, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
+++ exited with 0 +++

It seems like $$ is cat's pid rather than bash/sh's pid.

It's actually both. bash execves cat directly, so one becomes the other.

To understand what exactly is needed for the no-fork behaviour, we need to look at the source code. There's this comment:

       * IF
       *   we were invoked as `bash -c' (startup_state == 2) AND
       *   parse_and_execute has not been called recursively AND
       *   we're not running a trap AND
       *   we have parsed the full command (string == '\0') AND
       *   we're not going to run the exit trap AND
       *   we have a simple command without redirections AND
       *   the command is not being timed AND
       *   the command's return status is not being inverted
       * THEN
       *   tell the execution code that we don't need to fork