How to Create a Testable CLI using TypeScript?
Tags: typescript, tutorial, programming, computerscience
Published: 2023-01-30 05:18:41

> This article was originally posted on www.realpythonproject.com

<!-- wp:paragraph -->
<p>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</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><!-- wp:list-item -->
<li> command print a name</li>
<!-- /wp:list-item -->

<!-- wp:list-item -->
<li>command to perform basic arithmetic operations with unit tests</li>
<!-- /wp:list-item -->

<!-- wp:list-item -->
<li>command to make an API call</li>
<!-- /wp:list-item -->

<!-- wp:list-item -->
<li>We will also format our output by adding some color to it!</li>
<!-- /wp:list-item -->

<!-- wp:list-item -->
<li>Finally, we will use mocha and chai to write unit tests for our CLI functions</li>
<!-- /wp:list-item --></ul>
<!-- /wp:list -->

<!-- wp:heading -->
<h2>Setup</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Create a new folder and set up a project.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">mkdir my-cli
cd my-cli
npm init -y</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>This will create a new directory called <code>my-cli</code> and initialize it as a npm project with default settings</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Now, let's install some of the required packages</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">npm install typescript @types/node --save-dev
npm install commander</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p><a href="https://www.npmjs.com/package/commander">Commander.js</a> is an NPM package that makes it easier to build CLI tools. You can find its documentation <a href="https://github.com/tj/commander.js">over here</a></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Configure Typescript for your project by creating a <code>tsconfig.json</code> with the following</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="json" class="language-json">{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "dist",
    "strict": true
  },
  "include": ["src/**/*", "test/cli.spec.ts"]
}</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Create two folders <code>src</code> and <code>test</code>.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Inside the <code>src</code> folder, create a file named <code>index.ts</code> and add a Hello World Statment</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="typescript" class="language-typescript">console.log("Hello World!");</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>You can either compile the above file and use node to execute the result javascript file</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">npx tsc &amp;&amp; node ./dist/src/index.js</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>or alternatively you can install <code>tsnode</code> and directly execute the typescript file</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">npm install ts-node --save-dev

npx ts-node src/index.ts</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>I recommend installing ts-node since it'll be used when setting up mocha for unit testing.</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2>Commands</h2>
<!-- /wp:heading -->

<!-- wp:heading {"level":3} -->
<h3>Hello World Command</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Create a file named <code>utils.ts</code>, this will contain our utility functions </p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="typescript" class="language-typescript">export const printName = (name: string) =&gt; {
  console.log(`Hello, ${name || "World"}!`);
};</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>It accepts a string as an argument. If no string is provided, it simply outputs <code>Hello,World!</code>. If a string is provided, it will output the name <code>Hello,[name]!</code></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Instead of <code>console.log</code>, you can also use other methods such <code>console.error</code> and <code>console.warn</code>. In this tutorial, we will only work with <code>console.log</code>.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Now go back to <code>index.ts</code>. We will create an instance of <code>Commander</code> and register our first command.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="typescript" class="language-typescript">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);</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Update the name, description and version number of your CLI as needed. When registering a command, the following syntax is used</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">${command name} ${[arguement1]} ${[arguement2]} ${&lt;arguement3&gt;} ......</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>If an arguement/flag is required, it should be specified inside <code>&lt;..&gt;</code> and if it is optional, it should be specified inside <code>[...]</code> In this case, we want the arguement to be optional since we have a default output <code>Hello, World!</code>. You can alternatively register a command in the following way as well</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="typescript" class="language-typescript">program
    .command("Hello")
    .argument("[name]")
    .action(printName);

program.parse(process.argv);</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Next we will pass the <code>printName</code> function as a callback method.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Now run the following in your terminal to run the CLI</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">npx ts-node src/index.ts hello</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>or</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">npx ts-node src/index.ts hello Jake</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Now, let's add some color to the output</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>For this, we will have to install a npm package called <code><a href="https://www.npmjs.com/package/chalk" target="_blank" rel="noopener" title="">Chalk</a></code></p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">npm install chalk</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>The various color functions provided by <code>Chalk</code> 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 <code>console.log</code></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Go back to <code>utils.ts</code> and update the <code>printName</code> function as follows</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="typescript" class="language-typescript">import * as chalk from "chalk";

export const printName = (name: string) =&gt; {
  console.log(chalk.yellow(`Hello, ${name || "World"}!`));
};</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>This will print the output in yellow.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Try running the below command again to see the output</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">npx ts-node src/index.ts hello</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>You can even add the above command as a script in <code>package.json</code></p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="json" class="language-json">  "scripts": {
    "local-cli": "ts-node src/index.ts"
  }</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Now you could run <code>index.ts</code> with the following command</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">npm run local-cli</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>If you are using the script from package.json, you need to use -- before adding the command</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">npm run local-cli -- hello</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>or</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">npm run local-cli -- hello Jake</code></pre>
<!-- /wp:code -->

<!-- wp:heading {"level":3} -->
<h3>Command to Perform Operations on Numbers</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>First, let's write the a function to perform various operations</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="typescript" class="language-typescript">export const performOperation = (
  num1: number,
  num2: number,
  options: { operation: string }
) =&gt; {
  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));
};</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>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.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p> For this command we need 3 values, the 2 numbers and the operation.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="typescript" class="language-typescript">import {performOperation} from "./utils";

program
  .command("calculate &lt;num1&gt; &lt;num2&gt;")
  .option(
    "-o, --operation &lt;operation&gt;",
    "operation to perform: multiply, or divide"
  )
  .action(performOperation);

program.parse(process.argv);</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p> In the code above, we add an option for the operation. We can also provide a help message if the user uses <code>-h</code> flag.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Test the above using the following command</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">npm run local-cli -- calculate 1 2 -o multiply</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>The following command should return an error message</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">npm run local-cli -- calculate 1 2 -o add</code></pre>
<!-- /wp:code -->

<!-- wp:heading {"level":3} -->
<h3>Command to make an API call</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>To make the API call, we will be using <code>axios</code></p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">npm install axios</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>We will be using the <a href="https://agify.io/" target="_blank" rel="noopener" title="">agify API</a>, it tries to predict the age of a name. Below is the function to make a request to the API</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Like before, write it in the <code>utils.ts</code> file</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="typescript" class="language-typescript">import axios from "axios";

export const getAge = async (name: string) =&gt; {
  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"])} `);
};</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Registering the command is pretty straight forward</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="typescript" class="language-typescript">import {getAge} from "./utils";

program
    .command('age [name]')
    .action(getAge)

program.parse(process.argv);</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Test it using the below command</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">npm run local-cli -- age rahul</code></pre>
<!-- /wp:code -->

<!-- wp:heading -->
<h2>How to make the CLI Tool "Global"?</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>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 <code>tcli</code> to run it. Below is how you can add that functionality</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Add the following shebang line at the top of your <code>index.ts</code> or the file where you created an instance of <code>Commander</code></p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="typescript" class="language-typescript">#!/usr/bin/env node</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Now, update your <code>package.json</code> file by adding the following</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">  "bin": {
    "tcli": "./dist/src/index.js"
  }</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>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 <code>tcli</code> to run our CLI tool.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>You can also test it by installing your package globally </p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">npm i -g</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Now if you type <code>tcli</code> in your CLI, it should run your CLI tool</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">tcli hello</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>You can uninstall the package as well</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">npm uninstall typescriptcli -g</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>You can also update the <code>scripts</code> in <code>package.json</code> with some more helpful commands</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="json" class="language-json"> "scripts": {
    "install-cli": "npx tsc &amp;&amp; npm i -g",
    "uninstall-cli": "npm uninstall typescriptcli -g",
    "test-cli": "tcli hello",
  }</code></pre>
<!-- /wp:code -->

<!-- wp:heading -->
<h2>How to Test a CLI Tool?</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>We will use <code>mocha</code> and <code>chai</code> for unit testing. Install the required dependencies </p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="Python" class="language-Python">npm i mocha chai @types/mocha @types/chai --save-dev</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Create a folder called <code>test</code> and a file called <code>cli.spec.ts</code> inside it</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Before any of the unit tests are run, we need to install our package globally so that our unit tests can test the CLI</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="Python" class="language-Python">import { before, after, test } from "mocha";
import * as path from "path";
import * as childProcess from "child_process";

before(() =&gt; {
  childProcess.execSync(`npm install -g`, { cwd: path.join(__dirname, "..") });
  console.log("Installed CLI");
});

after(() =&gt; {
  childProcess.execSync(`npm uninstall -g`, { cwd: path.join(__dirname, "..") });
  console.log("Uninstalled CLI");
});</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>We will use <code>childProcess.execSync</code> to install and later uninstall the CLI. Now in the same file, let's add some unit tests to test the <code>hello</code> command and the <code>calculate</code> command</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="Python" class="language-Python">test("should print hello world", () =&gt; {
  // 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", () =&gt; {
  // 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", () =&gt; {
  // 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");
});</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>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 <code>age</code> command as well.</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2>Conclusion</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>A CLI is a powerful tool and I hope you found this tutorial useful.  Here a few other articles that you may find interesting</p>
<!-- /wp:paragraph -->

<!-- wp:embed {"url":"https://www.realpythonproject.com/what-is-axios-create-in-javascript/","type":"wp-embed","providerNameSlug":"software-development-for-everyone","align":"center"} -->
<figure class="wp-block-embed aligncenter is-type-wp-embed is-provider-software-development-for-everyone wp-block-embed-software-development-for-everyone"><div class="wp-block-embed__wrapper">
https://www.realpythonproject.com/what-is-axios-create-in-javascript/
</div></figure>
<!-- /wp:embed -->