Skip to content

Commit 9c2c88a

Browse files
committed
feat(cli): adding cli parsing to the stdlib
1 parent 167f031 commit 9c2c88a

File tree

2 files changed

+245
-0
lines changed

2 files changed

+245
-0
lines changed

Cli.ark

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
(import std.List :map :flatMap :forEach :find)
2+
(import std.String :join)
3+
(import std.Dict)
4+
5+
(let _param (fun (name desc default kind) {
6+
(let required (= kind "value"))
7+
(let value default)
8+
9+
(fun (
10+
&kind
11+
&name
12+
&desc
13+
&default
14+
&value
15+
&required) ()) }))
16+
17+
(let _group (fun (children all_of) {
18+
(let kind "group")
19+
20+
(fun (
21+
&kind
22+
&children
23+
&all_of) ()) }))
24+
25+
# @brief Defines a command line flag
26+
# @details All flags are optional and turned off
27+
# @param name how the flag is named, eg "--debug"
28+
# @param desc what the flag achieves
29+
(let flag (fun (name desc)
30+
(_param name desc false "flag")))
31+
32+
# @brief Defines a command line value
33+
# @details All values are required and equal to their default value
34+
# @param name how the value is named, eg "filename"
35+
# @param desc what the value is for
36+
# @param default default value for the value
37+
(let value (fun (name desc default)
38+
(_param name desc default "value")))
39+
40+
# @brief Creates a group of flags/values/groups where only one of the subgroup has to match
41+
# @param params list of flags/values/groups
42+
(let oneOf (fun (params)
43+
(_group params false)))
44+
45+
# @brief Creates a group of flags/values/groups where all of the subgroups have to match
46+
# @param params list of flags/values/groups
47+
(let group (fun (params)
48+
(_group params true)))
49+
50+
(let _synopsis_group (fun (cli)
51+
(if cli.all_of
52+
{
53+
(let partially_fmt (map
54+
cli.children
55+
(fun (param) (_format_param param))))
56+
57+
(mut options [""])
58+
(mut i 0)
59+
60+
(while (< i (len partially_fmt)) {
61+
(let e (@ partially_fmt i))
62+
(if (= (type e) "String")
63+
(set options (map
64+
options
65+
(fun (alt)
66+
(if (empty? alt)
67+
e
68+
(format "{} {}" alt e)))))
69+
# if we have a list, add (@ options 0) to each element of the list, then added to options
70+
(concat! options (map e (fun (alt) (format "{} {}" (@ options 0) alt)))))
71+
(set i (+ 1 i)) })
72+
73+
options
74+
}
75+
# any_of, return a list of each formatted param
76+
(flatMap
77+
cli.children
78+
(fun (param) (_format_param param))))))
79+
80+
(let _synopsis_flag (fun (param)
81+
(if param.required
82+
(format "{}" cli.name)
83+
(format "[{}]" cli.name))))
84+
85+
(let _synopsis_value (fun (param)
86+
(format "<{}>" cli.name)))
87+
88+
(let _format_param (fun (cli)
89+
(if (= cli.kind "group")
90+
(_synopsis_group cli)
91+
(if (= cli.kind "flag")
92+
(_synopsis_flag cli)
93+
# if not group nor flag, then it must be a value
94+
(_synopsis_value cli)))))
95+
96+
(let _get_options (fun (d param)
97+
(if (= param.kind "group")
98+
(forEach
99+
param.children
100+
(fun (p) (_get_options d p)))
101+
(dict:add d param.name param))))
102+
103+
(let help (fun (program desc cli) {
104+
(let headers (format "DESCRIPTION\n\t{}\n\nSYNOPSIS\n" desc))
105+
(mut synopsis (_format_param cli))
106+
(if (= (type synopsis) "List")
107+
(set synopsis (join (flatMap synopsis (fun (alt) (format "\t{} {}" program alt))) "\n")))
108+
109+
(let params (dict))
110+
(_get_options params cli)
111+
(let options (join
112+
(map (dict:keys params) (fun (name) {
113+
(let param (dict:get params name))
114+
(let fmt_name (if (= param.kind "value") (format "<{}>" name) name))
115+
(format "\t{:<28} {}" fmt_name param.desc) }))
116+
"\n"))
117+
118+
(+ headers synopsis "\n\nOPTIONS\n" options) }))
119+
120+
(let _match_group (fun (parsed args param) {
121+
(mut missing_param false)
122+
(mut i 0)
123+
(while (and (not missing_param) (< i (len param.children))) {
124+
(let elem (@ param.children i))
125+
126+
# in a group, we expect arguments in order
127+
(if (= elem.kind "flag")
128+
(if (= (head args) elem.name)
129+
{
130+
(dict:add parsed elem.name true)
131+
(set args (tail args)) }
132+
{
133+
(dict:add parsed elem.name false)
134+
(set missing_param true) })
135+
(if (= elem.kind "value")
136+
{
137+
(dict:add parsed elem.name (head args))
138+
(set args (tail args)) }
139+
# group
140+
{
141+
(if elem.all_of
142+
(set missing_param (not (_match_group parsed args elem)))
143+
(set missing_param (not (_match_one_of parsed args elem))))
144+
(set args (tail args)) }))
145+
146+
(set i (+ 1 i)) })
147+
148+
(not missing_param) }))
149+
150+
(let _match_one_of (fun (parsed args param) {
151+
(mut i 0)
152+
(mut matched false)
153+
(while (and (not matched) (< i (len param.children))) {
154+
(let elem (@ param.children i))
155+
156+
(if (= elem.kind "flag")
157+
{
158+
(let maybe_flag (find args elem.name))
159+
(if (!= maybe_flag -1)
160+
{
161+
(dict:add parsed elem.name true)
162+
(set matched true) }
163+
(dict:add parsed elem.name false)) }
164+
(if (= elem.kind "value")
165+
{
166+
(dict:add parsed elem.name (head args))
167+
(set matched true) }
168+
# group
169+
(if elem.all_of
170+
(set matched (_match_group parsed args elem))
171+
(set matched (_match_one_of parsed args elem)))))
172+
173+
(set i (+ 1 i)) })
174+
175+
matched }))
176+
177+
# @brief Parse a list of arguments given a CLI definition
178+
# @details Recursively visit the CLI to parse the argument list
179+
# @param args list of arguments, eg sys:args
180+
# @param cli cli definition
181+
# =begin
182+
# (import std.Cli)
183+
# (let command_line
184+
# (cli:oneOf [
185+
# (cli:flag "--help" "Display an help message")
186+
# (cli:flag "--repl" "Start the REPL")
187+
# (cli:group [
188+
# (cli:flag "-c" "Compile a given file")
189+
# (cli:value "file" "Path to the file to run" nil) ])]))
190+
#
191+
# (print (cli:help "miniark" "A mini ArkScript CLI" command_line))
192+
# (print (cli:parseArgs ["-c" "path.ark"] command_line))
193+
# =end
194+
# @author https://github.com/SuperFola
195+
(let parseArgs (fun (args cli) {
196+
(let parsed (dict))
197+
198+
(if (= cli.kind "flag")
199+
{
200+
(let maybe_flag (find args cli.name))
201+
(if (!= maybe_flag -1)
202+
(dict:add parsed cli.name true)) }
203+
(if (= cli.kind "value")
204+
(dict:add parsed cli.name (head args))
205+
# group
206+
(if cli.all_of (_match_group parsed args cli) (_match_one_of parsed args cli))))
207+
208+
parsed }))
209+

tests/cli-tests.ark

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
(import std.String :stripMargin)
2+
(import std.Cli)
3+
(import std.Testing)
4+
5+
(test:suite cli {
6+
(let command_line
7+
(cli:oneOf [
8+
(cli:flag "--help" "Display an help message")
9+
(cli:flag "--repl" "Start the REPL")
10+
(cli:group [
11+
(cli:flag "-c" "Compile a given file")
12+
(cli:value "file" "Path to the file to run" nil) ])]))
13+
14+
(test:eq (cli:help "miniark" "A mini ArkScript CLI" command_line)
15+
(stripMargin "DESCRIPTION
16+
|\tA mini ArkScript CLI
17+
|
18+
|SYNOPSIS
19+
|\tminiark [--help]
20+
|\tminiark [--repl]
21+
|\tminiark [-c] <file>
22+
|
23+
|OPTIONS
24+
|\t--help Display an help message
25+
|\t--repl Start the REPL
26+
|\t-c Compile a given file
27+
|\t<file> Path to the file to run"))
28+
29+
(test:eq (cli:parseArgs [] command_line) (dict "--help" false "--repl" false "-c" false))
30+
(test:eq (cli:parseArgs ["a" "b" "c" "d"] command_line) (dict "--help" false "--repl" false "-c" false))
31+
(test:eq (cli:parseArgs ["asdf" "-c" "path.ark"] command_line) (dict "--help" false "--repl" false "-c" false))
32+
33+
(test:eq (cli:parseArgs ["--help" "--repl"] command_line) (dict "--help" true))
34+
(test:eq (cli:parseArgs ["--repl"] command_line) (dict "--help" false "--repl" true))
35+
(test:eq (cli:parseArgs ["-c" "path.ark"] command_line) (dict "--help" false "--repl" false "-c" true "file" "path.ark")) })
36+

0 commit comments

Comments
 (0)