diff --git a/crates/nu-cli/src/menus/help_completions.rs b/crates/nu-cli/src/menus/help_completions.rs index a2deecf65..6b28262c1 100644 --- a/crates/nu-cli/src/menus/help_completions.rs +++ b/crates/nu-cli/src/menus/help_completions.rs @@ -17,7 +17,7 @@ impl NuHelpCompleter { //Vec<(Signature, Vec, bool, bool)> { let mut commands = full_commands .iter() - .filter(|(sig, _, _, _)| { + .filter(|(sig, _, _, _, _)| { sig.name.to_lowercase().contains(&line.to_lowercase()) || sig.usage.to_lowercase().contains(&line.to_lowercase()) || sig @@ -31,7 +31,7 @@ impl NuHelpCompleter { }) .collect::>(); - commands.sort_by(|(a, _, _, _), (b, _, _, _)| { + commands.sort_by(|(a, _, _, _, _), (b, _, _, _, _)| { let a_distance = levenshtein_distance(line, &a.name); let b_distance = levenshtein_distance(line, &b.name); a_distance.cmp(&b_distance) @@ -39,7 +39,7 @@ impl NuHelpCompleter { commands .into_iter() - .map(|(sig, examples, _, _)| { + .map(|(sig, examples, _, _, _)| { let mut long_desc = String::new(); let usage = &sig.usage; diff --git a/crates/nu-command/src/bits/bits_.rs b/crates/nu-command/src/bits/bits_.rs index 1fda4f0d1..ad6509c02 100644 --- a/crates/nu-command/src/bits/bits_.rs +++ b/crates/nu-command/src/bits/bits_.rs @@ -29,7 +29,13 @@ impl Command for Bits { _input: PipelineData, ) -> Result { Ok(Value::String { - val: get_full_help(&Bits.signature(), &Bits.examples(), engine_state, stack), + val: get_full_help( + &Bits.signature(), + &Bits.examples(), + engine_state, + stack, + self.is_parser_keyword(), + ), span: call.head, } .into_pipeline_data()) diff --git a/crates/nu-command/src/bytes/bytes_.rs b/crates/nu-command/src/bytes/bytes_.rs index 0e277e825..8725dafa0 100644 --- a/crates/nu-command/src/bytes/bytes_.rs +++ b/crates/nu-command/src/bytes/bytes_.rs @@ -29,7 +29,13 @@ impl Command for Bytes { _input: PipelineData, ) -> Result { Ok(Value::String { - val: get_full_help(&Bytes.signature(), &Bytes.examples(), engine_state, stack), + val: get_full_help( + &Bytes.signature(), + &Bytes.examples(), + engine_state, + stack, + self.is_parser_keyword(), + ), span: call.head, } .into_pipeline_data()) diff --git a/crates/nu-command/src/conversions/into/command.rs b/crates/nu-command/src/conversions/into/command.rs index 0c82ffb6a..2917f1249 100644 --- a/crates/nu-command/src/conversions/into/command.rs +++ b/crates/nu-command/src/conversions/into/command.rs @@ -29,7 +29,13 @@ impl Command for Into { _input: PipelineData, ) -> Result { Ok(Value::String { - val: get_full_help(&Into.signature(), &[], engine_state, stack), + val: get_full_help( + &Into.signature(), + &[], + engine_state, + stack, + self.is_parser_keyword(), + ), span: call.head, } .into_pipeline_data()) diff --git a/crates/nu-command/src/core_commands/export.rs b/crates/nu-command/src/core_commands/export.rs index f5e2c40e0..d0972b188 100644 --- a/crates/nu-command/src/core_commands/export.rs +++ b/crates/nu-command/src/core_commands/export.rs @@ -45,6 +45,7 @@ impl Command for ExportCommand { &ExportCommand.examples(), engine_state, stack, + self.is_parser_keyword(), ), span: call.head, } diff --git a/crates/nu-command/src/core_commands/help.rs b/crates/nu-command/src/core_commands/help.rs index 25ab9fda3..926af2605 100644 --- a/crates/nu-command/src/core_commands/help.rs +++ b/crates/nu-command/src/core_commands/help.rs @@ -1,5 +1,4 @@ use fancy_regex::Regex; -use itertools::Itertools; use nu_ansi_term::{ Color::{Default, Red, White}, Style, @@ -105,6 +104,7 @@ fn help( let mut vals = vec![]; let decl = engine_state.get_decl(decl_id); let sig = decl.signature().update_from_command(decl.borrow()); + let signatures = sig.to_string(); let key = sig.name; let usage = sig.usage; let search_terms = sig.search_terms; @@ -154,11 +154,11 @@ fn help( cols.push("signatures".into()); vals.push(Value::String { - val: sig - .input_output_types - .iter() - .map(|(i, o)| format!("{:?} => {:?}", i.to_shape(), o.to_shape())) - .join("\n"), + val: if decl.is_parser_keyword() { + "".to_string() + } else { + signatures + }, span: head, }); @@ -219,6 +219,7 @@ fn help( let decl = engine_state.get_decl(decl_id); let sig = decl.signature().update_from_command(decl.borrow()); + let signatures = sig.to_string(); let key = sig.name; let usage = sig.usage; let search_terms = sig.search_terms; @@ -249,11 +250,11 @@ fn help( cols.push("signatures".into()); vals.push(Value::String { - val: sig - .input_output_types - .iter() - .map(|(i, o)| format!("{:?} => {:?}", i.to_shape(), o.to_shape())) - .join("\n"), + val: if decl.is_parser_keyword() { + "".to_string() + } else { + signatures + }, span: head, }); @@ -290,9 +291,9 @@ fn help( let output = engine_state .get_signatures_with_examples(false) .iter() - .filter(|(signature, _, _, _)| signature.name == name) - .map(|(signature, examples, _, _)| { - get_full_help(signature, examples, engine_state, stack) + .filter(|(signature, _, _, _, _)| signature.name == name) + .map(|(signature, examples, _, _, is_parser_keyword)| { + get_full_help(signature, examples, engine_state, stack, *is_parser_keyword) }) .collect::>(); diff --git a/crates/nu-command/src/core_commands/overlay/command.rs b/crates/nu-command/src/core_commands/overlay/command.rs index 6e77a7509..0679edf69 100644 --- a/crates/nu-command/src/core_commands/overlay/command.rs +++ b/crates/nu-command/src/core_commands/overlay/command.rs @@ -38,7 +38,13 @@ impl Command for Overlay { _input: PipelineData, ) -> Result { Ok(Value::String { - val: get_full_help(&Overlay.signature(), &[], engine_state, stack), + val: get_full_help( + &Overlay.signature(), + &[], + engine_state, + stack, + self.is_parser_keyword(), + ), span: call.head, } .into_pipeline_data()) diff --git a/crates/nu-command/src/date/date_.rs b/crates/nu-command/src/date/date_.rs index d0750c61d..a5c321053 100644 --- a/crates/nu-command/src/date/date_.rs +++ b/crates/nu-command/src/date/date_.rs @@ -53,7 +53,13 @@ fn date( let head = call.head; Ok(Value::String { - val: get_full_help(&Date.signature(), &Date.examples(), engine_state, stack), + val: get_full_help( + &Date.signature(), + &Date.examples(), + engine_state, + stack, + false, + ), span: head, } .into_pipeline_data()) diff --git a/crates/nu-command/src/env/config/config_.rs b/crates/nu-command/src/env/config/config_.rs index d1f41f828..ce1110bc8 100644 --- a/crates/nu-command/src/env/config/config_.rs +++ b/crates/nu-command/src/env/config/config_.rs @@ -34,6 +34,7 @@ impl Command for ConfigMeta { &ConfigMeta.examples(), engine_state, stack, + self.is_parser_keyword(), ), span: call.head, } diff --git a/crates/nu-command/src/filters/roll/roll_.rs b/crates/nu-command/src/filters/roll/roll_.rs index 1fc34a560..72a47d88e 100644 --- a/crates/nu-command/src/filters/roll/roll_.rs +++ b/crates/nu-command/src/filters/roll/roll_.rs @@ -31,7 +31,13 @@ impl Command for Roll { _input: PipelineData, ) -> Result { Ok(Value::String { - val: get_full_help(&Roll.signature(), &Roll.examples(), engine_state, stack), + val: get_full_help( + &Roll.signature(), + &Roll.examples(), + engine_state, + stack, + self.is_parser_keyword(), + ), span: call.head, } .into_pipeline_data()) diff --git a/crates/nu-command/src/formats/from/command.rs b/crates/nu-command/src/formats/from/command.rs index 125b9bef0..2c1ae5562 100644 --- a/crates/nu-command/src/formats/from/command.rs +++ b/crates/nu-command/src/formats/from/command.rs @@ -27,7 +27,13 @@ impl Command for From { _input: PipelineData, ) -> Result { Ok(Value::String { - val: get_full_help(&From.signature(), &From.examples(), engine_state, stack), + val: get_full_help( + &From.signature(), + &From.examples(), + engine_state, + stack, + self.is_parser_keyword(), + ), span: call.head, } .into_pipeline_data()) diff --git a/crates/nu-command/src/formats/to/command.rs b/crates/nu-command/src/formats/to/command.rs index 82092dfb5..cc7fd04d3 100644 --- a/crates/nu-command/src/formats/to/command.rs +++ b/crates/nu-command/src/formats/to/command.rs @@ -27,7 +27,13 @@ impl Command for To { _input: PipelineData, ) -> Result { Ok(Value::String { - val: get_full_help(&To.signature(), &To.examples(), engine_state, stack), + val: get_full_help( + &To.signature(), + &To.examples(), + engine_state, + stack, + self.is_parser_keyword(), + ), span: call.head, } .into_pipeline_data()) diff --git a/crates/nu-command/src/hash/hash_.rs b/crates/nu-command/src/hash/hash_.rs index 8a3b1d193..380e20bd1 100644 --- a/crates/nu-command/src/hash/hash_.rs +++ b/crates/nu-command/src/hash/hash_.rs @@ -27,7 +27,13 @@ impl Command for Hash { _input: PipelineData, ) -> Result { Ok(Value::String { - val: get_full_help(&Self.signature(), &Self.examples(), engine_state, stack), + val: get_full_help( + &Self.signature(), + &Self.examples(), + engine_state, + stack, + self.is_parser_keyword(), + ), span: call.head, } .into_pipeline_data()) diff --git a/crates/nu-command/src/math/math_.rs b/crates/nu-command/src/math/math_.rs index ae2e13c18..0d34e2cea 100644 --- a/crates/nu-command/src/math/math_.rs +++ b/crates/nu-command/src/math/math_.rs @@ -34,6 +34,7 @@ impl Command for MathCommand { &MathCommand.examples(), engine_state, stack, + self.is_parser_keyword(), ), span: call.head, } diff --git a/crates/nu-command/src/network/url/url_.rs b/crates/nu-command/src/network/url/url_.rs index 53dc12708..6b572fc74 100644 --- a/crates/nu-command/src/network/url/url_.rs +++ b/crates/nu-command/src/network/url/url_.rs @@ -35,7 +35,13 @@ impl Command for Url { _input: PipelineData, ) -> Result { Ok(Value::String { - val: get_full_help(&Url.signature(), &Url.examples(), engine_state, stack), + val: get_full_help( + &Url.signature(), + &Url.examples(), + engine_state, + stack, + self.is_parser_keyword(), + ), span: call.head, } .into_pipeline_data()) diff --git a/crates/nu-command/src/path/path_.rs b/crates/nu-command/src/path/path_.rs index 5fe335a0b..e5d0d2447 100644 --- a/crates/nu-command/src/path/path_.rs +++ b/crates/nu-command/src/path/path_.rs @@ -49,6 +49,7 @@ the path literal."# &PathCommand.examples(), engine_state, stack, + self.is_parser_keyword(), ), span: call.head, } diff --git a/crates/nu-command/src/platform/reedline_commands/keybindings.rs b/crates/nu-command/src/platform/reedline_commands/keybindings.rs index 7cdfeddc7..eed87ebd7 100644 --- a/crates/nu-command/src/platform/reedline_commands/keybindings.rs +++ b/crates/nu-command/src/platform/reedline_commands/keybindings.rs @@ -38,6 +38,7 @@ impl Command for Keybindings { &Keybindings.examples(), engine_state, stack, + self.is_parser_keyword(), ), span: call.head, } diff --git a/crates/nu-command/src/random/random_.rs b/crates/nu-command/src/random/random_.rs index ad2cdd1e8..fcef5f491 100644 --- a/crates/nu-command/src/random/random_.rs +++ b/crates/nu-command/src/random/random_.rs @@ -38,6 +38,7 @@ impl Command for RandomCommand { &RandomCommand.examples(), engine_state, stack, + self.is_parser_keyword(), ), span: call.head, } diff --git a/crates/nu-command/src/strings/split/command.rs b/crates/nu-command/src/strings/split/command.rs index 30d4c8690..88d338599 100644 --- a/crates/nu-command/src/strings/split/command.rs +++ b/crates/nu-command/src/strings/split/command.rs @@ -34,6 +34,7 @@ impl Command for SplitCommand { &SplitCommand.examples(), engine_state, stack, + self.is_parser_keyword(), ), span: call.head, } diff --git a/crates/nu-command/src/strings/str_/case/str_.rs b/crates/nu-command/src/strings/str_/case/str_.rs index 72198a5b4..b808b4b4d 100644 --- a/crates/nu-command/src/strings/str_/case/str_.rs +++ b/crates/nu-command/src/strings/str_/case/str_.rs @@ -29,7 +29,13 @@ impl Command for Str { _input: PipelineData, ) -> Result { Ok(Value::String { - val: get_full_help(&Str.signature(), &Str.examples(), engine_state, stack), + val: get_full_help( + &Str.signature(), + &Str.examples(), + engine_state, + stack, + self.is_parser_keyword(), + ), span: call.head, } .into_pipeline_data()) diff --git a/crates/nu-command/tests/commands/help.rs b/crates/nu-command/tests/commands/help.rs index 01b3d5552..679c124d9 100644 --- a/crates/nu-command/tests/commands/help.rs +++ b/crates/nu-command/tests/commands/help.rs @@ -14,3 +14,15 @@ fn help_commands_length() { let is_positive = output_int.is_positive(); assert!(is_positive); } + +#[test] +fn help_shows_signature() { + let actual = nu!(cwd: ".", pipeline("help str distance")); + assert!(actual + .out + .contains(" | str distance -> ")); + + // don't show signature for parser keyword + let actual = nu!(cwd: ".", pipeline("help alias")); + assert!(!actual.out.contains("Signatures")); +} diff --git a/crates/nu-command/tests/commands/reject.rs b/crates/nu-command/tests/commands/reject.rs index 5d3519037..466ca86df 100644 --- a/crates/nu-command/tests/commands/reject.rs +++ b/crates/nu-command/tests/commands/reject.rs @@ -92,7 +92,7 @@ fn reject_record_from_raw_eval() { ) ); - assert!(actual.out.contains("record<>")); + assert!(actual.out.contains("record")); } #[test] diff --git a/crates/nu-engine/src/documentation.rs b/crates/nu-engine/src/documentation.rs index 26c5223d2..c736521dd 100644 --- a/crates/nu-engine/src/documentation.rs +++ b/crates/nu-engine/src/documentation.rs @@ -10,6 +10,7 @@ pub fn get_full_help( examples: &[Example], engine_state: &EngineState, stack: &mut Stack, + is_parser_keyword: bool, ) -> String { let config = engine_state.get_config(); let doc_config = DocumentationConfig { @@ -17,7 +18,14 @@ pub fn get_full_help( no_color: !config.use_ansi_coloring, brief: false, }; - get_documentation(sig, examples, engine_state, stack, &doc_config) + get_documentation( + sig, + examples, + engine_state, + stack, + &doc_config, + is_parser_keyword, + ) } #[derive(Default)] @@ -34,6 +42,7 @@ fn get_documentation( engine_state: &EngineState, stack: &mut Stack, config: &DocumentationConfig, + is_parser_keyword: bool, ) -> String { // Create ansi colors const G: &str = "\x1b[32m"; // green @@ -89,6 +98,18 @@ fn get_documentation( long_desc.push_str(&get_flags_section(sig)) } + if !is_parser_keyword && !sig.input_output_types.is_empty() { + if sig.operates_on_cell_paths() { + let _ = writeln!( + long_desc, + "\n{}Signatures(Cell paths are supported){}:\n{}", + G, RESET, sig + ); + } else { + let _ = writeln!(long_desc, "\n{}Signatures{}:\n{}", G, RESET, sig); + } + } + if !sig.required_positional.is_empty() || !sig.optional_positional.is_empty() || sig.rest_positional.is_some() diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index a57255e5e..87c8ff302 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -43,7 +43,13 @@ pub fn eval_call( signature.usage = decl.usage().to_string(); signature.extra_usage = decl.extra_usage().to_string(); - let full_help = get_full_help(&signature, &decl.examples(), engine_state, caller_stack); + let full_help = get_full_help( + &signature, + &decl.examples(), + engine_state, + caller_stack, + decl.is_parser_keyword(), + ); Ok(Value::String { val: full_help, span: call.head, diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index a41cf3295..876855674 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -700,7 +700,7 @@ impl EngineState { pub fn get_signatures_with_examples( &self, include_hidden: bool, - ) -> Vec<(Signature, Vec, bool, bool)> { + ) -> Vec<(Signature, Vec, bool, bool, bool)> { self.get_decl_ids_sorted(include_hidden) .map(|id| { let decl = self.get_decl(id); @@ -712,6 +712,7 @@ impl EngineState { decl.examples(), decl.is_plugin().is_some(), decl.get_block_id().is_some(), + decl.is_parser_keyword(), ) }) .collect() diff --git a/crates/nu-protocol/src/signature.rs b/crates/nu-protocol/src/signature.rs index de2a0a084..4d919a327 100644 --- a/crates/nu-protocol/src/signature.rs +++ b/crates/nu-protocol/src/signature.rs @@ -122,6 +122,72 @@ pub struct Signature { pub category: Category, } +/// Fromat argumet type for user readable output. +/// +/// In general: +/// if argument type is a simple type(like string), we'll wrapped with `<>`, the result will be `` +/// if argument type is already contains `<>`, like `list`, the result will be `list`. +fn fmt_type(arg_type: &Type, optional: bool) -> String { + let arg_type = arg_type.to_string(); + if arg_type.contains('<') && arg_type.contains('>') { + if optional { + format!("{arg_type}?") + } else { + arg_type + } + } else if optional { + format!("<{arg_type}?>") + } else { + format!("<{arg_type}>") + } +} + +// in general, a commands signature should looks like this: +// +// | , => string +// +// More detail explaination: +// the first one is the input from previous command, aka, pipeline input +// then followed by `|`, then positional arguments type +// then optional arguments type, which ends with `?` +// Then followed by `->` +// Finally output type. +// +// If a command contains multiple input/output types, separate them in different lines. +impl std::fmt::Display for Signature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut args = self + .required_positional + .iter() + .map(|p| fmt_type(&p.shape.to_type(), false)) + .collect::>(); + args.append( + &mut self + .optional_positional + .iter() + .map(|p| fmt_type(&p.shape.to_type(), true)) + .collect::>(), + ); + let args = args.join(", "); + + let mut signatures = vec![]; + for (input_type, output_type) in self.input_output_types.iter() { + // ident with two spaces for user friendly output. + let input_type = fmt_type(input_type, false); + let output_type = fmt_type(output_type, false); + if args.is_empty() { + signatures.push(format!(" {input_type} | {} -> {output_type}", self.name)) + } else { + signatures.push(format!( + " {input_type} | {} {args} -> {output_type}", + self.name + )) + } + } + write!(f, "{}", signatures.join("\n")) + } +} + impl PartialEq for Signature { fn eq(&self, other: &Self) -> bool { self.name == other.name diff --git a/crates/nu-protocol/src/ty.rs b/crates/nu-protocol/src/ty.rs index f00675f97..4cc478bfa 100644 --- a/crates/nu-protocol/src/ty.rs +++ b/crates/nu-protocol/src/ty.rs @@ -107,24 +107,36 @@ impl Display for Type { Type::Float => write!(f, "float"), Type::Int => write!(f, "int"), Type::Range => write!(f, "range"), - Type::Record(fields) => write!( - f, - "record<{}>", - fields - .iter() - .map(|(x, y)| format!("{}: {}", x, y)) - .collect::>() - .join(", "), - ), - Type::Table(columns) => write!( - f, - "table<{}>", - columns - .iter() - .map(|(x, y)| format!("{}: {}", x, y)) - .collect::>() - .join(", ") - ), + Type::Record(fields) => { + if fields.is_empty() { + write!(f, "record") + } else { + write!( + f, + "record<{}>", + fields + .iter() + .map(|(x, y)| format!("{}: {}", x, y)) + .collect::>() + .join(", "), + ) + } + } + Type::Table(columns) => { + if columns.is_empty() { + write!(f, "table") + } else { + write!( + f, + "table<{}>", + columns + .iter() + .map(|(x, y)| format!("{}: {}", x, y)) + .collect::>() + .join(", ") + ) + } + } Type::List(l) => write!(f, "list<{}>", l), Type::Nothing => write!(f, "nothing"), Type::Number => write!(f, "number"), diff --git a/src/main.rs b/src/main.rs index e853352e6..32ee1de13 100644 --- a/src/main.rs +++ b/src/main.rs @@ -563,8 +563,13 @@ fn parse_commandline_args( let help = call.has_flag("help"); if help { - let full_help = - get_full_help(&Nu.signature(), &Nu.examples(), engine_state, &mut stack); + let full_help = get_full_help( + &Nu.signature(), + &Nu.examples(), + engine_state, + &mut stack, + true, + ); let _ = std::panic::catch_unwind(move || stdout_write_all_and_flush(full_help)); @@ -600,7 +605,13 @@ fn parse_commandline_args( } // Just give the help and exit if the above fails - let full_help = get_full_help(&Nu.signature(), &Nu.examples(), engine_state, &mut stack); + let full_help = get_full_help( + &Nu.signature(), + &Nu.examples(), + engine_state, + &mut stack, + true, + ); print!("{}", full_help); std::process::exit(1); } @@ -731,7 +742,7 @@ impl Command for Nu { _input: PipelineData, ) -> Result { Ok(Value::String { - val: get_full_help(&Nu.signature(), &Nu.examples(), engine_state, stack), + val: get_full_help(&Nu.signature(), &Nu.examples(), engine_state, stack, true), span: call.head, } .into_pipeline_data()) diff --git a/src/tests/test_engine.rs b/src/tests/test_engine.rs index 8e0796c4e..15f5463cf 100644 --- a/src/tests/test_engine.rs +++ b/src/tests/test_engine.rs @@ -53,7 +53,7 @@ fn in_and_if_else() -> TestResult { #[test] fn help_works_with_missing_requirements() -> TestResult { - run_test(r#"each --help | lines | length"#, "37") + run_test(r#"each --help | lines | length"#, "40") } #[test]