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:
- Deno’s permissions model, which provides granular control over application access
- Secure use of environment variables
- Parsing command line arguments for flexible user input
- Interacting with users through prompts and alerts
- Access host system information
- Add visual flair with colorful console output
These features combine to make Deno a secure, versatile, and easy-to-use runtime for building modern CLI applications.