diff --git a/Cargo.lock b/Cargo.lock index 6d6b7893b6..b70b7cf907 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -551,6 +551,7 @@ dependencies = [ name = "nu-engine" version = "0.1.0" dependencies = [ + "itertools", "nu-parser", "nu-path", "nu-protocol", diff --git a/crates/nu-command/src/core_commands/do_.rs b/crates/nu-command/src/core_commands/do_.rs index 20bd0eb7d2..d87148fa1a 100644 --- a/crates/nu-command/src/core_commands/do_.rs +++ b/crates/nu-command/src/core_commands/do_.rs @@ -15,7 +15,7 @@ impl Command for Do { } fn signature(&self) -> nu_protocol::Signature { - Signature::build("do").required( + Signature::build("do").desc(self.usage()).required( "block", SyntaxShape::Block(Some(vec![])), "the block to run", diff --git a/crates/nu-command/src/core_commands/help.rs b/crates/nu-command/src/core_commands/help.rs index 0313f25534..6bc36ef542 100644 --- a/crates/nu-command/src/core_commands/help.rs +++ b/crates/nu-command/src/core_commands/help.rs @@ -1,10 +1,10 @@ use nu_protocol::{ ast::Call, engine::{Command, EvaluationContext}, - Example, ShellError, Signature, Spanned, SyntaxShape, Value, + span, Example, ShellError, Signature, Spanned, SyntaxShape, Value, }; -use nu_engine::CallExt; +use nu_engine::{get_full_help, CallExt}; pub struct Help; @@ -73,11 +73,11 @@ impl Command for Help { } fn help(context: &EvaluationContext, call: &Call) -> Result { - let span = call.head; + let head = call.head; let find: Option> = call.get_flag(context, "find")?; let rest: Vec> = call.rest(context, 0)?; - let full_commands = context.get_commands_info(); + let full_commands = context.get_signatures_with_examples(); if let Some(f) = find { let search_string = f.item; @@ -87,29 +87,36 @@ fn help(context: &EvaluationContext, call: &Call) -> Result { let mut cols = vec![]; let mut vals = vec![]; - let key = cmd.name.clone(); - let c = cmd.usage.clone(); - let e = cmd.extra_usage.clone(); + let key = cmd.0.name.clone(); + let c = cmd.0.usage.clone(); + let e = cmd.0.extra_usage.clone(); if key.to_lowercase().contains(&search_string) || c.to_lowercase().contains(&search_string) || e.to_lowercase().contains(&search_string) { cols.push("name".into()); - vals.push(Value::String { val: key, span }); + vals.push(Value::String { + val: key, + span: head, + }); cols.push("usage".into()); - vals.push(Value::String { val: c, span }); + vals.push(Value::String { val: c, span: head }); cols.push("extra_usage".into()); - vals.push(Value::String { val: e, span }); + vals.push(Value::String { val: e, span: head }); - found_cmds_vec.push(Value::Record { cols, vals, span }); + found_cmds_vec.push(Value::Record { + cols, + vals, + span: head, + }); } } return Ok(Value::List { vals: found_cmds_vec, - span, + span: head, }); } @@ -121,25 +128,38 @@ fn help(context: &EvaluationContext, call: &Call) -> Result { let mut cols = vec![]; let mut vals = vec![]; - let key = cmd.name.clone(); - let c = cmd.usage.clone(); - let e = cmd.extra_usage.clone(); + let key = cmd.0.name.clone(); + let c = cmd.0.usage.clone(); + let e = cmd.0.extra_usage.clone(); cols.push("name".into()); - vals.push(Value::String { val: key, span }); + vals.push(Value::String { + val: key, + span: head, + }); cols.push("usage".into()); - vals.push(Value::String { val: c, span }); + vals.push(Value::String { val: c, span: head }); cols.push("extra_usage".into()); - vals.push(Value::String { val: e, span }); + vals.push(Value::String { val: e, span: head }); - found_cmds_vec.push(Value::Record { cols, vals, span }); + found_cmds_vec.push(Value::Record { + cols, + vals, + span: head, + }); } + + Ok(Value::List { + vals: found_cmds_vec, + span: head, + }) } else { let mut name = String::new(); + let mut output = String::new(); - for r in rest { + for r in &rest { if !name.is_empty() { name.push(' '); } @@ -147,31 +167,24 @@ fn help(context: &EvaluationContext, call: &Call) -> Result { } for cmd in full_commands { - let mut cols = vec![]; - let mut vals = vec![]; - - let key = cmd.name.clone(); - let c = cmd.usage.clone(); - let e = cmd.extra_usage.clone(); - - if key.starts_with(&name) { - cols.push("name".into()); - vals.push(Value::String { val: key, span }); - - cols.push("usage".into()); - vals.push(Value::String { val: c, span }); - - cols.push("extra_usage".into()); - vals.push(Value::String { val: e, span }); - - found_cmds_vec.push(Value::Record { cols, vals, span }); + if cmd.0.name == name { + let help = get_full_help(&cmd.0, &cmd.1, context); + output.push_str(&help); } } + + if !output.is_empty() { + Ok(Value::String { + val: output, + span: call.head, + }) + } else { + Err(ShellError::CommandNotFound(span(&[ + rest[0].span, + rest[rest.len() - 1].span, + ]))) + } } - Ok(Value::List { - vals: found_cmds_vec, - span, - }) // FIXME: the fancy help stuff needs to be reimplemented /* @@ -341,7 +354,7 @@ You can also learn more at https://www.nushell.sh/book/"#; Ok(Value::String { val: msg.into(), - span, + span: head, }) } } diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index e35574a7ab..9eb6c1c0bc 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -41,6 +41,8 @@ pub fn create_default_context() -> Rc> { working_set.add_decl(Box::new(Mv)); working_set.add_decl(Box::new(Ps)); working_set.add_decl(Box::new(Select)); + working_set.add_decl(Box::new(Split)); + working_set.add_decl(Box::new(SplitChars)); working_set.add_decl(Box::new(Sys)); working_set.add_decl(Box::new(Table)); working_set.add_decl(Box::new(Touch)); diff --git a/crates/nu-command/src/strings/mod.rs b/crates/nu-command/src/strings/mod.rs index 8691acce1e..d3df18f05a 100644 --- a/crates/nu-command/src/strings/mod.rs +++ b/crates/nu-command/src/strings/mod.rs @@ -1,3 +1,5 @@ mod build_string; +mod split; pub use build_string::BuildString; +pub use split::*; diff --git a/crates/nu-command/src/strings/split/chars.rs b/crates/nu-command/src/strings/split/chars.rs new file mode 100644 index 0000000000..f77f873692 --- /dev/null +++ b/crates/nu-command/src/strings/split/chars.rs @@ -0,0 +1,157 @@ +use nu_protocol::{ + ast::Call, + engine::{Command, EvaluationContext}, + Example, IntoValueStream, ShellError, Signature, Span, Type, Value, +}; + +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "split chars" + } + + fn signature(&self) -> Signature { + Signature::build("split chars") + } + + fn usage(&self) -> &str { + "splits a string's characters into separate rows" + } + + fn run( + &self, + _context: &EvaluationContext, + call: &Call, + input: Value, + ) -> Result { + split_chars(call, input) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Split the string's characters into separate rows", + example: "echo 'hello' | split chars", + result: Some(vec![ + Value::String { + val: "h".into(), + span: Span::unknown(), + }, + Value::String { + val: "e".into(), + span: Span::unknown(), + }, + Value::String { + val: "l".into(), + span: Span::unknown(), + }, + Value::String { + val: "l".into(), + span: Span::unknown(), + }, + Value::String { + val: "o".into(), + span: Span::unknown(), + }, + ]), + }] + } +} + +fn split_chars(call: &Call, input: Value) -> Result { + let name = call.head; + + Ok(match input { + Value::List { vals, span } => Value::List { + vals: vals + .iter() + .flat_map(move |v| { + if let Ok(s) = v.as_string() { + let v_span = v.span(); + s.chars() + .collect::>() + .into_iter() + .map(move |x| Value::String { + val: x.to_string(), + span: v_span, + }) + .collect() + } else { + vec![Value::Error { + error: ShellError::PipelineMismatch { + expected: Type::String, + expected_span: name, + origin: v.span(), + }, + }] + } + }) + .collect(), + span, + }, + Value::Stream { stream, span } => Value::Stream { + stream: stream + .flat_map(move |v| { + if let Ok(s) = v.as_string() { + let v_span = v.span(); + s.chars() + .collect::>() + .into_iter() + .map(move |x| Value::String { + val: x.to_string(), + span: v_span, + }) + .collect() + } else { + vec![Value::Error { + error: ShellError::PipelineMismatch { + expected: Type::String, + expected_span: name, + origin: v.span(), + }, + }] + } + }) + .into_value_stream(), + span, + }, + v => { + let v_span = v.span(); + if let Ok(s) = v.as_string() { + Value::List { + vals: s + .chars() + .collect::>() + .into_iter() + .map(move |x| Value::String { + val: x.to_string(), + span: v_span, + }) + .collect(), + span: v_span, + } + } else { + Value::Error { + error: ShellError::PipelineMismatch { + expected: Type::String, + expected_span: name, + origin: v.span(), + }, + } + } + } + }) +} + +// #[cfg(test)] +// mod tests { +// use super::ShellError; +// use super::SubCommand; + +// #[test] +// fn examples_work_as_expected() -> Result<(), ShellError> { +// use crate::examples::test as test_examples; + +// test_examples(SubCommand {}) +// } +// } diff --git a/crates/nu-command/src/strings/split/command.rs b/crates/nu-command/src/strings/split/command.rs new file mode 100644 index 0000000000..87df229275 --- /dev/null +++ b/crates/nu-command/src/strings/split/command.rs @@ -0,0 +1,48 @@ +use nu_engine::get_full_help; +use nu_protocol::{ + ast::Call, + engine::{Command, EvaluationContext}, + Signature, Value, +}; + +#[derive(Clone)] +pub struct SplitCommand; + +impl Command for SplitCommand { + fn name(&self) -> &str { + "split" + } + + fn signature(&self) -> Signature { + Signature::build("split") + } + + fn usage(&self) -> &str { + "Split contents across desired subcommand (like row, column) via the separator." + } + + fn run( + &self, + context: &EvaluationContext, + call: &Call, + _input: Value, + ) -> Result { + Ok(Value::String { + val: get_full_help(&SplitCommand.signature(), &SplitCommand.examples(), context), + span: call.head, + }) + } +} + +// #[cfg(test)] +// mod tests { +// use super::Command; +// use super::ShellError; + +// #[test] +// fn examples_work_as_expected() -> Result<(), ShellError> { +// use crate::examples::test as test_examples; + +// test_examples(Command {}) +// } +// } diff --git a/crates/nu-command/src/strings/split/mod.rs b/crates/nu-command/src/strings/split/mod.rs new file mode 100644 index 0000000000..3a65f0e124 --- /dev/null +++ b/crates/nu-command/src/strings/split/mod.rs @@ -0,0 +1,9 @@ +pub mod chars; +// pub mod column; +pub mod command; +// pub mod row; + +pub use chars::SubCommand as SplitChars; +// pub use column::SubCommand as SplitColumn; +pub use command::SplitCommand as Split; +// pub use row::SubCommand as SplitRow; diff --git a/crates/nu-engine/Cargo.toml b/crates/nu-engine/Cargo.toml index 6bb588bc0a..35ebfa91ca 100644 --- a/crates/nu-engine/Cargo.toml +++ b/crates/nu-engine/Cargo.toml @@ -6,4 +6,5 @@ edition = "2018" [dependencies] nu-parser = { path = "../nu-parser" } nu-protocol = { path = "../nu-protocol" } -nu-path = { path = "../nu-path" } \ No newline at end of file +nu-path = { path = "../nu-path" } +itertools = "0.10.1" \ No newline at end of file diff --git a/crates/nu-engine/src/documentation.rs b/crates/nu-engine/src/documentation.rs new file mode 100644 index 0000000000..b0269f340b --- /dev/null +++ b/crates/nu-engine/src/documentation.rs @@ -0,0 +1,315 @@ +use itertools::Itertools; +use nu_protocol::{engine::EvaluationContext, Example, Signature, Span, Value}; +use std::collections::HashMap; + +const COMMANDS_DOCS_DIR: &str = "docs/commands"; + +pub struct DocumentationConfig { + no_subcommands: bool, + //FIXME: + #[allow(dead_code)] + no_color: bool, + brief: bool, +} + +impl Default for DocumentationConfig { + fn default() -> Self { + DocumentationConfig { + no_subcommands: false, + no_color: false, + brief: false, + } + } +} + +fn generate_doc(name: &str, context: &EvaluationContext) -> (Vec, Vec) { + let mut cols = vec![]; + let mut vals = vec![]; + + let engine_state = context.engine_state.borrow(); + + let command = engine_state + .find_decl(name.as_bytes()) + .map(|decl_id| engine_state.get_decl(decl_id)) + .unwrap_or_else(|| panic!("Expected command '{}' from names to be in registry", name)); + + cols.push("name".to_string()); + vals.push(Value::String { + val: name.into(), + span: Span::unknown(), + }); + + cols.push("usage".to_string()); + vals.push(Value::String { + val: command.usage().to_owned(), + span: Span::unknown(), + }); + + if let Some(link) = retrieve_doc_link(name) { + cols.push("doc_link".into()); + vals.push(Value::String { + val: link, + span: Span::unknown(), + }); + } + + cols.push("documentation".to_owned()); + vals.push(Value::String { + val: get_documentation( + &command.signature(), + &command.examples(), + context, + &DocumentationConfig { + no_subcommands: true, + no_color: true, + brief: false, + }, + ), + span: Span::unknown(), + }); + + (cols, vals) +} + +// generate_docs gets the documentation from each command and returns a Table as output +pub fn generate_docs(context: &EvaluationContext) -> Value { + let signatures = context.get_signatures(); + + // cmap will map parent commands to it's subcommands e.g. to -> [to csv, to yaml, to bson] + let mut cmap: HashMap> = HashMap::new(); + for sig in &signatures { + if sig.name.contains(' ') { + let mut split_name = sig.name.split_whitespace(); + let parent_name = split_name.next().expect("Expected a parent command name"); + if cmap.contains_key(parent_name) { + let sub_names = cmap + .get_mut(parent_name) + .expect("Expected an entry for parent"); + sub_names.push(sig.name.to_owned()); + } + } else { + cmap.insert(sig.name.to_owned(), Vec::new()); + }; + } + // Return documentation for each command + // Sub-commands are nested under their respective parent commands + let mut table = Vec::new(); + for sig in &signatures { + // Must be a sub-command, skip since it's being handled underneath when we hit the parent command + if !cmap.contains_key(&sig.name) { + continue; + } + let mut row_entries = generate_doc(&sig.name, context); + // Iterate over all the subcommands of the parent command + let mut sub_table = Vec::new(); + for sub_name in cmap.get(&sig.name).unwrap_or(&Vec::new()) { + let (cols, vals) = generate_doc(sub_name, context); + sub_table.push(Value::Record { + cols, + vals, + span: Span::unknown(), + }); + } + + if !sub_table.is_empty() { + row_entries.0.push("subcommands".into()); + row_entries.1.push(Value::List { + vals: sub_table, + span: Span::unknown(), + }); + } + table.push(Value::Record { + cols: row_entries.0, + vals: row_entries.1, + span: Span::unknown(), + }); + } + Value::List { + vals: table, + span: Span::unknown(), + } +} + +fn retrieve_doc_link(name: &str) -> Option { + let doc_name = name.split_whitespace().join("_"); // Because .replace(" ", "_") didn't work + let mut entries = + std::fs::read_dir(COMMANDS_DOCS_DIR).expect("Directory for command docs are missing!"); + entries.find_map(|r| { + r.map_or(None, |de| { + if de.file_name().to_string_lossy() == format!("{}.{}", &doc_name, "md") { + Some(format!("/commands/{}.{}", &doc_name, "html")) + } else { + None + } + }) + }) +} + +#[allow(clippy::cognitive_complexity)] +pub fn get_documentation( + sig: &Signature, + examples: &[Example], + context: &EvaluationContext, + config: &DocumentationConfig, +) -> String { + let cmd_name = &sig.name; + let mut long_desc = String::new(); + + let usage = &sig.usage; + if !usage.is_empty() { + long_desc.push_str(usage); + long_desc.push_str("\n\n"); + } + + let extra_usage = if config.brief { "" } else { &sig.extra_usage }; + if !extra_usage.is_empty() { + long_desc.push_str(extra_usage); + long_desc.push_str("\n\n"); + } + + let mut subcommands = vec![]; + if !config.no_subcommands { + let signatures = context.get_signatures(); + for sig in signatures { + if sig.name.starts_with(&format!("{} ", cmd_name)) { + subcommands.push(format!(" {} - {}", sig.name, sig.usage)); + } + } + } + + let mut one_liner = String::new(); + one_liner.push_str(&sig.name); + one_liner.push(' '); + + for positional in &sig.required_positional { + one_liner.push_str(&format!("<{}> ", positional.name)); + } + for positional in &sig.optional_positional { + one_liner.push_str(&format!("({}) ", positional.name)); + } + + if sig.rest_positional.is_some() { + one_liner.push_str("...args "); + } + + if !subcommands.is_empty() { + one_liner.push_str(" "); + } + + if !sig.named.is_empty() { + one_liner.push_str("{flags} "); + } + + long_desc.push_str(&format!("Usage:\n > {}\n", one_liner)); + + if !subcommands.is_empty() { + long_desc.push_str("\nSubcommands:\n"); + subcommands.sort(); + long_desc.push_str(&subcommands.join("\n")); + long_desc.push('\n'); + } + + if !sig.required_positional.is_empty() + || !sig.optional_positional.is_empty() + || sig.rest_positional.is_some() + { + long_desc.push_str("\nParameters:\n"); + for positional in &sig.required_positional { + long_desc.push_str(&format!(" <{}> {}\n", positional.name, positional.desc)); + } + for positional in &sig.optional_positional { + long_desc.push_str(&format!(" ({}) {}\n", positional.name, positional.desc)); + } + + if let Some(rest_positional) = &sig.rest_positional { + long_desc.push_str(&format!(" ...args: {}\n", rest_positional.desc)); + } + } + if !sig.named.is_empty() { + long_desc.push_str(&get_flags_section(sig)) + } + + if !examples.is_empty() { + long_desc.push_str("\nExamples:"); + } + for example in examples { + long_desc.push('\n'); + long_desc.push_str(" "); + long_desc.push_str(example.description); + + // if config.no_color { + long_desc.push_str(&format!("\n > {}\n", example.example)); + // } else { + // let colored_example = + + // crate::shell::painter::Painter::paint_string(example.example, scope, &palette); + // long_desc.push_str(&format!("\n > {}\n", colored_example)); + // } + } + + long_desc.push('\n'); + + long_desc +} + +fn get_flags_section(signature: &Signature) -> String { + let mut long_desc = String::new(); + long_desc.push_str("\nFlags:\n"); + for flag in &signature.named { + let msg = if let Some(arg) = &flag.arg { + if let Some(short) = flag.short { + if flag.required { + format!( + " -{}, --{} (required parameter){:?} {}\n", + short, flag.long, arg, flag.desc + ) + } else { + format!(" -{}, --{} {:?} {}\n", short, flag.long, arg, flag.desc) + } + } else if flag.required { + format!( + " --{} (required parameter){:?} {}\n", + flag.long, arg, flag.desc + ) + } else { + format!(" --{} {:?} {}\n", flag.long, arg, flag.desc) + } + } else if let Some(short) = flag.short { + if flag.required { + format!( + " -{}, --{} (required parameter) {}\n", + short, flag.long, flag.desc + ) + } else { + format!(" -{}, --{} {}\n", short, flag.long, flag.desc) + } + } else if flag.required { + format!(" --{} (required parameter) {}\n", flag.long, flag.desc) + } else { + format!(" --{} {}\n", flag.long, flag.desc) + }; + long_desc.push_str(&msg); + } + long_desc +} + +pub fn get_brief_help( + sig: &Signature, + examples: &[Example], + context: &EvaluationContext, +) -> String { + get_documentation( + sig, + examples, + context, + &DocumentationConfig { + no_subcommands: false, + no_color: false, + brief: true, + }, + ) +} + +pub fn get_full_help(sig: &Signature, examples: &[Example], context: &EvaluationContext) -> String { + get_documentation(sig, examples, context, &DocumentationConfig::default()) +} diff --git a/crates/nu-engine/src/lib.rs b/crates/nu-engine/src/lib.rs index 2cbbbee638..cc7e7a1236 100644 --- a/crates/nu-engine/src/lib.rs +++ b/crates/nu-engine/src/lib.rs @@ -1,7 +1,9 @@ mod call_ext; +mod documentation; mod eval; mod from_value; pub use call_ext::CallExt; +pub use documentation::{generate_docs, get_brief_help, get_documentation, get_full_help}; pub use eval::{eval_block, eval_expression, eval_operator}; pub use from_value::FromValue; diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index 3d8f2c8229..c93c0ace7d 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -1,5 +1,5 @@ use super::Command; -use crate::{ast::Block, BlockId, DeclId, Signature, Span, Type, VarId}; +use crate::{ast::Block, BlockId, DeclId, Example, Signature, Span, Type, VarId}; use core::panic; use std::{ collections::{HashMap, HashSet}, @@ -178,7 +178,7 @@ impl EngineState { .expect("internal error: missing declaration") } - pub fn get_decls(&self) -> Vec { + pub fn get_signatures(&self) -> Vec { let mut output = vec![]; for decl in self.decls.iter() { if decl.get_block_id().is_none() { @@ -193,6 +193,21 @@ impl EngineState { output } + pub fn get_signatures_with_examples(&self) -> Vec<(Signature, Vec)> { + let mut output = vec![]; + for decl in self.decls.iter() { + if decl.get_block_id().is_none() { + let mut signature = (*decl).signature(); + signature.usage = decl.usage().to_string(); + signature.extra_usage = decl.extra_usage().to_string(); + + output.push((signature, decl.examples())); + } + } + + output + } + pub fn get_block(&self, block_id: BlockId) -> &Block { self.blocks .get(block_id) diff --git a/crates/nu-protocol/src/engine/evaluation_context.rs b/crates/nu-protocol/src/engine/evaluation_context.rs index 721c21c811..07a71ff91a 100644 --- a/crates/nu-protocol/src/engine/evaluation_context.rs +++ b/crates/nu-protocol/src/engine/evaluation_context.rs @@ -1,7 +1,7 @@ use super::EngineState; use std::{cell::RefCell, collections::HashMap, rc::Rc}; -use crate::{ShellError, Signature, Value, VarId}; +use crate::{Example, ShellError, Signature, Value, VarId}; #[derive(Clone)] pub struct EvaluationContext { @@ -47,8 +47,12 @@ impl EvaluationContext { self.stack.print_stack(); } - pub fn get_commands_info(&self) -> Vec { - self.engine_state.borrow().get_decls() + pub fn get_signatures(&self) -> Vec { + self.engine_state.borrow().get_signatures() + } + + pub fn get_signatures_with_examples(&self) -> Vec<(Signature, Vec)> { + self.engine_state.borrow().get_signatures_with_examples() } } diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index 4be91303d0..87f15fc3b1 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -19,6 +19,16 @@ pub enum ShellError { rhs_span: Span, }, + #[error("Pipeline mismatch.")] + #[diagnostic(code(nu::shell::pipeline_mismatch), url(docsrs))] + PipelineMismatch { + expected: Type, + #[label("expected: {expected}")] + expected_span: Span, + #[label("value originates from here")] + origin: Span, + }, + #[error("Unsupported operator: {0}.")] #[diagnostic(code(nu::shell::unsupported_operator), url(docsrs))] UnsupportedOperator(Operator, #[label = "unsupported operator"] Span), @@ -79,6 +89,10 @@ pub enum ShellError { #[diagnostic(code(nu::shell::unsupported_input), url(docsrs))] UnsupportedInput(String, #[label("{0}")] Span), + #[error("Command not found")] + #[diagnostic(code(nu::shell::command_not_found), url(docsrs))] + CommandNotFound(#[label("command not found")] Span), + #[error("Flag not found")] #[diagnostic(code(nu::shell::flag_not_found), url(docsrs))] FlagNotFound(String, #[label("{0} not found")] Span),