use std::env; use std::path::Path; use std::io::Write; use std::io::Result as IoResult; use inquire::{Text, validator::Validation}; use inquire::{Select, Confirm}; use inquire::ui::{RenderConfig, Styled}; use regex::Regex; use std::io::{Error, ErrorKind}; use std::process::{Command, Output}; use std::ffi::CString; use libc::{c_char, execvp, fork, waitpid, WIFEXITED, WEXITSTATUS}; static AUTHOR_STRING: &str = r#" Author: Spencer Description: A shell to promote proper system changes! Ask for the source code! "#; fn cd(path: &str) -> Result<(), std::io::Error> { env::set_current_dir(Path::new(path)) } fn execute_command(command: &str) -> IoResult { // Checking to see if the command is a builtin match command.split_whitespace().next() { Some(first_word) if first_word == "cd" => { let path: String = command.split_whitespace().skip(1).collect::>().join(" "); match cd(&path) { Ok(_) => return Ok("".to_string()), Err(e) => return Err(e), } }, Some(first_word) if first_word == "author" => { println!("{}", AUTHOR_STRING); return Ok("".to_string()) } Some(_) => (), None => (), }; // Split the command into arguments let args: Vec<&str> = command.split_whitespace().collect(); if args.is_empty() { return Err(Error::new(ErrorKind::Other, "No command provided")); } // Convert command and arguments to CStrings let c_args: Vec = args.iter() .map(|s| CString::new(*s).expect("CString::new failed")) .collect(); // Create an array of pointers to the CStrings let mut argv: Vec<*const c_char> = c_args.iter() .map(|s| s.as_ptr()) .chain(std::iter::once(std::ptr::null())) .collect(); // Fork and exec unsafe { match fork() { -1 => return Err(Error::last_os_error()), 0 => { // Child process if execvp(c_args[0].as_ptr(), argv.as_mut_ptr()) == -1 { let e = Error::last_os_error(); std::process::exit(e.raw_os_error().unwrap_or(1)); } else { Err(Error::last_os_error()) } }, pid => { // Parent process let mut status: i32 = 0; if waitpid(pid, &mut status, 0) == -1 { return Err(Error::last_os_error()); } if WIFEXITED(status) && WEXITSTATUS(status) == 0 { Ok(String::new()) } else { let exit_status = WEXITSTATUS(status); Err(Error::new(ErrorKind::Other, format!("Command exited with status {}", exit_status))) } }, } } } fn prompt(name: &str) -> String { let mut line = String::new(); print!("{}", name); std::io::stdout().flush().unwrap(); std::io::stdin().read_line(&mut line).expect("Error: Could not read a line"); return line.trim().to_string() } fn generate_prompt() -> IoResult { let command = r#"echo -n "[${USER}@${HOSTNAME} ${PWD##*/}]$ ""#; let output: Output = Command::new("bash") .arg("-c") .arg(command) .output()?; if output.status.success() { // Convert the stdout of the command to a String Ok(String::from_utf8_lossy(&output.stdout).to_string()) } else { // Handle errors by converting stderr to a String and returning an Err variant let error_message = String::from_utf8_lossy(&output.stderr); Err(std::io::Error::new( std::io::ErrorKind::Other, format!("Command failed with error: {}", error_message), )) } } fn main() { // RenderConfig for inquire let mut empty_render_config: RenderConfig = RenderConfig::default(); empty_render_config = empty_render_config.with_prompt_prefix(Styled::new("")); empty_render_config = empty_render_config.with_answered_prompt_prefix(Styled::new("")); empty_render_config = empty_render_config.with_canceled_prompt_indicator(Styled::new("")); // Change type options let options: Vec<&str> = vec!["File change", "Package Update", "Package Removal", "File Removal", "Misc. Command"]; // Change request input validator let re = Regex::new(r"CHG[0-9]{6}$").unwrap(); let change_validator = move |input: &str| if input.chars().count() == 0 { Ok(Validation::Invalid("Change request cannot be empty".into())) } else if re.is_match(input) { Ok(Validation::Valid) } else { Ok(Validation::Invalid("Invalid change request (e.g: CHGXXXXXX)".into())) }; let mut bypass_change = false; loop { let prompt_command = match generate_prompt() { Ok(output) => { let prompt_output = prompt(&output); if prompt_output.chars().count() == 0 { continue; } else if prompt_output.starts_with("runme ") || prompt_output == "runme" { bypass_change = true; if prompt_output.len() > 6 { match execute_command(prompt_output.split_at(6).1) { Ok(output) => print!("{}", output), Err(e) => eprintln!("Command failed: {}", e), } } continue; } else { prompt_output } }, Err(_) => return }; if !bypass_change { let change_request = match Text::new("(e.g. CHGXXXXXX) Enter change request: ") .with_render_config(empty_render_config.clone()) .with_validator(change_validator.clone()) .prompt() { Ok(input) => input, Err(_) => continue, }; println!("Change request validated: {}", change_request); let _change_type = Select::new("Change Type?", options.clone()) .with_render_config(empty_render_config.clone()) .prompt(); let ans = Confirm::new("Are you within the change window?") .with_default(false) .with_help_message("Ensure you are running commands within the change time window") .prompt(); match ans { Ok(true) => { match execute_command(&prompt_command) { Ok(output) => print!("{}", output), Err(e) => eprintln!("Command failed: {}", e), } } Ok(false) => { continue } Err(_) => println!("not running command..."), } } else { match execute_command(&prompt_command) { Ok(output) => print!("{}", output), Err(e) => eprintln!("Command failed: {}", e) } } } }