Full Code of carlobaldassi/ArgParse.jl for AI

master db1ad6f67b31 cached
50 files
229.1 KB
60.4k tokens
1 requests
Download .txt
Showing preview only (243K chars total). Download the full file or copy to clipboard to get everything.
Repository: carlobaldassi/ArgParse.jl
Branch: master
Commit: db1ad6f67b31
Files: 50
Total size: 229.1 KB

Directory structure:
gitextract_2653z8hp/

├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       ├── TagBot.yml
│       ├── ci.yml
│       └── documentation.yml
├── LICENSE.md
├── Project.toml
├── README.md
├── docs/
│   ├── .gitignore
│   ├── Project.toml
│   ├── make.jl
│   └── src/
│       ├── arg_table.md
│       ├── conflicts.md
│       ├── custom.md
│       ├── details.md
│       ├── import.md
│       ├── index.md
│       ├── parse_args.md
│       └── settings.md
├── examples/
│   ├── argparse_example1.jl
│   ├── argparse_example2.jl
│   ├── argparse_example3.jl
│   ├── argparse_example4.jl
│   ├── argparse_example5.jl
│   ├── argparse_example6.jl
│   ├── argparse_example7.jl
│   └── argparse_example8.jl
├── src/
│   ├── ArgParse.jl
│   ├── common.jl
│   ├── deprecated.jl
│   ├── parsing.jl
│   └── settings.jl
└── test/
    ├── Project.toml
    ├── argparse_test01.jl
    ├── argparse_test02.jl
    ├── argparse_test03.jl
    ├── argparse_test04.jl
    ├── argparse_test05.jl
    ├── argparse_test06.jl
    ├── argparse_test07.jl
    ├── argparse_test08.jl
    ├── argparse_test09.jl
    ├── argparse_test10.jl
    ├── argparse_test11.jl
    ├── argparse_test12.jl
    ├── argparse_test13.jl
    ├── argparse_test14.jl
    ├── args-file1
    ├── args-file2
    ├── common.jl
    └── runtests.jl

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/dependabot.yml
================================================
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/" # Location of package manifests
    schedule:
      interval: "monthly"


================================================
FILE: .github/workflows/TagBot.yml
================================================
name: TagBot
on:
  issue_comment:
    types:
      - created
  workflow_dispatch:
jobs:
  TagBot:
    if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot'
    runs-on: ubuntu-latest
    steps:
      - uses: JuliaRegistries/TagBot@v1
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          ssh: ${{ secrets.DOCUMENTER_KEY }}


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
  push:
    branches:
      - master
    tags: '*'
  pull_request:
  schedule:
    # Run CI against `master` every Sunday
    - cron: '0 0 * * 0'
jobs:
  test:
    name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }}
    runs-on: ${{ matrix.os }}
    continue-on-error: ${{ matrix.version == 'nightly' }}
    strategy:
      matrix:
        version:
          - '1.0'  # Oldest supported version
          - '1'    # Latest release
          - 'nightly'
        os:
          - ubuntu-latest
          - macOS-latest
          - windows-latest
        arch:
          - x64
          - x86
        exclude:
          # Remove some configurations from the build matrix to reduce CI time.
          # See https://github.com/marketplace/actions/setup-julia-environment
          - os: macOS-latest
            arch: x86
          - os: windows-latest
            arch: x86
    steps:
      - uses: actions/checkout@v6
      - uses: julia-actions/setup-julia@v2
        with:
          version: ${{ matrix.version }}
          arch: ${{ matrix.arch }}
      - uses: julia-actions/cache@v3
      - uses: julia-actions/julia-buildpkg@v1
      - uses: julia-actions/julia-runtest@v1
      - uses: julia-actions/julia-processcoverage@v1
      - uses: codecov/codecov-action@v6
        with:
          files: lcov.info


================================================
FILE: .github/workflows/documentation.yml
================================================
name: Documentation

on:
  push:
    branches:
      - master
    tags: '*'
  pull_request:

jobs:
  build:
    permissions:
      actions: write
      contents: write
      pull-requests: read
      statuses: write
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: julia-actions/setup-julia@v2
        with:
          version: '1.12'
      - uses: julia-actions/cache@v3
      - name: Install dependencies
        shell: julia --color=yes --project=docs {0}
        run: |
          using Pkg
          Pkg.develop(PackageSpec(path=pwd()))
          Pkg.instantiate()
      - name: Build and deploy
        run: julia --color=yes --project=docs docs/make.jl
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # If authenticating with GitHub Actions token
          DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # If authenticating with SSH deploy key


================================================
FILE: LICENSE.md
================================================
The ArgParse Julia module is licensed under the MIT License:

> Copyright (c) 2012: Carlo Baldassi
>
> 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:
>
> The above copyright notice and this permission notice shall be
> included in all copies or substantial portions of the Software.
>
> 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.

Portions of this software are derived from Python 3.5.1 code, and therefore is licensed under
the permissive but slightly more strict Python Software Foundation license, and is partially
copyright 2001-2016 Python Software Foundation; All Rights Reserved.

> 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and
>    the Individual or Organization ("Licensee") accessing and otherwise using Python
>    3.5.1 software in source or binary form and its associated documentation.
>
> 2. Subject to the terms and conditions of this License Agreement, PSF hereby
>    grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
>    analyze, test, perform and/or display publicly, prepare derivative works,
>    distribute, and otherwise use Python 3.5.1 alone or in any derivative
>    version, provided, however, that PSF's License Agreement and PSF's notice of
>    copyright, i.e., "Copyright © 2001-2016 Python Software Foundation; All Rights
>    Reserved" are retained in Python 3.5.1 alone or in any derivative version
>    prepared by Licensee.
>
> 3. In the event Licensee prepares a derivative work that is based on or
>    incorporates Python 3.5.1 or any part thereof, and wants to make the
>    derivative work available to others as provided herein, then Licensee hereby
>    agrees to include in any such work a brief summary of the changes made to Python
>    3.5.1.
>
> 4. PSF is making Python 3.5.1 available to Licensee on an "AS IS" basis.
>    PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED.  BY WAY OF
>    EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR
>    WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE
>    USE OF PYTHON 3.5.1 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS.
>
> 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 3.5.1
>    FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF
>    MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 3.5.1, OR ANY DERIVATIVE
>    THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
>
> 6. This License Agreement will automatically terminate upon a material breach of
>    its terms and conditions.
>
> 7. Nothing in this License Agreement shall be deemed to create any relationship
>    of agency, partnership, or joint venture between PSF and Licensee.  This License
>    Agreement does not grant permission to use PSF trademarks or trade name in a
>    trademark sense to endorse or promote products or services of Licensee, or any
>    third party.
>
> 8. By copying, installing or otherwise using Python 3.5.1, Licensee agrees
>    to be bound by the terms and conditions of this License Agreement.


================================================
FILE: Project.toml
================================================
name = "ArgParse"
uuid = "c7e460c6-2fb9-53a9-8c5b-16f535851c63"
author = ["Carlo Baldassi <carlobaldassi@gmail.com>"]
version = "1.2.0"

[deps]
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
TextWrap = "b718987f-49a8-5099-9789-dcd902bef87d"

[compat]
TextWrap = "0.1.4, 1"
julia = "1"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test"]

[workspace]
projects = ["docs"]


================================================
FILE: README.md
================================================
# ArgParse.jl

[![DOCS][docs-img]][docs-url] [![CI][CI-img]][CI-url] [![CODECOV][codecov-img]][codecov-url]

ArgParse.jl is a package for parsing command-line arguments to [Julia][julia] programs.

### Installation and usage

To install the module, use Julia's package manager: start pkg mode by pressing `]` and then enter:

```
(v1.5) pkg> add ArgParse
```

The module can then be loaded like any other Julia module:

```
julia> using ArgParse
```

### Documentation

- The manual is [HERE][docs-url].
- See also the examples in the [examples directory](examples).

## Changes in release 1.2.0

* Add options to control the help text formatting ([#132][PR132])
* Allow defaults that can be converted into the target argument type ([#133][PR133])

## Changes in release 1.1.5

* Fix ambiguity with julia 1.11 new `wrap` function (see [#128][PR128])
* Throw a new `ArgParseSettingError` for all settings-related errors
* Fixed some tests

## Changes in release 1.1.4

* Fix in @project_version macro (see [#107][PR107])

## Changes in release 1.1.3

* Added a @project_version macro (see [#106][PR106])

## Changes in release 1.1.2

* Faster startup time by disabling optimizations/inference (see [#104][PR104])

## Changes in release 1.1.1

* Fixed the case when using symbol keys, commands are not required, no command is provided

## Changes in release 1.1.0

* Try using the constructor for types that don't define a `convert` method from `AbstractString`

## Changes in release 1.0.1

* Small fixes in docs

## Changes in release 1.0.0

* Drop support for Julia versions v0.6/v0.7
* Renamed a few functions and macros (old versions can be used but produce deprecation warnings):
  + `@add_arg_table` → `@add_arg_table!`
  + `add_arg_table` → `add_arg_table!`
  + `add_arg_group` → `add_arg_group!`
  + `set_default_arg_group` → `set_default_arg_group!`
  + `import_settings` → `import_settings!`. The signature of this function has also changed:
    `args_only` is now a keyword argument
* Parsing does not exit julia by default when in interactive mode now
* Added mutually-exclusive and/or required argument groups
* Added command aliases

## Changes in release 0.6.2

* Fix a remaining compatibility issue (`@warn`)

## Changes in release 0.6.1

* Testing infrastructure update, tiny docs fixes

## Changes in release 0.6.0

* Added support for Julia v0.7, dropped support for Julia v0.5.
* Added `exit_after_help` setting to control whether to exit julia after help/version info is displayed
  (which is still the defult) or to just abort the parsing and return `nothing` instead.

## Changes in release 0.5.0

* Added support for Julia v0.6, dropped support for Julia v0.4.
* The default output type is now `Dict{String,Any}`, as stated in the docs,
  rather than `Dict{AbstractString,Any}`.
* Added docstrings, moved documentation to Documenter.jl

## Changes in release 0.4.0

### New features

* Added support for vectors of METAVAR names (see [#33][PR33])

### Other changes

* Support for Julia v0.3 was dropped.

## Changes in release 0.3.1

### New available settings

* `fromfile_prexif_chars` (see [#27][PR27])
* `preformatted_desciption`/`preformatted_epilog` (see [#28][PR28])

## Changes in release 0.3.0

### Breaking changes

Upgrading from versions 0.2.X to 0.3.X, the following API changes were made,
which may break existing code:

* Option arguments are no longer evaluated by default. This is for security
  reasons. Evaluation can be forced on a per-option basis with the
  `eval_arg=true` setting (although this is discuraged).
* The syntax of the `add_arg_table` function has changed, it now takes a `Dict`
  object instead of an `@options` opbject, since the dependency on the
  Options.jl module was removed. (The `@add_arg_table` macro is unchanged
  though.)

### Other changes

* Documented that overloading the function `ArgParse.parse_item` can be used to
  instruct ArgParse on how to parse custom types. Parse error reporting was
  also improved
* Removed dependecy on the Options.jl module
* Enabled precompilation on Julia 0.4


[Julia]: http://julialang.org

[docs-img]: https://img.shields.io/badge/docs-stable-blue.svg
[docs-url]: https://carlobaldassi.github.io/ArgParse.jl/stable

[codecov-img]: https://codecov.io/gh/carlobaldassi/ArgParse.jl/branch/master/graph/badge.svg
[codecov-url]: https://codecov.io/gh/carlobaldassi/ArgParse.jl

[CI-img]: https://github.com/carlobaldassi/ArgParse.jl/actions/workflows/ci.yml/badge.svg
[CI-url]: https://github.com/carlobaldassi/ArgParse.jl/actions/workflows/ci.yml

[PR27]: https://github.com/carlobaldassi/ArgParse.jl/pull/27
[PR28]: https://github.com/carlobaldassi/ArgParse.jl/pull/28
[PR33]: https://github.com/carlobaldassi/ArgParse.jl/pull/33
[PR104]: https://github.com/carlobaldassi/ArgParse.jl/pull/104
[PR106]: https://github.com/carlobaldassi/ArgParse.jl/pull/106
[PR107]: https://github.com/carlobaldassi/ArgParse.jl/pull/107
[PR128]: https://github.com/carlobaldassi/ArgParse.jl/pull/128
[PR132]: https://github.com/carlobaldassi/ArgParse.jl/pull/132
[PR133]: https://github.com/carlobaldassi/ArgParse.jl/pull/133


================================================
FILE: docs/.gitignore
================================================
Manifest.toml
build/


================================================
FILE: docs/Project.toml
================================================
[deps]
ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"

[compat]
Documenter = "1"


================================================
FILE: docs/make.jl
================================================
using Documenter, ArgParse

CIbuild = get(ENV, "CI", nothing) == "true"

makedocs(
    modules  = [ArgParse],
    format   = Documenter.HTML(prettyurls = CIbuild),
    sitename = "ArgParse.jl",
    pages    = Any[
        "Home" => "index.md",
        "Manual" => [
            "parse_args.md",
            "settings.md",
            "arg_table.md",
            "import.md",
            "conflicts.md",
            "custom.md",
            "details.md"
        ]
    ],
    warnonly = [:missing_docs],
)

deploydocs(
    repo   = "github.com/carlobaldassi/ArgParse.jl.git",
)


================================================
FILE: docs/src/arg_table.md
================================================
# Argument table

The argument table is used to store allowed arguments and options in an [`ArgParseSettings`](@ref) object.

Each entry of the table consist of an argument name and a list of argument settings, e.g.:

```julia
"--verbose"
    help = "verbose output"
    action = :store_true
```

There are two very similar methods to populate a table:

```@docs
@add_arg_table!
```

```@docs
add_arg_table!
```

## Argument names

Argument names are strings or, in the case of options, lists of strings. An argument is an option if it begins with a `'-'`
character, otherwise it'a positional argument. A single `'-'` introduces a short option, which must consist of a single
character; long options begin with `"--"` instead.

Positional argument names can be any string, except all-uppercase strings between `'%'` characters, which are reserved
(e.g. `"%COMMAND%"`).
Option names can contain any character except `'='`, whitespaces and non-breakable spaces.
Depending on the value of the `add_help` and `add_version` settings, options `--help`, `-h` and `--version` may
be reserved.
If the `allow_ambiguous_opts` setting is `false`, some characters are not allowed as short options: all digits, the dot,
the underscore and the opening parethesis (e.g. `-1`, `-.`, `-_`, `-(`).

For positional arguments, the argument name will be used as the key in the `Dict` object returned by the [`parse_args`](@ref)
function.  For options, it will be used to produce a default key in case a `dest_name` is not explicitly specified in the table
entry, using either the first long option name in the list or the first short option name if no long options are present.
For example:

| argument name                | default `dest_name`     |
|:-----------------------------|:------------------------|
| `"--long"`                   | `"long"`                |
| `"--long", "-s"`             | `"long"`                |
| `"-s", "--long1", "--long2"` | `"long1"`               |
| `"-s", "-x"`                 | `"s"`                   |

In case the `autofix_names` setting is `true` (it is `false` by default), dashes in the names of arguments and long options will be
converted to underscores: for example, `"--my-opt"` will yield `"my_opt"` as the default `dest_name`.

The argument name is also used to generate a default metavar in case `metavar` is not explicitly set in the table entry. The rules
are the same used to determine the default `dest_name`, but for options the result will be uppercased (e.g. `"--long"` will
become `LONG`). Note that this poses additional constraints on the positional argument names (e.g. whitespace is not allowed in
metavars).

## Argument entry settings

Argument entry settings determine all aspects of an argument's behavior. Some settings combinations are contradictory and will produce
an error (e.g. using both `action = :store_true` and `nargs = 1`, or using `action = :store_true` with a positional argument).
Also, some settings are only meaningful under some conditions (e.g. passing a `metavar` to a flag-like option does not make sense)
and will be ignored with a warning (unless the `suppress_warnings` general setting is `true`).

This is the list of all available settings:

* `nargs` (default = `'A'`): the number of extra command-line tokens parsed with the entry. See
  the section [Available actions and nargs values](@ref) for a complete desctiption.
* `action`: the action performed when the argument is parsed. It can be passed as a `String` or as a `Symbol` (e.g. both
  `:store_arg` and `"store_arg"` are accepted). The default action is `:store_arg` unless `nargs` is `0`, in which case the
  default is `:store_true`. See the section [Available actions and nargs values](@ref) for a list of all available actions and a
  detailed explanation.
* `arg_type` (default = `Any`): the type of the argument. Only makes sense with non-flag arguments. Only works out-of-the-box with
  string, symbol and number types, but see the section [Parsing to custom types](@ref) for details on how to make it work for
  general types (including user-defined ones).
* `default` (default = `nothing`): the default value if the option or positional argument is not parsed. Only makes sense with
  non-flag arguments, or when the action is `:store_const` or `:append_const`. Unless it's `nothing`, it must be consistent with
  `arg_type` and `range_tester`.
* `constant` (default = `nothing`): this value is used by the `:store_const` and `:append_const` actions, or when `nargs = '?'`
  and the option argument is not provided.
* `required` (default = `false`): determines if an argument is required (this setting is ignored by flags, which are always
  optional, and in general should be avoided for options if possible).
* `range_tester` (default = `x->true`): a function returning `true` if an argument is allowed and otherwise returning `false` (e.g. you could use `arg_type = Integer` and `range_tester = isodd` to allow only odd integer values)
* `dest_name` (default = auto-generated): the key which will be associated with the argument in the `Dict` object returned by
  `parse_args`. The auto-generation rules are explained in the [Argument names](@ref) section. Multiple arguments can share
  the same destination, provided their actions and types are compatible.
* `help` (default = `""`): the help string which will be shown in the auto-generated help screen. It's a `String` which will
  be automaticaly formatted; also, `arg_type` and `default` will be automatically appended to it if provided.
* `metavar` (default = auto-generated): a token which will be used in usage and help screens to describe the argument syntax. For
  positional arguments, it will also be used as an identifier in all other messages (e.g. in reporting errors), therefore it must
  be unique. For optional arguments, if `nargs > 1` then `metavar` can be a `Vector` of `String`s of length `nargs`. The
  auto-generations rules are explained in the [Argument names](@ref) section.
* `force_override`: if `true`, conflicts are ignored when adding this entry in the argument table (see also the
  [Conflicts and overrides](@ref) section). By default, it follows the general `error_on_conflict` settings.
* `group`: the option group to which the argument will be assigned to (see the [Argument groups](@ref) section). By default, the
  current default group is used if specified, otherwise the assignment is automatic.
* `eval_arg` (default: `false`): if `true`, the argument will be parsed as a Julia expression and evaluated, which means that
  for example `"2+2"` will yield the integer `4` rather than a string. Note that this is a security risk for outside-facing
  programs and should generally be avoided: overload `ArgParse.parse_item` instead (see the section [Parsing to custom types](@ref)).
  Only makes sense for non-flag arguments.

## Available actions and nargs values

The `nargs` and `action` argument entry settings are used together to determine how many tokens will be parsed from the command
line and what action will be performed on them.

The `nargs` setting can be a number or a character; the possible values are:

* `'A'`: automatic, i.e. inferred from the action (this is the default). In practice, it means `0` for options with flag actions
(see the actions categorization below, in this section) and `1` for options with non-flag actions (but it's different from
using an explicit `1` because the result is not stored in a `Vector`).
* `0`: this is the only possibility (besides `'A'`) for flag actions, and it means no extra tokens will be parsed from
  the command line. If `action` is not specified, setting `nargs` to `0` will make `action` default to `:store_true`.
* a positive integer number `N`: exactly `N` tokens will be parsed from the command-line, and stored into a `Vector`
  of length `N`. Note that `nargs=1` produces a `Vector` of one item.
* `'?'`: optional, i.e. a token will only be parsed if it does not look like an option (see the [Parsing details](@ref) section
  for a discussion of how exactly this is established). If the option string is not given, the `default` argument value will be
  used. If the option string is given but not followed by an option parameter, the `constant` argument value will be used
  instead. This only makes sense with options.
* `'*'`: any number, i.e. all subsequent tokens are stored into a `Vector`, up until a token which looks like an option is
  encountered, or all tokens are consumed.
* `'+'`: like `'*'`, but at least one token is required.
* `'R'`: all remainder tokens, i.e. like `'*'` but it does not stop at options.

Actions can be categorized in many ways; one prominent distinction is flag vs. non-flag: some actions are for options which take no
argument (i.e. flags), all others (except `command`, which is special) are for other options and positional arguments:

* flag actions are only compatible with `nargs = 0` or `nargs = 'A'`
* non-flag actions are not compatible with `nargs = 0`.

In other words, all flags (takes no argument) are options (starts with a dash), but not all options (starts with a dash)
are flags (takes no argument).

This is the list of all available actions (in each example, suppose we defined `settings = ArgParseSettings()`):

* `store_arg` (non-flag): store the argument. This is the default unless `nargs` is `0`. Example:

  ```julia-repl
  julia> @add_arg_table!(settings, "arg", action => :store_arg);

  julia> parse_args(["x"], settings)
  Dict{String,Any} with 1 entry:
    "arg" => "x"
  ```

  The result is a vector if `nargs` is a non-zero number, or one of `'*'`, `'+'`, `'R'`:

  ```julia-repl
  julia> @add_arg_table!(settings, "arg", action => :store_arg, nargs => 2);

  julia> parse_args(["x", "y"], settings)
  Dict{String,Any} with 1 entry:
    "arg" => Any["x","y"]
  ```

* `store_true` (flag): store `true` if given, otherwise `false`. Example:

  ```julia-repl
  julia> @add_arg_table!(settings, "-v", action => :store_true);

  julia> parse_args([], settings)
  Dict{String,Any} with 1 entry:
    "v" => false

  julia> parse_args(["-v"], settings)
  Dict{String,Any} with 1 entry:
    "v" => true
  ```

* `store_false` (flag): store `false` if given, otherwise `true`. Example:

  ```julia-repl
  julia> @add_arg_table!(settings, "-v", action => :store_false);

  julia> parse_args([], settings)
  Dict{String,Any} with 1 entry:
    "v" => true

  julia> parse_args(["-v"], settings)
  Dict{String,Any} with 1 entry:
    "v" => false
  ```

* `store_const` (flag): store the value passed as `constant` in the entry settings if given, otherwise `default`.
  Example:

  ```julia-repl
  julia> @add_arg_table!(settings, "-v", action => :store_const,
                                         constant => 1,
                                         default => 0);

  julia> parse_args([], settings)
  Dict{String,Any} with 1 entry:
    "v" => 0

  julia> parse_args(["-v"], settings)
  Dict{String,Any} with 1 entry:
    "v" => 1
  ```

* `append_arg` (non-flag): append the argument to the result. Example:

  ```julia-repl
  julia> @add_arg_table!(settings, "-x", action => :append_arg);

  julia> parse_args(["-x", "1", "-x", "2"], settings)
  Dict{String,Any} with 1 entry:
    "x" => Any["1","2"]
  ```

  The result will be a `Vector{Vector}` if `nargs` is a non-zero number, or one of `'*'`, `'+'`, `'R'`:

  ```julia-repl
  julia> @add_arg_table!(settings, "-x", action => :append_arg, nargs => '*');

  julia> parse_args(["-x", "1", "2", "-x", "3"], settings)
  Dict{String,Any} with 1 entry:
    "x" => Array{Any,1}[Any["1","2"],Any["3"]]
  ```

* `append_const` (flag): append the value passed as `constant` in the entry settings. Example:

  ```julia-repl
  julia> @add_arg_table!(settings, "-x", action => :append_const, constant => 1);

  julia> parse_args(["-x", "-x", "-x"], settings)
  Dict{String,Any} with 1 entry:
    "x" => Any[1,1,1]
  ```

* `count_invocations` (flag): increase a counter; the final result will be the number of times the option was
  invoked. Example:

  ```julia-repl
  julia> @add_arg_table!(settings, "-x", action => :count_invocations);

  julia> parse_args(["-x", "-x", "-x"], settings)
  Dict{String,Any} with 1 entry:
    "x" => 3
  ```

* `show_help` (flag): show the help screen and exit. This is useful if the `add_help` general setting is
  `false`. Example:

  ```julia-repl
  julia> @add_arg_table!(settings, "-x", action => :show_help);

  julia> parse_args(["-x"], settings)
  usage: <PROGRAM> [-x]

  optional arguments:
    -x
  ```

* `show_version` (flag): show the version information and exit. This is useful if the `add_version` general
  setting is `false`. Example:

  ```julia-repl
  julia> settings.version = "1.0";

  julia> @add_arg_table!(settings, "-v", action => :show_version);

  julia> parse_args(["-v"], settings)
  1.0
  ```

* `command` (special): the argument or option is a command, i.e. it starts a sub-parsing session (see the
  [Commands](@ref) section).


## Commands

Commands are a special kind of arguments which introduce sub-parsing sessions as soon as they are encountered by `parse_args`
(and are therefore mutually exclusive).
The `ArgParse` module allows commands to look both as positional arguments or as options, with minor differences between the two.
Unlike actual positional arguments, commands that *look* like positional arguments can have extra names (aliases).

Commands are introduced by the `action = :command` setting in the argument table. Suppose we save the following script in
a file called `cmd_example.jl`:

```julia
using ArgParse

function parse_commandline()
    s = ArgParseSettings()

    @add_arg_table! s begin
        "cmd1", "C"
            help = "first command"
            action = :command
        "cmd2", "K"
            help = "second command"
            action = :command
    end

    return parse_args(s)
end

parsed_args = parse_commandline()
println(parsed_args)
```

Invoking the script from the command line, we would get the following help screen:

```text
$ julia cmd_example.jl --help
usage: cmd_example.jl [-h] {cmd1|cmd2}

commands:
  cmd1        first command (aliases: C)
  cmd2        second command (aliases: K)

optional arguments:
  -h, --help  show this help message and exit
```

If commands are present in the argument table, `parse_args` will set the special key `"%COMMAND%"` in the returned `Dict` and
fill it with the invoked command (or `nothing` if no command was given):

```text
$ julia cmd_example.jl cmd1
Dict("%COMMAND%"=>"cmd1", "cmd1"=>Dict())
```

This is unless `parse_args` is invoked with `as_symbols=true`, in which case the special key becomes `:_COMMAND_`. (In that case,
no other argument is allowed to use `_COMMAND_` as its `dest_name`, or an error will be raised.)

Aliases are recognized when parsing, but the returned `Dict` will always use the command's name (the first entry in the
table):

```text
$ julia cmd_example.jl C
Dict("%COMMAND%"=>"cmd1", "cmd1"=>Dict())
```

Since commands introduce sub-parsing sessions, an additional key will be added for the called command (`"cmd1"` in this case) whose
associated value is another `Dict{String, Any}` containing the result of the sub-parsing (in the above case it's empty). In fact,
with the default settings, commands have their own help screens:

```text
$ julia cmd_example.jl cmd1 --help
usage: cmd_example.jl cmd1 [-h]

optional arguments:
  -h, --help  show this help message and exit
```

The argument settings and tables for commands can be accessed by using a dict-like notation, i.e. `settings["cmd1"]` is an
`ArgParseSettings` object specific to the `"cmd1"` command. Therefore, to populate a command sub-argument-table, simply
use `@add_arg_table!(settings["cmd1"], table...)` and similar.

These sub-settings are created when a command is added to the argument table, and by default they inherit their parent general
settings except for the `prog` setting (which is auto-generated, as can be seen in the above example) and the
`description`, `epilog` and `usage` settings (which are left empty).

Commands can also have sub-commands.

By default, if commands exist, they are required; this can be avoided by setting the `commands_are_required = false` general setting.

The only meaningful settings for commands in an argument entry besides `action` are `help`, `force_override`, `group` and
(for flags only) `dest_name`.

The only differences between positional-arguments-like and option-like commands are in the way they are parsed, and the fact
that options accept a `dest_name` setting.

Note that short-form option-like commands will be still be recognized in the middle of a short options group and trigger a sub-parsing
session: for example, if an option `-c` is associated to a command, then `-xch` will parse option `-x` according to the parent
settings, and option `-h` according to the command sub-settings.

## Argument groups

By default, the auto-generated help screen divides arguments into three groups: commands, positional arguments and optional
arguments, displayed in that order. Example:

```@setup args
using ArgParse
```

```@repl args
settings = ArgParseSettings();

@add_arg_table! settings begin
   "--opt"
   "arg"
     required = true
   "cmd1"
     action = :command
   "cmd2"
     action = :command
end;

settings.exit_after_help = false # hide

parse_args(["--help"], settings)
```

It is possible to partition the arguments differently by defining and using customized argument groups.
Groups of options can also be declared to be mutually exclusive, meaning that no more than one of the
options in the group can be provided. A group can also be declared to be required, meaning that at least
one argument in the group needs to be provided.

```@docs
add_arg_group!
```

```@docs
set_default_arg_group!
```

Besides setting a default group with `add_arg_group!` and `set_default_group!`, it's also possible to assign individual arguments
to a group by using the `group` setting in the argument table entry, which follows the same rules as `set_default_group!`.

Note that if the `add_help` or `add_version` general settings are `true`, the `--help, -h` and `--version` options
will always be added to the `optional` group.

## Argument table styles

Here are some examples of styles for the [`@add_arg_table!`](@ref) marco and [`add_arg_table!`](@ref) function invocation:

```julia
@add_arg_table! settings begin
    "--opt", "-o"
        help = "an option"
    "arg"
        help = "a positional argument"
end

@add_arg_table!(settings
    , ["--opt", "-o"]
    ,    help => "an option"
    , "arg"
    ,    help => "a positional argument"
    )

@add_arg_table! settings begin
    (["--opt", "-o"]; help = an option)
    ("arg"; help = "a positional argument")
end

@add_arg_table!(settings,
    ["-opt", "-o"],
    begin
        help = "an option"
    end,
    "arg",
    begin
        help = "a positional argument"
    end)

add_arg_table!(settings,
    ["-opt", "-o"], Dict(:help => "an option"),
    "arg"         , Dict(:help => "a positional argument")
    )
```

One restriction is that groups introduced by `begin...end` blocks or semicolon-separated lists between parentheses
cannot introduce argument names unless the first item in the block is an argument name.


================================================
FILE: docs/src/conflicts.md
================================================
# Conflicts and overrides

Conflicts between arguments, be them options, positional arguments or commands, can arise for a variety of reasons:

* Two options have the same name (either long or short)
* Two arguments have the same destination key, but different types (e.g. one is `Any` and the other `String`)
* Two arguments have the same destination key, but incompatible actions (e.g. one does `:store_arg` and the other
  `:append_arg`)
* Two positional arguments have the same metavar (and are therefore indistinguishable in the usage and help screens
  and in error messages)
* An argument's destination key is the same as a command name
* Two commands with the same name are given, but one has a long-option form (e.g. `test` and `--test`)
* A command alias is equal to another command's name or alias

When the general setting `error_on_conflict` is `true`, or any time the specific `force_override` table entry
setting is `false`, any of the above conditions leads to an error.

On the other hand, setting `error_on_conflict` to `false`, or `force_override` to `true`, will try to force
the resolution of most of the conflicts in favor of the newest added entry. The general rules are the following:

* In case of duplicate options, all conflicting forms of the older options are removed; if all forms of an
  option are removed, the option is deleted entirely
* In case of duplicate destination key and incompatible types or actions, the older argument is deleted
* In case of duplicate positional arguments metavars, the older argument is deleted
* A command can override an argument with the same destination key
* However, an argument can never override a command; neither can a command override another command when added
  with `@add_arg_table!` (compatible commands are merged by [`import_settings!`](@ref) though)
* Conflicting command aliases are removed


================================================
FILE: docs/src/custom.md
================================================
# Parsing to custom types

If you specify an `arg_type` setting (see the [Argument entry settings](@ref) section) for an
option or an argument, `parse_args` will try to parse it, i.e. to convert the string to the
specified type. For `Number` types, Julia's built-in `parse` function will be used. For other
types, first `convert` and then the type's constructor will be tried. In order to extend this
functionality, e.g. to user-defined custom types, without adding methods to `convert` or the
constructor, you can overload the `ArgParse.parse_item` function. Example:

```julia
struct CustomType
    val::Int
end

function ArgParse.parse_item(::Type{CustomType}, x::AbstractString)
    return CustomType(parse(Int, x))
end
```

Note that the second argument needs to be of type `AbstractString` to avoid ambiguity errors. Also
note that if your type is parametric (e.g. `CustomType{T}`), you need to overload the function like
this: `function ArgParse.parse_item(::Type{CustomType{T}}, x::AbstractString) where {T}`.


================================================
FILE: docs/src/details.md
================================================
# Parsing details

During parsing, `parse_args` must determine whether an argument is an option, an option argument, a positional
argument, or a command. The general rules are explained in the [`parse_args`](@ref) documentation, but
ambiguities may arise under particular circumstances. In particular, negative numbers like `-1` or `-.1e5`
may look like options. Under the default settings, such options are forbidden, and therefore those tokens are
always recognized as non-options. However, if the `allow_ambiguous_opts` general setting is `true`, existing
options in the argument table will take precedence: for example, if the option `-1` is added, and it takes an
argument, then `-123` will be parsed as that option, and `23` will be its argument.

Some ambiguities still remains though, because the `ArgParse` module can actually accept and parse expressions,
not only numbers (although this is not the default), and therefore one may try to pass arguments like `-e` or
`-pi`; in that case, these will always be at risk of being recognized as options. The easiest workaround is to
put them in parentheses, e.g. `(-e)`.

When an option is declared to accept a fixed positive number of arguments or the remainder of the command line
(i.e. if `nargs` is a non-zero number, or `'A'`, or `'R'`), `parse_args` will not try to check if the
argument(s) looks like an option.

If `nargs` is one of `'?'` or `'*'` or `'+'`, then `parse_args` will take in only arguments which do not
look like options.

When `nargs` is `'+'` or `'*'` and an option is being parsed, then using the `'='` character will mark what
follows as an argument (i.e. not an option); all which follows goes under the rules explained above. The same is true
when short option groups are being parsed. For example, if the option in question is `-x`, then both
`-y -x=-2 4 -y` and `-yx-2 4 -y` will parse `"-2"` and `"4"` as the arguments of `-x`.

Finally, note that with the `eval_arg` setting expressions are evaluated during parsing, which means that there is no
safeguard against passing things like ```run(`rm -rf someimportantthing`)``` and seeing your data evaporate
(don't try that!). Be careful and generally try to avoid using the `eval_arg` setting.


================================================
FILE: docs/src/import.md
================================================
# Importing settings

It may be useful in some cases to import an argument table into the one which is to be used, for example to create
specialized versions of a common interface.

```@docs
import_settings!
```


================================================
FILE: docs/src/index.md
================================================
# ArgParse.jl documentation

```@meta
CurrentModule = ArgParse
```

This [Julia](http://julialang.org) package allows the creation of user-friendly command-line interfaces
to Julia programs: the program defines which arguments, options and sub-commands it accepts, and the
`ArgParse` module does the actual parsing, issues errors when the input is invalid, and automatically
generates help and usage messages.

Users familiar with Python's `argparse` module will find many similarities, but some important differences
as well.

## Installation

To install the module, use Julia's package manager: start pkg mode by pressing `]` and then enter:

```
pkg> add ArgParse
```

Dependencies will be installed automatically.

## Quick overview and a simple example

First of all, the module needs to be loaded:

```julia
using ArgParse
```

There are two main steps for defining a command-line interface: creating an [`ArgParseSettings`](@ref) object, and
populating it with allowed arguments and options using either the macro [`@add_arg_table!`](@ref) or the
function [`add_arg_table!`](@ref) (see the [Argument table](@ref) section):

```julia
s = ArgParseSettings()
@add_arg_table! s begin
    "--opt1"
        help = "an option with an argument"
    "--opt2", "-o"
        help = "another option with an argument"
        arg_type = Int
        default = 0
    "--flag1"
        help = "an option without argument, i.e. a flag"
        action = :store_true
    "arg1"
        help = "a positional argument"
        required = true
end
```

In the macro, options and positional arguments are specified within a `begin...end` block, by one or more names
in a line, optionally followed by a list of settings.
So, in the above example, there are three options:

* the first one, `"--opt1"` takes an argument, but doesn't check for its type, and it doesn't have a default value
* the second one can be invoked in two different forms (`"--opt2"` and `"-o"`); it also takes an argument, but
  it must be of `Int` type (or convertible to it) and its default value is `0`
* the third one, `--flag1`, is a flag, i.e. it doesn't take any argument.

There is also only one positional argument, `"arg1"`, which is declared as mandatory.

When the settings are in place, the actual argument parsing is performed via the [`parse_args`](@ref) function:

```julia
parsed_args = parse_args(ARGS, s)
```

The parameter `ARGS` can be omitted. In case no errors are found, the result will be a `Dict{String,Any}` object.
In the above example, it will contain the keys `"opt1"`, `"opt2"`, `"flag1"` and `"arg1"`, so that e.g.
`parsed_args["arg1"]` will yield the value associated with the positional argument.

(The `parse_args` function also accepts an optional `as_symbols` keyword argument: when set to `true`, the
result of the parsing will be a `Dict{Symbol,Any}`, which can be useful e.g. for passing it as the keywords to a Julia
function.)

Putting all this together in a file, we can see how a basic command-line interface is created:

```julia
using ArgParse

function parse_commandline()
    s = ArgParseSettings()

    @add_arg_table! s begin
        "--opt1"
            help = "an option with an argument"
        "--opt2", "-o"
            help = "another option with an argument"
            arg_type = Int
            default = 0
        "--flag1"
            help = "an option without argument, i.e. a flag"
            action = :store_true
        "arg1"
            help = "a positional argument"
            required = true
    end

    return parse_args(s)
end

function main()
    parsed_args = parse_commandline()
    println("Parsed args:")
    for (arg,val) in parsed_args
        println("  $arg  =>  $val")
    end
end

main()
```

If we save this as a file called `myprog1.jl`, we can see how a `--help` option is added by default,
and a help message is automatically generated and formatted:

```text
$ julia myprog1.jl --help
usage: myprog1.jl [--opt1 OPT1] [-o OPT2] [--flag1] [-h] arg1

positional arguments:
  arg1             a positional argument

optional arguments:
  --opt1 OPT1      an option with an argument
  -o, --opt2 OPT2  another option with an argument (type: Int64,
                   default: 0)
  --flag1          an option without argument, i.e. a flag
  -h, --help       show this help message and exit
```

Also, we can see how invoking it with the wrong arguments produces errors:

```text
$ julia myprog1.jl
required argument arg1 was not provided
usage: myprog1.jl [--opt1 OPT1] [-o OPT2] [--flag1] [-h] arg1

$ julia myprog1.jl somearg anotherarg
too many arguments
usage: myprog1.jl [--opt1 OPT1] [-o OPT2] [--flag1] [-h] arg1

$ julia myprog1.jl --opt2 1.5 somearg
invalid argument: 1.5 (conversion to type Int64 failed; you may need to overload ArgParse.parse_item;
                  the error was: ArgumentError("invalid base 10 digit '.' in \"1.5\""))
usage: myprog1.jl [--opt1 OPT1] [-o OPT2] [--flag1] arg1
```

When everything goes fine instead, our program will print the resulting `Dict`:

```text
$ julia myprog1.jl somearg
Parsed args:
  arg1  =>  somearg
  opt2  =>  0
  opt1  =>  nothing
  flag1  =>  false

$ julia myprog1.jl --opt1 "2+2" --opt2 "4" somearg --flag
Parsed args:
  arg1  =>  somearg
  opt2  =>  4
  opt1  =>  2+2
  flag1  =>  true
```

From these examples, a number of things can be noticed:

* `opt1` defaults to `nothing`, since no `default` setting was used for it in `@add_arg_table!`
* `opt1` argument type, begin unspecified, defaults to `Any`, but in practice it's parsed as a
  string (e.g. `"2+2"`)
* `opt2` instead has `Int` argument type, so `"4"` will be parsed and converted to an integer,
  an error is emitted if the conversion fails
* positional arguments can be passed in between options
* long options can be passed in abbreviated form (e.g. `--flag` instead of `--flag1`) as long as
  there's no ambiguity

More examples can be found in the `examples` directory, and the complete documentation in the
manual pages.

## Contents

```@contents
Pages = [
  "parse_args.md",
  "settings.md",
  "arg_table.md",
  "import.md",
  "conflicts.md",
  "custom.md",
  "details.md"
]
```


================================================
FILE: docs/src/parse_args.md
================================================
# The `parse_args` function

```@docs
parse_args
```



================================================
FILE: docs/src/settings.md
================================================
# Settings

```@docs
ArgParseSettings
@project_version
```


================================================
FILE: examples/argparse_example1.jl
================================================
# example 1: minimal options/arguments, auto-generated help/version

using ArgParse

function main(args)

    # initialize the settings (the description is for the help screen)
    s = ArgParseSettings(description = "Example 1 for argparse.jl: minimal usage.")

    @add_arg_table! s begin
        "--opt1"               # an option (will take an argument)
        "--opt2", "-o"         # another option, with short form
        "arg1"                 # a positional argument
    end

    parsed_args = parse_args(s) # the result is a Dict{String,Any}
    println("Parsed args:")
    for (key,val) in parsed_args
        println("  $key  =>  $(repr(val))")
    end
end

main(ARGS)


================================================
FILE: examples/argparse_example2.jl
================================================
# example 2: add some flags and the help lines for options

using ArgParse

function main(args)

    s = ArgParseSettings("Example 2 for argparse.jl: " *  # description
                         "flags, options help, " *
                         "required arguments.")

    @add_arg_table! s begin
        "--opt1"
            help = "an option"     # used by the help screen
        "--opt2", "-o"
            action = :store_true   # this makes it a flag
            help = "a flag"
        "arg1"
            help = "an argument"
            required = true        # makes the argument mandatory
    end

    parsed_args = parse_args(args, s)
    println("Parsed args:")
    for (key,val) in parsed_args
        println("  $key  =>  $(repr(val))")
    end
end

main(ARGS)


================================================
FILE: examples/argparse_example3.jl
================================================
# example 3: version information, default values, options with
#            types and variable number of arguments

using ArgParse

function main(args)

    s = ArgParseSettings("Example 3 for argparse.jl: " *
                         "version info, default values, " *
                         "options with types, variable " *
                         "number of arguments.",
                         version = "Version 1.0", # version info
                         add_version = true)      # audo-add version option

    @add_arg_table! s begin
        "--opt1"
            nargs = '?'              # '?' means optional argument
            arg_type = Int           # only Int arguments allowed
            default = 0              # this is used when the option is not passed
            constant = 1             # this is used if --opt1 is paseed with no argument
            help = "an option"
        "--karma", "-k"
            action = :count_invocations  # increase a counter each time the option is given
            help = "increase karma"
        "arg1"
            nargs = 2                        # eats up two arguments; puts the result in a Vector
            help = "first argument, two " *
                   "entries at once"
            required = true
        "arg2"
            nargs = '*'                            # eats up as many arguments as possible before an option
            default = Any["no_arg_given"]          # since the result will be a Vector{Any}, the default must
                                                   # also be (or it can be [] or nothing)
            help = "second argument, eats up " *
                   "as many items as possible " *
                   "before an option"
    end

    parsed_args = parse_args(args, s)
    println("Parsed args:")
    for (key,val) in parsed_args
        println("  $key  =>  $(repr(val))")
    end
end

main(ARGS)


================================================
FILE: examples/argparse_example4.jl
================================================
# example 4: dest_name, metavar, range_tester, alternative
#            actions, epilog with examples

using ArgParse

function main(args)

    s = ArgParseSettings("Example 4 for argparse.jl: " *
                         "more tweaking of the arg fields: " *
                         "dest_name, metvar, range_tested, " *
                         "alternative actions.")

    @add_arg_table! s begin
        "--opt1"
            action = :append_const   # appends 'constant' to 'dest_name'
            arg_type = String        # the only utility of this is restricting the dest array type
            constant = "O1"
            dest_name = "O_stack"    # this changes the destination
            help = "append O1"
        "--opt2"
            action = :append_const
            arg_type = String
            constant = "O2"
            dest_name = "O_stack"    # same dest_name as opt1, different constant
            help = "append O2"
        "-k"
            action = :store_const    # stores constant if given, default otherwise
            default = 0
            constant = 42
            help = "provide the answer"
        "--awkward-option"
            nargs = '+'                         # eats up as many argument as found (at least 1)
            action = :append_arg                # argument chunks are appended when the option is
                                                # called repeatedly
            arg_type = String
            dest_name = "awk"
            range_tester = (x->x=="X"||x=="Y")  # each argument must be either "X" or "Y"
            metavar = "XY"
            help = "either X or Y; all XY's are " *
                   "stored in chunks"
    end

    # we add an epilog and provide usage examples, also demonstrating
    # how to have some control on the formatting: we use additional '\n' at
    # the end of lines to force newlines, and '\ua0' to put non-breakable spaces.
    # Non-breakable spaces are not removed and are not used to split lines; they
    # will be substituted with spaces in the final output.
    s.epilog = """
        examples:\n
        \n
        \ua0\ua0$(basename(Base.source_path())) --opt1 --opt2 --opt2 -k\n
        \n
        \ua0\ua0$(basename(Base.source_path())) --awkward X X --opt1 --awkward X Y X --opt2\n
        \n
        The first form takes option 1 once, than option 2, then activates the answer flag,
        while the second form takes only option 1 and then 2, and intersperses them with "X\ua0X"
        and "X\ua0Y\ua0X" groups, for no particular reason.
        """

    # the epilog section will be displayed like this in the help screen:
    #
    #     examples:
    #
    #       argparse_example4.jl --opt1 --opt2 --opt2 -kkkkk
    #
    #       argparse_example4.jl --awkward X X --opt1 --awkward X Y X --opt2
    #
    #     The first form takes option 1 once, than option 2, then activates the
    #     answer flag, while the second form takes only option 1 and then 2, and
    #     intersperses them with "X X" and "X Y X" groups, for no particular
    #     reason.

    parsed_args = parse_args(args, s)
    println("Parsed args:")
    for (key,val) in parsed_args
        println("  $key  =>  $(repr(val))")
    end
end

main(ARGS)


================================================
FILE: examples/argparse_example5.jl
================================================
# example 5: manual help/version, import another parser

using ArgParse

function main(args)

    s0 = ArgParseSettings()  # a "parent" structure e.g. one with some useful set of rules
                             # which we want to extend

    # So we just add a simple table
    @add_arg_table! s0 begin
        "--parent-flag", "-o"
            action=>"store_true"
            help=>"parent flag"
        "--flag"
            action=>"store_true"
            help=>"another parent flag"
        "parent-argument"
            help = "parent argument"
    end

    s = ArgParseSettings("Example 5 for argparse.jl: " *
                         "importing another parser, " *
                         "manual help and version.",
                         add_help = false,           # disable auto-add of --help option
                         version = "Version 1.0")    # we set the version info, but --version won't be added

    import_settings!(s, s0)      # now s has all of s0 arguments (except help/version)

    s.error_on_conflict = false  # do not error-out when trying to override an option

    @add_arg_table! s begin
        "-o"                       # this will partially override s0's --parent-flag
            action = :store_true
            help = "child flag"
        "--flag"                   # this will fully override s0's --flag
            action = :store_true
            help = "another child flag"
        "-?", "--HELP", "--¡ḧëļṕ"                # (almost) all characters allowed
            action = :show_help                  # will invoke the help generator
            help = "this will help you"
        "-v", "--VERSION"
            action = :show_version               # will show version information
            help = "show version information" *
                   "and exit"
    end

    parsed_args = parse_args(args, s)
    println("Parsed args:")
    for (key,val) in parsed_args
        println("  $key  =>  $(repr(val))")
    end
end

main(ARGS)


================================================
FILE: examples/argparse_example6.jl
================================================
# example 6: commands & subtables

using ArgParse

function main(args)

    s = ArgParseSettings("Example 6 for argparse.jl: " *
                         "commands and their associated sub-tables.")

    @add_arg_table! s begin
        "run"
            action = :command        # adds a command which will be read from an argument
            help = "start running mode"
        "jump", "ju", "J"            # another one, this one has two aliases
            action = :command
            help = "start jumping mode"
    end

    @add_arg_table! s["run"] begin    # add command arg_table: same as usual, but invoked on s["cmd"]
        "--speed"
            arg_type = Float64
            default = 10.
            help = "running speed, in Å/month"
    end

    s["jump"].description = "Jump mode for example 6"  # this is how settings are tweaked
                                                       # for commands
    s["jump"].commands_are_required = false            # this makes the sub-commands optional
    s["jump"].autofix_names = true                     # this uses dashes in long options, underscores
                                                       # in auto-generated dest_names

    @add_arg_table! s["jump"] begin
        "--higher"
            action = :store_true
            help = "enhance jumping"
        "--somersault"
            action = :command        # this adds a sub-command (passed via an option instead)
            dest_name = "som"        # flag commands can set a dest_name
            help = "somersault jumping mode"
        "--clap-feet"                # dest_name will be "clap_feet" (see the "autofix_names" settings")
            action = :command
            help = "clap feet jumping mode"
    end

    s["jump"]["som"].description = "Somersault jump " *  # this is how settings are tweaked
                                   "mode for example 6"  # for sub-commands

    s["jump"]["clap_feet"].description = "Clap-feet jump " *  # notice the underscore in the name
                                         "mode for example 6"

    parsed_args = parse_args(args, s)
    println("Parsed args:")
    for (key,val) in parsed_args
        println("  $key  =>  $(repr(val))")
    end
    println()

    # parsed_args will have a special field "%COMMAND%"
    # which will hold the executed command name (or 'nothing')
    println("Command: ", parsed_args["%COMMAND%"])

    # thus, the command args are in parsed_args[parsed_args["%COMMAND%]]
    println("Parsed command args:")
    command_args = parsed_args[parsed_args["%COMMAND%"]]
    for (key,val) in command_args
        println("  $key  =>  $(repr(val))")
    end
end

main(ARGS)


================================================
FILE: examples/argparse_example7.jl
================================================
# example 7: argument groups

using ArgParse

function main(args)

    s = ArgParseSettings("Example 7 for argparse.jl: " *
                         "argument groups.")

    add_arg_group!(s, "stack options") # add a group and sets it as the default
    @add_arg_table! s begin            # all options (and arguments) in this table
                                       # will be assigned to the newly added group
        "--opt1"
            action = :append_const
            arg_type = String
            constant = "O1"
            dest_name = "O_stack"
            help = "append O1 to the stack"
        "--opt2"
            action = :append_const
            arg_type = String
            constant = "O2"
            dest_name = "O_stack"
            help = "append O2 to the stack"
    end

    add_arg_group!(s, "weird options", "weird") # another group, this time with a tag which allows
                                                # to refer to it

    set_default_arg_group!(s, "weird") # set the default group (useless here, since we just added it)

    @add_arg_table! s begin
        "--awkward-option"
            nargs = '+'
            action = :append_arg
            dest_name = "awk"
            arg_type = String
            range_tester = (x->x=="X"||x=="Y")
            metavar = "XY"
            help = "either X or Y; all XY's are " *
                   "stored in chunks"
    end

    set_default_arg_group!(s) # reset the default arg group (which means arguments
                              # are automatically assigned to commands/options/pos.args
                              # groups)
    @add_arg_table! s begin
        "-k"
            action = :store_const
            default = 0
            constant = 42
            help = "provide the answer"
        "--şİłłÿ"
            action = :store_true
            help = "an option with a silly name"
            group = "weird"   # this overrides the default group: this option
                              # will be grouped together with --awkward-option
    end

    parsed_args = parse_args(args, s)
    println("Parsed args:")
    for (key,val) in parsed_args
        println("  $key  =>  $(repr(val))")
    end
end

main(ARGS)


================================================
FILE: examples/argparse_example8.jl
================================================
# example 8: mutually exculsive and required groups

using ArgParse

function main(args)

    s = ArgParseSettings("Example 8 for argparse.jl: " *
                         "mutually exclusive and requiredd groups.")

    add_arg_group!(s, "Mutually exclusive options", exclusive=true)
    @add_arg_table! s begin
        "--maybe", "-M"
            action = :store_true
            help = "maybe..."
        "--maybe-not", "-N"
            action = :store_true
            help = "maybe not..."
    end

    add_arg_group!(s, "Required mutually exclusive options", exclusive=true, required=true)
    @add_arg_table! s begin
        "--either", "-E"
            action = :store_true
            help = "choose the `either` option"
        "--or", "-O"
            action = :store_arg
            arg_type = Int
            help = "set the `or` option"
    end

    add_arg_group!(s, "Required arguments", required=true)
    @add_arg_table! s begin
        "--enhance", "-+"
            action = :store_const
            default = 0
            constant = 42
            help = "set the enhancement option"
        "arg1"
            nargs = 2                        # eats up two arguments; puts the result in a Vector
            help = "first argument, two " *
                   "entries at once"
    end

    parsed_args = parse_args(args, s)
    println("Parsed args:")
    for (key,val) in parsed_args
        println("  $key  =>  $(repr(val))")
    end
end

main(ARGS)


================================================
FILE: src/ArgParse.jl
================================================
"""
    ArgParse

This module allows the creation of user-friendly command-line interfaces to Julia programs:
the program defines which arguments, options and sub-commands it accepts, and the `ArgParse` module
does the actual parsing, issues errors when the input is invalid, and automatically generates help
and usage messages.

Users familiar with Python's `argparse` module will find many similarities, but some important
differences as well.
"""
module ArgParse

import TextWrap

if isdefined(Base, :Experimental) && isdefined(Base.Experimental, Symbol("@compiler_options"))
    @eval Base.Experimental.@compiler_options compile=min optimize=0 infer=false
end

export
# types
    ArgParseSettings,
    ArgParseSettingsError,
    ArgParseError,

# functions & macros
    add_arg_table!,
    @add_arg_table!,
    add_arg_group!,
    set_default_arg_group!,
    import_settings!,
    usage_string,
    parse_args,
    @project_version

import Base: show, getindex, setindex!, haskey

@nospecialize # use only declared type signatures, helps with compile time

include("common.jl")
include("settings.jl")
include("parsing.jl")
include("deprecated.jl")

end # module ArgParse


================================================
FILE: src/common.jl
================================================
## Some common functions, constants, macros

# auxiliary functions/constants
found_a_bug() = error("you just found a bug in the ArgParse module, please report it.")
const nbspc = '\u00a0'
const nbsps = "$nbspc"
println_unnbsp(io::IO, args...) = println(io, map(s->replace(s, nbspc => ' '), args)...)

macro defaults(opts, ex)
    @assert ex.head == :block
    lines = filter(x->!(x isa LineNumberNode), ex.args)
    @assert all(x->Meta.isexpr(x, :(=)), lines)

    opts = esc(opts)
    # Transform the opts array into a Dict
    exret = :($opts = Dict{Symbol,Any}($opts))
    # Initialize the checks
    found = esc(gensym("found"))
    exret = quote
        $exret
        $found = Dict{Symbol,Bool}(k => false for (k,v) in $opts)
    end

    for y in lines
        sym = y.args[1]
        qsym = Expr(:quote, sym)
        exret = quote
            $exret
            if haskey($opts, $qsym)
                $(esc(sym)) = $opts[$qsym]
                $found[$qsym] = true
            else
                $(esc(y))
            end
        end
    end
    exret = quote
        $exret
        for (k,v) in $found
            v || serror("unknown description field: $k")
        end
    end
    exret
end



================================================
FILE: src/deprecated.jl
================================================

@deprecate add_arg_table add_arg_table!
@deprecate import_settings(settings, other) import_settings!(settings, other)
@deprecate import_settings(settings, other, ao) import_settings!(settings, other; args_only=ao)
@deprecate add_arg_group(args...; kw...) add_arg_group!(args...; kw...)
@deprecate set_default_arg_group set_default_arg_group!

# The Base.@deprecate macro doesn't work with macros
# Here's an attempt at mimicking most of what that does
# and deprecate @add_arg_table -> @add_arg_table!
using Base: JLOptions, CoreLogging
using Logging: @logmsg

function callframe(st, name)
    found = false
    for sf in st
        sf == StackTraces.UNKNOWN && continue
        if found && sf.func == Symbol("top-level scope")
            return sf
        end
        sf.func == name && (found = true)
    end
    return StackTraces.UNKNOWN
end

export @add_arg_table
macro add_arg_table(s, x...)
    opts = JLOptions()
    msg = "`@add_arg_table` is deprecated, use `@add_arg_table!` instead"
    opts.depwarn == 2 && throw(ErrorException(msg))
    deplevel = opts.depwarn == 1 ? CoreLogging.Warn : CoreLogging.BelowMinLevel
    st = stacktrace()
    caller = callframe(st, Symbol("@add_arg_table"))
    @logmsg(deplevel, msg,
            _file = String(caller.file),
            _line = caller.line,
            _group = :depwarn,
            maxlog = 1)

    return _add_arg_table!(s, x...)
end


================================================
FILE: src/parsing.jl
================================================
## All types, functions and constants related to the actual process of
## parsing the arguments

# ArgParseError
struct ArgParseError <: Exception
    text::AbstractString
end

argparse_error(x...) = throw(ArgParseError(string(x...)))

# parsing checks
function test_range(range_tester::Function, arg, name::AbstractString)
    local rng_chk::Bool
    try
        rng_chk = range_tester(arg)
    catch
        rng_chk = false
    end
    rng_chk || argparse_error("out of range input for $name: $arg")
    return
end

function test_exclusive_groups!(exc_groups::Dict{ArgParseGroup,AbstractString},
                                settings::ArgParseSettings,
                                f::ArgParseField,
                                name::AbstractString)
    arg_group = get_group(f.group, f, settings)
    if haskey(exc_groups, arg_group)
        prev_id = exc_groups[arg_group]
        if isempty(prev_id)
            exc_groups[arg_group] = idstring(f)
        elseif prev_id != idstring(f)
            argparse_error("option $name not allowed with $prev_id")
        end
    end
    return
end

function test_required_args(settings::ArgParseSettings, found_args::Set{AbstractString})
    req_groups = Dict{ArgParseGroup,Bool}(g=>false for g in settings.args_groups if g.required)
    fields = settings.args_table.fields
    for f in fields
        found = idstring(f) ∈ found_args
        !is_cmd(f) && f.required && !found &&
            argparse_error("required $(idstring(f)) was not provided")
        found && (req_groups[get_group(f.group, f, settings)] = true)
    end
    for (g,found) in req_groups
        found && continue
        ids = String[idstring(f) for f in fields if get_group(f.group, f, settings) ≡ g]
        argparse_error("one of these is required: " * join(ids, ", "))
    end
    return true
end

function check_settings_can_use_symbols(settings::ArgParseSettings)
    args_table = settings.args_table
    if !isempty(args_table.subsettings)
        for f in args_table.fields
            if f.dest_name == string(scmd_dest_name)
                serror("the dest_name $scmd_dest_name cannot be used with the as_symbols option")
            end
        end
        for subs in values(args_table.subsettings)
            check_settings_can_use_symbols(subs)
        end
    end
    settings.suppress_warnings && return true
    for f in args_table.fields
        if '-' in f.dest_name
            @warn "dest_name=$(f.dest_name) contains a hyphen; use the autofix_names=true setting to have it converted to an underscore"
        end
    end
    return true
end

# parsing aux functions
function parse_item_wrapper(::Type{T}, x::AbstractString) where {T}
    local r::T
    try
        r = parse_item(T, x)
    catch err
        argparse_error("""
            invalid argument: $x (conversion to type $T failed; you may need to overload
                              ArgParse.parse_item; the error was: $err)""")
    end
    return r
end

parse_item(::Type{Any}, x::AbstractString) = x
parse_item(::Type{T}, x::AbstractString) where {T<:Number} = parse(T, x)
parse_item(::Type{T}, x::AbstractString) where {T} = applicable(convert, T, x) ? convert(T, x) : T(x)

function parse_item_eval(::Type{T}, x::AbstractString) where {T}
    local r::T
    try
        r = convert(T, eval(Meta.parse(x)))
    catch err
        argparse_error("""
            invalid argument: $x (must evaluate or convert to type $T;
                              the error was: $err)""")
    end
    return r
end

const number_regex =
    r"^[+-]?                                          # optional sign
        (
          0x[0-9a-fA-F](_?[0-9a-fA-F])*             | # hex
          0o[0-7](_?[0-7])*                         | # oct
          0b[01](_?[01])*                           | # bin
          (                                           # float mantissa
            [0-9](_?[0-9])*(\.([0-9](_?[0-9])*)?)?  | #   start with digit
            \.[0-9](_?[0-9])*                         #   start with dot
          )([eEf][-+]?[0-9]+)?                        # float optional exp
        )
      $"x

function looks_like_an_option(arg::AbstractString, settings::ArgParseSettings)
    arg == "-" && return false
    startswith(arg, "--") && return true
    startswith(arg, '-') || return false
    # begins with '-'
    # check if it's a number:
    occursin(number_regex, arg) || return true
    # looks like a number; but is it overridden by an option?
    d = arg[2:2]
    for a in settings.args_table.fields, s in a.short_opt_name
        s == d && return true
    end
    # it's a number
    return false
end

function usage_string(settings::ArgParseSettings)
    isempty(settings.usage) || return settings.usage

    usage_pre = "usage: " * (isempty(settings.prog) ? "<PROGRAM>" : settings.prog)

    lc_len_limit = settings.help_alignment_width

    cmd_lst = String[]
    pos_lst = String[]
    opt_lst = String[]
    exc_lst = Dict{String,Tuple{Bool,Vector{String}}}()
    for f in settings.args_table.fields
        arg_group = get_group(f.group, f, settings)
        if arg_group.exclusive
            (is_cmd(f) || is_arg(f)) && found_a_bug()
            _, tgt_opt_lst = get!(exc_lst, arg_group.name, (arg_group.required, String[]))
        else
            tgt_opt_lst = opt_lst
        end
        if is_cmd(f)
            if !isempty(f.short_opt_name)
                idstr = "-" * f.short_opt_name[1]
            elseif !isempty(f.long_opt_name)
                idstr = "--" * f.long_opt_name[1]
            else
                idstr = f.metavar
            end
            push!(cmd_lst, idstr)
        elseif is_arg(f)
            bra_pre, bra_post = f.required ? ("","") : ("[","]")
            if isa(f.nargs.desc, Int)
                if f.metavar isa AbstractString
                    arg_str = join(repeat([f.metavar], f.nargs.desc), nbsps)
                else
                    found_a_bug()
                end
            elseif f.nargs.desc == :A
                arg_str = f.metavar
            elseif f.nargs.desc == :?
                found_a_bug()
            elseif f.nargs.desc == :* || f.nargs.desc == :R || f.nargs.desc == :+
                arg_str = f.metavar * "..."
            else
                found_a_bug()
            end
            push!(pos_lst, bra_pre * arg_str * bra_post)
        else
            bra_pre, bra_post = (f.required || arg_group.exclusive) ? ("","") : ("[","]")
            if !isempty(f.short_opt_name)
                opt_str1 = "-" * f.short_opt_name[1]
            else
                opt_str1 = "--" * f.long_opt_name[1]
            end
            if is_flag(f)
                opt_str2 = ""
            else
                if f.nargs.desc isa Int
                    if f.metavar isa AbstractString
                        opt_str2 = string(ntuple(i->(nbsps * f.metavar), f.nargs.desc)...)
                    elseif f.metavar isa Vector
                        opt_str2 = string(ntuple(i->(nbsps * f.metavar[i]), f.nargs.desc)...)
                    else
                        found_a_bug()
                    end
                elseif f.nargs.desc == :A
                    opt_str2 = nbsps * f.metavar
                elseif f.nargs.desc == :?
                    opt_str2 = nbsps * "[" * f.metavar * "]"
                elseif f.nargs.desc == :* || f.nargs.desc == :R
                    opt_str2 = nbsps * "[" * f.metavar * "...]"
                elseif f.nargs.desc == :+
                    opt_str2 = nbsps * f.metavar * nbsps * "[" * f.metavar * "...]"
                else
                    found_a_bug()
                end
            end
            new_opt = bra_pre * opt_str1 * opt_str2 * bra_post
            push!(tgt_opt_lst, new_opt)
        end
    end
    excl_str = ""
    for (req,lst) in values(exc_lst)
        pre, post = req ? ("{","}") : ("[","]")
        excl_str *= " " * pre * join(lst, " | ") * post
    end
    if isempty(opt_lst)
        optl_str = ""
    else
        optl_str = " " * join(opt_lst, " ")
    end
    if isempty(pos_lst)
        posl_str = ""
    else
        posl_str = " " * join(pos_lst, " ")
    end
    if isempty(cmd_lst)
        cmdl_str = ""
    else
        bra_pre, bra_post = settings.commands_are_required ? ("{","}") :  ("[","]")
        cmdl_str = " " * bra_pre * join(cmd_lst, "|") * bra_post
    end

    usage_len = length(usage_pre) + 1

    str_nonwrapped = usage_pre * excl_str * optl_str * posl_str * cmdl_str
    str_wrapped = TextWrap.wrap(str_nonwrapped, break_long_words = false, break_on_hyphens = false,
                                subsequent_indent = min(usage_len, lc_len_limit),
                                width = settings.help_width)


    out_str = replace(str_wrapped, nbspc => ' ')
    return out_str
end

function string_compact(x...)
    io = IOBuffer()
    show(IOContext(io, :compact=>true), x...)
    return String(take!(io))
end

function gen_help_text(arg::ArgParseField, settings::ArgParseSettings)
    is_flag(arg) && return arg.help

    pre = isempty(arg.help) ? "" : " "
    type_str = ""
    default_str = ""
    const_str = ""
    alias_str = ""
    if !is_command_action(arg.action)
        if arg.arg_type ≠ Any && !(arg.arg_type <: AbstractString)
            type_str = pre * "(type: " * string_compact(arg.arg_type)
        end
        if arg.default ≢ nothing && !isequal(arg.default, [])
            mid = isempty(type_str) ? " (" : ", "
            default_str = mid * "default: " * string_compact(arg.default)
        end
        if arg.nargs.desc == :?
            mid = isempty(type_str) && isempty(default_str) ? " (" : ", "
            const_str = mid * "without arg: " * string_compact(arg.constant)
        end
    else
        is_arg(arg) || found_a_bug()
        if !isempty(arg.cmd_aliases)
            alias_str = pre * "(aliases: " * join(arg.cmd_aliases, ", ")
        end
    end
    post = all(isempty, (type_str, default_str, const_str, alias_str)) ? "" : ")"
    return arg.help * type_str * default_str * const_str * alias_str * post
end

function print_group(io::IO, lst::Vector, desc::AbstractString, lc_usable_len::Int, lc_len::Int,
                     lmargin::AbstractString, rmargin::AbstractString, sindent::AbstractString,
                     width::Int)
    isempty(lst) && return
    println(io, desc, ":")
    for l in lst
        l1len = length(l[1])
        if l1len ≤ lc_usable_len
            rfill = " "^(lc_len - l1len)
            ll_nonwrapped = l[1] * rfill * rmargin * l[2]
            ll_wrapped = TextWrap.wrap(ll_nonwrapped, break_long_words = false, break_on_hyphens = false,
                                       initial_indent = lmargin, subsequent_indent = sindent, width = width)
            println_unnbsp(io, ll_wrapped)
        else
            println_unnbsp(io, lmargin, l[1])
            l2_wrapped = TextWrap.wrap(l[2], break_long_words = false, break_on_hyphens = false,
                                       initial_indent = sindent, subsequent_indent = sindent, width = width)
            println_unnbsp(io, l2_wrapped)
        end
    end
    println(io)
end

show_help(settings::ArgParseSettings; kw...) = show_help(stdout, settings; kw...)

function show_help(io::IO, settings::ArgParseSettings; exit_when_done = !isinteractive())

    lc_len_limit = settings.help_alignment_width
    lc_left_indent = 2
    lc_right_margin = 2

    lc_usable_len = lc_len_limit - lc_left_indent - lc_right_margin
    max_lc_len = 0

    usage_str = usage_string(settings)

    group_lists = Dict{AbstractString,Vector{Any}}()
    for ag in settings.args_groups
        group_lists[ag.name] = Any[]
    end
    for f in settings.args_table.fields
        dest_lst = group_lists[f.group]
        if is_arg(f)
            push!(dest_lst, Any[f.metavar, gen_help_text(f, settings)])
            max_lc_len = max(max_lc_len, length(f.metavar))
        else
            opt_str1 = join([["-"*x for x in f.short_opt_name];
                             ["--"*x for x in f.long_opt_name]],
                            ", ")
            if is_flag(f)
                opt_str2 = ""
            else
                if f.nargs.desc isa Int
                    if f.metavar isa AbstractString
                        opt_str2 = string(ntuple(i->(nbsps * f.metavar), f.nargs.desc)...)
                    elseif isa(f.metavar, Vector)
                        opt_str2 = string(ntuple(i->(nbsps * f.metavar[i]), f.nargs.desc)...)
                    else
                        found_a_bug()
                    end
                elseif f.nargs.desc == :A
                    opt_str2 = nbsps * f.metavar
                elseif f.nargs.desc == :?
                    opt_str2 = nbsps * "[" * f.metavar * "]"
                elseif f.nargs.desc == :* || f.nargs.desc == :R
                    opt_str2 = nbsps * "[" * f.metavar * "...]"
                elseif f.nargs.desc == :+
                    opt_str2 = nbsps * f.metavar * nbsps * "[" * f.metavar * "...]"
                else
                    found_a_bug()
                end
            end
            new_opt = Any[opt_str1 * opt_str2, gen_help_text(f, settings)]
            push!(dest_lst, new_opt)
            max_lc_len = max(max_lc_len, length(new_opt[1]))
        end
    end

    lc_len = min(lc_usable_len, max_lc_len)
    lmargin = " "^lc_left_indent
    rmargin = " "^lc_right_margin

    sindent = lmargin * " "^lc_len * rmargin

    println(io, usage_str)
    println(io)
    show_message(io, settings.description, settings.preformatted_description, settings.help_width)

    for ag in settings.args_groups
        print_group(io, group_lists[ag.name], ag.desc, lc_usable_len, lc_len,
                    lmargin, rmargin, sindent, settings.help_width)
    end

    show_message(io, settings.epilog, settings.preformatted_epilog, settings.help_width)
    exit_when_done && exit(0)
    return
end

function show_message(io::IO, message::AbstractString, preformatted::Bool, width::Int)
    if !isempty(message)
        if preformatted
            print(io, message)
        else
            for l in split(message, "\n\n")
                message_wrapped = TextWrap.wrap(l, break_long_words = false, break_on_hyphens = false, width = width)
                println_unnbsp(io, message_wrapped)
            end
        end
        println(io)
    end
end

show_version(settings::ArgParseSettings; kw...) = show_version(stdout, settings; kw...)

function show_version(io::IO, settings::ArgParseSettings; exit_when_done = !isinteractive())
    println(io, settings.version)
    exit_when_done && exit(0)
    return
end

has_cmd(settings::ArgParseSettings) = any(is_cmd, settings.args_table.fields)

# parse_args & friends
function default_handler(settings::ArgParseSettings, err, err_code::Int = 1)
    isinteractive() ? debug_handler(settings, err) : cmdline_handler(settings, err, err_code)
end

function cmdline_handler(settings::ArgParseSettings, err, err_code::Int = 1)
    println(stderr, err.text)
    println(stderr, usage_string(settings))
    exit(err_code)
end

function debug_handler(settings::ArgParseSettings, err)
    rethrow(err)
end

parse_args(settings::ArgParseSettings; kw...) = parse_args(ARGS, settings; kw...)

"""
    parse_args([args,] settings; as_symbols::Bool = false)

This is the central function of the `ArgParse` module. It takes a `Vector` of arguments and an
[`ArgParseSettings`](@ref) object, and returns a `Dict{String,Any}`. If `args` is not provided, the
global variable `ARGS` will be used.

When the keyword argument `as_symbols` is `true`, the function will return a `Dict{Symbol,Any}`
instead.

The returned `Dict` keys are defined (possibly implicitly) in `settings`, and their associated
values are parsed from `args`. Special keys are used for more advanced purposes; at the moment, one
such key exists: `%COMMAND%` (`_COMMAND_` when using `as_symbols=true`; see the [Commands](@ref)
section).

Arguments are parsed in sequence and matched against the argument table in `settings` to determine
whether they are long options, short options, option arguments or positional arguments:

  * long options begin with a double dash `"--"`; if a `'='` character is found, the remainder is
    the option argument; therefore, `["--opt=arg"]` and `["--opt", "arg"]` are equivalent if `--opt`
    takes at least one argument. Long options can be abbreviated (e.g. `--opt` instead of
    `--option`) as long as there is no ambiguity.
  * short options begin with a single dash `"-"` and their name consists of a single character; they
    can be grouped together (e.g. `["-x", "-y"]` can become `["-xy"]`), but in that case only the
    last option in the group can take an argument (which can also be grouped, e.g.
    `["-a", "-f", "file.txt"]` can be passed as `["-affile.txt"]` if `-a` does not take an argument
    and `-f` does). The `'='` character can be used to separate option names from option arguments
    as well (e.g. `-af=file.txt`).
  * positional arguments are anything else; they can appear anywhere.

The special string `"--"` can be used to signal the end of all options; after that, everything is
considered as a positional argument (e.g. if `args = ["--opt1", "--", "--opt2"]`, the parser will
recognize `--opt1` as a long option without argument, and `--opt2` as a positional argument).

The special string `"-"` is always parsed as a positional argument.

The parsing can stop early if a `:show_help` or `:show_version` action is triggered, or if a parsing
error is found.

Some ambiguities can arise in parsing, see the [Parsing details](@ref) section for a detailed
description of how they're solved.
"""
function parse_args(args_list::Vector, settings::ArgParseSettings; as_symbols::Bool = false)
    as_symbols && check_settings_can_use_symbols(settings)
    local parsed_args
    try
        parsed_args = parse_args_unhandled(args_list, settings)
    catch err
        err isa ArgParseError || rethrow()
        settings.exc_handler(settings, err)
    end
    as_symbols && (parsed_args = convert_to_symbols(parsed_args))
    return parsed_args
end

mutable struct ParserState
    args_list::Vector
    arg_delim_found::Bool
    token::Union{AbstractString,Nothing}
    token_arg::Union{AbstractString,Nothing}
    arg_consumed::Bool
    last_arg::Int
    found_args::Set{AbstractString}
    command::Union{AbstractString,Nothing}
    truncated_shopts::Bool
    abort::Bool
    exc_groups::Dict{ArgParseGroup,AbstractString}
    out_dict::Dict{String,Any}
    function ParserState(args_list::Vector, settings::ArgParseSettings, truncated_shopts::Bool)
        exc_groups = Dict{ArgParseGroup,AbstractString}(
                g=>"" for g in settings.args_groups if g.exclusive)
        out_dict = Dict{String,Any}()
        for f in settings.args_table.fields
            f.action ∈ (:show_help, :show_version) && continue
            out_dict[f.dest_name] = deepcopy(f.default)
        end
        return new(deepcopy(args_list), false, nothing, nothing, false, 0, Set{AbstractString}(),
                   nothing, truncated_shopts, false, exc_groups, out_dict)
    end
end

found_command(state::ParserState) = state.command ≢ nothing
function parse_command_args!(state::ParserState, settings::ArgParseSettings)
    cmd = state.command
    haskey(settings, cmd) || argparse_error("unknown command: $cmd")
    #state.out_dict[cmd] = parse_args(state.args_list, settings[cmd])
    try
        state.out_dict[cmd] =
            parse_args_unhandled(state.args_list, settings[cmd], state.truncated_shopts)
    catch err
        err isa ArgParseError || rethrow(err)
        settings[cmd].exc_handler(settings[cmd], err)
    finally
        state.truncated_shopts = false
    end
    return state.out_dict[cmd]
end

function preparse!(c::Channel, state::ParserState, settings::ArgParseSettings)
    args_list = state.args_list
    while !isempty(args_list)
        state.arg_delim_found && (put!(c, :pos_arg); continue)
        arg = args_list[1]
        if state.truncated_shopts
            @assert arg[1] == '-'
            looks_like_an_option(arg, settings) ||
                argparse_error("illegal short options sequence after command: $arg")
            state.truncated_shopts = false
        end
        if arg == "--"
            state.arg_delim_found = true
            state.token = nothing
            state.token_arg = nothing
            popfirst!(args_list)
            continue
        elseif startswith(arg, "--")
            eq = findfirst(isequal('='), arg)
            if eq ≢ nothing
                opt_name = arg[3:prevind(arg,eq)]
                arg_after_eq = arg[nextind(arg,eq):end]
            else
                opt_name = arg[3:end]
                arg_after_eq = nothing
            end
            isempty(opt_name) && argparse_error("illegal option: $arg")
            popfirst!(args_list)
            state.token = opt_name
            state.token_arg = arg_after_eq
            put!(c, :long_option)
        elseif looks_like_an_option(arg, settings)
            shopts_lst = arg[2:end]
            popfirst!(args_list)
            state.token = shopts_lst
            state.token_arg = nothing
            put!(c, :short_option_list)
        else
            state.token = nothing
            state.token_arg = nothing
            put!(c, :pos_arg)
        end
    end
end

# faithful reproduction of Python 3.5.1 argparse.py
# partially Copyright © 2001-2016 Python Software Foundation; All Rights Reserved
function read_args_from_files(arg_strings, prefixes)
    new_arg_strings = AbstractString[]

    for arg_string in arg_strings
        if isempty(arg_string) || arg_string[1] ∉ prefixes
            # for regular arguments, just add them back into the list
            push!(new_arg_strings, arg_string)
        else
            # replace arguments referencing files with the file content
            open(arg_string[nextind(arg_string, 1):end]) do args_file
                arg_strings = AbstractString[]
                for arg_line in readlines(args_file)
                    push!(arg_strings, rstrip(arg_line, '\n'))
                end
                arg_strings = read_args_from_files(arg_strings, prefixes)
                append!(new_arg_strings, arg_strings)
            end
        end
    end

    # return the modified argument list
    return new_arg_strings
end

function parse_args_unhandled(args_list::Vector,
                              settings::ArgParseSettings,
                              truncated_shopts::Bool=false)
    all(x->(x isa AbstractString), args_list) || error("malformed args_list")
    if !isempty(settings.fromfile_prefix_chars)
        args_list = read_args_from_files(args_list, settings.fromfile_prefix_chars)
    end

    version_added = false
    help_added = false

    if settings.add_version
        settings.add_version = false
        add_arg_field!(settings, "--version",
            action = :show_version,
            help = "show version information and exit",
            group = ""
            )
        version_added = true
    end
    if settings.add_help
        settings.add_help = false
        add_arg_field!(settings, ["--help", "-h"],
            action = :show_help,
            help = "show this help message and exit",
            group = ""
            )
        help_added = true
    end

    state = ParserState(args_list, settings, truncated_shopts)
    preparser = Channel(c->preparse!(c, state, settings))

    try
        for tag in preparser
            if tag == :long_option
                parse_long_opt!(state, settings)
            elseif tag == :short_option_list
                parse_short_opt!(state, settings)
            elseif tag == :pos_arg
                parse_arg!(state, settings)
            else
                found_a_bug()
            end
            state.abort && return nothing
            found_command(state) && break
        end
        test_required_args(settings, state.found_args)
        if found_command(state)
            cmd_dict = parse_command_args!(state, settings)
            cmd_dict ≡ nothing && return nothing
        elseif settings.commands_are_required && has_cmd(settings)
            argparse_error("no command given")
        end
    catch err
        rethrow()
    finally
        if help_added
            pop!(settings.args_table.fields)
            settings.add_help = true
        end
        if version_added
            pop!(settings.args_table.fields)
            settings.add_version = true
        end
    end

    return state.out_dict
end

# common parse functions
function parse1_flag!(state::ParserState, settings::ArgParseSettings, f::ArgParseField,
                      has_arg::Bool, opt_name::AbstractString)
    has_arg && argparse_error("option $opt_name takes no arguments")
    test_exclusive_groups!(state.exc_groups, settings, f, opt_name)
    command = nothing
    out_dict = state.out_dict
    if f.action == :store_true
        out_dict[f.dest_name] = true
    elseif f.action == :store_false
        out_dict[f.dest_name] = false
    elseif f.action == :store_const
        out_dict[f.dest_name] = f.constant
    elseif f.action == :append_const
        push!(out_dict[f.dest_name], f.constant)
    elseif f.action == :count_invocations
        out_dict[f.dest_name] += 1
    elseif f.action == :command_flag
        out_dict[f.dest_name] = f.constant
        command = f.constant
    elseif f.action == :show_help
        show_help(settings, exit_when_done = settings.exit_after_help)
        state.abort = true
    elseif f.action == :show_version
        show_version(settings, exit_when_done = settings.exit_after_help)
        state.abort = true
    end
    state.command = command
    return
end

function parse1_optarg!(state::ParserState, settings::ArgParseSettings, f::ArgParseField,
                        rest, name::AbstractString)
    args_list = state.args_list
    arg_delim_found = state.arg_delim_found
    out_dict = state.out_dict

    test_exclusive_groups!(state.exc_groups, settings, f, name)

    arg_consumed = false
    parse_function = f.eval_arg ? parse_item_eval : parse_item_wrapper
    command = nothing
    is_multi_nargs(f.nargs) && (opt_arg = Array{f.arg_type}(undef, 0))
    if f.nargs.desc isa Int
        num::Int = f.nargs.desc
        num > 0 || found_a_bug()
        corr = (rest ≡ nothing) ? 0 : 1
        if length(args_list) + corr < num
            argparse_error("$name requires $num argument", num > 1 ? "s" : "")
        end
        if rest ≢ nothing
            a = parse_function(f.arg_type, rest)
            test_range(f.range_tester, a, name)
            push!(opt_arg, a)
            arg_consumed = true
        end
        for i = (1+corr):num
            a = parse_function(f.arg_type, popfirst!(args_list))
            test_range(f.range_tester, a, name)
            push!(opt_arg, a)
        end
    elseif f.nargs.desc == :A
        if rest ≢ nothing
            a = parse_function(f.arg_type, rest)
            test_range(f.range_tester, a, name)
            opt_arg = a
            arg_consumed = true
        else
            isempty(args_list) && argparse_error("option $name requires an argument")
            a = parse_function(f.arg_type, popfirst!(args_list))
            test_range(f.range_tester, a, name)
            opt_arg = a
        end
    elseif f.nargs.desc == :?
        if rest ≢ nothing
            a = parse_function(f.arg_type, rest)
            test_range(f.range_tester, a, name)
            opt_arg = a
            arg_consumed = true
        else
            if isempty(args_list) || looks_like_an_option(args_list[1], settings)
                opt_arg = deepcopy(f.constant)
            else
                a = parse_function(f.arg_type, popfirst!(args_list))
                test_range(f.range_tester, a, name)
                opt_arg = a
            end
        end
    elseif f.nargs.desc == :* || f.nargs.desc == :+
        arg_found = false
        if rest ≢ nothing
            a = parse_function(f.arg_type, rest)
            test_range(f.range_tester, a, name)
            push!(opt_arg, a)
            arg_consumed = true
            arg_found = true
        end
        while !isempty(args_list)
            if !arg_delim_found && looks_like_an_option(args_list[1], settings)
                break
            end
            a = parse_function(f.arg_type, popfirst!(args_list))
            test_range(f.range_tester, a, name)
            push!(opt_arg, a)
            arg_found = true
        end
        if f.nargs.desc == :+ && !arg_found
            argparse_error("option $name requires at least one not-option-looking argument")
        end
    elseif f.nargs.desc == :R
        if rest ≢ nothing
            a = parse_function(f.arg_type, rest)
            test_range(f.range_tester, a, name)
            push!(opt_arg, a)
            arg_consumed = true
        end
        while !isempty(args_list)
            a = parse_function(f.arg_type, popfirst!(args_list))
            test_range(f.range_tester, a, name)
            push!(opt_arg, a)
        end
    else
        found_a_bug()
    end
    if f.action == :store_arg
        out_dict[f.dest_name] = opt_arg
    elseif f.action == :append_arg
        push!(out_dict[f.dest_name], opt_arg)
    elseif f.action == :command_arg
        if !haskey(settings, opt_arg)
            found = false
            for f1 in settings.args_table.fields
                (is_cmd(f1) && is_arg(f1)) || continue
                for al in f1.cmd_aliases
                    if opt_arg == al
                        found = true
                        opt_arg = f1.constant
                        break
                    end
                end
                found && break
            end
            !found && argparse_error("unknown command: $opt_arg")
            haskey(settings, opt_arg) || found_a_bug()
        end
        out_dict[f.dest_name] = opt_arg
        command = opt_arg
    else
        found_a_bug()
    end
    state.arg_consumed = arg_consumed
    state.command = command
    return
end

# parse long opts
function parse_long_opt!(state::ParserState, settings::ArgParseSettings)
    opt_name = state.token
    arg_after_eq = state.token_arg
    local f::ArgParseField
    local fln::AbstractString
    exact_match = false
    nfound = 0
    for g in settings.args_table.fields
        for ln in g.long_opt_name
            if ln == opt_name
                exact_match = true
                nfound = 1
                f = g
                fln = ln
                break
            elseif startswith(ln, opt_name)
                nfound += 1
                f = g
                fln = ln
            end
        end
        exact_match && break
    end
    nfound == 0 && argparse_error("unrecognized option --$opt_name")
    nfound > 1 && argparse_error("long option --$opt_name is ambiguous ($nfound partial matches)")

    opt_name = fln

    if is_flag(f)
        parse1_flag!(state, settings, f, arg_after_eq ≢ nothing, "--"*opt_name)
    else
        parse1_optarg!(state, settings, f, arg_after_eq, "--"*opt_name)
    end
    push!(state.found_args, idstring(f))
    return
end

# parse short opts
function parse_short_opt!(state::ParserState, settings::ArgParseSettings)
    shopts_lst = state.token
    rest_as_arg = nothing
    sind = firstindex(shopts_lst)
    while sind ≤ ncodeunits(shopts_lst)
        opt_char, next_sind = iterate(shopts_lst, sind)
        if next_sind ≤ ncodeunits(shopts_lst)
            next_opt_char, next2_sind = iterate(shopts_lst, next_sind)
            if next_opt_char == '='
                next_is_eq = true
                rest_as_arg = shopts_lst[next2_sind:end]
            else
                next_is_eq = false
                rest_as_arg = shopts_lst[next_sind:end]
            end
        else
            next_is_eq = false
            rest_as_arg = nothing
        end

        opt_name = string(opt_char)

        local f::ArgParseField
        found = false
        for outer f in settings.args_table.fields
            found |= any(sn->sn==opt_name, f.short_opt_name)
            found && break
        end
        found || argparse_error("unrecognized option -$opt_name")
        if is_flag(f)
            parse1_flag!(state, settings, f, next_is_eq, "-"*opt_name)
        else
            parse1_optarg!(state, settings, f, rest_as_arg, "-"*opt_name)
        end
        push!(state.found_args, idstring(f))
        state.arg_consumed && break
        if found_command(state)
            if rest_as_arg ≢ nothing && !isempty(rest_as_arg)
                startswith(rest_as_arg, '-') &&
                    argparse_error("illegal short options sequence after command " *
                                   "$(state.command): $rest_as_arg")
                pushfirst!(state.args_list, "-" * rest_as_arg)
                state.truncated_shopts = true
            end
            return
        end
        sind = next_sind
    end
end

# parse arg
function parse_arg!(state::ParserState, settings::ArgParseSettings)
    found = false
    local f::ArgParseField
    for new_arg_ind = state.last_arg+1:length(settings.args_table.fields)
        f = settings.args_table.fields[new_arg_ind]
        if is_arg(f) && !f.fake
            found = true
            state.last_arg = new_arg_ind
            break
        end
    end
    found || argparse_error("too many arguments")

    parse1_optarg!(state, settings, f, nothing, f.dest_name)

    push!(state.found_args, idstring(f))
    return
end

# convert_to_symbols
convert_to_symbols(::Nothing) = nothing
function convert_to_symbols(parsed_args::Dict{String,Any})
    new_parsed_args = Dict{Symbol,Any}()
    cmd = nothing
    if haskey(parsed_args, cmd_dest_name)
        cmd = parsed_args[cmd_dest_name]
        if cmd ≡ nothing
            scmd = nothing
        else
            scmd = Symbol(cmd)
            new_parsed_args[scmd] = convert_to_symbols(parsed_args[cmd])
        end
        new_parsed_args[scmd_dest_name] = scmd
    end
    for (k,v) in parsed_args
        (k == cmd_dest_name || k == cmd) && continue
        new_parsed_args[Symbol(k)] = v
    end
    return new_parsed_args
end


================================================
FILE: src/settings.jl
================================================
## All types, functions and constants related to the specification of the arguments

# actions
const all_actions = [:store_arg, :store_true, :store_false, :store_const,
                     :append_arg, :append_const, :count_invocations,
                     :command, :show_help, :show_version]

const internal_actions = [:store_arg, :store_true, :store_false, :store_const,
                          :append_arg, :append_const, :count_invocations,
                          :command_arg, :command_flag,
                          :show_help, :show_version]

const nonflag_actions = [:store_arg, :append_arg, :command_arg]
is_flag_action(a::Symbol) = a ∉ nonflag_actions

const multi_actions = [:append_arg, :append_const]
is_multi_action(a::Symbol) = a ∈ multi_actions

const command_actions = [:command_arg, :command_flag]
is_command_action(a::Symbol) = a ∈ command_actions

# ArgParseSettingsError
struct ArgParseSettingsError <: Exception
    text::AbstractString
end

serror(x...) = throw(ArgParseSettingsError(string(x...)))

# ArgConsumerType
struct ArgConsumerType
    desc::Union{Int,Symbol}
    function ArgConsumerType(n::Integer)
        n ≥ 0 || serror("nargs can't be negative")
        new(n)
    end
    function ArgConsumerType(s::Symbol)
        s ∈ [:A, :?, :*, :+, :R] ||
            serror("nargs must be an integer or one of 'A', '?', '*', '+', 'R'")
        new(s)
    end
end
ArgConsumerType(c::Char) = ArgConsumerType(Symbol(c))
ArgConsumerType() = ArgConsumerType(:A)

function show(io::IO, nargs::ArgConsumerType)
    print(io, nargs.desc isa Int ? nargs.desc : "'" * string(nargs.desc) * "'")
end

is_multi_nargs(nargs::ArgConsumerType) = nargs.desc ∉ (0, :A, :?)

default_action(nargs::Integer) = nargs == 0 ? :store_true : :store_arg
default_action(nargs::Char) = :store_arg
default_action(nargs::Symbol) = :store_arg

default_action(nargs::ArgConsumerType) = default_action(nargs.desc)

# ArgParseGroup
mutable struct ArgParseGroup
    name::AbstractString
    desc::AbstractString
    exclusive::Bool
    required::Bool
    function ArgParseGroup(name::AbstractString,
                  desc::AbstractString,
                  exclusive::Bool = false,
                  required::Bool = false
                 )
        new(name, desc, exclusive, required)
    end
end

const cmd_group = ArgParseGroup("commands", "commands")
const pos_group = ArgParseGroup("positional", "positional arguments")
const opt_group = ArgParseGroup("optional", "optional arguments")

const std_groups = [cmd_group, pos_group, opt_group]

# ArgParseField
mutable struct ArgParseField
    dest_name::AbstractString
    long_opt_name::Vector{AbstractString}
    short_opt_name::Vector{AbstractString}
    arg_type::Type
    action::Symbol
    nargs::ArgConsumerType
    default
    constant
    range_tester::Function
    required::Bool
    eval_arg::Bool
    help::AbstractString
    metavar::Union{AbstractString,Vector{<:AbstractString}}
    cmd_aliases::Vector{AbstractString}
    group::AbstractString
    fake::Bool
    ArgParseField() = new("", AbstractString[], AbstractString[], Any, :store_true,
                          ArgConsumerType(), nothing, nothing, _->true, false, false, "", "",
                          AbstractString[], "", false)
end

is_flag(arg::ArgParseField) = is_flag_action(arg.action)

is_arg(arg::ArgParseField) = isempty(arg.long_opt_name) && isempty(arg.short_opt_name)

is_cmd(arg::ArgParseField) = is_command_action(arg.action)

const cmd_dest_name = "%COMMAND%"
const scmd_dest_name = :_COMMAND_

function show(io::IO, s::ArgParseField)
    println(io, "ArgParseField(")
    for f in fieldnames(ArgParseField)
        println(io, "  ", f, "=", getfield(s, f))
    end
    print(io, "  )")
end

# ArgParseTable
mutable struct ArgParseTable
    fields::Vector{ArgParseField}
    subsettings::Dict{AbstractString,Any} # will actually be a Dict{AbstractString,ArgParseSettings}
    ArgParseTable() = new(ArgParseField[], Dict{AbstractString,Any}())
end

# disallow alphanumeric, -
function check_prefix_chars(chars)
    result = Set{Char}()
    for c in chars
        if isletter(c) || isnumeric(c) || c == '-'
            throw(ArgParseError("‘$(c)’ is not allowed as prefix character"))
        end
        push!(result, c)
    end
    result
end

# ArgParseSettings
"""
    ArgParseSettings

The `ArgParseSettings` object contains all the settings to be used during argument parsing. Settings
are divided in two groups: general settings and argument-table-related settings.
While the argument table requires specialized functions such as [`@add_arg_table!`](@ref) to be
defined and manipulated, general settings are simply object fields (most of them are `Bool` or
`String`) and can be passed to the constructor as keyword arguments, or directly set at any time.

This is the list of general settings currently available:

* `prog` (default = `""`): the name of the program, as displayed in the auto-generated help and
  usage screens. If left empty, the source file name will be used.
* `description` (default = `""`): a description of what the program does, to be displayed in the
  auto-generated help-screen, between the usage lines and the arguments description. If
  `preformatted_description` is `false` (see below), it will be automatically formatted, but you can
  still force newlines by using two consecutive newlines in the string, and manually control spaces
  by using non-breakable spaces (the character `'\\ua0'`).
* `preformatted_description` (default = `false`): disable automatic formatting of `description`.
* `epilog` (default = `""`): like `description`, but will be displayed at the end of the
  help-screen, after the arguments description. The same formatting rules also apply.
* `preformatted_epilog` (default = `false`): disable automatic formatting of `epilog`.
* `usage` (default = `""`): the usage line(s) to be displayed in the help screen and when an error
  is found during parsing. If left empty, it will be auto-generated.
* `version` (default = `"Unknown version"`): version information. It's used by the `:show_version`
  action.
* `add_help` (default = `true`): if `true`, a `--help, -h` option (triggering the `:show_help`
  action) is added to the argument table.
* `add_version` (default = `false`): if `true`, a `--version` option (triggering the `:show_version`
  action) is added to the argument table.
* `help_width` (default = `70`): set the width of the help text. This does not affect the
  `usage` line if it is provided by the user rather than being auto-generated; it also does not
  affect the `description/epilog` lines if the `preformatted_description/epilog` options are set to
  `false`.
* `help_alignment_width` (default = `24`): set the maximum width of the left column of the help
  text, where options and arguments names are listed. This also affects the indentation of the usage
  line when it is auto-generated. It must be ≥ 4 and should be less than `help_width`.
* `fromfile_prefix_chars` (default = `Set{Char}()`): an argument beginning with one of these
  characters will specify a file from which arguments will be read, one argument read per line.
  Alphanumeric characters and the hyphen-minus (`'-'`) are prohibited.
* `autofix_names` (default = `false`): if `true`, will try to automatically fix the uses of dashes
  (`'-'`) and underscores (`'_'`) in option names and destinations: all underscores will be
  converted to dashes in long option names; also, associated destination names, if auto-generated
  (see the [Argument names](@ref) section), will have dashes replaced with underscores, both for
  long options and for positional arguments. For example, an option declared as `"--my-opt"` will be
  associated with the key `"my_opt"` by default. It is especially advisable to turn this option on
  then parsing with the `as_symbols=true` argument to `parse_args`.
* `error_on_conflict` (default = `true`): if `true`, throw an error in case conflicting entries are
  added to the argument table; if `false`, later entries will silently take precedence. See the
  [Conflicts and overrides](@ref) srction for a detailed description of what conflicts are and what
  is the exact behavior when this setting is `false`.
* `suppress_warnings` (default = `false`): if `true`, all warnings will be suppressed.
* `allow_ambiguous_opts` (default = `false`): if `true`, ambiguous options such as `-1` will be
  accepted.
* `commands_are_required` (default = `true`): if `true`, commands will be mandatory. See the
  [Commands](@ref) section.
* `exc_handler` (default = `ArgParse.default_handler`): this is a function which is invoked when an
  error is detected during parsing (e.g. an option is not recognized, a required argument is not
  passed etc.). It takes two arguments: the `settings::ArgParseSettings` object and the
  `err::ArgParseError` exception. The default handler behaves differently depending on whether it's
  invoked from a script or in an interactive environment (e.g. REPL/IJulia). In non-interactive
  (script) mode, it calls `ArgParse.cmdline_handler`, which prints the error text and the usage
  screen on standard error and exits Julia with error code 1:

  ```julia
  function cmdline_handler(settings::ArgParseSettings, err, err_code::Int = 1)
      println(stderr, err.text)
      println(stderr, usage_string(settings))
      exit(err_code)
  end
  ```

  In interactive mode instead it calls the function `ArgParse.debug_handler`, which just rethrows
  the error.
* `exit_after_help` (default = `!isinteractive()`): exit Julia (with error code `0`) when the
  `:show_help` or `:show_version` actions are triggered. If `false`, those actions will just stop
  the parsing and make `parse_args` return `nothing`.

Here is a usage example:

```julia
settings = ArgParseSettings(description = "This program does something",
                            commands_are_required = false,
                            version = "1.0",
                            add_version = true)
```

which is also equivalent to:

```julia
settings = ArgParseSettings()
settings.description = "This program does something."
settings.commands_are_required = false
settings.version = "1.0"
settings.add_version = true
```

As a shorthand, the `description` field can be passed without keyword, which makes this equivalent
to the above:

```julia
settings = ArgParseSettings("This program does something",
                            commands_are_required = false,
                            version = "1.0",
                            add_version = true)
```

Most settings won't take effect until `parse_args` is invoked, but a few will have immediate
effects: `autofix_names`, `error_on_conflict`, `suppress_warnings`, `allow_ambiguous_opts`.
"""
mutable struct ArgParseSettings
    prog::AbstractString
    description::AbstractString
    epilog::AbstractString
    usage::AbstractString
    version::AbstractString
    add_help::Bool
    add_version::Bool
    help_width::Int
    help_alignment_width::Int
    fromfile_prefix_chars::Set{Char}
    autofix_names::Bool
    error_on_conflict::Bool
    suppress_warnings::Bool
    allow_ambiguous_opts::Bool
    commands_are_required::Bool
    args_groups::Vector{ArgParseGroup}
    default_group::AbstractString
    args_table::ArgParseTable
    exc_handler::Function
    preformatted_description::Bool
    preformatted_epilog::Bool
    exit_after_help::Bool

    function ArgParseSettings(;prog::AbstractString = Base.source_path() ≢ nothing ?
                                                          basename(Base.source_path()) :
                                                          "",
                               description::AbstractString = "",
                               epilog::AbstractString = "",
                               usage::AbstractString = "",
                               version::AbstractString = "Unspecified version",
                               add_help::Bool = true,
                               add_version::Bool = false,
                               help_width::Integer = 70,
                               help_alignment_width::Integer = 24,
                               fromfile_prefix_chars = Set{Char}(),
                               autofix_names::Bool = false,
                               error_on_conflict::Bool = true,
                               suppress_warnings::Bool = false,
                               allow_ambiguous_opts::Bool = false,
                               commands_are_required::Bool = true,
                               exc_handler::Function = default_handler,
                               preformatted_description::Bool = false,
                               preformatted_epilog::Bool = false,
                               exit_after_help::Bool = !isinteractive()
                               )
        fromfile_prefix_chars = check_prefix_chars(fromfile_prefix_chars)
        return new(
            prog, description, epilog, usage, version, add_help, add_version,
            help_width, help_alignment_width, fromfile_prefix_chars, autofix_names,
            error_on_conflict, suppress_warnings, allow_ambiguous_opts, commands_are_required,
            copy(std_groups), "", ArgParseTable(), exc_handler,
            preformatted_description, preformatted_epilog,
            exit_after_help
            )
    end
end

ArgParseSettings(desc::AbstractString; kw...) = ArgParseSettings(; description = desc, kw...)

function show(io::IO, s::ArgParseSettings)
    println(io, "ArgParseSettings(")
    for f in fieldnames(ArgParseSettings)
        f ∈ (:args_groups, :args_table) && continue
        println(io, "  ", f, "=", getfield(s, f))
    end
    println(io, "  >> ", usage_string(s))
    print(io, "  )")
end

ArgName{T<:AbstractString} = Union{T, Vector{T}}

getindex(s::ArgParseSettings, c::AbstractString) = s.args_table.subsettings[c]
haskey(s::ArgParseSettings, c::AbstractString) = haskey(s.args_table.subsettings, c)
setindex!(s::ArgParseSettings, x::ArgParseSettings, c::AbstractString) =
    setindex!(s.args_table.subsettings, x, c)

# fields declarations sanity checks
function check_name_format(name::ArgName)
    isempty(name) && serror("empty name")
    name isa Vector || return true
    allopts = true
    allargs = true
    for n in name
        isempty(n) && serror("empty name")
        if startswith(n, '-')
            allargs = false
        else
            allopts = false
        end
    end
    !(allargs || allopts) && serror("multiple names must be either all options or all non-options")
    for i1 = 1:length(name), i2 = i1+1:length(name)
        name[i1] == name[i2] && serror("duplicate name $(name[i1])")
    end
    return true
end

function check_type(opt, T::Type, message::AbstractString)
    opt isa T || serror(message)
    return true
end

function check_eltype(opt, T::Type, message::AbstractString)
    eltype(opt) <: T || serror(message)
    return true
end

function warn_extra_opts(opts, valid_keys::Vector{Symbol})
    for k in opts
        k ∈ valid_keys || @warn "ignored option: $k"
    end
    return true
end

function check_action_is_valid(action::Symbol)
    action ∈ all_actions || serror("invalid action: $action")
end

function check_nargs_and_action(nargs::ArgConsumerType, action::Symbol)
    is_flag_action(action) && nargs.desc ≠ 0 && nargs.desc ≠ :A &&
        serror("incompatible nargs and action (flag-action $action, nargs=$nargs)")
    is_command_action(action) && nargs.desc ≠ :A &&
        serror("incompatible nargs and action (command action, nargs=$nargs)")
    !is_flag_action(action) && nargs.desc == 0 &&
        serror("incompatible nargs and action (non-flag-action $action, nargs=$nargs)")
    return true
end

function check_long_opt_name(name::AbstractString, settings::ArgParseSettings)
    '=' ∈ name            && serror("illegal option name: $name (contains '=')")
    occursin(r"\s", name) && serror("illegal option name: $name (contains whitespace)")
    settings.add_help     &&
        name == "help"    && serror("option --help is reserved in the current settings")
    settings.add_version  &&
        name == "version" && serror("option --version is reserved in the current settings")
    return true
end

function check_short_opt_name(name::AbstractString, settings::ArgParseSettings)
    length(name) ≠ 1      && serror("short options must use a single character")
    name == "="           && serror("illegal short option name: $name")
    occursin(r"\s", name) && serror("illegal option name: $name (contains whitespace)")
    !settings.allow_ambiguous_opts && occursin(r"[0-9.(]", name) &&
                             serror("ambiguous option name: $name (disabled in current settings)")
    settings.add_help && name == "h" &&
                             serror("option -h is reserved for help in the current settings")
    return true
end

function check_arg_name(name::AbstractString)
    occursin(r"^%[A-Z]*%$", name) && serror("invalid positional arg name: $name (is reserved)")
    return true
end

function check_cmd_name(name::AbstractString)
    isempty(name) && found_a_bug()
    startswith(name, '-') && found_a_bug()
    occursin(r"\s", name) && serror("invalid command name: $name (contains whitespace)")
    occursin(r"^%[A-Z]*%$", name) && serror("invalid command name: $name (is reserved)")
    return true
end

function check_dest_name(name::AbstractString)
    occursin(r"^%[A-Z]*%$", name) && serror("invalid dest_name: $name (is reserved)")
    return true
end

function idstring(arg::ArgParseField)
    if is_arg(arg)
        return "argument $(arg.metavar)"
    elseif !isempty(arg.long_opt_name)
        return "option --$(arg.long_opt_name[1])"
    else
        return "option -$(arg.short_opt_name[1])"
    end
end

# TODO improve (test more nonsensical cases)
function check_arg_makes_sense(settings::ArgParseSettings, arg::ArgParseField)
    is_arg(arg) || return true
    is_command_action(arg.action) && return true

    for f in settings.args_table.fields
        is_arg(f) || continue
        is_command_action(f.action) && serror("non-command $(idstring(arg)) can't follow commands")
        !f.required && arg.required &&
            serror("required $(idstring(arg)) can't follow non-required arguments")
    end
    return true
end

function check_conflicts_with_commands(settings::ArgParseSettings,
                                       new_arg::ArgParseField,
                                       allow_future_merge::Bool)
    for cmd in keys(settings.args_table.subsettings)
        cmd == new_arg.dest_name &&
            serror("$(idstring(new_arg)) has the same destination of a command: $cmd")
    end
    for a in settings.args_table.fields
        if is_cmd(a) && !is_cmd(new_arg)
            for l1 in a.long_opt_name, l2 in new_arg.long_opt_name
                # TODO be less strict here and below, and allow partial override?
                l1 == l2 && serror("long opt name --$(l1) already in use by command $(a.constant)")
            end
            for s1 in a.short_opt_name, s2 in new_arg.short_opt_name
                s1 == s2 && serror("short opt name -$(s1) already in use by command $(a.constant)")
            end
        elseif is_cmd(a) && is_cmd(new_arg)
            if a.constant == new_arg.constant
                allow_future_merge || serror("command $(a.constant) already in use")
                is_arg(a) ≠ is_arg(new_arg) &&
                    serror("$(idstring(a)) and $(idstring(new_arg)) are incompatible")
            else
                for al in new_arg.cmd_aliases
                    al == a.constant && serror("invalid alias $al, command already in use")
                end
            end
        end
    end
    return true
end

function check_conflicts_with_commands(settings::ArgParseSettings, new_cmd::AbstractString)
    for a in settings.args_table.fields
        new_cmd == a.dest_name &&
            serror("command $new_cmd has the same destination of $(idstring(a))")
    end
    return true
end

function check_for_duplicates(args::Vector{ArgParseField}, new_arg::ArgParseField)
    for a in args
        for l1 in a.long_opt_name, l2 in new_arg.long_opt_name
            l1 == l2 && serror("duplicate long opt name $l1")
        end
        for s1 in a.short_opt_name, s2 in new_arg.short_opt_name
            s1 == s2 && serror("duplicate short opt name $s1")
        end
        if is_arg(a) && is_arg(new_arg) && a.metavar == new_arg.metavar
            serror("two arguments have the same metavar: $(a.metavar)")
        end
        if is_cmd(a) && is_cmd(new_arg)
            for al1 in a.cmd_aliases, al2 in new_arg.cmd_aliases
                al1 == al2 &&
                    serror("both commands $(a.constant) and $(new_arg.constant) use the same alias $al1")
            end
            for al1 in a.cmd_aliases
                al1 == new_arg.constant &&
                    serror("$al1 already in use as an alias command $(a.constant)")
            end
            for al2 in new_arg.cmd_aliases
                al2 == a.constant && serror("invalid alias $al2, command already in use")
            end
        end
        if a.dest_name == new_arg.dest_name
            a.arg_type == new_arg.arg_type ||
                serror("$(idstring(a)) and $(idstring(new_arg)) have the same destination but different arg types")
            if (is_multi_action(a.action) && !is_multi_action(new_arg.action)) ||
               (!is_multi_action(a.action) && is_multi_action(new_arg.action))
                serror("$(idstring(a)) and $(idstring(new_arg)) have the same destination but incompatible actions")
            end
        end
    end
    return true
end

function typecompatible(default::D, arg_type::Type) where D
    D <: arg_type && return true
    try
        applicable(convert, arg_type, default) ? convert(arg_type, default) : arg_type(default)
        return true
    catch
    end
    return false
end

check_default_type(default::Nothing, arg_type::Type) = true
function check_default_type(default::D, arg_type::Type) where D
    typecompatible(default, arg_type) || serror("typeof(default)=$D is incompatible with arg_type=$arg_type)")
    return true
end

check_default_type_multi(default::Nothing, arg_type::Type) = true
function check_default_type_multi(default::Vector, arg_type::Type)
    all(x->typecompatible(x, arg_type), default) || serror("all elements of the default value must be of type $arg_type or convertible to it")
    return true
end
check_default_type_multi(default::D, arg_type::Type) where D =
    serror("typeof(default)=$D is incompatible with nargs, it should be a Vector")

check_default_type_multi2(default::Nothing, arg_type::Type) = true
function check_default_type_multi2(default::Vector{D}, arg_type::Type) where D
    all(y->(y isa Vector), default) ||
        serror("the default $(default) is incompatible with the action and nargs, it should be a Vector of Vectors")
    all(y->all(x->typecompatible(x, arg_type), y), default) || serror("all elements of the default value must be of type $arg_type or convertible to it")
    return true
end
check_default_type_multi2(default::D, arg_type::Type) where D =
    serror("the default $(default) is incompatible with the action and nargs, it should be a Vector of Vectors")

function _convert_default(arg_type::Type, default::D) where D
    D <: arg_type && return default
    applicable(convert, arg_type, default) ? convert(arg_type, default) : arg_type(default)
end

convert_default(arg_type::Type, default::Nothing) = nothing
convert_default(arg_type::Type, default) = _convert_default(arg_type, default)

convert_default_multi(arg_type::Type, default::Nothing) = Array{arg_type}(undef, 0)
convert_default_multi(arg_type::Type, default::Vector) = arg_type[_convert_default(arg_type, x) for x in default]

convert_default_multi2(arg_type::Type, default::Nothing) = Array{Vector{arg_type}}(undef, 0)
convert_default_multi2(arg_type::Type, default::Vector) = Vector{arg_type}[arg_type[_convert_default(arg_type, x) for x in y] for y in default]


check_range_default(default::Nothing, range_tester::Function) = true
function check_range_default(default, range_tester::Function)
    local res::Bool
    try
        res = range_tester(default)
    catch err
        serror("the range_tester function must be defined for the default value, and return a Bool")
    end
    res || serror("the default value must pass the range_tester function")
    return true
end

check_range_default_multi(default::Nothing, range_tester::Function) = true
function check_range_default_multi(default::Vector, range_tester::Function)
    for d in default
        local res::Bool
        try
            res = range_tester(d)
        catch err
            serror("the range_tester function must be defined for all the default values, and return a Bool")
        end
        res || serror("all of the default values must pass the range_tester function")
    end
    return true
end

check_range_default_multi2(default::Nothing, range_tester::Function) = true
function check_range_default_multi2(default::Vector, range_tester::Function)
    for dl in default, d in dl
        local res::Bool
        try
            res = range_tester(d)
        catch err
            serror("the range_tester function must be defined for all the default values, and return a Bool")
        end
        res || serror("all of the default values must pass the range_tester function")
    end
    return true
end

function check_metavar(metavar::AbstractString)
    isempty(metavar)         && serror("empty metavar")
    startswith(metavar, '-') && serror("metavars cannot begin with -")
    occursin(r"\s", metavar) && serror("illegal metavar name: $metavar (contains whitespace)")
    return true
end

function check_metavar(metavar::Vector{<:AbstractString})
    foreach(check_metavar, metavar)
    return true
end

function check_group_name(name::AbstractString)
    isempty(name)         && serror("empty group name")
    startswith(name, '#') && serror("invalid group name (starts with #)")
    return true
end

# add_arg_table! and related
function name_to_fieldnames!(settings::ArgParseSettings, name::ArgName)
    pos_arg = ""
    long_opts = AbstractString[]
    short_opts = AbstractString[]
    aliases = AbstractString[]
    r(n) = settings.autofix_names ? replace(n, '_' => '-') : n
    function do_one(n, cmd_check = true)
        if startswith(n, "--")
            n == "--" && serror("illegal option name: --")
            long_opt_name = r(n[3:end])
            check_long_opt_name(long_opt_name, settings)
            push!(long_opts, long_opt_name)
        elseif startswith(n, '-')
            n == "-" && serror("illegal option name: -")
            short_opt_name = n[2:end]
            check_short_opt_name(short_opt_name, settings)
            push!(short_opts, short_opt_name)
        else
            if cmd_check
                check_cmd_name(n)
            else
                check_arg_name(n)
            end
            if isempty(pos_arg)
                pos_arg = n
            else
                push!(aliases, n)
            end
        end
    end

    if name isa Vector
        foreach(do_one, name)
    else
        do_one(name, false)
    end
    return pos_arg, long_opts, short_opts, aliases
end

function auto_dest_name(pos_arg::AbstractString,
                        long_opts::Vector{AbstractString},
                        short_opts::Vector{AbstractString},
                        autofix_names::Bool)
    r(n) = autofix_names ? replace(n, '-' => '_') : n
    isempty(pos_arg) || return r(pos_arg)
    isempty(long_opts) || return r(long_opts[1])
    @assert !isempty(short_opts)
    return short_opts[1]
end

function auto_metavar(dest_name::AbstractString, is_opt::Bool)
    is_opt || return dest_name
    prefix = occursin(r"^[[:alpha:]_]", dest_name) ? "" : "_"
    return prefix * uppercase(dest_name)
end

function get_cmd_prog_hint(arg::ArgParseField)
    isempty(arg.short_opt_name) || return "-" * arg.short_opt_name[1]
    isempty(arg.long_opt_name) || return "--" * arg.long_opt_name[1]
    return arg.constant
end


"""
    add_arg_table!(settings, [arg_name [,arg_options]]...)

This function is very similar to the macro version [`@add_arg_table!`](@ref). Its syntax is stricter:
tuples and blocks are not allowed and argument options are explicitly specified as `Dict` objects.
However, since it doesn't involve macros, it offers more flexibility in other respects, e.g. the
`arg_name` entries need not be explicit, they can be anything which evaluates to a `String` or a
`Vector{String}`.

Example:

```julia
add_arg_table!(settings,
    ["--opt1", "-o"],
    Dict(
        :help => "an option with an argument"
    ),
    "--opt2",
    "arg1",
    Dict(
        :help => "a positional argument"
        :required => true
    ))
```
"""
function add_arg_table!(settings::ArgParseSettings, table::Union{ArgName,Vector,Dict}...)
    has_name = false
    for i = 1:length(table)
        !has_name && !(table[i] isa ArgName) &&
            serror("option field must be preceded by the arg name")
        has_name = true
    end
    i = 1
    while i ≤ length(table)
        if i+1 ≤ length(table) && !(table[i+1] isa ArgName)
            add_arg_field!(settings, table[i]; table[i+1]...)
            i += 2
        else
            add_arg_field!(settings, table[i])
            i += 1
        end
    end
    return settings
end

"""
    @add_arg_table!(settings, table...)

This macro adds a table of arguments and options to the given `settings`. It can be invoked multiple
times. The arguments groups are determined automatically, or the current default group is used if
specified (see the [Argument groups](@ref) section for more details).

The `table` is a list in which each element can be either `String`, or a tuple or a vector of
`String`, or an assigmment expression, or a block:

* a `String`, a tuple or a vector introduces a new positional argument or option. Tuples and vectors
  are only allowed for options or commands, and provide alternative names (e.g. `["--opt", "-o"]` or
  `["checkout", "co"]`)
* assignment expressions (i.e. expressions using `=`, `:=` or `=>`) describe the previous argument
  behavior (e.g.  `help = "an option"` or `required => false`).  See the
  [Argument entry settings](@ref) section for a complete description
* blocks (`begin...end` or lists of expressions in parentheses separated by semicolons) are useful
  to group entries and span multiple lines.

These rules allow for a variety usage styles, which are discussed in the
[Argument table styles](@ref) section. In the rest of the documentation, we will mostly use this
style:

```julia
@add_arg_table! settings begin
    "--opt1", "-o"
        help = "an option with an argument"
    "--opt2"
    "arg1"
        help = "a positional argument"
        required = true
end
```

In the above example, the `table` is put in a single `begin...end` block and the line
`"--opt1", "-o"` is parsed as a tuple; indentation is used to help readability.

See also the function [`add_arg_table!`](@ref).
"""
macro add_arg_table!(s, x...)
    _add_arg_table!(s, x...)
end

# Moved all the code to a function just to make the deprecation work
function _add_arg_table!(s, x...)
    # transform the tuple into a vector, so that
    # we can manipulate it
    x = Any[x...]
    # escape the ArgParseSettings
    s = esc(s)
    z = esc(gensym())
    # start building the return expression
    exret = quote
        $z = $s
        $z isa ArgParseSettings ||
            serror("first argument to @add_arg_table! must be of type ArgParseSettings")
    end
    # initialize the name and the options expression
    name = nothing
    exopt = Any[:Dict]

    # iterate over the arguments
    i = 1
    while i ≤ length(x)
        y = x[i]
        if Meta.isexpr(y, :block)
            # found a begin..end block: expand its contents
            # in-place and restart from the same position
            splice!(x, i, y.args)
            continue
        elseif Meta.isexpr(y, :macrocall) &&
               ((y.args[1] == GlobalRef(Core, Symbol("@doc"))) ||
               (Meta.isexpr(y.args[1], :core) && y.args[1].args[1] == Symbol("@doc")))
            # Was parsed as doc syntax. Split into components
            splice!(x, i, y.args[2:end])
            continue
        elseif (y isa AbstractString) || Meta.isexpr(y, (:vect, :tuple))
            Meta.isexpr(y, :tuple) && (y.head = :vect) # transform tuples into vectors
            if Meta.isexpr(y, :vect) && (isempty(y.args) || !all(x->x isa AbstractString, y.args))
                # heterogeneous elements: splice it in place, just like blocks
                splice!(x, i, y.args)
                continue
            end
            # found a string, or a vector/tuple of strings:
            # this must be the option name
            if name ≢ nothing
                # there was a previous arg field on hold
                # first, concretely build the options
                opt = Expr(:call, exopt...)
                kopts = Expr(:parameters, Expr(:(...), opt))
                # then, call add_arg_field!
                aaf = Expr(:call, :add_arg_field!, kopts, z, name)
                # store it in the output expression
                exret = quote
                    $exret
                    $aaf
                end
            end
            # put the name on hold, reinitialize the options expression
            name = y
            exopt = Any[:Dict]
            i += 1
        elseif Meta.isexpr(y, (:(=), :(:=), :kw))
            # found an assignment: add it to the current options expression
            name ≢ nothing ||
                serror("malformed table: description fields must be preceded by the arg name")
            push!(exopt, Expr(:call, :(=>), Expr(:quote, y.args[1]), esc(y.args[2])))
            i += 1
        elseif Meta.isexpr(y, :call) && y.args[1] == :(=>)
            # found an assignment: add it to the current options expression
            name ≢ nothing ||
                serror("malformed table: description fields must be preceded by the arg name")
            push!(exopt, Expr(:call, :(=>), Expr(:quote, y.args[2]), esc(y.args[3])))
            i += 1
        elseif (y isa LineNumberNode) || Meta.isexpr(y, :line)
            # a line number node, ignore
            i += 1
            continue
        else
            # anything else: ignore, but issue a warning
            @warn "@add_arg_table!: ignoring expression $y"
            i += 1
        end
    end
    if name ≢ nothing
        # there is an arg field on hold
        # same as above
        opt = Expr(:call, exopt...)
        kopts = Expr(:parameters, Expr(:(...), opt))
        aaf = Expr(:call, :add_arg_field!, kopts, z, name)
        exret = quote
            $exret
            $aaf
        end
    end

    # the return value when invoking the macro
    # will be the ArgParseSettings object
    exret = quote
        $exret
        $z
    end

    # return the resulting expression
    exret
end

function get_group(group::AbstractString, arg::ArgParseField, settings::ArgParseSettings)
    if isempty(group)
        is_cmd(arg) && return cmd_group
        is_arg(arg) && return pos_group
        return opt_group
    else
        for ag in settings.args_groups
            group == ag.name && return ag
        end
        serror("group $group not found, use add_arg_group! to add it")
    end
    found_a_bug()
end

function add_arg_field!(settings::ArgParseSettings, name::ArgName; desc...)
    check_name_format(name)

    supplied_opts = keys(desc)

    @defaults desc begin
        nargs = ArgConsumerType()
        action = default_action(nargs)
        arg_type = Any
        default = nothing
        constant = nothing
        required = false
        range_tester = x->true
        eval_arg = false
        dest_name = ""
        help = ""
        metavar = ""
        force_override = !settings.error_on_conflict
        group = settings.default_group
    end

    check_type(nargs, Union{ArgConsumerType,Int,Char}, "nargs must be an Int or a Char")
    check_type(action, Union{AbstractString,Symbol}, "action must be an AbstractString or a Symbol")
    check_type(arg_type, Type, "invalid arg_type")
    check_type(required, Bool, "required must be a Bool")
    check_type(range_tester, Function, "range_tester must be a Function")
    check_type(dest_name, AbstractString, "dest_name must be an AbstractString")
    check_type(help, AbstractString, "help must be an AbstractString")
    # Check metavar's type to be either an AbstractString or a
    # Vector{T<:AbstractString}
    metavar_error = "metavar must be an AbstractString or a Vector{<:AbstractString}"
    if !(metavar isa AbstractString)
        check_type(metavar, Vector, metavar_error)
        check_eltype(metavar, AbstractString, metavar_error)
        check_type(nargs, Integer, "nargs must be an integer for multiple metavars")
        length(metavar) == nargs || serror("metavar array must have length of nargs")
    end
    check_type(force_override, Bool, "force_override must be a Bool")
    check_type(group, Union{AbstractString,Symbol}, "group must be an AbstractString or a Symbol")

    nargs isa ArgConsumerType || (nargs = ArgConsumerType(nargs))
    action isa Symbol || (action = Symbol(action))

    is_opt = name isa Vector ?
        startswith(first(name), '-') :
        startswith(name, '-')

    check_action_is_valid(action)

    action == :command && (action = is_opt ? :command_flag : :command_arg)

    check_nargs_and_action(nargs, action)

    new_arg = ArgParseField()

    is_flag = is_flag_action(action)

    if !is_opt
        is_flag && serror("invalid action for positional argument: $action")
        nargs.desc == :? && serror("invalid 'nargs' for positional argument: '?'")
        metavar isa Vector && serror("multiple metavars only supported for optional arguments")
    end

    pos_arg, long_opts, short_opts, cmd_aliases = name_to_fieldnames!(settings, name)

    if !isempty(cmd_aliases)
        is_command_action(action) || serror("only command arguments can have multiple names (aliases)")
    end

    new_arg.dest_name = auto_dest_name(pos_arg, long_opts, short_opts, settings.autofix_names)

    new_arg.long_opt_name = long_opts
    new_arg.short_opt_name = short_opts
    new_arg.cmd_aliases = cmd_aliases
    new_arg.nargs = nargs
    new_arg.action = action

    group = string(group)
    if :group ∈ supplied_opts && !isempty(group)
        check_group_name(group)
    end
    arg_group = get_group(group, new_arg, settings)
    new_arg.group = arg_group.name
    if arg_group.exclusive && (!is_opt || is_command_action(action))
        serror("group $(new_arg.group) is mutually-exclusive, actions and commands are not allowed")
    end

    if action ∈ (:store_const, :append_const) && :constant ∉ supplied_opts
        serror("action $action requires the 'constant' field")
    end

    valid_keys = [:nargs, :action, :help, :force_override, :group]
    if is_flag
        if action ∈ (:store_const, :append_const)
            append!(valid_keys, [:default, :constant, :arg_type, :dest_name])
        elseif action ∈ (:store_true, :store_false, :count_invocations, :command_flag)
            push!(valid_keys, :dest_name)
        else
            action ∈ (:show_help, :show_version) || found_a_bug()
        end
    elseif is_opt
        append!(valid_keys,
                [:arg_type, :default, :range_tester, :dest_name, :required, :metavar, :eval_arg])
        nargs.desc == :? && push!(valid_keys, :constant)
    elseif action ≠ :command_arg
        append!(valid_keys, [:arg_type, :default, :range_tester, :required, :metavar])
    end
    settings.suppress_warnings || warn_extra_opts(supplied_opts, valid_keys)

    if is_command_action(action)
        if (:dest_name ∈ supplied_opts) && (:dest_name ∈ valid_keys)
            cmd_name = dest_name
        else
            cmd_name = new_arg.dest_name
        end
    end
    if (:dest_name ∈ supplied_opts) && (:dest_name ∈ valid_keys) && (action ≠ :command_flag)
        new_arg.dest_name = dest_name
    end

    check_dest_name(dest_name)

    set_if_valid(k, x) = k ∈ valid_keys && setfield!(new_arg, k, x)

    set_if_valid(:arg_type, arg_type)
    set_if_valid(:default, deepcopy(default))
    set_if_valid(:constant, deepcopy(constant))
    set_if_valid(:range_tester, range_tester)
    set_if_valid(:required, required)
    set_if_valid(:help, help)
    set_if_valid(:metavar, metavar)
    set_if_valid(:eval_arg, eval_arg)

    if !is_flag
        isempty(new_arg.metavar) && (new_arg.metavar = auto_metavar(new_arg.dest_name, is_opt))
        check_metavar(new_arg.metavar)
    end

    if is_command_action(action)
        new_arg.dest_name = cmd_dest_name
        new_arg.arg_type = AbstractString
        new_arg.constant = cmd_name
        new_arg.metavar = cmd_name
        cmd_prog_hint = get_cmd_prog_hint(new_arg)
    end

    if is_flag
        if action == :store_true
            new_arg.arg_type = Bool
            new_arg.default = false
            new_arg.constant =  true
        elseif action == :store_false
            new_arg.arg_type = Bool
            new_arg.default = true
            new_arg.constant =  false
        elseif action == :count_invocations
            new_arg.arg_type = Int
            new_arg.default = 0
        elseif action == :store_const
            check_default_type(new_arg.default, new_arg.arg_type)
            check_default_type(new_arg.constant, new_arg.arg_type)
            new_arg.default = convert_default(new_arg.arg_type, new_arg.default)
        elseif action == :append_const
            check_default_type(new_arg.constant, new_arg.arg_type)
            if :arg_type ∉ supplied_opts
                new_arg.arg_type = typeof(new_arg.constant)
            end
            check_default_type_multi(new_arg.default, new_arg.arg_type)
            new_arg.default = convert_default_multi(new_arg.arg_type, new_arg.default)
        elseif action == :command_flag
            # nothing to do
        elseif action == :show_help || action == :show_version
            # nothing to do
        else
            found_a_bug()
        end
    else
        arg_type = new_arg.arg_type
        range_tester = new_arg.range_tester
        default = new_arg.default

        if !is_multi_action(new_arg.action) && !is_multi_nargs(new_arg.nargs)
            check_default_type(default, arg_type)
            check_range_default(default, range_tester)
            new_arg.default = convert_default(arg_type, default)
        elseif !is_multi_action(new_arg.action) || !is_multi_nargs(new_arg.nargs)
            check_default_type_multi(default, arg_type)
            check_range_default_multi(default, range_tester)
            new_arg.default = convert_default_multi(arg_type, default)
        else
            check_default_type_multi2(default, arg_type)
            check_range_default_multi2(default, range_tester)
            new_arg.default = convert_default_multi2(arg_type, default)
        end

        if is_opt && nargs.desc == :?
            constant = new_arg.constant
            check_default_type(constant, arg_type)
            check_range_default(constant, range_tester)
            new_arg.constant = convert_default(arg_type, constant)
        end
    end

    if action == :command_arg
        for f in settings.args_table.fields
            if f.action == :command_arg
                new_arg.fake = true
                break
            end
        end
    end

    check_arg_makes_sense(settings, new_arg)

    check_conflicts_with_commands(settings, new_arg, false)
    if force_override
        override_duplicates!(settings.args_table.fields, new_arg)
    else
        check_for_duplicates(settings.args_table.fields, new_arg)
    end
    push!(settings.args_table.fields, new_arg)
    is_command_action(action) && add_command!(settings, cmd_name, cmd_prog_hint, force_override)
    return
end

function add_command!(settings::ArgParseSettings,
                      command::AbstractString,
                      prog_hint::AbstractString,
                      force_override::Bool)
    haskey(settings, command) && serror("command $command already added")
    if force_override
        override_conflicts_with_commands!(settings, command)
    else
        check_conflicts_with_commands(settings, command)
    end
    settings[command] = ArgParseSettings()
    ss = settings[command]
    ss.prog = "$(isempty(settings.prog) ? "<PROGRAM>" : settings.prog) $prog_hint"
    ss.description = ""
    ss.preformatted_description = settings.preformatted_description
    ss.epilog = ""
    ss.preformatted_epilog = settings.preformatted_epilog
    ss.usage = ""
    ss.version = settings.version
    ss.add_help = settings.add_help
    ss.add_version = settings.add_version
    ss.autofix_names = settings.autofix_names
    ss.fromfile_prefix_chars = settings.fromfile_prefix_chars
    ss.error_on_conflict = settings.error_on_conflict
    ss.suppress_warnings = settings.suppress_warnings
    ss.allow_ambiguous_opts = settings.allow_ambiguous_opts
    ss.exc_handler = settings.exc_handler
    ss.exit_after_help = settings.exit_after_help

    return ss
end

autogen_group_name(desc::AbstractString) = "#$(hash(desc))"

add_arg_group!(settings::ArgParseSettings, desc::AbstractString;
               exclusive::Bool = false, required::Bool = false) =
    _add_arg_group!(settings, desc, autogen_group_name(desc), true, exclusive, required)


"""
    add_arg_group!(settings, description, [name , [set_as_default]]; keywords...)

This function adds an argument group to the argument table in `settings`. The `description` is a
`String` used in the help screen as a title for that group. The `name` is a unique name which can be
provided to refer to that group at a later time.

Groups can be declared to be mutually exclusive and/or required, see below.

After invoking this function, all subsequent invocations of the [`@add_arg_table!`](@ref) macro and
[`add_arg_table!`](@ref) function will use the new group as the default, unless `set_as_default` is
set to `false` (the default is `true`, and the option can only be set if providing a `name`).
Therefore, the most obvious usage pattern is: for each group, add it and populate the argument
table of that group. Example:

```julia-repl
julia> settings = ArgParseSettings();

julia> add_arg_group!(settings, "custom group");

julia> @add_arg_table! settings begin
          "--opt"
          "arg"
       end;

julia> parse_args(["--help"], settings)
usage: <command> [--opt OPT] [-h] [arg]

optional arguments:
  -h, --help  show this help message and exit

custom group:
  --opt OPT
  arg
```

As seen from the example, new groups are always added at the end of existing ones.

The `name` can also be passed as a `Symbol`. Forbidden names are the standard groups names
(`"command"`, `"positional"` and `"optional"`) and those beginning with a hash character `'#'`.

In order to declare a group as mutually exclusive, use the keyword `exclusive = true`. Mutually
exclusive groups can only contain options, not arguments nor commands, and parsing will fail if more
than one option from the group is provided.

A group can be declared as required using the `required = true` keyword, in which case at least one
option or positional argument or command from the group must be provided.
"""
function add_arg_group!(settings::ArgParseSettings,
                        desc::AbstractString,
                        tag::Union{AbstractString,Symbol},
                        set_as_default::Bool = true;
                        exclusive::Bool = false,
                        required::Bool = false
                       )
    name = string(tag)
    check_group_name(name)
    _add_arg_group!(settings, desc, name, set_as_default, exclusive, required)
end

function _add_arg_group!(settings::ArgParseSettings,
                         desc::AbstractString,
                         name::AbstractString,
                         set_as_default::Bool,
                         exclusive::Bool,
                         required::Bool
                        )
    already_added = any(ag->ag.name==name, settings.args_groups)
    already_added || push!(settings.args_groups, ArgParseGroup(name, desc, exclusive, required))
    set_as_default && (settings.default_group = name)
    return settings
end

"""
    set_default_arg_group!(settings, [name])

Set the default group for subsequent invocations of the [`@add_arg_table!`](@ref) macro and
[`add_arg_table!`](@ref) function. `name` is a `String`, and must be one of the standard group names
(`"command"`, `"positional"` or `"optional"`) or one of the user-defined names given in
`add_arg_group!` (groups with no assigned name cannot be used with this function).

If `name` is not provided or is the empty string `""`, then the default behavior is reset (i.e.
arguments will be automatically assigned to the standard groups). The `name` can also be passed as a
`Symbol`.
"""
function set_default_arg_group!(settings::ArgParseSettings, name::Union{AbstractString,Symbol} = "")
    name = string(name)
    startswith(name, '#') && serror("invalid group name: $name (begins with #)")
    isempty(name) && (settings.default_group = ""; return)
    found = any(ag->ag.name==name, settings.args_groups)
    found || serror("group $name not found")
    settings.default_group = name
    return
end

# import_settings! & friends
function override_conflicts_with_commands!(settings::ArgParseSettings, new_cmd::AbstractString)
    ids0 = Int[]
    for ia in 1:length(settings.args_table.fields)
        a = settings.args_table.fields[ia]
        new_cmd == a.dest_name && push!(ids0, ia)
    end
    while !isempty(ids0)
        splice!(settings.args_table.fields, pop!(ids0))
    end
end
function override_duplicates!(args::Vector{ArgParseField}, new_arg::ArgParseField)
    ids0 = Int[]
    for (ia,a) in enumerate(args)
        if (a.dest_name == new_arg.dest_name) &&
            ((a.arg_type ≠ new_arg.arg_type) ||
             (is_multi_action(a.action) && !is_multi_action(new_arg.action)) ||
             (!is_multi_action(a.action) && is_multi_action(new_arg.action)))
            # unsolvable conflict, mark for deletion
            push!(ids0, ia)
            continue
        end
        if is_arg(a) && is_arg(new_arg) && !(is_cmd(a) && is_cmd(new_arg)) &&
            a.metavar == new_arg.metavar
            # unsolvable conflict, mark for deletion
            push!(ids0, ia)
            continue
        end

        # delete conflicting command aliases for different commands
        if is_cmd(a) && is_cmd(new_arg) && a.constant ≠ new_arg.constant
            ids = Int[]
            for (ial1, al1) in enumerate(a.cmd_aliases)
                if al1 == new_arg.constant
                    push!(ids, ial1)
                else
                    for al2 in new_arg.cmd_aliases
                        al1 == al2 && push!(ids, ial1)
                    end
                end
            end
            while !isempty(ids)
                splice!(a.cmd_aliases, pop!(ids))
            end
        end

        if is_arg(a) || is_arg(new_arg)
            # not an option, skip
            continue
        end

        if is_cmd(a) && is_cmd(new_arg) && a.constant == new_arg.constant && !is_arg(a)
            is_arg(new_arg) && found_a_bug() # this is ensured by check_settings_are_compatible
            # two command flags with the same command -> should have already been taken care of,
            # by either check_settings_are_compatible or merge_commands!
            continue
        end

        # delete conflicting long options
        ids = Int[]
        for il1 = 1:length(a.long_opt_name), l2 in new_arg.long_opt_name
            l1 = a.long_opt_name[il1]
            l1 == l2 && push!(ids, il1)
        end
        while !isempty(ids)
            splice!(a.long_opt_name, pop!(ids))
        end

        # delete conflicting short options
        ids = Int[]
        for is1 in 1:length(a.short_opt_name), s2 in new_arg.short_opt_name
            s1 = a.short_opt_name[is1]
            s1 == s2 && push!(ids, is1)
        end
        while !isempty(ids)
            splice!(a.short_opt_name, pop!(ids))
        end

        # if everything was deleted, remove the field altogether
        # (i.e. mark it for deletion)
        isempty(a.long_opt_name) && isempty(a.short_opt_name) && push!(ids0, ia)
    end

    # actually remove the marked fields
    while !isempty(ids0)
        splice!(args, pop!(ids0))
    end
end

function check_settings_are_compatible(settings::ArgParseSettings, other::ArgParseSettings)
    table = settings.args_table
    otable = other.args_table

    for a in otable.fields
        check_conflicts_with_commands(settings, a, true)
        settings.error_on_conflict && check_for_duplicates(table.fields, a)
    end

    for (subk, subs) in otable.subsettings
        settings.error_on_conflict && check_conflicts_with_commands(settings, subk)
        haskey(settings, subk) && check_settings_are_compatible(settings[subk], subs)
    end
    return true
end

function merge_commands!(fields::Vector{ArgParseField}, ofields::Vector{ArgParseField})
    oids = Int[]
    for a in fields, ioa = 1:length(ofields)
        oa = ofields[ioa]
        if is_cmd(a) && is_cmd(oa) && a.constant == oa.constant
            is_arg(a) ≠ is_arg(oa) && found_a_bug() # ensured by check_settings_are_compatible
            for l in oa.long_opt_name
                l ∈ a.long_opt_name || push!(a.long_opt_name, l)
            end
            for s in oa.short_opt_name
                s ∈ a.short_opt_name || push!(a.short_opt_name, s)
            end
            for al in oa.cmd_aliases
                al ∈ a.cmd_aliases || push!(a.cmd_aliases, al)
            end
            a.group = oa.group # note: the group may not be present yet, but it will be
                               #       added later
            push!(oids, ioa)
        end
    end
    # we return the merged ofields indices, since we still need to use them for overriding options
    # before we actually remove them
    return oids
end

function fix_commands_fields!(fields::Vector{ArgParseField})
    cmd_found = false
    for a in fields
        if is_arg(a) && is_cmd(a)
            a.fake = cmd_found
            cmd_found = true
        end
    end
end

"""
    import_settings!(settings, other_settings [,args_only])

Imports `other_settings` into `settings`, where both are [`ArgParseSettings`](@ref) objects. If
`args_only` is `true` (this is the default), only the argument table will be imported; otherwise,
the default argument group will also be imported, and all general settings except `prog`,
`description`, `epilog`, `usage` and `version`.

Sub-settings associated with commands will also be imported recursively; the `args_only` setting
applies to those as well. If there are common commands, their sub-settings will be merged.

While importing, conflicts may arise: if `settings.error_on_conflict` is `true`, this will result in
an error, otherwise conflicts will be resolved in favor of `other_settings` (see the
[Conflicts and overrides](@ref) section for a detailed discussion of how conflicts are handled).

Argument groups will also be imported; if two groups in `settings` and `other_settings` match, they
are merged (groups match either by name, or, if unnamed, by their description).

Note that the import will have effect immediately: any subsequent modification of `other_settings`
will not have any effect on `settings`.

This function can be used at any time.
"""
function import_settings!(settings::ArgParseSettings,
                          other::ArgParseSettings;
                          args_only::Bool = true)
    check_settings_are_compatible(settings, other)

    fields = settings.args_table.fields
    ofields = deepcopy(other.args_table.fields)
    merged_oids = merge_commands!(fields, ofields)
    if !settings.error_on_conflict
        for a in ofields
            override_duplicates!(fields, a)
        end
        for (subk, subs) in other.args_table.subsettings
            override_conflicts_with_commands!(settings, subk)
        end
    end
    while !isempty(merged_oids)
        splice!(ofields, pop!(merged_oids))
    end
    append!(fields, ofields)
    for oag in other.args_groups
        skip = false
        for ag in settings.args_groups
            # TODO: merge groups in some cases
            if oag.name == ag.name
                skip = true
                break
            end
        end
        skip && continue
        push!(settings.args_groups, deepcopy(oag))
    end

    fix_commands_fields!(fields)

    if !args_only
        settings.add_help = other.add_help
        settings.add_version = other.add_version
        settings.error_on_conflict = other.error_on_conflict
        settings.suppress_warnings = other.suppress_warnings
        settings.exc_handler = other.exc_handler
        settings.allow_ambiguous_opts = other.allow_ambiguous_opts
        settings.commands_are_required = other.commands_are_required
        settings.default_group = other.default_group
        settings.preformatted_description = other.preformatted_description
        settings.preformatted_epilog = other.preformatted_epilog
        settings.fromfile_prefix_chars = other.fromfile_prefix_chars
        settings.autofix_names = other.autofix_names
        settings.exit_after_help = other.exit_after_help
    end
    for (subk, subs) in other.args_table.subsettings
        cmd_prog_hint = ""
        for oa in other.args_table.fields
            if is_cmd(oa) && oa.constant == subk
                cmd_prog_hint = get_cmd_prog_hint(oa)
                break
            end
        end
        if !haskey(settings, subk)
            add_command!(settings, subk, cmd_prog_hint, !settings.error_on_conflict)
        elseif !isempty(cmd_prog_hint)
            settings[subk].prog = "$(settings.prog) $cmd_prog_hint"
        end
        import_settings!(settings[subk], subs, args_only=args_only)
    end
    return settings
end

"""
    @project_version
    @project_version(filename::String...)

Reads the version from the Project.toml file at the given filename, at compile time.
If no filename is given, defaults to `Base.current_project()`.
If multiple strings are given, they will be joined with `joinpath`.
Intended for use with the [`ArgParseSettings`](@ref) constructor,
to keep the settings version in sync with the project version.

## Example

```julia
ArgParseSettings(add_version = true, version = @project_version)
```
"""
macro project_version(filename::Vararg{String})
    project_version(isempty(filename) ? Base.current_project() : joinpath(filename...))
end

function project_version(filename::AbstractString)::String
    re = r"^version\s*=\s*\"(.*)\"\s*$"
    for line in eachline(filename)
        if startswith(line, "[")
            break
        end
        if !occursin(re, line)
            continue
        end
        return match(re, line)[1]
    end
    throw(ArgumentError("Could not find a version in the file at $(filename)"))
end


================================================
FILE: test/Project.toml
================================================
version = "1.0.0"

[deps]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"


================================================
FILE: test/argparse_test01.jl
================================================
# test 01: minimal options/arguments, auto-generated help/version;
#          function version of add_arg_table!

@testset "test 01" begin

function ap_settings1()

    s = ArgParseSettings()

    @add_arg_table! s begin
        "--opt1"               # an option (will take an argument)
        "--opt2", "-o"         # another option, with short form
        "arg1"                 # a positional argument
    end

    s.exc_handler = ArgParse.debug_handler

    return s
end

function ap_settings1b()

    s = ArgParseSettings(exc_handler = ArgParse.debug_handler)

    add_arg_table!(s,
        "--opt1",
        ["--opt2", "-o"],
        "arg1")

    return s
end


for s = [ap_settings1(), ap_settings1b()]
    ap_test1(args) = parse_args(args, s)

    @test stringhelp(s) == """
        usage: $(basename(Base.source_path())) [--opt1 OPT1] [-o OPT2] [arg1]

        positional arguments:
          arg1

        optional arguments:
          --opt1 OPT1
          -o, --opt2 OPT2

        """

    @test ap_test1([]) == Dict{String,Any}("opt1"=>nothing, "opt2"=>nothing, "arg1"=>nothing)
    @test ap_test1(["arg"]) == Dict{String,Any}("opt1"=>nothing, "opt2"=>nothing, "arg1"=>"arg")
    @test ap_test1(["--opt1", "X", "-o=5", "--", "-arg"]) == Dict{String,Any}("opt1"=>"X", "opt2"=>"5", "arg1"=>"-arg")
    @test ap_test1(["--opt1", ""]) == Dict{String,Any}("opt1"=>"", "opt2"=>nothing, "arg1"=>nothing)
    @ap_test_throws ap_test1(["--opt1", "X", "-o=5", "-arg"])
    @test ap_test1(["--opt1=", "--opt2=5"]) == Dict{String,Any}("opt1"=>"", "opt2"=>"5", "arg1"=>nothing)
    @test ap_test1(["-o", "-2"]) == Dict{String,Any}("opt1"=>nothing, "opt2"=>"-2", "arg1"=>nothing)
    @ap_test_throws ap_test1(["--opt", "3"]) # ambiguous
    @ap_test_throws ap_test1(["-o"])
    @ap_test_throws ap_test1(["--opt1"])

    @aps_test_throws @add_arg_table!(s, "--opt1") # long option already added
    @aps_test_throws @add_arg_table!(s, "-o") # short option already added
end

# test malformed tables
function ap_settings1c()

    @aps_test_throws @add_arg_table! begin
        "-a"
    end

    s = ArgParseSettings(exc_handler = ArgParse.debug_handler)

    @aps_test_throws add_arg_table!(s, Dict(:action => :store_true), "-a")
    @test_addtable_failure s begin
        action = :store_true
        "-a"
    end
    @test_addtable_failure s begin
        action => :store_true
    end
    @aps_test_throws add_arg_table!(s, "-a", Dict(:wat => :store_true))
    @aps_test_throws @add_arg_table! s begin
        "-a"
            wat => :store_true
    end
end

ap_settings1c()

end


================================================
FILE: test/argparse_test02.jl
================================================
# test 02: version information, default values, flags,
#          options with types, optional arguments, variable
#          number of arguments;
#          function version of add_arg_table!

@testset "test 02" begin

function ap_settings2()

    s = ArgParseSettings(description = "Test 2 for ArgParse.jl",
                         epilog = "Have fun!",
                         version = "Version 1.0",
                         add_version = true,
                         exc_handler = ArgParse.debug_handler)

    @add_arg_table! s begin
        "--opt1"
            nargs = '?'              # '?' means optional argument
            arg_type = Int           # only Int arguments allowed
            default = 0              # this is used when the option is not passed
            constant = 1             # this is used if --opt1 is paseed with no argument
            help = "an option"
        "-O"
            arg_type = Symbol
            default = :xyz
            help = "another option"
        "--flag", "-f"
            action = :store_true   # this makes it a flag
            help = "a flag"
        "--karma", "-k"
            action = :count_invocations  # increase a counter each time the option is given
            help = "increase karma"
        "arg1"
            nargs = 2                        # eats up two arguments; puts the result in a Vector
            help = "first argument, two " *
                   "entries at once"
            required = true
        "arg2"
            nargs = '*'                            # eats up as many arguments as possible before an option
            default = Any["no_arg_given"]          # since the result will be a Vector{Any}, the default must
                                                   # also be (or it can be [] or nothing)
            help = "second argument, eats up " *
                   "as many items as possible " *
                   "before an option"
    end

    return s
end

function ap_settings2b()

    s = ArgParseSettings(description = "Test 2 for ArgParse.jl",
                         epilog = "Have fun!",
                         version = "Version 1.0",
                         add_version = true,
                         exc_handler = ArgParse.debug_handler)

    add_arg_table!(s,
        "--opt1", Dict(
            :nargs => '?',             # '?' means optional argument
            :arg_type => Int,          # only Int arguments allowed
            :default => 0,             # this is used when the option is not passed
            :constant => 1,            # this is used if --opt1 is paseed with no argument
            :help => "an option"),
        ["-O"], Dict(
            :arg_type => Symbol,
            :default => :xyz,
            :help => "another option"),
        ["--flag", "-f"], Dict(
            :action => :store_true,  # this makes it a flag
            :help => "a flag"),
        ["--karma", "-k"], Dict(
            :action => :count_invocations, # increase a counter each time the option is given
            :help => "increase karma"),
        "arg1", Dict(
            :nargs => 2,                       # eats up two arguments; puts the result in a Vector
            :help => "first argument, two " *
                     "entries at once",
            :required => true),
        "arg2", Dict(
            :nargs => '*',                           # eats up as many arguments as possible before an option
            :default => Any["no_arg_given"],         # since the result will be a Vector{Any}, the default must
                                                   # also be (or it can be [] or nothing)
            :help => "second argument, eats up " *
                     "as many items as possible " *
                     "before an option")
    )

    return s
end

function ap_settings2c()

    s = ArgParseSettings(description = "Test 2 for ArgParse.jl",
                         epilog = "Have fun!",
                         version = "Version 1.0",
                         add_version = true,
                         exc_handler = ArgParse.debug_handler)

    @add_arg_table!(s
        , "--opt1"
        ,     nargs = '?'              # '?' means optional argument
        ,     arg_type = Int           # only Int arguments allowed
        ,     default = 0              # this is used when the option is not passed
        ,     constant = 1             # this is used if --opt1 is paseed with no argument
        ,     help = "an option"
        , "-O"
        ,     arg_type = Symbol
        ,     default = :xyz
        ,     help = "another option"
        , ["--flag", "-f"]
        ,     action = :store_true   # this makes it a flag
        ,     help = "a flag"
        , ["--karma", "-k"]
        ,     action = :count_invocations  # increase a counter each time the option is given
        ,     help = "increase karma"
        , "arg1"
        ,     nargs = 2                        # eats up two arguments; puts the result in a Vector
        ,     help = "first argument, two " *
                     "entries at once"
        ,     required = true
        , "arg2"
        ,     nargs = '*'                            # eats up as many arguments as possible before an option
        ,     default = Any["no_arg_given"]          # since the result will be a Vector{Any}, the default must
                                                     # also be (or it can be [] or nothing)
        ,     help = "second argument, eats up " *
                     "as many items as possible " *
                     "before an option"
        )

    return s
end

function ap_settings2d()

    s = ArgParseSettings(description = "Test 2 for ArgParse.jl",
                         epilog = "Have fun!",
                         version = "Version 1.0",
                         add_version = true,
                         exc_handler = ArgParse.debug_handler)

    @add_arg_table! s begin
        ("--opt1";
              nargs = '?';              # '?' means optional argument
              arg_type = Int;           # only Int arguments allowed
              default = 0;              # this is used when the option is not passed
              constant = 1;             # this is used if --opt1 is paseed with no argument
              help = "an option"),
        ("-O";
              arg_type = Symbol;
              default = :xyz;
              help = "another option"),
        (["--flag", "-f"];
              action = :store_true;     # this makes it a flag
              help = "a flag")
        (["--karma", "-k"];
              action = :count_invocations; # increase a counter each time the option is given
              help = "increase karma")
        ("arg1";
              nargs = 2;                       # eats up two arguments; puts the result in a Vector
              help = "first argument, two " *
                     "entries at once";
              required = true)
        ("arg2";
              nargs = '*';                            # eats up as many arguments as possible before an option
              default = Any["no_arg_given"];          # since the result will be a Vector{Any}, the default must
                                                      # also be (or it can be [] or nothing)
              help = "second argument, eats up " *
                     "as many items as possible " *
                     "before an option")
    end

    return s
end

function ap_settings2e()

    s = ArgParseSettings(description = "Test 2 for ArgParse.jl",
                         epilog = "Have fun!",
                         version = "Version 1.0",
                         add_version = true,
                         exc_handler = ArgParse.debug_handler)

    @add_arg_table!(s,
        "--opt1",
        begin
            nargs = '?'              # '?' means optional argument
            arg_type = Int           # only Int arguments allowed
            default = 0              # this is used when the option is not passed
            constant = 1             # this is used if --opt1 is paseed with no argument
            help = "an option"
        end,
        "-O",
        begin
            arg_type = Symbol
            default = :xyz
            help = "another option"
        end,
        ["--flag", "-f"],
        begin
            action = :store_true   # this makes it a flag
            help = "a flag"
        end,
        ["--karma", "-k"],
        begin
            action = :count_invocations  # increase a counter each time the option is given
            help = "increase karma"
        end,
        "arg1",
        begin
            nargs = 2                        # eats up two arguments; puts the result in a Vector
            help = "first argument, two " *
                   "entries at once"
            required = true
        end,
        "arg2",
        begin
            nargs = '*'                            # eats up as many arguments as possible before an option
            default = Any["no_arg_given"]          # since the result will be a Vector{Any}, the default must
                                                   # also be (or it can be [] or nothing)
            help = "second argument, eats up " *
                   "as many items as possible " *
                   "before an option"
        end)

    return s
end

for s = [ap_settings2(), ap_settings2b(), ap_settings2c(), ap_settings2d(), ap_settings2e()]
    ap_test2(args) = parse_args(args, s)

    @test stringhelp(s) == """
        usage: $(basename(Base.source_path())) [--opt1 [OPT1]] [-O O] [-f] [-k] arg1 arg1
                                [arg2...]

        Test 2 for ArgParse.jl

        positional arguments:
          arg1           first argument, two entries at once
          arg2           second argument, eats up as many items as possible
                         before an option (default: Any["no_arg_given"])

        optional arguments:
          --opt1 [OPT1]  an option (type: $Int, default: 0, without arg: 1)
          -O O           another option (type: $Symbol, default: :xyz)
          -f, --flag     a flag
          -k, --karma    increase karma

        Have fun!

        """

    @test stringversion(s) == "Version 1.0\n"

    @ap_test_throws ap_test2([])
    @test ap_test2(["X", "Y"]) == Dict{String,Any}("opt1"=>0, "O"=>:xyz, "flag"=>false, "karma"=>0, "arg1"=>Any["X", "Y"], "arg2"=>Any["no_arg_given"])
    @test ap_test2(["X", "Y", "-k", "-f", "Z", "--karma", "--opt"]) == Dict{String,Any}("opt1"=>1, "O"=>:xyz, "flag"=>true, "karma"=>2, "arg1"=>Any["X", "Y"], "arg2"=>Any["Z"])
    @test ap_test2(["X", "Y", "--opt", "-k", "-f", "Z", "--karma"]) == Dict{String,Any}("opt1"=>1, "O"=>:xyz, "flag"=>true, "karma"=>2, "arg1"=>Any["X", "Y"], "arg2"=>Any["Z"])
    @test ap_test2(["X", "Y", "--opt", "--karma", "-O", "XYZ", "-f", "Z", "-k"]) == Dict{String,Any}("opt1"=>1, "O"=>:XYZ, "flag"=>true, "karma"=>2, "arg1"=>Any["X", "Y"], "arg2"=>Any["Z"])
    @test ap_test2(["--opt", "-3", "X", "Y", "-k", "-f", "Z", "-O", "a b c", "--karma"]) == Dict{String,Any}("opt1"=>-3, "O"=>Symbol("a b c"), "flag"=>true, "karma"=>2, "arg1"=>Any["X", "Y"], "arg2"=>Any["Z"])
    @ap_test_throws ap_test2(["--opt"])
    @ap_test_throws ap_test2(["--opt="])
    @ap_test_throws ap_test2(["--opt", "", "X", "Y"])
    @ap_test_throws ap_test2(["--opt", "1e-2", "X", "Y"])

    @aps_test_throws @add_arg_table!(s, "required_arg_after_optional_args", required = true)
    # wrong default
    @aps_test_throws @add_arg_table!(s, "--opt", arg_type = Int, default = 1.5)
    @aps_test_throws @add_arg_table!(s, "--opt3", arg_type = Function, default = "string")
    # wrong range tester
    @aps_test_throws @add_arg_table!(s, "--opt", arg_type = Int, range_tester = x->string(x), default = 1)
    @aps_test_throws @add_arg_table!(s, "--opt", arg_type = Int, range_tester = x->sqrt(x)<1, default = -1)
end

end


================================================
FILE: test/argparse_test03.jl
================================================
# test 03: dest_name, metavar, range_tester, alternative
#          actions, custom parser

struct CustomType
end

@testset "test 03" begin

function ArgParse.parse_item(::Type{CustomType}, x::AbstractString)
    @assert x == "custom"
    return CustomType()
end

Base.show(io::IO, ::Type{CustomType}) = print(io, "CustomType")
Base.show(io::IO, c::CustomType) = print(io, "CustomType()")

function ap_settings3()

    s = ArgParseSettings("Test 3 for ArgParse.jl",
                         exc_handler = ArgParse.debug_handler)

    @add_arg_table! s begin
        "--opt1"
            action = :append_const   # appends 'constant' to 'dest_name'
            arg_type = String
            constant = "O1"
            dest_name = "O_stack"    # this changes the destination
            help = "append O1"
        "--opt2"
            action = :append_const
            arg_type = String
            constant = "O2"
            dest_name = "O_stack"    # same dest_name as opt1, different constant
            help = "append O2"
        "-k"
            action = :store_const    # stores constant if given, default otherwise
            default = 0
            constant = 42
            help = "provide the answer"
        "-u"
            action = :store_const    # stores constant if given, default otherwise
            default = 0
            constant = 42.0
            help = "provide the answer as floating point"
        "--array"
            default = [7, 3, 2]
            arg_type = Vector{Int}
            eval_arg = true          # enables evaluation of the argument. NOTE: security risk!
            help = "create an array"
        "--custom"
            default = CustomType()
            arg_type = CustomType    # uses the user-defined version of ArgParse.parse_item
            help = "the only accepted argument is \"custom\""
        "--oddint"
            default = 1
            arg_type = Int
            range_tester = x->(isodd(x) || error("not odd")) # range error ≡ false
            help = "an odd integer"
        "--collect"
            action = :append_arg
            arg_type = Int
            metavar = "C"
            help = "collect things"
        "--awkward-option"
            nargs = '+'                         # eats up as many argument as found (at least 1)
            action = :append_arg                # argument chunks are appended when the option is
                                                # called repeatedly
            dest_name = "awk"
            range_tester = (x->x=="X"||x=="Y")  # each argument must be either "X" or "Y"
            default = [["X"]]
            metavar = "XY"
            help = "either X or Y; all XY's are " *
                   "stored in chunks"
    end

    return s
end

let s = ap_settings3()
    ap_test3(args) = parse_args(args, s)

    ## ugly workaround for the change of printing Vectors in julia 1.6,
    ## from Array{Int,1} to Vector{Int}
    array_help_lines = if string(Vector{Any}) == "Vector{Any}"
        """
          --array ARRAY         create an array (type: Vector{$Int}, default:
                                $([7, 3, 2]))
        """
    else
        """
          --array ARRAY         create an array (type: Array{$Int,1},
                                default: $([7, 3, 2]))
        """
    end
    array_help_lines = array_help_lines[1:end-1] # remove an extra newline

    @test stringhelp(s) == """
        usage: $(basename(Base.source_path())) [--opt1] [--opt2] [-k] [-u] [--array ARRAY]
                                [--custom CUSTOM] [--oddint ODDINT]
                                [--collect C] [--awkward-option XY [XY...]]

        Test 3 for ArgParse.jl

        optional arguments:
          --opt1                append O1
          --opt2                append O2
          -k                    provide the answer
          -u                    provide the answer as floating point
        $array_help_lines
          --custom CUSTOM       the only accepted argument is "custom" (type:
                                CustomType, default: CustomType())
          --oddint ODDINT       an odd integer (type: $Int, default: 1)
          --collect C           collect things (type: $Int)
          --awkward-option XY [XY...]
                                either X or Y; all XY's are stored in chunks
                                (default: $(Vector{Any})[["X"]])

        """

    @test ap_test3([]) == Dict{String,Any}("O_stack"=>String[], "k"=>0, "u"=>0, "array"=>[7, 3, 2], "custom"=>CustomType(), "oddint"=>1, "collect"=>[], "awk"=>Any[Any["X"]])
    @test ap_test3(["--opt1", "--awk", "X", "X", "--opt2", "--opt2", "-k", "--coll", "5", "-u", "--array=[4]", "--custom", "custom", "--collect", "3", "--awkward-option=Y", "X", "--opt1", "--oddint", "-1"]) ==
    Dict{String,Any}("O_stack"=>String["O1", "O2", "O2", "O1"], "k"=>42, "u"=>42.0, "array"=>[4], "custom"=>CustomType(), "oddint"=>-1, "collect"=>[5, 3], "awk"=>Any[Any["X"], Any["X", "X"], Any["Y", "X"]])
    @ap_test_throws ap_test3(["X"])
    @ap_test_throws ap_test3(["--awk", "Z"])
    @ap_test_throws ap_test3(["--awk", "-2"])
    @ap_test_throws ap_test3(["--array", "7"])
    @ap_test_throws ap_test3(["--custom", "default"])
    @ap_test_throws ap_test3(["--oddint", "0"])
    @ap_test_throws ap_test3(["--collect", "0.5"])

    # invalid option name
    @aps_test_throws @add_arg_table!(s, "-2", action = :store_true)
    # wrong constants
    @aps_test_throws @add_arg_table!(s, "--opt", action = :store_const, arg_type = Int, default = 1, constant = 1.5)
    @aps_test_throws @add_arg_table!(s, "--opt", action = :append_const, arg_type = Int, constant = 1.5)
    # wrong defaults
    @aps_test_throws @add_arg_table!(s, "--opt", action = :append_arg, arg_type = Int, default = String["hello"])
    @aps_test_throws @add_arg_table!(s, "--opt", action = :append_arg, nargs = '+', arg_type = Int, default = Vector{Float64}[[1.5]])
    @aps_test_throws @add_arg_table!(s, "--opt", action = :store_arg, nargs = '+', arg_type = Int, default = [1.5])
    @aps_test_throws @add_arg_table!(s, "--opt", action = :store_arg, nargs = '+', arg_type = Int, default = 1)
    @aps_test_throws @add_arg_table!(s, "--opt", action = :append_arg, arg_type = Int, range_tester=x->x<=1, default = Int[0, 1, 2])
    @aps_test_throws @add_arg_table!(s, "--opt", action = :append_arg, nargs = '+', arg_type = Int, range_tester=x->x<=1, default = Vector{Int}[[1,1],[0,2]])
    @aps_test_throws @add_arg_table!(s, "--opt", action = :store_arg, nargs = '+', range_tester = x->x<=1, default = [1.5])
    # no constants
    @aps_test_throws @add_arg_table!(s, "--opt", action = :store_const, arg_type = Int, default = 1)
    @aps_test_throws @add_arg_table!(s, "--opt", action = :append_const, arg_type = Int)
    # incompatible action
    @aps_test_throws @add_arg_table!(s, "--opt3", action = :store_const, arg_type = String, constant = "O3", dest_name = "O_stack", help = "append O3")
    # wrong range tester
    @aps_test_throws @add_arg_table!(s, "--opt", action = :append_arg, arg_type = Int, range_tester=x->string(x), default = Int[0, 1, 2])
    @aps_test_throws @add_arg_table!(s, "--opt", action = :append_arg, nargs = '+', arg_type = Int, range_tester=x->string(x), default = Vector{Int}[[1,1],[0,2]])
    @aps_test_throws @add_arg_table!(s, "--opt", action = :store_arg, nargs = '+', range_tester = x->string(x), default = [1.5])
    @aps_test_throws @add_arg_table!(s, "--opt", action = :store_arg, nargs = '+', range_tester = x->sqrt(x)<2, default = [-1.5])
    @aps_test_throws @add_arg_table!(s, "--opt", action = :append_arg, nargs = '+', arg_type = Int, range_tester=x->sqrt(x)<2, default = Vector{Int}[[1,1],[0,-2]])

    # allow ambiguous options
    s.allow_ambiguous_opts = true
    @add_arg_table!(s, "-2", action = :store_true)
    @test ap_test3([]) == Dict{String,Any}("O_stack"=>String[], "k"=>0, "u"=>0, "array"=>[7, 3, 2], "custom"=>CustomType(), "oddint"=>1, "collect"=>[], "awk"=>Vector{Any}[["X"]], "2"=>false)
    @test ap_test3(["-2"]) == Dict{String,Any}("O_stack"=>String[], "k"=>0, "u"=>0, "array"=>[7, 3, 2], "custom"=>CustomType(), "oddint"=>1, "collect"=>[], "awk"=>Vector{Any}[["X"]], "2"=>true)
    @test ap_test3(["--awk", "X", "-2"]) == Dict{String,Any}("O_stack"=>String[], "k"=>0, "u"=>0, "array"=>[7, 3, 2], "custom"=>CustomType(), "oddint"=>1, "collect"=>[], "awk"=>Vector{Any}[["X"], ["X"]], "2"=>true)
    @ap_test_throws ap_test3(["--awk", "X", "-3"])

end

end


================================================
FILE: test/argparse_test04.jl
================================================
# test 04: manual help/version, import another parser

@testset "test 04" begin

function ap_settings4()

    s0 = ArgParseSettings()  # a "parent" structure e.g. one with some useful set of rules
                             # which we want to extend

    # So we just add a simple table
    @add_arg_table! s0 begin
        "--parent-flag", "-o"
            action = "store_true"
            help = "parent flag"
        "--flag"
            action = "store_true"
            help = "another parent flag"
        "parent-argument"
            help = "parent argument"
        "will-be-overridden"
            metavar = "ARG"
    end

    s = ArgParseSettings("Test 4 for ArgParse.jl",
                         add_help = false,           # disable auto-add of --help option
                         version = "Version 1.0",    # we set the version info, but --version won't be added
                         error_on_conflict = false,  # do not error-out when trying to override an option
                         exc_handler = ArgParse.debug_handler)

    import_settings!(s, s0)        # now s has all of s0 arguments (except help/version)

    @add_arg_table! s begin
        "-o"                       # this will partially override s0's --parent-flag
            action = :store_true
            help = "child flag"
        "--flag"                   # this will fully override s0's --flag
            action = :store_true
            help = "another child flag"
        "-?", "--HELP", "--¡ḧëļṕ"                # (almost) all characters allowed
            action = :show_help                  # will invoke the help generator
            help = "this will help you"
        "-v", "--VERSION"
            action = :show_version               # will show version information
            help = "show version information " *
                   "and exit"
        "child-argument"            # same metavar as "will-be-overridden"
            metavar = "ARG"
            help = "child argument"
    end

    return s
end

function ap_settings4b()

    # same as ap_settings4(), but imports all settings

    s0 = ArgParseSettings(add_help = false,
                          error_on_conflict = false,
                          exc_handler = ArgParse.debug_handler)

    # So we just add a simple table
    @add_arg_table! s0 begin
        "--parent-flag", "-o"
            action = "store_true"
            help = "parent flag"
        "--flag"
            action = "store_true"
            help = "another parent flag"
        "parent-argument"
            help = "parent argument"
        "will-be-overridden"
            metavar = "ARG"
    end

    s = ArgParseSettings("Test 4 for ArgParse.jl",
                         version = "Version 1.0")

    import_settings!(s, s0, args_only=false)

    @add_arg_table! s begin
        "-o"
            action = :store_true
            help = "child flag"
        "--flag"
            action = :store_true
            help = "another child flag"
        "-?", "--HELP", "--¡ḧëļṕ"
            action = :show_help
            help = "this will help you"
        "-v", "--VERSION"
            action = :show_version
            help = "show version information " *
                   "and exit"
        "child-argument"
            metavar = "ARG"
            help = "child argument"
    end

    return s
end


for s = [ap_settings4(), ap_settings4b()]
    ap_test4(args) = parse_args(args, s)

    @test stringhelp(s) == """
        usage: $(basename(Base.source_path())) [--parent-flag] [-o] [--flag] [-?] [-v]
                                [parent-argument] [ARG]

        Test 4 for ArgParse.jl

        positional arguments:
          parent-argument      parent argument
          ARG                  child argument

        optional arguments:
          --parent-flag        parent flag
          -o                   child flag
          --flag               another child flag
          -?, --HELP, --¡ḧëļṕ  this will help you
          -v, --VERSION        show version information and exit

        """

    @test stringversion(s) == "Version 1.0\n"

    @test ap_test4([]) == Dict{String,Any}("parent-flag"=>false, "o"=>false, "flag"=>false, "parent-argument"=>nothing, "child-argument"=>nothing)
    @test ap_test4(["-o", "X"]) == Dict{String,Any}("parent-flag"=>false, "o"=>true, "flag"=>false, "parent-argument"=>"X", "child-argument"=>nothing)
    @ap_test_throws ap_test4(["-h"])

    # same metavar as another argument
    s.error_on_conflict = true
    @aps_test_throws @add_arg_table!(s, "other-arg", metavar="parent-argument")
end


function ap_settings4_base()

    s = ArgParseSettings("Test 4 for ArgParse.jl",
                         add_help = false,
                         version = "Version 1.0",
                         exc_handler = ArgParse.debug_handler)

    @add_arg_table! s begin
        "-o"
            action = :store_true
            help = "child flag"
        "--flag"
            action = :store_true
            help = "another child flag"
    end

    return s
end

function ap_settings4_conflict1()

    s0 = ArgParseSettings()

    @add_arg_table! s0 begin
        "--parent-flag", "-o"
            action = "store_true"
            help = "parent flag"
    end

    return s0
end

function ap_settings4_conflict2()

    s0 = ArgParseSettings()

    @add_arg_table! s0 begin
        "--flag"
            action = "store_true"
            help = "another parent flag"
    end

    return s0
end

let s = ap_settings4_base()

    for s0 = [ap_settings4_conflict1(), ap_settings4_conflict2()]
        @aps_test_throws import_settings!(s, s0)
    end
end

end


================================================
FILE: test/argparse_test05.jl
================================================
# test 05: commands & subtables

@testset "test 05" begin

function ap_settings5()

    s = ArgParseSettings("Test 5 for ArgParse.jl",
                         exc_handler = ArgParse.debug_handler,
                         exit_after_help = false)

    @add_arg_table! s begin
        "run"
            action = :command
            help = "start running mode"
        "jump", "ju", "J"
            action = :command
            help = "start jumping mode"
    end

    @add_arg_table! s["run"] begin
        "--speed"
            arg_type = Float64
            default = 10.0
            help = "running speed, in Å/month"
    end

    s["jump"].description = "Jump mode"
    s["jump"].commands_are_required = false
    s["jump"].autofix_names = true

    @add_arg_table! s["jump"] begin
        "--higher"
            action = :store_true
            help = "enhance jumping"
        "--somersault", "-s"
            action = :command
            dest_name = "som"
            help = "somersault jumping mode"
        "--clap-feet", "-c"
            action = :command
            help = "clap feet jumping mode"
    end

    s["jump"]["som"].description = "Somersault jump mode"

    @add_arg_table! s["jump"]["som"] begin
        "-t"
            nargs = '?'
            arg_type = Int
            default = 1
            constant = 1
            help = "twist a number of times"
        "-b"
            action = :store_true
            help = "blink"
    end

    return s
end

let s = ap_settings5()
    ap_test5(args; kw...) = parse_args(args, s; kw...)

    @test stringhelp(s) == """
        usage: $(basename(Base.source_path())) {run|jump}

        Test 5 for ArgParse.jl

        commands:
          run   start running mode
          jump  start jumping mode (aliases: ju, J)

        """

    @test stringhelp(s["run"]) == """
        usage: $(basename(Base.source_path())) run [--speed SPEED]

        optional arguments:
          --speed SPEED  running speed, in Å/month (type: Float64, default:
                         10.0)

        """

    @test stringhelp(s["jump"]) == """
        usage: $(basename(Base.source_path())) jump [--higher] [-s|-c]

        Jump mode

        commands:
          -s, --somersault  somersault jumping mode
          -c, --clap-feet   clap feet jumping mode

        optional arguments:
          --higher          enhance jumping

        """

    @test stringhelp(s["jump"]["som"]) == """
        usage: $(basename(Base.source_path())) jump -s [-t [T]] [-b]

        Somersault jump mode

        optional arguments:
          -t [T]  twist a number of times (type: $Int, default: 1, without
                  arg: 1)
          -b      blink

        """

    @ap_test_throws ap_test5([])
    @noout_test ap_test5(["--help"]) ≡ nothing
    @test ap_test5(["run", "--speed", "3"]) == Dict{String,Any}("%COMMAND%"=>"run", "run"=>Dict{String,Any}("speed"=>3.0))
    @noout_test ap_test5(["jump", "--help"]) ≡ nothing
    @test ap_test5(["jump"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>false, "%COMMAND%"=>nothing))
    @test ap_test5(["jump", "--higher", "--clap"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>true, "%COMMAND%"=>"clap_feet", "clap_feet"=>Dict{String,Any}()))
    @test ap_test5(["ju", "--higher", "--clap"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>true, "%COMMAND%"=>"clap_feet", "clap_feet"=>Dict{String,Any}()))
    @test ap_test5(["J", "--higher", "--clap"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>true, "%COMMAND%"=>"clap_feet", "clap_feet"=>Dict{String,Any}()))
    @noout_test ap_test5(["jump", "--higher", "--clap", "--help"]) ≡ nothing
    @noout_test ap_test5(["jump", "--higher", "--clap", "--help"], as_symbols = true) ≡ nothing
    @ap_test_throws ap_test5(["jump", "--clap", "--higher"])
    @test ap_test5(["jump", "--somersault"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>false, "%COMMAND%"=>"som", "som"=>Dict{String,Any}("t"=>1, "b"=>false)))
    @test ap_test5(["jump", "-s", "-t"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>false, "%COMMAND%"=>"som", "som"=>Dict{String,Any}("t"=>1, "b"=>false)))
    @test ap_test5(["jump", "-st"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>false, "%COMMAND%"=>"som", "som"=>Dict{String,Any}("t"=>1, "b"=>false)))
    @test ap_test5(["jump", "-sbt"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>false, "%COMMAND%"=>"som", "som"=>Dict{String,Any}("t"=>1, "b"=>true)))
    @test ap_test5(["jump", "-s", "-t2"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>false, "%COMMAND%"=>"som", "som"=>Dict{String,Any}("t"=>2, "b"=>false)))
    @test ap_test5(["jump", "-sbt2"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>false, "%COMMAND%"=>"som", "som"=>Dict{String,Any}("t"=>2, "b"=>true)))
    @test ap_test5(["ju", "-sbt2"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>false, "%COMMAND%"=>"som", "som"=>Dict{String,Any}("t"=>2, "b"=>true)))
    @test ap_test5(["J", "-sbt2"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>false, "%COMMAND%"=>"som", "som"=>Dict{String,Any}("t"=>2, "b"=>true)))
    @noout_test ap_test5(["jump", "-sbht2"]) ≡ nothing
    @ap_test_throws ap_test5(["jump", "-st2b"])
    @ap_test_throws ap_test5(["jump", "-stb"])
    @ap_test_throws ap_test5(["jump", "-sb-"])
    @ap_test_throws ap_test5(["jump", "-s-b"])
    @ap_test_throws ap_test5(["ju", "-s-b"])
    @test ap_test5(["jump", "--higher"], as_symbols = true) == Dict{Symbol,Any}(:_COMMAND_=>:jump, :jump=>Dict{Symbol,Any}(:higher=>true, :_COMMAND_=>nothing))
    @test ap_test5(["run", "--speed", "3"], as_symbols = true) == Dict{Symbol,Any}(:_COMMAND_=>:run, :run=>Dict{Symbol,Any}(:speed=>3.0))

    # argument after command
    @aps_test_throws @add_arg_table!(s, "arg_after_command")
    # same name as command
    @aps_test_throws @add_arg_table!(s, "run")
    @aps_test_throws @add_arg_table!(s["jump"], "-c")
    @aps_test_throws @add_arg_table!(s["jump"], "--somersault")
    # same dest_name as command
    @aps_test_throws @add_arg_table!(s["jump"], "--som")
    @aps_test_throws @add_arg_table!(s["jump"], "-s", dest_name = "som")
    # same name as command alias
    @aps_test_throws @add_arg_table!(s, "ju")
    @aps_test_throws @add_arg_table!(s, "J")
    # new command with the same name as another one
    @aps_test_throws @add_arg_table!(s, ["run", "R"], action = :command)
    @aps_test_throws @add_arg_table!(s, "jump", action = :command)
    # new command with the same name as another one's alias
    @aps_test_throws @add_arg_table!(s, "ju", action = :command)
    @aps_test_throws @add_arg_table!(s, "J", action = :command)
    # new command with an alias which is the same as another command
    @aps_test_throws @add_arg_table!(s, ["fast", "run"], action = :command)
    @aps_test_throws @add_arg_table!(s, ["R", "jump"], action = :command)
    # new command with an alias which is already in use
    @aps_test_throws @add_arg_table!(s, ["R", "ju"], action = :command)
    @aps_test_throws @add_arg_table!(s, ["R", "S", "J"], action = :command)

    # alias overriding by a command name
    @add_arg_table!(s, "J", action = :command, force_override = true, help = "the J command")
    @test stringhelp(s) == """
        usage: $(basename(Base.source_path())) {run|jump|J}

        Test 5 for ArgParse.jl

        commands:
          run   start running mode
          jump  start jumping mode (aliases: ju)
          J     the J command

        """

    # alias overriding by a command alias
    @add_arg_table!(s, ["S", "ju"], action = :command, force_override = true, help = "the S command")
    @test stringhelp(s) == """
        usage: $(basename(Base.source_path())) {run|jump|J|S}

        Test 5 for ArgParse.jl

        commands:
          run   start running mode
          jump  start jumping mode
          J     the J command
          S     the S command (aliases: ju)

        """

    # cannot override a command name
    @aps_test_throws @add_arg_table!(s, ["J", "R"], action = :command, force_override = true)
    @aps_test_throws @add_arg_table!(s, ["R", "J"], action = :command, force_override = true)

    # conflict between dest_name and a reserved Symbol
    @add_arg_table!(s, "--COMMAND", dest_name="_COMMAND_")
    @aps_test_throws ap_test5(["run", "--speed", "3"], as_symbols = true)
end

function ap_settings5b()

    s0 = ArgParseSettings()

    s = ArgParseSettings(error_on_conflict = false,
                         exc_handler = ArgParse.debug_handler,
                         exit_after_help = false)

    @add_arg_table! s0 begin
        "run", "R"
            action = :command
            help = "start running mode"
        "jump", "ju"
            action = :command
            help = "start jumping mode"
        "--time"
            arg_type = String
            default = "now"
            metavar = "T"
            help = "time at which to " *
                   "perform the command"
    end

    @add_arg_table! s0["run"] begin
        "--speed"
            arg_type = Float64
            default = 10.
            help = "running speed, in Å/month"
    end

    s0["jump"].description = "Jump mode"
    s0["jump"].commands_are_required = false
    s0["jump"].autofix_names = true
    s0["jump"].add_help = false

    add_arg_group!(s0["jump"], "modifiers", "modifiers")
    set_default_arg_group!(s0["jump"])

    @add_arg_table! s0["jump"] begin
        "--higher"
            action = :store_true
            help = "enhance jumping"
            group = "modifiers"
        "--somersault"
            action = :command
            dest_name = "som"
            help = "somersault jumping mode"
        "--clap-feet", "-c"
            action = :command
            help = "clap feet jumping mode"
    end

    add_arg_group!(s0["jump"], "other")
    @add
Download .txt
gitextract_2653z8hp/

├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       ├── TagBot.yml
│       ├── ci.yml
│       └── documentation.yml
├── LICENSE.md
├── Project.toml
├── README.md
├── docs/
│   ├── .gitignore
│   ├── Project.toml
│   ├── make.jl
│   └── src/
│       ├── arg_table.md
│       ├── conflicts.md
│       ├── custom.md
│       ├── details.md
│       ├── import.md
│       ├── index.md
│       ├── parse_args.md
│       └── settings.md
├── examples/
│   ├── argparse_example1.jl
│   ├── argparse_example2.jl
│   ├── argparse_example3.jl
│   ├── argparse_example4.jl
│   ├── argparse_example5.jl
│   ├── argparse_example6.jl
│   ├── argparse_example7.jl
│   └── argparse_example8.jl
├── src/
│   ├── ArgParse.jl
│   ├── common.jl
│   ├── deprecated.jl
│   ├── parsing.jl
│   └── settings.jl
└── test/
    ├── Project.toml
    ├── argparse_test01.jl
    ├── argparse_test02.jl
    ├── argparse_test03.jl
    ├── argparse_test04.jl
    ├── argparse_test05.jl
    ├── argparse_test06.jl
    ├── argparse_test07.jl
    ├── argparse_test08.jl
    ├── argparse_test09.jl
    ├── argparse_test10.jl
    ├── argparse_test11.jl
    ├── argparse_test12.jl
    ├── argparse_test13.jl
    ├── argparse_test14.jl
    ├── args-file1
    ├── args-file2
    ├── common.jl
    └── runtests.jl
Condensed preview — 50 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (249K chars).
[
  {
    "path": ".github/dependabot.yml",
    "chars": 256,
    "preview": "# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\nversion: 2\nupda"
  },
  {
    "path": ".github/workflows/TagBot.yml",
    "chars": 362,
    "preview": "name: TagBot\non:\n  issue_comment:\n    types:\n      - created\n  workflow_dispatch:\njobs:\n  TagBot:\n    if: github.event_n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 1344,
    "preview": "name: CI\non:\n  push:\n    branches:\n      - master\n    tags: '*'\n  pull_request:\n  schedule:\n    # Run CI against `master"
  },
  {
    "path": ".github/workflows/documentation.yml",
    "chars": 902,
    "preview": "name: Documentation\n\non:\n  push:\n    branches:\n      - master\n    tags: '*'\n  pull_request:\n\njobs:\n  build:\n    permissi"
  },
  {
    "path": "LICENSE.md",
    "chars": 3895,
    "preview": "The ArgParse Julia module is licensed under the MIT License:\n\n> Copyright (c) 2012: Carlo Baldassi\n>\n> Permission is her"
  },
  {
    "path": "Project.toml",
    "chars": 403,
    "preview": "name = \"ArgParse\"\nuuid = \"c7e460c6-2fb9-53a9-8c5b-16f535851c63\"\nauthor = [\"Carlo Baldassi <carlobaldassi@gmail.com>\"]\nve"
  },
  {
    "path": "README.md",
    "chars": 5118,
    "preview": "# ArgParse.jl\n\n[![DOCS][docs-img]][docs-url] [![CI][CI-img]][CI-url] [![CODECOV][codecov-img]][codecov-url]\n\nArgParse.jl"
  },
  {
    "path": "docs/.gitignore",
    "chars": 21,
    "preview": "Manifest.toml\nbuild/\n"
  },
  {
    "path": "docs/Project.toml",
    "chars": 136,
    "preview": "[deps]\nArgParse = \"c7e460c6-2fb9-53a9-8c5b-16f535851c63\"\nDocumenter = \"e30172f5-a6a5-5a46-863b-614d45cd2de4\"\n\n[compat]\nD"
  },
  {
    "path": "docs/make.jl",
    "chars": 576,
    "preview": "using Documenter, ArgParse\n\nCIbuild = get(ENV, \"CI\", nothing) == \"true\"\n\nmakedocs(\n    modules  = [ArgParse],\n    format"
  },
  {
    "path": "docs/src/arg_table.md",
    "chars": 19322,
    "preview": "# Argument table\n\nThe argument table is used to store allowed arguments and options in an [`ArgParseSettings`](@ref) obj"
  },
  {
    "path": "docs/src/conflicts.md",
    "chars": 1873,
    "preview": "# Conflicts and overrides\n\nConflicts between arguments, be them options, positional arguments or commands, can arise for"
  },
  {
    "path": "docs/src/custom.md",
    "chars": 1018,
    "preview": "# Parsing to custom types\n\nIf you specify an `arg_type` setting (see the [Argument entry settings](@ref) section) for an"
  },
  {
    "path": "docs/src/details.md",
    "chars": 2227,
    "preview": "# Parsing details\n\nDuring parsing, `parse_args` must determine whether an argument is an option, an option argument, a p"
  },
  {
    "path": "docs/src/import.md",
    "chars": 212,
    "preview": "# Importing settings\n\nIt may be useful in some cases to import an argument table into the one which is to be used, for e"
  },
  {
    "path": "docs/src/index.md",
    "chars": 6158,
    "preview": "# ArgParse.jl documentation\n\n```@meta\nCurrentModule = ArgParse\n```\n\nThis [Julia](http://julialang.org) package allows th"
  },
  {
    "path": "docs/src/parse_args.md",
    "chars": 54,
    "preview": "# The `parse_args` function\n\n```@docs\nparse_args\n```\n\n"
  },
  {
    "path": "docs/src/settings.md",
    "chars": 59,
    "preview": "# Settings\n\n```@docs\nArgParseSettings\n@project_version\n```\n"
  },
  {
    "path": "examples/argparse_example1.jl",
    "chars": 682,
    "preview": "# example 1: minimal options/arguments, auto-generated help/version\n\nusing ArgParse\n\nfunction main(args)\n\n    # initiali"
  },
  {
    "path": "examples/argparse_example2.jl",
    "chars": 774,
    "preview": "# example 2: add some flags and the help lines for options\n\nusing ArgParse\n\nfunction main(args)\n\n    s = ArgParseSetting"
  },
  {
    "path": "examples/argparse_example3.jl",
    "chars": 1910,
    "preview": "# example 3: version information, default values, options with\n#            types and variable number of arguments\n\nusin"
  },
  {
    "path": "examples/argparse_example4.jl",
    "chars": 3241,
    "preview": "# example 4: dest_name, metavar, range_tester, alternative\n#            actions, epilog with examples\n\nusing ArgParse\n\nf"
  },
  {
    "path": "examples/argparse_example5.jl",
    "chars": 1994,
    "preview": "# example 5: manual help/version, import another parser\n\nusing ArgParse\n\nfunction main(args)\n\n    s0 = ArgParseSettings("
  },
  {
    "path": "examples/argparse_example6.jl",
    "chars": 2688,
    "preview": "# example 6: commands & subtables\n\nusing ArgParse\n\nfunction main(args)\n\n    s = ArgParseSettings(\"Example 6 for argparse"
  },
  {
    "path": "examples/argparse_example7.jl",
    "chars": 2223,
    "preview": "# example 7: argument groups\n\nusing ArgParse\n\nfunction main(args)\n\n    s = ArgParseSettings(\"Example 7 for argparse.jl: "
  },
  {
    "path": "examples/argparse_example8.jl",
    "chars": 1475,
    "preview": "# example 8: mutually exculsive and required groups\n\nusing ArgParse\n\nfunction main(args)\n\n    s = ArgParseSettings(\"Exam"
  },
  {
    "path": "src/ArgParse.jl",
    "chars": 1175,
    "preview": "\"\"\"\n    ArgParse\n\nThis module allows the creation of user-friendly command-line interfaces to Julia programs:\nthe progra"
  },
  {
    "path": "src/common.jl",
    "chars": 1206,
    "preview": "## Some common functions, constants, macros\n\n# auxiliary functions/constants\nfound_a_bug() = error(\"you just found a bug"
  },
  {
    "path": "src/deprecated.jl",
    "chars": 1401,
    "preview": "\n@deprecate add_arg_table add_arg_table!\n@deprecate import_settings(settings, other) import_settings!(settings, other)\n@"
  },
  {
    "path": "src/parsing.jl",
    "chars": 34269,
    "preview": "## All types, functions and constants related to the actual process of\n## parsing the arguments\n\n# ArgParseError\nstruct "
  },
  {
    "path": "src/settings.jl",
    "chars": 59409,
    "preview": "## All types, functions and constants related to the specification of the arguments\n\n# actions\nconst all_actions = [:sto"
  },
  {
    "path": "test/Project.toml",
    "chars": 72,
    "preview": "version = \"1.0.0\"\n\n[deps]\nTest = \"8dfed614-e22c-5e08-85e1-65c5234f0b40\"\n"
  },
  {
    "path": "test/argparse_test01.jl",
    "chars": 2583,
    "preview": "# test 01: minimal options/arguments, auto-generated help/version;\n#          function version of add_arg_table!\n\n@tests"
  },
  {
    "path": "test/argparse_test02.jl",
    "chars": 11941,
    "preview": "# test 02: version information, default values, flags,\n#          options with types, optional arguments, variable\n#    "
  },
  {
    "path": "test/argparse_test03.jl",
    "chars": 8501,
    "preview": "# test 03: dest_name, metavar, range_tester, alternative\n#          actions, custom parser\n\nstruct CustomType\nend\n\n@test"
  },
  {
    "path": "test/argparse_test04.jl",
    "chars": 5660,
    "preview": "# test 04: manual help/version, import another parser\n\n@testset \"test 04\" begin\n\nfunction ap_settings4()\n\n    s0 = ArgPa"
  },
  {
    "path": "test/argparse_test05.jl",
    "chars": 14687,
    "preview": "# test 05: commands & subtables\n\n@testset \"test 05\" begin\n\nfunction ap_settings5()\n\n    s = ArgParseSettings(\"Test 5 for"
  },
  {
    "path": "test/argparse_test06.jl",
    "chars": 4237,
    "preview": "# test 06: argument groups\n\n@testset \"test 06\" begin\n\nfunction ap_settings6()\n\n    s = ArgParseSettings(\"Test 6 for ArgP"
  },
  {
    "path": "test/argparse_test07.jl",
    "chars": 2163,
    "preview": "# test 07: required options, line breaks in desc/epilog\n\n@testset \"test 07\" begin\n\nfunction ap_settings7()\n\n    s = ArgP"
  },
  {
    "path": "test/argparse_test08.jl",
    "chars": 2588,
    "preview": "# test 08: read args from file, read version from project\n\n@testset \"test 08\" begin\n\nfunction ap_settings8a()\n\n    s = A"
  },
  {
    "path": "test/argparse_test09.jl",
    "chars": 1722,
    "preview": "# test 09: preformatted desc/epilog\n\n@testset \"test 09\" begin\n\nfunction ap_settings9()\n\n    s = ArgParseSettings(descrip"
  },
  {
    "path": "test/argparse_test10.jl",
    "chars": 7125,
    "preview": "# test 10: multiple metavars\n#          function version of add_arg_table!\n\n@testset \"test 10\" begin\n\nfunction ap_settin"
  },
  {
    "path": "test/argparse_test11.jl",
    "chars": 587,
    "preview": "@testset \"test 11\" begin\n\n    ARGS = split(\"say hello --to world\")\n\n    s = ArgParseSettings()\n\n    @add_arg_table! s be"
  },
  {
    "path": "test/argparse_test12.jl",
    "chars": 5193,
    "preview": "# test 12: mutually exclusive and required argument groups\n\n@testset \"test 12\" begin\n\nfunction ap_settings12()\n\n    s = "
  },
  {
    "path": "test/argparse_test13.jl",
    "chars": 7113,
    "preview": "# test 13: help_width and help_alignment_width settings\n\n@testset \"test 13\" begin\n\nfunction ap_settings13()\n\n    s = Arg"
  },
  {
    "path": "test/argparse_test14.jl",
    "chars": 2720,
    "preview": "# test 14: default values converted to arg_type\n\n@testset \"test 14\" begin\n\nfunction ap_settings14()\n\n    s = ArgParseSet"
  },
  {
    "path": "test/args-file1",
    "chars": 9,
    "preview": "--opt2=y\n"
  },
  {
    "path": "test/args-file2",
    "chars": 21,
    "preview": "@args-file1\n--opt1=x\n"
  },
  {
    "path": "test/common.jl",
    "chars": 1093,
    "preview": "using ArgParse\nusing Test\n\nmacro ap_test_throws(args)\n    :(@test_throws ArgParseError $(esc(args)))\nend\n\nmacro aps_test"
  },
  {
    "path": "test/runtests.jl",
    "chars": 223,
    "preview": "module ArgParseTests\n\ninclude(\"common.jl\")\n\nfor i = 1:14\n    try\n        s_i = lpad(string(i), 2, \"0\")\n        include(\""
  }
]

About this extraction

This page contains the full source code of the carlobaldassi/ArgParse.jl GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 50 files (229.1 KB), approximately 60.4k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!