Skip to content

Commit

Permalink
Added middleware capable of being applied to all commands _except_ a …
Browse files Browse the repository at this point in the history
…given list of commands (cherry picking). Also added a special _ignore_ parameter to the commonflag (universal flags) configuration, allowing applications to be applied to a filtered set of commands/subcommands. This closes #3 and it closes #4.
  • Loading branch information
coreybutler committed Jul 16, 2020
1 parent ae911e1 commit 3bc6fb2
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 4 deletions.
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,47 @@ mycli read
```
</details>
#### 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.
<details>
<summary><b>Cherry-picking example</b></summary>
<br/>
```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.
</details>
## 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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion examples/cli/index.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
2 changes: 1 addition & 1 deletion examples/json/index.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
10 changes: 10 additions & 0 deletions src/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 15 additions & 1 deletion src/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
26 changes: 26 additions & 0 deletions src/shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
45 changes: 45 additions & 0 deletions test/unit/01-sanity/02-middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions test/unit/01-sanity/05-commonflags.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})

0 comments on commit 3bc6fb2

Please sign in to comment.