mirror of
https://github.com/nushell/nushell.git
synced 2025-04-29 23:54:26 +02:00
# Description Before this PR, `help commands` uses the name from a command's declaration rather than the name in the scope. This is problematic when trying to view the help page for the `main` command of a module. For example, `std bench`: ```nushell use std/bench help bench # => Error: nu::parser::not_found # => # => × Not found. # => ╭─[entry #10:1:6] # => 1 │ help bench # => · ──┬── # => · ╰── did not find anything under this name # => ╰──── ``` This can also cause confusion when importing specific commands from modules. Furthermore, if there are multiple commands with the same name from different modules, the help text for _both_ will appear when querying their help text (this is especially problematic for `main` commands, see #14033): ```nushell use std/iter help iter find # => Error: nu::parser::not_found # => # => × Not found. # => ╭─[entry #3:1:6] # => 1│ help iter find # => · ────┬──── # => · ╰── did not find anything under this name # => ╰──── help find # => Searches terms in the input. # => # => Search terms: filter, regex, search, condition # => # => Usage: # => > find {flags} ...(rest) # [...] # => Returns the first element of the list that matches the # => closure predicate, `null` otherwise # [...] # (full text omitted for brevity) ``` This PR changes `help commands` to use the name as it is in scope, so prefixing any command in scope with `help` will show the correct help text. ```nushell use std/bench help bench # [help text for std bench] use std/iter help iter find # [help text for std iter find] use std help std bench # [help text for std bench] help std iter find # [help text for std iter find] ``` Additionally, the IR code generation for commands called with the `--help` text has been updated to reflect this change. This does have one side effect: when a module has a `main` command defined, running `help <name>` (which checks `help aliases`, then `help commands`, then `help modules`) will show the help text for the `main` command rather than the module. The help text for the module is still accessible with `help modules <name>`. Fixes #10499, #10311, #11609, #13470, #14033, and #14402. Partially fixes #10707. Does **not** fix #11447. # User-Facing Changes * Help text for commands can be obtained by running `help <command name>`, where the command name is the same thing you would type in order to execute the command. Previously, it was the name of the function as written in the source file. * For example, for the following module `spam` with command `meow`: ```nushell module spam { # help text export def meow [] {} } ``` * Before this PR: * Regardless of how `meow` is `use`d, the help text is viewable by running `help meow`. * After this PR: * When imported with `use spam`: The `meow` command is executed by running `spam meow` and the `help` text is viewable by running `help spam meow`. * When imported with `use spam foo`: The `meow` command is executed by running `meow` and the `help` text is viewable by running `meow`. * When a module has a `main` command defined, `help <module name>` will return help for the main command, rather than the module. To access the help for the module, use `help modules <module name>`. # Tests + Formatting - 🟢 `toolkit fmt` - 🟢 `toolkit clippy` - 🟢 `toolkit test` - 🟢 `toolkit test stdlib` # After Submitting N/A
230 lines
7.7 KiB
Rust
230 lines
7.7 KiB
Rust
use crate::help::highlight_search_in_table;
|
|
use nu_color_config::StyleComputer;
|
|
use nu_engine::{command_prelude::*, get_full_help};
|
|
|
|
#[derive(Clone)]
|
|
pub struct HelpCommands;
|
|
|
|
impl Command for HelpCommands {
|
|
fn name(&self) -> &str {
|
|
"help commands"
|
|
}
|
|
|
|
fn description(&self) -> &str {
|
|
"Show help on nushell commands."
|
|
}
|
|
|
|
fn signature(&self) -> Signature {
|
|
Signature::build("help commands")
|
|
.category(Category::Core)
|
|
.rest(
|
|
"rest",
|
|
SyntaxShape::String,
|
|
"The name of command to get help on.",
|
|
)
|
|
.named(
|
|
"find",
|
|
SyntaxShape::String,
|
|
"string to find in command names, descriptions, and search terms",
|
|
Some('f'),
|
|
)
|
|
.input_output_types(vec![(Type::Nothing, Type::table())])
|
|
.allow_variants_without_examples(true)
|
|
}
|
|
|
|
fn run(
|
|
&self,
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
call: &Call,
|
|
_input: PipelineData,
|
|
) -> Result<PipelineData, ShellError> {
|
|
help_commands(engine_state, stack, call)
|
|
}
|
|
}
|
|
|
|
pub fn help_commands(
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
call: &Call,
|
|
) -> Result<PipelineData, ShellError> {
|
|
let head = call.head;
|
|
let find: Option<Spanned<String>> = call.get_flag(engine_state, stack, "find")?;
|
|
let rest: Vec<Spanned<String>> = call.rest(engine_state, stack, 0)?;
|
|
|
|
// 🚩The following two-lines are copied from filters/find.rs:
|
|
let style_computer = StyleComputer::from_config(engine_state, stack);
|
|
// Currently, search results all use the same style.
|
|
// Also note that this sample string is passed into user-written code (the closure that may or may not be
|
|
// defined for "string").
|
|
let string_style = style_computer.compute("string", &Value::string("search result", head));
|
|
let highlight_style =
|
|
style_computer.compute("search_result", &Value::string("search result", head));
|
|
|
|
if let Some(f) = find {
|
|
let all_cmds_vec = build_help_commands(engine_state, head);
|
|
let found_cmds_vec = highlight_search_in_table(
|
|
all_cmds_vec,
|
|
&f.item,
|
|
&["name", "description", "search_terms"],
|
|
&string_style,
|
|
&highlight_style,
|
|
)?;
|
|
|
|
return Ok(Value::list(found_cmds_vec, head).into_pipeline_data());
|
|
}
|
|
|
|
if rest.is_empty() {
|
|
let found_cmds_vec = build_help_commands(engine_state, head);
|
|
Ok(Value::list(found_cmds_vec, head).into_pipeline_data())
|
|
} else {
|
|
let mut name = String::new();
|
|
|
|
for r in &rest {
|
|
if !name.is_empty() {
|
|
name.push(' ');
|
|
}
|
|
name.push_str(&r.item);
|
|
}
|
|
|
|
if let Some(decl) = engine_state.find_decl(name.as_bytes(), &[]) {
|
|
let cmd = engine_state.get_decl(decl);
|
|
let help_text = get_full_help(cmd, engine_state, stack);
|
|
Ok(Value::string(help_text, call.head).into_pipeline_data())
|
|
} else {
|
|
Err(ShellError::CommandNotFound {
|
|
span: Span::merge_many(rest.iter().map(|s| s.span)),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
fn build_help_commands(engine_state: &EngineState, span: Span) -> Vec<Value> {
|
|
let commands = engine_state.get_decls_sorted(false);
|
|
let mut found_cmds_vec = Vec::new();
|
|
|
|
for (_, decl_id) in commands {
|
|
let decl = engine_state.get_decl(decl_id);
|
|
let sig = decl.signature().update_from_command(decl);
|
|
|
|
let key = sig.name;
|
|
let description = sig.description;
|
|
let search_terms = sig.search_terms;
|
|
|
|
let command_type = decl.command_type().to_string();
|
|
|
|
// Build table of parameters
|
|
let param_table = {
|
|
let mut vals = vec![];
|
|
|
|
for required_param in &sig.required_positional {
|
|
vals.push(Value::record(
|
|
record! {
|
|
"name" => Value::string(&required_param.name, span),
|
|
"type" => Value::string(required_param.shape.to_string(), span),
|
|
"required" => Value::bool(true, span),
|
|
"description" => Value::string(&required_param.desc, span),
|
|
},
|
|
span,
|
|
));
|
|
}
|
|
|
|
for optional_param in &sig.optional_positional {
|
|
vals.push(Value::record(
|
|
record! {
|
|
"name" => Value::string(&optional_param.name, span),
|
|
"type" => Value::string(optional_param.shape.to_string(), span),
|
|
"required" => Value::bool(false, span),
|
|
"description" => Value::string(&optional_param.desc, span),
|
|
},
|
|
span,
|
|
));
|
|
}
|
|
|
|
if let Some(rest_positional) = &sig.rest_positional {
|
|
vals.push(Value::record(
|
|
record! {
|
|
"name" => Value::string(format!("...{}", rest_positional.name), span),
|
|
"type" => Value::string(rest_positional.shape.to_string(), span),
|
|
"required" => Value::bool(false, span),
|
|
"description" => Value::string(&rest_positional.desc, span),
|
|
},
|
|
span,
|
|
));
|
|
}
|
|
|
|
for named_param in &sig.named {
|
|
let name = if let Some(short) = named_param.short {
|
|
if named_param.long.is_empty() {
|
|
format!("-{}", short)
|
|
} else {
|
|
format!("--{}(-{})", named_param.long, short)
|
|
}
|
|
} else {
|
|
format!("--{}", named_param.long)
|
|
};
|
|
|
|
let typ = if let Some(arg) = &named_param.arg {
|
|
arg.to_string()
|
|
} else {
|
|
"switch".to_string()
|
|
};
|
|
|
|
vals.push(Value::record(
|
|
record! {
|
|
"name" => Value::string(name, span),
|
|
"type" => Value::string(typ, span),
|
|
"required" => Value::bool(named_param.required, span),
|
|
"description" => Value::string(&named_param.desc, span),
|
|
},
|
|
span,
|
|
));
|
|
}
|
|
|
|
Value::list(vals, span)
|
|
};
|
|
|
|
// Build the signature input/output table
|
|
let input_output_table = {
|
|
let mut vals = vec![];
|
|
|
|
for (input_type, output_type) in sig.input_output_types {
|
|
vals.push(Value::record(
|
|
record! {
|
|
"input" => Value::string(input_type.to_string(), span),
|
|
"output" => Value::string(output_type.to_string(), span),
|
|
},
|
|
span,
|
|
));
|
|
}
|
|
|
|
Value::list(vals, span)
|
|
};
|
|
|
|
let record = record! {
|
|
"name" => Value::string(key, span),
|
|
"category" => Value::string(sig.category.to_string(), span),
|
|
"command_type" => Value::string(command_type, span),
|
|
"description" => Value::string(description, span),
|
|
"params" => param_table,
|
|
"input_output" => input_output_table,
|
|
"search_terms" => Value::string(search_terms.join(", "), span),
|
|
"is_const" => Value::bool(decl.is_const(), span),
|
|
};
|
|
|
|
found_cmds_vec.push(Value::record(record, span));
|
|
}
|
|
|
|
found_cmds_vec
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
#[test]
|
|
fn test_examples() {
|
|
use super::HelpCommands;
|
|
use crate::test_examples;
|
|
test_examples(HelpCommands {})
|
|
}
|
|
}
|