Parenthesis Spawn a Subshell

In the following code you might think that Unhappy finish will be triggered, since unhappy_func, returns 1 and we have || right after unhappy func.

unhappy_func(){
  echo "unhappy_func triggered"

  return 1
}

happy_func(){
  echo "happy_func triggered"
  
  return 0
}

main() {
  happy_func && (unhappy_func || return 1)

  echo.green "happy finish"
}

main "${@}" || echo.red "Unhappy finish"

But the output will instead be:

happy_func triggered
unhappy_func triggered
happy finish

What in the world?!

Well. The answer lies in () spawning a sub-shell, meaning a new child process.

Another gotcha related to when you decide to use $$ to spot that child process. (to try to see different ProcessID) Well... you won't see a separate ID, which can make you think that it's still the same process and no sub-shells are running. Look at example below if curious.

Example usage of $$ with ()

unhappy_func(){
  echo "unhappy_func triggered (ScriptPID: $$)"

  return 1
}

happy_func(){
  echo "happy_func triggered (ScriptPID: $$)"

  return 0
}

main() {
  happy_func && (unhappy_func || return 1)

  echo.green "happy finish"
}

main "${@}" || echo.red "Unhappy finish"

Output:

happy_func triggered (ScriptPID: 53279)
unhappy_func triggered (ScriptPID: 53279)
happy finish

This is due to $$ giving the process id of the original shell when sub-shells are spawned using ()


$BASHPID to the rescue

In bash there is another way to spot the real process id: $BASHPID

Let's look at the usage of $BASHPID

unhappy_func(){
  echo "unhappy_func triggered (BASHPID: $BASHPID)"

  return 1
}

happy_func(){
  echo "happy_func triggered (BASHPID: $BASHPID)"

  return 0
}

main() {
  echo "main PID: $$"

  happy_func && (unhappy_func || return 1)

  echo.green "happy finish"
}

main "${@}" || echo.red "Unhappy finish"

Output:

main PID: 54419
happy_func triggered (BASHPID: 54419)
unhappy_func triggered (BASHPID: 54421)
happy finish

Notice two things:

  • BASHPID is equal to $$ while in happy_func
  • BASHPID is not equal to $$ while in unhappy_func

Hence, we can use BASHPID to spot whether we are in a different subshell.

() is a separate process: You env variable changes won't propagate up.

This can be good or bad. But if you modify variable within the function running within () they actually won't change your parent process env variable.

It can be good or bad. However, if you aren't aware that () is spawning a child process and expecting a parent process ENV variable to be changed from a function running within the subshell then it can be quite surprising when it doesn't change.

Example of parent ENV variable not being changed

unhappy_func(){
  MY_ENV="unhappy_func"
  echo "unhappy_func triggered (BASHPID: $BASHPID), MY_ENV: $MY_ENV"

  return 1
}

happy_func(){
  echo "happy_func triggered (BASHPID: $BASHPID), MY_ENV: $MY_ENV"

  return 0
}

main() {
  MY_ENV="main"
  echo "main PID: $$"

  happy_func && (unhappy_func || return 1)

  echo "main MY_ENV: $MY_ENV"
  echo.green "happy finish"
}

main "${@}" || echo.red "Unhappy finish"
main PID: 55154
happy_func triggered (BASHPID: 55154), MY_ENV: main
unhappy_func triggered (BASHPID: 55155), MY_ENV: unhappy_func
main MY_ENV: main
happy finish

What to use instead of () when you want to execute multiple functions after chain event like && or ||

So () should be reserved for special cases when we do in fact want to spawn a sub-shell process using some function that is available in our shell.

However, for typical cases that's not what we want to do. But what do we do when we want to group multiple functions to execute after a chaining && or ||?

Well we can use {}

Use `{}` instead.
unhappy_func1(){
  echo "unhappy_func1 triggered (BASHPID: $BASHPID)"

  return 1
}

unhappy_func2(){
  echo "unhappy_func2 triggered (BASHPID: $BASHPID)"

  return 1
}

happy_func(){
  echo "happy_func triggered (BASHPID: $BASHPID)"

  return 0
}

main() {
  echo "main PID: $$"

  happy_func && {
    unhappy_func1 || return 1
    unhappy_func2 || return 1
  }

  echo.green "happy finish"
}

main "${@}" || echo.red "Unhappy finish"

Output:

main PID: 55814
happy_func triggered (BASHPID: 55814)
unhappy_func1 triggered (BASHPID: 55814)
Unhappy finish

Notice how the BASHPID stays constant. So we aren't spawning any sub shells. Notice that we never hit unhappy_func2 since we || return 1 after call to unhappy_func1 within {}.


Backlinks