Skip to content

Commit 7638347

Browse files
committed
Unbound splat arguments, fixes GH-54
Unfortunately, this is a breaking change and requires a major release. The patch makes it so that we no longer reject more than 999 repetitions for a splat argument, and that by default we allow for unlimited arguments (maxcount=0 or nil) instead of 1.
1 parent 076fc27 commit 7638347

File tree

9 files changed

+163
-35
lines changed

9 files changed

+163
-35
lines changed

README.md

+28
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,34 @@ Many thanks to everyone who reported bugs, provided fixes, and added entirely ne
106106

107107
## Changelog
108108

109+
### Changes from 3.x to 4.0
110+
111+
This release contains only a single change but since it breaks the API it is
112+
published as a major release.
113+
114+
Splat arguments were reworked so that they allow for unlimited repititions
115+
**by default** (see [GH-54](https://github.com/amireh/lua_cliargs/issues/54)
116+
for more context.)
117+
118+
Previously, if you were defining a splat argument _without_ specifying a
119+
`maxcount` value (the 4th argument) the library would assume a maxcount of 1,
120+
indicating that your splat argument is just an optional argument and will be
121+
provided as a string value instead of a table.
122+
123+
If you need to maintain this behavior, you must now explicitly set the maxcount
124+
to `1`:
125+
126+
```lua
127+
-- version 3
128+
cli:splat('MY_SPLAT', 'Description')
129+
130+
-- version 4
131+
cli:splat('MY_SPLAT', 'Description', nil, 1)
132+
```
133+
134+
Also, the library internally had an arbitrary limit of 999 repetitions for the
135+
splat argument. That limit has been relieved.
136+
109137
### Changes from 2.5.x 3.0
110138

111139
This major version release contains BREAKING API CHANGES. See the UPGRADE guide for help in updating your code to make use of it.

bin/coverage

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ if [ $? -ne 0 ]; then
2424
exit 1
2525
fi
2626

27+
rm luacov.stats.out
2728
busted -c
2829
luacov src/
29-
rm luacov.stats.out
3030
grep -zPo "(?s)={10,}\nSummary\n={10,}.+" luacov.report.out

spec/core_spec.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ describe("cliargs::core", function()
9797

9898
describe('#redefine_default', function()
9999
it('allows me to change the default for an optargument', function()
100-
cli:splat('ROOT', '...', 'foo')
100+
cli:splat('ROOT', '...', 'foo', 1)
101101
assert.equal(cli:parse({}).ROOT, 'foo')
102102

103103
cli:redefine_default('ROOT', 'bar')

spec/features/integration_spec.lua

+62
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,68 @@ describe("integration: parsing", function()
1111
assert.are.same(helpers.parse(cli, ''), {})
1212
end)
1313

14+
describe('validating number of arguments', function()
15+
context('when no arguments are defined', function()
16+
it('raises nothing', function()
17+
helpers.parse(cli, '')
18+
end)
19+
end)
20+
21+
context('with a required argument', function()
22+
it('raises an error on extraneous arguments', function()
23+
cli:argument('FOO', '...')
24+
25+
local _, err = helpers.parse(cli, 'foo bar')
26+
27+
assert.equal(err, 'bad number of arguments: expected exactly 1 argument not 2')
28+
end)
29+
30+
it('raises an error on few arguments', function()
31+
cli:argument('FOO', '...')
32+
33+
local _, err = helpers.parse(cli, '')
34+
35+
assert.equal(err, 'bad number of arguments: expected exactly 1 argument not 0')
36+
end)
37+
end)
38+
39+
context('with a splat with unlimited reptitions', function()
40+
it('does not raise an error if nothing is passed in', function()
41+
cli:splat('FOO', '...')
42+
43+
local _, err = helpers.parse(cli, '')
44+
45+
assert.equal(nil, err)
46+
end)
47+
48+
it('does not raise an error if something was passed in', function()
49+
cli:splat('FOO', '...')
50+
51+
local _, err = helpers.parse(cli, 'foo')
52+
53+
assert.equal(nil, err)
54+
end)
55+
end)
56+
57+
context('with a splat with bounded reptitions', function()
58+
it('does not raise an error if passed count is within bounds', function()
59+
cli:splat('FOO', '...', nil, 3)
60+
61+
local _, err = helpers.parse(cli, 'foo bar')
62+
63+
assert.equal(nil, err)
64+
end)
65+
66+
it('raises an error if passed count is outside of bounds', function()
67+
cli:splat('FOO', '...', nil, 3)
68+
69+
local _, err = helpers.parse(cli, 'foo bar bax hax')
70+
71+
assert.equal(err, 'bad number of arguments: expected 0-3 arguments not 4')
72+
end)
73+
end)
74+
end)
75+
1476
context('given a set of arguments', function()
1577
it('works when all are passed in', function()
1678
cli:argument('FOO', '...')

spec/features/splatarg_spec.lua

+8-2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ describe("cliargs - splat arguments", function()
3333
cli:splat('SOME_SPLAT', 'some repeatable arg')
3434
end, 'Only one splat')
3535
end)
36+
37+
it('rejects repetition count less than 0', function()
38+
assert.error_matches(function()
39+
cli:splat('SOME_SPLAT', 'some repeatable arg', nil, -1)
40+
end, 'Maxcount must be a number equal to or greater than 0')
41+
end)
3642
end)
3743

3844
describe('default value', function()
@@ -42,7 +48,7 @@ describe("cliargs - splat arguments", function()
4248

4349
context('when only 1 occurrence is allowed', function()
4450
before_each(function()
45-
cli:splat('SPLAT', 'some repeatable arg', 'foo')
51+
cli:splat('SPLAT', 'some repeatable arg', 'foo', 1)
4652
end)
4753

4854
it('uses the default value when nothing is passed in', function()
@@ -104,7 +110,7 @@ describe("cliargs - splat arguments", function()
104110
it('invokes the callback every time a value for the splat arg is parsed', function()
105111
local call_args = {}
106112

107-
cli:splat('SPLAT', 'foobar', nil, 2, function(_, value)
113+
cli:splat('SPLAT', 'foobar', nil, nil, function(_, value)
108114
table.insert(call_args, value)
109115
end)
110116

spec/printer_spec.lua

+7
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ describe('printer', function()
7676
it('prints a splat arg with reptitions > 2', function()
7777
cli:splat('OUTPUT', '...', nil, 5)
7878

79+
assert_msg [==[
80+
Usage: [--] [OUTPUT-1 [OUTPUT-2 [... [OUTPUT-5]]]]
81+
]==]
82+
end)
83+
it('prints a splat arg with unlimited reptitions', function()
84+
cli:splat('OUTPUT', '...', nil, 0)
85+
7986
assert_msg [==[
8087
Usage: [--] [OUTPUT-1 [OUTPUT-2 [...]]]
8188
]==]

src/cliargs/core.lua

+8-4
Original file line numberDiff line numberDiff line change
@@ -327,8 +327,12 @@ local function create_core()
327327
--- @param {*} [default=nil]
328328
--- A default value.
329329
---
330-
--- @param {number} [maxcount=1]
330+
--- @param {number} [maxcount=0]
331331
--- The maximum number of occurences allowed.
332+
--- When set to 0 (the default), an unlimited amount of repetitions is
333+
--- allowed.
334+
--- When set to 1, the value of the splat argument will be provided as
335+
--- a primitive (a string) instead of a table.
332336
---
333337
--- @param {function} [callback]
334338
--- A function to call **everytime** a value for this argument is
@@ -347,10 +351,10 @@ local function create_core()
347351
"Default value must either be omitted or be a string"
348352
)
349353

350-
maxcount = tonumber(maxcount or 1)
354+
maxcount = tonumber(maxcount or 0)
351355

352-
assert(maxcount > 0 and maxcount < 1000,
353-
"Maxcount must be a number from 1 to 999"
356+
assert(maxcount >= 0,
357+
"Maxcount must be a number equal to or greater than 0"
354358
)
355359

356360
assert(is_callable(callback) or callback == nil,

src/cliargs/parser.lua

+37-18
Original file line numberDiff line numberDiff line change
@@ -215,28 +215,47 @@ end
215215

216216
function p.validate(options, arg_count, done)
217217
local required = filter(options, 'type', K.TYPE_ARGUMENT)
218-
local splatarg = filter(options, 'type', K.TYPE_SPLAT)[1] or { maxcount = 0 }
219-
218+
local splat = filter(options, 'type', K.TYPE_SPLAT)[1]
220219
local min_arg_count = #required
221-
local max_arg_count = #required + splatarg.maxcount
222-
223-
-- missing any required arguments, or too many?
224-
if arg_count < min_arg_count or arg_count > max_arg_count then
225-
if splatarg.maxcount > 0 then
226-
return nil, (
227-
"bad number of arguments: " ..
228-
min_arg_count .. "-" .. max_arg_count ..
229-
" argument(s) must be specified, not " .. arg_count
230-
)
220+
221+
local function get_range_description()
222+
if splat and splat.maxcount == 0 then
223+
return "at least " .. min_arg_count
224+
elseif splat and splat.maxcount > 0 then
225+
return min_arg_count .. "-" .. (#required + splat.maxcount)
231226
else
232-
return nil, (
233-
"bad number of arguments: " ..
234-
min_arg_count .. " argument(s) must be specified, not " .. arg_count
235-
)
227+
return "exactly " .. min_arg_count
236228
end
237229
end
238230

239-
return done()
231+
local function is_count_valid()
232+
if splat and splat.maxcount == 0 then
233+
return arg_count >= #required
234+
elseif splat and splat.maxcount > 0 then
235+
return arg_count >= #required and arg_count <= #required + splat.maxcount
236+
else
237+
return arg_count == #required
238+
end
239+
end
240+
241+
local function plural(word, count)
242+
if count == 1 then
243+
return word
244+
else
245+
return word .. 's'
246+
end
247+
end
248+
249+
if is_count_valid() then
250+
return done()
251+
else
252+
return nil, (
253+
"bad number of arguments: expected " ..
254+
get_range_description() .. " " ..
255+
plural('argument', #required + (splat and splat.maxcount or 0)) ..
256+
" not " .. arg_count
257+
)
258+
end
240259
end
241260

242261
function p.collect_results(cli_values, options)
@@ -268,7 +287,7 @@ function p.collect_results(cli_values, options)
268287
local maxcount = entry.maxcount
269288

270289
if maxcount == nil then
271-
maxcount = type(entry.default) == 'table' and 999 or 1
290+
maxcount = type(entry.default) == 'table' and 0 or 1
272291
end
273292

274293
local entry_value = entry_cli_values

src/cliargs/printer.lua

+11-9
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ local function create_printer(get_parser_state)
4747

4848
local required = filter(state.options, 'type', K.TYPE_ARGUMENT)
4949
local optional = filter(state.options, 'type', K.TYPE_OPTION)
50-
local optargument = filter(state.options, 'type', K.TYPE_SPLAT)[1]
50+
local splat = filter(state.options, 'type', K.TYPE_SPLAT)[1]
5151

5252
if #state.name > 0 then
5353
msg = msg .. ' ' .. tostring(state.name)
@@ -57,7 +57,7 @@ local function create_printer(get_parser_state)
5757
msg = msg .. " [OPTIONS]"
5858
end
5959

60-
if #required > 0 or optargument then
60+
if #required > 0 or splat then
6161
msg = msg .. " [--]"
6262
end
6363

@@ -67,13 +67,15 @@ local function create_printer(get_parser_state)
6767
end
6868
end
6969

70-
if optargument then
71-
if optargument.maxcount == 1 then
72-
msg = msg .. " [" .. optargument.key .. "]"
73-
elseif optargument.maxcount == 2 then
74-
msg = msg .. " [" .. optargument.key .. "-1 [" .. optargument.key .. "-2]]"
75-
elseif optargument.maxcount > 2 then
76-
msg = msg .. " [" .. optargument.key .. "-1 [" .. optargument.key .. "-2 [...]]]"
70+
if splat then
71+
if splat.maxcount == 1 then
72+
msg = msg .. " [" .. splat.key .. "]"
73+
elseif splat.maxcount == 2 then
74+
msg = msg .. " [" .. splat.key .. "-1 [" .. splat.key .. "-2]]"
75+
elseif splat.maxcount > 0 then
76+
msg = msg .. " [" .. splat.key .. "-1 [" .. splat.key .. "-2 [... [" .. splat.key .. "-" .. splat.maxcount .. "]]]]"
77+
else
78+
msg = msg .. " [" .. splat.key .. "-1 [" .. splat.key .. "-2 [...]]]"
7779
end
7880
end
7981

0 commit comments

Comments
 (0)