Recommended Free Tools
Write the script as a Node.js program, then start commands with node:child_process. For most tasks, use spawn() or execFile() with a fixed executable and a separate argument array. Use exec() only when you deliberately need shell features such as pipes or redirection—and never build its command string from untrusted input.
What a JavaScript shell script is
JavaScript does not replace the operating system’s shell. A JavaScript shell script is a Node.js program that coordinates other programs by starting child processes. Node’s built-in node:child_process module provides the main APIs: spawn(), execFile(), and exec(). Their differences matter because they determine whether a shell parses the command, how output is delivered, and how much control you have over arguments.
Choose the right way to run a command
| Option | Best for | Shell parsing | Output | Key consideration |
|---|---|---|---|---|
spawn() |
Long-running tasks or output that should appear as it is produced | Off by default | Streams | The executable and its flags may differ by operating system. |
execFile() |
Running one executable with a bounded set of arguments | Off by default on Unix-like systems | Buffered result | Windows .bat and .cmd files require a shell-aware strategy. |
exec() |
Commands that need pipes, globs, redirection, or compound shell syntax | On | Buffered result with a configurable limit | The command string is shell code; quoting and special characters vary by shell. |
| Google zx | Concise shell-like automation using JavaScript control flow | Uses a configurable shell wrapper | Promise-based process result | Still depends on the selected shell and installed commands. |
| ShellJS | Scripts that want familiar Unix-like command APIs | Library-dependent | API-oriented | Command behavior and availability can still vary across platforms. |
These APIs and their options are documented by Node.js. The safest default is to keep the executable and flags in your code and pass variable values as individual arguments.
Run a command and stream its output with spawn()
Use spawn() when the process may run for a while or when you want its output to flow directly to the terminal. This example runs Git’s status command in the current working directory:
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minute#1 Best Overall
import { spawn } from 'node:child_process';
const child = spawn('git', ['status', '--short'], { stdio: 'inherit' });
child.on('close', code => {
if (code !== 0) process.exitCode = code ?? 1;
});
The executable, git, is separate from the argument array. That preserves argument boundaries and avoids asking a shell to interpret the command. Setting stdio: 'inherit' connects the child’s standard input, output, and error streams to the parent’s terminal. The close handler propagates a nonzero exit status so that a failed Git command does not look like a successful script run.
For production scripts, decide deliberately whether to set options such as cwd, env, signal, timeout, or killSignal. These control the child’s working directory, environment, and cancellation behavior; consult the Node.js child-process documentation for platform-specific details.
Rank #2
Capture a bounded result with execFile()
When you need the output of a single executable rather than live streaming, execFile() is a useful fit. Promisifying it lets you use await:
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const run = promisify(execFile);
const { stdout } = await run('node', ['--version']);
console.log(stdout.trim());
On Unix-like systems, execFile() does not spawn a shell by default. That avoids shell parsing for ordinary executable calls, but output is collected rather than streamed. On Windows, directly launching .bat or .cmd files needs special handling; follow Node’s documented Windows guidance instead of assuming the Unix-like behavior applies.
Use exec() only when shell syntax is intentional
A pipe is shell grammar, so a shell must interpret it. For example, this command pipes Git output to head:
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
const runShell = promisify(exec);
const { stdout } = await runShell('git status --short | head -n 20', {
timeout: 10_000,
maxBuffer: 1024 * 1024,
});
console.log(stdout);
Node passes the command string to a shell when you call exec(). Shell metacharacters, quoting rules, and even shell selection can change the meaning of that string. The Node.js documentation warns that untrusted input combined with shell execution can enable arbitrary command execution.
Rank #4
- Do not concatenate user input, filenames, or other external values into an
exec()command string. - Prefer
spawn()orexecFile()with an argument array when shell syntax is unnecessary. - If shell syntax is essential, keep the command structure fixed, validate any variable values, and set an appropriate timeout and output limit.
Use zx for concise shell-like automation
Google zx wraps child-process operations to make scripts read more like shell commands while retaining JavaScript features such as variables and await. Install it with npm install zx, save a script as an .mjs file, and run it using the zx CLI or its documented shebang:
#!/usr/bin/env zx
const branch = await $`git branch --show-current`;
await $`git checkout -b ${'feature/example'}`;
console.log(branch.stdout.trim());
The zx documentation says interpolated values are escaped and quoted by its template tag. You can select the shell through the API, CLI, or environment, so check which shell the script will use. Escaping does not remove the need to validate inputs or account for the commands and shell installed on the target machine. See the zx documentation for installation and usage details.
Free tools Windows power users keep installed
One-click scans. No signup required.
Best Value
Use ShellJS for Unix-like command ergonomics
ShellJS provides a Node.js API for familiar Unix shell commands and describes itself as portable across Windows, Linux, and macOS. It can make file operations and command-oriented scripts feel more familiar, but it does not make every underlying command identical across platforms. Review how the library invokes commands, and verify command availability and input handling for the environments where the script will run.
Make the script reliable across machines
A JavaScript wrapper may run on several operating systems while the command it launches does not. Portability depends on the executable, its flags, the shell (if any), path conventions, quoting rules, and Windows handling of .bat or .cmd files. Before relying on a script across platforms, make these choices explicit:
- Input boundaries: Keep executable names and flags in code; pass external values as separate arguments and validate them.
- Failure behavior: Check exit codes and make errors visible. A fulfilled promise alone is not proof that the external task succeeded.
- Hanging processes: Add a timeout or cancellation signal for commands that may stall.
- Output handling: Choose whether output should stream to the terminal, be captured in memory, or be written to a file. Set
maxBufferwhen using buffered APIs. - Reproducibility: Set the working directory and environment explicitly when the script depends on them.
- Platform assumptions: Document the required shell, commands, paths, and Windows-specific behavior.
For API signatures, platform caveats, and process options, refer to the Node.js child_process reference. For zx’s shell configuration, consult its project documentation.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

