fn build_command(input: &Path, output: &Path, config: Config) -> Result<()> {
let source = fs::read_to_string(input).map_err(Error::Io)?;
let shell_code = transpile(&source, &config).map_err(|e| with_context(e, input, &source))?;
fs::write(output, shell_code).map_err(Error::Io)?;
info!("Successfully transpiled to {}", output.display());
if config.emit_proof {
let proof_path = output.with_extension("proof");
generate_proof(&source, &proof_path, &config)?;
info!("Proof generated at {}", proof_path.display());
}
Ok(())
}
fn check_command(input: &Path) -> Result<()> {
let source = fs::read_to_string(input).map_err(Error::Io)?;
let is_shell_script = is_shell_script_file(input, &source);
if is_shell_script {
return Err(Error::CommandFailed {
message: format!(
"File '{}' appears to be a shell script, not Rash source.\n\n\
The 'check' command is for verifying Rash (.rs) source files that will be\n\
transpiled to shell scripts.\n\n\
For linting existing shell scripts, use:\n\
\x1b[1m bashrs lint {}\x1b[0m\n\n\
For purifying shell scripts (adding determinism/idempotency):\n\
\x1b[1m bashrs purify {}\x1b[0m",
input.display(),
input.display(),
input.display()
),
});
}
check(&source).map_err(|e| with_context(e, input, &source))?;
info!("✓ {} is compatible with Rash", input.display());
Ok(())
}
fn init_command(path: &Path, name: Option<&str>) -> Result<()> {
if !path.exists() {
fs::create_dir_all(path).map_err(Error::Io)?;
}
let project_name = name.unwrap_or(
path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("my-installer"),
);
let cargo_toml = format!(
r#"[package]
name = "{project_name}"
version = "0.1.0"
edition = "2021"
[dependencies]
# No dependencies needed - Rash transpiles to pure shell
[[bin]]
name = "install"
path = "src/main.rs"
"#
);
fs::write(path.join("Cargo.toml"), cargo_toml).map_err(Error::Io)?;
let src_dir = path.join("src");
fs::create_dir_all(&src_dir).map_err(Error::Io)?;
let main_rs = r#"/// Example installer script for Rash
/// This will be transpiled to POSIX-compliant shell script
use std::env;
use std::fs;
use std::path::Path;
use std::process::{Command, exit};
const VERSION: &str = "0.1.0";
const BINARY_NAME: &str = "myapp";
fn main() {
println!("{} installer v{}", BINARY_NAME, VERSION);
println!("=======================");
// Parse arguments
let args: Vec<String> = env::args().collect();
if args.contains(&"--help".to_string()) {
print_help();
return;
}
// Determine installation directory
let prefix = env::var("PREFIX").unwrap_or_else(|_| "/usr/local".to_string());
let bin_dir = format!("{}/bin", prefix);
println!("Installing to: {}", bin_dir);
// Create directory
if let Err(e) = fs::create_dir_all(&bin_dir) {
eprintln!("Failed to create directory: {}", e);
exit(1);
}
// Download binary (example URL)
let url = format!(
"https://github.com/example/{}/releases/download/v{}/{}-{}.tar.gz",
BINARY_NAME, VERSION, BINARY_NAME, detect_platform()
);
println!("Downloading from: {}", url);
// Installation logic would go here:
// - Download binary
// - Verify checksum
// - Extract and install
// - Set permissions
println!("\n✓ {} installed successfully!", BINARY_NAME);
println!("\nTo get started, run:");
println!(" {} --help", BINARY_NAME);
}
fn print_help() {
println!("Usage: install.sh [OPTIONS]");
println!("\nOptions:");
println!(" --help Show this help message");
println!(" --prefix DIR Install to DIR (default: /usr/local)");
}
fn detect_platform() -> &'static str {
// Simplified platform detection
if cfg!(target_os = "linux") {
if cfg!(target_arch = "x86_64") {
"linux-amd64"
} else {
"linux-arm64"
}
} else if cfg!(target_os = "macos") {
"darwin-amd64"
} else {
panic!("Unsupported platform");
}
}"#;
fs::write(src_dir.join("main.rs"), main_rs).map_err(Error::Io)?;
let rash_toml = r##"# Rash configuration file
[transpiler]
target = "posix" # Target shell dialect
strict_mode = true # Fail on warnings
preserve_comments = false # Strip comments for smaller output
[validation]
level = "strict" # ShellCheck compliance level
rules = ["all"] # Can disable specific: ["-SC2034"]
external_check = false # Run actual shellcheck binary
[output]
shebang = "#!/bin/sh" # POSIX shebang
set_flags = "euf" # set -euf (no pipefail in POSIX)
optimize_size = true # Minimize output script size
[style]
indent = " " # 4 spaces
max_line_length = 100 # Wrap long commands
"##;
fs::write(path.join(".rash.toml"), rash_toml).map_err(Error::Io)?;
info!("✓ Initialized Rash project '{}'", project_name);
info!(" Run 'cd {}' to enter the project", path.display());
info!(" Run 'rash build src/main.rs' to build");
Ok(())
}
fn verify_command(
rust_source: &Path,
shell_script: &Path,
target: crate::models::ShellDialect,
verify_level: crate::models::VerificationLevel,
) -> Result<()> {
let rust_code = fs::read_to_string(rust_source).map_err(Error::Io)?;
let shell_code = fs::read_to_string(shell_script).map_err(Error::Io)?;
let config = Config {
target,
verify: verify_level,
emit_proof: false,
optimize: true,
strict_mode: true,
validation_level: Some(crate::validation::ValidationLevel::Strict),
};
let generated_shell = transpile(&rust_code, &config)?;
if normalize_shell_script(&generated_shell) == normalize_shell_script(&shell_code) {
info!("✓ Shell script matches Rust source");
Ok(())
} else {
warn!("Shell script does not match Rust source");
Err(Error::Verification("Script mismatch".to_string()))
}
}
fn generate_proof(source: &str, proof_path: &Path, config: &Config) -> Result<()> {
let proof = format!(
r#"{{
"version": "1.0",
"source_hash": "{}",
"verification_level": "{:?}",
"target": "{:?}",
"timestamp": "{}",
"properties": ["no-injection", "deterministic"]
}}"#,
blake3::hash(source.as_bytes()),
config.verify,
config.target,
chrono::Utc::now().to_rfc3339()
);
fs::write(proof_path, proof).map_err(Error::Io)?;
Ok(())
}
fn inspect_command(
input: &str,
format: InspectionFormat,
output: Option<&Path>,
_detailed: bool,
) -> Result<()> {
use crate::formal::{AbstractState, ProofInspector, TinyAst};
let ast = if input.starts_with('{') {
serde_json::from_str::<TinyAst>(input)
.map_err(|e| Error::Internal(format!("Invalid AST JSON: {e}")))?
} else {
match input {
"echo-example" => TinyAst::ExecuteCommand {
command_name: "echo".to_string(),
args: vec!["Hello, World!".to_string()],
},
"bootstrap-example" => TinyAst::Sequence {
commands: vec![
TinyAst::SetEnvironmentVariable {
name: "INSTALL_DIR".to_string(),
value: "/opt/rash".to_string(),
},
TinyAst::ExecuteCommand {
command_name: "mkdir".to_string(),
args: vec!["-p".to_string(), "/opt/rash/bin".to_string()],
},
TinyAst::ChangeDirectory {
path: "/opt/rash".to_string(),
},
TinyAst::ExecuteCommand {
command_name: "echo".to_string(),
args: vec!["Installation ready".to_string()],
},
],
},
_ => {
return Err(Error::Internal(format!("Unknown example: {input}. Try 'echo-example' or 'bootstrap-example', or provide JSON AST")));
}
}
};
if !ast.is_valid() {
return Err(Error::Validation("Invalid AST".to_string()));
}
let mut initial_state = AbstractState::new();
initial_state.filesystem.insert(
std::path::PathBuf::from("/opt"),
crate::formal::FileSystemEntry::Directory,
);
let report = ProofInspector::inspect(&ast, initial_state);
let output_content = match format {
InspectionFormat::Markdown => ProofInspector::generate_report(&report),
InspectionFormat::Json => serde_json::to_string_pretty(&report)
.map_err(|e| Error::Internal(format!("JSON serialization failed: {e}")))?,
InspectionFormat::Html => {
let markdown = ProofInspector::generate_report(&report);
format!(
r"<!DOCTYPE html>
<html>
<head>
<title>Formal Verification Report</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 2em; }}
pre {{ background: #f5f5f5; padding: 1em; border-radius: 4px; }}
.success {{ color: green; }}
.failure {{ color: red; }}
.warning {{ color: orange; }}
</style>
</head>
<body>
<pre>{}</pre>
</body>
</html>",
markdown
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
)
}
};
match output {
Some(path) => {
fs::write(path, &output_content).map_err(Error::Io)?;
info!("Inspection report written to {}", path.display());
}
None => {
println!("{output_content}");
}
}
Ok(())
}
fn handle_compile(
rust_source: &Path,
output: &Path,
runtime: CompileRuntime,
self_extracting: bool,
container: bool,
container_format: ContainerFormatArg,
config: &Config,
) -> Result<()> {
use crate::compiler::{create_self_extracting_script, BinaryCompiler, RuntimeType};
use crate::container::{ContainerFormat, DistrolessBuilder};
info!(
"Compiling {} to {}",
rust_source.display(),
output.display()
);
let source = fs::read_to_string(rust_source).map_err(Error::Io)?;
let shell_code = transpile(&source, config)?;
if self_extracting {
let output_str = output
.to_str()
.ok_or_else(|| Error::Validation("Output path contains invalid UTF-8".to_string()))?;
create_self_extracting_script(&shell_code, output_str)?;
info!("Created self-extracting script at {}", output.display());
} else if container {
let runtime_type = match runtime {
CompileRuntime::Dash => RuntimeType::Dash,
CompileRuntime::Busybox => RuntimeType::Busybox,
CompileRuntime::Minimal => RuntimeType::Minimal,
};
let compiler = BinaryCompiler::new(runtime_type);
let binary = compiler.compile(&shell_code)?;
let format = match container_format {
ContainerFormatArg::Oci => ContainerFormat::OCI,
ContainerFormatArg::Docker => ContainerFormat::Docker,
};
let builder = DistrolessBuilder::new(binary).with_format(format);
let container_data = builder.build()?;
fs::write(output, container_data).map_err(Error::Io)?;
info!("Created container image at {}", output.display());
} else {
warn!(
"Binary compilation not yet fully implemented, creating self-extracting script instead"
);
let output_str = output
.to_str()
.ok_or_else(|| Error::Validation("Output path contains invalid UTF-8".to_string()))?;
create_self_extracting_script(&shell_code, output_str)?;
}
Ok(())
}
fn handle_repl_command(
debug: bool,
sandboxed: bool,
max_memory: Option<usize>,
timeout: Option<u64>,
max_depth: Option<usize>,
) -> Result<()> {
use crate::repl::{run_repl, ReplConfig};
use std::time::Duration;
let mut config = if sandboxed {
ReplConfig::sandboxed()
} else {
ReplConfig::default()
};
if debug {
config = config.with_debug();
}
if let Some(mem) = max_memory {
config = config.with_max_memory(mem);
}
if let Some(t) = timeout {
config = config.with_timeout(Duration::from_secs(t));
}
if let Some(depth) = max_depth {
config = config.with_max_depth(depth);
}
run_repl(config).map_err(|e| Error::Internal(format!("REPL error: {e}")))
}