This article was originally posted on www.realpythonproject.com
A CLI is a tool that allows users to interact with a computer program through the command line. It can perform various tasks, such as running commands, executing scripts, and interacting with other programs. This tutorial will teach you how to build a simple CLI in TypeScript. We will create a CLI that has the following commands
command print a name
command to perform basic arithmetic operations with unit tests
command to make an API call
We will also format our output by adding some color to it!
Finally, we will use mocha and chai to write unit tests for our CLI functions
Setup
Create a new folder and set up a project.
mkdir my-cli
cd my-cli
npm init -y
This will create a new directory called my-cli
and initialize it as a npm project with default settings
Now, let's install some of the required packages
npm install typescript @types/node --save-dev
npm install commander
Commander.js is an NPM package that makes it easier to build CLI tools. You can find its documentation over here
Configure Typescript for your project by creating a tsconfig.json
with the following
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "dist",
"strict": true
},
"include": ["src/**/*", "test/cli.spec.ts"]
}
Create two folders src
and test
.
Inside the src
folder, create a file named index.ts
and add a Hello World Statment
console.log("Hello World!");
You can either compile the above file and use node to execute the result javascript file
npx tsc && node ./dist/src/index.js
or alternatively you can install tsnode
and directly execute the typescript file
npm install ts-node --save-dev
npx ts-node src/index.ts
I recommend installing ts-node since it'll be used when setting up mocha for unit testing.
Commands
Hello World Command
Create a file named utils.ts
, this will contain our utility functions
export const printName = (name: string) => {
console.log(`Hello, ${name || "World"}!`);
};
It accepts a string as an argument. If no string is provided, it simply outputs Hello,World!
. If a string is provided, it will output the name Hello,[name]!
Instead of console.log
, you can also use other methods such console.error
and console.warn
. In this tutorial, we will only work with console.log
.
Now go back to index.ts
. We will create an instance of Commander
and register our first command.
import { printName } from "./utils";
import { Command } from "commander";
const program = new Command();
program
.name("tcli")
.description("Learn how to build a CLI using TypeScript")
.version("1.0.0");
program
.command("hello [name]")
.action(printName);
Update the name, description and version number of your CLI as needed. When registering a command, the following syntax is used
${command name} ${[arguement1]} ${[arguement2]} ${<arguement3>} ......
If an arguement/flag is required, it should be specified inside <..>
and if it is optional, it should be specified inside [...]
In this case, we want the arguement to be optional since we have a default output Hello, World!
. You can alternatively register a command in the following way as well
program
.command("Hello")
.argument("[name]")
.action(printName);
program.parse(process.argv);
Next we will pass the printName
function as a callback method.
Now run the following in your terminal to run the CLI
npx ts-node src/index.ts hello
or
npx ts-node src/index.ts hello Jake
Now, let's add some color to the output
For this, we will have to install a npm package called Chalk
npm install chalk
The various color functions provided by Chalk
take a string as an argument and return a string, so we basically need to wrap our strings in one of Chalk's functions and can directly use it with console.log
Go back to utils.ts
and update the printName
function as follows
import * as chalk from "chalk";
export const printName = (name: string) => {
console.log(chalk.yellow(`Hello, ${name || "World"}!`));
};
This will print the output in yellow.
Try running the below command again to see the output
npx ts-node src/index.ts hello
You can even add the above command as a script in package.json
"scripts": {
"local-cli": "ts-node src/index.ts"
}
Now you could run index.ts
with the following command
npm run local-cli
If you are using the script from package.json, you need to use -- before adding the command
npm run local-cli -- hello
or
npm run local-cli -- hello Jake
Command to Perform Operations on Numbers
First, let's write the a function to perform various operations
export const performOperation = (
num1: number,
num2: number,
options: { operation: string }
) => {
let result: number;
switch (options.operation) {
case "multiply":
result = num1 * num2;
break;
case "divide":
result = num1 / num2;
break;
default:
console.error(chalk.red("Invalid operation"));
process.exit(1);
}
console.log(chalk.green(result));
};
It is a pretty simple function, it accepts a couple of numbers and the operation to perform. It has a switch case block to perform either multiplication or division. If any other operation is provided, it prints an error message in red and exits.
For this command we need 3 values, the 2 numbers and the operation.
import {performOperation} from "./utils";
program
.command("calculate <num1> <num2>")
.option(
"-o, --operation <operation>",
"operation to perform: multiply, or divide"
)
.action(performOperation);
program.parse(process.argv);
In the code above, we add an option for the operation. We can also provide a help message if the user uses -h
flag.
Test the above using the following command
npm run local-cli -- calculate 1 2 -o multiply
The following command should return an error message
npm run local-cli -- calculate 1 2 -o add
Command to make an API call
To make the API call, we will be using axios
npm install axios
We will be using the agify API, it tries to predict the age of a name. Below is the function to make a request to the API
Like before, write it in the utils.ts
file
import axios from "axios";
export const getAge = async (name: string) => {
const response = await axios.get(`https://api.agify.io/?name=${name}`);
const data = await response.data;
console.log(`${chalk.yellow(data["name"])} - ${chalk.gray(data["age"])} `);
};
Registering the command is pretty straight forward
import {getAge} from "./utils";
program
.command('age [name]')
.action(getAge)
program.parse(process.argv);
Test it using the below command
npm run local-cli -- age rahul
How to make the CLI Tool "Global"?
Right now, we have to manually run the script to run our CLI Tool but this is not optimal. We want users to be able to install it as an npm package and then use a command like tcli
to run it. Below is how you can add that functionality
Add the following shebang line at the top of your index.ts
or the file where you created an instance of Commander
#!/usr/bin/env node
Now, update your package.json
file by adding the following
"bin": {
"tcli": "./dist/src/index.js"
}
They key is what the user would have the call to run the CLI and the value is the path to the compiled javascript file. In this case, the user would have to type tcli
to run our CLI tool.
You can also test it by installing your package globally
npm i -g
Now if you type tcli
in your CLI, it should run your CLI tool
tcli hello
You can uninstall the package as well
npm uninstall typescriptcli -g
You can also update the scripts
in package.json
with some more helpful commands
"scripts": {
"install-cli": "npx tsc && npm i -g",
"uninstall-cli": "npm uninstall typescriptcli -g",
"test-cli": "tcli hello",
}
How to Test a CLI Tool?
We will use mocha
and chai
for unit testing. Install the required dependencies
npm i mocha chai @types/mocha @types/chai --save-dev
Create a folder called test
and a file called cli.spec.ts
inside it
Before any of the unit tests are run, we need to install our package globally so that our unit tests can test the CLI
import { before, after, test } from "mocha";
import * as path from "path";
import * as childProcess from "child_process";
before(() => {
childProcess.execSync(`npm install -g`, { cwd: path.join(__dirname, "..") });
console.log("Installed CLI");
});
after(() => {
childProcess.execSync(`npm uninstall -g`, { cwd: path.join(__dirname, "..") });
console.log("Uninstalled CLI");
});
We will use childProcess.execSync
to install and later uninstall the CLI. Now in the same file, let's add some unit tests to test the hello
command and the calculate
command
test("should print hello world", () => {
// Run the hello command and capture the output
const output = childProcess.execSync(`tcli hello`, {
encoding: "utf8",
});
// Check that the output is correct
expect(output).to.equal("Hello, World!\n");
});
test("should print a greeting", () => {
// Run the hello command and capture the output
const output = childProcess.execSync(`tcli Hello Rahul`, {
encoding: "utf8",
});
// Check that the output is correct
expect(output).to.equal("Hello, Rahul!\n");
});
test("should perform the specified operation", () => {
// Run the calculate command and capture the output
const output = childProcess.execSync(`tcli calculate 3 4 -o multiply`, {
encoding: "utf8",
});
// Check that the output is correct
expect(output).to.equal("12\n");
});
This will only check the string value output in the console, it doesn't check the color of the output. Similarly you add an unit test for the age
command as well.
Conclusion
A CLI is a powerful tool and I hope you found this tutorial useful. Here a few other articles that you may find interesting
https://www.realpythonproject.com/what-is-axios-create-in-javascript/