vl (violet)

deno CI

Shell scripting with TypeScript. Runs on top of Deno.

vl started as a port of zx to deno. The main motivation for this tool was the lack of native typescript support in zx. That’s where I got the idea to use deno as the runtime, since it supports TypeScript natively.

Table of contents

Motivation

Bash is great, but when it comes to writing scripts, it has it’s limitations and people tend to go for more expressive programming languages. JavaScript/TypeScript is a great choice, since they are approachable by a huge number of developers. The vl package provides provides wrappers around child_process, and a number of other things for all your shell scripting needs. Since vl uses deno, it has access to both the rich standard library of deno, and the battle tested standard libraries of node through it’s node compatibility.

Example Program

#!/usr/bin/env vl

import "https://deno.land/x/violet/globals.d.ts";

const { create, mkdir } = Deno;

await Promise.all([
  $`sleep 1; echo 1`,
  <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal"></mi><mi>s</mi><mi>l</mi><mi>e</mi><mi>e</mi><mi>p</mi><mn>2</mn><mo separator="true">;</mo><mi>e</mi><mi>c</mi><mi>h</mi><mi>o</mi></mrow><annotation encoding="application/x-tex">`sleep 2; echo </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord">‘</span><span class="mord mathnormal">s</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">ee</span><span class="mord mathnormal">p</span><span class="mord">2</span><span class="mpunct">;</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">ec</span><span class="mord mathnormal">h</span><span class="mord mathnormal">o</span></span></span></span>{2}`,
  $`sleep 3; echo 3`,
]);

await fetch("https://google.com");

await mkdir("tests");
await create("tests/test.txt");

cd("tests");
await $`ls`;
cd("..");

// clean up
await $`rm -rf tests`;

Usage

$command

Executes the given command by spawning a subprocess. Everything passed through ${} will be automatically quoted.

const fileName = "awesome";
await <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal"></mi><mi>t</mi><mi>o</mi><mi>u</mi><mi>c</mi><mi>h</mi></mrow><annotation encoding="application/x-tex">`touch </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord">‘</span><span class="mord mathnormal">t</span><span class="mord mathnormal">o</span><span class="mord mathnormal">u</span><span class="mord mathnormal">c</span><span class="mord mathnormal">h</span></span></span></span>{awesome}.txt`;

You can also pass an array of arguments.

const flags = ["--oneline", "--decorate", "--color"];
const { exitCode } = await <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal"></mi><mi>g</mi><mi>i</mi><mi>t</mi><mi>l</mi><mi>o</mi><mi>g</mi></mrow><annotation encoding="application/x-tex">`git log </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord">‘</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mord mathnormal">i</span><span class="mord mathnormal" style="margin-right:0.01968em;">tl</span><span class="mord mathnormal">o</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span></span></span></span>{flags}`;

Pipelines

#!/usr/bin/env vl

import "https://deno.land/x/violet/globals.d.ts";

await $`echo "Hello, stdout!"`.pipe(
  fs.createWriteStream("/tmp/output.txt", {}),
);

await $`cat /tmp/output.txt`;

Environment variables

#!/usr/bin/env vl

import "https://deno.land/x/violet/globals.d.ts";

Deno.env.set("FOO", "bar");

await <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal"></mi><mi>e</mi><mi>c</mi><mi>h</mi><mi>o</mi></mrow><annotation encoding="application/x-tex">`echo </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord">‘</span><span class="mord mathnormal">ec</span><span class="mord mathnormal">h</span><span class="mord mathnormal">o</span></span></span></span>FOO > tmp.txt`;
await $`cat tmp.txt`;

Functions

fetch works, since it’s natively supported in deno.

const resp = await fetch("https://wttr.in");
console.log(await resp.text());

ask reads a line from stdin.

const resp = await ask("What is your name?");
console.log(resp);

choose prompts the user to choose from the provided options.

const resp = await choose("Would you like a foo or a bar?", ["foo", "bar"]);
console.log(resp);

sleep sleeps for specified ms.

await sleep(2000);

noThrow() Changes the behaviour of $ to not throw an exception on non-zero exit codes. You can still access the exitCode from the response.

const { exitCode } = await noThrow($`exit 1`);
console.log(exitCode);

quiet() Changes the behaviour of $ to disable verbose output.

usage:

await quiet($`echo foobar`); // command and output will not be displayed.

sanitize() Changes the behaviour of $ to redact output.

usage:

await sanitize($`echo a SECRET_KEY`, ['SECRET_KEY']);
> a ********

os package.

usage:

await <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal"></mi><mi>e</mi><mi>c</mi><mi>h</mi><mi>o</mi></mrow><annotation encoding="application/x-tex">`echo </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord">‘</span><span class="mord mathnormal">ec</span><span class="mord mathnormal">h</span><span class="mord mathnormal">o</span></span></span></span>{os.homedir()}`;

retry()

Retries a command as many times as specified. Returns the first successful attempt, or will throw after specified attempts count.

usage:

await retry(5)`curl localhost`;

// or with a specified delay (500ms)
await retry(5, 500)`curl localhost`;

startSpinner()

Displays a spinner as an loading indicator. Useful with long running processes.

usage:

const { stopSpinner } = startSpinner();
await sleep(5000);
stopSpinner();

Configuration

$.shell

Which shell is used. Default is bash.

$.shell = "/usr/bin/zsh";

CLI argument --shell=/usr/bin/zsh can be used to achieve the same result.

$.verbose can be used to specify verbosity.

Default is true.

CLI argument --quiet sets verbosity to false.

Install

Prerequisites

You need to have deno installed. You can find installation instructions at deno.land.

deno install --allow-all -f https://deno.land/x/violet@<version_number>/vl.ts

Execution

There are two modes of execution, either using the shebang of #!/usr/bin/env vl or using a verbose deno command.

Shebang Mode

#!/usr/bin/env vl

import "https://deno.land/x/violet/globals.d.ts";

await $`echo 1`;

Deno basic mode

#!/usr/bin/env -S deno run --allow-all

import "https://deno.land/x/violet/globals.d.ts";
// Import the global functions into namespace, required in this method
import "https://deno.land/x/violet@0.1.0/globals.ts";

await $`echo 1`;

Credits

The project wouldn’t have been possible without these resources, so I’m forever grateful for the existence of these!