tightened up some logic with indenting and joining lines

added more linebuf tests

extracted all verb match arms into private methods on LineBuf
This commit is contained in:
2026-03-13 19:24:30 -04:00
parent 13227943c6
commit 307386ffc6
43 changed files with 3783 additions and 3408 deletions

View File

@@ -38,28 +38,30 @@ pub fn alias(node: Node) -> ShResult<()> {
write(stdout, alias_output.as_bytes())?; // Write it write(stdout, alias_output.as_bytes())?; // Write it
} else { } else {
for (arg, span) in argv { for (arg, span) in argv {
let Some((name, body)) = arg.split_once('=') else { let Some((name, body)) = arg.split_once('=') else {
let Some(alias) = read_logic(|l| l.get_alias(&arg)) else { let Some(alias) = read_logic(|l| l.get_alias(&arg)) else {
return Err(ShErr::at( return Err(ShErr::at(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
span, span,
"alias: Expected an assignment in alias args", "alias: Expected an assignment in alias args",
)); ));
}; };
let alias_output = format!("{arg}='{alias}'"); let alias_output = format!("{arg}='{alias}'");
let stdout = borrow_fd(STDOUT_FILENO); let stdout = borrow_fd(STDOUT_FILENO);
write(stdout, alias_output.as_bytes())?; // Write it write(stdout, alias_output.as_bytes())?; // Write it
state::set_status(0); state::set_status(0);
return Ok(()); return Ok(());
}; };
if name == "command" || name == "builtin" { if name == "command" || name == "builtin" {
return Err(ShErr::at( return Err(ShErr::at(
ShErrKind::ExecFail, ShErrKind::ExecFail,
span, span,
format!("alias: Cannot assign alias to reserved name '{}'", name.fg(next_color())), format!(
"alias: Cannot assign alias to reserved name '{}'",
name.fg(next_color())
),
)); ));
} }
write_logic(|l| l.insert_alias(name, body, span.clone())); write_logic(|l| l.insert_alias(name, body, span.clone()));
@@ -118,7 +120,7 @@ pub fn unalias(node: Node) -> ShResult<()> {
mod tests { mod tests {
use crate::state::{self, read_logic}; use crate::state::{self, read_logic};
use crate::testutil::{TestGuard, test_input}; use crate::testutil::{TestGuard, test_input};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
#[test] #[test]
fn alias_set_and_expand() { fn alias_set_and_expand() {

View File

@@ -229,9 +229,9 @@ pub fn get_arr_op_opts(opts: Vec<Opt>) -> ShResult<ArrOpOpts> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::VecDeque; use crate::state::{self, VarFlags, VarKind, read_vars, write_vars};
use crate::state::{self, read_vars, write_vars, VarFlags, VarKind};
use crate::testutil::{TestGuard, test_input}; use crate::testutil::{TestGuard, test_input};
use std::collections::VecDeque;
fn set_arr(name: &str, elems: &[&str]) { fn set_arr(name: &str, elems: &[&str]) {
let arr = VecDeque::from_iter(elems.iter().map(|s| s.to_string())); let arr = VecDeque::from_iter(elems.iter().map(|s| s.to_string()));

View File

@@ -159,7 +159,10 @@ mod tests {
test_input("autocmd post-cmd 'echo post'").unwrap(); test_input("autocmd post-cmd 'echo post'").unwrap();
assert_eq!(read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd)).len(), 1); assert_eq!(read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd)).len(), 1);
assert_eq!(read_logic(|l| l.get_autocmds(AutoCmdKind::PostCmd)).len(), 1); assert_eq!(
read_logic(|l| l.get_autocmds(AutoCmdKind::PostCmd)).len(),
1
);
} }
// ===================== Pattern ===================== // ===================== Pattern =====================
@@ -205,7 +208,10 @@ mod tests {
test_input("autocmd -c pre-cmd").unwrap(); test_input("autocmd -c pre-cmd").unwrap();
assert_eq!(read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd)).len(), 0); assert_eq!(read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd)).len(), 0);
assert_eq!(read_logic(|l| l.get_autocmds(AutoCmdKind::PostCmd)).len(), 1); assert_eq!(
read_logic(|l| l.get_autocmds(AutoCmdKind::PostCmd)).len(),
1
);
} }
#[test] #[test]
@@ -245,12 +251,22 @@ mod tests {
fn all_kinds_parse() { fn all_kinds_parse() {
let _guard = TestGuard::new(); let _guard = TestGuard::new();
let kinds = [ let kinds = [
"pre-cmd", "post-cmd", "pre-change-dir", "post-change-dir", "pre-cmd",
"on-job-finish", "pre-prompt", "post-prompt", "post-cmd",
"pre-mode-change", "post-mode-change", "pre-change-dir",
"on-history-open", "on-history-close", "on-history-select", "post-change-dir",
"on-completion-start", "on-completion-cancel", "on-completion-select", "on-job-finish",
"on-exit" "pre-prompt",
"post-prompt",
"pre-mode-change",
"post-mode-change",
"on-history-open",
"on-history-close",
"on-history-select",
"on-completion-start",
"on-completion-cancel",
"on-completion-select",
"on-exit",
]; ];
for kind in kinds { for kind in kinds {
test_input(format!("autocmd {kind} 'true'")).unwrap(); test_input(format!("autocmd {kind} 'true'")).unwrap();

View File

@@ -78,159 +78,165 @@ pub fn cd(node: Node) -> ShResult<()> {
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use std::env; use std::env;
use std::fs; use std::fs;
use tempfile::TempDir; use tempfile::TempDir;
use crate::state; use crate::state;
use crate::testutil::{TestGuard, test_input}; use crate::testutil::{TestGuard, test_input};
// ===================== Basic Navigation ===================== // ===================== Basic Navigation =====================
#[test] #[test]
fn cd_simple() { fn cd_simple() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let old_dir = env::current_dir().unwrap(); let old_dir = env::current_dir().unwrap();
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
test_input(format!("cd {}", temp_dir.path().display())).unwrap(); test_input(format!("cd {}", temp_dir.path().display())).unwrap();
let new_dir = env::current_dir().unwrap(); let new_dir = env::current_dir().unwrap();
assert_ne!(old_dir, new_dir); assert_ne!(old_dir, new_dir);
assert_eq!(new_dir.display().to_string(), temp_dir.path().display().to_string()); assert_eq!(
} new_dir.display().to_string(),
temp_dir.path().display().to_string()
);
}
#[test] #[test]
fn cd_no_args_goes_home() { fn cd_no_args_goes_home() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
unsafe { env::set_var("HOME", temp_dir.path()) }; unsafe { env::set_var("HOME", temp_dir.path()) };
test_input("cd").unwrap(); test_input("cd").unwrap();
let cwd = env::current_dir().unwrap(); let cwd = env::current_dir().unwrap();
assert_eq!(cwd.display().to_string(), temp_dir.path().display().to_string()); assert_eq!(
} cwd.display().to_string(),
temp_dir.path().display().to_string()
);
}
#[test] #[test]
fn cd_relative_path() { fn cd_relative_path() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let sub = temp_dir.path().join("child"); let sub = temp_dir.path().join("child");
fs::create_dir(&sub).unwrap(); fs::create_dir(&sub).unwrap();
test_input(format!("cd {}", temp_dir.path().display())).unwrap(); test_input(format!("cd {}", temp_dir.path().display())).unwrap();
test_input("cd child").unwrap(); test_input("cd child").unwrap();
let cwd = env::current_dir().unwrap(); let cwd = env::current_dir().unwrap();
assert_eq!(cwd.display().to_string(), sub.display().to_string()); assert_eq!(cwd.display().to_string(), sub.display().to_string());
} }
// ===================== Environment ===================== // ===================== Environment =====================
#[test] #[test]
fn cd_sets_pwd_env() { fn cd_sets_pwd_env() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
test_input(format!("cd {}", temp_dir.path().display())).unwrap(); test_input(format!("cd {}", temp_dir.path().display())).unwrap();
let pwd = env::var("PWD").unwrap(); let pwd = env::var("PWD").unwrap();
assert_eq!(pwd, env::current_dir().unwrap().display().to_string()); assert_eq!(pwd, env::current_dir().unwrap().display().to_string());
} }
#[test] #[test]
fn cd_status_zero_on_success() { fn cd_status_zero_on_success() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
test_input(format!("cd {}", temp_dir.path().display())).unwrap(); test_input(format!("cd {}", temp_dir.path().display())).unwrap();
assert_eq!(state::get_status(), 0); assert_eq!(state::get_status(), 0);
} }
// ===================== Error Cases ===================== // ===================== Error Cases =====================
#[test] #[test]
fn cd_nonexistent_dir_fails() { fn cd_nonexistent_dir_fails() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let result = test_input("cd /nonexistent_path_that_does_not_exist_xyz"); let result = test_input("cd /nonexistent_path_that_does_not_exist_xyz");
assert!(result.is_err()); assert!(result.is_err());
} }
#[test] #[test]
fn cd_file_not_directory_fails() { fn cd_file_not_directory_fails() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("afile.txt"); let file_path = temp_dir.path().join("afile.txt");
fs::write(&file_path, "hello").unwrap(); fs::write(&file_path, "hello").unwrap();
let result = test_input(format!("cd {}", file_path.display())); let result = test_input(format!("cd {}", file_path.display()));
assert!(result.is_err()); assert!(result.is_err());
} }
// ===================== Multiple cd ===================== // ===================== Multiple cd =====================
#[test] #[test]
fn cd_multiple_times() { fn cd_multiple_times() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let dir_a = TempDir::new().unwrap(); let dir_a = TempDir::new().unwrap();
let dir_b = TempDir::new().unwrap(); let dir_b = TempDir::new().unwrap();
test_input(format!("cd {}", dir_a.path().display())).unwrap(); test_input(format!("cd {}", dir_a.path().display())).unwrap();
assert_eq!( assert_eq!(
env::current_dir().unwrap().display().to_string(), env::current_dir().unwrap().display().to_string(),
dir_a.path().display().to_string() dir_a.path().display().to_string()
); );
test_input(format!("cd {}", dir_b.path().display())).unwrap(); test_input(format!("cd {}", dir_b.path().display())).unwrap();
assert_eq!( assert_eq!(
env::current_dir().unwrap().display().to_string(), env::current_dir().unwrap().display().to_string(),
dir_b.path().display().to_string() dir_b.path().display().to_string()
); );
} }
#[test] #[test]
fn cd_nested_subdirectories() { fn cd_nested_subdirectories() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let deep = temp_dir.path().join("a").join("b").join("c"); let deep = temp_dir.path().join("a").join("b").join("c");
fs::create_dir_all(&deep).unwrap(); fs::create_dir_all(&deep).unwrap();
test_input(format!("cd {}", deep.display())).unwrap(); test_input(format!("cd {}", deep.display())).unwrap();
assert_eq!( assert_eq!(
env::current_dir().unwrap().display().to_string(), env::current_dir().unwrap().display().to_string(),
deep.display().to_string() deep.display().to_string()
); );
} }
// ===================== Autocmd Integration ===================== // ===================== Autocmd Integration =====================
#[test] #[test]
fn cd_fires_post_change_dir_autocmd() { fn cd_fires_post_change_dir_autocmd() {
let guard = TestGuard::new(); let guard = TestGuard::new();
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
test_input("autocmd post-change-dir 'echo cd-hook-fired'").unwrap(); test_input("autocmd post-change-dir 'echo cd-hook-fired'").unwrap();
guard.read_output(); guard.read_output();
test_input(format!("cd {}", temp_dir.path().display())).unwrap(); test_input(format!("cd {}", temp_dir.path().display())).unwrap();
let out = guard.read_output(); let out = guard.read_output();
assert!(out.contains("cd-hook-fired")); assert!(out.contains("cd-hook-fired"));
} }
#[test] #[test]
fn cd_fires_pre_change_dir_autocmd() { fn cd_fires_pre_change_dir_autocmd() {
let guard = TestGuard::new(); let guard = TestGuard::new();
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
test_input("autocmd pre-change-dir 'echo pre-cd'").unwrap(); test_input("autocmd pre-change-dir 'echo pre-cd'").unwrap();
guard.read_output(); guard.read_output();
test_input(format!("cd {}", temp_dir.path().display())).unwrap(); test_input(format!("cd {}", temp_dir.path().display())).unwrap();
let out = guard.read_output(); let out = guard.read_output();
assert!(out.contains("pre-cd")); assert!(out.contains("pre-cd"));
} }
} }

View File

@@ -176,20 +176,20 @@ pub fn complete_builtin(node: Node) -> ShResult<()> {
read_meta(|m| -> ShResult<()> { read_meta(|m| -> ShResult<()> {
let specs = m.comp_specs().values(); let specs = m.comp_specs().values();
for spec in specs { for spec in specs {
let stdout = borrow_fd(STDOUT_FILENO); let stdout = borrow_fd(STDOUT_FILENO);
write(stdout, spec.source().as_bytes())?; write(stdout, spec.source().as_bytes())?;
} }
Ok(()) Ok(())
})?; })?;
} else { } else {
read_meta(|m| -> ShResult<()> { read_meta(|m| -> ShResult<()> {
for (cmd, _) in &argv { for (cmd, _) in &argv {
if let Some(spec) = m.comp_specs().get(cmd) { if let Some(spec) = m.comp_specs().get(cmd) {
let stdout = borrow_fd(STDOUT_FILENO); let stdout = borrow_fd(STDOUT_FILENO);
write(stdout, spec.source().as_bytes())?; write(stdout, spec.source().as_bytes())?;
} }
} }
Ok(()) Ok(())
})?; })?;
} }
@@ -316,10 +316,10 @@ pub fn get_comp_opts(opts: Vec<Opt>) -> ShResult<CompOpts> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::state::{self, VarFlags, VarKind, read_meta, write_vars};
use crate::testutil::{TestGuard, test_input};
use std::fs; use std::fs;
use tempfile::TempDir; use tempfile::TempDir;
use crate::state::{self, read_meta, write_vars, VarFlags, VarKind};
use crate::testutil::{TestGuard, test_input};
// ===================== complete: Registration ===================== // ===================== complete: Registration =====================

View File

@@ -12,12 +12,13 @@ use crate::{
}; };
pub fn truncate_home_path(path: String) -> String { pub fn truncate_home_path(path: String) -> String {
if let Ok(home) = env::var("HOME") if let Ok(home) = env::var("HOME")
&& path.starts_with(&home) { && path.starts_with(&home)
let new = path.strip_prefix(&home).unwrap(); {
return format!("~{new}"); let new = path.strip_prefix(&home).unwrap();
} return format!("~{new}");
path.to_string() }
path.to_string()
} }
enum StackIdx { enum StackIdx {
@@ -376,8 +377,7 @@ pub fn dirs(node: Node) -> ShResult<()> {
.map(|d| d.to_string_lossy().to_string()); .map(|d| d.to_string_lossy().to_string());
if abbreviate_home { if abbreviate_home {
stack.map(truncate_home_path) stack.map(truncate_home_path).collect()
.collect()
} else { } else {
stack.collect() stack.collect()
} }
@@ -428,189 +428,198 @@ pub fn dirs(node: Node) -> ShResult<()> {
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use std::{env, path::PathBuf}; use crate::{
use crate::{state::{self, read_meta}, testutil::{TestGuard, test_input}}; state::{self, read_meta},
use pretty_assertions::{assert_ne,assert_eq}; testutil::{TestGuard, test_input},
use tempfile::TempDir; };
use pretty_assertions::{assert_eq, assert_ne};
use std::{env, path::PathBuf};
use tempfile::TempDir;
#[test] #[test]
fn test_pushd_interactive() { fn test_pushd_interactive() {
let g = TestGuard::new(); let g = TestGuard::new();
let current_dir = env::current_dir().unwrap(); let current_dir = env::current_dir().unwrap();
test_input("pushd /tmp").unwrap(); test_input("pushd /tmp").unwrap();
let new_dir = env::current_dir().unwrap(); let new_dir = env::current_dir().unwrap();
assert_ne!(new_dir, current_dir); assert_ne!(new_dir, current_dir);
assert_eq!(new_dir, PathBuf::from("/tmp")); assert_eq!(new_dir, PathBuf::from("/tmp"));
let dir_stack = read_meta(|m| m.dirs().clone()); let dir_stack = read_meta(|m| m.dirs().clone());
assert_eq!(dir_stack.len(), 1); assert_eq!(dir_stack.len(), 1);
assert_eq!(dir_stack[0], current_dir); assert_eq!(dir_stack[0], current_dir);
let out = g.read_output(); let out = g.read_output();
let path = super::truncate_home_path(current_dir.to_string_lossy().to_string()); let path = super::truncate_home_path(current_dir.to_string_lossy().to_string());
assert_eq!(out, format!("/tmp {path}\n")); assert_eq!(out, format!("/tmp {path}\n"));
} }
#[test] #[test]
fn test_popd_interactive() { fn test_popd_interactive() {
let g = TestGuard::new(); let g = TestGuard::new();
let current_dir = env::current_dir().unwrap(); let current_dir = env::current_dir().unwrap();
let tempdir = TempDir::new().unwrap(); let tempdir = TempDir::new().unwrap();
let tempdir_raw = tempdir.path().to_path_buf().to_string_lossy().to_string(); let tempdir_raw = tempdir.path().to_path_buf().to_string_lossy().to_string();
test_input(format!("pushd {tempdir_raw}")).unwrap(); test_input(format!("pushd {tempdir_raw}")).unwrap();
let dir_stack = read_meta(|m| m.dirs().clone()); let dir_stack = read_meta(|m| m.dirs().clone());
assert_eq!(dir_stack.len(), 1); assert_eq!(dir_stack.len(), 1);
assert_eq!(dir_stack[0], current_dir); assert_eq!(dir_stack[0], current_dir);
assert_eq!(env::current_dir().unwrap(), tempdir.path()); assert_eq!(env::current_dir().unwrap(), tempdir.path());
g.read_output(); // consume output of pushd g.read_output(); // consume output of pushd
test_input("popd").unwrap(); test_input("popd").unwrap();
assert_eq!(env::current_dir().unwrap(), current_dir); assert_eq!(env::current_dir().unwrap(), current_dir);
let out = g.read_output(); let out = g.read_output();
let path = super::truncate_home_path(current_dir.to_string_lossy().to_string()); let path = super::truncate_home_path(current_dir.to_string_lossy().to_string());
assert_eq!(out, format!("{path}\n")); assert_eq!(out, format!("{path}\n"));
} }
#[test] #[test]
fn test_popd_empty_stack() { fn test_popd_empty_stack() {
let _g = TestGuard::new(); let _g = TestGuard::new();
test_input("popd").unwrap_err(); test_input("popd").unwrap_err();
assert_ne!(state::get_status(), 0); assert_ne!(state::get_status(), 0);
} }
#[test] #[test]
fn test_pushd_multiple_then_popd() { fn test_pushd_multiple_then_popd() {
let g = TestGuard::new(); let g = TestGuard::new();
let original = env::current_dir().unwrap(); let original = env::current_dir().unwrap();
let tmp1 = TempDir::new().unwrap(); let tmp1 = TempDir::new().unwrap();
let tmp2 = TempDir::new().unwrap(); let tmp2 = TempDir::new().unwrap();
let path1 = tmp1.path().to_path_buf(); let path1 = tmp1.path().to_path_buf();
let path2 = tmp2.path().to_path_buf(); let path2 = tmp2.path().to_path_buf();
test_input(format!("pushd {}", path1.display())).unwrap(); test_input(format!("pushd {}", path1.display())).unwrap();
test_input(format!("pushd {}", path2.display())).unwrap(); test_input(format!("pushd {}", path2.display())).unwrap();
g.read_output(); g.read_output();
assert_eq!(env::current_dir().unwrap(), path2); assert_eq!(env::current_dir().unwrap(), path2);
let stack = read_meta(|m| m.dirs().clone()); let stack = read_meta(|m| m.dirs().clone());
assert_eq!(stack.len(), 2); assert_eq!(stack.len(), 2);
assert_eq!(stack[0], path1); assert_eq!(stack[0], path1);
assert_eq!(stack[1], original); assert_eq!(stack[1], original);
test_input("popd").unwrap(); test_input("popd").unwrap();
assert_eq!(env::current_dir().unwrap(), path1); assert_eq!(env::current_dir().unwrap(), path1);
test_input("popd").unwrap(); test_input("popd").unwrap();
assert_eq!(env::current_dir().unwrap(), original); assert_eq!(env::current_dir().unwrap(), original);
let stack = read_meta(|m| m.dirs().clone()); let stack = read_meta(|m| m.dirs().clone());
assert_eq!(stack.len(), 0); assert_eq!(stack.len(), 0);
} }
#[test] #[test]
fn test_pushd_rotate_plus() { fn test_pushd_rotate_plus() {
let g = TestGuard::new(); let g = TestGuard::new();
let original = env::current_dir().unwrap(); let original = env::current_dir().unwrap();
let tmp1 = TempDir::new().unwrap(); let tmp1 = TempDir::new().unwrap();
let tmp2 = TempDir::new().unwrap(); let tmp2 = TempDir::new().unwrap();
let path1 = tmp1.path().to_path_buf(); let path1 = tmp1.path().to_path_buf();
let path2 = tmp2.path().to_path_buf(); let path2 = tmp2.path().to_path_buf();
// Build stack: cwd=original, then pushd path1, pushd path2 // Build stack: cwd=original, then pushd path1, pushd path2
// Stack after: cwd=path2, [path1, original] // Stack after: cwd=path2, [path1, original]
test_input(format!("pushd {}", path1.display())).unwrap(); test_input(format!("pushd {}", path1.display())).unwrap();
test_input(format!("pushd {}", path2.display())).unwrap(); test_input(format!("pushd {}", path2.display())).unwrap();
g.read_output(); g.read_output();
// pushd +1 rotates: [path2, path1, original] -> rotate_left(1) -> [path1, original, path2] // pushd +1 rotates: [path2, path1, original] -> rotate_left(1) -> [path1, original, path2]
// pop front -> cwd=path1, stack=[original, path2] // pop front -> cwd=path1, stack=[original, path2]
test_input("pushd +1").unwrap(); test_input("pushd +1").unwrap();
assert_eq!(env::current_dir().unwrap(), path1); assert_eq!(env::current_dir().unwrap(), path1);
let stack = read_meta(|m| m.dirs().clone()); let stack = read_meta(|m| m.dirs().clone());
assert_eq!(stack.len(), 2); assert_eq!(stack.len(), 2);
assert_eq!(stack[0], original); assert_eq!(stack[0], original);
assert_eq!(stack[1], path2); assert_eq!(stack[1], path2);
} }
#[test] #[test]
fn test_pushd_no_cd_flag() { fn test_pushd_no_cd_flag() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let original = env::current_dir().unwrap(); let original = env::current_dir().unwrap();
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let path = tmp.path().to_path_buf(); let path = tmp.path().to_path_buf();
test_input(format!("pushd -n {}", path.display())).unwrap(); test_input(format!("pushd -n {}", path.display())).unwrap();
// -n means don't cd, but the dir should still be on the stack // -n means don't cd, but the dir should still be on the stack
assert_eq!(env::current_dir().unwrap(), original); assert_eq!(env::current_dir().unwrap(), original);
} }
#[test] #[test]
fn test_dirs_clear() { fn test_dirs_clear() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
test_input(format!("pushd {}", tmp.path().display())).unwrap(); test_input(format!("pushd {}", tmp.path().display())).unwrap();
assert_eq!(read_meta(|m| m.dirs().len()), 1); assert_eq!(read_meta(|m| m.dirs().len()), 1);
test_input("dirs -c").unwrap(); test_input("dirs -c").unwrap();
assert_eq!(read_meta(|m| m.dirs().len()), 0); assert_eq!(read_meta(|m| m.dirs().len()), 0);
} }
#[test] #[test]
fn test_dirs_one_per_line() { fn test_dirs_one_per_line() {
let g = TestGuard::new(); let g = TestGuard::new();
let original = env::current_dir().unwrap(); let original = env::current_dir().unwrap();
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let path = tmp.path().to_path_buf(); let path = tmp.path().to_path_buf();
test_input(format!("pushd {}", path.display())).unwrap(); test_input(format!("pushd {}", path.display())).unwrap();
g.read_output(); g.read_output();
test_input("dirs -p").unwrap(); test_input("dirs -p").unwrap();
let out = g.read_output(); let out = g.read_output();
let lines: Vec<&str> = out.split('\n').filter(|l| !l.is_empty()).collect(); let lines: Vec<&str> = out.split('\n').filter(|l| !l.is_empty()).collect();
assert_eq!(lines.len(), 2); assert_eq!(lines.len(), 2);
assert_eq!(lines[0], super::truncate_home_path(path.to_string_lossy().to_string())); assert_eq!(
assert_eq!(lines[1], super::truncate_home_path(original.to_string_lossy().to_string())); lines[0],
} super::truncate_home_path(path.to_string_lossy().to_string())
);
assert_eq!(
lines[1],
super::truncate_home_path(original.to_string_lossy().to_string())
);
}
#[test] #[test]
fn test_popd_indexed_from_top() { fn test_popd_indexed_from_top() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let original = env::current_dir().unwrap(); let original = env::current_dir().unwrap();
let tmp1 = TempDir::new().unwrap(); let tmp1 = TempDir::new().unwrap();
let tmp2 = TempDir::new().unwrap(); let tmp2 = TempDir::new().unwrap();
let path1 = tmp1.path().to_path_buf(); let path1 = tmp1.path().to_path_buf();
let path2 = tmp2.path().to_path_buf(); let path2 = tmp2.path().to_path_buf();
// Stack: cwd=path2, [path1, original] // Stack: cwd=path2, [path1, original]
test_input(format!("pushd {}", path1.display())).unwrap(); test_input(format!("pushd {}", path1.display())).unwrap();
test_input(format!("pushd {}", path2.display())).unwrap(); test_input(format!("pushd {}", path2.display())).unwrap();
// popd +1 removes index (1-1)=0 from stored dirs, i.e. path1 // popd +1 removes index (1-1)=0 from stored dirs, i.e. path1
test_input("popd +1").unwrap(); test_input("popd +1").unwrap();
assert_eq!(env::current_dir().unwrap(), path2); // no cd assert_eq!(env::current_dir().unwrap(), path2); // no cd
let stack = read_meta(|m| m.dirs().clone()); let stack = read_meta(|m| m.dirs().clone());
assert_eq!(stack.len(), 1); assert_eq!(stack.len(), 1);
assert_eq!(stack[0], original); assert_eq!(stack[0], original);
} }
#[test] #[test]
fn test_pushd_nonexistent_dir() { fn test_pushd_nonexistent_dir() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let result = test_input("pushd /nonexistent_dir_12345"); let result = test_input("pushd /nonexistent_dir_12345");
assert!(result.is_err()); assert!(result.is_err());
} }
} }

View File

@@ -31,7 +31,7 @@ bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct EchoFlags: u32 { pub struct EchoFlags: u32 {
const NO_NEWLINE = 0b000001; const NO_NEWLINE = 0b000001;
const NO_ESCAPE = 0b000010; const NO_ESCAPE = 0b000010;
const USE_ESCAPE = 0b000100; const USE_ESCAPE = 0b000100;
const USE_PROMPT = 0b001000; const USE_PROMPT = 0b001000;
} }
@@ -55,16 +55,17 @@ pub fn echo(node: Node) -> ShResult<()> {
} }
let output_channel = borrow_fd(STDOUT_FILENO); let output_channel = borrow_fd(STDOUT_FILENO);
let xpg_echo = read_shopts(|o| o.core.xpg_echo); // If true, echo expands escape sequences by default, and -E opts out let xpg_echo = read_shopts(|o| o.core.xpg_echo); // If true, echo expands escape sequences by default, and -E opts out
let use_escape = (xpg_echo && !flags.contains(EchoFlags::NO_ESCAPE)) || flags.contains(EchoFlags::USE_ESCAPE); let use_escape =
(xpg_echo && !flags.contains(EchoFlags::NO_ESCAPE)) || flags.contains(EchoFlags::USE_ESCAPE);
let mut echo_output = prepare_echo_args( let mut echo_output = prepare_echo_args(
argv argv
.into_iter() .into_iter()
.map(|a| a.0) // Extract the String from the tuple of (String,Span) .map(|a| a.0) // Extract the String from the tuple of (String,Span)
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
use_escape, use_escape,
flags.contains(EchoFlags::USE_PROMPT), flags.contains(EchoFlags::USE_PROMPT),
)? )?
.join(" "); .join(" ");
@@ -207,7 +208,7 @@ pub fn get_echo_flags(opts: Vec<Opt>) -> ShResult<EchoFlags> {
Opt::Short('n') => flags |= EchoFlags::NO_NEWLINE, Opt::Short('n') => flags |= EchoFlags::NO_NEWLINE,
Opt::Short('e') => flags |= EchoFlags::USE_ESCAPE, Opt::Short('e') => flags |= EchoFlags::USE_ESCAPE,
Opt::Short('p') => flags |= EchoFlags::USE_PROMPT, Opt::Short('p') => flags |= EchoFlags::USE_PROMPT,
Opt::Short('E') => flags |= EchoFlags::NO_ESCAPE, Opt::Short('E') => flags |= EchoFlags::NO_ESCAPE,
_ => { _ => {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::ExecFail, ShErrKind::ExecFail,
@@ -308,11 +309,7 @@ mod tests {
#[test] #[test]
fn prepare_multiple_args() { fn prepare_multiple_args() {
let result = prepare_echo_args( let result = prepare_echo_args(vec!["hello".into(), "world".into()], false, false).unwrap();
vec!["hello".into(), "world".into()],
false,
false,
).unwrap();
assert_eq!(result, vec!["hello", "world"]); assert_eq!(result, vec!["hello", "world"]);
} }

View File

@@ -37,7 +37,7 @@ pub fn eval(node: Node) -> ShResult<()> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::state::{self, read_vars, write_vars, VarFlags, VarKind}; use crate::state::{self, VarFlags, VarKind, read_vars, write_vars};
use crate::testutil::{TestGuard, test_input}; use crate::testutil::{TestGuard, test_input};
// ===================== Basic ===================== // ===================== Basic =====================
@@ -80,7 +80,8 @@ mod tests {
#[test] #[test]
fn eval_expands_variable() { fn eval_expands_variable() {
let guard = TestGuard::new(); let guard = TestGuard::new();
write_vars(|v| v.set_var("CMD", VarKind::Str("echo evaluated".into()), VarFlags::NONE)).unwrap(); write_vars(|v| v.set_var("CMD", VarKind::Str("echo evaluated".into()), VarFlags::NONE))
.unwrap();
test_input("eval $CMD").unwrap(); test_input("eval $CMD").unwrap();
let out = guard.read_output(); let out = guard.read_output();

View File

@@ -50,7 +50,7 @@ pub fn exec_builtin(node: Node) -> ShResult<()> {
mod tests { mod tests {
use crate::state; use crate::state;
use crate::testutil::{TestGuard, test_input}; use crate::testutil::{TestGuard, test_input};
// Testing exec is a bit tricky since it replaces the current process, so we just test that it correctly handles the case of no arguments and the case of a nonexistent command. We can't really test that it successfully executes a command since that would replace the test process itself. // Testing exec is a bit tricky since it replaces the current process, so we just test that it correctly handles the case of no arguments and the case of a nonexistent command. We can't really test that it successfully executes a command since that would replace the test process itself.
#[test] #[test]
fn exec_no_args_succeeds() { fn exec_no_args_succeeds() {
@@ -62,7 +62,9 @@ mod tests {
#[test] #[test]
fn exec_nonexistent_command_fails() { fn exec_nonexistent_command_fails() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let result = test_input("exec _____________no_such_______command_xyz_____________hopefully______this_doesnt______exist_____somewhere_in___your______PATH__________________"); let result = test_input(
"exec _____________no_such_______command_xyz_____________hopefully______this_doesnt______exist_____somewhere_in___your______PATH__________________",
);
assert!(result.is_err()); assert!(result.is_err());
} }
} }

View File

@@ -185,7 +185,7 @@ mod tests {
let out = guard.read_output(); let out = guard.read_output();
assert!(out.contains("cat")); assert!(out.contains("cat"));
assert!(out.contains("is")); assert!(out.contains("is"));
assert!(out.contains("/")); // Should show a path assert!(out.contains("/")); // Should show a path
} }
// ===================== Not found ===================== // ===================== Not found =====================

View File

@@ -81,10 +81,10 @@ impl KeyMapOpts {
opt: Opt::Short('o'), // operator-pending mode opt: Opt::Short('o'), // operator-pending mode
takes_arg: false, takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Long("remove".into()), opt: Opt::Long("remove".into()),
takes_arg: true, takes_arg: true,
}, },
OptSpec { OptSpec {
opt: Opt::Short('r'), // replace mode opt: Opt::Short('r'), // replace mode
takes_arg: false, takes_arg: false,
@@ -180,8 +180,8 @@ pub fn keymap(node: Node) -> ShResult<()> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::getopt::Opt;
use crate::expand::expand_keymap; use crate::expand::expand_keymap;
use crate::getopt::Opt;
use crate::state::{self, read_logic}; use crate::state::{self, read_logic};
use crate::testutil::{TestGuard, test_input}; use crate::testutil::{TestGuard, test_input};
@@ -217,7 +217,8 @@ mod tests {
let opts = KeyMapOpts::from_opts(&[ let opts = KeyMapOpts::from_opts(&[
Opt::Short('n'), Opt::Short('n'),
Opt::LongWithArg("remove".into(), "jk".into()), Opt::LongWithArg("remove".into(), "jk".into()),
]).unwrap(); ])
.unwrap();
assert_eq!(opts.remove, Some("jk".into())); assert_eq!(opts.remove, Some("jk".into()));
} }
@@ -273,10 +274,7 @@ mod tests {
let _g = TestGuard::new(); let _g = TestGuard::new();
test_input("keymap -n jk '<ESC>'").unwrap(); test_input("keymap -n jk '<ESC>'").unwrap();
let maps = read_logic(|l| l.keymaps_filtered( let maps = read_logic(|l| l.keymaps_filtered(KeyMapFlags::NORMAL, &expand_keymap("jk")));
KeyMapFlags::NORMAL,
&expand_keymap("jk"),
));
assert!(!maps.is_empty()); assert!(!maps.is_empty());
} }
@@ -285,10 +283,7 @@ mod tests {
let _g = TestGuard::new(); let _g = TestGuard::new();
test_input("keymap -i jk '<ESC>'").unwrap(); test_input("keymap -i jk '<ESC>'").unwrap();
let maps = read_logic(|l| l.keymaps_filtered( let maps = read_logic(|l| l.keymaps_filtered(KeyMapFlags::INSERT, &expand_keymap("jk")));
KeyMapFlags::INSERT,
&expand_keymap("jk"),
));
assert!(!maps.is_empty()); assert!(!maps.is_empty());
} }
@@ -298,10 +293,7 @@ mod tests {
test_input("keymap -n jk '<ESC>'").unwrap(); test_input("keymap -n jk '<ESC>'").unwrap();
test_input("keymap -n jk 'dd'").unwrap(); test_input("keymap -n jk 'dd'").unwrap();
let maps = read_logic(|l| l.keymaps_filtered( let maps = read_logic(|l| l.keymaps_filtered(KeyMapFlags::NORMAL, &expand_keymap("jk")));
KeyMapFlags::NORMAL,
&expand_keymap("jk"),
));
assert_eq!(maps.len(), 1); assert_eq!(maps.len(), 1);
assert_eq!(maps[0].action, "dd"); assert_eq!(maps[0].action, "dd");
} }
@@ -312,10 +304,7 @@ mod tests {
test_input("keymap -n jk '<ESC>'").unwrap(); test_input("keymap -n jk '<ESC>'").unwrap();
test_input("keymap -n --remove jk").unwrap(); test_input("keymap -n --remove jk").unwrap();
let maps = read_logic(|l| l.keymaps_filtered( let maps = read_logic(|l| l.keymaps_filtered(KeyMapFlags::NORMAL, &expand_keymap("jk")));
KeyMapFlags::NORMAL,
&expand_keymap("jk"),
));
assert!(maps.is_empty()); assert!(maps.is_empty());
} }

View File

@@ -389,7 +389,7 @@ pub fn get_map_opts(opts: Vec<Opt>) -> MapOpts {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{MapNode, MapFlags, get_map_opts}; use super::{MapFlags, MapNode, get_map_opts};
use crate::getopt::Opt; use crate::getopt::Opt;
use crate::state::{self, read_vars}; use crate::state::{self, read_vars};
use crate::testutil::{TestGuard, test_input}; use crate::testutil::{TestGuard, test_input};
@@ -433,10 +433,7 @@ mod tests {
#[test] #[test]
fn mapnode_remove_nested() { fn mapnode_remove_nested() {
let mut root = MapNode::default(); let mut root = MapNode::default();
root.set( root.set(&["a".into(), "b".into()], MapNode::StaticLeaf("val".into()));
&["a".into(), "b".into()],
MapNode::StaticLeaf("val".into()),
);
root.remove(&["a".into(), "b".into()]); root.remove(&["a".into(), "b".into()]);
assert!(root.get(&["a".into(), "b".into()]).is_none()); assert!(root.get(&["a".into(), "b".into()]).is_none());
// Parent branch should still exist // Parent branch should still exist

View File

@@ -17,20 +17,20 @@ pub mod keymap;
pub mod map; pub mod map;
pub mod pwd; pub mod pwd;
pub mod read; pub mod read;
pub mod resource;
pub mod shift; pub mod shift;
pub mod shopt; pub mod shopt;
pub mod source; pub mod source;
pub mod test; // [[ ]] thing pub mod test; // [[ ]] thing
pub mod trap; pub mod trap;
pub mod varcmds; pub mod varcmds;
pub mod resource;
pub const BUILTINS: [&str; 49] = [ pub const BUILTINS: [&str; 49] = [
"echo", "cd", "read", "export", "local", "pwd", "source", ".", "shift", "jobs", "fg", "bg", "disown", "echo", "cd", "read", "export", "local", "pwd", "source", ".", "shift", "jobs", "fg", "bg",
"alias", "unalias", "return", "break", "continue", "exit", "shopt", "builtin", "disown", "alias", "unalias", "return", "break", "continue", "exit", "shopt", "builtin",
"command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly", "command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly",
"unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type", "unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type",
"getopts", "keymap", "read_key", "autocmd", "ulimit", "umask" "getopts", "keymap", "read_key", "autocmd", "ulimit", "umask",
]; ];
pub fn true_builtin() -> ShResult<()> { pub fn true_builtin() -> ShResult<()> {
@@ -50,31 +50,34 @@ pub fn noop_builtin() -> ShResult<()> {
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use crate::{state, testutil::{TestGuard, test_input}}; use crate::{
state,
testutil::{TestGuard, test_input},
};
// You can never be too sure!!!!!! // You can never be too sure!!!!!!
#[test] #[test]
fn test_true() { fn test_true() {
let _g = TestGuard::new(); let _g = TestGuard::new();
test_input("true").unwrap(); test_input("true").unwrap();
assert_eq!(state::get_status(), 0); assert_eq!(state::get_status(), 0);
} }
#[test] #[test]
fn test_false() { fn test_false() {
let _g = TestGuard::new(); let _g = TestGuard::new();
test_input("false").unwrap(); test_input("false").unwrap();
assert_eq!(state::get_status(), 1); assert_eq!(state::get_status(), 1);
} }
#[test] #[test]
fn test_noop() { fn test_noop() {
let _g = TestGuard::new(); let _g = TestGuard::new();
test_input(":").unwrap(); test_input(":").unwrap();
assert_eq!(state::get_status(), 0); assert_eq!(state::get_status(), 0);
} }
} }

View File

@@ -27,10 +27,10 @@ pub fn pwd(node: Node) -> ShResult<()> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::env;
use tempfile::TempDir;
use crate::state; use crate::state;
use crate::testutil::{TestGuard, test_input}; use crate::testutil::{TestGuard, test_input};
use std::env;
use tempfile::TempDir;
#[test] #[test]
fn pwd_prints_cwd() { fn pwd_prints_cwd() {

View File

@@ -367,7 +367,7 @@ pub fn get_read_key_opts(opts: Vec<Opt>) -> ShResult<ReadKeyOpts> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::state::{self, read_vars, write_vars, VarFlags, VarKind}; use crate::state::{self, VarFlags, VarKind, read_vars, write_vars};
use crate::testutil::{TestGuard, test_input}; use crate::testutil::{TestGuard, test_input};
// ===================== Basic read into REPLY ===================== // ===================== Basic read into REPLY =====================

View File

@@ -1,92 +1,115 @@
use ariadne::Fmt; use ariadne::Fmt;
use nix::{libc::STDOUT_FILENO, sys::{resource::{Resource, getrlimit, setrlimit}, stat::{Mode, umask}}, unistd::write}; use nix::{
libc::STDOUT_FILENO,
use crate::{ sys::{
getopt::{Opt, OptSpec, get_opts_from_tokens_strict}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color}, parse::{NdRule, Node}, procio::borrow_fd, state::{self} resource::{Resource, getrlimit, setrlimit},
stat::{Mode, umask},
},
unistd::write,
}; };
fn ulimit_opt_spec() -> [OptSpec;5] { use crate::{
[ getopt::{Opt, OptSpec, get_opts_from_tokens_strict},
OptSpec { libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color},
opt: Opt::Short('n'), // file descriptors parse::{NdRule, Node},
takes_arg: true, procio::borrow_fd,
}, state::{self},
OptSpec { };
opt: Opt::Short('u'), // max user processes
takes_arg: true, fn ulimit_opt_spec() -> [OptSpec; 5] {
}, [
OptSpec { OptSpec {
opt: Opt::Short('s'), // stack size opt: Opt::Short('n'), // file descriptors
takes_arg: true, takes_arg: true,
}, },
OptSpec { OptSpec {
opt: Opt::Short('c'), // core dump file size opt: Opt::Short('u'), // max user processes
takes_arg: true, takes_arg: true,
}, },
OptSpec { OptSpec {
opt: Opt::Short('v'), // virtual memory opt: Opt::Short('s'), // stack size
takes_arg: true, takes_arg: true,
} },
] OptSpec {
opt: Opt::Short('c'), // core dump file size
takes_arg: true,
},
OptSpec {
opt: Opt::Short('v'), // virtual memory
takes_arg: true,
},
]
} }
struct UlimitOpts { struct UlimitOpts {
fds: Option<u64>, fds: Option<u64>,
procs: Option<u64>, procs: Option<u64>,
stack: Option<u64>, stack: Option<u64>,
core: Option<u64>, core: Option<u64>,
vmem: Option<u64>, vmem: Option<u64>,
} }
fn get_ulimit_opts(opt: &[Opt]) -> ShResult<UlimitOpts> { fn get_ulimit_opts(opt: &[Opt]) -> ShResult<UlimitOpts> {
let mut opts = UlimitOpts { let mut opts = UlimitOpts {
fds: None, fds: None,
procs: None, procs: None,
stack: None, stack: None,
core: None, core: None,
vmem: None, vmem: None,
}; };
for o in opt { for o in opt {
match o { match o {
Opt::ShortWithArg('n', arg) => { Opt::ShortWithArg('n', arg) => {
opts.fds = Some(arg.parse().map_err(|_| ShErr::simple( opts.fds = Some(arg.parse().map_err(|_| {
ShErrKind::ParseErr, ShErr::simple(
format!("invalid argument for -n: {}", arg.fg(next_color())), ShErrKind::ParseErr,
))?); format!("invalid argument for -n: {}", arg.fg(next_color())),
}, )
Opt::ShortWithArg('u', arg) => { })?);
opts.procs = Some(arg.parse().map_err(|_| ShErr::simple( }
ShErrKind::ParseErr, Opt::ShortWithArg('u', arg) => {
format!("invalid argument for -u: {}", arg.fg(next_color())), opts.procs = Some(arg.parse().map_err(|_| {
))?); ShErr::simple(
}, ShErrKind::ParseErr,
Opt::ShortWithArg('s', arg) => { format!("invalid argument for -u: {}", arg.fg(next_color())),
opts.stack = Some(arg.parse().map_err(|_| ShErr::simple( )
ShErrKind::ParseErr, })?);
format!("invalid argument for -s: {}", arg.fg(next_color())), }
))?); Opt::ShortWithArg('s', arg) => {
}, opts.stack = Some(arg.parse().map_err(|_| {
Opt::ShortWithArg('c', arg) => { ShErr::simple(
opts.core = Some(arg.parse().map_err(|_| ShErr::simple( ShErrKind::ParseErr,
ShErrKind::ParseErr, format!("invalid argument for -s: {}", arg.fg(next_color())),
format!("invalid argument for -c: {}", arg.fg(next_color())), )
))?); })?);
}, }
Opt::ShortWithArg('v', arg) => { Opt::ShortWithArg('c', arg) => {
opts.vmem = Some(arg.parse().map_err(|_| ShErr::simple( opts.core = Some(arg.parse().map_err(|_| {
ShErrKind::ParseErr, ShErr::simple(
format!("invalid argument for -v: {}", arg.fg(next_color())), ShErrKind::ParseErr,
))?); format!("invalid argument for -c: {}", arg.fg(next_color())),
}, )
o => return Err(ShErr::simple( })?);
ShErrKind::ParseErr, }
format!("invalid option: {}", o.fg(next_color())), Opt::ShortWithArg('v', arg) => {
)), opts.vmem = Some(arg.parse().map_err(|_| {
} ShErr::simple(
} ShErrKind::ParseErr,
format!("invalid argument for -v: {}", arg.fg(next_color())),
)
})?);
}
o => {
return Err(ShErr::simple(
ShErrKind::ParseErr,
format!("invalid option: {}", o.fg(next_color())),
));
}
}
}
Ok(opts) Ok(opts)
} }
pub fn ulimit(node: Node) -> ShResult<()> { pub fn ulimit(node: Node) -> ShResult<()> {
@@ -99,282 +122,308 @@ pub fn ulimit(node: Node) -> ShResult<()> {
unreachable!() unreachable!()
}; };
let (_, opts) = get_opts_from_tokens_strict(argv, &ulimit_opt_spec()).promote_err(span.clone())?; let (_, opts) =
let ulimit_opts = get_ulimit_opts(&opts).promote_err(span.clone())?; get_opts_from_tokens_strict(argv, &ulimit_opt_spec()).promote_err(span.clone())?;
let ulimit_opts = get_ulimit_opts(&opts).promote_err(span.clone())?;
if let Some(fds) = ulimit_opts.fds { if let Some(fds) = ulimit_opts.fds {
let (_, hard) = getrlimit(Resource::RLIMIT_NOFILE).map_err(|e| ShErr::at( let (_, hard) = getrlimit(Resource::RLIMIT_NOFILE).map_err(|e| {
ShErrKind::ExecFail, ShErr::at(
span.clone(), ShErrKind::ExecFail,
format!("failed to get file descriptor limit: {}", e), span.clone(),
))?; format!("failed to get file descriptor limit: {}", e),
setrlimit(Resource::RLIMIT_NOFILE, fds, hard).map_err(|e| ShErr::at( )
ShErrKind::ExecFail, })?;
span.clone(), setrlimit(Resource::RLIMIT_NOFILE, fds, hard).map_err(|e| {
format!("failed to set file descriptor limit: {}", e), ShErr::at(
))?; ShErrKind::ExecFail,
} span.clone(),
if let Some(procs) = ulimit_opts.procs { format!("failed to set file descriptor limit: {}", e),
let (_, hard) = getrlimit(Resource::RLIMIT_NPROC).map_err(|e| ShErr::at( )
ShErrKind::ExecFail, })?;
span.clone(), }
format!("failed to get process limit: {}", e), if let Some(procs) = ulimit_opts.procs {
))?; let (_, hard) = getrlimit(Resource::RLIMIT_NPROC).map_err(|e| {
setrlimit(Resource::RLIMIT_NPROC, procs, hard).map_err(|e| ShErr::at( ShErr::at(
ShErrKind::ExecFail, ShErrKind::ExecFail,
span.clone(), span.clone(),
format!("failed to set process limit: {}", e), format!("failed to get process limit: {}", e),
))?; )
} })?;
if let Some(stack) = ulimit_opts.stack { setrlimit(Resource::RLIMIT_NPROC, procs, hard).map_err(|e| {
let (_, hard) = getrlimit(Resource::RLIMIT_STACK).map_err(|e| ShErr::at( ShErr::at(
ShErrKind::ExecFail, ShErrKind::ExecFail,
span.clone(), span.clone(),
format!("failed to get stack size limit: {}", e), format!("failed to set process limit: {}", e),
))?; )
setrlimit(Resource::RLIMIT_STACK, stack, hard).map_err(|e| ShErr::at( })?;
ShErrKind::ExecFail, }
span.clone(), if let Some(stack) = ulimit_opts.stack {
format!("failed to set stack size limit: {}", e), let (_, hard) = getrlimit(Resource::RLIMIT_STACK).map_err(|e| {
))?; ShErr::at(
} ShErrKind::ExecFail,
if let Some(core) = ulimit_opts.core { span.clone(),
let (_, hard) = getrlimit(Resource::RLIMIT_CORE).map_err(|e| ShErr::at( format!("failed to get stack size limit: {}", e),
ShErrKind::ExecFail, )
span.clone(), })?;
format!("failed to get core dump size limit: {}", e), setrlimit(Resource::RLIMIT_STACK, stack, hard).map_err(|e| {
))?; ShErr::at(
setrlimit(Resource::RLIMIT_CORE, core, hard).map_err(|e| ShErr::at( ShErrKind::ExecFail,
ShErrKind::ExecFail, span.clone(),
span.clone(), format!("failed to set stack size limit: {}", e),
format!("failed to set core dump size limit: {}", e), )
))?; })?;
} }
if let Some(vmem) = ulimit_opts.vmem { if let Some(core) = ulimit_opts.core {
let (_, hard) = getrlimit(Resource::RLIMIT_AS).map_err(|e| ShErr::at( let (_, hard) = getrlimit(Resource::RLIMIT_CORE).map_err(|e| {
ShErrKind::ExecFail, ShErr::at(
span.clone(), ShErrKind::ExecFail,
format!("failed to get virtual memory limit: {}", e), span.clone(),
))?; format!("failed to get core dump size limit: {}", e),
setrlimit(Resource::RLIMIT_AS, vmem, hard).map_err(|e| ShErr::at( )
ShErrKind::ExecFail, })?;
span.clone(), setrlimit(Resource::RLIMIT_CORE, core, hard).map_err(|e| {
format!("failed to set virtual memory limit: {}", e), ShErr::at(
))?; ShErrKind::ExecFail,
} span.clone(),
format!("failed to set core dump size limit: {}", e),
)
})?;
}
if let Some(vmem) = ulimit_opts.vmem {
let (_, hard) = getrlimit(Resource::RLIMIT_AS).map_err(|e| {
ShErr::at(
ShErrKind::ExecFail,
span.clone(),
format!("failed to get virtual memory limit: {}", e),
)
})?;
setrlimit(Resource::RLIMIT_AS, vmem, hard).map_err(|e| {
ShErr::at(
ShErrKind::ExecFail,
span.clone(),
format!("failed to set virtual memory limit: {}", e),
)
})?;
}
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }
pub fn umask_builtin(node: Node) -> ShResult<()> { pub fn umask_builtin(node: Node) -> ShResult<()> {
let span = node.get_span(); let span = node.get_span();
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
} = node.class else { unreachable!() }; } = node.class
else {
unreachable!()
};
let (argv, opts) = get_opts_from_tokens_strict( let (argv, opts) = get_opts_from_tokens_strict(
argv, argv,
&[OptSpec { opt: Opt::Short('S'), takes_arg: false }], &[OptSpec {
)?; opt: Opt::Short('S'),
let argv = &argv[1..]; // skip command name takes_arg: false,
}],
)?;
let argv = &argv[1..]; // skip command name
let old = umask(Mode::empty()); let old = umask(Mode::empty());
umask(old); umask(old);
let mut old_bits = old.bits(); let mut old_bits = old.bits();
if !argv.is_empty() { if !argv.is_empty() {
if argv.len() > 1 { if argv.len() > 1 {
return Err(ShErr::at( return Err(ShErr::at(
ShErrKind::ParseErr, ShErrKind::ParseErr,
span.clone(), span.clone(),
format!("umask takes at most one argument, got {}", argv.len()), format!("umask takes at most one argument, got {}", argv.len()),
)); ));
} }
let arg = argv[0].clone(); let arg = argv[0].clone();
let raw = arg.as_str(); let raw = arg.as_str();
if raw.chars().any(|c| c.is_ascii_digit()) { if raw.chars().any(|c| c.is_ascii_digit()) {
let mode_raw = u32::from_str_radix(raw, 8).map_err(|_| ShErr::at( let mode_raw = u32::from_str_radix(raw, 8).map_err(|_| {
ShErrKind::ParseErr, ShErr::at(
span.clone(), ShErrKind::ParseErr,
format!("invalid numeric umask: {}", raw.fg(next_color())), span.clone(),
))?; format!("invalid numeric umask: {}", raw.fg(next_color())),
)
})?;
let mode = Mode::from_bits(mode_raw).ok_or_else(|| ShErr::at( let mode = Mode::from_bits(mode_raw).ok_or_else(|| {
ShErrKind::ParseErr, ShErr::at(
span.clone(), ShErrKind::ParseErr,
format!("invalid umask value: {}", raw.fg(next_color())), span.clone(),
))?; format!("invalid umask value: {}", raw.fg(next_color())),
)
})?;
umask(mode); umask(mode);
} else { } else {
let parts = raw.split(','); let parts = raw.split(',');
for part in parts { for part in parts {
if let Some((who,bits)) = part.split_once('=') { if let Some((who, bits)) = part.split_once('=') {
let mut new_bits = 0; let mut new_bits = 0;
if bits.contains('r') { if bits.contains('r') {
new_bits |= 4; new_bits |= 4;
} }
if bits.contains('w') { if bits.contains('w') {
new_bits |= 2; new_bits |= 2;
} }
if bits.contains('x') { if bits.contains('x') {
new_bits |= 1; new_bits |= 1;
} }
for ch in who.chars() { for ch in who.chars() {
match ch { match ch {
'o' => { 'o' => {
old_bits &= !0o7; old_bits &= !0o7;
old_bits |= !new_bits & 0o7; old_bits |= !new_bits & 0o7;
} }
'g' => { 'g' => {
old_bits &= !(0o7 << 3); old_bits &= !(0o7 << 3);
old_bits |= (!new_bits & 0o7) << 3; old_bits |= (!new_bits & 0o7) << 3;
} }
'u' => { 'u' => {
old_bits &= !(0o7 << 6); old_bits &= !(0o7 << 6);
old_bits |= (!new_bits & 0o7) << 6; old_bits |= (!new_bits & 0o7) << 6;
} }
'a' => { 'a' => {
let denied = !new_bits & 0o7; let denied = !new_bits & 0o7;
old_bits = denied | (denied << 3) | (denied << 6); old_bits = denied | (denied << 3) | (denied << 6);
} }
_ => { _ => {
return Err(ShErr::at( return Err(ShErr::at(
ShErrKind::ParseErr, ShErrKind::ParseErr,
span.clone(), span.clone(),
format!("invalid umask 'who' character: {}", ch.fg(next_color())), format!("invalid umask 'who' character: {}", ch.fg(next_color())),
)); ));
} }
} }
} }
umask(Mode::from_bits_truncate(old_bits)); umask(Mode::from_bits_truncate(old_bits));
} else if let Some((who,bits)) = part.split_once('+') { } else if let Some((who, bits)) = part.split_once('+') {
let mut new_bits = 0; let mut new_bits = 0;
if bits.contains('r') { if bits.contains('r') {
new_bits |= 4; new_bits |= 4;
} }
if bits.contains('w') { if bits.contains('w') {
new_bits |= 2; new_bits |= 2;
} }
if bits.contains('x') { if bits.contains('x') {
new_bits |= 1; new_bits |= 1;
} }
for ch in who.chars() { for ch in who.chars() {
match ch { match ch {
'o' => { 'o' => {
old_bits &= !(new_bits & 0o7); old_bits &= !(new_bits & 0o7);
} }
'g' => { 'g' => {
old_bits &= !((new_bits & 0o7) << 3); old_bits &= !((new_bits & 0o7) << 3);
} }
'u' => { 'u' => {
old_bits &= !((new_bits & 0o7) << 6); old_bits &= !((new_bits & 0o7) << 6);
} }
'a' => { 'a' => {
let mask = new_bits & 0o7; let mask = new_bits & 0o7;
old_bits &= !(mask | (mask << 3) | (mask << 6)); old_bits &= !(mask | (mask << 3) | (mask << 6));
} }
_ => { _ => {
return Err(ShErr::at( return Err(ShErr::at(
ShErrKind::ParseErr, ShErrKind::ParseErr,
span.clone(), span.clone(),
format!("invalid umask 'who' character: {}", ch.fg(next_color())), format!("invalid umask 'who' character: {}", ch.fg(next_color())),
)); ));
} }
} }
} }
umask(Mode::from_bits_truncate(old_bits)); umask(Mode::from_bits_truncate(old_bits));
} else if let Some((who,bits)) = part.split_once('-') { } else if let Some((who, bits)) = part.split_once('-') {
let mut new_bits = 0; let mut new_bits = 0;
if bits.contains('r') { if bits.contains('r') {
new_bits |= 4; new_bits |= 4;
} }
if bits.contains('w') { if bits.contains('w') {
new_bits |= 2; new_bits |= 2;
} }
if bits.contains('x') { if bits.contains('x') {
new_bits |= 1; new_bits |= 1;
} }
for ch in who.chars() { for ch in who.chars() {
match ch { match ch {
'o' => { 'o' => {
old_bits |= new_bits & 0o7; old_bits |= new_bits & 0o7;
} }
'g' => { 'g' => {
old_bits |= (new_bits << 3) & (0o7 << 3); old_bits |= (new_bits << 3) & (0o7 << 3);
} }
'u' => { 'u' => {
old_bits |= (new_bits << 6) & (0o7 << 6); old_bits |= (new_bits << 6) & (0o7 << 6);
} }
'a' => { 'a' => {
old_bits |= (new_bits | (new_bits << 3) | (new_bits << 6)) & 0o777; old_bits |= (new_bits | (new_bits << 3) | (new_bits << 6)) & 0o777;
} }
_ => { _ => {
return Err(ShErr::at( return Err(ShErr::at(
ShErrKind::ParseErr, ShErrKind::ParseErr,
span.clone(), span.clone(),
format!("invalid umask 'who' character: {}", ch.fg(next_color())), format!("invalid umask 'who' character: {}", ch.fg(next_color())),
)); ));
} }
} }
} }
umask(Mode::from_bits_truncate(old_bits)); umask(Mode::from_bits_truncate(old_bits));
} else { } else {
return Err(ShErr::at( return Err(ShErr::at(
ShErrKind::ParseErr, ShErrKind::ParseErr,
span.clone(), span.clone(),
format!("invalid symbolic umask part: {}", part.fg(next_color())), format!("invalid symbolic umask part: {}", part.fg(next_color())),
)); ));
} }
} }
} }
} else if !opts.is_empty() {
let u = (old_bits >> 6) & 0o7;
let g = (old_bits >> 3) & 0o7;
let o = old_bits & 0o7;
let mut u_str = String::from("u=");
let mut g_str = String::from("g=");
let mut o_str = String::from("o=");
let stuff = [(u, &mut u_str), (g, &mut g_str), (o, &mut o_str)];
for (bits, out) in stuff.into_iter() {
if bits & 4 == 0 {
out.push('r');
}
if bits & 2 == 0 {
out.push('w');
}
if bits & 1 == 0 {
out.push('x');
}
}
} else if !opts.is_empty() { let msg = [u_str, g_str, o_str].join(",");
let u = (old_bits >> 6) & 0o7; let stdout = borrow_fd(STDOUT_FILENO);
let g = (old_bits >> 3) & 0o7; write(stdout, msg.as_bytes())?;
let o = old_bits & 0o7; write(stdout, b"\n")?;
let mut u_str = String::from("u="); } else {
let mut g_str = String::from("g="); let raw = format!("{:04o}\n", old_bits);
let mut o_str = String::from("o=");
let stuff = [
(u, &mut u_str),
(g, &mut g_str),
(o, &mut o_str),
];
for (bits, out) in stuff.into_iter() {
if bits & 4 == 0 {
out.push('r');
}
if bits & 2 == 0 {
out.push('w');
}
if bits & 1 == 0 {
out.push('x');
}
}
let msg = [u_str,g_str,o_str].join(","); let stdout = borrow_fd(STDOUT_FILENO);
let stdout = borrow_fd(STDOUT_FILENO); write(stdout, raw.as_bytes())?;
write(stdout, msg.as_bytes())?; }
write(stdout, b"\n")?;
} else {
let raw = format!("{:04o}\n", old_bits);
let stdout = borrow_fd(STDOUT_FILENO); state::set_status(0);
write(stdout, raw.as_bytes())?; Ok(())
}
state::set_status(0);
Ok(())
} }
#[cfg(test)] #[cfg(test)]
@@ -423,7 +472,8 @@ mod tests {
let opts = get_ulimit_opts(&[ let opts = get_ulimit_opts(&[
Opt::ShortWithArg('n', "256".into()), Opt::ShortWithArg('n', "256".into()),
Opt::ShortWithArg('c', "0".into()), Opt::ShortWithArg('c', "0".into()),
]).unwrap(); ])
.unwrap();
assert_eq!(opts.fds, Some(256)); assert_eq!(opts.fds, Some(256));
assert_eq!(opts.core, Some(0)); assert_eq!(opts.core, Some(0));
assert!(opts.procs.is_none()); assert!(opts.procs.is_none());

View File

@@ -44,128 +44,128 @@ pub fn source(node: Node) -> ShResult<()> {
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use std::io::Write; use std::io::Write;
use tempfile::{NamedTempFile, TempDir}; use crate::state::{self, read_logic, read_vars};
use crate::state::{self, read_logic, read_vars}; use crate::testutil::{TestGuard, test_input};
use crate::testutil::{TestGuard, test_input}; use tempfile::{NamedTempFile, TempDir};
#[test] #[test]
fn source_simple() { fn source_simple() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let mut file = NamedTempFile::new().unwrap(); let mut file = NamedTempFile::new().unwrap();
let path = file.path().display().to_string(); let path = file.path().display().to_string();
file.write_all(b"some_var=some_val").unwrap(); file.write_all(b"some_var=some_val").unwrap();
test_input(format!("source {path}")).unwrap(); test_input(format!("source {path}")).unwrap();
let var = read_vars(|v| v.get_var("some_var")); let var = read_vars(|v| v.get_var("some_var"));
assert_eq!(var, "some_val".to_string()); assert_eq!(var, "some_val".to_string());
} }
#[test] #[test]
fn source_multiple_commands() { fn source_multiple_commands() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let mut file = NamedTempFile::new().unwrap(); let mut file = NamedTempFile::new().unwrap();
let path = file.path().display().to_string(); let path = file.path().display().to_string();
file.write_all(b"x=1\ny=2\nz=3").unwrap(); file.write_all(b"x=1\ny=2\nz=3").unwrap();
test_input(format!("source {path}")).unwrap(); test_input(format!("source {path}")).unwrap();
assert_eq!(read_vars(|v| v.get_var("x")), "1"); assert_eq!(read_vars(|v| v.get_var("x")), "1");
assert_eq!(read_vars(|v| v.get_var("y")), "2"); assert_eq!(read_vars(|v| v.get_var("y")), "2");
assert_eq!(read_vars(|v| v.get_var("z")), "3"); assert_eq!(read_vars(|v| v.get_var("z")), "3");
} }
#[test] #[test]
fn source_defines_function() { fn source_defines_function() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let mut file = NamedTempFile::new().unwrap(); let mut file = NamedTempFile::new().unwrap();
let path = file.path().display().to_string(); let path = file.path().display().to_string();
file.write_all(b"greet() { echo hi; }").unwrap(); file.write_all(b"greet() { echo hi; }").unwrap();
test_input(format!("source {path}")).unwrap(); test_input(format!("source {path}")).unwrap();
let func = read_logic(|l| l.get_func("greet")); let func = read_logic(|l| l.get_func("greet"));
assert!(func.is_some()); assert!(func.is_some());
} }
#[test] #[test]
fn source_defines_alias() { fn source_defines_alias() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let mut file = NamedTempFile::new().unwrap(); let mut file = NamedTempFile::new().unwrap();
let path = file.path().display().to_string(); let path = file.path().display().to_string();
file.write_all(b"alias ll='ls -la'").unwrap(); file.write_all(b"alias ll='ls -la'").unwrap();
test_input(format!("source {path}")).unwrap(); test_input(format!("source {path}")).unwrap();
let alias = read_logic(|l| l.get_alias("ll")); let alias = read_logic(|l| l.get_alias("ll"));
assert!(alias.is_some()); assert!(alias.is_some());
} }
#[test] #[test]
fn source_output_captured() { fn source_output_captured() {
let guard = TestGuard::new(); let guard = TestGuard::new();
let mut file = NamedTempFile::new().unwrap(); let mut file = NamedTempFile::new().unwrap();
let path = file.path().display().to_string(); let path = file.path().display().to_string();
file.write_all(b"echo sourced").unwrap(); file.write_all(b"echo sourced").unwrap();
test_input(format!("source {path}")).unwrap(); test_input(format!("source {path}")).unwrap();
let out = guard.read_output(); let out = guard.read_output();
assert!(out.contains("sourced")); assert!(out.contains("sourced"));
} }
#[test] #[test]
fn source_multiple_files() { fn source_multiple_files() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let mut file1 = NamedTempFile::new().unwrap(); let mut file1 = NamedTempFile::new().unwrap();
let mut file2 = NamedTempFile::new().unwrap(); let mut file2 = NamedTempFile::new().unwrap();
let path1 = file1.path().display().to_string(); let path1 = file1.path().display().to_string();
let path2 = file2.path().display().to_string(); let path2 = file2.path().display().to_string();
file1.write_all(b"a=from_file1").unwrap(); file1.write_all(b"a=from_file1").unwrap();
file2.write_all(b"b=from_file2").unwrap(); file2.write_all(b"b=from_file2").unwrap();
test_input(format!("source {path1} {path2}")).unwrap(); test_input(format!("source {path1} {path2}")).unwrap();
assert_eq!(read_vars(|v| v.get_var("a")), "from_file1"); assert_eq!(read_vars(|v| v.get_var("a")), "from_file1");
assert_eq!(read_vars(|v| v.get_var("b")), "from_file2"); assert_eq!(read_vars(|v| v.get_var("b")), "from_file2");
} }
// ===================== Dot syntax ===================== // ===================== Dot syntax =====================
#[test] #[test]
fn source_dot_syntax() { fn source_dot_syntax() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let mut file = NamedTempFile::new().unwrap(); let mut file = NamedTempFile::new().unwrap();
let path = file.path().display().to_string(); let path = file.path().display().to_string();
file.write_all(b"dot_var=dot_val").unwrap(); file.write_all(b"dot_var=dot_val").unwrap();
test_input(format!(". {path}")).unwrap(); test_input(format!(". {path}")).unwrap();
assert_eq!(read_vars(|v| v.get_var("dot_var")), "dot_val"); assert_eq!(read_vars(|v| v.get_var("dot_var")), "dot_val");
} }
// ===================== Error cases ===================== // ===================== Error cases =====================
#[test] #[test]
fn source_nonexistent_file() { fn source_nonexistent_file() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let result = test_input("source /tmp/__no_such_file_xyz__"); let result = test_input("source /tmp/__no_such_file_xyz__");
assert!(result.is_err()); assert!(result.is_err());
} }
#[test] #[test]
fn source_directory_fails() { fn source_directory_fails() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let result = test_input(format!("source {}", dir.path().display())); let result = test_input(format!("source {}", dir.path().display()));
assert!(result.is_err()); assert!(result.is_err());
} }
// ===================== Status ===================== // ===================== Status =====================
#[test] #[test]
fn source_status_zero() { fn source_status_zero() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let mut file = NamedTempFile::new().unwrap(); let mut file = NamedTempFile::new().unwrap();
let path = file.path().display().to_string(); let path = file.path().display().to_string();
file.write_all(b"true").unwrap(); file.write_all(b"true").unwrap();
test_input(format!("source {path}")).unwrap(); test_input(format!("source {path}")).unwrap();
assert_eq!(state::get_status(), 0); assert_eq!(state::get_status(), 0);
} }
} }

View File

@@ -94,7 +94,10 @@ impl FromStr for TestOp {
"-ge" => Ok(Self::IntGe), "-ge" => Ok(Self::IntGe),
"-le" => Ok(Self::IntLe), "-le" => Ok(Self::IntLe),
_ if TEST_UNARY_OPS.contains(&s) => Ok(Self::Unary(s.parse::<UnaryOp>()?)), _ if TEST_UNARY_OPS.contains(&s) => Ok(Self::Unary(s.parse::<UnaryOp>()?)),
_ => Err(ShErr::simple(ShErrKind::SyntaxErr, format!("Invalid test operator '{}'", s))), _ => Err(ShErr::simple(
ShErrKind::SyntaxErr,
format!("Invalid test operator '{}'", s),
)),
} }
} }
} }
@@ -121,7 +124,7 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
}; };
let mut last_result = false; let mut last_result = false;
let mut conjunct_op: Option<ConjunctOp>; let mut conjunct_op: Option<ConjunctOp>;
log::trace!("test cases: {:#?}", cases); log::trace!("test cases: {:#?}", cases);
for case in cases { for case in cases {
let result = match case { let result = match case {
@@ -305,10 +308,10 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::fs;
use tempfile::{TempDir, NamedTempFile};
use crate::state; use crate::state;
use crate::testutil::{TestGuard, test_input}; use crate::testutil::{TestGuard, test_input};
use std::fs;
use tempfile::{NamedTempFile, TempDir};
// ===================== Unary: file tests ===================== // ===================== Unary: file tests =====================
@@ -590,9 +593,10 @@ mod tests {
fn parse_unary_ops() { fn parse_unary_ops() {
use super::UnaryOp; use super::UnaryOp;
use std::str::FromStr; use std::str::FromStr;
for op in ["-e", "-d", "-f", "-h", "-L", "-r", "-w", "-x", "-s", for op in [
"-p", "-S", "-b", "-c", "-k", "-O", "-G", "-N", "-u", "-e", "-d", "-f", "-h", "-L", "-r", "-w", "-x", "-s", "-p", "-S", "-b", "-c", "-k", "-O",
"-g", "-t", "-n", "-z"] { "-G", "-N", "-u", "-g", "-t", "-n", "-z",
] {
assert!(UnaryOp::from_str(op).is_ok(), "failed to parse {op}"); assert!(UnaryOp::from_str(op).is_ok(), "failed to parse {op}");
} }
} }

View File

@@ -171,10 +171,10 @@ pub fn trap(node: Node) -> ShResult<()> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::TrapTarget; use super::TrapTarget;
use std::str::FromStr;
use nix::sys::signal::Signal;
use crate::state::{self, read_logic}; use crate::state::{self, read_logic};
use crate::testutil::{TestGuard, test_input}; use crate::testutil::{TestGuard, test_input};
use nix::sys::signal::Signal;
use std::str::FromStr;
// ===================== Pure: TrapTarget parsing ===================== // ===================== Pure: TrapTarget parsing =====================
@@ -231,7 +231,9 @@ mod tests {
#[test] #[test]
fn display_signal_roundtrip() { fn display_signal_roundtrip() {
for name in &["INT", "QUIT", "TERM", "USR1", "USR2", "ALRM", "CHLD", "WINCH"] { for name in &[
"INT", "QUIT", "TERM", "USR1", "USR2", "ALRM", "CHLD", "WINCH",
] {
let target = TrapTarget::from_str(name).unwrap(); let target = TrapTarget::from_str(name).unwrap();
assert_eq!(target.to_string(), *name); assert_eq!(target.to_string(), *name);
} }

View File

@@ -245,8 +245,16 @@ mod tests {
test_input("readonly a=1 b=2").unwrap(); test_input("readonly a=1 b=2").unwrap();
assert_eq!(read_vars(|v| v.get_var("a")), "1"); assert_eq!(read_vars(|v| v.get_var("a")), "1");
assert_eq!(read_vars(|v| v.get_var("b")), "2"); assert_eq!(read_vars(|v| v.get_var("b")), "2");
assert!(read_vars(|v| v.get_var_flags("a")).unwrap().contains(VarFlags::READONLY)); assert!(
assert!(read_vars(|v| v.get_var_flags("b")).unwrap().contains(VarFlags::READONLY)); read_vars(|v| v.get_var_flags("a"))
.unwrap()
.contains(VarFlags::READONLY)
);
assert!(
read_vars(|v| v.get_var_flags("b"))
.unwrap()
.contains(VarFlags::READONLY)
);
} }
#[test] #[test]
@@ -385,7 +393,11 @@ mod tests {
let _g = TestGuard::new(); let _g = TestGuard::new();
test_input("local mylocal").unwrap(); test_input("local mylocal").unwrap();
assert_eq!(read_vars(|v| v.get_var("mylocal")), ""); assert_eq!(read_vars(|v| v.get_var("mylocal")), "");
assert!(read_vars(|v| v.get_var_flags("mylocal")).unwrap().contains(VarFlags::LOCAL)); assert!(
read_vars(|v| v.get_var_flags("mylocal"))
.unwrap()
.contains(VarFlags::LOCAL)
);
} }
#[test] #[test]

View File

@@ -639,8 +639,10 @@ pub fn expand_glob(raw: &str) -> ShResult<String> {
{ {
let entry = let entry =
entry.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid filename found in glob"))?; entry.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid filename found in glob"))?;
let entry_raw = entry.to_str().ok_or_else(|| ShErr::simple(ShErrKind::SyntaxErr, "Non-UTF8 filename found in glob"))?; let entry_raw = entry
let escaped = escape_str(entry_raw, true); .to_str()
.ok_or_else(|| ShErr::simple(ShErrKind::SyntaxErr, "Non-UTF8 filename found in glob"))?;
let escaped = escape_str(entry_raw, true);
words.push(escaped) words.push(escaped)
} }
@@ -1327,57 +1329,38 @@ pub fn unescape_str(raw: &str) -> String {
/// Opposite of unescape_str - escapes a string to be executed as literal text /// Opposite of unescape_str - escapes a string to be executed as literal text
/// Used for completion results, and glob filename matches. /// Used for completion results, and glob filename matches.
pub fn escape_str(raw: &str, use_marker: bool) -> String { pub fn escape_str(raw: &str, use_marker: bool) -> String {
let mut result = String::new(); let mut result = String::new();
let mut chars = raw.chars(); let mut chars = raw.chars();
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
'\''| '\'' | '"' | '\\' | '|' | '&' | ';' | '(' | ')' | '<' | '>' | '$' | '*' | '!' | '`' | '{'
'"' | | '?' | '[' | '#' | ' ' | '\t' | '\n' => {
'\\' | if use_marker {
'|' | result.push(markers::ESCAPE);
'&' | } else {
';' | result.push('\\');
'(' | }
')' | result.push(ch);
'<' | continue;
'>' | }
'$' | '~' if result.is_empty() => {
'*' | if use_marker {
'!' | result.push(markers::ESCAPE);
'`' | } else {
'{' | result.push('\\');
'?' | }
'[' | result.push(ch);
'#' | continue;
' ' | }
'\t'| _ => {
'\n' => { result.push(ch);
if use_marker { continue;
result.push(markers::ESCAPE); }
} else { }
result.push('\\'); }
}
result.push(ch);
continue;
}
'~' if result.is_empty() => {
if use_marker {
result.push(markers::ESCAPE);
} else {
result.push('\\');
}
result.push(ch);
continue;
}
_ => {
result.push(ch);
continue;
}
}
}
result result
} }
pub fn unescape_math(raw: &str) -> String { pub fn unescape_math(raw: &str) -> String {
@@ -1657,7 +1640,8 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
ParamExp::RemShortestPrefix(prefix) => { ParamExp::RemShortestPrefix(prefix) => {
let value = vars.get_var(&var_name); let value = vars.get_var(&var_name);
let unescaped = unescape_str(&prefix); let unescaped = unescape_str(&prefix);
let expanded = strip_escape_markers(&expand_raw(&mut unescaped.chars().peekable()).unwrap_or(prefix)); let expanded =
strip_escape_markers(&expand_raw(&mut unescaped.chars().peekable()).unwrap_or(prefix));
let pattern = Pattern::new(&expanded).unwrap(); let pattern = Pattern::new(&expanded).unwrap();
for i in 0..=value.len() { for i in 0..=value.len() {
let sliced = &value[..i]; let sliced = &value[..i];
@@ -1670,7 +1654,8 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
ParamExp::RemLongestPrefix(prefix) => { ParamExp::RemLongestPrefix(prefix) => {
let value = vars.get_var(&var_name); let value = vars.get_var(&var_name);
let unescaped = unescape_str(&prefix); let unescaped = unescape_str(&prefix);
let expanded = strip_escape_markers(&expand_raw(&mut unescaped.chars().peekable()).unwrap_or(prefix)); let expanded =
strip_escape_markers(&expand_raw(&mut unescaped.chars().peekable()).unwrap_or(prefix));
let pattern = Pattern::new(&expanded).unwrap(); let pattern = Pattern::new(&expanded).unwrap();
for i in (0..=value.len()).rev() { for i in (0..=value.len()).rev() {
let sliced = &value[..i]; let sliced = &value[..i];
@@ -1683,7 +1668,8 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
ParamExp::RemShortestSuffix(suffix) => { ParamExp::RemShortestSuffix(suffix) => {
let value = vars.get_var(&var_name); let value = vars.get_var(&var_name);
let unescaped = unescape_str(&suffix); let unescaped = unescape_str(&suffix);
let expanded = strip_escape_markers(&expand_raw(&mut unescaped.chars().peekable()).unwrap_or(suffix)); let expanded =
strip_escape_markers(&expand_raw(&mut unescaped.chars().peekable()).unwrap_or(suffix));
let pattern = Pattern::new(&expanded).unwrap(); let pattern = Pattern::new(&expanded).unwrap();
for i in (0..=value.len()).rev() { for i in (0..=value.len()).rev() {
let sliced = &value[i..]; let sliced = &value[i..];
@@ -1696,8 +1682,9 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
ParamExp::RemLongestSuffix(suffix) => { ParamExp::RemLongestSuffix(suffix) => {
let value = vars.get_var(&var_name); let value = vars.get_var(&var_name);
let unescaped = unescape_str(&suffix); let unescaped = unescape_str(&suffix);
let expanded_suffix = let expanded_suffix = strip_escape_markers(
strip_escape_markers(&expand_raw(&mut unescaped.chars().peekable()).unwrap_or(suffix.clone())); &expand_raw(&mut unescaped.chars().peekable()).unwrap_or(suffix.clone()),
);
let pattern = Pattern::new(&expanded_suffix).unwrap(); let pattern = Pattern::new(&expanded_suffix).unwrap();
for i in 0..=value.len() { for i in 0..=value.len() {
let sliced = &value[i..]; let sliced = &value[i..];
@@ -1711,8 +1698,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
let value = vars.get_var(&var_name); let value = vars.get_var(&var_name);
let search = unescape_str(&search); let search = unescape_str(&search);
let replace = unescape_str(&replace); let replace = unescape_str(&replace);
let expanded_search = strip_escape_markers(&expand_raw(&mut search.chars().peekable()).unwrap_or(search)); let expanded_search =
let expanded_replace = strip_escape_markers(&expand_raw(&mut replace.chars().peekable()).unwrap_or(replace)); strip_escape_markers(&expand_raw(&mut search.chars().peekable()).unwrap_or(search));
let expanded_replace =
strip_escape_markers(&expand_raw(&mut replace.chars().peekable()).unwrap_or(replace));
let regex = glob_to_regex(&expanded_search, false); // unanchored pattern let regex = glob_to_regex(&expanded_search, false); // unanchored pattern
if let Some(mat) = regex.find(&value) { if let Some(mat) = regex.find(&value) {
@@ -1728,8 +1717,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
let value = vars.get_var(&var_name); let value = vars.get_var(&var_name);
let search = unescape_str(&search); let search = unescape_str(&search);
let replace = unescape_str(&replace); let replace = unescape_str(&replace);
let expanded_search = strip_escape_markers(&expand_raw(&mut search.chars().peekable()).unwrap_or(search)); let expanded_search =
let expanded_replace = strip_escape_markers(&expand_raw(&mut replace.chars().peekable()).unwrap_or(replace)); strip_escape_markers(&expand_raw(&mut search.chars().peekable()).unwrap_or(search));
let expanded_replace =
strip_escape_markers(&expand_raw(&mut replace.chars().peekable()).unwrap_or(replace));
let regex = glob_to_regex(&expanded_search, false); let regex = glob_to_regex(&expanded_search, false);
let mut result = String::new(); let mut result = String::new();
let mut last_match_end = 0; let mut last_match_end = 0;
@@ -1748,8 +1739,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
let value = vars.get_var(&var_name); let value = vars.get_var(&var_name);
let search = unescape_str(&search); let search = unescape_str(&search);
let replace = unescape_str(&replace); let replace = unescape_str(&replace);
let expanded_search = strip_escape_markers(&expand_raw(&mut search.chars().peekable()).unwrap_or(search)); let expanded_search =
let expanded_replace = strip_escape_markers(&expand_raw(&mut replace.chars().peekable()).unwrap_or(replace)); strip_escape_markers(&expand_raw(&mut search.chars().peekable()).unwrap_or(search));
let expanded_replace =
strip_escape_markers(&expand_raw(&mut replace.chars().peekable()).unwrap_or(replace));
let pattern = Pattern::new(&expanded_search).unwrap(); let pattern = Pattern::new(&expanded_search).unwrap();
for i in (0..=value.len()).rev() { for i in (0..=value.len()).rev() {
let sliced = &value[..i]; let sliced = &value[..i];
@@ -1763,8 +1756,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
let value = vars.get_var(&var_name); let value = vars.get_var(&var_name);
let search = unescape_str(&search); let search = unescape_str(&search);
let replace = unescape_str(&replace); let replace = unescape_str(&replace);
let expanded_search = strip_escape_markers(&expand_raw(&mut search.chars().peekable()).unwrap_or(search)); let expanded_search =
let expanded_replace = strip_escape_markers(&expand_raw(&mut replace.chars().peekable()).unwrap_or(replace)); strip_escape_markers(&expand_raw(&mut search.chars().peekable()).unwrap_or(search));
let expanded_replace =
strip_escape_markers(&expand_raw(&mut replace.chars().peekable()).unwrap_or(replace));
let pattern = Pattern::new(&expanded_search).unwrap(); let pattern = Pattern::new(&expanded_search).unwrap();
for i in (0..=value.len()).rev() { for i in (0..=value.len()).rev() {
let sliced = &value[i..]; let sliced = &value[i..];
@@ -2455,11 +2450,11 @@ pub fn parse_key_alias(alias: &str) -> Option<KeyEvent> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::time::Duration;
use crate::readline::keys::{KeyCode, KeyEvent, ModKeys};
use crate::state::{write_vars, read_vars, ArrIndex, VarKind, VarFlags};
use crate::parse::lex::Span; use crate::parse::lex::Span;
use crate::readline::keys::{KeyCode, KeyEvent, ModKeys};
use crate::state::{ArrIndex, VarFlags, VarKind, read_vars, write_vars};
use crate::testutil::{TestGuard, test_input}; use crate::testutil::{TestGuard, test_input};
use std::time::Duration;
// ===================== has_braces ===================== // ===================== has_braces =====================
@@ -2599,10 +2594,7 @@ mod tests {
#[test] #[test]
fn braces_simple_list() { fn braces_simple_list() {
assert_eq!( assert_eq!(expand_braces_full("{a,b,c}").unwrap(), vec!["a", "b", "c"]);
expand_braces_full("{a,b,c}").unwrap(),
vec!["a", "b", "c"]
);
} }
#[test] #[test]
@@ -2688,11 +2680,23 @@ mod tests {
assert_eq!(result, vec!["prepost", "preapost"]); assert_eq!(result, vec!["prepost", "preapost"]);
} }
#[test] #[test]
fn braces_cursed() { fn braces_cursed() {
let result = expand_braces_full("foo{a,{1,2,3,{1..4},5},c}{5..1}bar").unwrap(); let result = expand_braces_full("foo{a,{1,2,3,{1..4},5},c}{5..1}bar").unwrap();
assert_eq!(result, vec![ "fooa5bar", "fooa4bar", "fooa3bar", "fooa2bar", "fooa1bar", "foo15bar", "foo14bar", "foo13bar", "foo12bar", "foo11bar", "foo25bar", "foo24bar", "foo23bar", "foo22bar", "foo21bar", "foo35bar", "foo34bar", "foo33bar", "foo32bar", "foo31bar", "foo15bar", "foo14bar", "foo13bar", "foo12bar", "foo11bar", "foo25bar", "foo24bar", "foo23bar", "foo22bar", "foo21bar", "foo35bar", "foo34bar", "foo33bar", "foo32bar", "foo31bar", "foo45bar", "foo44bar", "foo43bar", "foo42bar", "foo41bar", "foo55bar", "foo54bar", "foo53bar", "foo52bar", "foo51bar", "fooc5bar", "fooc4bar", "fooc3bar", "fooc2bar", "fooc1bar", ]) assert_eq!(
} result,
vec![
"fooa5bar", "fooa4bar", "fooa3bar", "fooa2bar", "fooa1bar", "foo15bar", "foo14bar",
"foo13bar", "foo12bar", "foo11bar", "foo25bar", "foo24bar", "foo23bar", "foo22bar",
"foo21bar", "foo35bar", "foo34bar", "foo33bar", "foo32bar", "foo31bar", "foo15bar",
"foo14bar", "foo13bar", "foo12bar", "foo11bar", "foo25bar", "foo24bar", "foo23bar",
"foo22bar", "foo21bar", "foo35bar", "foo34bar", "foo33bar", "foo32bar", "foo31bar",
"foo45bar", "foo44bar", "foo43bar", "foo42bar", "foo41bar", "foo55bar", "foo54bar",
"foo53bar", "foo52bar", "foo51bar", "fooc5bar", "fooc4bar", "fooc3bar", "fooc2bar",
"fooc1bar",
]
)
}
// ===================== Arithmetic ===================== // ===================== Arithmetic =====================
@@ -3164,10 +3168,22 @@ mod tests {
#[test] #[test]
fn key_alias_arrows() { fn key_alias_arrows() {
assert_eq!(parse_key_alias("UP").unwrap(), KeyEvent(KeyCode::Up, ModKeys::NONE)); assert_eq!(
assert_eq!(parse_key_alias("DOWN").unwrap(), KeyEvent(KeyCode::Down, ModKeys::NONE)); parse_key_alias("UP").unwrap(),
assert_eq!(parse_key_alias("LEFT").unwrap(), KeyEvent(KeyCode::Left, ModKeys::NONE)); KeyEvent(KeyCode::Up, ModKeys::NONE)
assert_eq!(parse_key_alias("RIGHT").unwrap(), KeyEvent(KeyCode::Right, ModKeys::NONE)); );
assert_eq!(
parse_key_alias("DOWN").unwrap(),
KeyEvent(KeyCode::Down, ModKeys::NONE)
);
assert_eq!(
parse_key_alias("LEFT").unwrap(),
KeyEvent(KeyCode::Left, ModKeys::NONE)
);
assert_eq!(
parse_key_alias("RIGHT").unwrap(),
KeyEvent(KeyCode::Right, ModKeys::NONE)
);
} }
#[test] #[test]
@@ -3179,7 +3195,13 @@ mod tests {
#[test] #[test]
fn key_alias_ctrl_shift_alt_modifier() { fn key_alias_ctrl_shift_alt_modifier() {
let key = parse_key_alias("C-S-A-b").unwrap(); let key = parse_key_alias("C-S-A-b").unwrap();
assert_eq!(key, KeyEvent(KeyCode::Char('B'), ModKeys::CTRL | ModKeys::SHIFT | ModKeys::ALT)); assert_eq!(
key,
KeyEvent(
KeyCode::Char('B'),
ModKeys::CTRL | ModKeys::SHIFT | ModKeys::ALT
)
);
} }
#[test] #[test]
@@ -3371,7 +3393,14 @@ mod tests {
#[test] #[test]
fn param_remove_shortest_prefix() { fn param_remove_shortest_prefix() {
let _guard = TestGuard::new(); let _guard = TestGuard::new();
write_vars(|v| v.set_var("PATH", VarKind::Str("/usr/local/bin".into()), VarFlags::NONE)).unwrap(); write_vars(|v| {
v.set_var(
"PATH",
VarKind::Str("/usr/local/bin".into()),
VarFlags::NONE,
)
})
.unwrap();
let result = perform_param_expansion("PATH#*/").unwrap(); let result = perform_param_expansion("PATH#*/").unwrap();
assert_eq!(result, "usr/local/bin"); assert_eq!(result, "usr/local/bin");
@@ -3380,7 +3409,14 @@ mod tests {
#[test] #[test]
fn param_remove_longest_prefix() { fn param_remove_longest_prefix() {
let _guard = TestGuard::new(); let _guard = TestGuard::new();
write_vars(|v| v.set_var("PATH", VarKind::Str("/usr/local/bin".into()), VarFlags::NONE)).unwrap(); write_vars(|v| {
v.set_var(
"PATH",
VarKind::Str("/usr/local/bin".into()),
VarFlags::NONE,
)
})
.unwrap();
let result = perform_param_expansion("PATH##*/").unwrap(); let result = perform_param_expansion("PATH##*/").unwrap();
assert_eq!(result, "bin"); assert_eq!(result, "bin");
@@ -3494,7 +3530,9 @@ mod tests {
fn word_split_default_ifs() { fn word_split_default_ifs() {
let _guard = TestGuard::new(); let _guard = TestGuard::new();
let mut exp = Expander { raw: "hello world\tfoo".to_string() }; let mut exp = Expander {
raw: "hello world\tfoo".to_string(),
};
let words = exp.split_words(); let words = exp.split_words();
assert_eq!(words, vec!["hello", "world", "foo"]); assert_eq!(words, vec!["hello", "world", "foo"]);
} }
@@ -3502,9 +3540,13 @@ mod tests {
#[test] #[test]
fn word_split_custom_ifs() { fn word_split_custom_ifs() {
let _guard = TestGuard::new(); let _guard = TestGuard::new();
unsafe { std::env::set_var("IFS", ":"); } unsafe {
std::env::set_var("IFS", ":");
}
let mut exp = Expander { raw: "a:b:c".to_string() }; let mut exp = Expander {
raw: "a:b:c".to_string(),
};
let words = exp.split_words(); let words = exp.split_words();
assert_eq!(words, vec!["a", "b", "c"]); assert_eq!(words, vec!["a", "b", "c"]);
} }
@@ -3512,9 +3554,13 @@ mod tests {
#[test] #[test]
fn word_split_empty_ifs() { fn word_split_empty_ifs() {
let _guard = TestGuard::new(); let _guard = TestGuard::new();
unsafe { std::env::set_var("IFS", ""); } unsafe {
std::env::set_var("IFS", "");
}
let mut exp = Expander { raw: "hello world".to_string() }; let mut exp = Expander {
raw: "hello world".to_string(),
};
let words = exp.split_words(); let words = exp.split_words();
assert_eq!(words, vec!["hello world"]); assert_eq!(words, vec!["hello world"]);
} }
@@ -3554,7 +3600,9 @@ mod tests {
#[test] #[test]
fn word_split_escaped_custom_ifs() { fn word_split_escaped_custom_ifs() {
let _guard = TestGuard::new(); let _guard = TestGuard::new();
unsafe { std::env::set_var("IFS", ":"); } unsafe {
std::env::set_var("IFS", ":");
}
let raw = format!("a{}b:c", unescape_str("\\:")); let raw = format!("a{}b:c", unescape_str("\\:"));
let mut exp = Expander { raw }; let mut exp = Expander { raw };
@@ -3610,8 +3658,13 @@ mod tests {
fn array_index_first() { fn array_index_first() {
let _guard = TestGuard::new(); let _guard = TestGuard::new();
write_vars(|v| { write_vars(|v| {
v.set_var("arr", VarKind::arr_from_vec(vec!["a".into(), "b".into(), "c".into()]), VarFlags::NONE) v.set_var(
}).unwrap(); "arr",
VarKind::arr_from_vec(vec!["a".into(), "b".into(), "c".into()]),
VarFlags::NONE,
)
})
.unwrap();
let val = read_vars(|v| v.index_var("arr", ArrIndex::Literal(0))).unwrap(); let val = read_vars(|v| v.index_var("arr", ArrIndex::Literal(0))).unwrap();
assert_eq!(val, "a"); assert_eq!(val, "a");
@@ -3621,8 +3674,13 @@ mod tests {
fn array_index_second() { fn array_index_second() {
let _guard = TestGuard::new(); let _guard = TestGuard::new();
write_vars(|v| { write_vars(|v| {
v.set_var("arr", VarKind::arr_from_vec(vec!["x".into(), "y".into(), "z".into()]), VarFlags::NONE) v.set_var(
}).unwrap(); "arr",
VarKind::arr_from_vec(vec!["x".into(), "y".into(), "z".into()]),
VarFlags::NONE,
)
})
.unwrap();
let val = read_vars(|v| v.index_var("arr", ArrIndex::Literal(1))).unwrap(); let val = read_vars(|v| v.index_var("arr", ArrIndex::Literal(1))).unwrap();
assert_eq!(val, "y"); assert_eq!(val, "y");
@@ -3632,8 +3690,13 @@ mod tests {
fn array_all_elems() { fn array_all_elems() {
let _guard = TestGuard::new(); let _guard = TestGuard::new();
write_vars(|v| { write_vars(|v| {
v.set_var("arr", VarKind::arr_from_vec(vec!["a".into(), "b".into(), "c".into()]), VarFlags::NONE) v.set_var(
}).unwrap(); "arr",
VarKind::arr_from_vec(vec!["a".into(), "b".into(), "c".into()]),
VarFlags::NONE,
)
})
.unwrap();
let elems = read_vars(|v| v.get_arr_elems("arr")).unwrap(); let elems = read_vars(|v| v.get_arr_elems("arr")).unwrap();
assert_eq!(elems, vec!["a", "b", "c"]); assert_eq!(elems, vec!["a", "b", "c"]);
@@ -3643,8 +3706,13 @@ mod tests {
fn array_elem_count() { fn array_elem_count() {
let _guard = TestGuard::new(); let _guard = TestGuard::new();
write_vars(|v| { write_vars(|v| {
v.set_var("arr", VarKind::arr_from_vec(vec!["a".into(), "b".into(), "c".into()]), VarFlags::NONE) v.set_var(
}).unwrap(); "arr",
VarKind::arr_from_vec(vec!["a".into(), "b".into(), "c".into()]),
VarFlags::NONE,
)
})
.unwrap();
let elems = read_vars(|v| v.get_arr_elems("arr")).unwrap(); let elems = read_vars(|v| v.get_arr_elems("arr")).unwrap();
assert_eq!(elems.len(), 3); assert_eq!(elems.len(), 3);
@@ -3657,7 +3725,9 @@ mod tests {
let _guard = TestGuard::new(); let _guard = TestGuard::new();
let dummy_span = Span::default(); let dummy_span = Span::default();
crate::state::SHED.with(|s| { crate::state::SHED.with(|s| {
s.logic.borrow_mut().insert_alias("ll", "ls -la", dummy_span.clone()); s.logic
.borrow_mut()
.insert_alias("ll", "ls -la", dummy_span.clone());
}); });
let log_tab = crate::state::SHED.with(|s| s.logic.borrow().clone()); let log_tab = crate::state::SHED.with(|s| s.logic.borrow().clone());
@@ -3670,7 +3740,9 @@ mod tests {
let _guard = TestGuard::new(); let _guard = TestGuard::new();
let dummy_span = Span::default(); let dummy_span = Span::default();
crate::state::SHED.with(|s| { crate::state::SHED.with(|s| {
s.logic.borrow_mut().insert_alias("foo", "foo --verbose", dummy_span.clone()); s.logic
.borrow_mut()
.insert_alias("foo", "foo --verbose", dummy_span.clone());
}); });
let log_tab = crate::state::SHED.with(|s| s.logic.borrow().clone()); let log_tab = crate::state::SHED.with(|s| s.logic.borrow().clone());
@@ -3682,26 +3754,47 @@ mod tests {
// ===================== Direct Input Tests (TestGuard) ===================== // ===================== Direct Input Tests (TestGuard) =====================
#[test] #[test]
fn index_simple() { fn index_simple() {
let guard = TestGuard::new(); let guard = TestGuard::new();
write_vars(|v| v.set_var("arr", VarKind::Arr(VecDeque::from(["foo".into(), "bar".into(), "biz".into()])), VarFlags::NONE)).unwrap(); write_vars(|v| {
v.set_var(
"arr",
VarKind::Arr(VecDeque::from(["foo".into(), "bar".into(), "biz".into()])),
VarFlags::NONE,
)
})
.unwrap();
test_input("echo $arr").unwrap(); test_input("echo $arr").unwrap();
let out = guard.read_output(); let out = guard.read_output();
assert_eq!(out, "foo bar biz\n"); assert_eq!(out, "foo bar biz\n");
} }
#[test] #[test]
fn index_cursed() { fn index_cursed() {
let guard = TestGuard::new(); let guard = TestGuard::new();
write_vars(|v| v.set_var("arr", VarKind::Arr(VecDeque::from(["foo".into(), "bar".into(), "biz".into()])), VarFlags::NONE)).unwrap(); write_vars(|v| {
write_vars(|v| v.set_var("i", VarKind::Arr(VecDeque::from(["0".into(), "1".into(), "2".into()])), VarFlags::NONE)).unwrap(); v.set_var(
"arr",
VarKind::Arr(VecDeque::from(["foo".into(), "bar".into(), "biz".into()])),
VarFlags::NONE,
)
})
.unwrap();
write_vars(|v| {
v.set_var(
"i",
VarKind::Arr(VecDeque::from(["0".into(), "1".into(), "2".into()])),
VarFlags::NONE,
)
})
.unwrap();
test_input("echo $echo ${var:-${arr[$(($(echo ${i[0]}) + 1))]}}").unwrap(); test_input("echo $echo ${var:-${arr[$(($(echo ${i[0]}) + 1))]}}").unwrap();
let out = guard.read_output(); let out = guard.read_output();
assert_eq!(out, "bar\n"); assert_eq!(out, "bar\n");
} }
} }

View File

@@ -3,7 +3,11 @@ use std::sync::Arc;
use ariadne::Fmt; use ariadne::Fmt;
use fmt::Display; use fmt::Display;
use crate::{libsh::error::{ShErr, ShErrKind, ShResult, next_color}, parse::lex::Tk, prelude::*}; use crate::{
libsh::error::{ShErr, ShErrKind, ShResult, next_color},
parse::lex::Tk,
prelude::*,
};
pub type OptSet = Arc<[Opt]>; pub type OptSet = Arc<[Opt]>;
@@ -69,20 +73,24 @@ pub fn get_opts(words: Vec<String>) -> (Vec<String>, Vec<Opt>) {
} }
pub fn get_opts_from_tokens_strict( pub fn get_opts_from_tokens_strict(
tokens: Vec<Tk>, tokens: Vec<Tk>,
opt_specs: &[OptSpec], opt_specs: &[OptSpec],
) -> ShResult<(Vec<Tk>, Vec<Opt>)> { ) -> ShResult<(Vec<Tk>, Vec<Opt>)> {
sort_tks(tokens, opt_specs, true) sort_tks(tokens, opt_specs, true)
} }
pub fn get_opts_from_tokens( pub fn get_opts_from_tokens(
tokens: Vec<Tk>, tokens: Vec<Tk>,
opt_specs: &[OptSpec], opt_specs: &[OptSpec],
) -> ShResult<(Vec<Tk>, Vec<Opt>)> { ) -> ShResult<(Vec<Tk>, Vec<Opt>)> {
sort_tks(tokens, opt_specs, false) sort_tks(tokens, opt_specs, false)
} }
pub fn sort_tks(tokens: Vec<Tk>, opt_specs: &[OptSpec], strict: bool) -> ShResult<(Vec<Tk>, Vec<Opt>)> { pub fn sort_tks(
tokens: Vec<Tk>,
opt_specs: &[OptSpec],
strict: bool,
) -> ShResult<(Vec<Tk>, Vec<Opt>)> {
let mut tokens_iter = tokens let mut tokens_iter = tokens
.into_iter() .into_iter()
.map(|t| t.expand()) .map(|t| t.expand())
@@ -125,14 +133,14 @@ pub fn sort_tks(tokens: Vec<Tk>, opt_specs: &[OptSpec], strict: bool) -> ShResul
} }
} }
if !pushed { if !pushed {
if strict { if strict {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::ParseErr, ShErrKind::ParseErr,
format!("Unknown option: {}", opt.to_string().fg(next_color())), format!("Unknown option: {}", opt.to_string().fg(next_color())),
)); ));
} else { } else {
non_opts.push(token.clone()); non_opts.push(token.clone());
} }
} }
} }
} }
@@ -140,12 +148,11 @@ pub fn sort_tks(tokens: Vec<Tk>, opt_specs: &[OptSpec], strict: bool) -> ShResul
Ok((non_opts, opts)) Ok((non_opts, opts))
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::parse::lex::{LexFlags, LexStream}; use crate::parse::lex::{LexFlags, LexStream};
use super::*; use super::*;
#[test] #[test]
fn parse_short_single() { fn parse_short_single() {
@@ -156,7 +163,10 @@ use super::*;
#[test] #[test]
fn parse_short_combined() { fn parse_short_combined() {
let opts = Opt::parse("-abc"); let opts = Opt::parse("-abc");
assert_eq!(opts, vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]); assert_eq!(
opts,
vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]
);
} }
#[test] #[test]
@@ -173,7 +183,12 @@ use super::*;
#[test] #[test]
fn get_opts_basic() { fn get_opts_basic() {
let words = vec!["file.txt".into(), "-v".into(), "--help".into(), "arg".into()]; let words = vec![
"file.txt".into(),
"-v".into(),
"--help".into(),
"arg".into(),
];
let (non_opts, opts) = get_opts(words); let (non_opts, opts) = get_opts(words);
assert_eq!(non_opts, vec!["file.txt", "arg"]); assert_eq!(non_opts, vec!["file.txt", "arg"]);
assert_eq!(opts, vec![Opt::Short('v'), Opt::Long("help".into())]); assert_eq!(opts, vec![Opt::Short('v'), Opt::Long("help".into())]);
@@ -191,7 +206,10 @@ use super::*;
fn get_opts_combined_short() { fn get_opts_combined_short() {
let words = vec!["-abc".into(), "file".into()]; let words = vec!["-abc".into(), "file".into()];
let (non_opts, opts) = get_opts(words); let (non_opts, opts) = get_opts(words);
assert_eq!(opts, vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]); assert_eq!(
opts,
vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]
);
assert_eq!(non_opts, vec!["file"]); assert_eq!(non_opts, vec!["file"]);
} }
@@ -215,128 +233,175 @@ use super::*;
assert_eq!(Opt::Short('v').to_string(), "-v"); assert_eq!(Opt::Short('v').to_string(), "-v");
assert_eq!(Opt::Long("help".into()).to_string(), "--help"); assert_eq!(Opt::Long("help".into()).to_string(), "--help");
assert_eq!(Opt::ShortWithArg('o', "file".into()).to_string(), "-o file"); assert_eq!(Opt::ShortWithArg('o', "file".into()).to_string(), "-o file");
assert_eq!(Opt::LongWithArg("output".into(), "file".into()).to_string(), "--output file"); assert_eq!(
Opt::LongWithArg("output".into(), "file".into()).to_string(),
"--output file"
);
} }
fn lex(input: &str) -> Vec<Tk> { fn lex(input: &str) -> Vec<Tk> {
LexStream::new(Arc::new(input.to_string()), LexFlags::empty()) LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.collect::<ShResult<Vec<Tk>>>() .collect::<ShResult<Vec<Tk>>>()
.unwrap() .unwrap()
} }
#[test] #[test]
fn get_opts_from_tks() { fn get_opts_from_tks() {
let tokens = lex("file.txt --help -v arg"); let tokens = lex("file.txt --help -v arg");
let opt_spec = vec![ let opt_spec = vec![
OptSpec { opt: Opt::Short('v'), takes_arg: false }, OptSpec {
OptSpec { opt: Opt::Long("help".into()), takes_arg: false }, opt: Opt::Short('v'),
]; takes_arg: false,
},
OptSpec {
opt: Opt::Long("help".into()),
takes_arg: false,
},
];
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap(); let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
let mut opts = opts.into_iter(); let mut opts = opts.into_iter();
assert!(opts.any(|o| o == Opt::Short('v') || o == Opt::Long("help".into()))); assert!(opts.any(|o| o == Opt::Short('v') || o == Opt::Long("help".into())));
assert!(opts.any(|o| o == Opt::Short('v') || o == Opt::Long("help".into()))); assert!(opts.any(|o| o == Opt::Short('v') || o == Opt::Long("help".into())));
let mut non_opts = non_opts.into_iter().map(|s| s.to_string()); let mut non_opts = non_opts.into_iter().map(|s| s.to_string());
assert!(non_opts.any(|s| s == "file.txt" || s == "arg")); assert!(non_opts.any(|s| s == "file.txt" || s == "arg"));
assert!(non_opts.any(|s| s == "file.txt" || s == "arg")); assert!(non_opts.any(|s| s == "file.txt" || s == "arg"));
} }
#[test] #[test]
fn tks_short_with_arg() { fn tks_short_with_arg() {
let tokens = lex("-o output.txt file.txt"); let tokens = lex("-o output.txt file.txt");
let opt_spec = vec![ let opt_spec = vec![OptSpec {
OptSpec { opt: Opt::Short('o'), takes_arg: true }, opt: Opt::Short('o'),
]; takes_arg: true,
}];
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap(); let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
assert_eq!(opts, vec![Opt::ShortWithArg('o', "output.txt".into())]); assert_eq!(opts, vec![Opt::ShortWithArg('o', "output.txt".into())]);
let non_opts: Vec<String> = non_opts.into_iter().map(|s| s.to_string()).collect(); let non_opts: Vec<String> = non_opts.into_iter().map(|s| s.to_string()).collect();
assert!(non_opts.contains(&"file.txt".to_string())); assert!(non_opts.contains(&"file.txt".to_string()));
} }
#[test] #[test]
fn tks_long_with_arg() { fn tks_long_with_arg() {
let tokens = lex("--output result.txt input.txt"); let tokens = lex("--output result.txt input.txt");
let opt_spec = vec![ let opt_spec = vec![OptSpec {
OptSpec { opt: Opt::Long("output".into()), takes_arg: true }, opt: Opt::Long("output".into()),
]; takes_arg: true,
}];
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap(); let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
assert_eq!(opts, vec![Opt::LongWithArg("output".into(), "result.txt".into())]); assert_eq!(
let non_opts: Vec<String> = non_opts.into_iter().map(|s| s.to_string()).collect(); opts,
assert!(non_opts.contains(&"input.txt".to_string())); vec![Opt::LongWithArg("output".into(), "result.txt".into())]
} );
let non_opts: Vec<String> = non_opts.into_iter().map(|s| s.to_string()).collect();
assert!(non_opts.contains(&"input.txt".to_string()));
}
#[test] #[test]
fn tks_double_dash_stops() { fn tks_double_dash_stops() {
let tokens = lex("-v -- -a --foo"); let tokens = lex("-v -- -a --foo");
let opt_spec = vec![ let opt_spec = vec![
OptSpec { opt: Opt::Short('v'), takes_arg: false }, OptSpec {
OptSpec { opt: Opt::Short('a'), takes_arg: false }, opt: Opt::Short('v'),
]; takes_arg: false,
},
OptSpec {
opt: Opt::Short('a'),
takes_arg: false,
},
];
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap(); let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
assert_eq!(opts, vec![Opt::Short('v')]); assert_eq!(opts, vec![Opt::Short('v')]);
let non_opts: Vec<String> = non_opts.into_iter().map(|s| s.to_string()).collect(); let non_opts: Vec<String> = non_opts.into_iter().map(|s| s.to_string()).collect();
assert!(non_opts.contains(&"-a".to_string())); assert!(non_opts.contains(&"-a".to_string()));
assert!(non_opts.contains(&"--foo".to_string())); assert!(non_opts.contains(&"--foo".to_string()));
} }
#[test] #[test]
fn tks_combined_short_with_spec() { fn tks_combined_short_with_spec() {
let tokens = lex("-abc"); let tokens = lex("-abc");
let opt_spec = vec![ let opt_spec = vec![
OptSpec { opt: Opt::Short('a'), takes_arg: false }, OptSpec {
OptSpec { opt: Opt::Short('b'), takes_arg: false }, opt: Opt::Short('a'),
OptSpec { opt: Opt::Short('c'), takes_arg: false }, takes_arg: false,
]; },
OptSpec {
opt: Opt::Short('b'),
takes_arg: false,
},
OptSpec {
opt: Opt::Short('c'),
takes_arg: false,
},
];
let (_non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap(); let (_non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
assert_eq!(opts, vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]); assert_eq!(
} opts,
vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]
);
}
#[test] #[test]
fn tks_unknown_opt_becomes_non_opt() { fn tks_unknown_opt_becomes_non_opt() {
let tokens = lex("-v -x file"); let tokens = lex("-v -x file");
let opt_spec = vec![ let opt_spec = vec![OptSpec {
OptSpec { opt: Opt::Short('v'), takes_arg: false }, opt: Opt::Short('v'),
]; takes_arg: false,
}];
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap(); let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
assert_eq!(opts, vec![Opt::Short('v')]); assert_eq!(opts, vec![Opt::Short('v')]);
// -x is not in spec, so its token goes to non_opts // -x is not in spec, so its token goes to non_opts
assert!(non_opts.into_iter().map(|s| s.to_string()).any(|s| s == "-x" || s == "file")); assert!(
} non_opts
.into_iter()
.map(|s| s.to_string())
.any(|s| s == "-x" || s == "file")
);
}
#[test] #[test]
fn tks_mixed_short_and_long_with_args() { fn tks_mixed_short_and_long_with_args() {
let tokens = lex("-n 5 --output file.txt input"); let tokens = lex("-n 5 --output file.txt input");
let opt_spec = vec![ let opt_spec = vec![
OptSpec { opt: Opt::Short('n'), takes_arg: true }, OptSpec {
OptSpec { opt: Opt::Long("output".into()), takes_arg: true }, opt: Opt::Short('n'),
]; takes_arg: true,
},
OptSpec {
opt: Opt::Long("output".into()),
takes_arg: true,
},
];
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap(); let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
assert_eq!(opts, vec![ assert_eq!(
Opt::ShortWithArg('n', "5".into()), opts,
Opt::LongWithArg("output".into(), "file.txt".into()), vec![
]); Opt::ShortWithArg('n', "5".into()),
let non_opts: Vec<String> = non_opts.into_iter().map(|s| s.to_string()).collect(); Opt::LongWithArg("output".into(), "file.txt".into()),
assert!(non_opts.contains(&"input".to_string())); ]
} );
let non_opts: Vec<String> = non_opts.into_iter().map(|s| s.to_string()).collect();
assert!(non_opts.contains(&"input".to_string()));
}
} }

View File

@@ -1,10 +1,10 @@
use ariadne::{Color, Fmt}; use ariadne::{Color, Fmt};
use ariadne::{Report, ReportKind}; use ariadne::{Report, ReportKind};
use rand::TryRng; use rand::TryRng;
use yansi::Paint;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use std::fmt::Display; use std::fmt::Display;
use yansi::Paint;
use crate::procio::RedirGuard; use crate::procio::RedirGuard;
use crate::{ use crate::{
@@ -150,7 +150,7 @@ impl Display for Note {
writeln!(f, "{note}: {main}")?; writeln!(f, "{note}: {main}")?;
} else { } else {
let bar_break = Fmt::fg("-", Color::Cyan); let bar_break = Fmt::fg("-", Color::Cyan);
let bar_break = bar_break.bold(); let bar_break = bar_break.bold();
let indent = " ".repeat(self.depth); let indent = " ".repeat(self.depth);
writeln!(f, " {indent}{bar_break} {main}")?; writeln!(f, " {indent}{bar_break} {main}")?;
} }

View File

@@ -40,7 +40,9 @@ use crate::procio::borrow_fd;
use crate::readline::term::{LineWriter, RawModeGuard, raw_mode}; use crate::readline::term::{LineWriter, RawModeGuard, raw_mode};
use crate::readline::{Prompt, ReadlineEvent, ShedVi}; use crate::readline::{Prompt, ReadlineEvent, ShedVi};
use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending}; use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending};
use crate::state::{AutoCmdKind, read_logic, read_shopts, source_rc, write_jobs, write_meta, write_shopts}; use crate::state::{
AutoCmdKind, read_logic, read_shopts, source_rc, write_jobs, write_meta, write_shopts,
};
use clap::Parser; use clap::Parser;
use state::write_vars; use state::write_vars;
@@ -116,14 +118,15 @@ fn main() -> ExitCode {
return ExitCode::SUCCESS; return ExitCode::SUCCESS;
} }
// Increment SHLVL, or set to 1 if not present or invalid. // Increment SHLVL, or set to 1 if not present or invalid.
// This var represents how many nested shell instances we're in // This var represents how many nested shell instances we're in
if let Ok(var) = env::var("SHLVL") if let Ok(var) = env::var("SHLVL")
&& let Ok(lvl) = var.parse::<u32>() { && let Ok(lvl) = var.parse::<u32>()
unsafe { env::set_var("SHLVL", (lvl + 1).to_string()) }; {
} else { unsafe { env::set_var("SHLVL", (lvl + 1).to_string()) };
unsafe { env::set_var("SHLVL", "1") }; } else {
} unsafe { env::set_var("SHLVL", "1") };
}
if let Err(e) = if let Some(path) = args.script { if let Err(e) = if let Some(path) = args.script {
run_script(path, args.script_args) run_script(path, args.script_args)
@@ -131,8 +134,8 @@ fn main() -> ExitCode {
exec_dash_c(cmd) exec_dash_c(cmd)
} else { } else {
let res = shed_interactive(args); let res = shed_interactive(args);
write(borrow_fd(*TTY_FILENO), b"\x1b[?2004l").ok(); // disable bracketed paste mode on exit write(borrow_fd(*TTY_FILENO), b"\x1b[?2004l").ok(); // disable bracketed paste mode on exit
res res
} { } {
e.print_error(); e.print_error();
}; };
@@ -202,7 +205,7 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
} }
}; };
readline.writer.flush_write("\x1b[?2004h")?; // enable bracketed paste mode readline.writer.flush_write("\x1b[?2004h")?; // enable bracketed paste mode
// Main poll loop // Main poll loop
loop { loop {
@@ -221,9 +224,9 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
readline.reset_active_widget(false)?; readline.reset_active_widget(false)?;
} }
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
_ => e.print_error(), _ => e.print_error(),
} }
} }
@@ -235,7 +238,7 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
// may have moved it during resize/rewrap // may have moved it during resize/rewrap
readline.writer.update_t_cols(); readline.writer.update_t_cols();
readline.mark_dirty(); readline.mark_dirty();
} }
if JOB_DONE.swap(false, Ordering::SeqCst) { if JOB_DONE.swap(false, Ordering::SeqCst) {
// update the prompt so any job count escape sequences update dynamically // update the prompt so any job count escape sequences update dynamically
@@ -250,38 +253,38 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
PollFlags::POLLIN, PollFlags::POLLIN,
)]; )];
let mut exec_if_timeout = None; let mut exec_if_timeout = None;
let timeout = if readline.pending_keymap.is_empty() { let timeout = if readline.pending_keymap.is_empty() {
let screensaver_cmd = read_shopts(|o| o.prompt.screensaver_cmd.clone()); let screensaver_cmd = read_shopts(|o| o.prompt.screensaver_cmd.clone());
let screensaver_idle_time = read_shopts(|o| o.prompt.screensaver_idle_time); let screensaver_idle_time = read_shopts(|o| o.prompt.screensaver_idle_time);
if screensaver_idle_time > 0 && !screensaver_cmd.is_empty() { if screensaver_idle_time > 0 && !screensaver_cmd.is_empty() {
exec_if_timeout = Some(screensaver_cmd); exec_if_timeout = Some(screensaver_cmd);
PollTimeout::from((screensaver_idle_time * 1000) as u16) PollTimeout::from((screensaver_idle_time * 1000) as u16)
} else { } else {
PollTimeout::MAX PollTimeout::MAX
} }
} else { } else {
PollTimeout::from(1000u16) PollTimeout::from(1000u16)
}; };
match poll(&mut fds, timeout) { match poll(&mut fds, timeout) {
Ok(0) => { Ok(0) => {
// We timed out. // We timed out.
if let Some(cmd) = exec_if_timeout { if let Some(cmd) = exec_if_timeout {
let prepared = ReadlineEvent::Line(cmd); let prepared = ReadlineEvent::Line(cmd);
let saved_hist_opt = read_shopts(|o| o.core.auto_hist); let saved_hist_opt = read_shopts(|o| o.core.auto_hist);
let _guard = scopeguard::guard(saved_hist_opt, |opt| { let _guard = scopeguard::guard(saved_hist_opt, |opt| {
write_shopts(|o| o.core.auto_hist = opt); write_shopts(|o| o.core.auto_hist = opt);
}); });
write_shopts(|o| o.core.auto_hist = false); // don't save screensaver command to history write_shopts(|o| o.core.auto_hist = false); // don't save screensaver command to history
match handle_readline_event(&mut readline, Ok(prepared))? { match handle_readline_event(&mut readline, Ok(prepared))? {
true => return Ok(()), true => return Ok(()),
false => continue false => continue,
} }
} }
} }
Err(Errno::EINTR) => { Err(Errno::EINTR) => {
// Interrupted by signal, loop back to handle it // Interrupted by signal, loop back to handle it
continue; continue;

View File

@@ -8,7 +8,28 @@ use ariadne::Fmt;
use crate::{ use crate::{
builtin::{ builtin::{
alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, autocmd::autocmd, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, getopts::getopts, intro, jobctl::{self, JobBehavior, continue_job, disown, jobs}, keymap, map, pwd::pwd, read::{self, read_builtin}, resource::{ulimit, umask_builtin}, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset} alias::{alias, unalias},
arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate},
autocmd::autocmd,
cd::cd,
complete::{compgen_builtin, complete_builtin},
dirstack::{dirs, popd, pushd},
echo::echo,
eval, exec,
flowctl::flowctl,
getopts::getopts,
intro,
jobctl::{self, JobBehavior, continue_job, disown, jobs},
keymap, map,
pwd::pwd,
read::{self, read_builtin},
resource::{ulimit, umask_builtin},
shift::shift,
shopt::shopt,
source::source,
test::double_bracket_test,
trap::{TrapTarget, trap},
varcmds::{export, local, readonly, unset},
}, },
expand::{expand_aliases, expand_case_pattern, glob_to_regex}, expand::{expand_aliases, expand_case_pattern, glob_to_regex},
jobs::{ChildProc, JobStack, attach_tty, dispatch_job}, jobs::{ChildProc, JobStack, attach_tty, dispatch_job},
@@ -136,13 +157,18 @@ pub fn exec_dash_c(input: String) -> ShResult<()> {
if nodes.len() == 1 { if nodes.len() == 1 {
let is_single_cmd = match &nodes[0].class { let is_single_cmd = match &nodes[0].class {
NdRule::Command { .. } => true, NdRule::Command { .. } => true,
NdRule::Pipeline { cmds } => cmds.len() == 1 && matches!(cmds[0].class, NdRule::Command { .. }), NdRule::Pipeline { cmds } => {
cmds.len() == 1 && matches!(cmds[0].class, NdRule::Command { .. })
}
NdRule::Conjunction { elements } => { NdRule::Conjunction { elements } => {
elements.len() == 1 && match &elements[0].cmd.class { elements.len() == 1
NdRule::Pipeline { cmds } => cmds.len() == 1 && matches!(cmds[0].class, NdRule::Command { .. }), && match &elements[0].cmd.class {
NdRule::Command { .. } => true, NdRule::Pipeline { cmds } => {
_ => false, cmds.len() == 1 && matches!(cmds[0].class, NdRule::Command { .. })
} }
NdRule::Command { .. } => true,
_ => false,
}
} }
_ => false, _ => false,
}; };
@@ -151,8 +177,12 @@ pub fn exec_dash_c(input: String) -> ShResult<()> {
let mut node = nodes.remove(0); let mut node = nodes.remove(0);
loop { loop {
match node.class { match node.class {
NdRule::Conjunction { mut elements } => { node = *elements.remove(0).cmd; } NdRule::Conjunction { mut elements } => {
NdRule::Pipeline { mut cmds } => { node = cmds.remove(0); } node = *elements.remove(0).cmd;
}
NdRule::Pipeline { mut cmds } => {
node = cmds.remove(0);
}
NdRule::Command { .. } => break, NdRule::Command { .. } => break,
_ => break, _ => break,
} }
@@ -250,7 +280,7 @@ impl Dispatcher {
NdRule::CaseNode { .. } => self.exec_case(node)?, NdRule::CaseNode { .. } => self.exec_case(node)?,
NdRule::BraceGrp { .. } => self.exec_brc_grp(node)?, NdRule::BraceGrp { .. } => self.exec_brc_grp(node)?,
NdRule::FuncDef { .. } => self.exec_func_def(node)?, NdRule::FuncDef { .. } => self.exec_func_def(node)?,
NdRule::Negate { .. } => self.exec_negated(node)?, NdRule::Negate { .. } => self.exec_negated(node)?,
NdRule::Command { .. } => self.dispatch_cmd(node)?, NdRule::Command { .. } => self.dispatch_cmd(node)?,
NdRule::Test { .. } => self.exec_test(node)?, NdRule::Test { .. } => self.exec_test(node)?,
_ => unreachable!(), _ => unreachable!(),
@@ -258,8 +288,14 @@ impl Dispatcher {
Ok(()) Ok(())
} }
pub fn dispatch_cmd(&mut self, node: Node) -> ShResult<()> { pub fn dispatch_cmd(&mut self, node: Node) -> ShResult<()> {
let (line, _) = node.get_span().clone().line_and_col(); let (line, _) = node.get_span().clone().line_and_col();
write_vars(|v| v.set_var("LINENO", VarKind::Str((line + 1).to_string()), VarFlags::NONE))?; write_vars(|v| {
v.set_var(
"LINENO",
VarKind::Str((line + 1).to_string()),
VarFlags::NONE,
)
})?;
let Some(cmd) = node.get_command() else { let Some(cmd) = node.get_command() else {
return self.exec_cmd(node); // Argv is empty, probably an assignment return self.exec_cmd(node); // Argv is empty, probably an assignment
@@ -288,16 +324,16 @@ impl Dispatcher {
self.exec_cmd(node) self.exec_cmd(node)
} }
} }
pub fn exec_negated(&mut self, node: Node) -> ShResult<()> { pub fn exec_negated(&mut self, node: Node) -> ShResult<()> {
let NdRule::Negate { cmd } = node.class else { let NdRule::Negate { cmd } = node.class else {
unreachable!() unreachable!()
}; };
self.dispatch_node(*cmd)?; self.dispatch_node(*cmd)?;
let status = state::get_status(); let status = state::get_status();
state::set_status(if status == 0 { 1 } else { 0 }); state::set_status(if status == 0 { 1 } else { 0 });
Ok(()) Ok(())
} }
pub fn exec_conjunction(&mut self, conjunction: Node) -> ShResult<()> { pub fn exec_conjunction(&mut self, conjunction: Node) -> ShResult<()> {
let NdRule::Conjunction { elements } = conjunction.class else { let NdRule::Conjunction { elements } = conjunction.class else {
unreachable!() unreachable!()
@@ -364,7 +400,7 @@ impl Dispatcher {
Ok(()) Ok(())
} }
fn exec_subsh(&mut self, subsh: Node) -> ShResult<()> { fn exec_subsh(&mut self, subsh: Node) -> ShResult<()> {
let _blame = subsh.get_span().clone(); let _blame = subsh.get_span().clone();
let NdRule::Command { assignments, argv } = subsh.class else { let NdRule::Command { assignments, argv } = subsh.class else {
unreachable!() unreachable!()
}; };
@@ -769,11 +805,14 @@ impl Dispatcher {
self.fg_job = !is_bg && self.interactive; self.fg_job = !is_bg && self.interactive;
let mut cmd = cmds.into_iter().next().unwrap(); let mut cmd = cmds.into_iter().next().unwrap();
if is_bg && !matches!(cmd.class, NdRule::Command { .. }) { if is_bg && !matches!(cmd.class, NdRule::Command { .. }) {
self.run_fork(&cmd.get_command().map(|t| t.to_string()).unwrap_or_default(), |s| { self.run_fork(
if let Err(e) = s.dispatch_node(cmd) { &cmd.get_command().map(|t| t.to_string()).unwrap_or_default(),
e.print_error(); |s| {
} if let Err(e) = s.dispatch_node(cmd) {
})?; e.print_error();
}
},
)?;
} else { } else {
self.dispatch_node(cmd)?; self.dispatch_node(cmd)?;
} }
@@ -972,8 +1011,8 @@ impl Dispatcher {
"keymap" => keymap::keymap(cmd), "keymap" => keymap::keymap(cmd),
"read_key" => read::read_key(cmd), "read_key" => read::read_key(cmd),
"autocmd" => autocmd(cmd), "autocmd" => autocmd(cmd),
"ulimit" => ulimit(cmd), "ulimit" => ulimit(cmd),
"umask" => umask_builtin(cmd), "umask" => umask_builtin(cmd),
"true" | ":" => { "true" | ":" => {
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
@@ -1331,94 +1370,94 @@ mod tests {
assert_eq!(state::get_status(), 0); assert_eq!(state::get_status(), 0);
} }
// ===================== other stuff ===================== // ===================== other stuff =====================
#[test] #[test]
fn for_loop_var_zip() { fn for_loop_var_zip() {
let g = TestGuard::new(); let g = TestGuard::new();
test_input("for a b in 1 2 3 4 5 6; do echo $a $b; done").unwrap(); test_input("for a b in 1 2 3 4 5 6; do echo $a $b; done").unwrap();
let out = g.read_output(); let out = g.read_output();
assert_eq!(out, "1 2\n3 4\n5 6\n"); assert_eq!(out, "1 2\n3 4\n5 6\n");
} }
#[test] #[test]
fn for_loop_unsets_zipped() { fn for_loop_unsets_zipped() {
let g = TestGuard::new(); let g = TestGuard::new();
test_input("for a b c d in 1 2 3 4 5 6; do echo $a $b $c $d; done").unwrap(); test_input("for a b c d in 1 2 3 4 5 6; do echo $a $b $c $d; done").unwrap();
let out = g.read_output(); let out = g.read_output();
assert_eq!(out, "1 2 3 4\n5 6\n"); assert_eq!(out, "1 2 3 4\n5 6\n");
} }
// ===================== negation (!) status ===================== // ===================== negation (!) status =====================
#[test] #[test]
fn negate_true() { fn negate_true() {
let _g = TestGuard::new(); let _g = TestGuard::new();
test_input("! true").unwrap(); test_input("! true").unwrap();
assert_eq!(state::get_status(), 1); assert_eq!(state::get_status(), 1);
} }
#[test] #[test]
fn negate_false() { fn negate_false() {
let _g = TestGuard::new(); let _g = TestGuard::new();
test_input("! false").unwrap(); test_input("! false").unwrap();
assert_eq!(state::get_status(), 0); assert_eq!(state::get_status(), 0);
} }
#[test] #[test]
fn double_negate_true() { fn double_negate_true() {
let _g = TestGuard::new(); let _g = TestGuard::new();
test_input("! ! true").unwrap(); test_input("! ! true").unwrap();
assert_eq!(state::get_status(), 0); assert_eq!(state::get_status(), 0);
} }
#[test] #[test]
fn double_negate_false() { fn double_negate_false() {
let _g = TestGuard::new(); let _g = TestGuard::new();
test_input("! ! false").unwrap(); test_input("! ! false").unwrap();
assert_eq!(state::get_status(), 1); assert_eq!(state::get_status(), 1);
} }
#[test] #[test]
fn negate_pipeline_last_cmd() { fn negate_pipeline_last_cmd() {
let _g = TestGuard::new(); let _g = TestGuard::new();
// pipeline status = last cmd (false) = 1, negated → 0 // pipeline status = last cmd (false) = 1, negated → 0
test_input("! true | false").unwrap(); test_input("! true | false").unwrap();
assert_eq!(state::get_status(), 0); assert_eq!(state::get_status(), 0);
} }
#[test] #[test]
fn negate_pipeline_last_cmd_true() { fn negate_pipeline_last_cmd_true() {
let _g = TestGuard::new(); let _g = TestGuard::new();
// pipeline status = last cmd (true) = 0, negated → 1 // pipeline status = last cmd (true) = 0, negated → 1
test_input("! false | true").unwrap(); test_input("! false | true").unwrap();
assert_eq!(state::get_status(), 1); assert_eq!(state::get_status(), 1);
} }
#[test] #[test]
fn negate_in_conjunction() { fn negate_in_conjunction() {
let _g = TestGuard::new(); let _g = TestGuard::new();
// ! binds to pipeline, not conjunction: (! (true && false)) && true // ! binds to pipeline, not conjunction: (! (true && false)) && true
test_input("! (true && false) && true").unwrap(); test_input("! (true && false) && true").unwrap();
assert_eq!(state::get_status(), 0); assert_eq!(state::get_status(), 0);
} }
#[test] #[test]
fn negate_in_if_condition() { fn negate_in_if_condition() {
let g = TestGuard::new(); let g = TestGuard::new();
test_input("if ! false; then echo yes; fi").unwrap(); test_input("if ! false; then echo yes; fi").unwrap();
assert_eq!(state::get_status(), 0); assert_eq!(state::get_status(), 0);
assert_eq!(g.read_output(), "yes\n"); assert_eq!(g.read_output(), "yes\n");
} }
#[test] #[test]
fn empty_var_in_test() { fn empty_var_in_test() {
let _g = TestGuard::new(); let _g = TestGuard::new();
// POSIX specifies that a quoted unset variable expands to an empty string, so the shell actually sees `[ -n "" ]`, which returns false // POSIX specifies that a quoted unset variable expands to an empty string, so the shell actually sees `[ -n "" ]`, which returns false
test_input("[ -n \"$EMPTYVAR_PROBABLY_NOT_SET_TO_ANYTHING\" ]").unwrap(); test_input("[ -n \"$EMPTYVAR_PROBABLY_NOT_SET_TO_ANYTHING\" ]").unwrap();
assert_eq!(state::get_status(), 1); assert_eq!(state::get_status(), 1);
// Without quotes, word splitting causes an empty var to be removed entirely, so the shell actually sees `[ -n ]`, testing the value of ']', which returns true // Without quotes, word splitting causes an empty var to be removed entirely, so the shell actually sees `[ -n ]`, testing the value of ']', which returns true
test_input("[ -n $EMPTYVAR_PROBABLY_NOT_SET_TO_ANYTHING ]").unwrap(); test_input("[ -n $EMPTYVAR_PROBABLY_NOT_SET_TO_ANYTHING ]").unwrap();
assert_eq!(state::get_status(), 0); assert_eq!(state::get_status(), 0);
} }
} }

View File

@@ -19,7 +19,7 @@ use crate::{
pub const KEYWORDS: [&str; 17] = [ pub const KEYWORDS: [&str; 17] = [
"if", "then", "elif", "else", "fi", "while", "until", "select", "for", "in", "do", "done", "if", "then", "elif", "else", "fi", "while", "until", "select", "for", "in", "do", "done",
"case", "esac", "[[", "]]", "!" "case", "esac", "[[", "]]", "!",
]; ];
pub const OPENERS: [&str; 6] = ["if", "while", "until", "for", "select", "case"]; pub const OPENERS: [&str; 6] = ["if", "while", "until", "for", "select", "case"];
@@ -166,7 +166,7 @@ pub enum TkRule {
ErrPipe, ErrPipe,
And, And,
Or, Or,
Bang, Bang,
Bg, Bg,
Sep, Sep,
Redir, Redir,
@@ -883,14 +883,14 @@ impl Iterator for LexStream {
return self.next(); return self.next();
} }
} }
'!' if self.next_is_cmd() => { '!' if self.next_is_cmd() => {
self.cursor += 1; self.cursor += 1;
let tk_type = TkRule::Bang; let tk_type = TkRule::Bang;
let mut tk = self.get_token((self.cursor - 1)..self.cursor, tk_type); let mut tk = self.get_token((self.cursor - 1)..self.cursor, tk_type);
tk.flags |= TkFlags::KEYWORD; tk.flags |= TkFlags::KEYWORD;
tk tk
} }
'|' => { '|' => {
let ch_idx = self.cursor; let ch_idx = self.cursor;
self.cursor += 1; self.cursor += 1;

File diff suppressed because one or more lines are too long

View File

@@ -33,5 +33,4 @@ pub use nix::{
}, },
}; };
// Additional utilities, if needed, can be added here // Additional utilities, if needed, can be added here

View File

@@ -389,154 +389,166 @@ impl Iterator for PipeGenerator {
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use crate::testutil::{TestGuard, has_cmd, has_cmds, test_input}; use crate::testutil::{TestGuard, has_cmd, has_cmds, test_input};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
#[test] #[test]
fn pipeline_simple() { fn pipeline_simple() {
if !has_cmd("sed") { return }; if !has_cmd("sed") {
let g = TestGuard::new(); return;
};
let g = TestGuard::new();
test_input("echo foo | sed 's/foo/bar/'").unwrap(); test_input("echo foo | sed 's/foo/bar/'").unwrap();
let out = g.read_output(); let out = g.read_output();
assert_eq!(out, "bar\n"); assert_eq!(out, "bar\n");
} }
#[test] #[test]
fn pipeline_multi() { fn pipeline_multi() {
if !has_cmds(&[ if !has_cmds(&["cut", "sed"]) {
"cut", return;
"sed" }
]) { return; } let g = TestGuard::new();
let g = TestGuard::new();
test_input("echo foo bar baz | cut -d ' ' -f 2 | sed 's/a/A/'").unwrap(); test_input("echo foo bar baz | cut -d ' ' -f 2 | sed 's/a/A/'").unwrap();
let out = g.read_output(); let out = g.read_output();
assert_eq!(out, "bAr\n"); assert_eq!(out, "bAr\n");
} }
#[test] #[test]
fn rube_goldberg_pipeline() { fn rube_goldberg_pipeline() {
if !has_cmds(&[ if !has_cmds(&["sed", "cat"]) {
"sed", return;
"cat", }
]) { return } let g = TestGuard::new();
let g = TestGuard::new();
test_input("{ echo foo; echo bar } | if cat; then :; else echo failed; fi | (read line && echo $line | sed 's/foo/baz/'; sed 's/bar/buzz/')").unwrap(); test_input("{ echo foo; echo bar } | if cat; then :; else echo failed; fi | (read line && echo $line | sed 's/foo/baz/'; sed 's/bar/buzz/')").unwrap();
let out = g.read_output(); let out = g.read_output();
assert_eq!(out, "baz\nbuzz\n"); assert_eq!(out, "baz\nbuzz\n");
} }
#[test] #[test]
fn simple_file_redir() { fn simple_file_redir() {
let mut g = TestGuard::new(); let mut g = TestGuard::new();
test_input("echo this is in a file > /tmp/simple_file_redir.txt").unwrap(); test_input("echo this is in a file > /tmp/simple_file_redir.txt").unwrap();
g.add_cleanup(|| { std::fs::remove_file("/tmp/simple_file_redir.txt").ok(); }); g.add_cleanup(|| {
let contents = std::fs::read_to_string("/tmp/simple_file_redir.txt").unwrap(); std::fs::remove_file("/tmp/simple_file_redir.txt").ok();
});
let contents = std::fs::read_to_string("/tmp/simple_file_redir.txt").unwrap();
assert_eq!(contents, "this is in a file\n"); assert_eq!(contents, "this is in a file\n");
} }
#[test] #[test]
fn append_file_redir() { fn append_file_redir() {
let dir = tempfile::TempDir::new().unwrap(); let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("append.txt"); let path = dir.path().join("append.txt");
let _g = TestGuard::new(); let _g = TestGuard::new();
test_input(format!("echo first > {}", path.display())).unwrap(); test_input(format!("echo first > {}", path.display())).unwrap();
test_input(format!("echo second >> {}", path.display())).unwrap(); test_input(format!("echo second >> {}", path.display())).unwrap();
let contents = std::fs::read_to_string(&path).unwrap(); let contents = std::fs::read_to_string(&path).unwrap();
assert_eq!(contents, "first\nsecond\n"); assert_eq!(contents, "first\nsecond\n");
} }
#[test] #[test]
fn input_redir() { fn input_redir() {
if !has_cmd("cat") { return; } if !has_cmd("cat") {
let dir = tempfile::TempDir::new().unwrap(); return;
let path = dir.path().join("input.txt"); }
std::fs::write(&path, "hello from file\n").unwrap(); let dir = tempfile::TempDir::new().unwrap();
let g = TestGuard::new(); let path = dir.path().join("input.txt");
std::fs::write(&path, "hello from file\n").unwrap();
let g = TestGuard::new();
test_input(format!("cat < {}", path.display())).unwrap(); test_input(format!("cat < {}", path.display())).unwrap();
let out = g.read_output(); let out = g.read_output();
assert_eq!(out, "hello from file\n"); assert_eq!(out, "hello from file\n");
} }
#[test] #[test]
fn stderr_redir_to_file() { fn stderr_redir_to_file() {
let dir = tempfile::TempDir::new().unwrap(); let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("err.txt"); let path = dir.path().join("err.txt");
let g = TestGuard::new(); let g = TestGuard::new();
test_input(format!("echo error msg 2> {} >&2", path.display())).unwrap(); test_input(format!("echo error msg 2> {} >&2", path.display())).unwrap();
let contents = std::fs::read_to_string(&path).unwrap(); let contents = std::fs::read_to_string(&path).unwrap();
assert_eq!(contents, "error msg\n"); assert_eq!(contents, "error msg\n");
// stdout should be empty since we redirected to stderr // stdout should be empty since we redirected to stderr
let out = g.read_output(); let out = g.read_output();
assert_eq!(out, ""); assert_eq!(out, "");
} }
#[test] #[test]
fn pipe_and_stderr() { fn pipe_and_stderr() {
if !has_cmd("cat") { return; } if !has_cmd("cat") {
let g = TestGuard::new(); return;
}
let g = TestGuard::new();
test_input("echo on stderr >&2 |& cat").unwrap(); test_input("echo on stderr >&2 |& cat").unwrap();
let out = g.read_output(); let out = g.read_output();
assert_eq!(out, "on stderr\n"); assert_eq!(out, "on stderr\n");
} }
#[test] #[test]
fn output_redir_clobber() { fn output_redir_clobber() {
let dir = tempfile::TempDir::new().unwrap(); let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("clobber.txt"); let path = dir.path().join("clobber.txt");
let _g = TestGuard::new(); let _g = TestGuard::new();
test_input(format!("echo first > {}", path.display())).unwrap(); test_input(format!("echo first > {}", path.display())).unwrap();
test_input(format!("echo second > {}", path.display())).unwrap(); test_input(format!("echo second > {}", path.display())).unwrap();
let contents = std::fs::read_to_string(&path).unwrap(); let contents = std::fs::read_to_string(&path).unwrap();
assert_eq!(contents, "second\n"); assert_eq!(contents, "second\n");
} }
#[test] #[test]
fn pipeline_preserves_exit_status() { fn pipeline_preserves_exit_status() {
if !has_cmd("cat") { return; } if !has_cmd("cat") {
let _g = TestGuard::new(); return;
}
let _g = TestGuard::new();
test_input("false | cat").unwrap(); test_input("false | cat").unwrap();
// Pipeline exit status is the last command // Pipeline exit status is the last command
let status = crate::state::get_status(); let status = crate::state::get_status();
assert_eq!(status, 0); assert_eq!(status, 0);
test_input("cat < /dev/null | false").unwrap(); test_input("cat < /dev/null | false").unwrap();
let status = crate::state::get_status(); let status = crate::state::get_status();
assert_ne!(status, 0); assert_ne!(status, 0);
} }
#[test] #[test]
fn fd_duplication() { fn fd_duplication() {
let dir = tempfile::TempDir::new().unwrap(); let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("dup.txt"); let path = dir.path().join("dup.txt");
let _g = TestGuard::new(); let _g = TestGuard::new();
// Redirect stdout to file, then dup stderr to stdout — both should go to file // Redirect stdout to file, then dup stderr to stdout — both should go to file
test_input(format!("{{ echo out; echo err >&2 }} > {} 2>&1", path.display())).unwrap(); test_input(format!(
"{{ echo out; echo err >&2 }} > {} 2>&1",
path.display()
))
.unwrap();
let contents = std::fs::read_to_string(&path).unwrap(); let contents = std::fs::read_to_string(&path).unwrap();
assert!(contents.contains("out")); assert!(contents.contains("out"));
assert!(contents.contains("err")); assert!(contents.contains("err"));
} }
} }

View File

@@ -9,17 +9,24 @@ use nix::sys::signal::Signal;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use crate::{ use crate::{
builtin::complete::{CompFlags, CompOptFlags, CompOpts}, expand::escape_str, libsh::{error::ShResult, guards::var_ctx_guard, sys::TTY_FILENO, utils::TkVecUtils}, parse::{ builtin::complete::{CompFlags, CompOptFlags, CompOpts},
expand::escape_str,
libsh::{error::ShResult, guards::var_ctx_guard, sys::TTY_FILENO, utils::TkVecUtils},
parse::{
execute::exec_input, execute::exec_input,
lex::{self, LexFlags, Tk, TkRule, ends_with_unescaped}, lex::{self, LexFlags, Tk, TkRule, ends_with_unescaped},
}, readline::{ },
readline::{
Marker, annotate_input_recursive, Marker, annotate_input_recursive,
keys::{KeyCode as C, KeyEvent as K, ModKeys as M}, keys::{KeyCode as C, KeyEvent as K, ModKeys as M},
linebuf::{ClampedUsize, LineBuf}, linebuf::{ClampedUsize, LineBuf},
markers::{self, is_marker}, markers::{self, is_marker},
term::{LineWriter, TermWriter, calc_str_width, get_win_size}, term::{LineWriter, TermWriter, calc_str_width, get_win_size},
vimode::{ViInsert, ViMode}, vimode::{ViInsert, ViMode},
}, state::{VarFlags, VarKind, read_jobs, read_logic, read_meta, read_shopts, read_vars, write_vars} },
state::{
VarFlags, VarKind, read_jobs, read_logic, read_meta, read_shopts, read_vars, write_vars,
},
}; };
pub fn complete_signals(start: &str) -> Vec<String> { pub fn complete_signals(start: &str) -> Vec<String> {
@@ -170,10 +177,10 @@ fn complete_commands(start: &str) -> Vec<String> {
.collect() .collect()
}); });
if read_shopts(|o| o.core.autocd) { if read_shopts(|o| o.core.autocd) {
let dirs = complete_dirs(start); let dirs = complete_dirs(start);
candidates.extend(dirs); candidates.extend(dirs);
} }
candidates.sort(); candidates.sort();
candidates candidates
@@ -561,15 +568,17 @@ pub trait Completer {
fn reset(&mut self); fn reset(&mut self);
fn reset_stay_active(&mut self); fn reset_stay_active(&mut self);
fn is_active(&self) -> bool; fn is_active(&self) -> bool;
fn all_candidates(&self) -> Vec<String> { vec![] } fn all_candidates(&self) -> Vec<String> {
vec![]
}
fn selected_candidate(&self) -> Option<String>; fn selected_candidate(&self) -> Option<String>;
fn token_span(&self) -> (usize, usize); fn token_span(&self) -> (usize, usize);
fn original_input(&self) -> &str; fn original_input(&self) -> &str;
fn token(&self) -> &str { fn token(&self) -> &str {
let orig = self.original_input(); let orig = self.original_input();
let (s,e) = self.token_span(); let (s, e) = self.token_span();
orig.get(s..e).unwrap_or(orig) orig.get(s..e).unwrap_or(orig)
} }
fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()>; fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()>;
fn clear(&mut self, _writer: &mut TermWriter) -> ShResult<()> { fn clear(&mut self, _writer: &mut TermWriter) -> ShResult<()> {
Ok(()) Ok(())
@@ -789,21 +798,21 @@ impl FuzzySelector {
} }
} }
pub fn candidates(&self) -> &[String] { pub fn candidates(&self) -> &[String] {
&self.candidates &self.candidates
} }
pub fn filtered(&self) -> &[ScoredCandidate] { pub fn filtered(&self) -> &[ScoredCandidate] {
&self.filtered &self.filtered
} }
pub fn filtered_len(&self) -> usize { pub fn filtered_len(&self) -> usize {
self.filtered.len() self.filtered.len()
} }
pub fn candidates_len(&self) -> usize { pub fn candidates_len(&self) -> usize {
self.candidates.len() self.candidates.len()
} }
pub fn activate(&mut self, candidates: Vec<String>) { pub fn activate(&mut self, candidates: Vec<String>) {
self.active = true; self.active = true;
@@ -1158,9 +1167,9 @@ impl Default for FuzzyCompleter {
} }
impl Completer for FuzzyCompleter { impl Completer for FuzzyCompleter {
fn all_candidates(&self) -> Vec<String> { fn all_candidates(&self) -> Vec<String> {
self.selector.candidates.clone() self.selector.candidates.clone()
} }
fn set_prompt_line_context(&mut self, line_width: u16, cursor_col: u16) { fn set_prompt_line_context(&mut self, line_width: u16, cursor_col: u16) {
self self
.selector .selector
@@ -1174,10 +1183,14 @@ impl Completer for FuzzyCompleter {
let selected = self.selector.selected_candidate().unwrap_or_default(); let selected = self.selector.selected_candidate().unwrap_or_default();
let (mut start, end) = self.completer.token_span; let (mut start, end) = self.completer.token_span;
let slice = self.completer.original_input.get(start..end).unwrap_or_default(); let slice = self
start += slice.width(); .completer
let completion = selected.strip_prefix(slice).unwrap_or(&selected); .original_input
let escaped = escape_str(completion, false); .get(start..end)
.unwrap_or_default();
start += slice.width();
let completion = selected.strip_prefix(slice).unwrap_or(&selected);
let escaped = escape_str(completion, false);
let ret = format!( let ret = format!(
"{}{}{}", "{}{}{}",
&self.completer.original_input[..start], &self.completer.original_input[..start],
@@ -1253,9 +1266,9 @@ pub struct SimpleCompleter {
} }
impl Completer for SimpleCompleter { impl Completer for SimpleCompleter {
fn all_candidates(&self) -> Vec<String> { fn all_candidates(&self) -> Vec<String> {
self.candidates.clone() self.candidates.clone()
} }
fn reset_stay_active(&mut self) { fn reset_stay_active(&mut self) {
let active = self.is_active(); let active = self.is_active();
self.reset(); self.reset();
@@ -1435,10 +1448,10 @@ impl SimpleCompleter {
let selected = &self.candidates[self.selected_idx]; let selected = &self.candidates[self.selected_idx];
let (mut start, end) = self.token_span; let (mut start, end) = self.token_span;
let slice = self.original_input.get(start..end).unwrap_or(""); let slice = self.original_input.get(start..end).unwrap_or("");
start += slice.width(); start += slice.width();
let completion = selected.strip_prefix(slice).unwrap_or(selected); let completion = selected.strip_prefix(slice).unwrap_or(selected);
let escaped = escape_str(completion, false); let escaped = escape_str(completion, false);
format!( format!(
"{}{}{}", "{}{}{}",
&self.original_input[..start], &self.original_input[..start],
@@ -1604,11 +1617,13 @@ impl SimpleCompleter {
// If token contains any COMP_WORDBREAKS, break the word // If token contains any COMP_WORDBREAKS, break the word
let token_str = cur_token.span.as_str(); let token_str = cur_token.span.as_str();
let word_breaks = read_vars(|v| v.try_get_var("COMP_WORDBREAKS")).unwrap_or("=".into()); let word_breaks = read_vars(|v| v.try_get_var("COMP_WORDBREAKS")).unwrap_or("=".into());
if let Some(break_pos) = token_str.rfind(|c: char| word_breaks.contains(c)) { if let Some(break_pos) = token_str.rfind(|c: char| word_breaks.contains(c)) {
self.token_span.0 = cur_token.span.range().start + break_pos + 1; self.token_span.0 = cur_token.span.range().start + break_pos + 1;
cur_token.span.set_range(self.token_span.0..self.token_span.1); cur_token
} .span
.set_range(self.token_span.0..self.token_span.1);
}
let raw_tk = cur_token.as_str().to_string(); let raw_tk = cur_token.as_str().to_string();
let expanded_tk = cur_token.expand()?; let expanded_tk = cur_token.expand()?;
@@ -1654,12 +1669,12 @@ impl SimpleCompleter {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::os::fd::AsRawFd;
use crate::{ use crate::{
readline::{Prompt, ShedVi}, readline::{Prompt, ShedVi},
state::{VarFlags, VarKind, write_vars}, state::{VarFlags, VarKind, write_vars},
testutil::TestGuard, testutil::TestGuard,
}; };
use std::os::fd::AsRawFd;
fn test_vi(initial: &str) -> (ShedVi, TestGuard) { fn test_vi(initial: &str) -> (ShedVi, TestGuard) {
let g = TestGuard::new(); let g = TestGuard::new();
@@ -1793,13 +1808,20 @@ mod tests {
let _ = comp.get_candidates(line.clone(), cursor); let _ = comp.get_candidates(line.clone(), cursor);
let eq_idx = line.find('=').unwrap(); let eq_idx = line.find('=').unwrap();
assert_eq!(comp.token_span.0, eq_idx + 1, "token_span.0 ({}) should be right after '=' ({})", comp.token_span.0, eq_idx); assert_eq!(
comp.token_span.0,
eq_idx + 1,
"token_span.0 ({}) should be right after '=' ({})",
comp.token_span.0,
eq_idx
);
} }
#[test] #[test]
fn wordbreak_colon_when_set() { fn wordbreak_colon_when_set() {
let _g = TestGuard::new(); let _g = TestGuard::new();
write_vars(|v| v.set_var("COMP_WORDBREAKS", VarKind::Str("=:".into()), VarFlags::NONE)).unwrap(); write_vars(|v| v.set_var("COMP_WORDBREAKS", VarKind::Str("=:".into()), VarFlags::NONE))
.unwrap();
let mut comp = SimpleCompleter::new(); let mut comp = SimpleCompleter::new();
let line = "scp host:foo".to_string(); let line = "scp host:foo".to_string();
@@ -1807,13 +1829,20 @@ mod tests {
let _ = comp.get_candidates(line.clone(), cursor); let _ = comp.get_candidates(line.clone(), cursor);
let colon_idx = line.find(':').unwrap(); let colon_idx = line.find(':').unwrap();
assert_eq!(comp.token_span.0, colon_idx + 1, "token_span.0 ({}) should be right after ':' ({})", comp.token_span.0, colon_idx); assert_eq!(
comp.token_span.0,
colon_idx + 1,
"token_span.0 ({}) should be right after ':' ({})",
comp.token_span.0,
colon_idx
);
} }
#[test] #[test]
fn wordbreak_rightmost_wins() { fn wordbreak_rightmost_wins() {
let _g = TestGuard::new(); let _g = TestGuard::new();
write_vars(|v| v.set_var("COMP_WORDBREAKS", VarKind::Str("=:".into()), VarFlags::NONE)).unwrap(); write_vars(|v| v.set_var("COMP_WORDBREAKS", VarKind::Str("=:".into()), VarFlags::NONE))
.unwrap();
let mut comp = SimpleCompleter::new(); let mut comp = SimpleCompleter::new();
let line = "cmd --opt=host:val".to_string(); let line = "cmd --opt=host:val".to_string();
@@ -1821,7 +1850,11 @@ mod tests {
let _ = comp.get_candidates(line.clone(), cursor); let _ = comp.get_candidates(line.clone(), cursor);
let colon_idx = line.rfind(':').unwrap(); let colon_idx = line.rfind(':').unwrap();
assert_eq!(comp.token_span.0, colon_idx + 1, "should break at rightmost wordbreak char"); assert_eq!(
comp.token_span.0,
colon_idx + 1,
"should break at rightmost wordbreak char"
);
} }
// ===================== SimpleCompleter cycling ===================== // ===================== SimpleCompleter cycling =====================
@@ -1884,7 +1917,10 @@ mod tests {
#[test] #[test]
fn escape_str_all_shell_metacharacters() { fn escape_str_all_shell_metacharacters() {
use crate::expand::escape_str; use crate::expand::escape_str;
for ch in ['\'', '"', '\\', '|', '&', ';', '(', ')', '<', '>', '$', '*', '!', '`', '{', '?', '[', '#', ' ', '\t', '\n'] { for ch in [
'\'', '"', '\\', '|', '&', ';', '(', ')', '<', '>', '$', '*', '!', '`', '{', '?', '[', '#',
' ', '\t', '\n',
] {
let input = format!("a{ch}b"); let input = format!("a{ch}b");
let escaped = escape_str(&input, false); let escaped = escape_str(&input, false);
let expected = format!("a\\{ch}b"); let expected = format!("a\\{ch}b");

View File

@@ -88,7 +88,9 @@ impl Highlighter {
while prefix_chars.peek().is_some() { while prefix_chars.peek().is_some() {
match chars.next() { match chars.next() {
Some(c) if c == markers::VISUAL_MODE_START || c == markers::VISUAL_MODE_END => continue, Some(c) if c == markers::VISUAL_MODE_START || c == markers::VISUAL_MODE_END => continue,
Some(c) if Some(&c) == prefix_chars.peek() => { prefix_chars.next(); } Some(c) if Some(&c) == prefix_chars.peek() => {
prefix_chars.next();
}
_ => return text.to_string(), // mismatch, return original _ => return text.to_string(), // mismatch, return original
} }
} }
@@ -104,7 +106,9 @@ impl Highlighter {
let mut si = suffix_chars.len(); let mut si = suffix_chars.len();
while si > 0 { while si > 0 {
if ti == 0 { return text.to_string(); } if ti == 0 {
return text.to_string();
}
ti -= 1; ti -= 1;
if chars[ti] == markers::VISUAL_MODE_START || chars[ti] == markers::VISUAL_MODE_END { if chars[ti] == markers::VISUAL_MODE_START || chars[ti] == markers::VISUAL_MODE_END {
continue; // skip visual markers continue; // skip visual markers
@@ -346,7 +350,9 @@ impl Highlighter {
recursive_highlighter.highlight(); recursive_highlighter.highlight();
// Read back visual state — selection may have started/ended inside // Read back visual state — selection may have started/ended inside
self.in_selection = recursive_highlighter.in_selection; self.in_selection = recursive_highlighter.in_selection;
self.style_stack.append(&mut recursive_highlighter.style_stack); self
.style_stack
.append(&mut recursive_highlighter.style_stack);
if selection_at_entry { if selection_at_entry {
self.emit_style(Style::BgWhite | Style::Black); self.emit_style(Style::BgWhite | Style::Black);
self.output.push_str(prefix); self.output.push_str(prefix);

View File

@@ -500,12 +500,8 @@ mod tests {
env::set_var(key, val); env::set_var(key, val);
} }
guard(prev, move |p| match p { guard(prev, move |p| match p {
Some(v) => unsafe { Some(v) => unsafe { env::set_var(key, v) },
env::set_var(key, v) None => unsafe { env::remove_var(key) },
},
None => unsafe {
env::remove_var(key)
},
}) })
} }
@@ -522,12 +518,7 @@ mod tests {
fn write_history_file(path: &Path) { fn write_history_file(path: &Path) {
fs::write( fs::write(
path, path,
[ [": 1;1;first\n", ": 2;1;second\n", ": 3;1;third\n"].concat(),
": 1;1;first\n",
": 2;1;second\n",
": 3;1;third\n",
]
.concat(),
) )
.unwrap(); .unwrap();
} }
@@ -586,12 +577,7 @@ mod tests {
let hist_path = tmp.path().join("history"); let hist_path = tmp.path().join("history");
fs::write( fs::write(
&hist_path, &hist_path,
[ [": 1;1;repeat\n", ": 2;1;unique\n", ": 3;1;repeat\n"].concat(),
": 1;1;repeat\n",
": 2;1;unique\n",
": 3;1;repeat\n",
]
.concat(),
) )
.unwrap(); .unwrap();

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,8 @@ use crate::readline::complete::{FuzzyCompleter, SelectorResponse};
use crate::readline::term::{Pos, TermReader, calc_str_width}; use crate::readline::term::{Pos, TermReader, calc_str_width};
use crate::readline::vimode::{ViEx, ViVerbatim}; use crate::readline::vimode::{ViEx, ViVerbatim};
use crate::state::{ use crate::state::{
AutoCmdKind, ShellParam, Var, VarFlags, VarKind, read_logic, read_shopts, with_vars, write_meta, write_vars AutoCmdKind, ShellParam, Var, VarFlags, VarKind, read_logic, read_shopts, with_vars, write_meta,
write_vars,
}; };
use crate::{ use crate::{
libsh::error::ShResult, libsh::error::ShResult,
@@ -240,7 +241,7 @@ impl Default for Prompt {
pub struct ShedVi { pub struct ShedVi {
pub reader: PollReader, pub reader: PollReader,
pub writer: TermWriter, pub writer: TermWriter,
pub tty: RawFd, pub tty: RawFd,
pub prompt: Prompt, pub prompt: Prompt,
pub highlighter: Highlighter, pub highlighter: Highlighter,
@@ -266,7 +267,7 @@ impl ShedVi {
reader: PollReader::new(), reader: PollReader::new(),
writer: TermWriter::new(tty), writer: TermWriter::new(tty),
prompt, prompt,
tty, tty,
completer: Box::new(FuzzyCompleter::default()), completer: Box::new(FuzzyCompleter::default()),
highlighter: Highlighter::new(), highlighter: Highlighter::new(),
mode: Box::new(ViInsert::new()), mode: Box::new(ViInsert::new()),
@@ -293,37 +294,37 @@ impl ShedVi {
Ok(new) Ok(new)
} }
pub fn new_no_hist(prompt: Prompt, tty: RawFd) -> ShResult<Self> { pub fn new_no_hist(prompt: Prompt, tty: RawFd) -> ShResult<Self> {
let mut new = Self { let mut new = Self {
reader: PollReader::new(), reader: PollReader::new(),
writer: TermWriter::new(tty), writer: TermWriter::new(tty),
tty, tty,
prompt, prompt,
completer: Box::new(FuzzyCompleter::default()), completer: Box::new(FuzzyCompleter::default()),
highlighter: Highlighter::new(), highlighter: Highlighter::new(),
mode: Box::new(ViInsert::new()), mode: Box::new(ViInsert::new()),
next_is_escaped: false, next_is_escaped: false,
saved_mode: None, saved_mode: None,
pending_keymap: Vec::new(), pending_keymap: Vec::new(),
old_layout: None, old_layout: None,
repeat_action: None, repeat_action: None,
repeat_motion: None, repeat_motion: None,
editor: LineBuf::new(), editor: LineBuf::new(),
history: History::empty(), history: History::empty(),
needs_redraw: true, needs_redraw: true,
}; };
write_vars(|v| { write_vars(|v| {
v.set_var( v.set_var(
"SHED_VI_MODE", "SHED_VI_MODE",
VarKind::Str(new.mode.report_mode().to_string()), VarKind::Str(new.mode.report_mode().to_string()),
VarFlags::NONE, VarFlags::NONE,
) )
})?; })?;
new.prompt.refresh(); new.prompt.refresh();
new.writer.flush_write("\n")?; // ensure we start on a new line, in case the previous command didn't end with a newline new.writer.flush_write("\n")?; // ensure we start on a new line, in case the previous command didn't end with a newline
new.print_line(false)?; new.print_line(false)?;
Ok(new) Ok(new)
} }
pub fn with_initial(mut self, initial: &str) -> Self { pub fn with_initial(mut self, initial: &str) -> Self {
self.editor = LineBuf::new().with_initial(initial, 0); self.editor = LineBuf::new().with_initial(initial, 0);
@@ -335,7 +336,7 @@ impl ShedVi {
/// Feed raw bytes from stdin into the reader's buffer /// Feed raw bytes from stdin into the reader's buffer
pub fn feed_bytes(&mut self, bytes: &[u8]) { pub fn feed_bytes(&mut self, bytes: &[u8]) {
self.reader.feed_bytes(bytes); self.reader.feed_bytes(bytes);
} }
/// Mark that the display needs to be redrawn (e.g., after SIGWINCH) /// Mark that the display needs to be redrawn (e.g., after SIGWINCH)
@@ -354,10 +355,10 @@ impl ShedVi {
self.completer.reset_stay_active(); self.completer.reset_stay_active();
self.needs_redraw = true; self.needs_redraw = true;
Ok(()) Ok(())
} else if self.history.fuzzy_finder.is_active() { } else if self.history.fuzzy_finder.is_active() {
self.history.fuzzy_finder.reset_stay_active(); self.history.fuzzy_finder.reset_stay_active();
self.needs_redraw = true; self.needs_redraw = true;
Ok(()) Ok(())
} else { } else {
self.reset(full_redraw) self.reset(full_redraw)
} }
@@ -443,7 +444,11 @@ impl ShedVi {
// Process all available keys // Process all available keys
while let Some(key) = self.reader.read_key()? { while let Some(key) = self.reader.read_key()? {
log::debug!("Read key: {key:?} in mode {:?}, self.reader.verbatim = {}", self.mode.report_mode(), self.reader.verbatim); log::debug!(
"Read key: {key:?} in mode {:?}, self.reader.verbatim = {}",
self.mode.report_mode(),
self.reader.verbatim
);
// If completer or history search are active, delegate input to it // If completer or history search are active, delegate input to it
if self.history.fuzzy_finder.is_active() { if self.history.fuzzy_finder.is_active() {
self.print_line(false)?; self.print_line(false)?;
@@ -688,13 +693,14 @@ impl ShedVi {
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get())); .update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
let hint = self.history.get_hint(); let hint = self.history.get_hint();
self.editor.set_hint(hint); self.editor.set_hint(hint);
write_vars(|v| { write_vars(|v| {
v.set_var( v.set_var(
"SHED_VI_MODE", "SHED_VI_MODE",
VarKind::Str(self.mode.report_mode().to_string()), VarKind::Str(self.mode.report_mode().to_string()),
VarFlags::NONE, VarFlags::NONE,
) )
}).ok(); })
.ok();
// If we are here, we hit a case where pressing tab returned a single candidate // If we are here, we hit a case where pressing tab returned a single candidate
// So we can just go ahead and reset the completer after this // So we can just go ahead and reset the completer after this
@@ -702,15 +708,21 @@ impl ShedVi {
} }
Ok(None) => { Ok(None) => {
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnCompletionStart)); let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnCompletionStart));
let candidates = self.completer.all_candidates(); let candidates = self.completer.all_candidates();
let num_candidates = candidates.len(); let num_candidates = candidates.len();
with_vars([ with_vars(
("_NUM_MATCHES".into(), Into::<Var>::into(num_candidates)), [
("_MATCHES".into(), Into::<Var>::into(candidates)), ("_NUM_MATCHES".into(), Into::<Var>::into(num_candidates)),
("_SEARCH_STR".into(), Into::<Var>::into(self.completer.token())), ("_MATCHES".into(), Into::<Var>::into(candidates)),
], || { (
post_cmds.exec(); "_SEARCH_STR".into(),
}); Into::<Var>::into(self.completer.token()),
),
],
|| {
post_cmds.exec();
},
);
if self.completer.is_active() { if self.completer.is_active() {
write_vars(|v| { write_vars(|v| {
@@ -725,22 +737,21 @@ impl ShedVi {
self.needs_redraw = true; self.needs_redraw = true;
self.editor.set_hint(None); self.editor.set_hint(None);
} else { } else {
self.writer.send_bell().ok(); self.writer.send_bell().ok();
} }
} }
} }
self.needs_redraw = true; self.needs_redraw = true;
return Ok(None); return Ok(None);
} else if let KeyEvent(KeyCode::Char('R'), ModKeys::CTRL) = key } else if let KeyEvent(KeyCode::Char('R'), ModKeys::CTRL) = key
&& self.mode.report_mode() == ModeReport::Insert { && self.mode.report_mode() == ModeReport::Insert
{
let initial = self.editor.as_str(); let initial = self.editor.as_str();
match self.history.start_search(initial) { match self.history.start_search(initial) {
Some(entry) => { Some(entry) => {
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistorySelect)); let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistorySelect));
with_vars([ with_vars([("_HIST_ENTRY".into(), entry.clone())], || {
("_HIST_ENTRY".into(), entry.clone()),
], || {
post_cmds.exec_with(&entry); post_cmds.exec_with(&entry);
}); });
@@ -753,25 +764,30 @@ impl ShedVi {
} }
None => { None => {
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistoryOpen)); let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistoryOpen));
let entries = self.history.fuzzy_finder.candidates(); let entries = self.history.fuzzy_finder.candidates();
let matches = self.history.fuzzy_finder let matches = self
.filtered() .history
.iter() .fuzzy_finder
.cloned() .filtered()
.map(|sc| sc.content) .iter()
.collect::<Vec<_>>(); .cloned()
.map(|sc| sc.content)
.collect::<Vec<_>>();
let num_entries = entries.len(); let num_entries = entries.len();
let num_matches = matches.len(); let num_matches = matches.len();
with_vars([ with_vars(
("_ENTRIES".into(),Into::<Var>::into(entries)), [
("_NUM_ENTRIES".into(),Into::<Var>::into(num_entries)), ("_ENTRIES".into(), Into::<Var>::into(entries)),
("_MATCHES".into(),Into::<Var>::into(matches)), ("_NUM_ENTRIES".into(), Into::<Var>::into(num_entries)),
("_NUM_MATCHES".into(),Into::<Var>::into(num_matches)), ("_MATCHES".into(), Into::<Var>::into(matches)),
("_SEARCH_STR".into(), Into::<Var>::into(initial)), ("_NUM_MATCHES".into(), Into::<Var>::into(num_matches)),
], || { ("_SEARCH_STR".into(), Into::<Var>::into(initial)),
post_cmds.exec(); ],
}); || {
post_cmds.exec();
},
);
if self.history.fuzzy_finder.is_active() { if self.history.fuzzy_finder.is_active() {
write_vars(|v| { write_vars(|v| {
@@ -786,8 +802,8 @@ impl ShedVi {
self.needs_redraw = true; self.needs_redraw = true;
self.editor.set_hint(None); self.editor.set_hint(None);
} else { } else {
self.writer.send_bell().ok(); self.writer.send_bell().ok();
} }
} }
} }
} }
@@ -1055,7 +1071,7 @@ impl ShedVi {
let pending_seq = self.mode.pending_seq().unwrap_or_default(); let pending_seq = self.mode.pending_seq().unwrap_or_default();
write!(buf, "\n: {pending_seq}").unwrap(); write!(buf, "\n: {pending_seq}").unwrap();
new_layout.end.row += 1; new_layout.end.row += 1;
new_layout.cursor.row += 1; new_layout.cursor.row += 1;
} }
write!(buf, "{}", &self.mode.cursor_style()).unwrap(); write!(buf, "{}", &self.mode.cursor_style()).unwrap();
@@ -1129,7 +1145,11 @@ impl ShedVi {
match cmd.verb().unwrap().1 { match cmd.verb().unwrap().1 {
Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => { Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => {
is_insert_mode = true; is_insert_mode = true;
Box::new(ViInsert::new().with_count(count as u16).record_cmd(cmd.clone())) Box::new(
ViInsert::new()
.with_count(count as u16)
.record_cmd(cmd.clone()),
)
} }
Verb::ExMode => Box::new(ViEx::new()), Verb::ExMode => Box::new(ViEx::new()),
@@ -1217,17 +1237,17 @@ impl ShedVi {
Ok(()) Ok(())
} }
pub fn clone_mode(&self) -> Box<dyn ViMode> { pub fn clone_mode(&self) -> Box<dyn ViMode> {
match self.mode.report_mode() { match self.mode.report_mode() {
ModeReport::Normal => Box::new(ViNormal::new()), ModeReport::Normal => Box::new(ViNormal::new()),
ModeReport::Insert => Box::new(ViInsert::new()), ModeReport::Insert => Box::new(ViInsert::new()),
ModeReport::Visual => Box::new(ViVisual::new()), ModeReport::Visual => Box::new(ViVisual::new()),
ModeReport::Ex => Box::new(ViEx::new()), ModeReport::Ex => Box::new(ViEx::new()),
ModeReport::Replace => Box::new(ViReplace::new()), ModeReport::Replace => Box::new(ViReplace::new()),
ModeReport::Verbatim => Box::new(ViVerbatim::new()), ModeReport::Verbatim => Box::new(ViVerbatim::new()),
ModeReport::Unknown => unreachable!(), ModeReport::Unknown => unreachable!(),
} }
} }
pub fn exec_cmd(&mut self, mut cmd: ViCmd, from_replay: bool) -> ShResult<()> { pub fn exec_cmd(&mut self, mut cmd: ViCmd, from_replay: bool) -> ShResult<()> {
if cmd.is_mode_transition() { if cmd.is_mode_transition() {
@@ -1244,35 +1264,39 @@ impl ShedVi {
repeat = count as u16; repeat = count as u16;
} }
let old_mode = self.mode.report_mode(); let old_mode = self.mode.report_mode();
for _ in 0..repeat { for _ in 0..repeat {
let cmds = cmds.clone(); let cmds = cmds.clone();
for (i, cmd) in cmds.iter().enumerate() { for (i, cmd) in cmds.iter().enumerate() {
log::debug!("Replaying command {cmd:?} in mode {:?}, replay {i}/{repeat}", self.mode.report_mode()); log::debug!(
"Replaying command {cmd:?} in mode {:?}, replay {i}/{repeat}",
self.mode.report_mode()
);
self.exec_cmd(cmd.clone(), true)?; self.exec_cmd(cmd.clone(), true)?;
// After the first command, start merging so all subsequent // After the first command, start merging so all subsequent
// edits fold into one undo entry (e.g. cw + inserted chars) // edits fold into one undo entry (e.g. cw + inserted chars)
if i == 0 if i == 0
&& let Some(edit) = self.editor.undo_stack.last_mut() { && let Some(edit) = self.editor.undo_stack.last_mut()
edit.start_merge(); {
} edit.start_merge();
}
} }
// Stop merging at the end of the replay // Stop merging at the end of the replay
if let Some(edit) = self.editor.undo_stack.last_mut() { if let Some(edit) = self.editor.undo_stack.last_mut() {
edit.stop_merge(); edit.stop_merge();
} }
let old_mode_clone = match old_mode { let old_mode_clone = match old_mode {
ModeReport::Normal => Box::new(ViNormal::new()) as Box<dyn ViMode>, ModeReport::Normal => Box::new(ViNormal::new()) as Box<dyn ViMode>,
ModeReport::Insert => Box::new(ViInsert::new()) as Box<dyn ViMode>, ModeReport::Insert => Box::new(ViInsert::new()) as Box<dyn ViMode>,
ModeReport::Visual => Box::new(ViVisual::new()) as Box<dyn ViMode>, ModeReport::Visual => Box::new(ViVisual::new()) as Box<dyn ViMode>,
ModeReport::Ex => Box::new(ViEx::new()) as Box<dyn ViMode>, ModeReport::Ex => Box::new(ViEx::new()) as Box<dyn ViMode>,
ModeReport::Replace => Box::new(ViReplace::new()) as Box<dyn ViMode>, ModeReport::Replace => Box::new(ViReplace::new()) as Box<dyn ViMode>,
ModeReport::Verbatim => Box::new(ViVerbatim::new()) as Box<dyn ViMode>, ModeReport::Verbatim => Box::new(ViVerbatim::new()) as Box<dyn ViMode>,
ModeReport::Unknown => unreachable!(), ModeReport::Unknown => unreachable!(),
}; };
self.mode = old_mode_clone; self.mode = old_mode_clone;
} }
} }
CmdReplay::Single(mut cmd) => { CmdReplay::Single(mut cmd) => {
@@ -1354,7 +1378,11 @@ impl ShedVi {
self.editor.exec_cmd(cmd.clone())?; self.editor.exec_cmd(cmd.clone())?;
if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit() || v.1 == Verb::Yank) { if self.mode.report_mode() == ModeReport::Visual
&& cmd
.verb()
.is_some_and(|v| v.1.is_edit() || v.1 == Verb::Yank)
{
self.editor.stop_selecting(); self.editor.stop_selecting();
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new()); let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
self.swap_mode(&mut mode); self.swap_mode(&mut mode);
@@ -1503,7 +1531,7 @@ pub fn get_insertions(input: &str) -> Vec<(usize, Marker)> {
pub fn marker_for(class: &TkRule) -> Option<Marker> { pub fn marker_for(class: &TkRule) -> Option<Marker> {
match class { match class {
TkRule::Pipe TkRule::Pipe
| TkRule::Bang | TkRule::Bang
| TkRule::ErrPipe | TkRule::ErrPipe
| TkRule::And | TkRule::And
| TkRule::Or | TkRule::Or

View File

@@ -7,17 +7,17 @@ pub static SAVED_REGISTERS: Mutex<Option<Registers>> = Mutex::new(None);
#[cfg(test)] #[cfg(test)]
pub fn save_registers() { pub fn save_registers() {
let mut saved = SAVED_REGISTERS.lock().unwrap(); let mut saved = SAVED_REGISTERS.lock().unwrap();
*saved = Some(REGISTERS.lock().unwrap().clone()); *saved = Some(REGISTERS.lock().unwrap().clone());
} }
#[cfg(test)] #[cfg(test)]
pub fn restore_registers() { pub fn restore_registers() {
let mut saved = SAVED_REGISTERS.lock().unwrap(); let mut saved = SAVED_REGISTERS.lock().unwrap();
if let Some(ref registers) = *saved { if let Some(ref registers) = *saved {
*REGISTERS.lock().unwrap() = registers.clone(); *REGISTERS.lock().unwrap() = registers.clone();
} }
*saved = None; *saved = None;
} }
pub fn read_register(ch: Option<char>) -> Option<RegisterContent> { pub fn read_register(ch: Option<char>) -> Option<RegisterContent> {

View File

@@ -444,8 +444,8 @@ impl Perform for KeyCollector {
21 => KeyCode::F(10), 21 => KeyCode::F(10),
23 => KeyCode::F(11), 23 => KeyCode::F(11),
24 => KeyCode::F(12), 24 => KeyCode::F(12),
200 => KeyCode::BracketedPasteStart, 200 => KeyCode::BracketedPasteStart,
201 => KeyCode::BracketedPasteEnd, 201 => KeyCode::BracketedPasteEnd,
_ => return, _ => return,
}; };
KeyEvent(key, mods) KeyEvent(key, mods)
@@ -498,9 +498,9 @@ impl Perform for KeyCollector {
pub struct PollReader { pub struct PollReader {
parser: Parser, parser: Parser,
collector: KeyCollector, collector: KeyCollector,
byte_buf: VecDeque<u8>, byte_buf: VecDeque<u8>,
pub verbatim_single: bool, pub verbatim_single: bool,
pub verbatim: bool, pub verbatim: bool,
} }
impl PollReader { impl PollReader {
@@ -508,42 +508,45 @@ impl PollReader {
Self { Self {
parser: Parser::new(), parser: Parser::new(),
collector: KeyCollector::new(), collector: KeyCollector::new(),
byte_buf: VecDeque::new(), byte_buf: VecDeque::new(),
verbatim_single: false, verbatim_single: false,
verbatim: false, verbatim: false,
} }
} }
pub fn handle_bracket_paste(&mut self) -> Option<KeyEvent> { pub fn handle_bracket_paste(&mut self) -> Option<KeyEvent> {
let end_marker = b"\x1b[201~"; let end_marker = b"\x1b[201~";
let mut raw = vec![]; let mut raw = vec![];
while let Some(byte) = self.byte_buf.pop_front() { while let Some(byte) = self.byte_buf.pop_front() {
raw.push(byte); raw.push(byte);
if raw.ends_with(end_marker) { if raw.ends_with(end_marker) {
// Strip the end marker from the raw sequence // Strip the end marker from the raw sequence
raw.truncate(raw.len() - end_marker.len()); raw.truncate(raw.len() - end_marker.len());
let paste = String::from_utf8_lossy(&raw).to_string(); let paste = String::from_utf8_lossy(&raw).to_string();
self.verbatim = false; self.verbatim = false;
return Some(KeyEvent(KeyCode::Verbatim(paste.into()), ModKeys::empty())); return Some(KeyEvent(KeyCode::Verbatim(paste.into()), ModKeys::empty()));
} }
} }
self.verbatim = true; self.verbatim = true;
self.byte_buf.extend(raw); self.byte_buf.extend(raw);
None None
} }
pub fn read_one_verbatim(&mut self) -> Option<KeyEvent> { pub fn read_one_verbatim(&mut self) -> Option<KeyEvent> {
if self.byte_buf.is_empty() { if self.byte_buf.is_empty() {
return None; return None;
} }
let bytes: Vec<u8> = self.byte_buf.drain(..).collect(); let bytes: Vec<u8> = self.byte_buf.drain(..).collect();
let verbatim_str = String::from_utf8_lossy(&bytes).to_string(); let verbatim_str = String::from_utf8_lossy(&bytes).to_string();
Some(KeyEvent(KeyCode::Verbatim(verbatim_str.into()), ModKeys::empty())) Some(KeyEvent(
} KeyCode::Verbatim(verbatim_str.into()),
ModKeys::empty(),
))
}
pub fn feed_bytes(&mut self, bytes: &[u8]) { pub fn feed_bytes(&mut self, bytes: &[u8]) {
self.byte_buf.extend(bytes); self.byte_buf.extend(bytes);
} }
} }
@@ -555,44 +558,42 @@ impl Default for PollReader {
impl KeyReader for PollReader { impl KeyReader for PollReader {
fn read_key(&mut self) -> Result<Option<KeyEvent>, ShErr> { fn read_key(&mut self) -> Result<Option<KeyEvent>, ShErr> {
if self.verbatim_single { if self.verbatim_single {
if let Some(key) = self.read_one_verbatim() { if let Some(key) = self.read_one_verbatim() {
self.verbatim_single = false; self.verbatim_single = false;
return Ok(Some(key)); return Ok(Some(key));
} }
return Ok(None); return Ok(None);
} }
if self.verbatim { if self.verbatim {
if let Some(paste) = self.handle_bracket_paste() { if let Some(paste) = self.handle_bracket_paste() {
return Ok(Some(paste)); return Ok(Some(paste));
} }
// If we're in verbatim mode but haven't seen the end marker yet, don't attempt to parse keys // If we're in verbatim mode but haven't seen the end marker yet, don't attempt to parse keys
return Ok(None); return Ok(None);
} else if self.byte_buf.front() == Some(&b'\x1b') { } else if self.byte_buf.front() == Some(&b'\x1b') {
// Escape: if it's the only byte, or the next byte isn't a valid // Escape: if it's the only byte, or the next byte isn't a valid
// escape sequence prefix ([ or O), emit a standalone Escape // escape sequence prefix ([ or O), emit a standalone Escape
if self.byte_buf.len() == 1 if self.byte_buf.len() == 1 || !matches!(self.byte_buf.get(1), Some(b'[') | Some(b'O')) {
|| !matches!(self.byte_buf.get(1), Some(b'[') | Some(b'O')) self.byte_buf.pop_front();
{ return Ok(Some(KeyEvent(KeyCode::Esc, ModKeys::empty())));
self.byte_buf.pop_front(); }
return Ok(Some(KeyEvent(KeyCode::Esc, ModKeys::empty()))); }
} while let Some(byte) = self.byte_buf.pop_front() {
} self.parser.advance(&mut self.collector, &[byte]);
while let Some(byte) = self.byte_buf.pop_front() { if let Some(key) = self.collector.pop() {
self.parser.advance(&mut self.collector, &[byte]); match key {
if let Some(key) = self.collector.pop() { KeyEvent(KeyCode::BracketedPasteStart, _) => {
match key { if let Some(paste) = self.handle_bracket_paste() {
KeyEvent(KeyCode::BracketedPasteStart, _) => { return Ok(Some(paste));
if let Some(paste) = self.handle_bracket_paste() { } else {
return Ok(Some(paste)); continue;
} else { }
continue; }
} _ => return Ok(Some(key)),
} }
_ => return Ok(Some(key)) }
} }
}
}
Ok(None) Ok(None)
} }
} }
@@ -844,7 +845,7 @@ impl Default for Layout {
} }
pub struct TermWriter { pub struct TermWriter {
last_bell: Option<Instant>, last_bell: Option<Instant>,
out: RawFd, out: RawFd,
pub t_cols: Col, // terminal width pub t_cols: Col, // terminal width
buffer: String, buffer: String,
@@ -854,7 +855,7 @@ impl TermWriter {
pub fn new(out: RawFd) -> Self { pub fn new(out: RawFd) -> Self {
let (t_cols, _) = get_win_size(out); let (t_cols, _) = get_win_size(out);
Self { Self {
last_bell: None, last_bell: None,
out, out,
t_cols, t_cols,
buffer: String::new(), buffer: String::new(),
@@ -1091,24 +1092,24 @@ impl LineWriter for TermWriter {
Ok(()) Ok(())
} }
fn send_bell(&mut self) -> ShResult<()> { fn send_bell(&mut self) -> ShResult<()> {
if read_shopts(|o| o.core.bell_enabled) { if read_shopts(|o| o.core.bell_enabled) {
// we use a cooldown because I don't like having my ears assaulted by 1 million bells // we use a cooldown because I don't like having my ears assaulted by 1 million bells
// whenever i finish clearing the line using backspace. // whenever i finish clearing the line using backspace.
let now = Instant::now(); let now = Instant::now();
// surprisingly, a fixed cooldown like '100' is actually more annoying than 1 million bells. // surprisingly, a fixed cooldown like '100' is actually more annoying than 1 million bells.
// I've found this range of 50-150 to be the best balance // I've found this range of 50-150 to be the best balance
let cooldown = rand::random_range(50..150); let cooldown = rand::random_range(50..150);
let should_send = match self.last_bell { let should_send = match self.last_bell {
None => true, None => true,
Some(time) => now.duration_since(time).as_millis() > cooldown, Some(time) => now.duration_since(time).as_millis() > cooldown,
}; };
if should_send { if should_send {
self.flush_write("\x07")?; self.flush_write("\x07")?;
self.last_bell = Some(now); self.last_bell = Some(now);
} }
} }
Ok(()) Ok(())
} }
} }

View File

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

View File

@@ -13,10 +13,10 @@ impl ViInsert {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
pub fn record_cmd(mut self, cmd: ViCmd) -> Self { pub fn record_cmd(mut self, cmd: ViCmd) -> Self {
self.cmds.push(cmd); self.cmds.push(cmd);
self self
} }
pub fn with_count(mut self, repeat_count: u16) -> Self { pub fn with_count(mut self, repeat_count: u16) -> Self {
self.repeat_count = repeat_count; self.repeat_count = repeat_count;
self self
@@ -65,10 +65,12 @@ impl ViMode for ViInsert {
raw_seq: String::new(), raw_seq: String::new(),
flags: Default::default(), flags: Default::default(),
}), }),
E(K::Verbatim(seq), _) => { E(K::Verbatim(seq), _) => {
self.pending_cmd.set_verb(VerbCmd(1, Verb::Insert(seq.to_string()))); self
self.register_and_return() .pending_cmd
} .set_verb(VerbCmd(1, Verb::Insert(seq.to_string())));
self.register_and_return()
}
E(K::Char('W'), M::CTRL) => { E(K::Char('W'), M::CTRL) => {
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete)); self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
self.pending_cmd.set_motion(MotionCmd( self.pending_cmd.set_motion(MotionCmd(

View File

@@ -213,7 +213,7 @@ impl ViVisual {
let ch = chars_clone.next()?; let ch = chars_clone.next()?;
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(1, Verb::ReplaceCharInplace(ch,1))), verb: Some(VerbCmd(1, Verb::ReplaceCharInplace(ch, 1))),
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
@@ -301,13 +301,13 @@ impl ViVisual {
}); });
} }
'y' => { 'y' => {
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(count, Verb::Yank)), verb: Some(VerbCmd(count, Verb::Yank)),
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}); });
} }
'd' => { 'd' => {
chars = chars_clone; chars = chars_clone;

View File

@@ -185,12 +185,12 @@ impl ShOptCore {
"shopt: expected an integer for max_hist value (-1 for unlimited)", "shopt: expected an integer for max_hist value (-1 for unlimited)",
)); ));
}; };
if val < -1 { if val < -1 {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: expected a non-negative integer or -1 for max_hist value", "shopt: expected a non-negative integer or -1 for max_hist value",
)); ));
} }
self.max_hist = val; self.max_hist = val;
} }
"interactive_comments" => { "interactive_comments" => {
@@ -516,7 +516,8 @@ impl ShOptPrompt {
Ok(Some(output)) Ok(Some(output))
} }
"screensaver_idle_time" => { "screensaver_idle_time" => {
let mut output = String::from("Idle time in seconds before running screensaver_cmd (0 = disabled)\n"); let mut output =
String::from("Idle time in seconds before running screensaver_cmd (0 = disabled)\n");
output.push_str(&format!("{}", self.screensaver_idle_time)); output.push_str(&format!("{}", self.screensaver_idle_time));
Ok(Some(output)) Ok(Some(output))
} }
@@ -544,7 +545,10 @@ impl Display for ShOptPrompt {
output.push(format!("leader = {}", self.leader)); output.push(format!("leader = {}", self.leader));
output.push(format!("line_numbers = {}", self.line_numbers)); output.push(format!("line_numbers = {}", self.line_numbers));
output.push(format!("screensaver_cmd = {}", self.screensaver_cmd)); output.push(format!("screensaver_cmd = {}", self.screensaver_cmd));
output.push(format!("screensaver_idle_time = {}", self.screensaver_idle_time)); output.push(format!(
"screensaver_idle_time = {}",
self.screensaver_idle_time
));
let final_output = output.join("\n"); let final_output = output.join("\n");
@@ -575,23 +579,29 @@ mod tests {
#[test] #[test]
fn all_core_fields_covered() { fn all_core_fields_covered() {
let ShOptCore { let ShOptCore {
dotglob, autocd, hist_ignore_dupes, max_hist, dotglob,
interactive_comments, auto_hist, bell_enabled, max_recurse_depth, autocd,
xpg_echo, hist_ignore_dupes,
} = ShOptCore::default(); max_hist,
// If a field is added to the struct, this destructure fails to compile. interactive_comments,
let _ = ( auto_hist,
dotglob, bell_enabled,
autocd, max_recurse_depth,
hist_ignore_dupes, xpg_echo,
max_hist, } = ShOptCore::default();
interactive_comments, // If a field is added to the struct, this destructure fails to compile.
auto_hist, let _ = (
bell_enabled, dotglob,
max_recurse_depth, autocd,
xpg_echo, hist_ignore_dupes,
); max_hist,
interactive_comments,
auto_hist,
bell_enabled,
max_recurse_depth,
xpg_echo,
);
} }
#[test] #[test]
@@ -617,7 +627,7 @@ mod tests {
opts.set("core.max_hist", "-1").unwrap(); opts.set("core.max_hist", "-1").unwrap();
assert_eq!(opts.core.max_hist, -1); assert_eq!(opts.core.max_hist, -1);
assert!(opts.set("core.max_hist", "-500").is_err()); assert!(opts.set("core.max_hist", "-500").is_err());
} }
#[test] #[test]

View File

@@ -165,10 +165,10 @@ pub fn reset_signals(is_fg: bool) {
if sig == Signal::SIGKILL || sig == Signal::SIGSTOP { if sig == Signal::SIGKILL || sig == Signal::SIGSTOP {
continue; continue;
} }
if is_fg && (sig == Signal::SIGTTIN || sig == Signal::SIGTTOU) { if is_fg && (sig == Signal::SIGTTIN || sig == Signal::SIGTTOU) {
log::debug!("Not resetting SIGTTIN/SIGTTOU in foreground child"); log::debug!("Not resetting SIGTTIN/SIGTTOU in foreground child");
continue; continue;
} }
let _ = sigaction(sig, &default); let _ = sigaction(sig, &default);
} }
} }

View File

@@ -1,5 +1,11 @@
use std::{ use std::{
cell::RefCell, collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, fmt::Display, ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign}, os::unix::fs::PermissionsExt, str::FromStr, time::Duration cell::RefCell,
collections::{HashMap, HashSet, VecDeque, hash_map::Entry},
fmt::Display,
ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign},
os::unix::fs::PermissionsExt,
str::FromStr,
time::Duration,
}; };
use nix::unistd::{User, gethostname, getppid}; use nix::unistd::{User, gethostname, getppid};
@@ -36,7 +42,7 @@ thread_local! {
pub static SHED: Shed = Shed::new(); pub static SHED: Shed = Shed::new();
} }
#[derive(Clone,Debug)] #[derive(Clone, Debug)]
pub struct Shed { pub struct Shed {
pub jobs: RefCell<JobTab>, pub jobs: RefCell<JobTab>,
pub var_scopes: RefCell<ScopeStack>, pub var_scopes: RefCell<ScopeStack>,
@@ -44,8 +50,8 @@ pub struct Shed {
pub logic: RefCell<LogTab>, pub logic: RefCell<LogTab>,
pub shopts: RefCell<ShOpts>, pub shopts: RefCell<ShOpts>,
#[cfg(test)] #[cfg(test)]
saved: RefCell<Option<Box<Self>>>, saved: RefCell<Option<Box<Self>>>,
} }
impl Shed { impl Shed {
@@ -57,8 +63,8 @@ impl Shed {
logic: RefCell::new(LogTab::new()), logic: RefCell::new(LogTab::new()),
shopts: RefCell::new(ShOpts::default()), shopts: RefCell::new(ShOpts::default()),
#[cfg(test)] #[cfg(test)]
saved: RefCell::new(None), saved: RefCell::new(None),
} }
} }
} }
@@ -71,27 +77,27 @@ impl Default for Shed {
#[cfg(test)] #[cfg(test)]
impl Shed { impl Shed {
pub fn save(&self) { pub fn save(&self) {
let saved = Self { let saved = Self {
jobs: RefCell::new(self.jobs.borrow().clone()), jobs: RefCell::new(self.jobs.borrow().clone()),
var_scopes: RefCell::new(self.var_scopes.borrow().clone()), var_scopes: RefCell::new(self.var_scopes.borrow().clone()),
meta: RefCell::new(self.meta.borrow().clone()), meta: RefCell::new(self.meta.borrow().clone()),
logic: RefCell::new(self.logic.borrow().clone()), logic: RefCell::new(self.logic.borrow().clone()),
shopts: RefCell::new(self.shopts.borrow().clone()), shopts: RefCell::new(self.shopts.borrow().clone()),
saved: RefCell::new(None), saved: RefCell::new(None),
}; };
*self.saved.borrow_mut() = Some(Box::new(saved)); *self.saved.borrow_mut() = Some(Box::new(saved));
} }
pub fn restore(&self) { pub fn restore(&self) {
if let Some(saved) = self.saved.take() { if let Some(saved) = self.saved.take() {
*self.jobs.borrow_mut() = saved.jobs.into_inner(); *self.jobs.borrow_mut() = saved.jobs.into_inner();
*self.var_scopes.borrow_mut() = saved.var_scopes.into_inner(); *self.var_scopes.borrow_mut() = saved.var_scopes.into_inner();
*self.meta.borrow_mut() = saved.meta.into_inner(); *self.meta.borrow_mut() = saved.meta.into_inner();
*self.logic.borrow_mut() = saved.logic.into_inner(); *self.logic.borrow_mut() = saved.logic.into_inner();
*self.shopts.borrow_mut() = saved.shopts.into_inner(); *self.shopts.borrow_mut() = saved.shopts.into_inner();
} }
} }
} }
#[derive(Hash, Eq, PartialEq, Debug, Clone, Copy)] #[derive(Hash, Eq, PartialEq, Debug, Clone, Copy)]
@@ -315,34 +321,34 @@ impl ScopeStack {
}; };
scope.set_var(var_name, val, flags) scope.set_var(var_name, val, flags)
} }
pub fn get_magic_var(&self, var_name: &str) -> Option<String> { pub fn get_magic_var(&self, var_name: &str) -> Option<String> {
match var_name { match var_name {
"SECONDS" => { "SECONDS" => {
let shell_time = read_meta(|m| m.shell_time()); let shell_time = read_meta(|m| m.shell_time());
let secs = Instant::now().duration_since(shell_time).as_secs(); let secs = Instant::now().duration_since(shell_time).as_secs();
Some(secs.to_string()) Some(secs.to_string())
} }
"EPOCHREALTIME" => { "EPOCHREALTIME" => {
let epoch = std::time::SystemTime::now() let epoch = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap_or(Duration::from_secs(0)) .unwrap_or(Duration::from_secs(0))
.as_secs_f64(); .as_secs_f64();
Some(epoch.to_string()) Some(epoch.to_string())
} }
"EPOCHSECONDS" => { "EPOCHSECONDS" => {
let epoch = std::time::SystemTime::now() let epoch = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap_or(Duration::from_secs(0)) .unwrap_or(Duration::from_secs(0))
.as_secs(); .as_secs();
Some(epoch.to_string()) Some(epoch.to_string())
} }
"RANDOM" => { "RANDOM" => {
let random = rand::random_range(0..32768); let random = rand::random_range(0..32768);
Some(random.to_string()) Some(random.to_string())
} }
_ => None _ => None,
} }
} }
pub fn get_arr_elems(&self, var_name: &str) -> ShResult<Vec<String>> { pub fn get_arr_elems(&self, var_name: &str) -> ShResult<Vec<String>> {
for scope in self.scopes.iter().rev() { for scope in self.scopes.iter().rev() {
if scope.var_exists(var_name) if scope.var_exists(var_name)
@@ -468,9 +474,9 @@ impl ScopeStack {
pub fn try_get_var(&self, var_name: &str) -> Option<String> { pub fn try_get_var(&self, var_name: &str) -> Option<String> {
// This version of get_var() is mainly used internally // This version of get_var() is mainly used internally
// so that we have access to Option methods // so that we have access to Option methods
if let Some(magic) = self.get_magic_var(var_name) { if let Some(magic) = self.get_magic_var(var_name) {
return Some(magic); return Some(magic);
} else if let Ok(param) = var_name.parse::<ShellParam>() { } else if let Ok(param) = var_name.parse::<ShellParam>() {
let val = self.get_param(param); let val = self.get_param(param);
if !val.is_empty() { if !val.is_empty() {
return Some(val); return Some(val);
@@ -493,9 +499,9 @@ impl ScopeStack {
var var
} }
pub fn get_var(&self, var_name: &str) -> String { pub fn get_var(&self, var_name: &str) -> String {
if let Some(magic) = self.get_magic_var(var_name) { if let Some(magic) = self.get_magic_var(var_name) {
return magic; return magic;
} }
if let Ok(param) = var_name.parse::<ShellParam>() { if let Ok(param) = var_name.parse::<ShellParam>() {
return self.get_param(param); return self.get_param(param);
} }
@@ -528,7 +534,10 @@ impl ScopeStack {
return val.clone(); return val.clone();
} }
// Positional params are scope-local; only check the current scope // Positional params are scope-local; only check the current scope
if matches!(param, ShellParam::Pos(_) | ShellParam::AllArgs | ShellParam::AllArgsStr | ShellParam::ArgCount) { if matches!(
param,
ShellParam::Pos(_) | ShellParam::AllArgs | ShellParam::AllArgsStr | ShellParam::ArgCount
) {
if let Some(scope) = self.scopes.last() { if let Some(scope) = self.scopes.last() {
return scope.get_param(param); return scope.get_param(param);
} }
@@ -987,17 +996,17 @@ impl Display for Var {
} }
impl From<Vec<String>> for Var { impl From<Vec<String>> for Var {
fn from(value: Vec<String>) -> Self { fn from(value: Vec<String>) -> Self {
Self::new(VarKind::Arr(value.into()), VarFlags::NONE) Self::new(VarKind::Arr(value.into()), VarFlags::NONE)
} }
} }
impl From<&[String]> for Var { impl From<&[String]> for Var {
fn from(value: &[String]) -> Self { fn from(value: &[String]) -> Self {
let mut new = VecDeque::new(); let mut new = VecDeque::new();
new.extend(value.iter().cloned()); new.extend(value.iter().cloned());
Self::new(VarKind::Arr(new), VarFlags::NONE) Self::new(VarKind::Arr(new), VarFlags::NONE)
} }
} }
macro_rules! impl_var_from { macro_rules! impl_var_from {
@@ -1011,19 +1020,7 @@ macro_rules! impl_var_from {
} }
impl_var_from!( impl_var_from!(
i8, i8, i16, i32, i64, isize, u8, u16, u32, u64, usize, String, &str, bool
i16,
i32,
i64,
isize,
u8,
u16,
u32,
u64,
usize,
String,
&str,
bool
); );
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
@@ -1064,11 +1061,11 @@ impl VarTab {
params.insert(ShellParam::LastJob, "".into()); // PID of the last background job (if any) params.insert(ShellParam::LastJob, "".into()); // PID of the last background job (if any)
params params
} }
fn init_sh_vars() -> HashMap<String,Var> { fn init_sh_vars() -> HashMap<String, Var> {
let mut vars = HashMap::new(); let mut vars = HashMap::new();
vars.insert("COMP_WORDBREAKS".into(), " \t\n\"'@><=;|&(".into()); vars.insert("COMP_WORDBREAKS".into(), " \t\n\"'@><=;|&(".into());
vars vars
} }
fn init_env() { fn init_env() {
let pathbuf_to_string = let pathbuf_to_string =
|pb: Result<PathBuf, std::io::Error>| pb.unwrap_or_default().to_string_lossy().to_string(); |pb: Result<PathBuf, std::io::Error>| pb.unwrap_or_default().to_string_lossy().to_string();
@@ -1345,8 +1342,8 @@ impl VarTab {
/// A table of metadata for the shell /// A table of metadata for the shell
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct MetaTab { pub struct MetaTab {
// Time when the shell was started, used for calculating shell uptime // Time when the shell was started, used for calculating shell uptime
shell_time: Instant, shell_time: Instant,
// command running duration // command running duration
runtime_start: Option<Instant>, runtime_start: Option<Instant>,
@@ -1373,22 +1370,22 @@ pub struct MetaTab {
} }
impl Default for MetaTab { impl Default for MetaTab {
fn default() -> Self { fn default() -> Self {
Self { Self {
shell_time: Instant::now(), shell_time: Instant::now(),
runtime_start: None, runtime_start: None,
runtime_stop: None, runtime_stop: None,
system_msg: vec![], system_msg: vec![],
dir_stack: VecDeque::new(), dir_stack: VecDeque::new(),
getopts_offset: 0, getopts_offset: 0,
old_path: None, old_path: None,
old_pwd: None, old_pwd: None,
path_cache: HashSet::new(), path_cache: HashSet::new(),
cwd_cache: HashSet::new(), cwd_cache: HashSet::new(),
comp_specs: HashMap::new(), comp_specs: HashMap::new(),
pending_widget_keys: vec![], pending_widget_keys: vec![],
} }
} }
} }
impl MetaTab { impl MetaTab {
@@ -1398,9 +1395,9 @@ impl MetaTab {
..Default::default() ..Default::default()
} }
} }
pub fn shell_time(&self) -> Instant { pub fn shell_time(&self) -> Instant {
self.shell_time self.shell_time
} }
pub fn set_pending_widget_keys(&mut self, keys: &str) { pub fn set_pending_widget_keys(&mut self, keys: &str) {
let exp = expand_keymap(keys); let exp = expand_keymap(keys);
self.pending_widget_keys = exp; self.pending_widget_keys = exp;

View File

@@ -1,219 +1,240 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
env, env,
os::fd::{AsRawFd, BorrowedFd, OwnedFd}, os::fd::{AsRawFd, BorrowedFd, OwnedFd},
path::PathBuf, path::PathBuf,
sync::{self, Arc, MutexGuard}, sync::{self, Arc, MutexGuard},
}; };
use nix::{ use nix::{
fcntl::{FcntlArg, OFlag, fcntl}, fcntl::{FcntlArg, OFlag, fcntl},
pty::openpty, pty::openpty,
sys::termios::{OutputFlags, SetArg, tcgetattr, tcsetattr}, sys::termios::{OutputFlags, SetArg, tcgetattr, tcsetattr},
unistd::read, unistd::read,
}; };
use crate::{ use crate::{
expand::expand_aliases, libsh::error::ShResult, parse::{ParsedSrc, Redir, RedirType, execute::exec_input, lex::LexFlags}, procio::{IoFrame, IoMode, RedirGuard}, readline::register::{restore_registers, save_registers}, state::{MetaTab, SHED, read_logic} expand::expand_aliases,
libsh::error::ShResult,
parse::{ParsedSrc, Redir, RedirType, execute::exec_input, lex::LexFlags},
procio::{IoFrame, IoMode, RedirGuard},
readline::register::{restore_registers, save_registers},
state::{MetaTab, SHED, read_logic},
}; };
static TEST_MUTEX: sync::Mutex<()> = sync::Mutex::new(()); static TEST_MUTEX: sync::Mutex<()> = sync::Mutex::new(());
pub fn has_cmds(cmds: &[&str]) -> bool { pub fn has_cmds(cmds: &[&str]) -> bool {
let path_cmds = MetaTab::get_cmds_in_path(); let path_cmds = MetaTab::get_cmds_in_path();
path_cmds.iter().all(|c| cmds.iter().any(|&cmd| c == cmd)) path_cmds.iter().all(|c| cmds.iter().any(|&cmd| c == cmd))
} }
pub fn has_cmd(cmd: &str) -> bool { pub fn has_cmd(cmd: &str) -> bool {
MetaTab::get_cmds_in_path().into_iter().any(|c| c == cmd) MetaTab::get_cmds_in_path().into_iter().any(|c| c == cmd)
} }
pub fn test_input(input: impl Into<String>) -> ShResult<()> { pub fn test_input(input: impl Into<String>) -> ShResult<()> {
exec_input(input.into(), None, false, None) exec_input(input.into(), None, false, None)
} }
pub struct TestGuard { pub struct TestGuard {
_lock: MutexGuard<'static, ()>, _lock: MutexGuard<'static, ()>,
_redir_guard: RedirGuard, _redir_guard: RedirGuard,
old_cwd: PathBuf, old_cwd: PathBuf,
saved_env: HashMap<String, String>, saved_env: HashMap<String, String>,
pty_master: OwnedFd, pty_master: OwnedFd,
pty_slave: OwnedFd, pty_slave: OwnedFd,
cleanups: Vec<Box<dyn FnOnce()>> cleanups: Vec<Box<dyn FnOnce()>>,
} }
impl TestGuard { impl TestGuard {
pub fn new() -> Self { pub fn new() -> Self {
let _lock = TEST_MUTEX.lock().unwrap(); let _lock = TEST_MUTEX.lock().unwrap();
let pty = openpty(None, None).unwrap(); let pty = openpty(None, None).unwrap();
let (pty_master,pty_slave) = (pty.master, pty.slave); let (pty_master, pty_slave) = (pty.master, pty.slave);
let mut attrs = tcgetattr(&pty_slave).unwrap(); let mut attrs = tcgetattr(&pty_slave).unwrap();
attrs.output_flags &= !OutputFlags::ONLCR; attrs.output_flags &= !OutputFlags::ONLCR;
tcsetattr(&pty_slave, SetArg::TCSANOW, &attrs).unwrap(); tcsetattr(&pty_slave, SetArg::TCSANOW, &attrs).unwrap();
let mut frame = IoFrame::new(); let mut frame = IoFrame::new();
frame.push( frame.push(Redir::new(
Redir::new( IoMode::Fd {
IoMode::Fd { tgt_fd: 0,
tgt_fd: 0, src_fd: pty_slave.as_raw_fd(),
src_fd: pty_slave.as_raw_fd(), },
}, RedirType::Input,
RedirType::Input, ));
), frame.push(Redir::new(
); IoMode::Fd {
frame.push( tgt_fd: 1,
Redir::new( src_fd: pty_slave.as_raw_fd(),
IoMode::Fd { },
tgt_fd: 1, RedirType::Output,
src_fd: pty_slave.as_raw_fd(), ));
}, frame.push(Redir::new(
RedirType::Output, IoMode::Fd {
), tgt_fd: 2,
); src_fd: pty_slave.as_raw_fd(),
frame.push( },
Redir::new( RedirType::Output,
IoMode::Fd { ));
tgt_fd: 2,
src_fd: pty_slave.as_raw_fd(),
},
RedirType::Output,
),
);
let _redir_guard = frame.redirect().unwrap(); let _redir_guard = frame.redirect().unwrap();
let old_cwd = env::current_dir().unwrap(); let old_cwd = env::current_dir().unwrap();
let saved_env = env::vars().collect(); let saved_env = env::vars().collect();
SHED.with(|s| s.save()); SHED.with(|s| s.save());
save_registers(); save_registers();
Self { Self {
_lock, _lock,
_redir_guard, _redir_guard,
old_cwd, old_cwd,
saved_env, saved_env,
pty_master, pty_master,
pty_slave, pty_slave,
cleanups: vec![], cleanups: vec![],
} }
} }
pub fn pty_slave(&self) -> BorrowedFd { pub fn pty_slave(&self) -> BorrowedFd {
unsafe { BorrowedFd::borrow_raw(self.pty_slave.as_raw_fd()) } unsafe { BorrowedFd::borrow_raw(self.pty_slave.as_raw_fd()) }
} }
pub fn add_cleanup(&mut self, f: impl FnOnce() + 'static) { pub fn add_cleanup(&mut self, f: impl FnOnce() + 'static) {
self.cleanups.push(Box::new(f)); self.cleanups.push(Box::new(f));
} }
pub fn read_output(&self) -> String { pub fn read_output(&self) -> String {
let flags = fcntl(self.pty_master.as_raw_fd(), FcntlArg::F_GETFL).unwrap(); let flags = fcntl(self.pty_master.as_raw_fd(), FcntlArg::F_GETFL).unwrap();
let flags = OFlag::from_bits_truncate(flags); let flags = OFlag::from_bits_truncate(flags);
fcntl( fcntl(
self.pty_master.as_raw_fd(), self.pty_master.as_raw_fd(),
FcntlArg::F_SETFL(flags | OFlag::O_NONBLOCK), FcntlArg::F_SETFL(flags | OFlag::O_NONBLOCK),
).unwrap(); )
.unwrap();
let mut out = vec![]; let mut out = vec![];
let mut buf = [0;4096]; let mut buf = [0; 4096];
loop { loop {
match read(self.pty_master.as_raw_fd(), &mut buf) { match read(self.pty_master.as_raw_fd(), &mut buf) {
Ok(0) => break, Ok(0) => break,
Ok(n) => out.extend_from_slice(&buf[..n]), Ok(n) => out.extend_from_slice(&buf[..n]),
Err(_) => break, Err(_) => break,
} }
} }
fcntl( fcntl(self.pty_master.as_raw_fd(), FcntlArg::F_SETFL(flags)).unwrap();
self.pty_master.as_raw_fd(),
FcntlArg::F_SETFL(flags),
).unwrap();
String::from_utf8_lossy(&out).to_string() String::from_utf8_lossy(&out).to_string()
} }
} }
impl Default for TestGuard { impl Default for TestGuard {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()
} }
} }
impl Drop for TestGuard { impl Drop for TestGuard {
fn drop(&mut self) { fn drop(&mut self) {
env::set_current_dir(&self.old_cwd).ok(); env::set_current_dir(&self.old_cwd).ok();
for (k, _) in env::vars() { for (k, _) in env::vars() {
unsafe { env::remove_var(&k); } unsafe {
} env::remove_var(&k);
for (k, v) in &self.saved_env { }
unsafe { env::set_var(k, v); } }
} for (k, v) in &self.saved_env {
for cleanup in self.cleanups.drain(..).rev() { unsafe {
cleanup(); env::set_var(k, v);
} }
SHED.with(|s| s.restore()); }
restore_registers(); for cleanup in self.cleanups.drain(..).rev() {
} cleanup();
}
SHED.with(|s| s.restore());
restore_registers();
}
} }
pub fn get_ast(input: &str) -> ShResult<Vec<crate::parse::Node>> { pub fn get_ast(input: &str) -> ShResult<Vec<crate::parse::Node>> {
let log_tab = read_logic(|l| l.clone()); let log_tab = read_logic(|l| l.clone());
let input = expand_aliases(input.into(), HashSet::new(), &log_tab); let input = expand_aliases(input.into(), HashSet::new(), &log_tab);
let source_name = "test_input".to_string(); let source_name = "test_input".to_string();
let mut parser = ParsedSrc::new(Arc::new(input)) let mut parser = ParsedSrc::new(Arc::new(input))
.with_lex_flags(LexFlags::empty()) .with_lex_flags(LexFlags::empty())
.with_name(source_name.clone()); .with_name(source_name.clone());
parser.parse_src().map_err(|e| e.into_iter().next().unwrap())?; parser
.parse_src()
.map_err(|e| e.into_iter().next().unwrap())?;
Ok(parser.extract_nodes()) Ok(parser.extract_nodes())
} }
impl crate::parse::Node { impl crate::parse::Node {
pub fn assert_structure(&mut self, expected: &mut impl Iterator<Item = NdKind>) -> Result<(), String> { pub fn assert_structure(
let mut full_structure = vec![]; &mut self,
let mut before = vec![]; expected: &mut impl Iterator<Item = NdKind>,
let mut after = vec![]; ) -> Result<(), String> {
let mut offender = None; let mut full_structure = vec![];
let mut before = vec![];
let mut after = vec![];
let mut offender = None;
self.walk_tree(&mut |s| { self.walk_tree(&mut |s| {
let expected_rule = expected.next(); let expected_rule = expected.next();
full_structure.push(s.class.as_nd_kind()); full_structure.push(s.class.as_nd_kind());
if offender.is_none() && expected_rule.as_ref().map_or(true, |e| *e != s.class.as_nd_kind()) { if offender.is_none()
offender = Some((s.class.as_nd_kind(), expected_rule)); && expected_rule
} else if offender.is_none() { .as_ref()
before.push(s.class.as_nd_kind()); .map_or(true, |e| *e != s.class.as_nd_kind())
} else { {
after.push(s.class.as_nd_kind()); offender = Some((s.class.as_nd_kind(), expected_rule));
} } else if offender.is_none() {
}); before.push(s.class.as_nd_kind());
} else {
after.push(s.class.as_nd_kind());
}
});
assert!(expected.next().is_none(), "Expected structure has more nodes than actual structure"); assert!(
expected.next().is_none(),
"Expected structure has more nodes than actual structure"
);
if let Some((nd_kind, expected_rule)) = offender { if let Some((nd_kind, expected_rule)) = offender {
let expected_rule = expected_rule.map_or("(none — expected array too short)".into(), |e| format!("{e:?}")); let expected_rule = expected_rule.map_or("(none — expected array too short)".into(), |e| {
let full_structure_hint = full_structure.into_iter() format!("{e:?}")
.map(|s| format!("\tNdKind::{s:?},")) });
.collect::<Vec<String>>() let full_structure_hint = full_structure
.join("\n"); .into_iter()
let full_structure_hint = format!("let expected = &mut [\n{full_structure_hint}\n].into_iter();"); .map(|s| format!("\tNdKind::{s:?},"))
.collect::<Vec<String>>()
.join("\n");
let full_structure_hint =
format!("let expected = &mut [\n{full_structure_hint}\n].into_iter();");
let output = [ let output = [
"Structure assertion failed!\n".into(), "Structure assertion failed!\n".into(),
format!("Expected node type '{:?}', found '{:?}'", expected_rule, nd_kind), format!(
format!("Before offender: {:?}", before), "Expected node type '{:?}', found '{:?}'",
format!("After offender: {:?}\n", after), expected_rule, nd_kind
format!("hint: here is the full structure as an array\n {full_structure_hint}"), ),
].join("\n"); format!("Before offender: {:?}", before),
format!("After offender: {:?}\n", after),
format!("hint: here is the full structure as an array\n {full_structure_hint}"),
]
.join("\n");
Err(output) Err(output)
} else { } else {
Ok(()) Ok(())
} }
} }
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@@ -227,26 +248,26 @@ pub enum NdKind {
Conjunction, Conjunction,
Assignment, Assignment,
BraceGrp, BraceGrp,
Negate, Negate,
Test, Test,
FuncDef, FuncDef,
} }
impl crate::parse::NdRule { impl crate::parse::NdRule {
pub fn as_nd_kind(&self) -> NdKind { pub fn as_nd_kind(&self) -> NdKind {
match self { match self {
Self::Negate { .. } => NdKind::Negate, Self::Negate { .. } => NdKind::Negate,
Self::IfNode { .. } => NdKind::IfNode, Self::IfNode { .. } => NdKind::IfNode,
Self::LoopNode { .. } => NdKind::LoopNode, Self::LoopNode { .. } => NdKind::LoopNode,
Self::ForNode { .. } => NdKind::ForNode, Self::ForNode { .. } => NdKind::ForNode,
Self::CaseNode { .. } => NdKind::CaseNode, Self::CaseNode { .. } => NdKind::CaseNode,
Self::Command { .. } => NdKind::Command, Self::Command { .. } => NdKind::Command,
Self::Pipeline { .. } => NdKind::Pipeline, Self::Pipeline { .. } => NdKind::Pipeline,
Self::Conjunction { .. } => NdKind::Conjunction, Self::Conjunction { .. } => NdKind::Conjunction,
Self::Assignment { .. } => NdKind::Assignment, Self::Assignment { .. } => NdKind::Assignment,
Self::BraceGrp { .. } => NdKind::BraceGrp, Self::BraceGrp { .. } => NdKind::BraceGrp,
Self::Test { .. } => NdKind::Test, Self::Test { .. } => NdKind::Test,
Self::FuncDef { .. } => NdKind::FuncDef, Self::FuncDef { .. } => NdKind::FuncDef,
} }
} }
} }