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.
- Typing
- The
--configargument 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.
- Typing
- Options and arguments can be interleaved.
- For example, typing
bm nix --trace m<TAB>completes themtomanualusing my configuration.
- For example, typing
- If a configuration file is specified using a
--configoption, that configuration file is used for completion of arguments.