285 lines
9.1 KiB
JavaScript
285 lines
9.1 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { Command } = require('commander');
|
|
const shell = require('shelljs');
|
|
const util = require('util');
|
|
|
|
class This {
|
|
constructor() {
|
|
this.version = '0.0.1'; // Default version, can be overridden in subclasses
|
|
this.description = 'This is the parent class all other scripts should extend.';
|
|
this.scriptName = path.parse(process.argv[1]).base;
|
|
this.workingDir = path.parse(process.argv[1]).dir;
|
|
this.debugLevel = 0;
|
|
}
|
|
|
|
init() {
|
|
// implement commander abilities
|
|
this.program = new Command()
|
|
.name(this.scriptName)
|
|
.version(this.version)
|
|
.description(this.description)
|
|
.configureOutput({
|
|
// Visibly override write routines as example!
|
|
//writeOut: (str) => process.stdout.write(`[OUT] ${str}`),
|
|
//writeErr: (str) => process.stdout.write(`[ERR] ${str}`),
|
|
// Highlight errors in color.
|
|
outputError: (str, write) => write(this._echoInRed(str)),
|
|
});
|
|
|
|
// dynamically add all methods with appropriate docstring to commander
|
|
let cmds = [];
|
|
this.listMethods().forEach((method) => {
|
|
let cmd = this._parseDocStringToCmd(this[method]);
|
|
// exclude start function as commandline cmd, because its the scripts main entry point
|
|
if (cmd && cmd != 'start') {
|
|
cmds.push(cmd);
|
|
}
|
|
});
|
|
|
|
// add functions for shell completion
|
|
this.program
|
|
.command('compgen')
|
|
.option('-s, --shell <name>', 'You can select between [bash]', 'bash')
|
|
.action((arg) => {
|
|
console.log(cmds.join(os.EOL));
|
|
return process.exit();
|
|
});
|
|
}
|
|
|
|
getClassName() {
|
|
return this.constructor.name;
|
|
}
|
|
|
|
listMethods() {
|
|
const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this)).filter(
|
|
(prop) => typeof this[prop] === 'function' && prop !== 'constructor' && !prop.startsWith('_')
|
|
);
|
|
return methods;
|
|
}
|
|
|
|
listProperties() {
|
|
const properties = Object.keys(this).filter((prop) => typeof this[prop] !== 'function');
|
|
return properties;
|
|
}
|
|
|
|
discover() {
|
|
const methods = util.inspect(this.listMethods(), { depth: null, colors: true, showHidden: true });
|
|
const properties = util.inspect(this.listProperties(), { depth: null, colors: true, showHidden: true });
|
|
this.execCmd(
|
|
`${this.workingDir}/log echo 'My name is "${this.getClassName()}" and I have the version: ${this.version}'`
|
|
);
|
|
this.execCmd(`${this.workingDir}/log echo 'My methods are: ${methods}'`);
|
|
this.execCmd(`${this.workingDir}/log echo 'My properties are: ${properties}'`);
|
|
}
|
|
|
|
// Method to create a context manager for this instance
|
|
withContext(callback) {
|
|
callback(this);
|
|
}
|
|
|
|
start() {
|
|
/**
|
|
* @program()
|
|
*/
|
|
this.program.exitOverride();
|
|
|
|
try {
|
|
this.program.parse();
|
|
|
|
if (this.debugLevel == 0) {
|
|
process.exit(1);
|
|
}
|
|
} catch (err) {
|
|
// this.execCmd(`${this.workingDir}/log echo '\n'`);
|
|
// this.execCmd(`${this.workingDir}/log error '${err}'`);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (Object.keys(this.program.opts()).length || this.program.args.length) {
|
|
// Debugging commander options and arguments
|
|
const opts = util.inspect(this.program.opts(), { depth: null, colors: true, showHidden: true });
|
|
const args = util.inspect(this.program.args, { depth: null, colors: true, showHidden: true });
|
|
this.execCmd(`${this.workingDir}/log echo '\n'`);
|
|
this.execCmd(`${this.workingDir}/log echo 'Options: ${opts}'`);
|
|
this.execCmd(`${this.workingDir}/log echo 'Remaining arguments: ${args}'`);
|
|
}
|
|
}
|
|
|
|
execCmd(cmd) {
|
|
// Run external tool synchronously
|
|
if (shell.exec(cmd).code !== 0) {
|
|
shell.echo('[ERR] ' + this._echoInRed(`Command '${cmd}' failed`));
|
|
shell.exit(1);
|
|
}
|
|
}
|
|
|
|
_echoInRed(str) {
|
|
// Add ANSI escape codes to display text in red.
|
|
return `\x1b[31m${str}\x1b[0m`;
|
|
}
|
|
|
|
_echoInYellow(str) {
|
|
// Add ANSI escape codes to display text in yellow.
|
|
return `\x1b[33m${str}\x1b[0m`;
|
|
}
|
|
|
|
_echoInGreen(str) {
|
|
// Add ANSI escape codes to display text in green.
|
|
return `\x1b[32m${str}\x1b[0m`;
|
|
}
|
|
|
|
_getCurrentFunctionName() {
|
|
// Create an Error object (but don't throw it)
|
|
const err = new Error();
|
|
|
|
// Extract the current stack trace
|
|
Error.captureStackTrace(err, this._getCurrentFunctionName);
|
|
|
|
// Extract the function name from the stack trace
|
|
const callerName = err.stack.split('\n')[1].trim().split(' ')[1];
|
|
|
|
return callerName;
|
|
}
|
|
|
|
// Utility function to extract docstring from a function, e.g.
|
|
//
|
|
// start() {
|
|
// /**
|
|
// * @program()
|
|
// * @usage([options] [command])
|
|
// * @option('-d, --debug', 'Enable debug mode')
|
|
// ..
|
|
// }
|
|
//
|
|
//
|
|
// echo(str) {
|
|
// /**
|
|
// * @program()
|
|
// * @command()
|
|
// * @argument('<message>')
|
|
// * @option('-c, --color <name>', 'Prints in color mode')
|
|
// */
|
|
// console.log(str);
|
|
// }
|
|
//
|
|
// Attention:
|
|
// In JavaScript, when a script is executed as a bash script with a shebang (#!/usr/bin/env node),
|
|
// the toString method on functions does not include comments preceding the function definition.
|
|
_getFunctionDocString(fn) {
|
|
const fnStr = fn.toString();
|
|
const docStringMatch = fnStr.match(/\/\*[\s\S]*?\*\//);
|
|
return docStringMatch ? docStringMatch[0] : null;
|
|
}
|
|
|
|
// Parser function to convert docstring to commander statement
|
|
_parseDocStringToCmd(fn) {
|
|
const docString = this._getFunctionDocString(fn);
|
|
if (!docString) {
|
|
// No docstring found
|
|
return;
|
|
}
|
|
|
|
// Extract command and arguments
|
|
const programRegex = /@program\(\)/;
|
|
const usageRegex = /@usage\('(.+?)'\)/;
|
|
const commandRegex = /@command(?:\('(.+?)'\))?/;
|
|
const aliasRegex = /@alias\('(.+?)'\)/;
|
|
const argumentsRegex = /@argument\('(.+?)'\)/g;
|
|
const optionsRegex = /@option\('(?<flag>.+?)',\s*'(?<description>.+?)'(?:,\s*'(?<defaultValue>.+?)')?\)/g;
|
|
const defaultRegex = /@default/;
|
|
const executableRegex = /@executable\('(.+?)'\)/;
|
|
|
|
// get regex matches with capture groups
|
|
const programMatch = docString.match(programRegex);
|
|
const usageMatch = docString.match(usageRegex);
|
|
const commandMatch = docString.match(commandRegex);
|
|
const aliasMatch = docString.match(aliasRegex);
|
|
const defaultMatch = docString.match(defaultRegex);
|
|
const executableMatch = docString.match(executableRegex);
|
|
|
|
if (!programMatch) {
|
|
// No @program() tag found
|
|
return;
|
|
}
|
|
|
|
if (commandMatch) {
|
|
// Generate the commander statement
|
|
const commandName = fn.name;
|
|
const commandDescription = commandMatch ? commandMatch[1] : '';
|
|
const alias = aliasMatch ? aliasMatch[1] : '';
|
|
const defaultCommand = defaultMatch ? true : false;
|
|
const executable = executableMatch ? executableMatch[1] : '';
|
|
|
|
// Get the function reference
|
|
const func = this[commandName].bind(this);
|
|
|
|
// Create a command
|
|
// See https://github.com/tj/commander.js?tab=readme-ov-file#quick-start
|
|
// When `.command()` is invoked with a description argument,
|
|
// this tells Commander that you're going to use a stand-alone executable for the subcommand.
|
|
let command;
|
|
if (commandDescription) {
|
|
// build stand-alone command
|
|
command = this.program.command(commandName, commandDescription, {
|
|
isDefault: defaultCommand,
|
|
executableFile: executable,
|
|
});
|
|
|
|
if (alias) {
|
|
command.alias(alias);
|
|
}
|
|
} else {
|
|
// build command with function name as action
|
|
command = this.program.command(commandName, { isDefault: defaultCommand });
|
|
|
|
// Set the action for the command
|
|
command.action((arg) => {
|
|
func(arg);
|
|
});
|
|
}
|
|
|
|
// If the commandName expects arguments, we add them here
|
|
(docString.match(argumentsRegex) || [])
|
|
.map((e) => e.replace(argumentsRegex, '$1'))
|
|
.forEach((arg) => {
|
|
command.argument(arg);
|
|
});
|
|
|
|
// If the commandName expects options, we add them here
|
|
// See https://github.com/tj/commander.js?tab=readme-ov-file#options
|
|
for (const match of docString.matchAll(optionsRegex)) {
|
|
command.option(match.groups['flag'], match.groups['description'], match.groups['defaultValue']);
|
|
}
|
|
} else {
|
|
// Add usage
|
|
if (usageMatch) {
|
|
this.program.usage(usageMatch[1]);
|
|
}
|
|
|
|
// If the program expects options, we add them here
|
|
// See https://github.com/tj/commander.js?tab=readme-ov-file#options
|
|
for (const match of docString.matchAll(optionsRegex)) {
|
|
this.program.option(match.groups['flag'], match.groups['description'], match.groups['defaultValue']);
|
|
}
|
|
}
|
|
|
|
// return name of function, if docstring could be parsed successfully
|
|
return fn.name;
|
|
}
|
|
|
|
// Higher-order function to decorate other functions and provide logging
|
|
_logDecorator(fn) {
|
|
return function (...args) {
|
|
console.log(`Calling ${fn.name} with arguments:`, args);
|
|
const result = fn.apply(this, args); // Use apply to maintain context
|
|
console.log(`Result of ${fn.name}:`, result);
|
|
return result;
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = This;
|