1 | # args.ysh
|
2 | #
|
3 | # Usage:
|
4 | # source --builtin args.sh
|
5 | #
|
6 | # parser (&spec) {
|
7 | # flag -v --verbose (help="Verbosely") # default is Bool, false
|
8 | #
|
9 | # flag -P --max-procs ('int', default=-1, doc='''
|
10 | # Run at most P processes at a time
|
11 | # ''')
|
12 | #
|
13 | # flag -i --invert ('bool', default=true, doc='''
|
14 | # Long multiline
|
15 | # Description
|
16 | # ''')
|
17 | #
|
18 | # arg src (help='Source')
|
19 | # arg dest (help='Dest')
|
20 | # arg times (help='Foo')
|
21 | #
|
22 | # rest files
|
23 | # }
|
24 | #
|
25 | # var args = parseArgs(spec, ARGV)
|
26 | #
|
27 | # echo "Verbose $[args.verbose]"
|
28 |
|
29 | # TODO: See list
|
30 | # - It would be nice to keep `flag` and `arg` private, injecting them into the
|
31 | # proc namespace only within `Args`
|
32 | # - We need "type object" to replace the strings 'int', 'bool', etc.
|
33 | # - flag builtin:
|
34 | # - handle only long flag or only short flag
|
35 | # - flag aliases
|
36 |
|
37 | proc parser (; place ; ; block_def) {
|
38 | ## Create an args spec which can be passed to parseArgs.
|
39 | ##
|
40 | ## Example:
|
41 | ##
|
42 | ## # NOTE: &spec will create a variable named spec
|
43 | ## parser (&spec) {
|
44 | ## flag -v --verbose ('bool')
|
45 | ## }
|
46 | ##
|
47 | ## var args = parseArgs(spec, ARGV)
|
48 |
|
49 | var p = {flags: [], args: []}
|
50 | ctx push (p; ; block_def)
|
51 |
|
52 | # Validate that p.rest = [name] or null and reduce p.rest into name or null.
|
53 | if ('rest' in p) {
|
54 | if (len(p.rest) > 1) {
|
55 | error '`rest` was called more than once' (code=3)
|
56 | } else {
|
57 | setvar p.rest = p.rest[0]
|
58 | }
|
59 | } else {
|
60 | setvar p.rest = null
|
61 | }
|
62 |
|
63 | var names = {}
|
64 | for items in ([p.flags, p.args]) {
|
65 | for x in (items) {
|
66 | if (x.name in names) {
|
67 | error "Duplicate flag/arg name $[x.name] in spec" (code=3)
|
68 | }
|
69 |
|
70 | setvar names[x.name] = null
|
71 | }
|
72 | }
|
73 |
|
74 | # TODO: what about `flag --name` and then `arg name`?
|
75 |
|
76 | call place->setValue(p)
|
77 | }
|
78 |
|
79 | proc flag (short, long ; type='bool' ; default=null, help=null) {
|
80 | ## Declare a flag within an `arg-parse`.
|
81 | ##
|
82 | ## Examples:
|
83 | ##
|
84 | ## arg-parse (&spec) {
|
85 | ## flag -v --verbose
|
86 | ## flag -n --count ('int', default=1)
|
87 | ## flag -f --file ('str', help="File to process")
|
88 | ## }
|
89 |
|
90 | # bool has a default of false, not null
|
91 | if (type === 'bool' and default === null) {
|
92 | setvar default = false
|
93 | }
|
94 |
|
95 | # TODO: validate `type`
|
96 |
|
97 | # TODO: Should use "trimPrefix"
|
98 | var name = long[2:]
|
99 |
|
100 | ctx emit flags ({short, long, name, type, default, help})
|
101 | }
|
102 |
|
103 | proc arg (name ; ; help=null) {
|
104 | ## Declare a positional argument within an `arg-parse`.
|
105 | ##
|
106 | ## Examples:
|
107 | ##
|
108 | ## arg-parse (&spec) {
|
109 | ## arg name
|
110 | ## arg config (help="config file path")
|
111 | ## }
|
112 |
|
113 | ctx emit args ({name, help})
|
114 | }
|
115 |
|
116 | proc rest (name) {
|
117 | ## Take the remaining positional arguments within an `arg-parse`.
|
118 | ##
|
119 | ## Examples:
|
120 | ##
|
121 | ## arg-parse (&grepSpec) {
|
122 | ## arg query
|
123 | ## rest files
|
124 | ## }
|
125 |
|
126 | # We emit instead of set to detect multiple invocations of "rest"
|
127 | ctx emit rest (name)
|
128 | }
|
129 |
|
130 | func parseArgs(spec, argv) {
|
131 | ## Given a spec created by `parser`. Parse an array of strings `argv` per
|
132 | ## that spec.
|
133 | ##
|
134 | ## See `parser` for examples of use.
|
135 |
|
136 | var i = 0
|
137 | var positionalPos = 0
|
138 | var argc = len(argv)
|
139 | var args = {}
|
140 | var rest = []
|
141 |
|
142 | var value
|
143 | var found
|
144 | while (i < argc) {
|
145 | var arg = argv[i]
|
146 | if (arg.startsWith('-')) {
|
147 | setvar found = false
|
148 |
|
149 | for flag in (spec.flags) {
|
150 | if ( (flag.short and flag.short === arg) or
|
151 | (flag.long and flag.long === arg) ) {
|
152 | case (flag.type) {
|
153 | ('bool') | (null) { setvar value = true }
|
154 | int {
|
155 | setvar i += 1
|
156 | if (i >= len(argv)) {
|
157 | error "Expected integer after '$arg'" (code=2)
|
158 | }
|
159 |
|
160 | try { setvar value = int(argv[i]) }
|
161 | if (_status !== 0) {
|
162 | error "Expected integer after '$arg', got '$[argv[i]]'" (code=2)
|
163 | }
|
164 | }
|
165 | }
|
166 |
|
167 | setvar args[flag.name] = value
|
168 | setvar found = true
|
169 | break
|
170 | }
|
171 | }
|
172 |
|
173 | if (not found) {
|
174 | error "Unknown flag '$arg'" (code=2)
|
175 | }
|
176 | } elif (positionalPos >= len(spec.args)) {
|
177 | if (not spec.rest) {
|
178 | error "Too many arguments, unexpected '$arg'" (code=2)
|
179 | }
|
180 |
|
181 | call rest->append(arg)
|
182 | } else {
|
183 | var pos = spec.args[positionalPos]
|
184 | setvar positionalPos += 1
|
185 | setvar value = arg
|
186 | setvar args[pos.name] = value
|
187 | }
|
188 |
|
189 | setvar i += 1
|
190 | }
|
191 |
|
192 | if (spec.rest) {
|
193 | setvar args[spec.rest] = rest
|
194 | }
|
195 |
|
196 | # Set defaults for flags
|
197 | for flag in (spec.flags) {
|
198 | if (flag.name not in args) {
|
199 | setvar args[flag.name] = flag.default
|
200 | }
|
201 | }
|
202 |
|
203 | # Raise error on missing args
|
204 | for arg in (spec.args) {
|
205 | if (arg.name not in args) {
|
206 | error "Usage Error: Missing required argument $[arg.name]" (code=2)
|
207 | }
|
208 | }
|
209 |
|
210 | return (args)
|
211 | }
|