Deno on the CLI

Get ready to dive into the powerful world of Deno’s security model and command line features! In this chapter, we’ll explore how Deno’s permissions system keeps your applications secure, learn how to use environment variables, and discover the art of parsing command line arguments.

We’ll also uncover techniques for user interaction, gain insight into the host system, and even add a splash of color to your console output. In the end, you’ll have the tools to create secure, interactive, and visually appealing Deno applications that stand out from the crowd. Let’s unlock the full potential of the Deno runtime!

The Permission Model

A major drawback of NodeJS is that it will run any code that is launched by default with full permissions. This means that malicious code can run on our host without Node doing anything about it. Fortunately, this is not the case with Deno unless you explicitly give it the necessary permissions. For example, the --allow-net flag only allows network activities such as fetch requests.

There are two flags responsible for allowing access to the file system. --allow-read and --allow-write. Without these two flags, the application can try to read/write, but Deno will not allow it. This gives us an extra layer of security for our applications and host environments.

All available flags can be found in the documentation.

Specify flags

To avoid an “all or nothing” scenario and give our applications either full access or none at all, it is possible to specify the flags.

For example, we can customize the --allow-net flag to give access to example.com only. This can be done by passing domains to the --allow-net=example.com flag. If we try to make a request to mydomain.com, Deno will ask us if we allow it.

Let’s take the following example:

// main.ts
const resExample = await fetch("https://example.com");
const resDeno = await fetch("https://deno.com");

console.log(resExample);
console.log(resDeno);

If we run the short script with deno run --allow-net=example.com main.ts, we will see Deno asking us for more permissions. This is because we only allowed access to example.com and not deno.com.

 deno run --allow-net=example.com main.ts
 ⚠️ Deno requests net access to "deno.com:443".
┠─ Requested by `fetch()` API.
┠─ Learn more at: https://docs.deno.com/go/--allow-net
┠─ Run again with --allow-net to bypass this prompt.
 Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) >

We can restrict access to URLs or IPs, including ports. If we know in advance which domains will be used, we can provide our applications with a specific --allow-net flag for increased security.

See the documentation for more examples and information on restrictions for other flags.

Request access in apps

To prevent our applications from crashing unnecessarily while running, it is possible to check whether permissions are set or not. In the following example, we can check if it is possible for the application to access example.com.

const desc1 = { name: "net", host: "example.com" } as const;
console.log(await Deno.permissions.query(desc1));
await Deno.permissions.request(desc1);
console.log(await Deno.permissions.query(desc1));

As we will see below, the status is initially prompt, which means that the question has not yet been asked, so we need to use Deno.permissions.request to ask the user if it is okay to access example.com. If it is confirmed with Y, we will see in the next Deno.permissions.query call that the state: "granted" is set. Therefore we can now assume that we can communicate with example.com via network requests.

PermissionStatus { state: "prompt", onchange: null }
✅ Granted net access to "example.com".
PermissionStatus { state: "granted", onchange: null }

The same is also available for read and write, so that we can, for example, determine where we are allowed to write data or have to take a different path. More information can be found in the Deno docs on the Permissions API or the Permissions Management Example on Deno by Examples.

Using Environment Variables

Sooner or later, our applications will need to access the host’s environment variables. We can access them if the --allow-env flag was passed when the application was started. The operation is then done by simply calling the Deno.env API. For example, Deno.env.get("MY_VARIABLE").

A cool feature is Deno.env.toObject() - this gives us a complete object of all environment variables, where the key is the variable name and the value is the corresponding value of that variable. It would be possible to get the above variable like this

const { MY_VARIABLE } = Deno.env.toObject();

In many scenarios (e.g. in development) a lot of work is done with .env files. There is a module for this in the std library under @std/dotenv.

This works with an .env file as follows:

# .env
MY_VARIABLE=MY_VALUE

In the application, the .env files can then be accessed as follows.

import { load } from "jsr:@std/dotenv";

// 1st solution
const env = await load();
console.log(env["MY_VARIABLE"]);

// 2nd solution
await load({ export: true });
console.log(Deno.env.get("MY_VARIABLE"));

// 3rd solution
await load({ export: true });
const { MY_VARIABLE } = Deno.env.toObject();
console.log(MY_VARIABLE);

All three options have the same effect. The last two will export the variables to the underlying host at runtime. We need to make sure that Deno is also called with the --allow-read flag so that the .env file can be read.

Another nice security feature that Deno provides is the ability to restrict the variables that the application can access. For example, an application can only receive the PORT and HOST environment variables. In this case, all other host environment variables will be ignored or not passed to the application. To prevent the application from reading variables, it can be called with --allow-env=PORT,HOST. All other environment variables will then be inaccessible.

Parsing Command Line Arguments

No CLI tool is complete without variable input parameters to control the application. Accordingly, Deno offers the possibility to use and define arguments out of the box.

For this we have the parseArgs method from the std/cli library. This method uses the Deno.args API to read the passed parameters and define them in a meaningful object.

We have the option to define the possible arguments via a config. You can choose between Boolean, Collections or Strings. It offers the same features as minimist, which was used as an inspiration. In addition, we can define defaults, aliases and negatable variables.

The following is an extended example from Deno by Example, which includes a few more arguments.

import { parseArgs } from "jsr:@std/cli/parse-args";

const flags = parseArgs(Deno.args, {
  boolean: ["help", "color"],
  string: ["version"],
  default: { color: true },
  negatable: ["color"],
  collect: ["books"],
  alias: { help: "h" },
});

In the example we see that there are two boolean variables. One is help which shows the help output (--help) and the other is --color which can be used, for example, if you want to get colored output.

The advantage of this is that you do not have to enter --help true or --help=true, this is seen as the default if no value is given.

The --version flag can be filled with a variable string. For example, --version=1.0.0.

With the --books flag it is possible for us to record entries directly from the user, which can then be processed as an array. For example: --books alchemist --books 1984 --books "Atomic Habits" gives us an array of 3 strings [ "alchemist", 1984, "Atomic Habits" ] which we can process directly.

Negatable variables mean, for example, that instead of --color=false it is possible to write --no-color. The functionality remains the same. A --no- is added before the flag.

An alias is quickly explained. This gives us the option of calling up a flag under different names. In this case, we have the option of using --help or --h.

As an example we can look at the extended Deno by Example code:

import { parseArgs } from "jsr:@std/cli/parse-args";

const flags = parseArgs(Deno.args, {
  boolean: ["help", "color"],
  string: ["version"],
  default: { color: true },
  negatable: ["color"],
  collect: ["books"],
  alias: { help: "h" },
});

console.log("Wants help?", flags.help);
console.log("Version:", flags.version);
console.log("Wants color?:", flags.color);
console.log("Books:", flags.books);

console.log("Other:", flags._);

When we put this in a file called flags.ts for example, and run it with some random values for our flags, we get the output of:

 deno run flags.ts --help --books alchemist --books 1984 --books="Atomic Habits" --version=2.0.0 MyValue
Wants help? true
Version: 2.0.0
Wants color?: true
Books: [ "alchemist", 1984, "Atomic Habits" ]
Other: [ "MyValue" ]

What we can see here is that we have a value in our Other section. This comes from flags._ where everything that is not recognized as a flag is placed.

Prompt the user

From time to time user input is required within the application. Here we can use features that are available in almost all browsers (because Deno only uses browser technology, right?).

We can use prompt, alert and confirm to get input from the user.

Worth mentioning are alert and confirm, two nice helper functions that allow us to confirm (by pressing the Enter key) for alert, or to confirm the input of a y or N (confirm) without any effort. The latter returns a boolean value that we can use in our applications to find out whether the user has made a decision for or against the question.

// main.ts
alert("Be informed!");

const c = confirm("Are you sure?");
console.log("is the user sure?", c);
 deno run -A flags.ts
Be informed! [Enter]
Are you sure? [y/N] y
is the user sure? true

Host Interaction

We have in the Deno namespace some APIs that allow us to get information about the host system running our application.

For example, we can get the host name (Deno.hostname()), the uptime of the host (Deno.osUptime()), general information about the memory of the host or the running Deno process (Deno. systemMemoryInfo() or Deno.memoryUsage()), the process identifier (Deno.pid and Deno.ppid) or the path used by the Deno executable (Deno.execPath()).

An example of commands and their example outputs:

console.log("Hostname:", Deno.hostname());
console.log("uptime:", Deno.osUptime());
console.log("system Memory", Deno.systemMemoryInfo());
console.log("build:", Deno.build);
console.log("version:", Deno.version);

// Hostname: MacBook.local
// uptime: 52661
// system Memory [Object: null prototype] {
//   total: 17179869184,
//   free: 2177296,
//   available: 7781456,
//   buffers: 0,
//   cached: 0,
//   swapTotal: 0,
//   swapFree: 0
// }
// build: {
//   target: "aarch64-apple-darwin",
//   arch: "aarch64",
//   os: "darwin",
//   vendor: "apple",
//   env: undefined
// }
// version: { deno: "2.0.0-rc.10", v8: "12.9.202.13-rusty", typescript: "5.6.2" }

We also get information about the current working directory we are in (Deno.cwd()). This is because Deno allows us to interact with the file system and create, read or modify files. Additionally we can change file permissions or owners with the help of a Deno application as well.

A more detailed explanation of how Deno interacts with a host’s file system can be found in a later section “Deno and the filesystem”.

Colorful Message Logging

A good feature that is helpful for CLI applications is the ability to use simple CSS for console.log output. This can replace a library like chalk in some places.

For example, we can change the font color, decoration or size. This feature is available in many browsers, including Deno.

For this we need %c symbols that signal to the runtime where changes should take place, e.g. we can use console.log("Hello %cWorld%c", "color: green") to print the word “World” in green and thus indicate the success of a command in a CLI application, for example.

In addition to the simple color names, it is possible to specify these in hex or RGB values. Various changes can be defined by a string separated by a semicolon. E.g. console.log("Hello %cWorld", "color: green; text-decoration: underline; font-weight: bold").

📋 Further Documentation

Wrapping up Deno on the CLI

In this chapter, we’ve explored the robust security features and powerful CLI capabilities that set Deno apart. We’ve covered:

These features combine to make Deno a secure, versatile, and easy-to-use runtime for building modern CLI applications.