diff --git a/docsrc/arguments.rst b/docsrc/arguments.rst index 02d65ef..f946f47 100644 --- a/docsrc/arguments.rst +++ b/docsrc/arguments.rst @@ -68,3 +68,24 @@ If more than one argument can be consumed, a table is used to store the data. pair = {"foo", "bar"}, optional = "baz" } + +Setting argument choices +------------------------ + +The ``choices`` property can be used to restrict an argument to a set of choices. Its value is an array of string choices. + +.. code-block:: lua + :linenos: + + parser:argument "direction" + :choices {"north", "south", "east", "west"} + +.. code-block:: none + + $ lua script.lua foo + +.. code-block:: none + + Usage: script.lua [-h] {north,south,east,west} + + Error: argument 'direction' must be one of 'north', 'south', 'east', 'west' diff --git a/docsrc/misc.rst b/docsrc/misc.rst index bdd74e4..05a42ab 100644 --- a/docsrc/misc.rst +++ b/docsrc/misc.rst @@ -242,6 +242,7 @@ Property Type ``defmode`` String ``show_default`` Boolean ``argname`` String or table +``choices`` Table ``action`` Function or string ``init`` Any ``hidden`` Boolean @@ -273,6 +274,7 @@ Property Type ``show_default`` Boolean ``overwrite`` Booleans ``argname`` String or table +``choices`` Table ``action`` Function or string ``init`` Any ``hidden`` Boolean diff --git a/docsrc/options.rst b/docsrc/options.rst index bcc7556..d8f7a6e 100644 --- a/docsrc/options.rst +++ b/docsrc/options.rst @@ -107,6 +107,27 @@ 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. +Setting argument choices +------------------------ + +The ``choices`` property can be used to specify a list of choices for an option argument in the same way as for arguments. + +.. code-block:: lua + :linenos: + + parser:option "--format" + :choices {"short", "medium", "full"} + +.. code-block:: none + + $ lua script.lua --format foo + +.. code-block:: none + + Usage: script.lua [-h] [--format {short,medium,full}] + + Error: argument for option '--format' must be one of 'short', 'medium', 'full' + Setting number of invocations ----------------------------- diff --git a/spec/arguments_spec.lua b/spec/arguments_spec.lua index 8a1dbef..5cf09c4 100644 --- a/spec/arguments_spec.lua +++ b/spec/arguments_spec.lua @@ -161,5 +161,15 @@ describe("tests related to positional arguments", function() } assert.has_error(function() parser:parse{} end, "missing argument 'foo1'") end) + + it("handles invalid argument choices correctly", function() + local parse = Parser() + parse:argument "foo" { + choices = {"bar", "baz", "qu"} + } + assert.has_error(function() + parse:parse{"foo", "quu"} + end, "argument 'foo' must be one of 'bar', 'baz', 'qu'") + end) end) end) diff --git a/spec/help_spec.lua b/spec/help_spec.lua index 311104a..3c9c7a1 100644 --- a/spec/help_spec.lua +++ b/spec/help_spec.lua @@ -77,6 +77,34 @@ Options: --config ]], parser:get_help()) end) + it("creates correct help message for arguments with choices", function() + local parser = Parser "foo" + parser:argument "move" + :choices {"rock", "paper", "scissors"} + + assert.equal([[ +Usage: foo [-h] {rock,paper,scissors} + +Arguments: + {rock,paper,scissors} + +Options: + -h, --help Show this help message and exit.]], parser:get_help()) + end) + + it("creates correct help message for options with argument choices", function() + local parser = Parser "foo" + parser:option "--format" + :choices {"short", "medium", "full"} + + assert.equal([[ +Usage: foo [-h] [--format {short,medium,full}] + +Options: + -h, --help Show this help message and exit. + --format {short,medium,full}]], parser:get_help()) + end) + it("adds margin for multiline descriptions", function() local parser = Parser "foo" parser:flag "-v" diff --git a/spec/options_spec.lua b/spec/options_spec.lua index 40fa4b0..4fdffe8 100644 --- a/spec/options_spec.lua +++ b/spec/options_spec.lua @@ -290,6 +290,16 @@ describe("tests related to options", function() end, "too many arguments") end) + it("handles invalid argument choices correctly", function() + local parse = Parser() + parse:option "-s" "--server" { + choices = {"foo", "bar", "baz"} + } + assert.has_error(function() + parse:parse{"-slocalhost"} + end, "argument for option '-s' must be one of 'foo', 'bar', 'baz'") + end) + it("doesn't accept GNU-like long options when it doesn't need arguments", function() local parser = Parser() parser:flag "-q" "--quiet" diff --git a/spec/usage_spec.lua b/spec/usage_spec.lua index 072980d..3bb881e 100644 --- a/spec/usage_spec.lua +++ b/spec/usage_spec.lua @@ -89,6 +89,30 @@ Usage: foo ) end) + it("creates correct usage message for arguments with choices", function() + local parser = Parser "foo" + :add_help(false) + parser:argument "move" + :choices {"rock", "paper", "scissors"} + + assert.equal( + [=[Usage: foo {rock,paper,scissors}]=], + parser:get_usage() + ) + end) + + it("creates correct usage message for options with argument choices", function() + local parser = Parser "foo" + :add_help(false) + parser:option "--format" + :choices {"short", "medium", "full"} + + assert.equal( + [=[Usage: foo [--format {short,medium,full}]]=], + parser:get_usage() + ) + end) + it("creates correct usage message for commands", function() local parser = Parser "foo" :add_help(false) diff --git a/src/argparse.lua b/src/argparse.lua index 91ab584..06a252b 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -341,6 +341,7 @@ local Argument = class({ typechecked("defmode", "string"), typechecked("show_default", "boolean"), typechecked("argname", "string", "table"), + typechecked("choices", "table"), typechecked("hidden", "boolean"), option_action, option_init @@ -363,6 +364,7 @@ local Option = class({ typechecked("show_default", "boolean"), typechecked("overwrite", "boolean"), typechecked("argname", "string", "table"), + typechecked("choices", "table"), typechecked("hidden", "boolean"), option_action, option_init @@ -511,17 +513,33 @@ function Argument:_get_argname(narg) end end +function Argument:_get_choices_list() + return "{" .. table.concat(self._choices, ",") .. "}" +end + function Argument:_get_default_argname() - return "<" .. self._name .. ">" + if self._choices then + return self:_get_choices_list() + else + return "<" .. self._name .. ">" + end end function Option:_get_default_argname() - return "<" .. self:_get_default_target() .. ">" + if self._choices then + return self:_get_choices_list() + else + return "<" .. self:_get_default_target() .. ">" + end end -- Returns labels to be shown in the help message. function Argument:_get_label_lines() - return {self._name} + if self._choices then + return {self:_get_choices_list()} + else + return {self._name} + end end function Option:_get_label_lines() @@ -1204,7 +1222,21 @@ function ElementState:invoke() return self.open end +function ElementState:check_choices(argument) + if self.element._choices then + for _, choice in ipairs(self.element._choices) do + if argument == choice then + return + end + end + local choices_list = "'" .. table.concat(self.element._choices, "', '") .. "'" + local is_option = getmetatable(self.element) == Option + self:error("%s%s must be one of %s", is_option and "argument for " or "", self.name, choices_list) + end +end + function ElementState:pass(argument) + self:check_choices(argument) argument = self:convert(argument, #self.args + 1) table.insert(self.args, argument)