Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/argmojo_overall_planning.md
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,8 @@ Input: ["demo", "yuhao", "./src", "--ling", "-i", "--max-depth", "3"]
├─ If args[i].startswith("-") and len > 1:
│ ├─ IF _looks_like_number(token) AND (allow_negative_numbers OR no digit short opts):
│ │ Treat as positional argument (negative number passthrough)
│ ├─ IF allow_negative_expressions AND first char after '-' is not a registered short:
│ │ Treat as positional argument (negative expression passthrough)
│ └─ ELSE:
│ ├─ Single char → _parse_short_single(key, raw_args, i, result) → new i
│ └─ Multi char → _parse_short_merged(key, raw_args, i, result) → new i
Expand Down
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ This document tracks all notable changes to ArgMojo, including new features, API

<!--
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.

- 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()`.
-->

## 20260404 (v0.5.0)
Expand Down
33 changes: 18 additions & 15 deletions docs/declarative_api_planning.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ struct Positional[
default: StringLiteral = "",
required: Bool = False,
choices: StringLiteral = "",
# ── Parsing behaviour ──
allow_hyphen: Bool = False, # allow hyphen-prefixed values
# ── Display & help ──
value_name: StringLiteral = "",
group: StringLiteral = "",
Expand Down Expand Up @@ -1289,21 +1291,22 @@ var (git_args, result) = MyGit.parse_full_from_command(cmd^)

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

| Feature | Reason |
| -------------------------- | ------------------------------------------------------------------ |
| `mutually_exclusive()` | Partially declarative via `conflicts_with` (§6.4); builder for N>2 |
| `required_together()` | Partially declarative via `depends_on` (§6.4); builder for N>2 |
| `one_required()` | Cross-field constraint on N args |
| `required_if()` | Cross-field conditional |
| `implies()` | Cross-field chain with cycle detection |
| `confirmation_option()` | Adds a synthetic `--yes` arg |
| `help_on_no_arguments()` | Command-level behavior |
| `add_tip()` | Help formatting |
| Color config | Command-level presentation |
| Completions config | Command-level behavior |
| Response file config | Command-level behavior |
| `allow_negative_numbers()` | Parser behavior flag |
| `add_parent()` | Cross-command inheritance |
| Feature | Reason |
| ------------------------------ | ------------------------------------------------------------------ |
| `mutually_exclusive()` | Partially declarative via `conflicts_with` (§6.4); builder for N>2 |
| `required_together()` | Partially declarative via `depends_on` (§6.4); builder for N>2 |
| `one_required()` | Cross-field constraint on N args |
| `required_if()` | Cross-field conditional |
| `implies()` | Cross-field chain with cycle detection |
| `confirmation_option()` | Adds a synthetic `--yes` arg |
| `help_on_no_arguments()` | Command-level behavior |
| `add_tip()` | Help formatting |
| Color config | Command-level presentation |
| Completions config | Command-level behavior |
| Response file config | Command-level behavior |
| `allow_negative_numbers()` | Parser behavior flag |
| `allow_negative_expressions()` | Parser behavior flag (superset of allow_negative_numbers) |
| `add_parent()` | Cross-command inheritance |

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.

Expand Down
73 changes: 66 additions & 7 deletions docs/user_manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ Argument("name", help="...")
╠══ Command-level configuration (called on Command) ════════════════════════════
║ command.help_on_no_arguments() show help when invoked with no args
║ command.allow_negative_numbers() negative tokens treated as positionals
║ command.allow_negative_expressions() dash-prefixed expressions as positionals
║ command.allow_positional_with_subcommands() allow positionals + subcommands
║ command.add_tip("...") custom tip shown in help footer
║ command.command_aliases(["co"]) alternate names for this subcommand
Expand Down Expand Up @@ -2627,20 +2628,78 @@ calc -3 # operand = "-3" (NOT the -3 flag!)

---

**Approach 4: `allow_negative_expressions()` (expressions and arbitrary tokens)**

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.

```mojo
var command = Command("calc", "Expression calculator")
command.allow_negative_expressions()
command.add_argument(Argument("precision", help="Decimal places").long["precision"]().short["p"]())
command.add_argument(Argument("expr", help="Expression").positional().required())
```

```shell
calc "-1/3*pi" -p 10 # expr = "-1/3*pi", precision = "10"
calc "-sin(2)" # expr = "-sin(2)"
calc -e # expr = "-e" (because -e is not a registered short option)
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)
```

Rules:

- A single-hyphen token is treated as a positional **only when its first character after `-` does not match a registered short option**.
- Examples: `-1/3*pi`, `-sin(2)`, and `-e` are positional if `-1`, `-s`, and `-e` are not registered short options.
- 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).
- Long options (`--foo`) are never affected — they always parse normally.

> **Note:** `allow_negative_expressions()` is a superset of `allow_negative_numbers()`. You don't need to call both.

---

**When to use which approach**

| Scenario | Recommended approach |
| ------------------------------------------------------------------------- | ---------------------------------- |
| No digit short options registered | Auto-detect (nothing to configure) |
| You have digit short options (`-3`, `-5`, etc.) and need negative numbers | `allow_negative_numbers()` |
| You need to pass arbitrary dash-prefixed strings (not just numbers) | `--` separator |
| Legacy or defensive: works in all cases | `--` separator |
| Scenario | Recommended approach |
| ------------------------------------------------------------------------- | ---------------------------------------- |
| No digit short options registered | Auto-detect (nothing to configure) |
| You have digit short options (`-3`, `-5`, etc.) and need negative numbers | `allow_negative_numbers()` |
| You need to pass arbitrary dash-prefixed expressions (e.g. `-1/3*pi`) | `allow_negative_expressions()` |
| One specific argument needs to accept `-` (stdin) or dash-prefixed values | `allow_hyphen_values()` on that argument |
| You need to pass arbitrary dash-prefixed strings (not just numbers) | `--` separator |
| Legacy or defensive: works in all cases | `--` separator |

---

**`allow_negative_expressions()` vs `allow_hyphen_values()` — how do they relate?**

These two features partially overlap, especially when the command has a single positional argument. Here is a quick comparison:

| Aspect | `allow_negative_expressions()` | `allow_hyphen_values()` |
| -------------------------------- | --------------------------------------------------------- | -------------------------------------------------------- |
| **Scope** | Per-command (one call covers all positionals) | Per-argument (must be set on each argument individually) |
| **Bare `-` (stdin)** | Accepted (bare `-` never enters short-option parsing) | Accepted |
| **Expressions (e.g. `-1/3*pi`)** | Accepted | Accepted |
| **Works on options** | No (positionals only) | Yes (also value-taking options like `--file`) |
| **Parsing behavior** | Enables dash-prefixed expression handling for positionals | Broadens one argument to accept hyphen-prefixed values |

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.

Choose `allow_negative_expressions()` when:

- Your command has **multiple positionals** and you want a single switch for all of them.
- You want to signal intent: "this CLI handles math expressions."

Choose `allow_hyphen_values()` when:

- Only **one specific argument** should accept dash-prefixed values while others should not.
- You need the bare `-` (stdin/stdout convention).
- The argument is an **option** (`--file`), not a positional.

---

**What is NOT a number**

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.
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.

### Long Option Prefix Matching

Expand Down
5 changes: 5 additions & 0 deletions src/argmojo/argument_wrappers.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,8 @@ struct Positional[
default: StringLiteral = "",
required: Bool = False,
choices: StringLiteral = "",
# -- Parsing behaviour --
allow_hyphen: Bool = False,
# -- Display & help --
value_name: StringLiteral = "",
group: StringLiteral = "",
Expand All @@ -482,6 +484,7 @@ struct Positional[
default: Default value as a string literal.
required: If True, the positional must be provided.
choices: Comma-separated allowed values.
allow_hyphen: Allow hyphen-prefixed values.
value_name: Display name in help.
group: Help group heading.

Expand Down Expand Up @@ -558,6 +561,8 @@ struct Positional[
comptime if Self.choices != "":
for c in String(Self.choices).split(","):
arg._choice_values.append(String(c))
comptime if Self.allow_hyphen:
arg._allow_hyphen_values = True
comptime if Self.value_name != "":
arg._value_name = String(Self.value_name)
comptime if Self.group != "":
Expand Down
Loading
Loading