Skip to content

Repository files navigation

sh-cmd-tag

Template tag functions for shell command execution with streaming output, safe interpolation, and flexible I/O control.

Features

  • Template literal syntax for intuitive command construction
  • Safe interpolation with automatic shell escaping
  • Object/array interpolation - objects become command flags, arrays become arguments
  • Streaming output - real-time command output processing
  • Automatic shell escaping - protects against shell injection attacks
  • Comprehensive error handling with detailed ProcessError information
  • Both sync and async execution modes

Installation

This package is published to GitHub Packages. Create an .npmrc file in your project root to configure the registry:

.npmrc:

@chriscalo:registry=https://npm.pkg.github.com

Then install the package:

npm install @chriscalo/sh-cmd-tag

Quick Start

import { sh, cmd } from "sh-cmd-tag";

Direct command execution with cmd

const result = await cmd`echo "Hello World"`;

The resolved result object is a ProcessResult with information about the command execution:

{
  ok: true,
  output: "Hello World\n",
  debug: ""
}

Shell execution with sh

const result = await sh`echo "hello world" | wc -w`;

This example uses shell pipes to count words. The resolved result object contains:

{
  ok: true,
  output: "2\n",
  debug: "",
}

Escaped interpolation of variables

const filename = "my file.txt";
await sh`touch ${filename}`;

This automatically escapes the filename as 'my file.txt'. The final command that gets executed is:

touch 'my file.txt'

Note: cmd interpolation works similarly but without shell expansion.

Object interpolation for command flags

const options = { regexp: "error", "ignore-case": true, quiet: false };
await sh`grep app.log ${options}`;

This becomes the following command (note that falsy values like false are dropped):

grep app.log --regexp=error --ignore-case

Note: cmd interpolation works the same way.

Array interpolation for multiple arguments

const files = ["file1.txt", "file2.txt"];
await sh`rm ${files}`;

This becomes the following command:

rm file1.txt file2.txt

Note: cmd interpolation works the same way.

Streaming output

for await (const chunk of sh.stream`npm install`) {
  process.stdout.write(chunk);
}

API Reference

sh - Async Command Execution with Shell Expansion

const result = await sh`command ${arg}`;

Resolves to a ProcessResult object after command completion:

{
  ok: true,
  output: "command result\n",
  debug: "",
}

cmd - Async Command Execution without Shell Expansion

const result = await cmd`command ${arg}`;

Resolves to a ProcessResult object after the command completes:

{
  ok: true,
  output: "command output\n",
  debug: "",
}

Object/Array Interpolation

Objects are converted to command line flags and arrays become space-separated arguments:

const opts = { verbose: true, output: "file.txt" };
await sh`command ${opts}`;

Objects become --key=value pairs. This becomes the following command:

command --verbose --output=file.txt
const files = ["a.txt", "b.txt"];
await sh`rm ${files}`;

Arrays become space-separated values. This becomes the following command:

rm a.txt b.txt

Streaming

Real-time output processing using the Process class:

import { Process } from "sh-cmd-tag";

const process = new Process("npm run build");
process.start();

for await (const chunk of process.output) {
  console.log("Build output:", chunk.toString());
}

Stream chunks as they arrive during long-running operations.

Error Handling

You can choose whether commands throw exceptions on failure or return ProcessResult with .ok === false.

Throwing behavior (default):

try {
  await sh`ls /nonexistent/directory`;
} catch (error) {
  console.error(error);
}

The error will be an instance of ProcessError:

{
  name: "ProcessError",
  message: "Command failed with exit code 2: /bin/sh: 1: ls: cannot access '/nonexistent/directory': No such file or directory",
  code: 2,
  output: "",
  debug: "/bin/sh: 1: ls: cannot access '/nonexistent/directory': No such file or directory\n",
}

Non-throwing behavior with .safe:

const result = await sh.safe`ls /nonexistent/directory`;

The command that gets executed is:

ls /nonexistent/directory

The result variable contains information about the failed command execution:

{
  ok: false,
  output: "",
  debug: "/bin/sh: 1: ls: cannot access '/nonexistent/directory': No such file or directory\n",
  error: {
    name: "ProcessError",
    message: "Command failed with exit code 1: /bin/sh: 1: ls: cannot access '/nonexistent/directory': No such file or directory",
    code: 1,
    output: "",
    debug: "/bin/sh: 1: ls: cannot access '/nonexistent/directory': No such file or directory\n",
  },
}

Shell Escaping

All interpolated values are automatically escaped to protect against shell injection:

const userInput = "file with spaces; echo gotcha";
await sh`cat ${userInput}`;

This safely becomes the following command:

cat 'file with spaces; echo gotcha'

Use markSafeString() only for trusted input:

import { markSafeString } from "sh-cmd-tag";

const safeArgs = markSafeString("-la --color=auto");
await sh`ls ${safeArgs} /home/user`;

No escaping applied to marked safe strings. This becomes:

ls -la --color=auto /home/user

Testing

To run tests:

npm test

About

Template tag functions for shell command execution with streaming output, safe interpolation, and flexible I/O control

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages