惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
SegmentFault 最新的问题
G
Google Developers Blog
Stack Overflow Blog
Stack Overflow Blog
WordPress大学
WordPress大学
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
罗磊的独立博客
月光博客
月光博客
IT之家
IT之家
爱范儿
爱范儿
Google DeepMind News
Google DeepMind News
小众软件
小众软件
C
Check Point Blog
B
Blog RSS Feed
H
Help Net Security
博客园 - 司徒正美
L
LangChain Blog
MongoDB | Blog
MongoDB | Blog
B
Blog
The Cloudflare Blog
Apple Machine Learning Research
Apple Machine Learning Research
Microsoft Security Blog
Microsoft Security Blog
M
MIT News - Artificial intelligence
N
Netflix TechBlog - Medium
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

Deno

Deno 2.8 | Deno Claw Patrol: an open-source security firewall for agents | Deno Fresh 2.3: Zero JS by default, View Transitions, and Temporal support | Deno Deno 2.7: Temporal API, Windows ARM, and npm overrides | Deno Build a dinosaur runner game with Deno, pt. 6 | Deno Build a dinosaur runner game with Deno, pt. 5 | Deno Deno Deploy is Generally Available | Deno Introducing Deno Sandbox | Deno Build a dinosaur runner game with Deno, pt. 4 | Deno Build a dinosaur runner game with Deno, pt. 3 | Deno Build a dinosaur runner game with Deno, pt. 2 | Deno React / Next.js Denial-of-Service Vulnerability: Deno Deploy users protected | Deno Deno 2.6: dx is the new npx | Deno Build a dinosaur runner game with Deno, pt. 1 | Deno React Server Functions / Next.js Vulnerability: Deno Deploy users protected | Deno My highlights from the new Deno Deploy | Deno Deno's Other Open Source Projects | Deno How Deno protects against npm exploits | Deno Help Us Raise $200k to Free JavaScript from Oracle | Deno Deno 2.5: Permissions in the config file | Deno Fresh 2.0 Graduates to Beta, Adds Vite Support | Deno Deno 2.4: deno bundle is back | Deno JavaScript™ Trademark Update | Deno What's coming to JavaScript | Deno A brief history of JavaScript | Deno Reports of Deno's Demise Have Been Greatly Exaggerated | Deno An Update on Fresh | Deno How Plaid migrated 100 services to a new database platform 5x faster with Deno | Deno Deno 2.3: Improved deno compile, local npm packages, and more | Deno Add JSR packages with pnpm and Yarn | Deno
Build a Cross-Platform CLI with Deno in 5 minutes | Deno
Andy Jiang · 2023-09-01 · via Deno

Command line interfaces (“CLI”) are useful, simple to use, and in many cases, the fastest way to get something done. While there are many ways to build a CLI, Deno’s zero config, all-in-one modern tooling, ability to compile your script to a portable executable binary, makes building CLIs a breeze.

In this post, we’ll go over building a basic CLI - greetme-cli. It takes your name and a color as arguments, and outputs a random greeting:

$ greetme --name=Andy --color=blue
Hello, Andy!

Through building the CLI, we’ll cover:

  • Setup your CLI
  • Parsing Arguments
  • Interacting with Browser Methods
  • Managing State
  • Testing
  • Compiling and Distributing
  • Additional Resources

Setup your CLI

If you haven’t already, install Deno and setup your IDE.

Next, create a folder for your CLI. We’ll name ours greetme-cli.

In that folder, create main.ts, which will contain the logic, and greetings.json, which will contain a JSON array of random greetings.

In our main.ts:

import greetings from "./greetings.json" with { type: "json" };



function main(): void {
  console.log(
    `${greetings[Math.floor(Math.random() * greetings.length) - 1]}!`,
  );
}



main();

When we run it, we should see a random greeting:

$ deno run main.ts
Good evening!

Cool, but not very interactive. Let’s add a way to parse arguments and flags.

Parsing Arguments

Deno will automatically parse arguments from the command into a Deno.args array:

But instead of manually parsing Deno.arg, we can use the flags module from Deno’s standard library, which is a set of modules audited by the core team. Here’s an example:


import { parse } from "https://deno.land/std@0.200.0/flags/mod.ts";

console.dir(parse(Deno.args));

When we run parse.ts with flags and options, parse(Deno.args)) returns an object with flags and options mapped to keys and values:

$ deno run parse.ts -a beep -b boop
{ _: [], a: 'beep', b: 'boop' }

$ deno run parse.ts -x 3 -y 4 -n5 -abc --beep=boop foo bar baz
{ _: [ 'foo', 'bar', 'baz' ],
  x: 3,
  y: 4,
  n: 5,
  a: true,
  b: true,
  c: true,
  beep: 'boop' }

But the best part of parse() is the ability to define types, assign default values, and create aliases for each argument by passing an optional object:

const flags = parse(Deno.args, {
  boolean: ["help", "save"],
  string: [ "name", "color"]
  alias: { "help": "h" }
  default: { "color": "blue" }
})

For more information about parse(), refer to this example or this documentation.

For our greetme-cli example, let’s add the following flags:

-h --help        Display this help and exit
-s --save        Save settings for future greetings
-n --name        Set your name for the greeting
-c --color       Set the color of the greeting

Let’s create a new function called parseArguments in main.ts:

import { parse } from "https://deno.land/std@0.200.0/flags/mod.ts";
import type { Args } from "https://deno.land/std@0.200.0/flags/mod.ts";

function parseArguments(args: string[]): Args {
  
  const booleanArgs = [
    "help",
    "save",
  ];

  
  const stringArgs = [
    "name",
    "color",
  ];

  
  const alias = {
    "help": "h",
    "save": "s",
    "name": "n",
    "color": "c",
  };

  return parse(args, {
    alias,
    boolean: booleanArgs,
    string: stringArgs,
    stopEarly: false,
    "--": true,
  });
}

And also a printHelp function that’ll console.log information when the --help flag is enabled:

function printHelp(): void {
  console.log(`Usage: greetme [OPTIONS...]`);
  console.log("\nOptional flags:");
  console.log("  -h, --help                Display this help and exit");
  console.log("  -s, --save                Save settings for future greetings");
  console.log("  -n, --name                Set your name for the greeting");
  console.log("  -c, --color               Set the color of the greeting");
}

And finally let’s tie it all together in our main function:

function main(inputArgs: string[]): void {
  const args = parseArguments(inputArgs);

  
  if (args.help) {
    printHelp();
    Deno.exit(0);
  }

  let name: string | null = args.name;
  let color: string | null = args.color;
  let save: boolean = args.save;

  console.log(
    `%c${
      greetings[Math.floor(Math.random() * greetings.length) - 1]
    }, ${name}!`,
    `color: ${color}; font-weight: bold`,
  );
}

Now, let’s run the CLI with the newly supported flags:

$ deno run main.ts --help
Usage: greetme [OPTIONS...]

Optional flags:
  -h, --help                Display this help and exit
  -s, --save                Save settings for future greetings
  -n, --name                Set your name for the greeting
  -c, --color               Set the color of the greeting


$ deno run main.ts --name=Andy --color=blue
It's nice to see you, Andy!

$ deno run main.ts -n=Steve -c=red
Morning, Steve!

Looking good. But how do we add functionality for the --save option?

Managing state

Depending on your CLI, you may want to persist state across user sessions. As an example, let’s add save functionality via --save flag to greetme-cli.

We can add persistant storage to our CLI using Deno KV, which is a key-value data store built right into the runtime. It’s backed by SQLite locally and FoundationDB when deployed to Deno Deploy (though CLIs aren’t meant to be deployed).

Since it’s built into the runtime, we don’t need to manage any secret keys or environmental variables to get it setup. We can open a connection through one line of code:

const kv = await Deno.openKv("/tmp/kv.db");

Note we do need to pass an explicit path in .openKv(), as the compiled binary does not have a default storage directory set.

Let’s update our main function to use Deno KV:

- function main(inputArgs: string[]): void {
+ async function main(inputArgs: string[]): Promise<void> {

  const args = parseArguments(inputArgs);

  // If help flag enabled, print help.
  if (args.help) {
    printHelp();
    Deno.exit(0);
  }

  let name: string | null = args.name;
  let color: string | null = args.color;
  let save: boolean = args.save;

+  const kv = await Deno.openKv("/tmp/kv.db");
+  let askToSave = false;

+  if (!name) {
+    name = (await kv.get(["name"])).value as string;
+  }
+  if (!color) {
+    color = (await kv.get(["color"])).value as string;
+  }
+  if (save) {
+    await kv.set(["name"], name);
+    await kv.set(["color"], color);
+  }

  console.log(
    `%c${
      greetings[Math.floor(Math.random() * greetings.length) - 1]
    }, ${name}!`,
    `color: ${color}; font-weight: bold`,
  );
}

This simple addition opens a connection to Deno KV and writes the data with .set() if --save option is true. If no --name or --color is set in the command, it will read the data with .get().

Let’s try it out. Note that we’ll need to add the flags --unstable to use Deno KV, as well as --allow-read and --allow-write for writing and reading to the filesystem:

$ deno run --unstable --allow-read --allow-write main.ts --name=Andy --save
Greetings, Andy!

$ deno run --unstable --allow-read --allow-write main.ts
It's nice to see you, Andy!

The CLI remembered my name in the second command!

Interacting with Browser Methods

Sometimes you might want to offer other modes of interactivity aside from command line flags. An easy way to do that with Deno is via browser methods.

Deno offers web platform APIs where possible, and browser methods are no exception. That means you have access to alert(), confirm(), and prompt(), all which can be used on the command line.

Let’s update our main() function with some interactive prompts in situations where the flags are not set:

async function main(inputArgs: string[]): Promise<void> {
  const args = parseArguments(inputArgs);

  // If help flag enabled, print help.
  if (args.help) {
    printHelp();
    Deno.exit(0);
  }

  let name: string | null = args.name;
  let color: string | null = args.color;
  let save: boolean = args.save;

  const kv = await Deno.openKv("/tmp/kv.db");
  let askToSave = false;

  // If there isn't any name or color, then prompt.
  if (!name) {
    name = (await kv.get(["name"])).value as string;
+    if (!name) {
+      name = prompt("What is your name?");
+      askToSave = true;
+    }
  }
  if (!color) {
    color = (await kv.get(["color"])).value as string;
+    if (!color) {
+      color = prompt("What is your favorite color?");
+      askToSave = true;
+    }
  }
+  if (!save && askToSave) {
+    const savePrompt: string | null = prompt(
+      "Do you want to save these settings? Y/n",
+    );
+    if (savePrompt?.toUpperCase() === "Y") save = true;
+  }

  if (save) {
    await kv.set(["name"], name);
    await kv.set(["color"], color);
  }

  console.log(
    `%c${
      greetings[Math.floor(Math.random() * greetings.length) - 1]
    }, ${name}!`,
    `color: ${color}; font-weight: bold`,
  );
}

Now, when we run the command without flags, we’ll receive a prompt:

$ deno run --unstable --allow-read --allow-write main.ts
What is your name? Andy
What is your favorite color? blue
Do you want to save these settings? Y/n Y
Howdy, Andy!

$ deno run --unstable --allow-read --allow-write main.ts --name=Steve
Pleased to meet you, Steve!

Great! The second time reads the variables that we chose to save via the prompts.

Browser methods are a quick and simple way to add interactivity in your scripts or CLI.

Testing

Setting up a test runner in Deno is easy, since it’s built right into the runtime.

Let’s write a simple test to make sure that the CLI is parsing the input flags properly. Let’s create main_test.ts and register a test case using Deno.test():

import { assertEquals } from "https://deno.land/std@0.200.0/testing/asserts.ts";
import { parseArguments } from "./main.ts";

Deno.test("parseArguments should correctly parse CLI arguments", () => {
  const args = parseArguments([
    "-h",
    "--name",
    "Andy",
    "--color",
    "blue",
    "--save",
  ]);

  assertEquals(args, {
    _: [],
    help: true,
    h: true,
    name: "Andy",
    n: "Andy",
    color: "blue",
    c: "blue",
    save: true,
    s: true,
    "--": [],
  });
});

Now, we can run the test using deno test with the necessary flags:

$ deno test --unstable --allow-write --allow-read
What's happening, Andy!
running 1 test from ./main_test.ts
parseArguments should correctly parse CLI arguments ... ok (16ms)

ok | 1 passed | 0 failed (60ms)

Note if you’re using VS Code, Deno tests are automatically detected and you can run them right from your IDE.

Compiling and distributing

Deno makes it easy to distribute your CLI (or any Deno program for that matter) with deno compile, which compiles your JavaScript or TypeScript file into a single executable binary that will run on all major platforms.

Let’s deno compile our main.ts with the flags required to run the binary:

$ deno compile --allow-read --allow-write --unstable main.ts --output greetme
Check file:///Users/andyjiang/deno/greetme-cli/main.ts
Compile file:///Users/andyjiang/deno/greetme-cli/main.ts to greetme

You should now have a greetme binary in the same directory. Let’s run it:

$ ./greetme --name=Andy --color=blue --save
It's nice to see you, Andy!

And if we run it again:

Now, you can share the binary to be run on all major platforms. For an example of how the creator of Homebrew uses deno compile as part of their GitHub Actions build and release workflow, check out this blog post.

Additional Resources

While this tutorial showed how to build a CLI using Deno, it is very simple and didn’t require any third party dependencies. For more complex CLIs, having modules or frameworks can help in development.

Here are some helpful modules you can use when building your CLI (some more fun than others):

  • yargs: the modern, pirate-themed successor to optimist
  • cliffy: a simple and type-safe commandline framework
  • denomander: a Commander.js-inspired framework for building CLIs
  • tui: a simple framework for building terminal user interfaces
  • terminal_images: a TypeScript module for displaying images in the terminal
  • cliui: create complex multi-line CLIs
  • chalk: colorizes terminal output (and here’s the Deno module)
  • figlet.js: creates ASCII art from text
  • dax: Cross-platform shell tools for Deno inspired by zx

Are you building something with Deno? Let us know on Twitter or in Discord.