jasonwryan.com

Miscellaneous ephemera…

Improved Notes Utility

Nearly two years ago, I posted about my adaption of a simple command line note utility. I have used this setup on all of my machines on a daily basis since and it has worked marvellously. Symlinking to a folder in Dropbox means that the notes are accessible from all my machines, including my phone. There has only really been one aspect of this setup that has been sub-optimal.

As an inveterate note-taker (this is one of the “benefits” of ageing; the speed with which you forget information outpaces the acquisition of newer material) I have—in those intervening years—built up quite a store of notes. Consequently, in order to maintain a semblance of order, I have arranged them in a series of directories. There is a minor flaw with this approach: retreiving a note depended on two factors, a) excellent recall1 and, b) accurately typing out the full path. Neither of these are things that I am inherently good at or inclined to master.

This had been irritating me for some time before I came across this question on Unix & Linux StackExchange. This provided me with a partial solution to the issue but, as I note in my answer, I was not able to solve it for nested directories, which was my particular use case. Once I had muddled my way through the solution on U&L I pushed it to the back of my mind and tried to ignore it.

Recently, though, the accumulation of notes and the frustration of trying to access them without Tab completion drove me to do something about it.

The documentation on programmable completion is typically terse and searching the web returns very little in the way of instructions as to how to accomplish this.2 Undeterred, I decided to hack up a completion function that worked for nested directories.

What I arrived at was this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
shopt -s globstar
shopt -s progcomp

n() { $EDITOR $HOME/.notes/"$*".txt ;}

 # completion for notes
_notes() {
local cur
    cur=${COMP_WORDS[COMP_CWORD]}
    files=($HOME/.notes/**)
    file="${files[@]##*/}"
    COMPREPLY=( $( compgen -f "${file[@]}" -- ${cur} ) )
}
complete -o default -F _notes n

The best that can be said about it is that it nearly works…3

Realising that I was completely out of my depth, I turned to #bash for help, and I was indeed fortunate that geihra offered some much needed assistance. geirha’s solution is an elegant one:

1
2
3
4
5
6
7
8
9
10
n() {
local arg files=(); for arg; do files+=( ~/".notes/$arg" ); done
${EDITOR:-vi} "${files[@]}"
}

_notes() {
local files=($HOME/.notes/**/"$2"*)
    [[ -e ${files[0]} ]] && COMPREPLY=( "${files[@]##~/.notes/}" )
}
complete -o default -F _notes n

In addition to working exactly as I hoped, it had the benefit of introducing me to a couple more bash concepts that I hadn’t encountered; adding elements to an array with +=() being one. For posterity, the full script is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
n() {
local arg files=(); for arg; do files+=( ~/".notes/$arg" ); done
${EDITOR:-vi} "${files[@]}"
}

nls() {
tree -CR --noreport $HOME/.notes | awk '{ 
    if (NF==1) print $1; 
    else if (NF==2) print $2; 
    else if (NF==3) printf "  %s\n", $3 
    }'
}

 # TAB completion for notes
_notes() {
local files=($HOME/.notes/**/"$2"*)
    [[ -e ${files[0]} ]] && COMPREPLY=( "${files[@]##~/.notes/}" )
}
complete -o default -F _notes n

Notes

  1. The alternative to remembering the full path name is to list all of the notes before each operation with the nls function; this is not ideal either…
  2. Which means that it is either so straightforward that few have bothered to write up their experiences (most likely), or so arcane that not many have bothered (how it feels to me). However, there are a couple of pages that I referenced in addition to the official documentation:
  3. It fails, as geirha pointed out, because it breaks the filenames on whitespace.

Creative Commons image on Flickr by nicholasjon.

Comments