Shell levels in mksh(1)


GNU bash(1) has this interesting bit of behaviour where it sets the SHLVL shell parameter to the number of levels of nested bash(1) processes. This starts out at one, and then invoking bash(1) within bash(1) will result in this parameter being set to two in the second bash(1) process.

In practice this looks a bit like this:

multi@laptop:~$ echo $SHLVL
1
multi@laptop:~$ bash         # start a nested bash process
multi@laptop:~$ echo $SHLVL
2
multi@laptop:~$

Adding more levels of bash(1) increases the level of SHLVL.

Among my small collection of utility scripts, there are a number which involve spawning nested interactive shells (for example, running a shell under an ssh-agent(1)). One problem I've found with this is that if I tab away from the terminal where I have the nested shell running, I'll forget that it's nested when I come back, and only remember this when I try to close the tab, as exiting the nested shell will leave me back in the original shell. Adding the SHLVL shell level parameter to my PS1 would at least give some visual indication that I'm running inside a modified environment.

However, by habit I use mksh(1) instead of bash(1), and mksh(1) doesn't have any shell level handling built in. This doesn't mean we can't build this functionality outside of mksh(1) though.

SHLVL is set by bash(1) when the shell starts up, so there's no reason that we can't do the same in the startup files of mksh(1). The basic idea is to check if the shell level parameter is set, and if it is increment it, or set it to one otherwise. (Checking that it's set to an integral value is also a good idea.) In mksh(1) this might look something like this:

# our shell level parameter is called MKSH_SHLVL, to avoid collisions with bash,
# and we use mksh's extended glob (extglob) features to validate that any
# existing parameter contains an integral value
if [[ -n "$MKSH_SHLVL" && "$MKSH_SHLVL" = @([0-9]) ]]; then
    MKSH_SHLVL=$(( $MKSH_SHLVL + 1))
else
    MKSH_SHLVL=1
fi
export MKSH_SHLVL    # export to environment to make visible in child processes

Putting this in my mkshrc seemed work as intended, after some cursory testing.

I did notice one quirk with bash(1)'s behaviour, in that it doesn't check whether its parent process is also a bash(1) process; it unconditionally attempts to correctly set the SHLVL parameter if it already set in the environment. This means that spawning a command in bash(1) which in turn spawns a bash(1) means that the grandchild process will inherit and increment the original shell's SHLVL. For example:

multi@laptop ~> # starting out in mksh
multi@laptop ~> bash
multi@laptop:~$ echo $SHLVL
1
multi@laptop:~$ mksh  # switch to a different process
multi@laptop ~> bash  # switch back to bash
multi@laptop:~$ echo $SHLVL
2
multi@laptop:~$

This might not be desirable; the first example which came to mind here where I would not want this behaviour is where I log into a text console and then start Xorg using startx(1), in which case running bash(1) in a terminal emulator would inherit the original console shell's SHLVL.

One way to work around this particular case would be to unset SHLVL in the environment in the xinitrc file, however I then wondered about how this could be built into the shell level tracking in my mkshrc. The idea I came up with is to record mksh(1)'s process ID in the environment at startup, and then in the child mksh(1) process check whether the child's parent process ID is the same as the process ID in the environment. If this is not the case, then the shell level is reset to one.

This turns out looking something like this:

if [[ -n "$MKSH_SHLVL" && "$MKSH_SHLVL" = @([0-9]) && \
        -n "$__mksh_ppid" && "$__mksh_ppid" = "$PPID" ]]; then
    MKSH_SHLVL=$(( $MKSH_SHLVL + 1 ))
else
    MKSH_SHLVL=1
fi
__mksh_ppid="$$"
export MKSH_SHLVL __mksh_ppid

I'm going to run with this in my mkshrc for a while and see how it turns out. Adding the shell level to the PS1 string in a suitable manner is left as an exercise to the reader.



home