Go program stores directory paths

Programming Snapshot – cdbm

© Lead Image © lightwise, 123rf, 123RF.com

© Lead Image © lightwise, 123rf, 123RF.com

Article from Issue 228/2019

When you change directories at the command line, you often find yourself jumping back and forth between known paths. With a utility written in Go, Mike Schilli records the jumps and shows the way back.

While younger coworkers tend to edit their programs with clever IDEs, I still find it most natural to jump to local Git repositories with a quick cd at the command line and fire up Vim at files with the source code residing there. Typing in the directory path each time is a pain in the ass, and there are usually only half a dozen paths back and forth – so the command line should be able to remember that.

The C shell invented the pushd and popd commands many years ago, but wouldn't it be much more convenient to automatically record the directories you visit, store them in a database, and even offer search queries for previously visited directories based on criteria such as frequency or the timestamp of the last visit?

In this issue, a Go program by the name of cdbm collects the paths accessed by the user during a shell session; the command line user just uses cd, and some magic glue in the shell's configuration then taps into the $PS1 prompt generator. If the directory changes, cdbm gets called and stores the new path in an SQLite database on the disk, which later allows search queries whose results can be accessed directly by the user for navigation help. Bash users can modify their .bashrc file to enable this. On typing a newly introduced command c, users will see a selection list with the last directories visited (Figure 1). After selecting one of them with the cursor keys and pressing Enter, the shell directly jumps there (Figure 2).

Figure 1: The c command shows you the last directories you visited …
Figure 2: … and jumps to the one you select.

If the list of hits grows beyond the preconfigured set limit of five entries, the nifty terminal UI (as shown in Figure 1) displays a small down arrow, indicating that the user can move the cursor further down to reveal previously hidden entries. So how does this work?

Hitchhiking a Prompt Ride

Once the Bash shell has executed a command, it generates the line prompt so that the user knows that it is his turn again. Instead of boring $ or # characters, experienced shell users often define individual prompts in the $PS1 variable; they can display the username, the hostname, and the current directory. For example, the following statement

export PS1='\h.\u:\W$ '

defines a prompt with the hostname (\h), a separating dot, the username (\u), a separating colon, the current directory, a dollar sign, and a space. On my machine in the git directory, this comes up as:


Now the $PS1 prompt variable does not just support the placeholders used above, which it replaces with current values, but also commands to be executed, whose output it interpolates into the prompt string:

export PS1='$(cdbm -add)\h.\u:\W\$ '

This definition tells Bash to call the cdbm program with the -add option after every shell command executed. cdbm is the Go program in Listing 1 [1] that determines the current directory in -add mode and stores the path with the current timestamp in a table of an automatically created SQLite single file database. If the path already exists, cdbm only refreshes the timestamp for the entry. While the user keeps changing directories with cd, paths with timestamps accumulate in the database (Figure 3).

Listing 1



Figure 3: The SQLite database stores paths recently traveled along with timestamps.

Of course cdbm -add does not output anything, but returns without comment after the work is done, so that the $PS1 prompt defined above remains the same, even if the Bash shell secretly called the directory butler while composing the prompt.

Here We Go

To compile Listing 1, the following command sequence generates a new Go module in the same directory where the build process happens later on:

go mod init cdbm
go build

Listing 1 references a number of useful Go packages on GitHub, which the call to go build automatically retrieves as source code, because of the previous module definition, and compiles as libraries before compiling Listing 1. The resulting cdbm binary contains everything, including a driver for creating and querying SQLite databases.

Once you have copied the binary to a location where the shell can find it in the search $PATH, you have to change two things in the .bashrc bash profile in order to benefit from the new utility. First, add the $PS1 definition from above and second, define a Bash function c that calls cdbm in selection mode and later outputs the path the user selected:

export PS1='$(cdbm -add)\h.\u:\W\$ '
function c() { dir=$(cdbm 3>&1 1>&2 2>&3); cd $dir; }

Now, if you type c after .bashrc has run (either automatically when opening a new shell or manually via source .bashrc) in the shell, the newly defined bash function c above will call the cdbm program. The latter writes the selection list to stdout, the user then interacts with the cursor keys, selects a directory with Enter, and cdbm writes the result to stderr.

Now the function only has to pass the contents of stderr (the selected path) to the shell's cd function, which then changes to the specified directory. This is easier said than done, because cd is not a program, but a built-in shell function. A program could change its own working directory, but not that of the parent process, the shell itself. To complicate matters, unlike other Unix commands, cd insists on being provided with an actual argument, the directory, which it cannot read from stdin by way of a pipe.

This explains the wild trick the Bash function resorts to above. After calling cdbm it swaps its stdout and stderr channels. To do this, it first uses 3>&1 to define a file descriptor named 3 and points it to the same channel as the file descriptor 1 (i.e., stdout). The following redirection 1>&2 assigns a new value to the 1 descriptor and points it to a descriptor 2 (i.e., stderr). The third – that is 2>&3 – assigns the value of the temporarily used file descriptor 3 (i.e., the cached stdout) to stderr. In other words, the terminal UI's output of cdbm no longer ends up in stdout, but instead in stderr, and the result of the selected directory is sent to stdout. We in engineering call this a "switcheroo."

The dir=$(...) construct then grabs stdout and assigns it to the $dir variable. The following cd statement for the directory change, separated by a semicolon, receives the value from the variable and jumps to the specified directory. This whole rigmarole was necessary, because the easy way of capturing stdout does not work, as the terminal UI insists on writing to it, and redirecting it would leave the user without any visual output with which to interact.

Nitty Gritty

The cdbm.go program in Listing 1 only has to do two things. First, if the -add option is present, it stores the current working directory in the SQLite database. If the option is not set, it displays the terminal UI with the SQLite entries, lets the user select one, and outputs the chosen path to stderr.

To do so, it defines the -add option with the help of the standard flag package in line 15. If cdbm is called with -add, the pointer value dereferenced with *addMode has a true value after parsing the command-line arguments with flag.Parse(), and line 33 branches to the function dirInsert() starting in line 85. In display mode, the else branch starting in line 34 uses dirList() to fetch all the paths stored in the SQLite database and sorts them in descending order of the dates on which they were added.

The terminal UI for selecting a directory gets drawn by the promptui package, which offers Select() and Run() functions to configure the list and then switch to user interaction mode. The result, the path selected by the user as a string, is finally output to stderr by lines 44 and 45, whereupon the program terminates.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • SQLite Tutorial

    Several databases likely reside on your desktop and smartphone, and it is easy to manage the data in these files or to create similar databases yourself.

  • Patterns in the Archive

    To help him check his Google Drive files with three different pattern matchers, Mike builds a command-line tool in Go to maintain a meta cache.

  • Usql

    Usql is a useful tool that lets you manage many different databases from one prompt.

  • Programming Snapshot – Go

    To find files quickly in the deeply nested subdirectories of his home directory, Mike whips up a Go program to index file metadata in an SQLite database.

  • Perl: Archiving PDFs

    This month you’ll learn how to place articles in a private PDF archive and how to use a database to access those articles at a later time.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More