diff --git a/README.md b/README.md index b148438..b8e863b 100644 --- a/README.md +++ b/README.md @@ -524,6 +524,47 @@ mycli read ``` +#### Filtering Universal Flags + +Universal/common flags accept a special attribute named `ignore`, which will prevent the flags from being applied to specific commands. This should be used sparingly. + +
+Cherry-picking example +
+ +```javascript +const shell = new Shell({ + name: 'mycli', + commmonflags: { + ignore: 'info', // This can also be an array of string. Fully qualified subcommands will also be respected. + note: { + alias: 'n', + description: 'Save a note about the operation.' + } + }, + commands: [{ + name: 'create', + handler () {} + }, { + name: 'read', + handler () {} + }, { + name: 'update', + handler () {} + }, { + name: 'delete', + handler () {} + }, { + name: 'info', + handler () {} + }] +}) +``` + +Any command, except `info`, will accepts/parse the `note` flag. + +
+ ## Middleware When a command is called, it's handler function is executed. Sometimes it is desirable to pre-process one or more commands. The shell middleware feature supports "global" middleware and "assigned" middleware. @@ -562,6 +603,8 @@ shell.useWith('demo', function (metadata, next) { The code above would only run when the user inputs the `demo` command (or any `demo` subcommand). +#### Command-Specific Assignments + It is possible to assign middleware to more than one command at a time, and it is possible to target subcommands. For example: ```javascript @@ -596,6 +639,41 @@ cmd.use(function (metadata, next) { }) ``` +#### Command-Exclusion Assignments + +Sometimes middleware needs to be applied to all but a few commands. The `useExcept` method supports these needs. It is basically the opposite of `useWith`. Middleware is applied to all commands/subcommands _except_ those specified. + +For example: + +```javascript +const shell = new Shell({ + ..., + commands: [{ + name: 'add', + handler (meta) { + ... + } + }, { + name: 'subtract', + handler (meta) { + ... + } + }, { + name: 'info', + handler (meta) { + ... + } + }] +}) + +shell.useExcept(['info], function (meta, next) { + console.log(`this middleware is only applied to some math commands`) + next() +}) +``` + +In this example, the console statement would be displayed for all commands except the `info` command (and any info subcommands). + ### Built-in "Middleware" Displaying help and version information is built-in (overridable). diff --git a/examples/cli/index.js b/examples/cli/index.js index ce6abf8..6e6b282 100755 --- a/examples/cli/index.js +++ b/examples/cli/index.js @@ -1,4 +1,4 @@ -#!/usr/bin/env node --experimental-modules -r source-map-support/register +#!/usr/bin/env node -r source-map-support/register import fs from 'fs' import path from 'path' diff --git a/examples/json/index.js b/examples/json/index.js index f4a074d..c5d973c 100755 --- a/examples/json/index.js +++ b/examples/json/index.js @@ -1,4 +1,4 @@ -#!/usr/bin/env node --experimental-modules +#!/usr/bin/env node import fs from 'fs' import path from 'path' import { Command, Shell } from '../../src/index.js' diff --git a/package.json b/package.json index f992d95..4a34501 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@author.io/shell", - "version": "1.7.2", + "version": "1.8.0", "description": "A micro-framework for creating CLI-like experiences. This supports Node.js and browsers.", "main": "src/index.js", "scripts": { diff --git a/src/base.js b/src/base.js index e64a09a..4448f3f 100644 --- a/src/base.js +++ b/src/base.js @@ -408,6 +408,16 @@ export default class Base { return this.#processors } + get commandlist () { + const list = new Set() + this.commands.forEach(cmd => { + list.add(cmd.name) + cmd.commandlist.forEach(subcmd => list.add(`${cmd.name} ${subcmd}`)) + }) + + return Array.from(list).sort() + } + getCommand(name = null) { if (!name) { return null diff --git a/src/command.js b/src/command.js index c066038..5ddc214 100644 --- a/src/command.js +++ b/src/command.js @@ -379,7 +379,21 @@ export default class Command extends Base { // Parse the command input for flags const data = { command: this.name, input: input.trim() } - let flagConfig = Object.assign(this.__commonFlags, this.#flagConfig || {}) + let commonFlags = this.__commonFlags + if (commonFlags.ignore && (Array.isArray(commonFlags.ignore) || typeof commonFlags.ignore === 'string')) { + const ignore = new Set(Array.isArray(commonFlags.ignore) ? commonFlags.ignore : [commonFlags.ignore]) + delete commonFlags.ignore + const root = this.commandroot.replace(new RegExp(`^${this.shell.name}\\s+`, 'i'), '') + + for (const cmd of ignore) { + if (root.startsWith(cmd)) { + commonFlags = {} + break + } + } + } + + let flagConfig = Object.assign(commonFlags, this.#flagConfig || {}) if (!flagConfig.hasOwnProperty('help')) { flagConfig.help = { diff --git a/src/shell.js b/src/shell.js index 6a081b5..3bbbb5b 100644 --- a/src/shell.js +++ b/src/shell.js @@ -138,6 +138,32 @@ export default class Shell extends Base { commands.forEach(cmd => this.#middlewareGroups.set(cmd.trim(), (this.#middlewareGroups.get(cmd.trim()) || []).concat(fns))) } + useExcept (commands) { + if (arguments.length < 2) { + throw new Error('useExcept([\'command\', \'command\'], fn) requires two or more arguments.') + } + + commands = typeof commands === 'string' ? commands.split(/\s+/) : commands + + if (!Array.isArray(commands) || commands.filter(c => typeof c !== 'string').length > 0) { + throw new Error(`The first argument of useExcept must be a string or array of strings. Received ${typeof commands}`) + } + + const fns = Array.from(arguments).slice(1) + const all = new Set(this.commandlist.map(i => i.toLowerCase())) + + commands.forEach(cmd => { + all.delete(cmd) + for (const c of all) { + if (c.indexOf(cmd) === 0) { + all.delete(c) + } + } + }) + + this.useWith(Array.from(all), ...fns) + } + async exec (input, callback) { // The array check exists because people are passing process.argv.slice(2) into this // method, often forgetting to join the values into a string. diff --git a/test/unit/01-sanity/02-middleware.js b/test/unit/01-sanity/02-middleware.js index 32b2dc4..79e1e3f 100644 --- a/test/unit/01-sanity/02-middleware.js +++ b/test/unit/01-sanity/02-middleware.js @@ -113,6 +113,51 @@ test('Command Specific Middleware', t => { }) }) +test('Command Specific Middleware Exceptions', async t => { + let ok = false + + const shell = new Shell({ + name: 'test', + commands: [{ + name: 'a', + handler () { }, + commands: [{ + name: 'f', + handler () { } + }] + }, { + name: 'b', + handler () { }, + commands: [{ + name: 'e', + handler () {} + }] + }, { + name: 'c', + handler () { } + }, { + name: 'd', + handler () { + ok = true + } + }] + }) + + shell.useExcept(['b', 'c'], (meta, next) => { count++; next() }) + + let count = 0 + await shell.exec('a').catch(t.fail) + await shell.exec('b').catch(t.fail) + await shell.exec('c').catch(t.fail) + await shell.exec('d').catch(t.fail) + await shell.exec('b e').catch(t.fail) + await shell.exec('a f').catch(t.fail) + + t.ok(count === 3, `Expected 3 middleware operations to run. Recognized ${count}.`) + t.ok(ok, 'Handler executes at the end.') + t.end() +}) + test('Basic Trailers', t => { let ok = false let after = 0 diff --git a/test/unit/01-sanity/05-commonflags.js b/test/unit/01-sanity/05-commonflags.js index 0145bd3..772156b 100644 --- a/test/unit/01-sanity/05-commonflags.js +++ b/test/unit/01-sanity/05-commonflags.js @@ -88,3 +88,52 @@ test('Common flags (command-specific)', t => { }) .catch(e => console.log(e.stack) && t.fail(e.message) && t.end()) }) + +test('Common flags (exclusions)', async t => { + const shell = new Shell({ + name: 'account', + commonflags: { + ignore: ['other'], + typical: { + alias: 't', + description: 'A typical flag on all commands.', + default: true + } + }, + commands: [{ + name: 'create', + handler (meta) { + t.ok(meta.flag('typical'), `Retrieved common flag value. Expected true, received ${meta.flag('typical')}`) + } + }, { + name: 'delete', + handler (meta) { + t.ok(meta.flag('typical'), `Retrieved common flag value. Expected true, received ${meta.flag('typical')}`) + } + }, { + name: 'other', + handler (meta) { + t.ok(!meta.flags.recognized.hasOwnProperty('typical'), 'The common flag "typical" was successfully excluded.') + }, + commands: [{ + name: 'sub', + handler (meta) { + t.ok(!meta.flags.recognized.hasOwnProperty('typical'), 'The common flag "typical" was successfully excluded from a sub command.') + }, + commands: [{ + name: 'cmd', + handler (meta) { + t.ok(!meta.flags.recognized.hasOwnProperty('typical'), 'The common flag "typical" was successfully excluded from a nested sub command.') + } + }] + }] + }] + }) + + await shell.exec('create').catch(t.fail) + await shell.exec('delete').catch(t.fail) + await shell.exec('other').catch(t.fail) + await shell.exec('other sub').catch(t.fail) + await shell.exec('other sub cmd').catch(t.fail) + t.end() +})