diff --git a/src/main.rs b/src/main.rs index 68b0bf9..99aed34 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; @@ -23,7 +23,6 @@ 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; @@ -33,7 +32,7 @@ use crate::libsh::sys::TTY_FILENO; use crate::parse::execute::exec_input; use crate::prelude::*; use crate::prompt::get_prompt; -use crate::prompt::readline::term::raw_mode; +use crate::prompt::readline::term::{RawModeGuard, raw_mode}; use crate::prompt::readline::{FernVi, ReadlineEvent}; use crate::signal::{QUIT_CODE, check_signals, sig_setup, signals_pending}; use crate::state::{read_logic, source_rc, write_jobs, write_meta}; @@ -42,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, #[arg(short)] interactive: bool, @@ -71,7 +70,7 @@ 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(|_| {}); } /// We need to make sure that even if we panic, our child processes get sighup @@ -89,196 +88,192 @@ fn setup_panic_handler() { } fn main() -> ExitCode { - env_logger::init(); - kickstart_lazy_evals(); + env_logger::init(); + kickstart_lazy_evals(); setup_panic_handler(); - let mut args = FernArgs::parse(); + let mut args = FernArgs::parse(); if env::args().next().is_some_and(|a| a.starts_with('-')) { // first arg is '-fern' // meaning we are in a login shell args.login_shell = true; } - if args.version { - println!("fern {} ({} {})", env!("CARGO_PKG_VERSION"), std::env::consts::ARCH, std::env::consts::OS); - return ExitCode::SUCCESS; - } + if args.version { + println!("fern {} ({} {})", env!("CARGO_PKG_VERSION"), std::env::consts::ARCH, std::env::consts::OS); + 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}"); + } write_jobs(|j| j.hang_up()); - 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(), *TTY_FILENO) { - 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(), *TTY_FILENO) { + 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(*TTY_FILENO) }, - PollFlags::POLLIN, - )]; + // Poll for stdin input + let mut fds = [PollFd::new( + unsafe { BorrowedFd::borrow_raw(*TTY_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(*TTY_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(*TTY_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) = RawModeGuard::with_cooked_mode(|| 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 a0c965e..bd88ddd 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -155,7 +155,9 @@ pub fn exec_input(input: String, io_stack: Option, interactive: bool) - return Ok(()); } - let mut dispatcher = Dispatcher::new(parser.extract_nodes(), interactive); + let nodes = parser.extract_nodes(); + + let mut dispatcher = Dispatcher::new(nodes, interactive); if let Some(mut stack) = io_stack { dispatcher.io_stack.extend(stack.drain(..)); } diff --git a/src/parse/lex.rs b/src/parse/lex.rs index 19b350e..7455641 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -659,6 +659,9 @@ impl LexStream { } _ if is_cmd_sub(text) => { new_tk.mark(TkFlags::IS_CMDSUB); + if self.next_is_cmd() { + new_tk.mark(TkFlags::IS_CMD); + } self.set_next_is_cmd(false); } _ => { @@ -846,11 +849,26 @@ pub fn is_field_sep(ch: char) -> bool { } pub fn is_keyword(slice: &str) -> bool { - KEYWORDS.contains(&slice) || (slice.ends_with("()") && !slice.ends_with("\\()")) + KEYWORDS.contains(&slice) || ends_with_unescaped(slice, "()") } pub fn is_cmd_sub(slice: &str) -> bool { - (slice.starts_with("$(") && slice.ends_with(')')) && !slice.ends_with("\\)") + slice.starts_with("$(") && ends_with_unescaped(slice,")") +} + +pub fn ends_with_unescaped(slice: &str, pat: &str) -> bool { + slice.ends_with(pat) && !pos_is_escaped(slice, slice.len() - pat.len()) +} + +pub fn pos_is_escaped(slice: &str, pos: usize) -> bool { + let bytes = slice.as_bytes(); + let mut escaped = false; + let mut i = pos; + while i > 0 && bytes[i - 1] == b'\\' { + escaped = !escaped; + i -= 1; + } + escaped } pub fn lookahead(pat: &str, mut chars: Chars) -> Option { diff --git a/src/prompt/readline/highlight.rs b/src/prompt/readline/highlight.rs index e5d7310..416103b 100644 --- a/src/prompt/readline/highlight.rs +++ b/src/prompt/readline/highlight.rs @@ -184,14 +184,16 @@ impl Highlighter { input_chars.next(); } + let inner_clean = Self::strip_markers(&inner); + // 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("<(") { + if inner_clean.starts_with("<(") { "<(" - } else if inner.starts_with(">(") { + } else if inner_clean.starts_with(">(") { ">(" } else { "<(" @@ -199,14 +201,13 @@ impl Highlighter { } _ => unreachable!(), }; - let inner_content = if incomplete { - inner.strip_prefix(prefix).unwrap_or(&inner) + inner_clean.strip_prefix(prefix).unwrap_or(&inner_clean) } else { - inner + inner_clean .strip_prefix(prefix) .and_then(|s| s.strip_suffix(")")) - .unwrap_or(&inner) + .unwrap_or(&inner_clean) }; let mut recursive_highlighter = Self::new();