diff --git a/src/builtin/alias.rs b/src/builtin/alias.rs index 1062bd2..65a8ea8 100644 --- a/src/builtin/alias.rs +++ b/src/builtin/alias.rs @@ -38,28 +38,30 @@ pub fn alias(node: Node) -> ShResult<()> { write(stdout, alias_output.as_bytes())?; // Write it } else { for (arg, span) in argv { - let Some((name, body)) = arg.split_once('=') else { - let Some(alias) = read_logic(|l| l.get_alias(&arg)) else { - return Err(ShErr::at( - ShErrKind::SyntaxErr, - span, - "alias: Expected an assignment in alias args", - )); - }; + let Some(alias) = read_logic(|l| l.get_alias(&arg)) else { + return Err(ShErr::at( + ShErrKind::SyntaxErr, + span, + "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); - write(stdout, alias_output.as_bytes())?; // Write it - state::set_status(0); - return Ok(()); + let stdout = borrow_fd(STDOUT_FILENO); + write(stdout, alias_output.as_bytes())?; // Write it + state::set_status(0); + return Ok(()); }; if name == "command" || name == "builtin" { return Err(ShErr::at( ShErrKind::ExecFail, 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())); @@ -118,7 +120,7 @@ pub fn unalias(node: Node) -> ShResult<()> { mod tests { use crate::state::{self, read_logic}; use crate::testutil::{TestGuard, test_input}; - use pretty_assertions::assert_eq; + use pretty_assertions::assert_eq; #[test] fn alias_set_and_expand() { diff --git a/src/builtin/arrops.rs b/src/builtin/arrops.rs index 657b99d..0580619 100644 --- a/src/builtin/arrops.rs +++ b/src/builtin/arrops.rs @@ -229,9 +229,9 @@ pub fn get_arr_op_opts(opts: Vec) -> ShResult { #[cfg(test)] mod tests { - use std::collections::VecDeque; - 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 std::collections::VecDeque; fn set_arr(name: &str, elems: &[&str]) { let arr = VecDeque::from_iter(elems.iter().map(|s| s.to_string())); diff --git a/src/builtin/autocmd.rs b/src/builtin/autocmd.rs index 2b2ee55..9bd697b 100644 --- a/src/builtin/autocmd.rs +++ b/src/builtin/autocmd.rs @@ -159,7 +159,10 @@ mod tests { 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::PostCmd)).len(), 1); + assert_eq!( + read_logic(|l| l.get_autocmds(AutoCmdKind::PostCmd)).len(), + 1 + ); } // ===================== Pattern ===================== @@ -205,7 +208,10 @@ mod tests { 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::PostCmd)).len(), 1); + assert_eq!( + read_logic(|l| l.get_autocmds(AutoCmdKind::PostCmd)).len(), + 1 + ); } #[test] @@ -245,12 +251,22 @@ mod tests { fn all_kinds_parse() { let _guard = TestGuard::new(); let kinds = [ - "pre-cmd", "post-cmd", "pre-change-dir", "post-change-dir", - "on-job-finish", "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" + "pre-cmd", + "post-cmd", + "pre-change-dir", + "post-change-dir", + "on-job-finish", + "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 { test_input(format!("autocmd {kind} 'true'")).unwrap(); diff --git a/src/builtin/cd.rs b/src/builtin/cd.rs index a3a8ead..29c8a35 100644 --- a/src/builtin/cd.rs +++ b/src/builtin/cd.rs @@ -78,159 +78,165 @@ pub fn cd(node: Node) -> ShResult<()> { #[cfg(test)] pub mod tests { - use std::env; - use std::fs; + use std::env; + use std::fs; - use tempfile::TempDir; + use tempfile::TempDir; - use crate::state; - use crate::testutil::{TestGuard, test_input}; + use crate::state; + use crate::testutil::{TestGuard, test_input}; - // ===================== Basic Navigation ===================== + // ===================== Basic Navigation ===================== - #[test] - fn cd_simple() { - let _g = TestGuard::new(); - let old_dir = env::current_dir().unwrap(); - let temp_dir = TempDir::new().unwrap(); + #[test] + fn cd_simple() { + let _g = TestGuard::new(); + let old_dir = env::current_dir().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(); - assert_ne!(old_dir, new_dir); + let new_dir = env::current_dir().unwrap(); + 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] - fn cd_no_args_goes_home() { - let _g = TestGuard::new(); - let temp_dir = TempDir::new().unwrap(); - unsafe { env::set_var("HOME", temp_dir.path()) }; + #[test] + fn cd_no_args_goes_home() { + let _g = TestGuard::new(); + let temp_dir = TempDir::new().unwrap(); + unsafe { env::set_var("HOME", temp_dir.path()) }; - test_input("cd").unwrap(); + test_input("cd").unwrap(); - let cwd = env::current_dir().unwrap(); - assert_eq!(cwd.display().to_string(), temp_dir.path().display().to_string()); - } + let cwd = env::current_dir().unwrap(); + assert_eq!( + cwd.display().to_string(), + temp_dir.path().display().to_string() + ); + } - #[test] - fn cd_relative_path() { - let _g = TestGuard::new(); - let temp_dir = TempDir::new().unwrap(); - let sub = temp_dir.path().join("child"); - fs::create_dir(&sub).unwrap(); + #[test] + fn cd_relative_path() { + let _g = TestGuard::new(); + let temp_dir = TempDir::new().unwrap(); + let sub = temp_dir.path().join("child"); + fs::create_dir(&sub).unwrap(); - test_input(format!("cd {}", temp_dir.path().display())).unwrap(); - test_input("cd child").unwrap(); + test_input(format!("cd {}", temp_dir.path().display())).unwrap(); + test_input("cd child").unwrap(); - let cwd = env::current_dir().unwrap(); - assert_eq!(cwd.display().to_string(), sub.display().to_string()); - } + let cwd = env::current_dir().unwrap(); + assert_eq!(cwd.display().to_string(), sub.display().to_string()); + } - // ===================== Environment ===================== + // ===================== Environment ===================== - #[test] - fn cd_sets_pwd_env() { - let _g = TestGuard::new(); - let temp_dir = TempDir::new().unwrap(); + #[test] + fn cd_sets_pwd_env() { + let _g = TestGuard::new(); + 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(); - assert_eq!(pwd, env::current_dir().unwrap().display().to_string()); - } + let pwd = env::var("PWD").unwrap(); + assert_eq!(pwd, env::current_dir().unwrap().display().to_string()); + } - #[test] - fn cd_status_zero_on_success() { - let _g = TestGuard::new(); - let temp_dir = TempDir::new().unwrap(); + #[test] + fn cd_status_zero_on_success() { + let _g = TestGuard::new(); + 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] - fn cd_nonexistent_dir_fails() { - let _g = TestGuard::new(); - let result = test_input("cd /nonexistent_path_that_does_not_exist_xyz"); - assert!(result.is_err()); - } + #[test] + fn cd_nonexistent_dir_fails() { + let _g = TestGuard::new(); + let result = test_input("cd /nonexistent_path_that_does_not_exist_xyz"); + assert!(result.is_err()); + } - #[test] - fn cd_file_not_directory_fails() { - let _g = TestGuard::new(); - let temp_dir = TempDir::new().unwrap(); - let file_path = temp_dir.path().join("afile.txt"); - fs::write(&file_path, "hello").unwrap(); + #[test] + fn cd_file_not_directory_fails() { + let _g = TestGuard::new(); + let temp_dir = TempDir::new().unwrap(); + let file_path = temp_dir.path().join("afile.txt"); + fs::write(&file_path, "hello").unwrap(); - let result = test_input(format!("cd {}", file_path.display())); - assert!(result.is_err()); - } + let result = test_input(format!("cd {}", file_path.display())); + assert!(result.is_err()); + } - // ===================== Multiple cd ===================== + // ===================== Multiple cd ===================== - #[test] - fn cd_multiple_times() { - let _g = TestGuard::new(); - let dir_a = TempDir::new().unwrap(); - let dir_b = TempDir::new().unwrap(); + #[test] + fn cd_multiple_times() { + let _g = TestGuard::new(); + let dir_a = TempDir::new().unwrap(); + let dir_b = TempDir::new().unwrap(); - test_input(format!("cd {}", dir_a.path().display())).unwrap(); - assert_eq!( - env::current_dir().unwrap().display().to_string(), - dir_a.path().display().to_string() - ); + test_input(format!("cd {}", dir_a.path().display())).unwrap(); + assert_eq!( + env::current_dir().unwrap().display().to_string(), + dir_a.path().display().to_string() + ); - test_input(format!("cd {}", dir_b.path().display())).unwrap(); - assert_eq!( - env::current_dir().unwrap().display().to_string(), - dir_b.path().display().to_string() - ); - } + test_input(format!("cd {}", dir_b.path().display())).unwrap(); + assert_eq!( + env::current_dir().unwrap().display().to_string(), + dir_b.path().display().to_string() + ); + } - #[test] - fn cd_nested_subdirectories() { - let _g = TestGuard::new(); - let temp_dir = TempDir::new().unwrap(); - let deep = temp_dir.path().join("a").join("b").join("c"); - fs::create_dir_all(&deep).unwrap(); + #[test] + fn cd_nested_subdirectories() { + let _g = TestGuard::new(); + let temp_dir = TempDir::new().unwrap(); + let deep = temp_dir.path().join("a").join("b").join("c"); + fs::create_dir_all(&deep).unwrap(); - test_input(format!("cd {}", deep.display())).unwrap(); - assert_eq!( - env::current_dir().unwrap().display().to_string(), - deep.display().to_string() - ); - } + test_input(format!("cd {}", deep.display())).unwrap(); + assert_eq!( + env::current_dir().unwrap().display().to_string(), + deep.display().to_string() + ); + } - // ===================== Autocmd Integration ===================== + // ===================== Autocmd Integration ===================== - #[test] - fn cd_fires_post_change_dir_autocmd() { - let guard = TestGuard::new(); - let temp_dir = TempDir::new().unwrap(); + #[test] + fn cd_fires_post_change_dir_autocmd() { + let guard = TestGuard::new(); + let temp_dir = TempDir::new().unwrap(); - test_input("autocmd post-change-dir 'echo cd-hook-fired'").unwrap(); - guard.read_output(); + test_input("autocmd post-change-dir 'echo cd-hook-fired'").unwrap(); + guard.read_output(); - test_input(format!("cd {}", temp_dir.path().display())).unwrap(); - let out = guard.read_output(); - assert!(out.contains("cd-hook-fired")); - } + test_input(format!("cd {}", temp_dir.path().display())).unwrap(); + let out = guard.read_output(); + assert!(out.contains("cd-hook-fired")); + } - #[test] - fn cd_fires_pre_change_dir_autocmd() { - let guard = TestGuard::new(); - let temp_dir = TempDir::new().unwrap(); + #[test] + fn cd_fires_pre_change_dir_autocmd() { + let guard = TestGuard::new(); + let temp_dir = TempDir::new().unwrap(); - test_input("autocmd pre-change-dir 'echo pre-cd'").unwrap(); - guard.read_output(); + test_input("autocmd pre-change-dir 'echo pre-cd'").unwrap(); + guard.read_output(); - test_input(format!("cd {}", temp_dir.path().display())).unwrap(); - let out = guard.read_output(); - assert!(out.contains("pre-cd")); - } + test_input(format!("cd {}", temp_dir.path().display())).unwrap(); + let out = guard.read_output(); + assert!(out.contains("pre-cd")); + } } diff --git a/src/builtin/complete.rs b/src/builtin/complete.rs index 3a703a3..b2a11bc 100644 --- a/src/builtin/complete.rs +++ b/src/builtin/complete.rs @@ -176,20 +176,20 @@ pub fn complete_builtin(node: Node) -> ShResult<()> { read_meta(|m| -> ShResult<()> { let specs = m.comp_specs().values(); for spec in specs { - let stdout = borrow_fd(STDOUT_FILENO); - write(stdout, spec.source().as_bytes())?; + let stdout = borrow_fd(STDOUT_FILENO); + write(stdout, spec.source().as_bytes())?; } - Ok(()) + Ok(()) })?; } else { read_meta(|m| -> ShResult<()> { for (cmd, _) in &argv { if let Some(spec) = m.comp_specs().get(cmd) { - let stdout = borrow_fd(STDOUT_FILENO); - write(stdout, spec.source().as_bytes())?; + let stdout = borrow_fd(STDOUT_FILENO); + write(stdout, spec.source().as_bytes())?; } } - Ok(()) + Ok(()) })?; } @@ -316,10 +316,10 @@ pub fn get_comp_opts(opts: Vec) -> ShResult { #[cfg(test)] mod tests { + use crate::state::{self, VarFlags, VarKind, read_meta, write_vars}; + use crate::testutil::{TestGuard, test_input}; use std::fs; use tempfile::TempDir; - use crate::state::{self, read_meta, write_vars, VarFlags, VarKind}; - use crate::testutil::{TestGuard, test_input}; // ===================== complete: Registration ===================== diff --git a/src/builtin/dirstack.rs b/src/builtin/dirstack.rs index e337194..0c05647 100644 --- a/src/builtin/dirstack.rs +++ b/src/builtin/dirstack.rs @@ -12,12 +12,13 @@ use crate::{ }; pub fn truncate_home_path(path: String) -> String { - if let Ok(home) = env::var("HOME") - && path.starts_with(&home) { - let new = path.strip_prefix(&home).unwrap(); - return format!("~{new}"); - } - path.to_string() + if let Ok(home) = env::var("HOME") + && path.starts_with(&home) + { + let new = path.strip_prefix(&home).unwrap(); + return format!("~{new}"); + } + path.to_string() } enum StackIdx { @@ -376,8 +377,7 @@ pub fn dirs(node: Node) -> ShResult<()> { .map(|d| d.to_string_lossy().to_string()); if abbreviate_home { - stack.map(truncate_home_path) - .collect() + stack.map(truncate_home_path).collect() } else { stack.collect() } @@ -428,189 +428,198 @@ pub fn dirs(node: Node) -> ShResult<()> { #[cfg(test)] pub mod tests { - use std::{env, path::PathBuf}; - use crate::{state::{self, read_meta}, testutil::{TestGuard, test_input}}; - use pretty_assertions::{assert_ne,assert_eq}; -use tempfile::TempDir; + use crate::{ + state::{self, read_meta}, + testutil::{TestGuard, test_input}, + }; + use pretty_assertions::{assert_eq, assert_ne}; + use std::{env, path::PathBuf}; + use tempfile::TempDir; - #[test] - fn test_pushd_interactive() { - let g = TestGuard::new(); - let current_dir = env::current_dir().unwrap(); + #[test] + fn test_pushd_interactive() { + let g = TestGuard::new(); + 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_eq!(new_dir, PathBuf::from("/tmp")); + assert_ne!(new_dir, current_dir); + assert_eq!(new_dir, PathBuf::from("/tmp")); - let dir_stack = read_meta(|m| m.dirs().clone()); - assert_eq!(dir_stack.len(), 1); - assert_eq!(dir_stack[0], current_dir); + let dir_stack = read_meta(|m| m.dirs().clone()); + assert_eq!(dir_stack.len(), 1); + assert_eq!(dir_stack[0], current_dir); - let out = g.read_output(); - let path = super::truncate_home_path(current_dir.to_string_lossy().to_string()); - assert_eq!(out, format!("/tmp {path}\n")); - } + let out = g.read_output(); + let path = super::truncate_home_path(current_dir.to_string_lossy().to_string()); + assert_eq!(out, format!("/tmp {path}\n")); + } - #[test] - fn test_popd_interactive() { - let g = TestGuard::new(); - let current_dir = env::current_dir().unwrap(); - let tempdir = TempDir::new().unwrap(); - let tempdir_raw = tempdir.path().to_path_buf().to_string_lossy().to_string(); + #[test] + fn test_popd_interactive() { + let g = TestGuard::new(); + let current_dir = env::current_dir().unwrap(); + let tempdir = TempDir::new().unwrap(); + 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()); - assert_eq!(dir_stack.len(), 1); - assert_eq!(dir_stack[0], current_dir); + let dir_stack = read_meta(|m| m.dirs().clone()); + assert_eq!(dir_stack.len(), 1); + assert_eq!(dir_stack[0], current_dir); - assert_eq!(env::current_dir().unwrap(), tempdir.path()); - g.read_output(); // consume output of pushd + assert_eq!(env::current_dir().unwrap(), tempdir.path()); + g.read_output(); // consume output of pushd - test_input("popd").unwrap(); + test_input("popd").unwrap(); - assert_eq!(env::current_dir().unwrap(), current_dir); - let out = g.read_output(); - let path = super::truncate_home_path(current_dir.to_string_lossy().to_string()); - assert_eq!(out, format!("{path}\n")); - } + assert_eq!(env::current_dir().unwrap(), current_dir); + let out = g.read_output(); + let path = super::truncate_home_path(current_dir.to_string_lossy().to_string()); + assert_eq!(out, format!("{path}\n")); + } - #[test] - fn test_popd_empty_stack() { - let _g = TestGuard::new(); + #[test] + fn test_popd_empty_stack() { + let _g = TestGuard::new(); - test_input("popd").unwrap_err(); - assert_ne!(state::get_status(), 0); - } + test_input("popd").unwrap_err(); + assert_ne!(state::get_status(), 0); + } - #[test] - fn test_pushd_multiple_then_popd() { - let g = TestGuard::new(); - let original = env::current_dir().unwrap(); - let tmp1 = TempDir::new().unwrap(); - let tmp2 = TempDir::new().unwrap(); - let path1 = tmp1.path().to_path_buf(); - let path2 = tmp2.path().to_path_buf(); + #[test] + fn test_pushd_multiple_then_popd() { + let g = TestGuard::new(); + let original = env::current_dir().unwrap(); + let tmp1 = TempDir::new().unwrap(); + let tmp2 = TempDir::new().unwrap(); + let path1 = tmp1.path().to_path_buf(); + let path2 = tmp2.path().to_path_buf(); - test_input(format!("pushd {}", path1.display())).unwrap(); - test_input(format!("pushd {}", path2.display())).unwrap(); - g.read_output(); + test_input(format!("pushd {}", path1.display())).unwrap(); + test_input(format!("pushd {}", path2.display())).unwrap(); + g.read_output(); - assert_eq!(env::current_dir().unwrap(), path2); - let stack = read_meta(|m| m.dirs().clone()); - assert_eq!(stack.len(), 2); - assert_eq!(stack[0], path1); - assert_eq!(stack[1], original); + assert_eq!(env::current_dir().unwrap(), path2); + let stack = read_meta(|m| m.dirs().clone()); + assert_eq!(stack.len(), 2); + assert_eq!(stack[0], path1); + assert_eq!(stack[1], original); - test_input("popd").unwrap(); - assert_eq!(env::current_dir().unwrap(), path1); + test_input("popd").unwrap(); + assert_eq!(env::current_dir().unwrap(), path1); - test_input("popd").unwrap(); - assert_eq!(env::current_dir().unwrap(), original); + test_input("popd").unwrap(); + assert_eq!(env::current_dir().unwrap(), original); - let stack = read_meta(|m| m.dirs().clone()); - assert_eq!(stack.len(), 0); - } + let stack = read_meta(|m| m.dirs().clone()); + assert_eq!(stack.len(), 0); + } - #[test] - fn test_pushd_rotate_plus() { - let g = TestGuard::new(); - let original = env::current_dir().unwrap(); - let tmp1 = TempDir::new().unwrap(); - let tmp2 = TempDir::new().unwrap(); - let path1 = tmp1.path().to_path_buf(); - let path2 = tmp2.path().to_path_buf(); + #[test] + fn test_pushd_rotate_plus() { + let g = TestGuard::new(); + let original = env::current_dir().unwrap(); + let tmp1 = TempDir::new().unwrap(); + let tmp2 = TempDir::new().unwrap(); + let path1 = tmp1.path().to_path_buf(); + let path2 = tmp2.path().to_path_buf(); - // Build stack: cwd=original, then pushd path1, pushd path2 - // Stack after: cwd=path2, [path1, original] - test_input(format!("pushd {}", path1.display())).unwrap(); - test_input(format!("pushd {}", path2.display())).unwrap(); - g.read_output(); + // Build stack: cwd=original, then pushd path1, pushd path2 + // Stack after: cwd=path2, [path1, original] + test_input(format!("pushd {}", path1.display())).unwrap(); + test_input(format!("pushd {}", path2.display())).unwrap(); + g.read_output(); - // pushd +1 rotates: [path2, path1, original] -> rotate_left(1) -> [path1, original, path2] - // pop front -> cwd=path1, stack=[original, path2] - test_input("pushd +1").unwrap(); - assert_eq!(env::current_dir().unwrap(), path1); + // pushd +1 rotates: [path2, path1, original] -> rotate_left(1) -> [path1, original, path2] + // pop front -> cwd=path1, stack=[original, path2] + test_input("pushd +1").unwrap(); + assert_eq!(env::current_dir().unwrap(), path1); - let stack = read_meta(|m| m.dirs().clone()); - assert_eq!(stack.len(), 2); - assert_eq!(stack[0], original); - assert_eq!(stack[1], path2); - } + let stack = read_meta(|m| m.dirs().clone()); + assert_eq!(stack.len(), 2); + assert_eq!(stack[0], original); + assert_eq!(stack[1], path2); + } - #[test] - fn test_pushd_no_cd_flag() { - let _g = TestGuard::new(); - let original = env::current_dir().unwrap(); - let tmp = TempDir::new().unwrap(); - let path = tmp.path().to_path_buf(); + #[test] + fn test_pushd_no_cd_flag() { + let _g = TestGuard::new(); + let original = env::current_dir().unwrap(); + let tmp = TempDir::new().unwrap(); + 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 - assert_eq!(env::current_dir().unwrap(), original); - } + // -n means don't cd, but the dir should still be on the stack + assert_eq!(env::current_dir().unwrap(), original); + } - #[test] - fn test_dirs_clear() { - let _g = TestGuard::new(); - let tmp = TempDir::new().unwrap(); + #[test] + fn test_dirs_clear() { + let _g = TestGuard::new(); + let tmp = TempDir::new().unwrap(); - test_input(format!("pushd {}", tmp.path().display())).unwrap(); - assert_eq!(read_meta(|m| m.dirs().len()), 1); + test_input(format!("pushd {}", tmp.path().display())).unwrap(); + assert_eq!(read_meta(|m| m.dirs().len()), 1); - test_input("dirs -c").unwrap(); - assert_eq!(read_meta(|m| m.dirs().len()), 0); - } + test_input("dirs -c").unwrap(); + assert_eq!(read_meta(|m| m.dirs().len()), 0); + } - #[test] - fn test_dirs_one_per_line() { - let g = TestGuard::new(); - let original = env::current_dir().unwrap(); - let tmp = TempDir::new().unwrap(); - let path = tmp.path().to_path_buf(); + #[test] + fn test_dirs_one_per_line() { + let g = TestGuard::new(); + let original = env::current_dir().unwrap(); + let tmp = TempDir::new().unwrap(); + let path = tmp.path().to_path_buf(); - test_input(format!("pushd {}", path.display())).unwrap(); - g.read_output(); + test_input(format!("pushd {}", path.display())).unwrap(); + g.read_output(); - test_input("dirs -p").unwrap(); - let out = g.read_output(); - let lines: Vec<&str> = out.split('\n').filter(|l| !l.is_empty()).collect(); - assert_eq!(lines.len(), 2); - assert_eq!(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_input("dirs -p").unwrap(); + let out = g.read_output(); + let lines: Vec<&str> = out.split('\n').filter(|l| !l.is_empty()).collect(); + assert_eq!(lines.len(), 2); + assert_eq!( + 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] - fn test_popd_indexed_from_top() { - let _g = TestGuard::new(); - let original = env::current_dir().unwrap(); - let tmp1 = TempDir::new().unwrap(); - let tmp2 = TempDir::new().unwrap(); - let path1 = tmp1.path().to_path_buf(); - let path2 = tmp2.path().to_path_buf(); + #[test] + fn test_popd_indexed_from_top() { + let _g = TestGuard::new(); + let original = env::current_dir().unwrap(); + let tmp1 = TempDir::new().unwrap(); + let tmp2 = TempDir::new().unwrap(); + let path1 = tmp1.path().to_path_buf(); + let path2 = tmp2.path().to_path_buf(); - // Stack: cwd=path2, [path1, original] - test_input(format!("pushd {}", path1.display())).unwrap(); - test_input(format!("pushd {}", path2.display())).unwrap(); + // Stack: cwd=path2, [path1, original] + test_input(format!("pushd {}", path1.display())).unwrap(); + test_input(format!("pushd {}", path2.display())).unwrap(); - // popd +1 removes index (1-1)=0 from stored dirs, i.e. path1 - test_input("popd +1").unwrap(); - assert_eq!(env::current_dir().unwrap(), path2); // no cd + // popd +1 removes index (1-1)=0 from stored dirs, i.e. path1 + test_input("popd +1").unwrap(); + assert_eq!(env::current_dir().unwrap(), path2); // no cd - let stack = read_meta(|m| m.dirs().clone()); - assert_eq!(stack.len(), 1); - assert_eq!(stack[0], original); - } + let stack = read_meta(|m| m.dirs().clone()); + assert_eq!(stack.len(), 1); + assert_eq!(stack[0], original); + } - #[test] - fn test_pushd_nonexistent_dir() { - let _g = TestGuard::new(); + #[test] + fn test_pushd_nonexistent_dir() { + let _g = TestGuard::new(); - let result = test_input("pushd /nonexistent_dir_12345"); - assert!(result.is_err()); - } + let result = test_input("pushd /nonexistent_dir_12345"); + assert!(result.is_err()); + } } diff --git a/src/builtin/echo.rs b/src/builtin/echo.rs index 0c0793e..019c207 100644 --- a/src/builtin/echo.rs +++ b/src/builtin/echo.rs @@ -31,7 +31,7 @@ bitflags! { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct EchoFlags: u32 { const NO_NEWLINE = 0b000001; - const NO_ESCAPE = 0b000010; + const NO_ESCAPE = 0b000010; const USE_ESCAPE = 0b000100; const USE_PROMPT = 0b001000; } @@ -55,16 +55,17 @@ pub fn echo(node: Node) -> ShResult<()> { } 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( argv .into_iter() .map(|a| a.0) // Extract the String from the tuple of (String,Span) .collect::>(), - use_escape, + use_escape, flags.contains(EchoFlags::USE_PROMPT), )? .join(" "); @@ -207,7 +208,7 @@ pub fn get_echo_flags(opts: Vec) -> ShResult { Opt::Short('n') => flags |= EchoFlags::NO_NEWLINE, Opt::Short('e') => flags |= EchoFlags::USE_ESCAPE, 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( ShErrKind::ExecFail, @@ -308,11 +309,7 @@ mod tests { #[test] fn prepare_multiple_args() { - let result = prepare_echo_args( - vec!["hello".into(), "world".into()], - false, - false, - ).unwrap(); + let result = prepare_echo_args(vec!["hello".into(), "world".into()], false, false).unwrap(); assert_eq!(result, vec!["hello", "world"]); } diff --git a/src/builtin/eval.rs b/src/builtin/eval.rs index b572788..5da96be 100644 --- a/src/builtin/eval.rs +++ b/src/builtin/eval.rs @@ -37,7 +37,7 @@ pub fn eval(node: Node) -> ShResult<()> { #[cfg(test)] 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}; // ===================== Basic ===================== @@ -80,7 +80,8 @@ mod tests { #[test] fn eval_expands_variable() { 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(); let out = guard.read_output(); diff --git a/src/builtin/exec.rs b/src/builtin/exec.rs index 31c962a..d81ca26 100644 --- a/src/builtin/exec.rs +++ b/src/builtin/exec.rs @@ -50,7 +50,7 @@ pub fn exec_builtin(node: Node) -> ShResult<()> { mod tests { use crate::state; 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] fn exec_no_args_succeeds() { @@ -62,7 +62,9 @@ mod tests { #[test] fn exec_nonexistent_command_fails() { 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()); } } diff --git a/src/builtin/intro.rs b/src/builtin/intro.rs index 5476cdd..af49991 100644 --- a/src/builtin/intro.rs +++ b/src/builtin/intro.rs @@ -185,7 +185,7 @@ mod tests { let out = guard.read_output(); assert!(out.contains("cat")); assert!(out.contains("is")); - assert!(out.contains("/")); // Should show a path + assert!(out.contains("/")); // Should show a path } // ===================== Not found ===================== diff --git a/src/builtin/keymap.rs b/src/builtin/keymap.rs index c093f9f..e1b76ad 100644 --- a/src/builtin/keymap.rs +++ b/src/builtin/keymap.rs @@ -81,10 +81,10 @@ impl KeyMapOpts { opt: Opt::Short('o'), // operator-pending mode takes_arg: false, }, - OptSpec { - opt: Opt::Long("remove".into()), - takes_arg: true, - }, + OptSpec { + opt: Opt::Long("remove".into()), + takes_arg: true, + }, OptSpec { opt: Opt::Short('r'), // replace mode takes_arg: false, @@ -180,8 +180,8 @@ pub fn keymap(node: Node) -> ShResult<()> { #[cfg(test)] mod tests { use super::*; - use crate::getopt::Opt; use crate::expand::expand_keymap; + use crate::getopt::Opt; use crate::state::{self, read_logic}; use crate::testutil::{TestGuard, test_input}; @@ -217,7 +217,8 @@ mod tests { let opts = KeyMapOpts::from_opts(&[ Opt::Short('n'), Opt::LongWithArg("remove".into(), "jk".into()), - ]).unwrap(); + ]) + .unwrap(); assert_eq!(opts.remove, Some("jk".into())); } @@ -273,10 +274,7 @@ mod tests { let _g = TestGuard::new(); test_input("keymap -n jk ''").unwrap(); - let maps = read_logic(|l| l.keymaps_filtered( - KeyMapFlags::NORMAL, - &expand_keymap("jk"), - )); + let maps = read_logic(|l| l.keymaps_filtered(KeyMapFlags::NORMAL, &expand_keymap("jk"))); assert!(!maps.is_empty()); } @@ -285,10 +283,7 @@ mod tests { let _g = TestGuard::new(); test_input("keymap -i jk ''").unwrap(); - let maps = read_logic(|l| l.keymaps_filtered( - KeyMapFlags::INSERT, - &expand_keymap("jk"), - )); + let maps = read_logic(|l| l.keymaps_filtered(KeyMapFlags::INSERT, &expand_keymap("jk"))); assert!(!maps.is_empty()); } @@ -298,10 +293,7 @@ mod tests { test_input("keymap -n jk ''").unwrap(); test_input("keymap -n jk 'dd'").unwrap(); - let maps = read_logic(|l| l.keymaps_filtered( - KeyMapFlags::NORMAL, - &expand_keymap("jk"), - )); + let maps = read_logic(|l| l.keymaps_filtered(KeyMapFlags::NORMAL, &expand_keymap("jk"))); assert_eq!(maps.len(), 1); assert_eq!(maps[0].action, "dd"); } @@ -312,10 +304,7 @@ mod tests { test_input("keymap -n jk ''").unwrap(); test_input("keymap -n --remove jk").unwrap(); - let maps = read_logic(|l| l.keymaps_filtered( - KeyMapFlags::NORMAL, - &expand_keymap("jk"), - )); + let maps = read_logic(|l| l.keymaps_filtered(KeyMapFlags::NORMAL, &expand_keymap("jk"))); assert!(maps.is_empty()); } diff --git a/src/builtin/map.rs b/src/builtin/map.rs index ef33008..ff839b7 100644 --- a/src/builtin/map.rs +++ b/src/builtin/map.rs @@ -389,7 +389,7 @@ pub fn get_map_opts(opts: Vec) -> MapOpts { #[cfg(test)] mod tests { - use super::{MapNode, MapFlags, get_map_opts}; + use super::{MapFlags, MapNode, get_map_opts}; use crate::getopt::Opt; use crate::state::{self, read_vars}; use crate::testutil::{TestGuard, test_input}; @@ -433,10 +433,7 @@ mod tests { #[test] fn mapnode_remove_nested() { let mut root = MapNode::default(); - root.set( - &["a".into(), "b".into()], - MapNode::StaticLeaf("val".into()), - ); + root.set(&["a".into(), "b".into()], MapNode::StaticLeaf("val".into())); root.remove(&["a".into(), "b".into()]); assert!(root.get(&["a".into(), "b".into()]).is_none()); // Parent branch should still exist diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index a86bc09..9bc35b0 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -17,20 +17,20 @@ pub mod keymap; pub mod map; pub mod pwd; pub mod read; +pub mod resource; pub mod shift; pub mod shopt; pub mod source; pub mod test; // [[ ]] thing pub mod trap; pub mod varcmds; -pub mod resource; pub const BUILTINS: [&str; 49] = [ - "echo", "cd", "read", "export", "local", "pwd", "source", ".", "shift", "jobs", "fg", "bg", "disown", - "alias", "unalias", "return", "break", "continue", "exit", "shopt", "builtin", + "echo", "cd", "read", "export", "local", "pwd", "source", ".", "shift", "jobs", "fg", "bg", + "disown", "alias", "unalias", "return", "break", "continue", "exit", "shopt", "builtin", "command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly", "unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type", - "getopts", "keymap", "read_key", "autocmd", "ulimit", "umask" + "getopts", "keymap", "read_key", "autocmd", "ulimit", "umask", ]; pub fn true_builtin() -> ShResult<()> { @@ -50,31 +50,34 @@ pub fn noop_builtin() -> ShResult<()> { #[cfg(test)] 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] - fn test_true() { - let _g = TestGuard::new(); - test_input("true").unwrap(); + #[test] + fn test_true() { + let _g = TestGuard::new(); + test_input("true").unwrap(); - assert_eq!(state::get_status(), 0); - } + assert_eq!(state::get_status(), 0); + } - #[test] - fn test_false() { - let _g = TestGuard::new(); - test_input("false").unwrap(); + #[test] + fn test_false() { + let _g = TestGuard::new(); + test_input("false").unwrap(); - assert_eq!(state::get_status(), 1); - } + assert_eq!(state::get_status(), 1); + } - #[test] - fn test_noop() { - let _g = TestGuard::new(); - test_input(":").unwrap(); + #[test] + fn test_noop() { + let _g = TestGuard::new(); + test_input(":").unwrap(); - assert_eq!(state::get_status(), 0); - } + assert_eq!(state::get_status(), 0); + } } diff --git a/src/builtin/pwd.rs b/src/builtin/pwd.rs index f8a673f..14f5b27 100644 --- a/src/builtin/pwd.rs +++ b/src/builtin/pwd.rs @@ -27,10 +27,10 @@ pub fn pwd(node: Node) -> ShResult<()> { #[cfg(test)] mod tests { - use std::env; - use tempfile::TempDir; use crate::state; use crate::testutil::{TestGuard, test_input}; + use std::env; + use tempfile::TempDir; #[test] fn pwd_prints_cwd() { diff --git a/src/builtin/read.rs b/src/builtin/read.rs index 3e81bea..8ed593f 100644 --- a/src/builtin/read.rs +++ b/src/builtin/read.rs @@ -367,7 +367,7 @@ pub fn get_read_key_opts(opts: Vec) -> ShResult { #[cfg(test)] 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}; // ===================== Basic read into REPLY ===================== diff --git a/src/builtin/resource.rs b/src/builtin/resource.rs index b070e4c..69f50d9 100644 --- a/src/builtin/resource.rs +++ b/src/builtin/resource.rs @@ -1,92 +1,115 @@ use ariadne::Fmt; -use nix::{libc::STDOUT_FILENO, sys::{resource::{Resource, getrlimit, setrlimit}, stat::{Mode, umask}}, unistd::write}; - -use crate::{ - getopt::{Opt, OptSpec, get_opts_from_tokens_strict}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color}, parse::{NdRule, Node}, procio::borrow_fd, state::{self} +use nix::{ + libc::STDOUT_FILENO, + sys::{ + resource::{Resource, getrlimit, setrlimit}, + stat::{Mode, umask}, + }, + unistd::write, }; -fn ulimit_opt_spec() -> [OptSpec;5] { - [ - OptSpec { - opt: Opt::Short('n'), // file descriptors - takes_arg: true, - }, - OptSpec { - opt: Opt::Short('u'), // max user processes - takes_arg: true, - }, - OptSpec { - opt: Opt::Short('s'), // stack size - 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, - } - ] +use crate::{ + getopt::{Opt, OptSpec, get_opts_from_tokens_strict}, + libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color}, + parse::{NdRule, Node}, + procio::borrow_fd, + state::{self}, +}; + +fn ulimit_opt_spec() -> [OptSpec; 5] { + [ + OptSpec { + opt: Opt::Short('n'), // file descriptors + takes_arg: true, + }, + OptSpec { + opt: Opt::Short('u'), // max user processes + takes_arg: true, + }, + OptSpec { + opt: Opt::Short('s'), // stack size + 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 { - fds: Option, - procs: Option, - stack: Option, - core: Option, - vmem: Option, + fds: Option, + procs: Option, + stack: Option, + core: Option, + vmem: Option, } fn get_ulimit_opts(opt: &[Opt]) -> ShResult { - let mut opts = UlimitOpts { - fds: None, - procs: None, - stack: None, - core: None, - vmem: None, - }; + let mut opts = UlimitOpts { + fds: None, + procs: None, + stack: None, + core: None, + vmem: None, + }; - for o in opt { - match o { - Opt::ShortWithArg('n', arg) => { - opts.fds = Some(arg.parse().map_err(|_| ShErr::simple( - 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, - format!("invalid argument for -u: {}", arg.fg(next_color())), - ))?); - }, - Opt::ShortWithArg('s', arg) => { - opts.stack = Some(arg.parse().map_err(|_| ShErr::simple( - ShErrKind::ParseErr, - format!("invalid argument for -s: {}", arg.fg(next_color())), - ))?); - }, - Opt::ShortWithArg('c', arg) => { - opts.core = Some(arg.parse().map_err(|_| ShErr::simple( - ShErrKind::ParseErr, - format!("invalid argument for -c: {}", arg.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())), - )), - } - } + for o in opt { + match o { + Opt::ShortWithArg('n', arg) => { + opts.fds = Some(arg.parse().map_err(|_| { + ShErr::simple( + 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, + format!("invalid argument for -u: {}", arg.fg(next_color())), + ) + })?); + } + Opt::ShortWithArg('s', arg) => { + opts.stack = Some(arg.parse().map_err(|_| { + ShErr::simple( + ShErrKind::ParseErr, + format!("invalid argument for -s: {}", arg.fg(next_color())), + ) + })?); + } + Opt::ShortWithArg('c', arg) => { + opts.core = Some(arg.parse().map_err(|_| { + ShErr::simple( + ShErrKind::ParseErr, + format!("invalid argument for -c: {}", arg.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<()> { @@ -99,282 +122,308 @@ pub fn ulimit(node: Node) -> ShResult<()> { unreachable!() }; - let (_, opts) = get_opts_from_tokens_strict(argv, &ulimit_opt_spec()).promote_err(span.clone())?; - let ulimit_opts = get_ulimit_opts(&opts).promote_err(span.clone())?; + let (_, opts) = + 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 { - let (_, hard) = getrlimit(Resource::RLIMIT_NOFILE).map_err(|e| ShErr::at( - ShErrKind::ExecFail, - 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(), - format!("failed to set file descriptor limit: {}", e), - ))?; - } - if let Some(procs) = ulimit_opts.procs { - let (_, hard) = getrlimit(Resource::RLIMIT_NPROC).map_err(|e| ShErr::at( - ShErrKind::ExecFail, - span.clone(), - format!("failed to get process limit: {}", e), - ))?; - setrlimit(Resource::RLIMIT_NPROC, procs, hard).map_err(|e| ShErr::at( - ShErrKind::ExecFail, - span.clone(), - format!("failed to set process limit: {}", e), - ))?; - } - if let Some(stack) = ulimit_opts.stack { - let (_, hard) = getrlimit(Resource::RLIMIT_STACK).map_err(|e| ShErr::at( - ShErrKind::ExecFail, - span.clone(), - format!("failed to get stack size limit: {}", e), - ))?; - setrlimit(Resource::RLIMIT_STACK, stack, hard).map_err(|e| ShErr::at( - ShErrKind::ExecFail, - span.clone(), - format!("failed to set stack size limit: {}", e), - ))?; - } - if let Some(core) = ulimit_opts.core { - let (_, hard) = getrlimit(Resource::RLIMIT_CORE).map_err(|e| ShErr::at( - ShErrKind::ExecFail, - span.clone(), - format!("failed to get core dump size limit: {}", e), - ))?; - setrlimit(Resource::RLIMIT_CORE, core, hard).map_err(|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), - ))?; - } + if let Some(fds) = ulimit_opts.fds { + let (_, hard) = getrlimit(Resource::RLIMIT_NOFILE).map_err(|e| { + ShErr::at( + ShErrKind::ExecFail, + 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(), + format!("failed to set file descriptor limit: {}", e), + ) + })?; + } + if let Some(procs) = ulimit_opts.procs { + let (_, hard) = getrlimit(Resource::RLIMIT_NPROC).map_err(|e| { + ShErr::at( + ShErrKind::ExecFail, + span.clone(), + format!("failed to get process limit: {}", e), + ) + })?; + setrlimit(Resource::RLIMIT_NPROC, procs, hard).map_err(|e| { + ShErr::at( + ShErrKind::ExecFail, + span.clone(), + format!("failed to set process limit: {}", e), + ) + })?; + } + if let Some(stack) = ulimit_opts.stack { + let (_, hard) = getrlimit(Resource::RLIMIT_STACK).map_err(|e| { + ShErr::at( + ShErrKind::ExecFail, + span.clone(), + format!("failed to get stack size limit: {}", e), + ) + })?; + setrlimit(Resource::RLIMIT_STACK, stack, hard).map_err(|e| { + ShErr::at( + ShErrKind::ExecFail, + span.clone(), + format!("failed to set stack size limit: {}", e), + ) + })?; + } + if let Some(core) = ulimit_opts.core { + let (_, hard) = getrlimit(Resource::RLIMIT_CORE).map_err(|e| { + ShErr::at( + ShErrKind::ExecFail, + span.clone(), + format!("failed to get core dump size limit: {}", e), + ) + })?; + setrlimit(Resource::RLIMIT_CORE, core, hard).map_err(|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); Ok(()) } pub fn umask_builtin(node: Node) -> ShResult<()> { - let span = node.get_span(); - let NdRule::Command { - assignments: _, - argv, - } = node.class else { unreachable!() }; + let span = node.get_span(); + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; - let (argv, opts) = get_opts_from_tokens_strict( - argv, - &[OptSpec { opt: Opt::Short('S'), takes_arg: false }], - )?; - let argv = &argv[1..]; // skip command name + let (argv, opts) = get_opts_from_tokens_strict( + argv, + &[OptSpec { + opt: Opt::Short('S'), + takes_arg: false, + }], + )?; + let argv = &argv[1..]; // skip command name - let old = umask(Mode::empty()); - umask(old); - let mut old_bits = old.bits(); + let old = umask(Mode::empty()); + umask(old); + let mut old_bits = old.bits(); - if !argv.is_empty() { - if argv.len() > 1 { - return Err(ShErr::at( - ShErrKind::ParseErr, - span.clone(), - format!("umask takes at most one argument, got {}", argv.len()), - )); - } - let arg = argv[0].clone(); - let raw = arg.as_str(); - if raw.chars().any(|c| c.is_ascii_digit()) { - let mode_raw = u32::from_str_radix(raw, 8).map_err(|_| ShErr::at( - ShErrKind::ParseErr, - span.clone(), - format!("invalid numeric umask: {}", raw.fg(next_color())), - ))?; + if !argv.is_empty() { + if argv.len() > 1 { + return Err(ShErr::at( + ShErrKind::ParseErr, + span.clone(), + format!("umask takes at most one argument, got {}", argv.len()), + )); + } + let arg = argv[0].clone(); + let raw = arg.as_str(); + if raw.chars().any(|c| c.is_ascii_digit()) { + let mode_raw = u32::from_str_radix(raw, 8).map_err(|_| { + ShErr::at( + ShErrKind::ParseErr, + span.clone(), + format!("invalid numeric umask: {}", raw.fg(next_color())), + ) + })?; - let mode = Mode::from_bits(mode_raw).ok_or_else(|| ShErr::at( - ShErrKind::ParseErr, - span.clone(), - format!("invalid umask value: {}", raw.fg(next_color())), - ))?; + let mode = Mode::from_bits(mode_raw).ok_or_else(|| { + ShErr::at( + ShErrKind::ParseErr, + span.clone(), + format!("invalid umask value: {}", raw.fg(next_color())), + ) + })?; - umask(mode); - } else { - let parts = raw.split(','); + umask(mode); + } else { + let parts = raw.split(','); - for part in parts { - if let Some((who,bits)) = part.split_once('=') { - let mut new_bits = 0; - if bits.contains('r') { - new_bits |= 4; - } - if bits.contains('w') { - new_bits |= 2; - } - if bits.contains('x') { - new_bits |= 1; - } + for part in parts { + if let Some((who, bits)) = part.split_once('=') { + let mut new_bits = 0; + if bits.contains('r') { + new_bits |= 4; + } + if bits.contains('w') { + new_bits |= 2; + } + if bits.contains('x') { + new_bits |= 1; + } - for ch in who.chars() { - match ch { - 'o' => { - old_bits &= !0o7; - old_bits |= !new_bits & 0o7; - } - 'g' => { - old_bits &= !(0o7 << 3); - old_bits |= (!new_bits & 0o7) << 3; - } - 'u' => { - old_bits &= !(0o7 << 6); - old_bits |= (!new_bits & 0o7) << 6; - } - 'a' => { - let denied = !new_bits & 0o7; - old_bits = denied | (denied << 3) | (denied << 6); - } - _ => { - return Err(ShErr::at( - ShErrKind::ParseErr, - span.clone(), - format!("invalid umask 'who' character: {}", ch.fg(next_color())), - )); - } - } - } + for ch in who.chars() { + match ch { + 'o' => { + old_bits &= !0o7; + old_bits |= !new_bits & 0o7; + } + 'g' => { + old_bits &= !(0o7 << 3); + old_bits |= (!new_bits & 0o7) << 3; + } + 'u' => { + old_bits &= !(0o7 << 6); + old_bits |= (!new_bits & 0o7) << 6; + } + 'a' => { + let denied = !new_bits & 0o7; + old_bits = denied | (denied << 3) | (denied << 6); + } + _ => { + return Err(ShErr::at( + ShErrKind::ParseErr, + span.clone(), + format!("invalid umask 'who' character: {}", ch.fg(next_color())), + )); + } + } + } - umask(Mode::from_bits_truncate(old_bits)); - } else if let Some((who,bits)) = part.split_once('+') { - let mut new_bits = 0; - if bits.contains('r') { - new_bits |= 4; - } - if bits.contains('w') { - new_bits |= 2; - } - if bits.contains('x') { - new_bits |= 1; - } + umask(Mode::from_bits_truncate(old_bits)); + } else if let Some((who, bits)) = part.split_once('+') { + let mut new_bits = 0; + if bits.contains('r') { + new_bits |= 4; + } + if bits.contains('w') { + new_bits |= 2; + } + if bits.contains('x') { + new_bits |= 1; + } - for ch in who.chars() { - match ch { - 'o' => { - old_bits &= !(new_bits & 0o7); - } - 'g' => { - old_bits &= !((new_bits & 0o7) << 3); - } - 'u' => { - old_bits &= !((new_bits & 0o7) << 6); - } - 'a' => { - let mask = new_bits & 0o7; - old_bits &= !(mask | (mask << 3) | (mask << 6)); - } - _ => { - return Err(ShErr::at( - ShErrKind::ParseErr, - span.clone(), - format!("invalid umask 'who' character: {}", ch.fg(next_color())), - )); - } - } - } + for ch in who.chars() { + match ch { + 'o' => { + old_bits &= !(new_bits & 0o7); + } + 'g' => { + old_bits &= !((new_bits & 0o7) << 3); + } + 'u' => { + old_bits &= !((new_bits & 0o7) << 6); + } + 'a' => { + let mask = new_bits & 0o7; + old_bits &= !(mask | (mask << 3) | (mask << 6)); + } + _ => { + return Err(ShErr::at( + ShErrKind::ParseErr, + span.clone(), + format!("invalid umask 'who' character: {}", ch.fg(next_color())), + )); + } + } + } - umask(Mode::from_bits_truncate(old_bits)); - } else if let Some((who,bits)) = part.split_once('-') { - let mut new_bits = 0; - if bits.contains('r') { - new_bits |= 4; - } - if bits.contains('w') { - new_bits |= 2; - } - if bits.contains('x') { - new_bits |= 1; - } + umask(Mode::from_bits_truncate(old_bits)); + } else if let Some((who, bits)) = part.split_once('-') { + let mut new_bits = 0; + if bits.contains('r') { + new_bits |= 4; + } + if bits.contains('w') { + new_bits |= 2; + } + if bits.contains('x') { + new_bits |= 1; + } - for ch in who.chars() { - match ch { - 'o' => { - old_bits |= new_bits & 0o7; - } - 'g' => { - old_bits |= (new_bits << 3) & (0o7 << 3); - } - 'u' => { - old_bits |= (new_bits << 6) & (0o7 << 6); - } - 'a' => { - old_bits |= (new_bits | (new_bits << 3) | (new_bits << 6)) & 0o777; - } - _ => { - return Err(ShErr::at( - ShErrKind::ParseErr, - span.clone(), - format!("invalid umask 'who' character: {}", ch.fg(next_color())), - )); - } - } - } + for ch in who.chars() { + match ch { + 'o' => { + old_bits |= new_bits & 0o7; + } + 'g' => { + old_bits |= (new_bits << 3) & (0o7 << 3); + } + 'u' => { + old_bits |= (new_bits << 6) & (0o7 << 6); + } + 'a' => { + old_bits |= (new_bits | (new_bits << 3) | (new_bits << 6)) & 0o777; + } + _ => { + return Err(ShErr::at( + ShErrKind::ParseErr, + span.clone(), + format!("invalid umask 'who' character: {}", ch.fg(next_color())), + )); + } + } + } - umask(Mode::from_bits_truncate(old_bits)); - } else { - return Err(ShErr::at( - ShErrKind::ParseErr, - span.clone(), - format!("invalid symbolic umask part: {}", part.fg(next_color())), - )); - } - } - } + umask(Mode::from_bits_truncate(old_bits)); + } else { + return Err(ShErr::at( + ShErrKind::ParseErr, + span.clone(), + 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 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'); - } - } + let msg = [u_str, g_str, o_str].join(","); + let stdout = borrow_fd(STDOUT_FILENO); + write(stdout, msg.as_bytes())?; + write(stdout, b"\n")?; + } else { + let raw = format!("{:04o}\n", old_bits); - let msg = [u_str,g_str,o_str].join(","); - let stdout = borrow_fd(STDOUT_FILENO); - write(stdout, msg.as_bytes())?; - write(stdout, b"\n")?; - } else { - let raw = format!("{:04o}\n", old_bits); + let stdout = borrow_fd(STDOUT_FILENO); + write(stdout, raw.as_bytes())?; + } - let stdout = borrow_fd(STDOUT_FILENO); - write(stdout, raw.as_bytes())?; - } - - state::set_status(0); - Ok(()) + state::set_status(0); + Ok(()) } #[cfg(test)] @@ -423,7 +472,8 @@ mod tests { let opts = get_ulimit_opts(&[ Opt::ShortWithArg('n', "256".into()), Opt::ShortWithArg('c', "0".into()), - ]).unwrap(); + ]) + .unwrap(); assert_eq!(opts.fds, Some(256)); assert_eq!(opts.core, Some(0)); assert!(opts.procs.is_none()); diff --git a/src/builtin/source.rs b/src/builtin/source.rs index 6459e7a..098dc10 100644 --- a/src/builtin/source.rs +++ b/src/builtin/source.rs @@ -44,128 +44,128 @@ pub fn source(node: Node) -> ShResult<()> { #[cfg(test)] 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::testutil::{TestGuard, test_input}; + use crate::state::{self, read_logic, read_vars}; + use crate::testutil::{TestGuard, test_input}; + use tempfile::{NamedTempFile, TempDir}; - #[test] - fn source_simple() { - let _g = TestGuard::new(); - let mut file = NamedTempFile::new().unwrap(); - let path = file.path().display().to_string(); - file.write_all(b"some_var=some_val").unwrap(); + #[test] + fn source_simple() { + let _g = TestGuard::new(); + let mut file = NamedTempFile::new().unwrap(); + let path = file.path().display().to_string(); + file.write_all(b"some_var=some_val").unwrap(); - test_input(format!("source {path}")).unwrap(); - let var = read_vars(|v| v.get_var("some_var")); - assert_eq!(var, "some_val".to_string()); - } + test_input(format!("source {path}")).unwrap(); + let var = read_vars(|v| v.get_var("some_var")); + assert_eq!(var, "some_val".to_string()); + } - #[test] - fn source_multiple_commands() { - let _g = TestGuard::new(); - let mut file = NamedTempFile::new().unwrap(); - let path = file.path().display().to_string(); - file.write_all(b"x=1\ny=2\nz=3").unwrap(); + #[test] + fn source_multiple_commands() { + let _g = TestGuard::new(); + let mut file = NamedTempFile::new().unwrap(); + let path = file.path().display().to_string(); + file.write_all(b"x=1\ny=2\nz=3").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("y")), "2"); - assert_eq!(read_vars(|v| v.get_var("z")), "3"); - } + test_input(format!("source {path}")).unwrap(); + 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("z")), "3"); + } - #[test] - fn source_defines_function() { - let _g = TestGuard::new(); - let mut file = NamedTempFile::new().unwrap(); - let path = file.path().display().to_string(); - file.write_all(b"greet() { echo hi; }").unwrap(); + #[test] + fn source_defines_function() { + let _g = TestGuard::new(); + let mut file = NamedTempFile::new().unwrap(); + let path = file.path().display().to_string(); + file.write_all(b"greet() { echo hi; }").unwrap(); - test_input(format!("source {path}")).unwrap(); - let func = read_logic(|l| l.get_func("greet")); - assert!(func.is_some()); - } + test_input(format!("source {path}")).unwrap(); + let func = read_logic(|l| l.get_func("greet")); + assert!(func.is_some()); + } - #[test] - fn source_defines_alias() { - let _g = TestGuard::new(); - let mut file = NamedTempFile::new().unwrap(); - let path = file.path().display().to_string(); - file.write_all(b"alias ll='ls -la'").unwrap(); + #[test] + fn source_defines_alias() { + let _g = TestGuard::new(); + let mut file = NamedTempFile::new().unwrap(); + let path = file.path().display().to_string(); + file.write_all(b"alias ll='ls -la'").unwrap(); - test_input(format!("source {path}")).unwrap(); - let alias = read_logic(|l| l.get_alias("ll")); - assert!(alias.is_some()); - } + test_input(format!("source {path}")).unwrap(); + let alias = read_logic(|l| l.get_alias("ll")); + assert!(alias.is_some()); + } - #[test] - fn source_output_captured() { - let guard = TestGuard::new(); - let mut file = NamedTempFile::new().unwrap(); - let path = file.path().display().to_string(); - file.write_all(b"echo sourced").unwrap(); + #[test] + fn source_output_captured() { + let guard = TestGuard::new(); + let mut file = NamedTempFile::new().unwrap(); + let path = file.path().display().to_string(); + file.write_all(b"echo sourced").unwrap(); - test_input(format!("source {path}")).unwrap(); - let out = guard.read_output(); - assert!(out.contains("sourced")); - } + test_input(format!("source {path}")).unwrap(); + let out = guard.read_output(); + assert!(out.contains("sourced")); + } - #[test] - fn source_multiple_files() { - let _g = TestGuard::new(); - let mut file1 = NamedTempFile::new().unwrap(); - let mut file2 = NamedTempFile::new().unwrap(); - let path1 = file1.path().display().to_string(); - let path2 = file2.path().display().to_string(); - file1.write_all(b"a=from_file1").unwrap(); - file2.write_all(b"b=from_file2").unwrap(); + #[test] + fn source_multiple_files() { + let _g = TestGuard::new(); + let mut file1 = NamedTempFile::new().unwrap(); + let mut file2 = NamedTempFile::new().unwrap(); + let path1 = file1.path().display().to_string(); + let path2 = file2.path().display().to_string(); + file1.write_all(b"a=from_file1").unwrap(); + file2.write_all(b"b=from_file2").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("b")), "from_file2"); - } + 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("b")), "from_file2"); + } - // ===================== Dot syntax ===================== + // ===================== Dot syntax ===================== - #[test] - fn source_dot_syntax() { - let _g = TestGuard::new(); - let mut file = NamedTempFile::new().unwrap(); - let path = file.path().display().to_string(); - file.write_all(b"dot_var=dot_val").unwrap(); + #[test] + fn source_dot_syntax() { + let _g = TestGuard::new(); + let mut file = NamedTempFile::new().unwrap(); + let path = file.path().display().to_string(); + file.write_all(b"dot_var=dot_val").unwrap(); - test_input(format!(". {path}")).unwrap(); - assert_eq!(read_vars(|v| v.get_var("dot_var")), "dot_val"); - } + test_input(format!(". {path}")).unwrap(); + assert_eq!(read_vars(|v| v.get_var("dot_var")), "dot_val"); + } - // ===================== Error cases ===================== + // ===================== Error cases ===================== - #[test] - fn source_nonexistent_file() { - let _g = TestGuard::new(); - let result = test_input("source /tmp/__no_such_file_xyz__"); - assert!(result.is_err()); - } + #[test] + fn source_nonexistent_file() { + let _g = TestGuard::new(); + let result = test_input("source /tmp/__no_such_file_xyz__"); + assert!(result.is_err()); + } - #[test] - fn source_directory_fails() { - let _g = TestGuard::new(); - let dir = TempDir::new().unwrap(); - let result = test_input(format!("source {}", dir.path().display())); - assert!(result.is_err()); - } + #[test] + fn source_directory_fails() { + let _g = TestGuard::new(); + let dir = TempDir::new().unwrap(); + let result = test_input(format!("source {}", dir.path().display())); + assert!(result.is_err()); + } - // ===================== Status ===================== + // ===================== Status ===================== - #[test] - fn source_status_zero() { - let _g = TestGuard::new(); - let mut file = NamedTempFile::new().unwrap(); - let path = file.path().display().to_string(); - file.write_all(b"true").unwrap(); + #[test] + fn source_status_zero() { + let _g = TestGuard::new(); + let mut file = NamedTempFile::new().unwrap(); + let path = file.path().display().to_string(); + file.write_all(b"true").unwrap(); - test_input(format!("source {path}")).unwrap(); - assert_eq!(state::get_status(), 0); - } + test_input(format!("source {path}")).unwrap(); + assert_eq!(state::get_status(), 0); + } } diff --git a/src/builtin/test.rs b/src/builtin/test.rs index 0b3f16c..e3806eb 100644 --- a/src/builtin/test.rs +++ b/src/builtin/test.rs @@ -94,7 +94,10 @@ impl FromStr for TestOp { "-ge" => Ok(Self::IntGe), "-le" => Ok(Self::IntLe), _ if TEST_UNARY_OPS.contains(&s) => Ok(Self::Unary(s.parse::()?)), - _ => 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 { }; let mut last_result = false; let mut conjunct_op: Option; - log::trace!("test cases: {:#?}", cases); + log::trace!("test cases: {:#?}", cases); for case in cases { let result = match case { @@ -305,10 +308,10 @@ pub fn double_bracket_test(node: Node) -> ShResult { #[cfg(test)] mod tests { - use std::fs; - use tempfile::{TempDir, NamedTempFile}; use crate::state; use crate::testutil::{TestGuard, test_input}; + use std::fs; + use tempfile::{NamedTempFile, TempDir}; // ===================== Unary: file tests ===================== @@ -590,9 +593,10 @@ mod tests { fn parse_unary_ops() { use super::UnaryOp; use std::str::FromStr; - for op in ["-e", "-d", "-f", "-h", "-L", "-r", "-w", "-x", "-s", - "-p", "-S", "-b", "-c", "-k", "-O", "-G", "-N", "-u", - "-g", "-t", "-n", "-z"] { + for op in [ + "-e", "-d", "-f", "-h", "-L", "-r", "-w", "-x", "-s", "-p", "-S", "-b", "-c", "-k", "-O", + "-G", "-N", "-u", "-g", "-t", "-n", "-z", + ] { assert!(UnaryOp::from_str(op).is_ok(), "failed to parse {op}"); } } diff --git a/src/builtin/trap.rs b/src/builtin/trap.rs index 0951b76..c6a37fb 100644 --- a/src/builtin/trap.rs +++ b/src/builtin/trap.rs @@ -171,10 +171,10 @@ pub fn trap(node: Node) -> ShResult<()> { #[cfg(test)] mod tests { use super::TrapTarget; - use std::str::FromStr; - use nix::sys::signal::Signal; use crate::state::{self, read_logic}; use crate::testutil::{TestGuard, test_input}; + use nix::sys::signal::Signal; + use std::str::FromStr; // ===================== Pure: TrapTarget parsing ===================== @@ -231,7 +231,9 @@ mod tests { #[test] 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(); assert_eq!(target.to_string(), *name); } diff --git a/src/builtin/varcmds.rs b/src/builtin/varcmds.rs index 9815007..1fde359 100644 --- a/src/builtin/varcmds.rs +++ b/src/builtin/varcmds.rs @@ -245,8 +245,16 @@ mod tests { 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("b")), "2"); - assert!(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)); + assert!( + 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] @@ -385,7 +393,11 @@ mod tests { let _g = TestGuard::new(); test_input("local mylocal").unwrap(); 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] diff --git a/src/expand.rs b/src/expand.rs index cf80208..da3a9fe 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -639,8 +639,10 @@ pub fn expand_glob(raw: &str) -> ShResult { { let entry = 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 escaped = escape_str(entry_raw, true); + let entry_raw = entry + .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) } @@ -1327,57 +1329,38 @@ pub fn unescape_str(raw: &str) -> String { /// Opposite of unescape_str - escapes a string to be executed as literal text /// Used for completion results, and glob filename matches. pub fn escape_str(raw: &str, use_marker: bool) -> String { - let mut result = String::new(); - let mut chars = raw.chars(); + let mut result = String::new(); + let mut chars = raw.chars(); - while let Some(ch) = chars.next() { - 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; - } - _ => { - result.push(ch); - continue; - } - } - } + while let Some(ch) = chars.next() { + 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; + } + _ => { + result.push(ch); + continue; + } + } + } - result + result } pub fn unescape_math(raw: &str) -> String { @@ -1657,7 +1640,8 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { ParamExp::RemShortestPrefix(prefix) => { let value = vars.get_var(&var_name); 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(); for i in 0..=value.len() { let sliced = &value[..i]; @@ -1670,7 +1654,8 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { ParamExp::RemLongestPrefix(prefix) => { let value = vars.get_var(&var_name); 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(); for i in (0..=value.len()).rev() { let sliced = &value[..i]; @@ -1683,7 +1668,8 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { ParamExp::RemShortestSuffix(suffix) => { let value = vars.get_var(&var_name); 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(); for i in (0..=value.len()).rev() { let sliced = &value[i..]; @@ -1696,8 +1682,9 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { ParamExp::RemLongestSuffix(suffix) => { let value = vars.get_var(&var_name); let unescaped = unescape_str(&suffix); - let expanded_suffix = - strip_escape_markers(&expand_raw(&mut unescaped.chars().peekable()).unwrap_or(suffix.clone())); + let expanded_suffix = strip_escape_markers( + &expand_raw(&mut unescaped.chars().peekable()).unwrap_or(suffix.clone()), + ); let pattern = Pattern::new(&expanded_suffix).unwrap(); for i in 0..=value.len() { let sliced = &value[i..]; @@ -1711,8 +1698,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { let value = vars.get_var(&var_name); let search = unescape_str(&search); let replace = unescape_str(&replace); - let expanded_search = 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 expanded_search = + 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 if let Some(mat) = regex.find(&value) { @@ -1728,8 +1717,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { let value = vars.get_var(&var_name); let search = unescape_str(&search); let replace = unescape_str(&replace); - let expanded_search = 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 expanded_search = + 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 mut result = String::new(); let mut last_match_end = 0; @@ -1748,8 +1739,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { let value = vars.get_var(&var_name); let search = unescape_str(&search); let replace = unescape_str(&replace); - let expanded_search = 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 expanded_search = + 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(); for i in (0..=value.len()).rev() { let sliced = &value[..i]; @@ -1763,8 +1756,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { let value = vars.get_var(&var_name); let search = unescape_str(&search); let replace = unescape_str(&replace); - let expanded_search = 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 expanded_search = + 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(); for i in (0..=value.len()).rev() { let sliced = &value[i..]; @@ -2455,11 +2450,11 @@ pub fn parse_key_alias(alias: &str) -> Option { #[cfg(test)] mod tests { 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::readline::keys::{KeyCode, KeyEvent, ModKeys}; + use crate::state::{ArrIndex, VarFlags, VarKind, read_vars, write_vars}; use crate::testutil::{TestGuard, test_input}; + use std::time::Duration; // ===================== has_braces ===================== @@ -2599,10 +2594,7 @@ mod tests { #[test] fn braces_simple_list() { - assert_eq!( - expand_braces_full("{a,b,c}").unwrap(), - vec!["a", "b", "c"] - ); + assert_eq!(expand_braces_full("{a,b,c}").unwrap(), vec!["a", "b", "c"]); } #[test] @@ -2688,11 +2680,23 @@ mod tests { assert_eq!(result, vec!["prepost", "preapost"]); } - #[test] - fn braces_cursed() { - 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", ]) - } + #[test] + fn braces_cursed() { + 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", + ] + ) + } // ===================== Arithmetic ===================== @@ -3164,10 +3168,22 @@ mod tests { #[test] fn key_alias_arrows() { - assert_eq!(parse_key_alias("UP").unwrap(), KeyEvent(KeyCode::Up, 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)); + assert_eq!( + parse_key_alias("UP").unwrap(), + KeyEvent(KeyCode::Up, 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] @@ -3179,7 +3195,13 @@ mod tests { #[test] fn key_alias_ctrl_shift_alt_modifier() { 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] @@ -3371,7 +3393,14 @@ mod tests { #[test] fn param_remove_shortest_prefix() { 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(); assert_eq!(result, "usr/local/bin"); @@ -3380,7 +3409,14 @@ mod tests { #[test] fn param_remove_longest_prefix() { 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(); assert_eq!(result, "bin"); @@ -3494,7 +3530,9 @@ mod tests { fn word_split_default_ifs() { 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(); assert_eq!(words, vec!["hello", "world", "foo"]); } @@ -3502,9 +3540,13 @@ mod tests { #[test] fn word_split_custom_ifs() { 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(); assert_eq!(words, vec!["a", "b", "c"]); } @@ -3512,9 +3554,13 @@ mod tests { #[test] fn word_split_empty_ifs() { 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(); assert_eq!(words, vec!["hello world"]); } @@ -3554,7 +3600,9 @@ mod tests { #[test] fn word_split_escaped_custom_ifs() { 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 mut exp = Expander { raw }; @@ -3610,8 +3658,13 @@ mod tests { fn array_index_first() { let _guard = TestGuard::new(); write_vars(|v| { - v.set_var("arr", VarKind::arr_from_vec(vec!["a".into(), "b".into(), "c".into()]), VarFlags::NONE) - }).unwrap(); + v.set_var( + "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(); assert_eq!(val, "a"); @@ -3621,8 +3674,13 @@ mod tests { fn array_index_second() { let _guard = TestGuard::new(); write_vars(|v| { - v.set_var("arr", VarKind::arr_from_vec(vec!["x".into(), "y".into(), "z".into()]), VarFlags::NONE) - }).unwrap(); + v.set_var( + "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(); assert_eq!(val, "y"); @@ -3632,8 +3690,13 @@ mod tests { fn array_all_elems() { let _guard = TestGuard::new(); write_vars(|v| { - v.set_var("arr", VarKind::arr_from_vec(vec!["a".into(), "b".into(), "c".into()]), VarFlags::NONE) - }).unwrap(); + v.set_var( + "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(); assert_eq!(elems, vec!["a", "b", "c"]); @@ -3643,8 +3706,13 @@ mod tests { fn array_elem_count() { let _guard = TestGuard::new(); write_vars(|v| { - v.set_var("arr", VarKind::arr_from_vec(vec!["a".into(), "b".into(), "c".into()]), VarFlags::NONE) - }).unwrap(); + v.set_var( + "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(); assert_eq!(elems.len(), 3); @@ -3657,7 +3725,9 @@ mod tests { let _guard = TestGuard::new(); let dummy_span = Span::default(); 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()); @@ -3670,7 +3740,9 @@ mod tests { let _guard = TestGuard::new(); let dummy_span = Span::default(); 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()); @@ -3682,26 +3754,47 @@ mod tests { // ===================== Direct Input Tests (TestGuard) ===================== - #[test] - fn index_simple() { - let guard = TestGuard::new(); - write_vars(|v| v.set_var("arr", VarKind::Arr(VecDeque::from(["foo".into(), "bar".into(), "biz".into()])), VarFlags::NONE)).unwrap(); + #[test] + fn index_simple() { + let guard = TestGuard::new(); + 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(); - assert_eq!(out, "foo bar biz\n"); - } + let out = guard.read_output(); + assert_eq!(out, "foo bar biz\n"); + } - #[test] - fn index_cursed() { - 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("i", VarKind::Arr(VecDeque::from(["0".into(), "1".into(), "2".into()])), VarFlags::NONE)).unwrap(); + #[test] + fn index_cursed() { + 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( + "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(); - assert_eq!(out, "bar\n"); - } + let out = guard.read_output(); + assert_eq!(out, "bar\n"); + } } diff --git a/src/getopt.rs b/src/getopt.rs index 47d7064..aa56569 100644 --- a/src/getopt.rs +++ b/src/getopt.rs @@ -3,7 +3,11 @@ use std::sync::Arc; use ariadne::Fmt; 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]>; @@ -69,20 +73,24 @@ pub fn get_opts(words: Vec) -> (Vec, Vec) { } pub fn get_opts_from_tokens_strict( - tokens: Vec, - opt_specs: &[OptSpec], + tokens: Vec, + opt_specs: &[OptSpec], ) -> ShResult<(Vec, Vec)> { - sort_tks(tokens, opt_specs, true) + sort_tks(tokens, opt_specs, true) } pub fn get_opts_from_tokens( tokens: Vec, opt_specs: &[OptSpec], ) -> ShResult<(Vec, Vec)> { - sort_tks(tokens, opt_specs, false) + sort_tks(tokens, opt_specs, false) } -pub fn sort_tks(tokens: Vec, opt_specs: &[OptSpec], strict: bool) -> ShResult<(Vec, Vec)> { +pub fn sort_tks( + tokens: Vec, + opt_specs: &[OptSpec], + strict: bool, +) -> ShResult<(Vec, Vec)> { let mut tokens_iter = tokens .into_iter() .map(|t| t.expand()) @@ -125,14 +133,14 @@ pub fn sort_tks(tokens: Vec, opt_specs: &[OptSpec], strict: bool) -> ShResul } } if !pushed { - if strict { - return Err(ShErr::simple( - ShErrKind::ParseErr, - format!("Unknown option: {}", opt.to_string().fg(next_color())), - )); - } else { - non_opts.push(token.clone()); - } + if strict { + return Err(ShErr::simple( + ShErrKind::ParseErr, + format!("Unknown option: {}", opt.to_string().fg(next_color())), + )); + } else { + non_opts.push(token.clone()); + } } } } @@ -140,12 +148,11 @@ pub fn sort_tks(tokens: Vec, opt_specs: &[OptSpec], strict: bool) -> ShResul Ok((non_opts, opts)) } - #[cfg(test)] mod tests { use crate::parse::lex::{LexFlags, LexStream}; -use super::*; + use super::*; #[test] fn parse_short_single() { @@ -156,7 +163,10 @@ use super::*; #[test] fn parse_short_combined() { 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] @@ -173,7 +183,12 @@ use super::*; #[test] 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); assert_eq!(non_opts, vec!["file.txt", "arg"]); assert_eq!(opts, vec![Opt::Short('v'), Opt::Long("help".into())]); @@ -191,7 +206,10 @@ use super::*; fn get_opts_combined_short() { let words = vec!["-abc".into(), "file".into()]; 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"]); } @@ -215,128 +233,175 @@ use super::*; assert_eq!(Opt::Short('v').to_string(), "-v"); assert_eq!(Opt::Long("help".into()).to_string(), "--help"); 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 { - LexStream::new(Arc::new(input.to_string()), LexFlags::empty()) - .collect::>>() - .unwrap() - } + fn lex(input: &str) -> Vec { + LexStream::new(Arc::new(input.to_string()), LexFlags::empty()) + .collect::>>() + .unwrap() + } - #[test] - fn get_opts_from_tks() { - let tokens = lex("file.txt --help -v arg"); + #[test] + fn get_opts_from_tks() { + let tokens = lex("file.txt --help -v arg"); - let opt_spec = vec![ - OptSpec { opt: Opt::Short('v'), takes_arg: false }, - OptSpec { opt: Opt::Long("help".into()), takes_arg: false }, - ]; + let opt_spec = vec![ + OptSpec { + 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(); - 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 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()))); - 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")); - } + 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")); + } - #[test] - fn tks_short_with_arg() { - let tokens = lex("-o output.txt file.txt"); + #[test] + fn tks_short_with_arg() { + let tokens = lex("-o output.txt file.txt"); - let opt_spec = vec![ - OptSpec { opt: Opt::Short('o'), takes_arg: true }, - ]; + let opt_spec = vec![OptSpec { + 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())]); - let non_opts: Vec = non_opts.into_iter().map(|s| s.to_string()).collect(); - assert!(non_opts.contains(&"file.txt".to_string())); - } + assert_eq!(opts, vec![Opt::ShortWithArg('o', "output.txt".into())]); + let non_opts: Vec = non_opts.into_iter().map(|s| s.to_string()).collect(); + assert!(non_opts.contains(&"file.txt".to_string())); + } - #[test] - fn tks_long_with_arg() { - let tokens = lex("--output result.txt input.txt"); + #[test] + fn tks_long_with_arg() { + let tokens = lex("--output result.txt input.txt"); - let opt_spec = vec![ - OptSpec { opt: Opt::Long("output".into()), takes_arg: true }, - ]; + let opt_spec = vec![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![Opt::LongWithArg("output".into(), "result.txt".into())]); - let non_opts: Vec = non_opts.into_iter().map(|s| s.to_string()).collect(); - assert!(non_opts.contains(&"input.txt".to_string())); - } + assert_eq!( + opts, + vec![Opt::LongWithArg("output".into(), "result.txt".into())] + ); + let non_opts: Vec = non_opts.into_iter().map(|s| s.to_string()).collect(); + assert!(non_opts.contains(&"input.txt".to_string())); + } - #[test] - fn tks_double_dash_stops() { - let tokens = lex("-v -- -a --foo"); + #[test] + fn tks_double_dash_stops() { + let tokens = lex("-v -- -a --foo"); - let opt_spec = vec![ - OptSpec { opt: Opt::Short('v'), takes_arg: false }, - OptSpec { opt: Opt::Short('a'), takes_arg: false }, - ]; + let opt_spec = vec![ + OptSpec { + 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')]); - let non_opts: Vec = non_opts.into_iter().map(|s| s.to_string()).collect(); - assert!(non_opts.contains(&"-a".to_string())); - assert!(non_opts.contains(&"--foo".to_string())); - } + assert_eq!(opts, vec![Opt::Short('v')]); + let non_opts: Vec = non_opts.into_iter().map(|s| s.to_string()).collect(); + assert!(non_opts.contains(&"-a".to_string())); + assert!(non_opts.contains(&"--foo".to_string())); + } - #[test] - fn tks_combined_short_with_spec() { - let tokens = lex("-abc"); + #[test] + fn tks_combined_short_with_spec() { + let tokens = lex("-abc"); - let opt_spec = vec![ - OptSpec { opt: Opt::Short('a'), takes_arg: false }, - OptSpec { opt: Opt::Short('b'), takes_arg: false }, - OptSpec { opt: Opt::Short('c'), takes_arg: false }, - ]; + let opt_spec = vec![ + OptSpec { + opt: Opt::Short('a'), + 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] - fn tks_unknown_opt_becomes_non_opt() { - let tokens = lex("-v -x file"); + #[test] + fn tks_unknown_opt_becomes_non_opt() { + let tokens = lex("-v -x file"); - let opt_spec = vec![ - OptSpec { opt: Opt::Short('v'), takes_arg: false }, - ]; + let opt_spec = vec![OptSpec { + 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')]); - // -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_eq!(opts, vec![Opt::Short('v')]); + // -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") + ); + } - #[test] - fn tks_mixed_short_and_long_with_args() { - let tokens = lex("-n 5 --output file.txt input"); + #[test] + fn tks_mixed_short_and_long_with_args() { + let tokens = lex("-n 5 --output file.txt input"); - let opt_spec = vec![ - OptSpec { opt: Opt::Short('n'), takes_arg: true }, - OptSpec { opt: Opt::Long("output".into()), takes_arg: true }, - ]; + let opt_spec = vec![ + OptSpec { + 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![ - Opt::ShortWithArg('n', "5".into()), - Opt::LongWithArg("output".into(), "file.txt".into()), - ]); - let non_opts: Vec = non_opts.into_iter().map(|s| s.to_string()).collect(); - assert!(non_opts.contains(&"input".to_string())); - } + assert_eq!( + opts, + vec![ + Opt::ShortWithArg('n', "5".into()), + Opt::LongWithArg("output".into(), "file.txt".into()), + ] + ); + let non_opts: Vec = non_opts.into_iter().map(|s| s.to_string()).collect(); + assert!(non_opts.contains(&"input".to_string())); + } } diff --git a/src/libsh/error.rs b/src/libsh/error.rs index 3f78c5d..fa96fd7 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -1,10 +1,10 @@ use ariadne::{Color, Fmt}; use ariadne::{Report, ReportKind}; use rand::TryRng; -use yansi::Paint; use std::cell::RefCell; use std::collections::{HashMap, VecDeque}; use std::fmt::Display; +use yansi::Paint; use crate::procio::RedirGuard; use crate::{ @@ -150,7 +150,7 @@ impl Display for Note { writeln!(f, "{note}: {main}")?; } else { let bar_break = Fmt::fg("-", Color::Cyan); - let bar_break = bar_break.bold(); + let bar_break = bar_break.bold(); let indent = " ".repeat(self.depth); writeln!(f, " {indent}{bar_break} {main}")?; } diff --git a/src/main.rs b/src/main.rs index cbf7cf3..3ca003f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,7 +40,9 @@ use crate::procio::borrow_fd; use crate::readline::term::{LineWriter, RawModeGuard, raw_mode}; use crate::readline::{Prompt, ReadlineEvent, ShedVi}; 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 state::write_vars; @@ -116,14 +118,15 @@ fn main() -> ExitCode { return ExitCode::SUCCESS; } - // Increment SHLVL, or set to 1 if not present or invalid. - // This var represents how many nested shell instances we're in - if let Ok(var) = env::var("SHLVL") - && let Ok(lvl) = var.parse::() { - unsafe { env::set_var("SHLVL", (lvl + 1).to_string()) }; - } else { - unsafe { env::set_var("SHLVL", "1") }; - } + // Increment SHLVL, or set to 1 if not present or invalid. + // This var represents how many nested shell instances we're in + if let Ok(var) = env::var("SHLVL") + && let Ok(lvl) = var.parse::() + { + unsafe { env::set_var("SHLVL", (lvl + 1).to_string()) }; + } else { + unsafe { env::set_var("SHLVL", "1") }; + } if let Err(e) = if let Some(path) = args.script { run_script(path, args.script_args) @@ -131,8 +134,8 @@ fn main() -> ExitCode { exec_dash_c(cmd) } else { let res = shed_interactive(args); - write(borrow_fd(*TTY_FILENO), b"\x1b[?2004l").ok(); // disable bracketed paste mode on exit - res + write(borrow_fd(*TTY_FILENO), b"\x1b[?2004l").ok(); // disable bracketed paste mode on exit + res } { 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 loop { @@ -221,9 +224,9 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> { readline.reset_active_widget(false)?; } ShErrKind::CleanExit(code) => { - QUIT_CODE.store(*code, Ordering::SeqCst); - return Ok(()); - } + QUIT_CODE.store(*code, Ordering::SeqCst); + return Ok(()); + } _ => e.print_error(), } } @@ -235,7 +238,7 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> { // may have moved it during resize/rewrap readline.writer.update_t_cols(); readline.mark_dirty(); - } + } if JOB_DONE.swap(false, Ordering::SeqCst) { // update the prompt so any job count escape sequences update dynamically @@ -250,38 +253,38 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> { PollFlags::POLLIN, )]; - let mut exec_if_timeout = None; + let mut exec_if_timeout = None; let timeout = if readline.pending_keymap.is_empty() { - let screensaver_cmd = read_shopts(|o| o.prompt.screensaver_cmd.clone()); - let screensaver_idle_time = read_shopts(|o| o.prompt.screensaver_idle_time); - if screensaver_idle_time > 0 && !screensaver_cmd.is_empty() { - exec_if_timeout = Some(screensaver_cmd); - PollTimeout::from((screensaver_idle_time * 1000) as u16) - } else { - PollTimeout::MAX - } + let screensaver_cmd = read_shopts(|o| o.prompt.screensaver_cmd.clone()); + let screensaver_idle_time = read_shopts(|o| o.prompt.screensaver_idle_time); + if screensaver_idle_time > 0 && !screensaver_cmd.is_empty() { + exec_if_timeout = Some(screensaver_cmd); + PollTimeout::from((screensaver_idle_time * 1000) as u16) + } else { + PollTimeout::MAX + } } else { - PollTimeout::from(1000u16) + PollTimeout::from(1000u16) }; match poll(&mut fds, timeout) { - Ok(0) => { - // We timed out. - if let Some(cmd) = exec_if_timeout { - let prepared = ReadlineEvent::Line(cmd); - let saved_hist_opt = read_shopts(|o| o.core.auto_hist); - let _guard = scopeguard::guard(saved_hist_opt, |opt| { - write_shopts(|o| o.core.auto_hist = opt); - }); - write_shopts(|o| o.core.auto_hist = false); // don't save screensaver command to history + Ok(0) => { + // We timed out. + if let Some(cmd) = exec_if_timeout { + let prepared = ReadlineEvent::Line(cmd); + let saved_hist_opt = read_shopts(|o| o.core.auto_hist); + let _guard = scopeguard::guard(saved_hist_opt, |opt| { + write_shopts(|o| o.core.auto_hist = opt); + }); + write_shopts(|o| o.core.auto_hist = false); // don't save screensaver command to history - match handle_readline_event(&mut readline, Ok(prepared))? { - true => return Ok(()), - false => continue - } - } - } + match handle_readline_event(&mut readline, Ok(prepared))? { + true => return Ok(()), + false => continue, + } + } + } Err(Errno::EINTR) => { // Interrupted by signal, loop back to handle it continue; diff --git a/src/parse/execute.rs b/src/parse/execute.rs index 6904d36..60a8116 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -8,7 +8,28 @@ use ariadne::Fmt; use crate::{ builtin::{ - alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, autocmd::autocmd, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, getopts::getopts, intro, jobctl::{self, JobBehavior, continue_job, disown, jobs}, keymap, map, pwd::pwd, read::{self, read_builtin}, resource::{ulimit, umask_builtin}, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset} + alias::{alias, unalias}, + arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, + autocmd::autocmd, + cd::cd, + complete::{compgen_builtin, complete_builtin}, + dirstack::{dirs, popd, pushd}, + echo::echo, + eval, exec, + flowctl::flowctl, + getopts::getopts, + intro, + jobctl::{self, JobBehavior, continue_job, disown, jobs}, + keymap, map, + pwd::pwd, + read::{self, read_builtin}, + resource::{ulimit, umask_builtin}, + shift::shift, + shopt::shopt, + source::source, + test::double_bracket_test, + trap::{TrapTarget, trap}, + varcmds::{export, local, readonly, unset}, }, expand::{expand_aliases, expand_case_pattern, glob_to_regex}, jobs::{ChildProc, JobStack, attach_tty, dispatch_job}, @@ -136,13 +157,18 @@ pub fn exec_dash_c(input: String) -> ShResult<()> { if nodes.len() == 1 { let is_single_cmd = match &nodes[0].class { 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 } => { - elements.len() == 1 && match &elements[0].cmd.class { - NdRule::Pipeline { cmds } => cmds.len() == 1 && matches!(cmds[0].class, NdRule::Command { .. }), - NdRule::Command { .. } => true, - _ => false, - } + elements.len() == 1 + && match &elements[0].cmd.class { + NdRule::Pipeline { cmds } => { + cmds.len() == 1 && matches!(cmds[0].class, NdRule::Command { .. }) + } + NdRule::Command { .. } => true, + _ => false, + } } _ => false, }; @@ -151,8 +177,12 @@ pub fn exec_dash_c(input: String) -> ShResult<()> { let mut node = nodes.remove(0); loop { match node.class { - NdRule::Conjunction { mut elements } => { node = *elements.remove(0).cmd; } - NdRule::Pipeline { mut cmds } => { node = cmds.remove(0); } + NdRule::Conjunction { mut elements } => { + node = *elements.remove(0).cmd; + } + NdRule::Pipeline { mut cmds } => { + node = cmds.remove(0); + } NdRule::Command { .. } => break, _ => break, } @@ -250,7 +280,7 @@ impl Dispatcher { NdRule::CaseNode { .. } => self.exec_case(node)?, NdRule::BraceGrp { .. } => self.exec_brc_grp(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::Test { .. } => self.exec_test(node)?, _ => unreachable!(), @@ -258,8 +288,14 @@ impl Dispatcher { Ok(()) } pub fn dispatch_cmd(&mut self, node: Node) -> ShResult<()> { - let (line, _) = node.get_span().clone().line_and_col(); - write_vars(|v| v.set_var("LINENO", VarKind::Str((line + 1).to_string()), VarFlags::NONE))?; + let (line, _) = node.get_span().clone().line_and_col(); + write_vars(|v| { + v.set_var( + "LINENO", + VarKind::Str((line + 1).to_string()), + VarFlags::NONE, + ) + })?; let Some(cmd) = node.get_command() else { return self.exec_cmd(node); // Argv is empty, probably an assignment @@ -288,16 +324,16 @@ impl Dispatcher { self.exec_cmd(node) } } - pub fn exec_negated(&mut self, node: Node) -> ShResult<()> { - let NdRule::Negate { cmd } = node.class else { - unreachable!() - }; - self.dispatch_node(*cmd)?; - let status = state::get_status(); - state::set_status(if status == 0 { 1 } else { 0 }); + pub fn exec_negated(&mut self, node: Node) -> ShResult<()> { + let NdRule::Negate { cmd } = node.class else { + unreachable!() + }; + self.dispatch_node(*cmd)?; + let status = state::get_status(); + state::set_status(if status == 0 { 1 } else { 0 }); - Ok(()) - } + Ok(()) + } pub fn exec_conjunction(&mut self, conjunction: Node) -> ShResult<()> { let NdRule::Conjunction { elements } = conjunction.class else { unreachable!() @@ -364,7 +400,7 @@ impl Dispatcher { Ok(()) } 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 { unreachable!() }; @@ -769,11 +805,14 @@ impl Dispatcher { self.fg_job = !is_bg && self.interactive; let mut cmd = cmds.into_iter().next().unwrap(); if is_bg && !matches!(cmd.class, NdRule::Command { .. }) { - self.run_fork(&cmd.get_command().map(|t| t.to_string()).unwrap_or_default(), |s| { - if let Err(e) = s.dispatch_node(cmd) { - e.print_error(); - } - })?; + self.run_fork( + &cmd.get_command().map(|t| t.to_string()).unwrap_or_default(), + |s| { + if let Err(e) = s.dispatch_node(cmd) { + e.print_error(); + } + }, + )?; } else { self.dispatch_node(cmd)?; } @@ -972,8 +1011,8 @@ impl Dispatcher { "keymap" => keymap::keymap(cmd), "read_key" => read::read_key(cmd), "autocmd" => autocmd(cmd), - "ulimit" => ulimit(cmd), - "umask" => umask_builtin(cmd), + "ulimit" => ulimit(cmd), + "umask" => umask_builtin(cmd), "true" | ":" => { state::set_status(0); Ok(()) @@ -1331,94 +1370,94 @@ mod tests { assert_eq!(state::get_status(), 0); } - // ===================== other stuff ===================== + // ===================== other stuff ===================== - #[test] - fn for_loop_var_zip() { - let g = TestGuard::new(); - test_input("for a b in 1 2 3 4 5 6; do echo $a $b; done").unwrap(); - let out = g.read_output(); - assert_eq!(out, "1 2\n3 4\n5 6\n"); - } + #[test] + fn for_loop_var_zip() { + let g = TestGuard::new(); + test_input("for a b in 1 2 3 4 5 6; do echo $a $b; done").unwrap(); + let out = g.read_output(); + assert_eq!(out, "1 2\n3 4\n5 6\n"); + } - #[test] - fn for_loop_unsets_zipped() { - 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(); - let out = g.read_output(); - assert_eq!(out, "1 2 3 4\n5 6\n"); - } + #[test] + fn for_loop_unsets_zipped() { + 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(); + let out = g.read_output(); + assert_eq!(out, "1 2 3 4\n5 6\n"); + } - // ===================== negation (!) status ===================== + // ===================== negation (!) status ===================== - #[test] - fn negate_true() { - let _g = TestGuard::new(); - test_input("! true").unwrap(); - assert_eq!(state::get_status(), 1); - } + #[test] + fn negate_true() { + let _g = TestGuard::new(); + test_input("! true").unwrap(); + assert_eq!(state::get_status(), 1); + } - #[test] - fn negate_false() { - let _g = TestGuard::new(); - test_input("! false").unwrap(); - assert_eq!(state::get_status(), 0); - } + #[test] + fn negate_false() { + let _g = TestGuard::new(); + test_input("! false").unwrap(); + assert_eq!(state::get_status(), 0); + } - #[test] - fn double_negate_true() { - let _g = TestGuard::new(); - test_input("! ! true").unwrap(); - assert_eq!(state::get_status(), 0); - } + #[test] + fn double_negate_true() { + let _g = TestGuard::new(); + test_input("! ! true").unwrap(); + assert_eq!(state::get_status(), 0); + } - #[test] - fn double_negate_false() { - let _g = TestGuard::new(); - test_input("! ! false").unwrap(); - assert_eq!(state::get_status(), 1); - } + #[test] + fn double_negate_false() { + let _g = TestGuard::new(); + test_input("! ! false").unwrap(); + assert_eq!(state::get_status(), 1); + } - #[test] - fn negate_pipeline_last_cmd() { - let _g = TestGuard::new(); - // pipeline status = last cmd (false) = 1, negated → 0 - test_input("! true | false").unwrap(); - assert_eq!(state::get_status(), 0); - } + #[test] + fn negate_pipeline_last_cmd() { + let _g = TestGuard::new(); + // pipeline status = last cmd (false) = 1, negated → 0 + test_input("! true | false").unwrap(); + assert_eq!(state::get_status(), 0); + } - #[test] - fn negate_pipeline_last_cmd_true() { - let _g = TestGuard::new(); - // pipeline status = last cmd (true) = 0, negated → 1 - test_input("! false | true").unwrap(); - assert_eq!(state::get_status(), 1); - } + #[test] + fn negate_pipeline_last_cmd_true() { + let _g = TestGuard::new(); + // pipeline status = last cmd (true) = 0, negated → 1 + test_input("! false | true").unwrap(); + assert_eq!(state::get_status(), 1); + } - #[test] - fn negate_in_conjunction() { - let _g = TestGuard::new(); - // ! binds to pipeline, not conjunction: (! (true && false)) && true - test_input("! (true && false) && true").unwrap(); - assert_eq!(state::get_status(), 0); - } + #[test] + fn negate_in_conjunction() { + let _g = TestGuard::new(); + // ! binds to pipeline, not conjunction: (! (true && false)) && true + test_input("! (true && false) && true").unwrap(); + assert_eq!(state::get_status(), 0); + } - #[test] - fn negate_in_if_condition() { - let g = TestGuard::new(); - test_input("if ! false; then echo yes; fi").unwrap(); - assert_eq!(state::get_status(), 0); - assert_eq!(g.read_output(), "yes\n"); - } + #[test] + fn negate_in_if_condition() { + let g = TestGuard::new(); + test_input("if ! false; then echo yes; fi").unwrap(); + assert_eq!(state::get_status(), 0); + assert_eq!(g.read_output(), "yes\n"); + } - #[test] - fn empty_var_in_test() { - 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 - test_input("[ -n \"$EMPTYVAR_PROBABLY_NOT_SET_TO_ANYTHING\" ]").unwrap(); - 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 - test_input("[ -n $EMPTYVAR_PROBABLY_NOT_SET_TO_ANYTHING ]").unwrap(); - assert_eq!(state::get_status(), 0); - } + #[test] + fn empty_var_in_test() { + 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 + test_input("[ -n \"$EMPTYVAR_PROBABLY_NOT_SET_TO_ANYTHING\" ]").unwrap(); + 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 + test_input("[ -n $EMPTYVAR_PROBABLY_NOT_SET_TO_ANYTHING ]").unwrap(); + assert_eq!(state::get_status(), 0); + } } diff --git a/src/parse/lex.rs b/src/parse/lex.rs index 46d7820..2d7a764 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -19,7 +19,7 @@ use crate::{ pub const KEYWORDS: [&str; 17] = [ "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"]; @@ -166,7 +166,7 @@ pub enum TkRule { ErrPipe, And, Or, - Bang, + Bang, Bg, Sep, Redir, @@ -883,14 +883,14 @@ impl Iterator for LexStream { return self.next(); } } - '!' if self.next_is_cmd() => { - self.cursor += 1; - let tk_type = TkRule::Bang; + '!' if self.next_is_cmd() => { + self.cursor += 1; + let tk_type = TkRule::Bang; - let mut tk = self.get_token((self.cursor - 1)..self.cursor, tk_type); - tk.flags |= TkFlags::KEYWORD; - tk - } + let mut tk = self.get_token((self.cursor - 1)..self.cursor, tk_type); + tk.flags |= TkFlags::KEYWORD; + tk + } '|' => { let ch_idx = self.cursor; self.cursor += 1; diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 49915a7..9904095 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -87,7 +87,7 @@ impl ParsedSrc { Err(error) => return Err(vec![error]), } } - log::trace!("Tokens: {:#?}", tokens); + log::trace!("Tokens: {:#?}", tokens); let mut errors = vec![]; let mut nodes = vec![]; @@ -241,9 +241,9 @@ impl Node { } => { body.walk_tree(f); } - NdRule::Negate { ref mut cmd } => { - cmd.walk_tree(f); - } + NdRule::Negate { ref mut cmd } => { + cmd.walk_tree(f); + } NdRule::Test { cases: _ } => (), } } @@ -634,9 +634,9 @@ pub enum NdRule { arr: Vec, body: Vec, }, - Negate { - cmd: Box, - }, + Negate { + cmd: Box, + }, CaseNode { pattern: Tk, case_blocks: Vec, @@ -1159,40 +1159,40 @@ impl ParseStream { .with_label(src, label) .with_context(self.context.clone()) } - fn parse_negate(&mut self) -> ShResult> { + fn parse_negate(&mut self) -> ShResult> { let mut node_tks: Vec = vec![]; - if !self.check_keyword("!") || !self.next_tk_is_some() { - return Ok(None); - } - node_tks.push(self.next_tk().unwrap()); + if !self.check_keyword("!") || !self.next_tk_is_some() { + return Ok(None); + } + node_tks.push(self.next_tk().unwrap()); - let Some(cmd) = self.parse_block(true)? else { - self.panic_mode(&mut node_tks); - let span = node_tks.get_span().unwrap(); - let color = next_color(); - return Err( - self.make_err( - span.clone(), - Label::new(span) - .with_message("Expected a command after '!'") - .with_color(color), - ), - ); - }; + let Some(cmd) = self.parse_block(true)? else { + self.panic_mode(&mut node_tks); + let span = node_tks.get_span().unwrap(); + let color = next_color(); + return Err( + self.make_err( + span.clone(), + Label::new(span) + .with_message("Expected a command after '!'") + .with_color(color), + ), + ); + }; - node_tks.extend(cmd.tokens.clone()); - self.catch_separator(&mut node_tks); + node_tks.extend(cmd.tokens.clone()); + self.catch_separator(&mut node_tks); - let node = Node { - class: NdRule::Negate { cmd: Box::new(cmd) }, - flags: NdFlags::empty(), - redirs: vec![], - context: self.context.clone(), - tokens: node_tks, - }; - Ok(Some(node)) - } + let node = Node { + class: NdRule::Negate { cmd: Box::new(cmd) }, + flags: NdFlags::empty(), + redirs: vec![], + context: self.context.clone(), + tokens: node_tks, + }; + Ok(Some(node)) + } fn parse_if(&mut self) -> ShResult> { // Needs at last one 'if-then', // Any number of 'elif-then', @@ -1931,393 +1931,400 @@ where ref mut body, } => check_node(body, filter, operation), - NdRule::Negate { ref mut cmd } => check_node(cmd, filter, operation), + NdRule::Negate { ref mut cmd } => check_node(cmd, filter, operation), NdRule::Test { cases: _ } => (), } } #[cfg(test)] pub mod tests { - use crate::testutil::{NdKind, get_ast}; + use crate::testutil::{NdKind, get_ast}; - #[test] - fn parse_hello_world() { - let input = "echo hello world"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + #[test] + fn parse_hello_world() { + let input = "echo hello world"; + let expected = &mut [NdKind::Conjunction, NdKind::Pipeline, NdKind::Command].into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_if_statement() { - let input = "if echo foo; then echo bar; fi"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::IfNode, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + #[test] + fn parse_if_statement() { + let input = "if echo foo; then echo bar; fi"; + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::IfNode, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_pipeline() { - let input = "ls | grep foo | wc -l"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Command, - NdKind::Command, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + #[test] + fn parse_pipeline() { + let input = "ls | grep foo | wc -l"; + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Command, + NdKind::Command, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_conjunction_and() { - let input = "echo foo && echo bar"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Pipeline, - NdKind::Command, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + #[test] + fn parse_conjunction_and() { + let input = "echo foo && echo bar"; + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Pipeline, + NdKind::Command, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_while_loop() { - let input = "while true; do echo hello; done"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::LoopNode, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + #[test] + fn parse_while_loop() { + let input = "while true; do echo hello; done"; + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::LoopNode, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_for_loop() { - let input = "for i in a b c; do echo $i; done"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::ForNode, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + #[test] + fn parse_for_loop() { + let input = "for i in a b c; do echo $i; done"; + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::ForNode, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_case_statement() { - let input = "case foo in bar) echo bar;; baz) echo baz;; esac"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::CaseNode, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + #[test] + fn parse_case_statement() { + let input = "case foo in bar) echo bar;; baz) echo baz;; esac"; + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::CaseNode, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_func_def() { - let input = "foo() { echo hello; }"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::FuncDef, - NdKind::BraceGrp, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + #[test] + fn parse_func_def() { + let input = "foo() { echo hello; }"; + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::FuncDef, + NdKind::BraceGrp, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_assignment() { - let input = "FOO=bar"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Assignment, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + #[test] + fn parse_assignment() { + let input = "FOO=bar"; + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Assignment, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_assignment_with_command() { - let input = "FOO=bar echo hello"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Assignment, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + #[test] + fn parse_assignment_with_command() { + let input = "FOO=bar echo hello"; + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Assignment, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_if_elif_else() { - let input = "if true; then echo a; elif false; then echo b; else echo c; fi"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::IfNode, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + #[test] + fn parse_if_elif_else() { + let input = "if true; then echo a; elif false; then echo b; else echo c; fi"; + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::IfNode, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_brace_group() { - let input = "{ echo hello; echo world; }"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::BraceGrp, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + #[test] + fn parse_brace_group() { + let input = "{ echo hello; echo world; }"; + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::BraceGrp, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_nested_if_in_while() { - let input = "while true; do if false; then echo no; fi; done"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::LoopNode, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::IfNode, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + #[test] + fn parse_nested_if_in_while() { + let input = "while true; do if false; then echo no; fi; done"; + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::LoopNode, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::IfNode, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_test_bracket() { - let input = "[[ -n hello ]]"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Test, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + #[test] + fn parse_test_bracket() { + let input = "[[ -n hello ]]"; + let expected = &mut [NdKind::Conjunction, NdKind::Pipeline, NdKind::Test].into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_nested_func_with_if_and_loop() { - let input = "setup() { + #[test] + fn parse_nested_func_with_if_and_loop() { + let input = "setup() { for f in a b c; do if [[ -n $f ]]; then echo $f fi done }"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::FuncDef, - NdKind::BraceGrp, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::ForNode, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::IfNode, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Test, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::FuncDef, + NdKind::BraceGrp, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::ForNode, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::IfNode, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Test, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_pipeline_with_brace_groups() { - let input = "{ echo foo; echo bar; } | { grep foo; wc -l; }"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::BraceGrp, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::BraceGrp, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + #[test] + fn parse_pipeline_with_brace_groups() { + let input = "{ echo foo; echo bar; } | { grep foo; wc -l; }"; + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::BraceGrp, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::BraceGrp, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_deeply_nested_if() { - let input = "if true; then + #[test] + fn parse_deeply_nested_if() { + let input = "if true; then if false; then if true; then echo deep fi fi fi"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::IfNode, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::IfNode, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::IfNode, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::IfNode, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::IfNode, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::IfNode, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_case_with_multiple_commands() { - let input = "case $1 in + #[test] + fn parse_case_with_multiple_commands() { + let input = "case $1 in start) echo starting run_server @@ -2330,36 +2337,37 @@ pub mod tests { echo unknown ;; esac"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::CaseNode, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::CaseNode, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_func_with_case_and_conjunction() { - let input = "dispatch() { + #[test] + fn parse_func_with_case_and_conjunction() { + let input = "dispatch() { case $1 in build) make clean && make all @@ -2369,187 +2377,193 @@ pub mod tests { ;; esac }"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::FuncDef, - NdKind::BraceGrp, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::CaseNode, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Pipeline, - NdKind::Command, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::FuncDef, + NdKind::BraceGrp, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::CaseNode, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Pipeline, + NdKind::Command, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_while_with_pipeline_and_assignment() { - let input = "while read line; do + #[test] + fn parse_while_with_pipeline_and_assignment() { + let input = "while read line; do FOO=bar echo $line | grep pattern | wc -l done"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::LoopNode, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Assignment, - NdKind::Command, - NdKind::Command, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::LoopNode, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Assignment, + NdKind::Command, + NdKind::Command, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_nested_loops() { - let input = "for i in 1 2 3; do + #[test] + fn parse_nested_loops() { + let input = "for i in 1 2 3; do for j in a b c; do while true; do echo $i $j done done done"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::ForNode, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::ForNode, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::LoopNode, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::ForNode, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::ForNode, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::LoopNode, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_complex_conjunction_chain() { - let input = "mkdir -p dir && cd dir && touch file || echo failed && echo done"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Pipeline, - NdKind::Command, - NdKind::Pipeline, - NdKind::Command, - NdKind::Pipeline, - NdKind::Command, - NdKind::Pipeline, - NdKind::Command, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + #[test] + fn parse_complex_conjunction_chain() { + let input = "mkdir -p dir && cd dir && touch file || echo failed && echo done"; + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Pipeline, + NdKind::Command, + NdKind::Pipeline, + NdKind::Command, + NdKind::Pipeline, + NdKind::Command, + NdKind::Pipeline, + NdKind::Command, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_func_defining_inner_func() { - let input = "outer() { + #[test] + fn parse_func_defining_inner_func() { + let input = "outer() { inner() { echo hello from inner } inner }"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::FuncDef, - NdKind::BraceGrp, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::FuncDef, - NdKind::BraceGrp, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::FuncDef, + NdKind::BraceGrp, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::FuncDef, + NdKind::BraceGrp, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_multiline_if_elif_with_pipelines() { - let input = "if cat /etc/passwd | grep root; then + #[test] + fn parse_multiline_if_elif_with_pipelines() { + let input = "if cat /etc/passwd | grep root; then echo found root elif ls /tmp | wc -l; then echo tmp has files else echo fallback | tee log.txt fi"; - let expected = &mut [ - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::IfNode, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Conjunction, - NdKind::Pipeline, - NdKind::Command, - NdKind::Command, - ].into_iter(); - let ast = get_ast(input).unwrap(); - let mut node = ast[0].clone(); - if let Err(e) = node.assert_structure(expected) { - panic!("{}", e); - } - } + let expected = &mut [ + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::IfNode, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Conjunction, + NdKind::Pipeline, + NdKind::Command, + NdKind::Command, + ] + .into_iter(); + let ast = get_ast(input).unwrap(); + let mut node = ast[0].clone(); + if let Err(e) = node.assert_structure(expected) { + panic!("{}", e); + } + } - #[test] - fn parse_cursed_input() { - let input = "if if while if if until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; else while :; do :; done; fi; then if until :; do :; done; then until :; do :; done; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; else while :; do :; done; fi; elif while while :; do :; done; do until :; do :; done; done; then while until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; elif until case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; then until if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; else case foo in; foo) while :; do :; done;; bar) until :; do :; done;; biz) until :; do :; done;; esac; fi; do while case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) case foo in; foo) :;; bar) :;; biz) :;; esac;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; done; then until while if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; do until while :; do :; done; do until :; do :; done; done; done; elif until until until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; do case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) if :; then :; elif :; then :; elif :; then :; else :; fi;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac; done; then case foo in; foo) case foo in; foo) while :; do :; done;; bar) while :; do :; done;; biz) until :; do :; done;; esac;; bar) if until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; else while :; do :; done; fi;; biz) if until :; do :; done; then until :; do :; done; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; else while :; do :; done; fi;; esac; elif case foo in; foo) while while :; do :; done; do until :; do :; done; done;; bar) while until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done;; biz) until case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done;; esac; then if until if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; then case foo in; foo) while :; do :; done;; bar) until :; do :; done;; biz) until :; do :; done;; esac; elif case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) case foo in; foo) :;; bar) :;; biz) :;; esac;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac; then if if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; elif while :; do :; done; then until :; do :; done; elif until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi; elif if if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif while :; do :; done; then while :; do :; done; elif until :; do :; done; then until :; do :; done; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi; then while case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; else while if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; fi; else if until while :; do :; done; do until :; do :; done; done; then until until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; elif case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) if :; then :; elif :; then :; elif :; then :; else :; fi;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac; then case foo in; foo) while :; do :; done;; bar) while :; do :; done;; biz) until :; do :; done;; esac; elif if until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; else while :; do :; done; fi; then if until :; do :; done; then until :; do :; done; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; else while :; do :; done; fi; else while while :; do :; done; do until :; do :; done; done; fi; fi; then while while while until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; do until case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; done; do while until if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; do case foo in; foo) while :; do :; done;; bar) until :; do :; done;; biz) until :; do :; done;; esac; done; done; elif until until case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) case foo in; foo) :;; bar) :;; biz) :;; esac;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac; do if if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; elif while :; do :; done; then until :; do :; done; elif until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi; done; do until if if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif while :; do :; done; then while :; do :; done; elif until :; do :; done; then until :; do :; done; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi; do while case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; done; done; then case foo in; foo) case foo in; foo) while if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done;; bar) until while :; do :; done; do until :; do :; done; done;; biz) until until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done;; esac;; bar) case foo in; foo) case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) if :; then :; elif :; then :; elif :; then :; else :; fi;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac;; bar) case foo in; foo) while :; do :; done;; bar) while :; do :; done;; biz) until :; do :; done;; esac;; biz) if until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; else while :; do :; done; fi;; esac;; biz) if if until :; do :; done; then until :; do :; done; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; else while :; do :; done; fi; then while while :; do :; done; do until :; do :; done; done; elif while until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; then until case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; elif until if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; then case foo in; foo) while :; do :; done;; bar) until :; do :; done;; biz) until :; do :; done;; esac; else case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) case foo in; foo) :;; bar) :;; biz) :;; esac;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac; fi;; esac; elif if if if if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; elif while :; do :; done; then until :; do :; done; elif until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi; then if if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif while :; do :; done; then while :; do :; done; elif until :; do :; done; then until :; do :; done; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi; elif while case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; then while if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; elif until while :; do :; done; do until :; do :; done; done; then until until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; else case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) if :; then :; elif :; then :; elif :; then :; else :; fi;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac; fi; then while case foo in; foo) while :; do :; done;; bar) while :; do :; done;; biz) until :; do :; done;; esac; do if until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; else while :; do :; done; fi; done; elif while if until :; do :; done; then until :; do :; done; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; else while :; do :; done; fi; do while while :; do :; done; do until :; do :; done; done; done; then until while until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; do until case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; done; elif until until if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; do case foo in; foo) while :; do :; done;; bar) until :; do :; done;; biz) until :; do :; done;; esac; done; then case foo in; foo) case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) case foo in; foo) :;; bar) :;; biz) :;; esac;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac;; bar) if if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; elif while :; do :; done; then until :; do :; done; elif until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi;; biz) if if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif while :; do :; done; then while :; do :; done; elif until :; do :; done; then until :; do :; done; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi;; esac; else case foo in; foo) while case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done;; bar) while if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done;; biz) until while :; do :; done; do until :; do :; done; done;; esac; fi; then if if until until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; then case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) if :; then :; elif :; then :; elif :; then :; else :; fi;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac; elif case foo in; foo) while :; do :; done;; bar) while :; do :; done;; biz) until :; do :; done;; esac; then if until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; else while :; do :; done; fi; elif if until :; do :; done; then until :; do :; done; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; else while :; do :; done; fi; then while while :; do :; done; do until :; do :; done; done; else while until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; fi; then if until case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; then until if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; elif case foo in; foo) while :; do :; done;; bar) until :; do :; done;; biz) until :; do :; done;; esac; then case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) case foo in; foo) :;; bar) :;; biz) :;; esac;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac; elif if if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; elif while :; do :; done; then until :; do :; done; elif until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi; then if if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif while :; do :; done; then while :; do :; done; elif until :; do :; done; then until :; do :; done; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi; else while case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; fi; elif while while if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; do until while :; do :; done; do until :; do :; done; done; done; then while until until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; do case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) if :; then :; elif :; then :; elif :; then :; else :; fi;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac; done; elif until case foo in; foo) while :; do :; done;; bar) while :; do :; done;; biz) until :; do :; done;; esac; do if until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; else while :; do :; done; fi; done; then until if until :; do :; done; then until :; do :; done; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; else while :; do :; done; fi; do while while :; do :; done; do until :; do :; done; done; done; else case foo in; foo) while until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done;; bar) until case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done;; biz) until if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done;; esac; fi; else while case foo in; foo) case foo in; foo) while :; do :; done;; bar) until :; do :; done;; biz) until :; do :; done;; esac;; bar) case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) case foo in; foo) :;; bar) :;; biz) :;; esac;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac;; biz) if if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; elif while :; do :; done; then until :; do :; done; elif until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi;; esac; do if if if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif while :; do :; done; then while :; do :; done; elif until :; do :; done; then until :; do :; done; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi; then while case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; elif while if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; then until while :; do :; done; do until :; do :; done; done; elif until until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; then case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) if :; then :; elif :; then :; elif :; then :; else :; fi;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac; else case foo in; foo) while :; do :; done;; bar) while :; do :; done;; biz) until :; do :; done;; esac; fi; done; fi"; - assert!(get_ast(input).is_ok()); // lets spare our sanity and just say that "ok" means "it parsed correctly" - } + #[test] + fn parse_cursed_input() { + let input = "if if while if if until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; else while :; do :; done; fi; then if until :; do :; done; then until :; do :; done; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; else while :; do :; done; fi; elif while while :; do :; done; do until :; do :; done; done; then while until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; elif until case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; then until if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; else case foo in; foo) while :; do :; done;; bar) until :; do :; done;; biz) until :; do :; done;; esac; fi; do while case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) case foo in; foo) :;; bar) :;; biz) :;; esac;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; done; then until while if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; do until while :; do :; done; do until :; do :; done; done; done; elif until until until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; do case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) if :; then :; elif :; then :; elif :; then :; else :; fi;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac; done; then case foo in; foo) case foo in; foo) while :; do :; done;; bar) while :; do :; done;; biz) until :; do :; done;; esac;; bar) if until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; else while :; do :; done; fi;; biz) if until :; do :; done; then until :; do :; done; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; else while :; do :; done; fi;; esac; elif case foo in; foo) while while :; do :; done; do until :; do :; done; done;; bar) while until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done;; biz) until case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done;; esac; then if until if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; then case foo in; foo) while :; do :; done;; bar) until :; do :; done;; biz) until :; do :; done;; esac; elif case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) case foo in; foo) :;; bar) :;; biz) :;; esac;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac; then if if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; elif while :; do :; done; then until :; do :; done; elif until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi; elif if if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif while :; do :; done; then while :; do :; done; elif until :; do :; done; then until :; do :; done; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi; then while case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; else while if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; fi; else if until while :; do :; done; do until :; do :; done; done; then until until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; elif case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) if :; then :; elif :; then :; elif :; then :; else :; fi;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac; then case foo in; foo) while :; do :; done;; bar) while :; do :; done;; biz) until :; do :; done;; esac; elif if until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; else while :; do :; done; fi; then if until :; do :; done; then until :; do :; done; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; else while :; do :; done; fi; else while while :; do :; done; do until :; do :; done; done; fi; fi; then while while while until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; do until case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; done; do while until if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; do case foo in; foo) while :; do :; done;; bar) until :; do :; done;; biz) until :; do :; done;; esac; done; done; elif until until case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) case foo in; foo) :;; bar) :;; biz) :;; esac;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac; do if if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; elif while :; do :; done; then until :; do :; done; elif until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi; done; do until if if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif while :; do :; done; then while :; do :; done; elif until :; do :; done; then until :; do :; done; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi; do while case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; done; done; then case foo in; foo) case foo in; foo) while if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done;; bar) until while :; do :; done; do until :; do :; done; done;; biz) until until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done;; esac;; bar) case foo in; foo) case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) if :; then :; elif :; then :; elif :; then :; else :; fi;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac;; bar) case foo in; foo) while :; do :; done;; bar) while :; do :; done;; biz) until :; do :; done;; esac;; biz) if until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; else while :; do :; done; fi;; esac;; biz) if if until :; do :; done; then until :; do :; done; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; else while :; do :; done; fi; then while while :; do :; done; do until :; do :; done; done; elif while until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; then until case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; elif until if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; then case foo in; foo) while :; do :; done;; bar) until :; do :; done;; biz) until :; do :; done;; esac; else case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) case foo in; foo) :;; bar) :;; biz) :;; esac;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac; fi;; esac; elif if if if if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; elif while :; do :; done; then until :; do :; done; elif until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi; then if if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif while :; do :; done; then while :; do :; done; elif until :; do :; done; then until :; do :; done; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi; elif while case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; then while if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; elif until while :; do :; done; do until :; do :; done; done; then until until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; else case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) if :; then :; elif :; then :; elif :; then :; else :; fi;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac; fi; then while case foo in; foo) while :; do :; done;; bar) while :; do :; done;; biz) until :; do :; done;; esac; do if until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; else while :; do :; done; fi; done; elif while if until :; do :; done; then until :; do :; done; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; else while :; do :; done; fi; do while while :; do :; done; do until :; do :; done; done; done; then until while until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; do until case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; done; elif until until if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; do case foo in; foo) while :; do :; done;; bar) until :; do :; done;; biz) until :; do :; done;; esac; done; then case foo in; foo) case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) case foo in; foo) :;; bar) :;; biz) :;; esac;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac;; bar) if if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; elif while :; do :; done; then until :; do :; done; elif until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi;; biz) if if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif while :; do :; done; then while :; do :; done; elif until :; do :; done; then until :; do :; done; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi;; esac; else case foo in; foo) while case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done;; bar) while if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done;; biz) until while :; do :; done; do until :; do :; done; done;; esac; fi; then if if until until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; then case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) if :; then :; elif :; then :; elif :; then :; else :; fi;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac; elif case foo in; foo) while :; do :; done;; bar) while :; do :; done;; biz) until :; do :; done;; esac; then if until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; else while :; do :; done; fi; elif if until :; do :; done; then until :; do :; done; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; else while :; do :; done; fi; then while while :; do :; done; do until :; do :; done; done; else while until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; fi; then if until case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; then until if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; elif case foo in; foo) while :; do :; done;; bar) until :; do :; done;; biz) until :; do :; done;; esac; then case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) case foo in; foo) :;; bar) :;; biz) :;; esac;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac; elif if if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; elif while :; do :; done; then until :; do :; done; elif until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi; then if if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif while :; do :; done; then while :; do :; done; elif until :; do :; done; then until :; do :; done; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi; else while case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; fi; elif while while if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; do until while :; do :; done; do until :; do :; done; done; done; then while until until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; do case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) if :; then :; elif :; then :; elif :; then :; else :; fi;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac; done; elif until case foo in; foo) while :; do :; done;; bar) while :; do :; done;; biz) until :; do :; done;; esac; do if until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; else while :; do :; done; fi; done; then until if until :; do :; done; then until :; do :; done; elif case foo in; foo) :;; bar) :;; biz) :;; esac; then case foo in; foo) :;; bar) :;; biz) :;; esac; elif if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; else while :; do :; done; fi; do while while :; do :; done; do until :; do :; done; done; done; else case foo in; foo) while until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done;; bar) until case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done;; biz) until if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done;; esac; fi; else while case foo in; foo) case foo in; foo) while :; do :; done;; bar) until :; do :; done;; biz) until :; do :; done;; esac;; bar) case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) case foo in; foo) :;; bar) :;; biz) :;; esac;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac;; biz) if if :; then :; elif :; then :; elif :; then :; else :; fi; then while :; do :; done; elif while :; do :; done; then until :; do :; done; elif until :; do :; done; then case foo in; foo) :;; bar) :;; biz) :;; esac; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi;; esac; do if if if :; then :; elif :; then :; elif :; then :; else :; fi; then if :; then :; elif :; then :; elif :; then :; else :; fi; elif while :; do :; done; then while :; do :; done; elif until :; do :; done; then until :; do :; done; else case foo in; foo) :;; bar) :;; biz) :;; esac; fi; then while case foo in; foo) :;; bar) :;; biz) :;; esac; do if :; then :; elif :; then :; elif :; then :; else :; fi; done; elif while if :; then :; elif :; then :; elif :; then :; else :; fi; do while :; do :; done; done; then until while :; do :; done; do until :; do :; done; done; elif until until :; do :; done; do case foo in; foo) :;; bar) :;; biz) :;; esac; done; then case foo in; foo) case foo in; foo) :;; bar) :;; biz) :;; esac;; bar) if :; then :; elif :; then :; elif :; then :; else :; fi;; biz) if :; then :; elif :; then :; elif :; then :; else :; fi;; esac; else case foo in; foo) while :; do :; done;; bar) while :; do :; done;; biz) until :; do :; done;; esac; fi; done; fi"; + assert!(get_ast(input).is_ok()); // lets spare our sanity and just say that "ok" means "it parsed correctly" + } } diff --git a/src/prelude.rs b/src/prelude.rs index ef8a39f..01df639 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -33,5 +33,4 @@ pub use nix::{ }, }; - // Additional utilities, if needed, can be added here diff --git a/src/procio.rs b/src/procio.rs index 42424d5..fe6cce4 100644 --- a/src/procio.rs +++ b/src/procio.rs @@ -389,154 +389,166 @@ impl Iterator for PipeGenerator { #[cfg(test)] pub mod tests { - use crate::testutil::{TestGuard, has_cmd, has_cmds, test_input}; - use pretty_assertions::assert_eq; + use crate::testutil::{TestGuard, has_cmd, has_cmds, test_input}; + use pretty_assertions::assert_eq; - #[test] - fn pipeline_simple() { - if !has_cmd("sed") { return }; - let g = TestGuard::new(); + #[test] + fn pipeline_simple() { + if !has_cmd("sed") { + 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(); - assert_eq!(out, "bar\n"); - } + let out = g.read_output(); + assert_eq!(out, "bar\n"); + } - #[test] - fn pipeline_multi() { - if !has_cmds(&[ - "cut", - "sed" - ]) { return; } - let g = TestGuard::new(); + #[test] + fn pipeline_multi() { + if !has_cmds(&["cut", "sed"]) { + return; + } + 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(); - assert_eq!(out, "bAr\n"); - } + let out = g.read_output(); + assert_eq!(out, "bAr\n"); + } - #[test] - fn rube_goldberg_pipeline() { - if !has_cmds(&[ - "sed", - "cat", - ]) { return } - let g = TestGuard::new(); + #[test] + fn rube_goldberg_pipeline() { + if !has_cmds(&["sed", "cat"]) { + return; + } + 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(); - assert_eq!(out, "baz\nbuzz\n"); - } + let out = g.read_output(); + assert_eq!(out, "baz\nbuzz\n"); + } - #[test] - fn simple_file_redir() { - let mut g = TestGuard::new(); + #[test] + fn simple_file_redir() { + 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(); }); - let contents = std::fs::read_to_string("/tmp/simple_file_redir.txt").unwrap(); + g.add_cleanup(|| { + 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] - fn append_file_redir() { - let dir = tempfile::TempDir::new().unwrap(); - let path = dir.path().join("append.txt"); - let _g = TestGuard::new(); + #[test] + fn append_file_redir() { + let dir = tempfile::TempDir::new().unwrap(); + let path = dir.path().join("append.txt"); + let _g = TestGuard::new(); - test_input(format!("echo first > {}", path.display())).unwrap(); - test_input(format!("echo second >> {}", path.display())).unwrap(); + test_input(format!("echo first > {}", path.display())).unwrap(); + test_input(format!("echo second >> {}", path.display())).unwrap(); - let contents = std::fs::read_to_string(&path).unwrap(); - assert_eq!(contents, "first\nsecond\n"); - } + let contents = std::fs::read_to_string(&path).unwrap(); + assert_eq!(contents, "first\nsecond\n"); + } - #[test] - fn input_redir() { - if !has_cmd("cat") { return; } - let dir = tempfile::TempDir::new().unwrap(); - let path = dir.path().join("input.txt"); - std::fs::write(&path, "hello from file\n").unwrap(); - let g = TestGuard::new(); + #[test] + fn input_redir() { + if !has_cmd("cat") { + return; + } + let dir = tempfile::TempDir::new().unwrap(); + 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(); - assert_eq!(out, "hello from file\n"); - } + let out = g.read_output(); + assert_eq!(out, "hello from file\n"); + } - #[test] - fn stderr_redir_to_file() { - let dir = tempfile::TempDir::new().unwrap(); - let path = dir.path().join("err.txt"); - let g = TestGuard::new(); + #[test] + fn stderr_redir_to_file() { + let dir = tempfile::TempDir::new().unwrap(); + let path = dir.path().join("err.txt"); + 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(); - assert_eq!(contents, "error msg\n"); - // stdout should be empty since we redirected to stderr - let out = g.read_output(); - assert_eq!(out, ""); - } + let contents = std::fs::read_to_string(&path).unwrap(); + assert_eq!(contents, "error msg\n"); + // stdout should be empty since we redirected to stderr + let out = g.read_output(); + assert_eq!(out, ""); + } - #[test] - fn pipe_and_stderr() { - if !has_cmd("cat") { return; } - let g = TestGuard::new(); + #[test] + fn pipe_and_stderr() { + if !has_cmd("cat") { + 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(); - assert_eq!(out, "on stderr\n"); - } + let out = g.read_output(); + assert_eq!(out, "on stderr\n"); + } - #[test] - fn output_redir_clobber() { - let dir = tempfile::TempDir::new().unwrap(); - let path = dir.path().join("clobber.txt"); - let _g = TestGuard::new(); + #[test] + fn output_redir_clobber() { + let dir = tempfile::TempDir::new().unwrap(); + let path = dir.path().join("clobber.txt"); + let _g = TestGuard::new(); - test_input(format!("echo first > {}", path.display())).unwrap(); - test_input(format!("echo second > {}", path.display())).unwrap(); + test_input(format!("echo first > {}", path.display())).unwrap(); + test_input(format!("echo second > {}", path.display())).unwrap(); - let contents = std::fs::read_to_string(&path).unwrap(); - assert_eq!(contents, "second\n"); - } + let contents = std::fs::read_to_string(&path).unwrap(); + assert_eq!(contents, "second\n"); + } - #[test] - fn pipeline_preserves_exit_status() { - if !has_cmd("cat") { return; } - let _g = TestGuard::new(); + #[test] + fn pipeline_preserves_exit_status() { + if !has_cmd("cat") { + return; + } + let _g = TestGuard::new(); - test_input("false | cat").unwrap(); + test_input("false | cat").unwrap(); - // Pipeline exit status is the last command - let status = crate::state::get_status(); - assert_eq!(status, 0); + // Pipeline exit status is the last command + let status = crate::state::get_status(); + assert_eq!(status, 0); - test_input("cat < /dev/null | false").unwrap(); + test_input("cat < /dev/null | false").unwrap(); - let status = crate::state::get_status(); - assert_ne!(status, 0); - } + let status = crate::state::get_status(); + assert_ne!(status, 0); + } - #[test] - fn fd_duplication() { - let dir = tempfile::TempDir::new().unwrap(); - let path = dir.path().join("dup.txt"); - let _g = TestGuard::new(); + #[test] + fn fd_duplication() { + let dir = tempfile::TempDir::new().unwrap(); + let path = dir.path().join("dup.txt"); + let _g = TestGuard::new(); - // 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(); + // 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(); - let contents = std::fs::read_to_string(&path).unwrap(); - assert!(contents.contains("out")); - assert!(contents.contains("err")); - } + let contents = std::fs::read_to_string(&path).unwrap(); + assert!(contents.contains("out")); + assert!(contents.contains("err")); + } } diff --git a/src/readline/complete.rs b/src/readline/complete.rs index 2dd646a..b5264f3 100644 --- a/src/readline/complete.rs +++ b/src/readline/complete.rs @@ -9,17 +9,24 @@ use nix::sys::signal::Signal; use unicode_width::UnicodeWidthStr; 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, lex::{self, LexFlags, Tk, TkRule, ends_with_unescaped}, - }, readline::{ + }, + readline::{ Marker, annotate_input_recursive, keys::{KeyCode as C, KeyEvent as K, ModKeys as M}, linebuf::{ClampedUsize, LineBuf}, markers::{self, is_marker}, term::{LineWriter, TermWriter, calc_str_width, get_win_size}, 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 { @@ -170,10 +177,10 @@ fn complete_commands(start: &str) -> Vec { .collect() }); - if read_shopts(|o| o.core.autocd) { - let dirs = complete_dirs(start); - candidates.extend(dirs); - } + if read_shopts(|o| o.core.autocd) { + let dirs = complete_dirs(start); + candidates.extend(dirs); + } candidates.sort(); candidates @@ -561,15 +568,17 @@ pub trait Completer { fn reset(&mut self); fn reset_stay_active(&mut self); fn is_active(&self) -> bool; - fn all_candidates(&self) -> Vec { vec![] } + fn all_candidates(&self) -> Vec { + vec![] + } fn selected_candidate(&self) -> Option; fn token_span(&self) -> (usize, usize); fn original_input(&self) -> &str; - fn token(&self) -> &str { - let orig = self.original_input(); - let (s,e) = self.token_span(); - orig.get(s..e).unwrap_or(orig) - } + fn token(&self) -> &str { + let orig = self.original_input(); + let (s, e) = self.token_span(); + orig.get(s..e).unwrap_or(orig) + } fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()>; fn clear(&mut self, _writer: &mut TermWriter) -> ShResult<()> { Ok(()) @@ -789,21 +798,21 @@ impl FuzzySelector { } } - pub fn candidates(&self) -> &[String] { - &self.candidates - } + pub fn candidates(&self) -> &[String] { + &self.candidates + } - pub fn filtered(&self) -> &[ScoredCandidate] { - &self.filtered - } + pub fn filtered(&self) -> &[ScoredCandidate] { + &self.filtered + } - pub fn filtered_len(&self) -> usize { - self.filtered.len() - } + pub fn filtered_len(&self) -> usize { + self.filtered.len() + } - pub fn candidates_len(&self) -> usize { - self.candidates.len() - } + pub fn candidates_len(&self) -> usize { + self.candidates.len() + } pub fn activate(&mut self, candidates: Vec) { self.active = true; @@ -1158,9 +1167,9 @@ impl Default for FuzzyCompleter { } impl Completer for FuzzyCompleter { - fn all_candidates(&self) -> Vec { - self.selector.candidates.clone() - } + fn all_candidates(&self) -> Vec { + self.selector.candidates.clone() + } fn set_prompt_line_context(&mut self, line_width: u16, cursor_col: u16) { self .selector @@ -1174,10 +1183,14 @@ impl Completer for FuzzyCompleter { let selected = self.selector.selected_candidate().unwrap_or_default(); let (mut start, end) = self.completer.token_span; - let slice = self.completer.original_input.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 slice = self + .completer + .original_input + .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!( "{}{}{}", &self.completer.original_input[..start], @@ -1253,9 +1266,9 @@ pub struct SimpleCompleter { } impl Completer for SimpleCompleter { - fn all_candidates(&self) -> Vec { - self.candidates.clone() - } + fn all_candidates(&self) -> Vec { + self.candidates.clone() + } fn reset_stay_active(&mut self) { let active = self.is_active(); self.reset(); @@ -1435,10 +1448,10 @@ impl SimpleCompleter { let selected = &self.candidates[self.selected_idx]; let (mut start, end) = self.token_span; - let slice = self.original_input.get(start..end).unwrap_or(""); - start += slice.width(); - let completion = selected.strip_prefix(slice).unwrap_or(selected); - let escaped = escape_str(completion, false); + let slice = self.original_input.get(start..end).unwrap_or(""); + start += slice.width(); + let completion = selected.strip_prefix(slice).unwrap_or(selected); + let escaped = escape_str(completion, false); format!( "{}{}{}", &self.original_input[..start], @@ -1604,11 +1617,13 @@ impl SimpleCompleter { // If token contains any COMP_WORDBREAKS, break the word let token_str = cur_token.span.as_str(); - 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)) { - 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); - } + 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)) { + 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); + } let raw_tk = cur_token.as_str().to_string(); let expanded_tk = cur_token.expand()?; @@ -1654,12 +1669,12 @@ impl SimpleCompleter { #[cfg(test)] mod tests { use super::*; - use std::os::fd::AsRawFd; use crate::{ readline::{Prompt, ShedVi}, state::{VarFlags, VarKind, write_vars}, testutil::TestGuard, }; + use std::os::fd::AsRawFd; fn test_vi(initial: &str) -> (ShedVi, TestGuard) { let g = TestGuard::new(); @@ -1793,13 +1808,20 @@ mod tests { let _ = comp.get_candidates(line.clone(), cursor); 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] fn wordbreak_colon_when_set() { 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 line = "scp host:foo".to_string(); @@ -1807,13 +1829,20 @@ mod tests { let _ = comp.get_candidates(line.clone(), cursor); 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] fn wordbreak_rightmost_wins() { 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 line = "cmd --opt=host:val".to_string(); @@ -1821,7 +1850,11 @@ mod tests { let _ = comp.get_candidates(line.clone(), cursor); 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 ===================== @@ -1884,7 +1917,10 @@ mod tests { #[test] fn escape_str_all_shell_metacharacters() { use crate::expand::escape_str; - for ch in ['\'', '"', '\\', '|', '&', ';', '(', ')', '<', '>', '$', '*', '!', '`', '{', '?', '[', '#', ' ', '\t', '\n'] { + for ch in [ + '\'', '"', '\\', '|', '&', ';', '(', ')', '<', '>', '$', '*', '!', '`', '{', '?', '[', '#', + ' ', '\t', '\n', + ] { let input = format!("a{ch}b"); let escaped = escape_str(&input, false); let expected = format!("a\\{ch}b"); diff --git a/src/readline/highlight.rs b/src/readline/highlight.rs index 2129d5e..b0f5f01 100644 --- a/src/readline/highlight.rs +++ b/src/readline/highlight.rs @@ -88,7 +88,9 @@ impl Highlighter { while prefix_chars.peek().is_some() { match chars.next() { 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 } } @@ -104,7 +106,9 @@ impl Highlighter { let mut si = suffix_chars.len(); while si > 0 { - if ti == 0 { return text.to_string(); } + if ti == 0 { + return text.to_string(); + } ti -= 1; if chars[ti] == markers::VISUAL_MODE_START || chars[ti] == markers::VISUAL_MODE_END { continue; // skip visual markers @@ -346,7 +350,9 @@ impl Highlighter { recursive_highlighter.highlight(); // Read back visual state — selection may have started/ended inside 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 { self.emit_style(Style::BgWhite | Style::Black); self.output.push_str(prefix); diff --git a/src/readline/history.rs b/src/readline/history.rs index 2108172..a1e5371 100644 --- a/src/readline/history.rs +++ b/src/readline/history.rs @@ -500,12 +500,8 @@ mod tests { env::set_var(key, val); } guard(prev, move |p| match p { - Some(v) => unsafe { - env::set_var(key, v) - }, - None => unsafe { - env::remove_var(key) - }, + Some(v) => unsafe { env::set_var(key, v) }, + None => unsafe { env::remove_var(key) }, }) } @@ -522,12 +518,7 @@ mod tests { fn write_history_file(path: &Path) { fs::write( 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(); } @@ -586,12 +577,7 @@ mod tests { let hist_path = tmp.path().join("history"); fs::write( &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(); diff --git a/src/readline/linebuf.rs b/src/readline/linebuf.rs index d4660de..ebcfd2e 100644 --- a/src/readline/linebuf.rs +++ b/src/readline/linebuf.rs @@ -141,6 +141,12 @@ impl SelectMode { } #[derive(Debug, Clone, PartialEq, Eq)] +enum CaseTransform { + Toggle, + Lower, + Upper, +} + pub enum MotionKind { To(usize), // Absolute position, exclusive On(usize), // Absolute position, inclusive @@ -823,7 +829,7 @@ impl LineBuf { } Some(self.line_bounds(line_no)) } - pub fn word_at(&mut self, pos: usize, word: Word) -> (usize,usize) { + pub fn word_at(&mut self, pos: usize, word: Word) -> (usize, usize) { let start = if self.is_word_bound(self.cursor.get(), word, Direction::Backward) { self.cursor.get() } else { @@ -835,35 +841,41 @@ impl LineBuf { self.end_of_word_forward(self.cursor.get(), word) }; (start, end) - } + } pub fn this_word(&mut self, word: Word) -> (usize, usize) { - self.word_at(self.cursor.get(), word) + self.word_at(self.cursor.get(), word) } - pub fn number_at_cursor(&mut self) -> Option<(usize,usize)> { - self.number_at(self.cursor.get()) - } - pub fn number_at(&mut self, pos: usize) -> Option<(usize,usize)> { - // A number is a sequence of digits, possibly containing one dot, and possibly starting with a minus sign - let is_number_char = |c: &str| c == "." || c == "-" || c.chars().all(|c| c.is_ascii_digit()); - let is_digit = |gr: &str| gr.chars().all(|c| c.is_ascii_digit()); - if self.grapheme_at(pos).is_some_and(|gr| !is_number_char(gr)) { - return None; - } - let mut fwd_indices = self.directional_indices_iter_from(pos, Direction::Forward); - let mut bkwd_indices = self.directional_indices_iter_from(pos, Direction::Backward); + pub fn number_at_cursor(&mut self) -> Option<(usize, usize)> { + self.number_at(self.cursor.get()) + } + pub fn number_at(&mut self, pos: usize) -> Option<(usize, usize)> { + // A number is a sequence of digits, possibly containing one dot, and possibly starting with a minus sign + let is_number_char = |c: &str| c == "." || c == "-" || c.chars().all(|c| c.is_ascii_digit()); + let is_digit = |gr: &str| gr.chars().all(|c| c.is_ascii_digit()); + if self.grapheme_at(pos).is_some_and(|gr| !is_number_char(gr)) { + return None; + } + let mut fwd_indices = self.directional_indices_iter_from(pos, Direction::Forward); + let mut bkwd_indices = self.directional_indices_iter_from(pos, Direction::Backward); - // Find the digit span, then check if preceded by '-' - let mut start = bkwd_indices.find(|i| !self.grapheme_at(*i).is_some_and(is_digit)) - .map(|i| i + 1).unwrap_or(0); - let end = fwd_indices.find(|i| !self.grapheme_at(*i).is_some_and(is_digit)) - .map(|i| i - 1).unwrap_or(self.cursor.max); // inclusive end + // Find the digit span, then check if preceded by '-' + let mut start = bkwd_indices + .find(|i| !self.grapheme_at(*i).is_some_and(is_digit)) + .map(|i| i + 1) + .unwrap_or(0); + let end = fwd_indices + .find(|i| !self.grapheme_at(*i).is_some_and(is_digit)) + .map(|i| i - 1) + .unwrap_or(self.cursor.max); // inclusive end - // Check for leading minus - if start > 0 && self.grapheme_at(start - 1) == Some("-") { start -= 1; } + // Check for leading minus + if start > 0 && self.grapheme_at(start - 1) == Some("-") { + start -= 1; + } - Some((start, end)) - } + Some((start, end)) + } pub fn this_line_exclusive(&mut self) -> (usize, usize) { let line_no = self.cursor_line_number(); let (start, mut end) = self.line_bounds(line_no); @@ -975,7 +987,7 @@ impl LineBuf { dir: Direction, ) -> Box> { self.update_graphemes_lazy(); - let len = self.grapheme_indices().len(); + let len = self.grapheme_indices().len(); match dir { Direction::Forward => Box::new(pos + 1..len) as Box>, Direction::Backward => Box::new((0..pos).rev()) as Box>, @@ -2020,8 +2032,8 @@ impl LineBuf { self.buffer.replace_range(start..end, new); } pub fn calc_indent_level(&mut self) { - // FIXME: This implementation is extremely naive but it kind of sort of works for now - // Need to re-implement it and write tests + // FIXME: This implementation is extremely naive but it kind of sort of works for now + // Need to re-implement it and write tests let to_cursor = self .slice_to_cursor() .map(|s| s.to_string()) @@ -2403,20 +2415,20 @@ impl LineBuf { MotionKind::InclusiveWithTargetCol((self.start_of_line(), end), 0) } MotionCmd(count, Motion::ToColumn) => { - let s = self.start_of_line(); - let mut end = s; - for _ in 0..count { - let Some(gr) = self.grapheme_at(end) else { - end = self.grapheme_indices().len(); - break; - }; - if gr == "\n" { - break; - } - end += 1; - } - MotionKind::On(end.saturating_sub(1)) // count starts at 1, columns are "zero-indexed", so we subtract one - } + let s = self.start_of_line(); + let mut end = s; + for _ in 0..count { + let Some(gr) = self.grapheme_at(end) else { + end = self.grapheme_indices().len(); + break; + }; + if gr == "\n" { + break; + } + end += 1; + } + MotionKind::On(end.saturating_sub(1)) // count starts at 1, columns are "zero-indexed", so we subtract one + } MotionCmd(count, Motion::Range(start, end)) => { let mut final_end = end; if self.cursor.exclusive { @@ -2600,6 +2612,592 @@ impl LineBuf { }; Some(range) } + fn verb_ydc(&mut self, motion: MotionKind, register: RegisterName, verb: Verb) -> ShResult<()> { + let Some((start, end)) = self.range_from_motion(&motion) else { + return Ok(()); + }; + + let mut do_indent = false; + if verb == Verb::Change && (start, end) == self.this_line_exclusive() { + do_indent = read_shopts(|o| o.prompt.auto_indent); + } + + let mut text = if verb == Verb::Yank { + self + .slice(start..end) + .map(|c| c.to_string()) + .unwrap_or_default() + } else if start == self.grapheme_indices().len() && end == self.grapheme_indices().len() { + // user is in normal mode and pressed 'x' on the last char in the buffer + let drained = self.drain(end.saturating_sub(1)..end); + self.update_graphemes(); + drained + } else { + let drained = self.drain(start..end); + self.update_graphemes(); + drained + }; + let is_linewise = matches!( + motion, + MotionKind::InclusiveWithTargetCol(..) | MotionKind::ExclusiveWithTargetCol(..) + ) || matches!(self.select_mode, Some(SelectMode::Line(_))); + let register_content = if is_linewise { + if text.ends_with('\n') && !text.is_empty() { + text = text.strip_suffix('\n').unwrap().to_string(); + } + RegisterContent::Line(text) + } else { + RegisterContent::Span(text) + }; + register.write_to_register(register_content); + self.cursor.set(start); + if do_indent { + self.calc_indent_level(); + let tabs = (0..self.auto_indent_level).map(|_| '\t'); + for tab in tabs { + self.insert_at_cursor(tab); + self.cursor.add(1); + } + } else if verb != Verb::Change + && let MotionKind::InclusiveWithTargetCol((_, _), col) = motion + { + self.cursor.add(col); + } + Ok(()) + } + fn verb_rot13(&mut self, motion: MotionKind) -> ShResult<()> { + let Some((start, end)) = self.range_from_motion(&motion) else { + return Ok(()); + }; + let slice = self.slice(start..end).unwrap_or_default(); + let rot13 = rot13(slice); + self.buffer.replace_range(start..end, &rot13); + self.cursor.set(start); + Ok(()) + } + fn verb_replace_char(&mut self, motion: MotionKind, ch: char) -> ShResult<()> { + let mut buf = [0u8; 4]; + let new = ch.encode_utf8(&mut buf); + self.replace_at_cursor(new); + self.apply_motion(motion); + Ok(()) + } + pub fn verb_replace_char_inplace(&mut self, ch: char, count: u16) -> ShResult<()> { + if let Some((start, end)) = self.select_range() { + let end = (end + 1).min(self.grapheme_indices().len()); // inclusive + let replaced = ch.to_string().repeat(end.saturating_sub(start)); + self.replace_at(start, &replaced); + self.cursor.set(start); + } else { + for i in 0..count { + let mut buf = [0u8; 4]; + let new = ch.encode_utf8(&mut buf); + self.replace_at_cursor(new); + + // try to increment the cursor until we are on the last iteration + // or until we hit the end of the buffer + if i != count.saturating_sub(1) && !self.cursor.inc() { + break; + } + } + } + Ok(()) + } + fn verb_toggle_case_inplace(&mut self, count: u16) { + let mut did_something = false; + for i in 0..count { + let Some(gr) = self.grapheme_at_cursor() else { + return; + }; + if gr.len() > 1 || gr.is_empty() { + return; + } + let ch = gr.chars().next().unwrap(); + if !ch.is_alphabetic() { + return; + } + let mut buf = [0u8; 4]; + let new = if ch.is_ascii_lowercase() { + ch.to_ascii_uppercase().encode_utf8(&mut buf) + } else { + ch.to_ascii_lowercase().encode_utf8(&mut buf) + }; + self.replace_at_cursor(new); + did_something = true; + if i != count.saturating_sub(1) && !self.cursor.inc() { + break; + } + } + if did_something { + self.cursor.inc(); + } + } + fn verb_case_transform(&mut self, motion: MotionKind, transform: CaseTransform) -> ShResult<()> { + let Some((start, end)) = self.range_from_motion(&motion) else { + return Ok(()); + }; + for i in start..end { + let Some(gr) = self.grapheme_at(i) else { + continue; + }; + if gr.len() > 1 || gr.is_empty() { + continue; + } + let ch = gr.chars().next().unwrap(); + if !ch.is_alphabetic() { + continue; + } + let mut buf = [0u8; 4]; + let new = match transform { + CaseTransform::Toggle => { + if ch.is_ascii_lowercase() { + ch.to_ascii_uppercase().encode_utf8(&mut buf) + } else { + ch.to_ascii_lowercase().encode_utf8(&mut buf) + } + } + CaseTransform::Lower => { + if ch.is_ascii_uppercase() { + ch.to_ascii_lowercase().encode_utf8(&mut buf) + } else { + ch.encode_utf8(&mut buf) + } + } + CaseTransform::Upper => { + if ch.is_ascii_lowercase() { + ch.to_ascii_uppercase().encode_utf8(&mut buf) + } else { + ch.encode_utf8(&mut buf) + } + } + }; + self.replace_at(i, new); + } + self.cursor.set(start); + Ok(()) + } + fn verb_undo_redo(&mut self, verb: Verb) -> ShResult<()> { + let (edit_provider, edit_receiver) = match verb { + Verb::Redo => (&mut self.redo_stack, &mut self.undo_stack), + Verb::Undo => (&mut self.undo_stack, &mut self.redo_stack), + _ => unreachable!(), + }; + let Some(edit) = edit_provider.pop() else { + return Ok(()); + }; + let Edit { + pos, + cursor_pos, + old, + new, + merging: _, + } = edit; + self.buffer.replace_range(pos..pos + new.len(), &old); + let new_cursor_pos = self.cursor.get(); + self.cursor.set(cursor_pos); + edit_receiver.push(Edit { + pos, + cursor_pos: new_cursor_pos, + old: new, + new: old, + merging: false, + }); + self.update_graphemes(); + Ok(()) + } + fn verb_put(&mut self, anchor: Anchor, register: RegisterName) -> ShResult<()> { + let Some(content) = register.read_from_register() else { + return Ok(()); + }; + if content.is_empty() { + return Ok(()); + } + if let Some(range) = self.select_range() { + let register_text = self.drain_inclusive(range.0..=range.1); + write_register(None, RegisterContent::Span(register_text)); + let text = content.as_str(); + self.insert_str_at(range.0, text); + self.cursor.set(range.0 + content.char_count()); + self.select_range = None; + self.update_graphemes(); + return Ok(()); + } + match content { + RegisterContent::Span(ref text) => match anchor { + Anchor::After => { + let insert_idx = self + .cursor + .get() + .saturating_add(1) + .min(self.grapheme_indices().len()); + let offset = text.len().max(1); + self.insert_str_at(insert_idx, text); + self.cursor.add(offset); + } + Anchor::Before => { + let insert_idx = self.cursor.get(); + self.insert_str_at(insert_idx, text); + self.cursor.add(text.len().saturating_sub(1)); + } + }, + RegisterContent::Line(ref text) => { + let insert_idx = match anchor { + Anchor::After => self.end_of_line(), + Anchor::Before => self.start_of_line(), + }; + let mut full = text.to_string(); + let mut offset = 0; + match anchor { + Anchor::After => { + if self.grapheme_before(insert_idx).is_none_or(|gr| gr != "\n") { + full = format!("\n{text}"); + offset += 1; + } + if self.grapheme_at(insert_idx).is_some_and(|gr| gr != "\n") { + full = format!("{full}\n"); + } + } + Anchor::Before => { + full = format!("{full}\n"); + } + } + self.insert_str_at(insert_idx, &full); + self.cursor.set(insert_idx + offset); + } + RegisterContent::Empty => {} + } + Ok(()) + } + fn verb_swap_visual_anchor(&mut self) { + if let Some((start, end)) = self.select_range + && let Some(mut mode) = self.select_mode + { + mode.invert_anchor(); + let new_cursor_pos = match mode.anchor() { + SelectAnchor::Start => start, + SelectAnchor::End => end, + }; + self.cursor.set(new_cursor_pos); + self.select_mode = Some(mode) + } + } + fn verb_join_lines(&mut self) -> ShResult<()> { + let start = self.start_of_line(); + let Some((_, mut end)) = self.nth_next_line(1) else { + return Ok(()); + }; + end = end.saturating_sub(1); + let mut last_was_whitespace = false; + for i in start..end { + let Some(gr) = self.grapheme_at(i) else { + continue; + }; + if gr == "\n" { + if last_was_whitespace { + self.remove(i); + } else { + self.force_replace_at(i, " "); + } + last_was_whitespace = false; + let strip_pos = if self.grapheme_at(i) == Some(" ") { + i + 1 + } else { + i + }; + while self.grapheme_at(strip_pos) == Some("\t") { + self.remove(strip_pos); + } + self.cursor.set(i); + continue; + } + last_was_whitespace = is_whitespace(gr); + } + Ok(()) + } + fn verb_insert_char(&mut self, ch: char) { + self.insert_at_cursor(ch); + self.cursor.add(1); + let before = self.auto_indent_level; + if read_shopts(|o| o.prompt.auto_indent) + && let Some(line_content) = self.this_line_content() + { + match line_content.trim() { + "esac" | "done" | "fi" | "}" => { + self.calc_indent_level(); + if self.auto_indent_level < before { + let delta = before - self.auto_indent_level; + let line_start = self.start_of_line(); + for _ in 0..delta { + if self.grapheme_at(line_start).is_some_and(|gr| gr == "\t") { + self.remove(line_start); + if !self.cursor_at_max() { + self.cursor.sub(1); + } + } + } + } + } + _ => {} + } + } + } + fn verb_insert(&mut self, string: String) { + self.insert_str_at_cursor(&string); + let graphemes = string.graphemes(true).count(); + self.cursor.add(graphemes); + } + fn verb_indent(&mut self, motion: MotionKind) -> ShResult<()> { + let Some((start, end)) = self.range_from_motion(&motion) else { + return Ok(()); + }; + self.insert_at(start, '\t'); + self.update_graphemes(); + let end = end.clamp(0, self.grapheme_indices().len()); + let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter(); + while let Some(idx) = range_indices.next() { + let gr = self.grapheme_at(idx).unwrap(); + if gr == "\n" { + let Some(idx) = range_indices.next() else { + self.push('\t'); + break; + }; + self.insert_at(idx, '\t'); + } + } + match motion { + MotionKind::ExclusiveWithTargetCol((_, _), pos) + | MotionKind::InclusiveWithTargetCol((_, _), pos) => { + self.cursor.set(start); + let end = self.end_of_line(); + self.cursor.add(end.min(pos + 1)); + } + _ => self.cursor.set(start), + } + Ok(()) + } + fn verb_dedent(&mut self, motion: MotionKind) -> ShResult<()> { + let Some((start, mut end)) = self.range_from_motion(&motion) else { + return Ok(()); + }; + if self.grapheme_at(start) == Some("\t") { + self.remove(start); + } + end = end.min(self.grapheme_indices().len().saturating_sub(1)); + let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter(); + while let Some(idx) = range_indices.next() { + let gr = self.grapheme_at(idx).unwrap(); + if gr == "\n" { + let Some(idx) = range_indices.next() else { + if self.grapheme_at(self.grapheme_indices().len().saturating_sub(1)) == Some("\t") { + self.remove(self.grapheme_indices().len().saturating_sub(1)); + } + break; + }; + if self.grapheme_at(idx) == Some("\t") { + self.remove(idx); + } + } + } + match motion { + MotionKind::ExclusiveWithTargetCol((_, _), pos) + | MotionKind::InclusiveWithTargetCol((_, _), pos) => { + self.cursor.set(start); + let end = self.end_of_line(); + self.cursor.add(end.min(pos)); + } + _ => self.cursor.set(start), + } + Ok(()) + } + fn verb_insert_mode_line_break(&mut self, anchor: Anchor) -> ShResult<()> { + let (mut start, end) = self.this_line_exclusive(); + let auto_indent = read_shopts(|o| o.prompt.auto_indent); + if start == 0 && end == self.cursor.max { + match anchor { + Anchor::After => { + self.push('\n'); + if auto_indent { + self.calc_indent_level(); + for _ in 0..self.auto_indent_level { + self.push('\t'); + } + } + self.cursor.set(self.cursor_max()); + return Ok(()); + } + Anchor::Before => { + if auto_indent { + self.calc_indent_level(); + for _ in 0..self.auto_indent_level { + self.insert_at(0, '\t'); + } + } + self.insert_at(0, '\n'); + self.cursor.set(0); + return Ok(()); + } + } + } + start = start.saturating_sub(1).min(self.cursor.max); + match anchor { + Anchor::After => { + self.cursor.set(end); + self.insert_newline_with_indent(auto_indent); + } + Anchor::Before => { + self.cursor.set(start); + self.insert_newline_with_indent(auto_indent); + } + } + Ok(()) + } + fn insert_newline_with_indent(&mut self, auto_indent: bool) { + self.insert_at_cursor('\n'); + self.cursor.add(1); + if auto_indent { + self.calc_indent_level(); + for _ in 0..self.auto_indent_level { + self.insert_at_cursor('\t'); + self.cursor.add(1); + } + } + } + fn verb_accept_line_or_newline(&mut self) -> ShResult<()> { + if self.cursor.exclusive { + let motion = self.eval_motion(None, MotionCmd(1, Motion::LineDownCharwise)); + self.apply_motion(motion); + return Ok(()); + } + let auto_indent = read_shopts(|o| o.prompt.auto_indent); + self.insert_newline_with_indent(auto_indent); + Ok(()) + } + fn verb_adjust_number(&mut self, inc: i64) -> ShResult<()> { + let (s, e) = if let Some(r) = self.select_range() { + r + } else if let Some(r) = self.number_at_cursor() { + r + } else { + return Ok(()); + }; + let end = if self.select_range().is_some() { + if e < self.grapheme_indices().len() - 1 { + e + } else { + e + 1 + } + } else { + (e + 1).min(self.grapheme_indices().len()) + }; + let word = self.slice(s..end).unwrap_or_default().to_lowercase(); + let byte_start = self.index_byte_pos(s); + let byte_end = if end >= self.grapheme_indices().len() { + self.buffer.len() + } else { + self.index_byte_pos(end) + }; + + if word.starts_with("0x") { + let body = word.strip_prefix("0x").unwrap(); + let width = body.len(); + if let Ok(num) = i64::from_str_radix(body, 16) { + let new_num = num + inc; + self + .buffer + .replace_range(byte_start..byte_end, &format!("0x{new_num:0>width$x}")); + self.update_graphemes(); + self.cursor.set(s); + } + } else if word.starts_with("0b") { + let body = word.strip_prefix("0b").unwrap(); + let width = body.len(); + if let Ok(num) = i64::from_str_radix(body, 2) { + let new_num = num + inc; + self + .buffer + .replace_range(byte_start..byte_end, &format!("0b{new_num:0>width$b}")); + self.update_graphemes(); + self.cursor.set(s); + } + } else if word.starts_with("0o") { + let body = word.strip_prefix("0o").unwrap(); + let width = body.len(); + if let Ok(num) = i64::from_str_radix(body, 8) { + let new_num = num + inc; + self + .buffer + .replace_range(byte_start..byte_end, &format!("0o{new_num:0>width$o}")); + self.update_graphemes(); + self.cursor.set(s); + } + } else if let Ok(num) = word.parse::() { + let width = word.len(); + let new_num = num + inc; + let num_fmt = if new_num < 0 { + let abs = new_num.unsigned_abs(); + let digit_width = if num < 0 { width - 1 } else { width }; + format!("-{abs:0>digit_width$}") + } else if num < 0 { + let digit_width = width - 1; + format!("{new_num:0>digit_width$}") + } else { + format!("{new_num:0>width$}") + }; + self.buffer.replace_range(byte_start..byte_end, &num_fmt); + self.update_graphemes(); + self.cursor.set(s); + } + Ok(()) + } + fn verb_shell_cmd(&mut self, cmd: String) -> ShResult<()> { + let mut vars = HashSet::new(); + vars.insert("_BUFFER".into()); + vars.insert("_CURSOR".into()); + vars.insert("_ANCHOR".into()); + let _guard = var_ctx_guard(vars); + + let mut buf = self.as_str().to_string(); + let mut cursor = self.cursor.get(); + let mut anchor = self + .select_range() + .map(|r| if r.0 != cursor { r.0 } else { r.1 }) + .unwrap_or(cursor); + + write_vars(|v| { + v.set_var("_BUFFER", VarKind::Str(buf.clone()), VarFlags::EXPORT)?; + v.set_var( + "_CURSOR", + VarKind::Str(cursor.to_string()), + VarFlags::EXPORT, + )?; + v.set_var( + "_ANCHOR", + VarKind::Str(anchor.to_string()), + VarFlags::EXPORT, + ) + })?; + + RawModeGuard::with_cooked_mode(|| exec_input(cmd, None, true, Some("".into())))?; + + let keys = write_vars(|v| { + buf = v.take_var("_BUFFER"); + cursor = v.take_var("_CURSOR").parse().unwrap_or(cursor); + anchor = v.take_var("_ANCHOR").parse().unwrap_or(anchor); + v.take_var("_KEYS") + }); + + self.set_buffer(buf); + self.update_graphemes(); + self.cursor.set_max(self.buffer.graphemes(true).count()); + self.cursor.set(cursor); + if anchor != cursor && self.select_range.is_some() { + self.select_range = Some(ordered(cursor, anchor)); + } + if !keys.is_empty() { + write_meta(|m| m.set_pending_widget_keys(&keys)) + } + Ok(()) + } #[allow(clippy::unnecessary_to_owned)] pub fn exec_verb( &mut self, @@ -2608,608 +3206,28 @@ impl LineBuf { register: RegisterName, ) -> ShResult<()> { match verb { - Verb::Delete | Verb::Yank | Verb::Change => { - let Some((start, end)) = self.range_from_motion(&motion) else { - return Ok(()); - }; - - let mut do_indent = false; - if verb == Verb::Change && (start, end) == self.this_line_exclusive() { - do_indent = read_shopts(|o| o.prompt.auto_indent); - } - - let mut text = if verb == Verb::Yank { - self - .slice(start..end) - .map(|c| c.to_string()) - .unwrap_or_default() - } else if start == self.grapheme_indices().len() && end == self.grapheme_indices().len() { - // user is in normal mode and pressed 'x' on the last char in the buffer - let drained = self.drain(end.saturating_sub(1)..end); - self.update_graphemes(); - drained - } else { - let drained = self.drain(start..end); - self.update_graphemes(); - drained - }; - let is_linewise = matches!( - motion, - MotionKind::InclusiveWithTargetCol(..) | MotionKind::ExclusiveWithTargetCol(..) - ) || matches!(self.select_mode, Some(SelectMode::Line(_))); - let register_content = if is_linewise { - if text.ends_with('\n') && !text.is_empty() { - text = text.strip_suffix('\n').unwrap().to_string(); - } - RegisterContent::Line(text) - } else { - RegisterContent::Span(text) - }; - register.write_to_register(register_content); - self.cursor.set(start); - if do_indent { - self.calc_indent_level(); - let tabs = (0..self.auto_indent_level).map(|_| '\t'); - for tab in tabs { - self.insert_at_cursor(tab); - self.cursor.add(1); - } - } else if verb != Verb::Change - && let MotionKind::InclusiveWithTargetCol((_, _), col) = motion - { - self.cursor.add(col); - } - } - Verb::Rot13 => { - let Some((start, end)) = self.range_from_motion(&motion) else { - return Ok(()); - }; - let slice = self.slice(start..end).unwrap_or_default(); - let rot13 = rot13(slice); - self.buffer.replace_range(start..end, &rot13); - self.cursor.set(start); - } - Verb::ReplaceChar(ch) => { - let mut buf = [0u8; 4]; - let new = ch.encode_utf8(&mut buf); - self.replace_at_cursor(new); - self.apply_motion(motion); - } - Verb::ReplaceCharInplace(ch, count) => { - if let Some((start,end)) = self.select_range() { - let end = (end + 1).min(self.grapheme_indices().len()); // inclusive - let replaced = ch.to_string().repeat(end.saturating_sub(start)); - self.replace_at(start, &replaced); - self.cursor.set(start); - } else { - for i in 0..count { - let mut buf = [0u8; 4]; - let new = ch.encode_utf8(&mut buf); - self.replace_at_cursor(new); - - // try to increment the cursor until we are on the last iteration - // or until we hit the end of the buffer - if i != count.saturating_sub(1) && !self.cursor.inc() { - break; - } - } - } - } - Verb::ToggleCaseInplace(count) => { - let mut did_something = false; - for i in 0..count { - let Some(gr) = self.grapheme_at_cursor() else { - return Ok(()); - }; - if gr.len() > 1 || gr.is_empty() { - return Ok(()); - } - let ch = gr.chars().next().unwrap(); - if !ch.is_alphabetic() { - return Ok(()); - } - let mut buf = [0u8; 4]; - let new = if ch.is_ascii_lowercase() { - ch.to_ascii_uppercase().encode_utf8(&mut buf) - } else { - ch.to_ascii_lowercase().encode_utf8(&mut buf) - }; - self.replace_at_cursor(new); - - // try to increment the cursor until we are on the last iteration - // or until we hit the end of the buffer - did_something = true; - if i != count.saturating_sub(1) && !self.cursor.inc() { - break; - } - } - if did_something { - self.cursor.inc(); - } - } - Verb::ToggleCaseRange => { - let Some((start, end)) = self.range_from_motion(&motion) else { - return Ok(()); - }; - for i in start..end { - let Some(gr) = self.grapheme_at(i) else { - continue; - }; - if gr.len() > 1 || gr.is_empty() { - continue; - } - let ch = gr.chars().next().unwrap(); - if !ch.is_alphabetic() { - continue; - } - let mut buf = [0u8; 4]; - let new = if ch.is_ascii_lowercase() { - ch.to_ascii_uppercase().encode_utf8(&mut buf) - } else { - ch.to_ascii_lowercase().encode_utf8(&mut buf) - }; - self.replace_at(i, new); - } - self.cursor.set(start); - } - Verb::ToLower => { - let Some((start, end)) = self.range_from_motion(&motion) else { - return Ok(()); - }; - for i in start..end { - let Some(gr) = self.grapheme_at(i) else { - continue; - }; - if gr.len() > 1 || gr.is_empty() { - continue; - } - let ch = gr.chars().next().unwrap(); - if !ch.is_alphabetic() { - continue; - } - let mut buf = [0u8; 4]; - let new = if ch.is_ascii_uppercase() { - ch.to_ascii_lowercase().encode_utf8(&mut buf) - } else { - ch.encode_utf8(&mut buf) - }; - self.replace_at(i, new); - } - self.cursor.set(start); - } - Verb::ToUpper => { - let Some((start, end)) = self.range_from_motion(&motion) else { - return Ok(()); - }; - for i in start..end { - let Some(gr) = self.grapheme_at(i) else { - continue; - }; - if gr.len() > 1 || gr.is_empty() { - continue; - } - let ch = gr.chars().next().unwrap(); - if !ch.is_alphabetic() { - continue; - } - let mut buf = [0u8; 4]; - let new = if ch.is_ascii_lowercase() { - ch.to_ascii_uppercase().encode_utf8(&mut buf) - } else { - ch.encode_utf8(&mut buf) - }; - self.replace_at(i, new); - } - self.cursor.set(start); - } - Verb::Redo | Verb::Undo => { - let (edit_provider, edit_receiver) = match verb { - // Redo = pop from redo stack, push to undo stack - Verb::Redo => (&mut self.redo_stack, &mut self.undo_stack), - // Undo = pop from undo stack, push to redo stack - Verb::Undo => (&mut self.undo_stack, &mut self.redo_stack), - _ => unreachable!(), - }; - let Some(edit) = edit_provider.pop() else { - return Ok(()); - }; - let Edit { - pos, - cursor_pos, - old, - new, - merging: _, - } = edit; - - self.buffer.replace_range(pos..pos + new.len(), &old); - let new_cursor_pos = self.cursor.get(); - - self.cursor.set(cursor_pos); - let new_edit = Edit { - pos, - cursor_pos: new_cursor_pos, - old: new, - new: old, - merging: false, - }; - edit_receiver.push(new_edit); - self.update_graphemes(); - } - Verb::RepeatLast => todo!(), - Verb::Put(anchor) => { - let Some(content) = register.read_from_register() else { - return Ok(()); - }; - if content.is_empty() { - return Ok(()); - } - if let Some(range) = self.select_range() { - let register_text = self.drain_inclusive(range.0..=range.1); - write_register(None, RegisterContent::Span(register_text)); // swap deleted text into register - - let text = content.as_str(); - self.insert_str_at(range.0, text); - self.cursor.set(range.0 + content.char_count()); - self.select_range = None; - self.update_graphemes(); - return Ok(()); - } - match content { - RegisterContent::Span(ref text) => { - match anchor { - Anchor::After => { - let insert_idx = self - .cursor - .get() - .saturating_add(1) - .min(self.grapheme_indices().len()); - let offset = text.len().max(1); - - self.insert_str_at(insert_idx, text); - self.cursor.add(offset); - }, - Anchor::Before => { - let insert_idx = self.cursor.get(); - self.insert_str_at(insert_idx, text); - self.cursor.add(text.len().saturating_sub(1)); - }, - }; - } - RegisterContent::Line(ref text) => { - let insert_idx = match anchor { - Anchor::After => self.end_of_line(), - Anchor::Before => self.start_of_line(), - }; - let mut full = text.to_string(); - let mut offset = 0; - - match anchor { - Anchor::After => { - if self.grapheme_before(insert_idx).is_none_or(|gr| gr != "\n") { - full = format!("\n{text}"); - offset += 1; - } - if self.grapheme_at(insert_idx).is_some_and(|gr| gr != "\n") { - full = format!("{full}\n"); - } - } - Anchor::Before => { - full = format!("{full}\n"); - } - } - - self.insert_str_at(insert_idx, &full); - self.cursor.set(insert_idx + offset); - } - RegisterContent::Empty => {} - } - } - Verb::SwapVisualAnchor => { - if let Some((start, end)) = self.select_range - && let Some(mut mode) = self.select_mode - { - mode.invert_anchor(); - let new_cursor_pos = match mode.anchor() { - SelectAnchor::Start => start, - SelectAnchor::End => end, - }; - self.cursor.set(new_cursor_pos); - self.select_mode = Some(mode) - } - } - Verb::JoinLines => { - let start = self.start_of_line(); - let Some((_, mut end)) = self.nth_next_line(1) else { - return Ok(()); - }; - end = end.saturating_sub(1); // exclude the last newline - let mut last_was_whitespace = false; - for i in start..end { - let Some(gr) = self.grapheme_at(i) else { - continue; - }; - if gr == "\n" { - if last_was_whitespace { - self.remove(i); - } else { - self.force_replace_at(i, " "); - } - last_was_whitespace = false; - self.cursor.set(i); - continue; - } - last_was_whitespace = is_whitespace(gr); - } - } - Verb::InsertChar(ch) => { - self.insert_at_cursor(ch); - self.cursor.add(1); - let before = self.auto_indent_level; - if read_shopts(|o| o.prompt.auto_indent) - && let Some(line_content) = self.this_line_content() - { - match line_content.trim() { - "esac" | "done" | "fi" | "}" => { - self.calc_indent_level(); - if self.auto_indent_level < before { - let delta = before - self.auto_indent_level; - let line_start = self.start_of_line(); - for _ in 0..delta { - if self.grapheme_at(line_start).is_some_and(|gr| gr == "\t") { - self.remove(line_start); - if !self.cursor_at_max() { - self.cursor.sub(1); - } - } - } - } - } - _ => { /* nothing to see here */ } - } - } - } - Verb::Insert(string) => { - self.insert_str_at_cursor(&string); - let graphemes = string.graphemes(true).count(); - self.cursor.add(graphemes); - } - Verb::Indent => { - let Some((start, end)) = self.range_from_motion(&motion) else { - return Ok(()); - }; - let move_cursor = self.cursor.get() == start; - self.insert_at(start, '\t'); - if move_cursor { - self.cursor.add(1); - } - let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter(); - while let Some(idx) = range_indices.next() { - let gr = self.grapheme_at(idx).unwrap(); - if gr == "\n" { - let Some(idx) = range_indices.next() else { - self.push('\t'); - break; - }; - self.insert_at(idx, '\t'); - } - } - - match motion { - MotionKind::ExclusiveWithTargetCol((_, _), pos) - | MotionKind::InclusiveWithTargetCol((_, _), pos) => { - self.cursor.set(start); - let end = self.end_of_line(); - self.cursor.add(end.min(pos)); - } - _ => self.cursor.set(start), - } - } - Verb::Dedent => { - let Some((start, mut end)) = self.range_from_motion(&motion) else { - return Ok(()); - }; - if self.grapheme_at(start) == Some("\t") { - self.remove(start); - } - end = end.min(self.grapheme_indices().len().saturating_sub(1)); - let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter(); - while let Some(idx) = range_indices.next() { - let gr = self.grapheme_at(idx).unwrap(); - if gr == "\n" { - let Some(idx) = range_indices.next() else { - if self.grapheme_at(self.grapheme_indices().len().saturating_sub(1)) == Some("\t") { - self.remove(self.grapheme_indices().len().saturating_sub(1)); - } - break; - }; - if self.grapheme_at(idx) == Some("\t") { - self.remove(idx); - } - } - } - - match motion { - MotionKind::ExclusiveWithTargetCol((_, _), pos) - | MotionKind::InclusiveWithTargetCol((_, _), pos) => { - self.cursor.set(start); - let end = self.end_of_line(); - self.cursor.add(end.min(pos)); - } - _ => self.cursor.set(start), - } - } - Verb::Equalize => todo!(), - Verb::InsertModeLineBreak(anchor) => { - let (mut start, end) = self.this_line_exclusive(); - let auto_indent = read_shopts(|o| o.prompt.auto_indent); - if start == 0 && end == self.cursor.max { - match anchor { - Anchor::After => { - self.push('\n'); - if auto_indent { - self.calc_indent_level(); - let tabs = (0..self.auto_indent_level).map(|_| '\t'); - for tab in tabs { - self.push(tab); - } - } - self.cursor.set(self.cursor_max()); - return Ok(()); - } - Anchor::Before => { - if auto_indent { - self.calc_indent_level(); - let tabs = (0..self.auto_indent_level).map(|_| '\t'); - for tab in tabs { - self.insert_at(0, tab); - } - } - self.insert_at(0, '\n'); - self.cursor.set(0); - return Ok(()); - } - } - } - // We want the position of the newline, or start of buffer - start = start.saturating_sub(1).min(self.cursor.max); - match anchor { - Anchor::After => { - self.cursor.set(end); - self.insert_at_cursor('\n'); - self.cursor.add(1); - if auto_indent { - self.calc_indent_level(); - let tabs = (0..self.auto_indent_level).map(|_| '\t'); - for tab in tabs { - self.insert_at_cursor(tab); - self.cursor.add(1); - } - } - } - Anchor::Before => { - self.cursor.set(start); - self.insert_at_cursor('\n'); - self.cursor.add(1); - if auto_indent { - self.calc_indent_level(); - let tabs = (0..self.auto_indent_level).map(|_| '\t'); - for tab in tabs { - self.insert_at_cursor(tab); - self.cursor.add(1); - } - } - } - } - } - Verb::AcceptLineOrNewline => { - // If this verb has reached this function, it means we have incomplete input - // and therefore must insert a newline instead of accepting the input - if self.cursor.exclusive { - // in this case we are in normal/visual mode, so we don't insert anything - // and just move down a line - let motion = self.eval_motion(None, MotionCmd(1, Motion::LineDownCharwise)); - self.apply_motion(motion); - return Ok(()); - } - let auto_indent = read_shopts(|o| o.prompt.auto_indent); - self.insert_at_cursor('\n'); - self.cursor.add(1); - if auto_indent { - self.calc_indent_level(); - let tabs = (0..self.auto_indent_level).map(|_| '\t'); - for tab in tabs { - self.insert_at_cursor(tab); - self.cursor.add(1); - } - } - } - Verb::IncrementNumber(n) | Verb::DecrementNumber(n) => { - let inc = if matches!(verb, Verb::IncrementNumber(_)) { - n as i64 - } else { - -(n as i64) - }; - let (s, e) = if let Some(r) = self.select_range() { - r - } else if let Some(r) = self.number_at_cursor() { - r - } else { - return Ok(()); - }; - let end = if self.select_range().is_some() { - if e < self.grapheme_indices().len() - 1 { - e - } else { - e + 1 - } - } else { - (e + 1).min(self.grapheme_indices().len()) - }; // inclusive → exclusive, capped at buffer len - let word = self.slice(s..end).unwrap_or_default().to_lowercase(); - - let byte_start = self.index_byte_pos(s); - let byte_end = if end >= self.grapheme_indices().len() { - self.buffer.len() - } else { - self.index_byte_pos(end) - }; - - if word.starts_with("0x") { - let body = word.strip_prefix("0x").unwrap(); - let width = body.len(); - if let Ok(num) = i64::from_str_radix(body, 16) { - let new_num = num + inc; - self - .buffer - .replace_range(byte_start..byte_end, &format!("0x{new_num:0>width$x}")); - self.update_graphemes(); - self.cursor.set(s); - } - } else if word.starts_with("0b") { - let body = word.strip_prefix("0b").unwrap(); - let width = body.len(); - if let Ok(num) = i64::from_str_radix(body, 2) { - let new_num = num + inc; - self - .buffer - .replace_range(byte_start..byte_end, &format!("0b{new_num:0>width$b}")); - self.update_graphemes(); - self.cursor.set(s); - } - } else if word.starts_with("0o") { - let body = word.strip_prefix("0o").unwrap(); - let width = body.len(); - if let Ok(num) = i64::from_str_radix(body, 8) { - let new_num = num + inc; - self - .buffer - .replace_range(byte_start..byte_end, &format!("0o{new_num:0>width$o}")); - self.update_graphemes(); - self.cursor.set(s); - } - } else if let Ok(num) = word.parse::() { - let width = word.len(); - let new_num = num + inc; - let num_fmt = if new_num < 0 { - let abs = new_num.unsigned_abs(); - let digit_width = if num < 0 { width - 1 } else { width }; - format!("-{abs:0>digit_width$}") - } else if num < 0 { - // Was negative, now positive — pad to width-1 since - // the minus sign is gone (e.g. -001 + 2 = 00001) - let digit_width = width - 1; - format!("{new_num:0>digit_width$}") - } else { - format!("{new_num:0>width$}") - }; - self - .buffer - .replace_range(byte_start..byte_end, &num_fmt); - self.update_graphemes(); - self.cursor.set(s); - } - } - + Verb::Delete | Verb::Yank | Verb::Change => self.verb_ydc(motion, register, verb)?, + Verb::Rot13 => self.verb_rot13(motion)?, + Verb::ReplaceChar(ch) => self.verb_replace_char(motion, ch)?, + Verb::ReplaceCharInplace(ch, count) => self.verb_replace_char_inplace(ch, count)?, + Verb::ToggleCaseInplace(count) => self.verb_toggle_case_inplace(count), + Verb::ToggleCaseRange => self.verb_case_transform(motion, CaseTransform::Toggle)?, + Verb::ToLower => self.verb_case_transform(motion, CaseTransform::Lower)?, + Verb::ToUpper => self.verb_case_transform(motion, CaseTransform::Upper)?, + Verb::Redo | Verb::Undo => self.verb_undo_redo(verb)?, + Verb::RepeatLast => todo!(), + Verb::Put(anchor) => self.verb_put(anchor, register)?, + Verb::SwapVisualAnchor => self.verb_swap_visual_anchor(), + Verb::JoinLines => self.verb_join_lines()?, + Verb::InsertChar(ch) => self.verb_insert_char(ch), + Verb::Insert(string) => self.verb_insert(string), + Verb::Indent => self.verb_indent(motion)?, + Verb::Dedent => self.verb_dedent(motion)?, + Verb::Equalize => todo!(), + Verb::InsertModeLineBreak(anchor) => self.verb_insert_mode_line_break(anchor)?, + Verb::AcceptLineOrNewline => self.verb_accept_line_or_newline()?, + Verb::IncrementNumber(n) => self.verb_adjust_number(n as i64)?, + Verb::DecrementNumber(n) => self.verb_adjust_number(-(n as i64))?, Verb::Complete | Verb::ExMode | Verb::EndOfFile @@ -3221,64 +3239,14 @@ impl LineBuf { | Verb::VisualModeLine | Verb::VisualModeBlock | Verb::CompleteBackward - | Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these - - Verb::ShellCmd(cmd) => { - let mut vars = HashSet::new(); - vars.insert("_BUFFER".into()); - vars.insert("_CURSOR".into()); - vars.insert("_ANCHOR".into()); - let _guard = var_ctx_guard(vars); - - let mut buf = self.as_str().to_string(); - let mut cursor = self.cursor.get(); - let mut anchor = self - .select_range() - .map(|r| if r.0 != cursor { r.0 } else { r.1 }) - .unwrap_or(cursor); - - write_vars(|v| { - v.set_var("_BUFFER", VarKind::Str(buf.clone()), VarFlags::EXPORT)?; - v.set_var( - "_CURSOR", - VarKind::Str(cursor.to_string()), - VarFlags::EXPORT, - )?; - v.set_var( - "_ANCHOR", - VarKind::Str(anchor.to_string()), - VarFlags::EXPORT, - ) - })?; - - RawModeGuard::with_cooked_mode(|| { - exec_input(cmd, None, true, Some("".into())) - })?; - - let keys = write_vars(|v| { - buf = v.take_var("_BUFFER"); - cursor = v.take_var("_CURSOR").parse().unwrap_or(cursor); - anchor = v.take_var("_ANCHOR").parse().unwrap_or(anchor); - v.take_var("_KEYS") - }); - - self.set_buffer(buf); - self.update_graphemes(); - self.cursor.set_max(self.buffer.graphemes(true).count()); - self.cursor.set(cursor); - if anchor != cursor && self.select_range.is_some() { - self.select_range = Some(ordered(cursor, anchor)); - } - if !keys.is_empty() { - write_meta(|m| m.set_pending_widget_keys(&keys)) - } - } + | Verb::VisualModeSelectLast => self.apply_motion(motion), + Verb::ShellCmd(cmd) => self.verb_shell_cmd(cmd)?, Verb::Normal(_) | Verb::Read(_) | Verb::Write(_) | Verb::Substitute(..) | Verb::RepeatSubstitute - | Verb::RepeatGlobal => {} // Ex-mode verbs handled elsewhere + | Verb::RepeatGlobal => {} } Ok(()) } diff --git a/src/readline/mod.rs b/src/readline/mod.rs index 165ba1a..a206cb3 100644 --- a/src/readline/mod.rs +++ b/src/readline/mod.rs @@ -15,7 +15,8 @@ use crate::readline::complete::{FuzzyCompleter, SelectorResponse}; use crate::readline::term::{Pos, TermReader, calc_str_width}; use crate::readline::vimode::{ViEx, ViVerbatim}; 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::{ libsh::error::ShResult, @@ -240,7 +241,7 @@ impl Default for Prompt { pub struct ShedVi { pub reader: PollReader, pub writer: TermWriter, - pub tty: RawFd, + pub tty: RawFd, pub prompt: Prompt, pub highlighter: Highlighter, @@ -266,7 +267,7 @@ impl ShedVi { reader: PollReader::new(), writer: TermWriter::new(tty), prompt, - tty, + tty, completer: Box::new(FuzzyCompleter::default()), highlighter: Highlighter::new(), mode: Box::new(ViInsert::new()), @@ -293,37 +294,37 @@ impl ShedVi { Ok(new) } - pub fn new_no_hist(prompt: Prompt, tty: RawFd) -> ShResult { - let mut new = Self { - reader: PollReader::new(), - writer: TermWriter::new(tty), - tty, - prompt, - completer: Box::new(FuzzyCompleter::default()), - highlighter: Highlighter::new(), - mode: Box::new(ViInsert::new()), - next_is_escaped: false, - saved_mode: None, - pending_keymap: Vec::new(), - old_layout: None, - repeat_action: None, - repeat_motion: None, - editor: LineBuf::new(), - history: History::empty(), - needs_redraw: true, - }; - write_vars(|v| { - v.set_var( - "SHED_VI_MODE", - VarKind::Str(new.mode.report_mode().to_string()), - VarFlags::NONE, - ) - })?; - 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.print_line(false)?; - Ok(new) - } + pub fn new_no_hist(prompt: Prompt, tty: RawFd) -> ShResult { + let mut new = Self { + reader: PollReader::new(), + writer: TermWriter::new(tty), + tty, + prompt, + completer: Box::new(FuzzyCompleter::default()), + highlighter: Highlighter::new(), + mode: Box::new(ViInsert::new()), + next_is_escaped: false, + saved_mode: None, + pending_keymap: Vec::new(), + old_layout: None, + repeat_action: None, + repeat_motion: None, + editor: LineBuf::new(), + history: History::empty(), + needs_redraw: true, + }; + write_vars(|v| { + v.set_var( + "SHED_VI_MODE", + VarKind::Str(new.mode.report_mode().to_string()), + VarFlags::NONE, + ) + })?; + 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.print_line(false)?; + Ok(new) + } pub fn with_initial(mut self, initial: &str) -> Self { self.editor = LineBuf::new().with_initial(initial, 0); @@ -335,7 +336,7 @@ impl ShedVi { /// Feed raw bytes from stdin into the reader's buffer 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) @@ -354,10 +355,10 @@ impl ShedVi { self.completer.reset_stay_active(); self.needs_redraw = true; Ok(()) - } else if self.history.fuzzy_finder.is_active() { - self.history.fuzzy_finder.reset_stay_active(); - self.needs_redraw = true; - Ok(()) + } else if self.history.fuzzy_finder.is_active() { + self.history.fuzzy_finder.reset_stay_active(); + self.needs_redraw = true; + Ok(()) } else { self.reset(full_redraw) } @@ -443,7 +444,11 @@ impl ShedVi { // Process all available keys while let Some(key) = self.reader.read_key()? { - log::debug!("Read key: {key:?} in mode {:?}, self.reader.verbatim = {}", self.mode.report_mode(), self.reader.verbatim); + log::debug!( + "Read key: {key:?} in mode {:?}, self.reader.verbatim = {}", + self.mode.report_mode(), + self.reader.verbatim + ); // If completer or history search are active, delegate input to it if self.history.fuzzy_finder.is_active() { self.print_line(false)?; @@ -688,13 +693,14 @@ impl ShedVi { .update_pending_cmd((self.editor.as_str(), self.editor.cursor.get())); let hint = self.history.get_hint(); self.editor.set_hint(hint); - write_vars(|v| { - v.set_var( - "SHED_VI_MODE", - VarKind::Str(self.mode.report_mode().to_string()), - VarFlags::NONE, - ) - }).ok(); + write_vars(|v| { + v.set_var( + "SHED_VI_MODE", + VarKind::Str(self.mode.report_mode().to_string()), + VarFlags::NONE, + ) + }) + .ok(); // 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 @@ -702,15 +708,21 @@ impl ShedVi { } Ok(None) => { let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnCompletionStart)); - let candidates = self.completer.all_candidates(); - let num_candidates = candidates.len(); - with_vars([ - ("_NUM_MATCHES".into(), Into::::into(num_candidates)), - ("_MATCHES".into(), Into::::into(candidates)), - ("_SEARCH_STR".into(), Into::::into(self.completer.token())), - ], || { - post_cmds.exec(); - }); + let candidates = self.completer.all_candidates(); + let num_candidates = candidates.len(); + with_vars( + [ + ("_NUM_MATCHES".into(), Into::::into(num_candidates)), + ("_MATCHES".into(), Into::::into(candidates)), + ( + "_SEARCH_STR".into(), + Into::::into(self.completer.token()), + ), + ], + || { + post_cmds.exec(); + }, + ); if self.completer.is_active() { write_vars(|v| { @@ -725,22 +737,21 @@ impl ShedVi { self.needs_redraw = true; self.editor.set_hint(None); } else { - self.writer.send_bell().ok(); - } + self.writer.send_bell().ok(); + } } } self.needs_redraw = true; return Ok(None); } 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(); match self.history.start_search(initial) { Some(entry) => { let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistorySelect)); - with_vars([ - ("_HIST_ENTRY".into(), entry.clone()), - ], || { + with_vars([("_HIST_ENTRY".into(), entry.clone())], || { post_cmds.exec_with(&entry); }); @@ -753,25 +764,30 @@ impl ShedVi { } None => { let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistoryOpen)); - let entries = self.history.fuzzy_finder.candidates(); - let matches = self.history.fuzzy_finder - .filtered() - .iter() - .cloned() - .map(|sc| sc.content) - .collect::>(); + let entries = self.history.fuzzy_finder.candidates(); + let matches = self + .history + .fuzzy_finder + .filtered() + .iter() + .cloned() + .map(|sc| sc.content) + .collect::>(); - let num_entries = entries.len(); - let num_matches = matches.len(); - with_vars([ - ("_ENTRIES".into(),Into::::into(entries)), - ("_NUM_ENTRIES".into(),Into::::into(num_entries)), - ("_MATCHES".into(),Into::::into(matches)), - ("_NUM_MATCHES".into(),Into::::into(num_matches)), - ("_SEARCH_STR".into(), Into::::into(initial)), - ], || { - post_cmds.exec(); - }); + let num_entries = entries.len(); + let num_matches = matches.len(); + with_vars( + [ + ("_ENTRIES".into(), Into::::into(entries)), + ("_NUM_ENTRIES".into(), Into::::into(num_entries)), + ("_MATCHES".into(), Into::::into(matches)), + ("_NUM_MATCHES".into(), Into::::into(num_matches)), + ("_SEARCH_STR".into(), Into::::into(initial)), + ], + || { + post_cmds.exec(); + }, + ); if self.history.fuzzy_finder.is_active() { write_vars(|v| { @@ -786,8 +802,8 @@ impl ShedVi { self.needs_redraw = true; self.editor.set_hint(None); } 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(); write!(buf, "\n: {pending_seq}").unwrap(); new_layout.end.row += 1; - new_layout.cursor.row += 1; + new_layout.cursor.row += 1; } write!(buf, "{}", &self.mode.cursor_style()).unwrap(); @@ -1129,7 +1145,11 @@ impl ShedVi { match cmd.verb().unwrap().1 { Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => { 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()), @@ -1217,17 +1237,17 @@ impl ShedVi { Ok(()) } - pub fn clone_mode(&self) -> Box { - match self.mode.report_mode() { - ModeReport::Normal => Box::new(ViNormal::new()), - ModeReport::Insert => Box::new(ViInsert::new()), - ModeReport::Visual => Box::new(ViVisual::new()), - ModeReport::Ex => Box::new(ViEx::new()), - ModeReport::Replace => Box::new(ViReplace::new()), - ModeReport::Verbatim => Box::new(ViVerbatim::new()), - ModeReport::Unknown => unreachable!(), - } - } + pub fn clone_mode(&self) -> Box { + match self.mode.report_mode() { + ModeReport::Normal => Box::new(ViNormal::new()), + ModeReport::Insert => Box::new(ViInsert::new()), + ModeReport::Visual => Box::new(ViVisual::new()), + ModeReport::Ex => Box::new(ViEx::new()), + ModeReport::Replace => Box::new(ViReplace::new()), + ModeReport::Verbatim => Box::new(ViVerbatim::new()), + ModeReport::Unknown => unreachable!(), + } + } pub fn exec_cmd(&mut self, mut cmd: ViCmd, from_replay: bool) -> ShResult<()> { if cmd.is_mode_transition() { @@ -1244,35 +1264,39 @@ impl ShedVi { repeat = count as u16; } - let old_mode = self.mode.report_mode(); + let old_mode = self.mode.report_mode(); for _ in 0..repeat { let cmds = cmds.clone(); for (i, cmd) in cmds.iter().enumerate() { - log::debug!("Replaying command {cmd:?} in mode {:?}, replay {i}/{repeat}", self.mode.report_mode()); + log::debug!( + "Replaying command {cmd:?} in mode {:?}, replay {i}/{repeat}", + self.mode.report_mode() + ); self.exec_cmd(cmd.clone(), true)?; // After the first command, start merging so all subsequent // edits fold into one undo entry (e.g. cw + inserted chars) if i == 0 - && let Some(edit) = self.editor.undo_stack.last_mut() { - edit.start_merge(); - } + && let Some(edit) = self.editor.undo_stack.last_mut() + { + edit.start_merge(); + } } // Stop merging at the end of the replay if let Some(edit) = self.editor.undo_stack.last_mut() { edit.stop_merge(); } - let old_mode_clone = match old_mode { - ModeReport::Normal => Box::new(ViNormal::new()) as Box, - ModeReport::Insert => Box::new(ViInsert::new()) as Box, - ModeReport::Visual => Box::new(ViVisual::new()) as Box, - ModeReport::Ex => Box::new(ViEx::new()) as Box, - ModeReport::Replace => Box::new(ViReplace::new()) as Box, - ModeReport::Verbatim => Box::new(ViVerbatim::new()) as Box, - ModeReport::Unknown => unreachable!(), - }; - self.mode = old_mode_clone; + let old_mode_clone = match old_mode { + ModeReport::Normal => Box::new(ViNormal::new()) as Box, + ModeReport::Insert => Box::new(ViInsert::new()) as Box, + ModeReport::Visual => Box::new(ViVisual::new()) as Box, + ModeReport::Ex => Box::new(ViEx::new()) as Box, + ModeReport::Replace => Box::new(ViReplace::new()) as Box, + ModeReport::Verbatim => Box::new(ViVerbatim::new()) as Box, + ModeReport::Unknown => unreachable!(), + }; + self.mode = old_mode_clone; } } CmdReplay::Single(mut cmd) => { @@ -1354,7 +1378,11 @@ impl ShedVi { 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(); let mut mode: Box = Box::new(ViNormal::new()); 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 { match class { TkRule::Pipe - | TkRule::Bang + | TkRule::Bang | TkRule::ErrPipe | TkRule::And | TkRule::Or diff --git a/src/readline/register.rs b/src/readline/register.rs index 6fffcbf..dde4329 100644 --- a/src/readline/register.rs +++ b/src/readline/register.rs @@ -7,17 +7,17 @@ pub static SAVED_REGISTERS: Mutex> = Mutex::new(None); #[cfg(test)] pub fn save_registers() { - let mut saved = SAVED_REGISTERS.lock().unwrap(); - *saved = Some(REGISTERS.lock().unwrap().clone()); + let mut saved = SAVED_REGISTERS.lock().unwrap(); + *saved = Some(REGISTERS.lock().unwrap().clone()); } #[cfg(test)] pub fn restore_registers() { - let mut saved = SAVED_REGISTERS.lock().unwrap(); - if let Some(ref registers) = *saved { - *REGISTERS.lock().unwrap() = registers.clone(); - } - *saved = None; + let mut saved = SAVED_REGISTERS.lock().unwrap(); + if let Some(ref registers) = *saved { + *REGISTERS.lock().unwrap() = registers.clone(); + } + *saved = None; } pub fn read_register(ch: Option) -> Option { diff --git a/src/readline/term.rs b/src/readline/term.rs index 66865af..d6d3b38 100644 --- a/src/readline/term.rs +++ b/src/readline/term.rs @@ -444,8 +444,8 @@ impl Perform for KeyCollector { 21 => KeyCode::F(10), 23 => KeyCode::F(11), 24 => KeyCode::F(12), - 200 => KeyCode::BracketedPasteStart, - 201 => KeyCode::BracketedPasteEnd, + 200 => KeyCode::BracketedPasteStart, + 201 => KeyCode::BracketedPasteEnd, _ => return, }; KeyEvent(key, mods) @@ -498,9 +498,9 @@ impl Perform for KeyCollector { pub struct PollReader { parser: Parser, collector: KeyCollector, - byte_buf: VecDeque, - pub verbatim_single: bool, - pub verbatim: bool, + byte_buf: VecDeque, + pub verbatim_single: bool, + pub verbatim: bool, } impl PollReader { @@ -508,42 +508,45 @@ impl PollReader { Self { parser: Parser::new(), collector: KeyCollector::new(), - byte_buf: VecDeque::new(), - verbatim_single: false, - verbatim: false, + byte_buf: VecDeque::new(), + verbatim_single: false, + verbatim: false, } } - pub fn handle_bracket_paste(&mut self) -> Option { - let end_marker = b"\x1b[201~"; - let mut raw = vec![]; - while let Some(byte) = self.byte_buf.pop_front() { - raw.push(byte); - if raw.ends_with(end_marker) { - // Strip the end marker from the raw sequence - raw.truncate(raw.len() - end_marker.len()); - let paste = String::from_utf8_lossy(&raw).to_string(); - self.verbatim = false; - return Some(KeyEvent(KeyCode::Verbatim(paste.into()), ModKeys::empty())); - } - } + pub fn handle_bracket_paste(&mut self) -> Option { + let end_marker = b"\x1b[201~"; + let mut raw = vec![]; + while let Some(byte) = self.byte_buf.pop_front() { + raw.push(byte); + if raw.ends_with(end_marker) { + // Strip the end marker from the raw sequence + raw.truncate(raw.len() - end_marker.len()); + let paste = String::from_utf8_lossy(&raw).to_string(); + self.verbatim = false; + return Some(KeyEvent(KeyCode::Verbatim(paste.into()), ModKeys::empty())); + } + } - self.verbatim = true; - self.byte_buf.extend(raw); - None - } + self.verbatim = true; + self.byte_buf.extend(raw); + None + } - pub fn read_one_verbatim(&mut self) -> Option { - if self.byte_buf.is_empty() { - return None; - } - let bytes: Vec = self.byte_buf.drain(..).collect(); - let verbatim_str = String::from_utf8_lossy(&bytes).to_string(); - Some(KeyEvent(KeyCode::Verbatim(verbatim_str.into()), ModKeys::empty())) - } + pub fn read_one_verbatim(&mut self) -> Option { + if self.byte_buf.is_empty() { + return None; + } + let bytes: Vec = self.byte_buf.drain(..).collect(); + let verbatim_str = String::from_utf8_lossy(&bytes).to_string(); + Some(KeyEvent( + KeyCode::Verbatim(verbatim_str.into()), + ModKeys::empty(), + )) + } 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 { fn read_key(&mut self) -> Result, ShErr> { - if self.verbatim_single { - if let Some(key) = self.read_one_verbatim() { - self.verbatim_single = false; - return Ok(Some(key)); - } - return Ok(None); - } - if self.verbatim { - if let Some(paste) = self.handle_bracket_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 - return Ok(None); - } 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 sequence prefix ([ or O), emit a standalone Escape - if self.byte_buf.len() == 1 - || !matches!(self.byte_buf.get(1), Some(b'[') | Some(b'O')) - { - 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]); - if let Some(key) = self.collector.pop() { - match key { - KeyEvent(KeyCode::BracketedPasteStart, _) => { - if let Some(paste) = self.handle_bracket_paste() { - return Ok(Some(paste)); - } else { - continue; - } - } - _ => return Ok(Some(key)) - } - } - } + if self.verbatim_single { + if let Some(key) = self.read_one_verbatim() { + self.verbatim_single = false; + return Ok(Some(key)); + } + return Ok(None); + } + if self.verbatim { + if let Some(paste) = self.handle_bracket_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 + return Ok(None); + } 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 sequence prefix ([ or O), emit a standalone Escape + if self.byte_buf.len() == 1 || !matches!(self.byte_buf.get(1), Some(b'[') | Some(b'O')) { + 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]); + if let Some(key) = self.collector.pop() { + match key { + KeyEvent(KeyCode::BracketedPasteStart, _) => { + if let Some(paste) = self.handle_bracket_paste() { + return Ok(Some(paste)); + } else { + continue; + } + } + _ => return Ok(Some(key)), + } + } + } Ok(None) } } @@ -844,7 +845,7 @@ impl Default for Layout { } pub struct TermWriter { - last_bell: Option, + last_bell: Option, out: RawFd, pub t_cols: Col, // terminal width buffer: String, @@ -854,7 +855,7 @@ impl TermWriter { pub fn new(out: RawFd) -> Self { let (t_cols, _) = get_win_size(out); Self { - last_bell: None, + last_bell: None, out, t_cols, buffer: String::new(), @@ -1091,24 +1092,24 @@ impl LineWriter for TermWriter { Ok(()) } - fn send_bell(&mut self) -> ShResult<()> { - 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 - // whenever i finish clearing the line using backspace. - let now = Instant::now(); + fn send_bell(&mut self) -> ShResult<()> { + 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 + // whenever i finish clearing the line using backspace. + let now = Instant::now(); - // 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 - let cooldown = rand::random_range(50..150); - let should_send = match self.last_bell { - None => true, - Some(time) => now.duration_since(time).as_millis() > cooldown, - }; - if should_send { - self.flush_write("\x07")?; - self.last_bell = Some(now); - } - } - Ok(()) - } + // 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 + let cooldown = rand::random_range(50..150); + let should_send = match self.last_bell { + None => true, + Some(time) => now.duration_since(time).as_millis() > cooldown, + }; + if should_send { + self.flush_write("\x07")?; + self.last_bell = Some(now); + } + } + Ok(()) + } } diff --git a/src/readline/tests.rs b/src/readline/tests.rs index c0ccdd3..d774605 100644 --- a/src/readline/tests.rs +++ b/src/readline/tests.rs @@ -1,7 +1,10 @@ #![allow(non_snake_case)] 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. macro_rules! vi_test { @@ -24,207 +27,209 @@ macro_rules! vi_test { } fn test_vi(initial: &str) -> (ShedVi, TestGuard) { - let g = TestGuard::new(); - let prompt = Prompt::default(); - let vi = ShedVi::new_no_hist(prompt, g.pty_slave().as_raw_fd()) - .unwrap() - .with_initial(initial); + let g = TestGuard::new(); + let prompt = Prompt::default(); + let vi = ShedVi::new_no_hist(prompt, g.pty_slave().as_raw_fd()) + .unwrap() + .with_initial(initial); - (vi, g) + (vi, g) } // Why can't I marry a programming language vi_test! { - vi_dw_basic : "hello world" => "dw" => "world", 0; - vi_dw_middle : "one two three" => "wdw" => "one three", 4; - vi_dd_whole_line : "hello world" => "dd" => "", 0; - vi_x_single : "hello" => "x" => "ello", 0; - vi_x_middle : "hello" => "llx" => "helo", 2; - vi_X_backdelete : "hello" => "llX" => "hllo", 1; - vi_h_motion : "hello" => "$h" => "hello", 3; - vi_l_motion : "hello" => "l" => "hello", 1; - vi_h_at_start : "hello" => "h" => "hello", 0; - vi_l_at_end : "hello" => "$l" => "hello", 4; - vi_w_forward : "one two three" => "w" => "one two three", 4; - vi_b_backward : "one two three" => "$b" => "one two three", 8; - vi_e_end : "one two three" => "e" => "one two three", 2; - vi_ge_back_end : "one two three" => "$ge" => "one two three", 6; - vi_w_punctuation : "foo.bar baz" => "w" => "foo.bar baz", 3; - vi_e_punctuation : "foo.bar baz" => "e" => "foo.bar baz", 2; - vi_b_punctuation : "foo.bar baz" => "$b" => "foo.bar baz", 8; - vi_w_at_eol : "hello" => "$w" => "hello", 4; - vi_b_at_bol : "hello" => "b" => "hello", 0; - vi_W_forward : "foo.bar baz" => "W" => "foo.bar baz", 8; - vi_B_backward : "foo.bar baz" => "$B" => "foo.bar baz", 8; - vi_E_end : "foo.bar baz" => "E" => "foo.bar baz", 6; - vi_gE_back_end : "one two three" => "$gE" => "one two three", 6; - vi_W_skip_punct : "one-two three" => "W" => "one-two three", 8; - vi_B_skip_punct : "one two-three" => "$B" => "one two-three", 4; - vi_E_skip_punct : "one-two three" => "E" => "one-two three", 6; - vi_dW_big : "foo.bar baz" => "dW" => "baz", 0; - vi_cW_big : "foo.bar baz" => "cWx\x1b" => "x baz", 0; - vi_zero_bol : " hello" => "$0" => " hello", 0; - vi_caret_first_char : " hello" => "$^" => " hello", 2; - vi_dollar_eol : "hello world" => "$" => "hello world", 10; - vi_g_last_nonws : "hello " => "g_" => "hello ", 4; - vi_g_no_trailing : "hello" => "g_" => "hello", 4; - vi_pipe_column : "hello world" => "6|" => "hello world", 5; - vi_pipe_col1 : "hello world" => "1|" => "hello world", 0; - vi_I_insert_front : " hello" => "Iworld \x1b" => " world hello", 7; - vi_A_append_end : "hello" => "A world\x1b" => "hello world", 10; - vi_f_find : "hello world" => "fo" => "hello world", 4; - vi_F_find_back : "hello world" => "$Fo" => "hello world", 7; - vi_t_till : "hello world" => "tw" => "hello world", 5; - vi_T_till_back : "hello world" => "$To" => "hello world", 8; - vi_f_no_match : "hello" => "fz" => "hello", 0; - vi_semicolon_repeat : "abcabc" => "fa;;" => "abcabc", 3; - vi_comma_reverse : "abcabc" => "fa;;," => "abcabc", 0; - vi_df_semicolon : "abcabc" => "fa;;dfa" => "abcabc", 3; - vi_t_at_target : "aab" => "lta" => "aab", 1; - vi_D_to_end : "hello world" => "wD" => "hello ", 5; - vi_d_dollar : "hello world" => "wd$" => "hello ", 5; - vi_d0_to_start : "hello world" => "$d0" => "d", 0; - vi_dw_multiple : "one two three" => "d2w" => "three", 0; - vi_dt_char : "hello world" => "dtw" => "world", 0; - vi_df_char : "hello world" => "dfw" => "orld", 0; - vi_dh_back : "hello" => "lldh" => "hllo", 1; - vi_dl_forward : "hello" => "dl" => "ello", 0; - vi_dge_back_end : "one two three" => "$dge" => "one tw", 5; - vi_dG_to_end : "hello world" => "dG" => "", 0; - vi_dgg_to_start : "hello world" => "$dgg" => "", 0; - vi_d_semicolon : "abcabc" => "fad;" => "abcabc", 3; - vi_cw_basic : "hello world" => "cwfoo\x1b" => "foo world", 2; - vi_C_to_end : "hello world" => "wCfoo\x1b" => "hello foo", 8; - vi_cc_whole : "hello world" => "ccfoo\x1b" => "foo", 2; - vi_ct_char : "hello world" => "ctwfoo\x1b" => "fooworld", 2; - vi_s_single : "hello" => "sfoo\x1b" => "fooello", 2; - vi_S_whole_line : "hello world" => "Sfoo\x1b" => "foo", 2; - vi_cl_forward : "hello" => "clX\x1b" => "Xello", 0; - vi_ch_backward : "hello" => "llchX\x1b" => "hXllo", 1; - vi_cb_word_back : "hello world" => "$cbfoo\x1b" => "hello food", 8; - vi_ce_word_end : "hello world" => "cefoo\x1b" => "foo world", 2; - vi_c0_to_start : "hello world" => "wc0foo\x1b" => "fooworld", 2; - vi_yw_p_basic : "hello world" => "ywwP" => "hello hello world", 11; - vi_dw_p_paste : "hello world" => "dwP" => "hello world", 5; - vi_dd_p_paste : "hello world" => "ddp" => "\nhello world", 1; - vi_y_dollar_p : "hello world" => "wy$P" => "hello worldworld", 10; - vi_ye_p : "hello world" => "yewP" => "hello helloworld", 10; - vi_yy_p : "hello world" => "yyp" => "hello world\nhello world", 12; - vi_Y_p : "hello world" => "Yp" => "hhello worldello world", 11; - vi_p_after_x : "hello" => "xp" => "ehllo", 1; - vi_P_before : "hello" => "llxP" => "hello", 2; - vi_paste_empty : "hello" => "p" => "hello", 0; - vi_r_replace : "hello" => "ra" => "aello", 0; - vi_r_middle : "hello" => "llra" => "healo", 2; - vi_r_at_end : "hello" => "$ra" => "hella", 4; - vi_r_space : "hello" => "r " => " ello", 0; - vi_r_with_count : "hello" => "3rx" => "xxxlo", 2; - vi_tilde_single : "hello" => "~" => "Hello", 1; - vi_tilde_count : "hello" => "3~" => "HELlo", 3; - vi_tilde_at_end : "HELLO" => "$~" => "HELLo", 4; - vi_tilde_mixed : "hElLo" => "5~" => "HeLlO", 4; - vi_gu_word : "HELLO world" => "guw" => "hello world", 0; - vi_gU_word : "hello WORLD" => "gUw" => "HELLO WORLD", 0; - vi_gu_dollar : "HELLO WORLD" => "gu$" => "hello world", 0; - vi_gU_dollar : "hello world" => "gU$" => "HELLO WORLD", 0; - vi_gu_0 : "HELLO WORLD" => "$gu0" => "hello worlD", 0; - vi_gU_0 : "hello world" => "$gU0" => "HELLO WORLd", 0; - vi_gtilde_word : "hello WORLD" => "g~w" => "HELLO WORLD", 0; - vi_gtilde_dollar : "hello WORLD" => "g~$" => "HELLO world", 0; - vi_diw_inner : "one two three" => "wdiw" => "one three", 4; - vi_ciw_replace : "hello world" => "ciwfoo\x1b" => "foo world", 2; - vi_daw_around : "one two three" => "wdaw" => "one three", 4; - vi_yiw_p : "hello world" => "yiwAp \x1bp" => "hello worldp hello", 17; - vi_diW_big_inner : "one-two three" => "diW" => " three", 0; - vi_daW_big_around : "one two-three end" => "wdaW" => "one end", 4; - vi_ciW_big : "one-two three" => "ciWx\x1b" => "x three", 0; - vi_di_dquote : "one \"two\" three" => "f\"di\"" => "one \"\" three", 5; - vi_da_dquote : "one \"two\" three" => "f\"da\"" => "one three", 4; - vi_ci_dquote : "one \"two\" three" => "f\"ci\"x\x1b" => "one \"x\" three", 5; - vi_di_squote : "one 'two' three" => "f'di'" => "one '' three", 5; - vi_da_squote : "one 'two' three" => "f'da'" => "one three", 4; - vi_di_backtick : "one `two` three" => "f`di`" => "one `` three", 5; - vi_da_backtick : "one `two` three" => "f`da`" => "one three", 4; - vi_ci_dquote_empty : "one \"\" three" => "f\"ci\"x\x1b" => "one \"x\" three", 5; - vi_di_paren : "one (two) three" => "f(di(" => "one () three", 5; - vi_da_paren : "one (two) three" => "f(da(" => "one three", 4; - vi_ci_paren : "one (two) three" => "f(ci(x\x1b" => "one (x) three", 5; - vi_di_brace : "one {two} three" => "f{di{" => "one {} three", 5; - vi_da_brace : "one {two} three" => "f{da{" => "one three", 4; - vi_di_bracket : "one [two] three" => "f[di[" => "one [] three", 5; - vi_da_bracket : "one [two] three" => "f[da[" => "one three", 4; - vi_di_angle : "one three" => "f "one <> three", 5; - vi_da_angle : "one three" => "f "one three", 4; - vi_di_paren_nested : "fn(a, (b, c))" => "f(di(" => "fn()", 3; - vi_di_paren_empty : "fn() end" => "f(di(" => "fn() end", 3; - vi_dib_alias : "one (two) three" => "f(dib" => "one () three", 5; - vi_diB_alias : "one {two} three" => "f{diB" => "one {} three", 5; - vi_percent_paren : "(hello) world" => "%" => "(hello) world", 6; - vi_percent_brace : "{hello} world" => "%" => "{hello} world", 6; - vi_percent_bracket : "[hello] world" => "%" => "[hello] world", 6; - vi_percent_from_close: "(hello) world" => "f)%" => "(hello) world", 0; - vi_d_percent_paren : "(hello) world" => "d%" => " world", 0; - vi_i_insert : "hello" => "iX\x1b" => "Xhello", 0; - vi_a_append : "hello" => "aX\x1b" => "hXello", 1; - vi_I_front : " hello" => "IX\x1b" => " Xhello", 2; - vi_A_end : "hello" => "AX\x1b" => "helloX", 5; - vi_o_open_below : "hello" => "oworld\x1b" => "hello\nworld", 10; - vi_O_open_above : "hello" => "Oworld\x1b" => "world\nhello", 4; - vi_empty_input : "" => "i hello\x1b" => " hello", 5; - vi_insert_escape : "hello" => "aX\x1b" => "hXello", 1; - vi_ctrl_w_del_word : "hello world" => "A\x17\x1b" => "hello ", 5; - vi_ctrl_h_backspace : "hello" => "A\x08\x1b" => "hell", 3; - vi_u_undo_delete : "hello world" => "dwu" => "hello world", 0; - vi_u_undo_change : "hello world" => "ciwfoo\x1bu" => "hello world", 0; - vi_u_undo_x : "hello" => "xu" => "hello", 0; - vi_ctrl_r_redo : "hello" => "xu\x12" => "ello", 0; - vi_u_multiple : "hello world" => "xdwu" => "ello world", 0; - vi_redo_after_undo : "hello world" => "dwu\x12" => "world", 0; - vi_dot_repeat_x : "hello" => "x." => "llo", 0; - vi_dot_repeat_dw : "one two three" => "dw." => "three", 0; - vi_dot_repeat_cw : "one two three" => "cwfoo\x1bw." => "foo foo three", 6; - vi_dot_repeat_r : "hello" => "ra.." => "aello", 0; - vi_dot_repeat_s : "hello" => "sX\x1bl." => "XXllo", 1; - vi_count_h : "hello world" => "$3h" => "hello world", 7; - vi_count_l : "hello world" => "3l" => "hello world", 3; - vi_count_w : "one two three four" => "2w" => "one two three four", 8; - vi_count_b : "one two three four" => "$2b" => "one two three four", 8; - vi_count_x : "hello" => "3x" => "lo", 0; - vi_count_dw : "one two three four" => "2dw" => "three four", 0; - vi_verb_count_motion : "one two three four" => "d2w" => "three four", 0; - vi_count_s : "hello" => "3sX\x1b" => "Xlo", 0; - vi_indent_line : "hello" => ">>" => "\thello", 0; - vi_dedent_line : "\thello" => "<<" => "hello", 0; - vi_indent_double : "hello" => ">>>>" => "\t\thello", 0; - vi_J_join_lines : "hello\nworld" => "J" => "hello world", 5; - vi_v_u_lower : "HELLO" => "vlllu" => "hellO", 0; - vi_v_U_upper : "hello" => "vlllU" => "HELLo", 0; - vi_v_d_delete : "hello world" => "vwwd" => "", 0; - vi_v_x_delete : "hello world" => "vwwx" => "", 0; - vi_v_c_change : "hello world" => "vwcfoo\x1b" => "fooorld", 2; - vi_v_y_p_yank : "hello world" => "vwyAp \x1bp" => "hello worldp hello w", 19; - vi_v_dollar_d : "hello world" => "wv$d" => "hello ", 5; - vi_v_0_d : "hello world" => "$v0d" => "", 0; - vi_ve_d : "hello world" => "ved" => " world", 0; - vi_v_o_swap : "hello world" => "vllod" => "lo world", 0; - vi_v_r_replace : "hello" => "vlllrx" => "xxxxo", 0; - vi_v_tilde_case : "hello" => "vlll~" => "HELLo", 0; - vi_V_d_delete : "hello world" => "Vd" => "", 0; - vi_V_y_p : "hello world" => "Vyp" => "hello world\nhello world", 12; - vi_V_S_change : "hello world" => "VSfoo\x1b" => "foo", 2; - vi_ctrl_a_inc : "num 5 end" => "w\x01" => "num 6 end", 4; - vi_ctrl_x_dec : "num 5 end" => "w\x18" => "num 4 end", 4; - vi_ctrl_a_negative : "num -3 end" => "w\x01" => "num -2 end", 4; - vi_ctrl_x_to_neg : "num 0 end" => "w\x18" => "num -1 end", 4; - vi_ctrl_a_count : "num 5 end" => "w3\x01" => "num 8 end", 4; - vi_ctrl_a_width : "num -00001 end" => "w\x01" => "num 00000 end", 4; - vi_delete_empty : "" => "x" => "", 0; - vi_undo_on_empty : "" => "u" => "", 0; - vi_w_single_char : "a b c" => "w" => "a b c", 2; - vi_dw_last_word : "hello" => "dw" => "", 0; - vi_dollar_single : "h" => "$" => "h", 0; - vi_caret_no_ws : "hello" => "$^" => "hello", 0; - vi_f_last_char : "hello" => "fo" => "hello", 4; - vi_r_on_space : "hello world" => "5|r-" => "hell- world", 4; - vi_vw_doesnt_crash : "" => "vw" => "", 0 + vi_dw_basic : "hello world" => "dw" => "world", 0; + vi_dw_middle : "one two three" => "wdw" => "one three", 4; + vi_dd_whole_line : "hello world" => "dd" => "", 0; + vi_x_single : "hello" => "x" => "ello", 0; + vi_x_middle : "hello" => "llx" => "helo", 2; + vi_X_backdelete : "hello" => "llX" => "hllo", 1; + vi_h_motion : "hello" => "$h" => "hello", 3; + vi_l_motion : "hello" => "l" => "hello", 1; + vi_h_at_start : "hello" => "h" => "hello", 0; + vi_l_at_end : "hello" => "$l" => "hello", 4; + vi_w_forward : "one two three" => "w" => "one two three", 4; + vi_b_backward : "one two three" => "$b" => "one two three", 8; + vi_e_end : "one two three" => "e" => "one two three", 2; + vi_ge_back_end : "one two three" => "$ge" => "one two three", 6; + vi_w_punctuation : "foo.bar baz" => "w" => "foo.bar baz", 3; + vi_e_punctuation : "foo.bar baz" => "e" => "foo.bar baz", 2; + vi_b_punctuation : "foo.bar baz" => "$b" => "foo.bar baz", 8; + vi_w_at_eol : "hello" => "$w" => "hello", 4; + vi_b_at_bol : "hello" => "b" => "hello", 0; + vi_W_forward : "foo.bar baz" => "W" => "foo.bar baz", 8; + vi_B_backward : "foo.bar baz" => "$B" => "foo.bar baz", 8; + vi_E_end : "foo.bar baz" => "E" => "foo.bar baz", 6; + vi_gE_back_end : "one two three" => "$gE" => "one two three", 6; + vi_W_skip_punct : "one-two three" => "W" => "one-two three", 8; + vi_B_skip_punct : "one two-three" => "$B" => "one two-three", 4; + vi_E_skip_punct : "one-two three" => "E" => "one-two three", 6; + vi_dW_big : "foo.bar baz" => "dW" => "baz", 0; + vi_cW_big : "foo.bar baz" => "cWx\x1b" => "x baz", 0; + vi_zero_bol : " hello" => "$0" => " hello", 0; + vi_caret_first_char : " hello" => "$^" => " hello", 2; + vi_dollar_eol : "hello world" => "$" => "hello world", 10; + vi_g_last_nonws : "hello " => "g_" => "hello ", 4; + vi_g_no_trailing : "hello" => "g_" => "hello", 4; + vi_pipe_column : "hello world" => "6|" => "hello world", 5; + vi_pipe_col1 : "hello world" => "1|" => "hello world", 0; + vi_I_insert_front : " hello" => "Iworld \x1b" => " world hello", 7; + vi_A_append_end : "hello" => "A world\x1b" => "hello world", 10; + vi_f_find : "hello world" => "fo" => "hello world", 4; + vi_F_find_back : "hello world" => "$Fo" => "hello world", 7; + vi_t_till : "hello world" => "tw" => "hello world", 5; + vi_T_till_back : "hello world" => "$To" => "hello world", 8; + vi_f_no_match : "hello" => "fz" => "hello", 0; + vi_semicolon_repeat : "abcabc" => "fa;;" => "abcabc", 3; + vi_comma_reverse : "abcabc" => "fa;;," => "abcabc", 0; + vi_df_semicolon : "abcabc" => "fa;;dfa" => "abcabc", 3; + vi_t_at_target : "aab" => "lta" => "aab", 1; + vi_D_to_end : "hello world" => "wD" => "hello ", 5; + vi_d_dollar : "hello world" => "wd$" => "hello ", 5; + vi_d0_to_start : "hello world" => "$d0" => "d", 0; + vi_dw_multiple : "one two three" => "d2w" => "three", 0; + vi_dt_char : "hello world" => "dtw" => "world", 0; + vi_df_char : "hello world" => "dfw" => "orld", 0; + vi_dh_back : "hello" => "lldh" => "hllo", 1; + vi_dl_forward : "hello" => "dl" => "ello", 0; + vi_dge_back_end : "one two three" => "$dge" => "one tw", 5; + vi_dG_to_end : "hello world" => "dG" => "", 0; + vi_dgg_to_start : "hello world" => "$dgg" => "", 0; + vi_d_semicolon : "abcabc" => "fad;" => "abcabc", 3; + vi_cw_basic : "hello world" => "cwfoo\x1b" => "foo world", 2; + vi_C_to_end : "hello world" => "wCfoo\x1b" => "hello foo", 8; + vi_cc_whole : "hello world" => "ccfoo\x1b" => "foo", 2; + vi_ct_char : "hello world" => "ctwfoo\x1b" => "fooworld", 2; + vi_s_single : "hello" => "sfoo\x1b" => "fooello", 2; + vi_S_whole_line : "hello world" => "Sfoo\x1b" => "foo", 2; + vi_cl_forward : "hello" => "clX\x1b" => "Xello", 0; + vi_ch_backward : "hello" => "llchX\x1b" => "hXllo", 1; + vi_cb_word_back : "hello world" => "$cbfoo\x1b" => "hello food", 8; + vi_ce_word_end : "hello world" => "cefoo\x1b" => "foo world", 2; + vi_c0_to_start : "hello world" => "wc0foo\x1b" => "fooworld", 2; + vi_yw_p_basic : "hello world" => "ywwP" => "hello hello world", 11; + vi_dw_p_paste : "hello world" => "dwP" => "hello world", 5; + vi_dd_p_paste : "hello world" => "ddp" => "\nhello world", 1; + vi_y_dollar_p : "hello world" => "wy$P" => "hello worldworld", 10; + vi_ye_p : "hello world" => "yewP" => "hello helloworld", 10; + vi_yy_p : "hello world" => "yyp" => "hello world\nhello world", 12; + vi_Y_p : "hello world" => "Yp" => "hhello worldello world", 11; + vi_p_after_x : "hello" => "xp" => "ehllo", 1; + vi_P_before : "hello" => "llxP" => "hello", 2; + vi_paste_empty : "hello" => "p" => "hello", 0; + vi_r_replace : "hello" => "ra" => "aello", 0; + vi_r_middle : "hello" => "llra" => "healo", 2; + vi_r_at_end : "hello" => "$ra" => "hella", 4; + vi_r_space : "hello" => "r " => " ello", 0; + vi_r_with_count : "hello" => "3rx" => "xxxlo", 2; + vi_tilde_single : "hello" => "~" => "Hello", 1; + vi_tilde_count : "hello" => "3~" => "HELlo", 3; + vi_tilde_at_end : "HELLO" => "$~" => "HELLo", 4; + vi_tilde_mixed : "hElLo" => "5~" => "HeLlO", 4; + vi_gu_word : "HELLO world" => "guw" => "hello world", 0; + vi_gU_word : "hello WORLD" => "gUw" => "HELLO WORLD", 0; + vi_gu_dollar : "HELLO WORLD" => "gu$" => "hello world", 0; + vi_gU_dollar : "hello world" => "gU$" => "HELLO WORLD", 0; + vi_gu_0 : "HELLO WORLD" => "$gu0" => "hello worlD", 0; + vi_gU_0 : "hello world" => "$gU0" => "HELLO WORLd", 0; + vi_gtilde_word : "hello WORLD" => "g~w" => "HELLO WORLD", 0; + vi_gtilde_dollar : "hello WORLD" => "g~$" => "HELLO world", 0; + vi_diw_inner : "one two three" => "wdiw" => "one three", 4; + vi_ciw_replace : "hello world" => "ciwfoo\x1b" => "foo world", 2; + vi_daw_around : "one two three" => "wdaw" => "one three", 4; + vi_yiw_p : "hello world" => "yiwAp \x1bp" => "hello worldp hello", 17; + vi_diW_big_inner : "one-two three" => "diW" => " three", 0; + vi_daW_big_around : "one two-three end" => "wdaW" => "one end", 4; + vi_ciW_big : "one-two three" => "ciWx\x1b" => "x three", 0; + vi_di_dquote : "one \"two\" three" => "f\"di\"" => "one \"\" three", 5; + vi_da_dquote : "one \"two\" three" => "f\"da\"" => "one three", 4; + vi_ci_dquote : "one \"two\" three" => "f\"ci\"x\x1b" => "one \"x\" three", 5; + vi_di_squote : "one 'two' three" => "f'di'" => "one '' three", 5; + vi_da_squote : "one 'two' three" => "f'da'" => "one three", 4; + vi_di_backtick : "one `two` three" => "f`di`" => "one `` three", 5; + vi_da_backtick : "one `two` three" => "f`da`" => "one three", 4; + vi_ci_dquote_empty : "one \"\" three" => "f\"ci\"x\x1b" => "one \"x\" three", 5; + vi_di_paren : "one (two) three" => "f(di(" => "one () three", 5; + vi_da_paren : "one (two) three" => "f(da(" => "one three", 4; + vi_ci_paren : "one (two) three" => "f(ci(x\x1b" => "one (x) three", 5; + vi_di_brace : "one {two} three" => "f{di{" => "one {} three", 5; + vi_da_brace : "one {two} three" => "f{da{" => "one three", 4; + vi_di_bracket : "one [two] three" => "f[di[" => "one [] three", 5; + vi_da_bracket : "one [two] three" => "f[da[" => "one three", 4; + vi_di_angle : "one three" => "f "one <> three", 5; + vi_da_angle : "one three" => "f "one three", 4; + vi_di_paren_nested : "fn(a, (b, c))" => "f(di(" => "fn()", 3; + vi_di_paren_empty : "fn() end" => "f(di(" => "fn() end", 3; + vi_dib_alias : "one (two) three" => "f(dib" => "one () three", 5; + vi_diB_alias : "one {two} three" => "f{diB" => "one {} three", 5; + vi_percent_paren : "(hello) world" => "%" => "(hello) world", 6; + vi_percent_brace : "{hello} world" => "%" => "{hello} world", 6; + vi_percent_bracket : "[hello] world" => "%" => "[hello] world", 6; + vi_percent_from_close: "(hello) world" => "f)%" => "(hello) world", 0; + vi_d_percent_paren : "(hello) world" => "d%" => " world", 0; + vi_i_insert : "hello" => "iX\x1b" => "Xhello", 0; + vi_a_append : "hello" => "aX\x1b" => "hXello", 1; + vi_I_front : " hello" => "IX\x1b" => " Xhello", 2; + vi_A_end : "hello" => "AX\x1b" => "helloX", 5; + vi_o_open_below : "hello" => "oworld\x1b" => "hello\nworld", 10; + vi_O_open_above : "hello" => "Oworld\x1b" => "world\nhello", 4; + vi_empty_input : "" => "i hello\x1b" => " hello", 5; + vi_insert_escape : "hello" => "aX\x1b" => "hXello", 1; + vi_ctrl_w_del_word : "hello world" => "A\x17\x1b" => "hello ", 5; + vi_ctrl_h_backspace : "hello" => "A\x08\x1b" => "hell", 3; + vi_u_undo_delete : "hello world" => "dwu" => "hello world", 0; + vi_u_undo_change : "hello world" => "ciwfoo\x1bu" => "hello world", 0; + vi_u_undo_x : "hello" => "xu" => "hello", 0; + vi_ctrl_r_redo : "hello" => "xu\x12" => "ello", 0; + vi_u_multiple : "hello world" => "xdwu" => "ello world", 0; + vi_redo_after_undo : "hello world" => "dwu\x12" => "world", 0; + vi_dot_repeat_x : "hello" => "x." => "llo", 0; + vi_dot_repeat_dw : "one two three" => "dw." => "three", 0; + vi_dot_repeat_cw : "one two three" => "cwfoo\x1bw." => "foo foo three", 6; + vi_dot_repeat_r : "hello" => "ra.." => "aello", 0; + vi_dot_repeat_s : "hello" => "sX\x1bl." => "XXllo", 1; + vi_count_h : "hello world" => "$3h" => "hello world", 7; + vi_count_l : "hello world" => "3l" => "hello world", 3; + vi_count_w : "one two three four" => "2w" => "one two three four", 8; + vi_count_b : "one two three four" => "$2b" => "one two three four", 8; + vi_count_x : "hello" => "3x" => "lo", 0; + vi_count_dw : "one two three four" => "2dw" => "three four", 0; + vi_verb_count_motion : "one two three four" => "d2w" => "three four", 0; + vi_count_s : "hello" => "3sX\x1b" => "Xlo", 0; + vi_indent_line : "hello" => ">>" => "\thello", 1; + vi_dedent_line : "\thello" => "<<" => "hello", 0; + vi_indent_double : "hello" => ">>>>" => "\t\thello", 2; + vi_J_join_lines : "hello\nworld" => "J" => "hello world", 5; + vi_v_u_lower : "HELLO" => "vlllu" => "hellO", 0; + vi_v_U_upper : "hello" => "vlllU" => "HELLo", 0; + vi_v_d_delete : "hello world" => "vwwd" => "", 0; + vi_v_x_delete : "hello world" => "vwwx" => "", 0; + vi_v_c_change : "hello world" => "vwcfoo\x1b" => "fooorld", 2; + vi_v_y_p_yank : "hello world" => "vwyAp \x1bp" => "hello worldp hello w", 19; + vi_v_dollar_d : "hello world" => "wv$d" => "hello ", 5; + vi_v_0_d : "hello world" => "$v0d" => "", 0; + vi_ve_d : "hello world" => "ved" => " world", 0; + vi_v_o_swap : "hello world" => "vllod" => "lo world", 0; + vi_v_r_replace : "hello" => "vlllrx" => "xxxxo", 0; + vi_v_tilde_case : "hello" => "vlll~" => "HELLo", 0; + vi_V_d_delete : "hello world" => "Vd" => "", 0; + vi_V_y_p : "hello world" => "Vyp" => "hello world\nhello world", 12; + vi_V_S_change : "hello world" => "VSfoo\x1b" => "foo", 2; + vi_ctrl_a_inc : "num 5 end" => "w\x01" => "num 6 end", 4; + vi_ctrl_x_dec : "num 5 end" => "w\x18" => "num 4 end", 4; + vi_ctrl_a_negative : "num -3 end" => "w\x01" => "num -2 end", 4; + vi_ctrl_x_to_neg : "num 0 end" => "w\x18" => "num -1 end", 4; + vi_ctrl_a_count : "num 5 end" => "w3\x01" => "num 8 end", 4; + vi_ctrl_a_width : "num -00001 end" => "w\x01" => "num 00000 end", 4; + vi_delete_empty : "" => "x" => "", 0; + vi_undo_on_empty : "" => "u" => "", 0; + vi_w_single_char : "a b c" => "w" => "a b c", 2; + vi_dw_last_word : "hello" => "dw" => "", 0; + vi_dollar_single : "h" => "$" => "h", 0; + vi_caret_no_ws : "hello" => "$^" => "hello", 0; + vi_f_last_char : "hello" => "fo" => "hello", 4; + vi_r_on_space : "hello world" => "5|r-" => "hell- world", 4; + vi_vw_doesnt_crash : "" => "vw" => "", 0; + vi_indent_cursor_pos : "echo foo" => ">>" => "\techo foo", 1; + vi_join_indent_lines : "echo foo\n\t\techo bar" => "J" => "echo foo echo bar", 8 } diff --git a/src/readline/vimode/insert.rs b/src/readline/vimode/insert.rs index f55a1c4..f2b30db 100644 --- a/src/readline/vimode/insert.rs +++ b/src/readline/vimode/insert.rs @@ -13,10 +13,10 @@ impl ViInsert { pub fn new() -> Self { Self::default() } - pub fn record_cmd(mut self, cmd: ViCmd) -> Self { - self.cmds.push(cmd); - self - } + pub fn record_cmd(mut self, cmd: ViCmd) -> Self { + self.cmds.push(cmd); + self + } pub fn with_count(mut self, repeat_count: u16) -> Self { self.repeat_count = repeat_count; self @@ -65,10 +65,12 @@ impl ViMode for ViInsert { raw_seq: String::new(), flags: Default::default(), }), - E(K::Verbatim(seq), _) => { - self.pending_cmd.set_verb(VerbCmd(1, Verb::Insert(seq.to_string()))); - self.register_and_return() - } + E(K::Verbatim(seq), _) => { + self + .pending_cmd + .set_verb(VerbCmd(1, Verb::Insert(seq.to_string()))); + self.register_and_return() + } E(K::Char('W'), M::CTRL) => { self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete)); self.pending_cmd.set_motion(MotionCmd( diff --git a/src/readline/vimode/visual.rs b/src/readline/vimode/visual.rs index ba9e5a9..3933346 100644 --- a/src/readline/vimode/visual.rs +++ b/src/readline/vimode/visual.rs @@ -213,7 +213,7 @@ impl ViVisual { let ch = chars_clone.next()?; return Some(ViCmd { register, - verb: Some(VerbCmd(1, Verb::ReplaceCharInplace(ch,1))), + verb: Some(VerbCmd(1, Verb::ReplaceCharInplace(ch, 1))), motion: None, raw_seq: self.take_cmd(), flags: CmdFlags::empty(), @@ -301,13 +301,13 @@ impl ViVisual { }); } 'y' => { - return Some(ViCmd { - register, - verb: Some(VerbCmd(count, Verb::Yank)), - motion: None, - raw_seq: self.take_cmd(), - flags: CmdFlags::empty(), - }); + return Some(ViCmd { + register, + verb: Some(VerbCmd(count, Verb::Yank)), + motion: None, + raw_seq: self.take_cmd(), + flags: CmdFlags::empty(), + }); } 'd' => { chars = chars_clone; diff --git a/src/shopt.rs b/src/shopt.rs index 1bc6e9e..963f68a 100644 --- a/src/shopt.rs +++ b/src/shopt.rs @@ -185,12 +185,12 @@ impl ShOptCore { "shopt: expected an integer for max_hist value (-1 for unlimited)", )); }; - if val < -1 { - return Err(ShErr::simple( - ShErrKind::SyntaxErr, - "shopt: expected a non-negative integer or -1 for max_hist value", - )); - } + if val < -1 { + return Err(ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: expected a non-negative integer or -1 for max_hist value", + )); + } self.max_hist = val; } "interactive_comments" => { @@ -516,7 +516,8 @@ impl ShOptPrompt { Ok(Some(output)) } "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)); Ok(Some(output)) } @@ -544,7 +545,10 @@ impl Display for ShOptPrompt { output.push(format!("leader = {}", self.leader)); output.push(format!("line_numbers = {}", self.line_numbers)); 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"); @@ -575,23 +579,29 @@ mod tests { #[test] fn all_core_fields_covered() { - let ShOptCore { - dotglob, autocd, hist_ignore_dupes, max_hist, - interactive_comments, auto_hist, bell_enabled, max_recurse_depth, - xpg_echo, - } = ShOptCore::default(); - // If a field is added to the struct, this destructure fails to compile. - let _ = ( - dotglob, - autocd, - hist_ignore_dupes, - max_hist, - interactive_comments, - auto_hist, - bell_enabled, - max_recurse_depth, - xpg_echo, - ); + let ShOptCore { + dotglob, + autocd, + hist_ignore_dupes, + max_hist, + interactive_comments, + auto_hist, + bell_enabled, + max_recurse_depth, + xpg_echo, + } = ShOptCore::default(); + // If a field is added to the struct, this destructure fails to compile. + let _ = ( + dotglob, + autocd, + hist_ignore_dupes, + max_hist, + interactive_comments, + auto_hist, + bell_enabled, + max_recurse_depth, + xpg_echo, + ); } #[test] @@ -617,7 +627,7 @@ mod tests { opts.set("core.max_hist", "-1").unwrap(); 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] diff --git a/src/signal.rs b/src/signal.rs index 2c33418..615a6cd 100644 --- a/src/signal.rs +++ b/src/signal.rs @@ -165,10 +165,10 @@ pub fn reset_signals(is_fg: bool) { if sig == Signal::SIGKILL || sig == Signal::SIGSTOP { continue; } - if is_fg && (sig == Signal::SIGTTIN || sig == Signal::SIGTTOU) { - log::debug!("Not resetting SIGTTIN/SIGTTOU in foreground child"); - continue; - } + if is_fg && (sig == Signal::SIGTTIN || sig == Signal::SIGTTOU) { + log::debug!("Not resetting SIGTTIN/SIGTTOU in foreground child"); + continue; + } let _ = sigaction(sig, &default); } } diff --git a/src/state.rs b/src/state.rs index 2788cef..1ecfab7 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,5 +1,11 @@ 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}; @@ -36,7 +42,7 @@ thread_local! { pub static SHED: Shed = Shed::new(); } -#[derive(Clone,Debug)] +#[derive(Clone, Debug)] pub struct Shed { pub jobs: RefCell, pub var_scopes: RefCell, @@ -44,8 +50,8 @@ pub struct Shed { pub logic: RefCell, pub shopts: RefCell, - #[cfg(test)] - saved: RefCell>>, + #[cfg(test)] + saved: RefCell>>, } impl Shed { @@ -57,8 +63,8 @@ impl Shed { logic: RefCell::new(LogTab::new()), shopts: RefCell::new(ShOpts::default()), - #[cfg(test)] - saved: RefCell::new(None), + #[cfg(test)] + saved: RefCell::new(None), } } } @@ -71,27 +77,27 @@ impl Default for Shed { #[cfg(test)] impl Shed { - pub fn save(&self) { - let saved = Self { - jobs: RefCell::new(self.jobs.borrow().clone()), - var_scopes: RefCell::new(self.var_scopes.borrow().clone()), - meta: RefCell::new(self.meta.borrow().clone()), - logic: RefCell::new(self.logic.borrow().clone()), - shopts: RefCell::new(self.shopts.borrow().clone()), - saved: RefCell::new(None), - }; - *self.saved.borrow_mut() = Some(Box::new(saved)); - } + pub fn save(&self) { + let saved = Self { + jobs: RefCell::new(self.jobs.borrow().clone()), + var_scopes: RefCell::new(self.var_scopes.borrow().clone()), + meta: RefCell::new(self.meta.borrow().clone()), + logic: RefCell::new(self.logic.borrow().clone()), + shopts: RefCell::new(self.shopts.borrow().clone()), + saved: RefCell::new(None), + }; + *self.saved.borrow_mut() = Some(Box::new(saved)); + } - pub fn restore(&self) { - if let Some(saved) = self.saved.take() { - *self.jobs.borrow_mut() = saved.jobs.into_inner(); - *self.var_scopes.borrow_mut() = saved.var_scopes.into_inner(); - *self.meta.borrow_mut() = saved.meta.into_inner(); - *self.logic.borrow_mut() = saved.logic.into_inner(); - *self.shopts.borrow_mut() = saved.shopts.into_inner(); - } - } + pub fn restore(&self) { + if let Some(saved) = self.saved.take() { + *self.jobs.borrow_mut() = saved.jobs.into_inner(); + *self.var_scopes.borrow_mut() = saved.var_scopes.into_inner(); + *self.meta.borrow_mut() = saved.meta.into_inner(); + *self.logic.borrow_mut() = saved.logic.into_inner(); + *self.shopts.borrow_mut() = saved.shopts.into_inner(); + } + } } #[derive(Hash, Eq, PartialEq, Debug, Clone, Copy)] @@ -315,34 +321,34 @@ impl ScopeStack { }; scope.set_var(var_name, val, flags) } - pub fn get_magic_var(&self, var_name: &str) -> Option { - match var_name { - "SECONDS" => { - let shell_time = read_meta(|m| m.shell_time()); - let secs = Instant::now().duration_since(shell_time).as_secs(); - Some(secs.to_string()) - } - "EPOCHREALTIME" => { - let epoch = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or(Duration::from_secs(0)) - .as_secs_f64(); - Some(epoch.to_string()) - } - "EPOCHSECONDS" => { - let epoch = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or(Duration::from_secs(0)) - .as_secs(); - Some(epoch.to_string()) - } - "RANDOM" => { - let random = rand::random_range(0..32768); - Some(random.to_string()) - } - _ => None - } - } + pub fn get_magic_var(&self, var_name: &str) -> Option { + match var_name { + "SECONDS" => { + let shell_time = read_meta(|m| m.shell_time()); + let secs = Instant::now().duration_since(shell_time).as_secs(); + Some(secs.to_string()) + } + "EPOCHREALTIME" => { + let epoch = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or(Duration::from_secs(0)) + .as_secs_f64(); + Some(epoch.to_string()) + } + "EPOCHSECONDS" => { + let epoch = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or(Duration::from_secs(0)) + .as_secs(); + Some(epoch.to_string()) + } + "RANDOM" => { + let random = rand::random_range(0..32768); + Some(random.to_string()) + } + _ => None, + } + } pub fn get_arr_elems(&self, var_name: &str) -> ShResult> { for scope in self.scopes.iter().rev() { if scope.var_exists(var_name) @@ -468,9 +474,9 @@ impl ScopeStack { pub fn try_get_var(&self, var_name: &str) -> Option { // This version of get_var() is mainly used internally // so that we have access to Option methods - if let Some(magic) = self.get_magic_var(var_name) { - return Some(magic); - } else if let Ok(param) = var_name.parse::() { + if let Some(magic) = self.get_magic_var(var_name) { + return Some(magic); + } else if let Ok(param) = var_name.parse::() { let val = self.get_param(param); if !val.is_empty() { return Some(val); @@ -493,9 +499,9 @@ impl ScopeStack { var } pub fn get_var(&self, var_name: &str) -> String { - if let Some(magic) = self.get_magic_var(var_name) { - return magic; - } + if let Some(magic) = self.get_magic_var(var_name) { + return magic; + } if let Ok(param) = var_name.parse::() { return self.get_param(param); } @@ -528,7 +534,10 @@ impl ScopeStack { return val.clone(); } // 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() { return scope.get_param(param); } @@ -987,17 +996,17 @@ impl Display for Var { } impl From> for Var { - fn from(value: Vec) -> Self { - Self::new(VarKind::Arr(value.into()), VarFlags::NONE) - } + fn from(value: Vec) -> Self { + Self::new(VarKind::Arr(value.into()), VarFlags::NONE) + } } impl From<&[String]> for Var { - fn from(value: &[String]) -> Self { - let mut new = VecDeque::new(); - new.extend(value.iter().cloned()); - Self::new(VarKind::Arr(new), VarFlags::NONE) - } + fn from(value: &[String]) -> Self { + let mut new = VecDeque::new(); + new.extend(value.iter().cloned()); + Self::new(VarKind::Arr(new), VarFlags::NONE) + } } macro_rules! impl_var_from { @@ -1011,19 +1020,7 @@ macro_rules! impl_var_from { } impl_var_from!( - i8, - i16, - i32, - i64, - isize, - u8, - u16, - u32, - u64, - usize, - String, - &str, - bool + i8, i16, i32, i64, isize, u8, u16, u32, u64, usize, String, &str, bool ); #[derive(Default, Clone, Debug)] @@ -1064,11 +1061,11 @@ impl VarTab { params.insert(ShellParam::LastJob, "".into()); // PID of the last background job (if any) params } - fn init_sh_vars() -> HashMap { - let mut vars = HashMap::new(); - vars.insert("COMP_WORDBREAKS".into(), " \t\n\"'@><=;|&(".into()); - vars - } + fn init_sh_vars() -> HashMap { + let mut vars = HashMap::new(); + vars.insert("COMP_WORDBREAKS".into(), " \t\n\"'@><=;|&(".into()); + vars + } fn init_env() { let pathbuf_to_string = |pb: Result| pb.unwrap_or_default().to_string_lossy().to_string(); @@ -1345,8 +1342,8 @@ impl VarTab { /// A table of metadata for the shell #[derive(Clone, Debug)] pub struct MetaTab { - // Time when the shell was started, used for calculating shell uptime - shell_time: Instant, + // Time when the shell was started, used for calculating shell uptime + shell_time: Instant, // command running duration runtime_start: Option, @@ -1373,22 +1370,22 @@ pub struct MetaTab { } impl Default for MetaTab { - fn default() -> Self { - Self { - shell_time: Instant::now(), - runtime_start: None, - runtime_stop: None, - system_msg: vec![], - dir_stack: VecDeque::new(), - getopts_offset: 0, - old_path: None, - old_pwd: None, - path_cache: HashSet::new(), - cwd_cache: HashSet::new(), - comp_specs: HashMap::new(), - pending_widget_keys: vec![], - } - } + fn default() -> Self { + Self { + shell_time: Instant::now(), + runtime_start: None, + runtime_stop: None, + system_msg: vec![], + dir_stack: VecDeque::new(), + getopts_offset: 0, + old_path: None, + old_pwd: None, + path_cache: HashSet::new(), + cwd_cache: HashSet::new(), + comp_specs: HashMap::new(), + pending_widget_keys: vec![], + } + } } impl MetaTab { @@ -1398,9 +1395,9 @@ impl MetaTab { ..Default::default() } } - pub fn shell_time(&self) -> Instant { - self.shell_time - } + pub fn shell_time(&self) -> Instant { + self.shell_time + } pub fn set_pending_widget_keys(&mut self, keys: &str) { let exp = expand_keymap(keys); self.pending_widget_keys = exp; diff --git a/src/testutil.rs b/src/testutil.rs index 65da6c4..e289d3a 100644 --- a/src/testutil.rs +++ b/src/testutil.rs @@ -1,219 +1,240 @@ use std::{ - collections::{HashMap, HashSet}, - env, - os::fd::{AsRawFd, BorrowedFd, OwnedFd}, - path::PathBuf, - sync::{self, Arc, MutexGuard}, + collections::{HashMap, HashSet}, + env, + os::fd::{AsRawFd, BorrowedFd, OwnedFd}, + path::PathBuf, + sync::{self, Arc, MutexGuard}, }; use nix::{ - fcntl::{FcntlArg, OFlag, fcntl}, - pty::openpty, - sys::termios::{OutputFlags, SetArg, tcgetattr, tcsetattr}, - unistd::read, + fcntl::{FcntlArg, OFlag, fcntl}, + pty::openpty, + sys::termios::{OutputFlags, SetArg, tcgetattr, tcsetattr}, + unistd::read, }; 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(()); pub fn has_cmds(cmds: &[&str]) -> bool { - let path_cmds = MetaTab::get_cmds_in_path(); - path_cmds.iter().all(|c| cmds.iter().any(|&cmd| c == cmd)) + let path_cmds = MetaTab::get_cmds_in_path(); + path_cmds.iter().all(|c| cmds.iter().any(|&cmd| c == cmd)) } 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) -> ShResult<()> { - exec_input(input.into(), None, false, None) + exec_input(input.into(), None, false, None) } pub struct TestGuard { - _lock: MutexGuard<'static, ()>, - _redir_guard: RedirGuard, - old_cwd: PathBuf, - saved_env: HashMap, - pty_master: OwnedFd, - pty_slave: OwnedFd, + _lock: MutexGuard<'static, ()>, + _redir_guard: RedirGuard, + old_cwd: PathBuf, + saved_env: HashMap, + pty_master: OwnedFd, + pty_slave: OwnedFd, - cleanups: Vec> + cleanups: Vec>, } impl TestGuard { - pub fn new() -> Self { - let _lock = TEST_MUTEX.lock().unwrap(); + pub fn new() -> Self { + let _lock = TEST_MUTEX.lock().unwrap(); - let pty = openpty(None, None).unwrap(); - let (pty_master,pty_slave) = (pty.master, pty.slave); - let mut attrs = tcgetattr(&pty_slave).unwrap(); - attrs.output_flags &= !OutputFlags::ONLCR; - tcsetattr(&pty_slave, SetArg::TCSANOW, &attrs).unwrap(); + let pty = openpty(None, None).unwrap(); + let (pty_master, pty_slave) = (pty.master, pty.slave); + let mut attrs = tcgetattr(&pty_slave).unwrap(); + attrs.output_flags &= !OutputFlags::ONLCR; + tcsetattr(&pty_slave, SetArg::TCSANOW, &attrs).unwrap(); - let mut frame = IoFrame::new(); - frame.push( - Redir::new( - IoMode::Fd { - tgt_fd: 0, - src_fd: pty_slave.as_raw_fd(), - }, - RedirType::Input, - ), - ); - frame.push( - Redir::new( - IoMode::Fd { - tgt_fd: 1, - src_fd: pty_slave.as_raw_fd(), - }, - RedirType::Output, - ), - ); - frame.push( - Redir::new( - IoMode::Fd { - tgt_fd: 2, - src_fd: pty_slave.as_raw_fd(), - }, - RedirType::Output, - ), - ); + let mut frame = IoFrame::new(); + frame.push(Redir::new( + IoMode::Fd { + tgt_fd: 0, + src_fd: pty_slave.as_raw_fd(), + }, + RedirType::Input, + )); + frame.push(Redir::new( + IoMode::Fd { + tgt_fd: 1, + src_fd: pty_slave.as_raw_fd(), + }, + RedirType::Output, + )); + frame.push(Redir::new( + 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 saved_env = env::vars().collect(); - SHED.with(|s| s.save()); - save_registers(); - Self { - _lock, - _redir_guard, - old_cwd, - saved_env, - pty_master, - pty_slave, - cleanups: vec![], - } - } + let old_cwd = env::current_dir().unwrap(); + let saved_env = env::vars().collect(); + SHED.with(|s| s.save()); + save_registers(); + Self { + _lock, + _redir_guard, + old_cwd, + saved_env, + pty_master, + pty_slave, + cleanups: vec![], + } + } - pub fn pty_slave(&self) -> BorrowedFd { - unsafe { BorrowedFd::borrow_raw(self.pty_slave.as_raw_fd()) } - } + pub fn pty_slave(&self) -> BorrowedFd { + unsafe { BorrowedFd::borrow_raw(self.pty_slave.as_raw_fd()) } + } - pub fn add_cleanup(&mut self, f: impl FnOnce() + 'static) { - self.cleanups.push(Box::new(f)); - } + pub fn add_cleanup(&mut self, f: impl FnOnce() + 'static) { + self.cleanups.push(Box::new(f)); + } - pub fn read_output(&self) -> String { - let flags = fcntl(self.pty_master.as_raw_fd(), FcntlArg::F_GETFL).unwrap(); - let flags = OFlag::from_bits_truncate(flags); - fcntl( - self.pty_master.as_raw_fd(), - FcntlArg::F_SETFL(flags | OFlag::O_NONBLOCK), - ).unwrap(); + pub fn read_output(&self) -> String { + let flags = fcntl(self.pty_master.as_raw_fd(), FcntlArg::F_GETFL).unwrap(); + let flags = OFlag::from_bits_truncate(flags); + fcntl( + self.pty_master.as_raw_fd(), + FcntlArg::F_SETFL(flags | OFlag::O_NONBLOCK), + ) + .unwrap(); - let mut out = vec![]; - let mut buf = [0;4096]; - loop { - match read(self.pty_master.as_raw_fd(), &mut buf) { - Ok(0) => break, - Ok(n) => out.extend_from_slice(&buf[..n]), - Err(_) => break, - } - } + let mut out = vec![]; + let mut buf = [0; 4096]; + loop { + match read(self.pty_master.as_raw_fd(), &mut buf) { + Ok(0) => break, + Ok(n) => out.extend_from_slice(&buf[..n]), + Err(_) => break, + } + } - fcntl( - self.pty_master.as_raw_fd(), - FcntlArg::F_SETFL(flags), - ).unwrap(); + fcntl(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 { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } impl Drop for TestGuard { - fn drop(&mut self) { - env::set_current_dir(&self.old_cwd).ok(); - for (k, _) in env::vars() { - unsafe { env::remove_var(&k); } - } - for (k, v) in &self.saved_env { - unsafe { env::set_var(k, v); } - } - for cleanup in self.cleanups.drain(..).rev() { - cleanup(); - } - SHED.with(|s| s.restore()); - restore_registers(); - } + fn drop(&mut self) { + env::set_current_dir(&self.old_cwd).ok(); + for (k, _) in env::vars() { + unsafe { + env::remove_var(&k); + } + } + for (k, v) in &self.saved_env { + unsafe { + env::set_var(k, v); + } + } + for cleanup in self.cleanups.drain(..).rev() { + cleanup(); + } + SHED.with(|s| s.restore()); + restore_registers(); + } } pub fn get_ast(input: &str) -> ShResult> { - let log_tab = read_logic(|l| l.clone()); - let input = expand_aliases(input.into(), HashSet::new(), &log_tab); + let log_tab = read_logic(|l| l.clone()); + let input = expand_aliases(input.into(), HashSet::new(), &log_tab); - let source_name = "test_input".to_string(); - let mut parser = ParsedSrc::new(Arc::new(input)) - .with_lex_flags(LexFlags::empty()) - .with_name(source_name.clone()); + let source_name = "test_input".to_string(); + let mut parser = ParsedSrc::new(Arc::new(input)) + .with_lex_flags(LexFlags::empty()) + .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 { - pub fn assert_structure(&mut self, expected: &mut impl Iterator) -> Result<(), String> { - let mut full_structure = vec![]; - let mut before = vec![]; - let mut after = vec![]; - let mut offender = None; + pub fn assert_structure( + &mut self, + expected: &mut impl Iterator, + ) -> Result<(), String> { + let mut full_structure = vec![]; + let mut before = vec![]; + let mut after = vec![]; + let mut offender = None; - self.walk_tree(&mut |s| { - let expected_rule = expected.next(); - full_structure.push(s.class.as_nd_kind()); + self.walk_tree(&mut |s| { + let expected_rule = expected.next(); + 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()) { - 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()); - } - }); + if offender.is_none() + && expected_rule + .as_ref() + .map_or(true, |e| *e != 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 { - let expected_rule = expected_rule.map_or("(none — expected array too short)".into(), |e| format!("{e:?}")); - let full_structure_hint = full_structure.into_iter() - .map(|s| format!("\tNdKind::{s:?},")) - .collect::>() - .join("\n"); - let full_structure_hint = format!("let expected = &mut [\n{full_structure_hint}\n].into_iter();"); + 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 full_structure_hint = full_structure + .into_iter() + .map(|s| format!("\tNdKind::{s:?},")) + .collect::>() + .join("\n"); + let full_structure_hint = + format!("let expected = &mut [\n{full_structure_hint}\n].into_iter();"); - let output = [ - "Structure assertion failed!\n".into(), - format!("Expected node type '{:?}', found '{:?}'", expected_rule, nd_kind), - 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"); + let output = [ + "Structure assertion failed!\n".into(), + format!( + "Expected node type '{:?}', found '{:?}'", + expected_rule, nd_kind + ), + 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) - } else { - Ok(()) - } - } + Err(output) + } else { + Ok(()) + } + } } #[derive(Clone, Debug, PartialEq)] @@ -227,26 +248,26 @@ pub enum NdKind { Conjunction, Assignment, BraceGrp, - Negate, + Negate, Test, FuncDef, } impl crate::parse::NdRule { - pub fn as_nd_kind(&self) -> NdKind { - match self { - Self::Negate { .. } => NdKind::Negate, - Self::IfNode { .. } => NdKind::IfNode, - Self::LoopNode { .. } => NdKind::LoopNode, - Self::ForNode { .. } => NdKind::ForNode, - Self::CaseNode { .. } => NdKind::CaseNode, - Self::Command { .. } => NdKind::Command, - Self::Pipeline { .. } => NdKind::Pipeline, - Self::Conjunction { .. } => NdKind::Conjunction, - Self::Assignment { .. } => NdKind::Assignment, - Self::BraceGrp { .. } => NdKind::BraceGrp, - Self::Test { .. } => NdKind::Test, - Self::FuncDef { .. } => NdKind::FuncDef, - } - } + pub fn as_nd_kind(&self) -> NdKind { + match self { + Self::Negate { .. } => NdKind::Negate, + Self::IfNode { .. } => NdKind::IfNode, + Self::LoopNode { .. } => NdKind::LoopNode, + Self::ForNode { .. } => NdKind::ForNode, + Self::CaseNode { .. } => NdKind::CaseNode, + Self::Command { .. } => NdKind::Command, + Self::Pipeline { .. } => NdKind::Pipeline, + Self::Conjunction { .. } => NdKind::Conjunction, + Self::Assignment { .. } => NdKind::Assignment, + Self::BraceGrp { .. } => NdKind::BraceGrp, + Self::Test { .. } => NdKind::Test, + Self::FuncDef { .. } => NdKind::FuncDef, + } + } }