Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/argmojo_overall_planning.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ These features appear across multiple libraries and depend only on string operat
| `NO_COLOR` env variable | — | — | — | — | I need it personally | **Done** |
| Response file (`@args.txt`) | ✓ | — | — | — | javac, MSBuild | **Done** |
| Argument parents (shared args) | ✓ | — | — | — | | Phase 5 |
| Interactive prompting | — | ✓ | — | — | | Phase 5 |
| Interactive prompting | — | ✓ | — | — | | **Done** |
| Password / masked input | — | ✓ | — | — | | Phase 5 |
| Confirmation (`--yes` / `-y`) | — | ✓ | — | — | | Phase 5 |
| Pre/Post run hooks | — | — | ✓ | — | | Phase 5 |
Expand Down Expand Up @@ -166,7 +166,8 @@ tests/
├── test_response_file.mojo # response file (@args.txt) expansion tests
├── test_remainder_known.mojo # remainder, parse_known_arguments, allow_hyphen_values tests
├── test_fullwidth.mojo # full-width → half-width auto-correction tests
└── test_groups_help.mojo # argument groups in help + value_name wrapping tests
├── test_groups_help.mojo # argument groups in help + value_name wrapping tests
└── test_prompt.mojo # interactive prompting tests
examples/
├── demo.mojo # comprehensive showcase of all ArgMojo features
├── mgrep.mojo # grep-like CLI example (no subcommands)
Expand Down Expand Up @@ -235,6 +236,7 @@ examples/
| CJK punctuation auto-correction (em-dash `U+2014` → hyphen-minus) | ✓ | ✓ |
| Compile-time `StringLiteral` builder params (`.long[]`, `.short[]`, `.choice[]`, colours, etc.) | ✓ | — |
| Registration-time validation for group constraints (`mutually_exclusive`, `required_together`, etc.) | ✓ | ✓ |
| Interactive prompting (`.prompt()`, `.prompt["..."]()` → prompt for missing args) | ✓ | ✓ |

> ⚠ Response file support is temporarily disabled due to a Mojo compiler deadlock under `-D ASSERT=all`. The implementation is preserved and will be re-enabled when the compiler bug is fixed.

Expand Down
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Comment out unreleased changes here. This file will be edited just before each r
11. **Argument groups in help.** Add `.group["name"]()` builder method on `Argument`. Arguments assigned to the same group are displayed under a dedicated heading in `--help` output, in first-appearance order. Ungrouped arguments remain under the default "Options:" heading. Persistent arguments are collected under "Global Options:" as before (PR #17).
12. **Value-name wrapping control.** Change `.value_name()` to accept compile-time parameters: `.value_name["NAME"]()` or `.value_name["NAME", False]()`. When `wrapped` is `True` (the default), the custom value name is displayed in angle brackets (`<NAME>`) in help output — matching the convention used by clap, cargo, pixi, and git. When `wrapped` is `False`, the value name is displayed bare (`NAME`). The auto-generated default placeholder (`<arg_name>`) is not affected (PR #17).
13. **Registration-time validation for group constraints.** `mutually_exclusive()`, `required_together()`, `one_required()`, and `required_if()` now validate argument names against `self.args` at the moment they are called. An `Error` is raised immediately if any name is unknown, empty lists are rejected, and duplicates are silently deduplicated. `required_if()` additionally rejects self-referential rules (`target == condition`). This catches developer typos on the very first `mojo run`, without waiting for end-user input (PR #22).
14. **Interactive prompting.** Add `.prompt()` and `.prompt["text"]()` builder methods on `Argument`. When an argument marked with `.prompt()` is not provided on the command line, the user is interactively prompted for its value before validation runs. Use `.prompt()` to prompt with the argument's help text, or `.prompt["Custom text"]()` to set a custom message. Works on both required and optional arguments. Prompts show valid choices for `.choice[]()` arguments and show default values in parentheses. For flag arguments, `y`/`n` input is accepted. When stdin is not a terminal (e.g., piped input, CI environments, `/dev/null`), or when `input()` otherwise raises, the exception is caught, prompting stops gracefully, and any values collected so far are preserved (PR #23).

### 🦋 Changed in v0.4.0

Expand Down Expand Up @@ -57,6 +58,7 @@ Comment out unreleased changes here. This file will be edited just before each r
- Add 5 tests to `tests/test_groups.mojo` covering registration-time validation: unknown argument detection for `mutually_exclusive`, `required_together`, `one_required`, and `required_if` (both target and condition) (PR #22).
- Add Developer Validation section to user manual documenting the two-layer validation model (compile-time `StringLiteral` + runtime registration-time `raises`) with recommended workflow (PR #22).
- Add `pixi run debug` task that runs all examples under `-D ASSERT=all` with `--help` to exercise registration-time validation in CI (PR #22).
- Add `tests/test_prompt.mojo` with tests covering interactive prompting builder methods, optional/required prompt arguments, prompting skipped when values are provided, choices and defaults integration, field propagation through copy, and combined features (PR #23).

---

Expand Down
204 changes: 204 additions & 0 deletions docs/user_manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,19 @@ from argmojo import Argument, Command
- [Remainder Positional (`.remainder()`)](#remainder-positional-remainder)
- [Allow Hyphen Values (`.allow_hyphen_values()`)](#allow-hyphen-values-allow_hyphen_values)
- [Partial Parsing (`parse_known_arguments()`)](#partial-parsing-parse_known_arguments)
- [Interactive Prompting](#interactive-prompting)
- [Setup Example](#setup-example)
- [Enabling Prompting](#enabling-prompting)
- [Interactive Session Examples](#interactive-session-examples)
- [All arguments missing — full prompting](#all-arguments-missing--full-prompting)
- [Partial arguments — only missing ones are prompted](#partial-arguments--only-missing-ones-are-prompted)
- [All arguments provided — no prompting at all](#all-arguments-provided--no-prompting-at-all)
- [Empty input with a default — default value is used](#empty-input-with-a-default--default-value-is-used)
- [Flag argument — y/n prompt](#flag-argument--yn-prompt)
- [Argument with choices — choices are shown](#argument-with-choices--choices-are-shown)
- [Prompt Format](#prompt-format)
- [Interaction with Other Features](#interaction-with-other-features)
- [Non-Interactive Use (CI / Piped Input)](#non-interactive-use-ci--piped-input)
- [Shell Completion](#shell-completion)
- [Built-in `--completions` Flag](#built-in---completions-flag)
- [Disabling the Built-in Flag](#disabling-the-built-in-flag)
Expand Down Expand Up @@ -398,6 +411,8 @@ Argument("name", help="...")
║ .persistent() inherit to subcommands (named only)
║ .default_if_no_value["val"]() default-if-no-value (value only)
║ .require_equals() force --key=value syntax (named value only)
║ .prompt() prompt interactively (any)
║ .prompt["msg"]() custom prompt message (any; implies .prompt())
╠══ Command-level constraints (called on Command, not Argument) ════════════════
║ command.mutually_exclusive(["a","b"]) at most one from the group
Expand Down Expand Up @@ -466,6 +481,8 @@ The table below shows which builder methods can be used with each argument mode.
| `.default_if_no_value["val"]()` | ✓ | — | — | — |
| `.allow_hyphen_values()` | ✓ | — | — | ✓ |
| `.remainder()` | — | — | — | ✓ |
| `.prompt()` | ✓ | ✓ | ✓ | ✓ |
| `.prompt["msg"]()` | ✓ | ✓ | ✓ | ✓ |
| `.require_equals()` | ✓ | — | — | — |
| `command.mutually_exclusive()` ³ | ✓ | ✓ | ✓ | — |
| `command.one_required()` ³ | ✓ | ✓ | ✓ | — |
Expand Down Expand Up @@ -2899,6 +2916,191 @@ command.response_file_prefix("+")

end of Response Files section -->

## Interactive Prompting

ArgMojo supports **interactive prompting** for missing arguments. When an argument marked with `.prompt()` is not provided on the command line, the user is asked to enter its value interactively before validation runs.

This is useful for required credentials, configuration wizards, or any scenario where guided input improves the user experience.

### Setup Example

The examples below use this `login` command:

```mojo
from argmojo import Argument, Command

fn main() raises:
var command = Command("login", "Authenticate with the service")
command.add_argument(
Argument("user", help="Username")
.long["user"]()
.required()
.prompt()
)
command.add_argument(
Argument("token", help="API token")
.long["token"]()
.required()
.prompt["Enter your API token"]()
)
command.add_argument(
Argument("region", help="Server region")
.long["region"]()
.choice["us"]()
.choice["eu"]()
.choice["ap"]()
.default["us"]()
.prompt()
)
var result = command.parse()
```

Three arguments are prompt-enabled:

- `--user` — required, prompt uses the help text `"Username"`.
- `--token` — required, prompt uses custom text `"Enter your API token"`.
- `--region` — optional with choices and a default, prompt shows choices and default.

### Enabling Prompting

Use `.prompt()` on any argument — both required and optional — to enable interactive prompting:

```mojo
# Prompt using the argument's help text (or name as fallback).
Argument("user", help="Username").long["user"]().prompt()

# Prompt with custom text.
Argument("token", help="API token").long["token"]().prompt["Enter your API token"]()
```

`.prompt()` and `.prompt["custom text"]()` are the same builder method. When no text is given, the argument's help text is displayed. When custom text is provided, it overrides the help text in the prompt.

### Interactive Session Examples

#### All arguments missing — full prompting

When none of the prompt-enabled arguments are provided, the user is prompted for each one in order:

```console
$ ./login
Username: alice
Enter your API token: secret-123
Server region [us/eu/ap] (us): eu
```

The parsed result contains `user="alice"`, `token="secret-123"`, `region="eu"`.

#### Partial arguments — only missing ones are prompted

When some arguments are already provided on the command line, only the missing ones trigger a prompt:

```console
$ ./login --user alice
Enter your API token: secret-123
Server region [us/eu/ap] (us): ap
```

`--user` was given on the CLI, so `Username:` is **not** asked.

#### All arguments provided — no prompting at all

```console
$ ./login --user alice --token secret-123 --region eu
```

No prompts appear. The CLI values are used directly.

#### Empty input with a default — default value is used

When the user presses Enter without typing anything and the argument has a `.default[]()`, the default is applied:

```console
$ ./login
Username: alice
Enter your API token: secret-123
Server region [us/eu/ap] (us):
```

The user pressed Enter at `Server region`, so `region` gets the default value `"us"`.

#### Flag argument — y/n prompt

Flag arguments accept `y`/`n`/`yes`/`no` (case-insensitive):

```mojo
Argument("verbose", help="Enable verbose output")
.long["verbose"]()
.flag()
.prompt()
```

```console
$ ./app
Enable verbose output [y/n]: y
```

Answering `y` or `yes` sets the flag to `True`. Answering `n` or `no` sets it to `False`.

#### Argument with choices — choices are shown

When a prompt-enabled argument has `.choice[]()` values, they are displayed in brackets. If a default exists, it is shown in parentheses:

```console
$ ./login --user alice --token secret
Server region [us/eu/ap] (us): eu
```

The user sees the valid options and the default before typing.

### Prompt Format

The prompt message is built automatically from the argument's metadata:

```text
<text> [choice1/choice2/choice3] (default_value): _
```

Where:

- **`<text>`** — custom prompt text if given via `.prompt["..."]()`, otherwise the argument's help text, otherwise the argument name.
- **`[choices]`** — shown only when `.choice[]()` values exist.
- **`(default)`** — shown only when `.default[]()` is set.
- **`[y/n]`** — shown instead of choices for `.flag()` arguments.

Examples of prompt lines:

```console
Username: ← help text, no choices, no default
Enter your API token: ← custom prompt text
Server region [us/eu/ap] (us): ← help text + choices + default
Enable verbose output [y/n]: ← flag prompt
```

### Interaction with Other Features

- **`.required()`**: Prompting happens *before* validation. If the user provides a value via the prompt, the required check passes. `.prompt()` does **not** require `.required()` — it works on any argument.
- **`.default[]()` **: If the user presses Enter (empty input), the default is applied by the normal default-filling phase.
- **`.choice[]()` **: Choices are displayed in the prompt. If the user enters an invalid choice, a validation error is raised after prompting.
- **Subcommands**: Each subcommand can have its own prompt-enabled arguments.
- **Persistent flags**: Persistent arguments with `.prompt()` are prompted at the level where they are missing.
- **`help_on_no_arguments()`**: Cannot be combined with `.prompt()` on the same command. When no arguments are given, `help_on_no_arguments()` prints help and exits *before* prompting runs, making prompt-enabled arguments unreachable. ArgMojo raises a registration-time error if you attempt this combination.

### Non-Interactive Use (CI / Piped Input)

When stdin is not a terminal (piped input, CI environments, `< /dev/null`), the `input()` call raises on EOF. ArgMojo catches this gracefully and stops prompting — any values collected so far are preserved, defaults are then applied normally, and validation proceeds as usual.

```console
$ echo "" | ./login --user alice --token secret
```

Prompts are still printed to stdout, but `input()` reads from the pipe. Once the pipe is exhausted, `input()` raises and prompting stops. `--region` gets its default `"us"`.

To avoid prompting entirely, always provide all arguments on the command line:

```console
$ ./login --user alice --token secret --region eu
```

## Shell Completion

ArgMojo can generate **shell completion scripts** for Bash, Zsh, and Fish. These scripts enable tab-completion for your CLI's options, flags, subcommands, and choice values — with zero runtime overhead.
Expand Down Expand Up @@ -3170,6 +3372,8 @@ The table below maps every ArgMojo builder method / command-level method to its
| `.require_equals()` | — | — | `.require_equals(true)` | — |
| `.remainder()` | `nargs=argparse.REMAINDER` | — | `.trailing_var_arg(true)` ¹¹ | `TraverseChildren` ¹² |
| `.allow_hyphen_values()` | — | — | `.allow_hyphen_values(true)` | — |
| `.prompt()` | — | `prompt=True` | — | — |
| `.prompt["msg"]()` | — | `prompt="msg"` | — | — |

### Command-Level Constraint Methods

Expand Down
39 changes: 37 additions & 2 deletions examples/demo.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ clamping, value delimiter, nargs, key-value map, aliases, deprecated args,
negative number passthrough, allow_positional_with_subcommands, custom tips,
help_on_no_arguments, default_if_no_value, require_equals, response files,
remainder positionals, allow_hyphen_values, parse_known_arguments,
argument groups in help (.group()), and value_name wrapping control
(.value_name["NAME"]() or .value_name["NAME", False]()).
argument groups in help (.group()), value_name wrapping control
(.value_name["NAME"]() or .value_name["NAME", False]()), and interactive
prompting (.prompt(), .prompt["..."]()).

Note: This demo looks very strange, but useful :D

Expand Down Expand Up @@ -98,6 +99,11 @@ Try these (build first with: pixi run package && mojo build -I src -o demo examp
./demo run myapp --verbose -x --output=foo.txt
./demo run - # stdin convention via allow_hyphen_values
./demo run myapp # no extra args → empty remainder

# ── Subcommand: login (interactive prompting) ────────────────────────
./demo login # prompts for user and token interactively
./demo login --user alice --token secret # no prompts needed
./demo login --user alice # prompts only for token
"""

from argmojo import Argument, Command
Expand Down Expand Up @@ -345,6 +351,35 @@ fn main() raises:
run.help_on_no_arguments()
app.add_subcommand(run^)

# ── Subcommand: login (interactive prompting) ────────────────────────
# Demonstrates .prompt() and .prompt["..."]() for interactive input.
# Missing arguments are prompted for interactively.
var login = Command("login", "Authenticate with the service")
login.add_argument(
Argument("user", help="Username")
.long["user"]()
.short["u"]()
.required()
.prompt()
)
login.add_argument(
Argument("token", help="API token")
.long["token"]()
.short["t"]()
.required()
.prompt["Enter your API token"]()
)
login.add_argument(
Argument("region", help="Server region")
.long["region"]()
.choice["us"]()
.choice["eu"]()
.choice["ap"]()
.default["us"]()
.prompt()
)
app.add_subcommand(login^)

# ── Show help when invoked with no arguments ─────────────────────────
app.help_on_no_arguments()

Expand Down
3 changes: 2 additions & 1 deletion pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ test = """\
&& mojo run -I src -D ASSERT=all tests/test_const_require_equals.mojo \
&& mojo run -I src -D ASSERT=all tests/test_remainder_known.mojo \
&& mojo run -I src -D ASSERT=all tests/test_fullwidth.mojo \
&& mojo run -I src -D ASSERT=all tests/test_groups_help.mojo"""
&& mojo run -I src -D ASSERT=all tests/test_groups_help.mojo \
&& mojo run -I src -D ASSERT=all tests/test_prompt.mojo < /dev/null"""
# NOTE: test_response_file.mojo is excluded — response file expansion
# is temporarily disabled to work around a Mojo compiler deadlock
# with -D ASSERT=all. Re-enable when the compiler bug is fixed.
Expand Down
Loading
Loading