├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── SECURITY.md ├── example.js ├── index.js ├── leven.js ├── package.json └── test.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [14.x, 16.x, 18.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Use Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | 21 | - name: Install 22 | run: | 23 | npm install 24 | 25 | - name: Run tests 26 | run: | 27 | npm run test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2022 Matteo Collina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | commist 2 | ======= 3 | 4 | Build command line application with multiple commands the easy way. 5 | To be used with [minimist](http://npm.im/minimist). 6 | 7 | ```js 8 | 'use strict' 9 | 10 | const program = require('commist')() 11 | const result = program 12 | .register('abcd', function(args) { 13 | console.log('just do', args) 14 | }) 15 | .register({ command: 'restore', equals: true }, function(args) { 16 | console.log('restore', args) 17 | }) 18 | .register('args', function(args) { 19 | args = minimist(args) 20 | console.log('just do', args) 21 | }) 22 | .register('abcde code', function(args) { 23 | console.log('doing something', args) 24 | }) 25 | .register('another command', function(args) { 26 | console.log('anothering', args) 27 | }) 28 | .parse(process.argv.splice(2)) 29 | 30 | if (result) { 31 | console.log('no command called, args', result) 32 | } 33 | ``` 34 | 35 | To handle `async` operations, use `parseAsync` instead, 36 | which let you await on registered commands execution. 37 | 38 | ```js 39 | 'use strict' 40 | 41 | const program = require('commist')() 42 | 43 | const result = await program 44 | .register('abcd', async function(args) { 45 | await executeCommand(args) 46 | await doOtherStuff() 47 | }) 48 | .parseAsync(process.argv.splice(2)) 49 | 50 | if (result) { 51 | console.log('no command called, args', result) 52 | } 53 | ``` 54 | 55 | When calling _commist_ programs, you can abbreviate down to three char 56 | words. In the above example, these are valid commands: 57 | 58 | ``` 59 | node example.js abc 60 | node example.js abc cod 61 | node example.js anot comm 62 | ``` 63 | 64 | Moreover, little spelling mistakes are corrected too: 65 | 66 | ``` 67 | node example.js abcs cod 68 | ``` 69 | 70 | If you want that the command must be strict equals, you can register the 71 | command with the json configuration: 72 | 73 | ```js 74 | program.register({ command: 'restore', strict: true }, function(args) { 75 | console.log('restore', args) 76 | }) 77 | ``` 78 | 79 | If you want to limit the maximum levenshtein distance of your commands, 80 | you can use `maxDistance: 2`: 81 | 82 | ```js 83 | const program = require('commist')() 84 | const minimist = require('minimist') 85 | 86 | const result = program 87 | .register('abcd', function(args) { 88 | console.log('just do', args) 89 | }) 90 | .register({ command: 'restore', equals: true }, function(args) { 91 | console.log('restore', args) 92 | }) 93 | .register('args', function(args) { 94 | args = minimist(args) 95 | console.log('just do', args) 96 | }) 97 | .register('abcde code', function(args) { 98 | console.log('doing something', args) 99 | }) 100 | .register('another command', function(args) { 101 | console.log('anothering', args) 102 | }) 103 | .parse(process.argv.splice(2)) 104 | 105 | if (result) { 106 | console.log('no command called, args', result) 107 | } 108 | ``` 109 | 110 | License 111 | ------- 112 | 113 | MIT 114 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 3.x.x | :white_check_mark: | 8 | | 2.x.x | :white_check_mark: | 9 | | 1.x.x | :white_check_mark: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | Please report all vulnerabilities at [https://github.com/mcollina/commist/security](https://github.com/mcollina/commist/security). 14 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const program = require('./')() 4 | const minimist = require('minimist') 5 | const result = program 6 | .register('abcd', function (args) { 7 | console.log('just do', args) 8 | }) 9 | .register({ command: 'restore', strict: true }, function (args) { 10 | console.log('restore', args) 11 | }) 12 | .register('args', function (args) { 13 | args = minimist(args) 14 | console.log('just do', args) 15 | }) 16 | .register('abcde code', function (args) { 17 | console.log('doing something', args) 18 | }) 19 | .register('another command', function (args) { 20 | console.log('anothering', args) 21 | }) 22 | .parse(process.argv.splice(2)) 23 | 24 | if (result) { 25 | console.log('no command called, args', result) 26 | } 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2014-2022 Matteo Collina 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | 'use strict' 26 | 27 | const leven = require('./leven') 28 | 29 | function commist (opts) { 30 | opts = opts || {} 31 | const commands = [] 32 | const maxDistance = opts.maxDistance || Infinity 33 | 34 | function lookup (array) { 35 | if (typeof array === 'string') { array = array.split(' ') } 36 | 37 | let res = commands.map(function (cmd) { 38 | return cmd.match(array) 39 | }) 40 | 41 | res = res.filter(function (match) { 42 | if (match.partsNotMatched !== 0) { 43 | return false 44 | } 45 | return match.distances.reduce(function (acc, curr) { 46 | return acc && curr <= maxDistance 47 | }, true) 48 | }) 49 | 50 | res = res.sort(function (a, b) { 51 | if (a.inputNotMatched > b.inputNotMatched) { return 1 } 52 | 53 | if (a.inputNotMatched === b.inputNotMatched && a.totalDistance > b.totalDistance) { return 1 } 54 | 55 | return -1 56 | }) 57 | 58 | res = res.map(function (match) { 59 | return match.cmd 60 | }) 61 | 62 | return res 63 | } 64 | 65 | function parse (args) { 66 | const matching = lookup(args) 67 | 68 | if (matching.length > 0) { 69 | matching[0].call(args) 70 | 71 | // return null to indicate there is nothing left to do 72 | return null 73 | } 74 | 75 | return args 76 | } 77 | 78 | async function parseAsync (args) { 79 | const matching = lookup(args) 80 | 81 | if (matching.length > 0) { 82 | await matching[0].call(args) 83 | // return null to indicate there is nothing left to do 84 | return null 85 | } 86 | 87 | return args 88 | } 89 | 90 | function register (inputCommand, func) { 91 | let commandOptions = { 92 | command: inputCommand, 93 | strict: false, 94 | func 95 | } 96 | 97 | if (typeof inputCommand === 'object') { 98 | commandOptions = Object.assign(commandOptions, inputCommand) 99 | } 100 | 101 | const matching = lookup(commandOptions.command) 102 | 103 | matching.forEach(function (match) { 104 | if (match.string === commandOptions.command) { throw new Error('command already registered: ' + commandOptions.command) } 105 | }) 106 | 107 | commands.push(new Command(commandOptions)) 108 | 109 | return this 110 | } 111 | 112 | return { 113 | register, 114 | parse, 115 | parseAsync, 116 | lookup 117 | } 118 | } 119 | 120 | function Command (options) { 121 | this.string = options.command 122 | this.strict = options.strict 123 | this.parts = this.string.split(' ') 124 | this.length = this.parts.length 125 | this.func = options.func 126 | } 127 | 128 | Command.prototype.call = function call (argv) { 129 | return this.func(argv.slice(this.length)) 130 | } 131 | 132 | Command.prototype.match = function match (string) { 133 | return new CommandMatch(this, string) 134 | } 135 | 136 | function CommandMatch (cmd, array) { 137 | this.cmd = cmd 138 | this.distances = cmd.parts.map(function (elem, i) { 139 | if (array[i] !== undefined) { 140 | if (cmd.strict) { 141 | return elem === array[i] ? 0 : undefined 142 | } else { 143 | return leven(elem, array[i]) 144 | } 145 | } else { return undefined } 146 | }).filter(function (distance, i) { 147 | return distance !== undefined && distance < cmd.parts[i].length 148 | }) 149 | 150 | this.partsNotMatched = cmd.length - this.distances.length 151 | this.inputNotMatched = array.length - this.distances.length 152 | this.totalDistance = this.distances.reduce(function (acc, i) { return acc + i }, 0) 153 | } 154 | 155 | module.exports = commist 156 | -------------------------------------------------------------------------------- /leven.js: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) Sindre Sorhus (https://sindresorhus.com) 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software 6 | * and associated documentation files (the "Software"), to deal in the Software without restriction, 7 | * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all copies or substantial 12 | * portions of the Software. 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 15 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 16 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 17 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | */ 19 | 20 | const array = [] 21 | const characterCodeCache = [] 22 | 23 | module.exports = function leven (first, second) { 24 | if (first === second) { 25 | return 0 26 | } 27 | 28 | const swap = first 29 | 30 | // Swapping the strings if `a` is longer than `b` so we know which one is the 31 | // shortest & which one is the longest 32 | if (first.length > second.length) { 33 | first = second 34 | second = swap 35 | } 36 | 37 | let firstLength = first.length 38 | let secondLength = second.length 39 | 40 | // Performing suffix trimming: 41 | // We can linearly drop suffix common to both strings since they 42 | // don't increase distance at all 43 | // Note: `~-` is the bitwise way to perform a `- 1` operation 44 | while (firstLength > 0 && (first.charCodeAt(~-firstLength) === second.charCodeAt(~-secondLength))) { 45 | firstLength-- 46 | secondLength-- 47 | } 48 | 49 | // Performing prefix trimming 50 | // We can linearly drop prefix common to both strings since they 51 | // don't increase distance at all 52 | let start = 0 53 | 54 | while (start < firstLength && (first.charCodeAt(start) === second.charCodeAt(start))) { 55 | start++ 56 | } 57 | 58 | firstLength -= start 59 | secondLength -= start 60 | 61 | if (firstLength === 0) { 62 | return secondLength 63 | } 64 | 65 | let bCharacterCode 66 | let result 67 | let temporary 68 | let temporary2 69 | let index = 0 70 | let index2 = 0 71 | 72 | while (index < firstLength) { 73 | characterCodeCache[index] = first.charCodeAt(start + index) 74 | array[index] = ++index 75 | } 76 | 77 | while (index2 < secondLength) { 78 | bCharacterCode = second.charCodeAt(start + index2) 79 | temporary = index2++ 80 | result = index2 81 | 82 | for (index = 0; index < firstLength; index++) { 83 | temporary2 = bCharacterCode === characterCodeCache[index] ? temporary : temporary + 1 84 | temporary = array[index] 85 | // eslint-disable-next-line no-multi-assign 86 | result = array[index] = temporary > result ? (temporary2 > result ? result + 1 : temporary2) : (temporary2 > temporary ? temporary + 1 : temporary2) 87 | } 88 | } 89 | 90 | return result 91 | } 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "commist", 3 | "version": "3.2.0", 4 | "description": "Build your commands on minimist!", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard && tape test.js" 8 | }, 9 | "pre-commit": "test", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/mcollina/commist.git" 13 | }, 14 | "author": "Matteo Collina ", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/mcollina/commist/issues" 18 | }, 19 | "homepage": "https://github.com/mcollina/commist", 20 | "dependencies": { 21 | }, 22 | "devDependencies": { 23 | "minimist": "^1.1.0", 24 | "pre-commit": "^1.0.0", 25 | "standard": "^17.0.0", 26 | "tape": "^5.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape').test 4 | 5 | const commist = require('./') 6 | const leven = require('./leven') 7 | 8 | test('registering a command', function (t) { 9 | t.plan(2) 10 | 11 | const program = commist() 12 | 13 | program.register('hello', function (args) { 14 | t.deepEqual(args, ['a', '-x', '23']) 15 | }) 16 | 17 | const result = program.parse(['hello', 'a', '-x', '23']) 18 | 19 | t.notOk(result, 'must return null, the command have been handled') 20 | }) 21 | 22 | test('registering two commands', function (t) { 23 | t.plan(1) 24 | 25 | const program = commist() 26 | 27 | program.register('hello', function (args) { 28 | t.ok(false, 'must pick the right command') 29 | }) 30 | 31 | program.register('world', function (args) { 32 | t.deepEqual(args, ['a', '-x', '23']) 33 | }) 34 | 35 | program.parse(['world', 'a', '-x', '23']) 36 | }) 37 | 38 | test('registering two commands (bis)', function (t) { 39 | t.plan(1) 40 | 41 | const program = commist() 42 | 43 | program.register('hello', function (args) { 44 | t.deepEqual(args, ['a', '-x', '23']) 45 | }) 46 | 47 | program.register('world', function (args) { 48 | t.ok(false, 'must pick the right command') 49 | }) 50 | 51 | program.parse(['hello', 'a', '-x', '23']) 52 | }) 53 | 54 | test('registering two words commands', function (t) { 55 | t.plan(1) 56 | 57 | const program = commist() 58 | 59 | program.register('hello', function (args) { 60 | t.ok(false, 'must pick the right command') 61 | }) 62 | 63 | program.register('hello world', function (args) { 64 | t.deepEqual(args, ['a', '-x', '23']) 65 | }) 66 | 67 | program.parse(['hello', 'world', 'a', '-x', '23']) 68 | }) 69 | 70 | test('registering two words commands (bis)', function (t) { 71 | t.plan(1) 72 | 73 | const program = commist() 74 | 75 | program.register('hello', function (args) { 76 | t.deepEqual(args, ['a', '-x', '23']) 77 | }) 78 | 79 | program.register('hello world', function (args) { 80 | t.ok(false, 'must pick the right command') 81 | }) 82 | 83 | program.parse(['hello', 'a', '-x', '23']) 84 | }) 85 | 86 | test('registering ambiguous commands throws exception', function (t) { 87 | const program = commist() 88 | 89 | function noop () {} 90 | 91 | program.register('hello', noop) 92 | program.register('hello world', noop) 93 | program.register('hello world matteo', noop) 94 | 95 | try { 96 | program.register('hello world', noop) 97 | t.ok(false, 'must throw if double-registering the same command') 98 | } catch (err) { 99 | } 100 | 101 | t.end() 102 | }) 103 | 104 | test('looking up commands', function (t) { 105 | const program = commist() 106 | 107 | function noop1 () {} 108 | function noop2 () {} 109 | function noop3 () {} 110 | 111 | program.register('hello', noop1) 112 | program.register('hello world matteo', noop3) 113 | program.register('hello world', noop2) 114 | 115 | t.equal(program.lookup('hello')[0].func, noop1) 116 | t.equal(program.lookup('hello world matteo')[0].func, noop3) 117 | t.equal(program.lookup('hello world')[0].func, noop2) 118 | 119 | t.end() 120 | }) 121 | 122 | test('looking up commands with abbreviations', function (t) { 123 | const program = commist() 124 | 125 | function noop1 () {} 126 | function noop2 () {} 127 | function noop3 () {} 128 | 129 | program.register('hello', noop1) 130 | program.register('hello world matteo', noop3) 131 | program.register('hello world', noop2) 132 | 133 | t.equal(program.lookup('hel')[0].func, noop1) 134 | t.equal(program.lookup('hel wor mat')[0].func, noop3) 135 | t.equal(program.lookup('hel wor')[0].func, noop2) 136 | 137 | t.end() 138 | }) 139 | 140 | test('looking up strict commands', function (t) { 141 | const program = commist() 142 | 143 | function noop1 () {} 144 | function noop2 () {} 145 | 146 | program.register({ command: 'restore', strict: true }, noop1) 147 | program.register({ command: 'rest', strict: true }, noop2) 148 | 149 | t.equal(program.lookup('restore')[0].func, noop1) 150 | t.equal(program.lookup('rest')[0].func, noop2) 151 | t.equal(program.lookup('remove')[0], undefined) 152 | 153 | t.end() 154 | }) 155 | 156 | test('executing commands from abbreviations', function (t) { 157 | t.plan(1) 158 | 159 | const program = commist() 160 | 161 | program.register('hello', function (args) { 162 | t.deepEqual(args, ['a', '-x', '23']) 163 | }) 164 | 165 | program.register('hello world', function (args) { 166 | t.ok(false, 'must pick the right command') 167 | }) 168 | 169 | program.parse(['hel', 'a', '-x', '23']) 170 | }) 171 | 172 | test('executing async command', function (t) { 173 | t.plan(1) 174 | 175 | const program = commist() 176 | 177 | program.register('hello', async function (args) { 178 | t.deepEqual(args, ['a', '-x', '23']) 179 | }) 180 | 181 | program.parseAsync(['hello', 'a', '-x', '23']) 182 | }) 183 | 184 | test('async execution resolves when correctly matched one', function (t) { 185 | t.plan(1) 186 | 187 | const program = commist() 188 | 189 | program.register('hello', async function () { 190 | return 1337 191 | }) 192 | 193 | program.parseAsync(['hello', 'a', '-x', '23']).then((result) => { 194 | t.equal(result, null) 195 | }) 196 | }) 197 | 198 | test('async execution resolves with args if no commands matched', function (t) { 199 | t.plan(1) 200 | 201 | const program = commist() 202 | 203 | program.register('hello', async function () { 204 | t.ok(false, 'command should not be picked') 205 | }) 206 | 207 | program.parseAsync(['whoops', 'a', '-x', '23']).then((args) => { 208 | t.deepEqual(args, ['whoops', 'a', '-x', '23']) 209 | }) 210 | }) 211 | 212 | test('async execution should wait intil registered command finishes', function (t) { 213 | t.plan(1) 214 | 215 | const program = commist() 216 | 217 | program.register('hello', async function () { 218 | const res = await Promise.resolve(42) 219 | return res 220 | }) 221 | 222 | program.parseAsync(['hello', 'a', '-x', '23']).then((result) => { 223 | t.equal(result, null) 224 | }) 225 | }) 226 | 227 | test('async execution should work with sync commands', function (t) { 228 | t.plan(1) 229 | 230 | const program = commist() 231 | 232 | program.register('hello', function (args) { 233 | t.deepEqual(args, ['a', '-x', '23']) 234 | }) 235 | 236 | program.parseAsync(['hello', 'a', '-x', '23']) 237 | }) 238 | 239 | test('sync execution should work with async commands', function (t) { 240 | t.plan(1) 241 | 242 | const program = commist() 243 | 244 | program.register('hello', async function (args) { 245 | t.deepEqual(args, ['a', '-x', '23']) 246 | }) 247 | 248 | program.parse(['hello', 'a', '-x', '23']) 249 | }) 250 | 251 | test('one char command', function (t) { 252 | const program = commist() 253 | 254 | function noop1 () {} 255 | 256 | program.register('h', noop1) 257 | t.equal(program.lookup('h')[0].func, noop1) 258 | 259 | t.end() 260 | }) 261 | 262 | test('two char command', function (t) { 263 | const program = commist() 264 | 265 | function noop1 () {} 266 | 267 | program.register('he', noop1) 268 | t.equal(program.lookup('he')[0].func, noop1) 269 | 270 | t.end() 271 | }) 272 | 273 | test('a command part must be at least 3 chars', function (t) { 274 | const program = commist() 275 | 276 | function noop1 () {} 277 | 278 | program.register('h b', noop1) 279 | 280 | t.equal(program.lookup('h b')[0].func, noop1) 281 | 282 | t.end() 283 | }) 284 | 285 | test('short commands match exactly', function (t) { 286 | const program = commist() 287 | 288 | function noop1 () {} 289 | function noop2 () {} 290 | 291 | program.register('h', noop1) 292 | program.register('help', noop2) 293 | 294 | t.equal(program.lookup('h')[0].func, noop1) 295 | t.equal(program.lookup('he')[0].func, noop2) 296 | t.equal(program.lookup('hel')[0].func, noop2) 297 | t.equal(program.lookup('help')[0].func, noop2) 298 | 299 | t.end() 300 | }) 301 | 302 | test('leven', function (t) { 303 | t.is(leven('a', 'b'), 1) 304 | t.is(leven('ab', 'ac'), 1) 305 | t.is(leven('ac', 'bc'), 1) 306 | t.is(leven('abc', 'axc'), 1) 307 | t.is(leven('kitten', 'sitting'), 3) 308 | t.is(leven('xabxcdxxefxgx', '1ab2cd34ef5g6'), 6) 309 | t.is(leven('cat', 'cow'), 2) 310 | t.is(leven('xabxcdxxefxgx', 'abcdefg'), 6) 311 | t.is(leven('javawasneat', 'scalaisgreat'), 7) 312 | t.is(leven('example', 'samples'), 3) 313 | t.is(leven('sturgeon', 'urgently'), 6) 314 | t.is(leven('levenshtein', 'frankenstein'), 6) 315 | t.is(leven('distance', 'difference'), 5) 316 | t.is(leven('因為我是中國人所以我會說中文', '因為我是英國人所以我會說英文'), 2) 317 | t.end() 318 | }) 319 | 320 | test('max distance', function (t) { 321 | const program = commist({ maxDistance: 2 }) 322 | 323 | function noop1 () {} 324 | function noop2 () {} 325 | function noop3 () {} 326 | 327 | program.register('hello', noop1) 328 | program.register('hello world matteo', noop3) 329 | program.register('hello world', noop2) 330 | 331 | t.equal(program.lookup('hel')[0].func, noop1) 332 | t.equal(program.lookup('hel wor mat')[0].func, noop2) 333 | t.equal(program.lookup('hello world matt')[0].func, noop3) 334 | t.equal(program.lookup('hello wor')[0].func, noop2) 335 | t.deepEqual(program.lookup('he wor'), []) 336 | 337 | t.end() 338 | }) 339 | 340 | test('help foobar vs start', function (t) { 341 | const program = commist({ maxDistance: 2 }) 342 | 343 | function noop1 () {} 344 | function noop2 () {} 345 | 346 | program.register('help', noop1) 347 | program.register('start', noop2) 348 | 349 | t.equal(program.lookup('help')[0].func, noop1) 350 | t.deepEqual(program.lookup('help foobar')[0].func, noop1) 351 | t.equal(program.lookup('start')[0].func, noop2) 352 | 353 | t.end() 354 | }) 355 | 356 | test('registering a command with maxDistance', function (t) { 357 | t.plan(2) 358 | 359 | const program = commist({ maxDistance: 2 }) 360 | 361 | program.register('hello', function (args) { 362 | t.deepEqual(args, ['a', '-x', '23']) 363 | }) 364 | 365 | const result = program.parse(['hello', 'a', '-x', '23']) 366 | 367 | t.notOk(result, 'must return null, the command have been handled') 368 | }) 369 | --------------------------------------------------------------------------------