Notes on Unix

As a long-time user of Unix-like systems, I prefer to do as much work in the command-line as possible. I store data in plain text whenever appropriate. I edit in vim and take advantage of the pipeline to manipulate the data with powerful tools like awk and grep.

Notes are one such instance where plain text makes sense. All of my notes – which are scratch-pads for ideas, reference material, logs, and whatnot – are kept as individual text files in the directory ~/documents/notes. The entire ~/documents directory is synced between my laptop and my work computer, and of course it is backed up with tarsnap.

When I want to read, edit or create a note, my habit is simply to open the file in vim.

1
$ vim ~/documents/notes/todo.txt

When I want to view a list of my notes, I can just ls the directory. I pass along the -t and -r flags. The first flag sorts the files by modification date, newest first. The second flag reverses the order. The result is that the most recently modified files end up at the bottom of the list, nearest the prompt. This allows me to quickly see which notes I have recently created or changed. These notes are generally active – they’re the ones I’m currently doing something with, so they’re the ones I want to see. Using ls to see which files have been most recently modified is incredibly useful, and a behaviour that I use often enough to have created an alias for it.

1
$ lt ~/documents/notes

Recently I was inspired by some extremely simple shell functions on the One Thing Well blog to make working with my notes even easier.

The first function, n(), takes the name of the note as an argument – minus the file extension – and opens it.

1
2
3
function n {
nano ~/Dropbox/Notes/$1.txt 
}

I liked the idea. It would allow me to open a note from anywhere in the filesystem without specifying the full path. After changing the editor and the path, I could open the same note as before with far fewer keystrokes.

1
$ n todo

I needed to make a few changes to the function to increase its flexibility.

First, the extension. Most of my notes have .txt extensions. Some have a .gpg extension.1 Some have no extension. I didn’t want to force the .txt extension in the function.

If I specified a file extension, that extension should be used. If I failed to specify the extension, I wanted the function to open the file as I specified it only if it existed. Otherwise, I wanted it to look for that file with a .gpg extension and open that if it was found. As a last resort, I wanted it to open the file with a .txt extension regardless of whether it existed or not. I implemented this behaviour in a separate function, buildfile(), so that I could take advantage of it wherever I wanted.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Take a text file and build it with an extension, if needed.
# Prefer gpg extension over txt.
buildfile() {
    # If an extension was given, use it.
    if [[ "$1" == *.* ]]; then
        echo "$1"

    # If no extension was given...
    else
        # ... try the file without any extension.
        if [ -e "$1" ]; then
            echo "$1"
        # ... try the file with a gpg extension.
        elif [ -e "$1".gpg ]; then
            echo "$1".gpg
        # ... use a txt extension.
        else
            echo "$1".txt
        fi
    fi
}

I then rewrote the original note function to take advantage of this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Set the note directory.
NOTEDIR=~/documents/notes

# Create or edit notes.
n() {
    # If no note was given, list the notes.
    if [ -z "$1" ]; then
        lt "$NOTEDIR"
    # If a note was given, open it.
    else
        $EDITOR $(buildfile "$NOTEDIR"/"$1")
    fi
}

Now I can edit the note ~/documents/notes/todo.txt by simply passing along the filename without an extension.

1
$ n todo

If I want to specify the extension, that will work too.

1
$ n todo.txt

If I have a note called ~/documents/notes/world-domination.gpg, I can edit that without specifying the extension.

1
$ n world-domination

If I have a note called ~/documents/notes/readme, I can edit that and the function will respect the lack of an extension.

1
$ n readme

Finally, if I want to create a note, I can just specify the name of the note and a file with that name will be created with a .txt extension.

The other change I made was to make the function print a reverse-chronologically sorted list of my notes if it was called with no arguments. This allows me to view my notes by typing a single character.

1
$ n

The original blog post also included a function ns() for searching notes by their title.

1
2
3
function ns {
ls -c ~/Dropbox/Notes | grep $1
}

I thought this was a good idea, but I considered the behaviour to be finding a note rather than searching a note. I renamed the function to reflect its behaviour, took advantage of my ls alias, and made the search case-insensitive. I also modified it so that if no argument was given, it would simply print an ordered list of the notes, just like n().

1
2
3
4
5
6
7
8
# Find a note by title.
nf() {
    if [ -z "$1" ]; then
        lt "$NOTEDIR"
    else
        lt "$NOTEDIR" | grep -i $1
    fi
}

I thought it would be nice to also have a quick way to search within notes. That was accomplished with ns(), the simplest function of the trio.

1
2
# Search within notes.
ns() { cd $NOTEDIR; grep -rin $1; cd "$OLDPWD"; }

I chose to change into the note directory before searching so that the results do not have the full path prefixed to the filename. Thanks to the first function I don’t need to specify the full path of the note to open it, and this makes the output easier to read.

1
2
3
$ ns "take over"
world-domination.txt:1:This is my plan to take over the world.
$ n world-domination

To finish it up, I added completion to zsh so that I can tab-complete filenames when using n.

1
2
# Set autocompletion for notes.
compctl -W $NOTEDIR -f n

If you’re interested in seeing some of the other way that I personalize my working environment, all of my dotfiles are on GitHub. The note functions are in shellrc, which is my shell-agnostic configuration file that I source from both zshrc and bashrc2.

Notes

  1. I use vim with the gnupg.vim plugin for seamless editing of PGP-encrypted files.
  2. I prefer zsh, but I do still find myself in bash on some machines. I find it prudent to maintain configurations for both shells. There's a lot of cross-over between them and I like to stick to the DRY principle.