npm.io
1.1.31 • Published 1 month ago

@j-o-r/sh

Licence
Apache License, Version 2.0
Version
1.1.31
Deps
0
Size
89 kB
Vulns
0
Weekly
0

@j-o-r/sh   npm License Codeberg

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`` → SHDispatch for .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: AsyncTracker via async_hooks.
  • Raw-by-default interpolation with explicit bashEscape quoting for untrusted argument values; full JSDoc.

No runtime deps. ESM-only (ES2020+).

Quick Install

npm i @j-o-r/sh

Usage

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 any

Advanced

  • 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 SH object 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 via pgrep -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 publish

Repo: Codeberg | Issues: Codeberg Issues

License

Apache-2.0 Jorrit Duin

Keywords