1 | # args.ysh
|
2 | #
|
3 | # Usage:
|
4 | # source --builtin args.sh
|
5 |
|
6 | const __provide__ = :| parser parseArgs |
|
7 |
|
8 | #
|
9 | #
|
10 | # parser (&spec) {
|
11 | # flag -v --verbose (help="Verbosely") # default is Bool, false
|
12 | #
|
13 | # flag -P --max-procs (Int, default=-1, doc='''
|
14 | # Run at most P processes at a time
|
15 | # ''')
|
16 | #
|
17 | # flag -i --invert (Bool, default=true, doc='''
|
18 | # Long multiline
|
19 | # Description
|
20 | # ''')
|
21 | #
|
22 | # arg src (help='Source')
|
23 | # arg dest (help='Dest')
|
24 | # arg times (help='Foo')
|
25 | #
|
26 | # rest files
|
27 | # }
|
28 | #
|
29 | # var args = parseArgs(spec, ARGV)
|
30 | #
|
31 | # echo "Verbose $[args.verbose]"
|
32 |
|
33 | # TODO: See list
|
34 | # - flag builtin:
|
35 | # - handle only long flag or only short flag
|
36 | # - flag aliases
|
37 | # - support repeated or character-delimited multi-value flags
|
38 |
|
39 | proc parser (; place ; ; block_def) {
|
40 | ## Create an args spec which can be passed to parseArgs.
|
41 | ##
|
42 | ## Example:
|
43 | ##
|
44 | ## # NOTE: &spec will create a variable named spec
|
45 | ## parser (&spec) {
|
46 | ## flag -v --verbose (Bool)
|
47 | ## }
|
48 | ##
|
49 | ## var args = parseArgs(spec, ARGV)
|
50 |
|
51 | var p = {flags: [], args: []}
|
52 | ctx push (p) {
|
53 | call io->eval(block_def, vars={flag, arg, rest})
|
54 | }
|
55 |
|
56 | # Validate that p.rest = [name] or null and reduce p.rest into name or null.
|
57 | if ('rest' in p) {
|
58 | if (len(p.rest) > 1) {
|
59 | error '`rest` was called more than once' (code=3)
|
60 | } else {
|
61 | setvar p.rest = p.rest[0]
|
62 | }
|
63 | } else {
|
64 | setvar p.rest = null
|
65 | }
|
66 |
|
67 | var names = {}
|
68 | for items in ([p.flags, p.args]) {
|
69 | for x in (items) {
|
70 | if (x.name in names) {
|
71 | error "Duplicate flag/arg name $[x.name] in spec" (code=3)
|
72 | }
|
73 |
|
74 | setvar names[x.name] = null
|
75 | }
|
76 | }
|
77 |
|
78 | # TODO: what about `flag --name` and then `arg name`?
|
79 |
|
80 | call place->setValue(p)
|
81 | }
|
82 |
|
83 | const kValidTypes = [Bool, Float, Int, Str]
|
84 | const kValidTypeNames = []
|
85 | for vt in (kValidTypes) {
|
86 | call kValidTypeNames->append(vt.name)
|
87 | }
|
88 |
|
89 | func isValidType (type) {
|
90 | try {
|
91 | for valid in (kValidTypes) {
|
92 | if (type is valid) {
|
93 | return (true)
|
94 | }
|
95 | }
|
96 | }
|
97 | return (false)
|
98 | }
|
99 |
|
100 | proc flag (short, long ; type=Bool ; default=null, help=null) {
|
101 | ## Declare a flag within an `arg-parse`.
|
102 | ##
|
103 | ## Examples:
|
104 | ##
|
105 | ## arg-parse (&spec) {
|
106 | ## flag -v --verbose
|
107 | ## flag -n --count (Int, default=1)
|
108 | ## flag -p --percent (Float, default=0.0)
|
109 | ## flag -f --file (Str, help="File to process")
|
110 | ## }
|
111 |
|
112 | if (type !== null and not isValidType(type)) {
|
113 | var type_names = ([null] ++ kValidTypeNames) => join(', ')
|
114 | error "Expected flag type to be one of: $type_names" (code=2)
|
115 | }
|
116 |
|
117 | # Bool has a default of false, not null
|
118 | if (type is Bool and default === null) {
|
119 | setvar default = false
|
120 | }
|
121 |
|
122 | var name = long => trimStart('--')
|
123 |
|
124 | ctx emit flags ({short, long, name, type, default, help})
|
125 | }
|
126 |
|
127 | proc arg (name ; ; help=null) {
|
128 | ## Declare a positional argument within an `arg-parse`.
|
129 | ##
|
130 | ## Examples:
|
131 | ##
|
132 | ## arg-parse (&spec) {
|
133 | ## arg name
|
134 | ## arg config (help="config file path")
|
135 | ## }
|
136 |
|
137 | ctx emit args ({name, help})
|
138 | }
|
139 |
|
140 | proc rest (name) {
|
141 | ## Take the remaining positional arguments within an `arg-parse`.
|
142 | ##
|
143 | ## Examples:
|
144 | ##
|
145 | ## arg-parse (&grepSpec) {
|
146 | ## arg query
|
147 | ## rest files
|
148 | ## }
|
149 |
|
150 | # We emit instead of set to detect multiple invocations of "rest"
|
151 | ctx emit rest (name)
|
152 | }
|
153 |
|
154 | func parseArgs(spec, argv) {
|
155 | ## Given a spec created by `parser`. Parse an array of strings `argv` per
|
156 | ## that spec.
|
157 | ##
|
158 | ## See `parser` for examples of use.
|
159 |
|
160 | var i = 0
|
161 | var positionalPos = 0
|
162 | var argc = len(argv)
|
163 | var args = {}
|
164 | var rest = []
|
165 |
|
166 | var value
|
167 | var found
|
168 | while (i < argc) {
|
169 | var arg = argv[i]
|
170 | if (arg.startsWith('-')) {
|
171 | setvar found = false
|
172 |
|
173 | for flag in (spec.flags) {
|
174 | if ( (flag.short and flag.short === arg) or
|
175 | (flag.long and flag.long === arg) ) {
|
176 | if (flag.type === null or flag.type is Bool) {
|
177 | setvar value = true
|
178 | } elif (flag.type is Int) {
|
179 | setvar i += 1
|
180 | if (i >= len(argv)) {
|
181 | error "Expected Int after '$arg'" (code=2)
|
182 | }
|
183 |
|
184 | try { setvar value = int(argv[i]) }
|
185 | if (_status !== 0) {
|
186 | error "Expected Int after '$arg', got '$[argv[i]]'" (code=2)
|
187 | }
|
188 | } elif (flag.type is Float) {
|
189 | setvar i += 1
|
190 | if (i >= len(argv)) {
|
191 | error "Expected Float after '$arg'" (code=2)
|
192 | }
|
193 |
|
194 | try { setvar value = float(argv[i]) }
|
195 | if (_status !== 0) {
|
196 | error "Expected Float after '$arg', got '$[argv[i]]'" (code=2)
|
197 | }
|
198 | } elif (flag.type is Str) {
|
199 | setvar i += 1
|
200 | if (i >= len(argv)) {
|
201 | error "Expected Str after '$arg'" (code=2)
|
202 | }
|
203 |
|
204 | setvar value = argv[i]
|
205 | }
|
206 |
|
207 | setvar args[flag.name] = value
|
208 | setvar found = true
|
209 | break
|
210 | }
|
211 | }
|
212 |
|
213 | if (not found) {
|
214 | error "Unknown flag '$arg'" (code=2)
|
215 | }
|
216 | } elif (positionalPos >= len(spec.args)) {
|
217 | if (not spec.rest) {
|
218 | error "Too many arguments, unexpected '$arg'" (code=2)
|
219 | }
|
220 |
|
221 | call rest->append(arg)
|
222 | } else {
|
223 | var pos = spec.args[positionalPos]
|
224 | setvar positionalPos += 1
|
225 | setvar value = arg
|
226 | setvar args[pos.name] = value
|
227 | }
|
228 |
|
229 | setvar i += 1
|
230 | }
|
231 |
|
232 | if (spec.rest) {
|
233 | setvar args[spec.rest] = rest
|
234 | }
|
235 |
|
236 | # Set defaults for flags
|
237 | for flag in (spec.flags) {
|
238 | if (flag.name not in args) {
|
239 | setvar args[flag.name] = flag.default
|
240 | }
|
241 | }
|
242 |
|
243 | # Raise error on missing args
|
244 | for arg in (spec.args) {
|
245 | if (arg.name not in args) {
|
246 | error "Usage Error: Missing required argument $[arg.name]" (code=2)
|
247 | }
|
248 | }
|
249 |
|
250 | return (args)
|
251 | }
|