Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

Re: Bug report: Strange behaviour of $SHLVL in subshell.

On Sat, 1 Nov 2014 15:31:21 +0100
Eliseo Martínez <eliseomarmol@xxxxxxxxx> wrote:
> To me, the following is not logical/expected behaviour:
>     $ echo $SHLVL
>     1
>     $ zsh -c 'echo $SHLVL'
>     2
>     $ zsh -c '(echo $SHLVL)'
>     1
> I’d just like to know if it is a bug, or has some kind of explanation.

I think the explanation is it's a bug.  There's a complication owing to
the shell knowing it doesn't actually need to fork, but it certainly
looks like one weirdness too far to expose to the user.

In a bit more detail:  there are optimisations in the shell so that it
knows it doesn't need to fork a new shell when it's about to exit the
parent shell anway.  A quick reading of POSIX (2.12, Shell Execution
Environment, which describes a subshell environment at the end) suggests
this is OK --- it's not tied to what actual process you're in, just how
it affects the environment seen by the command and the parent shell, the
last being academic in this case as you're about to exit.  When it
doesn't fork, it assumes that means it's about to start some command
that now takes over the function of the current shell.  But that's not
true here --- we're actually going to be running code within the current
shell but treating it as a subshell environment.

So currently we don't set the "forked" variable in execcmd() in this
case.  But the other uses of this variable are mostly to do with noting
that if we haven't forked, we need to be able to restore the current
shell environment after the command, which is also irrelevant here.  So
as far as I can see, we can ensure what the user sees is sane by setting
the "forked" flag for an explicit subshell environment even if we
haven't forked.

Decrementing SHLVL to reflect an exec when we're not forked --- i.e. if
we're starting a new shell we note that we're not one level deeper
because in this case the new shell replaced the old one --- was a bit
controversial when it was introduced.  It's the right thing to do if
SHLVL is trying to answer the question "how many levels of shell are
over my head right now in terms of processes", wrong if SHLVL is trying
to answer the question "how many levels of shell execution did I go
through before arriving at this point".

It's possible setting forked = 1 other times when we set is_exec is the
right thing to do.


diff --git a/Src/exec.c b/Src/exec.c
index 5bbd4e1..d2d4e80 100644
--- a/Src/exec.c
+++ b/Src/exec.c
@@ -2995,6 +2995,15 @@ execcmd(Estate state, int input, int output, int how, int last1)
 	 * Note that any form of exec means that the subshell is fake *
 	 * (but we may be in a subshell already).                     */
 	is_exec = 1;
+	/*
+	 * If we are in a subshell environment anyway, say we're forked,
+	 * even if we're actually not forked because we know the
+	 * subshell is exiting.  This ensures SHLVL reflects the current
+	 * shell, and also optimises out any save/restore we'd need to
+	 * do if we were returning to the main shell.
+	 */
+	if (type == WC_SUBSH)
+	    forked = 1;
     if ((esglob = !(cflags & BINF_NOGLOB)) && args && htok) {
diff --git a/Test/D04parameter.ztst b/Test/D04parameter.ztst
index d7f39cb..0cbe6c9 100644
--- a/Test/D04parameter.ztst
+++ b/Test/D04parameter.ztst
@@ -1548,7 +1548,7 @@
    print ${foo:wq}
    print ${:wq}
-0:Empty parameter shouldn't cause modifiers to crash the shell
+0:Empty parameter should not cause modifiers to crash the shell
@@ -1656,3 +1656,10 @@
+  SHLVL=1
+  $ZTST_testdir/../Src/zsh -c 'echo $SHLVL'
+  $ZTST_testdir/../Src/zsh -c '(echo $SHLVL)'
+0:SHLVL appears sensible when about to exit shell

Messages sorted by: Reverse Date, Date, Thread, Author