Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docsrc/arguments.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ String ``N+``, where ``N`` is a number ``N`` or more arguments
String ``?`` An optional argument
String ``*`` Any number of arguments
String ``+`` At least one argument
String ``...`` All remaining arguments
================================================= =============================

If more than one argument can be consumed, a table is used to store the data.
Expand Down Expand Up @@ -89,3 +90,21 @@ The ``choices`` property can be used to restrict an argument to a set of choices
Usage: script.lua [-h] {north,south,east,west}

Error: argument 'direction' must be one of 'north', 'south', 'east', 'west'

The ``...`` value can be used to capture all remaining command-line arguments, including options-like tokens. This is useful when passing arguments through to another command.

.. code-block:: lua
:linenos:

parser:argument "pass_through"
:args "..."

.. code-block:: none

$ lua script.lua foo --bar baz

.. code-block:: lua

{
pass_through = {"foo", "--bar", "baz"}
}
19 changes: 19 additions & 0 deletions docsrc/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,25 @@ Just as arguments, options can be configured to take several command line argume

Note that the data passed to ``optional`` option is stored in an array. That is necessary to distinguish whether the option was invoked without an argument or it was not invoked at all.

The ``...`` value works the same as for positional arguments: after the option is invoked, all remaining command-line arguments are captured as its values, including options-like tokens.

.. code-block:: lua
:linenos:

parser:option "--rest"
:args "..."

.. code-block:: none

$ lua script.lua --rest foo --bar baz

.. code-block:: lua

{
rest = {"foo", "--bar", "baz"}
}


Setting argument choices
------------------------

Expand Down
29 changes: 29 additions & 0 deletions spec/arguments_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -171,5 +171,34 @@ describe("tests related to positional arguments", function()
parse:parse{"foo", "quu"}
end, "argument 'foo' must be one of 'bar', 'baz', 'qu'")
end)

it("handles remainder argument correctly", function()
local parser = Parser()
parser:argument "foo" {
args = "..."
}
local args = parser:parse({"bar", "--baz", "-qu"})
assert.same({foo = {"bar", "--baz", "-qu"}}, args)
end)

it("stops parsing options after remainder argument starts", function()
local parser = Parser()
parser:option "-f" "--foo"
parser:argument "rest" {
args = "..."
}
local args = parser:parse({"--foo", "value", "--foo", "other"})
assert.same({foo = "value", rest = {"--foo", "other"}}, args)
end)

it("captures all remaining arguments including options-like", function()
local parser = Parser()
parser:argument "input"
parser:argument "rest" {
args = "..."
}
local args = parser:parse({"in", "--foo", "-bar", "baz"})
assert.same({input = "in", rest = {"--foo", "-bar", "baz"}}, args)
end)
end)
end)
57 changes: 57 additions & 0 deletions spec/commands_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,61 @@ describe("tests related to commands", function()

assert.has_error(function() parser:parse{"run"} end, "unknown command 'run'")
end)

it("stops parsing options after command with nargs='...'", function()
local parser = Parser "name"
:add_help(false)
local cmd = parser:command "run"
:args("...")
cmd:option "--foo"

local args = parser:parse{"run", "--bar", "--baz", "qux"}
assert.same({run = {"--bar", "--baz", "qux"}}, args)
end)

it("parses options before command with nargs='...'", function()
local parser = Parser "name"
:add_help(false)
parser:option "--global"
local cmd = parser:command "run"
:args("...")
cmd:option "--foo"

local args = parser:parse{"--global", "value", "run", "--bar", "--baz"}
assert.same({global = "value", run = {"--bar", "--baz"}}, args)
end)

it("handles command with nargs='...' without remainder args", function()
local parser = Parser "name"
:add_help(false)
local cmd = parser:command "run"
:args("...")
cmd:option "--foo"

local args = parser:parse{"run"}
assert.same({run = true}, args)
end)

it("handles nested commands with nargs='...'", function()
local parser = Parser "name"
:add_help(false)
local foo = parser:command "foo"
local bar = foo:command "bar"
:args("...")
bar:option "--opt"

local args = parser:parse{"foo", "bar", "--something", "--else"}
assert.same({foo = true, bar = {"--something", "--else"}}, args)
end)

it("command with nargs='...' captures options-like strings", function()
local parser = Parser "name"
:add_help(false)
parser:option "--before"
local cmd = parser:command "exec"
:args("...")

local args = parser:parse{"--before", "val", "exec", "-x", "--flag", "arg"}
assert.same({before = "val", exec = {"-x", "--flag", "arg"}}, args)
end)
end)
19 changes: 19 additions & 0 deletions spec/options_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -339,5 +339,24 @@ describe("tests related to options", function()
function() parser:parse{}
end, "missing option '-f'")
end)

it("handles remainder option correctly", function()
local parser = Parser()
parser:option "-r" "--rest" {
args = "..."
}
local args = parser:parse({"--rest", "foo", "--bar", "-baz"})
assert.same({rest = {"foo", "--bar", "-baz"}}, args)
end)

it("stops parsing after remainder option starts", function()
local parser = Parser()
parser:option "-f" "--foo"
parser:option "-r" "--rest" {
args = "..."
}
local args = parser:parse({"--rest", "foo", "--foo", "bar"})
assert.same({rest = {"foo", "--foo", "bar"}}, args)
end)
end)
end)
88 changes: 83 additions & 5 deletions src/argparse.lua
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ local function parse_boundaries(str)
return 0, 1
end

if str == "..." then
return 0, math.huge, true
end

if str:match "^%d+%-%d+$" then
local min, max = str:match "^(%d+)%-(%d+)$"
return tonumber(min), tonumber(max)
Expand All @@ -186,13 +190,17 @@ local function boundaries(name)
return {name, function(self, value)
typecheck(name, {"number", "string"}, value)

local min, max = parse_boundaries(value)
local min, max, remainder = parse_boundaries(value)

if not min then
error(("bad property '%s'"):format(name))
end

self["_min" .. name], self["_max" .. name] = min, max

if remainder then
self["_remainder" .. name] = true
end
end}
end

Expand Down Expand Up @@ -276,7 +284,9 @@ local Parser = class({

local Command = class({
_aliases = {},
_public_aliases = {}
_public_aliases = {},
_minargs = 0,
_maxargs = 0,
}, {
args = 3,
multiname,
Expand All @@ -298,7 +308,8 @@ local Command = class({
typechecked("help_description_margin", "number"),
typechecked("help_max_width", "number"),
typechecked("hidden", "boolean"),
add_help
add_help,
boundaries("args")
}, Parser)

local Argument = class({
Expand Down Expand Up @@ -396,6 +407,10 @@ function Argument:_get_argument_list()
table.insert(buf, "...")
end

if self._remainderargs then
table.insert(buf, "...")
end

return buf
end

Expand Down Expand Up @@ -1909,16 +1924,56 @@ function ParseState:invoke(option, name)
end

function ParseState:pass(arg)
if self._stop_parsing then
if self.option and self.option.element._remainderargs then
self.option:pass(arg)
return
end

if self.argument and self.argument.element._remainderargs then
self.argument:pass(arg)
return
end

-- If a command with nargs="..." triggered stop, collect remaining args
if self._command_remainderargs_table then
table.insert(self._command_remainderargs_table, arg)
return
end
end

if self.option then
if self.option.element._remainderargs then
self.option:pass(arg)
self._stop_parsing = true
return
end

if not self.option:pass(arg) then
self.option = nil
-- If next argument is a remainder argument, stop option parsing
if self.argument and self.argument.element._remainderargs then
self._stop_parsing = true
end
end
elseif self.argument then
if self.argument.element._remainderargs then
self:check_mutexes(self.argument)
self.argument:pass(arg)
self._stop_parsing = true
self.handle_options = false
return
end

self:check_mutexes(self.argument)

if not self.argument:pass(arg) then
self.argument_i = self.argument_i + 1
self.argument = self.arguments[self.argument_i]

if self.argument and self.argument.element._remainderargs then
self._stop_parsing = true
end
end
else
local command = self:get_command(arg)
Expand All @@ -1928,6 +1983,13 @@ function ParseState:pass(arg)
self.result[self.parser._command_target] = command._name
end

-- If command has nargs="...", prepare to collect remaining args
if command._remainderargs then
self._command_remainder_target = command._target or command._name
self._command_remainderargs_table = {}
self._stop_parsing = true
end

self:switch(command)
end
end
Expand All @@ -1951,6 +2013,14 @@ function ParseState:finalize()
end
end

-- Store command remainder args into result
if self._command_remainderargs_table and self._command_remainder_target then
-- If there are remainder args, store them; otherwise keep the true value
if #self._command_remainderargs_table > 0 then
self.result[self._command_remainder_target] = self._command_remainderargs_table
end
end

if self.parser._require_command and #self.commands > 0 then
self:error("a command is required")
end
Expand Down Expand Up @@ -1991,10 +2061,14 @@ function ParseState:parse(args)
for _, arg in ipairs(args) do
local plain = true

if self.handle_options then
if not self._stop_parsing and not self._command_remainderargs_table and self.handle_options then
local first = arg:sub(1, 1)

if self.charset[first] then
-- Skip option parsing if current argument is remainder args and we've already collected at least one arg
-- But allow the first option to be parsed normally
if self.argument and self.argument.element._remainderargs and #self.argument.args > 0 then
plain = true
elseif self.charset[first] then
if #arg > 1 then
plain = false

Expand Down Expand Up @@ -2044,6 +2118,10 @@ function ParseState:parse(args)
if plain then
self:pass(arg)
end

if self._stop_parsing then
self.handle_options = false
end
end

self:finalize()
Expand Down