File: README.md

package info (click to toggle)
haskell-butcher 1.3.3.2-1
  • links: PTS
  • area: main
  • in suites: bookworm, bullseye, sid
  • size: 260 kB
  • sloc: haskell: 2,844; pascal: 358; makefile: 6
file content (241 lines) | stat: -rwxr-xr-x 8,111 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# 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

~~~~.hs
main = mainFromCmdParser $ addCmdImpl $ putStrLn "Hello, World!"
~~~~

But lets look at a more feature-complete example:

~~~~.hs
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](example1.md)
- [Example of a pure usage of a CmdParser](example2.md)
- [Example of using a CmdParser on interactive input](example3.md)
- The [brittany](https://github.com/lspitzner/brittany) formatting tool is a
  program that uses butcher for implementing its commandline interface. See
  its [main module source](https://github.com/lspitzner/brittany/blob/master/src-brittany/Main.hs)
  or [the config flag parser](https://github.com/lspitzner/brittany/blob/master/src/Language/Haskell/Brittany/Config.hs).

## 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

- ~~~~ .hs
  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\"."

- ~~~~ .hs
  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:
  ~~~~ .hs
  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:
  ~~~~ .hs
  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:<br>could not parse expected input -j number with remaining input:<br>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:
  ~~~~ .hs
  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