80

Problem: Find how many shells deep I am.

Details: I open the shell from vim a lot. Build and run and exit. Sometimes I forget and open another vim inside and then yet another shell. :(

I want to know how many shells deep I am, perhaps even have it on my shell screen at all times. (I can manage that part).

My solution: Parse the process tree and look for vim and bash/zsh and figure out the current process's depth within it.

Does something like that already exist? I could not find anything.

11
  • 28
    Is the $SHLVL variable (maintained by several shells) what you're looking for? Jun 27, 2017 at 16:30
  • 1
    To clarify, you're not really interested in how many (directly nested) shells, indicated by SHLVL, but whether your current shell is a descendant of vim?
    – Jeff Schaller
    Jun 27, 2017 at 19:36
  • 14
    This seems to be a bit of an XY problem - my workflow is ^Z to escape from the vim instance into the parent shell, and fg to get back, which doesn't have this issue.
    – Doorknob
    Jun 28, 2017 at 15:33
  • 3
    @Doorknob, I do that too. but I prefer this one, because then I would have to keep checking "jobs". and There can be many running at times on my machine. now, add TMUX with the equation. It become complex and overflowing. If I spawn shell inside vim, it would be less of a scatter. (However I end up making the mess and hence the question).
    – Pranay
    Jun 28, 2017 at 19:24
  • 4
    @Doorknob: With all due respect, that seems like responding to the question, “How do I drive from point A from point B?” with the suggestion, “Don’t drive; just take an Uber.”  If the user has a workflow that involves editing multiple files concurrently, then having multiple parallel stopped vim jobs might be more confusing than having a stack of nested processes.  By the by, I prefer having multiple windows, so I can easily jump back and forth quickly, but I wouldn’t call this an XY problem just because I prefer a different workflow. Jun 28, 2017 at 22:16

7 Answers 7

49

When I read your question, my first thought was $SHLVL.  Then I saw that you wanted to count vim levels in addition to shell levels.  A simple way to do this is to define a shell function:

vim()  { ( ((SHLVL++)); command vim  "$@");}

This will automatically and silently increment SHLVL each time you type a vim command.  You will need to do this for each variant of vi/vim that you ever use; e.g.,

vi()   { ( ((SHLVL++)); command vi   "$@");}
view() { ( ((SHLVL++)); command view "$@");}

The outer set of parentheses creates a subshell, so the manual change in the value of SHLVL doesn’t contaminate the current (parent) shell environment.  Of course the command keyword is there to prevent the functions from calling themselves (which would result in an infinite recursion loop).  And of course you should put these definitions into your .bashrc or other shell initialization file.


There’s a slight inefficiency in the above.  In some shells (bash being one), if you say

(cmd1; cmd2;; cmdn)

where cmdn is an external, executable program (i.e., not a built-in command), the shell keeps an extra process lying around, just to wait for cmdn to terminate.  This is (arguably) not necessary; the advantages and disadvantages are debatable.  If you don’t mind tying up a bit of memory and a process slot (and to seeing one more shell process than you need when you do a ps), then do the above and skip to the next section.  Ditto if you’re using a shell that doesn’t keep the extra process lying around.  But, if you want to avoid the extra process, a first thing to try is

vim()  { ( ((SHLVL++)); exec vim  "$@");}

The exec command is there to prevent the extra shell process from lingering.

But, there’s a gotcha.  The shell’s handling of SHLVL is somewhat intuitive: When the shell starts, it checks whether SHLVL is set.  If it’s not set (or set to something other than a number), the shell sets it to 1.  If it is set (to a number), the shell adds 1 to it.

But, by this logic, if you say exec sh, your SHLVL should go up.  But that’s undesirable, because your real shell level hasn’t increased.  The shell handles this by subtracting one from SHLVL when you do an exec:

$ echo "$SHLVL"
1

$ set | grep SHLVL
SHLVL=1

$ env | grep SHLVL
SHLVL=1

$ (env | grep SHLVL)
SHLVL=1

$ (env) | grep SHLVL
SHLVL=1

$ (exec env) | grep SHLVL
SHLVL=0

So

vim()  { ( ((SHLVL++)); exec vim  "$@");}

is a wash; it increments SHLVL only to decrement it again. You might as well just say vim, without benefit of a function.

Note:
According to Stéphane Chazelas (who knows everything), some shells are smart enough not to do this if the exec is in a subshell.

To fix this, you would do

vim()  { ( ((SHLVL+=2)); exec vim  "$@");}

Then I saw that you wanted to count vim levels independently of shell levels.  Well, the exact same trick works (well, with a minor modification):

vim() { ( ((SHLVL++, VILVL++)); export VILVL; exec vim "$@");}

(and so on for vi, view, etc.)  The export is necessary because VILVL isn’t defined as an environment variable by default.  But it doesn’t need to be part of the function; you can just say export VILVL as a separate command (in your .bashrc).  And, as discussed above, if the extra shell process isn’t an issue for you, you can do command vim instead of exec vim, and leave SHLVL alone:

vim() { ( ((VILVL++)); command vim "$@");}

Personal Preference:
You may want to rename VILVL to something like VIM_LEVEL.  When I look at “VILVL”, my eyes hurt; they can’t tell whether it’s a misspelling of “vinyl” or a malformed Roman numeral.


If you are using a shell that doesn’t support SHLVL (e.g., dash), you can implement it yourself as long as the shell implements a startup file.  Just do something like

if [ "$SHELL_LEVEL" = "" ]
then
    SHELL_LEVEL=1
else
    SHELL_LEVEL=$(expr "$SHELL_LEVEL" + 1)
fi
export SHELL_LEVEL

in your .profile or applicable file.  (You should probably not use the name SHLVL, as that will cause chaos if you ever start using a shell that supports SHLVL.)


Other answers have addressed the issue of embedding environment variable value(s) into your shell prompt, so I won’t repeat that, especially you say you already know how to do it.

9
  • 1
    I’m somewhat puzzled that so many answers suggest executing an external executable program, like ps or pstree, when you can do this with shell builtins. Jun 28, 2017 at 22:16
  • This answer is perfect. I have marked this as the solution (unfortunately it does not have that many votes yet).
    – Pranay
    Jun 28, 2017 at 22:23
  • 1
    Your approach is amazing and you are using only the primitives which means including this in my .profile/.shellrc would not break anything. I pull those on any machine I work on.
    – Pranay
    Jun 28, 2017 at 22:29
  • 1
    Note that dash has arithmetic expansion. SHELL_LEVEL=$((SHELL_LEVEL + 1)) should be enough even if $SHELL_LEVEL was previously unset or empty. It's only if you had to be portable to the Bourne shell that you'd need to resort to expr, but then you'd also need to replace $(...) with `..`. SHELL_LEVEL=`expr "${SHELL_LEVEL:-0}" + 1` Jun 28, 2017 at 22:29
  • 2
    @Pranay, it's unlikely to be a problem. If an attacker can inject any arbitrary env var, then things like PATH/LD_PRELOAD are more obvious choices, but if non-problematic variables get through, like with sudo configured without reset_env (and one can force a bash script to read ~/.bashrc by making stdin a socket for instance), then that can become a problem. That's a lot of "if"s, but something to keep at the back of one's mind (unsanitized data in arithmetic context is dangerous) Jun 29, 2017 at 16:14
37

You could count as many time you need to go up the process tree until you find a session leader. Like with zsh on Linux:

lvl() {
  local n=0 pid=$$ buf
  until
    IFS= read -rd '' buf < /proc/$pid/stat
    set -- ${(s: :)buf##*\)}
    ((pid == $4))
  do
    ((n++))
    pid=$2
  done
  echo $n
}

Or POSIXly (but less efficient):

lvl() (
  unset IFS
  pid=$$ n=0
  until
    set -- $(ps -o ppid= -o sid= -p "$pid")
    [ "$pid" -eq "$2" ]
  do
    n=$((n + 1)) pid=$1
  done
  echo "$n"
)

That would give 0 for the shell that was started by your terminal emulator or getty and one more for each descendant.

You only need to do that once on startup. For instance with:

PS1="[$(lvl)]$PS1"

in your ~/.zshrc or equivalent to have it in your prompt.

tcsh and several other shells (zsh, ksh93, fish and bash at least) maintain a $SHLVL variable which they increment on startup (and decrement before running another command with exec (unless that exec is in a subshell if they're not buggy (but many are))). That only tracks the amount of shell nesting though, not process nesting. Also level 0 is not guaranteed to be the session leader.

1
  • Yeah.. this or similar. I did not wish to write this of my own, and it was not my intention to have anyone write this for me. :(. I was hoping for some feature in vim or shell or some plugin that is regularly maintained. I searched but did not find something.
    – Pranay
    Jun 27, 2017 at 16:51
34

Use echo $SHLVL. Use the KISS principle. Depending on your program's complexity, this may be enough.

4
  • 2
    Works for bash, but not for dash.
    – agc
    Jun 27, 2017 at 18:58
  • SHLVL does not help me. I knew about it, and it also came up in the search when I searched. :) There are more details in the question.
    – Pranay
    Jun 29, 2017 at 1:39
  • @Pranay Are you certain vim itself does not provide this information?
    – user2497
    Jun 29, 2017 at 17:55
  • @user2497, I am somewhat. This is the premise of the question. I searched everywhere, I was aware of SHLVL too. I wanted -- > a) be certain there is no such thing. b) do it with least amount of dependencies/maintenance.
    – Pranay
    Jun 29, 2017 at 21:17
17

One potential solution is to look at the output of pstree. When run inside a shell that was spawned from within vi, the part of the tree tree that lists pstree should show you how deep you are. For example:

$ pstree <my-user-ID>
...
       ├─gnome-terminal-─┬─bash───vi───sh───vi───sh───pstree
...
1
  • Yeah that is what I suggested as my solution (in the question). I do not wish to parse the pstree though :(. this is good for manually reading it, I was thinking of writing a program to do it for me and let me know. I am not very inclined to write a parser if a plugin/tool already does it :).
    – Pranay
    Jun 27, 2017 at 16:39
12

First variant - shell depth only.

Simple solution for bash: add to the .bashrc next two lines (or change your current PS1 value):

PS1="${SHLVL} \w\$ "
export PS1

Result:

1 ~$ bash
2 ~$ bash
3 ~$ exit
exit
2 ~$ exit
exit
1 ~$

Number at the beginning of prompt string will be denote shell level.

Second variant, with nested vim and shell levels both.

add this lines to the .bashrc

branch=$(pstree -ls $$)
vim_lvl=$(grep -o vim <<< "$branch" | wc -l)
sh_lvl=$(grep -o bash <<< "$branch" | wc -l)
PS1="v:${vim_lvl};s:$((sh_lvl - 1)):\w\$ "
export PS1

Result:

v:0;s:1:/etc$ bash
v:0;s:2:/etc$ bash
v:0;s:3:/etc$ vim
##### do ':sh' command in the vim, shell level is increasing by 1
v:1;s:4:/etc$ vim
##### do ':sh' command in the vim, shell level is increasing by 1
v:2;s:5:/etc$ bash
v:2;s:6:/etc$

v:1 - vim depth level
s:3 - shell depth level

3
  • this will give me the bash nesting. It won't give me the vim nests. :)
    – Pranay
    Jun 27, 2017 at 20:25
  • @Pranay Check the new solution. It doing what you want.
    – MiniMax
    Jun 27, 2017 at 22:39
  • Yeah, this is a good solution. I can add more shells and it would work :).
    – Pranay
    Jun 28, 2017 at 16:27
8

In the question you mentioned parsing of pstree. Here is a relatively simple way:

bash-4.3$ pstree -Aals $$ | grep -E '^ *`-((|ba|da|k|c|tc|z)sh|vim?)( |$)'
                  `-bash
                      `-bash --posix
                          `-vi -y
                              `-dash
                                  `-vim testfile.txt
                                      `-tcsh
                                          `-csh
                                              `-sh -
                                                  `-zsh
                                                      `-bash --norc --verbose

The pstree options:

  • -A - ASCII output for easier filtering (in our case every command is preceded by `-)
  • -a - show also command arguments, as a side-effect every command is shown on a separate line and we can easily filter the output using grep
  • -l - do not truncate long lines
  • -s - show parents of the selected process
    (unfortunately not supported in old versions of pstree)
  • $$ - the selected process - the PID of the current shell
1
  • Yeah I was doing this pretty much. I also had something to count "bash" and "vim" etc. I just did not wish to maintain it. It is also not feasible to have a lot of custom functionality when you have to switch over a lot of VM's and develop on them sometimes.
    – Pranay
    Jun 28, 2017 at 16:23
4

This doesn't strictly answer the question but in many cases may make it unnecessary to do so:

When you first launch your shell, run set -o ignoreeof. Don't put it in your ~/.bashrc.

Make it a habit to type Ctrl-D when you think you are in the top level shell and want to be sure.

If you're not in the top level shell, Ctrl-D will signal "end of input" to the current shell and you will drop back one level.

If you are in the top level shell, you will get a message:

Use "logout" to leave the shell.

I use this all the time for chained SSH sessions, to make it easy to drop back to a specific level of the SSH chain. It works for nested shells just as well.

1
  • 1
    This definitely helps and yes it will remove a lot of complications :). I might just combine this with the accepted answer :)). Conditionally set, so I might not have to look at my prompt all the time.
    – Pranay
    Jun 30, 2017 at 0:36

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .