Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bash 'swallowing' sub-shell children process when executing a single command

Bumped into an unexpected bash/sh behavior and I wonder someone can explain the rationale behind it, and provide a solution to the question below.

In an interactive bash shell session, I execute:

$ bash -c 'sleep 10 && echo'

With ps on Linux it looks like this:

\_ -bash \_ bash -c sleep 10 && echo \_ sleep 10

The process tree is what I would expect:

  • My interactive bash shell process ($)
  • A children shell process (bash -c ...)
  • a sleep children process

However, if the command portion of my bash -c is a single command, e.g.:

$ bash -c 'sleep 10'

Then the middle sub-shell is swallowed, and my interactive terminal session executes sleep "directly" as children process. The process tree looks like this:

\_ -bash \_ sleep 10

So from process tree perspective, these two produce the same result:

  • $ bash -c 'sleep 10'
  • $ sleep 10

What is going on here?

Now to my question: is there a way to force the intermediate shell, regardless of the complexity of the expression passed to bash -c ...?

(I could append something like ; echo; to my actual command and that "works", but I'd rather not. Is there a more proper way to force the intermediate process into existence?)

(edit: typo in ps output; removed sh tag as suggested in comments; one more typo)

like image 337
Marco Avatar asked Jun 15 '17 20:06

Marco


People also ask

How does bash execute a command?

Executing programs from a script. When the program being executed is a shell script, bash will create a new bash process using a fork. This subshell reads the lines from the shell script one line at a time. Commands on each line are read, interpreted and executed as if they would have come directly from the keyboard.

What is $1 and $2 in shell script?

These are positional arguments of the script. Executing ./script.sh Hello World. Will make $0 = ./script.sh $1 = Hello $2 = World. Note. If you execute ./script.sh , $0 will give output ./script.sh but if you execute it with bash script.sh it will give output script.sh .

What does $@ do bash?

bash [filename] runs the commands saved in a file. $@ refers to all of a shell script's command-line arguments. $1 , $2 , etc., refer to the first command-line argument, the second command-line argument, etc. Place variables in quotes if the values might have spaces in them.

What is $() called in bash?

It turns out, $() is called a command substitution. The command in between $() or backticks (“) is run and the output replaces $() . It can also be described as executing a command inside of another command.


1 Answers

There's actually a comment in the bash source that describes much of the rationale for this feature:

/* If this is a simple command, tell execute_disk_command that it
   might be able to get away without forking and simply exec.
   This means things like ( sleep 10 ) will only cause one fork.
   If we're timing the command or inverting its return value, however,
   we cannot do this optimization. */
if ((user_subshell || user_coproc) && (tcom->type == cm_simple || tcom->type == cm_subshell) &&
    ((tcom->flags & CMD_TIME_PIPELINE) == 0) &&
    ((tcom->flags & CMD_INVERT_RETURN) == 0))
  {
    tcom->flags |= CMD_NO_FORK;
    if (tcom->type == cm_simple)
      tcom->value.Simple->flags |= CMD_NO_FORK;
  }

In the bash -c '...' case, the CMD_NO_FORK flag is set when determined by the should_suppress_fork function in builtins/evalstring.c.

It is always to your benefit to let the shell do this. It only happens when:

  • Input is from a hardcoded string, and the shell is at the last command in that string.
  • There are no further commands, traps, hooks, etc. to be run after the command is complete.
  • The exit status does not need to be inverted or otherwise modified.
  • No redirections need to be backed out.

This saves memory, causes the startup time of the process to be slightly faster (since it doesn't need to be forked), and ensures that signals delivered to your PID go direct to the process you're running, making it possible for the parent of sh -c 'sleep 10' to determine exactly which signal killed sleep, should it in fact be killed by a signal.

However, if for some reason you want to inhibit it, you need but set a trap -- any trap will do:

# run the noop command (:) at exit
bash -c 'trap : EXIT; sleep 10'
like image 146
Charles Duffy Avatar answered Sep 30 '22 20:09

Charles Duffy