44

Sometimes I do things such as starting a sub-shell from vim with :sh. How do I know if I'm in a sub-shell where exit will just return me out one level, vs. being in the outermost shell where exit will log me out or close my session.

Is there some kind of Inception totem I can spin or something to know how many levels deep I am?

43

You can use the command pstree (that comes by default with Ubuntu). Here is example - currently I'm having only one open terminal window on WSL:

User@Wsl:~$ pstree
init─┬─init───bash───pstree
     └─{init}

User@Wsl:~$ bash
User@Wsl:~$ sh
$ bash
User@Wsl:~$ pstree
init─┬─init───bash───bash───sh───bash───pstree
     └─{init}

Within an actual Linux/Ubuntu environment the process tree will be more complicated. We can filter the tree by the option -s that will show the parents of a selected process. So our command could be pstree -s $$, where $$ is an environment variable that contains the current PID:

User@Ubuntu:~$ pstree -s $$
systemd──lightdm──lightdm──upstart──gnome-terminal-──bash──pstree

User@Ubuntu:~$ bash
User@Ubuntu:~$ sh
$ bash
User@Ubuntu:~$ pstree -s $$
systemd──lightdm──lightdm──upstart──gnome-terminal-──bash──bash──sh──bash──pstree

References:


Add indicator to the shell's prompt: Based on the @waltinator's idea, in order to have a counter in the front of the prompt for several different shells when the level is deeper than one, I've added the lines, shown below the demo, at the bottom of the relevant run commands (~/.*rc) files.

I've made tests on WSL, Ubuntu 16.04, Ubuntu 18.04 (server/desktop), Ubuntu 19.04, within gnome-terminal, tty and ssh session. Here is how this works:

enter image description here

The limitation is that: the counter works only for 13-14 levels of depth, depending on the OS. I do not intend to investigate the reasons :)

  • bash > .bashrc:

    DEPTH=$(($(pstree -s $$ | sed -r 's/-+/\n/g' | grep -Ec '\<(bash|zsh|sh|dash|ksh|csh|tcsh)\>') - 1))
    if (( DEPTH > 1 )); then PS1=$DEPTH:$PS1; fi
    
  • csh and tcsh > .cshrc:

    @ DEPTH = `pstree -s $$ | sed -r 's/-+/\n/g' | grep -Ec '\<(bash|zsh|sh|dash|ksh|csh|tcsh)\>'` - 0
    if ( $DEPTH > 1 ) then; set prompt="$DEPTH":"$prompt"; endif
    
  • zsh > .zshrc:

    DEPTH=$(($(pstree -s $$ | sed -r 's/-+/\n/g' | grep -Ec '\<(bash|zsh|sh|dash|ksh|csh|tcsh)\>') - 1))
    if (( DEPTH > 1 )); then PROMPT=$DEPTH:$PROMPT; fi
    
  • ksh > .kshrc:

    DEPTH=$(($(pstree -s $$ | sed -r 's/\-+/\n/g' | grep -Ec '\<(bash|zsh|sh|dash|ksh|csh|tcsh)\>') - 0))
    if (( DEPTH > 1 )); then PS1="$DEPTH":"$PS1"'$ '; fi
    
  • sh that is actually dash on Ubuntu - here the things are little bit complicated and wired (read the references below for more information):

    1. Edit the ~/.profile file and add the following line at the bottom:

      ENV=$HOME/.shrc; export ENV
      
    2. Create the file ~/.shrc with the following content, note ksh also reads the $ENV:

      #!/bin/dash
      DEPTH=$(pstree -s $$ | sed -r 's/-+/\n/g' | grep -Ec '\<(bash|zsh|sh|dash|ksh|csh|tcsh)\>')
      if [ "$0" != 'ksh' ]; then DEPTH=$((DEPTH - 1)); fi
      if [ "$DEPTH" -gt 1 ]; then export PS1='$DEPTH:\$ '; fi
      

References:


Create a command that will output the depth: Another option is to create shell command that will output the depth. For this purpose create the executable file /usr/local/bin/depth (thus it should be accessible system wide):

sudo touch /usr/local/bin/depth
sudo chmod +x /usr/local/bin/depth

Edit the file with your favorite editor and add the following lines as its content:

#!/bin/bash

SHELLS='(bash|zsh|sh|dash|ksh|csh|tcsh)'
DEPTH=$(pstree -s $$ | sed -r 's/-+/\n/g' | grep -Ec "\<$SHELLS\>")

if [[ $@ =~ -v ]]
then
        pstree -s $$ | sed -r 's/-+/\n/g' | grep -E "\<$SHELLS\>" | cat -n
fi

echo "DEPTH: $DEPTH"

[[ $DEPTH -gt 1 ]] && exit 0 || exit 1

The above script has two options -v or --verbose that will output a list of the involved shells. And the another option that will check whether the depth is greater than one and based on this will return exit 0 or exit 1, so you can use it in this way depth && exit. Here are few examples of usage:

User@Ubuntu:~$ depth          # we are at the 1st level - bash
DEPTH: 1
User@Ubuntu:~$ sh           
$ csh                         # we are at the 2nd level - dash
Ubuntu:~% depth               # we are at the 3rd level - csh
DEPTH: 3
Ubuntu:~% ksh
$ depth -v                    # we are at the 4th level - ksh
     1  bash
     2  sh
     3  csh
     4  ksh
DEPTH: 4
$ depth && exit               # exit to the 3rd level - csh
DEPTH: 4
Ubuntu:~% depth && exit       # exit to the 2nd level - dash
DEPTH: 3
exit
$ depth && exit               # exit to the 1st level - bash
DEPTH: 2
User@Ubuntu:~$ depth && exit  # stay at the 1st level - bash
DEPTH: 1
User@Ubuntu:~$ depth && exit  # stay at the 1st level - bash
DEPTH: 1

Comparison by the other solutions: I spent some additional time to find out some weaknesses of the approaches provided here. I was able to imagine the following two cases (the capital letters are needed for better syntax highlighting):

  • When su or sudo -i are involved:

    User@Ubuntu:~$ ps | grep -Ec '\<(bash|zsh|sh|dash|ksh|csh|tcsh|su|sudo)\>'
    1
    User@Ubuntu:~$ echo $SHLVL
    1
    User@Ubuntu:~$ depth
    DEPTH: 1
    
    User@Ubuntu:~$ su spas
    Password:
    
    Spas@Ubuntu:~$ ps | grep -Ec '\<(bash|zsh|sh|dash|ksh|csh|tcsh|su|sudo)\>'
    1
    Spas@Ubuntu:~$ echo $SHLVL
    2
    Spas@Ubuntu:~$ depth
    DEPTH: 2
    
    Spas@Ubuntu:~$ sudo -i
    [sudo] password for spas:
    
    Root@Ubuntu:~# ps | grep -Ec '\<(bash|zsh|sh|dash|ksh|csh|tcsh|su|sudo)\>'
    3
    Root@Ubuntu:~# echo $SHLVL
    1
    Root@Ubuntu:~# depth
    DEPTH: 3
    
  • When there a background process is launched:

    User@Ubuntu:~$ bash
    User@Ubuntu:~$ ps | grep -Ec '\<(bash|zsh|sh|dash|ksh|csh|tcsh)\>'
    2
    User@Ubuntu:~$ echo $SHLVL
    2
    User@Ubuntu:~$ depth
    DEPTH: 2
    
    User@Ubuntu:~$ while true; do sleep 10; done &
    [1] 10886
    User@Ubuntu:~$ ps | grep -Ec '\<(bash|zsh|sh|dash|ksh|csh|tcsh)\>'
    3
    User@Ubuntu:~$ echo $SHLVL
    2
    User@Ubuntu:~$ depth
    DEPTH: 2
    
    # Note: $SHLVL is not supported only by sh/dash.  
    #       It works with all other tested shells: bash, zsh, csh, tcsh, ksh
    
    User@Ubuntu:~$ sh
    $ ps | grep -Ec '\<(bash|zsh|sh|dash|ksh|csh|tcsh)\>'
    4
    $ echo $SHLVL
    2
    $ depth
    DEPTH: 3
    
| improve this answer | |
  • Now I'm puzzled about output I got on my system: systemd───xfce4-terminal───bash───pstree. Why is it this way? – val says Reinstate Monica Sep 28 '19 at 19:00
  • 1
    @val: systemd is the init process, the parent of all other processes. You are apparently using xfce4-terminal, which launched a bash shell, within which you ran pstree, which reported itself and its parents. If you mean the lack of steps between systemd and xfce4-terminal, it could be that whatever launched xfce4-terminal died or disowned it, in which case it would be inherited by init. – Nick Matteo Sep 29 '19 at 4:48
  • Any reason not to read SHLVL ? Portability across processes and systems, I assume, but then pstree may not be installed.. – D. Ben Knoble Sep 29 '19 at 23:16
  • Hello, @D.BenKnoble, as it is discussed under the @egmont's answer, $SHLVL is not supported by some shells. More specific, according to the environment from the demo above it is not supported only by sh (dash) - and this shell is not counted at all by this variable. On the other hand pstree is part of the package psmisc that provides also fuser, killall and few others - it is main component of Ubuntu - I haven't installed it on the systems mentioned in this answer. – pa4080 Sep 30 '19 at 6:36
30

Check the value of the SHLVL shell variable:

echo $SHLVL

Quoting from bash's manual page:

SHLVL  Incremented by one each time an instance of bash is started.

It is also supported by zsh.

| improve this answer | |
  • 4
    But sh is not counted, so the example given, with sh, would not have incremented SHLVL. Still, this is something that might be useful for those not switching shells too much – ubfan1 Sep 27 '19 at 22:50
  • 3
    @ubfan1 unless there's an overriding vimrc definition, :sh defaults to the user's login shell I think (it's really an abbreviated form of :shell rather than the name of a specific shell binary) – steeldriver Sep 27 '19 at 23:27
  • 3
    I'm not familiar with vim's details, but I've tried out :sh from vim before posting this answer, and it did increment the shell level for me. My login shell is bash. – egmont Sep 28 '19 at 7:38
9

In my .bashrc, I use $SHLVL to adjust $PS1, by appending "+" signs to my $SUBSHELL variable:

...
# set a variable to reflect SHLVL > 1 (Ubuntu 12.04)
if [[ $SHLVL -gt 1 ]] ; then
    export SUBSHELL="${SUBSHELL:+$SUBSHELL}+"
else
    export SUBSHELL=""
fi
...

if [[ "$color_prompt" = yes ]]; then
#             chroot?                       Depth      green       user@host nocolor  :   green      $PWD  red      (status) off   $ or # space             
    PS1='${debian_chroot:+($debian_chroot)}${SUBSHELL}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[1;31m\]($?)\[\033[00m\]\$ '
else
    PS1='${debian_chroot:+($debian_chroot)}${SUBSHELL}\u@\h:\w\$ '
fi
...

Then, I can see how deep I am:

walt@bat:~(1)$ ed foo
263
!bash
+walt@bat:~(0)$ bash
++walt@bat:~(0)$ bash
+++walt@bat:~(0)$ exit
exit
++walt@bat:~(0)$ exit
exit
+walt@bat:~(0)$ exit
exit
!
q
walt@bat:~(0)$ 
| improve this answer | |
4

awk:

# Count the occurrence of (sh)ells.
DEPTH_REGEX='^(ash|bash|busybox|csh|dash|fish|mksh|sh|tcsh|zsh)$'

DEPTH=$(/bin/ps -s $(/bin/ps -p $$ -osid --no-headers) -ocomm --no-headers | \
awk -v R=$DEPTH_REGEX '{for (A=1; A<=(NR-2); A++) {if ($A ~ R) {B++}}} END {print B}')

pgrep:

DEPTH=$(/usr/bin/pgrep -c -s $(/bin/ps -p $$ -osid --no-headers) '^(ash|bash|busybox|csh|dash|fish|mksh|sh|tcsh|zsh)$')

You can place one of the two versions in a file and use source to make $DEPTH available.

# Set 256 colors in terminal.
if [ -x /usr/bin/tput ] && [ "$(SHELL=/bin/sh tput colors)" -ge 8 ]; then
    export TERM="xterm-256color"
fi

# change these if you don't dig my colors!

NM="\[\033[0;1;37m\]"   #means no background and white lines
HI="\[\033[0;37m\]"     #change this for letter colors
SI="\[\033[38;5;202m\]" #this is for the current directory
NI="\[\033[0;1;30m\]"   #for @ symbol
IN="\[\033[0m\]"

# Count the occurrence of (sh)ells.
source /usr/share/shell-depth/depth

PS1="${NM}[${HI}\u${NI}@${HI}\h ${SI}\w${NM} \A](${HI}${DEPTH}${NM}): ${IN}"
| improve this answer | |
2

You can simply use ps without any additional arguments to see the whole shell stack (including the current one). It will be also showing all the background jobs you've started as well as ps itself, but it can give you a rough estimate of how deep you are.

| improve this answer | |
  • This works { echo hello world; ps; } & to prove the ps answer above. – WinEunuuchs2Unix Sep 29 '19 at 23:29
  • @WinEunuuchs2Unix, I mean something like this: paste.ubuntu.com/p/6Kfg8TqR9V – pa4080 Sep 30 '19 at 6:32
  • Is there a way to mimic pstree -s $$ with ps? – bac0n Oct 1 '19 at 4:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.