mirror of
https://github.com/nushell/nushell.git
synced 2025-02-18 11:31:14 +01:00
Merge branch 'main' of https://github.com/nushell/engine-q into unit-test
This commit is contained in:
commit
41366f6cc4
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
history.txt
|
history.txt
|
||||||
/target
|
/target
|
||||||
|
/.vscode
|
75
Cargo.lock
generated
75
Cargo.lock
generated
@ -35,6 +35,12 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayvec"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "assert_cmd"
|
name = "assert_cmd"
|
||||||
version = "1.0.8"
|
version = "1.0.8"
|
||||||
@ -294,6 +300,7 @@ dependencies = [
|
|||||||
"nu-path",
|
"nu-path",
|
||||||
"nu-protocol",
|
"nu-protocol",
|
||||||
"nu-table",
|
"nu-table",
|
||||||
|
"nu-term-grid",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"reedline",
|
"reedline",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
@ -401,6 +408,16 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lscolors"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd0aa49b10c47f9a4391a99198b5e65c74f9ca771c0dcc856bb75a3f46c8627d"
|
||||||
|
dependencies = [
|
||||||
|
"ansi_term",
|
||||||
|
"crossterm",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.4.1"
|
version = "2.4.1"
|
||||||
@ -518,13 +535,16 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"glob",
|
"glob",
|
||||||
|
"lscolors",
|
||||||
"nu-engine",
|
"nu-engine",
|
||||||
"nu-json",
|
"nu-json",
|
||||||
"nu-parser",
|
"nu-parser",
|
||||||
"nu-path",
|
"nu-path",
|
||||||
"nu-protocol",
|
"nu-protocol",
|
||||||
"nu-table",
|
"nu-table",
|
||||||
|
"nu-term-grid",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
|
"terminal_size",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -532,6 +552,7 @@ dependencies = [
|
|||||||
name = "nu-engine"
|
name = "nu-engine"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"itertools",
|
||||||
"nu-parser",
|
"nu-parser",
|
||||||
"nu-path",
|
"nu-path",
|
||||||
"nu-protocol",
|
"nu-protocol",
|
||||||
@ -588,6 +609,14 @@ dependencies = [
|
|||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-term-grid"
|
||||||
|
version = "0.36.0"
|
||||||
|
dependencies = [
|
||||||
|
"strip-ansi-escapes",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
version = "0.1.44"
|
version = "0.1.44"
|
||||||
@ -970,6 +999,15 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
|
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strip-ansi-escapes"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8"
|
||||||
|
dependencies = [
|
||||||
|
"vte",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "supports-color"
|
name = "supports-color"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@ -1048,6 +1086,16 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "terminal_size"
|
||||||
|
version = "0.1.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "textwrap"
|
name = "textwrap"
|
||||||
version = "0.14.2"
|
version = "0.14.2"
|
||||||
@ -1129,6 +1177,33 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7cf7d77f457ef8dfa11e4cd5933c5ddb5dc52a94664071951219a97710f0a32b"
|
checksum = "7cf7d77f457ef8dfa11e4cd5933c5ddb5dc52a94664071951219a97710f0a32b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vte"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"utf8parse",
|
||||||
|
"vte_generate_state_changes",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vte_generate_state_changes"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wait-timeout"
|
name = "wait-timeout"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -19,6 +19,7 @@ nu-parser = { path="./crates/nu-parser" }
|
|||||||
nu-path = { path="./crates/nu-path" }
|
nu-path = { path="./crates/nu-path" }
|
||||||
nu-protocol = { path = "./crates/nu-protocol" }
|
nu-protocol = { path = "./crates/nu-protocol" }
|
||||||
nu-table = { path = "./crates/nu-table" }
|
nu-table = { path = "./crates/nu-table" }
|
||||||
|
nu-term-grid = { path = "./crates/nu-term-grid" }
|
||||||
miette = "3.0.0"
|
miette = "3.0.0"
|
||||||
# mimalloc = { version = "*", default-features = false }
|
# mimalloc = { version = "*", default-features = false }
|
||||||
|
|
||||||
|
@ -39,8 +39,12 @@ impl Highlighter for NuHighlighter {
|
|||||||
.to_string();
|
.to_string();
|
||||||
match shape.1 {
|
match shape.1 {
|
||||||
FlatShape::Custom(..) => output.push((Style::new().bold(), next_token)),
|
FlatShape::Custom(..) => output.push((Style::new().bold(), next_token)),
|
||||||
FlatShape::External => output.push((Style::new().bold(), next_token)),
|
FlatShape::External => {
|
||||||
FlatShape::ExternalArg => output.push((Style::new().bold(), next_token)),
|
output.push((Style::new().fg(nu_ansi_term::Color::Green), next_token))
|
||||||
|
}
|
||||||
|
FlatShape::ExternalArg => {
|
||||||
|
output.push((Style::new().fg(nu_ansi_term::Color::Green), next_token))
|
||||||
|
}
|
||||||
FlatShape::Garbage => output.push((
|
FlatShape::Garbage => output.push((
|
||||||
Style::new()
|
Style::new()
|
||||||
.fg(nu_ansi_term::Color::White)
|
.fg(nu_ansi_term::Color::White)
|
||||||
|
@ -11,10 +11,13 @@ nu-json = { path = "../nu-json" }
|
|||||||
nu-path = { path = "../nu-path" }
|
nu-path = { path = "../nu-path" }
|
||||||
nu-protocol = { path = "../nu-protocol" }
|
nu-protocol = { path = "../nu-protocol" }
|
||||||
nu-table = { path = "../nu-table" }
|
nu-table = { path = "../nu-table" }
|
||||||
|
nu-term-grid = { path = "../nu-term-grid" }
|
||||||
nu-parser = { path = "../nu-parser" }
|
nu-parser = { path = "../nu-parser" }
|
||||||
|
|
||||||
# Potential dependencies for extras
|
# Potential dependencies for extras
|
||||||
glob = "0.3.0"
|
glob = "0.3.0"
|
||||||
thiserror = "1.0.29"
|
thiserror = "1.0.29"
|
||||||
sysinfo = "0.20.4"
|
sysinfo = "0.20.4"
|
||||||
chrono = { version="0.4.19", features=["serde"] }
|
chrono = { version = "0.4.19", features = ["serde"] }
|
||||||
|
terminal_size = "0.1.17"
|
||||||
|
lscolors = { version = "0.8.0", features = ["crossterm"] }
|
||||||
|
@ -15,7 +15,7 @@ impl Command for Do {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn signature(&self) -> nu_protocol::Signature {
|
fn signature(&self) -> nu_protocol::Signature {
|
||||||
Signature::build("do").required(
|
Signature::build("do").desc(self.usage()).required(
|
||||||
"block",
|
"block",
|
||||||
SyntaxShape::Block(Some(vec![])),
|
SyntaxShape::Block(Some(vec![])),
|
||||||
"the block to run",
|
"the block to run",
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::Call,
|
ast::Call,
|
||||||
engine::{Command, EvaluationContext},
|
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;
|
pub struct Help;
|
||||||
|
|
||||||
@ -73,11 +73,11 @@ impl Command for Help {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn help(context: &EvaluationContext, call: &Call) -> Result<Value, ShellError> {
|
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 find: Option<Spanned<String>> = call.get_flag(context, "find")?;
|
||||||
let rest: Vec<Spanned<String>> = call.rest(context, 0)?;
|
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 {
|
if let Some(f) = find {
|
||||||
let search_string = f.item;
|
let search_string = f.item;
|
||||||
@ -87,29 +87,36 @@ fn help(context: &EvaluationContext, call: &Call) -> Result<Value, ShellError> {
|
|||||||
let mut cols = vec![];
|
let mut cols = vec![];
|
||||||
let mut vals = vec![];
|
let mut vals = vec![];
|
||||||
|
|
||||||
let key = cmd.name.clone();
|
let key = cmd.0.name.clone();
|
||||||
let c = cmd.usage.clone();
|
let c = cmd.0.usage.clone();
|
||||||
let e = cmd.extra_usage.clone();
|
let e = cmd.0.extra_usage.clone();
|
||||||
if key.to_lowercase().contains(&search_string)
|
if key.to_lowercase().contains(&search_string)
|
||||||
|| c.to_lowercase().contains(&search_string)
|
|| c.to_lowercase().contains(&search_string)
|
||||||
|| e.to_lowercase().contains(&search_string)
|
|| e.to_lowercase().contains(&search_string)
|
||||||
{
|
{
|
||||||
cols.push("name".into());
|
cols.push("name".into());
|
||||||
vals.push(Value::String { val: key, span });
|
vals.push(Value::String {
|
||||||
|
val: key,
|
||||||
|
span: head,
|
||||||
|
});
|
||||||
|
|
||||||
cols.push("usage".into());
|
cols.push("usage".into());
|
||||||
vals.push(Value::String { val: c, span });
|
vals.push(Value::String { val: c, span: head });
|
||||||
|
|
||||||
cols.push("extra_usage".into());
|
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 {
|
return Ok(Value::List {
|
||||||
vals: found_cmds_vec,
|
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 cols = vec![];
|
||||||
let mut vals = vec![];
|
let mut vals = vec![];
|
||||||
|
|
||||||
let key = cmd.name.clone();
|
let key = cmd.0.name.clone();
|
||||||
let c = cmd.usage.clone();
|
let c = cmd.0.usage.clone();
|
||||||
let e = cmd.extra_usage.clone();
|
let e = cmd.0.extra_usage.clone();
|
||||||
|
|
||||||
cols.push("name".into());
|
cols.push("name".into());
|
||||||
vals.push(Value::String { val: key, span });
|
vals.push(Value::String {
|
||||||
|
val: key,
|
||||||
|
span: head,
|
||||||
|
});
|
||||||
|
|
||||||
cols.push("usage".into());
|
cols.push("usage".into());
|
||||||
vals.push(Value::String { val: c, span });
|
vals.push(Value::String { val: c, span: head });
|
||||||
|
|
||||||
cols.push("extra_usage".into());
|
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 {
|
} else {
|
||||||
let mut name = String::new();
|
let mut name = String::new();
|
||||||
|
let mut output = String::new();
|
||||||
|
|
||||||
for r in rest {
|
for r in &rest {
|
||||||
if !name.is_empty() {
|
if !name.is_empty() {
|
||||||
name.push(' ');
|
name.push(' ');
|
||||||
}
|
}
|
||||||
@ -147,31 +167,24 @@ fn help(context: &EvaluationContext, call: &Call) -> Result<Value, ShellError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for cmd in full_commands {
|
for cmd in full_commands {
|
||||||
let mut cols = vec![];
|
if cmd.0.name == name {
|
||||||
let mut vals = vec![];
|
let help = get_full_help(&cmd.0, &cmd.1, context);
|
||||||
|
output.push_str(&help);
|
||||||
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 !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
|
// 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 {
|
Ok(Value::String {
|
||||||
val: msg.into(),
|
val: msg.into(),
|
||||||
span,
|
span: head,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ pub fn create_default_context() -> Rc<RefCell<EngineState>> {
|
|||||||
working_set.add_decl(Box::new(From));
|
working_set.add_decl(Box::new(From));
|
||||||
working_set.add_decl(Box::new(FromJson));
|
working_set.add_decl(Box::new(FromJson));
|
||||||
working_set.add_decl(Box::new(Get));
|
working_set.add_decl(Box::new(Get));
|
||||||
|
working_set.add_decl(Box::new(Griddle));
|
||||||
working_set.add_decl(Box::new(Help));
|
working_set.add_decl(Box::new(Help));
|
||||||
working_set.add_decl(Box::new(Hide));
|
working_set.add_decl(Box::new(Hide));
|
||||||
working_set.add_decl(Box::new(If));
|
working_set.add_decl(Box::new(If));
|
||||||
@ -35,12 +36,18 @@ pub fn create_default_context() -> Rc<RefCell<EngineState>> {
|
|||||||
working_set.add_decl(Box::new(LetEnv));
|
working_set.add_decl(Box::new(LetEnv));
|
||||||
working_set.add_decl(Box::new(Lines));
|
working_set.add_decl(Box::new(Lines));
|
||||||
working_set.add_decl(Box::new(Ls));
|
working_set.add_decl(Box::new(Ls));
|
||||||
|
working_set.add_decl(Box::new(Mkdir));
|
||||||
working_set.add_decl(Box::new(Module));
|
working_set.add_decl(Box::new(Module));
|
||||||
working_set.add_decl(Box::new(Mv));
|
working_set.add_decl(Box::new(Mv));
|
||||||
working_set.add_decl(Box::new(Ps));
|
working_set.add_decl(Box::new(Ps));
|
||||||
working_set.add_decl(Box::new(Select));
|
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(SplitColumn));
|
||||||
|
working_set.add_decl(Box::new(SplitRow));
|
||||||
working_set.add_decl(Box::new(Sys));
|
working_set.add_decl(Box::new(Sys));
|
||||||
working_set.add_decl(Box::new(Table));
|
working_set.add_decl(Box::new(Table));
|
||||||
|
working_set.add_decl(Box::new(Touch));
|
||||||
working_set.add_decl(Box::new(Use));
|
working_set.add_decl(Box::new(Use));
|
||||||
working_set.add_decl(Box::new(Where));
|
working_set.add_decl(Box::new(Where));
|
||||||
working_set.add_decl(Box::new(Wrap));
|
working_set.add_decl(Box::new(Wrap));
|
||||||
|
@ -60,7 +60,7 @@ impl Command for Cp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let any_source_is_dir = sources.iter().any(|f| matches!(f, Ok(f) if f.is_dir()));
|
let any_source_is_dir = sources.iter().any(|f| matches!(f, Ok(f) if f.is_dir()));
|
||||||
let recursive = call.named.iter().any(|p| &p.0 == "recursive");
|
let recursive: bool = call.has_flag("recursive");
|
||||||
if any_source_is_dir && !recursive {
|
if any_source_is_dir && !recursive {
|
||||||
return Err(ShellError::MoveNotPossibleSingle(
|
return Err(ShellError::MoveNotPossibleSingle(
|
||||||
"Directories must be copied using \"--recursive\"".to_string(),
|
"Directories must be copied using \"--recursive\"".to_string(),
|
||||||
|
74
crates/nu-command/src/filesystem/mkdir.rs
Normal file
74
crates/nu-command/src/filesystem/mkdir.rs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::env::current_dir;
|
||||||
|
|
||||||
|
use nu_engine::CallExt;
|
||||||
|
use nu_protocol::ast::Call;
|
||||||
|
use nu_protocol::engine::{Command, EvaluationContext};
|
||||||
|
use nu_protocol::{ShellError, Signature, SyntaxShape, Value, ValueStream};
|
||||||
|
|
||||||
|
pub struct Mkdir;
|
||||||
|
|
||||||
|
impl Command for Mkdir {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"mkdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> Signature {
|
||||||
|
Signature::build("mkdir")
|
||||||
|
.rest(
|
||||||
|
"rest",
|
||||||
|
SyntaxShape::Filepath,
|
||||||
|
"the name(s) of the path(s) to create",
|
||||||
|
)
|
||||||
|
.switch("show-created-paths", "show the path(s) created.", Some('s'))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage(&self) -> &str {
|
||||||
|
"Make directories, creates intermediary directories as required."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
context: &EvaluationContext,
|
||||||
|
call: &Call,
|
||||||
|
_input: Value,
|
||||||
|
) -> Result<Value, ShellError> {
|
||||||
|
let path = current_dir()?;
|
||||||
|
let mut directories = call
|
||||||
|
.rest::<String>(context, 0)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|dir| path.join(dir))
|
||||||
|
.peekable();
|
||||||
|
|
||||||
|
let show_created_paths = call.has_flag("show-created-paths");
|
||||||
|
let mut stream: VecDeque<Value> = VecDeque::new();
|
||||||
|
|
||||||
|
if directories.peek().is_none() {
|
||||||
|
return Err(ShellError::MissingParameter(
|
||||||
|
"requires directory paths".to_string(),
|
||||||
|
call.head,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, dir) in directories.enumerate() {
|
||||||
|
let span = call.positional[i].span;
|
||||||
|
let dir_res = std::fs::create_dir_all(&dir);
|
||||||
|
|
||||||
|
if let Err(reason) = dir_res {
|
||||||
|
return Err(ShellError::CreateNotPossible(
|
||||||
|
format!("failed to create directory: {}", reason),
|
||||||
|
call.positional[i].span,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if show_created_paths {
|
||||||
|
let val = format!("{:}", dir.to_string_lossy());
|
||||||
|
stream.push_back(Value::String { val, span });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let stream = ValueStream::from_stream(stream.into_iter());
|
||||||
|
let span = call.head;
|
||||||
|
Ok(Value::Stream { stream, span })
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,14 @@
|
|||||||
mod cd;
|
mod cd;
|
||||||
mod cp;
|
mod cp;
|
||||||
mod ls;
|
mod ls;
|
||||||
|
mod mkdir;
|
||||||
mod mv;
|
mod mv;
|
||||||
|
mod touch;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
pub use cd::Cd;
|
pub use cd::Cd;
|
||||||
pub use cp::Cp;
|
pub use cp::Cp;
|
||||||
pub use ls::Ls;
|
pub use ls::Ls;
|
||||||
|
pub use mkdir::Mkdir;
|
||||||
pub use mv::Mv;
|
pub use mv::Mv;
|
||||||
|
pub use touch::Touch;
|
||||||
|
52
crates/nu-command/src/filesystem/touch.rs
Normal file
52
crates/nu-command/src/filesystem/touch.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
use std::fs::OpenOptions;
|
||||||
|
|
||||||
|
use nu_engine::CallExt;
|
||||||
|
use nu_protocol::ast::Call;
|
||||||
|
use nu_protocol::engine::{Command, EvaluationContext};
|
||||||
|
use nu_protocol::{ShellError, Signature, SyntaxShape, Value};
|
||||||
|
|
||||||
|
pub struct Touch;
|
||||||
|
|
||||||
|
impl Command for Touch {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"touch"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> Signature {
|
||||||
|
Signature::build("touch")
|
||||||
|
.required(
|
||||||
|
"filename",
|
||||||
|
SyntaxShape::Filepath,
|
||||||
|
"the path of the file you want to create",
|
||||||
|
)
|
||||||
|
.rest("rest", SyntaxShape::Filepath, "additional files to create")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage(&self) -> &str {
|
||||||
|
"Creates one or more files."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
context: &EvaluationContext,
|
||||||
|
call: &Call,
|
||||||
|
_input: Value,
|
||||||
|
) -> Result<Value, ShellError> {
|
||||||
|
let target: String = call.req(context, 0)?;
|
||||||
|
let rest: Vec<String> = call.rest(context, 1)?;
|
||||||
|
|
||||||
|
for (index, item) in vec![target].into_iter().chain(rest).enumerate() {
|
||||||
|
match OpenOptions::new().write(true).create(true).open(&item) {
|
||||||
|
Ok(_) => continue,
|
||||||
|
Err(err) => {
|
||||||
|
return Err(ShellError::CreateNotPossible(
|
||||||
|
format!("Failed to create file: {}", err),
|
||||||
|
call.positional[index].span,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::Nothing { span: call.head })
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
mod build_string;
|
mod build_string;
|
||||||
|
mod split;
|
||||||
|
|
||||||
pub use build_string::BuildString;
|
pub use build_string::BuildString;
|
||||||
|
pub use split::*;
|
||||||
|
100
crates/nu-command/src/strings/split/chars.rs
Normal file
100
crates/nu-command/src/strings/split/chars.rs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
use nu_protocol::{
|
||||||
|
ast::Call,
|
||||||
|
engine::{Command, EvaluationContext},
|
||||||
|
Example, 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 span = call.head;
|
||||||
|
|
||||||
|
Ok(input.flat_map(span, move |x| split_chars_helper(&x, span)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_chars_helper(v: &Value, name: Span) -> Vec<Value> {
|
||||||
|
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(),
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[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 {})
|
||||||
|
// }
|
||||||
|
// }
|
130
crates/nu-command/src/strings/split/column.rs
Normal file
130
crates/nu-command/src/strings/split/column.rs
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
use nu_engine::CallExt;
|
||||||
|
use nu_protocol::{
|
||||||
|
ast::Call,
|
||||||
|
engine::{Command, EvaluationContext},
|
||||||
|
ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct SubCommand;
|
||||||
|
|
||||||
|
impl Command for SubCommand {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"split column"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> Signature {
|
||||||
|
Signature::build("split column")
|
||||||
|
.required(
|
||||||
|
"separator",
|
||||||
|
SyntaxShape::String,
|
||||||
|
"the character that denotes what separates columns",
|
||||||
|
)
|
||||||
|
.switch("collapse-empty", "remove empty columns", Some('c'))
|
||||||
|
.rest(
|
||||||
|
"rest",
|
||||||
|
SyntaxShape::String,
|
||||||
|
"column names to give the new columns",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage(&self) -> &str {
|
||||||
|
"splits contents across multiple columns via the separator."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
context: &EvaluationContext,
|
||||||
|
call: &Call,
|
||||||
|
input: Value,
|
||||||
|
) -> Result<nu_protocol::Value, nu_protocol::ShellError> {
|
||||||
|
split_column(context, call, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_column(
|
||||||
|
context: &EvaluationContext,
|
||||||
|
call: &Call,
|
||||||
|
input: Value,
|
||||||
|
) -> Result<nu_protocol::Value, nu_protocol::ShellError> {
|
||||||
|
let name_span = call.head;
|
||||||
|
let separator: Spanned<String> = call.req(context, 0)?;
|
||||||
|
let rest: Vec<Spanned<String>> = call.rest(context, 1)?;
|
||||||
|
let collapse_empty = call.has_flag("collapse-empty");
|
||||||
|
|
||||||
|
Ok(input.map(name_span, move |x| {
|
||||||
|
split_column_helper(&x, &separator, &rest, collapse_empty, name_span)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_column_helper(
|
||||||
|
v: &Value,
|
||||||
|
separator: &Spanned<String>,
|
||||||
|
rest: &[Spanned<String>],
|
||||||
|
collapse_empty: bool,
|
||||||
|
head: Span,
|
||||||
|
) -> Value {
|
||||||
|
if let Ok(s) = v.as_string() {
|
||||||
|
let splitter = separator.item.replace("\\n", "\n");
|
||||||
|
|
||||||
|
let split_result: Vec<_> = if collapse_empty {
|
||||||
|
s.split(&splitter).filter(|s| !s.is_empty()).collect()
|
||||||
|
} else {
|
||||||
|
s.split(&splitter).collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
let positional: Vec<_> = rest.iter().map(|f| f.item.clone()).collect();
|
||||||
|
|
||||||
|
// If they didn't provide column names, make up our own
|
||||||
|
|
||||||
|
let mut cols = vec![];
|
||||||
|
let mut vals = vec![];
|
||||||
|
if positional.is_empty() {
|
||||||
|
let mut gen_columns = vec![];
|
||||||
|
for i in 0..split_result.len() {
|
||||||
|
gen_columns.push(format!("Column{}", i + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (&k, v) in split_result.iter().zip(&gen_columns) {
|
||||||
|
cols.push(v.to_string());
|
||||||
|
vals.push(Value::String {
|
||||||
|
val: k.into(),
|
||||||
|
span: head,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (&k, v) in split_result.iter().zip(&positional) {
|
||||||
|
cols.push(v.into());
|
||||||
|
vals.push(Value::String {
|
||||||
|
val: k.into(),
|
||||||
|
span: head,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Record {
|
||||||
|
cols,
|
||||||
|
vals,
|
||||||
|
span: head,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Value::Error {
|
||||||
|
error: ShellError::PipelineMismatch {
|
||||||
|
expected: Type::String,
|
||||||
|
expected_span: head,
|
||||||
|
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 {})
|
||||||
|
// }
|
||||||
|
// }
|
48
crates/nu-command/src/strings/split/command.rs
Normal file
48
crates/nu-command/src/strings/split/command.rs
Normal 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 {})
|
||||||
|
// }
|
||||||
|
// }
|
9
crates/nu-command/src/strings/split/mod.rs
Normal file
9
crates/nu-command/src/strings/split/mod.rs
Normal 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;
|
87
crates/nu-command/src/strings/split/row.rs
Normal file
87
crates/nu-command/src/strings/split/row.rs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
use nu_engine::CallExt;
|
||||||
|
use nu_protocol::{
|
||||||
|
ast::Call,
|
||||||
|
engine::{Command, EvaluationContext},
|
||||||
|
ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct SubCommand;
|
||||||
|
|
||||||
|
impl Command for SubCommand {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"split row"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> Signature {
|
||||||
|
Signature::build("split row").required(
|
||||||
|
"separator",
|
||||||
|
SyntaxShape::String,
|
||||||
|
"the character that denotes what separates rows",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage(&self) -> &str {
|
||||||
|
"splits contents over multiple rows via the separator."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
context: &EvaluationContext,
|
||||||
|
call: &Call,
|
||||||
|
input: Value,
|
||||||
|
) -> Result<nu_protocol::Value, nu_protocol::ShellError> {
|
||||||
|
split_row(context, call, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_row(
|
||||||
|
context: &EvaluationContext,
|
||||||
|
call: &Call,
|
||||||
|
input: Value,
|
||||||
|
) -> Result<nu_protocol::Value, nu_protocol::ShellError> {
|
||||||
|
let name_span = call.head;
|
||||||
|
let separator: Spanned<String> = call.req(context, 0)?;
|
||||||
|
|
||||||
|
Ok(input.flat_map(name_span, move |x| {
|
||||||
|
split_row_helper(&x, &separator, name_span)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_row_helper(v: &Value, separator: &Spanned<String>, name: Span) -> Vec<Value> {
|
||||||
|
if let Ok(s) = v.as_string() {
|
||||||
|
let splitter = separator.item.replace("\\n", "\n");
|
||||||
|
s.split(&splitter)
|
||||||
|
.filter_map(|s| {
|
||||||
|
if s.trim() != "" {
|
||||||
|
Some(Value::String {
|
||||||
|
val: s.into(),
|
||||||
|
span: v.span(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
vec![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 {})
|
||||||
|
// }
|
||||||
|
// }
|
334
crates/nu-command/src/viewers/griddle.rs
Normal file
334
crates/nu-command/src/viewers/griddle.rs
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
use lscolors::{LsColors, Style};
|
||||||
|
use nu_engine::CallExt;
|
||||||
|
use nu_protocol::{
|
||||||
|
ast::{Call, PathMember},
|
||||||
|
engine::{Command, EvaluationContext},
|
||||||
|
Signature, Span, SyntaxShape, Value,
|
||||||
|
};
|
||||||
|
use nu_term_grid::grid::{Alignment, Cell, Direction, Filling, Grid, GridOptions};
|
||||||
|
use terminal_size::{Height, Width};
|
||||||
|
|
||||||
|
pub struct Griddle;
|
||||||
|
|
||||||
|
impl Command for Griddle {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"grid"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage(&self) -> &str {
|
||||||
|
"Renders the output to a textual terminal grid."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> nu_protocol::Signature {
|
||||||
|
Signature::build("grid")
|
||||||
|
.named(
|
||||||
|
"width",
|
||||||
|
SyntaxShape::Int,
|
||||||
|
"number of columns wide",
|
||||||
|
Some('w'),
|
||||||
|
)
|
||||||
|
.switch("color", "draw output with color", Some('c'))
|
||||||
|
.named(
|
||||||
|
"separator",
|
||||||
|
SyntaxShape::String,
|
||||||
|
"character to separate grid with",
|
||||||
|
Some('s'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_usage(&self) -> &str {
|
||||||
|
r#"grid was built to give a concise gridded layout for ls. however,
|
||||||
|
it determines what to put in the grid by looking for a column named
|
||||||
|
'name'. this works great for tables and records but for lists we
|
||||||
|
need to do something different. such as with '[one two three] | grid'
|
||||||
|
it creates a fake column called 'name' for these values so that it
|
||||||
|
prints out the list properly."#
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
context: &EvaluationContext,
|
||||||
|
call: &Call,
|
||||||
|
input: Value,
|
||||||
|
) -> Result<nu_protocol::Value, nu_protocol::ShellError> {
|
||||||
|
let width_param: Option<String> = call.get_flag(context, "width")?;
|
||||||
|
let color_param: bool = call.has_flag("color");
|
||||||
|
let seperator_param: Option<String> = call.get_flag(context, "separator")?;
|
||||||
|
|
||||||
|
match input {
|
||||||
|
Value::List { vals, .. } => {
|
||||||
|
// dbg!("value::list");
|
||||||
|
let data = convert_to_list2(vals);
|
||||||
|
if let Some(items) = data {
|
||||||
|
Ok(create_grid_output2(
|
||||||
|
items,
|
||||||
|
call,
|
||||||
|
width_param,
|
||||||
|
color_param,
|
||||||
|
seperator_param,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(Value::Nothing { span: call.head })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Stream { stream, .. } => {
|
||||||
|
// dbg!("value::stream");
|
||||||
|
let data = convert_to_list2(stream);
|
||||||
|
if let Some(items) = data {
|
||||||
|
Ok(create_grid_output2(
|
||||||
|
items,
|
||||||
|
call,
|
||||||
|
width_param,
|
||||||
|
color_param,
|
||||||
|
seperator_param,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
// dbg!(data);
|
||||||
|
Ok(Value::Nothing { span: call.head })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Record { cols, vals, .. } => {
|
||||||
|
// dbg!("value::record");
|
||||||
|
let mut items = vec![];
|
||||||
|
|
||||||
|
for (i, (c, v)) in cols.into_iter().zip(vals.into_iter()).enumerate() {
|
||||||
|
items.push((i, c, v.into_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(create_grid_output2(
|
||||||
|
items,
|
||||||
|
call,
|
||||||
|
width_param,
|
||||||
|
color_param,
|
||||||
|
seperator_param,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
x => {
|
||||||
|
// dbg!("other value");
|
||||||
|
// dbg!(x.get_type());
|
||||||
|
Ok(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_grid_output2(
|
||||||
|
items: Vec<(usize, String, String)>,
|
||||||
|
call: &Call,
|
||||||
|
width_param: Option<String>,
|
||||||
|
color_param: bool,
|
||||||
|
separator_param: Option<String>,
|
||||||
|
) -> Value {
|
||||||
|
let ls_colors = LsColors::from_env().unwrap_or_default();
|
||||||
|
let cols = if let Some(col) = width_param {
|
||||||
|
col.parse::<u16>().unwrap_or(80)
|
||||||
|
} else if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() {
|
||||||
|
w
|
||||||
|
} else {
|
||||||
|
80u16
|
||||||
|
};
|
||||||
|
let sep = if let Some(seperator) = separator_param {
|
||||||
|
seperator
|
||||||
|
} else {
|
||||||
|
" │ ".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut grid = Grid::new(GridOptions {
|
||||||
|
direction: Direction::TopToBottom,
|
||||||
|
filling: Filling::Text(sep),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (_row_index, header, value) in items {
|
||||||
|
// only output value if the header name is 'name'
|
||||||
|
if header == "name" {
|
||||||
|
if color_param {
|
||||||
|
let style = ls_colors.style_for_path(value.clone());
|
||||||
|
let ansi_style = style.map(Style::to_crossterm_style).unwrap_or_default();
|
||||||
|
let mut cell = Cell::from(ansi_style.apply(value).to_string());
|
||||||
|
cell.alignment = Alignment::Right;
|
||||||
|
grid.add(cell);
|
||||||
|
} else {
|
||||||
|
let mut cell = Cell::from(value);
|
||||||
|
cell.alignment = Alignment::Right;
|
||||||
|
grid.add(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(grid_display) = grid.fit_into_width(cols as usize) {
|
||||||
|
Value::String {
|
||||||
|
val: grid_display.to_string(),
|
||||||
|
span: call.head,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Value::String {
|
||||||
|
val: format!("Couldn't fit grid into {} columns!", cols),
|
||||||
|
span: call.head,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn create_grid_output(
|
||||||
|
// items: Vec<Vec<String>>,
|
||||||
|
// call: &Call,
|
||||||
|
// columns_param: Option<String>,
|
||||||
|
// ) -> Value {
|
||||||
|
// let mut grid = Grid::new(GridOptions {
|
||||||
|
// direction: Direction::TopToBottom,
|
||||||
|
// filling: Filling::Text(" | ".into()),
|
||||||
|
// });
|
||||||
|
|
||||||
|
// for list in items {
|
||||||
|
// dbg!(&list);
|
||||||
|
// // looks like '&list = [ "0", "one",]'
|
||||||
|
// let a_string = (&list[1]).to_string();
|
||||||
|
// let mut cell = Cell::from(a_string);
|
||||||
|
// cell.alignment = Alignment::Right;
|
||||||
|
// grid.add(cell);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let cols = if let Some(col) = columns_param {
|
||||||
|
// col.parse::<u16>().unwrap_or(80)
|
||||||
|
// } else {
|
||||||
|
// // 80usize
|
||||||
|
// if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() {
|
||||||
|
// w
|
||||||
|
// } else {
|
||||||
|
// 80u16
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // eprintln!("columns size = {}", cols);
|
||||||
|
// if let Some(grid_display) = grid.fit_into_width(cols as usize) {
|
||||||
|
// // println!("{}", grid_display);
|
||||||
|
// Value::String {
|
||||||
|
// val: grid_display.to_string(),
|
||||||
|
// span: call.head,
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// // println!("Couldn't fit grid into 80 columns!");
|
||||||
|
// Value::String {
|
||||||
|
// val: format!("Couldn't fit grid into {} columns!", cols),
|
||||||
|
// span: call.head,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
fn convert_to_list2(iter: impl IntoIterator<Item = Value>) -> Option<Vec<(usize, String, String)>> {
|
||||||
|
let mut iter = iter.into_iter().peekable();
|
||||||
|
|
||||||
|
if let Some(first) = iter.peek() {
|
||||||
|
let mut headers = first.columns();
|
||||||
|
|
||||||
|
if !headers.is_empty() {
|
||||||
|
headers.insert(0, "#".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut data = vec![];
|
||||||
|
|
||||||
|
for (row_num, item) in iter.enumerate() {
|
||||||
|
let mut row = vec![row_num.to_string()];
|
||||||
|
|
||||||
|
if headers.is_empty() {
|
||||||
|
row.push(item.into_string())
|
||||||
|
} else {
|
||||||
|
for header in headers.iter().skip(1) {
|
||||||
|
let result = match item {
|
||||||
|
Value::Record { .. } => {
|
||||||
|
item.clone().follow_cell_path(&[PathMember::String {
|
||||||
|
val: header.into(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
_ => Ok(item.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(value) => row.push(value.into_string()),
|
||||||
|
Err(_) => row.push(String::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: later, let's color these string with LS_COLORS
|
||||||
|
// let h: Vec<String> = headers.into_iter().map(|x| x.trim().to_string()).collect();
|
||||||
|
// let d: Vec<Vec<String>> = data.into_iter().map(|x| x.into_iter().collect()).collect();
|
||||||
|
|
||||||
|
let mut h: Vec<String> = headers.into_iter().collect();
|
||||||
|
// let d: Vec<Vec<String>> = data.into_iter().collect();
|
||||||
|
|
||||||
|
// This is just a list
|
||||||
|
if h.is_empty() {
|
||||||
|
// let's fake the header
|
||||||
|
h.push("#".to_string());
|
||||||
|
h.push("name".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// this tuple is (row_index, header_name, value)
|
||||||
|
let mut interleaved = vec![];
|
||||||
|
for (i, v) in data.into_iter().enumerate() {
|
||||||
|
for (n, s) in v.into_iter().enumerate() {
|
||||||
|
if h.len() == 1 {
|
||||||
|
// always get the 1th element since this is a simple list
|
||||||
|
// and we hacked the header above because it was empty
|
||||||
|
// 0th element is an index, 1th element is the value
|
||||||
|
interleaved.push((i, h[1].clone(), s))
|
||||||
|
} else {
|
||||||
|
interleaved.push((i, h[n].clone(), s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(interleaved)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn convert_to_list(iter: impl IntoIterator<Item = Value>) -> Option<Vec<Vec<String>>> {
|
||||||
|
// let mut iter = iter.into_iter().peekable();
|
||||||
|
// let mut data = vec![];
|
||||||
|
|
||||||
|
// if let Some(first) = iter.peek() {
|
||||||
|
// // dbg!(&first);
|
||||||
|
// let mut headers = first.columns();
|
||||||
|
|
||||||
|
// if !headers.is_empty() {
|
||||||
|
// headers.insert(0, "#".into());
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for (row_num, item) in iter.enumerate() {
|
||||||
|
// let mut row = vec![row_num.to_string()];
|
||||||
|
|
||||||
|
// if headers.is_empty() {
|
||||||
|
// row.push(item.into_string())
|
||||||
|
// } else {
|
||||||
|
// for header in headers.iter().skip(1) {
|
||||||
|
// let result = match item {
|
||||||
|
// Value::Record { .. } => {
|
||||||
|
// item.clone().follow_cell_path(&[PathMember::String {
|
||||||
|
// val: header.into(),
|
||||||
|
// span: Span::unknown(),
|
||||||
|
// }])
|
||||||
|
// }
|
||||||
|
// _ => Ok(item.clone()),
|
||||||
|
// };
|
||||||
|
|
||||||
|
// match result {
|
||||||
|
// Ok(value) => row.push(value.into_string()),
|
||||||
|
// Err(_) => row.push(String::new()),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// data.push(row);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Some(data)
|
||||||
|
// } else {
|
||||||
|
// None
|
||||||
|
// }
|
||||||
|
// }
|
@ -1,3 +1,5 @@
|
|||||||
|
mod griddle;
|
||||||
mod table;
|
mod table;
|
||||||
|
|
||||||
|
pub use griddle::Griddle;
|
||||||
pub use table::Table;
|
pub use table::Table;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use nu_protocol::ast::{Call, PathMember};
|
use nu_protocol::ast::{Call, PathMember};
|
||||||
use nu_protocol::engine::{Command, EvaluationContext};
|
use nu_protocol::engine::{Command, EvaluationContext};
|
||||||
use nu_protocol::{Signature, Span, Value};
|
use nu_protocol::{Signature, Span, Value};
|
||||||
use nu_table::StyledString;
|
use nu_table::StyledString;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use terminal_size::{Height, Width};
|
||||||
|
|
||||||
pub struct Table;
|
pub struct Table;
|
||||||
|
|
||||||
@ -27,12 +27,18 @@ impl Command for Table {
|
|||||||
call: &Call,
|
call: &Call,
|
||||||
input: Value,
|
input: Value,
|
||||||
) -> Result<nu_protocol::Value, nu_protocol::ShellError> {
|
) -> Result<nu_protocol::Value, nu_protocol::ShellError> {
|
||||||
|
let term_width = if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() {
|
||||||
|
w as usize
|
||||||
|
} else {
|
||||||
|
80usize
|
||||||
|
};
|
||||||
|
|
||||||
match input {
|
match input {
|
||||||
Value::List { vals, .. } => {
|
Value::List { vals, .. } => {
|
||||||
let table = convert_to_table(vals);
|
let table = convert_to_table(vals);
|
||||||
|
|
||||||
if let Some(table) = table {
|
if let Some(table) = table {
|
||||||
let result = nu_table::draw_table(&table, 80, &HashMap::new());
|
let result = nu_table::draw_table(&table, term_width, &HashMap::new());
|
||||||
|
|
||||||
Ok(Value::String {
|
Ok(Value::String {
|
||||||
val: result,
|
val: result,
|
||||||
@ -46,7 +52,7 @@ impl Command for Table {
|
|||||||
let table = convert_to_table(stream);
|
let table = convert_to_table(stream);
|
||||||
|
|
||||||
if let Some(table) = table {
|
if let Some(table) = table {
|
||||||
let result = nu_table::draw_table(&table, 80, &HashMap::new());
|
let result = nu_table::draw_table(&table, term_width, &HashMap::new());
|
||||||
|
|
||||||
Ok(Value::String {
|
Ok(Value::String {
|
||||||
val: result,
|
val: result,
|
||||||
@ -78,7 +84,7 @@ impl Command for Table {
|
|||||||
theme: nu_table::Theme::rounded(),
|
theme: nu_table::Theme::rounded(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = nu_table::draw_table(&table, 80, &HashMap::new());
|
let result = nu_table::draw_table(&table, term_width, &HashMap::new());
|
||||||
|
|
||||||
Ok(Value::String {
|
Ok(Value::String {
|
||||||
val: result,
|
val: result,
|
||||||
|
@ -6,4 +6,5 @@ edition = "2018"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
nu-parser = { path = "../nu-parser" }
|
nu-parser = { path = "../nu-parser" }
|
||||||
nu-protocol = { path = "../nu-protocol" }
|
nu-protocol = { path = "../nu-protocol" }
|
||||||
nu-path = { path = "../nu-path" }
|
nu-path = { path = "../nu-path" }
|
||||||
|
itertools = "0.10.1"
|
315
crates/nu-engine/src/documentation.rs
Normal file
315
crates/nu-engine/src/documentation.rs
Normal 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())
|
||||||
|
}
|
@ -70,8 +70,9 @@ fn eval_call(context: &EvaluationContext, call: &Call, input: Value) -> Result<V
|
|||||||
|
|
||||||
fn eval_external(
|
fn eval_external(
|
||||||
context: &EvaluationContext,
|
context: &EvaluationContext,
|
||||||
name: &Span,
|
name: &str,
|
||||||
args: &[Span],
|
name_span: &Span,
|
||||||
|
args: &[Expression],
|
||||||
input: Value,
|
input: Value,
|
||||||
last_expression: bool,
|
last_expression: bool,
|
||||||
) -> Result<Value, ShellError> {
|
) -> Result<Value, ShellError> {
|
||||||
@ -79,25 +80,22 @@ fn eval_external(
|
|||||||
|
|
||||||
let decl_id = engine_state
|
let decl_id = engine_state
|
||||||
.find_decl("run_external".as_bytes())
|
.find_decl("run_external".as_bytes())
|
||||||
.ok_or_else(|| ShellError::ExternalNotSupported(*name))?;
|
.ok_or_else(|| ShellError::ExternalNotSupported(*name_span))?;
|
||||||
|
|
||||||
let command = engine_state.get_decl(decl_id);
|
let command = engine_state.get_decl(decl_id);
|
||||||
|
|
||||||
let mut call = Call::new();
|
let mut call = Call::new();
|
||||||
call.positional = [*name]
|
|
||||||
.iter()
|
call.positional.push(Expression {
|
||||||
.chain(args.iter())
|
expr: Expr::String(name.trim_start_matches('^').to_string()),
|
||||||
.map(|span| {
|
span: *name_span,
|
||||||
let contents = engine_state.get_span_contents(span);
|
ty: Type::String,
|
||||||
let val = String::from_utf8_lossy(contents);
|
custom_completion: None,
|
||||||
Expression {
|
});
|
||||||
expr: Expr::String(val.into()),
|
|
||||||
span: *span,
|
for arg in args {
|
||||||
ty: Type::String,
|
call.positional.push(arg.clone())
|
||||||
custom_completion: None,
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if last_expression {
|
if last_expression {
|
||||||
call.named.push(("last_expression".into(), None))
|
call.named.push(("last_expression".into(), None))
|
||||||
@ -171,8 +169,8 @@ pub fn eval_expression(
|
|||||||
}
|
}
|
||||||
Expr::RowCondition(_, expr) => eval_expression(context, expr),
|
Expr::RowCondition(_, expr) => eval_expression(context, expr),
|
||||||
Expr::Call(call) => eval_call(context, call, Value::nothing()),
|
Expr::Call(call) => eval_call(context, call, Value::nothing()),
|
||||||
Expr::ExternalCall(name, args) => {
|
Expr::ExternalCall(name, span, args) => {
|
||||||
eval_external(context, name, args, Value::nothing(), true)
|
eval_external(context, name, span, args, Value::nothing(), true)
|
||||||
}
|
}
|
||||||
Expr::Operator(_) => Ok(Value::Nothing { span: expr.span }),
|
Expr::Operator(_) => Ok(Value::Nothing { span: expr.span }),
|
||||||
Expr::BinaryOp(lhs, op, rhs) => {
|
Expr::BinaryOp(lhs, op, rhs) => {
|
||||||
@ -273,12 +271,13 @@ pub fn eval_block(
|
|||||||
input = eval_call(context, call, input)?;
|
input = eval_call(context, call, input)?;
|
||||||
}
|
}
|
||||||
Expression {
|
Expression {
|
||||||
expr: Expr::ExternalCall(name, args),
|
expr: Expr::ExternalCall(name, name_span, args),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
input = eval_external(
|
input = eval_external(
|
||||||
context,
|
context,
|
||||||
name,
|
name,
|
||||||
|
name_span,
|
||||||
args,
|
args,
|
||||||
input,
|
input,
|
||||||
i == pipeline.expressions.len() - 1,
|
i == pipeline.expressions.len() - 1,
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
mod call_ext;
|
mod call_ext;
|
||||||
|
mod documentation;
|
||||||
mod eval;
|
mod eval;
|
||||||
mod from_value;
|
mod from_value;
|
||||||
|
|
||||||
pub use call_ext::CallExt;
|
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 eval::{eval_block, eval_expression, eval_operator};
|
||||||
pub use from_value::FromValue;
|
pub use from_value::FromValue;
|
||||||
|
@ -63,11 +63,23 @@ pub fn flatten_expression(
|
|||||||
}
|
}
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
Expr::ExternalCall(name, args) => {
|
Expr::ExternalCall(_, name_span, args) => {
|
||||||
let mut output = vec![(*name, FlatShape::External)];
|
let mut output = vec![(*name_span, FlatShape::External)];
|
||||||
|
|
||||||
for arg in args {
|
for arg in args {
|
||||||
output.push((*arg, FlatShape::ExternalArg));
|
//output.push((*arg, FlatShape::ExternalArg));
|
||||||
|
match arg {
|
||||||
|
Expression {
|
||||||
|
expr: Expr::String(..),
|
||||||
|
span,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
output.push((*span, FlatShape::ExternalArg));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
output.extend(flatten_expression(working_set, arg));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
output
|
output
|
||||||
|
@ -104,23 +104,38 @@ pub fn check_name<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_external_call(
|
pub fn parse_external_call(
|
||||||
_working_set: &mut StateWorkingSet,
|
working_set: &mut StateWorkingSet,
|
||||||
spans: &[Span],
|
spans: &[Span],
|
||||||
) -> (Expression, Option<ParseError>) {
|
) -> (Expression, Option<ParseError>) {
|
||||||
// TODO: add external parsing
|
|
||||||
let mut args = vec![];
|
let mut args = vec![];
|
||||||
let name = spans[0];
|
let name_span = spans[0];
|
||||||
|
let name = String::from_utf8_lossy(working_set.get_span_contents(name_span)).to_string();
|
||||||
|
let mut error = None;
|
||||||
|
|
||||||
for span in &spans[1..] {
|
for span in &spans[1..] {
|
||||||
args.push(*span);
|
let contents = working_set.get_span_contents(*span);
|
||||||
|
|
||||||
|
if contents.starts_with(b"$") || contents.starts_with(b"(") {
|
||||||
|
let (arg, err) = parse_expression(working_set, &[*span], true);
|
||||||
|
error = error.or(err);
|
||||||
|
args.push(arg);
|
||||||
|
} else {
|
||||||
|
args.push(Expression {
|
||||||
|
expr: Expr::String(String::from_utf8_lossy(contents).to_string()),
|
||||||
|
span: *span,
|
||||||
|
ty: Type::String,
|
||||||
|
custom_completion: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
(
|
(
|
||||||
Expression {
|
Expression {
|
||||||
expr: Expr::ExternalCall(name, args),
|
expr: Expr::ExternalCall(name, name_span, args),
|
||||||
span: span(spans),
|
span: span(spans),
|
||||||
ty: Type::Unknown,
|
ty: Type::Unknown,
|
||||||
custom_completion: None,
|
custom_completion: None,
|
||||||
},
|
},
|
||||||
None,
|
error,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -359,7 +374,7 @@ fn parse_multispan_value(
|
|||||||
(arg, error)
|
(arg, error)
|
||||||
}
|
}
|
||||||
SyntaxShape::Expression => {
|
SyntaxShape::Expression => {
|
||||||
let (arg, err) = parse_expression(working_set, &spans[*spans_idx..]);
|
let (arg, err) = parse_expression(working_set, &spans[*spans_idx..], true);
|
||||||
error = error.or(err);
|
error = error.or(err);
|
||||||
*spans_idx = spans.len() - 1;
|
*spans_idx = spans.len() - 1;
|
||||||
|
|
||||||
@ -586,7 +601,7 @@ pub fn parse_call(
|
|||||||
new_spans.extend(&spans[(pos + 1)..]);
|
new_spans.extend(&spans[(pos + 1)..]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (result, err) = parse_call(working_set, &new_spans, false);
|
let (result, err) = parse_expression(working_set, &new_spans, false);
|
||||||
|
|
||||||
let expression = match result {
|
let expression = match result {
|
||||||
Expression {
|
Expression {
|
||||||
@ -631,7 +646,7 @@ pub fn parse_call(
|
|||||||
new_spans.extend(&spans[(pos + 1)..]);
|
new_spans.extend(&spans[(pos + 1)..]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (result, err) = parse_call(working_set, &new_spans, false);
|
let (result, err) = parse_expression(working_set, &new_spans, false);
|
||||||
|
|
||||||
let expression = match result {
|
let expression = match result {
|
||||||
Expression {
|
Expression {
|
||||||
@ -2832,13 +2847,14 @@ pub fn parse_math_expression(
|
|||||||
pub fn parse_expression(
|
pub fn parse_expression(
|
||||||
working_set: &mut StateWorkingSet,
|
working_set: &mut StateWorkingSet,
|
||||||
spans: &[Span],
|
spans: &[Span],
|
||||||
|
expand_aliases: bool,
|
||||||
) -> (Expression, Option<ParseError>) {
|
) -> (Expression, Option<ParseError>) {
|
||||||
let bytes = working_set.get_span_contents(spans[0]);
|
let bytes = working_set.get_span_contents(spans[0]);
|
||||||
|
|
||||||
match bytes[0] {
|
match bytes[0] {
|
||||||
b'0' | b'1' | b'2' | b'3' | b'4' | b'5' | b'6' | b'7' | b'8' | b'9' | b'(' | b'{'
|
b'0' | b'1' | b'2' | b'3' | b'4' | b'5' | b'6' | b'7' | b'8' | b'9' | b'(' | b'{'
|
||||||
| b'[' | b'$' | b'"' | b'\'' | b'-' => parse_math_expression(working_set, spans, None),
|
| b'[' | b'$' | b'"' | b'\'' | b'-' => parse_math_expression(working_set, spans, None),
|
||||||
_ => parse_call(working_set, spans, true),
|
_ => parse_call(working_set, spans, expand_aliases),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2878,7 +2894,7 @@ pub fn parse_statement(
|
|||||||
),
|
),
|
||||||
b"hide" => parse_hide(working_set, spans),
|
b"hide" => parse_hide(working_set, spans),
|
||||||
_ => {
|
_ => {
|
||||||
let (expr, err) = parse_expression(working_set, spans);
|
let (expr, err) = parse_expression(working_set, spans, true);
|
||||||
(Statement::Pipeline(Pipeline::from_vec(vec![expr])), err)
|
(Statement::Pipeline(Pipeline::from_vec(vec![expr])), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2914,7 +2930,7 @@ pub fn parse_block(
|
|||||||
.commands
|
.commands
|
||||||
.iter()
|
.iter()
|
||||||
.map(|command| {
|
.map(|command| {
|
||||||
let (expr, err) = parse_expression(working_set, &command.parts);
|
let (expr, err) = parse_expression(working_set, &command.parts, true);
|
||||||
|
|
||||||
if error.is_none() {
|
if error.is_none() {
|
||||||
error = err;
|
error = err;
|
||||||
|
@ -14,7 +14,7 @@ pub enum Expr {
|
|||||||
),
|
),
|
||||||
Var(VarId),
|
Var(VarId),
|
||||||
Call(Box<Call>),
|
Call(Box<Call>),
|
||||||
ExternalCall(Span, Vec<Span>),
|
ExternalCall(String, Span, Vec<Expression>),
|
||||||
Operator(Operator),
|
Operator(Operator),
|
||||||
RowCondition(VarId, Box<Expression>),
|
RowCondition(VarId, Box<Expression>),
|
||||||
BinaryOp(Box<Expression>, Box<Expression>, Box<Expression>), //lhs, op, rhs
|
BinaryOp(Box<Expression>, Box<Expression>, Box<Expression>), //lhs, op, rhs
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use super::Command;
|
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 core::panic;
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
@ -178,7 +178,7 @@ impl EngineState {
|
|||||||
.expect("internal error: missing declaration")
|
.expect("internal error: missing declaration")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_decls(&self) -> Vec<Signature> {
|
pub fn get_signatures(&self) -> Vec<Signature> {
|
||||||
let mut output = vec![];
|
let mut output = vec![];
|
||||||
for decl in self.decls.iter() {
|
for decl in self.decls.iter() {
|
||||||
if decl.get_block_id().is_none() {
|
if decl.get_block_id().is_none() {
|
||||||
@ -193,6 +193,21 @@ impl EngineState {
|
|||||||
output
|
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 {
|
pub fn get_block(&self, block_id: BlockId) -> &Block {
|
||||||
self.blocks
|
self.blocks
|
||||||
.get(block_id)
|
.get(block_id)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use super::EngineState;
|
use super::EngineState;
|
||||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||||
|
|
||||||
use crate::{ShellError, Signature, Value, VarId};
|
use crate::{Example, ShellError, Signature, Value, VarId};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct EvaluationContext {
|
pub struct EvaluationContext {
|
||||||
@ -47,8 +47,12 @@ impl EvaluationContext {
|
|||||||
self.stack.print_stack();
|
self.stack.print_stack();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_commands_info(&self) -> Vec<Signature> {
|
pub fn get_signatures(&self) -> Vec<Signature> {
|
||||||
self.engine_state.borrow().get_decls()
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,16 @@ pub enum ShellError {
|
|||||||
rhs_span: Span,
|
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}.")]
|
#[error("Unsupported operator: {0}.")]
|
||||||
#[diagnostic(code(nu::shell::unsupported_operator), url(docsrs))]
|
#[diagnostic(code(nu::shell::unsupported_operator), url(docsrs))]
|
||||||
UnsupportedOperator(Operator, #[label = "unsupported operator"] Span),
|
UnsupportedOperator(Operator, #[label = "unsupported operator"] Span),
|
||||||
@ -27,6 +37,10 @@ pub enum ShellError {
|
|||||||
#[diagnostic(code(nu::shell::unknown_operator), url(docsrs))]
|
#[diagnostic(code(nu::shell::unknown_operator), url(docsrs))]
|
||||||
UnknownOperator(String, #[label = "unsupported operator"] Span),
|
UnknownOperator(String, #[label = "unsupported operator"] Span),
|
||||||
|
|
||||||
|
#[error("Missing parameter: {0}.")]
|
||||||
|
#[diagnostic(code(nu::shell::missing_parameter), url(docsrs))]
|
||||||
|
MissingParameter(String, #[label = "missing parameter: {0}"] Span),
|
||||||
|
|
||||||
#[error("External commands not yet supported")]
|
#[error("External commands not yet supported")]
|
||||||
#[diagnostic(code(nu::shell::external_commands), url(docsrs))]
|
#[diagnostic(code(nu::shell::external_commands), url(docsrs))]
|
||||||
ExternalNotSupported(#[label = "external not supported"] Span),
|
ExternalNotSupported(#[label = "external not supported"] Span),
|
||||||
@ -75,6 +89,10 @@ pub enum ShellError {
|
|||||||
#[diagnostic(code(nu::shell::unsupported_input), url(docsrs))]
|
#[diagnostic(code(nu::shell::unsupported_input), url(docsrs))]
|
||||||
UnsupportedInput(String, #[label("{0}")] Span),
|
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")]
|
#[error("Flag not found")]
|
||||||
#[diagnostic(code(nu::shell::flag_not_found), url(docsrs))]
|
#[diagnostic(code(nu::shell::flag_not_found), url(docsrs))]
|
||||||
FlagNotFound(String, #[label("{0} not found")] Span),
|
FlagNotFound(String, #[label("{0} not found")] Span),
|
||||||
@ -109,6 +127,10 @@ pub enum ShellError {
|
|||||||
#[error("Move not possible")]
|
#[error("Move not possible")]
|
||||||
#[diagnostic(code(nu::shell::move_not_possible_single), url(docsrs))]
|
#[diagnostic(code(nu::shell::move_not_possible_single), url(docsrs))]
|
||||||
MoveNotPossibleSingle(String, #[label("{0}")] Span),
|
MoveNotPossibleSingle(String, #[label("{0}")] Span),
|
||||||
|
|
||||||
|
#[error("Create not possible")]
|
||||||
|
#[diagnostic(code(nu::shell::create_not_possible), url(docsrs))]
|
||||||
|
CreateNotPossible(String, #[label("{0}")] Span),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<std::io::Error> for ShellError {
|
impl From<std::io::Error> for ShellError {
|
||||||
|
@ -364,6 +364,73 @@ impl Value {
|
|||||||
_ => vec![],
|
_ => vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn map<F>(self, span: Span, mut f: F) -> Value
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
F: FnMut(Self) -> Value + 'static,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Value::List { vals, .. } => Value::List {
|
||||||
|
vals: vals.into_iter().map(f).collect(),
|
||||||
|
span,
|
||||||
|
},
|
||||||
|
Value::Stream { stream, .. } => Value::Stream {
|
||||||
|
stream: stream.map(f).into_value_stream(),
|
||||||
|
span,
|
||||||
|
},
|
||||||
|
v => {
|
||||||
|
if v.as_string().is_ok() {
|
||||||
|
Value::List {
|
||||||
|
vals: vec![f(v)],
|
||||||
|
span,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Value::Error {
|
||||||
|
error: ShellError::PipelineMismatch {
|
||||||
|
expected: Type::String,
|
||||||
|
expected_span: span,
|
||||||
|
origin: v.span(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flat_map<U, F>(self, span: Span, mut f: F) -> Value
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
U: IntoIterator<Item = Value>,
|
||||||
|
F: FnMut(Self) -> U + 'static,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Value::List { vals, .. } => Value::List {
|
||||||
|
vals: vals.into_iter().map(f).flatten().collect(),
|
||||||
|
span,
|
||||||
|
},
|
||||||
|
Value::Stream { stream, .. } => Value::Stream {
|
||||||
|
stream: stream.map(f).flatten().into_value_stream(),
|
||||||
|
span,
|
||||||
|
},
|
||||||
|
v => {
|
||||||
|
if v.as_string().is_ok() {
|
||||||
|
Value::List {
|
||||||
|
vals: f(v).into_iter().collect(),
|
||||||
|
span,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Value::Error {
|
||||||
|
error: ShellError::PipelineMismatch {
|
||||||
|
expected: Type::String,
|
||||||
|
expected_span: span,
|
||||||
|
origin: v.span(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Value {
|
impl PartialEq for Value {
|
||||||
|
22
crates/nu-term-grid/.gitignore
vendored
Normal file
22
crates/nu-term-grid/.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/target
|
||||||
|
/scratch
|
||||||
|
**/*.rs.bk
|
||||||
|
history.txt
|
||||||
|
tests/fixtures/nuplayground
|
||||||
|
crates/*/target
|
||||||
|
|
||||||
|
# Debian/Ubuntu
|
||||||
|
debian/.debhelper/
|
||||||
|
debian/debhelper-build-stamp
|
||||||
|
debian/files
|
||||||
|
debian/nu.substvars
|
||||||
|
debian/nu/
|
||||||
|
|
||||||
|
# macOS junk
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# JetBrains' IDE items
|
||||||
|
.idea/*
|
||||||
|
|
||||||
|
# VSCode's IDE items
|
||||||
|
.vscode/*
|
16
crates/nu-term-grid/Cargo.toml
Normal file
16
crates/nu-term-grid/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
authors = ["The Nu Project Contributors"]
|
||||||
|
description = "Nushell grid printing"
|
||||||
|
edition = "2018"
|
||||||
|
license = "MIT"
|
||||||
|
name = "nu-term-grid"
|
||||||
|
version = "0.36.0"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
[[bin]]
|
||||||
|
name = "grid"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
unicode-width = "0.1.9"
|
||||||
|
strip-ansi-escapes = "0.1.1"
|
769
crates/nu-term-grid/src/grid.rs
Normal file
769
crates/nu-term-grid/src/grid.rs
Normal file
@ -0,0 +1,769 @@
|
|||||||
|
// Thanks to https://github.com/ogham/rust-term-grid for making this available
|
||||||
|
|
||||||
|
//! This library arranges textual data in a grid format suitable for
|
||||||
|
//! fixed-width fonts, using an algorithm to minimise the amount of space
|
||||||
|
//! needed. For example:
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use nu_term_grid::grid::{Grid, GridOptions, Direction, Filling, Cell};
|
||||||
|
//!
|
||||||
|
//! let mut grid = Grid::new(GridOptions {
|
||||||
|
//! filling: Filling::Spaces(1),
|
||||||
|
//! direction: Direction::LeftToRight,
|
||||||
|
//! });
|
||||||
|
//!
|
||||||
|
//! for s in &["one", "two", "three", "four", "five", "six", "seven",
|
||||||
|
//! "eight", "nine", "ten", "eleven", "twelve"]
|
||||||
|
//! {
|
||||||
|
//! grid.add(Cell::from(*s));
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! println!("{}", grid.fit_into_width(24).unwrap());
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Produces the following tabular result:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! one two three four
|
||||||
|
//! five six seven eight
|
||||||
|
//! nine ten eleven twelve
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//!
|
||||||
|
//! ## Creating a grid
|
||||||
|
//!
|
||||||
|
//! To add data to a grid, first create a new [`Grid`] value, and then add
|
||||||
|
//! cells to them with the `add` function.
|
||||||
|
//!
|
||||||
|
//! There are two options that must be specified in the [`GridOptions`] value
|
||||||
|
//! that dictate how the grid is formatted:
|
||||||
|
//!
|
||||||
|
//! - `filling`: what to put in between two columns — either a number of
|
||||||
|
//! spaces, or a text string;
|
||||||
|
//! - `direction`, which specifies whether the cells should go along
|
||||||
|
//! rows, or columns:
|
||||||
|
//! - `Direction::LeftToRight` starts them in the top left and
|
||||||
|
//! moves *rightwards*, going to the start of a new row after reaching the
|
||||||
|
//! final column;
|
||||||
|
//! - `Direction::TopToBottom` starts them in the top left and moves
|
||||||
|
//! *downwards*, going to the top of a new column after reaching the final
|
||||||
|
//! row.
|
||||||
|
//!
|
||||||
|
//!
|
||||||
|
//! ## Displaying a grid
|
||||||
|
//!
|
||||||
|
//! When display a grid, you can either specify the number of columns in advance,
|
||||||
|
//! or try to find the maximum number of columns that can fit in an area of a
|
||||||
|
//! given width.
|
||||||
|
//!
|
||||||
|
//! Splitting a series of cells into columns — or, in other words, starting a new
|
||||||
|
//! row every <var>n</var> cells — is achieved with the [`fit_into_columns`] function
|
||||||
|
//! on a `Grid` value. It takes as its argument the number of columns.
|
||||||
|
//!
|
||||||
|
//! Trying to fit as much data onto one screen as possible is the main use case
|
||||||
|
//! for specifying a maximum width instead. This is achieved with the
|
||||||
|
//! [`fit_into_width`] function. It takes the maximum allowed width, including
|
||||||
|
//! separators, as its argument. However, it returns an *optional* [`Display`]
|
||||||
|
//! value, depending on whether any of the cells actually had a width greater than
|
||||||
|
//! the maximum width! If this is the case, your best bet is to just output the
|
||||||
|
//! cells with one per line.
|
||||||
|
//!
|
||||||
|
//!
|
||||||
|
//! ## Cells and data
|
||||||
|
//!
|
||||||
|
//! Grids to not take `String`s or `&str`s — they take [`Cell`] values.
|
||||||
|
//!
|
||||||
|
//! A **Cell** is a struct containing an individual cell’s contents, as a string,
|
||||||
|
//! and its pre-computed length, which gets used when calculating a grid’s final
|
||||||
|
//! dimensions. Usually, you want the *Unicode width* of the string to be used for
|
||||||
|
//! this, so you can turn a `String` into a `Cell` with the `.into()` function.
|
||||||
|
//!
|
||||||
|
//! However, you may also want to supply your own width: when you already know the
|
||||||
|
//! width in advance, or when you want to change the measurement, such as skipping
|
||||||
|
//! over terminal control characters. For cases like these, the fields on the
|
||||||
|
//! `Cell` values are public, meaning you can construct your own instances as
|
||||||
|
//! necessary.
|
||||||
|
//!
|
||||||
|
//! [`Cell`]: ./struct.Cell.html
|
||||||
|
//! [`Display`]: ./struct.Display.html
|
||||||
|
//! [`Grid`]: ./struct.Grid.html
|
||||||
|
//! [`fit_into_columns`]: ./struct.Grid.html#method.fit_into_columns
|
||||||
|
//! [`fit_into_width`]: ./struct.Grid.html#method.fit_into_width
|
||||||
|
//! [`GridOptions`]: ./struct.GridOptions.html
|
||||||
|
|
||||||
|
use std::cmp::max;
|
||||||
|
use std::fmt;
|
||||||
|
use std::iter::repeat;
|
||||||
|
use strip_ansi_escapes::strip;
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
fn unicode_width_strip_ansi(astring: &str) -> usize {
|
||||||
|
let stripped_string: String = {
|
||||||
|
if let Ok(bytes) = strip(astring) {
|
||||||
|
String::from_utf8_lossy(&bytes).to_string()
|
||||||
|
} else {
|
||||||
|
astring.to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
UnicodeWidthStr::width(&stripped_string[..])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alignment indicate on which side the content should stick if some filling
|
||||||
|
/// is required.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Alignment {
|
||||||
|
/// The content will stick to the left.
|
||||||
|
Left,
|
||||||
|
|
||||||
|
/// The content will stick to the right.
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A **Cell** is the combination of a string and its pre-computed length.
|
||||||
|
///
|
||||||
|
/// The easiest way to create a Cell is just by using `string.into()`, which
|
||||||
|
/// uses the **unicode width** of the string (see the `unicode_width` crate).
|
||||||
|
/// However, the fields are public, if you wish to provide your own length.
|
||||||
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
|
pub struct Cell {
|
||||||
|
/// The string to display when this cell gets rendered.
|
||||||
|
pub contents: String,
|
||||||
|
|
||||||
|
/// The pre-computed length of the string.
|
||||||
|
pub width: Width,
|
||||||
|
|
||||||
|
/// The side (left/right) to align the content if some filling is required.
|
||||||
|
pub alignment: Alignment,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for Cell {
|
||||||
|
fn from(string: String) -> Self {
|
||||||
|
Self {
|
||||||
|
width: unicode_width_strip_ansi(&*string),
|
||||||
|
contents: string,
|
||||||
|
alignment: Alignment::Left,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a str> for Cell {
|
||||||
|
fn from(string: &'a str) -> Self {
|
||||||
|
Self {
|
||||||
|
width: unicode_width_strip_ansi(&*string),
|
||||||
|
contents: string.into(),
|
||||||
|
alignment: Alignment::Left,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Direction cells should be written in — either across, or downwards.
|
||||||
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||||
|
pub enum Direction {
|
||||||
|
/// Starts at the top left and moves rightwards, going back to the first
|
||||||
|
/// column for a new row, like a typewriter.
|
||||||
|
LeftToRight,
|
||||||
|
|
||||||
|
/// Starts at the top left and moves downwards, going back to the first
|
||||||
|
/// row for a new column, like how `ls` lists files by default.
|
||||||
|
TopToBottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The width of a cell, in columns.
|
||||||
|
pub type Width = usize;
|
||||||
|
|
||||||
|
/// The text to put in between each pair of columns.
|
||||||
|
/// This does not include any spaces used when aligning cells.
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
pub enum Filling {
|
||||||
|
/// A certain number of spaces should be used as the separator.
|
||||||
|
Spaces(Width),
|
||||||
|
|
||||||
|
/// An arbitrary string.
|
||||||
|
/// `"|"` is a common choice.
|
||||||
|
Text(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filling {
|
||||||
|
fn width(&self) -> Width {
|
||||||
|
match *self {
|
||||||
|
Filling::Spaces(w) => w,
|
||||||
|
Filling::Text(ref t) => unicode_width_strip_ansi(&t[..]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The user-assignable options for a grid view that should be passed to
|
||||||
|
/// [`Grid::new()`](struct.Grid.html#method.new).
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
pub struct GridOptions {
|
||||||
|
/// The direction that the cells should be written in — either
|
||||||
|
/// across, or downwards.
|
||||||
|
pub direction: Direction,
|
||||||
|
|
||||||
|
/// The number of spaces to put in between each column of cells.
|
||||||
|
pub filling: Filling,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
struct Dimensions {
|
||||||
|
/// The number of lines in the grid.
|
||||||
|
num_lines: Width,
|
||||||
|
|
||||||
|
/// The width of each column in the grid. The length of this vector serves
|
||||||
|
/// as the number of columns.
|
||||||
|
widths: Vec<Width>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dimensions {
|
||||||
|
fn total_width(&self, separator_width: Width) -> Width {
|
||||||
|
if self.widths.is_empty() {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
let values = self.widths.iter().sum::<Width>();
|
||||||
|
let separators = separator_width * (self.widths.len() - 1);
|
||||||
|
values + separators
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Everything needed to format the cells with the grid options.
|
||||||
|
///
|
||||||
|
/// For more information, see the [`grid` crate documentation](index.html).
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
pub struct Grid {
|
||||||
|
options: GridOptions,
|
||||||
|
cells: Vec<Cell>,
|
||||||
|
widest_cell_length: Width,
|
||||||
|
width_sum: Width,
|
||||||
|
cell_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Grid {
|
||||||
|
/// Creates a new grid view with the given options.
|
||||||
|
pub fn new(options: GridOptions) -> Self {
|
||||||
|
let cells = Vec::new();
|
||||||
|
Self {
|
||||||
|
options,
|
||||||
|
cells,
|
||||||
|
widest_cell_length: 0,
|
||||||
|
width_sum: 0,
|
||||||
|
cell_count: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reserves space in the vector for the given number of additional cells
|
||||||
|
/// to be added. (See the `Vec::reserve` function.)
|
||||||
|
pub fn reserve(&mut self, additional: usize) {
|
||||||
|
self.cells.reserve(additional)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds another cell onto the vector.
|
||||||
|
pub fn add(&mut self, cell: Cell) {
|
||||||
|
if cell.width > self.widest_cell_length {
|
||||||
|
self.widest_cell_length = cell.width;
|
||||||
|
}
|
||||||
|
self.width_sum += cell.width;
|
||||||
|
self.cell_count += 1;
|
||||||
|
self.cells.push(cell)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a displayable grid that’s been packed to fit into the given
|
||||||
|
/// width in the fewest number of rows.
|
||||||
|
///
|
||||||
|
/// Returns `None` if any of the cells has a width greater than the
|
||||||
|
/// maximum width.
|
||||||
|
pub fn fit_into_width(&self, maximum_width: Width) -> Option<Display<'_>> {
|
||||||
|
self.width_dimensions(maximum_width).map(|dims| Display {
|
||||||
|
grid: self,
|
||||||
|
dimensions: dims,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a displayable grid with the given number of columns, and no
|
||||||
|
/// maximum width.
|
||||||
|
pub fn fit_into_columns(&self, num_columns: usize) -> Display<'_> {
|
||||||
|
Display {
|
||||||
|
grid: self,
|
||||||
|
dimensions: self.columns_dimensions(num_columns),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn columns_dimensions(&self, num_columns: usize) -> Dimensions {
|
||||||
|
let mut num_lines = self.cells.len() / num_columns;
|
||||||
|
if self.cells.len() % num_columns != 0 {
|
||||||
|
num_lines += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.column_widths(num_lines, num_columns)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn column_widths(&self, num_lines: usize, num_columns: usize) -> Dimensions {
|
||||||
|
let mut widths: Vec<Width> = repeat(0).take(num_columns).collect();
|
||||||
|
for (index, cell) in self.cells.iter().enumerate() {
|
||||||
|
let index = match self.options.direction {
|
||||||
|
Direction::LeftToRight => index % num_columns,
|
||||||
|
Direction::TopToBottom => index / num_lines,
|
||||||
|
};
|
||||||
|
widths[index] = max(widths[index], cell.width);
|
||||||
|
}
|
||||||
|
|
||||||
|
Dimensions { num_lines, widths }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn theoretical_max_num_lines(&self, maximum_width: usize) -> usize {
|
||||||
|
// TODO: Make code readable / efficient.
|
||||||
|
let mut theoretical_min_num_cols = 0;
|
||||||
|
let mut col_total_width_so_far = 0;
|
||||||
|
|
||||||
|
let mut cells = self.cells.clone();
|
||||||
|
cells.sort_unstable_by(|a, b| b.width.cmp(&a.width)); // Sort in reverse order
|
||||||
|
|
||||||
|
for cell in &cells {
|
||||||
|
if cell.width + col_total_width_so_far <= maximum_width {
|
||||||
|
theoretical_min_num_cols += 1;
|
||||||
|
col_total_width_so_far += cell.width;
|
||||||
|
} else {
|
||||||
|
let mut theoretical_max_num_lines = self.cell_count / theoretical_min_num_cols;
|
||||||
|
if self.cell_count % theoretical_min_num_cols != 0 {
|
||||||
|
theoretical_max_num_lines += 1;
|
||||||
|
}
|
||||||
|
return theoretical_max_num_lines;
|
||||||
|
}
|
||||||
|
col_total_width_so_far += self.options.filling.width()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we make it to this point, we have exhausted all cells before
|
||||||
|
// reaching the maximum width; the theoretical max number of lines
|
||||||
|
// needed to display all cells is 1.
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn width_dimensions(&self, maximum_width: Width) -> Option<Dimensions> {
|
||||||
|
if self.widest_cell_length > maximum_width {
|
||||||
|
// Largest cell is wider than maximum width; it is impossible to fit.
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.cell_count == 0 {
|
||||||
|
return Some(Dimensions {
|
||||||
|
num_lines: 0,
|
||||||
|
widths: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.cell_count == 1 {
|
||||||
|
let the_cell = &self.cells[0];
|
||||||
|
return Some(Dimensions {
|
||||||
|
num_lines: 1,
|
||||||
|
widths: vec![the_cell.width],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let theoretical_max_num_lines = self.theoretical_max_num_lines(maximum_width);
|
||||||
|
if theoretical_max_num_lines == 1 {
|
||||||
|
// This if—statement is neccesary for the function to work correctly
|
||||||
|
// for small inputs.
|
||||||
|
return Some(Dimensions {
|
||||||
|
num_lines: 1,
|
||||||
|
// I clone self.cells twice. Once here, and once in
|
||||||
|
// self.theoretical_max_num_lines. Perhaps not the best for
|
||||||
|
// performance?
|
||||||
|
widths: self
|
||||||
|
.cells
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(|cell| cell.width)
|
||||||
|
.collect(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Instead of numbers of columns, try to find the fewest number of *lines*
|
||||||
|
// that the output will fit in.
|
||||||
|
let mut smallest_dimensions_yet = None;
|
||||||
|
for num_lines in (1..=theoretical_max_num_lines).rev() {
|
||||||
|
// The number of columns is the number of cells divided by the number
|
||||||
|
// of lines, *rounded up*.
|
||||||
|
let mut num_columns = self.cell_count / num_lines;
|
||||||
|
if self.cell_count % num_lines != 0 {
|
||||||
|
num_columns += 1;
|
||||||
|
}
|
||||||
|
// Early abort: if there are so many columns that the width of the
|
||||||
|
// *column separators* is bigger than the width of the screen, then
|
||||||
|
// don’t even try to tabulate it.
|
||||||
|
// This is actually a necessary check, because the width is stored as
|
||||||
|
// a usize, and making it go negative makes it huge instead, but it
|
||||||
|
// also serves as a speed-up.
|
||||||
|
let total_separator_width = (num_columns - 1) * self.options.filling.width();
|
||||||
|
if maximum_width < total_separator_width {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the separator width from the available space.
|
||||||
|
let adjusted_width = maximum_width - total_separator_width;
|
||||||
|
let potential_dimensions = self.column_widths(num_lines, num_columns);
|
||||||
|
if potential_dimensions.widths.iter().sum::<Width>() < adjusted_width {
|
||||||
|
smallest_dimensions_yet = Some(potential_dimensions);
|
||||||
|
} else {
|
||||||
|
return smallest_dimensions_yet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A displayable representation of a [`Grid`](struct.Grid.html).
|
||||||
|
///
|
||||||
|
/// This type implements `Display`, so you can get the textual version
|
||||||
|
/// of the grid by calling `.to_string()`.
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
pub struct Display<'grid> {
|
||||||
|
/// The grid to display.
|
||||||
|
grid: &'grid Grid,
|
||||||
|
|
||||||
|
/// The pre-computed column widths for this grid.
|
||||||
|
dimensions: Dimensions,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display<'_> {
|
||||||
|
/// Returns how many columns this display takes up, based on the separator
|
||||||
|
/// width and the number and width of the columns.
|
||||||
|
pub fn width(&self) -> Width {
|
||||||
|
self.dimensions
|
||||||
|
.total_width(self.grid.options.filling.width())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns how many rows this display takes up.
|
||||||
|
pub fn row_count(&self) -> usize {
|
||||||
|
self.dimensions.num_lines
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether this display takes up as many columns as were allotted
|
||||||
|
/// to it.
|
||||||
|
///
|
||||||
|
/// It’s possible to construct tables that don’t actually use up all the
|
||||||
|
/// columns that they could, such as when there are more columns than
|
||||||
|
/// cells! In this case, a column would have a width of zero. This just
|
||||||
|
/// checks for that.
|
||||||
|
pub fn is_complete(&self) -> bool {
|
||||||
|
self.dimensions.widths.iter().all(|&x| x > 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Display<'_> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||||
|
for y in 0..self.dimensions.num_lines {
|
||||||
|
for x in 0..self.dimensions.widths.len() {
|
||||||
|
let num = match self.grid.options.direction {
|
||||||
|
Direction::LeftToRight => y * self.dimensions.widths.len() + x,
|
||||||
|
Direction::TopToBottom => y + self.dimensions.num_lines * x,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Abandon a line mid-way through if that’s where the cells end
|
||||||
|
if num >= self.grid.cells.len() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cell = &self.grid.cells[num];
|
||||||
|
if x == self.dimensions.widths.len() - 1 {
|
||||||
|
match cell.alignment {
|
||||||
|
Alignment::Left => {
|
||||||
|
// The final column doesn’t need to have trailing spaces,
|
||||||
|
// as long as it’s left-aligned.
|
||||||
|
write!(f, "{}", cell.contents)?;
|
||||||
|
}
|
||||||
|
Alignment::Right => {
|
||||||
|
let extra_spaces = self.dimensions.widths[x] - cell.width;
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
pad_string(&cell.contents, extra_spaces, Alignment::Right)
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert!(self.dimensions.widths[x] >= cell.width);
|
||||||
|
match (&self.grid.options.filling, cell.alignment) {
|
||||||
|
(Filling::Spaces(n), Alignment::Left) => {
|
||||||
|
let extra_spaces = self.dimensions.widths[x] - cell.width + n;
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
pad_string(&cell.contents, extra_spaces, cell.alignment)
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
(Filling::Spaces(n), Alignment::Right) => {
|
||||||
|
let s = spaces(*n);
|
||||||
|
let extra_spaces = self.dimensions.widths[x] - cell.width;
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}{}",
|
||||||
|
pad_string(&cell.contents, extra_spaces, cell.alignment),
|
||||||
|
s
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
(Filling::Text(ref t), _) => {
|
||||||
|
let extra_spaces = self.dimensions.widths[x] - cell.width;
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}{}",
|
||||||
|
pad_string(&cell.contents, extra_spaces, cell.alignment),
|
||||||
|
t
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pad a string with the given number of spaces.
|
||||||
|
fn spaces(length: usize) -> String {
|
||||||
|
" ".repeat(length)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pad a string with the given alignment and number of spaces.
|
||||||
|
///
|
||||||
|
/// This doesn’t take the width the string *should* be, rather the number
|
||||||
|
/// of spaces to add.
|
||||||
|
fn pad_string(string: &str, padding: usize, alignment: Alignment) -> String {
|
||||||
|
if alignment == Alignment::Left {
|
||||||
|
format!("{}{}", string, spaces(padding))
|
||||||
|
} else {
|
||||||
|
format!("{}{}", spaces(padding), string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_items() {
|
||||||
|
let grid = Grid::new(GridOptions {
|
||||||
|
direction: Direction::TopToBottom,
|
||||||
|
filling: Filling::Spaces(2),
|
||||||
|
});
|
||||||
|
|
||||||
|
let display = grid.fit_into_width(40).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(display.dimensions.num_lines, 0);
|
||||||
|
assert!(display.dimensions.widths.is_empty());
|
||||||
|
|
||||||
|
assert_eq!(display.width(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn one_item() {
|
||||||
|
let mut grid = Grid::new(GridOptions {
|
||||||
|
direction: Direction::TopToBottom,
|
||||||
|
filling: Filling::Spaces(2),
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.add(Cell::from("1"));
|
||||||
|
|
||||||
|
let display = grid.fit_into_width(40).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(display.dimensions.num_lines, 1);
|
||||||
|
assert_eq!(display.dimensions.widths, vec![1]);
|
||||||
|
|
||||||
|
assert_eq!(display.width(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn one_item_exact_width() {
|
||||||
|
let mut grid = Grid::new(GridOptions {
|
||||||
|
direction: Direction::TopToBottom,
|
||||||
|
filling: Filling::Spaces(2),
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.add(Cell::from("1234567890"));
|
||||||
|
|
||||||
|
let display = grid.fit_into_width(10).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(display.dimensions.num_lines, 1);
|
||||||
|
assert_eq!(display.dimensions.widths, vec![10]);
|
||||||
|
|
||||||
|
assert_eq!(display.width(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn one_item_just_over() {
|
||||||
|
let mut grid = Grid::new(GridOptions {
|
||||||
|
direction: Direction::TopToBottom,
|
||||||
|
filling: Filling::Spaces(2),
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.add(Cell::from("1234567890!"));
|
||||||
|
|
||||||
|
assert_eq!(grid.fit_into_width(10), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_small_items() {
|
||||||
|
let mut grid = Grid::new(GridOptions {
|
||||||
|
direction: Direction::TopToBottom,
|
||||||
|
filling: Filling::Spaces(2),
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.add(Cell::from("1"));
|
||||||
|
grid.add(Cell::from("2"));
|
||||||
|
|
||||||
|
let display = grid.fit_into_width(40).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(display.dimensions.num_lines, 1);
|
||||||
|
assert_eq!(display.dimensions.widths, vec![1, 1]);
|
||||||
|
|
||||||
|
assert_eq!(display.width(), 1 + 2 + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_medium_size_items() {
|
||||||
|
let mut grid = Grid::new(GridOptions {
|
||||||
|
direction: Direction::TopToBottom,
|
||||||
|
filling: Filling::Spaces(2),
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.add(Cell::from("hello there"));
|
||||||
|
grid.add(Cell::from("how are you today?"));
|
||||||
|
|
||||||
|
let display = grid.fit_into_width(40).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(display.dimensions.num_lines, 1);
|
||||||
|
assert_eq!(display.dimensions.widths, vec![11, 18]);
|
||||||
|
|
||||||
|
assert_eq!(display.width(), 11 + 2 + 18);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_big_items() {
|
||||||
|
let mut grid = Grid::new(GridOptions {
|
||||||
|
direction: Direction::TopToBottom,
|
||||||
|
filling: Filling::Spaces(2),
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.add(Cell::from(
|
||||||
|
"nuihuneihsoenhisenouiuteinhdauisdonhuisudoiosadiuohnteihaosdinhteuieudi",
|
||||||
|
));
|
||||||
|
grid.add(Cell::from(
|
||||||
|
"oudisnuthasuouneohbueobaugceoduhbsauglcobeuhnaeouosbubaoecgueoubeohubeo",
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(grid.fit_into_width(40), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn that_example_from_earlier() {
|
||||||
|
let mut grid = Grid::new(GridOptions {
|
||||||
|
filling: Filling::Spaces(1),
|
||||||
|
direction: Direction::LeftToRight,
|
||||||
|
});
|
||||||
|
|
||||||
|
for s in &[
|
||||||
|
"one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten",
|
||||||
|
"eleven", "twelve",
|
||||||
|
] {
|
||||||
|
grid.add(Cell::from(*s));
|
||||||
|
}
|
||||||
|
|
||||||
|
let bits = "one two three four\nfive six seven eight\nnine ten eleven twelve\n";
|
||||||
|
assert_eq!(grid.fit_into_width(24).unwrap().to_string(), bits);
|
||||||
|
assert_eq!(grid.fit_into_width(24).unwrap().row_count(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn number_grid_with_pipe() {
|
||||||
|
let mut grid = Grid::new(GridOptions {
|
||||||
|
filling: Filling::Text("|".into()),
|
||||||
|
direction: Direction::LeftToRight,
|
||||||
|
});
|
||||||
|
|
||||||
|
for s in &[
|
||||||
|
"one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten",
|
||||||
|
"eleven", "twelve",
|
||||||
|
] {
|
||||||
|
grid.add(Cell::from(*s));
|
||||||
|
}
|
||||||
|
|
||||||
|
let bits = "one |two|three |four\nfive|six|seven |eight\nnine|ten|eleven|twelve\n";
|
||||||
|
assert_eq!(grid.fit_into_width(24).unwrap().to_string(), bits);
|
||||||
|
assert_eq!(grid.fit_into_width(24).unwrap().row_count(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn numbers_right() {
|
||||||
|
let mut grid = Grid::new(GridOptions {
|
||||||
|
filling: Filling::Spaces(1),
|
||||||
|
direction: Direction::LeftToRight,
|
||||||
|
});
|
||||||
|
|
||||||
|
for s in &[
|
||||||
|
"one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten",
|
||||||
|
"eleven", "twelve",
|
||||||
|
] {
|
||||||
|
let mut cell = Cell::from(*s);
|
||||||
|
cell.alignment = Alignment::Right;
|
||||||
|
grid.add(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bits = " one two three four\nfive six seven eight\nnine ten eleven twelve\n";
|
||||||
|
assert_eq!(grid.fit_into_width(24).unwrap().to_string(), bits);
|
||||||
|
assert_eq!(grid.fit_into_width(24).unwrap().row_count(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn numbers_right_pipe() {
|
||||||
|
let mut grid = Grid::new(GridOptions {
|
||||||
|
filling: Filling::Text("|".into()),
|
||||||
|
direction: Direction::LeftToRight,
|
||||||
|
});
|
||||||
|
|
||||||
|
for s in &[
|
||||||
|
"one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten",
|
||||||
|
"eleven", "twelve",
|
||||||
|
] {
|
||||||
|
let mut cell = Cell::from(*s);
|
||||||
|
cell.alignment = Alignment::Right;
|
||||||
|
grid.add(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bits = " one|two| three| four\nfive|six| seven| eight\nnine|ten|eleven|twelve\n";
|
||||||
|
assert_eq!(grid.fit_into_width(24).unwrap().to_string(), bits);
|
||||||
|
assert_eq!(grid.fit_into_width(24).unwrap().row_count(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn huge_separator() {
|
||||||
|
let mut grid = Grid::new(GridOptions {
|
||||||
|
filling: Filling::Spaces(100),
|
||||||
|
direction: Direction::LeftToRight,
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.add("a".into());
|
||||||
|
grid.add("b".into());
|
||||||
|
|
||||||
|
assert_eq!(grid.fit_into_width(99), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn huge_yet_unused_separator() {
|
||||||
|
let mut grid = Grid::new(GridOptions {
|
||||||
|
filling: Filling::Spaces(100),
|
||||||
|
direction: Direction::LeftToRight,
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.add("abcd".into());
|
||||||
|
|
||||||
|
let display = grid.fit_into_width(99).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(display.dimensions.num_lines, 1);
|
||||||
|
assert_eq!(display.dimensions.widths, vec![4]);
|
||||||
|
|
||||||
|
assert_eq!(display.width(), 4);
|
||||||
|
}
|
||||||
|
}
|
3
crates/nu-term-grid/src/lib.rs
Normal file
3
crates/nu-term-grid/src/lib.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod grid;
|
||||||
|
|
||||||
|
pub use grid::Grid;
|
30
crates/nu-term-grid/src/main.rs
Normal file
30
crates/nu-term-grid/src/main.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
use nu_term_grid::grid::{Alignment, Cell, Direction, Filling, Grid, GridOptions};
|
||||||
|
|
||||||
|
// This produces:
|
||||||
|
//
|
||||||
|
// 1 | 128 | 16384 | 2097152 | 268435456 | 34359738368 | 4398046511104
|
||||||
|
// 2 | 256 | 32768 | 4194304 | 536870912 | 68719476736 | 8796093022208
|
||||||
|
// 4 | 512 | 65536 | 8388608 | 1073741824 | 137438953472 | 17592186044416
|
||||||
|
// 8 | 1024 | 131072 | 16777216 | 2147483648 | 274877906944 | 35184372088832
|
||||||
|
// 16 | 2048 | 262144 | 33554432 | 4294967296 | 549755813888 | 70368744177664
|
||||||
|
// 32 | 4096 | 524288 | 67108864 | 8589934592 | 1099511627776 | 140737488355328
|
||||||
|
// 64 | 8192 | 1048576 | 134217728 | 17179869184 | 2199023255552 |
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut grid = Grid::new(GridOptions {
|
||||||
|
direction: Direction::TopToBottom,
|
||||||
|
filling: Filling::Text(" | ".into()),
|
||||||
|
});
|
||||||
|
|
||||||
|
for i in 0..48 {
|
||||||
|
let mut cell = Cell::from(format!("{}", 2_isize.pow(i)));
|
||||||
|
cell.alignment = Alignment::Right;
|
||||||
|
grid.add(cell)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(grid_display) = grid.fit_into_width(80) {
|
||||||
|
println!("{}", grid_display);
|
||||||
|
} else {
|
||||||
|
println!("Couldn't fit grid into 80 columns!");
|
||||||
|
}
|
||||||
|
}
|
13
src/tests.rs
13
src/tests.rs
@ -540,3 +540,16 @@ fn string_cell_path() -> TestResult {
|
|||||||
"c",
|
"c",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split_row() -> TestResult {
|
||||||
|
run_test(r#""hello world" | split row " " | get 1"#, "world")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split_column() -> TestResult {
|
||||||
|
run_test(
|
||||||
|
r#""hello world" | split column " " | get "Column1".0"#,
|
||||||
|
"hello",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user