From 1f9d59b5467d1aba947802a2a0797aa87d417409 Mon Sep 17 00:00:00 2001 From: pagedmov Date: Sun, 15 Mar 2026 11:11:35 -0400 Subject: [PATCH] fixed ss3 escape code parsing, added a cursor mode reset that triggers on child exit --- src/builtin/mod.rs | 4 +- src/builtin/seek.rs | 152 ++++++------ src/expand.rs | 219 ++++++++-------- src/getopt.rs | 8 +- src/libsh/error.rs | 6 +- src/libsh/guards.rs | 8 +- src/libsh/sys.rs | 3 +- src/parse/execute.rs | 46 +++- src/parse/lex.rs | 534 ++++++++++++++++++++-------------------- src/parse/mod.rs | 325 ++++++++++++------------ src/procio.rs | 158 ++++++------ src/readline/linebuf.rs | 251 +++++++++---------- src/readline/mod.rs | 15 -- src/readline/term.rs | 73 ++++-- src/readline/tests.rs | 223 ++++++++++------- src/shopt.rs | 3 +- src/state.rs | 28 +-- 17 files changed, 1099 insertions(+), 957 deletions(-) diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index 9dcc65d..db8f624 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -18,20 +18,20 @@ pub mod map; pub mod pwd; pub mod read; pub mod resource; +pub mod seek; pub mod shift; pub mod shopt; pub mod source; pub mod test; // [[ ]] thing pub mod trap; pub mod varcmds; -pub mod seek; pub const BUILTINS: [&str; 50] = [ "echo", "cd", "read", "export", "local", "pwd", "source", ".", "shift", "jobs", "fg", "bg", "disown", "alias", "unalias", "return", "break", "continue", "exit", "shopt", "builtin", "command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly", "unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type", - "getopts", "keymap", "read_key", "autocmd", "ulimit", "umask", "seek" + "getopts", "keymap", "read_key", "autocmd", "ulimit", "umask", "seek", ]; pub fn true_builtin() -> ShResult<()> { diff --git a/src/builtin/seek.rs b/src/builtin/seek.rs index 8e69258..208d3af 100644 --- a/src/builtin/seek.rs +++ b/src/builtin/seek.rs @@ -1,91 +1,101 @@ -use nix::{libc::STDOUT_FILENO, unistd::{Whence, lseek, write}}; +use nix::{ + libc::STDOUT_FILENO, + unistd::{Whence, lseek, write}, +}; -use crate::{getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node, execute::prepare_argv}, procio::borrow_fd, state}; +use crate::{ + getopt::{Opt, OptSpec, get_opts_from_tokens}, + libsh::error::{ShErr, ShErrKind, ShResult}, + parse::{NdRule, Node, execute::prepare_argv}, + procio::borrow_fd, + state, +}; -pub const LSEEK_OPTS: [OptSpec;2] = [ - OptSpec { - opt: Opt::Short('c'), - takes_arg: false - }, - OptSpec { - opt: Opt::Short('e'), - takes_arg: false - }, +pub const LSEEK_OPTS: [OptSpec; 2] = [ + OptSpec { + opt: Opt::Short('c'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('e'), + takes_arg: false, + }, ]; pub struct LseekOpts { - cursor_rel: bool, - end_rel: bool + cursor_rel: bool, + end_rel: bool, } pub fn seek(node: Node) -> ShResult<()> { - let NdRule::Command { - assignments: _, - argv, - } = node.class else { unreachable!() }; + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; - let (argv, opts) = get_opts_from_tokens(argv, &LSEEK_OPTS)?; - let lseek_opts = get_lseek_opts(opts)?; - let mut argv = prepare_argv(argv)?.into_iter(); - argv.next(); // drop 'seek' + let (argv, opts) = get_opts_from_tokens(argv, &LSEEK_OPTS)?; + let lseek_opts = get_lseek_opts(opts)?; + let mut argv = prepare_argv(argv)?.into_iter(); + argv.next(); // drop 'seek' - let Some(fd) = argv.next() else { - return Err(ShErr::simple( - ShErrKind::ExecFail, - "lseek: Missing required argument 'fd'", - )); - }; - let Ok(fd) = fd.0.parse::() else { - return Err(ShErr::at( - ShErrKind::ExecFail, - fd.1, - "Invalid file descriptor", - ).with_note("file descriptors are integers")); - }; + let Some(fd) = argv.next() else { + return Err(ShErr::simple( + ShErrKind::ExecFail, + "lseek: Missing required argument 'fd'", + )); + }; + let Ok(fd) = fd.0.parse::() else { + return Err( + ShErr::at(ShErrKind::ExecFail, fd.1, "Invalid file descriptor") + .with_note("file descriptors are integers"), + ); + }; - let Some(offset) = argv.next() else { - return Err(ShErr::simple( - ShErrKind::ExecFail, - "lseek: Missing required argument 'offset'", - )); - }; - let Ok(offset) = offset.0.parse::() else { - return Err(ShErr::at( - ShErrKind::ExecFail, - offset.1, - "Invalid offset", - ).with_note("offset can be a positive or negative integer")); - }; + let Some(offset) = argv.next() else { + return Err(ShErr::simple( + ShErrKind::ExecFail, + "lseek: Missing required argument 'offset'", + )); + }; + let Ok(offset) = offset.0.parse::() else { + return Err( + ShErr::at(ShErrKind::ExecFail, offset.1, "Invalid offset") + .with_note("offset can be a positive or negative integer"), + ); + }; - let whence = if lseek_opts.cursor_rel { - Whence::SeekCur - } else if lseek_opts.end_rel { - Whence::SeekEnd - } else { - Whence::SeekSet - }; + let whence = if lseek_opts.cursor_rel { + Whence::SeekCur + } else if lseek_opts.end_rel { + Whence::SeekEnd + } else { + Whence::SeekSet + }; - match lseek(fd as i32, offset, whence) { - Ok(new_offset) => { - let stdout = borrow_fd(STDOUT_FILENO); - let buf = new_offset.to_string() + "\n"; - write(stdout, buf.as_bytes())?; - } - Err(e) => { - state::set_status(1); - return Err(e.into()) - } - } + match lseek(fd as i32, offset, whence) { + Ok(new_offset) => { + let stdout = borrow_fd(STDOUT_FILENO); + let buf = new_offset.to_string() + "\n"; + write(stdout, buf.as_bytes())?; + } + Err(e) => { + state::set_status(1); + return Err(e.into()); + } + } - state::set_status(0); - Ok(()) + state::set_status(0); + Ok(()) } pub fn get_lseek_opts(opts: Vec) -> ShResult { - let mut lseek_opts = LseekOpts { - cursor_rel: false, - end_rel: false, - }; + let mut lseek_opts = LseekOpts { + cursor_rel: false, + end_rel: false, + }; for opt in opts { match opt { diff --git a/src/expand.rs b/src/expand.rs index 8f9f14a..731012b 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -40,7 +40,7 @@ impl Tk { } pub struct Expander { - flags: TkFlags, + flags: TkFlags, raw: String, } @@ -51,12 +51,15 @@ impl Expander { } pub fn from_raw(raw: &str, flags: TkFlags) -> ShResult { let raw = expand_braces_full(raw)?.join(" "); - let unescaped = if flags.contains(TkFlags::IS_HEREDOC) { - unescape_heredoc(&raw) - } else { - unescape_str(&raw) - }; - Ok(Self { raw: unescaped, flags }) + let unescaped = if flags.contains(TkFlags::IS_HEREDOC) { + unescape_heredoc(&raw) + } else { + unescape_str(&raw) + }; + Ok(Self { + raw: unescaped, + flags, + }) } pub fn expand(&mut self) -> ShResult> { let mut chars = self.raw.chars().peekable(); @@ -80,11 +83,11 @@ impl Expander { self.raw.insert_str(0, "./"); } - if self.flags.contains(TkFlags::IS_HEREDOC) { - Ok(vec![self.raw.clone()]) - } else { - Ok(self.split_words()) - } + if self.flags.contains(TkFlags::IS_HEREDOC) { + Ok(vec![self.raw.clone()]) + } else { + Ok(self.split_words()) + } } pub fn split_words(&mut self) -> Vec { let mut words = vec![]; @@ -1378,89 +1381,89 @@ pub fn unescape_str(raw: &str) -> String { /// - Backslash escapes (only before $, `, \, and newline) /// Everything else (quotes, tildes, globs, process subs, etc.) is literal. pub fn unescape_heredoc(raw: &str) -> String { - let mut chars = raw.chars().peekable(); - let mut result = String::new(); + let mut chars = raw.chars().peekable(); + let mut result = String::new(); - while let Some(ch) = chars.next() { - match ch { - '\\' => { - match chars.peek() { - Some('$') | Some('`') | Some('\\') | Some('\n') => { - let next_ch = chars.next().unwrap(); - if next_ch == '\n' { - // line continuation — discard both backslash and newline - continue; - } - result.push(markers::ESCAPE); - result.push(next_ch); - } - _ => { - // backslash is literal - result.push('\\'); - } - } - } - '$' if chars.peek() == Some(&'(') => { - result.push(markers::VAR_SUB); - chars.next(); // consume '(' - result.push(markers::SUBSH); - let mut paren_count = 1; - while let Some(subsh_ch) = chars.next() { - match subsh_ch { - '\\' => { - result.push(subsh_ch); - if let Some(next_ch) = chars.next() { - result.push(next_ch); - } - } - '(' => { - paren_count += 1; - result.push(subsh_ch); - } - ')' => { - paren_count -= 1; - if paren_count == 0 { - result.push(markers::SUBSH); - break; - } else { - result.push(subsh_ch); - } - } - _ => result.push(subsh_ch), - } - } - } - '$' => { - result.push(markers::VAR_SUB); - if chars.peek() == Some(&'$') { - chars.next(); - result.push('$'); - } - } - '`' => { - result.push(markers::VAR_SUB); - result.push(markers::SUBSH); - while let Some(bt_ch) = chars.next() { - match bt_ch { - '\\' => { - result.push(bt_ch); - if let Some(next_ch) = chars.next() { - result.push(next_ch); - } - } - '`' => { - result.push(markers::SUBSH); - break; - } - _ => result.push(bt_ch), - } - } - } - _ => result.push(ch), - } - } + while let Some(ch) = chars.next() { + match ch { + '\\' => { + match chars.peek() { + Some('$') | Some('`') | Some('\\') | Some('\n') => { + let next_ch = chars.next().unwrap(); + if next_ch == '\n' { + // line continuation — discard both backslash and newline + continue; + } + result.push(markers::ESCAPE); + result.push(next_ch); + } + _ => { + // backslash is literal + result.push('\\'); + } + } + } + '$' if chars.peek() == Some(&'(') => { + result.push(markers::VAR_SUB); + chars.next(); // consume '(' + result.push(markers::SUBSH); + let mut paren_count = 1; + while let Some(subsh_ch) = chars.next() { + match subsh_ch { + '\\' => { + result.push(subsh_ch); + if let Some(next_ch) = chars.next() { + result.push(next_ch); + } + } + '(' => { + paren_count += 1; + result.push(subsh_ch); + } + ')' => { + paren_count -= 1; + if paren_count == 0 { + result.push(markers::SUBSH); + break; + } else { + result.push(subsh_ch); + } + } + _ => result.push(subsh_ch), + } + } + } + '$' => { + result.push(markers::VAR_SUB); + if chars.peek() == Some(&'$') { + chars.next(); + result.push('$'); + } + } + '`' => { + result.push(markers::VAR_SUB); + result.push(markers::SUBSH); + while let Some(bt_ch) = chars.next() { + match bt_ch { + '\\' => { + result.push(bt_ch); + if let Some(next_ch) = chars.next() { + result.push(next_ch); + } + } + '`' => { + result.push(markers::SUBSH); + break; + } + _ => result.push(bt_ch), + } + } + } + _ => result.push(ch), + } + } - result + result } /// Opposite of unescape_str - escapes a string to be executed as literal text @@ -3669,7 +3672,7 @@ mod tests { let mut exp = Expander { raw: "hello world\tfoo".to_string(), - flags: TkFlags::empty() + flags: TkFlags::empty(), }; let words = exp.split_words(); assert_eq!(words, vec!["hello", "world", "foo"]); @@ -3684,7 +3687,7 @@ mod tests { let mut exp = Expander { raw: "a:b:c".to_string(), - flags: TkFlags::empty() + flags: TkFlags::empty(), }; let words = exp.split_words(); assert_eq!(words, vec!["a", "b", "c"]); @@ -3699,7 +3702,7 @@ mod tests { let mut exp = Expander { raw: "hello world".to_string(), - flags: TkFlags::empty() + flags: TkFlags::empty(), }; let words = exp.split_words(); assert_eq!(words, vec!["hello world"]); @@ -3711,9 +3714,9 @@ mod tests { let raw = format!("{}hello world{}", markers::DUB_QUOTE, markers::DUB_QUOTE); let mut exp = Expander { - raw, - flags: TkFlags::empty() - }; + raw, + flags: TkFlags::empty(), + }; let words = exp.split_words(); assert_eq!(words, vec!["hello world"]); } @@ -3726,9 +3729,9 @@ mod tests { let raw = format!("hello{}world", unescape_str("\\ ")); let mut exp = Expander { - raw, - flags: TkFlags::empty() - }; + raw, + flags: TkFlags::empty(), + }; let words = exp.split_words(); assert_eq!(words, vec!["hello world"]); } @@ -3739,9 +3742,9 @@ mod tests { let raw = format!("hello{}world", unescape_str("\\\t")); let mut exp = Expander { - raw, - flags: TkFlags::empty() - }; + raw, + flags: TkFlags::empty(), + }; let words = exp.split_words(); assert_eq!(words, vec!["hello\tworld"]); } @@ -3755,9 +3758,9 @@ mod tests { let raw = format!("a{}b:c", unescape_str("\\:")); let mut exp = Expander { - raw, - flags: TkFlags::empty() - }; + raw, + flags: TkFlags::empty(), + }; let words = exp.split_words(); assert_eq!(words, vec!["a:b", "c"]); } diff --git a/src/getopt.rs b/src/getopt.rs index 3f98b2b..dd97580 100644 --- a/src/getopt.rs +++ b/src/getopt.rs @@ -96,15 +96,15 @@ pub fn sort_tks( .map(|t| t.expand()) .collect::>>()? .into_iter() - .peekable(); + .peekable(); let mut opts = vec![]; let mut non_opts = vec![]; while let Some(token) = tokens_iter.next() { if &token.to_string() == "--" { - non_opts.push(token); - non_opts.extend(tokens_iter); - break; + non_opts.push(token); + non_opts.extend(tokens_iter); + break; } let parsed_opts = Opt::parse(&token.to_string()); diff --git a/src/libsh/error.rs b/src/libsh/error.rs index 910be14..59f3989 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -201,7 +201,7 @@ impl ShErr { pub fn is_flow_control(&self) -> bool { self.kind.is_flow_control() } - /// Promotes a shell error from a simple error to an error that blames a span + /// Promotes a shell error from a simple error to an error that blames a span pub fn promote(mut self, span: Span) -> Self { if self.notes.is_empty() { return self; @@ -210,8 +210,8 @@ impl ShErr { if self.notes.len() > 1 { self.notes = self.notes[1..].to_vec(); } else { - self.notes = vec![]; - } + self.notes = vec![]; + } self.labeled(span, first) } diff --git a/src/libsh/guards.rs b/src/libsh/guards.rs index c669b97..afb5d4c 100644 --- a/src/libsh/guards.rs +++ b/src/libsh/guards.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; use std::os::fd::{BorrowedFd, RawFd}; use nix::sys::termios::{self, LocalFlags, Termios, tcgetattr, tcsetattr}; -use nix::unistd::isatty; +use nix::unistd::{isatty, write}; use scopeguard::guard; thread_local! { @@ -150,6 +150,7 @@ impl RawModeGuard { tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &orig).ok(); let res = f(); tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, ¤t).ok(); + unsafe { write(BorrowedFd::borrow_raw(*TTY_FILENO), b"\x1b[?1l\x1b>").ok() }; res } } @@ -157,11 +158,12 @@ impl RawModeGuard { impl Drop for RawModeGuard { fn drop(&mut self) { unsafe { - let _ = termios::tcsetattr( + termios::tcsetattr( BorrowedFd::borrow_raw(self.fd), termios::SetArg::TCSANOW, &self.orig, - ); + ) + .ok(); } } } diff --git a/src/libsh/sys.rs b/src/libsh/sys.rs index 73a5b70..eb36553 100644 --- a/src/libsh/sys.rs +++ b/src/libsh/sys.rs @@ -9,7 +9,8 @@ pub static TTY_FILENO: LazyLock = LazyLock::new(|| { let fd = open("/dev/tty", OFlag::O_RDWR, Mode::empty()).expect("Failed to open /dev/tty"); // Move the tty fd above the user-accessible range so that // `exec 3>&-` and friends don't collide with shell internals. - let high = fcntl(fd, FcntlArg::F_DUPFD_CLOEXEC(MIN_INTERNAL_FD)).expect("Failed to dup /dev/tty high"); + let high = + fcntl(fd, FcntlArg::F_DUPFD_CLOEXEC(MIN_INTERNAL_FD)).expect("Failed to dup /dev/tty high"); close(fd).ok(); high }); diff --git a/src/parse/execute.rs b/src/parse/execute.rs index fc306ae..1a9d19f 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -8,7 +8,29 @@ use ariadne::Fmt; use crate::{ builtin::{ - alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, autocmd::autocmd, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, getopts::getopts, intro, jobctl::{self, JobBehavior, continue_job, disown, jobs}, keymap, seek::seek, map, pwd::pwd, read::{self, read_builtin}, resource::{ulimit, umask_builtin}, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset} + alias::{alias, unalias}, + arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, + autocmd::autocmd, + cd::cd, + complete::{compgen_builtin, complete_builtin}, + dirstack::{dirs, popd, pushd}, + echo::echo, + eval, exec, + flowctl::flowctl, + getopts::getopts, + intro, + jobctl::{self, JobBehavior, continue_job, disown, jobs}, + keymap, map, + pwd::pwd, + read::{self, read_builtin}, + resource::{ulimit, umask_builtin}, + seek::seek, + shift::shift, + shopt::shopt, + source::source, + test::double_bracket_test, + trap::{TrapTarget, trap}, + varcmds::{export, local, readonly, unset}, }, expand::{expand_aliases, expand_case_pattern, glob_to_regex}, jobs::{ChildProc, JobStack, attach_tty, dispatch_job}, @@ -319,12 +341,12 @@ impl Dispatcher { }; let mut elem_iter = elements.into_iter(); - let mut skip = false; + let mut skip = false; while let Some(element) = elem_iter.next() { let ConjunctNode { cmd, operator } = element; - if !skip { - self.dispatch_node(*cmd)?; - } + if !skip { + self.dispatch_node(*cmd)?; + } let status = state::get_status(); skip = match operator { @@ -351,7 +373,11 @@ impl Dispatcher { }; let body_span = body.get_span(); let body = body_span.as_str().to_string(); - let name = name.span.as_str().strip_suffix("()").unwrap_or(name.span.as_str()); + let name = name + .span + .as_str() + .strip_suffix("()") + .unwrap_or(name.span.as_str()); if KEYWORDS.contains(&name) { return Err(ShErr::at( @@ -863,9 +889,9 @@ impl Dispatcher { if fork_builtins { log::trace!("Forking builtin: {}", cmd_raw); let guard = self.io_stack.pop_frame().redirect()?; - if cmd_raw.as_str() == "exec" { - guard.persist(); - } + if cmd_raw.as_str() == "exec" { + guard.persist(); + } self.run_fork(&cmd_raw, |s| { if let Err(e) = s.dispatch_builtin(cmd) { e.print_error(); @@ -990,7 +1016,7 @@ impl Dispatcher { "autocmd" => autocmd(cmd), "ulimit" => ulimit(cmd), "umask" => umask_builtin(cmd), - "seek" => seek(cmd), + "seek" => seek(cmd), "true" | ":" => { state::set_status(0); Ok(()) diff --git a/src/parse/lex.rs b/src/parse/lex.rs index 5c9c642..a8694ad 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -218,31 +218,30 @@ impl Tk { self.span.as_str().trim() == ";;" } - pub fn is_opener(&self) -> bool { - OPENERS.contains(&self.as_str()) || - matches!(self.class, TkRule::BraceGrpStart) || - matches!(self.class, TkRule::CasePattern) - } - pub fn is_closer(&self) -> bool { - matches!(self.as_str(), "fi" | "done" | "esac") || - self.has_double_semi() || - matches!(self.class, TkRule::BraceGrpEnd) - } + pub fn is_opener(&self) -> bool { + OPENERS.contains(&self.as_str()) + || matches!(self.class, TkRule::BraceGrpStart) + || matches!(self.class, TkRule::CasePattern) + } + pub fn is_closer(&self) -> bool { + matches!(self.as_str(), "fi" | "done" | "esac") + || self.has_double_semi() + || matches!(self.class, TkRule::BraceGrpEnd) + } - pub fn is_closer_for(&self, other: &Tk) -> bool { - if (matches!(other.class, TkRule::BraceGrpStart) && matches!(self.class, TkRule::BraceGrpEnd)) - || (matches!(other.class, TkRule::CasePattern) && self.has_double_semi()) { - return true; - } - match other.as_str() { - "for" | - "while" | - "until" => matches!(self.as_str(), "done"), - "if" => matches!(self.as_str(), "fi"), - "case" => matches!(self.as_str(), "esac"), - _ => false - } - } + pub fn is_closer_for(&self, other: &Tk) -> bool { + if (matches!(other.class, TkRule::BraceGrpStart) && matches!(self.class, TkRule::BraceGrpEnd)) + || (matches!(other.class, TkRule::CasePattern) && self.has_double_semi()) + { + return true; + } + match other.as_str() { + "for" | "while" | "until" => matches!(self.as_str(), "done"), + "if" => matches!(self.as_str(), "fi"), + "case" => matches!(self.as_str(), "esac"), + _ => false, + } + } } impl Display for Tk { @@ -267,9 +266,9 @@ bitflags! { const ASSIGN = 0b0000000001000000; const BUILTIN = 0b0000000010000000; const IS_PROCSUB = 0b0000000100000000; - const IS_HEREDOC = 0b0000001000000000; - const LIT_HEREDOC = 0b0000010000000000; - const TAB_HEREDOC = 0b0000100000000000; + const IS_HEREDOC = 0b0000001000000000; + const LIT_HEREDOC = 0b0000010000000000; + const TAB_HEREDOC = 0b0000100000000000; } } @@ -322,11 +321,10 @@ pub struct LexStream { brc_grp_depth: usize, brc_grp_start: Option, case_depth: usize, - heredoc_skip: Option, + heredoc_skip: Option, flags: LexFlags, } - impl LexStream { pub fn new(source: Arc, flags: LexFlags) -> Self { let flags = flags | LexFlags::FRESH | LexFlags::NEXT_IS_CMD; @@ -338,7 +336,7 @@ impl LexStream { quote_state: QuoteState::default(), brc_grp_depth: 0, brc_grp_start: None, - heredoc_skip: None, + heredoc_skip: None, case_depth: 0, } } @@ -411,13 +409,13 @@ impl LexStream { return None; // It's a process sub } pos += 1; - if let Some('|') = chars.peek() { - // noclobber force '>|' - chars.next(); - pos += 1; - tk = self.get_token(self.cursor..pos, TkRule::Redir); - break - } + if let Some('|') = chars.peek() { + // noclobber force '>|' + chars.next(); + pos += 1; + tk = self.get_token(self.cursor..pos, TkRule::Redir); + break; + } if let Some('>') = chars.peek() { chars.next(); @@ -428,34 +426,34 @@ impl LexStream { break; }; - chars.next(); - pos += 1; + chars.next(); + pos += 1; - let mut found_fd = false; - if chars.peek().is_some_and(|ch| *ch == '-') { - chars.next(); - found_fd = true; - pos += 1; - } else { - while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) { - chars.next(); - found_fd = true; - pos += 1; - } - } + let mut found_fd = false; + if chars.peek().is_some_and(|ch| *ch == '-') { + chars.next(); + found_fd = true; + pos += 1; + } else { + while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) { + chars.next(); + found_fd = true; + pos += 1; + } + } - if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) { - let span_start = self.cursor; - self.cursor = pos; - return Some(Err(ShErr::at( - ShErrKind::ParseErr, - Span::new(span_start..pos, self.source.clone()), - "Invalid redirection", - ))); - } else { - tk = self.get_token(self.cursor..pos, TkRule::Redir); - break; - } + if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) { + let span_start = self.cursor; + self.cursor = pos; + return Some(Err(ShErr::at( + ShErrKind::ParseErr, + Span::new(span_start..pos, self.source.clone()), + "Invalid redirection", + ))); + } else { + tk = self.get_token(self.cursor..pos, TkRule::Redir); + break; + } } '<' => { if chars.peek() == Some(&'(') { @@ -463,93 +461,93 @@ impl LexStream { } pos += 1; - match chars.peek() { - Some('<') => { - chars.next(); - pos += 1; + match chars.peek() { + Some('<') => { + chars.next(); + pos += 1; - match chars.peek() { - Some('<') => { - chars.next(); - pos += 1; - } + match chars.peek() { + Some('<') => { + chars.next(); + pos += 1; + } - Some(ch) => { - let mut ch = *ch; - while is_field_sep(ch) { - let Some(next_ch) = chars.next() else { - // Incomplete input — fall through to emit << as Redir - break; - }; - pos += next_ch.len_utf8(); - ch = next_ch; - } + Some(ch) => { + let mut ch = *ch; + while is_field_sep(ch) { + let Some(next_ch) = chars.next() else { + // Incomplete input — fall through to emit << as Redir + break; + }; + pos += next_ch.len_utf8(); + ch = next_ch; + } - if is_field_sep(ch) { - // Ran out of input while skipping whitespace — fall through - } else { - let saved_cursor = self.cursor; - match self.read_heredoc(pos) { - Ok(Some(heredoc_tk)) => { - // cursor is set to after the delimiter word; - // heredoc_skip is set to after the body - pos = self.cursor; - self.cursor = saved_cursor; - tk = heredoc_tk; - break; - } - Ok(None) => { - // Incomplete heredoc — restore cursor and fall through - self.cursor = saved_cursor; - } - Err(e) => return Some(Err(e)), - } - } - } - _ => { - // No delimiter yet — input is incomplete - // Fall through to emit the << as a Redir token - } - } - } - Some('>') => { - chars.next(); - pos += 1; - tk = self.get_token(self.cursor..pos, TkRule::Redir); - break; - } - Some('&') => { - chars.next(); - pos += 1; + if is_field_sep(ch) { + // Ran out of input while skipping whitespace — fall through + } else { + let saved_cursor = self.cursor; + match self.read_heredoc(pos) { + Ok(Some(heredoc_tk)) => { + // cursor is set to after the delimiter word; + // heredoc_skip is set to after the body + pos = self.cursor; + self.cursor = saved_cursor; + tk = heredoc_tk; + break; + } + Ok(None) => { + // Incomplete heredoc — restore cursor and fall through + self.cursor = saved_cursor; + } + Err(e) => return Some(Err(e)), + } + } + } + _ => { + // No delimiter yet — input is incomplete + // Fall through to emit the << as a Redir token + } + } + } + Some('>') => { + chars.next(); + pos += 1; + tk = self.get_token(self.cursor..pos, TkRule::Redir); + break; + } + Some('&') => { + chars.next(); + pos += 1; - let mut found_fd = false; - if chars.peek().is_some_and(|ch| *ch == '-') { - chars.next(); - found_fd = true; - pos += 1; - } else { - while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) { - chars.next(); - found_fd = true; - pos += 1; - } - } + let mut found_fd = false; + if chars.peek().is_some_and(|ch| *ch == '-') { + chars.next(); + found_fd = true; + pos += 1; + } else { + while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) { + chars.next(); + found_fd = true; + pos += 1; + } + } - if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) { - let span_start = self.cursor; - self.cursor = pos; - return Some(Err(ShErr::at( - ShErrKind::ParseErr, - Span::new(span_start..pos, self.source.clone()), - "Invalid redirection", - ))); - } else { - tk = self.get_token(self.cursor..pos, TkRule::Redir); - break; - } - } - _ => {} - } + if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) { + let span_start = self.cursor; + self.cursor = pos; + return Some(Err(ShErr::at( + ShErrKind::ParseErr, + Span::new(span_start..pos, self.source.clone()), + "Invalid redirection", + ))); + } else { + tk = self.get_token(self.cursor..pos, TkRule::Redir); + break; + } + } + _ => {} + } tk = self.get_token(self.cursor..pos, TkRule::Redir); break; @@ -574,130 +572,133 @@ impl LexStream { self.cursor = pos; Some(Ok(tk)) } - pub fn read_heredoc(&mut self, mut pos: usize) -> ShResult> { - let slice = self.slice(pos..).unwrap_or_default().to_string(); - let mut chars = slice.chars(); - let mut delim = String::new(); - let mut flags = TkFlags::empty(); - let mut first_char = true; - // Parse the delimiter word, stripping quotes - while let Some(ch) = chars.next() { - match ch { - '-' if first_char => { - pos += 1; - flags |= TkFlags::TAB_HEREDOC; - } - '\"' => { - pos += 1; - self.quote_state.toggle_double(); - flags |= TkFlags::LIT_HEREDOC; - } - '\'' => { - pos += 1; - self.quote_state.toggle_single(); - flags |= TkFlags::LIT_HEREDOC; - } - _ if self.quote_state.in_quote() => { - pos += ch.len_utf8(); - delim.push(ch); - } - ch if is_hard_sep(ch) => { - break; - } - ch => { - pos += ch.len_utf8(); - delim.push(ch); - } - } - first_char = false; - } + pub fn read_heredoc(&mut self, mut pos: usize) -> ShResult> { + let slice = self.slice(pos..).unwrap_or_default().to_string(); + let mut chars = slice.chars(); + let mut delim = String::new(); + let mut flags = TkFlags::empty(); + let mut first_char = true; + // Parse the delimiter word, stripping quotes + while let Some(ch) = chars.next() { + match ch { + '-' if first_char => { + pos += 1; + flags |= TkFlags::TAB_HEREDOC; + } + '\"' => { + pos += 1; + self.quote_state.toggle_double(); + flags |= TkFlags::LIT_HEREDOC; + } + '\'' => { + pos += 1; + self.quote_state.toggle_single(); + flags |= TkFlags::LIT_HEREDOC; + } + _ if self.quote_state.in_quote() => { + pos += ch.len_utf8(); + delim.push(ch); + } + ch if is_hard_sep(ch) => { + break; + } + ch => { + pos += ch.len_utf8(); + delim.push(ch); + } + } + first_char = false; + } - // pos is now right after the delimiter word — this is where - // the cursor should return so the rest of the line gets lexed - let cursor_after_delim = pos; + // pos is now right after the delimiter word — this is where + // the cursor should return so the rest of the line gets lexed + let cursor_after_delim = pos; - // Re-slice from cursor_after_delim so iterator and pos are in sync - // (the old chars iterator consumed the hard_sep without advancing pos) - let rest = self.slice(cursor_after_delim..).unwrap_or_default().to_string(); - let mut chars = rest.chars(); + // Re-slice from cursor_after_delim so iterator and pos are in sync + // (the old chars iterator consumed the hard_sep without advancing pos) + let rest = self + .slice(cursor_after_delim..) + .unwrap_or_default() + .to_string(); + let mut chars = rest.chars(); - // Scan forward to the newline (or use heredoc_skip from a previous heredoc) - let body_start = if let Some(skip) = self.heredoc_skip { - // A previous heredoc on this line already read its body; - // our body starts where that one ended - let skip_offset = skip - cursor_after_delim; - for _ in 0..skip_offset { - chars.next(); - } - skip - } else { - // Skip the rest of the current line to find where the body begins - let mut scan = pos; - let mut found_newline = false; - while let Some(ch) = chars.next() { - scan += ch.len_utf8(); - if ch == '\n' { - found_newline = true; - break; - } - } - if !found_newline { - if self.flags.contains(LexFlags::LEX_UNFINISHED) { - return Ok(None); - } else { - return Err(ShErr::at( - ShErrKind::ParseErr, - Span::new(pos..pos, self.source.clone()), - "Heredoc delimiter not found", - )); - } - } - scan - }; + // Scan forward to the newline (or use heredoc_skip from a previous heredoc) + let body_start = if let Some(skip) = self.heredoc_skip { + // A previous heredoc on this line already read its body; + // our body starts where that one ended + let skip_offset = skip - cursor_after_delim; + for _ in 0..skip_offset { + chars.next(); + } + skip + } else { + // Skip the rest of the current line to find where the body begins + let mut scan = pos; + let mut found_newline = false; + while let Some(ch) = chars.next() { + scan += ch.len_utf8(); + if ch == '\n' { + found_newline = true; + break; + } + } + if !found_newline { + if self.flags.contains(LexFlags::LEX_UNFINISHED) { + return Ok(None); + } else { + return Err(ShErr::at( + ShErrKind::ParseErr, + Span::new(pos..pos, self.source.clone()), + "Heredoc delimiter not found", + )); + } + } + scan + }; - pos = body_start; - let start = pos; + pos = body_start; + let start = pos; - // Read lines until we find one that matches the delimiter exactly - let mut line = String::new(); - let mut line_start = pos; - while let Some(ch) = chars.next() { - pos += ch.len_utf8(); - if ch == '\n' { - let trimmed = line.trim_end_matches('\r'); - if trimmed == delim { - let mut tk = self.get_token(start..line_start, TkRule::Redir); - tk.flags |= TkFlags::IS_HEREDOC | flags; - self.heredoc_skip = Some(pos); - self.cursor = cursor_after_delim; - return Ok(Some(tk)); - } - line.clear(); - line_start = pos; - } else { - line.push(ch); - } - } - // Check the last line (no trailing newline) - let trimmed = line.trim_end_matches('\r'); - if trimmed == delim { - let mut tk = self.get_token(start..line_start, TkRule::Redir); - tk.flags |= TkFlags::IS_HEREDOC | flags; - self.heredoc_skip = Some(pos); - self.cursor = cursor_after_delim; - return Ok(Some(tk)); - } + // Read lines until we find one that matches the delimiter exactly + let mut line = String::new(); + let mut line_start = pos; + while let Some(ch) = chars.next() { + pos += ch.len_utf8(); + if ch == '\n' { + let trimmed = line.trim_end_matches('\r'); + if trimmed == delim { + let mut tk = self.get_token(start..line_start, TkRule::Redir); + tk.flags |= TkFlags::IS_HEREDOC | flags; + self.heredoc_skip = Some(pos); + self.cursor = cursor_after_delim; + return Ok(Some(tk)); + } + line.clear(); + line_start = pos; + } else { + line.push(ch); + } + } + // Check the last line (no trailing newline) + let trimmed = line.trim_end_matches('\r'); + if trimmed == delim { + let mut tk = self.get_token(start..line_start, TkRule::Redir); + tk.flags |= TkFlags::IS_HEREDOC | flags; + self.heredoc_skip = Some(pos); + self.cursor = cursor_after_delim; + return Ok(Some(tk)); + } - if !self.flags.contains(LexFlags::LEX_UNFINISHED) { - Err(ShErr::at( - ShErrKind::ParseErr, - Span::new(start..pos, self.source.clone()), - format!("Heredoc delimiter '{}' not found", delim), - )) - } else { - Ok(None) - } - } + if !self.flags.contains(LexFlags::LEX_UNFINISHED) { + Err(ShErr::at( + ShErrKind::ParseErr, + Span::new(start..pos, self.source.clone()), + format!("Heredoc delimiter '{}' not found", delim), + )) + } else { + Ok(None) + } + } pub fn read_string(&mut self) -> ShResult { assert!(self.cursor <= self.source.len()); let slice = self.slice_from_cursor().unwrap().to_string(); @@ -1113,9 +1114,10 @@ impl Iterator for LexStream { // If a heredoc was parsed on this line, skip past the body // Only on newline — ';' is a command separator within the same line if (ch == '\n' || ch == '\r') - && let Some(skip) = self.heredoc_skip.take() { - self.cursor = skip; - } + && let Some(skip) = self.heredoc_skip.take() + { + self.cursor = skip; + } while let Some(ch) = get_char(&self.source, self.cursor) { match ch { diff --git a/src/parse/mod.rs b/src/parse/mod.rs index d99f1ca..0d920fd 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -12,7 +12,8 @@ use crate::{ }, parse::lex::clean_input, prelude::*, - procio::IoMode, state::read_shopts, + procio::IoMode, + state::read_shopts, }; pub mod execute; @@ -280,17 +281,21 @@ bitflags! { pub struct Redir { pub io_mode: IoMode, pub class: RedirType, - pub span: Option + pub span: Option, } impl Redir { pub fn new(io_mode: IoMode, class: RedirType) -> Self { - Self { io_mode, class, span: None } + Self { + io_mode, + class, + span: None, + } + } + pub fn with_span(mut self, span: Span) -> Self { + self.span = Some(span); + self } - pub fn with_span(mut self, span: Span) -> Self { - self.span = Some(span); - self - } } #[derive(Default, Debug)] @@ -298,7 +303,7 @@ pub struct RedirBldr { pub io_mode: Option, pub class: Option, pub tgt_fd: Option, - pub span: Option, + pub span: Option, } impl RedirBldr { @@ -306,36 +311,36 @@ impl RedirBldr { Default::default() } pub fn with_io_mode(self, io_mode: IoMode) -> Self { - Self { - io_mode: Some(io_mode), - ..self - } + Self { + io_mode: Some(io_mode), + ..self + } } pub fn with_class(self, class: RedirType) -> Self { - Self { - class: Some(class), - ..self - } + Self { + class: Some(class), + ..self + } } pub fn with_tgt(self, tgt_fd: RawFd) -> Self { - Self { - tgt_fd: Some(tgt_fd), - ..self - } + Self { + tgt_fd: Some(tgt_fd), + ..self + } + } + pub fn with_span(self, span: Span) -> Self { + Self { + span: Some(span), + ..self + } } - pub fn with_span(self, span: Span) -> Self { - Self { - span: Some(span), - ..self - } - } pub fn build(self) -> Redir { let new = Redir::new(self.io_mode.unwrap(), self.class.unwrap()); - if let Some(span) = self.span { - new.with_span(span) - } else { - new - } + if let Some(span) = self.span { + new.with_span(span) + } else { + new + } } } @@ -355,23 +360,23 @@ impl FromStr for RedirBldr { chars.next(); redir = redir.with_class(RedirType::Append); } else if let Some('|') = chars.peek() { - chars.next(); - redir = redir.with_class(RedirType::OutputForce); - } + chars.next(); + redir = redir.with_class(RedirType::OutputForce); + } } '<' => { redir = redir.with_class(RedirType::Input); let mut count = 0; - if chars.peek() == Some(&'>') { - chars.next(); // consume the '>' - redir = redir.with_class(RedirType::ReadWrite); - } else { - while count < 2 && matches!(chars.peek(), Some('<')) { - chars.next(); - count += 1; - } - } + if chars.peek() == Some(&'>') { + chars.next(); // consume the '>' + redir = redir.with_class(RedirType::ReadWrite); + } else { + while count < 2 && matches!(chars.peek(), Some('<')) { + chars.next(); + count += 1; + } + } redir = match count { 1 => redir.with_class(RedirType::HereDoc), @@ -380,23 +385,23 @@ impl FromStr for RedirBldr { }; } '&' => { - if chars.peek() == Some(&'-') { - chars.next(); - src_fd.push('-'); - } else { - while let Some(next_ch) = chars.next() { - if next_ch.is_ascii_digit() { - src_fd.push(next_ch) - } else { - break; - } - } - } + if chars.peek() == Some(&'-') { + chars.next(); + src_fd.push('-'); + } else { + while let Some(next_ch) = chars.next() { + if next_ch.is_ascii_digit() { + src_fd.push(next_ch) + } else { + break; + } + } + } if src_fd.is_empty() { - return Err(ShErr::simple( - ShErrKind::ParseErr, - format!("Invalid character '{}' in redirection operator", ch), - )); + return Err(ShErr::simple( + ShErrKind::ParseErr, + format!("Invalid character '{}' in redirection operator", ch), + )); } } _ if ch.is_ascii_digit() && tgt_fd.is_empty() => { @@ -410,27 +415,26 @@ impl FromStr for RedirBldr { } } } - _ => return Err(ShErr::simple( - ShErrKind::ParseErr, - format!("Invalid character '{}' in redirection operator", ch), - )), + _ => { + return Err(ShErr::simple( + ShErrKind::ParseErr, + format!("Invalid character '{}' in redirection operator", ch), + )); + } } } let tgt_fd = tgt_fd .parse::() .unwrap_or_else(|_| match redir.class.unwrap() { - RedirType::Input | - RedirType::ReadWrite | - RedirType::HereDoc | - RedirType::HereString => 0, + RedirType::Input | RedirType::ReadWrite | RedirType::HereDoc | RedirType::HereString => 0, _ => 1, }); redir = redir.with_tgt(tgt_fd); - if src_fd.as_str() == "-" { - let io_mode = IoMode::Close { tgt_fd }; - redir = redir.with_io_mode(io_mode); - } else if let Ok(src_fd) = src_fd.parse::() { + if src_fd.as_str() == "-" { + let io_mode = IoMode::Close { tgt_fd }; + redir = redir.with_io_mode(io_mode); + } else if let Ok(src_fd) = src_fd.parse::() { let io_mode = IoMode::fd(tgt_fd, src_fd); redir = redir.with_io_mode(io_mode); } @@ -439,40 +443,40 @@ impl FromStr for RedirBldr { } impl TryFrom for RedirBldr { - type Error = ShErr; - fn try_from(tk: Tk) -> Result { - let span = tk.span.clone(); - if tk.flags.contains(TkFlags::IS_HEREDOC) { - let flags = tk.flags; + type Error = ShErr; + fn try_from(tk: Tk) -> Result { + let span = tk.span.clone(); + if tk.flags.contains(TkFlags::IS_HEREDOC) { + let flags = tk.flags; - Ok(RedirBldr { - io_mode: Some(IoMode::buffer(0, tk.to_string(), flags)?), - class: Some(RedirType::HereDoc), - tgt_fd: Some(0), - span: Some(span) - }) - } else { - match Self::from_str(tk.as_str()) { - Ok(bldr) => Ok(bldr.with_span(span)), - Err(e) => Err(e.promote(span)), - } - } - } + Ok(RedirBldr { + io_mode: Some(IoMode::buffer(0, tk.to_string(), flags)?), + class: Some(RedirType::HereDoc), + tgt_fd: Some(0), + span: Some(span), + }) + } else { + match Self::from_str(tk.as_str()) { + Ok(bldr) => Ok(bldr.with_span(span)), + Err(e) => Err(e.promote(span)), + } + } + } } #[derive(PartialEq, Clone, Copy, Debug)] pub enum RedirType { - Null, // Default - Pipe, // | - PipeAnd, // |&, redirs stderr and stdout - Input, // < - Output, // > - OutputForce,// >| - Append, // >> - HereDoc, // << - IndentHereDoc, // <<-, strips leading tabs - HereString, // <<< - ReadWrite, // <>, fd is opened for reading and writing + Null, // Default + Pipe, // | + PipeAnd, // |&, redirs stderr and stdout + Input, // < + Output, // > + OutputForce, // >| + Append, // >> + HereDoc, // << + IndentHereDoc, // <<-, strips leading tabs + HereString, // <<< + ReadWrite, // <>, fd is opened for reading and writing } #[derive(Clone, Debug)] @@ -887,7 +891,9 @@ impl ParseStream { // Two forms: "name()" as one token, or "name" followed by "()" as separate tokens let spaced_form = !is_func_name(self.peek_tk()) - && self.peek_tk().is_some_and(|tk| tk.flags.contains(TkFlags::IS_CMD)) + && self + .peek_tk() + .is_some_and(|tk| tk.flags.contains(TkFlags::IS_CMD)) && is_func_parens(self.tokens.get(1)); if !is_func_name(self.peek_tk()) && !spaced_form { @@ -1032,7 +1038,7 @@ impl ParseStream { Ok(Some(node)) } fn parse_brc_grp(&mut self, from_func_def: bool) -> ShResult> { - log::debug!("Trying to parse a brace group"); + log::debug!("Trying to parse a brace group"); let mut node_tks: Vec = vec![]; let mut body: Vec = vec![]; let mut redirs: Vec = vec![]; @@ -1045,7 +1051,7 @@ impl ParseStream { self.catch_separator(&mut node_tks); loop { - log::debug!("Parsing a brace group body"); + log::debug!("Parsing a brace group body"); if *self.next_tk_class() == TkRule::BraceGrpEnd { node_tks.push(self.next_tk().unwrap()); break; @@ -1054,25 +1060,25 @@ impl ParseStream { node_tks.extend(node.tokens.clone()); body.push(node); } else if *self.next_tk_class() != TkRule::BraceGrpEnd { - let next = self.peek_tk().cloned(); - let err = match next { - Some(tk) => Err(parse_err_full( - &format!("Unexpected token '{}' in brace group body", tk.as_str()), - &tk.span, - self.context.clone(), - )), - None => Err(parse_err_full( - "Unexpected end of input while parsing brace group body", - &node_tks.get_span().unwrap(), - self.context.clone(), - )), - }; + let next = self.peek_tk().cloned(); + let err = match next { + Some(tk) => Err(parse_err_full( + &format!("Unexpected token '{}' in brace group body", tk.as_str()), + &tk.span, + self.context.clone(), + )), + None => Err(parse_err_full( + "Unexpected end of input while parsing brace group body", + &node_tks.get_span().unwrap(), + self.context.clone(), + )), + }; self.panic_mode(&mut node_tks); return err; } self.catch_separator(&mut node_tks); if !self.next_tk_is_some() { - log::debug!("Hit end of input while parsing a brace group body, entering panic mode"); + log::debug!("Hit end of input while parsing a brace group body, entering panic mode"); self.panic_mode(&mut node_tks); return Err(parse_err_full( "Expected a closing brace for this brace group", @@ -1082,13 +1088,15 @@ impl ParseStream { } } - log::debug!("Finished parsing brace group body, now looking for redirections if it's not a function definition"); + log::debug!( + "Finished parsing brace group body, now looking for redirections if it's not a function definition" + ); if !from_func_def { self.parse_redir(&mut redirs, &mut node_tks)?; } - log::debug!("Finished parsing brace group redirections, constructing node"); + log::debug!("Finished parsing brace group redirections, constructing node"); let node = Node { class: NdRule::BraceGrp { body }, @@ -1106,7 +1114,11 @@ impl ParseStream { context: LabelCtx, ) -> ShResult { let redir_bldr = RedirBldr::try_from(redir_tk.clone()).unwrap(); - let next_tk = if redir_bldr.io_mode.is_none() { next() } else { None }; + let next_tk = if redir_bldr.io_mode.is_none() { + next() + } else { + None + }; if redir_bldr.io_mode.is_some() { return Ok(redir_bldr.build()); } @@ -1126,11 +1138,7 @@ impl ParseStream { "Expected a string after this redirection", )); } - let mut string = next_tk - .unwrap() - .expand()? - .get_words() - .join(" "); + let mut string = next_tk.unwrap().expand()?.get_words().join(" "); string.push('\n'); let io_mode = IoMode::buffer(redir_bldr.tgt_fd.unwrap_or(0), string, redir_tk.flags)?; Ok(redir_bldr.with_io_mode(io_mode).build()) @@ -1155,7 +1163,7 @@ impl ParseStream { while self.check_redir() { let tk = self.next_tk().unwrap(); node_tks.push(tk.clone()); - let ctx = self.context.clone(); + let ctx = self.context.clone(); let redir = Self::build_redir(&tk, || self.next_tk(), node_tks, ctx)?; redirs.push(redir); } @@ -1663,7 +1671,7 @@ impl ParseStream { node_tks.push(prefix_tk.clone()); assignments.push(assign) } else if is_keyword { - return Ok(None) + return Ok(None); } else if prefix_tk.class == TkRule::Sep { // Separator ends the prefix section - add it so commit() consumes it node_tks.push(prefix_tk.clone()); @@ -1721,7 +1729,7 @@ impl ParseStream { } TkRule::Redir => { node_tks.push(tk.clone()); - let ctx = self.context.clone(); + let ctx = self.context.clone(); let redir = Self::build_redir(tk, || tk_iter.next().cloned(), &mut node_tks, ctx)?; redirs.push(redir); } @@ -1882,34 +1890,33 @@ pub fn get_redir_file>(class: RedirType, path: P) -> ShResult OpenOptions::new().read(true).open(Path::new(&path)), - RedirType::Output => { - if read_shopts(|o| o.core.noclobber) && path.is_file() { - return Err(ShErr::simple( - ShErrKind::ExecFail, - format!("shopt core.noclobber is set, refusing to overwrite existing file `{}`", path.display()), - )); - } - OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(path) - }, - RedirType::ReadWrite => { - OpenOptions::new() - .write(true) - .read(true) - .create(true) - .truncate(false) - .open(path) - } - RedirType::OutputForce => { - OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(path) - } + RedirType::Output => { + if read_shopts(|o| o.core.noclobber) && path.is_file() { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!( + "shopt core.noclobber is set, refusing to overwrite existing file `{}`", + path.display() + ), + )); + } + OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(path) + } + RedirType::ReadWrite => OpenOptions::new() + .write(true) + .read(true) + .create(true) + .truncate(false) + .open(path), + RedirType::OutputForce => OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(path), RedirType::Append => OpenOptions::new().create(true).append(true).open(path), _ => unimplemented!("Unimplemented redir type: {:?}", class), }; @@ -1936,9 +1943,7 @@ fn is_func_name(tk: Option<&Tk>) -> bool { } fn is_func_parens(tk: Option<&Tk>) -> bool { - tk.is_some_and(|tk| { - tk.flags.contains(TkFlags::KEYWORD) && tk.span.as_str() == "()" - }) + tk.is_some_and(|tk| tk.flags.contains(TkFlags::KEYWORD) && tk.span.as_str() == "()") } /// Perform an operation on the child nodes of a given node @@ -2814,8 +2819,8 @@ pub mod tests { // ===================== Heredoc Execution ===================== - use crate::testutil::{TestGuard, test_input}; use crate::state::{VarFlags, VarKind, write_vars}; + use crate::testutil::{TestGuard, test_input}; #[test] fn heredoc_basic_output() { diff --git a/src/procio.rs b/src/procio.rs index 9192f8c..0c5f2a6 100644 --- a/src/procio.rs +++ b/src/procio.rs @@ -12,7 +12,8 @@ use crate::{ utils::RedirVecUtils, }, parse::{Redir, RedirType, get_redir_file, lex::TkFlags}, - prelude::*, state, + prelude::*, + state, }; // Credit to fish-shell for many of the implementation ideas present in this @@ -48,9 +49,9 @@ pub enum IoMode { pipe: Arc, }, Buffer { - tgt_fd: RawFd, + tgt_fd: RawFd, buf: String, - flags: TkFlags, // so we can see if its a heredoc or not + flags: TkFlags, // so we can see if its a heredoc or not }, Close { tgt_fd: RawFd, @@ -91,7 +92,9 @@ impl IoMode { if let IoMode::File { tgt_fd, path, mode } = self { let path_raw = path.as_os_str().to_str().unwrap_or_default().to_string(); - let expanded_path = Expander::from_raw(&path_raw, TkFlags::empty())?.expand()?.join(" "); // should just be one string, will have to find some way to handle a return of multiple paths + let expanded_path = Expander::from_raw(&path_raw, TkFlags::empty())? + .expand()? + .join(" "); // should just be one string, will have to find some way to handle a return of multiple paths let expanded_pathbuf = PathBuf::from(expanded_path); @@ -100,8 +103,7 @@ impl IoMode { // collides with the target fd (e.g. `3>/tmp/foo` where open() returns 3, // causing dup2(3,3) to be a no-op and then OwnedFd drop closes it). let raw = file.as_raw_fd(); - let high = fcntl(raw, FcntlArg::F_DUPFD_CLOEXEC(MIN_INTERNAL_FD)) - .map_err(ShErr::from)?; + let high = fcntl(raw, FcntlArg::F_DUPFD_CLOEXEC(MIN_INTERNAL_FD)).map_err(ShErr::from)?; drop(file); // closes the original low fd self = IoMode::OpenedFile { tgt_fd, @@ -110,9 +112,9 @@ impl IoMode { } Ok(self) } - pub fn buffer(tgt_fd: RawFd, buf: String, flags: TkFlags) -> ShResult { - Ok(Self::Buffer { tgt_fd, buf, flags }) - } + pub fn buffer(tgt_fd: RawFd, buf: String, flags: TkFlags) -> ShResult { + Ok(Self::Buffer { tgt_fd, buf, flags }) + } pub fn get_pipes() -> (Self, Self) { let (rpipe, wpipe) = nix::unistd::pipe2(OFlag::O_CLOEXEC).unwrap(); ( @@ -244,74 +246,78 @@ impl<'e> IoFrame { fn apply_redirs(&mut self) -> ShResult<()> { for redir in &mut self.redirs { let io_mode = &mut redir.io_mode; - match io_mode { - IoMode::Close { tgt_fd } => { - if *tgt_fd == *TTY_FILENO { - // Don't let user close the shell's tty fd. - continue; - } - close(*tgt_fd).ok(); - continue; - } - IoMode::File { .. } => { - match io_mode.clone().open_file() { - Ok(file) => *io_mode = file, - Err(e) => { - if let Some(span) = redir.span.as_ref() { - return Err(e.promote(span.clone())); - } - return Err(e) - } - } - } - IoMode::Buffer { tgt_fd, buf, flags } => { - let (rpipe, wpipe) = nix::unistd::pipe()?; - let mut text = if flags.contains(TkFlags::LIT_HEREDOC) { - buf.clone() - } else { - let words = Expander::from_raw(buf, *flags)?.expand()?; - if flags.contains(TkFlags::IS_HEREDOC) { - words.into_iter().next().unwrap_or_default() - } else { - let ifs = state::get_separator(); - words.join(&ifs).trim().to_string() + "\n" - } - }; - if flags.contains(TkFlags::TAB_HEREDOC) { - let lines = text.lines(); - let mut min_tabs = usize::MAX; - for line in lines { - if line.is_empty() { continue; } - let line_len = line.len(); - let after_strip = line.trim_start_matches('\t').len(); - let delta = line_len - after_strip; - min_tabs = min_tabs.min(delta); - } - if min_tabs == usize::MAX { - // let's avoid possibly allocating a string with 18 quintillion tabs - min_tabs = 0; - } + match io_mode { + IoMode::Close { tgt_fd } => { + if *tgt_fd == *TTY_FILENO { + // Don't let user close the shell's tty fd. + continue; + } + close(*tgt_fd).ok(); + continue; + } + IoMode::File { .. } => match io_mode.clone().open_file() { + Ok(file) => *io_mode = file, + Err(e) => { + if let Some(span) = redir.span.as_ref() { + return Err(e.promote(span.clone())); + } + return Err(e); + } + }, + IoMode::Buffer { tgt_fd, buf, flags } => { + let (rpipe, wpipe) = nix::unistd::pipe()?; + let mut text = if flags.contains(TkFlags::LIT_HEREDOC) { + buf.clone() + } else { + let words = Expander::from_raw(buf, *flags)?.expand()?; + if flags.contains(TkFlags::IS_HEREDOC) { + words.into_iter().next().unwrap_or_default() + } else { + let ifs = state::get_separator(); + words.join(&ifs).trim().to_string() + "\n" + } + }; + if flags.contains(TkFlags::TAB_HEREDOC) { + let lines = text.lines(); + let mut min_tabs = usize::MAX; + for line in lines { + if line.is_empty() { + continue; + } + let line_len = line.len(); + let after_strip = line.trim_start_matches('\t').len(); + let delta = line_len - after_strip; + min_tabs = min_tabs.min(delta); + } + if min_tabs == usize::MAX { + // let's avoid possibly allocating a string with 18 quintillion tabs + min_tabs = 0; + } - if min_tabs > 0 { - let stripped = text.lines() - .fold(vec![], |mut acc, ln| { - if ln.is_empty() { - acc.push(""); - return acc; - } - let stripped_ln = ln.strip_prefix(&"\t".repeat(min_tabs)).unwrap(); - acc.push(stripped_ln); - acc - }) - .join("\n"); - text = stripped + "\n"; - } - } - write(wpipe, text.as_bytes())?; - *io_mode = IoMode::Pipe { tgt_fd: *tgt_fd, pipe: rpipe.into() }; - } - _ => {} - } + if min_tabs > 0 { + let stripped = text + .lines() + .fold(vec![], |mut acc, ln| { + if ln.is_empty() { + acc.push(""); + return acc; + } + let stripped_ln = ln.strip_prefix(&"\t".repeat(min_tabs)).unwrap(); + acc.push(stripped_ln); + acc + }) + .join("\n"); + text = stripped + "\n"; + } + } + write(wpipe, text.as_bytes())?; + *io_mode = IoMode::Pipe { + tgt_fd: *tgt_fd, + pipe: rpipe.into(), + }; + } + _ => {} + } let tgt_fd = io_mode.tgt_fd(); let src_fd = io_mode.src_fd(); if let Err(e) = dup2(src_fd, tgt_fd) { diff --git a/src/readline/linebuf.rs b/src/readline/linebuf.rs index ca3b9af..be10fee 100644 --- a/src/readline/linebuf.rs +++ b/src/readline/linebuf.rs @@ -352,69 +352,73 @@ impl ClampedUsize { #[derive(Default, Clone, Debug)] pub struct IndentCtx { - depth: usize, - ctx: Vec, - in_escaped_line: bool + depth: usize, + ctx: Vec, + in_escaped_line: bool, } impl IndentCtx { - pub fn new() -> Self { Self::default() } + pub fn new() -> Self { + Self::default() + } - pub fn depth(&self) -> usize { - self.depth - } + pub fn depth(&self) -> usize { + self.depth + } - pub fn ctx(&self) -> &[Tk] { - &self.ctx - } + pub fn ctx(&self) -> &[Tk] { + &self.ctx + } - pub fn descend(&mut self, tk: Tk) { - self.ctx.push(tk); - self.depth += 1; - } + pub fn descend(&mut self, tk: Tk) { + self.ctx.push(tk); + self.depth += 1; + } - pub fn ascend(&mut self) { - self.depth = self.depth.saturating_sub(1); - self.ctx.pop(); - } + pub fn ascend(&mut self) { + self.depth = self.depth.saturating_sub(1); + self.ctx.pop(); + } - pub fn reset(&mut self) { - std::mem::take(self); - } + pub fn reset(&mut self) { + std::mem::take(self); + } - pub fn check_tk(&mut self, tk: Tk) { - if tk.is_opener() { - self.descend(tk); - } else if self.ctx.last().is_some_and(|t| tk.is_closer_for(t)) { - self.ascend(); - } else if matches!(tk.class, TkRule::Sep) && self.in_escaped_line { - self.in_escaped_line = false; - self.depth = self.depth.saturating_sub(1); - } - } + pub fn check_tk(&mut self, tk: Tk) { + if tk.is_opener() { + self.descend(tk); + } else if self.ctx.last().is_some_and(|t| tk.is_closer_for(t)) { + self.ascend(); + } else if matches!(tk.class, TkRule::Sep) && self.in_escaped_line { + self.in_escaped_line = false; + self.depth = self.depth.saturating_sub(1); + } + } - pub fn calculate(&mut self, input: &str) -> usize { - self.depth = 0; - self.ctx.clear(); - self.in_escaped_line = false; + pub fn calculate(&mut self, input: &str) -> usize { + self.depth = 0; + self.ctx.clear(); + self.in_escaped_line = false; - let input_arc = Arc::new(input.to_string()); - let Ok(tokens) = LexStream::new(input_arc, LexFlags::LEX_UNFINISHED).collect::>>() else { - log::error!("Lexing failed during depth calculation: {:?}", input); - return 0; - }; + let input_arc = Arc::new(input.to_string()); + let Ok(tokens) = + LexStream::new(input_arc, LexFlags::LEX_UNFINISHED).collect::>>() + else { + log::error!("Lexing failed during depth calculation: {:?}", input); + return 0; + }; - for tk in tokens { - self.check_tk(tk); - } + for tk in tokens { + self.check_tk(tk); + } if input.ends_with("\\\n") { - self.in_escaped_line = true; + self.in_escaped_line = true; self.depth += 1; } - self.depth - } + self.depth + } } #[derive(Default, Clone, Debug)] @@ -684,17 +688,17 @@ impl LineBuf { pub fn read_slice_to_cursor(&self) -> Option<&str> { self.read_slice_to(self.cursor.get()) } - pub fn cursor_is_escaped(&mut self) -> bool { - let Some(to_cursor) = self.slice_to_cursor() else { - return false; - }; + pub fn cursor_is_escaped(&mut self) -> bool { + let Some(to_cursor) = self.slice_to_cursor() else { + return false; + }; - // count the number of backslashes - let delta = to_cursor.len() - to_cursor.trim_end_matches('\\').len(); + // count the number of backslashes + let delta = to_cursor.len() - to_cursor.trim_end_matches('\\').len(); - // an even number of backslashes means each one is escaped - delta % 2 != 0 - } + // an even number of backslashes means each one is escaped + delta % 2 != 0 + } pub fn slice_to_cursor_inclusive(&mut self) -> Option<&str> { self.slice_to(self.cursor.ret_add(1)) } @@ -2928,29 +2932,29 @@ impl LineBuf { }; end = end.saturating_sub(1); let mut last_was_whitespace = false; - let mut last_was_escape = false; - let mut i = start; + let mut last_was_escape = false; + let mut i = start; while i < end { let Some(gr) = self.grapheme_at(i) else { - i += 1; + i += 1; continue; }; if gr == "\n" { if last_was_whitespace { self.remove(i); - end -= 1; + end -= 1; } else { self.force_replace_at(i, " "); } - if last_was_escape { - // if we are here, then we just joined an escaped newline - // semantically, echo foo\\nbar == echo foo bar - // so a joined line should remove the escape. - self.remove(i - 1); - end -= 1; - } + if last_was_escape { + // if we are here, then we just joined an escaped newline + // semantically, echo foo\\nbar == echo foo bar + // so a joined line should remove the escape. + self.remove(i - 1); + end -= 1; + } last_was_whitespace = false; - last_was_escape = false; + last_was_escape = false; let strip_pos = if self.grapheme_at(i) == Some(" ") { i + 1 } else { @@ -2958,24 +2962,24 @@ impl LineBuf { }; while self.grapheme_at(strip_pos) == Some("\t") { self.remove(strip_pos); - end -= 1; + end -= 1; } self.cursor.set(i); - i += 1; + i += 1; continue; } else if gr == "\\" { - if last_was_whitespace && last_was_escape { - // if we are here, then the pattern of the last three chars was this: - // ' \\', a space and two backslashes. - // This means the "last" was an escaped backslash, not whitespace. - last_was_whitespace = false; - } - last_was_escape = !last_was_escape; - } else { - last_was_whitespace = is_whitespace(gr); - last_was_escape = false; - } - i += 1; + if last_was_whitespace && last_was_escape { + // if we are here, then the pattern of the last three chars was this: + // ' \\', a space and two backslashes. + // This means the "last" was an escaped backslash, not whitespace. + last_was_whitespace = false; + } + last_was_escape = !last_was_escape; + } else { + last_was_whitespace = is_whitespace(gr); + last_was_escape = false; + } + i += 1; } Ok(()) } @@ -2984,24 +2988,23 @@ impl LineBuf { self.cursor.add(1); let before_escaped = self.indent_ctx.in_escaped_line; let before = self.indent_ctx.depth(); - if read_shopts(|o| o.prompt.auto_indent) { - let after = self.calc_indent_level(); - // Only dedent if the depth decrease came from a closer, not from - // a line continuation bonus going away - if after < before - && !(before_escaped && !self.indent_ctx.in_escaped_line) { - let delta = before - after; - let line_start = self.start_of_line(); - for _ in 0..delta { - if self.grapheme_at(line_start).is_some_and(|gr| gr == "\t") { - self.remove(line_start); - if !self.cursor_at_max() { - self.cursor.sub(1); - } - } - } - } - } + if read_shopts(|o| o.prompt.auto_indent) { + let after = self.calc_indent_level(); + // Only dedent if the depth decrease came from a closer, not from + // a line continuation bonus going away + if after < before && !(before_escaped && !self.indent_ctx.in_escaped_line) { + let delta = before - after; + let line_start = self.start_of_line(); + for _ in 0..delta { + if self.grapheme_at(line_start).is_some_and(|gr| gr == "\t") { + self.remove(line_start); + if !self.cursor_at_max() { + self.cursor.sub(1); + } + } + } + } + } } fn verb_insert(&mut self, string: String) { self.insert_str_at_cursor(&string); @@ -3038,7 +3041,7 @@ impl LineBuf { } Ok(()) } - #[allow(clippy::unnecessary_to_owned)] + #[allow(clippy::unnecessary_to_owned)] fn verb_dedent(&mut self, motion: MotionKind) -> ShResult<()> { let Some((start, mut end)) = self.range_from_motion(&motion) else { return Ok(()); @@ -3270,27 +3273,27 @@ impl LineBuf { ) -> ShResult<()> { match verb { Verb::Delete | Verb::Yank | Verb::Change => self.verb_ydc(motion, register, verb)?, - Verb::Rot13 => self.verb_rot13(motion)?, - Verb::ReplaceChar(ch) => self.verb_replace_char(motion, ch)?, - Verb::ReplaceCharInplace(ch, count) => self.verb_replace_char_inplace(ch, count)?, - Verb::ToggleCaseInplace(count) => self.verb_toggle_case_inplace(count), - Verb::ToggleCaseRange => self.verb_case_transform(motion, CaseTransform::Toggle)?, - Verb::ToLower => self.verb_case_transform(motion, CaseTransform::Lower)?, - Verb::ToUpper => self.verb_case_transform(motion, CaseTransform::Upper)?, - Verb::Redo | Verb::Undo => self.verb_undo_redo(verb)?, - Verb::RepeatLast => todo!(), - Verb::Put(anchor) => self.verb_put(anchor, register)?, - Verb::SwapVisualAnchor => self.verb_swap_visual_anchor(), - Verb::JoinLines => self.verb_join_lines()?, - Verb::InsertChar(ch) => self.verb_insert_char(ch), - Verb::Insert(string) => self.verb_insert(string), - Verb::Indent => self.verb_indent(motion)?, - Verb::Dedent => self.verb_dedent(motion)?, - Verb::Equalize => todo!(), - Verb::InsertModeLineBreak(anchor) => self.verb_insert_mode_line_break(anchor)?, - Verb::AcceptLineOrNewline => self.verb_accept_line_or_newline()?, - Verb::IncrementNumber(n) => self.verb_adjust_number(n as i64)?, - Verb::DecrementNumber(n) => self.verb_adjust_number(-(n as i64))?, + Verb::Rot13 => self.verb_rot13(motion)?, + Verb::ReplaceChar(ch) => self.verb_replace_char(motion, ch)?, + Verb::ReplaceCharInplace(ch, count) => self.verb_replace_char_inplace(ch, count)?, + Verb::ToggleCaseInplace(count) => self.verb_toggle_case_inplace(count), + Verb::ToggleCaseRange => self.verb_case_transform(motion, CaseTransform::Toggle)?, + Verb::ToLower => self.verb_case_transform(motion, CaseTransform::Lower)?, + Verb::ToUpper => self.verb_case_transform(motion, CaseTransform::Upper)?, + Verb::Redo | Verb::Undo => self.verb_undo_redo(verb)?, + Verb::RepeatLast => todo!(), + Verb::Put(anchor) => self.verb_put(anchor, register)?, + Verb::SwapVisualAnchor => self.verb_swap_visual_anchor(), + Verb::JoinLines => self.verb_join_lines()?, + Verb::InsertChar(ch) => self.verb_insert_char(ch), + Verb::Insert(string) => self.verb_insert(string), + Verb::Indent => self.verb_indent(motion)?, + Verb::Dedent => self.verb_dedent(motion)?, + Verb::Equalize => todo!(), + Verb::InsertModeLineBreak(anchor) => self.verb_insert_mode_line_break(anchor)?, + Verb::AcceptLineOrNewline => self.verb_accept_line_or_newline()?, + Verb::IncrementNumber(n) => self.verb_adjust_number(n as i64)?, + Verb::DecrementNumber(n) => self.verb_adjust_number(-(n as i64))?, Verb::Complete | Verb::ExMode | Verb::EndOfFile @@ -3349,9 +3352,9 @@ impl LineBuf { /* * Let's evaluate the motion now * If we got some weird command like 'dvw' we will - * have to simulate a visual selection to get the range - * If motion is None, we will try to use self.select_range - * If self.select_range is None, we will use MotionKind::Null + * have to simulate a visual selection to get the range + * If motion is None, we will try to use self.select_range + * If self.select_range is None, we will use MotionKind::Null */ let motion_eval = if flags.intersects(CmdFlags::VISUAL | CmdFlags::VISUAL_LINE | CmdFlags::VISUAL_BLOCK) { diff --git a/src/readline/mod.rs b/src/readline/mod.rs index 8d31446..de6077e 100644 --- a/src/readline/mod.rs +++ b/src/readline/mod.rs @@ -441,11 +441,6 @@ impl ShedVi { // Process all available keys while let Some(key) = self.reader.read_key()? { - log::debug!( - "Read key: {key:?} in mode {:?}, self.reader.verbatim = {}", - self.mode.report_mode(), - self.reader.verbatim - ); // If completer or history search are active, delegate input to it if self.history.fuzzy_finder.is_active() { self.print_line(false)?; @@ -628,10 +623,6 @@ impl ShedVi { pub fn handle_key(&mut self, key: KeyEvent) -> ShResult> { if self.should_accept_hint(&key) { - log::debug!( - "Accepting hint on key {key:?} in mode {:?}", - self.mode.report_mode() - ); self.editor.accept_hint(); if !self.history.at_pending() { self.history.reset_to_pending(); @@ -1257,10 +1248,6 @@ impl ShedVi { for _ in 0..repeat { let cmds = cmds.clone(); for (i, cmd) in cmds.iter().enumerate() { - log::debug!( - "Replaying command {cmd:?} in mode {:?}, replay {i}/{repeat}", - self.mode.report_mode() - ); self.exec_cmd(cmd.clone(), true)?; // After the first command, start merging so all subsequent // edits fold into one undo entry (e.g. cw + inserted chars) @@ -1430,8 +1417,6 @@ pub fn annotate_input(input: &str) -> String { for tk in tokens.into_iter().rev() { let insertions = annotate_token(tk); for (pos, marker) in insertions { - log::info!("pos: {pos}, marker: {marker:?}"); - log::info!("before: {annotated:?}"); let pos = pos.max(0).min(annotated.len()); annotated.insert(pos, marker); } diff --git a/src/readline/term.rs b/src/readline/term.rs index d5841b6..d11ae19 100644 --- a/src/readline/term.rs +++ b/src/readline/term.rs @@ -294,12 +294,14 @@ impl Read for TermBuffer { struct KeyCollector { events: VecDeque, + ss3_pending: bool, } impl KeyCollector { fn new() -> Self { Self { events: VecDeque::new(), + ss3_pending: false, } } @@ -337,7 +339,55 @@ impl Default for KeyCollector { impl Perform for KeyCollector { fn print(&mut self, c: char) { + log::trace!("print: {c:?}"); // vte routes 0x7f (DEL) to print instead of execute + if self.ss3_pending { + self.ss3_pending = false; + match c { + 'A' => { + self.push(KeyEvent(KeyCode::Up, ModKeys::empty())); + return; + } + 'B' => { + self.push(KeyEvent(KeyCode::Down, ModKeys::empty())); + return; + } + 'C' => { + self.push(KeyEvent(KeyCode::Right, ModKeys::empty())); + return; + } + 'D' => { + self.push(KeyEvent(KeyCode::Left, ModKeys::empty())); + return; + } + 'H' => { + self.push(KeyEvent(KeyCode::Home, ModKeys::empty())); + return; + } + 'F' => { + self.push(KeyEvent(KeyCode::End, ModKeys::empty())); + return; + } + 'P' => { + self.push(KeyEvent(KeyCode::F(1), ModKeys::empty())); + return; + } + 'Q' => { + self.push(KeyEvent(KeyCode::F(2), ModKeys::empty())); + return; + } + 'R' => { + self.push(KeyEvent(KeyCode::F(3), ModKeys::empty())); + return; + } + 'S' => { + self.push(KeyEvent(KeyCode::F(4), ModKeys::empty())); + return; + } + _ => {} + } + } + if c == '\x7f' { self.push(KeyEvent(KeyCode::Backspace, ModKeys::empty())); } else { @@ -346,6 +396,7 @@ impl Perform for KeyCollector { } fn execute(&mut self, byte: u8) { + log::trace!("execute: {byte:#04x}"); let event = match byte { 0x00 => KeyEvent(KeyCode::Char(' '), ModKeys::CTRL), // Ctrl+Space / Ctrl+@ 0x09 => KeyEvent(KeyCode::Tab, ModKeys::empty()), // Tab (Ctrl+I) @@ -370,6 +421,9 @@ impl Perform for KeyCollector { _ignore: bool, action: char, ) { + log::trace!( + "CSI dispatch: params={params:?}, intermediates={intermediates:?}, action={action:?}" + ); let params: Vec = params .iter() .map(|p| p.first().copied().unwrap_or(0)) @@ -481,22 +535,11 @@ impl Perform for KeyCollector { } fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) { + log::trace!("ESC dispatch: intermediates={intermediates:?}, byte={byte:#04x}"); // SS3 sequences - if intermediates == [b'O'] { - let key = match byte { - b'P' => KeyCode::F(1), - b'Q' => KeyCode::F(2), - b'R' => KeyCode::F(3), - b'S' => KeyCode::F(4), - b'A' => KeyCode::Up, - b'B' => KeyCode::Down, - b'C' => KeyCode::Right, - b'D' => KeyCode::Left, - b'H' => KeyCode::Home, - b'F' => KeyCode::End, - _ => return, - }; - self.push(KeyEvent(key, ModKeys::empty())); + if byte == b'O' { + self.ss3_pending = true; + return; } } } diff --git a/src/readline/tests.rs b/src/readline/tests.rs index f59ba0c..88b01ff 100644 --- a/src/readline/tests.rs +++ b/src/readline/tests.rs @@ -35,194 +35,249 @@ macro_rules! vi_test { #[test] fn annotate_simple_command() { - assert_annotated("echo hello", - "\u{e101}echo\u{e11a} \u{e102}hello\u{e11a}"); + assert_annotated("echo hello", "\u{e101}echo\u{e11a} \u{e102}hello\u{e11a}"); } #[test] fn annotate_pipeline() { - assert_annotated("ls | grep foo", - "\u{e100}ls\u{e11a} \u{e104}|\u{e11a} \u{e100}grep\u{e11a} \u{e102}foo\u{e11a}"); + assert_annotated( + "ls | grep foo", + "\u{e100}ls\u{e11a} \u{e104}|\u{e11a} \u{e100}grep\u{e11a} \u{e102}foo\u{e11a}", + ); } #[test] fn annotate_conjunction() { - assert_annotated("echo foo && echo bar", - "\u{e101}echo\u{e11a} \u{e102}foo\u{e11a} \u{e104}&&\u{e11a} \u{e101}echo\u{e11a} \u{e102}bar\u{e11a}"); + assert_annotated( + "echo foo && echo bar", + "\u{e101}echo\u{e11a} \u{e102}foo\u{e11a} \u{e104}&&\u{e11a} \u{e101}echo\u{e11a} \u{e102}bar\u{e11a}", + ); } #[test] fn annotate_redirect_output() { - assert_annotated("echo hello > file.txt", - "\u{e101}echo\u{e11a} \u{e102}hello\u{e11a} \u{e105}>\u{e11a} \u{e102}file.txt\u{e11a}"); + assert_annotated( + "echo hello > file.txt", + "\u{e101}echo\u{e11a} \u{e102}hello\u{e11a} \u{e105}>\u{e11a} \u{e102}file.txt\u{e11a}", + ); } #[test] fn annotate_redirect_append() { - assert_annotated("echo hello >> file.txt", - "\u{e101}echo\u{e11a} \u{e102}hello\u{e11a} \u{e105}>>\u{e11a} \u{e102}file.txt\u{e11a}"); + assert_annotated( + "echo hello >> file.txt", + "\u{e101}echo\u{e11a} \u{e102}hello\u{e11a} \u{e105}>>\u{e11a} \u{e102}file.txt\u{e11a}", + ); } #[test] fn annotate_redirect_input() { - assert_annotated("cat < file.txt", - "\u{e100}cat\u{e11a} \u{e105}<\u{e11a} \u{e102}file.txt\u{e11a}"); + assert_annotated( + "cat < file.txt", + "\u{e100}cat\u{e11a} \u{e105}<\u{e11a} \u{e102}file.txt\u{e11a}", + ); } #[test] fn annotate_fd_redirect() { - assert_annotated("cmd 2>&1", - "\u{e100}cmd\u{e11a} \u{e105}2>&1\u{e11a}"); + assert_annotated("cmd 2>&1", "\u{e100}cmd\u{e11a} \u{e105}2>&1\u{e11a}"); } #[test] fn annotate_variable_sub() { - assert_annotated("echo $HOME", - "\u{e101}echo\u{e11a} \u{e102}\u{e10c}$HOME\u{e10d}\u{e11a}"); + assert_annotated( + "echo $HOME", + "\u{e101}echo\u{e11a} \u{e102}\u{e10c}$HOME\u{e10d}\u{e11a}", + ); } #[test] fn annotate_variable_brace_sub() { - assert_annotated("echo ${HOME}", - "\u{e101}echo\u{e11a} \u{e102}\u{e10c}${HOME}\u{e10d}\u{e11a}"); + assert_annotated( + "echo ${HOME}", + "\u{e101}echo\u{e11a} \u{e102}\u{e10c}${HOME}\u{e10d}\u{e11a}", + ); } #[test] fn annotate_command_sub() { - assert_annotated("echo $(ls)", - "\u{e101}echo\u{e11a} \u{e102}\u{e10e}$(ls)\u{e10f}\u{e11a}"); + assert_annotated( + "echo $(ls)", + "\u{e101}echo\u{e11a} \u{e102}\u{e10e}$(ls)\u{e10f}\u{e11a}", + ); } #[test] fn annotate_single_quoted_string() { - assert_annotated("echo 'hello world'", - "\u{e101}echo\u{e11a} \u{e102}\u{e114}'hello world'\u{e115}\u{e11a}"); + assert_annotated( + "echo 'hello world'", + "\u{e101}echo\u{e11a} \u{e102}\u{e114}'hello world'\u{e115}\u{e11a}", + ); } #[test] fn annotate_double_quoted_string() { - assert_annotated("echo \"hello world\"", - "\u{e101}echo\u{e11a} \u{e102}\u{e112}\"hello world\"\u{e113}\u{e11a}"); + assert_annotated( + "echo \"hello world\"", + "\u{e101}echo\u{e11a} \u{e102}\u{e112}\"hello world\"\u{e113}\u{e11a}", + ); } #[test] fn annotate_assignment() { - assert_annotated("FOO=bar", - "\u{e107}FOO=bar\u{e11a}"); + assert_annotated("FOO=bar", "\u{e107}FOO=bar\u{e11a}"); } #[test] fn annotate_assignment_with_command() { - assert_annotated("FOO=bar echo hello", - "\u{e107}FOO=bar\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}"); + assert_annotated( + "FOO=bar echo hello", + "\u{e107}FOO=bar\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}", + ); } #[test] fn annotate_if_statement() { - assert_annotated("if true; then echo yes; fi", - "\u{e103}if\u{e11a} \u{e101}true\u{e11a}\u{e108}; \u{e11a}\u{e103}then\u{e11a} \u{e101}echo\u{e11a} \u{e102}yes\u{e11a}\u{e108}; \u{e11a}\u{e103}fi\u{e11a}"); + assert_annotated( + "if true; then echo yes; fi", + "\u{e103}if\u{e11a} \u{e101}true\u{e11a}\u{e108}; \u{e11a}\u{e103}then\u{e11a} \u{e101}echo\u{e11a} \u{e102}yes\u{e11a}\u{e108}; \u{e11a}\u{e103}fi\u{e11a}", + ); } #[test] fn annotate_for_loop() { - assert_annotated("for i in a b c; do echo $i; done", - "\u{e103}for\u{e11a} \u{e102}i\u{e11a} \u{e103}in\u{e11a} \u{e102}a\u{e11a} \u{e102}b\u{e11a} \u{e102}c\u{e11a}\u{e108}; \u{e11a}\u{e103}do\u{e11a} \u{e101}echo\u{e11a} \u{e102}\u{e10c}$i\u{e10d}\u{e11a}\u{e108}; \u{e11a}\u{e103}done\u{e11a}"); + assert_annotated( + "for i in a b c; do echo $i; done", + "\u{e103}for\u{e11a} \u{e102}i\u{e11a} \u{e103}in\u{e11a} \u{e102}a\u{e11a} \u{e102}b\u{e11a} \u{e102}c\u{e11a}\u{e108}; \u{e11a}\u{e103}do\u{e11a} \u{e101}echo\u{e11a} \u{e102}\u{e10c}$i\u{e10d}\u{e11a}\u{e108}; \u{e11a}\u{e103}done\u{e11a}", + ); } #[test] fn annotate_while_loop() { - assert_annotated("while true; do echo hello; done", - "\u{e103}while\u{e11a} \u{e101}true\u{e11a}\u{e108}; \u{e11a}\u{e103}do\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}\u{e108}; \u{e11a}\u{e103}done\u{e11a}"); + assert_annotated( + "while true; do echo hello; done", + "\u{e103}while\u{e11a} \u{e101}true\u{e11a}\u{e108}; \u{e11a}\u{e103}do\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}\u{e108}; \u{e11a}\u{e103}done\u{e11a}", + ); } #[test] fn annotate_case_statement() { - assert_annotated("case foo in bar) echo bar;; esac", - "\u{e103}case\u{e11a} \u{e102}foo\u{e11a} \u{e103}in\u{e11a} \u{e104}bar\u{e109})\u{e11a} \u{e101}echo\u{e11a} \u{e102}bar\u{e11a}\u{e108};; \u{e11a}\u{e103}esac\u{e11a}"); + assert_annotated( + "case foo in bar) echo bar;; esac", + "\u{e103}case\u{e11a} \u{e102}foo\u{e11a} \u{e103}in\u{e11a} \u{e104}bar\u{e109})\u{e11a} \u{e101}echo\u{e11a} \u{e102}bar\u{e11a}\u{e108};; \u{e11a}\u{e103}esac\u{e11a}", + ); } #[test] fn annotate_brace_group() { - assert_annotated("{ echo hello; }", - "\u{e104}{\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}\u{e108}; \u{e11a}\u{e104}}\u{e11a}"); + assert_annotated( + "{ echo hello; }", + "\u{e104}{\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}\u{e108}; \u{e11a}\u{e104}}\u{e11a}", + ); } #[test] fn annotate_comment() { - assert_annotated("echo hello # this is a comment", - "\u{e101}echo\u{e11a} \u{e102}hello\u{e11a} \u{e106}# this is a comment\u{e11a}"); + assert_annotated( + "echo hello # this is a comment", + "\u{e101}echo\u{e11a} \u{e102}hello\u{e11a} \u{e106}# this is a comment\u{e11a}", + ); } #[test] fn annotate_semicolon_sep() { - assert_annotated("echo foo; echo bar", - "\u{e101}echo\u{e11a} \u{e102}foo\u{e11a}\u{e108}; \u{e11a}\u{e101}echo\u{e11a} \u{e102}bar\u{e11a}"); + assert_annotated( + "echo foo; echo bar", + "\u{e101}echo\u{e11a} \u{e102}foo\u{e11a}\u{e108}; \u{e11a}\u{e101}echo\u{e11a} \u{e102}bar\u{e11a}", + ); } #[test] fn annotate_escaped_char() { - assert_annotated("echo hello\\ world", - "\u{e101}echo\u{e11a} \u{e102}hello\\ world\u{e11a}"); + assert_annotated( + "echo hello\\ world", + "\u{e101}echo\u{e11a} \u{e102}hello\\ world\u{e11a}", + ); } #[test] fn annotate_glob() { - assert_annotated("ls *.txt", - "\u{e100}ls\u{e11a} \u{e102}\u{e117}*\u{e11a}.txt\u{e11a}"); + assert_annotated( + "ls *.txt", + "\u{e100}ls\u{e11a} \u{e102}\u{e117}*\u{e11a}.txt\u{e11a}", + ); } #[test] fn annotate_heredoc_operator() { - assert_annotated("cat < out.txt 2> err.txt", - "\u{e100}cmd\u{e11a} \u{e105}>\u{e11a} \u{e102}out.txt\u{e11a} \u{e105}2>\u{e11a} \u{e102}err.txt\u{e11a}"); + assert_annotated( + "cmd > out.txt 2> err.txt", + "\u{e100}cmd\u{e11a} \u{e105}>\u{e11a} \u{e102}out.txt\u{e11a} \u{e105}2>\u{e11a} \u{e102}err.txt\u{e11a}", + ); } // ===================== Vi Tests ===================== @@ -437,27 +492,27 @@ vi_test! { #[test] fn vi_auto_indent() { - let (mut vi, _g) = test_vi(""); + let (mut vi, _g) = test_vi(""); - // Type each line and press Enter separately so auto-indent triggers - let lines = [ - "func() {", - "case foo in", - "bar)", - "while true; do", - "echo foo \\\rbar \\\rbiz \\\rbazz\rbreak\rdone\r;;\resac\r}" - ]; + // Type each line and press Enter separately so auto-indent triggers + let lines = [ + "func() {", + "case foo in", + "bar)", + "while true; do", + "echo foo \\\rbar \\\rbiz \\\rbazz\rbreak\rdone\r;;\resac\r}", + ]; - for (i,line) in lines.iter().enumerate() { - vi.feed_bytes(line.as_bytes()); - if i != lines.len() - 1 { - vi.feed_bytes(b"\r"); - } - vi.process_input().unwrap(); - } + for (i, line) in lines.iter().enumerate() { + vi.feed_bytes(line.as_bytes()); + if i != lines.len() - 1 { + vi.feed_bytes(b"\r"); + } + vi.process_input().unwrap(); + } - assert_eq!( - vi.editor.as_str(), - "func() {\n\tcase foo in\n\t\tbar)\n\t\t\twhile true; do\n\t\t\t\techo foo \\\n\t\t\t\t\tbar \\\n\t\t\t\t\tbiz \\\n\t\t\t\t\tbazz\n\t\t\t\tbreak\n\t\t\tdone\n\t\t;;\n\tesac\n}" - ); + assert_eq!( + vi.editor.as_str(), + "func() {\n\tcase foo in\n\t\tbar)\n\t\t\twhile true; do\n\t\t\t\techo foo \\\n\t\t\t\t\tbar \\\n\t\t\t\t\tbiz \\\n\t\t\t\t\tbazz\n\t\t\t\tbreak\n\t\t\tdone\n\t\t;;\n\tesac\n}" + ); } diff --git a/src/shopt.rs b/src/shopt.rs index e174586..6bb6c62 100644 --- a/src/shopt.rs +++ b/src/shopt.rs @@ -315,7 +315,8 @@ impl ShOptCore { Ok(Some(output)) } "noclobber" => { - let mut output = String::from("Prevent > from overwriting existing files (use >| to override)\n"); + let mut output = + String::from("Prevent > from overwriting existing files (use >| to override)\n"); output.push_str(&format!("{}", self.noclobber)); Ok(Some(output)) } diff --git a/src/state.rs b/src/state.rs index 630881a..9d513a1 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1330,14 +1330,14 @@ impl VarTab { .get(&ShellParam::Status) .map(|s| s.to_string()) .unwrap_or("0".into()), - ShellParam::AllArgsStr => { - let ifs = get_separator(); - self - .params - .get(&ShellParam::AllArgs) - .map(|s| s.replace(markers::ARG_SEP, &ifs).to_string()) - .unwrap_or_default() - } + ShellParam::AllArgsStr => { + let ifs = get_separator(); + self + .params + .get(&ShellParam::AllArgs) + .map(|s| s.replace(markers::ARG_SEP, &ifs).to_string()) + .unwrap_or_default() + } _ => self .params @@ -1852,12 +1852,12 @@ pub fn change_dir>(dir: P) -> ShResult<()> { } pub fn get_separator() -> String { - env::var("IFS") - .unwrap_or(String::from(" ")) - .chars() - .next() - .unwrap() - .to_string() + env::var("IFS") + .unwrap_or(String::from(" ")) + .chars() + .next() + .unwrap() + .to_string() } pub fn get_status() -> i32 {