UNPKG

commandos

Version:

Command line parser, compatible with DOS style command

295 lines (259 loc) 7.9 kB
'use strict'; const { profileEnd } = require('console'); const MODULE_REQUIRE = 1 /* built-in */ , fs = require('fs') , os = require('os') , path = require('path') /* NPM */ , colors = require('colors') , if2 = require('if2') , meant = require('meant') , minimatch = require('minimatch') /* in-package */ , parse = require('./parse') , more = require('./lib/more') ; const SPACE = String.fromCharCode(32); const DASH = '-'; /** * Command entrance. * @param {string[]} argv - command line arguments * @param {Object} options - basic info about the command set. * * @param {string} options.name * @param {string} options.desc - command description * @param {string} options.commandDir - command description * @param {boolean} [options.useManon] * @param {Function} [options.afterRun] * @param {Function} [options.beforeRun] * @param {Function} [options.onError] * -- deprecated params -- * @param {string[]} options.names - command name(s) * @param {string} options.root - parent directory of basic 'command' directory */ async function run(argv, options) { let printManual = text => { if (options.useManon) { text = require('manon').format(text, 'console'); } more(text); }; let commandName = null; let commandBaseDir = null; if (options.names) { commandName = options.names.join(SPACE); commandBaseDir = `${options.root}/command`; for (let i = 1; i < options.names.length; i++) { commandBaseDir = `${commandBaseDir}/${options.names[i]}/command`; } } else { commandName = options.name; commandBaseDir = options.commandDir; } let subCommand = null; if (argv.length && !argv[0].startsWith(DASH)) { subCommand = argv.shift(); } /** * subCommand "help" is a virtual one. * Actually, "help foobar" is regarded as "foobar help". */ if (subCommand == 'help') { subCommand = argv[0]; argv[0] = '--help'; } const allSubCommandNames = fs.readdirSync(commandBaseDir); /** * Replace with alais if match. */ if (subCommand && !allSubCommandNames.includes(subCommand) && options.alias) { options.alias.find(couple => { let [ pesudo, target ] = couple; if (!Array.isArray(pesudo)) { pesudo = [ pesudo ]; } if (!Array.isArray(target)) { target = [ target ]; } let [ pesudoSubCommand, ...pesudoArgs ] = pesudo; let [ targetSubCommand, ...targetArgs ] = target; if (minimatch(subCommand, pesudoSubCommand) && argv.length >= pesudoArgs.length && pesudoArgs.every((s, index) => minimatch(argv[index], s)) ) { /** * Replace place holders. * 替换占位符。 */ targetArgs = targetArgs.map(arg => { return arg.replace(/\$(\d+)/g, (placeholder, num) => { return num == 0 ? subCommand : argv[num - 1]; }); }); argv = [].concat(targetArgs, argv.slice(pesudoArgs.length)); subCommand = targetSubCommand; return true; } }); } if (subCommand && !allSubCommandNames.includes(subCommand)) { let matching = allSubCommandNames.filter(name => name.startsWith(subCommand)); if (matching.length == 1) { subCommand = matching[0]; } else { console.error(colors.bold.yellow('-- WRANING --')); console.error(`Sub command not found: ${colors.italic.yellow(subCommand)}`); let similiars = meant(subCommand, allSubCommandNames); if (similiars.length > 0) { console.log(`Did you mean ${similiars.length == 1 ? 'this' : 'one of these'}?`); similiars.forEach(name => console.log(`- ${colors.italic(commandName)} ${colors.italic.blue(name)}`)); return; } subCommand = null; } } if (subCommand) { let subCommandBase = `${commandBaseDir}/${subCommand}`; if ((argv[0] == 'help' || argv.includes('--help') || argv.includes('-h')) && fs.existsSync(`${subCommandBase}/help.txt`)) { printManual(fs.readFileSync(`${commandBaseDir}/${subCommand}/help.txt`, 'utf8')); } else { // Command name. let name = subCommand; let def = null; try { def = require(`${subCommandBase}/options`); } catch (error) { // DO NOTHING. } if (def) { try { argv = parse.onlyArgs(argv, def); } catch (error) { console.log(error.message); process.exit(1); } } if (options.beforeRun) { await options.beforeRun({ name, argv, }); } /** * In generally, subCommandDir is a directory containing index.js, * options.js and help.txt. It can also be made up of `index.js` * and `commands` sub directory. * 通常,subCommandDir 是一个目录,它可以包含组成一个命令的若干要素文件。 * 如果子命令是一个命令簇,那么它也可能包含一个 commands 子目录。 */ let subCommandDir = path.join(commandBaseDir, subCommand); let subCommandEntrypoint = require(subCommandDir); let error = null; let result = null; if (typeof subCommandEntrypoint != 'function') { let options2 = { ...subCommandEntrypoint } ; /** * The command name is decided by parent commands. * 完整命令名取决于父命令。 */ options2.name = [ commandName, subCommand ].join(SPACE); /** * Inherit some action settings from parent command. * 从父命令继承部分行为配置。 */ options2.useManon = if2.defined(options2.useManon, options.useManon); if (!options2.commandDir) { let pathname = path.join(subCommandDir, 'command'); if (fs.existsSync(pathname)) { options2.commandDir = pathname; } } if (options2.commandDir) { await run(argv, options2); } else { error = new Error(`Sub command ${subCommand} is neither a function nor a command set.`); } } else { /** * The command function `subCommandEntrypoint()` may be a normal function, * or someone (e.g. an async function) returns a Promise instance. */ try { result = subCommandEntrypoint(argv); } catch (ex) { error = ex; } } if (result instanceof Promise) { result = await result.catch(ex => error = ex); } if (options.afterRun) { await options.afterRun({ name, argv, result, error, }); } else if (error) { process.exitCode = 1; console.error(error); } /** * @see https://tldp.org/LDP/abs/html/exitcodes.html * Exit Codes With Special Meanings */ } } // Display existing manual. else if (fs.existsSync(`${commandBaseDir}/help.txt`)) { printManual(fs.readFileSync(`${commandBaseDir}/help.txt`, 'utf8')); } // Generate and display manual. else { let manual = []; let L = line => manual.push(line); manual.push(''); manual.push('NAME'); manual.push(`\t${commandName} - ${options.desc}`); manual.push(''); manual.push('SYNOPSIS'); manual.push(`\t${commandName} help <sub-command-name>`); manual.push('\t# Show help info of specified sub command.'); manual.push(''); allSubCommandNames.forEach((name) => { name = name.replace(/\.js$/, ''); try { manual.push(`\t${commandName} ${name}`); let subCommandEntrypoint = require(`${commandBaseDir}/${name}`); if (subCommandEntrypoint.desc) { subCommandEntrypoint.desc.split(/[\r\n]+/).forEach(desc => { manual.push(`\t# ${desc}`); }); } manual.push(''); } catch (error) { // DO NOTHING. // Ignore invalid directory/file. } }); if (options.alias && options.alias.length > 0) { manual.push(''); manual.push('ALIAS'); options.alias.forEach(couple => { let newname = Array.isArray(couple[0]) ? couple[0].join(SPACE) : couple[0]; let oldname = Array.isArray(couple[1]) ? couple[1].join(SPACE) : couple[1]; manual.push(`\t* \`${commandName} ${newname}\` = \`${commandName} ${oldname}\``); }); manual.push(''); } printManual(manual.join(os.EOL)); } } module.exports = run;