Process - External Command Execution #
The process module enables safe, cross-platform execution of external commands and subprocess management, similar to Python's subprocess module.
Import #
use "std/process"
Quick Start #
use "std/process"
# Simple command execution
let result = process.run(["ls", "-la"])
if result.success()
puts(result.stdout())
end
# Streaming with spawn
let proc = process.spawn(["grep", "error"])
proc.stdin.write("line 1\nerror in line 2\n")
proc.stdin.close()
let matches = proc.stdout.read()
proc.wait()
Security First #
⚠️ IMPORTANT: Quest uses array-based arguments by default, preventing shell injection attacks:
# ✅ SAFE - Arguments properly escaped
let file = user_input
process.run(["cat", file])
# ❌ DANGEROUS - Don't concatenate user input into shell commands
process.shell("cat " .. user_input) # Vulnerable to injection!
Module Functions #
process.run(command, options?) #
Execute a command and wait for completion. Captures all output.
Parameters:
command(Array[Str]) - Command and arguments, e.g.,["ls", "-la", "/tmp"]options(Dict, optional):cwd(Str) - Working directoryenv(Dict[Str, Str]) - Environment variablestimeout(Int or Float) - Timeout in secondsstdin(Str or Bytes) - Data to send to stdin
Returns: ProcessResult object with:
stdout()→ Str - Standard output as UTF-8 stringstderr()→ Str - Standard error as UTF-8 stringstdout_bytes()→ Bytes - Raw stdout bytes (for binary data)stderr_bytes()→ Bytes - Raw stderr bytescode()→ Int - Exit code (0 = success)success()→ Bool - True if exit code is 0
Example:
use "std/process"
# Basic execution
let result = process.run(["git", "status", "--short"])
if result.success()
puts("Git status:")
puts(result.stdout())
else
puts("Git failed:", result.stderr())
end
# With options
let result = process.run(
["npm", "install"],
{
"cwd": "/projects/myapp",
"env": {"NODE_ENV": "production"},
"timeout": 300
}
)
# With stdin
let result = process.run(
["grep", "pattern"],
{"stdin": "line 1\nline 2 with pattern\n"}
)
puts(result.stdout())
ProcessResult Truthiness:
let result = process.run(["test", "-f", "file.txt"])
if result
puts("File exists (exit code 0)")
else
puts("File doesn't exist (non-zero exit)")
end
process.spawn(command, options?) #
Spawn a process with piped I/O for streaming and interactive use.
Parameters:
command(Array[Str]) - Command and argumentsoptions(Dict, optional):cwd(Str) - Working directoryenv(Dict[Str, Str]) - Environment variables
Returns: Process object with:
stdin(WritableStream) - Write to process stdinstdout(ReadableStream) - Read from process stdoutstderr(ReadableStream) - Read from process stderrwait()→ Int - Wait for completion, return exit codewait_with_timeout(seconds)→ Int or nil - Wait with timeout, nil on timeoutcommunicate(input)→ Dict - Write input, read output, wait for completionkill()- Terminate process forcefully (SIGKILL)terminate()- Terminate gracefully (SIGTERM on Unix)pid()→ Int - Get process ID
Example:
use "std/process"
# Streaming I/O
let proc = process.spawn(["sort"])
proc.stdin.write("zebra\napple\nmango\n")
proc.stdin.close()
let sorted = proc.stdout.read()
puts(sorted)
proc.wait()
# Interactive process
let proc = process.spawn(["python3", "-i"])
proc.stdin.write("print('Hello from Python')\n")
proc.stdin.write("2 + 2\n")
proc.stdin.write("exit()\n")
proc.stdin.close()
puts(proc.stdout.read())
proc.wait()
process.check_run(command, options?) #
Execute command and return stdout, raising error on non-zero exit. Fail-fast pattern for scripts.
Parameters:
command(Array[Str]) - Command and argumentsoptions(Dict, optional) - Same asrun()
Returns: Str - stdout on success
Raises: Error with stdout/stderr details on non-zero exit
Example:
use "std/process"
# Returns stdout if successful
let output = process.check_run(["git", "rev-parse", "HEAD"])
puts("Current commit:", output.trim())
# Raises error on failure
try
process.check_run(["make", "build"])
catch e
puts("Build failed:", e.message())
sys.exit(1)
end
process.shell(command, options?) #
⚠️ DANGEROUS - Execute command through system shell.
Security Warning: Vulnerable to command injection. Only use with fully trusted input.
Parameters:
command(Str) - Shell command stringoptions(Dict, optional) - Same asrun()
Returns: ProcessResult
Example:
use "std/process"
# Only use with trusted, hardcoded commands
let result = process.shell("ls *.txt | wc -l")
puts("Text files:", result.stdout().trim())
# ❌ NEVER with user input
let user_file = get_user_input()
process.shell("cat " .. user_file) # DANGEROUS!
# ✅ Use this instead
process.run(["cat", user_file]) # Safe
process.pipeline(commands) #
Execute multiple commands in a pipeline (stdout of each feeds stdin of next).
Parameters:
commands(Array[Array[Str]]) - Array of command arrays
Returns: ProcessResult with output of final command
Example:
use "std/process"
# Equivalent to: cat file.txt | grep error | wc -l
let result = process.pipeline([
["cat", "file.txt"],
["grep", "error"],
["wc", "-l"]
])
puts("Error lines:", result.stdout().trim())
# Complex pipeline
let result = process.pipeline([
["find", ".", "-name", "*.q"],
["xargs", "wc", "-l"],
["sort", "-n"],
["tail", "-5"]
])
puts("5 largest files:")
puts(result.stdout())
Process Object #
Returned by process.spawn() for streaming subprocess control.
Stream Access #
Access streams as properties:
let proc = process.spawn(["cat"])
proc.stdin.write("data")
proc.stdin.close()
let output = proc.stdout.read()
Methods #
wait() #
Wait for process to complete and return exit code.
let proc = process.spawn(["command"])
# ... interact with process ...
let code = proc.wait()
puts("Exited with code:", code)
wait_with_timeout(seconds) #
Wait with timeout. Returns exit code or nil on timeout.
let proc = process.spawn(["long-task"])
let code = proc.wait_with_timeout(30)
if code == nil
puts("Timed out!")
proc.kill()
else
puts("Completed with code:", code)
end
communicate(input) #
Write input to stdin, close it, read stdout/stderr, and wait for completion.
let proc = process.spawn(["grep", "pattern"])
let result = proc.communicate("line 1\nline 2\n")
puts("stdout:", result["stdout"])
puts("stderr:", result["stderr"])
puts("code:", result["code"])
kill() #
Forcefully terminate the process (SIGKILL on Unix, TerminateProcess on Windows).
let proc = process.spawn(["stuck-process"])
proc.kill()
proc.wait()
terminate() #
Gracefully request process termination (SIGTERM on Unix).
let proc = process.spawn(["server"])
proc.terminate() # Ask nicely
let code = proc.wait_with_timeout(5)
if code == nil
proc.kill() # Force kill if it didn't stop
end
pid() #
Get the process ID.
let proc = process.spawn(["command"])
puts("Process ID:", proc.pid())
Context Manager #
Process objects support the with statement for automatic cleanup:
with process.spawn(["cat"]) as proc
proc.stdin.write("data")
proc.stdin.close()
let output = proc.stdout.read()
puts(output)
end # Automatically calls proc.wait()
# Exception-safe cleanup
with process.spawn(["task"]) as proc
proc.stdin.write("data")
raise "Error!"
end # proc.wait() still called!
WritableStream (stdin) #
Stream for writing to process stdin.
Methods #
write(data) #
Write string or bytes to stdin.
Returns: Int - Number of bytes written
proc.stdin.write("text data")
proc.stdin.write(b"\xFF\x00") # Binary data
writelines(lines) #
Write multiple lines from an array.
proc.stdin.writelines(["line1\n", "line2\n", "line3\n"])
close() #
Close stdin stream (signals EOF to process).
proc.stdin.close()
flush() #
Flush write buffer to process.
proc.stdin.flush()
ReadableStream (stdout/stderr) #
Stream for reading from process output.
Methods #
read() / read(n) #
Read all output as UTF-8 string, or read up to n bytes.
let all = proc.stdout.read() # Read all
let chunk = proc.stdout.read(1024) # Read up to 1024 bytes
read_bytes() / read_bytes(n) #
Read as raw bytes.
let data = proc.stdout.read_bytes() # All bytes
let chunk = proc.stdout.read_bytes(100) # First 100 bytes
readline() #
Read one line (including newline character).
let line = proc.stdout.readline()
# Returns "" at EOF
readlines() #
Read all lines as an array.
let lines = proc.stdout.readlines()
for line in lines
puts("Line:", line.trim())
end
Common Patterns #
Check Command Success #
let result = process.run(["make", "test"])
if result.success()
puts("Tests passed!")
else
puts("Tests failed with code", result.code())
puts(result.stderr())
sys.exit(1)
end
Capture and Parse JSON #
use "std/process"
use "std/encoding/json"
let result = process.run(["gh", "api", "/user"])
let user = json.parse(result.stdout())
puts("Username:", user["login"])
Pipeline Data Processing #
# Count unique values in CSV column
let result = process.pipeline([
["cut", "-d,", "-f2", "data.csv"],
["sort"],
["uniq", "-c"]
])
puts(result.stdout())
Long-Running Task with Timeout #
let proc = process.spawn(["database-backup.sh"])
let code = proc.wait_with_timeout(3600) # 1 hour
if code == nil
puts("Backup timed out!")
proc.terminate()
proc.wait_with_timeout(30) # Grace period
proc.kill() # Force kill if needed
sys.exit(1)
elif code != 0
puts("Backup failed with code", code)
sys.exit(code)
else
puts("Backup completed successfully")
end
Interactive Process #
let proc = process.spawn(["python3"])
proc.stdin.writelines([
"import math\n",
"print(math.pi)\n",
"print(math.sqrt(16))\n",
"exit()\n"
])
proc.stdin.close()
puts(proc.stdout.read())
proc.wait()
Parallel Execution #
# Run multiple tasks in parallel
let tasks = [
process.spawn(["task1.sh"]),
process.spawn(["task2.sh"]),
process.spawn(["task3.sh"])
]
# Do other work while tasks run...
# Wait for all
let i = 0
while i < tasks.len()
let code = tasks[i].wait()
puts("Task", i + 1, "finished with code", code)
i = i + 1
end
Build System #
use "std/process"
fun build_project(target)
puts("Cleaning...")
process.check_run(["make", "clean"])
puts("Building", target .. "...")
try
let output = process.check_run(
["make", target],
{"timeout": 600}
)
puts("Build successful!")
return true
catch e
puts("Build failed:")
puts(e.message())
return false
end
end
if not build_project("release")
sys.exit(1)
end
Error Handling #
Command Not Found #
try
process.run(["nonexistent-command"])
catch e
if e.message().contains("No such file")
puts("Command not found")
end
end
Timeout Handling #
try
process.run(["slow-task"], {"timeout": 30})
catch e
if e.message().contains("timeout")
puts("Task took too long!")
end
end
Exit Code Checking #
let result = process.run(["grep", "pattern", "file.txt"])
# grep exit codes:
# 0 = found, 1 = not found, 2 = error
if result.code() == 0
puts("Pattern found")
elif result.code() == 1
puts("Pattern not found")
else
puts("Grep error:", result.stderr())
end
Cross-Platform Support #
Platform-Specific Commands #
use "std/sys"
use "std/process"
let cmd = if sys.platform == "win32"
["dir", "/B"]
else
["ls"]
end
let result = process.run(cmd)
puts(result.stdout())
Path Handling #
# Use forward slashes (works on all platforms)
let result = process.run(
["python", "scripts/build.py"],
{"cwd": "projects/myapp"}
)
Advanced Examples #
Data Pipeline #
use "std/process"
use "std/io"
# Process log files
let result = process.pipeline([
["cat", "access.log"],
["grep", "ERROR"],
["awk", "{print $1}"],
["sort"],
["uniq", "-c"],
["sort", "-rn"],
["head", "-10"]
])
puts("Top 10 error sources:")
puts(result.stdout())
Two-Way Communication #
# Send data, get processed result
let proc = process.spawn(["awk", "{print toupper($0)}"])
let result = proc.communicate("convert this to uppercase\n")
puts(result["stdout"]) # "CONVERT THIS TO UPPERCASE"
Resource Cleanup #
# Manual cleanup
let proc = process.spawn(["task"])
try
proc.stdin.write("data")
let output = proc.stdout.read()
ensure
proc.wait() # Always cleanup
end
# Automatic with 'with' statement (recommended)
with process.spawn(["task"]) as proc
proc.stdin.write("data")
let output = proc.stdout.read()
end # Auto cleanup
Best Practices #
1. Always Use Array Arguments #
# ✅ Safe
process.run(["rm", filename])
# ❌ Unsafe - Use only with trusted input
process.shell("rm " .. filename)
2. Always Wait for Processes #
# ✅ Good - explicit wait
let proc = process.spawn(["task"])
try
# ... work with process ...
ensure
proc.wait()
end
# ✅ Better - context manager
with process.spawn(["task"]) as proc
# ... work with process ...
end # Auto wait
3. Handle Timeouts #
# ✅ Prevent hangs
let result = process.run(
["external-service"],
{"timeout": 30}
)
4. Check Exit Codes #
# ✅ Explicit checking
let result = process.run(["critical-task"])
if not result.success()
puts("Task failed:", result.stderr())
sys.exit(result.code())
end
# ✅ Fail-fast pattern
try
process.check_run(["critical-task"])
catch e
puts("Task failed:", e.message())
sys.exit(1)
end
5. Close Stdin When Done #
# ✅ Signal EOF
proc.stdin.write("data")
proc.stdin.close() # Important!
let output = proc.stdout.read() # Won't block
Performance Tips #
Streaming for Large Data #
# ❌ Buffers entire output in memory
let result = process.run(["find", "/", "-name", "*.txt"])
# ✅ Stream line by line
let proc = process.spawn(["find", "/", "-name", "*.txt"])
proc.stdin.close()
while true
let line = proc.stdout.readline()
if line == ""
break
end
process_line(line)
end
proc.wait()
Parallel Execution #
# ✅ Run tasks in parallel
let procs = [
process.spawn(["task1"]),
process.spawn(["task2"]),
process.spawn(["task3"])
]
# Do other work...
# Wait for all
procs.each(fun (p) p.wait() end)
Limitations and Notes #
- Zombie processes: Processes must be waited on or they become zombies (Unix/macOS)
- Buffering: Child processes may buffer output - use unbuffered mode if needed
- Blocking:
read()blocks until EOF - usereadline()for line-by-line - Windows: Some Unix-specific features behave differently (signals, exit codes)
- Shell features: For pipes/redirects/globs, use
pipeline()orshell()(carefully)