37

I'm trying to write a function to replace the functionality of the exit builtin to prevent myself from exiting the terminal.

I have attempted to use the SHLVL environment variable but it doesn't seem to change within subshells:

$ echo $SHLVL
1
$ ( echo $SHLVL )
1
$ bash -c 'echo $SHLVL'
2

My function is as follows:

exit () {
    if [[ $SHLVL -eq 1 ]]; then
        printf '%s\n' "Nice try!" >&2
    else
        command exit
    fi
}

This won't allow me to use exit within subshells though:

$ exit
Nice try!
$ (exit)
Nice try!

What is a good method to detect whether or not I am in a subshell?

9
  • 3
    stackoverflow.com/questions/4511407/…
    – K7AAY
    Jun 12, 2019 at 18:40
  • 1
    That is because of this. $SHLVL is 1 because you are still in shell level 1 even though the echo $SHLVL command is run in a "subshell". According to that post, subshells spawned with parenthesis (...) inherit all the properties of the parent process. The answers provided are more robust solutions to determining your shell level.
    – kemotep
    Jun 12, 2019 at 18:52
  • 1
    Possible duplicate of How can I get the pid of a subshell?
    – user313992
    Jun 13, 2019 at 5:45
  • 5
    @mosvy I feel like that is a different question. e.g. the BASH_SUBSHELL answer (even if controversial) wouldn't apply to that question.
    – Sparhawk
    Jun 13, 2019 at 9:39
  • 2
    Saw the title on HNQ and thought this was a quantum mechanics question...
    – user541686
    Jun 13, 2019 at 22:47

3 Answers 3

49

In bash, you can compare $BASHPID to $$

$ ( if [ "$$" -eq "$BASHPID" ]; then echo not subshell; else echo subshell; fi )
subshell
$   if [ "$$" -eq "$BASHPID" ]; then echo not subshell; else echo subshell; fi
not subshell

If you're not in bash, $$ should remain the same in a subshell, so you'd need some other way of getting your actual process ID.

One way to get your actual pid is sh -c 'echo $PPID'. If you just put that in a plain ( … ) it may appear not to work, as your shell has optimized away the fork. Try extra no-op commands ( : ; sh -c 'echo $PPID'; : ) to make it think the subshell is too complicated to optimize away. Credit goes to John1024 on Stack Overflow for that approach.

2
  • You might want to change that to (sh -c 'echo $PPID'; : ) — see my comment on John1024’s answer. Jun 19, 2019 at 21:15
  • @G-Man Well, that was just to test it (since in actual use it'd be in something way more complicated)... but yeah, would be best if the test worked in all shells. So I've put a no-op both before and after, that will hopefully handle everything.
    – derobert
    Jun 19, 2019 at 21:45
48

How about BASH_SUBSHELL?

BASH_SUBSHELL
      Incremented by one within each subshell or subshell environment when the shell
      begins executing in that environment. The initial value is 0.

$ echo $BASH_SUBSHELL
0
$ (echo $BASH_SUBSHELL)
1
2
  • 21
    It would have been a convenient command in the movie Inception. Jun 13, 2019 at 18:56
  • In Inception it's probably $SHLVL Jun 14, 2019 at 17:14
21

[this should've been a comment, but my comments tend to be deleted by moderators, so this will stay as an answer that I could use it as a reference even if deleted]

Using BASH_SUBSHELL is completely unreliable as it be only set to 1 in some subshells, not in all subshells.

$ (echo $BASH_SUBSHELL)
1
$ echo $BASH_SUBSHELL | cat
0

Before claiming that the subprocess a pipeline command is run in is not a really real subshell, consider this man bash snippet:

Each command in a pipeline is executed as a separate process (i.e., in a subshell).

and the practical implications -- it's whether a script fragment is run a subprocess or not which is essential, not some terminology quibble.

The only solution, as already explained in the answers to this question is to check whether $BASHPID equals $$ or, portably but much less efficient:

if [ "$(exec sh -c 'echo "$PPID"')" != "$$" ]; then
    echo you\'re in a subshell
fi
8
  • 11
    Nit: BASH_SUBSHELL is set pretty reliably, but getting its value correctly is iffy. Note what the docs say: "Incremented by one within each subshell or subshell environment when the shell begins executing in that environment." I think that in the pipe example, bash hasn't yet begun executing in that subshell when the variable is expanded. You can compare echo $BASH_VERSION with declare -p BASH_VERSION - the latter should reliably output 1 with pipes, background jobs, etc.
    – muru
    Jun 13, 2019 at 1:20
  • 6
    Even say, eval 'echo $BASH_SUBSHELL $BASHPID' | cat will output 1 for BASH_SUBSHELL, because the variable is expanded after execution has started.
    – muru
    Jun 13, 2019 at 6:02
  • 4
    all those arguments should also apply to to process & commands substitution, bg processes, yet it's only the pipelines which are different. Looking at the code, incrementing subshell_level really is deferred in the case of foreground pipelines, which probably has some reason, but which I'm not able to make out ;-)
    – user313992
    Jun 13, 2019 at 6:18
  • 2
    You're right. Seems Chet explicitly intends it that way. lists.gnu.org/archive/html/bug-bash/2015-06/msg00050.html : "BASH_SUBSHELL measures (...) subshells, not pipeline elements." lists.gnu.org/archive/html/bug-bash/2015-06/msg00054.html: "I'm going to think about whether I should document the status quo or expand the definition of `subshell' that $BASH_SUBSHELL reflects."
    – muru
    Jun 13, 2019 at 7:11
  • 2
    @JoL you're wrong, the expansion happens in the separate process too, please read the links and examples from this discussion above; or just try with echo $$ $BASHPID $BASH_SUBSHELL | cat.
    – user313992
    Jun 13, 2019 at 19:45

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .