How do I run an arbitrary shell command from Deno?
Asked Answered
B

5

21

I want to run any arbitrary bash command from Deno, like I would with a child_process in Node. Is that possible in Deno?

Baluchistan answered 1/6, 2020 at 23:53 Comment(0)
A
19

Deno 1.28.0 added a new API to run a shell command: Deno.Command (--allow-run permission required)

let cmd = new Deno.Command("echo", { args: ["hello world"] });
let { code, stdout, stderr } = await cmd.output();
// stdout & stderr are a Uint8Array
console.log(new TextDecoder().decode(stdout)); // hello world

More advanced usage:

const command = new Deno.Command(Deno.execPath(), {
  args: [
    "eval",
    "console.log('Hello World')",
  ],
  stdin: "piped",
  stdout: "piped",
});
const child = command.spawn();

// open a file and pipe the subprocess output to it.
child.stdout.pipeTo(
  Deno.openSync("output", { write: true, create: true }).writable,
);

// manually close stdin
child.stdin.close();
const status = await child.status;

const s = await c.status;
console.log(s);

This API replaced the deprecated Deno.run


OLD ANSWER:

In order to run a shell command, you have to use Deno.run, which requires --allow-run permissions.

There's an ongoing discussion to use --allow-all instead for running a subprocess


The following will output to stdout.

// --allow-run
const process = Deno.run({
  cmd: ["echo", "hello world"]
});

// Close to release Deno's resources associated with the process.
// The process will continue to run after close(). To wait for it to
// finish `await process.status()` or `await process.output()`.
process.close();

If you want to store the output, you'll have to set stdout/stderr to "piped"

const process = Deno.run({
  cmd: ["echo", "hello world"], 
  stdout: "piped",
  stderr: "piped"
});


const output = await process.output() // "piped" must be set
const outStr = new TextDecoder().decode(output);

/* 
const error = await p.stderrOutput();
const errorStr = new TextDecoder().decode(error); 
*/

process.close();
Abuzz answered 2/6, 2020 at 9:36 Comment(3)
echo: program not found. Why is that?Spellman
What OS are you using? Try the whole path to the command. In linux usually /usr/bin/echoAbuzz
Windows with Powershell. I found out that I have to do this: new Deno.Command("pwsh", { args: ["-c", "echo", "hello world"] });Spellman
T
9

Make sure to await status or output of the child process created with Deno.run.

Otherwise, the process might be killed, before having executed any code. For example:

deno run --allow-run main.ts
main.ts:
const p = Deno.run({
  cmd: ["deno", "run", "--allow-write", "child.ts"],
});
const { code } = await p.status(); // (*1); wait here for child to finish
p.close();
child.ts:
// If we don't wait at (*1), no file is written after 3 sec delay
setTimeout(async () => {
  await Deno.writeTextFile("file.txt", "Some content here");
  console.log("finished!");
}, 3000);

Pass arguments via stdin / stdout:

main.ts:
const p = Deno.run({
  cmd: ["deno", "run", "--allow-write", "child.ts"],
  // Enable pipe between processes
  stdin: "piped",
  stdout: "piped",
  stderr: "piped",
});
if (!p.stdin) throw Error();

// pass input to child
await p.stdin.write(new TextEncoder().encode("foo"));
await p.stdin.close();

const { code } = await p.status();
if (code === 0) {
  const rawOutput = await p.output();
  await Deno.stdout.write(rawOutput); // could do some processing with output
} else { /* error */ }
child.ts:
import { readLines } from "https://deno.land/std/io/bufio.ts"; // convenient wrapper

// read given input argument
let args = "";
for await (const line of readLines(Deno.stdin)) {
  args += line;
}

setTimeout(async () => {
  await Deno.writeTextFile("file.txt", `Some content here with ${args}`);
  console.log(`${args} finished!`); // prints "foo finished!""
}, 3000);

There is also a good example resource in Deno docs.

Threepence answered 3/6, 2020 at 19:24 Comment(0)
B
4

You can do that with the run like this:

// myscript.js
Deno.run({
  cmd: ["echo", "hello world"]
})

You'll have to --allow-run when running the script in order for this to work:

deno run --allow-run ./myscript.js
Baluchistan answered 1/6, 2020 at 23:53 Comment(1)
He definitely doesn't have to use the --allow-all flag there's --allow-runNeckcloth
H
1

If your shell command prints out some messages before the process is about to end, you really want pipe stdin and stdout to your own streams and also throw an exception which you can catch.

You can even alter the output while piping the process streams to your own streams:

async function run(cwd, ...cmd) {
    const stdout = []
    const stderr = []
    cwd = cwd || Deno.cwd()

    const p = Deno.run({
        cmd,
        cwd,
        stdout: "piped",
        stderr: "piped"
    })
    console.debug(`$ ${cmd.join(" ")}`)

    const decoder = new TextDecoder()
    
    streams.readableStreamFromReader(p.stdout).pipeTo(new WritableStream({
        write(chunk) {
            for (const line of decoder.decode(chunk).split(/\r?\n/)) {
                stdout.push(line)
                console.info(`[ ${cmd[0]} ] ${line}`)
            }
        },
    }))

    streams.readableStreamFromReader(p.stderr).pipeTo(new WritableStream({
        write(chunk) {
            for (const line of decoder.decode(chunk).split(/\r?\n/)) {
                stderr.push(line)
                console.error(`[ ${cmd[0]} ] ${line}`)
            }
        },
    }))

    const status = await p.status()
    if (!status.success) {
        throw new Error(`[ ${cmd[0]} ] failed with exit code ${status.code}`)
    }
    return {
        status,
        stdout,
        stderr,
    }
}

If you don't have different logic for each writable stream, you can also combine them to one:

    streams.mergeReadableStreams(
        streams.readableStreamFromReader(p.stdout),
        streams.readableStreamFromReader(p.stderr),
    ).pipeTo(new WritableStream({
        write(chunk): void {
            for (const line of decoder.decode(chunk).split(/\r?\n/)) {
                console.error(`[ ${cmd[0]} ] ${line}`)
            }
        },
    }))
Hypophosphate answered 10/3, 2022 at 2:41 Comment(0)
R
0

Alternatively, you can also invoke shell command via task runner such as drake as below

import { desc, run, task, sh } from "https://deno.land/x/[email protected]/mod.ts";

desc("Minimal Drake task");
task("hello", [], async function () {
  console.log("Hello World!");
  await sh("deno run --allow-env src/main.ts");
});

run();

$ deno run -A drakefile.ts hello
Retinite answered 3/10, 2021 at 15:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.