From 4cc8f820b994e67f1224720b48e9e0b02c532098 Mon Sep 17 00:00:00 2001 From: sagie gur ari Date: Wed, 5 Feb 2020 19:09:27 +0000 Subject: [PATCH] The if/else and not commands now support complex conditions --- CHANGELOG.md | 1 + README.md | 4 +- docs/_includes/content.md | 4 +- docs/sdk.md | 68 ++++- duckscript_sdk/src/lib_test.rs | 17 +- .../src/sdk/std/flowcontrol/ifelse/help.md | 18 +- duckscript_sdk/src/sdk/std/not/help.md | 50 ++- duckscript_sdk/src/utils/condition.rs | 135 +++++++- duckscript_sdk/src/utils/condition_test.rs | 287 ++++++++++++++++++ examples/if_else.ds | 4 +- .../{ => flowcontrol}/flow_control_test.ds | 0 test/std/{ => flowcontrol}/for_in_test.ds | 0 test/std/{ => flowcontrol}/goto_test.ds | 0 test/std/{ => flowcontrol}/if_else_test.ds | 16 + test/std/not_test.ds | 24 ++ 15 files changed, 581 insertions(+), 47 deletions(-) rename test/std/{ => flowcontrol}/flow_control_test.ds (100%) rename test/std/{ => flowcontrol}/for_in_test.ds (100%) rename test/std/{ => flowcontrol}/goto_test.ds (100%) rename test/std/{ => flowcontrol}/if_else_test.ds (81%) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf8104c..2a2375c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### v0.1.9 +* The if/else and not commands now support complex conditions. * Release command now support recursive option. * New map_clear command. * New map_to_properties command. diff --git a/README.md b/README.md index 9da5064..8aed200 100644 --- a/README.md +++ b/README.md @@ -348,12 +348,12 @@ end value = set false if ${value} echo should not be here -elseif true +elseif true or false echo in else if but not done yet value = set true - if not false + if not true and false echo nested if value = set "some text" diff --git a/docs/_includes/content.md b/docs/_includes/content.md index 9af612d..6e178da 100755 --- a/docs/_includes/content.md +++ b/docs/_includes/content.md @@ -304,12 +304,12 @@ end value = set false if ${value} echo should not be here -elseif true +elseif true or false echo in else if but not done yet value = set true - if not false + if not true and false echo nested if value = set "some text" diff --git a/docs/sdk.md b/docs/sdk.md index 5c9de65..b7063a0 100644 --- a/docs/sdk.md +++ b/docs/sdk.md @@ -343,7 +343,7 @@ goto ## std::If ```sh -if command|value +if [command|value|condition] # commands elseif command|value # commands @@ -363,8 +363,9 @@ if and elseif commands accept either: * A command with optional arguments and invokes it * A single value which doesn't match any known command +* A condition statement -If the value or the result of the command is one of the following: +If the result is one of the following: * No output * false (case insensitive) @@ -378,6 +379,9 @@ If a truthy (non falsy) output is found, it will invoke the commands of that cod if blocks can be nested in other if blocks (see examples). +A condition statement is made up of values, or/and keywords and '('/')' groups.
+Each must be separated with a space character. + #### Parameters * if/elseif - A command and its arguments to invoke and evaluate its output, if a single value is provided an no such command exists, it is evaluated as a value. @@ -433,6 +437,16 @@ elseif set true else echo should not be here end + +valid = set false +if true and false or true and false or ( true and true or false ) + valid = set true +end +assert ${valid} + +if true and false or true and false or ( true and true or false ) and false + assert_fail +end ``` @@ -469,7 +483,7 @@ is_defined ## std::Not ```sh -output = not command|value +output = not [command|value|condition] ``` Enables to switch falsy to true and truthy to false.
@@ -477,8 +491,9 @@ The **not** commands accept either: * A command with optional arguments and invokes it * A single value which doesn't match any known command +* A condition statement -If the value or the result of the command is one of the following: +If the result is one of the following: * No output * false (case insensitive) @@ -488,6 +503,9 @@ If the value or the result of the command is one of the following: It will return true, otherwise it will return false. +A condition statement is made up of values, or/and keywords and '('/')' groups.
+Each must be separated with a space character. + #### Parameters A command and its arguments to invoke and evaluate its output, if a single value is provided an no such command exists, it is evaluated as a value. @@ -499,19 +517,41 @@ The switched value of the input. #### Examples ```sh -# Simple example of converting true/false values -is_false = not true -echo is false: ${is_false} +fn test_not_true + value = not true -is_true = not false -echo is true: ${is_true} + assert_false ${value} +end -# Example of converting command output value -is_false = not set true -echo is false: ${is_false} +fn test_not_false + value = not false -is_true = not set false -echo is true: ${is_true} + assert ${value} +end + +fn test_not_command_true + value = not set true + + assert_false ${value} +end + +fn test_not_command_false + value = not set false + + assert ${value} +end + +fn test_not_condition_true + value = not true and false or true and false or ( true and true or false ) + + assert_false ${value} +end + +fn test_not_condition_false + value = not true and false or true and false or ( true and true or false ) and false + + assert ${value} +end ``` diff --git a/duckscript_sdk/src/lib_test.rs b/duckscript_sdk/src/lib_test.rs index e5288bf..1c284cf 100644 --- a/duckscript_sdk/src/lib_test.rs +++ b/duckscript_sdk/src/lib_test.rs @@ -14,11 +14,18 @@ fn load_valid() { let mut context = Context::new(); let result = load(&mut context.commands); - match result { - Ok(_) => (), - Err(e) => panic!("{:#?}", e), - }; - // assert!(result.is_ok()); + assert!(result.is_ok()); + + assert!(!context.commands.get_all_command_names().is_empty()); +} + +#[test] +fn test_scripts() { + let mut context = Context::new(); + let result = load(&mut context.commands); + + assert!(result.is_ok()); + assert!(!context.commands.get_all_command_names().is_empty()); let result = runner::run_script("test_directory ../test", context); diff --git a/duckscript_sdk/src/sdk/std/flowcontrol/ifelse/help.md b/duckscript_sdk/src/sdk/std/flowcontrol/ifelse/help.md index 1d319f4..ecb56ba 100644 --- a/duckscript_sdk/src/sdk/std/flowcontrol/ifelse/help.md +++ b/duckscript_sdk/src/sdk/std/flowcontrol/ifelse/help.md @@ -1,5 +1,5 @@ ```sh -if command|value +if [command|value|condition] # commands elseif command|value # commands @@ -19,8 +19,9 @@ if and elseif commands accept either: * A command with optional arguments and invokes it * A single value which doesn't match any known command +* A condition statement -If the value or the result of the command is one of the following: +If the result is one of the following: * No output * false (case insensitive) @@ -34,6 +35,9 @@ If a truthy (non falsy) output is found, it will invoke the commands of that cod if blocks can be nested in other if blocks (see examples). +A condition statement is made up of values, or/and keywords and '('/')' groups.
+Each must be separated with a space character. + #### Parameters * if/elseif - A command and its arguments to invoke and evaluate its output, if a single value is provided an no such command exists, it is evaluated as a value. @@ -89,4 +93,14 @@ elseif set true else echo should not be here end + +valid = set false +if true and false or true and false or ( true and true or false ) + valid = set true +end +assert ${valid} + +if true and false or true and false or ( true and true or false ) and false + assert_fail +end ``` diff --git a/duckscript_sdk/src/sdk/std/not/help.md b/duckscript_sdk/src/sdk/std/not/help.md index 10cde40..792b937 100644 --- a/duckscript_sdk/src/sdk/std/not/help.md +++ b/duckscript_sdk/src/sdk/std/not/help.md @@ -1,5 +1,5 @@ ```sh -output = not command|value +output = not [command|value|condition] ``` Enables to switch falsy to true and truthy to false.
@@ -7,8 +7,9 @@ The **not** commands accept either: * A command with optional arguments and invokes it * A single value which doesn't match any known command +* A condition statement -If the value or the result of the command is one of the following: +If the result is one of the following: * No output * false (case insensitive) @@ -18,6 +19,9 @@ If the value or the result of the command is one of the following: It will return true, otherwise it will return false. +A condition statement is made up of values, or/and keywords and '('/')' groups.
+Each must be separated with a space character. + #### Parameters A command and its arguments to invoke and evaluate its output, if a single value is provided an no such command exists, it is evaluated as a value. @@ -29,17 +33,39 @@ The switched value of the input. #### Examples ```sh -# Simple example of converting true/false values -is_false = not true -echo is false: ${is_false} +fn test_not_true + value = not true -is_true = not false -echo is true: ${is_true} + assert_false ${value} +end -# Example of converting command output value -is_false = not set true -echo is false: ${is_false} +fn test_not_false + value = not false -is_true = not set false -echo is true: ${is_true} + assert ${value} +end + +fn test_not_command_true + value = not set true + + assert_false ${value} +end + +fn test_not_command_false + value = not set false + + assert ${value} +end + +fn test_not_condition_true + value = not true and false or true and false or ( true and true or false ) + + assert_false ${value} +end + +fn test_not_condition_false + value = not true and false or true and false or ( true and true or false ) and false + + assert ${value} +end ``` diff --git a/duckscript_sdk/src/utils/condition.rs b/duckscript_sdk/src/utils/condition.rs index 8b46378..6dac475 100644 --- a/duckscript_sdk/src/utils/condition.rs +++ b/duckscript_sdk/src/utils/condition.rs @@ -7,6 +7,13 @@ use std::collections::HashMap; #[path = "./condition_test.rs"] mod condition_test; +enum FoundToken { + None, + And, + Or, + Value, +} + pub(crate) fn is_true(value: Option) -> bool { let failed = match value { Some(value_str) => { @@ -28,11 +35,7 @@ pub(crate) fn eval_condition( if arguments.is_empty() { Ok(is_true(None)) } else { - let eval_statement = if arguments.len() == 1 { - commands.exists(&arguments[0]) - } else { - true - }; + let eval_statement = commands.exists(&arguments[0]); if eval_statement { match eval::eval_with_error(&arguments, state, variables, commands) { @@ -45,9 +48,125 @@ pub(crate) fn eval_condition( _ => Err("Invalid condition evaluation result.".to_string()), } } else { - let passed = is_true(Some(arguments[0].to_string())); - - Ok(passed) + eval_condition_for_slice(&arguments[..]) + } + } +} + +pub(crate) fn eval_condition_for_slice(arguments: &[String]) -> Result { + if arguments.is_empty() { + Ok(is_true(None)) + } else { + let mut searching_block_end = false; + let mut start_block = 0; + let mut counter = 0; + let mut index = 0; + let mut total_evaluated = None; + let mut partial_evaluated = None; + let mut found_token = FoundToken::None; + for argument in arguments { + if argument == "(" { + searching_block_end = true; + if counter == 0 { + start_block = index + 1 + } + counter = counter + 1; + } else if argument == ")" { + counter = counter - 1; + + if counter == 0 { + searching_block_end = false; + + match eval_condition_for_slice(&arguments[start_block..index]) { + Ok(evaluated) => { + start_block = 0; + + match found_token { + FoundToken::None => { + total_evaluated = Some(evaluated); + found_token = FoundToken::Value; + } + FoundToken::And => { + partial_evaluated = Some(evaluated); + found_token = FoundToken::Value; + } + FoundToken::Or => { + partial_evaluated = + Some(evaluated || partial_evaluated.unwrap_or(false)); + found_token = FoundToken::Value; + } + FoundToken::Value => { + return Err( + format!("Unexpected value: {}", argument).to_string() + ); + } + } + } + Err(error) => return Err(error), + }; + } else if counter < 0 { + return Err("Unexpected ')'".to_string()); + } + } else if !searching_block_end { + if argument == "and" { + match found_token { + FoundToken::Value => { + found_token = FoundToken::And; + + total_evaluated = Some( + total_evaluated.unwrap_or(true) + && partial_evaluated.unwrap_or(true), + ); + partial_evaluated = None; + + if !total_evaluated.unwrap() { + return Ok(false); + } + } + _ => return Err("Unexpected 'and'".to_string()), + } + } else if argument == "or" { + match found_token { + FoundToken::Value => found_token = FoundToken::Or, + _ => return Err("Unexpected 'or'".to_string()), + } + } else { + let evaluated = is_true(Some(argument.to_string())); + + match found_token { + FoundToken::None => { + partial_evaluated = Some(evaluated); + found_token = FoundToken::Value; + } + FoundToken::And => { + partial_evaluated = Some(evaluated); + found_token = FoundToken::Value; + } + FoundToken::Or => { + partial_evaluated = + Some(evaluated || partial_evaluated.unwrap_or(false)); + found_token = FoundToken::Value; + } + FoundToken::Value => { + return Err(format!("Unexpected value: {}", argument).to_string()); + } + } + } + } + + index = index + 1; + } + + if searching_block_end { + Err("Missing ')'".to_string()) + } else { + let total_bool = if total_evaluated.is_none() && partial_evaluated.is_none() { + is_true(None) + } else { + partial_evaluated.unwrap_or(true) && total_evaluated.unwrap_or(true) + }; + + Ok(total_bool) } } } diff --git a/duckscript_sdk/src/utils/condition_test.rs b/duckscript_sdk/src/utils/condition_test.rs index f61ff7f..8b69835 100644 --- a/duckscript_sdk/src/utils/condition_test.rs +++ b/duckscript_sdk/src/utils/condition_test.rs @@ -166,3 +166,290 @@ fn eval_condition_command_error() { assert!(result.is_err()); } + +#[test] +fn eval_condition_for_slice_empty() { + let result = eval_condition_for_slice(&vec![]); + + let output = result.unwrap(); + assert!(!output); +} + +#[test] +fn eval_condition_for_slice_true() { + let result = eval_condition_for_slice(&vec!["true".to_string()]); + + let output = result.unwrap(); + assert!(output); +} + +#[test] +fn eval_condition_for_slice_false() { + let result = eval_condition_for_slice(&vec!["false".to_string()]); + + let output = result.unwrap(); + assert!(!output); +} + +#[test] +fn eval_condition_for_slice_true_and_false() { + let result = eval_condition_for_slice(&vec![ + "true".to_string(), + "and".to_string(), + "false".to_string(), + ]); + + let output = result.unwrap(); + assert!(!output); +} + +#[test] +fn eval_condition_for_slice_false_and_true() { + let result = eval_condition_for_slice(&vec![ + "false".to_string(), + "and".to_string(), + "true".to_string(), + ]); + + let output = result.unwrap(); + assert!(!output); +} + +#[test] +fn eval_condition_for_slice_true_or_false() { + let result = eval_condition_for_slice(&vec![ + "true".to_string(), + "or".to_string(), + "false".to_string(), + ]); + + let output = result.unwrap(); + assert!(output); +} + +#[test] +fn eval_condition_for_slice_false_or_true() { + let result = eval_condition_for_slice(&vec![ + "false".to_string(), + "or".to_string(), + "true".to_string(), + ]); + + let output = result.unwrap(); + assert!(output); +} + +#[test] +fn eval_condition_for_slice_complex_no_parts() { + let mut result = eval_condition_for_slice(&vec![ + "false".to_string(), + "or".to_string(), + "true".to_string(), + "or".to_string(), + "false".to_string(), + ]); + assert!(result.unwrap()); + + result = eval_condition_for_slice(&vec![ + "true".to_string(), + "and".to_string(), + "false".to_string(), + "or".to_string(), + "true".to_string(), + "and".to_string(), + "false".to_string(), + "or".to_string(), + "true".to_string(), + ]); + assert!(result.unwrap()); + + result = eval_condition_for_slice(&vec![ + "false".to_string(), + "and".to_string(), + "false".to_string(), + "or".to_string(), + "true".to_string(), + "and".to_string(), + "false".to_string(), + "or".to_string(), + "true".to_string(), + ]); + assert!(!result.unwrap()); + + result = eval_condition_for_slice(&vec![ + "true".to_string(), + "and".to_string(), + "false".to_string(), + "or".to_string(), + "true".to_string(), + "and".to_string(), + "false".to_string(), + "or".to_string(), + "false".to_string(), + ]); + assert!(!result.unwrap()); +} + +#[test] +fn eval_condition_for_slice_complex_with_parts() { + let mut result = eval_condition_for_slice(&vec![ + "(".to_string(), + "false".to_string(), + "or".to_string(), + "true".to_string(), + ")".to_string(), + ]); + assert!(result.unwrap()); + + result = eval_condition_for_slice(&vec![ + "(".to_string(), + "false".to_string(), + "or".to_string(), + "(".to_string(), + "true".to_string(), + ")".to_string(), + ")".to_string(), + ]); + assert!(result.unwrap()); + + result = eval_condition_for_slice(&vec![ + "true".to_string(), + "and".to_string(), + "(".to_string(), + "false".to_string(), + "or".to_string(), + "true".to_string(), + ")".to_string(), + "and".to_string(), + "false".to_string(), + "or".to_string(), + "false".to_string(), + ]); + assert!(!result.unwrap()); + + result = eval_condition_for_slice(&vec![ + "true".to_string(), + "and".to_string(), + "(".to_string(), + "false".to_string(), + "or".to_string(), + "true".to_string(), + "and".to_string(), + "false".to_string(), + ")".to_string(), + "or".to_string(), + "true".to_string(), + ]); + assert!(result.unwrap()); + + result = eval_condition_for_slice(&vec![ + "(".to_string(), + "(".to_string(), + "(".to_string(), + ")".to_string(), + ")".to_string(), + ")".to_string(), + ]); + assert!(!result.unwrap()); + + result = eval_condition_for_slice(&vec![ + "true".to_string(), + "and".to_string(), + "false".to_string(), + "or".to_string(), + "true".to_string(), + "and".to_string(), + "false".to_string(), + "or".to_string(), + "(".to_string(), + "true".to_string(), + "and".to_string(), + "true".to_string(), + "or".to_string(), + "false".to_string(), + ")".to_string(), + "and".to_string(), + "false".to_string(), + ]); + assert!(!result.unwrap()); + + result = eval_condition_for_slice(&vec![ + "true".to_string(), + "and".to_string(), + "false".to_string(), + "or".to_string(), + "true".to_string(), + "and".to_string(), + "false".to_string(), + "or".to_string(), + "(".to_string(), + "true".to_string(), + "and".to_string(), + "true".to_string(), + "or".to_string(), + "false".to_string(), + ")".to_string(), + ]); + assert!(result.unwrap()); +} + +#[test] +fn eval_condition_for_slice_parse_errors() { + let mut result = eval_condition_for_slice(&vec!["or".to_string()]); + assert!(result.is_err()); + + result = eval_condition_for_slice(&vec!["and".to_string()]); + assert!(result.is_err()); + + result = eval_condition_for_slice(&vec!["(".to_string()]); + assert!(result.is_err()); + + result = eval_condition_for_slice(&vec![")".to_string()]); + assert!(result.is_err()); + + result = eval_condition_for_slice(&vec!["(".to_string(), "true".to_string()]); + assert!(result.is_err()); + + result = eval_condition_for_slice(&vec![ + "false".to_string(), + "or".to_string(), + "true".to_string(), + ")".to_string(), + ]); + assert!(result.is_err()); + + result = eval_condition_for_slice(&vec!["false".to_string(), "true".to_string()]); + assert!(result.is_err()); + + result = eval_condition_for_slice(&vec![ + "true".to_string(), + "or".to_string(), + "or".to_string(), + "true".to_string(), + ]); + assert!(result.is_err()); + + result = eval_condition_for_slice(&vec![ + "true".to_string(), + "and".to_string(), + "and".to_string(), + "true".to_string(), + ]); + assert!(result.is_err()); + + result = eval_condition_for_slice(&vec![ + "true".to_string(), + "and".to_string(), + "or".to_string(), + "true".to_string(), + ]); + assert!(result.is_err()); + + result = eval_condition_for_slice(&vec![ + "true".to_string(), + "or".to_string(), + "and".to_string(), + "true".to_string(), + ]); + assert!(result.is_err()); +} diff --git a/examples/if_else.ds b/examples/if_else.ds index 75ec8fc..245a4ff 100644 --- a/examples/if_else.ds +++ b/examples/if_else.ds @@ -31,7 +31,7 @@ end if set false echo should not be here -elseif set true +elseif true or false echo in else if but not done yet if set true @@ -49,7 +49,7 @@ elseif true value = set true - if not false + if not true and false echo nested if value = set "some text" diff --git a/test/std/flow_control_test.ds b/test/std/flowcontrol/flow_control_test.ds similarity index 100% rename from test/std/flow_control_test.ds rename to test/std/flowcontrol/flow_control_test.ds diff --git a/test/std/for_in_test.ds b/test/std/flowcontrol/for_in_test.ds similarity index 100% rename from test/std/for_in_test.ds rename to test/std/flowcontrol/for_in_test.ds diff --git a/test/std/goto_test.ds b/test/std/flowcontrol/goto_test.ds similarity index 100% rename from test/std/goto_test.ds rename to test/std/flowcontrol/goto_test.ds diff --git a/test/std/if_else_test.ds b/test/std/flowcontrol/if_else_test.ds similarity index 81% rename from test/std/if_else_test.ds rename to test/std/flowcontrol/if_else_test.ds index 8509da5..ce7e339 100644 --- a/test/std/if_else_test.ds +++ b/test/std/flowcontrol/if_else_test.ds @@ -35,6 +35,22 @@ fn test_if_command_returns_true assert ${valid} end +fn test_if_condition_true + valid = set false + + if true and false or true and false or ( true and true or false ) + valid = set true + end + + assert ${valid} +end + +fn test_if_condition_false + if true and false or true and false or ( true and true or false ) and false + assert_fail + end +end + fn test_simple_else valid = set false diff --git a/test/std/not_test.ds b/test/std/not_test.ds index adbded9..e9c0c60 100644 --- a/test/std/not_test.ds +++ b/test/std/not_test.ds @@ -10,3 +10,27 @@ fn test_not_false assert ${value} end + +fn test_not_command_true + value = not set true + + assert_false ${value} +end + +fn test_not_command_false + value = not set false + + assert ${value} +end + +fn test_not_condition_true + value = not true and false or true and false or ( true and true or false ) + + assert_false ${value} +end + +fn test_not_condition_false + value = not true and false or true and false or ( true and true or false ) and false + + assert ${value} +end