-
Notifications
You must be signed in to change notification settings - Fork 25
Expand file tree
/
Copy pathparser.lua
More file actions
335 lines (269 loc) · 8.91 KB
/
parser.lua
File metadata and controls
335 lines (269 loc) · 8.91 KB
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
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
local K = require 'cliargs.constants'
-------------------------------------------------------------------------------
-- UTILS
-------------------------------------------------------------------------------
local shallow_copy = require 'cliargs.utils.shallow_copy'
local filter = require 'cliargs.utils.filter'
local disect_argument = require 'cliargs.utils.disect_argument'
local lookup = require 'cliargs.utils.lookup'
local function clone_table_shift(t)
local clone = shallow_copy(t)
table.remove(clone, 1)
return clone
end
local function clone_table_remove(t, index)
local clone = shallow_copy(t)
table.remove(clone, index)
return clone
end
-------------------------------------------------------------------------------
-- PARSE ROUTINES
-------------------------------------------------------------------------------
local p = {}
function p.invoke_command(args, options, done)
local commands = filter(options, 'type', K.TYPE_COMMAND)
for index, opt in ipairs(args) do
local command = filter(commands, '__key__', opt)[1]
if command then
local command_args = clone_table_remove(args, index)
if command.__action__ then
local parsed_command_args, err = command:parse(command_args)
if err then
return nil, err
end
return command.__action__(parsed_command_args)
elseif command.__file__ then
local filename = command.__file__
if type(filename) == 'function' then
filename = filename()
end
local run_command_file = function()
_G.arg = command_args
local res, err = assert(loadfile(filename))()
_G.arg = args
return res, err
end
return run_command_file()
end
end
end
return done()
end
function p.print_help(args, printer, done)
-- has --help or -h ? display the help listing and abort!
for _, v in pairs(args) do
if v == "--help" or v == "-h" then
return nil, printer.generate_help_and_usage()
end
end
return done()
end
function p.track_dump_request(args, done)
-- starts with --__DUMP__; set dump to true to dump the parsed arguments
if args[1] == "--__DUMP__" then
return done(true, clone_table_shift(args))
else
return done(false, args)
end
end
function p.process_arguments(args, options, done)
local values = {}
local cursor = 0
local argument_cursor = 1
local argument_delimiter_found = false
local function consume()
cursor = cursor + 1
return args[cursor]
end
local required = filter(options, 'type', K.TYPE_ARGUMENT)
while cursor < #args do
local curr_opt = consume()
local symbol, key, value, flag_negated = disect_argument(curr_opt)
-- end-of-options indicator:
if curr_opt == "--" then
argument_delimiter_found = true
-- an option:
elseif not argument_delimiter_found and symbol then
local entry = lookup(key, key, options)
if not key or not entry then
local option_type = value and "option" or "flag"
return nil, "unknown/bad " .. option_type .. ": " .. curr_opt
end
if flag_negated and not entry.negatable then
return nil, "flag '" .. curr_opt .. "' may not be negated using --no-"
end
-- a flag and a value specified? that's an error
if entry.flag and value then
return nil, "flag " .. curr_opt .. " does not take a value"
elseif entry.flag then
value = not flag_negated
-- an option:
else
-- the value might be in the next argument, e.g:
--
-- --compress lzma
if not value then
-- if the option contained a = and there's no value, it means they
-- want to nullify an option's default value. eg:
--
-- --compress=
if curr_opt:find('=') then
value = '__CLIARGS_NULL__'
else
-- NOTE: this has the potential to be buggy and swallow the next
-- entry as this entry's value even though that entry may be an
-- actual argument/option
--
-- this would be a user error and there is no determinate way to
-- figure it out because if there's no leading symbol (- or --)
-- in that entry it can be an actual argument. :shrug:
value = consume()
if not value then
return nil, "option " .. curr_opt .. " requires a value to be set"
end
end
end
end
table.insert(values, { entry = entry, value = value })
if entry.callback then
local altkey = entry.key
local status, err
if key == entry.key then
altkey = entry.expanded_key
else
key = entry.expanded_key
end
status, err = entry.callback(key, value, altkey, curr_opt)
if status == nil and err then
return nil, err
end
end
-- a regular argument:
elseif argument_cursor <= #required then
local entry = required[argument_cursor]
table.insert(values, { entry = entry, value = curr_opt })
if entry.callback then
local status, err = entry.callback(entry.key, curr_opt)
if status == nil and err then
return nil, err
end
end
argument_cursor = argument_cursor + 1
-- a splat argument:
else
local entry = filter(options, 'type', K.TYPE_SPLAT)[1]
if entry then
table.insert(values, { entry = entry, value = curr_opt })
if entry.callback then
local status, err = entry.callback(entry.key, curr_opt)
if status == nil and err then
return nil, err
end
end
end
argument_cursor = argument_cursor + 1
end
end
return done(values, argument_cursor - 1)
end
function p.validate(options, arg_count, done)
local required = filter(options, 'type', K.TYPE_ARGUMENT)
local splat = filter(options, 'type', K.TYPE_SPLAT)[1]
local min_arg_count = #required
local function get_range_description()
if splat and splat.maxcount == 0 then
return "at least " .. min_arg_count
elseif splat and splat.maxcount > 0 then
return min_arg_count .. "-" .. (#required + splat.maxcount)
else
return "exactly " .. min_arg_count
end
end
local function is_count_valid()
if splat and splat.maxcount == 0 then
return arg_count >= #required
elseif splat and splat.maxcount > 0 then
return arg_count >= #required and arg_count <= #required + splat.maxcount
else
return arg_count == #required
end
end
local function plural(word, count)
if count == 1 then
return word
else
return word .. 's'
end
end
if is_count_valid() then
return done()
else
return nil, (
"bad number of arguments: expected " ..
get_range_description() .. " " ..
plural('argument', #required + (splat and splat.maxcount or 0)) ..
" not " .. arg_count
)
end
end
function p.collect_results(cli_values, options)
local results = {}
local function collect_with_default(entry)
local entry_values = {}
local _
for _, item in ipairs(cli_values) do
if item.entry == entry then
table.insert(entry_values, item.value)
end
end
if #entry_values == 0 then
return type(entry.default) == 'table' and entry.default or { entry.default }
else
return entry_values
end
end
local function write(entry, value)
if entry.key then results[entry.key] = value end
if entry.expanded_key then results[entry.expanded_key] = value end
end
for _, entry in pairs(options) do
local entry_cli_values = collect_with_default(entry)
local maxcount = entry.maxcount
if maxcount == nil then
maxcount = type(entry.default) == 'table' and 0 or 1
end
local entry_value = entry_cli_values
if maxcount == 1 and type(entry_cli_values) == 'table' then
-- take the last value
entry_value = entry_cli_values[#entry_cli_values]
if entry_value == '__CLIARGS_NULL__' then
entry_value = nil
end
end
write(entry, entry_value)
end
return results
end
return function(arguments, options, printer)
assert(arguments == nil or type(arguments) == "table",
"expected an argument table to be passed in, " ..
"got something of type " .. type(arguments)
)
local args = arguments or _G.arg or {}
-- the spiral of DOOM:
return p.invoke_command(args, options, function()
return p.track_dump_request(args, function(dump, args_without_dump)
return p.print_help(args_without_dump, printer, function()
return p.process_arguments(args_without_dump, options, function(values, arg_count)
return p.validate(options, arg_count, function()
if dump then
return nil, printer.dump_internal_state(values)
else
return p.collect_results(values, options)
end
end)
end)
end)
end)
end)
end