completely rewrote test suite for top level src files and all builtin files
This commit is contained in:
@@ -38,21 +38,30 @@ pub fn alias(node: Node) -> ShResult<()> {
|
||||
write(stdout, alias_output.as_bytes())?; // Write it
|
||||
} else {
|
||||
for (arg, span) in argv {
|
||||
if arg == "command" || arg == "builtin" {
|
||||
|
||||
let Some((name, body)) = arg.split_once('=') else {
|
||||
let Some(alias) = read_logic(|l| l.get_alias(&arg)) else {
|
||||
return Err(ShErr::at(
|
||||
ShErrKind::SyntaxErr,
|
||||
span,
|
||||
"alias: Expected an assignment in alias args",
|
||||
));
|
||||
};
|
||||
|
||||
let alias_output = format!("{arg}='{alias}'");
|
||||
|
||||
let stdout = borrow_fd(STDOUT_FILENO);
|
||||
write(stdout, alias_output.as_bytes())?; // Write it
|
||||
state::set_status(0);
|
||||
return Ok(());
|
||||
};
|
||||
if name == "command" || name == "builtin" {
|
||||
return Err(ShErr::at(
|
||||
ShErrKind::ExecFail,
|
||||
span,
|
||||
format!("alias: Cannot assign alias to reserved name '{arg}'"),
|
||||
format!("alias: Cannot assign alias to reserved name '{}'", name.fg(next_color())),
|
||||
));
|
||||
}
|
||||
|
||||
let Some((name, body)) = arg.split_once('=') else {
|
||||
return Err(ShErr::at(
|
||||
ShErrKind::SyntaxErr,
|
||||
span,
|
||||
"alias: Expected an assignment in alias args",
|
||||
));
|
||||
};
|
||||
write_logic(|l| l.insert_alias(name, body, span.clone()));
|
||||
}
|
||||
}
|
||||
@@ -60,6 +69,7 @@ pub fn alias(node: Node) -> ShResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove one or more aliases by name
|
||||
pub fn unalias(node: Node) -> ShResult<()> {
|
||||
let NdRule::Command {
|
||||
assignments: _,
|
||||
@@ -103,3 +113,164 @@ pub fn unalias(node: Node) -> ShResult<()> {
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::state::{self, read_logic};
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn alias_set_and_expand() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("alias ll='ls -la'").unwrap();
|
||||
|
||||
let alias = read_logic(|l| l.get_alias("ll"));
|
||||
assert!(alias.is_some());
|
||||
assert_eq!(alias.unwrap().body, "ls -la");
|
||||
|
||||
test_input("alias ll").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("ll"));
|
||||
assert!(out.contains("ls -la"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_multiple() {
|
||||
let _guard = TestGuard::new();
|
||||
test_input("alias a='echo a' b='echo b'").unwrap();
|
||||
|
||||
assert_eq!(read_logic(|l| l.get_alias("a")).unwrap().body, "echo a");
|
||||
assert_eq!(read_logic(|l| l.get_alias("b")).unwrap().body, "echo b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_overwrite() {
|
||||
let _guard = TestGuard::new();
|
||||
test_input("alias x='first'").unwrap();
|
||||
test_input("alias x='second'").unwrap();
|
||||
|
||||
assert_eq!(read_logic(|l| l.get_alias("x")).unwrap().body, "second");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_list_sorted() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("alias z='zzz' a='aaa' m='mmm'").unwrap();
|
||||
guard.read_output();
|
||||
|
||||
test_input("alias").unwrap();
|
||||
let out = guard.read_output();
|
||||
let lines: Vec<&str> = out.lines().collect();
|
||||
|
||||
assert!(lines.len() >= 3);
|
||||
let a_pos = lines.iter().position(|l| l.contains("a =")).unwrap();
|
||||
let m_pos = lines.iter().position(|l| l.contains("m =")).unwrap();
|
||||
let z_pos = lines.iter().position(|l| l.contains("z =")).unwrap();
|
||||
assert!(a_pos < m_pos);
|
||||
assert!(m_pos < z_pos);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_reserved_name_command() {
|
||||
let _guard = TestGuard::new();
|
||||
let result = test_input("alias command='something'");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_reserved_name_builtin() {
|
||||
let _guard = TestGuard::new();
|
||||
let result = test_input("alias builtin='something'");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_missing_equals() {
|
||||
let _guard = TestGuard::new();
|
||||
let result = test_input("alias noequals");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_expansion_in_command() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("alias greet='echo hello'").unwrap();
|
||||
guard.read_output();
|
||||
|
||||
test_input("greet").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "hello\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_expansion_with_args() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("alias e='echo'").unwrap();
|
||||
guard.read_output();
|
||||
|
||||
test_input("e foo bar").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "foo bar\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unalias_removes() {
|
||||
let _guard = TestGuard::new();
|
||||
test_input("alias tmp='something'").unwrap();
|
||||
assert!(read_logic(|l| l.get_alias("tmp")).is_some());
|
||||
|
||||
test_input("unalias tmp").unwrap();
|
||||
assert!(read_logic(|l| l.get_alias("tmp")).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unalias_nonexistent() {
|
||||
let _guard = TestGuard::new();
|
||||
let result = test_input("unalias nosuchalias");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unalias_multiple() {
|
||||
let _guard = TestGuard::new();
|
||||
test_input("alias a='1' b='2' c='3'").unwrap();
|
||||
test_input("unalias a c").unwrap();
|
||||
|
||||
assert!(read_logic(|l| l.get_alias("a")).is_none());
|
||||
assert!(read_logic(|l| l.get_alias("b")).is_some());
|
||||
assert!(read_logic(|l| l.get_alias("c")).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unalias_no_args_lists() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("alias x='hello'").unwrap();
|
||||
guard.read_output();
|
||||
|
||||
test_input("unalias").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("x"));
|
||||
assert!(out.contains("hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_empty_body() {
|
||||
let _guard = TestGuard::new();
|
||||
test_input("alias empty=''").unwrap();
|
||||
|
||||
let alias = read_logic(|l| l.get_alias("empty"));
|
||||
assert!(alias.is_some());
|
||||
assert_eq!(alias.unwrap().body, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_status_zero() {
|
||||
let _guard = TestGuard::new();
|
||||
test_input("alias ok='true'").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
|
||||
test_input("unalias ok").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,3 +226,266 @@ pub fn get_arr_op_opts(opts: Vec<Opt>) -> ShResult<ArrOpOpts> {
|
||||
}
|
||||
Ok(arr_op_opts)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::VecDeque;
|
||||
use crate::state::{self, read_vars, write_vars, VarFlags, VarKind};
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
|
||||
fn set_arr(name: &str, elems: &[&str]) {
|
||||
let arr = VecDeque::from_iter(elems.iter().map(|s| s.to_string()));
|
||||
write_vars(|v| v.set_var(name, VarKind::Arr(arr), VarFlags::NONE)).unwrap();
|
||||
}
|
||||
|
||||
fn get_arr(name: &str) -> Vec<String> {
|
||||
read_vars(|v| v.get_arr_elems(name)).unwrap()
|
||||
}
|
||||
|
||||
// ===================== push =====================
|
||||
|
||||
#[test]
|
||||
fn push_to_existing_array() {
|
||||
let _guard = TestGuard::new();
|
||||
set_arr("arr", &["a", "b"]);
|
||||
|
||||
test_input("push arr c").unwrap();
|
||||
assert_eq!(get_arr("arr"), vec!["a", "b", "c"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_creates_array() {
|
||||
let _guard = TestGuard::new();
|
||||
|
||||
test_input("push newarr hello").unwrap();
|
||||
assert_eq!(get_arr("newarr"), vec!["hello"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_multiple_values() {
|
||||
let _guard = TestGuard::new();
|
||||
set_arr("arr", &["a"]);
|
||||
|
||||
test_input("push arr b c d").unwrap();
|
||||
assert_eq!(get_arr("arr"), vec!["a", "b", "c", "d"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_no_array_name() {
|
||||
let _guard = TestGuard::new();
|
||||
let result = test_input("push");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ===================== fpush =====================
|
||||
|
||||
#[test]
|
||||
fn fpush_to_existing_array() {
|
||||
let _guard = TestGuard::new();
|
||||
set_arr("arr", &["b", "c"]);
|
||||
|
||||
test_input("fpush arr a").unwrap();
|
||||
assert_eq!(get_arr("arr"), vec!["a", "b", "c"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fpush_multiple_values() {
|
||||
let _guard = TestGuard::new();
|
||||
set_arr("arr", &["c"]);
|
||||
|
||||
test_input("fpush arr a b").unwrap();
|
||||
// Each value is pushed to the front in order: c -> a,c -> b,a,c
|
||||
assert_eq!(get_arr("arr"), vec!["b", "a", "c"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fpush_creates_array() {
|
||||
let _guard = TestGuard::new();
|
||||
|
||||
test_input("fpush newarr x").unwrap();
|
||||
assert_eq!(get_arr("newarr"), vec!["x"]);
|
||||
}
|
||||
|
||||
// ===================== pop =====================
|
||||
|
||||
#[test]
|
||||
fn pop_removes_last() {
|
||||
let guard = TestGuard::new();
|
||||
set_arr("arr", &["a", "b", "c"]);
|
||||
|
||||
test_input("pop arr").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "c\n");
|
||||
assert_eq!(get_arr("arr"), vec!["a", "b"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pop_with_count() {
|
||||
let guard = TestGuard::new();
|
||||
set_arr("arr", &["a", "b", "c", "d"]);
|
||||
|
||||
test_input("pop -c 2 arr").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "d\nc\n");
|
||||
assert_eq!(get_arr("arr"), vec!["a", "b"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pop_into_variable() {
|
||||
let _guard = TestGuard::new();
|
||||
set_arr("arr", &["x", "y", "z"]);
|
||||
|
||||
test_input("pop -v result arr").unwrap();
|
||||
let val = read_vars(|v| v.get_var("result"));
|
||||
assert_eq!(val, "z");
|
||||
assert_eq!(get_arr("arr"), vec!["x", "y"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pop_empty_array_fails() {
|
||||
let _guard = TestGuard::new();
|
||||
set_arr("arr", &[]);
|
||||
|
||||
test_input("pop arr").unwrap();
|
||||
assert_eq!(state::get_status(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pop_nonexistent_array() {
|
||||
let _guard = TestGuard::new();
|
||||
|
||||
test_input("pop nosucharray").unwrap();
|
||||
assert_eq!(state::get_status(), 1);
|
||||
}
|
||||
|
||||
// ===================== fpop =====================
|
||||
|
||||
#[test]
|
||||
fn fpop_removes_first() {
|
||||
let guard = TestGuard::new();
|
||||
set_arr("arr", &["a", "b", "c"]);
|
||||
|
||||
test_input("fpop arr").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "a\n");
|
||||
assert_eq!(get_arr("arr"), vec!["b", "c"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fpop_with_count() {
|
||||
let guard = TestGuard::new();
|
||||
set_arr("arr", &["a", "b", "c", "d"]);
|
||||
|
||||
test_input("fpop -c 2 arr").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "a\nb\n");
|
||||
assert_eq!(get_arr("arr"), vec!["c", "d"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fpop_into_variable() {
|
||||
let _guard = TestGuard::new();
|
||||
set_arr("arr", &["first", "second"]);
|
||||
|
||||
test_input("fpop -v result arr").unwrap();
|
||||
let val = read_vars(|v| v.get_var("result"));
|
||||
assert_eq!(val, "first");
|
||||
assert_eq!(get_arr("arr"), vec!["second"]);
|
||||
}
|
||||
|
||||
// ===================== rotate =====================
|
||||
|
||||
#[test]
|
||||
fn rotate_left_default() {
|
||||
let _guard = TestGuard::new();
|
||||
set_arr("arr", &["a", "b", "c", "d"]);
|
||||
|
||||
test_input("rotate arr").unwrap();
|
||||
assert_eq!(get_arr("arr"), vec!["b", "c", "d", "a"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotate_left_with_count() {
|
||||
let _guard = TestGuard::new();
|
||||
set_arr("arr", &["a", "b", "c", "d"]);
|
||||
|
||||
test_input("rotate -c 2 arr").unwrap();
|
||||
assert_eq!(get_arr("arr"), vec!["c", "d", "a", "b"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotate_right() {
|
||||
let _guard = TestGuard::new();
|
||||
set_arr("arr", &["a", "b", "c", "d"]);
|
||||
|
||||
test_input("rotate -r arr").unwrap();
|
||||
assert_eq!(get_arr("arr"), vec!["d", "a", "b", "c"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotate_right_with_count() {
|
||||
let _guard = TestGuard::new();
|
||||
set_arr("arr", &["a", "b", "c", "d"]);
|
||||
|
||||
test_input("rotate -r -c 2 arr").unwrap();
|
||||
assert_eq!(get_arr("arr"), vec!["c", "d", "a", "b"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotate_count_exceeds_len() {
|
||||
let _guard = TestGuard::new();
|
||||
set_arr("arr", &["a", "b"]);
|
||||
|
||||
// count clamped to arr.len(), so rotate by 2 on len=2 is a no-op
|
||||
test_input("rotate -c 5 arr").unwrap();
|
||||
assert_eq!(get_arr("arr"), vec!["a", "b"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotate_single_element() {
|
||||
let _guard = TestGuard::new();
|
||||
set_arr("arr", &["only"]);
|
||||
|
||||
test_input("rotate arr").unwrap();
|
||||
assert_eq!(get_arr("arr"), vec!["only"]);
|
||||
}
|
||||
|
||||
// ===================== combined ops =====================
|
||||
|
||||
#[test]
|
||||
fn push_then_pop_roundtrip() {
|
||||
let guard = TestGuard::new();
|
||||
set_arr("arr", &["a"]);
|
||||
|
||||
test_input("push arr b").unwrap();
|
||||
test_input("pop arr").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "b\n");
|
||||
assert_eq!(get_arr("arr"), vec!["a"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fpush_then_fpop_roundtrip() {
|
||||
let guard = TestGuard::new();
|
||||
set_arr("arr", &["a"]);
|
||||
|
||||
test_input("fpush arr z").unwrap();
|
||||
test_input("fpop arr").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "z\n");
|
||||
assert_eq!(get_arr("arr"), vec!["a"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pop_until_empty() {
|
||||
let _guard = TestGuard::new();
|
||||
set_arr("arr", &["x", "y"]);
|
||||
|
||||
test_input("pop arr").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
test_input("pop arr").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
test_input("pop arr").unwrap();
|
||||
assert_eq!(state::get_status(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,3 +111,202 @@ pub fn autocmd(node: Node) -> ShResult<()> {
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::state::{self, AutoCmdKind, read_logic, write_logic};
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
|
||||
// ===================== Registration =====================
|
||||
|
||||
#[test]
|
||||
fn register_pre_cmd() {
|
||||
let _guard = TestGuard::new();
|
||||
test_input("autocmd pre-cmd 'echo hello'").unwrap();
|
||||
|
||||
let cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd));
|
||||
assert_eq!(cmds.len(), 1);
|
||||
assert_eq!(cmds[0].command, "echo hello");
|
||||
assert!(cmds[0].pattern.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_post_cmd() {
|
||||
let _guard = TestGuard::new();
|
||||
test_input("autocmd post-cmd 'echo done'").unwrap();
|
||||
|
||||
let cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::PostCmd));
|
||||
assert_eq!(cmds.len(), 1);
|
||||
assert_eq!(cmds[0].command, "echo done");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_multiple_same_kind() {
|
||||
let _guard = TestGuard::new();
|
||||
test_input("autocmd pre-cmd 'echo first'").unwrap();
|
||||
test_input("autocmd pre-cmd 'echo second'").unwrap();
|
||||
|
||||
let cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd));
|
||||
assert_eq!(cmds.len(), 2);
|
||||
assert_eq!(cmds[0].command, "echo first");
|
||||
assert_eq!(cmds[1].command, "echo second");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_different_kinds() {
|
||||
let _guard = TestGuard::new();
|
||||
test_input("autocmd pre-cmd 'echo pre'").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::PostCmd)).len(), 1);
|
||||
}
|
||||
|
||||
// ===================== Pattern =====================
|
||||
|
||||
#[test]
|
||||
fn register_with_pattern() {
|
||||
let _guard = TestGuard::new();
|
||||
test_input("autocmd -p '^git' pre-cmd 'echo git cmd'").unwrap();
|
||||
|
||||
let cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd));
|
||||
assert_eq!(cmds.len(), 1);
|
||||
assert!(cmds[0].pattern.is_some());
|
||||
let pat = cmds[0].pattern.as_ref().unwrap();
|
||||
assert!(pat.is_match("git status"));
|
||||
assert!(!pat.is_match("echo git"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_regex_pattern() {
|
||||
let _guard = TestGuard::new();
|
||||
let result = test_input("autocmd -p '[invalid' pre-cmd 'echo bad'");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ===================== Clear =====================
|
||||
|
||||
#[test]
|
||||
fn clear_autocmds() {
|
||||
let _guard = TestGuard::new();
|
||||
test_input("autocmd pre-cmd 'echo a'").unwrap();
|
||||
test_input("autocmd pre-cmd 'echo b'").unwrap();
|
||||
assert_eq!(read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd)).len(), 2);
|
||||
|
||||
test_input("autocmd -c pre-cmd").unwrap();
|
||||
assert_eq!(read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd)).len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_only_affects_specified_kind() {
|
||||
let _guard = TestGuard::new();
|
||||
test_input("autocmd pre-cmd 'echo pre'").unwrap();
|
||||
test_input("autocmd post-cmd 'echo post'").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::PostCmd)).len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_empty_is_noop() {
|
||||
let _guard = TestGuard::new();
|
||||
// Clearing when nothing is registered should not error
|
||||
test_input("autocmd -c pre-cmd").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
// ===================== Error Cases =====================
|
||||
|
||||
#[test]
|
||||
fn missing_kind() {
|
||||
let _guard = TestGuard::new();
|
||||
let result = test_input("autocmd");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_kind() {
|
||||
let _guard = TestGuard::new();
|
||||
let result = test_input("autocmd not-a-real-kind 'echo hi'");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_command() {
|
||||
let _guard = TestGuard::new();
|
||||
let result = test_input("autocmd pre-cmd");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ===================== All valid kind strings =====================
|
||||
|
||||
#[test]
|
||||
fn all_kinds_parse() {
|
||||
let _guard = TestGuard::new();
|
||||
let kinds = [
|
||||
"pre-cmd", "post-cmd", "pre-change-dir", "post-change-dir",
|
||||
"on-job-finish", "pre-prompt", "post-prompt",
|
||||
"pre-mode-change", "post-mode-change",
|
||||
"on-history-open", "on-history-close", "on-history-select",
|
||||
"on-completion-start", "on-completion-cancel", "on-completion-select",
|
||||
"on-exit",
|
||||
];
|
||||
for kind in kinds {
|
||||
test_input(&format!("autocmd {kind} 'true'")).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== Execution =====================
|
||||
|
||||
#[test]
|
||||
fn exec_fires_autocmd() {
|
||||
let guard = TestGuard::new();
|
||||
// Register a post-change-dir autocmd and trigger it via cd
|
||||
test_input("autocmd post-change-dir 'echo changed'").unwrap();
|
||||
guard.read_output();
|
||||
|
||||
test_input("cd /tmp").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("changed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_with_pattern_match() {
|
||||
let guard = TestGuard::new();
|
||||
// Pattern that matches "cd" commands
|
||||
test_input("autocmd -p '/tmp' post-change-dir 'echo matched'").unwrap();
|
||||
guard.read_output();
|
||||
|
||||
test_input("cd /tmp").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("matched"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_with_pattern_no_match() {
|
||||
let guard = TestGuard::new();
|
||||
// Pattern that won't match /tmp
|
||||
test_input("autocmd -p '^/usr' post-change-dir 'echo nope'").unwrap();
|
||||
guard.read_output();
|
||||
|
||||
test_input("cd /tmp").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(!out.contains("nope"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_preserves_status() {
|
||||
let _guard = TestGuard::new();
|
||||
// autocmd exec should restore the status code from before it ran
|
||||
test_input("autocmd post-change-dir 'false'").unwrap();
|
||||
|
||||
test_input("true").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
|
||||
test_input("cd /tmp").unwrap();
|
||||
// cd itself succeeds, autocmd runs `false` but status should be
|
||||
// restored to cd's success
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,3 +75,162 @@ pub fn cd(node: Node) -> ShResult<()> {
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use std::env;
|
||||
use std::fs;
|
||||
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::state;
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
|
||||
// ===================== Basic Navigation =====================
|
||||
|
||||
#[test]
|
||||
fn cd_simple() {
|
||||
let _g = TestGuard::new();
|
||||
let old_dir = env::current_dir().unwrap();
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
|
||||
test_input(format!("cd {}", temp_dir.path().display())).unwrap();
|
||||
|
||||
let new_dir = env::current_dir().unwrap();
|
||||
assert_ne!(old_dir, new_dir);
|
||||
|
||||
assert_eq!(new_dir.display().to_string(), temp_dir.path().display().to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cd_no_args_goes_home() {
|
||||
let _g = TestGuard::new();
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
unsafe { env::set_var("HOME", temp_dir.path()) };
|
||||
|
||||
test_input("cd").unwrap();
|
||||
|
||||
let cwd = env::current_dir().unwrap();
|
||||
assert_eq!(cwd.display().to_string(), temp_dir.path().display().to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cd_relative_path() {
|
||||
let _g = TestGuard::new();
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let sub = temp_dir.path().join("child");
|
||||
fs::create_dir(&sub).unwrap();
|
||||
|
||||
test_input(format!("cd {}", temp_dir.path().display())).unwrap();
|
||||
test_input("cd child").unwrap();
|
||||
|
||||
let cwd = env::current_dir().unwrap();
|
||||
assert_eq!(cwd.display().to_string(), sub.display().to_string());
|
||||
}
|
||||
|
||||
// ===================== Environment =====================
|
||||
|
||||
#[test]
|
||||
fn cd_sets_pwd_env() {
|
||||
let _g = TestGuard::new();
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
|
||||
test_input(format!("cd {}", temp_dir.path().display())).unwrap();
|
||||
|
||||
let pwd = env::var("PWD").unwrap();
|
||||
assert_eq!(pwd, env::current_dir().unwrap().display().to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cd_status_zero_on_success() {
|
||||
let _g = TestGuard::new();
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
|
||||
test_input(format!("cd {}", temp_dir.path().display())).unwrap();
|
||||
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
// ===================== Error Cases =====================
|
||||
|
||||
#[test]
|
||||
fn cd_nonexistent_dir_fails() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("cd /nonexistent_path_that_does_not_exist_xyz");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cd_file_not_directory_fails() {
|
||||
let _g = TestGuard::new();
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let file_path = temp_dir.path().join("afile.txt");
|
||||
fs::write(&file_path, "hello").unwrap();
|
||||
|
||||
let result = test_input(format!("cd {}", file_path.display()));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ===================== Multiple cd =====================
|
||||
|
||||
#[test]
|
||||
fn cd_multiple_times() {
|
||||
let _g = TestGuard::new();
|
||||
let dir_a = TempDir::new().unwrap();
|
||||
let dir_b = TempDir::new().unwrap();
|
||||
|
||||
test_input(format!("cd {}", dir_a.path().display())).unwrap();
|
||||
assert_eq!(
|
||||
env::current_dir().unwrap().display().to_string(),
|
||||
dir_a.path().display().to_string()
|
||||
);
|
||||
|
||||
test_input(format!("cd {}", dir_b.path().display())).unwrap();
|
||||
assert_eq!(
|
||||
env::current_dir().unwrap().display().to_string(),
|
||||
dir_b.path().display().to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cd_nested_subdirectories() {
|
||||
let _g = TestGuard::new();
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let deep = temp_dir.path().join("a").join("b").join("c");
|
||||
fs::create_dir_all(&deep).unwrap();
|
||||
|
||||
test_input(format!("cd {}", deep.display())).unwrap();
|
||||
assert_eq!(
|
||||
env::current_dir().unwrap().display().to_string(),
|
||||
deep.display().to_string()
|
||||
);
|
||||
}
|
||||
|
||||
// ===================== Autocmd Integration =====================
|
||||
|
||||
#[test]
|
||||
fn cd_fires_post_change_dir_autocmd() {
|
||||
let guard = TestGuard::new();
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
|
||||
test_input("autocmd post-change-dir 'echo cd-hook-fired'").unwrap();
|
||||
guard.read_output();
|
||||
|
||||
test_input(format!("cd {}", temp_dir.path().display())).unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("cd-hook-fired"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cd_fires_pre_change_dir_autocmd() {
|
||||
let guard = TestGuard::new();
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
|
||||
test_input("autocmd pre-change-dir 'echo pre-cd'").unwrap();
|
||||
guard.read_output();
|
||||
|
||||
test_input(format!("cd {}", temp_dir.path().display())).unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("pre-cd"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,20 +173,24 @@ pub fn complete_builtin(node: Node) -> ShResult<()> {
|
||||
|
||||
if comp_opts.flags.contains(CompFlags::PRINT) {
|
||||
if argv.is_empty() {
|
||||
read_meta(|m| {
|
||||
read_meta(|m| -> ShResult<()> {
|
||||
let specs = m.comp_specs().values();
|
||||
for spec in specs {
|
||||
println!("{}", spec.source());
|
||||
let stdout = borrow_fd(STDOUT_FILENO);
|
||||
write(stdout, spec.source().as_bytes())?;
|
||||
}
|
||||
})
|
||||
Ok(())
|
||||
})?;
|
||||
} else {
|
||||
read_meta(|m| {
|
||||
read_meta(|m| -> ShResult<()> {
|
||||
for (cmd, _) in &argv {
|
||||
if let Some(spec) = m.comp_specs().get(cmd) {
|
||||
println!("{}", spec.source());
|
||||
let stdout = borrow_fd(STDOUT_FILENO);
|
||||
write(stdout, spec.source().as_bytes())?;
|
||||
}
|
||||
}
|
||||
})
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
|
||||
state::set_status(0);
|
||||
@@ -309,3 +313,318 @@ pub fn get_comp_opts(opts: Vec<Opt>) -> ShResult<CompOpts> {
|
||||
|
||||
Ok(comp_opts)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
use crate::state::{self, read_meta, write_vars, VarFlags, VarKind};
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
|
||||
// ===================== complete: Registration =====================
|
||||
|
||||
#[test]
|
||||
fn complete_register_wordlist() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("complete -W 'foo bar baz' mycmd").unwrap();
|
||||
|
||||
let spec = read_meta(|m| m.get_comp_spec("mycmd"));
|
||||
assert!(spec.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_register_files() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("complete -f mycmd").unwrap();
|
||||
|
||||
let spec = read_meta(|m| m.get_comp_spec("mycmd"));
|
||||
assert!(spec.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_register_dirs() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("complete -d mycmd").unwrap();
|
||||
|
||||
let spec = read_meta(|m| m.get_comp_spec("mycmd"));
|
||||
assert!(spec.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_register_multiple_commands() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("complete -W 'x y' cmd1 cmd2").unwrap();
|
||||
|
||||
assert!(read_meta(|m| m.get_comp_spec("cmd1")).is_some());
|
||||
assert!(read_meta(|m| m.get_comp_spec("cmd2")).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_register_function() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("complete -F _my_comp mycmd").unwrap();
|
||||
|
||||
let spec = read_meta(|m| m.get_comp_spec("mycmd"));
|
||||
assert!(spec.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_register_combined_flags() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("complete -f -d -v mycmd").unwrap();
|
||||
|
||||
let spec = read_meta(|m| m.get_comp_spec("mycmd"));
|
||||
assert!(spec.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_overwrite_spec() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("complete -W 'old' mycmd").unwrap();
|
||||
test_input("complete -W 'new' mycmd").unwrap();
|
||||
|
||||
let spec = read_meta(|m| m.get_comp_spec("mycmd"));
|
||||
assert!(spec.is_some());
|
||||
// Verify the source reflects the latest registration
|
||||
assert!(spec.unwrap().source().contains("new"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_no_command_fails() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("complete -W 'foo'");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ===================== complete -r: Removal =====================
|
||||
|
||||
#[test]
|
||||
fn complete_remove_spec() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("complete -W 'foo' mycmd").unwrap();
|
||||
assert!(read_meta(|m| m.get_comp_spec("mycmd")).is_some());
|
||||
|
||||
test_input("complete -r mycmd").unwrap();
|
||||
assert!(read_meta(|m| m.get_comp_spec("mycmd")).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_remove_multiple() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("complete -W 'a' cmd1").unwrap();
|
||||
test_input("complete -W 'b' cmd2").unwrap();
|
||||
|
||||
test_input("complete -r cmd1 cmd2").unwrap();
|
||||
assert!(read_meta(|m| m.get_comp_spec("cmd1")).is_none());
|
||||
assert!(read_meta(|m| m.get_comp_spec("cmd2")).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_remove_nonexistent_is_ok() {
|
||||
let _g = TestGuard::new();
|
||||
// Removing a spec that doesn't exist should not error
|
||||
test_input("complete -r nosuchcmd").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
// ===================== complete -p: Print =====================
|
||||
|
||||
#[test]
|
||||
fn complete_print_specific() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("complete -W 'alpha beta' mycmd").unwrap();
|
||||
guard.read_output();
|
||||
|
||||
test_input("complete -p mycmd").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("mycmd"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_print_all() {
|
||||
let guard = TestGuard::new();
|
||||
// Clear any existing specs and register two
|
||||
test_input("complete -W 'a' cmd1").unwrap();
|
||||
test_input("complete -W 'b' cmd2").unwrap();
|
||||
guard.read_output();
|
||||
|
||||
test_input("complete -p").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("cmd1"));
|
||||
assert!(out.contains("cmd2"));
|
||||
}
|
||||
|
||||
// ===================== complete -o: Option flags =====================
|
||||
|
||||
#[test]
|
||||
fn complete_option_default() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("complete -o default -W 'foo' mycmd").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_option_dirnames() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("complete -o dirnames -W 'foo' mycmd").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_option_invalid() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("complete -o bogus -W 'foo' mycmd");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ===================== compgen -W: Word list =====================
|
||||
|
||||
#[test]
|
||||
fn compgen_wordlist_no_prefix() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("compgen -W 'alpha beta gamma'").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("alpha"));
|
||||
assert!(out.contains("beta"));
|
||||
assert!(out.contains("gamma"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compgen_wordlist_with_prefix() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("compgen -W 'apple banana avocado' a").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("apple"));
|
||||
assert!(out.contains("avocado"));
|
||||
assert!(!out.contains("banana"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compgen_wordlist_no_match() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("compgen -W 'foo bar baz' z").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.trim().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compgen_wordlist_exact_match() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("compgen -W 'hello help helm' hel").unwrap();
|
||||
let out = guard.read_output();
|
||||
let lines: Vec<&str> = out.lines().collect();
|
||||
assert_eq!(lines.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compgen_wordlist_single_match() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("compgen -W 'alpha beta gamma' g").unwrap();
|
||||
let out = guard.read_output();
|
||||
let lines: Vec<&str> = out.lines().collect();
|
||||
assert_eq!(lines.len(), 1);
|
||||
assert_eq!(lines[0], "gamma");
|
||||
}
|
||||
|
||||
// ===================== compgen -v: Variables =====================
|
||||
|
||||
#[test]
|
||||
fn compgen_variables() {
|
||||
let guard = TestGuard::new();
|
||||
write_vars(|v| v.set_var("TESTCOMPVAR", VarKind::Str("x".into()), VarFlags::NONE)).unwrap();
|
||||
|
||||
test_input("compgen -v TESTCOMP").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("TESTCOMPVAR"));
|
||||
}
|
||||
|
||||
// ===================== compgen -a: Aliases =====================
|
||||
|
||||
#[test]
|
||||
fn compgen_aliases() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("alias testcompalias='echo hi'").unwrap();
|
||||
guard.read_output();
|
||||
|
||||
test_input("compgen -a testcomp").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("testcompalias"));
|
||||
}
|
||||
|
||||
// ===================== compgen -d: Directories =====================
|
||||
|
||||
#[test]
|
||||
fn compgen_dirs() {
|
||||
let guard = TestGuard::new();
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let sub = tmp.path().join("subdir");
|
||||
fs::create_dir(&sub).unwrap();
|
||||
|
||||
let prefix = format!("{}/", tmp.path().display());
|
||||
test_input(format!("compgen -d {prefix}")).unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("subdir"));
|
||||
}
|
||||
|
||||
// ===================== compgen -f: Files =====================
|
||||
|
||||
#[test]
|
||||
fn compgen_files() {
|
||||
let guard = TestGuard::new();
|
||||
let tmp = TempDir::new().unwrap();
|
||||
fs::write(tmp.path().join("testfile.txt"), "").unwrap();
|
||||
fs::create_dir(tmp.path().join("testdir")).unwrap();
|
||||
|
||||
let prefix = format!("{}/test", tmp.path().display());
|
||||
test_input(format!("compgen -f {prefix}")).unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("testfile.txt"));
|
||||
assert!(out.contains("testdir"));
|
||||
}
|
||||
|
||||
// ===================== compgen -F: Completion function =====================
|
||||
|
||||
#[test]
|
||||
fn compgen_function() {
|
||||
let guard = TestGuard::new();
|
||||
// Define a completion function that sets COMPREPLY
|
||||
test_input("_mycomp() { COMPREPLY=(opt1 opt2 opt3); }").unwrap();
|
||||
guard.read_output();
|
||||
|
||||
test_input("compgen -F _mycomp").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("opt1"));
|
||||
assert!(out.contains("opt2"));
|
||||
assert!(out.contains("opt3"));
|
||||
}
|
||||
|
||||
// ===================== compgen: combined flags =====================
|
||||
|
||||
#[test]
|
||||
fn compgen_wordlist_and_aliases() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("alias testcga='true'").unwrap();
|
||||
guard.read_output();
|
||||
|
||||
test_input("compgen -W 'testcgw' -a testcg").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("testcgw"));
|
||||
assert!(out.contains("testcga"));
|
||||
}
|
||||
|
||||
// ===================== Status =====================
|
||||
|
||||
#[test]
|
||||
fn complete_status_zero() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("complete -W 'x' mycmd").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compgen_status_zero() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("compgen -W 'hello'").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,21 @@ use nix::{libc::STDOUT_FILENO, unistd::write};
|
||||
use yansi::Color;
|
||||
|
||||
use crate::{
|
||||
libsh::error::{ShErr, ShErrKind, ShResult, next_color},
|
||||
libsh::{error::{ShErr, ShErrKind, ShResult, next_color}, sys::TTY_FILENO},
|
||||
parse::{NdRule, Node, execute::prepare_argv, lex::Span},
|
||||
procio::borrow_fd,
|
||||
state::{self, read_meta, write_meta},
|
||||
};
|
||||
|
||||
pub fn truncate_home_path(path: String) -> String {
|
||||
if let Ok(home) = env::var("HOME")
|
||||
&& path.starts_with(&home) {
|
||||
let new = path.strip_prefix(&home).unwrap();
|
||||
return format!("~{new}");
|
||||
}
|
||||
path.to_string()
|
||||
}
|
||||
|
||||
enum StackIdx {
|
||||
FromTop(usize),
|
||||
FromBottom(usize),
|
||||
@@ -23,18 +32,7 @@ fn print_dirs() -> ShResult<()> {
|
||||
.into_iter()
|
||||
.chain(dirs_iter)
|
||||
.map(|d| d.to_string_lossy().to_string())
|
||||
.map(|d| {
|
||||
let Ok(home) = env::var("HOME") else {
|
||||
return d;
|
||||
};
|
||||
|
||||
if d.starts_with(&home) {
|
||||
let new = d.strip_prefix(&home).unwrap();
|
||||
format!("~{new}")
|
||||
} else {
|
||||
d
|
||||
}
|
||||
})
|
||||
.map(truncate_home_path)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
@@ -378,18 +376,7 @@ pub fn dirs(node: Node) -> ShResult<()> {
|
||||
.map(|d| d.to_string_lossy().to_string());
|
||||
|
||||
if abbreviate_home {
|
||||
let Ok(home) = env::var("HOME") else {
|
||||
return stack.collect();
|
||||
};
|
||||
stack
|
||||
.map(|d| {
|
||||
if d.starts_with(&home) {
|
||||
let new = d.strip_prefix(&home).unwrap();
|
||||
format!("~{new}")
|
||||
} else {
|
||||
d
|
||||
}
|
||||
})
|
||||
stack.map(truncate_home_path)
|
||||
.collect()
|
||||
} else {
|
||||
stack.collect()
|
||||
@@ -438,3 +425,192 @@ pub fn dirs(node: Node) -> ShResult<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use std::{env, path::PathBuf};
|
||||
use crate::{parse::execute::exec_input, state::{self, read_meta}, testutil::TestGuard};
|
||||
use pretty_assertions::{assert_ne,assert_eq};
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_pushd_interactive() {
|
||||
let g = TestGuard::new();
|
||||
let current_dir = env::current_dir().unwrap();
|
||||
|
||||
exec_input("pushd /tmp".into(), None, true, None).unwrap();
|
||||
|
||||
let new_dir = env::current_dir().unwrap();
|
||||
|
||||
assert_ne!(new_dir, current_dir);
|
||||
assert_eq!(new_dir, PathBuf::from("/tmp"));
|
||||
|
||||
let dir_stack = read_meta(|m| m.dirs().clone());
|
||||
assert_eq!(dir_stack.len(), 1);
|
||||
assert_eq!(dir_stack[0], current_dir);
|
||||
|
||||
let out = g.read_output();
|
||||
let path = super::truncate_home_path(current_dir.to_string_lossy().to_string());
|
||||
assert_eq!(out, format!("/tmp {path}\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_popd_interactive() {
|
||||
let g = TestGuard::new();
|
||||
let current_dir = env::current_dir().unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
let tempdir_raw = tempdir.path().to_path_buf().to_string_lossy().to_string();
|
||||
|
||||
exec_input(format!("pushd {tempdir_raw}"), None, true, None).unwrap();
|
||||
|
||||
let dir_stack = read_meta(|m| m.dirs().clone());
|
||||
assert_eq!(dir_stack.len(), 1);
|
||||
assert_eq!(dir_stack[0], current_dir);
|
||||
|
||||
assert_eq!(env::current_dir().unwrap(), tempdir.path());
|
||||
g.read_output(); // consume output of pushd
|
||||
|
||||
exec_input("popd".into(), None, true, None).unwrap();
|
||||
|
||||
assert_eq!(env::current_dir().unwrap(), current_dir);
|
||||
let out = g.read_output();
|
||||
let path = super::truncate_home_path(current_dir.to_string_lossy().to_string());
|
||||
assert_eq!(out, format!("{path}\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_popd_empty_stack() {
|
||||
let _g = TestGuard::new();
|
||||
|
||||
exec_input("popd".into(), None, false, None).unwrap_err();
|
||||
assert_ne!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pushd_multiple_then_popd() {
|
||||
let g = TestGuard::new();
|
||||
let original = env::current_dir().unwrap();
|
||||
let tmp1 = TempDir::new().unwrap();
|
||||
let tmp2 = TempDir::new().unwrap();
|
||||
let path1 = tmp1.path().to_path_buf();
|
||||
let path2 = tmp2.path().to_path_buf();
|
||||
|
||||
exec_input(format!("pushd {}", path1.display()), None, false, None).unwrap();
|
||||
exec_input(format!("pushd {}", path2.display()), None, false, None).unwrap();
|
||||
g.read_output();
|
||||
|
||||
assert_eq!(env::current_dir().unwrap(), path2);
|
||||
let stack = read_meta(|m| m.dirs().clone());
|
||||
assert_eq!(stack.len(), 2);
|
||||
assert_eq!(stack[0], path1);
|
||||
assert_eq!(stack[1], original);
|
||||
|
||||
exec_input("popd".into(), None, false, None).unwrap();
|
||||
assert_eq!(env::current_dir().unwrap(), path1);
|
||||
|
||||
exec_input("popd".into(), None, false, None).unwrap();
|
||||
assert_eq!(env::current_dir().unwrap(), original);
|
||||
|
||||
let stack = read_meta(|m| m.dirs().clone());
|
||||
assert_eq!(stack.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pushd_rotate_plus() {
|
||||
let g = TestGuard::new();
|
||||
let original = env::current_dir().unwrap();
|
||||
let tmp1 = TempDir::new().unwrap();
|
||||
let tmp2 = TempDir::new().unwrap();
|
||||
let path1 = tmp1.path().to_path_buf();
|
||||
let path2 = tmp2.path().to_path_buf();
|
||||
|
||||
// Build stack: cwd=original, then pushd path1, pushd path2
|
||||
// Stack after: cwd=path2, [path1, original]
|
||||
exec_input(format!("pushd {}", path1.display()), None, false, None).unwrap();
|
||||
exec_input(format!("pushd {}", path2.display()), None, false, None).unwrap();
|
||||
g.read_output();
|
||||
|
||||
// pushd +1 rotates: [path2, path1, original] -> rotate_left(1) -> [path1, original, path2]
|
||||
// pop front -> cwd=path1, stack=[original, path2]
|
||||
exec_input("pushd +1".into(), None, false, None).unwrap();
|
||||
assert_eq!(env::current_dir().unwrap(), path1);
|
||||
|
||||
let stack = read_meta(|m| m.dirs().clone());
|
||||
assert_eq!(stack.len(), 2);
|
||||
assert_eq!(stack[0], original);
|
||||
assert_eq!(stack[1], path2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pushd_no_cd_flag() {
|
||||
let _g = TestGuard::new();
|
||||
let original = env::current_dir().unwrap();
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let path = tmp.path().to_path_buf();
|
||||
|
||||
exec_input(format!("pushd -n {}", path.display()), None, false, None).unwrap();
|
||||
|
||||
// -n means don't cd, but the dir should still be on the stack
|
||||
assert_eq!(env::current_dir().unwrap(), original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dirs_clear() {
|
||||
let _g = TestGuard::new();
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
exec_input(format!("pushd {}", tmp.path().display()), None, false, None).unwrap();
|
||||
assert_eq!(read_meta(|m| m.dirs().len()), 1);
|
||||
|
||||
exec_input("dirs -c".into(), None, false, None).unwrap();
|
||||
assert_eq!(read_meta(|m| m.dirs().len()), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dirs_one_per_line() {
|
||||
let g = TestGuard::new();
|
||||
let original = env::current_dir().unwrap();
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let path = tmp.path().to_path_buf();
|
||||
|
||||
exec_input(format!("pushd {}", path.display()), None, false, None).unwrap();
|
||||
g.read_output();
|
||||
|
||||
exec_input("dirs -p".into(), None, false, None).unwrap();
|
||||
let out = g.read_output();
|
||||
let lines: Vec<&str> = out.split('\n').filter(|l| !l.is_empty()).collect();
|
||||
assert_eq!(lines.len(), 2);
|
||||
assert_eq!(lines[0], super::truncate_home_path(path.to_string_lossy().to_string()));
|
||||
assert_eq!(lines[1], super::truncate_home_path(original.to_string_lossy().to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_popd_indexed_from_top() {
|
||||
let _g = TestGuard::new();
|
||||
let original = env::current_dir().unwrap();
|
||||
let tmp1 = TempDir::new().unwrap();
|
||||
let tmp2 = TempDir::new().unwrap();
|
||||
let path1 = tmp1.path().to_path_buf();
|
||||
let path2 = tmp2.path().to_path_buf();
|
||||
|
||||
// Stack: cwd=path2, [path1, original]
|
||||
exec_input(format!("pushd {}", path1.display()), None, false, None).unwrap();
|
||||
exec_input(format!("pushd {}", path2.display()), None, false, None).unwrap();
|
||||
|
||||
// popd +1 removes index (1-1)=0 from stored dirs, i.e. path1
|
||||
exec_input("popd +1".into(), None, false, None).unwrap();
|
||||
assert_eq!(env::current_dir().unwrap(), path2); // no cd
|
||||
|
||||
let stack = read_meta(|m| m.dirs().clone());
|
||||
assert_eq!(stack.len(), 1);
|
||||
assert_eq!(stack[0], original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pushd_nonexistent_dir() {
|
||||
let _g = TestGuard::new();
|
||||
|
||||
let result = exec_input("pushd /nonexistent_dir_12345".into(), None, false, None);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
parse::{NdRule, Node, execute::prepare_argv},
|
||||
prelude::*,
|
||||
procio::borrow_fd,
|
||||
state,
|
||||
state::{self, read_shopts},
|
||||
};
|
||||
|
||||
pub const ECHO_OPTS: [OptSpec; 4] = [
|
||||
@@ -31,7 +31,7 @@ bitflags! {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct EchoFlags: u32 {
|
||||
const NO_NEWLINE = 0b000001;
|
||||
const USE_STDERR = 0b000010;
|
||||
const NO_ESCAPE = 0b000010;
|
||||
const USE_ESCAPE = 0b000100;
|
||||
const USE_PROMPT = 0b001000;
|
||||
}
|
||||
@@ -54,18 +54,17 @@ pub fn echo(node: Node) -> ShResult<()> {
|
||||
argv.remove(0);
|
||||
}
|
||||
|
||||
let output_channel = if flags.contains(EchoFlags::USE_STDERR) {
|
||||
borrow_fd(STDERR_FILENO)
|
||||
} else {
|
||||
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 use_escape = (xpg_echo && !flags.contains(EchoFlags::NO_ESCAPE)) || flags.contains(EchoFlags::USE_ESCAPE);
|
||||
|
||||
let mut echo_output = prepare_echo_args(
|
||||
argv
|
||||
.into_iter()
|
||||
.map(|a| a.0) // Extract the String from the tuple of (String,Span)
|
||||
.collect::<Vec<_>>(),
|
||||
flags.contains(EchoFlags::USE_ESCAPE),
|
||||
use_escape,
|
||||
flags.contains(EchoFlags::USE_PROMPT),
|
||||
)?
|
||||
.join(" ");
|
||||
@@ -206,9 +205,9 @@ pub fn get_echo_flags(opts: Vec<Opt>) -> ShResult<EchoFlags> {
|
||||
for opt in opts {
|
||||
match opt {
|
||||
Opt::Short('n') => flags |= EchoFlags::NO_NEWLINE,
|
||||
Opt::Short('r') => flags |= EchoFlags::USE_STDERR,
|
||||
Opt::Short('e') => flags |= EchoFlags::USE_ESCAPE,
|
||||
Opt::Short('p') => flags |= EchoFlags::USE_PROMPT,
|
||||
Opt::Short('E') => flags |= EchoFlags::NO_ESCAPE,
|
||||
_ => {
|
||||
return Err(ShErr::simple(
|
||||
ShErrKind::ExecFail,
|
||||
@@ -220,3 +219,254 @@ pub fn get_echo_flags(opts: Vec<Opt>) -> ShResult<EchoFlags> {
|
||||
|
||||
Ok(flags)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::prepare_echo_args;
|
||||
use crate::state::{self, write_shopts};
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
|
||||
// ===================== Pure: prepare_echo_args =====================
|
||||
|
||||
#[test]
|
||||
fn prepare_no_escape() {
|
||||
let result = prepare_echo_args(vec!["hello\\nworld".into()], false, false).unwrap();
|
||||
assert_eq!(result, vec!["hello\\nworld"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_escape_newline() {
|
||||
let result = prepare_echo_args(vec!["hello\\nworld".into()], true, false).unwrap();
|
||||
assert_eq!(result, vec!["hello\nworld"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_escape_tab() {
|
||||
let result = prepare_echo_args(vec!["a\\tb".into()], true, false).unwrap();
|
||||
assert_eq!(result, vec!["a\tb"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_escape_carriage_return() {
|
||||
let result = prepare_echo_args(vec!["a\\rb".into()], true, false).unwrap();
|
||||
assert_eq!(result, vec!["a\rb"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_escape_bell() {
|
||||
let result = prepare_echo_args(vec!["a\\ab".into()], true, false).unwrap();
|
||||
assert_eq!(result, vec!["a\x07b"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_escape_backspace() {
|
||||
let result = prepare_echo_args(vec!["a\\bb".into()], true, false).unwrap();
|
||||
assert_eq!(result, vec!["a\x08b"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_escape_escape_char() {
|
||||
let result = prepare_echo_args(vec!["a\\eb".into()], true, false).unwrap();
|
||||
assert_eq!(result, vec!["a\x1bb"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_escape_upper_e() {
|
||||
let result = prepare_echo_args(vec!["a\\Eb".into()], true, false).unwrap();
|
||||
assert_eq!(result, vec!["a\x1bb"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_escape_backslash() {
|
||||
let result = prepare_echo_args(vec!["a\\\\b".into()], true, false).unwrap();
|
||||
assert_eq!(result, vec!["a\\b"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_escape_hex() {
|
||||
let result = prepare_echo_args(vec!["\\x41".into()], true, false).unwrap();
|
||||
assert_eq!(result, vec!["A"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_escape_hex_lowercase() {
|
||||
let result = prepare_echo_args(vec!["\\x61".into()], true, false).unwrap();
|
||||
assert_eq!(result, vec!["a"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_escape_octal() {
|
||||
let result = prepare_echo_args(vec!["\\0101".into()], true, false).unwrap();
|
||||
assert_eq!(result, vec!["A"]); // octal 101 = 65 = 'A'
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_escape_multiple() {
|
||||
let result = prepare_echo_args(vec!["a\\nb\\tc".into()], true, false).unwrap();
|
||||
assert_eq!(result, vec!["a\nb\tc"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_multiple_args() {
|
||||
let result = prepare_echo_args(
|
||||
vec!["hello".into(), "world".into()],
|
||||
false,
|
||||
false,
|
||||
).unwrap();
|
||||
assert_eq!(result, vec!["hello", "world"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_trailing_backslash() {
|
||||
let result = prepare_echo_args(vec!["hello\\".into()], true, false).unwrap();
|
||||
assert_eq!(result, vec!["hello\\"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_unknown_escape_literal() {
|
||||
// Unknown escape like \z should keep the backslash
|
||||
let result = prepare_echo_args(vec!["\\z".into()], true, false).unwrap();
|
||||
assert_eq!(result, vec!["\\z"]);
|
||||
}
|
||||
|
||||
// ===================== Integration: basic echo =====================
|
||||
|
||||
#[test]
|
||||
fn echo_simple() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("echo hello").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "hello\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_multiple_args() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("echo hello world").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "hello world\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_no_args() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("echo").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_status_zero() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("echo hello").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
// ===================== Integration: -n flag =====================
|
||||
|
||||
#[test]
|
||||
fn echo_no_newline() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("echo -n hello").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_no_newline_no_args() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("echo -n").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "");
|
||||
}
|
||||
|
||||
// ===================== Integration: -e flag =====================
|
||||
|
||||
#[test]
|
||||
fn echo_escape_newline() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("echo -e 'hello\\nworld'").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "hello\nworld\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_escape_tab() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("echo -e 'a\\tb'").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "a\tb\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_no_escape_by_default() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("echo 'hello\\nworld'").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "hello\\nworld\n");
|
||||
}
|
||||
|
||||
// ===================== Integration: -E flag + xpg_echo =====================
|
||||
|
||||
#[test]
|
||||
fn echo_xpg_echo_expands_by_default() {
|
||||
let guard = TestGuard::new();
|
||||
write_shopts(|o| o.core.xpg_echo = true);
|
||||
|
||||
test_input("echo 'hello\\nworld'").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "hello\nworld\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_xpg_echo_suppressed_by_big_e() {
|
||||
let guard = TestGuard::new();
|
||||
write_shopts(|o| o.core.xpg_echo = true);
|
||||
|
||||
test_input("echo -E 'hello\\nworld'").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "hello\\nworld\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_small_e_overrides_without_xpg() {
|
||||
let guard = TestGuard::new();
|
||||
write_shopts(|o| o.core.xpg_echo = false);
|
||||
|
||||
test_input("echo -e 'a\\tb'").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "a\tb\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_big_e_noop_without_xpg() {
|
||||
let guard = TestGuard::new();
|
||||
write_shopts(|o| o.core.xpg_echo = false);
|
||||
|
||||
// -E without xpg_echo is a no-op — escapes already off
|
||||
test_input("echo -E 'hello\\nworld'").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "hello\\nworld\n");
|
||||
}
|
||||
|
||||
// ===================== Integration: combined flags =====================
|
||||
|
||||
#[test]
|
||||
fn echo_n_and_e() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("echo -n -e 'a\\nb'").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "a\nb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_xpg_n_suppresses_newline() {
|
||||
let guard = TestGuard::new();
|
||||
write_shopts(|o| o.core.xpg_echo = true);
|
||||
|
||||
test_input("echo -n 'hello\\nworld'").unwrap();
|
||||
let out = guard.read_output();
|
||||
// xpg_echo expands \n, -n suppresses trailing newline
|
||||
assert_eq!(out, "hello\nworld");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,3 +34,90 @@ pub fn eval(node: Node) -> ShResult<()> {
|
||||
|
||||
exec_input(joined_argv, None, false, Some("eval".into()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::state::{self, read_vars, write_vars, VarFlags, VarKind};
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
|
||||
// ===================== Basic =====================
|
||||
|
||||
#[test]
|
||||
fn eval_simple_command() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("eval echo hello").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "hello\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_no_args_succeeds() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("eval").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_status_zero() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("eval true").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
// ===================== Joins args =====================
|
||||
|
||||
#[test]
|
||||
fn eval_joins_args() {
|
||||
let guard = TestGuard::new();
|
||||
// eval receives "echo" "hello" "world" as separate args, joins to "echo hello world"
|
||||
test_input("eval echo hello world").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "hello world\n");
|
||||
}
|
||||
|
||||
// ===================== Re-evaluation =====================
|
||||
|
||||
#[test]
|
||||
fn eval_expands_variable() {
|
||||
let guard = TestGuard::new();
|
||||
write_vars(|v| v.set_var("CMD", VarKind::Str("echo evaluated".into()), VarFlags::NONE)).unwrap();
|
||||
|
||||
test_input("eval $CMD").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "evaluated\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_sets_variable() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("eval x=42").unwrap();
|
||||
let val = read_vars(|v| v.get_var("x"));
|
||||
assert_eq!(val, "42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_pipeline() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("eval 'echo hello | cat'").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out, "hello\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_compound_command() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("eval 'echo first; echo second'").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("first"));
|
||||
assert!(out.contains("second"));
|
||||
}
|
||||
|
||||
// ===================== Status propagation =====================
|
||||
|
||||
#[test]
|
||||
fn eval_propagates_failure_status() {
|
||||
let _g = TestGuard::new();
|
||||
let _ = test_input("eval false");
|
||||
assert_ne!(state::get_status(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,3 +45,24 @@ pub fn exec_builtin(node: Node) -> ShResult<()> {
|
||||
_ => Err(ShErr::at(ShErrKind::Errno(e), span, format!("{e}"))),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::state;
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
// Testing exec is a bit tricky since it replaces the current process, so we just test that it correctly handles the case of no arguments and the case of a nonexistent command. We can't really test that it successfully executes a command since that would replace the test process itself.
|
||||
|
||||
#[test]
|
||||
fn exec_no_args_succeeds() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("exec").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_nonexistent_command_fails() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("exec _____________no_such_______command_xyz_____________hopefully______this_doesnt______exist_____somewhere_in___your______PATH__________________");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,3 +41,105 @@ pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {
|
||||
|
||||
Err(ShErr::simple(kind, message))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::libsh::error::ShErrKind;
|
||||
use crate::state;
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
|
||||
// ===================== break =====================
|
||||
|
||||
#[test]
|
||||
fn break_exits_loop() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("for i in 1 2 3; do echo $i; break; done").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out.trim(), "1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn break_outside_loop_errors() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("break");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn break_non_numeric_errors() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("for i in 1; do break abc; done");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ===================== continue =====================
|
||||
|
||||
#[test]
|
||||
fn continue_skips_iteration() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("for i in 1 2 3; do if [[ $i == 2 ]]; then continue; fi; echo $i; done").unwrap();
|
||||
let out = guard.read_output();
|
||||
let lines: Vec<&str> = out.lines().collect();
|
||||
assert_eq!(lines, vec!["1", "3"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn continue_outside_loop_errors() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("continue");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ===================== return =====================
|
||||
|
||||
#[test]
|
||||
fn return_exits_function() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("f() { echo before; return; echo after; }").unwrap();
|
||||
test_input("f").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out.trim(), "before");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn return_with_status() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("f() { return 42; }").unwrap();
|
||||
test_input("f").unwrap();
|
||||
assert_eq!(state::get_status(), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn return_outside_function_errors() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("return");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ===================== exit =====================
|
||||
|
||||
#[test]
|
||||
fn exit_returns_clean_exit() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("exit 0");
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
assert!(matches!(err.kind(), ShErrKind::CleanExit(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_with_code() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("exit 5");
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
assert!(matches!(err.kind(), ShErrKind::CleanExit(5)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_non_numeric_errors() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("exit abc");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,3 +251,217 @@ pub fn getopts(node: Node) -> ShResult<()> {
|
||||
getopts_inner(&opts_spec, &opt_var.0, &pos_params, span)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::state::{self, read_vars};
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
|
||||
fn get_var(name: &str) -> String {
|
||||
read_vars(|v| v.get_var(name))
|
||||
}
|
||||
|
||||
// ===================== Spec parsing =====================
|
||||
|
||||
#[test]
|
||||
fn parse_simple_spec() {
|
||||
use super::GetOptsSpec;
|
||||
use std::str::FromStr;
|
||||
let spec = GetOptsSpec::from_str("abc").unwrap();
|
||||
assert!(!spec.silent_err);
|
||||
assert_eq!(spec.opt_specs.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_spec_with_args() {
|
||||
use super::GetOptsSpec;
|
||||
use std::str::FromStr;
|
||||
let spec = GetOptsSpec::from_str("a:bc:").unwrap();
|
||||
assert!(!spec.silent_err);
|
||||
assert!(spec.opt_specs[0].takes_arg); // a:
|
||||
assert!(!spec.opt_specs[1].takes_arg); // b
|
||||
assert!(spec.opt_specs[2].takes_arg); // c:
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_silent_spec() {
|
||||
use super::GetOptsSpec;
|
||||
use std::str::FromStr;
|
||||
let spec = GetOptsSpec::from_str(":ab").unwrap();
|
||||
assert!(spec.silent_err);
|
||||
assert_eq!(spec.opt_specs.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_invalid_char() {
|
||||
use super::GetOptsSpec;
|
||||
use std::str::FromStr;
|
||||
let result = GetOptsSpec::from_str("a@b");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ===================== Basic option matching =====================
|
||||
|
||||
#[test]
|
||||
fn getopts_simple_flag() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("getopts ab opt -a").unwrap();
|
||||
assert_eq!(get_var("opt"), "a");
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn getopts_second_flag() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("getopts ab opt -b").unwrap();
|
||||
assert_eq!(get_var("opt"), "b");
|
||||
}
|
||||
|
||||
// ===================== Option with argument =====================
|
||||
|
||||
#[test]
|
||||
fn getopts_option_with_separate_arg() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("getopts a: opt -a value").unwrap();
|
||||
assert_eq!(get_var("opt"), "a");
|
||||
assert_eq!(get_var("OPTARG"), "value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn getopts_option_with_attached_arg() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("getopts a: opt -avalue").unwrap();
|
||||
assert_eq!(get_var("opt"), "a");
|
||||
assert_eq!(get_var("OPTARG"), "value");
|
||||
}
|
||||
|
||||
// ===================== Bundled options =====================
|
||||
|
||||
#[test]
|
||||
fn getopts_bundled_flags() {
|
||||
let _g = TestGuard::new();
|
||||
|
||||
// First call gets 'a' from -ab
|
||||
test_input("getopts abc opt -ab").unwrap();
|
||||
assert_eq!(get_var("opt"), "a");
|
||||
|
||||
// Second call gets 'b' from same -ab
|
||||
test_input("getopts abc opt -ab").unwrap();
|
||||
assert_eq!(get_var("opt"), "b");
|
||||
}
|
||||
|
||||
// ===================== OPTIND advancement =====================
|
||||
|
||||
#[test]
|
||||
fn getopts_advances_optind() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("getopts ab opt -a").unwrap();
|
||||
|
||||
let optind: usize = get_var("OPTIND").parse().unwrap();
|
||||
assert_eq!(optind, 2); // Advanced past -a
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn getopts_arg_option_advances_by_two() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("getopts a: opt -a val").unwrap();
|
||||
|
||||
let optind: usize = get_var("OPTIND").parse().unwrap();
|
||||
assert_eq!(optind, 3); // Advanced past both -a and val
|
||||
}
|
||||
|
||||
// ===================== Multiple calls (loop simulation) =====================
|
||||
|
||||
#[test]
|
||||
fn getopts_multiple_separate_args() {
|
||||
let _g = TestGuard::new();
|
||||
|
||||
test_input("getopts ab opt -a -b").unwrap();
|
||||
assert_eq!(get_var("opt"), "a");
|
||||
assert_eq!(state::get_status(), 0);
|
||||
|
||||
test_input("getopts ab opt -a -b").unwrap();
|
||||
assert_eq!(get_var("opt"), "b");
|
||||
assert_eq!(state::get_status(), 0);
|
||||
|
||||
// Third call: no more options
|
||||
test_input("getopts ab opt -a -b").unwrap();
|
||||
assert_eq!(state::get_status(), 1);
|
||||
}
|
||||
|
||||
// ===================== End of options =====================
|
||||
|
||||
#[test]
|
||||
fn getopts_no_options_returns_1() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("getopts ab opt foo").unwrap();
|
||||
assert_eq!(state::get_status(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn getopts_double_dash_stops() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("getopts ab opt -- -a").unwrap();
|
||||
assert_eq!(state::get_status(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn getopts_bare_dash_stops() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("getopts ab opt -").unwrap();
|
||||
assert_eq!(state::get_status(), 1);
|
||||
}
|
||||
|
||||
// ===================== Unknown option =====================
|
||||
|
||||
#[test]
|
||||
fn getopts_unknown_option() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("getopts ab opt -z").unwrap();
|
||||
assert_eq!(get_var("opt"), "?");
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
// ===================== Silent error mode =====================
|
||||
|
||||
#[test]
|
||||
fn getopts_silent_unknown_sets_optarg() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("getopts :ab opt -z").unwrap();
|
||||
assert_eq!(get_var("opt"), "?");
|
||||
assert_eq!(get_var("OPTARG"), "z");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn getopts_silent_missing_arg() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("getopts :a: opt -a").unwrap();
|
||||
assert_eq!(get_var("opt"), ":");
|
||||
assert_eq!(get_var("OPTARG"), "a");
|
||||
}
|
||||
|
||||
// ===================== Missing required argument (non-silent) =====================
|
||||
|
||||
#[test]
|
||||
fn getopts_missing_arg_non_silent() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("getopts a: opt -a").unwrap();
|
||||
assert_eq!(get_var("opt"), "?");
|
||||
}
|
||||
|
||||
// ===================== Error cases =====================
|
||||
|
||||
#[test]
|
||||
fn getopts_missing_spec() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("getopts");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn getopts_missing_varname() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("getopts ab");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{env, os::unix::fs::PermissionsExt, path::Path};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use ariadne::{Fmt, Span};
|
||||
|
||||
@@ -6,6 +6,8 @@ use crate::{
|
||||
builtin::BUILTINS,
|
||||
libsh::error::{ShErr, ShErrKind, ShResult, next_color},
|
||||
parse::{NdRule, Node, execute::prepare_argv, lex::KEYWORDS},
|
||||
prelude::*,
|
||||
procio::borrow_fd,
|
||||
state::{self, ShAlias, ShFunc, read_logic},
|
||||
};
|
||||
|
||||
@@ -31,28 +33,33 @@ pub fn type_builtin(node: Node) -> ShResult<()> {
|
||||
*/
|
||||
|
||||
'outer: for (arg, span) in argv {
|
||||
let stdout = borrow_fd(STDOUT_FILENO);
|
||||
if let Some(func) = read_logic(|v| v.get_func(&arg)) {
|
||||
let ShFunc { body: _, source } = func;
|
||||
let (line, col) = source.line_and_col();
|
||||
let name = source.source().name();
|
||||
println!(
|
||||
"{arg} is a function defined at {name}:{}:{}",
|
||||
let msg = format!(
|
||||
"{arg} is a function defined at {name}:{}:{}\n",
|
||||
line + 1,
|
||||
col + 1
|
||||
);
|
||||
write(stdout, msg.as_bytes())?;
|
||||
} else if let Some(alias) = read_logic(|v| v.get_alias(&arg)) {
|
||||
let ShAlias { body, source } = alias;
|
||||
let (line, col) = source.line_and_col();
|
||||
let name = source.source().name();
|
||||
println!(
|
||||
"{arg} is an alias for '{body}' defined at {name}:{}:{}",
|
||||
let msg = format!(
|
||||
"{arg} is an alias for '{body}' defined at {name}:{}:{}\n",
|
||||
line + 1,
|
||||
col + 1
|
||||
);
|
||||
write(stdout, msg.as_bytes())?;
|
||||
} else if BUILTINS.contains(&arg.as_str()) {
|
||||
println!("{arg} is a shell builtin");
|
||||
let msg = format!("{arg} is a shell builtin\n");
|
||||
write(stdout, msg.as_bytes())?;
|
||||
} else if KEYWORDS.contains(&arg.as_str()) {
|
||||
println!("{arg} is a shell keyword");
|
||||
let msg = format!("{arg} is a shell keyword\n");
|
||||
write(stdout, msg.as_bytes())?;
|
||||
} else {
|
||||
let path = env::var("PATH").unwrap_or_default();
|
||||
let paths = path.split(':').map(Path::new).collect::<Vec<_>>();
|
||||
@@ -70,7 +77,8 @@ pub fn type_builtin(node: Node) -> ShResult<()> {
|
||||
&& let Some(name) = entry.file_name().to_str()
|
||||
&& name == arg
|
||||
{
|
||||
println!("{arg} is {}", entry.path().display());
|
||||
let msg = format!("{arg} is {}\n", entry.path().display());
|
||||
write(stdout, msg.as_bytes())?;
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
@@ -92,3 +100,136 @@ pub fn type_builtin(node: Node) -> ShResult<()> {
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::state::{self};
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
|
||||
// ===================== Builtins =====================
|
||||
|
||||
#[test]
|
||||
fn type_builtin_echo() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("type echo").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("echo"));
|
||||
assert!(out.contains("shell builtin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_builtin_cd() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("type cd").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("cd"));
|
||||
assert!(out.contains("shell builtin"));
|
||||
}
|
||||
|
||||
// ===================== Keywords =====================
|
||||
|
||||
#[test]
|
||||
fn type_keyword_if() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("type if").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("if"));
|
||||
assert!(out.contains("shell keyword"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_keyword_for() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("type for").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("for"));
|
||||
assert!(out.contains("shell keyword"));
|
||||
}
|
||||
|
||||
// ===================== Functions =====================
|
||||
|
||||
#[test]
|
||||
fn type_function() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("myfn() { echo hi; }").unwrap();
|
||||
guard.read_output();
|
||||
|
||||
test_input("type myfn").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("myfn"));
|
||||
assert!(out.contains("function"));
|
||||
}
|
||||
|
||||
// ===================== Aliases =====================
|
||||
|
||||
#[test]
|
||||
fn type_alias() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("alias ll='ls -la'").unwrap();
|
||||
guard.read_output();
|
||||
|
||||
test_input("type ll").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("ll"));
|
||||
assert!(out.contains("alias"));
|
||||
assert!(out.contains("ls -la"));
|
||||
}
|
||||
|
||||
// ===================== External commands =====================
|
||||
|
||||
#[test]
|
||||
fn type_external_command() {
|
||||
let guard = TestGuard::new();
|
||||
// /bin/cat or /usr/bin/cat should exist on any Unix system
|
||||
test_input("type cat").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("cat"));
|
||||
assert!(out.contains("is"));
|
||||
assert!(out.contains("/")); // Should show a path
|
||||
}
|
||||
|
||||
// ===================== Not found =====================
|
||||
|
||||
#[test]
|
||||
fn type_not_found() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("type __hopefully____not_______a____command__");
|
||||
assert!(result.is_err());
|
||||
assert_eq!(state::get_status(), 1);
|
||||
}
|
||||
|
||||
// ===================== Priority order =====================
|
||||
|
||||
#[test]
|
||||
fn type_function_shadows_builtin() {
|
||||
let guard = TestGuard::new();
|
||||
// Define a function named 'echo' — should shadow the builtin
|
||||
test_input("echo() { true; }").unwrap();
|
||||
guard.read_output();
|
||||
|
||||
test_input("type echo").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("function"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_alias_shadows_external() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("alias cat='echo meow'").unwrap();
|
||||
guard.read_output();
|
||||
|
||||
test_input("type cat").unwrap();
|
||||
let out = guard.read_output();
|
||||
// alias check comes before external PATH scan
|
||||
assert!(out.contains("alias"));
|
||||
}
|
||||
|
||||
// ===================== Status =====================
|
||||
|
||||
#[test]
|
||||
fn type_status_zero_on_found() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("type echo").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ impl KeyMapOpts {
|
||||
}
|
||||
Ok(Self { remove, flags })
|
||||
}
|
||||
pub fn keymap_opts() -> [OptSpec; 6] {
|
||||
pub fn keymap_opts() -> [OptSpec; 7] {
|
||||
[
|
||||
OptSpec {
|
||||
opt: Opt::Short('n'), // normal mode
|
||||
@@ -81,6 +81,10 @@ impl KeyMapOpts {
|
||||
opt: Opt::Short('o'), // operator-pending mode
|
||||
takes_arg: false,
|
||||
},
|
||||
OptSpec {
|
||||
opt: Opt::Long("remove".into()),
|
||||
takes_arg: true,
|
||||
},
|
||||
OptSpec {
|
||||
opt: Opt::Short('r'), // replace mode
|
||||
takes_arg: false,
|
||||
@@ -172,3 +176,169 @@ pub fn keymap(node: Node) -> ShResult<()> {
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::getopt::Opt;
|
||||
use crate::expand::expand_keymap;
|
||||
use crate::state::{self, read_logic};
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
|
||||
// ===================== KeyMapOpts parsing =====================
|
||||
|
||||
#[test]
|
||||
fn opts_normal_mode() {
|
||||
let opts = KeyMapOpts::from_opts(&[Opt::Short('n')]).unwrap();
|
||||
assert!(opts.flags.contains(KeyMapFlags::NORMAL));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opts_insert_mode() {
|
||||
let opts = KeyMapOpts::from_opts(&[Opt::Short('i')]).unwrap();
|
||||
assert!(opts.flags.contains(KeyMapFlags::INSERT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opts_multiple_modes() {
|
||||
let opts = KeyMapOpts::from_opts(&[Opt::Short('n'), Opt::Short('i')]).unwrap();
|
||||
assert!(opts.flags.contains(KeyMapFlags::NORMAL));
|
||||
assert!(opts.flags.contains(KeyMapFlags::INSERT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opts_no_mode_errors() {
|
||||
let result = KeyMapOpts::from_opts(&[]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opts_remove() {
|
||||
let opts = KeyMapOpts::from_opts(&[
|
||||
Opt::Short('n'),
|
||||
Opt::LongWithArg("remove".into(), "jk".into()),
|
||||
]).unwrap();
|
||||
assert_eq!(opts.remove, Some("jk".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opts_duplicate_remove_errors() {
|
||||
let result = KeyMapOpts::from_opts(&[
|
||||
Opt::Short('n'),
|
||||
Opt::LongWithArg("remove".into(), "jk".into()),
|
||||
Opt::LongWithArg("remove".into(), "kj".into()),
|
||||
]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ===================== KeyMap::compare =====================
|
||||
|
||||
#[test]
|
||||
fn compare_exact_match() {
|
||||
let km = KeyMap {
|
||||
flags: KeyMapFlags::NORMAL,
|
||||
keys: "jk".into(),
|
||||
action: "<ESC>".into(),
|
||||
};
|
||||
let keys = expand_keymap("jk");
|
||||
assert_eq!(km.compare(&keys), KeyMapMatch::IsExact);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compare_prefix_match() {
|
||||
let km = KeyMap {
|
||||
flags: KeyMapFlags::NORMAL,
|
||||
keys: "jk".into(),
|
||||
action: "<ESC>".into(),
|
||||
};
|
||||
let keys = expand_keymap("j");
|
||||
assert_eq!(km.compare(&keys), KeyMapMatch::IsPrefix);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compare_no_match() {
|
||||
let km = KeyMap {
|
||||
flags: KeyMapFlags::NORMAL,
|
||||
keys: "jk".into(),
|
||||
action: "<ESC>".into(),
|
||||
};
|
||||
let keys = expand_keymap("zz");
|
||||
assert_eq!(km.compare(&keys), KeyMapMatch::NoMatch);
|
||||
}
|
||||
|
||||
// ===================== Registration via test_input =====================
|
||||
|
||||
#[test]
|
||||
fn keymap_register() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("keymap -n jk '<ESC>'").unwrap();
|
||||
|
||||
let maps = read_logic(|l| l.keymaps_filtered(
|
||||
KeyMapFlags::NORMAL,
|
||||
&expand_keymap("jk"),
|
||||
));
|
||||
assert!(!maps.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keymap_register_insert() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("keymap -i jk '<ESC>'").unwrap();
|
||||
|
||||
let maps = read_logic(|l| l.keymaps_filtered(
|
||||
KeyMapFlags::INSERT,
|
||||
&expand_keymap("jk"),
|
||||
));
|
||||
assert!(!maps.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keymap_overwrite() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("keymap -n jk '<ESC>'").unwrap();
|
||||
test_input("keymap -n jk 'dd'").unwrap();
|
||||
|
||||
let maps = read_logic(|l| l.keymaps_filtered(
|
||||
KeyMapFlags::NORMAL,
|
||||
&expand_keymap("jk"),
|
||||
));
|
||||
assert_eq!(maps.len(), 1);
|
||||
assert_eq!(maps[0].action, "dd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keymap_remove() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("keymap -n jk '<ESC>'").unwrap();
|
||||
test_input("keymap -n --remove jk").unwrap();
|
||||
|
||||
let maps = read_logic(|l| l.keymaps_filtered(
|
||||
KeyMapFlags::NORMAL,
|
||||
&expand_keymap("jk"),
|
||||
));
|
||||
assert!(maps.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keymap_status_zero() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("keymap -n jk '<ESC>'").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
// ===================== Error cases =====================
|
||||
|
||||
#[test]
|
||||
fn keymap_missing_keys() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("keymap -n");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keymap_missing_action() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("keymap -n jk");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,6 +368,240 @@ pub fn map(node: Node) -> ShResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{MapNode, MapFlags, get_map_opts};
|
||||
use crate::getopt::Opt;
|
||||
use crate::state::{self, read_vars};
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
|
||||
// ===================== Pure: MapNode get/set/remove =====================
|
||||
|
||||
#[test]
|
||||
fn mapnode_set_and_get() {
|
||||
let mut root = MapNode::default();
|
||||
root.set(&["key".into()], MapNode::StaticLeaf("val".into()));
|
||||
let node = root.get(&["key".into()]).unwrap();
|
||||
assert!(matches!(node, MapNode::StaticLeaf(s) if s == "val"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapnode_nested_set_and_get() {
|
||||
let mut root = MapNode::default();
|
||||
root.set(
|
||||
&["a".into(), "b".into(), "c".into()],
|
||||
MapNode::StaticLeaf("deep".into()),
|
||||
);
|
||||
let node = root.get(&["a".into(), "b".into(), "c".into()]).unwrap();
|
||||
assert!(matches!(node, MapNode::StaticLeaf(s) if s == "deep"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapnode_get_missing() {
|
||||
let root = MapNode::default();
|
||||
assert!(root.get(&["nope".into()]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapnode_remove() {
|
||||
let mut root = MapNode::default();
|
||||
root.set(&["key".into()], MapNode::StaticLeaf("val".into()));
|
||||
let removed = root.remove(&["key".into()]);
|
||||
assert!(removed.is_some());
|
||||
assert!(root.get(&["key".into()]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapnode_remove_nested() {
|
||||
let mut root = MapNode::default();
|
||||
root.set(
|
||||
&["a".into(), "b".into()],
|
||||
MapNode::StaticLeaf("val".into()),
|
||||
);
|
||||
root.remove(&["a".into(), "b".into()]);
|
||||
assert!(root.get(&["a".into(), "b".into()]).is_none());
|
||||
// Parent branch should still exist
|
||||
assert!(root.get(&["a".into()]).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapnode_keys() {
|
||||
let mut root = MapNode::default();
|
||||
root.set(&["x".into()], MapNode::StaticLeaf("1".into()));
|
||||
root.set(&["y".into()], MapNode::StaticLeaf("2".into()));
|
||||
let mut keys = root.keys();
|
||||
keys.sort();
|
||||
assert_eq!(keys, vec!["x", "y"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapnode_display_leaf() {
|
||||
let leaf = MapNode::StaticLeaf("hello".into());
|
||||
assert_eq!(leaf.display(false, false).unwrap(), "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapnode_display_json() {
|
||||
let mut root = MapNode::default();
|
||||
root.set(&["k".into()], MapNode::StaticLeaf("v".into()));
|
||||
let json = root.display(true, false).unwrap();
|
||||
assert!(json.contains("\"k\""));
|
||||
assert!(json.contains("\"v\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapnode_overwrite() {
|
||||
let mut root = MapNode::default();
|
||||
root.set(&["key".into()], MapNode::StaticLeaf("old".into()));
|
||||
root.set(&["key".into()], MapNode::StaticLeaf("new".into()));
|
||||
let node = root.get(&["key".into()]).unwrap();
|
||||
assert!(matches!(node, MapNode::StaticLeaf(s) if s == "new"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapnode_promote_leaf_to_branch() {
|
||||
let mut root = MapNode::default();
|
||||
root.set(&["key".into()], MapNode::StaticLeaf("leaf".into()));
|
||||
// Setting a sub-path should promote the leaf to a branch
|
||||
root.set(
|
||||
&["key".into(), "sub".into()],
|
||||
MapNode::StaticLeaf("nested".into()),
|
||||
);
|
||||
let node = root.get(&["key".into(), "sub".into()]).unwrap();
|
||||
assert!(matches!(node, MapNode::StaticLeaf(s) if s == "nested"));
|
||||
}
|
||||
|
||||
// ===================== Pure: MapNode JSON round-trip =====================
|
||||
|
||||
#[test]
|
||||
fn mapnode_json_roundtrip() {
|
||||
let mut root = MapNode::default();
|
||||
root.set(&["name".into()], MapNode::StaticLeaf("test".into()));
|
||||
root.set(&["count".into()], MapNode::StaticLeaf("42".into()));
|
||||
|
||||
let val: serde_json::Value = root.clone().into();
|
||||
let back: MapNode = val.into();
|
||||
assert!(back.get(&["name".into()]).is_some());
|
||||
assert!(back.get(&["count".into()]).is_some());
|
||||
}
|
||||
|
||||
// ===================== Pure: option parsing =====================
|
||||
|
||||
#[test]
|
||||
fn parse_remove_flag() {
|
||||
let opts = get_map_opts(vec![Opt::Short('r')]);
|
||||
assert!(opts.flags.contains(MapFlags::REMOVE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_json_flag() {
|
||||
let opts = get_map_opts(vec![Opt::Short('j')]);
|
||||
assert!(opts.flags.contains(MapFlags::JSON));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_keys_flag() {
|
||||
let opts = get_map_opts(vec![Opt::Short('k')]);
|
||||
assert!(opts.flags.contains(MapFlags::KEYS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_pretty_flag() {
|
||||
let opts = get_map_opts(vec![Opt::Long("pretty".into())]);
|
||||
assert!(opts.flags.contains(MapFlags::PRETTY));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_func_flag() {
|
||||
let opts = get_map_opts(vec![Opt::Short('F')]);
|
||||
assert!(opts.flags.contains(MapFlags::FUNC));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_combined_flags() {
|
||||
let opts = get_map_opts(vec![Opt::Short('j'), Opt::Short('k')]);
|
||||
assert!(opts.flags.contains(MapFlags::JSON));
|
||||
assert!(opts.flags.contains(MapFlags::KEYS));
|
||||
}
|
||||
|
||||
// ===================== Integration =====================
|
||||
|
||||
#[test]
|
||||
fn map_set_and_read() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("map mymap.key=hello").unwrap();
|
||||
test_input("map mymap.key").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out.trim(), "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_nested_path() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("map mymap.a.b.c=deep").unwrap();
|
||||
test_input("map mymap.a.b.c").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out.trim(), "deep");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_remove() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("map mymap.key=val").unwrap();
|
||||
test_input("map -r mymap.key").unwrap();
|
||||
let has = read_vars(|v| {
|
||||
v.get_map("mymap")
|
||||
.and_then(|m| m.get(&["key".into()]).cloned())
|
||||
.is_some()
|
||||
});
|
||||
assert!(!has);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_remove_entire() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("map mymap.key=val").unwrap();
|
||||
test_input("map -r mymap").unwrap();
|
||||
let has = read_vars(|v| v.get_map("mymap").is_some());
|
||||
assert!(!has);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_keys() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("map mymap.x=1").unwrap();
|
||||
test_input("map mymap.y=2").unwrap();
|
||||
test_input("map -k mymap").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("x"));
|
||||
assert!(out.contains("y"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_json_output() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("map mymap.key=val").unwrap();
|
||||
test_input("map -j mymap").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("\"key\""));
|
||||
assert!(out.contains("\"val\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_nonexistent_errors() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("map __no_such_map__");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_status_zero() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("map mymap.key=val").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_map_opts(opts: Vec<Opt>) -> MapOpts {
|
||||
let mut map_opts = MapOpts {
|
||||
flags: MapFlags::empty(),
|
||||
|
||||
@@ -23,12 +23,11 @@ pub mod source;
|
||||
pub mod test; // [[ ]] thing
|
||||
pub mod trap;
|
||||
pub mod varcmds;
|
||||
pub mod zoltraak;
|
||||
pub mod resource;
|
||||
|
||||
pub const BUILTINS: [&str; 49] = [
|
||||
"echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown",
|
||||
"alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin",
|
||||
"echo", "cd", "read", "export", "local", "pwd", "source", ".", "shift", "jobs", "fg", "bg", "disown",
|
||||
"alias", "unalias", "return", "break", "continue", "exit", "shopt", "builtin",
|
||||
"command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly",
|
||||
"unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type",
|
||||
"getopts", "keymap", "read_key", "autocmd", "ulimit", "umask"
|
||||
@@ -48,3 +47,34 @@ pub fn noop_builtin() -> ShResult<()> {
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{state, testutil::{TestGuard, test_input}};
|
||||
|
||||
// You can never be too sure!!!!!!
|
||||
|
||||
#[test]
|
||||
fn test_true() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("true").unwrap();
|
||||
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_false() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("false").unwrap();
|
||||
|
||||
assert_eq!(state::get_status(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_noop() {
|
||||
let _g = TestGuard::new();
|
||||
test_input(":").unwrap();
|
||||
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,3 +24,41 @@ pub fn pwd(node: Node) -> ShResult<()> {
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env;
|
||||
use tempfile::TempDir;
|
||||
use crate::state;
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
|
||||
#[test]
|
||||
fn pwd_prints_cwd() {
|
||||
let guard = TestGuard::new();
|
||||
let cwd = env::current_dir().unwrap();
|
||||
|
||||
test_input("pwd").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out.trim(), cwd.display().to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pwd_after_cd() {
|
||||
let guard = TestGuard::new();
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
test_input(format!("cd {}", tmp.path().display())).unwrap();
|
||||
guard.read_output();
|
||||
|
||||
test_input("pwd").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out.trim(), tmp.path().display().to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pwd_status_zero() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("pwd").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +212,8 @@ pub fn read_builtin(node: Node) -> ShResult<()> {
|
||||
for (i, arg) in argv.iter().enumerate() {
|
||||
if i == argv.len() - 1 {
|
||||
// Last arg, stuff the rest of the input into it
|
||||
write_vars(|v| v.set_var(&arg.0, VarKind::Str(remaining.clone()), VarFlags::NONE))?;
|
||||
let trimmed = remaining.trim_start_matches(|c: char| field_sep.contains(c));
|
||||
write_vars(|v| v.set_var(&arg.0, VarKind::Str(trimmed.to_string()), VarFlags::NONE))?;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -340,6 +341,134 @@ pub fn read_key(node: Node) -> ShResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::state::{self, read_vars, write_vars, VarFlags, VarKind};
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
|
||||
// ===================== Basic read into REPLY =====================
|
||||
|
||||
#[test]
|
||||
fn read_pipe_into_reply() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("read < <(echo hello)").unwrap();
|
||||
let val = read_vars(|v| v.get_var("REPLY"));
|
||||
assert_eq!(val, "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_pipe_into_named_var() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("read myvar < <(echo world)").unwrap();
|
||||
let val = read_vars(|v| v.get_var("myvar"));
|
||||
assert_eq!(val, "world");
|
||||
}
|
||||
|
||||
// ===================== Field splitting =====================
|
||||
|
||||
#[test]
|
||||
fn read_two_vars() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("read a b < <(echo 'hello world')").unwrap();
|
||||
assert_eq!(read_vars(|v| v.get_var("a")), "hello");
|
||||
assert_eq!(read_vars(|v| v.get_var("b")), "world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_last_var_gets_remainder() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("read a b < <(echo 'one two three four')").unwrap();
|
||||
assert_eq!(read_vars(|v| v.get_var("a")), "one");
|
||||
assert_eq!(read_vars(|v| v.get_var("b")), "two three four");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_more_vars_than_fields() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("read a b c < <(echo 'only')").unwrap();
|
||||
assert_eq!(read_vars(|v| v.get_var("a")), "only");
|
||||
// b and c get empty strings since there are no more fields
|
||||
assert_eq!(read_vars(|v| v.get_var("b")), "");
|
||||
assert_eq!(read_vars(|v| v.get_var("c")), "");
|
||||
}
|
||||
|
||||
// ===================== Custom IFS =====================
|
||||
|
||||
#[test]
|
||||
fn read_custom_ifs() {
|
||||
let _g = TestGuard::new();
|
||||
write_vars(|v| v.set_var("IFS", VarKind::Str(":".into()), VarFlags::NONE)).unwrap();
|
||||
|
||||
test_input("read x y z < <(echo 'a:b:c')").unwrap();
|
||||
assert_eq!(read_vars(|v| v.get_var("x")), "a");
|
||||
assert_eq!(read_vars(|v| v.get_var("y")), "b");
|
||||
assert_eq!(read_vars(|v| v.get_var("z")), "c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_custom_ifs_remainder() {
|
||||
let _g = TestGuard::new();
|
||||
write_vars(|v| v.set_var("IFS", VarKind::Str(":".into()), VarFlags::NONE)).unwrap();
|
||||
|
||||
test_input("read x y < <(echo 'a:b:c:d')").unwrap();
|
||||
assert_eq!(read_vars(|v| v.get_var("x")), "a");
|
||||
assert_eq!(read_vars(|v| v.get_var("y")), "b:c:d");
|
||||
}
|
||||
|
||||
// ===================== Custom delimiter =====================
|
||||
|
||||
#[test]
|
||||
fn read_custom_delim() {
|
||||
let _g = TestGuard::new();
|
||||
// -d sets the delimiter; printf sends "hello,world" — read stops at ','
|
||||
test_input("read -d , myvar < <(echo -n 'hello,world')").unwrap();
|
||||
assert_eq!(read_vars(|v| v.get_var("myvar")), "hello");
|
||||
}
|
||||
|
||||
// ===================== Status =====================
|
||||
|
||||
#[test]
|
||||
fn read_status_zero() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("read < <(echo hello)").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_eof_status_one() {
|
||||
let _g = TestGuard::new();
|
||||
// Empty input / EOF should set status 1
|
||||
test_input("read < <(echo -n '')").unwrap();
|
||||
assert_eq!(state::get_status(), 1);
|
||||
}
|
||||
|
||||
// ===================== Flag parsing (pure) =====================
|
||||
|
||||
#[test]
|
||||
fn flags_raw_mode() {
|
||||
use super::get_read_flags;
|
||||
use crate::getopt::Opt;
|
||||
let flags = get_read_flags(vec![Opt::Short('r')]).unwrap();
|
||||
assert!(flags.flags.contains(super::ReadFlags::NO_ESCAPES));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flags_prompt() {
|
||||
use super::get_read_flags;
|
||||
use crate::getopt::Opt;
|
||||
let flags = get_read_flags(vec![Opt::ShortWithArg('p', "Enter: ".into())]).unwrap();
|
||||
assert_eq!(flags.prompt, Some("Enter: ".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flags_delimiter() {
|
||||
use super::get_read_flags;
|
||||
use crate::getopt::Opt;
|
||||
let flags = get_read_flags(vec![Opt::ShortWithArg('d', ",".into())]).unwrap();
|
||||
assert_eq!(flags.delim, b',');
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_read_key_opts(opts: Vec<Opt>) -> ShResult<ReadKeyOpts> {
|
||||
let mut read_key_opts = ReadKeyOpts {
|
||||
var_name: None,
|
||||
|
||||
@@ -3,7 +3,7 @@ use nix::sys::resource::{Resource, getrlimit, setrlimit};
|
||||
use yansi::Color;
|
||||
|
||||
use crate::{
|
||||
getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color}, parse::{NdRule, Node, execute::prepare_argv}, prelude::*, state::{self}
|
||||
getopt::{Opt, OptSpec, get_opts_from_tokens, get_opts_from_tokens_strict}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color}, parse::{NdRule, Node, execute::prepare_argv}, prelude::*, state::{self}
|
||||
};
|
||||
|
||||
fn ulimit_opt_spec() -> [OptSpec;5] {
|
||||
@@ -100,7 +100,7 @@ pub fn ulimit(node: Node) -> ShResult<()> {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let (_, opts) = get_opts_from_tokens(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())?;
|
||||
|
||||
if let Some(fds) = ulimit_opts.fds {
|
||||
@@ -167,3 +167,99 @@ pub fn ulimit(node: Node) -> ShResult<()> {
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::get_ulimit_opts;
|
||||
use crate::getopt::Opt;
|
||||
use crate::state;
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
use nix::sys::resource::{Resource, getrlimit};
|
||||
|
||||
// ===================== Pure: option parsing =====================
|
||||
|
||||
#[test]
|
||||
fn parse_fds() {
|
||||
let opts = get_ulimit_opts(&[Opt::ShortWithArg('n', "1024".into())]).unwrap();
|
||||
assert_eq!(opts.fds, Some(1024));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_procs() {
|
||||
let opts = get_ulimit_opts(&[Opt::ShortWithArg('u', "512".into())]).unwrap();
|
||||
assert_eq!(opts.procs, Some(512));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_stack() {
|
||||
let opts = get_ulimit_opts(&[Opt::ShortWithArg('s', "8192".into())]).unwrap();
|
||||
assert_eq!(opts.stack, Some(8192));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_core() {
|
||||
let opts = get_ulimit_opts(&[Opt::ShortWithArg('c', "0".into())]).unwrap();
|
||||
assert_eq!(opts.core, Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_vmem() {
|
||||
let opts = get_ulimit_opts(&[Opt::ShortWithArg('v', "100000".into())]).unwrap();
|
||||
assert_eq!(opts.vmem, Some(100000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_multiple() {
|
||||
let opts = get_ulimit_opts(&[
|
||||
Opt::ShortWithArg('n', "256".into()),
|
||||
Opt::ShortWithArg('c', "0".into()),
|
||||
]).unwrap();
|
||||
assert_eq!(opts.fds, Some(256));
|
||||
assert_eq!(opts.core, Some(0));
|
||||
assert!(opts.procs.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_non_numeric_fails() {
|
||||
let result = get_ulimit_opts(&[Opt::ShortWithArg('n', "abc".into())]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_invalid_option() {
|
||||
let result = get_ulimit_opts(&[Opt::Short('z')]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ===================== Integration =====================
|
||||
|
||||
#[test]
|
||||
fn ulimit_set_core_zero() {
|
||||
let _g = TestGuard::new();
|
||||
// Setting core dump size to 0 is always safe
|
||||
test_input("ulimit -c 0").unwrap();
|
||||
let (soft, _) = getrlimit(Resource::RLIMIT_CORE).unwrap();
|
||||
assert_eq!(soft, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ulimit_invalid_flag() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("ulimit -z 100");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ulimit_non_numeric_value() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("ulimit -n abc");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ulimit_status_zero() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("ulimit -c 0").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,3 +35,53 @@ pub fn shift(node: Node) -> ShResult<()> {
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::state;
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
|
||||
#[test]
|
||||
fn shift_in_function() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("f() { echo $1; shift 1; echo $1; }").unwrap();
|
||||
test_input("f a b").unwrap();
|
||||
let out = guard.read_output();
|
||||
let lines: Vec<&str> = out.lines().collect();
|
||||
assert_eq!(lines[0], "a");
|
||||
assert_eq!(lines[1], "b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_multiple() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("f() { shift 2; echo $1; }").unwrap();
|
||||
test_input("f a b c").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out.trim(), "c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_all_params() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("f() { shift 3; echo \"[$1]\"; }").unwrap();
|
||||
test_input("f a b c").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert_eq!(out.trim(), "[]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_non_numeric_fails() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("shift abc");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_status_zero() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("f() { shift 1; }").unwrap();
|
||||
test_input("f a b").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,3 +45,104 @@ pub fn shopt(node: Node) -> ShResult<()> {
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::state::{self, read_shopts};
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
|
||||
// ===================== Display =====================
|
||||
|
||||
#[test]
|
||||
fn shopt_no_args_displays_all() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("shopt").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("dotglob"));
|
||||
assert!(out.contains("autocd"));
|
||||
assert!(out.contains("max_hist"));
|
||||
assert!(out.contains("edit_mode"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shopt_query_category() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("shopt core").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("dotglob"));
|
||||
assert!(out.contains("autocd"));
|
||||
// Should not contain prompt opts
|
||||
assert!(!out.contains("edit_mode"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shopt_query_single_opt() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("shopt core.dotglob").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("false"));
|
||||
}
|
||||
|
||||
// ===================== Set =====================
|
||||
|
||||
#[test]
|
||||
fn shopt_set_bool() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("shopt core.dotglob=true").unwrap();
|
||||
assert!(read_shopts(|o| o.core.dotglob));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shopt_set_int() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("shopt core.max_hist=500").unwrap();
|
||||
assert_eq!(read_shopts(|o| o.core.max_hist), 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shopt_set_string() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("shopt prompt.leader=space").unwrap();
|
||||
assert_eq!(read_shopts(|o| o.prompt.leader.clone()), "space");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shopt_set_edit_mode() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("shopt prompt.edit_mode=emacs").unwrap();
|
||||
let mode = read_shopts(|o| format!("{}", o.prompt.edit_mode));
|
||||
assert_eq!(mode, "emacs");
|
||||
}
|
||||
|
||||
// ===================== Error cases =====================
|
||||
|
||||
#[test]
|
||||
fn shopt_invalid_category() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("shopt bogus.dotglob");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shopt_invalid_option() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("shopt core.nonexistent");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shopt_invalid_value() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("shopt core.dotglob=notabool");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ===================== Status =====================
|
||||
|
||||
#[test]
|
||||
fn shopt_status_zero() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("shopt core.autocd=true").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,3 +41,131 @@ pub fn source(node: Node) -> ShResult<()> {
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use std::io::Write;
|
||||
|
||||
use tempfile::{NamedTempFile, TempDir};
|
||||
use crate::state::{self, read_logic, read_vars};
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
|
||||
#[test]
|
||||
fn source_simple() {
|
||||
let _g = TestGuard::new();
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
let path = file.path().display().to_string();
|
||||
file.write_all(b"some_var=some_val").unwrap();
|
||||
|
||||
test_input(format!("source {path}")).unwrap();
|
||||
let var = read_vars(|v| v.get_var("some_var"));
|
||||
assert_eq!(var, "some_val".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_multiple_commands() {
|
||||
let _g = TestGuard::new();
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
let path = file.path().display().to_string();
|
||||
file.write_all(b"x=1\ny=2\nz=3").unwrap();
|
||||
|
||||
test_input(format!("source {path}")).unwrap();
|
||||
assert_eq!(read_vars(|v| v.get_var("x")), "1");
|
||||
assert_eq!(read_vars(|v| v.get_var("y")), "2");
|
||||
assert_eq!(read_vars(|v| v.get_var("z")), "3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_defines_function() {
|
||||
let _g = TestGuard::new();
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
let path = file.path().display().to_string();
|
||||
file.write_all(b"greet() { echo hi; }").unwrap();
|
||||
|
||||
test_input(format!("source {path}")).unwrap();
|
||||
let func = read_logic(|l| l.get_func("greet"));
|
||||
assert!(func.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_defines_alias() {
|
||||
let _g = TestGuard::new();
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
let path = file.path().display().to_string();
|
||||
file.write_all(b"alias ll='ls -la'").unwrap();
|
||||
|
||||
test_input(format!("source {path}")).unwrap();
|
||||
let alias = read_logic(|l| l.get_alias("ll"));
|
||||
assert!(alias.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_output_captured() {
|
||||
let guard = TestGuard::new();
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
let path = file.path().display().to_string();
|
||||
file.write_all(b"echo sourced").unwrap();
|
||||
|
||||
test_input(format!("source {path}")).unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("sourced"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_multiple_files() {
|
||||
let _g = TestGuard::new();
|
||||
let mut file1 = NamedTempFile::new().unwrap();
|
||||
let mut file2 = NamedTempFile::new().unwrap();
|
||||
let path1 = file1.path().display().to_string();
|
||||
let path2 = file2.path().display().to_string();
|
||||
file1.write_all(b"a=from_file1").unwrap();
|
||||
file2.write_all(b"b=from_file2").unwrap();
|
||||
|
||||
test_input(format!("source {path1} {path2}")).unwrap();
|
||||
assert_eq!(read_vars(|v| v.get_var("a")), "from_file1");
|
||||
assert_eq!(read_vars(|v| v.get_var("b")), "from_file2");
|
||||
}
|
||||
|
||||
// ===================== Dot syntax =====================
|
||||
|
||||
#[test]
|
||||
fn source_dot_syntax() {
|
||||
let _g = TestGuard::new();
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
let path = file.path().display().to_string();
|
||||
file.write_all(b"dot_var=dot_val").unwrap();
|
||||
|
||||
test_input(format!(". {path}")).unwrap();
|
||||
assert_eq!(read_vars(|v| v.get_var("dot_var")), "dot_val");
|
||||
}
|
||||
|
||||
// ===================== Error cases =====================
|
||||
|
||||
#[test]
|
||||
fn source_nonexistent_file() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("source /tmp/__no_such_file_xyz__");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_directory_fails() {
|
||||
let _g = TestGuard::new();
|
||||
let dir = TempDir::new().unwrap();
|
||||
let result = test_input(format!("source {}", dir.path().display()));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ===================== Status =====================
|
||||
|
||||
#[test]
|
||||
fn source_status_zero() {
|
||||
let _g = TestGuard::new();
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
let path = file.path().display().to_string();
|
||||
file.write_all(b"true").unwrap();
|
||||
|
||||
test_input(format!("source {path}")).unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ impl FromStr for TestOp {
|
||||
"-ge" => Ok(Self::IntGe),
|
||||
"-le" => Ok(Self::IntLe),
|
||||
_ if TEST_UNARY_OPS.contains(&s) => Ok(Self::Unary(s.parse::<UnaryOp>()?)),
|
||||
_ => Err(ShErr::simple(ShErrKind::SyntaxErr, "Invalid test operator")),
|
||||
_ => Err(ShErr::simple(ShErrKind::SyntaxErr, format!("Invalid test operator '{}'", s))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,6 +121,7 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
|
||||
};
|
||||
let mut last_result = false;
|
||||
let mut conjunct_op: Option<ConjunctOp>;
|
||||
log::trace!("test cases: {:#?}", cases);
|
||||
|
||||
for case in cases {
|
||||
let result = match case {
|
||||
@@ -290,21 +291,332 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
|
||||
}
|
||||
};
|
||||
|
||||
last_result = result;
|
||||
if let Some(op) = conjunct_op {
|
||||
match op {
|
||||
ConjunctOp::And if !last_result => {
|
||||
last_result = result;
|
||||
break;
|
||||
}
|
||||
ConjunctOp::Or if last_result => {
|
||||
last_result = result;
|
||||
break;
|
||||
}
|
||||
ConjunctOp::And if !last_result => break,
|
||||
ConjunctOp::Or if last_result => break,
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
last_result = result;
|
||||
}
|
||||
}
|
||||
Ok(last_result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs;
|
||||
use tempfile::{TempDir, NamedTempFile};
|
||||
use crate::state;
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
|
||||
// ===================== Unary: file tests =====================
|
||||
|
||||
#[test]
|
||||
fn test_exists_true() {
|
||||
let _g = TestGuard::new();
|
||||
let file = NamedTempFile::new().unwrap();
|
||||
test_input(format!("[[ -e {} ]]", file.path().display())).unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exists_false() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ -e /tmp/__no_such_file_test_rs__ ]]").unwrap();
|
||||
assert_ne!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_directory() {
|
||||
let _g = TestGuard::new();
|
||||
let dir = TempDir::new().unwrap();
|
||||
test_input(format!("[[ -d {} ]]", dir.path().display())).unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_directory_false() {
|
||||
let _g = TestGuard::new();
|
||||
let file = NamedTempFile::new().unwrap();
|
||||
test_input(format!("[[ -d {} ]]", file.path().display())).unwrap();
|
||||
assert_ne!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_file() {
|
||||
let _g = TestGuard::new();
|
||||
let file = NamedTempFile::new().unwrap();
|
||||
test_input(format!("[[ -f {} ]]", file.path().display())).unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_file_false() {
|
||||
let _g = TestGuard::new();
|
||||
let dir = TempDir::new().unwrap();
|
||||
test_input(format!("[[ -f {} ]]", dir.path().display())).unwrap();
|
||||
assert_ne!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readable() {
|
||||
let _g = TestGuard::new();
|
||||
let file = NamedTempFile::new().unwrap();
|
||||
test_input(format!("[[ -r {} ]]", file.path().display())).unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_writable() {
|
||||
let _g = TestGuard::new();
|
||||
let file = NamedTempFile::new().unwrap();
|
||||
test_input(format!("[[ -w {} ]]", file.path().display())).unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_empty_file() {
|
||||
let _g = TestGuard::new();
|
||||
let file = NamedTempFile::new().unwrap();
|
||||
fs::write(file.path(), "content").unwrap();
|
||||
test_input(format!("[[ -s {} ]]", file.path().display())).unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_file() {
|
||||
let _g = TestGuard::new();
|
||||
let file = NamedTempFile::new().unwrap();
|
||||
test_input(format!("[[ -s {} ]]", file.path().display())).unwrap();
|
||||
assert_ne!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
// ===================== Unary: string tests =====================
|
||||
|
||||
#[test]
|
||||
fn test_non_null_true() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ -n hello ]]").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_null_empty() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ -n '' ]]").unwrap();
|
||||
assert_ne!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_null_true() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ -z '' ]]").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_null_false() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ -z hello ]]").unwrap();
|
||||
assert_ne!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
// ===================== Binary: string comparison =====================
|
||||
|
||||
#[test]
|
||||
fn test_string_eq() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ hello == hello ]]").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_eq_false() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ hello == world ]]").unwrap();
|
||||
assert_ne!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_neq() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ hello != world ]]").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_neq_false() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ hello != hello ]]").unwrap();
|
||||
assert_ne!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_glob_match() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ hello == hel* ]]").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_glob_no_match() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ hello == wor* ]]").unwrap();
|
||||
assert_ne!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
// ===================== Binary: integer comparison =====================
|
||||
|
||||
#[test]
|
||||
fn test_int_eq() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ 42 -eq 42 ]]").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_int_eq_false() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ 42 -eq 43 ]]").unwrap();
|
||||
assert_ne!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_int_ne() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ 1 -ne 2 ]]").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_int_gt() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ 10 -gt 5 ]]").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_int_gt_false() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ 5 -gt 10 ]]").unwrap();
|
||||
assert_ne!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_int_lt() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ 5 -lt 10 ]]").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_int_ge() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ 10 -ge 10 ]]").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_int_le() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ 5 -le 5 ]]").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_int_negative() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ -5 -lt 0 ]]").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_int_non_integer_errors() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("[[ abc -eq 1 ]]");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ===================== Binary: regex match =====================
|
||||
|
||||
#[test]
|
||||
fn test_regex_match() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ hello123 =~ ^hello[0-9]+$ ]]").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_regex_no_match() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ goodbye =~ ^hello ]]").unwrap();
|
||||
assert_ne!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
// ===================== Conjuncts =====================
|
||||
|
||||
#[test]
|
||||
fn test_and_both_true() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ -n hello && -n world ]]").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_and_first_false() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ -z hello && -n world ]]").unwrap();
|
||||
assert_ne!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_or_first_true() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ -n hello || -z hello ]]").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_or_both_false() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("[[ -z hello || -z world ]]").unwrap();
|
||||
assert_ne!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
// ===================== Pure: operator parsing =====================
|
||||
|
||||
#[test]
|
||||
fn parse_unary_ops() {
|
||||
use super::UnaryOp;
|
||||
use std::str::FromStr;
|
||||
for op in ["-e", "-d", "-f", "-h", "-L", "-r", "-w", "-x", "-s",
|
||||
"-p", "-S", "-b", "-c", "-k", "-O", "-G", "-N", "-u",
|
||||
"-g", "-t", "-n", "-z"] {
|
||||
assert!(UnaryOp::from_str(op).is_ok(), "failed to parse {op}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_invalid_unary_op() {
|
||||
use super::UnaryOp;
|
||||
use std::str::FromStr;
|
||||
assert!(UnaryOp::from_str("-Q").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_binary_ops() {
|
||||
use super::TestOp;
|
||||
use std::str::FromStr;
|
||||
for op in ["==", "!=", "=~", "-eq", "-ne", "-gt", "-lt", "-ge", "-le"] {
|
||||
assert!(TestOp::from_str(op).is_ok(), "failed to parse {op}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_invalid_binary_op() {
|
||||
use super::TestOp;
|
||||
use std::str::FromStr;
|
||||
assert!(TestOp::from_str("~=").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,3 +167,146 @@ pub fn trap(node: Node) -> ShResult<()> {
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::TrapTarget;
|
||||
use std::str::FromStr;
|
||||
use nix::sys::signal::Signal;
|
||||
use crate::state::{self, read_logic};
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
|
||||
// ===================== Pure: TrapTarget parsing =====================
|
||||
|
||||
#[test]
|
||||
fn parse_exit() {
|
||||
assert_eq!(TrapTarget::from_str("EXIT").unwrap(), TrapTarget::Exit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_err() {
|
||||
assert_eq!(TrapTarget::from_str("ERR").unwrap(), TrapTarget::Error);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_signal_int() {
|
||||
assert_eq!(
|
||||
TrapTarget::from_str("INT").unwrap(),
|
||||
TrapTarget::Signal(Signal::SIGINT)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_signal_term() {
|
||||
assert_eq!(
|
||||
TrapTarget::from_str("TERM").unwrap(),
|
||||
TrapTarget::Signal(Signal::SIGTERM)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_signal_usr1() {
|
||||
assert_eq!(
|
||||
TrapTarget::from_str("USR1").unwrap(),
|
||||
TrapTarget::Signal(Signal::SIGUSR1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_invalid() {
|
||||
assert!(TrapTarget::from_str("BOGUS").is_err());
|
||||
}
|
||||
|
||||
// ===================== Pure: Display round-trip =====================
|
||||
|
||||
#[test]
|
||||
fn display_exit() {
|
||||
assert_eq!(TrapTarget::Exit.to_string(), "EXIT");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_err() {
|
||||
assert_eq!(TrapTarget::Error.to_string(), "ERR");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_signal_roundtrip() {
|
||||
for name in &["INT", "QUIT", "TERM", "USR1", "USR2", "ALRM", "CHLD", "WINCH"] {
|
||||
let target = TrapTarget::from_str(name).unwrap();
|
||||
assert_eq!(target.to_string(), *name);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== Integration: registration =====================
|
||||
|
||||
#[test]
|
||||
fn trap_registers_exit() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("trap 'echo bye' EXIT").unwrap();
|
||||
let cmd = read_logic(|l| l.get_trap(TrapTarget::Exit));
|
||||
assert_eq!(cmd.unwrap(), "echo bye");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trap_registers_signal() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("trap 'echo caught' INT").unwrap();
|
||||
let cmd = read_logic(|l| l.get_trap(TrapTarget::Signal(Signal::SIGINT)));
|
||||
assert_eq!(cmd.unwrap(), "echo caught");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trap_multiple_signals() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("trap 'handle' INT TERM").unwrap();
|
||||
let int = read_logic(|l| l.get_trap(TrapTarget::Signal(Signal::SIGINT)));
|
||||
let term = read_logic(|l| l.get_trap(TrapTarget::Signal(Signal::SIGTERM)));
|
||||
assert_eq!(int.unwrap(), "handle");
|
||||
assert_eq!(term.unwrap(), "handle");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trap_remove() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("trap 'echo hi' EXIT").unwrap();
|
||||
assert!(read_logic(|l| l.get_trap(TrapTarget::Exit)).is_some());
|
||||
test_input("trap - EXIT").unwrap();
|
||||
assert!(read_logic(|l| l.get_trap(TrapTarget::Exit)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trap_display() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("trap 'echo bye' EXIT").unwrap();
|
||||
test_input("trap").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("echo bye"));
|
||||
assert!(out.contains("EXIT"));
|
||||
}
|
||||
|
||||
// ===================== Error cases =====================
|
||||
|
||||
#[test]
|
||||
fn trap_single_arg_usage() {
|
||||
let _g = TestGuard::new();
|
||||
// Single arg prints usage and sets status 1
|
||||
test_input("trap 'echo hi'").unwrap();
|
||||
assert_eq!(state::get_status(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trap_invalid_signal() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("trap 'echo hi' BOGUS");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ===================== Status =====================
|
||||
|
||||
#[test]
|
||||
fn trap_status_zero() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("trap 'echo bye' EXIT").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,3 +196,219 @@ pub fn local(node: Node) -> ShResult<()> {
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::state::{self, VarFlags, read_vars};
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
|
||||
// ===================== readonly =====================
|
||||
|
||||
#[test]
|
||||
fn readonly_sets_flag() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("readonly myvar").unwrap();
|
||||
let flags = read_vars(|v| v.get_var_flags("myvar"));
|
||||
assert!(flags.unwrap().contains(VarFlags::READONLY));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_with_value() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("readonly myvar=hello").unwrap();
|
||||
assert_eq!(read_vars(|v| v.get_var("myvar")), "hello");
|
||||
let flags = read_vars(|v| v.get_var_flags("myvar"));
|
||||
assert!(flags.unwrap().contains(VarFlags::READONLY));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_prevents_reassignment() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("readonly myvar=hello").unwrap();
|
||||
let result = test_input("myvar=world");
|
||||
assert!(result.is_err());
|
||||
assert_eq!(read_vars(|v| v.get_var("myvar")), "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_display() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("readonly rdo_test_var=abc").unwrap();
|
||||
test_input("readonly").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("rdo_test_var=abc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_multiple() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("readonly a=1 b=2").unwrap();
|
||||
assert_eq!(read_vars(|v| v.get_var("a")), "1");
|
||||
assert_eq!(read_vars(|v| v.get_var("b")), "2");
|
||||
assert!(read_vars(|v| v.get_var_flags("a")).unwrap().contains(VarFlags::READONLY));
|
||||
assert!(read_vars(|v| v.get_var_flags("b")).unwrap().contains(VarFlags::READONLY));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_status_zero() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("readonly x=1").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
// ===================== unset =====================
|
||||
|
||||
#[test]
|
||||
fn unset_removes_variable() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("myvar=hello").unwrap();
|
||||
assert_eq!(read_vars(|v| v.get_var("myvar")), "hello");
|
||||
test_input("unset myvar").unwrap();
|
||||
assert_eq!(read_vars(|v| v.get_var("myvar")), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unset_multiple() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("a=1").unwrap();
|
||||
test_input("b=2").unwrap();
|
||||
test_input("unset a b").unwrap();
|
||||
assert_eq!(read_vars(|v| v.get_var("a")), "");
|
||||
assert_eq!(read_vars(|v| v.get_var("b")), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unset_nonexistent_fails() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("unset __no_such_var__");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unset_no_args_fails() {
|
||||
let _g = TestGuard::new();
|
||||
let result = test_input("unset");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unset_readonly_fails() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("readonly myvar=protected").unwrap();
|
||||
let result = test_input("unset myvar");
|
||||
assert!(result.is_err());
|
||||
assert_eq!(read_vars(|v| v.get_var("myvar")), "protected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unset_status_zero() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("x=1").unwrap();
|
||||
test_input("unset x").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
// ===================== export =====================
|
||||
|
||||
#[test]
|
||||
fn export_with_value() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("export SHED_TEST_VAR=hello_export").unwrap();
|
||||
assert_eq!(read_vars(|v| v.get_var("SHED_TEST_VAR")), "hello_export");
|
||||
assert_eq!(std::env::var("SHED_TEST_VAR").unwrap(), "hello_export");
|
||||
unsafe { std::env::remove_var("SHED_TEST_VAR") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_existing_variable() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("SHED_TEST_VAR2=existing").unwrap();
|
||||
test_input("export SHED_TEST_VAR2").unwrap();
|
||||
assert_eq!(std::env::var("SHED_TEST_VAR2").unwrap(), "existing");
|
||||
unsafe { std::env::remove_var("SHED_TEST_VAR2") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_sets_flag() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("export SHED_TEST_VAR3=flagged").unwrap();
|
||||
let flags = read_vars(|v| v.get_var_flags("SHED_TEST_VAR3"));
|
||||
assert!(flags.unwrap().contains(VarFlags::EXPORT));
|
||||
unsafe { std::env::remove_var("SHED_TEST_VAR3") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_display() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("export").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("PATH=") || out.contains("HOME="));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_multiple() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("export SHED_A=1 SHED_B=2").unwrap();
|
||||
assert_eq!(std::env::var("SHED_A").unwrap(), "1");
|
||||
assert_eq!(std::env::var("SHED_B").unwrap(), "2");
|
||||
unsafe { std::env::remove_var("SHED_A") };
|
||||
unsafe { std::env::remove_var("SHED_B") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_status_zero() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("export SHED_ST=1").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
unsafe { std::env::remove_var("SHED_ST") };
|
||||
}
|
||||
|
||||
// ===================== local =====================
|
||||
|
||||
#[test]
|
||||
fn local_sets_variable() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("local mylocal=hello").unwrap();
|
||||
assert_eq!(read_vars(|v| v.get_var("mylocal")), "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_sets_flag() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("local mylocal=val").unwrap();
|
||||
let flags = read_vars(|v| v.get_var_flags("mylocal"));
|
||||
assert!(flags.unwrap().contains(VarFlags::LOCAL));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_empty_value() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("local mylocal").unwrap();
|
||||
assert_eq!(read_vars(|v| v.get_var("mylocal")), "");
|
||||
assert!(read_vars(|v| v.get_var_flags("mylocal")).unwrap().contains(VarFlags::LOCAL));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_display() {
|
||||
let guard = TestGuard::new();
|
||||
test_input("lv_test=display_val").unwrap();
|
||||
test_input("local").unwrap();
|
||||
let out = guard.read_output();
|
||||
assert!(out.contains("lv_test=display_val"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_multiple() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("local x=10 y=20").unwrap();
|
||||
assert_eq!(read_vars(|v| v.get_var("x")), "10");
|
||||
assert_eq!(read_vars(|v| v.get_var("y")), "20");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_status_zero() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("local z=1").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
|
||||
use crate::{
|
||||
getopt::{Opt, OptSpec, get_opts_from_tokens},
|
||||
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
||||
parse::{NdRule, Node, execute::prepare_argv},
|
||||
prelude::*,
|
||||
procio::borrow_fd,
|
||||
};
|
||||
|
||||
bitflags! {
|
||||
#[derive(Clone,Copy,Debug,PartialEq,Eq)]
|
||||
struct ZoltFlags: u32 {
|
||||
const DRY = 0b000001;
|
||||
const CONFIRM = 0b000010;
|
||||
const NO_PRESERVE_ROOT = 0b000100;
|
||||
const RECURSIVE = 0b001000;
|
||||
const FORCE = 0b010000;
|
||||
const VERBOSE = 0b100000;
|
||||
}
|
||||
}
|
||||
|
||||
/// Annihilate a file
|
||||
///
|
||||
/// This command works similarly to 'rm', but behaves more destructively.
|
||||
/// The file given as an argument is completely destroyed. The command works by
|
||||
/// shredding all of the data contained in the file, before truncating the
|
||||
/// length of the file to 0 to ensure that not even any metadata remains.
|
||||
pub fn zoltraak(node: Node) -> ShResult<()> {
|
||||
let NdRule::Command {
|
||||
assignments: _,
|
||||
argv,
|
||||
} = node.class
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let zolt_opts = [
|
||||
OptSpec {
|
||||
opt: Opt::Long("dry-run".into()),
|
||||
takes_arg: false,
|
||||
},
|
||||
OptSpec {
|
||||
opt: Opt::Long("confirm".into()),
|
||||
takes_arg: false,
|
||||
},
|
||||
OptSpec {
|
||||
opt: Opt::Long("no-preserve-root".into()),
|
||||
takes_arg: false,
|
||||
},
|
||||
OptSpec {
|
||||
opt: Opt::Short('r'),
|
||||
takes_arg: false,
|
||||
},
|
||||
OptSpec {
|
||||
opt: Opt::Short('f'),
|
||||
takes_arg: false,
|
||||
},
|
||||
OptSpec {
|
||||
opt: Opt::Short('v'),
|
||||
takes_arg: false,
|
||||
},
|
||||
];
|
||||
let mut flags = ZoltFlags::empty();
|
||||
|
||||
let (argv, opts) = get_opts_from_tokens(argv, &zolt_opts)?;
|
||||
|
||||
for opt in opts {
|
||||
match opt {
|
||||
Opt::Long(flag) => match flag.as_str() {
|
||||
"no-preserve-root" => flags |= ZoltFlags::NO_PRESERVE_ROOT,
|
||||
"confirm" => flags |= ZoltFlags::CONFIRM,
|
||||
"dry-run" => flags |= ZoltFlags::DRY,
|
||||
_ => {
|
||||
return Err(ShErr::simple(
|
||||
ShErrKind::SyntaxErr,
|
||||
format!("zoltraak: unrecognized option '{flag}'"),
|
||||
));
|
||||
}
|
||||
},
|
||||
Opt::Short(flag) => match flag {
|
||||
'r' => flags |= ZoltFlags::RECURSIVE,
|
||||
'f' => flags |= ZoltFlags::FORCE,
|
||||
'v' => flags |= ZoltFlags::VERBOSE,
|
||||
_ => {
|
||||
return Err(ShErr::simple(
|
||||
ShErrKind::SyntaxErr,
|
||||
format!("zoltraak: unrecognized option '{flag}'"),
|
||||
));
|
||||
}
|
||||
},
|
||||
Opt::LongWithArg(flag, _) => {
|
||||
return Err(ShErr::simple(
|
||||
ShErrKind::SyntaxErr,
|
||||
format!("zoltraak: unrecognized option '{flag}'"),
|
||||
));
|
||||
}
|
||||
Opt::ShortWithArg(flag, _) => {
|
||||
return Err(ShErr::simple(
|
||||
ShErrKind::SyntaxErr,
|
||||
format!("zoltraak: unrecognized option '{flag}'"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut argv = prepare_argv(argv)?;
|
||||
if !argv.is_empty() {
|
||||
argv.remove(0);
|
||||
}
|
||||
|
||||
for (arg, span) in argv {
|
||||
if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) {
|
||||
return Err(
|
||||
ShErr::simple(
|
||||
ShErrKind::ExecFail,
|
||||
"zoltraak: Attempted to destroy root directory '/'",
|
||||
)
|
||||
.with_note("If you really want to do this, you can use the --no-preserve-root flag"),
|
||||
);
|
||||
}
|
||||
annihilate(&arg, flags).blame(span)?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn annihilate(path: &str, flags: ZoltFlags) -> ShResult<()> {
|
||||
let path_buf = PathBuf::from(path);
|
||||
let is_recursive = flags.contains(ZoltFlags::RECURSIVE);
|
||||
let is_verbose = flags.contains(ZoltFlags::VERBOSE);
|
||||
|
||||
const BLOCK_SIZE: u64 = 4096;
|
||||
|
||||
if !path_buf.exists() {
|
||||
return Err(ShErr::simple(
|
||||
ShErrKind::ExecFail,
|
||||
format!("zoltraak: File '{path}' not found"),
|
||||
));
|
||||
}
|
||||
|
||||
if path_buf.is_file() {
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.custom_flags(libc::O_DIRECT)
|
||||
.open(path_buf)?;
|
||||
|
||||
let meta = file.metadata()?;
|
||||
let file_size = meta.len();
|
||||
let full_blocks = file_size / BLOCK_SIZE;
|
||||
let byte_remainder = file_size % BLOCK_SIZE;
|
||||
|
||||
let full_buf = vec![0; BLOCK_SIZE as usize];
|
||||
let remainder_buf = vec![0; byte_remainder as usize];
|
||||
|
||||
for _ in 0..full_blocks {
|
||||
file.write_all(&full_buf)?;
|
||||
}
|
||||
|
||||
if byte_remainder > 0 {
|
||||
file.write_all(&remainder_buf)?;
|
||||
}
|
||||
|
||||
file.set_len(0)?;
|
||||
mem::drop(file);
|
||||
fs::remove_file(path)?;
|
||||
if is_verbose {
|
||||
let stderr = borrow_fd(STDERR_FILENO);
|
||||
write(stderr, format!("shredded file '{path}'\n").as_bytes())?;
|
||||
}
|
||||
} else if path_buf.is_dir() {
|
||||
if is_recursive {
|
||||
annihilate_recursive(path, flags)?; // scary
|
||||
} else {
|
||||
return Err(
|
||||
ShErr::simple(
|
||||
ShErrKind::ExecFail,
|
||||
format!("zoltraak: '{path}' is a directory"),
|
||||
)
|
||||
.with_note("Use the '-r' flag to recursively shred directories"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn annihilate_recursive(dir: &str, flags: ZoltFlags) -> ShResult<()> {
|
||||
let dir_path = PathBuf::from(dir);
|
||||
let is_verbose = flags.contains(ZoltFlags::VERBOSE);
|
||||
|
||||
for dir_entry in fs::read_dir(&dir_path)? {
|
||||
let entry = dir_entry?.path();
|
||||
let file = entry.to_str().unwrap();
|
||||
|
||||
if entry.is_file() {
|
||||
annihilate(file, flags)?;
|
||||
} else if entry.is_dir() {
|
||||
annihilate_recursive(file, flags)?;
|
||||
}
|
||||
}
|
||||
fs::remove_dir(dir)?;
|
||||
if is_verbose {
|
||||
let stderr = borrow_fd(STDERR_FILENO);
|
||||
write(stderr, format!("shredded directory '{dir}'\n").as_bytes())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
1203
src/expand.rs
1203
src/expand.rs
File diff suppressed because it is too large
Load Diff
224
src/getopt.rs
224
src/getopt.rs
@@ -1,8 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ariadne::Fmt;
|
||||
use fmt::Display;
|
||||
|
||||
use crate::{libsh::error::ShResult, parse::lex::Tk, prelude::*};
|
||||
use crate::{libsh::error::{ShErr, ShErrKind, ShResult, next_color}, parse::lex::Tk, prelude::*};
|
||||
|
||||
pub type OptSet = Arc<[Opt]>;
|
||||
|
||||
@@ -67,10 +68,21 @@ pub fn get_opts(words: Vec<String>) -> (Vec<String>, Vec<Opt>) {
|
||||
(non_opts, opts)
|
||||
}
|
||||
|
||||
pub fn get_opts_from_tokens_strict(
|
||||
tokens: Vec<Tk>,
|
||||
opt_specs: &[OptSpec],
|
||||
) -> ShResult<(Vec<Tk>, Vec<Opt>)> {
|
||||
sort_tks(tokens, opt_specs, true)
|
||||
}
|
||||
|
||||
pub fn get_opts_from_tokens(
|
||||
tokens: Vec<Tk>,
|
||||
opt_specs: &[OptSpec],
|
||||
) -> ShResult<(Vec<Tk>, Vec<Opt>)> {
|
||||
sort_tks(tokens, opt_specs, false)
|
||||
}
|
||||
|
||||
pub fn sort_tks(tokens: Vec<Tk>, opt_specs: &[OptSpec], strict: bool) -> ShResult<(Vec<Tk>, Vec<Opt>)> {
|
||||
let mut tokens_iter = tokens
|
||||
.into_iter()
|
||||
.map(|t| t.expand())
|
||||
@@ -113,10 +125,218 @@ pub fn get_opts_from_tokens(
|
||||
}
|
||||
}
|
||||
if !pushed {
|
||||
non_opts.push(token.clone());
|
||||
if strict {
|
||||
return Err(ShErr::simple(
|
||||
ShErrKind::ParseErr,
|
||||
format!("Unknown option: {}", opt.to_string().fg(next_color())),
|
||||
));
|
||||
} else {
|
||||
non_opts.push(token.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((non_opts, opts))
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::parse::lex::{LexFlags, LexStream};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_short_single() {
|
||||
let opts = Opt::parse("-a");
|
||||
assert_eq!(opts, vec![Opt::Short('a')]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_short_combined() {
|
||||
let opts = Opt::parse("-abc");
|
||||
assert_eq!(opts, vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_long() {
|
||||
let opts = Opt::parse("--verbose");
|
||||
assert_eq!(opts, vec![Opt::Long("verbose".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_non_option() {
|
||||
let opts = Opt::parse("hello");
|
||||
assert!(opts.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_opts_basic() {
|
||||
let words = vec!["file.txt".into(), "-v".into(), "--help".into(), "arg".into()];
|
||||
let (non_opts, opts) = get_opts(words);
|
||||
assert_eq!(non_opts, vec!["file.txt", "arg"]);
|
||||
assert_eq!(opts, vec![Opt::Short('v'), Opt::Long("help".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_opts_double_dash_stops_parsing() {
|
||||
let words = vec!["-a".into(), "--".into(), "-b".into(), "--foo".into()];
|
||||
let (non_opts, opts) = get_opts(words);
|
||||
assert_eq!(opts, vec![Opt::Short('a')]);
|
||||
assert_eq!(non_opts, vec!["-b", "--foo"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_opts_combined_short() {
|
||||
let words = vec!["-abc".into(), "file".into()];
|
||||
let (non_opts, opts) = get_opts(words);
|
||||
assert_eq!(opts, vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]);
|
||||
assert_eq!(non_opts, vec!["file"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_opts_no_flags() {
|
||||
let words = vec!["foo".into(), "bar".into()];
|
||||
let (non_opts, opts) = get_opts(words);
|
||||
assert!(opts.is_empty());
|
||||
assert_eq!(non_opts, vec!["foo", "bar"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_opts_empty_input() {
|
||||
let (non_opts, opts) = get_opts(vec![]);
|
||||
assert!(opts.is_empty());
|
||||
assert!(non_opts.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_formatting() {
|
||||
assert_eq!(Opt::Short('v').to_string(), "-v");
|
||||
assert_eq!(Opt::Long("help".into()).to_string(), "--help");
|
||||
assert_eq!(Opt::ShortWithArg('o', "file".into()).to_string(), "-o file");
|
||||
assert_eq!(Opt::LongWithArg("output".into(), "file".into()).to_string(), "--output file");
|
||||
}
|
||||
|
||||
fn lex(input: &str) -> Vec<Tk> {
|
||||
LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.collect::<ShResult<Vec<Tk>>>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_opts_from_tks() {
|
||||
let tokens = lex("file.txt --help -v arg");
|
||||
|
||||
let opt_spec = vec![
|
||||
OptSpec { opt: Opt::Short('v'), takes_arg: false },
|
||||
OptSpec { opt: Opt::Long("help".into()), takes_arg: false },
|
||||
];
|
||||
|
||||
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
||||
|
||||
let mut opts = opts.into_iter();
|
||||
assert!(opts.any(|o| o == Opt::Short('v') || o == Opt::Long("help".into())));
|
||||
assert!(opts.any(|o| o == Opt::Short('v') || o == Opt::Long("help".into())));
|
||||
|
||||
let mut non_opts = non_opts.into_iter().map(|s| s.to_string());
|
||||
assert!(non_opts.any(|s| s == "file.txt" || s == "arg"));
|
||||
assert!(non_opts.any(|s| s == "file.txt" || s == "arg"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tks_short_with_arg() {
|
||||
let tokens = lex("-o output.txt file.txt");
|
||||
|
||||
let opt_spec = vec![
|
||||
OptSpec { opt: Opt::Short('o'), takes_arg: true },
|
||||
];
|
||||
|
||||
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
||||
|
||||
assert_eq!(opts, vec![Opt::ShortWithArg('o', "output.txt".into())]);
|
||||
let non_opts: Vec<String> = non_opts.into_iter().map(|s| s.to_string()).collect();
|
||||
assert!(non_opts.contains(&"file.txt".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tks_long_with_arg() {
|
||||
let tokens = lex("--output result.txt input.txt");
|
||||
|
||||
let opt_spec = vec![
|
||||
OptSpec { opt: Opt::Long("output".into()), takes_arg: true },
|
||||
];
|
||||
|
||||
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
||||
|
||||
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();
|
||||
assert!(non_opts.contains(&"input.txt".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tks_double_dash_stops() {
|
||||
let tokens = lex("-v -- -a --foo");
|
||||
|
||||
let opt_spec = vec![
|
||||
OptSpec { opt: Opt::Short('v'), takes_arg: false },
|
||||
OptSpec { opt: Opt::Short('a'), takes_arg: false },
|
||||
];
|
||||
|
||||
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
||||
|
||||
assert_eq!(opts, vec![Opt::Short('v')]);
|
||||
let non_opts: Vec<String> = non_opts.into_iter().map(|s| s.to_string()).collect();
|
||||
assert!(non_opts.contains(&"-a".to_string()));
|
||||
assert!(non_opts.contains(&"--foo".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tks_combined_short_with_spec() {
|
||||
let tokens = lex("-abc");
|
||||
|
||||
let opt_spec = vec![
|
||||
OptSpec { opt: Opt::Short('a'), takes_arg: false },
|
||||
OptSpec { opt: Opt::Short('b'), takes_arg: false },
|
||||
OptSpec { opt: Opt::Short('c'), takes_arg: false },
|
||||
];
|
||||
|
||||
let (_non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
||||
|
||||
assert_eq!(opts, vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tks_unknown_opt_becomes_non_opt() {
|
||||
let tokens = lex("-v -x file");
|
||||
|
||||
let opt_spec = vec![
|
||||
OptSpec { opt: Opt::Short('v'), takes_arg: false },
|
||||
];
|
||||
|
||||
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
||||
|
||||
assert_eq!(opts, vec![Opt::Short('v')]);
|
||||
// -x is not in spec, so its token goes to non_opts
|
||||
assert!(non_opts.into_iter().map(|s| s.to_string()).any(|s| s == "-x" || s == "file"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tks_mixed_short_and_long_with_args() {
|
||||
let tokens = lex("-n 5 --output file.txt input");
|
||||
|
||||
let opt_spec = vec![
|
||||
OptSpec { opt: Opt::Short('n'), takes_arg: true },
|
||||
OptSpec { opt: Opt::Long("output".into()), takes_arg: true },
|
||||
];
|
||||
|
||||
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
||||
|
||||
assert_eq!(opts, vec![
|
||||
Opt::ShortWithArg('n', "5".into()),
|
||||
Opt::LongWithArg("output".into(), "file.txt".into()),
|
||||
]);
|
||||
let non_opts: Vec<String> = non_opts.into_iter().map(|s| s.to_string()).collect();
|
||||
assert!(non_opts.contains(&"input".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
12
src/jobs.rs
12
src/jobs.rs
@@ -1,4 +1,6 @@
|
||||
use ariadne::Fmt;
|
||||
use scopeguard::defer;
|
||||
use yansi::Color;
|
||||
|
||||
use crate::{
|
||||
libsh::{
|
||||
@@ -149,7 +151,7 @@ pub struct RegisteredFd {
|
||||
pub owner_pid: Pid,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct JobTab {
|
||||
fg: Option<Job>,
|
||||
order: Vec<usize>,
|
||||
@@ -724,12 +726,12 @@ impl Job {
|
||||
stat_line = format!("{}{} ", pid, stat_line);
|
||||
stat_line = format!("{} {}", stat_line, cmd);
|
||||
stat_line = match job_stat {
|
||||
WtStat::Stopped(..) | WtStat::Signaled(..) => stat_line.styled(Style::Magenta),
|
||||
WtStat::Stopped(..) | WtStat::Signaled(..) => stat_line.fg(Color::Magenta).to_string(),
|
||||
WtStat::Exited(_, code) => match code {
|
||||
0 => stat_line.styled(Style::Green),
|
||||
_ => stat_line.styled(Style::Red),
|
||||
0 => stat_line.fg(Color::Green).to_string(),
|
||||
_ => stat_line.fg(Color::Red).to_string(),
|
||||
},
|
||||
_ => stat_line.styled(Style::Cyan),
|
||||
_ => stat_line.fg(Color::Cyan).to_string(),
|
||||
};
|
||||
if i != 0 {
|
||||
let padding = " ".repeat(id_box.len() - 1);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use ariadne::Color;
|
||||
use ariadne::{Color, Fmt};
|
||||
use ariadne::{Report, ReportKind};
|
||||
use rand::TryRng;
|
||||
use yansi::Paint;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::procio::RedirGuard;
|
||||
use crate::{
|
||||
libsh::term::{Style, Styled},
|
||||
parse::lex::{Span, SpanSource},
|
||||
prelude::*,
|
||||
};
|
||||
@@ -144,12 +144,13 @@ impl Note {
|
||||
|
||||
impl Display for Note {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let note = "note".styled(Style::Green);
|
||||
let note = Fmt::fg("note", Color::Green);
|
||||
let main = &self.main;
|
||||
if self.depth == 0 {
|
||||
writeln!(f, "{note}: {main}")?;
|
||||
} else {
|
||||
let bar_break = "-".styled(Style::Cyan | Style::Bold);
|
||||
let bar_break = Fmt::fg("-", Color::Cyan);
|
||||
let bar_break = bar_break.bold();
|
||||
let indent = " ".repeat(self.depth);
|
||||
writeln!(f, " {indent}{bar_break} {main}")?;
|
||||
}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use super::term::{Style, Styled};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, PartialOrd, Ord, Eq, Debug)]
|
||||
#[repr(u8)]
|
||||
pub enum ShedLogLevel {
|
||||
NONE = 0,
|
||||
ERROR = 1,
|
||||
WARN = 2,
|
||||
INFO = 3,
|
||||
DEBUG = 4,
|
||||
TRACE = 5,
|
||||
}
|
||||
|
||||
impl Display for ShedLogLevel {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
use ShedLogLevel::*;
|
||||
match self {
|
||||
ERROR => write!(f, "{}", "ERROR".styled(Style::Red | Style::Bold)),
|
||||
WARN => write!(f, "{}", "WARN".styled(Style::Yellow | Style::Bold)),
|
||||
INFO => write!(f, "{}", "INFO".styled(Style::Green | Style::Bold)),
|
||||
DEBUG => write!(f, "{}", "DEBUG".styled(Style::Magenta | Style::Bold)),
|
||||
TRACE => write!(f, "{}", "TRACE".styled(Style::Blue | Style::Bold)),
|
||||
NONE => write!(f, ""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_level() -> ShedLogLevel {
|
||||
use ShedLogLevel::*;
|
||||
let level = std::env::var("SHED_LOG_LEVEL").unwrap_or_default();
|
||||
match level.to_lowercase().as_str() {
|
||||
"error" => ERROR,
|
||||
"warn" => WARN,
|
||||
"info" => INFO,
|
||||
"debug" => DEBUG,
|
||||
"trace" => TRACE,
|
||||
_ => NONE,
|
||||
}
|
||||
}
|
||||
|
||||
/// A structured logging macro designed for `shed`.
|
||||
///
|
||||
/// `flog!` was implemented because `rustyline` uses `env_logger`, which
|
||||
/// clutters the debug output. This macro prints log messages in a structured
|
||||
/// format, including the log level, filename, and line number.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// The macro supports three types of arguments:
|
||||
///
|
||||
/// ## 1. **Formatted Messages**
|
||||
/// Similar to `println!` or `format!`, allows embedding values inside a
|
||||
/// formatted string.
|
||||
///
|
||||
/// ```rust
|
||||
/// flog!(ERROR, "foo is {}", foo);
|
||||
/// ```
|
||||
/// **Output:**
|
||||
/// ```plaintext
|
||||
/// [ERROR][file.rs:10] foo is <value of foo>
|
||||
/// ```
|
||||
///
|
||||
/// ## 2. **Literals**
|
||||
/// Directly prints each literal argument as a separate line.
|
||||
///
|
||||
/// ```rust
|
||||
/// flog!(WARN, "foo", "bar");
|
||||
/// ```
|
||||
/// **Output:**
|
||||
/// ```plaintext
|
||||
/// [WARN][file.rs:10] foo
|
||||
/// [WARN][file.rs:10] bar
|
||||
/// ```
|
||||
///
|
||||
/// ## 3. **Expressions**
|
||||
/// Logs the evaluated result of each given expression, displaying both the
|
||||
/// expression and its value.
|
||||
///
|
||||
/// ```rust
|
||||
/// flog!(INFO, 1.min(2));
|
||||
/// ```
|
||||
/// **Output:**
|
||||
/// ```plaintext
|
||||
/// [INFO][file.rs:10] 1
|
||||
/// ```
|
||||
///
|
||||
/// # Considerations
|
||||
/// - This macro uses `eprintln!()` internally, so its formatting rules must be
|
||||
/// followed.
|
||||
/// - **Literals and formatted messages** require arguments that implement
|
||||
/// [`std::fmt::Display`].
|
||||
/// - **Expressions** require arguments that implement [`std::fmt::Debug`].
|
||||
#[macro_export]
|
||||
macro_rules! flog {
|
||||
($level:path, $fmt:literal, $($args:expr),+ $(,)?) => {{
|
||||
use $crate::libsh::flog::log_level;
|
||||
use $crate::libsh::term::Styled;
|
||||
use $crate::libsh::term::Style;
|
||||
|
||||
if $level <= log_level() {
|
||||
let file = file!().styled(Style::Cyan);
|
||||
let line = line!().to_string().styled(Style::Cyan);
|
||||
|
||||
eprintln!(
|
||||
"[{}][{}:{}] {}",
|
||||
$level, file, line, format!($fmt, $($args),+)
|
||||
);
|
||||
}
|
||||
}};
|
||||
|
||||
($level:path, $($val:expr),+ $(,)?) => {{
|
||||
use $crate::libsh::flog::log_level;
|
||||
use $crate::libsh::term::Styled;
|
||||
use $crate::libsh::term::Style;
|
||||
|
||||
if $level <= log_level() {
|
||||
let file = file!().styled(Style::Cyan);
|
||||
let line = line!().to_string().styled(Style::Cyan);
|
||||
|
||||
$(
|
||||
let val_name = stringify!($val);
|
||||
eprintln!(
|
||||
"[{}][{}:{}] {} = {:#?}",
|
||||
$level, file, line, val_name, &$val
|
||||
);
|
||||
)+
|
||||
}
|
||||
}};
|
||||
|
||||
($level:path, $($lit:literal),+ $(,)?) => {{
|
||||
use $crate::libsh::flog::log_level;
|
||||
use $crate::libsh::term::Styled;
|
||||
use $crate::libsh::term::Style;
|
||||
|
||||
if $level <= log_level() {
|
||||
let file = file!().styled(Style::Cyan);
|
||||
let line = line!().to_string().styled(Style::Cyan);
|
||||
|
||||
$(
|
||||
eprintln!(
|
||||
"[{}][{}:{}] {}",
|
||||
$level, file, line, $lit
|
||||
);
|
||||
)+
|
||||
}
|
||||
}};
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod error;
|
||||
pub mod flog;
|
||||
pub mod guards;
|
||||
pub mod sys;
|
||||
pub mod term;
|
||||
|
||||
@@ -17,6 +17,9 @@ pub mod shopt;
|
||||
pub mod signal;
|
||||
pub mod state;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod testutil;
|
||||
|
||||
use std::os::fd::BorrowedFd;
|
||||
use std::process::ExitCode;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -361,11 +364,13 @@ fn handle_readline_event(readline: &mut ShedVi, event: ShResult<ReadlineEvent>)
|
||||
|
||||
pre_exec.exec_with(&input);
|
||||
|
||||
// Time this command and temporarily restore cooked terminal mode while it runs.
|
||||
let start = Instant::now();
|
||||
write_meta(|m| m.start_timer());
|
||||
if let Err(e) = RawModeGuard::with_cooked_mode(|| {
|
||||
exec_input(input.clone(), None, true, Some("<stdin>".into()))
|
||||
}) {
|
||||
// CleanExit signals an intentional shell exit; any other error is printed.
|
||||
match e.kind() {
|
||||
ShErrKind::CleanExit(code) => {
|
||||
QUIT_CODE.store(*code, Ordering::SeqCst);
|
||||
|
||||
@@ -5,11 +5,10 @@ use std::{
|
||||
};
|
||||
|
||||
use ariadne::Fmt;
|
||||
use nix::sys::resource;
|
||||
|
||||
use crate::{
|
||||
builtin::{
|
||||
alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, autocmd::autocmd, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, getopts::getopts, intro, jobctl::{self, JobBehavior, continue_job, disown, jobs}, keymap, map, pwd::pwd, read::{self, read_builtin}, resource::ulimit, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset}, zoltraak::zoltraak
|
||||
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, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset}
|
||||
},
|
||||
expand::{expand_aliases, expand_case_pattern, glob_to_regex},
|
||||
jobs::{ChildProc, JobStack, attach_tty, dispatch_job},
|
||||
@@ -450,7 +449,6 @@ impl Dispatcher {
|
||||
let fork_builtins = brc_grp.flags.contains(NdFlags::FORK_BUILTINS);
|
||||
|
||||
self.io_stack.append_to_frame(brc_grp.redirs);
|
||||
if self.interactive {}
|
||||
let guard = self.io_stack.pop_frame().redirect()?;
|
||||
let brc_grp_logic = |s: &mut Self| -> ShResult<()> {
|
||||
for node in body {
|
||||
@@ -911,7 +909,7 @@ impl Dispatcher {
|
||||
"export" => export(cmd),
|
||||
"local" => local(cmd),
|
||||
"pwd" => pwd(cmd),
|
||||
"source" => source(cmd),
|
||||
"source" | "." => source(cmd),
|
||||
"shift" => shift(cmd),
|
||||
"fg" => continue_job(cmd, JobBehavior::Foregound),
|
||||
"bg" => continue_job(cmd, JobBehavior::Background),
|
||||
@@ -923,7 +921,6 @@ impl Dispatcher {
|
||||
"break" => flowctl(cmd, ShErrKind::LoopBreak(0)),
|
||||
"continue" => flowctl(cmd, ShErrKind::LoopContinue(0)),
|
||||
"exit" => flowctl(cmd, ShErrKind::CleanExit(0)),
|
||||
"zoltraak" => zoltraak(cmd),
|
||||
"shopt" => shopt(cmd),
|
||||
"read" => read_builtin(cmd),
|
||||
"trap" => trap(cmd),
|
||||
|
||||
@@ -18,9 +18,9 @@ use crate::{
|
||||
pub mod execute;
|
||||
pub mod lex;
|
||||
|
||||
pub const TEST_UNARY_OPS: [&str; 21] = [
|
||||
"-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p", "-r", "-s", "-S", "-t", "-u",
|
||||
"-w", "-x", "-O", "-G", "-N",
|
||||
pub const TEST_UNARY_OPS: [&str; 23] = [
|
||||
"-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-n", "-p", "-r", "-s", "-S", "-t",
|
||||
"-u", "-w", "-x", "-z", "-O", "-G", "-N",
|
||||
];
|
||||
|
||||
/// Try to match a specific parsing rule
|
||||
|
||||
@@ -33,7 +33,5 @@ pub use nix::{
|
||||
},
|
||||
};
|
||||
|
||||
pub use crate::flog;
|
||||
pub use crate::libsh::flog::ShedLogLevel::*;
|
||||
|
||||
// Additional utilities, if needed, can be added here
|
||||
|
||||
154
src/procio.rs
154
src/procio.rs
@@ -385,3 +385,157 @@ impl Iterator for PipeGenerator {
|
||||
Some((rpipe, Some(wpipe)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::testutil::{TestGuard, has_cmd, has_cmds, test_input};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn pipeline_simple() {
|
||||
if !has_cmd("sed") { return };
|
||||
let g = TestGuard::new();
|
||||
|
||||
test_input("echo foo | sed 's/foo/bar/'").unwrap();
|
||||
|
||||
let out = g.read_output();
|
||||
assert_eq!(out, "bar\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pipeline_multi() {
|
||||
if !has_cmds(&[
|
||||
"cut",
|
||||
"sed"
|
||||
]) { return; }
|
||||
let g = TestGuard::new();
|
||||
|
||||
test_input("echo foo bar baz | cut -d ' ' -f 2 | sed 's/a/A/'").unwrap();
|
||||
|
||||
let out = g.read_output();
|
||||
assert_eq!(out, "bAr\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rube_goldberg_pipeline() {
|
||||
if !has_cmds(&[
|
||||
"sed",
|
||||
"cat",
|
||||
]) { return }
|
||||
let g = TestGuard::new();
|
||||
|
||||
test_input("{ echo foo; echo bar } | if cat; then :; else echo failed; fi | (read line && echo $line | sed 's/foo/baz/'; sed 's/bar/buzz/')").unwrap();
|
||||
|
||||
let out = g.read_output();
|
||||
assert_eq!(out, "baz\nbuzz\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_file_redir() {
|
||||
let mut g = TestGuard::new();
|
||||
|
||||
test_input("echo this is in a file > /tmp/simple_file_redir.txt").unwrap();
|
||||
|
||||
g.add_cleanup(|| { std::fs::remove_file("/tmp/simple_file_redir.txt").ok(); });
|
||||
let contents = std::fs::read_to_string("/tmp/simple_file_redir.txt").unwrap();
|
||||
|
||||
assert_eq!(contents, "this is in a file\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_file_redir() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let path = dir.path().join("append.txt");
|
||||
let _g = TestGuard::new();
|
||||
|
||||
test_input(format!("echo first > {}", path.display())).unwrap();
|
||||
test_input(format!("echo second >> {}", path.display())).unwrap();
|
||||
|
||||
let contents = std::fs::read_to_string(&path).unwrap();
|
||||
assert_eq!(contents, "first\nsecond\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_redir() {
|
||||
if !has_cmd("cat") { return; }
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let path = dir.path().join("input.txt");
|
||||
std::fs::write(&path, "hello from file\n").unwrap();
|
||||
let g = TestGuard::new();
|
||||
|
||||
test_input(format!("cat < {}", path.display())).unwrap();
|
||||
|
||||
let out = g.read_output();
|
||||
assert_eq!(out, "hello from file\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stderr_redir_to_file() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let path = dir.path().join("err.txt");
|
||||
let g = TestGuard::new();
|
||||
|
||||
test_input(format!("echo error msg 2> {} >&2", path.display())).unwrap();
|
||||
|
||||
let contents = std::fs::read_to_string(&path).unwrap();
|
||||
assert_eq!(contents, "error msg\n");
|
||||
// stdout should be empty since we redirected to stderr
|
||||
let out = g.read_output();
|
||||
assert_eq!(out, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pipe_and_stderr() {
|
||||
if !has_cmd("cat") { return; }
|
||||
let g = TestGuard::new();
|
||||
|
||||
test_input("echo on stderr >&2 |& cat").unwrap();
|
||||
|
||||
let out = g.read_output();
|
||||
assert_eq!(out, "on stderr\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_redir_clobber() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let path = dir.path().join("clobber.txt");
|
||||
let _g = TestGuard::new();
|
||||
|
||||
test_input(format!("echo first > {}", path.display())).unwrap();
|
||||
test_input(format!("echo second > {}", path.display())).unwrap();
|
||||
|
||||
let contents = std::fs::read_to_string(&path).unwrap();
|
||||
assert_eq!(contents, "second\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pipeline_preserves_exit_status() {
|
||||
if !has_cmd("cat") { return; }
|
||||
let _g = TestGuard::new();
|
||||
|
||||
test_input("false | cat").unwrap();
|
||||
|
||||
// Pipeline exit status is the last command
|
||||
let status = crate::state::get_status();
|
||||
assert_eq!(status, 0);
|
||||
|
||||
test_input("cat < /dev/null | false").unwrap();
|
||||
|
||||
let status = crate::state::get_status();
|
||||
assert_ne!(status, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fd_duplication() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let path = dir.path().join("dup.txt");
|
||||
let _g = TestGuard::new();
|
||||
|
||||
// Redirect stdout to file, then dup stderr to stdout — both should go to file
|
||||
test_input(format!("{{ echo out; echo err >&2 }} > {} 2>&1", path.display())).unwrap();
|
||||
|
||||
let contents = std::fs::read_to_string(&path).unwrap();
|
||||
assert!(contents.contains("out"));
|
||||
assert!(contents.contains("err"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,21 @@ pub fn complete_vars(start: &str) -> Vec<String> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn complete_vars_raw(raw: &str) -> Vec<String> {
|
||||
if !read_vars(|v| v.get_var(raw)).is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
// if we are here, we have a variable substitution that isn't complete
|
||||
// so let's try to complete it
|
||||
read_vars(|v| {
|
||||
v.flatten_vars()
|
||||
.keys()
|
||||
.filter(|k| k.starts_with(raw) && *k != raw)
|
||||
.map(|k| k.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn extract_var_name(text: &str) -> Option<(String, usize, usize)> {
|
||||
let mut chars = text.chars().peekable();
|
||||
let mut name = String::new();
|
||||
@@ -422,7 +437,7 @@ impl CompSpec for BashCompSpec {
|
||||
candidates.extend(complete_commands(&expanded));
|
||||
}
|
||||
if self.vars {
|
||||
candidates.extend(complete_vars(&expanded));
|
||||
candidates.extend(complete_vars_raw(&expanded));
|
||||
}
|
||||
if self.users {
|
||||
candidates.extend(complete_users(&expanded));
|
||||
|
||||
@@ -466,3 +466,125 @@ impl History {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{state, testutil::TestGuard};
|
||||
use scopeguard::guard;
|
||||
use std::{env, fs, path::Path, sync::Mutex};
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn with_env_var(key: &str, val: &str) -> impl Drop {
|
||||
let prev = env::var(key).ok();
|
||||
unsafe {
|
||||
env::set_var(key, val);
|
||||
}
|
||||
guard(prev, move |p| match p {
|
||||
Some(v) => unsafe {
|
||||
env::set_var(key, v)
|
||||
},
|
||||
None => unsafe {
|
||||
env::remove_var(key)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Temporarily mutate shell options for a test and restore the
|
||||
/// previous values when the returned guard is dropped.
|
||||
fn with_shopts(modifier: impl FnOnce(&mut crate::shopt::ShOpts)) -> impl Drop {
|
||||
let original = state::read_shopts(|s| s.clone());
|
||||
state::write_shopts(|s| modifier(s));
|
||||
guard(original, |orig| {
|
||||
state::write_shopts(|s| *s = orig);
|
||||
})
|
||||
}
|
||||
|
||||
fn write_history_file(path: &Path) {
|
||||
fs::write(
|
||||
path,
|
||||
[
|
||||
": 1;1;first\n",
|
||||
": 2;1;second\n",
|
||||
": 3;1;third\n",
|
||||
]
|
||||
.concat(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_new_respects_max_hist_limit() {
|
||||
let _lock = TestGuard::new();
|
||||
let tmp = tempdir().unwrap();
|
||||
let hist_path = tmp.path().join("history");
|
||||
write_history_file(&hist_path);
|
||||
|
||||
let _env_guard = with_env_var("SHEDHIST", hist_path.to_str().unwrap());
|
||||
let _opts_guard = with_shopts(|s| {
|
||||
s.core.max_hist = 2;
|
||||
s.core.hist_ignore_dupes = true;
|
||||
});
|
||||
|
||||
let history = History::new().unwrap();
|
||||
|
||||
assert_eq!(history.entries.len(), 2);
|
||||
assert_eq!(history.search_mask.len(), 2);
|
||||
assert_eq!(history.cursor, 2);
|
||||
assert_eq!(history.max_size, Some(2));
|
||||
assert!(history.ignore_dups);
|
||||
assert!(history.pending.is_none());
|
||||
assert_eq!(history.entries[0].command(), "second");
|
||||
assert_eq!(history.entries[1].command(), "third");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_new_keeps_all_when_unlimited() {
|
||||
let _lock = TestGuard::new();
|
||||
let tmp = tempdir().unwrap();
|
||||
let hist_path = tmp.path().join("history");
|
||||
write_history_file(&hist_path);
|
||||
|
||||
let _env_guard = with_env_var("SHEDHIST", hist_path.to_str().unwrap());
|
||||
let _opts_guard = with_shopts(|s| {
|
||||
s.core.max_hist = -1;
|
||||
s.core.hist_ignore_dupes = false;
|
||||
});
|
||||
|
||||
let history = History::new().unwrap();
|
||||
|
||||
assert_eq!(history.entries.len(), 3);
|
||||
assert_eq!(history.search_mask.len(), 3);
|
||||
assert_eq!(history.cursor, 3);
|
||||
assert_eq!(history.max_size, None);
|
||||
assert!(!history.ignore_dups);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_new_dedupes_search_mask_to_latest_occurrence() {
|
||||
let _lock = TestGuard::new();
|
||||
let tmp = tempdir().unwrap();
|
||||
let hist_path = tmp.path().join("history");
|
||||
fs::write(
|
||||
&hist_path,
|
||||
[
|
||||
": 1;1;repeat\n",
|
||||
": 2;1;unique\n",
|
||||
": 3;1;repeat\n",
|
||||
]
|
||||
.concat(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let _env_guard = with_env_var("SHEDHIST", hist_path.to_str().unwrap());
|
||||
let _opts_guard = with_shopts(|s| {
|
||||
s.core.max_hist = 10;
|
||||
});
|
||||
|
||||
let history = History::new().unwrap();
|
||||
|
||||
let masked: Vec<_> = history.search_mask.iter().map(|e| e.command()).collect();
|
||||
assert_eq!(masked, vec!["unique", "repeat"]);
|
||||
assert_eq!(history.cursor, history.search_mask.len());
|
||||
}
|
||||
}
|
||||
|
||||
148
src/shopt.rs
148
src/shopt.rs
@@ -145,6 +145,7 @@ pub struct ShOptCore {
|
||||
pub auto_hist: bool,
|
||||
pub bell_enabled: bool,
|
||||
pub max_recurse_depth: usize,
|
||||
pub xpg_echo: bool,
|
||||
}
|
||||
|
||||
impl ShOptCore {
|
||||
@@ -184,6 +185,12 @@ impl ShOptCore {
|
||||
"shopt: expected an integer for max_hist value (-1 for unlimited)",
|
||||
));
|
||||
};
|
||||
if val < -1 {
|
||||
return Err(ShErr::simple(
|
||||
ShErrKind::SyntaxErr,
|
||||
"shopt: expected a non-negative integer or -1 for max_hist value",
|
||||
));
|
||||
}
|
||||
self.max_hist = val;
|
||||
}
|
||||
"interactive_comments" => {
|
||||
@@ -222,6 +229,15 @@ impl ShOptCore {
|
||||
};
|
||||
self.max_recurse_depth = val;
|
||||
}
|
||||
"xpg_echo" => {
|
||||
let Ok(val) = val.parse::<bool>() else {
|
||||
return Err(ShErr::simple(
|
||||
ShErrKind::SyntaxErr,
|
||||
"shopt: expected 'true' or 'false' for xpg_echo value",
|
||||
));
|
||||
};
|
||||
self.xpg_echo = val;
|
||||
}
|
||||
_ => {
|
||||
return Err(ShErr::simple(
|
||||
ShErrKind::SyntaxErr,
|
||||
@@ -283,6 +299,11 @@ impl ShOptCore {
|
||||
output.push_str(&format!("{}", self.max_recurse_depth));
|
||||
Ok(Some(output))
|
||||
}
|
||||
"xpg_echo" => {
|
||||
let mut output = String::from("Whether echo expands escape sequences by default\n");
|
||||
output.push_str(&format!("{}", self.xpg_echo));
|
||||
Ok(Some(output))
|
||||
}
|
||||
_ => Err(ShErr::simple(
|
||||
ShErrKind::SyntaxErr,
|
||||
format!("shopt: Unexpected 'core' option '{query}'"),
|
||||
@@ -305,6 +326,7 @@ impl Display for ShOptCore {
|
||||
output.push(format!("auto_hist = {}", self.auto_hist));
|
||||
output.push(format!("bell_enabled = {}", self.bell_enabled));
|
||||
output.push(format!("max_recurse_depth = {}", self.max_recurse_depth));
|
||||
output.push(format!("xpg_echo = {}", self.xpg_echo));
|
||||
|
||||
let final_output = output.join("\n");
|
||||
|
||||
@@ -323,6 +345,7 @@ impl Default for ShOptCore {
|
||||
auto_hist: true,
|
||||
bell_enabled: true,
|
||||
max_recurse_depth: 1000,
|
||||
xpg_echo: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -517,3 +540,128 @@ impl Default for ShOptPrompt {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn all_core_fields_covered() {
|
||||
let ShOptCore {
|
||||
dotglob, autocd, hist_ignore_dupes, max_hist,
|
||||
interactive_comments, auto_hist, bell_enabled, max_recurse_depth,
|
||||
xpg_echo,
|
||||
} = ShOptCore::default();
|
||||
// If a field is added to the struct, this destructure fails to compile.
|
||||
let _ = (
|
||||
dotglob,
|
||||
autocd,
|
||||
hist_ignore_dupes,
|
||||
max_hist,
|
||||
interactive_comments,
|
||||
auto_hist,
|
||||
bell_enabled,
|
||||
max_recurse_depth,
|
||||
xpg_echo,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_and_get_core_bool() {
|
||||
let mut opts = ShOpts::default();
|
||||
assert!(!opts.core.dotglob);
|
||||
|
||||
opts.set("core.dotglob", "true").unwrap();
|
||||
assert!(opts.core.dotglob);
|
||||
|
||||
opts.set("core.dotglob", "false").unwrap();
|
||||
assert!(!opts.core.dotglob);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_and_get_core_int() {
|
||||
let mut opts = ShOpts::default();
|
||||
assert_eq!(opts.core.max_hist, 10_000);
|
||||
|
||||
opts.set("core.max_hist", "500").unwrap();
|
||||
assert_eq!(opts.core.max_hist, 500);
|
||||
|
||||
opts.set("core.max_hist", "-1").unwrap();
|
||||
assert_eq!(opts.core.max_hist, -1);
|
||||
|
||||
assert!(opts.set("core.max_hist", "-500").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_and_get_prompt_opts() {
|
||||
let mut opts = ShOpts::default();
|
||||
|
||||
opts.set("prompt.edit_mode", "emacs").unwrap();
|
||||
assert!(matches!(opts.prompt.edit_mode, ShedEditMode::Emacs));
|
||||
|
||||
opts.set("prompt.edit_mode", "vi").unwrap();
|
||||
assert!(matches!(opts.prompt.edit_mode, ShedEditMode::Vi));
|
||||
|
||||
opts.set("prompt.comp_limit", "50").unwrap();
|
||||
assert_eq!(opts.prompt.comp_limit, 50);
|
||||
|
||||
opts.set("prompt.leader", "space").unwrap();
|
||||
assert_eq!(opts.prompt.leader, "space");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_set_returns_none() {
|
||||
let mut opts = ShOpts::default();
|
||||
let result = opts.query("core.autocd=true").unwrap();
|
||||
assert!(result.is_none());
|
||||
assert!(opts.core.autocd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_get_returns_some() {
|
||||
let opts = ShOpts::default();
|
||||
let result = opts.get("core.dotglob").unwrap();
|
||||
assert!(result.is_some());
|
||||
let text = result.unwrap();
|
||||
assert!(text.contains("false"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_category_errors() {
|
||||
let mut opts = ShOpts::default();
|
||||
assert!(opts.set("bogus.dotglob", "true").is_err());
|
||||
assert!(opts.get("bogus.dotglob").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_option_errors() {
|
||||
let mut opts = ShOpts::default();
|
||||
assert!(opts.set("core.nonexistent", "true").is_err());
|
||||
assert!(opts.set("prompt.nonexistent", "true").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_value_errors() {
|
||||
let mut opts = ShOpts::default();
|
||||
assert!(opts.set("core.dotglob", "notabool").is_err());
|
||||
assert!(opts.set("core.max_hist", "notanint").is_err());
|
||||
assert!(opts.set("core.max_recurse_depth", "-5").is_err());
|
||||
assert!(opts.set("prompt.edit_mode", "notepad").is_err());
|
||||
assert!(opts.set("prompt.comp_limit", "abc").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_category_lists_all() {
|
||||
let opts = ShOpts::default();
|
||||
let core_output = opts.get("core").unwrap().unwrap();
|
||||
assert!(core_output.contains("dotglob"));
|
||||
assert!(core_output.contains("autocd"));
|
||||
assert!(core_output.contains("max_hist"));
|
||||
assert!(core_output.contains("bell_enabled"));
|
||||
|
||||
let prompt_output = opts.get("prompt").unwrap().unwrap();
|
||||
assert!(prompt_output.contains("edit_mode"));
|
||||
assert!(prompt_output.contains("comp_limit"));
|
||||
assert!(prompt_output.contains("highlight"));
|
||||
}
|
||||
}
|
||||
|
||||
42
src/state.rs
42
src/state.rs
@@ -32,12 +32,20 @@ use crate::{
|
||||
shopt::ShOpts,
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
pub static SHED: Shed = Shed::new();
|
||||
}
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct Shed {
|
||||
pub jobs: RefCell<JobTab>,
|
||||
pub var_scopes: RefCell<ScopeStack>,
|
||||
pub meta: RefCell<MetaTab>,
|
||||
pub logic: RefCell<LogTab>,
|
||||
pub shopts: RefCell<ShOpts>,
|
||||
|
||||
#[cfg(test)]
|
||||
saved: RefCell<Option<Box<Self>>>,
|
||||
}
|
||||
|
||||
impl Shed {
|
||||
@@ -48,6 +56,9 @@ impl Shed {
|
||||
meta: RefCell::new(MetaTab::new()),
|
||||
logic: RefCell::new(LogTab::new()),
|
||||
shopts: RefCell::new(ShOpts::default()),
|
||||
|
||||
#[cfg(test)]
|
||||
saved: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,6 +69,31 @@ impl Default for Shed {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl Shed {
|
||||
pub fn save(&self) {
|
||||
let saved = Self {
|
||||
jobs: RefCell::new(self.jobs.borrow().clone()),
|
||||
var_scopes: RefCell::new(self.var_scopes.borrow().clone()),
|
||||
meta: RefCell::new(self.meta.borrow().clone()),
|
||||
logic: RefCell::new(self.logic.borrow().clone()),
|
||||
shopts: RefCell::new(self.shopts.borrow().clone()),
|
||||
saved: RefCell::new(None),
|
||||
};
|
||||
*self.saved.borrow_mut() = Some(Box::new(saved));
|
||||
}
|
||||
|
||||
pub fn restore(&self) {
|
||||
if let Some(saved) = self.saved.take() {
|
||||
*self.jobs.borrow_mut() = saved.jobs.into_inner();
|
||||
*self.var_scopes.borrow_mut() = saved.var_scopes.into_inner();
|
||||
*self.meta.borrow_mut() = saved.meta.into_inner();
|
||||
*self.logic.borrow_mut() = saved.logic.into_inner();
|
||||
*self.shopts.borrow_mut() = saved.shopts.into_inner();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Hash, Eq, PartialEq, Debug, Clone, Copy)]
|
||||
pub enum ShellParam {
|
||||
// Global
|
||||
@@ -485,10 +521,6 @@ impl ScopeStack {
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
pub static SHED: Shed = Shed::new();
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ShAlias {
|
||||
pub body: String,
|
||||
@@ -1258,7 +1290,7 @@ impl VarTab {
|
||||
}
|
||||
|
||||
/// A table of metadata for the shell
|
||||
#[derive(Default, Debug)]
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct MetaTab {
|
||||
// command running duration
|
||||
runtime_start: Option<Instant>,
|
||||
|
||||
155
src/testutil.rs
Normal file
155
src/testutil.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env,
|
||||
os::fd::{AsRawFd, OwnedFd},
|
||||
path::PathBuf,
|
||||
sync::{self, MutexGuard},
|
||||
};
|
||||
|
||||
use nix::{
|
||||
fcntl::{FcntlArg, OFlag, fcntl},
|
||||
pty::openpty,
|
||||
sys::termios::{OutputFlags, SetArg, tcgetattr, tcsetattr},
|
||||
unistd::read,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
libsh::error::ShResult,
|
||||
parse::{Redir, RedirType, execute::exec_input},
|
||||
procio::{IoFrame, IoMode, RedirGuard},
|
||||
state::{MetaTab, SHED},
|
||||
};
|
||||
|
||||
static TEST_MUTEX: sync::Mutex<()> = sync::Mutex::new(());
|
||||
|
||||
pub fn has_cmds(cmds: &[&str]) -> bool {
|
||||
let path_cmds = MetaTab::get_cmds_in_path();
|
||||
path_cmds.iter().all(|c| cmds.iter().any(|&cmd| c == cmd))
|
||||
}
|
||||
|
||||
pub fn has_cmd(cmd: &str) -> bool {
|
||||
MetaTab::get_cmds_in_path().into_iter().any(|c| c == cmd)
|
||||
}
|
||||
|
||||
pub fn test_input(input: impl Into<String>) -> ShResult<()> {
|
||||
exec_input(input.into(), None, true, None)
|
||||
}
|
||||
|
||||
pub struct TestGuard {
|
||||
_lock: MutexGuard<'static, ()>,
|
||||
_redir_guard: RedirGuard,
|
||||
old_cwd: PathBuf,
|
||||
saved_env: HashMap<String, String>,
|
||||
pty_master: OwnedFd,
|
||||
pty_slave: OwnedFd,
|
||||
|
||||
cleanups: Vec<Box<dyn FnOnce()>>
|
||||
}
|
||||
|
||||
impl TestGuard {
|
||||
pub fn new() -> Self {
|
||||
let _lock = TEST_MUTEX.lock().unwrap();
|
||||
|
||||
let pty = openpty(None, None).unwrap();
|
||||
let (pty_master,pty_slave) = (pty.master, pty.slave);
|
||||
let mut attrs = tcgetattr(&pty_slave).unwrap();
|
||||
attrs.output_flags &= !OutputFlags::ONLCR;
|
||||
tcsetattr(&pty_slave, SetArg::TCSANOW, &attrs).unwrap();
|
||||
|
||||
let mut frame = IoFrame::new();
|
||||
frame.push(
|
||||
Redir::new(
|
||||
IoMode::Fd {
|
||||
tgt_fd: 0,
|
||||
src_fd: pty_slave.as_raw_fd(),
|
||||
},
|
||||
RedirType::Input,
|
||||
),
|
||||
);
|
||||
frame.push(
|
||||
Redir::new(
|
||||
IoMode::Fd {
|
||||
tgt_fd: 1,
|
||||
src_fd: pty_slave.as_raw_fd(),
|
||||
},
|
||||
RedirType::Output,
|
||||
),
|
||||
);
|
||||
frame.push(
|
||||
Redir::new(
|
||||
IoMode::Fd {
|
||||
tgt_fd: 2,
|
||||
src_fd: pty_slave.as_raw_fd(),
|
||||
},
|
||||
RedirType::Output,
|
||||
),
|
||||
);
|
||||
|
||||
let _redir_guard = frame.redirect().unwrap();
|
||||
|
||||
let old_cwd = env::current_dir().unwrap();
|
||||
let saved_env = env::vars().collect();
|
||||
SHED.with(|s| s.save());
|
||||
Self {
|
||||
_lock,
|
||||
_redir_guard,
|
||||
old_cwd,
|
||||
saved_env,
|
||||
pty_master,
|
||||
pty_slave,
|
||||
cleanups: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_cleanup(&mut self, f: impl FnOnce() + 'static) {
|
||||
self.cleanups.push(Box::new(f));
|
||||
}
|
||||
|
||||
pub fn read_output(&self) -> String {
|
||||
let flags = fcntl(self.pty_master.as_raw_fd(), FcntlArg::F_GETFL).unwrap();
|
||||
let flags = OFlag::from_bits_truncate(flags);
|
||||
fcntl(
|
||||
self.pty_master.as_raw_fd(),
|
||||
FcntlArg::F_SETFL(flags | OFlag::O_NONBLOCK),
|
||||
).unwrap();
|
||||
|
||||
let mut out = vec![];
|
||||
let mut buf = [0;4096];
|
||||
loop {
|
||||
match read(self.pty_master.as_raw_fd(), &mut buf) {
|
||||
Ok(0) => break,
|
||||
Ok(n) => out.extend_from_slice(&buf[..n]),
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
fcntl(
|
||||
self.pty_master.as_raw_fd(),
|
||||
FcntlArg::F_SETFL(flags),
|
||||
).unwrap();
|
||||
|
||||
String::from_utf8_lossy(&out).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TestGuard {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestGuard {
|
||||
fn drop(&mut self) {
|
||||
env::set_current_dir(&self.old_cwd).ok();
|
||||
for (k, _) in env::vars() {
|
||||
unsafe { env::remove_var(&k); }
|
||||
}
|
||||
for (k, v) in &self.saved_env {
|
||||
unsafe { env::set_var(k, v); }
|
||||
}
|
||||
for cleanup in self.cleanups.drain(..).rev() {
|
||||
cleanup();
|
||||
}
|
||||
SHED.with(|s| s.restore());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user