1before:
2  hell = require "specl.shell"
3
4specify std.optparse:
5- before: |
6    OptionParser = require "std.optparse"
7
8    help = [[
9    parseme (stdlib spec) 0α1
10
11    Copyright © 2015 Gary V. Vaughan
12    This test program comes with ABSOLUTELY NO WARRANTY.
13
14    Usage: parseme [<options>] <file>...
15
16    Banner text.
17
18    Long description.
19
20    Options:
21
22      -h, --help               display this help, then exit
23          --version            display version information, then exit
24      -b                       a short option with no long option
25          --long               a long option with no short option
26          --another-long       a long option with internal hypen
27          --true               a Lua keyword as an option name
28      -v, --verbose            a combined short and long option
29      -n, --dryrun, --dry-run  several spellings of the same option
30      -u, --name=USER          require an argument
31      -o, --output=[FILE]      accept an optional argument
32      --                       end of options
33
34    Footer text.
35
36    Please report bugs at <http://github.com/lua-stdlib/lua-stdlib/issues>.
37    ]]
38
39    -- strip off the leading whitespace required for YAML
40    parser = OptionParser (help:gsub ("^    ", ""))
41
42- context when required:
43  - context by name:
44    - it does not touch the global table:
45        expect (show_apis {added_to="_G", by="std.optparse"}).
46          to_equal {}
47
48- describe OptionParser:
49  - it recognises the program name:
50      expect (parser.program).to_be "parseme"
51  - it recognises the version number:
52      expect (parser.version).to_be "0α1"
53  - it recognises the version text:
54      expect (parser.versiontext).
55        to_match "^parseme .*Copyright .*NO WARRANTY%."
56  - it recognises the help text: |
57      expect (parser.helptext).
58        to_match ("^Usage: parseme .*Banner .*Long .*Options:.*" ..
59                      "Footer .*/issues>%.")
60  - it diagnoses incorrect input text:
61      expect (OptionParser "garbage in").to_raise "argument must match"
62
63- describe parser:
64  - before: |
65      code = [[
66        package.path = "]] .. package.path .. [["
67        local OptionParser = require 'std.optparse'
68        local help = [=[]] .. help .. [[]=]
69        help = help:match ("^[%s\n]*(.-)[%s\n]*$")
70
71        local parser = OptionParser (help)
72        local arg, opts = parser:parse (_G.arg)
73
74        o = {}
75        for k, v in pairs (opts) do
76          table.insert (o, k .. " = " .. tostring (v))
77        end
78        if #o > 0 then
79          table.sort (o)
80          print ("opts = { " .. table.concat (o, ", ") .. " }")
81        end
82        if #arg > 0 then
83          print ("args = { " .. table.concat (arg, ", ") .. " }")
84        end
85      ]]
86      parse = bind (luaproc, {code})
87
88  - it responds to --version with version text:
89      expect (parse {"--version"}).
90        to_match_output "^%s*parseme .*Copyright .*NO WARRANTY%.\n$"
91  - it responds to --help with help text: |
92      expect (parse {"--help"}).
93        to_match_output ("^%s*Usage: parseme .*Banner.*Long.*" ..
94                              "Options:.*Footer.*/issues>%.\n$")
95  - it leaves behind unrecognised short options:
96      expect (parse {"-x"}).to_output "args = { -x }\n"
97  - it recognises short options:
98      expect (parse {"-b"}).to_output "opts = { b = true }\n"
99  - it leaves behind unrecognised options:
100      expect (parse {"--not-an-option"}).
101        to_output "args = { --not-an-option }\n"
102  - it recognises long options:
103      expect (parse {"--long"}).to_output "opts = { long = true }\n"
104  - it recognises long options with hyphens:
105      expect (parse {"--another-long"}).
106        to_output "opts = { another_long = true }\n"
107  - it recognises long options named after Lua keywords:
108      expect (parse {"--true"}).to_output "opts = { true = true }\n"
109  - it recognises combined short and long option specs:
110      expect (parse {"-v"}).to_output "opts = { verbose = true }\n"
111      expect (parse {"--verbose"}).to_output "opts = { verbose = true }\n"
112  - it recognises options with several spellings:
113      expect (parse {"-n"}).to_output "opts = { dry_run = true }\n"
114      expect (parse {"--dry-run"}).to_output "opts = { dry_run = true }\n"
115      expect (parse {"--dryrun"}).to_output "opts = { dry_run = true }\n"
116  - it recognises end of options marker:
117      expect (parse {"-- -n"}).to_output "args = { -n }\n"
118  - context given an unhandled long option:
119    - it leaves behind unmangled argument:
120        expect (parse {"--not-an-option=with-an-argument"}).
121          to_output "args = { --not-an-option=with-an-argument }\n"
122  - context given an option with a required argument:
123    - it records an argument to a long option following an '=' delimiter:
124        expect (parse {"--name=Gary"}).
125          to_output "opts = { name = Gary }\n"
126    - it records an argument to a short option without a space:
127        expect (parse {"-uGary"}).
128          to_output "opts = { name = Gary }\n"
129    - it records an argument to a long option following a space:
130        expect (parse {"--name Gary"}).
131          to_output "opts = { name = Gary }\n"
132    - it records an argument to a short option following a space:
133        expect (parse {"-u Gary"}).
134          to_output "opts = { name = Gary }\n"
135    - it diagnoses a missing argument:
136        expect (parse {"--name"}).
137          to_contain_error "'--name' requires an argument"
138        expect (parse {"-u"}).
139          to_contain_error "'-u' requires an argument"
140  - context given an option with an optional argument:
141    - it records an argument to a long option following an '=' delimiter:
142        expect (parse {"--output=filename"}).
143          to_output "opts = { output = filename }\n"
144    - it records an argument to a short option without a space:
145        expect (parse {"-ofilename"}).
146          to_output "opts = { output = filename }\n"
147    - it records an argument to a long option following a space:
148        expect (parse {"--output filename"}).
149          to_output "opts = { output = filename }\n"
150    - it records an argument to a short option following a space:
151        expect (parse {"-o filename"}).
152          to_output "opts = { output = filename }\n"
153    - it doesn't consume the following option:
154        expect (parse {"--output -v"}).
155          to_output "opts = { output = true, verbose = true }\n"
156        expect (parse {"-o -v"}).
157          to_output "opts = { output = true, verbose = true }\n"
158  - context when splitting combined short options:
159    - it separates non-argument options:
160        expect (parse {"-bn"}).
161          to_output "opts = { b = true, dry_run = true }\n"
162        expect (parse {"-vbn"}).
163          to_output "opts = { b = true, dry_run = true, verbose = true }\n"
164    - it stops separating at a required argument option:
165        expect (parse {"-vuname"}).
166          to_output "opts = { name = name, verbose = true }\n"
167        expect (parse {"-vuob"}).
168          to_output "opts = { name = ob, verbose = true }\n"
169    - it stops separating at an optional argument option:
170        expect (parse {"-vofilename"}).
171          to_output "opts = { output = filename, verbose = true }\n"
172        expect (parse {"-vobn"}).
173          to_output "opts = { output = bn, verbose = true }\n"
174    - it leaves behind unsplittable short options:
175        expect (parse {"-xvb"}).to_output "args = { -xvb }\n"
176        expect (parse {"-vxb"}).to_output "args = { -vxb }\n"
177        expect (parse {"-vbx"}).to_output "args = { -vbx }\n"
178    - it separates short options before unsplittable options:
179        expect (parse {"-vb -xvb"}).
180          to_output "opts = { b = true, verbose = true }\nargs = { -xvb }\n"
181        expect (parse {"-vb -vxb"}).
182          to_output "opts = { b = true, verbose = true }\nargs = { -vxb }\n"
183        expect (parse {"-vb -vbx"}).
184          to_output "opts = { b = true, verbose = true }\nargs = { -vbx }\n"
185    - it separates short options after unsplittable options:
186        expect (parse {"-xvb -vb"}).
187          to_output "opts = { b = true, verbose = true }\nargs = { -xvb }\n"
188        expect (parse {"-vxb -vb"}).
189          to_output "opts = { b = true, verbose = true }\nargs = { -vxb }\n"
190        expect (parse {"-vbx -vb"}).
191          to_output "opts = { b = true, verbose = true }\nargs = { -vbx }\n"
192
193  - context with option defaults:
194    - before: |
195        function main (arg)
196          local OptionParser = require "std.optparse"
197          local parser = OptionParser ("program 0\nUsage: program\n" ..
198                           "  -x  set x\n" ..
199                           "  -y  set y\n" ..
200                           "  -z  set z\n")
201          local state = { arg = {}, opts = { x={"t"}, y=false }}
202          state.arg, state.opts = parser:parse (arg, state.opts)
203          return state
204        end
205    - it prefers supplied argument:
206        expect (main {"-x", "-y"}).
207          to_equal { arg = {}, opts = { x=true, y=true }}
208        expect (main {"-x", "-y", "-z"}).
209          to_equal { arg = {}, opts = { x=true, y=true, z=true }}
210        expect (main {"-w", "-x", "-y"}).
211          to_equal { arg = {"-w"}, opts = { x=true, y=true }}
212    - it defers to default value:
213        expect (main {}).
214          to_equal { arg = {}, opts = { x={"t"}, y=false }}
215        expect (main {"-z"}).
216          to_equal { arg = {}, opts = { x={"t"}, y=false, z=true }}
217        expect (main {"-w"}).
218          to_equal { arg = {"-w"}, opts = { x={"t"}, y=false }}
219
220  - context with io.die:
221    - before: |
222        runscript = function (code)
223          return luaproc ([[
224            local OptionParser = require "std.optparse"
225            local parser = OptionParser ("program 0\nUsage: program\n")
226            _G.arg, _G.opts = parser:parse (_G.arg)
227            ]] .. code .. [[
228            require "std.io".die "By 'eck!"
229          ]])
230        end
231    - it prefers `prog.name` to `opts.program`: |
232        code = [[prog = { file = "file", name = "name" }]]
233        expect (runscript (code)).to_fail_while_matching ": name: By 'eck!\n"
234    - it prefers `prog.file` to `opts.program`: |
235        code = [[prog = { file = "file" }]]
236        expect (runscript (code)).to_fail_while_matching ": file: By 'eck!\n"
237    - it appends `prog.line` if any to `prog.file` over using `opts`: |
238        code = [[
239          prog = { file = "file", line = 125 }; opts.line = 99]]
240        expect (runscript (code)).
241          to_fail_while_matching ": file:125: By 'eck!\n"
242    - it prefixes `opts.program` if any: |
243        expect (runscript ("")).to_fail_while_matching ": program: By 'eck!\n"
244    - it appends `opts.line` if any, to `opts.program`: |
245        code = [[opts.line = 99]]
246        expect (runscript (code)).
247          to_fail_while_matching ": program:99: By 'eck!\n"
248
249  - context with io.warn:
250    - before: |
251        runscript = function (code)
252          return luaproc ([[
253            local OptionParser = require "std.optparse"
254            local parser = OptionParser ("program 0\nUsage: program\n")
255            _G.arg, _G.opts = parser:parse (_G.arg)
256            ]] .. code .. [[
257            require "std.io".warn "Ayup!"
258          ]])
259        end
260    - it prefers `prog.name` to `opts.program`: |
261        code = [[prog = { file = "file", name = "name" }]]
262        expect (runscript (code)).to_output_error "name: Ayup!\n"
263    - it prefers `prog.file` to `opts.program`: |
264        code = [[prog = { file = "file" }]]
265        expect (runscript (code)).to_output_error "file: Ayup!\n"
266    - it appends `prog.line` if any to `prog.file` over using `opts`: |
267        code = [[
268          prog = { file = "file", line = 125 }; opts.line = 99]]
269        expect (runscript (code)).
270          to_output_error "file:125: Ayup!\n"
271    - it prefixes `opts.program` if any: |
272        expect (runscript ("")).to_output_error "program: Ayup!\n"
273    - it appends `opts.line` if any, to `opts.program`: |
274        code = [[opts.line = 99]]
275        expect (runscript (code)).
276          to_output_error "program:99: Ayup!\n"
277
278- describe parser:on:
279  - before: |
280      function parseargs (onargstr, arglist)
281        code = [[
282          package.path = "]] .. package.path .. [["
283          local OptionParser = require 'std.optparse'
284          local help = [=[]] .. help .. [[]=]
285          help = help:match ("^[%s\n]*(.-)[%s\n]*$")
286
287          local parser = OptionParser (help)
288
289          parser:on (]] .. onargstr .. [[)
290
291          _G.arg, _G.opts = parser:parse (_G.arg)
292
293          o = {}
294          for k, v in pairs (opts) do
295            table.insert (o, k .. " = " .. tostring (v))
296          end
297          if #o > 0 then
298            table.sort (o)
299            print ("opts = { " .. table.concat (o, ", ") .. " }")
300          end
301          if #arg > 0 then
302            print ("args = { " .. table.concat (arg, ", ") .. " }")
303          end
304        ]]
305
306        return luaproc (code, arglist)
307      end
308
309  - it recognises short options:
310      expect (parseargs ([["x"]], {"-x"})).
311        to_output "opts = { x = true }\n"
312  - it recognises long options:
313      expect (parseargs([["something"]], {"--something"})).
314        to_output "opts = { something = true }\n"
315  - it recognises long options with hyphens:
316      expect (parseargs([["some-thing"]], {"--some-thing"})).
317        to_output "opts = { some_thing = true }\n"
318  - it recognises long options named after Lua keywords:
319      expect (parseargs ([["if"]], {"--if"})).
320        to_output "opts = { if = true }\n"
321  - it recognises combined short and long option specs:
322      expect (parseargs ([[{"x", "if"}]], {"-x"})).
323        to_output "opts = { if = true }\n"
324      expect (parseargs ([[{"x", "if"}]], {"--if"})).
325        to_output "opts = { if = true }\n"
326  - it recognises options with several spellings:
327      expect (parseargs ([[{"x", "blah", "if"}]], {"-x"})).
328        to_output "opts = { if = true }\n"
329      expect (parseargs ([[{"x", "blah", "if"}]], {"--blah"})).
330        to_output "opts = { if = true }\n"
331      expect (parseargs ([[{"x", "blah", "if"}]], {"--if"})).
332        to_output "opts = { if = true }\n"
333  - it recognises end of options marker:
334      expect (parseargs ([["x"]], {"--", "-x"})).
335        to_output "args = { -x }\n"
336  - context given an option with a required argument:
337    - it records an argument to a short option without a space:
338        expect (parseargs ([["x", parser.required]], {"-y", "-xarg", "-b"})).
339          to_contain_output "opts = { b = true, x = arg }"
340    - it records an argument to a short option following a space:
341        expect (parseargs ([["x", parser.required]], {"-y", "-x", "arg", "-b"})).
342          to_contain_output "opts = { b = true, x = arg }\n"
343    - it records an argument to a long option following a space:
344        expect (parseargs ([["this", parser.required]], {"--this", "arg"})).
345          to_output "opts = { this = arg }\n"
346    - it records an argument to a long option following an '=' delimiter:
347        expect (parseargs ([["this", parser.required]], {"--this=arg"})).
348          to_output "opts = { this = arg }\n"
349    - it diagnoses a missing argument:
350        expect (parseargs ([[{"x", "this"}, parser.required]], {"-x"})).
351          to_contain_error "'-x' requires an argument"
352        expect (parseargs ([[{"x", "this"}, parser.required]], {"--this"})).
353          to_contain_error "'--this' requires an argument"
354    - context with a boolean handler function:
355      - it records a truthy argument:
356          for _, optarg in ipairs {"1", "TRUE", "true", "yes", "Yes", "y"}
357          do
358            expect (parseargs ([["x", parser.required, parser.boolean]],
359                               {"-x", optarg})).
360              to_output "opts = { x = true }\n"
361          end
362      - it records a falsey argument:
363          for _, optarg in ipairs {"0", "FALSE", "false", "no", "No", "n"}
364          do
365            expect (parseargs ([["x", parser.required, parser.boolean]],
366                               {"-x", optarg})).
367              to_output "opts = { x = false }\n"
368          end
369    - context with a file handler function:
370      - it records an existing file:
371          expect (parseargs ([["x", parser.required, parser.file]],
372                             {"-x", "/dev/null"})).
373            to_output "opts = { x = /dev/null }\n"
374      - it diagnoses a missing file: |
375          expect (parseargs ([["x", parser.required, parser.file]],
376                             {"-x", "/this/file/does/not/exist"})).
377            to_contain_error "error: /this/file/does/not/exist: "
378    - context with a custom handler function:
379      - it calls the handler:
380          expect (parseargs ([["x", parser.required, function (p,o,a)
381                                                       return "custom"
382                                                     end
383                             ]], {"-x", "ignored"})).
384            to_output "opts = { x = custom }\n"
385      - it diagnoses a missing argument:
386          expect (parseargs ([["x", parser.required, function (p,o,a)
387                                                       return "custom"
388                                                     end
389                             ]], {"-x"})).
390            to_contain_error "option '-x' requires an argument"
391  - context given an option with an optional argument:
392    - it records an argument to a short option without a space:
393        expect (parseargs ([["x", parser.optional]], {"-y", "-xarg", "-b"})).
394          to_contain_output "opts = { b = true, x = arg }"
395    - it records an argument to a short option following a space:
396        expect (parseargs ([["x", parser.optional]], {"-y", "-x", "arg", "-b"})).
397          to_contain_output "opts = { b = true, x = arg }\n"
398    - it records an argument to a long option following a space:
399        expect (parseargs ([["this", parser.optional]], {"--this", "arg"})).
400          to_output "opts = { this = arg }\n"
401    - it records an argument to a long option following an '=' delimiter:
402        expect (parseargs ([["this", parser.optional]], {"--this=arg"})).
403          to_output "opts = { this = arg }\n"
404    - it does't consume the following option:
405        expect (parseargs ([[{"x", "this"}, parser.optional]], {"-x", "-b"})).
406          to_output "opts = { b = true, this = true }\n"
407          expect (parseargs ([[{"x", "this"}, parser.optional]], {"--this", "-b"})).
408          to_output "opts = { b = true, this = true }\n"
409    - context with a boolean handler function:
410      - it records a truthy argument:
411          for _, optarg in ipairs {"1", "TRUE", "true", "yes", "Yes", "y"}
412          do
413            expect (parseargs ([["x", parser.optional, parser.boolean]],
414                               {"-x", optarg})).
415              to_output "opts = { x = true }\n"
416          end
417      - it records a falsey argument:
418          for _, optarg in ipairs {"0", "FALSE", "false", "no", "No", "n"}
419          do
420            expect (parseargs ([["x", parser.optional, parser.boolean]],
421                               {"-x", optarg})).
422              to_output "opts = { x = false }\n"
423          end
424      - it defaults to a truthy value:
425          expect (parseargs ([["x", parser.optional, parser.boolean]],
426                             {"-x", "-b"})).
427            to_output "opts = { b = true, x = true }\n"
428    - context with a file handler function:
429      - it records an existing file:
430          expect (parseargs ([["x", parser.optional, parser.file]],
431                             {"-x", "/dev/null"})).
432            to_output "opts = { x = /dev/null }\n"
433      - it diagnoses a missing file: |
434          expect (parseargs ([["x", parser.optional, parser.file]],
435                             {"-x", "/this/file/does/not/exist"})).
436            to_contain_error "error: /this/file/does/not/exist: "
437    - context with a custom handler function:
438      - it calls the handler:
439          expect (parseargs ([["x", parser.optional, function (p,o,a)
440                                                       return "custom"
441                                                     end
442                             ]], {"-x", "ignored"})).
443            to_output "opts = { x = custom }\n"
444      - it does not consume a following option:
445          expect (parseargs ([["x", parser.optional, function (p,o,a)
446                                                       return a or "default"
447                                                     end
448                             ]], {"-x", "-b"})).
449            to_output "opts = { b = true, x = default }\n"
450  - context when splitting combined short options:
451    - it separates non-argument options:
452        expect (parseargs ([["x"]], {"-xb"})).
453          to_output "opts = { b = true, x = true }\n"
454        expect (parseargs ([["x"]], {"-vxb"})).
455          to_output "opts = { b = true, verbose = true, x = true }\n"
456    - it stops separating at a required argument option:
457        expect (parseargs ([[{"x", "this"}, parser.required]], {"-bxbit"})).
458          to_output "opts = { b = true, this = bit }\n"
459    - it stops separating at an optional argument option:
460        expect (parseargs ([[{"x", "this"}, parser.optional]], {"-bxbit"})).
461          to_output "opts = { b = true, this = bit }\n"
462