1. Overview

When we’re working with multiple directories, switching between them is kind of inevitable. In this tutorial, we’ll learn about the pushd and popd commands as a means of fast and efficient bookmarking of directories.

2. Why Do We Need pushd and popd?

When it comes to directory navigation, the cd command is a de-facto standard that can solve most of our use cases:

As we can see, we can do three kinds of directory jumps:

  • Direct jump to a particular directory by using its absolute path cd <path>
  • Jumping up by one-level to the parent directory with cd .. 
  • Jump to the last visited directory with cd – 

But, as the number of directories that we need to track grows, this approach becomes more and more inefficient:

  • Most of these paths are usually too long to type
  • Even with a path autocomplete feature, we’re expected to remember the exact path prefix

What if, after timestamp t4, we could directly jump to dir1 without typing a single letter from its path? Well, we’re in luck, as the combination of pushd and popd commands gives us such flexibility.

3. Simple Navigation

For some of us, breaking our habit of using the cd command to switch directories can be challenging. So, once and for all, let’s fix the underlying resistance that could keep us away from exploring these new tools.

3.1. pushd and popd

First, let’s start by using the pushd command to jump to any directory by using its absolute or relative path, just like the cd command:

$ pushd <directory-path>

And, if we need to visit the last directory that we visited using the pushd command, then we can use the popd command without any argument:

$ popd

At this point, we must clearly understand that the cd – and popd commands do not exhibit the same behavior. While the former takes us to the last visited directory, the latter, on the other hand, takes us to the previous directory that we used with the pushd command.

3.2. Aliases

Of course, there’s no denying that aliases make life easy for us. With the cd command, we could use aliases to quickly jump across directories:

$ cd [alias]

In here, alias could be any valid directory alias:

  • . for the current working directory
  • .. to identify the parent directory
  • – for the last visited directory

Internally, these aliases make use of some of the special directory variables:

  • $PWD for the current working directory
  • ${PWD%/*} makes use of prefix parameter substitution to identify the parent directory
  • $OLDPWD for the last visited directory

Well, the thing is that all these aliases and path variables are not specific to the cd command. So, we can use them with the pushd command as well:

$ pushd [alias]

 

Further, in the absence of an alias or a directory path, pushd makes use of the default directory. And, that’s usually the home directory of the user identified by the $HOME variable.

With that, we can now use the pushd command for making all kinds of directory jumps that are possible with the cd commands. And, this will come in handy to help us break our habit of using the cd command every time.

4. Directory Stack Ecosystem

So far, we’ve just seen the basic change-directory functionality of the pushd and popd commands. Now, let’s take a holistic approach of the entire directory stack ecosystem comprised of the dirs, pushd, and popd commands.

4.1. Basics

Both pushd and popd commands do more than just change the current working directory. Internally, they manage a stack data structure to facilitate easy switching between the directories. While the pushd command adds a directory on top of the stack, on the other hand, the popd command removes an item from the top.

Well, let’s not forget about the dirs command, which plays an important part in documenting the contents of the directory stack. In fact, each time we use either pushd or popd command, a default invocation to the dirs command is also made:

$ dirs

Next, let’s digest these basic concepts by seeing all of these commands in action:

We must note one subtle characteristic of a managed directory stack. And, that concerns the directory present on the top of the stack. At any point in time, if we examine the contents of this directory stack by using the dirs command, then we’ll always find the current directory at the top.

4.2. Enumeration

Most of the common implementations of a stack data structure usually expose only the topmost element of the stack through push or pop functionality. But, the directory stack also gives us access to all the directories in the stack through direction-based relative indexes.

If we’re counting with respect to the top of the stack as our reference, then positive indexes are used. On the other hand, negative indexes are used when using the bottom of the stack as our reference:

And, as we can see, there are two variants of indexing available:

  • +N or -N indexes can be used only by the commands that are internal to the directory stack ecosystem — namely, dirs, pushd, and popd
  • ~+N or ~-N indexes can be used even by external commands such as cd, ls, and chmod that accept a directory path as an argument

And while we can do an index lookup by using the dirs command with the -v option:

$ dirs -v

On the other hand, we can use the +N or -N index to do a reverse lookup for the directory at a particular index within the directory content:

$ dirs [+N|-N]

4.3. Pass-by-Value vs. Pass-by-Reference

Another subtle but important way to look at the two types of enumeration is by seeing whether the directory argument is being passed as a value or as a reference. While the ~+N/~-N enumeration uses a pass-by-value strategy, on the other hand, +N/-N enumeration uses a pass-by-reference strategy.

Intuitively speaking, commands such as cd and ls, among others, are unaware of the internal structure and implementation of the directory stack. So, it makes sense that all the external commands can never mutate the directory stack, as they are limited by the pass-by-value strategy.

On the other hand, commands such as dirs, pushd, and popd are well-aware of the internal structure of the directory stack. As a result, they can mutate specific positions of the stack with the pass-by-reference strategy.

As we go further, we’ll solve some advanced use cases that’ll make use of these strategies to refer to a particular directory within the stack.

5. Directory Bookmarks

Switching between personal and professional work is quite common for most of us. In this section, we’ll learn how we can initialize the directory stack with a set of directories in an efficient manner.

5.1. Directory Structure

Let’s imagine that we have separate directories under our home directory to store our personal and work-related data:

~
|-- personal
|   |-- family
|   |-- movies
|   `-- pictures
`-- work
    |-- proj1
    |-- proj2
    `-- proj3

We’ll be using this directory structure to solve a few advanced use cases related to directory navigation and directory stack management.

5.2. -n Switch

Let’s say that we want to focus only on work. So, having all the relevant ~/work/<project> directories in the stack is likely to boost our productivity.

By default, the top of our directory stack is pre-populated with the default directory, which in our case is the home directory:

$ dirs -p -v
 0  ~

That’s perfect, as we don’t intend to change our current working directory.

Although we can easily use the pushd command here, by default, both pushd and popd commands also change the current working directory and mutate the top of the stack. And, that’s not something we want at the moment. As a matter of fact, what we need is that the top of the stack should remain as it is while we mutate the rest of the stack.

By now, we know about the invariant that the top of the stack is always associated with the current working directory. So, to achieve immutability for the top of the stack, we need a way to disable the directory change functionality of the pushd command. And, that’s where the -n switch is going to help us:

Next, let’s use this to populate our directory stack with our work-specific directories:

$ for dir in $(echo "~/work/proj"{1,2,3}); do
> pushd -n $dir 1>/dev/null
> done

Finally, let’s examine the directory stack:

$ dirs -p -v
 0  ~
 1  ~/work/proj3
 2  ~/work/proj2
 3  ~/work/proj1

Looks good, except that we just realized that proj1 is already completed. No worries, let’s pop it off by using its reference:

$ popd +3 1>/dev/null
$ dirs -p -v
 0  ~
 1  ~/work/proj3
 2  ~/work/proj2

6. Bookmarks Clean-up

With time, we might inadvertently push some directories onto the stack that are not related to our current focus area. So, let’s learn how we can use pushd and popd within a Bash script to automate a process of cleaning all the out-of-context directory bookmarks from the stack.

6.1. Automation

First, let’s see a sample of directory stack that needs a clean-up:

$ dirs -v -p
 0  ~/work/proj1
 1  ~/personal/pictures
 2  ~/work/proj2
 3  ~/personal/family
 4  ~

Now, let’s plan out a clean-up strategy:

  • We can pop all directories that don’t fall under the ~/<current_focus> directory
  • If all the directories are out-of-context, then put ~/<current_focus> directory on top of the stack

Well, it’d be wise on our part to automate this clean-up task. But, for that, we need to understand that dirs, pushd, and popd are built-in shell functions, and each running instance of the shell maintains its own copy of the directory stack.

As our goal is to clean up the directory stack for the current interactive Bash session, we need to have our code in the ~/.bashrc file. That’s because every interactive Bash session will invoke this script and load all the functions into the current bash process.

6.2. Script in Action

Let’s start by writing a bash function called clean_dir_bookmarks() that will treat the first argument ($1) as the desired focus area:

function clean_dir_bookmarks() {
    focus="$1"
    focus_dir=$HOME/$focus
    typeset -i index=1
}

Next, let’s loop through the directory stack within the same function. And, if we encounter a directory that doesn’t belong to the current focus area, then we’ll pop it off without changing the current directory:

while true
do
    stack_index="$(printf "+%d" $index)"
    dir="$(dirs -l $stack_index 2>/dev/null)"
    if [ $? -eq 0 ]
    then
        if [[ ! "$dir" =~ "$focus_dir" ]]
        then
            popd -n $stack_index 1>/dev/null
        fi
    else
        break
    fi
    index=index+1
done

We must notice that we rely on the successful execution of the dirs command to stay in the loop. That’s possible because the dirs command guarantees a non-zero exit code when used to inspect a non-existent position in the stack.

So far, we haven’t handled the top of the stack, as we need different handling for it. When the top directory belongs to the current focus area, there’s no action required. But, if it’s out-of-context, then we replace it with the next available directory. And, if that’s not available, then we simply keep the ~/<focus> directory on the top.

Well, let’s go ahead and finish our clean_dir_bookmarks() function:

top_dir="$(dirs -l +0 2>/dev/null)"
if [[ ! "$top_dir" =~ "$focus_dir" ]]
then
    dirs +1 1>/dev/null 2>/dev/null
    if [ $? -eq 0 ]
    then
        popd 1>/dev/null
    else
        pushd $focus_dir 1>/dev/null
        popd +1 1>/dev/null
    fi
fi

Finally, let’s use our function to do the required clean-up:

$ clean_dir_bookmarks personal
$ dirs -v -p
 0  ~/personal/pictures
 1  ~/personal/family

7. Stack Rotation

We don’t always need an explicit loop to work with multiple directories in the stack. In this section, we’ll learn about the advanced concepts related to directory stack rotation.

7.1. Incomplete Rotation Cycle

Let’s imagine that we were working on a few work-related projects, and then we decided to take a short break. Further, during the break, we started looking through some family pictures. As a result, our work-related directories are now pushed towards the bottom of the stack:

$ dirs -v -p
 0  ~/personal/family
 1  ~/personal/pictures
 2  ~/work/proj3
 3  ~/work/proj1

Now, we want to switch our focus to work, but we might want to revisit the personal directories in a short while. Naturally, using popd isn’t a good option in such a situation as that’ll pop out the personal directories. However, if we take a look at the stack’s content, we can see that our work directories are lying between the [+2,+3] stack indices. And, what we really need is to move all these directories up towards the top.

Well, it’s a very common use case for switching between the directories. And, the good news is that the pushd command addresses this by offering a stack rotation feature. Whenever we use a stack index with pushd, it performs an incomplete cycle of rotation with respect to the index, and the top of the stack changes to the directory referenced by that index:

As such, we can use either the positive or the negative index for this job. So, let’s go ahead and rotate our directory stack by using the positive index:

$ pushd +2
$ dirs -v -p
 0  ~/work/proj3
 1  ~/work/proj1
 2  ~/personal/family
 3  ~/personal/pictures

Voila! Our directory stack is exactly as we wanted it to be.

7.2. Replacement Using Complete Rotation Cycle

By a single invocation of pushd for the +i position, we can get an incomplete cycle of rotation comprising of N-i clockwise rotations. Now, if we invoke pushd again, but for the +(N-i) position, then we’ll get another incomplete cycle that has N-(N-i)=i clockwise rotations:

$ pushd +i
$ pushd +(N-i)

And, if we add them up, then we have exactly one full cycle of rotation. That essentially translates to no net change in the position of any directory item in the stack.

Now, let’s say we want to keep all our work-specific project directories in the stack. Further, if we examine the directory stack, we see that all such directories are available in the stack, except for the ~/work/proj3 directory:

$ dirs -v -p
 0  ~/work/proj1
 1  ~/work
 2  ~/personal/family
 3  ~/work/proj2

Moreover, we notice that ~/personal/family is occupying the place where we want to place our proj3 directory.

By now, we know that we can use positional arguments with popd to remove specific directories from the stack. But, so far, we haven’t learned a sophisticated replacement strategy that can help us to replace specific positions in the stack without disturbing other directories. Nonetheless, if we apply our learnings about the complete rotation cycle, then replacing a specific directory isn’t so difficult.

First, let’s do an incomplete rotation cycle to bring the ~/personal/family directory to the top of the stack:

$ pushd +2
$ dirs -v -p
 0  ~/personal/family
 1  ~/work/proj2
 2  ~/work/proj1
 3  ~/work

Next, let’s replace the top of the stack with ~/work/proj3 by doing a sequential execution of popd and pushd commands:

$ popd
$ pushd ~/work/proj3
$ dirs -v -p
 0  ~/work/proj3
 1  ~/work/proj2
 2  ~/work/proj1
 3  ~/work

Finally, let’s complete the rotation cycle so that ~/work/proj3 goes to the +2 index without any net change to other directories:

$ pushd +2
$ dirs -v -p
 0  ~/work/proj1
 1  ~/work
 2  ~/work/proj3
 3  ~/work/proj2

It looks like we’ve been successful in our efforts.

7.3. Replacement Using Cloning and Rotation

Now, let’s say we have all our work-specific project directories in the directory stack:

$ dirs -v -p
 0  ~/work
 1  ~/work/proj1
 2  ~/work/proj3

And, let’s say we decide to archive the ~/work/proj1 directory by moving it to ~/work/archived/proj1. Unfortunately, in such scenarios, the existing directories in the directory stack don’t rename themselves. It looks like we’ll have to do it ourselves.

Moreover, we can’t do this by a simple replacement strategy that uses a complete rotation cycle. That’s because the intermediary popd command will attempt to do a directory change to a non-existent directory, and end with a failure.

That’s where we can leverage a cloning-based rotation cycle:

$ pushd -n +i

The idea is that by using the -n switch, we can preserve the top of the stack. And, the result of such an operation is a modification to a simple rotation cycle where the top of the directory replaces the +(N-i) positioned directory with its clone:

So, this time, let’s start by pushing the target directory onto the stack:

$ pushd ~/work/archived/proj1
$ dirs -v -p
 0  ~/work/archived/proj1
 1  ~/work
 2  ~/work/proj1
 3  ~/work/proj3

Next, let’s do a full-cycle rotation using the cloning technique:

$ pushd -n +2
$ pushd -n +2
$ dirs -v -p
 0  ~/work/archived/proj1
 1  ~/work
 2  ~/work/archived/proj1
 3  ~/work/proj3

Finally, let’s pop off the duplicate item from the top of the stack:

$ popd
$ dirs -v -p
 0  ~/work
 1  ~/work/archived/proj1
 2  ~/work/proj3

Yay! We did it.

8. Conclusion

In this tutorial, we started by exploring the need for the pushd and popd commands. From there, we empowered ourselves to break the habit of relying on the cd command for all our directory navigation use cases.

Later, not only did we dedicate ourselves to a deep-dive exploration of the concepts associated with the directory stack, but we also applied them to solve a few common and advanced use cases.

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments