├── .busted ├── .github └── workflows │ ├── busted.yml │ ├── deploy.yml │ └── luacheck.yml ├── .gitignore ├── .luacheckrc ├── LICENSE ├── README.md ├── UPGRADE.md ├── bin ├── coverage ├── docs ├── lint ├── release └── watch-tests.sh ├── doc ├── package.json └── tinydoc.conf.js ├── examples ├── 00_general.lua ├── 01_multiple_options.lua ├── 02_parse_callbacks.lua ├── 03_config_file.lua ├── 04_commands--git-log.lua └── 04_commands--git.lua ├── lua_cliargs-dev-1.rockspec ├── rockspecs ├── lua_cliargs-3.0-2.rockspec └── lua_cliargs-3.0.2-1.rockspec ├── spec ├── cliargs_parsing_spec.lua ├── cliargs_spec.lua ├── config_loader_spec.lua ├── core_spec.lua ├── features │ ├── argument_spec.lua │ ├── command_spec.lua │ ├── flag_spec.lua │ ├── integration_spec.lua │ ├── option_spec.lua │ └── splatarg_spec.lua ├── fixtures │ ├── config.ini │ ├── config.json │ ├── config.lua │ ├── config.yml │ └── test-command.lua ├── printer_spec.lua ├── spec_helper.lua └── utils │ ├── disect_argument_spec.lua │ ├── disect_spec.lua │ ├── split_spec.lua │ └── wordwrap_spec.lua └── src ├── cliargs.lua └── cliargs ├── config_loader.lua ├── constants.lua ├── core.lua ├── parser.lua ├── printer.lua └── utils ├── disect.lua ├── disect_argument.lua ├── filter.lua ├── lookup.lua ├── shallow_copy.lua ├── split.lua ├── trim.lua └── wordwrap.lua /.busted: -------------------------------------------------------------------------------- 1 | return { 2 | _all = { 3 | coverage = false 4 | }, 5 | default = { 6 | coverage = false, 7 | verbose = true, 8 | ROOT = {"spec"}, 9 | lpath = "src/?.lua;src/cliargs/?.lua;spec/?.lua;" 10 | } 11 | } -------------------------------------------------------------------------------- /.github/workflows/busted.yml: -------------------------------------------------------------------------------- 1 | name: Busted 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | 7 | busted: 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | luaVersion: [ "5.4", "5.3", "5.2", "5.1", "luajit" ] # , "luajit-openresty" 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup ‘lua’ 18 | uses: leafo/gh-actions-lua@v10 19 | with: 20 | luaVersion: ${{ matrix.luaVersion }} 21 | 22 | - name: Setup ‘luarocks’ 23 | uses: leafo/gh-actions-luarocks@v4 24 | 25 | - name: Setup test dependencies 26 | run: | 27 | luarocks install --deps-only lua_cliargs-dev-1.rockspec 28 | luarocks install busted 29 | luarocks install dkjson 30 | luarocks install inifile 31 | ${{ matrix.luaVersion != '5.4' && 'luarocks install yaml' || '' }} # https://github.com/lubyk/yaml/issues/7 32 | 33 | - name: Replace system cliargs with self 34 | run: | 35 | luarocks remove --force lua_cliargs 36 | luarocks make 37 | 38 | - name: Run regression tests 39 | # disable project-local path prefixes to force use of system installation 40 | run: busted -v --lpath="" --cpath="" -Xoutput --color 41 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: [ push, workflow_dispatch ] 4 | 5 | jobs: 6 | 7 | affected: 8 | uses: lunarmodules/.github/.github/workflows/list_affected_rockspecs.yml@main 9 | 10 | build: 11 | needs: affected 12 | if: ${{ needs.affected.outputs.rockspecs }} 13 | uses: lunarmodules/.github/.github/workflows/test_build_rock.yml@main 14 | with: 15 | rockspecs: ${{ needs.affected.outputs.rockspecs }} 16 | 17 | upload: 18 | needs: [ affected, build ] 19 | # Only run upload if: 20 | # 1. We are on the canonical repository (no uploads from forks) 21 | # 2. The current commit is either tagged or on the default branch (the workflow will upload dev/scm rockspecs any 22 | # time they are touched, tagged ones whenever the edited rockspec and tag match) 23 | # 3. Some rockspecs were changed — this implies the commit changing the rockspec is the same one that gets tagged 24 | if: >- 25 | ${{ 26 | github.repository == 'lunarmodules/lua_cliargs' && 27 | ( github.ref_name == 'master' || startsWith(github.ref, 'refs/tags/') ) && 28 | needs.affected.outputs.rockspecs 29 | }} 30 | uses: lunarmodules/.github/.github/workflows/upload_to_luarocks.yml@main 31 | with: 32 | rockspecs: ${{ needs.affected.outputs.rockspecs }} 33 | secrets: 34 | apikey: ${{ secrets.LUAROCKS_APIKEY }} 35 | -------------------------------------------------------------------------------- /.github/workflows/luacheck.yml: -------------------------------------------------------------------------------- 1 | name: Luacheck 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | luacheck: 8 | runs-on: ubuntu-22.04 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | - name: Luacheck 13 | uses: lunarmodules/luacheck@v1 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /luacov.report.out 2 | /luacov.stats.out 3 | /.env.local 4 | /*.rock 5 | .swp 6 | /.luacheckcache 7 | /doc/compiled 8 | /doc/node_modules -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "min" 2 | cache = false 3 | 4 | files["spec"] = { 5 | std = "+busted" 6 | } 7 | 8 | ignore = { "211/_" } 9 | exclude_files = { "examples" } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2015 Ahmad Amireh 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua_cliargs 2 | 3 | [![Luacheck](https://img.shields.io/github/actions/workflow/status/lunarmodules/lua_cliargs/luacheck.yml?branch=master&label=Luacheck&logo=Lua)](https://github.com/lunarmodules/lua_cliargs/actions?workflow=Luacheck) 4 | [![Busted](https://img.shields.io/github/actions/workflow/status/lunarmodules/lua_cliargs/busted.yml?branch=master&label=Busted&logo=Lua)](https://github.com/lunarmodules/lua_cliargs/actions?workflow=Busted) 5 | 6 | cliargs is a command-line argument parser for Lua. It supports several types of arguments: 7 | 8 | 1. required arguments 9 | 2. optional arguments with different notations: `-short-key VALUE` and/or `--expanded-key=VALUE` 10 | 3. optional arguments with multiple-values that get appended to a list 11 | 4. optional "flag" arguments (on/off options) with notations: `-short-key` and/or `--expanded-key` 12 | 5. a single optional "splat" argument which can be repeated (must be the last argument) 13 | 14 | Optional arguments can have default values (strings), flags always default to 'true'. 15 | 16 | ## Usage Examples 17 | 18 | See the examples under the `examples/` directory. 19 | 20 | ## API 21 | 22 | See http://lua-cliargs.netlify.com/ for the API docs. 23 | 24 | ## Help listings `--help` 25 | 26 | A help listing will be automatically generated and accessed using the `--help` argument. When such an option is encountered, `cli:parse()` will abort and return `nil, string` with the help message; you are free to print it to the screen using `print()` if you want. 27 | 28 | You can also force its display in the code using `cli:print_help()`. 29 | 30 | This is the result for our example (see `examples/00_general.lua`): 31 | 32 | ``` 33 | Usage: cli_example.lua [OPTIONS] INPUT [OUTPUT-1 [OUTPUT-2 [...]]] 34 | 35 | ARGUMENTS: 36 | INPUT path to the input file (required) 37 | OUTPUT multiple output paths (optional, default: 38 | /dev/stdout) 39 | 40 | OPTIONS: 41 | -c, --compress=FILTER the filter to use for compressing output: gzip, 42 | lzma, bzip2, or none (default: gzip) 43 | -o FILE path to output file (default: /dev/stdout) 44 | -d script will run in DEBUG mode 45 | -v, --version prints the program's version and exits 46 | --verbose the script output will be very verbose 47 | ``` 48 | 49 | ## Validations 50 | 51 | ### Runtime argument validation 52 | 53 | From a parsing point of view, there are 3 cases that need to be handled which are outlined below. If I missed something, please open a ticket! 54 | 55 | **Missing a required argument** 56 | 57 | ``` 58 | $ lua examples/00_general.lua 59 | cli_example.lua: error: bad number of arguments; 1-4 argument(s) must be specified, not 0; re-run with --help for usage. 60 | ``` 61 | 62 | **Missing value for an optional argument** 63 | 64 | ``` 65 | $ lua examples/00_general.lua --compress inputfile 66 | cli_example.lua: error: option --compress requires a value to be set; re-run with --help for usage. 67 | ``` 68 | 69 | **Unknown arguments** 70 | 71 | ``` 72 | $ lua examples/00_general.lua -f inputfile 73 | cli_example.lua: error: unknown/bad flag; -f; re-run with --help for usage. 74 | ``` 75 | 76 | ### Some sanity guards 77 | 78 | In the following cases, `cliargs` will report an error to you and terminate the running script: 79 | 80 | 1. flag options can not accept a value. For example: `cli:add_flag('-v VERSION')` will return an error 81 | 2. duplicate keys are not allowed: defining two options with the key `--input` will be rejected 82 | 83 | ## Tests 84 | 85 | Running test specs is done using [busted](http://olivinelabs.com/busted/). You can install it using [LuaRocks](http://www.luarocks.org/), and then just call it with the `spec` folder: 86 | 87 | ``` 88 | luarocks install busted 89 | cd /path/to/lua_cliargs/ 90 | busted spec 91 | ``` 92 | 93 | ## Contributions 94 | 95 | If you come across a bug and you'd like to patch it, please fork the repository, commit your patch, and request a pull. 96 | 97 | ## Thanks to 98 | 99 | Many thanks to everyone who reported bugs, provided fixes, and added entirely new features: 100 | 101 | 1. [Thijs Schreijer](https://github.com/Tieske) 102 | 1. [Jack Lawson](https://github.com/ajacksified) 103 | 1. [Robert Andrew Ditthardt](https://github.com/DorianGray) 104 | 1. [Oscar Lim](https://github.com/o-lim) 105 | 106 | *If I missed you, don't hesitate to update this file or just email me.* 107 | 108 | ## Changelog 109 | 110 | ### 3.0-2 111 | 112 | - optimized an internal routine responsible for word-wrapping. Thanks to 113 | @Tieske, refs GH-47 114 | 115 | ### Changes from 2.5.x 3.0 116 | 117 | This major version release contains BREAKING API CHANGES. See the UPGRADE guide for help in updating your code to make use of it. 118 | 119 | **More flexible parsing** 120 | 121 | - options can occur anywhere now even after arguments (unless the `--` indicator is specified, then no options are parsed afterwards.) Previously, options were accepted only before arguments. 122 | - options using the short-key notation can be specified using `=` as a value delimiter as well as a space (e.g. `-c=lzma` and `-c lzma`) 123 | - the library is now more flexible with option definitions (notations like `-key VALUE`, `--key=VALUE`, `-k=VALUE` are all treated equally) 124 | - `--help` or `-h` will now cause the help listing to be displayed no matter where they are. Previously, this only happened if they were supplied as the first option. 125 | 126 | **Basic command support** 127 | 128 | You may now define commands with custom handlers. A command may be invoked by supplying its name as the first argument (options can still come before or afterwards). lua_cliargs will forward the rest of the options to that command to handle, which can be in a separate file. 129 | 130 | See `examples/04_commands--git.lua` for an example. 131 | 132 | **Re-defining defaults** 133 | 134 | It is now possible to pass a table containing default values (and override any 135 | existing defaults). 136 | 137 | The function for doing this is called `cli:load_defaults().`. 138 | 139 | This makes it possible to load run-time defaults from a configuration file, for example. 140 | 141 | **Reading configuration files** 142 | 143 | `cliargs` now exposes some convenience helpers for loading configuration from files (and a separate hook, `cli:load_defaults()` to inject this config if you want) found in `cli:read_defaults()`. This method takes a file-path and an optional file format and it will parse it for you, provided you have the necessary libraries installed. 144 | 145 | See the API docs for using this hook. 146 | 147 | **Other changes** 148 | 149 | - internal code changes and more comprehensive test-coverage 150 | 151 | ### Changes from 2.5.1 to 2.5.2 152 | 153 | - No longer tracking the (legacy) tarballs in git or the luarocks package. Instead, we use the GitHub release tarballs for each version. 154 | 155 | ### Changes in 2.4.0 from 2.3-4 156 | 157 | 1. All arguments now accept a callback that will be invoked when parsing of those arguments was successful 158 | 2. (**POSSIBLY BREAKING**) Default value for flags is now `nil` instead of `false`. This will only affect existing behavior if you were explicitly testing unset flags to equal `false` (i.e. `if flag == false then`) as opposed to `if flag then` (or `if not flag then`). 159 | 3. Minor bugfixes 160 | 161 | ### Changes in 2.3.0 162 | 163 | 1. the parser will now understand `--` to denote the end of optional arguments and will map whatever comes after it to required/splat args 164 | 2. `-short VALUE` is now properly supported, so is `-short=VALUE` 165 | 3. short-key options can now officially be composed of more than 1 character 166 | 4. the parser now accepts callbacks that will be invoked as soon as options are parsed so that you can bail out of parsing preemptively (like for `--version` or `--help` options) 167 | 5. options can now accept multiple values via multiple invocations if a table was provided as a default value (passed-in values will be appended to that list) 168 | 169 | ### Changes in 2.2-0 from 2.1-2 170 | 171 | 1. the `=` that separates keys from values in the `--expanded-key` notation is no longer mandatory; using either a space or a `=` will map the value to the key (e.g., `--compress lzma` is equal to `--compress=lzma`) 172 | 173 | ### Changes in 2.0.0 from 1.x.x 174 | 175 | 1. added the 'splat' argument, an optional repetitive argument for which a maximum number of occurrences can be set 176 | 1. removed the reference, arguments are now solely returned by their key/expanded-key (BREAKING!) 177 | 1. removed object overhead and the `new()` method as the library will only be used once on program start-up (BREAKING!) 178 | 1. after parsing completed successfully, the library will effectively delete itself to free resources (BREAKING!) 179 | 1. option/flag is now allowed with only an expanded-key defined 180 | 1. Debug aid implemented; adding a first option `--__DUMP__`, will dump the results of parsing the command line. Especially for testing how to use the commandline with arguments containing spaces either quoted or not. 181 | 1. the `print_usage()` and `print_help()` now have a 'noprint' parameter that will not print the message, but return it as an error string (`nil + errmsg`) 182 | 183 | ## License 184 | 185 | The code is released under the MIT terms. Feel free to use it in both open and closed software as you please. 186 | 187 | Copyright (c) 2011-2015 Ahmad Amireh 188 | 189 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 190 | 191 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 192 | 193 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 194 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | ## Upgrading from 2.x to 3.0 2 | 3 | - `cli._VERSION` has been renamed to `cli.VERSION` 4 | 5 | **Function renames** 6 | 7 | The functions for defining arguments of all types have been renamed to drop the 8 | `_add` prefix from their names. This affects the following functions: 9 | 10 | - `cli:add_argument` has been renamed to `cli:argument` 11 | - `cli:add_option` has been renamed to `cli:option` 12 | - `cli:add_flag` has been renamed to `cli:flag` 13 | - `cli:optarg` has been renamed to `cli:splat` 14 | 15 | **Function alias removals** 16 | 17 | - `cli:add_opt` has been removed. Use `cli:option` instead 18 | - `cli:add_arg` has been removed. Use `cli:argument` instead 19 | - `cli:parse_args` has been removed. Use `cli:parse` instead 20 | 21 | **`cli:parse()` invocation changes** 22 | 23 | `cli:parse()` no longer accepts the auxiliary arguments `noprint` and `dump` as the second and third arguments; only one argument is now accepted and that is a custom arguments table. If left unspecified, we use the global `_G['arg']` program argument table as usual. 24 | 25 | So, the new signature is: 26 | 27 | `cli:parse(args: table) -> table` 28 | 29 | - to make the parser silent, use `cli:set_silent(true)` before invoking the parser 30 | - to generate the internal state dump, a runtime argument `--__DUMP__` must be passed as the first argument 31 | 32 | **Private function are now hidden** 33 | 34 | Hopefully you weren't relying on any of these because they are no longer exposed, and they weren't documented. The affected previous exports are: 35 | 36 | - `cli:__lookup()` 37 | - `cli:__add_opt()` 38 | -------------------------------------------------------------------------------- /bin/coverage: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run busted and collect spec coverage using LuaCov. 4 | # Output will be found at /luacov.report.out. 5 | # 6 | # Usage: 7 | # 8 | # $ ./bin/coverage 9 | # $ cat luacov.report.out 10 | 11 | which luacov &> /dev/null 12 | 13 | if [ $? -ne 0 ]; then 14 | echo "You must have luacov installed." 15 | echo "Run 'luarocks install luacov' then try again". 16 | exit 1 17 | fi 18 | 19 | which busted &> /dev/null 20 | 21 | if [ $? -ne 0 ]; then 22 | echo "You must have luacov installed." 23 | echo "Run 'luarocks install busted' then try again". 24 | exit 1 25 | fi 26 | 27 | busted -c 28 | luacov src/ 29 | rm luacov.stats.out 30 | grep -zPo "(?s)={10,}\nSummary\n={10,}.+" luacov.report.out -------------------------------------------------------------------------------- /bin/docs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ ! -d "doc" ]; then 4 | echo "Must be run from lua_cliargs root" 5 | exit 1 6 | fi 7 | 8 | which npm &> /dev/null 9 | 10 | if [ $? -ne 0 ]; then 11 | echo "You must have npm (node.js) installed." 12 | exit 1 13 | fi 14 | 15 | cd doc 16 | 17 | npm install 18 | 19 | ./node_modules/tinydoc/cli/tinydoc run 20 | 21 | echo "Docs compiled successfully. Open doc/compiled/index.html in a browser." -------------------------------------------------------------------------------- /bin/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | which luacheck &> /dev/null 4 | 5 | if [ $? -ne 0 ]; then 6 | echo "You must have luacheck installed." 7 | echo "Run 'luarocks install luacheck' then try again". 8 | exit 1 9 | fi 10 | 11 | if [ ! -d src ]; then 12 | echo "You must run $0 from lua_cliargs root." 13 | exit 1 14 | fi 15 | 16 | luacheck --codes . $@ -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -f ".env" ] && source ".env" 4 | [ -f ".env.local" ] && source ".env.local" 5 | 6 | function abort { 7 | echo -e "\e[00;31m[ FAILED ]\e[00m ${1}" 8 | exit 1 9 | } 10 | 11 | function confirm { 12 | echo "${1} [y/N]" 13 | read confirmation 14 | 15 | if [ "${confirmation}" != "y" ]; then 16 | exit 0 17 | fi 18 | } 19 | 20 | [ -z "${GITHUB_TOKEN}" ] && abort "Missing GITHUB_TOKEN env variable." 21 | [ -z "${GITHUB_USER}" ] && abort "Missing GITHUB_USER env variable." 22 | [ -z "${GITHUB_REPO}" ] && abort "Missing GITHUB_REPO env variable." 23 | [ -z "${LUAROCKS_API_KEY}" ] && abort "Missing LUAROCKS_API_KEY env variable." 24 | 25 | VERSION=$1 26 | SRC_FILE="src/cliargs.lua" 27 | SRC_VERSION=$(grep "VERSION" src/cliargs.lua | sed -e 's/.*=//' -e 's/.* //' -e 's/"//g') 28 | 29 | NEW_ROCKSPEC="lua_cliargs-${VERSION}.rockspec" 30 | OLD_ROCKSPEC="lua_cliargs-${SRC_VERSION}.rockspec" 31 | 32 | if [ "${VERSION}" == "${SRC_VERSION}" ]; then 33 | abort "Version specified is the same as the current one in rockspec" 34 | fi 35 | 36 | # Publish to GitHub 37 | JSON_PAYLOAD=$( 38 | printf '{ 39 | "tag_name": "v%s", 40 | "target_commitish": "master", 41 | "name": "v%s", 42 | "body": "Release of version %s", 43 | "draft": false, 44 | "prerelease": false 45 | }' $VERSION $VERSION $VERSION 46 | ) 47 | 48 | echo $JSON_PAYLOAD 49 | echo "Releasing version ${VERSION}..." 50 | 51 | if [ ! -f $OLD_ROCKSPEC ]; then 52 | abort "Version in ${SRC_FILE} does not match the rockspec file!" 53 | fi 54 | 55 | # rename rockspec file 56 | mv $OLD_ROCKSPEC $NEW_ROCKSPEC 57 | 58 | # bump version in rockspec 59 | perl -p -i -e "s/${SRC_VERSION}/${VERSION}/g" $NEW_ROCKSPEC 60 | 61 | # bump version in src 62 | perl -p -i -e "s/${SRC_VERSION}/${VERSION}/" $SRC_FILE 63 | 64 | confirm "rockspec and source file have been modified, please confirm the changes. Proceed?" 65 | 66 | echo "Creating git release v${VERSION}..." 67 | 68 | git add $NEW_ROCKSPEC 69 | git rm $OLD_ROCKSPEC 70 | git add $SRC_FILE 71 | git commit -m "Release v${VERSION}" 72 | git push origin master 73 | 74 | echo "Done." 75 | confirm "Create a new GitHub release?" 76 | 77 | # the API will automatically create the tag for us, no need to do it manually! 78 | curl \ 79 | --data "$JSON_PAYLOAD" \ 80 | -X POST \ 81 | -H "Content-Type: application/json; charset=utf-8" \ 82 | -H "Accept: application/json" \ 83 | "https://api.github.com/repos/${GITHUB_USER}/${GITHUB_REPO}/releases?access_token=${GITHUB_TOKEN}" 84 | 85 | echo "Done." 86 | 87 | confirm "Publish to luarocks?" 88 | 89 | luarocks --api-key=$LUAROCKS_API_KEY upload $NEW_ROCKSPEC 90 | 91 | echo -e "\e[00;32m[ SUCCESS ]\e[00m" 92 | -------------------------------------------------------------------------------- /bin/watch-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Watches spec files and re-runs the busted suite when a spec or source file 4 | # changes. 5 | # 6 | # Usage: 7 | # 8 | # $0 [--focus] 9 | # 10 | # If --focus is passed, only the last spec file that has changed will be run 11 | # when a _source_ file changes. Otherwise, all specs will run on source changes. 12 | # 13 | # Requires inotify-tools[1]. 14 | # 15 | # [1] http://linux.die.net/man/1/inotifywait 16 | 17 | if [ ! -d spec ]; then 18 | echo "Must be run from lua_cliargs root." 19 | exit 1 20 | fi 21 | 22 | LAST_FILE="spec/" 23 | 24 | inotifywait -rm --format '%w %f' -e close_write -e create src/ spec/ | while read dir file; do 25 | FILEPATH="${dir}${file}" 26 | 27 | if [[ $FILEPATH =~ src\/ ]]; then 28 | busted $@ "${LAST_FILE}" 29 | else 30 | if [[ $1 =~ "focus" ]]; then 31 | LAST_FILE=$FILEPATH 32 | fi 33 | 34 | busted $@ "${FILEPATH}" 35 | fi 36 | done -------------------------------------------------------------------------------- /doc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lua_cliargs_docs", 3 | "version": "1.0.0", 4 | "description": "docs for lua_cliargs", 5 | "main": "tinydoc.conf.js", 6 | "private": true, 7 | "dependencies": { 8 | "tinydoc-plugin-lua": "1.0.x", 9 | "tinydoc-theme-gitbooks": "1.0.x", 10 | "tinydoc": "3.2.x" 11 | }, 12 | "devDependencies": {}, 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "author": "Ahmad Amireh", 17 | "license": "MIT" 18 | } 19 | -------------------------------------------------------------------------------- /doc/tinydoc.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | exports.assetRoot = path.resolve(__dirname, '..'); 4 | exports.outputDir = path.resolve(__dirname, 'compiled'); 5 | exports.layout = 'single-page'; 6 | exports.title = 'lua_cliargs'; 7 | exports.scrollSpying = true; 8 | exports.resizableSidebar = false; 9 | exports.collapsibleSidebar = true; 10 | 11 | exports.plugins = [ 12 | require('tinydoc-plugin-lua')({ 13 | source: 'src/**/*.lua' 14 | }), 15 | 16 | require('tinydoc-theme-gitbooks')() 17 | ]; 18 | -------------------------------------------------------------------------------- /examples/00_general.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Try this file with the following commands lines; 4 | example.lua --help 5 | example.lua -o myfile -d --compress=gzip inputfile 6 | example.lua --__DUMP__ -o myfile -d --compress=gzip inputfile 7 | 8 | --]] 9 | 10 | local cli = require "cliargs" 11 | 12 | -- this is called when the flag -v or --version is set 13 | local function print_version() 14 | print("cli_example.lua: version 1.2.1") 15 | print("lua_cliargs: version " .. cli.VERSION) 16 | os.exit(0) 17 | end 18 | 19 | cli:set_name("cli_example.lua") 20 | 21 | -- Required arguments: 22 | cli:argument("OUTPUT", "path to the output file") 23 | 24 | -- Optional (repetitive) arguments 25 | -- only the last argument can be optional. Being set to maximum 3 optionals. 26 | cli:splat("INPUTS", "the source files to read from", "/tmp/foo", 3) 27 | 28 | -- Optional parameters: 29 | cli:option("-c, --compress=FILTER", "the filter to use for compressing output: gzip, lzma, bzip2, or none", "gzip") 30 | -- cli:option("-o FILE", "path to output file", "/dev/stdout") 31 | 32 | -- Flags: a flag is a boolean option. Defaults to false 33 | -- A flag with short-key notation only 34 | cli:flag("-d", "script will run in DEBUG mode") 35 | -- A flag with both the short-key and --expanded-key notations, and callback function 36 | cli:flag("-v, --version", "prints the program's version and exits", print_version) 37 | -- A flag with --expanded-key notation only 38 | cli:flag("--verbose", "the script output will be very verbose") 39 | -- A flag that can be negated using --no- as a prefix, but you'll still have 40 | -- to access its value without that prefix. See below for an example. 41 | cli:flag('--[no-]ice-cream', 'ice cream, or not', true) 42 | 43 | -- Parses from _G['arg'] 44 | local args, err = cli:parse(arg) 45 | 46 | if not args and err then 47 | -- something wrong happened and an error was printed 48 | print(string.format('%s: %s; re-run with help for usage', cli.name, err)) 49 | os.exit(1) 50 | elseif not args['ice-cream'] then 51 | print('kernel panic: NO ICE CREAM?!11') 52 | os.exit(1000) 53 | end 54 | 55 | -- argument parsing was successful, arguments can be found in `args` 56 | -- upon successful parsing cliargs will delete itslef to free resources 57 | -- for k,item in pairs(args) do print(k .. " => " .. tostring(item)) end 58 | 59 | print("Output file: " .. args["OUTPUT"]) 60 | 61 | print("Input files:") 62 | 63 | for i, out in ipairs(args.INPUTS) do 64 | print(" " .. i .. ". " .. out) 65 | end 66 | 67 | print(args.c) 68 | if not args['c'] or args['c'] == 'none' then 69 | print("Won't be compressing") 70 | else 71 | print("Compressing using " .. args['c']) 72 | end 73 | 74 | if args['ice-cream'] then 75 | print('And, one ice cream for you.') 76 | end -------------------------------------------------------------------------------- /examples/01_multiple_options.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | This example shows how to use multiple-value options that get appended into a 3 | list. 4 | 5 | Try this file with the following invocations; 6 | 7 | multiple_options.lua --help 8 | 9 | multiple_options.lua \ 10 | -i http://www.google.com \ 11 | -i http://www.yahoo.com \ 12 | -j 2 \ 13 | combined.html 14 | --]] 15 | 16 | local cli = require "cliargs" 17 | 18 | cli:set_name("example.lua") 19 | 20 | cli:splat("OUTPUT", "Path to where the combined HTML output should be saved.", "./a.html") 21 | 22 | cli:option("-i URLs...", "A url to download. You can pass in as many as needed", {} --[[ this is the important bit! ]]) 23 | cli:option("-j THREADS", "Concurrency threshold; the higher the number, the more files will be downloaded in parallel.", "2") 24 | 25 | -- Parses from _G['arg'] 26 | local args, err = cli:parse() 27 | 28 | if not args and err then 29 | print(err) 30 | os.exit(1) -- something wrong happened and an error was printed 31 | end 32 | 33 | if #args.i > 0 then 34 | print("Source URLs:") 35 | 36 | for i, url in ipairs(args.i) do 37 | print(" " .. i .. ". " .. url) 38 | end 39 | 40 | print("Downloading ".. #args.i .. " files in " .. tonumber(args.j) .. " threads.") 41 | print("Output will be found at " .. args.OUTPUT) 42 | else 43 | print("No source URLs provided, nothing to do!") 44 | end 45 | -------------------------------------------------------------------------------- /examples/02_parse_callbacks.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Shows how to define a callback to be invoked as soon as an option is parsed. 4 | Callbacks are useful for abruptive options like "--help" or "--version" where 5 | you might want to stop the execution of the parser/program if passed. 6 | 7 | Try this file with the following commands lines; 8 | example.lua --version 9 | example.lua -v 10 | example.lua 11 | --]] 12 | 13 | local cli = require "cliargs" 14 | 15 | -- this is called when the flag -v or --version is set 16 | local function print_version() 17 | print(cli.name .. ": version 1.2.1") 18 | os.exit(0) 19 | end 20 | 21 | cli:set_name("try_my_version.lua") 22 | cli:flag("-v, --version", "prints the program's version and exits", print_version) 23 | 24 | -- Parses from _G['arg'] 25 | local args, err = cli:parse() 26 | 27 | -- something wrong happened, we print the error and exit 28 | if not args then 29 | print(err) 30 | os.exit(1) 31 | end 32 | 33 | -- if we got to this point, it means -v (or --version) were not passed: 34 | print "Why, hi!" 35 | -------------------------------------------------------------------------------- /examples/03_config_file.lua: -------------------------------------------------------------------------------- 1 | local cli = require 'cliargs' 2 | local tablex = require 'pl.tablex' -- we'll need this for merging tables 3 | 4 | cli:option('--config=FILEPATH', 'path to a config file', '.programrc') 5 | cli:flag('--quiet', 'Do not output anything to STDOUT', false) 6 | 7 | -- This example shows how to read default values from a base configuration file 8 | -- and optionally, if the user passes in a custom config file using --config 9 | -- we merge those with the parsed ones. 10 | 11 | local function load_config_file(file_path) 12 | local config = {} 13 | local success = pcall(function() 14 | config = loadfile(file_path)() 15 | end) 16 | 17 | if success then 18 | return config 19 | end 20 | end 21 | 22 | -- first, let's load from a ".programrc" file in the current-working directory 23 | -- if it exists and tell cliargs to use the defaults specified in that file: 24 | local base_config = load_config_file('.programrc') 25 | 26 | if base_config then 27 | cli:load_defaults(base_config) 28 | end 29 | 30 | -- now we parse the options like usual: 31 | local args, err = cli:parse() 32 | 33 | if not args and err then 34 | print(err) 35 | os.exit(1) 36 | end 37 | 38 | -- finally, let's check if the user passed in a config file using --config: 39 | if args.config then 40 | local custom_config = load_config_file(args.config) 41 | 42 | if custom_config then 43 | -- We merge the user defaults with the run-time ones. Note that run-time 44 | -- arguments should always have precedence over config defined in files. 45 | args = tablex.merge({}, custom_config, args, true) 46 | end 47 | end 48 | 49 | -- args is now ready for use: 50 | -- args.quiet will be whatever was set in the following priority: 51 | -- 52 | -- 1. --quiet or --no-quiet on the CLI 53 | -- 2. ["quiet"] in the user config file if --config was present 54 | -- 3. ["quiet"] in the base config file (.programrc) if it existed 55 | print(args.quiet) 56 | -------------------------------------------------------------------------------- /examples/04_commands--git-log.lua: -------------------------------------------------------------------------------- 1 | local cli = require('cliargs') 2 | 3 | cli:set_name('git-log') 4 | cli:set_description('Show commit logs') 5 | 6 | cli:flag('--[no-]follow', 'Continue listing the history of a file beyond renames (works only for a single file).') 7 | 8 | local args, err = cli:parse() 9 | 10 | if not args and err then 11 | print(err) 12 | os.exit(1) 13 | end 14 | 15 | print("git-log: follow?", args.follow) -------------------------------------------------------------------------------- /examples/04_commands--git.lua: -------------------------------------------------------------------------------- 1 | local cli = require('cliargs') 2 | 3 | cli:set_name('git') 4 | cli:set_description('the stupid content tracker') 5 | 6 | cli 7 | :command('diff', 'Show changes between commits, commit and working tree, etc') 8 | :splat('path', 'This form is to view the changes you made relative to the index (staging area for the next commit)', nil, 999) 9 | :flag('-p, --patch', 'This form is to view the changes you made relative to the index (staging area for the next commit)', true) 10 | :action(function(options) 11 | -- diff implementation goes here 12 | print("git-diff called with:", options.path, options.flag, options.patch) 13 | end) 14 | 15 | 16 | cli:command('log'):file('examples/04_commands--git-log.lua') 17 | 18 | local args, err = cli:parse() 19 | 20 | if not args and err then 21 | return print(err) 22 | elseif args then 23 | print('git with no command') 24 | end -------------------------------------------------------------------------------- /lua_cliargs-dev-1.rockspec: -------------------------------------------------------------------------------- 1 | local package_name = "lua_cliargs" 2 | local package_version = "dev" 3 | local rockspec_revision = "1" 4 | local github_account_name = "lunarmodules" 5 | local github_repo_name = package_name 6 | 7 | rockspec_format = "3.0" 8 | package = package_name 9 | version = package_version .. "-" .. rockspec_revision 10 | 11 | source = { 12 | url = "git+https://github.com/" .. github_account_name .. "/" .. github_repo_name .. ".git" 13 | } 14 | if package_version == "dev" then source.branch = "master" else source.tag = "v" .. package_version end 15 | 16 | description = { 17 | summary = "A command-line argument parsing module for Lua", 18 | detailed = [[ 19 | This module adds support for accepting CLI arguments easily using multiple 20 | notations and argument types. 21 | 22 | cliargs allows you to define required, optional, and flag arguments. 23 | ]], 24 | homepage = "https://github.com/"..github_account_name.."/"..github_repo_name, 25 | issues_url = "https://github.com/"..github_account_name.."/"..github_repo_name.."/issues", 26 | license = "MIT" 27 | } 28 | 29 | dependencies = { 30 | "lua >= 5.1" 31 | } 32 | 33 | test_dependencies = { 34 | "busted", 35 | "dkjson", 36 | "inifile", 37 | "yaml", 38 | } 39 | 40 | test = { 41 | type = "busted", 42 | } 43 | 44 | build = { 45 | type = "builtin", 46 | modules = { 47 | ["cliargs"] = "src/cliargs.lua", 48 | ["cliargs.config_loader"] = "src/cliargs/config_loader.lua", 49 | ["cliargs.constants"] = "src/cliargs/constants.lua", 50 | ["cliargs.core"] = "src/cliargs/core.lua", 51 | ["cliargs.parser"] = "src/cliargs/parser.lua", 52 | ["cliargs.printer"] = "src/cliargs/printer.lua", 53 | ["cliargs.utils.disect"] = "src/cliargs/utils/disect.lua", 54 | ["cliargs.utils.disect_argument"] = "src/cliargs/utils/disect_argument.lua", 55 | ["cliargs.utils.filter"] = "src/cliargs/utils/filter.lua", 56 | ["cliargs.utils.lookup"] = "src/cliargs/utils/lookup.lua", 57 | ["cliargs.utils.shallow_copy"] = "src/cliargs/utils/shallow_copy.lua", 58 | ["cliargs.utils.split"] = "src/cliargs/utils/split.lua", 59 | ["cliargs.utils.trim"] = "src/cliargs/utils/trim.lua", 60 | ["cliargs.utils.wordwrap"] = "src/cliargs/utils/wordwrap.lua", 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /rockspecs/lua_cliargs-3.0-2.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua_cliargs" 2 | version = "3.0-2" 3 | source = { 4 | url = "https://github.com/amireh/lua_cliargs/archive/v3.0-2.tar.gz", 5 | dir = "lua_cliargs-3.0-2" 6 | } 7 | description = { 8 | summary = "A command-line argument parser.", 9 | detailed = [[ 10 | This module adds support for accepting CLI arguments easily using multiple 11 | notations and argument types. 12 | 13 | cliargs allows you to define required, optional, and flag arguments. 14 | ]], 15 | homepage = "https://github.com/amireh/lua_cliargs", 16 | license = "MIT " 17 | } 18 | dependencies = { 19 | "lua >= 5.1" 20 | } 21 | build = { 22 | type = "builtin", 23 | modules = { 24 | ["cliargs"] = "src/cliargs.lua", 25 | ["cliargs.config_loader"] = "src/cliargs/config_loader.lua", 26 | ["cliargs.constants"] = "src/cliargs/constants.lua", 27 | ["cliargs.core"] = "src/cliargs/core.lua", 28 | ["cliargs.parser"] = "src/cliargs/parser.lua", 29 | ["cliargs.printer"] = "src/cliargs/printer.lua", 30 | ["cliargs.utils.disect"] = "src/cliargs/utils/disect.lua", 31 | ["cliargs.utils.disect_argument"] = "src/cliargs/utils/disect_argument.lua", 32 | ["cliargs.utils.filter"] = "src/cliargs/utils/filter.lua", 33 | ["cliargs.utils.lookup"] = "src/cliargs/utils/lookup.lua", 34 | ["cliargs.utils.shallow_copy"] = "src/cliargs/utils/shallow_copy.lua", 35 | ["cliargs.utils.split"] = "src/cliargs/utils/split.lua", 36 | ["cliargs.utils.trim"] = "src/cliargs/utils/trim.lua", 37 | ["cliargs.utils.wordwrap"] = "src/cliargs/utils/wordwrap.lua", 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /rockspecs/lua_cliargs-3.0.2-1.rockspec: -------------------------------------------------------------------------------- 1 | local package_name = "lua_cliargs" 2 | local package_version = "3.0.2" 3 | local rockspec_revision = "1" 4 | local github_account_name = "lunarmodules" 5 | local github_repo_name = package_name 6 | 7 | package = package_name 8 | version = package_version .. "-" .. rockspec_revision 9 | 10 | source = { 11 | url = "git+https://github.com/" .. github_account_name .. "/" .. github_repo_name .. ".git" 12 | } 13 | if package_version == "dev" then source.branch = "master" else source.tag = "v" .. package_version end 14 | 15 | description = { 16 | summary = "A command-line argument parsing module for Lua", 17 | detailed = [[ 18 | This module adds support for accepting CLI arguments easily using multiple 19 | notations and argument types. 20 | 21 | cliargs allows you to define required, optional, and flag arguments. 22 | ]], 23 | license = "MIT" 24 | } 25 | 26 | dependencies = { 27 | "lua >= 5.1" 28 | } 29 | 30 | build = { 31 | type = "builtin", 32 | modules = { 33 | ["cliargs"] = "src/cliargs.lua", 34 | ["cliargs.config_loader"] = "src/cliargs/config_loader.lua", 35 | ["cliargs.constants"] = "src/cliargs/constants.lua", 36 | ["cliargs.core"] = "src/cliargs/core.lua", 37 | ["cliargs.parser"] = "src/cliargs/parser.lua", 38 | ["cliargs.printer"] = "src/cliargs/printer.lua", 39 | ["cliargs.utils.disect"] = "src/cliargs/utils/disect.lua", 40 | ["cliargs.utils.disect_argument"] = "src/cliargs/utils/disect_argument.lua", 41 | ["cliargs.utils.filter"] = "src/cliargs/utils/filter.lua", 42 | ["cliargs.utils.lookup"] = "src/cliargs/utils/lookup.lua", 43 | ["cliargs.utils.shallow_copy"] = "src/cliargs/utils/shallow_copy.lua", 44 | ["cliargs.utils.split"] = "src/cliargs/utils/split.lua", 45 | ["cliargs.utils.trim"] = "src/cliargs/utils/trim.lua", 46 | ["cliargs.utils.wordwrap"] = "src/cliargs/utils/wordwrap.lua", 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /spec/cliargs_parsing_spec.lua: -------------------------------------------------------------------------------- 1 | describe("Testing cliargs library parsing commandlines", function() 2 | local cli 3 | 4 | before_each(function() 5 | cli = require("cliargs.core")() 6 | end) 7 | 8 | -- TODO, move to feature specs 9 | describe("Tests argument parsing with callback", function() 10 | local cb = {} 11 | 12 | local function callback(key, value) 13 | cb.key, cb.value = key, value 14 | return true 15 | end 16 | 17 | local function callback_arg(key, value) 18 | table.insert(cb, { key = key, value = value }) 19 | return true 20 | end 21 | 22 | local function callback_fail(key) 23 | return nil, "bad argument for " .. key 24 | end 25 | 26 | before_each(function() 27 | cb = {} 28 | end) 29 | 30 | it("tests one required argument", function() 31 | cli:argument("ARG", "arg description", callback) 32 | local expected = { ARG = "arg_val" } 33 | local result = cli:parse({ "arg_val" }) 34 | assert.are.same(expected, result) 35 | assert.are.equal("ARG", cb.key) 36 | assert.are.equal("arg_val", cb.value) 37 | end) 38 | 39 | it("tests required argument callback returning error", function() 40 | cli:argument("ARG", "arg description", callback_fail) 41 | 42 | local _, err = cli:parse({ "arg_val" }) 43 | assert.matches('bad argument for ARG', err) 44 | end) 45 | 46 | it("tests many required arguments", function() 47 | cli:argument("ARG1", "arg1 description", callback_arg) 48 | cli:argument("ARG2", "arg2 description", callback_arg) 49 | cli:argument("ARG3", "arg3 description", callback_arg) 50 | local expected = { ARG1 = "arg1_val", ARG2 = "arg2_val", ARG3 = "arg3_val" } 51 | local result = cli:parse({ "arg1_val", "arg2_val", "arg3_val" }) 52 | assert.are.same(expected, result) 53 | assert.are.same({ key = "ARG1", value = "arg1_val"}, cb[1]) 54 | assert.are.same({ key = "ARG2", value = "arg2_val"}, cb[2]) 55 | assert.are.same({ key = "ARG3", value = "arg3_val"}, cb[3]) 56 | end) 57 | 58 | it("tests one optional argument", function() 59 | cli:splat("OPTARG", "optional arg description", nil, 1, callback) 60 | local expected = { OPTARG = "opt_arg" } 61 | local result = cli:parse({ "opt_arg" }) 62 | assert.are.same(expected, result) 63 | assert.are.equal("OPTARG", cb.key) 64 | assert.are.equal("opt_arg", cb.value) 65 | end) 66 | 67 | it("tests optional argument callback returning error", function() 68 | cli:set_name('myapp') 69 | cli:splat("OPTARG", "optinoal arg description", nil, 1, callback_fail) 70 | 71 | local _, err = cli:parse({ "opt_arg" }) 72 | assert.matches('bad argument for OPTARG', err) 73 | end) 74 | 75 | it("tests many optional arguments", function() 76 | cli:splat("OPTARG", "optional arg description", nil, 3, callback_arg) 77 | local expected = { OPTARG = { "opt_arg1", "opt_arg2", "opt_arg3" } } 78 | local result = cli:parse({ "opt_arg1", "opt_arg2", "opt_arg3" }) 79 | assert.are.same(expected, result) 80 | assert.are.same({ key = "OPTARG", value = "opt_arg1"}, cb[1]) 81 | assert.are.same({ key = "OPTARG", value = "opt_arg2"}, cb[2]) 82 | assert.are.same({ key = "OPTARG", value = "opt_arg3"}, cb[3]) 83 | end) 84 | end) 85 | end) 86 | -------------------------------------------------------------------------------- /spec/cliargs_spec.lua: -------------------------------------------------------------------------------- 1 | dofile("spec/spec_helper.lua") 2 | local cliargs = require 'cliargs' 3 | 4 | describe('cliargs', function() 5 | setup(function() 6 | cliargs:flag('--foo', '...') 7 | end) 8 | 9 | after_each(function() 10 | cliargs = require 'cliargs' 11 | end) 12 | 13 | it('does not blow up!', function() 14 | end) 15 | 16 | it('yields a default core instance', function() 17 | assert.equal(type(cliargs), 'table') 18 | end) 19 | 20 | describe('#parse', function() 21 | it('works', function() 22 | local args 23 | 24 | assert.has_no_errors(function() 25 | args = cliargs:parse({}) 26 | end) 27 | 28 | assert.equal(type(args), 'table') 29 | end) 30 | 31 | it('propagates errors', function() 32 | local args, err = cliargs:parse({ '--bar' }, true) 33 | 34 | assert.equal(type(err), 'string') 35 | assert.is_nil(args) 36 | end) 37 | end) 38 | 39 | describe('#cleanup', function() 40 | 41 | it('exposes a cleanup routine', function() 42 | assert.equal(type(cliargs.cleanup), 'function') 43 | end) 44 | 45 | it('actually cleans up', function() 46 | local modules = {} 47 | 48 | for k, _ in pairs(package.loaded) do 49 | if k:match('cliargs') then 50 | table.insert(modules, k) 51 | end 52 | end 53 | 54 | assert.is_not_equal(#modules, 0) 55 | assert.is_not_nil(package.loaded['cliargs']) 56 | 57 | cliargs:cleanup() 58 | 59 | for k, _ in pairs(modules) do 60 | assert.is_nil(package.loaded[k]) 61 | end 62 | 63 | assert.is_nil(package.loaded['cliargs']) 64 | end) 65 | end) 66 | end) 67 | -------------------------------------------------------------------------------- /spec/config_loader_spec.lua: -------------------------------------------------------------------------------- 1 | dofile("spec/spec_helper.lua") 2 | 3 | describe("cliargs.config_loader", function() 4 | local cli, args, err 5 | 6 | before_each(function() 7 | cli = require("cliargs.core")() 8 | cli:flag('-q, --quiet', '...', false) 9 | cli:option('-c, --compress=VALUE', '...', 'lzma') 10 | cli:option('--config=FILE', '...', nil, function(_, path) 11 | local config 12 | 13 | config, err = cli:read_defaults(path) 14 | 15 | if config and not err then 16 | cli:load_defaults(config) 17 | end 18 | end) 19 | end) 20 | 21 | after_each(function() 22 | assert.equal(err, nil) 23 | assert.equal(args.c, 'bz2') 24 | assert.equal(args.compress, 'bz2') 25 | assert.equal(args.q, true) 26 | assert.equal(args.quiet, true) 27 | end) 28 | 29 | describe('#from_json', function() 30 | it('works', function() 31 | args, err = cli:parse({ '--config=spec/fixtures/config.json' }) 32 | end) 33 | end) 34 | 35 | describe('#from_ini', function() 36 | it('works', function() 37 | args, err = cli:parse({ '--config=spec/fixtures/config.ini' }) 38 | end) 39 | end) 40 | 41 | describe('#from_yaml', function() 42 | -- Because it isn't easy to install on Lua 5.4, some environments can't run this test 43 | -- https://github.com/lubyk/yaml/issues/7 44 | local hasyaml = pcall(require, "yaml") 45 | if hasyaml then 46 | it('works', function() 47 | args, err = cli:parse({ '--config', 'spec/fixtures/config.yml' }) 48 | end) 49 | end 50 | end) 51 | 52 | describe('#from_lua', function() 53 | it('works', function() 54 | args, err = cli:parse({ '--config', 'spec/fixtures/config.lua' }) 55 | end) 56 | end) 57 | end) 58 | -------------------------------------------------------------------------------- /spec/core_spec.lua: -------------------------------------------------------------------------------- 1 | local helpers = dofile("spec/spec_helper.lua") 2 | 3 | describe("cliargs::core", function() 4 | local cli 5 | 6 | before_each(function() 7 | cli = require("../src.cliargs.core")() 8 | end) 9 | 10 | describe('#parse', function() 11 | context('when invoked without the arguments table', function() 12 | local global_arg 13 | 14 | before_each(function() 15 | global_arg = _G['arg'] 16 | end) 17 | 18 | after_each(function() 19 | _G['arg'] = global_arg 20 | end) 21 | 22 | it('uses the global _G["arg"] one', function() 23 | _G["arg"] = {"--quiet"} 24 | 25 | cli:option('--quiet', '...') 26 | 27 | assert.equal(cli:parse().quiet, true) 28 | end) 29 | end) 30 | 31 | it('does not mutate the argument table', function() 32 | local arguments = { "--quiet" } 33 | cli:option('--quiet', '...') 34 | 35 | cli:parse(arguments) 36 | 37 | assert.equal(#arguments, 1) 38 | assert.equal(arguments[1], "--quiet") 39 | end) 40 | 41 | it("generates the help listing but does not print it to STDOUT", function() 42 | local res, err = cli:parse({'--help'}) 43 | 44 | assert.equal(type(res), "nil") 45 | assert.equal(type(err), "string") 46 | end) 47 | 48 | it("returns error strings but does not print them to STDOUT", function() 49 | local res, err = cli:parse({ "arg1" }) 50 | 51 | assert.equal(type(res), "nil") 52 | assert.equal(type(err), "string") 53 | end) 54 | 55 | describe('displaying the help listing', function() 56 | local res, err 57 | 58 | before_each(function() 59 | cli:argument('INPUT', '...') 60 | cli:flag('--quiet', '...') 61 | end) 62 | 63 | after_each(function() 64 | assert.equal(type(res), "nil") 65 | assert.equal(type(err), "string") 66 | assert.equal(err, cli.printer.generate_help_and_usage()) 67 | end) 68 | 69 | it('works with --help in the beginning', function() 70 | res, err = helpers.parse(cli, '--help something') 71 | end) 72 | 73 | it('works with --help in the end of options', function() 74 | res, err = helpers.parse(cli, '--quiet --help something') 75 | end) 76 | 77 | it('works with --help after an argument', function() 78 | res, err = helpers.parse(cli, '--quiet something --help') 79 | end) 80 | end) 81 | end) 82 | 83 | describe('#parse - the --__DUMP__ special option', function() 84 | it('dumps the state and errors out', function() 85 | stub(cli.printer, 'print') 86 | 87 | cli:argument('OUTPUT', '...') 88 | cli:splat('INPUTS', '...', nil, 5) 89 | cli:option('-c, --compress=VALUE', '...') 90 | cli:flag('-q, --quiet', '...', true) 91 | 92 | local _, err = cli:parse({'--__DUMP__', '/tmp/out', '/tmp/in.1', '/tmp/in.2', '/tmp/in.3' }) 93 | 94 | assert.matches('======= Provided command line =============', err) 95 | end) 96 | end) 97 | 98 | describe('#redefine_default', function() 99 | it('allows me to change the default for an optargument', function() 100 | cli:splat('ROOT', '...', 'foo') 101 | assert.equal(cli:parse({}).ROOT, 'foo') 102 | 103 | cli:redefine_default('ROOT', 'bar') 104 | assert.equal(cli:parse({}).ROOT, 'bar') 105 | end) 106 | 107 | it('allows me to change the default for an option', function() 108 | cli:option('-c, --compress=VALUE', '...', 'lzma') 109 | assert.equal(cli:parse({}).compress, 'lzma') 110 | 111 | cli:redefine_default('compress', 'bz2') 112 | assert.equal(cli:parse({}).compress, 'bz2') 113 | end) 114 | 115 | it('allows me to change the default for a flag', function() 116 | cli:flag('-q, --quiet', '...', false) 117 | assert.equal(cli:parse({}).quiet, false) 118 | 119 | cli:redefine_default('quiet', true) 120 | assert.equal(cli:parse({}).quiet, true) 121 | end) 122 | end) 123 | 124 | describe('#load_defaults', function() 125 | local args, err 126 | 127 | before_each(function() 128 | cli:option('-c, --compress=VALUE', '...', 'lzma') 129 | cli:flag('-q, --quiet', '...', false) 130 | end) 131 | 132 | it('works', function() 133 | cli:load_defaults({ 134 | compress = 'bz2', 135 | quiet = true 136 | }) 137 | 138 | args, err = cli:parse({}) 139 | 140 | assert.equal(err, nil) 141 | assert.same(args, { 142 | c = 'bz2', 143 | compress = 'bz2', 144 | q = true, 145 | quiet = true 146 | }) 147 | end) 148 | 149 | context('when @strict is not true', function() 150 | it('ignores keys that could not be mapped', function() 151 | cli:load_defaults({ 152 | compress = 'bz2', 153 | quiet = true, 154 | what = 'woot!' 155 | }) 156 | 157 | args, err = cli:parse({}) 158 | 159 | assert.equal(err, nil) 160 | assert.same(args, { 161 | c = 'bz2', 162 | compress = 'bz2', 163 | q = true, 164 | quiet = true 165 | }) 166 | end) 167 | end) 168 | 169 | context('when @strict is true', function() 170 | it('returns an error message if a key could not be mapped', function() 171 | args, err = cli:load_defaults({ 172 | what = 'woot!' 173 | }, true) 174 | 175 | assert.equal(args, nil) 176 | assert.equal(err, "Unrecognized option with the key 'what'") 177 | end) 178 | end) 179 | end) 180 | 181 | describe('#read_defaults_from_ini_file', function() 182 | local args, err 183 | 184 | before_each(function() 185 | cli:option('-c, --compress=VALUE', '...', 'lzma') 186 | cli:flag('-q, --quiet', '...', false) 187 | local config 188 | 189 | config, err = cli:read_defaults('spec/fixtures/config.ini') 190 | 191 | assert.equal(err, nil) 192 | assert.same(config, { 193 | compress = 'bz2', 194 | quiet = true, 195 | }) 196 | 197 | if config and not err then 198 | cli:load_defaults(config) 199 | end 200 | end) 201 | 202 | it('works', function() 203 | args, err = cli:parse({}) 204 | 205 | assert.equal(err, nil) 206 | assert.same(args, { 207 | c = 'bz2', 208 | compress = 'bz2', 209 | q = true, 210 | quiet = true 211 | }) 212 | end) 213 | end) 214 | 215 | 216 | describe('#read_defaults_from_ini_file_group_no_cast', function() 217 | local args, err 218 | 219 | before_each(function() 220 | cli:option('-h, --host=VALUE', '...', '127.0.0.1') 221 | cli:option('-p, --port=VALUE', '...', 8088) 222 | 223 | local config 224 | 225 | config, err = cli:read_defaults('spec/fixtures/config.ini', 'ini', 'database', true) 226 | 227 | assert.equal(err, nil) 228 | assert.same(config, { 229 | host = 'localhost', 230 | port = 5432, 231 | }) 232 | 233 | if config and not err then 234 | cli:load_defaults(config) 235 | end 236 | end) 237 | 238 | it('works', function() 239 | args, err = cli:parse({}) 240 | 241 | assert.equal(err, nil) 242 | assert.same(args, { 243 | h = 'localhost', 244 | host = 'localhost', 245 | p = 5432, 246 | port = 5432, 247 | }) 248 | end) 249 | end) 250 | 251 | 252 | describe('#read_defaults_from_ini_file_group_with_cast', function() 253 | local args, err 254 | 255 | before_each(function() 256 | cli:option('-h, --host=VALUE', '...', '127.0.0.1') 257 | cli:option('-p, --port=VALUE', '...', 8088) 258 | 259 | local config 260 | 261 | -- failing test case for #64 below 262 | -- config, err = cli:read_defaults('spec/fixtures/config.ini', 'ini', 'database', false) 263 | 264 | -- intermediate: prevent failure with Travis CI 265 | config, err = cli:read_defaults('spec/fixtures/config.ini', 'ini', 'database', true) 266 | 267 | assert.equal(err, nil) 268 | assert.same(config, { 269 | host = 'localhost', 270 | port = 5432, 271 | }) 272 | 273 | if config and not err then 274 | cli:load_defaults(config) 275 | end 276 | end) 277 | 278 | it('works', function() 279 | args, err = cli:parse({}) 280 | 281 | assert.equal(err, nil) 282 | assert.same(args, { 283 | h = 'localhost', 284 | host = 'localhost', 285 | p = 5432, 286 | port = 5432, 287 | }) 288 | end) 289 | end) 290 | 291 | end) 292 | -------------------------------------------------------------------------------- /spec/features/argument_spec.lua: -------------------------------------------------------------------------------- 1 | local helpers = dofile("spec/spec_helper.lua") 2 | 3 | describe("cliargs - arguments", function() 4 | local cli 5 | 6 | before_each(function() 7 | cli = require("cliargs.core")() 8 | end) 9 | 10 | describe('defining arguments', function() 11 | it('works', function() 12 | assert.has_no_errors(function() 13 | cli:argument('PATH', 'path to a file') 14 | end) 15 | end) 16 | 17 | it('requires a key', function() 18 | assert.error_matches(function() 19 | cli:argument() 20 | end, 'Key and description are mandatory arguments') 21 | end) 22 | 23 | it('requires a description', function() 24 | assert.error_matches(function() 25 | cli:argument('PATH') 26 | end, 'Key and description are mandatory arguments') 27 | end) 28 | 29 | it('rejects a bad callback', function() 30 | assert.error_matches(function() 31 | cli:argument('PATH', 'path to a file', 'lolol') 32 | end, 'Callback argument must be a function') 33 | end) 34 | 35 | it('rejects duplicate arguments', function() 36 | cli:argument('PATH', 'path to a file') 37 | 38 | assert.error_matches(function() 39 | cli:argument('PATH', '...') 40 | end, 'Duplicate argument') 41 | end) 42 | end) 43 | 44 | describe('parsing arguments', function() 45 | it('works with a single argument', function() 46 | cli:argument('PATH', 'path to a file') 47 | 48 | local args = helpers.parse(cli, '/some/where') 49 | 50 | assert.equal(args.PATH, '/some/where') 51 | end) 52 | 53 | it('works with multiple arguments', function() 54 | cli:argument('INPUT', 'path to the input file') 55 | cli:argument('OUTPUT', 'path to the output file') 56 | 57 | local args = helpers.parse(cli, '/some/where /some/where/else') 58 | 59 | assert.equal(args.INPUT, '/some/where') 60 | assert.equal(args.OUTPUT, '/some/where/else') 61 | end) 62 | 63 | it('bails on missing arguments', function() 64 | cli:argument('INPUT', 'path to the input file') 65 | cli:argument('OUTPUT', 'path to the output file') 66 | 67 | local _, err = helpers.parse(cli, '/some/where') 68 | assert.matches('bad number of arguments', err) 69 | end) 70 | 71 | it('bails on too many arguments', function() 72 | cli:argument('INPUT', 'path to the input file') 73 | 74 | local _, err = helpers.parse(cli, 'foo bar') 75 | 76 | assert.matches('bad number of arguments', err) 77 | end) 78 | end) 79 | 80 | describe('@callback', function() 81 | local call_args 82 | local function capture(key, value, altkey) 83 | table.insert(call_args, { key, value, altkey }) 84 | end 85 | 86 | before_each(function() 87 | call_args = {} 88 | end) 89 | 90 | context('given a single argument', function() 91 | before_each(function() 92 | cli:argument('PATH', 'path to a file', capture) 93 | end) 94 | 95 | it('invokes the callback when the argument is parsed', function() 96 | helpers.parse(cli, '/some/where') 97 | 98 | assert.equal(call_args[1][1], 'PATH') 99 | assert.equal(call_args[1][2], '/some/where') 100 | assert.equal(call_args[1][3], nil) 101 | end) 102 | end) 103 | 104 | context('given multiple arguments', function() 105 | before_each(function() 106 | cli:argument('INPUT', '...', capture) 107 | cli:argument('OUTPUT', '...', capture) 108 | end) 109 | 110 | it('invokes the callback for each argument parsed', function() 111 | helpers.parse(cli, '/some/where /some/where/else') 112 | 113 | assert.equal(call_args[1][1], 'INPUT') 114 | assert.equal(call_args[1][2], '/some/where') 115 | assert.equal(call_args[2][1], 'OUTPUT') 116 | assert.equal(call_args[2][2], '/some/where/else') 117 | end) 118 | end) 119 | end) 120 | end) 121 | -------------------------------------------------------------------------------- /spec/features/command_spec.lua: -------------------------------------------------------------------------------- 1 | describe("cliargs - commands", function() 2 | local cli 3 | 4 | before_each(function() 5 | cli = require("cliargs.core")() 6 | end) 7 | 8 | describe('defining commands', function() 9 | it('works', function() 10 | assert.has_no_errors(function() 11 | cli:command('run', '...') 12 | end) 13 | end) 14 | end) 15 | 16 | describe('running commands', function() 17 | context('given an action callback', function() 18 | local cmd 19 | local action 20 | 21 | before_each(function() 22 | action = stub() 23 | 24 | cmd = cli:command('run', '...') 25 | cmd:action(action) 26 | end) 27 | 28 | it('works with no arguments', function() 29 | cli:parse({'run'}) 30 | end) 31 | 32 | it('runs the command parser and passes on the args to the command', function() 33 | cmd:argument('ROOT', '...') 34 | 35 | cli:parse({'run', '/some/path'}) 36 | 37 | assert.stub(action).called_with({ ROOT ='/some/path' }) 38 | end) 39 | 40 | it('propagates parsing errors', function() 41 | local _, err = cli:parse({'run', 'some_undefined_arg'}) 42 | 43 | assert.match("bad number of arguments", err) 44 | end) 45 | end) 46 | 47 | context('given a command file', function() 48 | local cmd 49 | 50 | before_each(function() 51 | cmd = cli:command('run', '...') 52 | cmd:file('spec/fixtures/test-command.lua') 53 | end) 54 | 55 | it('works with no arguments', function() 56 | cli:parse({'run'}) 57 | end) 58 | 59 | it('returns an error on bad file', function() 60 | cmd:file('foo') 61 | 62 | assert.error_matches(function() 63 | cli:parse({'run'}) 64 | end, 'cannot open foo'); 65 | end) 66 | 67 | it('passes on arguments to the command', function() 68 | cmd:argument('ROOT', '...') 69 | 70 | local res, err = cli:parse({'run', '/some/path'}) 71 | 72 | assert.equal(nil, err); 73 | assert.equal(res.ROOT, '/some/path') 74 | end) 75 | 76 | it('propagates parsing errors', function() 77 | cmd:argument('ROOT', '...') 78 | 79 | local res, err = cli:parse({'run', '/some/path', 'foo'}) 80 | 81 | assert.equal(nil, res); 82 | assert.match('bad number of arguments', err) 83 | end) 84 | end) 85 | end) 86 | end) -------------------------------------------------------------------------------- /spec/features/flag_spec.lua: -------------------------------------------------------------------------------- 1 | local helpers = dofile("spec/spec_helper.lua") 2 | 3 | describe("cliargs - flags", function() 4 | local cli 5 | 6 | before_each(function() 7 | cli = require("cliargs.core")() 8 | end) 9 | 10 | describe('defining flags', function() 11 | it('works', function() 12 | assert.has_no_errors(function() 13 | cli:flag('--quiet', 'suppress output') 14 | end) 15 | end) 16 | 17 | it('requires a key', function() 18 | assert.error_matches(function() 19 | cli:flag() 20 | end, 'Key and description are mandatory arguments') 21 | end) 22 | 23 | it('requires a description', function() 24 | assert.error_matches(function() 25 | cli:flag('--quiet') 26 | end, 'Key and description are mandatory arguments') 27 | end) 28 | 29 | it('rejects a value label', function() 30 | assert.error_matches(function() 31 | cli:flag('--quiet=QUIET', '...') 32 | end, 'A flag type option cannot have a value set') 33 | end) 34 | 35 | it('rejects a duplicate flag', function() 36 | cli:flag('--quiet', '...') 37 | 38 | assert.error_matches(function() 39 | cli:flag('--quiet', '...') 40 | end, 'Duplicate') 41 | end) 42 | end) 43 | 44 | describe('parsing', function() 45 | it('works with only a short key: -v', function() 46 | cli:flag('-v', '...') 47 | assert.equal(helpers.parse(cli, '-v').v, true) 48 | end) 49 | 50 | it('works with only an expanded key: --verbose', function() 51 | cli:flag('--verbose', '...') 52 | assert.equal(helpers.parse(cli, '--verbose').verbose, true) 53 | end) 54 | 55 | it('works with both: -v, --verbose', function() 56 | cli:flag('-v, --verbose', '...') 57 | assert.equal(helpers.parse(cli, '--verbose').verbose, true) 58 | assert.equal(helpers.parse(cli, '-v').verbose, true) 59 | end) 60 | 61 | context('given a default value', function() 62 | it('accepts a nil', function() 63 | assert.has_no_errors(function() 64 | cli:flag('--quiet', '...', nil) 65 | end) 66 | 67 | assert.equal(helpers.parse(cli, '').quiet, nil) 68 | end) 69 | 70 | it('accepts a true value', function() 71 | assert.has_no_errors(function() 72 | cli:flag('--quiet', '...', true) 73 | end) 74 | 75 | assert.equal(helpers.parse(cli, '').quiet, true) 76 | end) 77 | 78 | it('accepts a false value', function() 79 | assert.has_no_errors(function() 80 | cli:flag('--quiet', '...', false) 81 | end) 82 | 83 | assert.equal(helpers.parse(cli, '').quiet, false) 84 | end) 85 | end) 86 | 87 | context('given an unknown flag', function() 88 | it('bails', function() 89 | local _, err = helpers.parse(cli, '--asdf', true) 90 | assert.matches('unknown', err) 91 | end) 92 | end) 93 | end) 94 | 95 | describe('parsing an option with a short key longer than 1 char', function() 96 | before_each(function() 97 | cli:flag('-Wno-unsigned', '...') 98 | end) 99 | 100 | it('works', function() 101 | local args = helpers.parse(cli, '-Wno-unsigned') 102 | assert.equal(args['Wno-unsigned'], true) 103 | end) 104 | end) 105 | 106 | describe('parsing negatable flags', function() 107 | context('when a flag is negatable (-q, --[no-]quiet)', function() 108 | before_each(function() 109 | cli:flag('-q, --[no-]quiet', '...') 110 | end) 111 | 112 | it('works', function() 113 | local args = helpers.parse(cli, '--no-quiet') 114 | assert.equal(args.quiet, false) 115 | end) 116 | 117 | it('overrides the original version (-q --no-quiet => false)', function() 118 | local args = helpers.parse(cli, '-q --no-quiet') 119 | assert.equal(args.quiet, false) 120 | end) 121 | 122 | it('ignores the negated version (--no-quiet -q => true)', function() 123 | local args = helpers.parse(cli, '--no-quiet -q') 124 | assert.equal(args.quiet, true) 125 | end) 126 | end) 127 | 128 | context('when a flag is NON-negatable (-q, --quiet)', function() 129 | before_each(function() 130 | cli:flag('-q, --quiet', '...') 131 | end) 132 | 133 | it('bails', function() 134 | local _, err = helpers.parse(cli, '--no-quiet') 135 | assert.matches('may not be negated', err) 136 | end) 137 | end) 138 | end) 139 | 140 | describe('@callback', function() 141 | local call_args 142 | local function capture(key, value, altkey) 143 | table.insert(call_args, { key, value, altkey }) 144 | end 145 | 146 | context('given a single flag', function() 147 | before_each(function() 148 | call_args = {} 149 | 150 | cli:flag('-q, --quiet', '...', nil, capture) 151 | end) 152 | 153 | it('invokes the callback when the flag is parsed', function() 154 | helpers.parse(cli, '--quiet') 155 | 156 | assert.equal(call_args[1][1], 'quiet') 157 | assert.equal(call_args[1][2], true) 158 | assert.equal(call_args[1][3], 'q') 159 | end) 160 | end) 161 | 162 | context('given a negated flag', function() 163 | before_each(function() 164 | call_args = {} 165 | 166 | cli:flag('-q, --[no-]quiet', '...', nil, capture) 167 | end) 168 | 169 | it('invokes the callback when the flag is parsed', function() 170 | helpers.parse(cli, '--no-quiet') 171 | 172 | assert.equal(call_args[1][1], 'quiet') 173 | assert.equal(call_args[1][2], false) 174 | assert.equal(call_args[1][3], 'q') 175 | end) 176 | end) 177 | 178 | context('given multiple flags', function() 179 | before_each(function() 180 | call_args = {} 181 | 182 | cli:flag('-q, --quiet', '...', nil, capture) 183 | cli:flag('--verbose', '...', nil, capture) 184 | end) 185 | 186 | it('invokes the callback for each flag parsed', function() 187 | helpers.parse(cli, '--quiet --verbose') 188 | 189 | assert.equal(call_args[1][1], 'quiet') 190 | assert.equal(call_args[1][2], true) 191 | assert.equal(call_args[1][3], 'q') 192 | 193 | assert.equal(call_args[2][1], 'verbose') 194 | assert.equal(call_args[2][2], true) 195 | assert.equal(call_args[2][3], nil) 196 | end) 197 | end) 198 | end) 199 | end) 200 | -------------------------------------------------------------------------------- /spec/features/integration_spec.lua: -------------------------------------------------------------------------------- 1 | local helpers = dofile("spec/spec_helper.lua") 2 | 3 | describe("integration: parsing", function() 4 | local cli 5 | 6 | before_each(function() 7 | cli = require("cliargs.core")() 8 | end) 9 | 10 | it('is a no-op when no arguments or options are defined', function() 11 | assert.are.same(helpers.parse(cli, ''), {}) 12 | end) 13 | 14 | context('given a set of arguments', function() 15 | it('works when all are passed in', function() 16 | cli:argument('FOO', '...') 17 | cli:argument('BAR', '...') 18 | 19 | local args = helpers.parse(cli, 'foo bar') 20 | 21 | assert.same(args, { FOO = "foo", BAR = "bar" }) 22 | end) 23 | end) 24 | 25 | context('given an argument and a splat', function() 26 | before_each(function() 27 | cli:argument('FOO', '...') 28 | cli:splat('BAR', '...', nil, 2) 29 | end) 30 | 31 | it('works when only the argument is passed in', function() 32 | local args = helpers.parse(cli, 'foo') 33 | 34 | assert.same(args, { FOO = "foo", BAR = {} }) 35 | end) 36 | 37 | it('works when both are passed in', function() 38 | local args = helpers.parse(cli, 'foo bar') 39 | 40 | assert.same(args, { FOO = "foo", BAR = { "bar" } }) 41 | end) 42 | 43 | it('works when both are passed in with repetition for the splat', function() 44 | local args = helpers.parse(cli, 'foo bar zoo') 45 | 46 | assert.same(args, { FOO = "foo", BAR = { "bar", "zoo" } }) 47 | end) 48 | end) 49 | 50 | context('given a set of options', function() 51 | it('works when nothing is passed in', function() 52 | cli:option('--foo FOO', '...') 53 | cli:option('--bar BAR', '...') 54 | 55 | local args = helpers.parse(cli, '') 56 | 57 | assert.same(args, {}) 58 | end) 59 | 60 | it('works when they are passed in', function() 61 | cli:option('-f, --foo FOO', '...') 62 | cli:option('--bar BAR', '...') 63 | 64 | local args = helpers.parse(cli, '-f something --bar=BAZ') 65 | 66 | assert.same(args, { 67 | f = "something", 68 | foo = "something", 69 | bar = "BAZ" 70 | }) 71 | end) 72 | end) 73 | 74 | context('given arguments, options, and flags', function() 75 | before_each(function() 76 | cli:argument('FOO', '...') 77 | cli:option('--input=SOURCE', '...') 78 | cli:flag('--quiet', '...') 79 | end) 80 | 81 | it('works when nothing but arguments are passed in', function() 82 | local args = helpers.parse(cli, 'asdf') 83 | 84 | assert.same(args, { 85 | FOO = 'asdf', 86 | input = nil, 87 | quiet = nil 88 | }) 89 | end) 90 | 91 | it('works when arguments and options are passed in', function() 92 | local args = helpers.parse(cli, '--input /tmp/file asdf') 93 | 94 | assert.same(args, { 95 | FOO = 'asdf', 96 | input = '/tmp/file', 97 | quiet = nil 98 | }) 99 | end) 100 | 101 | it('works when everything is passed in', function() 102 | local args = helpers.parse(cli, '--input /tmp/file --quiet asdf') 103 | 104 | assert.same(args, { 105 | FOO = 'asdf', 106 | input = '/tmp/file', 107 | quiet = true 108 | }) 109 | end) 110 | 111 | it('works when an option comes after an argument', function() 112 | local args, err = helpers.parse(cli, 'asdf --quiet') 113 | 114 | assert.equal(err, nil) 115 | assert.same(args, { 116 | FOO = 'asdf', 117 | quiet = true 118 | }) 119 | end) 120 | end) 121 | 122 | describe('using -- to separate options from arguments', function() 123 | before_each(function() 124 | cli:argument('INPUT', '...') 125 | cli:splat('OUTPUT', '...', nil, 1) 126 | cli:flag('--verbose', '...') 127 | cli:flag('--quiet', '...') 128 | end) 129 | 130 | it('works', function() 131 | local args = helpers.parse(cli, '--verbose -- --input -d') 132 | 133 | assert.same(args, { 134 | INPUT = "--input", 135 | OUTPUT = "-d", 136 | verbose = true, 137 | quiet = nil 138 | }) 139 | end) 140 | 141 | it('does not actually parse an option if it comes after --', function() 142 | local args = helpers.parse(cli, '-- --input --quiet') 143 | 144 | assert.same(args, { 145 | INPUT = "--input", 146 | OUTPUT = "--quiet", 147 | verbose = nil, 148 | quiet = nil 149 | }) 150 | end) 151 | end) 152 | end) 153 | -------------------------------------------------------------------------------- /spec/features/option_spec.lua: -------------------------------------------------------------------------------- 1 | local helpers = dofile("spec/spec_helper.lua") 2 | 3 | describe("cliargs - options", function() 4 | local cli 5 | 6 | before_each(function() 7 | cli = require("cliargs.core")() 8 | end) 9 | 10 | describe('defining options', function() 11 | it('requires a key', function() 12 | assert.error_matches(function() 13 | cli:option() 14 | end, 'Key and description are mandatory arguments') 15 | end) 16 | 17 | it('requires a description', function() 18 | assert.error_matches(function() 19 | cli:option('--url=URL') 20 | end, 'Key and description are mandatory arguments') 21 | end) 22 | 23 | it('works', function() 24 | assert.has_no_errors(function() 25 | cli:option('--url=URL', '...') 26 | end) 27 | end) 28 | 29 | it('rejects a duplicate option', function() 30 | cli:option('--url=URL', '...') 31 | 32 | assert.error_matches(function() 33 | cli:option('--url=URL', '...') 34 | end, 'Duplicate') 35 | end) 36 | end) 37 | 38 | it('works with only a short key: -u VALUE', function() 39 | cli:option('-u VALUE', '...') 40 | assert.equal(helpers.parse(cli, '-u something').u, 'something') 41 | end) 42 | 43 | it('works with only an expanded key: --url=VALUE', function() 44 | cli:option('--url=VALUE', '...') 45 | assert.equal(helpers.parse(cli, '--url=something').url, 'something') 46 | end) 47 | 48 | it('works with only an expanded key using space as a delimiter: --url VALUE', function() 49 | cli:option('--url VALUE', '...') 50 | assert.equal(helpers.parse(cli, '--url something').url, 'something') 51 | end) 52 | 53 | it('works with both: -u, --url=VALUE', function() 54 | cli:option('-u, --url=VALUE', '...') 55 | assert.equal(helpers.parse(cli, '--url=something').url, 'something') 56 | assert.equal(helpers.parse(cli, '-u=something').url, 'something') 57 | end) 58 | 59 | it('works with both keys and no comma between them: -u --url VALUE', function() 60 | cli:option('-u --url=VALUE', '...') 61 | assert.equal(helpers.parse(cli, '--url something').url, 'something') 62 | assert.equal(helpers.parse(cli, '-u something').url, 'something') 63 | end) 64 | 65 | context('given no value indicator (an implicit flag, e.g. --quiet)', function() 66 | it('proxies to #flag', function() 67 | stub(cli, 'flag') 68 | 69 | cli:option('-q', '...') 70 | 71 | assert.stub(cli.flag).was.called(); 72 | end) 73 | end) 74 | 75 | describe('parsing', function() 76 | before_each(function() 77 | cli:option('-s, --source=SOURCE', '...') 78 | end) 79 | 80 | context('using a -short key and space as a delimiter', function() 81 | it('works', function() 82 | local args = helpers.parse(cli, '-s /foo/**/*.lua') 83 | assert.equal(args.source, '/foo/**/*.lua') 84 | end) 85 | end) 86 | 87 | context('using a -short key and = as a delimiter', function() 88 | it('works', function() 89 | local args = helpers.parse(cli, '-s=/foo/**/*.lua') 90 | assert.equal(args.source, '/foo/**/*.lua') 91 | end) 92 | end) 93 | 94 | context('using an --expanded-key and space as a delimiter', function() 95 | it('works', function() 96 | local args = helpers.parse(cli, '--source /foo/**/*.lua') 97 | assert.equal(args.source, '/foo/**/*.lua') 98 | end) 99 | end) 100 | 101 | context('using an --expanded-key and = as a delimiter', function() 102 | it('works', function() 103 | local args = helpers.parse(cli, '--source=/foo/**/*.lua') 104 | assert.equal(args.source, '/foo/**/*.lua') 105 | end) 106 | end) 107 | 108 | context('for an option with a short key longer than 1 char', function() 109 | before_each(function() 110 | cli:option('-Xassembler OPTIONS', '...') 111 | end) 112 | 113 | it('works', function() 114 | local args = helpers.parse(cli, '-Xassembler foo') 115 | assert.equal(args.Xassembler, 'foo') 116 | end) 117 | end) 118 | 119 | context('given multiple values', function() 120 | before_each(function() 121 | cli:option('-k, --key=OPTIONS', '...', {}) 122 | end) 123 | 124 | it('works', function() 125 | local args = helpers.parse(cli, '-k 1 --key=3 -k asdf') 126 | 127 | assert.equal(type(args.k), 'table') 128 | assert.equal(#args.k, 3) 129 | assert.equal(args.k[1], '1') 130 | assert.equal(args.k[2], '3') 131 | assert.equal(args.k[3], 'asdf') 132 | end) 133 | end) 134 | 135 | context('given an unknown option', function() 136 | it('bails', function() 137 | local _, err = helpers.parse(cli, '--asdf=jkl;', true) 138 | assert.matches('unknown', err) 139 | end) 140 | end) 141 | 142 | it('bails if no value was passed', function() 143 | local _, err = helpers.parse(cli, '-s') 144 | assert.matches("option %-s requires a value to be set", err) 145 | end) 146 | end) 147 | 148 | describe('parsing with a default value', function() 149 | it('accepts a nil', function() 150 | cli:option('--compress=VALUE', '...', nil) 151 | assert.equal(helpers.parse(cli, '').compress, nil) 152 | end) 153 | 154 | it('accepts a string', function() 155 | cli:option('--compress=VALUE', '...', 'lzma') 156 | assert.equal(helpers.parse(cli, '').compress, 'lzma') 157 | end) 158 | 159 | it('accepts a number', function() 160 | cli:option('--count=VALUE', '...', 5) 161 | assert.equal(helpers.parse(cli, '').count, 5) 162 | end) 163 | 164 | it('accepts a boolean', function() 165 | cli:option('--quiet=VALUE', '...', true) 166 | assert.equal(helpers.parse(cli, '').quiet, true) 167 | end) 168 | 169 | it('accepts an empty table', function() 170 | cli:option('--sources=VALUE', '...', {}) 171 | assert.same(helpers.parse(cli, '').sources, {}) 172 | end) 173 | 174 | it('lets me override/reset the default value', function() 175 | cli:option('--compress=URL', '...', 'lzma') 176 | assert.equal(helpers.parse(cli, '--compress=').compress, nil) 177 | end) 178 | end) 179 | 180 | describe('@callback', function() 181 | local call_args 182 | local function capture(key, value, altkey) 183 | table.insert(call_args, { key, value, altkey }) 184 | end 185 | 186 | context('given a single option', function() 187 | before_each(function() 188 | call_args = {} 189 | 190 | cli:option('-c, --compress=VALUE', '...', nil, capture) 191 | end) 192 | 193 | it('invokes the callback when the option is parsed', function() 194 | helpers.parse(cli, '--compress=lzma') 195 | 196 | assert.equal(call_args[1][1], 'compress') 197 | assert.equal(call_args[1][2], 'lzma') 198 | assert.equal(call_args[1][3], 'c') 199 | end) 200 | 201 | it('invokes the callback with the latest value when the option is a list', function() 202 | cli:option('--tags=VALUE', '...', {}, capture) 203 | 204 | helpers.parse(cli, '--tags only --tags foo') 205 | 206 | assert.equal(call_args[1][1], 'tags') 207 | assert.equal(call_args[1][2], 'only') 208 | assert.equal(call_args[1][3], nil) 209 | 210 | assert.equal(call_args[2][1], 'tags') 211 | assert.equal(call_args[2][2], 'foo') 212 | assert.equal(call_args[2][3], nil) 213 | end) 214 | end) 215 | 216 | context('when the callback returns an error message', function() 217 | it('propagates the error', function() 218 | cli:option('-c, --compress=VALUE', '...', nil, function() 219 | return nil, ">>> bad argument <<<" 220 | end) 221 | 222 | local _, err = helpers.parse(cli, '-c lzma', true) 223 | assert.equal('>>> bad argument <<<', err) 224 | end) 225 | end) 226 | 227 | context('given multiple options', function() 228 | before_each(function() 229 | call_args = {} 230 | 231 | cli:option('-c, --compress=VALUE', '...', nil, capture) 232 | cli:option('--input=PATH', '...', nil, capture) 233 | end) 234 | 235 | it('invokes the callback for each option parsed', function() 236 | helpers.parse(cli, '-c lzma --input=/tmp') 237 | 238 | assert.equal(call_args[1][1], 'c') 239 | assert.equal(call_args[1][2], 'lzma') 240 | assert.equal(call_args[1][3], 'compress') 241 | 242 | assert.equal(call_args[2][1], 'input') 243 | assert.equal(call_args[2][2], '/tmp') 244 | assert.equal(call_args[2][3], nil) 245 | end) 246 | end) 247 | end) 248 | end) 249 | -------------------------------------------------------------------------------- /spec/features/splatarg_spec.lua: -------------------------------------------------------------------------------- 1 | local helpers = dofile("spec/spec_helper.lua") 2 | 3 | describe("cliargs - splat arguments", function() 4 | local cli 5 | 6 | before_each(function() 7 | cli = require("cliargs.core")() 8 | end) 9 | 10 | describe('defining the splat arg', function() 11 | it('works', function() 12 | assert.has_no_error(function() 13 | cli:splat('SPLAT', 'some repeatable arg') 14 | end) 15 | end) 16 | 17 | it('requires a key', function() 18 | assert.error_matches(function() 19 | cli:splat() 20 | end, 'Key and description are mandatory arguments') 21 | end) 22 | 23 | it('requires a description', function() 24 | assert.error_matches(function() 25 | cli:splat('SPLAT') 26 | end, 'Key and description are mandatory arguments') 27 | end) 28 | 29 | it('rejects multiple definitions', function() 30 | cli:splat('SPLAT', 'some repeatable arg') 31 | 32 | assert.error_matches(function() 33 | cli:splat('SOME_SPLAT', 'some repeatable arg') 34 | end, 'Only one splat') 35 | end) 36 | end) 37 | 38 | describe('default value', function() 39 | it('allows me to define a default value', function() 40 | cli:splat('SPLAT', 'some repeatable arg', 'foo') 41 | end) 42 | 43 | context('when only 1 occurrence is allowed', function() 44 | before_each(function() 45 | cli:splat('SPLAT', 'some repeatable arg', 'foo') 46 | end) 47 | 48 | it('uses the default value when nothing is passed in', function() 49 | assert.equal(helpers.parse(cli, '').SPLAT, 'foo') 50 | end) 51 | end) 52 | 53 | context('when more than 1 occurrence is allowed', function() 54 | before_each(function() 55 | cli:splat('SPLAT', 'some repeatable arg', 'foo', 3) 56 | end) 57 | 58 | it('uses the default value only once when nothing is passed in', function() 59 | assert.same(helpers.parse(cli, '').SPLAT, { 'foo' }) 60 | end) 61 | 62 | it('does not use the default value if something was passed in at least once', function() 63 | assert.same(helpers.parse(cli, 'asdf').SPLAT, { 'asdf' }) 64 | end) 65 | end) 66 | end) 67 | 68 | describe('repetition count', function() 69 | it('accepts a repetition count', function() 70 | assert.has_no_error(function() 71 | cli:splat('SPLAT', 'some repeatable arg', nil, 2) 72 | end) 73 | end) 74 | 75 | it('appends the values to a list', function() 76 | cli:splat('SPLAT', 'some repeatable arg', nil, 2) 77 | local args = helpers.parse(cli, 'a b') 78 | 79 | assert.equal(#args.SPLAT, 2) 80 | assert.equal(args.SPLAT[1], 'a') 81 | assert.equal(args.SPLAT[2], 'b') 82 | end) 83 | 84 | it('bails if more values were passed than acceptable', function() 85 | cli:splat('SPLAT', 'foobar', nil, 2) 86 | 87 | local _, err = helpers.parse(cli, 'a b c') 88 | assert.matches("bad number of arguments", err) 89 | end) 90 | end) 91 | 92 | context("given a splatarg as the only argument/option", function() 93 | it("works", function() 94 | cli:splat('SPLAT', 'foobar', nil, 1) 95 | 96 | local args = helpers.parse(cli, 'asdf') 97 | 98 | assert.equal(type(args.SPLAT), "string") 99 | assert.equal(args.SPLAT, "asdf") 100 | end) 101 | end) 102 | 103 | describe('@callback', function() 104 | it('invokes the callback every time a value for the splat arg is parsed', function() 105 | local call_args = {} 106 | 107 | cli:splat('SPLAT', 'foobar', nil, 2, function(_, value) 108 | table.insert(call_args, value) 109 | end) 110 | 111 | helpers.parse(cli, 'a b') 112 | 113 | assert.equal(#call_args, 2) 114 | assert.equal(call_args[1], 'a') 115 | assert.equal(call_args[2], 'b') 116 | end) 117 | end) 118 | end) 119 | -------------------------------------------------------------------------------- /spec/fixtures/config.ini: -------------------------------------------------------------------------------- 1 | [cli] 2 | quiet = true 3 | compress = bz2 4 | [database] 5 | host=localhost 6 | port=5432 -------------------------------------------------------------------------------- /spec/fixtures/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "quiet": true, 3 | "compress": "bz2" 4 | } -------------------------------------------------------------------------------- /spec/fixtures/config.lua: -------------------------------------------------------------------------------- 1 | return { 2 | ["quiet"] = true, 3 | ["compress"] = "bz2" 4 | } -------------------------------------------------------------------------------- /spec/fixtures/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | quiet: true 3 | compress: bz2 -------------------------------------------------------------------------------- /spec/fixtures/test-command.lua: -------------------------------------------------------------------------------- 1 | local cli = require('cliargs') 2 | 3 | cli:set_name('test-command') 4 | cli:argument('ROOT', '...') 5 | 6 | return cli:parse() -------------------------------------------------------------------------------- /spec/printer_spec.lua: -------------------------------------------------------------------------------- 1 | local helpers = dofile("spec/spec_helper.lua") 2 | local trim = helpers.trim 3 | 4 | describe('printer', function() 5 | local cli 6 | 7 | before_each(function() 8 | cli = require("cliargs.core")() 9 | end) 10 | 11 | describe('#generate_usage', function() 12 | local function assert_msg(expected_msg) 13 | local actual_msg = cli.printer.generate_usage() 14 | 15 | assert.equal(trim(expected_msg), trim(actual_msg)) 16 | end 17 | 18 | it('works with 0 arguments', function() 19 | assert_msg 'Usage:' 20 | end) 21 | 22 | it('works with 1 argument', function() 23 | cli:argument('INPUT', 'path to the input file') 24 | 25 | assert_msg [==[ 26 | Usage: [--] INPUT 27 | ]==] 28 | end) 29 | 30 | it('works with 2+ arguments', function() 31 | cli:argument('INPUT', '...') 32 | cli:argument('OUTPUT', '...') 33 | 34 | assert_msg [==[ 35 | Usage: [--] INPUT OUTPUT 36 | ]==] 37 | end) 38 | 39 | it('prints the app name', function() 40 | cli:set_name('foo') 41 | assert_msg 'Usage: foo' 42 | end) 43 | 44 | it('prints options', function() 45 | cli:option('--foo=VALUE', '...') 46 | 47 | assert_msg [==[ 48 | Usage: [OPTIONS] 49 | ]==] 50 | end) 51 | 52 | it('prints flags', function() 53 | cli:flag('--foo', '...') 54 | 55 | assert_msg [==[ 56 | Usage: [OPTIONS] 57 | ]==] 58 | end) 59 | 60 | it('prints a splat arg with reptitions == 1', function() 61 | cli:splat('OUTPUT', '...', nil, 1) 62 | 63 | assert_msg [==[ 64 | Usage: [--] [OUTPUT] 65 | ]==] 66 | end) 67 | 68 | it('prints a splat arg with reptitions == 2', function() 69 | cli:splat('OUTPUT', '...', nil, 2) 70 | 71 | assert_msg [==[ 72 | Usage: [--] [OUTPUT-1 [OUTPUT-2]] 73 | ]==] 74 | end) 75 | 76 | it('prints a splat arg with reptitions > 2', function() 77 | cli:splat('OUTPUT', '...', nil, 5) 78 | 79 | assert_msg [==[ 80 | Usage: [--] [OUTPUT-1 [OUTPUT-2 [...]]] 81 | ]==] 82 | end) 83 | end) 84 | 85 | describe('#generate_help', function() 86 | local function assert_msg(expected_msg) 87 | local actual_msg = cli.printer.generate_help() 88 | 89 | assert.equal(trim(expected_msg), trim(actual_msg)) 90 | end 91 | 92 | it('works with nothing', function() 93 | assert_msg '' 94 | end) 95 | 96 | it('works with 1 argument', function() 97 | cli:argument('INPUT', 'path to the input file') 98 | 99 | assert_msg [==[ 100 | ARGUMENTS: 101 | INPUT path to the input file (required) 102 | ]==] 103 | end) 104 | 105 | it('works with 2+ arguments', function() 106 | cli:argument('INPUT', 'path to the input file') 107 | cli:argument('OUTPUT', 'path to the output file') 108 | 109 | assert_msg [==[ 110 | ARGUMENTS: 111 | INPUT path to the input file (required) 112 | OUTPUT path to the output file (required) 113 | ]==] 114 | end) 115 | 116 | it('works with 1 option', function() 117 | cli:option('--compress=VALUE', 'compression algorithm to use') 118 | 119 | assert_msg [==[ 120 | OPTIONS: 121 | --compress=VALUE compression algorithm to use 122 | ]==] 123 | end) 124 | 125 | it("prints an option's default value", function() 126 | cli:option('--compress=VALUE', 'compression algorithm to use', 'lzma') 127 | 128 | assert_msg [==[ 129 | OPTIONS: 130 | --compress=VALUE compression algorithm to use (default: lzma) 131 | ]==] 132 | end) 133 | 134 | it("prints a repeatable option", function() 135 | cli:option('--compress=VALUE', 'compression algorithm to use', { 'lzma' }) 136 | 137 | assert_msg [==[ 138 | OPTIONS: 139 | --compress=VALUE compression algorithm to use (default: []) 140 | ]==] 141 | end) 142 | 143 | it('works with many options', function() 144 | cli:option('--compress=VALUE', 'compression algorithm to use') 145 | cli:option('-u, --url=URL', '...') 146 | 147 | assert_msg [==[ 148 | OPTIONS: 149 | --compress=VALUE compression algorithm to use 150 | -u, --url=URL ... 151 | ]==] 152 | end) 153 | 154 | context('given a flag', function() 155 | it('prints it under OPTIONS', function() 156 | cli:flag('-q, --quiet', '...') 157 | 158 | assert_msg [==[ 159 | OPTIONS: 160 | -q, --quiet ... 161 | ]==] 162 | end) 163 | end) 164 | 165 | context('given a flag with a default value but is not negatable', function() 166 | it('does not print "on" or "off"', function() 167 | cli:flag('--quiet', '...', true) 168 | 169 | assert_msg [==[ 170 | OPTIONS: 171 | --quiet ... 172 | ]==] 173 | end) 174 | end) 175 | 176 | context('given a negatable flag', function() 177 | it('prints it along with its default value', function() 178 | cli:flag('--[no-]quiet', '...', true) 179 | cli:flag('--[no-]debug', '...', false) 180 | 181 | assert_msg [==[ 182 | OPTIONS: 183 | --[no-]quiet ... (default: on) 184 | --[no-]debug ... (default: off) 185 | ]==] 186 | end) 187 | end) 188 | 189 | context('given a splat arg', function() 190 | it('prints it with a repetition of 1', function() 191 | cli:splat("INPUTS", "directories to read from") 192 | assert_msg [==[ 193 | ARGUMENTS: 194 | INPUTS directories to read from (optional) 195 | ]==] 196 | end) 197 | 198 | it('prints it with a repetition of > 1', function() 199 | cli:splat("INPUTS", "directories to read from", nil, 3) 200 | assert_msg [==[ 201 | ARGUMENTS: 202 | INPUTS directories to read from (optional) 203 | ]==] 204 | end) 205 | 206 | it('prints it without a default value', function() 207 | cli:splat("INPUTS", "directories to read from") 208 | assert_msg [==[ 209 | ARGUMENTS: 210 | INPUTS directories to read from (optional) 211 | ]==] 212 | end) 213 | 214 | it('prints it with a default value', function() 215 | cli:splat("INPUTS", "directories to read from", 'foo') 216 | assert_msg [==[ 217 | ARGUMENTS: 218 | INPUTS directories to read from (optional, default: foo) 219 | ]==] 220 | end) 221 | end) 222 | end) 223 | 224 | describe('#dump_internal_state', function() 225 | local original_arg 226 | 227 | before_each(function() 228 | original_arg = _G['arg'] 229 | _G['arg'] = { 'spec/printer_spec.lua' } 230 | end) 231 | 232 | after_each(function() 233 | _G['arg'] = original_arg 234 | end) 235 | 236 | it('works', function() 237 | cli:argument('OUTPUT', '...') 238 | cli:splat('INPUTS', '...', nil, 100) 239 | cli:option('-c, --compress=VALUE', '...') 240 | cli:flag('-q, --quiet', '...', true) 241 | 242 | assert.equal(trim [==[ 243 | ======= Provided command line ============= 244 | 245 | Number of arguments: 246 | 1 = 'spec/printer_spec.lua' 247 | 248 | ======= Parsed command line =============== 249 | 250 | Arguments: 251 | OUTPUT => 'nil' 252 | 253 | Optional arguments:INPUTS; allowed are 100 arguments 254 | 255 | Optional parameters: 256 | -c, --compress=VALUE => nil (nil) 257 | -q, --quiet => nil (nil) 258 | 259 | =========================================== 260 | ]==], trim(cli.printer.dump_internal_state({}))) 261 | end) 262 | 263 | it('does not fail with an optarg of 1 reptitions', function() 264 | cli:splat('INPUTS', '...', nil, 1) 265 | cli.printer.dump_internal_state({}) 266 | end) 267 | 268 | it('does not fail with an optarg of many reptitions', function() 269 | cli:splat('INPUTS', '...', nil, 5) 270 | cli.printer.dump_internal_state({}) 271 | end) 272 | end) 273 | end) 274 | -------------------------------------------------------------------------------- /spec/spec_helper.lua: -------------------------------------------------------------------------------- 1 | -- luacheck: ignore 111 2 | 3 | local exports = {} 4 | local split = require 'cliargs.utils.split' 5 | local busted = require 'busted' 6 | 7 | function odescribe(desc, runner) 8 | busted.describe("#only " .. desc, runner) 9 | end 10 | 11 | function xdescribe() 12 | end 13 | 14 | function oit(desc, runner) 15 | busted.it("#only " .. desc, runner) 16 | end 17 | 18 | function xit(desc, _) 19 | busted.it(desc) 20 | end 21 | 22 | exports.parse = function(cli, str) 23 | return cli:parse(split(str, '%s+')) 24 | end 25 | 26 | exports.trim = function(s) 27 | local lines = split(s, "\n") 28 | local _ 29 | 30 | if #lines == 0 then 31 | return s 32 | end 33 | 34 | local padding = lines[1]:find('%S') or 0 35 | local buffer = '' 36 | 37 | for _, line in pairs(lines) do 38 | buffer = buffer .. line:sub(padding, -1):gsub("%s+$", '') .. "\n" 39 | end 40 | 41 | return buffer:gsub("%s+$", '') 42 | end 43 | 44 | return exports 45 | -------------------------------------------------------------------------------- /spec/utils/disect_argument_spec.lua: -------------------------------------------------------------------------------- 1 | dofile("spec/spec_helper.lua") 2 | local disect_argument = require('cliargs.utils.disect_argument') 3 | 4 | describe("utils::disect_argument", function() 5 | local function assert_disect(pattern, expected) 6 | it("works with '" .. pattern .. "'", function() 7 | local symbol, key, value, negated = disect_argument(pattern) 8 | 9 | assert.equal(symbol, expected[1]) 10 | assert.equal(key, expected[2]) 11 | assert.equal(value, expected[3]) 12 | assert.equal(negated, expected[4]) 13 | end) 14 | end 15 | 16 | -- flags 17 | assert_disect("", { nil, nil, nil, false }) 18 | assert_disect("-q", { '-', 'q', nil, false }) 19 | assert_disect("--quiet", { '--', 'quiet', nil, false }) 20 | 21 | -- -- -- -- flag negation 22 | assert_disect("--no-quiet", { '--', 'quiet', nil, true }) 23 | assert_disect("--no-q", { '--', 'q', nil, true }) 24 | 25 | -- -- options 26 | assert_disect("-v=VALUE", { '-', 'v', 'VALUE', false }) 27 | assert_disect("--value=VALUE", { '--', 'value', 'VALUE', false }) 28 | assert_disect('--value=with whitespace', { '--', 'value', 'with whitespace', false }) 29 | 30 | -- -- end-of-options indicator 31 | assert_disect('--', { '--', nil, nil, false }) 32 | 33 | -- -- values 34 | assert_disect('value', { nil, nil, 'value', false }) 35 | assert_disect('/path/to/something', { nil, nil, '/path/to/something', false }) 36 | assert_disect('oops-look-at--me', { nil, nil, 'oops-look-at--me', false }) 37 | end) 38 | -------------------------------------------------------------------------------- /spec/utils/disect_spec.lua: -------------------------------------------------------------------------------- 1 | local disect = require('cliargs.utils.disect') 2 | 3 | describe("utils::disect", function() 4 | local function assert_disect(pattern, expected) 5 | it("works with '" .. pattern .. "'", function() 6 | local k, ek, v = disect(pattern) 7 | 8 | assert.equal(k, expected[1]) 9 | assert.equal(ek, expected[2]) 10 | assert.equal(v, expected[3]) 11 | end) 12 | end 13 | 14 | assert_disect("-q", { 'q', nil, nil }) 15 | assert_disect("-Wno-unsigned", { 'Wno-unsigned', nil, nil }) 16 | assert_disect("-q, --quiet", { 'q', 'quiet', nil }) 17 | assert_disect("-q --quiet", { 'q', 'quiet', nil }) 18 | 19 | -- now with value indicators 20 | assert_disect("-v VALUE", { 'v', nil, 'VALUE' }) 21 | assert_disect("-v=VALUE", { 'v', nil, 'VALUE' }) 22 | 23 | assert_disect("--value VALUE", { nil, 'value', 'VALUE' }) 24 | assert_disect("--value=VALUE", { nil, 'value', 'VALUE' }) 25 | 26 | assert_disect("-v --value=VALUE", { 'v', 'value', 'VALUE' }) 27 | assert_disect("-v --value VALUE", { 'v', 'value', 'VALUE' }) 28 | assert_disect("-v, --value=VALUE", { 'v', 'value', 'VALUE' }) 29 | assert_disect("-v, --value VALUE", { 'v', 'value', 'VALUE' }) 30 | end) 31 | -------------------------------------------------------------------------------- /spec/utils/split_spec.lua: -------------------------------------------------------------------------------- 1 | local subject = require('cliargs.utils.split') 2 | 3 | describe("utils::split", function() 4 | it("should work", function() 5 | -- takes: str, split-char 6 | local expected, result 7 | 8 | result = subject("hello,world",",") 9 | expected = {"hello", "world"} 10 | assert.is.same(result, expected) 11 | 12 | result = subject("hello,world,",",") 13 | expected = {"hello", "world"} 14 | assert.is.same(result, expected) 15 | 16 | result = subject("hello",",") 17 | expected = {"hello"} 18 | assert.is.same(result, expected) 19 | 20 | result = subject("",",") 21 | expected = {} 22 | assert.is.same(result, expected) 23 | end) 24 | end) 25 | -------------------------------------------------------------------------------- /spec/utils/wordwrap_spec.lua: -------------------------------------------------------------------------------- 1 | local subject = require('cliargs.utils.wordwrap') 2 | 3 | describe("utils::wordwrap", function() 4 | it("should work", function() 5 | -- takes: text, size, padding 6 | local text = "123456789 123456789 123456789!" 7 | local expected, result 8 | 9 | result = subject(text, 10) 10 | expected = { "123456789", "123456789", "123456789!" } 11 | assert.is.same(result, expected) 12 | 13 | -- exact length + 1 overflow 14 | result = subject(text, 9) 15 | expected = { "123456789", "123456789", "123456789", "!" } 16 | assert.is.same(result, expected) 17 | 18 | result = subject(text, 9, true) 19 | expected = { "123456789", "123456789", "123456789!" } 20 | assert.is.same(result, expected) 21 | 22 | result = subject(text, 8) 23 | expected = { "12345678", "9", "12345678", "9", "12345678", "9!" } 24 | assert.is.same(result, expected) 25 | end) 26 | end) 27 | -------------------------------------------------------------------------------- /src/cliargs.lua: -------------------------------------------------------------------------------- 1 | -- luacheck: ignore 212 2 | 3 | local core = require('cliargs.core')() 4 | local unpack = _G.unpack or table.unpack -- luacheck: compat 5 | 6 | local cli = setmetatable({},{ __index = core }) 7 | 8 | function cli:parse(arguments, no_cleanup) 9 | if not no_cleanup then 10 | cli:cleanup() 11 | end 12 | 13 | local out = { core.parse(self, arguments) } 14 | 15 | return unpack(out) 16 | end 17 | 18 | -- Clean up the entire module (unload the scripts) as it's expected to be 19 | -- discarded after use. 20 | function cli:cleanup() 21 | for k, v in pairs(package.loaded) do 22 | if (v == cli) or (k:match('cliargs')) then 23 | package.loaded[k] = nil 24 | end 25 | end 26 | 27 | cli = nil 28 | end 29 | 30 | cli.VERSION = "3.0.2" 31 | 32 | return cli 33 | -------------------------------------------------------------------------------- /src/cliargs/config_loader.lua: -------------------------------------------------------------------------------- 1 | local trim = require 'cliargs.utils.trim' 2 | 3 | local function read_file(filepath) 4 | local f, err = io.open(filepath, "r") 5 | 6 | if not f then 7 | return nil, err 8 | end 9 | 10 | local contents = f:read('*all') 11 | 12 | f:close() 13 | 14 | return contents 15 | end 16 | 17 | return { 18 | FORMAT_LOADERS = { 19 | ["lua"] = "from_lua", 20 | ["json"] = "from_json", 21 | ["yaml"] = "from_yaml", 22 | ["yml"] = "from_yaml", 23 | ["ini"] = "from_ini", 24 | }, 25 | 26 | --- Load configuration from a Lua file that exports a table. 27 | from_lua = function(filepath) 28 | local file, err = loadfile(filepath) 29 | 30 | if not file and err then 31 | return nil, err 32 | end 33 | 34 | return file() 35 | end, 36 | 37 | --- Load configuration from a JSON file. 38 | --- 39 | --- Requires the "dkjson"[1] module to be present on the system. Get it with: 40 | --- 41 | --- luarocks install dkjson 42 | --- 43 | --- [1] http://dkolf.de/src/dkjson-lua.fsl/home 44 | from_json = function(filepath) 45 | local src, config, _, err 46 | local json = require 'dkjson' 47 | 48 | src, err = read_file(filepath) 49 | 50 | if not src and err then 51 | return nil, err 52 | end 53 | 54 | config, _, err = json.decode(src) 55 | 56 | if err then 57 | return nil, err 58 | end 59 | 60 | return config 61 | end, 62 | 63 | --- Load configuration from an INI file. 64 | --- 65 | --- Requires the "inifile"[1] module to be present on the system. Get it with: 66 | --- 67 | --- luarocks install inifile 68 | --- 69 | --- The INI file must contain a group that lists the default values. For 70 | --- example: 71 | --- 72 | --- [cli] 73 | --- quiet = true 74 | --- compress = lzma 75 | --- 76 | --- The routine will automatically cast boolean values ("true" and "false") 77 | --- into Lua booleans. You may opt out of this behavior by passing `false` 78 | --- to `no_cast`. 79 | --- 80 | --- [1] http://docs.bartbes.com/inifile 81 | from_ini = function(filepath, group, no_cast) 82 | local inifile = require 'inifile' 83 | local config, err 84 | 85 | group = group or 'cli' 86 | 87 | assert(type(group) == 'string', 88 | 'You must provide an INI group to read from.' 89 | ) 90 | 91 | config, err = inifile.parse(filepath) 92 | 93 | if not config and err then 94 | return nil, err 95 | end 96 | 97 | if not no_cast then 98 | for k, src_value in pairs(config[group]) do 99 | local v = trim(src_value) 100 | 101 | if v == 'true' then 102 | v = true 103 | elseif v == 'false' then 104 | v = false 105 | end 106 | 107 | config[group][k] = v 108 | end 109 | end 110 | 111 | return config[group] 112 | end, 113 | 114 | --- Load configuration from a YAML file. 115 | --- 116 | --- Requires the "yaml"[1] module to be present on the system. Get it with: 117 | --- 118 | --- luarocks install yaml 119 | --- 120 | --- [1] http://doc.lubyk.org/yaml.html 121 | from_yaml = function(filepath) 122 | local src, config, err 123 | local yaml = require 'yaml' 124 | 125 | src, err = read_file(filepath) 126 | 127 | if not src and err then 128 | return nil, err 129 | end 130 | 131 | config, err = yaml.load(src) 132 | 133 | if not config and err then 134 | return nil, err 135 | end 136 | 137 | return config 138 | end 139 | } 140 | -------------------------------------------------------------------------------- /src/cliargs/constants.lua: -------------------------------------------------------------------------------- 1 | return { 2 | TYPE_COMMAND = 'command', 3 | TYPE_ARGUMENT = 'argument', 4 | TYPE_SPLAT = 'splat', 5 | TYPE_OPTION = 'option', 6 | } 7 | -------------------------------------------------------------------------------- /src/cliargs/core.lua: -------------------------------------------------------------------------------- 1 | -- luacheck: ignore 212 2 | 3 | local _ 4 | local disect = require('cliargs.utils.disect') 5 | local lookup = require('cliargs.utils.lookup') 6 | local filter = require('cliargs.utils.filter') 7 | local shallow_copy = require('cliargs.utils.shallow_copy') 8 | local create_printer = require('cliargs.printer') 9 | local config_loader = require('cliargs.config_loader') 10 | local parser = require('cliargs.parser') 11 | local K = require 'cliargs.constants' 12 | 13 | local function is_callable(fn) 14 | return type(fn) == "function" or (getmetatable(fn) or {}).__call 15 | end 16 | 17 | local function cast_to_boolean(v) 18 | if v == nil then 19 | return v 20 | else 21 | return v and true or false 22 | end 23 | end 24 | 25 | -- -------- -- 26 | -- CLI Main -- 27 | -- -------- -- 28 | local function create_core() 29 | --- @module 30 | --- 31 | --- The primary export you receive when you require the library. For example: 32 | --- 33 | --- local cli = require 'cliargs' 34 | local cli = {} 35 | local colsz = { 0, 0 } -- column width, help text. Set to 0 for auto detect 36 | local options = {} 37 | 38 | cli.name = "" 39 | cli.description = "" 40 | 41 | cli.printer = create_printer(function() 42 | return { 43 | name = cli.name, 44 | description = cli.description, 45 | options = options, 46 | colsz = colsz 47 | } 48 | end) 49 | 50 | -- Used internally to add an option 51 | local function define_option(k, ek, v, label, desc, default, callback) 52 | local flag = (v == nil) -- no value, so it's a flag 53 | local negatable = flag and (ek and ek:find('^%[no%-]') ~= nil) 54 | 55 | if negatable then 56 | ek = ek:sub(6) 57 | end 58 | 59 | -- guard against duplicates 60 | if lookup(k, ek, options) then 61 | error("Duplicate option: " .. (k or ek) .. ", please rename one of them.") 62 | end 63 | 64 | if negatable and lookup(nil, "no-"..ek, options) then 65 | error("Duplicate option: " .. ("no-"..ek) .. ", please rename one of them.") 66 | end 67 | 68 | -- below description of full entry record, nils included for reference 69 | local entry = { 70 | type = K.TYPE_OPTION, 71 | key = k, 72 | expanded_key = ek, 73 | desc = desc, 74 | default = default, 75 | label = label, 76 | flag = flag, 77 | negatable = negatable, 78 | callback = callback 79 | } 80 | 81 | table.insert(options, entry) 82 | end 83 | 84 | local function define_command_option(key) 85 | --- @module 86 | --- 87 | --- This is a special instance of the [cli]() module that you receive when 88 | --- you define a new command using [cli#command](). 89 | local cmd = create_core() 90 | 91 | cmd.__key__ = key 92 | cmd.type = K.TYPE_COMMAND 93 | 94 | --- Specify a file that the command should run. The rest of the arguments 95 | --- are forward to that file to process, which is free to use or not use 96 | --- lua_cliargs in turn. 97 | --- 98 | --- @param {string} file_path 99 | --- Absolute file-path to a lua script to execute. 100 | function cmd:file(file_path) 101 | cmd.__file__ = file_path 102 | return cmd 103 | end 104 | 105 | --- Define a command handler. This callback will be invoked if the command 106 | --- argument was supplied by the user at runtime. What you return from this 107 | --- callback will be returned to the parent CLI library's parse routine and 108 | --- it will return that in turn! 109 | --- 110 | --- @param {function} callback 111 | function cmd:action(callback) 112 | cmd.__action__ = callback 113 | return cmd 114 | end 115 | 116 | return cmd 117 | end 118 | 119 | -- ------------------------------------------------------------------------ -- 120 | -- PUBLIC API 121 | -- ------------------------------------------------------------------------ -- 122 | 123 | --- CONFIG 124 | 125 | --- Assigns the name of the program which will be used for logging. 126 | function cli:set_name(in_name) 127 | cli.name = in_name 128 | 129 | return self 130 | end 131 | 132 | --- Write down a brief, 1-liner description of what the program does. 133 | function cli:set_description(in_description) 134 | cli.description = in_description 135 | 136 | return self 137 | end 138 | 139 | --- Sets the amount of space allocated to the argument keys and descriptions 140 | --- in the help listing. 141 | --- 142 | --- The sizes are used for wrapping long argument keys and descriptions. 143 | --- 144 | --- @param {number} [key_cols=0] 145 | --- The number of columns assigned to the argument keys, set to 0 to 146 | --- auto detect. 147 | --- 148 | --- @param {number} [desc_cols=0] 149 | --- The number of columns assigned to the argument descriptions, set to 150 | --- 0 to auto set the total width to 72. 151 | function cli:set_colsz(key_cols, desc_cols) 152 | colsz = { key_cols or colsz[1], desc_cols or colsz[2] } 153 | end 154 | 155 | function cli:redefine_default(key, new_default) 156 | local entry = lookup(key, key, options) 157 | 158 | if not entry then 159 | return nil 160 | end 161 | 162 | if entry.flag then 163 | new_default = cast_to_boolean(new_default) 164 | end 165 | 166 | entry.default = shallow_copy(new_default) 167 | 168 | return true 169 | end 170 | 171 | --- Load default values from a table. 172 | --- 173 | --- @param {table} config 174 | --- Your new set of defaults. The keys could either point to the short 175 | --- or expanded option keys, and their values are the new defaults. 176 | --- 177 | --- @param {boolean} [strict=false] 178 | --- Turn this on to return nil and an error message if a key in the 179 | --- config table could not be mapped to any CLI option. 180 | --- 181 | --- @return {true} 182 | --- When the new defaults were loaded successfully, or strict was not 183 | --- set. 184 | --- 185 | --- @return {union} 186 | --- When strict was set and there was an error. 187 | function cli:load_defaults(config, strict) 188 | for k, v in pairs(config) do 189 | local success = self:redefine_default(k, v) 190 | 191 | if strict and not success then 192 | return nil, "Unrecognized option with the key '" .. k .. "'" 193 | end 194 | end 195 | 196 | return true 197 | end 198 | 199 | --- Read config values from a configuration file. 200 | --- 201 | --- @param {string} path 202 | --- Absolute file path. 203 | --- 204 | --- @param {string} [format=nil] 205 | --- The config file format, which has to be one of: 206 | --- "lua", "json", "ini", or "yaml". 207 | --- When this is left blank, we try to auto-detect the format from the 208 | --- file extension. 209 | --- 210 | --- @param {string} [group="cli"] 211 | --- INI files only: group that lists the default values. For example: 212 | --- 213 | --- [cli] 214 | --- quiet=true 215 | --- compress=lzma 216 | --- 217 | --- @param {bool} [default=true] 218 | --- INI files only: whether or not boolean values ("true" and "false") 219 | --- will be cast into Lua booleans automatically. If set to false, 220 | --- string values "true" or "false" will be assigned to config value. 221 | --- 222 | --- @return {true|union} 223 | --- Returns true on successful load. Otherwise, nil and an error 224 | --- message are returned instead. 225 | function cli:read_defaults(path, format, group, no_cast) 226 | if not format then 227 | format = path:match('%.([^%.]+)$') 228 | end 229 | 230 | local loader = config_loader.FORMAT_LOADERS[format] 231 | 232 | if not loader then 233 | return nil, 'Unsupported file format "' .. format .. '"' 234 | end 235 | 236 | return config_loader[loader](path, group, no_cast) 237 | end 238 | 239 | --- Define a required argument. 240 | --- 241 | --- 242 | --- Required arguments do not take a symbol like `-` or `--`, may not have a 243 | --- default value, and are parsed in the order they are defined. 244 | --- 245 | --- 246 | --- For example: 247 | --- 248 | --- ```lua 249 | --- cli:argument('INPUT', 'path to the input file') 250 | --- cli:argument('OUTPUT', 'path to the output file') 251 | --- ``` 252 | --- 253 | --- At run-time, the arguments have to be specified using the following 254 | --- notation: 255 | --- 256 | --- ```bash 257 | --- $ ./script.lua ./main.c ./a.out 258 | --- ``` 259 | --- 260 | --- If the user does not pass a value to _every_ argument, the parser will 261 | --- raise an error. 262 | --- 263 | --- @param {string} key 264 | --- 265 | --- The argument identifier that will be displayed to the user and 266 | --- be used to reference the run-time value. 267 | --- 268 | --- @param {string} desc 269 | --- 270 | --- A description for this argument to display in usage help. 271 | --- 272 | --- @param {function} [callback] 273 | --- Callback to invoke when this argument is parsed. 274 | function cli:argument(key, desc, callback) 275 | assert(type(key) == "string" and type(desc) == "string", 276 | "Key and description are mandatory arguments (Strings)" 277 | ) 278 | 279 | assert(callback == nil or is_callable(callback), 280 | "Callback argument must be a function" 281 | ) 282 | 283 | if lookup(key, key, options) then 284 | error("Duplicate argument: " .. key .. ", please rename one of them.") 285 | end 286 | 287 | table.insert(options, { 288 | type = K.TYPE_ARGUMENT, 289 | key = key, 290 | desc = desc, 291 | callback = callback 292 | }) 293 | 294 | return self 295 | end 296 | 297 | --- Defines a "splat" (or catch-all) argument. 298 | --- 299 | --- This is a special kind of argument that may be specified 0 or more times, 300 | --- the values being appended to a list. 301 | --- 302 | --- For example, let's assume our program takes a single output file and works 303 | --- on multiple source files: 304 | --- 305 | --- ```lua 306 | --- cli:argument('OUTPUT', 'path to the output file') 307 | --- cli:splat('INPUTS', 'the sources to compile', nil, 10) -- up to 10 source files 308 | --- ``` 309 | --- 310 | --- At run-time, it could be invoked as such: 311 | --- 312 | --- ```bash 313 | --- $ ./script.lua ./a.out file1.c file2.c main.c 314 | --- ``` 315 | --- 316 | --- If you want to make the output optional, you could do something like this: 317 | --- 318 | --- ```lua 319 | --- cli:option('-o, --output=FILE', 'path to the output file', './a.out') 320 | --- cli:splat('INPUTS', 'the sources to compile', nil, 10) 321 | --- ``` 322 | --- 323 | --- And now we may omit the output file path: 324 | --- 325 | --- ```bash 326 | --- $ ./script.lua file1.c file2.c main.c 327 | --- ``` 328 | --- 329 | --- @param {string} key 330 | --- The argument's "name" that will be displayed to the user. 331 | --- 332 | --- @param {string} desc 333 | --- A description of the argument. 334 | --- 335 | --- @param {*} [default=nil] 336 | --- A default value. 337 | --- 338 | --- @param {number} [maxcount=1] 339 | --- The maximum number of occurences allowed. 340 | --- 341 | --- @param {function} [callback] 342 | --- A function to call **everytime** a value for this argument is 343 | --- parsed. 344 | --- 345 | function cli:splat(key, desc, default, maxcount, callback) 346 | assert(#filter(options, 'type', K.TYPE_SPLAT) == 0, 347 | "Only one splat argument may be defined." 348 | ) 349 | 350 | assert(type(key) == "string" and type(desc) == "string", 351 | "Key and description are mandatory arguments (Strings)" 352 | ) 353 | 354 | assert(type(default) == "string" or default == nil, 355 | "Default value must either be omitted or be a string" 356 | ) 357 | 358 | maxcount = tonumber(maxcount or 1) 359 | 360 | assert(maxcount > 0 and maxcount < 1000, 361 | "Maxcount must be a number from 1 to 999" 362 | ) 363 | 364 | assert(is_callable(callback) or callback == nil, 365 | "Callback argument: expected a function or nil" 366 | ) 367 | 368 | local typed_default = default or {} 369 | 370 | if type(typed_default) ~= 'table' then 371 | typed_default = { typed_default } 372 | end 373 | 374 | table.insert(options, { 375 | type = K.TYPE_SPLAT, 376 | key = key, 377 | desc = desc, 378 | default = typed_default, 379 | maxcount = maxcount, 380 | callback = callback 381 | }) 382 | 383 | return self 384 | end 385 | 386 | --- Defines an optional argument. 387 | --- 388 | --- Optional arguments can use 3 different notations, and can accept a value. 389 | --- 390 | --- @param {string} key 391 | --- 392 | --- The argument identifier. This can either be `-key`, or 393 | --- `-key, --expanded-key`. 394 | --- Values can be specified either by appending a space after the 395 | --- identifier (e.g. `-key value` or `--expanded-key value`) or by 396 | --- separating them with a `=` (e.g. `-key=value` or 397 | --- `--expanded-key=value`). 398 | --- 399 | --- @param {string} desc 400 | --- 401 | --- A description for the argument to be shown in --help. 402 | --- 403 | --- @param {bool} [default=nil] 404 | --- 405 | --- A default value to use in case the option was not specified at 406 | --- run-time (the default value is nil if you leave this blank.) 407 | --- 408 | --- @param {function} [callback] 409 | --- 410 | --- A callback to invoke when this option is parsed. 411 | --- 412 | --- @example 413 | --- 414 | --- The following option will be stored in `args["i"]` and `args["input"]` 415 | --- with a default value of `file.txt`: 416 | --- 417 | --- cli:option("-i, --input=FILE", "path to the input file", "file.txt") 418 | function cli:option(key, desc, default, callback) 419 | assert(type(key) == "string" and type(desc) == "string", 420 | "Key and description are mandatory arguments (Strings)" 421 | ) 422 | 423 | assert(is_callable(callback) or callback == nil, 424 | "Callback argument: expected a function or nil" 425 | ) 426 | 427 | local k, ek, v = disect(key) 428 | 429 | -- if there's no VALUE indicator anywhere, what they want really is a flag. 430 | -- e.g: 431 | -- 432 | -- cli:option('-q, --quiet', '...') 433 | if v == nil then 434 | return self:flag(key, desc, default, callback) 435 | end 436 | 437 | define_option(k, ek, v, key, desc, default, callback) 438 | 439 | return self 440 | end 441 | 442 | --- Define an optional "flag" argument. 443 | --- 444 | --- Flags are a special subset of options that can either be `true` or `false`. 445 | --- 446 | --- For example: 447 | --- ```lua 448 | --- cli:flag('-q, --quiet', 'Suppress output.', true) 449 | --- ``` 450 | --- 451 | --- At run-time: 452 | --- 453 | --- ```bash 454 | --- $ ./script.lua --quiet 455 | --- $ ./script.lua -q 456 | --- ``` 457 | --- 458 | --- Passing a value to a flag raises an error: 459 | --- 460 | --- ```bash 461 | --- $ ./script.lua --quiet=foo 462 | --- $ echo $? # => 1 463 | --- ``` 464 | --- 465 | --- Flags may be _negatable_ by prepending `[no-]` to their key: 466 | --- 467 | --- ```lua 468 | --- cli:flag('-c, --[no-]compress', 'whether to compress or not', true) 469 | --- ``` 470 | --- 471 | --- Now the user gets to pass `--no-compress` if they want to skip 472 | --- compression, or either specify `--compress` explicitly or leave it 473 | --- unspecified to use compression. 474 | --- 475 | --- @param {string} key 476 | --- @param {string} desc 477 | --- @param {*} default 478 | --- @param {function} callback 479 | function cli:flag(key, desc, default, callback) 480 | if type(default) == "function" then 481 | callback = default 482 | default = nil 483 | end 484 | 485 | assert(type(key) == "string" and type(desc) == "string", 486 | "Key and description are mandatory arguments (Strings)" 487 | ) 488 | 489 | local k, ek, v = disect(key) 490 | 491 | if v ~= nil then 492 | error("A flag type option cannot have a value set: " .. key) 493 | end 494 | 495 | define_option(k, ek, nil, key, desc, cast_to_boolean(default), callback) 496 | 497 | return self 498 | end 499 | 500 | --- Define a command argument. 501 | --- 502 | --- @param {string} name 503 | --- The name of the command and the argument that the user has to 504 | --- supply to invoke it. 505 | --- 506 | --- @param {string} [desc] 507 | --- An optional string to show in the help listing which should 508 | --- describe what the command does. It will be displayed if --help 509 | --- was run on the main program. 510 | --- 511 | --- 512 | --- @return {cmd} 513 | --- Another instance of the CLI library which is scoped to that 514 | --- command. 515 | function cli:command(name, desc) 516 | local cmd = define_command_option(name) 517 | 518 | cmd:set_name(cli.name .. ' ' .. name) 519 | cmd:set_description(desc) 520 | 521 | table.insert(options, cmd) 522 | 523 | return cmd 524 | end 525 | 526 | --- Parse the process arguments table. 527 | --- 528 | --- @param {table} [arguments=_G.arg] 529 | --- The list of arguments to parse. Defaults to the global `arg` table 530 | --- which contains the arguments the process was started with. 531 | --- 532 | --- @return {table} 533 | --- A table containing all the arguments, options, flags, 534 | --- and splat arguments that were specified or had a default 535 | --- (where applicable). 536 | --- 537 | --- @return {array} 538 | --- If a parsing error has occured, note that the --help option is 539 | --- also considered an error. 540 | function cli:parse(arguments) 541 | return parser(arguments, options, cli.printer) 542 | end 543 | 544 | --- Prints the USAGE message. 545 | --- 546 | --- @return {string} 547 | --- The USAGE message. 548 | function cli:print_usage() 549 | cli.printer.print(cli:get_usage_message()) 550 | end 551 | 552 | function cli:get_usage_message() 553 | return cli.printer.generate_usage() 554 | end 555 | 556 | --- Prints the HELP information. 557 | --- 558 | --- @return {string} 559 | --- The HELP message. 560 | function cli:print_help() 561 | cli.printer.print(cli.printer.generate_help_and_usage()) 562 | end 563 | 564 | return cli 565 | end 566 | 567 | return create_core 568 | -------------------------------------------------------------------------------- /src/cliargs/parser.lua: -------------------------------------------------------------------------------- 1 | local K = require 'cliargs.constants' 2 | 3 | ------------------------------------------------------------------------------- 4 | -- UTILS 5 | ------------------------------------------------------------------------------- 6 | local shallow_copy = require 'cliargs.utils.shallow_copy' 7 | local filter = require 'cliargs.utils.filter' 8 | local disect_argument = require 'cliargs.utils.disect_argument' 9 | local lookup = require 'cliargs.utils.lookup' 10 | 11 | local function clone_table_shift(t) 12 | local clone = shallow_copy(t) 13 | table.remove(clone, 1) 14 | return clone 15 | end 16 | 17 | local function clone_table_remove(t, index) 18 | local clone = shallow_copy(t) 19 | table.remove(clone, index) 20 | return clone 21 | end 22 | 23 | ------------------------------------------------------------------------------- 24 | -- PARSE ROUTINES 25 | ------------------------------------------------------------------------------- 26 | local p = {} 27 | function p.invoke_command(args, options, done) 28 | local commands = filter(options, 'type', K.TYPE_COMMAND) 29 | 30 | for index, opt in ipairs(args) do 31 | local command = filter(commands, '__key__', opt)[1] 32 | 33 | if command then 34 | local command_args = clone_table_remove(args, index) 35 | 36 | if command.__action__ then 37 | local parsed_command_args, err = command:parse(command_args) 38 | 39 | if err then 40 | return nil, err 41 | end 42 | 43 | return command.__action__(parsed_command_args) 44 | elseif command.__file__ then 45 | local filename = command.__file__ 46 | 47 | if type(filename) == 'function' then 48 | filename = filename() 49 | end 50 | 51 | local run_command_file = function() 52 | _G.arg = command_args 53 | 54 | local res, err = assert(loadfile(filename))() 55 | 56 | _G.arg = args 57 | 58 | return res, err 59 | end 60 | 61 | return run_command_file() 62 | end 63 | end 64 | end 65 | 66 | return done() 67 | end 68 | 69 | function p.print_help(args, printer, done) 70 | -- has --help or -h ? display the help listing and abort! 71 | for _, v in pairs(args) do 72 | if v == "--help" or v == "-h" then 73 | return nil, printer.generate_help_and_usage() 74 | end 75 | end 76 | 77 | return done() 78 | end 79 | 80 | function p.track_dump_request(args, done) 81 | -- starts with --__DUMP__; set dump to true to dump the parsed arguments 82 | if args[1] == "--__DUMP__" then 83 | return done(true, clone_table_shift(args)) 84 | else 85 | return done(false, args) 86 | end 87 | end 88 | 89 | function p.process_arguments(args, options, done) 90 | local values = {} 91 | local cursor = 0 92 | local argument_cursor = 1 93 | local argument_delimiter_found = false 94 | local function consume() 95 | cursor = cursor + 1 96 | 97 | return args[cursor] 98 | end 99 | 100 | local required = filter(options, 'type', K.TYPE_ARGUMENT) 101 | 102 | while cursor < #args do 103 | local curr_opt = consume() 104 | local symbol, key, value, flag_negated = disect_argument(curr_opt) 105 | 106 | -- end-of-options indicator: 107 | if curr_opt == "--" then 108 | argument_delimiter_found = true 109 | 110 | -- an option: 111 | elseif not argument_delimiter_found and symbol then 112 | local entry = lookup(key, key, options) 113 | 114 | if not key or not entry then 115 | local option_type = value and "option" or "flag" 116 | 117 | return nil, "unknown/bad " .. option_type .. ": " .. curr_opt 118 | end 119 | 120 | if flag_negated and not entry.negatable then 121 | return nil, "flag '" .. curr_opt .. "' may not be negated using --no-" 122 | end 123 | 124 | -- a flag and a value specified? that's an error 125 | if entry.flag and value then 126 | return nil, "flag " .. curr_opt .. " does not take a value" 127 | elseif entry.flag then 128 | value = not flag_negated 129 | -- an option: 130 | else 131 | -- the value might be in the next argument, e.g: 132 | -- 133 | -- --compress lzma 134 | if not value then 135 | -- if the option contained a = and there's no value, it means they 136 | -- want to nullify an option's default value. eg: 137 | -- 138 | -- --compress= 139 | if curr_opt:find('=') then 140 | value = '__CLIARGS_NULL__' 141 | else 142 | -- NOTE: this has the potential to be buggy and swallow the next 143 | -- entry as this entry's value even though that entry may be an 144 | -- actual argument/option 145 | -- 146 | -- this would be a user error and there is no determinate way to 147 | -- figure it out because if there's no leading symbol (- or --) 148 | -- in that entry it can be an actual argument. :shrug: 149 | value = consume() 150 | 151 | if not value then 152 | return nil, "option " .. curr_opt .. " requires a value to be set" 153 | end 154 | end 155 | end 156 | end 157 | 158 | table.insert(values, { entry = entry, value = value }) 159 | 160 | if entry.callback then 161 | local altkey = entry.key 162 | local status, err 163 | 164 | if key == entry.key then 165 | altkey = entry.expanded_key 166 | else 167 | key = entry.expanded_key 168 | end 169 | 170 | status, err = entry.callback(key, value, altkey, curr_opt) 171 | 172 | if status == nil and err then 173 | return nil, err 174 | end 175 | end 176 | 177 | -- a regular argument: 178 | elseif argument_cursor <= #required then 179 | local entry = required[argument_cursor] 180 | 181 | table.insert(values, { entry = entry, value = curr_opt }) 182 | 183 | if entry.callback then 184 | local status, err = entry.callback(entry.key, curr_opt) 185 | 186 | if status == nil and err then 187 | return nil, err 188 | end 189 | end 190 | 191 | argument_cursor = argument_cursor + 1 192 | 193 | -- a splat argument: 194 | else 195 | local entry = filter(options, 'type', K.TYPE_SPLAT)[1] 196 | 197 | if entry then 198 | table.insert(values, { entry = entry, value = curr_opt }) 199 | 200 | if entry.callback then 201 | local status, err = entry.callback(entry.key, curr_opt) 202 | 203 | if status == nil and err then 204 | return nil, err 205 | end 206 | end 207 | end 208 | 209 | argument_cursor = argument_cursor + 1 210 | end 211 | end 212 | 213 | return done(values, argument_cursor - 1) 214 | end 215 | 216 | function p.validate(options, arg_count, done) 217 | local required = filter(options, 'type', K.TYPE_ARGUMENT) 218 | local splatarg = filter(options, 'type', K.TYPE_SPLAT)[1] or { maxcount = 0 } 219 | 220 | local min_arg_count = #required 221 | local max_arg_count = #required + splatarg.maxcount 222 | 223 | -- missing any required arguments, or too many? 224 | if arg_count < min_arg_count or arg_count > max_arg_count then 225 | if splatarg.maxcount > 0 then 226 | return nil, ( 227 | "bad number of arguments: " .. 228 | min_arg_count .. "-" .. max_arg_count .. 229 | " argument(s) must be specified, not " .. arg_count 230 | ) 231 | else 232 | return nil, ( 233 | "bad number of arguments: " .. 234 | min_arg_count .. " argument(s) must be specified, not " .. arg_count 235 | ) 236 | end 237 | end 238 | 239 | return done() 240 | end 241 | 242 | function p.collect_results(cli_values, options) 243 | local results = {} 244 | local function collect_with_default(entry) 245 | local entry_values = {} 246 | local _ 247 | 248 | for _, item in ipairs(cli_values) do 249 | if item.entry == entry then 250 | table.insert(entry_values, item.value) 251 | end 252 | end 253 | 254 | if #entry_values == 0 then 255 | return type(entry.default) == 'table' and entry.default or { entry.default } 256 | else 257 | return entry_values 258 | end 259 | end 260 | 261 | local function write(entry, value) 262 | if entry.key then results[entry.key] = value end 263 | if entry.expanded_key then results[entry.expanded_key] = value end 264 | end 265 | 266 | for _, entry in pairs(options) do 267 | local entry_cli_values = collect_with_default(entry) 268 | local maxcount = entry.maxcount 269 | 270 | if maxcount == nil then 271 | maxcount = type(entry.default) == 'table' and 999 or 1 272 | end 273 | 274 | local entry_value = entry_cli_values 275 | 276 | if maxcount == 1 and type(entry_cli_values) == 'table' then 277 | -- take the last value 278 | entry_value = entry_cli_values[#entry_cli_values] 279 | 280 | if entry_value == '__CLIARGS_NULL__' then 281 | entry_value = nil 282 | end 283 | end 284 | 285 | write(entry, entry_value) 286 | end 287 | 288 | return results 289 | end 290 | 291 | 292 | return function(arguments, options, printer) 293 | assert(arguments == nil or type(arguments) == "table", 294 | "expected an argument table to be passed in, " .. 295 | "got something of type " .. type(arguments) 296 | ) 297 | 298 | local args = arguments or _G.arg or {} 299 | 300 | -- the spiral of DOOM: 301 | return p.invoke_command(args, options, function() 302 | return p.track_dump_request(args, function(dump, args_without_dump) 303 | return p.print_help(args_without_dump, printer, function() 304 | return p.process_arguments(args_without_dump, options, function(values, arg_count) 305 | return p.validate(options, arg_count, function() 306 | if dump then 307 | return nil, printer.dump_internal_state(values) 308 | else 309 | return p.collect_results(values, options) 310 | end 311 | end) 312 | end) 313 | end) 314 | end) 315 | end) 316 | end 317 | -------------------------------------------------------------------------------- /src/cliargs/printer.lua: -------------------------------------------------------------------------------- 1 | local wordwrap = require('cliargs.utils.wordwrap') 2 | local filter = require('cliargs.utils.filter') 3 | local K = require('cliargs.constants') 4 | local MAX_COLS = 72 5 | local _ 6 | 7 | local function create_printer(get_parser_state) 8 | local printer = {} 9 | 10 | function printer.print(msg) 11 | return _G.print(msg) 12 | end 13 | 14 | local function get_max_label_length() 15 | local maxsz = 0 16 | local state = get_parser_state() 17 | local optargument = filter(state.options, 'type', K.TYPE_SPLAT)[1] 18 | local commands = filter(state.options, 'type', K.TYPE_COMMAND) 19 | 20 | for _, entry in ipairs(commands) do 21 | if #entry.__key__ > maxsz then 22 | maxsz = #entry.__key__ 23 | end 24 | end 25 | 26 | for _,table_name in ipairs({"options"}) do 27 | for _, entry in ipairs(state[table_name]) do 28 | local key = entry.label or entry.key or entry.__key__ 29 | 30 | if #key > maxsz then 31 | maxsz = #key 32 | end 33 | end 34 | end 35 | 36 | if optargument and #optargument.key > maxsz then 37 | maxsz = #optargument.key 38 | end 39 | 40 | return maxsz 41 | end 42 | 43 | -- Generate the USAGE heading message. 44 | function printer.generate_usage() 45 | local state = get_parser_state() 46 | local msg = "Usage:" 47 | 48 | local required = filter(state.options, 'type', K.TYPE_ARGUMENT) 49 | local optional = filter(state.options, 'type', K.TYPE_OPTION) 50 | local optargument = filter(state.options, 'type', K.TYPE_SPLAT)[1] 51 | 52 | if #state.name > 0 then 53 | msg = msg .. ' ' .. tostring(state.name) 54 | end 55 | 56 | if #optional > 0 then 57 | msg = msg .. " [OPTIONS]" 58 | end 59 | 60 | if #required > 0 or optargument then 61 | msg = msg .. " [--]" 62 | end 63 | 64 | if #required > 0 then 65 | for _,entry in ipairs(required) do 66 | msg = msg .. " " .. entry.key 67 | end 68 | end 69 | 70 | if optargument then 71 | if optargument.maxcount == 1 then 72 | msg = msg .. " [" .. optargument.key .. "]" 73 | elseif optargument.maxcount == 2 then 74 | msg = msg .. " [" .. optargument.key .. "-1 [" .. optargument.key .. "-2]]" 75 | elseif optargument.maxcount > 2 then 76 | msg = msg .. " [" .. optargument.key .. "-1 [" .. optargument.key .. "-2 [...]]]" 77 | end 78 | end 79 | 80 | return msg 81 | end 82 | 83 | function printer.generate_help() 84 | local msg = '' 85 | local state = get_parser_state() 86 | local col1 = state.colsz[1] 87 | local col2 = state.colsz[2] 88 | local required = filter(state.options, 'type', K.TYPE_ARGUMENT) 89 | local optional = filter(state.options, 'type', K.TYPE_OPTION) 90 | local commands = filter(state.options, 'type', K.TYPE_COMMAND) 91 | local optargument = filter(state.options, 'type', K.TYPE_SPLAT)[1] 92 | 93 | local function append(label, desc) 94 | label = " " .. label .. string.rep(" ", col1 - (#label + 2)) 95 | desc = table.concat(wordwrap(desc, col2), "\n") -- word-wrap 96 | desc = desc:gsub("\n", "\n" .. string.rep(" ", col1)) -- add padding 97 | 98 | msg = msg .. label .. desc .. "\n" 99 | end 100 | 101 | if col1 == 0 then 102 | col1 = get_max_label_length(state) 103 | end 104 | 105 | -- add margins 106 | col1 = col1 + 3 107 | 108 | if col2 == 0 then 109 | col2 = MAX_COLS - col1 110 | end 111 | 112 | if col2 < 10 then 113 | col2 = 10 114 | end 115 | 116 | if #commands > 0 then 117 | msg = msg .. "\nCOMMANDS: \n" 118 | 119 | for _, entry in ipairs(commands) do 120 | append(entry.__key__, entry.description or '') 121 | end 122 | end 123 | 124 | if required[1] or optargument then 125 | msg = msg .. "\nARGUMENTS: \n" 126 | 127 | for _,entry in ipairs(required) do 128 | append(entry.key, entry.desc .. " (required)") 129 | end 130 | end 131 | 132 | if optargument then 133 | local optarg_desc = ' ' .. optargument.desc 134 | local default_value = optargument.maxcount > 1 and 135 | optargument.default[1] or 136 | optargument.default 137 | 138 | if #optargument.default > 0 then 139 | optarg_desc = optarg_desc .. " (optional, default: " .. tostring(default_value[1]) .. ")" 140 | else 141 | optarg_desc = optarg_desc .. " (optional)" 142 | end 143 | 144 | append(optargument.key, optarg_desc) 145 | end 146 | 147 | if #optional > 0 then 148 | msg = msg .. "\nOPTIONS: \n" 149 | 150 | for _,entry in ipairs(optional) do 151 | local desc = entry.desc 152 | if not entry.flag and entry.default and #tostring(entry.default) > 0 then 153 | local readable_default = type(entry.default) == "table" and "[]" or tostring(entry.default) 154 | desc = desc .. " (default: " .. readable_default .. ")" 155 | elseif entry.flag and entry.negatable then 156 | local readable_default = entry.default and 'on' or 'off' 157 | desc = desc .. " (default: " .. readable_default .. ")" 158 | end 159 | append(entry.label, desc) 160 | end 161 | end 162 | 163 | return msg 164 | end 165 | 166 | function printer.dump_internal_state(values) 167 | local state = get_parser_state() 168 | local required = filter(state.options, 'type', K.TYPE_ARGUMENT) 169 | local optional = filter(state.options, 'type', K.TYPE_OPTION) 170 | local optargument = filter(state.options, 'type', K.TYPE_SPLAT)[1] 171 | local maxlabel = get_max_label_length() 172 | local msg = '' 173 | 174 | local function print(fragment) 175 | msg = msg .. fragment .. '\n' 176 | end 177 | 178 | print("\n======= Provided command line =============") 179 | print("\nNumber of arguments: ", #arg) 180 | 181 | for i,v in ipairs(arg) do -- use gloabl 'arg' not the modified local 'args' 182 | print(string.format("%3i = '%s'", i, v)) 183 | end 184 | 185 | print("\n======= Parsed command line ===============") 186 | if #required > 0 then print("\nArguments:") end 187 | for _, entry in ipairs(required) do 188 | print( 189 | " " .. 190 | entry.key .. string.rep(" ", maxlabel + 2 - #entry.key) .. 191 | " => '" .. 192 | tostring(values[entry]) .. 193 | "'" 194 | ) 195 | end 196 | 197 | if optargument then 198 | print( 199 | "\nOptional arguments:" .. 200 | optargument.key .. 201 | "; allowed are " .. 202 | tostring(optargument.maxcount) .. 203 | " arguments" 204 | ) 205 | 206 | if optargument.maxcount == 1 then 207 | print( 208 | " " .. optargument.key .. 209 | string.rep(" ", maxlabel + 2 - #optargument.key) .. 210 | " => '" .. 211 | optargument.key .. 212 | "'" 213 | ) 214 | else 215 | for i = 1, optargument.maxcount do 216 | if values[optargument] and values[optargument][i] then 217 | print( 218 | " " .. tostring(i) .. 219 | string.rep(" ", maxlabel + 2 - #tostring(i)) .. 220 | " => '" .. 221 | tostring(values[optargument][i]) .. 222 | "'" 223 | ) 224 | end 225 | end 226 | end 227 | end 228 | 229 | if #optional > 0 then print("\nOptional parameters:") end 230 | local doubles = {} 231 | for _, entry in pairs(optional) do 232 | if not doubles[entry] then 233 | local value = values[entry] 234 | 235 | if type(value) == "string" then 236 | value = "'"..value.."'" 237 | else 238 | value = tostring(value) .." (" .. type(value) .. ")" 239 | end 240 | 241 | print(" " .. entry.label .. string.rep(" ", maxlabel + 2 - #entry.label) .. " => " .. value) 242 | 243 | doubles[entry] = entry 244 | end 245 | end 246 | 247 | print("\n===========================================\n\n") 248 | 249 | return msg 250 | end 251 | 252 | function printer.generate_help_and_usage() 253 | local msg = '' 254 | 255 | msg = msg .. printer.generate_usage() .. '\n' 256 | msg = msg .. printer.generate_help() 257 | 258 | return msg 259 | end 260 | 261 | return printer 262 | end 263 | 264 | return create_printer -------------------------------------------------------------------------------- /src/cliargs/utils/disect.lua: -------------------------------------------------------------------------------- 1 | local split = require('cliargs.utils.split') 2 | 3 | local RE_ADD_COMMA = "^%-([%a%d]+)[%s]%-%-" 4 | local RE_ADJUST_DELIMITER = "(%-%-?)([%a%d]+)[%s]" 5 | 6 | -- parameterize the key if needed, possible variations: 7 | -- 8 | -- -key 9 | -- -key VALUE 10 | -- -key=VALUE 11 | -- 12 | -- -key, --expanded 13 | -- -key, --expanded VALUE 14 | -- -key, --expanded=VALUE 15 | -- 16 | -- -key --expanded 17 | -- -key --expanded VALUE 18 | -- -key --expanded=VALUE 19 | -- 20 | -- --expanded 21 | -- --expanded VALUE 22 | -- --expanded=VALUE 23 | local function disect(key) 24 | -- characters allowed are a-z, A-Z, 0-9 25 | -- extended + values also allow; # @ _ + - 26 | local k, ek, v, _ 27 | local dummy 28 | 29 | -- leading "-" or "--" 30 | local prefix 31 | 32 | -- if there is no comma, between short and extended, add one 33 | _, _, dummy = key:find(RE_ADD_COMMA) 34 | if dummy then 35 | key = key:gsub(RE_ADD_COMMA, "-" .. dummy .. ", --", 1) 36 | end 37 | 38 | -- replace space delimiting the value indicator by "=" 39 | -- 40 | -- -key VALUE => -key=VALUE 41 | -- --expanded-key VALUE => --expanded-key=VALUE 42 | _, _, prefix, dummy = key:find(RE_ADJUST_DELIMITER) 43 | if prefix and dummy then 44 | key = key:gsub(RE_ADJUST_DELIMITER, prefix .. dummy .. "=", 1) 45 | end 46 | 47 | -- if there is no "=", then append one 48 | if not key:find("=") then 49 | key = key .. "=" 50 | end 51 | 52 | -- get value 53 | _, _, v = key:find(".-%=(.+)") 54 | 55 | -- get key(s), remove spaces 56 | key = split(key, "=")[1]:gsub(" ", "") 57 | 58 | -- get short key & extended key 59 | _, _, k = key:find("^%-([^-][^%s,]*)") 60 | _, _, ek = key:find("%-%-(.+)$") 61 | 62 | if v == "" then 63 | v = nil 64 | end 65 | 66 | return k,ek,v 67 | end 68 | 69 | return disect -------------------------------------------------------------------------------- /src/cliargs/utils/disect_argument.lua: -------------------------------------------------------------------------------- 1 | local function disect_argument(str) 2 | local _, symbol, key, value 3 | local negated = false 4 | 5 | _, _, symbol, key = str:find("^([%-]*)(.*)") 6 | 7 | if key then 8 | local actual_key 9 | 10 | -- split value and key 11 | _, _, actual_key, value = key:find("([^%=]+)[%=]?(.*)") 12 | 13 | if value then 14 | key = actual_key 15 | end 16 | 17 | if key:sub(1,3) == "no-" then 18 | key = key:sub(4,-1) 19 | negated = true 20 | end 21 | end 22 | 23 | -- no leading symbol means the sole fragment is the value. 24 | if #symbol == 0 then 25 | value = str 26 | key = nil 27 | end 28 | 29 | return 30 | #symbol > 0 and symbol or nil, 31 | key and #key > 0 and key or nil, 32 | value and #value > 0 and value or nil, 33 | negated and true or false 34 | end 35 | 36 | return disect_argument -------------------------------------------------------------------------------- /src/cliargs/utils/filter.lua: -------------------------------------------------------------------------------- 1 | return function(t, k, v) 2 | local out = {} 3 | 4 | for _, item in ipairs(t) do 5 | if item[k] == v then 6 | table.insert(out, item) 7 | end 8 | end 9 | 10 | return out 11 | end -------------------------------------------------------------------------------- /src/cliargs/utils/lookup.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Used internally to lookup an entry using either its short or expanded keys 3 | local function lookup(k, ek, ...) 4 | local _ 5 | 6 | for _, t in ipairs({...}) do 7 | for _, entry in ipairs(t) do 8 | if k and entry.key == k then 9 | return entry 10 | end 11 | 12 | if ek and entry.expanded_key == ek then 13 | return entry 14 | end 15 | 16 | if entry.negatable then 17 | if ek and ("no-"..entry.expanded_key) == ek then return entry end 18 | end 19 | end 20 | end 21 | 22 | return nil 23 | end 24 | 25 | return lookup -------------------------------------------------------------------------------- /src/cliargs/utils/shallow_copy.lua: -------------------------------------------------------------------------------- 1 | -- courtesy of http://lua-users.org/wiki/CopyTable 2 | local function shallow_copy(orig) 3 | if type(orig) == 'table' then 4 | local copy = {} 5 | 6 | for orig_key, orig_value in pairs(orig) do 7 | copy[orig_key] = orig_value 8 | end 9 | 10 | return copy 11 | else -- number, string, boolean, etc 12 | return orig 13 | end 14 | end 15 | 16 | return shallow_copy -------------------------------------------------------------------------------- /src/cliargs/utils/split.lua: -------------------------------------------------------------------------------- 1 | local function split(str, pat) 2 | local t = {} 3 | local fpat = "(.-)" .. pat 4 | local last_end = 1 5 | local s, e, cap = str:find(fpat, 1) 6 | 7 | while s do 8 | if s ~= 1 or cap ~= "" then 9 | table.insert(t,cap) 10 | end 11 | 12 | last_end = e + 1 13 | s, e, cap = str:find(fpat, last_end) 14 | end 15 | 16 | if last_end <= #str then 17 | cap = str:sub(last_end) 18 | table.insert(t, cap) 19 | end 20 | 21 | return t 22 | end 23 | 24 | return split -------------------------------------------------------------------------------- /src/cliargs/utils/trim.lua: -------------------------------------------------------------------------------- 1 | -- courtesy of the jungle: http://lua-users.org/wiki/StringTrim 2 | return function(str) 3 | return str:match "^%s*(.-)%s*$" 4 | end -------------------------------------------------------------------------------- /src/cliargs/utils/wordwrap.lua: -------------------------------------------------------------------------------- 1 | local split = require('cliargs.utils.split') 2 | 3 | local function buildline(words, size, overflow) 4 | -- if overflow is set, a word longer than size, will overflow the size 5 | -- otherwise it will be chopped in line-length pieces 6 | local line = {} 7 | if #words[1] > size then 8 | -- word longer than line 9 | if overflow then 10 | line[1] = words[1] 11 | table.remove(words, 1) 12 | else 13 | line[1] = words[1]:sub(1, size) 14 | words[1] = words[1]:sub(size + 1, -1) 15 | end 16 | else 17 | local len = 0 18 | while words[1] and (len + #words[1] + 1 <= size) or (len == 0 and #words[1] == size) do 19 | line[#line+1] = words[1] 20 | len = len + #words[1] + 1 21 | table.remove(words, 1) 22 | end 23 | end 24 | return table.concat(line, " "), words 25 | end 26 | 27 | local function wordwrap(str, size, overflow) 28 | -- if overflow is set, then words longer than a line will overflow 29 | -- otherwise, they'll be chopped in pieces 30 | local out, words = {}, split(str, ' ') 31 | while words[1] do 32 | out[#out+1], words = buildline(words, size, overflow) 33 | end 34 | return out 35 | end 36 | 37 | return wordwrap --------------------------------------------------------------------------------