Skip to content

fix: fixup parsing negative numbers and quoted strings - #6250

Merged
mark-wiemer merged 10 commits into
mainfrom
mark-wiemer-cli-parser-regression
Aug 31, 2026
Merged

mark-wiemer merged 10 commits into
mainfrom
mark-wiemer-cli-parser-regression

Conversation

@mark-wiemer

@mark-wiemer mark-wiemer commented Aug 28, 2026 •

Copy link
Copy Markdown
Member

PR Checklist

Overview

We naively split args on single spaces earlier, which ignored cases like quoted strings, tabs, and more.

We also rejected negative numbers and dash-prefixed option values (--grep -foo is rejected in 12 RC 6), where Mocha 11 does not reject these cases.

This PR adds a splitArgs helper to handle these cases better and the same as Mocha 11.

@mark-wiemer mark-wiemer changed the title Fixup parsing split arguments fix: fixup parsing split arguments Aug 28, 2026
@codecov

codecov Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.27586% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 82.12%. Comparing base (505b5e9) to head (bf03330).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
lib/cli/parse-args.js 98.27% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6250      +/-   ##
==========================================
+ Coverage   81.93%   82.12%   +0.18%     
==========================================
  Files          64       64              
  Lines        4866     4922      +56     
  Branches     1100     1086      -14     
==========================================
+ Hits         3987     4042      +55     
- Misses        879      880       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mark-wiemer

mark-wiemer commented Aug 29, 2026 •

Copy link
Copy Markdown
Member Author

Without this fix, one new test is broken, and 11 others fail. Skipping the broken should allow negative numeric values in MOCHA_OPTIONS, we get:

  1219 passing (6s)
  4 pending
  11 failing

  1) options
       loadOptions()
         env options
           should parse quoted flags from MOCHA_OPTIONS:
     UnexpectedError: 
expected
{
  _: [ 'bar\'' ], timeout: 1000, extension: [ 'js' ], grep: '\'foo',
  color: true, config: false, package: false
}
to satisfy { grep: 'foo bar', color: true }

{
  _: [ 'bar\'' ],
  timeout: 1000,
  extension: [ 'js' ],
  grep: '\'foo', // should equal 'foo bar'
                 //
                 // 'foo
                 // foo bar
  color: true,
  config: false,
  package: false
}

      at Context.<anonymous> (file:///.../test/node-unit/cli/options.spec.js:486:9)
      at callFn (lib/runnable.js:360:23)
      at Test.run (lib/runnable.js:348:7)
      at Runner.runTest (lib/runner.cjs:809:10)
      at fn (lib/runner.cjs:959:12)
      at next (lib/runner.cjs:724:14)
      at fn (lib/runner.cjs:734:7)
      at next (lib/runner.cjs:595:14)
      at Immediate._onImmediate (lib/runner.cjs:702:5)
      at process.processImmediate (node:internal/timers:485:21)
      set UNEXPECTED_FULL_TRACE=true to see the full stack trace

  2) parse-args
       accepts negative numeric option values in separated form:
     Error: Not enough arguments following: timeout
      at <anonymous> (lib/cli/parse-args.js:202:13)
      at Array.forEach (<anonymous>)
      at allArgs (lib/cli/parse-args.js:193:11)
      at parseMochaArgs (lib/cli/parse-args.js:293:3)
      at Context.<anonymous> (file:///.../test/node-unit/cli/parse-args.spec.js:243:12)
      at callFn (lib/runnable.js:360:23)
      at Test.run (lib/runnable.js:348:7)
      at Runner.runTest (lib/runner.cjs:809:10)
      at fn (lib/runner.cjs:959:12)
      at next (lib/runner.cjs:724:14)
      at fn (lib/runner.cjs:734:7)
      at next (lib/runner.cjs:595:14)
      at Immediate._onImmediate (lib/runner.cjs:702:5)
      at process.processImmediate (node:internal/timers:485:21)

  3) parse-args
       accepts negative numeric option values from strings:
     Error: Not enough arguments following: timeout
      at <anonymous> (lib/cli/parse-args.js:202:13)
      at Array.forEach (<anonymous>)
      at allArgs (lib/cli/parse-args.js:193:11)
      at parseMochaArgs (lib/cli/parse-args.js:293:3)
      at Context.<anonymous> (file:///.../test/node-unit/cli/parse-args.spec.js:249:12)
      at callFn (lib/runnable.js:360:23)
      at Test.run (lib/runnable.js:348:7)
      at Runner.runTest (lib/runner.cjs:809:10)
      at fn (lib/runner.cjs:959:12)
      at next (lib/runner.cjs:724:14)
      at fn (lib/runner.cjs:734:7)
      at next (lib/runner.cjs:595:14)
      at Immediate._onImmediate (lib/runner.cjs:702:5)
      at process.processImmediate (node:internal/timers:485:21)

  4) parse-args
       accepts decimal and exponential negative numeric option values:
     Error: Not enough arguments following: timeout
      at <anonymous> (lib/cli/parse-args.js:202:13)
      at Array.forEach (<anonymous>)
      at allArgs (lib/cli/parse-args.js:193:11)
      at parseMochaArgs (lib/cli/parse-args.js:293:3)
      at file:///.../test/node-unit/cli/parse-args.spec.js:256:14
      at Array.forEach (<anonymous>)
      at Context.<anonymous> (file:///.../test/node-unit/cli/parse-args.spec.js:255:29)
      at callFn (lib/runnable.js:360:23)
      at Test.run (lib/runnable.js:348:7)
      at Runner.runTest (lib/runner.cjs:809:10)
      at fn (lib/runner.cjs:959:12)
      at next (lib/runner.cjs:724:14)
      at fn (lib/runner.cjs:734:7)
      at next (lib/runner.cjs:595:14)
      at Immediate._onImmediate (lib/runner.cjs:702:5)
      at process.processImmediate (node:internal/timers:485:21)

  5) parse-args
       parses double-quoted arguments from strings:
     UnexpectedError: 
expected
{
  _: [ 'bar"' ], timeout: 1000, extension: [ 'js' ], grep: '"foo', retries: 3,
  color: true
}
to satisfy { grep: 'foo bar', retries: 3, color: true }

{
  _: [ 'bar"' ],
  timeout: 1000,
  extension: [ 'js' ],
  grep: '"foo', // should equal 'foo bar'
                //
                // "foo
                // foo bar
  retries: 3,
  color: true
}

      at Context.<anonymous> (file:///.../test/node-unit/cli/parse-args.spec.js:263:5)
      at callFn (lib/runnable.js:360:23)
      at Test.run (lib/runnable.js:348:7)
      at Runner.runTest (lib/runner.cjs:809:10)
      at fn (lib/runner.cjs:959:12)
      at next (lib/runner.cjs:724:14)
      at fn (lib/runner.cjs:734:7)
      at next (lib/runner.cjs:595:14)
      at Immediate._onImmediate (lib/runner.cjs:702:5)
      at process.processImmediate (node:internal/timers:485:21)
      set UNEXPECTED_FULL_TRACE=true to see the full stack trace

  6) parse-args
       parses single-quoted arguments from strings:
     UnexpectedError: 
expected { _: [ 'bar\'' ], timeout: 1000, extension: [ 'js' ], grep: '\'foo', color: true }
to satisfy { grep: 'foo bar', color: true }

{
  _: [ 'bar\'' ],
  timeout: 1000,
  extension: [ 'js' ],
  grep: '\'foo', // should equal 'foo bar'
                 //
                 // 'foo
                 // foo bar
  color: true
}

      at Context.<anonymous> (file:///.../test/node-unit/cli/parse-args.spec.js:275:5)
      at callFn (lib/runnable.js:360:23)
      at Test.run (lib/runnable.js:348:7)
      at Runner.runTest (lib/runner.cjs:809:10)
      at fn (lib/runner.cjs:959:12)
      at next (lib/runner.cjs:724:14)
      at fn (lib/runner.cjs:734:7)
      at next (lib/runner.cjs:595:14)
      at Immediate._onImmediate (lib/runner.cjs:702:5)
      at process.processImmediate (node:internal/timers:485:21)
      set UNEXPECTED_FULL_TRACE=true to see the full stack trace

  7) parse-args
       parses quoted values attached to options from strings:
     UnexpectedError: 
expected { _: [ 'bar"' ], timeout: 1000, extension: [ 'js' ], grep: '"foo' }
to satisfy { grep: 'foo bar' }

{
  _: [ 'bar"' ],
  timeout: 1000,
  extension: [ 'js' ],
  grep: '"foo' // should equal 'foo bar'
               //
               // "foo
               // foo bar
}

      at Context.<anonymous> (file:///.../test/node-unit/cli/parse-args.spec.js:286:5)
      at callFn (lib/runnable.js:360:23)
      at Test.run (lib/runnable.js:348:7)
      at Runner.runTest (lib/runner.cjs:809:10)
      at fn (lib/runner.cjs:959:12)
      at next (lib/runner.cjs:724:14)
      at fn (lib/runner.cjs:734:7)
      at next (lib/runner.cjs:595:14)
      at Immediate._onImmediate (lib/runner.cjs:702:5)
      at process.processImmediate (node:internal/timers:485:21)
      set UNEXPECTED_FULL_TRACE=true to see the full stack trace

  8) parse-args
       preserves empty quoted option values from strings:
     UnexpectedError: 
expected { _: [], timeout: 1000, extension: [ 'js' ], grep: '""' } to satisfy { grep: '' }

{
  _: [],
  timeout: 1000,
  extension: [ 'js' ],
  grep: '""' // should equal ''
             //
             // ""
}

      at Context.<anonymous> (file:///.../test/node-unit/cli/parse-args.spec.js:292:5)
      at callFn (lib/runnable.js:360:23)
      at Test.run (lib/runnable.js:348:7)
      at Runner.runTest (lib/runner.cjs:809:10)
      at fn (lib/runner.cjs:959:12)
      at next (lib/runner.cjs:724:14)
      at fn (lib/runner.cjs:734:7)
      at next (lib/runner.cjs:595:14)
      at Immediate._onImmediate (lib/runner.cjs:702:5)
      at process.processImmediate (node:internal/timers:485:21)
      set UNEXPECTED_FULL_TRACE=true to see the full stack trace

  9) parse-args
       parses escaped spaces and quotes from strings:
     UnexpectedError: 
expected
{
  _: [ 'bar', '\\"quote\\""' ],
  timeout: 1000,
  extension: [ 'js' ],
  grep: 'foo\\',
  fgrep: '"a'
}
to satisfy { grep: 'foo bar', fgrep: 'a "quote"' }

{
  _: [ 'bar', '\\"quote\\""' ],
  timeout: 1000,
  extension: [ 'js' ],
  grep: 'foo\\', // should equal 'foo bar'
                 //
                 // foo\
                 // foo bar
  fgrep: '"a' // should equal 'a "quote"'
              //
              // "a
              // a "quote"
}

      at Context.<anonymous> (file:///.../test/node-unit/cli/parse-args.spec.js:298:5)
      at callFn (lib/runnable.js:360:23)
      at Test.run (lib/runnable.js:348:7)
      at Runner.runTest (lib/runner.cjs:809:10)
      at fn (lib/runner.cjs:959:12)
      at next (lib/runner.cjs:724:14)
      at fn (lib/runner.cjs:734:7)
      at next (lib/runner.cjs:595:14)
      at Immediate._onImmediate (lib/runner.cjs:702:5)
      at process.processImmediate (node:internal/timers:485:21)
      set UNEXPECTED_FULL_TRACE=true to see the full stack trace

  10) parse-args
       splits arguments on all whitespace:
     UnexpectedError: 
expected { _: [ '\t--grep\nfoo\r\n--color', '', '' ], timeout: 1000, extension: [ 'js' ] }
to satisfy { grep: 'foo', color: true }

{
  _: [ '\t--grep\nfoo\r\n--color', '', '' ],
  timeout: 1000,
  extension: [ 'js' ]
  // missing grep: 'foo'
  // missing color: true
}

      at Context.<anonymous> (file:///.../test/node-unit/cli/parse-args.spec.js:315:5)
      at callFn (lib/runnable.js:360:23)
      at Test.run (lib/runnable.js:348:7)
      at Runner.runTest (lib/runner.cjs:809:10)
      at fn (lib/runner.cjs:959:12)
      at next (lib/runner.cjs:724:14)
      at fn (lib/runner.cjs:734:7)
      at next (lib/runner.cjs:595:14)
      at Immediate._onImmediate (lib/runner.cjs:702:5)
      at process.processImmediate (node:internal/timers:485:21)
      set UNEXPECTED_FULL_TRACE=true to see the full stack trace

  11) parse-args
       rejects unterminated quoted arguments:
     UnexpectedError: 
expected () => parseMochaArgs('--grep "foo', defaults)
to throw /Unterminated quote in arguments/
  expected function to throw
    did not throw

      at Context.<anonymous> (file:///.../test/node-unit/cli/parse-args.spec.js:326:5)
      at callFn (lib/runnable.js:360:23)
      at Test.run (lib/runnable.js:348:7)
      at Runner.runTest (lib/runner.cjs:809:10)
      at fn (lib/runner.cjs:959:12)
      at next (lib/runner.cjs:724:14)
      at fn (lib/runner.cjs:734:7)
      at next (lib/runner.cjs:595:14)
      at Immediate._onImmediate (lib/runner.cjs:702:5)
      at process.processImmediate (node:internal/timers:485:21)
      set UNEXPECTED_FULL_TRACE=true to see the full stack trace

Comment thread test/node-unit/cli/options.spec.js
Comment thread test/node-unit/cli/options.spec.js
Comment thread test/node-unit/cli/parse-args.spec.js Outdated
Comment thread test/node-unit/cli/parse-args.spec.js Outdated
Comment thread test/node-unit/cli/parse-args.spec.js Outdated
Comment thread test/node-unit/cli/options.spec.js
@mark-wiemer
mark-wiemer marked this pull request as ready for review August 29, 2026 20:41
@mark-wiemer mark-wiemer changed the title fix: fixup parsing split arguments fix: fixup parsing negative numbers and quoted strings Aug 29, 2026

@JoshuaKGoldberg JoshuaKGoldberg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This does seem like an unfortunate amount of code to take internally rather than just use something like yargs-parser 🙃. But yargs-parser is big and I couldn't find another parser that's (a) well-maintained (b) not big (c) handles the cases we need. Ah well.

LGTM! ✅

@mark-wiemer
mark-wiemer merged commit 2962875 into main Aug 31, 2026
377 of 379 checks passed
@mark-wiemer
mark-wiemer deleted the mark-wiemer-cli-parser-regression branch August 31, 2026 13:43
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.

🐛 Bug: Mocha 12 RC 6 CLI parser fails to handle negative numbers and quoted flags

2 participants