Commit abdb02af authored by Fabian Würfl's avatar Fabian Würfl Committed by Michael Aaron Murphy
Browse files

Implement Exists Builtin Command (#504)

parent 5d35de2d
# Same tests as in src/builtins/exists.rs:test_evaluate_arguments()
# TODO: find a better way than to write "echo $?" between each test cases
exists
echo $?
exists foo bar
echo $?
exists --help
echo $?
exists --help unused params
echo $?
exists ""
echo $?
exists string
echo $?
exists "string with spaces"
echo $?
exists "-startswithdash"
echo $?
exists -a
echo $?
let emptyarray = []
echo @emptyarray
exists -a emptyarray
echo $?
let array = [ "element" ]
echo @array
exists -a array
echo $?
exists -a array
echo $?
exists -b
echo $?
let OLDPATH = $PATH
let PATH = testing/
echo "PATH = $PATH"
ls -1 $PATH
exists -b executable_file
echo $?
exists -b empty_file
echo $?
exists -b file_does_not_exist
echo $?
let PATH = $OLDPATH
echo "Reset PATH to old path"
exists -d
echo $?
exists -d testing/
echo $?
exists -d testing/empty_file
echo $?
exists -d does/not/exist
echo $?
exists -f
echo $?
exists -f testing/
echo $?
exists -f testing/empty_file
echo $?
exists -f does-not-exist
echo $?
fn testFunc a
echo $a
end
exists --fn testFunc
echo $?
exists -s
echo $?
let emptyvar = ""
echo "emptyvar = $emptyvar"
exists -s emptyvar
echo $?
let testvar = "foobar"
echo "testvar = $testvar"
exists -s testvar
echo $?
drop testvar
echo "testvar = $testvar"
exists -s testvar
echo $?
exists --foo
echo $?
exists -x
echo $?
1
0
NAME
exists - check whether items exist
SYNOPSIS
exists [EXPRESSION]
DESCRIPTION
Checks whether the given item exists and returns an exit status of 0 if it does, else 1.
OPTIONS
-a ARRAY
array var is not empty
-b BINARY
binary is in PATH
-d PATH
path is a directory
This is the same as test -d
-f PATH
path is a file
This is the same as test -f
--fn FUNCTION
function is defined
-s STRING
string var is not empty
STRING
string is not empty
This is the same as test -n
EXAMPLES
Test if the file exists:
exists -f FILE && echo "The FILE exists" || echo "The FILE does not exist"
Test if some-command exists in the path and is executable:
exists -b some-command && echo "some-command exists" || echo "some-command does not exist"
Test if variable exists AND is not empty
exists -s myVar && echo "myVar exists: $myVar" || echo "myVar does not exist or is empty"
NOTE: Don't use the '$' sigil, but only the name of the variable to check
Test if array exists and is not empty
exists -a myArr && echo "myArr exists: @myArr" || echo "myArr does not exist or is empty"
NOTE: Don't use the '@' sigil, but only the name of the array to check
Test if a function named 'myFunc' exists
exists --fn myFunc && myFunc || echo "No function with name myFunc found"
AUTHOR
Written by Fabian Würfl.
Heavily based on implementation of the test builtin, which was written by Michael Murph.
0
NAME
exists - check whether items exist
SYNOPSIS
exists [EXPRESSION]
DESCRIPTION
Checks whether the given item exists and returns an exit status of 0 if it does, else 1.
OPTIONS
-a ARRAY
array var is not empty
-b BINARY
binary is in PATH
-d PATH
path is a directory
This is the same as test -d
-f PATH
path is a file
This is the same as test -f
--fn FUNCTION
function is defined
-s STRING
string var is not empty
STRING
string is not empty
This is the same as test -n
EXAMPLES
Test if the file exists:
exists -f FILE && echo "The FILE exists" || echo "The FILE does not exist"
Test if some-command exists in the path and is executable:
exists -b some-command && echo "some-command exists" || echo "some-command does not exist"
Test if variable exists AND is not empty
exists -s myVar && echo "myVar exists: $myVar" || echo "myVar does not exist or is empty"
NOTE: Don't use the '$' sigil, but only the name of the variable to check
Test if array exists and is not empty
exists -a myArr && echo "myArr exists: @myArr" || echo "myArr does not exist or is empty"
NOTE: Don't use the '@' sigil, but only the name of the array to check
Test if a function named 'myFunc' exists
exists --fn myFunc && myFunc || echo "No function with name myFunc found"
AUTHOR
Written by Fabian Würfl.
Heavily based on implementation of the test builtin, which was written by Michael Murph.
0
1
0
0
0
0
1
element
0
0
0
PATH = testing/
empty_file
executable_file
file_with_text
symlink
0
1
1
Reset PATH to old path
0
0
1
1
0
1
0
1
0
0
emptyvar =
1
testvar = foobar
0
testvar =
1
0
0
......@@ -5,7 +5,7 @@ Cargo.toml
Cargo.lock Cargo.toml
Cargo.toml
Cargo.toml
examples/else_if.ion examples/fail.ion examples/fibonacci.ion examples/fn.ion examples/for.ion examples/function_piping.ion
examples/else_if.ion examples/exists.ion examples/fail.ion examples/fibonacci.ion examples/fn.ion examples/for.ion examples/function_piping.ion
one three two
three two
three two
use std::error::Error;
use std::fs;
use std::io::{self, BufWriter};
use std::os::unix::fs::{PermissionsExt};
#[cfg(test)]
use smallstring::SmallString;
#[cfg(test)]
use smallvec::SmallVec;
#[cfg(test)]
use builtins::Builtin;
#[cfg(test)]
use shell::flow_control::{Function, FunctionArgument, Statement};
use shell::Shell;
const MAN_PAGE: &'static str = r#"NAME
exists - check whether items exist
SYNOPSIS
exists [EXPRESSION]
DESCRIPTION
Checks whether the given item exists and returns an exit status of 0 if it does, else 1.
OPTIONS
-a ARRAY
array var is not empty
-b BINARY
binary is in PATH
-d PATH
path is a directory
This is the same as test -d
-f PATH
path is a file
This is the same as test -f
--fn FUNCTION
function is defined
-s STRING
string var is not empty
STRING
string is not empty
This is the same as test -n
EXAMPLES
Test if the file exists:
exists -f FILE && echo "The FILE exists" || echo "The FILE does not exist"
Test if some-command exists in the path and is executable:
exists -b some-command && echo "some-command exists" || echo "some-command does not exist"
Test if variable exists AND is not empty
exists -s myVar && echo "myVar exists: $myVar" || echo "myVar does not exist or is empty"
NOTE: Don't use the '$' sigil, but only the name of the variable to check
Test if array exists and is not empty
exists -a myArr && echo "myArr exists: @myArr" || echo "myArr does not exist or is empty"
NOTE: Don't use the '@' sigil, but only the name of the array to check
Test if a function named 'myFunc' exists
exists --fn myFunc && myFunc || echo "No function with name myFunc found"
AUTHOR
Written by Fabian Würfl.
Heavily based on implementation of the test builtin, which was written by Michael Murph.
"#; /* @MANEND */
pub fn exists(args: &[&str], shell: &Shell) -> Result<bool, String> {
let stdout = io::stdout();
let mut buffer = BufWriter::new(stdout.lock());
let arguments = &args[1..];
evaluate_arguments(arguments, &mut buffer, shell)
}
fn evaluate_arguments<W: io::Write>(arguments: &[&str], buffer: &mut W, shell: &Shell) -> Result<bool, String> {
match arguments.first() {
Some(&"--help") => {
// not handled by the second case, so that we don't have to pass the buffer around
buffer.write_all(MAN_PAGE.as_bytes()).map_err(|x| {
x.description().to_owned()
})?;
buffer.flush().map_err(|x| x.description().to_owned())?;
Ok(true)
}
Some(&s) if s.starts_with("--") => {
let (_, option) = s.split_at(2);
// If no argument was given, return `SUCCESS`, as this means a string starting
// with a dash was given
arguments.get(1).map_or(Ok(true), {
|arg|
// Match the correct function to the associated flag
Ok(match_option_argument(option, arg, shell))
})
}
Some(&s) if s.starts_with("-") => {
// Access the second character in the flag string: this will be type of the flag.
// If no flag was given, return `SUCCESS`, as this means a string with value "-" was
// checked.
s.chars().nth(1).map_or(Ok(true), |flag| {
// If no argument was given, return `SUCCESS`, as this means a string starting
// with a dash was given
arguments.get(1).map_or(Ok(true), {
|arg|
// Match the correct function to the associated flag
Ok(match_flag_argument(flag, arg, shell))
})
})
}
Some(string) => {
Ok(string_is_nonzero(string))
}
None => Ok(false),
}
}
/// Matches flag arguments to their respective functionaity when the `-` character is detected.
fn match_flag_argument(flag: char, argument: &str, shell: &Shell) -> bool {
match flag {
'a' => array_var_is_not_empty(argument, shell),
'b' => binary_is_in_path(argument, shell),
'd' => path_is_directory(argument),
'f' => path_is_file(argument),
's' => string_var_is_not_empty(argument, shell),
_ => false,
}
}
// Matches option arguments to their respective functionality
fn match_option_argument(option: &str, argument: &str, shell: &Shell) -> bool {
match option {
"fn" => function_is_defined(argument, &shell),
_ => false,
}
}
/// Returns true if the file is a regular file
fn path_is_file(filepath: &str) -> bool {
fs::metadata(filepath).ok().map_or(false, |metadata| {
metadata.file_type().is_file()
})
}
/// Returns true if the file is a directory
fn path_is_directory(filepath: &str) -> bool {
fs::metadata(filepath).ok().map_or(false, |metadata| {
metadata.file_type().is_dir()
})
}
/// Returns true if the binary is found in path (and is executable)
fn binary_is_in_path(binaryname: &str, shell: &Shell) -> bool {
// TODO: Maybe this function should reflect the logic for spawning new processes
// TODO: Right now they use an entirely different logic which means that it *might* be possible
// TODO: that `exists` reports a binary to be in the path, while the shell cannot find it or
// TODO: vice-versa
if let Some(path) = shell.variables.get_var("PATH") {
for dir in path.split(":") {
let fname = format!("{}/{}", dir, binaryname);
if let Ok(metadata) = fs::metadata(&fname) {
if metadata.is_file() && file_has_execute_permission(&fname) {
return true;
}
}
}
};
false
}
/// Returns true if the file has execute permissions. This function is rather low level because
/// Rust currently does not have a higher level abstraction for obtaining non-standard file modes.
/// To extract the permissions from the mode, the bitwise AND operator will be used and compared
/// with the respective execute bits.
/// Note: This function is 1:1 the same as src/builtins/test.rs:file_has_execute_permission
/// If you change the following function, please also update the one in src/builtins/test.rs
fn file_has_execute_permission(filepath: &str) -> bool {
const USER: u32 = 0b1000000;
const GROUP: u32 = 0b1000;
const GUEST: u32 = 0b1;
// Collect the mode of permissions for the file
fs::metadata(filepath).map(|metadata| metadata.permissions().mode()).ok()
// If the mode is equal to any of the above, return `SUCCESS`
.map_or(false, |mode| mode & (USER + GROUP + GUEST) != 0)
}
/// Returns true if the string is not empty
fn string_is_nonzero(string: &str) -> bool { !string.is_empty() }
/// Returns true if the variable is an array and the array is not empty
fn array_var_is_not_empty(arrayvar: &str, shell: &Shell) -> bool {
match shell.variables.get_array(arrayvar) {
Some(array) => !array.is_empty(),
None => false
}
}
/// Returns true if the variable is a string and the string is not empty
fn string_var_is_not_empty(stringvar: &str, shell: &Shell) -> bool {
match shell.variables.get_var(stringvar) {
Some(string) => !string.is_empty(),
None => false
}
}
/// Returns true if a function with the given name is defined
fn function_is_defined(function: &str, shell: &Shell) -> bool {
match shell.functions.get(function) {
Some(_) => true,
None => false
}
}
#[test]
fn test_evaluate_arguments() {
let builtins = Builtin::map();
let mut shell = Shell::new(&builtins);
let mut sink = BufWriter::new(io::sink());
// assert_eq!(evaluate_arguments(&[], &mut sink, &shell), Ok(false));
// no parameters
assert_eq!(evaluate_arguments(&[], &mut sink, &shell), Ok(false));
// multiple arguments
// ignores all but the first argument
assert_eq!(evaluate_arguments(&["foo", "bar"], &mut sink, &shell), Ok(true));
// check whether --help returns SUCCESS
assert_eq!(evaluate_arguments(&["--help"], &mut sink, &shell), Ok(true));
assert_eq!(evaluate_arguments(&["--help", "unused", "params"], &mut sink, &shell), Ok(true));
// check `exists STRING`
assert_eq!(evaluate_arguments(&[""], &mut sink, &shell), Ok(false));
assert_eq!(evaluate_arguments(&["string"], &mut sink, &shell), Ok(true));
assert_eq!(evaluate_arguments(&["string with space"], &mut sink, &shell), Ok(true));
assert_eq!(evaluate_arguments(&["-startswithdash"], &mut sink, &shell), Ok(true));
// check `exists -a`
// no argument means we treat it as a string
assert_eq!(evaluate_arguments(&["-a"], &mut sink, &shell), Ok(true));
shell.variables.set_array("emptyarray", SmallVec::from_vec(Vec::new()));
assert_eq!(evaluate_arguments(&["-a", "emptyarray"], &mut sink, &shell), Ok(false));
let mut vec = Vec::new();
vec.push("element".to_owned());
shell.variables.set_array("array", SmallVec::from_vec(vec));
assert_eq!(evaluate_arguments(&["-a", "array"], &mut sink, &shell), Ok(true));
shell.variables.unset_array("array");
assert_eq!(evaluate_arguments(&["-a", "array"], &mut sink, &shell), Ok(false));
// check `exists -b`
// TODO: see test_binary_is_in_path()
// no argument means we treat it as a string
assert_eq!(evaluate_arguments(&["-b"], &mut sink, &shell), Ok(true));
let oldpath = shell.variables.get_var("PATH").unwrap_or("/usr/bin".to_owned());
shell.variables.set_var("PATH", "testing/");
assert_eq!(evaluate_arguments(&["-b", "executable_file"], &mut sink, &shell), Ok(true));
assert_eq!(evaluate_arguments(&["-b", "empty_file"], &mut sink, &shell), Ok(false));
assert_eq!(evaluate_arguments(&["-b", "file_does_not_exist"], &mut sink, &shell), Ok(false));
// restore original PATH. Not necessary for the currently defined test cases but this might
// change in the future? Better safe than sorry!
shell.variables.set_var("PATH", &oldpath);
// check `exists -d`
// no argument means we treat it as a string
assert_eq!(evaluate_arguments(&["-d"], &mut sink, &shell), Ok(true));
assert_eq!(evaluate_arguments(&["-d", "testing/"], &mut sink, &shell), Ok(true));
assert_eq!(evaluate_arguments(&["-d", "testing/empty_file"], &mut sink, &shell), Ok(false));
assert_eq!(evaluate_arguments(&["-d", "does/not/exist/"], &mut sink, &shell), Ok(false));
// check `exists -f`
// no argument means we treat it as a string
assert_eq!(evaluate_arguments(&["-f"], &mut sink, &shell), Ok(true));
assert_eq!(evaluate_arguments(&["-f", "testing/"], &mut sink, &shell), Ok(false));
assert_eq!(evaluate_arguments(&["-f", "testing/empty_file"], &mut sink, &shell), Ok(true));
assert_eq!(evaluate_arguments(&["-f", "does-not-exist"], &mut sink, &shell), Ok(false));
// check `exists -s`
// no argument means we treat it as a string
assert_eq!(evaluate_arguments(&["-s"], &mut sink, &shell), Ok(true));
shell.variables.set_var("emptyvar", "");
assert_eq!(evaluate_arguments(&["-s", "emptyvar"], &mut sink, &shell), Ok(false));
shell.variables.set_var("testvar", "foobar");
assert_eq!(evaluate_arguments(&["-s", "testvar"], &mut sink, &shell), Ok(true));
shell.variables.unset_var("testvar");
assert_eq!(evaluate_arguments(&["-s", "testvar"], &mut sink, &shell), Ok(false));
// also check that it doesn't trigger on arrays
let mut vec = Vec::new();
vec.push("element".to_owned());
shell.variables.unset_var("array");
shell.variables.set_array("array", SmallVec::from_vec(vec));
assert_eq!(evaluate_arguments(&["-s", "array"], &mut sink, &shell), Ok(false));
// check `exists --fn`
let name_str = "test_function";
let name = SmallString::from_str(name_str);
let mut args = Vec::new();
args.push(FunctionArgument::Untyped("testy".to_owned()));
let mut statements = Vec::new();
statements.push(Statement::End);
let description = "description".to_owned();
shell.functions.insert(
name.clone(),
Function {
name: name,
args: args,
statements: statements,
description: description,
},
);
assert_eq!(evaluate_arguments(&["--fn", name_str], &mut sink, &shell), Ok(true));
shell.functions.remove(name_str);
assert_eq!(evaluate_arguments(&["--fn", name_str], &mut sink, &shell), Ok(false));
// check invalid flags / parameters (should all be treated as strings and therefore succeed)
assert_eq!(evaluate_arguments(&["--foo"], &mut sink, &shell), Ok(true));
assert_eq!(evaluate_arguments(&["-x"], &mut sink, &shell), Ok(true));
}
#[test]
fn test_match_flag_argument() {
let builtins = Builtin::map();
let shell = Shell::new(&builtins);
// we don't really care about the passed values, as long as both sited return the same value
assert_eq!(match_flag_argument('a', "ARRAY", &shell), array_var_is_not_empty("ARRAY", &shell));
assert_eq!(match_flag_argument('b', "binary", &shell), binary_is_in_path("binary", &shell));
assert_eq!(match_flag_argument('d', "path", &shell), path_is_directory("path"));
assert_eq!(match_flag_argument('f', "file", &shell), path_is_file("file"));
assert_eq!(match_flag_argument('s', "STR", &shell), string_var_is_not_empty("STR", &shell));
// Any flag which is not implemented
assert_eq!(match_flag_argument('x', "ARG", &shell), false);
}
#[test]
fn test_match_option_argument() {
let builtins = Builtin::map();
let shell = Shell::new(&builtins);
// we don't really care about the passed values, as long as both sited return the same value
assert_eq!(match_option_argument("fn", "FUN", &shell), array_var_is_not_empty("FUN", &shell));
// Any option which is not implemented
assert_eq!(match_option_argument("foo", "ARG", &shell), false);
}
#[test]
fn test_path_is_file() {
assert_eq!(path_is_file("testing/empty_file"), true);
assert_eq!(path_is_file("this-does-not-exist"), false);
}
#[test]
fn test_path_is_directory() {
assert_eq!(path_is_directory("testing"), true);
assert_eq!(path_is_directory("testing/empty_file"), false);
}
#[test]