Skip to content

Commit 185c629

Browse files
committed
Add confirmation option (--yes / -y)
Add confirmation_option() and confirmation_option["prompt"]() builder methods on Command. When enabled, auto-registers --yes/-y flag and prompts the user for confirmation after parsing. Passing --yes or -y skips the prompt. Aborts on decline or non-interactive stdin. Equivalent to Click's confirmation_option decorator. - 13 new tests in tests/test_confirmation.mojo - Update user manual, changelog, planning doc - 526 tests across 18 files, all passing
1 parent 10da0cf commit 185c629

File tree

6 files changed

+481
-5
lines changed

6 files changed

+481
-5
lines changed

docs/argmojo_overall_planning.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ These features appear across multiple libraries and depend only on string operat
6969
| Argument parents (shared args) ||||| | **Done** |
7070
| Interactive prompting ||||| | **Done** |
7171
| Password / masked input ||||| | Phase 5 |
72-
| Confirmation (`--yes` / `-y`) ||||| | Phase 5 |
72+
| Confirmation (`--yes` / `-y`) ||||| | **Done** |
7373
| Pre/Post run hooks ||||| | Phase 5 |
7474
| REMAINDER number_of_values ||||| | **Done** |
7575
| Partial parsing (known args) ||||| | **Done** |
@@ -168,7 +168,8 @@ tests/
168168
├── test_fullwidth.mojo # full-width → half-width auto-correction tests
169169
├── test_groups_help.mojo # argument groups in help + value_name wrapping tests
170170
├── test_prompt.mojo # interactive prompting tests
171-
└── test_parents.mojo # argument parents (shared definitions) tests
171+
├── test_parents.mojo # argument parents (shared definitions) tests
172+
└── test_confirmation.mojo # confirmation option (--yes / -y) tests
172173
examples/
173174
├── demo.mojo # comprehensive showcase of all ArgMojo features
174175
├── mgrep.mojo # grep-like CLI example (no subcommands)
@@ -239,6 +240,7 @@ examples/
239240
| Registration-time validation for group constraints (`mutually_exclusive`, `required_together`, etc.) |||
240241
| Interactive prompting (`.prompt()`, `.prompt["..."]()` → prompt for missing args) |||
241242
| Argument parents (`add_parent(parent)` → share args across commands) |||
243+
| Confirmation option (`confirmation_option()``--yes`/`-y` to skip confirmation) |||
242244

243245
> ⚠ 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.
244246
@@ -587,7 +589,7 @@ Before adding Phase 5 features, further decompose `parse_arguments()` for readab
587589
- [x] **Argument parents**`add_parent(parent)` copies all arguments and group constraints from a parent Command, sharing definitions across multiple commands (argparse `parents`) (PR #25)
588590
- [x] **Interactive prompting** — prompt user for missing required args instead of erroring (Click `prompt=True`) (PR #23)
589591
- [ ] **Password / masked input** — hide typed characters for sensitive values (Click `hide_input=True`)
590-
- [ ] **Confirmation option**built-in `--yes` / `-y` to skip confirmation prompts (Click `confirmation_option`)
592+
- [x] **Confirmation option**`confirmation_option()` or `confirmation_option["prompt"]()` auto-registers `--yes`/`-y` flag; prompts user for confirmation after parsing; aborts on decline or non-interactive stdin (Click `confirmation_option`) (PR #26)
591593
- [ ] **Pre/Post run hooks** — callbacks before/after main logic (cobra `PreRun`/`PostRun`)
592594
- [x] **Remainder positional**`.remainder()` consumes ALL remaining tokens (including `-` prefixed); at most one per command, must be last positional (argparse `nargs=REMAINDER`, clap `trailing_var_arg`) (PR #13)
593595
- [x] **Allow hyphen values**`.allow_hyphen_values()` on positional accepts dash-prefixed tokens as values without `--`; remainder enables this automatically (clap `allow_hyphen_values`) (PR #13)

docs/changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Comment out unreleased changes here. This file will be edited just before each r
2525
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).
2626
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).
2727
15. **Argument parents.** Add `add_parent(parent)` method on `Command`. Copies all argument definitions and group constraints (mutually exclusive, required together, one-required, conditional requirements, implications) from a parent `Command` into the current command. This lets you share a common set of arguments across multiple commands without repeating them — equivalent to Python argparse's `parents` parameter. The parent is not modified. All registration-time validation guards run on each inherited argument as usual (PR #25).
28+
16. **Confirmation option.** Add `confirmation_option()` and `confirmation_option["prompt"]()` builder methods on `Command`. When enabled, the command automatically registers a `--yes` / `-y` flag and prompts the user for confirmation after parsing (and after interactive prompting, if any). If the user does not confirm (`y`/`yes`), the command aborts with an error. Passing `--yes` or `-y` on the command line skips the prompt entirely. When stdin is not interactive (piped input, `/dev/null`), the command aborts gracefully. This is equivalent to Click's `confirmation_option` decorator (PR #26).
2829

2930
### 🦋 Changed in v0.4.0
3031

@@ -61,6 +62,7 @@ Comment out unreleased changes here. This file will be edited just before each r
6162
- Add `pixi run debug` task that runs all examples under `-D ASSERT=all` with `--help` to exercise registration-time validation in CI (PR #22).
6263
- 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).
6364
- Add `tests/test_parents.mojo` with 20 tests covering argument parents: basic flag/value/positional/default inheritance, short flags, multiple parents, child-own args coexistence, group constraint inheritance (mutually exclusive, required together, one-required, conditional, implications), parent shared across children, count/append/range argument inheritance, empty parent, parent immutability, and parent with subcommands (PR #25).
65+
- Add `tests/test_confirmation.mojo` with 13 tests covering confirmation option: `--yes`/`-y` flag skips, non-interactive stdin abort, custom prompt text, coexistence with other arguments, no-confirmation normal behavior, subcommand integration, copy preservation, prompt argument integration, and parent argument integration (PR #26).
6466

6567
---
6668

docs/user_manual.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ from argmojo import Argument, Command
9292
- [Multiple Parents](#multiple-parents)
9393
- [Using with Subcommands](#using-with-subcommands)
9494
- [Notes](#notes)
95+
- [Confirmation Option](#confirmation-option)
96+
- [Basic Usage](#basic-usage)
97+
- [Custom Prompt Text](#custom-prompt-text)
98+
- [Using with Subcommands](#using-with-subcommands-1)
99+
- [Non-Interactive Use](#non-interactive-use)
95100
- [Shell Completion](#shell-completion)
96101
- [Built-in `--completions` Flag](#built-in---completions-flag)
97102
- [Disabling the Built-in Flag](#disabling-the-built-in-flag)
@@ -3193,6 +3198,85 @@ var result = app.parse()
31933198
- Child arguments added via `add_argument()` coexist with inherited ones.
31943199
- If you need different constraints for different children, apply them after `add_parent()` on each child individually.
31953200

3201+
## Confirmation Option
3202+
3203+
Some commands are destructive or irreversible — dropping databases, deleting files, deploying to production. The **confirmation option** adds a built-in `--yes` / `-y` flag that lets users skip an interactive confirmation prompt. This is equivalent to Click's `confirmation_option` decorator.
3204+
3205+
### Basic Usage
3206+
3207+
```mojo
3208+
from argmojo import Command, Argument
3209+
3210+
fn main() raises:
3211+
var cmd = Command("drop", "Drop the database")
3212+
cmd.add_argument(
3213+
Argument("name", help="Database name").positional().required()
3214+
)
3215+
cmd.confirmation_option()
3216+
3217+
var result = cmd.parse()
3218+
# Without --yes: prompts "Are you sure? [y/N]: "
3219+
# With --yes or -y: skips the prompt
3220+
print("Dropping database:", result.get_string("name"))
3221+
```
3222+
3223+
Running without `--yes`:
3224+
3225+
```sh
3226+
$ ./drop mydb
3227+
Are you sure? [y/N]: y
3228+
Dropping database: mydb
3229+
```
3230+
3231+
Running with `--yes`:
3232+
3233+
```sh
3234+
$ ./drop mydb --yes
3235+
Dropping database: mydb
3236+
```
3237+
3238+
### Custom Prompt Text
3239+
3240+
Use the compile-time parameter overload to set a custom prompt:
3241+
3242+
```mojo
3243+
cmd.confirmation_option["Drop the database? This cannot be undone."]()
3244+
```
3245+
3246+
This changes the prompt to:
3247+
3248+
```sh
3249+
Drop the database? This cannot be undone. [y/N]:
3250+
```
3251+
3252+
### Using with Subcommands
3253+
3254+
Confirmation works naturally with subcommands. The `--yes` flag is registered on the command that calls `confirmation_option()`:
3255+
3256+
```mojo
3257+
var app = Command("app", "My app")
3258+
app.confirmation_option()
3259+
3260+
var deploy = Command("deploy", "Deploy to production")
3261+
deploy.add_argument(Argument("env", help="Environment").positional().required())
3262+
app.add_subcommand(deploy^)
3263+
3264+
var result = app.parse()
3265+
# app --yes deploy prod → skips confirmation
3266+
```
3267+
3268+
### Non-Interactive Use
3269+
3270+
When stdin is not available (piped input, CI environments, `/dev/null`), the confirmation prompt cannot be displayed. In this case, the command **aborts with an error** unless `--yes` is passed. This ensures that destructive commands never run silently without explicit opt-in:
3271+
3272+
```sh
3273+
$ echo "" | ./drop mydb
3274+
error: drop: Aborted (no interactive input available)
3275+
3276+
$ ./drop mydb --yes # works in CI
3277+
Dropping database: mydb
3278+
```
3279+
31963280
## Shell Completion
31973281

31983282
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.
@@ -3479,6 +3563,7 @@ The table below maps every ArgMojo builder method / command-level method to its
34793563
| `parse_known_arguments()` | `parse_known_args()` || — ¹¹ | `FParseErrWhitelist` ¹² |
34803564
| `response_file_prefix()` | `fromfile_prefix_chars="@"` ||||
34813565
| `add_parent(parent)` | `parents=[parent]` ||||
3566+
| `confirmation_option()` || `confirmation_option` |||
34823567

34833568
### Notes
34843569

pixi.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ test = """\
4343
&& mojo run -I src -D ASSERT=all tests/test_fullwidth.mojo \
4444
&& mojo run -I src -D ASSERT=all tests/test_groups_help.mojo \
4545
&& mojo run -I src -D ASSERT=all tests/test_prompt.mojo < /dev/null \
46-
&& mojo run -I src -D ASSERT=all tests/test_parents.mojo"""
46+
&& mojo run -I src -D ASSERT=all tests/test_parents.mojo \
47+
&& mojo run -I src -D ASSERT=all tests/test_confirmation.mojo < /dev/null"""
4748
# NOTE: test_response_file.mojo is excluded — response file expansion
4849
# is temporarily disabled to work around a Mojo compiler deadlock
4950
# with -D ASSERT=all. Re-enable when the compiler bug is fixed.

src/argmojo/command.mojo

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,14 @@ struct Command(Copyable, Movable, Stringable, Writable):
242242
CJK punctuation (e.g. em-dash ``U+2014``) is substituted before
243243
running Levenshtein typo suggestion. Call
244244
``disable_punctuation_correction()`` to opt out."""
245+
var _confirmation_enabled: Bool
246+
"""When True, the command prompts the user for confirmation before
247+
returning the parse result. If ``--yes`` / ``-y`` is provided on
248+
the command line, the prompt is skipped. Enable via
249+
``confirmation_option()``."""
250+
var _confirmation_prompt: String
251+
"""The prompt text shown when asking for confirmation.
252+
Defaults to ``"Are you sure?"``."""
245253

246254
# ===------------------------------------------------------------------=== #
247255
# Life cycle methods
@@ -285,6 +293,8 @@ struct Command(Copyable, Movable, Stringable, Writable):
285293
self._response_file_max_depth = 10
286294
self._disable_fullwidth_correction = False
287295
self._disable_punctuation_correction = False
296+
self._confirmation_enabled = False
297+
self._confirmation_prompt = String("")
288298
self._header_color = _DEFAULT_HEADER_COLOR
289299
self._arg_color = _DEFAULT_ARG_COLOR
290300
self._warn_color = _DEFAULT_WARN_COLOR
@@ -325,6 +335,8 @@ struct Command(Copyable, Movable, Stringable, Writable):
325335
self._disable_punctuation_correction = (
326336
move._disable_punctuation_correction
327337
)
338+
self._confirmation_enabled = move._confirmation_enabled
339+
self._confirmation_prompt = move._confirmation_prompt^
328340
self._header_color = move._header_color^
329341
self._arg_color = move._arg_color^
330342
self._warn_color = move._warn_color^
@@ -382,6 +394,8 @@ struct Command(Copyable, Movable, Stringable, Writable):
382394
self._disable_punctuation_correction = (
383395
copy._disable_punctuation_correction
384396
)
397+
self._confirmation_enabled = copy._confirmation_enabled
398+
self._confirmation_prompt = copy._confirmation_prompt
385399
self._header_color = copy._header_color
386400
self._arg_color = copy._arg_color
387401
self._warn_color = copy._warn_color
@@ -1301,6 +1315,83 @@ struct Command(Copyable, Movable, Stringable, Writable):
13011315
)
13021316
self._help_on_no_arguments = True
13031317

1318+
fn confirmation_option(mut self) raises:
1319+
"""Adds a ``--yes`` / ``-y`` flag to skip a confirmation prompt.
1320+
1321+
When enabled, the command will prompt the user with
1322+
``"Are you sure? [y/N]: "`` after parsing (and after interactive
1323+
prompting, if any) but before validation. If the user does not
1324+
answer ``y`` or ``yes``, the command aborts with an error.
1325+
1326+
Passing ``--yes`` or ``-y`` on the command line skips the prompt
1327+
entirely. This is equivalent to Click's ``confirmation_option``
1328+
decorator.
1329+
1330+
Raises:
1331+
Error if a ``--yes`` / ``-y`` argument is already registered.
1332+
1333+
Example:
1334+
1335+
```mojo
1336+
from argmojo import Command, Argument
1337+
1338+
var cmd = Command("drop", "Drop the database")
1339+
cmd.add_argument(
1340+
Argument("name", help="Database name")
1341+
.positional().required()
1342+
)
1343+
cmd.confirmation_option()
1344+
# Users must pass --yes (or -y) to skip the prompt:
1345+
# drop mydb --yes
1346+
```
1347+
"""
1348+
self._confirmation_enabled = True
1349+
self._confirmation_prompt = String("Are you sure?")
1350+
self.add_argument(
1351+
Argument("yes", help="Skip confirmation prompt")
1352+
.long["yes"]()
1353+
.short["y"]()
1354+
.flag()
1355+
)
1356+
1357+
fn confirmation_option[prompt: StringLiteral](mut self) raises:
1358+
"""Adds a ``--yes`` / ``-y`` flag with a custom confirmation prompt.
1359+
1360+
Behaves like ``confirmation_option()`` but uses the provided
1361+
``prompt`` text instead of the default ``"Are you sure?"``.
1362+
1363+
Parameters:
1364+
prompt: The custom prompt text to display.
1365+
1366+
Raises:
1367+
Error if a ``--yes`` / ``-y`` argument is already registered.
1368+
1369+
Example:
1370+
1371+
```mojo
1372+
from argmojo import Command, Argument
1373+
1374+
var cmd = Command("drop", "Drop the database")
1375+
cmd.add_argument(
1376+
Argument("name", help="Database name")
1377+
.positional().required()
1378+
)
1379+
cmd.confirmation_option["Drop the database? This cannot be undone."]()
1380+
```
1381+
"""
1382+
constrained[
1383+
len(prompt) > 0,
1384+
"confirmation_option: prompt text must not be empty",
1385+
]()
1386+
self._confirmation_enabled = True
1387+
self._confirmation_prompt = String(prompt)
1388+
self.add_argument(
1389+
Argument("yes", help="Skip confirmation prompt")
1390+
.long["yes"]()
1391+
.short["y"]()
1392+
.flag()
1393+
)
1394+
13041395
# [Mojo Miji]
13051396
# `name` is a type parameter (StringLiteral) rather than a runtime
13061397
# argument so that the colour name is validated at compile time.
@@ -1833,11 +1924,12 @@ struct Command(Copyable, Movable, Stringable, Writable):
18331924

18341925
# Apply defaults, propagate implications, prompt for remaining
18351926
# values, re-apply implications (in case prompts triggered new
1836-
# ones), then validate constraints.
1927+
# ones), confirm if required, then validate constraints.
18371928
self._apply_defaults(result)
18381929
self._apply_implications(result)
18391930
self._prompt_missing_args(result)
18401931
self._apply_implications(result)
1932+
self._confirm(result)
18411933
self._validate(result)
18421934

18431935
return result^
@@ -2558,6 +2650,39 @@ struct Command(Copyable, Movable, Stringable, Writable):
25582650
)
25592651
return -1
25602652

2653+
# ===------------------------------------------------------------------=== #
2654+
# Confirmation prompt
2655+
# ===------------------------------------------------------------------=== #
2656+
2657+
fn _confirm(self, result: ParseResult) raises:
2658+
"""Prompts the user for confirmation if ``confirmation_option()`` is
2659+
enabled and ``--yes`` / ``-y`` was not passed.
2660+
2661+
Aborts with an error if the user does not confirm, or if stdin is
2662+
not interactive (e.g. piped / ``/dev/null``).
2663+
2664+
Args:
2665+
result: The parse result (read-only; only checks the ``yes``
2666+
flag).
2667+
2668+
Raises:
2669+
Error: If the user declines or stdin is unavailable.
2670+
"""
2671+
if not self._confirmation_enabled:
2672+
return
2673+
if result.get_flag("yes"):
2674+
return
2675+
var prompt = self._confirmation_prompt + " [y/N]: "
2676+
var value: String
2677+
try:
2678+
value = input(prompt)
2679+
except:
2680+
self._error("Aborted (no interactive input available)")
2681+
return
2682+
var lower = value.lower()
2683+
if lower != "y" and lower != "yes":
2684+
self._error("Aborted")
2685+
25612686
# ===------------------------------------------------------------------=== #
25622687
# Interactive prompting
25632688
# ===------------------------------------------------------------------=== #

0 commit comments

Comments
 (0)