From 3894a65d51cea722eb16908258fbc11a10aa63b7 Mon Sep 17 00:00:00 2001 From: Viacheslav Lotsmanov Date: Wed, 2 Feb 2022 03:41:39 +0200 Subject: [PATCH] Refactor arguments parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use “optparse-applicative” library. The parser is also the spec. The same code describes the arguments and parses them. Usage info is generated automatically from the parser. `--help` and `-h` are also handled automatically as a generic thing. No need to describe them separately. Initially I’ve tried to call `taskell --help` and got this in response: > Create /home/wenzel/dev/haskell/taskell/--help? (Y/n): Which confused me. I went to look at the code to see how the arguments parsing can be improved. Usually you parse some pure data type out from command-line arguments. And then you decide what to do with that pure data. But I parsed monadic actions directly in order to keep this implementation close to the previous one. An example of the auto-generated usage info: ``` console $ result/bin/taskell --help Taskell - A CLI kanban board/task manager Usage: taskell [file | (-v|--version) | (-t|--trello ) file | (-g|--github [orgs/ | repos//]) file | [-i|--info] file] Available options: -h,--help Show this help text -v,--version Show version number -t,--trello Create a new taskell file from the given Trello board ID -g,--github [orgs/ | repos//] Create a new taskell file from the given GitHub identifier -i,--info Display information about a file ``` --- package.yaml | 1 + src/Taskell/Config.hs | 3 -- src/Taskell/IO.hs | 66 +++++++++++++++++++++++++++++++++++-------- templates/usage.txt | 9 ------ 4 files changed, 56 insertions(+), 23 deletions(-) delete mode 100644 templates/usage.txt diff --git a/package.yaml b/package.yaml index 56bceaa0..b249498c 100644 --- a/package.yaml +++ b/package.yaml @@ -75,6 +75,7 @@ library: - http-types - lens - mtl + - optparse-applicative - template-haskell - text - time diff --git a/src/Taskell/Config.hs b/src/Taskell/Config.hs index d4f811f6..1938dcfe 100644 --- a/src/Taskell/Config.hs +++ b/src/Taskell/Config.hs @@ -17,6 +17,3 @@ trelloUsage = decodeUtf8 $(embedFile "templates/trello-token.txt") githubUsage :: Text githubUsage = decodeUtf8 $(embedFile "templates/github-token.txt") - -usage :: Text -usage = decodeUtf8 $(embedFile "templates/usage.txt") diff --git a/src/Taskell/IO.hs b/src/Taskell/IO.hs index 62a2d66b..f6cb95db 100644 --- a/src/Taskell/IO.hs +++ b/src/Taskell/IO.hs @@ -9,7 +9,11 @@ import Data.Either (fromRight) import Data.Time.Zones (TZ) -import Taskell.Config (githubUsage, trelloUsage, usage, version) +import Options.Applicative (Parser, ParserInfo, ParserPrefs (..), + switch, strOption, strArgument, long, short, metavar, help, + info, helper, headerDoc, customExecParser, defaultPrefs) + +import Taskell.Config (githubUsage, trelloUsage, version) import Taskell.Data.Lists (Lists, analyse, initial) import Taskell.IO.Config (Config, general, getDir, github, markdown, templatePath, @@ -48,18 +52,58 @@ getPath path = do isDir <- lift $ doesDirectoryExist canonicial pure $ if isDir then canonicial defaultFilename else canonicial -parseArgs :: [Text] -> ReaderConfig Next -parseArgs ["-v"] = pure $ Output version -parseArgs ["-h"] = pure $ Output usage -parseArgs ["-t", boardID, file] = getPath file >>= loadTrello boardID -parseArgs ["-g", identifier, file] = getPath file >>= loadGitHub identifier -parseArgs ["-i", file] = getPath file >>= fileInfo -parseArgs [file] = getPath file >>= loadFile -parseArgs [] = getPath "" >>= loadFile -parseArgs _ = pure $ Error (unlines ["Invalid options", "", usage]) +-- | Parse command-line arguments to an action +commandLineArgsParser :: Parser (ReaderConfig Next) +commandLineArgsParser = foldl' (<|>) noArgs + [ openBoardFile <$> fileParser + + , pure (Output version) <$ + switch (short 'v' <> long "version" <> help "Show version number") + + , (\boardID file -> getPath file >>= loadTrello boardID) + <$> strOption + ( short 't' + <> long "trello" + <> metavar "" + <> help "Create a new taskell file from the given Trello board ID" + ) + <*> fileParser + + , (\identifier file -> getPath file >>= loadGitHub identifier) + <$> strOption + ( short 'g' + <> long "github" + <> metavar "[orgs/ | repos//]" + <> help "Create a new taskell file from the given GitHub identifier" + ) + <*> fileParser + + , fmap (getPath >=> fileInfo) $ + switch + ( short 'i' + <> long "info" + <> help "Display information about a file" + ) + *> fileParser + ] + where + fileParser = strArgument $ metavar "file" + openBoardFile = getPath >=> loadFile + + -- | Open default board file + noArgs = pure $ openBoardFile mempty + +commandLineArgsParserInfo :: ParserInfo (ReaderConfig Next) +commandLineArgsParserInfo = + info (helper <*> commandLineArgsParser) $ + headerDoc (Just "Taskell - A CLI kanban board/task manager") load :: ReaderConfig Next -load = getArgs >>= parseArgs +load = + join . liftIO $ + customExecParser + defaultPrefs { prefShowHelpOnError = True } + commandLineArgsParserInfo colonic :: FilePath -> Text -> Text colonic path = ((pack path <> ": ") <>) diff --git a/templates/usage.txt b/templates/usage.txt deleted file mode 100644 index adcd399b..00000000 --- a/templates/usage.txt +++ /dev/null @@ -1,9 +0,0 @@ -Usage: taskell ([options] | [-i | -t | -g [orgs/ | repos//]] file) - -Options: - --h Help --v Version number --i file Display information about a file --t file Create a new taskell file from the given Trello board ID --g [orgs/ | repos//] file Create a new taskell file from the given GitHub identifier