Skip to content

Commit 02923c5

Browse files
authored
Rename @batteries/cliargs to @batteries/cli and clean up the code. (#109)
This PR does some basic tidying of `@batteries/cli` (after renaming it from `@batteries/cliargs`). Clean up includes some minor changes to the name of the interface, cleaning up some of the parsing code itself to have easier to follow control flow, and fixing some of the incorrect type info in the library.
1 parent e6b8774 commit 02923c5

File tree

3 files changed

+184
-158
lines changed

3 files changed

+184
-158
lines changed

batteries/cli.luau

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
local cli = {}
2+
cli.__index = cli
3+
4+
type ArgKind = "positional" | "flag" | "option"
5+
type ArgOptions = {
6+
help: string?,
7+
aliases: { string }?,
8+
default: string?,
9+
required: boolean?,
10+
}
11+
12+
type ArgData = {
13+
name: string,
14+
kind: ArgKind,
15+
options: ArgOptions,
16+
}
17+
18+
type ParseResult = {
19+
values: { [string]: string },
20+
flags: { [string]: boolean },
21+
fwdArgs: { string },
22+
}
23+
24+
type ParserData = {
25+
arguments: { [number]: ArgData },
26+
positional: { [number] : ArgData},
27+
parsed: ParseResult,
28+
}
29+
30+
type ParserInterface = typeof(cli)
31+
export type Parser = setmetatable<ParserData, ParserInterface>
32+
33+
function cli.parser(): Parser
34+
local self = {
35+
arguments = {},
36+
positional = {},
37+
parsed = { values = {}, flags = {}, fwdArgs = {} },
38+
}
39+
40+
return setmetatable(self, cli)
41+
end
42+
43+
function cli.add(self: Parser, name: string, kind: ArgKind, options: ArgOptions): ()
44+
local argument = {
45+
name = name,
46+
kind = kind,
47+
options = options or { aliases = {}, required = false },
48+
}
49+
50+
table.insert(self.arguments, argument)
51+
if kind == "positional" then
52+
table.insert(self.positional, argument)
53+
end
54+
end
55+
56+
function cli.parse(self: Parser, args: {string}): ()
57+
local i = 0
58+
local pos_index = 1
59+
60+
while i < #args do
61+
i += 1
62+
63+
local arg = args[i]
64+
65+
-- if the argument is exactly "--", we pass everything along
66+
if arg == "--" then
67+
table.move(args, i + 1, #args, 1, self.parsed.fwdArgs)
68+
break
69+
end
70+
71+
-- if the argument starts with two dashes, we're parsing either a flag or an option
72+
if string.sub(arg, 1, 2) == "--" then
73+
local name = string.sub(arg, 3)
74+
local found = false
75+
76+
for _, argument in self.arguments do
77+
local aliases = argument.options.aliases or {}
78+
79+
if argument.name == name or table.find(aliases, name) then
80+
found = true
81+
82+
if argument.kind == "option" then
83+
-- advance past the argument
84+
i += 1
85+
86+
assert(i <= #args, "Missing value for argument: " .. argument.name)
87+
self.parsed.values[argument.name] = args[i]
88+
89+
break
90+
end
91+
92+
self.parsed.flags[argument.name] = true
93+
break
94+
end
95+
end
96+
97+
assert(found, "Unknown argument: " .. name)
98+
continue
99+
end
100+
101+
-- if the argument starts with a single dash, we're parsing a flag
102+
if string.sub(arg, 1, 1) == "-" then
103+
local flags = string.sub(arg, 2)
104+
105+
for j = 1, #flags do
106+
local name = string.sub(flags, j, j)
107+
local found = false
108+
for _, argument in self.arguments do
109+
local aliases = argument.options.aliases or {}
110+
if argument.name == name or table.find(aliases, name) then
111+
found = true
112+
if argument.kind == "option" then
113+
i += 1
114+
assert(i <= #args, "Missing value for argument: " .. argument.name)
115+
self.parsed.values[argument.name] = args[i]
116+
else
117+
self.parsed.flags[argument.name] = true
118+
end
119+
break
120+
end
121+
end
122+
123+
assert(found, "Unknown argument: " .. name)
124+
end
125+
126+
continue
127+
end
128+
129+
-- if we have positional arguments left, we can take this argument as one
130+
if pos_index <= #self.positional then
131+
self.parsed.values[self.positional[pos_index].name] = arg
132+
pos_index += 1
133+
continue
134+
end
135+
136+
-- otherwise, the argument is forwarded on
137+
table.insert(self.parsed.fwdArgs, arg)
138+
end
139+
140+
-- check that all required arguments are present, and set any default values as needed
141+
for _, argument in self.arguments do
142+
assert(argument)
143+
144+
if argument.options.required and self.parsed.values[argument.name] == nil then
145+
assert(self.parsed.values[argument.name], "Missing required argument: " .. argument.name)
146+
end
147+
148+
if self.parsed.values[argument.name] == nil and argument.options.default then
149+
self.parsed.values[argument.name] = argument.options.default
150+
end
151+
end
152+
end
153+
154+
function cli.get(self: Parser, name: string): boolean
155+
return self.parsed.values[name]
156+
end
157+
158+
function cli.has(self: Parser, name: string): boolean
159+
return self.parsed.flags[name] ~= nil
160+
end
161+
162+
function cli.forwaded(): { string }?
163+
return self.parsed.fwdArgs
164+
end
165+
166+
function cli.help(self: Parser): ()
167+
print("Usage:")
168+
for _, argument in self.arguments do
169+
local aliasStr = table.concat(argument.options.aliases or {}, ", ")
170+
local reqStr = if argument.options.required then "(required) " else ""
171+
print(string.format(" --%s (-%s) - %s%s", argument.name, aliasStr, reqStr, argument.options.help or ""))
172+
end
173+
end
174+
175+
return cli

batteries/cliargs.luau

Lines changed: 0 additions & 147 deletions
This file was deleted.

examples/cliargs.luau

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
local cliargs = require("@batteries/cliargs")
1+
local cli = require("@batteries/cli")
22

3-
local parser = cliargs.new()
3+
local args = cli.parser()
44

5-
parser:add("input", "positional", { help = "Input file" })
6-
parser:add("verbose", "flag", { help = "Enable verbose mode", aliases = { "v" } })
7-
parser:add("output", "option", { help = "Output file", aliases = { "o" }, default = "default.txt" })
5+
args:add("input", "positional", { help = "Input file" })
6+
args:add("verbose", "flag", { help = "Enable verbose mode", aliases = { "v" } })
7+
args:add("output", "option", { help = "Output file", aliases = { "o" }, default = "default.txt" })
88

9-
local args = { ... }
9+
args:parse({ ... })
1010

11-
parser:parse(args)
12-
13-
print("Input File:", parser:get("input")) -- "input.txt"
14-
print("Verbose Mode:", parser:has("verbose")) -- true
15-
print("Output File:", parser:get("output")) -- "result.txt"
11+
print("Input File:", args:get("input")) -- "input.txt"
12+
print("Verbose Mode:", args:has("verbose")) -- true
13+
print("Output File:", args:get("output")) -- "result.txt"

0 commit comments

Comments
 (0)