Compare commits

...

5 Commits

Author SHA1 Message Date
101d8434f8 fixed heredocs using the same expansion pathway as regular strings
implemented backtick command subs

deferred heredoc expansion until redir time instead of parse time

implemented "$*" expansions

function defs like 'func   ()  { }' now parse correctly

fixed conjunctions short circuiting instead of skipping
2026-03-15 10:49:24 -04:00
9bd9c66b92 implemented '<>' redirects, and the 'seek' builtin
'seek' is a wrapper around the lseek() syscall

added noclobber to core shopts and implemented '>|' redirection syntax

properly implemented fd close syntax

fixed saved fds being leaked into exec'd programs
2026-03-14 20:04:20 -04:00
5173e1908d heredocs and herestrings implemented
added more tests to the test suite
2026-03-14 13:40:00 -04:00
1f9c96f24e more improvements to auto indent depth tracking
added test cases for the auto indent/dedent feature
2026-03-14 01:14:30 -04:00
09024728f6 Add token-aware depth calculator for indentation, improve brace group error handling, and clean up warnings 2026-03-13 20:57:04 -04:00
22 changed files with 1991 additions and 531 deletions

View File

@@ -8,7 +8,7 @@ A Linux shell written in Rust. The name is a nod to the original Unix utilities
### Line Editor ### Line Editor
`shed` includes a built-in `vim` emulator as its line editor, written from scratch. It aims to provide a more precise vim-like editing experience at the shell prompt. `shed` includes a built-in `vim` emulator as its line editor, written from scratch. It aims to provide a more precise vim-like editing experience at the shell prompt than conventional `vi` mode implementations.
- **Normal mode** - motions (`w`, `b`, `e`, `f`, `t`, `%`, `0`, `$`, etc.), verbs (`d`, `c`, `y`, `p`, `r`, `x`, `~`, etc.), text objects (`iw`, `aw`, `i"`, `a{`, `is`, etc.), registers, `.` repeat, `;`/`,` repeat, and counts - **Normal mode** - motions (`w`, `b`, `e`, `f`, `t`, `%`, `0`, `$`, etc.), verbs (`d`, `c`, `y`, `p`, `r`, `x`, `~`, etc.), text objects (`iw`, `aw`, `i"`, `a{`, `is`, etc.), registers, `.` repeat, `;`/`,` repeat, and counts
- **Insert mode** - insert, append, replace, with Ctrl+W word deletion and undo/redo - **Insert mode** - insert, append, replace, with Ctrl+W word deletion and undo/redo

View File

@@ -24,13 +24,14 @@ 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; 49] = [ 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", "getopts", "keymap", "read_key", "autocmd", "ulimit", "umask", "seek"
]; ];
pub fn true_builtin() -> ShResult<()> { pub fn true_builtin() -> ShResult<()> {

253
src/builtin/seek.rs Normal file
View File

@@ -0,0 +1,253 @@
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};
pub const LSEEK_OPTS: [OptSpec;2] = [
OptSpec {
opt: Opt::Short('c'),
takes_arg: false
},
OptSpec {
opt: Opt::Short('e'),
takes_arg: false
},
];
pub struct LseekOpts {
cursor_rel: bool,
end_rel: bool
}
pub fn seek(node: Node) -> ShResult<()> {
let NdRule::Command {
assignments: _,
argv,
} = node.class else { unreachable!() };
let (argv, opts) = get_opts_from_tokens(argv, &LSEEK_OPTS)?;
let lseek_opts = get_lseek_opts(opts)?;
let mut argv = prepare_argv(argv)?.into_iter();
argv.next(); // drop 'seek'
let Some(fd) = argv.next() else {
return Err(ShErr::simple(
ShErrKind::ExecFail,
"lseek: Missing required argument 'fd'",
));
};
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"));
};
let Some(offset) = argv.next() else {
return Err(ShErr::simple(
ShErrKind::ExecFail,
"lseek: Missing required argument 'offset'",
));
};
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"));
};
let whence = if lseek_opts.cursor_rel {
Whence::SeekCur
} else if lseek_opts.end_rel {
Whence::SeekEnd
} else {
Whence::SeekSet
};
match lseek(fd as i32, offset, whence) {
Ok(new_offset) => {
let stdout = borrow_fd(STDOUT_FILENO);
let buf = new_offset.to_string() + "\n";
write(stdout, buf.as_bytes())?;
}
Err(e) => {
state::set_status(1);
return Err(e.into())
}
}
state::set_status(0);
Ok(())
}
pub fn get_lseek_opts(opts: Vec<Opt>) -> ShResult<LseekOpts> {
let mut lseek_opts = LseekOpts {
cursor_rel: false,
end_rel: false,
};
for opt in opts {
match opt {
Opt::Short('c') => lseek_opts.cursor_rel = true,
Opt::Short('e') => lseek_opts.end_rel = true,
_ => {
return Err(ShErr::simple(
ShErrKind::ExecFail,
format!("lseek: Unexpected flag '{opt}'"),
));
}
}
}
Ok(lseek_opts)
}
#[cfg(test)]
mod tests {
use crate::testutil::{TestGuard, test_input};
use pretty_assertions::assert_eq;
#[test]
fn seek_set_beginning() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("seek.txt");
std::fs::write(&path, "hello world\n").unwrap();
let g = TestGuard::new();
test_input(format!("exec 9<> {}", path.display())).unwrap();
test_input("seek 9 0").unwrap();
let out = g.read_output();
assert_eq!(out, "0\n");
}
#[test]
fn seek_set_offset() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("seek.txt");
std::fs::write(&path, "hello world\n").unwrap();
let g = TestGuard::new();
test_input(format!("exec 9<> {}", path.display())).unwrap();
test_input("seek 9 6").unwrap();
let out = g.read_output();
assert_eq!(out, "6\n");
}
#[test]
fn seek_then_read() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("seek.txt");
std::fs::write(&path, "hello world\n").unwrap();
let g = TestGuard::new();
test_input(format!("exec 9<> {}", path.display())).unwrap();
test_input("seek 9 6").unwrap();
// Clear the seek output
g.read_output();
test_input("read line <&9").unwrap();
let val = crate::state::read_vars(|v| v.get_var("line"));
assert_eq!(val, "world");
}
#[test]
fn seek_cur_relative() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("seek.txt");
std::fs::write(&path, "abcdefghij\n").unwrap();
let g = TestGuard::new();
test_input(format!("exec 9<> {}", path.display())).unwrap();
test_input("seek 9 3").unwrap();
test_input("seek -c 9 4").unwrap();
let out = g.read_output();
assert_eq!(out, "3\n7\n");
}
#[test]
fn seek_end() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("seek.txt");
std::fs::write(&path, "hello\n").unwrap(); // 6 bytes
let g = TestGuard::new();
test_input(format!("exec 9<> {}", path.display())).unwrap();
test_input("seek -e 9 0").unwrap();
let out = g.read_output();
assert_eq!(out, "6\n");
}
#[test]
fn seek_end_negative() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("seek.txt");
std::fs::write(&path, "hello\n").unwrap(); // 6 bytes
let g = TestGuard::new();
test_input(format!("exec 9<> {}", path.display())).unwrap();
test_input("seek -e 9 -2").unwrap();
let out = g.read_output();
assert_eq!(out, "4\n");
}
#[test]
fn seek_write_overwrite() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("seek.txt");
std::fs::write(&path, "hello world\n").unwrap();
let _g = TestGuard::new();
test_input(format!("exec 9<> {}", path.display())).unwrap();
test_input("seek 9 6").unwrap();
test_input("echo -n 'WORLD' >&9").unwrap();
let contents = std::fs::read_to_string(&path).unwrap();
assert_eq!(contents, "hello WORLD\n");
}
#[test]
fn seek_rewind_full_read() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("seek.txt");
std::fs::write(&path, "abc\n").unwrap();
let g = TestGuard::new();
test_input(format!("exec 9<> {}", path.display())).unwrap();
// Read moves cursor to EOF
test_input("read line <&9").unwrap();
// Rewind
test_input("seek 9 0").unwrap();
// Clear output from seek
g.read_output();
// Read again from beginning
test_input("read line <&9").unwrap();
let val = crate::state::read_vars(|v| v.get_var("line"));
assert_eq!(val, "abc");
}
#[test]
fn seek_bad_fd() {
let _g = TestGuard::new();
let result = test_input("seek 99 0");
assert!(result.is_err());
}
#[test]
fn seek_missing_args() {
let _g = TestGuard::new();
let result = test_input("seek");
assert!(result.is_err());
let result = test_input("seek 9");
assert!(result.is_err());
}
}

View File

@@ -40,18 +40,23 @@ impl Tk {
} }
pub struct Expander { pub struct Expander {
flags: TkFlags,
raw: String, raw: String,
} }
impl Expander { impl Expander {
pub fn new(raw: Tk) -> ShResult<Self> { pub fn new(raw: Tk) -> ShResult<Self> {
let raw = raw.span.as_str(); let tk_raw = raw.span.as_str();
Self::from_raw(raw) Self::from_raw(tk_raw, raw.flags)
} }
pub fn from_raw(raw: &str) -> 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 = unescape_str(&raw); let unescaped = if flags.contains(TkFlags::IS_HEREDOC) {
Ok(Self { raw: unescaped }) unescape_heredoc(&raw)
} else {
unescape_str(&raw)
};
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();
@@ -75,7 +80,11 @@ impl Expander {
self.raw.insert_str(0, "./"); self.raw.insert_str(0, "./");
} }
Ok(self.split_words()) if self.flags.contains(TkFlags::IS_HEREDOC) {
Ok(vec![self.raw.clone()])
} else {
Ok(self.split_words())
}
} }
pub fn split_words(&mut self) -> Vec<String> { pub fn split_words(&mut self) -> Vec<String> {
let mut words = vec![]; let mut words = vec![];
@@ -1154,6 +1163,25 @@ pub fn unescape_str(raw: &str) -> String {
} }
} }
} }
'`' => {
result.push(markers::VAR_SUB);
result.push(markers::SUBSH);
while let Some(bt_ch) = chars.next() {
match bt_ch {
'\\' => {
result.push(bt_ch);
if let Some(next_ch) = chars.next() {
result.push(next_ch);
}
}
'`' => {
result.push(markers::SUBSH);
break;
}
_ => result.push(bt_ch),
}
}
}
'"' => { '"' => {
result.push(markers::DUB_QUOTE); result.push(markers::DUB_QUOTE);
break; break;
@@ -1318,6 +1346,25 @@ pub fn unescape_str(raw: &str) -> String {
result.push('$'); result.push('$');
} }
} }
'`' => {
result.push(markers::VAR_SUB);
result.push(markers::SUBSH);
while let Some(bt_ch) = chars.next() {
match bt_ch {
'\\' => {
result.push(bt_ch);
if let Some(next_ch) = chars.next() {
result.push(next_ch);
}
}
'`' => {
result.push(markers::SUBSH);
break;
}
_ => result.push(bt_ch),
}
}
}
_ => result.push(ch), _ => result.push(ch),
} }
first_char = false; first_char = false;
@@ -1326,6 +1373,96 @@ pub fn unescape_str(raw: &str) -> String {
result result
} }
/// Like unescape_str but for heredoc bodies. Only processes:
/// - $var / ${var} / $(cmd) substitution markers
/// - Backslash escapes (only before $, `, \, and newline)
/// Everything else (quotes, tildes, globs, process subs, etc.) is literal.
pub fn unescape_heredoc(raw: &str) -> String {
let mut chars = raw.chars().peekable();
let mut result = String::new();
while let Some(ch) = chars.next() {
match ch {
'\\' => {
match chars.peek() {
Some('$') | Some('`') | Some('\\') | Some('\n') => {
let next_ch = chars.next().unwrap();
if next_ch == '\n' {
// line continuation — discard both backslash and newline
continue;
}
result.push(markers::ESCAPE);
result.push(next_ch);
}
_ => {
// backslash is literal
result.push('\\');
}
}
}
'$' if chars.peek() == Some(&'(') => {
result.push(markers::VAR_SUB);
chars.next(); // consume '('
result.push(markers::SUBSH);
let mut paren_count = 1;
while let Some(subsh_ch) = chars.next() {
match subsh_ch {
'\\' => {
result.push(subsh_ch);
if let Some(next_ch) = chars.next() {
result.push(next_ch);
}
}
'(' => {
paren_count += 1;
result.push(subsh_ch);
}
')' => {
paren_count -= 1;
if paren_count == 0 {
result.push(markers::SUBSH);
break;
} else {
result.push(subsh_ch);
}
}
_ => result.push(subsh_ch),
}
}
}
'$' => {
result.push(markers::VAR_SUB);
if chars.peek() == Some(&'$') {
chars.next();
result.push('$');
}
}
'`' => {
result.push(markers::VAR_SUB);
result.push(markers::SUBSH);
while let Some(bt_ch) = chars.next() {
match bt_ch {
'\\' => {
result.push(bt_ch);
if let Some(next_ch) = chars.next() {
result.push(next_ch);
}
}
'`' => {
result.push(markers::SUBSH);
break;
}
_ => result.push(bt_ch),
}
}
}
_ => result.push(ch),
}
}
result
}
/// 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
/// Used for completion results, and glob filename matches. /// Used for completion results, and glob filename matches.
pub fn escape_str(raw: &str, use_marker: bool) -> String { pub fn escape_str(raw: &str, use_marker: bool) -> String {
@@ -3532,6 +3669,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()
}; };
let words = exp.split_words(); let words = exp.split_words();
assert_eq!(words, vec!["hello", "world", "foo"]); assert_eq!(words, vec!["hello", "world", "foo"]);
@@ -3546,6 +3684,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()
}; };
let words = exp.split_words(); let words = exp.split_words();
assert_eq!(words, vec!["a", "b", "c"]); assert_eq!(words, vec!["a", "b", "c"]);
@@ -3560,6 +3699,7 @@ mod tests {
let mut exp = Expander { let mut exp = Expander {
raw: "hello world".to_string(), raw: "hello world".to_string(),
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"]);
@@ -3570,7 +3710,10 @@ mod tests {
let _guard = TestGuard::new(); let _guard = TestGuard::new();
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 { raw }; let mut exp = Expander {
raw,
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"]);
} }
@@ -3582,7 +3725,10 @@ mod tests {
let _guard = TestGuard::new(); let _guard = TestGuard::new();
let raw = format!("hello{}world", unescape_str("\\ ")); let raw = format!("hello{}world", unescape_str("\\ "));
let mut exp = Expander { raw }; let mut exp = Expander {
raw,
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"]);
} }
@@ -3592,7 +3738,10 @@ mod tests {
let _guard = TestGuard::new(); let _guard = TestGuard::new();
let raw = format!("hello{}world", unescape_str("\\\t")); let raw = format!("hello{}world", unescape_str("\\\t"));
let mut exp = Expander { raw }; let mut exp = Expander {
raw,
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"]);
} }
@@ -3605,7 +3754,10 @@ mod tests {
} }
let raw = format!("a{}b:c", unescape_str("\\:")); let raw = format!("a{}b:c", unescape_str("\\:"));
let mut exp = Expander { raw }; let mut exp = Expander {
raw,
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

@@ -95,14 +95,16 @@ pub fn sort_tks(
.into_iter() .into_iter()
.map(|t| t.expand()) .map(|t| t.expand())
.collect::<ShResult<Vec<_>>>()? .collect::<ShResult<Vec<_>>>()?
.into_iter(); .into_iter()
.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.extend(tokens_iter); non_opts.push(token);
break; non_opts.extend(tokens_iter);
break;
} }
let parsed_opts = Opt::parse(&token.to_string()); let parsed_opts = Opt::parse(&token.to_string());

View File

@@ -201,6 +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
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;
@@ -208,7 +209,9 @@ impl ShErr {
let first = self.notes[0].clone(); let first = self.notes[0].clone();
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 {
self.notes = vec![];
}
self.labeled(span, first) self.labeled(span, first)
} }

View File

@@ -147,11 +147,9 @@ impl RawModeGuard {
let orig = ORIG_TERMIOS let orig = ORIG_TERMIOS
.with(|cell| cell.borrow().clone()) .with(|cell| cell.borrow().clone())
.expect("with_cooked_mode called before raw_mode()"); .expect("with_cooked_mode called before raw_mode()");
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &orig) tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &orig).ok();
.expect("Failed to restore cooked mode");
let res = f(); let res = f();
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &current) tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &current).ok();
.expect("Failed to restore raw mode");
res res
} }
} }

View File

@@ -2,6 +2,14 @@ use std::sync::LazyLock;
use crate::prelude::*; use crate::prelude::*;
/// Minimum fd number for shell-internal file descriptors.
const MIN_INTERNAL_FD: RawFd = 10;
pub static TTY_FILENO: LazyLock<RawFd> = LazyLock::new(|| { pub static TTY_FILENO: LazyLock<RawFd> = LazyLock::new(|| {
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
// `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");
close(fd).ok();
high
}); });

View File

@@ -31,7 +31,6 @@ use nix::unistd::read;
use crate::builtin::keymap::KeyMapMatch; use crate::builtin::keymap::KeyMapMatch;
use crate::builtin::trap::TrapTarget; use crate::builtin::trap::TrapTarget;
use crate::libsh::error::{self, ShErr, ShErrKind, ShResult}; use crate::libsh::error::{self, ShErr, ShErrKind, ShResult};
use crate::libsh::guards::scope_guard;
use crate::libsh::sys::TTY_FILENO; use crate::libsh::sys::TTY_FILENO;
use crate::libsh::utils::AutoCmdVecUtils; use crate::libsh::utils::AutoCmdVecUtils;
use crate::parse::execute::{exec_dash_c, exec_input}; use crate::parse::execute::{exec_dash_c, exec_input};

View File

@@ -8,28 +8,7 @@ use ariadne::Fmt;
use crate::{ use crate::{
builtin::{ builtin::{
alias::{alias, unalias}, 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}
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},
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},
@@ -340,24 +319,19 @@ impl Dispatcher {
}; };
let mut elem_iter = elements.into_iter(); let mut elem_iter = elements.into_iter();
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;
self.dispatch_node(*cmd)?; if !skip {
self.dispatch_node(*cmd)?;
}
let status = state::get_status(); let status = state::get_status();
match operator { skip = match operator {
ConjunctOp::And => { ConjunctOp::And => status != 0,
if status != 0 { ConjunctOp::Or => status == 0,
break;
}
}
ConjunctOp::Or => {
if status == 0 {
break;
}
}
ConjunctOp::Null => break, ConjunctOp::Null => break,
} };
} }
Ok(()) Ok(())
} }
@@ -377,7 +351,7 @@ 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(); 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(
@@ -803,7 +777,7 @@ impl Dispatcher {
self.job_stack.new_job(); self.job_stack.new_job();
if cmds.len() == 1 { if cmds.len() == 1 {
self.fg_job = !is_bg && self.interactive; self.fg_job = !is_bg && self.interactive;
let mut cmd = cmds.into_iter().next().unwrap(); let cmd = cmds.into_iter().next().unwrap();
if is_bg && !matches!(cmd.class, NdRule::Command { .. }) { if is_bg && !matches!(cmd.class, NdRule::Command { .. }) {
self.run_fork( self.run_fork(
&cmd.get_command().map(|t| t.to_string()).unwrap_or_default(), &cmd.get_command().map(|t| t.to_string()).unwrap_or_default(),
@@ -888,7 +862,10 @@ 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" {
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();
@@ -1013,6 +990,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),
"true" | ":" => { "true" | ":" => {
state::set_status(0); state::set_status(0);
Ok(()) Ok(())

View File

@@ -217,6 +217,32 @@ impl Tk {
}; };
self.span.as_str().trim() == ";;" self.span.as_str().trim() == ";;"
} }
pub fn is_opener(&self) -> bool {
OPENERS.contains(&self.as_str()) ||
matches!(self.class, TkRule::BraceGrpStart) ||
matches!(self.class, TkRule::CasePattern)
}
pub fn is_closer(&self) -> bool {
matches!(self.as_str(), "fi" | "done" | "esac") ||
self.has_double_semi() ||
matches!(self.class, TkRule::BraceGrpEnd)
}
pub fn is_closer_for(&self, other: &Tk) -> bool {
if (matches!(other.class, TkRule::BraceGrpStart) && matches!(self.class, TkRule::BraceGrpEnd))
|| (matches!(other.class, TkRule::CasePattern) && self.has_double_semi()) {
return true;
}
match other.as_str() {
"for" |
"while" |
"until" => matches!(self.as_str(), "done"),
"if" => matches!(self.as_str(), "fi"),
"case" => matches!(self.as_str(), "esac"),
_ => false
}
}
} }
impl Display for Tk { impl Display for Tk {
@@ -241,20 +267,12 @@ 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 LIT_HEREDOC = 0b0000010000000000;
const TAB_HEREDOC = 0b0000100000000000;
} }
} }
pub struct LexStream {
source: Arc<String>,
pub cursor: usize,
pub name: String,
quote_state: QuoteState,
brc_grp_depth: usize,
brc_grp_start: Option<usize>,
case_depth: usize,
flags: LexFlags,
}
bitflags! { bitflags! {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct LexFlags: u32 { pub struct LexFlags: u32 {
@@ -296,6 +314,19 @@ pub fn clean_input(input: &str) -> String {
output output
} }
pub struct LexStream {
source: Arc<String>,
pub cursor: usize,
pub name: String,
quote_state: QuoteState,
brc_grp_depth: usize,
brc_grp_start: Option<usize>,
case_depth: usize,
heredoc_skip: Option<usize>,
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;
@@ -307,6 +338,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,
case_depth: 0, case_depth: 0,
} }
} }
@@ -367,7 +399,7 @@ impl LexStream {
} }
pub fn read_redir(&mut self) -> Option<ShResult<Tk>> { pub fn read_redir(&mut self) -> Option<ShResult<Tk>> {
assert!(self.cursor <= self.source.len()); assert!(self.cursor <= self.source.len());
let slice = self.slice(self.cursor..)?; let slice = self.slice(self.cursor..)?.to_string();
let mut pos = self.cursor; let mut pos = self.cursor;
let mut chars = slice.chars().peekable(); let mut chars = slice.chars().peekable();
let mut tk = Tk::default(); let mut tk = Tk::default();
@@ -379,37 +411,51 @@ 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() {
// noclobber force '>|'
chars.next();
pos += 1;
tk = self.get_token(self.cursor..pos, TkRule::Redir);
break
}
if let Some('>') = chars.peek() { if let Some('>') = chars.peek() {
chars.next(); chars.next();
pos += 1; pos += 1;
} }
if let Some('&') = chars.peek() { let Some('&') = chars.peek() else {
chars.next();
pos += 1;
let mut found_fd = false;
while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) {
chars.next();
found_fd = true;
pos += 1;
}
if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
let span_start = self.cursor;
self.cursor = pos;
return Some(Err(ShErr::at(
ShErrKind::ParseErr,
Span::new(span_start..pos, self.source.clone()),
"Invalid redirection",
)));
} else {
tk = self.get_token(self.cursor..pos, TkRule::Redir);
break;
}
} else {
tk = self.get_token(self.cursor..pos, TkRule::Redir); tk = self.get_token(self.cursor..pos, TkRule::Redir);
break; break;
} };
chars.next();
pos += 1;
let mut found_fd = false;
if chars.peek().is_some_and(|ch| *ch == '-') {
chars.next();
found_fd = true;
pos += 1;
} else {
while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) {
chars.next();
found_fd = true;
pos += 1;
}
}
if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
let span_start = self.cursor;
self.cursor = pos;
return Some(Err(ShErr::at(
ShErrKind::ParseErr,
Span::new(span_start..pos, self.source.clone()),
"Invalid redirection",
)));
} else {
tk = self.get_token(self.cursor..pos, TkRule::Redir);
break;
}
} }
'<' => { '<' => {
if chars.peek() == Some(&'(') { if chars.peek() == Some(&'(') {
@@ -417,14 +463,94 @@ impl LexStream {
} }
pos += 1; pos += 1;
for _ in 0..2 { match chars.peek() {
if let Some('<') = chars.peek() { Some('<') => {
chars.next(); chars.next();
pos += 1; pos += 1;
} else {
break; match chars.peek() {
} Some('<') => {
} chars.next();
pos += 1;
}
Some(ch) => {
let mut ch = *ch;
while is_field_sep(ch) {
let Some(next_ch) = chars.next() else {
// Incomplete input — fall through to emit << as Redir
break;
};
pos += next_ch.len_utf8();
ch = next_ch;
}
if is_field_sep(ch) {
// Ran out of input while skipping whitespace — fall through
} else {
let saved_cursor = self.cursor;
match self.read_heredoc(pos) {
Ok(Some(heredoc_tk)) => {
// cursor is set to after the delimiter word;
// heredoc_skip is set to after the body
pos = self.cursor;
self.cursor = saved_cursor;
tk = heredoc_tk;
break;
}
Ok(None) => {
// Incomplete heredoc — restore cursor and fall through
self.cursor = saved_cursor;
}
Err(e) => return Some(Err(e)),
}
}
}
_ => {
// No delimiter yet — input is incomplete
// Fall through to emit the << as a Redir token
}
}
}
Some('>') => {
chars.next();
pos += 1;
tk = self.get_token(self.cursor..pos, TkRule::Redir);
break;
}
Some('&') => {
chars.next();
pos += 1;
let mut found_fd = false;
if chars.peek().is_some_and(|ch| *ch == '-') {
chars.next();
found_fd = true;
pos += 1;
} else {
while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) {
chars.next();
found_fd = true;
pos += 1;
}
}
if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
let span_start = self.cursor;
self.cursor = pos;
return Some(Err(ShErr::at(
ShErrKind::ParseErr,
Span::new(span_start..pos, self.source.clone()),
"Invalid redirection",
)));
} else {
tk = self.get_token(self.cursor..pos, TkRule::Redir);
break;
}
}
_ => {}
}
tk = self.get_token(self.cursor..pos, TkRule::Redir); tk = self.get_token(self.cursor..pos, TkRule::Redir);
break; break;
} }
@@ -448,6 +574,130 @@ 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>> {
let slice = self.slice(pos..).unwrap_or_default().to_string();
let mut chars = slice.chars();
let mut delim = String::new();
let mut flags = TkFlags::empty();
let mut first_char = true;
// Parse the delimiter word, stripping quotes
while let Some(ch) = chars.next() {
match ch {
'-' if first_char => {
pos += 1;
flags |= TkFlags::TAB_HEREDOC;
}
'\"' => {
pos += 1;
self.quote_state.toggle_double();
flags |= TkFlags::LIT_HEREDOC;
}
'\'' => {
pos += 1;
self.quote_state.toggle_single();
flags |= TkFlags::LIT_HEREDOC;
}
_ if self.quote_state.in_quote() => {
pos += ch.len_utf8();
delim.push(ch);
}
ch if is_hard_sep(ch) => {
break;
}
ch => {
pos += ch.len_utf8();
delim.push(ch);
}
}
first_char = false;
}
// pos is now right after the delimiter word — this is where
// the cursor should return so the rest of the line gets lexed
let cursor_after_delim = pos;
// Re-slice from cursor_after_delim so iterator and pos are in sync
// (the old chars iterator consumed the hard_sep without advancing pos)
let rest = self.slice(cursor_after_delim..).unwrap_or_default().to_string();
let mut chars = rest.chars();
// Scan forward to the newline (or use heredoc_skip from a previous heredoc)
let body_start = if let Some(skip) = self.heredoc_skip {
// A previous heredoc on this line already read its body;
// our body starts where that one ended
let skip_offset = skip - cursor_after_delim;
for _ in 0..skip_offset {
chars.next();
}
skip
} else {
// Skip the rest of the current line to find where the body begins
let mut scan = pos;
let mut found_newline = false;
while let Some(ch) = chars.next() {
scan += ch.len_utf8();
if ch == '\n' {
found_newline = true;
break;
}
}
if !found_newline {
if self.flags.contains(LexFlags::LEX_UNFINISHED) {
return Ok(None);
} else {
return Err(ShErr::at(
ShErrKind::ParseErr,
Span::new(pos..pos, self.source.clone()),
"Heredoc delimiter not found",
));
}
}
scan
};
pos = body_start;
let start = pos;
// Read lines until we find one that matches the delimiter exactly
let mut line = String::new();
let mut line_start = pos;
while let Some(ch) = chars.next() {
pos += ch.len_utf8();
if ch == '\n' {
let trimmed = line.trim_end_matches('\r');
if trimmed == delim {
let mut tk = self.get_token(start..line_start, TkRule::Redir);
tk.flags |= TkFlags::IS_HEREDOC | flags;
self.heredoc_skip = Some(pos);
self.cursor = cursor_after_delim;
return Ok(Some(tk));
}
line.clear();
line_start = pos;
} else {
line.push(ch);
}
}
// Check the last line (no trailing newline)
let trimmed = line.trim_end_matches('\r');
if trimmed == delim {
let mut tk = self.get_token(start..line_start, TkRule::Redir);
tk.flags |= TkFlags::IS_HEREDOC | flags;
self.heredoc_skip = Some(pos);
self.cursor = cursor_after_delim;
return Ok(Some(tk));
}
if !self.flags.contains(LexFlags::LEX_UNFINISHED) {
Err(ShErr::at(
ShErrKind::ParseErr,
Span::new(start..pos, self.source.clone()),
format!("Heredoc delimiter '{}' not found", delim),
))
} else {
Ok(None)
}
}
pub fn read_string(&mut self) -> ShResult<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();
@@ -625,6 +875,16 @@ impl LexStream {
)); ));
} }
} }
'(' if can_be_subshell && chars.peek() == Some(&')') => {
// standalone "()" — function definition marker
pos += 2;
chars.next();
let mut tk = self.get_token(self.cursor..pos, TkRule::Str);
tk.mark(TkFlags::KEYWORD);
self.cursor = pos;
self.set_next_is_cmd(true);
return Ok(tk);
}
'(' if self.next_is_cmd() && can_be_subshell => { '(' if self.next_is_cmd() && can_be_subshell => {
pos += 1; pos += 1;
let mut paren_count = 1; let mut paren_count = 1;
@@ -845,10 +1105,18 @@ impl Iterator for LexStream {
let token = match get_char(&self.source, self.cursor).unwrap() { let token = match get_char(&self.source, self.cursor).unwrap() {
'\r' | '\n' | ';' => { '\r' | '\n' | ';' => {
let ch = get_char(&self.source, self.cursor).unwrap();
let ch_idx = self.cursor; let ch_idx = self.cursor;
self.cursor += 1; self.cursor += 1;
self.set_next_is_cmd(true); self.set_next_is_cmd(true);
// If a heredoc was parsed on this line, skip past the body
// Only on newline — ';' is a command separator within the same line
if (ch == '\n' || ch == '\r')
&& let Some(skip) = self.heredoc_skip.take() {
self.cursor = skip;
}
while let Some(ch) = get_char(&self.source, self.cursor) { while let Some(ch) = get_char(&self.source, self.cursor) {
match ch { match ch {
'\\' if get_char(&self.source, self.cursor + 1) == Some('\n') => { '\\' if get_char(&self.source, self.cursor + 1) == Some('\n') => {

File diff suppressed because one or more lines are too long

View File

@@ -19,7 +19,7 @@ pub use std::os::unix::io::{AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd,
pub use bitflags::bitflags; pub use bitflags::bitflags;
pub use nix::{ pub use nix::{
errno::Errno, errno::Errno,
fcntl::{OFlag, open}, fcntl::{FcntlArg, OFlag, fcntl, open},
libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}, libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
sys::{ sys::{
signal::{self, SigHandler, SigSet, SigmaskHow, Signal, kill, killpg, pthread_sigmask, signal}, signal::{self, SigHandler, SigSet, SigmaskHow, Signal, kill, killpg, pthread_sigmask, signal},

View File

@@ -8,15 +8,26 @@ use crate::{
expand::Expander, expand::Expander,
libsh::{ libsh::{
error::{ShErr, ShErrKind, ShResult}, error::{ShErr, ShErrKind, ShResult},
sys::TTY_FILENO,
utils::RedirVecUtils, utils::RedirVecUtils,
}, },
parse::{Redir, RedirType, get_redir_file}, parse::{Redir, RedirType, get_redir_file, lex::TkFlags},
prelude::*, 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
// module https://fishshell.com/ // module https://fishshell.com/
/// Minimum fd number for shell-internal file descriptors.
/// User-visible fds (0-9) are kept clear so `exec 3>&-` etc. work as expected.
const MIN_INTERNAL_FD: RawFd = 10;
/// Like `dup()`, but places the new fd at `MIN_INTERNAL_FD` or above so it
/// doesn't collide with user-managed fds.
fn dup_high(fd: RawFd) -> nix::Result<RawFd> {
fcntl(fd, FcntlArg::F_DUPFD_CLOEXEC(MIN_INTERNAL_FD))
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum IoMode { pub enum IoMode {
Fd { Fd {
@@ -37,8 +48,9 @@ pub enum IoMode {
pipe: Arc<OwnedFd>, pipe: Arc<OwnedFd>,
}, },
Buffer { Buffer {
tgt_fd: RawFd,
buf: String, buf: String,
pipe: Arc<OwnedFd>, flags: TkFlags, // so we can see if its a heredoc or not
}, },
Close { Close {
tgt_fd: RawFd, tgt_fd: RawFd,
@@ -79,19 +91,28 @@ 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)?.expand()?.join(" "); // should just be one string, will have to find some way to handle a return of 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
// multiple
let expanded_pathbuf = PathBuf::from(expanded_path); let expanded_pathbuf = PathBuf::from(expanded_path);
let file = get_redir_file(mode, expanded_pathbuf)?; let file = get_redir_file(mode, expanded_pathbuf)?;
// Move the opened fd above the user-accessible range so it never
// 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)?;
drop(file); // closes the original low fd
self = IoMode::OpenedFile { self = IoMode::OpenedFile {
tgt_fd, tgt_fd,
file: Arc::new(OwnedFd::from(file)), file: Arc::new(unsafe { OwnedFd::from_raw_fd(high) }),
} }
} }
Ok(self) Ok(self)
} }
pub fn buffer(tgt_fd: RawFd, buf: String, flags: TkFlags) -> ShResult<Self> {
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();
( (
@@ -206,24 +227,103 @@ impl<'e> IoFrame {
) )
} }
pub fn save(&'e mut self) { pub fn save(&'e mut self) {
let saved_in = dup(STDIN_FILENO).unwrap(); let saved_in = dup_high(STDIN_FILENO).unwrap();
let saved_out = dup(STDOUT_FILENO).unwrap(); let saved_out = dup_high(STDOUT_FILENO).unwrap();
let saved_err = dup(STDERR_FILENO).unwrap(); let saved_err = dup_high(STDERR_FILENO).unwrap();
self.saved_io = Some(IoGroup(saved_in, saved_out, saved_err)); self.saved_io = Some(IoGroup(saved_in, saved_out, saved_err));
} }
pub fn redirect(mut self) -> ShResult<RedirGuard> { pub fn redirect(mut self) -> ShResult<RedirGuard> {
self.save(); self.save();
for redir in &mut self.redirs { if let Err(e) = self.apply_redirs() {
let io_mode = &mut redir.io_mode; // Restore saved fds before propagating the error so they don't leak.
if let IoMode::File { .. } = io_mode { self.restore().ok();
*io_mode = io_mode.clone().open_file()?; return Err(e);
};
let tgt_fd = io_mode.tgt_fd();
let src_fd = io_mode.src_fd();
dup2(src_fd, tgt_fd)?;
} }
Ok(RedirGuard::new(self)) Ok(RedirGuard::new(self))
} }
fn apply_redirs(&mut self) -> ShResult<()> {
for redir in &mut self.redirs {
let io_mode = &mut redir.io_mode;
match io_mode {
IoMode::Close { tgt_fd } => {
if *tgt_fd == *TTY_FILENO {
// Don't let user close the shell's tty fd.
continue;
}
close(*tgt_fd).ok();
continue;
}
IoMode::File { .. } => {
match io_mode.clone().open_file() {
Ok(file) => *io_mode = file,
Err(e) => {
if let Some(span) = redir.span.as_ref() {
return Err(e.promote(span.clone()));
}
return Err(e)
}
}
}
IoMode::Buffer { tgt_fd, buf, flags } => {
let (rpipe, wpipe) = nix::unistd::pipe()?;
let mut text = if flags.contains(TkFlags::LIT_HEREDOC) {
buf.clone()
} else {
let words = Expander::from_raw(buf, *flags)?.expand()?;
if flags.contains(TkFlags::IS_HEREDOC) {
words.into_iter().next().unwrap_or_default()
} else {
let ifs = state::get_separator();
words.join(&ifs).trim().to_string() + "\n"
}
};
if flags.contains(TkFlags::TAB_HEREDOC) {
let lines = text.lines();
let mut min_tabs = usize::MAX;
for line in lines {
if line.is_empty() { continue; }
let line_len = line.len();
let after_strip = line.trim_start_matches('\t').len();
let delta = line_len - after_strip;
min_tabs = min_tabs.min(delta);
}
if min_tabs == usize::MAX {
// let's avoid possibly allocating a string with 18 quintillion tabs
min_tabs = 0;
}
if min_tabs > 0 {
let stripped = text.lines()
.fold(vec![], |mut acc, ln| {
if ln.is_empty() {
acc.push("");
return acc;
}
let stripped_ln = ln.strip_prefix(&"\t".repeat(min_tabs)).unwrap();
acc.push(stripped_ln);
acc
})
.join("\n");
text = stripped + "\n";
}
}
write(wpipe, text.as_bytes())?;
*io_mode = IoMode::Pipe { tgt_fd: *tgt_fd, pipe: rpipe.into() };
}
_ => {}
}
let tgt_fd = io_mode.tgt_fd();
let src_fd = io_mode.src_fd();
if let Err(e) = dup2(src_fd, tgt_fd) {
if let Some(span) = redir.span.as_ref() {
return Err(ShErr::from(e).promote(span.clone()));
} else {
return Err(e.into());
}
}
}
Ok(())
}
pub fn restore(&mut self) -> ShResult<()> { pub fn restore(&mut self) -> ShResult<()> {
if let Some(saved) = self.saved_io.take() { if let Some(saved) = self.saved_io.take() {
dup2(saved.0, STDIN_FILENO)?; dup2(saved.0, STDIN_FILENO)?;
@@ -334,6 +434,8 @@ pub fn borrow_fd<'f>(fd: i32) -> BorrowedFd<'f> {
} }
type PipeFrames = Map<PipeGenerator, fn((Option<Redir>, Option<Redir>)) -> IoFrame>; type PipeFrames = Map<PipeGenerator, fn((Option<Redir>, Option<Redir>)) -> IoFrame>;
/// An iterator that lazily creates a specific number of pipes.
pub struct PipeGenerator { pub struct PipeGenerator {
num_cmds: usize, num_cmds: usize,
cursor: usize, cursor: usize,

View File

@@ -1,7 +1,7 @@
use std::{ use std::{
collections::HashSet, collections::HashSet,
fmt::{Debug, Write}, fmt::{Debug, Write},
path::{Path, PathBuf}, path::PathBuf,
sync::Arc, sync::Arc,
}; };

View File

@@ -15,7 +15,7 @@ use crate::{
libsh::{error::ShResult, guards::var_ctx_guard}, libsh::{error::ShResult, guards::var_ctx_guard},
parse::{ parse::{
execute::exec_input, execute::exec_input,
lex::{LexFlags, LexStream, QuoteState, Tk, TkFlags, TkRule}, lex::{LexFlags, LexStream, QuoteState, Tk, TkRule},
}, },
prelude::*, prelude::*,
readline::{ readline::{
@@ -350,6 +350,73 @@ impl ClampedUsize {
} }
} }
#[derive(Default, Clone, Debug)]
pub struct IndentCtx {
depth: usize,
ctx: Vec<Tk>,
in_escaped_line: bool
}
impl IndentCtx {
pub fn new() -> Self { Self::default() }
pub fn depth(&self) -> usize {
self.depth
}
pub fn ctx(&self) -> &[Tk] {
&self.ctx
}
pub fn descend(&mut self, tk: Tk) {
self.ctx.push(tk);
self.depth += 1;
}
pub fn ascend(&mut self) {
self.depth = self.depth.saturating_sub(1);
self.ctx.pop();
}
pub fn reset(&mut self) {
std::mem::take(self);
}
pub fn check_tk(&mut self, tk: Tk) {
if tk.is_opener() {
self.descend(tk);
} else if self.ctx.last().is_some_and(|t| tk.is_closer_for(t)) {
self.ascend();
} else if matches!(tk.class, TkRule::Sep) && self.in_escaped_line {
self.in_escaped_line = false;
self.depth = self.depth.saturating_sub(1);
}
}
pub fn calculate(&mut self, input: &str) -> usize {
self.depth = 0;
self.ctx.clear();
self.in_escaped_line = false;
let input_arc = Arc::new(input.to_string());
let Ok(tokens) = LexStream::new(input_arc, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>() else {
log::error!("Lexing failed during depth calculation: {:?}", input);
return 0;
};
for tk in tokens {
self.check_tk(tk);
}
if input.ends_with("\\\n") {
self.in_escaped_line = true;
self.depth += 1;
}
self.depth
}
}
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
pub struct LineBuf { pub struct LineBuf {
pub buffer: String, pub buffer: String,
@@ -363,7 +430,7 @@ pub struct LineBuf {
pub insert_mode_start_pos: Option<usize>, pub insert_mode_start_pos: Option<usize>,
pub saved_col: Option<usize>, pub saved_col: Option<usize>,
pub auto_indent_level: usize, pub indent_ctx: IndentCtx,
pub undo_stack: Vec<Edit>, pub undo_stack: Vec<Edit>,
pub redo_stack: Vec<Edit>, pub redo_stack: Vec<Edit>,
@@ -617,6 +684,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 {
let Some(to_cursor) = self.slice_to_cursor() else {
return false;
};
// count the number of backslashes
let delta = to_cursor.len() - to_cursor.trim_end_matches('\\').len();
// an even number of backslashes means each one is escaped
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))
} }
@@ -829,7 +907,7 @@ impl LineBuf {
} }
Some(self.line_bounds(line_no)) Some(self.line_bounds(line_no))
} }
pub fn word_at(&mut self, pos: usize, word: Word) -> (usize, usize) { pub fn word_at(&mut self, _pos: usize, word: Word) -> (usize, usize) {
let start = if self.is_word_bound(self.cursor.get(), word, Direction::Backward) { let start = if self.is_word_bound(self.cursor.get(), word, Direction::Backward) {
self.cursor.get() self.cursor.get()
} else { } else {
@@ -2031,51 +2109,13 @@ impl LineBuf {
let end = start + (new.len().max(gr.len())); let end = start + (new.len().max(gr.len()));
self.buffer.replace_range(start..end, new); self.buffer.replace_range(start..end, new);
} }
pub fn calc_indent_level(&mut self) { pub fn calc_indent_level(&mut self) -> usize {
// FIXME: This implementation is extremely naive but it kind of sort of works for now
// Need to re-implement it and write tests
let to_cursor = self let to_cursor = self
.slice_to_cursor() .slice_to_cursor()
.map(|s| s.to_string()) .map(|s| s.to_string())
.unwrap_or(self.buffer.clone()); .unwrap_or(self.buffer.clone());
let mut level: usize = 0; self.indent_ctx.calculate(&to_cursor)
if to_cursor.ends_with("\\\n") {
level += 1; // Line continuation, so we need to add an extra level
}
let input = Arc::new(to_cursor);
let Ok(tokens) = LexStream::new(input, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()
else {
log::error!("Failed to lex buffer for indent calculation");
return;
};
let mut last_keyword: Option<String> = None;
for tk in tokens {
if tk.flags.contains(TkFlags::KEYWORD) {
match tk.as_str() {
"in" => {
if last_keyword.as_deref() == Some("case") {
level += 1;
} else {
// 'in' is also used in for loops, but we already increment level on 'do' for those
// so we just skip it here
}
}
"then" | "do" => level += 1,
"done" | "fi" | "esac" => level = level.saturating_sub(1),
_ => { /* Continue */ }
}
last_keyword = Some(tk.to_string());
} else if tk.class == TkRule::BraceGrpStart {
level += 1;
} else if tk.class == TkRule::BraceGrpEnd {
level = level.saturating_sub(1);
}
}
self.auto_indent_level = level;
} }
pub fn eval_motion(&mut self, verb: Option<&Verb>, motion: MotionCmd) -> MotionKind { pub fn eval_motion(&mut self, verb: Option<&Verb>, motion: MotionCmd) -> MotionKind {
let buffer = self.buffer.clone(); let buffer = self.buffer.clone();
@@ -2652,8 +2692,8 @@ impl LineBuf {
register.write_to_register(register_content); register.write_to_register(register_content);
self.cursor.set(start); self.cursor.set(start);
if do_indent { if do_indent {
self.calc_indent_level(); let depth = self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t'); let tabs = (0..depth).map(|_| '\t');
for tab in tabs { for tab in tabs {
self.insert_at_cursor(tab); self.insert_at_cursor(tab);
self.cursor.add(1); self.cursor.add(1);
@@ -2888,17 +2928,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;
for i in start..end { let mut last_was_escape = false;
let mut i = start;
while i < end {
let Some(gr) = self.grapheme_at(i) else { let Some(gr) = self.grapheme_at(i) else {
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;
} else { } else {
self.force_replace_at(i, " "); self.force_replace_at(i, " ");
} }
if last_was_escape {
// if we are here, then we just joined an escaped newline
// semantically, echo foo\\nbar == echo foo bar
// so a joined line should remove the escape.
self.remove(i - 1);
end -= 1;
}
last_was_whitespace = false; last_was_whitespace = 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 {
@@ -2906,46 +2958,57 @@ 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;
} }
self.cursor.set(i); self.cursor.set(i);
i += 1;
continue; continue;
} } else if gr == "\\" {
last_was_whitespace = is_whitespace(gr); if last_was_whitespace && last_was_escape {
// if we are here, then the pattern of the last three chars was this:
// ' \\', a space and two backslashes.
// This means the "last" was an escaped backslash, not whitespace.
last_was_whitespace = false;
}
last_was_escape = !last_was_escape;
} else {
last_was_whitespace = is_whitespace(gr);
last_was_escape = false;
}
i += 1;
} }
Ok(()) Ok(())
} }
fn verb_insert_char(&mut self, ch: char) { fn verb_insert_char(&mut self, ch: char) {
self.insert_at_cursor(ch); self.insert_at_cursor(ch);
self.cursor.add(1); self.cursor.add(1);
let before = self.auto_indent_level; let before_escaped = self.indent_ctx.in_escaped_line;
if read_shopts(|o| o.prompt.auto_indent) let before = self.indent_ctx.depth();
&& let Some(line_content) = self.this_line_content() if read_shopts(|o| o.prompt.auto_indent) {
{ let after = self.calc_indent_level();
match line_content.trim() { // Only dedent if the depth decrease came from a closer, not from
"esac" | "done" | "fi" | "}" => { // a line continuation bonus going away
self.calc_indent_level(); if after < before
if self.auto_indent_level < before { && !(before_escaped && !self.indent_ctx.in_escaped_line) {
let delta = before - self.auto_indent_level; 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);
let graphemes = string.graphemes(true).count(); let graphemes = string.graphemes(true).count();
self.cursor.add(graphemes); self.cursor.add(graphemes);
} }
#[allow(clippy::unnecessary_to_owned)]
fn verb_indent(&mut self, motion: MotionKind) -> ShResult<()> { fn verb_indent(&mut self, motion: MotionKind) -> ShResult<()> {
let Some((start, end)) = self.range_from_motion(&motion) else { let Some((start, end)) = self.range_from_motion(&motion) else {
return Ok(()); return Ok(());
@@ -2975,6 +3038,7 @@ impl LineBuf {
} }
Ok(()) Ok(())
} }
#[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(());
@@ -3017,8 +3081,8 @@ impl LineBuf {
Anchor::After => { Anchor::After => {
self.push('\n'); self.push('\n');
if auto_indent { if auto_indent {
self.calc_indent_level(); let depth = self.calc_indent_level();
for _ in 0..self.auto_indent_level { for _ in 0..depth {
self.push('\t'); self.push('\t');
} }
} }
@@ -3027,8 +3091,8 @@ impl LineBuf {
} }
Anchor::Before => { Anchor::Before => {
if auto_indent { if auto_indent {
self.calc_indent_level(); let depth = self.calc_indent_level();
for _ in 0..self.auto_indent_level { for _ in 0..depth {
self.insert_at(0, '\t'); self.insert_at(0, '\t');
} }
} }
@@ -3055,8 +3119,8 @@ impl LineBuf {
self.insert_at_cursor('\n'); self.insert_at_cursor('\n');
self.cursor.add(1); self.cursor.add(1);
if auto_indent { if auto_indent {
self.calc_indent_level(); let depth = self.calc_indent_level();
for _ in 0..self.auto_indent_level { for _ in 0..depth {
self.insert_at_cursor('\t'); self.insert_at_cursor('\t');
self.cursor.add(1); self.cursor.add(1);
} }
@@ -3198,7 +3262,6 @@ impl LineBuf {
} }
Ok(()) Ok(())
} }
#[allow(clippy::unnecessary_to_owned)]
pub fn exec_verb( pub fn exec_verb(
&mut self, &mut self,
verb: Verb, verb: Verb,
@@ -3285,10 +3348,10 @@ impl LineBuf {
/* /*
* Let's evaluate the motion now * Let's evaluate the motion now
* If we got some weird command like 'dvw' we will have to simulate a visual * If we got some weird command like 'dvw' we will
* selection to get the range If motion is None, we will try to use * have to simulate a visual selection to get the range
* self.select_range If self.select_range is None, we will use * If motion is None, we will try to use self.select_range
* 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

@@ -253,7 +253,6 @@ pub struct ShedVi {
pub repeat_action: Option<CmdReplay>, pub repeat_action: Option<CmdReplay>,
pub repeat_motion: Option<MotionCmd>, pub repeat_motion: Option<MotionCmd>,
pub editor: LineBuf, pub editor: LineBuf,
pub next_is_escaped: bool,
pub old_layout: Option<Layout>, pub old_layout: Option<Layout>,
pub history: History, pub history: History,
@@ -271,7 +270,6 @@ impl ShedVi {
completer: Box::new(FuzzyCompleter::default()), completer: Box::new(FuzzyCompleter::default()),
highlighter: Highlighter::new(), highlighter: Highlighter::new(),
mode: Box::new(ViInsert::new()), mode: Box::new(ViInsert::new()),
next_is_escaped: false,
saved_mode: None, saved_mode: None,
pending_keymap: Vec::new(), pending_keymap: Vec::new(),
old_layout: None, old_layout: None,
@@ -303,7 +301,6 @@ impl ShedVi {
completer: Box::new(FuzzyCompleter::default()), completer: Box::new(FuzzyCompleter::default()),
highlighter: Highlighter::new(), highlighter: Highlighter::new(),
mode: Box::new(ViInsert::new()), mode: Box::new(ViInsert::new()),
next_is_escaped: false,
saved_mode: None, saved_mode: None,
pending_keymap: Vec::new(), pending_keymap: Vec::new(),
old_layout: None, old_layout: None,
@@ -417,7 +414,7 @@ impl ShedVi {
LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<_>>>(); LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<_>>>();
let lex_result2 = let lex_result2 =
LexStream::new(Arc::clone(&input), LexFlags::empty()).collect::<ShResult<Vec<_>>>(); LexStream::new(Arc::clone(&input), LexFlags::empty()).collect::<ShResult<Vec<_>>>();
let is_top_level = self.editor.auto_indent_level == 0; let is_top_level = self.editor.indent_ctx.ctx().is_empty();
let is_complete = match (lex_result1.is_err(), lex_result2.is_err()) { let is_complete = match (lex_result1.is_err(), lex_result2.is_err()) {
(true, true) => { (true, true) => {
@@ -808,14 +805,6 @@ impl ShedVi {
} }
} }
if let KeyEvent(KeyCode::Char('\\'), ModKeys::NONE) = key
&& !self.next_is_escaped
{
self.next_is_escaped = true;
} else {
self.next_is_escaped = false;
}
let Ok(cmd) = self.mode.handle_key_fallible(key) else { let Ok(cmd) = self.mode.handle_key_fallible(key) else {
// it's an ex mode error // it's an ex mode error
self.mode = Box::new(ViNormal::new()) as Box<dyn ViMode>; self.mode = Box::new(ViNormal::new()) as Box<dyn ViMode>;
@@ -834,8 +823,7 @@ impl ShedVi {
} }
if cmd.is_submit_action() if cmd.is_submit_action()
&& !self.next_is_escaped && !self.editor.cursor_is_escaped()
&& !self.editor.buffer.ends_with('\\')
&& (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete)) && (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete))
{ {
if self.editor.attempt_history_expansion(&self.history) { if self.editor.attempt_history_expansion(&self.history) {
@@ -1442,6 +1430,8 @@ 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);
} }
@@ -1623,6 +1613,12 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
let mut insertions: Vec<(usize, Marker)> = vec![]; let mut insertions: Vec<(usize, Marker)> = vec![];
// Heredoc tokens have spans covering the body content far from the <<
// operator, which breaks position tracking after marker insertions
if token.flags.contains(TkFlags::IS_HEREDOC) {
return insertions;
}
if token.class != TkRule::Str if token.class != TkRule::Str
&& let Some(marker) = marker_for(&token.class) && let Some(marker) = marker_for(&token.class)
{ {

View File

@@ -481,13 +481,19 @@ 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) {
// SS3 sequences (ESC O P/Q/R/S for F1-F4) // SS3 sequences
if intermediates == [b'O'] { if intermediates == [b'O'] {
let key = match byte { let key = match byte {
b'P' => KeyCode::F(1), b'P' => KeyCode::F(1),
b'Q' => KeyCode::F(2), b'Q' => KeyCode::F(2),
b'R' => KeyCode::F(3), b'R' => KeyCode::F(3),
b'S' => KeyCode::F(4), 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, _ => return,
}; };
self.push(KeyEvent(key, ModKeys::empty())); self.push(KeyEvent(key, ModKeys::empty()));

View File

@@ -2,10 +2,15 @@
use std::os::fd::AsRawFd; use std::os::fd::AsRawFd;
use crate::{ use crate::{
readline::{Prompt, ShedVi}, readline::{Prompt, ShedVi, annotate_input},
testutil::TestGuard, testutil::TestGuard,
}; };
fn assert_annotated(input: &str, expected: &str) {
let result = annotate_input(input);
assert_eq!(result, expected, "\nInput: {input:?}");
}
/// Tests for our vim logic emulation. Each test consists of an initial text, a sequence of keys to feed, and the expected final text and cursor position. /// Tests for our vim logic emulation. Each test consists of an initial text, a sequence of keys to feed, and the expected final text and cursor position.
macro_rules! vi_test { macro_rules! vi_test {
{ $($name:ident: $input:expr => $op:expr => $expected_text:expr,$expected_cursor:expr);* } => { { $($name:ident: $input:expr => $op:expr => $expected_text:expr,$expected_cursor:expr);* } => {
@@ -26,6 +31,202 @@ macro_rules! vi_test {
}; };
} }
// ===================== Annotation Tests =====================
#[test]
fn annotate_simple_command() {
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}");
}
#[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}");
}
#[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}");
}
#[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}");
}
#[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}");
}
#[test]
fn annotate_fd_redirect() {
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}");
}
#[test]
fn annotate_variable_brace_sub() {
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}");
}
#[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}");
}
#[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}");
}
#[test]
fn annotate_assignment() {
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}");
}
#[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}");
}
#[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}");
}
#[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}");
}
#[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}");
}
#[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}");
}
#[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}");
}
#[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}");
}
#[test]
fn annotate_escaped_char() {
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}");
}
#[test]
fn annotate_heredoc_operator() {
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}");
}
#[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}");
}
#[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}");
}
#[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}");
}
#[test]
fn annotate_negate() {
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}");
}
#[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}");
}
#[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}");
}
// ===================== Vi Tests =====================
fn test_vi(initial: &str) -> (ShedVi, TestGuard) { fn test_vi(initial: &str) -> (ShedVi, TestGuard) {
let g = TestGuard::new(); let g = TestGuard::new();
let prompt = Prompt::default(); let prompt = Prompt::default();
@@ -38,198 +239,225 @@ fn test_vi(initial: &str) -> (ShedVi, TestGuard) {
// Why can't I marry a programming language // Why can't I marry a programming language
vi_test! { vi_test! {
vi_dw_basic : "hello world" => "dw" => "world", 0; vi_dw_basic : "hello world" => "dw" => "world", 0;
vi_dw_middle : "one two three" => "wdw" => "one three", 4; vi_dw_middle : "one two three" => "wdw" => "one three", 4;
vi_dd_whole_line : "hello world" => "dd" => "", 0; vi_dd_whole_line : "hello world" => "dd" => "", 0;
vi_x_single : "hello" => "x" => "ello", 0; vi_x_single : "hello" => "x" => "ello", 0;
vi_x_middle : "hello" => "llx" => "helo", 2; vi_x_middle : "hello" => "llx" => "helo", 2;
vi_X_backdelete : "hello" => "llX" => "hllo", 1; vi_X_backdelete : "hello" => "llX" => "hllo", 1;
vi_h_motion : "hello" => "$h" => "hello", 3; vi_h_motion : "hello" => "$h" => "hello", 3;
vi_l_motion : "hello" => "l" => "hello", 1; vi_l_motion : "hello" => "l" => "hello", 1;
vi_h_at_start : "hello" => "h" => "hello", 0; vi_h_at_start : "hello" => "h" => "hello", 0;
vi_l_at_end : "hello" => "$l" => "hello", 4; vi_l_at_end : "hello" => "$l" => "hello", 4;
vi_w_forward : "one two three" => "w" => "one two three", 4; vi_w_forward : "one two three" => "w" => "one two three", 4;
vi_b_backward : "one two three" => "$b" => "one two three", 8; vi_b_backward : "one two three" => "$b" => "one two three", 8;
vi_e_end : "one two three" => "e" => "one two three", 2; vi_e_end : "one two three" => "e" => "one two three", 2;
vi_ge_back_end : "one two three" => "$ge" => "one two three", 6; vi_ge_back_end : "one two three" => "$ge" => "one two three", 6;
vi_w_punctuation : "foo.bar baz" => "w" => "foo.bar baz", 3; vi_w_punctuation : "foo.bar baz" => "w" => "foo.bar baz", 3;
vi_e_punctuation : "foo.bar baz" => "e" => "foo.bar baz", 2; vi_e_punctuation : "foo.bar baz" => "e" => "foo.bar baz", 2;
vi_b_punctuation : "foo.bar baz" => "$b" => "foo.bar baz", 8; vi_b_punctuation : "foo.bar baz" => "$b" => "foo.bar baz", 8;
vi_w_at_eol : "hello" => "$w" => "hello", 4; vi_w_at_eol : "hello" => "$w" => "hello", 4;
vi_b_at_bol : "hello" => "b" => "hello", 0; vi_b_at_bol : "hello" => "b" => "hello", 0;
vi_W_forward : "foo.bar baz" => "W" => "foo.bar baz", 8; vi_W_forward : "foo.bar baz" => "W" => "foo.bar baz", 8;
vi_B_backward : "foo.bar baz" => "$B" => "foo.bar baz", 8; vi_B_backward : "foo.bar baz" => "$B" => "foo.bar baz", 8;
vi_E_end : "foo.bar baz" => "E" => "foo.bar baz", 6; vi_E_end : "foo.bar baz" => "E" => "foo.bar baz", 6;
vi_gE_back_end : "one two three" => "$gE" => "one two three", 6; vi_gE_back_end : "one two three" => "$gE" => "one two three", 6;
vi_W_skip_punct : "one-two three" => "W" => "one-two three", 8; vi_W_skip_punct : "one-two three" => "W" => "one-two three", 8;
vi_B_skip_punct : "one two-three" => "$B" => "one two-three", 4; vi_B_skip_punct : "one two-three" => "$B" => "one two-three", 4;
vi_E_skip_punct : "one-two three" => "E" => "one-two three", 6; vi_E_skip_punct : "one-two three" => "E" => "one-two three", 6;
vi_dW_big : "foo.bar baz" => "dW" => "baz", 0; vi_dW_big : "foo.bar baz" => "dW" => "baz", 0;
vi_cW_big : "foo.bar baz" => "cWx\x1b" => "x baz", 0; vi_cW_big : "foo.bar baz" => "cWx\x1b" => "x baz", 0;
vi_zero_bol : " hello" => "$0" => " hello", 0; vi_zero_bol : " hello" => "$0" => " hello", 0;
vi_caret_first_char : " hello" => "$^" => " hello", 2; vi_caret_first_char : " hello" => "$^" => " hello", 2;
vi_dollar_eol : "hello world" => "$" => "hello world", 10; vi_dollar_eol : "hello world" => "$" => "hello world", 10;
vi_g_last_nonws : "hello " => "g_" => "hello ", 4; vi_g_last_nonws : "hello " => "g_" => "hello ", 4;
vi_g_no_trailing : "hello" => "g_" => "hello", 4; vi_g_no_trailing : "hello" => "g_" => "hello", 4;
vi_pipe_column : "hello world" => "6|" => "hello world", 5; vi_pipe_column : "hello world" => "6|" => "hello world", 5;
vi_pipe_col1 : "hello world" => "1|" => "hello world", 0; vi_pipe_col1 : "hello world" => "1|" => "hello world", 0;
vi_I_insert_front : " hello" => "Iworld \x1b" => " world hello", 7; vi_I_insert_front : " hello" => "Iworld \x1b" => " world hello", 7;
vi_A_append_end : "hello" => "A world\x1b" => "hello world", 10; vi_A_append_end : "hello" => "A world\x1b" => "hello world", 10;
vi_f_find : "hello world" => "fo" => "hello world", 4; vi_f_find : "hello world" => "fo" => "hello world", 4;
vi_F_find_back : "hello world" => "$Fo" => "hello world", 7; vi_F_find_back : "hello world" => "$Fo" => "hello world", 7;
vi_t_till : "hello world" => "tw" => "hello world", 5; vi_t_till : "hello world" => "tw" => "hello world", 5;
vi_T_till_back : "hello world" => "$To" => "hello world", 8; vi_T_till_back : "hello world" => "$To" => "hello world", 8;
vi_f_no_match : "hello" => "fz" => "hello", 0; vi_f_no_match : "hello" => "fz" => "hello", 0;
vi_semicolon_repeat : "abcabc" => "fa;;" => "abcabc", 3; vi_semicolon_repeat : "abcabc" => "fa;;" => "abcabc", 3;
vi_comma_reverse : "abcabc" => "fa;;," => "abcabc", 0; vi_comma_reverse : "abcabc" => "fa;;," => "abcabc", 0;
vi_df_semicolon : "abcabc" => "fa;;dfa" => "abcabc", 3; vi_df_semicolon : "abcabc" => "fa;;dfa" => "abcabc", 3;
vi_t_at_target : "aab" => "lta" => "aab", 1; vi_t_at_target : "aab" => "lta" => "aab", 1;
vi_D_to_end : "hello world" => "wD" => "hello ", 5; vi_D_to_end : "hello world" => "wD" => "hello ", 5;
vi_d_dollar : "hello world" => "wd$" => "hello ", 5; vi_d_dollar : "hello world" => "wd$" => "hello ", 5;
vi_d0_to_start : "hello world" => "$d0" => "d", 0; vi_d0_to_start : "hello world" => "$d0" => "d", 0;
vi_dw_multiple : "one two three" => "d2w" => "three", 0; vi_dw_multiple : "one two three" => "d2w" => "three", 0;
vi_dt_char : "hello world" => "dtw" => "world", 0; vi_dt_char : "hello world" => "dtw" => "world", 0;
vi_df_char : "hello world" => "dfw" => "orld", 0; vi_df_char : "hello world" => "dfw" => "orld", 0;
vi_dh_back : "hello" => "lldh" => "hllo", 1; vi_dh_back : "hello" => "lldh" => "hllo", 1;
vi_dl_forward : "hello" => "dl" => "ello", 0; vi_dl_forward : "hello" => "dl" => "ello", 0;
vi_dge_back_end : "one two three" => "$dge" => "one tw", 5; vi_dge_back_end : "one two three" => "$dge" => "one tw", 5;
vi_dG_to_end : "hello world" => "dG" => "", 0; vi_dG_to_end : "hello world" => "dG" => "", 0;
vi_dgg_to_start : "hello world" => "$dgg" => "", 0; vi_dgg_to_start : "hello world" => "$dgg" => "", 0;
vi_d_semicolon : "abcabc" => "fad;" => "abcabc", 3; vi_d_semicolon : "abcabc" => "fad;" => "abcabc", 3;
vi_cw_basic : "hello world" => "cwfoo\x1b" => "foo world", 2; vi_cw_basic : "hello world" => "cwfoo\x1b" => "foo world", 2;
vi_C_to_end : "hello world" => "wCfoo\x1b" => "hello foo", 8; vi_C_to_end : "hello world" => "wCfoo\x1b" => "hello foo", 8;
vi_cc_whole : "hello world" => "ccfoo\x1b" => "foo", 2; vi_cc_whole : "hello world" => "ccfoo\x1b" => "foo", 2;
vi_ct_char : "hello world" => "ctwfoo\x1b" => "fooworld", 2; vi_ct_char : "hello world" => "ctwfoo\x1b" => "fooworld", 2;
vi_s_single : "hello" => "sfoo\x1b" => "fooello", 2; vi_s_single : "hello" => "sfoo\x1b" => "fooello", 2;
vi_S_whole_line : "hello world" => "Sfoo\x1b" => "foo", 2; vi_S_whole_line : "hello world" => "Sfoo\x1b" => "foo", 2;
vi_cl_forward : "hello" => "clX\x1b" => "Xello", 0; vi_cl_forward : "hello" => "clX\x1b" => "Xello", 0;
vi_ch_backward : "hello" => "llchX\x1b" => "hXllo", 1; vi_ch_backward : "hello" => "llchX\x1b" => "hXllo", 1;
vi_cb_word_back : "hello world" => "$cbfoo\x1b" => "hello food", 8; vi_cb_word_back : "hello world" => "$cbfoo\x1b" => "hello food", 8;
vi_ce_word_end : "hello world" => "cefoo\x1b" => "foo world", 2; vi_ce_word_end : "hello world" => "cefoo\x1b" => "foo world", 2;
vi_c0_to_start : "hello world" => "wc0foo\x1b" => "fooworld", 2; vi_c0_to_start : "hello world" => "wc0foo\x1b" => "fooworld", 2;
vi_yw_p_basic : "hello world" => "ywwP" => "hello hello world", 11; vi_yw_p_basic : "hello world" => "ywwP" => "hello hello world", 11;
vi_dw_p_paste : "hello world" => "dwP" => "hello world", 5; vi_dw_p_paste : "hello world" => "dwP" => "hello world", 5;
vi_dd_p_paste : "hello world" => "ddp" => "\nhello world", 1; vi_dd_p_paste : "hello world" => "ddp" => "\nhello world", 1;
vi_y_dollar_p : "hello world" => "wy$P" => "hello worldworld", 10; vi_y_dollar_p : "hello world" => "wy$P" => "hello worldworld", 10;
vi_ye_p : "hello world" => "yewP" => "hello helloworld", 10; vi_ye_p : "hello world" => "yewP" => "hello helloworld", 10;
vi_yy_p : "hello world" => "yyp" => "hello world\nhello world", 12; vi_yy_p : "hello world" => "yyp" => "hello world\nhello world", 12;
vi_Y_p : "hello world" => "Yp" => "hhello worldello world", 11; vi_Y_p : "hello world" => "Yp" => "hhello worldello world", 11;
vi_p_after_x : "hello" => "xp" => "ehllo", 1; vi_p_after_x : "hello" => "xp" => "ehllo", 1;
vi_P_before : "hello" => "llxP" => "hello", 2; vi_P_before : "hello" => "llxP" => "hello", 2;
vi_paste_empty : "hello" => "p" => "hello", 0; vi_paste_empty : "hello" => "p" => "hello", 0;
vi_r_replace : "hello" => "ra" => "aello", 0; vi_r_replace : "hello" => "ra" => "aello", 0;
vi_r_middle : "hello" => "llra" => "healo", 2; vi_r_middle : "hello" => "llra" => "healo", 2;
vi_r_at_end : "hello" => "$ra" => "hella", 4; vi_r_at_end : "hello" => "$ra" => "hella", 4;
vi_r_space : "hello" => "r " => " ello", 0; vi_r_space : "hello" => "r " => " ello", 0;
vi_r_with_count : "hello" => "3rx" => "xxxlo", 2; vi_r_with_count : "hello" => "3rx" => "xxxlo", 2;
vi_tilde_single : "hello" => "~" => "Hello", 1; vi_tilde_single : "hello" => "~" => "Hello", 1;
vi_tilde_count : "hello" => "3~" => "HELlo", 3; vi_tilde_count : "hello" => "3~" => "HELlo", 3;
vi_tilde_at_end : "HELLO" => "$~" => "HELLo", 4; vi_tilde_at_end : "HELLO" => "$~" => "HELLo", 4;
vi_tilde_mixed : "hElLo" => "5~" => "HeLlO", 4; vi_tilde_mixed : "hElLo" => "5~" => "HeLlO", 4;
vi_gu_word : "HELLO world" => "guw" => "hello world", 0; vi_gu_word : "HELLO world" => "guw" => "hello world", 0;
vi_gU_word : "hello WORLD" => "gUw" => "HELLO WORLD", 0; vi_gU_word : "hello WORLD" => "gUw" => "HELLO WORLD", 0;
vi_gu_dollar : "HELLO WORLD" => "gu$" => "hello world", 0; vi_gu_dollar : "HELLO WORLD" => "gu$" => "hello world", 0;
vi_gU_dollar : "hello world" => "gU$" => "HELLO WORLD", 0; vi_gU_dollar : "hello world" => "gU$" => "HELLO WORLD", 0;
vi_gu_0 : "HELLO WORLD" => "$gu0" => "hello worlD", 0; vi_gu_0 : "HELLO WORLD" => "$gu0" => "hello worlD", 0;
vi_gU_0 : "hello world" => "$gU0" => "HELLO WORLd", 0; vi_gU_0 : "hello world" => "$gU0" => "HELLO WORLd", 0;
vi_gtilde_word : "hello WORLD" => "g~w" => "HELLO WORLD", 0; vi_gtilde_word : "hello WORLD" => "g~w" => "HELLO WORLD", 0;
vi_gtilde_dollar : "hello WORLD" => "g~$" => "HELLO world", 0; vi_gtilde_dollar : "hello WORLD" => "g~$" => "HELLO world", 0;
vi_diw_inner : "one two three" => "wdiw" => "one three", 4; vi_diw_inner : "one two three" => "wdiw" => "one three", 4;
vi_ciw_replace : "hello world" => "ciwfoo\x1b" => "foo world", 2; vi_ciw_replace : "hello world" => "ciwfoo\x1b" => "foo world", 2;
vi_daw_around : "one two three" => "wdaw" => "one three", 4; vi_daw_around : "one two three" => "wdaw" => "one three", 4;
vi_yiw_p : "hello world" => "yiwAp \x1bp" => "hello worldp hello", 17; vi_yiw_p : "hello world" => "yiwAp \x1bp" => "hello worldp hello", 17;
vi_diW_big_inner : "one-two three" => "diW" => " three", 0; vi_diW_big_inner : "one-two three" => "diW" => " three", 0;
vi_daW_big_around : "one two-three end" => "wdaW" => "one end", 4; vi_daW_big_around : "one two-three end" => "wdaW" => "one end", 4;
vi_ciW_big : "one-two three" => "ciWx\x1b" => "x three", 0; vi_ciW_big : "one-two three" => "ciWx\x1b" => "x three", 0;
vi_di_dquote : "one \"two\" three" => "f\"di\"" => "one \"\" three", 5; vi_di_dquote : "one \"two\" three" => "f\"di\"" => "one \"\" three", 5;
vi_da_dquote : "one \"two\" three" => "f\"da\"" => "one three", 4; vi_da_dquote : "one \"two\" three" => "f\"da\"" => "one three", 4;
vi_ci_dquote : "one \"two\" three" => "f\"ci\"x\x1b" => "one \"x\" three", 5; vi_ci_dquote : "one \"two\" three" => "f\"ci\"x\x1b" => "one \"x\" three", 5;
vi_di_squote : "one 'two' three" => "f'di'" => "one '' three", 5; vi_di_squote : "one 'two' three" => "f'di'" => "one '' three", 5;
vi_da_squote : "one 'two' three" => "f'da'" => "one three", 4; vi_da_squote : "one 'two' three" => "f'da'" => "one three", 4;
vi_di_backtick : "one `two` three" => "f`di`" => "one `` three", 5; vi_di_backtick : "one `two` three" => "f`di`" => "one `` three", 5;
vi_da_backtick : "one `two` three" => "f`da`" => "one three", 4; vi_da_backtick : "one `two` three" => "f`da`" => "one three", 4;
vi_ci_dquote_empty : "one \"\" three" => "f\"ci\"x\x1b" => "one \"x\" three", 5; vi_ci_dquote_empty : "one \"\" three" => "f\"ci\"x\x1b" => "one \"x\" three", 5;
vi_di_paren : "one (two) three" => "f(di(" => "one () three", 5; vi_di_paren : "one (two) three" => "f(di(" => "one () three", 5;
vi_da_paren : "one (two) three" => "f(da(" => "one three", 4; vi_da_paren : "one (two) three" => "f(da(" => "one three", 4;
vi_ci_paren : "one (two) three" => "f(ci(x\x1b" => "one (x) three", 5; vi_ci_paren : "one (two) three" => "f(ci(x\x1b" => "one (x) three", 5;
vi_di_brace : "one {two} three" => "f{di{" => "one {} three", 5; vi_di_brace : "one {two} three" => "f{di{" => "one {} three", 5;
vi_da_brace : "one {two} three" => "f{da{" => "one three", 4; vi_da_brace : "one {two} three" => "f{da{" => "one three", 4;
vi_di_bracket : "one [two] three" => "f[di[" => "one [] three", 5; vi_di_bracket : "one [two] three" => "f[di[" => "one [] three", 5;
vi_da_bracket : "one [two] three" => "f[da[" => "one three", 4; vi_da_bracket : "one [two] three" => "f[da[" => "one three", 4;
vi_di_angle : "one <two> three" => "f<di<" => "one <> three", 5; vi_di_angle : "one <two> three" => "f<di<" => "one <> three", 5;
vi_da_angle : "one <two> three" => "f<da<" => "one three", 4; vi_da_angle : "one <two> three" => "f<da<" => "one three", 4;
vi_di_paren_nested : "fn(a, (b, c))" => "f(di(" => "fn()", 3; vi_di_paren_nested : "fn(a, (b, c))" => "f(di(" => "fn()", 3;
vi_di_paren_empty : "fn() end" => "f(di(" => "fn() end", 3; vi_di_paren_empty : "fn() end" => "f(di(" => "fn() end", 3;
vi_dib_alias : "one (two) three" => "f(dib" => "one () three", 5; vi_dib_alias : "one (two) three" => "f(dib" => "one () three", 5;
vi_diB_alias : "one {two} three" => "f{diB" => "one {} three", 5; vi_diB_alias : "one {two} three" => "f{diB" => "one {} three", 5;
vi_percent_paren : "(hello) world" => "%" => "(hello) world", 6; vi_percent_paren : "(hello) world" => "%" => "(hello) world", 6;
vi_percent_brace : "{hello} world" => "%" => "{hello} world", 6; vi_percent_brace : "{hello} world" => "%" => "{hello} world", 6;
vi_percent_bracket : "[hello] world" => "%" => "[hello] world", 6; vi_percent_bracket : "[hello] world" => "%" => "[hello] world", 6;
vi_percent_from_close: "(hello) world" => "f)%" => "(hello) world", 0; vi_percent_from_close: "(hello) world" => "f)%" => "(hello) world", 0;
vi_d_percent_paren : "(hello) world" => "d%" => " world", 0; vi_d_percent_paren : "(hello) world" => "d%" => " world", 0;
vi_i_insert : "hello" => "iX\x1b" => "Xhello", 0; vi_i_insert : "hello" => "iX\x1b" => "Xhello", 0;
vi_a_append : "hello" => "aX\x1b" => "hXello", 1; vi_a_append : "hello" => "aX\x1b" => "hXello", 1;
vi_I_front : " hello" => "IX\x1b" => " Xhello", 2; vi_I_front : " hello" => "IX\x1b" => " Xhello", 2;
vi_A_end : "hello" => "AX\x1b" => "helloX", 5; vi_A_end : "hello" => "AX\x1b" => "helloX", 5;
vi_o_open_below : "hello" => "oworld\x1b" => "hello\nworld", 10; vi_o_open_below : "hello" => "oworld\x1b" => "hello\nworld", 10;
vi_O_open_above : "hello" => "Oworld\x1b" => "world\nhello", 4; vi_O_open_above : "hello" => "Oworld\x1b" => "world\nhello", 4;
vi_empty_input : "" => "i hello\x1b" => " hello", 5; vi_empty_input : "" => "i hello\x1b" => " hello", 5;
vi_insert_escape : "hello" => "aX\x1b" => "hXello", 1; vi_insert_escape : "hello" => "aX\x1b" => "hXello", 1;
vi_ctrl_w_del_word : "hello world" => "A\x17\x1b" => "hello ", 5; vi_ctrl_w_del_word : "hello world" => "A\x17\x1b" => "hello ", 5;
vi_ctrl_h_backspace : "hello" => "A\x08\x1b" => "hell", 3; vi_ctrl_h_backspace : "hello" => "A\x08\x1b" => "hell", 3;
vi_u_undo_delete : "hello world" => "dwu" => "hello world", 0; vi_u_undo_delete : "hello world" => "dwu" => "hello world", 0;
vi_u_undo_change : "hello world" => "ciwfoo\x1bu" => "hello world", 0; vi_u_undo_change : "hello world" => "ciwfoo\x1bu" => "hello world", 0;
vi_u_undo_x : "hello" => "xu" => "hello", 0; vi_u_undo_x : "hello" => "xu" => "hello", 0;
vi_ctrl_r_redo : "hello" => "xu\x12" => "ello", 0; vi_ctrl_r_redo : "hello" => "xu\x12" => "ello", 0;
vi_u_multiple : "hello world" => "xdwu" => "ello world", 0; vi_u_multiple : "hello world" => "xdwu" => "ello world", 0;
vi_redo_after_undo : "hello world" => "dwu\x12" => "world", 0; vi_redo_after_undo : "hello world" => "dwu\x12" => "world", 0;
vi_dot_repeat_x : "hello" => "x." => "llo", 0; vi_dot_repeat_x : "hello" => "x." => "llo", 0;
vi_dot_repeat_dw : "one two three" => "dw." => "three", 0; vi_dot_repeat_dw : "one two three" => "dw." => "three", 0;
vi_dot_repeat_cw : "one two three" => "cwfoo\x1bw." => "foo foo three", 6; vi_dot_repeat_cw : "one two three" => "cwfoo\x1bw." => "foo foo three", 6;
vi_dot_repeat_r : "hello" => "ra.." => "aello", 0; vi_dot_repeat_r : "hello" => "ra.." => "aello", 0;
vi_dot_repeat_s : "hello" => "sX\x1bl." => "XXllo", 1; vi_dot_repeat_s : "hello" => "sX\x1bl." => "XXllo", 1;
vi_count_h : "hello world" => "$3h" => "hello world", 7; vi_count_h : "hello world" => "$3h" => "hello world", 7;
vi_count_l : "hello world" => "3l" => "hello world", 3; vi_count_l : "hello world" => "3l" => "hello world", 3;
vi_count_w : "one two three four" => "2w" => "one two three four", 8; vi_count_w : "one two three four" => "2w" => "one two three four", 8;
vi_count_b : "one two three four" => "$2b" => "one two three four", 8; vi_count_b : "one two three four" => "$2b" => "one two three four", 8;
vi_count_x : "hello" => "3x" => "lo", 0; vi_count_x : "hello" => "3x" => "lo", 0;
vi_count_dw : "one two three four" => "2dw" => "three four", 0; vi_count_dw : "one two three four" => "2dw" => "three four", 0;
vi_verb_count_motion : "one two three four" => "d2w" => "three four", 0; vi_verb_count_motion : "one two three four" => "d2w" => "three four", 0;
vi_count_s : "hello" => "3sX\x1b" => "Xlo", 0; vi_count_s : "hello" => "3sX\x1b" => "Xlo", 0;
vi_indent_line : "hello" => ">>" => "\thello", 1; vi_indent_line : "hello" => ">>" => "\thello", 1;
vi_dedent_line : "\thello" => "<<" => "hello", 0; vi_dedent_line : "\thello" => "<<" => "hello", 0;
vi_indent_double : "hello" => ">>>>" => "\t\thello", 2; vi_indent_double : "hello" => ">>>>" => "\t\thello", 2;
vi_J_join_lines : "hello\nworld" => "J" => "hello world", 5; vi_J_join_lines : "hello\nworld" => "J" => "hello world", 5;
vi_v_u_lower : "HELLO" => "vlllu" => "hellO", 0; vi_v_u_lower : "HELLO" => "vlllu" => "hellO", 0;
vi_v_U_upper : "hello" => "vlllU" => "HELLo", 0; vi_v_U_upper : "hello" => "vlllU" => "HELLo", 0;
vi_v_d_delete : "hello world" => "vwwd" => "", 0; vi_v_d_delete : "hello world" => "vwwd" => "", 0;
vi_v_x_delete : "hello world" => "vwwx" => "", 0; vi_v_x_delete : "hello world" => "vwwx" => "", 0;
vi_v_c_change : "hello world" => "vwcfoo\x1b" => "fooorld", 2; vi_v_c_change : "hello world" => "vwcfoo\x1b" => "fooorld", 2;
vi_v_y_p_yank : "hello world" => "vwyAp \x1bp" => "hello worldp hello w", 19; vi_v_y_p_yank : "hello world" => "vwyAp \x1bp" => "hello worldp hello w", 19;
vi_v_dollar_d : "hello world" => "wv$d" => "hello ", 5; vi_v_dollar_d : "hello world" => "wv$d" => "hello ", 5;
vi_v_0_d : "hello world" => "$v0d" => "", 0; vi_v_0_d : "hello world" => "$v0d" => "", 0;
vi_ve_d : "hello world" => "ved" => " world", 0; vi_ve_d : "hello world" => "ved" => " world", 0;
vi_v_o_swap : "hello world" => "vllod" => "lo world", 0; vi_v_o_swap : "hello world" => "vllod" => "lo world", 0;
vi_v_r_replace : "hello" => "vlllrx" => "xxxxo", 0; vi_v_r_replace : "hello" => "vlllrx" => "xxxxo", 0;
vi_v_tilde_case : "hello" => "vlll~" => "HELLo", 0; vi_v_tilde_case : "hello" => "vlll~" => "HELLo", 0;
vi_V_d_delete : "hello world" => "Vd" => "", 0; vi_V_d_delete : "hello world" => "Vd" => "", 0;
vi_V_y_p : "hello world" => "Vyp" => "hello world\nhello world", 12; vi_V_y_p : "hello world" => "Vyp" => "hello world\nhello world", 12;
vi_V_S_change : "hello world" => "VSfoo\x1b" => "foo", 2; vi_V_S_change : "hello world" => "VSfoo\x1b" => "foo", 2;
vi_ctrl_a_inc : "num 5 end" => "w\x01" => "num 6 end", 4; vi_ctrl_a_inc : "num 5 end" => "w\x01" => "num 6 end", 4;
vi_ctrl_x_dec : "num 5 end" => "w\x18" => "num 4 end", 4; vi_ctrl_x_dec : "num 5 end" => "w\x18" => "num 4 end", 4;
vi_ctrl_a_negative : "num -3 end" => "w\x01" => "num -2 end", 4; vi_ctrl_a_negative : "num -3 end" => "w\x01" => "num -2 end", 4;
vi_ctrl_x_to_neg : "num 0 end" => "w\x18" => "num -1 end", 4; vi_ctrl_x_to_neg : "num 0 end" => "w\x18" => "num -1 end", 4;
vi_ctrl_a_count : "num 5 end" => "w3\x01" => "num 8 end", 4; vi_ctrl_a_count : "num 5 end" => "w3\x01" => "num 8 end", 4;
vi_ctrl_a_width : "num -00001 end" => "w\x01" => "num 00000 end", 4; vi_ctrl_a_width : "num -00001 end" => "w\x01" => "num 00000 end", 4;
vi_delete_empty : "" => "x" => "", 0; vi_delete_empty : "" => "x" => "", 0;
vi_undo_on_empty : "" => "u" => "", 0; vi_undo_on_empty : "" => "u" => "", 0;
vi_w_single_char : "a b c" => "w" => "a b c", 2; vi_w_single_char : "a b c" => "w" => "a b c", 2;
vi_dw_last_word : "hello" => "dw" => "", 0; vi_dw_last_word : "hello" => "dw" => "", 0;
vi_dollar_single : "h" => "$" => "h", 0; vi_dollar_single : "h" => "$" => "h", 0;
vi_caret_no_ws : "hello" => "$^" => "hello", 0; vi_caret_no_ws : "hello" => "$^" => "hello", 0;
vi_f_last_char : "hello" => "fo" => "hello", 4; vi_f_last_char : "hello" => "fo" => "hello", 4;
vi_r_on_space : "hello world" => "5|r-" => "hell- world", 4; vi_r_on_space : "hello world" => "5|r-" => "hell- world", 4;
vi_vw_doesnt_crash : "" => "vw" => "", 0; vi_vw_doesnt_crash : "" => "vw" => "", 0;
vi_indent_cursor_pos : "echo foo" => ">>" => "\techo foo", 1; vi_indent_cursor_pos : "echo foo" => ">>" => "\techo foo", 1;
vi_join_indent_lines : "echo foo\n\t\techo bar" => "J" => "echo foo echo bar", 8 vi_join_indent_lines : "echo foo\n\t\techo bar" => "J" => "echo foo echo bar", 8
} }
#[test]
fn vi_auto_indent() {
let (mut vi, _g) = test_vi("");
// Type each line and press Enter separately so auto-indent triggers
let lines = [
"func() {",
"case foo in",
"bar)",
"while true; do",
"echo foo \\\rbar \\\rbiz \\\rbazz\rbreak\rdone\r;;\resac\r}"
];
for (i,line) in lines.iter().enumerate() {
vi.feed_bytes(line.as_bytes());
if i != lines.len() - 1 {
vi.feed_bytes(b"\r");
}
vi.process_input().unwrap();
}
assert_eq!(
vi.editor.as_str(),
"func() {\n\tcase foo in\n\t\tbar)\n\t\t\twhile true; do\n\t\t\t\techo foo \\\n\t\t\t\t\tbar \\\n\t\t\t\t\tbiz \\\n\t\t\t\t\tbazz\n\t\t\t\tbreak\n\t\t\tdone\n\t\t;;\n\tesac\n}"
);
}

View File

@@ -146,6 +146,7 @@ pub struct ShOptCore {
pub bell_enabled: bool, pub bell_enabled: bool,
pub max_recurse_depth: usize, pub max_recurse_depth: usize,
pub xpg_echo: bool, pub xpg_echo: bool,
pub noclobber: bool,
} }
impl ShOptCore { impl ShOptCore {
@@ -238,6 +239,15 @@ impl ShOptCore {
}; };
self.xpg_echo = val; self.xpg_echo = val;
} }
"noclobber" => {
let Ok(val) = val.parse::<bool>() else {
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
"shopt: expected 'true' or 'false' for noclobber value",
));
};
self.noclobber = val;
}
_ => { _ => {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
@@ -304,6 +314,11 @@ impl ShOptCore {
output.push_str(&format!("{}", self.xpg_echo)); output.push_str(&format!("{}", self.xpg_echo));
Ok(Some(output)) Ok(Some(output))
} }
"noclobber" => {
let mut output = String::from("Prevent > from overwriting existing files (use >| to override)\n");
output.push_str(&format!("{}", self.noclobber));
Ok(Some(output))
}
_ => Err(ShErr::simple( _ => Err(ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
format!("shopt: Unexpected 'core' option '{query}'"), format!("shopt: Unexpected 'core' option '{query}'"),
@@ -327,6 +342,7 @@ impl Display for ShOptCore {
output.push(format!("bell_enabled = {}", self.bell_enabled)); output.push(format!("bell_enabled = {}", self.bell_enabled));
output.push(format!("max_recurse_depth = {}", self.max_recurse_depth)); output.push(format!("max_recurse_depth = {}", self.max_recurse_depth));
output.push(format!("xpg_echo = {}", self.xpg_echo)); output.push(format!("xpg_echo = {}", self.xpg_echo));
output.push(format!("noclobber = {}", self.noclobber));
let final_output = output.join("\n"); let final_output = output.join("\n");
@@ -346,6 +362,7 @@ impl Default for ShOptCore {
bell_enabled: true, bell_enabled: true,
max_recurse_depth: 1000, max_recurse_depth: 1000,
xpg_echo: false, xpg_echo: false,
noclobber: false,
} }
} }
} }
@@ -589,6 +606,7 @@ mod tests {
bell_enabled, bell_enabled,
max_recurse_depth, max_recurse_depth,
xpg_echo, xpg_echo,
noclobber,
} = ShOptCore::default(); } = ShOptCore::default();
// If a field is added to the struct, this destructure fails to compile. // If a field is added to the struct, this destructure fails to compile.
let _ = ( let _ = (
@@ -601,6 +619,7 @@ mod tests {
bell_enabled, bell_enabled,
max_recurse_depth, max_recurse_depth,
xpg_echo, xpg_echo,
noclobber,
); );
} }

View File

@@ -1330,6 +1330,15 @@ 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 => {
let ifs = get_separator();
self
.params
.get(&ShellParam::AllArgs)
.map(|s| s.replace(markers::ARG_SEP, &ifs).to_string())
.unwrap_or_default()
}
_ => self _ => self
.params .params
.get(&param) .get(&param)
@@ -1842,6 +1851,15 @@ pub fn change_dir<P: AsRef<Path>>(dir: P) -> ShResult<()> {
Ok(()) Ok(())
} }
pub fn get_separator() -> String {
env::var("IFS")
.unwrap_or(String::from(" "))
.chars()
.next()
.unwrap()
.to_string()
}
pub fn get_status() -> i32 { pub fn get_status() -> i32 {
read_vars(|v| v.get_param(ShellParam::Status)) read_vars(|v| v.get_param(ShellParam::Status))
.parse::<i32>() .parse::<i32>()

View File

@@ -98,7 +98,7 @@ impl TestGuard {
} }
} }
pub fn pty_slave(&self) -> BorrowedFd { pub fn pty_slave(&self) -> BorrowedFd<'_> {
unsafe { BorrowedFd::borrow_raw(self.pty_slave.as_raw_fd()) } unsafe { BorrowedFd::borrow_raw(self.pty_slave.as_raw_fd()) }
} }
@@ -191,7 +191,7 @@ impl crate::parse::Node {
if offender.is_none() if offender.is_none()
&& expected_rule && expected_rule
.as_ref() .as_ref()
.map_or(true, |e| *e != s.class.as_nd_kind()) .is_none_or(|e| *e != s.class.as_nd_kind())
{ {
offender = Some((s.class.as_nd_kind(), expected_rule)); offender = Some((s.class.as_nd_kind(), expected_rule));
} else if offender.is_none() { } else if offender.is_none() {