Building an Extensible Node.js CLI Tool Using the Command Pattern
The article walks through creating a lightweight, extensible Node.js command‑line utility without third‑party libraries, covering the command‑pattern design, implementation of split, upper, and word‑count commands, dynamic command loading, and best practices for modular, maintainable CLI development on Linux.
Background
The article demonstrates building a packet‑sniffing‑style command‑line utility from scratch on Linux without third‑party libraries, focusing on the Node.js ecosystem in 2022.
What the tool does
A simple, extensible CLI is created that processes strings with three commands:
Split a string into words, optionally using a custom delimiter.
Convert a string to uppercase.
Count words in a string or count occurrences of a specific word.
Each command provides comprehensive help text and argument validation.
Tools used
The implementation relies on the commander package (https://www.npmjs.com/package/commander) for argument parsing. The code runs on Node.js and can be typed with TypeScript.
Command pattern
The Command design pattern is applied so that each operation is encapsulated in its own class (a “capsule”) that implements a common interface. This isolates business logic, enables reuse, and allows new commands to be added without modifying core client code.
Implementation – client code
const { Command } = require('commander');
const { commands } = require('./commands');
const program = new Command();
commands.forEach(c => {
const commandDef = c.definition();
const subCommand = program
.command(commandDef.command)
.description(commandDef.help);
commandDef.arguments.forEach(arg => {
subCommand.argument(arg[0], arg[1]);
});
commandDef.options.forEach(o => {
subCommand.option([o[0], o[1]].join(','), o[2], o[3]);
});
subCommand.action(function() {
c.action.apply(c, arguments);
console.log(c.getResult());
});
});
program.parse();The client imports commander and the list of command objects, registers each command with its arguments and options, and finally parses the command‑line input.
Base Command class
class Command {
constructor(name, descr, args = [], options = []) {
this.command = name;
this.help = descr;
this.arguments = args;
this.options = options;
this.result = "";
}
getResult() { return this.result; }
definition() {
return {
command: this.command,
help: this.help,
arguments: this.arguments,
options: this.options
};
}
action() { throw Error("This method needs to be implemented by each specific command"); }
}
module.exports.Command = Command;This abstract class defines the common shape of all commands and forces each concrete command to implement its own action method.
Example command – Word Counter
const { Command } = require('./Command');
module.exports = class WordCounterCommand extends Command {
constructor() {
super(
"wc",
"Counts the number of words in the given string",
[["<string>", "The string to analyze"]],
[
["-l", "--limit <char>", "The character that signals the end of a word, default is space", " "],
["-w", "--word <string>", "Counts the number of times this single word appears inside the string"]
]
);
}
_countWords(str, splitter) { return "Total words: " + str.split(splitter).length; }
_countSingleWord(str, w) {
const matches = str.match(new RegExp(w, 'g')).length;
let respStr = "The word " + w + " appears ";
if (matches == 0) return "The word " + w + " doesn't appear on the string";
if (matches == 1) respStr += "once inside the string"; else respStr += matches + " times in the string";
return respStr;
}
action(str, options) {
const splitter = options.limit;
if (!options.word) {
this.result = this._countWords(str, splitter);
} else {
this.result = this._countSingleWord(str, options.word);
}
}
};The command defines its constructor, two private helper methods ( _countWords and _countSingleWord), and the public action that implements the business logic based on the provided options.
Dynamic command loading
const normalizedPath = require("path").join(__dirname, "./");
let importedCommands = require("fs")
.readdirSync(normalizedPath)
.filter(file => file.match(/[a-zA-Z]+Command.js/))
.map(function(file) {
const c = require("./" + file);
return new c();
});
module.exports.commands = importedCommands;The index file scans the commands directory, imports every file whose name ends with Command.js, instantiates the class, and exports the array for the client code to consume. Adding a new command is as simple as dropping a correctly named file into the folder.
Extensibility
Because each command is isolated in its own class and the command list is built dynamically, extending the tool only requires adding a new command file. No changes to the core client code are necessary, keeping the impact on existing code minimal.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code DAO
We deliver AI algorithm tutorials and the latest news, curated by a team of researchers from Peking University, Shanghai Jiao Tong University, Central South University, and leading AI companies such as Huawei, Kuaishou, and SenseTime. Join us in the AI alchemy—making life better!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
