@j-o-r/sh
v1.1.31
Published
Execute shell commands on Linux-based systems from javascript
Maintainers
Readme
@j-o-r/sh

Execute shell commands from JavaScript on Linux.
Introduction
@j-o-r/sh is a lightweight Node.js module for running shell commands with a clean, zx-inspired API. It fixes namespace pollution, adds rolling timeouts (reset on stdout/stderr), no-shell mode (/usr/bin/env -S), process tree kills, buffer limits (1MB default), and a built-in Test framework + AsyncTracker for leaks.
Key Features:
- Template tag
SHcmd`` →SHDispatchfor.options().run(). - Global options:
SH.timeout = '5s'; SH.cwd = '/tmp';. - Sync/async
run()/runSync(); stdin payload; detached mode. - Utils:
sleep,retry,expBackoff,cd,parseArgs,userIn,readIn. - Testing:
new Test().add('name', () => assert(...)).run(). - Async leak detection:
AsyncTrackerviaasync_hooks. - Raw-by-default interpolation with explicit
bashEscapequoting for untrusted argument values; full JSDoc.
No runtime deps. ESM-only (ES2020+).
Quick Install
npm i @j-o-r/shUsage
Basics
import { SH, cd, sleep } from '@j-o-r/sh';
cd('/tmp');
const out = await SH`ls -la`.run();
console.log(out); // Captured stdout (trimmed)Interpolation: raw by default
Interpolated values are inserted into the shell command as raw shell source. This is convenient for trusted shell fragments such as flags, pipes, redirects, and compound expressions, but it is not safe for untrusted input.
import { SH, bashEscape } from '@j-o-r/sh';
const flags = ['-l', '-a'];
await SH`ls ${flags}`.run(); // Executes: ls -l -a
const fragment = 'printf ok; printf done';
await SH`${fragment}`.run(); // Raw shell syntax is intentional
const userInput = 'name; rm -rf /';
await SH`printf '%s\n' ${bashEscape(userInput)}`.run();
// Prints the value literally instead of executing `rm`.bashEscape(value) returns one POSIX-shell-quoted argument. It preserves empty
strings, whitespace, quotes, semicolons, glob characters, tabs, and newlines.
For arrays of untrusted values, quote each element yourself:
const args = ['two words', 'semi;colon', "quote's"];
await SH`printf '%s\n' ${args.map(bashEscape)}`.run();Chaining & Options
SH`curl -s ip.js.org`
.options({ timeout: '2s', shell: false }) // No-shell: /usr/bin/env -S
.run()
.catch(e => console.error(e.message)); // "Command failed with code 1: ..."Parallel & Context
import { within } from '@j-o-r/sh';
const results = await within(async () => Promise.all([
SH`sleep 1; echo ok`.run(),
sleep('500ms'),
SH`uname`.run()
]));Retry & Backoff
import { retry, expBackoff } from '@j-o-r/sh';
try {
const res = await retry(3, expBackoff('10s'), () =>
SH`curl -s unreachable`.run()
);
} catch (e) {
// Last error
}Interactive / Stdin
import { userIn, readIn } from '@j-o-r/sh';
// User prompt (abortable)
const { input, abort } = userIn('Password: ');
const pw = await input;
// Piped stdin
const out = await SH`grep secret`.run(await readIn());Vim / TTY (Sync)
SH`vim`.options({ stdio: 'inherit' }).runSync();Testing
Full-featured tester with async support, error reporting, unresolved Promise detection.
import { Test, assert, jsType } from '@j-o-r/sh';
const t = new Test(true); // quiet: no console
t.add('sync assert', () => assert.strictEqual(1 + 1, 2));
t.add('async', async () => {
await sleep('100ms');
assert.strictEqual(jsType([]), 'Array');
});
const report = await t.run();
console.log(report); // { tests: 2, executed: 2, duration: 150, errors: 0 }
t.unresolved(); // Logs leaks if anyAdvanced
SSH Example (interactive; use keys/sshpass for automation):
// TTY/inherit for prompts SH`ssh user@host`.options({ stdio: 'inherit' }).runSync(); // Or sshpass: SH`sshpass -p pw ssh user@host`.run()See
scenarios/for full demos.CLI Args:
parseArgs()→{ port: '8080', _: ['file'] }.Global Defaults: Set global options on the
SHobject to apply to all subsequent commands. These can be overridden per-command via.options(). Examples:SH.timeout = '5s';– Default timeout for all runs.SH.cwd = '/tmp';– Default working directory.SH.shell = false;– Disable shell mode (uses/usr/bin/env -S).SH.maxBuffer = 1 * 1024 * 1024;– Override default buffer limit (500kb) to 1MB per stream (stdout/stderr). Buffering captures output up to this limit; excess is truncated with markers.
Kill Tree:
dispatch.kill('SIGKILL')→ children viapgrep -P.
API
Full JSDoc in lib/*.js. Key exports:
| Utility | Description |
|---------|-------------|
| SHcmd`` | Template → {@link SHDispatch} |
| cd(dir) | process.chdir() |
| sleep('1s') | Promise delay |
| retry(3, '1s', fn) | Retry w/ delay/gen |
| userIn(prompt) | { input: Promise, abort() } |
| Test | Test runner |
| AsyncTracker | Async leak detector |
| parseArgs(argv) | CLI parser |
| bashEscape(value) | Quote one value as a POSIX shell argument for safe interpolation |
See types/SH.d.ts for TS defs.
Development
npm run types # Generate types/
npm test # Run scenarios/sh.js
npm run release # Pack for publish
npm run publish # npm publishRepo: Codeberg | Issues: Codeberg Issues
License
Apache-2.0 © Jorrit Duin
