Run a child process or execute shell commands in Deno
In the past, Deno has taken multiple shots at creating APIs for running child process or run shell commands & optionally collect the output. There is an API called Deno.run. There was another one called Deno.spawn. Recently, Deno has introduced another API called Deno.Command. This is the latest attempt on designing a versatile API for creating any child process. The Deno.Command API obsoletes Deno.spawn API. The Deno.Command API is still under unstable umbrella. In this article, we’ll see examples of Deno.Command API to create a sub process for:
- Running a child Deno process
- Running a child Node.js process
- Running a shell command
- Running any arbitrary shell script
Basic Usage
The Deno.Command API needs to be used with the new keyword. The C in Command is capital. The child process can be awaited to completion using async output API or sync outputSync API. The outputSync API blocks the event loop as expected.
// async version
const proc = await new Deno.Command("cmd", { args: ['1', '2'] }).output();
// sync version
const proc = new Deno.Command("cmd", { args: ['1', '2'] }).outputSync();
The basic usage of Deno.Command API is:
- Exec: The first input is the executable name. It is a string that’s either path to the executable, executable (if in path), or shell command, etc.
- args: The args in the options bag is a simple array of strings containing the command-line arguments for the child process.
- stdout: The default is inherit i.e. send child’s output to parent. If parent is not interested, the stdout can be set to null.
- stderr: Same as stdout.
- env: List of environment variables with values to be passed to the child process
The output (i.e. the proc variable in the sample code) will be available when the child process finishes. The output contains:
- success: true or false
- code: the child process exit code
- signal: set if child process caught a signal
- stdout: A getter to receive the output buffer from the child process
- stderr: A getter to receive the error buffer from the child process
// A sample of proc
{ success: true, code: 0, signal: null, stdout: [Getter], stderr: [Getter] }
The stdout and stderr are returned in raw format: Uint8Array. The returned buffer can be converted to string using TextDecoder APIs.
Regarding permissions, running a child process is subject to — allow-run permissions.
With this basic info, let’s jump over to some use-cases.
Running a child Deno process
To run a child Deno process, the exec should be the path to the Deno executable (available through Deno.execPath API). As Deno needs subcommands, run and child TS file is passed through args object.
child.ts
console.log("STANDARD OUTPUT FROM CHILD");
console.error("STANDARD ERROR FROM CHILD");
parent.ts
const td = new TextDecoder();
const p = await new Deno.Command(Deno.execPath(), { args: ["run", "child.ts"] })
.output();
const out = td.decode(p.stdout).trim();
const err = td.decode(p.stderr).trim();
console.log("STDOUT ==> ", out);
console.log("STDERR ==> ", err);
The output is as follows:
> deno run --allow-run --allow-read --unstable app.ts
STDOUT ==> STANDARD OUTPUT FROM CHILD
STDERR ==> STANDARD ERROR FROM CHILD
Running a child Node.js process
To run a child Node.js process, the exec should be the path to the Node.js executable. The path has to be specified unless the node executable is present in the path of the running user. The only input required to Node.js child process is the child file. The same child.ts is used in this use-case too.
parent.ts
const td = new TextDecoder();
const p = await new Deno.Command("node", { args: ["child.ts"] })
.output();
const out = td.decode(p.stdout).trim();
const err = td.decode(p.stderr).trim();
console.log("STDOUT ==> ", out);
console.log("STDERR ==> ", err);
The output is as follows:
> deno run --allow-run --allow-read --unstable app.ts
STDOUT ==> STANDARD OUTPUT FROM CHILD
STDERR ==> STANDARD ERROR FROM CHILD
Running a shell command
Running a shell command needs the command name as the first argument, followed by optional args. The following code runs ls -l in /var/tmp.
const td = new TextDecoder();
const p = await new Deno.Command("ls", { args: ["-l", "/var/tmp"] })
.output();
const out = td.decode(p.stdout).trim();
const err = td.decode(p.stderr).trim();
console.log("STDOUT ==> ", out);
console.log("STDERR ==> ", err);
The output is as follows:
> deno run --allow-run --allow-read --unstable app.ts
STDOUT ==> total 248
-rw-r--r-- 1 mayankc wheel 5649 Dec 5 15:21 a.sh
-rw-r--r-- 1 _windowserver wheel 81920 Dec 5 13:29 cbrgbc_1.sqlite
-rw-r--r-- 1 _windowserver wheel 32768 Dec 5 13:29 cbrgbc_1.sqlite-shm
-rw-r--r-- 1 _windowserver wheel 0 Dec 5 13:29 cbrgbc_1.sqlite-wal
drwxr-xr-x 2 root wheel 64 Dec 5 13:29 kernel_panics
drwxr-xr-x 281 mayankc wheel 8992 Jan 2 12:54 perfResults
-rwxr-xr-x 1 mayankc wheel 54 Dec 14 22:24 someScript.sh
STDERR ==>
Running arbitrary shell script
An arbitrary shell script either needs a full path or should be present in the path of the current user. Again, optional command-line args can be passed through the args object.
The following example runs a script called someScript.sh, that prints the first argument on stdout and second argument on stderr.
someScript.sh
echo "Hello world to $1"
echo "Hello world to $2" >&2
parent.ts
const td = new TextDecoder();
const p = await new Deno.Command(
"/Users/mayankc/Work/source/denoExamples/someScript.sh",
{ args: ["Mayank", "Choubey"] },
)
.output();
const out = td.decode(p.stdout).trim();
const err = td.decode(p.stderr).trim();
console.log("STDOUT ==> ", out);
console.log("STDERR ==> ", err);
The output is as follows:
> deno run --allow-run --allow-read --unstable app.ts
STDOUT ==> Hello world to Mayank
STDERR ==> Hello world to Choubey
More articles on similar topics can be seen in the magazine: The JS runtimes.