├── .gitignore ├── .luacheckrc ├── .luacov ├── docsrc ├── index.rst ├── mutexes.rst ├── arguments.rst ├── defaults.rst ├── commands.rst ├── parsers.rst ├── options.rst ├── callbacks.rst ├── misc.rst ├── conf.py └── messages.rst ├── .travis.yml ├── argparse-scm-1.rockspec ├── spec ├── script.lua ├── pparse_spec.lua ├── invalid_property_spec.lua ├── convert_spec.lua ├── commands_spec.lua ├── tip_spec.lua ├── mutex_spec.lua ├── integrity_spec.lua ├── default_spec.lua ├── arguments_spec.lua ├── actions_spec.lua ├── usage_spec.lua ├── options_spec.lua └── help_spec.lua ├── LICENSE ├── README.md ├── CHANGELOG.md └── src └── argparse.lua /.gitignore: -------------------------------------------------------------------------------- 1 | luacov.report.out 2 | luacov.stats.out 3 | doc 4 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "min" 2 | files["spec/"].std = "+busted" 3 | -------------------------------------------------------------------------------- /.luacov: -------------------------------------------------------------------------------- 1 | return {modules = {argparse = "src/argparse.lua"}} 2 | -------------------------------------------------------------------------------- /docsrc/index.rst: -------------------------------------------------------------------------------- 1 | Argparse tutorial 2 | ================= 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | 8 | parsers 9 | arguments 10 | options 11 | mutexes 12 | commands 13 | defaults 14 | callbacks 15 | messages 16 | misc 17 | 18 | This is a tutorial for `argparse `_, a feature-rich command line parser for Lua. 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | 4 | env: 5 | - LUA="lua 5.1" 6 | - LUA="lua 5.2" 7 | - LUA="lua 5.3" 8 | - LUA="luajit 2.0" 9 | - LUA="luajit 2.1" 10 | 11 | before_install: 12 | - pip install codecov 13 | - pip install hererocks 14 | - hererocks lua_install --$LUA -r latest 15 | - source lua_install/bin/activate 16 | - luarocks install busted 17 | - luarocks install cluacov 18 | - luarocks install luacheck 19 | 20 | install: 21 | - luarocks make 22 | 23 | script: 24 | - luacheck src spec 25 | - busted -c 26 | 27 | after_script: 28 | - luacov 29 | - codecov -X gcov 30 | -------------------------------------------------------------------------------- /argparse-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "argparse" 2 | version = "scm-1" 3 | source = { 4 | url = "git+https://github.com/mpeterv/argparse.git" 5 | } 6 | description = { 7 | summary = "A feature-rich command-line argument parser", 8 | detailed = "Argparse supports positional arguments, options, flags, optional arguments, subcommands and more. Argparse automatically generates usage, help and error messages.", 9 | homepage = "https://github.com/mpeterv/argparse", 10 | license = "MIT" 11 | } 12 | dependencies = { 13 | "lua >= 5.1, < 5.4" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | argparse = "src/argparse.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /spec/script.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | local Parser = require "argparse" 3 | 4 | local parser = Parser() 5 | :description "A testing program." 6 | :require_command(false) 7 | 8 | parser:argument "input" 9 | 10 | parser:flag "-v" "--verbose" 11 | :description "Sets verbosity level." 12 | :target "verbosity" 13 | :count "0-2" 14 | 15 | local install = parser:command "install" 16 | :description "Install a rock." 17 | 18 | install:argument "rock" 19 | :description "Name of the rock." 20 | 21 | install:argument "version" 22 | :description "Version of the rock." 23 | :args "?" 24 | 25 | install:option "-f" "--from" 26 | :description "Fetch the rock from this server." 27 | :target "server" 28 | 29 | parser:get_usage() 30 | parser:get_help() 31 | local args = parser:parse() 32 | 33 | print(args.input) 34 | print(args.verbosity) 35 | print(args.install) 36 | print(args.rock) 37 | print(args.version) 38 | print(args.server) 39 | -------------------------------------------------------------------------------- /spec/pparse_spec.lua: -------------------------------------------------------------------------------- 1 | local Parser = require "argparse" 2 | getmetatable(Parser()).error = function(_, msg) error(msg) end 3 | 4 | describe("tests related to :pparse()", function() 5 | it("returns true and result on success", function() 6 | local parser = Parser() 7 | parser:option "-s --server" 8 | local ok, args = parser:pparse{"--server", "foo"} 9 | assert.is_true(ok) 10 | assert.same({server = "foo"}, args) 11 | end) 12 | 13 | it("returns false and bare error message on failure", function() 14 | local parser = Parser() 15 | parser:argument "foo" 16 | local ok, errmsg = parser:pparse{} 17 | assert.is_false(ok) 18 | assert.equal("missing argument 'foo'", errmsg) 19 | end) 20 | 21 | it("rethrows errors from callbacks", function() 22 | local parser = Parser() 23 | parser:flag "--foo" 24 | :action(function() error("some error message") end) 25 | assert.error_matches(function() parser:pparse{"--foo"} end, "some error message") 26 | end) 27 | end) 28 | -------------------------------------------------------------------------------- /docsrc/mutexes.rst: -------------------------------------------------------------------------------- 1 | Mutually exclusive groups 2 | ========================= 3 | 4 | A group of arguments and options can be marked as mutually exclusive using ``:mutex(argument_or_option, ...)`` method of the Parser class. 5 | 6 | .. code-block:: lua 7 | :linenos: 8 | 9 | parser:mutex( 10 | parser:argument "input" 11 | :args "?", 12 | parser:flag "--process-stdin" 13 | ) 14 | 15 | parser:mutex( 16 | parser:flag "-q --quiet", 17 | parser:flag "-v --verbose" 18 | ) 19 | 20 | If more than one element of a mutually exclusive group is used, an error is raised. 21 | 22 | .. code-block:: none 23 | 24 | $ lua script.lua -qv 25 | 26 | .. code-block:: none 27 | 28 | Usage: script.lua ([-q] | [-v]) [-h] ([] | [--process-stdin]) 29 | 30 | Error: option '-v' can not be used together with option '-q' 31 | 32 | .. code-block:: none 33 | 34 | $ lua script.lua file --process-stdin 35 | 36 | .. code-block:: none 37 | 38 | Usage: script.lua ([-q] | [-v]) [-h] ([] | [--process-stdin]) 39 | 40 | Error: option '--process-stdin' can not be used together with argument 'input' 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 - 2018 Peter Melnichenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /spec/invalid_property_spec.lua: -------------------------------------------------------------------------------- 1 | local Parser = require "argparse" 2 | 3 | describe("invalid property detection", function() 4 | it("detects properties with invalid type", function() 5 | assert.has_error(function() 6 | Parser():name(12345) 7 | end, "bad property 'name' (string expected, got number)") 8 | 9 | assert.has_error(function() 10 | Parser():option "--foo":convert(true) 11 | end, "bad property 'convert' (function or table expected, got boolean)") 12 | end) 13 | 14 | it("detects invalid count and args properties", function() 15 | assert.has_error(function() 16 | Parser():option "--foo":count(false) 17 | end, "bad property 'count' (number or string expected, got boolean)") 18 | 19 | assert.has_error(function() 20 | Parser():option "--foo":args({}) 21 | end, "bad property 'args' (number or string expected, got table)") 22 | 23 | assert.has_error(function() 24 | Parser():option "--foo":count("foobar") 25 | end, "bad property 'count'") 26 | 27 | assert.has_error(function() 28 | Parser():option "--foo":args("123-") 29 | end, "bad property 'args'") 30 | end) 31 | 32 | it("detects unknown named actions", function() 33 | assert.has_error(function() 34 | Parser():option "--foo":action(false) 35 | end, "bad property 'action' (function or string expected, got boolean)") 36 | 37 | assert.has_error(function() 38 | Parser():option "--foo":action("catcat") 39 | end, "unknown action 'catcat'") 40 | end) 41 | end) 42 | -------------------------------------------------------------------------------- /docsrc/arguments.rst: -------------------------------------------------------------------------------- 1 | Adding and configuring arguments 2 | ================================ 3 | 4 | Positional arguments can be added using ``:argument(name, description, default, convert, args)`` method. It returns an Argument instance, which can be configured in the same way as Parsers. The ``name`` property is required. 5 | 6 | This and the following examples show contents of the result table returned by `parser:parse()` when the script is executed with given command-line arguments. 7 | 8 | .. code-block:: lua 9 | :linenos: 10 | 11 | parser:argument "input" 12 | 13 | .. code-block:: none 14 | 15 | $ lua script.lua foo 16 | 17 | .. code-block:: lua 18 | 19 | { 20 | input = "foo" 21 | } 22 | 23 | The data passed to the argument is stored in the result table at index ``input`` because it is the argument's name. The index can be changed using ``target`` property. 24 | 25 | Setting number of consumed arguments 26 | ------------------------------------ 27 | 28 | ``args`` property sets how many command line arguments the argument consumes. Its value is interpreted as follows: 29 | 30 | ================================================= ============================= 31 | Value Interpretation 32 | ================================================= ============================= 33 | Number ``N`` Exactly ``N`` arguments 34 | String ``A-B``, where ``A`` and ``B`` are numbers From ``A`` to ``B`` arguments 35 | String ``N+``, where ``N`` is a number ``N`` or more arguments 36 | String ``?`` An optional argument 37 | String ``*`` Any number of arguments 38 | String ``+`` At least one argument 39 | ================================================= ============================= 40 | 41 | If more than one argument can be consumed, a table is used to store the data. 42 | 43 | .. code-block:: lua 44 | :linenos: 45 | 46 | parser:argument("pair", "A pair of arguments.") 47 | :args(2) 48 | parser:argument("optional", "An optional argument.") 49 | :args "?" 50 | 51 | .. code-block:: none 52 | 53 | $ lua script.lua foo bar 54 | 55 | .. code-block:: lua 56 | 57 | { 58 | pair = {"foo", "bar"} 59 | } 60 | 61 | .. code-block:: none 62 | 63 | $ lua script.lua foo bar baz 64 | 65 | .. code-block:: lua 66 | 67 | { 68 | pair = {"foo", "bar"}, 69 | optional = "baz" 70 | } 71 | -------------------------------------------------------------------------------- /spec/convert_spec.lua: -------------------------------------------------------------------------------- 1 | local Parser = require "argparse" 2 | getmetatable(Parser()).error = function(_, msg) error(msg) end 3 | 4 | describe("tests related to converters", function() 5 | it("converts arguments", function() 6 | local parser = Parser() 7 | parser:argument "numbers" { 8 | convert = tonumber, 9 | args = "+" 10 | } 11 | 12 | local args = parser:parse{"1", "2", "500"} 13 | assert.same({numbers = {1, 2, 500}}, args) 14 | end) 15 | 16 | it("accepts an array of converters", function() 17 | local function tocoords(str) 18 | local x, y = str:match("^([^,]*),([^,]*)$") 19 | x = tonumber(x) 20 | y = tonumber(y) 21 | return x and y and {x, y} 22 | end 23 | 24 | local parser = Parser() 25 | parser:option "-c --circle" { 26 | convert = {tonumber, tocoords}, 27 | args = 2 28 | } 29 | 30 | local args = parser:parse{"-c", "123", "456,567"} 31 | assert.same({circle = {123, {456, 567}}}, args) 32 | end) 33 | 34 | it("converts arguments using mapping", function() 35 | local choice = { 36 | foo = 1, 37 | bar = 2 38 | } 39 | 40 | local parser = Parser() 41 | parser:argument "choice" { 42 | convert = choice, 43 | args = "+" 44 | } 45 | 46 | local args = parser:parse{"foo", "bar"} 47 | assert.same({choice = {1, 2}}, args) 48 | end) 49 | 50 | it("accepts false", function() 51 | local function toboolean(x) 52 | if x == "true" then 53 | return true 54 | elseif x == "false" then 55 | return false 56 | end 57 | end 58 | 59 | local parser = Parser() 60 | parser:argument "booleans" { 61 | convert = toboolean, 62 | args = "+" 63 | } 64 | 65 | local args = parser:parse{"true", "false"} 66 | assert.same({booleans = {true, false}}, args) 67 | end) 68 | 69 | it("raises an error when it can't convert", function() 70 | local parser = Parser() 71 | parser:argument "numbers" { 72 | convert = tonumber, 73 | args = "+" 74 | } 75 | 76 | assert.has_error(function() parser:parse{"foo", "bar", "baz"} end, "malformed argument 'foo'") 77 | end) 78 | 79 | it("second return value is used as error message", function() 80 | local parser = Parser() 81 | parser:argument "numbers" { 82 | convert = function(x) return tonumber(x), x .. " is not a number" end 83 | } 84 | 85 | assert.has_error(function() parser:parse{"foo"} end, "foo is not a number") 86 | end) 87 | end) 88 | -------------------------------------------------------------------------------- /spec/commands_spec.lua: -------------------------------------------------------------------------------- 1 | local Parser = require "argparse" 2 | getmetatable(Parser()).error = function(_, msg) error(msg) end 3 | 4 | describe("tests related to commands", function() 5 | it("handles commands after arguments", function() 6 | local parser = Parser "name" 7 | parser:argument "file" 8 | parser:command "create" 9 | parser:command "remove" 10 | 11 | local args = parser:parse{"temp.txt", "remove"} 12 | assert.same({file = "temp.txt", remove = true}, args) 13 | end) 14 | 15 | it("switches context properly", function() 16 | local parser = Parser "name" 17 | :add_help(false) 18 | local install = parser:command "install" 19 | install:flag "-q" "--quiet" 20 | 21 | local args = parser:parse{"install", "-q"} 22 | assert.same({install = true, quiet = true}, args) 23 | assert.has_error(function() parser:parse{"-q", "install"} end, "unknown option '-q'") 24 | end) 25 | 26 | it("uses command_target property to save command name", function() 27 | local parser = Parser "name" 28 | :add_help(false) 29 | :command_target("command") 30 | local install = parser:command "install" 31 | install:flag "-q" "--quiet" 32 | 33 | local args = parser:parse{"install", "-q"} 34 | assert.same({install = true, quiet = true, command = "install"}, args) 35 | end) 36 | 37 | it("allows to continue passing old options", function() 38 | local parser = Parser "name" 39 | parser:flag "-v" "--verbose" { 40 | count = "*" 41 | } 42 | parser:command "install" 43 | 44 | local args = parser:parse{"-vv", "install", "--verbose"} 45 | assert.same({install = true, verbose = 3}, args) 46 | end) 47 | 48 | it("handles nested commands", function() 49 | local parser = Parser "name" 50 | local foo = parser:command "foo" 51 | foo:command "bar" 52 | foo:command "baz" 53 | 54 | local args = parser:parse{"foo", "bar"} 55 | assert.same({foo = true, bar = true}, args) 56 | end) 57 | 58 | it("handles no commands depending on parser.require_command", function() 59 | local parser = Parser "name" 60 | parser:command "install" 61 | 62 | assert.has_error(function() parser:parse{} end, "a command is required") 63 | 64 | parser:require_command(false) 65 | local args = parser:parse{} 66 | assert.same({}, args) 67 | end) 68 | 69 | it("Detects wrong commands", function() 70 | local parser = Parser "name" 71 | parser:command "install" 72 | 73 | assert.has_error(function() parser:parse{"run"} end, "unknown command 'run'") 74 | end) 75 | end) 76 | -------------------------------------------------------------------------------- /docsrc/defaults.rst: -------------------------------------------------------------------------------- 1 | Default values 2 | ============== 3 | 4 | For elements such as arguments and options, if ``default`` property is set to a string, its value is stored in case the element was not used (if it's not a string, it'll be used as ``init`` property instead, see :ref:`actions`). 5 | 6 | .. code-block:: lua 7 | :linenos: 8 | 9 | parser:option("-o --output", "Output file.", "a.out") 10 | -- Equivalent: 11 | parser:option "-o" "--output" 12 | :description "Output file." 13 | :default "a.out" 14 | 15 | .. code-block:: none 16 | 17 | $ lua script.lua 18 | 19 | .. code-block:: lua 20 | 21 | { 22 | output = "a.out" 23 | } 24 | 25 | The existence of a default value is reflected in help message, unless ``show_default`` property is set to ``false``. 26 | 27 | .. code-block:: none 28 | 29 | $ lua script.lua --help 30 | 31 | .. code-block:: none 32 | 33 | Usage: script.lua [-o ] [-h] 34 | 35 | Options: 36 | -o , --output 37 | Output file. (default: a.out) 38 | -h, --help Show this help message and exit. 39 | 40 | Note that invocation without required arguments is still an error. 41 | 42 | .. code-block:: none 43 | 44 | $ lua script.lua -o 45 | 46 | .. code-block:: none 47 | 48 | Usage: script.lua [-o ] [-h] 49 | 50 | Error: too few arguments 51 | 52 | Default mode 53 | ------------ 54 | 55 | ``defmode`` property regulates how argparse should use the default value of an element. 56 | 57 | By default, or if ``defmode`` contains ``u`` (for unused), the default value will be automatically passed to the element if it was not invoked at all. 58 | It will be passed minimal required of times, so that if the element is allowed to consume no arguments (e.g. using ``:args "?"``), the default value is ignored. 59 | 60 | If ``defmode`` contains ``a`` (for argument), the default value will be automatically passed to the element if not enough arguments were passed, or not enough invocations were made. 61 | 62 | Consider the difference: 63 | 64 | .. code-block:: lua 65 | :linenos: 66 | 67 | parser:option "-o" 68 | :default "a.out" 69 | parser:option "-p" 70 | :default "password" 71 | :defmode "arg" 72 | 73 | .. code-block:: none 74 | 75 | $ lua script.lua -h 76 | 77 | .. code-block:: none 78 | 79 | Usage: script.lua [-o ] [-p [

]] [-h] 80 | 81 | Options: 82 | -o default: a.out 83 | -p [

] default: password 84 | -h, --help Show this help message and exit. 85 | 86 | .. code-block:: none 87 | 88 | $ lua script.lua 89 | 90 | .. code-block:: lua 91 | 92 | { 93 | o = "a.out" 94 | } 95 | 96 | .. code-block:: none 97 | 98 | $ lua script.lua -p 99 | 100 | 101 | .. code-block:: lua 102 | 103 | { 104 | o = "a.out", 105 | p = "password" 106 | } 107 | 108 | .. code-block:: none 109 | 110 | $ lua script.lua -o 111 | 112 | .. code-block:: none 113 | 114 | Usage: script.lua [-o ] [-p [

]] [-h] 115 | 116 | Error: too few arguments 117 | -------------------------------------------------------------------------------- /spec/tip_spec.lua: -------------------------------------------------------------------------------- 1 | local Parser = require "argparse" 2 | getmetatable(Parser()).error = function(_, msg) error(msg) end 3 | 4 | describe("tests related to tips", function() 5 | describe("provides tips when data is too long", function() 6 | it("for options", function() 7 | local parser = Parser() 8 | parser:option "-q" "--quiet" 9 | 10 | assert.has_error(function() parser:parse{"--quiett=true"} end, 11 | "unknown option '--quiett'\nDid you mean '--quiet'?") 12 | end) 13 | 14 | it("for commands", function() 15 | local parser = Parser "name" 16 | parser:command "install" 17 | 18 | assert.has_error(function() parser:parse{"installq"} end, 19 | "unknown command 'installq'\nDid you mean 'install'?") 20 | end) 21 | end) 22 | 23 | describe("provides tips when data is too short", function() 24 | it("for options", function() 25 | local parser = Parser() 26 | parser:option "-q" "--quiet" 27 | 28 | assert.has_error(function() parser:parse{"--quet=true"} end, 29 | "unknown option '--quet'\nDid you mean '--quiet'?") 30 | end) 31 | 32 | it("for commands", function() 33 | local parser = Parser "name" 34 | parser:command "install" 35 | 36 | assert.has_error(function() parser:parse{"nstall"} end, 37 | "unknown command 'nstall'\nDid you mean 'install'?") 38 | end) 39 | end) 40 | 41 | describe("provides tips on substitution", function() 42 | it("for options", function() 43 | local parser = Parser() 44 | parser:option "-q" "--quiet" 45 | 46 | assert.has_error(function() parser:parse{"--qriet=true"} end, 47 | "unknown option '--qriet'\nDid you mean '--quiet'?") 48 | end) 49 | 50 | it("for commands", function() 51 | local parser = Parser "name" 52 | parser:command "install" 53 | 54 | assert.has_error(function() parser:parse{"inntall"} end, 55 | "unknown command 'inntall'\nDid you mean 'install'?") 56 | end) 57 | end) 58 | 59 | describe("provides tips on transpositions", function() 60 | it("for options", function() 61 | local parser = Parser() 62 | parser:option "-q" "--quiet" 63 | 64 | assert.has_error(function() parser:parse{"--queit=true"} end, 65 | "unknown option '--queit'\nDid you mean '--quiet'?") 66 | end) 67 | 68 | it("for commands", function() 69 | local parser = Parser "name" 70 | parser:command "install" 71 | 72 | assert.has_error(function() parser:parse{"isntall"} end, 73 | "unknown command 'isntall'\nDid you mean 'install'?") 74 | end) 75 | end) 76 | 77 | describe("provides multiple tips", function() 78 | it("for options", function() 79 | local parser = Parser() 80 | parser:option "-q" "--quiet" 81 | parser:option "--quick" 82 | 83 | assert.has_error(function() parser:parse{"--quiec=true"} end, 84 | "unknown option '--quiec'\nDid you mean one of these: '--quick' '--quiet'?") 85 | end) 86 | 87 | it("for commands", function() 88 | local parser = Parser "name" 89 | parser:command "install" 90 | parser:command "instant" 91 | 92 | assert.has_error(function() parser:parse{"instanl"} end, 93 | "unknown command 'instanl'\nDid you mean one of these: 'install' 'instant'?") 94 | end) 95 | end) 96 | end) 97 | -------------------------------------------------------------------------------- /docsrc/commands.rst: -------------------------------------------------------------------------------- 1 | Adding and configuring commands 2 | =============================== 3 | 4 | A command is a subparser invoked when its name is passed as an argument. For example, in `git `_ CLI ``add``, ``commit``, ``push``, etc. are commands. Each command has its own set of arguments and options, but inherits options of its parent. 5 | 6 | Commands can be added using ``:command(name, description, epilog)`` method. Just as options, commands can have several aliases. 7 | 8 | .. code-block:: lua 9 | :linenos: 10 | 11 | parser:command "install i" 12 | 13 | If a command it used, ``true`` is stored in the corresponding field of the result table. 14 | 15 | .. code-block:: none 16 | 17 | $ lua script.lua install 18 | 19 | .. code-block:: lua 20 | 21 | { 22 | install = true 23 | } 24 | 25 | A typo will result in an appropriate error message. 26 | 27 | .. code-block:: none 28 | 29 | $ lua script.lua instal 30 | 31 | .. code-block:: none 32 | 33 | Usage: script.lua [-h] ... 34 | 35 | Error: unknown command 'instal' 36 | Did you mean 'install'? 37 | 38 | Getting name of selected command 39 | -------------------------------- 40 | 41 | Use ``command_target`` property of the parser to store the name of used command in a field of the result table. 42 | 43 | .. code-block:: lua 44 | :linenos: 45 | 46 | parser:command_target("command") 47 | parser:command("install") 48 | parser:command("remove") 49 | 50 | .. code-block:: none 51 | 52 | $ lua script.lua install 53 | 54 | .. code-block:: lua 55 | 56 | { 57 | install = true, 58 | command = "install" 59 | } 60 | 61 | Adding elements to commands 62 | --------------------------- 63 | 64 | The Command class is a subclass of the Parser class, so all the Parser's methods for adding elements work on commands, too. 65 | 66 | .. code-block:: lua 67 | :linenos: 68 | 69 | local install = parser:command "install" 70 | install:argument "rock" 71 | install:option "-f --from" 72 | 73 | .. code-block:: none 74 | 75 | $ lua script.lua install foo --from=bar 76 | 77 | 78 | .. code-block:: lua 79 | 80 | { 81 | install = true, 82 | rock = "foo", 83 | from = "bar" 84 | } 85 | 86 | Commands have their own usage and help messages. 87 | 88 | .. code-block:: none 89 | 90 | $ lua script.lua install 91 | 92 | .. code-block:: none 93 | 94 | Usage: script.lua install [-f ] [-h] 95 | 96 | Error: too few arguments 97 | 98 | .. code-block:: none 99 | 100 | $ lua script.lua install --help 101 | 102 | .. code-block:: none 103 | 104 | Usage: script.lua install [-f ] [-h] 105 | 106 | Arguments: 107 | rock 108 | 109 | Options: 110 | -f , --from 111 | -h, --help Show this help message and exit. 112 | 113 | Making a command optional 114 | ------------------------- 115 | 116 | By default, if a parser has commands, using one of them is obligatory. 117 | 118 | 119 | .. code-block:: lua 120 | :linenos: 121 | 122 | local parser = argparse() 123 | parser:command "install" 124 | 125 | .. code-block:: none 126 | 127 | $ lua script.lua 128 | 129 | .. code-block:: none 130 | 131 | Usage: script.lua [-h] ... 132 | 133 | Error: a command is required 134 | 135 | This can be changed using ``require_command`` property. 136 | 137 | .. code-block:: lua 138 | :linenos: 139 | 140 | local parser = argparse() 141 | :require_command(false) 142 | parser:command "install" 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # argparse 2 | 3 | [![Build Status](https://travis-ci.org/mpeterv/argparse.png?branch=master)](https://travis-ci.org/mpeterv/argparse) 4 | [![Coverage status](https://codecov.io/gh/mpeterv/argparse/branch/master/graph/badge.svg)](https://codecov.io/gh/mpeterv/argparse) 5 | 6 | Argparse is a feature-rich command line parser for Lua inspired by argparse for Python. 7 | 8 | Argparse supports positional arguments, options, flags, optional arguments, subcommands and more. Argparse automatically generates usage, help and error messages. 9 | 10 | ## Contents 11 | 12 | * [Example](#example) 13 | * [Installation](#installation) 14 | * [Tutorial](#tutorial) 15 | * [Testing](#testing) 16 | * [License](#license) 17 | 18 | ## Example 19 | 20 | Simple example: 21 | 22 | ```lua 23 | -- script.lua 24 | local argparse = require "argparse" 25 | 26 | local parser = argparse("script", "An example.") 27 | parser:argument("input", "Input file.") 28 | parser:option("-o --output", "Output file.", "a.out") 29 | parser:option("-I --include", "Include locations."):count("*") 30 | 31 | local args = parser:parse() 32 | ``` 33 | 34 | `args` contents depending on command line arguments: 35 | 36 | ```bash 37 | $ lua script.lua foo 38 | ``` 39 | 40 | ```lua 41 | { 42 | input = "foo", 43 | output = "a.out", 44 | include = {} 45 | } 46 | ``` 47 | 48 | ```bash 49 | $ lua script.lua foo -I/usr/local/include -Isrc -o bar 50 | ``` 51 | 52 | ```lua 53 | { 54 | input = "foo", 55 | output = "bar", 56 | include = {"/usr/local/include", "src"} 57 | } 58 | ``` 59 | 60 | Error messages depending on command line arguments: 61 | 62 | ```bash 63 | $ lua script.lua foo bar 64 | ``` 65 | 66 | ``` 67 | Usage: script [-o ] [-I ] [-h] 68 | 69 | Error: too many arguments 70 | ``` 71 | 72 | ```bash 73 | $ lua script.lua --help 74 | ``` 75 | 76 | ``` 77 | Usage: script [-o ] [-I ] [-h] 78 | 79 | An example. 80 | 81 | Arguments: 82 | input Input file. 83 | 84 | Options: 85 | -o , --output 86 | Output file. (default: a.out) 87 | -I , --include 88 | Include locations. 89 | -h, --help Show this help message and exit. 90 | ``` 91 | 92 | ```bash 93 | $ lua script.lua foo --outptu=bar 94 | ``` 95 | 96 | ``` 97 | Usage: script [-o ] [-I ] [-h] 98 | 99 | Error: unknown option '--outptu' 100 | Did you mean '--output'? 101 | ``` 102 | 103 | ## Installation 104 | 105 | ### Using LuaRocks 106 | 107 | Installing argparse using [LuaRocks](http://luarocks.org) is simple: 108 | 109 | ```bash 110 | $ luarocks install argparse 111 | ``` 112 | 113 | ### Without LuaRocks 114 | 115 | Download `src/argparse.lua` file and put it into the directory for Lua libraries or your working directory. 116 | 117 | ## Tutorial 118 | 119 | The tutorial is available [online](http://argparse.readthedocs.org). If argparse has been installed using LuaRocks 2.1.2 or later, it can be viewed using `luarocks doc argparse` command. 120 | 121 | Tutorial HTML files can be built using [Sphinx](http://sphinx-doc.org/): `sphinx-build docsrc doc`, the files will be found inside `doc/`. 122 | 123 | ## Testing 124 | 125 | argparse comes with a testing suite located in `spec` directory. [busted](http://olivinelabs.com/busted/) is required for testing, it can be installed using LuaRocks. Run the tests using `busted` command from the argparse folder. 126 | 127 | ## License 128 | 129 | argparse is licensed under the same terms as Lua itself (MIT license). 130 | -------------------------------------------------------------------------------- /docsrc/parsers.rst: -------------------------------------------------------------------------------- 1 | Creating and using parsers 2 | ========================== 3 | 4 | The ``argparse`` module is a function which, when called, creates an instance of the Parser class. 5 | 6 | .. code-block:: lua 7 | :linenos: 8 | 9 | -- script.lua 10 | local argparse = require "argparse" 11 | local parser = argparse() 12 | 13 | ``parser`` is now an empty parser which does not recognize any command line arguments or options. 14 | 15 | Parsing command line arguments 16 | ------------------------------ 17 | 18 | ``:parse([argv])`` method of the Parser class returns a table with processed data from the command line or ``argv`` array. 19 | 20 | .. code-block:: lua 21 | :linenos: 22 | 23 | local args = parser:parse() 24 | 25 | After this is executed with ``lua script.lua``, ``args`` is an empty table because the parser is empty and no command line arguments were supplied. 26 | 27 | Error handling 28 | ^^^^^^^^^^^^^^ 29 | 30 | If the provided command line arguments are not recognized by the parser, it will print an error message and call ``os.exit(1)``. 31 | 32 | .. code-block:: none 33 | 34 | $ lua script.lua foo 35 | 36 | .. code-block:: none 37 | 38 | Usage: script.lua [-h] 39 | 40 | Error: too many arguments 41 | 42 | If halting the program is undesirable, ``:pparse([args])`` method should be used. It returns boolean flag indicating success of parsing and result or error message. 43 | 44 | An error can raised manually using ``:error()`` method. 45 | 46 | .. code-block:: lua 47 | :linenos: 48 | 49 | parser:error("manual argument validation failed") 50 | 51 | .. code-block:: none 52 | 53 | Usage: script.lua [-h] 54 | 55 | Error: manual argument validation failed 56 | 57 | Help option 58 | ^^^^^^^^^^^ 59 | 60 | As the automatically generated usage message states, there is a help option ``-h`` added to any parser by default. 61 | 62 | When a help option is used, parser will print a help message and call ``os.exit(0)``. 63 | 64 | .. code-block:: none 65 | 66 | $ lua script.lua -h 67 | 68 | .. code-block:: none 69 | 70 | Usage: script.lua [-h] 71 | 72 | Options: 73 | -h, --help Show this help message and exit. 74 | 75 | Typo autocorrection 76 | ^^^^^^^^^^^^^^^^^^^ 77 | 78 | When an option is not recognized by the parser, but there is an option with a similar name, a suggestion is automatically added to the error message. 79 | 80 | .. code-block:: none 81 | 82 | $ lua script.lua --hepl 83 | 84 | .. code-block:: none 85 | 86 | Usage: script.lua [-h] 87 | 88 | Error: unknown option '--hepl' 89 | Did you mean '--help'? 90 | 91 | Configuring parsers 92 | ------------------- 93 | 94 | Parsers have several properties affecting their behavior. For example, ``description`` and ``epilog`` properties set the text to be displayed in the help message after the usage message and after the listings of options and arguments, respectively. Another is ``name``, which overwrites the name of the program which is used in the usage message (default value is inferred from command line arguments). 95 | 96 | There are several ways to set properties. The first is to chain setter methods of Parser object. 97 | 98 | .. code-block:: lua 99 | :linenos: 100 | 101 | local parser = argparse() 102 | :name "script" 103 | :description "A testing script." 104 | :epilog "For more info, see http://example.com" 105 | 106 | The second is to call a parser with a table containing some properties. 107 | 108 | .. code-block:: lua 109 | :linenos: 110 | 111 | local parser = argparse() { 112 | name = "script", 113 | description = "A testing script.", 114 | epilog "For more info, see http://example.com." 115 | } 116 | 117 | Finally, ``name``. ``description`` and ``epilog`` properties can be passed as arguments when calling a parser. 118 | 119 | .. code-block:: lua 120 | :linenos: 121 | 122 | local parser = argparse("script", "A testing script.", "For more info, see http://example.com.") 123 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.6.0 (2018-04-13) 4 | 5 | ### New features 6 | 7 | * An array of functions can now be as the value of `converter` property, 8 | so that multi-argumnet options can use different conversion rules 9 | for different arguments (#14). 10 | * Mutexes can now include positional arguments (#11). 11 | * Added `hidden` property for arguments, options and commands, 12 | removing them from the autogenerated usage and help strings. 13 | * Added `:group(name, ...)` method to Parser and Command objects, allowing 14 | custom grouping of arguments, options, and commands in autogenerated 15 | help string. 16 | * Added `help_vertical_space` property for configuring number of extra 17 | newlines between help strings for different arguments and options in 18 | autogenerated help string. 19 | * Added `usage_margin` and `usage_max_width` properties for configuring 20 | usage string autogeneration. 21 | * Added `help_usage_margin` and `help_description_margin` properties 22 | for configuring help string autogeneration. 23 | * Added `help_max_width` property. If set, descriptions in help string 24 | are automatically wrapped to fit into given number of columns. 25 | * Argparse version is now available as `argparse.version`. 26 | 27 | ### Improvements 28 | 29 | * `--` can now be used as a normal option name, with arguments 30 | after `--` always passed to it verbatim (#17). 31 | * When generating help messages for options with arguments and multiple 32 | aliases, usage strings for different aliases are put on separate lines and 33 | vertically aligned (#15). 34 | 35 | ## 0.5.0 (2015-12-09) 36 | 37 | ### New features 38 | 39 | * Actions can now be used to implement custom processing logic. 40 | * Added actions for arguments. 41 | * Added string aliases for actions such as `store_false`. 42 | * Command actions are now called after the parsing, with result target 43 | as the argument. 44 | * Added `command_target` property for storing name of used command. 45 | 46 | ### Improvements 47 | 48 | * Improved error messages on missing arguments. 49 | * `-f123` is now interpreted as `-f 123` when `-f` takes an optional argument. 50 | 51 | ## 0.4.1 (2015-08-08) 52 | 53 | ### Miscellaneous 54 | 55 | * Added license header to `argparse.lua` for ease of bundling 56 | and packaging (#3). 57 | 58 | ## 0.4.0 (2015-06-20) 59 | 60 | ### Breaking changes 61 | 62 | * Removed `aliases` property, aliases can now be set using several space 63 | separated strings as name, or, as it was possible before, by setting `name` 64 | property several times. 65 | 66 | ### New features 67 | 68 | * Added `handle_options` property (#2). 69 | * Often used properties now can be passed as arguments when calling or 70 | constructing an element. 71 | 72 | ### Improvements 73 | 74 | * Cleaned up trailing whitespace in generated messages. 75 | 76 | ## 0.3.2 (2015-01-15) 77 | 78 | ### Miscellaneous 79 | 80 | * Argparse no longer bundles 30log. 81 | 82 | ## 0.3.1 (2014-11-06) 83 | 84 | ### Fixes 85 | 86 | * Fixed incompatibility with old Luarocks versions. 87 | 88 | ## 0.3.0 (2014-08-25) 89 | 90 | ### New features 91 | 92 | * New `show_default` field disables automatic indication of default values in 93 | help messages. 94 | 95 | ### Improvements 96 | 97 | * In default targets `-` is now replaced with `_`. 98 | * Changed usage message generation to show options taking variable number of 99 | arguments after positional arguments. 100 | 101 | ### Fixes 102 | 103 | * Fixed incompatibility with strict.lua and other checkers (#1). 104 | 105 | ## 0.2.0 (2014-03-09) 106 | 107 | ### New features 108 | 109 | * Added mutually exclusive groups. 110 | * Options and arguments can now be configured to use different argument 111 | placeholders for first, second, etc. arguments. E.g. `--pair `. 112 | 113 | ### Fixes 114 | 115 | * Fixed script name inference not working for `Parser:get_help()` and 116 | `Parser:get_usage()` methods. 117 | 118 | ## 0.1.0 (2014-03-02) 119 | 120 | Initial release. 121 | -------------------------------------------------------------------------------- /spec/mutex_spec.lua: -------------------------------------------------------------------------------- 1 | local Parser = require "argparse" 2 | getmetatable(Parser()).error = function(_, msg) error(msg) end 3 | 4 | describe("tests related to mutexes", function() 5 | it("handles mutex correctly", function() 6 | local parser = Parser() 7 | parser:mutex( 8 | parser:flag "-q" "--quiet" 9 | :description "Supress logging. ", 10 | parser:flag "-v" "--verbose" 11 | :description "Print additional debug information. " 12 | ) 13 | 14 | local args = parser:parse{"-q"} 15 | assert.same({quiet = true}, args) 16 | args = parser:parse{"-v"} 17 | assert.same({verbose = true}, args) 18 | args = parser:parse{} 19 | assert.same({}, args) 20 | end) 21 | 22 | it("handles mutex with an argument", function() 23 | local parser = Parser() 24 | parser:mutex( 25 | parser:flag "-q" "--quiet" 26 | :description "Supress output.", 27 | parser:argument "log" 28 | :args "?" 29 | :description "Log file" 30 | ) 31 | 32 | local args = parser:parse{"-q"} 33 | assert.same({quiet = true}, args) 34 | args = parser:parse{"log.txt"} 35 | assert.same({log = "log.txt"}, args) 36 | args = parser:parse{} 37 | assert.same({}, args) 38 | end) 39 | 40 | it("handles mutex with default value", function() 41 | local parser = Parser() 42 | parser:mutex( 43 | parser:flag "-q" "--quiet", 44 | parser:option "-o" "--output" 45 | :default "a.out" 46 | ) 47 | 48 | local args = parser:parse{"-q"} 49 | assert.same({quiet = true, output = "a.out"}, args) 50 | end) 51 | 52 | it("raises an error if mutex is broken", function() 53 | local parser = Parser() 54 | parser:mutex( 55 | parser:flag "-q" "--quiet" 56 | :description "Supress logging. ", 57 | parser:flag "-v" "--verbose" 58 | :description "Print additional debug information. " 59 | ) 60 | 61 | assert.has_error(function() 62 | parser:parse{"-qv"} 63 | end, "option '-v' can not be used together with option '-q'") 64 | assert.has_error(function() 65 | parser:parse{"-v", "--quiet"} 66 | end, "option '--quiet' can not be used together with option '-v'") 67 | end) 68 | 69 | it("raises an error if mutex with an argument is broken", function() 70 | local parser = Parser() 71 | parser:mutex( 72 | parser:flag "-q" "--quiet" 73 | :description "Supress output.", 74 | parser:argument "log" 75 | :args "?" 76 | :description "Log file" 77 | ) 78 | 79 | assert.has_error(function() 80 | parser:parse{"-q", "log.txt"} 81 | end, "argument 'log' can not be used together with option '-q'") 82 | assert.has_error(function() 83 | parser:parse{"log.txt", "--quiet"} 84 | end, "option '--quiet' can not be used together with argument 'log'") 85 | end) 86 | 87 | it("handles multiple mutexes", function() 88 | local parser = Parser() 89 | parser:mutex( 90 | parser:flag "-q" "--quiet", 91 | parser:flag "-v" "--verbose" 92 | ) 93 | parser:mutex( 94 | parser:flag "-l" "--local", 95 | parser:option "-f" "--from" 96 | ) 97 | 98 | local args = parser:parse{"-qq", "-fTHERE"} 99 | assert.same({quiet = true, from = "THERE"}, args) 100 | args = parser:parse{"-vl"} 101 | assert.same({verbose = true, ["local"] = true}, args) 102 | end) 103 | 104 | it("handles mutexes in commands", function() 105 | local parser = Parser() 106 | parser:mutex( 107 | parser:flag "-q" "--quiet", 108 | parser:flag "-v" "--verbose" 109 | ) 110 | local install = parser:command "install" 111 | install:mutex( 112 | install:flag "-l" "--local", 113 | install:option "-f" "--from" 114 | ) 115 | 116 | local args = parser:parse{"install", "-l"} 117 | assert.same({install = true, ["local"] = true}, args) 118 | assert.has_error(function() 119 | parser:parse{"install", "-qlv"} 120 | end, "option '-v' can not be used together with option '-q'") 121 | end) 122 | end) 123 | -------------------------------------------------------------------------------- /docsrc/options.rst: -------------------------------------------------------------------------------- 1 | Adding and configuring options 2 | ============================== 3 | 4 | Options can be added using ``:option(name, description, default, convert, args, count)`` method. It returns an Option instance, which can be configured in the same way as Parsers. The ``name`` property is required. An option can have several aliases, which can be set as space separated substrings in its name or by continuously setting ``name`` property. 5 | 6 | .. code-block:: lua 7 | :linenos: 8 | 9 | -- These lines are equivalent: 10 | parser:option "-f" "--from" 11 | parser:option "-f --from" 12 | 13 | .. code-block:: none 14 | 15 | $ lua script.lua --from there 16 | $ lua script.lua --from=there 17 | $ lua script.lua -f there 18 | $ lua script.lua -fthere 19 | 20 | .. code-block:: lua 21 | 22 | { 23 | from = "there" 24 | } 25 | 26 | For an option, default index used to store arguments passed to it is the first "long" alias (an alias starting with two control characters, typically hyphens) or just the first alias, without control characters. Hyphens in the default index are replaced with underscores. In the following table it is assumed that ``local args = parser:parse()`` has been executed. 27 | 28 | ======================== ============================== 29 | Option's aliases Location of option's arguments 30 | ======================== ============================== 31 | ``-o`` ``args.o`` 32 | ``-o`` ``--output`` ``args.output`` 33 | ``-s`` ``--from-server`` ``args.from_server`` 34 | ======================== ============================== 35 | 36 | As with arguments, the index can be explicitly set using ``target`` property. 37 | 38 | Flags 39 | ----- 40 | 41 | Flags are almost identical to options, except that they don't take an argument by default. 42 | 43 | .. code-block:: lua 44 | :linenos: 45 | 46 | parser:flag("-q --quiet") 47 | 48 | .. code-block:: none 49 | 50 | $ lua script.lua -q 51 | 52 | .. code-block:: lua 53 | 54 | { 55 | quiet = true 56 | } 57 | 58 | Control characters 59 | ------------------ 60 | 61 | The first characters of all aliases of all options of a parser form the set of control characters, used to distinguish options from arguments. Typically the set only consists of a hyphen. 62 | 63 | Setting number of consumed arguments 64 | ------------------------------------ 65 | 66 | Just as arguments, options can be configured to take several command line arguments. 67 | 68 | .. code-block:: lua 69 | :linenos: 70 | 71 | parser:option "--pair" 72 | :args(2) 73 | parser:option "--optional" 74 | :args "?" 75 | 76 | .. code-block:: none 77 | 78 | $ lua script.lua --pair foo bar 79 | 80 | .. code-block:: lua 81 | 82 | { 83 | pair = {"foo", "bar"} 84 | } 85 | 86 | .. code-block:: none 87 | 88 | $ lua script.lua --pair foo bar --optional 89 | 90 | .. code-block:: lua 91 | 92 | { 93 | pair = {"foo", "bar"}, 94 | optional = {} 95 | } 96 | 97 | .. code-block:: none 98 | 99 | $ lua script.lua --optional=baz 100 | 101 | .. code-block:: lua 102 | 103 | { 104 | optional = {"baz"} 105 | } 106 | 107 | 108 | 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. 109 | 110 | Setting number of invocations 111 | ----------------------------- 112 | 113 | For options, it is possible to control how many times they can be used. argparse uses ``count`` property to set how many times an option can be invoked. The value of the property is interpreted in the same way ``args`` is. 114 | 115 | .. code-block:: lua 116 | :linenos: 117 | 118 | parser:option("-e --exclude") 119 | :count "*" 120 | 121 | .. code-block:: none 122 | 123 | $ lua script.lua -eFOO -eBAR 124 | 125 | .. code-block:: lua 126 | 127 | { 128 | exclude = {"FOO", "BAR"} 129 | } 130 | 131 | If an option can be used more than once and it can consume more than one argument, the data is stored as an array of invocations, each being an array of arguments. 132 | 133 | As a special case, if an option can be used more than once and it consumes no arguments (e.g. it's a flag), than the number of invocations is stored in the associated field of the result table. 134 | 135 | .. code-block:: lua 136 | :linenos: 137 | 138 | parser:flag("-v --verbose", "Sets verbosity level.") 139 | :count "0-2" 140 | :target "verbosity" 141 | 142 | .. code-block:: none 143 | 144 | $ lua script.lua -vv 145 | 146 | .. code-block:: lua 147 | 148 | { 149 | verbosity = 2 150 | } 151 | -------------------------------------------------------------------------------- /spec/integrity_spec.lua: -------------------------------------------------------------------------------- 1 | local script = "./spec/script.lua" 2 | local script_cmd = "lua" 3 | 4 | if package.loaded["luacov.runner"] then 5 | script_cmd = script_cmd .. " -lluacov" 6 | end 7 | 8 | script_cmd = script_cmd .. " " .. script 9 | 10 | local function get_output(args) 11 | local handler = io.popen(script_cmd .. " " .. args .. " 2>&1", "r") 12 | local output = handler:read("*a") 13 | handler:close() 14 | return output 15 | end 16 | 17 | describe("tests related to CLI behaviour #unsafe", function() 18 | describe("error messages", function() 19 | it("generates correct error message without arguments", function() 20 | assert.equal([[ 21 | Usage: ]]..script..[[ [-v] [-h] [] ... 22 | 23 | Error: missing argument 'input' 24 | ]], get_output("")) 25 | end) 26 | 27 | it("generates correct error message with too many arguments", function() 28 | assert.equal([[ 29 | Usage: ]]..script..[[ [-v] [-h] [] ... 30 | 31 | Error: unknown command 'bar' 32 | ]], get_output("foo bar")) 33 | end) 34 | 35 | it("generates correct error message with unexpected argument", function() 36 | assert.equal([[ 37 | Usage: ]]..script..[[ [-v] [-h] [] ... 38 | 39 | Error: option '--verbose' does not take arguments 40 | ]], get_output("--verbose=true")) 41 | end) 42 | 43 | it("generates correct error message with unexpected option", function() 44 | assert.equal([[ 45 | Usage: ]]..script..[[ [-v] [-h] [] ... 46 | 47 | Error: unknown option '-q' 48 | Did you mean one of these: '-h' '-v'? 49 | ]], get_output("-vq")) 50 | end) 51 | 52 | it("generates correct error message and tip with unexpected command", function() 53 | assert.equal([[ 54 | Usage: ]]..script..[[ [-v] [-h] [] ... 55 | 56 | Error: unknown command 'nstall' 57 | Did you mean 'install'? 58 | ]], get_output("foo nstall")) 59 | end) 60 | 61 | it("generates correct error message without arguments in command", function() 62 | assert.equal([[ 63 | Usage: ]]..script..[[ install [-f ] [-h] [] 64 | 65 | Error: missing argument 'rock' 66 | ]], get_output("foo install")) 67 | end) 68 | 69 | it("generates correct error message and tip in command", function() 70 | assert.equal([[ 71 | Usage: ]]..script..[[ install [-f ] [-h] [] 72 | 73 | Error: unknown option '--form' 74 | Did you mean '--from'? 75 | ]], get_output("foo install bar --form=there")) 76 | end) 77 | end) 78 | 79 | describe("help messages", function() 80 | it("generates correct help message", function() 81 | assert.equal([[ 82 | Usage: ]]..script..[[ [-v] [-h] [] ... 83 | 84 | A testing program. 85 | 86 | Arguments: 87 | input 88 | 89 | Options: 90 | -v, --verbose Sets verbosity level. 91 | -h, --help Show this help message and exit. 92 | 93 | Commands: 94 | install Install a rock. 95 | ]], get_output("--help")) 96 | end) 97 | 98 | it("generates correct help message for command", function() 99 | assert.equal([[ 100 | Usage: ]]..script..[[ install [-f ] [-h] [] 101 | 102 | Install a rock. 103 | 104 | Arguments: 105 | rock Name of the rock. 106 | version Version of the rock. 107 | 108 | Options: 109 | -f , Fetch the rock from this server. 110 | --from 111 | -h, --help Show this help message and exit. 112 | ]], get_output("foo install --help")) 113 | end) 114 | end) 115 | 116 | describe("data flow", function() 117 | it("works with one argument", function() 118 | local handler = io.popen(script_cmd .. " foo 2>&1", "r") 119 | assert.equal("foo", handler:read "*l") 120 | assert.equal("0", handler:read "*l") 121 | handler:close() 122 | end) 123 | 124 | it("works with one argument and a flag", function() 125 | local handler = io.popen(script_cmd .. " -v foo --verbose 2>&1", "r") 126 | assert.equal("foo", handler:read "*l") 127 | assert.equal("2", handler:read "*l") 128 | handler:close() 129 | end) 130 | 131 | it("works with command", function() 132 | local handler = io.popen(script_cmd .. " foo -v install bar 2>&1", "r") 133 | assert.equal("foo", handler:read "*l") 134 | assert.equal("1", handler:read "*l") 135 | assert.equal("true", handler:read "*l") 136 | assert.equal("bar", handler:read "*l") 137 | assert.equal("nil", handler:read "*l") 138 | assert.equal("nil", handler:read "*l") 139 | handler:close() 140 | end) 141 | 142 | it("works with command and options", function() 143 | local handler = io.popen(script_cmd .. " foo --verbose install bar 0.1 --from=there -vv 2>&1", "r") 144 | assert.equal("foo", handler:read "*l") 145 | assert.equal("2", handler:read "*l") 146 | assert.equal("true", handler:read "*l") 147 | assert.equal("bar", handler:read "*l") 148 | assert.equal("0.1", handler:read "*l") 149 | assert.equal("there", handler:read "*l") 150 | handler:close() 151 | end) 152 | end) 153 | end) 154 | -------------------------------------------------------------------------------- /spec/default_spec.lua: -------------------------------------------------------------------------------- 1 | local Parser = require "argparse" 2 | getmetatable(Parser()).error = function(_, msg) error(msg) end 3 | 4 | describe("tests related to default values", function() 5 | describe("default values for arguments", function() 6 | it("handles default argument correctly", function() 7 | local parser = Parser() 8 | parser:argument "foo" 9 | :default "bar" 10 | local args = parser:parse{} 11 | assert.same({foo = "bar"}, args) 12 | args = parser:parse{"baz"} 13 | assert.same({foo = "baz"}, args) 14 | end) 15 | 16 | it("handles default argument for multi-argument correctly", function() 17 | local parser = Parser() 18 | parser:argument "foo" { 19 | args = 3, 20 | default = "bar", 21 | defmode = "arg" 22 | } 23 | local args = parser:parse{"baz"} 24 | assert.same({foo = {"baz", "bar", "bar"}}, args) 25 | end) 26 | 27 | it("handles default value for multi-argument correctly", function() 28 | local parser = Parser() 29 | parser:argument "foo" { 30 | args = 3, 31 | default = "bar" 32 | } 33 | local args = parser:parse{} 34 | assert.same({foo = {"bar", "bar", "bar"}}, args) 35 | end) 36 | 37 | it("does not use default values if not needed", function() 38 | local parser = Parser() 39 | parser:argument "foo" { 40 | args = "1-2", 41 | default = "bar" 42 | } 43 | local args = parser:parse({"baz"}) 44 | assert.same({foo = {"baz"}}, args) 45 | end) 46 | end) 47 | 48 | describe("default values for options", function() 49 | it("handles option with default value correctly", function() 50 | local parser = Parser() 51 | parser:option "-o" "--output" 52 | :default "a.out" 53 | :defmode "unused" 54 | local args = parser:parse{} 55 | assert.same({output = "a.out"}, args) 56 | args = parser:parse{"--output", "foo.txt"} 57 | assert.same({output = "foo.txt"}, args) 58 | assert.has_error(function() parser:parse{"-o"} end, "option '-o' requires an argument") 59 | end) 60 | 61 | it("handles option with default value for multi-argument option correctly", function() 62 | local parser = Parser() 63 | parser:option("-s --several", "Two or three things", "foo", nil, "2-3") 64 | local args = parser:parse{} 65 | assert.same({several = {"foo", "foo"}}, args) 66 | end) 67 | 68 | it("handles option with default value and argument", function() 69 | local parser = Parser() 70 | parser:option "-o" "--output" { 71 | default = "a.out", 72 | defmode = "arg+count" 73 | } 74 | local args = parser:parse{} 75 | assert.same({output = "a.out"}, args) 76 | args = parser:parse{"-o"} 77 | assert.same({output = "a.out"}, args) 78 | args = parser:parse{"-o", "value"} 79 | assert.same({output = "value"}, args) 80 | end) 81 | 82 | it("handles option with default argument correctly", function() 83 | local parser = Parser() 84 | parser:option "-p" "--protected" 85 | :target "password" 86 | :default "password" 87 | :defmode "arg" 88 | local args = parser:parse{"-p"} 89 | assert.same({password = "password"}, args) 90 | end) 91 | 92 | it("doesn't use default argument if option is not invoked", function() 93 | local parser = Parser() 94 | parser:option "-f" "--foo" { 95 | default = "bar", 96 | defmode = "arg" 97 | } 98 | local args = parser:parse{} 99 | assert.same({}, args) 100 | end) 101 | 102 | it("handles default multi-argument correctly", function() 103 | local parser = Parser() 104 | parser:option "-f" "--foo" { 105 | args = 3, 106 | default = "bar", 107 | defmode = "arg" 108 | } 109 | local args = parser:parse({"--foo=baz"}) 110 | assert.same({foo = {"baz", "bar", "bar"}}, args) 111 | end) 112 | 113 | it("does not use default values if not needed", function() 114 | local parser = Parser() 115 | parser:option "-f" "--foo" { 116 | args = "1-2", 117 | default = "bar", 118 | defmode = "arg" 119 | } 120 | local args = parser:parse({"-f", "baz"}) 121 | assert.same({foo = {"baz"}}, args) 122 | end) 123 | 124 | it("handles multi-count options with default value correctly", function() 125 | local parser = Parser() 126 | parser:option "-f" "--foo" { 127 | count = "*", 128 | default = "bar", 129 | defmode = "arg + count" 130 | } 131 | local args = parser:parse({"-f", "--foo=baz", "--foo"}) 132 | assert.same({foo = {"bar", "baz", "bar"}}, args) 133 | end) 134 | 135 | it("completes missing invocations for multi-count options with default argument", function() 136 | local parser = Parser() 137 | parser:option "-f" "--foo" { 138 | count = "2", 139 | default = "bar", 140 | defmode = "arg" 141 | } 142 | local args = parser:parse({"-ffff"}) 143 | assert.same({foo = {"fff", "bar"}}, args) 144 | end) 145 | end) 146 | end) 147 | -------------------------------------------------------------------------------- /spec/arguments_spec.lua: -------------------------------------------------------------------------------- 1 | local Parser = require "argparse" 2 | getmetatable(Parser()).error = function(_, msg) error(msg) end 3 | 4 | describe("tests related to positional arguments", function() 5 | describe("passing correct arguments", function() 6 | it("handles empty parser correctly", function() 7 | local parser = Parser() 8 | local args = parser:parse({}) 9 | assert.same({}, args) 10 | end) 11 | 12 | it("handles one argument correctly", function() 13 | local parser = Parser() 14 | parser:argument "foo" 15 | local args = parser:parse({"bar"}) 16 | assert.same({foo = "bar"}, args) 17 | end) 18 | 19 | it("handles optional argument correctly", function() 20 | local parser = Parser() 21 | parser:argument "foo" 22 | :args "?" 23 | local args = parser:parse({"bar"}) 24 | assert.same({foo = "bar"}, args) 25 | end) 26 | 27 | it("handles several arguments correctly", function() 28 | local parser = Parser() 29 | parser:argument "foo1" 30 | parser:argument "foo2" 31 | local args = parser:parse({"bar", "baz"}) 32 | assert.same({foo1 = "bar", foo2 = "baz"}, args) 33 | end) 34 | 35 | it("handles multi-argument correctly", function() 36 | local parser = Parser() 37 | parser:argument "foo" { 38 | args = "*" 39 | } 40 | local args = parser:parse({"bar", "baz", "qu"}) 41 | assert.same({foo = {"bar", "baz", "qu"}}, args) 42 | end) 43 | 44 | it("handles restrained multi-argument correctly", function() 45 | local parser = Parser() 46 | parser:argument "foo" { 47 | args = "2-4" 48 | } 49 | local args = parser:parse({"bar", "baz"}) 50 | assert.same({foo = {"bar", "baz"}}, args) 51 | end) 52 | 53 | it("handles several multi-arguments correctly", function() 54 | local parser = Parser() 55 | parser:argument "foo1" { 56 | args = "1-2" 57 | } 58 | parser:argument "foo2" { 59 | args = "*" 60 | } 61 | local args = parser:parse({"bar"}) 62 | assert.same({foo1 = {"bar"}, foo2 = {}}, args) 63 | args = parser:parse({"bar", "baz", "qu"}) 64 | assert.same({foo1 = {"bar", "baz"}, foo2 = {"qu"}}, args) 65 | end) 66 | 67 | it("handles hyphen correctly", function() 68 | local parser = Parser() 69 | parser:argument "foo" 70 | local args = parser:parse({"-"}) 71 | assert.same({foo = "-"}, args) 72 | end) 73 | 74 | it("handles double hyphen correctly", function() 75 | local parser = Parser() 76 | parser:argument "foo" 77 | local args = parser:parse({"--", "-q"}) 78 | assert.same({foo = "-q"}, args) 79 | end) 80 | end) 81 | 82 | describe("passing incorrect arguments", function() 83 | it("handles extra arguments with empty parser correctly", function() 84 | local parser = Parser() 85 | 86 | assert.has_error(function() parser:parse{"foo"} end, "too many arguments") 87 | end) 88 | 89 | it("handles extra arguments with one argument correctly", function() 90 | local parser = Parser() 91 | parser:argument "foo" 92 | 93 | assert.has_error(function() parser:parse{"bar", "baz"} end, "too many arguments") 94 | end) 95 | 96 | it("handles too few arguments with one argument correctly", function() 97 | local parser = Parser() 98 | parser:argument "foo" 99 | 100 | assert.has_error(function() parser:parse{} end, "missing argument 'foo'") 101 | end) 102 | 103 | it("handles extra arguments with several arguments correctly", function() 104 | local parser = Parser() 105 | parser:argument "foo1" 106 | parser:argument "foo2" 107 | 108 | assert.has_error(function() parser:parse{"bar", "baz", "qu"} end, "too many arguments") 109 | end) 110 | 111 | it("handles too few arguments with several arguments correctly", function() 112 | local parser = Parser() 113 | parser:argument "foo1" 114 | parser:argument "foo2" 115 | 116 | assert.has_error(function() parser:parse{"bar"} end, "missing argument 'foo2'") 117 | end) 118 | 119 | it("handles too few arguments with multi-argument correctly", function() 120 | local parser = Parser() 121 | parser:argument "foo" { 122 | args = "+" 123 | } 124 | assert.has_error(function() parser:parse{} end, "missing argument 'foo'") 125 | end) 126 | 127 | it("handles too many arguments with multi-argument correctly", function() 128 | local parser = Parser() 129 | parser:argument "foo" { 130 | args = "2-4" 131 | } 132 | assert.has_error(function() parser:parse{"foo", "bar", "baz", "qu", "quu"} end, "too many arguments") 133 | end) 134 | 135 | it("handles too few arguments with multi-argument correctly", function() 136 | local parser = Parser() 137 | parser:argument "foo" { 138 | args = "2-4" 139 | } 140 | assert.has_error(function() parser:parse{"foo"} end, "argument 'foo' requires at least 2 arguments") 141 | end) 142 | 143 | it("handles too many arguments with several multi-arguments correctly", function() 144 | local parser = Parser() 145 | parser:argument "foo1" { 146 | args = "1-2" 147 | } 148 | parser:argument "foo2" { 149 | args = "0-1" 150 | } 151 | assert.has_error(function() parser:parse{"foo", "bar", "baz", "qu"} end, "too many arguments") 152 | end) 153 | 154 | it("handles too few arguments with several multi-arguments correctly", function() 155 | local parser = Parser() 156 | parser:argument "foo1" { 157 | args = "1-2" 158 | } 159 | parser:argument "foo2" { 160 | args = "*" 161 | } 162 | assert.has_error(function() parser:parse{} end, "missing argument 'foo1'") 163 | end) 164 | end) 165 | end) 166 | -------------------------------------------------------------------------------- /docsrc/callbacks.rst: -------------------------------------------------------------------------------- 1 | Callbacks 2 | ========= 3 | 4 | Converters 5 | ---------- 6 | 7 | argparse can perform automatic validation and conversion on arguments. If ``convert`` property of an element is a function, it will be applied to all the arguments passed to it. The function should return ``nil`` and, optionally, an error message if conversion failed. Standard ``tonumber`` and ``io.open`` functions work exactly like that. 8 | 9 | .. code-block:: lua 10 | :linenos: 11 | 12 | parser:argument "input" 13 | :convert(io.open) 14 | parser:option "-t --times" 15 | :convert(tonumber) 16 | 17 | .. code-block:: none 18 | 19 | $ lua script.lua foo.txt -t5 20 | 21 | .. code-block:: lua 22 | 23 | { 24 | input = file_object, 25 | times = 5 26 | } 27 | 28 | .. code-block:: none 29 | 30 | $ lua script.lua nonexistent.txt 31 | 32 | .. code-block:: none 33 | 34 | Usage: script.lua [-t ] [-h] 35 | 36 | Error: nonexistent.txt: No such file or directory 37 | 38 | .. code-block:: none 39 | 40 | $ lua script.lua foo.txt --times=many 41 | 42 | .. code-block:: none 43 | 44 | Usage: script.lua [-t ] [-h] 45 | 46 | Error: malformed argument 'many' 47 | 48 | If ``convert`` property of an element is an array of functions, they will be used as converters for corresponding arguments 49 | in case the element accepts multiple arguments. 50 | 51 | .. code-block:: lua 52 | :linenos: 53 | 54 | parser:option "--line-style" 55 | :args(2) 56 | :convert {string.lower, tonumber} 57 | 58 | .. code-block:: none 59 | 60 | $ lua script.lua --line-style DASHED 1.5 61 | 62 | .. code-block:: lua 63 | 64 | { 65 | line_style = {"dashed", 1.5} 66 | } 67 | 68 | 69 | Table converters 70 | ^^^^^^^^^^^^^^^^ 71 | 72 | If convert property of an element is a table and doesn't have functions in array part, 73 | arguments passed to it will be used as keys. If a key is missing, an error is raised. 74 | 75 | .. code-block:: lua 76 | :linenos: 77 | 78 | parser:argument "choice" 79 | :convert { 80 | foo = "Something foo-related", 81 | bar = "Something bar-related" 82 | } 83 | 84 | .. code-block:: none 85 | 86 | $ lua script.lua bar 87 | 88 | .. code-block:: lua 89 | 90 | { 91 | choice = "Something bar-related" 92 | } 93 | 94 | .. code-block:: none 95 | 96 | $ lua script.lua baz 97 | 98 | .. code-block:: none 99 | 100 | Usage: script.lua [-h] 101 | 102 | Error: malformed argument 'baz' 103 | 104 | Actions 105 | ------- 106 | 107 | .. _actions: 108 | 109 | Argument and option actions 110 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 111 | 112 | argparse uses action callbacks to process invocations of arguments and options. Default actions simply put passed arguments into the result table as a single value or insert into an array depending on number of arguments the option can take and how many times it can be used. 113 | 114 | A custom action can be set using ``action`` property. An action must be a function. and will be called after each invocation of the option or the argument it is assigned to. Four arguments are passed: result table, target index in that table, an argument or an array of arguments passed by user, and overwrite flag used when an option is invoked too many times. 115 | 116 | Converters are applied before actions. 117 | 118 | Initial value to be stored at target index in the result table can be set using ``init`` property, or also using ``default`` property if the value is not a string. 119 | 120 | .. code-block:: lua 121 | :linenos: 122 | 123 | parser:option("--exceptions"):args("*"):action(function(args, _, exceptions) 124 | for _, exception in ipairs(exceptions) do 125 | table.insert(args.exceptions, exception) 126 | end 127 | end):init({"foo", "bar"}) 128 | 129 | parser:flag("--no-exceptions"):action(function(args) 130 | args.exceptions = {} 131 | end) 132 | 133 | .. code-block:: none 134 | 135 | $ lua script.lua --exceptions x y --exceptions z t 136 | 137 | .. code-block:: lua 138 | 139 | { 140 | exceptions = { 141 | "foo", 142 | "bar", 143 | "x", 144 | "y", 145 | "z", 146 | "t" 147 | } 148 | } 149 | 150 | .. code-block:: none 151 | 152 | $ lua script.lua --exceptions x y --no-exceptions 153 | 154 | .. code-block:: lua 155 | 156 | { 157 | exceptions = {} 158 | } 159 | 160 | Actions can also be used when a flag needs to print some message and exit without parsing remaining arguments. 161 | 162 | .. code-block:: lua 163 | :linenos: 164 | 165 | parser:flag("-v --version"):action(function() 166 | print("script v1.0.0") 167 | os.exit(0) 168 | end) 169 | 170 | .. code-block:: none 171 | 172 | $ lua script.lua -v 173 | 174 | .. code-block:: none 175 | 176 | script v1.0.0 177 | 178 | Built-in actions 179 | ^^^^^^^^^^^^^^^^ 180 | 181 | These actions can be referred to by their string names when setting ``action`` property: 182 | 183 | =========== ======================================================= 184 | Name Description 185 | =========== ======================================================= 186 | store Stores argument or arguments at target index. 187 | store_true Stores ``true`` at target index. 188 | store_false Stores ``false`` at target index. 189 | count Increments number at target index. 190 | append Appends argument or arguments to table at target index. 191 | concat Appends arguments one by one to table at target index. 192 | =========== ======================================================= 193 | 194 | Examples using ``store_false`` and ``concat`` actions: 195 | 196 | .. code-block:: lua 197 | :linenos: 198 | 199 | parser:flag("--candy") 200 | parser:flag("--no-candy"):target("candy"):action("store_false") 201 | parser:flag("--rain", "Enable rain", false) 202 | parser:option("--exceptions"):args("*"):action("concat"):init({"foo", "bar"}) 203 | 204 | .. code-block:: none 205 | 206 | $ lua script.lua 207 | 208 | .. code-block:: lua 209 | 210 | { 211 | rain = false 212 | } 213 | 214 | .. code-block:: none 215 | 216 | $ lua script.lua --candy 217 | 218 | .. code-block:: lua 219 | 220 | { 221 | candy = true, 222 | rain = false 223 | } 224 | 225 | .. code-block:: none 226 | 227 | $ lua script.lua --no-candy --rain 228 | 229 | .. code-block:: lua 230 | 231 | { 232 | candy = false, 233 | rain = true 234 | } 235 | 236 | .. code-block:: none 237 | 238 | $ lua script.lua --exceptions x y --exceptions z t 239 | 240 | .. code-block:: lua 241 | 242 | { 243 | exceptions = { 244 | "foo", 245 | "bar", 246 | "x", 247 | "y", 248 | "z", 249 | "t" 250 | }, 251 | rain = false 252 | } 253 | 254 | Command actions 255 | ^^^^^^^^^^^^^^^ 256 | 257 | Actions for parsers and commands are simply callbacks invoked after parsing, with result table and command name as the arguments. Actions for nested commands are called first. 258 | 259 | .. code-block:: lua 260 | :linenos: 261 | 262 | local install = parser:command("install"):action(function(args, name) 263 | print("Running " .. name) 264 | -- Use args here 265 | ) 266 | 267 | parser:action(function(args) 268 | print("Callbacks are fun!") 269 | end) 270 | 271 | .. code-block:: none 272 | 273 | $ lua script.lua install 274 | 275 | .. code-block:: none 276 | 277 | Running install 278 | Callbacks are fun! 279 | -------------------------------------------------------------------------------- /docsrc/misc.rst: -------------------------------------------------------------------------------- 1 | Miscellaneous 2 | ============= 3 | 4 | Argparse version 5 | ---------------- 6 | 7 | ``argparse`` module is a table with ``__call`` metamethod. ``argparse.version`` is a string in ``MAJOR.MINOR.PATCH`` format specifying argparse version. 8 | 9 | Overwriting default help option 10 | ------------------------------- 11 | 12 | If the property ``add_help`` of a parser is set to ``false``, no help option will be added to it. Otherwise, the value of the field will be used to configure it. 13 | 14 | .. code-block:: lua 15 | :linenos: 16 | 17 | local parser = argparse() 18 | :add_help "/?" 19 | 20 | .. code-block:: none 21 | 22 | $ lua script.lua /? 23 | 24 | .. code-block:: none 25 | 26 | Usage: script.lua [/?] 27 | 28 | Options: 29 | /? Show this help message and exit. 30 | 31 | Disabling option handling 32 | ------------------------- 33 | 34 | When ``handle_options`` property of a parser or a command is set to ``false``, all options will be passed verbatim to the argument list, as if the input included double-hyphens. 35 | 36 | .. code-block:: lua 37 | :linenos: 38 | 39 | parser:handle_options(false) 40 | parser:argument "input" 41 | :args "*" 42 | parser:option "-f" "--foo" 43 | :args "*" 44 | 45 | .. code-block:: none 46 | 47 | $ lua script.lua bar -f --foo bar 48 | 49 | .. code-block:: lua 50 | 51 | { 52 | input = {"bar", "-f", "--foo", "bar"} 53 | } 54 | 55 | Prohibiting overuse of options 56 | ------------------------------ 57 | 58 | By default, if an option is invoked too many times, latest invocations overwrite the data passed earlier. 59 | 60 | .. code-block:: lua 61 | :linenos: 62 | 63 | parser:option "-o --output" 64 | 65 | .. code-block:: none 66 | 67 | $ lua script.lua -oFOO -oBAR 68 | 69 | .. code-block:: lua 70 | 71 | { 72 | output = "BAR" 73 | } 74 | 75 | Set ``overwrite`` property to ``false`` to prohibit this behavior. 76 | 77 | .. code-block:: lua 78 | :linenos: 79 | 80 | parser:option "-o --output" 81 | :overwrite(false) 82 | 83 | .. code-block:: none 84 | 85 | $ lua script.lua -oFOO -oBAR 86 | 87 | .. code-block:: none 88 | 89 | Usage: script.lua [-o ] [-h] 90 | 91 | Error: option '-o' must be used at most 1 time 92 | 93 | Parsing algorithm 94 | ----------------- 95 | 96 | argparse interprets command line arguments in the following way: 97 | 98 | ============= ================================================================================================================ 99 | Argument Interpretation 100 | ============= ================================================================================================================ 101 | ``foo`` An argument of an option or a positional argument. 102 | ``--foo`` An option. 103 | ``--foo=bar`` An option and its argument. The option must be able to take arguments. 104 | ``-f`` An option. 105 | ``-abcdef`` Letters are interpreted as options. If one of them can take an argument, the rest of the string is passed to it. 106 | ``--`` The rest of the command line arguments will be interpreted as positional arguments. 107 | ============= ================================================================================================================ 108 | 109 | Property lists 110 | -------------- 111 | 112 | Parser properties 113 | ^^^^^^^^^^^^^^^^^ 114 | 115 | Properties that can be set as arguments when calling or constructing a parser, in this order: 116 | 117 | =============== ====== 118 | Property Type 119 | =============== ====== 120 | ``name`` String 121 | ``description`` String 122 | ``epilog`` String 123 | =============== ====== 124 | 125 | Other properties: 126 | 127 | =========================== ========================== 128 | Property Type 129 | =========================== ========================== 130 | ``usage`` String 131 | ``help`` String 132 | ``require_command`` Boolean 133 | ``handle_options`` Boolean 134 | ``add_help`` Boolean or string or table 135 | ``command_target`` String 136 | ``usage_max_width`` Number 137 | ``usage_margin`` Number 138 | ``help_max_width`` Number 139 | ``help_usage_margin`` Number 140 | ``help_description_margin`` Number 141 | ``help_vertical_space`` Number 142 | =========================== ========================== 143 | 144 | Command properties 145 | ^^^^^^^^^^^^^^^^^^ 146 | 147 | Properties that can be set as arguments when calling or constructing a command, in this order: 148 | 149 | =============== ====== 150 | Property Type 151 | =============== ====== 152 | ``name`` String 153 | ``description`` String 154 | ``epilog`` String 155 | =============== ====== 156 | 157 | Other properties: 158 | 159 | =========================== ========================== 160 | Property Type 161 | =========================== ========================== 162 | ``target`` String 163 | ``usage`` String 164 | ``help`` String 165 | ``require_command`` Boolean 166 | ``handle_options`` Boolean 167 | ``action`` Function 168 | ``add_help`` Boolean or string or table 169 | ``command_target`` String 170 | ``hidden`` Boolean 171 | ``usage_max_width`` Number 172 | ``usage_margin`` Number 173 | ``help_max_width`` Number 174 | ``help_usage_margin`` Number 175 | ``help_description_margin`` Number 176 | ``help_vertical_space`` Number 177 | =========================== ========================== 178 | 179 | Argument properties 180 | ^^^^^^^^^^^^^^^^^^^ 181 | 182 | Properties that can be set as arguments when calling or constructing an argument, in this order: 183 | 184 | =============== ================= 185 | Property Type 186 | =============== ================= 187 | ``name`` String 188 | ``description`` String 189 | ``default`` Any 190 | ``convert`` Function or table 191 | ``args`` Number or string 192 | =============== ================= 193 | 194 | Other properties: 195 | 196 | =================== =============== 197 | Property Type 198 | =================== =============== 199 | ``target`` String 200 | ``defmode`` String 201 | ``show_default`` Boolean 202 | ``argname`` String or table 203 | ``action`` Function or string 204 | ``init`` Any 205 | ``hidden`` Boolean 206 | =================== =============== 207 | 208 | Option and flag properties 209 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 210 | 211 | Properties that can be set as arguments when calling or constructing an option or a flag, in this order: 212 | 213 | =============== ================= 214 | Property Type 215 | =============== ================= 216 | ``name`` String 217 | ``description`` String 218 | ``default`` Any 219 | ``convert`` Function or table 220 | ``args`` Number or string 221 | ``count`` Number or string 222 | =============== ================= 223 | 224 | Other properties: 225 | 226 | =================== ================== 227 | Property Type 228 | =================== ================== 229 | ``target`` String 230 | ``defmode`` String 231 | ``show_default`` Boolean 232 | ``overwrite`` Booleans 233 | ``argname`` String or table 234 | ``action`` Function or string 235 | ``init`` Any 236 | ``hidden`` Boolean 237 | =================== ================== 238 | -------------------------------------------------------------------------------- /spec/actions_spec.lua: -------------------------------------------------------------------------------- 1 | local Parser = require "argparse" 2 | getmetatable(Parser()).error = function(_, msg) error(msg) end 3 | 4 | describe("actions", function() 5 | it("for arguments are called", function() 6 | local parser = Parser() 7 | local foo 8 | parser:argument("foo"):action(function(_, _, passed_foo) 9 | foo = passed_foo 10 | end) 11 | local baz 12 | parser:argument("baz"):args("*"):action(function(_, _, passed_baz) 13 | baz = passed_baz 14 | end) 15 | 16 | parser:parse{"a"} 17 | assert.equals("a", foo) 18 | assert.same({}, baz) 19 | 20 | parser:parse{"b", "c"} 21 | assert.equals("b", foo) 22 | assert.same({"c"}, baz) 23 | 24 | parser:parse{"d", "e", "f"} 25 | assert.equals("d", foo) 26 | assert.same({"e", "f"}, baz) 27 | end) 28 | 29 | it("for options are called", function() 30 | local action1 = spy.new(function(_, _, arg) 31 | assert.equal("nowhere", arg) 32 | end) 33 | local expected_args = {"Alice", "Bob"} 34 | local action2 = spy.new(function(_, _, args) 35 | assert.same(expected_args, args) 36 | expected_args = {"Emma", "John"} 37 | end) 38 | 39 | local parser = Parser() 40 | parser:option "-f" "--from" { 41 | action = function(...) return action1(...) end 42 | } 43 | parser:option "-p" "--pair" { 44 | action = function(...) return action2(...) end, 45 | count = "*", 46 | args = 2 47 | } 48 | 49 | parser:parse{"-fnowhere", "--pair", "Alice", "Bob", "-p", "Emma", "John"} 50 | assert.spy(action1).called(1) 51 | assert.spy(action2).called(2) 52 | end) 53 | 54 | it("for flags are called", function() 55 | local action1 = spy.new(function() end) 56 | local action2 = spy.new(function() end) 57 | local action3 = spy.new(function() end) 58 | 59 | local parser = Parser() 60 | parser:flag "-v" "--verbose" { 61 | action = function(...) return action1(...) end, 62 | count = "0-3" 63 | } 64 | parser:flag "-q" "--quiet" { 65 | action = function(...) return action2(...) end 66 | } 67 | parser:flag "-a" "--another-flag" { 68 | action = function(...) return action3(...) end 69 | } 70 | 71 | parser:parse{"-vv", "--quiet"} 72 | assert.spy(action1).called(2) 73 | assert.spy(action2).called(1) 74 | assert.spy(action3).called(0) 75 | end) 76 | 77 | it("for options allow custom storing of arguments", function() 78 | local parser = Parser() 79 | parser:option("-p --path"):action(function(result, target, argument) 80 | result[target] = (result[target] or ".") .. "/" .. argument 81 | end) 82 | 83 | local args = parser:parse{"-pfirst", "--path", "second", "--path=third"} 84 | assert.same({path = "./first/second/third"}, args) 85 | end) 86 | 87 | it("for options with several arguments allow custom storing of arguments", function() 88 | local parser = Parser() 89 | parser:option("-p --path"):args("*"):action(function(result, target, arguments) 90 | for _, argument in ipairs(arguments) do 91 | result[target] = (result[target] or ".") .. "/" .. argument 92 | end 93 | end) 94 | 95 | local args = parser:parse{"-p", "first", "second", "third"} 96 | assert.same({path = "./first/second/third"}, args) 97 | end) 98 | 99 | it("for options allow using strings as actions", function() 100 | local parser = Parser() 101 | parser:flag("--no-foo"):target("foo"):action("store_false") 102 | parser:flag("--no-bar"):target("bar"):action("store_false") 103 | parser:option("--things"):args("+"):count("*"):action("concat") 104 | 105 | local args = parser:parse{"--things", "a", "b", "--no-foo", "--things", "c", "d"} 106 | assert.same({foo = false, things = {"a", "b", "c", "d"}}, args) 107 | end) 108 | 109 | it("for options allow setting initial stored value", function() 110 | local parser = Parser() 111 | parser:flag("--no-foo"):target("foo"):action("store_false"):init(true) 112 | parser:flag("--no-bar"):target("bar"):action("store_false"):init(true) 113 | 114 | local args = parser:parse{"--no-foo"} 115 | assert.same({foo = false, bar = true}, args) 116 | end) 117 | 118 | it("'append' and 'concat' respect initial value", function() 119 | local parser = Parser() 120 | parser:option("-f"):count("*"):init(nil) 121 | parser:option("-g"):args("*"):count("*"):action("concat"):init(nil) 122 | 123 | local args = parser:parse{} 124 | assert.same({}, args) 125 | 126 | args = parser:parse{"-fabc", "-fdef", "-g"} 127 | assert.same({f = {"abc", "def"}, g = {}}, args) 128 | 129 | args = parser:parse{"-g", "abc", "def", "-g123", "-f123"} 130 | assert.same({f = {"123"}, g = {"abc", "def", "123"}}, args) 131 | end) 132 | 133 | it("'concat' action can't handle too many invocations", function() 134 | local parser = Parser() 135 | parser:option("-x"):args("*"):count("0-2"):action("concat") 136 | 137 | assert.has_error(function() 138 | parser:parse{"-x", "foo", "-x", "bar", "baz", "-x", "thing"} 139 | end, "'concat' action can't handle too many invocations") 140 | end) 141 | 142 | it("for options allow setting initial stored value as non-string argument to default", function() 143 | local parser = Parser() 144 | parser:flag("--no-foo", "Foo the bar.", true):target("foo"):action("store_false") 145 | parser:flag("--no-bar", "Bar the foo.", true):target("bar"):action("store_false") 146 | 147 | local args = parser:parse{"--no-foo"} 148 | assert.same({foo = false, bar = true}, args) 149 | end) 150 | 151 | it("pass overwrite flag as the fourth argument", function() 152 | local parser = Parser() 153 | local overwrites = {} 154 | parser:flag("-f"):count("0-2"):action(function(_, _, _, overwrite) 155 | table.insert(overwrites, overwrite) 156 | end) 157 | 158 | parser:parse{"-ffff"} 159 | assert.same({false, false, true, true}, overwrites) 160 | end) 161 | 162 | it("pass user-defined target", function() 163 | local parser = Parser() 164 | local target 165 | parser:flag("-f"):target("force"):action(function(_, passed_target) 166 | target = passed_target 167 | end) 168 | 169 | parser:parse{"-f"} 170 | assert.equals("force", target) 171 | end) 172 | 173 | it("apply convert before passing arguments", function() 174 | local parser = Parser() 175 | local numbers = {} 176 | parser:option("-n"):convert(tonumber):default("0"):defmode("a"):action(function(_, _, n) 177 | table.insert(numbers, n) 178 | end) 179 | 180 | parser:parse{"-n", "-n1", "-n", "-n", "2"} 181 | assert.same({0, 1, 0, 2}, numbers) 182 | end) 183 | 184 | it("for parser are called", function() 185 | local parser = Parser() 186 | parser:flag("-f"):count("0-3") 187 | local args 188 | parser:action(function(passed_args) args = passed_args end) 189 | parser:parse{"-ff"} 190 | assert.same({f = 2}, args) 191 | end) 192 | 193 | it("for commands are called in reverse order", function() 194 | local args = {} 195 | 196 | local parser = Parser():action(function(passed_args) 197 | args[1] = passed_args 198 | args.last = 1 199 | end) 200 | parser:flag("-f"):count("0-3") 201 | local foo = parser:command("foo"):action(function(passed_args, name) 202 | assert.equals("foo", name) 203 | args[2] = passed_args 204 | args.last = 2 205 | end) 206 | foo:flag("-g") 207 | parser:parse{"foo", "-f", "-g", "-f"} 208 | assert.same({ 209 | last = 1, 210 | {foo = true, f = 2, g = true}, 211 | {foo = true, f = 2, g = true} 212 | }, args) 213 | end) 214 | end) 215 | -------------------------------------------------------------------------------- /docsrc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # argparse documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Jun 20 14:03:24 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [] 32 | 33 | # Add any paths that contain templates here, relative to this directory. 34 | templates_path = [] 35 | 36 | # The suffix of source filenames. 37 | source_suffix = '.rst' 38 | 39 | # The encoding of source files. 40 | #source_encoding = 'utf-8-sig' 41 | 42 | # The master toctree document. 43 | master_doc = 'index' 44 | 45 | # General information about the project. 46 | project = u'argparse' 47 | copyright = u'2013 - 2018, Peter Melnichenko' 48 | 49 | # The version info for the project you're documenting, acts as replacement for 50 | # |version| and |release|, also used in various other places throughout the 51 | # built documents. 52 | # 53 | # The short X.Y version. 54 | version = '0.6.0' 55 | # The full version, including alpha/beta/rc tags. 56 | release = '0.6.0' 57 | 58 | # The language for content autogenerated by Sphinx. Refer to documentation 59 | # for a list of supported languages. 60 | #language = None 61 | 62 | # There are two options for replacing |today|: either, you set today to some 63 | # non-false value, then it is used: 64 | #today = '' 65 | # Else, today_fmt is used as the format for a strftime call. 66 | #today_fmt = '%B %d, %Y' 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | exclude_patterns = [] 71 | 72 | # The reST default role (used for this markup: `text`) to use for all 73 | # documents. 74 | #default_role = None 75 | 76 | # If true, '()' will be appended to :func: etc. cross-reference text. 77 | #add_function_parentheses = True 78 | 79 | # If true, the current module name will be prepended to all description 80 | # unit titles (such as .. function::). 81 | #add_module_names = True 82 | 83 | # If true, sectionauthor and moduleauthor directives will be shown in the 84 | # output. They are ignored by default. 85 | #show_authors = False 86 | 87 | # The name of the Pygments (syntax highlighting) style to use. 88 | pygments_style = 'sphinx' 89 | 90 | # A list of ignored prefixes for module index sorting. 91 | #modindex_common_prefix = [] 92 | 93 | # If true, keep warnings as "system message" paragraphs in the built documents. 94 | #keep_warnings = False 95 | 96 | 97 | # -- Options for HTML output ---------------------------------------------- 98 | 99 | # The theme to use for HTML and HTML Help pages. See the documentation for 100 | # a list of builtin themes. 101 | if os.environ.get('READTHEDOCS', None) != 'True': 102 | try: 103 | import sphinx_rtd_theme 104 | html_theme = 'sphinx_rtd_theme' 105 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 106 | except ImportError: 107 | pass 108 | 109 | # Theme options are theme-specific and customize the look and feel of a theme 110 | # further. For a list of options available for each theme, see the 111 | # documentation. 112 | #html_theme_options = {} 113 | 114 | # Add any paths that contain custom themes here, relative to this directory. 115 | #html_theme_path = [] 116 | 117 | # The name for this set of Sphinx documents. If None, it defaults to 118 | # " v documentation". 119 | html_title = "argparse 0.6.0 tutorial" 120 | 121 | # A shorter title for the navigation bar. Default is the same as html_title. 122 | #html_short_title = None 123 | 124 | # The name of an image file (relative to this directory) to place at the top 125 | # of the sidebar. 126 | #html_logo = None 127 | 128 | # The name of an image file (within the static path) to use as favicon of the 129 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 130 | # pixels large. 131 | #html_favicon = None 132 | 133 | # Add any paths that contain custom static files (such as style sheets) here, 134 | # relative to this directory. They are copied after the builtin static files, 135 | # so a file named "default.css" will overwrite the builtin "default.css". 136 | html_static_path = [] 137 | 138 | # Add any extra paths that contain custom files (such as robots.txt or 139 | # .htaccess) here, relative to this directory. These files are copied 140 | # directly to the root of the documentation. 141 | #html_extra_path = [] 142 | 143 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 144 | # using the given strftime format. 145 | #html_last_updated_fmt = '%b %d, %Y' 146 | 147 | # If true, SmartyPants will be used to convert quotes and dashes to 148 | # typographically correct entities. 149 | #html_use_smartypants = True 150 | 151 | # Custom sidebar templates, maps document names to template names. 152 | #html_sidebars = {} 153 | 154 | # Additional templates that should be rendered to pages, maps page names to 155 | # template names. 156 | #html_additional_pages = {} 157 | 158 | # If false, no module index is generated. 159 | #html_domain_indices = True 160 | 161 | # If false, no index is generated. 162 | #html_use_index = True 163 | 164 | # If true, the index is split into individual pages for each letter. 165 | #html_split_index = False 166 | 167 | # If true, links to the reST sources are added to the pages. 168 | #html_show_sourcelink = True 169 | 170 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 171 | #html_show_sphinx = True 172 | 173 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 174 | #html_show_copyright = True 175 | 176 | # If true, an OpenSearch description file will be output, and all pages will 177 | # contain a tag referring to it. The value of this option must be the 178 | # base URL from which the finished HTML is served. 179 | #html_use_opensearch = '' 180 | 181 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 182 | #html_file_suffix = None 183 | 184 | # Output file base name for HTML help builder. 185 | htmlhelp_basename = 'argparsetutorial' 186 | 187 | 188 | # -- Options for LaTeX output --------------------------------------------- 189 | 190 | latex_elements = { 191 | # The paper size ('letterpaper' or 'a4paper'). 192 | #'papersize': 'letterpaper', 193 | 194 | # The font size ('10pt', '11pt' or '12pt'). 195 | #'pointsize': '10pt', 196 | 197 | # Additional stuff for the LaTeX preamble. 198 | #'preamble': '', 199 | } 200 | 201 | # Grouping the document tree into LaTeX files. List of tuples 202 | # (source start file, target name, title, 203 | # author, documentclass [howto, manual, or own class]). 204 | latex_documents = [ 205 | ('index', 'argparse.tex', u'argparse tutorial', 206 | u'Peter Melnichenko', 'manual') 207 | ] 208 | 209 | # The name of an image file (relative to this directory) to place at the top of 210 | # the title page. 211 | #latex_logo = None 212 | 213 | # For "manual" documents, if this is true, then toplevel headings are parts, 214 | # not chapters. 215 | #latex_use_parts = False 216 | 217 | # If true, show page references after internal links. 218 | #latex_show_pagerefs = False 219 | 220 | # If true, show URL addresses after external links. 221 | #latex_show_urls = False 222 | 223 | # Documents to append as an appendix to all manuals. 224 | #latex_appendices = [] 225 | 226 | # If false, no module index is generated. 227 | #latex_domain_indices = True 228 | 229 | 230 | # -- Options for manual page output --------------------------------------- 231 | 232 | # One entry per manual page. List of tuples 233 | # (source start file, name, description, authors, manual section). 234 | man_pages = [ 235 | ('index', 'argparse', u'argparse tutorial', 236 | [u'Peter Melnichenko'], 1) 237 | ] 238 | 239 | # If true, show URL addresses after external links. 240 | #man_show_urls = False 241 | 242 | 243 | # -- Options for Texinfo output ------------------------------------------- 244 | 245 | # Grouping the document tree into Texinfo files. List of tuples 246 | # (source start file, target name, title, author, 247 | # dir menu entry, description, category) 248 | texinfo_documents = [ 249 | ('index', 'argparse', u'argparse tutorial', 250 | u'Peter Melnichenko', 'argparse', 'Command line parser for Lua.', 251 | 'Miscellaneous') 252 | ] 253 | 254 | # Documents to append as an appendix to all manuals. 255 | #texinfo_appendices = [] 256 | 257 | # If false, no module index is generated. 258 | #texinfo_domain_indices = True 259 | 260 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 261 | #texinfo_show_urls = 'footnote' 262 | 263 | # If true, do not generate a @detailmenu in the "Top" node's menu. 264 | #texinfo_no_detailmenu = False 265 | -------------------------------------------------------------------------------- /docsrc/messages.rst: -------------------------------------------------------------------------------- 1 | Configuring help and usage messages 2 | =================================== 3 | 4 | The usage and help messages of parsers and commands can be generated on demand using ``:get_usage()`` and ``:get_help()`` methods, and overridden using ``help`` and ``usage`` properties. 5 | When using the autogenerated usage and help messages, there are several ways to adjust them. 6 | 7 | Hiding arguments, options, and commands from messages 8 | ----------------------------------------------------- 9 | 10 | If ``hidden`` property for an argument, an option or a command is set to ``true``, 11 | it is not included into help and usage messages, but otherwise works normally. 12 | 13 | .. code-block:: lua 14 | :linenos: 15 | 16 | parser:option "--normal-option" 17 | parser:option "--deprecated-option" 18 | :hidden(true) 19 | 20 | .. code-block:: none 21 | 22 | $ lua script.lua --help 23 | 24 | .. code-block:: none 25 | 26 | Usage: script.lua [--normal-option ] [-h] 27 | 28 | Options: 29 | --normal-option 30 | -h, --help Show this help message and exit. 31 | 32 | .. code-block:: none 33 | 34 | $ lua script.lua --deprecated-option value 35 | 36 | .. code-block:: lua 37 | 38 | { 39 | deprecated_option = "value" 40 | } 41 | 42 | 43 | Setting argument placeholder 44 | ---------------------------- 45 | 46 | For options and arguments, ``argname`` property controls the placeholder for the argument in the usage message. 47 | 48 | .. code-block:: lua 49 | :linenos: 50 | 51 | parser:option "-f" "--from" 52 | :argname "" 53 | 54 | .. code-block:: none 55 | 56 | $ lua script.lua --help 57 | 58 | .. code-block:: none 59 | 60 | Usage: script.lua [-f ] [-h] 61 | 62 | Options: 63 | -f , --from 64 | -h, --help Show this help message and exit. 65 | 66 | ``argname`` can be an array of placeholders. 67 | 68 | .. code-block:: lua 69 | :linenos: 70 | 71 | parser:option "--pair" 72 | :args(2) 73 | :argname {"", ""} 74 | 75 | .. code-block:: none 76 | 77 | $ lua script.lua --help 78 | 79 | .. code-block:: none 80 | 81 | Usage: script.lua [--pair ] [-h] 82 | 83 | Options: 84 | --pair 85 | -h, --help Show this help message and exit. 86 | 87 | Grouping elements 88 | ----------------- 89 | 90 | ``:group(name, ...)`` method of parsers and commands puts passed arguments, options, and commands into 91 | a named group with its own section in the help message. Elements outside any groups are put into a default section. 92 | 93 | .. code-block:: lua 94 | :linenos: 95 | 96 | parser:group("Configuring output format", 97 | parser:flag "-v --verbose", 98 | parser:flag "--use-colors", 99 | parser:option "--encoding" 100 | ) 101 | 102 | parser:group("Configuring processing", 103 | parser:option "--compression-level", 104 | parser:flag "--skip-broken-chunks" 105 | ) 106 | 107 | parser:flag "--version" 108 | :action(function() print("script.lua 1.0.0") os.exit(0) end) 109 | 110 | .. code-block:: none 111 | 112 | $ lua script.lua --help 113 | 114 | .. code-block:: none 115 | 116 | Usage: script.lua [-v] [--use-colors] [--encoding ] 117 | [--compression-level ] 118 | [--skip-broken-chunks] [--version] [-h] 119 | 120 | Configuring output format: 121 | -v, --verbose 122 | --use-colors 123 | --encoding 124 | 125 | Configuring processing: 126 | --compression-level 127 | --skip-broken-chunks 128 | 129 | Other options: 130 | --version 131 | -h, --help Show this help message and exit. 132 | 133 | Help message line wrapping 134 | -------------------------- 135 | 136 | If ``help_max_width`` property of a parser or a command is set, when generating its help message, argparse will automatically 137 | wrap lines, attempting to fit into given number of columns. This includes wrapping lines in parser description and epilog 138 | and descriptions of arguments, options, and commands. 139 | 140 | Line wrapping preserves existing line endings and only splits overly long input lines. 141 | When breaking a long line, it replicates indentation of the line in the continuation lines. 142 | Additionally, if the first non-space token in a line is ``*``, ``+``, or ``-``, the line is considered a list item, 143 | and the continuation lines are aligned with the first word after the list item marker. 144 | 145 | .. code-block:: lua 146 | :linenos: 147 | 148 | parser:help_max_width(80) 149 | 150 | parser:option "-f --foo" 151 | :description("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " .. 152 | "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation " .. 153 | "ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" .. 154 | "The next paragraph is indented:\n" .. 155 | " Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " .. 156 | "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") 157 | 158 | parser:option "-b --bar" 159 | :description("Here is a list:\n".. 160 | "* Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...\n" .. 161 | "* Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip...\n" .. 162 | "* Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.") 163 | 164 | .. code-block:: none 165 | 166 | $ lua script.lua --help 167 | 168 | .. code-block:: none 169 | 170 | Usage: script.lua [-f ] [-b ] [-h] 171 | 172 | Options: 173 | -f , Lorem ipsum dolor sit amet, consectetur adipiscing 174 | --foo elit, sed do eiusmod tempor incididunt ut labore et 175 | dolore magna aliqua. Ut enim ad minim veniam, quis 176 | nostrud exercitation ullamco laboris nisi ut aliquip ex 177 | ea commodo consequat. 178 | The next paragraph is indented: 179 | Duis aute irure dolor in reprehenderit in voluptate 180 | velit esse cillum dolore eu fugiat nulla pariatur. 181 | Excepteur sint occaecat cupidatat non proident, sunt 182 | in culpa qui officia deserunt mollit anim id est 183 | laborum. 184 | -b , Here is a list: 185 | --bar * Lorem ipsum dolor sit amet, consectetur adipiscing 186 | elit, sed do eiusmod tempor... 187 | * Ut enim ad minim veniam, quis nostrud exercitation 188 | ullamco laboris nisi ut aliquip... 189 | * Duis aute irure dolor in reprehenderit in voluptate 190 | velit esse cillum dolore eu fugiat nulla pariatur. 191 | -h, --help Show this help message and exit. 192 | 193 | ``help_max_width`` property is inherited by commands. 194 | 195 | Configuring help and usage message layout 196 | ----------------------------------------- 197 | 198 | Several other parser and command properties can be used to tweak help and usage message format. 199 | Like ``help_max_width``, all of them are inherited by commands when set on the parser or a parent command. 200 | 201 | ``usage_max_width`` property sets maximum width of the usage string. Default is ``70``. 202 | 203 | ``usage_margin`` property sets margin width used when line wrapping long usage strings. Default is ``7``. 204 | 205 | .. code-block:: lua 206 | :linenos: 207 | 208 | parser:usage_max_width(50) 209 | :usage_margin(#"Usage: script.lua ") 210 | 211 | parser:option "--foo" 212 | parser:option "--bar" 213 | parser:option "--baz" 214 | parser:option "--qux" 215 | 216 | print(parser:get_usage()) 217 | 218 | .. code-block:: none 219 | 220 | $ lua script.lua 221 | 222 | .. code-block:: none 223 | 224 | Usage: script.lua [--foo ] [--bar ] 225 | [--baz ] [--qux ] [-h] 226 | 227 | Help message for a group of arguments, options, or commands is organized into two columns, with usage 228 | template on the left side and descriptions on the right side. 229 | ``help_usage_margin`` property sets horizontal offset for the first column (``3`` by default). 230 | ``help_description_margin`` property sets horizontal offset for the second column (``25`` by default). 231 | 232 | ``help_vertical_space`` property sets number of extra empty lines to put between descriptions for different elements 233 | within a group (``0`` by default). 234 | 235 | .. code-block:: lua 236 | :linenos: 237 | 238 | parser:help_usage_margin(2) 239 | :help_description_margin(17) 240 | :help_vertical_space(1) 241 | 242 | parser:option("--foo", "Set foo.") 243 | parser:option("--bar", "Set bar.") 244 | parser:option("--baz", "Set baz.") 245 | parser:option("--qux", "Set qux.") 246 | 247 | .. code-block:: none 248 | 249 | $ lua script.lua --help 250 | 251 | .. code-block:: none 252 | 253 | Usage: script.lua [--foo ] [--bar ] [--baz ] 254 | [--qux ] [-h] 255 | 256 | Options: 257 | 258 | --foo Set foo. 259 | 260 | --bar Set bar. 261 | 262 | --baz Set baz. 263 | 264 | --qux Set qux. 265 | 266 | -h, --help Show this help message and exit. 267 | -------------------------------------------------------------------------------- /spec/usage_spec.lua: -------------------------------------------------------------------------------- 1 | local Parser = require "argparse" 2 | getmetatable(Parser()).error = function(_, msg) error(msg) end 3 | 4 | describe("tests related to usage message generation", function() 5 | it("creates correct usage message for empty parser", function() 6 | local parser = Parser "foo" 7 | :add_help(false) 8 | assert.equal(parser:get_usage(), "Usage: foo") 9 | end) 10 | 11 | it("creates correct usage message for arguments", function() 12 | local parser = Parser "foo" 13 | :add_help(false) 14 | parser:argument "first" 15 | parser:argument "second-and-third" 16 | :args "2" 17 | parser:argument "maybe-fourth" 18 | :args "?" 19 | parser:argument "others" 20 | :args "*" 21 | 22 | assert.equal([[ 23 | Usage: foo 24 | [] [] ...]], parser:get_usage() 25 | ) 26 | end) 27 | 28 | it("creates correct usage message for options", function() 29 | local parser = Parser "foo" 30 | :add_help(false) 31 | parser:flag "-q" "--quiet" 32 | parser:option "--from" 33 | :count "1" 34 | :target "server" 35 | parser:option "--config" 36 | 37 | assert.equal( 38 | [=[Usage: foo [-q] --from [--config ]]=], 39 | parser:get_usage() 40 | ) 41 | end) 42 | 43 | it("creates correct usage message for options with variable argument count", function() 44 | local parser = Parser "foo" 45 | :add_help(false) 46 | parser:argument "files" 47 | :args "+" 48 | parser:flag "-q" "--quiet" 49 | parser:option "--globals" 50 | :args "*" 51 | 52 | assert.equal( 53 | [=[Usage: foo [-q] [] ... [--globals [] ...]]=], 54 | parser:get_usage() 55 | ) 56 | end) 57 | 58 | it("creates correct usage message for arguments with default value", function() 59 | local parser = Parser "foo" 60 | :add_help(false) 61 | parser:argument "input" 62 | :default "a.in" 63 | parser:argument "pair" 64 | :args(2) 65 | :default "foo" 66 | parser:argument "pair2" 67 | :args(2) 68 | :default "bar" 69 | :defmode "arg" 70 | 71 | assert.equal( 72 | [=[Usage: foo [] [ ] [] []]=], 73 | parser:get_usage() 74 | ) 75 | end) 76 | 77 | it("creates correct usage message for options with default value", function() 78 | local parser = Parser "foo" 79 | :add_help(false) 80 | parser:option "-f" "--from" 81 | :default "there" 82 | parser:option "-o" "--output" 83 | :default "a.out" 84 | :defmode "arg" 85 | 86 | assert.equal( 87 | [=[Usage: foo [-f ] [-o []]]=], 88 | parser:get_usage() 89 | ) 90 | end) 91 | 92 | it("creates correct usage message for commands", function() 93 | local parser = Parser "foo" 94 | :add_help(false) 95 | parser:flag "-q" "--quiet" 96 | local run = parser:command "run" 97 | run:option "--where" 98 | 99 | assert.equal( 100 | [=[Usage: foo [-q] ...]=], 101 | parser:get_usage() 102 | ) 103 | end) 104 | 105 | it("creates correct usage message for subcommands", function() 106 | local parser = Parser "foo" 107 | :add_help(false) 108 | parser:flag "-q" "--quiet" 109 | local run = parser:command "run" 110 | :add_help(false) 111 | run:option "--where" 112 | 113 | assert.equal( 114 | [=[Usage: foo run [--where ]]=], 115 | run:get_usage() 116 | ) 117 | end) 118 | 119 | it("omits usage for hidden arguments and options", function() 120 | local parser = Parser "foo" 121 | :add_help(false) 122 | parser:flag "-d" "--deprecated" 123 | :hidden(true) 124 | parser:flag "-n" "--not-deprecated" 125 | parser:argument "normal" 126 | parser:argument "deprecated" 127 | :args "?" 128 | :hidden(true) 129 | 130 | assert.equal( 131 | [=[Usage: foo [-n] ]=], 132 | parser:get_usage() 133 | ) 134 | end) 135 | 136 | it("omits usage for mutexes if all elements are hidden", function() 137 | local parser = Parser "foo" 138 | :add_help(false) 139 | parser:mutex( 140 | parser:flag "--misfeature" 141 | :hidden(true), 142 | parser:flag "--no-misfeature" 143 | :action "store_false" 144 | :target "misfeature" 145 | :hidden(true) 146 | ) 147 | parser:flag "--feature" 148 | 149 | assert.equal( 150 | [=[Usage: foo [--feature]]=], 151 | parser:get_usage() 152 | ) 153 | end) 154 | 155 | it("usage messages for commands are correct after several invocations", function() 156 | local parser = Parser "foo" 157 | :add_help(false) 158 | parser:flag "-q" "--quiet" 159 | local run = parser:command "run" 160 | :add_help(false) 161 | run:option "--where" 162 | 163 | parser:parse{"run"} 164 | parser:parse{"run"} 165 | 166 | assert.equal( 167 | [=[Usage: foo run [--where ]]=], 168 | run:get_usage() 169 | ) 170 | end) 171 | 172 | describe("usage generation can be customized", function() 173 | it("uses message provided by user", function() 174 | local parser = Parser "foo" 175 | :usage "Usage: obvious" 176 | :add_help(false) 177 | parser:flag "-q" "--quiet" 178 | 179 | assert.equal( 180 | [=[Usage: obvious]=], 181 | parser:get_usage() 182 | ) 183 | end) 184 | 185 | it("uses argnames provided by user", function() 186 | local parser = Parser "foo" 187 | :add_help(false) 188 | parser:argument "inputs" 189 | :args "1-2" 190 | :argname "" 191 | 192 | assert.equal( 193 | [=[Usage: foo []]=], 194 | parser:get_usage() 195 | ) 196 | end) 197 | 198 | it("uses array of argnames provided by user", function() 199 | local parser = Parser "foo" 200 | :add_help(false) 201 | parser:option "--pair" 202 | :args(2) 203 | :count "*" 204 | :argname{"", ""} 205 | 206 | assert.equal( 207 | [=[Usage: foo [--pair ]]=], 208 | parser:get_usage() 209 | ) 210 | end) 211 | end) 212 | 213 | it("creates correct usage message for mutexes", function() 214 | local parser = Parser "foo" 215 | :add_help(false) 216 | parser:mutex( 217 | parser:flag "-q" "--quiet", 218 | parser:flag "-v" "--verbose", 219 | parser:flag "-i" "--interactive" 220 | ) 221 | parser:mutex( 222 | parser:flag "-l" "--local", 223 | parser:option "-f" "--from" 224 | ) 225 | parser:option "--yet-another-option" 226 | 227 | assert.equal([=[ 228 | Usage: foo ([-q] | [-v] | [-i]) ([-l] | [-f ]) 229 | [--yet-another-option ]]=], parser:get_usage() 230 | ) 231 | end) 232 | 233 | it("creates correct usage message for mutexes with arguments", function() 234 | local parser = Parser "foo" 235 | :add_help(false) 236 | 237 | parser:argument "first" 238 | parser:mutex( 239 | parser:flag "-q" "--quiet", 240 | parser:flag "-v" "--verbose", 241 | parser:argument "second":args "?" 242 | ) 243 | parser:argument "third" 244 | parser:mutex( 245 | parser:flag "-l" "--local", 246 | parser:option "-f" "--from" 247 | ) 248 | parser:option "--yet-another-option" 249 | 250 | assert.equal([=[ 251 | Usage: foo ([-l] | [-f ]) 252 | [--yet-another-option ] 253 | ([-q] | [-v] | []) ]=], parser:get_usage() 254 | ) 255 | end) 256 | 257 | it("puts vararg option and mutex usages after positional arguments", function() 258 | local parser = Parser "foo" 259 | :add_help(false) 260 | parser:argument("argument") 261 | parser:mutex( 262 | parser:flag "-q" "--quiet", 263 | parser:flag "-v" "--verbose", 264 | parser:flag "-i" "--interactive" 265 | ) 266 | parser:mutex( 267 | parser:flag "-a -all", 268 | parser:option "-i --ignore":args("*") 269 | ) 270 | parser:option "--yet-another-option" 271 | parser:option "--vararg-option":args("1-2") 272 | 273 | assert.equal([=[ 274 | Usage: foo ([-q] | [-v] | [-i]) 275 | [--yet-another-option ] 276 | ([-a] | [-i [] ...]) 277 | [--vararg-option []]]=], parser:get_usage() 278 | ) 279 | end) 280 | 281 | it("doesn't repeat usage of elements within several mutexes", function() 282 | local parser = Parser "foo" 283 | :add_help(false) 284 | 285 | parser:argument("arg1") 286 | local arg2 = parser:argument("arg2"):args "?" 287 | parser:argument("arg3"):args "?" 288 | local arg4 = parser:argument("arg4"):args "?" 289 | 290 | local opt1 = parser:option("--opt1") 291 | local opt2 = parser:option("--opt2") 292 | local opt3 = parser:option("--opt3") 293 | local opt4 = parser:option("--opt4") 294 | local opt5 = parser:option("--opt5") 295 | local opt6 = parser:option("--opt6") 296 | parser:option("--opt7") 297 | 298 | parser:mutex(arg2, opt1, opt2) 299 | parser:mutex(arg4, opt2, opt3, opt4) 300 | parser:mutex(opt1, opt3, opt5) 301 | parser:mutex(opt1, opt3, opt6) 302 | 303 | assert.equal([=[ 304 | Usage: foo ([--opt1 ] | [--opt3 ] | [--opt5 ]) 305 | [--opt6 ] [--opt7 ] 306 | ([] | [--opt2 ]) [] 307 | ([] | [--opt4 ])]=], parser:get_usage() 308 | ) 309 | end) 310 | 311 | it("allows configuring usage margin using usage_margin property", function() 312 | local parser = Parser "foo" 313 | :usage_margin(2) 314 | 315 | parser:argument "long_argument_name" 316 | parser:argument "very_long_words" 317 | 318 | parser:option "--set-important-property" 319 | parser:option "--include" 320 | :args "*" 321 | 322 | assert.equals([=[ 323 | Usage: foo [--set-important-property ] [-h] 324 | [--include [] ...]]=], parser:get_usage()) 325 | end) 326 | 327 | it("allows configuring max usage width using usage_max_width property", function() 328 | local parser = Parser "foo" 329 | :usage_max_width(50) 330 | 331 | parser:argument "long_argument_name" 332 | parser:argument "very_long_words" 333 | 334 | parser:option "--set-important-property" 335 | parser:option "--include" 336 | :args "*" 337 | 338 | assert.equals([=[ 339 | Usage: foo 340 | [--set-important-property ] 341 | [-h] 342 | [--include [] ...]]=], parser:get_usage()) 343 | end) 344 | end) 345 | -------------------------------------------------------------------------------- /spec/options_spec.lua: -------------------------------------------------------------------------------- 1 | local Parser = require "argparse" 2 | getmetatable(Parser()).error = function(_, msg) error(msg) end 3 | 4 | describe("tests related to options", function() 5 | describe("passing correct options", function() 6 | it("handles no options passed correctly", function() 7 | local parser = Parser() 8 | parser:option "-s" "--server" 9 | local args = parser:parse({}) 10 | assert.same({}, args) 11 | end) 12 | 13 | it("handles one option correctly", function() 14 | local parser = Parser() 15 | parser:option "-s" "--server" 16 | local args = parser:parse({"--server", "foo"}) 17 | assert.same({server = "foo"}, args) 18 | end) 19 | 20 | it("normalizes default target", function() 21 | local parser = Parser() 22 | parser:option "--from-server" 23 | local args = parser:parse({"--from-server", "foo"}) 24 | assert.same({from_server = "foo"}, args) 25 | end) 26 | 27 | it("handles non-standard charset", function() 28 | local parser = Parser() 29 | parser:option "/s" 30 | parser:flag "/?" 31 | local args = parser:parse{"/s", "foo", "/?"} 32 | assert.same({s = "foo", ["?"] = true}, args) 33 | end) 34 | 35 | it("handles GNU-style long options", function() 36 | local parser = Parser() 37 | parser:option "-s" "--server" 38 | local args = parser:parse({"--server=foo"}) 39 | assert.same({server = "foo"}, args) 40 | end) 41 | 42 | it("handles GNU-style long options even when it could take more arguments", function() 43 | local parser = Parser() 44 | parser:option "-s" "--server" { 45 | args = "*" 46 | } 47 | local args = parser:parse({"--server=foo"}) 48 | assert.same({server = {"foo"}}, args) 49 | end) 50 | 51 | it("handles GNU-style long options for multi-argument options", function() 52 | local parser = Parser() 53 | parser:option "-s" "--server" { 54 | args = "1-2" 55 | } 56 | local args = parser:parse({"--server=foo", "bar"}) 57 | assert.same({server = {"foo", "bar"}}, args) 58 | end) 59 | 60 | it("handles short option correclty", function() 61 | local parser = Parser() 62 | parser:option "-s" "--server" 63 | local args = parser:parse({"-s", "foo"}) 64 | assert.same({server = "foo"}, args) 65 | end) 66 | 67 | it("handles flag correclty", function() 68 | local parser = Parser() 69 | parser:flag "-q" "--quiet" 70 | local args = parser:parse({"--quiet"}) 71 | assert.same({quiet = true}, args) 72 | args = parser:parse({}) 73 | assert.same({}, args) 74 | end) 75 | 76 | it("handles combined flags correclty", function() 77 | local parser = Parser() 78 | parser:flag "-q" "--quiet" 79 | parser:flag "-f" "--fast" 80 | local args = parser:parse({"-qf"}) 81 | assert.same({quiet = true, fast = true}, args) 82 | end) 83 | 84 | it("handles short options without space between option and argument", function() 85 | local parser = Parser() 86 | parser:option "-s" "--server" 87 | local args = parser:parse({"-sfoo"}) 88 | assert.same({server = "foo"}, args) 89 | end) 90 | 91 | it("handles flags combined with short option correclty", function() 92 | local parser = Parser() 93 | parser:flag "-q" "--quiet" 94 | parser:option "-s" "--server" 95 | local args = parser:parse({"-qsfoo"}) 96 | assert.same({quiet = true, server = "foo"}, args) 97 | end) 98 | 99 | it("interprets extra option arguments as positional arguments", function() 100 | local parser = Parser() 101 | parser:argument "input" 102 | :args "2+" 103 | parser:option "-s" "--server" 104 | local args = parser:parse{"foo", "-sFROM", "bar"} 105 | assert.same({input = {"foo", "bar"}, server = "FROM"}, args) 106 | end) 107 | 108 | it("does not interpret extra option arguments as other option's arguments", function() 109 | local parser = Parser() 110 | parser:argument "output" 111 | parser:option "--input" 112 | :args "+" 113 | parser:option "-s" "--server" 114 | local args = parser:parse{"--input", "foo", "-sFROM", "bar"} 115 | assert.same({input = {"foo"}, server = "FROM", output = "bar"}, args) 116 | end) 117 | 118 | it("does not pass arguments to options after double hyphen", function() 119 | local parser = Parser() 120 | parser:argument "input" 121 | :args "?" 122 | parser:option "--exclude" 123 | :args "*" 124 | local args = parser:parse{"--exclude", "--", "foo"} 125 | assert.same({input = "foo", exclude = {}}, args) 126 | end) 127 | 128 | it("does not interpret options if disabled", function() 129 | local parser = Parser() 130 | parser:handle_options(false) 131 | parser:argument "input" 132 | :args "*" 133 | parser:option "-f" "--foo" 134 | :args "*" 135 | local args = parser:parse{"bar", "-f", "--foo" , "bar"} 136 | assert.same({input = {"bar", "-f", "--foo" , "bar"}}, args) 137 | end) 138 | 139 | it("allows using -- as an option", function() 140 | local parser = Parser() 141 | parser:flag "--unrelated" 142 | parser:option "--" 143 | :args "*" 144 | :target "tail" 145 | local args = parser:parse{"--", "foo", "--unrelated", "bar"} 146 | assert.same({tail = {"foo", "--unrelated", "bar"}}, args) 147 | end) 148 | 149 | describe("Special chars set", function() 150 | it("handles windows-style options", function() 151 | local parser = Parser() 152 | :add_help(false) 153 | parser:option "\\I" 154 | :count "*" 155 | :target "include" 156 | local args = parser:parse{"\\I", "src", "\\I", "misc"} 157 | assert.same({include = {"src", "misc"}}, args) 158 | end) 159 | 160 | it("corrects charset in commands", function() 161 | local parser = Parser "name" 162 | :add_help(false) 163 | parser:flag "-v" "--verbose" 164 | :count "*" 165 | parser:command "deep" 166 | :add_help(false) 167 | :option "/s" 168 | local args = parser:parse{"-v", "deep", "/s", "foo", "-vv"} 169 | assert.same({verbose = 3, deep = true, s = "foo"}, args) 170 | end) 171 | end) 172 | 173 | describe("Options with optional argument", function() 174 | it("handles emptiness correctly", function() 175 | local parser = Parser() 176 | parser:option("-p --password", "Secure password for special security", nil, nil, "?") 177 | local args = parser:parse({}) 178 | assert.same({}, args) 179 | end) 180 | 181 | it("handles option without argument correctly", function() 182 | local parser = Parser() 183 | parser:option "-p" "--password" { 184 | args = "?" 185 | } 186 | local args = parser:parse({"-p"}) 187 | assert.same({password = {}}, args) 188 | end) 189 | 190 | it("handles option with argument correctly", function() 191 | local parser = Parser() 192 | parser:option "-p" "--password" { 193 | args = "?" 194 | } 195 | local args = parser:parse({"-p", "password"}) 196 | assert.same({password = {"password"}}, args) 197 | end) 198 | end) 199 | 200 | it("handles multi-argument options correctly", function() 201 | local parser = Parser() 202 | parser:option "--pair" { 203 | args = 2 204 | } 205 | local args = parser:parse({"--pair", "Alice", "Bob"}) 206 | assert.same({pair = {"Alice", "Bob"}}, args) 207 | end) 208 | 209 | describe("Multi-count options", function() 210 | it("handles multi-count option correctly", function() 211 | local parser = Parser() 212 | parser:option "-e" "--exclude" { 213 | count = "*" 214 | } 215 | local args = parser:parse({"-efoo", "--exclude=bar", "-e", "baz"}) 216 | assert.same({exclude = {"foo", "bar", "baz"}}, args) 217 | end) 218 | 219 | it("handles not used multi-count option correctly", function() 220 | local parser = Parser() 221 | parser:option "-e" "--exclude" { 222 | count = "*" 223 | } 224 | local args = parser:parse({}) 225 | assert.same({exclude = {}}, args) 226 | end) 227 | 228 | it("handles multi-count multi-argument option correctly", function() 229 | local parser = Parser() 230 | parser:option "-e" "--exclude" { 231 | count = "*", 232 | args = 2 233 | } 234 | local args = parser:parse({"-e", "Alice", "Bob", "-e", "Emma", "Jacob"}) 235 | assert.same({exclude = {{"Alice", "Bob"}, {"Emma", "Jacob"}}}, args) 236 | end) 237 | 238 | it("handles multi-count flag correctly", function() 239 | local parser = Parser() 240 | parser:flag "-q" "--quiet" { 241 | count = "*" 242 | } 243 | local args = parser:parse({"-qq", "--quiet"}) 244 | assert.same({quiet = 3}, args) 245 | end) 246 | 247 | it("overwrites old invocations", function() 248 | local parser = Parser() 249 | parser:option "-u" "--user" { 250 | count = "0-2" 251 | } 252 | local args = parser:parse({"-uAlice", "--user=Bob", "--user", "John"}) 253 | assert.same({user = {"Bob", "John"}}, args) 254 | end) 255 | 256 | it("handles not used multi-count flag correctly", function() 257 | local parser = Parser() 258 | parser:flag "-q" "--quiet" { 259 | count = "*" 260 | } 261 | local args = parser:parse({}) 262 | assert.same({quiet = 0}, args) 263 | end) 264 | end) 265 | end) 266 | 267 | describe("passing incorrect options", function() 268 | it("handles lack of required argument correctly", function() 269 | local parser = Parser() 270 | parser:option "-s" "--server" 271 | assert.has_error(function() parser:parse{"--server"} end, "option '--server' requires an argument") 272 | assert.has_error(function() parser:parse{"-s"} end, "option '-s' requires an argument") 273 | end) 274 | 275 | it("handles unknown options correctly", function() 276 | local parser = Parser() 277 | :add_help(false) 278 | parser:option "--option" 279 | assert.has_error(function() parser:parse{"--server"} end, "unknown option '--server'") 280 | assert.has_error(function() parser:parse{"--server=localhost"} end, "unknown option '--server'") 281 | assert.has_error(function() parser:parse{"-s"} end, "unknown option '-s'") 282 | assert.has_error(function() parser:parse{"-slocalhost"} end, "unknown option '-s'") 283 | end) 284 | 285 | it("handles too many arguments correctly", function() 286 | local parser = Parser() 287 | parser:option "-s" "--server" 288 | assert.has_error(function() 289 | parser:parse{"-sfoo", "bar"} 290 | end, "too many arguments") 291 | end) 292 | 293 | it("doesn't accept GNU-like long options when it doesn't need arguments", function() 294 | local parser = Parser() 295 | parser:flag "-q" "--quiet" 296 | assert.has_error(function() 297 | parser:parse{"--quiet=very_quiet"} 298 | end, "option '--quiet' does not take arguments") 299 | end) 300 | 301 | it("handles too many invocations correctly", function() 302 | local parser = Parser() 303 | parser:flag "-q" "--quiet" { 304 | count = 1, 305 | overwrite = false 306 | } 307 | assert.has_error(function() 308 | parser:parse{"-qq"} 309 | end, "option '-q' must be used 1 time") 310 | end) 311 | 312 | it("handles too few invocations correctly", function() 313 | local parser = Parser() 314 | parser:option "-f" "--foo" { 315 | count = "3-4" 316 | } 317 | assert.has_error(function() 318 | parser:parse{"-fFOO", "--foo=BAR"} 319 | end, "option '--foo' must be used at least 3 times") 320 | assert.has_error( 321 | function() parser:parse{} 322 | end, "missing option '-f'") 323 | end) 324 | end) 325 | end) 326 | -------------------------------------------------------------------------------- /spec/help_spec.lua: -------------------------------------------------------------------------------- 1 | local Parser = require "argparse" 2 | getmetatable(Parser()).error = function(_, msg) error(msg) end 3 | 4 | describe("tests related to help message generation", function() 5 | it("creates correct help message for empty parser", function() 6 | local parser = Parser "foo" 7 | assert.equal([[ 8 | Usage: foo [-h] 9 | 10 | Options: 11 | -h, --help Show this help message and exit.]], parser:get_help()) 12 | end) 13 | 14 | it("does not create extra help options when :prepare is called several times", function() 15 | local parser = Parser "foo" 16 | assert.equal([[ 17 | Usage: foo [-h] 18 | 19 | Options: 20 | -h, --help Show this help message and exit.]], parser:get_help()) 21 | end) 22 | 23 | it("uses custom help option ", function() 24 | local parser = Parser "foo" 25 | :add_help "/?" 26 | assert.equal([[ 27 | Usage: foo [/?] 28 | 29 | Options: 30 | /? Show this help message and exit.]], parser:get_help()) 31 | end) 32 | 33 | it("uses description and epilog", function() 34 | local parser = Parser("foo", "A description.", "An epilog.") 35 | 36 | assert.equal([[ 37 | Usage: foo [-h] 38 | 39 | A description. 40 | 41 | Options: 42 | -h, --help Show this help message and exit. 43 | 44 | An epilog.]], parser:get_help()) 45 | end) 46 | 47 | it("creates correct help message for arguments", function() 48 | local parser = Parser "foo" 49 | parser:argument "first" 50 | parser:argument "second-and-third" 51 | :args "2" 52 | parser:argument "maybe-fourth" 53 | :args "?" 54 | parser:argument("others", "Optional.") 55 | :args "*" 56 | 57 | assert.equal([[ 58 | Usage: foo [-h] 59 | [] [] ... 60 | 61 | Arguments: 62 | first 63 | second-and-third 64 | maybe-fourth 65 | others Optional. 66 | 67 | Options: 68 | -h, --help Show this help message and exit.]], parser:get_help()) 69 | end) 70 | 71 | it("creates correct help message for options", function() 72 | local parser = Parser "foo" 73 | parser:flag "-q" "--quiet" 74 | parser:option "--from" 75 | :count "1" 76 | :target "server" 77 | parser:option "--config" 78 | 79 | assert.equal([[ 80 | Usage: foo [-q] --from [--config ] [-h] 81 | 82 | Options: 83 | -q, --quiet 84 | --from 85 | --config 86 | -h, --help Show this help message and exit.]], parser:get_help()) 87 | end) 88 | 89 | it("adds margin for multiline descriptions", function() 90 | local parser = Parser "foo" 91 | parser:flag "-v" 92 | :count "0-2" 93 | :target "verbosity" 94 | :description [[ 95 | Sets verbosity level. 96 | -v: Report all warnings. 97 | -vv: Report all debugging information.]] 98 | 99 | assert.equal([[ 100 | Usage: foo [-v] [-h] 101 | 102 | Options: 103 | -v Sets verbosity level. 104 | -v: Report all warnings. 105 | -vv: Report all debugging information. 106 | -h, --help Show this help message and exit.]], parser:get_help()) 107 | end) 108 | 109 | it("puts different aliases on different lines if there are arguments", function() 110 | local parser = Parser "foo" 111 | 112 | parser:option "-o --output" 113 | 114 | assert.equal([[ 115 | Usage: foo [-o ] [-h] 116 | 117 | Options: 118 | -o , 119 | --output 120 | -h, --help Show this help message and exit.]], parser:get_help()) 121 | end) 122 | 123 | it("handles description with more lines than usage", function() 124 | local parser = Parser "foo" 125 | 126 | parser:option "-o --output" 127 | :description [[ 128 | Sets output file. 129 | If missing, 'a.out' is used by default. 130 | If '-' is passed, output to stdount. 131 | ]] 132 | 133 | assert.equal([[ 134 | Usage: foo [-o ] [-h] 135 | 136 | Options: 137 | -o , Sets output file. 138 | --output If missing, 'a.out' is used by default. 139 | If '-' is passed, output to stdount. 140 | -h, --help Show this help message and exit.]], parser:get_help()) 141 | end) 142 | 143 | it("handles description with less lines than usage", function() 144 | local parser = Parser "foo" 145 | 146 | parser:option "-o --output" 147 | :description "Sets output file." 148 | 149 | assert.equal([[ 150 | Usage: foo [-o ] [-h] 151 | 152 | Options: 153 | -o , Sets output file. 154 | --output 155 | -h, --help Show this help message and exit.]], parser:get_help()) 156 | end) 157 | 158 | it("handles very long argument lists", function() 159 | local parser = Parser "foo" 160 | 161 | parser:option "-t --at-least-three" 162 | :args("3+") 163 | :argname {"", "", ""} 164 | :description "Sometimes argument lists are really long." 165 | 166 | assert.equal([[ 167 | Usage: foo [-h] [-t ...] 168 | 169 | Options: 170 | -t ..., 171 | --at-least-three ... 172 | Sometimes argument lists are really long. 173 | -h, --help Show this help message and exit.]], parser:get_help()) 174 | end) 175 | 176 | it("shows default values", function() 177 | local parser = Parser "foo" 178 | parser:option "-o" 179 | :default "a.out" 180 | parser:option "-p" 181 | :default "8080" 182 | :description "Port." 183 | 184 | assert.equal([[ 185 | Usage: foo [-o ] [-p

] [-h] 186 | 187 | Options: 188 | -o default: a.out 189 | -p

Port. (default: 8080) 190 | -h, --help Show this help message and exit.]], parser:get_help()) 191 | end) 192 | 193 | it("does not show default value when show_default == false", function() 194 | local parser = Parser "foo" 195 | parser:option "-o" 196 | :default "a.out" 197 | :show_default(false) 198 | parser:option "-p" 199 | :default "8080" 200 | :show_default(false) 201 | :description "Port." 202 | 203 | assert.equal([[ 204 | Usage: foo [-o ] [-p

] [-h] 205 | 206 | Options: 207 | -o 208 | -p

Port. 209 | -h, --help Show this help message and exit.]], parser:get_help()) 210 | end) 211 | 212 | it("creates correct help message for commands", function() 213 | local parser = Parser "foo" 214 | parser:flag "-q --quiet" 215 | local run = parser:command "run" 216 | :description "Run! " 217 | run:option "--where" 218 | 219 | assert.equal([[ 220 | Usage: foo [-q] [-h] ... 221 | 222 | Options: 223 | -q, --quiet 224 | -h, --help Show this help message and exit. 225 | 226 | Commands: 227 | run Run! ]], parser:get_help()) 228 | end) 229 | 230 | it("creates correct help message for subcommands", function() 231 | local parser = Parser "foo" 232 | parser:flag "-q" "--quiet" 233 | local run = parser:command "run" 234 | run:option "--where" 235 | 236 | assert.equal([[ 237 | Usage: foo run [--where ] [-h] 238 | 239 | Options: 240 | --where 241 | -h, --help Show this help message and exit.]], run:get_help()) 242 | end) 243 | 244 | it("uses message provided by user", function() 245 | local parser = Parser "foo" 246 | :help "I don't like your format of help messages" 247 | parser:flag "-q" "--quiet" 248 | 249 | assert.equal([[ 250 | I don't like your format of help messages]], parser:get_help()) 251 | end) 252 | 253 | it("does not mention hidden arguments, options, and commands", function() 254 | local parser = Parser "foo" 255 | parser:argument "normal" 256 | parser:argument "deprecated" 257 | :args "?" 258 | :hidden(true) 259 | parser:flag "--feature" 260 | parser:flag "--misfeature" 261 | :hidden(true) 262 | parser:command "good" 263 | parser:command "okay" 264 | parser:command "never-use-this-one" 265 | :hidden(true) 266 | 267 | assert.equal([[ 268 | Usage: foo [--feature] [-h] ... 269 | 270 | Arguments: 271 | normal 272 | 273 | Options: 274 | --feature 275 | -h, --help Show this help message and exit. 276 | 277 | Commands: 278 | good 279 | okay]], parser:get_help()) 280 | end) 281 | 282 | it("omits categories if all elements are hidden", function() 283 | local parser = Parser "foo" 284 | :add_help(false) 285 | parser:argument "deprecated" 286 | :args "?" 287 | :hidden(true) 288 | parser:flag "--misfeature" 289 | :hidden(true) 290 | 291 | assert.equal([[ 292 | Usage: foo]], parser:get_help()) 293 | end) 294 | 295 | it("supports grouping options", function() 296 | local parser = Parser "foo" 297 | :add_help(false) 298 | parser:argument "thing" 299 | 300 | parser:group("Options for setting position", 301 | parser:option "--coords" 302 | :args(2) 303 | :argname {"", ""} 304 | :description "Set coordinates.", 305 | parser:option "--polar" 306 | :args(2) 307 | :argname {"", ""} 308 | :description "Set polar coordinates." 309 | ) 310 | 311 | parser:group("Options for setting style", 312 | parser:flag "--dotted" 313 | :description "More dots.", 314 | parser:option "--width" 315 | :argname "" 316 | :description "Set width." 317 | ) 318 | 319 | assert.equal([[ 320 | Usage: foo [--coords ] [--polar ] [--dotted] 321 | [--width ] 322 | 323 | Arguments: 324 | thing 325 | 326 | Options for setting position: 327 | --coords Set coordinates. 328 | --polar Set polar coordinates. 329 | 330 | Options for setting style: 331 | --dotted More dots. 332 | --width Set width.]], parser:get_help()) 333 | end) 334 | 335 | it("adds default group with 'other' prefix if not all elements of a type are grouped", function() 336 | local parser = Parser "foo" 337 | 338 | parser:group("Main arguments", 339 | parser:argument "foo", 340 | parser:argument "bar", 341 | parser:flag "--use-default-args" 342 | ) 343 | 344 | parser:argument "optional" 345 | :args "?" 346 | 347 | parser:group("Main options", 348 | parser:flag "--something", 349 | parser:option "--test" 350 | ) 351 | 352 | parser:flag "--version" 353 | 354 | parser:group("Some commands", 355 | parser:command "foo", 356 | parser:command "bar" 357 | ) 358 | 359 | parser:command "another-command" 360 | 361 | assert.equal([[ 362 | Usage: foo [--use-default-args] [--something] [--test ] 363 | [--version] [-h] [] ... 364 | 365 | Main arguments: 366 | foo 367 | bar 368 | --use-default-args 369 | 370 | Other arguments: 371 | optional 372 | 373 | Main options: 374 | --something 375 | --test 376 | 377 | Other options: 378 | --version 379 | -h, --help Show this help message and exit. 380 | 381 | Some commands: 382 | foo 383 | bar 384 | 385 | Other commands: 386 | another-command]], parser:get_help()) 387 | end) 388 | 389 | it("allows spacing out element help blocks more with help_vertical_space", function() 390 | local parser = Parser "foo" 391 | :help_vertical_space(1) 392 | 393 | parser:argument "arg1" 394 | :description "Argument number one." 395 | parser:argument "arg2" 396 | :description "Argument number two." 397 | 398 | parser:flag "-p" 399 | :description "This is a thing." 400 | parser:option "-f --foo" 401 | :description [[ 402 | And this things uses many lines. 403 | Because it has lots of complex behaviour. 404 | That needs documenting.]] 405 | 406 | assert.equal([[ 407 | Usage: foo [-p] [-f ] [-h] 408 | 409 | Arguments: 410 | 411 | arg1 Argument number one. 412 | 413 | arg2 Argument number two. 414 | 415 | Options: 416 | 417 | -p This is a thing. 418 | 419 | -f , And this things uses many lines. 420 | --foo Because it has lots of complex behaviour. 421 | That needs documenting. 422 | 423 | -h, --help Show this help message and exit.]], parser:get_help()) 424 | end) 425 | 426 | it("inherits help_vertical_space in commands", function() 427 | local parser = Parser "foo" 428 | :help_vertical_space(1) 429 | 430 | local cmd1 = parser:command "cmd1" 431 | :help_vertical_space(2) 432 | 433 | cmd1:flag("-a", "Do a thing.") 434 | cmd1:flag("-b", "Do b thing.") 435 | 436 | local cmd2 = parser:command "cmd2" 437 | 438 | cmd2:flag("-c", "Do c thing.") 439 | cmd2:flag("-d", "Do d thing.") 440 | 441 | assert.equal([[ 442 | Usage: foo cmd1 [-a] [-b] [-h] 443 | 444 | Options: 445 | 446 | 447 | -a Do a thing. 448 | 449 | 450 | -b Do b thing. 451 | 452 | 453 | -h, --help Show this help message and exit.]], cmd1:get_help()) 454 | 455 | assert.equal([[ 456 | Usage: foo cmd2 [-c] [-d] [-h] 457 | 458 | Options: 459 | 460 | -c Do c thing. 461 | 462 | -d Do d thing. 463 | 464 | -h, --help Show this help message and exit.]], cmd2:get_help()) 465 | end) 466 | 467 | it("allows configuring margins using help_usage_margin and help_description_margin", function() 468 | local parser = Parser "foo" 469 | :help_usage_margin(2) 470 | :help_description_margin(15) 471 | 472 | parser:argument "arg1" 473 | :description "Argument number one." 474 | parser:argument "arg2" 475 | :description "Argument number two." 476 | 477 | parser:flag "-p" 478 | :description "This is a thing." 479 | parser:option "-f --foo" 480 | :description [[ 481 | And this things uses many lines. 482 | Because it has lots of complex behaviour. 483 | That needs documenting.]] 484 | 485 | assert.equal([[ 486 | Usage: foo [-p] [-f ] [-h] 487 | 488 | Arguments: 489 | arg1 Argument number one. 490 | arg2 Argument number two. 491 | 492 | Options: 493 | -p This is a thing. 494 | -f , And this things uses many lines. 495 | --foo Because it has lots of complex behaviour. 496 | That needs documenting. 497 | -h, --help Show this help message and exit.]], parser:get_help()) 498 | end) 499 | 500 | describe("autowrap", function() 501 | it("automatically wraps descriptions to match given max width", function() 502 | local parser = Parser "foo" 503 | :help_max_width(80) 504 | 505 | parser:option "-f --foo" 506 | :description("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " .. 507 | "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation " .. 508 | "ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit " .. 509 | "in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat " .. 510 | "non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") 511 | parser:option "-b --bar" 512 | :description "See above." 513 | 514 | assert.equal([[ 515 | Usage: foo [-f ] [-b ] [-h] 516 | 517 | Options: 518 | -f , Lorem ipsum dolor sit amet, consectetur adipiscing 519 | --foo elit, sed do eiusmod tempor incididunt ut labore et 520 | dolore magna aliqua. Ut enim ad minim veniam, quis 521 | nostrud exercitation ullamco laboris nisi ut aliquip ex 522 | ea commodo consequat. Duis aute irure dolor in 523 | reprehenderit in voluptate velit esse cillum dolore eu 524 | fugiat nulla pariatur. Excepteur sint occaecat 525 | cupidatat non proident, sunt in culpa qui officia 526 | deserunt mollit anim id est laborum. 527 | -b , See above. 528 | --bar 529 | -h, --help Show this help message and exit.]], parser:get_help()) 530 | end) 531 | 532 | it("preserves existing line breaks", function() 533 | local parser = Parser "foo" 534 | :help_max_width(80) 535 | 536 | parser:option "-f --foo" 537 | :description("This is a long line, it should be broken down into several lines. " .. [[ 538 | It just keeps going and going. 539 | This should always be a new line. 540 | Another one. 541 | ]]) 542 | parser:option "-b --bar" 543 | 544 | assert.equal([[ 545 | Usage: foo [-f ] [-b ] [-h] 546 | 547 | Options: 548 | -f , This is a long line, it should be broken down into 549 | --foo several lines. It just keeps going and going. 550 | This should always be a new line. 551 | Another one. 552 | -b , 553 | --bar 554 | -h, --help Show this help message and exit.]], parser:get_help()) 555 | end) 556 | 557 | it("preserves indentation", function() 558 | local parser = Parser "foo" 559 | :help_max_width(80) 560 | 561 | parser:option "-f --foo" 562 | :description("This is a long line, it should be broken down into several lines.\n" .. 563 | " This paragraph is indented with three spaces, so when it gets broken down into several lines, " .. 564 | "they will be, too.\n\n" .. 565 | " That was an empty line there, preserve it.") 566 | 567 | assert.equal([[ 568 | Usage: foo [-f ] [-h] 569 | 570 | Options: 571 | -f , This is a long line, it should be broken down into 572 | --foo several lines. 573 | This paragraph is indented with three spaces, so 574 | when it gets broken down into several lines, they 575 | will be, too. 576 | 577 | That was an empty line there, preserve it. 578 | -h, --help Show this help message and exit.]], parser:get_help()) 579 | end) 580 | 581 | it("preserves indentation of list items", function() 582 | local parser = Parser "foo" 583 | :help_max_width(80) 584 | 585 | parser:option "-f --foo" 586 | :description("Let's start a list:\n\n" .. 587 | "* Here is a list item.\n" .. 588 | "* Here is another one, this one is very long so it needs several lines. More words. Word. Word.\n" .. 589 | " + Here is a nested list item. Word. Word. Word. Word. Word. Bird. Word. Bird. Bird. Bird.\n" .. 590 | "* Back to normal list, this one uses several spaces after the list item mark. Bird. Bird. Bird.") 591 | 592 | 593 | assert.equal([[ 594 | Usage: foo [-f ] [-h] 595 | 596 | Options: 597 | -f , Let's start a list: 598 | --foo 599 | * Here is a list item. 600 | * Here is another one, this one is very long so it 601 | needs several lines. More words. Word. Word. 602 | + Here is a nested list item. Word. Word. Word. Word. 603 | Word. Bird. Word. Bird. Bird. Bird. 604 | * Back to normal list, this one uses several spaces 605 | after the list item mark. Bird. Bird. Bird. 606 | -h, --help Show this help message and exit.]], parser:get_help()) 607 | end) 608 | 609 | it("preserves multiple spaces between words", function() 610 | local parser = Parser "foo" 611 | :help_max_width(80) 612 | 613 | parser:option "-f --foo" 614 | :description("This is a long line with two spaces between words, it should be broken down.") 615 | 616 | assert.equal([[ 617 | Usage: foo [-f ] [-h] 618 | 619 | Options: 620 | -f , This is a long line with two spaces between 621 | --foo words, it should be broken down. 622 | -h, --help Show this help message and exit.]], parser:get_help()) 623 | end) 624 | 625 | it("autowraps description and epilog", function() 626 | local parser = Parser "foo" 627 | :help_max_width(80) 628 | :description("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " .. 629 | "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation " .. 630 | "ullamco laboris nisi ut aliquip ex ea commodo consequat.") 631 | :epilog("Duis aute irure dolor in reprehenderit " .. 632 | "in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat " .. 633 | "non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") 634 | 635 | assert.equal([[ 636 | Usage: foo [-h] 637 | 638 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor 639 | incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis 640 | nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 641 | 642 | Options: 643 | -h, --help Show this help message and exit. 644 | 645 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu 646 | fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in 647 | culpa qui officia deserunt mollit anim id est laborum.]], parser:get_help()) 648 | end) 649 | end) 650 | end) 651 | -------------------------------------------------------------------------------- /src/argparse.lua: -------------------------------------------------------------------------------- 1 | -- The MIT License (MIT) 2 | 3 | -- Copyright (c) 2013 - 2018 Peter Melnichenko 4 | 5 | -- Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | -- this software and associated documentation files (the "Software"), to deal in 7 | -- the Software without restriction, including without limitation the rights to 8 | -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | -- the Software, and to permit persons to whom the Software is furnished to do so, 10 | -- subject to the following conditions: 11 | 12 | -- The above copyright notice and this permission notice shall be included in all 13 | -- copies or substantial portions of the Software. 14 | 15 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | -- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | -- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | -- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | -- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | local function deep_update(t1, t2) 23 | for k, v in pairs(t2) do 24 | if type(v) == "table" then 25 | v = deep_update({}, v) 26 | end 27 | 28 | t1[k] = v 29 | end 30 | 31 | return t1 32 | end 33 | 34 | -- A property is a tuple {name, callback}. 35 | -- properties.args is number of properties that can be set as arguments 36 | -- when calling an object. 37 | local function class(prototype, properties, parent) 38 | -- Class is the metatable of its instances. 39 | local cl = {} 40 | cl.__index = cl 41 | 42 | if parent then 43 | cl.__prototype = deep_update(deep_update({}, parent.__prototype), prototype) 44 | else 45 | cl.__prototype = prototype 46 | end 47 | 48 | if properties then 49 | local names = {} 50 | 51 | -- Create setter methods and fill set of property names. 52 | for _, property in ipairs(properties) do 53 | local name, callback = property[1], property[2] 54 | 55 | cl[name] = function(self, value) 56 | if not callback(self, value) then 57 | self["_" .. name] = value 58 | end 59 | 60 | return self 61 | end 62 | 63 | names[name] = true 64 | end 65 | 66 | function cl.__call(self, ...) 67 | -- When calling an object, if the first argument is a table, 68 | -- interpret keys as property names, else delegate arguments 69 | -- to corresponding setters in order. 70 | if type((...)) == "table" then 71 | for name, value in pairs((...)) do 72 | if names[name] then 73 | self[name](self, value) 74 | end 75 | end 76 | else 77 | local nargs = select("#", ...) 78 | 79 | for i, property in ipairs(properties) do 80 | if i > nargs or i > properties.args then 81 | break 82 | end 83 | 84 | local arg = select(i, ...) 85 | 86 | if arg ~= nil then 87 | self[property[1]](self, arg) 88 | end 89 | end 90 | end 91 | 92 | return self 93 | end 94 | end 95 | 96 | -- If indexing class fails, fallback to its parent. 97 | local class_metatable = {} 98 | class_metatable.__index = parent 99 | 100 | function class_metatable.__call(self, ...) 101 | -- Calling a class returns its instance. 102 | -- Arguments are delegated to the instance. 103 | local object = deep_update({}, self.__prototype) 104 | setmetatable(object, self) 105 | return object(...) 106 | end 107 | 108 | return setmetatable(cl, class_metatable) 109 | end 110 | 111 | local function typecheck(name, types, value) 112 | for _, type_ in ipairs(types) do 113 | if type(value) == type_ then 114 | return true 115 | end 116 | end 117 | 118 | error(("bad property '%s' (%s expected, got %s)"):format(name, table.concat(types, " or "), type(value))) 119 | end 120 | 121 | local function typechecked(name, ...) 122 | local types = {...} 123 | return {name, function(_, value) typecheck(name, types, value) end} 124 | end 125 | 126 | local multiname = {"name", function(self, value) 127 | typecheck("name", {"string"}, value) 128 | 129 | for alias in value:gmatch("%S+") do 130 | self._name = self._name or alias 131 | table.insert(self._aliases, alias) 132 | end 133 | 134 | -- Do not set _name as with other properties. 135 | return true 136 | end} 137 | 138 | local function parse_boundaries(str) 139 | if tonumber(str) then 140 | return tonumber(str), tonumber(str) 141 | end 142 | 143 | if str == "*" then 144 | return 0, math.huge 145 | end 146 | 147 | if str == "+" then 148 | return 1, math.huge 149 | end 150 | 151 | if str == "?" then 152 | return 0, 1 153 | end 154 | 155 | if str:match "^%d+%-%d+$" then 156 | local min, max = str:match "^(%d+)%-(%d+)$" 157 | return tonumber(min), tonumber(max) 158 | end 159 | 160 | if str:match "^%d+%+$" then 161 | local min = str:match "^(%d+)%+$" 162 | return tonumber(min), math.huge 163 | end 164 | end 165 | 166 | local function boundaries(name) 167 | return {name, function(self, value) 168 | typecheck(name, {"number", "string"}, value) 169 | 170 | local min, max = parse_boundaries(value) 171 | 172 | if not min then 173 | error(("bad property '%s'"):format(name)) 174 | end 175 | 176 | self["_min" .. name], self["_max" .. name] = min, max 177 | end} 178 | end 179 | 180 | local actions = {} 181 | 182 | local option_action = {"action", function(_, value) 183 | typecheck("action", {"function", "string"}, value) 184 | 185 | if type(value) == "string" and not actions[value] then 186 | error(("unknown action '%s'"):format(value)) 187 | end 188 | end} 189 | 190 | local option_init = {"init", function(self) 191 | self._has_init = true 192 | end} 193 | 194 | local option_default = {"default", function(self, value) 195 | if type(value) ~= "string" then 196 | self._init = value 197 | self._has_init = true 198 | return true 199 | end 200 | end} 201 | 202 | local add_help = {"add_help", function(self, value) 203 | typecheck("add_help", {"boolean", "string", "table"}, value) 204 | 205 | if self._has_help then 206 | table.remove(self._options) 207 | self._has_help = false 208 | end 209 | 210 | if value then 211 | local help = self:flag() 212 | :description "Show this help message and exit." 213 | :action(function() 214 | print(self:get_help()) 215 | os.exit(0) 216 | end) 217 | 218 | if value ~= true then 219 | help = help(value) 220 | end 221 | 222 | if not help._name then 223 | help "-h" "--help" 224 | end 225 | 226 | self._has_help = true 227 | end 228 | end} 229 | 230 | local Parser = class({ 231 | _arguments = {}, 232 | _options = {}, 233 | _commands = {}, 234 | _mutexes = {}, 235 | _groups = {}, 236 | _require_command = true, 237 | _handle_options = true 238 | }, { 239 | args = 3, 240 | typechecked("name", "string"), 241 | typechecked("description", "string"), 242 | typechecked("epilog", "string"), 243 | typechecked("usage", "string"), 244 | typechecked("help", "string"), 245 | typechecked("require_command", "boolean"), 246 | typechecked("handle_options", "boolean"), 247 | typechecked("action", "function"), 248 | typechecked("command_target", "string"), 249 | typechecked("help_vertical_space", "number"), 250 | typechecked("usage_margin", "number"), 251 | typechecked("usage_max_width", "number"), 252 | typechecked("help_usage_margin", "number"), 253 | typechecked("help_description_margin", "number"), 254 | typechecked("help_max_width", "number"), 255 | add_help 256 | }) 257 | 258 | local Command = class({ 259 | _aliases = {} 260 | }, { 261 | args = 3, 262 | multiname, 263 | typechecked("description", "string"), 264 | typechecked("epilog", "string"), 265 | typechecked("target", "string"), 266 | typechecked("usage", "string"), 267 | typechecked("help", "string"), 268 | typechecked("require_command", "boolean"), 269 | typechecked("handle_options", "boolean"), 270 | typechecked("action", "function"), 271 | typechecked("command_target", "string"), 272 | typechecked("help_vertical_space", "number"), 273 | typechecked("usage_margin", "number"), 274 | typechecked("usage_max_width", "number"), 275 | typechecked("help_usage_margin", "number"), 276 | typechecked("help_description_margin", "number"), 277 | typechecked("help_max_width", "number"), 278 | typechecked("hidden", "boolean"), 279 | add_help 280 | }, Parser) 281 | 282 | local Argument = class({ 283 | _minargs = 1, 284 | _maxargs = 1, 285 | _mincount = 1, 286 | _maxcount = 1, 287 | _defmode = "unused", 288 | _show_default = true 289 | }, { 290 | args = 5, 291 | typechecked("name", "string"), 292 | typechecked("description", "string"), 293 | option_default, 294 | typechecked("convert", "function", "table"), 295 | boundaries("args"), 296 | typechecked("target", "string"), 297 | typechecked("defmode", "string"), 298 | typechecked("show_default", "boolean"), 299 | typechecked("argname", "string", "table"), 300 | typechecked("hidden", "boolean"), 301 | option_action, 302 | option_init 303 | }) 304 | 305 | local Option = class({ 306 | _aliases = {}, 307 | _mincount = 0, 308 | _overwrite = true 309 | }, { 310 | args = 6, 311 | multiname, 312 | typechecked("description", "string"), 313 | option_default, 314 | typechecked("convert", "function", "table"), 315 | boundaries("args"), 316 | boundaries("count"), 317 | typechecked("target", "string"), 318 | typechecked("defmode", "string"), 319 | typechecked("show_default", "boolean"), 320 | typechecked("overwrite", "boolean"), 321 | typechecked("argname", "string", "table"), 322 | typechecked("hidden", "boolean"), 323 | option_action, 324 | option_init 325 | }, Argument) 326 | 327 | function Parser:_inherit_property(name, default) 328 | local element = self 329 | 330 | while true do 331 | local value = element["_" .. name] 332 | 333 | if value ~= nil then 334 | return value 335 | end 336 | 337 | if not element._parent then 338 | return default 339 | end 340 | 341 | element = element._parent 342 | end 343 | end 344 | 345 | function Argument:_get_argument_list() 346 | local buf = {} 347 | local i = 1 348 | 349 | while i <= math.min(self._minargs, 3) do 350 | local argname = self:_get_argname(i) 351 | 352 | if self._default and self._defmode:find "a" then 353 | argname = "[" .. argname .. "]" 354 | end 355 | 356 | table.insert(buf, argname) 357 | i = i+1 358 | end 359 | 360 | while i <= math.min(self._maxargs, 3) do 361 | table.insert(buf, "[" .. self:_get_argname(i) .. "]") 362 | i = i+1 363 | 364 | if self._maxargs == math.huge then 365 | break 366 | end 367 | end 368 | 369 | if i < self._maxargs then 370 | table.insert(buf, "...") 371 | end 372 | 373 | return buf 374 | end 375 | 376 | function Argument:_get_usage() 377 | local usage = table.concat(self:_get_argument_list(), " ") 378 | 379 | if self._default and self._defmode:find "u" then 380 | if self._maxargs > 1 or (self._minargs == 1 and not self._defmode:find "a") then 381 | usage = "[" .. usage .. "]" 382 | end 383 | end 384 | 385 | return usage 386 | end 387 | 388 | function actions.store_true(result, target) 389 | result[target] = true 390 | end 391 | 392 | function actions.store_false(result, target) 393 | result[target] = false 394 | end 395 | 396 | function actions.store(result, target, argument) 397 | result[target] = argument 398 | end 399 | 400 | function actions.count(result, target, _, overwrite) 401 | if not overwrite then 402 | result[target] = result[target] + 1 403 | end 404 | end 405 | 406 | function actions.append(result, target, argument, overwrite) 407 | result[target] = result[target] or {} 408 | table.insert(result[target], argument) 409 | 410 | if overwrite then 411 | table.remove(result[target], 1) 412 | end 413 | end 414 | 415 | function actions.concat(result, target, arguments, overwrite) 416 | if overwrite then 417 | error("'concat' action can't handle too many invocations") 418 | end 419 | 420 | result[target] = result[target] or {} 421 | 422 | for _, argument in ipairs(arguments) do 423 | table.insert(result[target], argument) 424 | end 425 | end 426 | 427 | function Argument:_get_action() 428 | local action, init 429 | 430 | if self._maxcount == 1 then 431 | if self._maxargs == 0 then 432 | action, init = "store_true", nil 433 | else 434 | action, init = "store", nil 435 | end 436 | else 437 | if self._maxargs == 0 then 438 | action, init = "count", 0 439 | else 440 | action, init = "append", {} 441 | end 442 | end 443 | 444 | if self._action then 445 | action = self._action 446 | end 447 | 448 | if self._has_init then 449 | init = self._init 450 | end 451 | 452 | if type(action) == "string" then 453 | action = actions[action] 454 | end 455 | 456 | return action, init 457 | end 458 | 459 | -- Returns placeholder for `narg`-th argument. 460 | function Argument:_get_argname(narg) 461 | local argname = self._argname or self:_get_default_argname() 462 | 463 | if type(argname) == "table" then 464 | return argname[narg] 465 | else 466 | return argname 467 | end 468 | end 469 | 470 | function Argument:_get_default_argname() 471 | return "<" .. self._name .. ">" 472 | end 473 | 474 | function Option:_get_default_argname() 475 | return "<" .. self:_get_default_target() .. ">" 476 | end 477 | 478 | -- Returns labels to be shown in the help message. 479 | function Argument:_get_label_lines() 480 | return {self._name} 481 | end 482 | 483 | function Option:_get_label_lines() 484 | local argument_list = self:_get_argument_list() 485 | 486 | if #argument_list == 0 then 487 | -- Don't put aliases for simple flags like `-h` on different lines. 488 | return {table.concat(self._aliases, ", ")} 489 | end 490 | 491 | local longest_alias_length = -1 492 | 493 | for _, alias in ipairs(self._aliases) do 494 | longest_alias_length = math.max(longest_alias_length, #alias) 495 | end 496 | 497 | local argument_list_repr = table.concat(argument_list, " ") 498 | local lines = {} 499 | 500 | for i, alias in ipairs(self._aliases) do 501 | local line = (" "):rep(longest_alias_length - #alias) .. alias .. " " .. argument_list_repr 502 | 503 | if i ~= #self._aliases then 504 | line = line .. "," 505 | end 506 | 507 | table.insert(lines, line) 508 | end 509 | 510 | return lines 511 | end 512 | 513 | function Command:_get_label_lines() 514 | return {table.concat(self._aliases, ", ")} 515 | end 516 | 517 | function Argument:_get_description() 518 | if self._default and self._show_default then 519 | if self._description then 520 | return ("%s (default: %s)"):format(self._description, self._default) 521 | else 522 | return ("default: %s"):format(self._default) 523 | end 524 | else 525 | return self._description or "" 526 | end 527 | end 528 | 529 | function Command:_get_description() 530 | return self._description or "" 531 | end 532 | 533 | function Option:_get_usage() 534 | local usage = self:_get_argument_list() 535 | table.insert(usage, 1, self._name) 536 | usage = table.concat(usage, " ") 537 | 538 | if self._mincount == 0 or self._default then 539 | usage = "[" .. usage .. "]" 540 | end 541 | 542 | return usage 543 | end 544 | 545 | function Argument:_get_default_target() 546 | return self._name 547 | end 548 | 549 | function Option:_get_default_target() 550 | local res 551 | 552 | for _, alias in ipairs(self._aliases) do 553 | if alias:sub(1, 1) == alias:sub(2, 2) then 554 | res = alias:sub(3) 555 | break 556 | end 557 | end 558 | 559 | res = res or self._name:sub(2) 560 | return (res:gsub("-", "_")) 561 | end 562 | 563 | function Option:_is_vararg() 564 | return self._maxargs ~= self._minargs 565 | end 566 | 567 | function Parser:_get_fullname() 568 | local parent = self._parent 569 | local buf = {self._name} 570 | 571 | while parent do 572 | table.insert(buf, 1, parent._name) 573 | parent = parent._parent 574 | end 575 | 576 | return table.concat(buf, " ") 577 | end 578 | 579 | function Parser:_update_charset(charset) 580 | charset = charset or {} 581 | 582 | for _, command in ipairs(self._commands) do 583 | command:_update_charset(charset) 584 | end 585 | 586 | for _, option in ipairs(self._options) do 587 | for _, alias in ipairs(option._aliases) do 588 | charset[alias:sub(1, 1)] = true 589 | end 590 | end 591 | 592 | return charset 593 | end 594 | 595 | function Parser:argument(...) 596 | local argument = Argument(...) 597 | table.insert(self._arguments, argument) 598 | return argument 599 | end 600 | 601 | function Parser:option(...) 602 | local option = Option(...) 603 | 604 | if self._has_help then 605 | table.insert(self._options, #self._options, option) 606 | else 607 | table.insert(self._options, option) 608 | end 609 | 610 | return option 611 | end 612 | 613 | function Parser:flag(...) 614 | return self:option():args(0)(...) 615 | end 616 | 617 | function Parser:command(...) 618 | local command = Command():add_help(true)(...) 619 | command._parent = self 620 | table.insert(self._commands, command) 621 | return command 622 | end 623 | 624 | function Parser:mutex(...) 625 | local elements = {...} 626 | 627 | for i, element in ipairs(elements) do 628 | local mt = getmetatable(element) 629 | assert(mt == Option or mt == Argument, ("bad argument #%d to 'mutex' (Option or Argument expected)"):format(i)) 630 | end 631 | 632 | table.insert(self._mutexes, elements) 633 | return self 634 | end 635 | 636 | function Parser:group(name, ...) 637 | assert(type(name) == "string", ("bad argument #1 to 'group' (string expected, got %s)"):format(type(name))) 638 | 639 | local group = {name = name, ...} 640 | 641 | for i, element in ipairs(group) do 642 | local mt = getmetatable(element) 643 | assert(mt == Option or mt == Argument or mt == Command, 644 | ("bad argument #%d to 'group' (Option or Argument or Command expected)"):format(i + 1)) 645 | end 646 | 647 | table.insert(self._groups, group) 648 | return self 649 | end 650 | 651 | local usage_welcome = "Usage: " 652 | 653 | function Parser:get_usage() 654 | if self._usage then 655 | return self._usage 656 | end 657 | 658 | local usage_margin = self:_inherit_property("usage_margin", #usage_welcome) 659 | local max_usage_width = self:_inherit_property("usage_max_width", 70) 660 | local lines = {usage_welcome .. self:_get_fullname()} 661 | 662 | local function add(s) 663 | if #lines[#lines]+1+#s <= max_usage_width then 664 | lines[#lines] = lines[#lines] .. " " .. s 665 | else 666 | lines[#lines+1] = (" "):rep(usage_margin) .. s 667 | end 668 | end 669 | 670 | -- Normally options are before positional arguments in usage messages. 671 | -- However, vararg options should be after, because they can't be reliable used 672 | -- before a positional argument. 673 | -- Mutexes come into play, too, and are shown as soon as possible. 674 | -- Overall, output usages in the following order: 675 | -- 1. Mutexes that don't have positional arguments or vararg options. 676 | -- 2. Options that are not in any mutexes and are not vararg. 677 | -- 3. Positional arguments - on their own or as a part of a mutex. 678 | -- 4. Remaining mutexes. 679 | -- 5. Remaining options. 680 | 681 | local elements_in_mutexes = {} 682 | local added_elements = {} 683 | local added_mutexes = {} 684 | local argument_to_mutexes = {} 685 | 686 | local function add_mutex(mutex, main_argument) 687 | if added_mutexes[mutex] then 688 | return 689 | end 690 | 691 | added_mutexes[mutex] = true 692 | local buf = {} 693 | 694 | for _, element in ipairs(mutex) do 695 | if not element._hidden and not added_elements[element] then 696 | if getmetatable(element) == Option or element == main_argument then 697 | table.insert(buf, element:_get_usage()) 698 | added_elements[element] = true 699 | end 700 | end 701 | end 702 | 703 | if #buf == 1 then 704 | add(buf[1]) 705 | elseif #buf > 1 then 706 | add("(" .. table.concat(buf, " | ") .. ")") 707 | end 708 | end 709 | 710 | local function add_element(element) 711 | if not element._hidden and not added_elements[element] then 712 | add(element:_get_usage()) 713 | added_elements[element] = true 714 | end 715 | end 716 | 717 | for _, mutex in ipairs(self._mutexes) do 718 | local is_vararg = false 719 | local has_argument = false 720 | 721 | for _, element in ipairs(mutex) do 722 | if getmetatable(element) == Option then 723 | if element:_is_vararg() then 724 | is_vararg = true 725 | end 726 | else 727 | has_argument = true 728 | argument_to_mutexes[element] = argument_to_mutexes[element] or {} 729 | table.insert(argument_to_mutexes[element], mutex) 730 | end 731 | 732 | elements_in_mutexes[element] = true 733 | end 734 | 735 | if not is_vararg and not has_argument then 736 | add_mutex(mutex) 737 | end 738 | end 739 | 740 | for _, option in ipairs(self._options) do 741 | if not elements_in_mutexes[option] and not option:_is_vararg() then 742 | add_element(option) 743 | end 744 | end 745 | 746 | -- Add usages for positional arguments, together with one mutex containing them, if they are in a mutex. 747 | for _, argument in ipairs(self._arguments) do 748 | -- Pick a mutex as a part of which to show this argument, take the first one that's still available. 749 | local mutex 750 | 751 | if elements_in_mutexes[argument] then 752 | for _, argument_mutex in ipairs(argument_to_mutexes[argument]) do 753 | if not added_mutexes[argument_mutex] then 754 | mutex = argument_mutex 755 | end 756 | end 757 | end 758 | 759 | if mutex then 760 | add_mutex(mutex, argument) 761 | else 762 | add_element(argument) 763 | end 764 | end 765 | 766 | for _, mutex in ipairs(self._mutexes) do 767 | add_mutex(mutex) 768 | end 769 | 770 | for _, option in ipairs(self._options) do 771 | add_element(option) 772 | end 773 | 774 | if #self._commands > 0 then 775 | if self._require_command then 776 | add("") 777 | else 778 | add("[]") 779 | end 780 | 781 | add("...") 782 | end 783 | 784 | return table.concat(lines, "\n") 785 | end 786 | 787 | local function split_lines(s) 788 | if s == "" then 789 | return {} 790 | end 791 | 792 | local lines = {} 793 | 794 | if s:sub(-1) ~= "\n" then 795 | s = s .. "\n" 796 | end 797 | 798 | for line in s:gmatch("([^\n]*)\n") do 799 | table.insert(lines, line) 800 | end 801 | 802 | return lines 803 | end 804 | 805 | local function autowrap_line(line, max_length) 806 | -- Algorithm for splitting lines is simple and greedy. 807 | local result_lines = {} 808 | 809 | -- Preserve original indentation of the line, put this at the beginning of each result line. 810 | -- If the first word looks like a list marker ('*', '+', or '-'), add spaces so that starts 811 | -- of the second and the following lines vertically align with the start of the second word. 812 | local indentation = line:match("^ *") 813 | 814 | if line:find("^ *[%*%+%-]") then 815 | indentation = indentation .. " " .. line:match("^ *[%*%+%-]( *)") 816 | end 817 | 818 | -- Parts of the last line being assembled. 819 | local line_parts = {} 820 | 821 | -- Length of the current line. 822 | local line_length = 0 823 | 824 | -- Index of the next character to consider. 825 | local index = 1 826 | 827 | while true do 828 | local word_start, word_finish, word = line:find("([^ ]+)", index) 829 | 830 | if not word_start then 831 | -- Ignore trailing spaces, if any. 832 | break 833 | end 834 | 835 | local preceding_spaces = line:sub(index, word_start - 1) 836 | index = word_finish + 1 837 | 838 | if (#line_parts == 0) or (line_length + #preceding_spaces + #word <= max_length) then 839 | -- Either this is the very first word or it fits as an addition to the current line, add it. 840 | table.insert(line_parts, preceding_spaces) -- For the very first word this adds the indentation. 841 | table.insert(line_parts, word) 842 | line_length = line_length + #preceding_spaces + #word 843 | else 844 | -- Does not fit, finish current line and put the word into a new one. 845 | table.insert(result_lines, table.concat(line_parts)) 846 | line_parts = {indentation, word} 847 | line_length = #indentation + #word 848 | end 849 | end 850 | 851 | if #line_parts > 0 then 852 | table.insert(result_lines, table.concat(line_parts)) 853 | end 854 | 855 | if #result_lines == 0 then 856 | -- Preserve empty lines. 857 | result_lines[1] = "" 858 | end 859 | 860 | return result_lines 861 | end 862 | 863 | -- Automatically wraps lines within given array, 864 | -- attempting to limit line length to `max_length`. 865 | -- Existing line splits are preserved. 866 | local function autowrap(lines, max_length) 867 | local result_lines = {} 868 | 869 | for _, line in ipairs(lines) do 870 | local autowrapped_lines = autowrap_line(line, max_length) 871 | 872 | for _, autowrapped_line in ipairs(autowrapped_lines) do 873 | table.insert(result_lines, autowrapped_line) 874 | end 875 | end 876 | 877 | return result_lines 878 | end 879 | 880 | function Parser:_get_element_help(element) 881 | local label_lines = element:_get_label_lines() 882 | local description_lines = split_lines(element:_get_description()) 883 | 884 | local result_lines = {} 885 | 886 | -- All label lines should have the same length (except the last one, it has no comma). 887 | -- If too long, start description after all the label lines. 888 | -- Otherwise, combine label and description lines. 889 | 890 | local usage_margin_len = self:_inherit_property("help_usage_margin", 3) 891 | local usage_margin = (" "):rep(usage_margin_len) 892 | local description_margin_len = self:_inherit_property("help_description_margin", 25) 893 | local description_margin = (" "):rep(description_margin_len) 894 | 895 | local help_max_width = self:_inherit_property("help_max_width") 896 | 897 | if help_max_width then 898 | local description_max_width = math.max(help_max_width - description_margin_len, 10) 899 | description_lines = autowrap(description_lines, description_max_width) 900 | end 901 | 902 | if #label_lines[1] >= (description_margin_len - usage_margin_len) then 903 | for _, label_line in ipairs(label_lines) do 904 | table.insert(result_lines, usage_margin .. label_line) 905 | end 906 | 907 | for _, description_line in ipairs(description_lines) do 908 | table.insert(result_lines, description_margin .. description_line) 909 | end 910 | else 911 | for i = 1, math.max(#label_lines, #description_lines) do 912 | local label_line = label_lines[i] 913 | local description_line = description_lines[i] 914 | 915 | local line = "" 916 | 917 | if label_line then 918 | line = usage_margin .. label_line 919 | end 920 | 921 | if description_line and description_line ~= "" then 922 | line = line .. (" "):rep(description_margin_len - #line) .. description_line 923 | end 924 | 925 | table.insert(result_lines, line) 926 | end 927 | end 928 | 929 | return table.concat(result_lines, "\n") 930 | end 931 | 932 | local function get_group_types(group) 933 | local types = {} 934 | 935 | for _, element in ipairs(group) do 936 | types[getmetatable(element)] = true 937 | end 938 | 939 | return types 940 | end 941 | 942 | function Parser:_add_group_help(blocks, added_elements, label, elements) 943 | local buf = {label} 944 | 945 | for _, element in ipairs(elements) do 946 | if not element._hidden and not added_elements[element] then 947 | added_elements[element] = true 948 | table.insert(buf, self:_get_element_help(element)) 949 | end 950 | end 951 | 952 | if #buf > 1 then 953 | table.insert(blocks, table.concat(buf, ("\n"):rep(self:_inherit_property("help_vertical_space", 0) + 1))) 954 | end 955 | end 956 | 957 | function Parser:get_help() 958 | if self._help then 959 | return self._help 960 | end 961 | 962 | local blocks = {self:get_usage()} 963 | 964 | local help_max_width = self:_inherit_property("help_max_width") 965 | 966 | if self._description then 967 | local description = self._description 968 | 969 | if help_max_width then 970 | description = table.concat(autowrap(split_lines(description), help_max_width), "\n") 971 | end 972 | 973 | table.insert(blocks, description) 974 | end 975 | 976 | -- 1. Put groups containing arguments first, then other arguments. 977 | -- 2. Put remaining groups containing options, then other options. 978 | -- 3. Put remaining groups containing commands, then other commands. 979 | -- Assume that an element can't be in several groups. 980 | local groups_by_type = { 981 | [Argument] = {}, 982 | [Option] = {}, 983 | [Command] = {} 984 | } 985 | 986 | for _, group in ipairs(self._groups) do 987 | local group_types = get_group_types(group) 988 | 989 | for _, mt in ipairs({Argument, Option, Command}) do 990 | if group_types[mt] then 991 | table.insert(groups_by_type[mt], group) 992 | break 993 | end 994 | end 995 | end 996 | 997 | local default_groups = { 998 | {name = "Arguments", type = Argument, elements = self._arguments}, 999 | {name = "Options", type = Option, elements = self._options}, 1000 | {name = "Commands", type = Command, elements = self._commands} 1001 | } 1002 | 1003 | local added_elements = {} 1004 | 1005 | for _, default_group in ipairs(default_groups) do 1006 | local type_groups = groups_by_type[default_group.type] 1007 | 1008 | for _, group in ipairs(type_groups) do 1009 | self:_add_group_help(blocks, added_elements, group.name .. ":", group) 1010 | end 1011 | 1012 | local default_label = default_group.name .. ":" 1013 | 1014 | if #type_groups > 0 then 1015 | default_label = "Other " .. default_label:gsub("^.", string.lower) 1016 | end 1017 | 1018 | self:_add_group_help(blocks, added_elements, default_label, default_group.elements) 1019 | end 1020 | 1021 | if self._epilog then 1022 | local epilog = self._epilog 1023 | 1024 | if help_max_width then 1025 | epilog = table.concat(autowrap(split_lines(epilog), help_max_width), "\n") 1026 | end 1027 | 1028 | table.insert(blocks, epilog) 1029 | end 1030 | 1031 | return table.concat(blocks, "\n\n") 1032 | end 1033 | 1034 | local function get_tip(context, wrong_name) 1035 | local context_pool = {} 1036 | local possible_name 1037 | local possible_names = {} 1038 | 1039 | for name in pairs(context) do 1040 | if type(name) == "string" then 1041 | for i = 1, #name do 1042 | possible_name = name:sub(1, i - 1) .. name:sub(i + 1) 1043 | 1044 | if not context_pool[possible_name] then 1045 | context_pool[possible_name] = {} 1046 | end 1047 | 1048 | table.insert(context_pool[possible_name], name) 1049 | end 1050 | end 1051 | end 1052 | 1053 | for i = 1, #wrong_name + 1 do 1054 | possible_name = wrong_name:sub(1, i - 1) .. wrong_name:sub(i + 1) 1055 | 1056 | if context[possible_name] then 1057 | possible_names[possible_name] = true 1058 | elseif context_pool[possible_name] then 1059 | for _, name in ipairs(context_pool[possible_name]) do 1060 | possible_names[name] = true 1061 | end 1062 | end 1063 | end 1064 | 1065 | local first = next(possible_names) 1066 | 1067 | if first then 1068 | if next(possible_names, first) then 1069 | local possible_names_arr = {} 1070 | 1071 | for name in pairs(possible_names) do 1072 | table.insert(possible_names_arr, "'" .. name .. "'") 1073 | end 1074 | 1075 | table.sort(possible_names_arr) 1076 | return "\nDid you mean one of these: " .. table.concat(possible_names_arr, " ") .. "?" 1077 | else 1078 | return "\nDid you mean '" .. first .. "'?" 1079 | end 1080 | else 1081 | return "" 1082 | end 1083 | end 1084 | 1085 | local ElementState = class({ 1086 | invocations = 0 1087 | }) 1088 | 1089 | function ElementState:__call(state, element) 1090 | self.state = state 1091 | self.result = state.result 1092 | self.element = element 1093 | self.target = element._target or element:_get_default_target() 1094 | self.action, self.result[self.target] = element:_get_action() 1095 | return self 1096 | end 1097 | 1098 | function ElementState:error(fmt, ...) 1099 | self.state:error(fmt, ...) 1100 | end 1101 | 1102 | function ElementState:convert(argument, index) 1103 | local converter = self.element._convert 1104 | 1105 | if converter then 1106 | local ok, err 1107 | 1108 | if type(converter) == "function" then 1109 | ok, err = converter(argument) 1110 | elseif type(converter[index]) == "function" then 1111 | ok, err = converter[index](argument) 1112 | else 1113 | ok = converter[argument] 1114 | end 1115 | 1116 | if ok == nil then 1117 | self:error(err and "%s" or "malformed argument '%s'", err or argument) 1118 | end 1119 | 1120 | argument = ok 1121 | end 1122 | 1123 | return argument 1124 | end 1125 | 1126 | function ElementState:default(mode) 1127 | return self.element._defmode:find(mode) and self.element._default 1128 | end 1129 | 1130 | local function bound(noun, min, max, is_max) 1131 | local res = "" 1132 | 1133 | if min ~= max then 1134 | res = "at " .. (is_max and "most" or "least") .. " " 1135 | end 1136 | 1137 | local number = is_max and max or min 1138 | return res .. tostring(number) .. " " .. noun .. (number == 1 and "" or "s") 1139 | end 1140 | 1141 | function ElementState:set_name(alias) 1142 | self.name = ("%s '%s'"):format(alias and "option" or "argument", alias or self.element._name) 1143 | end 1144 | 1145 | function ElementState:invoke() 1146 | self.open = true 1147 | self.overwrite = false 1148 | 1149 | if self.invocations >= self.element._maxcount then 1150 | if self.element._overwrite then 1151 | self.overwrite = true 1152 | else 1153 | local num_times_repr = bound("time", self.element._mincount, self.element._maxcount, true) 1154 | self:error("%s must be used %s", self.name, num_times_repr) 1155 | end 1156 | else 1157 | self.invocations = self.invocations + 1 1158 | end 1159 | 1160 | self.args = {} 1161 | 1162 | if self.element._maxargs <= 0 then 1163 | self:close() 1164 | end 1165 | 1166 | return self.open 1167 | end 1168 | 1169 | function ElementState:pass(argument) 1170 | argument = self:convert(argument, #self.args + 1) 1171 | table.insert(self.args, argument) 1172 | 1173 | if #self.args >= self.element._maxargs then 1174 | self:close() 1175 | end 1176 | 1177 | return self.open 1178 | end 1179 | 1180 | function ElementState:complete_invocation() 1181 | while #self.args < self.element._minargs do 1182 | self:pass(self.element._default) 1183 | end 1184 | end 1185 | 1186 | function ElementState:close() 1187 | if self.open then 1188 | self.open = false 1189 | 1190 | if #self.args < self.element._minargs then 1191 | if self:default("a") then 1192 | self:complete_invocation() 1193 | else 1194 | if #self.args == 0 then 1195 | if getmetatable(self.element) == Argument then 1196 | self:error("missing %s", self.name) 1197 | elseif self.element._maxargs == 1 then 1198 | self:error("%s requires an argument", self.name) 1199 | end 1200 | end 1201 | 1202 | self:error("%s requires %s", self.name, bound("argument", self.element._minargs, self.element._maxargs)) 1203 | end 1204 | end 1205 | 1206 | local args 1207 | 1208 | if self.element._maxargs == 0 then 1209 | args = self.args[1] 1210 | elseif self.element._maxargs == 1 then 1211 | if self.element._minargs == 0 and self.element._mincount ~= self.element._maxcount then 1212 | args = self.args 1213 | else 1214 | args = self.args[1] 1215 | end 1216 | else 1217 | args = self.args 1218 | end 1219 | 1220 | self.action(self.result, self.target, args, self.overwrite) 1221 | end 1222 | end 1223 | 1224 | local ParseState = class({ 1225 | result = {}, 1226 | options = {}, 1227 | arguments = {}, 1228 | argument_i = 1, 1229 | element_to_mutexes = {}, 1230 | mutex_to_element_state = {}, 1231 | command_actions = {} 1232 | }) 1233 | 1234 | function ParseState:__call(parser, error_handler) 1235 | self.parser = parser 1236 | self.error_handler = error_handler 1237 | self.charset = parser:_update_charset() 1238 | self:switch(parser) 1239 | return self 1240 | end 1241 | 1242 | function ParseState:error(fmt, ...) 1243 | self.error_handler(self.parser, fmt:format(...)) 1244 | end 1245 | 1246 | function ParseState:switch(parser) 1247 | self.parser = parser 1248 | 1249 | if parser._action then 1250 | table.insert(self.command_actions, {action = parser._action, name = parser._name}) 1251 | end 1252 | 1253 | for _, option in ipairs(parser._options) do 1254 | option = ElementState(self, option) 1255 | table.insert(self.options, option) 1256 | 1257 | for _, alias in ipairs(option.element._aliases) do 1258 | self.options[alias] = option 1259 | end 1260 | end 1261 | 1262 | for _, mutex in ipairs(parser._mutexes) do 1263 | for _, element in ipairs(mutex) do 1264 | if not self.element_to_mutexes[element] then 1265 | self.element_to_mutexes[element] = {} 1266 | end 1267 | 1268 | table.insert(self.element_to_mutexes[element], mutex) 1269 | end 1270 | end 1271 | 1272 | for _, argument in ipairs(parser._arguments) do 1273 | argument = ElementState(self, argument) 1274 | table.insert(self.arguments, argument) 1275 | argument:set_name() 1276 | argument:invoke() 1277 | end 1278 | 1279 | self.handle_options = parser._handle_options 1280 | self.argument = self.arguments[self.argument_i] 1281 | self.commands = parser._commands 1282 | 1283 | for _, command in ipairs(self.commands) do 1284 | for _, alias in ipairs(command._aliases) do 1285 | self.commands[alias] = command 1286 | end 1287 | end 1288 | end 1289 | 1290 | function ParseState:get_option(name) 1291 | local option = self.options[name] 1292 | 1293 | if not option then 1294 | self:error("unknown option '%s'%s", name, get_tip(self.options, name)) 1295 | else 1296 | return option 1297 | end 1298 | end 1299 | 1300 | function ParseState:get_command(name) 1301 | local command = self.commands[name] 1302 | 1303 | if not command then 1304 | if #self.commands > 0 then 1305 | self:error("unknown command '%s'%s", name, get_tip(self.commands, name)) 1306 | else 1307 | self:error("too many arguments") 1308 | end 1309 | else 1310 | return command 1311 | end 1312 | end 1313 | 1314 | function ParseState:check_mutexes(element_state) 1315 | if self.element_to_mutexes[element_state.element] then 1316 | for _, mutex in ipairs(self.element_to_mutexes[element_state.element]) do 1317 | local used_element_state = self.mutex_to_element_state[mutex] 1318 | 1319 | if used_element_state and used_element_state ~= element_state then 1320 | self:error("%s can not be used together with %s", element_state.name, used_element_state.name) 1321 | else 1322 | self.mutex_to_element_state[mutex] = element_state 1323 | end 1324 | end 1325 | end 1326 | end 1327 | 1328 | function ParseState:invoke(option, name) 1329 | self:close() 1330 | option:set_name(name) 1331 | self:check_mutexes(option, name) 1332 | 1333 | if option:invoke() then 1334 | self.option = option 1335 | end 1336 | end 1337 | 1338 | function ParseState:pass(arg) 1339 | if self.option then 1340 | if not self.option:pass(arg) then 1341 | self.option = nil 1342 | end 1343 | elseif self.argument then 1344 | self:check_mutexes(self.argument) 1345 | 1346 | if not self.argument:pass(arg) then 1347 | self.argument_i = self.argument_i + 1 1348 | self.argument = self.arguments[self.argument_i] 1349 | end 1350 | else 1351 | local command = self:get_command(arg) 1352 | self.result[command._target or command._name] = true 1353 | 1354 | if self.parser._command_target then 1355 | self.result[self.parser._command_target] = command._name 1356 | end 1357 | 1358 | self:switch(command) 1359 | end 1360 | end 1361 | 1362 | function ParseState:close() 1363 | if self.option then 1364 | self.option:close() 1365 | self.option = nil 1366 | end 1367 | end 1368 | 1369 | function ParseState:finalize() 1370 | self:close() 1371 | 1372 | for i = self.argument_i, #self.arguments do 1373 | local argument = self.arguments[i] 1374 | if #argument.args == 0 and argument:default("u") then 1375 | argument:complete_invocation() 1376 | else 1377 | argument:close() 1378 | end 1379 | end 1380 | 1381 | if self.parser._require_command and #self.commands > 0 then 1382 | self:error("a command is required") 1383 | end 1384 | 1385 | for _, option in ipairs(self.options) do 1386 | option.name = option.name or ("option '%s'"):format(option.element._name) 1387 | 1388 | if option.invocations == 0 then 1389 | if option:default("u") then 1390 | option:invoke() 1391 | option:complete_invocation() 1392 | option:close() 1393 | end 1394 | end 1395 | 1396 | local mincount = option.element._mincount 1397 | 1398 | if option.invocations < mincount then 1399 | if option:default("a") then 1400 | while option.invocations < mincount do 1401 | option:invoke() 1402 | option:close() 1403 | end 1404 | elseif option.invocations == 0 then 1405 | self:error("missing %s", option.name) 1406 | else 1407 | self:error("%s must be used %s", option.name, bound("time", mincount, option.element._maxcount)) 1408 | end 1409 | end 1410 | end 1411 | 1412 | for i = #self.command_actions, 1, -1 do 1413 | self.command_actions[i].action(self.result, self.command_actions[i].name) 1414 | end 1415 | end 1416 | 1417 | function ParseState:parse(args) 1418 | for _, arg in ipairs(args) do 1419 | local plain = true 1420 | 1421 | if self.handle_options then 1422 | local first = arg:sub(1, 1) 1423 | 1424 | if self.charset[first] then 1425 | if #arg > 1 then 1426 | plain = false 1427 | 1428 | if arg:sub(2, 2) == first then 1429 | if #arg == 2 then 1430 | if self.options[arg] then 1431 | local option = self:get_option(arg) 1432 | self:invoke(option, arg) 1433 | else 1434 | self:close() 1435 | end 1436 | 1437 | self.handle_options = false 1438 | else 1439 | local equals = arg:find "=" 1440 | if equals then 1441 | local name = arg:sub(1, equals - 1) 1442 | local option = self:get_option(name) 1443 | 1444 | if option.element._maxargs <= 0 then 1445 | self:error("option '%s' does not take arguments", name) 1446 | end 1447 | 1448 | self:invoke(option, name) 1449 | self:pass(arg:sub(equals + 1)) 1450 | else 1451 | local option = self:get_option(arg) 1452 | self:invoke(option, arg) 1453 | end 1454 | end 1455 | else 1456 | for i = 2, #arg do 1457 | local name = first .. arg:sub(i, i) 1458 | local option = self:get_option(name) 1459 | self:invoke(option, name) 1460 | 1461 | if i ~= #arg and option.element._maxargs > 0 then 1462 | self:pass(arg:sub(i + 1)) 1463 | break 1464 | end 1465 | end 1466 | end 1467 | end 1468 | end 1469 | end 1470 | 1471 | if plain then 1472 | self:pass(arg) 1473 | end 1474 | end 1475 | 1476 | self:finalize() 1477 | return self.result 1478 | end 1479 | 1480 | function Parser:error(msg) 1481 | io.stderr:write(("%s\n\nError: %s\n"):format(self:get_usage(), msg)) 1482 | os.exit(1) 1483 | end 1484 | 1485 | -- Compatibility with strict.lua and other checkers: 1486 | local default_cmdline = rawget(_G, "arg") or {} 1487 | 1488 | function Parser:_parse(args, error_handler) 1489 | return ParseState(self, error_handler):parse(args or default_cmdline) 1490 | end 1491 | 1492 | function Parser:parse(args) 1493 | return self:_parse(args, self.error) 1494 | end 1495 | 1496 | local function xpcall_error_handler(err) 1497 | return tostring(err) .. "\noriginal " .. debug.traceback("", 2):sub(2) 1498 | end 1499 | 1500 | function Parser:pparse(args) 1501 | local parse_error 1502 | 1503 | local ok, result = xpcall(function() 1504 | return self:_parse(args, function(_, err) 1505 | parse_error = err 1506 | error(err, 0) 1507 | end) 1508 | end, xpcall_error_handler) 1509 | 1510 | if ok then 1511 | return true, result 1512 | elseif not parse_error then 1513 | error(result, 0) 1514 | else 1515 | return false, parse_error 1516 | end 1517 | end 1518 | 1519 | local argparse = {} 1520 | 1521 | argparse.version = "0.6.0" 1522 | 1523 | setmetatable(argparse, {__call = function(_, ...) 1524 | return Parser(default_cmdline[0]):add_help(true)(...) 1525 | end}) 1526 | 1527 | return argparse 1528 | --------------------------------------------------------------------------------