Skip to content

fix: skip parent run() when subcommand executes, support = in alias values - #265

Open
marceli1404 wants to merge 1 commit into
unjs:mainfrom
marceli1404:fix/subcommand-and-alias-parsing
Open

fix: skip parent run() when subcommand executes, support = in alias values#265
marceli1404 wants to merge 1 commit into
unjs:mainfrom
marceli1404:fix/subcommand-and-alias-parsing

Conversation

@marceli1404

@marceli1404 marceli1404 commented Jul 29, 2026

Copy link
Copy Markdown

Fixes

#253 - Parent command's run() executes when subcommand runs

When a subcommand is explicitly provided and executed, the parent command's run() function was still being called. This caused both the subcommand and parent to execute their run() logic.

Fix: Added a subCommandExecuted flag that is set to true when a subcommand is found and executed. The parent's run() is now gated behind !subCommandExecuted.

#237 - Alias values with = separator (e.g. -n=John)

The native Node.js parseArgs includes the = in the value when a single-dash alias uses = syntax (e.g., -n=John is parsed as n: "=John").

Fix: Added preprocessing in parseRawArgs that splits single-dash args on the first = into separate tokens ([-n, John]) before passing them to Node.js parseArgs.

Summary by CodeRabbit

  • New Features

    • Added support for single-dash options with values, such as -x=value, treating them consistently with option aliases.
  • Bug Fixes

    • Prevented a parent command from running when a subcommand is selected, avoiding unintended duplicate command execution.
    • Confirmed previously unsupported alias-value syntax now works as expected.
  • Tests

    • Added coverage ensuring only the selected subcommand runs when applicable.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The parser now supports -x=value alias syntax by splitting it into option and value tokens. Subcommand execution tracks dispatch state so the parent command’s run() callback is skipped, with tests covering both behaviors.

Changes

Argument parsing

Layer / File(s) Summary
Split aliased argument values
src/_parser.ts, test/parser.test.ts
Single-dash arguments containing = are split into separate option and value tokens, and the corresponding alias test is treated as passing.

Subcommand execution

Layer / File(s) Summary
Gate parent command execution
src/command.ts, test/main.test.ts
Subcommand dispatch records execution before recursive invocation, preventing the parent command’s run() callback from running while the subcommand callback runs once.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • unjs/citty#231: Both changes adjust subcommand dispatch and argument handling around distinguishing flags, values, and subcommands.
  • unjs/citty#236: Both changes modify src/command.ts subcommand resolution and execution flow.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes both main fixes: skipping parent run() for subcommands and supporting = syntax in single-dash alias values.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/main.test.ts (1)

146-167: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for default subcommand dispatch.

This test verifies the explicit ["foo"] path, but src/command.ts also changed the default-subcommand path. Add a case with default: "foo" and no positional subcommand to confirm the parent run() remains skipped and the default child runs once.

Suggested test coverage
+  it("does not run parent command's run() when the default subcommand is executed", async () => {
+    const parentRunMock = vi.fn();
+    const subRunMock = vi.fn();
+
+    const command = defineCommand({
+      default: "foo",
+      subCommands: {
+        foo: { run: async () => subRunMock() },
+      },
+      run: async () => parentRunMock(),
+    });
+
+    await runMain(command, { rawArgs: [] });
+
+    expect(parentRunMock).not.toHaveBeenCalled();
+    expect(subRunMock).toHaveBeenCalledOnce();
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/main.test.ts` around lines 146 - 167, Extend the test around
defineCommand and runMain to cover default subcommand dispatch by configuring
subCommands.foo as the default, invoking runMain without a positional
subcommand, and asserting parentRunMock is not called while subRunMock is called
once. Keep the existing explicit ["foo"] coverage unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/_parser.ts`:
- Around line 127-132: Restrict the single-dash equals-sign preprocessing in the
argument-processing flow to tokens whose alias portion is exactly one character
and is present in the configured aliases. Preserve the original argument for
unrecognized or multi-character forms, including hyphen-prefixed values and
boolean forms such as -v=false, while retaining splitting for recognized
-x=value aliases.

---

Nitpick comments:
In `@test/main.test.ts`:
- Around line 146-167: Extend the test around defineCommand and runMain to cover
default subcommand dispatch by configuring subCommands.foo as the default,
invoking runMain without a positional subcommand, and asserting parentRunMock is
not called while subRunMock is called once. Keep the existing explicit ["foo"]
coverage unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 04f32a58-7d15-4803-9dbb-0a965a97d1c7

📥 Commits

Reviewing files that changed from the base of the PR and between 03a9e6d and e1b2f2f.

📒 Files selected for processing (4)
  • src/_parser.ts
  • src/command.ts
  • test/main.test.ts
  • test/parser.test.ts

Comment thread src/_parser.ts
Comment on lines +127 to +132
// Handle -x=value (single-dash alias with = separator)
if (arg.startsWith("-") && !arg.startsWith("--") && arg.includes("=")) {
const eqIndex = arg.indexOf("=");
processedArgs.push(arg.slice(0, eqIndex), arg.slice(eqIndex + 1));
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restrict preprocessing to recognized one-character aliases.

This condition rewrites every single-dash token containing =, not only -x=value aliases. That regresses valid hyphen-prefixed string values such as ["--name", "-test=value"], which are split before parseArgs can consume them as one value. It can also misparse boolean forms such as -v=false.

Gate this transformation on a one-character, configured alias; otherwise preserve the original argument.

Proposed fix
+    const eqIndex = arg.indexOf("=");
-    if (arg.startsWith("-") && !arg.startsWith("--") && arg.includes("=")) {
-      const eqIndex = arg.indexOf("=");
+    const alias = eqIndex > 0 ? arg.slice(1, eqIndex) : "";
+    if (
+      arg.startsWith("-") &&
+      !arg.startsWith("--") &&
+      eqIndex === 2 &&
+      aliasToMain.has(alias)
+    ) {
      processedArgs.push(arg.slice(0, eqIndex), arg.slice(eqIndex + 1));
      continue;
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Handle -x=value (single-dash alias with = separator)
if (arg.startsWith("-") && !arg.startsWith("--") && arg.includes("=")) {
const eqIndex = arg.indexOf("=");
processedArgs.push(arg.slice(0, eqIndex), arg.slice(eqIndex + 1));
continue;
}
// Handle -x=value (single-dash alias with = separator)
const eqIndex = arg.indexOf("=");
const alias = eqIndex > 0 ? arg.slice(1, eqIndex) : "";
if (
arg.startsWith("-") &&
!arg.startsWith("--") &&
eqIndex === 2 &&
aliasToMain.has(alias)
) {
processedArgs.push(arg.slice(0, eqIndex), arg.slice(eqIndex + 1));
continue;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/_parser.ts` around lines 127 - 132, Restrict the single-dash equals-sign
preprocessing in the argument-processing flow to tokens whose alias portion is
exactly one character and is present in the configured aliases. Preserve the
original argument for unrecognized or multi-character forms, including
hyphen-prefixed values and boolean forms such as -v=false, while retaining
splitting for recognized -x=value aliases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant