This app provides monitoring and information features for the common freifunk user and the technical stuff of a freifunk community.
Code base is taken from a TUM Practical Course project and added here to see if Freifunk Altdorf can use it.
https://www.freifunk-altdorf.de
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
329 lines
11 KiB
329 lines
11 KiB
'use strict' |
|
const path = require('path') |
|
const inspect = require('util').inspect |
|
const camelCase = require('camelcase') |
|
|
|
const DEFAULT_MARKER = '*' |
|
|
|
// handles parsing positional arguments, |
|
// and populating argv with said positional |
|
// arguments. |
|
module.exports = function command (yargs, usage, validation) { |
|
const self = {} |
|
|
|
let handlers = {} |
|
let aliasMap = {} |
|
let defaultCommand |
|
self.addHandler = function addHandler (cmd, description, builder, handler) { |
|
let aliases = [] |
|
handler = handler || (() => {}) |
|
|
|
if (Array.isArray(cmd)) { |
|
aliases = cmd.slice(1) |
|
cmd = cmd[0] |
|
} else if (typeof cmd === 'object') { |
|
let command = (Array.isArray(cmd.command) || typeof cmd.command === 'string') ? cmd.command : moduleName(cmd) |
|
if (cmd.aliases) command = [].concat(command).concat(cmd.aliases) |
|
self.addHandler(command, extractDesc(cmd), cmd.builder, cmd.handler) |
|
return |
|
} |
|
|
|
// allow a module to be provided instead of separate builder and handler |
|
if (typeof builder === 'object' && builder.builder && typeof builder.handler === 'function') { |
|
self.addHandler([cmd].concat(aliases), description, builder.builder, builder.handler) |
|
return |
|
} |
|
|
|
// parse positionals out of cmd string |
|
const parsedCommand = self.parseCommand(cmd) |
|
|
|
// remove positional args from aliases only |
|
aliases = aliases.map(alias => self.parseCommand(alias).cmd) |
|
|
|
// check for default and filter out '*'' |
|
let isDefault = false |
|
const parsedAliases = [parsedCommand.cmd].concat(aliases).filter((c) => { |
|
if (c === DEFAULT_MARKER) { |
|
isDefault = true |
|
return false |
|
} |
|
return true |
|
}) |
|
|
|
// short-circuit if default with no aliases |
|
if (isDefault && parsedAliases.length === 0) { |
|
defaultCommand = { |
|
original: cmd.replace(DEFAULT_MARKER, '').trim(), |
|
handler, |
|
builder: builder || {}, |
|
demanded: parsedCommand.demanded, |
|
optional: parsedCommand.optional |
|
} |
|
return |
|
} |
|
|
|
// shift cmd and aliases after filtering out '*' |
|
if (isDefault) { |
|
parsedCommand.cmd = parsedAliases[0] |
|
aliases = parsedAliases.slice(1) |
|
cmd = cmd.replace(DEFAULT_MARKER, parsedCommand.cmd) |
|
} |
|
|
|
// populate aliasMap |
|
aliases.forEach((alias) => { |
|
aliasMap[alias] = parsedCommand.cmd |
|
}) |
|
|
|
if (description !== false) { |
|
usage.command(cmd, description, isDefault, aliases) |
|
} |
|
|
|
handlers[parsedCommand.cmd] = { |
|
original: cmd, |
|
handler, |
|
builder: builder || {}, |
|
demanded: parsedCommand.demanded, |
|
optional: parsedCommand.optional |
|
} |
|
|
|
if (isDefault) defaultCommand = handlers[parsedCommand.cmd] |
|
} |
|
|
|
self.addDirectory = function addDirectory (dir, context, req, callerFile, opts) { |
|
opts = opts || {} |
|
// disable recursion to support nested directories of subcommands |
|
if (typeof opts.recurse !== 'boolean') opts.recurse = false |
|
// exclude 'json', 'coffee' from require-directory defaults |
|
if (!Array.isArray(opts.extensions)) opts.extensions = ['js'] |
|
// allow consumer to define their own visitor function |
|
const parentVisit = typeof opts.visit === 'function' ? opts.visit : o => o |
|
// call addHandler via visitor function |
|
opts.visit = function visit (obj, joined, filename) { |
|
const visited = parentVisit(obj, joined, filename) |
|
// allow consumer to skip modules with their own visitor |
|
if (visited) { |
|
// check for cyclic reference |
|
// each command file path should only be seen once per execution |
|
if (~context.files.indexOf(joined)) return visited |
|
// keep track of visited files in context.files |
|
context.files.push(joined) |
|
self.addHandler(visited) |
|
} |
|
return visited |
|
} |
|
require('require-directory')({ require: req, filename: callerFile }, dir, opts) |
|
} |
|
|
|
// lookup module object from require()d command and derive name |
|
// if module was not require()d and no name given, throw error |
|
function moduleName (obj) { |
|
const mod = require('which-module')(obj) |
|
if (!mod) throw new Error(`No command name given for module: ${inspect(obj)}`) |
|
return commandFromFilename(mod.filename) |
|
} |
|
|
|
// derive command name from filename |
|
function commandFromFilename (filename) { |
|
return path.basename(filename, path.extname(filename)) |
|
} |
|
|
|
function extractDesc (obj) { |
|
for (let keys = ['describe', 'description', 'desc'], i = 0, l = keys.length, test; i < l; i++) { |
|
test = obj[keys[i]] |
|
if (typeof test === 'string' || typeof test === 'boolean') return test |
|
} |
|
return false |
|
} |
|
|
|
self.parseCommand = function parseCommand (cmd) { |
|
const extraSpacesStrippedCommand = cmd.replace(/\s{2,}/g, ' ') |
|
const splitCommand = extraSpacesStrippedCommand.split(/\s+(?![^[]*]|[^<]*>)/) |
|
const bregex = /\.*[\][<>]/g |
|
const parsedCommand = { |
|
cmd: (splitCommand.shift()).replace(bregex, ''), |
|
demanded: [], |
|
optional: [] |
|
} |
|
splitCommand.forEach((cmd, i) => { |
|
let variadic = false |
|
cmd = cmd.replace(/\s/g, '') |
|
if (/\.+[\]>]/.test(cmd) && i === splitCommand.length - 1) variadic = true |
|
if (/^\[/.test(cmd)) { |
|
parsedCommand.optional.push({ |
|
cmd: cmd.replace(bregex, '').split('|'), |
|
variadic |
|
}) |
|
} else { |
|
parsedCommand.demanded.push({ |
|
cmd: cmd.replace(bregex, '').split('|'), |
|
variadic |
|
}) |
|
} |
|
}) |
|
return parsedCommand |
|
} |
|
|
|
self.getCommands = () => Object.keys(handlers).concat(Object.keys(aliasMap)) |
|
|
|
self.getCommandHandlers = () => handlers |
|
|
|
self.hasDefaultCommand = () => !!defaultCommand |
|
|
|
self.runCommand = function runCommand (command, yargs, parsed, commandIndex) { |
|
let aliases = parsed.aliases |
|
const commandHandler = handlers[command] || handlers[aliasMap[command]] || defaultCommand |
|
const currentContext = yargs.getContext() |
|
let numFiles = currentContext.files.length |
|
const parentCommands = currentContext.commands.slice() |
|
|
|
// what does yargs look like after the buidler is run? |
|
let innerArgv = parsed.argv |
|
let innerYargs = null |
|
let positionalMap = {} |
|
|
|
if (command) currentContext.commands.push(command) |
|
if (typeof commandHandler.builder === 'function') { |
|
// a function can be provided, which builds |
|
// up a yargs chain and possibly returns it. |
|
innerYargs = commandHandler.builder(yargs.reset(parsed.aliases)) |
|
// if the builder function did not yet parse argv with reset yargs |
|
// and did not explicitly set a usage() string, then apply the |
|
// original command string as usage() for consistent behavior with |
|
// options object below. |
|
if (yargs.parsed === false) { |
|
if (typeof yargs.getUsageInstance().getUsage() === 'undefined') { |
|
yargs.usage(`$0 ${parentCommands.length ? `${parentCommands.join(' ')} ` : ''}${commandHandler.original}`) |
|
} |
|
innerArgv = innerYargs ? innerYargs._parseArgs(null, null, true, commandIndex) : yargs._parseArgs(null, null, true, commandIndex) |
|
} else { |
|
innerArgv = yargs.parsed.argv |
|
} |
|
|
|
if (innerYargs && yargs.parsed === false) aliases = innerYargs.parsed.aliases |
|
else aliases = yargs.parsed.aliases |
|
} else if (typeof commandHandler.builder === 'object') { |
|
// as a short hand, an object can instead be provided, specifying |
|
// the options that a command takes. |
|
innerYargs = yargs.reset(parsed.aliases) |
|
innerYargs.usage(`$0 ${parentCommands.length ? `${parentCommands.join(' ')} ` : ''}${commandHandler.original}`) |
|
Object.keys(commandHandler.builder).forEach((key) => { |
|
innerYargs.option(key, commandHandler.builder[key]) |
|
}) |
|
innerArgv = innerYargs._parseArgs(null, null, true, commandIndex) |
|
aliases = innerYargs.parsed.aliases |
|
} |
|
|
|
if (!yargs._hasOutput()) { |
|
positionalMap = populatePositionals(commandHandler, innerArgv, currentContext, yargs) |
|
} |
|
|
|
// we apply validation post-hoc, so that custom |
|
// checks get passed populated positional arguments. |
|
if (!yargs._hasOutput()) yargs._runValidation(innerArgv, aliases, positionalMap, yargs.parsed.error) |
|
|
|
if (commandHandler.handler && !yargs._hasOutput()) { |
|
yargs._setHasOutput() |
|
commandHandler.handler(innerArgv) |
|
} |
|
|
|
if (command) currentContext.commands.pop() |
|
numFiles = currentContext.files.length - numFiles |
|
if (numFiles > 0) currentContext.files.splice(numFiles * -1, numFiles) |
|
|
|
return innerArgv |
|
} |
|
|
|
// transcribe all positional arguments "command <foo> <bar> [apple]" |
|
// onto argv. |
|
function populatePositionals (commandHandler, argv, context, yargs) { |
|
argv._ = argv._.slice(context.commands.length) // nuke the current commands |
|
const demanded = commandHandler.demanded.slice(0) |
|
const optional = commandHandler.optional.slice(0) |
|
const positionalMap = {} |
|
|
|
validation.positionalCount(demanded.length, argv._.length) |
|
|
|
while (demanded.length) { |
|
const demand = demanded.shift() |
|
populatePositional(demand, argv, yargs, positionalMap) |
|
} |
|
|
|
while (optional.length) { |
|
const maybe = optional.shift() |
|
populatePositional(maybe, argv, yargs, positionalMap) |
|
} |
|
|
|
argv._ = context.commands.concat(argv._) |
|
return positionalMap |
|
} |
|
|
|
// populate a single positional argument and its |
|
// aliases onto argv. |
|
function populatePositional (positional, argv, yargs, positionalMap) { |
|
// "positional" consists of the positional.cmd, an array representing |
|
// the positional's name and aliases, and positional.variadic |
|
// indicating whether or not it is a variadic array. |
|
let variadics = null |
|
let value = null |
|
for (let i = 0, cmd; (cmd = positional.cmd[i]) !== undefined; i++) { |
|
if (positional.variadic) { |
|
if (variadics) argv[cmd] = variadics.slice(0) |
|
else argv[cmd] = variadics = argv._.splice(0) |
|
} else { |
|
if (!value && !argv._.length) continue |
|
if (value) argv[cmd] = value |
|
else argv[cmd] = value = argv._.shift() |
|
} |
|
positionalMap[cmd] = true |
|
postProcessPositional(yargs, argv, cmd) |
|
addCamelCaseExpansions(argv, cmd) |
|
} |
|
} |
|
|
|
// TODO move positional arg logic to yargs-parser and remove this duplication |
|
function postProcessPositional (yargs, argv, key) { |
|
const coerce = yargs.getOptions().coerce[key] |
|
if (typeof coerce === 'function') { |
|
try { |
|
argv[key] = coerce(argv[key]) |
|
} catch (err) { |
|
yargs.getUsageInstance().fail(err.message, err) |
|
} |
|
} |
|
} |
|
|
|
function addCamelCaseExpansions (argv, option) { |
|
if (/-/.test(option)) { |
|
const cc = camelCase(option) |
|
if (typeof argv[option] === 'object') argv[cc] = argv[option].slice(0) |
|
else argv[cc] = argv[option] |
|
} |
|
} |
|
|
|
self.reset = () => { |
|
handlers = {} |
|
aliasMap = {} |
|
defaultCommand = undefined |
|
return self |
|
} |
|
|
|
// used by yargs.parse() to freeze |
|
// the state of commands such that |
|
// we can apply .parse() multiple times |
|
// with the same yargs instance. |
|
let frozen |
|
self.freeze = () => { |
|
frozen = {} |
|
frozen.handlers = handlers |
|
frozen.aliasMap = aliasMap |
|
frozen.defaultCommand = defaultCommand |
|
} |
|
self.unfreeze = () => { |
|
handlers = frozen.handlers |
|
aliasMap = frozen.aliasMap |
|
defaultCommand = frozen.defaultCommand |
|
frozen = undefined |
|
} |
|
|
|
return self |
|
}
|
|
|