Bash Tips and Tricks: 'cd' with style

What is Bash? "Descended from the Bourne Shell, Bash is a GNU product, the "Bourne Again SHell." It's the standard command line interface on most Linux machines. It excels at interactivity, supporting command line editing, completion, and recall." --Bash Prompt HOWTO


Okay, so one of the very first things one learns when being introduced to the command line (regardless of which shell) is how to change directory (the 'cd' command). This hinges on the concept of the "Present (or Current) Working Directory", analagous to one's "location" in the filesystem.

To see your current working directory, use the shell built-in command 'pwd'. (NOTE: There is also a binary, /bin/pwd, but this is often superseded by the shell built-in.)

Whatever commands you execute are performed within the context of your current working directory. If you use a relative path, say ../bin/pwd, to reference a program, then the location in the filesystem that this path refers to depends on your current working directory. Also, just typing a command can have its pitfalls depending on the value of the $PATH variable. $PATH can also contain relative paths, such as ./ which, depending on your current location, could alter the results of the search.

Alright, so now you all know the basic idea behind the current working directory. There are some interesting lesser-known tricks in Bash that make changing directory much more convenient and powerful, though...


First, a simple one that somehow evaded my attention for far too long. I would often spend a minute or more trying to find a particular buried directory, like /usr/X11R6/lib/X11/etc/. Then I would remember that in order to do what I went there to accomplish, I had to first do something elsewhere in the filesystem.

Rather than lose my place, not wanting to retype in (even with Bash's path-completion) that long path, I would take the time to open up another shell, perhaps even establishing multiple ssh connections into a single box.

If this option was particularly unappealing, I could run 'pwd', copy the contents into the X buffer with the mouse, and then use that to paste in an argument to 'cd' after doing whatever it is I had to do elsewhere. The X buffer is volatile, however, and relying on it is therefore risky.

The simple trick is that Bash has a built-in memory of the previous working directory. To return to this location, just type 'cd -'. Just remembering this very simple form of the 'cd' command will save you time and keystrokes!


This capability to remember the prior directory has been extended using the programming analogy of a stack. A stack is a structure onto which you can "push" things and off of which you can "pop" things. You can only access the "top" of the stack.

You will not be surprised to learn that the commands for remembering directories are 'pushd' and 'popd'. The command for printing the contents of the stack is 'dirs'.

There are a few subtleties to using the 'pushd' command. First, you actually have to specify the directory to be added. Fortunately the directory to be added is usually the current directory ('.'). If you do not, the top two entries on the stack are swapped and you are taken to the new top directory.

'popd' is rather straightforward. With no arguments, it takes the topmost entry from the stack and changes to that directory.

'dirs' actually prints the current directory followed by the contents of the stack, starting from the top. Interestingly, observing the behavior of 'pushd', the current directory is actually considered to be the topmost directory on the stack!

An example will demonstrate how this all fits together:

[jason@jabby jason]$ pushd .
~ ~
[jason@jabby jason]$ cd /tmp
[jason@jabby tmp]$ pushd .
/tmp /tmp ~
[jason@jabby tmp]$ cd /usr/X11R6/lib/X11/etc/
[jason@jabby etc]$ pushd .
/usr/X11R6/lib/X11/etc /usr/X11R6/lib/X11/etc /tmp ~
Okay, so at this point I have added three directories to the stack and the first directory on the line is my current directory.
[jason@jabby etc]$ cd
[jason@jabby jason]$ dirs
~ /usr/X11R6/lib/X11/etc /tmp ~
Now you see that the first directory displayed has changed because I've changed directories.
[jason@jabby jason]$ pushd
/usr/X11R6/lib/X11/etc ~ /tmp ~
Note that the top two entries have swapped places and my "location" has changed.
[jason@jabby etc]$ popd
~ /tmp ~
[jason@jabby jason]$ popd
/tmp ~
[jason@jabby tmp]$ popd
~
[jason@jabby jason]$ popd
bash: popd: directory stack empty
[jason@jabby jason]$ dirs
~
[jason@jabby jason]$ 
The stack is empty, but 'dirs' still displays my current directory.


Another interesting trick is the $CDPATH variable. Similar in nature to the $PATH variable, it allows an argument to the 'cd' command to be considered not only in the current working directory, but in an ordered list of directories!

It is not straightforward to see the utility of this feature, but bear with me. Each user has a particular area of interest on a given Linux system. A system administrator (root) would be interested in /etc and /var/log. A home desktop user (joe) would be interested in ~/Documents. Someone at work (juser) might spend most of his or her time in a /usr/local/projects directory.

So, by placing each user's commonly visited paths in that user's $CDPATH variable, one can change to a known subdirectory without first having to navigate there! For example:

(pwd = /home/juser)
cd /usr/local/projects/abc
vs.
cd abc
(pwd = /usr/local/projects/abc)

(pwd = /usr/local/projects/abc/src/db/old/migration/)
cd ../../../..
vs.
cd abc
(pwd = /usr/local/projects/abc)

Indeed, one can find many subtle uses for this feature. A reasonable list to start with on a Red Hat Linux system, for example, might be

.:~:~/docs:/mnt:/usr/src/redhat:/usr/src/redhat/RPMS:/usr/src:/usr/lib:/usr/local

*NOTE: An empty entry ('::' or a leading or trailing ':') in $CDPATH (or in $PATH) is interpreted as the current working directory!


Something you may have seen before in other systems (the much maligned SCO OSes, for example) is this handy option:

shopt -s cdspell

"This will correct minor spelling errors in a 'cd' command, so that instances of transposed characters, missing characters and extra characters are corrected without the need for retyping."

[jason@localhost jason]$ cd Documnts
Documents
[jason@localhost Documents]$

[jason@localhost jason]$ cd documents
Documents
[jason@localhost Documents]$

Unfortunately, it's flexibility is rather limited...

[jason@localhost jason]$ cd document
bash: cd: document: No such file or directory
[jason@localhost jason]$


Finally, I want to show you how to write your own custom replacement for the 'cd' command.

Do you find yourself always typing the same thing upon changing into a directory? You probably at least list the files there every time, perhaps so much that your hands automatically type 'ls' after every 'cd'.

Well, by trying every way I could think of, it turns out there's only one way to properly accomplish the goal we're seeking. We have to create a shell function.

Shell functions are part of shell programming. Like in compiled programming languages, functions provide a sort of procedural modularizability. One can create a generic function to perform an often-used bit of logic or computation with different parameters. In this case, the parameter is the current working directory.

Here's a simple one:

function cs () {
	cd $1
	ls
}

Note that this is evaluated as if it were entered at the command line, so any aliases already loaded in that shell apply. This has implications about the order in which you define your functions and your aliases in your .bashrc file. So, the above example might actually be stored in memory like this:

[jason@localhost jason]$ type cs
cs is a function
cs ()
{
	cd $1;
	ls -F --color=auto
}

This simple function just changes to the directory specified ($1) and then lists the contents. You can elaborate on this function to any degree you wish. I've come to settle on the following, although I admit I don't use it as a full-time replacement for 'cd':

function cs () {
	clear
	# only change directory if a directory is specified
	[ -n "${1}" ] && cd $1
	# filesystem stats
	echo "`df -hT .`"
	echo ""
	echo -n "[`pwd`:"
	# count files
	echo -n " <`find . -maxdepth 1 -mindepth 1 -type f | wc -l | tr -d '[:space:]'` files>"
	# count sub-directories
	echo -n " <`find . -maxdepth 1 -mindepth 1 -type d | wc -l | tr -d '[:space:]'` dirs/>"
	# count links
	echo -n " <`find . -maxdepth 1 -mindepth 1 -type l | wc -l | tr -d '[:space:]'` links@>"
	# total disk space used by this directory and all subdirectories
	echo " <~`du -sh . 2> /dev/null | cut -f1`>]"
	ROWS=`stty size | cut -d' ' -f1`
	FILES=`find . -maxdepth 1 -mindepth 1 |
	wc -l | tr -d '[:space:]'`
	# if the terminal has enough lines, do a long listing
	if [ `expr "${ROWS}" - 6` -lt "${FILES}" ]; then
		ls -ACF
	else
		ls -hlAF --full-time
	fi
}

WARNING: Even on a fast box, the 'du' can sometimes take a while ('cs /usr'!).


I hope this has been an informative and useful guide to Bash's changing directory features. Hopefully next time I'll get together a presentation on programmable completion! :-)

Jason Bechtel <jason bechtel a@t care 2 d.o.t com>