diff --git a/src/builtin/alias.rs b/src/builtin/alias.rs index 412726c..f56f491 100644 --- a/src/builtin/alias.rs +++ b/src/builtin/alias.rs @@ -3,7 +3,7 @@ use crate::{ libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, prelude::*, - procio::{borrow_fd, IoStack}, + procio::{IoStack, borrow_fd}, state::{self, read_logic, write_logic}, }; diff --git a/src/builtin/cd.rs b/src/builtin/cd.rs index bdddf5b..af5dce3 100644 --- a/src/builtin/cd.rs +++ b/src/builtin/cd.rs @@ -43,15 +43,19 @@ pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> { } if let Err(e) = env::set_current_dir(new_dir) { - return Err(ShErr::full( - ShErrKind::ExecFail, - format!("cd: Failed to change directory: {}", e), - span, - )); - } - let new_dir = env::current_dir().map_err( - |e| ShErr::full(ShErrKind::ExecFail, format!("cd: Failed to get current directory: {}", e), span) - )?; + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("cd: Failed to change directory: {}", e), + span, + )); + } + let new_dir = env::current_dir().map_err(|e| { + ShErr::full( + ShErrKind::ExecFail, + format!("cd: Failed to get current directory: {}", e), + span, + ) + })?; unsafe { env::set_var("PWD", new_dir) }; state::set_status(0); diff --git a/src/builtin/echo.rs b/src/builtin/echo.rs index 1b03d6e..4619707 100644 --- a/src/builtin/echo.rs +++ b/src/builtin/echo.rs @@ -1,14 +1,34 @@ use std::sync::LazyLock; use crate::{ - builtin::setup_builtin, expand::expand_prompt, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{IoStack, borrow_fd}, state + builtin::setup_builtin, + expand::expand_prompt, + getopt::{Opt, OptSpec, get_opts_from_tokens}, + jobs::JobBldr, + libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, + parse::{NdRule, Node}, + prelude::*, + procio::{IoStack, borrow_fd}, + state, }; -pub const ECHO_OPTS: [OptSpec;4] = [ - OptSpec { opt: Opt::Short('n'), takes_arg: false }, - OptSpec { opt: Opt::Short('E'), takes_arg: false }, - OptSpec { opt: Opt::Short('e'), takes_arg: false }, - OptSpec { opt: Opt::Short('p'), takes_arg: false }, +pub const ECHO_OPTS: [OptSpec; 4] = [ + OptSpec { + opt: Opt::Short('n'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('E'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('e'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('p'), + takes_arg: false, + }, ]; bitflags! { @@ -16,7 +36,7 @@ bitflags! { const NO_NEWLINE = 0b000001; const USE_STDERR = 0b000010; const USE_ESCAPE = 0b000100; - const USE_PROMPT = 0b001000; + const USE_PROMPT = 0b001000; } } @@ -40,13 +60,15 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( borrow_fd(STDOUT_FILENO) }; - let mut echo_output = prepare_echo_args(argv - .into_iter() - .map(|a| a.0) // Extract the String from the tuple of (String,Span) - .collect::>(), - flags.contains(EchoFlags::USE_ESCAPE), - flags.contains(EchoFlags::USE_PROMPT) - )?.join(" "); + let mut echo_output = prepare_echo_args( + argv + .into_iter() + .map(|a| a.0) // Extract the String from the tuple of (String,Span) + .collect::>(), + flags.contains(EchoFlags::USE_ESCAPE), + flags.contains(EchoFlags::USE_PROMPT), + )? + .join(" "); if !flags.contains(EchoFlags::NO_NEWLINE) && !echo_output.ends_with('\n') { echo_output.push('\n') @@ -58,137 +80,141 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( Ok(()) } -pub fn prepare_echo_args(argv: Vec, use_escape: bool, use_prompt: bool) -> ShResult> { - if !use_escape { - if use_prompt { - let expanded: ShResult> = argv - .into_iter() - .map(|s| expand_prompt(s.as_str())) - .collect(); - return expanded - } - return Ok(argv); - } +pub fn prepare_echo_args( + argv: Vec, + use_escape: bool, + use_prompt: bool, +) -> ShResult> { + if !use_escape { + if use_prompt { + let expanded: ShResult> = argv + .into_iter() + .map(|s| expand_prompt(s.as_str())) + .collect(); + return expanded; + } + return Ok(argv); + } - let mut prepared_args = Vec::with_capacity(argv.len()); + let mut prepared_args = Vec::with_capacity(argv.len()); - for arg in argv { - let mut prepared_arg = String::new(); - if use_prompt { - prepared_arg = expand_prompt(&prepared_arg)?; - } + for arg in argv { + let mut prepared_arg = String::new(); + if use_prompt { + prepared_arg = expand_prompt(&prepared_arg)?; + } - let mut chars = arg.chars().peekable(); + let mut chars = arg.chars().peekable(); - while let Some(c) = chars.next() { - if c == '\\' { - if let Some(&next_char) = chars.peek() { - match next_char { - 'n' => { - prepared_arg.push('\n'); - chars.next(); - } - 't' => { - prepared_arg.push('\t'); - chars.next(); - } - 'r' => { - prepared_arg.push('\r'); - chars.next(); - } - 'a' => { - prepared_arg.push('\x07'); - chars.next(); - } - 'b' => { - prepared_arg.push('\x08'); - chars.next(); - } - 'e' | 'E' => { - prepared_arg.push('\x1b'); - chars.next(); - } - 'x' => { - chars.next(); // consume 'x' - let mut hex_digits = String::new(); - for _ in 0..2 { - if let Some(&hex_char) = chars.peek() { - if hex_char.is_ascii_hexdigit() { - hex_digits.push(hex_char); - chars.next(); - } else { - break; - } - } else { - break; - } - } - if let Ok(value) = u8::from_str_radix(&hex_digits, 16) { - prepared_arg.push(value as char); - } else { - prepared_arg.push('\\'); - prepared_arg.push('x'); - prepared_arg.push_str(&hex_digits); - } - } - '0' => { - chars.next(); // consume '0' - let mut octal_digits = String::new(); - for _ in 0..3 { - if let Some(&octal_char) = chars.peek() { - if ('0'..='7').contains(&octal_char) { - octal_digits.push(octal_char); - chars.next(); - } else { - break; - } - } else { - break; - } - } - if let Ok(value) = u8::from_str_radix(&octal_digits, 8) { - prepared_arg.push(value as char); - } else { - prepared_arg.push('\\'); - prepared_arg.push('0'); - prepared_arg.push_str(&octal_digits); - } - } - '\\' => { - prepared_arg.push('\\'); - chars.next(); - } - _ => prepared_arg.push(c), - } - } else { - prepared_arg.push(c); - } - } else { - prepared_arg.push(c); - } - } + while let Some(c) = chars.next() { + if c == '\\' { + if let Some(&next_char) = chars.peek() { + match next_char { + 'n' => { + prepared_arg.push('\n'); + chars.next(); + } + 't' => { + prepared_arg.push('\t'); + chars.next(); + } + 'r' => { + prepared_arg.push('\r'); + chars.next(); + } + 'a' => { + prepared_arg.push('\x07'); + chars.next(); + } + 'b' => { + prepared_arg.push('\x08'); + chars.next(); + } + 'e' | 'E' => { + prepared_arg.push('\x1b'); + chars.next(); + } + 'x' => { + chars.next(); // consume 'x' + let mut hex_digits = String::new(); + for _ in 0..2 { + if let Some(&hex_char) = chars.peek() { + if hex_char.is_ascii_hexdigit() { + hex_digits.push(hex_char); + chars.next(); + } else { + break; + } + } else { + break; + } + } + if let Ok(value) = u8::from_str_radix(&hex_digits, 16) { + prepared_arg.push(value as char); + } else { + prepared_arg.push('\\'); + prepared_arg.push('x'); + prepared_arg.push_str(&hex_digits); + } + } + '0' => { + chars.next(); // consume '0' + let mut octal_digits = String::new(); + for _ in 0..3 { + if let Some(&octal_char) = chars.peek() { + if ('0'..='7').contains(&octal_char) { + octal_digits.push(octal_char); + chars.next(); + } else { + break; + } + } else { + break; + } + } + if let Ok(value) = u8::from_str_radix(&octal_digits, 8) { + prepared_arg.push(value as char); + } else { + prepared_arg.push('\\'); + prepared_arg.push('0'); + prepared_arg.push_str(&octal_digits); + } + } + '\\' => { + prepared_arg.push('\\'); + chars.next(); + } + _ => prepared_arg.push(c), + } + } else { + prepared_arg.push(c); + } + } else { + prepared_arg.push(c); + } + } - prepared_args.push(prepared_arg); - } + prepared_args.push(prepared_arg); + } - Ok(prepared_args) + Ok(prepared_args) } pub fn get_echo_flags(opts: Vec) -> ShResult { let mut flags = EchoFlags::empty(); - for opt in opts { + for opt in opts { match opt { Opt::Short('n') => flags |= EchoFlags::NO_NEWLINE, Opt::Short('r') => flags |= EchoFlags::USE_STDERR, Opt::Short('e') => flags |= EchoFlags::USE_ESCAPE, - Opt::Short('p') => flags |= EchoFlags::USE_PROMPT, - _ => { - return Err(ShErr::simple( - ShErrKind::ExecFail, - format!("echo: Unexpected flag '{opt}'"), - )); - } + Opt::Short('p') => flags |= EchoFlags::USE_PROMPT, + _ => { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("echo: Unexpected flag '{opt}'"), + )); + } } } diff --git a/src/builtin/export.rs b/src/builtin/export.rs index 8030f6d..f7fb020 100644 --- a/src/builtin/export.rs +++ b/src/builtin/export.rs @@ -35,10 +35,10 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult for (arg, _) in argv { if let Some((var, val)) = arg.split_once('=') { write_vars(|v| v.set_var(var, val, VarFlags::EXPORT)); // Export an assignment like - // 'foo=bar' + // 'foo=bar' } else { write_vars(|v| v.export_var(&arg)); // Export an existing variable, if - // any + // any } } } diff --git a/src/builtin/flowctl.rs b/src/builtin/flowctl.rs index 236ff33..f057fe3 100644 --- a/src/builtin/flowctl.rs +++ b/src/builtin/flowctl.rs @@ -1,6 +1,6 @@ use crate::{ libsh::error::{ShErr, ShErrKind, ShResult}, - parse::{execute::prepare_argv, NdRule, Node}, + parse::{NdRule, Node, execute::prepare_argv}, prelude::*, }; @@ -32,7 +32,6 @@ pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> { code = status; } - let kind = match kind { LoopContinue(_) => LoopContinue(code), LoopBreak(_) => LoopBreak(code), diff --git a/src/builtin/jobctl.rs b/src/builtin/jobctl.rs index b18c496..c23b7a8 100644 --- a/src/builtin/jobctl.rs +++ b/src/builtin/jobctl.rs @@ -1,9 +1,9 @@ use crate::{ jobs::{JobBldr, JobCmdFlags, JobID}, libsh::error::{ShErr, ShErrKind, ShResult}, - parse::{lex::Span, NdRule, Node}, + parse::{NdRule, Node, lex::Span}, prelude::*, - procio::{borrow_fd, IoStack}, + procio::{IoStack, borrow_fd}, state::{self, read_jobs, write_jobs}, }; @@ -168,7 +168,7 @@ pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( ShErrKind::SyntaxErr, "Invalid flag in jobs call", span, - )) + )); } }; flags |= flag diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index cb2f1a9..8dde960 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -4,7 +4,9 @@ use crate::{ jobs::{ChildProc, JobBldr}, libsh::error::ShResult, parse::{ - Redir, execute::prepare_argv, lex::{Span, Tk} + Redir, + execute::prepare_argv, + lex::{Span, Tk}, }, procio::{IoFrame, IoStack, RedirGuard}, }; @@ -16,19 +18,17 @@ pub mod export; pub mod flowctl; pub mod jobctl; pub mod pwd; +pub mod read; pub mod shift; pub mod shopt; pub mod source; pub mod test; // [[ ]] thing -pub mod read; -pub mod zoltraak; pub mod trap; +pub mod zoltraak; pub const BUILTINS: [&str; 21] = [ - "echo", "cd", "read", "export", "pwd", "source", - "shift", "jobs", "fg", "bg", "alias", "unalias", - "return", "break", "continue", "exit", "zoltraak", - "shopt", "builtin", "command", "trap" + "echo", "cd", "read", "export", "pwd", "source", "shift", "jobs", "fg", "bg", "alias", "unalias", + "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap", ]; /// Sets up a builtin command diff --git a/src/builtin/pwd.rs b/src/builtin/pwd.rs index 3cc4acf..6e37dde 100644 --- a/src/builtin/pwd.rs +++ b/src/builtin/pwd.rs @@ -3,7 +3,7 @@ use crate::{ libsh::error::ShResult, parse::{NdRule, Node}, prelude::*, - procio::{borrow_fd, IoStack}, + procio::{IoStack, borrow_fd}, state, }; diff --git a/src/builtin/read.rs b/src/builtin/read.rs index c7f0961..639aa41 100644 --- a/src/builtin/read.rs +++ b/src/builtin/read.rs @@ -1,188 +1,237 @@ use bitflags::bitflags; -use nix::{errno::Errno, libc::{STDIN_FILENO, STDOUT_FILENO}, unistd::{isatty, read, write}}; +use nix::{ + errno::Errno, + libc::{STDIN_FILENO, STDOUT_FILENO}, + unistd::{isatty, read, write}, +}; -use crate::{builtin::setup_builtin, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, procio::{IoStack, borrow_fd}, prompt::readline::term::RawModeGuard, state::{self, VarFlags, read_vars, write_vars}}; +use crate::{ + builtin::setup_builtin, + getopt::{Opt, OptSpec, get_opts_from_tokens}, + jobs::JobBldr, + libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, + parse::{NdRule, Node}, + procio::{IoStack, borrow_fd}, + prompt::readline::term::RawModeGuard, + state::{self, VarFlags, read_vars, write_vars}, +}; -pub const READ_OPTS: [OptSpec;7] = [ - OptSpec { opt: Opt::Short('r'), takes_arg: false }, // don't allow backslash escapes - OptSpec { opt: Opt::Short('s'), takes_arg: false }, // don't echo input - OptSpec { opt: Opt::Short('a'), takes_arg: false }, // read into array - OptSpec { opt: Opt::Short('n'), takes_arg: false }, // read only N characters - OptSpec { opt: Opt::Short('t'), takes_arg: false }, // timeout - OptSpec { opt: Opt::Short('p'), takes_arg: true }, // prompt - OptSpec { opt: Opt::Short('d'), takes_arg: true }, // read until delimiter +pub const READ_OPTS: [OptSpec; 7] = [ + OptSpec { + opt: Opt::Short('r'), + takes_arg: false, + }, // don't allow backslash escapes + OptSpec { + opt: Opt::Short('s'), + takes_arg: false, + }, // don't echo input + OptSpec { + opt: Opt::Short('a'), + takes_arg: false, + }, // read into array + OptSpec { + opt: Opt::Short('n'), + takes_arg: false, + }, // read only N characters + OptSpec { + opt: Opt::Short('t'), + takes_arg: false, + }, // timeout + OptSpec { + opt: Opt::Short('p'), + takes_arg: true, + }, // prompt + OptSpec { + opt: Opt::Short('d'), + takes_arg: true, + }, // read until delimiter ]; bitflags! { - pub struct ReadFlags: u32 { - const NO_ESCAPES = 0b000001; - const NO_ECHO = 0b000010; // TODO: unused - const ARRAY = 0b000100; // TODO: unused - const N_CHARS = 0b001000; // TODO: unused - const TIMEOUT = 0b010000; // TODO: unused - } + pub struct ReadFlags: u32 { + const NO_ESCAPES = 0b000001; + const NO_ECHO = 0b000010; // TODO: unused + const ARRAY = 0b000100; // TODO: unused + const N_CHARS = 0b001000; // TODO: unused + const TIMEOUT = 0b010000; // TODO: unused + } } pub struct ReadOpts { - prompt: Option, - delim: u8, // byte representation of the delimiter character - flags: ReadFlags, + prompt: Option, + delim: u8, // byte representation of the delimiter character + flags: ReadFlags, } pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { - let blame = node.get_span().clone(); - let NdRule::Command { - assignments: _, - argv - } = node.class else { - unreachable!() - }; + let blame = node.get_span().clone(); + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; - let (argv, opts) = get_opts_from_tokens(argv, &READ_OPTS); - let read_opts = get_read_flags(opts).blame(blame.clone())?; - let (argv, _) = setup_builtin(argv, job, None).blame(blame.clone())?; + let (argv, opts) = get_opts_from_tokens(argv, &READ_OPTS); + let read_opts = get_read_flags(opts).blame(blame.clone())?; + let (argv, _) = setup_builtin(argv, job, None).blame(blame.clone())?; - if let Some(prompt) = read_opts.prompt { - write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?; - } + if let Some(prompt) = read_opts.prompt { + write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?; + } - let input = if isatty(STDIN_FILENO)? { - // Restore default terminal settings - RawModeGuard::with_cooked_mode(|| { - let mut input: Vec = vec![]; - let mut escaped = false; - loop { - let mut buf = [0u8;1]; - match read(STDIN_FILENO, &mut buf) { - Ok(0) => { - state::set_status(1); - let str_result = String::from_utf8(input.clone()).map_err(|e| ShErr::simple( - ShErrKind::ExecFail, - format!("read: Input was not valid UTF-8: {e}"), - ))?; - return Ok(str_result); // EOF - } - Ok(_) => { - if buf[0] == read_opts.delim { - if read_opts.flags.contains(ReadFlags::NO_ESCAPES) && escaped { - input.push(buf[0]); - } else { - // Delimiter reached, stop reading - break; - } - } - else if read_opts.flags.contains(ReadFlags::NO_ESCAPES) - && buf[0] == b'\\' { - escaped = true; - } else { - input.push(buf[0]); - } - } - Err(Errno::EINTR) => continue, - Err(e) => return Err(ShErr::simple( - ShErrKind::ExecFail, - format!("read: Failed to read from stdin: {e}"), - )), - } - } + let input = if isatty(STDIN_FILENO)? { + // Restore default terminal settings + RawModeGuard::with_cooked_mode(|| { + let mut input: Vec = vec![]; + let mut escaped = false; + loop { + let mut buf = [0u8; 1]; + match read(STDIN_FILENO, &mut buf) { + Ok(0) => { + state::set_status(1); + let str_result = String::from_utf8(input.clone()).map_err(|e| { + ShErr::simple( + ShErrKind::ExecFail, + format!("read: Input was not valid UTF-8: {e}"), + ) + })?; + return Ok(str_result); // EOF + } + Ok(_) => { + if buf[0] == read_opts.delim { + if read_opts.flags.contains(ReadFlags::NO_ESCAPES) && escaped { + input.push(buf[0]); + } else { + // Delimiter reached, stop reading + break; + } + } else if read_opts.flags.contains(ReadFlags::NO_ESCAPES) && buf[0] == b'\\' { + escaped = true; + } else { + input.push(buf[0]); + } + } + Err(Errno::EINTR) => continue, + Err(e) => { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("read: Failed to read from stdin: {e}"), + )); + } + } + } - state::set_status(0); - let str_result = String::from_utf8(input.clone()).map_err(|e| ShErr::simple( - ShErrKind::ExecFail, - format!("read: Input was not valid UTF-8: {e}"), - ))?; - Ok(str_result) - }).blame(blame)? - } else { - let mut input: Vec = vec![]; - loop { - let mut buf = [0u8;1]; - match read(STDIN_FILENO, &mut buf) { - Ok(0) => { - state::set_status(1); - break; // EOF - } - Ok(_) => { - if buf[0] == read_opts.delim { - break; // Delimiter reached, stop reading - } - input.push(buf[0]); - } - Err(Errno::EINTR) => continue, - Err(e) => return Err(ShErr::simple( - ShErrKind::ExecFail, - format!("read: Failed to read from stdin: {e}"), - )), - } - } - String::from_utf8(input).map_err(|e| ShErr::simple( - ShErrKind::ExecFail, - format!("read: Input was not valid UTF-8: {e}"), - ))? - }; + state::set_status(0); + let str_result = String::from_utf8(input.clone()).map_err(|e| { + ShErr::simple( + ShErrKind::ExecFail, + format!("read: Input was not valid UTF-8: {e}"), + ) + })?; + Ok(str_result) + }) + .blame(blame)? + } else { + let mut input: Vec = vec![]; + loop { + let mut buf = [0u8; 1]; + match read(STDIN_FILENO, &mut buf) { + Ok(0) => { + state::set_status(1); + break; // EOF + } + Ok(_) => { + if buf[0] == read_opts.delim { + break; // Delimiter reached, stop reading + } + input.push(buf[0]); + } + Err(Errno::EINTR) => continue, + Err(e) => { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("read: Failed to read from stdin: {e}"), + )); + } + } + } + String::from_utf8(input).map_err(|e| { + ShErr::simple( + ShErrKind::ExecFail, + format!("read: Input was not valid UTF-8: {e}"), + ) + })? + }; - if argv.is_empty() { - write_vars(|v| { - v.set_var("REPLY", &input, VarFlags::NONE); - }); - } else { - // get our field separator - let mut field_sep = read_vars(|v| v.get_var("IFS")); - if field_sep.is_empty() { field_sep = " ".to_string() } - let mut remaining = input; + if argv.is_empty() { + write_vars(|v| { + v.set_var("REPLY", &input, VarFlags::NONE); + }); + } else { + // get our field separator + let mut field_sep = read_vars(|v| v.get_var("IFS")); + if field_sep.is_empty() { + field_sep = " ".to_string() + } + let mut remaining = input; - for (i, arg) in argv.iter().enumerate() { - if i == argv.len() - 1 { - // Last arg, stuff the rest of the input into it - write_vars(|v| v.set_var(&arg.0, &remaining, VarFlags::NONE)); - break; - } + for (i, arg) in argv.iter().enumerate() { + if i == argv.len() - 1 { + // Last arg, stuff the rest of the input into it + write_vars(|v| v.set_var(&arg.0, &remaining, VarFlags::NONE)); + break; + } - // trim leading IFS characters - let trimmed = remaining.trim_start_matches(|c: char| field_sep.contains(c)); + // trim leading IFS characters + let trimmed = remaining.trim_start_matches(|c: char| field_sep.contains(c)); - if let Some(idx) = trimmed.find(|c: char| field_sep.contains(c)) { - // We found a field separator, split at the char index - let (field, rest) = trimmed.split_at(idx); - write_vars(|v| v.set_var(&arg.0, field, VarFlags::NONE)); + if let Some(idx) = trimmed.find(|c: char| field_sep.contains(c)) { + // We found a field separator, split at the char index + let (field, rest) = trimmed.split_at(idx); + write_vars(|v| v.set_var(&arg.0, field, VarFlags::NONE)); - // note that this doesn't account for consecutive IFS characters, which is what that trim above is for - remaining = rest.to_string(); - } else { - write_vars(|v| v.set_var(&arg.0, trimmed, VarFlags::NONE)); - remaining.clear(); - } - } - } + // note that this doesn't account for consecutive IFS characters, which is what + // that trim above is for + remaining = rest.to_string(); + } else { + write_vars(|v| v.set_var(&arg.0, trimmed, VarFlags::NONE)); + remaining.clear(); + } + } + } - state::set_status(0); - Ok(()) + state::set_status(0); + Ok(()) } pub fn get_read_flags(opts: Vec) -> ShResult { - let mut read_opts = ReadOpts { - prompt: None, - delim: b'\n', - flags: ReadFlags::empty(), - }; + let mut read_opts = ReadOpts { + prompt: None, + delim: b'\n', + flags: ReadFlags::empty(), + }; - for opt in opts { - match opt { - Opt::Short('r') => read_opts.flags |= ReadFlags::NO_ESCAPES, - Opt::Short('s') => read_opts.flags |= ReadFlags::NO_ECHO, - Opt::Short('a') => read_opts.flags |= ReadFlags::ARRAY, - Opt::Short('n') => read_opts.flags |= ReadFlags::N_CHARS, - Opt::Short('t') => read_opts.flags |= ReadFlags::TIMEOUT, - Opt::ShortWithArg('p', prompt) => read_opts.prompt = Some(prompt), - Opt::ShortWithArg('d', delim) => read_opts.delim = delim.chars().map(|c| c as u8).next().unwrap_or(b'\n'), - _ => { - return Err(ShErr::simple( - ShErrKind::ExecFail, - format!("read: Unexpected flag '{opt}'"), - )); - } - } - } + for opt in opts { + match opt { + Opt::Short('r') => read_opts.flags |= ReadFlags::NO_ESCAPES, + Opt::Short('s') => read_opts.flags |= ReadFlags::NO_ECHO, + Opt::Short('a') => read_opts.flags |= ReadFlags::ARRAY, + Opt::Short('n') => read_opts.flags |= ReadFlags::N_CHARS, + Opt::Short('t') => read_opts.flags |= ReadFlags::TIMEOUT, + Opt::ShortWithArg('p', prompt) => read_opts.prompt = Some(prompt), + Opt::ShortWithArg('d', delim) => { + read_opts.delim = delim.chars().map(|c| c as u8).next().unwrap_or(b'\n') + } + _ => { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("read: Unexpected flag '{opt}'"), + )); + } + } + } - Ok(read_opts) + Ok(read_opts) } diff --git a/src/builtin/shopt.rs b/src/builtin/shopt.rs index 96dc4f6..fe6ac9d 100644 --- a/src/builtin/shopt.rs +++ b/src/builtin/shopt.rs @@ -3,7 +3,7 @@ use crate::{ libsh::error::{ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, - procio::{borrow_fd, IoStack}, + procio::{IoStack, borrow_fd}, state::write_shopts, }; diff --git a/src/builtin/test.rs b/src/builtin/test.rs index 551b4fa..655c65c 100644 --- a/src/builtin/test.rs +++ b/src/builtin/test.rs @@ -8,7 +8,7 @@ use regex::Regex; use crate::{ libsh::error::{ShErr, ShErrKind, ShResult}, - parse::{ConjunctOp, NdRule, Node, TestCase, TEST_UNARY_OPS}, + parse::{ConjunctOp, NdRule, Node, TEST_UNARY_OPS, TestCase}, prelude::*, }; @@ -254,7 +254,7 @@ pub fn double_bracket_test(node: Node) -> ShResult { msg: "Expected a binary operator in this test call; found a unary operator".into(), notes: vec![], span: err_span, - }) + }); } TestOp::StringEq => rhs.trim() == lhs.trim(), TestOp::StringNeq => rhs.trim() != lhs.trim(), diff --git a/src/builtin/trap.rs b/src/builtin/trap.rs index 2ab3a08..7c817ba 100644 --- a/src/builtin/trap.rs +++ b/src/builtin/trap.rs @@ -1,162 +1,171 @@ use std::{fmt::Display, str::FromStr}; -use nix::{libc::{STDERR_FILENO, STDOUT_FILENO}, sys::signal::Signal, unistd::write}; +use nix::{ + libc::{STDERR_FILENO, STDOUT_FILENO}, + sys::signal::Signal, + unistd::write, +}; -use crate::{builtin::setup_builtin, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, procio::{IoStack, borrow_fd}, state::{self, read_logic, write_logic}}; +use crate::{ + builtin::setup_builtin, + jobs::JobBldr, + libsh::error::{ShErr, ShErrKind, ShResult}, + parse::{NdRule, Node}, + procio::{IoStack, borrow_fd}, + state::{self, read_logic, write_logic}, +}; #[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)] pub enum TrapTarget { - Exit, - Error, - Signal(Signal) + Exit, + Error, + Signal(Signal), } impl FromStr for TrapTarget { - type Err = ShErr; - fn from_str(s: &str) -> Result { - match s { - "EXIT" => Ok(TrapTarget::Exit), - "ERR" => Ok(TrapTarget::Error), + type Err = ShErr; + fn from_str(s: &str) -> Result { + match s { + "EXIT" => Ok(TrapTarget::Exit), + "ERR" => Ok(TrapTarget::Error), - "INT" => Ok(TrapTarget::Signal(Signal::SIGINT)), - "QUIT" => Ok(TrapTarget::Signal(Signal::SIGQUIT)), - "ILL" => Ok(TrapTarget::Signal(Signal::SIGILL)), - "TRAP" => Ok(TrapTarget::Signal(Signal::SIGTRAP)), - "ABRT" => Ok(TrapTarget::Signal(Signal::SIGABRT)), - "BUS" => Ok(TrapTarget::Signal(Signal::SIGBUS)), - "FPE" => Ok(TrapTarget::Signal(Signal::SIGFPE)), - "KILL" => Ok(TrapTarget::Signal(Signal::SIGKILL)), - "USR1" => Ok(TrapTarget::Signal(Signal::SIGUSR1)), - "SEGV" => Ok(TrapTarget::Signal(Signal::SIGSEGV)), - "USR2" => Ok(TrapTarget::Signal(Signal::SIGUSR2)), - "PIPE" => Ok(TrapTarget::Signal(Signal::SIGPIPE)), - "ALRM" => Ok(TrapTarget::Signal(Signal::SIGALRM)), - "TERM" => Ok(TrapTarget::Signal(Signal::SIGTERM)), - "STKFLT" => Ok(TrapTarget::Signal(Signal::SIGSTKFLT)), - "CHLD" => Ok(TrapTarget::Signal(Signal::SIGCHLD)), - "CONT" => Ok(TrapTarget::Signal(Signal::SIGCONT)), - "STOP" => Ok(TrapTarget::Signal(Signal::SIGSTOP)), - "TSTP" => Ok(TrapTarget::Signal(Signal::SIGTSTP)), - "TTIN" => Ok(TrapTarget::Signal(Signal::SIGTTIN)), - "TTOU" => Ok(TrapTarget::Signal(Signal::SIGTTOU)), - "URG" => Ok(TrapTarget::Signal(Signal::SIGURG)), - "XCPU" => Ok(TrapTarget::Signal(Signal::SIGXCPU)), - "XFSZ" => Ok(TrapTarget::Signal(Signal::SIGXFSZ)), - "VTALRM" => Ok(TrapTarget::Signal(Signal::SIGVTALRM)), - "PROF" => Ok(TrapTarget::Signal(Signal::SIGPROF)), - "WINCH" => Ok(TrapTarget::Signal(Signal::SIGWINCH)), - "IO" => Ok(TrapTarget::Signal(Signal::SIGIO)), - "PWR" => Ok(TrapTarget::Signal(Signal::SIGPWR)), - "SYS" => Ok(TrapTarget::Signal(Signal::SIGSYS)), - _ => { - return Err(ShErr::simple( - ShErrKind::ExecFail, - format!("invalid trap target '{}'", s), - )) - } - } - } + "INT" => Ok(TrapTarget::Signal(Signal::SIGINT)), + "QUIT" => Ok(TrapTarget::Signal(Signal::SIGQUIT)), + "ILL" => Ok(TrapTarget::Signal(Signal::SIGILL)), + "TRAP" => Ok(TrapTarget::Signal(Signal::SIGTRAP)), + "ABRT" => Ok(TrapTarget::Signal(Signal::SIGABRT)), + "BUS" => Ok(TrapTarget::Signal(Signal::SIGBUS)), + "FPE" => Ok(TrapTarget::Signal(Signal::SIGFPE)), + "KILL" => Ok(TrapTarget::Signal(Signal::SIGKILL)), + "USR1" => Ok(TrapTarget::Signal(Signal::SIGUSR1)), + "SEGV" => Ok(TrapTarget::Signal(Signal::SIGSEGV)), + "USR2" => Ok(TrapTarget::Signal(Signal::SIGUSR2)), + "PIPE" => Ok(TrapTarget::Signal(Signal::SIGPIPE)), + "ALRM" => Ok(TrapTarget::Signal(Signal::SIGALRM)), + "TERM" => Ok(TrapTarget::Signal(Signal::SIGTERM)), + "STKFLT" => Ok(TrapTarget::Signal(Signal::SIGSTKFLT)), + "CHLD" => Ok(TrapTarget::Signal(Signal::SIGCHLD)), + "CONT" => Ok(TrapTarget::Signal(Signal::SIGCONT)), + "STOP" => Ok(TrapTarget::Signal(Signal::SIGSTOP)), + "TSTP" => Ok(TrapTarget::Signal(Signal::SIGTSTP)), + "TTIN" => Ok(TrapTarget::Signal(Signal::SIGTTIN)), + "TTOU" => Ok(TrapTarget::Signal(Signal::SIGTTOU)), + "URG" => Ok(TrapTarget::Signal(Signal::SIGURG)), + "XCPU" => Ok(TrapTarget::Signal(Signal::SIGXCPU)), + "XFSZ" => Ok(TrapTarget::Signal(Signal::SIGXFSZ)), + "VTALRM" => Ok(TrapTarget::Signal(Signal::SIGVTALRM)), + "PROF" => Ok(TrapTarget::Signal(Signal::SIGPROF)), + "WINCH" => Ok(TrapTarget::Signal(Signal::SIGWINCH)), + "IO" => Ok(TrapTarget::Signal(Signal::SIGIO)), + "PWR" => Ok(TrapTarget::Signal(Signal::SIGPWR)), + "SYS" => Ok(TrapTarget::Signal(Signal::SIGSYS)), + _ => { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("invalid trap target '{}'", s), + )); + } + } + } } impl Display for TrapTarget { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TrapTarget::Exit => write!(f, "EXIT"), - TrapTarget::Error => write!(f, "ERR"), - TrapTarget::Signal(s) => { - match s { - Signal::SIGHUP => write!(f, "HUP"), - Signal::SIGINT => write!(f, "INT"), - Signal::SIGQUIT => write!(f, "QUIT"), - Signal::SIGILL => write!(f, "ILL"), - Signal::SIGTRAP => write!(f, "TRAP"), - Signal::SIGABRT => write!(f, "ABRT"), - Signal::SIGBUS => write!(f, "BUS"), - Signal::SIGFPE => write!(f, "FPE"), - Signal::SIGKILL => write!(f, "KILL"), - Signal::SIGUSR1 => write!(f, "USR1"), - Signal::SIGSEGV => write!(f, "SEGV"), - Signal::SIGUSR2 => write!(f, "USR2"), - Signal::SIGPIPE => write!(f, "PIPE"), - Signal::SIGALRM => write!(f, "ALRM"), - Signal::SIGTERM => write!(f, "TERM"), - Signal::SIGSTKFLT => write!(f, "STKFLT"), - Signal::SIGCHLD => write!(f, "CHLD"), - Signal::SIGCONT => write!(f, "CONT"), - Signal::SIGSTOP => write!(f, "STOP"), - Signal::SIGTSTP => write!(f, "TSTP"), - Signal::SIGTTIN => write!(f, "TTIN"), - Signal::SIGTTOU => write!(f, "TTOU"), - Signal::SIGURG => write!(f, "URG"), - Signal::SIGXCPU => write!(f, "XCPU"), - Signal::SIGXFSZ => write!(f, "XFSZ"), - Signal::SIGVTALRM => write!(f, "VTALRM"), - Signal::SIGPROF => write!(f, "PROF"), - Signal::SIGWINCH => write!(f, "WINCH"), - Signal::SIGIO => write!(f, "IO"), - Signal::SIGPWR => write!(f, "PWR"), - Signal::SIGSYS => write!(f, "SYS"), + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TrapTarget::Exit => write!(f, "EXIT"), + TrapTarget::Error => write!(f, "ERR"), + TrapTarget::Signal(s) => match s { + Signal::SIGHUP => write!(f, "HUP"), + Signal::SIGINT => write!(f, "INT"), + Signal::SIGQUIT => write!(f, "QUIT"), + Signal::SIGILL => write!(f, "ILL"), + Signal::SIGTRAP => write!(f, "TRAP"), + Signal::SIGABRT => write!(f, "ABRT"), + Signal::SIGBUS => write!(f, "BUS"), + Signal::SIGFPE => write!(f, "FPE"), + Signal::SIGKILL => write!(f, "KILL"), + Signal::SIGUSR1 => write!(f, "USR1"), + Signal::SIGSEGV => write!(f, "SEGV"), + Signal::SIGUSR2 => write!(f, "USR2"), + Signal::SIGPIPE => write!(f, "PIPE"), + Signal::SIGALRM => write!(f, "ALRM"), + Signal::SIGTERM => write!(f, "TERM"), + Signal::SIGSTKFLT => write!(f, "STKFLT"), + Signal::SIGCHLD => write!(f, "CHLD"), + Signal::SIGCONT => write!(f, "CONT"), + Signal::SIGSTOP => write!(f, "STOP"), + Signal::SIGTSTP => write!(f, "TSTP"), + Signal::SIGTTIN => write!(f, "TTIN"), + Signal::SIGTTOU => write!(f, "TTOU"), + Signal::SIGURG => write!(f, "URG"), + Signal::SIGXCPU => write!(f, "XCPU"), + Signal::SIGXFSZ => write!(f, "XFSZ"), + Signal::SIGVTALRM => write!(f, "VTALRM"), + Signal::SIGPROF => write!(f, "PROF"), + Signal::SIGWINCH => write!(f, "WINCH"), + Signal::SIGIO => write!(f, "IO"), + Signal::SIGPWR => write!(f, "PWR"), + Signal::SIGSYS => write!(f, "SYS"), - _ => { - log::warn!("TrapTarget::fmt() : unrecognized signal {}", s); - Err(std::fmt::Error) - } - } - } - } - } + _ => { + log::warn!("TrapTarget::fmt() : unrecognized signal {}", s); + Err(std::fmt::Error) + } + }, + } + } } pub fn trap(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { - let span = node.get_span(); - let NdRule::Command { - assignments: _, - argv, - } = node.class - else { - unreachable!() - }; + let span = node.get_span(); + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; - let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; - if argv.is_empty() { - let stdout = borrow_fd(STDOUT_FILENO); + if argv.is_empty() { + let stdout = borrow_fd(STDOUT_FILENO); - return read_logic(|l| -> ShResult<()> { - for l in l.traps() { - let target = l.0; - let command = l.1; - write(stdout, format!("trap -- '{command}' {target}\n").as_bytes())?; - } - Ok(()) - }); - } + return read_logic(|l| -> ShResult<()> { + for l in l.traps() { + let target = l.0; + let command = l.1; + write(stdout, format!("trap -- '{command}' {target}\n").as_bytes())?; + } + Ok(()) + }); + } - if argv.len() == 1 { - let stderr = borrow_fd(STDERR_FILENO); - write(stderr, b"usage: trap [SIGNAL...]\n")?; - state::set_status(1); - return Ok(()) - } + if argv.len() == 1 { + let stderr = borrow_fd(STDERR_FILENO); + write(stderr, b"usage: trap [SIGNAL...]\n")?; + state::set_status(1); + return Ok(()); + } - let mut args = argv.into_iter(); + let mut args = argv.into_iter(); - let command = args.next().unwrap().0; - let mut targets = vec![]; + let command = args.next().unwrap().0; + let mut targets = vec![]; - while let Some((arg, _)) = args.next() { - let target = arg.parse::()?; - targets.push(target); - } + while let Some((arg, _)) = args.next() { + let target = arg.parse::()?; + targets.push(target); + } - for target in targets { - if &command == "-" { - write_logic(|l| l.remove_trap(target)) - } else { - write_logic(|l| l.insert_trap(target, command.clone())) - } - } + for target in targets { + if &command == "-" { + write_logic(|l| l.remove_trap(target)) + } else { + write_logic(|l| l.insert_trap(target, command.clone())) + } + } - state::set_status(0); - Ok(()) + state::set_status(0); + Ok(()) } diff --git a/src/builtin/zoltraak.rs b/src/builtin/zoltraak.rs index ca0a095..2d5d173 100644 --- a/src/builtin/zoltraak.rs +++ b/src/builtin/zoltraak.rs @@ -37,14 +37,32 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu else { unreachable!() }; - let zolt_opts = [ - OptSpec { opt: Opt::Long("dry-run".into()), takes_arg: false }, - OptSpec { opt: Opt::Long("confirm".into()), takes_arg: false }, - OptSpec { opt: Opt::Long("no-preserve-root".into()), takes_arg: false }, - OptSpec { opt: Opt::Short('r'), takes_arg: false }, - OptSpec { opt: Opt::Short('f'), takes_arg: false }, - OptSpec { opt: Opt::Short('v'), takes_arg: false } - ]; + let zolt_opts = [ + OptSpec { + opt: Opt::Long("dry-run".into()), + takes_arg: false, + }, + OptSpec { + opt: Opt::Long("confirm".into()), + takes_arg: false, + }, + OptSpec { + opt: Opt::Long("no-preserve-root".into()), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('r'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('f'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('v'), + takes_arg: false, + }, + ]; let mut flags = ZoltFlags::empty(); let (argv, opts) = get_opts_from_tokens(argv, &zolt_opts); @@ -56,41 +74,40 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu "confirm" => flags |= ZoltFlags::CONFIRM, "dry-run" => flags |= ZoltFlags::DRY, _ => { - return Err(ShErr::simple( - ShErrKind::SyntaxErr, - format!("zoltraak: unrecognized option '{flag}'"), - )); - } + return Err(ShErr::simple( + ShErrKind::SyntaxErr, + format!("zoltraak: unrecognized option '{flag}'"), + )); + } }, Opt::Short(flag) => match flag { 'r' => flags |= ZoltFlags::RECURSIVE, 'f' => flags |= ZoltFlags::FORCE, 'v' => flags |= ZoltFlags::VERBOSE, _ => { - return Err(ShErr::simple( - ShErrKind::SyntaxErr, - format!("zoltraak: unrecognized option '{flag}'"), - )); - } + return Err(ShErr::simple( + ShErrKind::SyntaxErr, + format!("zoltraak: unrecognized option '{flag}'"), + )); + } }, - Opt::LongWithArg(flag, _) => { - return Err(ShErr::simple( - ShErrKind::SyntaxErr, - format!("zoltraak: unrecognized option '{flag}'"), - )); - } - Opt::ShortWithArg(flag, _) => { - return Err(ShErr::simple( - ShErrKind::SyntaxErr, - format!("zoltraak: unrecognized option '{flag}'"), - )); - } + Opt::LongWithArg(flag, _) => { + return Err(ShErr::simple( + ShErrKind::SyntaxErr, + format!("zoltraak: unrecognized option '{flag}'"), + )); + } + Opt::ShortWithArg(flag, _) => { + return Err(ShErr::simple( + ShErrKind::SyntaxErr, + format!("zoltraak: unrecognized option '{flag}'"), + )); + } } } let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; - for (arg, span) in argv { if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) { return Err( @@ -109,7 +126,6 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu } } - Ok(()) } diff --git a/src/expand.rs b/src/expand.rs index 575bd4e..296b5a9 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -7,11 +7,13 @@ use regex::Regex; use crate::libsh::error::{ShErr, ShErrKind, ShResult}; use crate::parse::execute::exec_input; -use crate::parse::lex::{is_field_sep, is_hard_sep, LexFlags, LexStream, Tk, TkFlags, TkRule}; +use crate::parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule, is_field_sep, is_hard_sep}; use crate::parse::{Redir, RedirType}; -use crate::{jobs, prelude::*}; use crate::procio::{IoBuf, IoFrame, IoMode, IoStack}; -use crate::state::{LogTab, VarFlags, read_jobs, read_logic, read_vars, write_jobs, write_meta, write_vars}; +use crate::state::{ + LogTab, VarFlags, read_jobs, read_logic, read_vars, write_jobs, write_meta, write_vars, +}; +use crate::{jobs, prelude::*}; const PARAMETERS: [char; 7] = ['@', '*', '#', '$', '?', '!', '0']; @@ -30,8 +32,9 @@ pub const PROC_SUB_IN: char = '\u{fdd5}'; /// Output process sub marker pub const PROC_SUB_OUT: char = '\u{fdd6}'; /// Marker for null expansion -/// This is used for when "$@" or "$*" are used in quotes and there are no arguments -/// Without this marker, it would be handled like an empty string, which breaks some commands +/// This is used for when "$@" or "$*" are used in quotes and there are no +/// arguments Without this marker, it would be handled like an empty string, +/// which breaks some commands pub const NULL_EXPAND: char = '\u{fdd7}'; impl Tk { @@ -58,34 +61,35 @@ pub struct Expander { impl Expander { pub fn new(raw: Tk) -> ShResult { - let raw = raw.span.as_str(); - Self::from_raw(raw) + let raw = raw.span.as_str(); + Self::from_raw(raw) + } + pub fn from_raw(raw: &str) -> ShResult { + let raw = expand_braces_full(raw)?.join(" "); + let unescaped = unescape_str(&raw); + Ok(Self { raw: unescaped }) } - pub fn from_raw(raw: &str) -> ShResult { - let raw = expand_braces_full(raw)?.join(" "); - let unescaped = unescape_str(&raw); - Ok(Self { raw: unescaped }) - } pub fn expand(&mut self) -> ShResult> { let mut chars = self.raw.chars().peekable(); self.raw = expand_raw(&mut chars)?; - let has_trailing_slash = self.raw.ends_with('/'); - let has_leading_dot_slash = self.raw.starts_with("./"); + let has_trailing_slash = self.raw.ends_with('/'); + let has_leading_dot_slash = self.raw.starts_with("./"); if let Ok(glob_exp) = expand_glob(&self.raw) - && !glob_exp.is_empty() { - self.raw = glob_exp; - } + && !glob_exp.is_empty() + { + self.raw = glob_exp; + } - if has_trailing_slash && !self.raw.ends_with('/') { - // glob expansion can remove trailing slashes and leading dot-slashes, but we want to preserve them - // so that things like tab completion don't break - self.raw.push('/'); - } - if has_leading_dot_slash && !self.raw.starts_with("./") { - self.raw.insert_str(0, "./"); - } + if has_trailing_slash && !self.raw.ends_with('/') { + // glob expansion can remove trailing slashes and leading dot-slashes, but we + // want to preserve them so that things like tab completion don't break + self.raw.push('/'); + } + if has_leading_dot_slash && !self.raw.starts_with("./") { + self.raw.insert_str(0, "./"); + } Ok(self.split_words()) } @@ -93,7 +97,7 @@ impl Expander { let mut words = vec![]; let mut chars = self.raw.chars(); let mut cur_word = String::new(); - let mut was_quoted = false; + let mut was_quoted = false; 'outer: while let Some(ch) = chars.next() { match ch { @@ -101,347 +105,387 @@ impl Expander { while let Some(q_ch) = chars.next() { match q_ch { _ if q_ch == ch => { - was_quoted = true; - continue 'outer; // Isn't rust cool - } + was_quoted = true; + continue 'outer; // Isn't rust cool + } _ => cur_word.push(q_ch), } } } _ if is_field_sep(ch) => { - if cur_word.is_empty() && !was_quoted { - cur_word.clear(); - } else { - words.push(mem::take(&mut cur_word)); - } - was_quoted = false; + if cur_word.is_empty() && !was_quoted { + cur_word.clear(); + } else { + words.push(mem::take(&mut cur_word)); + } + was_quoted = false; } _ => cur_word.push(ch), } } - if words.is_empty() && (cur_word.is_empty() && !was_quoted) { - return words; - } else { - words.push(cur_word); - } + if words.is_empty() && (cur_word.is_empty() && !was_quoted) { + return words; + } else { + words.push(cur_word); + } - words.retain(|w| w != &NULL_EXPAND.to_string()); + words.retain(|w| w != &NULL_EXPAND.to_string()); words } } /// Check if a string contains valid brace expansion patterns. -/// Returns true if there's a valid {a,b} or {1..5} pattern at the outermost level. +/// Returns true if there's a valid {a,b} or {1..5} pattern at the outermost +/// level. fn has_braces(s: &str) -> bool { - let mut chars = s.chars().peekable(); - let mut depth = 0; - let mut found_open = false; - let mut has_comma = false; - let mut has_range = false; - let mut cur_quote: Option = None; + let mut chars = s.chars().peekable(); + let mut depth = 0; + let mut found_open = false; + let mut has_comma = false; + let mut has_range = false; + let mut cur_quote: Option = None; - while let Some(ch) = chars.next() { - match ch { - '\\' => { chars.next(); } // skip escaped char - '\'' if cur_quote.is_none() => cur_quote = Some('\''), - '\'' if cur_quote == Some('\'') => cur_quote = None, - '"' if cur_quote.is_none() => cur_quote = Some('"'), - '"' if cur_quote == Some('"') => cur_quote = None, - '{' if cur_quote.is_none() => { - if depth == 0 { - found_open = true; - has_comma = false; - has_range = false; - } - depth += 1; - } - '}' if cur_quote.is_none() && depth > 0 => { - depth -= 1; - if depth == 0 && found_open && (has_comma || has_range) { - return true; - } - } - ',' if cur_quote.is_none() && depth == 1 => { - has_comma = true; - } - '.' if cur_quote.is_none() && depth == 1 => { - if chars.peek() == Some(&'.') { - chars.next(); - has_range = true; - } - } - _ => {} - } - } - false + while let Some(ch) = chars.next() { + match ch { + '\\' => { + chars.next(); + } // skip escaped char + '\'' if cur_quote.is_none() => cur_quote = Some('\''), + '\'' if cur_quote == Some('\'') => cur_quote = None, + '"' if cur_quote.is_none() => cur_quote = Some('"'), + '"' if cur_quote == Some('"') => cur_quote = None, + '{' if cur_quote.is_none() => { + if depth == 0 { + found_open = true; + has_comma = false; + has_range = false; + } + depth += 1; + } + '}' if cur_quote.is_none() && depth > 0 => { + depth -= 1; + if depth == 0 && found_open && (has_comma || has_range) { + return true; + } + } + ',' if cur_quote.is_none() && depth == 1 => { + has_comma = true; + } + '.' if cur_quote.is_none() && depth == 1 => { + if chars.peek() == Some(&'.') { + chars.next(); + has_range = true; + } + } + _ => {} + } + } + false } /// Expand braces in a string, zsh-style: one level per call, loop until done. /// Returns a Vec of expanded strings. fn expand_braces_full(input: &str) -> ShResult> { - let mut results = vec![input.to_string()]; + let mut results = vec![input.to_string()]; - // Keep expanding until no results contain braces - loop { - let mut any_expanded = false; - let mut new_results = Vec::new(); + // Keep expanding until no results contain braces + loop { + let mut any_expanded = false; + let mut new_results = Vec::new(); - for word in results { - if has_braces(&word) { - any_expanded = true; - let expanded = expand_one_brace(&word)?; - new_results.extend(expanded); - } else { - new_results.push(word); - } - } + for word in results { + if has_braces(&word) { + any_expanded = true; + let expanded = expand_one_brace(&word)?; + new_results.extend(expanded); + } else { + new_results.push(word); + } + } - results = new_results; - if !any_expanded { - break; - } - } + results = new_results; + if !any_expanded { + break; + } + } - Ok(results) + Ok(results) } /// Expand the first (outermost) brace expression in a word. /// "pre{a,b}post" -> ["preapost", "prebpost"] /// "pre{1..3}post" -> ["pre1post", "pre2post", "pre3post"] fn expand_one_brace(word: &str) -> ShResult> { - let (prefix, inner, suffix) = match get_brace_parts(word) { - Some(parts) => parts, - None => return Ok(vec![word.to_string()]), // No valid braces - }; + let (prefix, inner, suffix) = match get_brace_parts(word) { + Some(parts) => parts, + None => return Ok(vec![word.to_string()]), // No valid braces + }; - // Split the inner content on top-level commas, or expand as range - let parts = split_brace_inner(&inner); + // Split the inner content on top-level commas, or expand as range + let parts = split_brace_inner(&inner); - // If we got back a single part with no expansion, treat as literal - if parts.len() == 1 && parts[0] == inner { - // Check if it's a range - if let Some(range_parts) = try_expand_range(&inner) { - return Ok(range_parts - .into_iter() - .map(|p| format!("{}{}{}", prefix, p, suffix)) - .collect()); - } - // Not a valid brace expression, return as-is with literal braces - return Ok(vec![format!("{}{{{}}}{}", prefix, inner, suffix)]); - } + // If we got back a single part with no expansion, treat as literal + if parts.len() == 1 && parts[0] == inner { + // Check if it's a range + if let Some(range_parts) = try_expand_range(&inner) { + return Ok( + range_parts + .into_iter() + .map(|p| format!("{}{}{}", prefix, p, suffix)) + .collect(), + ); + } + // Not a valid brace expression, return as-is with literal braces + return Ok(vec![format!("{}{{{}}}{}", prefix, inner, suffix)]); + } - Ok(parts - .into_iter() - .map(|p| format!("{}{}{}", prefix, p, suffix)) - .collect()) + Ok( + parts + .into_iter() + .map(|p| format!("{}{}{}", prefix, p, suffix)) + .collect(), + ) } /// Extract prefix, inner, and suffix from a brace expression. /// "pre{a,b}post" -> Some(("pre", "a,b", "post")) fn get_brace_parts(word: &str) -> Option<(String, String, String)> { - let mut chars = word.chars().peekable(); - let mut prefix = String::new(); - let mut cur_quote: Option = None; + let mut chars = word.chars().peekable(); + let mut prefix = String::new(); + let mut cur_quote: Option = None; - // Find the opening brace - while let Some(ch) = chars.next() { - match ch { - '\\' => { - prefix.push(ch); - if let Some(next) = chars.next() { - prefix.push(next); - } - } - '\'' if cur_quote.is_none() => { cur_quote = Some('\''); - prefix.push(ch); } - '\'' if cur_quote == Some('\'') => { cur_quote = None; - prefix.push(ch); } - '"' if cur_quote.is_none() => { cur_quote = Some('"'); prefix.push(ch); } - '"' if cur_quote == Some('"') => { cur_quote = None; prefix.push(ch); } - '{' if cur_quote.is_none() => { - break; - } - _ => prefix.push(ch), - } - } + // Find the opening brace + while let Some(ch) = chars.next() { + match ch { + '\\' => { + prefix.push(ch); + if let Some(next) = chars.next() { + prefix.push(next); + } + } + '\'' if cur_quote.is_none() => { + cur_quote = Some('\''); + prefix.push(ch); + } + '\'' if cur_quote == Some('\'') => { + cur_quote = None; + prefix.push(ch); + } + '"' if cur_quote.is_none() => { + cur_quote = Some('"'); + prefix.push(ch); + } + '"' if cur_quote == Some('"') => { + cur_quote = None; + prefix.push(ch); + } + '{' if cur_quote.is_none() => { + break; + } + _ => prefix.push(ch), + } + } - // Find matching closing brace - let mut depth = 1; - let mut inner = String::new(); - cur_quote = None; + // Find matching closing brace + let mut depth = 1; + let mut inner = String::new(); + cur_quote = None; - while let Some(ch) = chars.next() { - match ch { - '\\' => { - inner.push(ch); - if let Some(next) = chars.next() { - inner.push(next); - } - } - '\'' if cur_quote.is_none() => { cur_quote = Some('\''); inner.push(ch); } - '\'' if cur_quote == Some('\'') => { cur_quote = None; inner.push(ch); } - '"' if cur_quote.is_none() => { cur_quote = Some('"'); inner.push(ch); } - '"' if cur_quote == Some('"') => { cur_quote = None; inner.push(ch); } - '{' if cur_quote.is_none() => { - depth += 1; - inner.push(ch); - } - '}' if cur_quote.is_none() => { - depth -= 1; - if depth == 0 { - break; - } - inner.push(ch); - } - _ => inner.push(ch), - } - } + while let Some(ch) = chars.next() { + match ch { + '\\' => { + inner.push(ch); + if let Some(next) = chars.next() { + inner.push(next); + } + } + '\'' if cur_quote.is_none() => { + cur_quote = Some('\''); + inner.push(ch); + } + '\'' if cur_quote == Some('\'') => { + cur_quote = None; + inner.push(ch); + } + '"' if cur_quote.is_none() => { + cur_quote = Some('"'); + inner.push(ch); + } + '"' if cur_quote == Some('"') => { + cur_quote = None; + inner.push(ch); + } + '{' if cur_quote.is_none() => { + depth += 1; + inner.push(ch); + } + '}' if cur_quote.is_none() => { + depth -= 1; + if depth == 0 { + break; + } + inner.push(ch); + } + _ => inner.push(ch), + } + } - if depth != 0 { - return None; // Unbalanced braces - } + if depth != 0 { + return None; // Unbalanced braces + } - // Collect suffix - let suffix: String = chars.collect(); + // Collect suffix + let suffix: String = chars.collect(); - Some((prefix, inner, suffix)) + Some((prefix, inner, suffix)) } /// Split brace inner content on top-level commas. /// "a,b,c" -> ["a", "b", "c"] /// "a,{b,c},d" -> ["a", "{b,c}", "d"] fn split_brace_inner(inner: &str) -> Vec { - let mut parts = Vec::new(); - let mut current = String::new(); - let mut chars = inner.chars().peekable(); - let mut depth = 0; - let mut cur_quote: Option = None; + let mut parts = Vec::new(); + let mut current = String::new(); + let mut chars = inner.chars().peekable(); + let mut depth = 0; + let mut cur_quote: Option = None; - while let Some(ch) = chars.next() { - match ch { - '\\' => { - current.push(ch); - if let Some(next) = chars.next() { - current.push(next); - } - } - '\'' if cur_quote.is_none() => { cur_quote = Some('\''); current.push(ch); } - '\'' if cur_quote == Some('\'') => { cur_quote = None; current.push(ch); } - '"' if cur_quote.is_none() => { cur_quote = Some('"'); current.push(ch); } - '"' if cur_quote == Some('"') => { cur_quote = None; current.push(ch); } - '{' if cur_quote.is_none() => { - depth += 1; - current.push(ch); - } - '}' if cur_quote.is_none() => { - depth -= 1; - current.push(ch); - } - ',' if cur_quote.is_none() && depth == 0 => { - parts.push(std::mem::take(&mut current)); - } - _ => current.push(ch), - } - } + while let Some(ch) = chars.next() { + match ch { + '\\' => { + current.push(ch); + if let Some(next) = chars.next() { + current.push(next); + } + } + '\'' if cur_quote.is_none() => { + cur_quote = Some('\''); + current.push(ch); + } + '\'' if cur_quote == Some('\'') => { + cur_quote = None; + current.push(ch); + } + '"' if cur_quote.is_none() => { + cur_quote = Some('"'); + current.push(ch); + } + '"' if cur_quote == Some('"') => { + cur_quote = None; + current.push(ch); + } + '{' if cur_quote.is_none() => { + depth += 1; + current.push(ch); + } + '}' if cur_quote.is_none() => { + depth -= 1; + current.push(ch); + } + ',' if cur_quote.is_none() && depth == 0 => { + parts.push(std::mem::take(&mut current)); + } + _ => current.push(ch), + } + } - parts.push(current); - parts + parts.push(current); + parts } /// Try to expand a range like "1..5" or "a..z" or "1..10..2" fn try_expand_range(inner: &str) -> Option> { - // Look for ".." pattern - let parts: Vec<&str> = inner.split("..").collect(); + // Look for ".." pattern + let parts: Vec<&str> = inner.split("..").collect(); - match parts.len() { - 2 => { - let start = parts[0]; - let end = parts[1]; - expand_range(start, end, 1) - } - 3 => { - let start = parts[0]; - let end = parts[1]; - let step: i32 = parts[2].parse().ok()?; - if step == 0 { return None; } - expand_range(start, end, step.unsigned_abs() as usize) - } - _ => None, - } + match parts.len() { + 2 => { + let start = parts[0]; + let end = parts[1]; + expand_range(start, end, 1) + } + 3 => { + let start = parts[0]; + let end = parts[1]; + let step: i32 = parts[2].parse().ok()?; + if step == 0 { + return None; + } + expand_range(start, end, step.unsigned_abs() as usize) + } + _ => None, + } } -fn expand_range(start: &str, end: &str, step: usize) -> -Option> { - // Try character range first - if is_alpha_range_bound(start) && is_alpha_range_bound(end) { - let start_char = start.chars().next()? as u8; - let end_char = end.chars().next()? as u8; - let reverse = end_char < start_char; +fn expand_range(start: &str, end: &str, step: usize) -> Option> { + // Try character range first + if is_alpha_range_bound(start) && is_alpha_range_bound(end) { + let start_char = start.chars().next()? as u8; + let end_char = end.chars().next()? as u8; + let reverse = end_char < start_char; - let (lo, hi) = if reverse { - (end_char, start_char) - } else { - (start_char, end_char) - }; + let (lo, hi) = if reverse { + (end_char, start_char) + } else { + (start_char, end_char) + }; - let chars: Vec = (lo..=hi) - .step_by(step) - .map(|c| (c as char).to_string()) - .collect(); + let chars: Vec = (lo..=hi) + .step_by(step) + .map(|c| (c as char).to_string()) + .collect(); - return Some(if reverse { - chars.into_iter().rev().collect() - } else { - chars - }); - } + return Some(if reverse { + chars.into_iter().rev().collect() + } else { + chars + }); + } - // Try numeric range - if is_numeric_range_bound(start) && is_numeric_range_bound(end) { - let start_num: i32 = start.parse().ok()?; - let end_num: i32 = end.parse().ok()?; - let reverse = end_num < start_num; + // Try numeric range + if is_numeric_range_bound(start) && is_numeric_range_bound(end) { + let start_num: i32 = start.parse().ok()?; + let end_num: i32 = end.parse().ok()?; + let reverse = end_num < start_num; - // Handle zero-padding - let pad_width = start.len().max(end.len()); - let needs_padding = start.starts_with('0') || - end.starts_with('0'); + // Handle zero-padding + let pad_width = start.len().max(end.len()); + let needs_padding = start.starts_with('0') || end.starts_with('0'); - let (lo, hi) = if reverse { - (end_num, start_num) - } else { - (start_num, end_num) - }; + let (lo, hi) = if reverse { + (end_num, start_num) + } else { + (start_num, end_num) + }; - let nums: Vec = (lo..=hi) - .step_by(step) - .map(|n| { - if needs_padding { - format!("{:0>width$}", n, width = pad_width) - } else { - n.to_string() - } - }) - .collect(); + let nums: Vec = (lo..=hi) + .step_by(step) + .map(|n| { + if needs_padding { + format!("{:0>width$}", n, width = pad_width) + } else { + n.to_string() + } + }) + .collect(); - return Some(if reverse { - nums.into_iter().rev().collect() - } else { - nums - }); - } + return Some(if reverse { + nums.into_iter().rev().collect() + } else { + nums + }); + } - None + None } - fn is_alpha_range_bound(word: &str) -> bool { - word.len() == 1 && word.chars().all(|c| c.is_ascii_alphabetic()) + word.len() == 1 && word.chars().all(|c| c.is_ascii_alphabetic()) } fn is_numeric_range_bound(word: &str) -> bool { - !word.is_empty() && word.chars().all(|c| c.is_ascii_digit()) + !word.is_empty() && word.chars().all(|c| c.is_ascii_digit()) } pub fn expand_raw(chars: &mut Peekable>) -> ShResult { @@ -493,19 +537,19 @@ pub fn expand_var(chars: &mut Peekable>) -> ShResult { SUBSH if var_name.is_empty() => { chars.next(); // now safe to consume let mut subsh_body = String::new(); - let mut found_end = false; + let mut found_end = false; while let Some(c) = chars.next() { if c == SUBSH { - found_end = true; + found_end = true; break; } subsh_body.push(c); } - if !found_end { - // if there isnt a closing SUBSH, we are probably in some tab completion context - // and we got passed some unfinished input. Just treat it as literal text - return Ok(format!("$({subsh_body}")); - } + if !found_end { + // if there isnt a closing SUBSH, we are probably in some tab completion context + // and we got passed some unfinished input. Just treat it as literal text + return Ok(format!("$({subsh_body}")); + } let expanded = expand_cmd_sub(&subsh_body)?; return Ok(expanded); } @@ -527,9 +571,9 @@ pub fn expand_var(chars: &mut Peekable>) -> ShResult { let parameter = format!("{ch}"); let val = read_vars(|v| v.get_var(¶meter)); - if (ch == '@' || ch == '*') && val.is_empty() { - return Ok(NULL_EXPAND.to_string()); - } + if (ch == '@' || ch == '*') && val.is_empty() { + return Ok(NULL_EXPAND.to_string()); + } return Ok(val); } @@ -558,8 +602,8 @@ pub fn expand_glob(raw: &str) -> ShResult { require_literal_leading_dot: !crate::state::read_shopts(|s| s.core.dotglob), ..Default::default() }; - for entry in - glob::glob_with(raw, opts).map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid glob pattern"))? + for entry in glob::glob_with(raw, opts) + .map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid glob pattern"))? { let entry = entry.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid filename found in glob"))?; @@ -625,7 +669,7 @@ impl ArithTk { kind: ShErrKind::ParseErr, msg: "Invalid character in arithmetic substitution".into(), notes: vec![], - }) + }); } } } @@ -707,7 +751,7 @@ impl ArithTk { kind: ShErrKind::ParseErr, msg: "Unexpected token during evaluation".into(), notes: vec![], - }) + }); } } } @@ -807,10 +851,12 @@ pub fn expand_proc_sub(raw: &str, is_input: bool) -> ShResult { /// Get the command output of a given command input as a String pub fn expand_cmd_sub(raw: &str) -> ShResult { - if raw.starts_with('(') && raw.ends_with(')') - && let Ok(output) = expand_arithmetic(raw) { - return Ok(output); // It's actually an arithmetic sub - } + if raw.starts_with('(') + && raw.ends_with(')') + && let Ok(output) = expand_arithmetic(raw) + { + return Ok(output); // It's actually an arithmetic sub + } let (rpipe, wpipe) = IoMode::get_pipes(); let cmd_sub_redir = Redir::new(wpipe, RedirType::Output); let cmd_sub_io_frame = IoFrame::from_redir(cmd_sub_redir); @@ -829,7 +875,8 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult { ForkResult::Parent { child } => { std::mem::drop(cmd_sub_io_frame); // Closes the write pipe - // Read output first (before waiting) to avoid deadlock if child fills pipe buffer + // Read output first (before waiting) to avoid deadlock if child fills pipe + // buffer loop { match io_buf.fill_buffer() { Ok(()) => break, @@ -851,9 +898,7 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult { jobs::take_term()?; match status { - WtStat::Exited(_, _) => { - Ok(io_buf.as_str()?.trim_end().to_string()) - } + WtStat::Exited(_, _) => Ok(io_buf.as_str()?.trim_end().to_string()), _ => Err(ShErr::simple(ShErrKind::InternalErr, "Command sub failed")), } } @@ -914,14 +959,14 @@ pub fn unescape_str(raw: &str) -> String { match q_ch { '\\' => { if let Some(next_ch) = chars.next() { - match next_ch { - '"' | '\\' | '`' | '$' => { - // discard the backslash - } - _ => { - result.push(q_ch); - } - } + match next_ch { + '"' | '\\' | '`' | '$' => { + // discard the backslash + } + _ => { + result.push(q_ch); + } + } result.push(next_ch); } } @@ -1035,77 +1080,77 @@ pub fn unescape_str(raw: &str) -> String { } } } - '$' if chars.peek() == Some(&'\'') => { - chars.next(); - result.push(SNG_QUOTE); - while let Some(q_ch) = chars.next() { - match q_ch { - '\'' => { - result.push(SNG_QUOTE); - break; - } - '\\' => { - if let Some(esc) = chars.next() { - match esc { - 'n' => result.push('\n'), - 't' => result.push('\t'), - 'r' => result.push('\r'), - '\'' => result.push('\''), - '\\' => result.push('\\'), - 'a' => result.push('\x07'), - 'b' => result.push('\x08'), - 'e' | 'E' => result.push('\x1b'), - 'v' => result.push('\x0b'), - 'x' => { - let mut hex = String::new(); - if let Some(h1) = chars.next() { - hex.push(h1); - } else { - result.push_str("\\x"); - continue; - } - if let Some(h2) = chars.next() { - hex.push(h2); - } else { - result.push_str(&format!("\\x{hex}")); - continue; - } - if let Ok(byte) = u8::from_str_radix(&hex, 16) { - result.push(byte as char); - } else { - result.push_str(&format!("\\x{hex}")); - continue; - } - } - 'o' => { - let mut oct = String::new(); - for _ in 0..3 { - if let Some(o) = chars.peek() { - if o.is_digit(8) { - oct.push(*o); - chars.next(); - } else { - break; - } - } else { - break; - } - } - if let Ok(byte) = u8::from_str_radix(&oct, 8) { - result.push(byte as char); - } else { - result.push_str(&format!("\\o{oct}")); - continue; - } - } - _ => result.push(esc), - } - } - } - _ => result.push(q_ch), - } - } - } + '$' if chars.peek() == Some(&'\'') => { + chars.next(); + result.push(SNG_QUOTE); + while let Some(q_ch) = chars.next() { + match q_ch { + '\'' => { + result.push(SNG_QUOTE); + break; + } + '\\' => { + if let Some(esc) = chars.next() { + match esc { + 'n' => result.push('\n'), + 't' => result.push('\t'), + 'r' => result.push('\r'), + '\'' => result.push('\''), + '\\' => result.push('\\'), + 'a' => result.push('\x07'), + 'b' => result.push('\x08'), + 'e' | 'E' => result.push('\x1b'), + 'v' => result.push('\x0b'), + 'x' => { + let mut hex = String::new(); + if let Some(h1) = chars.next() { + hex.push(h1); + } else { + result.push_str("\\x"); + continue; + } + if let Some(h2) = chars.next() { + hex.push(h2); + } else { + result.push_str(&format!("\\x{hex}")); + continue; + } + if let Ok(byte) = u8::from_str_radix(&hex, 16) { + result.push(byte as char); + } else { + result.push_str(&format!("\\x{hex}")); + continue; + } + } + 'o' => { + let mut oct = String::new(); + for _ in 0..3 { + if let Some(o) = chars.peek() { + if o.is_digit(8) { + oct.push(*o); + chars.next(); + } else { + break; + } + } else { + break; + } + } + if let Ok(byte) = u8::from_str_radix(&oct, 8) { + result.push(byte as char); + } else { + result.push_str(&format!("\\o{oct}")); + continue; + } + } + _ => result.push(esc), + } + } + } + _ => result.push(q_ch), + } + } + } '$' => { result.push(VAR_SUB); if chars.peek() == Some(&'$') { @@ -1547,10 +1592,10 @@ pub enum PromptTk { AsciiOct(i32), Text(String), AnsiSeq(String), - Function(String), // Expands to the output of any defined shell function + Function(String), // Expands to the output of any defined shell function VisGrp, UserSeq, - RuntimeMillis, + RuntimeMillis, RuntimeFormatted, Weekday, Dquote, @@ -1568,8 +1613,8 @@ pub enum PromptTk { SuccessSymbol, FailureSymbol, JobCount, - VisGroupOpen, - VisGroupClose, + VisGroupOpen, + VisGroupClose, } pub fn format_cmd_runtime(dur: std::time::Duration) -> String { @@ -1779,41 +1824,40 @@ fn tokenize_prompt(raw: &str) -> Vec { '\\' => tokens.push(PromptTk::Text("\\".into())), '"' => tokens.push(PromptTk::Text("\"".into())), '\'' => tokens.push(PromptTk::Text("'".into())), - '(' => tokens.push(PromptTk::VisGroupOpen), - ')' => tokens.push(PromptTk::VisGroupClose), - '!' => { - let mut func_name = String::new(); - let is_braced = chars.peek() == Some(&'{'); - while let Some(ch) = chars.peek() { - - match ch { - '}' if is_braced => { - chars.next(); - break; - } - 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' => { - func_name.push(*ch); - chars.next(); - } - _ => { - if is_braced { - // Invalid character in braced function name - tokens.push(PromptTk::Text(format!("\\!{{{func_name}"))); - break; - } else { - // End of unbraced function name - let func_exists = read_logic(|l| l.get_func(&func_name).is_some()); - if func_exists { - tokens.push(PromptTk::Function(func_name)); - } else { - tokens.push(PromptTk::Text(format!("\\!{func_name}"))); - } - break; - } - } - } - } - } + '(' => tokens.push(PromptTk::VisGroupOpen), + ')' => tokens.push(PromptTk::VisGroupClose), + '!' => { + let mut func_name = String::new(); + let is_braced = chars.peek() == Some(&'{'); + while let Some(ch) = chars.peek() { + match ch { + '}' if is_braced => { + chars.next(); + break; + } + 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' => { + func_name.push(*ch); + chars.next(); + } + _ => { + if is_braced { + // Invalid character in braced function name + tokens.push(PromptTk::Text(format!("\\!{{{func_name}"))); + break; + } else { + // End of unbraced function name + let func_exists = read_logic(|l| l.get_func(&func_name).is_some()); + if func_exists { + tokens.push(PromptTk::Function(func_name)); + } else { + tokens.push(PromptTk::Text(format!("\\!{func_name}"))); + } + break; + } + } + } + } + } 'e' => { if chars.next() == Some('[') { let mut params = String::new(); @@ -1897,84 +1941,84 @@ pub fn expand_prompt(raw: &str) -> ShResult { 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::RuntimeMillis => { - if let Some(runtime) = write_meta(|m| m.get_time()) { - let runtime_millis = runtime.as_millis().to_string(); - result.push_str(&runtime_millis); - } - } - PromptTk::RuntimeFormatted => { - if let Some(runtime) = write_meta(|m| m.get_time()) { - let runtime_fmt = format_cmd_runtime(runtime); - result.push_str(&runtime_fmt); - } - } - PromptTk::Pwd => { - let mut pwd = std::env::var("PWD").unwrap(); - let home = std::env::var("HOME").unwrap(); - if pwd.starts_with(&home) { - pwd = pwd.replacen(&home, "~", 1); - } - result.push_str(&pwd); - } - PromptTk::PwdShort => { - let mut path = std::env::var("PWD").unwrap(); - let home = std::env::var("HOME").unwrap(); - 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.iter(); - let max_segments = crate::state::read_shopts(|s| s.prompt.trunc_prompt_path); - while segments > max_segments { - 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("HOST").unwrap(); - result.push_str(&hostname); - } - PromptTk::HostnameShort => todo!(), - PromptTk::ShellName => result.push_str("fern"), - PromptTk::Username => { - let username = std::env::var("USER").unwrap(); - result.push_str(&username); - } - PromptTk::PromptSymbol => { - let uid = std::env::var("UID").unwrap(); - let symbol = if &uid == "0" { '#' } else { '$' }; - result.push(symbol); - } - PromptTk::ExitCode => todo!(), - PromptTk::SuccessSymbol => todo!(), - PromptTk::FailureSymbol => todo!(), - PromptTk::JobCount => todo!(), - PromptTk::Function(f) => { - let output = expand_cmd_sub(&f)?; - result.push_str(&output); - } - PromptTk::VisGrp => todo!(), - PromptTk::UserSeq => todo!(), - PromptTk::Weekday => todo!(), - PromptTk::Dquote => todo!(), - PromptTk::Squote => todo!(), - PromptTk::Return => todo!(), - PromptTk::Newline => todo!(), - PromptTk::VisGroupOpen => todo!(), - PromptTk::VisGroupClose => todo!(), - } + match token { + PromptTk::AsciiOct(_) => todo!(), + PromptTk::Text(txt) => result.push_str(&txt), + PromptTk::AnsiSeq(params) => result.push_str(¶ms), + PromptTk::RuntimeMillis => { + if let Some(runtime) = write_meta(|m| m.get_time()) { + let runtime_millis = runtime.as_millis().to_string(); + result.push_str(&runtime_millis); + } + } + PromptTk::RuntimeFormatted => { + if let Some(runtime) = write_meta(|m| m.get_time()) { + let runtime_fmt = format_cmd_runtime(runtime); + result.push_str(&runtime_fmt); + } + } + PromptTk::Pwd => { + let mut pwd = std::env::var("PWD").unwrap(); + let home = std::env::var("HOME").unwrap(); + if pwd.starts_with(&home) { + pwd = pwd.replacen(&home, "~", 1); + } + result.push_str(&pwd); + } + PromptTk::PwdShort => { + let mut path = std::env::var("PWD").unwrap(); + let home = std::env::var("HOME").unwrap(); + 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.iter(); + let max_segments = crate::state::read_shopts(|s| s.prompt.trunc_prompt_path); + while segments > max_segments { + 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("HOST").unwrap(); + result.push_str(&hostname); + } + PromptTk::HostnameShort => todo!(), + PromptTk::ShellName => result.push_str("fern"), + PromptTk::Username => { + let username = std::env::var("USER").unwrap(); + result.push_str(&username); + } + PromptTk::PromptSymbol => { + let uid = std::env::var("UID").unwrap(); + let symbol = if &uid == "0" { '#' } else { '$' }; + result.push(symbol); + } + PromptTk::ExitCode => todo!(), + PromptTk::SuccessSymbol => todo!(), + PromptTk::FailureSymbol => todo!(), + PromptTk::JobCount => todo!(), + PromptTk::Function(f) => { + let output = expand_cmd_sub(&f)?; + result.push_str(&output); + } + PromptTk::VisGrp => todo!(), + PromptTk::UserSeq => todo!(), + PromptTk::Weekday => todo!(), + PromptTk::Dquote => todo!(), + PromptTk::Squote => todo!(), + PromptTk::Return => todo!(), + PromptTk::Newline => todo!(), + PromptTk::VisGroupOpen => todo!(), + PromptTk::VisGroupClose => todo!(), + } } Ok(result) diff --git a/src/getopt.rs b/src/getopt.rs index 07d9db5..0b61d8b 100644 --- a/src/getopt.rs +++ b/src/getopt.rs @@ -9,14 +9,14 @@ pub type OptSet = Arc<[Opt]>; #[derive(Clone, PartialEq, Eq, Debug)] pub enum Opt { Long(String), - LongWithArg(String,String), + LongWithArg(String, String), Short(char), - ShortWithArg(char,String), + ShortWithArg(char, String), } pub struct OptSpec { - pub opt: Opt, - pub takes_arg: bool, + pub opt: Opt, + pub takes_arg: bool, } impl Opt { @@ -41,8 +41,8 @@ impl Display for Opt { match self { Self::Long(opt) => write!(f, "--{}", opt), Self::Short(opt) => write!(f, "-{}", opt), - Self::LongWithArg(opt, arg) => write!(f, "--{} {}", opt, arg), - Self::ShortWithArg(opt, arg) => write!(f, "-{} {}", opt, arg) + Self::LongWithArg(opt, arg) => write!(f, "--{} {}", opt, arg), + Self::ShortWithArg(opt, arg) => write!(f, "-{} {}", opt, arg), } } } @@ -82,32 +82,33 @@ pub fn get_opts_from_tokens(tokens: Vec, opt_specs: &[OptSpec]) -> (Vec, if parsed_opts.is_empty() { non_opts.push(token) } else { - for opt in parsed_opts { - let mut pushed = false; - for opt_spec in opt_specs { - if opt_spec.opt == opt { - if opt_spec.takes_arg { - let arg = tokens_iter.next() - .map(|t| t.to_string()) - .unwrap_or_default(); + for opt in parsed_opts { + let mut pushed = false; + for opt_spec in opt_specs { + if opt_spec.opt == opt { + if opt_spec.takes_arg { + let arg = tokens_iter + .next() + .map(|t| t.to_string()) + .unwrap_or_default(); - let opt = match opt { - Opt::Long(ref opt) => Opt::LongWithArg(opt.to_string(), arg), - Opt::Short(opt) => Opt::ShortWithArg(opt, arg), - _ => unreachable!(), - }; - opts.push(opt); - pushed = true; - } else { - opts.push(opt.clone()); - pushed = true; - } - } - } - if !pushed { - non_opts.push(token.clone()); - } - } + let opt = match opt { + Opt::Long(ref opt) => Opt::LongWithArg(opt.to_string(), arg), + Opt::Short(opt) => Opt::ShortWithArg(opt, arg), + _ => unreachable!(), + }; + opts.push(opt); + pushed = true; + } else { + opts.push(opt.clone()); + pushed = true; + } + } + } + if !pushed { + non_opts.push(token.clone()); + } + } } } (non_opts, opts) diff --git a/src/jobs.rs b/src/jobs.rs index 007d4be..061be3c 100644 --- a/src/jobs.rs +++ b/src/jobs.rs @@ -2,7 +2,11 @@ use crate::{ libsh::{ error::ShResult, term::{Style, Styled}, - }, prelude::*, procio::{IoMode, borrow_fd}, signal::{disable_reaping, enable_reaping}, state::{self, set_status, read_jobs, write_jobs} + }, + prelude::*, + procio::{IoMode, borrow_fd}, + signal::{disable_reaping, enable_reaping}, + state::{self, read_jobs, set_status, write_jobs}, }; pub const SIG_EXIT_OFFSET: i32 = 128; @@ -685,7 +689,9 @@ pub fn wait_fg(job: Job) -> ShResult<()> { } // If job wasn't stopped (moved to bg), clear the fg slot if !was_stopped { - write_jobs(|j| { j.take_fg(); }); + write_jobs(|j| { + j.take_fg(); + }); } take_term()?; set_status(code); diff --git a/src/libsh/error.rs b/src/libsh/error.rs index bc4026e..8cd49f3 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -408,12 +408,12 @@ pub enum ShErrKind { ReadlineIntr(String), ReadlineErr, - // Not really errors, more like internal signals + // Not really errors, more like internal signals CleanExit(i32), FuncReturn(i32), LoopContinue(i32), LoopBreak(i32), - ClearReadline, + ClearReadline, Null, } @@ -437,7 +437,7 @@ impl Display for ShErrKind { Self::LoopBreak(_) => "", Self::ReadlineIntr(_) => "", Self::ReadlineErr => "Readline Error", - Self::ClearReadline => "", + Self::ClearReadline => "", Self::Null => "", }; write!(f, "{output}") diff --git a/src/libsh/sys.rs b/src/libsh/sys.rs index e33c6e4..8900159 100644 --- a/src/libsh/sys.rs +++ b/src/libsh/sys.rs @@ -1,6 +1,6 @@ use termios::{LocalFlags, Termios}; -use crate::{prelude::*}; +use crate::prelude::*; /// /// The previous state of the terminal options. /// @@ -33,44 +33,43 @@ pub(crate) static mut SAVED_TERMIOS: Option> = None; #[derive(Debug)] pub struct TermiosGuard { - saved_termios: Option + saved_termios: Option, } impl TermiosGuard { - pub fn new(new_termios: Termios) -> Self { - let mut new = Self { saved_termios: None }; + pub fn new(new_termios: Termios) -> Self { + let mut new = Self { + saved_termios: None, + }; - if isatty(std::io::stdin().as_raw_fd()).unwrap() { - let current_termios = termios::tcgetattr(std::io::stdin()).unwrap(); - new.saved_termios = Some(current_termios); + if isatty(std::io::stdin().as_raw_fd()).unwrap() { + let current_termios = termios::tcgetattr(std::io::stdin()).unwrap(); + new.saved_termios = Some(current_termios); - termios::tcsetattr( - std::io::stdin(), - nix::sys::termios::SetArg::TCSANOW, - &new_termios, - ).unwrap(); - } + termios::tcsetattr( + std::io::stdin(), + nix::sys::termios::SetArg::TCSANOW, + &new_termios, + ) + .unwrap(); + } - new - } + new + } } impl Default for TermiosGuard { - fn default() -> Self { - let mut termios = termios::tcgetattr(std::io::stdin()).unwrap(); - termios.local_flags &= !LocalFlags::ECHOCTL; - Self::new(termios) - } + fn default() -> Self { + let mut termios = termios::tcgetattr(std::io::stdin()).unwrap(); + termios.local_flags &= !LocalFlags::ECHOCTL; + Self::new(termios) + } } impl Drop for TermiosGuard { - fn drop(&mut self) { - if let Some(saved) = &self.saved_termios { - termios::tcsetattr( - std::io::stdin(), - nix::sys::termios::SetArg::TCSANOW, - saved, - ).unwrap(); - } - } + fn drop(&mut self) { + if let Some(saved) = &self.saved_termios { + termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, saved).unwrap(); + } + } } diff --git a/src/libsh/utils.rs b/src/libsh/utils.rs index 5e602ab..d77c221 100644 --- a/src/libsh/utils.rs +++ b/src/libsh/utils.rs @@ -83,8 +83,7 @@ impl TkVecUtils for Vec { } } fn debug_tokens(&self) { - for token in self { - } + for token in self {} } } diff --git a/src/main.rs b/src/main.rs index edd18eb..495bed0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ #![allow( - clippy::derivable_impls, - clippy::tabs_in_doc_comments, - clippy::while_let_on_iterator + clippy::derivable_impls, + clippy::tabs_in_doc_comments, + clippy::while_let_on_iterator )] pub mod builtin; pub mod expand; @@ -22,10 +22,10 @@ use std::os::fd::BorrowedFd; use std::process::ExitCode; use std::sync::atomic::Ordering; +use nix::errno::Errno; use nix::libc::STDIN_FILENO; use nix::poll::{PollFd, PollFlags, PollTimeout, poll}; use nix::unistd::read; -use nix::errno::Errno; use crate::builtin::trap::TrapTarget; use crate::libsh::error::{ShErr, ShErrKind, ShResult}; @@ -41,16 +41,16 @@ use state::{read_vars, write_vars}; #[derive(Parser, Debug)] struct FernArgs { - script: Option, + script: Option, - #[arg(short)] - command: Option, + #[arg(short)] + command: Option, - #[arg(trailing_var_arg = true)] - script_args: Vec, + #[arg(trailing_var_arg = true)] + script_args: Vec, - #[arg(long)] - version: bool, + #[arg(long)] + version: bool, } /// Force evaluation of lazily-initialized values early in shell startup. @@ -64,178 +64,192 @@ struct FernArgs { /// closure, which forces access to the variable table and causes its `LazyLock` /// constructor to run. fn kickstart_lazy_evals() { - read_vars(|_| {}); + read_vars(|_| {}); } fn main() -> ExitCode { - env_logger::init(); - kickstart_lazy_evals(); - let args = FernArgs::parse(); - if args.version { - println!("fern {}", env!("CARGO_PKG_VERSION")); - return ExitCode::SUCCESS; - } + env_logger::init(); + kickstart_lazy_evals(); + let args = FernArgs::parse(); + if args.version { + println!("fern {}", env!("CARGO_PKG_VERSION")); + return ExitCode::SUCCESS; + } - if let Err(e) = if let Some(path) = args.script { - run_script(path, args.script_args) - } else if let Some(cmd) = args.command { - exec_input(cmd, None, false) - } else { - fern_interactive() - } { - eprintln!("fern: {e}"); - }; + if let Err(e) = if let Some(path) = args.script { + run_script(path, args.script_args) + } else if let Some(cmd) = args.command { + exec_input(cmd, None, false) + } else { + fern_interactive() + } { + eprintln!("fern: {e}"); + }; - if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit)) - && let Err(e) = exec_input(trap, None, false) { - eprintln!("fern: error running EXIT trap: {e}"); - } + if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit)) + && let Err(e) = exec_input(trap, None, false) + { + eprintln!("fern: error running EXIT trap: {e}"); + } - ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8) + ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8) } fn run_script>(path: P, args: Vec) -> ShResult<()> { - let path = path.as_ref(); - if !path.is_file() { - eprintln!("fern: Failed to open input file: {}", path.display()); - QUIT_CODE.store(1, Ordering::SeqCst); - return Err(ShErr::simple(ShErrKind::CleanExit(1), "input file not found")); - } - let Ok(input) = fs::read_to_string(path) else { - eprintln!("fern: Failed to read input file: {}", path.display()); - QUIT_CODE.store(1, Ordering::SeqCst); - return Err(ShErr::simple(ShErrKind::CleanExit(1), "failed to read input file")); - }; + let path = path.as_ref(); + if !path.is_file() { + eprintln!("fern: Failed to open input file: {}", path.display()); + QUIT_CODE.store(1, Ordering::SeqCst); + return Err(ShErr::simple( + ShErrKind::CleanExit(1), + "input file not found", + )); + } + let Ok(input) = fs::read_to_string(path) else { + eprintln!("fern: Failed to read input file: {}", path.display()); + QUIT_CODE.store(1, Ordering::SeqCst); + return Err(ShErr::simple( + ShErrKind::CleanExit(1), + "failed to read input file", + )); + }; - write_vars(|v| v.cur_scope_mut().bpush_arg(path.to_string_lossy().to_string())); - for arg in args { - write_vars(|v| v.cur_scope_mut().bpush_arg(arg)) - } + write_vars(|v| { + v.cur_scope_mut() + .bpush_arg(path.to_string_lossy().to_string()) + }); + for arg in args { + write_vars(|v| v.cur_scope_mut().bpush_arg(arg)) + } - exec_input(input, None, false) + exec_input(input, None, false) } fn fern_interactive() -> ShResult<()> { - let _raw_mode = raw_mode(); // sets raw mode, restores termios on drop - sig_setup(); + let _raw_mode = raw_mode(); // sets raw mode, restores termios on drop + sig_setup(); - if let Err(e) = source_rc() { - eprintln!("{e}"); - } + if let Err(e) = source_rc() { + eprintln!("{e}"); + } - // Create readline instance with initial prompt - let mut readline = match FernVi::new(get_prompt().ok()) { - Ok(rl) => rl, - Err(e) => { - eprintln!("Failed to initialize readline: {e}"); - QUIT_CODE.store(1, Ordering::SeqCst); - return Err(ShErr::simple(ShErrKind::CleanExit(1), "readline initialization failed")); - } - }; + // Create readline instance with initial prompt + let mut readline = match FernVi::new(get_prompt().ok()) { + Ok(rl) => rl, + Err(e) => { + eprintln!("Failed to initialize readline: {e}"); + QUIT_CODE.store(1, Ordering::SeqCst); + return Err(ShErr::simple( + ShErrKind::CleanExit(1), + "readline initialization failed", + )); + } + }; - // Main poll loop - loop { - // Handle any pending signals - while signals_pending() { - if let Err(e) = check_signals() { - match e.kind() { - ShErrKind::ClearReadline => { - // Ctrl+C - clear current input and show new prompt - readline.reset(get_prompt().ok()); - } - ShErrKind::CleanExit(code) => { - QUIT_CODE.store(*code, Ordering::SeqCst); - return Ok(()); - } - _ => eprintln!("{e}"), - } - } - } + // Main poll loop + loop { + // Handle any pending signals + while signals_pending() { + if let Err(e) = check_signals() { + match e.kind() { + ShErrKind::ClearReadline => { + // Ctrl+C - clear current input and show new prompt + readline.reset(get_prompt().ok()); + } + ShErrKind::CleanExit(code) => { + QUIT_CODE.store(*code, Ordering::SeqCst); + return Ok(()); + } + _ => eprintln!("{e}"), + } + } + } - readline.print_line()?; + readline.print_line()?; - // Poll for stdin input - let mut fds = [PollFd::new( - unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }, - PollFlags::POLLIN, - )]; + // Poll for stdin input + let mut fds = [PollFd::new( + unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }, + PollFlags::POLLIN, + )]; - match poll(&mut fds, PollTimeout::MAX) { - Ok(_) => {} - Err(Errno::EINTR) => { - // Interrupted by signal, loop back to handle it - continue; - } - Err(e) => { - eprintln!("poll error: {e}"); - break; - } - } + match poll(&mut fds, PollTimeout::MAX) { + Ok(_) => {} + Err(Errno::EINTR) => { + // Interrupted by signal, loop back to handle it + continue; + } + Err(e) => { + eprintln!("poll error: {e}"); + break; + } + } - // Check if stdin has data - if fds[0].revents().is_some_and(|r| r.contains(PollFlags::POLLIN)) { - let mut buffer = [0u8; 1024]; - match read(STDIN_FILENO, &mut buffer) { - Ok(0) => { - // EOF - break; - } - Ok(n) => { - readline.feed_bytes(&buffer[..n]); - } - Err(Errno::EINTR) => { - // Interrupted, continue to handle signals - continue; - } - Err(e) => { - eprintln!("read error: {e}"); - break; - } - } - } + // Check if stdin has data + if fds[0] + .revents() + .is_some_and(|r| r.contains(PollFlags::POLLIN)) + { + let mut buffer = [0u8; 1024]; + match read(STDIN_FILENO, &mut buffer) { + Ok(0) => { + // EOF + break; + } + Ok(n) => { + readline.feed_bytes(&buffer[..n]); + } + Err(Errno::EINTR) => { + // Interrupted, continue to handle signals + continue; + } + Err(e) => { + eprintln!("read error: {e}"); + break; + } + } + } - // Process any available input - match readline.process_input() { - Ok(ReadlineEvent::Line(input)) => { - let start = Instant::now(); - write_meta(|m| m.start_timer()); - if let Err(e) = exec_input(input, None, true) { - match e.kind() { - ShErrKind::CleanExit(code) => { - QUIT_CODE.store(*code, Ordering::SeqCst); - return Ok(()); - } - _ => eprintln!("{e}"), - } - } - let command_run_time = start.elapsed(); - log::info!("Command executed in {:.2?}", command_run_time); - write_meta(|m| m.stop_timer()); + // Process any available input + match readline.process_input() { + Ok(ReadlineEvent::Line(input)) => { + let start = Instant::now(); + write_meta(|m| m.start_timer()); + if let Err(e) = exec_input(input, None, true) { + match e.kind() { + ShErrKind::CleanExit(code) => { + QUIT_CODE.store(*code, Ordering::SeqCst); + return Ok(()); + } + _ => eprintln!("{e}"), + } + } + let command_run_time = start.elapsed(); + log::info!("Command executed in {:.2?}", command_run_time); + write_meta(|m| m.stop_timer()); - // Reset for next command with fresh prompt - readline.reset(get_prompt().ok()); - let real_end = start.elapsed(); - log::info!("Total round trip time: {:.2?}", real_end); - } - Ok(ReadlineEvent::Eof) => { - // Ctrl+D on empty line - QUIT_CODE.store(0, Ordering::SeqCst); - return Ok(()); - } - Ok(ReadlineEvent::Pending) => { - // No complete input yet, keep polling - } - Err(e) => { - match e.kind() { - ShErrKind::CleanExit(code) => { - QUIT_CODE.store(*code, Ordering::SeqCst); - return Ok(()); - } - _ => eprintln!("{e}"), - } - } - } - } + // Reset for next command with fresh prompt + readline.reset(get_prompt().ok()); + let real_end = start.elapsed(); + log::info!("Total round trip time: {:.2?}", real_end); + } + Ok(ReadlineEvent::Eof) => { + // Ctrl+D on empty line + QUIT_CODE.store(0, Ordering::SeqCst); + return Ok(()); + } + Ok(ReadlineEvent::Pending) => { + // No complete input yet, keep polling + } + Err(e) => match e.kind() { + ShErrKind::CleanExit(code) => { + QUIT_CODE.store(*code, Ordering::SeqCst); + return Ok(()); + } + _ => eprintln!("{e}"), + }, + } + } - Ok(()) + Ok(()) } diff --git a/src/parse/execute.rs b/src/parse/execute.rs index c868e3a..af71201 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -1,750 +1,749 @@ use std::collections::{HashSet, VecDeque}; use crate::{ - builtin::{ - alias::{alias, unalias}, cd::cd, echo::echo, export::export, flowctl::flowctl, jobctl::{JobBehavior, continue_job, jobs}, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, zoltraak::zoltraak - }, - expand::expand_aliases, - jobs::{ChildProc, JobStack, dispatch_job}, - libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, - prelude::*, - procio::{IoMode, IoStack}, - state::{ - self, ShFunc, VarFlags, read_logic, write_logic, write_vars - }, + builtin::{ + alias::{alias, unalias}, + cd::cd, + echo::echo, + export::export, + flowctl::flowctl, + jobctl::{JobBehavior, continue_job, jobs}, + pwd::pwd, + read::read_builtin, + shift::shift, + shopt::shopt, + source::source, + test::double_bracket_test, + trap::{TrapTarget, trap}, + zoltraak::zoltraak, + }, + expand::expand_aliases, + jobs::{ChildProc, JobStack, dispatch_job}, + libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, + prelude::*, + procio::{IoMode, IoStack}, + state::{self, ShFunc, VarFlags, read_logic, write_logic, write_vars}, }; use super::{ - lex::{Span, Tk, TkFlags, KEYWORDS}, - AssignKind, CaseNode, CondNode, ConjunctNode, ConjunctOp, LoopKind, NdFlags, NdRule, Node, - ParsedSrc, Redir, RedirType, + AssignKind, CaseNode, CondNode, ConjunctNode, ConjunctOp, LoopKind, NdFlags, NdRule, Node, + ParsedSrc, Redir, RedirType, + lex::{KEYWORDS, Span, Tk, TkFlags}, }; thread_local! { - static RECURSE_DEPTH: std::cell::Cell = const { std::cell::Cell::new(0) }; + static RECURSE_DEPTH: std::cell::Cell = const { std::cell::Cell::new(0) }; } pub struct ScopeGuard; - impl ScopeGuard { - pub fn exclusive_scope(args: Option>) -> Self { - let argv = args.map(|a| a.into_iter().map(|(s, _)| s).collect::>()); - write_vars(|v| v.descend(argv)); - Self - } - pub fn shared_scope() -> Self { - // used in environments that inherit from the parent, like subshells - write_vars(|v| v.descend(None)); - Self - } + pub fn exclusive_scope(args: Option>) -> Self { + let argv = args.map(|a| a.into_iter().map(|(s, _)| s).collect::>()); + write_vars(|v| v.descend(argv)); + Self + } + pub fn shared_scope() -> Self { + // used in environments that inherit from the parent, like subshells + write_vars(|v| v.descend(None)); + Self + } } impl Drop for ScopeGuard { - fn drop(&mut self) { - write_vars(|v| v.ascend()); - } + fn drop(&mut self) { + write_vars(|v| v.ascend()); + } } /// Used to throw away variables that exist in temporary contexts /// such as 'VAR=value ' /// or for-loop variables struct VarCtxGuard { - vars: HashSet + vars: HashSet, } impl VarCtxGuard { - fn new(vars: HashSet) -> Self { - Self { vars } - } + fn new(vars: HashSet) -> Self { + Self { vars } + } } impl Drop for VarCtxGuard { - fn drop(&mut self) { - write_vars(|v| { - for var in &self.vars { - v.unset_var(var); - } - }); - } + fn drop(&mut self) { + write_vars(|v| { + for var in &self.vars { + v.unset_var(var); + } + }); + } } pub enum AssignBehavior { - Export, - Set, + Export, + Set, } /// Arguments to the execvpe function pub struct ExecArgs { - pub cmd: (CString, Span), - pub argv: Vec, - pub envp: Vec, + pub cmd: (CString, Span), + pub argv: Vec, + pub envp: Vec, } impl ExecArgs { - pub fn new(argv: Vec) -> ShResult { - assert!(!argv.is_empty()); - let argv = prepare_argv(argv)?; - let cmd = Self::get_cmd(&argv); - let argv = Self::get_argv(argv); - let envp = Self::get_envp(); - Ok(Self { cmd, argv, envp }) - } - pub fn get_cmd(argv: &[(String, Span)]) -> (CString, Span) { - let cmd = argv[0].0.as_str(); - let span = argv[0].1.clone(); - (CString::new(cmd).unwrap(), span) - } - pub fn get_argv(argv: Vec<(String, Span)>) -> Vec { - argv - .into_iter() - .map(|s| CString::new(s.0).unwrap()) - .collect() - } - pub fn get_envp() -> Vec { - std::env::vars() - .map(|v| CString::new(format!("{}={}", v.0, v.1)).unwrap()) - .collect() - } + pub fn new(argv: Vec) -> ShResult { + assert!(!argv.is_empty()); + let argv = prepare_argv(argv)?; + let cmd = Self::get_cmd(&argv); + let argv = Self::get_argv(argv); + let envp = Self::get_envp(); + Ok(Self { cmd, argv, envp }) + } + pub fn get_cmd(argv: &[(String, Span)]) -> (CString, Span) { + let cmd = argv[0].0.as_str(); + let span = argv[0].1.clone(); + (CString::new(cmd).unwrap(), span) + } + pub fn get_argv(argv: Vec<(String, Span)>) -> Vec { + argv + .into_iter() + .map(|s| CString::new(s.0).unwrap()) + .collect() + } + pub fn get_envp() -> Vec { + std::env::vars() + .map(|v| CString::new(format!("{}={}", v.0, v.1)).unwrap()) + .collect() + } } pub fn exec_input(input: String, io_stack: Option, interactive: bool) -> ShResult<()> { - let log_tab = read_logic(|l| l.clone()); - let input = expand_aliases(input, HashSet::new(), &log_tab); - let lex_flags = if interactive { - super::lex::LexFlags::INTERACTIVE - } else { - super::lex::LexFlags::empty() - }; - let mut parser = ParsedSrc::new(Arc::new(input)).with_lex_flags(lex_flags); - if let Err(errors) = parser.parse_src() { - for error in errors { - eprintln!("{error}"); - } - return Ok(()); - } + let log_tab = read_logic(|l| l.clone()); + let input = expand_aliases(input, HashSet::new(), &log_tab); + let lex_flags = if interactive { + super::lex::LexFlags::INTERACTIVE + } else { + super::lex::LexFlags::empty() + }; + let mut parser = ParsedSrc::new(Arc::new(input)).with_lex_flags(lex_flags); + if let Err(errors) = parser.parse_src() { + for error in errors { + eprintln!("{error}"); + } + return Ok(()); + } - let mut dispatcher = Dispatcher::new(parser.extract_nodes(), interactive); - if let Some(mut stack) = io_stack { - dispatcher.io_stack.extend(stack.drain(..)); - } - let result = dispatcher.begin_dispatch(); + let mut dispatcher = Dispatcher::new(parser.extract_nodes(), interactive); + if let Some(mut stack) = io_stack { + dispatcher.io_stack.extend(stack.drain(..)); + } + let result = dispatcher.begin_dispatch(); - if state::get_status() != 0 - && let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Error)) { - let saved_status = state::get_status(); - exec_input(trap, None, false)?; - state::set_status(saved_status); - } + if state::get_status() != 0 + && let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Error)) + { + let saved_status = state::get_status(); + exec_input(trap, None, false)?; + state::set_status(saved_status); + } - result + result } pub struct Dispatcher { - nodes: VecDeque, - interactive: bool, - pub io_stack: IoStack, - pub job_stack: JobStack, + nodes: VecDeque, + interactive: bool, + pub io_stack: IoStack, + pub job_stack: JobStack, } impl Dispatcher { - pub fn new(nodes: Vec, interactive: bool) -> Self { - let nodes = VecDeque::from(nodes); - Self { - nodes, - interactive, - io_stack: IoStack::new(), - job_stack: JobStack::new(), - } - } - pub fn begin_dispatch(&mut self) -> ShResult<()> { - while let Some(node) = self.nodes.pop_front() { - let blame = node.get_span(); - self.dispatch_node(node).try_blame(blame)?; - } - Ok(()) - } - pub fn dispatch_node(&mut self, node: Node) -> ShResult<()> { - match node.class { - NdRule::Conjunction { .. } => self.exec_conjunction(node)?, - NdRule::Pipeline { .. } => self.exec_pipeline(node)?, - NdRule::IfNode { .. } => self.exec_if(node)?, - NdRule::LoopNode { .. } => self.exec_loop(node)?, - NdRule::ForNode { .. } => self.exec_for(node)?, - NdRule::CaseNode { .. } => self.exec_case(node)?, - NdRule::BraceGrp { .. } => self.exec_brc_grp(node)?, - NdRule::FuncDef { .. } => self.exec_func_def(node)?, - NdRule::Command { .. } => self.dispatch_cmd(node)?, - NdRule::Test { .. } => self.exec_test(node)?, - _ => unreachable!(), - } - Ok(()) - } - pub fn dispatch_cmd(&mut self, node: Node) -> ShResult<()> { - let Some(cmd) = node.get_command() else { - return self.exec_cmd(node); // Argv is empty, probably an assignment - }; - if is_func(node.get_command().cloned()) { - self.exec_func(node) - } else if cmd.flags.contains(TkFlags::BUILTIN) { - self.exec_builtin(node) - } else if is_subsh(node.get_command().cloned()) { - self.exec_subsh(node) - } else if crate::state::read_shopts(|s| s.core.autocd) && Path::new(cmd.span.as_str()).is_dir() { - let dir = cmd.span.as_str().to_string(); - let stack = IoStack { stack: self.io_stack.clone() }; - exec_input(format!("cd {dir}"), Some(stack), self.interactive) - } else { - self.exec_cmd(node) - } - } - pub fn exec_conjunction(&mut self, conjunction: Node) -> ShResult<()> { - let NdRule::Conjunction { elements } = conjunction.class else { - unreachable!() - }; + pub fn new(nodes: Vec, interactive: bool) -> Self { + let nodes = VecDeque::from(nodes); + Self { + nodes, + interactive, + io_stack: IoStack::new(), + job_stack: JobStack::new(), + } + } + pub fn begin_dispatch(&mut self) -> ShResult<()> { + while let Some(node) = self.nodes.pop_front() { + let blame = node.get_span(); + self.dispatch_node(node).try_blame(blame)?; + } + Ok(()) + } + pub fn dispatch_node(&mut self, node: Node) -> ShResult<()> { + match node.class { + NdRule::Conjunction { .. } => self.exec_conjunction(node)?, + NdRule::Pipeline { .. } => self.exec_pipeline(node)?, + NdRule::IfNode { .. } => self.exec_if(node)?, + NdRule::LoopNode { .. } => self.exec_loop(node)?, + NdRule::ForNode { .. } => self.exec_for(node)?, + NdRule::CaseNode { .. } => self.exec_case(node)?, + NdRule::BraceGrp { .. } => self.exec_brc_grp(node)?, + NdRule::FuncDef { .. } => self.exec_func_def(node)?, + NdRule::Command { .. } => self.dispatch_cmd(node)?, + NdRule::Test { .. } => self.exec_test(node)?, + _ => unreachable!(), + } + Ok(()) + } + pub fn dispatch_cmd(&mut self, node: Node) -> ShResult<()> { + let Some(cmd) = node.get_command() else { + return self.exec_cmd(node); // Argv is empty, probably an assignment + }; + if is_func(node.get_command().cloned()) { + self.exec_func(node) + } else if cmd.flags.contains(TkFlags::BUILTIN) { + self.exec_builtin(node) + } else if is_subsh(node.get_command().cloned()) { + self.exec_subsh(node) + } else if crate::state::read_shopts(|s| s.core.autocd) && Path::new(cmd.span.as_str()).is_dir() + { + let dir = cmd.span.as_str().to_string(); + let stack = IoStack { + stack: self.io_stack.clone(), + }; + exec_input(format!("cd {dir}"), Some(stack), self.interactive) + } else { + self.exec_cmd(node) + } + } + pub fn exec_conjunction(&mut self, conjunction: Node) -> ShResult<()> { + let NdRule::Conjunction { elements } = conjunction.class else { + unreachable!() + }; - let mut elem_iter = elements.into_iter(); - while let Some(element) = elem_iter.next() { - let ConjunctNode { cmd, operator } = element; - self.dispatch_node(*cmd)?; + let mut elem_iter = elements.into_iter(); + while let Some(element) = elem_iter.next() { + let ConjunctNode { cmd, operator } = element; + self.dispatch_node(*cmd)?; - let status = state::get_status(); - match operator { - ConjunctOp::And => { - if status != 0 { - break; - } - } - ConjunctOp::Or => { - if status == 0 { - break; - } - } - ConjunctOp::Null => break, - } - } - Ok(()) - } - pub fn exec_test(&mut self, node: Node) -> ShResult<()> { - let test_result = double_bracket_test(node)?; - match test_result { - true => state::set_status(0), - false => state::set_status(1), - } - Ok(()) - } - pub fn exec_func_def(&mut self, func_def: Node) -> ShResult<()> { - let blame = func_def.get_span(); - let NdRule::FuncDef { name, body } = func_def.class else { - unreachable!() - }; - let body_span = body.get_span(); - let body = body_span.as_str().to_string(); - let name = name.span.as_str().strip_suffix("()").unwrap(); + let status = state::get_status(); + match operator { + ConjunctOp::And => { + if status != 0 { + break; + } + } + ConjunctOp::Or => { + if status == 0 { + break; + } + } + ConjunctOp::Null => break, + } + } + Ok(()) + } + pub fn exec_test(&mut self, node: Node) -> ShResult<()> { + let test_result = double_bracket_test(node)?; + match test_result { + true => state::set_status(0), + false => state::set_status(1), + } + Ok(()) + } + pub fn exec_func_def(&mut self, func_def: Node) -> ShResult<()> { + let blame = func_def.get_span(); + let NdRule::FuncDef { name, body } = func_def.class else { + unreachable!() + }; + let body_span = body.get_span(); + let body = body_span.as_str().to_string(); + let name = name.span.as_str().strip_suffix("()").unwrap(); - if KEYWORDS.contains(&name) { - return Err(ShErr::full( - ShErrKind::SyntaxErr, - format!("function: Forbidden function name `{name}`"), - blame, - )); - } + if KEYWORDS.contains(&name) { + return Err(ShErr::full( + ShErrKind::SyntaxErr, + format!("function: Forbidden function name `{name}`"), + blame, + )); + } - let mut func_parser = ParsedSrc::new(Arc::new(body)); - if let Err(errors) = func_parser.parse_src() { - for error in errors { - eprintln!("{error}"); - } - return Ok(()); - } + let mut func_parser = ParsedSrc::new(Arc::new(body)); + if let Err(errors) = func_parser.parse_src() { + for error in errors { + eprintln!("{error}"); + } + return Ok(()); + } - let func = ShFunc::new(func_parser); - write_logic(|l| l.insert_func(name, func)); // Store the AST - Ok(()) - } - fn exec_subsh(&mut self, subsh: Node) -> ShResult<()> { - let NdRule::Command { assignments, argv } = subsh.class else { - unreachable!() - }; + let func = ShFunc::new(func_parser); + write_logic(|l| l.insert_func(name, func)); // Store the AST + Ok(()) + } + fn exec_subsh(&mut self, subsh: Node) -> ShResult<()> { + let NdRule::Command { assignments, argv } = subsh.class else { + unreachable!() + }; - let env_vars = self.set_assignments(assignments, AssignBehavior::Export)?; - let _var_guard = VarCtxGuard::new(env_vars.into_iter().collect()); - self.io_stack.append_to_frame(subsh.redirs); - let mut argv = prepare_argv(argv)?; + let env_vars = self.set_assignments(assignments, AssignBehavior::Export)?; + let _var_guard = VarCtxGuard::new(env_vars.into_iter().collect()); + self.io_stack.append_to_frame(subsh.redirs); + let mut argv = prepare_argv(argv)?; - let subsh = argv.remove(0); - let subsh_body = subsh.0.to_string(); - let _guard = ScopeGuard::shared_scope(); + let subsh = argv.remove(0); + let subsh_body = subsh.0.to_string(); + let _guard = ScopeGuard::shared_scope(); - exec_input(subsh_body, None, self.interactive)?; + exec_input(subsh_body, None, self.interactive)?; - Ok(()) - } - fn exec_func(&mut self, func: Node) -> ShResult<()> { - let blame = func.get_span().clone(); - let NdRule::Command { - assignments, - mut argv, - } = func.class - else { - unreachable!() - }; + Ok(()) + } + fn exec_func(&mut self, func: Node) -> ShResult<()> { + let blame = func.get_span().clone(); + let NdRule::Command { + assignments, + mut argv, + } = func.class + else { + unreachable!() + }; - let max_depth = crate::state::read_shopts(|s| s.core.max_recurse_depth); - let depth = RECURSE_DEPTH.with(|d| { - let cur = d.get(); - d.set(cur + 1); - cur + 1 - }); - if depth > max_depth { - RECURSE_DEPTH.with(|d| d.set(d.get() - 1)); - return Err(ShErr::full( - ShErrKind::InternalErr, - format!("maximum recursion depth ({max_depth}) exceeded"), - blame, - )); - } + let max_depth = crate::state::read_shopts(|s| s.core.max_recurse_depth); + let depth = RECURSE_DEPTH.with(|d| { + let cur = d.get(); + d.set(cur + 1); + cur + 1 + }); + if depth > max_depth { + RECURSE_DEPTH.with(|d| d.set(d.get() - 1)); + return Err(ShErr::full( + ShErrKind::InternalErr, + format!("maximum recursion depth ({max_depth}) exceeded"), + blame, + )); + } - let env_vars = self.set_assignments(assignments, AssignBehavior::Export)?; - let _var_guard = VarCtxGuard::new(env_vars.into_iter().collect()); + let env_vars = self.set_assignments(assignments, AssignBehavior::Export)?; + let _var_guard = VarCtxGuard::new(env_vars.into_iter().collect()); - self.io_stack.append_to_frame(func.redirs); + self.io_stack.append_to_frame(func.redirs); - let func_name = argv.remove(0).span.as_str().to_string(); - let argv = prepare_argv(argv)?; - let result = if let Some(func) = read_logic(|l| l.get_func(&func_name)) { - let _guard = ScopeGuard::exclusive_scope(Some(argv)); + let func_name = argv.remove(0).span.as_str().to_string(); + let argv = prepare_argv(argv)?; + let result = if let Some(func) = read_logic(|l| l.get_func(&func_name)) { + let _guard = ScopeGuard::exclusive_scope(Some(argv)); - if let Err(e) = self.exec_brc_grp((*func).clone()) { - match e.kind() { - ShErrKind::FuncReturn(code) => { - state::set_status(*code); - Ok(()) - } - _ => Err(e).blame(blame), - } - } else { - Ok(()) - } - } else { - Err(ShErr::full( - ShErrKind::InternalErr, - format!("Failed to find function '{}'", func_name), - blame, - )) - }; + if let Err(e) = self.exec_brc_grp((*func).clone()) { + match e.kind() { + ShErrKind::FuncReturn(code) => { + state::set_status(*code); + Ok(()) + } + _ => Err(e).blame(blame), + } + } else { + Ok(()) + } + } else { + Err(ShErr::full( + ShErrKind::InternalErr, + format!("Failed to find function '{}'", func_name), + blame, + )) + }; - RECURSE_DEPTH.with(|d| d.set(d.get() - 1)); - result - } - fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> { - let NdRule::BraceGrp { body } = brc_grp.class else { - unreachable!() - }; - self.io_stack.append_to_frame(brc_grp.redirs); - let _guard = self.io_stack - .pop_frame() - .redirect()?; + RECURSE_DEPTH.with(|d| d.set(d.get() - 1)); + result + } + fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> { + let NdRule::BraceGrp { body } = brc_grp.class else { + unreachable!() + }; + self.io_stack.append_to_frame(brc_grp.redirs); + let _guard = self.io_stack.pop_frame().redirect()?; - for node in body { - let blame = node.get_span(); - self.dispatch_node(node).try_blame(blame)?; - } + for node in body { + let blame = node.get_span(); + self.dispatch_node(node).try_blame(blame)?; + } - Ok(()) - } - fn exec_case(&mut self, case_stmt: Node) -> ShResult<()> { - let NdRule::CaseNode { - pattern, - case_blocks, - } = case_stmt.class - else { - unreachable!() - }; + Ok(()) + } + fn exec_case(&mut self, case_stmt: Node) -> ShResult<()> { + let NdRule::CaseNode { + pattern, + case_blocks, + } = case_stmt.class + else { + unreachable!() + }; - self.io_stack.append_to_frame(case_stmt.redirs); - let _guard = self.io_stack - .pop_frame() - .redirect()?; + self.io_stack.append_to_frame(case_stmt.redirs); + let _guard = self.io_stack.pop_frame().redirect()?; - let exp_pattern = pattern.clone().expand()?; - let pattern_raw = exp_pattern - .get_words() - .first() - .map(|s| s.to_string()) - .unwrap_or_default(); + let exp_pattern = pattern.clone().expand()?; + let pattern_raw = exp_pattern + .get_words() + .first() + .map(|s| s.to_string()) + .unwrap_or_default(); - 'outer: for block in case_blocks { - let CaseNode { pattern, body } = block; - let block_pattern_raw = pattern.span.as_str().trim_end_matches(')').trim(); - // Split at '|' to allow for multiple patterns like `foo|bar)` - let block_patterns = block_pattern_raw.split('|'); + 'outer: for block in case_blocks { + let CaseNode { pattern, body } = block; + let block_pattern_raw = pattern.span.as_str().trim_end_matches(')').trim(); + // Split at '|' to allow for multiple patterns like `foo|bar)` + let block_patterns = block_pattern_raw.split('|'); - for pattern in block_patterns { - if pattern_raw == pattern || pattern == "*" { - for node in &body { - self.dispatch_node(node.clone())?; - } - break 'outer; - } - } - } + for pattern in block_patterns { + if pattern_raw == pattern || pattern == "*" { + for node in &body { + self.dispatch_node(node.clone())?; + } + break 'outer; + } + } + } - Ok(()) - } - fn exec_loop(&mut self, loop_stmt: Node) -> ShResult<()> { - let NdRule::LoopNode { kind, cond_node } = loop_stmt.class else { - unreachable!(); - }; - let keep_going = |kind: LoopKind, status: i32| -> bool { - match kind { - LoopKind::While => status == 0, - LoopKind::Until => status != 0, - } - }; + Ok(()) + } + fn exec_loop(&mut self, loop_stmt: Node) -> ShResult<()> { + let NdRule::LoopNode { kind, cond_node } = loop_stmt.class else { + unreachable!(); + }; + let keep_going = |kind: LoopKind, status: i32| -> bool { + match kind { + LoopKind::While => status == 0, + LoopKind::Until => status != 0, + } + }; - self.io_stack.append_to_frame(loop_stmt.redirs); - let _guard = self.io_stack - .pop_frame() - .redirect()?; + self.io_stack.append_to_frame(loop_stmt.redirs); + let _guard = self.io_stack.pop_frame().redirect()?; - let CondNode { cond, body } = cond_node; - 'outer: loop { - if let Err(e) = self.dispatch_node(*cond.clone()) { - state::set_status(1); - return Err(e); - } + let CondNode { cond, body } = cond_node; + 'outer: loop { + if let Err(e) = self.dispatch_node(*cond.clone()) { + state::set_status(1); + return Err(e); + } - let status = state::get_status(); - if keep_going(kind, status) { - for node in &body { - if let Err(e) = self.dispatch_node(node.clone()) { - match e.kind() { - ShErrKind::LoopBreak(code) => { - state::set_status(*code); - break 'outer; - } - ShErrKind::LoopContinue(code) => { - state::set_status(*code); - continue 'outer; - } - _ => { - return Err(e); - } - } - } - } - } else { - break; - } - } + let status = state::get_status(); + if keep_going(kind, status) { + for node in &body { + if let Err(e) = self.dispatch_node(node.clone()) { + match e.kind() { + ShErrKind::LoopBreak(code) => { + state::set_status(*code); + break 'outer; + } + ShErrKind::LoopContinue(code) => { + state::set_status(*code); + continue 'outer; + } + _ => { + return Err(e); + } + } + } + } + } else { + break; + } + } - Ok(()) - } - fn exec_for(&mut self, for_stmt: Node) -> ShResult<()> { - let NdRule::ForNode { vars, arr, body } = for_stmt.class else { - unreachable!(); - }; + Ok(()) + } + fn exec_for(&mut self, for_stmt: Node) -> ShResult<()> { + let NdRule::ForNode { vars, arr, body } = for_stmt.class else { + unreachable!(); + }; - let to_expanded_strings = |tks: Vec| -> ShResult> { - Ok(tks.into_iter() - .map(|tk| tk.expand().map(|tk| tk.get_words())) - .collect::>>>()? - .into_iter() - .flatten() - .collect::>()) - }; + let to_expanded_strings = |tks: Vec| -> ShResult> { + Ok( + tks + .into_iter() + .map(|tk| tk.expand().map(|tk| tk.get_words())) + .collect::>>>()? + .into_iter() + .flatten() + .collect::>(), + ) + }; - // Expand all array variables - let arr: Vec = to_expanded_strings(arr)?; - let vars: Vec = to_expanded_strings(vars)?; + // Expand all array variables + let arr: Vec = to_expanded_strings(arr)?; + let vars: Vec = to_expanded_strings(vars)?; - let mut for_guard = VarCtxGuard::new( - vars.iter().map(|v| v.to_string()).collect() - ); + let mut for_guard = VarCtxGuard::new(vars.iter().map(|v| v.to_string()).collect()); - self.io_stack.append_to_frame(for_stmt.redirs); - let _guard = self.io_stack - .pop_frame() - .redirect()?; + self.io_stack.append_to_frame(for_stmt.redirs); + let _guard = self.io_stack.pop_frame().redirect()?; - 'outer: for chunk in arr.chunks(vars.len()) { - let empty = String::new(); - let chunk_iter = vars.iter().zip( - chunk.iter().chain(std::iter::repeat(&empty)), - ); + 'outer: for chunk in arr.chunks(vars.len()) { + let empty = String::new(); + let chunk_iter = vars + .iter() + .zip(chunk.iter().chain(std::iter::repeat(&empty))); - for (var, val) in chunk_iter { - write_vars(|v| v.set_var(&var.to_string(), &val.to_string(), VarFlags::NONE)); - for_guard.vars.insert(var.to_string()); - } + for (var, val) in chunk_iter { + write_vars(|v| v.set_var(&var.to_string(), &val.to_string(), VarFlags::NONE)); + for_guard.vars.insert(var.to_string()); + } - for node in body.clone() { - if let Err(e) = self.dispatch_node(node) { - match e.kind() { - ShErrKind::LoopBreak(code) => { - state::set_status(*code); - break 'outer; - } - ShErrKind::LoopContinue(code) => { - state::set_status(*code); - continue 'outer; - } - _ => return Err(e), - } - } - } - } + for node in body.clone() { + if let Err(e) = self.dispatch_node(node) { + match e.kind() { + ShErrKind::LoopBreak(code) => { + state::set_status(*code); + break 'outer; + } + ShErrKind::LoopContinue(code) => { + state::set_status(*code); + continue 'outer; + } + _ => return Err(e), + } + } + } + } - Ok(()) - } - fn exec_if(&mut self, if_stmt: Node) -> ShResult<()> { - let NdRule::IfNode { - cond_nodes, - else_block, - } = if_stmt.class - else { - unreachable!(); - }; + Ok(()) + } + fn exec_if(&mut self, if_stmt: Node) -> ShResult<()> { + let NdRule::IfNode { + cond_nodes, + else_block, + } = if_stmt.class + else { + unreachable!(); + }; - self.io_stack.append_to_frame(if_stmt.redirs); - let _guard = self.io_stack - .pop_frame() - .redirect()?; + self.io_stack.append_to_frame(if_stmt.redirs); + let _guard = self.io_stack.pop_frame().redirect()?; - let mut matched = false; - for node in cond_nodes { - let CondNode { cond, body } = node; + let mut matched = false; + for node in cond_nodes { + let CondNode { cond, body } = node; - if let Err(e) = self.dispatch_node(*cond) { - state::set_status(1); - return Err(e); - } + if let Err(e) = self.dispatch_node(*cond) { + state::set_status(1); + return Err(e); + } - match state::get_status() { - 0 => { - matched = true; - for body_node in body { - self.dispatch_node(body_node)?; - } - break; // Don't check remaining elif conditions - } - _ => continue, - } - } + match state::get_status() { + 0 => { + matched = true; + for body_node in body { + self.dispatch_node(body_node)?; + } + break; // Don't check remaining elif conditions + } + _ => continue, + } + } - if !matched && !else_block.is_empty() { - for node in else_block { - self.dispatch_node(node)?; - } - } + if !matched && !else_block.is_empty() { + for node in else_block { + self.dispatch_node(node)?; + } + } - Ok(()) - } - fn exec_pipeline(&mut self, pipeline: Node) -> ShResult<()> { - let NdRule::Pipeline { cmds, pipe_err: _ } = pipeline.class else { - unreachable!() - }; - self.job_stack.new_job(); - // Zip the commands and their respective pipes into an iterator - let pipes_and_cmds = get_pipe_stack(cmds.len()).into_iter().zip(cmds); + Ok(()) + } + fn exec_pipeline(&mut self, pipeline: Node) -> ShResult<()> { + let NdRule::Pipeline { cmds, pipe_err: _ } = pipeline.class else { + unreachable!() + }; + self.job_stack.new_job(); + // Zip the commands and their respective pipes into an iterator + let pipes_and_cmds = get_pipe_stack(cmds.len()).into_iter().zip(cmds); - for ((rpipe, wpipe), cmd) in pipes_and_cmds { - if let Some(pipe) = rpipe { - self.io_stack.push_to_frame(pipe); - } - if let Some(pipe) = wpipe { - self.io_stack.push_to_frame(pipe); - } - self.dispatch_node(cmd)?; - } - let job = self.job_stack.finalize_job().unwrap(); - let is_bg = pipeline.flags.contains(NdFlags::BACKGROUND); - dispatch_job(job, is_bg)?; - Ok(()) - } - fn exec_builtin(&mut self, mut cmd: Node) -> ShResult<()> { - let NdRule::Command { - assignments, - argv, - } = &mut cmd.class - else { - unreachable!() - }; - let env_vars = self.set_assignments(mem::take(assignments), AssignBehavior::Export)?; - let _var_guard = VarCtxGuard::new(env_vars.into_iter().collect()); + for ((rpipe, wpipe), cmd) in pipes_and_cmds { + if let Some(pipe) = rpipe { + self.io_stack.push_to_frame(pipe); + } + if let Some(pipe) = wpipe { + self.io_stack.push_to_frame(pipe); + } + self.dispatch_node(cmd)?; + } + let job = self.job_stack.finalize_job().unwrap(); + let is_bg = pipeline.flags.contains(NdFlags::BACKGROUND); + dispatch_job(job, is_bg)?; + Ok(()) + } + fn exec_builtin(&mut self, mut cmd: Node) -> ShResult<()> { + let NdRule::Command { assignments, argv } = &mut cmd.class else { + unreachable!() + }; + let env_vars = self.set_assignments(mem::take(assignments), AssignBehavior::Export)?; + let _var_guard = VarCtxGuard::new(env_vars.into_iter().collect()); - let cmd_raw = argv.first().unwrap(); - let curr_job_mut = self.job_stack.curr_job_mut().unwrap(); - let io_stack_mut = &mut self.io_stack; + let cmd_raw = argv.first().unwrap(); + let curr_job_mut = self.job_stack.curr_job_mut().unwrap(); + let io_stack_mut = &mut self.io_stack; - if cmd_raw.as_str() == "builtin" { - *argv = argv - .iter_mut() - .skip(1) - .map(|tk| tk.clone()) - .collect::>(); - return self.exec_builtin(cmd); - } else if cmd_raw.as_str() == "command" { - *argv = argv - .iter_mut() - .skip(1) - .map(|tk| tk.clone()) - .collect::>(); - return self.dispatch_cmd(cmd); - } + if cmd_raw.as_str() == "builtin" { + *argv = argv + .iter_mut() + .skip(1) + .map(|tk| tk.clone()) + .collect::>(); + return self.exec_builtin(cmd); + } else if cmd_raw.as_str() == "command" { + *argv = argv + .iter_mut() + .skip(1) + .map(|tk| tk.clone()) + .collect::>(); + return self.dispatch_cmd(cmd); + } - let result = match cmd_raw.span.as_str() { - "echo" => echo(cmd, io_stack_mut, curr_job_mut), - "cd" => cd(cmd, curr_job_mut), - "export" => export(cmd, io_stack_mut, curr_job_mut), - "pwd" => pwd(cmd, io_stack_mut, curr_job_mut), - "source" => source(cmd, curr_job_mut), - "shift" => shift(cmd, curr_job_mut), - "fg" => continue_job(cmd, curr_job_mut, JobBehavior::Foregound), - "bg" => continue_job(cmd, curr_job_mut, JobBehavior::Background), - "jobs" => jobs(cmd, io_stack_mut, curr_job_mut), - "alias" => alias(cmd, io_stack_mut, curr_job_mut), - "unalias" => unalias(cmd, io_stack_mut, curr_job_mut), - "return" => flowctl(cmd, ShErrKind::FuncReturn(0)), - "break" => flowctl(cmd, ShErrKind::LoopBreak(0)), - "continue" => flowctl(cmd, ShErrKind::LoopContinue(0)), - "exit" => flowctl(cmd, ShErrKind::CleanExit(0)), - "zoltraak" => zoltraak(cmd, io_stack_mut, curr_job_mut), - "shopt" => shopt(cmd, io_stack_mut, curr_job_mut), - "read" => read_builtin(cmd, io_stack_mut, curr_job_mut), - "trap" => trap(cmd, io_stack_mut, curr_job_mut), - _ => unimplemented!( - "Have not yet added support for builtin '{}'", - cmd_raw.span.as_str() - ), - }; + let result = match cmd_raw.span.as_str() { + "echo" => echo(cmd, io_stack_mut, curr_job_mut), + "cd" => cd(cmd, curr_job_mut), + "export" => export(cmd, io_stack_mut, curr_job_mut), + "pwd" => pwd(cmd, io_stack_mut, curr_job_mut), + "source" => source(cmd, curr_job_mut), + "shift" => shift(cmd, curr_job_mut), + "fg" => continue_job(cmd, curr_job_mut, JobBehavior::Foregound), + "bg" => continue_job(cmd, curr_job_mut, JobBehavior::Background), + "jobs" => jobs(cmd, io_stack_mut, curr_job_mut), + "alias" => alias(cmd, io_stack_mut, curr_job_mut), + "unalias" => unalias(cmd, io_stack_mut, curr_job_mut), + "return" => flowctl(cmd, ShErrKind::FuncReturn(0)), + "break" => flowctl(cmd, ShErrKind::LoopBreak(0)), + "continue" => flowctl(cmd, ShErrKind::LoopContinue(0)), + "exit" => flowctl(cmd, ShErrKind::CleanExit(0)), + "zoltraak" => zoltraak(cmd, io_stack_mut, curr_job_mut), + "shopt" => shopt(cmd, io_stack_mut, curr_job_mut), + "read" => read_builtin(cmd, io_stack_mut, curr_job_mut), + "trap" => trap(cmd, io_stack_mut, curr_job_mut), + _ => unimplemented!( + "Have not yet added support for builtin '{}'", + cmd_raw.span.as_str() + ), + }; - if let Err(e) = result { - state::set_status(1); - return Err(e); - } - Ok(()) - } - fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> { - let NdRule::Command { assignments, argv } = cmd.class else { - unreachable!() - }; - let mut env_vars_to_unset = vec![]; - if !assignments.is_empty() { - let assign_behavior = if argv.is_empty() { - AssignBehavior::Set - } else { - AssignBehavior::Export - }; - env_vars_to_unset = self.set_assignments(assignments, assign_behavior)?; - } + if let Err(e) = result { + state::set_status(1); + return Err(e); + } + Ok(()) + } + fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> { + let NdRule::Command { assignments, argv } = cmd.class else { + unreachable!() + }; + let mut env_vars_to_unset = vec![]; + if !assignments.is_empty() { + let assign_behavior = if argv.is_empty() { + AssignBehavior::Set + } else { + AssignBehavior::Export + }; + env_vars_to_unset = self.set_assignments(assignments, assign_behavior)?; + } - if argv.is_empty() { - return Ok(()); - } + if argv.is_empty() { + return Ok(()); + } - self.io_stack.append_to_frame(cmd.redirs); + self.io_stack.append_to_frame(cmd.redirs); - let exec_args = ExecArgs::new(argv)?; + let exec_args = ExecArgs::new(argv)?; - let _guard = self.io_stack - .pop_frame() - .redirect()?; + let _guard = self.io_stack.pop_frame().redirect()?; - let job = self.job_stack.curr_job_mut().unwrap(); + let job = self.job_stack.curr_job_mut().unwrap(); - match unsafe { fork()? } { - ForkResult::Child => { - let cmd = &exec_args.cmd.0; - let span = exec_args.cmd.1; + match unsafe { fork()? } { + ForkResult::Child => { + let cmd = &exec_args.cmd.0; + let span = exec_args.cmd.1; - let Err(e) = execvpe(cmd, &exec_args.argv, &exec_args.envp); + let Err(e) = execvpe(cmd, &exec_args.argv, &exec_args.envp); - // execvpe only returns on error - let cmd_str = cmd.to_str().unwrap().to_string(); - match e { - Errno::ENOENT => { - let err = ShErr::full(ShErrKind::CmdNotFound(cmd_str), "", span); - eprintln!("{err}"); - } - _ => { - let err = ShErr::full(ShErrKind::Errno(e), format!("{e}"), span); - eprintln!("{err}"); - } - } - exit(e as i32) - } - ForkResult::Parent { child } => { - let cmd_name = exec_args.cmd.0.to_str().unwrap(); + // execvpe only returns on error + let cmd_str = cmd.to_str().unwrap().to_string(); + match e { + Errno::ENOENT => { + let err = ShErr::full(ShErrKind::CmdNotFound(cmd_str), "", span); + eprintln!("{err}"); + } + _ => { + let err = ShErr::full(ShErrKind::Errno(e), format!("{e}"), span); + eprintln!("{err}"); + } + } + exit(e as i32) + } + ForkResult::Parent { child } => { + let cmd_name = exec_args.cmd.0.to_str().unwrap(); - let child_pgid = if let Some(pgid) = job.pgid() { - pgid - } else { - job.set_pgid(child); - child - }; - let child_proc = ChildProc::new(child, Some(cmd_name), Some(child_pgid))?; - job.push_child(child_proc); - } - } + let child_pgid = if let Some(pgid) = job.pgid() { + pgid + } else { + job.set_pgid(child); + child + }; + let child_proc = ChildProc::new(child, Some(cmd_name), Some(child_pgid))?; + job.push_child(child_proc); + } + } - for var in env_vars_to_unset { - unsafe { std::env::set_var(&var, "") }; - } + for var in env_vars_to_unset { + unsafe { std::env::set_var(&var, "") }; + } - Ok(()) - } - fn set_assignments(&self, assigns: Vec, behavior: AssignBehavior) -> ShResult> { - let mut new_env_vars = vec![]; - match behavior { - AssignBehavior::Export => { - for assign in assigns { - let NdRule::Assignment { kind, var, val } = assign.class else { - unreachable!() - }; - let var = var.span.as_str(); - let val = val.expand()?.get_words().join(" "); - match kind { - AssignKind::Eq => write_vars(|v| v.set_var(var, &val, VarFlags::EXPORT)), - AssignKind::PlusEq => todo!(), - AssignKind::MinusEq => todo!(), - AssignKind::MultEq => todo!(), - AssignKind::DivEq => todo!(), - } - new_env_vars.push(var.to_string()); - } - } - AssignBehavior::Set => { - for assign in assigns { - let NdRule::Assignment { kind, var, val } = assign.class else { - unreachable!() - }; - let var = var.span.as_str(); - let val = val.expand()?.get_words().join(" "); - match kind { - AssignKind::Eq => write_vars(|v| v.set_var(var, &val, VarFlags::NONE)), - AssignKind::PlusEq => todo!(), - AssignKind::MinusEq => todo!(), - AssignKind::MultEq => todo!(), - AssignKind::DivEq => todo!(), - } - } - } - } - Ok(new_env_vars) - } + Ok(()) + } + fn set_assignments(&self, assigns: Vec, behavior: AssignBehavior) -> ShResult> { + let mut new_env_vars = vec![]; + match behavior { + AssignBehavior::Export => { + for assign in assigns { + let NdRule::Assignment { kind, var, val } = assign.class else { + unreachable!() + }; + let var = var.span.as_str(); + let val = val.expand()?.get_words().join(" "); + match kind { + AssignKind::Eq => write_vars(|v| v.set_var(var, &val, VarFlags::EXPORT)), + AssignKind::PlusEq => todo!(), + AssignKind::MinusEq => todo!(), + AssignKind::MultEq => todo!(), + AssignKind::DivEq => todo!(), + } + new_env_vars.push(var.to_string()); + } + } + AssignBehavior::Set => { + for assign in assigns { + let NdRule::Assignment { kind, var, val } = assign.class else { + unreachable!() + }; + let var = var.span.as_str(); + let val = val.expand()?.get_words().join(" "); + match kind { + AssignKind::Eq => write_vars(|v| v.set_var(var, &val, VarFlags::NONE)), + AssignKind::PlusEq => todo!(), + AssignKind::MinusEq => todo!(), + AssignKind::MultEq => todo!(), + AssignKind::DivEq => todo!(), + } + } + } + } + Ok(new_env_vars) + } } pub fn prepare_argv(argv: Vec) -> ShResult> { - let mut args = vec![]; + let mut args = vec![]; - for arg in argv { - let span = arg.span.clone(); - let expanded = arg.expand()?; - for exp in expanded.get_words() { - args.push((exp, span.clone())) - } - } - Ok(args) + for arg in argv { + let span = arg.span.clone(); + let expanded = arg.expand()?; + for exp in expanded.get_words() { + args.push((exp, span.clone())) + } + } + Ok(args) } /// Initialize the pipes for a pipeline @@ -753,30 +752,30 @@ pub fn prepare_argv(argv: Vec) -> ShResult> { /// Commands inbetween get `(RPipe, WPipe)` /// If there is only one command, it gets `(None, None)` pub fn get_pipe_stack(num_cmds: usize) -> Vec<(Option, Option)> { - let mut stack = Vec::with_capacity(num_cmds); - let mut prev_read: Option = None; + let mut stack = Vec::with_capacity(num_cmds); + let mut prev_read: Option = None; - for i in 0..num_cmds { - if i == num_cmds - 1 { - stack.push((prev_read.take(), None)); - } else { - let (rpipe, wpipe) = IoMode::get_pipes(); - let r_redir = Redir::new(rpipe, RedirType::Input); - let w_redir = Redir::new(wpipe, RedirType::Output); + for i in 0..num_cmds { + if i == num_cmds - 1 { + stack.push((prev_read.take(), None)); + } else { + let (rpipe, wpipe) = IoMode::get_pipes(); + let r_redir = Redir::new(rpipe, RedirType::Input); + let w_redir = Redir::new(wpipe, RedirType::Output); - // Push (prev_read, Some(w_redir)) and set prev_read to r_redir - stack.push((prev_read.take(), Some(w_redir))); - prev_read = Some(r_redir); - } - } - stack + // Push (prev_read, Some(w_redir)) and set prev_read to r_redir + stack.push((prev_read.take(), Some(w_redir))); + prev_read = Some(r_redir); + } + } + stack } pub fn is_func(tk: Option) -> bool { - let Some(tk) = tk else { return false }; - read_logic(|l| l.get_func(&tk.to_string())).is_some() + let Some(tk) = tk else { return false }; + read_logic(|l| l.get_func(&tk.to_string())).is_some() } pub fn is_subsh(tk: Option) -> bool { - tk.is_some_and(|tk| tk.flags.contains(TkFlags::IS_SUBSH)) + tk.is_some_and(|tk| tk.flags.contains(TkFlags::IS_SUBSH)) } diff --git a/src/parse/lex.rs b/src/parse/lex.rs index 33c6be6..f4bccfd 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -47,11 +47,11 @@ impl Span { pub fn range(&self) -> Range { self.range.clone() } - /// With great power comes great responsibility - /// Only use this in the most dire of circumstances - pub fn set_range(&mut self, range: Range) { - self.range = range; - } + /// With great power comes great responsibility + /// Only use this in the most dire of circumstances + pub fn set_range(&mut self, range: Range) { + self.range = range; + } } /// Allows simple access to the underlying range wrapped by the span @@ -324,13 +324,14 @@ impl LexStream { let can_be_subshell = chars.peek() == Some(&'('); if self.flags.contains(LexFlags::IN_CASE) - && let Some(count) = case_pat_lookahead(chars.clone()) { - pos += count; - let casepat_tk = self.get_token(self.cursor..pos, TkRule::CasePattern); - self.cursor = pos; - self.set_next_is_cmd(true); - return Ok(casepat_tk); - } + && let Some(count) = case_pat_lookahead(chars.clone()) + { + pos += count; + let casepat_tk = self.get_token(self.cursor..pos, TkRule::CasePattern); + self.cursor = pos; + self.set_next_is_cmd(true); + return Ok(casepat_tk); + } while let Some(ch) = chars.next() { match ch { @@ -740,7 +741,10 @@ impl Iterator for LexStream { } self.get_token(ch_idx..self.cursor, TkRule::Sep) } - '#' if !self.flags.contains(LexFlags::INTERACTIVE) || crate::state::read_shopts(|s| s.core.interactive_comments) => { + '#' + if !self.flags.contains(LexFlags::INTERACTIVE) + || crate::state::read_shopts(|s| s.core.interactive_comments) => + { let ch_idx = self.cursor; self.cursor += 1; diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 45ef8c3..7162a2a 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -1160,7 +1160,7 @@ impl ParseStream { let cond_node: CondNode; let mut node_tks = vec![]; - let mut redirs = vec![]; + let mut redirs = vec![]; if (!self.check_keyword("while") && !self.check_keyword("until")) || !self.next_tk_is_some() { return Ok(None); @@ -1238,18 +1238,18 @@ impl ParseStream { fn parse_pipeln(&mut self) -> ShResult> { let mut cmds = vec![]; let mut node_tks = vec![]; - let mut flags = NdFlags::empty(); + let mut flags = NdFlags::empty(); while let Some(cmd) = self.parse_block(false)? { let is_punctuated = node_is_punctuated(&cmd.tokens); node_tks.append(&mut cmd.tokens.clone()); cmds.push(cmd); - if *self.next_tk_class() == TkRule::Bg { - let tk = self.next_tk().unwrap(); - node_tks.push(tk.clone()); - flags |= NdFlags::BACKGROUND; - break; - } else if *self.next_tk_class() != TkRule::Pipe || is_punctuated { + if *self.next_tk_class() == TkRule::Bg { + let tk = self.next_tk().unwrap(); + node_tks.push(tk.clone()); + flags |= NdFlags::BACKGROUND; + break; + } else if *self.next_tk_class() != TkRule::Pipe || is_punctuated { break; } else if let Some(pipe) = self.next_tk() { node_tks.push(pipe) @@ -1278,7 +1278,7 @@ impl ParseStream { let mut node_tks = vec![]; let mut redirs = vec![]; let mut argv = vec![]; - let mut flags = NdFlags::empty(); + let mut flags = NdFlags::empty(); let mut assignments = vec![]; while let Some(prefix_tk) = tk_iter.next() { @@ -1315,27 +1315,32 @@ impl ParseStream { } if argv.is_empty() { - if assignments.is_empty() { - return Ok(None); - } else { - // If we have assignments but no command word, - // return the assignment-only command without parsing more tokens - self.commit(node_tks.len()); - return Ok(Some(Node { - class: NdRule::Command { assignments, argv }, - tokens: node_tks, - flags, - redirs, - })); - } + if assignments.is_empty() { + return Ok(None); + } else { + // If we have assignments but no command word, + // return the assignment-only command without parsing more tokens + self.commit(node_tks.len()); + return Ok(Some(Node { + class: NdRule::Command { assignments, argv }, + tokens: node_tks, + flags, + redirs, + })); + } } while let Some(tk) = tk_iter.next() { - if *self.next_tk_class() == TkRule::Bg { - break; - } + if *self.next_tk_class() == TkRule::Bg { + break; + } match tk.class { - TkRule::EOI | TkRule::Pipe | TkRule::And | TkRule::BraceGrpEnd | TkRule::Or | TkRule::Bg => break, + TkRule::EOI + | TkRule::Pipe + | TkRule::And + | TkRule::BraceGrpEnd + | TkRule::Or + | TkRule::Bg => break, TkRule::Sep => { node_tks.push(tk.clone()); break; diff --git a/src/prelude.rs b/src/prelude.rs index cfc238e..8a39fda 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -19,17 +19,17 @@ pub use std::os::unix::io::{AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, pub use bitflags::bitflags; pub use nix::{ errno::Errno, - fcntl::{open, OFlag}, + fcntl::{OFlag, open}, libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}, sys::{ - signal::{self, kill, killpg, pthread_sigmask, signal, SigHandler, SigSet, SigmaskHow, Signal}, + signal::{self, SigHandler, SigSet, SigmaskHow, Signal, kill, killpg, pthread_sigmask, signal}, stat::Mode, termios::{self}, - wait::{waitpid, WaitPidFlag as WtFlag, WaitStatus as WtStat}, + wait::{WaitPidFlag as WtFlag, WaitStatus as WtStat, waitpid}, }, unistd::{ - close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read, setpgid, tcgetpgrp, - tcsetpgrp, write, ForkResult, Pid, + ForkResult, Pid, close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read, + setpgid, tcgetpgrp, tcsetpgrp, write, }, }; diff --git a/src/procio.rs b/src/procio.rs index 5438529..700793b 100644 --- a/src/procio.rs +++ b/src/procio.rs @@ -4,10 +4,13 @@ use std::{ }; use crate::{ - expand::Expander, libsh::{ + expand::Expander, + libsh::{ error::{ShErr, ShErrKind, ShResult}, utils::RedirVecUtils, - }, parse::{Redir, RedirType, get_redir_file}, prelude::* + }, + parse::{Redir, RedirType, get_redir_file}, + prelude::*, }; // Credit to fish-shell for many of the implementation ideas present in this @@ -17,11 +20,11 @@ use crate::{ pub enum IoMode { Fd { tgt_fd: RawFd, - src_fd: RawFd, // Just the fd number - dup2 will handle it at execution time + src_fd: RawFd, // Just the fd number - dup2 will handle it at execution time }, OpenedFile { tgt_fd: RawFd, - file: Arc, // Owns the opened file descriptor + file: Arc, // Owns the opened file descriptor }, File { tgt_fd: RawFd, @@ -70,17 +73,12 @@ impl IoMode { } pub fn open_file(mut self) -> ShResult { if let IoMode::File { tgt_fd, path, mode } = self { - let path_raw = path - .as_os_str() - .to_str() - .unwrap_or_default() - .to_string(); + let path_raw = path.as_os_str().to_str().unwrap_or_default().to_string(); - let expanded_path = Expander::from_raw(&path_raw)? - .expand()? - .join(" "); // should just be one string, will have to find some way to handle a return of multiple + let expanded_path = Expander::from_raw(&path_raw)?.expand()?.join(" "); // should just be one string, will have to find some way to handle a return of + // multiple - let expanded_pathbuf = PathBuf::from(expanded_path); + let expanded_pathbuf = PathBuf::from(expanded_path); let file = get_redir_file(mode, expanded_pathbuf)?; self = IoMode::OpenedFile { @@ -155,9 +153,9 @@ impl IoBuf { pub struct RedirGuard(IoFrame); impl Drop for RedirGuard { - fn drop(&mut self) { - self.0.restore().ok(); - } + fn drop(&mut self) { + self.0.restore().ok(); + } } /// A struct wrapping three fildescs representing `stdin`, `stdout`, and diff --git a/src/prompt/mod.rs b/src/prompt/mod.rs index 559b0a0..b0e915d 100644 --- a/src/prompt/mod.rs +++ b/src/prompt/mod.rs @@ -1,7 +1,6 @@ pub mod readline; pub mod statusline; - use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*}; /// Initialize the line editor @@ -16,7 +15,7 @@ pub fn get_prompt() -> ShResult { "\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m "; return expand_prompt(default); }; - let sanitized = format!("\\e[0m{prompt}"); + let sanitized = format!("\\e[0m{prompt}"); expand_prompt(&sanitized) } diff --git a/src/prompt/readline/complete.rs b/src/prompt/readline/complete.rs index 30d3624..6ee4c54 100644 --- a/src/prompt/readline/complete.rs +++ b/src/prompt/readline/complete.rs @@ -1,445 +1,467 @@ use std::{env, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc}; -use crate::{builtin::BUILTINS, libsh::error::{ShErr, ShErrKind, ShResult}, parse::lex::{self, LexFlags, Tk, TkFlags}, prompt::readline::{Marker, annotate_input, annotate_input_recursive, get_insertions, markers::{self, is_marker}}, state::{read_logic, read_vars}}; +use crate::{ + builtin::BUILTINS, + libsh::error::{ShErr, ShErrKind, ShResult}, + parse::lex::{self, LexFlags, Tk, TkFlags}, + prompt::readline::{ + Marker, annotate_input, annotate_input_recursive, get_insertions, + markers::{self, is_marker}, + }, + state::{read_logic, read_vars}, +}; pub enum CompCtx { - CmdName, - FileName + CmdName, + FileName, } pub enum CompResult { - NoMatch, - Single { - result: String - }, - Many { - candidates: Vec - } + NoMatch, + Single { result: String }, + Many { candidates: Vec }, } impl CompResult { - pub fn from_candidates(candidates: Vec) -> Self { - if candidates.is_empty() { - Self::NoMatch - } else if candidates.len() == 1 { - Self::Single { result: candidates[0].clone() } - } else { - Self::Many { candidates } - } - } + pub fn from_candidates(candidates: Vec) -> Self { + if candidates.is_empty() { + Self::NoMatch + } else if candidates.len() == 1 { + Self::Single { + result: candidates[0].clone(), + } + } else { + Self::Many { candidates } + } + } } pub struct Completer { - pub candidates: Vec, - pub selected_idx: usize, - pub original_input: String, - pub token_span: (usize, usize), - pub active: bool, + pub candidates: Vec, + pub selected_idx: usize, + pub original_input: String, + pub token_span: (usize, usize), + pub active: bool, } impl Completer { - pub fn new() -> Self { - Self { - candidates: vec![], - selected_idx: 0, - original_input: String::new(), - token_span: (0, 0), - active: false, - } - } + pub fn new() -> Self { + Self { + candidates: vec![], + selected_idx: 0, + original_input: String::new(), + token_span: (0, 0), + active: false, + } + } - pub fn slice_line(line: &str, cursor_pos: usize) -> (&str, &str) { - let (before_cursor, after_cursor) = line.split_at(cursor_pos); - (before_cursor, after_cursor) - } + pub fn slice_line(line: &str, cursor_pos: usize) -> (&str, &str) { + let (before_cursor, after_cursor) = line.split_at(cursor_pos); + (before_cursor, after_cursor) + } - pub fn get_completion_context(&self, line: &str, cursor_pos: usize) -> (Vec, usize) { - let annotated = annotate_input_recursive(line); - log::debug!("Annotated input for completion context: {:?}", annotated); - let mut ctx = vec![markers::NULL]; - let mut last_priority = 0; - let mut ctx_start = 0; - let mut pos = 0; + pub fn get_completion_context(&self, line: &str, cursor_pos: usize) -> (Vec, usize) { + let annotated = annotate_input_recursive(line); + let mut ctx = vec![markers::NULL]; + let mut last_priority = 0; + let mut ctx_start = 0; + let mut pos = 0; - for ch in annotated.chars() { - match ch { - _ if is_marker(ch) => { - match ch { - markers::COMMAND | markers::BUILTIN => { - log::debug!("Found command marker at position {}", pos); - if last_priority < 2 { - if last_priority > 0 { - ctx.pop(); - } - ctx_start = pos; - last_priority = 2; - ctx.push(markers::COMMAND); - } - } - markers::VAR_SUB => { - log::debug!("Found variable substitution marker at position {}", pos); - if last_priority < 3 { - if last_priority > 0 { - ctx.pop(); - } - ctx_start = pos; - last_priority = 3; - ctx.push(markers::VAR_SUB); - } - } - markers::ARG | markers::ASSIGNMENT => { - log::debug!("Found argument/assignment marker at position {}", pos); - if last_priority < 1 { - ctx_start = pos; - ctx.push(markers::ARG); - } - } - _ => {} - } - } - _ => { - last_priority = 0; // reset priority on normal characters - pos += 1; // we hit a normal character, advance our position - if pos >= cursor_pos { - break; - } - } - } - } + for ch in annotated.chars() { + match ch { + _ if is_marker(ch) => match ch { + markers::COMMAND | markers::BUILTIN => { + if last_priority < 2 { + if last_priority > 0 { + ctx.pop(); + } + ctx_start = pos; + last_priority = 2; + ctx.push(markers::COMMAND); + } + } + markers::VAR_SUB => { + if last_priority < 3 { + if last_priority > 0 { + ctx.pop(); + } + ctx_start = pos; + last_priority = 3; + ctx.push(markers::VAR_SUB); + } + } + markers::ARG | markers::ASSIGNMENT => { + if last_priority < 1 { + ctx_start = pos; + ctx.push(markers::ARG); + } + } + _ => {} + }, + _ => { + last_priority = 0; // reset priority on normal characters + pos += 1; // we hit a normal character, advance our position + if pos >= cursor_pos { + break; + } + } + } + } - (ctx, ctx_start) - } + (ctx, ctx_start) + } - pub fn reset(&mut self) { - self.candidates.clear(); - self.selected_idx = 0; - self.original_input.clear(); - self.token_span = (0, 0); - self.active = false; - } + pub fn reset(&mut self) { + self.candidates.clear(); + self.selected_idx = 0; + self.original_input.clear(); + self.token_span = (0, 0); + self.active = false; + } - pub fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult> { - if self.active { - Ok(Some(self.cycle_completion(direction))) - } else { - self.start_completion(line, cursor_pos) - } - } + pub fn complete( + &mut self, + line: String, + cursor_pos: usize, + direction: i32, + ) -> ShResult> { + if self.active { + Ok(Some(self.cycle_completion(direction))) + } else { + self.start_completion(line, cursor_pos) + } + } - pub fn selected_candidate(&self) -> Option { - self.candidates.get(self.selected_idx).cloned() - } + pub fn selected_candidate(&self) -> Option { + self.candidates.get(self.selected_idx).cloned() + } - pub fn cycle_completion(&mut self, direction: i32) -> String { - if self.candidates.is_empty() { - return self.original_input.clone(); - } + pub fn cycle_completion(&mut self, direction: i32) -> String { + if self.candidates.is_empty() { + return self.original_input.clone(); + } - let len = self.candidates.len(); - self.selected_idx = (self.selected_idx as i32 + direction).rem_euclid(len as i32) as usize; + let len = self.candidates.len(); + self.selected_idx = (self.selected_idx as i32 + direction).rem_euclid(len as i32) as usize; - self.get_completed_line() - } + self.get_completed_line() + } - pub fn start_completion(&mut self, line: String, cursor_pos: usize) -> ShResult> { - let result = self.get_candidates(line.clone(), cursor_pos)?; - match result { - CompResult::Many { candidates } => { - self.candidates = candidates.clone(); - self.selected_idx = 0; - self.original_input = line; - self.active = true; + pub fn start_completion(&mut self, line: String, cursor_pos: usize) -> ShResult> { + let result = self.get_candidates(line.clone(), cursor_pos)?; + match result { + CompResult::Many { candidates } => { + self.candidates = candidates.clone(); + self.selected_idx = 0; + self.original_input = line; + self.active = true; - Ok(Some(self.get_completed_line())) - } - CompResult::Single { result } => { - self.candidates = vec![result.clone()]; - self.selected_idx = 0; - self.original_input = line; - self.active = false; + Ok(Some(self.get_completed_line())) + } + CompResult::Single { result } => { + self.candidates = vec![result.clone()]; + self.selected_idx = 0; + self.original_input = line; + self.active = false; - Ok(Some(self.get_completed_line())) - } - CompResult::NoMatch => Ok(None) + Ok(Some(self.get_completed_line())) + } + CompResult::NoMatch => Ok(None), + } + } - } - } + pub fn extract_var_name(text: &str) -> Option<(String, usize, usize)> { + let mut chars = text.chars().peekable(); + let mut name = String::new(); + let mut reading_name = false; + let mut pos = 0; + let mut name_start = 0; + let mut name_end = 0; - pub fn extract_var_name(text: &str) -> Option<(String,usize,usize)> { - let mut chars = text.chars().peekable(); - let mut name = String::new(); - let mut reading_name = false; - let mut pos = 0; - let mut name_start = 0; - let mut name_end = 0; + while let Some(ch) = chars.next() { + match ch { + '$' => { + if chars.peek() == Some(&'{') { + continue; + } - while let Some(ch) = chars.next() { - match ch { - '$' => { - if chars.peek() == Some(&'{') { - continue; - } + reading_name = true; + name_start = pos + 1; // Start after the '$' + } + '{' if !reading_name => { + reading_name = true; + name_start = pos + 1; + } + ch if ch.is_alphanumeric() || ch == '_' => { + if reading_name { + name.push(ch); + } + } + _ => { + if reading_name { + name_end = pos; // End before the non-alphanumeric character + break; + } + } + } + pos += 1; + } - reading_name = true; - name_start = pos + 1; // Start after the '$' - } - '{' if !reading_name => { - reading_name = true; - name_start = pos + 1; - } - ch if ch.is_alphanumeric() || ch == '_' => { - if reading_name { - name.push(ch); - } - } - _ => { - if reading_name { - name_end = pos; // End before the non-alphanumeric character - break; - } - } - } - pos += 1; - } + if !reading_name { + return None; + } - if !reading_name { - return None; - } + if name_end == 0 { + name_end = pos; + } - if name_end == 0 { - name_end = pos; - } + Some((name, name_start, name_end)) + } - Some((name, name_start, name_end)) - } + pub fn get_completed_line(&self) -> String { + if self.candidates.is_empty() { + return self.original_input.clone(); + } - pub fn get_completed_line(&self) -> String { - if self.candidates.is_empty() { - return self.original_input.clone(); - } + let selected = &self.candidates[self.selected_idx]; + let (start, end) = self.token_span; + format!( + "{}{}{}", + &self.original_input[..start], + selected, + &self.original_input[end..] + ) + } - let selected = &self.candidates[self.selected_idx]; - let (start, end) = self.token_span; - format!("{}{}{}", &self.original_input[..start], selected, &self.original_input[end..]) - } + pub fn get_candidates(&mut self, line: String, cursor_pos: usize) -> ShResult { + let source = Arc::new(line.clone()); + let tokens = + lex::LexStream::new(source, LexFlags::LEX_UNFINISHED).collect::>>()?; - pub fn get_candidates(&mut self, line: String, cursor_pos: usize) -> ShResult { - let source = Arc::new(line.clone()); - let tokens = lex::LexStream::new(source, LexFlags::LEX_UNFINISHED).collect::>>()?; + let Some(mut cur_token) = tokens.into_iter().find(|tk| { + let start = tk.span.start; + let end = tk.span.end; + (start..=end).contains(&cursor_pos) + }) else { + let candidates = Self::complete_filename("./"); // Default to filename completion if no token is found + let end_pos = line.len(); + self.token_span = (end_pos, end_pos); + return Ok(CompResult::from_candidates(candidates)); + }; - let Some(mut cur_token) = tokens.into_iter().find(|tk| { - let start = tk.span.start; - let end = tk.span.end; - (start..=end).contains(&cursor_pos) - }) else { - log::debug!("No token found at cursor position"); - let candidates = Self::complete_filename("./"); // Default to filename completion if no token is found - let end_pos = line.len(); - self.token_span = (end_pos, end_pos); - return Ok(CompResult::from_candidates(candidates)); - }; + self.token_span = (cur_token.span.start, cur_token.span.end); - self.token_span = (cur_token.span.start, cur_token.span.end); + // Look for marker at the START of what we're completing, not at cursor + let (mut ctx, token_start) = self.get_completion_context(&line, cursor_pos); + self.token_span.0 = token_start; // Update start of token span based on context + cur_token + .span + .set_range(self.token_span.0..self.token_span.1); // Update token span to reflect context + // If token contains '=', only complete after the '=' + let token_str = cur_token.span.as_str(); + if let Some(eq_pos) = token_str.rfind('=') { + // Adjust span to only replace the part after '=' + self.token_span.0 = cur_token.span.start + eq_pos + 1; + } - // Look for marker at the START of what we're completing, not at cursor - let (mut ctx, token_start) = self.get_completion_context(&line, cursor_pos); - self.token_span.0 = token_start; // Update start of token span based on context - cur_token.span.set_range(self.token_span.0..self.token_span.1); // Update token span to reflect context + if ctx.last().is_some_and(|m| *m == markers::VAR_SUB) { + let var_sub = &cur_token.as_str(); + if let Some((var_name, start, end)) = Self::extract_var_name(var_sub) { + if read_vars(|v| v.get_var(&var_name)).is_empty() { + // if we are here, we have a variable substitution that isn't complete + // so let's try to complete it + let ret: ShResult = read_vars(|v| { + let var_matches = v + .flatten_vars() + .keys() + .filter(|k| k.starts_with(&var_name) && *k != &var_name) + .map(|k| k.to_string()) + .collect::>(); - // If token contains '=', only complete after the '=' - let token_str = cur_token.span.as_str(); - if let Some(eq_pos) = token_str.rfind('=') { - // Adjust span to only replace the part after '=' - self.token_span.0 = cur_token.span.start + eq_pos + 1; - } + if !var_matches.is_empty() { + let name_start = cur_token.span.start + start; + let name_end = cur_token.span.start + end; + self.token_span = (name_start, name_end); + cur_token + .span + .set_range(self.token_span.0..self.token_span.1); + Ok(CompResult::from_candidates(var_matches)) + } else { + Ok(CompResult::NoMatch) + } + }); - if ctx.last().is_some_and(|m| *m == markers::VAR_SUB) { - let var_sub = &cur_token.as_str(); - if let Some((var_name,start,end)) = Self::extract_var_name(var_sub) { - log::debug!("Extracted variable name for completion: {}", var_name); - if read_vars(|v| v.get_var(&var_name)).is_empty() { - // if we are here, we have a variable substitution that isn't complete - // so let's try to complete it - let ret: ShResult = read_vars(|v| { - let var_matches = v.flatten_vars() - .keys() - .filter(|k| k.starts_with(&var_name) && *k != &var_name) - .map(|k| k.to_string()) - .collect::>(); + if !matches!(ret, Ok(CompResult::NoMatch)) { + return ret; + } else { + ctx.pop(); + } + } else { + ctx.pop(); + } + } + } - if !var_matches.is_empty() { - let name_start = cur_token.span.start + start; - let name_end = cur_token.span.start + end; - self.token_span = (name_start, name_end); - cur_token.span.set_range(self.token_span.0..self.token_span.1); - Ok(CompResult::from_candidates(var_matches)) - } else { - Ok(CompResult::NoMatch) - } - }); + let raw_tk = cur_token.as_str().to_string(); + let expanded_tk = cur_token.expand()?; + let expanded_words = expanded_tk.get_words().into_iter().collect::>(); + let expanded = expanded_words.join("\\ "); - if !matches!(ret, Ok(CompResult::NoMatch)) { - return ret; - } else { - ctx.pop(); - } - } else { - ctx.pop(); - } - } - } + let mut candidates = match ctx.pop() { + Some(markers::COMMAND) => Self::complete_command(&expanded)?, + Some(markers::ARG) => Self::complete_filename(&expanded), + Some(_) => { + return Ok(CompResult::NoMatch); + } + None => { + return Ok(CompResult::NoMatch); + } + }; - let raw_tk = cur_token.as_str().to_string(); - let expanded_tk = cur_token.expand()?; - let expanded_words = expanded_tk.get_words().into_iter().collect::>(); - let expanded = expanded_words.join("\\ "); + // Now we are just going to graft the completed text + // onto the original token. This prevents something like + // $SOME_PATH/ + // from being completed into + // /path/to/some_path/file.txt + // and instead returns + // $SOME_PATH/file.txt + candidates = candidates + .into_iter() + .map(|c| match c.strip_prefix(&expanded) { + Some(suffix) => format!("{raw_tk}{suffix}"), + None => c, + }) + .collect(); - let mut candidates = match ctx.pop() { - Some(markers::COMMAND) => { - log::debug!("Completing command: {}", &expanded); - Self::complete_command(&expanded)? - } - Some(markers::ARG) => { - log::debug!("Completing filename: {}", &expanded); - Self::complete_filename(&expanded) - } - Some(m) => { - log::warn!("Unknown marker {:?} in completion context", m); - return Ok(CompResult::NoMatch); - } - None => { - log::warn!("No marker found in completion context"); - return Ok(CompResult::NoMatch); - } - }; + let limit = crate::state::read_shopts(|s| s.prompt.comp_limit); + candidates.truncate(limit); - // Now we are just going to graft the completed text - // onto the original token. This prevents something like - // $SOME_PATH/ - // from being completed into - // /path/to/some_path/file.txt - // and instead returns - // $SOME_PATH/file.txt - candidates = candidates.into_iter() - .map(|c| match c.strip_prefix(&expanded) { - Some(suffix) => format!("{raw_tk}{suffix}"), - None => c - }) - .collect(); + Ok(CompResult::from_candidates(candidates)) + } - let limit = crate::state::read_shopts(|s| s.prompt.comp_limit); - candidates.truncate(limit); + fn complete_command(start: &str) -> ShResult> { + let mut candidates = vec![]; - Ok(CompResult::from_candidates(candidates)) - } + let path = env::var("PATH").unwrap_or_default(); + let paths = path.split(':').map(PathBuf::from).collect::>(); + for path in paths { + // Skip directories that don't exist (common in PATH) + let Ok(entries) = std::fs::read_dir(path) else { + continue; + }; + for entry in entries { + let Ok(entry) = entry else { + continue; + }; + let Ok(meta) = entry.metadata() else { + continue; + }; - fn complete_command(start: &str) -> ShResult> { - let mut candidates = vec![]; + let file_name = entry.file_name().to_string_lossy().to_string(); - let path = env::var("PATH").unwrap_or_default(); - let paths = path.split(':').map(PathBuf::from).collect::>(); - for path in paths { - // Skip directories that don't exist (common in PATH) - let Ok(entries) = std::fs::read_dir(path) else { continue; }; - for entry in entries { - let Ok(entry) = entry else { continue; }; - let Ok(meta) = entry.metadata() else { continue; }; + if meta.is_file() + && (meta.permissions().mode() & 0o111) != 0 + && file_name.starts_with(start) + { + candidates.push(file_name); + } + } + } - let file_name = entry.file_name().to_string_lossy().to_string(); + let builtin_candidates = BUILTINS + .iter() + .filter(|b| b.starts_with(start)) + .map(|s| s.to_string()); - if meta.is_file() - && (meta.permissions().mode() & 0o111) != 0 - && file_name.starts_with(start) { - candidates.push(file_name); - } - } - } + candidates.extend(builtin_candidates); - let builtin_candidates = BUILTINS - .iter() - .filter(|b| b.starts_with(start)) - .map(|s| s.to_string()); + read_logic(|l| { + let func_table = l.funcs(); + let matches = func_table + .keys() + .filter(|k| k.starts_with(start)) + .map(|k| k.to_string()); - candidates.extend(builtin_candidates); + candidates.extend(matches); - read_logic(|l| { - let func_table = l.funcs(); - let matches = func_table - .keys() - .filter(|k| k.starts_with(start)) - .map(|k| k.to_string()); + let aliases = l.aliases(); + let matches = aliases + .keys() + .filter(|k| k.starts_with(start)) + .map(|k| k.to_string()); - candidates.extend(matches); + candidates.extend(matches); + }); - let aliases = l.aliases(); - let matches = aliases - .keys() - .filter(|k| k.starts_with(start)) - .map(|k| k.to_string()); + // Deduplicate (same command may appear in multiple PATH dirs) + candidates.sort(); + candidates.dedup(); - candidates.extend(matches); - }); + Ok(candidates) + } - // Deduplicate (same command may appear in multiple PATH dirs) - candidates.sort(); - candidates.dedup(); + fn complete_filename(start: &str) -> Vec { + let mut candidates = vec![]; + let has_dotslash = start.starts_with("./"); - Ok(candidates) - } + // Split path into directory and filename parts + // Use "." if start is empty (e.g., after "foo=") + let path = PathBuf::from(if start.is_empty() { "." } else { start }); + let (dir, prefix) = if start.ends_with('/') || start.is_empty() { + // Completing inside a directory: "src/" β†’ dir="src/", prefix="" + (path, "") + } else if let Some(parent) = path.parent() + && !parent.as_os_str().is_empty() + { + // Has directory component: "src/ma" β†’ dir="src", prefix="ma" + ( + parent.to_path_buf(), + path.file_name().unwrap().to_str().unwrap_or(""), + ) + } else { + // No directory: "fil" β†’ dir=".", prefix="fil" + (PathBuf::from("."), start) + }; - fn complete_filename(start: &str) -> Vec { - let mut candidates = vec![]; + let Ok(entries) = std::fs::read_dir(&dir) else { + return candidates; + }; - // If completing after '=', only use the part after it - let start = if let Some(eq_pos) = start.rfind('=') { - &start[eq_pos + 1..] - } else { - start - }; + for entry in entries.flatten() { + let file_name = entry.file_name(); + let file_str = file_name.to_string_lossy(); - // Split path into directory and filename parts - // Use "." if start is empty (e.g., after "foo=") - let path = PathBuf::from(if start.is_empty() { "." } else { start }); - let (dir, prefix) = if start.ends_with('/') || start.is_empty() { - // Completing inside a directory: "src/" β†’ dir="src/", prefix="" - (path, "") - } else if let Some(parent) = path.parent() - && !parent.as_os_str().is_empty() { - // Has directory component: "src/ma" β†’ dir="src", prefix="ma" - (parent.to_path_buf(), path.file_name().unwrap().to_str().unwrap_or("")) - } else { - // No directory: "fil" β†’ dir=".", prefix="fil" - (PathBuf::from("."), start) - }; + // Skip hidden files unless explicitly requested + if !prefix.starts_with('.') && file_str.starts_with('.') { + continue; + } - let Ok(entries) = std::fs::read_dir(&dir) else { - return candidates; - }; + if file_str.starts_with(prefix) { + // Reconstruct full path + let mut full_path = dir.join(&file_name); - for entry in entries.flatten() { - let file_name = entry.file_name(); - let file_str = file_name.to_string_lossy(); + // Add trailing slash for directories + if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) { + full_path.push(""); // adds trailing / + } - // Skip hidden files unless explicitly requested - if !prefix.starts_with('.') && file_str.starts_with('.') { - continue; - } + let mut path_raw = full_path.to_string_lossy().to_string(); + if path_raw.starts_with("./") && !has_dotslash { + path_raw = path_raw.trim_start_matches("./").to_string(); + } - if file_str.starts_with(prefix) { - // Reconstruct full path - let mut full_path = dir.join(&file_name); + candidates.push(path_raw); + } + } - // Add trailing slash for directories - if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) { - full_path.push(""); // adds trailing / - } - - candidates.push(full_path.to_string_lossy().to_string()); - } - } - - candidates.sort(); - candidates - } + candidates.sort(); + candidates + } +} + +impl Default for Completer { + fn default() -> Self { + Self::new() + } } diff --git a/src/prompt/readline/highlight.rs b/src/prompt/readline/highlight.rs index aa65179..5db8148 100644 --- a/src/prompt/readline/highlight.rs +++ b/src/prompt/readline/highlight.rs @@ -1,318 +1,395 @@ -use std::{env, os::unix::fs::PermissionsExt, path::{Path, PathBuf}}; +use std::{ + env, + os::unix::fs::PermissionsExt, + path::{Path, PathBuf}, +}; -use crate::{libsh::term::{Style, StyleSet, Styled}, prompt::readline::{annotate_input, markers}, state::{read_logic, read_shopts}}; +use crate::{ + libsh::term::{Style, StyleSet, Styled}, + prompt::readline::{annotate_input, markers}, + state::{read_logic, read_shopts}, +}; /// Syntax highlighter for shell input using Unicode marker-based annotation /// -/// The highlighter processes annotated input strings containing invisible Unicode markers -/// (U+FDD0-U+FDEF range) that indicate syntax elements. It generates ANSI escape codes -/// for terminal display while maintaining a style stack for proper color restoration -/// in nested constructs (e.g., variables inside strings inside command substitutions). +/// The highlighter processes annotated input strings containing invisible +/// Unicode markers (U+FDD0-U+FDEF range) that indicate syntax elements. It +/// generates ANSI escape codes for terminal display while maintaining a style +/// stack for proper color restoration in nested constructs (e.g., variables +/// inside strings inside command substitutions). pub struct Highlighter { - input: String, - output: String, - style_stack: Vec, - last_was_reset: bool, + input: String, + output: String, + style_stack: Vec, + last_was_reset: bool, } impl Highlighter { - /// Creates a new highlighter with empty buffers and reset state - pub fn new() -> Self { - Self { - input: String::new(), - output: String::new(), - style_stack: Vec::new(), - last_was_reset: true, // start as true so we don't emit a leading reset - } - } + /// Creates a new highlighter with empty buffers and reset state + pub fn new() -> Self { + Self { + input: String::new(), + output: String::new(), + style_stack: Vec::new(), + last_was_reset: true, // start as true so we don't emit a leading reset + } + } - /// Loads raw input text and annotates it with syntax markers - /// - /// The input is passed through the annotator which inserts Unicode markers - /// indicating token types and sub-token constructs (strings, variables, etc.) - pub fn load_input(&mut self, input: &str) { - let input = annotate_input(input); - self.input = input; - } + /// Loads raw input text and annotates it with syntax markers + /// + /// The input is passed through the annotator which inserts Unicode markers + /// indicating token types and sub-token constructs (strings, variables, etc.) + pub fn load_input(&mut self, input: &str) { + let input = annotate_input(input); + self.input = input; + } - /// Processes the annotated input and generates ANSI-styled output - /// - /// Walks through the input character by character, interpreting markers and - /// applying appropriate styles. Nested constructs (command substitutions, - /// subshells, strings) are handled recursively with proper style restoration. - pub fn highlight(&mut self) { - let input = self.input.clone(); - let mut input_chars = input.chars().peekable(); - while let Some(ch) = input_chars.next() { - match ch { - markers::STRING_DQ_END | - markers::STRING_SQ_END | - markers::VAR_SUB_END | - markers::CMD_SUB_END | - markers::PROC_SUB_END | - markers::SUBSH_END => self.pop_style(), + /// Processes the annotated input and generates ANSI-styled output + /// + /// Walks through the input character by character, interpreting markers and + /// applying appropriate styles. Nested constructs (command substitutions, + /// subshells, strings) are handled recursively with proper style restoration. + pub fn highlight(&mut self) { + let input = self.input.clone(); + let mut input_chars = input.chars().peekable(); + while let Some(ch) = input_chars.next() { + match ch { + markers::STRING_DQ_END + | markers::STRING_SQ_END + | markers::VAR_SUB_END + | markers::CMD_SUB_END + | markers::PROC_SUB_END + | markers::SUBSH_END => self.pop_style(), - markers::CMD_SEP | - markers::RESET => self.clear_styles(), + markers::CMD_SEP | markers::RESET => self.clear_styles(), + markers::STRING_DQ | markers::STRING_SQ | markers::KEYWORD => { + self.push_style(Style::Yellow) + } + markers::BUILTIN => self.push_style(Style::Green), + markers::CASE_PAT => self.push_style(Style::Blue), - markers::STRING_DQ | - markers::STRING_SQ | - markers::KEYWORD => self.push_style(Style::Yellow), - markers::BUILTIN => self.push_style(Style::Green), - markers::CASE_PAT => self.push_style(Style::Blue), - markers::ARG => self.push_style(Style::White), - markers::COMMENT => self.push_style(Style::BrightBlack), + markers::COMMENT => self.push_style(Style::BrightBlack), - markers::GLOB => self.push_style(Style::Blue), + markers::GLOB => self.push_style(Style::Blue), - markers::REDIRECT | - markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold), + markers::REDIRECT | markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold), - markers::ASSIGNMENT => { - let mut var_name = String::new(); + markers::ASSIGNMENT => { + let mut var_name = String::new(); - while let Some(ch) = input_chars.peek() { - if ch == &'=' { - input_chars.next(); // consume the '=' - break; - } - match *ch { - markers::RESET => break, - _ => { - var_name.push(*ch); - input_chars.next(); - } - } - } + while let Some(ch) = input_chars.peek() { + if ch == &'=' { + input_chars.next(); // consume the '=' + break; + } + match *ch { + markers::RESET => break, + _ => { + var_name.push(*ch); + input_chars.next(); + } + } + } - self.output.push_str(&var_name); - self.push_style(Style::Blue); - self.output.push('='); - self.pop_style(); - } + self.output.push_str(&var_name); + self.push_style(Style::Blue); + self.output.push('='); + self.pop_style(); + } - markers::COMMAND => { - let mut cmd_name = String::new(); - while let Some(ch) = input_chars.peek() { - if *ch == markers::RESET { - break; - } - cmd_name.push(*ch); - input_chars.next(); - } - let style = if Self::is_valid(&cmd_name) { - Style::Green.into() - } else { - Style::Red | Style::Bold - }; - self.push_style(style); - self.output.push_str(&cmd_name); - self.last_was_reset = false; - } - markers::CMD_SUB | markers::SUBSH | markers::PROC_SUB => { - let mut inner = String::new(); - let mut incomplete = true; - let end_marker = match ch { - markers::CMD_SUB => markers::CMD_SUB_END, - markers::SUBSH => markers::SUBSH_END, - markers::PROC_SUB => markers::PROC_SUB_END, - _ => unreachable!(), - }; - while let Some(ch) = input_chars.peek() { - if *ch == end_marker { - incomplete = false; - input_chars.next(); // consume the end marker - break; - } - inner.push(*ch); - input_chars.next(); - } + markers::ARG => { + let mut arg = String::new(); + let mut chars_clone = input_chars.clone(); + while let Some(ch) = chars_clone.next() { + if ch == markers::RESET { + break; + } + arg.push(ch); + } - // Determine prefix from content (handles both <( and >( for proc subs) - let prefix = match ch { - markers::CMD_SUB => "$(", - markers::SUBSH => "(", - markers::PROC_SUB => { - if inner.starts_with("<(") { "<(" } - else if inner.starts_with(">(") { ">(" } - else { "<(" } // fallback - } - _ => unreachable!(), - }; + let style = if Self::is_filename(&arg) { + Style::White | Style::Underline + } else { + Style::White.into() + }; - let inner_content = if incomplete { - inner - .strip_prefix(prefix) - .unwrap_or(&inner) - } else { - inner - .strip_prefix(prefix) - .and_then(|s| s.strip_suffix(")")) - .unwrap_or(&inner) - }; + self.push_style(style); + self.last_was_reset = false; + } - let mut recursive_highlighter = Self::new(); - recursive_highlighter.load_input(inner_content); - recursive_highlighter.highlight(); - self.push_style(Style::Blue); - self.output.push_str(prefix); - self.pop_style(); - self.output.push_str(&recursive_highlighter.take()); - if !incomplete { - self.push_style(Style::Blue); - self.output.push(')'); - self.pop_style(); - } - self.last_was_reset = false; - } - markers::VAR_SUB => { - let mut var_sub = String::new(); - while let Some(ch) = input_chars.peek() { - if *ch == markers::VAR_SUB_END { - input_chars.next(); // consume the end marker - break; - } else if markers::is_marker(*ch) { - input_chars.next(); // skip the marker - continue; - } - var_sub.push(*ch); - input_chars.next(); - } - let style = Style::Cyan; - self.push_style(style); - self.output.push_str(&var_sub); - self.pop_style(); - } - _ => { - if markers::is_marker(ch) { - } else { - self.output.push(ch); - self.last_was_reset = false; - } - } - } - } - } + markers::COMMAND => { + let mut cmd_name = String::new(); + let mut chars_clone = input_chars.clone(); + while let Some(ch) = chars_clone.next() { + if ch == markers::RESET { + break; + } + cmd_name.push(ch); + } + let style = if Self::is_valid(&cmd_name) { + Style::Green.into() + } else { + Style::Red | Style::Bold + }; + self.push_style(style); + self.last_was_reset = false; + } + markers::CMD_SUB | markers::SUBSH | markers::PROC_SUB => { + let mut inner = String::new(); + let mut incomplete = true; + let end_marker = match ch { + markers::CMD_SUB => markers::CMD_SUB_END, + markers::SUBSH => markers::SUBSH_END, + markers::PROC_SUB => markers::PROC_SUB_END, + _ => unreachable!(), + }; + while let Some(ch) = input_chars.peek() { + if *ch == end_marker { + incomplete = false; + input_chars.next(); // consume the end marker + break; + } + inner.push(*ch); + input_chars.next(); + } - /// Extracts the highlighted output and resets the highlighter state - /// - /// Clears the input buffer, style stack, and returns the generated output - /// containing ANSI escape codes. The highlighter is ready for reuse after this. - pub fn take(&mut self) -> String { - self.input.clear(); - self.clear_styles(); - std::mem::take(&mut self.output) - } + // Determine prefix from content (handles both <( and >( for proc subs) + let prefix = match ch { + markers::CMD_SUB => "$(", + markers::SUBSH => "(", + markers::PROC_SUB => { + if inner.starts_with("<(") { + "<(" + } else if inner.starts_with(">(") { + ">(" + } else { + "<(" + } // fallback + } + _ => unreachable!(), + }; - /// Checks if a command name is valid (exists in PATH, is a function, or is an alias) - /// - /// Searches: - /// 1. Current directory if command is a path - /// 2. All directories in PATH environment variable - /// 3. Shell functions and aliases in the current shell state - fn is_valid(command: &str) -> bool { - let path = env::var("PATH").unwrap_or_default(); - let paths = path.split(':'); - let cmd_path = PathBuf::from(&command); + let inner_content = if incomplete { + inner.strip_prefix(prefix).unwrap_or(&inner) + } else { + inner + .strip_prefix(prefix) + .and_then(|s| s.strip_suffix(")")) + .unwrap_or(&inner) + }; - if cmd_path.exists() { - // the user has given us an absolute path - if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) { - // this is a directory and autocd is enabled - return true; - } else { - let Ok(meta) = cmd_path.metadata() else { return false }; - // this is a file that is executable by someone - return meta.permissions().mode() & 0o111 == 0 - } - } else { - // they gave us a command name - // now we must traverse the PATH env var - // and see if we find any matches - for path in paths { - let path = PathBuf::from(path).join(command); - if path.exists() { - let Ok(meta) = path.metadata() else { continue }; - return meta.permissions().mode() & 0o111 != 0; - } - } + let mut recursive_highlighter = Self::new(); + recursive_highlighter.load_input(inner_content); + recursive_highlighter.highlight(); + self.push_style(Style::Blue); + self.output.push_str(prefix); + self.pop_style(); + self.output.push_str(&recursive_highlighter.take()); + if !incomplete { + self.push_style(Style::Blue); + self.output.push(')'); + self.pop_style(); + } + self.last_was_reset = false; + } + markers::VAR_SUB => { + let mut var_sub = String::new(); + while let Some(ch) = input_chars.peek() { + if *ch == markers::VAR_SUB_END { + input_chars.next(); // consume the end marker + break; + } else if markers::is_marker(*ch) { + input_chars.next(); // skip the marker + continue; + } + var_sub.push(*ch); + input_chars.next(); + } + let style = Style::Cyan; + self.push_style(style); + self.output.push_str(&var_sub); + self.pop_style(); + } + _ => { + if markers::is_marker(ch) { + } else { + self.output.push(ch); + self.last_was_reset = false; + } + } + } + } + } - // also check shell functions and aliases for any matches - let found = read_logic(|l| l.get_func(command).is_some() || l.get_alias(command).is_some()); - if found { - return true; - } - } + /// Extracts the highlighted output and resets the highlighter state + /// + /// Clears the input buffer, style stack, and returns the generated output + /// containing ANSI escape codes. The highlighter is ready for reuse after + /// this. + pub fn take(&mut self) -> String { + self.input.clear(); + self.clear_styles(); + std::mem::take(&mut self.output) + } - false - } + /// Checks if a command name is valid (exists in PATH, is a function, or is an + /// alias) + /// + /// Searches: + /// 1. Current directory if command is a path + /// 2. All directories in PATH environment variable + /// 3. Shell functions and aliases in the current shell state + fn is_valid(command: &str) -> bool { + let path = env::var("PATH").unwrap_or_default(); + let paths = path.split(':'); + let cmd_path = PathBuf::from(&command); - /// Emits a reset ANSI code to the output, with deduplication - /// - /// Only emits the reset if the last emitted code was not already a reset, - /// preventing redundant `\x1b[0m` sequences in the output. - fn emit_reset(&mut self) { - if !self.last_was_reset { - self.output.push_str(&Style::Reset.to_string()); - self.last_was_reset = true; - } - } + if cmd_path.exists() { + // the user has given us an absolute path + if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) { + // this is a directory and autocd is enabled + return true; + } else { + let Ok(meta) = cmd_path.metadata() else { + return false; + }; + // this is a file that is executable by someone + return meta.permissions().mode() & 0o111 == 0; + } + } else { + // they gave us a command name + // now we must traverse the PATH env var + // and see if we find any matches + for path in paths { + let path = PathBuf::from(path).join(command); + if path.exists() { + let Ok(meta) = path.metadata() else { continue }; + return meta.permissions().mode() & 0o111 != 0; + } + } - /// Emits a style ANSI code to the output - /// - /// Unconditionally appends the ANSI escape sequence for the given style - /// and marks that we're no longer in a reset state. - fn emit_style(&mut self, style: &StyleSet) { - self.output.push_str(&style.to_string()); - self.last_was_reset = false; - } + // also check shell functions and aliases for any matches + let found = read_logic(|l| l.get_func(command).is_some() || l.get_alias(command).is_some()); + if found { + return true; + } + } - /// Pushes a new style onto the stack and emits its ANSI code - /// - /// Used when entering a new syntax context (string, variable, command, etc.). - /// The style stack allows proper restoration when exiting nested constructs. - pub fn push_style(&mut self, style: impl Into) { - let set: StyleSet = style.into(); - self.style_stack.push(set.clone()); - self.emit_style(&set); - } + false + } - /// Pops a style from the stack and restores the previous style - /// - /// Used when exiting a syntax context. If there's a parent style on the stack, - /// it's re-emitted to restore the previous color. Otherwise, emits a reset. - /// This ensures colors are properly restored in nested constructs like - /// `"string with $VAR"` where the string color resumes after the variable. - pub fn pop_style(&mut self) { - self.style_stack.pop(); - if let Some(style) = self.style_stack.last().cloned() { - self.emit_style(&style); - } else { - self.emit_reset(); - } - } + fn is_filename(arg: &str) -> bool { + let path = PathBuf::from(arg); - /// Clears all styles from the stack and emits a reset - /// - /// Used at command separators and explicit reset markers to return to - /// the default terminal color between independent commands. - pub fn clear_styles(&mut self) { - self.style_stack.clear(); - self.emit_reset(); - } + if path.exists() { + return true; + } - /// Simple marker-to-ANSI replacement (unused in favor of stack-based highlighting) - /// - /// Performs direct string replacement of markers with ANSI codes, without - /// handling nesting or proper color restoration. Kept for reference but not - /// used in the current implementation. - pub fn trivial_replace(&mut self) { - self.input = self.input - .replace([markers::RESET, markers::ARG], "\x1b[0m") - .replace(markers::KEYWORD, "\x1b[33m") - .replace(markers::CASE_PAT, "\x1b[34m") - .replace(markers::COMMENT, "\x1b[90m") - .replace(markers::OPERATOR, "\x1b[35m"); - } + if let Some(parent_dir) = path.parent() + && let Ok(entries) = parent_dir.read_dir() + { + let files = entries + .filter_map(|e| e.ok()) + .map(|e| e.file_name().to_string_lossy().to_string()) + .collect::>(); + let Some(arg_filename) = PathBuf::from(arg) + .file_name() + .map(|s| s.to_string_lossy().to_string()) + else { + return false; + }; + for file in files { + if file.starts_with(&arg_filename) { + return true; + } + } + }; + + if let Ok(this_dir) = env::current_dir() + && let Ok(entries) = this_dir.read_dir() + { + let this_dir_files = entries + .filter_map(|e| e.ok()) + .map(|e| e.file_name().to_string_lossy().to_string()) + .collect::>(); + for file in this_dir_files { + if file.starts_with(arg) { + return true; + } + } + }; + false + } + + /// Emits a reset ANSI code to the output, with deduplication + /// + /// Only emits the reset if the last emitted code was not already a reset, + /// preventing redundant `\x1b[0m` sequences in the output. + fn emit_reset(&mut self) { + if !self.last_was_reset { + self.output.push_str(&Style::Reset.to_string()); + self.last_was_reset = true; + } + } + + /// Emits a style ANSI code to the output + /// + /// Unconditionally appends the ANSI escape sequence for the given style + /// and marks that we're no longer in a reset state. + fn emit_style(&mut self, style: &StyleSet) { + self.output.push_str(&style.to_string()); + self.last_was_reset = false; + } + + /// Pushes a new style onto the stack and emits its ANSI code + /// + /// Used when entering a new syntax context (string, variable, command, etc.). + /// The style stack allows proper restoration when exiting nested constructs. + pub fn push_style(&mut self, style: impl Into) { + let set: StyleSet = style.into(); + self.style_stack.push(set.clone()); + self.emit_style(&set); + } + + /// Pops a style from the stack and restores the previous style + /// + /// Used when exiting a syntax context. If there's a parent style on the + /// stack, it's re-emitted to restore the previous color. Otherwise, emits a + /// reset. This ensures colors are properly restored in nested constructs + /// like `"string with $VAR"` where the string color resumes after the + /// variable. + pub fn pop_style(&mut self) { + self.style_stack.pop(); + if let Some(style) = self.style_stack.last().cloned() { + self.emit_style(&style); + } else { + self.emit_reset(); + } + } + + /// Clears all styles from the stack and emits a reset + /// + /// Used at command separators and explicit reset markers to return to + /// the default terminal color between independent commands. + pub fn clear_styles(&mut self) { + self.style_stack.clear(); + self.emit_reset(); + } + + /// Simple marker-to-ANSI replacement (unused in favor of stack-based + /// highlighting) + /// + /// Performs direct string replacement of markers with ANSI codes, without + /// handling nesting or proper color restoration. Kept for reference but not + /// used in the current implementation. + pub fn trivial_replace(&mut self) { + self.input = self + .input + .replace([markers::RESET, markers::ARG], "\x1b[0m") + .replace(markers::KEYWORD, "\x1b[33m") + .replace(markers::CASE_PAT, "\x1b[34m") + .replace(markers::COMMENT, "\x1b[90m") + .replace(markers::OPERATOR, "\x1b[35m"); + } } diff --git a/src/prompt/readline/history.rs b/src/prompt/readline/history.rs index 7002cc7..81abb0b 100644 --- a/src/prompt/readline/history.rs +++ b/src/prompt/readline/history.rs @@ -189,8 +189,8 @@ fn read_hist_file(path: &Path) -> ShResult> { Ok(raw.parse::()?.0) } -/// Deduplicate entries, keeping only the most recent occurrence of each command. -/// Preserves chronological order (oldest to newest). +/// Deduplicate entries, keeping only the most recent occurrence of each +/// command. Preserves chronological order (oldest to newest). fn dedupe_entries(entries: &[HistEntry]) -> Vec { let mut seen = HashSet::new(); // Iterate backwards (newest first), keeping first occurrence of each command @@ -207,10 +207,10 @@ fn dedupe_entries(entries: &[HistEntry]) -> Vec { pub struct History { path: PathBuf, - pub pending: Option, + pub pending: Option<(String, usize)>, // command, cursor_pos entries: Vec, search_mask: Vec, - no_matches: bool, + no_matches: bool, pub cursor: usize, search_direction: Direction, ignore_dups: bool, @@ -235,9 +235,9 @@ impl History { Ok(Self { path, entries, - pending: None, + pending: None, search_mask, - no_matches: false, + no_matches: false, cursor, search_direction: Direction::Backward, ignore_dups, @@ -245,10 +245,10 @@ impl History { }) } - pub fn reset(&mut self) { - self.search_mask = dedupe_entries(&self.entries); - self.cursor = self.search_mask.len(); - } + pub fn reset(&mut self) { + self.search_mask = dedupe_entries(&self.entries); + self.cursor = self.search_mask.len(); + } pub fn entries(&self) -> &[HistEntry] { &self.entries @@ -270,14 +270,14 @@ impl History { self.cursor = self.search_mask.len(); } - pub fn update_pending_cmd(&mut self, command: &str) { - let cmd = command.to_string(); + pub fn update_pending_cmd(&mut self, buf: (&str, usize)) { + let cmd = buf.0.to_string(); let constraint = SearchConstraint { kind: SearchKind::Prefix, term: cmd.clone(), }; - self.pending = Some(cmd); + self.pending = Some((cmd, buf.1)); self.constrain_entries(constraint); } @@ -315,11 +315,11 @@ impl History { .collect(); self.search_mask = dedupe_entries(&filtered); - self.no_matches = self.search_mask.is_empty(); - if self.no_matches { - // If no matches, reset to full history so user can still scroll through it - self.search_mask = dedupe_entries(&self.entries); - } + self.no_matches = self.search_mask.is_empty(); + if self.no_matches { + // If no matches, reset to full history so user can still scroll through it + self.search_mask = dedupe_entries(&self.entries); + } } self.cursor = self.search_mask.len(); } @@ -328,12 +328,14 @@ impl History { } pub fn hint_entry(&self) -> Option<&HistEntry> { - if self.no_matches { return None }; + if self.no_matches { + return None; + }; self.search_mask.last() } pub fn get_hint(&self) -> Option { - if self.at_pending() && self.pending.as_ref().is_some_and(|p| !p.is_empty()) { + if self.at_pending() && self.pending.as_ref().is_some_and(|p| !p.0.is_empty()) { let entry = self.hint_entry()?; Some(entry.command().to_string()) } else { @@ -342,9 +344,15 @@ impl History { } pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> { - self.cursor = self.cursor.saturating_add_signed(offset).clamp(0, self.search_mask.len()); + self.cursor = self + .cursor + .saturating_add_signed(offset) + .clamp(0, self.search_mask.len()); - log::debug!("Scrolling history by offset {offset} from cursor at index {}", self.cursor); + log::debug!( + "Scrolling history by offset {offset} from cursor at index {}", + self.cursor + ); self.search_mask.get(self.cursor) } @@ -378,7 +386,8 @@ impl History { let last_file_entry = self .entries - .iter().rfind(|ent| !ent.new) + .iter() + .rfind(|ent| !ent.new) .map(|ent| ent.command.clone()) .unwrap_or_default(); @@ -399,8 +408,8 @@ impl History { } file.write_all(data.as_bytes())?; - self.pending = None; - self.reset(); + self.pending = None; + self.reset(); Ok(()) } diff --git a/src/prompt/readline/linebuf.rs b/src/prompt/readline/linebuf.rs index cf4b46d..17d4b3c 100644 --- a/src/prompt/readline/linebuf.rs +++ b/src/prompt/readline/linebuf.rs @@ -133,10 +133,10 @@ impl SelectMode { #[derive(Debug, Clone, PartialEq, Eq)] pub enum MotionKind { - To(usize), // Absolute position, exclusive - On(usize), // Absolute position, inclusive - Onto(usize), /* Absolute position, operations include the position but motions - * exclude it (wtf vim) */ + To(usize), // Absolute position, exclusive + On(usize), // Absolute position, inclusive + Onto(usize), /* Absolute position, operations include the position but motions + * exclude it (wtf vim) */ Inclusive((usize, usize)), // Range, inclusive Exclusive((usize, usize)), // Range, exclusive @@ -360,12 +360,12 @@ impl LineBuf { pub fn set_hint(&mut self, hint: Option) { if let Some(hint) = hint { if let Some(hint) = hint.strip_prefix(&self.buffer) { - if !hint.is_empty() { - self.hint = Some(hint.to_string()) - } else { - self.hint = None - } - } + if !hint.is_empty() { + self.hint = Some(hint.to_string()) + } else { + self.hint = None + } + } } else { self.hint = None } @@ -563,8 +563,8 @@ impl LineBuf { self.update_graphemes(); } pub fn drain(&mut self, start: usize, end: usize) -> String { - let start = start.max(0); - let end = end.min(self.grapheme_indices().len()); + let start = start.max(0); + let end = end.min(self.grapheme_indices().len()); let drained = if end == self.grapheme_indices().len() { if start == self.grapheme_indices().len() { return String::new(); @@ -628,8 +628,9 @@ impl LineBuf { self.next_sentence_start_from_punctuation(pos).is_some() } - /// If position is at sentence-ending punctuation, returns the position of the next sentence start. - /// Handles closing delimiters (`)`, `]`, `"`, `'`) after punctuation. + /// If position is at sentence-ending punctuation, returns the position of the + /// next sentence start. Handles closing delimiters (`)`, `]`, `"`, `'`) + /// after punctuation. #[allow(clippy::collapsible_if)] pub fn next_sentence_start_from_punctuation(&self, pos: usize) -> Option { if let Some(gr) = self.read_grapheme_at(pos) { @@ -956,9 +957,10 @@ impl LineBuf { let start = start.unwrap_or(0); if count > 1 - && let Some((_, new_end)) = self.text_obj_sentence(end, count - 1, bound) { - end = new_end; - } + && let Some((_, new_end)) = self.text_obj_sentence(end, count - 1, bound) + { + end = new_end; + } Some((start, end)) } @@ -1363,7 +1365,12 @@ impl LineBuf { } /// Find the start of the next word forward - pub fn start_of_word_forward(&mut self, mut pos: usize, word: Word, include_last_char: bool) -> usize { + pub fn start_of_word_forward( + &mut self, + mut pos: usize, + word: Word, + include_last_char: bool, + ) -> usize { let default = self.grapheme_indices().len(); let mut indices_iter = (pos..self.cursor.max).peekable(); @@ -1390,8 +1397,7 @@ impl LineBuf { let on_whitespace = is_whitespace(&cur_char); if !on_whitespace { - let Some(ws_pos) = - indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace)) + let Some(ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace)) else { return default; }; @@ -1457,7 +1463,12 @@ impl LineBuf { } /// Find the end of the previous word backward - pub fn end_of_word_backward(&mut self, mut pos: usize, word: Word, include_last_char: bool) -> usize { + pub fn end_of_word_backward( + &mut self, + mut pos: usize, + word: Word, + include_last_char: bool, + ) -> usize { let default = self.grapheme_indices().len(); let mut indices_iter = (0..pos).rev().peekable(); @@ -1484,8 +1495,7 @@ impl LineBuf { let on_whitespace = is_whitespace(&cur_char); if !on_whitespace { - let Some(ws_pos) = - indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace)) + let Some(ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace)) else { return default; }; @@ -1742,11 +1752,7 @@ impl LineBuf { }; pos = next_ws_pos; - if pos == 0 { - pos - } else { - pos + 1 - } + if pos == 0 { pos } else { pos + 1 } } } } @@ -1903,7 +1909,7 @@ impl LineBuf { && self.grapheme_at(target_pos) == Some("\n") { target_pos = target_pos.saturating_sub(1); // Don't land on the - // newline + // newline } MotionKind::InclusiveWithTargetCol((start, end), target_pos) } @@ -2141,7 +2147,7 @@ impl LineBuf { && self.grapheme_at(target_pos) == Some("\n") { target_pos = target_pos.saturating_sub(1); // Don't land on the - // newline + // newline } let (start, end) = match motion.1 { @@ -2575,15 +2581,16 @@ impl LineBuf { } Verb::SwapVisualAnchor => { if let Some((start, end)) = self.select_range() - && let Some(mut mode) = self.select_mode { - mode.invert_anchor(); - let new_cursor_pos = match mode.anchor() { - SelectAnchor::Start => start, - SelectAnchor::End => end, - }; - self.cursor.set(new_cursor_pos); - self.select_mode = Some(mode) - } + && let Some(mut mode) = self.select_mode + { + mode.invert_anchor(); + let new_cursor_pos = match mode.anchor() { + SelectAnchor::Start => start, + SelectAnchor::End => end, + }; + self.cursor.set(new_cursor_pos); + self.select_mode = Some(mode) + } } Verb::JoinLines => { let start = self.start_of_line(); @@ -2731,10 +2738,12 @@ impl LineBuf { let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging); // Merge character inserts into one edit - if edit_is_merging && cmd.verb.as_ref().is_none_or(|v| !v.1.is_char_insert()) - && let Some(edit) = self.undo_stack.last_mut() { - edit.stop_merge(); - } + if edit_is_merging + && cmd.verb.as_ref().is_none_or(|v| !v.1.is_char_insert()) + && let Some(edit) = self.undo_stack.last_mut() + { + edit.stop_merge(); + } let ViCmd { register, @@ -2821,10 +2830,9 @@ impl LineBuf { self.saved_col = None; } - if is_char_insert - && let Some(edit) = self.undo_stack.last_mut() { - edit.start_merge(); - } + if is_char_insert && let Some(edit) = self.undo_stack.last_mut() { + edit.start_merge(); + } Ok(()) } @@ -2832,9 +2840,13 @@ impl LineBuf { &self.buffer // FIXME: this will have to be fixed up later } - pub fn get_hint_text(&self) -> String { - self.hint.clone().map(|h| h.styled(Style::BrightBlack)).unwrap_or_default() - } + pub fn get_hint_text(&self) -> String { + self + .hint + .clone() + .map(|h| h.styled(Style::BrightBlack)) + .unwrap_or_default() + } } impl Display for LineBuf { diff --git a/src/prompt/readline/mod.rs b/src/prompt/readline/mod.rs index f603965..0403bf0 100644 --- a/src/prompt/readline/mod.rs +++ b/src/prompt/readline/mod.rs @@ -2,16 +2,22 @@ use history::History; use keys::{KeyCode, KeyEvent, ModKeys}; use linebuf::{LineBuf, SelectAnchor, SelectMode}; use nix::libc::STDOUT_FILENO; -use term::{get_win_size, KeyReader, Layout, LineWriter, PollReader, TermWriter}; -use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd}; +use term::{KeyReader, Layout, LineWriter, PollReader, TermWriter, get_win_size}; +use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, Verb, VerbCmd, ViCmd}; use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual}; -use crate::{libsh::{ - error::{ShErrKind, ShResult}, - term::{Style, Styled}, -}, parse::lex::{self, LexFlags, Tk, TkFlags, TkRule}, prompt::readline::{complete::{CompResult, Completer}, highlight::Highlighter}}; use crate::prelude::*; +use crate::{ + libsh::{ + error::ShResult, + term::{Style, Styled}, + }, + parse::lex::{self, LexFlags, Tk, TkFlags, TkRule}, + prompt::readline::{complete::Completer, highlight::Highlighter}, +}; +pub mod complete; +pub mod highlight; pub mod history; pub mod keys; pub mod layout; @@ -20,77 +26,59 @@ pub mod register; pub mod term; pub mod vicmd; pub mod vimode; -pub mod highlight; -pub mod complete; pub mod markers { - use super::Marker; + use super::Marker; - // token-level (derived from token class) - pub const COMMAND: Marker = '\u{fdd0}'; - pub const BUILTIN: Marker = '\u{fdd1}'; - pub const ARG: Marker = '\u{fdd2}'; - pub const KEYWORD: Marker = '\u{fdd3}'; - pub const OPERATOR: Marker = '\u{fdd4}'; - pub const REDIRECT: Marker = '\u{fdd5}'; - pub const COMMENT: Marker = '\u{fdd6}'; - pub const ASSIGNMENT: Marker = '\u{fdd7}'; - pub const CMD_SEP: Marker = '\u{fde0}'; - pub const CASE_PAT: Marker = '\u{fde1}'; - pub const SUBSH: Marker = '\u{fde7}'; - pub const SUBSH_END: Marker = '\u{fde8}'; + // token-level (derived from token class) + pub const COMMAND: Marker = '\u{fdd0}'; + pub const BUILTIN: Marker = '\u{fdd1}'; + pub const ARG: Marker = '\u{fdd2}'; + pub const KEYWORD: Marker = '\u{fdd3}'; + pub const OPERATOR: Marker = '\u{fdd4}'; + pub const REDIRECT: Marker = '\u{fdd5}'; + pub const COMMENT: Marker = '\u{fdd6}'; + pub const ASSIGNMENT: Marker = '\u{fdd7}'; + pub const CMD_SEP: Marker = '\u{fde0}'; + pub const CASE_PAT: Marker = '\u{fde1}'; + pub const SUBSH: Marker = '\u{fde7}'; + pub const SUBSH_END: Marker = '\u{fde8}'; - // sub-token (needs scanning) - pub const VAR_SUB: Marker = '\u{fdda}'; - pub const VAR_SUB_END: Marker = '\u{fde3}'; - pub const CMD_SUB: Marker = '\u{fdd8}'; - pub const CMD_SUB_END: Marker = '\u{fde4}'; - pub const PROC_SUB: Marker = '\u{fdd9}'; - pub const PROC_SUB_END: Marker = '\u{fde9}'; - pub const STRING_DQ: Marker = '\u{fddb}'; - pub const STRING_DQ_END: Marker = '\u{fde5}'; - pub const STRING_SQ: Marker = '\u{fddc}'; - pub const STRING_SQ_END: Marker = '\u{fde6}'; - pub const ESCAPE: Marker = '\u{fddd}'; - pub const GLOB: Marker = '\u{fdde}'; + // sub-token (needs scanning) + pub const VAR_SUB: Marker = '\u{fdda}'; + pub const VAR_SUB_END: Marker = '\u{fde3}'; + pub const CMD_SUB: Marker = '\u{fdd8}'; + pub const CMD_SUB_END: Marker = '\u{fde4}'; + pub const PROC_SUB: Marker = '\u{fdd9}'; + pub const PROC_SUB_END: Marker = '\u{fde9}'; + pub const STRING_DQ: Marker = '\u{fddb}'; + pub const STRING_DQ_END: Marker = '\u{fde5}'; + pub const STRING_SQ: Marker = '\u{fddc}'; + pub const STRING_SQ_END: Marker = '\u{fde6}'; + pub const ESCAPE: Marker = '\u{fddd}'; + pub const GLOB: Marker = '\u{fdde}'; - pub const RESET: Marker = '\u{fde2}'; + pub const RESET: Marker = '\u{fde2}'; - pub const NULL: Marker = '\u{fdef}'; + pub const NULL: Marker = '\u{fdef}'; - pub const END_MARKERS: [Marker;7] = [ - VAR_SUB_END, - CMD_SUB_END, - PROC_SUB_END, - STRING_DQ_END, - STRING_SQ_END, - SUBSH_END, - RESET - ]; - pub const TOKEN_LEVEL: [Marker;10] = [ - SUBSH, - COMMAND, - BUILTIN, - ARG, - KEYWORD, - OPERATOR, - REDIRECT, - CMD_SEP, - CASE_PAT, - ASSIGNMENT, - ]; - pub const SUB_TOKEN: [Marker;6] = [ - VAR_SUB, - CMD_SUB, - PROC_SUB, - STRING_DQ, - STRING_SQ, - GLOB, - ]; + pub const END_MARKERS: [Marker; 7] = [ + VAR_SUB_END, + CMD_SUB_END, + PROC_SUB_END, + STRING_DQ_END, + STRING_SQ_END, + SUBSH_END, + RESET, + ]; + pub const TOKEN_LEVEL: [Marker; 10] = [ + SUBSH, COMMAND, BUILTIN, ARG, KEYWORD, OPERATOR, REDIRECT, CMD_SEP, CASE_PAT, ASSIGNMENT, + ]; + pub const SUB_TOKEN: [Marker; 6] = [VAR_SUB, CMD_SUB, PROC_SUB, STRING_DQ, STRING_SQ, GLOB]; - pub fn is_marker(c: Marker) -> bool { - TOKEN_LEVEL.contains(&c) || SUB_TOKEN.contains(&c) || END_MARKERS.contains(&c) - } + pub fn is_marker(c: Marker) -> bool { + TOKEN_LEVEL.contains(&c) || SUB_TOKEN.contains(&c) || END_MARKERS.contains(&c) + } } type Marker = char; @@ -109,8 +97,8 @@ pub struct FernVi { pub writer: Box, pub prompt: String, - pub highlighter: Highlighter, - pub completer: Completer, + pub highlighter: Highlighter, + pub completer: Completer, pub mode: Box, pub repeat_action: Option, @@ -125,34 +113,34 @@ pub struct FernVi { impl FernVi { pub fn new(prompt: Option) -> ShResult { - let mut new = Self { - reader: PollReader::new(), - writer: Box::new(TermWriter::new(STDOUT_FILENO)), - prompt: prompt.unwrap_or("$ ".styled(Style::Green)), - completer: Completer::new(), - highlighter: Highlighter::new(), - mode: Box::new(ViInsert::new()), - old_layout: None, - repeat_action: None, - repeat_motion: None, - editor: LineBuf::new(), - history: History::new()?, - needs_redraw: true, - }; - new.print_line()?; - Ok(new) + let mut new = Self { + reader: PollReader::new(), + writer: Box::new(TermWriter::new(STDOUT_FILENO)), + prompt: prompt.unwrap_or("$ ".styled(Style::Green)), + completer: Completer::new(), + highlighter: Highlighter::new(), + mode: Box::new(ViInsert::new()), + old_layout: None, + repeat_action: None, + repeat_motion: None, + editor: LineBuf::new(), + history: History::new()?, + needs_redraw: true, + }; + new.print_line()?; + Ok(new) } pub fn with_initial(mut self, initial: &str) -> Self { self.editor = LineBuf::new().with_initial(initial, 0); - self.history.update_pending_cmd(self.editor.as_str()); + self + .history + .update_pending_cmd((self.editor.as_str(), self.editor.cursor.get())); self } /// Feed raw bytes from stdin into the reader's buffer pub fn feed_bytes(&mut self, bytes: &[u8]) { - let test_input = "echo \"hello $USER\" | grep $(whoami)"; - let annotated = annotate_input(test_input); self.reader.feed_bytes(bytes); } @@ -167,7 +155,7 @@ impl FernVi { self.prompt = p; } self.editor = Default::default(); - self.mode = Box::new(ViInsert::new()); + self.mode = Box::new(ViInsert::new()); self.old_layout = None; self.needs_redraw = true; self.history.pending = None; @@ -185,55 +173,63 @@ impl FernVi { // Process all available keys while let Some(key) = self.reader.read_key()? { - if self.should_accept_hint(&key) { self.editor.accept_hint(); if !self.history.at_pending() { self.history.reset_to_pending(); } - self.history.update_pending_cmd(self.editor.as_str()); + self + .history + .update_pending_cmd((self.editor.as_str(), self.editor.cursor.get())); self.needs_redraw = true; continue; } - if let KeyEvent(KeyCode::Tab, mod_keys) = key { - let direction = match mod_keys { - ModKeys::SHIFT => -1, - _ => 1, - }; - let line = self.editor.as_str().to_string(); - let cursor_pos = self.editor.cursor_byte_pos(); + if let KeyEvent(KeyCode::Tab, mod_keys) = key { + let direction = match mod_keys { + ModKeys::SHIFT => -1, + _ => 1, + }; + let line = self.editor.as_str().to_string(); + let cursor_pos = self.editor.cursor_byte_pos(); - match self.completer.complete(line, cursor_pos, direction)? { - Some(mut line) => { - let span_start = self.completer.token_span.0; - let new_cursor = span_start + self.completer.selected_candidate().map(|c| c.len()).unwrap_or_default(); + match self.completer.complete(line, cursor_pos, direction)? { + Some(line) => { + let span_start = self.completer.token_span.0; + let new_cursor = span_start + + self + .completer + .selected_candidate() + .map(|c| c.len()) + .unwrap_or_default(); - self.editor.set_buffer(line); - self.editor.cursor.set(new_cursor); + self.editor.set_buffer(line); + self.editor.cursor.set(new_cursor); - if !self.history.at_pending() { - self.history.reset_to_pending(); - } - self.history.update_pending_cmd(self.editor.as_str()); - let hint = self.history.get_hint(); - self.editor.set_hint(hint); - } - None => { - match crate::state::read_shopts(|s| s.core.bell_style) { - crate::shopt::FernBellStyle::Audible => { self.writer.flush_write("\x07")?; } - crate::shopt::FernBellStyle::Visible | crate::shopt::FernBellStyle::Disable => {} - } - } - } + if !self.history.at_pending() { + self.history.reset_to_pending(); + } + self + .history + .update_pending_cmd((self.editor.as_str(), self.editor.cursor.get())); + let hint = self.history.get_hint(); + self.editor.set_hint(hint); + } + None => match crate::state::read_shopts(|s| s.core.bell_style) { + crate::shopt::FernBellStyle::Audible => { + self.writer.flush_write("\x07")?; + } + crate::shopt::FernBellStyle::Visible | crate::shopt::FernBellStyle::Disable => {} + }, + } - self.needs_redraw = true; - continue; - } + self.needs_redraw = true; + continue; + } - // if we are here, we didnt press tab - // so we should reset the completer state - self.completer.reset(); + // if we are here, we didnt press tab + // so we should reset the completer state + self.completer.reset(); let Some(mut cmd) = self.mode.handle_key(key) else { continue; @@ -247,19 +243,18 @@ impl FernVi { } if cmd.should_submit() { - self.editor.set_hint(None); - self.print_line()?; + self.editor.set_hint(None); + self.print_line()?; self.writer.flush_write("\n")?; let buf = self.editor.take_buf(); // Save command to history if auto_hist is enabled - if crate::state::read_shopts(|s| s.core.auto_hist) - && !buf.is_empty() { - self.history.push(buf.clone()); - if let Err(e) = self.history.save() { - eprintln!("Failed to save history: {e}"); - } - } - self.history.reset(); + if crate::state::read_shopts(|s| s.core.auto_hist) && !buf.is_empty() { + self.history.push(buf.clone()); + if let Err(e) = self.history.save() { + eprintln!("Failed to save history: {e}"); + } + } + self.history.reset(); return Ok(ReadlineEvent::Line(buf)); } @@ -278,7 +273,9 @@ impl FernVi { let after = self.editor.as_str(); if before != after { - self.history.update_pending_cmd(self.editor.as_str()); + self + .history + .update_pending_cmd((self.editor.as_str(), self.editor.cursor.get())); } let hint = self.history.get_hint(); @@ -310,30 +307,27 @@ impl FernVi { */ let count = &cmd.motion().unwrap().0; let motion = &cmd.motion().unwrap().1; - let count = match motion { - Motion::LineUpCharwise => { - -(*count as isize) - } - Motion::LineDownCharwise => { - *count as isize - } + let count = match motion { + Motion::LineUpCharwise => -(*count as isize), + Motion::LineDownCharwise => *count as isize, _ => unreachable!(), }; let entry = self.history.scroll(count); - log::info!("Scrolled history, got entry: {:?}", entry.as_ref()); - if let Some(entry) = entry { - log::info!("Setting buffer to history entry: {}", entry.command()); - let pending = self.editor.take_buf(); - self.editor.set_buffer(entry.command().to_string()); - if self.history.pending.is_none() { - self.history.pending = Some(pending); - } - self.editor.set_hint(None); - } else if let Some(pending) = self.history.pending.take() { - log::info!("Setting buffer to pending command: {}", &pending); - self.editor.set_buffer(pending); - self.editor.set_hint(None); - } + log::info!("Scrolled history, got entry: {:?}", entry.as_ref()); + if let Some(entry) = entry { + log::info!("Setting buffer to history entry: {}", entry.command()); + let pending = self.editor.take_buf(); + self.editor.set_buffer(entry.command().to_string()); + if self.history.pending.is_none() { + self.history.pending = Some((pending, self.editor.cursor.get())); + } + self.editor.set_hint(None); + } else if let Some(pending) = self.history.pending.take() { + log::info!("Setting buffer to pending command: {}", &pending.0); + self.editor.set_buffer(pending.0); + self.editor.cursor.set(pending.1); + self.editor.set_hint(None); + } } pub fn should_accept_hint(&self, event: &KeyEvent) -> bool { if self.editor.cursor_at_max() && self.editor.has_hint() { @@ -365,29 +359,27 @@ impl FernVi { && self.editor.end_of_line() == self.editor.cursor_max()) } - pub fn line_text(&mut self) -> String { - let line = self.editor.to_string(); - let hint = self.editor.get_hint_text(); - if crate::state::read_shopts(|s| s.prompt.highlight) { - self.highlighter.load_input(&line); - self.highlighter.highlight(); - let highlighted = self.highlighter.take(); - format!("{highlighted}{hint}") - } else { - format!("{line}{hint}") - } - } + pub fn line_text(&mut self) -> String { + let line = self.editor.to_string(); + let hint = self.editor.get_hint_text(); + if crate::state::read_shopts(|s| s.prompt.highlight) { + self.highlighter.load_input(&line); + self.highlighter.highlight(); + let highlighted = self.highlighter.take(); + format!("{highlighted}{hint}") + } else { + format!("{line}{hint}") + } + } pub fn print_line(&mut self) -> ShResult<()> { - let line = self.line_text(); + let line = self.line_text(); let new_layout = self.get_layout(&line); if let Some(layout) = self.old_layout.as_ref() { self.writer.clear_rows(layout)?; } - self - .writer - .redraw(&self.prompt, &line, &new_layout)?; + self.writer.redraw(&self.prompt, &line, &new_layout)?; self.writer.flush_write(&self.mode.cursor_style())?; @@ -484,7 +476,7 @@ impl FernVi { } } else { return Ok(()); // it has to have a verb to be repeatable, - // something weird happened + // something weird happened } } self.editor.exec_cmd(cmd)?; @@ -553,8 +545,8 @@ impl FernVi { /// Annotates shell input with invisible Unicode markers for syntax highlighting /// -/// Takes raw shell input and inserts non-character markers (U+FDD0-U+FDEF range) -/// around syntax elements. These markers indicate: +/// Takes raw shell input and inserts non-character markers (U+FDD0-U+FDEF +/// range) around syntax elements. These markers indicate: /// - Token-level context (commands, arguments, operators, keywords) /// - Sub-token constructs (strings, variables, command substitutions, globs) /// @@ -571,289 +563,274 @@ impl FernVi { /// ``` /// (where COMMAND, RESET, etc. are invisible Unicode markers) pub fn annotate_input(input: &str) -> String { - let mut annotated = input.to_string(); - let input = Arc::new(input.to_string()); - let tokens: Vec = lex::LexStream::new(input, LexFlags::LEX_UNFINISHED) - .flatten() - .filter(|tk| !matches!(tk.class, TkRule::SOI | TkRule::EOI | TkRule::Null)) - .collect(); + let mut annotated = input.to_string(); + let input = Arc::new(input.to_string()); + let tokens: Vec = lex::LexStream::new(input, LexFlags::LEX_UNFINISHED) + .flatten() + .filter(|tk| !matches!(tk.class, TkRule::SOI | TkRule::EOI | TkRule::Null)) + .collect(); - for tk in tokens.into_iter().rev() { - let insertions = annotate_token(tk); - for (pos, marker) in insertions { - let pos = pos.max(0).min(annotated.len()); - annotated.insert(pos, marker); - } - } + for tk in tokens.into_iter().rev() { + let insertions = annotate_token(tk); + for (pos, marker) in insertions { + let pos = pos.max(0).min(annotated.len()); + annotated.insert(pos, marker); + } + } - annotated + annotated } /// Recursively annotates nested constructs in the input string pub fn annotate_input_recursive(input: &str) -> String { - let mut annotated = annotate_input(input); - let mut chars = annotated.char_indices().peekable(); - let mut changes = vec![]; + let mut annotated = annotate_input(input); + let mut chars = annotated.char_indices().peekable(); + let mut changes = vec![]; - while let Some((pos,ch)) = chars.next() { - match ch { - markers::CMD_SUB | - markers::SUBSH | - markers::PROC_SUB => { - let mut body = String::new(); - let span_start = pos + ch.len_utf8(); - let mut span_end = span_start; - let closing_marker = match ch { - markers::CMD_SUB => markers::CMD_SUB_END, - markers::SUBSH => markers::SUBSH_END, - markers::PROC_SUB => markers::PROC_SUB_END, - _ => unreachable!() - }; - while let Some((sub_pos,sub_ch)) = chars.next() { - match sub_ch { - _ if sub_ch == closing_marker => { - span_end = sub_pos; - break; - } - _ => body.push(sub_ch), - } - } - let prefix = match ch { - markers::PROC_SUB => { - match chars.peek().map(|(_, c)| *c) { - Some('>') => ">(", - Some('<') => "<(", - _ => { - log::error!("Unexpected character after PROC_SUB marker: expected '>' or '<'"); - "<(" - } - } - } - markers::CMD_SUB => "$(", - markers::SUBSH => "(", - _ => unreachable!() - }; + while let Some((pos, ch)) = chars.next() { + match ch { + markers::CMD_SUB | markers::SUBSH | markers::PROC_SUB => { + let mut body = String::new(); + let span_start = pos + ch.len_utf8(); + let mut span_end = span_start; + let closing_marker = match ch { + markers::CMD_SUB => markers::CMD_SUB_END, + markers::SUBSH => markers::SUBSH_END, + markers::PROC_SUB => markers::PROC_SUB_END, + _ => unreachable!(), + }; + while let Some((sub_pos, sub_ch)) = chars.next() { + match sub_ch { + _ if sub_ch == closing_marker => { + span_end = sub_pos; + break; + } + _ => body.push(sub_ch), + } + } + let prefix = match ch { + markers::PROC_SUB => match chars.peek().map(|(_, c)| *c) { + Some('>') => ">(", + Some('<') => "<(", + _ => { + log::error!("Unexpected character after PROC_SUB marker: expected '>' or '<'"); + "<(" + } + }, + markers::CMD_SUB => "$(", + markers::SUBSH => "(", + _ => unreachable!(), + }; - body = body.trim_start_matches(prefix).to_string(); - let annotated_body = annotate_input_recursive(&body); - let final_str = format!("{prefix}{annotated_body})"); - changes.push((span_start, span_end, final_str)); - } - _ => {} - } - } + body = body.trim_start_matches(prefix).to_string(); + let annotated_body = annotate_input_recursive(&body); + let final_str = format!("{prefix}{annotated_body})"); + changes.push((span_start, span_end, final_str)); + } + _ => {} + } + } - for change in changes.into_iter().rev() { - let (start, end, replacement) = change; - annotated.replace_range(start..end, &replacement); - } + for change in changes.into_iter().rev() { + let (start, end, replacement) = change; + annotated.replace_range(start..end, &replacement); + } - annotated + annotated } pub fn get_insertions(input: &str) -> Vec<(usize, Marker)> { - let input = Arc::new(input.to_string()); - let tokens: Vec = lex::LexStream::new(input, LexFlags::LEX_UNFINISHED) - .flatten() - .collect(); + let input = Arc::new(input.to_string()); + let tokens: Vec = lex::LexStream::new(input, LexFlags::LEX_UNFINISHED) + .flatten() + .collect(); - let mut insertions = vec![]; - for tk in tokens.into_iter().rev() { - insertions.extend(annotate_token(tk)); - } - insertions + let mut insertions = vec![]; + for tk in tokens.into_iter().rev() { + insertions.extend(annotate_token(tk)); + } + insertions } /// Maps token class to its corresponding marker character /// /// Returns the appropriate Unicode marker for token-level syntax elements. -/// Token-level markers are derived directly from the lexer's token classification -/// and represent complete tokens (operators, separators, etc.). +/// Token-level markers are derived directly from the lexer's token +/// classification and represent complete tokens (operators, separators, etc.). /// /// Returns `None` for: /// - String tokens (which need sub-token scanning for variables, quotes, etc.) /// - Structural markers (SOI, EOI, Null) /// - Unimplemented features (comments, brace groups) pub fn marker_for(class: &TkRule) -> Option { - match class { - TkRule::Pipe | - TkRule::ErrPipe | - TkRule::And | - TkRule::Or | - TkRule::Bg => Some(markers::OPERATOR), - TkRule::Sep => Some(markers::CMD_SEP), - TkRule::Redir => Some(markers::REDIRECT), - TkRule::CasePattern => Some(markers::CASE_PAT), - TkRule::BraceGrpStart => todo!(), - TkRule::BraceGrpEnd => todo!(), - TkRule::Comment => todo!(), - TkRule::Expanded { exp: _ } | - TkRule::EOI | - TkRule::SOI | - TkRule::Null | - TkRule::Str => None, - } + match class { + TkRule::Pipe | TkRule::ErrPipe | TkRule::And | TkRule::Or | TkRule::Bg => { + Some(markers::OPERATOR) + } + TkRule::Sep => Some(markers::CMD_SEP), + TkRule::Redir => Some(markers::REDIRECT), + TkRule::CasePattern => Some(markers::CASE_PAT), + TkRule::BraceGrpStart => todo!(), + TkRule::BraceGrpEnd => todo!(), + TkRule::Comment => todo!(), + TkRule::Expanded { exp: _ } | TkRule::EOI | TkRule::SOI | TkRule::Null | TkRule::Str => None, + } } pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { - // Sort by position descending, with priority ordering at same position: - // - RESET first (inserted first, ends up rightmost) - // - Regular markers middle - // - END markers last (inserted last, ends up leftmost) - // Result: [END][TOGGLE][RESET] - let sort_insertions = |insertions: &mut Vec<(usize, Marker)>| { - insertions.sort_by(|a, b| { - match b.0.cmp(&a.0) { - std::cmp::Ordering::Equal => { - let priority = |m: Marker| -> u8 { - match m { - markers::RESET => 0, - markers::VAR_SUB | - markers::VAR_SUB_END | - markers::CMD_SUB | - markers::CMD_SUB_END | - markers::PROC_SUB | - markers::PROC_SUB_END | - markers::STRING_DQ | - markers::STRING_DQ_END | - markers::STRING_SQ | - markers::STRING_SQ_END | - markers::SUBSH_END => 2, - markers::ARG => 3, - _ => 1, - } - }; - priority(a.1).cmp(&priority(b.1)) - } - other => other, - } - }); - }; + // Sort by position descending, with priority ordering at same position: + // - RESET first (inserted first, ends up rightmost) + // - Regular markers middle + // - END markers last (inserted last, ends up leftmost) + // Result: [END][TOGGLE][RESET] + let sort_insertions = |insertions: &mut Vec<(usize, Marker)>| { + insertions.sort_by(|a, b| match b.0.cmp(&a.0) { + std::cmp::Ordering::Equal => { + let priority = |m: Marker| -> u8 { + match m { + markers::RESET => 0, + markers::VAR_SUB + | markers::VAR_SUB_END + | markers::CMD_SUB + | markers::CMD_SUB_END + | markers::PROC_SUB + | markers::PROC_SUB_END + | markers::STRING_DQ + | markers::STRING_DQ_END + | markers::STRING_SQ + | markers::STRING_SQ_END + | markers::SUBSH_END => 2, + markers::ARG => 3, + _ => 1, + } + }; + priority(a.1).cmp(&priority(b.1)) + } + other => other, + }); + }; - let in_context = |c: Marker, insertions: &[(usize, Marker)]| -> bool { - let mut stack = insertions.to_vec(); - stack.sort_by(|a, b| { - match b.0.cmp(&a.0) { - std::cmp::Ordering::Equal => { - let priority = |m: Marker| -> u8 { - match m { - markers::RESET => 0, - markers::VAR_SUB | - markers::VAR_SUB_END | - markers::CMD_SUB | - markers::CMD_SUB_END | - markers::PROC_SUB | - markers::PROC_SUB_END | - markers::STRING_DQ | - markers::STRING_DQ_END | - markers::STRING_SQ | - markers::STRING_SQ_END | - markers::SUBSH_END => 2, - markers::ARG => 3, // Lowest priority - processed first, overridden by sub-tokens - _ => 1, - } - }; - priority(a.1).cmp(&priority(b.1)) - } - other => other, - } - }); - stack.retain(|(i, m)| *i <= token.span.start && !markers::END_MARKERS.contains(m)); + let in_context = |c: Marker, insertions: &[(usize, Marker)]| -> bool { + let mut stack = insertions.to_vec(); + stack.sort_by(|a, b| { + match b.0.cmp(&a.0) { + std::cmp::Ordering::Equal => { + let priority = |m: Marker| -> u8 { + match m { + markers::RESET => 0, + markers::VAR_SUB + | markers::VAR_SUB_END + | markers::CMD_SUB + | markers::CMD_SUB_END + | markers::PROC_SUB + | markers::PROC_SUB_END + | markers::STRING_DQ + | markers::STRING_DQ_END + | markers::STRING_SQ + | markers::STRING_SQ_END + | markers::SUBSH_END => 2, + markers::ARG => 3, // Lowest priority - processed first, overridden by sub-tokens + _ => 1, + } + }; + priority(a.1).cmp(&priority(b.1)) + } + other => other, + } + }); + stack.retain(|(i, m)| *i <= token.span.start && !markers::END_MARKERS.contains(m)); - let Some(ctx) = stack.last() else { - return false; - }; + let Some(ctx) = stack.last() else { + return false; + }; - ctx.1 == c - }; + ctx.1 == c + }; - let mut insertions: Vec<(usize, Marker)> = vec![]; + let mut insertions: Vec<(usize, Marker)> = vec![]; + if token.class != TkRule::Str + && let Some(marker) = marker_for(&token.class) + { + insertions.push((token.span.end, markers::RESET)); + insertions.push((token.span.start, marker)); + return insertions; + } else if token.flags.contains(TkFlags::IS_SUBSH) { + let token_raw = token.span.as_str(); + if token_raw.ends_with(')') { + insertions.push((token.span.end, markers::SUBSH_END)); + } + insertions.push((token.span.start, markers::SUBSH)); + return insertions; + } - if token.class != TkRule::Str - && let Some(marker) = marker_for(&token.class) { - insertions.push((token.span.end, markers::RESET)); - insertions.push((token.span.start, marker)); - return insertions; - } else if token.flags.contains(TkFlags::IS_SUBSH) { - let token_raw = token.span.as_str(); - if token_raw.ends_with(')') { - insertions.push((token.span.end, markers::SUBSH_END)); - } - insertions.push((token.span.start, markers::SUBSH)); - return insertions; - } + let token_raw = token.span.as_str(); + let mut token_chars = token_raw.char_indices().peekable(); + let span_start = token.span.start; - let token_raw = token.span.as_str(); - let mut token_chars = token_raw - .char_indices() - .peekable(); + let mut in_dub_qt = false; + let mut in_sng_qt = false; + let mut cmd_sub_depth = 0; + let mut proc_sub_depth = 0; - let span_start = token.span.start; + if token.flags.contains(TkFlags::BUILTIN) { + insertions.insert(0, (span_start, markers::BUILTIN)); + } else if token.flags.contains(TkFlags::IS_CMD) { + insertions.insert(0, (span_start, markers::COMMAND)); + } else if !token.flags.contains(TkFlags::KEYWORD) && !token.flags.contains(TkFlags::ASSIGN) { + insertions.insert(0, (span_start, markers::ARG)); + } - let mut in_dub_qt = false; - let mut in_sng_qt = false; - let mut cmd_sub_depth = 0; - let mut proc_sub_depth = 0; + if token.flags.contains(TkFlags::KEYWORD) { + insertions.insert(0, (span_start, markers::KEYWORD)); + } - if token.flags.contains(TkFlags::BUILTIN) { - insertions.insert(0, (span_start, markers::BUILTIN)); - } else if token.flags.contains(TkFlags::IS_CMD) { - insertions.insert(0, (span_start, markers::COMMAND)); - } else if !token.flags.contains(TkFlags::KEYWORD) && !token.flags.contains(TkFlags::ASSIGN) { - insertions.insert(0, (span_start, markers::ARG)); - } + if token.flags.contains(TkFlags::ASSIGN) { + insertions.insert(0, (span_start, markers::ASSIGNMENT)); + } - if token.flags.contains(TkFlags::KEYWORD) { - insertions.insert(0, (span_start, markers::KEYWORD)); - } + insertions.insert(0, (token.span.end, markers::RESET)); // reset at the end of the token - if token.flags.contains(TkFlags::ASSIGN) { - insertions.insert(0, (span_start, markers::ASSIGNMENT)); - } - - insertions.insert(0, (token.span.end, markers::RESET)); // reset at the end of the token - - while let Some((i,ch)) = token_chars.peek() { - let index = *i; // we have to dereference this here because rustc is a very pedantic program - match ch { - ')' if cmd_sub_depth > 0 || proc_sub_depth > 0 => { - token_chars.next(); // consume the paren - if cmd_sub_depth > 0 { - cmd_sub_depth -= 1; - if cmd_sub_depth == 0 { - insertions.push((span_start + index + 1, markers::CMD_SUB_END)); - } - } else if proc_sub_depth > 0 { - proc_sub_depth -= 1; - if proc_sub_depth == 0 { - insertions.push((span_start + index + 1, markers::PROC_SUB_END)); - } - } - } - '$' if !in_sng_qt => { - let dollar_pos = index; - token_chars.next(); // consume the dollar - if let Some((_, dollar_ch)) = token_chars.peek() { - match dollar_ch { - '(' => { - cmd_sub_depth += 1; - if cmd_sub_depth == 1 { - // only mark top level command subs - insertions.push((span_start + dollar_pos, markers::CMD_SUB)); - } - token_chars.next(); // consume the paren - } - '{' if cmd_sub_depth == 0 => { - insertions.push((span_start + dollar_pos, markers::VAR_SUB)); - token_chars.next(); // consume the brace - let mut end_pos; // position after ${ - while let Some((cur_i, br_ch)) = token_chars.peek() { - end_pos = *cur_i; - // TODO: implement better parameter expansion awareness here - // this is a little too permissive - if br_ch.is_ascii_alphanumeric() + while let Some((i, ch)) = token_chars.peek() { + let index = *i; // we have to dereference this here because rustc is a very pedantic program + match ch { + ')' if cmd_sub_depth > 0 || proc_sub_depth > 0 => { + token_chars.next(); // consume the paren + if cmd_sub_depth > 0 { + cmd_sub_depth -= 1; + if cmd_sub_depth == 0 { + insertions.push((span_start + index + 1, markers::CMD_SUB_END)); + } + } else if proc_sub_depth > 0 { + proc_sub_depth -= 1; + if proc_sub_depth == 0 { + insertions.push((span_start + index + 1, markers::PROC_SUB_END)); + } + } + } + '$' if !in_sng_qt => { + let dollar_pos = index; + token_chars.next(); // consume the dollar + if let Some((_, dollar_ch)) = token_chars.peek() { + match dollar_ch { + '(' => { + cmd_sub_depth += 1; + if cmd_sub_depth == 1 { + // only mark top level command subs + insertions.push((span_start + dollar_pos, markers::CMD_SUB)); + } + token_chars.next(); // consume the paren + } + '{' if cmd_sub_depth == 0 => { + insertions.push((span_start + dollar_pos, markers::VAR_SUB)); + token_chars.next(); // consume the brace + let mut end_pos; // position after ${ + while let Some((cur_i, br_ch)) = token_chars.peek() { + end_pos = *cur_i; + // TODO: implement better parameter expansion awareness here + // this is a little too permissive + if br_ch.is_ascii_alphanumeric() || *br_ch == '_' || *br_ch == '!' || *br_ch == '#' @@ -863,119 +840,121 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { || *br_ch == '+' || *br_ch == '=' || *br_ch == '/' // parameter expansion symbols - || *br_ch == '?' { - token_chars.next(); - } else if *br_ch == '}' { - token_chars.next(); // consume the closing brace - insertions.push((span_start + end_pos + 1, markers::VAR_SUB_END)); - break; - } else { - // malformed, insert end at current position - insertions.push((span_start + end_pos, markers::VAR_SUB_END)); - break; - } - } - } - _ if cmd_sub_depth == 0 && (dollar_ch.is_ascii_alphanumeric() || *dollar_ch == '_') => { - insertions.push((span_start + dollar_pos, markers::VAR_SUB)); - let mut end_pos = dollar_pos + 1; - // consume the var name - while let Some((cur_i, var_ch)) = token_chars.peek() { - if var_ch.is_ascii_alphanumeric() || *var_ch == '_' { - end_pos = *cur_i + 1; - token_chars.next(); - } else { - break; - } - } - insertions.push((span_start + end_pos, markers::VAR_SUB_END)); - } - _ => { /* Just a plain dollar sign, no marker needed */ } - } - } - } - ch if cmd_sub_depth > 0 || proc_sub_depth > 0 => { - // We are inside of a command sub or process sub right now - // We don't mark any of this text. It will later be recursively annotated - // by the syntax highlighter - token_chars.next(); // consume the char with no special handling - } + || *br_ch == '?' + { + token_chars.next(); + } else if *br_ch == '}' { + token_chars.next(); // consume the closing brace + insertions.push((span_start + end_pos + 1, markers::VAR_SUB_END)); + break; + } else { + // malformed, insert end at current position + insertions.push((span_start + end_pos, markers::VAR_SUB_END)); + break; + } + } + } + _ if cmd_sub_depth == 0 && (dollar_ch.is_ascii_alphanumeric() || *dollar_ch == '_') => { + insertions.push((span_start + dollar_pos, markers::VAR_SUB)); + let mut end_pos = dollar_pos + 1; + // consume the var name + while let Some((cur_i, var_ch)) = token_chars.peek() { + if var_ch.is_ascii_alphanumeric() || *var_ch == '_' { + end_pos = *cur_i + 1; + token_chars.next(); + } else { + break; + } + } + insertions.push((span_start + end_pos, markers::VAR_SUB_END)); + } + _ => { /* Just a plain dollar sign, no marker needed */ } + } + } + } + ch if cmd_sub_depth > 0 || proc_sub_depth > 0 => { + // We are inside of a command sub or process sub right now + // We don't mark any of this text. It will later be recursively annotated + // by the syntax highlighter + token_chars.next(); // consume the char with no special handling + } - '\\' if !in_sng_qt => { - token_chars.next(); // consume the backslash - if token_chars.peek().is_some() { - token_chars.next(); // consume the escaped char - } - } - '<' | '>' if !in_dub_qt && !in_sng_qt && cmd_sub_depth == 0 && proc_sub_depth == 0 => { - token_chars.next(); - if let Some((_, proc_sub_ch)) = token_chars.peek() - && *proc_sub_ch == '(' { - proc_sub_depth += 1; - token_chars.next(); // consume the paren - if proc_sub_depth == 1 { - insertions.push((span_start + index, markers::PROC_SUB)); - } - } - } - '"' if !in_sng_qt => { - if in_dub_qt { - insertions.push((span_start + *i + 1, markers::STRING_DQ_END)); - } else { - insertions.push((span_start + *i, markers::STRING_DQ)); - } - in_dub_qt = !in_dub_qt; - token_chars.next(); // consume the quote - } - '\'' if !in_dub_qt => { - if in_sng_qt { - insertions.push((span_start + *i + 1, markers::STRING_SQ_END)); - } else { - insertions.push((span_start + *i, markers::STRING_SQ)); - } - in_sng_qt = !in_sng_qt; - token_chars.next(); // consume the quote - } - '[' if !in_dub_qt && !in_sng_qt => { - token_chars.next(); // consume the opening bracket - let start_pos = span_start + index; - let mut is_glob_pat = false; - const VALID_CHARS: &[char] = &['!', '^', '-']; + '\\' if !in_sng_qt => { + token_chars.next(); // consume the backslash + if token_chars.peek().is_some() { + token_chars.next(); // consume the escaped char + } + } + '<' | '>' if !in_dub_qt && !in_sng_qt && cmd_sub_depth == 0 && proc_sub_depth == 0 => { + token_chars.next(); + if let Some((_, proc_sub_ch)) = token_chars.peek() + && *proc_sub_ch == '(' + { + proc_sub_depth += 1; + token_chars.next(); // consume the paren + if proc_sub_depth == 1 { + insertions.push((span_start + index, markers::PROC_SUB)); + } + } + } + '"' if !in_sng_qt => { + if in_dub_qt { + insertions.push((span_start + *i + 1, markers::STRING_DQ_END)); + } else { + insertions.push((span_start + *i, markers::STRING_DQ)); + } + in_dub_qt = !in_dub_qt; + token_chars.next(); // consume the quote + } + '\'' if !in_dub_qt => { + if in_sng_qt { + insertions.push((span_start + *i + 1, markers::STRING_SQ_END)); + } else { + insertions.push((span_start + *i, markers::STRING_SQ)); + } + in_sng_qt = !in_sng_qt; + token_chars.next(); // consume the quote + } + '[' if !in_dub_qt && !in_sng_qt => { + token_chars.next(); // consume the opening bracket + let start_pos = span_start + index; + let mut is_glob_pat = false; + const VALID_CHARS: &[char] = &['!', '^', '-']; - while let Some((cur_i, ch)) = token_chars.peek() { - if *ch == ']' { - is_glob_pat = true; - insertions.push((span_start + *cur_i + 1, markers::RESET)); - insertions.push((span_start + *cur_i, markers::GLOB)); - token_chars.next(); // consume the closing bracket - break; - } else if !ch.is_ascii_alphanumeric() && !VALID_CHARS.contains(ch) { - token_chars.next(); - break; - } else { - token_chars.next(); - } - } + while let Some((cur_i, ch)) = token_chars.peek() { + if *ch == ']' { + is_glob_pat = true; + insertions.push((span_start + *cur_i + 1, markers::RESET)); + insertions.push((span_start + *cur_i, markers::GLOB)); + token_chars.next(); // consume the closing bracket + break; + } else if !ch.is_ascii_alphanumeric() && !VALID_CHARS.contains(ch) { + token_chars.next(); + break; + } else { + token_chars.next(); + } + } - if is_glob_pat { - insertions.push((start_pos + 1, markers::RESET)); - insertions.push((start_pos, markers::GLOB)); - } - } - '*' | '?' if (!in_dub_qt && !in_sng_qt) => { - if !in_context(markers::COMMAND, &insertions) { - insertions.push((span_start + *i + 1, markers::RESET)); - insertions.push((span_start + *i, markers::GLOB)); - } - token_chars.next(); // consume the glob char - } - _ => { - token_chars.next(); // consume the char with no special handling - } - } - } + if is_glob_pat { + insertions.push((start_pos + 1, markers::RESET)); + insertions.push((start_pos, markers::GLOB)); + } + } + '*' | '?' if (!in_dub_qt && !in_sng_qt) => { + if !in_context(markers::COMMAND, &insertions) { + insertions.push((span_start + *i + 1, markers::RESET)); + insertions.push((span_start + *i, markers::GLOB)); + } + token_chars.next(); // consume the glob char + } + _ => { + token_chars.next(); // consume the char with no special handling + } + } + } - sort_insertions(&mut insertions); + sort_insertions(&mut insertions); - insertions + insertions } diff --git a/src/prompt/readline/term.rs b/src/prompt/readline/term.rs index 9299532..4e32097 100644 --- a/src/prompt/readline/term.rs +++ b/src/prompt/readline/term.rs @@ -17,11 +17,15 @@ use unicode_segmentation::UnicodeSegmentation; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use vte::{Parser, Perform}; -use crate::{prelude::*, procio::borrow_fd, state::{read_meta, write_meta}}; use crate::{ libsh::error::{ShErr, ShErrKind, ShResult}, prompt::readline::keys::{KeyCode, ModKeys}, }; +use crate::{ + prelude::*, + procio::borrow_fd, + state::{read_meta, write_meta}, +}; use super::{keys::KeyEvent, linebuf::LineBuf}; @@ -41,7 +45,7 @@ pub fn raw_mode() -> RawModeGuard { ) .expect("Failed to set terminal to raw mode"); - let (cols, rows) = get_win_size(STDIN_FILENO); + let (cols, rows) = get_win_size(STDIN_FILENO); RawModeGuard { orig, @@ -242,9 +246,7 @@ impl Read for TermBuffer { let result = nix::unistd::read(self.tty, buf); match result { Ok(n) => Ok(n), - Err(Errno::EINTR) => { - Err(Errno::EINTR.into()) - } + Err(Errno::EINTR) => Err(Errno::EINTR.into()), Err(e) => Err(std::io::Error::from_raw_os_error(e as i32)), } } @@ -280,17 +282,21 @@ impl RawModeGuard { } } - pub fn with_cooked_mode(f: F) -> R - where F: FnOnce() -> R { - let raw = tcgetattr(borrow_fd(STDIN_FILENO)).expect("Failed to get terminal attributes"); - let mut cooked = raw.clone(); - cooked.local_flags |= termios::LocalFlags::ICANON | termios::LocalFlags::ECHO; - cooked.input_flags |= termios::InputFlags::ICRNL; - tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &cooked).expect("Failed to set cooked mode"); - let res = f(); - tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &raw).expect("Failed to restore raw mode"); - res - } + pub fn with_cooked_mode(f: F) -> R + where + F: FnOnce() -> R, + { + let raw = tcgetattr(borrow_fd(STDIN_FILENO)).expect("Failed to get terminal attributes"); + let mut cooked = raw.clone(); + cooked.local_flags |= termios::LocalFlags::ICANON | termios::LocalFlags::ECHO; + cooked.input_flags |= termios::InputFlags::ICRNL; + tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &cooked) + .expect("Failed to set cooked mode"); + let res = f(); + tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &raw) + .expect("Failed to restore raw mode"); + res + } } impl Drop for RawModeGuard { @@ -333,9 +339,15 @@ impl KeyCollector { // CSI modifiers: param = 1 + (shift) + (alt*2) + (ctrl*4) + (meta*8) let bits = param.saturating_sub(1); let mut mods = ModKeys::empty(); - if bits & 1 != 0 { mods |= ModKeys::SHIFT; } - if bits & 2 != 0 { mods |= ModKeys::ALT; } - if bits & 4 != 0 { mods |= ModKeys::CTRL; } + if bits & 1 != 0 { + mods |= ModKeys::SHIFT; + } + if bits & 2 != 0 { + mods |= ModKeys::ALT; + } + if bits & 4 != 0 { + mods |= ModKeys::CTRL; + } mods } } @@ -374,46 +386,72 @@ impl Perform for KeyCollector { self.push(event); } - fn csi_dispatch(&mut self, params: &vte::Params, intermediates: &[u8], _ignore: bool, action: char) { - let params: Vec = params.iter() + fn csi_dispatch( + &mut self, + params: &vte::Params, + intermediates: &[u8], + _ignore: bool, + action: char, + ) { + let params: Vec = params + .iter() .map(|p| p.first().copied().unwrap_or(0)) .collect(); let event = match (intermediates, action) { // Arrow keys: CSI A/B/C/D or CSI 1;mod A/B/C/D ([], 'A') => { - let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty()); + let mods = params + .get(1) + .map(|&m| Self::parse_modifiers(m)) + .unwrap_or(ModKeys::empty()); KeyEvent(KeyCode::Up, mods) } ([], 'B') => { - let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty()); + let mods = params + .get(1) + .map(|&m| Self::parse_modifiers(m)) + .unwrap_or(ModKeys::empty()); KeyEvent(KeyCode::Down, mods) } ([], 'C') => { - let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty()); + let mods = params + .get(1) + .map(|&m| Self::parse_modifiers(m)) + .unwrap_or(ModKeys::empty()); KeyEvent(KeyCode::Right, mods) } ([], 'D') => { - let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty()); + let mods = params + .get(1) + .map(|&m| Self::parse_modifiers(m)) + .unwrap_or(ModKeys::empty()); KeyEvent(KeyCode::Left, mods) } // Home/End: CSI H/F or CSI 1;mod H/F ([], 'H') => { - let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty()); + let mods = params + .get(1) + .map(|&m| Self::parse_modifiers(m)) + .unwrap_or(ModKeys::empty()); KeyEvent(KeyCode::Home, mods) } ([], 'F') => { - let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty()); + let mods = params + .get(1) + .map(|&m| Self::parse_modifiers(m)) + .unwrap_or(ModKeys::empty()); KeyEvent(KeyCode::End, mods) } // Shift+Tab: CSI Z - ([], 'Z') => { - KeyEvent(KeyCode::Tab, ModKeys::SHIFT) - } + ([], 'Z') => KeyEvent(KeyCode::Tab, ModKeys::SHIFT), // Special keys with tilde: CSI num ~ or CSI num;mod ~ ([], '~') => { let key_num = params.first().copied().unwrap_or(0); - let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty()); + let mods = params + .get(1) + .map(|&m| Self::parse_modifiers(m)) + .unwrap_or(ModKeys::empty()); let key = match key_num { 1 | 7 => KeyCode::Home, 2 => KeyCode::Insert, @@ -473,7 +511,9 @@ impl PollReader { pub fn feed_bytes(&mut self, bytes: &[u8]) { if bytes == [b'\x1b'] { // Single escape byte - user pressed ESC key - self.collector.push(KeyEvent(KeyCode::Esc, ModKeys::empty())); + self + .collector + .push(KeyEvent(KeyCode::Esc, ModKeys::empty())); return; } @@ -914,13 +954,13 @@ impl LineWriter for TermWriter { let end = new_layout.end; let cursor = new_layout.cursor; - if read_meta(|m| m.system_msg_pending()) { - let mut system_msg = String::new(); - while let Some(msg) = write_meta(|m| m.pop_system_message()) { - writeln!(system_msg, "{msg}").map_err(err)?; - } - self.buffer.push_str(&system_msg); - } + if read_meta(|m| m.system_msg_pending()) { + let mut system_msg = String::new(); + while let Some(msg) = write_meta(|m| m.pop_system_message()) { + writeln!(system_msg, "{msg}").map_err(err)?; + } + self.buffer.push_str(&system_msg); + } self.buffer.push_str(prompt); self.buffer.push_str(line); diff --git a/src/prompt/readline/vicmd.rs b/src/prompt/readline/vicmd.rs index 7f22612..e6658a4 100644 --- a/src/prompt/readline/vicmd.rs +++ b/src/prompt/readline/vicmd.rs @@ -161,14 +161,16 @@ impl ViCmd { } /// If a ViCmd has a linewise motion, but no verb, we change it to charwise pub fn alter_line_motion_if_no_verb(&mut self) { - if self.is_line_motion() && self.verb.is_none() - && let Some(motion) = self.motion.as_mut() { - match motion.1 { - Motion::LineUp => motion.1 = Motion::LineUpCharwise, - Motion::LineDown => motion.1 = Motion::LineDownCharwise, - _ => unreachable!(), - } + if self.is_line_motion() + && self.verb.is_none() + && let Some(motion) = self.motion.as_mut() + { + match motion.1 { + Motion::LineUp => motion.1 = Motion::LineUpCharwise, + Motion::LineDown => motion.1 = Motion::LineDownCharwise, + _ => unreachable!(), } + } } pub fn is_mode_transition(&self) -> bool { self.verb.as_ref().is_some_and(|v| { diff --git a/src/prompt/readline/vimode.rs b/src/prompt/readline/vimode.rs index 032341d..fe95d7a 100644 --- a/src/prompt/readline/vimode.rs +++ b/src/prompt/readline/vimode.rs @@ -315,7 +315,7 @@ impl ViNormal { return match obj { TextObj::Sentence(_) | TextObj::Paragraph(_) => CmdState::Complete, _ => CmdState::Invalid, - } + }; } Some(_) => return CmdState::Complete, None => return CmdState::Pending, @@ -410,7 +410,7 @@ impl ViNormal { motion: None, raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } '~' => { chars_clone.next(); @@ -445,7 +445,7 @@ impl ViNormal { motion: None, raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } 'x' => { return Some(ViCmd { @@ -454,7 +454,7 @@ impl ViNormal { motion: Some(MotionCmd(1, Motion::ForwardChar)), raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } 'X' => { return Some(ViCmd { @@ -463,7 +463,7 @@ impl ViNormal { motion: Some(MotionCmd(1, Motion::BackwardChar)), raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } 's' => { return Some(ViCmd { @@ -472,7 +472,7 @@ impl ViNormal { motion: Some(MotionCmd(1, Motion::ForwardChar)), raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } 'S' => { return Some(ViCmd { @@ -481,7 +481,7 @@ impl ViNormal { motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } 'p' => { chars = chars_clone; @@ -516,7 +516,7 @@ impl ViNormal { motion: None, raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } '~' => { return Some(ViCmd { @@ -525,7 +525,7 @@ impl ViNormal { motion: None, raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } 'u' => { return Some(ViCmd { @@ -534,7 +534,7 @@ impl ViNormal { motion: None, raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } 'v' => { return Some(ViCmd { @@ -543,7 +543,7 @@ impl ViNormal { motion: None, raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } 'V' => { return Some(ViCmd { @@ -552,7 +552,7 @@ impl ViNormal { motion: None, raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } 'o' => { return Some(ViCmd { @@ -561,7 +561,7 @@ impl ViNormal { motion: None, raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } 'O' => { return Some(ViCmd { @@ -570,7 +570,7 @@ impl ViNormal { motion: None, raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } 'a' => { return Some(ViCmd { @@ -579,7 +579,7 @@ impl ViNormal { motion: Some(MotionCmd(1, Motion::ForwardChar)), raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } 'A' => { return Some(ViCmd { @@ -588,7 +588,7 @@ impl ViNormal { motion: Some(MotionCmd(1, Motion::EndOfLine)), raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } 'i' => { return Some(ViCmd { @@ -597,7 +597,7 @@ impl ViNormal { motion: None, raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } 'I' => { return Some(ViCmd { @@ -606,7 +606,7 @@ impl ViNormal { motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)), raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } 'J' => { return Some(ViCmd { @@ -615,7 +615,7 @@ impl ViNormal { motion: None, raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } 'y' => { chars = chars_clone; @@ -636,7 +636,7 @@ impl ViNormal { motion: Some(MotionCmd(1, Motion::EndOfLine)), raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } 'D' => { return Some(ViCmd { @@ -645,7 +645,7 @@ impl ViNormal { motion: Some(MotionCmd(1, Motion::EndOfLine)), raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } 'C' => { return Some(ViCmd { @@ -654,7 +654,7 @@ impl ViNormal { motion: Some(MotionCmd(1, Motion::EndOfLine)), raw_seq: self.take_cmd(), flags: self.flags(), - }) + }); } '=' => { chars = chars_clone; @@ -684,7 +684,7 @@ impl ViNormal { | ('~', Some(VerbCmd(_, Verb::ToggleCaseRange))) | ('>', Some(VerbCmd(_, Verb::Indent))) | ('<', Some(VerbCmd(_, Verb::Dedent))) => { - break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)) + break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)); } ('W', Some(VerbCmd(_, Verb::Change))) => { // Same with 'W' @@ -994,8 +994,7 @@ impl ViNormal { } }; - if chars.peek().is_some() { - } + if chars.peek().is_some() {} let verb_ref = verb.as_ref().map(|v| &v.1); let motion_ref = motion.as_ref().map(|m| &m.1); @@ -1185,7 +1184,7 @@ impl ViVisual { motion: None, raw_seq: self.take_cmd(), flags: CmdFlags::empty(), - }) + }); } '?' => { return Some(ViCmd { @@ -1194,7 +1193,7 @@ impl ViVisual { motion: None, raw_seq: self.take_cmd(), flags: CmdFlags::empty(), - }) + }); } _ => break 'verb_parse None, } @@ -1209,7 +1208,7 @@ impl ViVisual { motion: None, raw_seq: self.take_cmd(), flags: CmdFlags::empty(), - }) + }); } 'x' => { chars = chars_clone; @@ -1222,7 +1221,7 @@ impl ViVisual { motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), flags: CmdFlags::empty(), - }) + }); } 'Y' => { return Some(ViCmd { @@ -1231,7 +1230,7 @@ impl ViVisual { motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), flags: CmdFlags::empty(), - }) + }); } 'D' => { return Some(ViCmd { @@ -1240,7 +1239,7 @@ impl ViVisual { motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), flags: CmdFlags::empty(), - }) + }); } 'R' | 'C' => { return Some(ViCmd { @@ -1249,7 +1248,7 @@ impl ViVisual { motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), flags: CmdFlags::empty(), - }) + }); } '>' => { return Some(ViCmd { @@ -1258,7 +1257,7 @@ impl ViVisual { motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), flags: CmdFlags::empty(), - }) + }); } '<' => { return Some(ViCmd { @@ -1267,7 +1266,7 @@ impl ViVisual { motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), flags: CmdFlags::empty(), - }) + }); } '=' => { return Some(ViCmd { @@ -1276,7 +1275,7 @@ impl ViVisual { motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), flags: CmdFlags::empty(), - }) + }); } 'p' | 'P' => { chars = chars_clone; @@ -1299,7 +1298,7 @@ impl ViVisual { motion: None, raw_seq: self.take_cmd(), flags: CmdFlags::empty(), - }) + }); } 'u' => { return Some(ViCmd { @@ -1308,7 +1307,7 @@ impl ViVisual { motion: None, raw_seq: self.take_cmd(), flags: CmdFlags::empty(), - }) + }); } 'U' => { return Some(ViCmd { @@ -1317,7 +1316,7 @@ impl ViVisual { motion: None, raw_seq: self.take_cmd(), flags: CmdFlags::empty(), - }) + }); } 'O' | 'o' => { return Some(ViCmd { @@ -1326,7 +1325,7 @@ impl ViVisual { motion: None, raw_seq: self.take_cmd(), flags: CmdFlags::empty(), - }) + }); } 'A' => { return Some(ViCmd { @@ -1335,7 +1334,7 @@ impl ViVisual { motion: Some(MotionCmd(1, Motion::ForwardChar)), raw_seq: self.take_cmd(), flags: CmdFlags::empty(), - }) + }); } 'I' => { return Some(ViCmd { @@ -1344,7 +1343,7 @@ impl ViVisual { motion: Some(MotionCmd(1, Motion::BeginningOfLine)), raw_seq: self.take_cmd(), flags: CmdFlags::empty(), - }) + }); } 'J' => { return Some(ViCmd { @@ -1353,7 +1352,7 @@ impl ViVisual { motion: None, raw_seq: self.take_cmd(), flags: CmdFlags::empty(), - }) + }); } 'y' => { chars = chars_clone; @@ -1395,7 +1394,7 @@ impl ViVisual { | ('=', Some(VerbCmd(_, Verb::Equalize))) | ('>', Some(VerbCmd(_, Verb::Indent))) | ('<', Some(VerbCmd(_, Verb::Dedent))) => { - break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)) + break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)); } _ => {} } @@ -1652,8 +1651,7 @@ impl ViVisual { } }; - if chars.peek().is_some() { - } + if chars.peek().is_some() {} let verb_ref = verb.as_ref().map(|v| &v.1); let motion_ref = motion.as_ref().map(|m| &m.1); diff --git a/src/shopt.rs b/src/shopt.rs index b96b51f..704df86 100644 --- a/src/shopt.rs +++ b/src/shopt.rs @@ -117,7 +117,7 @@ impl ShOpts { Note::new("'shopt' takes arguments separated by periods to denote namespaces") .with_sub_notes(vec!["Example: 'shopt core.autocd=true'"]), ), - ) + ); } } Ok(()) @@ -263,7 +263,7 @@ impl ShOptCore { "max_recurse_depth", ]), ), - ) + ); } } Ok(()) @@ -445,18 +445,20 @@ impl ShOptPrompt { ShErrKind::SyntaxErr, format!("shopt: Unexpected 'prompt' option '{opt}'"), ) - .with_note(Note::new("options can be accessed like 'prompt.option_name'")) + .with_note(Note::new( + "options can be accessed like 'prompt.option_name'", + )) .with_note( Note::new("'prompt' contains the following options").with_sub_notes(vec![ - "trunc_prompt_path", - "edit_mode", - "comp_limit", - "highlight", - "tab_stop", - "custom", + "trunc_prompt_path", + "edit_mode", + "comp_limit", + "highlight", + "tab_stop", + "custom", ]), ), - ) + ); } } Ok(()) diff --git a/src/signal.rs b/src/signal.rs index 63c1740..dda561e 100644 --- a/src/signal.rs +++ b/src/signal.rs @@ -3,7 +3,12 @@ use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering}; use nix::sys::signal::{SaFlags, SigAction, sigaction}; use crate::{ - builtin::trap::TrapTarget, jobs::{JobCmdFlags, JobID, take_term}, libsh::error::{ShErr, ShErrKind, ShResult}, parse::execute::exec_input, prelude::*, state::{read_jobs, read_logic, write_jobs, write_meta} + builtin::trap::TrapTarget, + jobs::{JobCmdFlags, JobID, take_term}, + libsh::error::{ShErr, ShErrKind, ShResult}, + parse::execute::exec_input, + prelude::*, + state::{read_jobs, read_logic, write_jobs, write_meta}, }; static SIGNALS: AtomicU64 = AtomicU64::new(0); @@ -12,92 +17,91 @@ pub static REAPING_ENABLED: AtomicBool = AtomicBool::new(true); pub static SHOULD_QUIT: AtomicBool = AtomicBool::new(false); pub static QUIT_CODE: AtomicI32 = AtomicI32::new(0); -const MISC_SIGNALS: [Signal;22] = [ - Signal::SIGILL, - Signal::SIGTRAP, - Signal::SIGABRT, - Signal::SIGBUS, - Signal::SIGFPE, - Signal::SIGUSR1, - Signal::SIGSEGV, - Signal::SIGUSR2, - Signal::SIGPIPE, - Signal::SIGALRM, - Signal::SIGTERM, - Signal::SIGSTKFLT, - Signal::SIGCONT, - Signal::SIGURG, - Signal::SIGXCPU, - Signal::SIGXFSZ, - Signal::SIGVTALRM, - Signal::SIGPROF, - Signal::SIGWINCH, - Signal::SIGIO, - Signal::SIGPWR, - Signal::SIGSYS, +const MISC_SIGNALS: [Signal; 22] = [ + Signal::SIGILL, + Signal::SIGTRAP, + Signal::SIGABRT, + Signal::SIGBUS, + Signal::SIGFPE, + Signal::SIGUSR1, + Signal::SIGSEGV, + Signal::SIGUSR2, + Signal::SIGPIPE, + Signal::SIGALRM, + Signal::SIGTERM, + Signal::SIGSTKFLT, + Signal::SIGCONT, + Signal::SIGURG, + Signal::SIGXCPU, + Signal::SIGXFSZ, + Signal::SIGVTALRM, + Signal::SIGPROF, + Signal::SIGWINCH, + Signal::SIGIO, + Signal::SIGPWR, + Signal::SIGSYS, ]; pub fn signals_pending() -> bool { - SIGNALS.load(Ordering::SeqCst) != 0 || SHOULD_QUIT.load(Ordering::SeqCst) + SIGNALS.load(Ordering::SeqCst) != 0 || SHOULD_QUIT.load(Ordering::SeqCst) } pub fn check_signals() -> ShResult<()> { - let pending = SIGNALS.swap(0, Ordering::SeqCst); - let got_signal = |sig: Signal| -> bool { pending & (1 << sig as u64) != 0 }; - let run_trap = |sig: Signal| -> ShResult<()> { - if let Some(command) = read_logic(|l| l.get_trap(TrapTarget::Signal(sig))) { - exec_input(command, None, false)?; - } - Ok(()) - }; + let pending = SIGNALS.swap(0, Ordering::SeqCst); + let got_signal = |sig: Signal| -> bool { pending & (1 << sig as u64) != 0 }; + let run_trap = |sig: Signal| -> ShResult<()> { + if let Some(command) = read_logic(|l| l.get_trap(TrapTarget::Signal(sig))) { + exec_input(command, None, false)?; + } + Ok(()) + }; - if got_signal(Signal::SIGINT) { - interrupt()?; - run_trap(Signal::SIGINT)?; - return Err(ShErr::simple(ShErrKind::ClearReadline, "")); - } - if got_signal(Signal::SIGHUP) { - run_trap(Signal::SIGHUP)?; - hang_up(0); - } - if got_signal(Signal::SIGQUIT) { - run_trap(Signal::SIGQUIT)?; - hang_up(0); - } - if got_signal(Signal::SIGTSTP) { - run_trap(Signal::SIGTSTP)?; - terminal_stop()?; - } - if got_signal(Signal::SIGCHLD) && REAPING_ENABLED.load(Ordering::SeqCst) { - run_trap(Signal::SIGCHLD)?; - wait_child()?; - } + if got_signal(Signal::SIGINT) { + interrupt()?; + run_trap(Signal::SIGINT)?; + return Err(ShErr::simple(ShErrKind::ClearReadline, "")); + } + if got_signal(Signal::SIGHUP) { + run_trap(Signal::SIGHUP)?; + hang_up(0); + } + if got_signal(Signal::SIGQUIT) { + run_trap(Signal::SIGQUIT)?; + hang_up(0); + } + if got_signal(Signal::SIGTSTP) { + run_trap(Signal::SIGTSTP)?; + terminal_stop()?; + } + if got_signal(Signal::SIGCHLD) && REAPING_ENABLED.load(Ordering::SeqCst) { + run_trap(Signal::SIGCHLD)?; + wait_child()?; + } - for sig in MISC_SIGNALS { - if got_signal(sig) { - run_trap(sig)?; - } - } + for sig in MISC_SIGNALS { + if got_signal(sig) { + run_trap(sig)?; + } + } - if SHOULD_QUIT.load(Ordering::SeqCst) { - let code = QUIT_CODE.load(Ordering::SeqCst); - return Err(ShErr::simple(ShErrKind::CleanExit(code), "exit")); - } - Ok(()) + if SHOULD_QUIT.load(Ordering::SeqCst) { + let code = QUIT_CODE.load(Ordering::SeqCst); + return Err(ShErr::simple(ShErrKind::CleanExit(code), "exit")); + } + Ok(()) } pub fn disable_reaping() { - REAPING_ENABLED.store(false, Ordering::SeqCst); + REAPING_ENABLED.store(false, Ordering::SeqCst); } pub fn enable_reaping() { - REAPING_ENABLED.store(true, Ordering::SeqCst); + REAPING_ENABLED.store(true, Ordering::SeqCst); } pub fn sig_setup() { - let flags = SaFlags::empty(); - - let action = SigAction::new(SigHandler::Handler(handle_signal), flags, SigSet::empty()); + let flags = SaFlags::empty(); + let action = SigAction::new(SigHandler::Handler(handle_signal), flags, SigSet::empty()); let ignore = SigAction::new(SigHandler::SigIgn, flags, SigSet::empty()); @@ -136,12 +140,12 @@ pub fn sig_setup() { } extern "C" fn handle_signal(sig: libc::c_int) { - SIGNALS.fetch_or(1 << sig, Ordering::SeqCst); + SIGNALS.fetch_or(1 << sig, Ordering::SeqCst); } pub fn hang_up(_: libc::c_int) { - SHOULD_QUIT.store(true, Ordering::SeqCst); - QUIT_CODE.store(1, Ordering::SeqCst); + SHOULD_QUIT.store(true, Ordering::SeqCst); + QUIT_CODE.store(1, Ordering::SeqCst); write_jobs(|j| { for job in j.jobs_mut().iter_mut().flatten() { job.killpg(Signal::SIGTERM).ok(); @@ -154,10 +158,10 @@ pub fn terminal_stop() -> ShResult<()> { if let Some(job) = j.get_fg_mut() { job.killpg(Signal::SIGTSTP) } else { - Ok(()) - } + Ok(()) + } }) - // TODO: It seems like there is supposed to be a take_term() call here + // TODO: It seems like there is supposed to be a take_term() call here } pub fn interrupt() -> ShResult<()> { @@ -269,19 +273,19 @@ pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> { } else { None } - }) - && is_finished { - if is_fg { - take_term()?; - } else { - println!(); - let job_order = read_jobs(|j| j.order().to_vec()); - let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned()); - if let Some(job) = result { - let job_complete_msg = job.display(&job_order, JobCmdFlags::PIDS).to_string(); - write_meta(|m| m.post_system_message(job_complete_msg)) - } + }) && is_finished + { + if is_fg { + take_term()?; + } else { + println!(); + let job_order = read_jobs(|j| j.order().to_vec()); + let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned()); + if let Some(job) = result { + let job_complete_msg = job.display(&job_order, JobCmdFlags::PIDS).to_string(); + write_meta(|m| m.post_system_message(job_complete_msg)) } } + } Ok(()) } diff --git a/src/state.rs b/src/state.rs index b8e5974..a29b63d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,249 +1,262 @@ use std::{ - cell::RefCell, collections::{HashMap, VecDeque}, fmt::Display, ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref}, str::FromStr, time::Duration + cell::RefCell, + collections::{HashMap, VecDeque}, + fmt::Display, + ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref}, + str::FromStr, + time::Duration, }; -use nix::unistd::{gethostname, getppid, User}; +use nix::unistd::{User, gethostname, getppid}; use crate::{ - builtin::trap::TrapTarget, exec_input, jobs::JobTab, libsh::{ + builtin::trap::TrapTarget, + exec_input, + jobs::JobTab, + libsh::{ error::{ShErr, ShErrKind, ShResult}, utils::VecDequeExt, - }, parse::{ConjunctNode, NdRule, Node, ParsedSrc}, prelude::*, shopt::ShOpts + }, + parse::{ConjunctNode, NdRule, Node, ParsedSrc}, + prelude::*, + shopt::ShOpts, }; pub struct Fern { - pub jobs: RefCell, - pub var_scopes: RefCell, - pub meta: RefCell, - pub logic: RefCell, - pub shopts: RefCell, + pub jobs: RefCell, + pub var_scopes: RefCell, + pub meta: RefCell, + pub logic: RefCell, + pub shopts: RefCell, } impl Fern { - pub fn new() -> Self { - Self { - jobs: RefCell::new(JobTab::new()), - var_scopes: RefCell::new(ScopeStack::new()), - meta: RefCell::new(MetaTab::new()), - logic: RefCell::new(LogTab::new()), - shopts: RefCell::new(ShOpts::default()), - } - } + pub fn new() -> Self { + Self { + jobs: RefCell::new(JobTab::new()), + var_scopes: RefCell::new(ScopeStack::new()), + meta: RefCell::new(MetaTab::new()), + logic: RefCell::new(LogTab::new()), + shopts: RefCell::new(ShOpts::default()), + } + } } impl Default for Fern { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } #[derive(Hash, Eq, PartialEq, Debug, Clone, Copy)] pub enum ShellParam { - // Global - Status, - ShPid, - LastJob, - ShellName, + // Global + Status, + ShPid, + LastJob, + ShellName, - // Local - Pos(usize), - AllArgs, - AllArgsStr, - ArgCount + // Local + Pos(usize), + AllArgs, + AllArgsStr, + ArgCount, } impl ShellParam { - pub fn is_global(&self) -> bool { - matches!( - self, - Self::Status | Self::ShPid | Self::LastJob | Self::ShellName - ) - } + pub fn is_global(&self) -> bool { + matches!( + self, + Self::Status | Self::ShPid | Self::LastJob | Self::ShellName + ) + } } impl Display for ShellParam { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Status => write!(f, "?"), - Self::ShPid => write!(f, "$"), - Self::LastJob => write!(f, "!"), - Self::ShellName => write!(f, "0"), - Self::Pos(n) => write!(f, "{}", n), - Self::AllArgs => write!(f, "@"), - Self::AllArgsStr => write!(f, "*"), - Self::ArgCount => write!(f, "#"), - } - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Status => write!(f, "?"), + Self::ShPid => write!(f, "$"), + Self::LastJob => write!(f, "!"), + Self::ShellName => write!(f, "0"), + Self::Pos(n) => write!(f, "{}", n), + Self::AllArgs => write!(f, "@"), + Self::AllArgsStr => write!(f, "*"), + Self::ArgCount => write!(f, "#"), + } + } } impl FromStr for ShellParam { - type Err = ShErr; - fn from_str(s: &str) -> Result { - match s { - "?" => Ok(Self::Status), - "$" => Ok(Self::ShPid), - "!" => Ok(Self::LastJob), - "0" => Ok(Self::ShellName), - "@" => Ok(Self::AllArgs), - "*" => Ok(Self::AllArgsStr), - "#" => Ok(Self::ArgCount), - n if n.parse::().is_ok() => { - let idx = n.parse::().unwrap(); - Ok(Self::Pos(idx)) - } - _ => Err(ShErr::simple( - ShErrKind::InternalErr, - format!("Invalid shell parameter: {}", s), - )), - } - } + type Err = ShErr; + fn from_str(s: &str) -> Result { + match s { + "?" => Ok(Self::Status), + "$" => Ok(Self::ShPid), + "!" => Ok(Self::LastJob), + "0" => Ok(Self::ShellName), + "@" => Ok(Self::AllArgs), + "*" => Ok(Self::AllArgsStr), + "#" => Ok(Self::ArgCount), + n if n.parse::().is_ok() => { + let idx = n.parse::().unwrap(); + Ok(Self::Pos(idx)) + } + _ => Err(ShErr::simple( + ShErrKind::InternalErr, + format!("Invalid shell parameter: {}", s), + )), + } + } } #[derive(Clone, Default, Debug)] pub struct ScopeStack { - // ALWAYS keep one scope. - // The bottom scope is the global variable space. - // Scopes that come after that are pushed in functions, - // and only contain variables that are defined using `local`. - scopes: Vec, - depth: u32, + // ALWAYS keep one scope. + // The bottom scope is the global variable space. + // Scopes that come after that are pushed in functions, + // and only contain variables that are defined using `local`. + scopes: Vec, + depth: u32, - // Global parameters such as $?, $!, $$, etc - global_params: HashMap, + // Global parameters such as $?, $!, $$, etc + global_params: HashMap, } impl ScopeStack { - pub fn new() -> Self { - let mut new = Self::default(); - new.scopes.push(VarTab::new()); - let shell_name = std::env::args().next().unwrap_or_else(|| "fern".to_string()); - new.global_params.insert(ShellParam::ShellName.to_string(), shell_name); - new - } - pub fn descend(&mut self, argv: Option>) { - let mut new_vars = VarTab::new(); - if let Some(argv) = argv { - for arg in argv { - new_vars.bpush_arg(arg); - } - } - self.scopes.push(new_vars); - self.depth += 1; - } - pub fn ascend(&mut self) { - if self.depth >= 1 { - self.scopes.pop(); - self.depth -= 1; - } - } - pub fn cur_scope(&self) -> &VarTab { - self.scopes.last().unwrap() - } - pub fn cur_scope_mut(&mut self) -> &mut VarTab { - self.scopes.last_mut().unwrap() - } - pub fn unset_var(&mut self, var_name: &str) { - for scope in self.scopes.iter_mut().rev() { - if scope.var_exists(var_name) { - scope.unset_var(var_name); - return; - } - } - } - pub fn export_var(&mut self, var_name: &str) { - for scope in self.scopes.iter_mut().rev() { - if scope.var_exists(var_name) { - scope.export_var(var_name); - return; - } - } - } - pub fn var_exists(&self, var_name: &str) -> bool { - for scope in self.scopes.iter().rev() { - if scope.var_exists(var_name) { - return true; - } - } - if let Ok(param) = var_name.parse::() { - return self.global_params.contains_key(¶m.to_string()); - } - false - } - pub fn flatten_vars(&self) -> HashMap { - let mut flat_vars = HashMap::new(); - for scope in self.scopes.iter() { - for (var_name, var) in scope.vars() { - flat_vars.insert(var_name.clone(), var.clone()); - } - } - flat_vars - } - pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) { - if flags.contains(VarFlags::LOCAL) { - self.set_var_local(var_name, val, flags); - } else { - self.set_var_global(var_name, val, flags); - } - } - fn set_var_global(&mut self, var_name: &str, val: &str, flags: VarFlags) { - if let Some(scope) = self.scopes.first_mut() { - scope.set_var(var_name, val, flags); - } - } - fn set_var_local(&mut self, var_name: &str, val: &str, flags: VarFlags) { - if let Some(scope) = self.scopes.last_mut() { - scope.set_var(var_name, val, flags); - } - } - pub fn get_var(&self, var_name: &str) -> String { - if let Ok(param) = var_name.parse::() { - return self.get_param(param); - } - for scope in self.scopes.iter().rev() { - if scope.var_exists(var_name) { - return scope.get_var(var_name); - } - } - // Fallback to env var - std::env::var(var_name).unwrap_or_default() - } - pub fn get_param(&self, param: ShellParam) -> String { - if param.is_global() && let Some(val) = self.global_params.get(¶m.to_string()) { - return val.clone(); - } - for scope in self.scopes.iter().rev() { - let val = scope.get_param(param); - if !val.is_empty() { - return val; - } - } - // Fallback to empty string - "".into() - } - /// Set a shell parameter - /// Therefore, these are global state and we use the global scope - pub fn set_param(&mut self, param: ShellParam, val: &str) { - match param { - ShellParam::ShPid | - ShellParam::Status | - ShellParam::LastJob | - ShellParam::ShellName => { - self.global_params.insert(param.to_string(), val.to_string()); - } - ShellParam::Pos(_) | - ShellParam::AllArgs | - ShellParam::AllArgsStr | - ShellParam::ArgCount => { - if let Some(scope) = self.scopes.first_mut() { - scope.set_param(param, val); - } - } - } - } + pub fn new() -> Self { + let mut new = Self::default(); + new.scopes.push(VarTab::new()); + let shell_name = std::env::args() + .next() + .unwrap_or_else(|| "fern".to_string()); + new + .global_params + .insert(ShellParam::ShellName.to_string(), shell_name); + new + } + pub fn descend(&mut self, argv: Option>) { + let mut new_vars = VarTab::new(); + if let Some(argv) = argv { + for arg in argv { + new_vars.bpush_arg(arg); + } + } + self.scopes.push(new_vars); + self.depth += 1; + } + pub fn ascend(&mut self) { + if self.depth >= 1 { + self.scopes.pop(); + self.depth -= 1; + } + } + pub fn cur_scope(&self) -> &VarTab { + self.scopes.last().unwrap() + } + pub fn cur_scope_mut(&mut self) -> &mut VarTab { + self.scopes.last_mut().unwrap() + } + pub fn unset_var(&mut self, var_name: &str) { + for scope in self.scopes.iter_mut().rev() { + if scope.var_exists(var_name) { + scope.unset_var(var_name); + return; + } + } + } + pub fn export_var(&mut self, var_name: &str) { + for scope in self.scopes.iter_mut().rev() { + if scope.var_exists(var_name) { + scope.export_var(var_name); + return; + } + } + } + pub fn var_exists(&self, var_name: &str) -> bool { + for scope in self.scopes.iter().rev() { + if scope.var_exists(var_name) { + return true; + } + } + if let Ok(param) = var_name.parse::() { + return self.global_params.contains_key(¶m.to_string()); + } + false + } + pub fn flatten_vars(&self) -> HashMap { + let mut flat_vars = HashMap::new(); + for scope in self.scopes.iter() { + for (var_name, var) in scope.vars() { + flat_vars.insert(var_name.clone(), var.clone()); + } + } + flat_vars + } + pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) { + if flags.contains(VarFlags::LOCAL) { + self.set_var_local(var_name, val, flags); + } else { + self.set_var_global(var_name, val, flags); + } + } + fn set_var_global(&mut self, var_name: &str, val: &str, flags: VarFlags) { + if let Some(scope) = self.scopes.first_mut() { + scope.set_var(var_name, val, flags); + } + } + fn set_var_local(&mut self, var_name: &str, val: &str, flags: VarFlags) { + if let Some(scope) = self.scopes.last_mut() { + scope.set_var(var_name, val, flags); + } + } + pub fn get_var(&self, var_name: &str) -> String { + if let Ok(param) = var_name.parse::() { + return self.get_param(param); + } + for scope in self.scopes.iter().rev() { + if scope.var_exists(var_name) { + return scope.get_var(var_name); + } + } + // Fallback to env var + std::env::var(var_name).unwrap_or_default() + } + pub fn get_param(&self, param: ShellParam) -> String { + if param.is_global() + && let Some(val) = self.global_params.get(¶m.to_string()) + { + return val.clone(); + } + for scope in self.scopes.iter().rev() { + let val = scope.get_param(param); + if !val.is_empty() { + return val; + } + } + // Fallback to empty string + "".into() + } + /// Set a shell parameter + /// Therefore, these are global state and we use the global scope + pub fn set_param(&mut self, param: ShellParam, val: &str) { + match param { + ShellParam::ShPid | ShellParam::Status | ShellParam::LastJob | ShellParam::ShellName => { + self + .global_params + .insert(param.to_string(), val.to_string()); + } + ShellParam::Pos(_) | ShellParam::AllArgs | ShellParam::AllArgsStr | ShellParam::ArgCount => { + if let Some(scope) = self.scopes.first_mut() { + scope.set_param(param, val); + } + } + } + } } thread_local! { - pub static FERN: Fern = Fern::new(); + pub static FERN: Fern = Fern::new(); } /// A shell function @@ -287,7 +300,7 @@ impl Deref for ShFunc { pub struct LogTab { functions: HashMap, aliases: HashMap, - traps: HashMap, + traps: HashMap, } impl LogTab { @@ -297,18 +310,18 @@ impl LogTab { pub fn insert_func(&mut self, name: &str, src: ShFunc) { self.functions.insert(name.into(), src); } - pub fn insert_trap(&mut self, target: TrapTarget, command: String) { - self.traps.insert(target, command); - } - pub fn get_trap(&self, target: TrapTarget) -> Option { - self.traps.get(&target).cloned() - } - pub fn remove_trap(&mut self, target: TrapTarget) { - self.traps.remove(&target); - } - pub fn traps(&self) -> &HashMap { - &self.traps - } + pub fn insert_trap(&mut self, target: TrapTarget, command: String) { + self.traps.insert(target, command); + } + pub fn get_trap(&self, target: TrapTarget) -> Option { + self.traps.get(&target).cloned() + } + pub fn remove_trap(&mut self, target: TrapTarget) { + self.traps.remove(&target); + } + pub fn traps(&self) -> &HashMap { + &self.traps + } pub fn get_func(&self, name: &str) -> Option { self.functions.get(name).cloned() } @@ -339,103 +352,103 @@ impl LogTab { pub struct VarFlags(u8); impl VarFlags { - pub const NONE : Self = Self(0); - pub const EXPORT : Self = Self(1 << 0); - pub const LOCAL : Self = Self(1 << 1); - pub const READONLY : Self = Self(1 << 2); + pub const NONE: Self = Self(0); + pub const EXPORT: Self = Self(1 << 0); + pub const LOCAL: Self = Self(1 << 1); + pub const READONLY: Self = Self(1 << 2); } impl BitOr for VarFlags { - type Output = Self; - fn bitor(self, rhs: Self) -> Self::Output { - Self(self.0 | rhs.0) - } + type Output = Self; + fn bitor(self, rhs: Self) -> Self::Output { + Self(self.0 | rhs.0) + } } impl BitOrAssign for VarFlags { - fn bitor_assign(&mut self, rhs: Self) { - self.0 |= rhs.0; - } + fn bitor_assign(&mut self, rhs: Self) { + self.0 |= rhs.0; + } } impl BitAnd for VarFlags { - type Output = Self; - fn bitand(self, rhs: Self) -> Self::Output { - Self(self.0 & rhs.0) - } + type Output = Self; + fn bitand(self, rhs: Self) -> Self::Output { + Self(self.0 & rhs.0) + } } impl BitAndAssign for VarFlags { - fn bitand_assign(&mut self, rhs: Self) { - self.0 &= rhs.0; - } + fn bitand_assign(&mut self, rhs: Self) { + self.0 &= rhs.0; + } } impl VarFlags { - pub fn contains(&self, flag: Self) -> bool { - (self.0 & flag.0) == flag.0 - } - pub fn intersects(&self, flag: Self) -> bool { - (self.0 & flag.0) != 0 - } - pub fn is_empty(&self) -> bool { - self.0 == 0 - } + pub fn contains(&self, flag: Self) -> bool { + (self.0 & flag.0) == flag.0 + } + pub fn intersects(&self, flag: Self) -> bool { + (self.0 & flag.0) != 0 + } + pub fn is_empty(&self) -> bool { + self.0 == 0 + } - pub fn insert(&mut self, flag: Self) { - self.0 |= flag.0; - } - pub fn remove(&mut self, flag: Self) { - self.0 &= !flag.0; - } - pub fn toggle(&mut self, flag: Self) { - self.0 ^= flag.0; - } - pub fn set(&mut self, flag: Self, value: bool) { - if value { - self.insert(flag); - } else { - self.remove(flag); - } - } + pub fn insert(&mut self, flag: Self) { + self.0 |= flag.0; + } + pub fn remove(&mut self, flag: Self) { + self.0 &= !flag.0; + } + pub fn toggle(&mut self, flag: Self) { + self.0 ^= flag.0; + } + pub fn set(&mut self, flag: Self, value: bool) { + if value { + self.insert(flag); + } else { + self.remove(flag); + } + } } #[derive(Clone, Debug)] pub enum VarKind { - Str(String), - Int(i32), - Arr(Vec), - AssocArr(Vec<(String, String)>), + Str(String), + Int(i32), + Arr(Vec), + AssocArr(Vec<(String, String)>), } impl Display for VarKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - VarKind::Str(s) => write!(f, "{s}"), - VarKind::Int(i) => write!(f, "{i}"), - VarKind::Arr(items) => { - let mut item_iter = items.iter().peekable(); - while let Some(item) = item_iter.next() { - write!(f, "{item}")?; - if item_iter.peek().is_some() { - write!(f, " ")?; - } - } - Ok(()) - } - VarKind::AssocArr(items) => { - let mut item_iter = items.iter().peekable(); - while let Some(item) = item_iter.next() { - let (k,v) = item; - write!(f, "{k}={v}")?; - if item_iter.peek().is_some() { - write!(f, " ")?; - } - } - Ok(()) - } - } - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + VarKind::Str(s) => write!(f, "{s}"), + VarKind::Int(i) => write!(f, "{i}"), + VarKind::Arr(items) => { + let mut item_iter = items.iter().peekable(); + while let Some(item) = item_iter.next() { + write!(f, "{item}")?; + if item_iter.peek().is_some() { + write!(f, " ")?; + } + } + Ok(()) + } + VarKind::AssocArr(items) => { + let mut item_iter = items.iter().peekable(); + while let Some(item) = item_iter.next() { + let (k, v) = item; + write!(f, "{k}={v}")?; + if item_iter.peek().is_some() { + write!(f, " ")?; + } + } + Ok(()) + } + } + } } #[derive(Clone, Debug)] @@ -446,26 +459,23 @@ pub struct Var { impl Var { pub fn new(kind: VarKind, flags: VarFlags) -> Self { - Self { - flags, - kind - } + Self { flags, kind } + } + pub fn kind(&self) -> &VarKind { + &self.kind + } + pub fn kind_mut(&mut self) -> &mut VarKind { + &mut self.kind } - pub fn kind(&self) -> &VarKind { - &self.kind - } - pub fn kind_mut(&mut self) -> &mut VarKind { - &mut self.kind - } pub fn mark_for_export(&mut self) { self.flags.set(VarFlags::EXPORT, true); } } impl Display for Var { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.kind.fmt(f) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.kind.fmt(f) + } } #[derive(Default, Clone, Debug)] @@ -528,23 +538,23 @@ impl VarTab { .map(|hname| hname.to_string_lossy().to_string()) .unwrap_or_default(); - unsafe { - env::set_var("IFS", " \t\n"); - env::set_var("HOST", hostname.clone()); - env::set_var("UID", uid.to_string()); - env::set_var("PPID", getppid().to_string()); - env::set_var("TMPDIR", "/tmp"); - env::set_var("TERM", term); - env::set_var("LANG", "en_US.UTF-8"); - env::set_var("USER", username.clone()); - env::set_var("LOGNAME", username); - env::set_var("PWD", pathbuf_to_string(std::env::current_dir())); - env::set_var("OLDPWD", pathbuf_to_string(std::env::current_dir())); - env::set_var("HOME", home.clone()); - env::set_var("SHELL", pathbuf_to_string(std::env::current_exe())); - env::set_var("FERN_HIST", format!("{}/.fernhist", home)); - env::set_var("FERN_RC", format!("{}/.fernrc", home)); - } + unsafe { + env::set_var("IFS", " \t\n"); + env::set_var("HOST", hostname.clone()); + env::set_var("UID", uid.to_string()); + env::set_var("PPID", getppid().to_string()); + env::set_var("TMPDIR", "/tmp"); + env::set_var("TERM", term); + env::set_var("LANG", "en_US.UTF-8"); + env::set_var("USER", username.clone()); + env::set_var("LOGNAME", username); + env::set_var("PWD", pathbuf_to_string(std::env::current_dir())); + env::set_var("OLDPWD", pathbuf_to_string(std::env::current_dir())); + env::set_var("HOME", home.clone()); + env::set_var("SHELL", pathbuf_to_string(std::env::current_exe())); + env::set_var("FERN_HIST", format!("{}/.fernhist", home)); + env::set_var("FERN_RC", format!("{}/.fernrc", home)); + } } pub fn init_sh_argv(&mut self) { for arg in env::args() { @@ -575,7 +585,10 @@ impl VarTab { self.bpush_arg(env::current_exe().unwrap().to_str().unwrap().to_string()); } fn update_arg_params(&mut self) { - self.set_param(ShellParam::AllArgs, &self.sh_argv.clone().to_vec()[1..].join(" ")); + self.set_param( + ShellParam::AllArgs, + &self.sh_argv.clone().to_vec()[1..].join(" "), + ); self.set_param(ShellParam::ArgCount, &(self.sh_argv.len() - 1).to_string()); } /// Push an arg to the front of the arg deque @@ -619,29 +632,29 @@ impl VarTab { } } pub fn get_var(&self, var: &str) -> String { - if let Ok(param) = var.parse::() { + if let Ok(param) = var.parse::() { let param = self.get_param(param); if !param.is_empty() { return param; } - } + } if let Some(var) = self.vars.get(var).map(|s| s.to_string()) { var } else { std::env::var(var).unwrap_or_default() } } - pub fn unset_var(&mut self, var_name: &str) { - self.vars.remove(var_name); - unsafe { env::remove_var(var_name) }; - } + pub fn unset_var(&mut self, var_name: &str) { + self.vars.remove(var_name); + unsafe { env::remove_var(var_name) }; + } pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) { if let Some(var) = self.vars.get_mut(var_name) { var.kind = VarKind::Str(val.to_string()); if var.flags.contains(VarFlags::EXPORT) || flags.contains(VarFlags::EXPORT) { - if flags.contains(VarFlags::EXPORT) && !var.flags.contains(VarFlags::EXPORT) { - var.mark_for_export(); - } + if flags.contains(VarFlags::EXPORT) && !var.flags.contains(VarFlags::EXPORT) { + var.mark_for_export(); + } unsafe { env::set_var(var_name, val) }; } } else { @@ -663,39 +676,35 @@ impl VarTab { self.params.insert(param, val.to_string()); } pub fn get_param(&self, param: ShellParam) -> String { - match param { - ShellParam::Pos(n) => { - self - .sh_argv() - .get(n) - .map(|s| s.to_string()) - .unwrap_or_default() - } - ShellParam::Status => { - self - .params - .get(&ShellParam::Status) - .map(|s| s.to_string()) - .unwrap_or("0".into()) - } - _ => self - .params - .get(¶m) - .map(|s| s.to_string()) - .unwrap_or_default(), - } + match param { + ShellParam::Pos(n) => self + .sh_argv() + .get(n) + .map(|s| s.to_string()) + .unwrap_or_default(), + ShellParam::Status => self + .params + .get(&ShellParam::Status) + .map(|s| s.to_string()) + .unwrap_or("0".into()), + _ => self + .params + .get(¶m) + .map(|s| s.to_string()) + .unwrap_or_default(), + } } } /// A table of metadata for the shell #[derive(Default, Debug)] pub struct MetaTab { - // command running duration + // command running duration runtime_start: Option, - runtime_stop: Option, + runtime_stop: Option, - // pending system messages - system_msg: Vec + // pending system messages + system_msg: Vec, } impl MetaTab { @@ -708,76 +717,76 @@ impl MetaTab { pub fn stop_timer(&mut self) { self.runtime_stop = Some(Instant::now()); } - pub fn get_time(&self) -> Option { - if let (Some(start), Some(stop)) = (self.runtime_start, self.runtime_stop) { - Some(stop.duration_since(start)) - } else { - None - } - } - pub fn post_system_message(&mut self, message: String) { - self.system_msg.push(message); - } - pub fn pop_system_message(&mut self) -> Option { - self.system_msg.pop() - } - pub fn system_msg_pending(&self) -> bool { - !self.system_msg.is_empty() - } + pub fn get_time(&self) -> Option { + if let (Some(start), Some(stop)) = (self.runtime_start, self.runtime_stop) { + Some(stop.duration_since(start)) + } else { + None + } + } + pub fn post_system_message(&mut self, message: String) { + self.system_msg.push(message); + } + pub fn pop_system_message(&mut self) -> Option { + self.system_msg.pop() + } + pub fn system_msg_pending(&self) -> bool { + !self.system_msg.is_empty() + } } /// Read from the job table pub fn read_jobs T>(f: F) -> T { - FERN.with(|fern| f(&fern.jobs.borrow())) + FERN.with(|fern| f(&fern.jobs.borrow())) } /// Write to the job table pub fn write_jobs T>(f: F) -> T { - FERN.with(|fern| f(&mut fern.jobs.borrow_mut())) + FERN.with(|fern| f(&mut fern.jobs.borrow_mut())) } /// Read from the var scope stack pub fn read_vars T>(f: F) -> T { - FERN.with(|fern| f(&fern.var_scopes.borrow())) + FERN.with(|fern| f(&fern.var_scopes.borrow())) } /// Write to the variable table pub fn write_vars T>(f: F) -> T { - FERN.with(|fern| f(&mut fern.var_scopes.borrow_mut())) + FERN.with(|fern| f(&mut fern.var_scopes.borrow_mut())) } pub fn read_meta T>(f: F) -> T { - FERN.with(|fern| f(&fern.meta.borrow())) + FERN.with(|fern| f(&fern.meta.borrow())) } /// Write to the meta table pub fn write_meta T>(f: F) -> T { - FERN.with(|fern| f(&mut fern.meta.borrow_mut())) + FERN.with(|fern| f(&mut fern.meta.borrow_mut())) } /// Read from the logic table pub fn read_logic T>(f: F) -> T { - FERN.with(|fern| f(&fern.logic.borrow())) + FERN.with(|fern| f(&fern.logic.borrow())) } /// Write to the logic table pub fn write_logic T>(f: F) -> T { - FERN.with(|fern| f(&mut fern.logic.borrow_mut())) + FERN.with(|fern| f(&mut fern.logic.borrow_mut())) } pub fn read_shopts T>(f: F) -> T { - FERN.with(|fern| f(&fern.shopts.borrow())) + FERN.with(|fern| f(&fern.shopts.borrow())) } pub fn write_shopts T>(f: F) -> T { - FERN.with(|fern| f(&mut fern.shopts.borrow_mut())) + FERN.with(|fern| f(&mut fern.shopts.borrow_mut())) } pub fn descend_scope(argv: Option>) { - write_vars(|v| v.descend(argv)); + write_vars(|v| v.descend(argv)); } pub fn ascend_scope() { - write_vars(|v| v.ascend()); + write_vars(|v| v.ascend()); } /// This function is used internally and ideally never sees user input @@ -788,7 +797,9 @@ pub fn get_shopt(path: &str) -> String { } pub fn get_status() -> i32 { - read_vars(|v| v.get_param(ShellParam::Status)).parse::().unwrap() + read_vars(|v| v.get_param(ShellParam::Status)) + .parse::() + .unwrap() } #[track_caller] pub fn set_status(code: i32) { diff --git a/src/tests/complete.rs b/src/tests/complete.rs index 27595eb..965c77e 100644 --- a/src/tests/complete.rs +++ b/src/tests/complete.rs @@ -6,36 +6,36 @@ use tempfile::TempDir; use crate::prompt::readline::complete::Completer; use crate::prompt::readline::markers; -use crate::state::{write_logic, write_vars, VarFlags}; +use crate::state::{VarFlags, write_logic, write_vars}; use super::*; /// Helper to create a temp directory with test files fn setup_test_files() -> TempDir { - let temp_dir = tempfile::tempdir().unwrap(); - let path = temp_dir.path(); + let temp_dir = tempfile::tempdir().unwrap(); + let path = temp_dir.path(); - // Create some test files and directories - fs::write(path.join("file1.txt"), "").unwrap(); - fs::write(path.join("file2.txt"), "").unwrap(); - fs::write(path.join("script.sh"), "").unwrap(); - fs::create_dir(path.join("subdir")).unwrap(); - fs::write(path.join("subdir/nested.txt"), "").unwrap(); - fs::create_dir(path.join("another_dir")).unwrap(); + // Create some test files and directories + fs::write(path.join("file1.txt"), "").unwrap(); + fs::write(path.join("file2.txt"), "").unwrap(); + fs::write(path.join("script.sh"), "").unwrap(); + fs::create_dir(path.join("subdir")).unwrap(); + fs::write(path.join("subdir/nested.txt"), "").unwrap(); + fs::create_dir(path.join("another_dir")).unwrap(); - temp_dir + temp_dir } /// Helper to create a test directory in current dir for relative path tests fn setup_local_test_files() -> TempDir { - let temp_dir = tempfile::tempdir_in(".").unwrap(); - let path = temp_dir.path(); + let temp_dir = tempfile::tempdir_in(".").unwrap(); + let path = temp_dir.path(); - fs::write(path.join("local1.txt"), "").unwrap(); - fs::write(path.join("local2.txt"), "").unwrap(); - fs::create_dir(path.join("localdir")).unwrap(); + fs::write(path.join("local1.txt"), "").unwrap(); + fs::write(path.join("local2.txt"), "").unwrap(); + fs::create_dir(path.join("localdir")).unwrap(); - temp_dir + temp_dir } // ============================================================================ @@ -44,41 +44,42 @@ fn setup_local_test_files() -> TempDir { #[test] fn complete_command_from_path() { - let mut completer = Completer::new(); + let mut completer = Completer::new(); - // Try to complete "ec" - should find "echo" (which is in PATH) - let line = "ec".to_string(); - let cursor_pos = 2; + // Try to complete "ec" - should find "echo" (which is in PATH) + let line = "ec".to_string(); + let cursor_pos = 2; - let result = completer.complete(line, cursor_pos, 1); - assert!(result.is_ok()); - let completed = result.unwrap(); + let result = completer.complete(line, cursor_pos, 1); + assert!(result.is_ok()); + let completed = result.unwrap(); - // Should have found something - assert!(completed.is_some()); - let completed_line = completed.unwrap(); + // Should have found something + assert!(completed.is_some()); + let completed_line = completed.unwrap(); - // Should contain "echo" - assert!(completed_line.starts_with("echo") || completer.candidates.iter().any(|c| c == "echo")); + // Should contain "echo" + assert!(completed_line.starts_with("echo") || completer.candidates.iter().any(|c| c == "echo")); } #[test] fn complete_command_builtin() { - let mut completer = Completer::new(); + let mut completer = Completer::new(); - // Try to complete "ex" - should find "export" builtin - let line = "ex".to_string(); - let cursor_pos = 2; + // Try to complete "ex" - should find "export" builtin + let line = "ex".to_string(); + let cursor_pos = 2; - let result = completer.complete(line, cursor_pos, 1).unwrap(); - assert!(result.is_some()); + let result = completer.complete(line, cursor_pos, 1).unwrap(); + assert!(result.is_some()); - // Check candidates include "export" - assert!(completer.candidates.iter().any(|c| c == "export")); + // Check candidates include "export" + assert!(completer.candidates.iter().any(|c| c == "export")); } -// NOTE: Disabled - ShFunc constructor requires parsed AST which is complex to set up in tests -// TODO: Re-enable once we have a helper to create test functions +// NOTE: Disabled - ShFunc constructor requires parsed AST which is complex to +// set up in tests TODO: Re-enable once we have a helper to create test +// functions /* #[test] fn complete_command_function() { @@ -106,38 +107,38 @@ fn complete_command_function() { #[test] fn complete_command_alias() { - // Add alias outside of completion call to avoid RefCell borrow conflict - write_logic(|l| { - l.insert_alias("ll", "ls -la"); - }); + // Add alias outside of completion call to avoid RefCell borrow conflict + write_logic(|l| { + l.insert_alias("ll", "ls -la"); + }); - let mut completer = Completer::new(); - let line = "l".to_string(); - let cursor_pos = 1; + let mut completer = Completer::new(); + let line = "l".to_string(); + let cursor_pos = 1; - let result = completer.complete(line, cursor_pos, 1).unwrap(); - assert!(result.is_some()); + let result = completer.complete(line, cursor_pos, 1).unwrap(); + assert!(result.is_some()); - // Should find ll and ls - assert!(completer.candidates.iter().any(|c| c == "ll")); + // Should find ll and ls + assert!(completer.candidates.iter().any(|c| c == "ll")); - // Cleanup - write_logic(|l| { - l.clear_aliases(); - }); + // Cleanup + write_logic(|l| { + l.clear_aliases(); + }); } #[test] fn complete_command_no_matches() { - let mut completer = Completer::new(); + let mut completer = Completer::new(); - // Try to complete something that definitely doesn't exist - let line = "xyzabc123notacommand".to_string(); - let cursor_pos = line.len(); + // Try to complete something that definitely doesn't exist + let line = "xyzabc123notacommand".to_string(); + let cursor_pos = line.len(); - let result = completer.complete(line, cursor_pos, 1).unwrap(); - // Should return None when no matches - assert!(result.is_none()); + let result = completer.complete(line, cursor_pos, 1).unwrap(); + // Should return None when no matches + assert!(result.is_none()); } // ============================================================================ @@ -146,120 +147,125 @@ fn complete_command_no_matches() { #[test] fn complete_filename_basic() { - let temp_dir = setup_test_files(); - let path = temp_dir.path(); + let temp_dir = setup_test_files(); + let path = temp_dir.path(); - let mut completer = Completer::new(); - let line = format!("cat {}/fil", path.display()); - let cursor_pos = line.len(); + let mut completer = Completer::new(); + let line = format!("cat {}/fil", path.display()); + let cursor_pos = line.len(); - let result = completer.complete(line, cursor_pos, 1).unwrap(); - assert!(result.is_some()); + let result = completer.complete(line, cursor_pos, 1).unwrap(); + assert!(result.is_some()); - // Should have file1.txt and file2.txt as candidates - assert!(completer.candidates.len() >= 2); - assert!(completer.candidates.iter().any(|c| c.contains("file1.txt"))); - assert!(completer.candidates.iter().any(|c| c.contains("file2.txt"))); + // Should have file1.txt and file2.txt as candidates + assert!(completer.candidates.len() >= 2); + assert!(completer.candidates.iter().any(|c| c.contains("file1.txt"))); + assert!(completer.candidates.iter().any(|c| c.contains("file2.txt"))); } #[test] fn complete_filename_directory() { - let temp_dir = setup_test_files(); - let path = temp_dir.path(); + let temp_dir = setup_test_files(); + let path = temp_dir.path(); - let mut completer = Completer::new(); - let line = format!("cd {}/sub", path.display()); - let cursor_pos = line.len(); + let mut completer = Completer::new(); + let line = format!("cd {}/sub", path.display()); + let cursor_pos = line.len(); - let result = completer.complete(line, cursor_pos, 1).unwrap(); - assert!(result.is_some()); + let result = completer.complete(line, cursor_pos, 1).unwrap(); + assert!(result.is_some()); - // Should find "subdir" - assert!(completer.candidates.iter().any(|c| c.contains("subdir"))); + // Should find "subdir" + assert!(completer.candidates.iter().any(|c| c.contains("subdir"))); } #[test] fn complete_filename_with_slash() { - let temp_dir = setup_test_files(); - let path = temp_dir.path(); + let temp_dir = setup_test_files(); + let path = temp_dir.path(); - let mut completer = Completer::new(); - let line = format!("ls {}/subdir/", path.display()); - let cursor_pos = line.len(); + let mut completer = Completer::new(); + let line = format!("ls {}/subdir/", path.display()); + let cursor_pos = line.len(); - let result = completer.complete(line, cursor_pos, 1).unwrap(); + let result = completer.complete(line, cursor_pos, 1).unwrap(); - // Should complete files in subdir/ - if result.is_some() { - assert!(completer.candidates.iter().any(|c| c.contains("nested.txt"))); - } + // Should complete files in subdir/ + if result.is_some() { + assert!( + completer + .candidates + .iter() + .any(|c| c.contains("nested.txt")) + ); + } } #[test] fn complete_filename_preserves_trailing_slash() { - let temp_dir = setup_test_files(); - let path = temp_dir.path(); + let temp_dir = setup_test_files(); + let path = temp_dir.path(); - let mut completer = Completer::new(); - let line = format!("cd {}/sub", path.display()); - let cursor_pos = line.len(); + let mut completer = Completer::new(); + let line = format!("cd {}/sub", path.display()); + let cursor_pos = line.len(); - let result = completer.complete(line, cursor_pos, 1).unwrap(); - assert!(result.is_some()); + let result = completer.complete(line, cursor_pos, 1).unwrap(); + assert!(result.is_some()); - let completed = result.unwrap(); - // Directory completions should have trailing slash - assert!(completed.ends_with('/')); + let completed = result.unwrap(); + // Directory completions should have trailing slash + assert!(completed.ends_with('/')); } #[test] fn complete_filename_relative_path() { - let _temp_dir = setup_local_test_files(); - let dir_name = _temp_dir.path().file_name().unwrap().to_str().unwrap(); + let _temp_dir = setup_local_test_files(); + let dir_name = _temp_dir.path().file_name().unwrap().to_str().unwrap(); - let mut completer = Completer::new(); - let line = format!("cat {}/local", dir_name); - let cursor_pos = line.len(); + let mut completer = Completer::new(); + let line = format!("cat {}/local", dir_name); + let cursor_pos = line.len(); - let result = completer.complete(line, cursor_pos, 1).unwrap(); + let result = completer.complete(line, cursor_pos, 1).unwrap(); - if result.is_some() { - // Should find local1.txt and local2.txt - assert!(completer.candidates.len() >= 2); - } + if result.is_some() { + // Should find local1.txt and local2.txt + assert!(completer.candidates.len() >= 2); + } } #[test] fn complete_filename_current_dir() { - let mut completer = Completer::new(); + let mut completer = Completer::new(); - // Complete files in current directory - let line = "cat ".to_string(); - let cursor_pos = 4; + // Complete files in current directory + let line = "cat ".to_string(); + let cursor_pos = 4; - let result = completer.complete(line, cursor_pos, 1).unwrap(); + let result = completer.complete(line, cursor_pos, 1).unwrap(); - // Should find something in current dir (at least Cargo.toml should exist) - if result.is_some() { - assert!(!completer.candidates.is_empty()); - } + // Should find something in current dir (at least Cargo.toml should exist) + if result.is_some() { + assert!(!completer.candidates.is_empty()); + } } #[test] fn complete_filename_with_dot_slash() { - let _temp_dir = setup_local_test_files(); - let dir_name = _temp_dir.path().file_name().unwrap().to_str().unwrap(); + let _temp_dir = setup_local_test_files(); + let dir_name = _temp_dir.path().file_name().unwrap().to_str().unwrap(); - let mut completer = Completer::new(); - let line = format!("./{}/local", dir_name); - let cursor_pos = line.len(); + let mut completer = Completer::new(); + let line = format!("./{}/local", dir_name); + let cursor_pos = line.len(); - let result = completer.complete(line, cursor_pos, 1); + let result = completer.complete(line, cursor_pos, 1); - // Should preserve the ./ - if let Ok(Some(completed)) = result { - assert!(completed.starts_with("./")); - } + // Should preserve the ./ + if let Ok(Some(completed)) = result { + assert!(completed.starts_with("./")); + } } // ============================================================================ @@ -268,48 +274,48 @@ fn complete_filename_with_dot_slash() { #[test] fn complete_after_equals_assignment() { - let temp_dir = setup_test_files(); - let path = temp_dir.path(); + let temp_dir = setup_test_files(); + let path = temp_dir.path(); - let mut completer = Completer::new(); - let line = format!("FOO={}/fil", path.display()); - let cursor_pos = line.len(); + let mut completer = Completer::new(); + let line = format!("FOO={}/fil", path.display()); + let cursor_pos = line.len(); - let result = completer.complete(line, cursor_pos, 1).unwrap(); - assert!(result.is_some()); + let result = completer.complete(line, cursor_pos, 1).unwrap(); + assert!(result.is_some()); - // Should complete filenames after = - assert!(completer.candidates.iter().any(|c| c.contains("file"))); + // Should complete filenames after = + assert!(completer.candidates.iter().any(|c| c.contains("file"))); } #[test] fn complete_after_equals_option() { - let temp_dir = setup_test_files(); - let path = temp_dir.path(); + let temp_dir = setup_test_files(); + let path = temp_dir.path(); - let mut completer = Completer::new(); - let line = format!("command --output={}/fil", path.display()); - let cursor_pos = line.len(); + let mut completer = Completer::new(); + let line = format!("command --output={}/fil", path.display()); + let cursor_pos = line.len(); - let result = completer.complete(line, cursor_pos, 1).unwrap(); - assert!(result.is_some()); + let result = completer.complete(line, cursor_pos, 1).unwrap(); + assert!(result.is_some()); - // Should complete filenames after = in option - assert!(completer.candidates.iter().any(|c| c.contains("file"))); + // Should complete filenames after = in option + assert!(completer.candidates.iter().any(|c| c.contains("file"))); } #[test] fn complete_after_equals_empty() { - let mut completer = Completer::new(); - let line = "FOO=".to_string(); - let cursor_pos = 4; + let mut completer = Completer::new(); + let line = "FOO=".to_string(); + let cursor_pos = 4; - let result = completer.complete(line, cursor_pos, 1).unwrap(); + let result = completer.complete(line, cursor_pos, 1).unwrap(); - // Should complete files in current directory when path is empty after = - if result.is_some() { - assert!(!completer.candidates.is_empty()); - } + // Should complete files in current directory when path is empty after = + if result.is_some() { + assert!(!completer.candidates.is_empty()); + } } // ============================================================================ @@ -318,124 +324,175 @@ fn complete_after_equals_empty() { #[test] fn context_detection_command_position() { - let completer = Completer::new(); + let completer = Completer::new(); - // At the beginning - command context - let (ctx, _) = completer.get_completion_context("ech", 3); - assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context at start"); + // At the beginning - command context + let (ctx, _) = completer.get_completion_context("ech", 3); + assert!( + ctx.last() == Some(&markers::COMMAND), + "Should be in command context at start" + ); - // After whitespace - still command if no command yet - let (ctx, _) = completer.get_completion_context(" ech", 5); - assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after whitespace"); + // After whitespace - still command if no command yet + let (ctx, _) = completer.get_completion_context(" ech", 5); + assert!( + ctx.last() == Some(&markers::COMMAND), + "Should be in command context after whitespace" + ); } #[test] fn context_detection_argument_position() { - let completer = Completer::new(); + let completer = Completer::new(); - // After a complete command - argument context - let (ctx, _) = completer.get_completion_context("echo hello", 10); - assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context after command"); + // After a complete command - argument context + let (ctx, _) = completer.get_completion_context("echo hello", 10); + assert!( + ctx.last() != Some(&markers::COMMAND), + "Should be in argument context after command" + ); - let (ctx, _) = completer.get_completion_context("ls -la /tmp", 11); - assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context"); + let (ctx, _) = completer.get_completion_context("ls -la /tmp", 11); + assert!( + ctx.last() != Some(&markers::COMMAND), + "Should be in argument context" + ); } #[test] fn context_detection_nested_command_sub() { - let completer = Completer::new(); + let completer = Completer::new(); - // Inside $() - should be command context - let (ctx, _) = completer.get_completion_context("echo \"$(ech", 11); - assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context inside $()"); + // Inside $() - should be command context + let (ctx, _) = completer.get_completion_context("echo \"$(ech", 11); + assert!( + ctx.last() == Some(&markers::COMMAND), + "Should be in command context inside $()" + ); - // After command in $() - argument context - let (ctx, _) = completer.get_completion_context("echo \"$(echo hell", 17); - assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context inside $()"); + // After command in $() - argument context + let (ctx, _) = completer.get_completion_context("echo \"$(echo hell", 17); + assert!( + ctx.last() != Some(&markers::COMMAND), + "Should be in argument context inside $()" + ); } #[test] fn context_detection_pipe() { - let completer = Completer::new(); + let completer = Completer::new(); - // After pipe - command context - let (ctx, _) = completer.get_completion_context("ls | gre", 8); - assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after pipe"); + // After pipe - command context + let (ctx, _) = completer.get_completion_context("ls | gre", 8); + assert!( + ctx.last() == Some(&markers::COMMAND), + "Should be in command context after pipe" + ); } #[test] fn context_detection_command_sep() { - let completer = Completer::new(); + let completer = Completer::new(); - // After semicolon - command context - let (ctx, _) = completer.get_completion_context("echo foo; l", 11); - assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after semicolon"); + // After semicolon - command context + let (ctx, _) = completer.get_completion_context("echo foo; l", 11); + assert!( + ctx.last() == Some(&markers::COMMAND), + "Should be in command context after semicolon" + ); - // After && - command context - let (ctx, _) = completer.get_completion_context("true && l", 9); - assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after &&"); + // After && - command context + let (ctx, _) = completer.get_completion_context("true && l", 9); + assert!( + ctx.last() == Some(&markers::COMMAND), + "Should be in command context after &&" + ); } #[test] fn context_detection_variable_substitution() { - let completer = Completer::new(); + let completer = Completer::new(); - // $VAR at argument position - VAR_SUB should take priority over ARG - let (ctx, _) = completer.get_completion_context("echo $HOM", 9); - assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "Should be in var_sub context for $HOM"); + // $VAR at argument position - VAR_SUB should take priority over ARG + let (ctx, _) = completer.get_completion_context("echo $HOM", 9); + assert_eq!( + ctx.last(), + Some(&markers::VAR_SUB), + "Should be in var_sub context for $HOM" + ); - // $VAR at command position - VAR_SUB should take priority over COMMAND - let (ctx, _) = completer.get_completion_context("$HOM", 4); - assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "Should be in var_sub context for bare $HOM"); + // $VAR at command position - VAR_SUB should take priority over COMMAND + let (ctx, _) = completer.get_completion_context("$HOM", 4); + assert_eq!( + ctx.last(), + Some(&markers::VAR_SUB), + "Should be in var_sub context for bare $HOM" + ); } #[test] fn context_detection_variable_in_double_quotes() { - let completer = Completer::new(); + let completer = Completer::new(); - // $VAR inside double quotes - let (ctx, _) = completer.get_completion_context("echo \"$HOM", 10); - assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "Should be in var_sub context inside double quotes"); + // $VAR inside double quotes + let (ctx, _) = completer.get_completion_context("echo \"$HOM", 10); + assert_eq!( + ctx.last(), + Some(&markers::VAR_SUB), + "Should be in var_sub context inside double quotes" + ); } #[test] fn context_detection_stack_base_is_null() { - let completer = Completer::new(); + let completer = Completer::new(); - // Empty input - only NULL on the stack - let (ctx, _) = completer.get_completion_context("", 0); - assert_eq!(ctx, vec![markers::NULL], "Empty input should only have NULL marker"); + // Empty input - only NULL on the stack + let (ctx, _) = completer.get_completion_context("", 0); + assert_eq!( + ctx, + vec![markers::NULL], + "Empty input should only have NULL marker" + ); } #[test] fn context_detection_context_start_position() { - let completer = Completer::new(); + let completer = Completer::new(); - // Command at start - ctx_start should be 0 - let (_, ctx_start) = completer.get_completion_context("ech", 3); - assert_eq!(ctx_start, 0, "Command at start should have ctx_start=0"); + // Command at start - ctx_start should be 0 + let (_, ctx_start) = completer.get_completion_context("ech", 3); + assert_eq!(ctx_start, 0, "Command at start should have ctx_start=0"); - // Argument after command - ctx_start should be at arg position - let (_, ctx_start) = completer.get_completion_context("echo hel", 8); - assert_eq!(ctx_start, 5, "Argument ctx_start should point to arg start"); + // Argument after command - ctx_start should be at arg position + let (_, ctx_start) = completer.get_completion_context("echo hel", 8); + assert_eq!(ctx_start, 5, "Argument ctx_start should point to arg start"); - // Variable sub - ctx_start should point to the $ - let (_, ctx_start) = completer.get_completion_context("echo $HOM", 9); - assert_eq!(ctx_start, 5, "Var sub ctx_start should point to the $"); + // Variable sub - ctx_start should point to the $ + let (_, ctx_start) = completer.get_completion_context("echo $HOM", 9); + assert_eq!(ctx_start, 5, "Var sub ctx_start should point to the $"); } #[test] fn context_detection_priority_ordering() { - let completer = Completer::new(); + let completer = Completer::new(); - // COMMAND (priority 2) should override ARG (priority 1) - // After a pipe, the next token is a command even though it looks like an arg - let (ctx, _) = completer.get_completion_context("echo foo | gr", 13); - assert_eq!(ctx.last(), Some(&markers::COMMAND), "COMMAND should win over ARG after pipe"); + // COMMAND (priority 2) should override ARG (priority 1) + // After a pipe, the next token is a command even though it looks like an arg + let (ctx, _) = completer.get_completion_context("echo foo | gr", 13); + assert_eq!( + ctx.last(), + Some(&markers::COMMAND), + "COMMAND should win over ARG after pipe" + ); - // VAR_SUB (priority 3) should override COMMAND (priority 2) - let (ctx, _) = completer.get_completion_context("$PA", 3); - assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "VAR_SUB should win over COMMAND"); + // VAR_SUB (priority 3) should override COMMAND (priority 2) + let (ctx, _) = completer.get_completion_context("$PA", 3); + assert_eq!( + ctx.last(), + Some(&markers::VAR_SUB), + "VAR_SUB should win over COMMAND" + ); } // ============================================================================ @@ -444,115 +501,115 @@ fn context_detection_priority_ordering() { #[test] fn cycle_forward_through_candidates() { - let temp_dir = setup_test_files(); - let path = temp_dir.path(); + let temp_dir = setup_test_files(); + let path = temp_dir.path(); - let mut completer = Completer::new(); - let line = format!("cat {}/file", path.display()); - let cursor_pos = line.len(); + let mut completer = Completer::new(); + let line = format!("cat {}/file", path.display()); + let cursor_pos = line.len(); - // First tab - let result1 = completer.complete(line.clone(), cursor_pos, 1).unwrap(); - assert!(result1.is_some()); - let first_candidate = completer.selected_candidate().unwrap().clone(); + // First tab + let result1 = completer.complete(line.clone(), cursor_pos, 1).unwrap(); + assert!(result1.is_some()); + let first_candidate = completer.selected_candidate().unwrap().clone(); - // Second tab - should cycle to next - let result2 = completer.complete(line.clone(), cursor_pos, 1).unwrap(); - assert!(result2.is_some()); - let second_candidate = completer.selected_candidate().unwrap().clone(); + // Second tab - should cycle to next + let result2 = completer.complete(line.clone(), cursor_pos, 1).unwrap(); + assert!(result2.is_some()); + let second_candidate = completer.selected_candidate().unwrap().clone(); - // Should be different (if there are multiple candidates) - if completer.candidates.len() > 1 { - assert_ne!(first_candidate, second_candidate); - } + // Should be different (if there are multiple candidates) + if completer.candidates.len() > 1 { + assert_ne!(first_candidate, second_candidate); + } } #[test] fn cycle_backward_with_shift_tab() { - let temp_dir = setup_test_files(); - let path = temp_dir.path(); + let temp_dir = setup_test_files(); + let path = temp_dir.path(); - let mut completer = Completer::new(); - let line = format!("cat {}/file", path.display()); - let cursor_pos = line.len(); + let mut completer = Completer::new(); + let line = format!("cat {}/file", path.display()); + let cursor_pos = line.len(); - // Forward twice - completer.complete(line.clone(), cursor_pos, 1).unwrap(); - let after_first = completer.selected_idx; - completer.complete(line.clone(), cursor_pos, 1).unwrap(); + // Forward twice + completer.complete(line.clone(), cursor_pos, 1).unwrap(); + let after_first = completer.selected_idx; + completer.complete(line.clone(), cursor_pos, 1).unwrap(); - // Backward once (shift-tab = direction -1) - completer.complete(line.clone(), cursor_pos, -1).unwrap(); - let after_backward = completer.selected_idx; + // Backward once (shift-tab = direction -1) + completer.complete(line.clone(), cursor_pos, -1).unwrap(); + let after_backward = completer.selected_idx; - // Should be back to first selection - assert_eq!(after_first, after_backward); + // Should be back to first selection + assert_eq!(after_first, after_backward); } #[test] fn cycle_wraps_around() { - let temp_dir = setup_test_files(); - let path = temp_dir.path(); + let temp_dir = setup_test_files(); + let path = temp_dir.path(); - let mut completer = Completer::new(); - let line = format!("cat {}/", path.display()); - let cursor_pos = line.len(); + let mut completer = Completer::new(); + let line = format!("cat {}/", path.display()); + let cursor_pos = line.len(); - // Get all candidates - completer.complete(line.clone(), cursor_pos, 1).unwrap(); - let num_candidates = completer.candidates.len(); + // Get all candidates + completer.complete(line.clone(), cursor_pos, 1).unwrap(); + let num_candidates = completer.candidates.len(); - if num_candidates > 1 { - // Cycle through all and one more - for _ in 0..num_candidates { - completer.complete(line.clone(), cursor_pos, 1).unwrap(); - } - - // Should wrap back to first (index 0) - assert_eq!(completer.selected_idx, 0); + if num_candidates > 1 { + // Cycle through all and one more + for _ in 0..num_candidates { + completer.complete(line.clone(), cursor_pos, 1).unwrap(); } + + // Should wrap back to first (index 0) + assert_eq!(completer.selected_idx, 0); + } } #[test] fn cycle_reset_on_input_change() { - let temp_dir = setup_test_files(); - let path = temp_dir.path(); + let temp_dir = setup_test_files(); + let path = temp_dir.path(); - let mut completer = Completer::new(); - let line1 = format!("cat {}/file", path.display()); + let mut completer = Completer::new(); + let line1 = format!("cat {}/file", path.display()); - // Complete once - completer.complete(line1.clone(), line1.len(), 1).unwrap(); - let candidates_count = completer.candidates.len(); + // Complete once + completer.complete(line1.clone(), line1.len(), 1).unwrap(); + let candidates_count = completer.candidates.len(); - // Change input - let line2 = format!("cat {}/script", path.display()); - completer.complete(line2.clone(), line2.len(), 1).unwrap(); + // Change input + let line2 = format!("cat {}/script", path.display()); + completer.complete(line2.clone(), line2.len(), 1).unwrap(); - // Should have different candidates - // (or at least should have reset the completer state) - assert!(completer.active); + // Should have different candidates + // (or at least should have reset the completer state) + assert!(completer.active); } #[test] fn reset_clears_state() { - let mut completer = Completer::new(); - // Use a prefix that will definitely have completions - let line = "ec".to_string(); + let mut completer = Completer::new(); + // Use a prefix that will definitely have completions + let line = "ec".to_string(); - let result = completer.complete(line, 2, 1).unwrap(); - // Only check if we got completions - if result.is_some() { - // Should have candidates after completion - assert!(!completer.candidates.is_empty()); + let result = completer.complete(line, 2, 1).unwrap(); + // Only check if we got completions + if result.is_some() { + // Should have candidates after completion + assert!(!completer.candidates.is_empty()); - completer.reset(); + completer.reset(); - // After reset, state should be cleared - assert!(!completer.active); - assert!(completer.candidates.is_empty()); - assert_eq!(completer.selected_idx, 0); - } + // After reset, state should be cleared + assert!(!completer.active); + assert!(completer.candidates.is_empty()); + assert_eq!(completer.selected_idx, 0); + } } // ============================================================================ @@ -561,94 +618,99 @@ fn reset_clears_state() { #[test] fn complete_empty_input() { - let mut completer = Completer::new(); - let line = "".to_string(); - let cursor_pos = 0; + let mut completer = Completer::new(); + let line = "".to_string(); + let cursor_pos = 0; - let result = completer.complete(line, cursor_pos, 1).unwrap(); + let result = completer.complete(line, cursor_pos, 1).unwrap(); - // Empty input might return files in current dir or no completion - // Either is valid behavior + // Empty input might return files in current dir or no completion + // Either is valid behavior } #[test] fn complete_whitespace_only() { - let mut completer = Completer::new(); - let line = " ".to_string(); - let cursor_pos = 3; + let mut completer = Completer::new(); + let line = " ".to_string(); + let cursor_pos = 3; - let result = completer.complete(line, cursor_pos, 1); - // Should handle gracefully - assert!(result.is_ok()); + let result = completer.complete(line, cursor_pos, 1); + // Should handle gracefully + assert!(result.is_ok()); } #[test] fn complete_at_middle_of_word() { - let mut completer = Completer::new(); - let line = "echo hello world".to_string(); - let cursor_pos = 7; // In the middle of "hello" + let mut completer = Completer::new(); + let line = "echo hello world".to_string(); + let cursor_pos = 7; // In the middle of "hello" - let result = completer.complete(line, cursor_pos, 1); - // Should handle cursor in middle of word - assert!(result.is_ok()); + let result = completer.complete(line, cursor_pos, 1); + // Should handle cursor in middle of word + assert!(result.is_ok()); } #[test] fn complete_with_quotes() { - let temp_dir = setup_test_files(); - let path = temp_dir.path(); + let temp_dir = setup_test_files(); + let path = temp_dir.path(); - let mut completer = Completer::new(); - let line = format!("cat \"{}/fil", path.display()); - let cursor_pos = line.len(); + let mut completer = Completer::new(); + let line = format!("cat \"{}/fil", path.display()); + let cursor_pos = line.len(); - let result = completer.complete(line, cursor_pos, 1); + let result = completer.complete(line, cursor_pos, 1); - // Should handle quoted paths - assert!(result.is_ok()); + // Should handle quoted paths + assert!(result.is_ok()); } #[test] fn complete_incomplete_command_substitution() { - let mut completer = Completer::new(); - let line = "echo \"$(ech".to_string(); - let cursor_pos = 11; + let mut completer = Completer::new(); + let line = "echo \"$(ech".to_string(); + let cursor_pos = 11; - let result = completer.complete(line, cursor_pos, 1); + let result = completer.complete(line, cursor_pos, 1); - // Should not crash on incomplete command sub - assert!(result.is_ok()); + // Should not crash on incomplete command sub + assert!(result.is_ok()); } #[test] fn complete_with_multiple_spaces() { - let mut completer = Completer::new(); - let line = "echo hello world".to_string(); - let cursor_pos = line.len(); + let mut completer = Completer::new(); + let line = "echo hello world".to_string(); + let cursor_pos = line.len(); - let result = completer.complete(line, cursor_pos, 1); - assert!(result.is_ok()); + let result = completer.complete(line, cursor_pos, 1); + assert!(result.is_ok()); } #[test] fn complete_special_characters_in_filename() { - let temp_dir = tempfile::tempdir().unwrap(); - let path = temp_dir.path(); + let temp_dir = tempfile::tempdir().unwrap(); + let path = temp_dir.path(); - // Create files with special characters - fs::write(path.join("file-with-dash.txt"), "").unwrap(); - fs::write(path.join("file_with_underscore.txt"), "").unwrap(); + // Create files with special characters + fs::write(path.join("file-with-dash.txt"), "").unwrap(); + fs::write(path.join("file_with_underscore.txt"), "").unwrap(); - let mut completer = Completer::new(); - let line = format!("cat {}/file", path.display()); - let cursor_pos = line.len(); + let mut completer = Completer::new(); + let line = format!("cat {}/file", path.display()); + let cursor_pos = line.len(); - let result = completer.complete(line, cursor_pos, 1).unwrap(); + let result = completer.complete(line, cursor_pos, 1).unwrap(); - if result.is_some() { - // Should handle special chars in filenames - assert!(completer.candidates.iter().any(|c| c.contains("file-with-dash") || c.contains("file_with_underscore"))); - } + if result.is_some() { + // Should handle special chars in filenames + assert!( + completer + .candidates + .iter() + .any(|c| c.contains("file-with-dash") || c.contains("file_with_underscore")) + ); + } } // ============================================================================ @@ -657,49 +719,49 @@ fn complete_special_characters_in_filename() { #[test] fn complete_full_workflow() { - let temp_dir = setup_test_files(); - let path = temp_dir.path(); + let temp_dir = setup_test_files(); + let path = temp_dir.path(); - let mut completer = Completer::new(); - let line = format!("cat {}/fil", path.display()); - let cursor_pos = line.len(); + let mut completer = Completer::new(); + let line = format!("cat {}/fil", path.display()); + let cursor_pos = line.len(); - // Tab 1: Get first completion - let result = completer.complete(line.clone(), cursor_pos, 1).unwrap(); - assert!(result.is_some()); - let completion1 = result.unwrap(); - assert!(completion1.contains("file")); + // Tab 1: Get first completion + let result = completer.complete(line.clone(), cursor_pos, 1).unwrap(); + assert!(result.is_some()); + let completion1 = result.unwrap(); + assert!(completion1.contains("file")); - // Tab 2: Cycle to next - let result = completer.complete(line.clone(), cursor_pos, 1).unwrap(); - assert!(result.is_some()); - let completion2 = result.unwrap(); + // Tab 2: Cycle to next + let result = completer.complete(line.clone(), cursor_pos, 1).unwrap(); + assert!(result.is_some()); + let completion2 = result.unwrap(); - // Shift-Tab: Go back - let result = completer.complete(line.clone(), cursor_pos, -1).unwrap(); - assert!(result.is_some()); - let completion3 = result.unwrap(); + // Shift-Tab: Go back + let result = completer.complete(line.clone(), cursor_pos, -1).unwrap(); + assert!(result.is_some()); + let completion3 = result.unwrap(); - // Should be back to first - assert_eq!(completion1, completion3); + // Should be back to first + assert_eq!(completion1, completion3); } #[test] fn complete_mixed_command_and_file() { - let mut completer = Completer::new(); + let mut completer = Completer::new(); - // First part: command completion - let line1 = "ech".to_string(); - let result1 = completer.complete(line1, 3, 1).unwrap(); - assert!(result1.is_some()); + // First part: command completion + let line1 = "ech".to_string(); + let result1 = completer.complete(line1, 3, 1).unwrap(); + assert!(result1.is_some()); - // Reset for new completion - completer.reset(); + // Reset for new completion + completer.reset(); - // Second part: file completion - let line2 = "echo Cargo.tom".to_string(); - let result2 = completer.complete(line2, 14, 1).unwrap(); + // Second part: file completion + let line2 = "echo Cargo.tom".to_string(); + let result2 = completer.complete(line2, 14, 1).unwrap(); - // Both should work - assert!(result1.is_some()); + // Both should work + assert!(result1.is_some()); } diff --git a/src/tests/expand.rs b/src/tests/expand.rs index 2d79480..a8d95ee 100644 --- a/src/tests/expand.rs +++ b/src/tests/expand.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; -use crate::expand::{perform_param_expansion, DUB_QUOTE, VAR_SUB}; +use crate::expand::{DUB_QUOTE, VAR_SUB, perform_param_expansion}; use crate::state::VarFlags; use super::*; @@ -293,70 +293,78 @@ fn param_expansion_replacesuffix() { #[test] fn dquote_escape_dollar() { - // "\$foo" should strip backslash, produce literal $foo (no expansion) - let result = unescape_str(r#""\$foo""#); - assert!(!result.contains(VAR_SUB), "Escaped $ should not become VAR_SUB"); - assert!(result.contains('$'), "Literal $ should be preserved"); - assert!(!result.contains('\\'), "Backslash should be stripped"); + // "\$foo" should strip backslash, produce literal $foo (no expansion) + let result = unescape_str(r#""\$foo""#); + assert!( + !result.contains(VAR_SUB), + "Escaped $ should not become VAR_SUB" + ); + assert!(result.contains('$'), "Literal $ should be preserved"); + assert!(!result.contains('\\'), "Backslash should be stripped"); } #[test] fn dquote_escape_backslash() { - // "\\" in double quotes should produce a single backslash - let result = unescape_str(r#""\\""#); - let inner: String = result.chars() - .filter(|&c| c != DUB_QUOTE) - .collect(); - assert_eq!(inner, "\\", "Double backslash should produce single backslash"); + // "\\" in double quotes should produce a single backslash + let result = unescape_str(r#""\\""#); + let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect(); + assert_eq!( + inner, "\\", + "Double backslash should produce single backslash" + ); } #[test] fn dquote_escape_quote() { - // "\"" should produce a literal double quote - let result = unescape_str(r#""\"""#); - let inner: String = result.chars() - .filter(|&c| c != DUB_QUOTE) - .collect(); - assert!(inner.contains('"'), "Escaped quote should produce literal quote"); + // "\"" should produce a literal double quote + let result = unescape_str(r#""\"""#); + let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect(); + assert!( + inner.contains('"'), + "Escaped quote should produce literal quote" + ); } #[test] fn dquote_escape_backtick() { - // "\`" should strip backslash, produce literal backtick - let result = unescape_str(r#""\`""#); - let inner: String = result.chars() - .filter(|&c| c != DUB_QUOTE) - .collect(); - assert_eq!(inner, "`", "Escaped backtick should produce literal backtick"); + // "\`" should strip backslash, produce literal backtick + let result = unescape_str(r#""\`""#); + let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect(); + assert_eq!( + inner, "`", + "Escaped backtick should produce literal backtick" + ); } #[test] fn dquote_escape_nonspecial_preserves_backslash() { - // "\a" inside double quotes should preserve the backslash (a is not special) - let result = unescape_str(r#""\a""#); - let inner: String = result.chars() - .filter(|&c| c != DUB_QUOTE) - .collect(); - assert_eq!(inner, "\\a", "Backslash before non-special char should be preserved"); + // "\a" inside double quotes should preserve the backslash (a is not special) + let result = unescape_str(r#""\a""#); + let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect(); + assert_eq!( + inner, "\\a", + "Backslash before non-special char should be preserved" + ); } #[test] fn dquote_unescaped_dollar_expands() { - // "$foo" inside double quotes should produce VAR_SUB (expansion marker) - let result = unescape_str(r#""$foo""#); - assert!(result.contains(VAR_SUB), "Unescaped $ should become VAR_SUB"); + // "$foo" inside double quotes should produce VAR_SUB (expansion marker) + let result = unescape_str(r#""$foo""#); + assert!( + result.contains(VAR_SUB), + "Unescaped $ should become VAR_SUB" + ); } #[test] fn dquote_mixed_escapes() { - // "hello \$world \\end" should have literal $, single backslash - let result = unescape_str(r#""hello \$world \\end""#); - assert!(!result.contains(VAR_SUB), "Escaped $ should not expand"); - assert!(result.contains('$'), "Literal $ should be in output"); - // Should have exactly one backslash (from \\) - let inner: String = result.chars() - .filter(|&c| c != DUB_QUOTE) - .collect(); - let backslash_count = inner.chars().filter(|&c| c == '\\').count(); - assert_eq!(backslash_count, 1, "\\\\ should produce one backslash"); + // "hello \$world \\end" should have literal $, single backslash + let result = unescape_str(r#""hello \$world \\end""#); + assert!(!result.contains(VAR_SUB), "Escaped $ should not expand"); + assert!(result.contains('$'), "Literal $ should be in output"); + // Should have exactly one backslash (from \\) + let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect(); + let backslash_count = inner.chars().filter(|&c| c == '\\').count(); + assert_eq!(backslash_count, 1, "\\\\ should produce one backslash"); } diff --git a/src/tests/highlight.rs b/src/tests/highlight.rs index 4b6c3e3..2ea850c 100644 --- a/src/tests/highlight.rs +++ b/src/tests/highlight.rs @@ -1,27 +1,29 @@ use crate::prompt::readline::{ - annotate_input, annotate_input_recursive, markers, - highlight::Highlighter, + annotate_input, annotate_input_recursive, highlight::Highlighter, markers, }; use super::*; /// Helper to check if a marker exists at any position in the annotated string fn has_marker(annotated: &str, marker: char) -> bool { - annotated.contains(marker) + annotated.contains(marker) } /// Helper to find the position of a marker in the annotated string fn find_marker(annotated: &str, marker: char) -> Option { - annotated.find(marker) + annotated.find(marker) } /// Helper to check if markers appear in the correct order fn marker_before(annotated: &str, first: char, second: char) -> bool { - if let (Some(pos1), Some(pos2)) = (find_marker(annotated, first), find_marker(annotated, second)) { - pos1 < pos2 - } else { - false - } + if let (Some(pos1), Some(pos2)) = ( + find_marker(annotated, first), + find_marker(annotated, second), + ) { + pos1 < pos2 + } else { + false + } } // ============================================================================ @@ -30,69 +32,70 @@ fn marker_before(annotated: &str, first: char, second: char) -> bool { #[test] fn annotate_simple_command() { - let input = "/bin/ls -la"; - let annotated = annotate_input(input); + let input = "/bin/ls -la"; + let annotated = annotate_input(input); - // Should have COMMAND marker for "/bin/ls" (external command) - assert!(has_marker(&annotated, markers::COMMAND)); + // Should have COMMAND marker for "/bin/ls" (external command) + assert!(has_marker(&annotated, markers::COMMAND)); - // Should have ARG marker for "-la" - assert!(has_marker(&annotated, markers::ARG)); + // Should have ARG marker for "-la" + assert!(has_marker(&annotated, markers::ARG)); - // Should have RESET markers - assert!(has_marker(&annotated, markers::RESET)); + // Should have RESET markers + assert!(has_marker(&annotated, markers::RESET)); } #[test] fn annotate_builtin_command() { - let input = "export FOO=bar"; - let annotated = annotate_input(input); + let input = "export FOO=bar"; + let annotated = annotate_input(input); - // Should mark "export" as BUILTIN - assert!(has_marker(&annotated, markers::BUILTIN)); + // Should mark "export" as BUILTIN + assert!(has_marker(&annotated, markers::BUILTIN)); - // Should mark assignment (or ARG if assignment isn't specifically marked separately) - assert!(has_marker(&annotated, markers::ASSIGNMENT) || has_marker(&annotated, markers::ARG)); + // Should mark assignment (or ARG if assignment isn't specifically marked + // separately) + assert!(has_marker(&annotated, markers::ASSIGNMENT) || has_marker(&annotated, markers::ARG)); } #[test] fn annotate_operator() { - let input = "ls | grep foo"; - let annotated = annotate_input(input); + let input = "ls | grep foo"; + let annotated = annotate_input(input); - // Should have OPERATOR marker for pipe - assert!(has_marker(&annotated, markers::OPERATOR)); + // Should have OPERATOR marker for pipe + assert!(has_marker(&annotated, markers::OPERATOR)); - // Should have COMMAND markers for both commands - let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count(); - assert_eq!(command_count, 2); + // Should have COMMAND markers for both commands + let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count(); + assert_eq!(command_count, 2); } #[test] fn annotate_redirect() { - let input = "echo hello > output.txt"; - let annotated = annotate_input(input); + let input = "echo hello > output.txt"; + let annotated = annotate_input(input); - // Should have REDIRECT marker - assert!(has_marker(&annotated, markers::REDIRECT)); + // Should have REDIRECT marker + assert!(has_marker(&annotated, markers::REDIRECT)); } #[test] fn annotate_keyword() { - let input = "if true; then echo yes; fi"; - let annotated = annotate_input(input); + let input = "if true; then echo yes; fi"; + let annotated = annotate_input(input); - // Should have KEYWORD markers for if/then/fi - assert!(has_marker(&annotated, markers::KEYWORD)); + // Should have KEYWORD markers for if/then/fi + assert!(has_marker(&annotated, markers::KEYWORD)); } #[test] fn annotate_command_separator() { - let input = "echo foo; echo bar"; - let annotated = annotate_input(input); + let input = "echo foo; echo bar"; + let annotated = annotate_input(input); - // Should have CMD_SEP marker for semicolon - assert!(has_marker(&annotated, markers::CMD_SEP)); + // Should have CMD_SEP marker for semicolon + assert!(has_marker(&annotated, markers::CMD_SEP)); } // ============================================================================ @@ -101,83 +104,87 @@ fn annotate_command_separator() { #[test] fn annotate_variable_simple() { - let input = "echo $foo"; - let annotated = annotate_input(input); + let input = "echo $foo"; + let annotated = annotate_input(input); - // Should have VAR_SUB markers - assert!(has_marker(&annotated, markers::VAR_SUB)); - assert!(has_marker(&annotated, markers::VAR_SUB_END)); + // Should have VAR_SUB markers + assert!(has_marker(&annotated, markers::VAR_SUB)); + assert!(has_marker(&annotated, markers::VAR_SUB_END)); } #[test] fn annotate_variable_braces() { - let input = "echo ${foo}"; - let annotated = annotate_input(input); + let input = "echo ${foo}"; + let annotated = annotate_input(input); - // Should have VAR_SUB markers for ${foo} - assert!(has_marker(&annotated, markers::VAR_SUB)); - assert!(has_marker(&annotated, markers::VAR_SUB_END)); + // Should have VAR_SUB markers for ${foo} + assert!(has_marker(&annotated, markers::VAR_SUB)); + assert!(has_marker(&annotated, markers::VAR_SUB_END)); } #[test] fn annotate_double_quoted_string() { - let input = r#"echo "hello world""#; - let annotated = annotate_input(input); + let input = r#"echo "hello world""#; + let annotated = annotate_input(input); - // Should have STRING_DQ markers - assert!(has_marker(&annotated, markers::STRING_DQ)); - assert!(has_marker(&annotated, markers::STRING_DQ_END)); + // Should have STRING_DQ markers + assert!(has_marker(&annotated, markers::STRING_DQ)); + assert!(has_marker(&annotated, markers::STRING_DQ_END)); } #[test] fn annotate_single_quoted_string() { - let input = "echo 'hello world'"; - let annotated = annotate_input(input); + let input = "echo 'hello world'"; + let annotated = annotate_input(input); - // Should have STRING_SQ markers - assert!(has_marker(&annotated, markers::STRING_SQ)); - assert!(has_marker(&annotated, markers::STRING_SQ_END)); + // Should have STRING_SQ markers + assert!(has_marker(&annotated, markers::STRING_SQ)); + assert!(has_marker(&annotated, markers::STRING_SQ_END)); } #[test] fn annotate_variable_in_string() { - let input = r#"echo "hello $USER""#; - let annotated = annotate_input(input); + let input = r#"echo "hello $USER""#; + let annotated = annotate_input(input); - // Should have both STRING_DQ and VAR_SUB markers - assert!(has_marker(&annotated, markers::STRING_DQ)); - assert!(has_marker(&annotated, markers::VAR_SUB)); + // Should have both STRING_DQ and VAR_SUB markers + assert!(has_marker(&annotated, markers::STRING_DQ)); + assert!(has_marker(&annotated, markers::VAR_SUB)); - // VAR_SUB should be inside STRING_DQ - assert!(marker_before(&annotated, markers::STRING_DQ, markers::VAR_SUB)); + // VAR_SUB should be inside STRING_DQ + assert!(marker_before( + &annotated, + markers::STRING_DQ, + markers::VAR_SUB + )); } #[test] fn annotate_glob_asterisk() { - let input = "ls *.txt"; - let annotated = annotate_input(input); + let input = "ls *.txt"; + let annotated = annotate_input(input); - // Should have GLOB marker for * - assert!(has_marker(&annotated, markers::GLOB)); + // Should have GLOB marker for * + assert!(has_marker(&annotated, markers::GLOB)); } #[test] fn annotate_glob_question() { - let input = "ls file?.txt"; - let annotated = annotate_input(input); + let input = "ls file?.txt"; + let annotated = annotate_input(input); - // Should have GLOB marker for ? - assert!(has_marker(&annotated, markers::GLOB)); + // Should have GLOB marker for ? + assert!(has_marker(&annotated, markers::GLOB)); } #[test] fn annotate_glob_bracket() { - let input = "ls file[abc].txt"; - let annotated = annotate_input(input); + let input = "ls file[abc].txt"; + let annotated = annotate_input(input); - // Should have GLOB markers for bracket expression - let glob_count = annotated.chars().filter(|&c| c == markers::GLOB).count(); - assert!(glob_count >= 2); // Opening and closing + // Should have GLOB markers for bracket expression + let glob_count = annotated.chars().filter(|&c| c == markers::GLOB).count(); + assert!(glob_count >= 2); // Opening and closing } // ============================================================================ @@ -186,32 +193,32 @@ fn annotate_glob_bracket() { #[test] fn annotate_command_sub_basic() { - let input = "echo $(whoami)"; - let annotated = annotate_input(input); + let input = "echo $(whoami)"; + let annotated = annotate_input(input); - // Should have CMD_SUB markers (but not recursively annotated yet) - assert!(has_marker(&annotated, markers::CMD_SUB)); - assert!(has_marker(&annotated, markers::CMD_SUB_END)); + // Should have CMD_SUB markers (but not recursively annotated yet) + assert!(has_marker(&annotated, markers::CMD_SUB)); + assert!(has_marker(&annotated, markers::CMD_SUB_END)); } #[test] fn annotate_subshell_basic() { - let input = "(cd /tmp && ls)"; - let annotated = annotate_input(input); + let input = "(cd /tmp && ls)"; + let annotated = annotate_input(input); - // Should have SUBSH markers - assert!(has_marker(&annotated, markers::SUBSH)); - assert!(has_marker(&annotated, markers::SUBSH_END)); + // Should have SUBSH markers + assert!(has_marker(&annotated, markers::SUBSH)); + assert!(has_marker(&annotated, markers::SUBSH_END)); } #[test] fn annotate_process_sub_output() { - let input = "diff <(ls dir1) <(ls dir2)"; - let annotated = annotate_input(input); + let input = "diff <(ls dir1) <(ls dir2)"; + let annotated = annotate_input(input); - // Should have PROC_SUB markers - assert!(has_marker(&annotated, markers::PROC_SUB)); - assert!(has_marker(&annotated, markers::PROC_SUB_END)); + // Should have PROC_SUB markers + assert!(has_marker(&annotated, markers::PROC_SUB)); + assert!(has_marker(&annotated, markers::PROC_SUB_END)); } // ============================================================================ @@ -220,88 +227,97 @@ fn annotate_process_sub_output() { #[test] fn annotate_recursive_command_sub() { - let input = "echo $(whoami)"; - let annotated = annotate_input_recursive(input); + let input = "echo $(whoami)"; + let annotated = annotate_input_recursive(input); - // Should have CMD_SUB markers - assert!(has_marker(&annotated, markers::CMD_SUB)); - assert!(has_marker(&annotated, markers::CMD_SUB_END)); + // Should have CMD_SUB markers + assert!(has_marker(&annotated, markers::CMD_SUB)); + assert!(has_marker(&annotated, markers::CMD_SUB_END)); - // Inside the command sub, "whoami" should be marked as COMMAND - // The recursive annotator should have processed the inside - assert!(has_marker(&annotated, markers::COMMAND)); + // Inside the command sub, "whoami" should be marked as COMMAND + // The recursive annotator should have processed the inside + assert!(has_marker(&annotated, markers::COMMAND)); } #[test] fn annotate_recursive_nested_command_sub() { - let input = "echo $(echo $(whoami))"; - let annotated = annotate_input_recursive(input); + let input = "echo $(echo $(whoami))"; + let annotated = annotate_input_recursive(input); - // Should have multiple CMD_SUB markers (nested) - let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count(); - assert!(cmd_sub_count >= 2, "Should have at least 2 CMD_SUB markers for nested substitutions"); + // Should have multiple CMD_SUB markers (nested) + let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count(); + assert!( + cmd_sub_count >= 2, + "Should have at least 2 CMD_SUB markers for nested substitutions" + ); } #[test] fn annotate_recursive_command_sub_with_args() { - let input = "echo $(grep foo file.txt)"; - let annotated = annotate_input_recursive(input); + let input = "echo $(grep foo file.txt)"; + let annotated = annotate_input_recursive(input); - // Should have BUILTIN for echo and possibly COMMAND for grep (if in PATH) - // Just check that we have command-type markers - let builtin_count = annotated.chars().filter(|&c| c == markers::BUILTIN).count(); - let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count(); - assert!(builtin_count + command_count >= 2, "Expected at least 2 command markers (BUILTIN or COMMAND)"); + // Should have BUILTIN for echo and possibly COMMAND for grep (if in PATH) + // Just check that we have command-type markers + let builtin_count = annotated.chars().filter(|&c| c == markers::BUILTIN).count(); + let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count(); + assert!( + builtin_count + command_count >= 2, + "Expected at least 2 command markers (BUILTIN or COMMAND)" + ); } #[test] fn annotate_recursive_subshell() { - let input = "(echo hello; echo world)"; - let annotated = annotate_input_recursive(input); + let input = "(echo hello; echo world)"; + let annotated = annotate_input_recursive(input); - // Should have SUBSH markers - assert!(has_marker(&annotated, markers::SUBSH)); - assert!(has_marker(&annotated, markers::SUBSH_END)); + // Should have SUBSH markers + assert!(has_marker(&annotated, markers::SUBSH)); + assert!(has_marker(&annotated, markers::SUBSH_END)); - // Inside should be annotated with BUILTIN (echo is a builtin) and CMD_SEP - assert!(has_marker(&annotated, markers::BUILTIN)); - assert!(has_marker(&annotated, markers::CMD_SEP)); + // Inside should be annotated with BUILTIN (echo is a builtin) and CMD_SEP + assert!(has_marker(&annotated, markers::BUILTIN)); + assert!(has_marker(&annotated, markers::CMD_SEP)); } #[test] fn annotate_recursive_process_sub() { - let input = "diff <(ls -la)"; - let annotated = annotate_input_recursive(input); + let input = "diff <(ls -la)"; + let annotated = annotate_input_recursive(input); - // Should have PROC_SUB markers - assert!(has_marker(&annotated, markers::PROC_SUB)); + // Should have PROC_SUB markers + assert!(has_marker(&annotated, markers::PROC_SUB)); - // ls should be marked as COMMAND inside the process sub - assert!(has_marker(&annotated, markers::COMMAND)); + // ls should be marked as COMMAND inside the process sub + assert!(has_marker(&annotated, markers::COMMAND)); } #[test] fn annotate_recursive_command_sub_in_string() { - let input = r#"echo "current user: $(whoami)""#; - let annotated = annotate_input_recursive(input); + let input = r#"echo "current user: $(whoami)""#; + let annotated = annotate_input_recursive(input); - // Should have STRING_DQ, CMD_SUB, and COMMAND markers - assert!(has_marker(&annotated, markers::STRING_DQ)); - assert!(has_marker(&annotated, markers::CMD_SUB)); - assert!(has_marker(&annotated, markers::COMMAND)); + // Should have STRING_DQ, CMD_SUB, and COMMAND markers + assert!(has_marker(&annotated, markers::STRING_DQ)); + assert!(has_marker(&annotated, markers::CMD_SUB)); + assert!(has_marker(&annotated, markers::COMMAND)); } #[test] fn annotate_recursive_deeply_nested() { - let input = r#"echo "outer: $(echo "inner: $(whoami)")""#; - let annotated = annotate_input_recursive(input); + let input = r#"echo "outer: $(echo "inner: $(whoami)")""#; + let annotated = annotate_input_recursive(input); - // Should have multiple STRING_DQ and CMD_SUB markers - let string_count = annotated.chars().filter(|&c| c == markers::STRING_DQ).count(); - let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count(); + // Should have multiple STRING_DQ and CMD_SUB markers + let string_count = annotated + .chars() + .filter(|&c| c == markers::STRING_DQ) + .count(); + let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count(); - assert!(string_count >= 2, "Should have multiple STRING_DQ markers"); - assert!(cmd_sub_count >= 2, "Should have multiple CMD_SUB markers"); + assert!(string_count >= 2, "Should have multiple STRING_DQ markers"); + assert!(cmd_sub_count >= 2, "Should have multiple CMD_SUB markers"); } // ============================================================================ @@ -310,33 +326,37 @@ fn annotate_recursive_deeply_nested() { #[test] fn marker_priority_var_in_string() { - let input = r#""$foo""#; - let annotated = annotate_input(input); + let input = r#""$foo""#; + let annotated = annotate_input(input); - // STRING_DQ should come before VAR_SUB (outer before inner) - assert!(marker_before(&annotated, markers::STRING_DQ, markers::VAR_SUB)); + // STRING_DQ should come before VAR_SUB (outer before inner) + assert!(marker_before( + &annotated, + markers::STRING_DQ, + markers::VAR_SUB + )); } #[test] fn marker_priority_arg_vs_string() { - let input = r#"echo "hello""#; - let annotated = annotate_input(input); + let input = r#"echo "hello""#; + let annotated = annotate_input(input); - // Both ARG and STRING_DQ should be present - // STRING_DQ should be inside the ARG token's span - assert!(has_marker(&annotated, markers::ARG)); - assert!(has_marker(&annotated, markers::STRING_DQ)); + // Both ARG and STRING_DQ should be present + // STRING_DQ should be inside the ARG token's span + assert!(has_marker(&annotated, markers::ARG)); + assert!(has_marker(&annotated, markers::STRING_DQ)); } #[test] fn marker_priority_reset_placement() { - let input = "echo hello"; - let annotated = annotate_input(input); + let input = "echo hello"; + let annotated = annotate_input(input); - // RESET markers should appear after each token - // There should be multiple RESET markers - let reset_count = annotated.chars().filter(|&c| c == markers::RESET).count(); - assert!(reset_count >= 2); + // RESET markers should appear after each token + // There should be multiple RESET markers + let reset_count = annotated.chars().filter(|&c| c == markers::RESET).count(); + assert!(reset_count >= 2); } // ============================================================================ @@ -345,127 +365,131 @@ fn marker_priority_reset_placement() { #[test] fn highlighter_produces_ansi_codes() { - let mut highlighter = Highlighter::new(); - highlighter.load_input("echo hello"); - highlighter.highlight(); - let output = highlighter.take(); + let mut highlighter = Highlighter::new(); + highlighter.load_input("echo hello"); + highlighter.highlight(); + let output = highlighter.take(); - // Should contain ANSI escape codes - assert!(output.contains("\x1b["), "Output should contain ANSI escape sequences"); + // Should contain ANSI escape codes + assert!( + output.contains("\x1b["), + "Output should contain ANSI escape sequences" + ); - // Should still contain the original text - assert!(output.contains("echo")); - assert!(output.contains("hello")); + // Should still contain the original text + assert!(output.contains("echo")); + assert!(output.contains("hello")); } #[test] fn highlighter_handles_empty_input() { - let mut highlighter = Highlighter::new(); - highlighter.load_input(""); - highlighter.highlight(); - let output = highlighter.take(); + let mut highlighter = Highlighter::new(); + highlighter.load_input(""); + highlighter.highlight(); + let output = highlighter.take(); - // Should not crash and should return empty or minimal output - assert!(output.len() < 10); // Just escape codes or empty + // Should not crash and should return empty or minimal output + assert!(output.len() < 10); // Just escape codes or empty } #[test] fn highlighter_command_validation() { - let mut highlighter = Highlighter::new(); + let mut highlighter = Highlighter::new(); - // Valid command (echo exists) - highlighter.load_input("echo test"); - highlighter.highlight(); - let valid_output = highlighter.take(); + // Valid command (echo exists) + highlighter.load_input("echo test"); + highlighter.highlight(); + let valid_output = highlighter.take(); - // Invalid command (definitely doesn't exist) - highlighter.load_input("xyznotacommand123 test"); - highlighter.highlight(); - let invalid_output = highlighter.take(); + // Invalid command (definitely doesn't exist) + highlighter.load_input("xyznotacommand123 test"); + highlighter.highlight(); + let invalid_output = highlighter.take(); - // Both should have ANSI codes - assert!(valid_output.contains("\x1b[")); - assert!(invalid_output.contains("\x1b[")); + // Both should have ANSI codes + assert!(valid_output.contains("\x1b[")); + assert!(invalid_output.contains("\x1b[")); - // The color codes should be different (green vs red) - // Valid commands should have \x1b[32m (green) - // Invalid commands should have \x1b[31m (red) or \x1b[1;31m (bold red) + // The color codes should be different (green vs red) + // Valid commands should have \x1b[32m (green) + // Invalid commands should have \x1b[31m (red) or \x1b[1;31m (bold red) } #[test] fn highlighter_preserves_text_content() { - let input = "echo hello world"; - let mut highlighter = Highlighter::new(); - highlighter.load_input(input); - highlighter.highlight(); - let output = highlighter.take(); + let input = "echo hello world"; + let mut highlighter = Highlighter::new(); + highlighter.load_input(input); + highlighter.highlight(); + let output = highlighter.take(); - // Remove ANSI codes to check text content - let text_only: String = output.chars() - .filter(|c| !c.is_control() && *c != '\x1b') - .collect(); + // Remove ANSI codes to check text content + let text_only: String = output + .chars() + .filter(|c| !c.is_control() && *c != '\x1b') + .collect(); - // Should still contain the words (might have escape sequence fragments) - assert!(output.contains("echo")); - assert!(output.contains("hello")); - assert!(output.contains("world")); + // Should still contain the words (might have escape sequence fragments) + assert!(output.contains("echo")); + assert!(output.contains("hello")); + assert!(output.contains("world")); } #[test] fn highlighter_multiple_tokens() { - let mut highlighter = Highlighter::new(); - highlighter.load_input("ls -la | grep foo"); - highlighter.highlight(); - let output = highlighter.take(); + let mut highlighter = Highlighter::new(); + highlighter.load_input("ls -la | grep foo"); + highlighter.highlight(); + let output = highlighter.take(); - // Should contain all tokens - assert!(output.contains("ls")); - assert!(output.contains("-la")); - assert!(output.contains("|")); - assert!(output.contains("grep")); - assert!(output.contains("foo")); + // Should contain all tokens + assert!(output.contains("ls")); + assert!(output.contains("-la")); + assert!(output.contains("|")); + assert!(output.contains("grep")); + assert!(output.contains("foo")); - // Should have ANSI codes - assert!(output.contains("\x1b[")); + // Should have ANSI codes + assert!(output.contains("\x1b[")); } #[test] fn highlighter_string_with_variable() { - let mut highlighter = Highlighter::new(); - highlighter.load_input(r#"echo "hello $USER""#); - highlighter.highlight(); - let output = highlighter.take(); + let mut highlighter = Highlighter::new(); + highlighter.load_input(r#"echo "hello $USER""#); + highlighter.highlight(); + let output = highlighter.take(); - // Should contain the text - assert!(output.contains("echo")); - assert!(output.contains("hello")); - assert!(output.contains("USER")); + // Should contain the text + assert!(output.contains("echo")); + assert!(output.contains("hello")); + assert!(output.contains("USER")); - // Should have ANSI codes for different elements - assert!(output.contains("\x1b[")); + // Should have ANSI codes for different elements + assert!(output.contains("\x1b[")); } #[test] fn highlighter_reusable() { - let mut highlighter = Highlighter::new(); + let mut highlighter = Highlighter::new(); - // First input - highlighter.load_input("echo first"); - highlighter.highlight(); - let output1 = highlighter.take(); + // First input + highlighter.load_input("echo first"); + highlighter.highlight(); + let output1 = highlighter.take(); - // Second input (reusing same highlighter) - highlighter.load_input("echo second"); - highlighter.highlight(); - let output2 = highlighter.take(); + // Second input (reusing same highlighter) + highlighter.load_input("echo second"); + highlighter.highlight(); + let output2 = highlighter.take(); - // Both should work - assert!(output1.contains("first")); - assert!(output2.contains("second")); + // Both should work + assert!(output1.contains("first")); + assert!(output2.contains("second")); - // Should not contain each other's text - assert!(!output1.contains("second")); - assert!(!output2.contains("first")); + // Should not contain each other's text + assert!(!output1.contains("second")); + assert!(!output2.contains("first")); } // ============================================================================ @@ -474,133 +498,143 @@ fn highlighter_reusable() { #[test] fn annotate_unclosed_string() { - let input = r#"echo "hello"#; - let annotated = annotate_input(input); + let input = r#"echo "hello"#; + let annotated = annotate_input(input); - // Should handle unclosed string gracefully - assert!(has_marker(&annotated, markers::STRING_DQ)); - // May or may not have STRING_DQ_END depending on implementation + // Should handle unclosed string gracefully + assert!(has_marker(&annotated, markers::STRING_DQ)); + // May or may not have STRING_DQ_END depending on implementation } #[test] fn annotate_unclosed_command_sub() { - let input = "echo $(whoami"; - let annotated = annotate_input(input); + let input = "echo $(whoami"; + let annotated = annotate_input(input); - // Should handle unclosed command sub gracefully - assert!(has_marker(&annotated, markers::CMD_SUB)); + // Should handle unclosed command sub gracefully + assert!(has_marker(&annotated, markers::CMD_SUB)); } #[test] fn annotate_empty_command_sub() { - let input = "echo $()"; - let annotated = annotate_input_recursive(input); + let input = "echo $()"; + let annotated = annotate_input_recursive(input); - // Should handle empty command sub - assert!(has_marker(&annotated, markers::CMD_SUB)); - assert!(has_marker(&annotated, markers::CMD_SUB_END)); + // Should handle empty command sub + assert!(has_marker(&annotated, markers::CMD_SUB)); + assert!(has_marker(&annotated, markers::CMD_SUB_END)); } #[test] fn annotate_escaped_characters() { - let input = r#"echo \$foo \`bar\` \"test\""#; - let annotated = annotate_input(input); + let input = r#"echo \$foo \`bar\` \"test\""#; + let annotated = annotate_input(input); - // Should not mark escaped $ as variable - // This is tricky - the behavior depends on implementation - // At minimum, should not crash + // Should not mark escaped $ as variable + // This is tricky - the behavior depends on implementation + // At minimum, should not crash } #[test] fn annotate_special_variables() { - let input = "echo $0 $1 $2 $3 $4"; - let annotated = annotate_input(input); + let input = "echo $0 $1 $2 $3 $4"; + let annotated = annotate_input(input); - // Should mark positional parameters - let var_count = annotated.chars().filter(|&c| c == markers::VAR_SUB).count(); - assert!(var_count >= 5, "Expected at least 5 VAR_SUB markers, found {}", var_count); + // Should mark positional parameters + let var_count = annotated.chars().filter(|&c| c == markers::VAR_SUB).count(); + assert!( + var_count >= 5, + "Expected at least 5 VAR_SUB markers, found {}", + var_count + ); } #[test] fn annotate_variable_no_expansion_in_single_quotes() { - let input = "echo '$foo'"; - let annotated = annotate_input(input); + let input = "echo '$foo'"; + let annotated = annotate_input(input); - // Should have STRING_SQ markers - assert!(has_marker(&annotated, markers::STRING_SQ)); + // Should have STRING_SQ markers + assert!(has_marker(&annotated, markers::STRING_SQ)); - // Should NOT have VAR_SUB markers (variables don't expand in single quotes) - // Note: The annotator might still mark it - depends on implementation + // Should NOT have VAR_SUB markers (variables don't expand in single quotes) + // Note: The annotator might still mark it - depends on implementation } #[test] fn annotate_complex_pipeline() { - let input = "cat file.txt | grep pattern | sed 's/foo/bar/' | sort | uniq"; - let annotated = annotate_input(input); + let input = "cat file.txt | grep pattern | sed 's/foo/bar/' | sort | uniq"; + let annotated = annotate_input(input); - // Should have multiple OPERATOR markers for pipes - let operator_count = annotated.chars().filter(|&c| c == markers::OPERATOR).count(); - assert!(operator_count >= 4); + // Should have multiple OPERATOR markers for pipes + let operator_count = annotated + .chars() + .filter(|&c| c == markers::OPERATOR) + .count(); + assert!(operator_count >= 4); - // Should have multiple COMMAND markers - let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count(); - assert!(command_count >= 5); + // Should have multiple COMMAND markers + let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count(); + assert!(command_count >= 5); } #[test] fn annotate_assignment_with_command_sub() { - let input = "FOO=$(whoami)"; - let annotated = annotate_input_recursive(input); + let input = "FOO=$(whoami)"; + let annotated = annotate_input_recursive(input); - // Should have ASSIGNMENT marker - assert!(has_marker(&annotated, markers::ASSIGNMENT)); + // Should have ASSIGNMENT marker + assert!(has_marker(&annotated, markers::ASSIGNMENT)); - // Should have CMD_SUB marker - assert!(has_marker(&annotated, markers::CMD_SUB)); + // Should have CMD_SUB marker + assert!(has_marker(&annotated, markers::CMD_SUB)); - // Inside command sub should have COMMAND marker - assert!(has_marker(&annotated, markers::COMMAND)); + // Inside command sub should have COMMAND marker + assert!(has_marker(&annotated, markers::COMMAND)); } #[test] fn annotate_redirect_with_fd() { - let input = "command 2>&1"; - let annotated = annotate_input(input); + let input = "command 2>&1"; + let annotated = annotate_input(input); - // Should have REDIRECT marker for the redirect operator - assert!(has_marker(&annotated, markers::REDIRECT)); + // Should have REDIRECT marker for the redirect operator + assert!(has_marker(&annotated, markers::REDIRECT)); } #[test] fn annotate_multiple_redirects() { - let input = "command > out.txt 2>&1"; - let annotated = annotate_input(input); + let input = "command > out.txt 2>&1"; + let annotated = annotate_input(input); - // Should have multiple REDIRECT markers - let redirect_count = annotated.chars().filter(|&c| c == markers::REDIRECT).count(); - assert!(redirect_count >= 2); + // Should have multiple REDIRECT markers + let redirect_count = annotated + .chars() + .filter(|&c| c == markers::REDIRECT) + .count(); + assert!(redirect_count >= 2); } #[test] fn annotate_here_string() { - let input = "cat <<< 'hello world'"; - let annotated = annotate_input(input); + let input = "cat <<< 'hello world'"; + let annotated = annotate_input(input); - // Should have REDIRECT marker for <<< - assert!(has_marker(&annotated, markers::REDIRECT)); + // Should have REDIRECT marker for <<< + assert!(has_marker(&annotated, markers::REDIRECT)); - // Should have STRING_SQ markers - assert!(has_marker(&annotated, markers::STRING_SQ)); + // Should have STRING_SQ markers + assert!(has_marker(&annotated, markers::STRING_SQ)); } #[test] fn annotate_unicode_content() { - let input = "echo 'hello δΈ–η•Œ 🌍'"; - let annotated = annotate_input(input); + let input = "echo 'hello δΈ–η•Œ 🌍'"; + let annotated = annotate_input(input); - // Should handle unicode gracefully - assert!(has_marker(&annotated, markers::BUILTIN)); - assert!(has_marker(&annotated, markers::STRING_SQ)); + // Should handle unicode gracefully + assert!(has_marker(&annotated, markers::BUILTIN)); + assert!(has_marker(&annotated, markers::STRING_SQ)); } // ============================================================================ @@ -609,26 +643,26 @@ fn annotate_unicode_content() { #[test] fn regression_arg_marker_at_position_zero() { - // Regression test: ARG marker was appearing at position 3 for input "ech" - // This was caused by SOI/EOI tokens falling through to ARG annotation - let input = "ech"; - let annotated = annotate_input(input); + // Regression test: ARG marker was appearing at position 3 for input "ech" + // This was caused by SOI/EOI tokens falling through to ARG annotation + let input = "ech"; + let annotated = annotate_input(input); - // Should only have COMMAND marker, not ARG - // (incomplete command should still be marked as command attempt) - assert!(has_marker(&annotated, markers::COMMAND)); + // Should only have COMMAND marker, not ARG + // (incomplete command should still be marked as command attempt) + assert!(has_marker(&annotated, markers::COMMAND)); } #[test] fn regression_string_color_in_annotated_strings() { - // Regression test: ARG marker was overriding STRING_DQ color - let input = r#"echo "test""#; - let annotated = annotate_input(input); + // Regression test: ARG marker was overriding STRING_DQ color + let input = r#"echo "test""#; + let annotated = annotate_input(input); - // STRING_DQ should be present and properly positioned - assert!(has_marker(&annotated, markers::STRING_DQ)); - assert!(has_marker(&annotated, markers::STRING_DQ_END)); + // STRING_DQ should be present and properly positioned + assert!(has_marker(&annotated, markers::STRING_DQ)); + assert!(has_marker(&annotated, markers::STRING_DQ_END)); - // The string markers should come after the ARG marker - // (so they override it in the highlighting) + // The string markers should come after the ARG marker + // (so they override it in the highlighting) } diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 5caedde..59408e2 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -4,8 +4,9 @@ use super::*; use crate::expand::{expand_aliases, unescape_str}; use crate::libsh::error::{Note, ShErr, ShErrKind}; use crate::parse::{ + NdRule, Node, ParseStream, lex::{LexFlags, LexStream, Tk, TkRule}, - node_operation, NdRule, Node, ParseStream, + node_operation, }; use crate::state::{write_logic, write_vars}; diff --git a/src/tests/readline.rs b/src/tests/readline.rs index a2e7daa..268e171 100644 --- a/src/tests/readline.rs +++ b/src/tests/readline.rs @@ -1,14 +1,17 @@ use std::collections::VecDeque; use crate::{ - libsh::{error::ShErr, term::{Style, Styled}}, + libsh::{ + error::ShErr, + term::{Style, Styled}, + }, prompt::readline::{ + FernVi, history::History, keys::{KeyCode, KeyEvent, ModKeys}, linebuf::LineBuf, - term::{raw_mode, KeyReader, LineWriter}, + term::{KeyReader, LineWriter, raw_mode}, vimode::{ViInsert, ViMode, ViNormal}, - FernVi, }, }; @@ -173,8 +176,9 @@ impl LineWriter for TestWriter { } } -// NOTE: FernVi structure has changed significantly and readline() method no longer exists -// These test helpers are disabled until they can be properly updated +// NOTE: FernVi structure has changed significantly and readline() method no +// longer exists These test helpers are disabled until they can be properly +// updated /* impl FernVi { pub fn new_test(prompt: Option, input: &str, initial: &str) -> Self { @@ -612,10 +616,10 @@ fn fernvi_test_mode_change() { #[test] fn fernvi_test_lorem_ipsum_1() { assert_eq!(fernvi_test( - "\x1bwwwwwwww5dWdBdBjjdwjdwbbbcwasdasdasdasd\x1b\r", - LOREM_IPSUM), - "Lorem ipsum dolor sit amet, incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in repin voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur asdasdasdasd occaecat cupinon proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra." - ) + "\x1bwwwwwwww5dWdBdBjjdwjdwbbbcwasdasdasdasd\x1b\r", + LOREM_IPSUM), + "Lorem ipsum dolor sit amet, incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in repin voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur asdasdasdasd occaecat cupinon proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra." + ) } #[test] @@ -632,9 +636,9 @@ fn fernvi_test_lorem_ipsum_undo() { #[test] fn fernvi_test_lorem_ipsum_ctrl_w() { assert_eq!(fernvi_test( - "\x1bj5wiasdasdkjhaksjdhkajshd\x17wordswordswords\x17somemorewords\x17\x1b[D\x1b[D\x17\x1b\r", - LOREM_IPSUM), - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim am, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra." - ) + "\x1bj5wiasdasdkjhaksjdhkajshd\x17wordswordswords\x17somemorewords\x17\x1b[D\x1b[D\x17\x1b\r", + LOREM_IPSUM), + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim am, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra." + ) } */ diff --git a/src/tests/redir.rs b/src/tests/redir.rs index 824977c..a016296 100644 --- a/src/tests/redir.rs +++ b/src/tests/redir.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use crate::parse::{ - lex::{LexFlags, LexStream}, - Node, NdRule, ParseStream, RedirType, Redir, + NdRule, Node, ParseStream, Redir, RedirType, + lex::{LexFlags, LexStream}, }; use crate::procio::{IoFrame, IoMode, IoStack}; @@ -11,187 +11,238 @@ use crate::procio::{IoFrame, IoMode, IoStack}; // ============================================================================ fn parse_command(input: &str) -> Node { - let source = Arc::new(input.to_string()); - let tokens = LexStream::new(source, LexFlags::empty()) - .flatten() - .collect::>(); + let source = Arc::new(input.to_string()); + let tokens = LexStream::new(source, LexFlags::empty()) + .flatten() + .collect::>(); - let mut nodes = ParseStream::new(tokens) - .flatten() - .collect::>(); + let mut nodes = ParseStream::new(tokens).flatten().collect::>(); - assert_eq!(nodes.len(), 1, "Expected exactly one node"); - let top_node = nodes.remove(0); + assert_eq!(nodes.len(), 1, "Expected exactly one node"); + let top_node = nodes.remove(0); - // Navigate to the actual Command node within the AST structure - // Structure is typically: Conjunction -> Pipeline -> Command - match top_node.class { - NdRule::Conjunction { elements } => { - let first_element = elements.into_iter().next().expect("Expected at least one conjunction element"); - match first_element.cmd.class { - NdRule::Pipeline { cmds, .. } => { - let mut commands = cmds; - assert_eq!(commands.len(), 1, "Expected exactly one command in pipeline"); - commands.remove(0) - } - NdRule::Command { .. } => *first_element.cmd, - _ => panic!("Expected Command or Pipeline node, got {:?}", first_element.cmd.class), - } - } - NdRule::Pipeline { cmds, .. } => { - let mut commands = cmds; - assert_eq!(commands.len(), 1, "Expected exactly one command in pipeline"); - commands.remove(0) - } - NdRule::Command { .. } => top_node, - _ => panic!("Expected Conjunction, Pipeline, or Command node, got {:?}", top_node.class), - } + // Navigate to the actual Command node within the AST structure + // Structure is typically: Conjunction -> Pipeline -> Command + match top_node.class { + NdRule::Conjunction { elements } => { + let first_element = elements + .into_iter() + .next() + .expect("Expected at least one conjunction element"); + match first_element.cmd.class { + NdRule::Pipeline { cmds, .. } => { + let mut commands = cmds; + assert_eq!( + commands.len(), + 1, + "Expected exactly one command in pipeline" + ); + commands.remove(0) + } + NdRule::Command { .. } => *first_element.cmd, + _ => panic!( + "Expected Command or Pipeline node, got {:?}", + first_element.cmd.class + ), + } + } + NdRule::Pipeline { cmds, .. } => { + let mut commands = cmds; + assert_eq!( + commands.len(), + 1, + "Expected exactly one command in pipeline" + ); + commands.remove(0) + } + NdRule::Command { .. } => top_node, + _ => panic!( + "Expected Conjunction, Pipeline, or Command node, got {:?}", + top_node.class + ), + } } #[test] fn parse_output_redirect() { - let node = parse_command("echo hello > output.txt"); + let node = parse_command("echo hello > output.txt"); - assert_eq!(node.redirs.len(), 1); - let redir = &node.redirs[0]; + assert_eq!(node.redirs.len(), 1); + let redir = &node.redirs[0]; - assert!(matches!(redir.class, RedirType::Output)); - assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 1, .. })); + assert!(matches!(redir.class, RedirType::Output)); + assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 1, .. })); } #[test] fn parse_append_redirect() { - let node = parse_command("echo hello >> output.txt"); + let node = parse_command("echo hello >> output.txt"); - assert_eq!(node.redirs.len(), 1); - let redir = &node.redirs[0]; + assert_eq!(node.redirs.len(), 1); + let redir = &node.redirs[0]; - assert!(matches!(redir.class, RedirType::Append)); - assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 1, .. })); + assert!(matches!(redir.class, RedirType::Append)); + assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 1, .. })); } #[test] fn parse_input_redirect() { - let node = parse_command("cat < input.txt"); + let node = parse_command("cat < input.txt"); - assert_eq!(node.redirs.len(), 1); - let redir = &node.redirs[0]; + assert_eq!(node.redirs.len(), 1); + let redir = &node.redirs[0]; - assert!(matches!(redir.class, RedirType::Input)); - assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 0, .. })); + assert!(matches!(redir.class, RedirType::Input)); + assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 0, .. })); } #[test] fn parse_stderr_redirect() { - let node = parse_command("ls 2> errors.txt"); + let node = parse_command("ls 2> errors.txt"); - assert_eq!(node.redirs.len(), 1); - let redir = &node.redirs[0]; + assert_eq!(node.redirs.len(), 1); + let redir = &node.redirs[0]; - assert!(matches!(redir.class, RedirType::Output)); - assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 2, .. })); + assert!(matches!(redir.class, RedirType::Output)); + assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 2, .. })); } #[test] fn parse_stderr_to_stdout() { - let node = parse_command("ls 2>&1"); + let node = parse_command("ls 2>&1"); - assert_eq!(node.redirs.len(), 1); - let redir = &node.redirs[0]; + assert_eq!(node.redirs.len(), 1); + let redir = &node.redirs[0]; - assert!(matches!(redir.io_mode, IoMode::Fd { tgt_fd: 2, src_fd: 1 })); + assert!(matches!( + redir.io_mode, + IoMode::Fd { + tgt_fd: 2, + src_fd: 1 + } + )); } #[test] fn parse_stdout_to_stderr() { - let node = parse_command("echo test 1>&2"); + let node = parse_command("echo test 1>&2"); - assert_eq!(node.redirs.len(), 1); - let redir = &node.redirs[0]; + assert_eq!(node.redirs.len(), 1); + let redir = &node.redirs[0]; - assert!(matches!(redir.io_mode, IoMode::Fd { tgt_fd: 1, src_fd: 2 })); + assert!(matches!( + redir.io_mode, + IoMode::Fd { + tgt_fd: 1, + src_fd: 2 + } + )); } #[test] fn parse_multiple_redirects() { - let node = parse_command("cmd < input.txt > output.txt 2> errors.txt"); + let node = parse_command("cmd < input.txt > output.txt 2> errors.txt"); - assert_eq!(node.redirs.len(), 3); + assert_eq!(node.redirs.len(), 3); - // Input redirect - assert!(matches!(node.redirs[0].class, RedirType::Input)); - assert!(matches!(node.redirs[0].io_mode, IoMode::File { tgt_fd: 0, .. })); + // Input redirect + assert!(matches!(node.redirs[0].class, RedirType::Input)); + assert!(matches!( + node.redirs[0].io_mode, + IoMode::File { tgt_fd: 0, .. } + )); - // Stdout redirect - assert!(matches!(node.redirs[1].class, RedirType::Output)); - assert!(matches!(node.redirs[1].io_mode, IoMode::File { tgt_fd: 1, .. })); + // Stdout redirect + assert!(matches!(node.redirs[1].class, RedirType::Output)); + assert!(matches!( + node.redirs[1].io_mode, + IoMode::File { tgt_fd: 1, .. } + )); - // Stderr redirect - assert!(matches!(node.redirs[2].class, RedirType::Output)); - assert!(matches!(node.redirs[2].io_mode, IoMode::File { tgt_fd: 2, .. })); + // Stderr redirect + assert!(matches!(node.redirs[2].class, RedirType::Output)); + assert!(matches!( + node.redirs[2].io_mode, + IoMode::File { tgt_fd: 2, .. } + )); } #[test] fn parse_custom_fd_redirect() { - let node = parse_command("echo test 3> fd3.txt"); + let node = parse_command("echo test 3> fd3.txt"); - assert_eq!(node.redirs.len(), 1); - let redir = &node.redirs[0]; + assert_eq!(node.redirs.len(), 1); + let redir = &node.redirs[0]; - assert!(matches!(redir.class, RedirType::Output)); - assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 3, .. })); + assert!(matches!(redir.class, RedirType::Output)); + assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 3, .. })); } #[test] fn parse_custom_fd_dup() { - let node = parse_command("cmd 3>&4"); + let node = parse_command("cmd 3>&4"); - assert_eq!(node.redirs.len(), 1); - let redir = &node.redirs[0]; + assert_eq!(node.redirs.len(), 1); + let redir = &node.redirs[0]; - assert!(matches!(redir.io_mode, IoMode::Fd { tgt_fd: 3, src_fd: 4 })); + assert!(matches!( + redir.io_mode, + IoMode::Fd { + tgt_fd: 3, + src_fd: 4 + } + )); } #[test] fn parse_heredoc() { - let node = parse_command("cat << EOF"); + let node = parse_command("cat << EOF"); - assert_eq!(node.redirs.len(), 1); - let redir = &node.redirs[0]; + assert_eq!(node.redirs.len(), 1); + let redir = &node.redirs[0]; - assert!(matches!(redir.class, RedirType::HereDoc)); + assert!(matches!(redir.class, RedirType::HereDoc)); } #[test] fn parse_herestring() { - let node = parse_command("cat <<< 'hello world'"); + let node = parse_command("cat <<< 'hello world'"); - assert_eq!(node.redirs.len(), 1); - let redir = &node.redirs[0]; + assert_eq!(node.redirs.len(), 1); + let redir = &node.redirs[0]; - assert!(matches!(redir.class, RedirType::HereString)); + assert!(matches!(redir.class, RedirType::HereString)); } #[test] fn parse_redirect_with_no_space() { - let node = parse_command("echo hello >output.txt"); + let node = parse_command("echo hello >output.txt"); - assert_eq!(node.redirs.len(), 1); - assert!(matches!(node.redirs[0].class, RedirType::Output)); + assert_eq!(node.redirs.len(), 1); + assert!(matches!(node.redirs[0].class, RedirType::Output)); } #[test] fn parse_redirect_order_preserved() { - let node = parse_command("cmd 2>&1 > file.txt"); + let node = parse_command("cmd 2>&1 > file.txt"); - assert_eq!(node.redirs.len(), 2); + assert_eq!(node.redirs.len(), 2); - // First redirect: 2>&1 - assert!(matches!(node.redirs[0].io_mode, IoMode::Fd { tgt_fd: 2, src_fd: 1 })); + // First redirect: 2>&1 + assert!(matches!( + node.redirs[0].io_mode, + IoMode::Fd { + tgt_fd: 2, + src_fd: 1 + } + )); - // Second redirect: > file.txt - assert!(matches!(node.redirs[1].class, RedirType::Output)); - assert!(matches!(node.redirs[1].io_mode, IoMode::File { tgt_fd: 1, .. })); + // Second redirect: > file.txt + assert!(matches!(node.redirs[1].class, RedirType::Output)); + assert!(matches!( + node.redirs[1].io_mode, + IoMode::File { tgt_fd: 1, .. } + )); } // ============================================================================ @@ -200,148 +251,148 @@ fn parse_redirect_order_preserved() { #[test] fn iostack_new() { - let stack = IoStack::new(); + let stack = IoStack::new(); - assert_eq!(stack.len(), 1, "IoStack should start with one frame"); - assert_eq!(stack.curr_frame().len(), 0, "Initial frame should be empty"); + assert_eq!(stack.len(), 1, "IoStack should start with one frame"); + assert_eq!(stack.curr_frame().len(), 0, "Initial frame should be empty"); } #[test] fn iostack_push_pop_frame() { - let mut stack = IoStack::new(); + let mut stack = IoStack::new(); - // Push a new frame - stack.push_frame(IoFrame::new()); - assert_eq!(stack.len(), 2); + // Push a new frame + stack.push_frame(IoFrame::new()); + assert_eq!(stack.len(), 2); - // Pop it back - let frame = stack.pop_frame(); - assert_eq!(frame.len(), 0); - assert_eq!(stack.len(), 1); + // Pop it back + let frame = stack.pop_frame(); + assert_eq!(frame.len(), 0); + assert_eq!(stack.len(), 1); } #[test] fn iostack_never_empties() { - let mut stack = IoStack::new(); + let mut stack = IoStack::new(); - // Try to pop the last frame - let frame = stack.pop_frame(); - assert_eq!(frame.len(), 0); + // Try to pop the last frame + let frame = stack.pop_frame(); + assert_eq!(frame.len(), 0); - // Stack should still have one frame - assert_eq!(stack.len(), 1); + // Stack should still have one frame + assert_eq!(stack.len(), 1); - // Pop again - should still have one frame - let frame = stack.pop_frame(); - assert_eq!(frame.len(), 0); - assert_eq!(stack.len(), 1); + // Pop again - should still have one frame + let frame = stack.pop_frame(); + assert_eq!(frame.len(), 0); + assert_eq!(stack.len(), 1); } #[test] fn iostack_push_to_frame() { - let mut stack = IoStack::new(); + let mut stack = IoStack::new(); - let redir = crate::parse::Redir::new( - IoMode::fd(1, 2), - RedirType::Output, - ); + let redir = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output); - stack.push_to_frame(redir); - assert_eq!(stack.curr_frame().len(), 1); + stack.push_to_frame(redir); + assert_eq!(stack.curr_frame().len(), 1); } #[test] fn iostack_append_to_frame() { - let mut stack = IoStack::new(); + let mut stack = IoStack::new(); - let redirs = vec![ - crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output), - crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output), - ]; + let redirs = vec![ + crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output), + crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output), + ]; - stack.append_to_frame(redirs); - assert_eq!(stack.curr_frame().len(), 2); + stack.append_to_frame(redirs); + assert_eq!(stack.curr_frame().len(), 2); } #[test] fn iostack_frame_isolation() { - let mut stack = IoStack::new(); + let mut stack = IoStack::new(); - // Add redir to first frame - let redir1 = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output); - stack.push_to_frame(redir1); - assert_eq!(stack.curr_frame().len(), 1); + // Add redir to first frame + let redir1 = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output); + stack.push_to_frame(redir1); + assert_eq!(stack.curr_frame().len(), 1); - // Push new frame - stack.push_frame(IoFrame::new()); - assert_eq!(stack.curr_frame().len(), 0, "New frame should be empty"); + // Push new frame + stack.push_frame(IoFrame::new()); + assert_eq!(stack.curr_frame().len(), 0, "New frame should be empty"); - // Add redir to second frame - let redir2 = crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output); - stack.push_to_frame(redir2); - assert_eq!(stack.curr_frame().len(), 1); + // Add redir to second frame + let redir2 = crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output); + stack.push_to_frame(redir2); + assert_eq!(stack.curr_frame().len(), 1); - // Pop second frame - let frame2 = stack.pop_frame(); - assert_eq!(frame2.len(), 1); + // Pop second frame + let frame2 = stack.pop_frame(); + assert_eq!(frame2.len(), 1); - // First frame should still have its redir - assert_eq!(stack.curr_frame().len(), 1); + // First frame should still have its redir + assert_eq!(stack.curr_frame().len(), 1); } #[test] fn iostack_flatten() { - let mut stack = IoStack::new(); + let mut stack = IoStack::new(); - // Add redir to first frame - let redir1 = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output); - stack.push_to_frame(redir1); + // Add redir to first frame + let redir1 = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output); + stack.push_to_frame(redir1); - // Push new frame with redir - let mut frame2 = IoFrame::new(); - frame2.push(crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output)); - stack.push_frame(frame2); + // Push new frame with redir + let mut frame2 = IoFrame::new(); + frame2.push(crate::parse::Redir::new( + IoMode::fd(2, 1), + RedirType::Output, + )); + stack.push_frame(frame2); - // Push third frame with redir - let mut frame3 = IoFrame::new(); - frame3.push(crate::parse::Redir::new(IoMode::fd(0, 3), RedirType::Input)); - stack.push_frame(frame3); + // Push third frame with redir + let mut frame3 = IoFrame::new(); + frame3.push(crate::parse::Redir::new(IoMode::fd(0, 3), RedirType::Input)); + stack.push_frame(frame3); - assert_eq!(stack.len(), 3); + assert_eq!(stack.len(), 3); - // Flatten - stack.flatten(); + // Flatten + stack.flatten(); - // Should have one frame with all redirects - assert_eq!(stack.len(), 1); - assert_eq!(stack.curr_frame().len(), 3); + // Should have one frame with all redirects + assert_eq!(stack.len(), 1); + assert_eq!(stack.curr_frame().len(), 3); } #[test] fn ioframe_new() { - let frame = IoFrame::new(); - assert_eq!(frame.len(), 0); + let frame = IoFrame::new(); + assert_eq!(frame.len(), 0); } #[test] fn ioframe_from_redirs() { - let redirs = vec![ - crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output), - crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output), - ]; + let redirs = vec![ + crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output), + crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output), + ]; - let frame = IoFrame::from_redirs(redirs); - assert_eq!(frame.len(), 2); + let frame = IoFrame::from_redirs(redirs); + assert_eq!(frame.len(), 2); } #[test] fn ioframe_push() { - let mut frame = IoFrame::new(); + let mut frame = IoFrame::new(); - let redir = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output); - frame.push(redir); + let redir = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output); + frame.push(redir); - assert_eq!(frame.len(), 1); + assert_eq!(frame.len(), 1); } // ============================================================================ @@ -350,28 +401,28 @@ fn ioframe_push() { #[test] fn iomode_fd_construction() { - let io_mode = IoMode::fd(2, 1); + let io_mode = IoMode::fd(2, 1); - match io_mode { - IoMode::Fd { tgt_fd, src_fd } => { - assert_eq!(tgt_fd, 2); - assert_eq!(src_fd, 1); - } - _ => panic!("Expected IoMode::Fd"), - } + match io_mode { + IoMode::Fd { tgt_fd, src_fd } => { + assert_eq!(tgt_fd, 2); + assert_eq!(src_fd, 1); + } + _ => panic!("Expected IoMode::Fd"), + } } #[test] fn iomode_tgt_fd() { - let fd_mode = IoMode::fd(2, 1); - assert_eq!(fd_mode.tgt_fd(), 2); + let fd_mode = IoMode::fd(2, 1); + assert_eq!(fd_mode.tgt_fd(), 2); - let file_mode = IoMode::file(1, std::path::PathBuf::from("test.txt"), RedirType::Output); - assert_eq!(file_mode.tgt_fd(), 1); + let file_mode = IoMode::file(1, std::path::PathBuf::from("test.txt"), RedirType::Output); + assert_eq!(file_mode.tgt_fd(), 1); } #[test] fn iomode_src_fd() { - let fd_mode = IoMode::fd(2, 1); - assert_eq!(fd_mode.src_fd(), 1); + let fd_mode = IoMode::fd(2, 1); + assert_eq!(fd_mode.src_fd(), 1); } diff --git a/src/tests/state.rs b/src/tests/state.rs index f2a6f2b..c82bf69 100644 --- a/src/tests/state.rs +++ b/src/tests/state.rs @@ -6,264 +6,280 @@ use crate::state::{LogTab, ScopeStack, ShellParam, VarFlags, VarTab}; #[test] fn scopestack_new() { - let stack = ScopeStack::new(); + let stack = ScopeStack::new(); - // Should start with one global scope - assert!(stack.var_exists("PATH") || !stack.var_exists("PATH")); // Just check it doesn't panic + // Should start with one global scope + assert!(stack.var_exists("PATH") || !stack.var_exists("PATH")); // Just check + // it doesn't + // panic } #[test] fn scopestack_descend_ascend() { - let mut stack = ScopeStack::new(); + let mut stack = ScopeStack::new(); - // Set a global variable - stack.set_var("GLOBAL", "value1", VarFlags::NONE); - assert_eq!(stack.get_var("GLOBAL"), "value1"); + // Set a global variable + stack.set_var("GLOBAL", "value1", VarFlags::NONE); + assert_eq!(stack.get_var("GLOBAL"), "value1"); - // Descend into a new scope - stack.descend(None); + // Descend into a new scope + stack.descend(None); - // Global should still be visible - assert_eq!(stack.get_var("GLOBAL"), "value1"); + // Global should still be visible + assert_eq!(stack.get_var("GLOBAL"), "value1"); - // Set a local variable - stack.set_var("LOCAL", "value2", VarFlags::LOCAL); - assert_eq!(stack.get_var("LOCAL"), "value2"); + // Set a local variable + stack.set_var("LOCAL", "value2", VarFlags::LOCAL); + assert_eq!(stack.get_var("LOCAL"), "value2"); - // Ascend back to global scope - stack.ascend(); + // Ascend back to global scope + stack.ascend(); - // Global should still exist - assert_eq!(stack.get_var("GLOBAL"), "value1"); + // Global should still exist + assert_eq!(stack.get_var("GLOBAL"), "value1"); - // Local should no longer be visible - assert_eq!(stack.get_var("LOCAL"), ""); + // Local should no longer be visible + assert_eq!(stack.get_var("LOCAL"), ""); } #[test] fn scopestack_variable_shadowing() { - let mut stack = ScopeStack::new(); + let mut stack = ScopeStack::new(); - // Set global variable - stack.set_var("VAR", "global", VarFlags::NONE); - assert_eq!(stack.get_var("VAR"), "global"); + // Set global variable + stack.set_var("VAR", "global", VarFlags::NONE); + assert_eq!(stack.get_var("VAR"), "global"); - // Descend into local scope - stack.descend(None); + // Descend into local scope + stack.descend(None); - // Set local variable with same name - stack.set_var("VAR", "local", VarFlags::LOCAL); - assert_eq!(stack.get_var("VAR"), "local", "Local should shadow global"); + // Set local variable with same name + stack.set_var("VAR", "local", VarFlags::LOCAL); + assert_eq!(stack.get_var("VAR"), "local", "Local should shadow global"); - // Ascend back - stack.ascend(); + // Ascend back + stack.ascend(); - // Global should be restored - assert_eq!(stack.get_var("VAR"), "global", "Global should be unchanged after ascend"); + // Global should be restored + assert_eq!( + stack.get_var("VAR"), + "global", + "Global should be unchanged after ascend" + ); } #[test] fn scopestack_local_vs_global_flag() { - let mut stack = ScopeStack::new(); + let mut stack = ScopeStack::new(); - // Descend into a local scope - stack.descend(None); + // Descend into a local scope + stack.descend(None); - // Set with LOCAL flag - should go in current scope - stack.set_var("LOCAL_VAR", "local", VarFlags::LOCAL); + // Set with LOCAL flag - should go in current scope + stack.set_var("LOCAL_VAR", "local", VarFlags::LOCAL); - // Set without LOCAL flag - should go in global scope - stack.set_var("GLOBAL_VAR", "global", VarFlags::NONE); + // Set without LOCAL flag - should go in global scope + stack.set_var("GLOBAL_VAR", "global", VarFlags::NONE); - // Both visible from local scope - assert_eq!(stack.get_var("LOCAL_VAR"), "local"); - assert_eq!(stack.get_var("GLOBAL_VAR"), "global"); + // Both visible from local scope + assert_eq!(stack.get_var("LOCAL_VAR"), "local"); + assert_eq!(stack.get_var("GLOBAL_VAR"), "global"); - // Ascend to global - stack.ascend(); + // Ascend to global + stack.ascend(); - // Only global var should be visible - assert_eq!(stack.get_var("GLOBAL_VAR"), "global"); - assert_eq!(stack.get_var("LOCAL_VAR"), ""); + // Only global var should be visible + assert_eq!(stack.get_var("GLOBAL_VAR"), "global"); + assert_eq!(stack.get_var("LOCAL_VAR"), ""); } #[test] fn scopestack_multiple_levels() { - let mut stack = ScopeStack::new(); + let mut stack = ScopeStack::new(); - stack.set_var("LEVEL0", "global", VarFlags::NONE); + stack.set_var("LEVEL0", "global", VarFlags::NONE); - // Level 1 - stack.descend(None); - stack.set_var("LEVEL1", "first", VarFlags::LOCAL); + // Level 1 + stack.descend(None); + stack.set_var("LEVEL1", "first", VarFlags::LOCAL); - // Level 2 - stack.descend(None); - stack.set_var("LEVEL2", "second", VarFlags::LOCAL); + // Level 2 + stack.descend(None); + stack.set_var("LEVEL2", "second", VarFlags::LOCAL); - // All variables visible from deepest scope - assert_eq!(stack.get_var("LEVEL0"), "global"); - assert_eq!(stack.get_var("LEVEL1"), "first"); - assert_eq!(stack.get_var("LEVEL2"), "second"); + // All variables visible from deepest scope + assert_eq!(stack.get_var("LEVEL0"), "global"); + assert_eq!(stack.get_var("LEVEL1"), "first"); + assert_eq!(stack.get_var("LEVEL2"), "second"); - // Ascend to level 1 - stack.ascend(); - assert_eq!(stack.get_var("LEVEL0"), "global"); - assert_eq!(stack.get_var("LEVEL1"), "first"); - assert_eq!(stack.get_var("LEVEL2"), ""); + // Ascend to level 1 + stack.ascend(); + assert_eq!(stack.get_var("LEVEL0"), "global"); + assert_eq!(stack.get_var("LEVEL1"), "first"); + assert_eq!(stack.get_var("LEVEL2"), ""); - // Ascend to global - stack.ascend(); - assert_eq!(stack.get_var("LEVEL0"), "global"); - assert_eq!(stack.get_var("LEVEL1"), ""); - assert_eq!(stack.get_var("LEVEL2"), ""); + // Ascend to global + stack.ascend(); + assert_eq!(stack.get_var("LEVEL0"), "global"); + assert_eq!(stack.get_var("LEVEL1"), ""); + assert_eq!(stack.get_var("LEVEL2"), ""); } #[test] fn scopestack_cannot_ascend_past_global() { - let mut stack = ScopeStack::new(); + let mut stack = ScopeStack::new(); - stack.set_var("VAR", "value", VarFlags::NONE); + stack.set_var("VAR", "value", VarFlags::NONE); - // Try to ascend from global scope (should be no-op) - stack.ascend(); - stack.ascend(); - stack.ascend(); + // Try to ascend from global scope (should be no-op) + stack.ascend(); + stack.ascend(); + stack.ascend(); - // Variable should still exist - assert_eq!(stack.get_var("VAR"), "value"); + // Variable should still exist + assert_eq!(stack.get_var("VAR"), "value"); } #[test] fn scopestack_descend_with_args() { - let mut stack = ScopeStack::new(); + let mut stack = ScopeStack::new(); - // Get initial param values from global scope (test process args) - let global_param_1 = stack.get_param(ShellParam::Pos(1)); + // Get initial param values from global scope (test process args) + let global_param_1 = stack.get_param(ShellParam::Pos(1)); - // Descend with positional parameters - let args = vec!["local_arg1".to_string(), "local_arg2".to_string()]; - stack.descend(Some(args)); + // Descend with positional parameters + let args = vec!["local_arg1".to_string(), "local_arg2".to_string()]; + stack.descend(Some(args)); - // In local scope, positional params come from the VarTab created during descend - // VarTab::new() initializes with process args, then our args are appended - // So we check that SOME positional parameter exists (implementation detail may vary) - let local_param = stack.get_param(ShellParam::Pos(1)); - assert!(!local_param.is_empty(), "Should have positional parameters in local scope"); + // In local scope, positional params come from the VarTab created during descend + // VarTab::new() initializes with process args, then our args are appended + // So we check that SOME positional parameter exists (implementation detail may + // vary) + let local_param = stack.get_param(ShellParam::Pos(1)); + assert!( + !local_param.is_empty(), + "Should have positional parameters in local scope" + ); - // Ascend back - stack.ascend(); + // Ascend back + stack.ascend(); - // Should be back to global scope parameters - assert_eq!(stack.get_param(ShellParam::Pos(1)), global_param_1); + // Should be back to global scope parameters + assert_eq!(stack.get_param(ShellParam::Pos(1)), global_param_1); } #[test] fn scopestack_global_parameters() { - let mut stack = ScopeStack::new(); + let mut stack = ScopeStack::new(); - // Set global parameters - stack.set_param(ShellParam::Status, "0"); - stack.set_param(ShellParam::LastJob, "1234"); + // Set global parameters + stack.set_param(ShellParam::Status, "0"); + stack.set_param(ShellParam::LastJob, "1234"); - assert_eq!(stack.get_param(ShellParam::Status), "0"); - assert_eq!(stack.get_param(ShellParam::LastJob), "1234"); + assert_eq!(stack.get_param(ShellParam::Status), "0"); + assert_eq!(stack.get_param(ShellParam::LastJob), "1234"); - // Descend into local scope - stack.descend(None); + // Descend into local scope + stack.descend(None); - // Global parameters should still be visible - assert_eq!(stack.get_param(ShellParam::Status), "0"); - assert_eq!(stack.get_param(ShellParam::LastJob), "1234"); + // Global parameters should still be visible + assert_eq!(stack.get_param(ShellParam::Status), "0"); + assert_eq!(stack.get_param(ShellParam::LastJob), "1234"); - // Modify global parameter from local scope - stack.set_param(ShellParam::Status, "1"); - assert_eq!(stack.get_param(ShellParam::Status), "1"); + // Modify global parameter from local scope + stack.set_param(ShellParam::Status, "1"); + assert_eq!(stack.get_param(ShellParam::Status), "1"); - // Ascend - stack.ascend(); + // Ascend + stack.ascend(); - // Global parameter should retain modified value - assert_eq!(stack.get_param(ShellParam::Status), "1"); + // Global parameter should retain modified value + assert_eq!(stack.get_param(ShellParam::Status), "1"); } #[test] fn scopestack_unset_var() { - let mut stack = ScopeStack::new(); + let mut stack = ScopeStack::new(); - stack.set_var("VAR", "value", VarFlags::NONE); - assert_eq!(stack.get_var("VAR"), "value"); + stack.set_var("VAR", "value", VarFlags::NONE); + assert_eq!(stack.get_var("VAR"), "value"); - stack.unset_var("VAR"); - assert_eq!(stack.get_var("VAR"), ""); - assert!(!stack.var_exists("VAR")); + stack.unset_var("VAR"); + assert_eq!(stack.get_var("VAR"), ""); + assert!(!stack.var_exists("VAR")); } #[test] fn scopestack_unset_finds_innermost() { - let mut stack = ScopeStack::new(); + let mut stack = ScopeStack::new(); - // Set global - stack.set_var("VAR", "global", VarFlags::NONE); + // Set global + stack.set_var("VAR", "global", VarFlags::NONE); - // Descend and shadow - stack.descend(None); - stack.set_var("VAR", "local", VarFlags::LOCAL); - assert_eq!(stack.get_var("VAR"), "local"); + // Descend and shadow + stack.descend(None); + stack.set_var("VAR", "local", VarFlags::LOCAL); + assert_eq!(stack.get_var("VAR"), "local"); - // Unset should remove local, revealing global - stack.unset_var("VAR"); - assert_eq!(stack.get_var("VAR"), "global"); + // Unset should remove local, revealing global + stack.unset_var("VAR"); + assert_eq!(stack.get_var("VAR"), "global"); } #[test] fn scopestack_export_var() { - let mut stack = ScopeStack::new(); + let mut stack = ScopeStack::new(); - stack.set_var("VAR", "value", VarFlags::NONE); + stack.set_var("VAR", "value", VarFlags::NONE); - // Export the variable - stack.export_var("VAR"); + // Export the variable + stack.export_var("VAR"); - // Variable should still be accessible (flag is internal detail) - assert_eq!(stack.get_var("VAR"), "value"); + // Variable should still be accessible (flag is internal detail) + assert_eq!(stack.get_var("VAR"), "value"); } #[test] fn scopestack_var_exists() { - let mut stack = ScopeStack::new(); + let mut stack = ScopeStack::new(); - assert!(!stack.var_exists("NONEXISTENT")); + assert!(!stack.var_exists("NONEXISTENT")); - stack.set_var("EXISTS", "yes", VarFlags::NONE); - assert!(stack.var_exists("EXISTS")); + stack.set_var("EXISTS", "yes", VarFlags::NONE); + assert!(stack.var_exists("EXISTS")); - stack.descend(None); - assert!(stack.var_exists("EXISTS"), "Global var should be visible in local scope"); + stack.descend(None); + assert!( + stack.var_exists("EXISTS"), + "Global var should be visible in local scope" + ); - stack.set_var("LOCAL", "yes", VarFlags::LOCAL); - assert!(stack.var_exists("LOCAL")); + stack.set_var("LOCAL", "yes", VarFlags::LOCAL); + assert!(stack.var_exists("LOCAL")); - stack.ascend(); - assert!(!stack.var_exists("LOCAL"), "Local var should not exist after ascend"); + stack.ascend(); + assert!( + !stack.var_exists("LOCAL"), + "Local var should not exist after ascend" + ); } #[test] fn scopestack_flatten_vars() { - let mut stack = ScopeStack::new(); + let mut stack = ScopeStack::new(); - stack.set_var("GLOBAL1", "g1", VarFlags::NONE); - stack.set_var("GLOBAL2", "g2", VarFlags::NONE); + stack.set_var("GLOBAL1", "g1", VarFlags::NONE); + stack.set_var("GLOBAL2", "g2", VarFlags::NONE); - stack.descend(None); - stack.set_var("LOCAL1", "l1", VarFlags::LOCAL); + stack.descend(None); + stack.set_var("LOCAL1", "l1", VarFlags::LOCAL); - let flattened = stack.flatten_vars(); + let flattened = stack.flatten_vars(); - // Should contain variables from all scopes - assert!(flattened.contains_key("GLOBAL1")); - assert!(flattened.contains_key("GLOBAL2")); - assert!(flattened.contains_key("LOCAL1")); + // Should contain variables from all scopes + assert!(flattened.contains_key("GLOBAL1")); + assert!(flattened.contains_key("GLOBAL2")); + assert!(flattened.contains_key("LOCAL1")); } // ============================================================================ @@ -272,78 +288,81 @@ fn scopestack_flatten_vars() { #[test] fn logtab_new() { - let logtab = LogTab::new(); - assert_eq!(logtab.funcs().len(), 0); - assert_eq!(logtab.aliases().len(), 0); + let logtab = LogTab::new(); + assert_eq!(logtab.funcs().len(), 0); + assert_eq!(logtab.aliases().len(), 0); } #[test] fn logtab_insert_get_alias() { - let mut logtab = LogTab::new(); + let mut logtab = LogTab::new(); - logtab.insert_alias("ll", "ls -la"); - assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string())); - assert_eq!(logtab.get_alias("nonexistent"), None); + logtab.insert_alias("ll", "ls -la"); + assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string())); + assert_eq!(logtab.get_alias("nonexistent"), None); } #[test] fn logtab_overwrite_alias() { - let mut logtab = LogTab::new(); + let mut logtab = LogTab::new(); - logtab.insert_alias("ll", "ls -la"); - assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string())); + logtab.insert_alias("ll", "ls -la"); + assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string())); - logtab.insert_alias("ll", "ls -lah"); - assert_eq!(logtab.get_alias("ll"), Some("ls -lah".to_string())); + logtab.insert_alias("ll", "ls -lah"); + assert_eq!(logtab.get_alias("ll"), Some("ls -lah".to_string())); } #[test] fn logtab_remove_alias() { - let mut logtab = LogTab::new(); + let mut logtab = LogTab::new(); - logtab.insert_alias("ll", "ls -la"); - assert!(logtab.get_alias("ll").is_some()); + logtab.insert_alias("ll", "ls -la"); + assert!(logtab.get_alias("ll").is_some()); - logtab.remove_alias("ll"); - assert!(logtab.get_alias("ll").is_none()); + logtab.remove_alias("ll"); + assert!(logtab.get_alias("ll").is_none()); } #[test] fn logtab_clear_aliases() { - let mut logtab = LogTab::new(); + let mut logtab = LogTab::new(); - logtab.insert_alias("ll", "ls -la"); - logtab.insert_alias("la", "ls -A"); - logtab.insert_alias("l", "ls -CF"); + logtab.insert_alias("ll", "ls -la"); + logtab.insert_alias("la", "ls -A"); + logtab.insert_alias("l", "ls -CF"); - assert_eq!(logtab.aliases().len(), 3); + assert_eq!(logtab.aliases().len(), 3); - logtab.clear_aliases(); - assert_eq!(logtab.aliases().len(), 0); + logtab.clear_aliases(); + assert_eq!(logtab.aliases().len(), 0); } #[test] fn logtab_multiple_aliases() { - let mut logtab = LogTab::new(); + let mut logtab = LogTab::new(); - logtab.insert_alias("ll", "ls -la"); - logtab.insert_alias("la", "ls -A"); - logtab.insert_alias("grep", "grep --color=auto"); + logtab.insert_alias("ll", "ls -la"); + logtab.insert_alias("la", "ls -A"); + logtab.insert_alias("grep", "grep --color=auto"); - assert_eq!(logtab.aliases().len(), 3); - assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string())); - assert_eq!(logtab.get_alias("la"), Some("ls -A".to_string())); - assert_eq!(logtab.get_alias("grep"), Some("grep --color=auto".to_string())); + assert_eq!(logtab.aliases().len(), 3); + assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string())); + assert_eq!(logtab.get_alias("la"), Some("ls -A".to_string())); + assert_eq!( + logtab.get_alias("grep"), + Some("grep --color=auto".to_string()) + ); } -// Note: Function tests are limited because ShFunc requires complex setup (parsed AST) -// We'll test the basic storage/retrieval mechanics +// Note: Function tests are limited because ShFunc requires complex setup +// (parsed AST) We'll test the basic storage/retrieval mechanics #[test] fn logtab_funcs_empty_initially() { - let logtab = LogTab::new(); - assert_eq!(logtab.funcs().len(), 0); - assert!(logtab.get_func("nonexistent").is_none()); + let logtab = LogTab::new(); + assert_eq!(logtab.funcs().len(), 0); + assert!(logtab.get_func("nonexistent").is_none()); } // ============================================================================ @@ -352,109 +371,112 @@ fn logtab_funcs_empty_initially() { #[test] fn vartab_new() { - let vartab = VarTab::new(); - // VarTab initializes with some default params, just check it doesn't panic - assert!(vartab.get_var("NONEXISTENT").is_empty()); + let vartab = VarTab::new(); + // VarTab initializes with some default params, just check it doesn't panic + assert!(vartab.get_var("NONEXISTENT").is_empty()); } #[test] fn vartab_set_get_var() { - let mut vartab = VarTab::new(); + let mut vartab = VarTab::new(); - vartab.set_var("TEST", "value", VarFlags::NONE); - assert_eq!(vartab.get_var("TEST"), "value"); + vartab.set_var("TEST", "value", VarFlags::NONE); + assert_eq!(vartab.get_var("TEST"), "value"); } #[test] fn vartab_overwrite_var() { - let mut vartab = VarTab::new(); + let mut vartab = VarTab::new(); - vartab.set_var("VAR", "value1", VarFlags::NONE); - assert_eq!(vartab.get_var("VAR"), "value1"); + vartab.set_var("VAR", "value1", VarFlags::NONE); + assert_eq!(vartab.get_var("VAR"), "value1"); - vartab.set_var("VAR", "value2", VarFlags::NONE); - assert_eq!(vartab.get_var("VAR"), "value2"); + vartab.set_var("VAR", "value2", VarFlags::NONE); + assert_eq!(vartab.get_var("VAR"), "value2"); } #[test] fn vartab_var_exists() { - let mut vartab = VarTab::new(); + let mut vartab = VarTab::new(); - assert!(!vartab.var_exists("TEST")); + assert!(!vartab.var_exists("TEST")); - vartab.set_var("TEST", "value", VarFlags::NONE); - assert!(vartab.var_exists("TEST")); + vartab.set_var("TEST", "value", VarFlags::NONE); + assert!(vartab.var_exists("TEST")); } #[test] fn vartab_unset_var() { - let mut vartab = VarTab::new(); + let mut vartab = VarTab::new(); - vartab.set_var("VAR", "value", VarFlags::NONE); - assert!(vartab.var_exists("VAR")); + vartab.set_var("VAR", "value", VarFlags::NONE); + assert!(vartab.var_exists("VAR")); - vartab.unset_var("VAR"); - assert!(!vartab.var_exists("VAR")); - assert_eq!(vartab.get_var("VAR"), ""); + vartab.unset_var("VAR"); + assert!(!vartab.var_exists("VAR")); + assert_eq!(vartab.get_var("VAR"), ""); } #[test] fn vartab_export_var() { - let mut vartab = VarTab::new(); + let mut vartab = VarTab::new(); - vartab.set_var("VAR", "value", VarFlags::NONE); - vartab.export_var("VAR"); + vartab.set_var("VAR", "value", VarFlags::NONE); + vartab.export_var("VAR"); - // Variable should still be accessible - assert_eq!(vartab.get_var("VAR"), "value"); + // Variable should still be accessible + assert_eq!(vartab.get_var("VAR"), "value"); } #[test] fn vartab_positional_params() { - let mut vartab = VarTab::new(); + let mut vartab = VarTab::new(); - // Get the current argv length - let initial_len = vartab.sh_argv().len(); + // Get the current argv length + let initial_len = vartab.sh_argv().len(); - // Clear and reinitialize with known args - vartab.clear_args(); // This keeps $0 as current exe + // Clear and reinitialize with known args + vartab.clear_args(); // This keeps $0 as current exe - // After clear_args, should have just $0 - // Push additional args - vartab.bpush_arg("test_arg1".to_string()); - vartab.bpush_arg("test_arg2".to_string()); + // After clear_args, should have just $0 + // Push additional args + vartab.bpush_arg("test_arg1".to_string()); + vartab.bpush_arg("test_arg2".to_string()); - // Now sh_argv should be: [exe_path, test_arg1, test_arg2] - // Pos(0) = exe_path, Pos(1) = test_arg1, Pos(2) = test_arg2 - let final_len = vartab.sh_argv().len(); - assert!(final_len > initial_len || final_len >= 1, "Should have arguments"); + // Now sh_argv should be: [exe_path, test_arg1, test_arg2] + // Pos(0) = exe_path, Pos(1) = test_arg1, Pos(2) = test_arg2 + let final_len = vartab.sh_argv().len(); + assert!( + final_len > initial_len || final_len >= 1, + "Should have arguments" + ); - // Just verify we can retrieve the last args we pushed - let last_idx = final_len - 1; - assert_eq!(vartab.get_param(ShellParam::Pos(last_idx)), "test_arg2"); + // Just verify we can retrieve the last args we pushed + let last_idx = final_len - 1; + assert_eq!(vartab.get_param(ShellParam::Pos(last_idx)), "test_arg2"); } #[test] fn vartab_shell_argv_operations() { - let mut vartab = VarTab::new(); + let mut vartab = VarTab::new(); - // Clear initial args and set fresh ones - vartab.clear_args(); + // Clear initial args and set fresh ones + vartab.clear_args(); - // Push args (clear_args leaves $0, so these become $1, $2, $3) - vartab.bpush_arg("arg1".to_string()); - vartab.bpush_arg("arg2".to_string()); - vartab.bpush_arg("arg3".to_string()); + // Push args (clear_args leaves $0, so these become $1, $2, $3) + vartab.bpush_arg("arg1".to_string()); + vartab.bpush_arg("arg2".to_string()); + vartab.bpush_arg("arg3".to_string()); - // Get initial arg count - let initial_len = vartab.sh_argv().len(); + // Get initial arg count + let initial_len = vartab.sh_argv().len(); - // Pop first arg (removes $0) - let popped = vartab.fpop_arg(); - assert!(popped.is_some()); + // Pop first arg (removes $0) + let popped = vartab.fpop_arg(); + assert!(popped.is_some()); - // Should have one fewer arg - assert_eq!(vartab.sh_argv().len(), initial_len - 1); + // Should have one fewer arg + assert_eq!(vartab.sh_argv().len(), initial_len - 1); } // ============================================================================ @@ -463,39 +485,39 @@ fn vartab_shell_argv_operations() { #[test] fn varflags_none() { - let flags = VarFlags::NONE; - assert!(!flags.contains(VarFlags::EXPORT)); - assert!(!flags.contains(VarFlags::LOCAL)); - assert!(!flags.contains(VarFlags::READONLY)); + let flags = VarFlags::NONE; + assert!(!flags.contains(VarFlags::EXPORT)); + assert!(!flags.contains(VarFlags::LOCAL)); + assert!(!flags.contains(VarFlags::READONLY)); } #[test] fn varflags_export() { - let flags = VarFlags::EXPORT; - assert!(flags.contains(VarFlags::EXPORT)); - assert!(!flags.contains(VarFlags::LOCAL)); + let flags = VarFlags::EXPORT; + assert!(flags.contains(VarFlags::EXPORT)); + assert!(!flags.contains(VarFlags::LOCAL)); } #[test] fn varflags_local() { - let flags = VarFlags::LOCAL; - assert!(!flags.contains(VarFlags::EXPORT)); - assert!(flags.contains(VarFlags::LOCAL)); + let flags = VarFlags::LOCAL; + assert!(!flags.contains(VarFlags::EXPORT)); + assert!(flags.contains(VarFlags::LOCAL)); } #[test] fn varflags_combine() { - let flags = VarFlags::EXPORT | VarFlags::LOCAL; - assert!(flags.contains(VarFlags::EXPORT)); - assert!(flags.contains(VarFlags::LOCAL)); - assert!(!flags.contains(VarFlags::READONLY)); + let flags = VarFlags::EXPORT | VarFlags::LOCAL; + assert!(flags.contains(VarFlags::EXPORT)); + assert!(flags.contains(VarFlags::LOCAL)); + assert!(!flags.contains(VarFlags::READONLY)); } #[test] fn varflags_readonly() { - let flags = VarFlags::READONLY; - assert!(flags.contains(VarFlags::READONLY)); - assert!(!flags.contains(VarFlags::EXPORT)); + let flags = VarFlags::READONLY; + assert!(flags.contains(VarFlags::READONLY)); + assert!(!flags.contains(VarFlags::EXPORT)); } // ============================================================================ @@ -504,49 +526,70 @@ fn varflags_readonly() { #[test] fn shellparam_is_global() { - assert!(ShellParam::Status.is_global()); - assert!(ShellParam::ShPid.is_global()); - assert!(ShellParam::LastJob.is_global()); - assert!(ShellParam::ShellName.is_global()); + assert!(ShellParam::Status.is_global()); + assert!(ShellParam::ShPid.is_global()); + assert!(ShellParam::LastJob.is_global()); + assert!(ShellParam::ShellName.is_global()); - assert!(!ShellParam::Pos(1).is_global()); - assert!(!ShellParam::AllArgs.is_global()); - assert!(!ShellParam::AllArgsStr.is_global()); - assert!(!ShellParam::ArgCount.is_global()); + assert!(!ShellParam::Pos(1).is_global()); + assert!(!ShellParam::AllArgs.is_global()); + assert!(!ShellParam::AllArgsStr.is_global()); + assert!(!ShellParam::ArgCount.is_global()); } #[test] fn shellparam_from_str() { - assert!(matches!("?".parse::().unwrap(), ShellParam::Status)); - assert!(matches!("$".parse::().unwrap(), ShellParam::ShPid)); - assert!(matches!("!".parse::().unwrap(), ShellParam::LastJob)); - assert!(matches!("0".parse::().unwrap(), ShellParam::ShellName)); - assert!(matches!("@".parse::().unwrap(), ShellParam::AllArgs)); - assert!(matches!("*".parse::().unwrap(), ShellParam::AllArgsStr)); - assert!(matches!("#".parse::().unwrap(), ShellParam::ArgCount)); + assert!(matches!( + "?".parse::().unwrap(), + ShellParam::Status + )); + assert!(matches!( + "$".parse::().unwrap(), + ShellParam::ShPid + )); + assert!(matches!( + "!".parse::().unwrap(), + ShellParam::LastJob + )); + assert!(matches!( + "0".parse::().unwrap(), + ShellParam::ShellName + )); + assert!(matches!( + "@".parse::().unwrap(), + ShellParam::AllArgs + )); + assert!(matches!( + "*".parse::().unwrap(), + ShellParam::AllArgsStr + )); + assert!(matches!( + "#".parse::().unwrap(), + ShellParam::ArgCount + )); - match "1".parse::().unwrap() { - ShellParam::Pos(n) => assert_eq!(n, 1), - _ => panic!("Expected Pos(1)"), - } + match "1".parse::().unwrap() { + ShellParam::Pos(n) => assert_eq!(n, 1), + _ => panic!("Expected Pos(1)"), + } - match "42".parse::().unwrap() { - ShellParam::Pos(n) => assert_eq!(n, 42), - _ => panic!("Expected Pos(42)"), - } + match "42".parse::().unwrap() { + ShellParam::Pos(n) => assert_eq!(n, 42), + _ => panic!("Expected Pos(42)"), + } - assert!("invalid".parse::().is_err()); + assert!("invalid".parse::().is_err()); } #[test] fn shellparam_display() { - assert_eq!(ShellParam::Status.to_string(), "?"); - assert_eq!(ShellParam::ShPid.to_string(), "$"); - assert_eq!(ShellParam::LastJob.to_string(), "!"); - assert_eq!(ShellParam::ShellName.to_string(), "0"); - assert_eq!(ShellParam::AllArgs.to_string(), "@"); - assert_eq!(ShellParam::AllArgsStr.to_string(), "*"); - assert_eq!(ShellParam::ArgCount.to_string(), "#"); - assert_eq!(ShellParam::Pos(1).to_string(), "1"); - assert_eq!(ShellParam::Pos(99).to_string(), "99"); + assert_eq!(ShellParam::Status.to_string(), "?"); + assert_eq!(ShellParam::ShPid.to_string(), "$"); + assert_eq!(ShellParam::LastJob.to_string(), "!"); + assert_eq!(ShellParam::ShellName.to_string(), "0"); + assert_eq!(ShellParam::AllArgs.to_string(), "@"); + assert_eq!(ShellParam::AllArgsStr.to_string(), "*"); + assert_eq!(ShellParam::ArgCount.to_string(), "#"); + assert_eq!(ShellParam::Pos(1).to_string(), "1"); + assert_eq!(ShellParam::Pos(99).to_string(), "99"); }