Implemented prompt expansion

This commit is contained in:
2025-03-09 03:30:03 -04:00
parent 90a188d4b2
commit 58abe3bc3d
13 changed files with 430 additions and 23 deletions

View File

@@ -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_raw = argv_iter.next().map(|arg| shenv.input_slice(arg.span()).into()).unwrap_or(std::env::var("HOME")?);
let dir = PathBuf::from(&dir_raw); let dir = PathBuf::from(&dir_raw);
std::env::set_current_dir(dir)?; 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); shenv.set_code(0);
} }
Ok(()) Ok(())

View File

@@ -6,9 +6,9 @@ pub fn export(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let mut argv_iter = argv.into_iter(); let mut argv_iter = argv.into_iter();
argv_iter.next(); // Ignore 'export' argv_iter.next(); // Ignore 'export'
while let Some(arg) = argv_iter.next() { 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('=') { if let Some((var,val)) = arg_raw.split_once('=') {
shenv.vars_mut().export(var, val); shenv.vars_mut().export(var, &clean_string(val));
} else { } else {
eprintln!("Expected an assignment in export args, found this: {}", arg_raw) eprintln!("Expected an assignment in export args, found this: {}", arg_raw)
} }

View File

@@ -24,7 +24,9 @@ pub fn exec_input<S: Into<String>>(input: S, shenv: &mut ShEnv) -> ShResult<()>
let parse_time = std::time::Instant::now(); let parse_time = std::time::Instant::now();
let syn_tree = Parser::new(token_stream,shenv).parse()?; let syn_tree = Parser::new(token_stream,shenv).parse()?;
log!(INFO, "Parsing done in {:?}", parse_time.elapsed()); 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(); let exec_time = std::time::Instant::now();
if let Err(e) = Executor::new(syn_tree, shenv).walk() { if let Err(e) = Executor::new(syn_tree, shenv).walk() {
@@ -32,13 +34,18 @@ pub fn exec_input<S: Into<String>>(input: S, shenv: &mut ShEnv) -> ShResult<()>
let code = shenv.get_code(); let code = shenv.get_code();
sh_quit(code); sh_quit(code);
} else { } else {
shenv.reset_io()?; if !shenv.ctx().flags().contains(ExecFlags::IN_FUNC) {
shenv.reset_io()?;
}
return Err(e.into()) return Err(e.into())
} }
} }
log!(INFO, "Executing done in {:?}", exec_time.elapsed()); log!(INFO, "Executing done in {:?}", exec_time.elapsed());
log!(INFO, "Total time spent: {:?}", total_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(()) 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 body = shenv.logic().get_function(&func_name).unwrap().to_string();
let snapshot = shenv.clone(); let snapshot = shenv.clone();
shenv.vars_mut().reset_params(); shenv.vars_mut().reset_params();
shenv.ctx_mut().set_flag(ExecFlags::IN_FUNC);
while let Some(arg) = argv_iter.next() { while let Some(arg) = argv_iter.next() {
let arg_raw = shenv.input_slice(arg.span()).to_string(); let arg_raw = shenv.input_slice(arg.span()).to_string();
shenv.vars_mut().bpush_arg(&arg_raw); shenv.vars_mut().bpush_arg(&arg_raw);

View File

@@ -3,6 +3,7 @@ pub mod tilde;
pub mod alias; pub mod alias;
pub mod cmdsub; pub mod cmdsub;
pub mod arithmetic; pub mod arithmetic;
pub mod prompt;
use arithmetic::expand_arith_token; use arithmetic::expand_arith_token;
use cmdsub::expand_cmdsub_token; use cmdsub::expand_cmdsub_token;

385
src/expand/prompt.rs Normal file
View File

@@ -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<PromptTk> {
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<String> {
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(&params),
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)
}

View File

@@ -6,8 +6,7 @@ pub fn expand_tilde_token(tilde_sub: Token, shenv: &mut ShEnv) -> Token {
if result == tilde_sub_raw { if result == tilde_sub_raw {
return tilde_sub return tilde_sub
} }
let mut tokens = Lexer::new(result,shenv).lex(); shenv.expand_input(&result, tilde_sub.span()).pop().unwrap_or(tilde_sub)
tokens.pop().unwrap_or(tilde_sub)
} }
pub fn expand_tilde_string(s: &str) -> String { pub fn expand_tilde_string(s: &str) -> String {

View File

@@ -20,6 +20,7 @@ pub fn expand_string(s: &str, shenv: &mut ShEnv) -> ShResult<String> {
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
'\\' => { '\\' => {
result.push(ch);
if let Some(next_ch) = chars.next() { if let Some(next_ch) = chars.next() {
result.push(next_ch) result.push(next_ch)
} }

View File

@@ -303,15 +303,6 @@ impl CmdRedirs {
} }
Self { open: vec![], targets_fd, targets_file, targets_text } 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<()> { pub fn activate(&mut self) -> ShResult<()> {
self.open_file_tgts()?; self.open_file_tgts()?;
self.open_fd_tgts()?; self.open_fd_tgts()?;
@@ -328,12 +319,12 @@ impl CmdRedirs {
RedirTarget::HereDoc(body) | RedirTarget::HereDoc(body) |
RedirTarget::HereString(body) => { RedirTarget::HereString(body) => {
write(wpipe_fd, body.as_bytes())?; write(wpipe_fd, body.as_bytes())?;
close(wpipe)?; close(wpipe).ok();
} }
_ => unreachable!() _ => unreachable!()
} }
dup2(rpipe, src.as_raw_fd())?; dup2(rpipe, src.as_raw_fd())?;
close(rpipe)?; close(rpipe).ok();
} }
Ok(()) Ok(())
} }

View File

@@ -132,6 +132,7 @@ pub use crate::{
expand::{ expand::{
expand_argv, expand_argv,
expand_token, expand_token,
prompt::expand_prompt,
alias::expand_aliases alias::expand_aliases
}, },
shellenv::{ shellenv::{

View File

@@ -28,7 +28,8 @@ fn init_rl<'a>(shenv: &'a mut ShEnv) -> Editor<SynHelper<'a>, DefaultHistory> {
pub fn read_line(shenv: &mut ShEnv) -> ShResult<String> { pub fn read_line(shenv: &mut ShEnv) -> ShResult<String> {
log!(TRACE, "Entering prompt"); 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); let mut editor = init_rl(shenv);
match editor.readline(&prompt) { match editor.readline(&prompt) {
Ok(line) => { Ok(line) => {

View File

@@ -4,6 +4,7 @@ bitflags! {
#[derive(Copy,Clone,Debug,PartialEq,PartialOrd)] #[derive(Copy,Clone,Debug,PartialEq,PartialOrd)]
pub struct ExecFlags: u32 { pub struct ExecFlags: u32 {
const NO_FORK = 0x00000001; const NO_FORK = 0x00000001;
const IN_FUNC = 0x00000010;
} }
} }

View File

@@ -1,14 +1,32 @@
use std::time::{Duration, Instant};
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct MetaTab { pub struct MetaTab {
timer_start: Option<Instant>,
last_runtime: Option<Duration>,
last_status: i32 last_status: i32
} }
impl MetaTab { impl MetaTab {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
timer_start: None,
last_runtime: None,
last_status: 0 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<Duration> {
self.last_runtime
}
pub fn set_status(&mut self, code: i32) { pub fn set_status(&mut self, code: i32) {
self.last_status = code self.last_status = code
} }

View File

@@ -167,11 +167,11 @@ impl ShEnv {
let saved_out = saved.stdout; let saved_out = saved.stdout;
let saved_err = saved.stderr; let saved_err = saved.stderr;
dup2(saved_in,STDIN_FILENO)?; dup2(saved_in,STDIN_FILENO)?;
close(saved_in)?; close(saved_in).ok();
dup2(saved_out,STDOUT_FILENO)?; dup2(saved_out,STDOUT_FILENO)?;
close(saved_out)?; close(saved_out).ok();
dup2(saved_err,STDERR_FILENO)?; dup2(saved_err,STDERR_FILENO)?;
close(saved_err)?; close(saved_err).ok();
} }
Ok(()) Ok(())
} }