From 2ca1aed24a2862ffb8f6d87ac3e58e3318f79e18 Mon Sep 17 00:00:00 2001 From: PinStraw Date: Fri, 4 Apr 2025 17:37:44 -0400 Subject: [PATCH] allow interactive commands --- Cargo.lock | 35 ++++++++------- Cargo.toml | 5 ++- main.rs | 128 ++++++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 129 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5accb5e..de13f40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,9 +25,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "byteorder" @@ -42,10 +42,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "cr-shell" +name = "chg-shell" version = "0.1.0" dependencies = [ "inquire", + "libc", "regex", ] @@ -76,9 +77,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" [[package]] name = "fuzzy-matcher" @@ -104,7 +105,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "crossterm", "dyn-clone", "fuzzy-matcher", @@ -117,9 +118,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.167" +version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "lock_api" @@ -133,9 +134,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "memchr" @@ -166,9 +167,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "parking_lot" @@ -195,11 +196,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", ] [[package]] @@ -269,9 +270,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" [[package]] name = "thread_local" diff --git a/Cargo.toml b/Cargo.toml index 0576b69..eeb70e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,13 @@ [package] -name = "cr-shell" +name = "chg-shell" version = "0.1.0" edition = "2021" [dependencies] inquire = "0.7.5" +libc = "0.2.171" regex = "1.11.1" [[bin]] -name = "cr-shell" +name = "chg-shell" path = "main.rs" diff --git a/main.rs b/main.rs index 669f64e..b4e2e80 100644 --- a/main.rs +++ b/main.rs @@ -1,13 +1,101 @@ +use std::env; +use std::path::Path; use std::io::Write; -use std::process::{Command, Output}; use std::io::Result as IoResult; use inquire::{Text, validator::Validation}; -use inquire::Select; +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 { - // Use Command to run the bash shell with the -c option to pass the command string + // 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) @@ -26,27 +114,19 @@ fn execute_command(command: &str) -> IoResult { } } -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 main() { - let prompt_gen_command = "echo -n \"r-[${USER}@${HOSTNAME} ${PWD##*/}]$ \""; - + // 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("")); - let re = Regex::new(r"CHG[0-9]{6}$").unwrap(); + // 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) { @@ -57,7 +137,7 @@ fn main() { let mut bypass_change = false; loop { - let prompt_command = match execute_command(&prompt_gen_command) { + let prompt_command = match generate_prompt() { Ok(output) => { let prompt_output = prompt(&output); if prompt_output.chars().count() == 0 { @@ -80,25 +160,33 @@ fn main() { 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(_) => return, + Err(_) => continue, }; println!("Change request validated: {}", change_request); - let ans = Select::new("Change Type?", options.clone()) + 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(selected_option) => { - println!("Selected option: {}", selected_option); + 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 {