Add ability for users to elide ':' or '=' when CLI authors pass a (#7297)

* Add ability for users to elide ':' or '=' when CLI authors pass a
non-empty partial symbol table.  Behavior should be identical to the
old behavior if empty partial symbol tables are passed.  "Partialness"
of the symbol table refers to the fact that one need only specify
option keys that are toggles/booleans/do not take arguments, hence
the "NoArg" suffixes in shortNoArg and longNoArg.

commandLineParams() returns seq[TaintedString], so use that consistently
in getopt() and initOptParser(seq[TaintedString]) dropping the taint at
the quoting stage just as with the paramStr() logic.

Fix capitalization inconsistency of cmdLongOption.

Export OptParser.cmd and OptParser.pos so that, at least *in principle*,
users of this API can handle "--" option processing termination or some
"git-like" sub-command stop word with a separate option sub-syntax.
{ Eg., ``case p.key of "": echo "trailing non-option args: ", p.cmd[p.pos..^1]``
or ``case p.kind of cmdArgument: if p.key == "mysubcmd": ...``. }  Really,
searching for the last delimiter before p.pos is probably needed to frame
the trailing text..Not the nicest API, but still possible with effort.

* Make requested changes from string to seq[char]
(see https://github.com/nim-lang/Nim/pull/7297)

* Document new behavior and elaborate on some special cases.

* NoArg => NoVal to be less ambiguous/more clear.

* Add more documentation and an example snippet.

* Tweak language. Clarify still using ':'/'=' is ok.

* Add a test case for new NoVal behavior.
This commit is contained in:
c-blake
2018-03-08 02:12:34 -05:00
committed by Andreas Rumpf
parent 566cec74b6
commit 551d7b7dc1
2 changed files with 83 additions and 22 deletions

View File

@@ -11,11 +11,23 @@
## It supports one convenience iterator over all command line options and some
## lower-level features.
##
## Supported syntax:
## Supported syntax with default empty ``shortNoVal``/``longNoVal``:
##
## 1. short options - ``-abcd``, where a, b, c, d are names
## 2. long option - ``--foo:bar``, ``--foo=bar`` or ``--foo``
## 3. argument - everything else
##
## When ``shortNoVal``/``longNoVal`` are non-empty then the ':' and '=' above
## are still accepted, but become optional. Note that these option key sets
## must be updated along with the set of option keys taking no value, but
## keys which do take values need no special updates as their set evolves.
##
## When option values begin with ':' or '=' they need to be doubled up (as in
## ``--delim::``) or alternated (as in ``--delim=:``).
##
## The common ``--`` non-option argument delimiter appears as an empty string
## long option key. ``OptParser.cmd``, ``OptParser.pos``, and
## ``os.parseCmdLine`` may be used to complete parsing in that case.
{.push debugger: off.}
@@ -32,9 +44,11 @@ type
cmdShortOption ## a short option ``-c`` detected
OptParser* =
object of RootObj ## this object implements the command line parser
cmd: string
pos: int
cmd*: string # cmd,pos exported so caller can catch "--" as..
pos*: int # ..empty key or subcmd cmdArg & handle specially
inShortState: bool
shortNoVal: set[char]
longNoVal: seq[string]
kind*: CmdLineKind ## the dected command line token
key*, val*: TaintedString ## key and value pair; ``key`` is the option
## or the argument, ``value`` is not "" if
@@ -78,11 +92,19 @@ when declared(os.paramCount):
# we cannot provide this for NimRtl creation on Posix, because we can't
# access the command line arguments then!
proc initOptParser*(cmdline = ""): OptParser =
proc initOptParser*(cmdline = "", shortNoVal: set[char]={},
longNoVal: seq[string] = @[]): OptParser =
## inits the option parser. If ``cmdline == ""``, the real command line
## (as provided by the ``OS`` module) is taken.
## (as provided by the ``OS`` module) is taken. If ``shortNoVal`` is
## provided command users do not need to delimit short option keys and
## values with a ':' or '='. If ``longNoVal`` is provided command users do
## not need to delimit long option keys and values with a ':' or '='
## (though they still need at least a space). In both cases, ':' or '='
## may still be used if desired. They just become optional.
result.pos = 0
result.inShortState = false
result.shortNoVal = shortNoVal
result.longNoVal = longNoVal
if cmdline != "":
result.cmd = cmdline
else:
@@ -94,15 +116,19 @@ when declared(os.paramCount):
result.key = TaintedString""
result.val = TaintedString""
proc initOptParser*(cmdline: seq[string]): OptParser =
proc initOptParser*(cmdline: seq[TaintedString], shortNoVal: set[char]={},
longNoVal: seq[string] = @[]): OptParser =
## inits the option parser. If ``cmdline.len == 0``, the real command line
## (as provided by the ``OS`` module) is taken.
## (as provided by the ``OS`` module) is taken. ``shortNoVal`` and
## ``longNoVal`` behavior is the same as for ``initOptParser(string,...)``.
result.pos = 0
result.inShortState = false
result.shortNoVal = shortNoVal
result.longNoVal = longNoVal
result.cmd = ""
if cmdline.len != 0:
for i in 0..<cmdline.len:
result.cmd.add quote(cmdline[i])
result.cmd.add quote(cmdline[i].string)
result.cmd.add ' '
else:
for i in countup(1, paramCount()):
@@ -121,8 +147,9 @@ proc handleShortOption(p: var OptParser) =
while p.cmd[i] in {'\x09', ' '}:
inc(i)
p.inShortState = false
if p.cmd[i] in {':', '='}:
inc(i)
if p.cmd[i] in {':', '='} or card(p.shortNoVal) > 0 and p.key.string[0] notin p.shortNoVal:
if p.cmd[i] in {':', '='}:
inc(i)
p.inShortState = false
while p.cmd[i] in {'\x09', ' '}: inc(i)
i = parseWord(p.cmd, i, p.val.string)
@@ -146,12 +173,13 @@ proc next*(p: var OptParser) {.rtl, extern: "npo$1".} =
of '-':
inc(i)
if p.cmd[i] == '-':
p.kind = cmdLongoption
p.kind = cmdLongOption
inc(i)
i = parseWord(p.cmd, i, p.key.string, {'\0', ' ', '\x09', ':', '='})
while p.cmd[i] in {'\x09', ' '}: inc(i)
if p.cmd[i] in {':', '='}:
inc(i)
if p.cmd[i] in {':', '='} or len(p.longNoVal) > 0 and p.key.string notin p.longNoVal:
if p.cmd[i] in {':', '='}:
inc(i)
while p.cmd[i] in {'\x09', ' '}: inc(i)
p.pos = parseWord(p.cmd, i, p.val.string)
else:
@@ -172,7 +200,7 @@ iterator getopt*(p: var OptParser): tuple[kind: CmdLineKind, key, val: TaintedSt
## Example:
##
## .. code-block:: nim
## var p = initOptParser("--left --debug:3 -l=4 -r:2")
## var p = initOptParser("--left --debug:3 -l -r:2")
## for kind, key, val in p.getopt():
## case kind
## of cmdArgument:
@@ -192,17 +220,30 @@ iterator getopt*(p: var OptParser): tuple[kind: CmdLineKind, key, val: TaintedSt
yield (p.kind, p.key, p.val)
when declared(initOptParser):
iterator getopt*(): tuple[kind: CmdLineKind, key, val: TaintedString] =
## This is an convenience iterator for iterating over the command line arguments.
## This create a new OptParser object.
## See above for a more detailed example
iterator getopt*(cmdline: seq[TaintedString] = commandLineParams(),
shortNoVal: set[char]={}, longNoVal: seq[string] = @[]):
tuple[kind: CmdLineKind, key, val: TaintedString] =
## This is an convenience iterator for iterating over command line arguments.
## This creates a new OptParser. See the above ``getopt(var OptParser)``
## example for using default empty ``NoVal`` parameters. This example is
## for the same option keys as that example but here option key-value
## separators become optional for command users:
##
## .. code-block:: nim
## for kind, key, val in getopt():
## # this will iterate over all arguments passed to the cmdline.
## continue
## for kind, key, val in getopt(shortNoVal = { 'l' },
## longNoVal = @[ "left" ]):
## 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
## if filename == "":
## writeHelp()
##
var p = initOptParser()
var p = initOptParser(cmdline, shortNoVal=shortNoVal, longNoVal=longNoVal)
while true:
next(p)
if p.kind == cmdEnd: break

View File

@@ -9,6 +9,17 @@ kind: cmdLongOption key:val -- left:
kind: cmdLongOption key:val -- debug:3
kind: cmdShortOption key:val -- l:4
kind: cmdShortOption key:val -- r:2
parseoptNoVal
kind: cmdLongOption key:val -- left:
kind: cmdLongOption key:val -- debug:3
kind: cmdShortOption key:val -- l:
kind: cmdShortOption key:val -- r:2
kind: cmdLongOption key:val -- debug:2
kind: cmdLongOption key:val -- debug:1
kind: cmdShortOption key:val -- r:1
kind: cmdShortOption key:val -- r:0
kind: cmdShortOption key:val -- l:
kind: cmdShortOption key:val -- r:4
parseopt2
first round
kind: cmdLongOption key:val -- left:
@@ -39,6 +50,15 @@ block:
for kind, key, val in parseopt.getopt(p):
echo "kind: ", kind, "\tkey:val -- ", key, ":", val
block:
echo "parseoptNoVal"
# test NoVal mode with custom cmdline arguments
var argv = "--left --debug:3 -l -r:2 --debug 2 --debug=1 -r1 -r=0 -lr4"
var p = parseopt.initOptParser(argv,
shortNoVal = {'l'}, longNoVal = @["left"])
for kind, key, val in parseopt.getopt(p):
echo "kind: ", kind, "\tkey:val -- ", key, ":", val
block:
echo "parseopt2"
for kind, key, val in parseopt2.getopt():