Search code examples
bashshellunixzsh

How do I exit from nested shells at the command line without accidentally closing/exiting my terminal?


If I am typing in a terminal and do this

% bash
% [enter many commands in this shell]
% bash
% [enter many commands in this shell]
. . .

(could be zsh instead of bash) and I forget how many shells deep I am and want to get back to the top-level shell, how do I do it without accidentally closing my terminal?

The dangerous thing I try to do is

% exit
% exit
% exit

and if I enter one more exit than I need, I unintentionally close my terminal, possibly losing work. It can ruin a day.

Answers that aren't good enough:

I know I could scroll up or do a search in the terminal for the commands, but that's painful and isn't always possible. I know I could avoid this situation in the future with tricks in my environment or by just never creating new shells like that, but that doesn't answer the question. If I'm lucky, my command prompt changes, so I know I'm at the top when the prompt changes back, assuming I notice. But, let's say it doesn't change. Note that entering history at the command line doesn't help because the history of the previous shell typically doesn't carry over. Entering return doesn't help.

If my computer (a Mac) keeps history for all shells somewhere, that sounds like a potential guaranteed solution, but is that typical? It also still sounds painful to sift through. I'm hoping for a better answer, like a simple command that uses ps to show me my stack of nested shells in the current terminal. Keep in mind that I might have many terminal windows.


Solution

  • [Inspired by Gairfowl's comments] You could add something like this to your .bashrc and .zshrc:

    unshell() {
        case "$0" in
            -*) echo "Already at top-level shell" >&2 ;;
            *sh*)
                parent="$(ps -ocomm= -p$(ps -p$$ -oppid=))"
                case "$parent" in
                    -*) echo "Exiting to top" >&2 ;;
                    *sh* ) echo "Not at top yet" >&2 ;;
                    * ) echo "Not sure what the parent process ($parent) is." >&2 ;;
                esac
                exit ;;
            * ) echo "\$0 is weird ($0); not sure what to do." >&2 ;;
        esac
    }
    

    You'll still have to run it multiple times to exit out of multiple levels of shells, but it won't exit from the top level, and will let you know when you've exited the last sub-level shell.