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

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