Skip to main content

bm CLI Completion

One of the features that I would like to include in bm is completion of command-line options and arguments. This feature allows users to search for a bookmark by tabbing through the keyword hierarchy.

I am using the popular optparse-applicative library to implement option parsing. The library has built-in completion support, but I discovered that it is quite limited. The API makes it easy to provide completion for single options and arguments, but it does not work for the bm interface where a sequence of arguments determines what arguments may follow. The library does not provide a way to override the default implementation, unfortunately.

When I worked on it yesterday, I figured that I would need to rewrite the option parser for the application so that I could cleanly add completion support. I have some other tasks that I want to work on, so I planned on adding completion support in the future, after the first release. My brain would not stop thinking about how to proceed, however, so I ended up implementing it today so that I can clear my mind of the problem.

I decided to continue to use optparse-applicative and just handle completion arguments specially. I implemented a parseArgs function to parse the arguments:

main = do
    Options{..} <- parseArgs
    ...

The parseArgs function passes the command-line arguments to a handleCompletion function that handles completion arguments. When completion arguments are processed, the handleCompletion function does not return. Otherwise, it returns an error or the list of arguments for optparse-applicative to parse, which parseArgs handles.

parseArgs :: IO Options
parseArgs = do
    eeas <- handleCompletion =<< getArgs
    OA.handleParseResult $ case eeas of
      Right args -> OA.execParserPure OA.defaultPrefs pinfo args
      Left parseError ->
        OA.Failure $ OA.parserFailure OA.defaultPrefs pinfo parseError mempty
  where
    pinfo :: OA.ParserInfo Options
    pinfo = ...

The handleCompletion function checks for completion options that are built into optparse-applicative and turns them into errors. My custom implementation uses different options. When one of these options is seen, the corresponding implementation function is called, which does not return. Otherwise, the arguments are returned for processing by optparse-applicative like usual.

handleCompletion :: [String] -> IO (Either OA.ParseError [String])
handleCompletion args = case find isOAOption optArgs of
    Just arg ->
      return . Left $ OA.UnexpectedError arg (OAT.SomeParser options)
    Nothing
      | "--complete"      `elem` optArgs -> handleComplete args
      | "--complete-bash" `elem` optArgs -> handleCompleteBash args
      | otherwise                        -> return $ Right args
  where
    optArgs :: [String]
    optArgs = takeWhile (/= "--") args

    isOAOption :: String -> Bool
    isOAOption =
      (&&) <$> ("--" `isPrefixOf`) <*> ("completion" `isInfixOf`)

I only implemented Bash completion for the first release, though it should be easy to add completion for other shells in the future. The --complete-bash option prints Bash code to enable completion like optparse-applicative does. The --complete option preforms completion and should work for all shells. The usage information for both options is handled specially, since these options are not configured in the optparse-applicative Options.

I have tested the following behavior:

  • Completion is supported for all options.
    • Typing bm --<TAB> gives a list of options.
    • Partially input long options are completed.
    • Short options are handled correctly.
  • The --config argument is completed, showing directories and YAML files.
    • Since most completion is not of filenames, the filename option is turned off. Unfortunately, this results in the appending of a space when a directory name is selected. I will search for a solution to this issue.
  • The -- argument is correctly handled.
    • Typing bm -- --<TAB> does not provide option completion.
  • Options and arguments can be interleaved.
    • For example, typing bm nix --trace m<TAB> completes the m to manual using my configuration.
  • If a configuration file is specified using a --config option, that configuration file is used for completion of arguments.
Author

Travis Cardwell

Published

Tags
Related Blog Entries