Command Line Applications are very useful for developers. We can interact with them by executing specific commands in the terminal, giving us much control and flexibility. You probably already interact with various CLI applications, such as Git or NPM.
Interestingly, we can also use NestJS to create a Command Line Interface (CLI) application. In this article, we learn what we can do with CLI apps and how to implement one with NestJS and the Nest Commander.
Command Line Interface applications
A straightforward example of a CLI tool is cp, which is used to copy files.
1cp file.txt copiedFile.txtLet’s break it down:
- cp is the command name
- file.txt copiedFile.txt are arguments. The first one is the file name we want to copy, and the second is the destination.
Many options are configurable through options.
1cp -R directoryToCopy copiedDirectoryIn the above example, -R is an option. It means we want to copy a folder with all its contents recursively.
In some cases, options can have their own arguments as well. For example, the following command makes a GET request and displays the result in the terminal:
1curl https://www.google.comHowever, we can use the -o option to store the result of the request in the file. If we do that, we need to provide an additional argument with the name of the output file.
1curl -o output.html https://www.google.comIt is worth noticing that options are prefixed with a single dash if they use a shorthand or an abbreviated version of the option. On the other hand, options prefixed with two dashes are usually followed by a whole word or multiple words. For example, we can use --output instead of -o when using curl.
Creating CLI applications with NestJS
To create a CLI application with NestJS, let’s first create a new NestJS project.
1npx @nestjs/cli new nestjs-cliThe @nestjs/cli is a CLI tool created by the NestJS team.
We also need to install the Nest Commander library.
1npm install nest-commanderFirst, we need to adjust our bootstrap method in the main.ts file.
1import { AppModule } from './app.module';
2import { CommandFactory } from 'nest-commander';
3
4async function bootstrap() {
5 await CommandFactory.run(AppModule);
6}
7
8bootstrap();We can now create our first simple command. To do that, we need to create a class that extends the CommandRunner and has the run method.
1import { Command, CommandRunner } from 'nest-commander';
2
3@Command({ name: 'get-date', description: 'Prints the date' })
4export class GetDateCommand extends CommandRunner {
5 async run() {
6 const currentDate = new Date();
7 console.log(currentDate.toLocaleDateString());
8 }
9}We mark the run method with async because it needs to return a promise.
Above, we create the current-date command that prints the current date.
We must also add our class to the providers array in our module.
1import { Module } from '@nestjs/common';
2import { GetDateCommand } from './get-date/get-date.command';
3
4@Module({
5 providers: [GetDateCommand],
6})
7export class AppModule {}Running our command
To test our command, we must build our NestJS application and run it with Node.js. We can simplify this process by creating an appropriate script in our package.json file.
1{
2 "scripts": {
3 // ...
4 "start": "nest build && node dist/main.js"
5 },
6 // ...
7}We can now run the start script and provide the command name we want to run.
Using arguments
Let’s change our code so that the get-date command accepts a date it should format.
1import { Command, CommandRunner } from 'nest-commander';
2
3@Command({ name: 'get-date', description: 'Prints the date' })
4export class GetDateCommand extends CommandRunner {
5 parseDate(date?: string) {
6 if (date) {
7 return new Date(date);
8 }
9 return new Date();
10 }
11
12 async run(parameters: string[]) {
13 const parsedDate = this.parseDate(parameters[0]);
14 console.log(parsedDate.toLocaleDateString());
15 }
16}Any argument we pass to our command is available through the first argument in the run method.
There is a catch, though. Since we use npm to run our script, we must use the -- separator before providing the arguments.
Using options
Let’s handle an option that signifies that we want to include the time in the output. To do that, we need to use the @Option decorator.
1import { Command, CommandRunner, Option } from 'nest-commander';
2
3interface GetDateOptions {
4 time?: boolean;
5}
6
7@Command({ name: 'get-date', description: 'Prints the date' })
8export class GetDateCommand extends CommandRunner {
9 parseDate(date?: string) {
10 if (date) {
11 return new Date(date);
12 }
13 return new Date();
14 }
15
16 formatDate(parsedDate: Date, shouldIncludeTime: boolean) {
17 if (shouldIncludeTime) {
18 return `${parsedDate.toLocaleDateString()} ${parsedDate.toLocaleTimeString()}`;
19 }
20 return parsedDate.toLocaleDateString();
21 }
22
23 async run(parameters: string[], options: GetDateOptions) {
24 const parsedDate = this.parseDate(parameters[0]);
25 const formattedDate = this.formatDate(parsedDate, options.time);
26 console.log(formattedDate);
27 }
28
29 @Option({
30 flags: '-t, --time',
31 description: 'Means that the output should include time',
32 })
33 parseTimeOption() {}
34}We can provide additional arguments related to our option. For example, let’s allow the users to provide the timezone offset they want to use with the date.
All arguments we pass to our commands are treated as strings. The @Option is a method decorator, and we can use that to parse the argument from a string to a number.
1import { Command, CommandRunner, Option } from 'nest-commander';
2
3interface GetDateOptions {
4 time?: boolean;
5 timezoneOffsetInMinutes?: number;
6}
7
8@Command({ name: 'get-date', description: 'Prints the date' })
9export class GetDateCommand extends CommandRunner {
10 parseDate(date?: string) {
11 if (date) {
12 return new Date(date);
13 }
14 return new Date();
15 }
16
17 processTimezoneOffset(parsedDate: Date, timezoneOffset?: number) {
18 if (timezoneOffset) {
19 return new Date(parsedDate.getTime() + timezoneOffset * 60000);
20 }
21 return parsedDate;
22 }
23
24 formatDate(parsedDate: Date, shouldIncludeTime?: boolean) {
25 if (shouldIncludeTime) {
26 return `${parsedDate.toLocaleDateString()} ${parsedDate.toLocaleTimeString()}`;
27 }
28 return parsedDate.toLocaleDateString();
29 }
30
31 async run(parameters: string[], options: GetDateOptions) {
32 const parsedDate = this.parseDate(parameters[0]);
33 const dateWithTimezoneOffset = this.processTimezoneOffset(
34 parsedDate,
35 options.timezoneOffsetInMinutes,
36 );
37 const formattedDate = this.formatDate(dateWithTimezoneOffset, options.time);
38 console.log(formattedDate);
39 }
40
41 @Option({
42 flags: '-t, --time',
43 description: 'Means that the output should include time',
44 })
45 parseTimeOption() {}
46
47 @Option({
48 flags: '-z, --timezone-offset-in-minutes [timezoneOffsetInMinutes]',
49 description: 'The timezone offset in minutes',
50 })
51 parseTimezoneOffsetOption(value: string) {
52 return Number(value);
53 }
54}Thanks to that, we can provide the number of minutes we want to add or subtract from the provided date.
Summary
In this article, we’ve gone through how command-line interface applications work and how to control them with arguments and options. We also learned how to implement a CLI application using NestJS and the Nest Commander. With the right approach, command-line interface applications can be straightforward, practical, and valuable tools for developers.