haskell library to parse commandline args - an alternative to optparse-applicative
 
 
 
Go to file
Lennart Spitzner 5bde33baf7 Update/Correct changelog 2023-04-28 11:41:53 +02:00
examples Add example for butcher+barbies usage 2022-12-14 15:02:47 +01:00
src/UI/Butcher Expose types involved in the API of the module 2023-04-28 11:26:38 +02:00
src-tests Clean up haddocks 2022-12-14 15:02:47 +01:00
srcinc Stop using Semigroups.Option 2022-12-14 17:53:45 +01:00
.gitignore Switch to new seaaye setup 2022-12-14 15:02:47 +01:00
ChangeLog.md Update/Correct changelog 2023-04-28 11:41:53 +02:00
LICENSE initial commit 2016-06-08 12:42:22 +02:00
README.md Bump to 2.0.0.0, Append changelog, Update readme 2022-12-14 15:02:47 +01:00
Setup.hs initial commit 2016-06-08 12:42:22 +02:00
butcher.cabal Support ghc-9.2 2022-12-14 17:54:00 +01:00
default.nix Fix/Upgrade/Improve nix expressions 2020-08-23 17:11:38 +02:00
iridium.yaml Bump bound for ghc-8.8 2019-09-29 19:25:06 +02:00
seaaye.nix Support ghc-9.2 2022-12-14 17:54:00 +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
stack-9-0.yaml Bump bounds for ghc-9.0 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 two interfaces: One based on Applicative and one based on Monad. The monadic one is slightly more expressive, the applicative interface is conceptually cleaner but currently is less tested.

  • The monadic interface must be used as if ApplicativeDo was enabled, but does not actually require ApplicativeDo. This is implemented via some evil hackery, but nonetheless useful.

  • It is not necessary to define data-structure for diffenent child-commands. In general this is geared towards keeping names and definitions/parsers of flags/parameters/child-commands connected, while the default MyFlags <$> someParser <*> … <*> … <*> … <*> … <*> … is harder to read and prone to accidental swapping.

  • Supports connecting to "barbies" (see the barbies package). This allows re-using data-structure definitions for the parser and config values without losing track of field order.

Examples

The minimal example is

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

But lets look at a more feature-complete example:

main = mainFromCmdParser $ do

  helpDesc <- peekCmdDesc

  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 "1.0"
      else "example, version 1.0"

  addCmd "help" $ addCmdImpl $ print $ ppHelpShallow helpDesc

  short <- addSimpleBoolFlag "" ["short"]
    (flagHelpStr "make the greeting short")
  name <- addParamString "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