Skip to content

Commit 8b7475a

Browse files
committed
Detect options that need a value but didn't get one.
1 parent b940e06 commit 8b7475a

2 files changed

Lines changed: 63 additions & 40 deletions

File tree

tools/main.js

Lines changed: 63 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,6 @@ Fiber(function () {
350350
var command = null;
351351
var isRawCommand = false;
352352
var showHelp = false;
353-
var optimistOptions = {};
354353

355354
// Check to see if it is a "raw" command that handles its own
356355
// argument parsing.
@@ -371,21 +370,42 @@ Fiber(function () {
371370
// 'flag', or in the command 'thing' with an option 'flag' that is
372371
// set to 'stuff'? To resolve this we require that 'flag' be
373372
// consistently declared as a boolean (or not a boolean) across all
374-
// commands. We could just require the user to put the command
375-
// before any flag, but this being Meteor, we go to lengths to be
376-
// both correct and accommodating.
373+
// commands.
377374
//
378-
// (We used to do this in two passes, where the first pass just
379-
// pulled out the command and the release, and the second pass
380-
// parsed the arguments with knowledge of the command, but now that
381-
// we're determining upfront which options are boolean there's no
382-
// real benefit to two passes.)
383-
var opt = require('optimist')(process.argv.slice(2));
375+
// XXX The problem with the above is that which commands are boolean
376+
// may change across releases, and when we springboard, we actually
377+
// have to parse the options with the *target* version's
378+
// semantics. All in all, I think we might be better served to
379+
// require options to come after the command, other than special
380+
// options (--release, --help, and options that act as
381+
// commands). Then we don't have to require consistency of boolean
382+
// status between commands; we instead have to require consistency
383+
// of boolean status of a particular option, for a command, across
384+
// releases. Since we always start out by running the latest version
385+
// of Meteor, which can have knowledge of all past versions
386+
// (including the boolean status of formerly present but removed
387+
// options, including options to removed commands), this should let
388+
// us be 100% correct. (Of course, we could still do this if we
389+
// required options to be consistent across commands as well, but I
390+
// think this is a better tradeoff.) In this model, we'd do option
391+
// parsing in two passes, where the first pass just pulls out the
392+
// command, and the second parses the arguments with knowledge of
393+
// the command. I would make this change right now but we're on a
394+
// tight timetable for 1.0 and there is no advantage to doing it now
395+
// rather than later. #ImprovingCrossVersionOptionParsing
396+
//
397+
// If --foo is an argument that takes a String, and we've told
398+
// optimist to treat it as a string and not try to parse it as an
399+
// integer (which would irretrievably lose leading zeros), then
400+
// optimist provides no way to distinguish between 'meteor --foo'
401+
// (--foo with no value supplied) and 'meteor'. So, append a random
402+
// sentinel value to the end of the arguments to distinguish the two
403+
// cases.
404+
var sentinel = '$' + Math.random(); // '$' suppresses number conversion
405+
var opt = require('optimist')(process.argv.slice(2).concat([sentinel]));
384406
opt.alias("h", "help")
385407
.boolean("h")
386408
.boolean("help");
387-
optimistOptions['help'] = true;
388-
optimistOptions['h'] = true;
389409

390410
var isBoolean = { help: true, h: true };
391411
var walkCommands = function (node) {
@@ -412,8 +432,6 @@ Fiber(function () {
412432
// manually parse it into the correct type later
413433
opt.string(name);
414434

415-
optimistOptions[name] = true;
416-
417435
// side note: it's a little unfortunate that optimist
418436
// puts all options in the same namespace and can't
419437
// distinguish between '-a foo' and '--a foo'. one day
@@ -438,7 +456,6 @@ Fiber(function () {
438456
if (_.has(isBoolean, key))
439457
throw new Error("--" + key + " is both an option and a command?")
440458
opt.boolean(key);
441-
optimistOptions[key] = true;
442459
});
443460

444461
// The following line actually parses the arguments (argv is a
@@ -449,18 +466,23 @@ Fiber(function () {
449466
// even if it didn't appear on the command line. Delete the
450467
// options that didn't actually appear, which will have the value
451468
// 'false'.
452-
//
453-
// Only do this for options that we actually told optimist about,
454-
// so that if the user specifies '--unknownoption' at the end of a
455-
// command, we'll still print an error.
456-
//
457-
// XXX this strategy still doesn't help us catch a user "meteor
458-
// run --foo" if foo is an option that takes a string. rethink
459-
if (parsed[key] === false && _.has(optimistOptions, key))
469+
if (parsed[key] === false)
460470
delete parsed[key];
461471
});
462472
delete parsed['$0'];
463473

474+
// Pull off the sentinel and make sure there were no options that
475+
// expected a value but didn't get one.
476+
var missingSentinel = false;
477+
if (parsed._.length && parsed._[parsed._.length - 1] === sentinel) {
478+
// All good
479+
parsed._.pop();
480+
} else {
481+
// Enable a sanity check further down that makes sure we actually
482+
// found the option that wasn't given a value.
483+
missingSentinel = true;
484+
}
485+
464486
// Figure out if we're running in a directory that is part of a
465487
// Meteor application. Determine any additional directories to
466488
// search for packages.
@@ -489,7 +511,8 @@ Fiber(function () {
489511
// decide whether to exec the other version of meteor that would
490512
// interpret the flags. That's not ideal, but it should do fine in
491513
// practice, and it's better than assuming that all options are or
492-
// aren't boolean when interpreting --release.
514+
// aren't boolean when interpreting --release. See
515+
// #ImprovingCrossVersionOptionParsing.
493516

494517
var releaseOverride = null;
495518
if (_.has(parsed, 'release')) {
@@ -697,8 +720,8 @@ commandName + ": can't pass both -" + optionInfo.short + " and --" +
697720
"Try 'meteor help " + commandName + "' for help.\n");
698721
process.exit(1);
699722
}
700-
var actualOptionName = presentShort ? "-" + optionInfo.short :
701-
"--" + optionName;
723+
var helpfulOptionName = "--" + optionName +
724+
(presentShort ? " (-" + optionInfo.short + ")" : "");
702725

703726
// If you pass an option twice, optimist gives us an
704727
// array. OK. Concatenate all of the values we've got into one big
@@ -716,19 +739,24 @@ commandName + ": can't pass both -" + optionInfo.short + " and --" +
716739
// in the future, we could support multiple values, but we don't
717740
// for now since no command needs it
718741
process.stderr.write(
719-
commandName + ": can only take one " + actualOptionName + " option.\n" +
742+
commandName + ": can only take one " + helpfulOptionName + " option.\n" +
720743
"Try 'meteor help " + commandName + "' for help.\n");
721744
process.exit(1);
722745
} else if (values.length === 1) {
723746
// OK, they provided exactly one value. Check its type and add
724747
// to the output.
725748
var value = values[0];
726-
if (optionInfo.type === Number) {
749+
if (value === sentinel) {
750+
// This option requires a value and they didn't give it one
751+
// (it was the last word on the command line).
752+
process.stderr.write(
753+
commandName + ": the " + helpfulOptionName + " option needs a value.\n" +
754+
"Try 'meteor help " + commandName + "' for help.\n");
755+
process.exit(1);
756+
} else if (optionInfo.type === Number) {
727757
if (! value.match(/^[1-9][0-9]*$/)) {
728758
process.stderr.write(
729-
"--" + optionName + " " +
730-
(presentShort ? "(-" + optionInfo.short + ") " : "") +
731-
"must be a number.\n" +
759+
commandName + ": " + helpfulOptionName + " must be a number.\n" +
732760
"Try 'meteor help " + commandName + "' for help.\n");
733761
process.exit(1);
734762
}
@@ -778,6 +806,10 @@ longHelp(commandName) + "\n");
778806
process.exit(1);
779807
}
780808

809+
if (missingSentinel)
810+
// sanity check
811+
throw new Error("couldn't find the option that didn't have a value?");
812+
781813
// Check argument count.
782814
if (options.args.length < command.minArgs) {
783815
process.stderr.write(

tools/run-all.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,8 @@ var Updater = require('./run-updater.js').Updater;
1414
///////////////////////////////////////////////////////////////////////////////
1515
// XXX XXX NEXT (if you want to do more):
1616
//
17-
// - make files.getSettings return errors instead of throwing (or eliminate)
1817
// - deal with XXX's in updater about it needing to go though runlog since
1918
// no more stdout redirection
20-
// - auth stuff: log into galaxies automatically, reprompt for expired
21-
// credentials..
22-
// - deal with options last on command line without args being tolerated
23-
// - clean up argument parsing? require that only --release appear to
24-
// the left of the command, and do parsing in two phases.. but how
25-
// does this solve --release appearing after the command? well,
26-
// anyway, at least write a comment about what we'd like to do and
27-
// when we want to do it.
2819
//
2920
///////////////////////////////////////////////////////////////////////////////
3021

0 commit comments

Comments
 (0)