fixed ss3 escape code parsing, added a cursor mode reset that triggers on child exit
This commit is contained in:
@@ -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<()> {
|
||||
|
||||
@@ -1,28 +1,40 @@
|
||||
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
|
||||
takes_arg: false,
|
||||
},
|
||||
OptSpec {
|
||||
opt: Opt::Short('e'),
|
||||
takes_arg: false
|
||||
takes_arg: false,
|
||||
},
|
||||
];
|
||||
|
||||
pub struct LseekOpts {
|
||||
cursor_rel: bool,
|
||||
end_rel: bool
|
||||
end_rel: bool,
|
||||
}
|
||||
|
||||
pub fn seek(node: Node) -> ShResult<()> {
|
||||
let NdRule::Command {
|
||||
assignments: _,
|
||||
argv,
|
||||
} = node.class else { unreachable!() };
|
||||
} = node.class
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let (argv, opts) = get_opts_from_tokens(argv, &LSEEK_OPTS)?;
|
||||
let lseek_opts = get_lseek_opts(opts)?;
|
||||
@@ -36,11 +48,10 @@ pub fn seek(node: Node) -> ShResult<()> {
|
||||
));
|
||||
};
|
||||
let Ok(fd) = fd.0.parse::<u32>() else {
|
||||
return Err(ShErr::at(
|
||||
ShErrKind::ExecFail,
|
||||
fd.1,
|
||||
"Invalid file descriptor",
|
||||
).with_note("file descriptors are integers"));
|
||||
return Err(
|
||||
ShErr::at(ShErrKind::ExecFail, fd.1, "Invalid file descriptor")
|
||||
.with_note("file descriptors are integers"),
|
||||
);
|
||||
};
|
||||
|
||||
let Some(offset) = argv.next() else {
|
||||
@@ -50,11 +61,10 @@ pub fn seek(node: Node) -> ShResult<()> {
|
||||
));
|
||||
};
|
||||
let Ok(offset) = offset.0.parse::<i64>() else {
|
||||
return Err(ShErr::at(
|
||||
ShErrKind::ExecFail,
|
||||
offset.1,
|
||||
"Invalid offset",
|
||||
).with_note("offset can be a positive or negative integer"));
|
||||
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 {
|
||||
@@ -73,7 +83,7 @@ pub fn seek(node: Node) -> ShResult<()> {
|
||||
}
|
||||
Err(e) => {
|
||||
state::set_status(1);
|
||||
return Err(e.into())
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,10 @@ impl Expander {
|
||||
} else {
|
||||
unescape_str(&raw)
|
||||
};
|
||||
Ok(Self { raw: unescaped, flags })
|
||||
Ok(Self {
|
||||
raw: unescaped,
|
||||
flags,
|
||||
})
|
||||
}
|
||||
pub fn expand(&mut self) -> ShResult<Vec<String>> {
|
||||
let mut chars = self.raw.chars().peekable();
|
||||
@@ -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"]);
|
||||
@@ -3712,7 +3715,7 @@ mod tests {
|
||||
let raw = format!("{}hello world{}", markers::DUB_QUOTE, markers::DUB_QUOTE);
|
||||
let mut exp = Expander {
|
||||
raw,
|
||||
flags: TkFlags::empty()
|
||||
flags: TkFlags::empty(),
|
||||
};
|
||||
let words = exp.split_words();
|
||||
assert_eq!(words, vec!["hello world"]);
|
||||
@@ -3727,7 +3730,7 @@ mod tests {
|
||||
let raw = format!("hello{}world", unescape_str("\\ "));
|
||||
let mut exp = Expander {
|
||||
raw,
|
||||
flags: TkFlags::empty()
|
||||
flags: TkFlags::empty(),
|
||||
};
|
||||
let words = exp.split_words();
|
||||
assert_eq!(words, vec!["hello world"]);
|
||||
@@ -3740,7 +3743,7 @@ mod tests {
|
||||
let raw = format!("hello{}world", unescape_str("\\\t"));
|
||||
let mut exp = Expander {
|
||||
raw,
|
||||
flags: TkFlags::empty()
|
||||
flags: TkFlags::empty(),
|
||||
};
|
||||
let words = exp.split_words();
|
||||
assert_eq!(words, vec!["hello\tworld"]);
|
||||
@@ -3756,7 +3759,7 @@ mod tests {
|
||||
let raw = format!("a{}b:c", unescape_str("\\:"));
|
||||
let mut exp = Expander {
|
||||
raw,
|
||||
flags: TkFlags::empty()
|
||||
flags: TkFlags::empty(),
|
||||
};
|
||||
let words = exp.split_words();
|
||||
assert_eq!(words, vec!["a:b", "c"]);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
// 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
|
||||
});
|
||||
|
||||
@@ -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},
|
||||
@@ -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(
|
||||
|
||||
@@ -219,28 +219,27 @@ impl Tk {
|
||||
}
|
||||
|
||||
pub fn is_opener(&self) -> bool {
|
||||
OPENERS.contains(&self.as_str()) ||
|
||||
matches!(self.class, TkRule::BraceGrpStart) ||
|
||||
matches!(self.class, TkRule::CasePattern)
|
||||
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)
|
||||
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()) {
|
||||
|| (matches!(other.class, TkRule::CasePattern) && self.has_double_semi())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
match other.as_str() {
|
||||
"for" |
|
||||
"while" |
|
||||
"until" => matches!(self.as_str(), "done"),
|
||||
"for" | "while" | "until" => matches!(self.as_str(), "done"),
|
||||
"if" => matches!(self.as_str(), "fi"),
|
||||
"case" => matches!(self.as_str(), "esac"),
|
||||
_ => false
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,7 +325,6 @@ pub struct LexStream {
|
||||
flags: LexFlags,
|
||||
}
|
||||
|
||||
|
||||
impl LexStream {
|
||||
pub fn new(source: Arc<String>, flags: LexFlags) -> Self {
|
||||
let flags = flags | LexFlags::FRESH | LexFlags::NEXT_IS_CMD;
|
||||
@@ -416,7 +414,7 @@ impl LexStream {
|
||||
chars.next();
|
||||
pos += 1;
|
||||
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
||||
break
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some('>') = chars.peek() {
|
||||
@@ -618,7 +616,10 @@ impl LexStream {
|
||||
|
||||
// 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 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)
|
||||
@@ -1113,7 +1114,8 @@ 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() {
|
||||
&& let Some(skip) = self.heredoc_skip.take()
|
||||
{
|
||||
self.cursor = skip;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,12 +281,16 @@ bitflags! {
|
||||
pub struct Redir {
|
||||
pub io_mode: IoMode,
|
||||
pub class: RedirType,
|
||||
pub span: Option<Span>
|
||||
pub span: Option<Span>,
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -410,20 +415,19 @@ impl FromStr for RedirBldr {
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => return Err(ShErr::simple(
|
||||
_ => {
|
||||
return Err(ShErr::simple(
|
||||
ShErrKind::ParseErr,
|
||||
format!("Invalid character '{}' in redirection operator", ch),
|
||||
)),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tgt_fd = tgt_fd
|
||||
.parse::<i32>()
|
||||
.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);
|
||||
@@ -449,7 +453,7 @@ impl TryFrom<Tk> for RedirBldr {
|
||||
io_mode: Some(IoMode::buffer(0, tk.to_string(), flags)?),
|
||||
class: Some(RedirType::HereDoc),
|
||||
tgt_fd: Some(0),
|
||||
span: Some(span)
|
||||
span: Some(span),
|
||||
})
|
||||
} else {
|
||||
match Self::from_str(tk.as_str()) {
|
||||
@@ -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 {
|
||||
@@ -1082,7 +1088,9 @@ 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)?;
|
||||
@@ -1106,7 +1114,11 @@ impl ParseStream {
|
||||
context: LabelCtx,
|
||||
) -> ShResult<Redir> {
|
||||
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())
|
||||
@@ -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());
|
||||
@@ -1886,7 +1894,10 @@ pub fn get_redir_file<P: AsRef<Path>>(class: RedirType, path: P) -> ShResult<Fil
|
||||
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()),
|
||||
format!(
|
||||
"shopt core.noclobber is set, refusing to overwrite existing file `{}`",
|
||||
path.display()
|
||||
),
|
||||
));
|
||||
}
|
||||
OpenOptions::new()
|
||||
@@ -1894,22 +1905,18 @@ pub fn get_redir_file<P: AsRef<Path>>(class: RedirType, path: P) -> ShResult<Fil
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(path)
|
||||
},
|
||||
RedirType::ReadWrite => {
|
||||
OpenOptions::new()
|
||||
}
|
||||
RedirType::ReadWrite => OpenOptions::new()
|
||||
.write(true)
|
||||
.read(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.open(path)
|
||||
}
|
||||
RedirType::OutputForce => {
|
||||
OpenOptions::new()
|
||||
.open(path),
|
||||
RedirType::OutputForce => OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(path)
|
||||
}
|
||||
.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() {
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
@@ -253,17 +255,15 @@ impl<'e> IoFrame {
|
||||
close(*tgt_fd).ok();
|
||||
continue;
|
||||
}
|
||||
IoMode::File { .. } => {
|
||||
match io_mode.clone().open_file() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
},
|
||||
IoMode::Buffer { tgt_fd, buf, flags } => {
|
||||
let (rpipe, wpipe) = nix::unistd::pipe()?;
|
||||
let mut text = if flags.contains(TkFlags::LIT_HEREDOC) {
|
||||
@@ -281,7 +281,9 @@ impl<'e> IoFrame {
|
||||
let lines = text.lines();
|
||||
let mut min_tabs = usize::MAX;
|
||||
for line in lines {
|
||||
if line.is_empty() { continue; }
|
||||
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;
|
||||
@@ -293,7 +295,8 @@ impl<'e> IoFrame {
|
||||
}
|
||||
|
||||
if min_tabs > 0 {
|
||||
let stripped = text.lines()
|
||||
let stripped = text
|
||||
.lines()
|
||||
.fold(vec![], |mut acc, ln| {
|
||||
if ln.is_empty() {
|
||||
acc.push("");
|
||||
@@ -308,7 +311,10 @@ impl<'e> IoFrame {
|
||||
}
|
||||
}
|
||||
write(wpipe, text.as_bytes())?;
|
||||
*io_mode = IoMode::Pipe { tgt_fd: *tgt_fd, pipe: rpipe.into() };
|
||||
*io_mode = IoMode::Pipe {
|
||||
tgt_fd: *tgt_fd,
|
||||
pipe: rpipe.into(),
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -354,11 +354,13 @@ impl ClampedUsize {
|
||||
pub struct IndentCtx {
|
||||
depth: usize,
|
||||
ctx: Vec<Tk>,
|
||||
in_escaped_line: bool
|
||||
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
|
||||
@@ -399,7 +401,9 @@ impl IndentCtx {
|
||||
self.in_escaped_line = false;
|
||||
|
||||
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) =
|
||||
LexStream::new(input_arc, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()
|
||||
else {
|
||||
log::error!("Lexing failed during depth calculation: {:?}", input);
|
||||
return 0;
|
||||
};
|
||||
@@ -2988,8 +2992,7 @@ impl LineBuf {
|
||||
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) {
|
||||
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 {
|
||||
|
||||
@@ -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<Option<ReadlineEvent>> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -294,12 +294,14 @@ impl Read for TermBuffer {
|
||||
|
||||
struct KeyCollector {
|
||||
events: VecDeque<KeyEvent>,
|
||||
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<u16> = 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <<EOF",
|
||||
"\u{e100}cat\u{e11a} \u{e105}<<\u{e11a}\u{e102}EOF\u{e11a}");
|
||||
assert_annotated(
|
||||
"cat <<EOF",
|
||||
"\u{e100}cat\u{e11a} \u{e105}<<\u{e11a}\u{e102}EOF\u{e11a}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn annotate_herestring_operator() {
|
||||
assert_annotated("cat <<< hello",
|
||||
"\u{e100}cat\u{e11a} \u{e105}<<<\u{e11a} \u{e102}hello\u{e11a}");
|
||||
assert_annotated(
|
||||
"cat <<< hello",
|
||||
"\u{e100}cat\u{e11a} \u{e105}<<<\u{e11a} \u{e102}hello\u{e11a}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn annotate_nested_command_sub() {
|
||||
assert_annotated("echo $(echo $(ls))",
|
||||
"\u{e101}echo\u{e11a} \u{e102}\u{e10e}$(echo $(ls))\u{e10f}\u{e11a}");
|
||||
assert_annotated(
|
||||
"echo $(echo $(ls))",
|
||||
"\u{e101}echo\u{e11a} \u{e102}\u{e10e}$(echo $(ls))\u{e10f}\u{e11a}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn annotate_var_in_double_quotes() {
|
||||
assert_annotated("echo \"hello $USER\"",
|
||||
"\u{e101}echo\u{e11a} \u{e102}\u{e112}\"hello \u{e10c}$USER\u{e10d}\"\u{e113}\u{e11a}");
|
||||
assert_annotated(
|
||||
"echo \"hello $USER\"",
|
||||
"\u{e101}echo\u{e11a} \u{e102}\u{e112}\"hello \u{e10c}$USER\u{e10d}\"\u{e113}\u{e11a}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn annotate_func_def() {
|
||||
assert_annotated("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}");
|
||||
assert_annotated(
|
||||
"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]
|
||||
fn annotate_negate() {
|
||||
assert_annotated("! echo hello",
|
||||
"\u{e104}!\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}");
|
||||
assert_annotated(
|
||||
"! echo hello",
|
||||
"\u{e104}!\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn annotate_or_conjunction() {
|
||||
assert_annotated("false || echo fallback",
|
||||
"\u{e101}false\u{e11a} \u{e104}||\u{e11a} \u{e101}echo\u{e11a} \u{e102}fallback\u{e11a}");
|
||||
assert_annotated(
|
||||
"false || echo fallback",
|
||||
"\u{e101}false\u{e11a} \u{e104}||\u{e11a} \u{e101}echo\u{e11a} \u{e102}fallback\u{e11a}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn annotate_complex_pipeline() {
|
||||
assert_annotated("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}");
|
||||
assert_annotated(
|
||||
"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]
|
||||
fn annotate_multiple_redirects() {
|
||||
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}");
|
||||
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 =====================
|
||||
@@ -445,7 +500,7 @@ fn vi_auto_indent() {
|
||||
"case foo in",
|
||||
"bar)",
|
||||
"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() {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user