WIP: Merge 'dev' into 'main' #1

Draft
ID19810919hisaDClr wants to merge 12 commits from dev into main
3 changed files with 162 additions and 71 deletions
Showing only changes of commit 64815d15f6 - Show all commits

72
log
View File

@ -11,57 +11,47 @@ class Log extends This {
super.init(); // initialize commander with overridden version and description super.init(); // initialize commander with overridden version and description
} }
echo(str) { echo(message) {
console.log(str); /**
* @command
* @argument('<message>')
*/
console.log(message);
} }
success(str) { success(message) {
console.log(this._echoInGreen(str)); /**
* @command
* @argument('<message>')
*/
console.log(this._echoInGreen(message));
} }
info(str) { info(message) {
console.log(`[INFO] ${str}`); /**
* @command
* @argument('<message>')
*/
console.log(`[INFO] ${message}`);
} }
warn(str) { warn(message) {
console.warn(`[WARN] ${this._echoInYellow(str)}`); /**
* @command
* @argument('<message>')
*/
console.warn(`[WARN] ${this._echoInYellow(message)}`);
} }
error(str) { error(message) {
console.error(`[ERR] ${this._echoInRed(str)}`); /**
* @command
* @argument('<message>')
*/
console.error(`[ERR] ${this._echoInRed(message)}`);
} }
start() { start() {
this.program.command('echo')
.argument('<message>')
.action(message => {
this.echo(message);
});
this.program.command('success')
.argument('<message>')
.action(message => {
this.success(message);
});
this.program.command('info')
.argument('<message>')
.action(message => {
this.info(message);
});
this.program.command('warn')
.argument('<message>')
.action(message => {
this.warn(message);
});
this.program.command('error')
.argument('<message>')
.action(message => {
this.error(message);
});
this.program.parse(); this.program.parse();
if (Object.keys(this.program.opts()).length || this.program.args.length) { if (Object.keys(this.program.opts()).length || this.program.args.length) {
@ -78,4 +68,4 @@ class Log extends This {
// main // main
const log = new Log(); const log = new Log();
log.start(); log.start();

61
pm
View File

@ -1,6 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
const This = require('./this'); const This = require('./this');
const util = require('util');
// Example of subcommands which are implemented as stand-alone executable files. // Example of subcommands which are implemented as stand-alone executable files.
// //
@ -17,21 +18,34 @@ class Pm extends This {
} }
install() { install() {
this.program.command('install [name]', 'install one or more packages').alias('i'); /**
* @command('install one or more packages')
* @alias('i')
*/
// Calls stand-alone excutable `pm-install` because of @command(<description>) in docstring
} }
search() { search() {
this.program.command('search [query]', 'search with optional query').alias('s'); /**
* @command('search with optional query')
* @alias('s')
*/
// Calls stand-alone excutable `pm-search` because of @command(<description>) in docstring
} }
update() { update() {
this.program.command('update', 'update installed packages', { /**
executableFile: 'myUpdateSubCommand', * @command('update installed packages')
}); * @executable('myUpdateSubCommand')
*/
// Calls stand-alone excutable `myUpdateSubCommand` because of @command(<description>) in docstring
} }
list() { list() {
this.program.command('list', 'list packages installed', { isDefault: false }); /**
* @command('list packages installed')
*/
// Calls stand-alone excutable `pm-list` because of @command(<description>) in docstring
} }
// override This.discovery() // override This.discovery()
@ -104,30 +118,21 @@ class Pm extends This {
} }
start() { start() {
// Usage this.program.parse();
const pm = new Pm();
// Output: Methods of Pm: [ 'methodOne', 'methodTwo' ]
// Properties of Pm: [ 'propertyOne', 'propertyTwo', 'version' ] and Version: 1.1.0
//pm.discovery();
// this.program if (Object.keys(this.program.opts()).length || this.program.args.length) {
// .name(this.scriptName) // Debugging commander options and arguments
// .version('0.0.1') const opts = util.inspect(this.program.opts(), { depth: null, colors: true, showHidden: true });
// .description('Fake package manager') const args = util.inspect(this.program.args, { depth: null, colors: true, showHidden: true });
// .command('install [name]', 'install one or more packages') this.execCmd(`${this.workingDir}/log echo 'Options: ${opts}'`);
// .alias('i') this.execCmd(`${this.workingDir}/log echo 'Remaining arguments: ${args}'`);
// .command('search [query]', 'search with optional query') } else {
// .alias('s') this.program.outputHelp();
// .command('update', 'update installed packages', { }
// executableFile: 'myUpdateSubCommand',
// })
// .command('list', 'list packages installed', { isDefault: false });
//this.program.parse();
// applies logDecorator to this.discovery() and binds it to 'this' to maintain the correct context // applies logDecorator to this.discovery() and binds it to 'this' to maintain the correct context
const logDecoratedDiscovery = this._logDecorator(this.discovery).bind(this); //const logDecoratedDiscovery = this._logDecorator(this.discovery).bind(this);
logDecoratedDiscovery(); //logDecoratedDiscovery();
// Try the following on macOS or Linux: // Try the following on macOS or Linux:
// ./examples/pm // ./examples/pm
@ -148,4 +153,4 @@ const pm = new Pm();
// logDecoratedDiscovery(); // logDecoratedDiscovery();
pm.start(); pm.start();
pm.selfTest(); //pm.selfTest();

100
this
View File

@ -7,7 +7,7 @@ const shell = require('shelljs');
class This { class This {
constructor() { constructor() {
this.version = '0.0.1'; // Default version, can be overridden in subclasses 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.description = 'This is the parent class all other scripts should extend.';
this.scriptName = path.parse(process.argv[1]).base; this.scriptName = path.parse(process.argv[1]).base;
this.workingDir = path.parse(process.argv[1]).dir; this.workingDir = path.parse(process.argv[1]).dir;
} }
@ -25,6 +25,11 @@ class This {
// Highlight errors in color. // Highlight errors in color.
outputError: (str, write) => write(this._echoInRed(str)), outputError: (str, write) => write(this._echoInRed(str)),
}); });
// dynamically add all methods with appropriate docstring to commander
this.listMethods().forEach((method) => {
this._parseDocStringToCmd(this[method]);
});
} }
getClassName() { getClassName() {
@ -90,11 +95,102 @@ class This {
Error.captureStackTrace(err, this._getCurrentFunctionName); Error.captureStackTrace(err, this._getCurrentFunctionName);
// Extract the function name from the stack trace // Extract the function name from the stack trace
const callerName = err.stack.split("\n")[1].trim().split(" ")[1]; const callerName = err.stack.split('\n')[1].trim().split(' ')[1];
return callerName; return callerName;
} }
// Utility function to extract docstring from a function, e.g.
//
// echo(str) {
// /**
// @command
// @argument('<message>')
// */
// 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 commandMatch = docString.match(/@command(?:\('([^']*)'\))?/);
const aliasMatch = docString.match(/@alias\('(.+?)'\)/);
const argumentsMatch = /@argument\('(.+?)'\)/g;
const optionsMatch = /@option\('(.+?)'\)/g;
const defaultMatch = docString.match(/@default/);
const executableMatch = docString.match(/@executable\('(.+?)'\)/);
if (!commandMatch) {
// No @command tag found
return;
}
const commandName = fn.name;
const commandDescription = commandMatch ? commandMatch[1] : '';
const alias = aliasMatch ? aliasMatch[1] : '';
const defaultCommand = defaultMatch ? true : false;
const executable = executableMatch ? executableMatch[1] : '';
// Generate the commander statement
// Get the function reference
const func = this[commandName].bind(this);
// Create a command
// 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) {
command = this.program.command(commandName, commandDescription, {
isDefault: defaultCommand,
executableFile: executable,
});
if (alias) {
command.alias(alias);
}
} else {
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
let argMatch;
while ((argMatch = argumentsMatch.exec(docString)) !== null) {
command.argument(argMatch[1]);
}
// If the commandName expects options, we add them here
let optMatch;
while ((optMatch = optionsMatch.exec(docString)) !== null) {
command.option(optMatch[1]);
}
}
_addMethodsToClass(cls, metadata) {
if (cls) {
cls.commands.push(metadata);
}
}
// Higher-order function to decorate other functions and provide logging // Higher-order function to decorate other functions and provide logging
_logDecorator(fn) { _logDecorator(fn) {
return function (...args) { return function (...args) {