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.

Code DAO
Code DAO
Code DAO
Building an Extensible Node.js CLI Tool Using the Command Pattern

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Design PatternscliJavaScriptNode.jsLinuxCommanderCommand Pattern
Code DAO
Written by

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!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.