diff --git a/docs/deprecations.md b/docs/deprecations.md
index 3a2d162da..bacfa1c34 100644
--- a/docs/deprecations.md
+++ b/docs/deprecations.md
@@ -17,6 +17,16 @@ disabledDeprecations:
- '*' # To disable all deprecation messages
```
+
+
+## CLI command options should follow command
+
+Deprecation code: `CLI_OPTIONS_BEFORE_COMMAND`
+
+Starting with v3.0.0, Serverless will not support putting options before command, e.g. `sls -v deploy` will no longer be recognized as `deploy` command.
+
+Ensure to always format CLI command as `sls [command..] [options...]`
+
## `configValidationMode: error` will be new default`
diff --git a/lib/cli/commands-schema.js b/lib/cli/commands-schema.js
index 8b043a9b2..1ffd56a9a 100644
--- a/lib/cli/commands-schema.js
+++ b/lib/cli/commands-schema.js
@@ -44,6 +44,7 @@ commands.set('config credentials', {
overwrite: {
usage: 'Overwrite the existing profile configuration in the credentials file',
shortcut: 'o',
+ type: 'boolean',
},
},
});
@@ -105,6 +106,7 @@ commands.set('deploy', {
options: {
'conceal': {
usage: 'Hide secrets from the output (e.g. API Gateway key values)',
+ type: 'boolean',
},
'package': {
usage: 'Path of the deployment package',
@@ -113,9 +115,11 @@ commands.set('deploy', {
'verbose': {
usage: 'Show all stack events during deployment',
shortcut: 'v',
+ type: 'boolean',
},
'force': {
usage: 'Forces a deployment to take place',
+ type: 'boolean',
},
'function': {
usage: "Function name. Deploys a single function (see 'deploy function')",
@@ -123,6 +127,7 @@ commands.set('deploy', {
},
'aws-s3-accelerate': {
usage: 'Enables S3 Transfer Acceleration making uploading artifacts much faster.',
+ type: 'boolean',
},
},
});
@@ -139,10 +144,12 @@ commands.set('deploy function', {
},
'force': {
usage: 'Forces a deployment to take place',
+ type: 'boolean',
},
'update-config': {
usage: 'Updates function configuration, e.g. Timeout or Memory Size without deploying code', // eslint-disable-line max-len
shortcut: 'u',
+ type: 'boolean',
},
},
});
@@ -183,6 +190,7 @@ commands.set('info', {
options: {
conceal: {
usage: 'Hide secrets from the output (e.g. API Gateway key values)',
+ type: 'boolean',
},
},
});
@@ -227,6 +235,7 @@ commands.set('invoke', {
log: {
usage: 'Trigger logging data output',
shortcut: 'l',
+ type: 'boolean',
},
data: {
usage: 'Input data',
@@ -234,6 +243,7 @@ commands.set('invoke', {
},
raw: {
usage: 'Flag to pass input data as a raw string',
+ type: 'boolean',
},
context: {
usage: 'Context of the service',
@@ -264,6 +274,7 @@ commands.set('invoke local', {
},
'raw': {
usage: 'Flag to pass input data as a raw string',
+ type: 'boolean',
},
'context': {
usage: 'Context of the service',
@@ -276,7 +287,7 @@ commands.set('invoke local', {
usage: 'Override environment variables. e.g. --env VAR1=val1 --env VAR2=val2',
shortcut: 'e',
},
- 'docker': { usage: 'Flag to turn on docker use for node/python/ruby/java' },
+ 'docker': { usage: 'Flag to turn on docker use for node/python/ruby/java', type: 'boolean' },
'docker-arg': {
usage: 'Arguments to docker run command. e.g. --docker-arg "-p 9229:9229"',
},
@@ -304,6 +315,7 @@ commands.set('logs', {
tail: {
usage: 'Tail the log output',
shortcut: 't',
+ type: 'boolean',
},
startTime: {
usage:
@@ -447,6 +459,7 @@ commands.set('remove', {
verbose: {
usage: 'Show all stack events during deployment',
shortcut: 'v',
+ type: 'boolean',
},
},
});
@@ -464,6 +477,7 @@ commands.set('rollback', {
verbose: {
usage: 'Show all stack events during deployment',
shortcut: 'v',
+ type: 'boolean',
},
},
});
@@ -491,10 +505,12 @@ commands.set('slstats', {
enable: {
usage: 'Enable stats ("--enable")',
shortcut: 'e',
+ type: 'boolean',
},
disable: {
usage: 'Disable stats ("--disable")',
shortcut: 'd',
+ type: 'boolean',
},
},
});
@@ -507,6 +523,7 @@ commands.set('studio', {
autoStage: {
usage: 'If specified, generate a random stage. This stage will be removed on exit.',
shortcut: 'a',
+ type: 'boolean',
},
},
});
@@ -532,6 +549,7 @@ commands.set('upgrade', {
options: {
major: {
usage: 'Enable upgrade to a new major release',
+ type: 'boolean',
},
},
});
@@ -561,6 +579,7 @@ const awsServiceOptions = {
usage:
'Rely on locally resolved AWS credentials instead of loading them from ' +
'Dashboard provider settings (applies only to services integrated with Dashboard)',
+ type: 'boolean',
},
};
@@ -571,8 +590,8 @@ for (const [name, schema] of commands) {
if (schema.hasAwsExtension) Object.assign(schema.options, awsServiceOptions);
}
if (name) {
- schema.options.help = { usage: 'Show this message', shortcut: 'h' };
+ schema.options.help = { usage: 'Show this message', shortcut: 'h', type: 'boolean' };
} else {
- schema.options['help-interactive'] = { usage: 'Show this message' };
+ schema.options['help-interactive'] = { usage: 'Show this message', type: 'boolean' };
}
}
diff --git a/lib/cli/resolve-input.js b/lib/cli/resolve-input.js
index 785878cd4..d274e88a8 100644
--- a/lib/cli/resolve-input.js
+++ b/lib/cli/resolve-input.js
@@ -4,42 +4,83 @@
const memoizee = require('memoizee');
const parseArgs = require('./parse-args');
+const commandsSchema = require('./commands-schema');
-const customSAliasCommands = new Set(['config credentials'], ['config tabcompletion install']);
+const baseArgsSchema = {
+ boolean: new Set(['help', 'help-interactive', 'use-local-credentials', 'v', 'version']),
+ string: new Set(['app', 'config', 'org', 'stage']),
+ alias: new Map([
+ ['c', 'config'],
+ ['h', 'help'],
+ ]),
+};
+
+const resolveArgsSchema = (commandOptionsSchema) => {
+ const options = { boolean: new Set(), string: new Set(), alias: new Map() };
+ for (const [name, optionSchema] of Object.entries(commandOptionsSchema)) {
+ switch (optionSchema.type) {
+ case 'boolean':
+ options.boolean.add(name);
+ break;
+ case 'multiple':
+ options.multiple.add(name);
+ break;
+ default:
+ options.string.add(name);
+ }
+ if (optionSchema.shortcut) options.alias.set(optionSchema.shortcut, name);
+ }
+ return options;
+};
module.exports = memoizee(() => {
const args = process.argv.slice(2);
- const baseArgsSchema = {
- boolean: new Set(['help', 'help-interactive', 'v', 'version']),
- string: new Set(['app', 'config', 'org', 'stage']),
- alias: new Map([
- ['c', 'config'],
- ['h', 'help'],
- ]),
- };
-
+ // Ideally no options should be passed before command (to know what options are supported,
+ // and whether they're boolean or not, we need to know command name upfront).
+ // Still so far we (kind of) supported such notation and we need to maintain it in current major.
+ // Thefore at first resolution stage we use schema that recognizes just some popular options
let options = parseArgs(args, baseArgsSchema);
-
- const command = options._.join(' ');
- if (!command) {
- // Ideally we should output version info in whatever context "--version" or "-v" params
- // are used. Still "-v" is defined also as a "--verbose" alias for some commands.
- // Support for "--verbose" is expected to go away with
- // https://github.com/serverless/serverless/issues/1720
- // Until that's addressed we can recognize "-v" only with no commands
- baseArgsSchema.boolean.delete('v');
- baseArgsSchema.alias.set('v', 'version');
- }
- if (!customSAliasCommands.has(command)) {
- // Unfortunately, there are few command for which "-s" aliases different param than "--stage"
- // This handling ensures we do not break those commands
- baseArgsSchema.alias.set('s', 'stage');
- }
-
- options = parseArgs(args, baseArgsSchema);
-
- const commands = options._;
+ let commands = options._;
delete options._;
+
+ let command = commands.join(' ');
+
+ if (!command) {
+ // Handle eventual special cases, not reflected in commands schema
+ if (options.v) options.version = true;
+ if (options.help || options.version) return { commands, options };
+ }
+ if (command === 'help') return { commands, options };
+
+ // Having command potentially resolved, resolve options again with help of the command schema
+ let commandSchema = commandsSchema.get(command);
+ while (commandSchema) {
+ const resolvedOptions = parseArgs(args, resolveArgsSchema(commandSchema.options));
+ const resolvedCommand = resolvedOptions._.join(' ');
+ if (resolvedCommand === command) {
+ options = resolvedOptions;
+ commands = options._;
+ delete options._;
+ break;
+ }
+ // Unlikely scenario, where after applying the command schema different command resolves
+ // It can happen only in cases where e.g. for "sls deploy --force function -f foo"
+ // we intially assume "deploy" command, while after applying "deploy" command schema it's
+ // actually a "deploy function" command that resolves
+ command = resolvedCommand;
+ commandSchema = commandsSchema.get(resolvedCommand);
+ }
+
+ const argsString = args.join(' ');
+ if (command && argsString !== command && !argsString.startsWith(`${command} `)) {
+ // Some options were passed before command name (e.g. "sls -v deploy"), deprecate such usage
+ require('../utils/logDeprecation')(
+ 'CLI_OPTIONS_BEFORE_COMMAND',
+ '"serverless" command options are expected to follow command and not be put before the command.\n' +
+ 'Starting from next major Serverless will no longer support the latter form.'
+ );
+ }
+
return { commands, options };
});
diff --git a/test/unit/lib/cli/resolve-input.test.js b/test/unit/lib/cli/resolve-input.test.js
index 74891a400..28eaed672 100644
--- a/test/unit/lib/cli/resolve-input.test.js
+++ b/test/unit/lib/cli/resolve-input.test.js
@@ -71,7 +71,7 @@ describe('test/unit/lib/cli/resolve-input.test.js', () => {
resolveInput.clear();
data = overrideArgv(
{
- args: ['serverless', 'cmd1', 'cmd2', '-s', 'stage'],
+ args: ['serverless', 'package', '-s', 'stage'],
},
() => resolveInput()
);
@@ -124,5 +124,44 @@ describe('test/unit/lib/cli/resolve-input.test.js', () => {
it('should recognize --c alias', async () => {
expect(data.options.config).to.equal('conf');
});
+
+ it('should recognize --version', async () => {
+ resolveInput.clear();
+ data = overrideArgv(
+ {
+ args: ['serverless', '--version'],
+ },
+ () => resolveInput()
+ );
+ expect(data).to.deep.equal({ commands: [], options: { version: true } });
+ });
+
+ it('should recognize interactive setup', async () => {
+ resolveInput.clear();
+ data = overrideArgv(
+ {
+ args: ['serverless', '--app', 'foo'],
+ },
+ () => resolveInput()
+ );
+ expect(data).to.deep.equal({ commands: [], options: { app: 'foo' } });
+ });
+ });
+
+ describe('"help" command', () => {
+ let data;
+ before(() => {
+ resolveInput.clear();
+ data = overrideArgv(
+ {
+ args: ['serverless', 'help'],
+ },
+ () => resolveInput()
+ );
+ });
+
+ it('should recognize', async () => {
+ expect(data).to.deep.equal({ commands: ['help'], options: {} });
+ });
});
});