Every language
13 linguagens, copy-ready. One at a time with syntax highlighting, or all inline.
JSJavaScript
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const run = promisify(execFile); // execFile: argv list, NO shell
const { stdout } = await run('git', ['status']); // injection-safe
console.log(stdout);exec() splices the command into one SHELL string — a semicolon in user input is command injection. execFile('git', ['status']) takes an argv list and never touches a shell; promisify turns its callback into an awaitable.
TSTypeScript
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
export async function run(
cmd: string,
args: readonly string[]
): Promise<{ stdout: string; stderr: string; code: number }> {
try {
const r = await execFileAsync(cmd, args, { encoding: 'utf8' });
return { stdout: r.stdout, stderr: r.stderr, code: 0 };
} catch (err: any) { // non-zero exit lands HERE
return { stdout: err.stdout, stderr: err.stderr, code: err.code };
}
}execFile resolves ONLY on exit code 0 — every other exit rejects, with the streams riding the rejection (err.stdout, err.stderr, err.code). Catch and repackage into {stdout, stderr, code}, or callers lose the output of every failing command.
GoGo
import (
"fmt"
"os/exec"
)
out, err := exec.Command("git", "status").Output() // argv, no shell
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
fmt.Fatalf("exit %d: %s", exitErr.ExitCode(), exitErr.Stderr)
}
fmt.Fatalf("spawn failed: %v", err) // binary missing, etc.
}
fmt.Print(string(out))Output() captures stdout; CombinedOutput() merges stderr into it — both drain the pipes internally, so no deadlock. cmd.Run runs WITHOUT capture (it only returns the error). A non-zero exit is a non-nil error carrying *ExitError; ExitCode() is the data. Pre-spawn failures (ENOENT) are not ExitErrors.
RsRust
use std::process::Command;
let out = Command::new("git")
.arg("status") // argv — there is no shell to inject into
.output() // captures stdout AND stderr, no deadlock
.expect("failed to start git");
println!("exit={}", out.status.code().unwrap_or(-1));
println!("stdout:\n{}", String::from_utf8_lossy(&out.stdout));output() spawns reader threads for both pipes — the deadlock class is designed out. status() runs the same command and returns ONLY the exit status, no capture. code() is None when the child was killed by a signal, not exited.
PHPPHP
$proc = proc_open(['git', 'status'], [ // argv ARRAY — no shell
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
], $pipes);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$code = proc_close($proc); // exit code is datashell_exec() and backticks ALWAYS go through sh — user input in that string is injection. proc_open with an ARRAY argv bypasses the shell entirely. When a shell is truly needed (pipes, globs), wrap every argument in escapeshellarg() — quoting is not optional.
PyPython
import subprocess
r = subprocess.run(
['git', 'status'], # argv list — no shell
capture_output=True, # stdout AND stderr, no deadlock
text=True, # str, not bytes
check=False, # non-zero exit is not an exception
)
print(r.returncode, r.stdout, r.stderr)run() (3.5+) replaced the check_output/call/Popen-communicate ceremony. check=False keeps a non-zero exit as r.returncode instead of raising CalledProcessError — branch on the code, don't assume zero. text=True (formerly universal_newlines) avoids the bytes-not-str surprise.
CC
#include <stdio.h>
#include <sys/wait.h>
FILE *p = popen("git status", "r"); // ALWAYS runs via sh -c
if (!p) { perror("popen"); return 1; }
char line[256];
while (fgets(line, sizeof line, p)) // drain stdout (stderr not captured)
fputs(line, stdout);
int st = pclose(p); // ENCODED wait status, not the exit code
if (WIFEXITED(st))
printf("exit=%d\n", WEXITSTATUS(st));popen ALWAYS goes through sh — untrusted input in that string is command injection; posix_spawn with an argv array is the no-shell route. pclose's return is the raw waitpid status: decode it with WIFEXITED/WEXITSTATUS — WEXITSTATUS(st), never st itself.
C#C#
using System.Diagnostics;
var psi = new ProcessStartInfo("git")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false, // required for redirects — and no shell
};
psi.ArgumentList.Add("status"); // argv entries — NOT an Arguments string
using var process = Process.Start(psi)!;
// Start BOTH readers before waiting — draining one pipe while the
// other fills is the deadlock again, just sequential:
Task<string> stdoutTask = process.StandardOutput.ReadToEndAsync();
Task<string> stderrTask = process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
Console.WriteLine($"exit={process.ExitCode}");
Console.WriteLine(await stdoutTask);
Console.Error.WriteLine(await stderrTask);ArgumentList.Add() appends one argv entry — the Arguments STRING re-introduces quoting and injection bugs. Kick off both ReadToEndAsync tasks, THEN WaitForExitAsync: awaiting stdout while stderr fills its pipe deadlocks. UseShellExecute=false is mandatory for any redirect.
JvJava
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.List;
Process p = new ProcessBuilder(List.of("git", "status")) // argv — no shell
.redirectErrorStream(true) // merge the streams
.start();
// Read the output BEFORE waitFor — the child blocks when its pipe
// buffer fills, and waitFor blocks waiting for the child: deadlock.
StringBuilder sb = new StringBuilder();
try (BufferedReader r = new BufferedReader(
new InputStreamReader(p.getInputStream()))) {
r.lines().forEach(line -> sb.append(line).append('\n'));
}
int code = p.waitFor(); // NOW safe — pipes already drained
System.out.print(code + "\n" + sb);The ordering rule IS the bug class: drain getInputStream() to EOF before waitFor(), or both sides block — a few hundred KB of output hangs it. redirectErrorStream(true) merges stderr into stdout so one reader captures both. waitFor() returns the exit code — it is data.
SwSwift
import Foundation
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/git")
process.arguments = ["status"] // argv array — no shell
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = pipe // both streams, one reader
try process.run()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
process.waitUntilExit() // AFTER the drain
let output = String(data: data, encoding: .utf8) ?? ""
print("exit=\(process.terminationStatus)")
print(output)readDataToEndOfFile() BEFORE waitUntilExit() — same ordering rule as Java: the child blocks on a full pipe while you wait. Sharing one Pipe merges the streams; for separate pipes, read each on a background queue via readabilityHandler. terminationStatus is the exit code.
KtKotlin
val process = ProcessBuilder(listOf("git", "status")) // argv — no shell
.redirectErrorStream(true)
.start()
// Drain the merged stream BEFORE waitFor — same JVM deadlock rule:
val output = process.inputStream.bufferedReader().readText()
val code = process.waitFor()
println("exit=$code\n$output")Same JVM rules as Java — argv list (never a shell string), redirectErrorStream(true) to merge the pipes, drain before waitFor. readText() replaces the whole read-loop: the shortest safe spelling on the JVM.
RbRuby
require 'open3'
stdout, stderr, status = Open3.capture3('git', 'status') # argv, no shell
puts status.exitstatus # exit code is data
puts stdoutOpen3.capture3 takes an argv array and never spawns a shell — backticks and system('git status') do. It drains both streams to EOF on a wait thread (the '3'), so no pipe deadlock. The third return is a Process::Status: .exitstatus is the code, .success? the predicate.
ZigZig
const std = @import("std");
const result = try std.process.Child.run(.{
.allocator = allocator,
.argv = &.{ "git", "status" }, // argv slice — std.process has no shell
});
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);
switch (result.term) {
.Exited => |code| std.debug.print("exit={d}\n{s}", .{ code, result.stdout }),
else => std.debug.print("killed by signal\n", .{}),
}Child.run captures both streams on threads internally — no deadlock to hand-roll, and there is no shell anywhere in std.process to opt into. result.term is a tagged union: .Exited carries the code; a signaled child is .Signal, not exit 1. The returned buffers are owned by your allocator — free them.