@davidwells/dx-args
A forgiving argv parser for CLIs that should keep working when someone types a slightly imperfect command.
@davidwells/dx-args keeps oparser as the value parser. It adds argv normalization, file/glob grouping, single-dash long option recovery, and a predictable result shape around it.
const { dxParse } = require('@davidwells/dx-args')
const result = dxParse(['-files', 'README.md', '-dry'])
console.log(result.mergedOptions)
// { files: 'README.md', dry: true }
console.log(result.globGroups)
// [{ key: 'files', rawKey: '-files', values: ['README.md'] }]
Install
npm install @davidwells/dx-args
API
const {
dxParse,
getGlobGroupsFromArgs,
splitOutsideQuotes,
} = require('@davidwells/dx-args')
dxParse(argv, opts)
Parses an argv array and returns a ParseResult.
const result = dxParse(['--stage', 'prod', 'cool', '=', 'true'])
console.log(result.mergedOptions)
// { stage: 'prod', cool: true }
getGlobGroupsFromArgs(argv, opts)
Extracts file and glob-like values into grouped collections.
const result = getGlobGroupsFromArgs(
['--files', 'README.md', 'docs/**/*.md'],
{ globKeys: ['files'] }
)
console.log(result.globGroups)
// [{ key: 'files', rawKey: '--files', values: ['README.md', 'docs/**/*.md'] }]
splitOutsideQuotes(input)
Splits a normalized option string while preserving quoted strings, objects, arrays, and key = value forms.
splitOutsideQuotes('name = "David Wells" config={ enabled: true }')
// ['name="David Wells"', 'config={ enabled: true }']
Accepted Input Forms
| Input | mergedOptions |
|---|---|
--stage prod |
{ stage: 'prod' } |
--stage=prod |
{ stage: 'prod' } |
stage = prod |
{ stage: 'prod' } |
stage=prod |
{ stage: 'prod' } |
-stage prod |
{ stage: 'prod' } |
-f README.md |
{ f: 'README.md' } |
--no-cache |
{ cache: false } |
--count 3 |
{ count: 3 } |
--items=[1,2,3] |
{ items: [1, 2, 3] } |
File and glob-like values are also exposed through globGroups.
const result = dxParse(['README.md', 'docs/**/*.md', '--stage', 'dev'])
console.log(result.globGroups)
// [{ key: '', rawKey: '', values: ['README.md', 'docs/**/*.md'] }]
console.log(result.mergedOptions)
// { stage: 'dev' }
Result Shape
interface GlobGroup {
key: string
rawKey: string
values: Array<string | RegExp>
}
interface ParseResult {
rawArgv: string
leadingCommands: string[]
globGroups: GlobGroup[]
extraParse: Record<string, unknown>
mriOptionsOriginal: Record<string, unknown>
mriOptionsClean: Record<string, unknown>
mriDiff: boolean
yargsParsed: string
mergedOptions: Record<string, unknown>
}
Most callers should use:
mergedOptionsfor parsed options.globGroupsfor file and glob-aware workflows.leadingCommandswhen a CLI has command-like leading words before options.
The remaining fields are diagnostic:
extraParseis theoparser-derived parse result after argv normalization.mriOptionsOriginalis the rawmriresult.mriOptionsCleanismriafter cleanup of artifacts such as letters from-stage.mriDiffis true when cleanup changed themriresult.yargsParsedis not active; it is currently a compatibility/debug field.
Glob Groups
@davidwells/dx-args groups file-looking and glob-looking arguments so CLIs can handle files separately from ordinary options.
dxParse(['--files', 'README.md', 'docs/**/*.md']).globGroups
// [{ key: 'files', rawKey: '--files', values: ['README.md', 'docs/**/*.md'] }]
Bare file and glob values use an empty key:
dxParse(['README.md', 'docs/**/*.md']).globGroups
// [{ key: '', rawKey: '', values: ['README.md', 'docs/**/*.md'] }]
Custom glob keys are configurable:
dxParse(['--ignore', 'dist/**/*.md'], {
globKeys: ['files', 'file', 'path', 'ignore']
}).globGroups
// [{ key: 'ignore', rawKey: '--ignore', values: ['dist/**/*.md'] }]
Shell-expanded file lists are intentionally not treated as leading commands:
const result = dxParse(['README.md', 'NOTES.md', 'build', '=', 'false'])
console.log(result.globGroups)
// [{ key: '', rawKey: '', values: ['README.md', 'NOTES.md'] }]
console.log(result.mergedOptions)
// { build: false }
Values containing node_modules/ are currently filtered from glob groups.
Duplicate Values
Duplicate flags are last-write-wins by default.
dxParse(['--stage', 'dev', '--stage', 'prod']).mergedOptions
// { stage: 'prod' }
Accumulation is explicit:
dxParse(['--tag', 'one', '--tag', 'two'], {
accumulate: ['tag']
}).mergedOptions
// { tag: ['one', 'two'] }
The parser currently accepts three option names for accumulation:
accumulateaccumulateFlagsarrayKeys
Prefer accumulate in new code.
Single Dash Policy
Multi-character single-dash options are treated as forgiving long options:
dxParse(['-stage', 'prod']).mergedOptions
// { stage: 'prod' }
That also works when the value looks like a file:
dxParse(['-config', 'md.config.js']).mergedOptions
// { config: 'md.config.js' }
Separate short flags still work:
dxParse(['-l', '-a', '-h']).mergedOptions
// { l: true, a: true, h: true }
Short clusters are opt-in because they conflict with forgiving long option recovery:
dxParse(['-abc']).mergedOptions
// { abc: true }
dxParse(['-abc'], {
allowShortClusters: true,
shortFlags: ['a', 'b', 'c']
}).mergedOptions
// { a: true, b: true, c: true }
Markdown Magic Integration
Markdown Magic uses dxParse as its CLI parse boundary.
Markdown Magic maps parser output like this:
file,files,path, and bare file groups becomeconfig.files.ignoregroups becomeconfig.ignore.- Other
mergedOptionsbecome Markdown Magic config overrides. outputandoutputDirnormalization happens in Markdown Magic, not@davidwells/dx-args.
Example Markdown Magic commands:
md-magic -files README.md -config md.config.js -dry
md-magic --path "docs/**/*.md" --ignore "dist/**/*.md"
Debugging
The package includes a small CLI that prints the full parse result:
dx-args -files README.md -config md.config.js -dry
When debugging, check:
mergedOptionsglobGroupsmriOptionsOriginalmriOptionsCleanmriDiff
Known Gaps
- Space-separated list capture like
--numbers 1 2 3 --strings a b cis not supported yet. - Real shell argv with quotes should be tested as arrays, not only string-split fixtures.
- Diagnostic fields may move under a
debugortracefield before a stable1.0.0. node_modules/filtering is currently built into glob grouping.--rest passthrough should be specified and tested before it is documented as public API.