Dax
Cross-platform shell tools for Deno and Node.js inspired by zx.
Install / Use
/learn @dsherret/DaxREADME
dax
<img src="src/assets/logo.svg" height="150px" alt="dax logo">Cross-platform shell tools for Deno and Node.js inspired by zx.
Differences with zx
- Cross-platform shell.
- Makes more code work on Windows.
- Allows exporting the shell's environment to the current process.
- Uses deno_task_shell's parser.
- Has common commands built-in for better Windows support.
- Minimal globals or global configuration.
- Only a default instance of
$, but it's not mandatory to use this.
- Only a default instance of
- No custom CLI.
- Good for application code in addition to use as a shell script replacement.
- Named after my cat.
Install
Deno:
# or skip and import directly from `jsr:@david/dax@<version>`
deno add jsr:@david/dax
Node:
npm install dax
Executing commands
#!/usr/bin/env -S deno run --allow-all
import $ from "@david/dax"; // "dax" in Node
// run a command
await $`echo 5`; // outputs: 5
// outputting to stdout and running 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`,
]);
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"]
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
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();
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();
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")}`;
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());
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");
Silencing a command
Makes a command not output anything to stdout and stderr.
await $`echo 5`.quiet();
await $`echo 5`.quiet("stdout"); // or just stdout
await $`echo 5`.quiet("stderr"); // or just stderr
Output a command before executing it
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
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 doSomeOt
