tightened up some logic with indenting and joining lines

added more linebuf tests

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

View File

@@ -38,7 +38,6 @@ pub fn alias(node: Node) -> ShResult<()> {
write(stdout, alias_output.as_bytes())?; // Write it write(stdout, alias_output.as_bytes())?; // Write it
} else { } else {
for (arg, span) in argv { for (arg, span) in argv {
let Some((name, body)) = arg.split_once('=') else { let Some((name, body)) = arg.split_once('=') else {
let Some(alias) = read_logic(|l| l.get_alias(&arg)) else { let Some(alias) = read_logic(|l| l.get_alias(&arg)) else {
return Err(ShErr::at( return Err(ShErr::at(
@@ -59,7 +58,10 @@ pub fn alias(node: Node) -> ShResult<()> {
return Err(ShErr::at( return Err(ShErr::at(
ShErrKind::ExecFail, ShErrKind::ExecFail,
span, span,
format!("alias: Cannot assign alias to reserved name '{}'", name.fg(next_color())), format!(
"alias: Cannot assign alias to reserved name '{}'",
name.fg(next_color())
),
)); ));
} }
write_logic(|l| l.insert_alias(name, body, span.clone())); write_logic(|l| l.insert_alias(name, body, span.clone()));

View File

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

View File

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

View File

@@ -99,7 +99,10 @@ pub mod tests {
let new_dir = env::current_dir().unwrap(); let new_dir = env::current_dir().unwrap();
assert_ne!(old_dir, new_dir); assert_ne!(old_dir, new_dir);
assert_eq!(new_dir.display().to_string(), temp_dir.path().display().to_string()); assert_eq!(
new_dir.display().to_string(),
temp_dir.path().display().to_string()
);
} }
#[test] #[test]
@@ -111,7 +114,10 @@ pub mod tests {
test_input("cd").unwrap(); test_input("cd").unwrap();
let cwd = env::current_dir().unwrap(); let cwd = env::current_dir().unwrap();
assert_eq!(cwd.display().to_string(), temp_dir.path().display().to_string()); assert_eq!(
cwd.display().to_string(),
temp_dir.path().display().to_string()
);
} }
#[test] #[test]

View File

@@ -316,10 +316,10 @@ pub fn get_comp_opts(opts: Vec<Opt>) -> ShResult<CompOpts> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::state::{self, VarFlags, VarKind, read_meta, write_vars};
use crate::testutil::{TestGuard, test_input};
use std::fs; use std::fs;
use tempfile::TempDir; use tempfile::TempDir;
use crate::state::{self, read_meta, write_vars, VarFlags, VarKind};
use crate::testutil::{TestGuard, test_input};
// ===================== complete: Registration ===================== // ===================== complete: Registration =====================

View File

@@ -13,7 +13,8 @@ use crate::{
pub fn truncate_home_path(path: String) -> String { pub fn truncate_home_path(path: String) -> String {
if let Ok(home) = env::var("HOME") if let Ok(home) = env::var("HOME")
&& path.starts_with(&home) { && path.starts_with(&home)
{
let new = path.strip_prefix(&home).unwrap(); let new = path.strip_prefix(&home).unwrap();
return format!("~{new}"); return format!("~{new}");
} }
@@ -376,8 +377,7 @@ pub fn dirs(node: Node) -> ShResult<()> {
.map(|d| d.to_string_lossy().to_string()); .map(|d| d.to_string_lossy().to_string());
if abbreviate_home { if abbreviate_home {
stack.map(truncate_home_path) stack.map(truncate_home_path).collect()
.collect()
} else { } else {
stack.collect() stack.collect()
} }
@@ -428,9 +428,12 @@ pub fn dirs(node: Node) -> ShResult<()> {
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use crate::{
state::{self, read_meta},
testutil::{TestGuard, test_input},
};
use pretty_assertions::{assert_eq, assert_ne};
use std::{env, path::PathBuf}; 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 tempfile::TempDir;
#[test] #[test]
@@ -580,8 +583,14 @@ use tempfile::TempDir;
let out = g.read_output(); let out = g.read_output();
let lines: Vec<&str> = out.split('\n').filter(|l| !l.is_empty()).collect(); let lines: Vec<&str> = out.split('\n').filter(|l| !l.is_empty()).collect();
assert_eq!(lines.len(), 2); assert_eq!(lines.len(), 2);
assert_eq!(lines[0], super::truncate_home_path(path.to_string_lossy().to_string())); assert_eq!(
assert_eq!(lines[1], super::truncate_home_path(original.to_string_lossy().to_string())); lines[0],
super::truncate_home_path(path.to_string_lossy().to_string())
);
assert_eq!(
lines[1],
super::truncate_home_path(original.to_string_lossy().to_string())
);
} }
#[test] #[test]

View File

@@ -57,7 +57,8 @@ pub fn echo(node: Node) -> ShResult<()> {
let output_channel = borrow_fd(STDOUT_FILENO); let output_channel = borrow_fd(STDOUT_FILENO);
let xpg_echo = read_shopts(|o| o.core.xpg_echo); // If true, echo expands escape sequences by default, and -E opts out let xpg_echo = read_shopts(|o| o.core.xpg_echo); // If true, echo expands escape sequences by default, and -E opts out
let use_escape = (xpg_echo && !flags.contains(EchoFlags::NO_ESCAPE)) || flags.contains(EchoFlags::USE_ESCAPE); let use_escape =
(xpg_echo && !flags.contains(EchoFlags::NO_ESCAPE)) || flags.contains(EchoFlags::USE_ESCAPE);
let mut echo_output = prepare_echo_args( let mut echo_output = prepare_echo_args(
argv argv
@@ -308,11 +309,7 @@ mod tests {
#[test] #[test]
fn prepare_multiple_args() { fn prepare_multiple_args() {
let result = prepare_echo_args( let result = prepare_echo_args(vec!["hello".into(), "world".into()], false, false).unwrap();
vec!["hello".into(), "world".into()],
false,
false,
).unwrap();
assert_eq!(result, vec!["hello", "world"]); assert_eq!(result, vec!["hello", "world"]);
} }

View File

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

View File

@@ -62,7 +62,9 @@ mod tests {
#[test] #[test]
fn exec_nonexistent_command_fails() { fn exec_nonexistent_command_fails() {
let _g = TestGuard::new(); let _g = TestGuard::new();
let result = test_input("exec _____________no_such_______command_xyz_____________hopefully______this_doesnt______exist_____somewhere_in___your______PATH__________________"); let result = test_input(
"exec _____________no_such_______command_xyz_____________hopefully______this_doesnt______exist_____somewhere_in___your______PATH__________________",
);
assert!(result.is_err()); assert!(result.is_err());
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,19 @@
use ariadne::Fmt; use ariadne::Fmt;
use nix::{libc::STDOUT_FILENO, sys::{resource::{Resource, getrlimit, setrlimit}, stat::{Mode, umask}}, unistd::write}; use nix::{
libc::STDOUT_FILENO,
sys::{
resource::{Resource, getrlimit, setrlimit},
stat::{Mode, umask},
},
unistd::write,
};
use crate::{ 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} 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] { fn ulimit_opt_spec() -> [OptSpec; 5] {
@@ -26,7 +37,7 @@ fn ulimit_opt_spec() -> [OptSpec;5] {
OptSpec { OptSpec {
opt: Opt::Short('v'), // virtual memory opt: Opt::Short('v'), // virtual memory
takes_arg: true, takes_arg: true,
} },
] ]
} }
@@ -50,39 +61,51 @@ fn get_ulimit_opts(opt: &[Opt]) -> ShResult<UlimitOpts> {
for o in opt { for o in opt {
match o { match o {
Opt::ShortWithArg('n', arg) => { Opt::ShortWithArg('n', arg) => {
opts.fds = Some(arg.parse().map_err(|_| ShErr::simple( opts.fds = Some(arg.parse().map_err(|_| {
ShErr::simple(
ShErrKind::ParseErr, ShErrKind::ParseErr,
format!("invalid argument for -n: {}", arg.fg(next_color())), format!("invalid argument for -n: {}", arg.fg(next_color())),
))?); )
}, })?);
}
Opt::ShortWithArg('u', arg) => { Opt::ShortWithArg('u', arg) => {
opts.procs = Some(arg.parse().map_err(|_| ShErr::simple( opts.procs = Some(arg.parse().map_err(|_| {
ShErr::simple(
ShErrKind::ParseErr, ShErrKind::ParseErr,
format!("invalid argument for -u: {}", arg.fg(next_color())), format!("invalid argument for -u: {}", arg.fg(next_color())),
))?); )
}, })?);
}
Opt::ShortWithArg('s', arg) => { Opt::ShortWithArg('s', arg) => {
opts.stack = Some(arg.parse().map_err(|_| ShErr::simple( opts.stack = Some(arg.parse().map_err(|_| {
ShErr::simple(
ShErrKind::ParseErr, ShErrKind::ParseErr,
format!("invalid argument for -s: {}", arg.fg(next_color())), format!("invalid argument for -s: {}", arg.fg(next_color())),
))?); )
}, })?);
}
Opt::ShortWithArg('c', arg) => { Opt::ShortWithArg('c', arg) => {
opts.core = Some(arg.parse().map_err(|_| ShErr::simple( opts.core = Some(arg.parse().map_err(|_| {
ShErr::simple(
ShErrKind::ParseErr, ShErrKind::ParseErr,
format!("invalid argument for -c: {}", arg.fg(next_color())), format!("invalid argument for -c: {}", arg.fg(next_color())),
))?); )
}, })?);
}
Opt::ShortWithArg('v', arg) => { Opt::ShortWithArg('v', arg) => {
opts.vmem = Some(arg.parse().map_err(|_| ShErr::simple( opts.vmem = Some(arg.parse().map_err(|_| {
ShErr::simple(
ShErrKind::ParseErr, ShErrKind::ParseErr,
format!("invalid argument for -v: {}", arg.fg(next_color())), format!("invalid argument for -v: {}", arg.fg(next_color())),
))?); )
}, })?);
o => return Err(ShErr::simple( }
o => {
return Err(ShErr::simple(
ShErrKind::ParseErr, ShErrKind::ParseErr,
format!("invalid option: {}", o.fg(next_color())), format!("invalid option: {}", o.fg(next_color())),
)), ));
}
} }
} }
@@ -99,68 +122,89 @@ pub fn ulimit(node: Node) -> ShResult<()> {
unreachable!() unreachable!()
}; };
let (_, opts) = get_opts_from_tokens_strict(argv, &ulimit_opt_spec()).promote_err(span.clone())?; let (_, opts) =
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 ulimit_opts = get_ulimit_opts(&opts).promote_err(span.clone())?;
if let Some(fds) = ulimit_opts.fds { if let Some(fds) = ulimit_opts.fds {
let (_, hard) = getrlimit(Resource::RLIMIT_NOFILE).map_err(|e| ShErr::at( let (_, hard) = getrlimit(Resource::RLIMIT_NOFILE).map_err(|e| {
ShErr::at(
ShErrKind::ExecFail, ShErrKind::ExecFail,
span.clone(), span.clone(),
format!("failed to get file descriptor limit: {}", e), format!("failed to get file descriptor limit: {}", e),
))?; )
setrlimit(Resource::RLIMIT_NOFILE, fds, hard).map_err(|e| ShErr::at( })?;
setrlimit(Resource::RLIMIT_NOFILE, fds, hard).map_err(|e| {
ShErr::at(
ShErrKind::ExecFail, ShErrKind::ExecFail,
span.clone(), span.clone(),
format!("failed to set file descriptor limit: {}", e), format!("failed to set file descriptor limit: {}", e),
))?; )
})?;
} }
if let Some(procs) = ulimit_opts.procs { if let Some(procs) = ulimit_opts.procs {
let (_, hard) = getrlimit(Resource::RLIMIT_NPROC).map_err(|e| ShErr::at( let (_, hard) = getrlimit(Resource::RLIMIT_NPROC).map_err(|e| {
ShErr::at(
ShErrKind::ExecFail, ShErrKind::ExecFail,
span.clone(), span.clone(),
format!("failed to get process limit: {}", e), format!("failed to get process limit: {}", e),
))?; )
setrlimit(Resource::RLIMIT_NPROC, procs, hard).map_err(|e| ShErr::at( })?;
setrlimit(Resource::RLIMIT_NPROC, procs, hard).map_err(|e| {
ShErr::at(
ShErrKind::ExecFail, ShErrKind::ExecFail,
span.clone(), span.clone(),
format!("failed to set process limit: {}", e), format!("failed to set process limit: {}", e),
))?; )
})?;
} }
if let Some(stack) = ulimit_opts.stack { if let Some(stack) = ulimit_opts.stack {
let (_, hard) = getrlimit(Resource::RLIMIT_STACK).map_err(|e| ShErr::at( let (_, hard) = getrlimit(Resource::RLIMIT_STACK).map_err(|e| {
ShErr::at(
ShErrKind::ExecFail, ShErrKind::ExecFail,
span.clone(), span.clone(),
format!("failed to get stack size limit: {}", e), format!("failed to get stack size limit: {}", e),
))?; )
setrlimit(Resource::RLIMIT_STACK, stack, hard).map_err(|e| ShErr::at( })?;
setrlimit(Resource::RLIMIT_STACK, stack, hard).map_err(|e| {
ShErr::at(
ShErrKind::ExecFail, ShErrKind::ExecFail,
span.clone(), span.clone(),
format!("failed to set stack size limit: {}", e), format!("failed to set stack size limit: {}", e),
))?; )
})?;
} }
if let Some(core) = ulimit_opts.core { if let Some(core) = ulimit_opts.core {
let (_, hard) = getrlimit(Resource::RLIMIT_CORE).map_err(|e| ShErr::at( let (_, hard) = getrlimit(Resource::RLIMIT_CORE).map_err(|e| {
ShErr::at(
ShErrKind::ExecFail, ShErrKind::ExecFail,
span.clone(), span.clone(),
format!("failed to get core dump size limit: {}", e), format!("failed to get core dump size limit: {}", e),
))?; )
setrlimit(Resource::RLIMIT_CORE, core, hard).map_err(|e| ShErr::at( })?;
setrlimit(Resource::RLIMIT_CORE, core, hard).map_err(|e| {
ShErr::at(
ShErrKind::ExecFail, ShErrKind::ExecFail,
span.clone(), span.clone(),
format!("failed to set core dump size limit: {}", e), format!("failed to set core dump size limit: {}", e),
))?; )
})?;
} }
if let Some(vmem) = ulimit_opts.vmem { if let Some(vmem) = ulimit_opts.vmem {
let (_, hard) = getrlimit(Resource::RLIMIT_AS).map_err(|e| ShErr::at( let (_, hard) = getrlimit(Resource::RLIMIT_AS).map_err(|e| {
ShErr::at(
ShErrKind::ExecFail, ShErrKind::ExecFail,
span.clone(), span.clone(),
format!("failed to get virtual memory limit: {}", e), format!("failed to get virtual memory limit: {}", e),
))?; )
setrlimit(Resource::RLIMIT_AS, vmem, hard).map_err(|e| ShErr::at( })?;
setrlimit(Resource::RLIMIT_AS, vmem, hard).map_err(|e| {
ShErr::at(
ShErrKind::ExecFail, ShErrKind::ExecFail,
span.clone(), span.clone(),
format!("failed to set virtual memory limit: {}", e), format!("failed to set virtual memory limit: {}", e),
))?; )
})?;
} }
state::set_status(0); state::set_status(0);
@@ -172,11 +216,17 @@ pub fn umask_builtin(node: Node) -> ShResult<()> {
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
} = node.class else { unreachable!() }; } = node.class
else {
unreachable!()
};
let (argv, opts) = get_opts_from_tokens_strict( let (argv, opts) = get_opts_from_tokens_strict(
argv, argv,
&[OptSpec { opt: Opt::Short('S'), takes_arg: false }], &[OptSpec {
opt: Opt::Short('S'),
takes_arg: false,
}],
)?; )?;
let argv = &argv[1..]; // skip command name let argv = &argv[1..]; // skip command name
@@ -195,17 +245,21 @@ pub fn umask_builtin(node: Node) -> ShResult<()> {
let arg = argv[0].clone(); let arg = argv[0].clone();
let raw = arg.as_str(); let raw = arg.as_str();
if raw.chars().any(|c| c.is_ascii_digit()) { if raw.chars().any(|c| c.is_ascii_digit()) {
let mode_raw = u32::from_str_radix(raw, 8).map_err(|_| ShErr::at( let mode_raw = u32::from_str_radix(raw, 8).map_err(|_| {
ShErr::at(
ShErrKind::ParseErr, ShErrKind::ParseErr,
span.clone(), span.clone(),
format!("invalid numeric umask: {}", raw.fg(next_color())), format!("invalid numeric umask: {}", raw.fg(next_color())),
))?; )
})?;
let mode = Mode::from_bits(mode_raw).ok_or_else(|| ShErr::at( let mode = Mode::from_bits(mode_raw).ok_or_else(|| {
ShErr::at(
ShErrKind::ParseErr, ShErrKind::ParseErr,
span.clone(), span.clone(),
format!("invalid umask value: {}", raw.fg(next_color())), format!("invalid umask value: {}", raw.fg(next_color())),
))?; )
})?;
umask(mode); umask(mode);
} else { } else {
@@ -337,7 +391,6 @@ pub fn umask_builtin(node: Node) -> ShResult<()> {
} }
} }
} }
} else if !opts.is_empty() { } else if !opts.is_empty() {
let u = (old_bits >> 6) & 0o7; let u = (old_bits >> 6) & 0o7;
let g = (old_bits >> 3) & 0o7; let g = (old_bits >> 3) & 0o7;
@@ -345,11 +398,7 @@ pub fn umask_builtin(node: Node) -> ShResult<()> {
let mut u_str = String::from("u="); let mut u_str = String::from("u=");
let mut g_str = String::from("g="); let mut g_str = String::from("g=");
let mut o_str = String::from("o="); let mut o_str = String::from("o=");
let stuff = [ let stuff = [(u, &mut u_str), (g, &mut g_str), (o, &mut o_str)];
(u, &mut u_str),
(g, &mut g_str),
(o, &mut o_str),
];
for (bits, out) in stuff.into_iter() { for (bits, out) in stuff.into_iter() {
if bits & 4 == 0 { if bits & 4 == 0 {
out.push('r'); out.push('r');
@@ -423,7 +472,8 @@ mod tests {
let opts = get_ulimit_opts(&[ let opts = get_ulimit_opts(&[
Opt::ShortWithArg('n', "256".into()), Opt::ShortWithArg('n', "256".into()),
Opt::ShortWithArg('c', "0".into()), Opt::ShortWithArg('c', "0".into()),
]).unwrap(); ])
.unwrap();
assert_eq!(opts.fds, Some(256)); assert_eq!(opts.fds, Some(256));
assert_eq!(opts.core, Some(0)); assert_eq!(opts.core, Some(0));
assert!(opts.procs.is_none()); assert!(opts.procs.is_none());

View File

@@ -46,9 +46,9 @@ pub fn source(node: Node) -> ShResult<()> {
pub mod tests { pub mod tests {
use std::io::Write; use std::io::Write;
use tempfile::{NamedTempFile, TempDir};
use crate::state::{self, read_logic, read_vars}; use crate::state::{self, read_logic, read_vars};
use crate::testutil::{TestGuard, test_input}; use crate::testutil::{TestGuard, test_input};
use tempfile::{NamedTempFile, TempDir};
#[test] #[test]
fn source_simple() { fn source_simple() {

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,11 @@ use std::sync::Arc;
use ariadne::Fmt; use ariadne::Fmt;
use fmt::Display; use fmt::Display;
use crate::{libsh::error::{ShErr, ShErrKind, ShResult, next_color}, parse::lex::Tk, prelude::*}; use crate::{
libsh::error::{ShErr, ShErrKind, ShResult, next_color},
parse::lex::Tk,
prelude::*,
};
pub type OptSet = Arc<[Opt]>; pub type OptSet = Arc<[Opt]>;
@@ -82,7 +86,11 @@ pub fn get_opts_from_tokens(
sort_tks(tokens, opt_specs, false) sort_tks(tokens, opt_specs, false)
} }
pub fn sort_tks(tokens: Vec<Tk>, opt_specs: &[OptSpec], strict: bool) -> ShResult<(Vec<Tk>, Vec<Opt>)> { pub fn sort_tks(
tokens: Vec<Tk>,
opt_specs: &[OptSpec],
strict: bool,
) -> ShResult<(Vec<Tk>, Vec<Opt>)> {
let mut tokens_iter = tokens let mut tokens_iter = tokens
.into_iter() .into_iter()
.map(|t| t.expand()) .map(|t| t.expand())
@@ -140,7 +148,6 @@ pub fn sort_tks(tokens: Vec<Tk>, opt_specs: &[OptSpec], strict: bool) -> ShResul
Ok((non_opts, opts)) Ok((non_opts, opts))
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::parse::lex::{LexFlags, LexStream}; use crate::parse::lex::{LexFlags, LexStream};
@@ -156,7 +163,10 @@ use super::*;
#[test] #[test]
fn parse_short_combined() { fn parse_short_combined() {
let opts = Opt::parse("-abc"); let opts = Opt::parse("-abc");
assert_eq!(opts, vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]); assert_eq!(
opts,
vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]
);
} }
#[test] #[test]
@@ -173,7 +183,12 @@ use super::*;
#[test] #[test]
fn get_opts_basic() { fn get_opts_basic() {
let words = vec!["file.txt".into(), "-v".into(), "--help".into(), "arg".into()]; let words = vec![
"file.txt".into(),
"-v".into(),
"--help".into(),
"arg".into(),
];
let (non_opts, opts) = get_opts(words); let (non_opts, opts) = get_opts(words);
assert_eq!(non_opts, vec!["file.txt", "arg"]); assert_eq!(non_opts, vec!["file.txt", "arg"]);
assert_eq!(opts, vec![Opt::Short('v'), Opt::Long("help".into())]); assert_eq!(opts, vec![Opt::Short('v'), Opt::Long("help".into())]);
@@ -191,7 +206,10 @@ use super::*;
fn get_opts_combined_short() { fn get_opts_combined_short() {
let words = vec!["-abc".into(), "file".into()]; let words = vec!["-abc".into(), "file".into()];
let (non_opts, opts) = get_opts(words); let (non_opts, opts) = get_opts(words);
assert_eq!(opts, vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]); assert_eq!(
opts,
vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]
);
assert_eq!(non_opts, vec!["file"]); assert_eq!(non_opts, vec!["file"]);
} }
@@ -215,7 +233,10 @@ use super::*;
assert_eq!(Opt::Short('v').to_string(), "-v"); assert_eq!(Opt::Short('v').to_string(), "-v");
assert_eq!(Opt::Long("help".into()).to_string(), "--help"); assert_eq!(Opt::Long("help".into()).to_string(), "--help");
assert_eq!(Opt::ShortWithArg('o', "file".into()).to_string(), "-o file"); assert_eq!(Opt::ShortWithArg('o', "file".into()).to_string(), "-o file");
assert_eq!(Opt::LongWithArg("output".into(), "file".into()).to_string(), "--output file"); assert_eq!(
Opt::LongWithArg("output".into(), "file".into()).to_string(),
"--output file"
);
} }
fn lex(input: &str) -> Vec<Tk> { fn lex(input: &str) -> Vec<Tk> {
@@ -229,8 +250,14 @@ use super::*;
let tokens = lex("file.txt --help -v arg"); let tokens = lex("file.txt --help -v arg");
let opt_spec = vec![ let opt_spec = vec![
OptSpec { opt: Opt::Short('v'), takes_arg: false }, OptSpec {
OptSpec { opt: Opt::Long("help".into()), takes_arg: false }, opt: Opt::Short('v'),
takes_arg: false,
},
OptSpec {
opt: Opt::Long("help".into()),
takes_arg: false,
},
]; ];
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap(); let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
@@ -248,9 +275,10 @@ use super::*;
fn tks_short_with_arg() { fn tks_short_with_arg() {
let tokens = lex("-o output.txt file.txt"); let tokens = lex("-o output.txt file.txt");
let opt_spec = vec![ let opt_spec = vec![OptSpec {
OptSpec { opt: Opt::Short('o'), takes_arg: true }, opt: Opt::Short('o'),
]; takes_arg: true,
}];
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap(); let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
@@ -263,13 +291,17 @@ use super::*;
fn tks_long_with_arg() { fn tks_long_with_arg() {
let tokens = lex("--output result.txt input.txt"); let tokens = lex("--output result.txt input.txt");
let opt_spec = vec![ let opt_spec = vec![OptSpec {
OptSpec { opt: Opt::Long("output".into()), takes_arg: true }, opt: Opt::Long("output".into()),
]; takes_arg: true,
}];
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap(); let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
assert_eq!(opts, vec![Opt::LongWithArg("output".into(), "result.txt".into())]); assert_eq!(
opts,
vec![Opt::LongWithArg("output".into(), "result.txt".into())]
);
let non_opts: Vec<String> = non_opts.into_iter().map(|s| s.to_string()).collect(); let non_opts: Vec<String> = non_opts.into_iter().map(|s| s.to_string()).collect();
assert!(non_opts.contains(&"input.txt".to_string())); assert!(non_opts.contains(&"input.txt".to_string()));
} }
@@ -279,8 +311,14 @@ use super::*;
let tokens = lex("-v -- -a --foo"); let tokens = lex("-v -- -a --foo");
let opt_spec = vec![ let opt_spec = vec![
OptSpec { opt: Opt::Short('v'), takes_arg: false }, OptSpec {
OptSpec { opt: Opt::Short('a'), takes_arg: false }, opt: Opt::Short('v'),
takes_arg: false,
},
OptSpec {
opt: Opt::Short('a'),
takes_arg: false,
},
]; ];
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap(); let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
@@ -296,29 +334,47 @@ use super::*;
let tokens = lex("-abc"); let tokens = lex("-abc");
let opt_spec = vec![ let opt_spec = vec![
OptSpec { opt: Opt::Short('a'), takes_arg: false }, OptSpec {
OptSpec { opt: Opt::Short('b'), takes_arg: false }, opt: Opt::Short('a'),
OptSpec { opt: Opt::Short('c'), takes_arg: false }, takes_arg: false,
},
OptSpec {
opt: Opt::Short('b'),
takes_arg: false,
},
OptSpec {
opt: Opt::Short('c'),
takes_arg: false,
},
]; ];
let (_non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap(); let (_non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
assert_eq!(opts, vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]); assert_eq!(
opts,
vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]
);
} }
#[test] #[test]
fn tks_unknown_opt_becomes_non_opt() { fn tks_unknown_opt_becomes_non_opt() {
let tokens = lex("-v -x file"); let tokens = lex("-v -x file");
let opt_spec = vec![ let opt_spec = vec![OptSpec {
OptSpec { opt: Opt::Short('v'), takes_arg: false }, opt: Opt::Short('v'),
]; takes_arg: false,
}];
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap(); let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
assert_eq!(opts, vec![Opt::Short('v')]); assert_eq!(opts, vec![Opt::Short('v')]);
// -x is not in spec, so its token goes to non_opts // -x is not in spec, so its token goes to non_opts
assert!(non_opts.into_iter().map(|s| s.to_string()).any(|s| s == "-x" || s == "file")); assert!(
non_opts
.into_iter()
.map(|s| s.to_string())
.any(|s| s == "-x" || s == "file")
);
} }
#[test] #[test]
@@ -326,16 +382,25 @@ use super::*;
let tokens = lex("-n 5 --output file.txt input"); let tokens = lex("-n 5 --output file.txt input");
let opt_spec = vec![ let opt_spec = vec![
OptSpec { opt: Opt::Short('n'), takes_arg: true }, OptSpec {
OptSpec { opt: Opt::Long("output".into()), takes_arg: true }, opt: Opt::Short('n'),
takes_arg: true,
},
OptSpec {
opt: Opt::Long("output".into()),
takes_arg: true,
},
]; ];
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap(); let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
assert_eq!(opts, vec![ assert_eq!(
opts,
vec![
Opt::ShortWithArg('n', "5".into()), Opt::ShortWithArg('n', "5".into()),
Opt::LongWithArg("output".into(), "file.txt".into()), Opt::LongWithArg("output".into(), "file.txt".into()),
]); ]
);
let non_opts: Vec<String> = non_opts.into_iter().map(|s| s.to_string()).collect(); let non_opts: Vec<String> = non_opts.into_iter().map(|s| s.to_string()).collect();
assert!(non_opts.contains(&"input".to_string())); assert!(non_opts.contains(&"input".to_string()));
} }

View File

@@ -1,10 +1,10 @@
use ariadne::{Color, Fmt}; use ariadne::{Color, Fmt};
use ariadne::{Report, ReportKind}; use ariadne::{Report, ReportKind};
use rand::TryRng; use rand::TryRng;
use yansi::Paint;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use std::fmt::Display; use std::fmt::Display;
use yansi::Paint;
use crate::procio::RedirGuard; use crate::procio::RedirGuard;
use crate::{ use crate::{

View File

@@ -40,7 +40,9 @@ use crate::procio::borrow_fd;
use crate::readline::term::{LineWriter, RawModeGuard, raw_mode}; use crate::readline::term::{LineWriter, RawModeGuard, raw_mode};
use crate::readline::{Prompt, ReadlineEvent, ShedVi}; use crate::readline::{Prompt, ReadlineEvent, ShedVi};
use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending}; use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending};
use crate::state::{AutoCmdKind, read_logic, read_shopts, source_rc, write_jobs, write_meta, write_shopts}; use crate::state::{
AutoCmdKind, read_logic, read_shopts, source_rc, write_jobs, write_meta, write_shopts,
};
use clap::Parser; use clap::Parser;
use state::write_vars; use state::write_vars;
@@ -119,7 +121,8 @@ fn main() -> ExitCode {
// Increment SHLVL, or set to 1 if not present or invalid. // Increment SHLVL, or set to 1 if not present or invalid.
// This var represents how many nested shell instances we're in // This var represents how many nested shell instances we're in
if let Ok(var) = env::var("SHLVL") if let Ok(var) = env::var("SHLVL")
&& let Ok(lvl) = var.parse::<u32>() { && let Ok(lvl) = var.parse::<u32>()
{
unsafe { env::set_var("SHLVL", (lvl + 1).to_string()) }; unsafe { env::set_var("SHLVL", (lvl + 1).to_string()) };
} else { } else {
unsafe { env::set_var("SHLVL", "1") }; unsafe { env::set_var("SHLVL", "1") };
@@ -278,7 +281,7 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
match handle_readline_event(&mut readline, Ok(prepared))? { match handle_readline_event(&mut readline, Ok(prepared))? {
true => return Ok(()), true => return Ok(()),
false => continue false => continue,
} }
} }
} }

View File

@@ -8,7 +8,28 @@ use ariadne::Fmt;
use crate::{ use crate::{
builtin::{ builtin::{
alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, autocmd::autocmd, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, getopts::getopts, intro, jobctl::{self, JobBehavior, continue_job, disown, jobs}, keymap, map, pwd::pwd, read::{self, read_builtin}, resource::{ulimit, umask_builtin}, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset} alias::{alias, unalias},
arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate},
autocmd::autocmd,
cd::cd,
complete::{compgen_builtin, complete_builtin},
dirstack::{dirs, popd, pushd},
echo::echo,
eval, exec,
flowctl::flowctl,
getopts::getopts,
intro,
jobctl::{self, JobBehavior, continue_job, disown, jobs},
keymap, map,
pwd::pwd,
read::{self, read_builtin},
resource::{ulimit, umask_builtin},
shift::shift,
shopt::shopt,
source::source,
test::double_bracket_test,
trap::{TrapTarget, trap},
varcmds::{export, local, readonly, unset},
}, },
expand::{expand_aliases, expand_case_pattern, glob_to_regex}, expand::{expand_aliases, expand_case_pattern, glob_to_regex},
jobs::{ChildProc, JobStack, attach_tty, dispatch_job}, jobs::{ChildProc, JobStack, attach_tty, dispatch_job},
@@ -136,10 +157,15 @@ pub fn exec_dash_c(input: String) -> ShResult<()> {
if nodes.len() == 1 { if nodes.len() == 1 {
let is_single_cmd = match &nodes[0].class { let is_single_cmd = match &nodes[0].class {
NdRule::Command { .. } => true, NdRule::Command { .. } => true,
NdRule::Pipeline { cmds } => cmds.len() == 1 && matches!(cmds[0].class, NdRule::Command { .. }), NdRule::Pipeline { cmds } => {
cmds.len() == 1 && matches!(cmds[0].class, NdRule::Command { .. })
}
NdRule::Conjunction { elements } => { NdRule::Conjunction { elements } => {
elements.len() == 1 && match &elements[0].cmd.class { elements.len() == 1
NdRule::Pipeline { cmds } => cmds.len() == 1 && matches!(cmds[0].class, NdRule::Command { .. }), && match &elements[0].cmd.class {
NdRule::Pipeline { cmds } => {
cmds.len() == 1 && matches!(cmds[0].class, NdRule::Command { .. })
}
NdRule::Command { .. } => true, NdRule::Command { .. } => true,
_ => false, _ => false,
} }
@@ -151,8 +177,12 @@ pub fn exec_dash_c(input: String) -> ShResult<()> {
let mut node = nodes.remove(0); let mut node = nodes.remove(0);
loop { loop {
match node.class { match node.class {
NdRule::Conjunction { mut elements } => { node = *elements.remove(0).cmd; } NdRule::Conjunction { mut elements } => {
NdRule::Pipeline { mut cmds } => { node = cmds.remove(0); } node = *elements.remove(0).cmd;
}
NdRule::Pipeline { mut cmds } => {
node = cmds.remove(0);
}
NdRule::Command { .. } => break, NdRule::Command { .. } => break,
_ => break, _ => break,
} }
@@ -259,7 +289,13 @@ impl Dispatcher {
} }
pub fn dispatch_cmd(&mut self, node: Node) -> ShResult<()> { pub fn dispatch_cmd(&mut self, node: Node) -> ShResult<()> {
let (line, _) = node.get_span().clone().line_and_col(); let (line, _) = node.get_span().clone().line_and_col();
write_vars(|v| v.set_var("LINENO", VarKind::Str((line + 1).to_string()), VarFlags::NONE))?; write_vars(|v| {
v.set_var(
"LINENO",
VarKind::Str((line + 1).to_string()),
VarFlags::NONE,
)
})?;
let Some(cmd) = node.get_command() else { let Some(cmd) = node.get_command() else {
return self.exec_cmd(node); // Argv is empty, probably an assignment return self.exec_cmd(node); // Argv is empty, probably an assignment
@@ -769,11 +805,14 @@ impl Dispatcher {
self.fg_job = !is_bg && self.interactive; self.fg_job = !is_bg && self.interactive;
let mut cmd = cmds.into_iter().next().unwrap(); let mut cmd = cmds.into_iter().next().unwrap();
if is_bg && !matches!(cmd.class, NdRule::Command { .. }) { if is_bg && !matches!(cmd.class, NdRule::Command { .. }) {
self.run_fork(&cmd.get_command().map(|t| t.to_string()).unwrap_or_default(), |s| { self.run_fork(
&cmd.get_command().map(|t| t.to_string()).unwrap_or_default(),
|s| {
if let Err(e) = s.dispatch_node(cmd) { if let Err(e) = s.dispatch_node(cmd) {
e.print_error(); e.print_error();
} }
})?; },
)?;
} else { } else {
self.dispatch_node(cmd)?; self.dispatch_node(cmd)?;
} }

View File

@@ -19,7 +19,7 @@ use crate::{
pub const KEYWORDS: [&str; 17] = [ pub const KEYWORDS: [&str; 17] = [
"if", "then", "elif", "else", "fi", "while", "until", "select", "for", "in", "do", "done", "if", "then", "elif", "else", "fi", "while", "until", "select", "for", "in", "do", "done",
"case", "esac", "[[", "]]", "!" "case", "esac", "[[", "]]", "!",
]; ];
pub const OPENERS: [&str; 6] = ["if", "while", "until", "for", "select", "case"]; pub const OPENERS: [&str; 6] = ["if", "while", "until", "for", "select", "case"];

View File

@@ -1943,11 +1943,7 @@ pub mod tests {
#[test] #[test]
fn parse_hello_world() { fn parse_hello_world() {
let input = "echo hello world"; let input = "echo hello world";
let expected = &mut [ let expected = &mut [NdKind::Conjunction, NdKind::Pipeline, NdKind::Command].into_iter();
NdKind::Conjunction,
NdKind::Pipeline,
NdKind::Command
].into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -1968,7 +1964,8 @@ pub mod tests {
NdKind::Conjunction, NdKind::Conjunction,
NdKind::Pipeline, NdKind::Pipeline,
NdKind::Command, NdKind::Command,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -1985,7 +1982,8 @@ pub mod tests {
NdKind::Command, NdKind::Command,
NdKind::Command, NdKind::Command,
NdKind::Command, NdKind::Command,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2002,7 +2000,8 @@ pub mod tests {
NdKind::Command, NdKind::Command,
NdKind::Pipeline, NdKind::Pipeline,
NdKind::Command, NdKind::Command,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2023,7 +2022,8 @@ pub mod tests {
NdKind::Conjunction, NdKind::Conjunction,
NdKind::Pipeline, NdKind::Pipeline,
NdKind::Command, NdKind::Command,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2041,7 +2041,8 @@ pub mod tests {
NdKind::Conjunction, NdKind::Conjunction,
NdKind::Pipeline, NdKind::Pipeline,
NdKind::Command, NdKind::Command,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2062,7 +2063,8 @@ pub mod tests {
NdKind::Conjunction, NdKind::Conjunction,
NdKind::Pipeline, NdKind::Pipeline,
NdKind::Command, NdKind::Command,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2081,7 +2083,8 @@ pub mod tests {
NdKind::Conjunction, NdKind::Conjunction,
NdKind::Pipeline, NdKind::Pipeline,
NdKind::Command, NdKind::Command,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2097,7 +2100,8 @@ pub mod tests {
NdKind::Pipeline, NdKind::Pipeline,
NdKind::Command, NdKind::Command,
NdKind::Assignment, NdKind::Assignment,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2113,7 +2117,8 @@ pub mod tests {
NdKind::Pipeline, NdKind::Pipeline,
NdKind::Command, NdKind::Command,
NdKind::Assignment, NdKind::Assignment,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2143,7 +2148,8 @@ pub mod tests {
NdKind::Conjunction, NdKind::Conjunction,
NdKind::Pipeline, NdKind::Pipeline,
NdKind::Command, NdKind::Command,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2164,7 +2170,8 @@ pub mod tests {
NdKind::Conjunction, NdKind::Conjunction,
NdKind::Pipeline, NdKind::Pipeline,
NdKind::Command, NdKind::Command,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2191,7 +2198,8 @@ pub mod tests {
NdKind::Conjunction, NdKind::Conjunction,
NdKind::Pipeline, NdKind::Pipeline,
NdKind::Command, NdKind::Command,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2202,11 +2210,7 @@ pub mod tests {
#[test] #[test]
fn parse_test_bracket() { fn parse_test_bracket() {
let input = "[[ -n hello ]]"; let input = "[[ -n hello ]]";
let expected = &mut [ let expected = &mut [NdKind::Conjunction, NdKind::Pipeline, NdKind::Test].into_iter();
NdKind::Conjunction,
NdKind::Pipeline,
NdKind::Test,
].into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2240,7 +2244,8 @@ pub mod tests {
NdKind::Conjunction, NdKind::Conjunction,
NdKind::Pipeline, NdKind::Pipeline,
NdKind::Command, NdKind::Command,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2268,7 +2273,8 @@ pub mod tests {
NdKind::Conjunction, NdKind::Conjunction,
NdKind::Pipeline, NdKind::Pipeline,
NdKind::Command, NdKind::Command,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2307,7 +2313,8 @@ pub mod tests {
NdKind::Conjunction, NdKind::Conjunction,
NdKind::Pipeline, NdKind::Pipeline,
NdKind::Command, NdKind::Command,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2349,7 +2356,8 @@ pub mod tests {
NdKind::Conjunction, NdKind::Conjunction,
NdKind::Pipeline, NdKind::Pipeline,
NdKind::Command, NdKind::Command,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2387,7 +2395,8 @@ pub mod tests {
NdKind::Command, NdKind::Command,
NdKind::Pipeline, NdKind::Pipeline,
NdKind::Command, NdKind::Command,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2413,7 +2422,8 @@ pub mod tests {
NdKind::Assignment, NdKind::Assignment,
NdKind::Command, NdKind::Command,
NdKind::Command, NdKind::Command,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2446,7 +2456,8 @@ pub mod tests {
NdKind::Conjunction, NdKind::Conjunction,
NdKind::Pipeline, NdKind::Pipeline,
NdKind::Command, NdKind::Command,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2469,7 +2480,8 @@ pub mod tests {
NdKind::Command, NdKind::Command,
NdKind::Pipeline, NdKind::Pipeline,
NdKind::Command, NdKind::Command,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2500,7 +2512,8 @@ pub mod tests {
NdKind::Conjunction, NdKind::Conjunction,
NdKind::Pipeline, NdKind::Pipeline,
NdKind::Command, NdKind::Command,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {
@@ -2539,7 +2552,8 @@ pub mod tests {
NdKind::Pipeline, NdKind::Pipeline,
NdKind::Command, NdKind::Command,
NdKind::Command, NdKind::Command,
].into_iter(); ]
.into_iter();
let ast = get_ast(input).unwrap(); let ast = get_ast(input).unwrap();
let mut node = ast[0].clone(); let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) { if let Err(e) = node.assert_structure(expected) {

View File

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

View File

@@ -394,7 +394,9 @@ pub mod tests {
#[test] #[test]
fn pipeline_simple() { fn pipeline_simple() {
if !has_cmd("sed") { return }; if !has_cmd("sed") {
return;
};
let g = TestGuard::new(); let g = TestGuard::new();
test_input("echo foo | sed 's/foo/bar/'").unwrap(); test_input("echo foo | sed 's/foo/bar/'").unwrap();
@@ -405,10 +407,9 @@ pub mod tests {
#[test] #[test]
fn pipeline_multi() { fn pipeline_multi() {
if !has_cmds(&[ if !has_cmds(&["cut", "sed"]) {
"cut", return;
"sed" }
]) { return; }
let g = TestGuard::new(); let g = TestGuard::new();
test_input("echo foo bar baz | cut -d ' ' -f 2 | sed 's/a/A/'").unwrap(); test_input("echo foo bar baz | cut -d ' ' -f 2 | sed 's/a/A/'").unwrap();
@@ -419,10 +420,9 @@ pub mod tests {
#[test] #[test]
fn rube_goldberg_pipeline() { fn rube_goldberg_pipeline() {
if !has_cmds(&[ if !has_cmds(&["sed", "cat"]) {
"sed", return;
"cat", }
]) { return }
let g = TestGuard::new(); let g = TestGuard::new();
test_input("{ echo foo; echo bar } | if cat; then :; else echo failed; fi | (read line && echo $line | sed 's/foo/baz/'; sed 's/bar/buzz/')").unwrap(); test_input("{ echo foo; echo bar } | if cat; then :; else echo failed; fi | (read line && echo $line | sed 's/foo/baz/'; sed 's/bar/buzz/')").unwrap();
@@ -437,7 +437,9 @@ pub mod tests {
test_input("echo this is in a file > /tmp/simple_file_redir.txt").unwrap(); test_input("echo this is in a file > /tmp/simple_file_redir.txt").unwrap();
g.add_cleanup(|| { std::fs::remove_file("/tmp/simple_file_redir.txt").ok(); }); g.add_cleanup(|| {
std::fs::remove_file("/tmp/simple_file_redir.txt").ok();
});
let contents = std::fs::read_to_string("/tmp/simple_file_redir.txt").unwrap(); 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");
@@ -458,7 +460,9 @@ pub mod tests {
#[test] #[test]
fn input_redir() { fn input_redir() {
if !has_cmd("cat") { return; } if !has_cmd("cat") {
return;
}
let dir = tempfile::TempDir::new().unwrap(); let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("input.txt"); let path = dir.path().join("input.txt");
std::fs::write(&path, "hello from file\n").unwrap(); std::fs::write(&path, "hello from file\n").unwrap();
@@ -487,7 +491,9 @@ pub mod tests {
#[test] #[test]
fn pipe_and_stderr() { fn pipe_and_stderr() {
if !has_cmd("cat") { return; } if !has_cmd("cat") {
return;
}
let g = TestGuard::new(); let g = TestGuard::new();
test_input("echo on stderr >&2 |& cat").unwrap(); test_input("echo on stderr >&2 |& cat").unwrap();
@@ -511,7 +517,9 @@ pub mod tests {
#[test] #[test]
fn pipeline_preserves_exit_status() { fn pipeline_preserves_exit_status() {
if !has_cmd("cat") { return; } if !has_cmd("cat") {
return;
}
let _g = TestGuard::new(); let _g = TestGuard::new();
test_input("false | cat").unwrap(); test_input("false | cat").unwrap();
@@ -533,7 +541,11 @@ pub mod tests {
let _g = TestGuard::new(); let _g = TestGuard::new();
// Redirect stdout to file, then dup stderr to stdout — both should go to file // Redirect stdout to file, then dup stderr to stdout — both should go to file
test_input(format!("{{ echo out; echo err >&2 }} > {} 2>&1", path.display())).unwrap(); test_input(format!(
"{{ echo out; echo err >&2 }} > {} 2>&1",
path.display()
))
.unwrap();
let contents = std::fs::read_to_string(&path).unwrap(); let contents = std::fs::read_to_string(&path).unwrap();
assert!(contents.contains("out")); assert!(contents.contains("out"));

View File

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

View File

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

View File

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

View File

@@ -141,6 +141,12 @@ impl SelectMode {
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
enum CaseTransform {
Toggle,
Lower,
Upper,
}
pub enum MotionKind { pub enum MotionKind {
To(usize), // Absolute position, exclusive To(usize), // Absolute position, exclusive
On(usize), // Absolute position, inclusive On(usize), // Absolute position, inclusive
@@ -854,13 +860,19 @@ impl LineBuf {
let mut bkwd_indices = self.directional_indices_iter_from(pos, Direction::Backward); let mut bkwd_indices = self.directional_indices_iter_from(pos, Direction::Backward);
// Find the digit span, then check if preceded by '-' // 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)) let mut start = bkwd_indices
.map(|i| i + 1).unwrap_or(0); .find(|i| !self.grapheme_at(*i).is_some_and(is_digit))
let end = fwd_indices.find(|i| !self.grapheme_at(*i).is_some_and(is_digit)) .map(|i| i + 1)
.map(|i| i - 1).unwrap_or(self.cursor.max); // inclusive end .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 // Check for leading minus
if start > 0 && self.grapheme_at(start - 1) == Some("-") { start -= 1; } if start > 0 && self.grapheme_at(start - 1) == Some("-") {
start -= 1;
}
Some((start, end)) Some((start, end))
} }
@@ -2600,15 +2612,7 @@ impl LineBuf {
}; };
Some(range) Some(range)
} }
#[allow(clippy::unnecessary_to_owned)] fn verb_ydc(&mut self, motion: MotionKind, register: RegisterName, verb: Verb) -> ShResult<()> {
pub fn exec_verb(
&mut self,
verb: Verb,
motion: MotionKind,
register: RegisterName,
) -> ShResult<()> {
match verb {
Verb::Delete | Verb::Yank | Verb::Change => {
let Some((start, end)) = self.range_from_motion(&motion) else { let Some((start, end)) = self.range_from_motion(&motion) else {
return Ok(()); return Ok(());
}; };
@@ -2659,8 +2663,9 @@ impl LineBuf {
{ {
self.cursor.add(col); self.cursor.add(col);
} }
Ok(())
} }
Verb::Rot13 => { fn verb_rot13(&mut self, motion: MotionKind) -> ShResult<()> {
let Some((start, end)) = self.range_from_motion(&motion) else { let Some((start, end)) = self.range_from_motion(&motion) else {
return Ok(()); return Ok(());
}; };
@@ -2668,14 +2673,16 @@ impl LineBuf {
let rot13 = rot13(slice); let rot13 = rot13(slice);
self.buffer.replace_range(start..end, &rot13); self.buffer.replace_range(start..end, &rot13);
self.cursor.set(start); self.cursor.set(start);
Ok(())
} }
Verb::ReplaceChar(ch) => { fn verb_replace_char(&mut self, motion: MotionKind, ch: char) -> ShResult<()> {
let mut buf = [0u8; 4]; let mut buf = [0u8; 4];
let new = ch.encode_utf8(&mut buf); let new = ch.encode_utf8(&mut buf);
self.replace_at_cursor(new); self.replace_at_cursor(new);
self.apply_motion(motion); self.apply_motion(motion);
Ok(())
} }
Verb::ReplaceCharInplace(ch, count) => { pub fn verb_replace_char_inplace(&mut self, ch: char, count: u16) -> ShResult<()> {
if let Some((start, end)) = self.select_range() { if let Some((start, end)) = self.select_range() {
let end = (end + 1).min(self.grapheme_indices().len()); // inclusive let end = (end + 1).min(self.grapheme_indices().len()); // inclusive
let replaced = ch.to_string().repeat(end.saturating_sub(start)); let replaced = ch.to_string().repeat(end.saturating_sub(start));
@@ -2694,19 +2701,20 @@ impl LineBuf {
} }
} }
} }
Ok(())
} }
Verb::ToggleCaseInplace(count) => { fn verb_toggle_case_inplace(&mut self, count: u16) {
let mut did_something = false; let mut did_something = false;
for i in 0..count { for i in 0..count {
let Some(gr) = self.grapheme_at_cursor() else { let Some(gr) = self.grapheme_at_cursor() else {
return Ok(()); return;
}; };
if gr.len() > 1 || gr.is_empty() { if gr.len() > 1 || gr.is_empty() {
return Ok(()); return;
} }
let ch = gr.chars().next().unwrap(); let ch = gr.chars().next().unwrap();
if !ch.is_alphabetic() { if !ch.is_alphabetic() {
return Ok(()); return;
} }
let mut buf = [0u8; 4]; let mut buf = [0u8; 4];
let new = if ch.is_ascii_lowercase() { let new = if ch.is_ascii_lowercase() {
@@ -2715,9 +2723,6 @@ impl LineBuf {
ch.to_ascii_lowercase().encode_utf8(&mut buf) ch.to_ascii_lowercase().encode_utf8(&mut buf)
}; };
self.replace_at_cursor(new); 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; did_something = true;
if i != count.saturating_sub(1) && !self.cursor.inc() { if i != count.saturating_sub(1) && !self.cursor.inc() {
break; break;
@@ -2727,7 +2732,7 @@ impl LineBuf {
self.cursor.inc(); self.cursor.inc();
} }
} }
Verb::ToggleCaseRange => { fn verb_case_transform(&mut self, motion: MotionKind, transform: CaseTransform) -> ShResult<()> {
let Some((start, end)) = self.range_from_motion(&motion) else { let Some((start, end)) = self.range_from_motion(&motion) else {
return Ok(()); return Ok(());
}; };
@@ -2743,70 +2748,37 @@ impl LineBuf {
continue; continue;
} }
let mut buf = [0u8; 4]; let mut buf = [0u8; 4];
let new = if ch.is_ascii_lowercase() { let new = match transform {
CaseTransform::Toggle => {
if ch.is_ascii_lowercase() {
ch.to_ascii_uppercase().encode_utf8(&mut buf) ch.to_ascii_uppercase().encode_utf8(&mut buf)
} else { } else {
ch.to_ascii_lowercase().encode_utf8(&mut buf) ch.to_ascii_lowercase().encode_utf8(&mut buf)
};
self.replace_at(i, new);
} }
self.cursor.set(start);
} }
Verb::ToLower => { CaseTransform::Lower => {
let Some((start, end)) = self.range_from_motion(&motion) else { if ch.is_ascii_uppercase() {
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) ch.to_ascii_lowercase().encode_utf8(&mut buf)
} else { } else {
ch.encode_utf8(&mut buf) ch.encode_utf8(&mut buf)
};
self.replace_at(i, new);
} }
self.cursor.set(start);
} }
Verb::ToUpper => { CaseTransform::Upper => {
let Some((start, end)) = self.range_from_motion(&motion) else { if ch.is_ascii_lowercase() {
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) ch.to_ascii_uppercase().encode_utf8(&mut buf)
} else { } else {
ch.encode_utf8(&mut buf) ch.encode_utf8(&mut buf)
}
}
}; };
self.replace_at(i, new); self.replace_at(i, new);
} }
self.cursor.set(start); self.cursor.set(start);
Ok(())
} }
Verb::Redo | Verb::Undo => { fn verb_undo_redo(&mut self, verb: Verb) -> ShResult<()> {
let (edit_provider, edit_receiver) = match verb { 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), 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), Verb::Undo => (&mut self.undo_stack, &mut self.redo_stack),
_ => unreachable!(), _ => unreachable!(),
}; };
@@ -2820,23 +2792,20 @@ impl LineBuf {
new, new,
merging: _, merging: _,
} = edit; } = edit;
self.buffer.replace_range(pos..pos + new.len(), &old); self.buffer.replace_range(pos..pos + new.len(), &old);
let new_cursor_pos = self.cursor.get(); let new_cursor_pos = self.cursor.get();
self.cursor.set(cursor_pos); self.cursor.set(cursor_pos);
let new_edit = Edit { edit_receiver.push(Edit {
pos, pos,
cursor_pos: new_cursor_pos, cursor_pos: new_cursor_pos,
old: new, old: new,
new: old, new: old,
merging: false, merging: false,
}; });
edit_receiver.push(new_edit);
self.update_graphemes(); self.update_graphemes();
Ok(())
} }
Verb::RepeatLast => todo!(), fn verb_put(&mut self, anchor: Anchor, register: RegisterName) -> ShResult<()> {
Verb::Put(anchor) => {
let Some(content) = register.read_from_register() else { let Some(content) = register.read_from_register() else {
return Ok(()); return Ok(());
}; };
@@ -2845,8 +2814,7 @@ impl LineBuf {
} }
if let Some(range) = self.select_range() { if let Some(range) = self.select_range() {
let register_text = self.drain_inclusive(range.0..=range.1); let register_text = self.drain_inclusive(range.0..=range.1);
write_register(None, RegisterContent::Span(register_text)); // swap deleted text into register write_register(None, RegisterContent::Span(register_text));
let text = content.as_str(); let text = content.as_str();
self.insert_str_at(range.0, text); self.insert_str_at(range.0, text);
self.cursor.set(range.0 + content.char_count()); self.cursor.set(range.0 + content.char_count());
@@ -2855,8 +2823,7 @@ impl LineBuf {
return Ok(()); return Ok(());
} }
match content { match content {
RegisterContent::Span(ref text) => { RegisterContent::Span(ref text) => match anchor {
match anchor {
Anchor::After => { Anchor::After => {
let insert_idx = self let insert_idx = self
.cursor .cursor
@@ -2864,17 +2831,15 @@ impl LineBuf {
.saturating_add(1) .saturating_add(1)
.min(self.grapheme_indices().len()); .min(self.grapheme_indices().len());
let offset = text.len().max(1); let offset = text.len().max(1);
self.insert_str_at(insert_idx, text); self.insert_str_at(insert_idx, text);
self.cursor.add(offset); self.cursor.add(offset);
}, }
Anchor::Before => { Anchor::Before => {
let insert_idx = self.cursor.get(); let insert_idx = self.cursor.get();
self.insert_str_at(insert_idx, text); self.insert_str_at(insert_idx, text);
self.cursor.add(text.len().saturating_sub(1)); self.cursor.add(text.len().saturating_sub(1));
},
};
} }
},
RegisterContent::Line(ref text) => { RegisterContent::Line(ref text) => {
let insert_idx = match anchor { let insert_idx = match anchor {
Anchor::After => self.end_of_line(), Anchor::After => self.end_of_line(),
@@ -2882,7 +2847,6 @@ impl LineBuf {
}; };
let mut full = text.to_string(); let mut full = text.to_string();
let mut offset = 0; let mut offset = 0;
match anchor { match anchor {
Anchor::After => { Anchor::After => {
if self.grapheme_before(insert_idx).is_none_or(|gr| gr != "\n") { if self.grapheme_before(insert_idx).is_none_or(|gr| gr != "\n") {
@@ -2897,14 +2861,14 @@ impl LineBuf {
full = format!("{full}\n"); full = format!("{full}\n");
} }
} }
self.insert_str_at(insert_idx, &full); self.insert_str_at(insert_idx, &full);
self.cursor.set(insert_idx + offset); self.cursor.set(insert_idx + offset);
} }
RegisterContent::Empty => {} RegisterContent::Empty => {}
} }
Ok(())
} }
Verb::SwapVisualAnchor => { fn verb_swap_visual_anchor(&mut self) {
if let Some((start, end)) = self.select_range if let Some((start, end)) = self.select_range
&& let Some(mut mode) = self.select_mode && let Some(mut mode) = self.select_mode
{ {
@@ -2917,12 +2881,12 @@ impl LineBuf {
self.select_mode = Some(mode) self.select_mode = Some(mode)
} }
} }
Verb::JoinLines => { fn verb_join_lines(&mut self) -> ShResult<()> {
let start = self.start_of_line(); let start = self.start_of_line();
let Some((_, mut end)) = self.nth_next_line(1) else { let Some((_, mut end)) = self.nth_next_line(1) else {
return Ok(()); return Ok(());
}; };
end = end.saturating_sub(1); // exclude the last newline end = end.saturating_sub(1);
let mut last_was_whitespace = false; let mut last_was_whitespace = false;
for i in start..end { for i in start..end {
let Some(gr) = self.grapheme_at(i) else { let Some(gr) = self.grapheme_at(i) else {
@@ -2935,13 +2899,22 @@ impl LineBuf {
self.force_replace_at(i, " "); self.force_replace_at(i, " ");
} }
last_was_whitespace = false; 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); self.cursor.set(i);
continue; continue;
} }
last_was_whitespace = is_whitespace(gr); last_was_whitespace = is_whitespace(gr);
} }
Ok(())
} }
Verb::InsertChar(ch) => { fn verb_insert_char(&mut self, ch: char) {
self.insert_at_cursor(ch); self.insert_at_cursor(ch);
self.cursor.add(1); self.cursor.add(1);
let before = self.auto_indent_level; let before = self.auto_indent_level;
@@ -2964,24 +2937,22 @@ impl LineBuf {
} }
} }
} }
_ => { /* nothing to see here */ } _ => {}
} }
} }
} }
Verb::Insert(string) => { fn verb_insert(&mut self, string: String) {
self.insert_str_at_cursor(&string); self.insert_str_at_cursor(&string);
let graphemes = string.graphemes(true).count(); let graphemes = string.graphemes(true).count();
self.cursor.add(graphemes); self.cursor.add(graphemes);
} }
Verb::Indent => { fn verb_indent(&mut self, motion: MotionKind) -> ShResult<()> {
let Some((start, end)) = self.range_from_motion(&motion) else { let Some((start, end)) = self.range_from_motion(&motion) else {
return Ok(()); return Ok(());
}; };
let move_cursor = self.cursor.get() == start;
self.insert_at(start, '\t'); self.insert_at(start, '\t');
if move_cursor { self.update_graphemes();
self.cursor.add(1); let end = end.clamp(0, self.grapheme_indices().len());
}
let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter(); let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter();
while let Some(idx) = range_indices.next() { while let Some(idx) = range_indices.next() {
let gr = self.grapheme_at(idx).unwrap(); let gr = self.grapheme_at(idx).unwrap();
@@ -2993,18 +2964,18 @@ impl LineBuf {
self.insert_at(idx, '\t'); self.insert_at(idx, '\t');
} }
} }
match motion { match motion {
MotionKind::ExclusiveWithTargetCol((_, _), pos) MotionKind::ExclusiveWithTargetCol((_, _), pos)
| MotionKind::InclusiveWithTargetCol((_, _), pos) => { | MotionKind::InclusiveWithTargetCol((_, _), pos) => {
self.cursor.set(start); self.cursor.set(start);
let end = self.end_of_line(); let end = self.end_of_line();
self.cursor.add(end.min(pos)); self.cursor.add(end.min(pos + 1));
} }
_ => self.cursor.set(start), _ => self.cursor.set(start),
} }
Ok(())
} }
Verb::Dedent => { fn verb_dedent(&mut self, motion: MotionKind) -> ShResult<()> {
let Some((start, mut end)) = self.range_from_motion(&motion) else { let Some((start, mut end)) = self.range_from_motion(&motion) else {
return Ok(()); return Ok(());
}; };
@@ -3027,7 +2998,6 @@ impl LineBuf {
} }
} }
} }
match motion { match motion {
MotionKind::ExclusiveWithTargetCol((_, _), pos) MotionKind::ExclusiveWithTargetCol((_, _), pos)
| MotionKind::InclusiveWithTargetCol((_, _), pos) => { | MotionKind::InclusiveWithTargetCol((_, _), pos) => {
@@ -3037,9 +3007,9 @@ impl LineBuf {
} }
_ => self.cursor.set(start), _ => self.cursor.set(start),
} }
Ok(())
} }
Verb::Equalize => todo!(), fn verb_insert_mode_line_break(&mut self, anchor: Anchor) -> ShResult<()> {
Verb::InsertModeLineBreak(anchor) => {
let (mut start, end) = self.this_line_exclusive(); let (mut start, end) = self.this_line_exclusive();
let auto_indent = read_shopts(|o| o.prompt.auto_indent); let auto_indent = read_shopts(|o| o.prompt.auto_indent);
if start == 0 && end == self.cursor.max { if start == 0 && end == self.cursor.max {
@@ -3048,9 +3018,8 @@ impl LineBuf {
self.push('\n'); self.push('\n');
if auto_indent { if auto_indent {
self.calc_indent_level(); self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t'); for _ in 0..self.auto_indent_level {
for tab in tabs { self.push('\t');
self.push(tab);
} }
} }
self.cursor.set(self.cursor_max()); self.cursor.set(self.cursor_max());
@@ -3059,9 +3028,8 @@ impl LineBuf {
Anchor::Before => { Anchor::Before => {
if auto_indent { if auto_indent {
self.calc_indent_level(); self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t'); for _ in 0..self.auto_indent_level {
for tab in tabs { self.insert_at(0, '\t');
self.insert_at(0, tab);
} }
} }
self.insert_at(0, '\n'); self.insert_at(0, '\n');
@@ -3070,65 +3038,41 @@ impl LineBuf {
} }
} }
} }
// We want the position of the newline, or start of buffer
start = start.saturating_sub(1).min(self.cursor.max); start = start.saturating_sub(1).min(self.cursor.max);
match anchor { match anchor {
Anchor::After => { Anchor::After => {
self.cursor.set(end); self.cursor.set(end);
self.insert_at_cursor('\n'); self.insert_newline_with_indent(auto_indent);
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 => { Anchor::Before => {
self.cursor.set(start); 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.insert_at_cursor('\n');
self.cursor.add(1); self.cursor.add(1);
if auto_indent { if auto_indent {
self.calc_indent_level(); self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t'); for _ in 0..self.auto_indent_level {
for tab in tabs { self.insert_at_cursor('\t');
self.insert_at_cursor(tab);
self.cursor.add(1); self.cursor.add(1);
} }
} }
} }
} fn verb_accept_line_or_newline(&mut self) -> ShResult<()> {
}
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 { 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)); let motion = self.eval_motion(None, MotionCmd(1, Motion::LineDownCharwise));
self.apply_motion(motion); self.apply_motion(motion);
return Ok(()); return Ok(());
} }
let auto_indent = read_shopts(|o| o.prompt.auto_indent); let auto_indent = read_shopts(|o| o.prompt.auto_indent);
self.insert_at_cursor('\n'); self.insert_newline_with_indent(auto_indent);
self.cursor.add(1); Ok(())
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);
} }
} fn verb_adjust_number(&mut self, inc: i64) -> ShResult<()> {
}
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() { let (s, e) = if let Some(r) = self.select_range() {
r r
} else if let Some(r) = self.number_at_cursor() { } else if let Some(r) = self.number_at_cursor() {
@@ -3144,9 +3088,8 @@ impl LineBuf {
} }
} else { } else {
(e + 1).min(self.grapheme_indices().len()) (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 word = self.slice(s..end).unwrap_or_default().to_lowercase();
let byte_start = self.index_byte_pos(s); let byte_start = self.index_byte_pos(s);
let byte_end = if end >= self.grapheme_indices().len() { let byte_end = if end >= self.grapheme_indices().len() {
self.buffer.len() self.buffer.len()
@@ -3195,35 +3138,18 @@ impl LineBuf {
let digit_width = if num < 0 { width - 1 } else { width }; let digit_width = if num < 0 { width - 1 } else { width };
format!("-{abs:0>digit_width$}") format!("-{abs:0>digit_width$}")
} else if num < 0 { } 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; let digit_width = width - 1;
format!("{new_num:0>digit_width$}") format!("{new_num:0>digit_width$}")
} else { } else {
format!("{new_num:0>width$}") format!("{new_num:0>width$}")
}; };
self self.buffer.replace_range(byte_start..byte_end, &num_fmt);
.buffer
.replace_range(byte_start..byte_end, &num_fmt);
self.update_graphemes(); self.update_graphemes();
self.cursor.set(s); self.cursor.set(s);
} }
Ok(())
} }
fn verb_shell_cmd(&mut self, cmd: String) -> ShResult<()> {
Verb::Complete
| Verb::ExMode
| Verb::EndOfFile
| Verb::InsertMode
| Verb::NormalMode
| Verb::VisualMode
| Verb::VerbatimMode
| Verb::ReplaceMode
| 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(); let mut vars = HashSet::new();
vars.insert("_BUFFER".into()); vars.insert("_BUFFER".into());
vars.insert("_CURSOR".into()); vars.insert("_CURSOR".into());
@@ -3251,9 +3177,7 @@ impl LineBuf {
) )
})?; })?;
RawModeGuard::with_cooked_mode(|| { RawModeGuard::with_cooked_mode(|| exec_input(cmd, None, true, Some("<ex-mode-cmd>".into())))?;
exec_input(cmd, None, true, Some("<ex-mode-cmd>".into()))
})?;
let keys = write_vars(|v| { let keys = write_vars(|v| {
buf = v.take_var("_BUFFER"); buf = v.take_var("_BUFFER");
@@ -3272,13 +3196,57 @@ impl LineBuf {
if !keys.is_empty() { if !keys.is_empty() {
write_meta(|m| m.set_pending_widget_keys(&keys)) write_meta(|m| m.set_pending_widget_keys(&keys))
} }
Ok(())
} }
#[allow(clippy::unnecessary_to_owned)]
pub fn exec_verb(
&mut self,
verb: Verb,
motion: MotionKind,
register: RegisterName,
) -> ShResult<()> {
match verb {
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
| Verb::InsertMode
| Verb::NormalMode
| Verb::VisualMode
| Verb::VerbatimMode
| Verb::ReplaceMode
| Verb::VisualModeLine
| Verb::VisualModeBlock
| Verb::CompleteBackward
| Verb::VisualModeSelectLast => self.apply_motion(motion),
Verb::ShellCmd(cmd) => self.verb_shell_cmd(cmd)?,
Verb::Normal(_) Verb::Normal(_)
| Verb::Read(_) | Verb::Read(_)
| Verb::Write(_) | Verb::Write(_)
| Verb::Substitute(..) | Verb::Substitute(..)
| Verb::RepeatSubstitute | Verb::RepeatSubstitute
| Verb::RepeatGlobal => {} // Ex-mode verbs handled elsewhere | Verb::RepeatGlobal => {}
} }
Ok(()) Ok(())
} }

View File

@@ -15,7 +15,8 @@ use crate::readline::complete::{FuzzyCompleter, SelectorResponse};
use crate::readline::term::{Pos, TermReader, calc_str_width}; use crate::readline::term::{Pos, TermReader, calc_str_width};
use crate::readline::vimode::{ViEx, ViVerbatim}; use crate::readline::vimode::{ViEx, ViVerbatim};
use crate::state::{ use crate::state::{
AutoCmdKind, ShellParam, Var, VarFlags, VarKind, read_logic, read_shopts, with_vars, write_meta, write_vars AutoCmdKind, ShellParam, Var, VarFlags, VarKind, read_logic, read_shopts, with_vars, write_meta,
write_vars,
}; };
use crate::{ use crate::{
libsh::error::ShResult, libsh::error::ShResult,
@@ -443,7 +444,11 @@ impl ShedVi {
// Process all available keys // Process all available keys
while let Some(key) = self.reader.read_key()? { while let Some(key) = self.reader.read_key()? {
log::debug!("Read key: {key:?} in mode {:?}, self.reader.verbatim = {}", self.mode.report_mode(), self.reader.verbatim); log::debug!(
"Read key: {key:?} in mode {:?}, self.reader.verbatim = {}",
self.mode.report_mode(),
self.reader.verbatim
);
// If completer or history search are active, delegate input to it // If completer or history search are active, delegate input to it
if self.history.fuzzy_finder.is_active() { if self.history.fuzzy_finder.is_active() {
self.print_line(false)?; self.print_line(false)?;
@@ -694,7 +699,8 @@ impl ShedVi {
VarKind::Str(self.mode.report_mode().to_string()), VarKind::Str(self.mode.report_mode().to_string()),
VarFlags::NONE, VarFlags::NONE,
) )
}).ok(); })
.ok();
// If we are here, we hit a case where pressing tab returned a single candidate // If we are here, we hit a case where pressing tab returned a single candidate
// So we can just go ahead and reset the completer after this // So we can just go ahead and reset the completer after this
@@ -704,13 +710,19 @@ impl ShedVi {
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnCompletionStart)); let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnCompletionStart));
let candidates = self.completer.all_candidates(); let candidates = self.completer.all_candidates();
let num_candidates = candidates.len(); let num_candidates = candidates.len();
with_vars([ with_vars(
[
("_NUM_MATCHES".into(), Into::<Var>::into(num_candidates)), ("_NUM_MATCHES".into(), Into::<Var>::into(num_candidates)),
("_MATCHES".into(), Into::<Var>::into(candidates)), ("_MATCHES".into(), Into::<Var>::into(candidates)),
("_SEARCH_STR".into(), Into::<Var>::into(self.completer.token())), (
], || { "_SEARCH_STR".into(),
Into::<Var>::into(self.completer.token()),
),
],
|| {
post_cmds.exec(); post_cmds.exec();
}); },
);
if self.completer.is_active() { if self.completer.is_active() {
write_vars(|v| { write_vars(|v| {
@@ -733,14 +745,13 @@ impl ShedVi {
self.needs_redraw = true; self.needs_redraw = true;
return Ok(None); return Ok(None);
} else if let KeyEvent(KeyCode::Char('R'), ModKeys::CTRL) = key } else if let KeyEvent(KeyCode::Char('R'), ModKeys::CTRL) = key
&& self.mode.report_mode() == ModeReport::Insert { && self.mode.report_mode() == ModeReport::Insert
{
let initial = self.editor.as_str(); let initial = self.editor.as_str();
match self.history.start_search(initial) { match self.history.start_search(initial) {
Some(entry) => { Some(entry) => {
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistorySelect)); let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistorySelect));
with_vars([ with_vars([("_HIST_ENTRY".into(), entry.clone())], || {
("_HIST_ENTRY".into(), entry.clone()),
], || {
post_cmds.exec_with(&entry); post_cmds.exec_with(&entry);
}); });
@@ -754,7 +765,9 @@ impl ShedVi {
None => { None => {
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistoryOpen)); let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistoryOpen));
let entries = self.history.fuzzy_finder.candidates(); let entries = self.history.fuzzy_finder.candidates();
let matches = self.history.fuzzy_finder let matches = self
.history
.fuzzy_finder
.filtered() .filtered()
.iter() .iter()
.cloned() .cloned()
@@ -763,15 +776,18 @@ impl ShedVi {
let num_entries = entries.len(); let num_entries = entries.len();
let num_matches = matches.len(); let num_matches = matches.len();
with_vars([ with_vars(
[
("_ENTRIES".into(), Into::<Var>::into(entries)), ("_ENTRIES".into(), Into::<Var>::into(entries)),
("_NUM_ENTRIES".into(), Into::<Var>::into(num_entries)), ("_NUM_ENTRIES".into(), Into::<Var>::into(num_entries)),
("_MATCHES".into(), Into::<Var>::into(matches)), ("_MATCHES".into(), Into::<Var>::into(matches)),
("_NUM_MATCHES".into(), Into::<Var>::into(num_matches)), ("_NUM_MATCHES".into(), Into::<Var>::into(num_matches)),
("_SEARCH_STR".into(), Into::<Var>::into(initial)), ("_SEARCH_STR".into(), Into::<Var>::into(initial)),
], || { ],
|| {
post_cmds.exec(); post_cmds.exec();
}); },
);
if self.history.fuzzy_finder.is_active() { if self.history.fuzzy_finder.is_active() {
write_vars(|v| { write_vars(|v| {
@@ -1129,7 +1145,11 @@ impl ShedVi {
match cmd.verb().unwrap().1 { match cmd.verb().unwrap().1 {
Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => { Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => {
is_insert_mode = true; is_insert_mode = true;
Box::new(ViInsert::new().with_count(count as u16).record_cmd(cmd.clone())) Box::new(
ViInsert::new()
.with_count(count as u16)
.record_cmd(cmd.clone()),
)
} }
Verb::ExMode => Box::new(ViEx::new()), Verb::ExMode => Box::new(ViEx::new()),
@@ -1249,12 +1269,16 @@ impl ShedVi {
for _ in 0..repeat { for _ in 0..repeat {
let cmds = cmds.clone(); let cmds = cmds.clone();
for (i, cmd) in cmds.iter().enumerate() { for (i, cmd) in cmds.iter().enumerate() {
log::debug!("Replaying command {cmd:?} in mode {:?}, replay {i}/{repeat}", self.mode.report_mode()); log::debug!(
"Replaying command {cmd:?} in mode {:?}, replay {i}/{repeat}",
self.mode.report_mode()
);
self.exec_cmd(cmd.clone(), true)?; self.exec_cmd(cmd.clone(), true)?;
// After the first command, start merging so all subsequent // After the first command, start merging so all subsequent
// edits fold into one undo entry (e.g. cw + inserted chars) // edits fold into one undo entry (e.g. cw + inserted chars)
if i == 0 if i == 0
&& let Some(edit) = self.editor.undo_stack.last_mut() { && let Some(edit) = self.editor.undo_stack.last_mut()
{
edit.start_merge(); edit.start_merge();
} }
} }
@@ -1354,7 +1378,11 @@ impl ShedVi {
self.editor.exec_cmd(cmd.clone())?; self.editor.exec_cmd(cmd.clone())?;
if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit() || v.1 == Verb::Yank) { if self.mode.report_mode() == ModeReport::Visual
&& cmd
.verb()
.is_some_and(|v| v.1.is_edit() || v.1 == Verb::Yank)
{
self.editor.stop_selecting(); self.editor.stop_selecting();
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new()); let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
self.swap_mode(&mut mode); self.swap_mode(&mut mode);

View File

@@ -539,7 +539,10 @@ impl PollReader {
} }
let bytes: Vec<u8> = self.byte_buf.drain(..).collect(); let bytes: Vec<u8> = self.byte_buf.drain(..).collect();
let verbatim_str = String::from_utf8_lossy(&bytes).to_string(); let verbatim_str = String::from_utf8_lossy(&bytes).to_string();
Some(KeyEvent(KeyCode::Verbatim(verbatim_str.into()), ModKeys::empty())) Some(KeyEvent(
KeyCode::Verbatim(verbatim_str.into()),
ModKeys::empty(),
))
} }
pub fn feed_bytes(&mut self, bytes: &[u8]) { pub fn feed_bytes(&mut self, bytes: &[u8]) {
@@ -571,9 +574,7 @@ impl KeyReader for PollReader {
} else if self.byte_buf.front() == Some(&b'\x1b') { } else if self.byte_buf.front() == Some(&b'\x1b') {
// Escape: if it's the only byte, or the next byte isn't a valid // Escape: if it's the only byte, or the next byte isn't a valid
// escape sequence prefix ([ or O), emit a standalone Escape // escape sequence prefix ([ or O), emit a standalone Escape
if self.byte_buf.len() == 1 if self.byte_buf.len() == 1 || !matches!(self.byte_buf.get(1), Some(b'[') | Some(b'O')) {
|| !matches!(self.byte_buf.get(1), Some(b'[') | Some(b'O'))
{
self.byte_buf.pop_front(); self.byte_buf.pop_front();
return Ok(Some(KeyEvent(KeyCode::Esc, ModKeys::empty()))); return Ok(Some(KeyEvent(KeyCode::Esc, ModKeys::empty())));
} }
@@ -589,7 +590,7 @@ impl KeyReader for PollReader {
continue; continue;
} }
} }
_ => return Ok(Some(key)) _ => return Ok(Some(key)),
} }
} }
} }

View File

@@ -1,7 +1,10 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use std::os::fd::AsRawFd; use std::os::fd::AsRawFd;
use crate::{readline::{Prompt, ShedVi}, testutil::TestGuard}; use crate::{
readline::{Prompt, ShedVi},
testutil::TestGuard,
};
/// Tests for our vim logic emulation. Each test consists of an initial text, a sequence of keys to feed, and the expected final text and cursor position. /// Tests for our vim logic emulation. Each test consists of an initial text, a sequence of keys to feed, and the expected final text and cursor position.
macro_rules! vi_test { macro_rules! vi_test {
@@ -193,9 +196,9 @@ vi_test! {
vi_count_dw : "one two three four" => "2dw" => "three four", 0; vi_count_dw : "one two three four" => "2dw" => "three four", 0;
vi_verb_count_motion : "one two three four" => "d2w" => "three four", 0; vi_verb_count_motion : "one two three four" => "d2w" => "three four", 0;
vi_count_s : "hello" => "3sX\x1b" => "Xlo", 0; vi_count_s : "hello" => "3sX\x1b" => "Xlo", 0;
vi_indent_line : "hello" => ">>" => "\thello", 0; vi_indent_line : "hello" => ">>" => "\thello", 1;
vi_dedent_line : "\thello" => "<<" => "hello", 0; vi_dedent_line : "\thello" => "<<" => "hello", 0;
vi_indent_double : "hello" => ">>>>" => "\t\thello", 0; vi_indent_double : "hello" => ">>>>" => "\t\thello", 2;
vi_J_join_lines : "hello\nworld" => "J" => "hello world", 5; vi_J_join_lines : "hello\nworld" => "J" => "hello world", 5;
vi_v_u_lower : "HELLO" => "vlllu" => "hellO", 0; vi_v_u_lower : "HELLO" => "vlllu" => "hellO", 0;
vi_v_U_upper : "hello" => "vlllU" => "HELLo", 0; vi_v_U_upper : "hello" => "vlllU" => "HELLo", 0;
@@ -226,5 +229,7 @@ vi_test! {
vi_caret_no_ws : "hello" => "$^" => "hello", 0; vi_caret_no_ws : "hello" => "$^" => "hello", 0;
vi_f_last_char : "hello" => "fo" => "hello", 4; vi_f_last_char : "hello" => "fo" => "hello", 4;
vi_r_on_space : "hello world" => "5|r-" => "hell- world", 4; vi_r_on_space : "hello world" => "5|r-" => "hell- world", 4;
vi_vw_doesnt_crash : "" => "vw" => "", 0 vi_vw_doesnt_crash : "" => "vw" => "", 0;
vi_indent_cursor_pos : "echo foo" => ">>" => "\techo foo", 1;
vi_join_indent_lines : "echo foo\n\t\techo bar" => "J" => "echo foo echo bar", 8
} }

View File

@@ -66,7 +66,9 @@ impl ViMode for ViInsert {
flags: Default::default(), flags: Default::default(),
}), }),
E(K::Verbatim(seq), _) => { E(K::Verbatim(seq), _) => {
self.pending_cmd.set_verb(VerbCmd(1, Verb::Insert(seq.to_string()))); self
.pending_cmd
.set_verb(VerbCmd(1, Verb::Insert(seq.to_string())));
self.register_and_return() self.register_and_return()
} }
E(K::Char('W'), M::CTRL) => { E(K::Char('W'), M::CTRL) => {

View File

@@ -516,7 +516,8 @@ impl ShOptPrompt {
Ok(Some(output)) Ok(Some(output))
} }
"screensaver_idle_time" => { "screensaver_idle_time" => {
let mut output = String::from("Idle time in seconds before running screensaver_cmd (0 = disabled)\n"); let mut output =
String::from("Idle time in seconds before running screensaver_cmd (0 = disabled)\n");
output.push_str(&format!("{}", self.screensaver_idle_time)); output.push_str(&format!("{}", self.screensaver_idle_time));
Ok(Some(output)) Ok(Some(output))
} }
@@ -544,7 +545,10 @@ impl Display for ShOptPrompt {
output.push(format!("leader = {}", self.leader)); output.push(format!("leader = {}", self.leader));
output.push(format!("line_numbers = {}", self.line_numbers)); output.push(format!("line_numbers = {}", self.line_numbers));
output.push(format!("screensaver_cmd = {}", self.screensaver_cmd)); output.push(format!("screensaver_cmd = {}", self.screensaver_cmd));
output.push(format!("screensaver_idle_time = {}", self.screensaver_idle_time)); output.push(format!(
"screensaver_idle_time = {}",
self.screensaver_idle_time
));
let final_output = output.join("\n"); let final_output = output.join("\n");
@@ -576,8 +580,14 @@ mod tests {
#[test] #[test]
fn all_core_fields_covered() { fn all_core_fields_covered() {
let ShOptCore { let ShOptCore {
dotglob, autocd, hist_ignore_dupes, max_hist, dotglob,
interactive_comments, auto_hist, bell_enabled, max_recurse_depth, autocd,
hist_ignore_dupes,
max_hist,
interactive_comments,
auto_hist,
bell_enabled,
max_recurse_depth,
xpg_echo, xpg_echo,
} = ShOptCore::default(); } = ShOptCore::default();
// If a field is added to the struct, this destructure fails to compile. // If a field is added to the struct, this destructure fails to compile.

View File

@@ -1,5 +1,11 @@
use std::{ use std::{
cell::RefCell, collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, fmt::Display, ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign}, os::unix::fs::PermissionsExt, str::FromStr, time::Duration cell::RefCell,
collections::{HashMap, HashSet, VecDeque, hash_map::Entry},
fmt::Display,
ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign},
os::unix::fs::PermissionsExt,
str::FromStr,
time::Duration,
}; };
use nix::unistd::{User, gethostname, getppid}; use nix::unistd::{User, gethostname, getppid};
@@ -340,7 +346,7 @@ impl ScopeStack {
let random = rand::random_range(0..32768); let random = rand::random_range(0..32768);
Some(random.to_string()) Some(random.to_string())
} }
_ => None _ => None,
} }
} }
pub fn get_arr_elems(&self, var_name: &str) -> ShResult<Vec<String>> { pub fn get_arr_elems(&self, var_name: &str) -> ShResult<Vec<String>> {
@@ -528,7 +534,10 @@ impl ScopeStack {
return val.clone(); return val.clone();
} }
// Positional params are scope-local; only check the current scope // Positional params are scope-local; only check the current scope
if matches!(param, ShellParam::Pos(_) | ShellParam::AllArgs | ShellParam::AllArgsStr | ShellParam::ArgCount) { if matches!(
param,
ShellParam::Pos(_) | ShellParam::AllArgs | ShellParam::AllArgsStr | ShellParam::ArgCount
) {
if let Some(scope) = self.scopes.last() { if let Some(scope) = self.scopes.last() {
return scope.get_param(param); return scope.get_param(param);
} }
@@ -1011,19 +1020,7 @@ macro_rules! impl_var_from {
} }
impl_var_from!( impl_var_from!(
i8, i8, i16, i32, i64, isize, u8, u16, u32, u64, usize, String, &str, bool
i16,
i32,
i64,
isize,
u8,
u16,
u32,
u64,
usize,
String,
&str,
bool
); );
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]

View File

@@ -14,7 +14,12 @@ use nix::{
}; };
use crate::{ use crate::{
expand::expand_aliases, libsh::error::ShResult, parse::{ParsedSrc, Redir, RedirType, execute::exec_input, lex::LexFlags}, procio::{IoFrame, IoMode, RedirGuard}, readline::register::{restore_registers, save_registers}, state::{MetaTab, SHED, read_logic} expand::expand_aliases,
libsh::error::ShResult,
parse::{ParsedSrc, Redir, RedirType, execute::exec_input, lex::LexFlags},
procio::{IoFrame, IoMode, RedirGuard},
readline::register::{restore_registers, save_registers},
state::{MetaTab, SHED, read_logic},
}; };
static TEST_MUTEX: sync::Mutex<()> = sync::Mutex::new(()); static TEST_MUTEX: sync::Mutex<()> = sync::Mutex::new(());
@@ -40,7 +45,7 @@ pub struct TestGuard {
pty_master: OwnedFd, pty_master: OwnedFd,
pty_slave: OwnedFd, pty_slave: OwnedFd,
cleanups: Vec<Box<dyn FnOnce()>> cleanups: Vec<Box<dyn FnOnce()>>,
} }
impl TestGuard { impl TestGuard {
@@ -54,33 +59,27 @@ impl TestGuard {
tcsetattr(&pty_slave, SetArg::TCSANOW, &attrs).unwrap(); tcsetattr(&pty_slave, SetArg::TCSANOW, &attrs).unwrap();
let mut frame = IoFrame::new(); let mut frame = IoFrame::new();
frame.push( frame.push(Redir::new(
Redir::new(
IoMode::Fd { IoMode::Fd {
tgt_fd: 0, tgt_fd: 0,
src_fd: pty_slave.as_raw_fd(), src_fd: pty_slave.as_raw_fd(),
}, },
RedirType::Input, RedirType::Input,
), ));
); frame.push(Redir::new(
frame.push(
Redir::new(
IoMode::Fd { IoMode::Fd {
tgt_fd: 1, tgt_fd: 1,
src_fd: pty_slave.as_raw_fd(), src_fd: pty_slave.as_raw_fd(),
}, },
RedirType::Output, RedirType::Output,
), ));
); frame.push(Redir::new(
frame.push(
Redir::new(
IoMode::Fd { IoMode::Fd {
tgt_fd: 2, tgt_fd: 2,
src_fd: pty_slave.as_raw_fd(), src_fd: pty_slave.as_raw_fd(),
}, },
RedirType::Output, RedirType::Output,
), ));
);
let _redir_guard = frame.redirect().unwrap(); let _redir_guard = frame.redirect().unwrap();
@@ -113,7 +112,8 @@ impl TestGuard {
fcntl( fcntl(
self.pty_master.as_raw_fd(), self.pty_master.as_raw_fd(),
FcntlArg::F_SETFL(flags | OFlag::O_NONBLOCK), FcntlArg::F_SETFL(flags | OFlag::O_NONBLOCK),
).unwrap(); )
.unwrap();
let mut out = vec![]; let mut out = vec![];
let mut buf = [0; 4096]; let mut buf = [0; 4096];
@@ -125,10 +125,7 @@ impl TestGuard {
} }
} }
fcntl( fcntl(self.pty_master.as_raw_fd(), FcntlArg::F_SETFL(flags)).unwrap();
self.pty_master.as_raw_fd(),
FcntlArg::F_SETFL(flags),
).unwrap();
String::from_utf8_lossy(&out).to_string() String::from_utf8_lossy(&out).to_string()
} }
@@ -144,10 +141,14 @@ impl Drop for TestGuard {
fn drop(&mut self) { fn drop(&mut self) {
env::set_current_dir(&self.old_cwd).ok(); env::set_current_dir(&self.old_cwd).ok();
for (k, _) in env::vars() { for (k, _) in env::vars() {
unsafe { env::remove_var(&k); } unsafe {
env::remove_var(&k);
}
} }
for (k, v) in &self.saved_env { for (k, v) in &self.saved_env {
unsafe { env::set_var(k, v); } unsafe {
env::set_var(k, v);
}
} }
for cleanup in self.cleanups.drain(..).rev() { for cleanup in self.cleanups.drain(..).rev() {
cleanup(); cleanup();
@@ -166,13 +167,18 @@ pub fn get_ast(input: &str) -> ShResult<Vec<crate::parse::Node>> {
.with_lex_flags(LexFlags::empty()) .with_lex_flags(LexFlags::empty())
.with_name(source_name.clone()); .with_name(source_name.clone());
parser.parse_src().map_err(|e| e.into_iter().next().unwrap())?; parser
.parse_src()
.map_err(|e| e.into_iter().next().unwrap())?;
Ok(parser.extract_nodes()) Ok(parser.extract_nodes())
} }
impl crate::parse::Node { impl crate::parse::Node {
pub fn assert_structure(&mut self, expected: &mut impl Iterator<Item = NdKind>) -> Result<(), String> { pub fn assert_structure(
&mut self,
expected: &mut impl Iterator<Item = NdKind>,
) -> Result<(), String> {
let mut full_structure = vec![]; let mut full_structure = vec![];
let mut before = vec![]; let mut before = vec![];
let mut after = vec![]; let mut after = vec![];
@@ -182,7 +188,11 @@ impl crate::parse::Node {
let expected_rule = expected.next(); let expected_rule = expected.next();
full_structure.push(s.class.as_nd_kind()); full_structure.push(s.class.as_nd_kind());
if offender.is_none() && expected_rule.as_ref().map_or(true, |e| *e != s.class.as_nd_kind()) { if offender.is_none()
&& expected_rule
.as_ref()
.map_or(true, |e| *e != s.class.as_nd_kind())
{
offender = Some((s.class.as_nd_kind(), expected_rule)); offender = Some((s.class.as_nd_kind(), expected_rule));
} else if offender.is_none() { } else if offender.is_none() {
before.push(s.class.as_nd_kind()); before.push(s.class.as_nd_kind());
@@ -191,23 +201,34 @@ impl crate::parse::Node {
} }
}); });
assert!(expected.next().is_none(), "Expected structure has more nodes than actual structure"); assert!(
expected.next().is_none(),
"Expected structure has more nodes than actual structure"
);
if let Some((nd_kind, expected_rule)) = offender { if let Some((nd_kind, expected_rule)) = offender {
let expected_rule = expected_rule.map_or("(none — expected array too short)".into(), |e| format!("{e:?}")); let expected_rule = expected_rule.map_or("(none — expected array too short)".into(), |e| {
let full_structure_hint = full_structure.into_iter() format!("{e:?}")
});
let full_structure_hint = full_structure
.into_iter()
.map(|s| format!("\tNdKind::{s:?},")) .map(|s| format!("\tNdKind::{s:?},"))
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n"); .join("\n");
let full_structure_hint = format!("let expected = &mut [\n{full_structure_hint}\n].into_iter();"); let full_structure_hint =
format!("let expected = &mut [\n{full_structure_hint}\n].into_iter();");
let output = [ let output = [
"Structure assertion failed!\n".into(), "Structure assertion failed!\n".into(),
format!("Expected node type '{:?}', found '{:?}'", expected_rule, nd_kind), format!(
"Expected node type '{:?}', found '{:?}'",
expected_rule, nd_kind
),
format!("Before offender: {:?}", before), format!("Before offender: {:?}", before),
format!("After offender: {:?}\n", after), format!("After offender: {:?}\n", after),
format!("hint: here is the full structure as an array\n {full_structure_hint}"), format!("hint: here is the full structure as an array\n {full_structure_hint}"),
].join("\n"); ]
.join("\n");
Err(output) Err(output)
} else { } else {