use std::error::Error; use std::fmt; use std::fs; use std::io; use std::path::Path; use std::process::Command; // Define a custom error type for file system operations #[derive(Debug)] pub enum FsError { DirectoryNotFound(String), FileNotFound(String), CreateDirectoryFailed(io::Error), CopyFailed(io::Error), DeleteFailed(io::Error), CommandFailed(String), CommandNotFound(String), CommandExecutionError(io::Error), InvalidGlobPattern(glob::PatternError), NotADirectory(String), NotAFile(String), UnknownFileType(String), MetadataError(io::Error), ChangeDirFailed(io::Error), ReadFailed(io::Error), WriteFailed(io::Error), AppendFailed(io::Error), } // Implement Display for FsError impl fmt::Display for FsError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { FsError::DirectoryNotFound(dir) => write!(f, "Directory '{}' does not exist", dir), FsError::FileNotFound(pattern) => write!(f, "No files found matching '{}'", pattern), FsError::CreateDirectoryFailed(e) => { write!(f, "Failed to create parent directories: {}", e) } FsError::CopyFailed(e) => write!(f, "Failed to copy file: {}", e), FsError::DeleteFailed(e) => write!(f, "Failed to delete: {}", e), FsError::CommandFailed(e) => write!(f, "{}", e), FsError::CommandNotFound(e) => write!(f, "Command not found: {}", e), FsError::CommandExecutionError(e) => write!(f, "Failed to execute command: {}", e), FsError::InvalidGlobPattern(e) => write!(f, "Invalid glob pattern: {}", e), FsError::NotADirectory(path) => { write!(f, "Path '{}' exists but is not a directory", path) } FsError::NotAFile(path) => write!(f, "Path '{}' is not a regular file", path), FsError::UnknownFileType(path) => write!(f, "Unknown file type at '{}'", path), FsError::MetadataError(e) => write!(f, "Failed to get file metadata: {}", e), FsError::ChangeDirFailed(e) => write!(f, "Failed to change directory: {}", e), FsError::ReadFailed(e) => write!(f, "Failed to read file: {}", e), FsError::WriteFailed(e) => write!(f, "Failed to write to file: {}", e), FsError::AppendFailed(e) => write!(f, "Failed to append to file: {}", e), } } } // Implement Error trait for FsError impl Error for FsError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { FsError::CreateDirectoryFailed(e) => Some(e), FsError::CopyFailed(e) => Some(e), FsError::DeleteFailed(e) => Some(e), FsError::CommandExecutionError(e) => Some(e), FsError::InvalidGlobPattern(e) => Some(e), FsError::MetadataError(e) => Some(e), FsError::ChangeDirFailed(e) => Some(e), FsError::ReadFailed(e) => Some(e), FsError::WriteFailed(e) => Some(e), FsError::AppendFailed(e) => Some(e), _ => None, } } } /** * Recursively copy a file or directory from source to destination. * * # Arguments * * * `src` - The source path, which can include wildcards * * `dest` - The destination path * * # Returns * * * `Ok(String)` - A success message indicating what was copied * * `Err(FsError)` - An error if the copy operation failed * * # Examples * * ```no_run * use sal::os::copy; * * fn main() -> Result<(), Box> { * // Copy a single file * let result = copy("file.txt", "backup/file.txt")?; * * // Copy multiple files using wildcards * let result = copy("*.txt", "backup/")?; * * // Copy a directory recursively * let result = copy("src_dir", "dest_dir")?; * * Ok(()) * } * ``` */ pub fn copy(src: &str, dest: &str) -> Result { let dest_path = Path::new(dest); // Check if source path contains wildcards if src.contains('*') || src.contains('?') || src.contains('[') { // Create parent directories for destination if needed if let Some(parent) = dest_path.parent() { fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?; } // Use glob to expand wildcards let entries = glob::glob(src).map_err(FsError::InvalidGlobPattern)?; let paths: Vec<_> = entries.filter_map(Result::ok).collect(); if paths.is_empty() { return Err(FsError::FileNotFound(src.to_string())); } let mut success_count = 0; let dest_is_dir = dest_path.exists() && dest_path.is_dir(); for path in paths { let target_path = if dest_is_dir { // If destination is a directory, copy the file into it if path.is_file() { // For files, just use the filename dest_path.join(path.file_name().unwrap_or_default()) } else if path.is_dir() { // For directories, use the directory name dest_path.join(path.file_name().unwrap_or_default()) } else { // Fallback dest_path.join(path.file_name().unwrap_or_default()) } } else { // Otherwise use the destination as is (only makes sense for single file) dest_path.to_path_buf() }; if path.is_file() { // Copy file if let Err(e) = fs::copy(&path, &target_path) { println!("Warning: Failed to copy {}: {}", path.display(), e); } else { success_count += 1; } } else if path.is_dir() { // For directories, use platform-specific command #[cfg(target_os = "windows")] let output = Command::new("xcopy") .args(&[ "/E", "/I", "/H", "/Y", &path.to_string_lossy(), &target_path.to_string_lossy(), ]) .status(); #[cfg(not(target_os = "windows"))] let output = Command::new("cp") .args(&[ "-R", &path.to_string_lossy(), &target_path.to_string_lossy(), ]) .status(); match output { Ok(status) => { if status.success() { success_count += 1; } } Err(e) => println!( "Warning: Failed to copy directory {}: {}", path.display(), e ), } } } if success_count > 0 { Ok(format!( "Successfully copied {} items from '{}' to '{}'", success_count, src, dest )) } else { Err(FsError::CommandFailed(format!( "Failed to copy any files from '{}' to '{}'", src, dest ))) } } else { // Handle non-wildcard paths normally let src_path = Path::new(src); // Check if source exists if !src_path.exists() { return Err(FsError::FileNotFound(src.to_string())); } // Create parent directories if they don't exist if let Some(parent) = dest_path.parent() { fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?; } // Copy based on source type if src_path.is_file() { // If destination is a directory, copy the file into it if dest_path.exists() && dest_path.is_dir() { let file_name = src_path.file_name().unwrap_or_default(); let new_dest_path = dest_path.join(file_name); fs::copy(src_path, new_dest_path).map_err(FsError::CopyFailed)?; Ok(format!( "Successfully copied file '{}' to '{}/{}'", src, dest, file_name.to_string_lossy() )) } else { // Otherwise copy file to the specified destination fs::copy(src_path, dest_path).map_err(FsError::CopyFailed)?; Ok(format!("Successfully copied file '{}' to '{}'", src, dest)) } } else if src_path.is_dir() { // For directories, use platform-specific command #[cfg(target_os = "windows")] let output = Command::new("xcopy") .args(&["/E", "/I", "/H", "/Y", src, dest]) .output(); #[cfg(not(target_os = "windows"))] let output = Command::new("cp").args(&["-R", src, dest]).output(); match output { Ok(out) => { if out.status.success() { Ok(format!( "Successfully copied directory '{}' to '{}'", src, dest )) } else { let error = String::from_utf8_lossy(&out.stderr); Err(FsError::CommandFailed(format!( "Failed to copy directory: {}", error ))) } } Err(e) => Err(FsError::CommandExecutionError(e)), } } else { Err(FsError::UnknownFileType(src.to_string())) } } } /** * Check if a file or directory exists. * * # Arguments * * * `path` - The path to check * * # Returns * * * `bool` - True if the path exists, false otherwise * * # Examples * * ``` * use sal::os::exist; * * if exist("file.txt") { * println!("File exists"); * } * ``` */ pub fn exist(path: &str) -> bool { Path::new(path).exists() } /** * Find a file in a directory (with support for wildcards). * * # Arguments * * * `dir` - The directory to search in * * `filename` - The filename pattern to search for (can include wildcards) * * # Returns * * * `Ok(String)` - The path to the found file * * `Err(FsError)` - An error if no file is found or multiple files are found * * # Examples * * ```no_run * use sal::os::find_file; * * fn main() -> Result<(), Box> { * let file_path = find_file("/path/to/dir", "*.txt")?; * println!("Found file: {}", file_path); * Ok(()) * } * ``` */ pub fn find_file(dir: &str, filename: &str) -> Result { let dir_path = Path::new(dir); // Check if directory exists if !dir_path.exists() || !dir_path.is_dir() { return Err(FsError::DirectoryNotFound(dir.to_string())); } // Use glob to find files - use recursive pattern to find in subdirectories too let pattern = format!("{}/**/{}", dir, filename); let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?; let files: Vec<_> = entries .filter_map(Result::ok) .filter(|path| path.is_file()) .collect(); match files.len() { 0 => Err(FsError::FileNotFound(filename.to_string())), 1 => Ok(files[0].to_string_lossy().to_string()), _ => { // If multiple matches, just return the first one instead of erroring // This makes wildcard searches more practical println!( "Note: Multiple files found matching '{}', returning first match", filename ); Ok(files[0].to_string_lossy().to_string()) } } } /** * Find multiple files in a directory (recursive, with support for wildcards). * * # Arguments * * * `dir` - The directory to search in * * `filename` - The filename pattern to search for (can include wildcards) * * # Returns * * * `Ok(Vec)` - A vector of paths to the found files * * `Err(FsError)` - An error if the directory doesn't exist or the pattern is invalid * * # Examples * * ```no_run * use sal::os::find_files; * * fn main() -> Result<(), Box> { * let files = find_files("/path/to/dir", "*.txt")?; * for file in files { * println!("Found file: {}", file); * } * Ok(()) * } * ``` */ pub fn find_files(dir: &str, filename: &str) -> Result, FsError> { let dir_path = Path::new(dir); // Check if directory exists if !dir_path.exists() || !dir_path.is_dir() { return Err(FsError::DirectoryNotFound(dir.to_string())); } // Use glob to find files let pattern = format!("{}/**/{}", dir, filename); let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?; let files: Vec = entries .filter_map(Result::ok) .filter(|path| path.is_file()) .map(|path| path.to_string_lossy().to_string()) .collect(); Ok(files) } /** * Find a directory in a parent directory (with support for wildcards). * * # Arguments * * * `dir` - The parent directory to search in * * `dirname` - The directory name pattern to search for (can include wildcards) * * # Returns * * * `Ok(String)` - The path to the found directory * * `Err(FsError)` - An error if no directory is found or multiple directories are found * * # Examples * * ```no_run * use sal::os::find_dir; * * fn main() -> Result<(), Box> { * let dir_path = find_dir("/path/to/parent", "sub*")?; * println!("Found directory: {}", dir_path); * Ok(()) * } * ``` */ pub fn find_dir(dir: &str, dirname: &str) -> Result { let dir_path = Path::new(dir); // Check if directory exists if !dir_path.exists() || !dir_path.is_dir() { return Err(FsError::DirectoryNotFound(dir.to_string())); } // Use glob to find directories let pattern = format!("{}/{}", dir, dirname); let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?; let dirs: Vec<_> = entries .filter_map(Result::ok) .filter(|path| path.is_dir()) .collect(); match dirs.len() { 0 => Err(FsError::DirectoryNotFound(dirname.to_string())), 1 => Ok(dirs[0].to_string_lossy().to_string()), _ => Err(FsError::CommandFailed(format!( "Multiple directories found matching '{}', expected only one", dirname ))), } } /** * Find multiple directories in a parent directory (recursive, with support for wildcards). * * # Arguments * * * `dir` - The parent directory to search in * * `dirname` - The directory name pattern to search for (can include wildcards) * * # Returns * * * `Ok(Vec)` - A vector of paths to the found directories * * `Err(FsError)` - An error if the parent directory doesn't exist or the pattern is invalid * * # Examples * * ```no_run * use sal::os::find_dirs; * * fn main() -> Result<(), Box> { * let dirs = find_dirs("/path/to/parent", "sub*")?; * for dir in dirs { * println!("Found directory: {}", dir); * } * Ok(()) * } * ``` */ pub fn find_dirs(dir: &str, dirname: &str) -> Result, FsError> { let dir_path = Path::new(dir); // Check if directory exists if !dir_path.exists() || !dir_path.is_dir() { return Err(FsError::DirectoryNotFound(dir.to_string())); } // Use glob to find directories let pattern = format!("{}/**/{}", dir, dirname); let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?; let dirs: Vec = entries .filter_map(Result::ok) .filter(|path| path.is_dir()) .map(|path| path.to_string_lossy().to_string()) .collect(); Ok(dirs) } /** * Delete a file or directory (defensive - doesn't error if file doesn't exist). * * # Arguments * * * `path` - The path to delete * * # Returns * * * `Ok(String)` - A success message indicating what was deleted * * `Err(FsError)` - An error if the deletion failed * * # Examples * * ``` * use sal::os::delete; * * fn main() -> Result<(), Box> { * // Delete a file * let result = delete("file.txt")?; * * // Delete a directory and all its contents * let result = delete("directory/")?; * * Ok(()) * } * ``` */ pub fn delete(path: &str) -> Result { let path_obj = Path::new(path); // Check if path exists if !path_obj.exists() { return Ok(format!("Nothing to delete at '{}'", path)); } // Delete based on path type if path_obj.is_file() || path_obj.is_symlink() { fs::remove_file(path_obj).map_err(FsError::DeleteFailed)?; Ok(format!("Successfully deleted file '{}'", path)) } else if path_obj.is_dir() { fs::remove_dir_all(path_obj).map_err(FsError::DeleteFailed)?; Ok(format!("Successfully deleted directory '{}'", path)) } else { Err(FsError::UnknownFileType(path.to_string())) } } /** * Create a directory and all parent directories (defensive - doesn't error if directory exists). * * # Arguments * * * `path` - The path of the directory to create * * # Returns * * * `Ok(String)` - A success message indicating the directory was created * * `Err(FsError)` - An error if the creation failed * * # Examples * * ``` * use sal::os::mkdir; * * fn main() -> Result<(), Box> { * let result = mkdir("path/to/new/directory")?; * println!("{}", result); * Ok(()) * } * ``` */ pub fn mkdir(path: &str) -> Result { let path_obj = Path::new(path); // Check if path already exists if path_obj.exists() { if path_obj.is_dir() { return Ok(format!("Directory '{}' already exists", path)); } else { return Err(FsError::NotADirectory(path.to_string())); } } // Create directory and parents fs::create_dir_all(path_obj).map_err(FsError::CreateDirectoryFailed)?; Ok(format!("Successfully created directory '{}'", path)) } /** * Get the size of a file in bytes. * * # Arguments * * * `path` - The path of the file * * # Returns * * * `Ok(i64)` - The size of the file in bytes * * `Err(FsError)` - An error if the file doesn't exist or isn't a regular file * * # Examples * * ```no_run * use sal::os::file_size; * * fn main() -> Result<(), Box> { * let size = file_size("file.txt")?; * println!("File size: {} bytes", size); * Ok(()) * } * ``` */ pub fn file_size(path: &str) -> Result { let path_obj = Path::new(path); // Check if file exists if !path_obj.exists() { return Err(FsError::FileNotFound(path.to_string())); } // Check if it's a regular file if !path_obj.is_file() { return Err(FsError::NotAFile(path.to_string())); } // Get file metadata let metadata = fs::metadata(path_obj).map_err(FsError::MetadataError)?; Ok(metadata.len() as i64) } /** * Sync directories using rsync (or platform equivalent). * * # Arguments * * * `src` - The source directory * * `dest` - The destination directory * * # Returns * * * `Ok(String)` - A success message indicating the directories were synced * * `Err(FsError)` - An error if the sync failed * * # Examples * * ```no_run * use sal::os::rsync; * * fn main() -> Result<(), Box> { * let result = rsync("source_dir/", "backup_dir/")?; * println!("{}", result); * Ok(()) * } * ``` */ pub fn rsync(src: &str, dest: &str) -> Result { let src_path = Path::new(src); let dest_path = Path::new(dest); // Check if source exists if !src_path.exists() { return Err(FsError::FileNotFound(src.to_string())); } // Create parent directories if they don't exist if let Some(parent) = dest_path.parent() { fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?; } // Use platform-specific command for syncing #[cfg(target_os = "windows")] let output = Command::new("robocopy") .args(&[src, dest, "/MIR", "/NFL", "/NDL"]) .output(); #[cfg(any(target_os = "macos", target_os = "linux"))] let output = Command::new("rsync") .args(&["-a", "--delete", src, dest]) .output(); match output { Ok(out) => { if out.status.success() || out.status.code() == Some(1) { // rsync and robocopy return 1 for some non-error cases Ok(format!("Successfully synced '{}' to '{}'", src, dest)) } else { let error = String::from_utf8_lossy(&out.stderr); Err(FsError::CommandFailed(format!( "Failed to sync directories: {}", error ))) } } Err(e) => Err(FsError::CommandExecutionError(e)), } } /** * Change the current working directory. * * # Arguments * * * `path` - The path to change to * * # Returns * * * `Ok(String)` - A success message indicating the directory was changed * * `Err(FsError)` - An error if the directory change failed * * # Examples * * ```no_run * use sal::os::chdir; * * fn main() -> Result<(), Box> { * let result = chdir("/path/to/directory")?; * println!("{}", result); * Ok(()) * } * ``` */ pub fn chdir(path: &str) -> Result { let path_obj = Path::new(path); // Check if directory exists if !path_obj.exists() { return Err(FsError::DirectoryNotFound(path.to_string())); } // Check if it's a directory if !path_obj.is_dir() { return Err(FsError::NotADirectory(path.to_string())); } // Change directory std::env::set_current_dir(path_obj).map_err(FsError::ChangeDirFailed)?; Ok(format!("Successfully changed directory to '{}'", path)) } /** * Read the contents of a file. * * # Arguments * * * `path` - The path of the file to read * * # Returns * * * `Ok(String)` - The contents of the file * * `Err(FsError)` - An error if the file doesn't exist or can't be read * * # Examples * * ```no_run * use sal::os::file_read; * * fn main() -> Result<(), Box> { * let content = file_read("file.txt")?; * println!("File content: {}", content); * Ok(()) * } * ``` */ pub fn file_read(path: &str) -> Result { let path_obj = Path::new(path); // Check if file exists if !path_obj.exists() { return Err(FsError::FileNotFound(path.to_string())); } // Check if it's a regular file if !path_obj.is_file() { return Err(FsError::NotAFile(path.to_string())); } // Read file content fs::read_to_string(path_obj).map_err(FsError::ReadFailed) } /** * Write content to a file (creates the file if it doesn't exist, overwrites if it does). * * # Arguments * * * `path` - The path of the file to write to * * `content` - The content to write to the file * * # Returns * * * `Ok(String)` - A success message indicating the file was written * * `Err(FsError)` - An error if the file can't be written * * # Examples * * ``` * use sal::os::file_write; * * fn main() -> Result<(), Box> { * let result = file_write("file.txt", "Hello, world!")?; * println!("{}", result); * Ok(()) * } * ``` */ pub fn file_write(path: &str, content: &str) -> Result { let path_obj = Path::new(path); // Create parent directories if they don't exist if let Some(parent) = path_obj.parent() { fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?; } // Write content to file fs::write(path_obj, content).map_err(FsError::WriteFailed)?; Ok(format!("Successfully wrote to file '{}'", path)) } /** * Append content to a file (creates the file if it doesn't exist). * * # Arguments * * * `path` - The path of the file to append to * * `content` - The content to append to the file * * # Returns * * * `Ok(String)` - A success message indicating the content was appended * * `Err(FsError)` - An error if the file can't be appended to * * # Examples * * ``` * use sal::os::file_write_append; * * fn main() -> Result<(), Box> { * let result = file_write_append("log.txt", "New log entry\n")?; * println!("{}", result); * Ok(()) * } * ``` */ pub fn file_write_append(path: &str, content: &str) -> Result { let path_obj = Path::new(path); // Create parent directories if they don't exist if let Some(parent) = path_obj.parent() { fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?; } // Open file in append mode (or create if it doesn't exist) let mut file = fs::OpenOptions::new() .create(true) .append(true) .open(path_obj) .map_err(FsError::AppendFailed)?; // Append content to file use std::io::Write; file.write_all(content.as_bytes()) .map_err(FsError::AppendFailed)?; Ok(format!("Successfully appended to file '{}'", path)) } /** * Move a file or directory from source to destination. * * # Arguments * * * `src` - The source path * * `dest` - The destination path * * # Returns * * * `Ok(String)` - A success message indicating what was moved * * `Err(FsError)` - An error if the move operation failed * * # Examples * * ```no_run * use sal::os::mv; * * fn main() -> Result<(), Box> { * // Move a file * let result = mv("file.txt", "new_location/file.txt")?; * * // Move a directory * let result = mv("src_dir", "dest_dir")?; * * // Rename a file * let result = mv("old_name.txt", "new_name.txt")?; * * Ok(()) * } * ``` */ pub fn mv(src: &str, dest: &str) -> Result { let src_path = Path::new(src); let dest_path = Path::new(dest); // Check if source exists if !src_path.exists() { return Err(FsError::FileNotFound(src.to_string())); } // Create parent directories if they don't exist if let Some(parent) = dest_path.parent() { fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?; } // Handle the case where destination is a directory and exists let final_dest_path = if dest_path.exists() && dest_path.is_dir() && src_path.is_file() { // If destination is a directory and source is a file, move the file into the directory let file_name = src_path.file_name().unwrap_or_default(); dest_path.join(file_name) } else { dest_path.to_path_buf() }; // Clone the path for use in the error handler let final_dest_path_clone = final_dest_path.clone(); // Perform the move operation fs::rename(src_path, &final_dest_path).map_err(|e| { // If rename fails (possibly due to cross-device link), try copy and delete if e.kind() == std::io::ErrorKind::CrossesDevices { // For cross-device moves, we need to copy and then delete if src_path.is_file() { // Copy file match fs::copy(src_path, &final_dest_path_clone) { Ok(_) => { // Delete source after successful copy if let Err(del_err) = fs::remove_file(src_path) { return FsError::DeleteFailed(del_err); } return FsError::CommandFailed("".to_string()); // This is a hack to trigger the success message } Err(copy_err) => return FsError::CopyFailed(copy_err), } } else if src_path.is_dir() { // For directories, use platform-specific command #[cfg(target_os = "windows")] let output = Command::new("xcopy") .args(&["/E", "/I", "/H", "/Y", src, dest]) .status(); #[cfg(not(target_os = "windows"))] let output = Command::new("cp").args(&["-R", src, dest]).status(); match output { Ok(status) => { if status.success() { // Delete source after successful copy if let Err(del_err) = fs::remove_dir_all(src_path) { return FsError::DeleteFailed(del_err); } return FsError::CommandFailed("".to_string()); // This is a hack to trigger the success message } else { return FsError::CommandFailed( "Failed to copy directory for move operation".to_string(), ); } } Err(cmd_err) => return FsError::CommandExecutionError(cmd_err), } } } FsError::CommandFailed(format!("Failed to move '{}' to '{}': {}", src, dest, e)) })?; // If we get here, either the rename was successful or our copy-delete hack worked if src_path.is_file() { Ok(format!("Successfully moved file '{}' to '{}'", src, dest)) } else { Ok(format!( "Successfully moved directory '{}' to '{}'", src, dest )) } } /** * Check if a command exists in the system PATH. * * # Arguments * * * `command` - The command to check * * # Returns * * * `String` - Empty string if the command doesn't exist, path to the command if it does * * # Examples * * ``` * use sal::os::which; * * let cmd_path = which("ls"); * if cmd_path != "" { * println!("ls is available at: {}", cmd_path); * } * ``` */ pub fn which(command: &str) -> String { // Use the appropriate command based on the platform #[cfg(target_os = "windows")] let output = Command::new("where").arg(command).output(); #[cfg(not(target_os = "windows"))] let output = Command::new("which").arg(command).output(); match output { Ok(out) => { if out.status.success() { let path = String::from_utf8_lossy(&out.stdout).trim().to_string(); path } else { String::new() } } Err(_) => String::new(), } } /** * Ensure that one or more commands exist in the system PATH. * If any command doesn't exist, an error is thrown. * * # Arguments * * * `commands` - The command(s) to check, comma-separated for multiple commands * * # Returns * * * `Ok(String)` - A success message indicating all commands exist * * `Err(FsError)` - An error if any command doesn't exist * * # Examples * * ``` * use sal::os::cmd_ensure_exists; * * fn main() -> Result<(), Box> { * // Check if a single command exists * let result = cmd_ensure_exists("nerdctl")?; * * // Check if multiple commands exist * let result = cmd_ensure_exists("nerdctl,docker,containerd")?; * * Ok(()) * } * ``` */ pub fn cmd_ensure_exists(commands: &str) -> Result { // Split the input by commas to handle multiple commands let command_list: Vec<&str> = commands .split(',') .map(|s| s.trim()) .filter(|s| !s.is_empty()) .collect(); if command_list.is_empty() { return Err(FsError::CommandFailed( "No commands specified to check".to_string(), )); } let mut missing_commands = Vec::new(); // Check each command for cmd in &command_list { let cmd_path = which(cmd); if cmd_path.is_empty() { missing_commands.push(cmd.to_string()); } } // If any commands are missing, return an error if !missing_commands.is_empty() { return Err(FsError::CommandNotFound(missing_commands.join(", "))); } // All commands exist if command_list.len() == 1 { Ok(format!("Command '{}' exists", command_list[0])) } else { Ok(format!("All commands exist: {}", command_list.join(", "))) } }