diff options
Diffstat (limited to 'src/args.nim')
-rw-r--r-- | src/args.nim | 175 |
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) |