Advanced Examples: pushd and popd

We will conclude this chapter with a couple of functions that are already built into bash but are useful in demonstrating some of the concepts we have covered in this chapter. [59]

We will start by implementing a significant subset of their capabilities and finish the implementation in Chapter 6.

Think of a stack as a spring-loaded dish receptacle in a cafeteria. When you place dishes on the receptacle, the spring compresses so that the top stays at roughly the same level. The dish most recently placed on the stack is the first to be taken when someone wants food; thus, the stack is known as a “last-in, first-out” or LIFO structure. Putting something onto a stack is known in computer science parlance as pushing, and taking something off the top is called popping.

A stack is very handy for remembering directories, as we will see; it can “hold your place” up to an arbitrary number of times. The cd - form of the cd command does this, but only to one level. For example: if you are in firstdir and then you change to seconddir, you can type cd - to go back. But if you start out in firstdir, then change to seconddir, and then go to thirddir, you can use cd - only to go back to seconddir. If you type cd - again, you will be back in thirddir, because it is the previous directory. [60]

If you want the “nested” remember-and-change functionality that will take you back to firstdir, you need a stack of directories along with the pushd and popd commands. Here is how these work:

  • The first time pushd dir is called, pushd pushes the current directory onto the stack, then cds to dir and pushes it onto the stack.

  • Subsequent calls to pushd dir cd to dir and push dir only onto the stack.

  • popd removes the top directory off the stack, revealing a new top. Then it cds to the new top directory.

For example, consider the series of events in Table 4.3. Assume that you have just logged in, and that you are in your home directory (/home/you).

Table 4-3. pushd/popd Example

Command Stack Contents Result Directory
pushd lizard /home/you/lizard /home/you /home/you/lizard
pushd /etc /etc /home/you/lizard /home/you /etc
popd /home/you/lizard /home/you /home/you/lizard
popd /home/you /home/you
popd <empty>(error)

We will implement a stack as an environment variable containing a list of directories separated by spaces. [61]

Your directory stack should be initialized to the null string when you log in. To do this, put this in your .bash_profile:

DIR_STACK=""
export DIR_STACK

Do not put this in your environment file if you have one. The export statement guarantees that DIR_STACK is known to all subprocesses; you want to initialize it only once. If you put this code in an environment file, it will get reinitialized in every subshell, which you probably don’t want.

Next, we need to implement pushd and popd as functions. Here are our initial versions:

pushd ()
{
    dirname=$1
    DIR_STACK="$dirname ${DIR_STACK:-$PWD' '}"
    cd ${dirname:?"missing directory name."}
    echo "$DIR_STACK"
}
   
popd ()
{
    DIR_STACK=${DIR_STACK#* }
    cd ${DIR_STACK%% *}
    echo "$PWD"
}

Notice that there isn’t much code! Let’s go through the two functions and see how they work, starting with pushd. The first line merely saves the first argument in the variable dirname for readability reasons.

The second line of the function pushes the new directory onto the stack. The expression ${DIR_STACK :-$PWD' ' } evaluates to $DIR_STACK if it is non-null or $PWD '' (the current directory and a space) if it is null. The expression within double quotes, then, consists of the argument given, followed by a single space, followed by DIR_STACK or the current directory and a space. The trailing space on the current directory is required for pattern matching in the popd function; each directory in the stack is considered to be of the form "dirname “.

The double quotes in the assignment ensure that all of this is packaged into a single string for assignment back to DIR_STACK. Thus, this line of code handles the special initial case (when the stack is empty) as well as the more usual case (when it’s not empty).

The third line’s main purpose is to change to the new directory. We use the :? operator to handle the error when the argument is missing: if the argument is given, then the expression ${dirname :?"missing directory name. “} evaluates to $ dirname, but if it is not given, the shell will print the message pushd: dirname: missing directory name and exit from the function.

The last line merely prints the contents of the stack, with the implication that the leftmost directory is both the current directory and at the top of the stack. (This is why we chose spaces to separate directories, rather than the more customary colons as in PATH and MAILPATH.)

The popd function makes yet another use of the shell’s pattern-matching operators. Its first line uses the # operator, which tries to delete the shortest match of the pattern "* " (anything followed by a space) from the value of DIR_STACK. The result is that the top directory and the space following it are deleted from the stack. This is why we need the space on the end of the first directory pushed onto the stack.

The second line of popd uses the pattern-matching operator %% to delete the longest match to the pattern "*" (a space followed by anything) from DIR_STACK. This extracts the top directory as an argument to cd, but doesn’t affect the value of DIR_STACK because there is no assignment. The final line just prints a confirmation message.

This code is deficient in four ways. First, it has no provision for errors. For example:

  • What if the user tries to push a directory that doesn’t exist or is invalid?

  • What if the user tries popd and the stack is empty?

Test your understanding of the code by figuring out how it would respond to these error conditions. The second problem is that if you use pushd in a shell script, it will exit everything if no argument is given; $ { varname :? message } always exits from non-interactive shells. It won’t, however, exit an interactive shell from which the function is called. The third deficiency is that it implements only some of the functionality of bash’s pushd and popd commands—albeit the most useful parts. In the next chapter, we will see how to overcome all of these deficiencies.

The fourth problem with the code is that it will not work if, for some reason, a directory name contains a space. The code will treat the space as a separator character. We’ll accept this deficiency for now, but you might like to think about how to overcome it in the next few chapters.



[59] Your copy of bash may not have pushd and popd, since it can be configured without these built-ins.

[60] Think of cd - as a synonym for cd $OLDPWD; see the previous chapter.

[61] bash also maintains a directory stack for the pushd and popd built-ins, accessible through the environment variable DIRSTACK. Unlike our version, however, it is implemented as an array (see Chapter 6 for details on arrays).

Get Learning the bash Shell, Second Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.