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