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

211 lines, 100 significant
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
37proc 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
79proc 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
103proc 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
116proc 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
130func 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}