Compare commits

..

No commits in common. "7385a93110f95e1d67d803780efa014fdc960779" and "33e5c9e0725fc9d56f02cd01c5d04cc0cb739eac" have entirely different histories.

8 changed files with 142 additions and 500 deletions

1
.gitignore vendored
View file

@ -5,4 +5,3 @@ nimble.develop
nimble.paths nimble.paths
nimbledeps nimbledeps
public/ public/
*.out

View file

@ -26,7 +26,7 @@ task docs, "Deploy doc html + search index to public/ directory":
when defined(clean): when defined(clean):
echo fmt"clearing {deployDir}" echo fmt"clearing {deployDir}"
rmDir deployDir rmDir deployDir
for module in ["cligen", "chooser", "logging", "cli", "parseopt3"]: for module in ["cligen", "chooser", "logging", "cli"]:
selfExec fmt"{docCmd} --docRoot:{getCurrentDir()}/src/ src/hwylterm/{module}" selfExec fmt"{docCmd} --docRoot:{getCurrentDir()}/src/ src/hwylterm/{module}"
selfExec fmt"{docCmd} --project --project src/{pkgName}.nim" selfExec fmt"{docCmd} --project --project src/{pkgName}.nim"
docFixup(deployDir, pkgName) docFixup(deployDir, pkgName)

View file

@ -1,13 +1,12 @@
##[ ##[
# Hwylterm # Hwylterm
see also these utility modules: see also these utility modules with extra dependencies:
- [cli](./hwylterm/cli.html) - [cli](./hwylterm/cli.html)
- [cligen adapter](./hwylterm/cligen.html), requires [cligen](https://github.com/c-blake/cligen) - [cligen adapter](./hwylterm/cligen.html), requires [cligen](https://github.com/c-blake/cligen)
- [chooser](./hwylterm/chooser.html) - [chooser](./hwylterm/chooser.html)
- [logging](./hwylterm/logging.html) - [logging](./hwylterm/logging.html)
- [parseopt3](./hwylterm/parseopt3.html) borrowed from `cligen`
]## ]##

View file

@ -7,118 +7,9 @@
# TODO: # TODO:
#{.push raises:[].} #{.push raises:[].}
import std/[ import std/[os, sequtils, strformat, strutils]
macros, os, sequtils, strformat, strutils, terminal import ./bbansi/[styles, utils, colors]
] export utils
import ./bbansi/[styles, colors]
type
BbMode* = enum
On, NoColor, Off
ColorSystem = enum
TrueColor, EightBit, Standard, None
proc checkColorSupport(): BbMode =
when defined(bbansiOff):
return Off
when defined(bbansiNoColor):
return NoColor
else:
if getEnv("HWYLTERM_FORCE_COLOR") != "":
return On
elif getEnv("NO_COLOR") != "":
return NoColor
elif (getEnv("TERM") in ["dumb", "unknown"]) or not isatty(stdout):
return Off
proc checkColorSystem(): ColorSystem =
let colorterm = getEnv("COLORTERM").strip().toLowerAscii()
if colorterm in ["truecolor", "24bit"]:
return TrueColor
let term = getEnv("TERM", "").strip().toLowerAscii()
let colors = term.split("-")[^1]
return
case colors:
of "kitty": EightBit
of "256color": EightBit
of "16color": Standard
else: Standard
let bbMode* = checkColorSupport()
let colorSystem* = checkColorSystem()
func firstCapital(s: string): string = s.toLowerAscii().capitalizeAscii()
func normalizeStyle(style: string): string = style.replace("_","").toLowerAscii().capitalizeAscii()
func isHex(s: string): bool = (s.startswith "#") and (s.len == 7)
func toCode(style: BbStyle): string = $ord(style)
func toCode(abbr: BbStyleAbbr): string = abbr.toStyle().toCode()
func toCode(color: ColorXterm): string = "38;5;" & $ord(color)
func toBgCode(color: ColorXterm): string = "48;5;" & $ord(color)
func toCode(c: ColorRgb): string = "38;2;" & $c
func toBgCode(c: ColorRgb): string = "48:2;" & $c
func toCode(c: Color256): string = "38;5;" & $c
func toBgCode(c: Color256): string = "48;5;" & $c
macro enumNames(a: typed): untyped =
## unexported macro copied from std/enumutils
result = newNimNode(nnkBracket)
for ai in a.getType[1][1..^1]:
assert ai.kind == nnkSym
result.add newLit ai.strVal
const ColorXTermNames = enumNames(ColorXterm).mapIt(firstCapital(it))
const BbStyleNames = enumNames(BbStyle).mapIt(firstCapital(it))
const ColorDigitStrings = (1..255).toSeq().mapIt($it)
# TODO: write non-fallible parseStyle(s) procedure
proc toAnsiCode*(s: string): string =
if bbMode == Off: return
var
codes: seq[string]
styles: seq[string]
bgStyle: string
if " on " in s or s.startswith("on"):
let fgBgSplit = s.rsplit("on", maxsplit = 1)
styles = fgBgSplit[0].toLowerAscii().splitWhitespace()
bgStyle = fgBgSplit[1].strip().toLowerAscii()
else:
styles = s.splitWhitespace()
for style in styles:
let normalizedStyle = normalizeStyle(style)
if normalizedStyle in ["B", "I", "U"]:
codes.add parseEnum[BbStyleAbbr](normalizedStyle).toCode()
elif normalizedStyle in BbStyleNames:
codes.add parseEnum[BbStyle](normalizedStyle).toCode()
if not (bbMode == On): continue
if normalizedStyle in ColorXtermNames:
codes.add parseEnum[ColorXterm](normalizedStyle).toCode()
elif normalizedStyle.isHex():
codes.add normalizedStyle.hexToRgb.toCode()
elif normalizedStyle in ColorDigitStrings:
codes.add parseInt(normalizedStyle).toCode()
else:
when defined(debugBB): echo "unknown style: " & normalizedStyle
if bbMode == On and bgStyle != "":
let normalizedBgStyle = normalizeStyle(bgStyle)
if normalizedBgStyle in ColorXtermNames:
codes.add parseEnum[ColorXTerm](normalizedBgStyle).toBgCode()
elif normalizedBgStyle.isHex():
codes.add normalizedBgStyle.hexToRgb().toBgCode()
elif normalizedBgStyle in ColorDigitStrings:
codes.add parseInt(normalizedBgStyle).toBgCode()
else:
when defined(debugBB): echo "unknown bg style: " & normalizedBgStyle
if codes.len > 0:
result.add "\e["
result.add codes.join ";"
result.add "m"
func stripAnsi*(s: string): string = func stripAnsi*(s: string): string =
## remove all ansi escape codes from a string ## remove all ansi escape codes from a string
@ -333,7 +224,8 @@ proc bbEcho*(args: varargs[string, `$`]) {.sideEffect.} =
# NOTE: could move to standlone modules in the tools/ directory # NOTE: could move to standlone modules in the tools/ directory
when isMainModule: when isMainModule:
import ./[cli, parseopt3] import std/[parseopt]
import ./cli
const version = staticExec "git describe --tags --always --dirty=-dev" const version = staticExec "git describe --tags --always --dirty=-dev"
@ -367,7 +259,6 @@ bbansi "[[red]some red[[/red] but all italic" --style:italic
echo color, " -> ", fmt"[{color}]****".bb echo color, " -> ", fmt"[{color}]****".bb
for color in colors: for color in colors:
echo "on ", color, " -> ", fmt"[on {color}]****".bb echo "on ", color, " -> ", fmt"[on {color}]****".bb
quit(QuitSuccess)
proc debug(bbs: BbString): string = proc debug(bbs: BbString): string =
echo "bbString(" echo "bbString("
@ -384,29 +275,31 @@ bbansi "[[red]some red[[/red] but all italic" --style:italic
strArgs: seq[string] strArgs: seq[string]
style: string style: string
showDebug: bool showDebug: bool
var p = initOptParser( var p = initOptParser()
shortNoVal = {'h','v'},
longNoVal = @["help", "version", "testCard"]
)
for kind, key, val in p.getopt(): for kind, key, val in p.getopt():
case kind case kind
of cmdError: quit($(bb"[red]cli error[/]: " & p.message), 1) of cmdEnd:
of cmdEnd: assert(false) break
of cmdShortOption, cmdLongOption: of cmdShortOption, cmdLongOption:
case key case key
of "testCard" : testCard() of "help", "h":
of "help" , "h": writeHelp() writeHelp()
of "version", "v": writeVersion() of "version", "v":
of "style" , "s": writeVersion()
of "testCard":
testCard()
quit(QuitSuccess)
of "style", "s":
if val == "": if val == "":
bbEcho "[red]ERROR[/]: expected value for -s/--style" echo "[red]ERROR[/]: expected value for -s/--style".bb
quit(QuitFailure) quit(QuitFailure)
style = val style = val
of "debug": of "debug":
showDebug = true showDebug = true
else: else:
bbEcho "[yellow]warning[/]: unexpected option/value -> ", key, ", ", val echo bb"[yellow]warning[/]: unexpected option/value -> ", key, ", ", val
of cmdArgument: strArgs.add key of cmdArgument:
strArgs.add key
if strArgs.len == 0: if strArgs.len == 0:
writeHelp() writeHelp()
for arg in strArgs: for arg in strArgs:

View file

@ -1,3 +1,6 @@
import std/tables
export tables
type type
BbStyleAbbr* = enum BbStyleAbbr* = enum

View file

@ -1 +1,112 @@
import std/[
macros, os, sequtils, strutils, terminal,
]
import ./[styles, colors]
type
BbMode* = enum
On, NoColor, Off
ColorSystem = enum
TrueColor, EightBit, Standard, None
proc checkColorSupport(): BbMode =
when defined(bbansiOff):
return Off
when defined(bbansiNoColor):
return NoColor
else:
if getEnv("HWYLTERM_FORCE_COLOR") != "":
return On
elif getEnv("NO_COLOR") != "":
return NoColor
elif (getEnv("TERM") in ["dumb", "unknown"]) or not isatty(stdout):
return Off
proc checkColorSystem(): ColorSystem =
let colorterm = getEnv("COLORTERM").strip().toLowerAscii()
if colorterm in ["truecolor", "24bit"]:
return TrueColor
let term = getEnv("TERM", "").strip().toLowerAscii()
let colors = term.split("-")[^1]
return
case colors:
of "kitty": EightBit
of "256color": EightBit
of "16color": Standard
else: Standard
let bbMode* = checkColorSupport()
let colorSystem* = checkColorSystem()
func firstCapital(s: string): string = s.toLowerAscii().capitalizeAscii()
func normalizeStyle(style: string): string = style.replace("_","").toLowerAscii().capitalizeAscii()
func isHex(s: string): bool = (s.startswith "#") and (s.len == 7)
func toCode(style: BbStyle): string = $ord(style)
func toCode(abbr: BbStyleAbbr): string = abbr.toStyle().toCode()
func toCode(color: ColorXterm): string = "38;5;" & $ord(color)
func toBgCode(color: ColorXterm): string = "48;5;" & $ord(color)
func toCode(c: ColorRgb): string = "38;2;" & $c
func toBgCode(c: ColorRgb): string = "48:2;" & $c
func toCode(c: Color256): string = "38;5;" & $c
func toBgCode(c: Color256): string = "48;5;" & $c
macro enumNames(a: typed): untyped =
## unexported macro copied from std/enumutils
result = newNimNode(nnkBracket)
for ai in a.getType[1][1..^1]:
assert ai.kind == nnkSym
result.add newLit ai.strVal
const ColorXTermNames = enumNames(ColorXterm).mapIt(firstCapital(it))
const BbStyleNames = enumNames(BbStyle).mapIt(firstCapital(it))
const ColorDigitStrings = (1..255).toSeq().mapIt($it)
# TODO: write non-fallible parseStyle(s) procedure
proc toAnsiCode*(s: string): string =
if bbMode == Off: return
var
codes: seq[string]
styles: seq[string]
bgStyle: string
if " on " in s or s.startswith("on"):
let fgBgSplit = s.rsplit("on", maxsplit = 1)
styles = fgBgSplit[0].toLowerAscii().splitWhitespace()
bgStyle = fgBgSplit[1].strip().toLowerAscii()
else:
styles = s.splitWhitespace()
for style in styles:
let normalizedStyle = normalizeStyle(style)
if normalizedStyle in ["B", "I", "U"]:
codes.add parseEnum[BbStyleAbbr](normalizedStyle).toCode()
elif normalizedStyle in BbStyleNames:
codes.add parseEnum[BbStyle](normalizedStyle).toCode()
if not (bbMode == On): continue
if normalizedStyle in ColorXtermNames:
codes.add parseEnum[ColorXterm](normalizedStyle).toCode()
elif normalizedStyle.isHex():
codes.add normalizedStyle.hexToRgb.toCode()
elif normalizedStyle in ColorDigitStrings:
codes.add parseInt(normalizedStyle).toCode()
else:
when defined(debugBB): echo "unknown style: " & normalizedStyle
if bbMode == On and bgStyle != "":
let normalizedBgStyle = normalizeStyle(bgStyle)
if normalizedBgStyle in ColorXtermNames:
codes.add parseEnum[ColorXTerm](normalizedBgStyle).toBgCode()
elif normalizedBgStyle.isHex():
codes.add normalizedBgStyle.hexToRgb().toBgCode()
elif normalizedBgStyle in ColorDigitStrings:
codes.add parseInt(normalizedBgStyle).toBgCode()
else:
when defined(debugBB): echo "unknown bg style: " & normalizedBgStyle
if codes.len > 0:
result.add "\e["
result.add codes.join ";"
result.add "m"

View file

@ -127,14 +127,15 @@ proc choose*[T](things: openArray[T], height: Natural = 6): seq[T] =
when isMainModule: when isMainModule:
import ./[cli, parseopt3] import std/[parseopt]
import ./cli
proc writeHelp() = proc writeHelp() =
echo newHwylCli( echo newHwylCli(
"[bold]hwylchoose[/] [[[green]args...[/]] [[[faint]-h[/]]", "[bold]hwylchoose[/] [[[green]args...[/]] [[[faint]-h[/]]",
""" """
hwylchoose a b c d hwylchoose a b c d
hwylchoose a,b,c,d -s , hwylchoose a,b,c,d -s,
hwylchoose a,b,c,d --seperator "," hwylchoose a,b,c,d --seperator ","
""", """,
[ [
@ -151,8 +152,8 @@ hwylchoose a,b,c,d --seperator ","
) )
for kind, key, val in p.getopt(): for kind, key, val in p.getopt():
case kind case kind
of cmdError: quit($(bb"[red]cli error[/]: " & p.message), 1) of cmdEnd:
of cmdEnd: assert false break
of cmdShortOption, cmdLongOption: of cmdShortOption, cmdLongOption:
case key case key
of "help", "h": of "help", "h":

View file

@ -1,364 +0,0 @@
## Copyright Charles L. Blake, ISC License.
## For full license see [here](https://github.com/c-blake/cligen/blob/master/LICENSE)
##
## This module provides a Nim command line parser that is mostly API compatible
## with the Nim standard library parseopt (and the code derives from that).
## It supports one convenience iterator over all command line options and some
## lower-level features.
## Supported command syntax (here ``=|:`` may be any char in ``sepChars``):
##
## 1. short option bundles: ``-abx`` (where a, b, x *are in* ``shortNoVal``)
##
## 1a. bundles with one final value: ``-abc:Bar``, ``-abc=Bar``, ``-c Bar``,
## ``-abcBar`` (where ``c`` is *not in* ``shortNoVal``)
##
## 2. long options with values: ``--foo:bar``, ``--foo=bar``, ``--foo bar``
## (where ``foo`` is *not in* ``longNoVal``)
##
## 2a. long options without vals: ``--baz`` (where ``baz`` is in ``longNoVal``)
##
## 3. command parameters: everything else | anything after "--" or a stop word.
##
## The above is a *superset* of usual POSIX command syntax - it should accept
## any POSIX-inspired input, but it also accepts more forms/styles. (Note that
## POSIX itself is not super strict about this part of the standard. See:
## http://pubs.opengroup.org/onlinepubs/009604499/basedefs/xbd_chap12.html)
##
## When ``optionNormalize(key)`` is used, command authors provide command users
## additional flexibility to ``--spell_multi-word_options -aVarietyOfWays
## --as-Per_User-Preference``. This is similar to Nim style-insensitive
## identifier syntax, but by default allows dash ('-') as well as underscore
## ('_') word separation.
##
## The "separator free" forms above require appropriate ``shortNoVal`` and
## ``longNoVal`` lists to designate option keys that take no value (as well
## as ``requireSeparator == false``). If such lists are empty, the user must
## use separators when providing any value.
##
## A notable subtlety is when the first character of an option value is one of
## ``sepChars``. Even if ``requireSeparator`` is ``false``, passing such option
## values requires either A) putting the value in the next command parameter,
## as in ``"-c :"`` or B) prefixing the value with an element of ``sepChars``,
## as in ``-c=:`` or ``-c::``. Both choices fit into common quoting styles.
## It seems likely a POSIX-habituated end-user's second guess (after ``"-c:"``
## errored out with "argument expected") would just work as they expected.
## POSIX itself encourages authors & users to use the ``"-c :"`` form anyway.
## This small deviation lets this parser accept valid invocations with the
## original Nim option parser command syntax (with the same semantics), easing
## transition.
##
## To ease "nested" command-line parsing (such as with "git" where there may be
## early global options, a subcommand and later subcommand options), this parser
## also supports a set of "stop words" - special whole command parameters that
## prevent subsequent parameters being interpreted as options. This feature
## makes it easy to fully process a command line and then re-process its tail
## rather than mandating breaking out at a stop word with a manual test. Stop
## words are basically just like a POSIX "--" (which this parser also supports -
## even if "--" is not in ``stopWords``). Such stop words (or "--") can still
## be the **values** of option keys with no effect. Only usage as a non-option
## command parameter acts to stop possible option-treatment of later parameters.
##
## To facilitate syntax for operations beyond simple assignment, ``opChars`` is
## a set of chars that may prefix an element of ``sepChars``. The ``sep`` member
## of ``OptParser`` is the actual separator used for the current option, if any.
## E.g, a user entering "=" causes ``sep == "="`` while entering "+=" gets
## ``sep == "+="``, and "+/-+=" gets ``sep == "+/-+="``.
import std/[os, strutils, critbits]
proc optionNormalize*(s: string, wordSeparators="_-"): string {.noSideEffect.} =
## Normalizes option key ``s`` to allow command syntax to be style-insensitive
## in a similar way to Nim identifier syntax.
##
## Specifically this means to convert *all but the first* char to lower case
## and remove chars in ``wordSeparators`` ('_' and '-') by default. This way
## users can type "command --my-opt-key" or "command --myOptKey" and so on.
##
## Example:
##
## .. code-block:: nim
## for kind, key, val in p.getopt():
## case kind
## of cmdLongOption, cmdShortOption:
## case optionNormalize(key)
## of "myoptkey", "m": doSomething()
result = newString(s.len)
if s.len == 0: return
var wordSeps: set[char] # compile a set[char] from ``wordSeparators``
for c in wordSeparators:
wordSeps.incl(c)
result[0] = s[0]
var j = 1
for i in 1..len(s) - 1:
if s[i] in {'A'..'Z'}:
result[j] = chr(ord(s[i]) + (ord('a') - ord('A')))
inc j
elif s[i] notin wordSeps:
result[j] = s[i]
inc j
if j != s.len:
setLen(result, j)
{.push warning[ProveField]: off.}
proc valsWithPfx*[T](cb: CritBitTree[T], key: string): seq[T] =
for v in cb.valuesWithPrefix(optionNormalize(key)): result.add(v)
proc lengthen*[T](cb: CritBitTree[T], key: string, prefixOk=false): string =
## Use ``cb`` to find normalized long form of ``key``. Return empty string if
## ambiguous or unchanged string on no match.
let n = optionNormalize(key)
if not prefixOk:
return n
var ks: seq[string]
for k in cb.keysWithPrefix(n): ks.add(k)
if ks.len == 1:
return ks[0]
if ks.len > 1: # Can still have an exact match if..
for k in ks: #..one long key fully prefixes another,
if k == n: #..like "help" prefixing "help-syntax".
return n
if ks.len > 1: #No exact prefix-match above => ambiguity
return "" #=> of-clause that reports ambiguity in .msg.
return n #ks.len==0 => case-else clause suggests spelling in .msg.
{.pop.}
when not declared(TaintedString):
type TaintedString* = string
{.push warning[Deprecated]: off.}
type
CmdLineKind* = enum ## the detected command line token
cmdEnd, ## end of command line reached
cmdArgument, ## argument detected
cmdLongOption, ## a long option ``--option`` detected
cmdShortOption, ## a short option ``-c`` detected
cmdError ## error in primary option syntax usage
OptParser* = object of RootObj ## object to implement the command line parser
cmd*: seq[string] ## command line being parsed
pos*: int ## current command parameter to inspect
off*: int ## current offset in cmd[pos] for short key block
optsDone*: bool ## "--" has been seen
shortNoVal*: set[char] ## 1-letter options not requiring optarg
longNoVal*: CritBitTree[string] ## long options not requiring optarg
stopWords*: CritBitTree[string] ## special literal params acting like "--"
requireSep*: bool ## require separator between option key & val
sepChars*: set[char] ## all the chars that can be valid separators
opChars*: set[char] ## all chars that can prefix a sepChar
longPfxOk*: bool ## true means unique prefix is ok for longOpts
stopPfxOk*: bool ## true means unique prefix is ok for stopWords
sep*: string ## actual string separating key & value
message*: string ## message to display upon cmdError
kind*: CmdLineKind ## the detected command line token
key*, val*: TaintedString ## key and value pair; ``key`` is the option
## or the argument, ``value`` is not "" if
## the option was given a value
proc initOptParser*(cmdline: seq[string] = commandLineParams(),
shortNoVal: set[char] = {}, longNoVal: seq[string] = @[],
requireSeparator=false, sepChars={'=',':'},
opChars: set[char] = {}, stopWords: seq[string] = @[],
longPfxOk=true, stopPfxOk=true): OptParser =
## Initializes a parse. ``cmdline`` should not contain parameter 0, typically
## the program name. If ``cmdline`` is not given, default to current program
## parameters.
##
## ``shortNoVal`` and ``longNoVal`` specify respectively one-letter and long
## option keys that do *not* take arguments.
##
## If ``requireSeparator==true``, then option keys&values must be separated
## by an element of ``sepChars`` (default ``{'=',':'}``) in short or long
## option contexts. If ``requireSeparator==false``, the parser understands
## that only non-NoVal options will expect args and users may say ``-aboVal``
## or ``-o Val`` or ``--opt Val`` { as well as the `-o:Val|--opt=Val`
## separator style which always works }.
##
## If ``opChars`` is not empty then those characters before the ``:|==``
## separator are reported in the ``.sep`` field of an element parse. This
## allows "incremental" syntax like ``--values+=val``.
##
## If ``longPfxOk`` then unique prefix matching is done for long options.
## If ``stopPfxOk`` then unique prefix matching is done for stop words
## (usually subcommand names).
##
## Parameters following either "--" or any literal parameter in ``stopWords``
## are never interpreted as options.
result.cmd = cmdline
result.shortNoVal = shortNoVal
for s in longNoVal: #Take normalizer param vs. hard-coding optionNormalize?
if s.len > 0: result.longNoVal.incl(optionNormalize(s), s)
result.requireSep = requireSeparator
result.sepChars = sepChars
result.opChars = opChars
{.push warning[ProveField]: off.}
for w in stopWords:
if w.len > 0: result.stopWords.incl(optionNormalize(w), w)
{.pop.}
result.longPfxOk = longPfxOk
result.stopPfxOk = stopPfxOk
result.off = 0
result.optsDone = false
proc initOptParser*(cmdline: string): OptParser =
## Initializes option parses with cmdline. Splits cmdline in on spaces and
## calls `initOptParser(openarray[string])`. Should use a proper tokenizer.
if cmdline == "": # backward compatibility
return initOptParser(commandLineParams())
else:
return initOptParser(cmdline.split)
proc doShort(p: var OptParser) =
proc cur(p: OptParser): char =
if p.off < p.cmd[p.pos].len: result = p.cmd[p.pos][p.off]
else: result = '\0'
p.kind = cmdShortOption
p.val = ""
p.key = $p.cur; p.off += 1 # shift off first char as key
if p.cur in p.opChars or p.cur in p.sepChars:
let mark = p.off
while p.cur != '\0' and p.cur notin p.sepChars and p.cur in p.opChars:
p.off += 1
if p.cur in p.sepChars: #This may set p.val="" w/sepChar&NoData
p.sep = p.cmd[p.pos][mark..p.off] #..but since "--string=''" shows up this
p.val = p.cmd[p.pos][p.off+1..^1] #..way, we consider it an "Ok" sitch..As
p.pos += 1 #..a byproduct, "--string=" is also Ok.
p.off = 0
return
else: # Was just an opChars-starting value
p.off = mark
if p.key[0] in p.shortNoVal: # No explicit val, but that is ok
if p.off == p.cmd[p.pos].len:
p.off = 0
p.pos += 1
return
if p.requireSep:
p.message = "Expecting option key-val separator :|= after `" & p.key & "`"
p.kind = cmdError
return
if p.cmd[p.pos].len - p.off > 0:
p.val = p.cmd[p.pos][p.off .. ^1]
p.pos += 1
p.off = 0
return
if p.pos < p.cmd.len - 1: # opt val = next param
p.val = p.cmd[p.pos + 1]
p.pos += 2
p.off = 0
return
p.val = ""
p.off = 0
p.pos += 1
proc doLong(p: var OptParser) =
p.kind = cmdLongOption
p.val = ""
let param = p.cmd[p.pos]
p.pos += 1 # always consume at least 1 param
let sep = find(param, p.sepChars) # only very first occurrence of delim
if sep > 2:
var op = sep
while op > 2 and param[op-1] in p.opChars:
dec(op)
p.key = param[2 .. op-1]
p.sep = param[op .. sep]
p.val = param[sep+1..^1]
return
p.key = param[2..^1] # no sep; key is whole param past "--"
let k = p.longNoVal.lengthen(optionNormalize(p.key), p.longPfxOk)
if k in p.longNoVal:
return # No argument; done
if p.requireSep:
p.message = "Expecting option key-val separator :|= after `" & p.key & "`"
p.kind = cmdError
return
if p.pos < p.cmd.len: # Take opt arg from next param
p.val = p.cmd[p.pos]
p.pos += 1
elif p.longNoVal.len != 0:
p.val = ""
p.pos += 1
{.push warning[ProveField]: off.}
proc next*(p: var OptParser) =
p.sep = ""
if p.off > 0: #Step1: handle any remaining short opts
doShort(p)
return
if p.pos >= p.cmd.len: #Step2: end of params check
p.kind = cmdEnd
return
if not p.cmd[p.pos].startsWith("-") or p.optsDone: #Step3: non-option param
p.kind = cmdArgument
p.key = p.cmd[p.pos]
p.val = ""
let k = p.stopWords.lengthen(optionNormalize(p.cmd[p.pos]), p.stopPfxOk)
if k in p.stopWords: #Step4: chk for stop word
p.optsDone = true # should only hit Step3 henceforth
p.pos += 1
return
if p.cmd[p.pos].startsWith("--"): #Step5: "--*"
if p.cmd[p.pos].len == 2: # terminating "--" => pure param mode
p.optsDone = true # should only hit Step3 henceforth
p.pos += 1 # skip the "--" itself, unlike stopWords
next(p) # do next one so each parent next()..
return #..yields exactly 1 opt+arg|cmdparam
doLong(p)
else: #Step6: "-" but not "--" => short opt
if p.cmd[p.pos].len == 1: #Step6a: simply "-" => non-option param
p.kind = cmdArgument # {"-" often used to indicate "stdin"}
p.key = p.cmd[p.pos]
p.val = ""
p.pos += 1
else: #Step6b: maybe a block of short options
p.off = 1 # skip the initial "-"
doShort(p)
{.pop.}
type
GetoptResult* = tuple[kind: CmdLineKind, key, val: TaintedString]
iterator getopt*(p: var OptParser): GetoptResult =
## An convenience iterator for iterating over the given OptParser object.
## Example:
##
## .. code-block:: nim
## var filename: string = ""
## var p = initOptParser("--left --debug:3 -l=4 -r:2")
## for kind, key, val in p.getopt():
## case kind
## of cmdArgument:
## filename = key
## of cmdLongOption, cmdShortOption:
## case key
## of "help", "h": writeHelp()
## of "version", "v": writeVersion()
## of cmdEnd: assert(false) # cannot happen
## of cmdError: quit(p.message, 2)
## if filename == "":
## # no filename has been given, so we show the help:
## writeHelp()
p.pos = 0
while true:
next(p)
if p.kind == cmdEnd: break
yield (p.kind, p.key, p.val)
when declared(paramCount):
iterator getopt*(cmdline=commandLineParams(), shortNoVal: set[char] = {},
longNoVal: seq[string] = @[], requireSeparator=false,
sepChars={'=', ':'}, opChars: set[char] = {},
stopWords: seq[string] = @[]): GetoptResult =
## This is an convenience iterator for iterating over the command line.
## Parameters here are the same as for initOptParser. Example:
## See above for a more detailed example
##
## .. code-block:: nim
## for kind, key, val in getopt():
## # this will iterate over all arguments passed to the cmdline.
## continue
##
var p = initOptParser(cmdline, shortNoVal, longNoVal, requireSeparator,
sepChars, opChars, stopWords)
while true:
next(p)
if p.kind == cmdEnd: break
yield (p.kind, p.key, p.val)
{.pop.}