haskell library to parse commandline args - an alternative to optparse-applicative
 
 
 
Go to file
Lennart Spitzner 5d67167c87 Major changeset: Add applicative, Refactor monadic interface
- monadic interface now uses two-phase setup: First step is to create a
  full CommandDesc value, second is running the parser on input while the
  CommandDesc is chained along
- applicative interface has a somewhat nicer/cleaner implementation, is
  more secure by avoiding any demands on the API user that are not encoded
  in types, but is slightly less expressive and requires ApplicativeDo to
  get readable code.
- The applicative interface is *NOT* finished and the test-suite does not
  cover it.
- Add the `traverseBarbie` construct which introduces a dependency on
  the `barbies` library. This also effectively:
- Stop support for ghc < 8.4.
- Refactor the module structure a bit, and change the API of the central
  `runCmdParser` function. It now returns a `PartialParseInfo`. Essentially,
  `runCmdParser` is a combination of the previous `runCmdParser` and the
  previous `simpleCompletion`. This API design is a curious advantage to
  laziness: Returning a complex struct is harmless as fields that the user
  does not use won't be evaluated. The downside is that the core function now
  looks like a complex beast, but the upside is that there is no need to
  expose multiple functions that are supposed to be chained in a certain way
  to get all functionality (if desired), and we still _can_ provide simpler
  versions that are just projections on the `PartialParseInfo`.
- Stop support for an anti-feature: The implicit merging of multiple
  sub-commands definitions with the same name.
2022-12-14 15:02:47 +01:00
src/UI/Butcher Major changeset: Add applicative, Refactor monadic interface 2022-12-14 15:02:47 +01:00
src-tests Major changeset: Add applicative, Refactor monadic interface 2022-12-14 15:02:47 +01:00
srcinc Major changeset: Add applicative, Refactor monadic interface 2022-12-14 15:02:47 +01:00
.gitignore Switch to new seaaye setup 2022-12-14 15:02:47 +01:00
ChangeLog.md Drop support for ghc<8.4 2020-06-10 21:59:50 +02:00
LICENSE initial commit 2016-06-08 12:42:22 +02:00
README.md Update README.md: Add examples for cmds and flags 2017-05-16 23:47:12 +02:00
Setup.hs initial commit 2016-06-08 12:42:22 +02:00
butcher.cabal Major changeset: Add applicative, Refactor monadic interface 2022-12-14 15:02:47 +01:00
default.nix Fix/Upgrade/Improve nix expressions 2020-08-23 17:11:38 +02:00
example1.md Refactor module structure; Add haddock; Update README 2017-01-01 14:46:53 +01:00
example2.md Refactor module structure; Add haddock; Update README 2017-01-01 14:46:53 +01:00
example3.md Refactor module structure; Add haddock; Update README 2017-01-01 14:46:53 +01:00
iridium.yaml Bump bound for ghc-8.8 2019-09-29 19:25:06 +02:00
seaaye.nix Switch to new seaaye setup 2022-12-14 15:02:47 +01:00
shell.nix Fix/Upgrade/Improve nix expressions 2020-08-23 17:11:38 +02:00
stack-8-4.yaml Major changeset: Add applicative, Refactor monadic interface 2022-12-14 15:02:47 +01:00
stack-8-6.yaml Major changeset: Add applicative, Refactor monadic interface 2022-12-14 15:02:47 +01:00
stack-8-8.yaml Switch to new seaaye setup 2022-12-14 15:02:47 +01:00
stack-8-8_.yaml Switch to new seaaye setup 2022-12-14 15:02:47 +01:00
stack-8-10.yaml Major changeset: Add applicative, Refactor monadic interface 2022-12-14 15:02:47 +01:00

README.md

butcher

Chops a command or program invocation into digestable pieces.

Similar to the optparse-applicative package, but less features, more flexibility and more evil.

The main differences are:

  • Provides a pure interface by default

  • Exposes an evil monadic interface, which allows for much nicer binding of command part results to some variable name.

    In optparse-applicative you easily lose track of what field you are modifying after the 5th <*> (admittedly, i think -XRecordWildCards improves on that issue already.)

    Evil, because you are not allowed to use the monad's full power in this case, i.e. there is a constraint that is not statically enforced. See below.

  • The monadic interface allows much clearer definitions of commandparses with (nested) subcommands. No pesky sum-types are necessary.

Examples

The minimal example is

main = mainFromCmdParser $ addCmdImpl $ putStrLn "Hello, World!"

But lets look at a more feature-complete example:

main = mainFromCmdParserWithHelpDesc $ \helpDesc -> do

  addCmdSynopsis "a simple butcher example program"
  addCmdHelpStr "a very long help document"

  addCmd "version" $ do
    porcelain <- addSimpleBoolFlag "" ["porcelain"]
      (flagHelpStr "print nothing but the numeric version")
    addCmdHelpStr "prints the version of this program"
    addCmdImpl $ putStrLn $ if porcelain
      then "0.0.0.999"
      else "example, version 0.0.0.999"

  addCmd "help" $ addCmdImpl $ print $ ppHelpShallow helpDesc

  short <- addSimpleBoolFlag "" ["short"]
    (flagHelpStr "make the greeting short")
  name <- addStringParam "NAME"
    (paramHelpStr "your name, so you can be greeted properly")

  addCmdImpl $ do
    if short
      then putStrLn $ "hi, " ++ name ++ "!"
      else putStrLn $ "hello, " ++ name ++ ", welcome from butcher!"

Further:

The evil monadic interface

As long as you only use Applicative or (Kleisli) Arrow, you can use the interface freely. When you use Monad, there is one rule: Whenever you read any command-parts like in

f <- addFlag ...
p <- addParam ...

you are only allowed to use bindings bound thusly in any command's implemenation, i.e. inside the parameter to addCmdImpl. You are not allowed to force/inspect/patternmatch on them before that. good usage is:

addCmdImpl $ do
  print x
  print y

while bad would be

f <- addFlag
when f $ do
  p <- addParam
  -- evil: the existence of the param `p`
  -- depends on parse result for the flag `f`.

That means that checking if a combination of flags is allowed must be done after parsing. (But different commands and their subcommands (can) have separate sets of flags.)

(abstract) Package intentions

Consider a commandline invocation like "ghc -O -i src -Main.hs -o Main". This package provides a way for the programmer to simultaneously define the semantics of your program based on its arguments and retrieve documentation for the user. More specifically, i had three goals in mind:

  1. Straight-forward description of (sub)command and flag-specific behaviour
  2. Extract understandable usage/help commandline documents/texts from that descriptions, think of ghc --help or stack init --help.
  3. Extract necessary information to compute commandline completion results from any partial input. (This is not implemented to any serious degree.)

Semantics

Basic elements of a command are flags, parameters and subcommands. These can be composed in certain ways, i.e. flags can have a (or possibly multiple?) parameters; parameters can be grouped into sequences, and commands can have subcommands.

Commands are essentially String -> Either ParseError out where out can be chosen by the user. It could for example be IO ().

To allow more flexible composition, the parts of a command have the "classic" parser's type: String -> Maybe (p, String) where p depends on the part. Parse a prefix of the input and return something and the remaining input, or fail with Nothing.

A command-parser contains a sequence of parts and then a number of subcommands and/or some implementation.

Commands and Child-Commands

  • myParser :: CmdParser Identity Int ()
    myParser = return ()
    
    input runCmdParserSimple input myParser
    "" Left "command has no implementation"
    "x" Left "error parsing arguments: could not parse input/unprocessed input at: "x"."
  • myParser :: CmdParser Identity Int ()
    myParser = do
      addCmd "foo" $ addCmdImpl 2
      addCmd "bar" $ addCmdImpl 3
      addCmd "noimpl" $ pure ()
      addCmd "twoimpls" $ do
        addCmdImpl 4
        addCmdImpl 5
      addCmdImpl 1
    
    input runCmdParserSimple input myParser
    "" Right 1
    "x" Left "error parsing arguments: could not parse input/unprocessed input at: "x"."
    "foo" Right 2
    "bar" Right 3
    "foo bar" Left "error parsing arguments: could not parse input/unprocessed input at: "bar"."
    "noimpl" Left "command has no implementation"
    "twoimpls" Right 5

Flags

  • without any annotation, no reodering is allowed and the flags must appear in order:

    myParser :: CmdParser Identity (Bool, Int, Int) ()
    myParser = do
      b <- addSimpleBoolFlag "b" [] mempty
      c <- addSimpleCountFlag "c" [] mempty
      i <- addFlagReadParam "i" [] "number" (flagDefault 42)
      addCmdImpl $ (b, c, i)
    
    input runCmdParserSimple input myParser
    "" Right (False,0,42)
    "-b -c -i 3" Right (True,1,3)
    "-c -b" Left "error parsing arguments: could not parse input/unprocessed input at: "-b"."
    "-c -c -c" Right (False,3,42)
  • this time with reordering; also "j" has no default and thus becomes mandatory, still it must not occur more than once:

    myParser :: CmdParser Identity (Bool, Int, Int, Int) ()
    myParser = do
      reorderStart -- this time with reordering
      b <- addSimpleBoolFlag "b" [] mempty
      c <- addSimpleCountFlag "c" [] mempty
      i <- addFlagReadParam "i" [] "number" (flagDefault 42)
      j <- addFlagReadParam "j" [] "number" mempty -- no default: flag mandatory
      reorderStop
      addCmdImpl $ (b, c, i, j)
    
    input runCmdParserSimple input myParser
    "-b" Left "error parsing arguments:
    could not parse expected input -j number with remaining input:
    InputString "" at the end of input."
    "-j=5" Right (False,0,42,5)
    "-c -b -b -j=5" Right (True,1,42,5)
    "-j=5 -i=1 -c -b" Right (True,1,1,5)
    "-c -j=5 -c -i=5 -c" Right (False,3,5,5)
    "-j=5 -j=5" Left "error parsing arguments: could not parse input/unprocessed input at: "-j=5"."
  • addFlagReadParams - these can occur more than once. Note that defaults have slightly different semantics:

    myParser :: CmdParser Identity (Int, [Int]) ()
    myParser = do
      reorderStart
      i <- addFlagReadParam "i" [] "number" (flagDefault 42)
      js <- addFlagReadParams "j" [] "number" (flagDefault 50)
      reorderStop
      addCmdImpl $ (i, js)
    
    input runCmdParserSimple input myParser
    "" Right (42,[])
    "-i" Left "error parsing arguments: could not parse input/unprocessed input at: "-i"."
    "-j=1 -j=2 -j=3" Right (42,[1,2,3])
    "-j" Right (42,[50])
    "-i=1" Right (1,[])
    "-j=2" Right (42,[2])
    "-j=2 -i=1 -j=3" Right (1,[2,3])

Params

TODO