-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Behaviour for zero config --foo=a ? #24
Comments
@shadowspawn are you thinking that |
I don't think this is a good idea. We should be keeping as few assumptions out of this implementation as possible & force the user to define when/if values are to be captured & returned or not (which puts them into a better overall experience). What I think is happening here, & is concerning, is that we've changed the meaning of Looking back (ref. https://github.com/pkgjs/parseargs/blob/3f057c1b158a1bdbe878c64b57460c58e56e465f/README.md), Looking at the most recent version of the spec (2022/01/16): // unconfigured
const argv = ['-f', '--foo=a', '--bar', 'b'];
const options = {};
const { flags, values, positionals } = parseArgs(argv, options);
// flags = { f: true, bar: true }
// values = { foo: 'a' }
// positionals = ['b'] I now need to do two different lookups to find the existence of an argument, & we've created an arbitrary delineation of two "types" of arguments (ie. ones with & without values). Worse yet, afaict, there's no way to determine existence, vs. value, when using // unconfigured
const argv = ['-f', '--foo=a', '--bar', 'b'];
const options = {};
const { flags, values, positionals } = parseArgs(argv, options);
const fooExists = (flags.foo || Object.keys(values).foo)
// `withValue`
const argv = ['-f', '--foo=a', '--bar', 'b'];
const options = {
withValue: [ 'foo' ]
};
const { flags, values, positionals } = parseArgs(argv, options);
const fooExists = (flags.foo || Object.keys(values).foo) // always true Previously, I could write the following no matter what the options... const { args, values, positionals } = parseArgs(argv, options);
const fooExists = !!args.foo Either way, my knee-jerk reaction here is that if we assume parsing of
|
I was a driver for that, sorry. The intended usage and potential benefits were not apparent to me (#13). I am not sure there is more information with the original implementation, but I'll work through your example carefully before commenting further. Thanks for the detail, and I look forward to discussing the issues raised!
|
My original thought was that const { flags, values, positionals } = parseArgs(['--foo=a']);
// flags = { foo: true }
// values = { }
// positionals = []
Both. The ease of use & understanding Take these two examples: // node program.js --foo=
parseArgs().flags.foo // undefined
parseArgs().values.foo // undefined
// node program.js --foo
parseArgs().flags.foo // true
parseArgs().values.foo // undefined This feels like non-deterministic behavior as in both cases I've set a flag but only changed how I've declared it's value. If I were able to rely on Example: quick & dirty...// node program.js --debug # or...
// node program.js --debug="Some debugger prefix"
if (parseArgs().flags.debug) {
const prefix = parseArgs().values.debug || 'Debugger'
console.log(`${prefix}:`, process)
} Example: more fully-baked validation & aliasing...const { flags, values } = parseArgs({ withValue: ['org', 'force'] })
const settings = {
org: {
alias: 'b',
usage: 'run this program with `--org <name>`',
required: true,
validate: Object.toString
},
force: {
alias: 'f',
usage: 'run this program with `--force` to force some action',
default: false,
validate: (value) => {
if (~['true','false'].indexOf(value.toLowerCase())) {
return Boolean(value)
}
throw Error('Type validation error!')
}
}
}
Object.entries(settings).map(([flag, option]) => {
const isDefined = flags[arg] || flags[option.alias]
const value = values[flag] || values[option.alias]
if (!isDefined && option.required)
throw Error('Required arguments are missing', option.usage)
if (defined && option.validate)
values[flag] = option.validate(value)
}) On "zero config"...I'm honestly not that opposed to parsing Changes & explicit rules we might want to consider if we go this route...
Examples of this in practice...Notably, these all include/respect the idea that we'd return the existence of flags again in // no value
parseArgs(['--foo='])
/*
{
flags: { foo: true },
values { foo: undefined },
positionals: []
}
*/
// key=value
parseArgs(['--foo=a'])
/*
{
flags: { foo: true },
values { foo: 'a' },
positionals: []
}
*/
// multiple flags, no options
parseArgs(['--foo=a', '--foo'])
/*
{
flags: { foo: true },
values { foo: undefined },
positionals: []
}
*/
// multiple flags, with `multiples` option
parseArgs(['--foo=a', '--foo'], { multiples: [ 'foo' ] })
/*
{
flags: { foo: true },
values { foo: [ 'a', undefined ] },
positionals: []
}
*/
// positional, no options
parseArgs(['--foo=', 'bar'])
/*
{
flags: { foo: true },
values { foo: 'a' },
positionals: ['bar']
}
*/
// positional, with `acceptsPositionalValues` option
parseArgs(['--foo=', 'bar'], { acceptsPositionalValues: ['foo'] })
/*
{
flags: { foo: true },
values { foo: 'bar' },
positionals: []
}
*/ |
A quick note that I think (Early morning here. Hopefully I will have time tonight for some in depth replies.) |
No worries. Take your time. I know I wrote a lot (apologies; I'm playing catch-up for not having time over the last ~3-6months)
We might just have to agree to diasgree 🤷🏼♂️ (I noted as much in our other thread). I don't feel super strongly about it though unless that decision effects if/how we For me, my expectation is that
let foo=; // is an error
let foo; // is undefined
let foo=''; // is an empty string |
For normal command-line use, there is parsing going on from shell script before node or client code every see the args. I think this makes the distinction between
|
With that in mind, I'd think that |
|
I'll pick up this comment first because I think it highlights some key points of confusion or contention. I won't write an essay about every two sentences! And epic exposition in later comment #24 (comment) , thanks @darcyclarke , I'm going to read through that multiple times before commenting on it.
There are different user affordances provided by either approach. To be clear, I am not against your original intentions now you have explained it. From experience with Commander and reading about Yargs and Minimist, I expected a single result object rather than a pair and came up with a different pattern to try and justify the pair: an explicit delineation of two types of arguments, the ones with and without values. I actually thought this might have been part of the original intent, some sort of type safety, one holds booleans and one holds strings in the normal case. I was way wrong about that intent. :-) Let's assume for a moment we stick with the current suggestion, misguided as it may be. I don't think there is loss of information or difficulty in use. As the author, I know what I am expecting. If I want to check a boolean flag has been used, I simply check If I am testing for the existence of an argument with or without a value, I do need to check two values. But this does not seem like a happy-path case.
I don't think I understand. If |
(Treating
My concern with that approach when I came across it in the implementation was it has thrown away information. This seems inconsistent with "bring your own validation". |
I should say explicitly that I am not heavily invested in the current mutually exclusive I am fine with the Part of the point of my refactor in PR #26 was to make it easy to adjust the behaviour, and easy to reuse the behaviour when adding short options. |
This may be a side-track, but caught my eye. Example from #24 (comment) Example: quick & dirty... // node program.js --debug # or...
// node program.js --debug="Some debugger prefix"
if (parseArgs().flags.debug) {
const prefix = parseArgs().values.debug || 'Debugger'
console.log(`${prefix}:`, process)
} Alarm! Alarm! When I reread this, are you wanting to routinely support options which may or may not have a value supplied on the command line? If that is the intent, I have a longer story about why I think options with optional values should be avoided as a concept in a simple parser. (In Commander they are explicitly supported, so I have some experience. They seem best of both worlds, a boolean flag or an option with a value, but lead to end-user confusion about parsing outcomes. I admit they are easy to implement with You may have just used this as an example of conveniently coping with an error case, in which case disregard this alarm! You did say:
|
If we do parse the value, I still see
and to (potentially) make this a detectable error:
It is appealing that taking a value from My problem is I don't think silently parsing it as a boolean flag and discarding the value is appropriate. (Not least because of possible exposure to programs using Minimist and Yargs, and erroneous use of Hmm, a suggestion from most recent Tooling meeting was to pass-through unrecognised uses. Maybe this qualifies, so this could be parsed as a positional? A bit wild and would be ambiguous with uses of Maybe this is a good case for |
This is a nice example of serious intent! #24 (comment) I think the aliases need to be passed to The two lines that change are the first two lines (from example, with aliases removed):
Arguably it would be better for lone With XOR flags/values semantics:
To implement these with a combined options object with true for boolean flags, like Commander or Yargs:
|
I found that all worthwhile, but should I just change my refactor PR to the seen/value semantics and we run with that again as the original vision? (And separately improve the documentation so gentle readers understand the intent.) (And probably change the name of (And I am not a fan of storing |
Ok, back on topic for a fresh look informed by discussions. I asked what the behaviour should be for zero config
Early feedback: Ben and Jordan and I ok with (1). |
I think if not 1, we’d want 4. |
I think the 1st option is the best default to implement |
Option 1 is consistent with my new story for high level approach. Looks like a flag, stored like a flag. Looks like a value, stored like a value. |
In other words, the way they're stored matches the intention of the user, not the configurer, which will ensure the configurer can most accurately respond to the user's intentions. |
(Yes indeed. Nicely put @ljharb, thanks.) |
Here is a run through of my proposed behaviours for an option used with zero-config. The option is implicitly a boolean flag and not expecting a value. The naming and output I have used here is based on the "existence" pattern per: #38 (comment)
Remember, only the first usage is consistent with a flag which does not take a value. The other two are misuse which can be detected by the client if desired.
|
I’m confused; with zero-config, why would foo ever give true if there’s an equals sign present? |
Because I am using the convention from Darcy's initial spec that one of the results holds whether an option was used on the command line at all, either as a flag or with a value. So in this example the |
That makes sense, thanks - so we have "options", "values", and "flags"? |
Those are the terms I am using, yes. (Or are you asking if |
I'm asking if those are the three potential properties of that object. |
The returned object will have just two of those properties, depending on which we implement:
(I'm being very careful as I am not sure I understand your question and not sure if yes or no is the answer! Ask again if my answer is still not useful, we might be at cross-purposes.) |
If I configure one flag, one value, and three arguments are passed, I would expect "options" to contain all three, "flags" to contain one or two, and "values" to contain one or two. No? |
Short version: no, or at least it depends on the arguments. (If you give me an example call, I'll lay out the two result flavours I have in mind.) |
|
First clarification, there is currently no ability to define flags. (Which is a blocker for Second, I'll reply with a worked example, but have run out of time to make sure I get it right. Will answer later. |
If there were such an ability, then would my expectations match? ie, |
Long answer. In this comment:
// hypothetical `flags` and `strict` configuration
console.log(parseArgs(['--foo', '--bar', '--baz'], { flags: 'foo', withValue: ['bar'], strict: false })); foo: configured as a flag and used as a flag "options" and "values" (aka "existence")
"flags" and "values" (xor, option appears in one or the other)
Side note: I am suggesting we do not store |
That also means "needing to wonder about |
Yes indeed, (I started out working through examples testing for explicit Leaving aside Have a look at the uses cases I run through in #38 to see if I missed something important. |
I suggest the intent of the user is clear, and the value should be stored?
Added TL;DR proposal: #24 (comment)
The initial README did not clarify what happens with
--foo=a
with no configuration, as the result was overwritten by repeated option--foo b
.I assumed the value would be stored in update to README in #23, which has landed.
The current implementation ignores the value unless
withValue
is set forfoo
.The text was updated successfully, but these errors were encountered: