Skip to content

Commit d8763f9

Browse files
authored
[core] Implement response file (but temporarily not exposed to users due to issues when setting ASSERT=all) (#13)
This PR adds opt-in “response file” (`@args.txt`) expansion to ArgMojo commands, plus documentation/examples/tests, and clarifies `default_if_no_value` semantics. However, this functionality is temporarily not exposed to users due to issues when setting `ASSERT=all` (the issue is related to `with open()`). **Changes:** - Add response-file parsing support to `Command` (prefix + recursion depth), expanding `@file` tokens into per-line arguments. - Add a dedicated response-file test suite and wire it into the test runner. - Update docs/examples/changelog and adjust `default_if_no_value` wording + a related test rename.
1 parent 6c3c8a8 commit d8763f9

File tree

9 files changed

+757
-37
lines changed

9 files changed

+757
-37
lines changed

docs/argmojo_overall_planning.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ These features appear across multiple libraries and depend only on string operat
6565
| Cap and floor (clamp) for ranges | - |||| Click `IntRange(clamp=True)` | **Done** |
6666
| Hidden subcommands ||||| | **Done** |
6767
| `NO_COLOR` env variable ||||| I need it personally | **Done** |
68-
| Response file (`@args.txt`) ||||| javac, MSBuild | Phase 5 |
68+
| Response file (`@args.txt`) ||||| javac, MSBuild | **Done** |
6969
| Argument parents (shared args) ||||| | Phase 5 |
7070
| Interactive prompting ||||| | Phase 5 |
7171
| Password / masked input ||||| | Phase 5 |
@@ -154,7 +154,8 @@ tests/
154154
├── test_subcommands.mojo # Subcommand tests (dispatch, help sub, unknown sub, etc.)
155155
├── test_negative_numbers.mojo # Negative number passthrough tests
156156
├── test_persistent.mojo # Persistent (global) flag tests
157-
└── test_const_require_equals.mojo # default_if_no_value and require_equals tests
157+
├── test_const_require_equals.mojo # default_if_no_value and require_equals tests
158+
└── test_response_file.mojo # response file (@args.txt) expansion tests
158159
examples/
159160
├── mgrep.mojo # grep-like CLI example (no subcommands)
160161
└── mgit.mojo # git-like CLI example (with subcommands)
@@ -204,6 +205,7 @@ examples/
204205
| Range clamping (`.range[1, 100]().clamp()` → adjust + warn instead of error) |||
205206
| Default-if-no-value (`.default_if_no_value("gzip")` → optional value with fallback) |||
206207
| Require equals syntax (`.require_equals()``--key=value` only) |||
208+
| Response file (`command.response_file_prefix()``@args.txt` expands file contents) |||
207209

208210
### 4.3 API Design (Current)
209211

@@ -534,7 +536,7 @@ Before adding Phase 5 features, further decompose `parse_arguments()` for readab
534536
- [ ] **Partial parsing** — parse known args only, return unknown args as-is (argparse `parse_known_args`)
535537
- [ ] **Require equals syntax** — force `--key=value`, disallow `--key value` (clap `require_equals`)
536538
- [ ] **Default-if-no-value**`--opt` (no value) → use default-if-no-value; `--opt=val` → use val; absent → use default (argparse `const`)
537-
- [ ] **Response file**`mytool @args.txt` expands file contents as arguments (argparse `fromfile_prefix_chars`, javac, MSBuild)
539+
- [x] **Response file**`mytool @args.txt` expands file contents as arguments (argparse `fromfile_prefix_chars`, javac, MSBuild)
538540
- [ ] **Argument parents** — share a common set of Argument definitions across multiple Commands (argparse `parents`)
539541
- [ ] **Interactive prompting** — prompt user for missing required args instead of erroring (Click `prompt=True`)
540542
- [ ] **Password / masked input** — hide typed characters for sensitive values (Click `hide_input=True`)

docs/changelog.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,18 @@ Comment out unreleased changes here. This file will be edited just before each r
1313
1. Add `.default_if_no_value("value")` builder method for default-if-no-value semantics. When an option has a default-if-no-value, it may appear without an explicit value: `--compress` uses the default-if-no-value, while `--compress=bzip2` uses the explicit value. For long options, `.default_if_no_value()` implies `.require_equals()`. For short options, `-c` uses the default-if-no-value while `-cbzip2` uses the attached value (PR #12).
1414
2. Add `.require_equals()` builder method. When set, long options reject space-separated syntax (`--key value`) and require `--key=value`. Can be used standalone (the value is mandatory via `=`) or combined with `.default_if_no_value()` (the value is optional; omitting it uses default-if-no-value) (PR #12).
1515
3. Help output adapts to the new modifiers: `--key=<value>` for require_equals, `--key[=<value>]` for default_if_no_value (PR #12).
16+
4. ~~Add `response_file_prefix()` builder method on `Command` for response-file support. When enabled, tokens starting with the prefix (default `@`) are expanded by reading the referenced file — each non-empty, non-comment line becomes a separate argument. Supports comments (`#`), escape (`@@literal`), recursive nesting (configurable depth), and custom prefix characters (PR #12).~~ *(Temporarily disabled — triggers a Mojo compiler deadlock under `-D ASSERT=all`. The implementation is preserved as module-level functions and will be re-enabled when the Mojo compiler bug is fixed.)*
17+
18+
### 🔧 Fixes
19+
20+
- Clarify documentation and docstrings: `default_if_no_value` does not "reject" `--key value`; it simply does not consume the next token as a value (PR #12, review feedback).
21+
- Fix cross-library comparison: click is described as "Python CLI framework" instead of incorrectly saying "built on top of argparse" (PR #12, review feedback).
22+
- Reject `.require_equals()` / `.default_if_no_value()` combined with `.number_of_values[N]()` at `add_argument()` time with a clear error (PR #12, review feedback).
1623

1724
### 📚 Documentation and testing
1825

1926
- Add `tests/test_const_require_equals.mojo` with 30 tests covering default_if_no_value, require_equals, and their interactions with choices, append, prefix matching, merged short flags, persistent flags, and help formatting (PR #12).
27+
- Add `tests/test_response_file.mojo` with 17 tests covering basic expansion, comments, whitespace stripping, escape, recursive nesting, depth limit, custom prefix, disabled-by-default, and error handling (PR #12).
2028

2129
---
2230

docs/user_manual.md

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ from argmojo import Argument, Command
6868
- [Negative Number Passthrough](#negative-number-passthrough)
6969
- [Long Option Prefix Matching](#long-option-prefix-matching)
7070
- [The `--` Stop Marker](#the----stop-marker)
71+
<!-- Response Files (temporarily disabled — Mojo compiler deadlock with -D ASSERT=all)
72+
- [Response Files](#response-files)
73+
- [Enabling Response Files](#enabling-response-files)
74+
- [File Format](#file-format)
75+
- [Escaping the Prefix](#escaping-the-prefix)
76+
- [Recursive Response Files](#recursive-response-files)
77+
- [Custom Prefix](#custom-prefix)
78+
-->
7179
- [Shell Completion](#shell-completion)
7280
- [Built-in `--completions` Flag](#built-in---completions-flag)
7381
- [Disabling the Built-in Flag](#disabling-the-built-in-flag)
@@ -2062,7 +2070,7 @@ Options:
20622070

20632071
Use `.default_if_no_value("value")` to make an option's value **optional**. When the option is present without an explicit value, the default-if-no-value is used. When an explicit value is provided (via `=` for long options, or attached for short options), that value is used instead.
20642072

2065-
`.default_if_no_value()` automatically implies `.require_equals()` for long options, so `--key value` (space-separated) is rejected — the user must write `--key=value` to supply an explicit value.
2073+
`.default_if_no_value()` automatically implies `.require_equals()` for long options in the sense that `=` is required to attach an *explicit* value. A bare `--key` is still accepted and uses the default-if-no-value; `--key value` (space-separated) does *not* treat `value` as the argument to `--key` but leaves it to be parsed as a positional argument or another option. To supply an explicit value to the option itself, the user must write `--key=value`.
20662074

20672075
```mojo
20682076
command.add_argument(
@@ -2545,6 +2553,107 @@ myapp -- -10.18
25452553

25462554
> **Tip:** ArgMojo's [Auto-detect](#negative-number-passthrough) can handle most negative-number cases without `--`. Use `--` only when auto-detect is insufficient (e.g., a digit short option is registered without `allow_negative_numbers()`).
25472555
2556+
<!-- Response Files section temporarily disabled — Mojo compiler deadlock with -D ASSERT=all.
2557+
The implementation is preserved as module-level functions and will be re-enabled
2558+
when the Mojo compiler bug is fixed.
2559+
2560+
## Response Files
2561+
2562+
A **response file** (also called an **args file**) lets users store arguments in a text file and reference it on the command line with a prefix character (default `@`). This is useful when the argument list is very long or when the same set of arguments is reused frequently.
2563+
2564+
> Libraries with similar support: **argparse** (`fromfile_prefix_chars`), **javac** (`@argfile`), **MSBuild** (`@file`), **gcc** (`@file`).
2565+
2566+
### Enabling Response Files
2567+
2568+
Call `response_file_prefix()` on your command to enable the feature:
2569+
2570+
```mojo
2571+
var command = Command("mytool", "My CLI tool")
2572+
command.response_file_prefix() # default '@'
2573+
```
2574+
2575+
Now `mytool @args.txt` reads arguments from `args.txt`, with each line becoming a separate argument.
2576+
2577+
### File Format
2578+
2579+
Each non-empty line in the response file becomes one argument. Lines starting with `#` are comments and are ignored. Leading and trailing whitespace per line is stripped.
2580+
2581+
```text
2582+
# args.txt — common flags for the build
2583+
--verbose
2584+
--output=build/release
2585+
--jobs=4
2586+
2587+
# source files
2588+
src/main.mojo
2589+
src/utils.mojo
2590+
```
2591+
2592+
```bash
2593+
mytool @args.txt
2594+
# equivalent to: mytool --verbose --output=build/release --jobs=4 src/main.mojo src/utils.mojo
2595+
```
2596+
2597+
Response file arguments can be mixed freely with direct CLI arguments:
2598+
2599+
```bash
2600+
mytool --debug @args.txt --extra-flag
2601+
```
2602+
2603+
### Escaping the Prefix
2604+
2605+
To pass a literal token that starts with `@` (e.g., an email address), double the prefix:
2606+
2607+
```bash
2608+
mytool @@user@example.com
2609+
# parsed as: @user@example.com
2610+
```
2611+
2612+
The same escape works inside response files:
2613+
2614+
```text
2615+
# users.txt
2616+
@@admin
2617+
@@guest
2618+
```
2619+
2620+
### Recursive Response Files
2621+
2622+
Response files may reference other response files:
2623+
2624+
```text
2625+
# base-args.txt
2626+
--verbose
2627+
2628+
# build-args.txt
2629+
@base-args.txt
2630+
--output=build/release
2631+
```
2632+
2633+
```bash
2634+
mytool @build-args.txt
2635+
# expands to: mytool --verbose --output=build/release
2636+
```
2637+
2638+
Recursion depth is limited to 10 by default. Adjust with `response_file_max_depth()`:
2639+
2640+
```mojo
2641+
command.response_file_max_depth(5)
2642+
```
2643+
2644+
A self-referencing or circular response file triggers an error once the depth limit is reached.
2645+
2646+
### Custom Prefix
2647+
2648+
Use a different prefix character if `@` conflicts with your argument values:
2649+
2650+
```mojo
2651+
command.response_file_prefix("+")
2652+
# Now: mytool +args.txt
2653+
```
2654+
2655+
end of Response Files section -->
2656+
25482657
## Shell Completion
25492658

25502659
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.
@@ -2701,7 +2810,7 @@ The generated scripts cover the full command tree:
27012810

27022811
The table below maps every ArgMojo builder method / command-level method to its equivalent in four popular CLI libraries. **An empty cell means the name is identical (or near-identical) to ArgMojo's.** A filled cell shows the other library's name or approach. **** means the library has no built-in equivalent.
27032812

2704-
> Libraries compared: **argparse** (Python stdlib), **click** (Python, built on top of argparse), **clap** (Rust, derive & builder API), **cobra / pflag** (Go).
2813+
> Libraries compared: **argparse** (Python stdlib), **click** (Python CLI framework), **clap** (Rust, derive & builder API), **cobra / pflag** (Go).
27052814
27062815
### Argument-Level Builder Methods
27072816

@@ -2742,6 +2851,7 @@ The table below maps every ArgMojo builder method / command-level method to its
27422851
| `required_together(…)` ||| `.requires("x")` per arg | `MarkFlagsRequiredTogether()` ¹ |
27432852
| `required_if(target, cond)` ||| `.required_if_eq("x","v")` | `MarkFlagRequired…` ¹ |
27442853
| `implies(trigger, implied)` ||| `.requires_if("v","x")` ¹⁰ ||
2854+
| `response_file_prefix()` | `fromfile_prefix_chars="@"` ||||
27452855

27462856
### Notes
27472857

examples/demo.mojo

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ conditional requirements, negatable flags, color customisation
1010
(header_color, arg_color), numeric range validation, append with range
1111
clamping, value delimiter, nargs, key-value map, aliases, deprecated args,
1212
negative number passthrough, allow_positional_with_subcommands, custom tips,
13-
help_on_no_arguments, default_if_no_value, and require_equals.
13+
help_on_no_arguments, default_if_no_value, require_equals, and response files.
1414
1515
Note: This demo looks very strange, but useful :D
1616
@@ -70,6 +70,10 @@ Try these (build first with: pixi run package && mojo build -I src -o demo examp
7070
./demo input.txt --separator="|"
7171
./demo input.txt --separator "|" # error: requires '=' syntax
7272
73+
# response file: put arguments in a file and reference with @
74+
echo '--verbose\n--level=5\n--color' > /tmp/demo_args.txt
75+
./demo input.txt @/tmp/demo_args.txt
76+
7377
# negative number passthrough
7478
./demo -- -42
7579
@@ -105,6 +109,9 @@ fn main() raises:
105109
app.header_color("CYAN")
106110
app.arg_color("GREEN")
107111

112+
# ── Response file support ────────────────────────────────────────────
113+
app.response_file_prefix() # enables @args.txt expansion
114+
108115
# ── Positional arguments ─────────────────────────────────────────────
109116
app.add_argument(
110117
Argument("input", help="Input file to process")

pixi.toml

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,21 @@ package = "mojo package src/argmojo -o argmojo.mojopkg"
2727
test = """\
2828
pixi run format \
2929
&& pixi run package \
30-
&& mojo run -I src tests/test_parse.mojo \
31-
&& mojo run -I src tests/test_groups.mojo \
32-
&& mojo run -I src tests/test_collect.mojo \
33-
&& mojo run -I src tests/test_help.mojo \
34-
&& mojo run -I src tests/test_extras.mojo \
35-
&& mojo run -I src tests/test_subcommands.mojo \
36-
&& mojo run -I src tests/test_negative_numbers.mojo \
37-
&& mojo run -I src tests/test_persistent.mojo \
38-
&& mojo run -I src tests/test_typo_suggestions.mojo \
39-
&& mojo run -I src tests/test_completion.mojo \
40-
&& mojo run -I src tests/test_implies.mojo \
41-
&& mojo run -I src tests/test_const_require_equals.mojo"""
30+
&& mojo run -I src -D ASSERT=all tests/test_parse.mojo \
31+
&& mojo run -I src -D ASSERT=all tests/test_groups.mojo \
32+
&& mojo run -I src -D ASSERT=all tests/test_collect.mojo \
33+
&& mojo run -I src -D ASSERT=all tests/test_help.mojo \
34+
&& mojo run -I src -D ASSERT=all tests/test_extras.mojo \
35+
&& mojo run -I src -D ASSERT=all tests/test_subcommands.mojo \
36+
&& mojo run -I src -D ASSERT=all tests/test_negative_numbers.mojo \
37+
&& mojo run -I src -D ASSERT=all tests/test_persistent.mojo \
38+
&& mojo run -I src -D ASSERT=all tests/test_typo_suggestions.mojo \
39+
&& mojo run -I src -D ASSERT=all tests/test_completion.mojo \
40+
&& mojo run -I src -D ASSERT=all tests/test_implies.mojo \
41+
&& mojo run -I src -D ASSERT=all tests/test_const_require_equals.mojo"""
42+
# NOTE: test_response_file.mojo is excluded — response file expansion
43+
# is temporarily disabled to work around a Mojo compiler deadlock
44+
# with -D ASSERT=all. Re-enable when the compiler bug is fixed.
4245

4346
# build example binaries
4447
build = """pixi run package \

src/argmojo/argument.mojo

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -619,9 +619,13 @@ struct Argument(Copyable, Movable, Stringable, Writable):
619619
or attached form for short options (``-cbzip2``), that explicit
620620
value is used instead.
621621
622-
For long options this implies ``require_equals()``, so
623-
``--compress val`` (space-separated) is rejected — the user
624-
must write ``--compress=val`` to supply an explicit value.
622+
For long options this implies ``require_equals()``. A
623+
space-separated token like ``--compress val`` is not accepted
624+
as the option's value; in that case ``--compress`` uses its
625+
default-if-no-value and ``val`` is parsed as a separate
626+
argument (positional or another option). To supply an
627+
explicit value for the option, the user must write
628+
``--compress=val``.
625629
626630
Examples::
627631

0 commit comments

Comments
 (0)