Go program stores directory paths
Programming Snapshot – cdbm
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).
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:
mybox.mschilli:git$
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
cdbm.go
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
(incl. VAT)
Buy Linux Magazine
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.
News
-
New Slimbook EVO with Raw AMD Ryzen Power
If you're looking for serious power in a 14" ultrabook that is powered by Linux, Slimbook has just the thing for you.
-
The Gnome Foundation Struggling to Stay Afloat
The foundation behind the Gnome desktop environment is having to go through some serious belt-tightening due to continued financial problems.
-
Thousands of Linux Servers Infected with Stealth Malware Since 2021
Perfctl is capable of remaining undetected, which makes it dangerous and hard to mitigate.
-
Halcyon Creates Anti-Ransomware Protection for Linux
As more Linux systems are targeted by ransomware, Halcyon is stepping up its protection.
-
Valve and Arch Linux Announce Collaboration
Valve and Arch have come together for two projects that will have a serious impact on the Linux distribution.
-
Hacker Successfully Runs Linux on a CPU from the Early ‘70s
From the office of "Look what I can do," Dmitry Grinberg was able to get Linux running on a processor that was created in 1971.
-
OSI and LPI Form Strategic Alliance
With a goal of strengthening Linux and open source communities, this new alliance aims to nurture the growth of more highly skilled professionals.
-
Fedora 41 Beta Available with Some Interesting Additions
If you're a Fedora fan, you'll be excited to hear the beta version of the latest release is now available for testing and includes plenty of updates.
-
AlmaLinux Unveils New Hardware Certification Process
The AlmaLinux Hardware Certification Program run by the Certification Special Interest Group (SIG) aims to ensure seamless compatibility between AlmaLinux and a wide range of hardware configurations.
-
Wind River Introduces eLxr Pro Linux Solution
eLxr Pro offers an end-to-end Linux solution backed by expert commercial support.