Skip to content

Instantly share code, notes, and snippets.

@MarkSFrancis
Created March 8, 2024 15:31
Show Gist options
  • Select an option

  • Save MarkSFrancis/f40f5cfacc540e37955c2d878229e8ca to your computer and use it in GitHub Desktop.

Select an option

Save MarkSFrancis/f40f5cfacc540e37955c2d878229e8ca to your computer and use it in GitHub Desktop.
Builder pattern with async execution, demoing how to create a tiny shell script utility

Async builder

DO NOT COPY THIS CODE INTO PRODUCTION!

For this specific example, you should use packages like zx or bun shell which are battle-tested shell script runners for javascript environments

This is meant to showcase what a builder pattern for await operators can do.

Example usage

// Does nothing
const myScript = $`ls -al`;

// Executes here
await myScript;
await $`ls -al`;
await $`ls -al`.setEnv({ key: 'value' });
await $`cd ${'./folder name'}`;

How it works

This works by creating a pseudo promise (called PromiseLike in Typescript) which creates a block of code that only executes when the developer calls await.

import { SpawnOptions, spawn } from 'child_process';
type $Result = PromiseLike<void> & {
setEnv: (newEnv: NodeJS.ProcessEnv) => $Result;
};
/**
* Run a shell command, printing its output to stdout and stderr accordingly
* @param text The command to execute with its arguments
* @param arguments Arguments to pass to the shell command
* @see Inspired by https://bun.sh/docs/runtime/shell
* @see Inspired by https://github.com/google/zx
*/
export const $ = (
text: TemplateStringsArray,
...values: (string | number)[]
): $Result => {
let opts: SpawnOptions = {};
const args = getScriptParts(text, ...values);
const cmd = args[0];
const cmdArgs: string[] = args.slice(1);
const sh: $Result = {
setEnv: (newEnv) => {
opts = {
...opts,
env: newEnv,
};
return sh;
},
then: (resolve, reject) => {
return new Promise<void>((resolve, reject) => {
console.info(`$ ${cmd} ${cmdArgs.join(' ')}`);
const process = spawn(cmd, cmdArgs, {
stdio: 'inherit',
...(opts ?? {}),
});
process.on('close', (code) => {
if (code) reject(new Error(`Command failed with exit code ${code}`));
else resolve();
});
}).then(resolve, reject);
},
};
return sh;
};
const getScriptParts = (
text: TemplateStringsArray,
...values: (string | number)[]
) => {
const args: string[] = [];
let curArg = '';
for (let i = 0; i < text.length; i++) {
let curText = text[i];
let argEnd = curText.indexOf(' ');
while (argEnd >= 0) {
curArg += curText.substring(0, argEnd);
curText = curText.substring(argEnd + 1);
argEnd = curText.indexOf(' ');
args.push(curArg);
curArg = '';
}
curArg += curText;
if (values[i]) {
curArg += `${values[i]}`;
}
}
if (curArg) {
args.push(curArg);
}
return args;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment