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

215 lines, 101 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# - 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
41proc 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
83proc 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
107proc 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
120proc 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
134func 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}