Expand description
§scripty
Scripty - A simple and intuitive library that makes running shell commands and file operations easy and visible.
§Why scripty?
When you need to write system administration scripts, build tools, or automation in Rust,
you often find yourself wrestling with std::process::Command
and std::fs
. scripty
provides a clean, shell-script-like interface while keeping all the benefits of Rust’s
type safety and error handling.
§Key Features
- 🎨 Colorful output: See exactly what commands are being executed
- 🔗 Easy piping: Chain commands together naturally with stdout, stderr, or both
- 📁 File operations: Wrapper around
std::fs
with automatic logging - 🔧 Builder pattern: Fluent API for command construction
- ⚡ Minimal dependencies: Only uses
anstyle
for colors - 🛡️ Type safe: All the safety of Rust with the convenience of shell scripts
- 🚰 Streaming I/O: Efficient handling of large data with readers and writers
- 🔌 Reader Extensions: Fluent piping from any
Read
implementation to commands - ✍️ Write Methods: Direct output streaming to writers with stdout/stderr control
§Quick Start
Add this to your Cargo.toml
:
[dependencies]
scripty = "0.3.3"
§Platform Support
Currently supported platforms:
- Linux ✅ Full support with native pipe optimization
- macOS ✅ Full support with native pipe optimization
Scripty is designed for Unix-like systems and uses Unix shell commands and utilities.
§Requirements
- Rust 1.87.0 or later - Uses native
std::io::pipe
for optimal pipeline performance
§Security Considerations
⚠️ Warning: This library executes system commands with the privileges of the current process.
- Never pass untrusted user input directly to commands
- Validate and sanitize all inputs before use
- Be aware of command injection risks when constructing command arguments dynamically
- Consider using allowlists for command names and arguments when dealing with user input
Example of unsafe usage:
// DON'T DO THIS with untrusted input!
let user_input = get_user_input();
// cmd!("sh", "-c", user_input).run()?; // Dangerous!
§Basic Usage
§Command Execution
use scripty::*;
// Simple command execution
cmd!("echo", "Hello, World!").run()?;
// Get command output
let output = cmd!("date").output()?;
println!("Current date: {}", output.trim());
// Command with multiple arguments
cmd!("ls", "-la", "/tmp").run()?;
// Using the builder pattern
cmd!("grep", "error")
.arg("logfile.txt")
.current_dir("/var/log")
.env("LANG", "C")
.run()?;
§Reader-to-Command Piping
Pipe data directly from any Read
implementation to commands using the ReadExt
trait:
use scripty::*;
use std::fs::File;
use std::io::{BufReader, Cursor};
// Pipe file contents directly to commands
let file = File::open("data.txt")?;
let result = file.pipe(cmd!("grep", "pattern"))
.pipe(cmd!("sort"))
.pipe(cmd!("uniq"))
.output()?;
// Memory-efficient processing with BufReader
let large_file = File::open("huge_dataset.txt")?;
let reader = BufReader::new(large_file);
reader.pipe(cmd!("awk", "{sum += $1} END {print sum}"))
.run()?;
// In-memory data processing
let data = Cursor::new(b"zebra\napple\ncherry\n");
let sorted = data.pipe(cmd!("sort")).output()?;
§Command Piping
Chain commands together just like in shell scripts using native std::io::pipe
for enhanced performance and memory efficiency!
use scripty::*;
// Simple pipe (stdout)
cmd!("echo", "hello world")
.pipe(cmd!("tr", "[:lower:]", "[:upper:]"))
.run()?;
// Pipe stderr
cmd!("some-command")
.pipe_err(cmd!("grep", "ERROR"))
.run()?;
// Pipe both stdout and stderr
cmd!("some-command")
.pipe_out_err(cmd!("sort"))
.run()?;
// Multiple pipes using efficient native pipes
cmd!("cat", "/etc/passwd")
.pipe(cmd!("grep", "bash"))
.pipe(cmd!("wc", "-l"))
.run()?;
// Get piped output with efficient streaming
let result = cmd!("ps", "aux")
.pipe(cmd!("grep", "rust"))
.pipe(cmd!("wc", "-l"))
.output()?;
println!("Rust processes: {}", result.trim());
§Pipeline Performance Features
- Memory efficient: Uses streaming instead of buffering all data
- Better performance: Native pipes reduce process overhead
- Platform independent: No shell dependency for multi-command pipes
- Native implementation: Uses
std::io::pipe
for optimal performance
use scripty::*;
// Large data processing with efficient streaming
let large_data = "..."; // Megabytes of data
let result = cmd!("grep", "pattern")
.pipe(cmd!("sort"))
.pipe(cmd!("uniq", "-c"))
.input(large_data)
.output()?; // Processes without loading all data into memory
§Core API Reference
§The cmd!
Macro
The heart of scripty is the cmd!
macro for creating commands:
use scripty::*;
// Basic command
cmd!("ls").run()?;
// Command with arguments
cmd!("ls", "-la").run()?;
// Multiple arguments
cmd!("echo", "Hello", "World").run()?;
§Command Builder Methods
Commands support a fluent builder pattern:
use scripty::*;
cmd!("grep", "error")
.arg("logfile.txt") // Add single argument
.args(["--color", "always"]) // Add multiple arguments
.current_dir("/var/log") // Set working directory
.env("LANG", "C") // Set environment variable
.no_echo() // Suppress command echoing
.run()?;
§Execution Methods
Different ways to execute commands:
use scripty::*;
// Execute and check exit status
cmd!("echo", "hello").run()?;
// Capture text output
let output = cmd!("date").output()?;
println!("Current date: {}", output.trim());
// Capture binary output
let bytes = cmd!("cat", "binary-file").output_bytes()?;
§Output Streaming with Write Methods
Stream command output directly to writers with precise control over stdout/stderr:
use scripty::*;
use std::fs::File;
// Stream stdout to a file
let output_file = File::create("output.txt")?;
cmd!("ls", "-la").write_to(output_file)?;
// Stream stderr to an error log
let error_file = File::create("errors.log")?;
cmd!("risky-command").write_err_to(error_file)?;
// Stream both stdout and stderr to the same destination
let combined_file = File::create("full.log")?;
cmd!("verbose-app").write_both_to(combined_file)?;
// Use with any Writer (Vec, File, Cursor, etc.)
let mut buffer = Vec::new();
cmd!("echo", "test").write_to(&mut buffer)?;
§Input Methods
Provide input to commands in various ways:
use scripty::*;
use std::io::Cursor;
// Text input
let result = cmd!("sort")
.input("banana\napple\ncherry\n")
.output()?;
println!("Sorted fruits: {}", result.trim());
// Binary input
let bytes = cmd!("cat")
.input_bytes(b"binary data")
.output_bytes()?;
// Stream from reader using ReadExt
use std::fs::File;
let file = File::open("data.txt")?;
file.pipe(cmd!("sort")).run()?;
// Buffered reading for large files
use std::io::BufReader;
let large_file = File::open("large.txt")?;
let reader = BufReader::new(large_file);
reader.pipe(cmd!("grep", "pattern")).run()?;
§Advanced I/O Control with spawn_io_* Methods
For complex I/O scenarios, use the spawn methods for non-blocking control:
use scripty::*;
use std::io::{BufRead, BufReader, Write};
use std::thread;
// Full I/O control with spawn_io_all
let spawn = cmd!("sort").spawn_io_all()?;
// Handle input in separate thread
if let Some(mut stdin) = spawn.stdin {
thread::spawn(move || {
writeln!(stdin, "zebra").unwrap();
writeln!(stdin, "apple").unwrap();
writeln!(stdin, "banana").unwrap();
});
}
// Read output
if let Some(stdout) = spawn.stdout {
let reader = BufReader::new(stdout);
for line in reader.lines() {
println!("Line: {}", line?);
}
}
spawn.handle.wait()?;
§spawn_io_* Method Variants
Choose the right spawn method for your needs:
use scripty::*;
use std::io::Write;
// spawn_io_in - Control stdin only (common for sending data)
let (handle, stdin) = cmd!("grep", "pattern").spawn_io_in()?;
if let Some(mut stdin) = stdin {
writeln!(stdin, "test line with pattern")?;
writeln!(stdin, "another line")?;
drop(stdin); // Close to signal EOF
}
handle.wait()?;
// spawn_io_out - Control stdout only (common for reading output)
let (handle, stdout) = cmd!("ls", "-la").spawn_io_out()?;
if let Some(stdout) = stdout {
use std::io::Read;
let mut output = String::new();
stdout.take(100).read_to_string(&mut output)?;
println!("First 100 bytes: {}", output);
}
handle.wait()?;
// spawn_io_in_out - Control both stdin and stdout (interactive commands)
let (handle, stdin, stdout) = cmd!("sort", "-n").spawn_io_in_out()?;
// Send numbers to sort
if let Some(mut stdin) = stdin {
writeln!(stdin, "42")?;
writeln!(stdin, "7")?;
writeln!(stdin, "100")?;
drop(stdin);
}
// Read sorted output
if let Some(stdout) = stdout {
use std::io::BufRead;
let reader = std::io::BufReader::new(stdout);
for line in reader.lines() {
println!("Sorted: {}", line?);
}
}
handle.wait()?;
// spawn_io_err - Control stderr only (error monitoring)
let (handle, stderr) = cmd!("command-with-errors").spawn_io_err()?;
if let Some(mut stderr) = stderr {
use std::io::Read;
let mut errors = String::new();
stderr.read_to_string(&mut errors)?;
eprintln!("Errors: {}", errors);
}
handle.wait()?;
§Simple Reader-to-Writer Operations
For straightforward input-to-output scenarios:
use scripty::*;
use std::fs::File;
use std::io::Cursor;
// Process file data through command (stdout)
let input_file = File::open("data.txt")?;
let output_file = File::create("sorted.txt")?;
cmd!("sort").run_with_io(input_file, output_file)?;
// Capture error output while processing
let source_code = Cursor::new("fn main() { invalid syntax }");
let mut error_log = Vec::new();
let _ = cmd!("rustc", "-").run_with_err_io(source_code, &mut error_log);
println!("Compilation errors: {}", String::from_utf8_lossy(&error_log));
// Capture both stdout and stderr for comprehensive logging
let input_data = Cursor::new("test data\nmore data");
let log_file = File::create("process.log")?;
cmd!("complex-tool").run_with_both_io(input_data, log_file)?;
§File System Operations
All file operations are automatically logged:
use scripty::*;
// Basic file operations
fs::write("config.txt", "debug=true\nport=8080")?;
let content = fs::read_to_string("config.txt")?;
println!("Config: {}", content);
// Directory operations
fs::create_dir_all("project/src")?;
fs::copy("config.txt", "project/config.txt")?;
// Directory traversal
for entry in fs::read_dir("project")? {
let entry = entry?;
println!("Path: {}", entry.path().display());
}
// Cleanup
fs::remove_file("config.txt")?;
fs::remove_dir_all("project")?;
§Error Handling
Use standard Rust error handling patterns:
use scripty::*;
// Handle command failures gracefully
match cmd!("nonexistent-command").run() {
Ok(_) => println!("Command succeeded"),
Err(e) => println!("Command failed: {}", e),
}
// Check command availability
if cmd!("which", "git").no_echo().run().is_ok() {
println!("Git is available");
cmd!("git", "--version").run()?;
}
// Use the ? operator for early returns
fn deploy_app() -> Result<()> {
cmd!("cargo", "build", "--release").run()?;
cmd!("docker", "build", "-t", "myapp", ".").run()?;
cmd!("docker", "push", "myapp").run()?;
println!("Deployment complete!");
Ok(())
}
§Global Configuration
Control scripty’s behavior with environment variables:
NO_ECHO
: Set to any value to suppress command echoing globally
NO_ECHO=1 cargo run # Run without command echoing
Or use the .no_echo()
method on individual commands.
§Examples
This crate includes focused examples showcasing scripty’s core strengths: pipeline operations and I/O handling:
Examples are numbered for optimal learning progression:
01_simple_pipes.rs
- Basic pipeline operations and command chaining02_pipe_modes.rs
- Complete pipeline control with stdout/stderr piping03_read_ext.rs
- Fluent reader-to-command piping with ReadExt trait04_run_with_io.rs
- Blocking reader-writer I/O with run_with_*() methods05_spawn_io.rs
- Non-blocking I/O control with spawn_io_*() methods
Run examples in order for the best learning experience:
cargo run --example 01_simple_pipes # 1. Pipeline fundamentals
cargo run --example 02_pipe_modes # 2. Advanced piping control
cargo run --example 03_read_ext # 3. Reader-to-command piping
cargo run --example 04_run_with_io # 4. Blocking reader-writer I/O
cargo run --example 05_spawn_io # 5. Non-blocking I/O control
Learning Path: Start with 01_simple_pipes.rs
and progress through each numbered example in sequence to build your expertise with scripty’s pipeline and I/O capabilities.
§Real-World Example: cargo-xtask + clap + scripty
This project’s xtask/
demonstrates scripty with cargo-xtask and clap:
cargo xtask ci # Full CI pipeline
cargo xtask precommit # Pre-commit checks (includes version sync)
See xtask/src/main.rs
for the complete implementation combining all three tools.
§Advanced Pipeline Performance & Best Practices
§Performance Optimization
scripty’s native pipeline implementation provides significant performance benefits:
use scripty::*;
// ✅ Efficient: Native pipes with streaming
let result = cmd!("cat", "large_file.txt")
.pipe(cmd!("grep", "pattern"))
.pipe(cmd!("sort"))
.pipe(cmd!("uniq", "-c"))
.output()?; // Processes without loading all data into memory
// ✅ Memory efficient: Stream large data directly
use std::fs::File;
let large_file = File::open("multi_gb_file.txt")?;
large_file.pipe(cmd!("grep", "ERROR"))
.pipe(cmd!("wc", "-l"))
.output()?; // Handles gigabytes efficiently
§Pipeline Best Practices
Memory Management:
use scripty::*;
// ✅ Good: Stream processing for large data
cmd!("find", "/var/log", "-name", "*.log")
.pipe(cmd!("xargs", "grep", "ERROR"))
.pipe(cmd!("sort"))
.output()?;
// ❌ Avoid: Loading large outputs into memory first
// let large_output = cmd!("find", "/", "-type", "f").output()?; // Don't do this
// Instead, use streaming with pipes directly
cmd!("find", "/", "-type", "f")
.pipe(cmd!("grep", "pattern"))
.output()?;
Error-Prone Pipelines:
use scripty::*;
// ✅ Good: Graceful error handling in pipelines
match cmd!("risky-command")
.pipe(cmd!("sort"))
.no_echo()
.output()
{
Ok(result) => println!("Success: {}", result.trim()),
Err(_) => {
// Fallback strategy
println!("Using fallback approach");
cmd!("safe-alternative").run()?;
}
}
Complex Data Processing:
use scripty::*;
// ✅ Efficient multi-stage processing
let processed = cmd!("cat", "data.json")
.pipe(cmd!("jq", ".items[]")) // Extract items
.pipe(cmd!("grep", "active")) // Filter active
.pipe(cmd!("jq", "-r", ".name")) // Extract names
.pipe(cmd!("sort")) // Sort results
.output()?;
§Troubleshooting Common Issues
Large Data Processing:
use scripty::*;
// Problem: Memory usage with large files
// Solution: Use streaming with BufReader and ReadExt
use std::io::BufReader;
use std::fs::File;
let large_file = File::open("huge_dataset.txt")?;
let reader = BufReader::new(large_file);
reader.pipe(cmd!("awk", "{sum += $1} END {print sum}"))
.output()?; // Processes efficiently regardless of file size
Pipeline Debugging:
use scripty::*;
// Enable command echoing for debugging (set before running your program)
// export SCRIPTY_DEBUG=1
// Commands are echoed by default unless .no_echo() is used
cmd!("complex-command")
.pipe(cmd!("grep", "pattern"))
.pipe(cmd!("sort"))
.run()?; // Commands will be shown as they execute
Error Isolation:
use scripty::*;
// Test each stage of a complex pipeline individually
let stage1 = cmd!("stage1-command").output()?;
println!("Stage 1 output: {}", stage1);
let stage2 = cmd!("stage2-command").input(&stage1).output()?;
println!("Stage 2 output: {}", stage2);
// Then combine when each stage works correctly
§Platform Support
- Linux ✅ Full support with native pipe optimization
- macOS ✅ Full support with native pipe optimization
- Windows ❌ Not supported (Unix-like systems only)
§Contributing
We welcome contributions! Please see our GitHub repository for more information.
§License
This project is licensed under the MIT License.
Re-exports§
pub use std::ffi::OsStr;
pub use std::ffi::OsString;
pub use std::io::BufReader;
pub use std::io::BufWriter;
pub use std::path::Path;
pub use std::path::PathBuf;
pub use std::io::prelude::*;
Modules§
Macros§
- cmd
- Macro to create a new command.
Structs§
- Cmd
- A simple command builder.
- Error
- Command execution error.
- Pipeline
- A pipeline of commands.
- Pipeline
Handle - Handle to a spawned pipeline for waiting and collecting results.
- Pipeline
Spawn - Complete I/O access to a spawned pipeline.
Traits§
- ReadExt
- Extension trait for
std::io::Read
to enable fluent piping to commands.
Type Aliases§
- Result
- Result type with a boxed error for convenience