Commit 85eedc501c1a4b6e5ef8eb62c76011ae2f0c7ea1

Authored by Göran Krampe
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 +