OILS / stdlib / ysh / args.ysh View on Github | oilshell.org

281 lines, 162 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 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 List[Int]) {
189 setvar i += 1
190 if (i >= len(argv)) {
191 error "Expected Int after '$arg'" (code=2)
192 }
193
194 setvar value = args[flag.name] if (flag.name in args) else []
195 try { call value->append(int(argv[i])) }
196 if (_status !== 0) {
197 error "Expected Int after '$arg', got '$[argv[i]]'" (code=2)
198 }
199 } elif (flag.type is Float) {
200 setvar i += 1
201 if (i >= len(argv)) {
202 error "Expected Float after '$arg'" (code=2)
203 }
204
205 try { setvar value = float(argv[i]) }
206 if (_status !== 0) {
207 error "Expected Float after '$arg', got '$[argv[i]]'" (code=2)
208 }
209 } elif (flag.type is List[Float]) {
210 setvar i += 1
211 if (i >= len(argv)) {
212 error "Expected Float after '$arg'" (code=2)
213 }
214
215 setvar value = args[flag.name] if (flag.name in args) else []
216 try { call value->append(float(argv[i])) }
217 if (_status !== 0) {
218 error "Expected Float after '$arg', got '$[argv[i]]'" (code=2)
219 }
220 } elif (flag.type is Str) {
221 setvar i += 1
222 if (i >= len(argv)) {
223 error "Expected Str after '$arg'" (code=2)
224 }
225
226 setvar value = argv[i]
227 } elif (flag.type is List[Str]) {
228 setvar i += 1
229 if (i >= len(argv)) {
230 error "Expected Str after '$arg'" (code=2)
231 }
232
233 setvar value = args[flag.name] if (flag.name in args) else []
234 call value->append(argv[i])
235 }
236
237 setvar args[flag.name] = value
238 setvar found = true
239 break
240 }
241 }
242
243 if (not found) {
244 error "Unknown flag '$arg'" (code=2)
245 }
246 } elif (positionalPos >= len(spec.args)) {
247 if (not spec.rest) {
248 error "Too many arguments, unexpected '$arg'" (code=2)
249 }
250
251 call rest->append(arg)
252 } else {
253 var pos = spec.args[positionalPos]
254 setvar positionalPos += 1
255 setvar value = arg
256 setvar args[pos.name] = value
257 }
258
259 setvar i += 1
260 }
261
262 if (spec.rest) {
263 setvar args[spec.rest] = rest
264 }
265
266 # Set defaults for flags
267 for flag in (spec.flags) {
268 if (flag.name not in args) {
269 setvar args[flag.name] = flag.default
270 }
271 }
272
273 # Raise error on missing args
274 for arg in (spec.args) {
275 if (arg.name not in args) {
276 error "Usage Error: Missing required argument $[arg.name]" (code=2)
277 }
278 }
279
280 return (args)
281}