Skip to content

Commit 9ba3a4b

Browse files
committed
[core] Add --no-X negation
1 parent 90bfb1f commit 9ba3a4b

File tree

7 files changed

+276
-89
lines changed

7 files changed

+276
-89
lines changed

README.md

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ ArgMojo provides a builder-pattern API for defining and parsing command-line arg
3030
- **Hidden arguments**: exclude internal args from `--help` output
3131
- **Count flags**: `-vvv``get_count("verbose") == 3`
3232
- **Positional arg count validation**: reject extra positional args
33+
- **Negatable flags**: `--color` / `--no-color` paired flags with `.negatable()`
3334
- **Mutually exclusive groups**: prevent conflicting flags (e.g., `--json` vs `--yaml`)
3435
- **Required-together groups**: enforce that related flags are provided together (e.g., `--username` + `--password`)
3536

@@ -81,17 +82,18 @@ fn main() raises:
8182
.long("format").short("f").choices(formats^).default("table")
8283
)
8384
84-
# Mutually exclusive flags
85-
cmd.add_arg(Arg("color", help="Force colored output").long("color").flag())
86-
cmd.add_arg(Arg("no-color", help="Disable colored output").long("no-color").flag())
87-
var excl: List[String] = ["color", "no-color"]
88-
cmd.mutually_exclusive(excl^)
85+
# Negatable flag — --color enables, --no-color disables
86+
cmd.add_arg(
87+
Arg("color", help="Enable colored output")
88+
.long("color").flag().negatable()
89+
)
8990
9091
# Parse and use
9192
var result = cmd.parse()
9293
print("pattern:", result.get_string("pattern"))
9394
print("verbose:", result.get_count("verbose"))
9495
print("format: ", result.get_string("format"))
96+
print("color: ", result.get_flag("color"))
9597
```
9698

9799
## Usage Examples
@@ -161,14 +163,24 @@ The `--format` option only accepts `json`, `csv`, or `table`:
161163
./demo "pattern" --format xml # Error: Invalid value 'xml' for 'format'. Valid choices: json, csv, table
162164
```
163165

166+
### Negatable flags
167+
168+
A negatable flag pairs `--X` (sets `True`) with `--no-X` (sets `False`) automatically:
169+
170+
```bash
171+
./demo "pattern" --color # color = True
172+
./demo "pattern" --no-color # color = False
173+
./demo "pattern" # color = False (default)
174+
```
175+
164176
### Mutually exclusive groups
165177

166-
`--color` and `--no-color` are mutually exclusive — using both is an error:
178+
`--json` and `--yaml` are mutually exclusive — using both is an error:
167179

168180
```bash
169-
./demo "pattern" --color # OK
170-
./demo "pattern" --no-color # OK
171-
./demo "pattern" --color --no-color # Error: Arguments are mutually exclusive: '--color', '--no-color'
181+
./demo "pattern" --json # OK
182+
./demo "pattern" --yaml # OK
183+
./demo "pattern" --json --yaml # Error: Arguments are mutually exclusive: '--json', '--yaml'
172184
```
173185

174186
### `--` stop marker

docs/argmojo_overall_planning.md

Lines changed: 33 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,30 @@ These features appear across 3+ libraries and depend only on string operations a
3636
| `--` stop marker |||| **Done** |
3737
| Auto `--help` / `-h` |||| **Done** |
3838
| Auto `--version` / `-V` |||| **Done** |
39-
| Short flag merging (`-abc`) |||| Phase 2 |
40-
| Choices / enum validation |||| Phase 2 |
41-
| Subcommands |||| Phase 3 |
42-
| Mutually exclusive flags |||| Phase 3 |
43-
| Flags required together |||| Phase 3 |
44-
| Suggest on typo (Levenshtein) | ✓ (3.14) ||| Phase 4 |
45-
| Metavar (display name for value) |||| Phase 2 |
46-
| Positional arg count validation |||| Phase 2 |
47-
| `--no-X` negation flags | ✓ (3.9) ||| Phase 3 |
39+
| Short flag merging (`-abc`) |||| **Done** |
40+
| Metavar (display name for value) |||| **Done** |
41+
| Positional arg count validation |||| **Done** |
42+
| Choices / enum validation |||| **Done** |
43+
| Mutually exclusive flags |||| **Done** |
44+
| Flags required together |||| **Done** |
45+
| `--no-X` negation flags | ✓ (3.9) ||| **Done** |
46+
| Long option prefix matching |||| Phase 3 |
47+
| Append / collect action |||| Phase 3 |
48+
| One-required group |||| Phase 3 |
49+
| Subcommands |||| Phase 4 |
50+
| Suggest on typo (Levenshtein) | ✓ (3.14) ||| Phase 5 |
4851

4952
### 2.3 Features Excluded (Infeasible or Inappropriate)
5053

51-
| Feature | Reason for Exclusion |
52-
| --------------------------------------------- | ---------------------------------------------------------------------------------- |
53-
| Derive / decorator API | Mojo has no macros or decorators |
54-
| Shell auto-completion generation | Requires writing shell scripts; out of scope |
55-
| Usage-string-driven parsing (docopt style) | Too implicit; not a good fit for a typed systems language |
56-
| Type-conversion callbacks | Mojo has no first-class closures; use `get_int()` / `get_string()` pattern instead |
57-
| Config file reading (`fromfile_prefix_chars`) | Out of scope; users can pre-process argv |
58-
| Environment variable fallback | Can be done externally; not core parser responsibility |
59-
| Template-customisable help (Go cobra style) | Mojo has no template engine; help format is hardcoded |
54+
| Feature | Reason for Exclusion |
55+
| --------------------------------------------- | --------------------------------------------------------- |
56+
| Derive / decorator API | Mojo has no macros or decorators |
57+
| Shell auto-completion generation | Requires writing shell scripts; out of scope |
58+
| Usage-string-driven parsing (docopt style) | Too implicit; not a good fit for a typed systems language |
59+
| Type-conversion callbacks | Use `get_int()` / `get_string()` pattern instead |
60+
| Config file reading (`fromfile_prefix_chars`) | Out of scope; users can pre-process argv |
61+
| Environment variable fallback | Can be done externally; not core parser responsibility |
62+
| Template-customisable help (Go cobra style) | Mojo has no template engine; help format is hardcoded |
6063

6164
## 3. Technical Foundations
6265

@@ -105,7 +108,7 @@ src/argmojo/
105108
├── command.mojo # Command struct — command definition & parsing
106109
└── result.mojo # ParseResult struct — parsed values
107110
tests/
108-
└── test_argmojo.mojo # 38 tests, all passing ✓
111+
└── test_argmojo.mojo # Unit tests for ArgMojo, ensure robustness
109112
examples/
110113
└── demo.mojo # Demo CLI tool, compilable to binary
111114
```
@@ -135,7 +138,10 @@ examples/
135138
| Count action (`-vvv` → 3) |||
136139
| Positional arg count validation |||
137140
| Clean exit for `--help` / `--version` |||
138-
| Mutually exclusive groups ||| | Required-together groups |||
141+
| Mutually exclusive groups |||
142+
| Required-together groups |||
143+
| Negatable flags (`.negatable()``--no-X`) |||
144+
139145
### 4.3 API Design (Current)
140146

141147
```mojo
@@ -209,7 +215,10 @@ pattern # By order of add_arg() calls
209215

210216
- [x] **Mutually exclusive flags**`cmd.mutually_exclusive(["json", "yaml", "toml"])`
211217
- [x] **Flags required together**`cmd.required_together(["username", "password"])`
212-
- [ ] **`--no-X` negation**`--color` / `--no-color` paired flags (argparse BooleanOptionalAction)
218+
- [x] **`--no-X` negation**`--color` / `--no-color` paired flags (argparse BooleanOptionalAction)
219+
- [ ] **Long option prefix matching**`--verb` auto-resolves to `--verbose` when unambiguous (argparse `allow_abbrev`)
220+
- [ ] **Append / collect action**`--tag x --tag y``["x", "y"]` collects repeated options into a list (argparse `append`, cobra `StringArrayVar`, clap `Append`)
221+
- [ ] **One-required group**`cmd.one_required(["json", "yaml"])` requires at least one from the group (cobra `MarkFlagsOneRequired`, clap `ArgGroup::required`)
213222
- [ ] **Aliases** for long names — `.aliases(["colour"])` for `--color`
214223
- [ ] **Deprecated arguments**`.deprecated("Use --format instead")` prints warning (argparse 3.13)
215224

@@ -268,49 +277,14 @@ Input: ["demo", "yuhao", "./src", "--ling", "-i", "--max-depth", "3"]
268277
6. Return ParseResult
269278
```
270279

271-
## 7. Testing Strategy
272-
273-
All tests use `cmd.parse_args(List[String])` to inject arguments without needing a real binary.
274-
275-
Tests run via `mojo run -I src tests/test_argmojo.mojo` (mojo test is not available in 0.26.1).
276-
277-
### Current tests (11, all passing ✓)
278-
279-
| Test | What it verifies |
280-
| ------------------------------ | --------------------------------------- |
281-
| `test_flag_long` | `--verbose` sets flag to True |
282-
| `test_flag_short` | `-v` sets flag to True |
283-
| `test_flag_default_false` | Unset flag defaults to False |
284-
| `test_key_value_long_space` | `--output file.txt` |
285-
| `test_key_value_long_equals` | `--output=file.txt` |
286-
| `test_key_value_short` | `-o file.txt` |
287-
| `test_positional_args` | Two positional args |
288-
| `test_positional_with_default` | Second positional uses default |
289-
| `test_mixed_args` | Positional + flags + key-value together |
290-
| `test_double_dash_stop` | `--` stops option parsing |
291-
| `test_has` | `has()` returns correct results |
292-
293-
### Tests to add (per roadmap)
294-
295-
- Short flag merging: `-abc` → three separate flags
296-
- Short option attached value: `-ofile.txt`
297-
- Choices validation: pass/fail
298-
- Positional count: too many / too few
299-
- Hidden args: not shown in help
300-
- Count action: `-vvv` == 3
301-
- Mutually exclusive: error on `--json --yaml`
302-
- Required together: error on `--user` without `--pass`
303-
- Negation: `--no-color` sets color to False
304-
- Subcommands: correct dispatch
305-
- Typo suggestion: Levenshtein output
306-
307280
## 8. Mojo 0.26.1 Notes
308281

309-
Important Mojo-specific patterns used throughout this project:
282+
Here are some important Mojo-specific patterns used throughout this project. Mojo is rapidly evolving, so these may need to be updated in the future:
310283

311284
| Pattern | What & Why |
312285
| ---------------------- | --------------------------------------------------- |
313-
| `@fieldwise_init` | Replaces `@value` in Mojo 0.26.1 |
286+
| `"""Tests..."""` | Docstring convention |
287+
| `@fieldwise_init` | Replaces `@value` |
314288
| `var self` | Used for builder methods instead of `owned self` |
315289
| `String()` | Explicit conversion; `str()` is not available |
316290
| `[a, b, c]` for `List` | List literal syntax instead of variadic constructor |

docs/user_manual.md

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ myapp "hello" ./src
4545
# pattern path
4646
```
4747

48-
Positional arguments are assigned in the order they are registered with `add_arg()`. If fewer values are provided than defined arguments, the remaining ones use their default values (if any). If more are provided, an error is raised (see [Positional Arg Count Validation](#16-positional-arg-count-validation)).
48+
Positional arguments are assigned in the order they are registered with `add_arg()`. If fewer values are provided than defined arguments, the remaining ones use their default values (if any). If more are provided, an error is raised (see [Positional Arg Count Validation](#17-positional-arg-count-validation)).
4949

5050
**Retrieving:**
5151

@@ -388,7 +388,73 @@ Each group is validated independently — using `--json` and `--no-color` togeth
388388

389389
> **Note:** Pass the `List[String]` with `^` (ownership transfer).
390390
391-
## 15. Required-Together Groups
391+
## 15. Negatable Flags
392+
393+
A **negatable** flag automatically creates a `--no-X` counterpart. When the user passes `--X`, the flag is set to `True`; when they pass `--no-X`, it is explicitly set to `False`.
394+
395+
This replaces the manual pattern of defining two separate flags (`--color` and `--no-color`) and a mutually exclusive group.
396+
397+
### Defining a negatable flag
398+
399+
```mojo
400+
cmd.add_arg(
401+
Arg("color", help="Enable colored output")
402+
.long("color").flag().negatable()
403+
)
404+
```
405+
406+
### Behaviour
407+
408+
```bash
409+
myapp --color # color = True, has("color") = True
410+
myapp --no-color # color = False, has("color") = True
411+
myapp # color = False, has("color") = False (default)
412+
```
413+
414+
Use `result.has("color")` to distinguish between "user explicitly disabled colour" (`--no-color`) and "user didn't mention colour at all".
415+
416+
### Help output
417+
418+
Negatable flags are displayed as a paired form:
419+
420+
```text
421+
--color / --no-color Enable colored output
422+
```
423+
424+
### Comparison with manual approach
425+
426+
**Before (two flags + mutually exclusive):**
427+
428+
```mojo
429+
cmd.add_arg(Arg("color", help="Force colored output").long("color").flag())
430+
cmd.add_arg(Arg("no-color", help="Disable colored output").long("no-color").flag())
431+
var group: List[String] = ["color", "no-color"]
432+
cmd.mutually_exclusive(group^)
433+
```
434+
435+
**After (single negatable flag):**
436+
437+
```mojo
438+
cmd.add_arg(
439+
Arg("color", help="Enable colored output")
440+
.long("color").flag().negatable()
441+
)
442+
```
443+
444+
The negatable approach is simpler and uses only one entry in `ParseResult`.
445+
446+
### When to use
447+
448+
| Scenario | Example |
449+
| ------------------ | ------------------------------------ |
450+
| Colour control | `--color` / `--no-color` |
451+
| Feature toggle | `--cache` / `--no-cache` |
452+
| Header inclusion | `--headers` / `--no-headers` |
453+
| Interactive prompt | `--interactive` / `--no-interactive` |
454+
455+
> **Note:** Only flags (`.flag()`) can be made negatable. Calling `.negatable()` on a non-flag argument has no effect on parsing.
456+
457+
## 16. Required-Together Groups
392458

393459
**Required together** means "if any one of these arguments is provided, all the others must be provided too". If only some are given, parsing fails.
394460

@@ -458,7 +524,7 @@ cmd.mutually_exclusive(excl^)
458524

459525
> **Note:** Pass the `List[String]` with `^` (ownership transfer).
460526
461-
## 16. Positional Arg Count Validation
527+
## 17. Positional Arg Count Validation
462528

463529
ArgMojo ensures that the user does not provide more positional arguments than defined. Extra positional values trigger an error.
464530

@@ -484,7 +550,7 @@ myapp "hello" ./src # OK — pattern = "hello", path = "./src"
484550
myapp "hello" ./src /tmp # Error: Too many positional arguments: expected 2, got 3
485551
```
486552

487-
## 17. The `--` Stop Marker
553+
## 18. The `--` Stop Marker
488554

489555
A bare `--` tells the parser to **stop interpreting options**. Everything after `--` is treated as a positional argument, even if it looks like an option.
490556

@@ -508,7 +574,7 @@ myapp --ling -- "-v is not a flag here" ./src
508574
# path = "./src"
509575
```
510576

511-
## 18. Auto-generated Help
577+
## 19. Auto-generated Help
512578

513579
Every command automatically supports `--help` (or `-h`). The help text is generated from the registered argument definitions.
514580

@@ -552,7 +618,7 @@ Options:
552618

553619
After printing help, the program exits cleanly with exit code 0.
554620

555-
## 19. Version Display
621+
## 20. Version Display
556622

557623
Every command automatically supports `--version` (or `-V`).
558624

@@ -575,7 +641,7 @@ var cmd = Command("myapp", "Description", version="1.0.0")
575641

576642
After printing the version, the program exits cleanly with exit code 0.
577643

578-
## 20. Reading Parsed Results
644+
## 21. Reading Parsed Results
579645

580646
After calling `cmd.parse()` or `cmd.parse_args()`, you get a `ParseResult` with these typed accessors:
581647

examples/demo.mojo

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""Example: a mini CLI to demonstrate argmojo usage.
22
33
Showcases: positional args, flags, key-value options, choices,
4-
count flags, hidden args, mutually exclusive groups, and
5-
required-together groups.
4+
count flags, hidden args, negatable flags, mutually exclusive groups,
5+
and required-together groups.
66
"""
77

88
from argmojo import Arg, Command
@@ -60,20 +60,21 @@ fn main() raises:
6060
.default("table")
6161
)
6262

63-
# ── Mutually exclusive group ─────────────────────────────────────────
64-
# Only one of --color / --no-color may be used at a time.
63+
# ── Negatable flag ────────────────────────────────────────────────────
64+
# --color enables colour, --no-color disables it.
6565
cmd.add_arg(
66-
Arg("color", help="Force colored output")
66+
Arg("color", help="Enable colored output")
6767
.long("color")
6868
.flag()
69+
.negatable()
6970
)
70-
cmd.add_arg(
71-
Arg("no-color", help="Disable colored output")
72-
.long("no-color")
73-
.flag()
74-
)
75-
var color_group: List[String] = ["color", "no-color"]
76-
cmd.mutually_exclusive(color_group^)
71+
72+
# ── Mutually exclusive group ─────────────────────────────────────────
73+
# Only one of --json / --yaml may be used at a time.
74+
cmd.add_arg(Arg("json", help="Output as JSON").long("json").flag())
75+
cmd.add_arg(Arg("yaml", help="Output as YAML").long("yaml").flag())
76+
var format_excl: List[String] = ["json", "yaml"]
77+
cmd.mutually_exclusive(format_excl^)
7778

7879
# ── Hidden argument (internal / debug) ───────────────────────────────
7980
cmd.add_arg(
@@ -102,7 +103,9 @@ fn main() raises:
102103
print(" --verbose: ", result.get_count("verbose"))
103104
print(" --format: ", result.get_string("format"))
104105
print(" --color: ", result.get_flag("color"))
105-
print(" --no-color: ", result.get_flag("no-color"))
106+
if result.has("json") or result.has("yaml"):
107+
print(" --json: ", result.get_flag("json"))
108+
print(" --yaml: ", result.get_flag("yaml"))
106109
if result.has("max-depth"):
107110
print(" --max-depth: ", result.get_string("max-depth"))
108111
else:

0 commit comments

Comments
 (0)