added more linebuf tests extracted all verb match arms into private methods on LineBuf
427 lines
11 KiB
Rust
427 lines
11 KiB
Rust
use crate::{
|
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
|
parse::{NdRule, Node, execute::prepare_argv, lex::split_tk_at},
|
|
prelude::*,
|
|
procio::borrow_fd,
|
|
state::{self, VarFlags, VarKind, read_vars, write_vars},
|
|
};
|
|
|
|
pub fn readonly(node: Node) -> ShResult<()> {
|
|
let NdRule::Command {
|
|
assignments: _,
|
|
argv,
|
|
} = node.class
|
|
else {
|
|
unreachable!()
|
|
};
|
|
|
|
// Remove "readonly" from argv
|
|
let argv = if !argv.is_empty() {
|
|
&argv[1..]
|
|
} else {
|
|
&argv[..]
|
|
};
|
|
|
|
if argv.is_empty() {
|
|
// Display the local variables
|
|
let vars_output = read_vars(|v| {
|
|
let mut vars = v
|
|
.flatten_vars()
|
|
.into_iter()
|
|
.filter(|(_, v)| v.flags().contains(VarFlags::READONLY))
|
|
.map(|(k, v)| format!("{}={}", k, v))
|
|
.collect::<Vec<String>>();
|
|
vars.sort();
|
|
let mut vars_joined = vars.join("\n");
|
|
vars_joined.push('\n');
|
|
vars_joined
|
|
});
|
|
|
|
let stdout = borrow_fd(STDOUT_FILENO);
|
|
write(stdout, vars_output.as_bytes())?; // Write it
|
|
} else {
|
|
for tk in argv {
|
|
if let Some((var_tk, val_tk)) = split_tk_at(tk, "=") {
|
|
let var = var_tk.expand()?.get_words().join(" ");
|
|
let val = if val_tk.as_str().starts_with('(') && val_tk.as_str().ends_with(')') {
|
|
VarKind::arr_from_tk(val_tk.clone())?
|
|
} else {
|
|
VarKind::Str(val_tk.expand()?.get_words().join(" "))
|
|
};
|
|
write_vars(|v| v.set_var(&var, val, VarFlags::READONLY))?;
|
|
} else {
|
|
let arg = tk.clone().expand()?.get_words().join(" ");
|
|
write_vars(|v| v.set_var(&arg, VarKind::Str(String::new()), VarFlags::READONLY))?;
|
|
}
|
|
}
|
|
}
|
|
|
|
state::set_status(0);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn unset(node: Node) -> ShResult<()> {
|
|
let blame = node.get_span().clone();
|
|
let NdRule::Command {
|
|
assignments: _,
|
|
argv,
|
|
} = node.class
|
|
else {
|
|
unreachable!()
|
|
};
|
|
|
|
let mut argv = prepare_argv(argv)?;
|
|
if !argv.is_empty() {
|
|
argv.remove(0);
|
|
}
|
|
|
|
if argv.is_empty() {
|
|
return Err(ShErr::at(
|
|
ShErrKind::SyntaxErr,
|
|
blame,
|
|
"unset: Expected at least one argument",
|
|
));
|
|
}
|
|
|
|
for (arg, span) in argv {
|
|
if !read_vars(|v| v.var_exists(&arg)) {
|
|
return Err(ShErr::at(
|
|
ShErrKind::ExecFail,
|
|
span,
|
|
format!("unset: No such variable '{arg}'"),
|
|
));
|
|
}
|
|
write_vars(|v| v.unset_var(&arg))?;
|
|
}
|
|
|
|
state::set_status(0);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn export(node: Node) -> ShResult<()> {
|
|
let NdRule::Command {
|
|
assignments: _,
|
|
argv,
|
|
} = node.class
|
|
else {
|
|
unreachable!()
|
|
};
|
|
|
|
// Remove "export" from argv
|
|
let argv = if !argv.is_empty() {
|
|
&argv[1..]
|
|
} else {
|
|
&argv[..]
|
|
};
|
|
|
|
if argv.is_empty() {
|
|
// Display the environment variables
|
|
let mut env_output = env::vars()
|
|
.map(|var| format!("{}={}", var.0, var.1)) // Get all of them, zip them into one string
|
|
.collect::<Vec<_>>();
|
|
env_output.sort(); // Sort them alphabetically
|
|
let mut env_output = env_output.join("\n"); // Join them with newlines
|
|
env_output.push('\n'); // Push a final newline
|
|
|
|
let stdout = borrow_fd(STDOUT_FILENO);
|
|
write(stdout, env_output.as_bytes())?; // Write it
|
|
} else {
|
|
for tk in argv {
|
|
if let Some((var_tk, val_tk)) = split_tk_at(tk, "=") {
|
|
let var = var_tk.expand()?.get_words().join(" ");
|
|
let val = if val_tk.as_str().starts_with('(') && val_tk.as_str().ends_with(')') {
|
|
VarKind::arr_from_tk(val_tk.clone())?
|
|
} else {
|
|
VarKind::Str(val_tk.expand()?.get_words().join(" "))
|
|
};
|
|
write_vars(|v| v.set_var(&var, val, VarFlags::EXPORT))?;
|
|
} else {
|
|
let arg = tk.clone().expand()?.get_words().join(" ");
|
|
write_vars(|v| v.export_var(&arg)); // Export an existing variable, if any
|
|
}
|
|
}
|
|
}
|
|
state::set_status(0);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn local(node: Node) -> ShResult<()> {
|
|
let NdRule::Command {
|
|
assignments: _,
|
|
argv,
|
|
} = node.class
|
|
else {
|
|
unreachable!()
|
|
};
|
|
|
|
// Remove "local" from argv
|
|
let argv = if !argv.is_empty() {
|
|
&argv[1..]
|
|
} else {
|
|
&argv[..]
|
|
};
|
|
|
|
if argv.is_empty() {
|
|
// Display the local variables
|
|
let vars_output = read_vars(|v| {
|
|
let mut vars = v
|
|
.flatten_vars()
|
|
.into_iter()
|
|
.map(|(k, v)| format!("{}={}", k, v))
|
|
.collect::<Vec<String>>();
|
|
vars.sort();
|
|
let mut vars_joined = vars.join("\n");
|
|
vars_joined.push('\n');
|
|
vars_joined
|
|
});
|
|
|
|
let stdout = borrow_fd(STDOUT_FILENO);
|
|
write(stdout, vars_output.as_bytes())?; // Write it
|
|
} else {
|
|
for tk in argv {
|
|
if let Some((var_tk, val_tk)) = split_tk_at(tk, "=") {
|
|
let var = var_tk.expand()?.get_words().join(" ");
|
|
let val = if val_tk.as_str().starts_with('(') && val_tk.as_str().ends_with(')') {
|
|
VarKind::arr_from_tk(val_tk.clone())?
|
|
} else {
|
|
VarKind::Str(val_tk.expand()?.get_words().join(" "))
|
|
};
|
|
write_vars(|v| v.set_var(&var, val, VarFlags::LOCAL))?;
|
|
} else {
|
|
let arg = tk.clone().expand()?.get_words().join(" ");
|
|
write_vars(|v| v.set_var(&arg, VarKind::Str(String::new()), VarFlags::LOCAL))?;
|
|
}
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
}
|