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 | + |