loading...
Cover image for BASH tricks: The dir stack.

BASH tricks: The dir stack.

cpu profile image Daniel McCarney Updated on ・5 min read

The BASH shell is jam-packed with features. One of my favourite things is when I learn a cool BASH trick. It feels great to add a new tool to your toolbelt. Here's one tool that I quickly integrated into my day-to-day when I first learned about it: the BASH directory stack.

The hard way

A common scenario when using the command line is finding yourself in one directory but needing to quickly check something in another directory. Maybe you even need to peek at a few things in a subdirectory of that other directory. How do you get back to where you started when you're done?

Let's imagine I'm working on a Go application in ~/go/src/github.com/cpu/someproject and I want to remind myself how I configured MySQL by checking /etc/mysql/ and its subdirectory of config fragments /etc/mysql/my.cnf.d.

The most straight forward way is to just cd around:

daniel@noir:~/go/src/github.com/cpu/someproject $> cd /etc/mysql/
daniel@noir:/etc/mysql $> ls -l
total 8
-rw-r--r-- 1 root root  208 Jan 25 09:21 my.cnf
drwxr-xr-x 2 root root 4096 Feb  2 12:57 my.cnf.d
daniel@noir:/etc/mysql $> # Oh wait, I need to check the fragments in my.cnf.d
daniel@noir:/etc/mysql $> cd my.cnf.d
daniel@noir:/etc/mysql/my.cnf.d $> # Nope! Didn't find what I wanted, guess I need to check the base `my.cnf`.
daniel@noir:/etc/mysql/my.cnf.d $> cd ..
daniel@noir:/etc/mysql $> # Aha! That had what I needed! Back to work
daniel@nori:/etc/mysql $> cd ~/go/src/github.com/cpu/someproject
daniel@noir:~/go/src/github.com/cpu/someproject $>

That's four separate cd commands! Worse, I had to remember the full path of the project where I started to cd back to it. You can probably imagine a more complicated example with deeply nested directories where these problems are even more apparent. This is where in my informercial for the BASH directory stack I'd have the voice-over announce "Surely there's a better way!"

infomercial struggle

The easy way

Of course the answer is the BASH directory stack ! It works just like any other stack datastructure you might be familiar with already (the classic real life example is a stack of dinner plates). Entries are added and removed from the top of the stack and we can refer to other entries lower in the stack based on their relative position from the top.

Directories can be added to the stack with pushd. The pushd command accepts relative paths as well as absolute paths. You can remove the topmost directory and automatically cd to it with one popd command. Lastly you can view the current directory stack with dirs. I prefer to use dirs -v to see each stack entry on its own line with a numeric index (0 being the top of the stack, 1 being the 1st from the top, etc).

Let's see this in action by returning to the example from before, but this time I'll use the directory stack:

daniel@noir:~/go/src/github.com/cpu/someproject $> pushd /etc/mysql/
daniel@noir:/etc/mysql $> ls -l
total 8
-rw-r--r-- 1 root root  208 Jan 25 09:21 my.cnf
drwxr-xr-x 2 root root 4096 Feb  2 12:57 my.cnf.d
daniel@noir:/etc/mysql $> # Oh wait, I need to check the fragments in my.cnf.d
daniel@noir:/etc/mysql $> pushd my.cnf.d
daniel@noir:/etc/mysql/my.cnf.d $> # Nope! Didn't find what I wanted, guess I need to check the base `my.cnf`.
daniel@noir:/etc/mysql/my.cnf.d $> popd
daniel@noir:/etc/mysql $> # Aha! That had what I needed! Back to work
daniel@nori:/etc/mysql $> popd
daniel@noir:~/go/src/github.com/cpu/someproject $>

That feels easier! You can see I used pushd in two ways: once with an absolute path (pushd /etc/mysql/) and once with a relative path (pushd my.cnf.d). I was able to use popd to return from directories I changed into and I didn't need to remember anything about the previous directories I wanted to return to.

If I had run dirs -v after the second pushd into my.cnf.d the stack would have looked like this:

 0  /etc/mysql/my.cnf.d
 1  /etc/mysql
 2  ~/go/src/github.com/cpu/someproject

Scripting

Another place I use the BASH directory stack a lot is in small BASH scripts. Often I'll need to run a command in a specific directory but I will want to return to a different directory afterwards. One way to do that is to carefully balance cd commands in the script:

#!/usr/bin/env bash

# Sort some input data into an output directory
cat input/day1/data.csv | cut -f, -d1 | sort > output/day1/sorted.data.csv
# Change to the output directory
cd output/day1
# Run some tool that looks for *.csv files in the current directory
summaryTool -q -v
# Change back to the directory the script is being run in
cd ../../

This script is pretty brittle. It's easy to imagine that someone down the road might decide the day1 directory isn't needed and refactor the cat command and the first cd to:

cat input/data.csv | cut -f, -d1 | sort > output/sorted.data.csv
cd output/

If there are a lot of commands between the first cd and the second cd it could be easy to accidentally update only one of them. If this happens then the second cd ../../ will be out of sync with the first and change back one directory too far. Uh oh!

Using pushd and popd removes the possibility for this bug:

#!/usr/bin/env bash

# Sort some input data into an output directory
cat input/day1/data.csv | cut -f, -d1 | sort > output/day1/sorted.data.csv
# Change to the output directory
pushd output/day1
  # Run some tool that looks for *.csv files in the current directory
  summaryTool -q -v
# Change back to the directory the script is being run in
popd

(To make things even clearer I often indent the commands between the pushd and popd.)

Using popd lets you embrace the DRY principle and not duplicate information between two separate cd commands.

Conclusion

Try using pushd, popd, and dirs throughout your day as you use the command line. When you find yourself using cd ask yourself whether pushd would make your life easier. With practice you might find you like the BASH directory stack enough to add alias cd=pushd to your ~/.bashrc :-)

Posted on Feb 1 '19 by:

cpu profile

Daniel McCarney

@cpu

I live in the woods and help write the free software that powers Let's Encrypt.

Discussion

markdown guide
 

A lesser-known feature of most shells (including bash) is cd - which returns you to the previous directory you were in, without affecting (or relying on) the stack. This is useful if you cd somewhere and then realize you need to go back to the previous one. It's basically an undo for cd!

And of course you can combine this with pushd and do pushd -.

Some other tools make use of this nomenclature as well; for example, git checkout uses - as a quick alias to the previous branch you were in.

 

And of course you can combine this with pushd and do pushd -.

Cool! I knew about cd - but I never thought to try pushd -. Thanks for sharing @fluffy , TIL.

 

I've always either used cd - as @fluffy pointed out or more often I used subshells (if I'm not mistaken about the name) which is simply used through parenthesis.
For instance: (cd /etc/mysql && exec ls)
but the one you have taught here is even cooler than the ones I've been using and I didn't know it. Thanks!

 

Ah yeah subshells are great. I mostly use them to gather up the output from a bunch of commands but they’re also great for encapsulating environment. Note that you can also use ;s in them too!

 

You might also always want to secure your scripts, so that they exit on failure of changing the directory.

Different tools do different things; I'd advise against aliasing it to cd. The default behaviour is so fundamentally different and sometimes you don't necessary want to return to the directory where you have been before. You might be building up a stack that you don't need and not even want.

 

Another handy trick is to shove everything into a subshell. Doesn't work for everything, especially if you want to pass variables around, but for quick things that need to run in a particular directory without affecting the script state, this:

(cd output/day1 &&summaryTool -q -v)

does it all for you.

 
daniel@noir:/etc/mysql/my.cnf.d $> # Nope! Didn't find what I wanted, guess I need to check the base `my.cnf`.
daniel@noir:/etc/mysql $> cd ..
daniel@noir:/etc/mysql $> # Aha! That had what I needed! Back to work

and

daniel@noir:/etc/mysql/my.cnf.d $> # Nope! Didn't find what I wanted, guess I need to check the base `my.cnf`.
daniel@noir:/etc/mysql $> popd
daniel@noir:/etc/mysql $> # Aha! That had what I needed! Back to work

do not look right.

Should probably look like:

daniel@noir:/etc/mysql/my.cnf.d $> # Nope! Didn't find what I wanted, guess I need to check the base `my.cnf`.
daniel@noir:/etc/mysql/my.cnf.d $> cd ..
daniel@noir:/etc/mysql $> # Aha! That had what I needed! Back to work

and

daniel@noir:/etc/mysql/my.cnf.d $> # Nope! Didn't find what I wanted, guess I need to check the base `my.cnf`.
daniel@noir:/etc/mysql/my.cnf.d $> popd
daniel@noir:/etc/mysql $> # Aha! That had what I needed! Back to work

The directory needs to change as a result of a cd .. or popd.
The preceeding comment line isn't supposed to change the directory.

 

Hi @karlrado - You're 100% correct, thanks for pointing out the erroneous snippets 🏆. I updated the article text with fixes.

 

I had a similar issue not to long ago and I created scripts that I added to bashrc that allowed me traverse our fs much quicker than just using cd. Of course in a lot of cases it was unavoidable but, it started me in the right place. Great post.

 

Thanks, you both taught me something I plan to use a lot and gave me a new irrational fear of measuring tape.

 

Wait, pushd also changes the directory? I am always doing 'pushd .; cd whatever/path/'...

I feel a little dumb now 🙈 but thank you anyway! It all makes sense now.