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

251 lines, 136 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# - support repeated or character-delimited multi-value flags
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, Int, Str]
84const kValidTypeNames = []
85for vt in (kValidTypes) {
86 call kValidTypeNames->append(vt.name)
87}
88
89func 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
100proc 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
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 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}