### Problem Statement(s) 1. There's a difference between an option/flag being declared & it's definition 2. "options" nomenclature was overloaded 3. Inconsistant API (ex. arguments accepted, & values returned, could be a String|Array) 4. Throw if arguments passed don't pass validation/parsing (ie. exception handling) ### Success Criteria - npm version 9 will adopt this for parsing CLI arguments - npm version 8 can warn/polyfill to deprecate CLI arg parsing that would differ from this - jackspeak is reduced to merely a set of argument requirements, types, and validation Key requirement: easier than `for (const arg of process.mainArgs) { switch (arg) { ... } }` ### Add `process.mainArgs` This can be done even if the rest of it changes completely or is abandoned, it is unanimously desired by all process.argv parsers right now. Please add it asap. Implementation: ```javascript process.mainArgs = process.argv.slice(process._exec ? 1 : 2) ``` ```javascript #!/usr/bin/env node // foo.js console.log(process.mainArgs) ``` ``` # process.argv.slice(2) node foo.js a b c // [ 'a', 'b', 'c' ] # process.argv.slice(1) node -p 'process.mainArgs' a b c // ['a', 'b', 'c'] ``` Done, do it, ship it yesterday. ### Isaac's List Of ~~Annoying~~ Important Questions - Is `cmd --foo=bar baz` the same as `cmd baz --foo=bar`? - Yes, if `withValue: ['foo']`, otherwise no - Does the parser execute a function? - no - Does the parser execute one of several functions, depending on input? - no - Can subcommands take options that are distinct from the main command? - no (this might be a problem? at least it's a more definitive "opinion") - Does it output generated help when no options match? - no - Does it generated short usage? Like: `usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]` - no (no usage/help at all) - Does the user provide the long usage text? For each option? For the whole command? - no - Do subcommands (if implemented) have their own usage output? - no - Does usage print if the user runs `cmd --help`? - no - Does it set `process.exitCode`? - no - Does usage print to stderr or stdout? - N/A - Does it check types? (Say, specify that an option is a boolean, number, etc.) - no - Can an option have more than one type? (string or false, for example) - no - Can the user define a type? (Say, `type: path` to call `path.resolve()` on the argument.) - no - Does a `--foo=0o22` mean 0, 22, 18, or "0o22"? - `"0o22"` - Does it coerce types? - no - Does `--no-foo` coerce to `--foo=false`? For all flags? Only boolean flags? - no, it sets `{args:{'no-foo': true}}` - Is `--foo` the same as `--foo=true`? Only for known booleans? Only at the end? - no, `--foo` is the same as `--foo=` - Does it read environment variables? Ie, is `FOO=1 cmd` the same as `cmd --foo=1`? - no - Do unknown arguments raise an error? Are they parsed? Are they treated as positional arguments? - no, they are parsed, not treated as positionals - Does `--` signal the end of flags/options? - **open question** - If `--` signals the end, is `--` included as a positional? is `program -- foo` the same as `program foo`? Are both `{positionals:['foo']}`, or is the first one `{positionals:['--', 'foo']}`? - Does the API specify whether a `--` was present/relevant? - no - Is `-foo` the same as `--foo`? - yes <-- ! kind of a blocker for shortopts ! - Recommend: "No, -foo is shortopts form of --f --o --o" (assuming none are defined, or withValues) - Is `---foo` the same as `--foo`? - yes (?!) - other options: 1. `---foo` is a key of `-foo` 2. this is a positional 3. throw (we can't parse it) - This gets ridiculous: `node prog.js --------------------------------foo`? - Is `-` a positional? ie, `bash some-test.sh | tap -` - ?? - Any support for posix-style shortopts? How are they defined? - **open question** - Is `-F` the same as `--F`? - yes - Is `-AB` the same as `-A -B`? (Ever? Sometimes? Always?) - no, never - Is `-AB` the same as `-BA`? (Ever? Sometimes? Always?) - no, never - Is `-Bvalue` the same as `-B value`? (Ever? Sometimes? Always?) - `rm -rf` - `ls -laF` - `tap -bcRspec` -> `tap --bail --color --reporter=spec` - `tap -bxc` === `tap -xbc`, order is irrelevant - `tap -bxc` -> `tap --bail --x --color` - in other words: ```javascript parseArgs(['-bxc'], { short: { // this should throw if a key has length >1? b: 'bail', c: 'color', }, }) { args: { 'bail': true, 'x': true, 'color': true, }, values: { bail: [undefined], x: [undefined], color: [undefined], }, } ``` - pseudocode: - position = 0 - while position < string.length - if (string[position] is a shortopt) - add expanded form - if it takes a value - value is string.slice(position), break - break - else - position++ - continue - else - treat it as if it was `{short: { x: 'x' }}`, ie, expand as if it was `--bail --x` - value is "", key is the single char - `args.x = true` - - Is `-Bvalue` the same as `-B=value`? (Ever? Sometimes? Always?) - Any support for "set this multiple times for more thingness"? Eg, `foo -v` for some verbosity, `foo -vvvv` for lots of verbosity? - **open question** - yes, if shortopts - `{ args, values } = parseArgs(['-vvvv'], { short: { v: 'verbose' }, multiples: ['verbose']})` `const { values: { verbose: { length: verbosity }}} = { values: { 'verbose': [undefined, undefined, undefined, undefined ] }}`, I would look at `values.verbose.length` for level of verbosity. `args.verbose = true` Suggestion: Add a strict mode to throw if unknown args are encountered ```javascript const result = parseArgs(args, { strict: true }) ``` ### Potential Solution(s) 1. Split up the existence of an argument from it's value 2. Change names & corresponding documentation 3. Standardize on Arrays 4. Don't coerce values so that there is nothing to validate ```js const argv = ['--foo=a', '--', '--foo=b', '--bar=baz', '--', 'xyz'] const { args, values, positionals } = parseArgs(argv) expect({ args: { foo: true, }, values: { foo: ['a'], }, positionals: [ '--foo=b', '--bar=baz', '--', 'xyz' ] }) npm test --loglevel=silly -- --snapshot=true const argv = ['--foo=a', 'b', '--foo', 'c'] const { args, values, positionals } = parseArgs(argv) args // { foo: true } values // { foo: [''] } const argv = ['a', '--foo'] const { args, values, positionals } = parseArgs(argv, { withValue: ['foo'] }) // '--foo' === '--foo=' unless followed by positional and withValue:['foo'] const argv = ['--foo', 'a', 'b', '--foo', 'c'] const { args, values, positionals } = parseArgs(argv, { withValue: ['foo'] }) args // { foo: true } values // { foo: ['c'] } positionals // [] const argv = ['--foo', 'a', '--foo', 'b'] const { args, values, positionals } = parseArgs(argv, { withValue: ['foo'], multiples: ['foo'] }) args // { foo: true } values // { foo: ['a', 'b'] } positionals // [] const argv = ['--foo', 'a', 'b', '--foo', 'c'] const { args, values, positionals } = parseArgs(argv, { withValue: ['foo'], multiples: ['foo'] }) args // { foo: true, bar: true } values // { foo: ['a', 'c'] } positionals // ['b'] // node.js --a --b --c const { args, values, positionals } = util.parseArgs() args { a: true, b: true, c: true } values { a: undefined, b: undefined, c: undefined } positionals [] // if process._exec, default is process.argv.slice(1) node -p 'require("util").parseArgs()' foo --bar=baz print: { args: { bar: true, }, values: { bar: ['baz'] }, positionals: [ 'foo' ] } node program.js <top-level opts> subcommand <subcommand opts> ``` ```js // script.js // called via `node script.js --foo` const argv = util.parseArgs() argv.foo // true <- because it's the defaut value // called via `node script.js --foo=true` argv.foo // true <- Boolean: because we defined it // called via `node script.js --foo=hello` argv.foo // 'hello' <- String: because we defined it // called via `node script.js` argv.foo // ... undefined? // current const { options, positionals } = util.parseArgs() // new idea... const { args, values, positionals } = util.parseArgs() args // declarations values // definitions // -------------------------------------------------- //const { parseArgs } = require('util') // -------------------------------------------------- const argv = ['--foo=false', '-foo=true'] // new const { args, values, positionals } = parseArgs(argv, { multiples: ['foo']}) args.foo // true // return boolean values.foo // ['false', 'true'] // return Array positionals // Array // old const { options, positionals } = parseArgs(argv) options.foo // false <- do logic... // -------------------------------------------------- const argv = ['--foo=true'] const { args, values } = parseArgs(argv) args.foo // true values.foo // 'true' // -------------------------------------------------- const argv = ['--foo=bar', '--foo', 'baz'] const { args, values, positionals } = parseArgs(argv) args.foo // true values.foo // 'bar' positonals // ['baz'] // -------------------------------------------------- const argv = ['--foo=bar', '--foo', 'baz'] const { args, values, positionals } = parseArgs(argv) args.foo // true values.foo // 'bar' positionals // ['baz'] // -------------------------------------------------- const argv = ['--foo=bar', '--foo', 'baz'] const { values } = parseArgs(argv, { withValue: ['foo'] }) values.foo // 'baz' // -------------------------------------------------- const argv = ['--foo=bar', '--foo', 'baz'] const { values } = parseArgs(argv, { multiples: ['foo'] }) values.foo // ['bar', undefined] // -------------------------------------------------- const argv = ['--file=bar', '--foo', 'baz'] const { values } = parseArgs(argv, { withValue: ['foo'], multiples: ['foo'] }) values.foo // ['bar', 'baz'] // -------------------------------------------------- const argv = ['--foo=bar', '--foo', 'baz'] const { values } = parseArgs(argv, { withValue: ['foo'], multiples: ['foo'] }) values.foo // ['bar', 'baz'] // -------------------------------------------------- // Example of the 99%... const argv = ['--foo'] const { args } = parseArgs(argv) args.foo // true args.bar // undefined // -------------------------------------------------- npm install express graphql /// ```