dax logo dax

v0.45.0

dax — shell scripting
that runs the same everywhere.

Cross-platform shell tools for Deno and Node.js inspired by zx — a friendlier $ for spawning processes, with a built-in shell parser, built-in commands, prompts, requests, and a Path API.

dax logo — a cat with an arrow

Highlights #

Cross-platform shell

Same shell on macOS, Linux, and Windows. Common commands (cp, mv, rm, mkdir, …) are built in for better Windows support, and the shell's env can be exported back to the host process.

Safe by default

Template literal interpolations are escaped automatically — $`mkdir ${dir}` works whether dir is "foo" or "Dir with spaces & quotes". No manual quoting, no shell-injection bugs.

Structured output

Coerce stdout into the shape you need with chained methods: .text(), .json(), .lines(), .bytes(), or stream line-by-line with .linesIter().

Batteries included

Prompts ($.confirm, $.select), progress bars ($.progress), HTTP requests ($.request), and an immutable Path API — all in one library.

Portable

Pure TypeScript and WebAssembly — no native dependencies, no compile step, no postinstall scripts. The code runs the same across all Node-compatible runtimes — no vendor lock-in.

Install #

npm install dax

Executing commands #

import $ from "dax";

// run a command
await $`echo 5`; // outputs: 5

// output to stdout and run a sub process
await $`echo 1 && deno run main.ts`;

// parallel
await Promise.all([
  $`sleep 1 ; echo 1`,
  $`sleep 2 ; echo 2`,
  $`sleep 3 ; echo 3`,
]);

Providing arguments to a command #

Use an expression in a template literal to provide a single argument to a command:

const dirName = "some_dir";
await $`mkdir ${dirName}`; // executes as: mkdir some_dir

Arguments are escaped so strings with spaces get escaped and remain as a single argument:

const dirName = "Dir with spaces";
await $`mkdir ${dirName}`; // executes as: mkdir 'Dir with spaces'

Alternatively, provide an array for multiple arguments:

const dirNames = ["some_dir", "other dir"];
await $`mkdir ${dirNames}`; // executes as: mkdir some_dir 'other dir'

If you do not want to escape an argument in a template literal, you can opt out by using $.rawArg starting in 0.43.0:

const args = "arg1   arg2";
await $`echo ${$.rawArg(args)} ${args}`; // executes as: echo arg1 arg2 arg1   arg2

Alternatively, you can opt out completely by using $.raw:

const args = "arg1   arg2";
await $.raw`echo ${args}`; // executes as: echo arg1 arg2

// or escape a specific argument while using $.raw
await $.raw`echo ${$.escapeArg(args)} ${args}`; // executes as: echo "arg1  arg2" arg1 arg2

Providing stdout of one command to another is possible as follows:

// Note: This will read trim the last newline of the other command's stdout
const result = await $`echo 1`.stdout("piped"); // need to set stdout as piped for this to work
const finalText = await $`echo ${result}`.text();
console.log(finalText); // 1

...though it's probably more straightforward to just collect the output text of a command and provide that:

const result = await $`echo 1`.text();
const finalText = await $`echo ${result}`.text();
console.log(finalText); // 1

JavaScript objects to redirects

You can provide JavaScript objects to shell output redirects:

const buffer = new Uint8Array(2);
await $`echo 1 && (echo 2 > ${buffer}) && echo 3`; // 1\n3\n
console.log(buffer); // Uint8Array(2) [ 50, 10 ] (2\n)

Supported objects: Uint8Array, Path, WritableStream, any function that returns a WritableStream, any object that implements [$.symbols.writable](): WritableStream

Or input redirects:

// strings
const data = "my data in a string";
const bytes = await $`gzip < ${data}`;

// paths
const path = $.path("file.txt");
const bytes = await $`gzip < ${path}`;

// requests (this example does not make the request until after 5 seconds)
const request = $.request("https://plugins.dprint.dev/info.json")
  .showProgress(); // show a progress bar while downloading
const bytes = await $`sleep 5 && gzip < ${request}`.bytes();

Supported objects: string, Uint8Array, Path, RequestBuilder, ReadableStream, any function that returns a ReadableStream, any object that implements [$.symbols.readable](): ReadableStream

Providing stdin #

await $`command`.stdin("inherit"); // default
await $`command`.stdin("null");
await $`command`.stdin(new Uint8Array[1, 2, 3, 4]());
await $`command`.stdin(someReaderOrReadableStream);
await $`command`.stdin($.path("data.json"));
await $`command`.stdin($.request("https://plugins.dprint.dev/info.json"));
await $`command`.stdinText("some value");

Or using a redirect:

await $`command < ${$.path("data.json")}`;

Getting output #

Get the stdout of a command (makes stdout "quiet"):

const result = await $`echo 1`.text();
console.log(result); // 1

Get the result of stdout as json (makes stdout "quiet"):

const result = await $`echo '{ "prop": 5 }'`.json();
console.log(result.prop); // 5

Get the result of stdout as bytes (makes stdout "quiet"):

const bytes = await $`gzip < file.txt`.bytes();
console.log(bytes);

Get the result of stdout as a list of lines (makes stdout "quiet"):

const result = await $`echo 1 && echo 2`.lines();
console.log(result); // ["1", "2"]

Stream the output line-by-line without buffering the whole thing into memory (makes the chosen stream "quiet"):

for await (const line of $`cat big.txt`.linesIter()) {
  console.log(line);
}

// also works for stderr
for await (const line of $`some-command`.linesIter("stderr")) {
  console.log(line);
}

Breaking out of the loop early kills the child process. Lines split at \n or \r\n and the terminators are not included.

Get stderr's text:

const result = await $`deno eval "console.error(1)"`.text("stderr");
console.log(result); // 1

Working with a lower level result that provides more details:

const result = await $`deno eval 'console.log(1); console.error(2);'`
  .stdout("piped")
  .stderr("piped");
console.log(result.code); // 0
console.log(result.stdoutBytes); // Uint8Array(2) [ 49, 10 ]
console.log(result.stdout); // 1\n
console.log(result.stderr); // 2\n
const output = await $`echo '{ "test": 5 }'`.stdout("piped");
console.log(output.stdoutJson);

Getting the combined output:

const text = await $`deno eval 'console.log(1); console.error(2); console.log(3);'`
  .text("combined");

console.log(text); // 1\n2\n3\n

Streaming API #

Awaiting a command will get the CommandResult, but calling .spawn() on a command without await will return a CommandChild. This has some methods on it to get web streams of stdout and stderr of the executing command if the corresponding pipe is set to "piped". These can then be sent wherever you'd like, such as to the body of a $.request or another command's stdin.

For example, the following will output 1, wait 2 seconds, then output 2 to the current process' stderr:

const child = $`echo 1 && sleep 1 && echo 2`.stdout("piped").spawn();
await $`deno eval 'await Deno.stdin.readable.pipeTo(Deno.stderr.writable);'`
  .stdin(child.stdout());

Piping #

Piping stdout or stderr to a Deno.WriterSync:

await $`echo 1`.stdout(Deno.stderr);
await $`deno eval 'console.error(2);`.stderr(Deno.stdout);

Piping to a WritableStream:

await $`echo 1`.stdout(Deno.stderr.writable, { preventClose: true });
// or with a redirect
await $`echo 1 > ${someWritableStream}`;

To a file path:

await $`echo 1`.stdout($.path("data.txt"));
// or
await $`echo 1 > data.txt`;
// or
await $`echo 1 > ${$.path("data.txt")}`;

To a file:

using file = $.path("data.txt").openSync({ write: true, create: true });
await $`echo 1`.stdout(file);
// or
await $`echo 1 > ${file}`;

From one command to another:

const output = await $`echo foo && echo bar`
  .pipe($`grep foo`)
  .text();

// or using a pipe sequence
const output = await $`(echo foo && echo bar) | grep foo`
  .text();

Silencing a command #

Makes a command not output anything to stdout and stderr.

await $`echo 5`.quiet();
await $`echo 5`.quiet("stdout"); // only silence stdout
await $`echo 5`.quiet("stderr"); // only silence stderr

The following code:

const text = "example";
await $`echo ${text}`.printCommand();

Outputs the following (with the command text in blue):

> echo example
example

Enabling on a $

Like with any default in Dax, you can build a new $ turning on this option so this will occur with all commands (see Custom $). Alternatively, you can enable this globally by calling $.setPrintCommand(true);.

$.setPrintCommand(true);

const text = "example";
await $`echo ${text}`; // will output `> echo example` before running the command

Tail display (Docker-style partial scrolling) #

.tailDisplay() pins the command's output to a fixed-height region at the bottom of the terminal — only the most recent lines are shown live, and on completion they're cleared from the live region (the full output stays in scrollback above for failed commands).

// keep the last 5 lines of `./build.sh` pinned while it runs
await $`./build.sh`.tailDisplay();

// configure visible row count and header
await $`./build.sh`.tailDisplay({ maxLines: 2, header: false });

// percentage sizing — re-fits if the terminal is resized mid-run
await $`./build.sh`.tailDisplay({ maxLines: "50%" });

// custom header rendered verbatim (you supply any styling)
await $`./build.sh`.tailDisplay({
  maxLines: 10,
  header: ({ command }) => `building ${command}…`,
});

maxLines accepts a literal number, a "N%" string resolved against the terminal height at draw time, or a (ctx) => number callback. Defaults to 5.

header accepts:

Concurrent tailing commands compose into a single shared scrolling region, so multiple builds can run in parallel without spilling over each other:

await Promise.all([
  $`./build.sh frontend`.tailDisplay({ maxLines: 4 }),
  $`./build.sh backend`.tailDisplay({ maxLines: 4 }),
]);

Enabling on a $

Like printCommand, you can build a new $ turning this on (see Custom $) or enable it globally on the default $:

$.setTailDisplay(true);
// or with options
$.setTailDisplay({ maxLines: 10 });

Exit codes #

By default, commands will throw an error on non-zero exit code:

await $`exit 123`;
// Uncaught Error: Exited with code: 123
//    at CommandChild.pipedStdoutBuffer (...)

If you want to disable this behaviour, run a command with .noThrow():

const result = await $`exit 123`.noThrow();
console.log(result.code); // 123
// or only for certain exit codes
await $`exit 123`.noThrow(123);

Or handle the error case within the shell:

await $`failing_command || echo 'Errored!'`;

Note: if you want it to not throw by default, you can build a custom $ (see below).

Exit code helper

If you just want to get the exit code, you can use the .code() helper:

const code = await $`git diff --quiet`.code();

Capturing output for failure messages #

.errorTail() silently retains the trailing bytes of stdout and/or stderr while a command runs and appends them to the thrown Error.message if the command exits with a non-zero code. It targets streams the user can't see — piped to another command, redirected to a file, sent to a WritableStream, or discarded with "null" — so when the command fails you still get a glimpse of what was written. Streams routed to the terminal ("inherit" / "inheritPiped") are skipped, since the bytes already reached the scrollback.

The most common case is .quiet(): stdout/stderr are suppressed, so without .errorTail() you have no idea what the command was doing when it failed.

// surfaces the trailing stdout/stderr bytes in the error if the command fails
await $`./build.sh`.errorTail().quiet();

// raise the per-stream cap (default: 8 KiB)
await $`./build.sh`.errorTail({ maxBytes: 16 * 1024 }).quiet();

// only capture stderr
await $`./build.sh > out.log`.errorTail({ stdout: false }).quiet();

// merge stdout and stderr into one interleaved buffer so the error
// message preserves the order the bytes were written
await $`./build.sh > out.log 2> err.log`.errorTail({ combined: true }).quiet();

.errorTail() has no effect when the command succeeds (the buffer is discarded) or when .noThrow() swallows the failure.

Enabling on a $

You can build a new $ turning this on (see Custom $) or enable it globally on the default $:

$.setErrorTail(true);
// or with options
$.setErrorTail({ maxBytes: 16 * 1024 });

Setting environment variables #

Done via the .env(...) method:

// outputs: 1 2 3 4
await $`echo $var1 $var2 $var3 $var4`
  .env("var1", "1")
  .env("var2", "2")
  // or use object syntax
  .env({
    var3: "3",
    var4: "4",
  });

Setting cwd for command #

Use .cwd("new_cwd_goes_here"):

// outputs that it's in the someDir directory
await $`deno eval 'console.log(Deno.cwd());'`.cwd("./someDir");

Exporting the environment of the shell to JavaScript #

When executing commands in the shell, the environment will be contained to the shell and not exported to the current process. For example:

await $`cd src && export MY_VALUE=5`;
// will output nothing
await $`echo $MY_VALUE`;
// will both NOT output it's in the src dir
await $`echo $PWD`;
console.log(Deno.cwd());

You can change that by using exportEnv() on the command:

await $`cd src && export MY_VALUE=5`.exportEnv();
// will output "5"
await $`echo $MY_VALUE`;
// will both output it's in the src dir
await $`echo $PWD`;
console.log(Deno.cwd());

Timeout a command #

This will exit with code 124 after 1 second.

// timeout a command after a specified time
await $`echo 1 && sleep 100 && echo 2`.timeout("1s");

Aborting a command #

Instead of awaiting the template literal, you can get a command child by calling the .spawn() method:

const child = $`echo 1 && sleep 100 && echo 2`.spawn();

await doSomeOtherWork();
child.kill(); // defaults to "SIGTERM"
await child; // Error: Aborted with exit code: 124

KillController

In some cases you might want to send signals to many commands at the same time. This is possible via a KillController.

import $, { KillController } from "...";

const controller = new KillController();
const signal = controller.signal;

const promise = Promise.all([
  $`sleep 1000s`.signal(signal),
  $`sleep 2000s`.signal(signal),
  $`sleep 3000s`.signal(signal),
]);

$.sleep("1s").then(() => controller.kill()); // defaults to "SIGTERM"

await promise; // throws after 1 second

Combining this with the CommandBuilder API and building your own $ as shown later in the documentation, can be extremely useful for sending a Deno.Signal to all commands you've spawned.

Modifying a command before it is spawned #

Use .beforeCommand(callback) to register a hook that runs immediately before each command is spawned. The callback receives the current builder and may return a (possibly modified) builder — useful when an env var depends on an asynchronous operation, such as fetching a token:

await $`./build.sh`
  .beforeCommand(async (builder) => {
    return builder.env("AUTH_TOKEN", await getAccessToken());
  });

Multiple .beforeCommand(...) calls compose: each callback runs in the order it was registered, with the builder produced by the previous one.

Async hooks only run on the await / .then() path. Calling .spawn() on a builder with .beforeCommand hooks throws — for the streaming case, use the synchronous variant:

const child = $`./build.sh`
  .beforeCommandSync((builder) => builder.env("BUILD_ID", crypto.randomUUID()))
  .spawn();

.beforeCommandSync(callback) accepts a callback that returns synchronously (CommandBuilder or nothing). Sync hooks always run before async hooks during a single resolution pass.

Shell #

The shell is cross-platform and uses the parser from deno_task_shell.

Sequential lists:

// result will contain the directory in someDir
const result = await $`cd someDir ; deno eval 'console.log(Deno.cwd())'`;

Multi-line commands — write each command on its own line:

await $`
  echo one
  echo two
  echo three
`;

In multi-line input, errexit (set -e) is on by default — the first failing line stops the rest. Single-line input (a; b) will continue-on-failure. You can opt in or out explicitly with set -e / set +e:

// keep running even if an earlier line fails
await $`
  set +e
  (exit 3)
  echo still-ran
`;

Trailing &&, ||, |, and backslash-newline continue onto the next line as you'd expect.

Boolean lists:

// outputs to stdout with 1\n\2n
await $`echo 1 && echo 2`;
// outputs to stdout with 1\n
await $`echo 1 || echo 2`;

Pipe sequences:

await $`echo 1 | deno run main.ts`;

Redirects:

await $`echo 1 > output.txt`;
const gzippedBytes = await $`gzip < input.txt`.bytes();

Sub shells:

await $`(echo 1 && echo 2) > output.txt`;

Setting env var for command in the shell (generally you can just use .env(...) though):

// result will contain the directory in someDir
const result = await $`test=123 deno eval 'console.log(Deno.env.get('test'))'`;
console.log(result.stdout); // 123

Shell variables (these aren't exported):

// the 'test' variable WON'T be exported to the sub processes, so
// that will print a blank line, but it will be used in the final echo command
await $`test=123 && deno eval 'console.log(Deno.env.get('test'))' && echo $test`;

Env variables (these are exported):

// the 'test' variable WILL be exported to the sub processes and
// it will be used in the final echo command
await $`export test=123 && deno eval 'console.log(Deno.env.get('test'))' && echo $test`;

Variable substitution:

const result = await $`echo $TEST`.env("TEST", "123").text();
console.log(result); // 123

Custom cross-platform shell commands #

Currently implemented (though not every option is supported):

You can also register your own commands with the shell parser (see below).

Note that these cross-platform commands can be bypassed by running them through sh: sh -c <command> (ex. sh -c cp source destination). Obviously doing this won't work on Windows though.

Shell Options #

These can be configured via builder methods or when building a custom $:

const $ = build$({
  commandBuilder: (builder) =>
    builder
      .pipefail()
      .nullglob()
      .failglob()
      .globstar(false)
      .questionGlob(),
});

Cross-platform shebang support #

Users on unix-based platforms often write a script like so:

#!/usr/bin/env -S deno run
console.log("Hello there!");

...which can be executed on the command line by running ./file.ts. This doesn't work on the command line in Windows, but it does on all platforms in dax:

await $`./file.ts`;

Logging #

Dax comes with some helper functions for logging:

// logs with potential indentation
// Note: everything is logged over stderr by default
$.log("Hello!");
// log with the first word as bold green
$.logStep("Fetching data from server...");
// or force multiple words to be green by using two arguments
$.logStep("Setting up", "local directory...");
// similar to $.logStep, but with red
$.logError("Error Some error message.");
// similar to $.logStep, but with yellow
$.logWarn("Warning Some warning message.");
// logs out text in gray
$.logLight("Some unimportant message.");

You may wish to indent some text when logging, use $.logGroup to do so:

// log indented within (handles de-indenting when an error is thrown)
await $.logGroup(async () => {
  $.log("This will be indented.");
  await $.logGroup(async () => {
    $.log("This will indented even more.");
    // do maybe async stuff here
  });
});

// or use $.logGroup with $.logGroupEnd
$.logGroup();
$.log("Indented 1");
$.logGroup("Level 2");
$.log("Indented 2");
$.logGroupEnd();
$.logGroupEnd();

As mentioned previously, Dax logs to stderr for everything by default. This may not be desired, so you can change the current behaviour of a $ object by setting a logger for either "info", "warn", or "error".

// Set the loggers. For example, log everything
// on stdout instead of stderr
$.setInfoLogger(console.log);
$.setWarnLogger(console.log);
$.setErrorLogger(console.log);

// or a more advanced scenario
$.setInfoLogger((...args: any[]) => {
  console.error(...args);
  // write args to a file here...
};)

Making requests #

Dax ships with a slightly less verbose wrapper around fetch that will throw by default on non-2xx status codes (this is configurable per status code).

Download a file as JSON:

const data = await $.request("https://plugins.dprint.dev/info.json").json();
console.log(data.plugins);

Or as text:

const text = await $.request("https://example.com").text();

Or get the long form:

const response = await $.request("https://plugins.dprint.dev/info.json");
console.log(response.code);
console.log(await response.json());

Requests can be piped to commands:

const request = $.request("https://plugins.dprint.dev/info.json");
await $`deno run main.ts`.stdin(request);

// or as a redirect... this sleeps 5 seconds, then makes
// request and redirects the output to the command
await $`sleep 5 && deno run main.ts < ${request}`;

See the documentation on RequestBuilder for more details. It should be as flexible as fetch, but uses a builder API (ex. set headers via .header(...)).

Showing progress

You can have downloads show a progress bar by using the .showProgress() builder method:

const url = "https://dl.deno.land/release/v1.29.1/deno-x86_64-unknown-linux-gnu.zip";
const downloadPath = await $.request(url)
  .showProgress()
  .pipeToPath();

Custom progress reporting

If you'd rather render your own progress UI (or report progress somewhere other than the terminal), use .onProgress(callback) instead. The callback fires once per chunk read from the response body with the cumulative bytes received and the total expected size:

await $.request(url)
  .onProgress(({ loaded, total }) => {
    if (total != null) {
      console.log(`${(loaded / total * 100).toFixed(1)}%`);
    } else {
      console.log(`${loaded} bytes`);
    }
  })
  .pipeToPath();

total is taken from the content-length response header and will be undefined if the server doesn't provide one. Multiple callbacks may be registered by calling .onProgress repeatedly — each is invoked in the order it was added. .onProgress is independent of .showProgress, so the two can be combined or used on their own.

Modifying a request before it is sent

Use .beforeRequest(callback) to register a hook that runs immediately before the request is sent. The callback receives the current builder and may return a (possibly modified) builder — useful when a header value depends on an asynchronous operation, such as fetching an auth token:

$.request(`${baseUrl}${path}`)
  .header("Content-Type", "application/json")
  .beforeRequest(async (builder) => {
    return builder.header("Authorization", `Bearer ${await getAccessToken()}`);
  });

Multiple .beforeRequest(...) calls compose: each callback runs in the order it was registered, with the builder produced by the previous one.

Selections / Prompts #

There are a few selections/prompts that can be used.

By default, all prompts will exit the process if the user cancelled their selection via ctrl+c. If you don't want this behaviour, then use the maybe variant functions.

$.alert

Shows a message and blocks until the user acknowledges it. By default, any key press dismisses the alert:

await $.alert("Backup complete!");

// or require the user to press Enter
await $.alert("Backup complete!", {
  requireEnter: true,
});

// or provide an object
await $.alert({
  message: "Backup complete!",
  noClear: true, // don't clear the text on dismissal
});

$.prompt / $.maybePrompt

Gets a string value from the user:

const name = await $.prompt("What's your name?");

// or provide an object, which has some more options
const name = await $.prompt({
  message: "What's your name?",
  default: "Dax", // prefilled value
  noClear: true, // don't clear the text on result
});

// or hybrid
const name = await $.prompt("What's your name?", {
  default: "Dax",
});

// with a character mask (for password / secret input)
const password = await $.prompt("What's your password?", {
  mask: true,
});

Again, you can use $.maybePrompt("What's your name?") to get a nullable return value for when the user presses ctrl+c.

$.confirm / $.maybeConfirm

Gets the answer to a yes or no question:

const result = await $.confirm("Would you like to continue?");

// or with more options
const result = await $.confirm({
  message: "Would you like to continue?",
  default: true,
});

// or hybrid
const result = await $.confirm("Would you like to continue?", {
  default: false,
  noClear: true,
});

$.select / $.maybeSelect

Gets a single value:

const colours = ["Red", "Green", "Blue"];
const result = await $.select({
  message: "What's your favourite colour?",
  options: colours,
});

console.log(result.index); // e.g. 0
console.log(result.value); // e.g. "Red"
console.log(colours[result]); // also works — coerces to the index

$.multiSelect / $.maybeMultiSelect

Gets multiple or no values:

const result = await $.multiSelect({
  message: "Which of the following are days of the week?",
  options: [
    "Monday",
    {
      text: "Wednesday",
      selected: true, // defaults to false
    },
    "Blue",
  ],
});

for (const item of result) {
  console.log(item.index, item.value);
}

Progress indicator #

You may wish to indicate that some progress is occurring.

Indeterminate

const pb = $.progress("Updating Database");

await pb.with(async () => {
  // do some work here
});

The .with(async () => { ... }) API will hide the progress bar when the action completes including hiding it when an error is thrown. If you don't want to bother with this though you can just call pb.finish() instead.

const pb = $.progress("Updating Database");

try {
  // do some work here
} finally {
  pb.finish();
}

Determinate

Set a length to be determinate, which will display a progress bar:

const items = [/*...*/];
const pb = $.progress("Processing Items", {
  length: items.length,
});

await pb.with(async () => {
  for (const item of items) {
    await doWork(item);
    pb.increment(); // or use pb.position(val)
  }
});

Synchronous work

The progress bars are updated on an interval (via setInterval) to prevent rendering more than necessary. If you are doing a lot of synchronous work the progress bars won't update. Due to this, you can force a render where you think it would be appropriate by using the .forceRender() method:

const pb = $.progress("Processing Items", {
  length: items.length,
});

pb.with(() => {
  for (const item of items) {
    doWork(item);
    pb.increment();
    pb.forceRender();
  }
});

Path API #

The path API offers an immutable Path class via jsr:@david/path, which is a similar concept to Rust's PathBuf struct.

// create a `Path`
let srcDir = $.path("src");
// get information about the path
srcDir.isDirSync(); // false
// do actions on it
await srcDir.mkdir();
srcDir.isDirSync(); // true

srcDir.isRelative(); // true
srcDir = srcDir.resolve(); // resolve the path to be absolute
srcDir.isRelative(); // false
srcDir.isAbsolute(); // true

// join to get other paths and do actions on them
const textFile = srcDir.join("subDir").join("file.txt");
textFile.writeSync("some text");
console.log(textFile.readTextSync()); // "some text"

const jsonFile = srcDir.join("otherDir", "file.json");
console.log(jsonFile.parentOrThrow()); // path for otherDir
jsonFile.writeJsonSync({
  someValue: 5,
});
console.log(jsonFile.readJsonSync().someValue); // 5

It also works to provide these paths to commands:

const srcDir = $.path("src").resolve();

await $`echo ${srcDir}`;

Paths can be created in the following ways:

const pathRelative = $.path("./relative");
const pathAbsolute = $.path("/tmp");
const pathFileUrl = $.path(new URL("file:///tmp")); // converts to /tmp
const pathStringFileUrl = $.path("file:///tmp"); // converts to /tmp
const pathImportMeta = $.path(import.meta); // the path for the current module

There are a lot of helper methods here, so check the documentation on Path for more details.

Helper functions #

Sleeping asynchronously for a specified amount of time:

await $.sleep(100); // ms
await $.sleep("1.5s");
await $.sleep("1m30s");

Getting path to an executable based on a command name:

console.log(await $.which("deno")); // outputs the path to deno executable

Check if a command exists:

console.log(await $.commandExists("deno"));
console.log($.commandExistsSync("deno"));

Attempting to do an action until it succeeds or hits the maximum number of retries:

await $.withRetries({
  count: 5,
  // you may also specify an iterator here which is useful for exponential backoff
  delay: "5s",
  action: async () => {
    await $`cargo publish`;
  },
});

"Dedent" or remove leading whitespace from a string:

console.log($.dedent`
    This line will appear without any indentation.
      * This list will appear with 2 spaces more than previous line.
      * As will this line.

    Empty lines (like the one above) will not affect the common indentation.
  `);
This line will appear without any indentation.
  * This list will appear with 2 spaces more than previous line.
  * As will this line.

Empty lines (like the one above) will not affect the common indentation.

Remove ansi escape sequences from a string:

$.stripAnsi("[4mHello World[0m");
//=> 'Hello World'

CommandBuilder #

The builder APIs are what the library uses internally and they're useful for scenarios where you want to re-use some setup state. They're immutable so every function call returns a new object (which is the same thing that happens with the objects returned from $ and $.request).

CommandBuilder can be used for building up commands similar to what the tagged template $ does:

import { CommandBuilder } from "dax";

const commandBuilder = new CommandBuilder()
  .cwd("./subDir")
  .stdout("inheritPiped") // output to stdout and pipe to a buffer
  .noThrow();

const otherBuilder = commandBuilder
  .stderr("null");

const result = await commandBuilder
  // won't have a null stderr
  .command("deno run my_script.ts")
  .spawn();

const result2 = await otherBuilder
  // will have a null stderr
  .command("deno run my_script.ts")
  .spawn();

You can also register your own custom commands using the registerCommand or registerCommands methods:

const commandBuilder = new CommandBuilder()
  .registerCommand(
    "true",
    () => Promise.resolve({ code: 0 }),
  );

const result = await commandBuilder
  // now includes the 'true' command
  .command("true && echo yay")
  .spawn();

Default Commands

The CommandBuilder will always have the default cross-platform commands registered. You can unregister them by using the unregisterCommand function:

const commandBuilder = new CommandBuilder()
  .unregisterCommand("printenv"); // will use what's on the system now

RequestBuilder #

RequestBuilder can be used for building up requests similar to $.request:

import { RequestBuilder } from "dax";

const requestBuilder = new RequestBuilder()
  .header("SOME_VALUE", "some value to send in a header");

const result = await requestBuilder
  .url("https://example.com")
  .timeout("10s")
  .text();

Custom $ #

You may wish to create your own $ function that has a certain setup context (for example, custom commands or functions on $, a defined environment variable or cwd). You may do this by using the exported build$ with CommandBuilder and/or RequestBuilder, which is essentially what the main default exported $ uses internally to build itself. In addition, you may also add your own functions to $:

import { build$, createExecutableCommand } from "dax";

// creates a $ object with the provided starting environment
const $ = build$({
  commandBuilder: (builder) =>
    builder
      .registerCommand("deno", createExecutableCommand(Deno.execPath()))
      .cwd("./subDir")
      .env("HTTPS_PROXY", "some_value"),
  requestBuilder: (builder) =>
    builder
      .header("SOME_NAME", "some value"),
  extras: {
    add(a: number, b: number) {
      return a + b;
    },
  },
});

// this command will use the env described above, but the main
// process won't have its environment changed
await $`deno run my_script.ts`;

console.log(await $.request("https://plugins.dprint.dev/info.json").json());

// use your custom function
console.log($.add(1, 2));

This may be useful also if you want to change the default configuration. Another example:

const $ = build$({
  commandBuilder: (builder) =>
    builder.exportEnv()
      .noThrow(),
});

// since exportEnv() was set, this will now actually change
// the directory of the executing process
await $`cd test && export MY_VALUE=5`;
// will output "5"
await $`echo $MY_VALUE`;
// will both output it's in the test dir
await $`echo $PWD`;
// won't throw even though this command fails (because of `.noThrow()`)
await $`deno eval 'Deno.exit(1);'`;

Building $ from another $

You can build a $ from another $ by calling $.build$({ /* options go here */ }).

This might be useful in scenarios where you want to use a $ with a custom logger.

const local$ = $.build$();
local$.setInfoLogger((...args: any[]) => {
  // a more real example might be logging to a file
  console.log("Logging...");
  console.log(...args);
});
local$.log("Hello!");

Outputs:

Logging...
Hello!