Compare commits
5 Commits
307386ffc6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c6de4f4ec | |||
| 9bd9c66b92 | |||
| 5173e1908d | |||
| 1f9c96f24e | |||
| 09024728f6 |
@@ -8,7 +8,7 @@ A Linux shell written in Rust. The name is a nod to the original Unix utilities
|
||||
|
||||
### 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
|
||||
- **Insert mode** - insert, append, replace, with Ctrl+W word deletion and undo/redo
|
||||
|
||||
@@ -24,13 +24,14 @@ pub mod source;
|
||||
pub mod test; // [[ ]] thing
|
||||
pub mod trap;
|
||||
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",
|
||||
"disown", "alias", "unalias", "return", "break", "continue", "exit", "shopt", "builtin",
|
||||
"command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly",
|
||||
"unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type",
|
||||
"getopts", "keymap", "read_key", "autocmd", "ulimit", "umask",
|
||||
"getopts", "keymap", "read_key", "autocmd", "ulimit", "umask", "seek"
|
||||
];
|
||||
|
||||
pub fn true_builtin() -> ShResult<()> {
|
||||
|
||||
253
src/builtin/seek.rs
Normal file
253
src/builtin/seek.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
172
src/expand.rs
172
src/expand.rs
@@ -40,18 +40,23 @@ impl Tk {
|
||||
}
|
||||
|
||||
pub struct Expander {
|
||||
flags: TkFlags,
|
||||
raw: String,
|
||||
}
|
||||
|
||||
impl Expander {
|
||||
pub fn new(raw: Tk) -> ShResult<Self> {
|
||||
let raw = raw.span.as_str();
|
||||
Self::from_raw(raw)
|
||||
let tk_raw = raw.span.as_str();
|
||||
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 unescaped = unescape_str(&raw);
|
||||
Ok(Self { raw: unescaped })
|
||||
let unescaped = if flags.contains(TkFlags::IS_HEREDOC) {
|
||||
unescape_heredoc(&raw)
|
||||
} else {
|
||||
unescape_str(&raw)
|
||||
};
|
||||
Ok(Self { raw: unescaped, flags })
|
||||
}
|
||||
pub fn expand(&mut self) -> ShResult<Vec<String>> {
|
||||
let mut chars = self.raw.chars().peekable();
|
||||
@@ -75,7 +80,11 @@ impl Expander {
|
||||
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> {
|
||||
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);
|
||||
break;
|
||||
@@ -1318,6 +1346,25 @@ pub fn unescape_str(raw: &str) -> String {
|
||||
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),
|
||||
}
|
||||
first_char = false;
|
||||
@@ -1326,6 +1373,96 @@ pub fn unescape_str(raw: &str) -> String {
|
||||
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
|
||||
/// Used for completion results, and glob filename matches.
|
||||
pub fn escape_str(raw: &str, use_marker: bool) -> String {
|
||||
@@ -3532,6 +3669,7 @@ mod tests {
|
||||
|
||||
let mut exp = Expander {
|
||||
raw: "hello world\tfoo".to_string(),
|
||||
flags: TkFlags::empty()
|
||||
};
|
||||
let words = exp.split_words();
|
||||
assert_eq!(words, vec!["hello", "world", "foo"]);
|
||||
@@ -3546,6 +3684,7 @@ mod tests {
|
||||
|
||||
let mut exp = Expander {
|
||||
raw: "a:b:c".to_string(),
|
||||
flags: TkFlags::empty()
|
||||
};
|
||||
let words = exp.split_words();
|
||||
assert_eq!(words, vec!["a", "b", "c"]);
|
||||
@@ -3560,6 +3699,7 @@ mod tests {
|
||||
|
||||
let mut exp = Expander {
|
||||
raw: "hello world".to_string(),
|
||||
flags: TkFlags::empty()
|
||||
};
|
||||
let words = exp.split_words();
|
||||
assert_eq!(words, vec!["hello world"]);
|
||||
@@ -3570,7 +3710,10 @@ mod tests {
|
||||
let _guard = TestGuard::new();
|
||||
|
||||
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();
|
||||
assert_eq!(words, vec!["hello world"]);
|
||||
}
|
||||
@@ -3582,7 +3725,10 @@ mod tests {
|
||||
let _guard = TestGuard::new();
|
||||
|
||||
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();
|
||||
assert_eq!(words, vec!["hello world"]);
|
||||
}
|
||||
@@ -3592,7 +3738,10 @@ mod tests {
|
||||
let _guard = TestGuard::new();
|
||||
|
||||
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();
|
||||
assert_eq!(words, vec!["hello\tworld"]);
|
||||
}
|
||||
@@ -3605,7 +3754,10 @@ mod tests {
|
||||
}
|
||||
|
||||
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();
|
||||
assert_eq!(words, vec!["a:b", "c"]);
|
||||
}
|
||||
|
||||
@@ -95,14 +95,16 @@ pub fn sort_tks(
|
||||
.into_iter()
|
||||
.map(|t| t.expand())
|
||||
.collect::<ShResult<Vec<_>>>()?
|
||||
.into_iter();
|
||||
.into_iter()
|
||||
.peekable();
|
||||
let mut opts = vec![];
|
||||
let mut non_opts = vec![];
|
||||
|
||||
while let Some(token) = tokens_iter.next() {
|
||||
if &token.to_string() == "--" {
|
||||
non_opts.extend(tokens_iter);
|
||||
break;
|
||||
non_opts.push(token);
|
||||
non_opts.extend(tokens_iter);
|
||||
break;
|
||||
}
|
||||
let parsed_opts = Opt::parse(&token.to_string());
|
||||
|
||||
|
||||
@@ -201,6 +201,7 @@ impl ShErr {
|
||||
pub fn is_flow_control(&self) -> bool {
|
||||
self.kind.is_flow_control()
|
||||
}
|
||||
/// Promotes a shell error from a simple error to an error that blames a span
|
||||
pub fn promote(mut self, span: Span) -> Self {
|
||||
if self.notes.is_empty() {
|
||||
return self;
|
||||
@@ -208,7 +209,9 @@ impl ShErr {
|
||||
let first = self.notes[0].clone();
|
||||
if self.notes.len() > 1 {
|
||||
self.notes = self.notes[1..].to_vec();
|
||||
}
|
||||
} else {
|
||||
self.notes = vec![];
|
||||
}
|
||||
|
||||
self.labeled(span, first)
|
||||
}
|
||||
|
||||
@@ -147,11 +147,9 @@ impl RawModeGuard {
|
||||
let orig = ORIG_TERMIOS
|
||||
.with(|cell| cell.borrow().clone())
|
||||
.expect("with_cooked_mode called before raw_mode()");
|
||||
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &orig)
|
||||
.expect("Failed to restore cooked mode");
|
||||
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &orig).ok();
|
||||
let res = f();
|
||||
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, ¤t)
|
||||
.expect("Failed to restore raw mode");
|
||||
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, ¤t).ok();
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,14 @@ use std::sync::LazyLock;
|
||||
|
||||
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(|| {
|
||||
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
|
||||
});
|
||||
|
||||
@@ -31,7 +31,6 @@ use nix::unistd::read;
|
||||
use crate::builtin::keymap::KeyMapMatch;
|
||||
use crate::builtin::trap::TrapTarget;
|
||||
use crate::libsh::error::{self, ShErr, ShErrKind, ShResult};
|
||||
use crate::libsh::guards::scope_guard;
|
||||
use crate::libsh::sys::TTY_FILENO;
|
||||
use crate::libsh::utils::AutoCmdVecUtils;
|
||||
use crate::parse::execute::{exec_dash_c, exec_input};
|
||||
|
||||
@@ -8,28 +8,7 @@ use ariadne::Fmt;
|
||||
|
||||
use crate::{
|
||||
builtin::{
|
||||
alias::{alias, unalias},
|
||||
arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate},
|
||||
autocmd::autocmd,
|
||||
cd::cd,
|
||||
complete::{compgen_builtin, complete_builtin},
|
||||
dirstack::{dirs, popd, pushd},
|
||||
echo::echo,
|
||||
eval, exec,
|
||||
flowctl::flowctl,
|
||||
getopts::getopts,
|
||||
intro,
|
||||
jobctl::{self, JobBehavior, continue_job, disown, jobs},
|
||||
keymap, map,
|
||||
pwd::pwd,
|
||||
read::{self, read_builtin},
|
||||
resource::{ulimit, umask_builtin},
|
||||
shift::shift,
|
||||
shopt::shopt,
|
||||
source::source,
|
||||
test::double_bracket_test,
|
||||
trap::{TrapTarget, trap},
|
||||
varcmds::{export, local, readonly, unset},
|
||||
alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, autocmd::autocmd, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, getopts::getopts, intro, jobctl::{self, JobBehavior, continue_job, disown, jobs}, keymap, 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}
|
||||
},
|
||||
expand::{expand_aliases, expand_case_pattern, glob_to_regex},
|
||||
jobs::{ChildProc, JobStack, attach_tty, dispatch_job},
|
||||
@@ -340,24 +319,19 @@ impl Dispatcher {
|
||||
};
|
||||
|
||||
let mut elem_iter = elements.into_iter();
|
||||
let mut skip = false;
|
||||
while let Some(element) = elem_iter.next() {
|
||||
let ConjunctNode { cmd, operator } = element;
|
||||
self.dispatch_node(*cmd)?;
|
||||
if !skip {
|
||||
self.dispatch_node(*cmd)?;
|
||||
}
|
||||
|
||||
let status = state::get_status();
|
||||
match operator {
|
||||
ConjunctOp::And => {
|
||||
if status != 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ConjunctOp::Or => {
|
||||
if status == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
skip = match operator {
|
||||
ConjunctOp::And => status != 0,
|
||||
ConjunctOp::Or => status == 0,
|
||||
ConjunctOp::Null => break,
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -377,7 +351,7 @@ impl Dispatcher {
|
||||
};
|
||||
let body_span = body.get_span();
|
||||
let body = body_span.as_str().to_string();
|
||||
let name = name.span.as_str().strip_suffix("()").unwrap();
|
||||
let name = name.span.as_str().strip_suffix("()").unwrap_or(name.span.as_str());
|
||||
|
||||
if KEYWORDS.contains(&name) {
|
||||
return Err(ShErr::at(
|
||||
@@ -803,7 +777,7 @@ impl Dispatcher {
|
||||
self.job_stack.new_job();
|
||||
if cmds.len() == 1 {
|
||||
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 { .. }) {
|
||||
self.run_fork(
|
||||
&cmd.get_command().map(|t| t.to_string()).unwrap_or_default(),
|
||||
@@ -888,7 +862,10 @@ impl Dispatcher {
|
||||
|
||||
if fork_builtins {
|
||||
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| {
|
||||
if let Err(e) = s.dispatch_builtin(cmd) {
|
||||
e.print_error();
|
||||
@@ -1013,6 +990,7 @@ impl Dispatcher {
|
||||
"autocmd" => autocmd(cmd),
|
||||
"ulimit" => ulimit(cmd),
|
||||
"umask" => umask_builtin(cmd),
|
||||
"seek" => seek(cmd),
|
||||
"true" | ":" => {
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
|
||||
358
src/parse/lex.rs
358
src/parse/lex.rs
@@ -217,6 +217,32 @@ impl Tk {
|
||||
};
|
||||
self.span.as_str().trim() == ";;"
|
||||
}
|
||||
|
||||
pub fn is_opener(&self) -> bool {
|
||||
OPENERS.contains(&self.as_str()) ||
|
||||
matches!(self.class, TkRule::BraceGrpStart) ||
|
||||
matches!(self.class, TkRule::CasePattern)
|
||||
}
|
||||
pub fn is_closer(&self) -> bool {
|
||||
matches!(self.as_str(), "fi" | "done" | "esac") ||
|
||||
self.has_double_semi() ||
|
||||
matches!(self.class, TkRule::BraceGrpEnd)
|
||||
}
|
||||
|
||||
pub fn is_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 {
|
||||
@@ -241,20 +267,12 @@ bitflags! {
|
||||
const ASSIGN = 0b0000000001000000;
|
||||
const BUILTIN = 0b0000000010000000;
|
||||
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! {
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LexFlags: u32 {
|
||||
@@ -296,6 +314,19 @@ pub fn clean_input(input: &str) -> String {
|
||||
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 {
|
||||
pub fn new(source: Arc<String>, flags: LexFlags) -> Self {
|
||||
let flags = flags | LexFlags::FRESH | LexFlags::NEXT_IS_CMD;
|
||||
@@ -307,6 +338,7 @@ impl LexStream {
|
||||
quote_state: QuoteState::default(),
|
||||
brc_grp_depth: 0,
|
||||
brc_grp_start: None,
|
||||
heredoc_skip: None,
|
||||
case_depth: 0,
|
||||
}
|
||||
}
|
||||
@@ -367,7 +399,7 @@ impl LexStream {
|
||||
}
|
||||
pub fn read_redir(&mut self) -> Option<ShResult<Tk>> {
|
||||
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 chars = slice.chars().peekable();
|
||||
let mut tk = Tk::default();
|
||||
@@ -379,37 +411,51 @@ impl LexStream {
|
||||
return None; // It's a process sub
|
||||
}
|
||||
pos += 1;
|
||||
if let Some('|') = chars.peek() {
|
||||
// noclobber force '>|'
|
||||
chars.next();
|
||||
pos += 1;
|
||||
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
||||
break
|
||||
}
|
||||
|
||||
if let Some('>') = chars.peek() {
|
||||
chars.next();
|
||||
pos += 1;
|
||||
}
|
||||
if let Some('&') = chars.peek() {
|
||||
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 {
|
||||
let Some('&') = chars.peek() else {
|
||||
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
||||
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(&'(') {
|
||||
@@ -417,14 +463,94 @@ impl LexStream {
|
||||
}
|
||||
pos += 1;
|
||||
|
||||
for _ in 0..2 {
|
||||
if let Some('<') = chars.peek() {
|
||||
chars.next();
|
||||
pos += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
match chars.peek() {
|
||||
Some('<') => {
|
||||
chars.next();
|
||||
pos += 1;
|
||||
|
||||
match chars.peek() {
|
||||
Some('<') => {
|
||||
chars.next();
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
Some(ch) => {
|
||||
let mut ch = *ch;
|
||||
while is_field_sep(ch) {
|
||||
let Some(next_ch) = chars.next() else {
|
||||
// Incomplete input — fall through to emit << as Redir
|
||||
break;
|
||||
};
|
||||
pos += next_ch.len_utf8();
|
||||
ch = next_ch;
|
||||
}
|
||||
|
||||
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);
|
||||
break;
|
||||
}
|
||||
@@ -448,6 +574,130 @@ impl LexStream {
|
||||
self.cursor = pos;
|
||||
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> {
|
||||
assert!(self.cursor <= self.source.len());
|
||||
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 => {
|
||||
pos += 1;
|
||||
let mut paren_count = 1;
|
||||
@@ -845,10 +1105,18 @@ impl Iterator for LexStream {
|
||||
|
||||
let token = match get_char(&self.source, self.cursor).unwrap() {
|
||||
'\r' | '\n' | ';' => {
|
||||
let ch = get_char(&self.source, self.cursor).unwrap();
|
||||
let ch_idx = self.cursor;
|
||||
self.cursor += 1;
|
||||
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) {
|
||||
match ch {
|
||||
'\\' if get_char(&self.source, self.cursor + 1) == Some('\n') => {
|
||||
|
||||
592
src/parse/mod.rs
592
src/parse/mod.rs
File diff suppressed because one or more lines are too long
@@ -19,7 +19,7 @@ pub use std::os::unix::io::{AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd,
|
||||
pub use bitflags::bitflags;
|
||||
pub use nix::{
|
||||
errno::Errno,
|
||||
fcntl::{OFlag, open},
|
||||
fcntl::{FcntlArg, OFlag, fcntl, open},
|
||||
libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
|
||||
sys::{
|
||||
signal::{self, SigHandler, SigSet, SigmaskHow, Signal, kill, killpg, pthread_sigmask, signal},
|
||||
|
||||
136
src/procio.rs
136
src/procio.rs
@@ -8,15 +8,26 @@ use crate::{
|
||||
expand::Expander,
|
||||
libsh::{
|
||||
error::{ShErr, ShErrKind, ShResult},
|
||||
sys::TTY_FILENO,
|
||||
utils::RedirVecUtils,
|
||||
},
|
||||
parse::{Redir, RedirType, get_redir_file},
|
||||
prelude::*,
|
||||
parse::{Redir, RedirType, get_redir_file, lex::TkFlags},
|
||||
prelude::*, state,
|
||||
};
|
||||
|
||||
// Credit to fish-shell for many of the implementation ideas present in this
|
||||
// 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)]
|
||||
pub enum IoMode {
|
||||
Fd {
|
||||
@@ -37,8 +48,9 @@ pub enum IoMode {
|
||||
pipe: Arc<OwnedFd>,
|
||||
},
|
||||
Buffer {
|
||||
tgt_fd: RawFd,
|
||||
buf: String,
|
||||
pipe: Arc<OwnedFd>,
|
||||
flags: TkFlags, // so we can see if its a heredoc or not
|
||||
},
|
||||
Close {
|
||||
tgt_fd: RawFd,
|
||||
@@ -79,19 +91,28 @@ impl IoMode {
|
||||
if let IoMode::File { tgt_fd, path, mode } = self {
|
||||
let path_raw = path.as_os_str().to_str().unwrap_or_default().to_string();
|
||||
|
||||
let expanded_path = Expander::from_raw(&path_raw)?.expand()?.join(" "); // should just be one string, will have to find some way to handle a return of
|
||||
// multiple
|
||||
let expanded_path = Expander::from_raw(&path_raw, TkFlags::empty())?.expand()?.join(" "); // should just be one string, will have to find some way to handle a return of multiple paths
|
||||
|
||||
let expanded_pathbuf = PathBuf::from(expanded_path);
|
||||
|
||||
let 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 {
|
||||
tgt_fd,
|
||||
file: Arc::new(OwnedFd::from(file)),
|
||||
file: Arc::new(unsafe { OwnedFd::from_raw_fd(high) }),
|
||||
}
|
||||
}
|
||||
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) {
|
||||
let (rpipe, wpipe) = nix::unistd::pipe2(OFlag::O_CLOEXEC).unwrap();
|
||||
(
|
||||
@@ -206,24 +227,103 @@ impl<'e> IoFrame {
|
||||
)
|
||||
}
|
||||
pub fn save(&'e mut self) {
|
||||
let saved_in = dup(STDIN_FILENO).unwrap();
|
||||
let saved_out = dup(STDOUT_FILENO).unwrap();
|
||||
let saved_err = dup(STDERR_FILENO).unwrap();
|
||||
let saved_in = dup_high(STDIN_FILENO).unwrap();
|
||||
let saved_out = dup_high(STDOUT_FILENO).unwrap();
|
||||
let saved_err = dup_high(STDERR_FILENO).unwrap();
|
||||
self.saved_io = Some(IoGroup(saved_in, saved_out, saved_err));
|
||||
}
|
||||
pub fn redirect(mut self) -> ShResult<RedirGuard> {
|
||||
self.save();
|
||||
for redir in &mut self.redirs {
|
||||
let io_mode = &mut redir.io_mode;
|
||||
if let IoMode::File { .. } = io_mode {
|
||||
*io_mode = io_mode.clone().open_file()?;
|
||||
};
|
||||
let tgt_fd = io_mode.tgt_fd();
|
||||
let src_fd = io_mode.src_fd();
|
||||
dup2(src_fd, tgt_fd)?;
|
||||
if let Err(e) = self.apply_redirs() {
|
||||
// Restore saved fds before propagating the error so they don't leak.
|
||||
self.restore().ok();
|
||||
return Err(e);
|
||||
}
|
||||
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<()> {
|
||||
if let Some(saved) = self.saved_io.take() {
|
||||
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>;
|
||||
|
||||
/// An iterator that lazily creates a specific number of pipes.
|
||||
pub struct PipeGenerator {
|
||||
num_cmds: usize,
|
||||
cursor: usize,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt::{Debug, Write},
|
||||
path::{Path, PathBuf},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::{
|
||||
libsh::{error::ShResult, guards::var_ctx_guard},
|
||||
parse::{
|
||||
execute::exec_input,
|
||||
lex::{LexFlags, LexStream, QuoteState, Tk, TkFlags, TkRule},
|
||||
lex::{LexFlags, LexStream, QuoteState, Tk, TkRule},
|
||||
},
|
||||
prelude::*,
|
||||
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)]
|
||||
pub struct LineBuf {
|
||||
pub buffer: String,
|
||||
@@ -363,7 +430,7 @@ pub struct LineBuf {
|
||||
|
||||
pub insert_mode_start_pos: Option<usize>,
|
||||
pub saved_col: Option<usize>,
|
||||
pub auto_indent_level: usize,
|
||||
pub indent_ctx: IndentCtx,
|
||||
|
||||
pub undo_stack: Vec<Edit>,
|
||||
pub redo_stack: Vec<Edit>,
|
||||
@@ -617,6 +684,17 @@ impl LineBuf {
|
||||
pub fn read_slice_to_cursor(&self) -> Option<&str> {
|
||||
self.read_slice_to(self.cursor.get())
|
||||
}
|
||||
pub fn cursor_is_escaped(&mut self) -> bool {
|
||||
let Some(to_cursor) = self.slice_to_cursor() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// 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> {
|
||||
self.slice_to(self.cursor.ret_add(1))
|
||||
}
|
||||
@@ -829,7 +907,7 @@ impl LineBuf {
|
||||
}
|
||||
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) {
|
||||
self.cursor.get()
|
||||
} else {
|
||||
@@ -2031,51 +2109,13 @@ impl LineBuf {
|
||||
let end = start + (new.len().max(gr.len()));
|
||||
self.buffer.replace_range(start..end, new);
|
||||
}
|
||||
pub fn calc_indent_level(&mut self) {
|
||||
// FIXME: This implementation is extremely naive but it kind of sort of works for now
|
||||
// Need to re-implement it and write tests
|
||||
pub fn calc_indent_level(&mut self) -> usize {
|
||||
let to_cursor = self
|
||||
.slice_to_cursor()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or(self.buffer.clone());
|
||||
|
||||
let mut level: usize = 0;
|
||||
|
||||
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;
|
||||
self.indent_ctx.calculate(&to_cursor)
|
||||
}
|
||||
pub fn eval_motion(&mut self, verb: Option<&Verb>, motion: MotionCmd) -> MotionKind {
|
||||
let buffer = self.buffer.clone();
|
||||
@@ -2652,8 +2692,8 @@ impl LineBuf {
|
||||
register.write_to_register(register_content);
|
||||
self.cursor.set(start);
|
||||
if do_indent {
|
||||
self.calc_indent_level();
|
||||
let tabs = (0..self.auto_indent_level).map(|_| '\t');
|
||||
let depth = self.calc_indent_level();
|
||||
let tabs = (0..depth).map(|_| '\t');
|
||||
for tab in tabs {
|
||||
self.insert_at_cursor(tab);
|
||||
self.cursor.add(1);
|
||||
@@ -2888,17 +2928,29 @@ impl LineBuf {
|
||||
};
|
||||
end = end.saturating_sub(1);
|
||||
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 {
|
||||
i += 1;
|
||||
continue;
|
||||
};
|
||||
if gr == "\n" {
|
||||
if last_was_whitespace {
|
||||
self.remove(i);
|
||||
end -= 1;
|
||||
} else {
|
||||
self.force_replace_at(i, " ");
|
||||
}
|
||||
if last_was_escape {
|
||||
// if we are here, then we just joined an escaped newline
|
||||
// semantically, echo foo\\nbar == echo foo bar
|
||||
// so a joined line should remove the escape.
|
||||
self.remove(i - 1);
|
||||
end -= 1;
|
||||
}
|
||||
last_was_whitespace = false;
|
||||
last_was_escape = false;
|
||||
let strip_pos = if self.grapheme_at(i) == Some(" ") {
|
||||
i + 1
|
||||
} else {
|
||||
@@ -2906,46 +2958,57 @@ impl LineBuf {
|
||||
};
|
||||
while self.grapheme_at(strip_pos) == Some("\t") {
|
||||
self.remove(strip_pos);
|
||||
end -= 1;
|
||||
}
|
||||
self.cursor.set(i);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
last_was_whitespace = is_whitespace(gr);
|
||||
} else if gr == "\\" {
|
||||
if last_was_whitespace && last_was_escape {
|
||||
// if we are here, then the pattern of the last three chars was this:
|
||||
// ' \\', a space and two backslashes.
|
||||
// This means the "last" was an escaped backslash, not whitespace.
|
||||
last_was_whitespace = false;
|
||||
}
|
||||
last_was_escape = !last_was_escape;
|
||||
} else {
|
||||
last_was_whitespace = is_whitespace(gr);
|
||||
last_was_escape = false;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn verb_insert_char(&mut self, ch: char) {
|
||||
self.insert_at_cursor(ch);
|
||||
self.cursor.add(1);
|
||||
let before = self.auto_indent_level;
|
||||
if read_shopts(|o| o.prompt.auto_indent)
|
||||
&& let Some(line_content) = self.this_line_content()
|
||||
{
|
||||
match line_content.trim() {
|
||||
"esac" | "done" | "fi" | "}" => {
|
||||
self.calc_indent_level();
|
||||
if self.auto_indent_level < before {
|
||||
let delta = before - self.auto_indent_level;
|
||||
let line_start = self.start_of_line();
|
||||
for _ in 0..delta {
|
||||
if self.grapheme_at(line_start).is_some_and(|gr| gr == "\t") {
|
||||
self.remove(line_start);
|
||||
if !self.cursor_at_max() {
|
||||
self.cursor.sub(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let before_escaped = self.indent_ctx.in_escaped_line;
|
||||
let before = self.indent_ctx.depth();
|
||||
if read_shopts(|o| o.prompt.auto_indent) {
|
||||
let after = self.calc_indent_level();
|
||||
// Only dedent if the depth decrease came from a closer, not from
|
||||
// a line continuation bonus going away
|
||||
if after < before
|
||||
&& !(before_escaped && !self.indent_ctx.in_escaped_line) {
|
||||
let delta = before - after;
|
||||
let line_start = self.start_of_line();
|
||||
for _ in 0..delta {
|
||||
if self.grapheme_at(line_start).is_some_and(|gr| gr == "\t") {
|
||||
self.remove(line_start);
|
||||
if !self.cursor_at_max() {
|
||||
self.cursor.sub(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn verb_insert(&mut self, string: String) {
|
||||
self.insert_str_at_cursor(&string);
|
||||
let graphemes = string.graphemes(true).count();
|
||||
self.cursor.add(graphemes);
|
||||
}
|
||||
#[allow(clippy::unnecessary_to_owned)]
|
||||
fn verb_indent(&mut self, motion: MotionKind) -> ShResult<()> {
|
||||
let Some((start, end)) = self.range_from_motion(&motion) else {
|
||||
return Ok(());
|
||||
@@ -2975,6 +3038,7 @@ impl LineBuf {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[allow(clippy::unnecessary_to_owned)]
|
||||
fn verb_dedent(&mut self, motion: MotionKind) -> ShResult<()> {
|
||||
let Some((start, mut end)) = self.range_from_motion(&motion) else {
|
||||
return Ok(());
|
||||
@@ -3017,8 +3081,8 @@ impl LineBuf {
|
||||
Anchor::After => {
|
||||
self.push('\n');
|
||||
if auto_indent {
|
||||
self.calc_indent_level();
|
||||
for _ in 0..self.auto_indent_level {
|
||||
let depth = self.calc_indent_level();
|
||||
for _ in 0..depth {
|
||||
self.push('\t');
|
||||
}
|
||||
}
|
||||
@@ -3027,8 +3091,8 @@ impl LineBuf {
|
||||
}
|
||||
Anchor::Before => {
|
||||
if auto_indent {
|
||||
self.calc_indent_level();
|
||||
for _ in 0..self.auto_indent_level {
|
||||
let depth = self.calc_indent_level();
|
||||
for _ in 0..depth {
|
||||
self.insert_at(0, '\t');
|
||||
}
|
||||
}
|
||||
@@ -3055,8 +3119,8 @@ impl LineBuf {
|
||||
self.insert_at_cursor('\n');
|
||||
self.cursor.add(1);
|
||||
if auto_indent {
|
||||
self.calc_indent_level();
|
||||
for _ in 0..self.auto_indent_level {
|
||||
let depth = self.calc_indent_level();
|
||||
for _ in 0..depth {
|
||||
self.insert_at_cursor('\t');
|
||||
self.cursor.add(1);
|
||||
}
|
||||
@@ -3198,7 +3262,6 @@ impl LineBuf {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[allow(clippy::unnecessary_to_owned)]
|
||||
pub fn exec_verb(
|
||||
&mut self,
|
||||
verb: Verb,
|
||||
@@ -3285,10 +3348,10 @@ impl LineBuf {
|
||||
|
||||
/*
|
||||
* Let's evaluate the motion now
|
||||
* If we got some weird command like 'dvw' we will have to simulate a visual
|
||||
* selection to get the range If motion is None, we will try to use
|
||||
* self.select_range If self.select_range is None, we will use
|
||||
* MotionKind::Null
|
||||
* If we got some weird command like 'dvw' we will
|
||||
* have to simulate a visual selection to get the range
|
||||
* If motion is None, we will try to use self.select_range
|
||||
* If self.select_range is None, we will use MotionKind::Null
|
||||
*/
|
||||
let motion_eval =
|
||||
if flags.intersects(CmdFlags::VISUAL | CmdFlags::VISUAL_LINE | CmdFlags::VISUAL_BLOCK) {
|
||||
|
||||
@@ -253,7 +253,6 @@ pub struct ShedVi {
|
||||
pub repeat_action: Option<CmdReplay>,
|
||||
pub repeat_motion: Option<MotionCmd>,
|
||||
pub editor: LineBuf,
|
||||
pub next_is_escaped: bool,
|
||||
|
||||
pub old_layout: Option<Layout>,
|
||||
pub history: History,
|
||||
@@ -271,7 +270,6 @@ impl ShedVi {
|
||||
completer: Box::new(FuzzyCompleter::default()),
|
||||
highlighter: Highlighter::new(),
|
||||
mode: Box::new(ViInsert::new()),
|
||||
next_is_escaped: false,
|
||||
saved_mode: None,
|
||||
pending_keymap: Vec::new(),
|
||||
old_layout: None,
|
||||
@@ -303,7 +301,6 @@ impl ShedVi {
|
||||
completer: Box::new(FuzzyCompleter::default()),
|
||||
highlighter: Highlighter::new(),
|
||||
mode: Box::new(ViInsert::new()),
|
||||
next_is_escaped: false,
|
||||
saved_mode: None,
|
||||
pending_keymap: Vec::new(),
|
||||
old_layout: None,
|
||||
@@ -417,7 +414,7 @@ impl ShedVi {
|
||||
LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<_>>>();
|
||||
let lex_result2 =
|
||||
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()) {
|
||||
(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 {
|
||||
// it's an ex mode error
|
||||
self.mode = Box::new(ViNormal::new()) as Box<dyn ViMode>;
|
||||
@@ -834,8 +823,7 @@ impl ShedVi {
|
||||
}
|
||||
|
||||
if cmd.is_submit_action()
|
||||
&& !self.next_is_escaped
|
||||
&& !self.editor.buffer.ends_with('\\')
|
||||
&& !self.editor.cursor_is_escaped()
|
||||
&& (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete))
|
||||
{
|
||||
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() {
|
||||
let insertions = annotate_token(tk);
|
||||
for (pos, marker) in insertions {
|
||||
log::info!("pos: {pos}, marker: {marker:?}");
|
||||
log::info!("before: {annotated:?}");
|
||||
let pos = pos.max(0).min(annotated.len());
|
||||
annotated.insert(pos, marker);
|
||||
}
|
||||
@@ -1623,6 +1613,12 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
||||
|
||||
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
|
||||
&& let Some(marker) = marker_for(&token.class)
|
||||
{
|
||||
|
||||
@@ -2,10 +2,15 @@
|
||||
use std::os::fd::AsRawFd;
|
||||
|
||||
use crate::{
|
||||
readline::{Prompt, ShedVi},
|
||||
readline::{Prompt, ShedVi, annotate_input},
|
||||
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.
|
||||
macro_rules! vi_test {
|
||||
{ $($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) {
|
||||
let g = TestGuard::new();
|
||||
let prompt = Prompt::default();
|
||||
@@ -38,198 +239,225 @@ fn test_vi(initial: &str) -> (ShedVi, TestGuard) {
|
||||
|
||||
// Why can't I marry a programming language
|
||||
vi_test! {
|
||||
vi_dw_basic : "hello world" => "dw" => "world", 0;
|
||||
vi_dw_middle : "one two three" => "wdw" => "one three", 4;
|
||||
vi_dd_whole_line : "hello world" => "dd" => "", 0;
|
||||
vi_x_single : "hello" => "x" => "ello", 0;
|
||||
vi_x_middle : "hello" => "llx" => "helo", 2;
|
||||
vi_X_backdelete : "hello" => "llX" => "hllo", 1;
|
||||
vi_h_motion : "hello" => "$h" => "hello", 3;
|
||||
vi_l_motion : "hello" => "l" => "hello", 1;
|
||||
vi_h_at_start : "hello" => "h" => "hello", 0;
|
||||
vi_l_at_end : "hello" => "$l" => "hello", 4;
|
||||
vi_w_forward : "one two three" => "w" => "one two three", 4;
|
||||
vi_b_backward : "one two three" => "$b" => "one two three", 8;
|
||||
vi_e_end : "one two three" => "e" => "one two three", 2;
|
||||
vi_ge_back_end : "one two three" => "$ge" => "one two three", 6;
|
||||
vi_w_punctuation : "foo.bar baz" => "w" => "foo.bar baz", 3;
|
||||
vi_e_punctuation : "foo.bar baz" => "e" => "foo.bar baz", 2;
|
||||
vi_b_punctuation : "foo.bar baz" => "$b" => "foo.bar baz", 8;
|
||||
vi_w_at_eol : "hello" => "$w" => "hello", 4;
|
||||
vi_b_at_bol : "hello" => "b" => "hello", 0;
|
||||
vi_W_forward : "foo.bar baz" => "W" => "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_gE_back_end : "one two three" => "$gE" => "one two three", 6;
|
||||
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_E_skip_punct : "one-two three" => "E" => "one-two three", 6;
|
||||
vi_dW_big : "foo.bar baz" => "dW" => "baz", 0;
|
||||
vi_cW_big : "foo.bar baz" => "cWx\x1b" => "x baz", 0;
|
||||
vi_zero_bol : " hello" => "$0" => " hello", 0;
|
||||
vi_caret_first_char : " hello" => "$^" => " hello", 2;
|
||||
vi_dollar_eol : "hello world" => "$" => "hello world", 10;
|
||||
vi_g_last_nonws : "hello " => "g_" => "hello ", 4;
|
||||
vi_g_no_trailing : "hello" => "g_" => "hello", 4;
|
||||
vi_pipe_column : "hello world" => "6|" => "hello world", 5;
|
||||
vi_pipe_col1 : "hello world" => "1|" => "hello world", 0;
|
||||
vi_I_insert_front : " hello" => "Iworld \x1b" => " world hello", 7;
|
||||
vi_A_append_end : "hello" => "A world\x1b" => "hello world", 10;
|
||||
vi_f_find : "hello world" => "fo" => "hello world", 4;
|
||||
vi_F_find_back : "hello world" => "$Fo" => "hello world", 7;
|
||||
vi_t_till : "hello world" => "tw" => "hello world", 5;
|
||||
vi_T_till_back : "hello world" => "$To" => "hello world", 8;
|
||||
vi_f_no_match : "hello" => "fz" => "hello", 0;
|
||||
vi_semicolon_repeat : "abcabc" => "fa;;" => "abcabc", 3;
|
||||
vi_comma_reverse : "abcabc" => "fa;;," => "abcabc", 0;
|
||||
vi_df_semicolon : "abcabc" => "fa;;dfa" => "abcabc", 3;
|
||||
vi_t_at_target : "aab" => "lta" => "aab", 1;
|
||||
vi_D_to_end : "hello world" => "wD" => "hello ", 5;
|
||||
vi_d_dollar : "hello world" => "wd$" => "hello ", 5;
|
||||
vi_d0_to_start : "hello world" => "$d0" => "d", 0;
|
||||
vi_dw_multiple : "one two three" => "d2w" => "three", 0;
|
||||
vi_dt_char : "hello world" => "dtw" => "world", 0;
|
||||
vi_df_char : "hello world" => "dfw" => "orld", 0;
|
||||
vi_dh_back : "hello" => "lldh" => "hllo", 1;
|
||||
vi_dl_forward : "hello" => "dl" => "ello", 0;
|
||||
vi_dge_back_end : "one two three" => "$dge" => "one tw", 5;
|
||||
vi_dG_to_end : "hello world" => "dG" => "", 0;
|
||||
vi_dgg_to_start : "hello world" => "$dgg" => "", 0;
|
||||
vi_d_semicolon : "abcabc" => "fad;" => "abcabc", 3;
|
||||
vi_cw_basic : "hello world" => "cwfoo\x1b" => "foo world", 2;
|
||||
vi_C_to_end : "hello world" => "wCfoo\x1b" => "hello foo", 8;
|
||||
vi_cc_whole : "hello world" => "ccfoo\x1b" => "foo", 2;
|
||||
vi_ct_char : "hello world" => "ctwfoo\x1b" => "fooworld", 2;
|
||||
vi_s_single : "hello" => "sfoo\x1b" => "fooello", 2;
|
||||
vi_S_whole_line : "hello world" => "Sfoo\x1b" => "foo", 2;
|
||||
vi_cl_forward : "hello" => "clX\x1b" => "Xello", 0;
|
||||
vi_ch_backward : "hello" => "llchX\x1b" => "hXllo", 1;
|
||||
vi_cb_word_back : "hello world" => "$cbfoo\x1b" => "hello food", 8;
|
||||
vi_ce_word_end : "hello world" => "cefoo\x1b" => "foo world", 2;
|
||||
vi_c0_to_start : "hello world" => "wc0foo\x1b" => "fooworld", 2;
|
||||
vi_yw_p_basic : "hello world" => "ywwP" => "hello hello world", 11;
|
||||
vi_dw_p_paste : "hello world" => "dwP" => "hello world", 5;
|
||||
vi_dd_p_paste : "hello world" => "ddp" => "\nhello world", 1;
|
||||
vi_y_dollar_p : "hello world" => "wy$P" => "hello worldworld", 10;
|
||||
vi_ye_p : "hello world" => "yewP" => "hello helloworld", 10;
|
||||
vi_yy_p : "hello world" => "yyp" => "hello world\nhello world", 12;
|
||||
vi_Y_p : "hello world" => "Yp" => "hhello worldello world", 11;
|
||||
vi_p_after_x : "hello" => "xp" => "ehllo", 1;
|
||||
vi_P_before : "hello" => "llxP" => "hello", 2;
|
||||
vi_paste_empty : "hello" => "p" => "hello", 0;
|
||||
vi_r_replace : "hello" => "ra" => "aello", 0;
|
||||
vi_r_middle : "hello" => "llra" => "healo", 2;
|
||||
vi_r_at_end : "hello" => "$ra" => "hella", 4;
|
||||
vi_r_space : "hello" => "r " => " ello", 0;
|
||||
vi_r_with_count : "hello" => "3rx" => "xxxlo", 2;
|
||||
vi_tilde_single : "hello" => "~" => "Hello", 1;
|
||||
vi_tilde_count : "hello" => "3~" => "HELlo", 3;
|
||||
vi_tilde_at_end : "HELLO" => "$~" => "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_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_gtilde_word : "hello WORLD" => "g~w" => "HELLO WORLD", 0;
|
||||
vi_gtilde_dollar : "hello WORLD" => "g~$" => "HELLO world", 0;
|
||||
vi_diw_inner : "one two three" => "wdiw" => "one three", 4;
|
||||
vi_ciw_replace : "hello world" => "ciwfoo\x1b" => "foo world", 2;
|
||||
vi_daw_around : "one two three" => "wdaw" => "one three", 4;
|
||||
vi_yiw_p : "hello world" => "yiwAp \x1bp" => "hello worldp hello", 17;
|
||||
vi_diW_big_inner : "one-two three" => "diW" => " three", 0;
|
||||
vi_daW_big_around : "one two-three end" => "wdaW" => "one end", 4;
|
||||
vi_ciW_big : "one-two three" => "ciWx\x1b" => "x three", 0;
|
||||
vi_di_dquote : "one \"two\" three" => "f\"di\"" => "one \"\" three", 5;
|
||||
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_di_squote : "one 'two' three" => "f'di'" => "one '' three", 5;
|
||||
vi_da_squote : "one 'two' three" => "f'da'" => "one three", 4;
|
||||
vi_di_backtick : "one `two` three" => "f`di`" => "one `` three", 5;
|
||||
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_di_paren : "one (two) three" => "f(di(" => "one () three", 5;
|
||||
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_di_brace : "one {two} three" => "f{di{" => "one {} three", 5;
|
||||
vi_da_brace : "one {two} three" => "f{da{" => "one three", 4;
|
||||
vi_di_bracket : "one [two] three" => "f[di[" => "one [] three", 5;
|
||||
vi_da_bracket : "one [two] three" => "f[da[" => "one three", 4;
|
||||
vi_di_angle : "one <two> three" => "f<di<" => "one <> three", 5;
|
||||
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_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_percent_paren : "(hello) world" => "%" => "(hello) world", 6;
|
||||
vi_percent_brace : "{hello} world" => "%" => "{hello} world", 6;
|
||||
vi_percent_bracket : "[hello] world" => "%" => "[hello] world", 6;
|
||||
vi_percent_from_close: "(hello) world" => "f)%" => "(hello) world", 0;
|
||||
vi_d_percent_paren : "(hello) world" => "d%" => " world", 0;
|
||||
vi_i_insert : "hello" => "iX\x1b" => "Xhello", 0;
|
||||
vi_a_append : "hello" => "aX\x1b" => "hXello", 1;
|
||||
vi_I_front : " hello" => "IX\x1b" => " Xhello", 2;
|
||||
vi_A_end : "hello" => "AX\x1b" => "helloX", 5;
|
||||
vi_o_open_below : "hello" => "oworld\x1b" => "hello\nworld", 10;
|
||||
vi_O_open_above : "hello" => "Oworld\x1b" => "world\nhello", 4;
|
||||
vi_empty_input : "" => "i hello\x1b" => " hello", 5;
|
||||
vi_insert_escape : "hello" => "aX\x1b" => "hXello", 1;
|
||||
vi_ctrl_w_del_word : "hello world" => "A\x17\x1b" => "hello ", 5;
|
||||
vi_ctrl_h_backspace : "hello" => "A\x08\x1b" => "hell", 3;
|
||||
vi_u_undo_delete : "hello world" => "dwu" => "hello world", 0;
|
||||
vi_u_undo_change : "hello world" => "ciwfoo\x1bu" => "hello world", 0;
|
||||
vi_u_undo_x : "hello" => "xu" => "hello", 0;
|
||||
vi_ctrl_r_redo : "hello" => "xu\x12" => "ello", 0;
|
||||
vi_u_multiple : "hello world" => "xdwu" => "ello world", 0;
|
||||
vi_redo_after_undo : "hello world" => "dwu\x12" => "world", 0;
|
||||
vi_dot_repeat_x : "hello" => "x." => "llo", 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_r : "hello" => "ra.." => "aello", 0;
|
||||
vi_dot_repeat_s : "hello" => "sX\x1bl." => "XXllo", 1;
|
||||
vi_count_h : "hello world" => "$3h" => "hello world", 7;
|
||||
vi_count_l : "hello world" => "3l" => "hello world", 3;
|
||||
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_x : "hello" => "3x" => "lo", 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_count_s : "hello" => "3sX\x1b" => "Xlo", 0;
|
||||
vi_indent_line : "hello" => ">>" => "\thello", 1;
|
||||
vi_dedent_line : "\thello" => "<<" => "hello", 0;
|
||||
vi_indent_double : "hello" => ">>>>" => "\t\thello", 2;
|
||||
vi_J_join_lines : "hello\nworld" => "J" => "hello world", 5;
|
||||
vi_v_u_lower : "HELLO" => "vlllu" => "hellO", 0;
|
||||
vi_v_U_upper : "hello" => "vlllU" => "HELLo", 0;
|
||||
vi_v_d_delete : "hello world" => "vwwd" => "", 0;
|
||||
vi_v_x_delete : "hello world" => "vwwx" => "", 0;
|
||||
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_dollar_d : "hello world" => "wv$d" => "hello ", 5;
|
||||
vi_v_0_d : "hello world" => "$v0d" => "", 0;
|
||||
vi_ve_d : "hello world" => "ved" => " world", 0;
|
||||
vi_v_o_swap : "hello world" => "vllod" => "lo world", 0;
|
||||
vi_v_r_replace : "hello" => "vlllrx" => "xxxxo", 0;
|
||||
vi_v_tilde_case : "hello" => "vlll~" => "HELLo", 0;
|
||||
vi_V_d_delete : "hello world" => "Vd" => "", 0;
|
||||
vi_V_y_p : "hello world" => "Vyp" => "hello world\nhello world", 12;
|
||||
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_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_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_width : "num -00001 end" => "w\x01" => "num 00000 end", 4;
|
||||
vi_delete_empty : "" => "x" => "", 0;
|
||||
vi_undo_on_empty : "" => "u" => "", 0;
|
||||
vi_w_single_char : "a b c" => "w" => "a b c", 2;
|
||||
vi_dw_last_word : "hello" => "dw" => "", 0;
|
||||
vi_dollar_single : "h" => "$" => "h", 0;
|
||||
vi_caret_no_ws : "hello" => "$^" => "hello", 0;
|
||||
vi_f_last_char : "hello" => "fo" => "hello", 4;
|
||||
vi_r_on_space : "hello world" => "5|r-" => "hell- world", 4;
|
||||
vi_vw_doesnt_crash : "" => "vw" => "", 0;
|
||||
vi_indent_cursor_pos : "echo foo" => ">>" => "\techo foo", 1;
|
||||
vi_dw_basic : "hello world" => "dw" => "world", 0;
|
||||
vi_dw_middle : "one two three" => "wdw" => "one three", 4;
|
||||
vi_dd_whole_line : "hello world" => "dd" => "", 0;
|
||||
vi_x_single : "hello" => "x" => "ello", 0;
|
||||
vi_x_middle : "hello" => "llx" => "helo", 2;
|
||||
vi_X_backdelete : "hello" => "llX" => "hllo", 1;
|
||||
vi_h_motion : "hello" => "$h" => "hello", 3;
|
||||
vi_l_motion : "hello" => "l" => "hello", 1;
|
||||
vi_h_at_start : "hello" => "h" => "hello", 0;
|
||||
vi_l_at_end : "hello" => "$l" => "hello", 4;
|
||||
vi_w_forward : "one two three" => "w" => "one two three", 4;
|
||||
vi_b_backward : "one two three" => "$b" => "one two three", 8;
|
||||
vi_e_end : "one two three" => "e" => "one two three", 2;
|
||||
vi_ge_back_end : "one two three" => "$ge" => "one two three", 6;
|
||||
vi_w_punctuation : "foo.bar baz" => "w" => "foo.bar baz", 3;
|
||||
vi_e_punctuation : "foo.bar baz" => "e" => "foo.bar baz", 2;
|
||||
vi_b_punctuation : "foo.bar baz" => "$b" => "foo.bar baz", 8;
|
||||
vi_w_at_eol : "hello" => "$w" => "hello", 4;
|
||||
vi_b_at_bol : "hello" => "b" => "hello", 0;
|
||||
vi_W_forward : "foo.bar baz" => "W" => "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_gE_back_end : "one two three" => "$gE" => "one two three", 6;
|
||||
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_E_skip_punct : "one-two three" => "E" => "one-two three", 6;
|
||||
vi_dW_big : "foo.bar baz" => "dW" => "baz", 0;
|
||||
vi_cW_big : "foo.bar baz" => "cWx\x1b" => "x baz", 0;
|
||||
vi_zero_bol : " hello" => "$0" => " hello", 0;
|
||||
vi_caret_first_char : " hello" => "$^" => " hello", 2;
|
||||
vi_dollar_eol : "hello world" => "$" => "hello world", 10;
|
||||
vi_g_last_nonws : "hello " => "g_" => "hello ", 4;
|
||||
vi_g_no_trailing : "hello" => "g_" => "hello", 4;
|
||||
vi_pipe_column : "hello world" => "6|" => "hello world", 5;
|
||||
vi_pipe_col1 : "hello world" => "1|" => "hello world", 0;
|
||||
vi_I_insert_front : " hello" => "Iworld \x1b" => " world hello", 7;
|
||||
vi_A_append_end : "hello" => "A world\x1b" => "hello world", 10;
|
||||
vi_f_find : "hello world" => "fo" => "hello world", 4;
|
||||
vi_F_find_back : "hello world" => "$Fo" => "hello world", 7;
|
||||
vi_t_till : "hello world" => "tw" => "hello world", 5;
|
||||
vi_T_till_back : "hello world" => "$To" => "hello world", 8;
|
||||
vi_f_no_match : "hello" => "fz" => "hello", 0;
|
||||
vi_semicolon_repeat : "abcabc" => "fa;;" => "abcabc", 3;
|
||||
vi_comma_reverse : "abcabc" => "fa;;," => "abcabc", 0;
|
||||
vi_df_semicolon : "abcabc" => "fa;;dfa" => "abcabc", 3;
|
||||
vi_t_at_target : "aab" => "lta" => "aab", 1;
|
||||
vi_D_to_end : "hello world" => "wD" => "hello ", 5;
|
||||
vi_d_dollar : "hello world" => "wd$" => "hello ", 5;
|
||||
vi_d0_to_start : "hello world" => "$d0" => "d", 0;
|
||||
vi_dw_multiple : "one two three" => "d2w" => "three", 0;
|
||||
vi_dt_char : "hello world" => "dtw" => "world", 0;
|
||||
vi_df_char : "hello world" => "dfw" => "orld", 0;
|
||||
vi_dh_back : "hello" => "lldh" => "hllo", 1;
|
||||
vi_dl_forward : "hello" => "dl" => "ello", 0;
|
||||
vi_dge_back_end : "one two three" => "$dge" => "one tw", 5;
|
||||
vi_dG_to_end : "hello world" => "dG" => "", 0;
|
||||
vi_dgg_to_start : "hello world" => "$dgg" => "", 0;
|
||||
vi_d_semicolon : "abcabc" => "fad;" => "abcabc", 3;
|
||||
vi_cw_basic : "hello world" => "cwfoo\x1b" => "foo world", 2;
|
||||
vi_C_to_end : "hello world" => "wCfoo\x1b" => "hello foo", 8;
|
||||
vi_cc_whole : "hello world" => "ccfoo\x1b" => "foo", 2;
|
||||
vi_ct_char : "hello world" => "ctwfoo\x1b" => "fooworld", 2;
|
||||
vi_s_single : "hello" => "sfoo\x1b" => "fooello", 2;
|
||||
vi_S_whole_line : "hello world" => "Sfoo\x1b" => "foo", 2;
|
||||
vi_cl_forward : "hello" => "clX\x1b" => "Xello", 0;
|
||||
vi_ch_backward : "hello" => "llchX\x1b" => "hXllo", 1;
|
||||
vi_cb_word_back : "hello world" => "$cbfoo\x1b" => "hello food", 8;
|
||||
vi_ce_word_end : "hello world" => "cefoo\x1b" => "foo world", 2;
|
||||
vi_c0_to_start : "hello world" => "wc0foo\x1b" => "fooworld", 2;
|
||||
vi_yw_p_basic : "hello world" => "ywwP" => "hello hello world", 11;
|
||||
vi_dw_p_paste : "hello world" => "dwP" => "hello world", 5;
|
||||
vi_dd_p_paste : "hello world" => "ddp" => "\nhello world", 1;
|
||||
vi_y_dollar_p : "hello world" => "wy$P" => "hello worldworld", 10;
|
||||
vi_ye_p : "hello world" => "yewP" => "hello helloworld", 10;
|
||||
vi_yy_p : "hello world" => "yyp" => "hello world\nhello world", 12;
|
||||
vi_Y_p : "hello world" => "Yp" => "hhello worldello world", 11;
|
||||
vi_p_after_x : "hello" => "xp" => "ehllo", 1;
|
||||
vi_P_before : "hello" => "llxP" => "hello", 2;
|
||||
vi_paste_empty : "hello" => "p" => "hello", 0;
|
||||
vi_r_replace : "hello" => "ra" => "aello", 0;
|
||||
vi_r_middle : "hello" => "llra" => "healo", 2;
|
||||
vi_r_at_end : "hello" => "$ra" => "hella", 4;
|
||||
vi_r_space : "hello" => "r " => " ello", 0;
|
||||
vi_r_with_count : "hello" => "3rx" => "xxxlo", 2;
|
||||
vi_tilde_single : "hello" => "~" => "Hello", 1;
|
||||
vi_tilde_count : "hello" => "3~" => "HELlo", 3;
|
||||
vi_tilde_at_end : "HELLO" => "$~" => "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_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_gtilde_word : "hello WORLD" => "g~w" => "HELLO WORLD", 0;
|
||||
vi_gtilde_dollar : "hello WORLD" => "g~$" => "HELLO world", 0;
|
||||
vi_diw_inner : "one two three" => "wdiw" => "one three", 4;
|
||||
vi_ciw_replace : "hello world" => "ciwfoo\x1b" => "foo world", 2;
|
||||
vi_daw_around : "one two three" => "wdaw" => "one three", 4;
|
||||
vi_yiw_p : "hello world" => "yiwAp \x1bp" => "hello worldp hello", 17;
|
||||
vi_diW_big_inner : "one-two three" => "diW" => " three", 0;
|
||||
vi_daW_big_around : "one two-three end" => "wdaW" => "one end", 4;
|
||||
vi_ciW_big : "one-two three" => "ciWx\x1b" => "x three", 0;
|
||||
vi_di_dquote : "one \"two\" three" => "f\"di\"" => "one \"\" three", 5;
|
||||
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_di_squote : "one 'two' three" => "f'di'" => "one '' three", 5;
|
||||
vi_da_squote : "one 'two' three" => "f'da'" => "one three", 4;
|
||||
vi_di_backtick : "one `two` three" => "f`di`" => "one `` three", 5;
|
||||
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_di_paren : "one (two) three" => "f(di(" => "one () three", 5;
|
||||
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_di_brace : "one {two} three" => "f{di{" => "one {} three", 5;
|
||||
vi_da_brace : "one {two} three" => "f{da{" => "one three", 4;
|
||||
vi_di_bracket : "one [two] three" => "f[di[" => "one [] three", 5;
|
||||
vi_da_bracket : "one [two] three" => "f[da[" => "one three", 4;
|
||||
vi_di_angle : "one <two> three" => "f<di<" => "one <> three", 5;
|
||||
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_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_percent_paren : "(hello) world" => "%" => "(hello) world", 6;
|
||||
vi_percent_brace : "{hello} world" => "%" => "{hello} world", 6;
|
||||
vi_percent_bracket : "[hello] world" => "%" => "[hello] world", 6;
|
||||
vi_percent_from_close: "(hello) world" => "f)%" => "(hello) world", 0;
|
||||
vi_d_percent_paren : "(hello) world" => "d%" => " world", 0;
|
||||
vi_i_insert : "hello" => "iX\x1b" => "Xhello", 0;
|
||||
vi_a_append : "hello" => "aX\x1b" => "hXello", 1;
|
||||
vi_I_front : " hello" => "IX\x1b" => " Xhello", 2;
|
||||
vi_A_end : "hello" => "AX\x1b" => "helloX", 5;
|
||||
vi_o_open_below : "hello" => "oworld\x1b" => "hello\nworld", 10;
|
||||
vi_O_open_above : "hello" => "Oworld\x1b" => "world\nhello", 4;
|
||||
vi_empty_input : "" => "i hello\x1b" => " hello", 5;
|
||||
vi_insert_escape : "hello" => "aX\x1b" => "hXello", 1;
|
||||
vi_ctrl_w_del_word : "hello world" => "A\x17\x1b" => "hello ", 5;
|
||||
vi_ctrl_h_backspace : "hello" => "A\x08\x1b" => "hell", 3;
|
||||
vi_u_undo_delete : "hello world" => "dwu" => "hello world", 0;
|
||||
vi_u_undo_change : "hello world" => "ciwfoo\x1bu" => "hello world", 0;
|
||||
vi_u_undo_x : "hello" => "xu" => "hello", 0;
|
||||
vi_ctrl_r_redo : "hello" => "xu\x12" => "ello", 0;
|
||||
vi_u_multiple : "hello world" => "xdwu" => "ello world", 0;
|
||||
vi_redo_after_undo : "hello world" => "dwu\x12" => "world", 0;
|
||||
vi_dot_repeat_x : "hello" => "x." => "llo", 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_r : "hello" => "ra.." => "aello", 0;
|
||||
vi_dot_repeat_s : "hello" => "sX\x1bl." => "XXllo", 1;
|
||||
vi_count_h : "hello world" => "$3h" => "hello world", 7;
|
||||
vi_count_l : "hello world" => "3l" => "hello world", 3;
|
||||
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_x : "hello" => "3x" => "lo", 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_count_s : "hello" => "3sX\x1b" => "Xlo", 0;
|
||||
vi_indent_line : "hello" => ">>" => "\thello", 1;
|
||||
vi_dedent_line : "\thello" => "<<" => "hello", 0;
|
||||
vi_indent_double : "hello" => ">>>>" => "\t\thello", 2;
|
||||
vi_J_join_lines : "hello\nworld" => "J" => "hello world", 5;
|
||||
vi_v_u_lower : "HELLO" => "vlllu" => "hellO", 0;
|
||||
vi_v_U_upper : "hello" => "vlllU" => "HELLo", 0;
|
||||
vi_v_d_delete : "hello world" => "vwwd" => "", 0;
|
||||
vi_v_x_delete : "hello world" => "vwwx" => "", 0;
|
||||
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_dollar_d : "hello world" => "wv$d" => "hello ", 5;
|
||||
vi_v_0_d : "hello world" => "$v0d" => "", 0;
|
||||
vi_ve_d : "hello world" => "ved" => " world", 0;
|
||||
vi_v_o_swap : "hello world" => "vllod" => "lo world", 0;
|
||||
vi_v_r_replace : "hello" => "vlllrx" => "xxxxo", 0;
|
||||
vi_v_tilde_case : "hello" => "vlll~" => "HELLo", 0;
|
||||
vi_V_d_delete : "hello world" => "Vd" => "", 0;
|
||||
vi_V_y_p : "hello world" => "Vyp" => "hello world\nhello world", 12;
|
||||
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_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_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_width : "num -00001 end" => "w\x01" => "num 00000 end", 4;
|
||||
vi_delete_empty : "" => "x" => "", 0;
|
||||
vi_undo_on_empty : "" => "u" => "", 0;
|
||||
vi_w_single_char : "a b c" => "w" => "a b c", 2;
|
||||
vi_dw_last_word : "hello" => "dw" => "", 0;
|
||||
vi_dollar_single : "h" => "$" => "h", 0;
|
||||
vi_caret_no_ws : "hello" => "$^" => "hello", 0;
|
||||
vi_f_last_char : "hello" => "fo" => "hello", 4;
|
||||
vi_r_on_space : "hello world" => "5|r-" => "hell- world", 4;
|
||||
vi_vw_doesnt_crash : "" => "vw" => "", 0;
|
||||
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
|
||||
}
|
||||
|
||||
#[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}"
|
||||
);
|
||||
}
|
||||
|
||||
19
src/shopt.rs
19
src/shopt.rs
@@ -146,6 +146,7 @@ pub struct ShOptCore {
|
||||
pub bell_enabled: bool,
|
||||
pub max_recurse_depth: usize,
|
||||
pub xpg_echo: bool,
|
||||
pub noclobber: bool,
|
||||
}
|
||||
|
||||
impl ShOptCore {
|
||||
@@ -238,6 +239,15 @@ impl ShOptCore {
|
||||
};
|
||||
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(
|
||||
ShErrKind::SyntaxErr,
|
||||
@@ -304,6 +314,11 @@ impl ShOptCore {
|
||||
output.push_str(&format!("{}", self.xpg_echo));
|
||||
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(
|
||||
ShErrKind::SyntaxErr,
|
||||
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!("max_recurse_depth = {}", self.max_recurse_depth));
|
||||
output.push(format!("xpg_echo = {}", self.xpg_echo));
|
||||
output.push(format!("noclobber = {}", self.noclobber));
|
||||
|
||||
let final_output = output.join("\n");
|
||||
|
||||
@@ -346,6 +362,7 @@ impl Default for ShOptCore {
|
||||
bell_enabled: true,
|
||||
max_recurse_depth: 1000,
|
||||
xpg_echo: false,
|
||||
noclobber: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -589,6 +606,7 @@ mod tests {
|
||||
bell_enabled,
|
||||
max_recurse_depth,
|
||||
xpg_echo,
|
||||
noclobber,
|
||||
} = ShOptCore::default();
|
||||
// If a field is added to the struct, this destructure fails to compile.
|
||||
let _ = (
|
||||
@@ -601,6 +619,7 @@ mod tests {
|
||||
bell_enabled,
|
||||
max_recurse_depth,
|
||||
xpg_echo,
|
||||
noclobber,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
18
src/state.rs
18
src/state.rs
@@ -1330,6 +1330,15 @@ impl VarTab {
|
||||
.get(&ShellParam::Status)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or("0".into()),
|
||||
ShellParam::AllArgsStr => {
|
||||
let ifs = get_separator();
|
||||
self
|
||||
.params
|
||||
.get(&ShellParam::AllArgs)
|
||||
.map(|s| s.replace(markers::ARG_SEP, &ifs).to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
_ => self
|
||||
.params
|
||||
.get(¶m)
|
||||
@@ -1842,6 +1851,15 @@ pub fn change_dir<P: AsRef<Path>>(dir: P) -> ShResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_separator() -> String {
|
||||
env::var("IFS")
|
||||
.unwrap_or(String::from(" "))
|
||||
.chars()
|
||||
.next()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn get_status() -> i32 {
|
||||
read_vars(|v| v.get_param(ShellParam::Status))
|
||||
.parse::<i32>()
|
||||
|
||||
@@ -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()) }
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ impl crate::parse::Node {
|
||||
if offender.is_none()
|
||||
&& expected_rule
|
||||
.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));
|
||||
} else if offender.is_none() {
|
||||
|
||||
Reference in New Issue
Block a user