Shell Commands
Flokkit Flow provides seamless integration with shell commands, allowing you to use external programs as nodes in your flow graphs. This enables powerful data processing pipelines that leverage existing command-line tools.
Overview
Shell commands can be integrated as:
Sources: Stream output from commands
Transforms: Process data through commands
Sinks: Send data to commands
All shell integrations are:
Type-safe
Async-native
Support streaming
Handle backpressure
Provide proper error handling
Basic Usage
Shell Command as Source
Use shell commands to generate or read data:
from flow import flow
from flow.shell import shell_source
# Stream lines from a file
cat_source = shell_source("cat /var/log/app.log")
await (
flow()
.source(cat_source, str)
.transform(parse_log_line, LogEntry)
.sink(store_in_db)
.execute()
)
# Stream real-time logs
tail_logs = shell_source("tail -f /var/log/syslog")
await (
flow()
.source(tail_logs, str)
.filter(lambda line: "ERROR" in line)
.sink(alert_on_error)
.execute()
)
Shell Command as Transform
Process data through external commands:
from flow.shell import shell_transform
# Text transformation with sed
clean_text = shell_transform("sed 's/[^a-zA-Z0-9 ]//g'")
await (
flow()
.source(dirty_texts, str)
.transform(clean_text, str)
.sink(save_clean_text)
.execute()
)
# JSON processing with jq
extract_field = shell_transform("jq -r '.user.name'")
await (
flow()
.source(json_documents, str)
.transform(extract_field, str)
.sink(collect_names)
.execute()
)
Shell Command as Sink
Send data to external commands:
from flow.shell import shell_sink
# Append to log file
append_log = shell_sink("tee -a application.log")
await (
flow()
.source(events, str)
.transform(format_event, str)
.sink(append_log)
.execute()
)
# Send notifications
notify = shell_sink("notify-send 'Alert'", input_mode="args")
await (
flow()
.source(alerts, str)
.sink(notify)
.execute()
)
Advanced Features
The ShellCommand Class
For more control, use the ShellCommand class directly:
from flow.shell import ShellCommand
cmd = ShellCommand(
"grep -E 'pattern'",
input_mode="stdin", # How to pass input: "stdin", "args", or "none"
output_mode="lines", # How to capture output: "lines", "all", or "none"
cwd="/path/to/dir", # Working directory
env={"VAR": "value"}, # Environment variables
timeout=30.0, # Command timeout in seconds
check=True, # Raise on non-zero exit
)
# Use as transform
result = await cmd("input text")
Input Modes
Control how data is passed to commands:
# stdin mode - data passed via stdin
grep = shell_transform("grep ERROR", input_mode="stdin")
# args mode - data passed as command arguments
echo = shell_transform("echo", input_mode="args")
# none mode - no input (useful for sources)
date = shell_source("date +%s", input_mode="none")
Output Modes
Control how output is captured:
# lines mode - output split by lines (default)
lines = shell_source("ls -1", output_mode="lines")
# all mode - entire output as single string
content = shell_transform("cat", output_mode="all")
# none mode - no output captured (useful for sinks)
writer = shell_sink("tee output.txt", output_mode="none")
Custom Parsers
Parse command output into custom types:
import json
# Parse JSON output
def parse_json(text: str) -> dict:
return json.loads(text)
json_source = shell_source(
"curl -s https://api.example.com/data",
dict,
parser=parse_json
)
# Parse numbers
def parse_int(text: str) -> int:
return int(text.strip())
number_gen = shell_source(
"seq 1 10",
int,
parser=parse_int
)
Error Handling
Handle command failures gracefully:
# Don't raise on failure
cmd = ShellCommand("grep pattern", check=False)
async def safe_grep(text: str) -> str:
result = await cmd(text)
return result or "No match found"
# With timeout
risky_cmd = ShellCommand(
"slow-command",
timeout=5.0 # Kill after 5 seconds
)
try:
result = await risky_cmd.run()
except TimeoutError:
print("Command timed out")
Complex Pipelines
Build sophisticated pipelines combining multiple shell commands:
# Data processing pipeline
read_csv = shell_source("csvcut -c name,age,city data.csv")
filter_adults = shell_transform("awk -F, '$2 >= 18'")
sort_by_name = shell_transform("sort -t, -k1")
to_json = shell_transform("csvjson", parser=json.loads)
results = []
await (
flow("CSV Processing")
.source(read_csv, str)
.transform(filter_adults, str)
.transform(sort_by_name, str)
.transform(to_json, dict)
.sink(results.append)
.execute()
)
Best Practices
Security
Avoid shell injection: Never pass untrusted input directly to shell commands
Use argument lists: Prefer list form over string commands
Validate input: Always validate data before passing to commands
# BAD - vulnerable to injection
user_input = "file.txt; rm -rf /"
cmd = shell_source(f"cat {user_input}") # DANGER!
# GOOD - safe from injection
cmd = ShellCommand(["cat", user_input]) # Safe
Performance
Stream large data: Use streaming for large files
Set timeouts: Prevent hanging on slow commands
Handle backpressure: Commands respect flow backpressure
# Stream large files efficiently
large_file = shell_source("zcat huge.gz")
await (
flow()
.source(large_file, str)
.batch(1000) # Process in batches
.transform(process_batch, List[str])
.sink(bulk_insert)
.execute()
)
Error Handling
Check return codes: Use
check=Truefor critical commandsHandle timeouts: Set reasonable timeouts
Log stderr: Capture error output for debugging
cmd = ShellCommand(
"important-command",
check=True,
timeout=30.0
)
try:
result = await cmd.run()
if result.stderr:
logger.warning(f"Command warnings: {result.stderr}")
except RuntimeError as e:
logger.error(f"Command failed: {e}")
except TimeoutError:
logger.error("Command timed out")
Examples
Log Analysis Pipeline
# Analyze web server logs
async def analyze_logs():
# Source: Read compressed logs
read_logs = shell_source("zcat /var/log/nginx/access.log.*.gz")
# Transform: Extract timestamps and status codes
extract_fields = shell_transform(
"awk '{print $4, $9}'",
parser=lambda s: s.strip().split()
)
# Transform: Filter 5xx errors
filter_errors = shell_transform(
"grep ' 5[0-9][0-9]$'",
check=False # Don't fail if no matches
)
errors = []
await (
flow("Log Analysis")
.source(read_logs, str)
.transform(extract_fields, list)
.filter(lambda x: len(x) == 2)
.transform(lambda x: f"{x[0]} {x[1]}", str)
.transform(filter_errors, str)
.sink(errors.append)
.execute()
)
print(f"Found {len(errors)} 5xx errors")
Data ETL Pipeline
# Extract, transform, and load data
async def etl_pipeline():
# Extract: Download data
download = shell_source(
"curl -s https://example.com/data.json | jq -c '.[]'"
)
# Transform: Process with Python
async def enrich_record(json_str: str) -> dict:
record = json.loads(json_str)
record['processed_at'] = datetime.now().isoformat()
return record
# Load: Insert into database (via CLI tool)
insert_cmd = shell_sink(
"psql -c 'INSERT INTO records VALUES (%s)'",
input_mode="args"
)
await (
flow("ETL Pipeline")
.source(download, str)
.transform(enrich_record, dict)
.transform(json.dumps, str)
.sink(insert_cmd)
.execute()
)
Integration with Middleware
Shell commands work seamlessly with Flokkit Flow’s middleware system:
from flow import LoggingMiddleware, RetryMiddleware
# Add logging and retry to shell commands
logger = LoggingMiddleware()
retry = RetryMiddleware(max_attempts=3)
await (
flow()
.with_middleware(logger, retry)
.source(shell_source("unstable-api-call"), str)
.transform(parse_response, dict)
.sink(save_result)
.execute()
)
Limitations
Platform dependent: Commands must exist on the system
Text-based: Shell I/O is text-based (binary needs encoding)
Process overhead: Each command spawns a new process
Security risks: Be careful with untrusted input
Summary
Shell command integration in Flokkit Flow provides:
Seamless integration with existing tools
Type-safe command execution
Full async/streaming support
Flexible input/output modes
Robust error handling
This enables building powerful data pipelines that leverage the entire ecosystem of command-line tools while maintaining the safety and composability of Flokkit Flow.