fixed ss3 escape code parsing, added a cursor mode reset that triggers on child exit

This commit is contained in:
2026-03-15 11:11:35 -04:00
parent 101d8434f8
commit 1f9d59b546
17 changed files with 1099 additions and 957 deletions

View File

@@ -18,20 +18,20 @@ pub mod map;
pub mod pwd; pub mod pwd;
pub mod read; pub mod read;
pub mod resource; pub mod resource;
pub mod seek;
pub mod shift; pub mod shift;
pub mod shopt; pub mod shopt;
pub mod source; pub mod source;
pub mod test; // [[ ]] thing pub mod test; // [[ ]] thing
pub mod trap; pub mod trap;
pub mod varcmds; pub mod varcmds;
pub mod seek;
pub const BUILTINS: [&str; 50] = [ pub const BUILTINS: [&str; 50] = [
"echo", "cd", "read", "export", "local", "pwd", "source", ".", "shift", "jobs", "fg", "bg", "echo", "cd", "read", "export", "local", "pwd", "source", ".", "shift", "jobs", "fg", "bg",
"disown", "alias", "unalias", "return", "break", "continue", "exit", "shopt", "builtin", "disown", "alias", "unalias", "return", "break", "continue", "exit", "shopt", "builtin",
"command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly", "command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly",
"unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type", "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<()> { pub fn true_builtin() -> ShResult<()> {

View File

@@ -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] = [ pub const LSEEK_OPTS: [OptSpec; 2] = [
OptSpec { OptSpec {
opt: Opt::Short('c'), opt: Opt::Short('c'),
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('e'), opt: Opt::Short('e'),
takes_arg: false takes_arg: false,
}, },
]; ];
pub struct LseekOpts { pub struct LseekOpts {
cursor_rel: bool, cursor_rel: bool,
end_rel: bool end_rel: bool,
} }
pub fn seek(node: Node) -> ShResult<()> { pub fn seek(node: Node) -> ShResult<()> {
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
} = node.class else { unreachable!() }; } = node.class
else {
unreachable!()
};
let (argv, opts) = get_opts_from_tokens(argv, &LSEEK_OPTS)?; let (argv, opts) = get_opts_from_tokens(argv, &LSEEK_OPTS)?;
let lseek_opts = get_lseek_opts(opts)?; let lseek_opts = get_lseek_opts(opts)?;
let mut argv = prepare_argv(argv)?.into_iter(); let mut argv = prepare_argv(argv)?.into_iter();
argv.next(); // drop 'seek' argv.next(); // drop 'seek'
let Some(fd) = argv.next() else { let Some(fd) = argv.next() else {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::ExecFail, ShErrKind::ExecFail,
"lseek: Missing required argument 'fd'", "lseek: Missing required argument 'fd'",
)); ));
}; };
let Ok(fd) = fd.0.parse::<u32>() else { let Ok(fd) = fd.0.parse::<u32>() else {
return Err(ShErr::at( return Err(
ShErrKind::ExecFail, ShErr::at(ShErrKind::ExecFail, fd.1, "Invalid file descriptor")
fd.1, .with_note("file descriptors are integers"),
"Invalid file descriptor", );
).with_note("file descriptors are integers")); };
};
let Some(offset) = argv.next() else { let Some(offset) = argv.next() else {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::ExecFail, ShErrKind::ExecFail,
"lseek: Missing required argument 'offset'", "lseek: Missing required argument 'offset'",
)); ));
}; };
let Ok(offset) = offset.0.parse::<i64>() else { let Ok(offset) = offset.0.parse::<i64>() else {
return Err(ShErr::at( return Err(
ShErrKind::ExecFail, ShErr::at(ShErrKind::ExecFail, offset.1, "Invalid offset")
offset.1, .with_note("offset can be a positive or negative integer"),
"Invalid offset", );
).with_note("offset can be a positive or negative integer")); };
};
let whence = if lseek_opts.cursor_rel { let whence = if lseek_opts.cursor_rel {
Whence::SeekCur Whence::SeekCur
} else if lseek_opts.end_rel { } else if lseek_opts.end_rel {
Whence::SeekEnd Whence::SeekEnd
} else { } else {
Whence::SeekSet Whence::SeekSet
}; };
match lseek(fd as i32, offset, whence) { match lseek(fd as i32, offset, whence) {
Ok(new_offset) => { Ok(new_offset) => {
let stdout = borrow_fd(STDOUT_FILENO); let stdout = borrow_fd(STDOUT_FILENO);
let buf = new_offset.to_string() + "\n"; let buf = new_offset.to_string() + "\n";
write(stdout, buf.as_bytes())?; write(stdout, buf.as_bytes())?;
} }
Err(e) => { Err(e) => {
state::set_status(1); state::set_status(1);
return Err(e.into()) return Err(e.into());
} }
} }
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }
pub fn get_lseek_opts(opts: Vec<Opt>) -> ShResult<LseekOpts> { pub fn get_lseek_opts(opts: Vec<Opt>) -> ShResult<LseekOpts> {
let mut lseek_opts = LseekOpts { let mut lseek_opts = LseekOpts {
cursor_rel: false, cursor_rel: false,
end_rel: false, end_rel: false,
}; };
for opt in opts { for opt in opts {
match opt { match opt {

View File

@@ -40,7 +40,7 @@ impl Tk {
} }
pub struct Expander { pub struct Expander {
flags: TkFlags, flags: TkFlags,
raw: String, raw: String,
} }
@@ -51,12 +51,15 @@ impl Expander {
} }
pub fn from_raw(raw: &str, flags: TkFlags) -> ShResult<Self> { pub fn from_raw(raw: &str, flags: TkFlags) -> ShResult<Self> {
let raw = expand_braces_full(raw)?.join(" "); let raw = expand_braces_full(raw)?.join(" ");
let unescaped = if flags.contains(TkFlags::IS_HEREDOC) { let unescaped = if flags.contains(TkFlags::IS_HEREDOC) {
unescape_heredoc(&raw) unescape_heredoc(&raw)
} else { } else {
unescape_str(&raw) unescape_str(&raw)
}; };
Ok(Self { raw: unescaped, flags }) Ok(Self {
raw: unescaped,
flags,
})
} }
pub fn expand(&mut self) -> ShResult<Vec<String>> { pub fn expand(&mut self) -> ShResult<Vec<String>> {
let mut chars = self.raw.chars().peekable(); let mut chars = self.raw.chars().peekable();
@@ -80,11 +83,11 @@ impl Expander {
self.raw.insert_str(0, "./"); self.raw.insert_str(0, "./");
} }
if self.flags.contains(TkFlags::IS_HEREDOC) { if self.flags.contains(TkFlags::IS_HEREDOC) {
Ok(vec![self.raw.clone()]) Ok(vec![self.raw.clone()])
} else { } else {
Ok(self.split_words()) Ok(self.split_words())
} }
} }
pub fn split_words(&mut self) -> Vec<String> { pub fn split_words(&mut self) -> Vec<String> {
let mut words = vec![]; let mut words = vec![];
@@ -1378,89 +1381,89 @@ pub fn unescape_str(raw: &str) -> String {
/// - Backslash escapes (only before $, `, \, and newline) /// - Backslash escapes (only before $, `, \, and newline)
/// Everything else (quotes, tildes, globs, process subs, etc.) is literal. /// Everything else (quotes, tildes, globs, process subs, etc.) is literal.
pub fn unescape_heredoc(raw: &str) -> String { pub fn unescape_heredoc(raw: &str) -> String {
let mut chars = raw.chars().peekable(); let mut chars = raw.chars().peekable();
let mut result = String::new(); let mut result = String::new();
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
'\\' => { '\\' => {
match chars.peek() { match chars.peek() {
Some('$') | Some('`') | Some('\\') | Some('\n') => { Some('$') | Some('`') | Some('\\') | Some('\n') => {
let next_ch = chars.next().unwrap(); let next_ch = chars.next().unwrap();
if next_ch == '\n' { if next_ch == '\n' {
// line continuation — discard both backslash and newline // line continuation — discard both backslash and newline
continue; continue;
} }
result.push(markers::ESCAPE); result.push(markers::ESCAPE);
result.push(next_ch); result.push(next_ch);
} }
_ => { _ => {
// backslash is literal // backslash is literal
result.push('\\'); result.push('\\');
} }
} }
} }
'$' if chars.peek() == Some(&'(') => { '$' if chars.peek() == Some(&'(') => {
result.push(markers::VAR_SUB); result.push(markers::VAR_SUB);
chars.next(); // consume '(' chars.next(); // consume '('
result.push(markers::SUBSH); result.push(markers::SUBSH);
let mut paren_count = 1; let mut paren_count = 1;
while let Some(subsh_ch) = chars.next() { while let Some(subsh_ch) = chars.next() {
match subsh_ch { match subsh_ch {
'\\' => { '\\' => {
result.push(subsh_ch); result.push(subsh_ch);
if let Some(next_ch) = chars.next() { if let Some(next_ch) = chars.next() {
result.push(next_ch); result.push(next_ch);
} }
} }
'(' => { '(' => {
paren_count += 1; paren_count += 1;
result.push(subsh_ch); result.push(subsh_ch);
} }
')' => { ')' => {
paren_count -= 1; paren_count -= 1;
if paren_count == 0 { if paren_count == 0 {
result.push(markers::SUBSH); result.push(markers::SUBSH);
break; break;
} else { } else {
result.push(subsh_ch); result.push(subsh_ch);
} }
} }
_ => result.push(subsh_ch), _ => result.push(subsh_ch),
} }
} }
} }
'$' => { '$' => {
result.push(markers::VAR_SUB); result.push(markers::VAR_SUB);
if chars.peek() == Some(&'$') { if chars.peek() == Some(&'$') {
chars.next(); chars.next();
result.push('$'); result.push('$');
} }
} }
'`' => { '`' => {
result.push(markers::VAR_SUB); result.push(markers::VAR_SUB);
result.push(markers::SUBSH); result.push(markers::SUBSH);
while let Some(bt_ch) = chars.next() { while let Some(bt_ch) = chars.next() {
match bt_ch { match bt_ch {
'\\' => { '\\' => {
result.push(bt_ch); result.push(bt_ch);
if let Some(next_ch) = chars.next() { if let Some(next_ch) = chars.next() {
result.push(next_ch); result.push(next_ch);
} }
} }
'`' => { '`' => {
result.push(markers::SUBSH); result.push(markers::SUBSH);
break; break;
} }
_ => result.push(bt_ch), _ => result.push(bt_ch),
} }
} }
} }
_ => result.push(ch), _ => result.push(ch),
} }
} }
result result
} }
/// Opposite of unescape_str - escapes a string to be executed as literal text /// Opposite of unescape_str - escapes a string to be executed as literal text
@@ -3669,7 +3672,7 @@ mod tests {
let mut exp = Expander { let mut exp = Expander {
raw: "hello world\tfoo".to_string(), raw: "hello world\tfoo".to_string(),
flags: TkFlags::empty() flags: TkFlags::empty(),
}; };
let words = exp.split_words(); let words = exp.split_words();
assert_eq!(words, vec!["hello", "world", "foo"]); assert_eq!(words, vec!["hello", "world", "foo"]);
@@ -3684,7 +3687,7 @@ mod tests {
let mut exp = Expander { let mut exp = Expander {
raw: "a:b:c".to_string(), raw: "a:b:c".to_string(),
flags: TkFlags::empty() flags: TkFlags::empty(),
}; };
let words = exp.split_words(); let words = exp.split_words();
assert_eq!(words, vec!["a", "b", "c"]); assert_eq!(words, vec!["a", "b", "c"]);
@@ -3699,7 +3702,7 @@ mod tests {
let mut exp = Expander { let mut exp = Expander {
raw: "hello world".to_string(), raw: "hello world".to_string(),
flags: TkFlags::empty() flags: TkFlags::empty(),
}; };
let words = exp.split_words(); let words = exp.split_words();
assert_eq!(words, vec!["hello world"]); assert_eq!(words, vec!["hello world"]);
@@ -3711,9 +3714,9 @@ mod tests {
let raw = format!("{}hello world{}", markers::DUB_QUOTE, markers::DUB_QUOTE); let raw = format!("{}hello world{}", markers::DUB_QUOTE, markers::DUB_QUOTE);
let mut exp = Expander { let mut exp = Expander {
raw, raw,
flags: TkFlags::empty() flags: TkFlags::empty(),
}; };
let words = exp.split_words(); let words = exp.split_words();
assert_eq!(words, vec!["hello world"]); assert_eq!(words, vec!["hello world"]);
} }
@@ -3726,9 +3729,9 @@ mod tests {
let raw = format!("hello{}world", unescape_str("\\ ")); let raw = format!("hello{}world", unescape_str("\\ "));
let mut exp = Expander { let mut exp = Expander {
raw, raw,
flags: TkFlags::empty() flags: TkFlags::empty(),
}; };
let words = exp.split_words(); let words = exp.split_words();
assert_eq!(words, vec!["hello world"]); assert_eq!(words, vec!["hello world"]);
} }
@@ -3739,9 +3742,9 @@ mod tests {
let raw = format!("hello{}world", unescape_str("\\\t")); let raw = format!("hello{}world", unescape_str("\\\t"));
let mut exp = Expander { let mut exp = Expander {
raw, raw,
flags: TkFlags::empty() flags: TkFlags::empty(),
}; };
let words = exp.split_words(); let words = exp.split_words();
assert_eq!(words, vec!["hello\tworld"]); assert_eq!(words, vec!["hello\tworld"]);
} }
@@ -3755,9 +3758,9 @@ mod tests {
let raw = format!("a{}b:c", unescape_str("\\:")); let raw = format!("a{}b:c", unescape_str("\\:"));
let mut exp = Expander { let mut exp = Expander {
raw, raw,
flags: TkFlags::empty() flags: TkFlags::empty(),
}; };
let words = exp.split_words(); let words = exp.split_words();
assert_eq!(words, vec!["a:b", "c"]); assert_eq!(words, vec!["a:b", "c"]);
} }

View File

@@ -96,15 +96,15 @@ pub fn sort_tks(
.map(|t| t.expand()) .map(|t| t.expand())
.collect::<ShResult<Vec<_>>>()? .collect::<ShResult<Vec<_>>>()?
.into_iter() .into_iter()
.peekable(); .peekable();
let mut opts = vec![]; let mut opts = vec![];
let mut non_opts = vec![]; let mut non_opts = vec![];
while let Some(token) = tokens_iter.next() { while let Some(token) = tokens_iter.next() {
if &token.to_string() == "--" { if &token.to_string() == "--" {
non_opts.push(token); non_opts.push(token);
non_opts.extend(tokens_iter); non_opts.extend(tokens_iter);
break; break;
} }
let parsed_opts = Opt::parse(&token.to_string()); let parsed_opts = Opt::parse(&token.to_string());

View File

@@ -201,7 +201,7 @@ impl ShErr {
pub fn is_flow_control(&self) -> bool { pub fn is_flow_control(&self) -> bool {
self.kind.is_flow_control() 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 { pub fn promote(mut self, span: Span) -> Self {
if self.notes.is_empty() { if self.notes.is_empty() {
return self; return self;
@@ -210,8 +210,8 @@ impl ShErr {
if self.notes.len() > 1 { if self.notes.len() > 1 {
self.notes = self.notes[1..].to_vec(); self.notes = self.notes[1..].to_vec();
} else { } else {
self.notes = vec![]; self.notes = vec![];
} }
self.labeled(span, first) self.labeled(span, first)
} }

View File

@@ -3,7 +3,7 @@ use std::collections::HashSet;
use std::os::fd::{BorrowedFd, RawFd}; use std::os::fd::{BorrowedFd, RawFd};
use nix::sys::termios::{self, LocalFlags, Termios, tcgetattr, tcsetattr}; use nix::sys::termios::{self, LocalFlags, Termios, tcgetattr, tcsetattr};
use nix::unistd::isatty; use nix::unistd::{isatty, write};
use scopeguard::guard; use scopeguard::guard;
thread_local! { thread_local! {
@@ -150,6 +150,7 @@ impl RawModeGuard {
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &orig).ok(); tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &orig).ok();
let res = f(); let res = f();
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &current).ok(); tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &current).ok();
unsafe { write(BorrowedFd::borrow_raw(*TTY_FILENO), b"\x1b[?1l\x1b>").ok() };
res res
} }
} }
@@ -157,11 +158,12 @@ impl RawModeGuard {
impl Drop for RawModeGuard { impl Drop for RawModeGuard {
fn drop(&mut self) { fn drop(&mut self) {
unsafe { unsafe {
let _ = termios::tcsetattr( termios::tcsetattr(
BorrowedFd::borrow_raw(self.fd), BorrowedFd::borrow_raw(self.fd),
termios::SetArg::TCSANOW, termios::SetArg::TCSANOW,
&self.orig, &self.orig,
); )
.ok();
} }
} }
} }

View File

@@ -9,7 +9,8 @@ pub static TTY_FILENO: LazyLock<RawFd> = LazyLock::new(|| {
let fd = open("/dev/tty", OFlag::O_RDWR, Mode::empty()).expect("Failed to open /dev/tty"); 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 // Move the tty fd above the user-accessible range so that
// `exec 3>&-` and friends don't collide with shell internals. // `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(); close(fd).ok();
high high
}); });

View File

@@ -8,7 +8,29 @@ use ariadne::Fmt;
use crate::{ use crate::{
builtin::{ 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}, expand::{expand_aliases, expand_case_pattern, glob_to_regex},
jobs::{ChildProc, JobStack, attach_tty, dispatch_job}, jobs::{ChildProc, JobStack, attach_tty, dispatch_job},
@@ -319,12 +341,12 @@ impl Dispatcher {
}; };
let mut elem_iter = elements.into_iter(); let mut elem_iter = elements.into_iter();
let mut skip = false; let mut skip = false;
while let Some(element) = elem_iter.next() { while let Some(element) = elem_iter.next() {
let ConjunctNode { cmd, operator } = element; let ConjunctNode { cmd, operator } = element;
if !skip { if !skip {
self.dispatch_node(*cmd)?; self.dispatch_node(*cmd)?;
} }
let status = state::get_status(); let status = state::get_status();
skip = match operator { skip = match operator {
@@ -351,7 +373,11 @@ impl Dispatcher {
}; };
let body_span = body.get_span(); let body_span = body.get_span();
let body = body_span.as_str().to_string(); 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) { if KEYWORDS.contains(&name) {
return Err(ShErr::at( return Err(ShErr::at(
@@ -863,9 +889,9 @@ impl Dispatcher {
if fork_builtins { if fork_builtins {
log::trace!("Forking builtin: {}", cmd_raw); log::trace!("Forking builtin: {}", cmd_raw);
let guard = self.io_stack.pop_frame().redirect()?; let guard = self.io_stack.pop_frame().redirect()?;
if cmd_raw.as_str() == "exec" { if cmd_raw.as_str() == "exec" {
guard.persist(); guard.persist();
} }
self.run_fork(&cmd_raw, |s| { self.run_fork(&cmd_raw, |s| {
if let Err(e) = s.dispatch_builtin(cmd) { if let Err(e) = s.dispatch_builtin(cmd) {
e.print_error(); e.print_error();
@@ -990,7 +1016,7 @@ impl Dispatcher {
"autocmd" => autocmd(cmd), "autocmd" => autocmd(cmd),
"ulimit" => ulimit(cmd), "ulimit" => ulimit(cmd),
"umask" => umask_builtin(cmd), "umask" => umask_builtin(cmd),
"seek" => seek(cmd), "seek" => seek(cmd),
"true" | ":" => { "true" | ":" => {
state::set_status(0); state::set_status(0);
Ok(()) Ok(())

View File

@@ -218,31 +218,30 @@ impl Tk {
self.span.as_str().trim() == ";;" self.span.as_str().trim() == ";;"
} }
pub fn is_opener(&self) -> bool { pub fn is_opener(&self) -> bool {
OPENERS.contains(&self.as_str()) || OPENERS.contains(&self.as_str())
matches!(self.class, TkRule::BraceGrpStart) || || matches!(self.class, TkRule::BraceGrpStart)
matches!(self.class, TkRule::CasePattern) || matches!(self.class, TkRule::CasePattern)
} }
pub fn is_closer(&self) -> bool { pub fn is_closer(&self) -> bool {
matches!(self.as_str(), "fi" | "done" | "esac") || matches!(self.as_str(), "fi" | "done" | "esac")
self.has_double_semi() || || self.has_double_semi()
matches!(self.class, TkRule::BraceGrpEnd) || matches!(self.class, TkRule::BraceGrpEnd)
} }
pub fn is_closer_for(&self, other: &Tk) -> bool { pub fn is_closer_for(&self, other: &Tk) -> bool {
if (matches!(other.class, TkRule::BraceGrpStart) && matches!(self.class, TkRule::BraceGrpEnd)) if (matches!(other.class, TkRule::BraceGrpStart) && matches!(self.class, TkRule::BraceGrpEnd))
|| (matches!(other.class, TkRule::CasePattern) && self.has_double_semi()) { || (matches!(other.class, TkRule::CasePattern) && self.has_double_semi())
return true; {
} return true;
match other.as_str() { }
"for" | match other.as_str() {
"while" | "for" | "while" | "until" => matches!(self.as_str(), "done"),
"until" => matches!(self.as_str(), "done"), "if" => matches!(self.as_str(), "fi"),
"if" => matches!(self.as_str(), "fi"), "case" => matches!(self.as_str(), "esac"),
"case" => matches!(self.as_str(), "esac"), _ => false,
_ => false }
} }
}
} }
impl Display for Tk { impl Display for Tk {
@@ -267,9 +266,9 @@ bitflags! {
const ASSIGN = 0b0000000001000000; const ASSIGN = 0b0000000001000000;
const BUILTIN = 0b0000000010000000; const BUILTIN = 0b0000000010000000;
const IS_PROCSUB = 0b0000000100000000; const IS_PROCSUB = 0b0000000100000000;
const IS_HEREDOC = 0b0000001000000000; const IS_HEREDOC = 0b0000001000000000;
const LIT_HEREDOC = 0b0000010000000000; const LIT_HEREDOC = 0b0000010000000000;
const TAB_HEREDOC = 0b0000100000000000; const TAB_HEREDOC = 0b0000100000000000;
} }
} }
@@ -322,11 +321,10 @@ pub struct LexStream {
brc_grp_depth: usize, brc_grp_depth: usize,
brc_grp_start: Option<usize>, brc_grp_start: Option<usize>,
case_depth: usize, case_depth: usize,
heredoc_skip: Option<usize>, heredoc_skip: Option<usize>,
flags: LexFlags, flags: LexFlags,
} }
impl LexStream { impl LexStream {
pub fn new(source: Arc<String>, flags: LexFlags) -> Self { pub fn new(source: Arc<String>, flags: LexFlags) -> Self {
let flags = flags | LexFlags::FRESH | LexFlags::NEXT_IS_CMD; let flags = flags | LexFlags::FRESH | LexFlags::NEXT_IS_CMD;
@@ -338,7 +336,7 @@ impl LexStream {
quote_state: QuoteState::default(), quote_state: QuoteState::default(),
brc_grp_depth: 0, brc_grp_depth: 0,
brc_grp_start: None, brc_grp_start: None,
heredoc_skip: None, heredoc_skip: None,
case_depth: 0, case_depth: 0,
} }
} }
@@ -411,13 +409,13 @@ impl LexStream {
return None; // It's a process sub return None; // It's a process sub
} }
pos += 1; pos += 1;
if let Some('|') = chars.peek() { if let Some('|') = chars.peek() {
// noclobber force '>|' // noclobber force '>|'
chars.next(); chars.next();
pos += 1; pos += 1;
tk = self.get_token(self.cursor..pos, TkRule::Redir); tk = self.get_token(self.cursor..pos, TkRule::Redir);
break break;
} }
if let Some('>') = chars.peek() { if let Some('>') = chars.peek() {
chars.next(); chars.next();
@@ -428,34 +426,34 @@ impl LexStream {
break; break;
}; };
chars.next(); chars.next();
pos += 1; pos += 1;
let mut found_fd = false; let mut found_fd = false;
if chars.peek().is_some_and(|ch| *ch == '-') { if chars.peek().is_some_and(|ch| *ch == '-') {
chars.next(); chars.next();
found_fd = true; found_fd = true;
pos += 1; pos += 1;
} else { } else {
while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) { while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) {
chars.next(); chars.next();
found_fd = true; found_fd = true;
pos += 1; pos += 1;
} }
} }
if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) { if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
let span_start = self.cursor; let span_start = self.cursor;
self.cursor = pos; self.cursor = pos;
return Some(Err(ShErr::at( return Some(Err(ShErr::at(
ShErrKind::ParseErr, ShErrKind::ParseErr,
Span::new(span_start..pos, self.source.clone()), Span::new(span_start..pos, self.source.clone()),
"Invalid redirection", "Invalid redirection",
))); )));
} else { } else {
tk = self.get_token(self.cursor..pos, TkRule::Redir); tk = self.get_token(self.cursor..pos, TkRule::Redir);
break; break;
} }
} }
'<' => { '<' => {
if chars.peek() == Some(&'(') { if chars.peek() == Some(&'(') {
@@ -463,93 +461,93 @@ impl LexStream {
} }
pos += 1; pos += 1;
match chars.peek() { match chars.peek() {
Some('<') => { Some('<') => {
chars.next(); chars.next();
pos += 1; pos += 1;
match chars.peek() { match chars.peek() {
Some('<') => { Some('<') => {
chars.next(); chars.next();
pos += 1; pos += 1;
} }
Some(ch) => { Some(ch) => {
let mut ch = *ch; let mut ch = *ch;
while is_field_sep(ch) { while is_field_sep(ch) {
let Some(next_ch) = chars.next() else { let Some(next_ch) = chars.next() else {
// Incomplete input — fall through to emit << as Redir // Incomplete input — fall through to emit << as Redir
break; break;
}; };
pos += next_ch.len_utf8(); pos += next_ch.len_utf8();
ch = next_ch; ch = next_ch;
} }
if is_field_sep(ch) { if is_field_sep(ch) {
// Ran out of input while skipping whitespace — fall through // Ran out of input while skipping whitespace — fall through
} else { } else {
let saved_cursor = self.cursor; let saved_cursor = self.cursor;
match self.read_heredoc(pos) { match self.read_heredoc(pos) {
Ok(Some(heredoc_tk)) => { Ok(Some(heredoc_tk)) => {
// cursor is set to after the delimiter word; // cursor is set to after the delimiter word;
// heredoc_skip is set to after the body // heredoc_skip is set to after the body
pos = self.cursor; pos = self.cursor;
self.cursor = saved_cursor; self.cursor = saved_cursor;
tk = heredoc_tk; tk = heredoc_tk;
break; break;
} }
Ok(None) => { Ok(None) => {
// Incomplete heredoc — restore cursor and fall through // Incomplete heredoc — restore cursor and fall through
self.cursor = saved_cursor; self.cursor = saved_cursor;
} }
Err(e) => return Some(Err(e)), Err(e) => return Some(Err(e)),
} }
} }
} }
_ => { _ => {
// No delimiter yet — input is incomplete // No delimiter yet — input is incomplete
// Fall through to emit the << as a Redir token // Fall through to emit the << as a Redir token
} }
} }
} }
Some('>') => { Some('>') => {
chars.next(); chars.next();
pos += 1; pos += 1;
tk = self.get_token(self.cursor..pos, TkRule::Redir); tk = self.get_token(self.cursor..pos, TkRule::Redir);
break; break;
} }
Some('&') => { Some('&') => {
chars.next(); chars.next();
pos += 1; pos += 1;
let mut found_fd = false; let mut found_fd = false;
if chars.peek().is_some_and(|ch| *ch == '-') { if chars.peek().is_some_and(|ch| *ch == '-') {
chars.next(); chars.next();
found_fd = true; found_fd = true;
pos += 1; pos += 1;
} else { } else {
while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) { while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) {
chars.next(); chars.next();
found_fd = true; found_fd = true;
pos += 1; pos += 1;
} }
} }
if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) { if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
let span_start = self.cursor; let span_start = self.cursor;
self.cursor = pos; self.cursor = pos;
return Some(Err(ShErr::at( return Some(Err(ShErr::at(
ShErrKind::ParseErr, ShErrKind::ParseErr,
Span::new(span_start..pos, self.source.clone()), Span::new(span_start..pos, self.source.clone()),
"Invalid redirection", "Invalid redirection",
))); )));
} else { } else {
tk = self.get_token(self.cursor..pos, TkRule::Redir); tk = self.get_token(self.cursor..pos, TkRule::Redir);
break; break;
} }
} }
_ => {} _ => {}
} }
tk = self.get_token(self.cursor..pos, TkRule::Redir); tk = self.get_token(self.cursor..pos, TkRule::Redir);
break; break;
@@ -574,130 +572,133 @@ impl LexStream {
self.cursor = pos; self.cursor = pos;
Some(Ok(tk)) Some(Ok(tk))
} }
pub fn read_heredoc(&mut self, mut pos: usize) -> ShResult<Option<Tk>> { pub fn read_heredoc(&mut self, mut pos: usize) -> ShResult<Option<Tk>> {
let slice = self.slice(pos..).unwrap_or_default().to_string(); let slice = self.slice(pos..).unwrap_or_default().to_string();
let mut chars = slice.chars(); let mut chars = slice.chars();
let mut delim = String::new(); let mut delim = String::new();
let mut flags = TkFlags::empty(); let mut flags = TkFlags::empty();
let mut first_char = true; let mut first_char = true;
// Parse the delimiter word, stripping quotes // Parse the delimiter word, stripping quotes
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
'-' if first_char => { '-' if first_char => {
pos += 1; pos += 1;
flags |= TkFlags::TAB_HEREDOC; flags |= TkFlags::TAB_HEREDOC;
} }
'\"' => { '\"' => {
pos += 1; pos += 1;
self.quote_state.toggle_double(); self.quote_state.toggle_double();
flags |= TkFlags::LIT_HEREDOC; flags |= TkFlags::LIT_HEREDOC;
} }
'\'' => { '\'' => {
pos += 1; pos += 1;
self.quote_state.toggle_single(); self.quote_state.toggle_single();
flags |= TkFlags::LIT_HEREDOC; flags |= TkFlags::LIT_HEREDOC;
} }
_ if self.quote_state.in_quote() => { _ if self.quote_state.in_quote() => {
pos += ch.len_utf8(); pos += ch.len_utf8();
delim.push(ch); delim.push(ch);
} }
ch if is_hard_sep(ch) => { ch if is_hard_sep(ch) => {
break; break;
} }
ch => { ch => {
pos += ch.len_utf8(); pos += ch.len_utf8();
delim.push(ch); delim.push(ch);
} }
} }
first_char = false; first_char = false;
} }
// pos is now right after the delimiter word — this is where // pos is now right after the delimiter word — this is where
// the cursor should return so the rest of the line gets lexed // the cursor should return so the rest of the line gets lexed
let cursor_after_delim = pos; let cursor_after_delim = pos;
// Re-slice from cursor_after_delim so iterator and pos are in sync // Re-slice from cursor_after_delim so iterator and pos are in sync
// (the old chars iterator consumed the hard_sep without advancing pos) // (the old chars iterator consumed the hard_sep without advancing pos)
let rest = self.slice(cursor_after_delim..).unwrap_or_default().to_string(); let rest = self
let mut chars = rest.chars(); .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) // Scan forward to the newline (or use heredoc_skip from a previous heredoc)
let body_start = if let Some(skip) = self.heredoc_skip { let body_start = if let Some(skip) = self.heredoc_skip {
// A previous heredoc on this line already read its body; // A previous heredoc on this line already read its body;
// our body starts where that one ended // our body starts where that one ended
let skip_offset = skip - cursor_after_delim; let skip_offset = skip - cursor_after_delim;
for _ in 0..skip_offset { for _ in 0..skip_offset {
chars.next(); chars.next();
} }
skip skip
} else { } else {
// Skip the rest of the current line to find where the body begins // Skip the rest of the current line to find where the body begins
let mut scan = pos; let mut scan = pos;
let mut found_newline = false; let mut found_newline = false;
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
scan += ch.len_utf8(); scan += ch.len_utf8();
if ch == '\n' { if ch == '\n' {
found_newline = true; found_newline = true;
break; break;
} }
} }
if !found_newline { if !found_newline {
if self.flags.contains(LexFlags::LEX_UNFINISHED) { if self.flags.contains(LexFlags::LEX_UNFINISHED) {
return Ok(None); return Ok(None);
} else { } else {
return Err(ShErr::at( return Err(ShErr::at(
ShErrKind::ParseErr, ShErrKind::ParseErr,
Span::new(pos..pos, self.source.clone()), Span::new(pos..pos, self.source.clone()),
"Heredoc delimiter not found", "Heredoc delimiter not found",
)); ));
} }
} }
scan scan
}; };
pos = body_start; pos = body_start;
let start = pos; let start = pos;
// Read lines until we find one that matches the delimiter exactly // Read lines until we find one that matches the delimiter exactly
let mut line = String::new(); let mut line = String::new();
let mut line_start = pos; let mut line_start = pos;
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
pos += ch.len_utf8(); pos += ch.len_utf8();
if ch == '\n' { if ch == '\n' {
let trimmed = line.trim_end_matches('\r'); let trimmed = line.trim_end_matches('\r');
if trimmed == delim { if trimmed == delim {
let mut tk = self.get_token(start..line_start, TkRule::Redir); let mut tk = self.get_token(start..line_start, TkRule::Redir);
tk.flags |= TkFlags::IS_HEREDOC | flags; tk.flags |= TkFlags::IS_HEREDOC | flags;
self.heredoc_skip = Some(pos); self.heredoc_skip = Some(pos);
self.cursor = cursor_after_delim; self.cursor = cursor_after_delim;
return Ok(Some(tk)); return Ok(Some(tk));
} }
line.clear(); line.clear();
line_start = pos; line_start = pos;
} else { } else {
line.push(ch); line.push(ch);
} }
} }
// Check the last line (no trailing newline) // Check the last line (no trailing newline)
let trimmed = line.trim_end_matches('\r'); let trimmed = line.trim_end_matches('\r');
if trimmed == delim { if trimmed == delim {
let mut tk = self.get_token(start..line_start, TkRule::Redir); let mut tk = self.get_token(start..line_start, TkRule::Redir);
tk.flags |= TkFlags::IS_HEREDOC | flags; tk.flags |= TkFlags::IS_HEREDOC | flags;
self.heredoc_skip = Some(pos); self.heredoc_skip = Some(pos);
self.cursor = cursor_after_delim; self.cursor = cursor_after_delim;
return Ok(Some(tk)); return Ok(Some(tk));
} }
if !self.flags.contains(LexFlags::LEX_UNFINISHED) { if !self.flags.contains(LexFlags::LEX_UNFINISHED) {
Err(ShErr::at( Err(ShErr::at(
ShErrKind::ParseErr, ShErrKind::ParseErr,
Span::new(start..pos, self.source.clone()), Span::new(start..pos, self.source.clone()),
format!("Heredoc delimiter '{}' not found", delim), format!("Heredoc delimiter '{}' not found", delim),
)) ))
} else { } else {
Ok(None) Ok(None)
} }
} }
pub fn read_string(&mut self) -> ShResult<Tk> { pub fn read_string(&mut self) -> ShResult<Tk> {
assert!(self.cursor <= self.source.len()); assert!(self.cursor <= self.source.len());
let slice = self.slice_from_cursor().unwrap().to_string(); 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 // If a heredoc was parsed on this line, skip past the body
// Only on newline — ';' is a command separator within the same line // Only on newline — ';' is a command separator within the same line
if (ch == '\n' || ch == '\r') if (ch == '\n' || ch == '\r')
&& let Some(skip) = self.heredoc_skip.take() { && let Some(skip) = self.heredoc_skip.take()
self.cursor = skip; {
} self.cursor = skip;
}
while let Some(ch) = get_char(&self.source, self.cursor) { while let Some(ch) = get_char(&self.source, self.cursor) {
match ch { match ch {

View File

@@ -12,7 +12,8 @@ use crate::{
}, },
parse::lex::clean_input, parse::lex::clean_input,
prelude::*, prelude::*,
procio::IoMode, state::read_shopts, procio::IoMode,
state::read_shopts,
}; };
pub mod execute; pub mod execute;
@@ -280,17 +281,21 @@ bitflags! {
pub struct Redir { pub struct Redir {
pub io_mode: IoMode, pub io_mode: IoMode,
pub class: RedirType, pub class: RedirType,
pub span: Option<Span> pub span: Option<Span>,
} }
impl Redir { impl Redir {
pub fn new(io_mode: IoMode, class: RedirType) -> Self { 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)] #[derive(Default, Debug)]
@@ -298,7 +303,7 @@ pub struct RedirBldr {
pub io_mode: Option<IoMode>, pub io_mode: Option<IoMode>,
pub class: Option<RedirType>, pub class: Option<RedirType>,
pub tgt_fd: Option<RawFd>, pub tgt_fd: Option<RawFd>,
pub span: Option<Span>, pub span: Option<Span>,
} }
impl RedirBldr { impl RedirBldr {
@@ -306,36 +311,36 @@ impl RedirBldr {
Default::default() Default::default()
} }
pub fn with_io_mode(self, io_mode: IoMode) -> Self { pub fn with_io_mode(self, io_mode: IoMode) -> Self {
Self { Self {
io_mode: Some(io_mode), io_mode: Some(io_mode),
..self ..self
} }
} }
pub fn with_class(self, class: RedirType) -> Self { pub fn with_class(self, class: RedirType) -> Self {
Self { Self {
class: Some(class), class: Some(class),
..self ..self
} }
} }
pub fn with_tgt(self, tgt_fd: RawFd) -> Self { pub fn with_tgt(self, tgt_fd: RawFd) -> Self {
Self { Self {
tgt_fd: Some(tgt_fd), tgt_fd: Some(tgt_fd),
..self ..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 { pub fn build(self) -> Redir {
let new = Redir::new(self.io_mode.unwrap(), self.class.unwrap()); let new = Redir::new(self.io_mode.unwrap(), self.class.unwrap());
if let Some(span) = self.span { if let Some(span) = self.span {
new.with_span(span) new.with_span(span)
} else { } else {
new new
} }
} }
} }
@@ -355,23 +360,23 @@ impl FromStr for RedirBldr {
chars.next(); chars.next();
redir = redir.with_class(RedirType::Append); redir = redir.with_class(RedirType::Append);
} else if let Some('|') = chars.peek() { } else if let Some('|') = chars.peek() {
chars.next(); chars.next();
redir = redir.with_class(RedirType::OutputForce); redir = redir.with_class(RedirType::OutputForce);
} }
} }
'<' => { '<' => {
redir = redir.with_class(RedirType::Input); redir = redir.with_class(RedirType::Input);
let mut count = 0; let mut count = 0;
if chars.peek() == Some(&'>') { if chars.peek() == Some(&'>') {
chars.next(); // consume the '>' chars.next(); // consume the '>'
redir = redir.with_class(RedirType::ReadWrite); redir = redir.with_class(RedirType::ReadWrite);
} else { } else {
while count < 2 && matches!(chars.peek(), Some('<')) { while count < 2 && matches!(chars.peek(), Some('<')) {
chars.next(); chars.next();
count += 1; count += 1;
} }
} }
redir = match count { redir = match count {
1 => redir.with_class(RedirType::HereDoc), 1 => redir.with_class(RedirType::HereDoc),
@@ -380,23 +385,23 @@ impl FromStr for RedirBldr {
}; };
} }
'&' => { '&' => {
if chars.peek() == Some(&'-') { if chars.peek() == Some(&'-') {
chars.next(); chars.next();
src_fd.push('-'); src_fd.push('-');
} else { } else {
while let Some(next_ch) = chars.next() { while let Some(next_ch) = chars.next() {
if next_ch.is_ascii_digit() { if next_ch.is_ascii_digit() {
src_fd.push(next_ch) src_fd.push(next_ch)
} else { } else {
break; break;
} }
} }
} }
if src_fd.is_empty() { if src_fd.is_empty() {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::ParseErr, ShErrKind::ParseErr,
format!("Invalid character '{}' in redirection operator", ch), format!("Invalid character '{}' in redirection operator", ch),
)); ));
} }
} }
_ if ch.is_ascii_digit() && tgt_fd.is_empty() => { _ if ch.is_ascii_digit() && tgt_fd.is_empty() => {
@@ -410,27 +415,26 @@ impl FromStr for RedirBldr {
} }
} }
} }
_ => return Err(ShErr::simple( _ => {
ShErrKind::ParseErr, return Err(ShErr::simple(
format!("Invalid character '{}' in redirection operator", ch), ShErrKind::ParseErr,
)), format!("Invalid character '{}' in redirection operator", ch),
));
}
} }
} }
let tgt_fd = tgt_fd let tgt_fd = tgt_fd
.parse::<i32>() .parse::<i32>()
.unwrap_or_else(|_| match redir.class.unwrap() { .unwrap_or_else(|_| match redir.class.unwrap() {
RedirType::Input | RedirType::Input | RedirType::ReadWrite | RedirType::HereDoc | RedirType::HereString => 0,
RedirType::ReadWrite |
RedirType::HereDoc |
RedirType::HereString => 0,
_ => 1, _ => 1,
}); });
redir = redir.with_tgt(tgt_fd); redir = redir.with_tgt(tgt_fd);
if src_fd.as_str() == "-" { if src_fd.as_str() == "-" {
let io_mode = IoMode::Close { tgt_fd }; let io_mode = IoMode::Close { tgt_fd };
redir = redir.with_io_mode(io_mode); redir = redir.with_io_mode(io_mode);
} else if let Ok(src_fd) = src_fd.parse::<i32>() { } else if let Ok(src_fd) = src_fd.parse::<i32>() {
let io_mode = IoMode::fd(tgt_fd, src_fd); let io_mode = IoMode::fd(tgt_fd, src_fd);
redir = redir.with_io_mode(io_mode); redir = redir.with_io_mode(io_mode);
} }
@@ -439,40 +443,40 @@ impl FromStr for RedirBldr {
} }
impl TryFrom<Tk> for RedirBldr { impl TryFrom<Tk> for RedirBldr {
type Error = ShErr; type Error = ShErr;
fn try_from(tk: Tk) -> Result<Self, Self::Error> { fn try_from(tk: Tk) -> Result<Self, Self::Error> {
let span = tk.span.clone(); let span = tk.span.clone();
if tk.flags.contains(TkFlags::IS_HEREDOC) { if tk.flags.contains(TkFlags::IS_HEREDOC) {
let flags = tk.flags; let flags = tk.flags;
Ok(RedirBldr { Ok(RedirBldr {
io_mode: Some(IoMode::buffer(0, tk.to_string(), flags)?), io_mode: Some(IoMode::buffer(0, tk.to_string(), flags)?),
class: Some(RedirType::HereDoc), class: Some(RedirType::HereDoc),
tgt_fd: Some(0), tgt_fd: Some(0),
span: Some(span) span: Some(span),
}) })
} else { } else {
match Self::from_str(tk.as_str()) { match Self::from_str(tk.as_str()) {
Ok(bldr) => Ok(bldr.with_span(span)), Ok(bldr) => Ok(bldr.with_span(span)),
Err(e) => Err(e.promote(span)), Err(e) => Err(e.promote(span)),
} }
} }
} }
} }
#[derive(PartialEq, Clone, Copy, Debug)] #[derive(PartialEq, Clone, Copy, Debug)]
pub enum RedirType { pub enum RedirType {
Null, // Default Null, // Default
Pipe, // | Pipe, // |
PipeAnd, // |&, redirs stderr and stdout PipeAnd, // |&, redirs stderr and stdout
Input, // < Input, // <
Output, // > Output, // >
OutputForce,// >| OutputForce, // >|
Append, // >> Append, // >>
HereDoc, // << HereDoc, // <<
IndentHereDoc, // <<-, strips leading tabs IndentHereDoc, // <<-, strips leading tabs
HereString, // <<< HereString, // <<<
ReadWrite, // <>, fd is opened for reading and writing ReadWrite, // <>, fd is opened for reading and writing
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -887,7 +891,9 @@ impl ParseStream {
// Two forms: "name()" as one token, or "name" followed by "()" as separate tokens // Two forms: "name()" as one token, or "name" followed by "()" as separate tokens
let spaced_form = !is_func_name(self.peek_tk()) 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)); && is_func_parens(self.tokens.get(1));
if !is_func_name(self.peek_tk()) && !spaced_form { if !is_func_name(self.peek_tk()) && !spaced_form {
@@ -1032,7 +1038,7 @@ impl ParseStream {
Ok(Some(node)) Ok(Some(node))
} }
fn parse_brc_grp(&mut self, from_func_def: bool) -> ShResult<Option<Node>> { fn parse_brc_grp(&mut self, from_func_def: bool) -> ShResult<Option<Node>> {
log::debug!("Trying to parse a brace group"); log::debug!("Trying to parse a brace group");
let mut node_tks: Vec<Tk> = vec![]; let mut node_tks: Vec<Tk> = vec![];
let mut body: Vec<Node> = vec![]; let mut body: Vec<Node> = vec![];
let mut redirs: Vec<Redir> = vec![]; let mut redirs: Vec<Redir> = vec![];
@@ -1045,7 +1051,7 @@ impl ParseStream {
self.catch_separator(&mut node_tks); self.catch_separator(&mut node_tks);
loop { loop {
log::debug!("Parsing a brace group body"); log::debug!("Parsing a brace group body");
if *self.next_tk_class() == TkRule::BraceGrpEnd { if *self.next_tk_class() == TkRule::BraceGrpEnd {
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
break; break;
@@ -1054,25 +1060,25 @@ impl ParseStream {
node_tks.extend(node.tokens.clone()); node_tks.extend(node.tokens.clone());
body.push(node); body.push(node);
} else if *self.next_tk_class() != TkRule::BraceGrpEnd { } else if *self.next_tk_class() != TkRule::BraceGrpEnd {
let next = self.peek_tk().cloned(); let next = self.peek_tk().cloned();
let err = match next { let err = match next {
Some(tk) => Err(parse_err_full( Some(tk) => Err(parse_err_full(
&format!("Unexpected token '{}' in brace group body", tk.as_str()), &format!("Unexpected token '{}' in brace group body", tk.as_str()),
&tk.span, &tk.span,
self.context.clone(), self.context.clone(),
)), )),
None => Err(parse_err_full( None => Err(parse_err_full(
"Unexpected end of input while parsing brace group body", "Unexpected end of input while parsing brace group body",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone(), self.context.clone(),
)), )),
}; };
self.panic_mode(&mut node_tks); self.panic_mode(&mut node_tks);
return err; return err;
} }
self.catch_separator(&mut node_tks); self.catch_separator(&mut node_tks);
if !self.next_tk_is_some() { 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); self.panic_mode(&mut node_tks);
return Err(parse_err_full( return Err(parse_err_full(
"Expected a closing brace for this brace group", "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 { if !from_func_def {
self.parse_redir(&mut redirs, &mut node_tks)?; 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 { let node = Node {
class: NdRule::BraceGrp { body }, class: NdRule::BraceGrp { body },
@@ -1106,7 +1114,11 @@ impl ParseStream {
context: LabelCtx, context: LabelCtx,
) -> ShResult<Redir> { ) -> ShResult<Redir> {
let redir_bldr = RedirBldr::try_from(redir_tk.clone()).unwrap(); 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() { if redir_bldr.io_mode.is_some() {
return Ok(redir_bldr.build()); return Ok(redir_bldr.build());
} }
@@ -1126,11 +1138,7 @@ impl ParseStream {
"Expected a string after this redirection", "Expected a string after this redirection",
)); ));
} }
let mut string = next_tk let mut string = next_tk.unwrap().expand()?.get_words().join(" ");
.unwrap()
.expand()?
.get_words()
.join(" ");
string.push('\n'); string.push('\n');
let io_mode = IoMode::buffer(redir_bldr.tgt_fd.unwrap_or(0), string, redir_tk.flags)?; 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()) Ok(redir_bldr.with_io_mode(io_mode).build())
@@ -1155,7 +1163,7 @@ impl ParseStream {
while self.check_redir() { while self.check_redir() {
let tk = self.next_tk().unwrap(); let tk = self.next_tk().unwrap();
node_tks.push(tk.clone()); 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)?; let redir = Self::build_redir(&tk, || self.next_tk(), node_tks, ctx)?;
redirs.push(redir); redirs.push(redir);
} }
@@ -1663,7 +1671,7 @@ impl ParseStream {
node_tks.push(prefix_tk.clone()); node_tks.push(prefix_tk.clone());
assignments.push(assign) assignments.push(assign)
} else if is_keyword { } else if is_keyword {
return Ok(None) return Ok(None);
} else if prefix_tk.class == TkRule::Sep { } else if prefix_tk.class == TkRule::Sep {
// Separator ends the prefix section - add it so commit() consumes it // Separator ends the prefix section - add it so commit() consumes it
node_tks.push(prefix_tk.clone()); node_tks.push(prefix_tk.clone());
@@ -1721,7 +1729,7 @@ impl ParseStream {
} }
TkRule::Redir => { TkRule::Redir => {
node_tks.push(tk.clone()); 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)?; let redir = Self::build_redir(tk, || tk_iter.next().cloned(), &mut node_tks, ctx)?;
redirs.push(redir); redirs.push(redir);
} }
@@ -1882,34 +1890,33 @@ pub fn get_redir_file<P: AsRef<Path>>(class: RedirType, path: P) -> ShResult<Fil
let path = path.as_ref(); let path = path.as_ref();
let result = match class { let result = match class {
RedirType::Input => OpenOptions::new().read(true).open(Path::new(&path)), RedirType::Input => OpenOptions::new().read(true).open(Path::new(&path)),
RedirType::Output => { RedirType::Output => {
if read_shopts(|o| o.core.noclobber) && path.is_file() { if read_shopts(|o| o.core.noclobber) && path.is_file() {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("shopt core.noclobber is set, refusing to overwrite existing file `{}`", path.display()), format!(
)); "shopt core.noclobber is set, refusing to overwrite existing file `{}`",
} path.display()
OpenOptions::new() ),
.write(true) ));
.create(true) }
.truncate(true) OpenOptions::new()
.open(path) .write(true)
}, .create(true)
RedirType::ReadWrite => { .truncate(true)
OpenOptions::new() .open(path)
.write(true) }
.read(true) RedirType::ReadWrite => OpenOptions::new()
.create(true) .write(true)
.truncate(false) .read(true)
.open(path) .create(true)
} .truncate(false)
RedirType::OutputForce => { .open(path),
OpenOptions::new() RedirType::OutputForce => OpenOptions::new()
.write(true) .write(true)
.create(true) .create(true)
.truncate(true) .truncate(true)
.open(path) .open(path),
}
RedirType::Append => OpenOptions::new().create(true).append(true).open(path), RedirType::Append => OpenOptions::new().create(true).append(true).open(path),
_ => unimplemented!("Unimplemented redir type: {:?}", class), _ => 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 { fn is_func_parens(tk: Option<&Tk>) -> bool {
tk.is_some_and(|tk| { tk.is_some_and(|tk| tk.flags.contains(TkFlags::KEYWORD) && tk.span.as_str() == "()")
tk.flags.contains(TkFlags::KEYWORD) && tk.span.as_str() == "()"
})
} }
/// Perform an operation on the child nodes of a given node /// Perform an operation on the child nodes of a given node
@@ -2814,8 +2819,8 @@ pub mod tests {
// ===================== Heredoc Execution ===================== // ===================== Heredoc Execution =====================
use crate::testutil::{TestGuard, test_input};
use crate::state::{VarFlags, VarKind, write_vars}; use crate::state::{VarFlags, VarKind, write_vars};
use crate::testutil::{TestGuard, test_input};
#[test] #[test]
fn heredoc_basic_output() { fn heredoc_basic_output() {

View File

@@ -12,7 +12,8 @@ use crate::{
utils::RedirVecUtils, utils::RedirVecUtils,
}, },
parse::{Redir, RedirType, get_redir_file, lex::TkFlags}, 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 // Credit to fish-shell for many of the implementation ideas present in this
@@ -48,9 +49,9 @@ pub enum IoMode {
pipe: Arc<OwnedFd>, pipe: Arc<OwnedFd>,
}, },
Buffer { Buffer {
tgt_fd: RawFd, tgt_fd: RawFd,
buf: String, 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 { Close {
tgt_fd: RawFd, tgt_fd: RawFd,
@@ -91,7 +92,9 @@ impl IoMode {
if let IoMode::File { tgt_fd, path, mode } = self { 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, 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); 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, // 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). // causing dup2(3,3) to be a no-op and then OwnedFd drop closes it).
let raw = file.as_raw_fd(); let raw = file.as_raw_fd();
let high = fcntl(raw, FcntlArg::F_DUPFD_CLOEXEC(MIN_INTERNAL_FD)) let high = fcntl(raw, FcntlArg::F_DUPFD_CLOEXEC(MIN_INTERNAL_FD)).map_err(ShErr::from)?;
.map_err(ShErr::from)?;
drop(file); // closes the original low fd drop(file); // closes the original low fd
self = IoMode::OpenedFile { self = IoMode::OpenedFile {
tgt_fd, tgt_fd,
@@ -110,9 +112,9 @@ impl IoMode {
} }
Ok(self) Ok(self)
} }
pub fn buffer(tgt_fd: RawFd, buf: String, flags: TkFlags) -> ShResult<Self> { pub fn buffer(tgt_fd: RawFd, buf: String, flags: TkFlags) -> ShResult<Self> {
Ok(Self::Buffer { tgt_fd, buf, flags }) Ok(Self::Buffer { tgt_fd, buf, flags })
} }
pub fn get_pipes() -> (Self, Self) { pub fn get_pipes() -> (Self, Self) {
let (rpipe, wpipe) = nix::unistd::pipe2(OFlag::O_CLOEXEC).unwrap(); let (rpipe, wpipe) = nix::unistd::pipe2(OFlag::O_CLOEXEC).unwrap();
( (
@@ -244,74 +246,78 @@ impl<'e> IoFrame {
fn apply_redirs(&mut self) -> ShResult<()> { fn apply_redirs(&mut self) -> ShResult<()> {
for redir in &mut self.redirs { for redir in &mut self.redirs {
let io_mode = &mut redir.io_mode; let io_mode = &mut redir.io_mode;
match io_mode { match io_mode {
IoMode::Close { tgt_fd } => { IoMode::Close { tgt_fd } => {
if *tgt_fd == *TTY_FILENO { if *tgt_fd == *TTY_FILENO {
// Don't let user close the shell's tty fd. // Don't let user close the shell's tty fd.
continue; continue;
} }
close(*tgt_fd).ok(); close(*tgt_fd).ok();
continue; continue;
} }
IoMode::File { .. } => { IoMode::File { .. } => match io_mode.clone().open_file() {
match io_mode.clone().open_file() { Ok(file) => *io_mode = file,
Ok(file) => *io_mode = file, Err(e) => {
Err(e) => { if let Some(span) = redir.span.as_ref() {
if let Some(span) = redir.span.as_ref() { return Err(e.promote(span.clone()));
return Err(e.promote(span.clone())); }
} return Err(e);
return Err(e) }
} },
} IoMode::Buffer { tgt_fd, buf, flags } => {
} let (rpipe, wpipe) = nix::unistd::pipe()?;
IoMode::Buffer { tgt_fd, buf, flags } => { let mut text = if flags.contains(TkFlags::LIT_HEREDOC) {
let (rpipe, wpipe) = nix::unistd::pipe()?; buf.clone()
let mut text = if flags.contains(TkFlags::LIT_HEREDOC) { } else {
buf.clone() let words = Expander::from_raw(buf, *flags)?.expand()?;
} else { if flags.contains(TkFlags::IS_HEREDOC) {
let words = Expander::from_raw(buf, *flags)?.expand()?; words.into_iter().next().unwrap_or_default()
if flags.contains(TkFlags::IS_HEREDOC) { } else {
words.into_iter().next().unwrap_or_default() let ifs = state::get_separator();
} else { words.join(&ifs).trim().to_string() + "\n"
let ifs = state::get_separator(); }
words.join(&ifs).trim().to_string() + "\n" };
} if flags.contains(TkFlags::TAB_HEREDOC) {
}; let lines = text.lines();
if flags.contains(TkFlags::TAB_HEREDOC) { let mut min_tabs = usize::MAX;
let lines = text.lines(); for line in lines {
let mut min_tabs = usize::MAX; if line.is_empty() {
for line in lines { continue;
if line.is_empty() { continue; } }
let line_len = line.len(); let line_len = line.len();
let after_strip = line.trim_start_matches('\t').len(); let after_strip = line.trim_start_matches('\t').len();
let delta = line_len - after_strip; let delta = line_len - after_strip;
min_tabs = min_tabs.min(delta); min_tabs = min_tabs.min(delta);
} }
if min_tabs == usize::MAX { if min_tabs == usize::MAX {
// let's avoid possibly allocating a string with 18 quintillion tabs // let's avoid possibly allocating a string with 18 quintillion tabs
min_tabs = 0; min_tabs = 0;
} }
if min_tabs > 0 { if min_tabs > 0 {
let stripped = text.lines() let stripped = text
.fold(vec![], |mut acc, ln| { .lines()
if ln.is_empty() { .fold(vec![], |mut acc, ln| {
acc.push(""); if ln.is_empty() {
return acc; acc.push("");
} return acc;
let stripped_ln = ln.strip_prefix(&"\t".repeat(min_tabs)).unwrap(); }
acc.push(stripped_ln); let stripped_ln = ln.strip_prefix(&"\t".repeat(min_tabs)).unwrap();
acc acc.push(stripped_ln);
}) acc
.join("\n"); })
text = stripped + "\n"; .join("\n");
} text = stripped + "\n";
} }
write(wpipe, text.as_bytes())?; }
*io_mode = IoMode::Pipe { tgt_fd: *tgt_fd, pipe: rpipe.into() }; write(wpipe, text.as_bytes())?;
} *io_mode = IoMode::Pipe {
_ => {} tgt_fd: *tgt_fd,
} pipe: rpipe.into(),
};
}
_ => {}
}
let tgt_fd = io_mode.tgt_fd(); let tgt_fd = io_mode.tgt_fd();
let src_fd = io_mode.src_fd(); let src_fd = io_mode.src_fd();
if let Err(e) = dup2(src_fd, tgt_fd) { if let Err(e) = dup2(src_fd, tgt_fd) {

View File

@@ -352,69 +352,73 @@ impl ClampedUsize {
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
pub struct IndentCtx { pub struct IndentCtx {
depth: usize, depth: usize,
ctx: Vec<Tk>, ctx: Vec<Tk>,
in_escaped_line: bool in_escaped_line: bool,
} }
impl IndentCtx { impl IndentCtx {
pub fn new() -> Self { Self::default() } pub fn new() -> Self {
Self::default()
}
pub fn depth(&self) -> usize { pub fn depth(&self) -> usize {
self.depth self.depth
} }
pub fn ctx(&self) -> &[Tk] { pub fn ctx(&self) -> &[Tk] {
&self.ctx &self.ctx
} }
pub fn descend(&mut self, tk: Tk) { pub fn descend(&mut self, tk: Tk) {
self.ctx.push(tk); self.ctx.push(tk);
self.depth += 1; self.depth += 1;
} }
pub fn ascend(&mut self) { pub fn ascend(&mut self) {
self.depth = self.depth.saturating_sub(1); self.depth = self.depth.saturating_sub(1);
self.ctx.pop(); self.ctx.pop();
} }
pub fn reset(&mut self) { pub fn reset(&mut self) {
std::mem::take(self); std::mem::take(self);
} }
pub fn check_tk(&mut self, tk: Tk) { pub fn check_tk(&mut self, tk: Tk) {
if tk.is_opener() { if tk.is_opener() {
self.descend(tk); self.descend(tk);
} else if self.ctx.last().is_some_and(|t| tk.is_closer_for(t)) { } else if self.ctx.last().is_some_and(|t| tk.is_closer_for(t)) {
self.ascend(); self.ascend();
} else if matches!(tk.class, TkRule::Sep) && self.in_escaped_line { } else if matches!(tk.class, TkRule::Sep) && self.in_escaped_line {
self.in_escaped_line = false; self.in_escaped_line = false;
self.depth = self.depth.saturating_sub(1); self.depth = self.depth.saturating_sub(1);
} }
} }
pub fn calculate(&mut self, input: &str) -> usize { pub fn calculate(&mut self, input: &str) -> usize {
self.depth = 0; self.depth = 0;
self.ctx.clear(); self.ctx.clear();
self.in_escaped_line = false; self.in_escaped_line = false;
let input_arc = Arc::new(input.to_string()); let input_arc = Arc::new(input.to_string());
let Ok(tokens) = LexStream::new(input_arc, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>() else { let Ok(tokens) =
log::error!("Lexing failed during depth calculation: {:?}", input); LexStream::new(input_arc, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()
return 0; else {
}; log::error!("Lexing failed during depth calculation: {:?}", input);
return 0;
};
for tk in tokens { for tk in tokens {
self.check_tk(tk); self.check_tk(tk);
} }
if input.ends_with("\\\n") { if input.ends_with("\\\n") {
self.in_escaped_line = true; self.in_escaped_line = true;
self.depth += 1; self.depth += 1;
} }
self.depth self.depth
} }
} }
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
@@ -684,17 +688,17 @@ impl LineBuf {
pub fn read_slice_to_cursor(&self) -> Option<&str> { pub fn read_slice_to_cursor(&self) -> Option<&str> {
self.read_slice_to(self.cursor.get()) self.read_slice_to(self.cursor.get())
} }
pub fn cursor_is_escaped(&mut self) -> bool { pub fn cursor_is_escaped(&mut self) -> bool {
let Some(to_cursor) = self.slice_to_cursor() else { let Some(to_cursor) = self.slice_to_cursor() else {
return false; return false;
}; };
// count the number of backslashes // count the number of backslashes
let delta = to_cursor.len() - to_cursor.trim_end_matches('\\').len(); let delta = to_cursor.len() - to_cursor.trim_end_matches('\\').len();
// an even number of backslashes means each one is escaped // an even number of backslashes means each one is escaped
delta % 2 != 0 delta % 2 != 0
} }
pub fn slice_to_cursor_inclusive(&mut self) -> Option<&str> { pub fn slice_to_cursor_inclusive(&mut self) -> Option<&str> {
self.slice_to(self.cursor.ret_add(1)) self.slice_to(self.cursor.ret_add(1))
} }
@@ -2928,29 +2932,29 @@ impl LineBuf {
}; };
end = end.saturating_sub(1); end = end.saturating_sub(1);
let mut last_was_whitespace = false; let mut last_was_whitespace = false;
let mut last_was_escape = false; let mut last_was_escape = false;
let mut i = start; let mut i = start;
while i < end { while i < end {
let Some(gr) = self.grapheme_at(i) else { let Some(gr) = self.grapheme_at(i) else {
i += 1; i += 1;
continue; continue;
}; };
if gr == "\n" { if gr == "\n" {
if last_was_whitespace { if last_was_whitespace {
self.remove(i); self.remove(i);
end -= 1; end -= 1;
} else { } else {
self.force_replace_at(i, " "); self.force_replace_at(i, " ");
} }
if last_was_escape { if last_was_escape {
// if we are here, then we just joined an escaped newline // if we are here, then we just joined an escaped newline
// semantically, echo foo\\nbar == echo foo bar // semantically, echo foo\\nbar == echo foo bar
// so a joined line should remove the escape. // so a joined line should remove the escape.
self.remove(i - 1); self.remove(i - 1);
end -= 1; end -= 1;
} }
last_was_whitespace = false; last_was_whitespace = false;
last_was_escape = false; last_was_escape = false;
let strip_pos = if self.grapheme_at(i) == Some(" ") { let strip_pos = if self.grapheme_at(i) == Some(" ") {
i + 1 i + 1
} else { } else {
@@ -2958,24 +2962,24 @@ impl LineBuf {
}; };
while self.grapheme_at(strip_pos) == Some("\t") { while self.grapheme_at(strip_pos) == Some("\t") {
self.remove(strip_pos); self.remove(strip_pos);
end -= 1; end -= 1;
} }
self.cursor.set(i); self.cursor.set(i);
i += 1; i += 1;
continue; continue;
} else if gr == "\\" { } else if gr == "\\" {
if last_was_whitespace && last_was_escape { if last_was_whitespace && last_was_escape {
// if we are here, then the pattern of the last three chars was this: // if we are here, then the pattern of the last three chars was this:
// ' \\', a space and two backslashes. // ' \\', a space and two backslashes.
// This means the "last" was an escaped backslash, not whitespace. // This means the "last" was an escaped backslash, not whitespace.
last_was_whitespace = false; last_was_whitespace = false;
} }
last_was_escape = !last_was_escape; last_was_escape = !last_was_escape;
} else { } else {
last_was_whitespace = is_whitespace(gr); last_was_whitespace = is_whitespace(gr);
last_was_escape = false; last_was_escape = false;
} }
i += 1; i += 1;
} }
Ok(()) Ok(())
} }
@@ -2984,24 +2988,23 @@ impl LineBuf {
self.cursor.add(1); self.cursor.add(1);
let before_escaped = self.indent_ctx.in_escaped_line; let before_escaped = self.indent_ctx.in_escaped_line;
let before = self.indent_ctx.depth(); let before = self.indent_ctx.depth();
if read_shopts(|o| o.prompt.auto_indent) { if read_shopts(|o| o.prompt.auto_indent) {
let after = self.calc_indent_level(); let after = self.calc_indent_level();
// Only dedent if the depth decrease came from a closer, not from // Only dedent if the depth decrease came from a closer, not from
// a line continuation bonus going away // a line continuation bonus going away
if after < before if after < before && !(before_escaped && !self.indent_ctx.in_escaped_line) {
&& !(before_escaped && !self.indent_ctx.in_escaped_line) { let delta = before - after;
let delta = before - after; let line_start = self.start_of_line();
let line_start = self.start_of_line(); for _ in 0..delta {
for _ in 0..delta { if self.grapheme_at(line_start).is_some_and(|gr| gr == "\t") {
if self.grapheme_at(line_start).is_some_and(|gr| gr == "\t") { self.remove(line_start);
self.remove(line_start); if !self.cursor_at_max() {
if !self.cursor_at_max() { self.cursor.sub(1);
self.cursor.sub(1); }
} }
} }
} }
} }
}
} }
fn verb_insert(&mut self, string: String) { fn verb_insert(&mut self, string: String) {
self.insert_str_at_cursor(&string); self.insert_str_at_cursor(&string);
@@ -3038,7 +3041,7 @@ impl LineBuf {
} }
Ok(()) Ok(())
} }
#[allow(clippy::unnecessary_to_owned)] #[allow(clippy::unnecessary_to_owned)]
fn verb_dedent(&mut self, motion: MotionKind) -> ShResult<()> { fn verb_dedent(&mut self, motion: MotionKind) -> ShResult<()> {
let Some((start, mut end)) = self.range_from_motion(&motion) else { let Some((start, mut end)) = self.range_from_motion(&motion) else {
return Ok(()); return Ok(());
@@ -3270,27 +3273,27 @@ impl LineBuf {
) -> ShResult<()> { ) -> ShResult<()> {
match verb { match verb {
Verb::Delete | Verb::Yank | Verb::Change => self.verb_ydc(motion, register, verb)?, Verb::Delete | Verb::Yank | Verb::Change => self.verb_ydc(motion, register, verb)?,
Verb::Rot13 => self.verb_rot13(motion)?, Verb::Rot13 => self.verb_rot13(motion)?,
Verb::ReplaceChar(ch) => self.verb_replace_char(motion, ch)?, Verb::ReplaceChar(ch) => self.verb_replace_char(motion, ch)?,
Verb::ReplaceCharInplace(ch, count) => self.verb_replace_char_inplace(ch, count)?, Verb::ReplaceCharInplace(ch, count) => self.verb_replace_char_inplace(ch, count)?,
Verb::ToggleCaseInplace(count) => self.verb_toggle_case_inplace(count), Verb::ToggleCaseInplace(count) => self.verb_toggle_case_inplace(count),
Verb::ToggleCaseRange => self.verb_case_transform(motion, CaseTransform::Toggle)?, Verb::ToggleCaseRange => self.verb_case_transform(motion, CaseTransform::Toggle)?,
Verb::ToLower => self.verb_case_transform(motion, CaseTransform::Lower)?, Verb::ToLower => self.verb_case_transform(motion, CaseTransform::Lower)?,
Verb::ToUpper => self.verb_case_transform(motion, CaseTransform::Upper)?, Verb::ToUpper => self.verb_case_transform(motion, CaseTransform::Upper)?,
Verb::Redo | Verb::Undo => self.verb_undo_redo(verb)?, Verb::Redo | Verb::Undo => self.verb_undo_redo(verb)?,
Verb::RepeatLast => todo!(), Verb::RepeatLast => todo!(),
Verb::Put(anchor) => self.verb_put(anchor, register)?, Verb::Put(anchor) => self.verb_put(anchor, register)?,
Verb::SwapVisualAnchor => self.verb_swap_visual_anchor(), Verb::SwapVisualAnchor => self.verb_swap_visual_anchor(),
Verb::JoinLines => self.verb_join_lines()?, Verb::JoinLines => self.verb_join_lines()?,
Verb::InsertChar(ch) => self.verb_insert_char(ch), Verb::InsertChar(ch) => self.verb_insert_char(ch),
Verb::Insert(string) => self.verb_insert(string), Verb::Insert(string) => self.verb_insert(string),
Verb::Indent => self.verb_indent(motion)?, Verb::Indent => self.verb_indent(motion)?,
Verb::Dedent => self.verb_dedent(motion)?, Verb::Dedent => self.verb_dedent(motion)?,
Verb::Equalize => todo!(), Verb::Equalize => todo!(),
Verb::InsertModeLineBreak(anchor) => self.verb_insert_mode_line_break(anchor)?, Verb::InsertModeLineBreak(anchor) => self.verb_insert_mode_line_break(anchor)?,
Verb::AcceptLineOrNewline => self.verb_accept_line_or_newline()?, Verb::AcceptLineOrNewline => self.verb_accept_line_or_newline()?,
Verb::IncrementNumber(n) => self.verb_adjust_number(n as i64)?, Verb::IncrementNumber(n) => self.verb_adjust_number(n as i64)?,
Verb::DecrementNumber(n) => self.verb_adjust_number(-(n as i64))?, Verb::DecrementNumber(n) => self.verb_adjust_number(-(n as i64))?,
Verb::Complete Verb::Complete
| Verb::ExMode | Verb::ExMode
| Verb::EndOfFile | Verb::EndOfFile
@@ -3349,9 +3352,9 @@ impl LineBuf {
/* /*
* Let's evaluate the motion now * Let's evaluate the motion now
* If we got some weird command like 'dvw' we will * If we got some weird command like 'dvw' we will
* have to simulate a visual selection to get the range * have to simulate a visual selection to get the range
* If motion is None, we will try to use self.select_range * If motion is None, we will try to use self.select_range
* If self.select_range is None, we will use MotionKind::Null * If self.select_range is None, we will use MotionKind::Null
*/ */
let motion_eval = let motion_eval =
if flags.intersects(CmdFlags::VISUAL | CmdFlags::VISUAL_LINE | CmdFlags::VISUAL_BLOCK) { if flags.intersects(CmdFlags::VISUAL | CmdFlags::VISUAL_LINE | CmdFlags::VISUAL_BLOCK) {

View File

@@ -441,11 +441,6 @@ impl ShedVi {
// Process all available keys // Process all available keys
while let Some(key) = self.reader.read_key()? { 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 completer or history search are active, delegate input to it
if self.history.fuzzy_finder.is_active() { if self.history.fuzzy_finder.is_active() {
self.print_line(false)?; self.print_line(false)?;
@@ -628,10 +623,6 @@ impl ShedVi {
pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<Option<ReadlineEvent>> { pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<Option<ReadlineEvent>> {
if self.should_accept_hint(&key) { if self.should_accept_hint(&key) {
log::debug!(
"Accepting hint on key {key:?} in mode {:?}",
self.mode.report_mode()
);
self.editor.accept_hint(); self.editor.accept_hint();
if !self.history.at_pending() { if !self.history.at_pending() {
self.history.reset_to_pending(); self.history.reset_to_pending();
@@ -1257,10 +1248,6 @@ impl ShedVi {
for _ in 0..repeat { for _ in 0..repeat {
let cmds = cmds.clone(); let cmds = cmds.clone();
for (i, cmd) in cmds.iter().enumerate() { 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)?; self.exec_cmd(cmd.clone(), true)?;
// After the first command, start merging so all subsequent // After the first command, start merging so all subsequent
// edits fold into one undo entry (e.g. cw + inserted chars) // 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() { for tk in tokens.into_iter().rev() {
let insertions = annotate_token(tk); let insertions = annotate_token(tk);
for (pos, marker) in insertions { for (pos, marker) in insertions {
log::info!("pos: {pos}, marker: {marker:?}");
log::info!("before: {annotated:?}");
let pos = pos.max(0).min(annotated.len()); let pos = pos.max(0).min(annotated.len());
annotated.insert(pos, marker); annotated.insert(pos, marker);
} }

View File

@@ -294,12 +294,14 @@ impl Read for TermBuffer {
struct KeyCollector { struct KeyCollector {
events: VecDeque<KeyEvent>, events: VecDeque<KeyEvent>,
ss3_pending: bool,
} }
impl KeyCollector { impl KeyCollector {
fn new() -> Self { fn new() -> Self {
Self { Self {
events: VecDeque::new(), events: VecDeque::new(),
ss3_pending: false,
} }
} }
@@ -337,7 +339,55 @@ impl Default for KeyCollector {
impl Perform for KeyCollector { impl Perform for KeyCollector {
fn print(&mut self, c: char) { fn print(&mut self, c: char) {
log::trace!("print: {c:?}");
// vte routes 0x7f (DEL) to print instead of execute // 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' { if c == '\x7f' {
self.push(KeyEvent(KeyCode::Backspace, ModKeys::empty())); self.push(KeyEvent(KeyCode::Backspace, ModKeys::empty()));
} else { } else {
@@ -346,6 +396,7 @@ impl Perform for KeyCollector {
} }
fn execute(&mut self, byte: u8) { fn execute(&mut self, byte: u8) {
log::trace!("execute: {byte:#04x}");
let event = match byte { let event = match byte {
0x00 => KeyEvent(KeyCode::Char(' '), ModKeys::CTRL), // Ctrl+Space / Ctrl+@ 0x00 => KeyEvent(KeyCode::Char(' '), ModKeys::CTRL), // Ctrl+Space / Ctrl+@
0x09 => KeyEvent(KeyCode::Tab, ModKeys::empty()), // Tab (Ctrl+I) 0x09 => KeyEvent(KeyCode::Tab, ModKeys::empty()), // Tab (Ctrl+I)
@@ -370,6 +421,9 @@ impl Perform for KeyCollector {
_ignore: bool, _ignore: bool,
action: char, action: char,
) { ) {
log::trace!(
"CSI dispatch: params={params:?}, intermediates={intermediates:?}, action={action:?}"
);
let params: Vec<u16> = params let params: Vec<u16> = params
.iter() .iter()
.map(|p| p.first().copied().unwrap_or(0)) .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) { fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {
log::trace!("ESC dispatch: intermediates={intermediates:?}, byte={byte:#04x}");
// SS3 sequences // SS3 sequences
if intermediates == [b'O'] { if byte == b'O' {
let key = match byte { self.ss3_pending = true;
b'P' => KeyCode::F(1), return;
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()));
} }
} }
} }

View File

@@ -35,194 +35,249 @@ macro_rules! vi_test {
#[test] #[test]
fn annotate_simple_command() { fn annotate_simple_command() {
assert_annotated("echo hello", assert_annotated("echo hello", "\u{e101}echo\u{e11a} \u{e102}hello\u{e11a}");
"\u{e101}echo\u{e11a} \u{e102}hello\u{e11a}");
} }
#[test] #[test]
fn annotate_pipeline() { fn annotate_pipeline() {
assert_annotated("ls | grep foo", assert_annotated(
"\u{e100}ls\u{e11a} \u{e104}|\u{e11a} \u{e100}grep\u{e11a} \u{e102}foo\u{e11a}"); "ls | grep foo",
"\u{e100}ls\u{e11a} \u{e104}|\u{e11a} \u{e100}grep\u{e11a} \u{e102}foo\u{e11a}",
);
} }
#[test] #[test]
fn annotate_conjunction() { fn annotate_conjunction() {
assert_annotated("echo foo && echo bar", assert_annotated(
"\u{e101}echo\u{e11a} \u{e102}foo\u{e11a} \u{e104}&&\u{e11a} \u{e101}echo\u{e11a} \u{e102}bar\u{e11a}"); "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] #[test]
fn annotate_redirect_output() { fn annotate_redirect_output() {
assert_annotated("echo hello > file.txt", assert_annotated(
"\u{e101}echo\u{e11a} \u{e102}hello\u{e11a} \u{e105}>\u{e11a} \u{e102}file.txt\u{e11a}"); "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] #[test]
fn annotate_redirect_append() { fn annotate_redirect_append() {
assert_annotated("echo hello >> file.txt", assert_annotated(
"\u{e101}echo\u{e11a} \u{e102}hello\u{e11a} \u{e105}>>\u{e11a} \u{e102}file.txt\u{e11a}"); "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] #[test]
fn annotate_redirect_input() { fn annotate_redirect_input() {
assert_annotated("cat < file.txt", assert_annotated(
"\u{e100}cat\u{e11a} \u{e105}<\u{e11a} \u{e102}file.txt\u{e11a}"); "cat < file.txt",
"\u{e100}cat\u{e11a} \u{e105}<\u{e11a} \u{e102}file.txt\u{e11a}",
);
} }
#[test] #[test]
fn annotate_fd_redirect() { fn annotate_fd_redirect() {
assert_annotated("cmd 2>&1", assert_annotated("cmd 2>&1", "\u{e100}cmd\u{e11a} \u{e105}2>&1\u{e11a}");
"\u{e100}cmd\u{e11a} \u{e105}2>&1\u{e11a}");
} }
#[test] #[test]
fn annotate_variable_sub() { fn annotate_variable_sub() {
assert_annotated("echo $HOME", assert_annotated(
"\u{e101}echo\u{e11a} \u{e102}\u{e10c}$HOME\u{e10d}\u{e11a}"); "echo $HOME",
"\u{e101}echo\u{e11a} \u{e102}\u{e10c}$HOME\u{e10d}\u{e11a}",
);
} }
#[test] #[test]
fn annotate_variable_brace_sub() { fn annotate_variable_brace_sub() {
assert_annotated("echo ${HOME}", assert_annotated(
"\u{e101}echo\u{e11a} \u{e102}\u{e10c}${HOME}\u{e10d}\u{e11a}"); "echo ${HOME}",
"\u{e101}echo\u{e11a} \u{e102}\u{e10c}${HOME}\u{e10d}\u{e11a}",
);
} }
#[test] #[test]
fn annotate_command_sub() { fn annotate_command_sub() {
assert_annotated("echo $(ls)", assert_annotated(
"\u{e101}echo\u{e11a} \u{e102}\u{e10e}$(ls)\u{e10f}\u{e11a}"); "echo $(ls)",
"\u{e101}echo\u{e11a} \u{e102}\u{e10e}$(ls)\u{e10f}\u{e11a}",
);
} }
#[test] #[test]
fn annotate_single_quoted_string() { fn annotate_single_quoted_string() {
assert_annotated("echo 'hello world'", assert_annotated(
"\u{e101}echo\u{e11a} \u{e102}\u{e114}'hello world'\u{e115}\u{e11a}"); "echo 'hello world'",
"\u{e101}echo\u{e11a} \u{e102}\u{e114}'hello world'\u{e115}\u{e11a}",
);
} }
#[test] #[test]
fn annotate_double_quoted_string() { fn annotate_double_quoted_string() {
assert_annotated("echo \"hello world\"", assert_annotated(
"\u{e101}echo\u{e11a} \u{e102}\u{e112}\"hello world\"\u{e113}\u{e11a}"); "echo \"hello world\"",
"\u{e101}echo\u{e11a} \u{e102}\u{e112}\"hello world\"\u{e113}\u{e11a}",
);
} }
#[test] #[test]
fn annotate_assignment() { fn annotate_assignment() {
assert_annotated("FOO=bar", assert_annotated("FOO=bar", "\u{e107}FOO=bar\u{e11a}");
"\u{e107}FOO=bar\u{e11a}");
} }
#[test] #[test]
fn annotate_assignment_with_command() { fn annotate_assignment_with_command() {
assert_annotated("FOO=bar echo hello", assert_annotated(
"\u{e107}FOO=bar\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}"); "FOO=bar echo hello",
"\u{e107}FOO=bar\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}",
);
} }
#[test] #[test]
fn annotate_if_statement() { fn annotate_if_statement() {
assert_annotated("if true; then echo yes; fi", assert_annotated(
"\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}"); "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] #[test]
fn annotate_for_loop() { fn annotate_for_loop() {
assert_annotated("for i in a b c; do echo $i; done", assert_annotated(
"\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}"); "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] #[test]
fn annotate_while_loop() { fn annotate_while_loop() {
assert_annotated("while true; do echo hello; done", assert_annotated(
"\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}"); "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] #[test]
fn annotate_case_statement() { fn annotate_case_statement() {
assert_annotated("case foo in bar) echo bar;; esac", assert_annotated(
"\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}"); "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] #[test]
fn annotate_brace_group() { fn annotate_brace_group() {
assert_annotated("{ echo hello; }", assert_annotated(
"\u{e104}{\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}\u{e108}; \u{e11a}\u{e104}}\u{e11a}"); "{ 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] #[test]
fn annotate_comment() { fn annotate_comment() {
assert_annotated("echo hello # this is a comment", assert_annotated(
"\u{e101}echo\u{e11a} \u{e102}hello\u{e11a} \u{e106}# this is a comment\u{e11a}"); "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] #[test]
fn annotate_semicolon_sep() { fn annotate_semicolon_sep() {
assert_annotated("echo foo; echo bar", assert_annotated(
"\u{e101}echo\u{e11a} \u{e102}foo\u{e11a}\u{e108}; \u{e11a}\u{e101}echo\u{e11a} \u{e102}bar\u{e11a}"); "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] #[test]
fn annotate_escaped_char() { fn annotate_escaped_char() {
assert_annotated("echo hello\\ world", assert_annotated(
"\u{e101}echo\u{e11a} \u{e102}hello\\ world\u{e11a}"); "echo hello\\ world",
"\u{e101}echo\u{e11a} \u{e102}hello\\ world\u{e11a}",
);
} }
#[test] #[test]
fn annotate_glob() { fn annotate_glob() {
assert_annotated("ls *.txt", assert_annotated(
"\u{e100}ls\u{e11a} \u{e102}\u{e117}*\u{e11a}.txt\u{e11a}"); "ls *.txt",
"\u{e100}ls\u{e11a} \u{e102}\u{e117}*\u{e11a}.txt\u{e11a}",
);
} }
#[test] #[test]
fn annotate_heredoc_operator() { fn annotate_heredoc_operator() {
assert_annotated("cat <<EOF", assert_annotated(
"\u{e100}cat\u{e11a} \u{e105}<<\u{e11a}\u{e102}EOF\u{e11a}"); "cat <<EOF",
"\u{e100}cat\u{e11a} \u{e105}<<\u{e11a}\u{e102}EOF\u{e11a}",
);
} }
#[test] #[test]
fn annotate_herestring_operator() { fn annotate_herestring_operator() {
assert_annotated("cat <<< hello", assert_annotated(
"\u{e100}cat\u{e11a} \u{e105}<<<\u{e11a} \u{e102}hello\u{e11a}"); "cat <<< hello",
"\u{e100}cat\u{e11a} \u{e105}<<<\u{e11a} \u{e102}hello\u{e11a}",
);
} }
#[test] #[test]
fn annotate_nested_command_sub() { fn annotate_nested_command_sub() {
assert_annotated("echo $(echo $(ls))", assert_annotated(
"\u{e101}echo\u{e11a} \u{e102}\u{e10e}$(echo $(ls))\u{e10f}\u{e11a}"); "echo $(echo $(ls))",
"\u{e101}echo\u{e11a} \u{e102}\u{e10e}$(echo $(ls))\u{e10f}\u{e11a}",
);
} }
#[test] #[test]
fn annotate_var_in_double_quotes() { fn annotate_var_in_double_quotes() {
assert_annotated("echo \"hello $USER\"", assert_annotated(
"\u{e101}echo\u{e11a} \u{e102}\u{e112}\"hello \u{e10c}$USER\u{e10d}\"\u{e113}\u{e11a}"); "echo \"hello $USER\"",
"\u{e101}echo\u{e11a} \u{e102}\u{e112}\"hello \u{e10c}$USER\u{e10d}\"\u{e113}\u{e11a}",
);
} }
#[test] #[test]
fn annotate_func_def() { fn annotate_func_def() {
assert_annotated("foo() { echo hello; }", assert_annotated(
"\u{e103}foo()\u{e11a} \u{e104}{\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}\u{e108}; \u{e11a}\u{e104}}\u{e11a}"); "foo() { echo hello; }",
"\u{e103}foo()\u{e11a} \u{e104}{\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}\u{e108}; \u{e11a}\u{e104}}\u{e11a}",
);
} }
#[test] #[test]
fn annotate_negate() { fn annotate_negate() {
assert_annotated("! echo hello", assert_annotated(
"\u{e104}!\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}"); "! echo hello",
"\u{e104}!\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}",
);
} }
#[test] #[test]
fn annotate_or_conjunction() { fn annotate_or_conjunction() {
assert_annotated("false || echo fallback", assert_annotated(
"\u{e101}false\u{e11a} \u{e104}||\u{e11a} \u{e101}echo\u{e11a} \u{e102}fallback\u{e11a}"); "false || echo fallback",
"\u{e101}false\u{e11a} \u{e104}||\u{e11a} \u{e101}echo\u{e11a} \u{e102}fallback\u{e11a}",
);
} }
#[test] #[test]
fn annotate_complex_pipeline() { fn annotate_complex_pipeline() {
assert_annotated("cat file.txt | grep pattern | wc -l", assert_annotated(
"\u{e100}cat\u{e11a} \u{e102}file.txt\u{e11a} \u{e104}|\u{e11a} \u{e100}grep\u{e11a} \u{e102}pattern\u{e11a} \u{e104}|\u{e11a} \u{e100}wc\u{e11a} \u{e102}-l\u{e11a}"); "cat file.txt | grep pattern | wc -l",
"\u{e100}cat\u{e11a} \u{e102}file.txt\u{e11a} \u{e104}|\u{e11a} \u{e100}grep\u{e11a} \u{e102}pattern\u{e11a} \u{e104}|\u{e11a} \u{e100}wc\u{e11a} \u{e102}-l\u{e11a}",
);
} }
#[test] #[test]
fn annotate_multiple_redirects() { fn annotate_multiple_redirects() {
assert_annotated("cmd > out.txt 2> err.txt", assert_annotated(
"\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}"); "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 ===================== // ===================== Vi Tests =====================
@@ -437,27 +492,27 @@ vi_test! {
#[test] #[test]
fn vi_auto_indent() { 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 // Type each line and press Enter separately so auto-indent triggers
let lines = [ let lines = [
"func() {", "func() {",
"case foo in", "case foo in",
"bar)", "bar)",
"while true; do", "while true; do",
"echo foo \\\rbar \\\rbiz \\\rbazz\rbreak\rdone\r;;\resac\r}" "echo foo \\\rbar \\\rbiz \\\rbazz\rbreak\rdone\r;;\resac\r}",
]; ];
for (i,line) in lines.iter().enumerate() { for (i, line) in lines.iter().enumerate() {
vi.feed_bytes(line.as_bytes()); vi.feed_bytes(line.as_bytes());
if i != lines.len() - 1 { if i != lines.len() - 1 {
vi.feed_bytes(b"\r"); vi.feed_bytes(b"\r");
} }
vi.process_input().unwrap(); vi.process_input().unwrap();
} }
assert_eq!( assert_eq!(
vi.editor.as_str(), 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}" "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}"
); );
} }

View File

@@ -315,7 +315,8 @@ impl ShOptCore {
Ok(Some(output)) Ok(Some(output))
} }
"noclobber" => { "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)); output.push_str(&format!("{}", self.noclobber));
Ok(Some(output)) Ok(Some(output))
} }

View File

@@ -1330,14 +1330,14 @@ impl VarTab {
.get(&ShellParam::Status) .get(&ShellParam::Status)
.map(|s| s.to_string()) .map(|s| s.to_string())
.unwrap_or("0".into()), .unwrap_or("0".into()),
ShellParam::AllArgsStr => { ShellParam::AllArgsStr => {
let ifs = get_separator(); let ifs = get_separator();
self self
.params .params
.get(&ShellParam::AllArgs) .get(&ShellParam::AllArgs)
.map(|s| s.replace(markers::ARG_SEP, &ifs).to_string()) .map(|s| s.replace(markers::ARG_SEP, &ifs).to_string())
.unwrap_or_default() .unwrap_or_default()
} }
_ => self _ => self
.params .params
@@ -1852,12 +1852,12 @@ pub fn change_dir<P: AsRef<Path>>(dir: P) -> ShResult<()> {
} }
pub fn get_separator() -> String { pub fn get_separator() -> String {
env::var("IFS") env::var("IFS")
.unwrap_or(String::from(" ")) .unwrap_or(String::from(" "))
.chars() .chars()
.next() .next()
.unwrap() .unwrap()
.to_string() .to_string()
} }
pub fn get_status() -> i32 { pub fn get_status() -> i32 {