NodeJS can be very useful when it comes to building Command-line Interfaces also known as CLI's.
In this post, I'll teach you how to build a CLI that asks some questions and creates a file, based on the answers.
Getting started
Let's start by creating a brand new npm package
mkdir my-script
cd my-script
npm init
NPM will ask some questions. After that, we need to install some packages.
npm install --save chalk figlet inquirer shelljs
What these packages do:
- chalk - Terminal string styling done right
- figlet - Figlet is a program for making large letters out of ordinary text
- inquirer - A collection of common interactive command line user interfaces
- shelljs - Portable Unix shell commands for Node.js
index.js file
Now create a index.js
file with the following content:
#!/usr/bin/env node
const inquirer = require("inquirer");
const chalk = require("chalk");
const figlet = require("figlet");
const shell = require("shelljs");
Planning the CLI
it's always good to plan what a CLI needs to do before writing any code.
This CLI will do just one thing: Create a file.
It should ask a couple of questions and after that, show a success message with the created file path.
The questions are: what is the filename and what is the extension.
// index.js
const run = async () => {
// show script introduction
// ask questions
// create the file
// show success message
};
run();
The first function is the script introduction. Let's use chalk
and figlet
to get the job done.
const init = () => {
console.log(
chalk.green(
figlet.textSync("Node f*cking JS", {
font: "Ghost",
horizontalLayout: "default",
verticalLayout: "default"
})
)
);
}
const run = async () => {
// show script introduction
init();
// ask questions
// create the file
// show success message
};
run();
Now it's time to write a function that asks questions.
const askQuestions = () => {
const questions = [
{
name: "FILENAME",
type: "input",
message: "What is the name of the file without extension?"
},
{
type: "list",
name: "EXTENSION",
message: "What is the file extension?",
choices: [".rb", ".js", ".php", ".css"],
filter: function(val) {
return val.split(".")[1];
}
}
];
return inquirer.prompt(questions);
};
// ...
const run = async () => {
// show script introduction
init();
// ask questions
const answers = await askQuestions();
const { FILENAME, EXTENSION } = answers;
// create the file
// show success message
};
Notice the constants FILENAME and EXTENSIONS that came from inquirer
.
The next step is to create the file.
const createFile = (filename, extension) => {
const filePath = `${process.cwd()}/${filename}.${extension}`
shell.touch(filePath);
return filePath;
};
// ...
const run = async () => {
// show script introduction
init();
// ask questions
const answers = await askQuestions();
const { FILENAME, EXTENSION } = answers;
// create the file
const filePath = createFile(FILENAME, EXTENSION);
// show success message
};
And last but not least, show the success message along with the file path.
const success = (filepath) => {
console.log(
chalk.white.bgGreen.bold(`Done! File created at ${filepath}`)
);
};
// ...
const run = async () => {
// show script introduction
init();
// ask questions
const answers = await askQuestions();
const { FILENAME, EXTENSION } = answers;
// create the file
const filePath = createFile(FILENAME, EXTENSION);
// show success message
success(filePath);
};
Let's test the script by running node index.js
.
Yay! And here is the final code:
Final code
#!/usr/bin/env node
const inquirer = require("inquirer");
const chalk = require("chalk");
const figlet = require("figlet");
const shell = require("shelljs");
const init = () => {
console.log(
chalk.green(
figlet.textSync("Node f*cking JS", {
font: "Ghost",
horizontalLayout: "default",
verticalLayout: "default"
})
)
);
};
const askQuestions = () => {
const questions = [
{
name: "FILENAME",
type: "input",
message: "What is the name of the file without extension?"
},
{
type: "list",
name: "EXTENSION",
message: "What is the file extension?",
choices: [".rb", ".js", ".php", ".css"],
filter: function(val) {
return val.split(".")[1];
}
}
];
return inquirer.prompt(questions);
};
const createFile = (filename, extension) => {
const filePath = `${process.cwd()}/${filename}.${extension}`
shell.touch(filePath);
return filePath;
};
const success = filepath => {
console.log(
chalk.white.bgGreen.bold(`Done! File created at ${filepath}`)
);
};
const run = async () => {
// show script introduction
init();
// ask questions
const answers = await askQuestions();
const { FILENAME, EXTENSION } = answers;
// create the file
const filePath = createFile(FILENAME, EXTENSION);
// show success message
success(filePath);
};
run();
To execute this script anywhere add a bin section in your package.json
file and run npm link
{
"name": "creator",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"chalk": "^2.4.1",
"figlet": "^1.2.0",
"inquirer": "^6.0.0",
"shelljs": "^0.8.2"
},
"bin": {
"creator": "./index.js"
}
}
$ npm link
$ creator
Hope it helps :)
Top comments (4)
Great tutorial Hugo. There are also many people writing CLI tools using Go, have you ever built any CLI tool using Go? Any thoughts?
Thanks Lucas.
Yes, Go is also amazing on building CLIs! Never tried before but it should be really fast and easy to work with files.
Maybe in a near future!
Any tips?
Great tutorial!
Another nice thing to do is using argv (npmjs.org/package/argv) in order to allow non-interactive or silence execution by providing the answers to the questions through command line arguments
This is super awesome dude!
I wanted it to stay alive and all I had to do was make the run function recurse.
This is great work, keep it up