Command-line interfaces (CLI) are often used for automating tasks, such as building reports, synchronizing data between systems, migrating data, deploying applications, and so on and on. Over the years, I have built countless CLI apps to save time. If I ever find myself doing something more than once, I try to find a way to automate it!
Deno 2.0 is an excellent solution for writing CLI apps. It supports TypeScript and JavaScript, it's cross-platform (runs on Windows, macOS, and Linux), has dozens of powerful tools in its standard library, and can also tap into most Node.js modules. The only limit is your imagination!
In this tutorial, you will learn how to:
- Create a command-line interface with Deno 2.0
- Parse command-line arguments
- Print help and version information
- Prompt for additional information
- Compile your app into a standalone executable
Set Up Your CLI Project
First, let's make sure you have the tools you need!
- Install Deno
- A good text editor, such as Visual Studio Code
Open your computer's terminal (or command prompt). Change the current directory to the folder where you normally save projects.
Note: If you don't already have a folder where you store software projects, I like creating a folder at the root of my home directory named
projects
. More than likely, when you open your computer's terminal/console app, you are automatically placed in your "user home" folder. Usemkdir projects
(ormd projects
if you're on Windows) to create the folder. Then, usecd projects
to change to that new folder.
Verify you have Deno 2.0 (or higher) installed using the following command.
deno --version
You should see something like:
deno 2.0.5 (stable, release, aarch64-apple-darwin)
v8 12.9.202.13-rusty
typescript 5.6.2
If you receive an error, or if your version of Deno is 1.x, follow the installation.
Next, enter the following commands to initialize a new Deno project.
deno init deno-cli-demo
cd deno-cli-demo
We're going to use Deno's @std/cli standard library, so add that to the project using the following command.
deno add jsr:@std/cli
Create Your First CLI App
Open up your new project using your preferred editor. Create a new file named hello.ts
and add the following code.
const now = new Date();
const message = "The current time is: " + now.toTimeString();
console.log("Welcome to Deno 🦕 Land!");
console.log(message);
From your terminal, enter the following command to run the script.
deno run hello.ts
You've built your first Deno CLI application! Feel free to play around with writing other things to the console.
Using Command-Line Arguments
Arguments? No, we're not talking about getting into a heated debate with your terminal. Although that can certainly happen. Computers can be rather obstinate.
Command-line arguments are options and values you might provide the CLI when you run the app. When you enter deno run hello.ts
, deno
is the CLI, and run hello.ts
are two arguments you provide to the CLI.
Create a new file named add.ts
and add the following code.
import { parseArgs } from "@std/cli/parse-args";
const args = parseArgs(Deno.args);
console.log("Arguments:", args);
const a = args._[0];
const b = args._[1];
console.log(`${a} + ${b} = ` + (a + b));
The idea is to take two numbers and add them together. Try it out!
deno run add.ts 1 2
Experiment with additional arguments. Or none at all. The parseArgs
function can also handle arguments traditionally called switches and flags. Try the following and observe the output.
deno run add.ts 3 4 --what=up -t -y no
Advanced Command-Line Arguments
We've only just scratched the surface of what you can do with command-line arguments. Let's try a more advanced example!
Create a new file named sum.ts
and add the following code.
import { parseArgs, ParseOptions } from "@std/cli/parse-args";
import meta from "./deno.json" with { type: "json" };
function printUsage() {
console.log("");
console.log("Usage: sum <number1> <number2> ... <numberN>");
console.log("Options:");
console.log(" -h, --help Show this help message");
console.log(" -v, --version Show the version number");
}
const options: ParseOptions = {
boolean: ["help", "version"],
alias: { "help": "h", "version": "v" },
};
const args = parseArgs(Deno.args, options);
if (args.help || (args._.length === 0 && !args.version)) {
printUsage();
Deno.exit(0);
} else if (args.version) {
// Pro tip: add a version to your deno.json file
console.log(meta.version ? meta.version : "1.0.0");
Deno.exit(0);
}
// validate all arguments are numbers
const numbers: number[] = args._.filter((arg) => typeof arg === "number");
if (numbers.length !== args._.length) {
console.error("ERROR: All arguments must be numbers");
printUsage();
Deno.exit(1);
}
// sum up the number arguments
const sum = numbers.reduce((sum, val) => sum + val);
// print the numbers and the total
console.log(`${numbers.join(" + ")} = ${sum}`);
Whoa, there's a lot going on here 😬 Let's try it out first, and then we'll cover some of the highlights. Try the following commands and see how the output changes.
deno run sum.ts 1 2 3 4 5
deno run sum.ts --help
deno run sum.ts --version
deno run sum.ts -h
deno run sum.ts 1 2 three
Now, if you go back and look through the code in sum.ts
, you can probably figure out some of the logic involved in handling the different arguments. The main thing I want to point out is this block of code.
const options: ParseOptions = {
boolean: ["help", "version"],
alias: { "help": "h", "version": "v" },
};
const args = parseArgs(Deno.args, options);
The parseArgs
function supports quite a few options to support a wide variety of arguments.
-
boolean: ["help", "version"]
: defines the--help
and--version
flags. -
alias: { "help": "h", "version": "v" }
: defines alternate-h
and-v
flags.
As you may have guessed, Deno.exit(0);
causes Deno to immediately stop the current script.
More Input Required (Prompts)
Imagine you're creating a CLI app to automate a report. Your app might connect to a system that requires authentication, such as a database or API.
Never embed secrets (sensitive information such as user names, passwords, API keys, connection strings, etc.) in your code. You don't want your secrets ending up in the hands of the wrong people!
In this case, the CLI should prompt for the sensitive information when it runs. Let's create another example to demonstrate how this is done.
First, add a new dependency using the following command.
deno add jsr:@std/dotenv
Create a new file named taskRunner.ts
and add the following code.
import { Spinner } from "@std/cli/unstable-spinner";
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export async function simulateTask(ms: number, message: string) {
const spinner = new Spinner({ message, color: "yellow" });
const start = performance.now();
spinner.start();
await sleep(ms);
spinner.stop();
const finish = performance.now();
const duration = Math.round((finish - start) / 100) / 10;
console.log(`${message} (${duration.toFixed(1)}s).`);
}
This is a module that exports a function named simulateTask
that we'll use from the CLI app. The simulateTask
function includes a few tricks, such as keeping track of how long the task runs and displaying a cool spinning animation while the task is running 🤓
Create a new file named updater.ts
and add the following code.
import "@std/dotenv/load";
import { parseArgs, ParseOptions } from "@std/cli/parse-args";
import { promptSecret } from "@std/cli/prompt-secret";
import meta from "./deno.json" with { type: "json" };
import { simulateTask } from "./taskRunner.ts";
function printUsage() {
console.log("Usage: ");
console.log(" updater --input <input file> --output <output file>");
console.log("Options:");
console.log(" -h, --help Show this help message");
console.log(" -v, --version Show the version number");
console.log(" -i, --input Input file");
console.log(" -o, --output Output file");
}
const options: ParseOptions = {
boolean: ["help", "version"],
string: ["input", "output"],
default: { "input": "data.csv", "output": "report.pdf" },
alias: { "help": "h", "version": "v", "input": "i", "output": "o" },
};
const args = parseArgs(Deno.args, options);
if (args.help) {
printUsage();
Deno.exit(0);
} else if (args.version) {
// Pro tip: add a version to your deno.json file
console.log(meta.version ? meta.version : "1.0.0");
Deno.exit(0);
}
// validate the input and output arguments
if (!args.input || !args.output) {
console.log("You must specify both an input and output file");
printUsage();
Deno.exit(1);
}
// attempt to get the username and password from environment variables
let user = Deno.env.get("MY_APP_USER");
let password = Deno.env.get("MY_APP_PASSWORD");
if (user === undefined) {
const userPrompt = prompt("Please enter the username:");
user = userPrompt ?? "";
}
if (password === undefined) {
const passPrompt = promptSecret("Please enter the password:");
password = passPrompt ?? "";
}
// simulating a few long-running tasks
await simulateTask(1000, `Reading input file [${args.input}]`);
await simulateTask(1500, `Connecting with user [${user}]`);
await simulateTask(5000, `Reading data from external system`);
await simulateTask(3200, `Writing output file [${args.output}]`);
console.log("Done!");
Some of this code may look familiar to the previous sum.ts
CLI example. Try running the app and see what happens!
deno run updater.ts --help
Did you get a strange message like ⚠️ Deno requests read access to...
? What's going on here?
Deno security and permissions
Deno is secure by default. It won't be able to read/write files, access your local environment variables, connect to your network, and a number of other potentially risky operations unless you explicitly give it permission.
Now run it using the following command. Don't worry. It's not doing anything; it's just simulating what a real app might look like.
deno run --allow-read --allow-env updater.ts
Wasn't that cool??
Deno tasks
However, including those permissions is a lot to type every time you want to run the CLI app. Let's add a task to make it easier. Open up your deno.json
file and update the "tasks"
with the following.
"tasks": {
"updater": "deno run --allow-read --allow-env updater.ts"
},
Now you can use the following command.
deno task updater --input data.csv --output report.pdf
Prompts and environment variables
Let's revisit the code in updater.ts
. Specifically, this block of code.
// attempt to get the username and password from environment variables
let user = Deno.env.get("MY_APP_USER");
let password = Deno.env.get("MY_APP_PASSWORD");
if (user === undefined) {
const userPrompt = prompt("Please enter the username:");
user = userPrompt ?? "";
}
if (password === undefined) {
const passPrompt = promptSecret("Please enter the password:");
password = passPrompt ?? "";
}
It's common practice to store app configuration as environment variables. Deno can access environment variables (with permission) using Deno.env.get()
. If MY_APP_USER
or MY_APP_PASSWORD
are not set as environment variables, the app will use prompt()
or promptSecret()
to ask for those values. As you've already seen, promptSecret()
hides the characters you type.
Remember the dependency we added for jsr:@std/dotenv
? This standard library reads a file named .env
and adds any values as environment variables.
Create a new file in the project named .env
and add the following text.
MY_APP_USER=loluser
MY_APP_PASSWORD=p@ssw0rd1
Now run the updater
task again. It should run without prompting you for a username or password.
Compile Your CLI App to an Executable
Want to share your CLI app with others or run the app on another computer? You can compile a Deno-powered CLI app into a standalone executable! Standalone means it can run without installing Deno or any of the libraries. Everything is bundled 📦
Pro tip: Use a task scheduler to run the executable periodically.
deno compile --allow-read --allow-env updater.ts
You should now have an executable version!
./updater --help
Among other things, you can create executables for other platforms.
Wrapping Up and Additional CLI Tools for Deno
Thank you for joining me on this journey of learning Deno! If you have any questions or suggestions, please drop them in the comments.
- Get the source code for the examples
- Connect with me: X/Twitter | BlueSky | Threads | YouTube | LinkedIn
Here are more CLI tools for Deno you might explore!