From 41582672d5c4edc7592782c1a56a830de7a86aba Mon Sep 17 00:00:00 2001 From: pagedmov Date: Sun, 9 Mar 2025 03:30:03 -0400 Subject: [PATCH] Implemented prompt expansion --- src/builtin/cd.rs | 3 +- src/builtin/export.rs | 4 +- src/execute/mod.rs | 14 +- src/expand/mod.rs | 1 + src/expand/prompt.rs | 385 +++++++++++++++++++++++++++++++++++++++ src/expand/tilde.rs | 3 +- src/expand/vars.rs | 1 + src/libsh/utils.rs | 13 +- src/prelude.rs | 1 + src/prompt/mod.rs | 3 +- src/shellenv/exec_ctx.rs | 1 + src/shellenv/meta.rs | 18 ++ src/shellenv/shenv.rs | 6 +- 13 files changed, 430 insertions(+), 23 deletions(-) create mode 100644 src/expand/prompt.rs diff --git a/src/builtin/cd.rs b/src/builtin/cd.rs index 603cd1d..99d4b0c 100644 --- a/src/builtin/cd.rs +++ b/src/builtin/cd.rs @@ -8,7 +8,8 @@ pub fn cd(node: Node, shenv: &mut ShEnv) -> ShResult<()> { let dir_raw = argv_iter.next().map(|arg| shenv.input_slice(arg.span()).into()).unwrap_or(std::env::var("HOME")?); let dir = PathBuf::from(&dir_raw); std::env::set_current_dir(dir)?; - shenv.vars_mut().export("PWD",&dir_raw); + let new_dir = std::env::current_dir()?; + shenv.vars_mut().export("PWD",new_dir.to_str().unwrap()); shenv.set_code(0); } Ok(()) diff --git a/src/builtin/export.rs b/src/builtin/export.rs index fa2232c..d847348 100644 --- a/src/builtin/export.rs +++ b/src/builtin/export.rs @@ -6,9 +6,9 @@ pub fn export(node: Node, shenv: &mut ShEnv) -> ShResult<()> { let mut argv_iter = argv.into_iter(); argv_iter.next(); // Ignore 'export' while let Some(arg) = argv_iter.next() { - let arg_raw = shenv.input_slice(arg.span()).to_string(); + let arg_raw = arg.as_raw(shenv); if let Some((var,val)) = arg_raw.split_once('=') { - shenv.vars_mut().export(var, val); + shenv.vars_mut().export(var, &clean_string(val)); } else { eprintln!("Expected an assignment in export args, found this: {}", arg_raw) } diff --git a/src/execute/mod.rs b/src/execute/mod.rs index da7bd08..9e55459 100644 --- a/src/execute/mod.rs +++ b/src/execute/mod.rs @@ -24,7 +24,9 @@ pub fn exec_input>(input: S, shenv: &mut ShEnv) -> ShResult<()> let parse_time = std::time::Instant::now(); let syn_tree = Parser::new(token_stream,shenv).parse()?; log!(INFO, "Parsing done in {:?}", parse_time.elapsed()); - shenv.save_io()?; + if !shenv.ctx().flags().contains(ExecFlags::IN_FUNC) { + shenv.save_io()?; + } let exec_time = std::time::Instant::now(); if let Err(e) = Executor::new(syn_tree, shenv).walk() { @@ -32,13 +34,18 @@ pub fn exec_input>(input: S, shenv: &mut ShEnv) -> ShResult<()> let code = shenv.get_code(); sh_quit(code); } else { - shenv.reset_io()?; + if !shenv.ctx().flags().contains(ExecFlags::IN_FUNC) { + shenv.reset_io()?; + } return Err(e.into()) } } log!(INFO, "Executing done in {:?}", exec_time.elapsed()); log!(INFO, "Total time spent: {:?}", total_time.elapsed()); - shenv.reset_io()?; + if !shenv.ctx().flags().contains(ExecFlags::IN_FUNC) { + shenv.reset_io()?; + } + log!(INFO, "Io reset"); Ok(()) } @@ -151,6 +158,7 @@ fn exec_func(node: Node, shenv: &mut ShEnv) -> ShResult<()> { let body = shenv.logic().get_function(&func_name).unwrap().to_string(); let snapshot = shenv.clone(); shenv.vars_mut().reset_params(); + shenv.ctx_mut().set_flag(ExecFlags::IN_FUNC); while let Some(arg) = argv_iter.next() { let arg_raw = shenv.input_slice(arg.span()).to_string(); shenv.vars_mut().bpush_arg(&arg_raw); diff --git a/src/expand/mod.rs b/src/expand/mod.rs index 3f2d401..b49e049 100644 --- a/src/expand/mod.rs +++ b/src/expand/mod.rs @@ -3,6 +3,7 @@ pub mod tilde; pub mod alias; pub mod cmdsub; pub mod arithmetic; +pub mod prompt; use arithmetic::expand_arith_token; use cmdsub::expand_cmdsub_token; diff --git a/src/expand/prompt.rs b/src/expand/prompt.rs new file mode 100644 index 0000000..549c8bd --- /dev/null +++ b/src/expand/prompt.rs @@ -0,0 +1,385 @@ +use crate::prelude::*; + +#[derive(Debug)] +pub enum PromptTk { + AsciiOct(i32), + Text(String), + AnsiSeq(String), + VisGrp, + UserSeq, + Runtime, + Weekday, + Dquote, + Squote, + Return, + Newline, + Pwd, + PwdShort, + Hostname, + HostnameShort, + ShellName, + Username, + PromptSymbol, + ExitCode, + SuccessSymbol, + FailureSymbol, + JobCount +} + +pub fn format_cmd_runtime(dur: std::time::Duration) -> String { + const ETERNITY: u128 = f32::INFINITY as u128; + let mut micros = dur.as_micros(); + let mut millis = 0; + let mut seconds = 0; + let mut minutes = 0; + let mut hours = 0; + let mut days = 0; + let mut weeks = 0; + let mut months = 0; + let mut years = 0; + let mut decades = 0; + let mut centuries = 0; + let mut millennia = 0; + let mut epochs = 0; + let mut aeons = 0; + let mut eternities = 0; + + if micros >= 1000 { + millis = micros / 1000; + micros %= 1000; + } + if millis >= 1000 { + seconds = millis / 1000; + millis %= 1000; + } + if seconds >= 60 { + minutes = seconds / 60; + seconds %= 60; + } + if minutes >= 60 { + hours = minutes / 60; + minutes %= 60; + } + if hours >= 24 { + days = hours / 24; + hours %= 24; + } + if days >= 7 { + weeks = days / 7; + days %= 7; + } + if weeks >= 4 { + months = weeks / 4; + weeks %= 4; + } + if months >= 12 { + years = months / 12; + weeks %= 12; + } + if years >= 10 { + decades = years / 10; + years %= 10; + } + if decades >= 10 { + centuries = decades / 10; + decades %= 10; + } + if centuries >= 10 { + millennia = centuries / 10; + centuries %= 10; + } + if millennia >= 1000 { + epochs = millennia / 1000; + millennia %= 1000; + } + if epochs >= 1000 { + aeons = epochs / 1000; + epochs %= aeons; + } + if aeons == ETERNITY { + eternities = aeons / ETERNITY; + aeons %= ETERNITY; + } + + // Format the result + let mut result = Vec::new(); + if eternities > 0 { + let mut string = format!("{} eternit", eternities); + if eternities > 1 { + string.push_str("ies"); + } else { + string.push('y'); + } + result.push(string) + } + if aeons > 0 { + let mut string = format!("{} aeon", aeons); + if aeons > 1 { + string.push('s') + } + result.push(string) + } + if epochs > 0 { + let mut string = format!("{} epoch", epochs); + if epochs > 1 { + string.push('s') + } + result.push(string) + } + if millennia > 0 { + let mut string = format!("{} millenni", millennia); + if millennia > 1 { + string.push_str("um") + } else { + string.push('a') + } + result.push(string) + } + if centuries > 0 { + let mut string = format!("{} centur", centuries); + if centuries > 1 { + string.push_str("ies") + } else { + string.push('y') + } + result.push(string) + } + if decades > 0 { + let mut string = format!("{} decade", decades); + if decades > 1 { + string.push('s') + } + result.push(string) + } + if years > 0 { + let mut string = format!("{} year", years); + if years > 1 { + string.push('s') + } + result.push(string) + } + if months > 0 { + let mut string = format!("{} month", months); + if months > 1 { + string.push('s') + } + result.push(string) + } + if weeks > 0 { + let mut string = format!("{} week", weeks); + if weeks > 1 { + string.push('s') + } + result.push(string) + } + if days > 0 { + let mut string = format!("{} day", days); + if days > 1 { + string.push('s') + } + result.push(string) + } + if hours > 0 { + let string = format!("{}h", hours); + result.push(string); + } + if minutes > 0 { + let string = format!("{}m", minutes); + result.push(string); + } + if seconds > 0 { + let string = format!("{}s", seconds); + result.push(string); + } + if millis > 0 { + let string = format!("{}ms",millis); + result.push(string); + } + if result.is_empty() && micros > 0 { + let string = format!("{}ms",micros); + result.push(string); + } + + result.join(" ") +} + +fn tokenize_prompt(raw: &str) -> Vec { + let mut chars = raw.chars().peekable(); + let mut tk_text = String::new(); + let mut tokens = vec![]; + + while let Some(ch) = chars.next() { + log!(DEBUG,tokens); + log!(DEBUG,ch); + match ch { + '\\' => { + // Push any accumulated text as a token + if !tk_text.is_empty() { + tokens.push(PromptTk::Text(std::mem::take(&mut tk_text))); + } + + // Handle the escape sequence + if let Some(ch) = chars.next() { + match ch { + 'w' => tokens.push(PromptTk::Pwd), + 'W' => tokens.push(PromptTk::PwdShort), + 'h' => tokens.push(PromptTk::Hostname), + 'H' => tokens.push(PromptTk::HostnameShort), + 's' => tokens.push(PromptTk::ShellName), + 'u' => tokens.push(PromptTk::Username), + '$' => tokens.push(PromptTk::PromptSymbol), + 'n' => tokens.push(PromptTk::Text("\n".into())), + 'r' => tokens.push(PromptTk::Text("\r".into())), + 'T' => tokens.push(PromptTk::Runtime), + '\\' => tokens.push(PromptTk::Text("\\".into())), + '"' => tokens.push(PromptTk::Text("\"".into())), + '\'' => tokens.push(PromptTk::Text("'".into())), + 'e' => { + if chars.next() == Some('[') { + let mut params = String::new(); + + // Collect parameters and final character + while let Some(ch) = chars.next() { + match ch { + '0'..='9' | ';' | '?' | ':' => params.push(ch), // Valid parameter characters + 'A'..='Z' | 'a'..='z' => { // Final character (letter) + params.push(ch); + break; + } + _ => { + // Invalid character in ANSI sequence + tokens.push(PromptTk::Text(format!("\x1b[{params}"))); + break; + } + } + } + + tokens.push(PromptTk::AnsiSeq(format!("\x1b[{params}"))); + } else { + // Handle case where 'e' is not followed by '[' + tokens.push(PromptTk::Text("\\e".into())); + } + } + '0'..='7' => { + // Handle octal escape + let mut octal_str = String::new(); + octal_str.push(ch); + + // Collect up to 2 more octal digits + for _ in 0..2 { + if let Some(&next_ch) = chars.peek() { + if next_ch >= '0' && next_ch <= '7' { + octal_str.push(chars.next().unwrap()); + } else { + break; + } + } else { + break; + } + } + + // Parse the octal string into an integer + if let Ok(octal) = i32::from_str_radix(&octal_str, 8) { + tokens.push(PromptTk::AsciiOct(octal)); + } else { + // Fallback: treat as raw text + tokens.push(PromptTk::Text(format!("\\{octal_str}"))); + } + } + _ => { + // Unknown escape sequence: treat as raw text + tokens.push(PromptTk::Text(format!("\\{ch}"))); + } + } + } else { + // Handle trailing backslash + tokens.push(PromptTk::Text("\\".into())); + } + } + _ => { + // Accumulate non-escape characters + tk_text.push(ch); + } + } + } + + // Push any remaining text as a token + if !tk_text.is_empty() { + tokens.push(PromptTk::Text(tk_text)); + } + + tokens +} + +pub fn expand_prompt(raw: &str, shenv: &mut ShEnv) -> ShResult { + let mut tokens = tokenize_prompt(raw).into_iter(); + let mut result = String::new(); + + while let Some(token) = tokens.next() { + match token { + PromptTk::AsciiOct(_) => todo!(), + PromptTk::Text(txt) => result.push_str(&txt), + PromptTk::AnsiSeq(params) => result.push_str(¶ms), + PromptTk::Runtime => { + if let Some(runtime) = shenv.meta().get_runtime() { + let runtime_fmt = format_cmd_runtime(runtime); + result.push_str(&runtime_fmt); + } + } + PromptTk::Pwd => { + let mut pwd = std::env::var("PWD")?; + let home = std::env::var("HOME")?; + if pwd.starts_with(&home) { + pwd = pwd.replacen(&home, "~", 1); + } + result.push_str(&pwd); + } + PromptTk::PwdShort => { + let mut path = std::env::var("PWD")?; + let home = std::env::var("HOME")?; + if path.starts_with(&home) { + path = path.replacen(&home, "~", 1); + } + let pathbuf = PathBuf::from(&path); + let mut segments = pathbuf.iter().count(); + let mut path_iter = pathbuf.into_iter(); + while segments > 4 { + path_iter.next(); + segments -= 1; + } + let path_rebuilt: PathBuf = path_iter.collect(); + let mut path_rebuilt = path_rebuilt.to_str().unwrap().to_string(); + if path_rebuilt.starts_with(&home) { + path_rebuilt = path_rebuilt.replacen(&home, "~", 1); + } + result.push_str(&path_rebuilt); + } + PromptTk::Hostname => { + let hostname = std::env::var("HOSTNAME")?; + result.push_str(&hostname); + } + PromptTk::HostnameShort => todo!(), + PromptTk::ShellName => result.push_str("fern"), + PromptTk::Username => { + let username = std::env::var("USER")?; + result.push_str(&username); + } + PromptTk::PromptSymbol => { + let uid = std::env::var("UID")?; + let symbol = if &uid == "0" { + '#' + } else { + '$' + }; + result.push(symbol); + } + PromptTk::ExitCode => todo!(), + PromptTk::SuccessSymbol => todo!(), + PromptTk::FailureSymbol => todo!(), + PromptTk::JobCount => todo!(), + _ => unimplemented!() + } + } + + Ok(result) +} diff --git a/src/expand/tilde.rs b/src/expand/tilde.rs index adac341..afe3951 100644 --- a/src/expand/tilde.rs +++ b/src/expand/tilde.rs @@ -6,8 +6,7 @@ pub fn expand_tilde_token(tilde_sub: Token, shenv: &mut ShEnv) -> Token { if result == tilde_sub_raw { return tilde_sub } - let mut tokens = Lexer::new(result,shenv).lex(); - tokens.pop().unwrap_or(tilde_sub) + shenv.expand_input(&result, tilde_sub.span()).pop().unwrap_or(tilde_sub) } pub fn expand_tilde_string(s: &str) -> String { diff --git a/src/expand/vars.rs b/src/expand/vars.rs index a11ef77..bb4899b 100644 --- a/src/expand/vars.rs +++ b/src/expand/vars.rs @@ -20,6 +20,7 @@ pub fn expand_string(s: &str, shenv: &mut ShEnv) -> ShResult { while let Some(ch) = chars.next() { match ch { '\\' => { + result.push(ch); if let Some(next_ch) = chars.next() { result.push(next_ch) } diff --git a/src/libsh/utils.rs b/src/libsh/utils.rs index f462437..7a66c6c 100644 --- a/src/libsh/utils.rs +++ b/src/libsh/utils.rs @@ -303,15 +303,6 @@ impl CmdRedirs { } Self { open: vec![], targets_fd, targets_file, targets_text } } - pub fn close_all(&mut self) -> ShResult<()> { - while let Some(fd) = self.open.pop() { - if let Err(e) = close(fd) { - self.open.push(fd); - return Err(e.into()) - } - } - Ok(()) - } pub fn activate(&mut self) -> ShResult<()> { self.open_file_tgts()?; self.open_fd_tgts()?; @@ -328,12 +319,12 @@ impl CmdRedirs { RedirTarget::HereDoc(body) | RedirTarget::HereString(body) => { write(wpipe_fd, body.as_bytes())?; - close(wpipe)?; + close(wpipe).ok(); } _ => unreachable!() } dup2(rpipe, src.as_raw_fd())?; - close(rpipe)?; + close(rpipe).ok(); } Ok(()) } diff --git a/src/prelude.rs b/src/prelude.rs index 046b0ed..1edf234 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -132,6 +132,7 @@ pub use crate::{ expand::{ expand_argv, expand_token, + prompt::expand_prompt, alias::expand_aliases }, shellenv::{ diff --git a/src/prompt/mod.rs b/src/prompt/mod.rs index c7378bd..e20e529 100644 --- a/src/prompt/mod.rs +++ b/src/prompt/mod.rs @@ -28,7 +28,8 @@ fn init_rl<'a>(shenv: &'a mut ShEnv) -> Editor, DefaultHistory> { pub fn read_line(shenv: &mut ShEnv) -> ShResult { log!(TRACE, "Entering prompt"); - let prompt = "$ ".styled(Style::Green | Style::Bold); + let ps1 = std::env::var("PS1").unwrap_or("\\$ ".styled(Style::Green | Style::Bold)); + let prompt = expand_prompt(&ps1,shenv)?; let mut editor = init_rl(shenv); match editor.readline(&prompt) { Ok(line) => { diff --git a/src/shellenv/exec_ctx.rs b/src/shellenv/exec_ctx.rs index d7e6f1f..37f9dd7 100644 --- a/src/shellenv/exec_ctx.rs +++ b/src/shellenv/exec_ctx.rs @@ -4,6 +4,7 @@ bitflags! { #[derive(Copy,Clone,Debug,PartialEq,PartialOrd)] pub struct ExecFlags: u32 { const NO_FORK = 0x00000001; + const IN_FUNC = 0x00000010; } } diff --git a/src/shellenv/meta.rs b/src/shellenv/meta.rs index a1bc196..8fdfc29 100644 --- a/src/shellenv/meta.rs +++ b/src/shellenv/meta.rs @@ -1,14 +1,32 @@ +use std::time::{Duration, Instant}; + #[derive(Clone,Debug)] pub struct MetaTab { + timer_start: Option, + last_runtime: Option, last_status: i32 } impl MetaTab { pub fn new() -> Self { Self { + timer_start: None, + last_runtime: None, last_status: 0 } } + pub fn start_timer(&mut self) { + self.timer_start = Some(Instant::now()) + } + pub fn stop_timer(&mut self) { + let timer_start = self.timer_start.take(); + if let Some(instant) = timer_start { + self.last_runtime = Some(instant.elapsed()) + } + } + pub fn get_runtime(&self) -> Option { + self.last_runtime + } pub fn set_status(&mut self, code: i32) { self.last_status = code } diff --git a/src/shellenv/shenv.rs b/src/shellenv/shenv.rs index 62563f8..1c305f2 100644 --- a/src/shellenv/shenv.rs +++ b/src/shellenv/shenv.rs @@ -167,11 +167,11 @@ impl ShEnv { let saved_out = saved.stdout; let saved_err = saved.stderr; dup2(saved_in,STDIN_FILENO)?; - close(saved_in)?; + close(saved_in).ok(); dup2(saved_out,STDOUT_FILENO)?; - close(saved_out)?; + close(saved_out).ok(); dup2(saved_err,STDERR_FILENO)?; - close(saved_err)?; + close(saved_err).ok(); } Ok(()) }