add signature information when get help on one command (#7079)

* add signature information when help on one command

* tell user that one command support operated on cell paths

Also, make type output to be more friendly, like `record<>` should just be `record`

And the same to `table<>`, which should be `table`

* simplify code

* don't show signatures for parser keyword

* update comment

* output arg syntax shape as type, so it's the same as describe command

* fix string when no positional args

* update signature body

* update

* add help signature test

* fix arg output format for composed data type like list or record

* fix clippy

* add comment
This commit is contained in:
WindSoilder 2022-11-20 21:22:42 +08:00 committed by GitHub
parent a896892ac9
commit d01ccd5a54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 258 additions and 55 deletions

View File

@ -17,7 +17,7 @@ impl NuHelpCompleter {
//Vec<(Signature, Vec<Example>, bool, bool)> { //Vec<(Signature, Vec<Example>, bool, bool)> {
let mut commands = full_commands let mut commands = full_commands
.iter() .iter()
.filter(|(sig, _, _, _)| { .filter(|(sig, _, _, _, _)| {
sig.name.to_lowercase().contains(&line.to_lowercase()) sig.name.to_lowercase().contains(&line.to_lowercase())
|| sig.usage.to_lowercase().contains(&line.to_lowercase()) || sig.usage.to_lowercase().contains(&line.to_lowercase())
|| sig || sig
@ -31,7 +31,7 @@ impl NuHelpCompleter {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
commands.sort_by(|(a, _, _, _), (b, _, _, _)| { commands.sort_by(|(a, _, _, _, _), (b, _, _, _, _)| {
let a_distance = levenshtein_distance(line, &a.name); let a_distance = levenshtein_distance(line, &a.name);
let b_distance = levenshtein_distance(line, &b.name); let b_distance = levenshtein_distance(line, &b.name);
a_distance.cmp(&b_distance) a_distance.cmp(&b_distance)
@ -39,7 +39,7 @@ impl NuHelpCompleter {
commands commands
.into_iter() .into_iter()
.map(|(sig, examples, _, _)| { .map(|(sig, examples, _, _, _)| {
let mut long_desc = String::new(); let mut long_desc = String::new();
let usage = &sig.usage; let usage = &sig.usage;

View File

@ -29,7 +29,13 @@ impl Command for Bits {
_input: PipelineData, _input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> { ) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
Ok(Value::String { 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, span: call.head,
} }
.into_pipeline_data()) .into_pipeline_data())

View File

@ -29,7 +29,13 @@ impl Command for Bytes {
_input: PipelineData, _input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> { ) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
Ok(Value::String { 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, span: call.head,
} }
.into_pipeline_data()) .into_pipeline_data())

View File

@ -29,7 +29,13 @@ impl Command for Into {
_input: PipelineData, _input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> { ) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
Ok(Value::String { 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, span: call.head,
} }
.into_pipeline_data()) .into_pipeline_data())

View File

@ -45,6 +45,7 @@ impl Command for ExportCommand {
&ExportCommand.examples(), &ExportCommand.examples(),
engine_state, engine_state,
stack, stack,
self.is_parser_keyword(),
), ),
span: call.head, span: call.head,
} }

View File

@ -1,5 +1,4 @@
use fancy_regex::Regex; use fancy_regex::Regex;
use itertools::Itertools;
use nu_ansi_term::{ use nu_ansi_term::{
Color::{Default, Red, White}, Color::{Default, Red, White},
Style, Style,
@ -105,6 +104,7 @@ fn help(
let mut vals = vec![]; let mut vals = vec![];
let decl = engine_state.get_decl(decl_id); let decl = engine_state.get_decl(decl_id);
let sig = decl.signature().update_from_command(decl.borrow()); let sig = decl.signature().update_from_command(decl.borrow());
let signatures = sig.to_string();
let key = sig.name; let key = sig.name;
let usage = sig.usage; let usage = sig.usage;
let search_terms = sig.search_terms; let search_terms = sig.search_terms;
@ -154,11 +154,11 @@ fn help(
cols.push("signatures".into()); cols.push("signatures".into());
vals.push(Value::String { vals.push(Value::String {
val: sig val: if decl.is_parser_keyword() {
.input_output_types "".to_string()
.iter() } else {
.map(|(i, o)| format!("{:?} => {:?}", i.to_shape(), o.to_shape())) signatures
.join("\n"), },
span: head, span: head,
}); });
@ -219,6 +219,7 @@ fn help(
let decl = engine_state.get_decl(decl_id); let decl = engine_state.get_decl(decl_id);
let sig = decl.signature().update_from_command(decl.borrow()); let sig = decl.signature().update_from_command(decl.borrow());
let signatures = sig.to_string();
let key = sig.name; let key = sig.name;
let usage = sig.usage; let usage = sig.usage;
let search_terms = sig.search_terms; let search_terms = sig.search_terms;
@ -249,11 +250,11 @@ fn help(
cols.push("signatures".into()); cols.push("signatures".into());
vals.push(Value::String { vals.push(Value::String {
val: sig val: if decl.is_parser_keyword() {
.input_output_types "".to_string()
.iter() } else {
.map(|(i, o)| format!("{:?} => {:?}", i.to_shape(), o.to_shape())) signatures
.join("\n"), },
span: head, span: head,
}); });
@ -290,9 +291,9 @@ fn help(
let output = engine_state let output = engine_state
.get_signatures_with_examples(false) .get_signatures_with_examples(false)
.iter() .iter()
.filter(|(signature, _, _, _)| signature.name == name) .filter(|(signature, _, _, _, _)| signature.name == name)
.map(|(signature, examples, _, _)| { .map(|(signature, examples, _, _, is_parser_keyword)| {
get_full_help(signature, examples, engine_state, stack) get_full_help(signature, examples, engine_state, stack, *is_parser_keyword)
}) })
.collect::<Vec<String>>(); .collect::<Vec<String>>();

View File

@ -38,7 +38,13 @@ impl Command for Overlay {
_input: PipelineData, _input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> { ) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
Ok(Value::String { 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, span: call.head,
} }
.into_pipeline_data()) .into_pipeline_data())

View File

@ -53,7 +53,13 @@ fn date(
let head = call.head; let head = call.head;
Ok(Value::String { 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, span: head,
} }
.into_pipeline_data()) .into_pipeline_data())

View File

@ -34,6 +34,7 @@ impl Command for ConfigMeta {
&ConfigMeta.examples(), &ConfigMeta.examples(),
engine_state, engine_state,
stack, stack,
self.is_parser_keyword(),
), ),
span: call.head, span: call.head,
} }

View File

@ -31,7 +31,13 @@ impl Command for Roll {
_input: PipelineData, _input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
Ok(Value::String { 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, span: call.head,
} }
.into_pipeline_data()) .into_pipeline_data())

View File

@ -27,7 +27,13 @@ impl Command for From {
_input: PipelineData, _input: PipelineData,
) -> Result<nu_protocol::PipelineData, ShellError> { ) -> Result<nu_protocol::PipelineData, ShellError> {
Ok(Value::String { 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, span: call.head,
} }
.into_pipeline_data()) .into_pipeline_data())

View File

@ -27,7 +27,13 @@ impl Command for To {
_input: PipelineData, _input: PipelineData,
) -> Result<nu_protocol::PipelineData, ShellError> { ) -> Result<nu_protocol::PipelineData, ShellError> {
Ok(Value::String { 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, span: call.head,
} }
.into_pipeline_data()) .into_pipeline_data())

View File

@ -27,7 +27,13 @@ impl Command for Hash {
_input: PipelineData, _input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
Ok(Value::String { 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, span: call.head,
} }
.into_pipeline_data()) .into_pipeline_data())

View File

@ -34,6 +34,7 @@ impl Command for MathCommand {
&MathCommand.examples(), &MathCommand.examples(),
engine_state, engine_state,
stack, stack,
self.is_parser_keyword(),
), ),
span: call.head, span: call.head,
} }

View File

@ -35,7 +35,13 @@ impl Command for Url {
_input: PipelineData, _input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> { ) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
Ok(Value::String { 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, span: call.head,
} }
.into_pipeline_data()) .into_pipeline_data())

View File

@ -49,6 +49,7 @@ the path literal."#
&PathCommand.examples(), &PathCommand.examples(),
engine_state, engine_state,
stack, stack,
self.is_parser_keyword(),
), ),
span: call.head, span: call.head,
} }

View File

@ -38,6 +38,7 @@ impl Command for Keybindings {
&Keybindings.examples(), &Keybindings.examples(),
engine_state, engine_state,
stack, stack,
self.is_parser_keyword(),
), ),
span: call.head, span: call.head,
} }

View File

@ -38,6 +38,7 @@ impl Command for RandomCommand {
&RandomCommand.examples(), &RandomCommand.examples(),
engine_state, engine_state,
stack, stack,
self.is_parser_keyword(),
), ),
span: call.head, span: call.head,
} }

View File

@ -34,6 +34,7 @@ impl Command for SplitCommand {
&SplitCommand.examples(), &SplitCommand.examples(),
engine_state, engine_state,
stack, stack,
self.is_parser_keyword(),
), ),
span: call.head, span: call.head,
} }

View File

@ -29,7 +29,13 @@ impl Command for Str {
_input: PipelineData, _input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> { ) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
Ok(Value::String { 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, span: call.head,
} }
.into_pipeline_data()) .into_pipeline_data())

View File

@ -14,3 +14,15 @@ fn help_commands_length() {
let is_positive = output_int.is_positive(); let is_positive = output_int.is_positive();
assert!(is_positive); assert!(is_positive);
} }
#[test]
fn help_shows_signature() {
let actual = nu!(cwd: ".", pipeline("help str distance"));
assert!(actual
.out
.contains("<string> | str distance <string> -> <int>"));
// don't show signature for parser keyword
let actual = nu!(cwd: ".", pipeline("help alias"));
assert!(!actual.out.contains("Signatures"));
}

View File

@ -92,7 +92,7 @@ fn reject_record_from_raw_eval() {
) )
); );
assert!(actual.out.contains("record<>")); assert!(actual.out.contains("record"));
} }
#[test] #[test]

View File

@ -10,6 +10,7 @@ pub fn get_full_help(
examples: &[Example], examples: &[Example],
engine_state: &EngineState, engine_state: &EngineState,
stack: &mut Stack, stack: &mut Stack,
is_parser_keyword: bool,
) -> String { ) -> String {
let config = engine_state.get_config(); let config = engine_state.get_config();
let doc_config = DocumentationConfig { let doc_config = DocumentationConfig {
@ -17,7 +18,14 @@ pub fn get_full_help(
no_color: !config.use_ansi_coloring, no_color: !config.use_ansi_coloring,
brief: false, 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)] #[derive(Default)]
@ -34,6 +42,7 @@ fn get_documentation(
engine_state: &EngineState, engine_state: &EngineState,
stack: &mut Stack, stack: &mut Stack,
config: &DocumentationConfig, config: &DocumentationConfig,
is_parser_keyword: bool,
) -> String { ) -> String {
// Create ansi colors // Create ansi colors
const G: &str = "\x1b[32m"; // green const G: &str = "\x1b[32m"; // green
@ -89,6 +98,18 @@ fn get_documentation(
long_desc.push_str(&get_flags_section(sig)) 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() if !sig.required_positional.is_empty()
|| !sig.optional_positional.is_empty() || !sig.optional_positional.is_empty()
|| sig.rest_positional.is_some() || sig.rest_positional.is_some()

View File

@ -43,7 +43,13 @@ pub fn eval_call(
signature.usage = decl.usage().to_string(); signature.usage = decl.usage().to_string();
signature.extra_usage = decl.extra_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 { Ok(Value::String {
val: full_help, val: full_help,
span: call.head, span: call.head,

View File

@ -700,7 +700,7 @@ impl EngineState {
pub fn get_signatures_with_examples( pub fn get_signatures_with_examples(
&self, &self,
include_hidden: bool, include_hidden: bool,
) -> Vec<(Signature, Vec<Example>, bool, bool)> { ) -> Vec<(Signature, Vec<Example>, bool, bool, bool)> {
self.get_decl_ids_sorted(include_hidden) self.get_decl_ids_sorted(include_hidden)
.map(|id| { .map(|id| {
let decl = self.get_decl(id); let decl = self.get_decl(id);
@ -712,6 +712,7 @@ impl EngineState {
decl.examples(), decl.examples(),
decl.is_plugin().is_some(), decl.is_plugin().is_some(),
decl.get_block_id().is_some(), decl.get_block_id().is_some(),
decl.is_parser_keyword(),
) )
}) })
.collect() .collect()

View File

@ -122,6 +122,72 @@ pub struct Signature {
pub category: Category, 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 `<string>`
/// if argument type is already contains `<>`, like `list<any>`, the result will be `list<any>`.
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> | <string>, <int?> => 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::<Vec<String>>();
args.append(
&mut self
.optional_positional
.iter()
.map(|p| fmt_type(&p.shape.to_type(), true))
.collect::<Vec<String>>(),
);
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 { impl PartialEq for Signature {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.name == other.name self.name == other.name

View File

@ -107,24 +107,36 @@ impl Display for Type {
Type::Float => write!(f, "float"), Type::Float => write!(f, "float"),
Type::Int => write!(f, "int"), Type::Int => write!(f, "int"),
Type::Range => write!(f, "range"), Type::Range => write!(f, "range"),
Type::Record(fields) => write!( Type::Record(fields) => {
f, if fields.is_empty() {
"record<{}>", write!(f, "record")
fields } else {
.iter() write!(
.map(|(x, y)| format!("{}: {}", x, y)) f,
.collect::<Vec<String>>() "record<{}>",
.join(", "), fields
), .iter()
Type::Table(columns) => write!( .map(|(x, y)| format!("{}: {}", x, y))
f, .collect::<Vec<String>>()
"table<{}>", .join(", "),
columns )
.iter() }
.map(|(x, y)| format!("{}: {}", x, y)) }
.collect::<Vec<String>>() Type::Table(columns) => {
.join(", ") if columns.is_empty() {
), write!(f, "table")
} else {
write!(
f,
"table<{}>",
columns
.iter()
.map(|(x, y)| format!("{}: {}", x, y))
.collect::<Vec<String>>()
.join(", ")
)
}
}
Type::List(l) => write!(f, "list<{}>", l), Type::List(l) => write!(f, "list<{}>", l),
Type::Nothing => write!(f, "nothing"), Type::Nothing => write!(f, "nothing"),
Type::Number => write!(f, "number"), Type::Number => write!(f, "number"),

View File

@ -563,8 +563,13 @@ fn parse_commandline_args(
let help = call.has_flag("help"); let help = call.has_flag("help");
if help { if help {
let full_help = let full_help = get_full_help(
get_full_help(&Nu.signature(), &Nu.examples(), engine_state, &mut stack); &Nu.signature(),
&Nu.examples(),
engine_state,
&mut stack,
true,
);
let _ = std::panic::catch_unwind(move || stdout_write_all_and_flush(full_help)); 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 // 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); print!("{}", full_help);
std::process::exit(1); std::process::exit(1);
} }
@ -731,7 +742,7 @@ impl Command for Nu {
_input: PipelineData, _input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> { ) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
Ok(Value::String { 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, span: call.head,
} }
.into_pipeline_data()) .into_pipeline_data())

View File

@ -53,7 +53,7 @@ fn in_and_if_else() -> TestResult {
#[test] #[test]
fn help_works_with_missing_requirements() -> TestResult { fn help_works_with_missing_requirements() -> TestResult {
run_test(r#"each --help | lines | length"#, "37") run_test(r#"each --help | lines | length"#, "40")
} }
#[test] #[test]