Commit 85eedc501c1a4b6e5ef8eb62c76011ae2f0c7ea1
1 parent
e914743a
Added lapp.nim to get build working.
Showing
1 changed file
with
379 additions
and
0 deletions
lapp.nim
0 → 100644
1 | +import strutils | |
2 | +from os import paramCount, paramStr | |
3 | +import tables | |
4 | +export tables.`[]` | |
5 | + | |
6 | +#### Simple string lexer ### | |
7 | +type | |
8 | + PLexer = ref TLexer | |
9 | + TLexer = object | |
10 | + str: string | |
11 | + idx: int | |
12 | + TLexType = enum | |
13 | + tend | |
14 | + tword | |
15 | + tint | |
16 | + tfloat | |
17 | + trange | |
18 | + telipsis | |
19 | + tchar | |
20 | + | |
21 | +proc thisChar(L: PLexer):char = L.str[L.idx] | |
22 | +proc next(L: PLexer) = L.idx += 1 | |
23 | + | |
24 | +proc skipws(L: PLexer) = | |
25 | + while thisChar(L) in Whitespace: next(L) | |
26 | + | |
27 | +proc get(L: PLexer; t: var TLexType): string = | |
28 | + skipws(L) | |
29 | + let c = thisChar(L) | |
30 | + t = tend | |
31 | + if c == '\0': return nil | |
32 | + result = "" | |
33 | + result.add(c) | |
34 | + next(L) | |
35 | + t = tchar | |
36 | + case c | |
37 | + of '-': # '-", "--" | |
38 | + if thisChar(L) == '-': | |
39 | + result.add('-') | |
40 | + next(L) | |
41 | + of Letters: # word | |
42 | + t = tword | |
43 | + while thisChar(L) in Letters: | |
44 | + result.add(thisChar(L)) | |
45 | + next(L) | |
46 | + of Digits: # number | |
47 | + t = tint | |
48 | + while thisChar(L) in Digits: | |
49 | + result.add(thisChar(L)) | |
50 | + next(L) | |
51 | + if thisChar(L) == '.': | |
52 | + t = tfloat | |
53 | + result.add(c) | |
54 | + next(L) | |
55 | + while thisChar(L) in Digits: | |
56 | + result.add(c) | |
57 | + next(L) | |
58 | + of '.': # ".", "..", "..." | |
59 | + if thisChar(L) == '.': | |
60 | + t = trange | |
61 | + result.add('.') | |
62 | + next(L) | |
63 | + if thisChar(L) == '.': | |
64 | + t = telipsis | |
65 | + result.add('.') | |
66 | + next(L) | |
67 | + else: discard | |
68 | + | |
69 | +proc get(L: PLexer): string = | |
70 | + var t: TLexType | |
71 | + get(L,t) | |
72 | + | |
73 | +proc reset(L: PLexer, s: string) = | |
74 | + L.str = s | |
75 | + L.idx = 0 | |
76 | + | |
77 | +proc newLexer(s: string): PLexer = | |
78 | + new(result) | |
79 | + result.reset(s) | |
80 | + | |
81 | +### a container for values ### | |
82 | + | |
83 | +type | |
84 | + TValueKind = enum | |
85 | + vInt, | |
86 | + vFloat, | |
87 | + vString, | |
88 | + vBool, | |
89 | + vFile, | |
90 | + vSeq | |
91 | + | |
92 | + PValue = ref TValue | |
93 | + TValue = object | |
94 | + case kind: TValueKind | |
95 | + of vInt: asInt *: int | |
96 | + of vFloat: asFloat *: float | |
97 | + of vString: asString *: string | |
98 | + of vBool: asBool *: bool | |
99 | + of vFile: | |
100 | + asFile *: File | |
101 | + fileName *: string | |
102 | + of vSeq: asSeq *: seq[PValue] | |
103 | + | |
104 | +proc boolValue(c: bool): PValue = PValue(kind: vBool, asBool: c) | |
105 | + | |
106 | +proc fileValue(f: File, name: string): PValue = PValue(kind: vFile, asFile: f, fileName: name) | |
107 | + | |
108 | +proc strValue(s: string): PValue = PValue(kind: vString, asString: s) | |
109 | + | |
110 | +proc intValue(v: int): PValue = PValue(kind: vInt, asInt: v) | |
111 | + | |
112 | +proc floatValue(v: float): PValue = PValue(kind: vFloat, asFloat: v) | |
113 | + | |
114 | +proc seqValue(v: seq[PValue]): PValue = PValue(kind: vSeq, asSeq: v) | |
115 | + | |
116 | +const MAX_FILES = 30 | |
117 | + | |
118 | +type | |
119 | + PSpec = ref TSpec | |
120 | + TSpec = object | |
121 | + defVal: string | |
122 | + ptype: string | |
123 | + needsValue, multiple, used: bool | |
124 | +var | |
125 | + progname, usage: string | |
126 | + aliases: array[char,string] | |
127 | + parm_spec = initTable[string,PSpec]() | |
128 | + | |
129 | +proc fail(msg: string) = | |
130 | + stderr.writeln(progname & ": " & msg) | |
131 | + quit(usage) | |
132 | + | |
133 | +proc parseSpec(u: string) = | |
134 | + var | |
135 | + L: PLexer | |
136 | + tok: string | |
137 | + k = 1 | |
138 | + | |
139 | + let lines = u.splitLines | |
140 | + L = newLexer(lines[0]) | |
141 | + progname = L.get | |
142 | + usage = u | |
143 | + for line in lines[1..(-1)]: | |
144 | + var | |
145 | + isarg = false | |
146 | + multiple = false | |
147 | + getnext = true | |
148 | + name: string | |
149 | + alias: char | |
150 | + L.reset(line) | |
151 | + tok = L.get | |
152 | + if tok == "-" or tok == "--": # flag | |
153 | + if tok == "-": #short flag | |
154 | + let flag = L.get | |
155 | + if len(flag) != 1: fail("short option has one character!") | |
156 | + tok = L.get | |
157 | + if tok == ",": # which is alias for long flag | |
158 | + tok = L.get | |
159 | + if tok != "--": fail("expecting long --flag") | |
160 | + name = L.get | |
161 | + alias = flag[0] | |
162 | + else: # only short flag | |
163 | + name = flag | |
164 | + alias = flag[0] | |
165 | + getnext = false | |
166 | + else: # only long flag | |
167 | + name = L.get | |
168 | + alias = '\0' | |
169 | + elif tok == "<": # argument | |
170 | + isarg = true | |
171 | + name = L.get | |
172 | + alias = chr(k) | |
173 | + k += 1 | |
174 | + tok = L.get | |
175 | + if tok != ">": fail("argument must be enclosed in <...>") | |
176 | + | |
177 | + if getnext: tok = L.get | |
178 | + if tok == ":": # allowed to have colon after flags | |
179 | + tok = L.get | |
180 | + if tok == nil: continue | |
181 | + # default types for flags and arguments | |
182 | + var | |
183 | + ftype = if isarg: "string" else: "bool" | |
184 | + defValue = "" | |
185 | + if tok == "(": # typed flag/argument | |
186 | + var t = tchar | |
187 | + tok = L.get(t) | |
188 | + if tok == "default": # type from default value | |
189 | + defValue = L.get(t) | |
190 | + if t == tint: ftype = "int" | |
191 | + elif t == tfloat: ftype = "float" | |
192 | + elif t == tword: | |
193 | + if defValue == "stdin": ftype = "infile" | |
194 | + elif defValue == "stdout": ftype = "outfile" | |
195 | + else: ftype = "string" | |
196 | + else: fail("unknown default value " & tok) | |
197 | + else: # explicit type | |
198 | + if t == tword: | |
199 | + ftype = tok | |
200 | + if tok == "bool": defValue = "false" | |
201 | + else: fail("unknown type " & tok) | |
202 | + discard L.get(t) | |
203 | + multiple = t == telipsis | |
204 | + elif ftype == "bool": # no type or default | |
205 | + defValue = "false" | |
206 | + | |
207 | + if name != nil: | |
208 | + let spec = PSpec(defVal:defValue, ptype: ftype, needsValue: ftype != "bool",multiple:multiple) | |
209 | + aliases[alias] = name | |
210 | + parm_spec[name] = spec | |
211 | + | |
212 | +proc tail(s: string): string = s[1..(-1)] | |
213 | + | |
214 | +var | |
215 | + files: array[1..MAX_FILES,File] | |
216 | + nfiles = 0 | |
217 | + | |
218 | +proc closeFiles() {.noconv.} = | |
219 | + if nfiles == 0: return | |
220 | + for i in 1..nfiles: files[i].close() | |
221 | + | |
222 | +proc parseArguments(usage: string, args: seq[string]): Table[string,PValue] = | |
223 | + var | |
224 | + vars = initTable[string,PValue]() | |
225 | + n = len(args) - 1 | |
226 | + i = 1 | |
227 | + k = 1 | |
228 | + flag,value, arg: string | |
229 | + info: PSpec | |
230 | + short: bool | |
231 | + flagvalues: seq[seq[string]] | |
232 | + | |
233 | + proc next(): string = | |
234 | + if i > n: fail("an option required a value!") | |
235 | + result = args[i] | |
236 | + i += 1 | |
237 | + | |
238 | + proc get_alias(c: char): string = | |
239 | + result = aliases[c] | |
240 | + if result == nil: | |
241 | + n = ord(c) | |
242 | + if n < 20: | |
243 | + fail("no such argument: " & $n) | |
244 | + else: | |
245 | + fail("no such option: " & c) | |
246 | + | |
247 | + proc get_spec(name: string): PSpec = | |
248 | + result = parm_spec[name] | |
249 | + if result == nil: | |
250 | + fail("no such option: " & name) | |
251 | + | |
252 | + newSeq(flagvalues, 0) | |
253 | + parseSpec(usage) | |
254 | + addQuitProc(closeFiles) | |
255 | + | |
256 | + # parse the flags and arguments | |
257 | + while i <= n: | |
258 | + arg = next() | |
259 | + if arg[0] == '-': #flag | |
260 | + short = arg[1] != '-' | |
261 | + arg = arg.tail | |
262 | + if short: # all short args are aliases, even if only to themselves | |
263 | + flag = get_alias(arg[0]) | |
264 | + else: | |
265 | + flag = arg[1..high(arg)] | |
266 | + info = get_spec(flag) | |
267 | + if info.needsValue: | |
268 | + if short and len(arg) > 1: # value can follow short flag | |
269 | + value = arg.tail | |
270 | + else: # grab next argument | |
271 | + value = next() | |
272 | + else: | |
273 | + value = "true" | |
274 | + if short and len(arg) > 0: # short flags can be combined | |
275 | + for c in arg.tail: | |
276 | + let f = get_alias(c) | |
277 | + let i = get_spec(f) | |
278 | + if i.needsValue: fail("needs value! " & f) | |
279 | + flagvalues.add(@[f,"true"]) | |
280 | + i.used = true | |
281 | + else: # argument (stored as \001, \002, etc | |
282 | + flag = get_alias(chr(k)) | |
283 | + value = arg | |
284 | + info = get_spec(flag) | |
285 | + # don't move on if this is a varags last param | |
286 | + if not info.multiple: k += 1 | |
287 | + flagvalues.add(@[flag,value]) | |
288 | + info.used = true | |
289 | + | |
290 | + # any flags not mentioned? | |
291 | + for flag,info in parm_spec: | |
292 | + if not info.used: | |
293 | + if info.defVal == "": # no default! | |
294 | + fail("required option or argument missing: " & flag) | |
295 | + flagvalues.add(@[flag,info.defVal]) | |
296 | + | |
297 | + # cool, we have the info, can convert known flags | |
298 | + for item in flagvalues: | |
299 | + var pval: PValue; | |
300 | + let | |
301 | + flag = item[0] | |
302 | + value = item[1] | |
303 | + info = get_spec(flag) | |
304 | + case info.ptype | |
305 | + of "int": | |
306 | + var v: int | |
307 | + try: | |
308 | + v = value.parseInt | |
309 | + except: | |
310 | + fail("bad integer") | |
311 | + pval = intValue(v) | |
312 | + of "float": | |
313 | + var v: float | |
314 | + try: | |
315 | + v = value.parseFloat | |
316 | + except: | |
317 | + fail("bad integer") | |
318 | + pval = floatValue(v) | |
319 | + of "bool": | |
320 | + pval = boolValue(value.parseBool) | |
321 | + of "string": | |
322 | + pval = strValue(value) | |
323 | + of "infile","outfile": # we open files for the app... | |
324 | + var f: File | |
325 | + try: | |
326 | + if info.ptype == "infile": | |
327 | + f = if value=="stdin": stdin else: open(value,fmRead) | |
328 | + else: | |
329 | + f = if value=="stdout": stdout else: open(value,fmWrite) | |
330 | + # they will be closed automatically on program exit | |
331 | + nfiles += 1 | |
332 | + if nfiles <= MAX_FILES: files[nfiles] = f | |
333 | + except: | |
334 | + fail("cannot open " & value) | |
335 | + pval = fileValue(f,value) | |
336 | + else: discard | |
337 | + | |
338 | + var oval = vars[flag] | |
339 | + if info.multiple: # multiple flags are sequence values | |
340 | + if oval == nil: # first value! | |
341 | + pval = seqValue(@[pval]) | |
342 | + else: # just add to existing sequence | |
343 | + oval.asSeq.add(pval) | |
344 | + pval = oval | |
345 | + elif oval != nil: # cannot repeat a single flag! | |
346 | + fail("cannot use '" & flag & "' more than once") | |
347 | + vars[flag] = pval | |
348 | + | |
349 | + return vars | |
350 | + | |
351 | +proc parse*(usage: string): Table[string,PValue] = | |
352 | + var | |
353 | + args: seq[string] | |
354 | + n = paramCount() | |
355 | + newSeq(args,n+1) | |
356 | + for i in 0..n: | |
357 | + args[i] = paramStr(i) | |
358 | + return parseArguments(usage,args) | |
359 | + | |
360 | +when isMainModule: | |
361 | + var args = parse""" | |
362 | + head [flags] filename | |
363 | + -n: (default 10) number of lines | |
364 | + -v,--verbose: (bool...) verbosity level | |
365 | + -a,--alpha useless parm | |
366 | + <file>: (default stdin...) | |
367 | + |<out>: (default stdout) | |
368 | + """ | |
369 | + | |
370 | + echo args["n"].asInt | |
371 | + echo args["alpha"].asBool | |
372 | + | |
373 | + for v in args["verbose"].asSeq: | |
374 | + echo "got ",v.asBool | |
375 | + | |
376 | + let myfiles = args["files"].asSeq | |
377 | + for f in myfiles: | |
378 | + echo f.asFile.readLine() | |
379 | + | ... | ... |