38

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?

Evan Carroll
  • 30,763
  • 48
  • 183
  • 315
jesse_b
  • 37,005
  • 3
    https://stackoverflow.com/questions/4511407/how-do-i-know-if-im-running-a-nested-shell – K7AAY Jun 12 '19 at 18:40
  • I am no expert but quickly looking things up it looks like you are already doing things correctly. $SHLVL keeps track of what level you are at. anything more than 1 would be a subshell. – kemotep Jun 12 '19 at 18:41
  • @K7AAY: Yeah that's where I got the SHLVL idea from but unfortunately it doesn't work from a subshell only a new bash invocation. @kemotep if you look at the example at the top of my question you can see that SHLVL in fact does not work. – jesse_b Jun 12 '19 at 18:42
  • 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 '19 at 18:52
  • 1
  • 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 '19 at 09:39
  • 2
    Saw the title on HNQ and thought this was a quantum mechanics question... – user541686 Jun 13 '19 at 22:47
  • I feel like I saw a movie about this once. Something like you need to keep an item with you at all times as an anchor... like a top. If you are in a sub shell, the top keeps spinning and never falls over. If you are in the top level shell it eventually stops and topples over. – Greg Burghardt Jun 14 '19 at 17:36
  • @GregBurghardt Inception – jrw32982 Jun 19 '19 at 20:58

3 Answers3

50

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.

derobert
  • 109,670
49

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
Freddy
  • 25,565
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
  • 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 '19 at 01:20
  • @muru still feels like a bug. if it hadn't begun executing yet then how come that $BASHPID already has the right value? –  Jun 13 '19 at 05:47
  • Begun executing as in begun executing a command (which it hasn't, it's still doing various expansions). So process forks (BASHPID changes) -> expansions done -> executions starts (BASH_SUBSHELL increments here). So { echo $BASH_SUBSHELL $BASHPID; } | cat will have different output from echo $BASH_SUBSHELL $BASHPID | cat because in the first case, execution has begun with the compound command, and then it moves on to expanding the variables for the simple command. – muru Jun 13 '19 at 05:56
  • 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 '19 at 06: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 ;-) –  Jun 13 '19 at 06:18
  • 2
    You're right. Seems Chet explicitly intends it that way. https://lists.gnu.org/archive/html/bug-bash/2015-06/msg00050.html : "BASH_SUBSHELL measures (...) subshells, not pipeline elements." https://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 '19 at 07:11
  • The echo does run in a separate process, but the expansion of $BASH_SUBSHELL in your example doesn't. Bash has no need of a subshell there, because it's just one command it needs to exec. It doesn't need to run more shell-code after that. I would argue that the (i.e., in a subshell) you quoted from the manpage is a mistake in the documentation. They probably meant e.g. instead of i.e.. As an example of a pipe that does imply a subshell, for i in 1; do echo $BASH_SUBSHELL; done | cat outputs 1. – JoL Jun 13 '19 at 15:57
  • 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. –  Jun 13 '19 at 19:45