Skip to content

Commit 6e4d491

Browse files
authored
[core] Implement command aliases (#5)
This PR implements command aliases for the ArgMojo CLI argument parsing library. It also renames several API methods for consistency (`parse_args()` → `parse_arguments()`, `help_on_no_args()` → `help_on_no_arguments()`, `.nargs()` → `.number_of_values()`, `nargs_count` field → `num_values`).
1 parent 9243e2e commit 6e4d491

17 files changed

+834
-373
lines changed

docs/argmojo_overall_planning.md

Lines changed: 50 additions & 32 deletions
Large diffs are not rendered by default.

docs/changelog.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ ArgMojo v0.2.0 is compatible with Mojo v0.26.1.
1414
1. Auto-register a `help` subcommand so that `app help <command>` works out of the box; opt out with `disable_help_subcommand()`.
1515
1. Add `allow_positional_with_subcommands()` guard — prevents accidental mixing of positional args and subcommands on the same `Command`, following the cobra/clap convention. Requires explicit opt-in.
1616
1. Add `subcommand` and `subcommand_result` fields on `ParseResult` with `has_subcommand_result()` / `get_subcommand_result()` accessors.
17+
1. Add `command_aliases()` builder method for subcommand short names (e.g., `clone``cl`). Aliases dispatch to the canonical subcommand, appear in help output, shell completions, and typo suggestions.
1718

1819
**Persistent flags:**
1920

@@ -102,7 +103,7 @@ ArgMojo v0.1.0 is compatible with Mojo v0.26.1.
102103

103104
1. Append / collect action — `--tag x --tag y` collects repeated options into a list with `.append()`.
104105
1. Value delimiter — `--env dev,staging,prod` splits by delimiter into a list with `.delimiter(",")`.
105-
1. Multi-value options (nargs) — `--point 10 20` consumes N consecutive values with `.nargs(N)`.
106+
1. Multi-value options (nargs) — `--point 10 20` consumes N consecutive values with `.number_of_values(N)`.
106107
1. Key-value map option — `--define key=value` builds a `Dict` with `.key_value()`.
107108

108109
**Help & display:**

docs/user_manual.md

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,14 @@ fn main() raises:
8787

8888
---
8989

90-
**`parse()` vs `parse_args()`**
90+
**`parse()` vs `parse_arguments()`**
9191

9292
- **`command.parse()`** reads the real command-line via `sys.argv()`.
93-
- **`command.parse_args(args)`** accepts a `List[String]` — useful for testing without a real binary. Note that `args[0]` is expected to be the program name and will be skipped, so the actual arguments should start from index 1.
93+
- **`command.parse_arguments(args)`** accepts a `List[String]` — useful for testing without a real binary. Note that `args[0]` is expected to be the program name and will be skipped, so the actual arguments should start from index 1.
9494

9595
### Reading Parsed Results
9696

97-
After calling `command.parse()` or `command.parse_args()`, you get a `ParseResult` with these typed accessors:
97+
After calling `command.parse()` or `command.parse_arguments()`, you get a `ParseResult` with these typed accessors:
9898

9999
| Method | Returns | Description |
100100
| --------------------------- | -------------- | ------------------------------------------------- |
@@ -659,14 +659,14 @@ This is similar to Python argparse's `nargs=N` and Rust clap's `num_args`.
659659

660660
**Defining a multi-value option**
661661

662-
Use `.nargs(N)` to specify how many values the option consumes:
662+
Use `.number_of_values(N)` to specify how many values the option consumes:
663663

664664
```mojo
665-
command.add_argument(Argument("point", help="X Y coordinates").long("point").nargs(2))
666-
command.add_argument(Argument("rgb", help="RGB colour").long("rgb").short("c").nargs(3))
665+
command.add_argument(Argument("point", help="X Y coordinates").long("point").number_of_values(2))
666+
command.add_argument(Argument("rgb", help="RGB colour").long("rgb").short("c").number_of_values(3))
667667
```
668668

669-
`.nargs(N)` automatically implies `.append()` — values are stored in
669+
`.number_of_values(N)` automatically implies `.append()` — values are stored in
670670
`ParseResult.lists` and retrieved with `get_list()`.
671671

672672
---
@@ -720,7 +720,7 @@ Choices are validated for **each** value individually:
720720
```mojo
721721
var dirs: List[String] = ["north", "south", "east", "west"]
722722
command.add_argument(
723-
Argument("route", help="Start and end").long("route").nargs(2).choices(dirs^)
723+
Argument("route", help="Start and end").long("route").number_of_values(2).choices(dirs^)
724724
)
725725
```
726726

@@ -1490,6 +1490,37 @@ app.disable_help_subcommand()
14901490

14911491
This can be called before or after `add_subcommand()`. If called after, the auto-added `help` entry is removed.
14921492

1493+
### Subcommand Aliases
1494+
1495+
You can register short aliases for subcommands with `command_aliases()`. When the user types an alias, ArgMojo dispatches to the canonical subcommand and stores the **canonical name** (not the alias) in `result.subcommand`.
1496+
1497+
```mojo
1498+
var clone = Command("clone", "Clone a repository")
1499+
var aliases: List[String] = ["cl"]
1500+
clone.command_aliases(aliases^)
1501+
app.add_subcommand(clone^)
1502+
```
1503+
1504+
```bash
1505+
app cl https://example.com/repo.git # dispatches to "clone"
1506+
app clone https://example.com/repo.git # still works
1507+
```
1508+
1509+
```mojo
1510+
var result = app.parse()
1511+
print(result.subcommand) # always "clone", even if user typed "cl"
1512+
```
1513+
1514+
Aliases appear in help output alongside the primary name:
1515+
1516+
```
1517+
Commands:
1518+
clone, cl Clone a repository
1519+
commit, ci Record changes to the repository
1520+
```
1521+
1522+
Aliases are also included in shell-completion scripts and typo suggestions.
1523+
14931524
### Unknown Subcommand Error
14941525

14951526
When the root command has subcommands registered **and `allow_positional_with_subcommands()` has not been called**, an unrecognised token triggers an error listing available commands:
@@ -1531,7 +1562,7 @@ app.add_argument(Argument("fallback", help="Fallback").positional())
15311562
15321563
# "foo" doesn't match any subcommand → treated as positional
15331564
var args: List[String] = ["app", "foo"]
1534-
var result = app.parse_args(args)
1565+
var result = app.parse_arguments(args)
15351566
print(result.positionals[0]) # "foo"
15361567
```
15371568

@@ -1759,13 +1790,13 @@ After printing help, the program exits cleanly with exit code 0.
17591790

17601791
**Show Help When No Arguments Provided**
17611792

1762-
Use `help_on_no_args()` to automatically display help when the user invokes
1793+
Use `help_on_no_arguments()` to automatically display help when the user invokes
17631794
the command with no arguments (like `git`, `docker`, or `cargo`):
17641795

17651796
```mojo
17661797
var command = Command("myapp", "My application")
17671798
command.add_argument(Argument("file", help="Input file").long("file").required())
1768-
command.help_on_no_args()
1799+
command.help_on_no_arguments()
17691800
var result = command.parse()
17701801
```
17711802

examples/mgit.mojo

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ fn main() raises:
9393
.long("recurse-submodules")
9494
.flag()
9595
)
96-
clone.help_on_no_args()
96+
clone.help_on_no_arguments()
97+
var clone_aliases: List[String] = ["cl"]
98+
clone.command_aliases(clone_aliases^)
9799
app.add_subcommand(clone^)
98100

99101
# ── init ─────────────────────────────────────────────────────────────
@@ -184,6 +186,8 @@ fn main() raises:
184186
.long("cleanup-mode")
185187
.deprecated("Use --cleanup instead")
186188
)
189+
var commit_aliases: List[String] = ["ci"]
190+
commit.command_aliases(commit_aliases^)
187191
app.add_subcommand(commit^)
188192

189193
# ── push ─────────────────────────────────────────────────────────────
@@ -320,14 +324,14 @@ fn main() raises:
320324
.short("f")
321325
.flag()
322326
)
323-
remote_add.help_on_no_args()
327+
remote_add.help_on_no_arguments()
324328
remote.add_subcommand(remote_add^)
325329

326330
var remote_remove = Command("remove", "Remove a remote")
327331
remote_remove.add_argument(
328332
Argument("name", help="Remote name to remove").positional().required()
329333
)
330-
remote_remove.help_on_no_args()
334+
remote_remove.help_on_no_arguments()
331335
remote.add_subcommand(remote_remove^)
332336

333337
var remote_rename = Command("rename", "Rename a remote")
@@ -337,21 +341,23 @@ fn main() raises:
337341
remote_rename.add_argument(
338342
Argument("new", help="New remote name").positional().required()
339343
)
340-
remote_rename.help_on_no_args()
344+
remote_rename.help_on_no_arguments()
341345
remote.add_subcommand(remote_rename^)
342346

343347
var remote_show = Command("show", "Show information about a remote")
344348
remote_show.add_argument(
345349
Argument("name", help="Remote name").positional().required()
346350
)
347-
remote_show.help_on_no_args()
351+
remote_show.help_on_no_arguments()
348352
remote.add_subcommand(remote_show^)
349353

350-
remote.help_on_no_args()
354+
remote.help_on_no_arguments()
351355
app.add_subcommand(remote^)
352356

353357
# ── branch ───────────────────────────────────────────────────────────
354358
var branch = Command("branch", "List, create, or delete branches")
359+
var branch_aliases: List[String] = ["br"]
360+
branch.command_aliases(branch_aliases^)
355361
branch.add_argument(Argument("name", help="Branch name").positional())
356362
branch.add_argument(
357363
Argument("delete", help="Delete a branch")
@@ -381,6 +387,8 @@ fn main() raises:
381387

382388
# ── diff ─────────────────────────────────────────────────────────────
383389
var diff = Command("diff", "Show changes between commits, trees, etc.")
390+
var diff_aliases: List[String] = ["di"]
391+
diff.command_aliases(diff_aliases^)
384392
diff.add_argument(Argument("path", help="Path to diff").positional())
385393
diff.add_argument(
386394
Argument("staged", help="Show staged changes").long("staged").flag()
@@ -449,6 +457,8 @@ fn main() raises:
449457

450458
# ── stash ────────────────────────────────────────────────────────────
451459
var stash = Command("stash", "Stash changes in working directory")
460+
var stash_aliases: List[String] = ["st"]
461+
stash.command_aliases(stash_aliases^)
452462
stash.add_argument(
453463
Argument("stash-message", help="Stash message")
454464
.long("message")
@@ -469,7 +479,7 @@ fn main() raises:
469479
app.add_subcommand(stash^)
470480

471481
# ── Show help when invoked with no arguments ─────────────────────────
472-
app.help_on_no_args()
482+
app.help_on_no_arguments()
473483

474484
# ── Parse & display ──────────────────────────────────────────────────
475485
var result = app.parse()

examples/mgrep.mojo

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ fn main() raises:
148148
Argument("context", help="Print B lines before and A lines after match")
149149
.long("context")
150150
.short("C")
151-
.nargs(2)
151+
.number_of_values(2)
152152
.metavar("N")
153153
)
154154

@@ -242,7 +242,7 @@ fn main() raises:
242242
app.add_tip('Use quotes for patterns with spaces: grep "fn main" ./src')
243243

244244
# ── Show help when invoked with no arguments ─────────────────────────
245-
app.help_on_no_args()
245+
app.help_on_no_arguments()
246246

247247
# ── Parse & display ──────────────────────────────────────────────────
248248
var result = app.parse()

src/argmojo/argument.mojo

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ struct Argument(Copyable, Movable, Stringable, Writable):
4444
_ = Argument("env", help="...").long("env").delimiter(",")
4545
4646
# Multi-value (--point 1 2 → ["1","2"]) → result.get_list("point")
47-
_ = Argument("point", help="...").long("point").nargs(2)
47+
_ = Argument("point", help="...").long("point").number_of_values(2)
4848
4949
# Numeric range validation → result.get_int("port")
5050
_ = Argument("port", help="...").long("port").range(1, 65535)
@@ -96,7 +96,7 @@ struct Argument(Copyable, Movable, Stringable, Writable):
9696
"""If True, repeated uses collect values into a list (e.g., --tag x --tag y)."""
9797
var delimiter_char: String
9898
"""If non-empty, each value is split by this delimiter into multiple list entries."""
99-
var nargs_count: Int
99+
var num_values: Int
100100
"""Number of values to consume per occurrence (0 means single-value mode)."""
101101
var range_min: Int
102102
"""Minimum allowed value (inclusive) for numeric range validation."""
@@ -143,7 +143,7 @@ struct Argument(Copyable, Movable, Stringable, Writable):
143143
self.is_negatable = False
144144
self.is_append = False
145145
self.delimiter_char = ""
146-
self.nargs_count = 0
146+
self.num_values = 0
147147
self.range_min = 0
148148
self.range_max = 0
149149
self.has_range = False
@@ -176,7 +176,7 @@ struct Argument(Copyable, Movable, Stringable, Writable):
176176
self.is_negatable = copy.is_negatable
177177
self.is_append = copy.is_append
178178
self.delimiter_char = copy.delimiter_char
179-
self.nargs_count = copy.nargs_count
179+
self.num_values = copy.num_values
180180
self.range_min = copy.range_min
181181
self.range_max = copy.range_max
182182
self.has_range = copy.has_range
@@ -209,7 +209,7 @@ struct Argument(Copyable, Movable, Stringable, Writable):
209209
self.is_negatable = move.is_negatable
210210
self.is_append = move.is_append
211211
self.delimiter_char = move.delimiter_char^
212-
self.nargs_count = move.nargs_count
212+
self.num_values = move.num_values
213213
self.range_min = move.range_min
214214
self.range_max = move.range_max
215215
self.has_range = move.has_range
@@ -404,11 +404,11 @@ struct Argument(Copyable, Movable, Stringable, Writable):
404404
self.is_append = True
405405
return self^
406406

407-
fn nargs(var self, n: Int) -> Self:
407+
fn number_of_values(var self, n: Int) -> Self:
408408
"""Sets the number of values consumed per occurrence.
409409
410410
When set, each use of the option consumes exactly ``n``
411-
consecutive arguments. For example, ``.nargs(2)`` on
411+
consecutive arguments. For example, ``.number_of_values(2)`` on
412412
``--point`` causes ``--point 1 2`` to collect ``["1", "2"]``.
413413
Implies ``.append()`` so values are stored in
414414
``ParseResult.lists``.
@@ -417,9 +417,9 @@ struct Argument(Copyable, Movable, Stringable, Writable):
417417
n: Number of values to consume (must be ≥ 2).
418418
419419
Returns:
420-
Self with nargs and append mode set.
420+
Self with num_values and append mode set.
421421
"""
422-
self.nargs_count = n
422+
self.num_values = n
423423
self.is_append = True
424424
return self^
425425

0 commit comments

Comments
 (0)