diff --git a/Cargo.toml b/Cargo.toml index f3eb4a9..07fa450 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ glob = "0.3.1" # For file pattern matching tempfile = "3.5" # For temporary file operations log = "0.4" # Logging facade rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language +rand = "0.8.5" # Random number generation clap = "2.33" # Command-line argument parsing # Optional features for specific OS functionality diff --git a/examples/container_example.rs b/examples/container_example.rs index d572b3d..491cac7 100644 --- a/examples/container_example.rs +++ b/examples/container_example.rs @@ -44,7 +44,7 @@ fn main() -> Result<(), Box> { match Container::new("existing-container") { Ok(container) => { if container.container_id.is_some() { - println!("Found container with ID: {}", container.container_id.unwrap()); + println!("Found container with ID: {}", container.container_id.as_ref().unwrap()); // Perform operations on the existing container let status = container.status()?; diff --git a/src/examples/text_replace_example.rs b/src/examples/text_replace_example.rs new file mode 100644 index 0000000..437bc6d --- /dev/null +++ b/src/examples/text_replace_example.rs @@ -0,0 +1,93 @@ +use std::error::Error; +use std::fs::File; +use std::io::Write; +use tempfile::NamedTempFile; + +use sal::text::TextReplacer; + +fn main() -> Result<(), Box> { + // Create a temporary file for our examples + let mut temp_file = NamedTempFile::new()?; + writeln!(temp_file, "This is a foo bar example with FOO and foo occurrences.")?; + println!("Created temporary file at: {}", temp_file.path().display()); + + // Example 1: Simple regex replacement + println!("\n--- Example 1: Simple regex replacement ---"); + let replacer = TextReplacer::builder() + .pattern(r"\bfoo\b") + .replacement("replacement") + .regex(true) + .add_replacement()? + .build()?; + + let result = replacer.replace_file(temp_file.path())?; + println!("After regex replacement: {}", result); + + // Example 2: Multiple replacements in one pass + println!("\n--- Example 2: Multiple replacements in one pass ---"); + let replacer = TextReplacer::builder() + .pattern("foo") + .replacement("AAA") + .add_replacement()? + .pattern("bar") + .replacement("BBB") + .add_replacement()? + .build()?; + + // Write new content to the temp file + writeln!(temp_file.as_file_mut(), "foo bar foo baz")?; + temp_file.as_file_mut().flush()?; + + let result = replacer.replace_file(temp_file.path())?; + println!("After multiple replacements: {}", result); + + // Example 3: Case-insensitive replacement + println!("\n--- Example 3: Case-insensitive replacement ---"); + let replacer = TextReplacer::builder() + .pattern("foo") + .replacement("case-insensitive") + .regex(true) + .case_insensitive(true) + .add_replacement()? + .build()?; + + // Write new content to the temp file + writeln!(temp_file.as_file_mut(), "FOO foo Foo fOo")?; + temp_file.as_file_mut().flush()?; + + let result = replacer.replace_file(temp_file.path())?; + println!("After case-insensitive replacement: {}", result); + + // Example 4: File operations + println!("\n--- Example 4: File operations ---"); + let output_file = NamedTempFile::new()?; + + let replacer = TextReplacer::builder() + .pattern("example") + .replacement("EXAMPLE") + .add_replacement()? + .build()?; + + // Write new content to the temp file + writeln!(temp_file.as_file_mut(), "This is an example text file.")?; + temp_file.as_file_mut().flush()?; + + // Replace and write to a new file + replacer.replace_file_to(temp_file.path(), output_file.path())?; + + // Read the output file to verify + let output_content = std::fs::read_to_string(output_file.path())?; + println!("Content written to new file: {}", output_content); + + // Example 5: Replace in-place + println!("\n--- Example 5: Replace in-place ---"); + + // Replace in the same file + replacer.replace_file_in_place(temp_file.path())?; + + // Read the file to verify + let updated_content = std::fs::read_to_string(temp_file.path())?; + println!("Content after in-place replacement: {}", updated_content); + + Ok(()) +} \ No newline at end of file diff --git a/src/rhai/buildah.rs b/src/rhai/buildah.rs index 4e8087a..e0c7487 100644 --- a/src/rhai/buildah.rs +++ b/src/rhai/buildah.rs @@ -41,6 +41,8 @@ pub fn register_bah_module(engine: &mut Engine) -> Result<(), Box engine.register_fn("image_push", builder_image_push); engine.register_fn("image_tag", builder_image_tag); engine.register_fn("build", builder_build); + engine.register_fn("write_content", builder_write_content); + engine.register_fn("read_content", builder_read_content); Ok(()) } @@ -146,6 +148,16 @@ pub fn builder_config(builder: &mut Builder, options: Map) -> Result Result> { + bah_error_to_rhai_error(builder.write_content(content, dest_path)) +} + +/// Read content from a file in the container +pub fn builder_read_content(builder: &mut Builder, source_path: &str) -> Result> { + bah_error_to_rhai_error(builder.read_content(source_path)) +} + // Builder static methods pub fn builder_images(_builder: &mut Builder) -> Result> { let images = bah_error_to_rhai_error(Builder::images())?; diff --git a/src/rhaiexamples/04_buildah_operations.rhai b/src/rhaiexamples/04_buildah_operations.rhai deleted file mode 100644 index 19fba2a..0000000 --- a/src/rhaiexamples/04_buildah_operations.rhai +++ /dev/null @@ -1,106 +0,0 @@ -// 04_buildah_operations.rhai -// Demonstrates container operations using SAL's buildah integration -// Note: This script requires buildah to be installed and may need root privileges - -// Check if buildah is installed -let buildah_exists = which("buildah"); -println(`Buildah exists: ${buildah_exists}`); - -// Create a builder object -println("\nCreating a builder object:"); -let container_name = "my-container-example"; - -// Create a new builder -let builder = bah_new(container_name, "alpine:latest"); - -// Reset the builder to remove any existing container -println("\nResetting the builder to start fresh:"); -let reset_result = builder.reset(); -println(`Reset result: ${reset_result}`); - -// Create a new container after reset -println("\nCreating a new container after reset:"); -builder = bah_new(container_name, "alpine:latest"); -println(`Container created with ID: ${builder.container_id}`); -println(`Builder created with name: ${builder.name}, image: ${builder.image}`); - -// List available images (only if buildah is installed) -println("\nListing available container images:"); -// if ! buildah_exists != "" { -// //EXIT -// } -let images = builder.images(); -println(`Found ${images.len()} images`); - -// Print image details (limited to 3) -let count = 0; -for img in images { - if count >= 3 { - break; - } - println(` - ID: ${img.id}, Name: ${img.name}, Created: ${img.created}`); - count += 1; -} - -//Run a command in the container -println("\nRunning a command in the container:"); -let run_result = builder.run("echo 'Hello from container'"); -println(`Command output: ${run_result.stdout}`); - -//Add a file to the container -println("\nAdding a file to the container:"); -let test_file = "test_file.txt"; -// Create the test file using Rhai's file_write function -file_write(test_file, "Test content"); -println(`Created test file: ${test_file}`); -println(`Created test file: ${test_file}`); -let add_result = builder.add(test_file, "/"); -println(`Add result: ${add_result.success}`); - -//Commit the container to create a new image -println("\nCommitting the container to create a new image:"); -let commit_result = builder.commit("my-custom-image:latest"); -println(`Commit result: ${commit_result.success}`); - -//Remove the container -println("\nRemoving the container:"); -let remove_result = builder.remove(); -println(`Remove result: ${remove_result.success}`); - -//Clean up the test file -delete(test_file); - -// Demonstrate static methods -println("\nDemonstrating static methods:"); -println("Building an image from a Dockerfile:"); -let build_result = builder.build("example-image:latest", ".", "example_Dockerfile", "chroot"); -println(`Build result: ${build_result.success}`); - -// Pull an image -println("\nPulling an image:"); -let pull_result = builder.image_pull("alpine:latest", true); -println(`Pull result: ${pull_result.success}`); - -// Skip commit options demonstration since we removed the legacy functions -println("\nSkipping commit options demonstration (legacy functions removed)"); - -// Demonstrate config method -println("\nDemonstrating config method:"); -// Create a new container for config demonstration -println("Creating a new container for config demonstration:"); -builder = bah_new("config-demo-container", "alpine:latest"); -println(`Container created with ID: ${builder.container_id}`); - -let config_options = #{ - "author": "Rhai Example", - "cmd": "/bin/sh -c 'echo Hello from Buildah'" -}; -let config_result = builder.config(config_options); -println(`Config result: ${config_result.success}`); - -// Clean up the container -println("Removing the config demo container:"); -builder.remove(); - - -"Buildah operations script completed successfully!" \ No newline at end of file diff --git a/src/rhaiexamples/buildah.rhai b/src/rhaiexamples/buildah.rhai new file mode 100644 index 0000000..ca7f54f --- /dev/null +++ b/src/rhaiexamples/buildah.rhai @@ -0,0 +1,126 @@ +// buildah.rhai +// Demonstrates using buildah to create a custom image with golang and nginx, +// then using nerdctl to run a container from that image + +println("Starting buildah workflow to create a custom image..."); + +// Define image and container names +let base_image = "ubuntu:22.04"; +let container_name = "golang-nginx-container"; +let final_image_name = "custom-golang-nginx:latest"; + +println(`Creating container '${container_name}' from base image '${base_image}'...`); + +// Create a new buildah container using the builder pattern +let builder = bah_new(container_name, base_image); + +// Update package lists and install golang and nginx +println("Installing packages (this may take a while)..."); + +// Update package lists +let update_result = builder.run("apt-get update -y"); + +// Install required packages +let install_result = builder.run("apt-get install -y golang nginx"); + +// Verify installations +let go_version = builder.run("go version"); +println(`Go version: ${go_version.stdout}`); + +let nginx_version = builder.run("nginx -v"); +println(`Nginx version: ${nginx_version.stderr}`); // nginx outputs version to stderr + +// Create a simple Go web application +println("Creating a simple Go web application..."); + +// Create a directory for the Go application +builder.run("mkdir -p /app"); + +// Create a simple Go web server +let go_app = ` +package main + +import ( + "fmt" + "net/http" +) + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello from Go running in a custom container!") + }) + + fmt.Println("Starting server on :8080") + http.ListenAndServe(":8080", nil) +} +`; + +// Write the Go application to a file using the write_content method +builder.write_content(go_app, "/app/main.go"); + +// Compile the Go application +builder.run("cd /app && go build -o server main.go"); + +// Configure nginx to proxy to the Go application +let nginx_conf = ` +server { + listen 80; + server_name localhost; + + location / { + proxy_pass http://localhost:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +`; + +// Write the nginx configuration using the write_content method +let nginx_conf_result = builder.write_content(nginx_conf, "/etc/nginx/sites-available/default"); + +// Create a startup script +let startup_script = ` +#!/bin/bash +# Start the Go application in the background +cd /app && ./server & +# Start nginx in the foreground +nginx -g "daemon off;" +`; + +// Write the startup script using the write_content method +let startup_script_result = builder.write_content(startup_script, "/start.sh"); +builder.run("chmod +x /start.sh"); + +// Read back the startup script to verify it was written correctly +let read_script = builder.read_content("/start.sh"); +println("Startup script content verification:"); +println(read_script); + +// Commit the container to a new image +println(`Committing container to image '${final_image_name}'...`); +let commit_result = builder.commit(final_image_name); + +// Clean up the buildah container +println("Cleaning up buildah container..."); +builder.remove(); + +// Now use nerdctl to run a container from the new image +println("\nStarting container from the new image using nerdctl..."); + +// Create a container using the builder pattern +let container = nerdctl_container_from_image("golang-nginx-demo", final_image_name) + .with_detach(true) + .with_port("8080:80") // Map port 80 in the container to 8080 on the host + .with_restart_policy("unless-stopped") + .build(); + +// Start the container +let start_result = container.start(); + +println("\nWorkflow completed successfully!"); +println("The web server should be running at http://localhost:8080"); +println("You can check container logs with: nerdctl logs golang-nginx-demo"); +println("To stop the container: nerdctl stop golang-nginx-demo"); +println("To remove the container: nerdctl rm golang-nginx-demo"); + +"Buildah and nerdctl workflow completed successfully!" diff --git a/src/rhaiexamples/nerdctl_webserver copy.rhai b/src/rhaiexamples/nerdctl_webserver copy.rhai deleted file mode 100644 index 4e20b76..0000000 --- a/src/rhaiexamples/nerdctl_webserver copy.rhai +++ /dev/null @@ -1,130 +0,0 @@ -// 08_nerdctl_web_server.rhai -// Demonstrates a complete workflow to set up a web server using nerdctl -// Note: This script requires nerdctl to be installed and may need root privileges - -// Ensure nerdctl is installed -println("Checking if nerdctl is installed..."); -// Fix the typo in nerdctl and check all required commands -let result = cmd_ensure_exists("nerdctl,runc,buildah"); -println("All required commands are installed and available."); - -println("Starting nerdctl web server workflow..."); - -// Create and use a temporary directory for all files -let work_dir = "/tmp/nerdctl"; -mkdir(work_dir); -chdir(work_dir); -println(`Working in directory: ${work_dir}`); - - -println("\n=== Creating custom nginx configuration ==="); -let config_content = ` -server { - listen 80; - server_name localhost; - - location / { - root /usr/share/nginx/html; - index index.html; - } -} -`; - -let config_file = `${work_dir}/custom-nginx.conf`; -// Use file_write instead of run command -file_write(config_file, config_content); -println(`Created custom nginx configuration file at ${config_file}`); - -// Step 3: Create a custom index.html file -println("\n=== Creating custom index.html ==="); -let html_content = ` - - - - Rhai Nerdctl Demo - - - -

Hello from Rhai Nerdctl!

-

This page is served by an Nginx container created using the Rhai nerdctl wrapper.

-

Current time: ${now()}

- - -`; - -let html_file = `${work_dir}/index.html`; -// Use file_write instead of run command -file_write(html_file, html_content); -println(`Created custom index.html file at ${html_file}`); - -println("\n=== Creating nginx container ==="); -let container_name = "rhai-nginx-demo"; - -// First, try to remove any existing container with the same name -nerdctl_remove(container_name); - -let env_map = #{}; // Create an empty map -env_map["NGINX_HOST"] = "localhost"; -env_map["NGINX_PORT"] = "80"; -env_map["NGINX_WORKER_PROCESSES"] = "auto"; -let network_aliases = ["web-server", "nginx-demo", "http-service"]; - -// Create a container with a rich set of options using batch methods -let container = nerdctl_container_from_image(container_name, "nginx:latest") - .with_detach(true) - .with_ports(["8080:80"]) // Add multiple ports at once - .with_volumes([`${work_dir}:/usr/share/nginx/html`, "/var/log:/var/log/nginx"]) // Mount our work dir - .with_envs(env_map) // Add multiple environment variables at once - .with_network("bridge") - .with_network_aliases(network_aliases) // Add multiple network aliases at once - .with_cpu_limit("1.0") - .with_memory_limit("512m") - .with_memory_swap_limit("1g") // New method - .with_cpu_shares("1024") // New method - // .with_restart_policy("unless-stopped") - // .with_snapshotter("native") - // Add health check with a multiline script -// .with_health_check_options( -// `#!/bin/bash -// # Health check script for nginx container -// # This script will check if the nginx server is responding correctly - -// # Try to connect to the server -// response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/ || echo "failed") - -// # Check the response -// if [ "$response" = "200" ]; then -// echo "Nginx is healthy" -// exit 0 -// else -// echo "Nginx is not healthy, got response: $response" -// exit 1 -// fi`, -// "5s", // Interval -// "3s", // Timeout -// 3, // Retries -// "10s" // Start period -// ); - -// Build and start the container -println("Building and starting the container..."); -let built_container = container.build(); -let start_result = built_container.start(); -println(`Container started: ${start_result.success}`); - -println(`Successfully created and started container: ${container_name}`); - -println("\nNerdctl web server workflow completed successfully!"); -println("The web server is running at http://localhost:8080"); - -"Nerdctl web server script completed successfully!" \ No newline at end of file diff --git a/src/rhaiexamples/write_read.rhai b/src/rhaiexamples/write_read.rhai new file mode 100644 index 0000000..2b13f78 --- /dev/null +++ b/src/rhaiexamples/write_read.rhai @@ -0,0 +1,102 @@ +// write_read.rhai +// Demonstrates writing content to and reading content from a container +// using the write_content and read_content methods + +println("Starting write/read container example..."); + +// Define image and container names +let base_image = "ubuntu:22.04"; +let container_name = "write-read-demo"; +let final_image_name = "write-read-demo:latest"; + +println(`Creating container '${container_name}' from base image '${base_image}'...`); + +// Create a new buildah container +let builder = bah_new(container_name, base_image); + +// Update package lists +println("Updating package lists..."); +let update_result = builder.run("apt-get update -y"); +println(`Package update result: ${update_result.success ? "Success" : "Failed"}`); + +// Write a simple text file to the container +println("\nWriting content to the container..."); +let text_content = "This is a test file created using write_content.\nIt supports multiple lines.\n"; +let write_result = builder.write_content(text_content, "/test.txt"); +println(`Write result: ${write_result.success ? "Success" : "Failed"}`); + +// Write a simple HTML file to the container +println("\nWriting HTML content to the container..."); +let html_content = ` + + + + Write Content Demo + + + +

Hello from Buildah!

+

This HTML file was created using the write_content method.

+ + +`; +let html_write_result = builder.write_content(html_content, "/var/www/html/index.html"); +println(`HTML write result: ${html_write_result.success ? "Success" : "Failed"}`); + +// Write a simple shell script to the container +println("\nWriting shell script to the container..."); +let script_content = ` +#!/bin/bash +echo "This script was created using write_content" +echo "Current directory: $(pwd)" +echo "Files in current directory:" +ls -la +`; +let script_write_result = builder.write_content(script_content, "/test.sh"); +println(`Script write result: ${script_write_result.success ? "Success" : "Failed"}`); + +// Make the script executable +builder.run("chmod +x /test.sh"); + +// Read back the content we wrote +println("\nReading content from the container..."); +let read_text = builder.read_content("/test.txt"); +println("Text file content:"); +println(read_text); + +let read_html = builder.read_content("/var/www/html/index.html"); +println("\nHTML file content (first 100 characters):"); +println(read_html.substr(0, 100) + "..."); + +let read_script = builder.read_content("/test.sh"); +println("\nScript file content:"); +println(read_script); + +// Execute the script we created +println("\nExecuting the script we created..."); +let script_result = builder.run("/test.sh"); +println("Script output:"); +println(script_result.stdout); + +// Commit the container to an image +println(`\nCommitting container to image '${final_image_name}'...`); +let commit_result = builder.commit(final_image_name); +println(`Commit result: ${commit_result.success ? "Success" : "Failed"}`); + +// Clean up the buildah container +println("Cleaning up buildah container..."); +builder.remove(); + +println("\nWrite/read example completed successfully!"); + +"Write/read example completed successfully!" \ No newline at end of file diff --git a/src/text/README.md b/src/text/README.md index bf13ec2..c0da356 100644 --- a/src/text/README.md +++ b/src/text/README.md @@ -8,6 +8,7 @@ This module provides functions for text manipulation tasks such as: - Removing indentation from multiline strings - Adding prefixes to multiline strings - Normalizing filenames and paths +- Text replacement (regex and literal) with file operations ## Functions @@ -68,12 +69,86 @@ assert_eq!(path_fix("./relative/path/to/DOCUMENT-123.pdf"), "./relative/path/to/ - Only normalizes the filename portion, leaving the path structure intact - Handles both absolute and relative paths +### Text Replacement + +#### `TextReplacer` + +A flexible text replacement utility that supports both regex and literal replacements with a builder pattern. + +```rust +// Regex replacement +let replacer = TextReplacer::builder() + .pattern(r"\bfoo\b") + .replacement("bar") + .regex(true) + .add_replacement() + .unwrap() + .build() + .unwrap(); + +let result = replacer.replace("foo bar foo baz"); // "bar bar bar baz" +``` + +**Features:** +- Supports both regex and literal string replacements +- Builder pattern for fluent configuration +- Multiple replacements in a single pass +- Case-insensitive matching (for regex replacements) +- File reading and writing operations + +#### Multiple Replacements + +```rust +let replacer = TextReplacer::builder() + .pattern("foo") + .replacement("qux") + .add_replacement() + .unwrap() + .pattern("bar") + .replacement("baz") + .add_replacement() + .unwrap() + .build() + .unwrap(); + +let result = replacer.replace("foo bar foo"); // "qux baz qux" +``` + +#### File Operations + +```rust +// Replace in a file and get the result as a string +let result = replacer.replace_file("input.txt")?; + +// Replace in a file and write back to the same file +replacer.replace_file_in_place("input.txt")?; + +// Replace in a file and write to a new file +replacer.replace_file_to("input.txt", "output.txt")?; +``` + +#### Case-Insensitive Matching + +```rust +let replacer = TextReplacer::builder() + .pattern("foo") + .replacement("bar") + .regex(true) + .case_insensitive(true) + .add_replacement() + .unwrap() + .build() + .unwrap(); + +let result = replacer.replace("FOO foo Foo"); // "bar bar bar" +``` + ## Usage Import the functions from the module: ```rust -use your_crate::text::{dedent, prefix, name_fix, path_fix}; +use your_crate::text::{dedent, prefix, name_fix, path_fix, TextReplacer}; ``` ## Examples diff --git a/src/text/fix.rs b/src/text/fix.rs index 50fa399..3356006 100644 --- a/src/text/fix.rs +++ b/src/text/fix.rs @@ -8,9 +8,10 @@ pub fn name_fix(text: &str) -> String { // Keep only ASCII characters if c.is_ascii() { // Replace specific characters with underscore - if c.is_whitespace() || c == ',' || c == '-' || c == '"' || c == '\'' || - c == '#' || c == '!' || c == '(' || c == ')' || c == '[' || c == ']' || - c == '=' || c == '+' || c == '<' || c == '>' { + if c.is_whitespace() || c == ',' || c == '-' || c == '"' || c == '\'' || + c == '#' || c == '!' || c == '(' || c == ')' || c == '[' || c == ']' || + c == '=' || c == '+' || c == '<' || c == '>' || c == '@' || c == '$' || + c == '%' || c == '^' || c == '&' || c == '*' { // Only add underscore if the last character wasn't an underscore if !last_was_underscore { result.push('_'); diff --git a/src/text/mod.rs b/src/text/mod.rs index 2a7961e..fceb8ed 100644 --- a/src/text/mod.rs +++ b/src/text/mod.rs @@ -1,5 +1,7 @@ mod dedent; mod fix; +mod replace; pub use dedent::*; -pub use fix::*; \ No newline at end of file +pub use fix::*; +pub use replace::*; \ No newline at end of file diff --git a/src/text/replace.rs b/src/text/replace.rs new file mode 100644 index 0000000..7266dff --- /dev/null +++ b/src/text/replace.rs @@ -0,0 +1,293 @@ +use regex::Regex; +use std::fs; +use std::io::{self, Read, Seek, SeekFrom}; +use std::path::Path; + +/// Represents the type of replacement to perform. +#[derive(Clone)] +pub enum ReplaceMode { + /// Regex-based replacement using the `regex` crate + Regex(Regex), + /// Literal substring replacement (non-regex) + Literal(String), +} + +/// A single replacement operation with a pattern and replacement text +#[derive(Clone)] +pub struct ReplacementOperation { + mode: ReplaceMode, + replacement: String, +} + +impl ReplacementOperation { + /// Applies this replacement operation to the input text + fn apply(&self, input: &str) -> String { + match &self.mode { + ReplaceMode::Regex(re) => re.replace_all(input, self.replacement.as_str()).to_string(), + ReplaceMode::Literal(search) => input.replace(search, &self.replacement), + } + } +} + +/// Text replacer that can perform multiple replacement operations +/// in a single pass over the input text. +pub struct TextReplacer { + operations: Vec, +} + +impl TextReplacer { + /// Creates a new builder for configuring a TextReplacer + pub fn builder() -> TextReplacerBuilder { + TextReplacerBuilder::default() + } + + /// Applies all configured replacement operations to the input text + pub fn replace(&self, input: &str) -> String { + let mut result = input.to_string(); + + // Apply each replacement operation in sequence + for op in &self.operations { + result = op.apply(&result); + } + + result + } + + /// Reads a file, applies all replacements, and returns the result as a string + pub fn replace_file>(&self, path: P) -> io::Result { + let mut file = fs::File::open(path)?; + let mut content = String::new(); + file.read_to_string(&mut content)?; + + Ok(self.replace(&content)) + } + + /// Reads a file, applies all replacements, and writes the result back to the file + pub fn replace_file_in_place>(&self, path: P) -> io::Result<()> { + let content = self.replace_file(&path)?; + fs::write(path, content)?; + Ok(()) + } + + /// Reads a file, applies all replacements, and writes the result to a new file + pub fn replace_file_to, P2: AsRef>( + &self, + input_path: P1, + output_path: P2 + ) -> io::Result<()> { + let content = self.replace_file(&input_path)?; + fs::write(output_path, content)?; + Ok(()) + } +} + +/// Builder for the TextReplacer. +#[derive(Default)] +pub struct TextReplacerBuilder { + operations: Vec, + pattern: Option, + replacement: Option, + use_regex: bool, + case_insensitive: bool, +} + +impl TextReplacerBuilder { + /// Sets the pattern to search for + pub fn pattern(mut self, pat: &str) -> Self { + self.pattern = Some(pat.to_string()); + self + } + + /// Sets the replacement text + pub fn replacement(mut self, rep: &str) -> Self { + self.replacement = Some(rep.to_string()); + self + } + + /// Sets whether to use regex + pub fn regex(mut self, yes: bool) -> Self { + self.use_regex = yes; + self + } + + /// Sets whether the replacement should be case-insensitive + pub fn case_insensitive(mut self, yes: bool) -> Self { + self.case_insensitive = yes; + self + } + + /// Adds another replacement operation to the chain and resets the builder for a new operation + pub fn and(mut self) -> Self { + self.add_current_operation(); + self + } + + // Helper method to add the current operation to the list + fn add_current_operation(&mut self) -> bool { + if let Some(pattern) = self.pattern.take() { + let replacement = self.replacement.take().unwrap_or_default(); + let use_regex = self.use_regex; + let case_insensitive = self.case_insensitive; + + // Reset current settings + self.use_regex = false; + self.case_insensitive = false; + + // Create the replacement mode + let mode = if use_regex { + let mut regex_pattern = pattern; + + // If case insensitive, add the flag to the regex pattern + if case_insensitive && !regex_pattern.starts_with("(?i)") { + regex_pattern = format!("(?i){}", regex_pattern); + } + + match Regex::new(®ex_pattern) { + Ok(re) => ReplaceMode::Regex(re), + Err(_) => return false, // Failed to compile regex + } + } else { + // For literal replacement, we'll handle case insensitivity differently + // since String::replace doesn't have a case-insensitive option + if case_insensitive { + return false; // Case insensitive not supported for literal + } + ReplaceMode::Literal(pattern) + }; + + self.operations.push(ReplacementOperation { + mode, + replacement, + }); + + true + } else { + false + } + } + + /// Builds the TextReplacer with all configured replacement operations + pub fn build(mut self) -> Result { + // If there's a pending replacement operation, add it + self.add_current_operation(); + + // Ensure we have at least one replacement operation + if self.operations.is_empty() { + return Err("No replacement operations configured".to_string()); + } + + Ok(TextReplacer { + operations: self.operations, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::NamedTempFile; + + #[test] + fn test_regex_replace() { + let replacer = TextReplacer::builder() + .pattern(r"\bfoo\b") + .replacement("bar") + .regex(true) + .build() + .unwrap(); + + let input = "foo bar foo baz"; + let output = replacer.replace(input); + + assert_eq!(output, "bar bar bar baz"); + } + + #[test] + fn test_literal_replace() { + let replacer = TextReplacer::builder() + .pattern("foo") + .replacement("qux") + .regex(false) + .build() + .unwrap(); + + let input = "foo bar foo baz"; + let output = replacer.replace(input); + + assert_eq!(output, "qux bar qux baz"); + } + + #[test] + fn test_multiple_replacements() { + let replacer = TextReplacer::builder() + .pattern("foo") + .replacement("qux") + .and() + .pattern("bar") + .replacement("baz") + .build() + .unwrap(); + + let input = "foo bar foo"; + let output = replacer.replace(input); + + assert_eq!(output, "qux baz qux"); + } + + #[test] + fn test_case_insensitive_regex() { + let replacer = TextReplacer::builder() + .pattern("foo") + .replacement("bar") + .regex(true) + .case_insensitive(true) + .build() + .unwrap(); + + let input = "FOO foo Foo"; + let output = replacer.replace(input); + + assert_eq!(output, "bar bar bar"); + } + + #[test] + fn test_file_operations() -> io::Result<()> { + // Create a temporary file + let mut temp_file = NamedTempFile::new()?; + writeln!(temp_file, "foo bar foo baz")?; + + // Flush the file to ensure content is written + temp_file.as_file_mut().flush()?; + + let replacer = TextReplacer::builder() + .pattern("foo") + .replacement("qux") + .build() + .unwrap(); + + // Test replace_file + let result = replacer.replace_file(temp_file.path())?; + assert_eq!(result, "qux bar qux baz\n"); + + // Test replace_file_in_place + replacer.replace_file_in_place(temp_file.path())?; + + // Verify the file was updated - need to seek to beginning of file first + let mut content = String::new(); + temp_file.as_file_mut().seek(SeekFrom::Start(0))?; + temp_file.as_file_mut().read_to_string(&mut content)?; + assert_eq!(content, "qux bar qux baz\n"); + + // Test replace_file_to with a new temporary file + let output_file = NamedTempFile::new()?; + replacer.replace_file_to(temp_file.path(), output_file.path())?; + + // Verify the output file has the replaced content + let mut output_content = String::new(); + fs::File::open(output_file.path())?.read_to_string(&mut output_content)?; + assert_eq!(output_content, "qux bar qux baz\n"); + + Ok(()) + } +} \ No newline at end of file diff --git a/src/text/template.rs b/src/text/template.rs new file mode 100644 index 0000000..49652de --- /dev/null +++ b/src/text/template.rs @@ -0,0 +1,298 @@ +use std::collections::HashMap; +use std::fs; +use std::io; +use std::path::Path; +use tera::{Context, Tera}; + +/// A builder for creating and rendering templates using the Tera template engine. +pub struct TemplateBuilder { + template_path: String, + context: Context, + tera: Option, +} + +impl TemplateBuilder { + /// Creates a new TemplateBuilder with the specified template path. + /// + /// # Arguments + /// + /// * `template_path` - The path to the template file + /// + /// # Returns + /// + /// A new TemplateBuilder instance + /// + /// # Example + /// + /// ``` + /// use sal::text::TemplateBuilder; + /// + /// let builder = TemplateBuilder::open("templates/example.html"); + /// ``` + pub fn open>(template_path: P) -> io::Result { + let path_str = template_path.as_ref().to_string_lossy().to_string(); + + // Verify the template file exists + if !Path::new(&path_str).exists() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + format!("Template file not found: {}", path_str), + )); + } + + Ok(Self { + template_path: path_str, + context: Context::new(), + tera: None, + }) + } + + /// Adds a variable to the template context. + /// + /// # Arguments + /// + /// * `name` - The name of the variable to add + /// * `value` - The value to associate with the variable + /// + /// # Returns + /// + /// The builder instance for method chaining + /// + /// # Example + /// + /// ``` + /// use sal::text::TemplateBuilder; + /// + /// let builder = TemplateBuilder::open("templates/example.html")? + /// .add_var("title", "Hello World") + /// .add_var("username", "John Doe"); + /// ``` + pub fn add_var(mut self, name: S, value: V) -> Self + where + S: AsRef, + V: serde::Serialize, + { + self.context.insert(name.as_ref(), &value); + self + } + + /// Adds multiple variables to the template context from a HashMap. + /// + /// # Arguments + /// + /// * `vars` - A HashMap containing variable names and values + /// + /// # Returns + /// + /// The builder instance for method chaining + /// + /// # Example + /// + /// ``` + /// use sal::text::TemplateBuilder; + /// use std::collections::HashMap; + /// + /// let mut vars = HashMap::new(); + /// vars.insert("title", "Hello World"); + /// vars.insert("username", "John Doe"); + /// + /// let builder = TemplateBuilder::open("templates/example.html")? + /// .add_vars(vars); + /// ``` + pub fn add_vars(mut self, vars: HashMap) -> Self + where + S: AsRef, + V: serde::Serialize, + { + for (name, value) in vars { + self.context.insert(name.as_ref(), &value); + } + self + } + + /// Initializes the Tera template engine with the template file. + /// + /// This method is called automatically by render() if not called explicitly. + /// + /// # Returns + /// + /// The builder instance for method chaining + fn initialize_tera(&mut self) -> Result<(), tera::Error> { + if self.tera.is_none() { + // Create a new Tera instance with just this template + let mut tera = Tera::default(); + + // Read the template content + let template_content = fs::read_to_string(&self.template_path) + .map_err(|e| tera::Error::msg(format!("Failed to read template file: {}", e)))?; + + // Add the template to Tera + let template_name = Path::new(&self.template_path) + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("template"); + + tera.add_raw_template(template_name, &template_content)?; + self.tera = Some(tera); + } + + Ok(()) + } + + /// Renders the template with the current context. + /// + /// # Returns + /// + /// The rendered template as a string + /// + /// # Example + /// + /// ``` + /// use sal::text::TemplateBuilder; + /// + /// let result = TemplateBuilder::open("templates/example.html")? + /// .add_var("title", "Hello World") + /// .add_var("username", "John Doe") + /// .render()?; + /// + /// println!("Rendered template: {}", result); + /// ``` + pub fn render(&mut self) -> Result { + // Initialize Tera if not already done + self.initialize_tera()?; + + // Get the template name + let template_name = Path::new(&self.template_path) + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("template"); + + // Render the template + let tera = self.tera.as_ref().unwrap(); + tera.render(template_name, &self.context) + } + + /// Renders the template and writes the result to a file. + /// + /// # Arguments + /// + /// * `output_path` - The path where the rendered template should be written + /// + /// # Returns + /// + /// Result indicating success or failure + /// + /// # Example + /// + /// ``` + /// use sal::text::TemplateBuilder; + /// + /// TemplateBuilder::open("templates/example.html")? + /// .add_var("title", "Hello World") + /// .add_var("username", "John Doe") + /// .render_to_file("output.html")?; + /// ``` + pub fn render_to_file>(&mut self, output_path: P) -> io::Result<()> { + let rendered = self.render().map_err(|e| { + io::Error::new(io::ErrorKind::Other, format!("Template rendering error: {}", e)) + })?; + + fs::write(output_path, rendered) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::NamedTempFile; + + #[test] + fn test_template_rendering() -> Result<(), Box> { + // Create a temporary template file + let mut temp_file = NamedTempFile::new()?; + writeln!(temp_file, "Hello, {{ name }}! Welcome to {{ place }}.")?; + temp_file.flush()?; + + // Create a template builder and add variables + let mut builder = TemplateBuilder::open(temp_file.path())?; + builder = builder + .add_var("name", "John") + .add_var("place", "Rust"); + + // Render the template + let result = builder.render()?; + assert_eq!(result, "Hello, John! Welcome to Rust.\n"); + + Ok(()) + } + + #[test] + fn test_template_with_multiple_vars() -> Result<(), Box> { + // Create a temporary template file + let mut temp_file = NamedTempFile::new()?; + writeln!(temp_file, "{% if show_greeting %}Hello, {{ name }}!{% endif %}")?; + writeln!(temp_file, "{% for item in items %}{{ item }}{% if not loop.last %}, {% endif %}{% endfor %}")?; + temp_file.flush()?; + + // Create a template builder and add variables + let mut builder = TemplateBuilder::open(temp_file.path())?; + + // Add variables including a boolean and a vector + builder = builder + .add_var("name", "Alice") + .add_var("show_greeting", true) + .add_var("items", vec!["apple", "banana", "cherry"]); + + // Render the template + let result = builder.render()?; + assert_eq!(result, "Hello, Alice!\napple, banana, cherry\n"); + + Ok(()) + } + + #[test] + fn test_template_with_hashmap_vars() -> Result<(), Box> { + // Create a temporary template file + let mut temp_file = NamedTempFile::new()?; + writeln!(temp_file, "{{ greeting }}, {{ name }}!")?; + temp_file.flush()?; + + // Create a HashMap of variables + let mut vars = HashMap::new(); + vars.insert("greeting", "Hi"); + vars.insert("name", "Bob"); + + // Create a template builder and add variables from HashMap + let mut builder = TemplateBuilder::open(temp_file.path())?; + builder = builder.add_vars(vars); + + // Render the template + let result = builder.render()?; + assert_eq!(result, "Hi, Bob!\n"); + + Ok(()) + } + + #[test] + fn test_render_to_file() -> Result<(), Box> { + // Create a temporary template file + let mut temp_file = NamedTempFile::new()?; + writeln!(temp_file, "{{ message }}")?; + temp_file.flush()?; + + // Create an output file + let output_file = NamedTempFile::new()?; + + // Create a template builder, add a variable, and render to file + let mut builder = TemplateBuilder::open(temp_file.path())?; + builder = builder.add_var("message", "This is a test"); + builder.render_to_file(output_file.path())?; + + // Read the output file and verify its contents + let content = fs::read_to_string(output_file.path())?; + assert_eq!(content, "This is a test\n"); + + Ok(()) + } +} \ No newline at end of file diff --git a/src/virt/buildah/cmd.rs b/src/virt/buildah/cmd.rs index 29884e8..d882035 100644 --- a/src/virt/buildah/cmd.rs +++ b/src/virt/buildah/cmd.rs @@ -1,11 +1,26 @@ // Basic buildah operations for container management use std::process::Command; use crate::process::CommandResult; -use super::BuildahError; +use super::{BuildahError, Builder}; /// Execute a buildah command and return the result +/// +/// # Arguments +/// +/// * `args` - The command arguments +/// +/// # Returns +/// +/// * `Result` - Command result or error pub fn execute_buildah_command(args: &[&str]) -> Result { + // Get the current thread-local Builder instance if available + let debug = thread_local_debug(); + + if debug { + println!("Executing buildah command: buildah {}", args.join(" ")); + } + let output = Command::new("buildah") .args(args) .output(); @@ -22,15 +37,93 @@ pub fn execute_buildah_command(args: &[&str]) -> Result { + if debug { + println!("Command execution failed: {}", e); + } Err(BuildahError::CommandExecutionFailed(e)) } } } + +// Thread-local storage for debug flag +thread_local! { + static DEBUG: std::cell::RefCell = std::cell::RefCell::new(false); +} + +/// Set the debug flag for the current thread +pub fn set_thread_local_debug(debug: bool) { + DEBUG.with(|cell| { + *cell.borrow_mut() = debug; + }); +} + +/// Get the debug flag for the current thread +pub fn thread_local_debug() -> bool { + DEBUG.with(|cell| { + *cell.borrow() + }) +} + +/// Execute a buildah command with debug output +/// +/// # Arguments +/// +/// * `args` - The command arguments +/// * `builder` - Reference to a Builder instance for debug output +/// +/// # Returns +/// +/// * `Result` - Command result or error +pub fn execute_buildah_command_with_debug(args: &[&str], builder: &Builder) -> Result { + if builder.debug() { + println!("Executing buildah command: buildah {}", args.join(" ")); + } + + let result = execute_buildah_command(args); + + if builder.debug() { + match &result { + Ok(cmd_result) => { + if !cmd_result.stdout.is_empty() { + println!("Command stdout: {}", cmd_result.stdout); + } + + if !cmd_result.stderr.is_empty() { + println!("Command stderr: {}", cmd_result.stderr); + } + + println!("Command succeeded with code {}", cmd_result.code); + }, + Err(e) => { + println!("Command failed: {}", e); + } + } + } + + result +} diff --git a/src/virt/nerdctl/container_builder.rs b/src/virt/nerdctl/container_builder.rs index 4ab58f3..66dd02a 100644 --- a/src/virt/nerdctl/container_builder.rs +++ b/src/virt/nerdctl/container_builder.rs @@ -7,6 +7,7 @@ use super::health_check_script::prepare_health_check_command; impl Container { /// Reset the container configuration to defaults while keeping the name and image + /// If the container exists, it will be stopped and removed. /// /// # Returns /// @@ -14,12 +15,22 @@ impl Container { pub fn reset(mut self) -> Self { let name = self.name; let image = self.image.clone(); - let container_id = self.container_id.clone(); - // Create a new container with just the name, image, and container_id + // If container exists, stop and remove it + if let Some(container_id) = &self.container_id { + println!("Container exists. Stopping and removing container '{}'...", name); + + // Try to stop the container + let _ = execute_nerdctl_command(&["stop", container_id]); + + // Try to remove the container + let _ = execute_nerdctl_command(&["rm", container_id]); + } + + // Create a new container with just the name and image, but no container_id Self { name, - container_id, + container_id: None, // Reset container_id to None since we removed the container image, config: std::collections::HashMap::new(), ports: Vec::new(),