Merge pull request #108 from nushell/help_and_start_split

Port help and start porting split
This commit is contained in:
JT 2021-10-09 14:07:13 +13:00 committed by GitHub
commit 44fbf0fce3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 633 additions and 50 deletions

1
Cargo.lock generated
View File

@ -551,6 +551,7 @@ dependencies = [
name = "nu-engine"
version = "0.1.0"
dependencies = [
"itertools",
"nu-parser",
"nu-path",
"nu-protocol",

View File

@ -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",

View File

@ -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<Value, ShellError> {
let span = call.head;
let head = call.head;
let find: Option<Spanned<String>> = call.get_flag(context, "find")?;
let rest: Vec<Spanned<String>> = 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<Value, ShellError> {
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<Value, ShellError> {
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<Value, ShellError> {
}
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,
})
}
}

View File

@ -41,6 +41,8 @@ pub fn create_default_context() -> Rc<RefCell<EngineState>> {
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));

View File

@ -1,3 +1,5 @@
mod build_string;
mod split;
pub use build_string::BuildString;
pub use split::*;

View File

@ -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<nu_protocol::Value, nu_protocol::ShellError> {
split_chars(call, input)
}
fn examples(&self) -> Vec<Example> {
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<nu_protocol::Value, nu_protocol::ShellError> {
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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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 {})
// }
// }

View File

@ -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<nu_protocol::Value, nu_protocol::ShellError> {
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 {})
// }
// }

View File

@ -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;

View File

@ -6,4 +6,5 @@ edition = "2018"
[dependencies]
nu-parser = { path = "../nu-parser" }
nu-protocol = { path = "../nu-protocol" }
nu-path = { path = "../nu-path" }
nu-path = { path = "../nu-path" }
itertools = "0.10.1"

View File

@ -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<String>, Vec<Value>) {
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<String, Vec<String>> = 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<String> {
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("<subcommand> ");
}
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())
}

View File

@ -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;

View File

@ -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<Signature> {
pub fn get_signatures(&self) -> Vec<Signature> {
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<Example>)> {
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)

View File

@ -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<Signature> {
self.engine_state.borrow().get_decls()
pub fn get_signatures(&self) -> Vec<Signature> {
self.engine_state.borrow().get_signatures()
}
pub fn get_signatures_with_examples(&self) -> Vec<(Signature, Vec<Example>)> {
self.engine_state.borrow().get_signatures_with_examples()
}
}

View File

@ -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),