use std::ffi::OsStr;
use std::ffi::OsString;
use std::path::Path;
use futures::future::LocalBoxFuture;
use crate::shell::types::ExecuteResult;
use crate::shell::types::ShellState;
use super::ShellCommand;
use super::ShellCommandContext;
pub struct TestCommand;
impl TestCommand {
pub fn new_test() -> Self {
Self
}
}
impl ShellCommand for TestCommand {
fn execute(
&self,
mut context: ShellCommandContext,
) -> LocalBoxFuture<'static, ExecuteResult> {
let code = match evaluate(&context.args, &context.state) {
Ok(true) => 0,
Ok(false) => 1,
Err(msg) => {
let _ = context.stderr.write_line(&format!("test: {msg}"));
2
}
};
Box::pin(futures::future::ready(ExecuteResult::from_exit_code(code)))
}
}
fn evaluate(args: &[OsString], state: &ShellState) -> Result<bool, String> {
match args.len() {
0 => Ok(false),
1 => Ok(!args[0].is_empty()),
2 => {
if args[0] == "!" {
Ok(args[1].is_empty())
} else {
unary(&args[0], &args[1], state)
}
}
3 => {
if args[0] == "!" {
Ok(!unary(&args[1], &args[2], state)?)
} else {
binary(&args[0], &args[1], &args[2])
}
}
4 if args[0] == "!" => Ok(!binary(&args[1], &args[2], &args[3])?),
_ => Err("too many arguments".to_string()),
}
}
fn unary(op: &OsStr, arg: &OsStr, state: &ShellState) -> Result<bool, String> {
let op_str = op.to_str().ok_or_else(|| {
format!("unary operator expected: {}", op.to_string_lossy())
})?;
match op_str {
"-n" => Ok(!arg.is_empty()),
"-z" => Ok(arg.is_empty()),
"-e" | "-f" | "-d" | "-s" => {
let path = state.cwd().join(Path::new(arg));
let metadata = std::fs::metadata(&path).ok();
Ok(match op_str {
"-e" => metadata.is_some(),
"-f" => metadata.is_some_and(|m| m.is_file()),
"-d" => metadata.is_some_and(|m| m.is_dir()),
"-s" => metadata.is_some_and(|m| m.len() > 0),
_ => unreachable!(),
})
}
_ => Err(format!("unary operator expected: {op_str}")),
}
}
fn binary(lhs: &OsStr, op: &OsStr, rhs: &OsStr) -> Result<bool, String> {
let op_str = op.to_str().ok_or_else(|| {
format!("binary operator expected: {}", op.to_string_lossy())
})?;
match op_str {
"=" => Ok(lhs == rhs),
"!=" => Ok(lhs != rhs),
"-eq" | "-ne" | "-lt" | "-le" | "-gt" | "-ge" => {
let lhs = parse_int(lhs)?;
let rhs = parse_int(rhs)?;
Ok(match op_str {
"-eq" => lhs == rhs,
"-ne" => lhs != rhs,
"-lt" => lhs < rhs,
"-le" => lhs <= rhs,
"-gt" => lhs > rhs,
"-ge" => lhs >= rhs,
_ => unreachable!(),
})
}
_ => Err(format!("binary operator expected: {op_str}")),
}
}
fn parse_int(s: &OsStr) -> Result<i64, String> {
s.to_str()
.and_then(|s| s.parse::<i64>().ok())
.ok_or_else(|| {
format!("integer expression expected: {}", s.to_string_lossy())
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shell::types::KillSignal;
use std::path::PathBuf;
fn state() -> ShellState {
state_with_cwd(std::env::current_dir().unwrap())
}
fn state_with_cwd(cwd: PathBuf) -> ShellState {
ShellState::new(
Default::default(),
cwd,
Default::default(),
KillSignal::default(),
)
}
fn args(parts: &[&str]) -> Vec<OsString> {
parts.iter().map(OsString::from).collect()
}
#[test]
fn zero_args_is_false() {
assert!(!evaluate(&args(&[]), &state()).unwrap());
}
#[test]
fn single_arg_string_truthiness() {
assert!(!evaluate(&args(&[""]), &state()).unwrap());
assert!(evaluate(&args(&["x"]), &state()).unwrap());
assert!(evaluate(&args(&["0"]), &state()).unwrap());
}
#[test]
fn string_predicates() {
assert!(evaluate(&args(&["-n", "x"]), &state()).unwrap());
assert!(!evaluate(&args(&["-n", ""]), &state()).unwrap());
assert!(evaluate(&args(&["-z", ""]), &state()).unwrap());
assert!(!evaluate(&args(&["-z", "x"]), &state()).unwrap());
}
#[test]
fn string_comparison() {
assert!(evaluate(&args(&["a", "=", "a"]), &state()).unwrap());
assert!(!evaluate(&args(&["a", "=", "b"]), &state()).unwrap());
assert!(evaluate(&args(&["a", "!=", "b"]), &state()).unwrap());
assert!(!evaluate(&args(&["a", "!=", "a"]), &state()).unwrap());
}
#[test]
fn integer_comparison() {
let cases = [
(&["1", "-eq", "1"][..], true),
(&["1", "-eq", "2"][..], false),
(&["1", "-ne", "2"][..], true),
(&["1", "-lt", "2"][..], true),
(&["2", "-lt", "1"][..], false),
(&["2", "-le", "2"][..], true),
(&["3", "-gt", "2"][..], true),
(&["2", "-gt", "2"][..], false),
(&["2", "-ge", "2"][..], true),
(&["-1", "-lt", "0"][..], true),
];
for (input, expected) in cases {
assert_eq!(
evaluate(&args(input), &state()).unwrap(),
expected,
"case: {input:?}"
);
}
}
#[test]
fn integer_parse_error() {
let err = evaluate(&args(&["x", "-eq", "1"]), &state()).err().unwrap();
assert_eq!(err, "integer expression expected: x");
}
#[test]
fn negation() {
assert!(!evaluate(&args(&["!", "x"]), &state()).unwrap());
assert!(evaluate(&args(&["!", ""]), &state()).unwrap());
assert!(!evaluate(&args(&["!", "-n", "x"]), &state()).unwrap());
assert!(evaluate(&args(&["!", "-z", "x"]), &state()).unwrap());
assert!(!evaluate(&args(&["!", "1", "-eq", "1"]), &state()).unwrap());
assert!(evaluate(&args(&["!", "1", "-eq", "2"]), &state()).unwrap());
}
#[test]
fn file_predicates() {
let tmp = tempfile::tempdir().unwrap();
let file_path = tmp.path().join("a.txt");
std::fs::write(&file_path, b"hi").unwrap();
let empty_path = tmp.path().join("empty.txt");
std::fs::write(&empty_path, b"").unwrap();
let dir_path = tmp.path().join("sub");
std::fs::create_dir(&dir_path).unwrap();
let missing_path = tmp.path().join("nope");
let state = state_with_cwd(tmp.path().to_path_buf());
let cases: &[(&[&str], bool)] = &[
(&["-e", "a.txt"], true),
(&["-e", "sub"], true),
(&["-e", "nope"], false),
(&["-f", "a.txt"], true),
(&["-f", "sub"], false),
(&["-f", "nope"], false),
(&["-d", "sub"], true),
(&["-d", "a.txt"], false),
(&["-d", "nope"], false),
(&["-s", "a.txt"], true),
(&["-s", "empty.txt"], false),
(&["-s", "nope"], false),
];
for (input, expected) in cases {
assert_eq!(
evaluate(&args(input), &state).unwrap(),
*expected,
"case: {input:?}"
);
}
assert!(
evaluate(&args(&["-f", &file_path.to_string_lossy()]), &state).unwrap()
);
assert!(
!evaluate(&args(&["-e", &missing_path.to_string_lossy()]), &state)
.unwrap()
);
}
#[test]
fn syntax_errors() {
assert_eq!(
evaluate(&args(&["-x", "foo"]), &state()).err().unwrap(),
"unary operator expected: -x"
);
assert_eq!(
evaluate(&args(&["a", "+", "b"]), &state()).err().unwrap(),
"binary operator expected: +"
);
assert_eq!(
evaluate(&args(&["a", "b", "c", "d", "e"]), &state())
.err()
.unwrap(),
"too many arguments"
);
}
}