Ubuntu – How to know how many sub-shells deep I am

command line

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?

Best Answer

  • 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