Skip to content
Jason Smith edited this page Jan 31, 2022 · 5 revisions

How Does It Work?

What is It For?

Shell scripts are everywhere. If you need something done right now, you fire up vim, pound out a quick script, and run it. Problem solved.

But shell scripts are dangerous. Error handling is tricky, and there is edge-case behaviour everywhere. Moreover, you are often running these things as root, right? What is to stop you from accidentally running rm -rf / (written as rm -rf "$SAFE_TO_DELETE_DIR" in your script) and wiping your server?

Enter Deno. It is scriptish (roots in JavaScript), but not as much as bash. Unlike bash, it is type checked and has reasonable and predicatable error handling.

The most compelling feature to me is the sandbox. Even if I don't make my code 100% hack-proof, the sandbox lets me define bounds around what my program can or can't do. For the times when I can't test your code, this is very comforting.

Examples

Run an Inline Bash Script

Starting with something simple yet useful, this is an example of running a bash script using runner.

const pg = group();
try {
  console.log(
    await runner(emptyInput(), stringOutput())(pg).run({
      cmd: [
        "/bin/bash",
        "--login",
        "-c",
        "echo 'Hello, Deno.'",
      ],
    }),
  );
} finally {
  pg.close();
}

Raw stdin, stdout, and stderr from a process are streamed byte data. This is a simple definition, but it is not very useful. We need to be able to interpret data differently in different circumstances.

Streamed byte data is the fastest, so if we are just piping bytes from one process to another, we would use BytesIterableOutput() for stdout of process #1 and BytesIterableInput() for stdin of process #2.

If you have a small amount of data (it can be kept in memory), proc.stringInput() and proc.stringOutput() let you work with string data. For text data that is too big to fit in memory, or if you just want to work with real-time streamed text data, use proc.stringIterableInput() and proc.stringIterableOutput(). There is some overhead associated with processing streamed bytes into text lines, but this is how you will interact with process input and output much of the time.

My goal in writing proc was to put Deno process handling on par with bash. Simple bash scripts are wonderful, but they tend to grow unwieldy over time as things are added. I'd like to be able to replace some of my old bash scripts with something more robust, and Deno is the first scripting language I've found that feels like it could work for this.

First, there is Deno's "Secure by default." This is huge when I am writing admin scripts, where if I make a mistake, I can wipe out a server. The ability to define security boundaries from the command-line is a game changer to me. Second, there is Deno's approach to package management, which means I can just import what I want and use it without required project infrastructure. I can just write a script and run it. Third, there is tight coupling with Typescript, which means I get strongly typed dynamic programming. I want someone to catch my mistakes, but I also want to code as fast as possible.

But there is still the nagging problem of the process API in Deno. It feels a little bit like I am dropping down into a poorly abstracted C library. It is hard to use processes correctly in Deno with this API. I find that I often end up leaking resources or - sometimes - leaving orphaned processes hanging around. However, when I use the Deno process API correctly, it is very reliable, has predictable behavior, and it is fast.

proc provides a reasonable solution to the leaky resource problem and - at the same time - redefines the API to be more in line with modern JavaScript. I hope you find it useful and enjoyable!

Clone this wiki locally