UNPKG

clap

Version:

Command line argument parser

249 lines (212 loc) 7.07 kB
'use strict'; const help = require('./help.cjs'); const parseArgv = require('./parse-argv.cjs'); const params = require('./params.cjs'); const option = require('./option.cjs'); const noop = () => {}; // nothing todo const self = value => value; const defaultHelpAction = (instance, _, { commandPath }) => instance.outputHelp(commandPath); const defaultVersionAction = instance => console.log(instance.meta.version); const lastCommandHost = new WeakMap(); const lastAddedOption = new WeakMap(); class Command { constructor(usage = '', ...rest) { if (rest.length > 0) { throw new Error('Second parameter in Command constructor is deprecated, use first parameter to define args instead, i.e. command("name [arg1] [arg2]")'); } const [name, params$1] = usage.trim().split(/(\s+.*)$/); this.name = name; this.params = new params(params$1, `"${name}" command definition`); this.options = new Map(); this.commands = new Map(); this.meta = { description: '', version: '', help: null }; this.handlers = { init: noop, applyConfig: noop, finishContext: noop, action: self }; this.help(); } // handlers init(fn) { this.handlers.init = fn.bind(null); return this; } applyConfig(fn) { this.handlers.applyConfig = fn.bind(null); return this; } finishContext(fn) { this.handlers.finishContext = fn.bind(null); return this; } action(fn) { this.handlers.action = fn.bind(null); return this; } // definition chaining extend(fn, ...args) { fn(this, ...args); return this; } description(description) { this.meta.description = description; return this; } version(version, usage, description, action) { this.meta.version = version; this.option( usage || '-v, --version', description || 'Output version', { action: action || defaultVersionAction } ); return this; } help(usage, description, action) { if (this.meta.help) { this.meta.help.names().forEach(name => this.options.delete(name)); this.meta.help = null; } if (usage !== false) { this.option( usage || '-h, --help', description || 'Output usage information', { action: action || defaultHelpAction } ); this.meta.help = lastAddedOption.get(this); } return this; } option(usage, description, ...optionOpts) { const option$1 = new option(usage, description, ...optionOpts); const nameType = ['Long option', 'Short option', 'Option']; const names = option$1.names(); names.forEach((name, idx) => { if (this.options.has(name)) { throw new Error( `${nameType[names.length === 2 ? idx * 2 : idx]} name "${name}" already in use by ${this.getOption(name).messageRef()}` ); } }); for (const name of names) { this.options.set(name, option$1); } lastAddedOption.set(this, option$1); return this; } command(usageOrCommand) { const subcommand = typeof usageOrCommand === 'string' ? new Command(usageOrCommand) : usageOrCommand; const name = subcommand.name; // search for existing one if (this.commands.has(name)) { throw new Error( `Subcommand name "${name}" already in use by ${this.getCommand(name).messageRef()}` ); } // attach subcommand this.commands.set(name, subcommand); lastCommandHost.set(subcommand, this); return subcommand; } end() { const host = lastCommandHost.get(this) || null; lastCommandHost.delete(this); return host; } // parse & run parse(argv, suggest) { let chunk = { context: { commandPath: [], options: null, args: null, literalArgs: null }, next: { command: this, argv: argv || process.argv.slice(2) } }; do { chunk = parseArgv( chunk.next.command, chunk.next.argv, chunk.context, suggest ); } while (chunk.next); return chunk; } run(argv) { const chunk = this.parse(argv); if (typeof chunk.action === 'function') { return chunk.action.call(null, chunk.context); } } clone(deep) { const clone = Object.create(Object.getPrototypeOf(this)); for (const [key, value] of Object.entries(this)) { clone[key] = value && typeof value === 'object' ? (value instanceof Map ? new Map(value) : Object.assign(Object.create(Object.getPrototypeOf(value)), value)) : value; } if (deep) { for (const [name, subcommand] of clone.commands.entries()) { clone.commands.set(name, subcommand.clone(deep)); } } return clone; } createOptionValues(values) { const storage = Object.create(null); for (const { name, normalize, default: value } of this.getOptions()) { if (typeof value !== 'undefined') { storage[name] = normalize(value); } } return Object.assign(new Proxy(storage, { set: (obj, key, value, reciever) => { const option = this.getOption(key); if (!option) { return true; // throw new Error(`Unknown option: "${key}"`); } const oldValue = obj[option.name]; const newValue = option.normalize(value, oldValue); const retValue = Reflect.set(obj, option.name, newValue); if (option.shortcut) { Object.assign(reciever, option.shortcut.call(null, newValue, oldValue)); } return retValue; } }), values); } // misc messageRef() { return `${this.usage}${this.params.args.map(arg => ` ${arg.name}`)}`; } getOption(name) { return this.options.get(name) || null; } getOptions() { return [...new Set(this.options.values())]; } getCommand(name) { return this.commands.get(name) || null; } getCommands() { return [...this.commands.values()]; } outputHelp(commandPath) { console.log(help(this, Array.isArray(commandPath) ? commandPath.slice(0, -1) : null)); } } module.exports = Command;