39

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
  • 4
    stackoverflow.com/questions/4511407/… Commented 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. Commented Jun 12, 2019 at 18:52
  • 1
    Possible duplicate of How can I get the pid of a subshell? Commented 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. Commented Jun 13, 2019 at 9:39
  • 2
    Saw the title on HNQ and thought this was a quantum mechanics question... Commented Jun 13, 2019 at 22:47

3 Answers 3

54

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
4
  • 25
    It would have been a convenient command in the movie Inception. Commented Jun 13, 2019 at 18:56
  • In Inception it's probably $SHLVL Commented Jun 14, 2019 at 17:14
  • 1
    Link to the quoted manpage: man7.org/linux/man-pages/man1/bash.1.html Commented May 20, 2024 at 10:09
  • I have tested this on Fedora 40 - In a tty was executed 5 times the bash command, so if is executed either echo $SHLVL or (echo $SHLVL) is printed 5 but when is executed echo $BASH_SUBSHELL always is printed 0 and when is executed (echo $BASH_SUBSHELL) always is printed 1 - So what does Incremented by one within each subshell or subshell environment when the shell begins executing in that environment. The initial value is 0. exactly mean?. Thus it seems $BASH_SUBSHELL works as a simple switch indicator Commented Aug 19, 2024 at 23:42
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.

2
  • You might want to change that to (sh -c 'echo $PPID'; : ) — see my comment on John1024’s answer. Commented 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. Commented Jun 19, 2019 at 21:45
22

[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
  • 13
    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. Commented 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. Commented 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 ;-) Commented 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." Commented Jun 13, 2019 at 7:11
  • 3
    @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. Commented Jun 13, 2019 at 19:45

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.