butcher
Chops a command or program invocation into digestable pieces.
https://github.com/lspitzner/butcher/
LTS Haskell 20.26: | 1.3.3.2@rev:1 |
Stackage Nightly 2022-11-17: | 1.3.3.2@rev:1 |
Latest on Hackage: | 1.3.3.2@rev:1 |
butcher-1.3.3.2@sha256:9513d3bba9c58a43199c43d856dd3901d374117406fb438786dddef6ad8639b1,3301
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:
- Full description of the above example, including sample behaviour
- Example of a pure usage of a CmdParser
- Example of using a CmdParser on interactive input
- The brittany formatting tool is a program that uses butcher for implementing its commandline interface. See its main module source or the config flag parser.
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:
- Straight-forward description of (sub)command and flag-specific behaviour
- Extract understandable usage/help commandline documents/texts from that
descriptions, think of
ghc --help
orstack init --help
. - 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
Changes
Revision history for butcher
1.3.3.2 – June 2020
- Support ghc-8.10
- Drop support for ghc < 8.4
- Fix a somewhat hidden issue in the cabal file
- Add support for building / testing via haskell.nix nixpkgs overlay
1.3.3.1 – April 2020
- Fix a cabal file mistake
1.3.3.0 – April 2020
- Fix bug with params with default when parsing from commandline
- Add the
descendDescTo
function
1.3.2.3 – June 2019
- Fix broken build when using deque>=0.3
1.3.2.2 – June 2019 (broken, disabled on hackage)
- Fix too-shallow descriptions on siblings for partial parses returned for interactive usage
1.3.2.1 – October 2018
- Adapt/Use latest version of
deque
to fix ghc-8.6 problems
1.3.2.0 – October 2018
- Fix for simpleCompletion
- Expose some bindings that were forgotten in previous release
- Bounds fixed for ghc-8.6 (also via revision in 1.3.1.1)
1.3.1.1 – April 2018
- Fixup version bound
1.3.1.0 – April 2018
- Add/Expose two more functions: addAlternatives and varPartDesc
1.3.0.1 – April 2018
- Support ghc-8.4
- Drop support for ghc<8
1.3.0.0 – February 2018
- Experimental: Hidden commandparts (do not appear in help)
- Experimental: Bash completion
- Add addHelpCommandWith to support user-defined column count
- Fix help document printing (ribbons)
- Fix completion behaviour
1.2.1.0 – November 2017
- Fix bug in ‘ppUsageWithHelp’
- some utilities for interactive usage in new module
UI.Butcher.Monadic.Interactive
1.2.0.0 – October 2017
- Rename some
Monadic.Param.*
, deprecate old versions.addReadParam
->addParamRead
addReadParamOpt
->addParamReadOpt
addStringParam
->addParamString
addStringParamOpt
->addParamStringOpt
addStringParams
->addParamStrings
addRestOfInputStringParam
->addParamRestOfInput
- Add functions
addParamNoFlagString
,addParamNoFlagStringOpt
,addParamNoFlagStrings
- Fix flag parsing behaviour (ignore initial spaces)
1.1.1.0 – October 2017
- Add
addNullCmd
function that descends into childcommand on an epsilon match - Add
addStringParams
function that reads all remaining words
1.1.0.2 – September 2017
- Improve ‘usage’ pretty-printing
1.1.0.1 – August 2017
- Adapt for ghc-8.2
1.1.0.0 – May 2017
- First version. Released on an unsuspecting world.