Template tag functions for shell command execution with streaming output, safe interpolation, and flexible I/O control.
- 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
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-tagimport { sh, cmd } from "sh-cmd-tag";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: ""
}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: "",
}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.
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-caseNote: cmd interpolation works the same way.
const files = ["file1.txt", "file2.txt"];
await sh`rm ${files}`;This becomes the following command:
rm file1.txt file2.txtNote: cmd interpolation works the same way.
for await (const chunk of sh.stream`npm install`) {
process.stdout.write(chunk);
}const result = await sh`command ${arg}`;Resolves to a ProcessResult object after command completion:
{
ok: true,
output: "command result\n",
debug: "",
}const result = await cmd`command ${arg}`;Resolves to a ProcessResult object after the command completes:
{
ok: true,
output: "command output\n",
debug: "",
}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.txtconst files = ["a.txt", "b.txt"];
await sh`rm ${files}`;Arrays become space-separated values. This becomes the following command:
rm a.txt b.txtReal-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.
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/directoryThe 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",
},
}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/userTo run tests:
npm test