aboutsummaryrefslogtreecommitdiff
path: root/src/args.nim
diff options
context:
space:
mode:
Diffstat (limited to 'src/args.nim')
-rw-r--r--src/args.nim175
1 files changed, 175 insertions, 0 deletions
diff --git a/src/args.nim b/src/args.nim
new file mode 100644
index 0000000..12b31b3
--- /dev/null
+++ b/src/args.nim
@@ -0,0 +1,175 @@
+import
+ future, options, os, posix, sequtils, sets, strutils,
+ utils
+
+type
+ ArgumentType* {.pure.} = enum
+ short, long, target
+
+ Argument* = tuple[
+ key: string,
+ value: Option[string],
+ atype: ArgumentType
+ ]
+
+ OptionPair* = tuple[
+ short: Option[string],
+ long: string
+ ]
+
+ OptionKey* = tuple[
+ key: string,
+ long: bool
+ ]
+
+iterator readLines(): string =
+ try:
+ while true:
+ yield readLine(stdin)
+ except:
+ discard
+
+iterator splitSingle(valueFull: string, optionsWithParameter: HashSet[OptionKey],
+ next: Option[string]): tuple[key: string, value: Option[string], consumedNext: bool] =
+ var i = 0
+ while i < valueFull.len:
+ let key = $valueFull[i]
+ if (key, false) in optionsWithParameter:
+ if i == valueFull.high:
+ if next.isNone:
+ raise commandError(trc("%s: option requires an argument -- '%c'\n").strip
+ .replace("%s", "$#").replace("%c", "$#") % [getAppFilename(), key],
+ showError = false)
+ else:
+ yield (key, next, true)
+ else:
+ yield (key, some(valueFull[i + 1 .. ^1]), false)
+ i = valueFull.len
+ else:
+ yield (key, none(string), false)
+ i += 1
+
+proc splitArgs*(params: seq[string],
+ optionsWithParameter: HashSet[OptionKey]): seq[Argument] =
+ proc handleCurrentNext(current: string, next: Option[string],
+ stdinConsumed: bool, endOfOpts: bool): (seq[Argument], Option[string], bool, bool) =
+ if current == "-":
+ if stdinConsumed or isatty(0) == 1:
+ raise commandError(trp("argument '-' specified without input on stdin\n").strip)
+ else:
+ let args = lc[x | (y <- readLines(), x <- y.splitWhitespace), string]
+ .map(s => (s, none(string), ArgumentType.target))
+
+ return (args, next, true, endOfOpts)
+ elif endOfOpts:
+ return (@[(current, none(string), ArgumentType.target)], next, stdinConsumed, true)
+ elif current == "--":
+ return (@[], next, stdinConsumed, true)
+ elif current[0 .. 1] == "--":
+ let valueFull = current[2 .. ^1]
+ let index = valueFull.find("=")
+ let key = if index >= 0: valueFull[0 .. index - 1] else: valueFull
+ let valueOption = if index >= 0: some(valueFull[index + 1 .. ^1]) else: none(string)
+
+ if (key, true) in optionsWithParameter:
+ if valueOption.isSome:
+ return (@[(key, valueOption, ArgumentType.long)], next, stdinConsumed, false)
+ elif next.isSome:
+ return (@[(key, next, ArgumentType.long)], none(string), stdinConsumed, false)
+ else:
+ raise commandError(trc("%s: option '%s%s' requires an argument\n").strip
+ .replace("%s", "$#") % [getAppFilename(), "--", key], showError = false)
+ elif valueOption.isSome:
+ raise commandError(trc("%s: option '%s%s' doesn't allow an argument\n").strip
+ .replace("%s", "$#") % [getAppFilename(), "--", key], showError = false)
+ else:
+ return (@[(key, none(string), ArgumentType.long)], next, stdinConsumed, false)
+ elif current[0] == '-' and current.len >= 2:
+ let argsResult = toSeq(splitSingle(current[1 .. ^1], optionsWithParameter, next))
+ let consumedNext = argsResult.map(a => a.consumedNext).foldl(a or b)
+ let newNext = next.filter(n => not consumedNext)
+
+ return (lc[(x.key, x.value, ArgumentType.short) | (x <- argsResult), Argument],
+ newNext, stdinConsumed, false)
+ else:
+ return (@[(current, none(string), ArgumentType.target)], next, stdinConsumed, false)
+
+ type
+ ParseArgument = tuple[arg: Option[Argument],current: Option[string]]
+ ParseCycle = tuple[args: seq[ParseArgument], stdinConsumed: bool, endOfOpts: bool]
+
+ proc buildArgs(input: ParseCycle, next: Option[string]): ParseCycle =
+ if input.args.len == 0:
+ (@[(none(Argument), next)], input.stdinConsumed, input.endOfOpts)
+ else:
+ let last = input.args[^1]
+ if last.current.isSome:
+ let handleResult: tuple[args: seq[Argument], next: Option[string],
+ stdinConsumed: bool, endOfOpts: bool] = handleCurrentNext(last.current.unsafeGet,
+ next, input.stdinConsumed, input.endOfOpts)
+
+ let append = handleResult.args.map(a => (some(a), none(string)))
+ (input.args[0 .. ^2] & append & (none(Argument), handleResult.next),
+ handleResult.stdinConsumed, handleResult.endOfOpts)
+ elif next.isSome:
+ (input.args & (none(Argument), next), input.stdinConsumed, input.endOfOpts)
+ else:
+ input
+
+ let cycle: ParseCycle = (params.map(some) & none(string))
+ .foldl(buildArgs(a, b), (newSeq[ParseArgument](), false, false))
+
+ if cycle.stdinConsumed:
+ discard close(0)
+ discard open("/dev/tty", O_RDONLY)
+
+ lc[x | (y <- cycle.args, x <- y.arg), Argument]
+
+proc isShort*(arg: Argument): bool = arg.atype == ArgumentType.short
+proc isLong*(arg: Argument): bool = arg.atype == ArgumentType.long
+proc isTarget*(arg: Argument): bool = arg.atype == ArgumentType.target
+
+proc collectArg*(arg: Argument): seq[string] =
+ if arg.isShort:
+ let key = "-" & arg.key
+ arg.value.map(v => @[key, v]).get(@[key])
+ elif arg.isLong:
+ let key = "--" & arg.key
+ arg.value.map(v => @[key, v]).get(@[key])
+ elif arg.isTarget:
+ @[arg.key]
+ else:
+ @[]
+
+proc len*(op: OptionPair): int =
+ if op.short.isSome: 2 else: 1
+
+iterator items*(op: OptionPair): OptionKey =
+ if op.short.isSome:
+ yield (op.short.unsafeGet, false)
+ yield (op.long, true)
+
+proc filter*(args: seq[Argument], removeMatches: bool, keepTargets: bool,
+ pairs: varargs[OptionPair]): seq[Argument] =
+ let pairsSeq = @pairs
+ let argsSet = lc[x | (y <- pairsSeq, x <- y), OptionKey].toSet
+
+ args.filter(arg => (arg.isShort and (removeMatches xor (arg.key, false) in argsSet)) or
+ (arg.isLong and (removeMatches xor (arg.key, true) in argsSet)) or
+ (arg.isTarget and keepTargets))
+
+template count*(args: seq[Argument], pairs: varargs[OptionPair]): int =
+ args.filter(false, false, pairs).len
+
+template check*(args: seq[Argument], pairs: varargs[OptionPair]): bool =
+ args.count(pairs) > 0
+
+template whitelisted*(args: seq[Argument], pairs: varargs[OptionPair]): bool =
+ args.filter(true, false, pairs).len == 0
+
+proc matchOption*(arg: Argument, pair: OptionPair): bool =
+ (arg.isShort and pair.short.map(o => o == arg.key).get(false)) or
+ (arg.isLong and arg.key == pair.long)
+
+proc targets*(args: seq[Argument]): seq[string] =
+ args.filter(isTarget).map(a => a.key)