Parse Mode

When using Meta Apps, you may run into situations where:

  • A parent and subcommand both define a parameter with the same name (e.g., both use -v), and you need the CLI to determine which -v belongs to which command based on its position.

  • You want myapp --verbose subcommand to work, but myapp subcommand --verbose to be rejected because --verbose belongs to the parent, not the subcommand.

The App.parse_mode setting controls how parameters are scoped across command levels.

There are two modes:

  • "fallthrough" (default): Unmatched parameters fall through to parent levels. If a subcommand doesn't recognize a parameter, cyclopts checks whether a parent meta app defines it. When both levels define the same flag, the child wins.

  • "strict": Parameters only bind to the command level where they appear. A meta-app parameter placed after a subcommand is rejected with a helpful error message.

Fallthrough Mode

In the default "fallthrough" mode, meta-app parameters can appear anywhere in the token stream. This is convenient because users don't need to know which flags belong to which level.

from cyclopts import App, Parameter
from typing import Annotated

app = App()

@app.meta.default
def main(
    *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
    verbose: Annotated[bool, Parameter(alias="-v")] = False,
):
    if verbose:
        print("[verbose mode]")
    app(tokens)

@app.command
def greet(name: str, *, version: Annotated[bool, Parameter(alias="-v")] = False):
    """Greet someone."""
    if version:
        print("[version flag]")
    print(f"Hello, {name}!")

app.meta()

The --verbose flag can be placed before or after the subcommand:

$ my-script --verbose greet Alice
[verbose mode]
Hello, Alice!

$ my-script greet --verbose Alice
[verbose mode]
Hello, Alice!

When both the meta app and the subcommand define a parameter with the same name (-v), the child wins for flags placed after the subcommand:

$ my-script -v greet Alice
[verbose mode]
Hello, Alice!

$ my-script greet -v Alice
[version flag]
Hello, Alice!

$ my-script -v greet -v Alice
[verbose mode]
[version flag]
Hello, Alice!

Combined Short Options

In fallthrough mode, a combined short-option token (e.g. -xvf) is resolved against the merged flag namespace of both levels, since the person typing the command doesn't know (or care) which level implements each flag. A single left-to-right scan applies standard GNU rules to the merged table:

  • Each flag character binds at the level that owns it (the subcommand wins if both levels define the same letter).

  • The first value-taking option absorbs the rest of the token as its attached value (or, with no remainder, the next token) and ends the scan.

  • An unknown character is reported as an unknown option.

# meta owns -v/--verbose and -u/--user; ``cmd`` owns -x and -f:
$ my-script cmd -xvf        # -x, -f bind at cmd; -v binds at the meta
$ my-script cmd -xuroot     # -x binds at cmd; meta gets user="root"
$ my-script cmd -xu alice   # -x binds at cmd; meta gets user="alice"

Strict Mode

In "strict" mode, parameters are scoped to the command level where they appear. Each parameter must be placed directly after the command it belongs to — placing a parent-level parameter after a subcommand is rejected with a helpful error message.

This is useful when:

  • Migrating from Click or Typer, which use strict scoping by default.

  • Subcommands need to redefine a flag name for a different purpose.

  • You want to prevent users from accidentally passing parent-level flags in the wrong position.

app = App(parse_mode="strict")

@app.meta.default
def main(
    *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
    verbose: Annotated[bool, Parameter(alias="-v")] = False,
):
    if verbose:
        print("[verbose mode]")
    app(tokens)

@app.command
def greet(name: str, *, version: Annotated[bool, Parameter(alias="-v")] = False):
    """Greet someone."""
    if version:
        print("[version flag]")
    print(f"Hello, {name}!")

app.meta()

Parameters must be placed directly after the command they belong to. --verbose belongs to the root app, so it must appear directly after my-script:

$ my-script --verbose greet Alice
[verbose mode]
Hello, Alice!

$ my-script greet --verbose Alice
╭─ Error ────────────────────────────────────────────────────╮
│ Unknown option: "--verbose". Did you mean to place it      │
│ directly after "my-script"?                                │
╰────────────────────────────────────────────────────────────╯

When both levels share a name, each -v must be placed directly after the command it belongs to:

$ my-script -v greet -v Alice
[verbose mode]
[version flag]
Hello, Alice!

Help Pages

In strict mode, subcommand help pages only show parameters that are valid for that command — parent meta-app parameters are excluded.

$ my-script greet --help
Usage: my-script greet [OPTIONS] NAME

Greet someone.

╭─ Parameters ───────────────────────────────────────────────╮
│ *  NAME --name                [required]                   │
│    --version -v --no-version  [default: False]             │
╰────────────────────────────────────────────────────────────╯

In fallthrough mode, the same help page would also include --verbose from the meta app.

Inheritance

parse_mode is inherited by child apps. Setting it on the root app applies to all subcommands unless explicitly overridden:

app = App(parse_mode="strict")

# All subcommands inherit parse_mode="strict"
sub = App(name="sub")
app.command(sub)

# Override for a specific subcommand
special = App(name="special", parse_mode="fallthrough")
app.command(special)