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: {} }); + }); }); });