diff --git a/crates/nu-command/src/conversions/into/bool.rs b/crates/nu-command/src/conversions/into/bool.rs index c1af991d95..d093e69ad8 100644 --- a/crates/nu-command/src/conversions/into/bool.rs +++ b/crates/nu-command/src/conversions/into/bool.rs @@ -1,4 +1,4 @@ -use nu_cmd_base::input_handler::{operate, CellPathOnlyArgs}; +use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::command_prelude::*; #[derive(Clone)] @@ -16,10 +16,16 @@ impl Command for SubCommand { (Type::Number, Type::Bool), (Type::String, Type::Bool), (Type::Bool, Type::Bool), + (Type::Nothing, Type::Bool), (Type::List(Box::new(Type::Any)), Type::table()), (Type::table(), Type::table()), (Type::record(), Type::record()), ]) + .switch( + "relaxed", + "Relaxes conversion to also allow null and any strings.", + None, + ) .allow_variants_without_examples(true) .rest( "rest", @@ -44,7 +50,10 @@ impl Command for SubCommand { call: &Call, input: PipelineData, ) -> Result { - into_bool(engine_state, stack, call, input) + let relaxed = call + .has_flag(engine_state, stack, "relaxed") + .unwrap_or(false); + into_bool(engine_state, stack, call, input, relaxed) } fn examples(&self) -> Vec { @@ -95,22 +104,47 @@ impl Command for SubCommand { example: "'true' | into bool", result: Some(Value::test_bool(true)), }, + Example { + description: "interpret a null as false", + example: "null | into bool --relaxed", + result: Some(Value::test_bool(false)), + }, + Example { + description: "interpret any non-false, non-zero string as true", + example: "'something' | into bool --relaxed", + result: Some(Value::test_bool(true)), + }, ] } } +struct IntoBoolCmdArgument { + cell_paths: Option>, + relaxed: bool, +} + +impl CmdArgument for IntoBoolCmdArgument { + fn take_cell_paths(&mut self) -> Option> { + self.cell_paths.take() + } +} + fn into_bool( engine_state: &EngineState, stack: &mut Stack, call: &Call, input: PipelineData, + relaxed: bool, ) -> Result { - let cell_paths: Vec = call.rest(engine_state, stack, 0)?; - let args = CellPathOnlyArgs::from(cell_paths); + let cell_paths = Some(call.rest(engine_state, stack, 0)?).filter(|v| !v.is_empty()); + let args = IntoBoolCmdArgument { + cell_paths, + relaxed, + }; operate(action, args, input, call.head, engine_state.signals()) } -fn string_to_boolean(s: &str, span: Span) -> Result { +fn strict_string_to_boolean(s: &str, span: Span) -> Result { match s.trim().to_ascii_lowercase().as_str() { "true" => Ok(true), "false" => Ok(false), @@ -132,26 +166,31 @@ fn string_to_boolean(s: &str, span: Span) -> Result { } } -fn action(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value { - match input { - Value::Bool { .. } => input.clone(), - Value::Int { val, .. } => Value::bool(*val != 0, span), - Value::Float { val, .. } => Value::bool(val.abs() >= f64::EPSILON, span), - Value::String { val, .. } => match string_to_boolean(val, span) { +fn action(input: &Value, args: &IntoBoolCmdArgument, span: Span) -> Value { + let err = || { + Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "bool, int, float or string".into(), + wrong_type: input.get_type().to_string(), + dst_span: span, + src_span: input.span(), + }, + span, + ) + }; + + match (input, args.relaxed) { + (Value::Error { .. } | Value::Bool { .. }, _) => input.clone(), + // In strict mode is this an error, while in relaxed this is just `false` + (Value::Nothing { .. }, false) => err(), + (Value::String { val, .. }, false) => match strict_string_to_boolean(val, span) { Ok(val) => Value::bool(val, span), Err(error) => Value::error(error, span), }, - // Propagate errors by explicitly matching them before the final case. - Value::Error { .. } => input.clone(), - other => Value::error( - ShellError::OnlySupportsThisInputType { - exp_input_type: "bool, int, float or string".into(), - wrong_type: other.get_type().to_string(), - dst_span: span, - src_span: other.span(), - }, - span, - ), + _ => match input.coerce_bool() { + Ok(val) => Value::bool(val, span), + Err(_) => err(), + }, } } @@ -165,4 +204,32 @@ mod test { test_examples(SubCommand {}) } + + #[test] + fn test_strict_handling() { + let span = Span::test_data(); + let args = IntoBoolCmdArgument { + cell_paths: vec![].into(), + relaxed: false, + }; + + assert!(action(&Value::test_nothing(), &args, span).is_error()); + assert!(action(&Value::test_string("abc"), &args, span).is_error()); + assert!(action(&Value::test_string("true"), &args, span).is_true()); + assert!(action(&Value::test_string("FALSE"), &args, span).is_false()); + } + + #[test] + fn test_relaxed_handling() { + let span = Span::test_data(); + let args = IntoBoolCmdArgument { + cell_paths: vec![].into(), + relaxed: true, + }; + + assert!(action(&Value::test_nothing(), &args, span).is_false()); + assert!(action(&Value::test_string("abc"), &args, span).is_true()); + assert!(action(&Value::test_string("true"), &args, span).is_true()); + assert!(action(&Value::test_string("FALSE"), &args, span).is_false()); + } } diff --git a/crates/nu-protocol/src/config/ansi_coloring.rs b/crates/nu-protocol/src/config/ansi_coloring.rs index 408eee52cc..af016923da 100644 --- a/crates/nu-protocol/src/config/ansi_coloring.rs +++ b/crates/nu-protocol/src/config/ansi_coloring.rs @@ -48,7 +48,7 @@ impl UseAnsiColoring { let env_value = |env_name| { engine_state .get_env_var_insensitive(env_name) - .and_then(Value::as_env_bool) + .and_then(|v| v.coerce_bool().ok()) .unwrap_or(false) }; @@ -61,7 +61,7 @@ impl UseAnsiColoring { } if let Some(cli_color) = engine_state.get_env_var_insensitive("clicolor") { - if let Some(cli_color) = cli_color.as_env_bool() { + if let Ok(cli_color) = cli_color.coerce_bool() { return cli_color; } } diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index e55f89d586..f3b44d93dd 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -667,7 +667,7 @@ impl Value { /// /// The following rules are used: /// - Values representing `false`: - /// - Empty strings + /// - Empty strings or strings that equal to "false" in any case /// - The number `0` (as an integer, float or string) /// - `Nothing` /// - Explicit boolean `false` @@ -677,19 +677,19 @@ impl Value { /// - Explicit boolean `true` /// /// For all other, more complex variants of [`Value`], the function cannot determine a - /// boolean representation and returns `None`. - pub fn as_env_bool(&self) -> Option { + /// boolean representation and returns `Err`. + pub fn coerce_bool(&self) -> Result { match self { - Value::Bool { val: false, .. } - | Value::Int { val: 0, .. } - | Value::Float { val: 0.0, .. } - | Value::Nothing { .. } => Some(false), - Value::String { val, .. } => match val.as_str() { - "" | "0" => Some(false), - _ => Some(true), + Value::Bool { val: false, .. } | Value::Int { val: 0, .. } | Value::Nothing { .. } => { + Ok(false) + } + Value::Float { val, .. } if val <= &f64::EPSILON => Ok(false), + Value::String { val, .. } => match val.trim().to_ascii_lowercase().as_str() { + "" | "0" | "false" => Ok(false), + _ => Ok(true), }, - Value::Bool { .. } | Value::Int { .. } | Value::Float { .. } => Some(true), - _ => None, + Value::Bool { .. } | Value::Int { .. } | Value::Float { .. } => Ok(true), + _ => self.cant_convert_to("bool"), } } @@ -3916,39 +3916,36 @@ mod tests { #[test] fn test_env_as_bool() { // explicit false values - assert_eq!(Value::test_bool(false).as_env_bool(), Some(false)); - assert_eq!(Value::test_int(0).as_env_bool(), Some(false)); - assert_eq!(Value::test_float(0.0).as_env_bool(), Some(false)); - assert_eq!(Value::test_string("").as_env_bool(), Some(false)); - assert_eq!(Value::test_string("0").as_env_bool(), Some(false)); - assert_eq!(Value::test_nothing().as_env_bool(), Some(false)); + assert_eq!(Value::test_bool(false).coerce_bool(), Ok(false)); + assert_eq!(Value::test_int(0).coerce_bool(), Ok(false)); + assert_eq!(Value::test_float(0.0).coerce_bool(), Ok(false)); + assert_eq!(Value::test_string("").coerce_bool(), Ok(false)); + assert_eq!(Value::test_string("0").coerce_bool(), Ok(false)); + assert_eq!(Value::test_nothing().coerce_bool(), Ok(false)); // explicit true values - assert_eq!(Value::test_bool(true).as_env_bool(), Some(true)); - assert_eq!(Value::test_int(1).as_env_bool(), Some(true)); - assert_eq!(Value::test_float(1.0).as_env_bool(), Some(true)); - assert_eq!(Value::test_string("1").as_env_bool(), Some(true)); + assert_eq!(Value::test_bool(true).coerce_bool(), Ok(true)); + assert_eq!(Value::test_int(1).coerce_bool(), Ok(true)); + assert_eq!(Value::test_float(1.0).coerce_bool(), Ok(true)); + assert_eq!(Value::test_string("1").coerce_bool(), Ok(true)); // implicit true values - assert_eq!(Value::test_int(42).as_env_bool(), Some(true)); - assert_eq!(Value::test_float(0.5).as_env_bool(), Some(true)); - assert_eq!(Value::test_string("not zero").as_env_bool(), Some(true)); + assert_eq!(Value::test_int(42).coerce_bool(), Ok(true)); + assert_eq!(Value::test_float(0.5).coerce_bool(), Ok(true)); + assert_eq!(Value::test_string("not zero").coerce_bool(), Ok(true)); // complex values returning None - assert_eq!(Value::test_record(Record::default()).as_env_bool(), None); - assert_eq!( - Value::test_list(vec![Value::test_int(1)]).as_env_bool(), - None - ); - assert_eq!( - Value::test_date( - chrono::DateTime::parse_from_rfc3339("2024-01-01T12:00:00+00:00").unwrap(), - ) - .as_env_bool(), - None - ); - assert_eq!(Value::test_glob("*.rs").as_env_bool(), None); - assert_eq!(Value::test_binary(vec![1, 2, 3]).as_env_bool(), None); - assert_eq!(Value::test_duration(3600).as_env_bool(), None); + assert!(Value::test_record(Record::default()).coerce_bool().is_err()); + assert!(Value::test_list(vec![Value::test_int(1)]) + .coerce_bool() + .is_err()); + assert!(Value::test_date( + chrono::DateTime::parse_from_rfc3339("2024-01-01T12:00:00+00:00").unwrap(), + ) + .coerce_bool() + .is_err()); + assert!(Value::test_glob("*.rs").coerce_bool().is_err()); + assert!(Value::test_binary(vec![1, 2, 3]).coerce_bool().is_err()); + assert!(Value::test_duration(3600).coerce_bool().is_err()); } }