# How to test a CLI's --help and --version

Testing a CLI's --help and --version flags means checking that each prints the expected text to stdout, exits with code 0, and needs no config or network.

Last updated September 29, 2026, 9 min read

## Learning objectives

After reading this article you will be able to:

-   Explain why help and version flags are a contract
-   List what each flag should print and return
-   Check how the CLI handles an unknown flag

## Related content

-   [How to test a command-line application](https://specstory.com/learning/cli-and-web/testing-command-line-applications)
-   [What are exit codes?](https://specstory.com/learning/cli-and-web/exit-codes)
-   [What is the difference between stdout and stderr?](https://specstory.com/learning/cli-and-web/stdout-vs-stderr)
-   [How to verify a CLI that a coding agent built](https://specstory.com/learning/cli-and-web/verifying-agent-built-cli)
-   [What is an agent-native CLI?](https://specstory.com/learning/cli-and-web/agent-native-cli)

## Key points

-   Run each flag on a fresh install with an empty home directory and no network.
-   Expect both flags to print their text to stdout, leave stderr empty, and exit with code 0.
-   Expect an unknown flag to fail with a usage message on stderr and a nonzero exit code.

## How do you test a CLI's --help and --version?

To test the `--help` and `--version` flags of a [command-line interface](https://specstory.com/learning/cli-and-web/testing-command-line-applications) (CLI), run the installed program with each flag in a clean environment. Check that each prints its text to [stdout](https://specstory.com/learning/cli-and-web/stdout-vs-stderr), leaves stderr empty, and exits with code 0 without reading config or using the network. Then pass an unknown flag and expect a usage error.

Both flags belong to the CLI contract, the behavior that callers of a command depend on. Scripts run `--version` to check which release is installed, and a [coding agent](https://specstory.com/learning/ai-coding/coding-agent) can run `--help` to find a tool's flags. A change that breaks either flag breaks those callers.

The [GNU Coding Standards](https://www.gnu.org/prep/standards/html_node/_002d_002dhelp.html) treat both as standard options. They say that `--help` prints brief documentation on standard output and should then "exit successfully." The [version rule](https://www.gnu.org/prep/standards/html_node/_002d_002dversion.html) asks for a first line that another program can parse.

## What do you need before you start?

A test of the two flags needs five things:

-   **The installed program.** Install the branch into a fresh virtual environment, so the test runs the entry point that users run. Run pytest in the activated environment.
-   **An empty home directory.** Point `HOME` at an empty temporary directory, so the program finds no config file.
-   **No network.** Run the tests where connections fail, e.g. with `docker run --network none`.
-   **Fixed output settings.** Set `NO_COLOR=1` and `COLUMNS=80`. Python 3.14's argparse can add color codes to help unless [NO\_COLOR](https://specstory.com/learning/cli-and-web/no-color) is set, and it sets the help wrap width from `COLUMNS`.
-   **A runner that keeps streams apart.** Python's `subprocess.run` returns the exit code and each stream separately.

## How do you test --help and --version step by step?

Here is an illustrative example. Acme Co. sells furniture online, and its Python command-line tool, `acme`, exports orders. A developer at Acme asks a coding agent to "Add a `--version` flag to `acme`." The agent adds it with the `version` action of Python's [argparse](https://docs.python.org/3/library/argparse.html) module, and its output says the task is "done."

### 1\. Run each flag in a clean environment

The developer runs the flag from a fresh install with an empty home directory:

```text
$ mkdir /tmp/empty
$ HOME=/tmp/empty NO_COLOR=1 acme --version
Traceback (most recent call last):
  ...
FileNotFoundError: [Errno 2] No such file or directory: '/tmp/empty/.acme.toml'
$ echo $?
1
```

The program reads `~/.acme.toml` before it parses its arguments, and the agent's environment had that file. The fix moves `load_config()` below `parse_args()`, so both flags exit before any config or network code runs.

### 2\. Check the help text and its stream

Requested help is the result of the command, so it belongs on stdout. Python's argparse prints requested help to stdout, while Go's standard `flag` package writes it to stderr. This pytest test checks each result:

```python
import os
import subprocess

def run(*args, home):
    env = {"PATH": os.environ["PATH"], "HOME": str(home),
           "NO_COLOR": "1", "COLUMNS": "80"}
    return subprocess.run(["acme", *args], capture_output=True, text=True,
                          env=env, timeout=10)

def test_help_prints_usage_to_stdout(tmp_path):
    for flag in ("--help", "-h"):
        result = run(flag, home=tmp_path)
        assert result.returncode == 0
        assert result.stderr == ""
        assert result.stdout.startswith("usage: acme")
```

Help on the wrong stream still exits 0, so the test checks the [exit code](https://specstory.com/learning/cli-and-web/exit-codes) and both streams. The loop also runs `-h`. The [Command Line Interface Guidelines](https://clig.dev/) ask for full help on both forms and say that `-h` should mean only help. The `timeout` fails a flag that waits on the network.

### 3\. Check the version line

The GNU rule puts the version number after the last space of the first line. GNU programs follow it with a copyright line and a license line. This test compares the number with the installed package:

```python
from importlib.metadata import version

def test_version_matches_the_package(tmp_path):
    result = run("--version", home=tmp_path)
    assert (result.returncode, result.stderr) == (0, "")
    first_line = result.stdout.splitlines()[0]
    assert first_line.split()[-1] == version("acme")
```

The test fails with `AssertionError: assert '1.4.0' == '1.5.0'`, because the agent copied the version from an old README. The fix reads it from `importlib.metadata`.

### 4\. Check that help does no work

GNU also says that the program should ignore other options once either flag appears and skip its normal work. This test adds `--help` to a real export:

```python
def test_help_does_no_work(tmp_path):
    out = tmp_path / "orders.csv"
    result = run("export", "--output", str(out), "--help", home=tmp_path)
    assert result.returncode == 0
    assert result.stdout.startswith("usage: acme export")
    assert not out.exists()
```

### 5\. Save the help text as a golden file

A [golden file](https://specstory.com/learning/test-quality/golden-file-testing) keeps the approved `acme --help` output, and the test fails on any difference. A changed help line can mean a renamed flag that breaks scripts. Record the file from the installed `acme` command, because argparse by default takes the name in the usage line from how the program started, e.g. `python -m acme`. A [snapshot test](https://specstory.com/learning/testing/snapshot-testing) library rewrites the file on request, and a person reviews each rewrite.

### 6\. Run the checks on each build

The tests need no data and no network, so they can run as a [smoke test](https://specstory.com/learning/testing/smoke-testing) on each build.

This example is simplified. A real CLI would also keep a golden help file for each subcommand, e.g. `acme export --help`.

## How do you test unknown flags?

An unknown flag should stop the program before it does any work. The contract has three parts:

-   **A nonzero exit code.** The code is usually 2, which argparse and Go's `flag` package return for a usage error.
-   **A usage message on stderr.** The message names the bad flag, and stdout stays empty.
-   **No side effects.** The program writes no file and sends no request.

The developer tries a misspelled flag:

```text
$ acme export --fromat json
usage: acme [-h] [--version] {export} ...
acme: error: unrecognized arguments: --fromat json
$ echo $?
2
```

By default, argparse accepts any unambiguous prefix of a long flag, so `acme --ver` prints the version. A later `--verbose` flag would make `--ver` ambiguous and break scripts that use it. Setting `allow_abbrev=False` on the parser and each subparser turns prefix matching off.

One test keeps the usage error, and one rejects prefixes:

```python
def test_unknown_flag_is_a_usage_error(tmp_path):
    out = tmp_path / "orders.csv"
    result = run("export", "--fromat", "json", "--output", str(out),
                 home=tmp_path)
    assert (result.returncode, result.stdout) == (2, "")
    assert "unrecognized arguments: --fromat" in result.stderr
    assert not out.exists()

def test_flag_prefixes_are_rejected(tmp_path):
    assert run("--ver", home=tmp_path).returncode == 2
```

Python's `parse_known_args` does not fail on unknown flags. It returns them in a list, so unless the code checks that list, `acme export --fromat json` exports CSV and exits 0. A fuzzer can also send the parser random flags, and each crash it finds is a [fuzz finding](https://specstory.com/learning/debugging/fuzz-findings-regression-tests) to keep as a test.

## What changes when a coding agent writes the code?

A coding agent runs commands in its own environment, where a config file and a network connection may already exist. Its own tests can call `parse_args(["--help"])` inside the test process, which skips the entry point and the startup code before parsing, e.g. the config load in step 1.

Help text is also input for the next agent, which can run `--help` in a sandbox that blocks the network. A help flag that needs the network then fails. With no flag list, the agent can generate a flag that does not exist, a kind of [hallucination](https://specstory.com/learning/verification/ai-code-hallucinations).

An agent also writes each `help=` string as free text, which can drift from the parser, e.g. `(default: json)` on a flag whose default is `csv`. Accurate help is one property of an [agent-native CLI](https://specstory.com/learning/cli-and-web/agent-native-cli). A practical adjustment is to write `%(default)s` in place of the value, so argparse prints the real default. Running these tests from a fresh install is the first step to [verify a CLI](https://specstory.com/learning/cli-and-web/verifying-agent-built-cli) that a coding agent built.

## What are common mistakes?

These mistakes let a broken parser pass its tests:

-   **Leaving the bare command untested.** Python's argparse makes subcommands optional by default, so `acme` with no arguments can end in a traceback instead of usage text.
-   **Assuming help wins in any position.** Python's argparse reads arguments from left to right, so `acme export --format xml --help` exits 2 on the invalid value.
-   **Guessing the short version flag.** The Command Line Interface Guidelines warn that `-v` "can often mean either verbose or version," so a test records the tool's choice.

## How do you check that it worked?

The checks are complete when these statements are true on a fresh install with an empty home directory and no network:

-   `acme --help` and `acme -h` both print full help to stdout, write nothing to stderr, and exit 0.
-   The last word on the first line of `acme --version` matches the package version.
-   `acme export --output orders.csv --help` prints help and creates no file.
-   An unknown flag exits 2 with a usage message on stderr and nothing on stdout.
-   Each golden help file matches, and a person reviewed its last change.

Then move `load_config()` back above `parse_args()` and confirm that the tests fail. The tests cover the two flags and the parser, not what `acme export` exports. No findings is not the same as complete coverage.

## FAQs

### Should help text go to stdout or stderr?

Help text that a user asks for should go to stdout, because it is the result of the command. A usage message printed after an unknown flag belongs on stderr. Some parsers, e.g. Go's standard flag package, write requested help to stderr, so a test should check the stream.

### What exit code should a help flag return?

A help flag should return exit code 0 when a user asks for help, because the program did what it was asked. When a parser prints usage text because of an error, e.g. an unknown flag, the exit code should be nonzero, usually 2.

### What should a version flag print?

A version flag should print a first line on stdout that ends with the version number, after the last space, so scripts can parse it. GNU programs add a copyright line and a license line after it. The program then exits with code 0.

### Should the short help flag work too?

The short help flag should work too and show the same full help as the long help flag, as the Command Line Interface Guidelines recommend. The short flag should mean only help and never stand for another option.

---

Source: [How to test CLI help and version flags | SpecStory](https://specstory.com/learning/cli-and-web/cli-help-and-version)
