1 | # args.ysh
|
2 | #
|
3 | # Usage:
|
4 | # source --builtin args.sh
|
5 |
|
6 | const __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 |
|
40 | proc 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 |
|
82 | const kValidTypes = [Bool, Float, Int, Str]
|
83 | const kValidTypeNames = []
|
84 | for vt in (kValidTypes) {
|
85 | call kValidTypeNames->append(vt.name)
|
86 | }
|
87 |
|
88 | func 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 |
|
99 | proc 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 |
|
126 | proc 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 |
|
139 | proc 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 |
|
153 | func 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 | }
|