36

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
  • 28,578
  • 45
  • 164
  • 290
jesse_b
  • 35,934
  • 12
  • 91
  • 140
  • 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](https://unix.stackexchange.com/a/138498/276845). $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
    Possible duplicate of [How can I get the pid of a subshell?](https://unix.stackexchange.com/questions/484442/how-can-i-get-the-pid-of-a-subshell) –  Jun 13 '19 at 05: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 '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

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
Freddy
  • 25,172
  • 1
  • 21
  • 60
48

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
  • 107,579
  • 20
  • 231
  • 279
  • You might want to change that to `(sh -c 'echo $PPID'; : )` — see [my comment](https://stackoverflow.com/q/20725925/3960947#comment99919007_20726041) on [John1024’s answer](https://stackoverflow.com/q/20725925/3960947#20726041). – G-Man Says 'Reinstate Monica' Jun 19 '19 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 '19 at 21:45
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](https://www.gnu.org/software/bash/manual/bash.html#index-BASH_005fSUBSHELL) 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](http://git.savannah.gnu.org/cgit/bash.git/tree/execute_cmd.c#n4468) 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