This document covers best practices, common patterns, and pitfalls to avoid when using the command-stream library.
- Array Argument Handling
- String Interpolation
- Security Best Practices
- Error Handling
- Performance Tips
- Common Pitfalls
When you have multiple arguments in an array, pass the array directly to template interpolation. The library will automatically handle proper quoting for each element.
import { $ } from 'command-stream';
// CORRECT: Pass array directly
const args = ['file.txt', '--public', '--verbose'];
await $`command ${args}`;
// Executed: command file.txt --public --verbose
// CORRECT: Dynamic array building
const baseArgs = ['input.txt'];
if (isVerbose) baseArgs.push('--verbose');
if (isForce) baseArgs.push('--force');
await $`mycommand ${baseArgs}`;Calling .join(' ') on an array before passing to template interpolation is a common mistake that causes all elements to become a single argument.
// WRONG: Array becomes single argument
const args = ['file.txt', '--flag'];
await $`command ${args.join(' ')}`;
// Shell receives: ['command', 'file.txt --flag'] (1 argument!)
// CORRECT: Each element becomes separate argument
await $`command ${args}`;
// Shell receives: ['command', 'file.txt', '--flag'] (2 arguments)When combining static and dynamic arguments, use separate interpolations or arrays:
// CORRECT: Multiple interpolations
const file = 'data.txt';
const flags = ['--verbose', '--force'];
await $`process ${file} ${flags}`;
// CORRECT: Build complete array
const allArgs = [file, ...flags];
await $`process ${allArgs}`;
// WRONG: String concatenation
await $`process ${file + ' ' + flags.join(' ')}`;By default, all interpolated values are automatically quoted to prevent shell injection:
// User input is safely escaped
const userInput = "'; rm -rf /; echo '";
await $`echo ${userInput}`;
// Executed safely - input is quoted, not executedOnly use raw() with trusted, hardcoded command strings:
import { $, raw } from 'command-stream';
// CORRECT: Trusted command template
const trustedCmd = 'git log --oneline --graph';
await $`${raw(trustedCmd)}`;
// WRONG: User input with raw (security vulnerability!)
const userInput = req.body.command;
await $`${raw(userInput)}`; // DANGER: Shell injection!Paths containing spaces are automatically quoted:
const path = '/Users/name/My Documents/file.txt';
await $`cat ${path}`;
// Executed: cat '/Users/name/My Documents/file.txt'Always treat external input as potentially malicious:
// CORRECT: Auto-escaping protects against injection
const filename = req.query.file;
await $`cat ${filename}`;
// WRONG: Bypassing safety for user input
await $`${raw(userInput)}`;Add validation for critical operations:
import { $ } from 'command-stream';
async function deleteFile(filename) {
// Validate filename
if (filename.includes('..') || filename.startsWith('/')) {
throw new Error('Invalid filename');
}
await $`rm ${filename}`;
}Run commands with minimal required permissions:
// Use specific paths instead of wildcards when possible
await $`rm ${specificFile}`; // Better
await $`rm ${directory}/*`; // More riskyBy default, commands don't throw on non-zero exit codes:
const result = await $`ls nonexistent`;
if (result.code !== 0) {
console.error('Command failed:', result.stderr);
}Use shell settings for scripts that should fail on errors:
import { $, shell } from 'command-stream';
shell.errexit(true);
try {
await $`critical-operation`;
} catch (error) {
console.error('Critical operation failed:', error);
process.exit(1);
}const result = await $`command`;
switch (result.code) {
case 0:
console.log('Success:', result.stdout);
break;
case 1:
console.error('General error');
break;
case 127:
console.error('Command not found');
break;
default:
console.error(`Unknown error (code ${result.code})`);
}For commands that produce large outputs, use streaming to avoid memory issues:
// Memory efficient: Process chunks as they arrive
for await (const chunk of $`cat huge-file.log`.stream()) {
processChunk(chunk.data);
}
// Memory intensive: Buffers entire output
const result = await $`cat huge-file.log`;
processAll(result.stdout);Run independent commands in parallel:
// Sequential (slower)
await $`task1`;
await $`task2`;
await $`task3`;
// Parallel (faster)
await Promise.all([$`task1`, $`task2`, $`task3`]);Built-in commands are faster as they don't spawn system processes:
// Fast: Built-in command (pure JavaScript)
await $`mkdir -p build/output`;
// Slower: System command
await $`/bin/mkdir -p build/output`;Problem: Using .join(' ') before interpolation merges all arguments into one.
// WRONG
const args = ['file.txt', '--flag'];
await $`cmd ${args.join(' ')}`; // 1 argument: "file.txt --flag"
// CORRECT
await $`cmd ${args}`; // 2 arguments: "file.txt", "--flag"See Case Study: Issue #153 for detailed analysis.
Problem: Building commands with template strings creates single arguments.
// WRONG
const file = 'data.txt';
const flag = '--verbose';
await $`cmd ${`${file} ${flag}`}`; // 1 argument: "data.txt --verbose"
// CORRECT
await $`cmd ${file} ${flag}`; // 2 argumentsProblem: Commands return promises, forgetting await causes issues.
// WRONG: Command may not complete before next line
$`setup-task`;
$`main-task`; // May run before setup completes
// CORRECT: Wait for completion
await $`setup-task`;
await $`main-task`;Problem: Expecting immediate results without awaiting.
// WRONG
const cmd = $`echo hello`;
console.log(cmd.stdout); // undefined - not yet executed!
// CORRECT
const result = await $`echo hello`;
console.log(result.stdout); // "hello\n"Problem: Only checking stdout when errors go to stderr.
// INCOMPLETE
const result = await $`command`;
console.log(result.stdout);
// BETTER
const result = await $`command`;
if (result.code !== 0) {
console.error('Error:', result.stderr);
} else {
console.log('Success:', result.stdout);
}Problem: Assuming success without checking.
// WRONG
const result = await $`risky-command`;
processOutput(result.stdout); // May be empty on failure!
// CORRECT
const result = await $`risky-command`;
if (result.code === 0) {
processOutput(result.stdout);
} else {
handleError(result);
}- Pass arrays directly:
${args} - Use separate interpolations:
${file} ${flag} - Check exit codes after execution
- Use streaming for large outputs
- Validate user input before execution
- Use built-in commands when available
- Never use
args.join(' ')before interpolation - Never use
raw()with user input - Don't forget
awaiton commands - Don't assume success without checking
- Don't ignore stderr output
- README.md - Main documentation
- docs/case-studies/issue-153/README.md - Array.join() pitfall case study
- src/$.quote.mjs - Quote function implementation