Skip to main content

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):

  1. 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 to STDOUT and loaded into COMPREPLY by the Bash function:

    ./one/
    ./two/
  2. 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.

  3. The user types o and presses tab. The completion implementation searches the current directory for entries that start with o. The following is written to STDOUT and loaded into COMPREPLY by the Bash function:

    ./one/
  4. 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.

Author

Travis Cardwell

Published

Tags
Related Blog Entries