Skip to content

Commit a3ccfcd

Browse files
authored
[core] Add allow_negative_expressions() method for Command (#48)
This PR adds a new `Command.allow_negative_expressions()` mode to treat dash-prefixed mathematical expressions (and similar tokens) as positional arguments, expanding ArgMojo’s handling of “negative-looking” inputs beyond numeric literals. **Changes:** - Introduce `_allow_negative_expressions` flag + `allow_negative_expressions()` builder on `Command`. - Add parsing logic to consume certain `-...` tokens as positionals in both `parse_arguments()` and `parse_known_arguments()`. - Add documentation and tests describing/validating the new behavior.
1 parent 7798ee7 commit a3ccfcd

File tree

8 files changed

+418
-48
lines changed

8 files changed

+418
-48
lines changed

docs/argmojo_overall_planning.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,8 @@ Input: ["demo", "yuhao", "./src", "--ling", "-i", "--max-depth", "3"]
726726
├─ If args[i].startswith("-") and len > 1:
727727
│ ├─ IF _looks_like_number(token) AND (allow_negative_numbers OR no digit short opts):
728728
│ │ Treat as positional argument (negative number passthrough)
729+
│ ├─ IF allow_negative_expressions AND first char after '-' is not a registered short:
730+
│ │ Treat as positional argument (negative expression passthrough)
729731
│ └─ ELSE:
730732
│ ├─ Single char → _parse_short_single(key, raw_args, i, result) → new i
731733
│ └─ Multi char → _parse_short_merged(key, raw_args, i, result) → new i

docs/changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ This document tracks all notable changes to ArgMojo, including new features, API
44

55
<!--
66
Unreleased changes should be commented out from here. This file will be edited just before each release to reflect the final changelog for that version. Otherwise, the users would be confused.
7+
8+
- Add `allow_negative_expressions()` on `Command` — treats single-hyphen tokens as positional arguments when they don't conflict with registered short options. Handles mathematical expressions like `-1/3*pi`, `-sin(2)`, `-e^2`. Superset of `allow_negative_numbers()`.
79
-->
810

911
## 20260404 (v0.5.0)

docs/declarative_api_planning.md

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ struct Positional[
296296
default: StringLiteral = "",
297297
required: Bool = False,
298298
choices: StringLiteral = "",
299+
# ── Parsing behaviour ──
300+
allow_hyphen: Bool = False, # allow hyphen-prefixed values
299301
# ── Display & help ──
300302
value_name: StringLiteral = "",
301303
group: StringLiteral = "",
@@ -1289,21 +1291,22 @@ var (git_args, result) = MyGit.parse_full_from_command(cmd^)
12891291

12901292
Some features are inherently imperative and don't fit neatly into struct declarations. I'm keeping these builder-only (accessible via `to_command()`):
12911293

1292-
| Feature | Reason |
1293-
| -------------------------- | ------------------------------------------------------------------ |
1294-
| `mutually_exclusive()` | Partially declarative via `conflicts_with` (§6.4); builder for N>2 |
1295-
| `required_together()` | Partially declarative via `depends_on` (§6.4); builder for N>2 |
1296-
| `one_required()` | Cross-field constraint on N args |
1297-
| `required_if()` | Cross-field conditional |
1298-
| `implies()` | Cross-field chain with cycle detection |
1299-
| `confirmation_option()` | Adds a synthetic `--yes` arg |
1300-
| `help_on_no_arguments()` | Command-level behavior |
1301-
| `add_tip()` | Help formatting |
1302-
| Color config | Command-level presentation |
1303-
| Completions config | Command-level behavior |
1304-
| Response file config | Command-level behavior |
1305-
| `allow_negative_numbers()` | Parser behavior flag |
1306-
| `add_parent()` | Cross-command inheritance |
1294+
| Feature | Reason |
1295+
| ------------------------------ | ------------------------------------------------------------------ |
1296+
| `mutually_exclusive()` | Partially declarative via `conflicts_with` (§6.4); builder for N>2 |
1297+
| `required_together()` | Partially declarative via `depends_on` (§6.4); builder for N>2 |
1298+
| `one_required()` | Cross-field constraint on N args |
1299+
| `required_if()` | Cross-field conditional |
1300+
| `implies()` | Cross-field chain with cycle detection |
1301+
| `confirmation_option()` | Adds a synthetic `--yes` arg |
1302+
| `help_on_no_arguments()` | Command-level behavior |
1303+
| `add_tip()` | Help formatting |
1304+
| Color config | Command-level presentation |
1305+
| Completions config | Command-level behavior |
1306+
| Response file config | Command-level behavior |
1307+
| `allow_negative_numbers()` | Parser behavior flag |
1308+
| `allow_negative_expressions()` | Parser behavior flag (superset of allow_negative_numbers) |
1309+
| `add_parent()` | Cross-command inheritance |
13071310

13081311
I think this is the right call — these features describe *relationships between* arguments or *command-level* behavior, not individual argument metadata. Trying to force them into struct field attributes would create a confusing, non-composable API.
13091312

docs/user_manual.md

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,7 @@ Argument("name", help="...")
437437
╠══ Command-level configuration (called on Command) ════════════════════════════
438438
║ command.help_on_no_arguments() show help when invoked with no args
439439
║ command.allow_negative_numbers() negative tokens treated as positionals
440+
║ command.allow_negative_expressions() dash-prefixed expressions as positionals
440441
║ command.allow_positional_with_subcommands() allow positionals + subcommands
441442
║ command.add_tip("...") custom tip shown in help footer
442443
║ command.command_aliases(["co"]) alternate names for this subcommand
@@ -2627,20 +2628,78 @@ calc -3 # operand = "-3" (NOT the -3 flag!)
26272628
26282629
---
26292630

2631+
**Approach 4: `allow_negative_expressions()` (expressions and arbitrary tokens)**
2632+
2633+
When your CLI accepts mathematical expressions that start with `-` (e.g. `-1/3*pi`, `-sin(2)`, `-e^2`), `allow_negative_numbers()` is not enough because those tokens are not pure numeric literals. Call `allow_negative_expressions()` to treat any single-hyphen token as a positional argument, provided it doesn't conflict with a registered short option.
2634+
2635+
```mojo
2636+
var command = Command("calc", "Expression calculator")
2637+
command.allow_negative_expressions()
2638+
command.add_argument(Argument("precision", help="Decimal places").long["precision"]().short["p"]())
2639+
command.add_argument(Argument("expr", help="Expression").positional().required())
2640+
```
2641+
2642+
```shell
2643+
calc "-1/3*pi" -p 10 # expr = "-1/3*pi", precision = "10"
2644+
calc "-sin(2)" # expr = "-sin(2)"
2645+
calc -e # expr = "-e" (because -e is not a registered short option)
2646+
calc -p 10 hello # precision = "10", expr = "hello" (-p IS registered, so it's parsed as the -p short option taking 10 as its value)
2647+
```
2648+
2649+
Rules:
2650+
2651+
- A single-hyphen token is treated as a positional **only when its first character after `-` does not match a registered short option**.
2652+
- Examples: `-1/3*pi`, `-sin(2)`, and `-e` are positional if `-1`, `-s`, and `-e` are not registered short options.
2653+
- If the first character **is** a registered short option, the token is parsed normally (merged shorts like `-vp` and attached values like `-p10` continue to work).
2654+
- Long options (`--foo`) are never affected — they always parse normally.
2655+
2656+
> **Note:** `allow_negative_expressions()` is a superset of `allow_negative_numbers()`. You don't need to call both.
2657+
2658+
---
2659+
26302660
**When to use which approach**
26312661

2632-
| Scenario | Recommended approach |
2633-
| ------------------------------------------------------------------------- | ---------------------------------- |
2634-
| No digit short options registered | Auto-detect (nothing to configure) |
2635-
| You have digit short options (`-3`, `-5`, etc.) and need negative numbers | `allow_negative_numbers()` |
2636-
| You need to pass arbitrary dash-prefixed strings (not just numbers) | `--` separator |
2637-
| Legacy or defensive: works in all cases | `--` separator |
2662+
| Scenario | Recommended approach |
2663+
| ------------------------------------------------------------------------- | ---------------------------------------- |
2664+
| No digit short options registered | Auto-detect (nothing to configure) |
2665+
| You have digit short options (`-3`, `-5`, etc.) and need negative numbers | `allow_negative_numbers()` |
2666+
| You need to pass arbitrary dash-prefixed expressions (e.g. `-1/3*pi`) | `allow_negative_expressions()` |
2667+
| One specific argument needs to accept `-` (stdin) or dash-prefixed values | `allow_hyphen_values()` on that argument |
2668+
| You need to pass arbitrary dash-prefixed strings (not just numbers) | `--` separator |
2669+
| Legacy or defensive: works in all cases | `--` separator |
2670+
2671+
---
2672+
2673+
**`allow_negative_expressions()` vs `allow_hyphen_values()` — how do they relate?**
2674+
2675+
These two features partially overlap, especially when the command has a single positional argument. Here is a quick comparison:
2676+
2677+
| Aspect | `allow_negative_expressions()` | `allow_hyphen_values()` |
2678+
| -------------------------------- | --------------------------------------------------------- | -------------------------------------------------------- |
2679+
| **Scope** | Per-command (one call covers all positionals) | Per-argument (must be set on each argument individually) |
2680+
| **Bare `-` (stdin)** | Accepted (bare `-` never enters short-option parsing) | Accepted |
2681+
| **Expressions (e.g. `-1/3*pi`)** | Accepted | Accepted |
2682+
| **Works on options** | No (positionals only) | Yes (also value-taking options like `--file`) |
2683+
| **Parsing behavior** | Enables dash-prefixed expression handling for positionals | Broadens one argument to accept hyphen-prefixed values |
2684+
2685+
With a **single positional**, the two are nearly interchangeable for inputs like `-1/pi*sin(3)` — and a bare `-` is already treated as a positional token in both cases (it never enters short-option parsing). `allow_hyphen_values()` is still the better fit when one specific argument should accept arbitrary hyphen-prefixed values, especially for value-taking options.
2686+
2687+
Choose `allow_negative_expressions()` when:
2688+
2689+
- Your command has **multiple positionals** and you want a single switch for all of them.
2690+
- You want to signal intent: "this CLI handles math expressions."
2691+
2692+
Choose `allow_hyphen_values()` when:
2693+
2694+
- Only **one specific argument** should accept dash-prefixed values while others should not.
2695+
- You need the bare `-` (stdin/stdout convention).
2696+
- The argument is an **option** (`--file`), not a positional.
26382697

26392698
---
26402699

26412700
**What is NOT a number**
26422701

2643-
Tokens like `-1abc`, `-e5`, or `-1-2` are not valid numeric patterns. They will still be parsed as short-option strings and may raise "Unknown option" errors if unregistered.
2702+
Tokens like `-1abc`, `-e5`, or `-1-2` are not valid numeric patterns. Without `allow_negative_expressions()`, they will still be parsed as short-option strings and may raise "Unknown option" errors if unregistered. With `allow_negative_expressions()`, they are consumed as positional arguments.
26442703

26452704
### Long Option Prefix Matching
26462705

src/argmojo/argument_wrappers.mojo

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,8 @@ struct Positional[
465465
default: StringLiteral = "",
466466
required: Bool = False,
467467
choices: StringLiteral = "",
468+
# -- Parsing behaviour --
469+
allow_hyphen: Bool = False,
468470
# -- Display & help --
469471
value_name: StringLiteral = "",
470472
group: StringLiteral = "",
@@ -482,6 +484,7 @@ struct Positional[
482484
default: Default value as a string literal.
483485
required: If True, the positional must be provided.
484486
choices: Comma-separated allowed values.
487+
allow_hyphen: Allow hyphen-prefixed values.
485488
value_name: Display name in help.
486489
group: Help group heading.
487490
@@ -558,6 +561,8 @@ struct Positional[
558561
comptime if Self.choices != "":
559562
for c in String(Self.choices).split(","):
560563
arg._choice_values.append(String(c))
564+
comptime if Self.allow_hyphen:
565+
arg._allow_hyphen_values = True
561566
comptime if Self.value_name != "":
562567
arg._value_name = String(Self.value_name)
563568
comptime if Self.group != "":

0 commit comments

Comments
 (0)