Skip to content

Commit

Permalink
Merge pull request #4586 from serverless/add-levenshtein
Browse files Browse the repository at this point in the history
Add "did you mean..." suggestions when you make a typo
  • Loading branch information
horike37 committed Dec 20, 2017
2 parents 44d66a7 + 4a3e140 commit 851ba46
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 7 deletions.
6 changes: 4 additions & 2 deletions lib/classes/CLI.js
Expand Up @@ -5,6 +5,7 @@ const minimist = require('minimist');
const _ = require('lodash');
const os = require('os');
const chalk = require('chalk');
const getCommandSuggestion = require('../utils/getCommandSuggestion');

class CLI {
constructor(serverless, inputArray) {
Expand Down Expand Up @@ -149,7 +150,6 @@ class CLI {
this.consoleLog(chalk.dim('* Documentation: https://serverless.com/framework/docs/'));

this.consoleLog('');

if (!_.isEmpty(frameworkCommands)) {
_.forEach(frameworkCommands, (details, command) => {
this.displayCommandUsage(details, command);
Expand Down Expand Up @@ -252,8 +252,9 @@ class CLI {

// Throw error if command not found.
if (!command) {
const suggestedCommand = getCommandSuggestion(commandName, allCommands);
const errorMessage = [
`Serverless command "${commandName}" not found.`,
`Serverless command "${commandName}" not found. Did you mean "${suggestedCommand}"?`,
' Run "serverless help" for a list of all available commands.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
Expand All @@ -266,6 +267,7 @@ class CLI {
this.displayCommandOptions(command);

this.consoleLog('');
return null;
}

getVersionNumber() {
Expand Down
10 changes: 7 additions & 3 deletions lib/classes/PluginManager.js
Expand Up @@ -8,6 +8,7 @@ const writeFile = require('../utils/fs/writeFile');
const getCacheFilePath = require('../utils/getCacheFilePath');
const getServerlessConfigFile = require('../utils/getServerlessConfigFile');
const crypto = require('crypto');
const getCommandSuggestion = require('../utils/getCommandSuggestion');

/**
* @private
Expand Down Expand Up @@ -324,9 +325,12 @@ class PluginManager {
return current.commands[name];
}
const commandName = commandOrAlias.slice(0, index + 1).join(' ');
const errorMessage = `Serverless command "${commandName}" not found
Run "serverless help" for a list of all available commands.`;
const suggestedCommand = getCommandSuggestion(commandName,
this.serverless.cli.loadedCommands);
const errorMessage = [
`Serverless command "${commandName}" not found. Did you mean "${suggestedCommand}"?`,
' Run "serverless help" for a list of all available commands.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
}, { commands: this.commands });
}
Expand Down
36 changes: 36 additions & 0 deletions lib/utils/getCommandSuggestion.js
@@ -0,0 +1,36 @@
'use strict';
const _ = require('lodash');
const levenshtein = require('fast-levenshtein');

const getCollectCommandWords = (commandObject, commandWordsArray) => {
let wordsArray = _.isArray(commandWordsArray)
&& !_.isEmpty(commandWordsArray) ? commandWordsArray : [];
_.forEach(commandObject, (commandChildObject, commandChildName) => {
wordsArray.push(commandChildName);
if (commandChildObject.commands) {
wordsArray = getCollectCommandWords(commandChildObject.commands, wordsArray);
}
});
return _.uniq(wordsArray);
};

const getCommandSuggestion = (inputCommand, allCommandsObject) => {
let suggestion;
const commandWordsArray = getCollectCommandWords(allCommandsObject);
let minValue = 0;
_.forEach(commandWordsArray, correctCommand => {
const distance = levenshtein.get(inputCommand, correctCommand);
if (minValue === 0) {
suggestion = correctCommand;
minValue = distance;
}

if (minValue > distance) {
suggestion = correctCommand;
minValue = distance;
}
});
return suggestion;
};

module.exports = getCommandSuggestion;
25 changes: 25 additions & 0 deletions lib/utils/getCommandSuggestion.test.js
@@ -0,0 +1,25 @@
'use strict';

const expect = require('chai').expect;
const getCommandSuggestion = require('./getCommandSuggestion');
const Serverless = require('../../lib/Serverless');

const serverless = new Serverless();
serverless.init();

describe('#getCommandSuggestion', () => {
it('should return "package" as a suggested command if you input "pekage"', () => {
const suggestedCommand = getCommandSuggestion('pekage', serverless.cli.loadedCommands);
expect(suggestedCommand).to.be.equal('package');
});

it('should return "deploy" as a suggested command if you input "deploi"', () => {
const suggestedCommand = getCommandSuggestion('deploi', serverless.cli.loadedCommands);
expect(suggestedCommand).to.be.equal('deploy');
});

it('should return "invoke" as a suggested command if you input "lnvoke"', () => {
const suggestedCommand = getCommandSuggestion('lnvoke', serverless.cli.loadedCommands);
expect(suggestedCommand).to.be.equal('invoke');
});
});
3 changes: 1 addition & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -100,6 +100,7 @@
"chalk": "^2.0.0",
"ci-info": "^1.1.1",
"download": "^5.0.2",
"fast-levenshtein": "^2.0.6",
"filesize": "^3.3.0",
"fs-extra": "^0.26.7",
"get-stdin": "^5.0.1",
Expand Down

0 comments on commit 851ba46

Please sign in to comment.