On this page
Build CLI apps
Deno is a great way to ship command-line tools. Your tool is just TypeScript, so
there's no build step to run it. When you're ready to distribute it,
deno compile turns it into a single self-contained executable that runs
without Deno installed.
Read command-line arguments Jump to heading
Raw arguments are available on Deno.args. For real
tools, parse them with @std/cli, which handles
flags, options, and defaults:
import { parseArgs } from "jsr:@std/cli/parse-args";
const flags = parseArgs(Deno.args, {
string: ["name"],
default: { name: "world" },
});
console.log(`Hello, ${flags.name}!`);
$ deno run greet.ts --name=Deno
Hello, Deno!
Read from stdin and detect a TTY Jump to heading
Good CLI tools compose with pipes. Deno.stdin
exposes standard input as a web-standard ReadableStream, so the easiest way to
read all piped input as text is to wrap it in a Response:
const input = await new Response(Deno.stdin.readable).text();
When your tool can be used both ways, use
Deno.stdin.isTerminal() to tell whether
stdin is an interactive terminal or a pipe, and branch accordingly. This is also
how tools decide whether to print colors and progress bars (interactive) or
plain machine-readable output (piped). Here the tool falls back to asking the
user when nothing is piped in:
let input: string;
if (Deno.stdin.isTerminal()) {
input = prompt("Enter some text:") ?? "";
} else {
input = await new Response(Deno.stdin.readable).text();
}
console.log(`Got ${input.trim().length} characters`);
$ echo "hello from a pipe" | deno run count.ts
Got 17 characters
$ deno run count.ts
Enter some text: hi
Got 2 characters
Reading stdin requires no permission flags.
If you're porting a Node.js CLI or using npm libraries that read input,
Node-style process.stdin (from node:process or the process global) works
in Deno too, so that code runs unchanged.
Prompt the user Jump to heading
For simple interactive input, Deno ships the browser globals prompt(),
confirm(), and alert(). They block until the user responds (execution pauses
at that line and nothing else runs until input arrives), and they need no
imports or permissions:
const name = prompt("Project name:", "my-app");
const ok = confirm(`Create ${name}?`);
if (!ok) Deno.exit(1);
console.log(`Creating ${name}...`);
For anything fancier, reach for @std/cli. Besides
argument parsing, it includes spinners, progress bars, and select prompts. These
live in modules prefixed with unstable-, so their APIs may still change. The
Spinner class shows an animated status line while your tool works:
import { Spinner } from "jsr:@std/cli/unstable-spinner";
const spinner = new Spinner({ message: "Downloading..." });
spinner.start();
await new Promise((resolve) => setTimeout(resolve, 2000));
spinner.stop();
console.log("Done");
deno run spin.ts
Exit with the right code Jump to heading
Shells, scripts, and CI systems rely on your tool's exit code: zero means
success, anything else means failure. Deno.exit()
terminates the process immediately with the given code:
if (Deno.args.length === 0) {
console.error("Usage: check <file>...");
Deno.exit(2);
}
Because Deno.exit() stops the process on the spot,
pending work like flushing logs or finally blocks never runs. When you want to
record a failure but keep going, set
Deno.exitCode instead. The process continues,
finishes naturally, and then exits with the code you set. This is the right
pattern for linters and batch tools that should report every problem rather than
stop at the first one:
for (const file of Deno.args) {
try {
await Deno.lstat(file);
} catch {
console.error(`Missing: ${file}`);
Deno.exitCode = 1;
}
}
console.log("Check finished");
$ deno run --allow-read check.ts real.txt nope.txt
Missing: nope.txt
Check finished
$ echo $?
1
There is no fixed meaning for nonzero codes beyond "failure", but a common
convention is 1 for the errors your tool checks for and 2 for usage mistakes
like missing arguments.
Compile to a single executable Jump to heading
deno compile bundles your program and the
Deno runtime into one binary: no node_modules, no install step for your users.
deno compile greet.ts
./greet --name=Deno
Name the binary with --output. If your tool needs permissions, bake them in
with the usual --allow-* flags so it runs without prompting:
deno compile --output greet greet.ts
Embed assets in the binary Jump to heading
A single-file executable should stay single-file even when your tool needs data
such as templates, word lists, or default configs. Pass --include to bundle
files or directories into the binary:
deno compile --include names.csv --output greet greet.ts
At runtime, read the embedded files relative to your module's directory with
import.meta.dirname:
const names = Deno.readTextFileSync(import.meta.dirname + "/names.csv");
The same code works during development with deno run and inside the compiled
binary, regardless of where the user runs it from. See
including data files
for directory includes and for configuring the paths once in deno.json.
Cross-compile for other platforms Jump to heading
Build for a platform other than your own with --target, so you can ship
binaries for Windows, macOS, and Linux from one machine:
deno compile --target x86_64-pc-windows-msvc --output greet.exe greet.ts
deno compile --target aarch64-apple-darwin --output greet greet.ts
The full list of targets covers x86_64 and ARM builds of Linux, macOS, and Windows; see supported targets. If you distribute binaries to end users, you can also sign them so operating systems trust your tool; see code signing for the macOS and Windows steps.
Install a tool from source Jump to heading
To make a script available as a command on your own machine (without compiling),
install it globally with deno install:
deno install --global --name greet greet.ts
greet --name=Deno
Going further Jump to heading
- deno compile. All targets, flags, asset embedding, icons, and code signing.
- deno install. Install tools and dependencies.
- @std/cli. Argument parsing, spinners, progress bars, and interactive prompts.
- Deno API reference. Everything on the
Denonamespace, including stdin, exit codes, and file APIs. - Permissions. Choose exactly what your compiled tool may access.
