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 | # - We need "type object" to replace the strings 'int', 'bool', etc.
|
37 | # - flag builtin:
|
38 | # - handle only long flag or only short flag
|
39 | # - flag aliases
|
40 |
|
41 | proc parser (; place ; ; block_def) {
|
42 | ## Create an args spec which can be passed to parseArgs.
|
43 | ##
|
44 | ## Example:
|
45 | ##
|
46 | ## # NOTE: &spec will create a variable named spec
|
47 | ## parser (&spec) {
|
48 | ## flag -v --verbose ('bool')
|
49 | ## }
|
50 | ##
|
51 | ## var args = parseArgs(spec, ARGV)
|
52 |
|
53 | var p = {flags: [], args: []}
|
54 | ctx push (p; ; block_def)
|
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 |
|
83 | proc flag (short, long ; type='bool' ; default=null, help=null) {
|
84 | ## Declare a flag within an `arg-parse`.
|
85 | ##
|
86 | ## Examples:
|
87 | ##
|
88 | ## arg-parse (&spec) {
|
89 | ## flag -v --verbose
|
90 | ## flag -n --count ('int', default=1)
|
91 | ## flag -f --file ('str', help="File to process")
|
92 | ## }
|
93 |
|
94 | # bool has a default of false, not null
|
95 | if (type === 'bool' and default === null) {
|
96 | setvar default = false
|
97 | }
|
98 |
|
99 | # TODO: validate `type`
|
100 |
|
101 | # TODO: Should use "trimPrefix"
|
102 | var name = long[2:]
|
103 |
|
104 | ctx emit flags ({short, long, name, type, default, help})
|
105 | }
|
106 |
|
107 | proc arg (name ; ; help=null) {
|
108 | ## Declare a positional argument within an `arg-parse`.
|
109 | ##
|
110 | ## Examples:
|
111 | ##
|
112 | ## arg-parse (&spec) {
|
113 | ## arg name
|
114 | ## arg config (help="config file path")
|
115 | ## }
|
116 |
|
117 | ctx emit args ({name, help})
|
118 | }
|
119 |
|
120 | proc rest (name) {
|
121 | ## Take the remaining positional arguments within an `arg-parse`.
|
122 | ##
|
123 | ## Examples:
|
124 | ##
|
125 | ## arg-parse (&grepSpec) {
|
126 | ## arg query
|
127 | ## rest files
|
128 | ## }
|
129 |
|
130 | # We emit instead of set to detect multiple invocations of "rest"
|
131 | ctx emit rest (name)
|
132 | }
|
133 |
|
134 | func parseArgs(spec, argv) {
|
135 | ## Given a spec created by `parser`. Parse an array of strings `argv` per
|
136 | ## that spec.
|
137 | ##
|
138 | ## See `parser` for examples of use.
|
139 |
|
140 | var i = 0
|
141 | var positionalPos = 0
|
142 | var argc = len(argv)
|
143 | var args = {}
|
144 | var rest = []
|
145 |
|
146 | var value
|
147 | var found
|
148 | while (i < argc) {
|
149 | var arg = argv[i]
|
150 | if (arg.startsWith('-')) {
|
151 | setvar found = false
|
152 |
|
153 | for flag in (spec.flags) {
|
154 | if ( (flag.short and flag.short === arg) or
|
155 | (flag.long and flag.long === arg) ) {
|
156 | case (flag.type) {
|
157 | ('bool') | (null) { setvar value = true }
|
158 | int {
|
159 | setvar i += 1
|
160 | if (i >= len(argv)) {
|
161 | error "Expected integer after '$arg'" (code=2)
|
162 | }
|
163 |
|
164 | try { setvar value = int(argv[i]) }
|
165 | if (_status !== 0) {
|
166 | error "Expected integer after '$arg', got '$[argv[i]]'" (code=2)
|
167 | }
|
168 | }
|
169 | }
|
170 |
|
171 | setvar args[flag.name] = value
|
172 | setvar found = true
|
173 | break
|
174 | }
|
175 | }
|
176 |
|
177 | if (not found) {
|
178 | error "Unknown flag '$arg'" (code=2)
|
179 | }
|
180 | } elif (positionalPos >= len(spec.args)) {
|
181 | if (not spec.rest) {
|
182 | error "Too many arguments, unexpected '$arg'" (code=2)
|
183 | }
|
184 |
|
185 | call rest->append(arg)
|
186 | } else {
|
187 | var pos = spec.args[positionalPos]
|
188 | setvar positionalPos += 1
|
189 | setvar value = arg
|
190 | setvar args[pos.name] = value
|
191 | }
|
192 |
|
193 | setvar i += 1
|
194 | }
|
195 |
|
196 | if (spec.rest) {
|
197 | setvar args[spec.rest] = rest
|
198 | }
|
199 |
|
200 | # Set defaults for flags
|
201 | for flag in (spec.flags) {
|
202 | if (flag.name not in args) {
|
203 | setvar args[flag.name] = flag.default
|
204 | }
|
205 | }
|
206 |
|
207 | # Raise error on missing args
|
208 | for arg in (spec.args) {
|
209 | if (arg.name not in args) {
|
210 | error "Usage Error: Missing required argument $[arg.name]" (code=2)
|
211 | }
|
212 | }
|
213 |
|
214 | return (args)
|
215 | }
|