bm CLI Completion (Part 2)
In bm CLI
Completion, I discussed adding support for completion of
command-line options and arguments to bm. Completion for
bm
is primarily useful in completing keywords, allowing
users to search for a bookmark by tabbing through the keyword hierarchy.
Bookmark keywords are not handled like filenames, so the
complete
command is not configured to treat completions as
filename completions. The --config
argument is a
filename, however. Though I implemented completion for this argument,
the behavior was not correct because a space was inserted after
directory names during completion.
The Issue
Completion for a --config
argument is fairly involved.
If the argument is an existing directory, then directories and YAML
files in that directory are offered as possible completions. If the
argument is an existing YAML file, then it is offered as a possible
completion. If the argument does not exist, the directory part of the
argument is checked and directories and YAML files in that directory
that match the argument are offered as possible completions when the
directory exists. In other cases, no possible completions are
returned.
The implementation prints possible completions to
STDOUT
, one per line. The Bash function configured to
implement completion for the program calls the program with the
appropriate arguments, reads STDOUT
, and sets the
COMPREPLY
environment variable with the possible
completions.
Consider the following example, performed in a directory that only
contains two directories (one
and two
):
The user types
bm --config <TAB>
. The completion implementation searches the current directory by default, so the argument is rewritten to./
. The only possible completions are the two directories, so the following is written toSTDOUT
and loaded intoCOMPREPLY
by the Bash function:./one/ ./two/
The command line updates to the common prefix:
bm --config ./
. Pressing tab twice runs completion again, getting the same results and displaying them this time.The user types
o
and presses tab. The completion implementation searches the current directory for entries that start witho
. The following is written toSTDOUT
and loaded intoCOMPREPLY
by the Bash function:./one/
The command line updates to
bm --config ./one/
with a space at the end since there is only one option.
This is not the desired behavior. Only YAML files are valid arguments, and the directory is only part of a valid completion. The space should not be appended, so that the user can continue to use the tab key to search for the desired configuration file.
The Fix
This issue can be fixed by telling Bash to not append a space to such
completions. The Haskell program needs to let Bash know when to suppress
the space. To do this, I changed the API so that the first line printed
to STDOUT
is a control directive, where
NOSPACE
indicates that no space should be appended.
The Bash script is now like the following:
_bm() {
mapfile -t COMPREPLY < <(bm --complete ${COMP_CWORD} ${COMP_WORDS[@]})
test "${COMPREPLY[0]}" != "NOSPACE" || compopt -o nospace
unset "COMPREPLY[0]"
}
complete -F _bm bm
In the implementation of --config
argument completion,
the NOSPACE
directive is only output when the list of
possible completions consists of a single YAML file.
Note that the completion implementation is quite inelegant for Haskell code. It takes advantage of program termination in order the simplify the many conditionals. The resulting code is not exemplary functional programming, but the resulting behavior is “functional” in that it handles the many details correctly.
The code described in this blog entry can be browsed in 416caf15 app/Main.hs on GitHub.