jorgebucaran/getopts

Proper key for unknown arguments

thiagodp opened this issue · 5 comments

Problem
After parsing arguments with getopts, there is usually some validation to perform. Validation libraries such as joi or ow can validate the entire resulting object. However, that validation gets harder since the resulting object includes all the unknown arguments.

Separating unknown arguments currently requires something like this:

Example:

// example.js
const getopts = require( 'getopts' );
const unexpected = {};
const args = getopts(
    process.argv.splice(2),
    {
        default: { good: 10 },
	unknown: k => unexpected[ k ] = 1
    }
);
// node example.js --bad 20
console.log( args ); // { _: [], good: 10, bad: 20 }
for ( const k of Object.keys( args ) ) {
	if ( unexpected[ k ] ) {
		unexpected[ k ] = args[ k ];
		delete args[ k ];
	}
}
console.log( args ); // { _: [], good: 10 }
console.log( unexpected ); // { bad: 20 }

Proposed Solution
Use a key such as __ (double underscore) for unknown arguments.

Example:

// example.js
const getopts = require( 'getopts' );
const args = getopts(
    process.argv.splice(2),
    {
        default: { good: 10 }
    }
);
// node example.js --bad 20
console.log( args ); // { _: [], __: { bad: 20 }, good: 10 }

@thiagodp However, that validation gets harder since the resulting object includes all the unknown arguments.

There's a way to discard unknown arguments easily:

From the unknown docs:

Return false to discard the option.

 getopts(["--bad", "--worse"], {
  default: { good: 10 },
  unknown: (key) => key !== "bad" && key !== "worse",
}) // => { _: [], good: 10 }

@jorgebucaran The intent is not discarding them but putting them in a certain key inside the resulting object. The keys and values are still needed for validation, i.e., showing them or suggesting similar arguments (like Git does for instance).

Thus, I suggest to reopen the issue.

@thiagodp The keys and values are still needed for validation, i.e., showing them or suggesting similar arguments (like Git does for instance).

Here's a minimal autosuggest HOF you can use with getopts that relies on the unknown property.

const findBestMatch = (key, known) => known.find((k) => k.match(key))

export const autosuggest = (parse) => (args, opts) => {
  const known = Object.keys(opts.alias).reduce(
    (a, k) => a.concat(k, opts.alias[k]),
    []
  )

  return parse(args, {
    opts,
    unknown: (key) => {
      if (known.includes(key)) return true
      console.log(
        `error: Invalid key "${key}" ~ did you mean "${findBestMatch(key, known)}"?`
      )
      process.exit(1)
    },
  })
}

Then import it and use it like this:

import { autosuggest } from "./utils"

const argv = ["--max", "-s", 50, "-h"]

const options = autosuggest(getopts)(argv, {
  alias: {
    maxRandomSize: ["max-random-size", "s"],
    help: "h",
  }
})
error: Invalid key "max" ~ did you mean "maxRandomSize"?

What do you think?

@jorgebucaran I'm understanding that you want to keep this library to a minimum set of features. Thank you for your time.

(BTW, an auto-suggesting feature should be able to detect spelling mistakes, so using Levenshtein distance is probably a better idea)

@thiagodp Well, yes, that's my position as the "guardian" of the project, but you have the potential to convince me to change the API, add a new feature, etc. I'd like to know how you feel about my autosuggest HOF, and if you think it's manageable.

(BTW, an auto-suggesting feature should be able to detect spelling mistakes, so using Levenshtein distance is probably a better idea)

Absolutely! 👍