OILS / stdlib / ysh / args.ysh View on Github | oils.pub

291 lines, 170 significant
1# args.ysh
2#
3# Usage:
4# source --builtin args.sh
5
6const __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# - assert that default value has the declared type
38
39proc 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
83const kValidTypes = [Bool, Float, List[Float], Int, List[Int], Str, List[Str]]
84const kValidTypeNames = []
85for vt in (kValidTypes) {
86 var name = vt.name if ('name' in propView(vt)) else vt.unique_id
87 call kValidTypeNames->append(name)
88}
89
90func isValidType (type) {
91 for valid in (kValidTypes) {
92 if (type is valid) {
93 return (true)
94 }
95 }
96 return (false)
97}
98
99proc flag (short, long ; type=Bool ; default=null, help=null) {
100 ## Declare a flag within an `arg-parse`.
101 ##
102 ## Examples:
103 ##
104 ## arg-parse (&spec) {
105 ## flag -v --verbose
106 ## flag -n --count (Int, default=1)
107 ## flag -p --percent (Float, default=0.0)
108 ## flag -f --file (Str, help="File to process")
109 ## flag -e --exclude (List[Str], help="File to exclude")
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
127proc 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
140proc 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
154func 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 var escape_remaining = false
169 while (i < argc) {
170 var arg = argv[i]
171
172 if (escape_remaining) {
173 call rest->append(arg)
174 } elif (arg === '--') {
175 if (not spec.rest) {
176 error "Unexpected '--' argument - extraneous positional arguments are prohibited" (code=2)
177 }
178
179 setvar escape_remaining = true
180 } elif (arg.startsWith('-')) {
181 setvar found = false
182
183 for flag in (spec.flags) {
184 if ( (flag.short and flag.short === arg) or
185 (flag.long and flag.long === arg) ) {
186 if (flag.type === null or flag.type is Bool) {
187 setvar value = true
188 } elif (flag.type is Int) {
189 setvar i += 1
190 if (i >= len(argv)) {
191 error "Expected Int after '$arg'" (code=2)
192 }
193
194 try { setvar value = int(argv[i]) }
195 if failed {
196 error "Expected Int after '$arg', got '$[argv[i]]'" (code=2)
197 }
198 } elif (flag.type is List[Int]) {
199 setvar i += 1
200 if (i >= len(argv)) {
201 error "Expected Int after '$arg'" (code=2)
202 }
203
204 setvar value = get(args, flag.name, [])
205 try { call value->append(int(argv[i])) }
206 if failed {
207 error "Expected Int after '$arg', got '$[argv[i]]'" (code=2)
208 }
209 } elif (flag.type is Float) {
210 setvar i += 1
211 if (i >= len(argv)) {
212 error "Expected Float after '$arg'" (code=2)
213 }
214
215 try { setvar value = float(argv[i]) }
216 if failed {
217 error "Expected Float after '$arg', got '$[argv[i]]'" (code=2)
218 }
219 } elif (flag.type is List[Float]) {
220 setvar i += 1
221 if (i >= len(argv)) {
222 error "Expected Float after '$arg'" (code=2)
223 }
224
225 setvar value = get(args, flag.name, [])
226 try { call value->append(float(argv[i])) }
227 if failed {
228 error "Expected Float after '$arg', got '$[argv[i]]'" (code=2)
229 }
230 } elif (flag.type is Str) {
231 setvar i += 1
232 if (i >= len(argv)) {
233 error "Expected Str after '$arg'" (code=2)
234 }
235
236 setvar value = argv[i]
237 } elif (flag.type is List[Str]) {
238 setvar i += 1
239 if (i >= len(argv)) {
240 error "Expected Str after '$arg'" (code=2)
241 }
242
243 setvar value = get(args, flag.name, [])
244 call value->append(argv[i])
245 }
246
247 setvar args[flag.name] = value
248 setvar found = true
249 break
250 }
251 }
252
253 if (not found) {
254 error "Unknown flag '$arg'" (code=2)
255 }
256 } elif (positionalPos >= len(spec.args)) {
257 if (not spec.rest) {
258 error "Too many arguments, unexpected '$arg'" (code=2)
259 }
260
261 call rest->append(arg)
262 } else {
263 var pos = spec.args[positionalPos]
264 setvar positionalPos += 1
265 setvar value = arg
266 setvar args[pos.name] = value
267 }
268
269 setvar i += 1
270 }
271
272 if (spec.rest) {
273 setvar args[spec.rest] = rest
274 }
275
276 # Set defaults for flags
277 for flag in (spec.flags) {
278 if (flag.name not in args) {
279 setvar args[flag.name] = flag.default
280 }
281 }
282
283 # Raise error on missing args
284 for arg in (spec.args) {
285 if (arg.name not in args) {
286 error "Usage Error: Missing required argument $[arg.name]" (code=2)
287 }
288 }
289
290 return (args)
291}