mirror of
https://github.com/nushell/nushell.git
synced 2025-07-01 15:11:52 +02:00
Compare commits
60 Commits
Author | SHA1 | Date | |
---|---|---|---|
87d71604ad | |||
e372e7c448 | |||
0194dee3a6 | |||
cc3c10867c | |||
3c18169f63 | |||
43e9c89125 | |||
2ad07912d9 | |||
51ad019495 | |||
9264325e57 | |||
901157341b | |||
eb766b80c1 | |||
f0dbffd761 | |||
f14c0df582 | |||
362bb1bea3 | |||
724b177c97 | |||
50343f2d6a | |||
3122525b96 | |||
8232c6f185 | |||
6202705eb6 | |||
e1c5940b04 | |||
7f35bfc005 | |||
c48c092125 | |||
028fc9b9cd | |||
eeb9b4edcb | |||
3a7869b422 | |||
c48ea46c4f | |||
f33da33626 | |||
a88f5c7ae7 | |||
cda53b6cda | |||
ee734873ba | |||
9fb6f5cd09 | |||
4ef15b5f80 | |||
ba81278ffd | |||
10fbed3808 | |||
16cfc36aec | |||
aca7f71737 | |||
3282a509a9 | |||
878b748a41 | |||
18a4505b9b | |||
26e77a4b05 | |||
37f10cf273 | |||
5e0a9aecaa | |||
7e2c627044 | |||
4347339e9a | |||
e66a8258ec | |||
e4b42b54ad | |||
de18b9ca2c | |||
a77f0f7b41 | |||
6b31a006b8 | |||
2db4fe83d8 | |||
55a2f284d9 | |||
2d3b1e090a | |||
ed0c1038e3 | |||
0c20282200 | |||
e71f44d26f | |||
e3d7e46855 | |||
9b35aae5e8 | |||
7e9f87c57f | |||
5d17b72852 | |||
6b4634b293 |
@ -55,10 +55,10 @@ steps:
|
||||
- bash: RUSTFLAGS="-D warnings" cargo clippy --all --features=stable -- -D clippy::result_unwrap_used -D clippy::option_unwrap_used
|
||||
condition: eq(variables['style'], 'unflagged')
|
||||
displayName: Check clippy lints
|
||||
- bash: NUSHELL_ENABLE_ALL_FLAGS=1 RUSTFLAGS="-D warnings" cargo test --all --features stable
|
||||
- bash: RUSTFLAGS="-D warnings" cargo test --all --features stable
|
||||
condition: eq(variables['style'], 'canary')
|
||||
displayName: Run tests
|
||||
- bash: NUSHELL_ENABLE_ALL_FLAGS=1 RUSTFLAGS="-D warnings" cargo clippy --all --features=stable -- -D clippy::result_unwrap_used -D clippy::option_unwrap_used
|
||||
- bash: RUSTFLAGS="-D warnings" cargo clippy --all --features=stable -- -D clippy::result_unwrap_used -D clippy::option_unwrap_used
|
||||
condition: eq(variables['style'], 'canary')
|
||||
displayName: Check clippy lints
|
||||
- bash: RUSTFLAGS="-D warnings" cargo test --all --no-default-features
|
||||
|
@ -1 +0,0 @@
|
||||
[build]
|
846
Cargo.lock
generated
846
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
49
Cargo.toml
49
Cargo.toml
@ -10,7 +10,7 @@ license = "MIT"
|
||||
name = "nu"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/nushell/nushell"
|
||||
version = "0.17.0"
|
||||
version = "0.18.1"
|
||||
|
||||
[workspace]
|
||||
members = ["crates/*/"]
|
||||
@ -18,27 +18,28 @@ members = ["crates/*/"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
nu-cli = {version = "0.17.0", path = "./crates/nu-cli"}
|
||||
nu-errors = {version = "0.17.0", path = "./crates/nu-errors"}
|
||||
nu-parser = {version = "0.17.0", path = "./crates/nu-parser"}
|
||||
nu-plugin = {version = "0.17.0", path = "./crates/nu-plugin"}
|
||||
nu-protocol = {version = "0.17.0", path = "./crates/nu-protocol"}
|
||||
nu-source = {version = "0.17.0", path = "./crates/nu-source"}
|
||||
nu-value-ext = {version = "0.17.0", path = "./crates/nu-value-ext"}
|
||||
nu_plugin_binaryview = {version = "0.17.0", path = "./crates/nu_plugin_binaryview", optional = true}
|
||||
nu_plugin_fetch = {version = "0.17.0", path = "./crates/nu_plugin_fetch", optional = true}
|
||||
nu_plugin_from_bson = {version = "0.17.0", path = "./crates/nu_plugin_from_bson", optional = true}
|
||||
nu_plugin_from_sqlite = {version = "0.17.0", path = "./crates/nu_plugin_from_sqlite", optional = true}
|
||||
nu_plugin_inc = {version = "0.17.0", path = "./crates/nu_plugin_inc", optional = true}
|
||||
nu_plugin_match = {version = "0.17.0", path = "./crates/nu_plugin_match", optional = true}
|
||||
nu_plugin_post = {version = "0.17.0", path = "./crates/nu_plugin_post", optional = true}
|
||||
nu_plugin_ps = {version = "0.17.0", path = "./crates/nu_plugin_ps", optional = true}
|
||||
nu_plugin_start = {version = "0.17.0", path = "./crates/nu_plugin_start", optional = true}
|
||||
nu_plugin_sys = {version = "0.17.0", path = "./crates/nu_plugin_sys", optional = true}
|
||||
nu_plugin_textview = {version = "0.17.0", path = "./crates/nu_plugin_textview", optional = true}
|
||||
nu_plugin_to_bson = {version = "0.17.0", path = "./crates/nu_plugin_to_bson", optional = true}
|
||||
nu_plugin_to_sqlite = {version = "0.17.0", path = "./crates/nu_plugin_to_sqlite", optional = true}
|
||||
nu_plugin_tree = {version = "0.17.0", path = "./crates/nu_plugin_tree", optional = true}
|
||||
nu-cli = {version = "0.18.1", path = "./crates/nu-cli"}
|
||||
nu-errors = {version = "0.18.1", path = "./crates/nu-errors"}
|
||||
nu-parser = {version = "0.18.1", path = "./crates/nu-parser"}
|
||||
nu-plugin = {version = "0.18.1", path = "./crates/nu-plugin"}
|
||||
nu-protocol = {version = "0.18.1", path = "./crates/nu-protocol"}
|
||||
nu-source = {version = "0.18.1", path = "./crates/nu-source"}
|
||||
nu-value-ext = {version = "0.18.1", path = "./crates/nu-value-ext"}
|
||||
|
||||
nu_plugin_binaryview = {version = "0.18.1", path = "./crates/nu_plugin_binaryview", optional = true}
|
||||
nu_plugin_fetch = {version = "0.18.1", path = "./crates/nu_plugin_fetch", optional = true}
|
||||
nu_plugin_from_bson = {version = "0.18.1", path = "./crates/nu_plugin_from_bson", optional = true}
|
||||
nu_plugin_from_sqlite = {version = "0.18.1", path = "./crates/nu_plugin_from_sqlite", optional = true}
|
||||
nu_plugin_inc = {version = "0.18.1", path = "./crates/nu_plugin_inc", optional = true}
|
||||
nu_plugin_match = {version = "0.18.1", path = "./crates/nu_plugin_match", optional = true}
|
||||
nu_plugin_post = {version = "0.18.1", path = "./crates/nu_plugin_post", optional = true}
|
||||
nu_plugin_ps = {version = "0.18.1", path = "./crates/nu_plugin_ps", optional = true}
|
||||
nu_plugin_start = {version = "0.18.1", path = "./crates/nu_plugin_start", optional = true}
|
||||
nu_plugin_sys = {version = "0.18.1", path = "./crates/nu_plugin_sys", optional = true}
|
||||
nu_plugin_textview = {version = "0.18.1", path = "./crates/nu_plugin_textview", optional = true}
|
||||
nu_plugin_to_bson = {version = "0.18.1", path = "./crates/nu_plugin_to_bson", optional = true}
|
||||
nu_plugin_to_sqlite = {version = "0.18.1", path = "./crates/nu_plugin_to_sqlite", optional = true}
|
||||
nu_plugin_tree = {version = "0.18.1", path = "./crates/nu_plugin_tree", optional = true}
|
||||
|
||||
crossterm = {version = "0.17.5", optional = true}
|
||||
semver = {version = "0.10.0", optional = true}
|
||||
@ -51,13 +52,13 @@ dunce = "1.0.1"
|
||||
futures = {version = "0.3", features = ["compat", "io-compat"]}
|
||||
log = "0.4.8"
|
||||
pretty_env_logger = "0.4.0"
|
||||
quick-xml = "0.18.1"
|
||||
starship = "0.43.0"
|
||||
|
||||
[dev-dependencies]
|
||||
nu-test-support = {version = "0.17.0", path = "./crates/nu-test-support"}
|
||||
nu-test-support = {version = "0.18.1", path = "./crates/nu-test-support"}
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = {version = "0.17.0", path = "./crates/nu-build"}
|
||||
serde = {version = "1.0.110", features = ["derive"]}
|
||||
toml = "0.5.6"
|
||||
|
||||
|
12
README.md
12
README.md
@ -51,12 +51,12 @@ To build Nu, you will need to use the **latest stable (1.41 or later)** version
|
||||
Required dependencies:
|
||||
|
||||
* pkg-config and libssl (only needed on Linux)
|
||||
* on Debian/Ubuntu: `apt install pkg-config libssl-dev`
|
||||
* On Debian/Ubuntu: `apt install pkg-config libssl-dev`
|
||||
|
||||
Optional dependencies:
|
||||
|
||||
* To use Nu with all possible optional features enabled, you'll also need the following:
|
||||
* on Linux (on Debian/Ubuntu): `apt install libxcb-composite0-dev libx11-dev`
|
||||
* On Linux (on Debian/Ubuntu): `apt install libxcb-composite0-dev libx11-dev`
|
||||
|
||||
To install Nu via cargo (make sure you have installed [rustup](https://rustup.rs/) and the latest stable compiler via `rustup install stable`):
|
||||
|
||||
@ -194,7 +194,7 @@ For example, you can load a .toml file as structured data and explore it:
|
||||
> open Cargo.toml
|
||||
────────────────────┬───────────────────────────
|
||||
bin │ [table 18 rows]
|
||||
build-dependencies │ [row nu-build serde toml]
|
||||
build-dependencies │ [row serde toml]
|
||||
dependencies │ [row 29 columns]
|
||||
dev-dependencies │ [row nu-test-support]
|
||||
features │ [row 19 columns]
|
||||
@ -236,11 +236,11 @@ Here we use the variable `$it` to refer to the value being piped to the external
|
||||
|
||||
Nu has early support for configuring the shell. You can refer to the book for a list of [all supported variables](https://www.nushell.sh/book/en/configuration.html).
|
||||
|
||||
To set one of these variables, you can use `config --set`. For example:
|
||||
To set one of these variables, you can use `config set`. For example:
|
||||
|
||||
```shell
|
||||
> config --set [edit_mode "vi"]
|
||||
> config --set [path $nu.path]
|
||||
> config set edit_mode "vi"
|
||||
> config set path $nu.path
|
||||
```
|
||||
|
||||
### Shells
|
||||
|
Binary file not shown.
Binary file not shown.
3
build.rs
3
build.rs
@ -1,3 +0,0 @@
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
nu_build::build()
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
[package]
|
||||
authors = ["The Nu Project Contributors"]
|
||||
description = "Core build system for nushell"
|
||||
edition = "2018"
|
||||
license = "MIT"
|
||||
name = "nu-build"
|
||||
version = "0.17.0"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
lazy_static = "1.4.0"
|
||||
serde = {version = "1.0.114", features = ["derive"]}
|
||||
serde_json = "1.0.55"
|
||||
toml = "0.5.6"
|
@ -1,80 +0,0 @@
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Deserialize;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
|
||||
lazy_static! {
|
||||
static ref WORKSPACES: Mutex<BTreeMap<String, &'static Path>> = Mutex::new(BTreeMap::new());
|
||||
}
|
||||
|
||||
// got from https://github.com/mitsuhiko/insta/blob/b113499249584cb650150d2d01ed96ee66db6b30/src/runtime.rs#L67-L88
|
||||
|
||||
fn get_cargo_workspace(manifest_dir: &str) -> Result<Option<&Path>, Box<dyn std::error::Error>> {
|
||||
let mut workspaces = WORKSPACES.lock()?;
|
||||
if let Some(rv) = workspaces.get(manifest_dir) {
|
||||
Ok(Some(rv))
|
||||
} else {
|
||||
#[derive(Deserialize)]
|
||||
struct Manifest {
|
||||
workspace_root: String,
|
||||
}
|
||||
let output = std::process::Command::new(env!("CARGO"))
|
||||
.arg("metadata")
|
||||
.arg("--format-version=1")
|
||||
.current_dir(manifest_dir)
|
||||
.output()?;
|
||||
let manifest: Manifest = serde_json::from_slice(&output.stdout)?;
|
||||
let path = Box::leak(Box::new(PathBuf::from(manifest.workspace_root)));
|
||||
workspaces.insert(manifest_dir.to_string(), path.as_path());
|
||||
Ok(workspaces.get(manifest_dir).cloned())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Feature {
|
||||
#[allow(unused)]
|
||||
description: String,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
pub fn build() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let input = env::var("CARGO_MANIFEST_DIR")?;
|
||||
|
||||
let all_on = env::var("NUSHELL_ENABLE_ALL_FLAGS").is_ok();
|
||||
let flags: HashSet<String> = env::var("NUSHELL_ENABLE_FLAGS")
|
||||
.map(|s| s.split(',').map(|s| s.to_string()).collect())
|
||||
.unwrap_or_else(|_| HashSet::new());
|
||||
|
||||
if all_on && !flags.is_empty() {
|
||||
println!(
|
||||
"cargo:warning=Both NUSHELL_ENABLE_ALL_FLAGS and NUSHELL_ENABLE_FLAGS were set. You don't need both."
|
||||
);
|
||||
}
|
||||
|
||||
let workspace = match get_cargo_workspace(&input)? {
|
||||
// If the crate is being downloaded from crates.io, it won't have a workspace root, and that's ok
|
||||
None => return Ok(()),
|
||||
Some(workspace) => workspace,
|
||||
};
|
||||
|
||||
let path = Path::new(&workspace).join("features.toml");
|
||||
|
||||
// If the crate is being downloaded from crates.io, it won't have a features.toml, and that's ok
|
||||
if !path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let toml: HashMap<String, Feature> = toml::from_str(&std::fs::read_to_string(path)?)?;
|
||||
|
||||
for (key, value) in toml.iter() {
|
||||
if value.enabled || all_on || flags.contains(key) {
|
||||
println!("cargo:rustc-cfg={}", key);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -4,23 +4,23 @@ description = "CLI for nushell"
|
||||
edition = "2018"
|
||||
license = "MIT"
|
||||
name = "nu-cli"
|
||||
version = "0.17.0"
|
||||
version = "0.18.1"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
nu-errors = {version = "0.17.0", path = "../nu-errors"}
|
||||
nu-parser = {version = "0.17.0", path = "../nu-parser"}
|
||||
nu-plugin = {version = "0.17.0", path = "../nu-plugin"}
|
||||
nu-protocol = {version = "0.17.0", path = "../nu-protocol"}
|
||||
nu-source = {version = "0.17.0", path = "../nu-source"}
|
||||
nu-table = {version = "0.17.0", path = "../nu-table"}
|
||||
nu-test-support = {version = "0.17.0", path = "../nu-test-support"}
|
||||
nu-value-ext = {version = "0.17.0", path = "../nu-value-ext"}
|
||||
nu-errors = {version = "0.18.1", path = "../nu-errors"}
|
||||
nu-parser = {version = "0.18.1", path = "../nu-parser"}
|
||||
nu-plugin = {version = "0.18.1", path = "../nu-plugin"}
|
||||
nu-protocol = {version = "0.18.1", path = "../nu-protocol"}
|
||||
nu-source = {version = "0.18.1", path = "../nu-source"}
|
||||
nu-table = {version = "0.18.1", path = "../nu-table"}
|
||||
nu-test-support = {version = "0.18.1", path = "../nu-test-support"}
|
||||
nu-value-ext = {version = "0.18.1", path = "../nu-value-ext"}
|
||||
|
||||
ansi_term = "0.12.1"
|
||||
app_dirs = "1.2.1"
|
||||
app_dirs = {version = "2", package = "app_dirs2"}
|
||||
async-recursion = "0.3.1"
|
||||
async-trait = "0.1.36"
|
||||
base64 = "0.12.3"
|
||||
@ -36,6 +36,7 @@ ctrlc = {version = "3.1.4", optional = true}
|
||||
derive-new = "0.5.8"
|
||||
directories = {version = "2.0.2", optional = true}
|
||||
dirs = {version = "2.0.2", optional = true}
|
||||
dtparse = "1.1.0"
|
||||
dunce = "1.0.1"
|
||||
eml-parser = "0.1.0"
|
||||
filesize = "0.2.0"
|
||||
@ -66,6 +67,7 @@ query_interface = "0.3.5"
|
||||
rand = "0.7"
|
||||
regex = "1"
|
||||
roxmltree = "0.13.0"
|
||||
rust-embed = "5.6.0"
|
||||
rustyline = "6.2.0"
|
||||
serde = {version = "1.0.114", features = ["derive"]}
|
||||
serde-hjson = "0.9.1"
|
||||
@ -86,13 +88,16 @@ typetag = "0.1.5"
|
||||
umask = "1.0.0"
|
||||
unicode-xid = "0.2.1"
|
||||
uuid_crate = {package = "uuid", version = "0.8.1", features = ["v4"], optional = true}
|
||||
which = {version = "4.0.1", optional = true}
|
||||
which = {version = "4.0.2", optional = true}
|
||||
zip = "0.5.6"
|
||||
|
||||
clipboard = {version = "0.5", optional = true}
|
||||
encoding_rs = "0.8.23"
|
||||
quick-xml = "0.18.1"
|
||||
rayon = "1.3.1"
|
||||
starship = {version = "0.43.0", optional = true}
|
||||
trash = {version = "1.0.1", optional = true}
|
||||
url = {version = "2.1.1"}
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
users = "0.10.0"
|
||||
@ -109,7 +114,6 @@ optional = true
|
||||
version = "0.23.1"
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = {version = "0.17.0", path = "../nu-build"}
|
||||
|
||||
[dev-dependencies]
|
||||
quickcheck = "0.9"
|
||||
|
BIN
crates/nu-cli/assets/228_themes.zip
Normal file
BIN
crates/nu-cli/assets/228_themes.zip
Normal file
Binary file not shown.
@ -7,6 +7,8 @@ use crate::context::Context;
|
||||
use crate::git::current_branch;
|
||||
use crate::path::canonicalize;
|
||||
use crate::prelude::*;
|
||||
use crate::shell::completer::NuCompleter;
|
||||
use crate::shell::Helper;
|
||||
use crate::EnvironmentSyncer;
|
||||
use futures_codec::FramedRead;
|
||||
use nu_errors::{ProximateShellError, ShellDiagnostic, ShellError};
|
||||
@ -16,11 +18,9 @@ use nu_protocol::{Primitive, ReturnSuccess, Signature, UntaggedValue, Value};
|
||||
use nu_source::Tagged;
|
||||
|
||||
use log::{debug, trace};
|
||||
use rustyline::config::{ColorMode, CompletionType, Config};
|
||||
use rustyline::error::ReadlineError;
|
||||
use rustyline::{
|
||||
self, config::Configurer, config::EditMode, At, Cmd, ColorMode, CompletionType, Config, Editor,
|
||||
KeyPress, Movement, Word,
|
||||
};
|
||||
use rustyline::{self, config::Configurer, At, Cmd, Editor, KeyPress, Movement, Word};
|
||||
use std::error::Error;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::iter::Iterator;
|
||||
@ -203,12 +203,28 @@ pub struct History;
|
||||
impl History {
|
||||
pub fn path() -> PathBuf {
|
||||
const FNAME: &str = "history.txt";
|
||||
config::user_data()
|
||||
let default = config::user_data()
|
||||
.map(|mut p| {
|
||||
p.push(FNAME);
|
||||
p
|
||||
})
|
||||
.unwrap_or_else(|_| PathBuf::from(FNAME))
|
||||
.unwrap_or_else(|_| PathBuf::from(FNAME));
|
||||
|
||||
let cfg = crate::data::config::config(Tag::unknown());
|
||||
if let Ok(c) = cfg {
|
||||
match &c.get("history-path") {
|
||||
Some(Value {
|
||||
value: UntaggedValue::Primitive(p),
|
||||
..
|
||||
}) => match p {
|
||||
Primitive::String(path) => PathBuf::from(path),
|
||||
_ => default,
|
||||
},
|
||||
_ => default,
|
||||
}
|
||||
} else {
|
||||
default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -310,7 +326,13 @@ pub fn create_default_context(
|
||||
whole_stream_command(StrSubstring),
|
||||
whole_stream_command(StrSet),
|
||||
whole_stream_command(StrToDatetime),
|
||||
whole_stream_command(StrContains),
|
||||
whole_stream_command(StrIndexOf),
|
||||
whole_stream_command(StrTrim),
|
||||
whole_stream_command(StrTrimLeft),
|
||||
whole_stream_command(StrTrimRight),
|
||||
whole_stream_command(StrStartsWith),
|
||||
whole_stream_command(StrEndsWith),
|
||||
whole_stream_command(StrCollect),
|
||||
whole_stream_command(StrLength),
|
||||
whole_stream_command(StrReverse),
|
||||
@ -360,6 +382,7 @@ pub fn create_default_context(
|
||||
whole_stream_command(Wrap),
|
||||
whole_stream_command(Pivot),
|
||||
whole_stream_command(Headers),
|
||||
whole_stream_command(Reduce),
|
||||
// Data processing
|
||||
whole_stream_command(Histogram),
|
||||
whole_stream_command(Autoenv),
|
||||
@ -385,6 +408,7 @@ pub fn create_default_context(
|
||||
whole_stream_command(ToTSV),
|
||||
whole_stream_command(ToURL),
|
||||
whole_stream_command(ToYAML),
|
||||
whole_stream_command(ToXML),
|
||||
// File format input
|
||||
whole_stream_command(From),
|
||||
whole_stream_command(FromCSV),
|
||||
@ -410,6 +434,19 @@ pub fn create_default_context(
|
||||
whole_stream_command(RandomDice),
|
||||
#[cfg(feature = "uuid_crate")]
|
||||
whole_stream_command(RandomUUID),
|
||||
// Path
|
||||
whole_stream_command(PathCommand),
|
||||
whole_stream_command(PathExtension),
|
||||
whole_stream_command(PathBasename),
|
||||
whole_stream_command(PathExpand),
|
||||
whole_stream_command(PathExists),
|
||||
whole_stream_command(PathType),
|
||||
// Url
|
||||
whole_stream_command(UrlCommand),
|
||||
whole_stream_command(UrlScheme),
|
||||
whole_stream_command(UrlPath),
|
||||
whole_stream_command(UrlHost),
|
||||
whole_stream_command(UrlQuery),
|
||||
]);
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
@ -521,18 +558,12 @@ pub async fn run_pipeline_standalone(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The entry point for the CLI. Will register all known internal commands, load experimental commands, load plugins, then prepare the prompt and line reader for input.
|
||||
pub async fn cli(
|
||||
mut syncer: EnvironmentSyncer,
|
||||
mut context: Context,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
pub fn set_rustyline_configuration() -> (Editor<Helper>, IndexMap<String, Value>) {
|
||||
#[cfg(windows)]
|
||||
const DEFAULT_COMPLETION_MODE: CompletionType = CompletionType::Circular;
|
||||
#[cfg(not(windows))]
|
||||
const DEFAULT_COMPLETION_MODE: CompletionType = CompletionType::List;
|
||||
|
||||
let _ = load_plugins(&mut context);
|
||||
|
||||
let config = Config::builder().color_mode(ColorMode::Forced).build();
|
||||
let mut rl: Editor<_> = Editor::with_config(config);
|
||||
|
||||
@ -546,18 +577,185 @@ pub async fn cli(
|
||||
Cmd::Move(Movement::ForwardWord(1, At::AfterEnd, Word::Vi)),
|
||||
);
|
||||
|
||||
// Let's set the defaults up front and then override them later if the user indicates
|
||||
// defaults taken from here https://github.com/kkawakam/rustyline/blob/2fe886c9576c1ea13ca0e5808053ad491a6fe049/src/config.rs#L150-L167
|
||||
rl.set_max_history_size(100);
|
||||
rl.set_history_ignore_dups(true);
|
||||
rl.set_history_ignore_space(false);
|
||||
rl.set_completion_type(DEFAULT_COMPLETION_MODE);
|
||||
rl.set_completion_prompt_limit(100);
|
||||
rl.set_keyseq_timeout(-1);
|
||||
rl.set_edit_mode(rustyline::config::EditMode::Emacs);
|
||||
rl.set_auto_add_history(false);
|
||||
rl.set_bell_style(rustyline::config::BellStyle::default());
|
||||
rl.set_color_mode(rustyline::ColorMode::Enabled);
|
||||
rl.set_tab_stop(8);
|
||||
|
||||
if let Err(e) = crate::keybinding::load_keybindings(&mut rl) {
|
||||
println!("Error loading keybindings: {:?}", e);
|
||||
}
|
||||
|
||||
let config = match config::config(Tag::unknown()) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
eprintln!("Config could not be loaded.");
|
||||
if let ShellError {
|
||||
error: ProximateShellError::Diagnostic(ShellDiagnostic { diagnostic }),
|
||||
..
|
||||
} = e
|
||||
{
|
||||
eprintln!("{}", diagnostic.message);
|
||||
}
|
||||
IndexMap::new()
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(config) = config::config(Tag::unknown()) {
|
||||
if let Some(line_editor_vars) = config.get("line_editor") {
|
||||
for (idx, value) in line_editor_vars.row_entries() {
|
||||
match idx.as_ref() {
|
||||
"max_history_size" => {
|
||||
if let Ok(max_history_size) = value.as_u64() {
|
||||
rl.set_max_history_size(max_history_size as usize);
|
||||
}
|
||||
}
|
||||
"history_duplicates" => {
|
||||
// history_duplicates = match value.as_string() {
|
||||
// Ok(s) if s.to_lowercase() == "alwaysadd" => {
|
||||
// rustyline::config::HistoryDuplicates::AlwaysAdd
|
||||
// }
|
||||
// Ok(s) if s.to_lowercase() == "ignoreconsecutive" => {
|
||||
// rustyline::config::HistoryDuplicates::IgnoreConsecutive
|
||||
// }
|
||||
// _ => rustyline::config::HistoryDuplicates::AlwaysAdd,
|
||||
// };
|
||||
if let Ok(history_duplicates) = value.as_bool() {
|
||||
rl.set_history_ignore_dups(history_duplicates);
|
||||
}
|
||||
}
|
||||
"history_ignore_space" => {
|
||||
if let Ok(history_ignore_space) = value.as_bool() {
|
||||
rl.set_history_ignore_space(history_ignore_space);
|
||||
}
|
||||
}
|
||||
"completion_type" => {
|
||||
let completion_type = match value.as_string() {
|
||||
Ok(s) if s.to_lowercase() == "circular" => {
|
||||
rustyline::config::CompletionType::Circular
|
||||
}
|
||||
Ok(s) if s.to_lowercase() == "list" => {
|
||||
rustyline::config::CompletionType::List
|
||||
}
|
||||
#[cfg(all(unix, feature = "with-fuzzy"))]
|
||||
Ok(s) if s.to_lowercase() == "fuzzy" => {
|
||||
rustyline::config::CompletionType::Fuzzy
|
||||
}
|
||||
_ => DEFAULT_COMPLETION_MODE,
|
||||
};
|
||||
rl.set_completion_type(completion_type);
|
||||
}
|
||||
"completion_prompt_limit" => {
|
||||
if let Ok(completion_prompt_limit) = value.as_u64() {
|
||||
rl.set_completion_prompt_limit(completion_prompt_limit as usize);
|
||||
}
|
||||
}
|
||||
"keyseq_timeout_ms" => {
|
||||
if let Ok(keyseq_timeout_ms) = value.as_u64() {
|
||||
rl.set_keyseq_timeout(keyseq_timeout_ms as i32);
|
||||
}
|
||||
}
|
||||
"edit_mode" => {
|
||||
let edit_mode = match value.as_string() {
|
||||
Ok(s) if s.to_lowercase() == "vi" => rustyline::config::EditMode::Vi,
|
||||
Ok(s) if s.to_lowercase() == "emacs" => {
|
||||
rustyline::config::EditMode::Emacs
|
||||
}
|
||||
_ => rustyline::config::EditMode::Emacs,
|
||||
};
|
||||
rl.set_edit_mode(edit_mode);
|
||||
// Note: When edit_mode is Emacs, the keyseq_timeout_ms is set to -1
|
||||
// no matter what you may have configured. This is so that key chords
|
||||
// can be applied without having to do them in a given timeout. So,
|
||||
// it essentially turns off the keyseq timeout.
|
||||
}
|
||||
"auto_add_history" => {
|
||||
if let Ok(auto_add_history) = value.as_bool() {
|
||||
rl.set_auto_add_history(auto_add_history);
|
||||
}
|
||||
}
|
||||
"bell_style" => {
|
||||
let bell_style = match value.as_string() {
|
||||
Ok(s) if s.to_lowercase() == "audible" => {
|
||||
rustyline::config::BellStyle::Audible
|
||||
}
|
||||
Ok(s) if s.to_lowercase() == "none" => {
|
||||
rustyline::config::BellStyle::None
|
||||
}
|
||||
Ok(s) if s.to_lowercase() == "visible" => {
|
||||
rustyline::config::BellStyle::Visible
|
||||
}
|
||||
_ => rustyline::config::BellStyle::default(),
|
||||
};
|
||||
rl.set_bell_style(bell_style);
|
||||
}
|
||||
"color_mode" => {
|
||||
let color_mode = match value.as_string() {
|
||||
Ok(s) if s.to_lowercase() == "enabled" => rustyline::ColorMode::Enabled,
|
||||
Ok(s) if s.to_lowercase() == "forced" => rustyline::ColorMode::Forced,
|
||||
Ok(s) if s.to_lowercase() == "disabled" => {
|
||||
rustyline::ColorMode::Disabled
|
||||
}
|
||||
_ => rustyline::ColorMode::Enabled,
|
||||
};
|
||||
rl.set_color_mode(color_mode);
|
||||
}
|
||||
"tab_stop" => {
|
||||
if let Ok(tab_stop) = value.as_u64() {
|
||||
rl.set_tab_stop(tab_stop as usize);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we are ok if history does not exist
|
||||
let _ = rl.load_history(&History::path());
|
||||
|
||||
(rl, config)
|
||||
}
|
||||
|
||||
/// The entry point for the CLI. Will register all known internal commands, load experimental commands, load plugins, then prepare the prompt and line reader for input.
|
||||
pub async fn cli(
|
||||
mut syncer: EnvironmentSyncer,
|
||||
mut context: Context,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let _ = load_plugins(&mut context);
|
||||
|
||||
let (mut rl, config) = set_rustyline_configuration();
|
||||
|
||||
let skip_welcome_message = config
|
||||
.get("skip_welcome_message")
|
||||
.map(|x| x.is_true())
|
||||
.unwrap_or(false);
|
||||
if !skip_welcome_message {
|
||||
println!(
|
||||
"Welcome to Nushell {} (type 'help' for more info)",
|
||||
clap::crate_version!()
|
||||
);
|
||||
}
|
||||
|
||||
let use_starship = config
|
||||
.get("use_starship")
|
||||
.map(|x| x.is_true())
|
||||
.unwrap_or(false);
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = ansi_term::enable_ansi_support();
|
||||
}
|
||||
|
||||
// we are ok if history does not exist
|
||||
let _ = rl.load_history(&History::path());
|
||||
|
||||
#[cfg(feature = "ctrlc")]
|
||||
{
|
||||
let cc = context.ctrl_c.clone();
|
||||
@ -604,68 +802,10 @@ pub async fn cli(
|
||||
|
||||
let cwd = context.shell_manager.path();
|
||||
|
||||
rl.set_helper(Some(crate::shell::Helper::new(context.clone())));
|
||||
|
||||
let config = match config::config(Tag::unknown()) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
eprintln!("Config could not be loaded.");
|
||||
if let ShellError {
|
||||
error: ProximateShellError::Diagnostic(ShellDiagnostic { diagnostic }),
|
||||
..
|
||||
} = e
|
||||
{
|
||||
eprintln!("{}", diagnostic.message);
|
||||
}
|
||||
IndexMap::new()
|
||||
}
|
||||
};
|
||||
|
||||
let use_starship = match config.get("use_starship") {
|
||||
Some(b) => match b.as_bool() {
|
||||
Ok(b) => b,
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let edit_mode = config
|
||||
.get("edit_mode")
|
||||
.map(|s| match s.value.expect_string() {
|
||||
"vi" => EditMode::Vi,
|
||||
"emacs" => EditMode::Emacs,
|
||||
_ => EditMode::Emacs,
|
||||
})
|
||||
.unwrap_or(EditMode::Emacs);
|
||||
|
||||
rl.set_edit_mode(edit_mode);
|
||||
|
||||
let max_history_size = config
|
||||
.get("history_size")
|
||||
.map(|i| i.value.expect_int())
|
||||
.unwrap_or(100_000);
|
||||
|
||||
// rl.set_max_history_size(max_history_size as usize);
|
||||
rustyline::config::Configurer::set_max_history_size(&mut rl, max_history_size as usize);
|
||||
rustyline::Editor::set_max_history_size(&mut rl, max_history_size as usize);
|
||||
|
||||
let key_timeout = config
|
||||
.get("key_timeout")
|
||||
.map(|s| s.value.expect_int())
|
||||
.unwrap_or(1);
|
||||
|
||||
rl.set_keyseq_timeout(key_timeout as i32);
|
||||
|
||||
let completion_mode = config
|
||||
.get("completion_mode")
|
||||
.map(|s| match s.value.expect_string() {
|
||||
"list" => CompletionType::List,
|
||||
"circular" => CompletionType::Circular,
|
||||
_ => DEFAULT_COMPLETION_MODE,
|
||||
})
|
||||
.unwrap_or(DEFAULT_COMPLETION_MODE);
|
||||
|
||||
rl.set_completion_type(completion_mode);
|
||||
rl.set_helper(Some(crate::shell::Helper::new(
|
||||
Box::new(<NuCompleter as Default>::default()),
|
||||
context.clone(),
|
||||
)));
|
||||
|
||||
let colored_prompt = {
|
||||
if use_starship {
|
||||
@ -1012,18 +1152,15 @@ pub async fn process_line(
|
||||
let file = futures::io::AllowStdIo::new(std::io::stdin());
|
||||
let stream = FramedRead::new(file, MaybeTextCodec::default()).map(|line| {
|
||||
if let Ok(line) = line {
|
||||
match line {
|
||||
StringOrBinary::String(s) => Ok(Value {
|
||||
value: UntaggedValue::Primitive(Primitive::String(s)),
|
||||
tag: Tag::unknown(),
|
||||
}),
|
||||
StringOrBinary::Binary(b) => Ok(Value {
|
||||
value: UntaggedValue::Primitive(Primitive::Binary(
|
||||
b.into_iter().collect(),
|
||||
)),
|
||||
tag: Tag::unknown(),
|
||||
}),
|
||||
}
|
||||
let primitive = match line {
|
||||
StringOrBinary::String(s) => Primitive::String(s),
|
||||
StringOrBinary::Binary(b) => Primitive::Binary(b.into_iter().collect()),
|
||||
};
|
||||
|
||||
Ok(Value {
|
||||
value: UntaggedValue::Primitive(primitive),
|
||||
tag: Tag::unknown(),
|
||||
})
|
||||
} else {
|
||||
panic!("Internal error: could not read lines of text from stdin")
|
||||
}
|
||||
|
@ -76,6 +76,7 @@ pub(crate) mod next;
|
||||
pub(crate) mod nth;
|
||||
pub(crate) mod open;
|
||||
pub(crate) mod parse;
|
||||
pub(crate) mod path;
|
||||
pub(crate) mod pivot;
|
||||
pub(crate) mod plugin;
|
||||
pub(crate) mod prepend;
|
||||
@ -83,6 +84,7 @@ pub(crate) mod prev;
|
||||
pub(crate) mod pwd;
|
||||
pub(crate) mod random;
|
||||
pub(crate) mod range;
|
||||
pub(crate) mod reduce;
|
||||
pub(crate) mod reject;
|
||||
pub(crate) mod rename;
|
||||
pub(crate) mod reverse;
|
||||
@ -109,10 +111,12 @@ pub(crate) mod to_md;
|
||||
pub(crate) mod to_toml;
|
||||
pub(crate) mod to_tsv;
|
||||
pub(crate) mod to_url;
|
||||
pub(crate) mod to_xml;
|
||||
pub(crate) mod to_yaml;
|
||||
pub(crate) mod trim;
|
||||
pub(crate) mod uniq;
|
||||
pub(crate) mod update;
|
||||
pub(crate) mod url_;
|
||||
pub(crate) mod version;
|
||||
pub(crate) mod what;
|
||||
pub(crate) mod where_;
|
||||
@ -202,6 +206,7 @@ pub(crate) use next::Next;
|
||||
pub(crate) use nth::Nth;
|
||||
pub(crate) use open::Open;
|
||||
pub(crate) use parse::Parse;
|
||||
pub(crate) use path::{PathBasename, PathCommand, PathExists, PathExpand, PathExtension, PathType};
|
||||
pub(crate) use pivot::Pivot;
|
||||
pub(crate) use prepend::Prepend;
|
||||
pub(crate) use prev::Previous;
|
||||
@ -210,6 +215,7 @@ pub(crate) use pwd::Pwd;
|
||||
pub(crate) use random::RandomUUID;
|
||||
pub(crate) use random::{Random, RandomBool, RandomDice};
|
||||
pub(crate) use range::Range;
|
||||
pub(crate) use reduce::Reduce;
|
||||
pub(crate) use reject::Reject;
|
||||
pub(crate) use rename::Rename;
|
||||
pub(crate) use reverse::Reverse;
|
||||
@ -225,8 +231,9 @@ pub(crate) use sort_by::SortBy;
|
||||
pub(crate) use split::{Split, SplitChars, SplitColumn, SplitRow};
|
||||
pub(crate) use split_by::SplitBy;
|
||||
pub(crate) use str_::{
|
||||
Str, StrCapitalize, StrCollect, StrDowncase, StrFindReplace, StrFrom, StrLength, StrReverse,
|
||||
StrSet, StrSubstring, StrToDatetime, StrToDecimal, StrToInteger, StrTrim, StrUpcase,
|
||||
Str, StrCapitalize, StrCollect, StrContains, StrDowncase, StrEndsWith, StrFindReplace, StrFrom,
|
||||
StrIndexOf, StrLength, StrReverse, StrSet, StrStartsWith, StrSubstring, StrToDatetime,
|
||||
StrToDecimal, StrToInteger, StrTrim, StrTrimLeft, StrTrimRight, StrUpcase,
|
||||
};
|
||||
pub(crate) use table::Table;
|
||||
pub(crate) use tags::Tags;
|
||||
@ -238,10 +245,12 @@ pub(crate) use to_md::ToMarkdown;
|
||||
pub(crate) use to_toml::ToTOML;
|
||||
pub(crate) use to_tsv::ToTSV;
|
||||
pub(crate) use to_url::ToURL;
|
||||
pub(crate) use to_xml::ToXML;
|
||||
pub(crate) use to_yaml::ToYAML;
|
||||
pub(crate) use touch::Touch;
|
||||
pub(crate) use trim::Trim;
|
||||
pub(crate) use uniq::Uniq;
|
||||
pub(crate) use url_::{UrlCommand, UrlHost, UrlPath, UrlQuery, UrlScheme};
|
||||
pub(crate) use version::Version;
|
||||
pub(crate) use what::What;
|
||||
pub(crate) use where_::Where;
|
||||
|
@ -3,7 +3,7 @@ use crate::commands::WholeStreamCommand;
|
||||
use crate::data::value::format_leaf;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{hir, hir::Expression, hir::Literal, hir::SpannedExpression};
|
||||
use nu_protocol::hir::{self, Expression, ExternalRedirection, Literal, SpannedExpression};
|
||||
use nu_protocol::{Primitive, Scope, Signature, UntaggedValue, Value};
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
@ -328,7 +328,7 @@ fn create_default_command_args(context: &RunnableContextWithoutInput) -> RawComm
|
||||
positional: None,
|
||||
named: None,
|
||||
span,
|
||||
is_last: true,
|
||||
external_redirection: ExternalRedirection::Stdout,
|
||||
},
|
||||
name_tag: context.name.clone(),
|
||||
scope: Scope::new(),
|
||||
|
@ -64,17 +64,15 @@ async fn benchmark(
|
||||
)
|
||||
.await;
|
||||
|
||||
let output = match result {
|
||||
Ok(mut stream) => {
|
||||
let _ = stream.drain_vec().await;
|
||||
let run_duration: chrono::Duration = Utc::now().signed_duration_since(start_time);
|
||||
context.clear_errors();
|
||||
Ok(ReturnSuccess::Value(Value {
|
||||
value: UntaggedValue::Primitive(Primitive::from(run_duration)),
|
||||
tag: Tag::from(block.span),
|
||||
}))
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
let _ = result?.drain_vec().await;
|
||||
let run_duration: chrono::Duration = Utc::now().signed_duration_since(start_time);
|
||||
|
||||
context.clear_errors();
|
||||
|
||||
let output = Ok(ReturnSuccess::Value(Value {
|
||||
value: UntaggedValue::Primitive(Primitive::from(run_duration)),
|
||||
tag: Tag::from(block.span),
|
||||
}));
|
||||
|
||||
Ok(OutputStream::from(vec![output]))
|
||||
}
|
||||
|
@ -100,7 +100,7 @@ pub async fn cal(
|
||||
(current_month, current_month)
|
||||
};
|
||||
|
||||
let add_months_of_year_to_table_result = add_months_of_year_to_table(
|
||||
add_months_of_year_to_table(
|
||||
&args,
|
||||
&mut calendar_vec_deque,
|
||||
&tag,
|
||||
@ -108,12 +108,9 @@ pub async fn cal(
|
||||
month_range,
|
||||
current_month,
|
||||
current_day_option,
|
||||
);
|
||||
)?;
|
||||
|
||||
match add_months_of_year_to_table_result {
|
||||
Ok(()) => Ok(futures::stream::iter(calendar_vec_deque).to_output_stream()),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
Ok(futures::stream::iter(calendar_vec_deque).to_output_stream())
|
||||
}
|
||||
|
||||
fn get_invalid_year_shell_error(year_tag: &Tag) -> ShellError {
|
||||
|
@ -70,27 +70,21 @@ async fn run_pipeline(
|
||||
vars: &IndexMap<String, Value>,
|
||||
env: &IndexMap<String, String>,
|
||||
) -> Result<InputStream, ShellError> {
|
||||
let mut iter = commands.list.clone().into_iter().peekable();
|
||||
loop {
|
||||
let item: Option<ClassifiedCommand> = iter.next();
|
||||
let next: Option<&ClassifiedCommand> = iter.peek();
|
||||
|
||||
input = match (item, next) {
|
||||
(Some(ClassifiedCommand::Dynamic(_)), _) | (_, Some(ClassifiedCommand::Dynamic(_))) => {
|
||||
for item in commands.list.clone() {
|
||||
input = match item {
|
||||
ClassifiedCommand::Dynamic(_) => {
|
||||
return Err(ShellError::unimplemented("Dynamic commands"))
|
||||
}
|
||||
|
||||
(Some(ClassifiedCommand::Expr(expr)), _) => {
|
||||
ClassifiedCommand::Expr(expr) => {
|
||||
run_expression_block(*expr, ctx, it, vars, env).await?
|
||||
}
|
||||
(Some(ClassifiedCommand::Error(err)), _) => return Err(err.into()),
|
||||
(_, Some(ClassifiedCommand::Error(err))) => return Err(err.clone().into()),
|
||||
|
||||
(Some(ClassifiedCommand::Internal(left)), _) => {
|
||||
ClassifiedCommand::Error(err) => return Err(err.into()),
|
||||
|
||||
ClassifiedCommand::Internal(left) => {
|
||||
run_internal_command(left, ctx, input, it, vars, env).await?
|
||||
}
|
||||
|
||||
(None, _) => break,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ use futures_codec::FramedRead;
|
||||
use log::trace;
|
||||
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::hir::ExternalCommand;
|
||||
use nu_protocol::hir::{ExternalCommand, ExternalRedirection};
|
||||
use nu_protocol::{Primitive, Scope, ShellTypeName, UntaggedValue, Value};
|
||||
use nu_source::Tag;
|
||||
|
||||
@ -22,7 +22,7 @@ pub(crate) async fn run_external_command(
|
||||
context: &mut Context,
|
||||
input: InputStream,
|
||||
scope: &Scope,
|
||||
is_last: bool,
|
||||
external_redirection: ExternalRedirection,
|
||||
) -> Result<InputStream, ShellError> {
|
||||
trace!(target: "nu::run::external", "-> {}", command.name);
|
||||
|
||||
@ -34,7 +34,7 @@ pub(crate) async fn run_external_command(
|
||||
));
|
||||
}
|
||||
|
||||
run_with_stdin(command, context, input, scope, is_last).await
|
||||
run_with_stdin(command, context, input, scope, external_redirection).await
|
||||
}
|
||||
|
||||
async fn run_with_stdin(
|
||||
@ -42,7 +42,7 @@ async fn run_with_stdin(
|
||||
context: &mut Context,
|
||||
input: InputStream,
|
||||
scope: &Scope,
|
||||
is_last: bool,
|
||||
external_redirection: ExternalRedirection,
|
||||
) -> Result<InputStream, ShellError> {
|
||||
let path = context.shell_manager.path();
|
||||
|
||||
@ -62,9 +62,29 @@ async fn run_with_stdin(
|
||||
}
|
||||
|
||||
// Do the cleanup that we need to do on any argument going out:
|
||||
let trimmed_value_string = value.as_string()?.trim_end_matches('\n').to_string();
|
||||
|
||||
command_args.push(trimmed_value_string);
|
||||
match &value.value {
|
||||
UntaggedValue::Table(table) => {
|
||||
for t in table {
|
||||
match &t.value {
|
||||
UntaggedValue::Primitive(_) => {
|
||||
command_args
|
||||
.push(t.convert_to_string().trim_end_matches('\n').to_string());
|
||||
}
|
||||
_ => {
|
||||
return Err(ShellError::labeled_error(
|
||||
"Could not convert to positional arguments",
|
||||
"could not convert to positional arguments",
|
||||
value.tag(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let trimmed_value_string = value.as_string()?.trim_end_matches('\n').to_string();
|
||||
command_args.push(trimmed_value_string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let process_args = command_args
|
||||
@ -102,7 +122,14 @@ async fn run_with_stdin(
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
spawn(&command, &path, &process_args[..], input, is_last, scope)
|
||||
spawn(
|
||||
&command,
|
||||
&path,
|
||||
&process_args[..],
|
||||
input,
|
||||
external_redirection,
|
||||
scope,
|
||||
)
|
||||
}
|
||||
|
||||
fn spawn(
|
||||
@ -110,7 +137,7 @@ fn spawn(
|
||||
path: &str,
|
||||
args: &[String],
|
||||
input: InputStream,
|
||||
is_last: bool,
|
||||
external_redirection: ExternalRedirection,
|
||||
scope: &Scope,
|
||||
) -> Result<InputStream, ShellError> {
|
||||
let command = command.clone();
|
||||
@ -146,12 +173,22 @@ fn spawn(
|
||||
|
||||
// We want stdout regardless of what
|
||||
// we are doing ($it case or pipe stdin)
|
||||
if !is_last {
|
||||
process.stdout(Stdio::piped());
|
||||
trace!(target: "nu::run::external", "set up stdout pipe");
|
||||
|
||||
process.stderr(Stdio::piped());
|
||||
trace!(target: "nu::run::external", "set up stderr pipe");
|
||||
match external_redirection {
|
||||
ExternalRedirection::Stdout => {
|
||||
process.stdout(Stdio::piped());
|
||||
trace!(target: "nu::run::external", "set up stdout pipe");
|
||||
}
|
||||
ExternalRedirection::Stderr => {
|
||||
process.stderr(Stdio::piped());
|
||||
trace!(target: "nu::run::external", "set up stderr pipe");
|
||||
}
|
||||
ExternalRedirection::StdoutAndStderr => {
|
||||
process.stdout(Stdio::piped());
|
||||
trace!(target: "nu::run::external", "set up stdout pipe");
|
||||
process.stderr(Stdio::piped());
|
||||
trace!(target: "nu::run::external", "set up stderr pipe");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// open since we have some contents for stdin
|
||||
@ -235,7 +272,9 @@ fn spawn(
|
||||
});
|
||||
|
||||
std::thread::spawn(move || {
|
||||
if !is_last {
|
||||
if external_redirection == ExternalRedirection::Stdout
|
||||
|| external_redirection == ExternalRedirection::StdoutAndStderr
|
||||
{
|
||||
let stdout = if let Some(stdout) = child.stdout.take() {
|
||||
stdout
|
||||
} else {
|
||||
@ -250,20 +289,6 @@ fn spawn(
|
||||
return Err(());
|
||||
};
|
||||
|
||||
let stderr = if let Some(stderr) = child.stderr.take() {
|
||||
stderr
|
||||
} else {
|
||||
let _ = stdout_read_tx.send(Ok(Value {
|
||||
value: UntaggedValue::Error(ShellError::labeled_error(
|
||||
"Can't redirect the stderr for external command",
|
||||
"can't redirect stderr",
|
||||
&stdout_name_tag,
|
||||
)),
|
||||
tag: stdout_name_tag,
|
||||
}));
|
||||
return Err(());
|
||||
};
|
||||
|
||||
let file = futures::io::AllowStdIo::new(stdout);
|
||||
let stream = FramedRead::new(file, MaybeTextCodec::default());
|
||||
|
||||
@ -317,17 +342,34 @@ fn spawn(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if external_redirection == ExternalRedirection::Stderr
|
||||
|| external_redirection == ExternalRedirection::StdoutAndStderr
|
||||
{
|
||||
let stderr = if let Some(stderr) = child.stderr.take() {
|
||||
stderr
|
||||
} else {
|
||||
let _ = stdout_read_tx.send(Ok(Value {
|
||||
value: UntaggedValue::Error(ShellError::labeled_error(
|
||||
"Can't redirect the stderr for external command",
|
||||
"can't redirect stderr",
|
||||
&stdout_name_tag,
|
||||
)),
|
||||
tag: stdout_name_tag,
|
||||
}));
|
||||
return Err(());
|
||||
};
|
||||
|
||||
let file = futures::io::AllowStdIo::new(stderr);
|
||||
let err_stream = FramedRead::new(file, MaybeTextCodec::default());
|
||||
let stream = FramedRead::new(file, MaybeTextCodec::default());
|
||||
|
||||
for err_line in block_on_stream(err_stream) {
|
||||
match err_line {
|
||||
for line in block_on_stream(stream) {
|
||||
match line {
|
||||
Ok(line) => match line {
|
||||
StringOrBinary::String(s) => {
|
||||
let result = stdout_read_tx.send(Ok(Value {
|
||||
value: UntaggedValue::Error(
|
||||
ShellError::untagged_runtime_error(s.clone()),
|
||||
ShellError::untagged_runtime_error(s),
|
||||
),
|
||||
tag: stdout_name_tag.clone(),
|
||||
}));
|
||||
@ -339,9 +381,7 @@ fn spawn(
|
||||
StringOrBinary::Binary(_) => {
|
||||
let result = stdout_read_tx.send(Ok(Value {
|
||||
value: UntaggedValue::Error(
|
||||
ShellError::untagged_runtime_error(
|
||||
"Binary in stderr output",
|
||||
),
|
||||
ShellError::untagged_runtime_error("<binary stderr>"),
|
||||
),
|
||||
tag: stdout_name_tag.clone(),
|
||||
}));
|
||||
@ -363,8 +403,8 @@ fn spawn(
|
||||
if should_error {
|
||||
let _ = stdout_read_tx.send(Ok(Value {
|
||||
value: UntaggedValue::Error(ShellError::labeled_error(
|
||||
format!("Unable to read from stderr ({})", e),
|
||||
"unable to read from stderr",
|
||||
format!("Unable to read from stdout ({})", e),
|
||||
"unable to read from stdout",
|
||||
&stdout_name_tag,
|
||||
)),
|
||||
tag: stdout_name_tag.clone(),
|
||||
@ -435,10 +475,11 @@ pub fn did_find_command(#[allow(unused)] name: &str) -> bool {
|
||||
if which::which(name).is_ok() {
|
||||
true
|
||||
} else {
|
||||
// Reference: https://ss64.com/nt/syntax-internal.html
|
||||
let cmd_builtins = [
|
||||
"call", "cls", "color", "date", "dir", "echo", "find", "hostname", "pause",
|
||||
"start", "time", "title", "ver", "copy", "mkdir", "rename", "rd", "rmdir", "type",
|
||||
"mklink",
|
||||
"assoc", "break", "color", "copy", "date", "del", "dir", "dpath", "echo", "erase",
|
||||
"for", "ftype", "md", "mkdir", "mklink", "move", "path", "ren", "rename", "rd",
|
||||
"rmdir", "set", "start", "time", "title", "type", "ver", "verify", "vol",
|
||||
];
|
||||
|
||||
cmd_builtins.contains(&name)
|
||||
@ -528,16 +569,21 @@ mod tests {
|
||||
|
||||
#[cfg(feature = "which")]
|
||||
async fn non_existent_run() -> Result<(), ShellError> {
|
||||
use nu_protocol::hir::ExternalRedirection;
|
||||
let cmd = ExternalBuilder::for_name("i_dont_exist.exe").build();
|
||||
|
||||
let input = InputStream::empty();
|
||||
let mut ctx = Context::basic().expect("There was a problem creating a basic context.");
|
||||
|
||||
assert!(
|
||||
run_external_command(cmd, &mut ctx, input, &Scope::new(), false)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert!(run_external_command(
|
||||
cmd,
|
||||
&mut ctx,
|
||||
input,
|
||||
&Scope::new(),
|
||||
ExternalRedirection::Stdout
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ use crate::commands::UnevaluatedCallInfo;
|
||||
use crate::prelude::*;
|
||||
use log::{log_enabled, trace};
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::hir::InternalCommand;
|
||||
use nu_protocol::hir::{ExternalRedirection, InternalCommand};
|
||||
use nu_protocol::{CommandAction, Primitive, ReturnSuccess, Scope, UntaggedValue, Value};
|
||||
|
||||
pub(crate) async fn run_internal_command(
|
||||
@ -87,7 +87,7 @@ pub(crate) async fn run_internal_command(
|
||||
positional: None,
|
||||
named: None,
|
||||
span: Span::unknown(),
|
||||
is_last: false,
|
||||
external_redirection: ExternalRedirection::Stdout,
|
||||
},
|
||||
name_tag: Tag::unknown_anchor(command.name_span),
|
||||
scope: (&*scope).clone(),
|
||||
@ -182,10 +182,7 @@ pub(crate) async fn run_internal_command(
|
||||
}
|
||||
CommandAction::EnterShell(location) => {
|
||||
context.shell_manager.insert_at_current(Box::new(
|
||||
match FilesystemShell::with_location(
|
||||
location,
|
||||
context.registry().clone(),
|
||||
) {
|
||||
match FilesystemShell::with_location(location) {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
return InputStream::one(
|
||||
|
@ -262,10 +262,10 @@ impl EvaluatedCommandArgs {
|
||||
|
||||
/// Get the nth positional argument, error if not possible
|
||||
pub fn expect_nth(&self, pos: usize) -> Result<&Value, ShellError> {
|
||||
match self.call_info.args.nth(pos) {
|
||||
None => Err(ShellError::unimplemented("Better error: expect_nth")),
|
||||
Some(item) => Ok(item),
|
||||
}
|
||||
self.call_info
|
||||
.args
|
||||
.nth(pos)
|
||||
.ok_or_else(|| ShellError::unimplemented("Better error: expect_nth"))
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Option<&Value> {
|
||||
|
@ -49,7 +49,7 @@ impl WholeStreamCommand for Compact {
|
||||
},
|
||||
Example {
|
||||
description: "Filter out all directory entries having no 'target'",
|
||||
example: "ls -af | compact target",
|
||||
example: "ls -la | compact target",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
|
@ -41,7 +41,7 @@ impl WholeStreamCommand for SubCommand {
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "Remove the startup commands",
|
||||
example: "config --remove startup",
|
||||
example: "config remove startup",
|
||||
result: None,
|
||||
}]
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ pub async fn set_into(
|
||||
// existing config
|
||||
let mut result = crate::data::config::read(name_span, &None)?;
|
||||
|
||||
// In the original code, this is set to `Some` if the `--load flag is set`
|
||||
// In the original code, this is set to `Some` if the `load flag is set`
|
||||
let configuration = None;
|
||||
|
||||
let rows: Vec<Value> = input.collect().await;
|
||||
|
@ -45,7 +45,7 @@ impl WholeStreamCommand for Default {
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "Give a default 'target' to all file entries",
|
||||
example: "ls -af | default target 'nothing'",
|
||||
example: "ls -la | default target 'nothing'",
|
||||
result: None,
|
||||
}]
|
||||
}
|
||||
@ -60,13 +60,13 @@ async fn default(
|
||||
|
||||
Ok(input
|
||||
.map(move |item| {
|
||||
let should_add = match item {
|
||||
let should_add = matches!(
|
||||
item,
|
||||
Value {
|
||||
value: UntaggedValue::Row(ref r),
|
||||
..
|
||||
} => r.get_data(&column.item).borrow().is_none(),
|
||||
_ => false,
|
||||
};
|
||||
} if r.get_data(&column.item).borrow().is_none()
|
||||
);
|
||||
|
||||
if should_add {
|
||||
match item.insert_data_at_path(&column.item, value.clone()) {
|
||||
|
@ -2,7 +2,9 @@ use crate::commands::classified::block::run_block;
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{hir::Block, ReturnSuccess, Signature, SyntaxShape, Value};
|
||||
use nu_protocol::{
|
||||
hir::Block, hir::ExternalRedirection, ReturnSuccess, Signature, SyntaxShape, Value,
|
||||
};
|
||||
|
||||
pub struct Do;
|
||||
|
||||
@ -61,7 +63,7 @@ async fn do_(
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let registry = registry.clone();
|
||||
let is_last = raw_args.call_info.args.is_last;
|
||||
let external_redirection = raw_args.call_info.args.external_redirection;
|
||||
|
||||
let mut context = Context::from_raw(&raw_args, ®istry);
|
||||
let scope = raw_args.call_info.scope.clone();
|
||||
@ -72,7 +74,26 @@ async fn do_(
|
||||
},
|
||||
input,
|
||||
) = raw_args.process(®istry).await?;
|
||||
block.set_is_last(is_last);
|
||||
|
||||
let block_redirection = match external_redirection {
|
||||
ExternalRedirection::None => {
|
||||
if ignore_errors {
|
||||
ExternalRedirection::Stderr
|
||||
} else {
|
||||
ExternalRedirection::None
|
||||
}
|
||||
}
|
||||
ExternalRedirection::Stdout => {
|
||||
if ignore_errors {
|
||||
ExternalRedirection::StdoutAndStderr
|
||||
} else {
|
||||
ExternalRedirection::Stdout
|
||||
}
|
||||
}
|
||||
x => x,
|
||||
};
|
||||
|
||||
block.set_redirect(block_redirection);
|
||||
|
||||
let result = run_block(
|
||||
&block,
|
||||
@ -85,6 +106,9 @@ async fn do_(
|
||||
.await;
|
||||
|
||||
if ignore_errors {
|
||||
// To properly ignore errors we need to redirect stderr, consume it, and remove
|
||||
// any errors we see in the process.
|
||||
|
||||
match result {
|
||||
Ok(mut stream) => {
|
||||
let output = stream.drain_vec().await;
|
||||
|
@ -105,6 +105,14 @@ pub async fn process_row(
|
||||
.to_output_stream())
|
||||
}
|
||||
|
||||
pub(crate) fn make_indexed_item(index: usize, item: Value) -> Value {
|
||||
let mut dict = TaggedDictBuilder::new(item.tag());
|
||||
dict.insert_untagged("index", UntaggedValue::int(index));
|
||||
dict.insert_value("item", item);
|
||||
|
||||
dict.into_value()
|
||||
}
|
||||
|
||||
async fn each(
|
||||
raw_args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
@ -124,13 +132,10 @@ async fn each(
|
||||
let scope = scope.clone();
|
||||
let head = head.clone();
|
||||
let context = context.clone();
|
||||
|
||||
let mut dict = TaggedDictBuilder::new(input.1.tag());
|
||||
dict.insert_untagged("index", UntaggedValue::int(input.0));
|
||||
dict.insert_value("item", input.1);
|
||||
let row = make_indexed_item(input.0, input.1);
|
||||
|
||||
async {
|
||||
match process_row(block, scope, head, context, dict.into_value()).await {
|
||||
match process_row(block, scope, head, context, row).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => OutputStream::one(Err(e)),
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ use crate::commands::WholeStreamCommand;
|
||||
use crate::context::CommandRegistry;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::hir::ExternalRedirection;
|
||||
use nu_protocol::{
|
||||
CommandAction, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value,
|
||||
};
|
||||
@ -145,7 +146,7 @@ async fn enter(
|
||||
positional: None,
|
||||
named: None,
|
||||
span: Span::unknown(),
|
||||
is_last: false,
|
||||
external_redirection: ExternalRedirection::Stdout,
|
||||
},
|
||||
name_tag: tag.clone(),
|
||||
scope: scope.clone(),
|
||||
|
@ -135,7 +135,6 @@ async fn from_xml(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::commands::from_xml;
|
||||
use indexmap::IndexMap;
|
||||
use nu_protocol::{UntaggedValue, Value};
|
||||
|
@ -169,7 +169,7 @@ pub async fn group_by(
|
||||
|
||||
let values = UntaggedValue::table(&values).into_value(&name);
|
||||
|
||||
match group_strategy {
|
||||
let group_value = match group_strategy {
|
||||
Grouper::ByBlock => {
|
||||
let map = keys.clone();
|
||||
|
||||
@ -179,16 +179,12 @@ pub async fn group_by(
|
||||
None => as_string(row),
|
||||
});
|
||||
|
||||
match crate::utils::data::group(&values, &Some(block), &name) {
|
||||
Ok(grouped) => Ok(OutputStream::one(ReturnSuccess::value(grouped))),
|
||||
Err(reason) => Err(reason),
|
||||
}
|
||||
crate::utils::data::group(&values, &Some(block), &name)
|
||||
}
|
||||
Grouper::ByColumn(column_name) => match group(&column_name, &values, name) {
|
||||
Ok(grouped) => Ok(OutputStream::one(ReturnSuccess::value(grouped))),
|
||||
Err(reason) => Err(reason),
|
||||
},
|
||||
}
|
||||
Grouper::ByColumn(column_name) => group(&column_name, &values, name),
|
||||
};
|
||||
|
||||
Ok(OutputStream::one(ReturnSuccess::value(group_value?)))
|
||||
}
|
||||
|
||||
pub fn suggestions(tried: Tagged<&str>, for_value: &Value) -> ShellError {
|
||||
@ -241,10 +237,7 @@ pub fn group(
|
||||
crate::utils::data::group(&values, &Some(block), &name)
|
||||
}
|
||||
Grouper::ByColumn(None) => {
|
||||
let block = Box::new(move |_, row: &Value| match as_string(row) {
|
||||
Ok(group_key) => Ok(group_key),
|
||||
Err(reason) => Err(reason),
|
||||
});
|
||||
let block = Box::new(move |_, row: &Value| as_string(row));
|
||||
|
||||
crate::utils::data::group(&values, &Some(block), &name)
|
||||
}
|
||||
@ -257,77 +250,32 @@ pub fn group(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::group;
|
||||
use indexmap::IndexMap;
|
||||
use crate::utils::data::helpers::{committers, date, int, row, string, table};
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{UntaggedValue, Value};
|
||||
use nu_source::*;
|
||||
|
||||
fn string(input: impl Into<String>) -> Value {
|
||||
UntaggedValue::string(input.into()).into_untagged_value()
|
||||
}
|
||||
|
||||
fn row(entries: IndexMap<String, Value>) -> Value {
|
||||
UntaggedValue::row(entries).into_untagged_value()
|
||||
}
|
||||
|
||||
fn table(list: &[Value]) -> Value {
|
||||
UntaggedValue::table(list).into_untagged_value()
|
||||
}
|
||||
|
||||
fn nu_releases_committers() -> Vec<Value> {
|
||||
vec![
|
||||
row(
|
||||
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")},
|
||||
),
|
||||
row(
|
||||
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("August 23-2019")},
|
||||
),
|
||||
row(
|
||||
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")},
|
||||
),
|
||||
row(
|
||||
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("Sept 24-2019")},
|
||||
),
|
||||
row(
|
||||
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("October 10-2019")},
|
||||
),
|
||||
row(
|
||||
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("Sept 24-2019")},
|
||||
),
|
||||
row(
|
||||
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("October 10-2019")},
|
||||
),
|
||||
row(
|
||||
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("Sept 24-2019")},
|
||||
),
|
||||
row(
|
||||
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("August 23-2019")},
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn groups_table_by_date_column() -> Result<(), ShellError> {
|
||||
let for_key = Some(String::from("date").tagged_unknown());
|
||||
let sample = table(&nu_releases_committers());
|
||||
let sample = table(&committers());
|
||||
|
||||
assert_eq!(
|
||||
group(&for_key, &sample, Tag::unknown())?,
|
||||
row(indexmap! {
|
||||
"August 23-2019".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")}),
|
||||
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("August 23-2019")}),
|
||||
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("August 23-2019")})
|
||||
"2019-07-23".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => date("2019-07-23"), "chickens".into() => int(10) }),
|
||||
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => date("2019-07-23"), "chickens".into() => int(5) }),
|
||||
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => date("2019-07-23"), "chickens".into() => int(2) })
|
||||
]),
|
||||
"October 10-2019".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")}),
|
||||
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("October 10-2019")}),
|
||||
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("October 10-2019")})
|
||||
"2019-10-10".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => date("2019-10-10"), "chickens".into() => int(6) }),
|
||||
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => date("2019-10-10"), "chickens".into() => int(15) }),
|
||||
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => date("2019-10-10"), "chickens".into() => int(30) })
|
||||
]),
|
||||
"Sept 24-2019".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("Sept 24-2019")}),
|
||||
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("Sept 24-2019")}),
|
||||
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("Sept 24-2019")})
|
||||
"2019-09-24".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => date("2019-09-24"), "chickens".into() => int(20) }),
|
||||
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => date("2019-09-24"), "chickens".into() => int(4) }),
|
||||
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => date("2019-09-24"), "chickens".into() => int(10) })
|
||||
]),
|
||||
})
|
||||
);
|
||||
@ -338,25 +286,25 @@ mod tests {
|
||||
#[test]
|
||||
fn groups_table_by_country_column() -> Result<(), ShellError> {
|
||||
let for_key = Some(String::from("country").tagged_unknown());
|
||||
let sample = table(&nu_releases_committers());
|
||||
let sample = table(&committers());
|
||||
|
||||
assert_eq!(
|
||||
group(&for_key, &sample, Tag::unknown())?,
|
||||
row(indexmap! {
|
||||
"EC".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")}),
|
||||
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("Sept 24-2019")}),
|
||||
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("October 10-2019")})
|
||||
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => date("2019-07-23"), "chickens".into() => int(10) }),
|
||||
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => date("2019-09-24"), "chickens".into() => int(20) }),
|
||||
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => date("2019-10-10"), "chickens".into() => int(30) })
|
||||
]),
|
||||
"NZ".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("August 23-2019")}),
|
||||
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("October 10-2019")}),
|
||||
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("Sept 24-2019")})
|
||||
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => date("2019-07-23"), "chickens".into() => int(5) }),
|
||||
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => date("2019-10-10"), "chickens".into() => int(15) }),
|
||||
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => date("2019-09-24"), "chickens".into() => int(10) })
|
||||
]),
|
||||
"US".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")}),
|
||||
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("Sept 24-2019")}),
|
||||
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("August 23-2019")}),
|
||||
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => date("2019-10-10"), "chickens".into() => int(6) }),
|
||||
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => date("2019-09-24"), "chickens".into() => int(4) }),
|
||||
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => date("2019-07-23"), "chickens".into() => int(2) }),
|
||||
]),
|
||||
})
|
||||
);
|
||||
|
@ -100,18 +100,17 @@ pub async fn group_by_date(
|
||||
|
||||
let value_result = match (grouper_date, grouper_column) {
|
||||
(Grouper::ByDate(None), GroupByColumn::Name(None)) => {
|
||||
let block = Box::new(move |_, row: &Value| row.format("%Y-%b-%d"));
|
||||
let block = Box::new(move |_, row: &Value| row.format("%Y-%m-%d"));
|
||||
|
||||
crate::utils::data::group(&values, &Some(block), &name)
|
||||
}
|
||||
(Grouper::ByDate(None), GroupByColumn::Name(Some(column_name))) => {
|
||||
let block = Box::new(move |_, row: &Value| {
|
||||
let group_key = match row.get_data_by_key(column_name.borrow_spanned()) {
|
||||
Some(group_key) => Ok(group_key),
|
||||
None => Err(suggestions(column_name.borrow_tagged(), &row)),
|
||||
};
|
||||
let group_key = row
|
||||
.get_data_by_key(column_name.borrow_spanned())
|
||||
.ok_or_else(|| suggestions(column_name.borrow_tagged(), &row));
|
||||
|
||||
group_key?.format("%Y-%b-%d")
|
||||
group_key?.format("%Y-%m-%d")
|
||||
});
|
||||
|
||||
crate::utils::data::group(&values, &Some(block), &name)
|
||||
@ -123,10 +122,9 @@ pub async fn group_by_date(
|
||||
}
|
||||
(Grouper::ByDate(Some(fmt)), GroupByColumn::Name(Some(column_name))) => {
|
||||
let block = Box::new(move |_, row: &Value| {
|
||||
let group_key = match row.get_data_by_key(column_name.borrow_spanned()) {
|
||||
Some(group_key) => Ok(group_key),
|
||||
None => Err(suggestions(column_name.borrow_tagged(), &row)),
|
||||
};
|
||||
let group_key = row
|
||||
.get_data_by_key(column_name.borrow_spanned())
|
||||
.ok_or_else(|| suggestions(column_name.borrow_tagged(), &row));
|
||||
|
||||
group_key?.format(&fmt)
|
||||
});
|
||||
|
@ -1,19 +1,13 @@
|
||||
use crate::commands::group_by::group;
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use crate::utils::data_processing::{columns_sorted, evaluate, map_max, reduce, t_sort};
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{
|
||||
Primitive, ReturnSuccess, Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value,
|
||||
};
|
||||
use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value};
|
||||
use nu_source::Tagged;
|
||||
use num_traits::{ToPrimitive, Zero};
|
||||
|
||||
pub struct Histogram;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct HistogramArgs {
|
||||
column_name: Tagged<String>,
|
||||
rest: Vec<Tagged<String>>,
|
||||
}
|
||||
|
||||
@ -24,16 +18,10 @@ impl WholeStreamCommand for Histogram {
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("histogram")
|
||||
.required(
|
||||
"column_name",
|
||||
SyntaxShape::String,
|
||||
"the name of the column to graph by",
|
||||
)
|
||||
.rest(
|
||||
SyntaxShape::String,
|
||||
"column name to give the histogram's frequency column",
|
||||
)
|
||||
Signature::build("histogram").rest(
|
||||
SyntaxShape::String,
|
||||
"column name to give the histogram's frequency column",
|
||||
)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
@ -57,8 +45,8 @@ impl WholeStreamCommand for Histogram {
|
||||
},
|
||||
Example {
|
||||
description:
|
||||
"Get a histogram for the types of files, with frequency column named count",
|
||||
example: "ls | histogram type count",
|
||||
"Get a histogram for the types of files, with frequency column named percentage",
|
||||
example: "ls | histogram type percentage",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
@ -77,180 +65,105 @@ pub async fn histogram(
|
||||
let registry = registry.clone();
|
||||
let name = args.call_info.name_tag.clone();
|
||||
|
||||
let (HistogramArgs { column_name, rest }, input) = args.process(®istry).await?;
|
||||
let (HistogramArgs { rest: mut columns }, input) = args.process(®istry).await?;
|
||||
let values: Vec<Value> = input.collect().await;
|
||||
let values = UntaggedValue::table(&values).into_value(&name);
|
||||
|
||||
let groups = group(&Some(column_name.clone()), &values, &name)?;
|
||||
let group_labels = columns_sorted(Some(column_name.clone()), &groups, &name);
|
||||
let sorted = t_sort(Some(column_name.clone()), None, &groups, &name)?;
|
||||
let evaled = evaluate(&sorted, None, &name)?;
|
||||
let reduced = reduce(&evaled, None, &name)?;
|
||||
let maxima = map_max(&reduced, None, &name)?;
|
||||
let percents = percentages(&reduced, maxima, &name)?;
|
||||
|
||||
match percents {
|
||||
Value {
|
||||
value: UntaggedValue::Table(datasets),
|
||||
..
|
||||
} => {
|
||||
let mut idx = 0;
|
||||
|
||||
let column_names_supplied: Vec<_> = rest.iter().map(|f| f.item.clone()).collect();
|
||||
|
||||
let frequency_column_name = if column_names_supplied.is_empty() {
|
||||
"frequency".to_string()
|
||||
} else {
|
||||
column_names_supplied[0].clone()
|
||||
};
|
||||
|
||||
let column = (*column_name).clone();
|
||||
|
||||
let count_column_name = "count".to_string();
|
||||
let count_shell_error = ShellError::labeled_error(
|
||||
"Unable to load group count",
|
||||
"unabled to load group count",
|
||||
&name,
|
||||
);
|
||||
let mut count_values: Vec<u64> = Vec::new();
|
||||
|
||||
for table_entry in reduced.table_entries() {
|
||||
match table_entry {
|
||||
Value {
|
||||
value: UntaggedValue::Table(list),
|
||||
..
|
||||
} => {
|
||||
for i in list {
|
||||
if let Ok(count) = i.value.clone().into_value(&name).as_u64() {
|
||||
count_values.push(count);
|
||||
} else {
|
||||
return Err(count_shell_error);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(count_shell_error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Value {
|
||||
value: UntaggedValue::Table(start),
|
||||
..
|
||||
} = datasets.get(0).ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"Unable to load dataset",
|
||||
"unabled to load dataset",
|
||||
&name,
|
||||
)
|
||||
})? {
|
||||
let start = start.clone();
|
||||
Ok(
|
||||
futures::stream::iter(start.into_iter().map(move |percentage| {
|
||||
let mut fact = TaggedDictBuilder::new(&name);
|
||||
let value: Tagged<String> = group_labels
|
||||
.get(idx)
|
||||
.ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"Unable to load group labels",
|
||||
"unabled to load group labels",
|
||||
&name,
|
||||
)
|
||||
})?
|
||||
.clone();
|
||||
fact.insert_value(
|
||||
&column,
|
||||
UntaggedValue::string(value.item).into_value(value.tag),
|
||||
);
|
||||
|
||||
fact.insert_untagged(
|
||||
&count_column_name,
|
||||
UntaggedValue::int(count_values[idx]),
|
||||
);
|
||||
|
||||
if let Value {
|
||||
value: UntaggedValue::Primitive(Primitive::Int(ref num)),
|
||||
ref tag,
|
||||
} = percentage
|
||||
{
|
||||
let string = std::iter::repeat("*")
|
||||
.take(num.to_i32().ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"Expected a number",
|
||||
"expected a number",
|
||||
tag,
|
||||
)
|
||||
})? as usize)
|
||||
.collect::<String>();
|
||||
fact.insert_untagged(
|
||||
&frequency_column_name,
|
||||
UntaggedValue::string(string),
|
||||
);
|
||||
}
|
||||
|
||||
idx += 1;
|
||||
|
||||
ReturnSuccess::value(fact.into_value())
|
||||
}))
|
||||
.to_output_stream(),
|
||||
)
|
||||
} else {
|
||||
Ok(OutputStream::empty())
|
||||
}
|
||||
}
|
||||
_ => Ok(OutputStream::empty()),
|
||||
}
|
||||
}
|
||||
|
||||
fn percentages(values: &Value, max: Value, tag: impl Into<Tag>) -> Result<Value, ShellError> {
|
||||
let tag = tag.into();
|
||||
|
||||
let results: Value = match values {
|
||||
Value {
|
||||
value: UntaggedValue::Table(datasets),
|
||||
..
|
||||
} => {
|
||||
let datasets: Vec<_> = datasets
|
||||
.iter()
|
||||
.map(|subsets| match subsets {
|
||||
Value {
|
||||
value: UntaggedValue::Table(data),
|
||||
..
|
||||
} => {
|
||||
let data = data
|
||||
.iter()
|
||||
.map(|d| match d {
|
||||
Value {
|
||||
value: UntaggedValue::Primitive(Primitive::Int(n)),
|
||||
..
|
||||
} => {
|
||||
let max = match &max {
|
||||
Value {
|
||||
value: UntaggedValue::Primitive(Primitive::Int(maxima)),
|
||||
..
|
||||
} => maxima.clone(),
|
||||
_ => Zero::zero(),
|
||||
};
|
||||
|
||||
let n = (n * 100) / max;
|
||||
|
||||
UntaggedValue::int(n).into_value(&tag)
|
||||
}
|
||||
_ => UntaggedValue::int(0).into_value(&tag),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
UntaggedValue::Table(data).into_value(&tag)
|
||||
}
|
||||
_ => UntaggedValue::Table(vec![]).into_value(&tag),
|
||||
})
|
||||
.collect();
|
||||
|
||||
UntaggedValue::Table(datasets).into_value(&tag)
|
||||
}
|
||||
other => other.clone(),
|
||||
let column_grouper = if !columns.is_empty() {
|
||||
Some(columns.remove(0))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(results)
|
||||
let column_names_supplied: Vec<_> = columns.iter().map(|f| f.item.clone()).collect();
|
||||
|
||||
let frequency_column_name = if column_names_supplied.is_empty() {
|
||||
"frequency".to_string()
|
||||
} else {
|
||||
column_names_supplied[0].clone()
|
||||
};
|
||||
|
||||
let column = if let Some(ref column) = column_grouper {
|
||||
column.clone()
|
||||
} else {
|
||||
"value".to_string().tagged(&name)
|
||||
};
|
||||
|
||||
let results = crate::utils::data::report(
|
||||
&UntaggedValue::table(&values).into_value(&name),
|
||||
crate::utils::data::Operation {
|
||||
grouper: Some(Box::new(move |_, _| Ok(String::from("frequencies")))),
|
||||
splitter: Some(splitter(column_grouper)),
|
||||
format: None,
|
||||
eval: &None,
|
||||
},
|
||||
&name,
|
||||
)?;
|
||||
|
||||
let labels = results.labels.y.clone();
|
||||
let mut idx = 0;
|
||||
|
||||
Ok(futures::stream::iter(
|
||||
results
|
||||
.percentages
|
||||
.table_entries()
|
||||
.map(move |value| {
|
||||
let values = value.table_entries().cloned().collect::<Vec<_>>();
|
||||
let count = values.len();
|
||||
|
||||
(count, values[count - 1].clone())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.map(move |(count, value)| {
|
||||
let mut fact = TaggedDictBuilder::new(&name);
|
||||
let column_value = labels
|
||||
.get(idx)
|
||||
.ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"Unable to load group labels",
|
||||
"unabled to load group labels",
|
||||
&name,
|
||||
)
|
||||
})?
|
||||
.clone();
|
||||
|
||||
fact.insert_value(&column.item, column_value);
|
||||
fact.insert_untagged("count", UntaggedValue::int(count));
|
||||
|
||||
let string = std::iter::repeat("*")
|
||||
.take(value.as_u64().map_err(|_| {
|
||||
ShellError::labeled_error("expected a number", "expected a number", &name)
|
||||
})? as usize)
|
||||
.collect::<String>();
|
||||
|
||||
fact.insert_untagged(&frequency_column_name, UntaggedValue::string(string));
|
||||
|
||||
idx += 1;
|
||||
|
||||
ReturnSuccess::value(fact.into_value())
|
||||
}),
|
||||
)
|
||||
.to_output_stream())
|
||||
}
|
||||
|
||||
fn splitter(
|
||||
by: Option<Tagged<String>>,
|
||||
) -> Box<dyn Fn(usize, &Value) -> Result<String, ShellError> + Send> {
|
||||
match by {
|
||||
Some(column) => Box::new(move |_, row: &Value| {
|
||||
let key = &column;
|
||||
|
||||
match row.get_data_by_key(key.borrow_spanned()) {
|
||||
Some(key) => nu_value_ext::as_string(&key),
|
||||
None => Err(ShellError::labeled_error(
|
||||
"unknown column",
|
||||
"unknown column",
|
||||
key.tag(),
|
||||
)),
|
||||
}
|
||||
}),
|
||||
None => Box::new(move |_, row: &Value| nu_value_ext::as_string(&row)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -1,8 +1,10 @@
|
||||
use crate::commands::classified::block::run_block;
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::context::CommandRegistry;
|
||||
use crate::prelude::*;
|
||||
use futures::stream::once;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{ColumnPath, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value};
|
||||
use nu_protocol::{ColumnPath, ReturnSuccess, Scope, Signature, SyntaxShape, UntaggedValue, Value};
|
||||
use nu_value_ext::ValueExt;
|
||||
|
||||
pub struct Insert;
|
||||
@ -26,11 +28,7 @@ impl WholeStreamCommand for Insert {
|
||||
SyntaxShape::ColumnPath,
|
||||
"the column name to insert",
|
||||
)
|
||||
.required(
|
||||
"value",
|
||||
SyntaxShape::String,
|
||||
"the value to give the cell(s)",
|
||||
)
|
||||
.required("value", SyntaxShape::Any, "the value to give the cell(s)")
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
@ -46,26 +44,111 @@ impl WholeStreamCommand for Insert {
|
||||
}
|
||||
}
|
||||
|
||||
async fn insert(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream, ShellError> {
|
||||
let registry = registry.clone();
|
||||
async fn process_row(
|
||||
scope: Arc<Scope>,
|
||||
mut context: Arc<Context>,
|
||||
input: Value,
|
||||
mut value: Arc<Value>,
|
||||
column: Arc<ColumnPath>,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let value = Arc::make_mut(&mut value);
|
||||
|
||||
let (InsertArgs { column, value }, input) = args.process(®istry).await?;
|
||||
Ok(match value {
|
||||
Value {
|
||||
value: UntaggedValue::Block(block),
|
||||
..
|
||||
} => {
|
||||
let for_block = input.clone();
|
||||
let input_stream = once(async { Ok(for_block) }).to_input_stream();
|
||||
|
||||
Ok(input
|
||||
.map(move |row| match row {
|
||||
let result = run_block(
|
||||
&block,
|
||||
Arc::make_mut(&mut context),
|
||||
input_stream,
|
||||
&input,
|
||||
&scope.vars,
|
||||
&scope.env,
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(mut stream) => {
|
||||
let errors = context.get_errors();
|
||||
if let Some(error) = errors.first() {
|
||||
return Err(error.clone());
|
||||
}
|
||||
|
||||
match input {
|
||||
obj
|
||||
@
|
||||
Value {
|
||||
value: UntaggedValue::Row(_),
|
||||
..
|
||||
} => {
|
||||
if let Some(result) = stream.next().await {
|
||||
match obj.insert_data_at_column_path(&column, result) {
|
||||
Ok(v) => OutputStream::one(ReturnSuccess::value(v)),
|
||||
Err(e) => OutputStream::one(Err(e)),
|
||||
}
|
||||
} else {
|
||||
OutputStream::empty()
|
||||
}
|
||||
}
|
||||
Value { tag, .. } => OutputStream::one(Err(ShellError::labeled_error(
|
||||
"Unrecognized type in stream",
|
||||
"original value",
|
||||
tag,
|
||||
))),
|
||||
}
|
||||
}
|
||||
Err(e) => OutputStream::one(Err(e)),
|
||||
}
|
||||
}
|
||||
_ => match input {
|
||||
obj
|
||||
@
|
||||
Value {
|
||||
value: UntaggedValue::Row(_),
|
||||
..
|
||||
} => Ok(ReturnSuccess::Value(
|
||||
row.insert_data_at_column_path(&column, value.clone())?,
|
||||
)),
|
||||
|
||||
Value { tag, .. } => Err(ShellError::labeled_error(
|
||||
} => match obj.insert_data_at_column_path(&column, value.clone()) {
|
||||
Ok(v) => OutputStream::one(ReturnSuccess::value(v)),
|
||||
Err(e) => OutputStream::one(Err(e)),
|
||||
},
|
||||
Value { tag, .. } => OutputStream::one(Err(ShellError::labeled_error(
|
||||
"Unrecognized type in stream",
|
||||
"original value",
|
||||
tag,
|
||||
)),
|
||||
))),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async fn insert(
|
||||
raw_args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let registry = registry.clone();
|
||||
let scope = Arc::new(raw_args.call_info.scope.clone());
|
||||
let context = Arc::new(Context::from_raw(&raw_args, ®istry));
|
||||
let (InsertArgs { column, value }, input) = raw_args.process(®istry).await?;
|
||||
let value = Arc::new(value);
|
||||
let column = Arc::new(column);
|
||||
|
||||
Ok(input
|
||||
.then(move |input| {
|
||||
let scope = scope.clone();
|
||||
let context = context.clone();
|
||||
let value = value.clone();
|
||||
let column = column.clone();
|
||||
|
||||
async {
|
||||
match process_row(scope, context, input, value, column).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => OutputStream::one(Err(e)),
|
||||
}
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.to_output_stream())
|
||||
}
|
||||
|
||||
|
@ -106,14 +106,14 @@ async fn is_empty(
|
||||
..
|
||||
} => {
|
||||
if val.is_empty() {
|
||||
match obj.replace_data_at_column_path(&field, default.clone()) {
|
||||
Some(v) => Ok(v),
|
||||
None => Err(ShellError::labeled_error(
|
||||
"empty? could not find place to check emptiness",
|
||||
"column name",
|
||||
&field.tag,
|
||||
)),
|
||||
}
|
||||
obj.replace_data_at_column_path(&field, default.clone())
|
||||
.ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"empty? could not find place to check emptiness",
|
||||
"column name",
|
||||
&field.tag,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
Ok(obj)
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ pub struct Ls;
|
||||
pub struct LsArgs {
|
||||
pub path: Option<Tagged<PathBuf>>,
|
||||
pub all: bool,
|
||||
pub full: bool,
|
||||
pub long: bool,
|
||||
#[serde(rename = "short-names")]
|
||||
pub short_names: bool,
|
||||
#[serde(rename = "with-symlink-targets")]
|
||||
@ -33,25 +33,26 @@ impl WholeStreamCommand for Ls {
|
||||
SyntaxShape::Pattern,
|
||||
"a path to get the directory contents from",
|
||||
)
|
||||
.switch("all", "also show hidden files", Some('a'))
|
||||
.switch("all", "Show hidden files", Some('a'))
|
||||
.switch(
|
||||
"full",
|
||||
"list all available columns for each entry",
|
||||
Some('f'),
|
||||
"long",
|
||||
"List all available columns for each entry",
|
||||
Some('l'),
|
||||
)
|
||||
.switch(
|
||||
"short-names",
|
||||
"only print the file names and not the path",
|
||||
"Only print the file names and not the path",
|
||||
Some('s'),
|
||||
)
|
||||
.switch(
|
||||
// Delete this
|
||||
"with-symlink-targets",
|
||||
"display the paths to the target files that symlinks point to",
|
||||
"Display the paths to the target files that symlinks point to",
|
||||
Some('w'),
|
||||
)
|
||||
.switch(
|
||||
"du",
|
||||
"display the apparent directory size in place of the directory metadata size",
|
||||
"Display the apparent directory size in place of the directory metadata size",
|
||||
Some('d'),
|
||||
)
|
||||
}
|
||||
|
@ -1,14 +1,17 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::commands::math::reducers::{reducer_for, Reduce};
|
||||
use crate::commands::math::utils::run_with_function;
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use crate::utils::data_processing::{reducer_for, Reduce};
|
||||
use bigdecimal::{FromPrimitive, Zero};
|
||||
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{
|
||||
hir::{convert_number_to_u64, Number, Operator},
|
||||
Primitive, Signature, UntaggedValue, Value,
|
||||
};
|
||||
|
||||
use bigdecimal::FromPrimitive;
|
||||
|
||||
pub struct SubCommand;
|
||||
|
||||
#[async_trait]
|
||||
@ -55,19 +58,59 @@ impl WholeStreamCommand for SubCommand {
|
||||
}
|
||||
}
|
||||
|
||||
fn to_byte(value: &Value) -> Option<Value> {
|
||||
match &value.value {
|
||||
UntaggedValue::Primitive(Primitive::Int(num)) => Some(
|
||||
UntaggedValue::Primitive(Primitive::Filesize(convert_number_to_u64(&Number::Int(
|
||||
num.clone(),
|
||||
))))
|
||||
.into_untagged_value(),
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn average(values: &[Value], name: &Tag) -> Result<Value, ShellError> {
|
||||
let sum = reducer_for(Reduce::Summation);
|
||||
|
||||
let number = BigDecimal::from_usize(values.len()).ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"could not convert to big decimal",
|
||||
"could not convert to big decimal",
|
||||
&name.span,
|
||||
)
|
||||
ShellError::labeled_error("nothing to average", "nothing to average", &name.span)
|
||||
})?;
|
||||
|
||||
let total_rows = UntaggedValue::decimal(number);
|
||||
let total = sum(Value::zero(), values.to_vec())?;
|
||||
|
||||
let are_bytes = values
|
||||
.get(0)
|
||||
.ok_or_else(|| {
|
||||
ShellError::unexpected("Cannot perform aggregate math operation on empty data")
|
||||
})?
|
||||
.is_filesize();
|
||||
|
||||
let total = if are_bytes {
|
||||
to_byte(&sum(
|
||||
UntaggedValue::int(0).into_untagged_value(),
|
||||
values
|
||||
.to_vec()
|
||||
.iter()
|
||||
.map(|v| match v {
|
||||
Value {
|
||||
value: UntaggedValue::Primitive(Primitive::Filesize(num)),
|
||||
..
|
||||
} => UntaggedValue::int(*num as usize).into_untagged_value(),
|
||||
other => other.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)?)
|
||||
.ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"could not convert to big decimal",
|
||||
"could not convert to big decimal",
|
||||
&name.span,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
sum(UntaggedValue::int(0).into_untagged_value(), values.to_vec())
|
||||
}?;
|
||||
|
||||
match total {
|
||||
Value {
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::commands::math::reducers::{reducer_for, Reduce};
|
||||
use crate::commands::math::utils::run_with_function;
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use crate::utils::data_processing::{reducer_for, Reduce};
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Signature, UntaggedValue, Value};
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
use crate::commands::math::reducers::{reducer_for, Reduce};
|
||||
use crate::commands::math::utils::run_with_function;
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use crate::utils::data_processing::{reducer_for, Reduce};
|
||||
use bigdecimal::{FromPrimitive, Zero};
|
||||
use bigdecimal::FromPrimitive;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{
|
||||
hir::{convert_number_to_u64, Number, Operator},
|
||||
@ -130,7 +130,7 @@ fn compute_average(values: &[Value], name: impl Into<Tag>) -> Result<Value, Shel
|
||||
)
|
||||
})?;
|
||||
let total_rows = UntaggedValue::decimal(number);
|
||||
let total = sum(Value::zero(), values.to_vec())?;
|
||||
let total = sum(UntaggedValue::int(0).into_untagged_value(), values.to_vec())?;
|
||||
|
||||
match total {
|
||||
Value {
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::commands::math::reducers::{reducer_for, Reduce};
|
||||
use crate::commands::math::utils::run_with_function;
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use crate::utils::data_processing::{reducer_for, Reduce};
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Signature, UntaggedValue, Value};
|
||||
|
||||
|
@ -7,9 +7,11 @@ pub mod min;
|
||||
pub mod mode;
|
||||
pub mod stddev;
|
||||
pub mod sum;
|
||||
pub mod utils;
|
||||
pub mod variance;
|
||||
|
||||
mod reducers;
|
||||
mod utils;
|
||||
|
||||
pub use avg::SubCommand as MathAverage;
|
||||
pub use command::Command as Math;
|
||||
pub use eval::SubCommand as MathEval;
|
||||
|
135
crates/nu-cli/src/commands/math/reducers.rs
Normal file
135
crates/nu-cli/src/commands/math/reducers.rs
Normal file
@ -0,0 +1,135 @@
|
||||
use crate::data::value::{compare_values, compute_values};
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::hir::Operator;
|
||||
use nu_protocol::{UntaggedValue, Value};
|
||||
use nu_source::{SpannedItem, Tag};
|
||||
|
||||
// Re-usable error messages
|
||||
const ERR_EMPTY_DATA: &str = "Cannot perform aggregate math operation on empty data";
|
||||
|
||||
fn formula(
|
||||
acc_begin: Value,
|
||||
calculator: Box<dyn Fn(Vec<Value>) -> Result<Value, ShellError> + Send + Sync + 'static>,
|
||||
) -> Box<dyn Fn(Value, Vec<Value>) -> Result<Value, ShellError> + Send + Sync + 'static> {
|
||||
Box::new(move |acc, datax| -> Result<Value, ShellError> {
|
||||
let result = match compute_values(Operator::Multiply, &acc, &acc_begin) {
|
||||
Ok(v) => v.into_untagged_value(),
|
||||
Err((left_type, right_type)) => {
|
||||
return Err(ShellError::coerce_error(
|
||||
left_type.spanned_unknown(),
|
||||
right_type.spanned_unknown(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
match calculator(datax) {
|
||||
Ok(total) => Ok(match compute_values(Operator::Plus, &result, &total) {
|
||||
Ok(v) => v.into_untagged_value(),
|
||||
Err((left_type, right_type)) => {
|
||||
return Err(ShellError::coerce_error(
|
||||
left_type.spanned_unknown(),
|
||||
right_type.spanned_unknown(),
|
||||
))
|
||||
}
|
||||
}),
|
||||
Err(reason) => Err(reason),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reducer_for(
|
||||
command: Reduce,
|
||||
) -> Box<dyn Fn(Value, Vec<Value>) -> Result<Value, ShellError> + Send + Sync + 'static> {
|
||||
match command {
|
||||
Reduce::Summation | Reduce::Default => Box::new(formula(
|
||||
UntaggedValue::int(0).into_untagged_value(),
|
||||
Box::new(sum),
|
||||
)),
|
||||
Reduce::Minimum => Box::new(|_, values| min(values)),
|
||||
Reduce::Maximum => Box::new(|_, values| max(values)),
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Reduce {
|
||||
Summation,
|
||||
Minimum,
|
||||
Maximum,
|
||||
Default,
|
||||
}
|
||||
|
||||
pub fn sum(data: Vec<Value>) -> Result<Value, ShellError> {
|
||||
let mut acc = UntaggedValue::int(0).into_untagged_value();
|
||||
for value in data {
|
||||
match value.value {
|
||||
UntaggedValue::Primitive(_) => {
|
||||
acc = match compute_values(Operator::Plus, &acc, &value) {
|
||||
Ok(v) => v.into_untagged_value(),
|
||||
Err((left_type, right_type)) => {
|
||||
return Err(ShellError::coerce_error(
|
||||
left_type.spanned_unknown(),
|
||||
right_type.spanned_unknown(),
|
||||
))
|
||||
}
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
return Err(ShellError::labeled_error(
|
||||
"Attempted to compute the sum of a value that cannot be summed.",
|
||||
"value appears here",
|
||||
value.tag.span,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(acc)
|
||||
}
|
||||
|
||||
pub fn max(data: Vec<Value>) -> Result<Value, ShellError> {
|
||||
let mut biggest = data
|
||||
.first()
|
||||
.ok_or_else(|| ShellError::unexpected(ERR_EMPTY_DATA))?
|
||||
.value
|
||||
.clone();
|
||||
|
||||
for value in data.iter() {
|
||||
if let Ok(greater_than) = compare_values(Operator::GreaterThan, &value.value, &biggest) {
|
||||
if greater_than {
|
||||
biggest = value.value.clone();
|
||||
}
|
||||
} else {
|
||||
return Err(ShellError::unexpected(format!(
|
||||
"Could not compare\nleft: {:?}\nright: {:?}",
|
||||
biggest, value.value
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(Value {
|
||||
value: biggest,
|
||||
tag: Tag::unknown(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn min(data: Vec<Value>) -> Result<Value, ShellError> {
|
||||
let mut smallest = data
|
||||
.first()
|
||||
.ok_or_else(|| ShellError::unexpected(ERR_EMPTY_DATA))?
|
||||
.value
|
||||
.clone();
|
||||
|
||||
for value in data.iter() {
|
||||
if let Ok(greater_than) = compare_values(Operator::LessThan, &value.value, &smallest) {
|
||||
if greater_than {
|
||||
smallest = value.value.clone();
|
||||
}
|
||||
} else {
|
||||
return Err(ShellError::unexpected(format!(
|
||||
"Could not compare\nleft: {:?}\nright: {:?}",
|
||||
smallest, value.value
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(Value {
|
||||
value: smallest,
|
||||
tag: Tag::unknown(),
|
||||
})
|
||||
}
|
@ -1,13 +1,18 @@
|
||||
use super::variance::variance;
|
||||
use crate::commands::math::utils::run_with_function;
|
||||
use super::variance::compute_variance as variance;
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Primitive, Signature, UntaggedValue, Value};
|
||||
use nu_protocol::{Dictionary, Primitive, ReturnSuccess, Signature, UntaggedValue, Value};
|
||||
use nu_source::Tagged;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub struct SubCommand;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Arguments {
|
||||
sample: Tagged<bool>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for SubCommand {
|
||||
fn name(&self) -> &str {
|
||||
@ -15,7 +20,11 @@ impl WholeStreamCommand for SubCommand {
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("math stddev")
|
||||
Signature::build("math stddev").switch(
|
||||
"sample",
|
||||
"calculate sample standard deviation",
|
||||
Some('s'),
|
||||
)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
@ -27,20 +36,69 @@ impl WholeStreamCommand for SubCommand {
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
run_with_function(
|
||||
RunnableContext {
|
||||
input: args.input,
|
||||
registry: registry.clone(),
|
||||
shell_manager: args.shell_manager,
|
||||
host: args.host,
|
||||
ctrl_c: args.ctrl_c,
|
||||
current_errors: args.current_errors,
|
||||
name: args.call_info.name_tag,
|
||||
raw_input: args.raw_input,
|
||||
},
|
||||
stddev,
|
||||
)
|
||||
.await
|
||||
let name = args.call_info.name_tag.clone();
|
||||
let (Arguments { sample }, mut input) = args.process(®istry).await?;
|
||||
|
||||
let values: Vec<Value> = input.drain_vec().await;
|
||||
|
||||
let n = if let Tagged { item: true, .. } = sample {
|
||||
values.len() - 1
|
||||
} else {
|
||||
values.len()
|
||||
};
|
||||
|
||||
let res = if values.iter().all(|v| v.is_primitive()) {
|
||||
compute_stddev(&values, n, &name)
|
||||
} else {
|
||||
// If we are not dealing with Primitives, then perhaps we are dealing with a table
|
||||
// Create a key for each column name
|
||||
let mut column_values = IndexMap::new();
|
||||
for value in values {
|
||||
if let UntaggedValue::Row(row_dict) = &value.value {
|
||||
for (key, value) in row_dict.entries.iter() {
|
||||
column_values
|
||||
.entry(key.clone())
|
||||
.and_modify(|v: &mut Vec<Value>| v.push(value.clone()))
|
||||
.or_insert(vec![value.clone()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// The mathematical function operates over the columns of the table
|
||||
let mut column_totals = IndexMap::new();
|
||||
for (col_name, col_vals) in column_values {
|
||||
if let Ok(out) = compute_stddev(&col_vals, n, &name) {
|
||||
column_totals.insert(col_name, out);
|
||||
}
|
||||
}
|
||||
|
||||
if column_totals.keys().len() == 0 {
|
||||
return Err(ShellError::labeled_error(
|
||||
"Attempted to compute values that can't be operated on",
|
||||
"value appears here",
|
||||
name.span,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(UntaggedValue::Row(Dictionary {
|
||||
entries: column_totals,
|
||||
})
|
||||
.into_untagged_value())
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(v) => {
|
||||
if v.value.is_table() {
|
||||
Ok(OutputStream::from(
|
||||
v.table_entries()
|
||||
.map(|v| ReturnSuccess::value(v.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
} else {
|
||||
Ok(OutputStream::one(ReturnSuccess::value(v)))
|
||||
}
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
@ -52,8 +110,13 @@ impl WholeStreamCommand for SubCommand {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn stddev(values: &[Value], name: &Tag) -> Result<Value, ShellError> {
|
||||
let variance = variance(values, name)?.as_primitive()?;
|
||||
compute_stddev(values, values.len(), name)
|
||||
}
|
||||
|
||||
pub fn compute_stddev(values: &[Value], n: usize, name: &Tag) -> Result<Value, ShellError> {
|
||||
let variance = variance(values, n, name)?.as_primitive()?;
|
||||
let sqrt_var = match variance {
|
||||
Primitive::Decimal(var) => var.sqrt(),
|
||||
_ => {
|
||||
|
@ -1,10 +1,13 @@
|
||||
use crate::commands::math::reducers::{reducer_for, Reduce};
|
||||
use crate::commands::math::utils::run_with_function;
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use crate::utils::data_processing::{reducer_for, Reduce};
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Dictionary, Signature, UntaggedValue, Value};
|
||||
use num_traits::identities::Zero;
|
||||
|
||||
use nu_protocol::{
|
||||
hir::{convert_number_to_u64, Number},
|
||||
Primitive, Signature, UntaggedValue, Value,
|
||||
};
|
||||
|
||||
pub struct SubCommand;
|
||||
|
||||
@ -59,37 +62,63 @@ impl WholeStreamCommand for SubCommand {
|
||||
}
|
||||
}
|
||||
|
||||
fn to_byte(value: &Value) -> Option<Value> {
|
||||
match &value.value {
|
||||
UntaggedValue::Primitive(Primitive::Int(num)) => Some(
|
||||
UntaggedValue::Primitive(Primitive::Filesize(convert_number_to_u64(&Number::Int(
|
||||
num.clone(),
|
||||
))))
|
||||
.into_untagged_value(),
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn summation(values: &[Value], name: &Tag) -> Result<Value, ShellError> {
|
||||
let sum = reducer_for(Reduce::Summation);
|
||||
|
||||
if values.iter().all(|v| v.is_primitive()) {
|
||||
Ok(sum(Value::zero(), values.to_vec())?)
|
||||
} else {
|
||||
let mut column_values = IndexMap::new();
|
||||
let first = values.get(0).ok_or_else(|| {
|
||||
ShellError::unexpected("Cannot perform aggregate math operation on empty data")
|
||||
})?;
|
||||
|
||||
for value in values {
|
||||
if let UntaggedValue::Row(row_dict) = value.value.clone() {
|
||||
for (key, value) in row_dict.entries.iter() {
|
||||
column_values
|
||||
.entry(key.clone())
|
||||
.and_modify(|v: &mut Vec<Value>| v.push(value.clone()))
|
||||
.or_insert(vec![value.clone()]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let mut column_totals = IndexMap::new();
|
||||
|
||||
for (col_name, col_vals) in column_values {
|
||||
let sum = sum(Value::zero(), col_vals)?;
|
||||
|
||||
column_totals.insert(col_name, sum);
|
||||
}
|
||||
|
||||
Ok(UntaggedValue::Row(Dictionary {
|
||||
entries: column_totals,
|
||||
})
|
||||
.into_value(name))
|
||||
match first {
|
||||
v if v.is_filesize() => to_byte(&sum(
|
||||
UntaggedValue::int(0).into_untagged_value(),
|
||||
values
|
||||
.to_vec()
|
||||
.iter()
|
||||
.map(|v| match v {
|
||||
Value {
|
||||
value: UntaggedValue::Primitive(Primitive::Filesize(num)),
|
||||
..
|
||||
} => UntaggedValue::int(*num as usize).into_untagged_value(),
|
||||
other => other.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)?)
|
||||
.ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"could not convert to big decimal",
|
||||
"could not convert to big decimal",
|
||||
&name.span,
|
||||
)
|
||||
}),
|
||||
// v is nothing primitive
|
||||
v if v.is_none() => sum(
|
||||
UntaggedValue::int(0).into_untagged_value(),
|
||||
values
|
||||
.to_vec()
|
||||
.iter()
|
||||
.map(|v| match v {
|
||||
Value {
|
||||
value: UntaggedValue::Primitive(Primitive::Nothing),
|
||||
..
|
||||
} => UntaggedValue::int(0).into_untagged_value(),
|
||||
other => other.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
_ => sum(UntaggedValue::int(0).into_untagged_value(), values.to_vec()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ pub async fn run_with_function(
|
||||
mf: MathFunction,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let values: Vec<Value> = input.drain_vec().await;
|
||||
|
||||
let res = calculate(&values, &name, mf);
|
||||
match res {
|
||||
Ok(v) => {
|
||||
@ -50,7 +51,17 @@ pub fn calculate(values: &[Value], name: &Tag, mf: MathFunction) -> Result<Value
|
||||
// The mathematical function operates over the columns of the table
|
||||
let mut column_totals = IndexMap::new();
|
||||
for (col_name, col_vals) in column_values {
|
||||
column_totals.insert(col_name, mf(&col_vals, &name)?);
|
||||
if let Ok(out) = mf(&col_vals, &name) {
|
||||
column_totals.insert(col_name, out);
|
||||
}
|
||||
}
|
||||
|
||||
if column_totals.keys().len() == 0 {
|
||||
return Err(ShellError::labeled_error(
|
||||
"Attempted to compute values that can't be operated on",
|
||||
"value appears here",
|
||||
name.span,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(UntaggedValue::Row(Dictionary {
|
||||
|
@ -1,13 +1,20 @@
|
||||
use crate::commands::math::utils::run_with_function;
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::data::value::compute_values;
|
||||
use crate::prelude::*;
|
||||
use bigdecimal::{FromPrimitive, Zero};
|
||||
use bigdecimal::FromPrimitive;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{hir::Operator, Primitive, Signature, UntaggedValue, Value};
|
||||
use nu_protocol::{
|
||||
hir::Operator, Dictionary, Primitive, ReturnSuccess, Signature, UntaggedValue, Value,
|
||||
};
|
||||
use nu_source::Tagged;
|
||||
|
||||
pub struct SubCommand;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Arguments {
|
||||
sample: Tagged<bool>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for SubCommand {
|
||||
fn name(&self) -> &str {
|
||||
@ -15,7 +22,7 @@ impl WholeStreamCommand for SubCommand {
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("math variance")
|
||||
Signature::build("math variance").switch("sample", "calculate sample variance", Some('s'))
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
@ -27,20 +34,69 @@ impl WholeStreamCommand for SubCommand {
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
run_with_function(
|
||||
RunnableContext {
|
||||
input: args.input,
|
||||
registry: registry.clone(),
|
||||
shell_manager: args.shell_manager,
|
||||
host: args.host,
|
||||
ctrl_c: args.ctrl_c,
|
||||
current_errors: args.current_errors,
|
||||
name: args.call_info.name_tag,
|
||||
raw_input: args.raw_input,
|
||||
},
|
||||
variance,
|
||||
)
|
||||
.await
|
||||
let name = args.call_info.name_tag.clone();
|
||||
let (Arguments { sample }, mut input) = args.process(®istry).await?;
|
||||
|
||||
let values: Vec<Value> = input.drain_vec().await;
|
||||
|
||||
let n = if let Tagged { item: true, .. } = sample {
|
||||
values.len() - 1
|
||||
} else {
|
||||
values.len()
|
||||
};
|
||||
|
||||
let res = if values.iter().all(|v| v.is_primitive()) {
|
||||
compute_variance(&values, n, &name)
|
||||
} else {
|
||||
// If we are not dealing with Primitives, then perhaps we are dealing with a table
|
||||
// Create a key for each column name
|
||||
let mut column_values = IndexMap::new();
|
||||
for value in values {
|
||||
if let UntaggedValue::Row(row_dict) = &value.value {
|
||||
for (key, value) in row_dict.entries.iter() {
|
||||
column_values
|
||||
.entry(key.clone())
|
||||
.and_modify(|v: &mut Vec<Value>| v.push(value.clone()))
|
||||
.or_insert(vec![value.clone()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// The mathematical function operates over the columns of the table
|
||||
let mut column_totals = IndexMap::new();
|
||||
for (col_name, col_vals) in column_values {
|
||||
if let Ok(out) = compute_variance(&col_vals, n, &name) {
|
||||
column_totals.insert(col_name, out);
|
||||
}
|
||||
}
|
||||
|
||||
if column_totals.keys().len() == 0 {
|
||||
return Err(ShellError::labeled_error(
|
||||
"Attempted to compute values that can't be operated on",
|
||||
"value appears here",
|
||||
name.span,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(UntaggedValue::Row(Dictionary {
|
||||
entries: column_totals,
|
||||
})
|
||||
.into_untagged_value())
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(v) => {
|
||||
if v.value.is_table() {
|
||||
Ok(OutputStream::from(
|
||||
v.table_entries()
|
||||
.map(|v| ReturnSuccess::value(v.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
} else {
|
||||
Ok(OutputStream::one(ReturnSuccess::value(v)))
|
||||
}
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
@ -60,8 +116,8 @@ fn sum_of_squares(values: &[Value], name: &Tag) -> Result<Value, ShellError> {
|
||||
&name.span,
|
||||
)
|
||||
})?;
|
||||
let mut sum_x = Value::zero();
|
||||
let mut sum_x2 = Value::zero();
|
||||
let mut sum_x = UntaggedValue::int(0).into_untagged_value();
|
||||
let mut sum_x2 = UntaggedValue::int(0).into_untagged_value();
|
||||
for value in values {
|
||||
let v = match value {
|
||||
Value {
|
||||
@ -87,7 +143,17 @@ fn sum_of_squares(values: &[Value], name: &Tag) -> Result<Value, ShellError> {
|
||||
let v_squared = compute_values(Operator::Multiply, &v, &v);
|
||||
match v_squared {
|
||||
// X^2
|
||||
Ok(x2) => sum_x2 = sum_x2 + x2.into_untagged_value(),
|
||||
Ok(x2) => {
|
||||
sum_x2 = match compute_values(Operator::Plus, &sum_x2, &x2) {
|
||||
Ok(v) => v.into_untagged_value(),
|
||||
Err((left_type, right_type)) => {
|
||||
return Err(ShellError::coerce_error(
|
||||
left_type.spanned(name.span),
|
||||
right_type.spanned(name.span),
|
||||
))
|
||||
}
|
||||
};
|
||||
}
|
||||
Err((left_type, right_type)) => {
|
||||
return Err(ShellError::coerce_error(
|
||||
left_type.spanned(value.tag.span),
|
||||
@ -95,7 +161,15 @@ fn sum_of_squares(values: &[Value], name: &Tag) -> Result<Value, ShellError> {
|
||||
))
|
||||
}
|
||||
};
|
||||
sum_x = sum_x + v.into_untagged_value();
|
||||
sum_x = match compute_values(Operator::Plus, &sum_x, &v) {
|
||||
Ok(v) => v.into_untagged_value(),
|
||||
Err((left_type, right_type)) => {
|
||||
return Err(ShellError::coerce_error(
|
||||
left_type.spanned(name.span),
|
||||
right_type.spanned(name.span),
|
||||
))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let sum_x_squared = match compute_values(Operator::Multiply, &sum_x, &sum_x) {
|
||||
@ -129,9 +203,14 @@ fn sum_of_squares(values: &[Value], name: &Tag) -> Result<Value, ShellError> {
|
||||
Ok(ss)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn variance(values: &[Value], name: &Tag) -> Result<Value, ShellError> {
|
||||
compute_variance(values, values.len(), name)
|
||||
}
|
||||
|
||||
pub fn compute_variance(values: &[Value], n: usize, name: &Tag) -> Result<Value, ShellError> {
|
||||
let ss = sum_of_squares(values, name)?;
|
||||
let n = BigDecimal::from_usize(values.len()).ok_or_else(|| {
|
||||
let n = BigDecimal::from_usize(n).ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"could not convert to big decimal",
|
||||
"could not convert to big decimal",
|
||||
|
@ -41,6 +41,17 @@ impl WholeStreamCommand for Command {
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
operate(args, registry).await
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
let mut row = IndexMap::new();
|
||||
row.insert("foo".to_string(), Value::from("hi"));
|
||||
row.insert("bar".to_string(), Value::from("there"));
|
||||
vec![Example {
|
||||
description: "Parse a string into two named columns",
|
||||
example: "echo \"hi there\" | parse \"{foo} {bar}\"",
|
||||
result: Some(vec![UntaggedValue::row(row).into()]),
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn operate(
|
||||
|
61
crates/nu-cli/src/commands/path/basename.rs
Normal file
61
crates/nu-cli/src/commands/path/basename.rs
Normal file
@ -0,0 +1,61 @@
|
||||
use super::{operate, DefaultArguments};
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value};
|
||||
use std::path::Path;
|
||||
|
||||
pub struct PathBasename;
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for PathBasename {
|
||||
fn name(&self) -> &str {
|
||||
"path basename"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("path basename")
|
||||
.rest(SyntaxShape::ColumnPath, "optionally operate by path")
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"gets the filename of a path"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let tag = args.call_info.name_tag.clone();
|
||||
let (DefaultArguments { rest }, input) = args.process(®istry).await?;
|
||||
operate(input, rest, &action, tag.span).await
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "Get basename of a path",
|
||||
example: "echo '/home/joe/test.txt' | path basename",
|
||||
result: Some(vec![Value::from("test.txt")]),
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
fn action(path: &Path) -> UntaggedValue {
|
||||
UntaggedValue::string(match path.file_name() {
|
||||
Some(filename) => filename.to_string_lossy().to_string(),
|
||||
_ => "".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::PathBasename;
|
||||
|
||||
#[test]
|
||||
fn examples_work_as_expected() {
|
||||
use crate::examples::test as test_examples;
|
||||
|
||||
test_examples(PathBasename {})
|
||||
}
|
||||
}
|
46
crates/nu-cli/src/commands/path/command.rs
Normal file
46
crates/nu-cli/src/commands/path/command.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{ReturnSuccess, Signature, UntaggedValue};
|
||||
|
||||
pub struct Path;
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for Path {
|
||||
fn name(&self) -> &str {
|
||||
"path"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("path")
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Apply path function"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
_args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let registry = registry.clone();
|
||||
|
||||
Ok(OutputStream::one(ReturnSuccess::value(
|
||||
UntaggedValue::string(crate::commands::help::get_help(&Path, ®istry))
|
||||
.into_value(Tag::unknown()),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Path;
|
||||
|
||||
#[test]
|
||||
fn examples_work_as_expected() {
|
||||
use crate::examples::test as test_examples;
|
||||
|
||||
test_examples(Path {})
|
||||
}
|
||||
}
|
45
crates/nu-cli/src/commands/path/exists.rs
Normal file
45
crates/nu-cli/src/commands/path/exists.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use super::{operate, DefaultArguments};
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value};
|
||||
use std::path::Path;
|
||||
|
||||
pub struct PathExists;
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for PathExists {
|
||||
fn name(&self) -> &str {
|
||||
"path exists"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("path exists").rest(SyntaxShape::ColumnPath, "optionally operate by path")
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"checks whether the path exists"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let tag = args.call_info.name_tag.clone();
|
||||
let (DefaultArguments { rest }, input) = args.process(®istry).await?;
|
||||
operate(input, rest, &action, tag.span).await
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "Check if file exists",
|
||||
example: "echo '/home/joe/todo.txt' | path exists",
|
||||
result: Some(vec![Value::from(UntaggedValue::boolean(false))]),
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
fn action(path: &Path) -> UntaggedValue {
|
||||
UntaggedValue::boolean(path.exists())
|
||||
}
|
51
crates/nu-cli/src/commands/path/expand.rs
Normal file
51
crates/nu-cli/src/commands/path/expand.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use super::{operate, DefaultArguments};
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value};
|
||||
use std::path::Path;
|
||||
|
||||
pub struct PathExpand;
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for PathExpand {
|
||||
fn name(&self) -> &str {
|
||||
"path expand"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("path expand").rest(SyntaxShape::ColumnPath, "optionally operate by path")
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"expands the path to its absolute form"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let tag = args.call_info.name_tag.clone();
|
||||
let (DefaultArguments { rest }, input) = args.process(®istry).await?;
|
||||
operate(input, rest, &action, tag.span).await
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "Expand relative directories",
|
||||
example: "echo '/home/joe/foo/../bar' | path expand",
|
||||
result: Some(vec![Value::from("/home/joe/bar")]),
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
fn action(path: &Path) -> UntaggedValue {
|
||||
let ps = path.to_string_lossy();
|
||||
let expanded = shellexpand::tilde(&ps);
|
||||
let path: &Path = expanded.as_ref().as_ref();
|
||||
UntaggedValue::string(match path.canonicalize() {
|
||||
Ok(p) => p.to_string_lossy().to_string(),
|
||||
Err(_) => ps.to_string(),
|
||||
})
|
||||
}
|
68
crates/nu-cli/src/commands/path/extension.rs
Normal file
68
crates/nu-cli/src/commands/path/extension.rs
Normal file
@ -0,0 +1,68 @@
|
||||
use super::{operate, DefaultArguments};
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value};
|
||||
use std::path::Path;
|
||||
|
||||
pub struct PathExtension;
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for PathExtension {
|
||||
fn name(&self) -> &str {
|
||||
"path extension"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("path extension")
|
||||
.rest(SyntaxShape::ColumnPath, "optionally operate by path")
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"gets the extension of a path"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let tag = args.call_info.name_tag.clone();
|
||||
let (DefaultArguments { rest }, input) = args.process(®istry).await?;
|
||||
operate(input, rest, &action, tag.span).await
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Get extension of a path",
|
||||
example: "echo 'test.txt' | path extension",
|
||||
result: Some(vec![Value::from("txt")]),
|
||||
},
|
||||
Example {
|
||||
description: "You get an empty string if there is no extension",
|
||||
example: "echo 'test' | path extension",
|
||||
result: Some(vec![Value::from("")]),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn action(path: &Path) -> UntaggedValue {
|
||||
UntaggedValue::string(match path.extension() {
|
||||
Some(ext) => ext.to_string_lossy().to_string(),
|
||||
_ => "".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::PathExtension;
|
||||
|
||||
#[test]
|
||||
fn examples_work_as_expected() {
|
||||
use crate::examples::test as test_examples;
|
||||
|
||||
test_examples(PathExtension {})
|
||||
}
|
||||
}
|
75
crates/nu-cli/src/commands/path/mod.rs
Normal file
75
crates/nu-cli/src/commands/path/mod.rs
Normal file
@ -0,0 +1,75 @@
|
||||
mod basename;
|
||||
mod command;
|
||||
mod exists;
|
||||
mod expand;
|
||||
mod extension;
|
||||
mod r#type;
|
||||
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{ColumnPath, Primitive, ReturnSuccess, ShellTypeName, UntaggedValue, Value};
|
||||
use nu_source::Span;
|
||||
use std::path::Path;
|
||||
|
||||
pub use basename::PathBasename;
|
||||
pub use command::Path as PathCommand;
|
||||
pub use exists::PathExists;
|
||||
pub use expand::PathExpand;
|
||||
pub use extension::PathExtension;
|
||||
pub use r#type::PathType;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DefaultArguments {
|
||||
rest: Vec<ColumnPath>,
|
||||
}
|
||||
|
||||
fn handle_value<F>(action: &F, v: &Value, span: Span) -> Result<Value, ShellError>
|
||||
where
|
||||
F: Fn(&Path) -> UntaggedValue + Send + 'static,
|
||||
{
|
||||
let v = match &v.value {
|
||||
UntaggedValue::Primitive(Primitive::Path(buf)) => action(buf).into_value(v.tag()),
|
||||
UntaggedValue::Primitive(Primitive::String(s))
|
||||
| UntaggedValue::Primitive(Primitive::Line(s)) => action(s.as_ref()).into_value(v.tag()),
|
||||
other => {
|
||||
let got = format!("got {}", other.type_name());
|
||||
return Err(ShellError::labeled_error_with_secondary(
|
||||
"value is not string or path",
|
||||
got,
|
||||
span,
|
||||
"originates from here".to_string(),
|
||||
v.tag().span,
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
async fn operate<F>(
|
||||
input: crate::InputStream,
|
||||
paths: Vec<ColumnPath>,
|
||||
action: &'static F,
|
||||
span: Span,
|
||||
) -> Result<OutputStream, ShellError>
|
||||
where
|
||||
F: Fn(&Path) -> UntaggedValue + Send + Sync + 'static,
|
||||
{
|
||||
Ok(input
|
||||
.map(move |v| {
|
||||
if paths.is_empty() {
|
||||
ReturnSuccess::value(handle_value(&action, &v, span)?)
|
||||
} else {
|
||||
let mut ret = v;
|
||||
|
||||
for path in &paths {
|
||||
ret = ret.swap_data_by_column_path(
|
||||
path,
|
||||
Box::new(move |old| handle_value(&action, &old, span)),
|
||||
)?;
|
||||
}
|
||||
|
||||
ReturnSuccess::value(ret)
|
||||
}
|
||||
})
|
||||
.to_output_stream())
|
||||
}
|
62
crates/nu-cli/src/commands/path/type.rs
Normal file
62
crates/nu-cli/src/commands/path/type.rs
Normal file
@ -0,0 +1,62 @@
|
||||
use super::{operate, DefaultArguments};
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::data::files::get_file_type;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value};
|
||||
use std::path::Path;
|
||||
|
||||
pub struct PathType;
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for PathType {
|
||||
fn name(&self) -> &str {
|
||||
"path type"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("path type").rest(SyntaxShape::ColumnPath, "optionally operate by path")
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"gives the type of the object the path refers to (eg file, dir, symlink)"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let tag = args.call_info.name_tag.clone();
|
||||
let (DefaultArguments { rest }, input) = args.process(®istry).await?;
|
||||
operate(input, rest, &action, tag.span).await
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "Show type of a filepath",
|
||||
example: "echo '.' | path type",
|
||||
result: Some(vec![Value::from("Dir")]),
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
fn action(path: &Path) -> UntaggedValue {
|
||||
let meta = std::fs::symlink_metadata(path);
|
||||
UntaggedValue::string(match &meta {
|
||||
Ok(md) => get_file_type(md),
|
||||
Err(_) => "",
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::PathType;
|
||||
|
||||
#[test]
|
||||
fn examples_work_as_expected() {
|
||||
use crate::examples::test as test_examples;
|
||||
|
||||
test_examples(PathType {})
|
||||
}
|
||||
}
|
178
crates/nu-cli/src/commands/reduce.rs
Normal file
178
crates/nu-cli/src/commands/reduce.rs
Normal file
@ -0,0 +1,178 @@
|
||||
use crate::commands::classified::block::run_block;
|
||||
use crate::commands::each;
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use crate::{CommandArgs, CommandRegistry, Example, OutputStream};
|
||||
use futures::stream::once;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{hir::Block, Primitive, Scope, Signature, SyntaxShape, UntaggedValue, Value};
|
||||
use nu_source::Tagged;
|
||||
|
||||
pub struct Reduce;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ReduceArgs {
|
||||
block: Block,
|
||||
fold: Option<Value>,
|
||||
numbered: Tagged<bool>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for Reduce {
|
||||
fn name(&self) -> &str {
|
||||
"reduce"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("reduce")
|
||||
.named(
|
||||
"fold",
|
||||
SyntaxShape::Any,
|
||||
"reduce with initial value",
|
||||
Some('f'),
|
||||
)
|
||||
.required("block", SyntaxShape::Block, "reducing function")
|
||||
.switch(
|
||||
"numbered",
|
||||
"returned a numbered item ($it.index and $it.item)",
|
||||
Some('n'),
|
||||
)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Aggregate a list table to a single value using an accumulator block. Block must be
|
||||
(A, A) -> A unless --fold is selected, in which case it may be A, B -> A."
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
reduce(args, registry).await
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Simple summation (equivalent to math sum)",
|
||||
example: "echo 1 2 3 4 | reduce { = $acc + $it }",
|
||||
result: Some(vec![UntaggedValue::int(10).into()]),
|
||||
},
|
||||
Example {
|
||||
description: "Summation from starting value using fold",
|
||||
example: "echo 1 2 3 4 | reduce -f $(= -1) { = $acc + $it }",
|
||||
result: Some(vec![UntaggedValue::int(9).into()]),
|
||||
},
|
||||
Example {
|
||||
description: "Folding with rows",
|
||||
example: "<table> | reduce -f 1.6 { = $acc * $(echo $it.a | str to-int) + $(echo $it.b | str to-int) }",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Numbered reduce to find index of longest word",
|
||||
example: "echo one longest three bar | reduce -n { if $(echo $it.item | str length) > $(echo $acc.item | str length) {echo $it} {echo $acc}} | get index",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_row(
|
||||
block: Arc<Block>,
|
||||
scope: Arc<Scope>,
|
||||
mut context: Arc<Context>,
|
||||
row: Value,
|
||||
) -> Result<InputStream, ShellError> {
|
||||
let row_clone = row.clone();
|
||||
let input_stream = once(async { Ok(row_clone) }).to_input_stream();
|
||||
|
||||
Ok(run_block(
|
||||
&block,
|
||||
Arc::make_mut(&mut context),
|
||||
input_stream,
|
||||
&row,
|
||||
&scope.vars,
|
||||
&scope.env,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn reduce(
|
||||
raw_args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let registry = registry.clone();
|
||||
let base_scope = raw_args.call_info.scope.clone();
|
||||
let context = Arc::new(Context::from_raw(&raw_args, ®istry));
|
||||
let (reduce_args, mut input): (ReduceArgs, _) = raw_args.process(®istry).await?;
|
||||
let block = Arc::new(reduce_args.block);
|
||||
let (ioffset, start) = match reduce_args.fold {
|
||||
None => {
|
||||
let first = input
|
||||
.next()
|
||||
.await
|
||||
.expect("empty stream expected to contain Primitive::Nothing");
|
||||
if let UntaggedValue::Primitive(Primitive::Nothing) = first.value {
|
||||
return Err(ShellError::missing_value(None, "empty input"));
|
||||
}
|
||||
|
||||
(1, first)
|
||||
}
|
||||
Some(acc) => (0, acc),
|
||||
};
|
||||
|
||||
if reduce_args.numbered.item {
|
||||
// process_row returns Result<InputStream, ShellError>, so we must fold with one
|
||||
let initial = Ok(InputStream::one(each::make_indexed_item(
|
||||
ioffset - 1,
|
||||
start,
|
||||
)));
|
||||
|
||||
Ok(input
|
||||
.enumerate()
|
||||
.fold(initial, move |acc, input| {
|
||||
let block = Arc::clone(&block);
|
||||
let mut scope = base_scope.clone();
|
||||
let context = Arc::clone(&context);
|
||||
let row = each::make_indexed_item(input.0 + ioffset, input.1);
|
||||
|
||||
async {
|
||||
let f = acc?.into_vec().await[0].clone();
|
||||
scope.vars.insert(String::from("$acc"), f);
|
||||
process_row(block, Arc::new(scope), context, row).await
|
||||
}
|
||||
})
|
||||
.await?
|
||||
.to_output_stream())
|
||||
} else {
|
||||
let initial = Ok(InputStream::one(start));
|
||||
Ok(input
|
||||
.fold(initial, move |acc, row| {
|
||||
let block = Arc::clone(&block);
|
||||
let mut scope = base_scope.clone();
|
||||
let context = Arc::clone(&context);
|
||||
|
||||
async {
|
||||
scope
|
||||
.vars
|
||||
.insert(String::from("$acc"), acc?.into_vec().await[0].clone());
|
||||
process_row(block, Arc::new(scope), context, row).await
|
||||
}
|
||||
})
|
||||
.await?
|
||||
.to_output_stream())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Reduce;
|
||||
|
||||
#[test]
|
||||
fn examples_work_as_expected() {
|
||||
use crate::examples::test as test_examples;
|
||||
|
||||
test_examples(Reduce {})
|
||||
}
|
||||
}
|
@ -41,7 +41,7 @@ impl WholeStreamCommand for AliasCommand {
|
||||
let call_info = args.call_info.clone();
|
||||
let registry = registry.clone();
|
||||
let mut block = self.block.clone();
|
||||
block.set_is_last(call_info.args.is_last);
|
||||
block.set_redirect(call_info.args.external_redirection);
|
||||
|
||||
let alias_command = self.clone();
|
||||
let mut context = Context::from_args(&args, ®istry);
|
||||
|
@ -62,6 +62,8 @@ impl WholeStreamCommand for RunExternalCommand {
|
||||
|
||||
let mut positionals = positionals.into_iter();
|
||||
|
||||
let external_redirection = args.call_info.args.external_redirection;
|
||||
|
||||
let name = positionals
|
||||
.next()
|
||||
.ok_or_else(|| {
|
||||
@ -124,26 +126,24 @@ impl WholeStreamCommand for RunExternalCommand {
|
||||
let result = external_context
|
||||
.shell_manager
|
||||
.cd(cd_args, args.call_info.name_tag.clone());
|
||||
match result {
|
||||
Ok(stream) => return Ok(stream.to_output_stream()),
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(result?.to_output_stream());
|
||||
}
|
||||
}
|
||||
|
||||
let scope = args.call_info.scope.clone();
|
||||
let is_last = args.call_info.args.is_last;
|
||||
let input = args.input;
|
||||
let result =
|
||||
external::run_external_command(command, &mut external_context, input, &scope, is_last)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(stream) => Ok(stream.to_output_stream()),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
let input = args.input;
|
||||
let result = external::run_external_command(
|
||||
command,
|
||||
&mut external_context,
|
||||
input,
|
||||
&scope,
|
||||
external_redirection,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(result?.to_output_stream())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,10 @@
|
||||
use crate::commands::{UnevaluatedCallInfo, WholeStreamCommand};
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value};
|
||||
use nu_protocol::{
|
||||
hir::ExternalRedirection, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue,
|
||||
Value,
|
||||
};
|
||||
use nu_source::Tagged;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@ -226,7 +229,7 @@ async fn save(
|
||||
positional: None,
|
||||
named: None,
|
||||
span: Span::unknown(),
|
||||
is_last: false,
|
||||
external_redirection: ExternalRedirection::Stdout,
|
||||
},
|
||||
name_tag: name_tag.clone(),
|
||||
scope,
|
||||
@ -251,10 +254,7 @@ async fn save(
|
||||
};
|
||||
};
|
||||
|
||||
match content {
|
||||
Ok(save_data) => shell_manager.save(&full_path, &save_data, name.span),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
shell_manager.save(&full_path, &content?, name.span)
|
||||
}
|
||||
|
||||
fn string_from(input: &[Value]) -> String {
|
||||
|
@ -56,10 +56,8 @@ pub async fn split_by(
|
||||
));
|
||||
}
|
||||
|
||||
match split(&column_name, &values[0], &name) {
|
||||
Ok(splits) => Ok(OutputStream::one(ReturnSuccess::value(splits))),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
let split = split(&column_name, &values[0], &name)?;
|
||||
Ok(OutputStream::one(ReturnSuccess::value(split)))
|
||||
}
|
||||
|
||||
enum Grouper {
|
||||
@ -91,10 +89,7 @@ pub fn split(
|
||||
crate::utils::data::split(&values, &Some(block), &name)
|
||||
}
|
||||
Grouper::ByColumn(None) => {
|
||||
let block = Box::new(move |_, row: &Value| match as_string(row) {
|
||||
Ok(group_key) => Ok(group_key),
|
||||
Err(reason) => Err(reason),
|
||||
});
|
||||
let block = Box::new(move |_, row: &Value| as_string(row));
|
||||
|
||||
crate::utils::data::split(&values, &Some(block), &name)
|
||||
}
|
||||
@ -129,106 +124,52 @@ pub fn suggestions(tried: Tagged<&str>, for_value: &Value) -> ShellError {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::split;
|
||||
use crate::commands::group_by::group;
|
||||
use indexmap::IndexMap;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{UntaggedValue, Value};
|
||||
use crate::utils::data::helpers::{committers_grouped_by_date, date, int, row, string, table};
|
||||
use nu_protocol::UntaggedValue;
|
||||
use nu_source::*;
|
||||
|
||||
fn string(input: impl Into<String>) -> Value {
|
||||
UntaggedValue::string(input.into()).into_untagged_value()
|
||||
}
|
||||
|
||||
fn row(entries: IndexMap<String, Value>) -> Value {
|
||||
UntaggedValue::row(entries).into_untagged_value()
|
||||
}
|
||||
|
||||
fn table(list: &[Value]) -> Value {
|
||||
UntaggedValue::table(list).into_untagged_value()
|
||||
}
|
||||
|
||||
fn nu_releases_grouped_by_date() -> Result<Value, ShellError> {
|
||||
let key = Some(String::from("date").tagged_unknown());
|
||||
let sample = table(&nu_releases_committers());
|
||||
group(&key, &sample, Tag::unknown())
|
||||
}
|
||||
|
||||
fn nu_releases_committers() -> Vec<Value> {
|
||||
vec![
|
||||
row(
|
||||
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")},
|
||||
),
|
||||
row(
|
||||
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("August 23-2019")},
|
||||
),
|
||||
row(
|
||||
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")},
|
||||
),
|
||||
row(
|
||||
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("Sept 24-2019")},
|
||||
),
|
||||
row(
|
||||
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("October 10-2019")},
|
||||
),
|
||||
row(
|
||||
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("Sept 24-2019")},
|
||||
),
|
||||
row(
|
||||
indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("October 10-2019")},
|
||||
),
|
||||
row(
|
||||
indexmap! {"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("Sept 24-2019")},
|
||||
),
|
||||
row(
|
||||
indexmap! {"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("August 23-2019")},
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splits_inner_tables_by_key() -> Result<(), ShellError> {
|
||||
fn splits_inner_tables_by_key() {
|
||||
let for_key = Some(String::from("country").tagged_unknown());
|
||||
|
||||
assert_eq!(
|
||||
split(&for_key, &nu_releases_grouped_by_date()?, Tag::unknown())?,
|
||||
split(&for_key, &committers_grouped_by_date(), Tag::unknown()).unwrap(),
|
||||
UntaggedValue::row(indexmap! {
|
||||
"EC".into() => row(indexmap! {
|
||||
"August 23-2019".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")})
|
||||
"2019-07-23".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => date("2019-07-23"), "chickens".into() => int(10)})
|
||||
]),
|
||||
"Sept 24-2019".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("Sept 24-2019")})
|
||||
"2019-09-24".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => date("2019-09-24"), "chickens".into() => int(20)})
|
||||
]),
|
||||
"October 10-2019".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("October 10-2019")})
|
||||
"2019-10-10".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => date("2019-10-10"), "chickens".into() => int(30)})
|
||||
])
|
||||
}),
|
||||
"NZ".into() => row(indexmap! {
|
||||
"August 23-2019".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("August 23-2019")})
|
||||
"2019-07-23".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => date("2019-07-23"), "chickens".into() => int(5)})
|
||||
]),
|
||||
"Sept 24-2019".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("Sept 24-2019")})
|
||||
"2019-09-24".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => date("2019-09-24"), "chickens".into() => int(10)})
|
||||
]),
|
||||
"October 10-2019".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => string("October 10-2019")})
|
||||
"2019-10-10".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => date("2019-10-10"), "chickens".into() => int(15)})
|
||||
])
|
||||
}),
|
||||
"US".into() => row(indexmap! {
|
||||
"August 23-2019".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("August 23-2019")})
|
||||
"2019-07-23".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => date("2019-07-23"), "chickens".into() => int(2)})
|
||||
]),
|
||||
"Sept 24-2019".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("Sept 24-2019")})
|
||||
"2019-09-24".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => date("2019-09-24"), "chickens".into() => int(4)})
|
||||
]),
|
||||
"October 10-2019".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")})
|
||||
"2019-10-10".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => date("2019-10-10"), "chickens".into() => int(6)})
|
||||
])
|
||||
})
|
||||
}).into_untagged_value()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -236,11 +177,11 @@ mod tests {
|
||||
let for_key = Some(String::from("country").tagged_unknown());
|
||||
|
||||
let nu_releases = row(indexmap! {
|
||||
"August 23-2019".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")})
|
||||
"2019-07-23".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("2019-07-23")})
|
||||
]),
|
||||
"Sept 24-2019".into() => table(&[
|
||||
row(indexmap!{"name".into() => UntaggedValue::string("JT").into_value(Tag::from(Span::new(5,10))), "date".into() => string("Sept 24-2019")})
|
||||
"2019-09-24".into() => table(&[
|
||||
row(indexmap!{"name".into() => UntaggedValue::string("JT").into_value(Tag::from(Span::new(5,10))), "date".into() => string("2019-09-24")})
|
||||
]),
|
||||
"October 10-2019".into() => table(&[
|
||||
row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")})
|
||||
|
@ -1,10 +1,16 @@
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{ReturnSuccess, Signature, UntaggedValue, Value};
|
||||
use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value};
|
||||
use nu_source::Tagged;
|
||||
|
||||
pub struct SubCommand;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SubCommandArgs {
|
||||
separator: Option<Tagged<String>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for SubCommand {
|
||||
fn name(&self) -> &str {
|
||||
@ -12,7 +18,11 @@ impl WholeStreamCommand for SubCommand {
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("str collect")
|
||||
Signature::build("str collect").desc(self.usage()).optional(
|
||||
"separator",
|
||||
SyntaxShape::String,
|
||||
"the separator to put between the different values",
|
||||
)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
@ -22,16 +32,9 @@ impl WholeStreamCommand for SubCommand {
|
||||
async fn run(
|
||||
&self,
|
||||
args: CommandArgs,
|
||||
_registry: &CommandRegistry,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let output = args
|
||||
.input
|
||||
.collect_string(args.call_info.name_tag.clone())
|
||||
.await?;
|
||||
|
||||
Ok(OutputStream::one(ReturnSuccess::value(
|
||||
UntaggedValue::string(output.item).into_value(output.tag),
|
||||
)))
|
||||
collect(args, registry).await
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
@ -43,6 +46,24 @@ impl WholeStreamCommand for SubCommand {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn collect(
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let tag = args.call_info.name_tag.clone();
|
||||
let (SubCommandArgs { separator }, input) = args.process(registry).await?;
|
||||
let separator = separator.map(|tagged| tagged.item).unwrap_or_default();
|
||||
|
||||
let strings: Vec<Result<String, ShellError>> =
|
||||
input.map(|value| value.as_string()).collect().await;
|
||||
let strings: Vec<String> = strings.into_iter().collect::<Result<_, _>>()?;
|
||||
let output = strings.join(&separator);
|
||||
|
||||
Ok(OutputStream::one(ReturnSuccess::value(
|
||||
UntaggedValue::string(output).into_value(tag),
|
||||
)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::SubCommand;
|
||||
|
190
crates/nu-cli/src/commands/str_/contains.rs
Normal file
190
crates/nu-cli/src/commands/str_/contains.rs
Normal file
@ -0,0 +1,190 @@
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::ShellTypeName;
|
||||
use nu_protocol::{
|
||||
ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value,
|
||||
};
|
||||
use nu_source::{Tag, Tagged};
|
||||
use nu_value_ext::ValueExt;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Arguments {
|
||||
pattern: Tagged<String>,
|
||||
rest: Vec<ColumnPath>,
|
||||
insensitive: bool,
|
||||
}
|
||||
|
||||
pub struct SubCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for SubCommand {
|
||||
fn name(&self) -> &str {
|
||||
"str contains"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("str contains")
|
||||
.required("pattern", SyntaxShape::String, "the pattern to find")
|
||||
.rest(
|
||||
SyntaxShape::ColumnPath,
|
||||
"optionally check if string contains pattern by column paths",
|
||||
)
|
||||
.switch("insensitive", "search is case insensitive", Some('i'))
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Checks if string contains pattern"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
operate(args, registry).await
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Check if string contains pattern",
|
||||
example: "echo 'my_library.rb' | str contains '.rb'",
|
||||
result: Some(vec![UntaggedValue::boolean(true).into_untagged_value()]),
|
||||
},
|
||||
Example {
|
||||
description: "Check if string contains pattern case insensitive",
|
||||
example: "echo 'my_library.rb' | str contains -i '.RB'",
|
||||
result: Some(vec![UntaggedValue::boolean(true).into_untagged_value()]),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
async fn operate(
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let registry = registry.clone();
|
||||
|
||||
let (
|
||||
Arguments {
|
||||
pattern,
|
||||
rest,
|
||||
insensitive,
|
||||
},
|
||||
input,
|
||||
) = args.process(®istry).await?;
|
||||
let column_paths: Vec<_> = rest;
|
||||
|
||||
Ok(input
|
||||
.map(move |v| {
|
||||
if column_paths.is_empty() {
|
||||
ReturnSuccess::value(action(&v, &pattern, insensitive, v.tag())?)
|
||||
} else {
|
||||
let mut ret = v;
|
||||
|
||||
for path in &column_paths {
|
||||
let pattern = pattern.clone();
|
||||
ret = ret.swap_data_by_column_path(
|
||||
path,
|
||||
Box::new(move |old| action(old, &pattern, insensitive, old.tag())),
|
||||
)?;
|
||||
}
|
||||
|
||||
ReturnSuccess::value(ret)
|
||||
}
|
||||
})
|
||||
.to_output_stream())
|
||||
}
|
||||
|
||||
fn action(
|
||||
input: &Value,
|
||||
pattern: &str,
|
||||
insensitive: bool,
|
||||
tag: impl Into<Tag>,
|
||||
) -> Result<Value, ShellError> {
|
||||
match &input.value {
|
||||
UntaggedValue::Primitive(Primitive::Line(s))
|
||||
| UntaggedValue::Primitive(Primitive::String(s)) => {
|
||||
let contains = if insensitive {
|
||||
s.to_lowercase().find(&pattern.to_lowercase()).is_some()
|
||||
} else {
|
||||
s.find(pattern).is_some()
|
||||
};
|
||||
|
||||
Ok(UntaggedValue::boolean(contains).into_value(tag))
|
||||
}
|
||||
other => {
|
||||
let got = format!("got {}", other.type_name());
|
||||
Err(ShellError::labeled_error(
|
||||
"value is not string",
|
||||
got,
|
||||
tag.into().span,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{action, SubCommand};
|
||||
use nu_plugin::test_helpers::value::string;
|
||||
use nu_protocol::{Primitive, UntaggedValue};
|
||||
use nu_source::Tag;
|
||||
|
||||
#[test]
|
||||
fn examples_work_as_expected() {
|
||||
use crate::examples::test as test_examples;
|
||||
|
||||
test_examples(SubCommand {})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_contains_other_string_case_sensitive() {
|
||||
let word = string("Cargo.tomL");
|
||||
let pattern = ".tomL";
|
||||
let insensitive = false;
|
||||
let expected =
|
||||
UntaggedValue::Primitive(Primitive::Boolean(true.into())).into_untagged_value();
|
||||
|
||||
let actual = action(&word, &pattern, insensitive, Tag::unknown()).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_does_not_contain_other_string_case_sensitive() {
|
||||
let word = string("Cargo.tomL");
|
||||
let pattern = "Lomt.";
|
||||
let insensitive = false;
|
||||
let expected =
|
||||
UntaggedValue::Primitive(Primitive::Boolean(false.into())).into_untagged_value();
|
||||
|
||||
let actual = action(&word, &pattern, insensitive, Tag::unknown()).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_contains_other_string_case_insensitive() {
|
||||
let word = string("Cargo.ToMl");
|
||||
let pattern = ".TOML";
|
||||
let insensitive = true;
|
||||
let expected =
|
||||
UntaggedValue::Primitive(Primitive::Boolean(true.into())).into_untagged_value();
|
||||
|
||||
let actual = action(&word, &pattern, insensitive, Tag::unknown()).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_does_not_contain_other_string_case_insensitive() {
|
||||
let word = string("Cargo.tOml");
|
||||
let pattern = "lomt.";
|
||||
let insensitive = true;
|
||||
let expected =
|
||||
UntaggedValue::Primitive(Primitive::Boolean(false.into())).into_untagged_value();
|
||||
|
||||
let actual = action(&word, &pattern, insensitive, Tag::unknown()).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
138
crates/nu-cli/src/commands/str_/ends_with.rs
Normal file
138
crates/nu-cli/src/commands/str_/ends_with.rs
Normal file
@ -0,0 +1,138 @@
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::ShellTypeName;
|
||||
use nu_protocol::{
|
||||
ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value,
|
||||
};
|
||||
use nu_source::{Tag, Tagged};
|
||||
use nu_value_ext::ValueExt;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Arguments {
|
||||
pattern: Tagged<String>,
|
||||
rest: Vec<ColumnPath>,
|
||||
}
|
||||
pub struct SubCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for SubCommand {
|
||||
fn name(&self) -> &str {
|
||||
"str ends-with"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("str ends-with")
|
||||
.required("pattern", SyntaxShape::String, "the pattern to match")
|
||||
.rest(
|
||||
SyntaxShape::ColumnPath,
|
||||
"optionally matches suffix of text by column paths",
|
||||
)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"checks if string ends with pattern"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
operate(args, registry).await
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "Checks if string ends with '.rb' pattern",
|
||||
example: "echo 'my_library.rb' | str ends-with '.rb'",
|
||||
result: Some(vec![UntaggedValue::boolean(true).into_untagged_value()]),
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
async fn operate(
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let registry = registry.clone();
|
||||
|
||||
let (Arguments { pattern, rest }, input) = args.process(®istry).await?;
|
||||
|
||||
let column_paths: Vec<_> = rest;
|
||||
|
||||
Ok(input
|
||||
.map(move |v| {
|
||||
if column_paths.is_empty() {
|
||||
ReturnSuccess::value(action(&v, &pattern, v.tag())?)
|
||||
} else {
|
||||
let mut ret = v;
|
||||
|
||||
for path in &column_paths {
|
||||
let pattern = pattern.clone();
|
||||
ret = ret.swap_data_by_column_path(
|
||||
path,
|
||||
Box::new(move |old| action(old, &pattern, old.tag())),
|
||||
)?;
|
||||
}
|
||||
|
||||
ReturnSuccess::value(ret)
|
||||
}
|
||||
})
|
||||
.to_output_stream())
|
||||
}
|
||||
|
||||
fn action(input: &Value, pattern: &str, tag: impl Into<Tag>) -> Result<Value, ShellError> {
|
||||
match &input.value {
|
||||
UntaggedValue::Primitive(Primitive::Line(s))
|
||||
| UntaggedValue::Primitive(Primitive::String(s)) => {
|
||||
let ends_with = s.ends_with(pattern);
|
||||
Ok(UntaggedValue::boolean(ends_with).into_value(tag))
|
||||
}
|
||||
other => {
|
||||
let got = format!("got {}", other.type_name());
|
||||
Err(ShellError::labeled_error(
|
||||
"value is not string",
|
||||
got,
|
||||
tag.into().span,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{action, SubCommand};
|
||||
use nu_plugin::test_helpers::value::string;
|
||||
use nu_protocol::{Primitive, UntaggedValue};
|
||||
use nu_source::Tag;
|
||||
|
||||
#[test]
|
||||
fn examples_work_as_expected() {
|
||||
use crate::examples::test as test_examples;
|
||||
|
||||
test_examples(SubCommand {})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn str_ends_with_pattern() {
|
||||
let word = string("Cargo.toml");
|
||||
let pattern = ".toml";
|
||||
let expected =
|
||||
UntaggedValue::Primitive(Primitive::Boolean(true.into())).into_untagged_value();
|
||||
|
||||
let actual = action(&word, &pattern, Tag::unknown()).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn str_does_not_end_with_pattern() {
|
||||
let word = string("Cargo.toml");
|
||||
let pattern = "Car";
|
||||
let expected =
|
||||
UntaggedValue::Primitive(Primitive::Boolean(false.into())).into_untagged_value();
|
||||
|
||||
let actual = action(&word, &pattern, Tag::unknown()).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
use crate::commands::str_::trim::trim_char;
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
@ -196,7 +195,7 @@ fn format_decimal(mut decimal: BigDecimal, digits: Option<u64>, group_digits: bo
|
||||
.take(n as usize)
|
||||
.collect()
|
||||
} else {
|
||||
trim_char(&dec_part, '0', false, true)
|
||||
String::from(dec_part.trim_end_matches('0'))
|
||||
};
|
||||
|
||||
let format_default_loc = |int_part: BigInt| {
|
||||
|
314
crates/nu-cli/src/commands/str_/index_of.rs
Normal file
314
crates/nu-cli/src/commands/str_/index_of.rs
Normal file
@ -0,0 +1,314 @@
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::ShellTypeName;
|
||||
use nu_protocol::{
|
||||
ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value,
|
||||
};
|
||||
use nu_source::{Tag, Tagged};
|
||||
use nu_value_ext::{as_string, ValueExt};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Arguments {
|
||||
pattern: Tagged<String>,
|
||||
rest: Vec<ColumnPath>,
|
||||
range: Option<Value>,
|
||||
}
|
||||
|
||||
pub struct SubCommand;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IndexOfOptionalBounds(i32, i32);
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for SubCommand {
|
||||
fn name(&self) -> &str {
|
||||
"str index-of"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("str index-of")
|
||||
.required(
|
||||
"pattern",
|
||||
SyntaxShape::String,
|
||||
"the pattern to find index of",
|
||||
)
|
||||
.rest(
|
||||
SyntaxShape::ColumnPath,
|
||||
"optionally returns index of pattern in string by column paths",
|
||||
)
|
||||
.named(
|
||||
"range",
|
||||
SyntaxShape::Any,
|
||||
"optional start and/or end index",
|
||||
Some('r'),
|
||||
)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Returns starting index of given pattern in string counting from 0. Returns -1 when there are no results."
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
operate(args, registry).await
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Returns index of pattern in string",
|
||||
example: "echo 'my_library.rb' | str index-of '.rb'",
|
||||
result: Some(vec![UntaggedValue::int(10).into_untagged_value()]),
|
||||
},
|
||||
Example {
|
||||
description: "Returns index of pattern in string with start index",
|
||||
example: "echo '.rb.rb' | str index-of '.rb' -r '1,'",
|
||||
result: Some(vec![UntaggedValue::int(3).into_untagged_value()]),
|
||||
},
|
||||
Example {
|
||||
description: "Returns index of pattern in string with end index",
|
||||
example: "echo '123456' | str index-of '6' -r ',4'",
|
||||
result: Some(vec![UntaggedValue::int(-1).into_untagged_value()]),
|
||||
},
|
||||
Example {
|
||||
description: "Returns index of pattern in string with start and end index",
|
||||
example: "echo '123456' | str index-of '3' -r '1,4'",
|
||||
result: Some(vec![UntaggedValue::int(2).into_untagged_value()]),
|
||||
},
|
||||
Example {
|
||||
description: "Alternativly you can use this form",
|
||||
example: "echo '123456' | str index-of '3' -r [1 4]",
|
||||
result: Some(vec![UntaggedValue::int(2).into_untagged_value()]),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
async fn operate(
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let registry = registry.clone();
|
||||
|
||||
let (
|
||||
Arguments {
|
||||
pattern,
|
||||
rest,
|
||||
range,
|
||||
},
|
||||
input,
|
||||
) = args.process(®istry).await?;
|
||||
let range = range.unwrap_or_else(|| {
|
||||
UntaggedValue::Primitive(Primitive::String("".to_string())).into_untagged_value()
|
||||
});
|
||||
let column_paths: Vec<_> = rest;
|
||||
|
||||
Ok(input
|
||||
.map(move |v| {
|
||||
if column_paths.is_empty() {
|
||||
ReturnSuccess::value(action(&v, &pattern, &range, v.tag())?)
|
||||
} else {
|
||||
let mut ret = v;
|
||||
|
||||
for path in &column_paths {
|
||||
let range = range.clone();
|
||||
let pattern = pattern.clone();
|
||||
ret = ret.swap_data_by_column_path(
|
||||
path,
|
||||
Box::new(move |old| action(old, &pattern, &range, old.tag())),
|
||||
)?;
|
||||
}
|
||||
|
||||
ReturnSuccess::value(ret)
|
||||
}
|
||||
})
|
||||
.to_output_stream())
|
||||
}
|
||||
|
||||
fn action(
|
||||
input: &Value,
|
||||
pattern: &str,
|
||||
range: &Value,
|
||||
tag: impl Into<Tag>,
|
||||
) -> Result<Value, ShellError> {
|
||||
let r = process_range(&input, &range)?;
|
||||
match &input.value {
|
||||
UntaggedValue::Primitive(Primitive::Line(s))
|
||||
| UntaggedValue::Primitive(Primitive::String(s)) => {
|
||||
let start_index = r.0 as usize;
|
||||
let end_index = r.1 as usize;
|
||||
|
||||
if let Some(result) = s[start_index..end_index].find(pattern) {
|
||||
Ok(UntaggedValue::int(result + start_index).into_value(tag))
|
||||
} else {
|
||||
let not_found = -1;
|
||||
Ok(UntaggedValue::int(not_found).into_value(tag))
|
||||
}
|
||||
}
|
||||
other => {
|
||||
let got = format!("got {}", other.type_name());
|
||||
Err(ShellError::labeled_error(
|
||||
"value is not string",
|
||||
got,
|
||||
tag.into().span,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_range(input: &Value, range: &Value) -> Result<IndexOfOptionalBounds, ShellError> {
|
||||
let input_len = match &input.value {
|
||||
UntaggedValue::Primitive(Primitive::Line(s))
|
||||
| UntaggedValue::Primitive(Primitive::String(s)) => s.len(),
|
||||
_ => 0,
|
||||
};
|
||||
let min_index_str = String::from("0");
|
||||
let max_index_str = input_len.to_string();
|
||||
let r = match &range.value {
|
||||
UntaggedValue::Primitive(Primitive::Line(s))
|
||||
| UntaggedValue::Primitive(Primitive::String(s)) => {
|
||||
let indexes: Vec<&str> = s.split(',').collect();
|
||||
|
||||
let start_index = indexes.get(0).unwrap_or(&&min_index_str[..]).to_string();
|
||||
|
||||
let end_index = indexes.get(1).unwrap_or(&&max_index_str[..]).to_string();
|
||||
|
||||
Ok((start_index, end_index))
|
||||
}
|
||||
UntaggedValue::Table(indexes) => {
|
||||
if indexes.len() > 2 {
|
||||
Err(ShellError::labeled_error(
|
||||
"there shouldn't be more than two indexes",
|
||||
"too many indexes",
|
||||
range.tag(),
|
||||
))
|
||||
} else {
|
||||
let idx: Vec<String> = indexes
|
||||
.iter()
|
||||
.map(|v| as_string(v).unwrap_or_else(|_| String::from("")))
|
||||
.collect();
|
||||
|
||||
let start_index = idx.get(0).unwrap_or(&min_index_str).to_string();
|
||||
let end_index = idx.get(1).unwrap_or(&max_index_str).to_string();
|
||||
|
||||
Ok((start_index, end_index))
|
||||
}
|
||||
}
|
||||
other => {
|
||||
let got = format!("got {}", other.type_name());
|
||||
Err(ShellError::labeled_error(
|
||||
"value is not string",
|
||||
got,
|
||||
range.tag(),
|
||||
))
|
||||
}
|
||||
}?;
|
||||
|
||||
let start_index = r.0.parse::<i32>().unwrap_or(0);
|
||||
let end_index = r.1.parse::<i32>().unwrap_or(input_len as i32);
|
||||
|
||||
if start_index < 0 || start_index > end_index {
|
||||
return Err(ShellError::labeled_error(
|
||||
"start index can't be negative or greater than end index",
|
||||
"Invalid start index",
|
||||
range.tag(),
|
||||
));
|
||||
}
|
||||
|
||||
if end_index < 0 || end_index < start_index || end_index > input_len as i32 {
|
||||
return Err(ShellError::labeled_error(
|
||||
"end index can't be negative, smaller than start index or greater than input length",
|
||||
"Invalid end index",
|
||||
range.tag(),
|
||||
));
|
||||
}
|
||||
Ok(IndexOfOptionalBounds(start_index, end_index))
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{action, SubCommand};
|
||||
use nu_plugin::test_helpers::value::string;
|
||||
use nu_protocol::{Primitive, UntaggedValue};
|
||||
use nu_source::Tag;
|
||||
|
||||
#[test]
|
||||
fn examples_work_as_expected() {
|
||||
use crate::examples::test as test_examples;
|
||||
|
||||
test_examples(SubCommand {})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_index_of_substring() {
|
||||
let word = string("Cargo.tomL");
|
||||
let pattern = ".tomL";
|
||||
let index_of_bounds =
|
||||
UntaggedValue::Primitive(Primitive::String("".to_string())).into_untagged_value();
|
||||
let expected = UntaggedValue::Primitive(Primitive::Int(5.into())).into_untagged_value();
|
||||
|
||||
let actual = action(&word, &pattern, &index_of_bounds, Tag::unknown()).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
#[test]
|
||||
fn index_of_does_not_exist_in_string() {
|
||||
let word = string("Cargo.tomL");
|
||||
let pattern = "Lm";
|
||||
let index_of_bounds =
|
||||
UntaggedValue::Primitive(Primitive::String("".to_string())).into_untagged_value();
|
||||
let expected = UntaggedValue::Primitive(Primitive::Int((-1).into())).into_untagged_value();
|
||||
|
||||
let actual = action(&word, &pattern, &index_of_bounds, Tag::unknown()).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_index_of_next_substring() {
|
||||
let word = string("Cargo.Cargo");
|
||||
let pattern = "Cargo";
|
||||
let index_of_bounds =
|
||||
UntaggedValue::Primitive(Primitive::String("1,".to_string())).into_untagged_value();
|
||||
let expected = UntaggedValue::Primitive(Primitive::Int(6.into())).into_untagged_value();
|
||||
|
||||
let actual = action(&word, &pattern, &index_of_bounds, Tag::unknown()).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
#[test]
|
||||
fn index_does_not_exist_due_to_end_index() {
|
||||
let word = string("Cargo.Banana");
|
||||
let pattern = "Banana";
|
||||
let index_of_bounds =
|
||||
UntaggedValue::Primitive(Primitive::String(",5".to_string())).into_untagged_value();
|
||||
let expected = UntaggedValue::Primitive(Primitive::Int((-1).into())).into_untagged_value();
|
||||
|
||||
let actual = action(&word, &pattern, &index_of_bounds, Tag::unknown()).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_index_of_nums_in_middle_due_to_index_limit_from_both_ends() {
|
||||
let word = string("123123123");
|
||||
let pattern = "123";
|
||||
let index_of_bounds =
|
||||
UntaggedValue::Primitive(Primitive::String("2,6".to_string())).into_untagged_value();
|
||||
let expected = UntaggedValue::Primitive(Primitive::Int(3.into())).into_untagged_value();
|
||||
|
||||
let actual = action(&word, &pattern, &index_of_bounds, Tag::unknown()).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn index_does_not_exists_due_to_strict_bounds() {
|
||||
let word = string("123456");
|
||||
let pattern = "1";
|
||||
let index_of_bounds =
|
||||
UntaggedValue::Primitive(Primitive::String("2,4".to_string())).into_untagged_value();
|
||||
let expected = UntaggedValue::Primitive(Primitive::Int((-1).into())).into_untagged_value();
|
||||
|
||||
let actual = action(&word, &pattern, &index_of_bounds, Tag::unknown()).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
@ -1,12 +1,16 @@
|
||||
mod capitalize;
|
||||
mod collect;
|
||||
mod command;
|
||||
mod contains;
|
||||
mod downcase;
|
||||
mod ends_with;
|
||||
mod find_replace;
|
||||
mod from;
|
||||
mod index_of;
|
||||
mod length;
|
||||
mod reverse;
|
||||
mod set;
|
||||
mod starts_with;
|
||||
mod substring;
|
||||
mod to_datetime;
|
||||
mod to_decimal;
|
||||
@ -17,15 +21,21 @@ mod upcase;
|
||||
pub use capitalize::SubCommand as StrCapitalize;
|
||||
pub use collect::SubCommand as StrCollect;
|
||||
pub use command::Command as Str;
|
||||
pub use contains::SubCommand as StrContains;
|
||||
pub use downcase::SubCommand as StrDowncase;
|
||||
pub use ends_with::SubCommand as StrEndsWith;
|
||||
pub use find_replace::SubCommand as StrFindReplace;
|
||||
pub use from::SubCommand as StrFrom;
|
||||
pub use index_of::SubCommand as StrIndexOf;
|
||||
pub use length::SubCommand as StrLength;
|
||||
pub use reverse::SubCommand as StrReverse;
|
||||
pub use set::SubCommand as StrSet;
|
||||
pub use starts_with::SubCommand as StrStartsWith;
|
||||
pub use substring::SubCommand as StrSubstring;
|
||||
pub use to_datetime::SubCommand as StrToDatetime;
|
||||
pub use to_decimal::SubCommand as StrToDecimal;
|
||||
pub use to_integer::SubCommand as StrToInteger;
|
||||
pub use trim::SubCommand as StrTrim;
|
||||
pub use trim::Trim as StrTrim;
|
||||
pub use trim::TrimLeft as StrTrimLeft;
|
||||
pub use trim::TrimRight as StrTrimRight;
|
||||
pub use upcase::SubCommand as StrUpcase;
|
||||
|
138
crates/nu-cli/src/commands/str_/starts_with.rs
Normal file
138
crates/nu-cli/src/commands/str_/starts_with.rs
Normal file
@ -0,0 +1,138 @@
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::ShellTypeName;
|
||||
use nu_protocol::{
|
||||
ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value,
|
||||
};
|
||||
use nu_source::{Tag, Tagged};
|
||||
use nu_value_ext::ValueExt;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Arguments {
|
||||
pattern: Tagged<String>,
|
||||
rest: Vec<ColumnPath>,
|
||||
}
|
||||
pub struct SubCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for SubCommand {
|
||||
fn name(&self) -> &str {
|
||||
"str starts-with"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("str starts-with")
|
||||
.required("pattern", SyntaxShape::String, "the pattern to match")
|
||||
.rest(
|
||||
SyntaxShape::ColumnPath,
|
||||
"optionally matches prefix of text by column paths",
|
||||
)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"checks if string starts with pattern"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
operate(args, registry).await
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "Checks if string starts with 'my' pattern",
|
||||
example: "echo 'my_library.rb' | str starts-with 'my'",
|
||||
result: Some(vec![UntaggedValue::boolean(true).into_untagged_value()]),
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
async fn operate(
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let registry = registry.clone();
|
||||
|
||||
let (Arguments { pattern, rest }, input) = args.process(®istry).await?;
|
||||
|
||||
let column_paths: Vec<_> = rest;
|
||||
|
||||
Ok(input
|
||||
.map(move |v| {
|
||||
if column_paths.is_empty() {
|
||||
ReturnSuccess::value(action(&v, &pattern, v.tag())?)
|
||||
} else {
|
||||
let mut ret = v;
|
||||
|
||||
for path in &column_paths {
|
||||
let pattern = pattern.clone();
|
||||
ret = ret.swap_data_by_column_path(
|
||||
path,
|
||||
Box::new(move |old| action(old, &pattern, old.tag())),
|
||||
)?;
|
||||
}
|
||||
|
||||
ReturnSuccess::value(ret)
|
||||
}
|
||||
})
|
||||
.to_output_stream())
|
||||
}
|
||||
|
||||
fn action(input: &Value, pattern: &str, tag: impl Into<Tag>) -> Result<Value, ShellError> {
|
||||
match &input.value {
|
||||
UntaggedValue::Primitive(Primitive::Line(s))
|
||||
| UntaggedValue::Primitive(Primitive::String(s)) => {
|
||||
let starts_with = s.starts_with(pattern);
|
||||
Ok(UntaggedValue::boolean(starts_with).into_value(tag))
|
||||
}
|
||||
other => {
|
||||
let got = format!("got {}", other.type_name());
|
||||
Err(ShellError::labeled_error(
|
||||
"value is not string",
|
||||
got,
|
||||
tag.into().span,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{action, SubCommand};
|
||||
use nu_plugin::test_helpers::value::string;
|
||||
use nu_protocol::{Primitive, UntaggedValue};
|
||||
use nu_source::Tag;
|
||||
|
||||
#[test]
|
||||
fn examples_work_as_expected() {
|
||||
use crate::examples::test as test_examples;
|
||||
|
||||
test_examples(SubCommand {})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn str_starts_with_pattern() {
|
||||
let word = string("Cargo.toml");
|
||||
let pattern = "Car";
|
||||
let expected =
|
||||
UntaggedValue::Primitive(Primitive::Boolean(true.into())).into_untagged_value();
|
||||
|
||||
let actual = action(&word, &pattern, Tag::unknown()).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn str_does_not_start_with_pattern() {
|
||||
let word = string("Cargo.toml");
|
||||
let pattern = ".toml";
|
||||
let expected =
|
||||
UntaggedValue::Primitive(Primitive::Boolean(false.into())).into_untagged_value();
|
||||
|
||||
let actual = action(&word, &pattern, Tag::unknown()).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ use nu_protocol::{
|
||||
use nu_source::{Tag, Tagged};
|
||||
use nu_value_ext::ValueExt;
|
||||
|
||||
use chrono::DateTime;
|
||||
use chrono::{DateTime, FixedOffset, LocalResult, Offset, TimeZone};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Arguments {
|
||||
@ -51,11 +51,23 @@ impl WholeStreamCommand for SubCommand {
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "Convert to datetime",
|
||||
example: "echo '16.11.1984 8:00 am +0000' | str to-datetime",
|
||||
result: None,
|
||||
}]
|
||||
vec![
|
||||
Example {
|
||||
description: "Convert to datetime",
|
||||
example: "echo '16.11.1984 8:00 am +0000' | str to-datetime",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Convert to datetime",
|
||||
example: "echo '2020-08-04T16:39:18+00:00' | str to-datetime",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Convert to datetime using a custom format",
|
||||
example: "echo '20200904_163918+0000' | str to-datetime -f '%Y%m%d_%H%M%S%z'",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,9 +85,9 @@ async fn operate(
|
||||
let column_paths: Vec<_> = rest;
|
||||
|
||||
let options = if let Some(Tagged { item: fmt, .. }) = format {
|
||||
DatetimeFormat(fmt)
|
||||
Some(DatetimeFormat(fmt))
|
||||
} else {
|
||||
DatetimeFormat(String::from("%d.%m.%Y %H:%M %P %z"))
|
||||
None
|
||||
};
|
||||
|
||||
Ok(input
|
||||
@ -102,23 +114,49 @@ async fn operate(
|
||||
|
||||
fn action(
|
||||
input: &Value,
|
||||
options: &DatetimeFormat,
|
||||
options: &Option<DatetimeFormat>,
|
||||
tag: impl Into<Tag>,
|
||||
) -> Result<Value, ShellError> {
|
||||
match &input.value {
|
||||
UntaggedValue::Primitive(Primitive::Line(s))
|
||||
| UntaggedValue::Primitive(Primitive::String(s)) => {
|
||||
let dt = &options.0;
|
||||
|
||||
let out = match DateTime::parse_from_str(s, dt) {
|
||||
Ok(d) => UntaggedValue::date(d),
|
||||
Err(reason) => {
|
||||
return Err(ShellError::labeled_error(
|
||||
"could not parse as datetime",
|
||||
reason.to_string(),
|
||||
tag.into().span,
|
||||
))
|
||||
}
|
||||
let out = match options {
|
||||
Some(dt) => match DateTime::parse_from_str(s, &dt.0) {
|
||||
Ok(d) => UntaggedValue::date(d),
|
||||
Err(reason) => {
|
||||
return Err(ShellError::labeled_error(
|
||||
format!("could not parse as datetime using format '{}'", dt.0),
|
||||
reason.to_string(),
|
||||
tag.into().span,
|
||||
))
|
||||
}
|
||||
},
|
||||
None => match dtparse::parse(s) {
|
||||
Ok((native_dt, fixed_offset)) => {
|
||||
let offset = match fixed_offset {
|
||||
Some(fo) => fo,
|
||||
None => FixedOffset::east(0).fix(),
|
||||
};
|
||||
match offset.from_local_datetime(&native_dt) {
|
||||
LocalResult::Single(d) => UntaggedValue::date(d),
|
||||
LocalResult::Ambiguous(d, _) => UntaggedValue::date(d),
|
||||
LocalResult::None => {
|
||||
return Err(ShellError::labeled_error(
|
||||
"could not convert to a timezone-aware datetime",
|
||||
"local time representation is invalid",
|
||||
tag.into().span,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(reason) => {
|
||||
return Err(ShellError::labeled_error(
|
||||
"could not parse as datetime",
|
||||
reason.to_string(),
|
||||
tag.into().span,
|
||||
))
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Ok(out.into_value(tag))
|
||||
@ -152,7 +190,7 @@ mod tests {
|
||||
fn takes_a_date_format() {
|
||||
let date_str = string("16.11.1984 8:00 am +0000");
|
||||
|
||||
let fmt_options = DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string());
|
||||
let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()));
|
||||
|
||||
let actual = action(&date_str, &fmt_options, Tag::unknown()).unwrap();
|
||||
|
||||
@ -162,11 +200,21 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn takes_iso8601_date_format() {
|
||||
let date_str = string("2020-08-04T16:39:18+00:00");
|
||||
let actual = action(&date_str, &None, Tag::unknown()).unwrap();
|
||||
match actual.value {
|
||||
UntaggedValue::Primitive(Primitive::Date(_)) => {}
|
||||
_ => panic!("Didn't convert to date"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn communicates_parsing_error_given_an_invalid_datetimelike_string() {
|
||||
let date_str = string("16.11.1984 8:00 am Oops0000");
|
||||
|
||||
let fmt_options = DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string());
|
||||
let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()));
|
||||
|
||||
let actual = action(&date_str, &fmt_options, Tag::unknown());
|
||||
|
||||
|
@ -1,168 +0,0 @@
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::ShellTypeName;
|
||||
use nu_protocol::{
|
||||
ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value,
|
||||
};
|
||||
use nu_source::{Tag, Tagged};
|
||||
use nu_value_ext::ValueExt;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Arguments {
|
||||
rest: Vec<ColumnPath>,
|
||||
#[serde(rename(deserialize = "char"))]
|
||||
char_: Option<Tagged<char>>,
|
||||
}
|
||||
|
||||
pub struct SubCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for SubCommand {
|
||||
fn name(&self) -> &str {
|
||||
"str trim"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("str trim")
|
||||
.rest(
|
||||
SyntaxShape::ColumnPath,
|
||||
"optionally trim text by column paths",
|
||||
)
|
||||
.named(
|
||||
"char",
|
||||
SyntaxShape::String,
|
||||
"character to trim (default: whitespace)",
|
||||
Some('c'),
|
||||
)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"trims text"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
operate(args, registry).await
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Trim whitespace",
|
||||
example: "echo 'Nu shell ' | str trim",
|
||||
result: Some(vec![Value::from("Nu shell")]),
|
||||
},
|
||||
Example {
|
||||
description: "Trim a specific character",
|
||||
example: "echo '=== Nu shell ===' | str trim -c '=' | str trim",
|
||||
result: Some(vec![Value::from("Nu shell")]),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
async fn operate(
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let registry = registry.clone();
|
||||
|
||||
let (Arguments { rest, char_ }, input) = args.process(®istry).await?;
|
||||
|
||||
let column_paths: Vec<_> = rest;
|
||||
let to_trim = char_.map(|tagged| tagged.item);
|
||||
|
||||
Ok(input
|
||||
.map(move |v| {
|
||||
if column_paths.is_empty() {
|
||||
ReturnSuccess::value(action(&v, v.tag(), to_trim)?)
|
||||
} else {
|
||||
let mut ret = v;
|
||||
|
||||
for path in &column_paths {
|
||||
ret = ret.swap_data_by_column_path(
|
||||
path,
|
||||
Box::new(move |old| action(old, old.tag(), to_trim)),
|
||||
)?;
|
||||
}
|
||||
|
||||
ReturnSuccess::value(ret)
|
||||
}
|
||||
})
|
||||
.to_output_stream())
|
||||
}
|
||||
|
||||
fn action(input: &Value, tag: impl Into<Tag>, char_: Option<char>) -> Result<Value, ShellError> {
|
||||
match &input.value {
|
||||
UntaggedValue::Primitive(Primitive::Line(s))
|
||||
| UntaggedValue::Primitive(Primitive::String(s)) => {
|
||||
Ok(UntaggedValue::string(match char_ {
|
||||
None => String::from(s.trim()),
|
||||
Some(ch) => trim_char(s, ch, true, true),
|
||||
})
|
||||
.into_value(tag))
|
||||
}
|
||||
other => {
|
||||
let got = format!("got {}", other.type_name());
|
||||
Err(ShellError::labeled_error(
|
||||
"value is not string",
|
||||
got,
|
||||
tag.into().span,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trim_char(from: &str, to_trim: char, leading: bool, trailing: bool) -> String {
|
||||
let mut trimmed = String::from("");
|
||||
let mut backlog = String::from("");
|
||||
let mut at_left = true;
|
||||
from.chars().for_each(|ch| match ch {
|
||||
c if c == to_trim => {
|
||||
if !(leading && at_left) {
|
||||
if trailing {
|
||||
backlog.push(c)
|
||||
} else {
|
||||
trimmed.push(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
other => {
|
||||
at_left = false;
|
||||
if trailing {
|
||||
trimmed.push_str(backlog.as_str());
|
||||
backlog = String::from("");
|
||||
}
|
||||
trimmed.push(other);
|
||||
}
|
||||
});
|
||||
|
||||
trimmed
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{action, SubCommand};
|
||||
use nu_plugin::test_helpers::value::string;
|
||||
use nu_source::Tag;
|
||||
|
||||
#[test]
|
||||
fn examples_work_as_expected() {
|
||||
use crate::examples::test as test_examples;
|
||||
|
||||
test_examples(SubCommand {})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trims() {
|
||||
let word = string("andres ");
|
||||
let expected = string("andres");
|
||||
|
||||
let actual = action(&word, Tag::unknown(), None).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
81
crates/nu-cli/src/commands/str_/trim/mod.rs
Normal file
81
crates/nu-cli/src/commands/str_/trim/mod.rs
Normal file
@ -0,0 +1,81 @@
|
||||
mod trim_both_ends;
|
||||
mod trim_left;
|
||||
mod trim_right;
|
||||
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::ShellTypeName;
|
||||
use nu_protocol::{ColumnPath, Primitive, ReturnSuccess, UntaggedValue, Value};
|
||||
use nu_source::{Tag, Tagged};
|
||||
use nu_value_ext::ValueExt;
|
||||
|
||||
pub use trim_both_ends::SubCommand as Trim;
|
||||
pub use trim_left::SubCommand as TrimLeft;
|
||||
pub use trim_right::SubCommand as TrimRight;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Arguments {
|
||||
rest: Vec<ColumnPath>,
|
||||
#[serde(rename(deserialize = "char"))]
|
||||
char_: Option<Tagged<char>>,
|
||||
}
|
||||
|
||||
pub async fn operate<F>(
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
trim_operation: &'static F,
|
||||
) -> Result<OutputStream, ShellError>
|
||||
where
|
||||
F: Fn(&str, Option<char>) -> String + Send + Sync + 'static,
|
||||
{
|
||||
let registry = registry.clone();
|
||||
|
||||
let (Arguments { rest, char_ }, input) = args.process(®istry).await?;
|
||||
|
||||
let column_paths: Vec<_> = rest;
|
||||
let to_trim = char_.map(|tagged| tagged.item);
|
||||
|
||||
Ok(input
|
||||
.map(move |v| {
|
||||
if column_paths.is_empty() {
|
||||
ReturnSuccess::value(action(&v, v.tag(), to_trim, &trim_operation)?)
|
||||
} else {
|
||||
let mut ret = v;
|
||||
|
||||
for path in &column_paths {
|
||||
ret = ret.swap_data_by_column_path(
|
||||
path,
|
||||
Box::new(move |old| action(old, old.tag(), to_trim, &trim_operation)),
|
||||
)?;
|
||||
}
|
||||
|
||||
ReturnSuccess::value(ret)
|
||||
}
|
||||
})
|
||||
.to_output_stream())
|
||||
}
|
||||
|
||||
pub fn action<F>(
|
||||
input: &Value,
|
||||
tag: impl Into<Tag>,
|
||||
char_: Option<char>,
|
||||
trim_operation: &F,
|
||||
) -> Result<Value, ShellError>
|
||||
where
|
||||
F: Fn(&str, Option<char>) -> String + Send + Sync + 'static,
|
||||
{
|
||||
match &input.value {
|
||||
UntaggedValue::Primitive(Primitive::Line(s))
|
||||
| UntaggedValue::Primitive(Primitive::String(s)) => {
|
||||
Ok(UntaggedValue::string(trim_operation(s, char_)).into_value(tag))
|
||||
}
|
||||
other => {
|
||||
let got = format!("got {}", other.type_name());
|
||||
Err(ShellError::labeled_error(
|
||||
"value is not string",
|
||||
got,
|
||||
tag.into().span,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
94
crates/nu-cli/src/commands/str_/trim/trim_both_ends.rs
Normal file
94
crates/nu-cli/src/commands/str_/trim/trim_both_ends.rs
Normal file
@ -0,0 +1,94 @@
|
||||
use super::operate;
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Signature, SyntaxShape, Value};
|
||||
|
||||
pub struct SubCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for SubCommand {
|
||||
fn name(&self) -> &str {
|
||||
"str trim"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("str trim")
|
||||
.rest(
|
||||
SyntaxShape::ColumnPath,
|
||||
"optionally trim text by column paths",
|
||||
)
|
||||
.named(
|
||||
"char",
|
||||
SyntaxShape::String,
|
||||
"character to trim (default: whitespace)",
|
||||
Some('c'),
|
||||
)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"trims text"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
operate(args, registry, &trim).await
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Trim whitespace",
|
||||
example: "echo 'Nu shell ' | str trim",
|
||||
result: Some(vec![Value::from("Nu shell")]),
|
||||
},
|
||||
Example {
|
||||
description: "Trim a specific character",
|
||||
example: "echo '=== Nu shell ===' | str trim -c '=' | str trim",
|
||||
result: Some(vec![Value::from("Nu shell")]),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
fn trim(s: &str, char_: Option<char>) -> String {
|
||||
match char_ {
|
||||
None => String::from(s.trim()),
|
||||
Some(ch) => String::from(s.trim_matches(ch)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{trim, SubCommand};
|
||||
use crate::commands::str_::trim::action;
|
||||
use nu_plugin::test_helpers::value::string;
|
||||
use nu_source::Tag;
|
||||
|
||||
#[test]
|
||||
fn examples_work_as_expected() {
|
||||
use crate::examples::test as test_examples;
|
||||
|
||||
test_examples(SubCommand {})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trims() {
|
||||
let word = string("andres ");
|
||||
let expected = string("andres");
|
||||
|
||||
let actual = action(&word, Tag::unknown(), None, &trim).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trims_custom_character_both_ends() {
|
||||
let word = string("!#andres#!");
|
||||
let expected = string("#andres#");
|
||||
|
||||
let actual = action(&word, Tag::unknown(), Some('!'), &trim).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
94
crates/nu-cli/src/commands/str_/trim/trim_left.rs
Normal file
94
crates/nu-cli/src/commands/str_/trim/trim_left.rs
Normal file
@ -0,0 +1,94 @@
|
||||
use super::operate;
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Signature, SyntaxShape, Value};
|
||||
|
||||
pub struct SubCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for SubCommand {
|
||||
fn name(&self) -> &str {
|
||||
"str ltrim"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("str ltrim")
|
||||
.rest(
|
||||
SyntaxShape::ColumnPath,
|
||||
"optionally trim text starting from the beginning by column paths",
|
||||
)
|
||||
.named(
|
||||
"char",
|
||||
SyntaxShape::String,
|
||||
"character to trim (default: whitespace)",
|
||||
Some('c'),
|
||||
)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"trims whitespace or character from the beginning of text"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
operate(args, registry, &trim_left).await
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Trim whitespace from the beginning of string",
|
||||
example: "echo ' Nu shell ' | str ltrim",
|
||||
result: Some(vec![Value::from("Nu shell ")]),
|
||||
},
|
||||
Example {
|
||||
description: "Trim a specific character",
|
||||
example: "echo '=== Nu shell ===' | str ltrim -c '='",
|
||||
result: Some(vec![Value::from(" Nu shell ===")]),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn trim_left(s: &str, char_: Option<char>) -> String {
|
||||
match char_ {
|
||||
None => String::from(s.trim_start()),
|
||||
Some(ch) => String::from(s.trim_start_matches(ch)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{trim_left, SubCommand};
|
||||
use crate::commands::str_::trim::action;
|
||||
use nu_plugin::test_helpers::value::string;
|
||||
use nu_source::Tag;
|
||||
|
||||
#[test]
|
||||
fn examples_work_as_expected() {
|
||||
use crate::examples::test as test_examples;
|
||||
|
||||
test_examples(SubCommand {})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trims_whitespace_from_left() {
|
||||
let word = string(" andres ");
|
||||
let expected = string("andres ");
|
||||
|
||||
let actual = action(&word, Tag::unknown(), None, &trim_left).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
#[test]
|
||||
fn trims_custom_chars_from_left() {
|
||||
let word = string("!!! andres !!!");
|
||||
let expected = string(" andres !!!");
|
||||
|
||||
let actual = action(&word, Tag::unknown(), Some('!'), &trim_left).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
95
crates/nu-cli/src/commands/str_/trim/trim_right.rs
Normal file
95
crates/nu-cli/src/commands/str_/trim/trim_right.rs
Normal file
@ -0,0 +1,95 @@
|
||||
use super::operate;
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Signature, SyntaxShape, Value};
|
||||
|
||||
pub struct SubCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for SubCommand {
|
||||
fn name(&self) -> &str {
|
||||
"str rtrim"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("str rtrim")
|
||||
.rest(
|
||||
SyntaxShape::ColumnPath,
|
||||
"optionally trim text starting from the end by column paths",
|
||||
)
|
||||
.named(
|
||||
"char",
|
||||
SyntaxShape::String,
|
||||
"character to trim (default: whitespace)",
|
||||
Some('c'),
|
||||
)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"trims whitespace or character from the end of text"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
operate(args, registry, &trim_right).await
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Trim whitespace from the end of string",
|
||||
example: "echo ' Nu shell ' | str rtrim",
|
||||
result: Some(vec![Value::from(" Nu shell")]),
|
||||
},
|
||||
Example {
|
||||
description: "Trim a specific character",
|
||||
example: "echo '=== Nu shell ===' | str rtrim -c '='",
|
||||
result: Some(vec![Value::from("=== Nu shell ")]),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn trim_right(s: &str, char_: Option<char>) -> String {
|
||||
match char_ {
|
||||
None => String::from(s.trim_end()),
|
||||
Some(ch) => String::from(s.trim_end_matches(ch)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{trim_right, SubCommand};
|
||||
use crate::commands::str_::trim::action;
|
||||
use nu_plugin::test_helpers::value::string;
|
||||
use nu_source::Tag;
|
||||
|
||||
#[test]
|
||||
fn examples_work_as_expected() {
|
||||
use crate::examples::test as test_examples;
|
||||
|
||||
test_examples(SubCommand {})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trims_whitespace_from_right() {
|
||||
let word = string(" andres ");
|
||||
let expected = string(" andres");
|
||||
|
||||
let actual = action(&word, Tag::unknown(), None, &trim_right).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trims_custom_chars_from_right() {
|
||||
let word = string("#@! andres !@#");
|
||||
let expected = string("#@! andres !@");
|
||||
|
||||
let actual = action(&word, Tag::unknown(), Some('#'), &trim_right).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
@ -74,13 +74,10 @@ pub fn from_list(values: &[Value], starting_idx: usize) -> nu_table::Table {
|
||||
_ => ansi_term::Color::Green,
|
||||
};
|
||||
|
||||
let header_bold = match config.get("header_bold") {
|
||||
Some(b) => match b.as_bool() {
|
||||
Ok(b) => b,
|
||||
_ => true,
|
||||
},
|
||||
_ => true,
|
||||
};
|
||||
let header_bold = config
|
||||
.get("header_bold")
|
||||
.map(|x| x.as_bool().unwrap_or(true))
|
||||
.unwrap_or(true);
|
||||
|
||||
TextStyle {
|
||||
alignment: header_align,
|
||||
|
@ -3,10 +3,82 @@ use crate::data::value::format_leaf;
|
||||
use crate::prelude::*;
|
||||
use futures::StreamExt;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Primitive, ReturnSuccess, Signature, UntaggedValue, Value};
|
||||
use nu_source::AnchorLocation;
|
||||
use nu_protocol::{Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value};
|
||||
use nu_source::{AnchorLocation, Tagged};
|
||||
use regex::Regex;
|
||||
use rust_embed::RustEmbed;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::io::Read;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct HtmlThemes {
|
||||
themes: Vec<HtmlTheme>,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct HtmlTheme {
|
||||
name: String,
|
||||
black: String,
|
||||
red: String,
|
||||
green: String,
|
||||
yellow: String,
|
||||
blue: String,
|
||||
purple: String,
|
||||
cyan: String,
|
||||
white: String,
|
||||
brightBlack: String,
|
||||
brightRed: String,
|
||||
brightGreen: String,
|
||||
brightYellow: String,
|
||||
brightBlue: String,
|
||||
brightPurple: String,
|
||||
brightCyan: String,
|
||||
brightWhite: String,
|
||||
background: String,
|
||||
foreground: String,
|
||||
}
|
||||
|
||||
impl Default for HtmlThemes {
|
||||
fn default() -> Self {
|
||||
HtmlThemes {
|
||||
themes: vec![HtmlTheme::default()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HtmlTheme {
|
||||
fn default() -> Self {
|
||||
HtmlTheme {
|
||||
name: "nu_default".to_string(),
|
||||
black: "black".to_string(),
|
||||
red: "red".to_string(),
|
||||
green: "green".to_string(),
|
||||
yellow: "#717100".to_string(),
|
||||
blue: "blue".to_string(),
|
||||
purple: "#c800c8".to_string(),
|
||||
cyan: "#037979".to_string(),
|
||||
white: "white".to_string(),
|
||||
brightBlack: "black".to_string(),
|
||||
brightRed: "red".to_string(),
|
||||
brightGreen: "green".to_string(),
|
||||
brightYellow: "#717100".to_string(),
|
||||
brightBlue: "blue".to_string(),
|
||||
brightPurple: "#c800c8".to_string(),
|
||||
brightCyan: "#037979".to_string(),
|
||||
brightWhite: "white".to_string(),
|
||||
background: "white".to_string(),
|
||||
foreground: "black".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "assets/"]
|
||||
struct Assets;
|
||||
|
||||
pub struct ToHTML;
|
||||
|
||||
@ -14,8 +86,10 @@ pub struct ToHTML;
|
||||
pub struct ToHTMLArgs {
|
||||
html_color: bool,
|
||||
no_color: bool,
|
||||
dark_bg: bool,
|
||||
use_campbell: bool,
|
||||
dark: bool,
|
||||
partial: bool,
|
||||
theme: Option<Tagged<String>>,
|
||||
list: bool,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@ -26,18 +100,25 @@ impl WholeStreamCommand for ToHTML {
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("to html")
|
||||
.switch("html_color", "change ansi colors to html colors", Some('t'))
|
||||
.switch("html_color", "change ansi colors to html colors", Some('c'))
|
||||
.switch("no_color", "remove all ansi colors in output", Some('n'))
|
||||
.switch(
|
||||
"dark_bg",
|
||||
"dark",
|
||||
"indicate your background color is a darker color",
|
||||
Some('d'),
|
||||
)
|
||||
.switch(
|
||||
"use_campbell",
|
||||
"use microsoft's windows terminal color scheme named campell",
|
||||
Some('c'),
|
||||
"partial",
|
||||
"only output the html for the content itself",
|
||||
Some('p'),
|
||||
)
|
||||
.named(
|
||||
"theme",
|
||||
SyntaxShape::String,
|
||||
"the name of the theme to use (github, blulocolight, ...)",
|
||||
Some('t'),
|
||||
)
|
||||
.switch("list", "list the names of all available themes", Some('l'))
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
@ -53,121 +134,133 @@ impl WholeStreamCommand for ToHTML {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_campbell_theme(is_dark: bool) -> HashMap<&'static str, String> {
|
||||
// for reference here is Microsoft's Campbell Theme
|
||||
// taken from here
|
||||
// https://docs.microsoft.com/en-us/windows/terminal/customize-settings/color-schemes
|
||||
fn get_theme_from_asset_file(
|
||||
is_dark: bool,
|
||||
theme: &Option<Tagged<String>>,
|
||||
theme_tag: &Tag,
|
||||
) -> Result<HashMap<&'static str, String>, ShellError> {
|
||||
let theme_name = match theme {
|
||||
Some(s) => s.to_string(),
|
||||
None => "default".to_string(), // There is no theme named "default" so this will be HtmlTheme::default(), which is "nu_default".
|
||||
};
|
||||
|
||||
// 228 themes come from
|
||||
// https://github.com/mbadolato/iTerm2-Color-Schemes/tree/master/windowsterminal
|
||||
// we should find a hit on any name in there
|
||||
let asset = get_asset_by_name_as_html_themes("228_themes.zip", "228_themes.json");
|
||||
|
||||
// If asset doesn't work, make sure to return the default theme
|
||||
let asset = match asset {
|
||||
Ok(a) => a,
|
||||
_ => HtmlThemes::default(),
|
||||
};
|
||||
|
||||
// Find the theme by theme name
|
||||
let th = asset
|
||||
.themes
|
||||
.iter()
|
||||
.find(|&n| n.name.to_lowercase() == *theme_name.to_lowercase().as_str()); // case insensitive search
|
||||
|
||||
// If no theme is found by the name provided, ensure we return the default theme
|
||||
let default_theme = HtmlTheme::default();
|
||||
let th = match th {
|
||||
Some(t) => t,
|
||||
None => &default_theme,
|
||||
};
|
||||
|
||||
// this just means no theme was passed in
|
||||
if th.name.to_lowercase().eq(&"nu_default".to_string())
|
||||
// this means there was a theme passed in
|
||||
&& theme.is_some()
|
||||
{
|
||||
return Err(ShellError::labeled_error(
|
||||
"Error finding theme name",
|
||||
"Error finding theme name",
|
||||
theme_tag.span,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(convert_html_theme_to_hash_map(is_dark, th))
|
||||
}
|
||||
|
||||
fn get_asset_by_name_as_html_themes(
|
||||
zip_name: &str,
|
||||
json_name: &str,
|
||||
) -> Result<HtmlThemes, Box<dyn Error>> {
|
||||
match Assets::get(zip_name) {
|
||||
Some(content) => {
|
||||
let asset: Vec<u8> = match content {
|
||||
Cow::Borrowed(bytes) => bytes.into(),
|
||||
Cow::Owned(bytes) => bytes,
|
||||
};
|
||||
let reader = std::io::Cursor::new(asset);
|
||||
let mut archive = zip::ZipArchive::new(reader)?;
|
||||
let mut zip_file = archive.by_name(json_name)?;
|
||||
let mut contents = String::new();
|
||||
zip_file.read_to_string(&mut contents)?;
|
||||
Ok(serde_json::from_str(&contents)?)
|
||||
}
|
||||
None => {
|
||||
let th = HtmlThemes::default();
|
||||
Ok(th)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_html_theme_to_hash_map(
|
||||
is_dark: bool,
|
||||
theme: &HtmlTheme,
|
||||
) -> HashMap<&'static str, String> {
|
||||
let mut hm: HashMap<&str, String> = HashMap::new();
|
||||
|
||||
hm.insert("bold_black", theme.brightBlack[..].to_string());
|
||||
hm.insert("bold_red", theme.brightRed[..].to_string());
|
||||
hm.insert("bold_green", theme.brightGreen[..].to_string());
|
||||
hm.insert("bold_yellow", theme.brightYellow[..].to_string());
|
||||
hm.insert("bold_blue", theme.brightBlue[..].to_string());
|
||||
hm.insert("bold_magenta", theme.brightPurple[..].to_string());
|
||||
hm.insert("bold_cyan", theme.brightCyan[..].to_string());
|
||||
hm.insert("bold_white", theme.brightWhite[..].to_string());
|
||||
|
||||
hm.insert("black", theme.black[..].to_string());
|
||||
hm.insert("red", theme.red[..].to_string());
|
||||
hm.insert("green", theme.green[..].to_string());
|
||||
hm.insert("yellow", theme.yellow[..].to_string());
|
||||
hm.insert("blue", theme.blue[..].to_string());
|
||||
hm.insert("magenta", theme.purple[..].to_string());
|
||||
hm.insert("cyan", theme.cyan[..].to_string());
|
||||
hm.insert("white", theme.white[..].to_string());
|
||||
|
||||
// Try to make theme work with light or dark but
|
||||
// flipping the foreground and background but leave
|
||||
// the other colors the same.
|
||||
if is_dark {
|
||||
hm.insert("bold_black", "#767676".to_string());
|
||||
hm.insert("bold_red", "#E74856".to_string());
|
||||
hm.insert("bold_green", "#16C60C".to_string());
|
||||
hm.insert("bold_yellow", "#F9F1A5".to_string());
|
||||
hm.insert("bold_blue", "#3B78FF".to_string());
|
||||
hm.insert("bold_magenta", "#B4009E".to_string());
|
||||
hm.insert("bold_cyan", "#61D6D6".to_string());
|
||||
hm.insert("bold_white", "#F2F2F2".to_string());
|
||||
|
||||
hm.insert("black", "#0C0C0C".to_string());
|
||||
hm.insert("red", "#C50F1F".to_string());
|
||||
hm.insert("green", "#13A10E".to_string());
|
||||
hm.insert("yellow", "#C19C00".to_string());
|
||||
hm.insert("blue", "#0037DA".to_string());
|
||||
hm.insert("magenta", "#881798".to_string());
|
||||
hm.insert("cyan", "#3A96DD".to_string());
|
||||
hm.insert("white", "#CCCCCC".to_string());
|
||||
|
||||
hm.insert("background", "#0C0C0C".to_string());
|
||||
hm.insert("foreground", "#CCCCCC".to_string());
|
||||
hm.insert("background", theme.black[..].to_string());
|
||||
hm.insert("foreground", theme.white[..].to_string());
|
||||
} else {
|
||||
hm.insert("bold_black", "#767676".to_string());
|
||||
hm.insert("bold_red", "#E74856".to_string());
|
||||
hm.insert("bold_green", "#16C60C".to_string());
|
||||
hm.insert("bold_yellow", "#F9F1A5".to_string());
|
||||
hm.insert("bold_blue", "#3B78FF".to_string());
|
||||
hm.insert("bold_magenta", "#B4009E".to_string());
|
||||
hm.insert("bold_cyan", "#61D6D6".to_string());
|
||||
hm.insert("bold_white", "#F2F2F2".to_string());
|
||||
|
||||
hm.insert("black", "#0C0C0C".to_string());
|
||||
hm.insert("red", "#C50F1F".to_string());
|
||||
hm.insert("green", "#13A10E".to_string());
|
||||
hm.insert("yellow", "#C19C00".to_string());
|
||||
hm.insert("blue", "#0037DA".to_string());
|
||||
hm.insert("magenta", "#881798".to_string());
|
||||
hm.insert("cyan", "#3A96DD".to_string());
|
||||
hm.insert("white", "#CCCCCC".to_string());
|
||||
|
||||
hm.insert("background", "#CCCCCC".to_string());
|
||||
hm.insert("foreground", "#0C0C0C".to_string());
|
||||
hm.insert("background", theme.white[..].to_string());
|
||||
hm.insert("foreground", theme.black[..].to_string());
|
||||
}
|
||||
|
||||
hm
|
||||
}
|
||||
|
||||
fn get_default_theme(is_dark: bool) -> HashMap<&'static str, String> {
|
||||
let mut hm: HashMap<&str, String> = HashMap::new();
|
||||
fn get_list_of_theme_names() -> Vec<String> {
|
||||
let asset = get_asset_by_name_as_html_themes("228_themes.zip", "228_themes.json");
|
||||
|
||||
if is_dark {
|
||||
hm.insert("bold_black", "black".to_string());
|
||||
hm.insert("bold_red", "red".to_string());
|
||||
hm.insert("bold_green", "green".to_string());
|
||||
hm.insert("bold_yellow", "yellow".to_string());
|
||||
hm.insert("bold_blue", "blue".to_string());
|
||||
hm.insert("bold_magenta", "magenta".to_string());
|
||||
hm.insert("bold_cyan", "cyan".to_string());
|
||||
hm.insert("bold_white", "white".to_string());
|
||||
// If asset doesn't work, make sure to return the default theme
|
||||
let html_themes = match asset {
|
||||
Ok(a) => a,
|
||||
_ => HtmlThemes::default(),
|
||||
};
|
||||
|
||||
hm.insert("black", "black".to_string());
|
||||
hm.insert("red", "red".to_string());
|
||||
hm.insert("green", "green".to_string());
|
||||
hm.insert("yellow", "yellow".to_string());
|
||||
hm.insert("blue", "blue".to_string());
|
||||
hm.insert("magenta", "magenta".to_string());
|
||||
hm.insert("cyan", "cyan".to_string());
|
||||
hm.insert("white", "white".to_string());
|
||||
let theme_names: Vec<String> = html_themes
|
||||
.themes
|
||||
.iter()
|
||||
.map(|n| n.name[..].to_string())
|
||||
.collect();
|
||||
|
||||
hm.insert("background", "black".to_string());
|
||||
hm.insert("foreground", "white".to_string());
|
||||
} else {
|
||||
hm.insert("bold_black", "black".to_string());
|
||||
hm.insert("bold_red", "red".to_string());
|
||||
hm.insert("bold_green", "green".to_string());
|
||||
hm.insert("bold_yellow", "#717100".to_string());
|
||||
hm.insert("bold_blue", "blue".to_string());
|
||||
hm.insert("bold_magenta", "#c800c8".to_string());
|
||||
hm.insert("bold_cyan", "#037979".to_string());
|
||||
hm.insert("bold_white", "white".to_string());
|
||||
|
||||
hm.insert("black", "black".to_string());
|
||||
hm.insert("red", "red".to_string());
|
||||
hm.insert("green", "green".to_string());
|
||||
hm.insert("yellow", "#717100".to_string());
|
||||
hm.insert("blue", "blue".to_string());
|
||||
hm.insert("magenta", "#c800c8".to_string());
|
||||
hm.insert("cyan", "#037979".to_string());
|
||||
hm.insert("white", "white".to_string());
|
||||
|
||||
hm.insert("background", "white".to_string());
|
||||
hm.insert("foreground", "black".to_string());
|
||||
}
|
||||
|
||||
hm
|
||||
}
|
||||
|
||||
fn get_colors(is_dark: bool, use_campbell: bool) -> HashMap<&'static str, String> {
|
||||
// Currently now using bold_white and bold_black.
|
||||
// This is not theming but it is kind of a start. The intent here is to use the
|
||||
// regular terminal colors which appear on black for most people and are very
|
||||
// high contrast. But when there's a light background, use something that works
|
||||
// better for it.
|
||||
|
||||
if use_campbell {
|
||||
get_campbell_theme(is_dark)
|
||||
} else {
|
||||
get_default_theme(is_dark)
|
||||
}
|
||||
theme_names
|
||||
}
|
||||
|
||||
async fn to_html(
|
||||
@ -180,160 +273,232 @@ async fn to_html(
|
||||
ToHTMLArgs {
|
||||
html_color,
|
||||
no_color,
|
||||
dark_bg,
|
||||
use_campbell,
|
||||
dark,
|
||||
partial,
|
||||
theme,
|
||||
list,
|
||||
},
|
||||
input,
|
||||
) = args.process(®istry).await?;
|
||||
let input: Vec<Value> = input.collect().await;
|
||||
let headers = nu_protocol::merge_descriptors(&input);
|
||||
let mut output_string = "<html>".to_string();
|
||||
let headers = Some(headers)
|
||||
.filter(|headers| !headers.is_empty() && (headers.len() > 1 || headers[0] != ""));
|
||||
let mut output_string = String::new();
|
||||
let mut regex_hm: HashMap<u32, (&str, String)> = HashMap::new();
|
||||
let color_hm = get_colors(dark_bg, use_campbell);
|
||||
|
||||
// change the color of the page
|
||||
output_string.push_str(&format!(
|
||||
r"<style>body {{ background-color:{};color:{}; }}</style><body>",
|
||||
color_hm
|
||||
.get("background")
|
||||
.expect("Error getting background color"),
|
||||
color_hm
|
||||
.get("foreground")
|
||||
.expect("Error getting foreground color")
|
||||
));
|
||||
if list {
|
||||
// Get the list of theme names
|
||||
let theme_names = get_list_of_theme_names();
|
||||
|
||||
// Put that list into the output string
|
||||
for s in theme_names.iter() {
|
||||
output_string.push_str(&format!("{}\n", s));
|
||||
}
|
||||
|
||||
output_string.push_str("\nScreenshots of themes can be found here:\n");
|
||||
output_string.push_str("https://github.com/mbadolato/iTerm2-Color-Schemes\n");
|
||||
|
||||
// Short circuit and return the output_string
|
||||
Ok(OutputStream::one(ReturnSuccess::value(
|
||||
UntaggedValue::string(output_string).into_value(name_tag),
|
||||
)))
|
||||
} else {
|
||||
let theme_tag = match &theme {
|
||||
Some(v) => &v.tag,
|
||||
None => &name_tag,
|
||||
};
|
||||
|
||||
let color_hm = get_theme_from_asset_file(dark, &theme, &theme_tag);
|
||||
let color_hm = match color_hm {
|
||||
Ok(c) => c,
|
||||
_ => {
|
||||
return Err(ShellError::labeled_error(
|
||||
"Error finding theme name",
|
||||
"Error finding theme name",
|
||||
theme_tag.span,
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// change the color of the page
|
||||
if !partial {
|
||||
output_string.push_str(&format!(
|
||||
r"<html><style>body {{ background-color:{};color:{}; }}</style><body>",
|
||||
color_hm
|
||||
.get("background")
|
||||
.expect("Error getting background color"),
|
||||
color_hm
|
||||
.get("foreground")
|
||||
.expect("Error getting foreground color")
|
||||
));
|
||||
} else {
|
||||
output_string.push_str(&format!(
|
||||
"<div style=\"background-color:{};color:{};\">",
|
||||
color_hm
|
||||
.get("background")
|
||||
.expect("Error getting background color"),
|
||||
color_hm
|
||||
.get("foreground")
|
||||
.expect("Error getting foreground color")
|
||||
));
|
||||
}
|
||||
|
||||
let inner_value = match input.len() {
|
||||
0 => String::default(),
|
||||
1 => match headers {
|
||||
Some(headers) => html_table(input, headers),
|
||||
None => {
|
||||
let value = &input[0];
|
||||
html_value(value)
|
||||
}
|
||||
},
|
||||
_ => match headers {
|
||||
Some(headers) => html_table(input, headers),
|
||||
None => html_list(input),
|
||||
},
|
||||
};
|
||||
|
||||
output_string.push_str(&inner_value);
|
||||
|
||||
if !partial {
|
||||
output_string.push_str("</body></html>");
|
||||
} else {
|
||||
output_string.push_str("</div>")
|
||||
}
|
||||
|
||||
// Check to see if we want to remove all color or change ansi to html colors
|
||||
if html_color {
|
||||
setup_html_color_regexes(&mut regex_hm, &color_hm);
|
||||
output_string = run_regexes(®ex_hm, &output_string);
|
||||
} else if no_color {
|
||||
setup_no_color_regexes(&mut regex_hm);
|
||||
output_string = run_regexes(®ex_hm, &output_string);
|
||||
}
|
||||
|
||||
Ok(OutputStream::one(ReturnSuccess::value(
|
||||
UntaggedValue::string(output_string).into_value(name_tag),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn html_list(list: Vec<Value>) -> String {
|
||||
let mut output_string = String::new();
|
||||
output_string.push_str("<ol>");
|
||||
for value in list {
|
||||
output_string.push_str("<li>");
|
||||
output_string.push_str(&html_value(&value));
|
||||
output_string.push_str("</li>");
|
||||
}
|
||||
output_string.push_str("</ol>");
|
||||
output_string
|
||||
}
|
||||
|
||||
fn html_table(table: Vec<Value>, headers: Vec<String>) -> String {
|
||||
let mut output_string = String::new();
|
||||
// Add grid lines to html
|
||||
// let mut output_string = "<html><head><style>".to_string();
|
||||
// output_string.push_str("table, th, td { border: 2px solid black; border-collapse: collapse; padding: 10px; }");
|
||||
// output_string.push_str("</style></head><body>");
|
||||
|
||||
if !headers.is_empty() && (headers.len() > 1 || headers[0] != "") {
|
||||
// output_string.push_str("<table>");
|
||||
output_string.push_str("<table>");
|
||||
|
||||
// change the color of tables
|
||||
output_string.push_str(&format!(
|
||||
r"<table style='background-color:{};color:{};'>",
|
||||
color_hm
|
||||
.get("background")
|
||||
.expect("Error getting background color"),
|
||||
color_hm
|
||||
.get("foreground")
|
||||
.expect("Error getting foreground color")
|
||||
));
|
||||
|
||||
output_string.push_str("<tr>");
|
||||
|
||||
for header in &headers {
|
||||
output_string.push_str("<th>");
|
||||
output_string.push_str(&htmlescape::encode_minimal(&header));
|
||||
output_string.push_str("</th>");
|
||||
}
|
||||
output_string.push_str("</tr>");
|
||||
output_string.push_str("<tr>");
|
||||
for header in &headers {
|
||||
output_string.push_str("<th>");
|
||||
output_string.push_str(&htmlescape::encode_minimal(&header));
|
||||
output_string.push_str("</th>");
|
||||
}
|
||||
output_string.push_str("</tr>");
|
||||
|
||||
for row in input {
|
||||
match row.value {
|
||||
UntaggedValue::Primitive(Primitive::Binary(b)) => {
|
||||
// This might be a bit much, but it's fun :)
|
||||
match row.tag.anchor {
|
||||
Some(AnchorLocation::Url(f)) | Some(AnchorLocation::File(f)) => {
|
||||
let extension = f.split('.').last().map(String::from);
|
||||
match extension {
|
||||
Some(s)
|
||||
if ["png", "jpg", "bmp", "gif", "tiff", "jpeg"]
|
||||
.contains(&s.to_lowercase().as_str()) =>
|
||||
{
|
||||
output_string.push_str("<img src=\"data:image/");
|
||||
output_string.push_str(&s);
|
||||
output_string.push_str(";base64,");
|
||||
output_string.push_str(&base64::encode(&b));
|
||||
output_string.push_str("\">");
|
||||
}
|
||||
_ => {
|
||||
let output = pretty_hex::pretty_hex(&b);
|
||||
|
||||
output_string.push_str("<pre>");
|
||||
output_string.push_str(&output);
|
||||
output_string.push_str("</pre>");
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let output = pretty_hex::pretty_hex(&b);
|
||||
|
||||
output_string.push_str("<pre>");
|
||||
output_string.push_str(&output);
|
||||
output_string.push_str("</pre>");
|
||||
}
|
||||
}
|
||||
}
|
||||
UntaggedValue::Primitive(Primitive::String(ref b)) => {
|
||||
// This might be a bit much, but it's fun :)
|
||||
match row.tag.anchor {
|
||||
Some(AnchorLocation::Url(f)) | Some(AnchorLocation::File(f)) => {
|
||||
let extension = f.split('.').last().map(String::from);
|
||||
match extension {
|
||||
Some(s) if s.to_lowercase() == "svg" => {
|
||||
output_string.push_str("<img src=\"data:image/svg+xml;base64,");
|
||||
output_string.push_str(&base64::encode(&b.as_bytes()));
|
||||
output_string.push_str("\">");
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
output_string.push_str(
|
||||
&(htmlescape::encode_minimal(&format_leaf(&row.value).plain_string(100_000))
|
||||
.replace("\n", "<br>")),
|
||||
);
|
||||
}
|
||||
UntaggedValue::Row(row) => {
|
||||
output_string.push_str("<tr>");
|
||||
for header in &headers {
|
||||
let data = row.get_data(header);
|
||||
output_string.push_str("<td>");
|
||||
output_string.push_str(&format_leaf(data.borrow()).plain_string(100_000));
|
||||
output_string.push_str("</td>");
|
||||
}
|
||||
output_string.push_str("</tr>");
|
||||
}
|
||||
p => {
|
||||
output_string.push_str(
|
||||
&(htmlescape::encode_minimal(&format_leaf(&p).plain_string(100_000))
|
||||
.replace("\n", "<br>")),
|
||||
);
|
||||
for row in table {
|
||||
if let UntaggedValue::Row(row) = row.value {
|
||||
output_string.push_str("<tr>");
|
||||
for header in &headers {
|
||||
let data = row.get_data(header);
|
||||
output_string.push_str("<td>");
|
||||
output_string.push_str(&html_value(data.borrow()));
|
||||
output_string.push_str("</td>");
|
||||
}
|
||||
output_string.push_str("</tr>");
|
||||
}
|
||||
}
|
||||
output_string.push_str("</table>");
|
||||
|
||||
if !headers.is_empty() && (headers.len() > 1 || headers[0] != "") {
|
||||
output_string.push_str("</table>");
|
||||
output_string
|
||||
}
|
||||
|
||||
fn html_value(value: &Value) -> String {
|
||||
let mut output_string = String::new();
|
||||
match &value.value {
|
||||
UntaggedValue::Primitive(Primitive::Binary(b)) => {
|
||||
// This might be a bit much, but it's fun :)
|
||||
match &value.tag.anchor {
|
||||
Some(AnchorLocation::Url(f)) | Some(AnchorLocation::File(f)) => {
|
||||
let extension = f.split('.').last().map(String::from);
|
||||
match extension {
|
||||
Some(s)
|
||||
if ["png", "jpg", "bmp", "gif", "tiff", "jpeg"]
|
||||
.contains(&s.to_lowercase().as_str()) =>
|
||||
{
|
||||
output_string.push_str("<img src=\"data:image/");
|
||||
output_string.push_str(&s);
|
||||
output_string.push_str(";base64,");
|
||||
output_string.push_str(&base64::encode(&b));
|
||||
output_string.push_str("\">");
|
||||
}
|
||||
_ => {
|
||||
let output = pretty_hex::pretty_hex(&b);
|
||||
|
||||
output_string.push_str("<pre>");
|
||||
output_string.push_str(&output);
|
||||
output_string.push_str("</pre>");
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let output = pretty_hex::pretty_hex(&b);
|
||||
|
||||
output_string.push_str("<pre>");
|
||||
output_string.push_str(&output);
|
||||
output_string.push_str("</pre>");
|
||||
}
|
||||
}
|
||||
}
|
||||
UntaggedValue::Primitive(Primitive::String(ref b)) => {
|
||||
// This might be a bit much, but it's fun :)
|
||||
match &value.tag.anchor {
|
||||
Some(AnchorLocation::Url(f)) | Some(AnchorLocation::File(f)) => {
|
||||
let extension = f.split('.').last().map(String::from);
|
||||
match extension {
|
||||
Some(s) if s.to_lowercase() == "svg" => {
|
||||
output_string.push_str("<img src=\"data:image/svg+xml;base64,");
|
||||
output_string.push_str(&base64::encode(&b.as_bytes()));
|
||||
output_string.push_str("\">");
|
||||
return output_string;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
output_string.push_str(
|
||||
&htmlescape::encode_minimal(&format_leaf(&value.value).plain_string(100_000))
|
||||
.replace("\n", "<br>"),
|
||||
);
|
||||
}
|
||||
other => output_string.push_str(
|
||||
&htmlescape::encode_minimal(&format_leaf(other).plain_string(100_000))
|
||||
.replace("\n", "<br>"),
|
||||
),
|
||||
}
|
||||
output_string.push_str("</body></html>");
|
||||
|
||||
// Check to see if we want to remove all color or change ansi to html colors
|
||||
if html_color {
|
||||
setup_html_color_regexes(&mut regex_hm, dark_bg, use_campbell);
|
||||
output_string = run_regexes(®ex_hm, &output_string);
|
||||
} else if no_color {
|
||||
setup_no_color_regexes(&mut regex_hm);
|
||||
output_string = run_regexes(®ex_hm, &output_string);
|
||||
}
|
||||
|
||||
Ok(OutputStream::one(ReturnSuccess::value(
|
||||
UntaggedValue::string(output_string).into_value(name_tag),
|
||||
)))
|
||||
output_string
|
||||
}
|
||||
|
||||
fn setup_html_color_regexes(
|
||||
hash: &mut HashMap<u32, (&'static str, String)>,
|
||||
is_dark: bool,
|
||||
use_campbell: bool,
|
||||
color_hm: &HashMap<&str, String>,
|
||||
) {
|
||||
let color_hm = get_colors(is_dark, use_campbell);
|
||||
|
||||
// All the bold colors
|
||||
hash.insert(
|
||||
0,
|
||||
|
205
crates/nu-cli/src/commands/to_xml.rs
Normal file
205
crates/nu-cli/src/commands/to_xml.rs
Normal file
@ -0,0 +1,205 @@
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use indexmap::IndexMap;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value};
|
||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
|
||||
use std::collections::HashSet;
|
||||
use std::io::Cursor;
|
||||
use std::io::Write;
|
||||
use std::iter::FromIterator;
|
||||
|
||||
pub struct ToXML;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ToXMLArgs {
|
||||
pretty: Option<Value>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for ToXML {
|
||||
fn name(&self) -> &str {
|
||||
"to xml"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("to xml").named(
|
||||
"pretty",
|
||||
SyntaxShape::Int,
|
||||
"Formats the XML text with the provided indentation setting",
|
||||
Some('p'),
|
||||
)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Convert table into .xml text"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
to_xml(args, registry).await
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_attributes<'a>(
|
||||
element: &mut quick_xml::events::BytesStart<'a>,
|
||||
attributes: &'a IndexMap<String, String>,
|
||||
) {
|
||||
for (k, v) in attributes.iter() {
|
||||
element.push_attribute((k.as_str(), v.as_str()));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_attributes(row: &Value) -> Option<IndexMap<String, String>> {
|
||||
if let UntaggedValue::Row(r) = &row.value {
|
||||
if let Some(v) = r.entries.get("attributes") {
|
||||
if let UntaggedValue::Row(a) = &v.value {
|
||||
let mut h = IndexMap::new();
|
||||
for (k, v) in a.entries.iter() {
|
||||
h.insert(k.clone(), v.convert_to_string());
|
||||
}
|
||||
return Some(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_children(row: &Value) -> Option<&Vec<Value>> {
|
||||
if let UntaggedValue::Row(r) = &row.value {
|
||||
if let Some(v) = r.entries.get("children") {
|
||||
if let UntaggedValue::Table(t) = &v.value {
|
||||
return Some(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn is_xml_row(row: &Value) -> bool {
|
||||
if let UntaggedValue::Row(r) = &row.value {
|
||||
let keys: HashSet<&String> = HashSet::from_iter(r.keys());
|
||||
let children: String = "children".to_string();
|
||||
let attributes: String = "attributes".to_string();
|
||||
return keys.contains(&children) && keys.contains(&attributes) && keys.len() == 2;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn write_xml_events<W: Write>(
|
||||
current: &Value,
|
||||
writer: &mut quick_xml::Writer<W>,
|
||||
) -> Result<(), ShellError> {
|
||||
match ¤t.value {
|
||||
UntaggedValue::Row(o) => {
|
||||
for (k, v) in o.entries.iter() {
|
||||
let mut e = BytesStart::owned(k.as_bytes(), k.len());
|
||||
if !is_xml_row(v) {
|
||||
return Err(ShellError::labeled_error(
|
||||
"Expected a row with 'children' and 'attributes' columns",
|
||||
"missing 'children' and 'attributes' columns ",
|
||||
¤t.tag,
|
||||
));
|
||||
}
|
||||
let a = get_attributes(v);
|
||||
if let Some(ref a) = a {
|
||||
add_attributes(&mut e, a);
|
||||
}
|
||||
writer
|
||||
.write_event(Event::Start(e))
|
||||
.expect("Couldn't open XML node");
|
||||
let c = get_children(v);
|
||||
if let Some(c) = c {
|
||||
for v in c {
|
||||
write_xml_events(v, writer)?;
|
||||
}
|
||||
}
|
||||
writer
|
||||
.write_event(Event::End(BytesEnd::borrowed(k.as_bytes())))
|
||||
.expect("Couldn't close XML node");
|
||||
}
|
||||
}
|
||||
UntaggedValue::Table(t) => {
|
||||
for v in t {
|
||||
write_xml_events(v, writer)?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let s = current.convert_to_string();
|
||||
writer
|
||||
.write_event(Event::Text(BytesText::from_plain_str(s.as_str())))
|
||||
.expect("Couldn't write XML text");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn to_xml(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream, ShellError> {
|
||||
let registry = registry.clone();
|
||||
let name_tag = args.call_info.name_tag.clone();
|
||||
let name_span = name_tag.span;
|
||||
let (ToXMLArgs { pretty }, input) = args.process(®istry).await?;
|
||||
let input: Vec<Value> = input.collect().await;
|
||||
|
||||
let to_process_input = match input.len() {
|
||||
x if x > 1 => {
|
||||
let tag = input[0].tag.clone();
|
||||
vec![Value {
|
||||
value: UntaggedValue::Table(input),
|
||||
tag,
|
||||
}]
|
||||
}
|
||||
1 => input,
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
Ok(
|
||||
futures::stream::iter(to_process_input.into_iter().map(move |value| {
|
||||
let mut w = pretty.as_ref().map_or_else(
|
||||
|| quick_xml::Writer::new(Cursor::new(Vec::new())),
|
||||
|p| {
|
||||
quick_xml::Writer::new_with_indent(
|
||||
Cursor::new(Vec::new()),
|
||||
b' ',
|
||||
p.value.expect_int() as usize,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let value_span = value.tag.span;
|
||||
|
||||
match write_xml_events(&value, &mut w) {
|
||||
Ok(_) => {
|
||||
let b = w.into_inner().into_inner();
|
||||
let s = String::from_utf8(b)?;
|
||||
ReturnSuccess::value(
|
||||
UntaggedValue::Primitive(Primitive::String(s)).into_value(&name_tag),
|
||||
)
|
||||
}
|
||||
Err(_) => Err(ShellError::labeled_error_with_secondary(
|
||||
"Expected a table with XML-compatible structure from pipeline",
|
||||
"requires XML-compatible input",
|
||||
name_span,
|
||||
"originates from here".to_string(),
|
||||
value_span,
|
||||
)),
|
||||
}
|
||||
}))
|
||||
.to_output_stream(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ToXML;
|
||||
|
||||
#[test]
|
||||
fn examples_work_as_expected() {
|
||||
use crate::examples::test as test_examples;
|
||||
|
||||
test_examples(ToXML {})
|
||||
}
|
||||
}
|
@ -148,9 +148,9 @@ async fn update(
|
||||
|
||||
Ok(input
|
||||
.then(move |input| {
|
||||
let replacement = replacement.clone();
|
||||
let scope = scope.clone();
|
||||
let context = context.clone();
|
||||
let replacement = replacement.clone();
|
||||
let field = field.clone();
|
||||
|
||||
async {
|
||||
|
46
crates/nu-cli/src/commands/url_/command.rs
Normal file
46
crates/nu-cli/src/commands/url_/command.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{ReturnSuccess, Signature, UntaggedValue};
|
||||
|
||||
pub struct Url;
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for Url {
|
||||
fn name(&self) -> &str {
|
||||
"url"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("url")
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Apply url function"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
_args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let registry = registry.clone();
|
||||
|
||||
Ok(OutputStream::one(ReturnSuccess::value(
|
||||
UntaggedValue::string(crate::commands::help::get_help(&Url, ®istry))
|
||||
.into_value(Tag::unknown()),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Url;
|
||||
|
||||
#[test]
|
||||
fn examples_work_as_expected() {
|
||||
use crate::examples::test as test_examples;
|
||||
|
||||
test_examples(Url {})
|
||||
}
|
||||
}
|
58
crates/nu-cli/src/commands/url_/host.rs
Normal file
58
crates/nu-cli/src/commands/url_/host.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use url::Url;
|
||||
|
||||
use super::{operate, DefaultArguments};
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Signature, SyntaxShape, Value};
|
||||
|
||||
pub struct UrlHost;
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for UrlHost {
|
||||
fn name(&self) -> &str {
|
||||
"url host"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("url host")
|
||||
.rest(SyntaxShape::ColumnPath, "optionally operate by column path")
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"gets the host of a url"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let (DefaultArguments { rest }, input) = args.process(®istry).await?;
|
||||
operate(input, rest, &host).await
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "Get host of a url",
|
||||
example: "echo 'http://www.example.com/foo/bar' | url host",
|
||||
result: Some(vec![Value::from("www.example.com")]),
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
fn host(url: &Url) -> &str {
|
||||
url.host_str().unwrap_or("")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::UrlHost;
|
||||
|
||||
#[test]
|
||||
fn examples_work_as_expected() {
|
||||
use crate::examples::test as test_examples;
|
||||
|
||||
test_examples(UrlHost {})
|
||||
}
|
||||
}
|
72
crates/nu-cli/src/commands/url_/mod.rs
Normal file
72
crates/nu-cli/src/commands/url_/mod.rs
Normal file
@ -0,0 +1,72 @@
|
||||
mod command;
|
||||
mod host;
|
||||
mod path;
|
||||
mod query;
|
||||
mod scheme;
|
||||
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{ColumnPath, Primitive, ReturnSuccess, ShellTypeName, UntaggedValue, Value};
|
||||
use url::Url;
|
||||
|
||||
pub use command::Url as UrlCommand;
|
||||
pub use host::UrlHost;
|
||||
pub use path::UrlPath;
|
||||
pub use query::UrlQuery;
|
||||
pub use scheme::UrlScheme;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DefaultArguments {
|
||||
rest: Vec<ColumnPath>,
|
||||
}
|
||||
|
||||
fn handle_value<F>(action: &F, v: &Value) -> Result<Value, ShellError>
|
||||
where
|
||||
F: Fn(&Url) -> &str + Send + 'static,
|
||||
{
|
||||
let a = |url| UntaggedValue::string(action(url));
|
||||
let v = match &v.value {
|
||||
UntaggedValue::Primitive(Primitive::String(s))
|
||||
| UntaggedValue::Primitive(Primitive::Line(s)) => match Url::parse(s) {
|
||||
Ok(url) => a(&url).into_value(v.tag()),
|
||||
Err(_) => UntaggedValue::string("").into_value(v.tag()),
|
||||
},
|
||||
other => {
|
||||
let got = format!("got {}", other.type_name());
|
||||
return Err(ShellError::labeled_error(
|
||||
"value is not a string",
|
||||
got,
|
||||
v.tag().span,
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
async fn operate<F>(
|
||||
input: crate::InputStream,
|
||||
paths: Vec<ColumnPath>,
|
||||
action: &'static F,
|
||||
) -> Result<OutputStream, ShellError>
|
||||
where
|
||||
F: Fn(&Url) -> &str + Send + Sync + 'static,
|
||||
{
|
||||
Ok(input
|
||||
.map(move |v| {
|
||||
if paths.is_empty() {
|
||||
ReturnSuccess::value(handle_value(&action, &v)?)
|
||||
} else {
|
||||
let mut ret = v;
|
||||
|
||||
for path in &paths {
|
||||
ret = ret.swap_data_by_column_path(
|
||||
path,
|
||||
Box::new(move |old| handle_value(&action, &old)),
|
||||
)?;
|
||||
}
|
||||
|
||||
ReturnSuccess::value(ret)
|
||||
}
|
||||
})
|
||||
.to_output_stream())
|
||||
}
|
61
crates/nu-cli/src/commands/url_/path.rs
Normal file
61
crates/nu-cli/src/commands/url_/path.rs
Normal file
@ -0,0 +1,61 @@
|
||||
use url::Url;
|
||||
|
||||
use super::{operate, DefaultArguments};
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Signature, SyntaxShape, Value};
|
||||
|
||||
pub struct UrlPath;
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for UrlPath {
|
||||
fn name(&self) -> &str {
|
||||
"url path"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("url path")
|
||||
.rest(SyntaxShape::ColumnPath, "optionally operate by column path")
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"gets the path of a url"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let (DefaultArguments { rest }, input) = args.process(®istry).await?;
|
||||
operate(input, rest, &Url::path).await
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Get path of a url",
|
||||
example: "echo 'http://www.example.com/foo/bar' | url path",
|
||||
result: Some(vec![Value::from("/foo/bar")]),
|
||||
},
|
||||
Example {
|
||||
description: "A trailing slash will be reflected in the path",
|
||||
example: "echo 'http://www.example.com' | url path",
|
||||
result: Some(vec![Value::from("/")]),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::UrlPath;
|
||||
|
||||
#[test]
|
||||
fn examples_work_as_expected() {
|
||||
use crate::examples::test as test_examples;
|
||||
|
||||
test_examples(UrlPath {})
|
||||
}
|
||||
}
|
65
crates/nu-cli/src/commands/url_/query.rs
Normal file
65
crates/nu-cli/src/commands/url_/query.rs
Normal file
@ -0,0 +1,65 @@
|
||||
use url::Url;
|
||||
|
||||
use super::{operate, DefaultArguments};
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Signature, SyntaxShape, Value};
|
||||
|
||||
pub struct UrlQuery;
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for UrlQuery {
|
||||
fn name(&self) -> &str {
|
||||
"url query"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("url query")
|
||||
.rest(SyntaxShape::ColumnPath, "optionally operate by column path")
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"gets the query of a url"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let (DefaultArguments { rest }, input) = args.process(®istry).await?;
|
||||
operate(input, rest, &query).await
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Get query of a url",
|
||||
example: "echo 'http://www.example.com/?foo=bar&baz=quux' | url query",
|
||||
result: Some(vec![Value::from("foo=bar&baz=quux")]),
|
||||
},
|
||||
Example {
|
||||
description: "No query gives the empty string",
|
||||
example: "echo 'http://www.example.com/' | url query",
|
||||
result: Some(vec![Value::from("")]),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn query(url: &Url) -> &str {
|
||||
url.query().unwrap_or("")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::UrlQuery;
|
||||
|
||||
#[test]
|
||||
fn examples_work_as_expected() {
|
||||
use crate::examples::test as test_examples;
|
||||
|
||||
test_examples(UrlQuery {})
|
||||
}
|
||||
}
|
60
crates/nu-cli/src/commands/url_/scheme.rs
Normal file
60
crates/nu-cli/src/commands/url_/scheme.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use url::Url;
|
||||
|
||||
use super::{operate, DefaultArguments};
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Signature, SyntaxShape, Value};
|
||||
|
||||
pub struct UrlScheme;
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for UrlScheme {
|
||||
fn name(&self) -> &str {
|
||||
"url scheme"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("url scheme").rest(SyntaxShape::ColumnPath, "optionally operate by path")
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"gets the scheme (eg http, file) of a url"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let (DefaultArguments { rest }, input) = args.process(®istry).await?;
|
||||
operate(input, rest, &Url::scheme).await
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Get scheme of a url",
|
||||
example: "echo 'http://www.example.com' | url scheme",
|
||||
result: Some(vec![Value::from("http")]),
|
||||
},
|
||||
Example {
|
||||
description: "You get an empty string if there is no scheme",
|
||||
example: "echo 'test' | url scheme",
|
||||
result: Some(vec![Value::from("")]),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::UrlScheme;
|
||||
|
||||
#[test]
|
||||
fn examples_work_as_expected() {
|
||||
use crate::examples::test as test_examples;
|
||||
|
||||
test_examples(UrlScheme {})
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use crate::TaggedListBuilder;
|
||||
use indexmap::IndexMap;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Dictionary, Signature, UntaggedValue};
|
||||
@ -40,16 +41,42 @@ impl WholeStreamCommand for Version {
|
||||
pub fn version(args: CommandArgs, _registry: &CommandRegistry) -> Result<OutputStream, ShellError> {
|
||||
let tag = args.call_info.args.span;
|
||||
|
||||
let mut indexmap = IndexMap::new();
|
||||
let mut indexmap = IndexMap::with_capacity(4);
|
||||
|
||||
indexmap.insert(
|
||||
"version".to_string(),
|
||||
UntaggedValue::string(clap::crate_version!()).into_value(&tag),
|
||||
);
|
||||
|
||||
indexmap.insert("features".to_string(), features_enabled(&tag).into_value());
|
||||
|
||||
let value = UntaggedValue::Row(Dictionary::from(indexmap)).into_value(&tag);
|
||||
Ok(OutputStream::one(value))
|
||||
}
|
||||
|
||||
fn features_enabled(tag: impl Into<Tag>) -> TaggedListBuilder {
|
||||
let mut names = TaggedListBuilder::new(tag);
|
||||
|
||||
names.push_untagged(UntaggedValue::string("default"));
|
||||
|
||||
#[cfg(feature = "clipboard-cli")]
|
||||
{
|
||||
names.push_untagged(UntaggedValue::string("clipboard"));
|
||||
}
|
||||
|
||||
#[cfg(feature = "trash-support")]
|
||||
{
|
||||
names.push_untagged(UntaggedValue::string("trash"));
|
||||
}
|
||||
|
||||
#[cfg(feature = "starship-prompt")]
|
||||
{
|
||||
names.push_untagged(UntaggedValue::string("starship"));
|
||||
}
|
||||
|
||||
names
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Version;
|
||||
|
@ -1,16 +1,30 @@
|
||||
use nu_errors::ShellError;
|
||||
|
||||
use crate::context;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct Suggestion {
|
||||
pub display: String,
|
||||
pub replacement: String,
|
||||
}
|
||||
|
||||
pub struct Context<'a>(pub &'a rustyline::Context<'a>);
|
||||
pub struct Context<'a>(&'a context::Context, &'a rustyline::Context<'a>);
|
||||
|
||||
impl<'a> Context<'a> {
|
||||
pub fn new(a: &'a context::Context, b: &'a rustyline::Context<'a>) -> Context<'a> {
|
||||
Context(a, b)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AsRef<context::Context> for Context<'a> {
|
||||
fn as_ref(&self) -> &context::Context {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AsRef<rustyline::Context<'a>> for Context<'a> {
|
||||
fn as_ref(&self) -> &rustyline::Context<'a> {
|
||||
self.0
|
||||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -150,14 +150,14 @@ impl Context {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
Ok(Context {
|
||||
registry: registry.clone(),
|
||||
registry,
|
||||
host: Arc::new(parking_lot::Mutex::new(Box::new(
|
||||
crate::env::host::BasicHost,
|
||||
))),
|
||||
current_errors: Arc::new(Mutex::new(vec![])),
|
||||
ctrl_c: Arc::new(AtomicBool::new(false)),
|
||||
user_recently_used_autoenv_untrust: false,
|
||||
shell_manager: ShellManager::basic(registry)?,
|
||||
shell_manager: ShellManager::basic()?,
|
||||
windows_drives_previous_cwd: Arc::new(Mutex::new(std::collections::HashMap::new())),
|
||||
raw_input: String::default(),
|
||||
})
|
||||
@ -166,14 +166,14 @@ impl Context {
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
Ok(Context {
|
||||
registry: registry.clone(),
|
||||
registry,
|
||||
host: Arc::new(parking_lot::Mutex::new(Box::new(
|
||||
crate::env::host::BasicHost,
|
||||
))),
|
||||
current_errors: Arc::new(Mutex::new(vec![])),
|
||||
ctrl_c: Arc::new(AtomicBool::new(false)),
|
||||
user_recently_used_autoenv_untrust: false,
|
||||
shell_manager: ShellManager::basic(registry)?,
|
||||
shell_manager: ShellManager::basic()?,
|
||||
raw_input: String::default(),
|
||||
})
|
||||
}
|
||||
|
@ -173,19 +173,19 @@ mod tests {
|
||||
use num_bigint::BigInt;
|
||||
|
||||
fn string(input: impl Into<String>) -> Value {
|
||||
UntaggedValue::string(input.into()).into_untagged_value()
|
||||
crate::utils::data::helpers::string(input)
|
||||
}
|
||||
|
||||
fn int(input: impl Into<BigInt>) -> Value {
|
||||
UntaggedValue::int(input.into()).into_untagged_value()
|
||||
crate::utils::data::helpers::int(input)
|
||||
}
|
||||
|
||||
fn row(entries: IndexMap<String, Value>) -> Value {
|
||||
UntaggedValue::row(entries).into_untagged_value()
|
||||
crate::utils::data::helpers::row(entries)
|
||||
}
|
||||
|
||||
fn table(list: &[Value]) -> Value {
|
||||
UntaggedValue::table(list).into_untagged_value()
|
||||
crate::utils::data::helpers::table(list)
|
||||
}
|
||||
|
||||
fn error_callback(
|
||||
|
@ -54,14 +54,10 @@ impl DictionaryExt for Dictionary {
|
||||
}
|
||||
|
||||
fn get_mut_data_by_key(&mut self, name: &str) -> Option<&mut Value> {
|
||||
match self
|
||||
.entries
|
||||
self.entries
|
||||
.iter_mut()
|
||||
.find(|(desc_name, _)| *desc_name == name)
|
||||
{
|
||||
Some((_, v)) => Some(v),
|
||||
None => None,
|
||||
}
|
||||
.map_or_else(|| None, |x| Some(x.1))
|
||||
}
|
||||
|
||||
fn insert_data_at_key(&mut self, name: &str, value: Value) {
|
||||
|
@ -6,7 +6,7 @@ use nu_protocol::{TaggedDictBuilder, UntaggedValue, Value};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
|
||||
fn get_file_type(md: &std::fs::Metadata) -> &str {
|
||||
pub(crate) fn get_file_type(md: &std::fs::Metadata) -> &str {
|
||||
let ft = md.file_type();
|
||||
let mut file_type = "Unknown";
|
||||
if ft.is_dir() {
|
||||
@ -37,7 +37,7 @@ pub(crate) fn dir_entry_dict(
|
||||
filename: &std::path::Path,
|
||||
metadata: Option<&std::fs::Metadata>,
|
||||
tag: impl Into<Tag>,
|
||||
full: bool,
|
||||
long: bool,
|
||||
short_name: bool,
|
||||
with_symlink_targets: bool,
|
||||
du: bool,
|
||||
@ -46,7 +46,7 @@ pub(crate) fn dir_entry_dict(
|
||||
let tag = tag.into();
|
||||
let mut dict = TaggedDictBuilder::new(&tag);
|
||||
// Insert all columns first to maintain proper table alignment if we can't find (or are not allowed to view) any information
|
||||
if full {
|
||||
if long {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
for column in [
|
||||
@ -97,7 +97,7 @@ pub(crate) fn dir_entry_dict(
|
||||
dict.insert_untagged("type", get_file_type(md));
|
||||
}
|
||||
|
||||
if full || with_symlink_targets {
|
||||
if long || with_symlink_targets {
|
||||
if let Some(md) = metadata {
|
||||
if md.file_type().is_symlink() {
|
||||
let symlink_target_untagged_value: UntaggedValue;
|
||||
@ -113,7 +113,7 @@ pub(crate) fn dir_entry_dict(
|
||||
}
|
||||
}
|
||||
|
||||
if full {
|
||||
if long {
|
||||
if let Some(md) = metadata {
|
||||
dict.insert_untagged(
|
||||
"readonly",
|
||||
@ -181,7 +181,7 @@ pub(crate) fn dir_entry_dict(
|
||||
}
|
||||
|
||||
if let Some(md) = metadata {
|
||||
if full {
|
||||
if long {
|
||||
if let Ok(c) = md.created() {
|
||||
dict.insert_untagged("created", UntaggedValue::system_date(c));
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::data::base::coerce_compare;
|
||||
use crate::data::base::shape::{Column, InlineShape};
|
||||
use crate::data::primitive::style_primitive;
|
||||
use chrono::DateTime;
|
||||
use chrono::{DateTime, NaiveDate, Utc};
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::hir::Operator;
|
||||
use nu_protocol::ShellTypeName;
|
||||
@ -10,18 +10,40 @@ use nu_source::{DebugDocBuilder, PrettyDebug, Span, Tagged};
|
||||
use nu_table::TextStyle;
|
||||
use num_traits::Zero;
|
||||
|
||||
pub struct Date;
|
||||
|
||||
impl Date {
|
||||
pub fn from_regular_str(s: Tagged<&str>) -> Result<UntaggedValue, ShellError> {
|
||||
let date = DateTime::parse_from_rfc3339(s.item).map_err(|err| {
|
||||
ShellError::labeled_error(
|
||||
&format!("Date parse error: {}", err),
|
||||
"original value",
|
||||
s.tag,
|
||||
)
|
||||
})?;
|
||||
|
||||
let date = date.with_timezone(&chrono::offset::Utc);
|
||||
|
||||
Ok(UntaggedValue::Primitive(Primitive::Date(date)))
|
||||
}
|
||||
|
||||
pub fn naive_from_str(s: Tagged<&str>) -> Result<UntaggedValue, ShellError> {
|
||||
let date = NaiveDate::parse_from_str(s.item, "%Y-%m-%d").map_err(|reason| {
|
||||
ShellError::labeled_error(
|
||||
&format!("Date parse error: {}", reason),
|
||||
"original value",
|
||||
s.tag,
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(UntaggedValue::Primitive(Primitive::Date(
|
||||
DateTime::<Utc>::from_utc(date.and_hms(12, 34, 56), Utc),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn date_from_str(s: Tagged<&str>) -> Result<UntaggedValue, ShellError> {
|
||||
let date = DateTime::parse_from_rfc3339(s.item).map_err(|err| {
|
||||
ShellError::labeled_error(
|
||||
&format!("Date parse error: {}", err),
|
||||
"original value",
|
||||
s.tag,
|
||||
)
|
||||
})?;
|
||||
|
||||
let date = date.with_timezone(&chrono::offset::Utc);
|
||||
|
||||
Ok(UntaggedValue::Primitive(Primitive::Date(date)))
|
||||
Date::from_regular_str(s)
|
||||
}
|
||||
|
||||
pub fn merge_values(
|
||||
@ -162,17 +184,18 @@ pub fn compare_values(
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
let result = match (operator, ordering) {
|
||||
(Operator::Equal, Ordering::Equal) => true,
|
||||
(Operator::NotEqual, Ordering::Less) | (Operator::NotEqual, Ordering::Greater) => true,
|
||||
(Operator::LessThan, Ordering::Less) => true,
|
||||
(Operator::GreaterThan, Ordering::Greater) => true,
|
||||
(Operator::GreaterThanOrEqual, Ordering::Greater)
|
||||
| (Operator::GreaterThanOrEqual, Ordering::Equal) => true,
|
||||
(Operator::LessThanOrEqual, Ordering::Less)
|
||||
| (Operator::LessThanOrEqual, Ordering::Equal) => true,
|
||||
_ => false,
|
||||
};
|
||||
let result = matches!(
|
||||
(operator, ordering),
|
||||
(Operator::Equal, Ordering::Equal)
|
||||
| (Operator::GreaterThan, Ordering::Greater)
|
||||
| (Operator::GreaterThanOrEqual, Ordering::Greater)
|
||||
| (Operator::GreaterThanOrEqual, Ordering::Equal)
|
||||
| (Operator::LessThan, Ordering::Less)
|
||||
| (Operator::LessThanOrEqual, Ordering::Less)
|
||||
| (Operator::LessThanOrEqual, Ordering::Equal)
|
||||
| (Operator::NotEqual, Ordering::Greater)
|
||||
| (Operator::NotEqual, Ordering::Less)
|
||||
);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@ -203,23 +226,30 @@ pub fn format_for_column<'a>(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::UntaggedValue as v;
|
||||
use indexmap::indexmap;
|
||||
|
||||
use super::merge_values;
|
||||
use super::Date as d;
|
||||
use super::UntaggedValue as v;
|
||||
use nu_source::TaggedItem;
|
||||
|
||||
use indexmap::indexmap;
|
||||
|
||||
#[test]
|
||||
fn merges_tables() {
|
||||
let (author_1_date, author_2_date) = (
|
||||
"2020-04-29".to_string().tagged_unknown(),
|
||||
"2019-10-10".to_string().tagged_unknown(),
|
||||
);
|
||||
|
||||
let table_author_row = v::row(indexmap! {
|
||||
"name".into() => v::string("Andrés").into_untagged_value(),
|
||||
"country".into() => v::string("EC").into_untagged_value(),
|
||||
"date".into() => v::string("April 29-2020").into_untagged_value()
|
||||
"date".into() => d::naive_from_str(author_1_date.borrow_tagged()).unwrap().into_untagged_value()
|
||||
});
|
||||
|
||||
let other_table_author_row = v::row(indexmap! {
|
||||
"name".into() => v::string("YK").into_untagged_value(),
|
||||
"country".into() => v::string("US").into_untagged_value(),
|
||||
"date".into() => v::string("October 10-2019").into_untagged_value()
|
||||
"date".into() => d::naive_from_str(author_2_date.borrow_tagged()).unwrap().into_untagged_value()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
|
@ -12,6 +12,8 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
//Tests reside in /nushell/tests/shell/pipeline/commands/internal.rs
|
||||
|
||||
type EnvKey = String;
|
||||
type EnvVal = OsString;
|
||||
#[derive(Debug, Default)]
|
||||
@ -82,14 +84,11 @@ impl DirectorySpecificEnvironment {
|
||||
//We track which keys we set as we go up the directory hierarchy, so that we don't overwrite a value we set in a subdir.
|
||||
let mut added_keys = IndexSet::new();
|
||||
|
||||
//We note which directories we pass so we can clear unvisited dirs later.
|
||||
let mut seen_directories = IndexSet::new();
|
||||
|
||||
//Add all .nu-envs until we reach a dir which we have already added, or we reached the root.
|
||||
let mut new_visited_dirs = IndexSet::new();
|
||||
let mut popped = true;
|
||||
while popped && !self.visited_dirs.contains(&dir) {
|
||||
while popped {
|
||||
let nu_env_file = dir.join(".nu-env");
|
||||
if nu_env_file.exists() {
|
||||
if nu_env_file.exists() && !self.visited_dirs.contains(&dir) {
|
||||
let nu_env_doc = self.toml_if_trusted(&nu_env_file)?;
|
||||
|
||||
//add regular variables from the [env section]
|
||||
@ -98,7 +97,6 @@ impl DirectorySpecificEnvironment {
|
||||
self.maybe_add_key(&mut added_keys, &dir, &env_key, &env_val);
|
||||
}
|
||||
}
|
||||
self.visited_dirs.insert(dir.clone());
|
||||
|
||||
//Add variables that need to evaluate scripts to run, from [scriptvars] section
|
||||
if let Some(sv) = nu_env_doc.scriptvars {
|
||||
@ -122,14 +120,14 @@ impl DirectorySpecificEnvironment {
|
||||
self.exitscripts.insert(dir.clone(), es);
|
||||
}
|
||||
}
|
||||
seen_directories.insert(dir.clone());
|
||||
new_visited_dirs.insert(dir.clone());
|
||||
popped = dir.pop();
|
||||
}
|
||||
|
||||
//Time to clear out vars set by directories that we have left.
|
||||
let mut new_vars = IndexMap::new();
|
||||
for (dir, dirmap) in self.added_vars.drain(..) {
|
||||
if seen_directories.contains(&dir) {
|
||||
if new_visited_dirs.contains(&dir) {
|
||||
new_vars.insert(dir, dirmap);
|
||||
} else {
|
||||
for (k, v) in dirmap {
|
||||
@ -142,17 +140,10 @@ impl DirectorySpecificEnvironment {
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_visited = IndexSet::new();
|
||||
for dir in self.visited_dirs.drain(..) {
|
||||
if seen_directories.contains(&dir) {
|
||||
new_visited.insert(dir);
|
||||
}
|
||||
}
|
||||
|
||||
//Run exitscripts, can not be done in same loop as new vars as some files can contain only exitscripts
|
||||
let mut new_exitscripts = IndexMap::new();
|
||||
for (dir, scripts) in self.exitscripts.drain(..) {
|
||||
if seen_directories.contains(&dir) {
|
||||
if new_visited_dirs.contains(&dir) {
|
||||
new_exitscripts.insert(dir, scripts);
|
||||
} else {
|
||||
for s in scripts {
|
||||
@ -161,7 +152,7 @@ impl DirectorySpecificEnvironment {
|
||||
}
|
||||
}
|
||||
|
||||
self.visited_dirs = new_visited;
|
||||
self.visited_dirs = new_visited_dirs;
|
||||
self.exitscripts = new_exitscripts;
|
||||
self.added_vars = new_vars;
|
||||
self.last_seen_directory = current_dir()?;
|
||||
|
@ -41,7 +41,6 @@ pub(crate) async fn evaluate_args(
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ use crate::prelude::*;
|
||||
use async_recursion::async_recursion;
|
||||
use log::trace;
|
||||
use nu_errors::{ArgumentError, ShellError};
|
||||
use nu_protocol::hir::{self, Expression, SpannedExpression};
|
||||
use nu_protocol::hir::{self, Expression, ExternalRedirection, SpannedExpression};
|
||||
use nu_protocol::{
|
||||
ColumnPath, Primitive, RangeInclusion, UnspannedPathMember, UntaggedValue, Value,
|
||||
};
|
||||
@ -194,10 +194,10 @@ async fn evaluate_invocation(
|
||||
let mut context = Context::basic()?;
|
||||
context.registry = registry.clone();
|
||||
|
||||
let input = InputStream::empty();
|
||||
let input = InputStream::one(it.clone());
|
||||
|
||||
let mut block = block.clone();
|
||||
block.set_is_last(false);
|
||||
block.set_redirect(ExternalRedirection::Stdout);
|
||||
|
||||
let result = run_block(&block, &mut context, input, it, vars, env).await?;
|
||||
|
||||
|
@ -2,13 +2,10 @@ use crate::prelude::*;
|
||||
|
||||
pub fn current_branch() -> Option<String> {
|
||||
if let Ok(config) = crate::data::config::config(Tag::unknown()) {
|
||||
let use_starship = match config.get("use_starship") {
|
||||
Some(b) => match b.as_bool() {
|
||||
Ok(b) => b,
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
};
|
||||
let use_starship = config
|
||||
.get("use_starship")
|
||||
.map(|x| x.is_true())
|
||||
.unwrap_or(false);
|
||||
|
||||
if !use_starship {
|
||||
#[cfg(feature = "git2")]
|
||||
|
@ -1,24 +1,24 @@
|
||||
use crate::context::CommandRegistry;
|
||||
|
||||
use crate::data::config;
|
||||
use crate::prelude::*;
|
||||
use derive_new::new;
|
||||
#[cfg(all(windows, feature = "ichwh"))]
|
||||
use ichwh::IchwhError;
|
||||
#[cfg(all(windows, feature = "ichwh"))]
|
||||
use ichwh::IchwhResult;
|
||||
use indexmap::set::IndexSet;
|
||||
use rustyline::completion::{Completer, FilenameCompleter};
|
||||
use std::fs::{read_dir, DirEntry};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(new)]
|
||||
use indexmap::set::IndexSet;
|
||||
use nu_errors::ShellError;
|
||||
use rustyline::completion::{Completer as _, FilenameCompleter};
|
||||
use rustyline::hint::{Hinter as _, HistoryHinter};
|
||||
|
||||
#[cfg(all(windows, feature = "ichwh"))]
|
||||
use ichwh::{IchwhError, IchwhResult};
|
||||
|
||||
use crate::completion::{self, Completer};
|
||||
use crate::context;
|
||||
use crate::data::config;
|
||||
use crate::prelude::*;
|
||||
|
||||
pub(crate) struct NuCompleter {
|
||||
pub file_completer: FilenameCompleter,
|
||||
pub commands: CommandRegistry,
|
||||
pub homedir: Option<PathBuf>,
|
||||
file_completer: FilenameCompleter,
|
||||
hinter: HistoryHinter,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
@ -28,68 +28,57 @@ enum ReplacementLocation {
|
||||
}
|
||||
|
||||
impl NuCompleter {
|
||||
pub fn complete(
|
||||
fn complete_internal(
|
||||
&self,
|
||||
line: &str,
|
||||
pos: usize,
|
||||
context: &rustyline::Context,
|
||||
context: &completion::Context,
|
||||
) -> rustyline::Result<(usize, Vec<rustyline::completion::Pair>)> {
|
||||
let commands: Vec<String> = self.commands.names();
|
||||
|
||||
let line_chars: Vec<_> = line[..pos].chars().collect();
|
||||
|
||||
let (replace_pos, replace_loc) = self.get_replace_pos(line, pos);
|
||||
|
||||
let mut completions;
|
||||
let (replace_pos, replace_loc) = get_replace_pos(line, pos);
|
||||
|
||||
// See if we're a flag
|
||||
let mut completions;
|
||||
if pos > 0 && replace_pos < line_chars.len() && line_chars[replace_pos] == '-' {
|
||||
if let Ok(lite_block) = nu_parser::lite_parse(line, 0) {
|
||||
completions =
|
||||
self.get_matching_arguments(&lite_block, &line_chars, line, replace_pos, pos);
|
||||
completions = get_matching_arguments(
|
||||
context.as_ref(),
|
||||
&lite_block,
|
||||
&line_chars,
|
||||
line,
|
||||
replace_pos,
|
||||
pos,
|
||||
);
|
||||
} else {
|
||||
completions = self.file_completer.complete(line, pos, context)?.1;
|
||||
completions = self.file_completer.complete(line, pos, context.as_ref())?.1;
|
||||
}
|
||||
} else {
|
||||
completions = self.file_completer.complete(line, pos, context)?.1;
|
||||
|
||||
for completion in &mut completions {
|
||||
if completion.replacement.contains("\\ ") {
|
||||
completion.replacement = completion.replacement.replace("\\ ", " ");
|
||||
}
|
||||
if completion.replacement.contains("\\(") {
|
||||
completion.replacement = completion.replacement.replace("\\(", "(");
|
||||
}
|
||||
|
||||
if completion.replacement.contains(' ') || completion.replacement.contains('(') {
|
||||
if !completion.replacement.starts_with('\"') {
|
||||
completion.replacement = format!("\"{}", completion.replacement);
|
||||
}
|
||||
if !completion.replacement.ends_with('\"') {
|
||||
completion.replacement = format!("{}\"", completion.replacement);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let complete_from_path = match config::config(Tag::unknown()) {
|
||||
Ok(conf) => match conf.get("complete_from_path") {
|
||||
Some(val) => val.is_true(),
|
||||
_ => true,
|
||||
},
|
||||
_ => true,
|
||||
};
|
||||
completions = self.file_completer.complete(line, pos, context.as_ref())?.1;
|
||||
}
|
||||
|
||||
// Only complete executables or commands if the thing we're completing
|
||||
// is syntactically a command
|
||||
if replace_loc == ReplacementLocation::Command {
|
||||
let context: &context::Context = context.as_ref();
|
||||
let commands: Vec<String> = context.registry.names();
|
||||
let mut all_executables: IndexSet<_> = commands.iter().map(|x| x.to_string()).collect();
|
||||
|
||||
let complete_from_path = config::config(Tag::unknown())
|
||||
.map(|conf| {
|
||||
conf.get("complete_from_path")
|
||||
.map(|v| v.is_true())
|
||||
.unwrap_or(true)
|
||||
})
|
||||
.unwrap_or(true);
|
||||
|
||||
if complete_from_path {
|
||||
let path_executables = self.find_path_executables().unwrap_or_default();
|
||||
let path_executables = find_path_executables().unwrap_or_default();
|
||||
for path_exe in path_executables {
|
||||
all_executables.insert(path_exe);
|
||||
}
|
||||
};
|
||||
|
||||
for exe in all_executables.iter() {
|
||||
let mut pos = replace_pos;
|
||||
let mut matched = false;
|
||||
@ -98,6 +87,7 @@ impl NuCompleter {
|
||||
if line_chars[pos] != chr {
|
||||
break;
|
||||
}
|
||||
|
||||
pos += 1;
|
||||
if pos == line_chars.len() {
|
||||
matched = true;
|
||||
@ -115,179 +105,260 @@ impl NuCompleter {
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust replacement to deal with a quote already at the cursor. Specifically, if there's
|
||||
// already a quote at the cursor, but the replacement doesn't have one, we need to ensure
|
||||
// one exists (to be safe, even if the completion doesn't need it).
|
||||
for completion in &mut completions {
|
||||
// If the cursor is at a double-quote, remove the double-quote in the replacement
|
||||
// This prevents duplicate quotes
|
||||
let cursor_char = line.chars().nth(pos);
|
||||
if cursor_char.unwrap_or(' ') == '"' && completion.replacement.ends_with('"') {
|
||||
completion.replacement.pop();
|
||||
let cursor_char = line.chars().nth(replace_pos);
|
||||
if cursor_char.unwrap_or(' ') == '"' && !completion.replacement.starts_with('"') {
|
||||
completion.replacement.insert(0, '"');
|
||||
}
|
||||
}
|
||||
|
||||
Ok((replace_pos, completions))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_replace_pos(&self, line: &str, pos: usize) -> (usize, ReplacementLocation) {
|
||||
let line_chars: Vec<_> = line[..pos].chars().collect();
|
||||
let mut replace_pos = line_chars.len();
|
||||
let mut parsed_pos = false;
|
||||
let mut loc = ReplacementLocation::Other;
|
||||
if let Ok(lite_block) = nu_parser::lite_parse(line, 0) {
|
||||
'outer: for pipeline in lite_block.block.iter() {
|
||||
for command in pipeline.commands.iter() {
|
||||
let name_span = command.name.span;
|
||||
if name_span.start() <= pos && name_span.end() >= pos {
|
||||
replace_pos = name_span.start();
|
||||
parsed_pos = true;
|
||||
loc = ReplacementLocation::Command;
|
||||
break 'outer;
|
||||
}
|
||||
impl Completer for NuCompleter {
|
||||
fn complete(
|
||||
&self,
|
||||
line: &str,
|
||||
pos: usize,
|
||||
context: &completion::Context,
|
||||
) -> Result<(usize, Vec<completion::Suggestion>), ShellError> {
|
||||
let expanded = nu_parser::expand_ndots(&line);
|
||||
|
||||
for arg in command.args.iter() {
|
||||
if arg.span.start() <= pos && arg.span.end() >= pos {
|
||||
replace_pos = arg.span.start();
|
||||
parsed_pos = true;
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Find the first not-matching char position, if there is one
|
||||
let differ_pos = line
|
||||
.chars()
|
||||
.zip(expanded.chars())
|
||||
.enumerate()
|
||||
.find(|(_index, (a, b))| a != b)
|
||||
.map(|(differ_pos, _)| differ_pos);
|
||||
|
||||
let pos = if let Some(differ_pos) = differ_pos {
|
||||
if differ_pos < pos {
|
||||
pos + (expanded.len() - line.len())
|
||||
} else {
|
||||
pos
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pos
|
||||
};
|
||||
|
||||
if !parsed_pos {
|
||||
// If the command won't parse, naively detect the completion start point
|
||||
while replace_pos > 0 {
|
||||
if line_chars[replace_pos - 1] == ' ' {
|
||||
break;
|
||||
}
|
||||
replace_pos -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
(replace_pos, loc)
|
||||
self.complete_internal(&expanded, pos, context)
|
||||
.map_err(|e| ShellError::untagged_runtime_error(format!("{}", e)))
|
||||
.map(requote)
|
||||
.map(|(pos, completions)| {
|
||||
(
|
||||
pos,
|
||||
completions
|
||||
.into_iter()
|
||||
.map(|pair| completion::Suggestion {
|
||||
display: pair.display,
|
||||
replacement: pair.replacement,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn get_matching_arguments(
|
||||
&self,
|
||||
lite_block: &nu_parser::LiteBlock,
|
||||
line_chars: &[char],
|
||||
line: &str,
|
||||
replace_pos: usize,
|
||||
pos: usize,
|
||||
) -> Vec<rustyline::completion::Pair> {
|
||||
let mut matching_arguments = vec![];
|
||||
fn hint(&self, line: &str, pos: usize, ctx: &completion::Context<'_>) -> Option<String> {
|
||||
self.hinter.hint(line, pos, &ctx.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
let mut line_copy = line.to_string();
|
||||
let substring = line_chars[replace_pos..pos].iter().collect::<String>();
|
||||
let replace_string = (replace_pos..pos).map(|_| " ").collect::<String>();
|
||||
line_copy.replace_range(replace_pos..pos, &replace_string);
|
||||
impl Default for NuCompleter {
|
||||
fn default() -> NuCompleter {
|
||||
NuCompleter {
|
||||
file_completer: FilenameCompleter::new(),
|
||||
hinter: HistoryHinter {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = nu_parser::classify_block(&lite_block, &self.commands);
|
||||
fn get_matching_arguments(
|
||||
context: &context::Context,
|
||||
lite_block: &nu_parser::LiteBlock,
|
||||
line_chars: &[char],
|
||||
line: &str,
|
||||
replace_pos: usize,
|
||||
pos: usize,
|
||||
) -> Vec<rustyline::completion::Pair> {
|
||||
let mut matching_arguments = vec![];
|
||||
|
||||
for pipeline in &result.block.block {
|
||||
for command in &pipeline.list {
|
||||
if let nu_protocol::hir::ClassifiedCommand::Internal(
|
||||
nu_protocol::hir::InternalCommand { args, .. },
|
||||
) = command
|
||||
{
|
||||
if replace_pos >= args.span.start() && replace_pos <= args.span.end() {
|
||||
if let Some(named) = &args.named {
|
||||
for (name, _) in named.iter() {
|
||||
let full_flag = format!("--{}", name);
|
||||
let mut line_copy = line.to_string();
|
||||
let substring = line_chars[replace_pos..pos].iter().collect::<String>();
|
||||
let replace_string = (replace_pos..pos).map(|_| " ").collect::<String>();
|
||||
line_copy.replace_range(replace_pos..pos, &replace_string);
|
||||
|
||||
if full_flag.starts_with(&substring) {
|
||||
matching_arguments.push(rustyline::completion::Pair {
|
||||
display: full_flag.clone(),
|
||||
replacement: full_flag,
|
||||
});
|
||||
}
|
||||
let result = nu_parser::classify_block(&lite_block, &context.registry);
|
||||
|
||||
for pipeline in &result.block.block {
|
||||
for command in &pipeline.list {
|
||||
if let nu_protocol::hir::ClassifiedCommand::Internal(
|
||||
nu_protocol::hir::InternalCommand { args, .. },
|
||||
) = command
|
||||
{
|
||||
if replace_pos >= args.span.start() && replace_pos <= args.span.end() {
|
||||
if let Some(named) = &args.named {
|
||||
for (name, _) in named.iter() {
|
||||
let full_flag = format!("--{}", name);
|
||||
|
||||
if full_flag.starts_with(&substring) {
|
||||
matching_arguments.push(rustyline::completion::Pair {
|
||||
display: full_flag.clone(),
|
||||
replacement: full_flag,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matching_arguments
|
||||
}
|
||||
|
||||
// These is_executable/pathext implementations are copied from ichwh and modified
|
||||
// to not be async
|
||||
matching_arguments
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn pathext(&self) -> IchwhResult<Vec<String>> {
|
||||
Ok(std::env::var_os("PATHEXT")
|
||||
.ok_or(IchwhError::PathextNotDefined)?
|
||||
.to_string_lossy()
|
||||
.split(';')
|
||||
// Cut off the leading '.' character
|
||||
.map(|ext| ext[1..].to_string())
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
// These is_executable/pathext implementations are copied from ichwh and modified
|
||||
// to not be async
|
||||
|
||||
#[cfg(windows)]
|
||||
fn is_executable(&self, file: &DirEntry) -> bool {
|
||||
if let Ok(metadata) = file.metadata() {
|
||||
let file_type = metadata.file_type();
|
||||
#[cfg(windows)]
|
||||
fn pathext() -> IchwhResult<Vec<String>> {
|
||||
Ok(std::env::var_os("PATHEXT")
|
||||
.ok_or(IchwhError::PathextNotDefined)?
|
||||
.to_string_lossy()
|
||||
.split(';')
|
||||
// Cut off the leading '.' character
|
||||
.map(|ext| ext[1..].to_string())
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
// If the entry isn't a file, it cannot be executable
|
||||
if !(file_type.is_file() || file_type.is_symlink()) {
|
||||
return false;
|
||||
}
|
||||
#[cfg(windows)]
|
||||
fn is_executable(file: &DirEntry) -> bool {
|
||||
if let Ok(metadata) = file.metadata() {
|
||||
let file_type = metadata.file_type();
|
||||
|
||||
if let Some(extension) = file.path().extension() {
|
||||
if let Ok(exts) = self.pathext() {
|
||||
exts.iter()
|
||||
.any(|ext| extension.to_string_lossy().eq_ignore_ascii_case(ext))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
// If the entry isn't a file, it cannot be executable
|
||||
if !(file_type.is_file() || file_type.is_symlink()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(extension) = file.path().extension() {
|
||||
if let Ok(exts) = pathext() {
|
||||
exts.iter()
|
||||
.any(|ext| extension.to_string_lossy().eq_ignore_ascii_case(ext))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn is_executable(&self, file: &DirEntry) -> bool {
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn is_executable(&self, file: &DirEntry) -> bool {
|
||||
let metadata = file.metadata();
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn is_executable(file: &DirEntry) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
if let Ok(metadata) = metadata {
|
||||
let filetype = metadata.file_type();
|
||||
let permissions = metadata.permissions();
|
||||
#[cfg(unix)]
|
||||
fn is_executable(file: &DirEntry) -> bool {
|
||||
let metadata = file.metadata();
|
||||
|
||||
// The file is executable if it is a directory or a symlink and the permissions are set for
|
||||
// owner, group, or other
|
||||
(filetype.is_file() || filetype.is_symlink()) && (permissions.mode() & 0o111 != 0)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
if let Ok(metadata) = metadata {
|
||||
let filetype = metadata.file_type();
|
||||
let permissions = metadata.permissions();
|
||||
|
||||
// The file is executable if it is a directory or a symlink and the permissions are set for
|
||||
// owner, group, or other
|
||||
(filetype.is_file() || filetype.is_symlink()) && (permissions.mode() & 0o111 != 0)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn find_path_executables(&self) -> Option<IndexSet<String>> {
|
||||
let path_var = std::env::var_os("PATH")?;
|
||||
let paths: Vec<_> = std::env::split_paths(&path_var).collect();
|
||||
fn find_path_executables() -> Option<IndexSet<String>> {
|
||||
let path_var = std::env::var_os("PATH")?;
|
||||
let paths: Vec<_> = std::env::split_paths(&path_var).collect();
|
||||
|
||||
let mut executables: IndexSet<String> = IndexSet::new();
|
||||
for path in paths {
|
||||
if let Ok(mut contents) = read_dir(path) {
|
||||
while let Some(Ok(item)) = contents.next() {
|
||||
if self.is_executable(&item) {
|
||||
if let Ok(name) = item.file_name().into_string() {
|
||||
executables.insert(name);
|
||||
}
|
||||
let mut executables: IndexSet<String> = IndexSet::new();
|
||||
for path in paths {
|
||||
if let Ok(mut contents) = read_dir(path) {
|
||||
while let Some(Ok(item)) = contents.next() {
|
||||
if is_executable(&item) {
|
||||
if let Ok(name) = item.file_name().into_string() {
|
||||
executables.insert(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(executables)
|
||||
}
|
||||
|
||||
Some(executables)
|
||||
}
|
||||
|
||||
fn get_replace_pos(line: &str, pos: usize) -> (usize, ReplacementLocation) {
|
||||
let line_chars: Vec<_> = line[..pos].chars().collect();
|
||||
let mut replace_pos = line_chars.len();
|
||||
let mut parsed_pos = false;
|
||||
let mut loc = ReplacementLocation::Other;
|
||||
if let Ok(lite_block) = nu_parser::lite_parse(line, 0) {
|
||||
'outer: for pipeline in lite_block.block.iter() {
|
||||
for command in pipeline.commands.iter() {
|
||||
let name_span = command.name.span;
|
||||
if name_span.start() <= pos && name_span.end() >= pos {
|
||||
replace_pos = name_span.start();
|
||||
parsed_pos = true;
|
||||
loc = ReplacementLocation::Command;
|
||||
break 'outer;
|
||||
}
|
||||
|
||||
for arg in command.args.iter() {
|
||||
if arg.span.start() <= pos && arg.span.end() >= pos {
|
||||
replace_pos = arg.span.start();
|
||||
parsed_pos = true;
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !parsed_pos {
|
||||
// If the command won't parse, naively detect the completion start point
|
||||
while replace_pos > 0 {
|
||||
if line_chars[replace_pos - 1] == ' ' {
|
||||
break;
|
||||
}
|
||||
replace_pos -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
(replace_pos, loc)
|
||||
}
|
||||
|
||||
fn requote(
|
||||
items: (usize, Vec<rustyline::completion::Pair>),
|
||||
) -> (usize, Vec<rustyline::completion::Pair>) {
|
||||
let mut new_items = Vec::with_capacity(items.1.len());
|
||||
|
||||
for item in items.1 {
|
||||
let unescaped = rustyline::completion::unescape(&item.replacement, Some('\\'));
|
||||
let maybe_quote = if unescaped != item.replacement {
|
||||
"\""
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
new_items.push(rustyline::completion::Pair {
|
||||
display: item.display,
|
||||
replacement: format!("{}{}{}", maybe_quote, unescaped, maybe_quote),
|
||||
});
|
||||
}
|
||||
|
||||
(items.0, new_items)
|
||||
}
|
||||
|
@ -5,16 +5,12 @@ use crate::commands::ls::LsArgs;
|
||||
use crate::commands::mkdir::MkdirArgs;
|
||||
use crate::commands::move_::mv::Arguments as MvArgs;
|
||||
use crate::commands::rm::RemoveArgs;
|
||||
use crate::completion;
|
||||
use crate::data::dir_entry_dict;
|
||||
use crate::path::canonicalize;
|
||||
use crate::prelude::*;
|
||||
use crate::shell::completer::NuCompleter;
|
||||
use crate::shell::shell::Shell;
|
||||
use crate::utils::FileStructure;
|
||||
|
||||
use rustyline::completion::FilenameCompleter;
|
||||
use rustyline::hint::{Hinter, HistoryHinter};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Error, ErrorKind};
|
||||
use std::path::{Path, PathBuf};
|
||||
@ -28,15 +24,12 @@ use futures_util::TryStreamExt;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use nu_errors::ShellError;
|
||||
use nu_parser::expand_ndots;
|
||||
use nu_protocol::{Primitive, ReturnSuccess, UntaggedValue};
|
||||
use nu_source::Tagged;
|
||||
|
||||
pub struct FilesystemShell {
|
||||
pub(crate) path: String,
|
||||
pub(crate) last_path: String,
|
||||
completer: NuCompleter,
|
||||
hinter: HistoryHinter,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for FilesystemShell {
|
||||
@ -50,18 +43,12 @@ impl Clone for FilesystemShell {
|
||||
FilesystemShell {
|
||||
path: self.path.clone(),
|
||||
last_path: self.path.clone(),
|
||||
completer: NuCompleter {
|
||||
file_completer: FilenameCompleter::new(),
|
||||
commands: self.completer.commands.clone(),
|
||||
homedir: self.homedir(),
|
||||
},
|
||||
hinter: HistoryHinter {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FilesystemShell {
|
||||
pub fn basic(commands: CommandRegistry) -> Result<FilesystemShell, Error> {
|
||||
pub fn basic() -> Result<FilesystemShell, Error> {
|
||||
let path = match std::env::current_dir() {
|
||||
Ok(path) => path,
|
||||
Err(_) => PathBuf::from("/"),
|
||||
@ -70,33 +57,15 @@ impl FilesystemShell {
|
||||
Ok(FilesystemShell {
|
||||
path: path.to_string_lossy().to_string(),
|
||||
last_path: path.to_string_lossy().to_string(),
|
||||
completer: NuCompleter {
|
||||
file_completer: FilenameCompleter::new(),
|
||||
commands,
|
||||
homedir: homedir_if_possible(),
|
||||
},
|
||||
hinter: HistoryHinter {},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_location(
|
||||
path: String,
|
||||
commands: CommandRegistry,
|
||||
) -> Result<FilesystemShell, std::io::Error> {
|
||||
pub fn with_location(path: String) -> Result<FilesystemShell, std::io::Error> {
|
||||
let path = canonicalize(std::env::current_dir()?, &path)?;
|
||||
let path = path.display().to_string();
|
||||
let last_path = path.clone();
|
||||
|
||||
Ok(FilesystemShell {
|
||||
path,
|
||||
last_path,
|
||||
completer: NuCompleter {
|
||||
file_completer: FilenameCompleter::new(),
|
||||
commands,
|
||||
homedir: homedir_if_possible(),
|
||||
},
|
||||
hinter: HistoryHinter {},
|
||||
})
|
||||
Ok(FilesystemShell { path, last_path })
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,7 +95,7 @@ impl Shell for FilesystemShell {
|
||||
LsArgs {
|
||||
path,
|
||||
all,
|
||||
full,
|
||||
long,
|
||||
short_names,
|
||||
with_symlink_targets,
|
||||
du,
|
||||
@ -195,7 +164,7 @@ impl Shell for FilesystemShell {
|
||||
&path,
|
||||
metadata.as_ref(),
|
||||
name_tag.clone(),
|
||||
full,
|
||||
long,
|
||||
short_names,
|
||||
with_symlink_targets,
|
||||
du,
|
||||
@ -323,10 +292,7 @@ impl Shell for FilesystemShell {
|
||||
));
|
||||
}
|
||||
|
||||
let any_source_is_dir = sources.iter().any(|f| match f {
|
||||
Ok(f) => f.is_dir(),
|
||||
Err(_) => false,
|
||||
});
|
||||
let any_source_is_dir = sources.iter().any(|f| matches!(f, Ok(f) if f.is_dir()));
|
||||
|
||||
if any_source_is_dir && !recursive.item {
|
||||
return Err(ShellError::labeled_error(
|
||||
@ -740,56 +706,6 @@ impl Shell for FilesystemShell {
|
||||
}
|
||||
}
|
||||
|
||||
impl completion::Completer for FilesystemShell {
|
||||
fn complete(
|
||||
&self,
|
||||
line: &str,
|
||||
pos: usize,
|
||||
ctx: &completion::Context<'_>,
|
||||
) -> Result<(usize, Vec<completion::Suggestion>), ShellError> {
|
||||
let expanded = expand_ndots(&line);
|
||||
|
||||
// Find the first not-matching char position, if there is one
|
||||
let differ_pos = line
|
||||
.chars()
|
||||
.zip(expanded.chars())
|
||||
.enumerate()
|
||||
.find(|(_index, (a, b))| a != b)
|
||||
.map(|(differ_pos, _)| differ_pos);
|
||||
|
||||
let pos = if let Some(differ_pos) = differ_pos {
|
||||
if differ_pos < pos {
|
||||
pos + (expanded.len() - line.len())
|
||||
} else {
|
||||
pos
|
||||
}
|
||||
} else {
|
||||
pos
|
||||
};
|
||||
|
||||
self.completer
|
||||
.complete(&expanded, pos, ctx.as_ref())
|
||||
.map_err(|e| ShellError::untagged_runtime_error(format!("{}", e)))
|
||||
.map(requote)
|
||||
.map(|(pos, completions)| {
|
||||
(
|
||||
pos,
|
||||
completions
|
||||
.into_iter()
|
||||
.map(|pair| completion::Suggestion {
|
||||
display: pair.display,
|
||||
replacement: pair.replacement,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn hint(&self, line: &str, pos: usize, ctx: &completion::Context<'_>) -> Option<String> {
|
||||
self.hinter.hint(line, pos, ctx.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
struct TaggedPathBuf<'a>(&'a PathBuf, &'a Tag);
|
||||
|
||||
fn move_file(from: TaggedPathBuf, to: TaggedPathBuf) -> Result<(), ShellError> {
|
||||
@ -876,25 +792,3 @@ fn is_hidden_dir(dir: impl AsRef<Path>) -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn requote(
|
||||
items: (usize, Vec<rustyline::completion::Pair>),
|
||||
) -> (usize, Vec<rustyline::completion::Pair>) {
|
||||
let mut new_items = Vec::with_capacity(items.1.len());
|
||||
|
||||
for item in items.1 {
|
||||
let unescaped = rustyline::completion::unescape(&item.replacement, Some('\\'));
|
||||
let maybe_quote = if unescaped != item.replacement {
|
||||
"\""
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
new_items.push(rustyline::completion::Pair {
|
||||
display: item.display,
|
||||
replacement: format!("{}{}{}", maybe_quote, unescaped, maybe_quote),
|
||||
});
|
||||
}
|
||||
|
||||
(items.0, new_items)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::completion::{self, Completer as _};
|
||||
use crate::completion::{self, Completer};
|
||||
use crate::context::Context;
|
||||
use crate::shell::palette::{DefaultPalette, Palette};
|
||||
|
||||
@ -6,19 +6,19 @@ use ansi_term::{Color, Style};
|
||||
use nu_parser::SignatureRegistry;
|
||||
use nu_protocol::hir::FlatShape;
|
||||
use nu_source::{Spanned, Tag, Tagged};
|
||||
use rustyline::completion::Completer;
|
||||
use rustyline::highlight::Highlighter;
|
||||
use rustyline::hint::Hinter;
|
||||
use std::borrow::Cow::{self, Owned};
|
||||
|
||||
pub(crate) struct Helper {
|
||||
pub struct Helper {
|
||||
completer: Box<dyn Completer>,
|
||||
context: Context,
|
||||
pub colored_prompt: String,
|
||||
}
|
||||
|
||||
impl Helper {
|
||||
pub(crate) fn new(context: Context) -> Helper {
|
||||
pub(crate) fn new(completer: Box<dyn Completer>, context: Context) -> Helper {
|
||||
Helper {
|
||||
completer,
|
||||
context,
|
||||
colored_prompt: String::new(),
|
||||
}
|
||||
@ -35,7 +35,7 @@ impl rustyline::completion::Candidate for completion::Suggestion {
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for Helper {
|
||||
impl rustyline::completion::Completer for Helper {
|
||||
type Candidate = completion::Suggestion;
|
||||
|
||||
fn complete(
|
||||
@ -44,9 +44,8 @@ impl Completer for Helper {
|
||||
pos: usize,
|
||||
ctx: &rustyline::Context<'_>,
|
||||
) -> Result<(usize, Vec<Self::Candidate>), rustyline::error::ReadlineError> {
|
||||
let ctx = completion::Context(ctx);
|
||||
self.context
|
||||
.shell_manager
|
||||
let ctx = completion::Context::new(&self.context, ctx);
|
||||
self.completer
|
||||
.complete(line, pos, &ctx)
|
||||
.map_err(|_| rustyline::error::ReadlineError::Eof)
|
||||
}
|
||||
@ -54,12 +53,12 @@ impl Completer for Helper {
|
||||
|
||||
impl Hinter for Helper {
|
||||
fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option<String> {
|
||||
let ctx = completion::Context(ctx);
|
||||
self.context.shell_manager.hint(line, pos, &ctx)
|
||||
let ctx = completion::Context::new(&self.context, ctx);
|
||||
self.completer.hint(line, pos, &ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Highlighter for Helper {
|
||||
impl rustyline::highlight::Highlighter for Helper {
|
||||
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
|
||||
&'s self,
|
||||
prompt: &'p str,
|
||||
|
@ -6,7 +6,6 @@ use crate::commands::ls::LsArgs;
|
||||
use crate::commands::mkdir::MkdirArgs;
|
||||
use crate::commands::move_::mv::Arguments as MvArgs;
|
||||
use crate::commands::rm::RemoveArgs;
|
||||
use crate::completion;
|
||||
use crate::prelude::*;
|
||||
use crate::stream::OutputStream;
|
||||
|
||||
@ -14,7 +13,7 @@ use encoding_rs::Encoding;
|
||||
use nu_errors::ShellError;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub trait Shell: completion::Completer + std::fmt::Debug {
|
||||
pub trait Shell: std::fmt::Debug {
|
||||
fn name(&self) -> String;
|
||||
fn homedir(&self) -> Option<PathBuf>;
|
||||
|
||||
|
@ -6,7 +6,6 @@ use crate::commands::ls::LsArgs;
|
||||
use crate::commands::mkdir::MkdirArgs;
|
||||
use crate::commands::move_::mv::Arguments as MvArgs;
|
||||
use crate::commands::rm::RemoveArgs;
|
||||
use crate::completion::{self, Completer};
|
||||
use crate::prelude::*;
|
||||
use crate::shell::filesystem_shell::FilesystemShell;
|
||||
use crate::shell::shell::Shell;
|
||||
@ -27,12 +26,10 @@ pub struct ShellManager {
|
||||
}
|
||||
|
||||
impl ShellManager {
|
||||
pub fn basic(commands: CommandRegistry) -> Result<ShellManager, Box<dyn Error>> {
|
||||
pub fn basic() -> Result<ShellManager, Box<dyn Error>> {
|
||||
Ok(ShellManager {
|
||||
current_shell: Arc::new(AtomicUsize::new(0)),
|
||||
shells: Arc::new(Mutex::new(vec![Box::new(FilesystemShell::basic(
|
||||
commands,
|
||||
)?)])),
|
||||
shells: Arc::new(Mutex::new(vec![Box::new(FilesystemShell::basic()?)])),
|
||||
})
|
||||
}
|
||||
|
||||
@ -181,24 +178,3 @@ impl ShellManager {
|
||||
shells[self.current_shell()].mv(args, name, &path)
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for ShellManager {
|
||||
fn complete(
|
||||
&self,
|
||||
line: &str,
|
||||
pos: usize,
|
||||
ctx: &completion::Context<'_>,
|
||||
) -> Result<(usize, Vec<completion::Suggestion>), ShellError> {
|
||||
self.shells.lock()[self.current_shell()].complete(line, pos, ctx)
|
||||
}
|
||||
|
||||
fn hint(
|
||||
&self,
|
||||
line: &str,
|
||||
pos: usize,
|
||||
ctx: &completion::Context<'_>,
|
||||
//context: ExpandContext,
|
||||
) -> Option<String> {
|
||||
self.shells.lock()[self.current_shell()].hint(line, pos, ctx)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
pub mod data;
|
||||
pub mod data_processing;
|
||||
pub mod test_bins;
|
||||
|
||||
use crate::path::canonicalize;
|
||||
|
274
crates/nu-cli/src/utils/data/internal.rs
Normal file
274
crates/nu-cli/src/utils/data/internal.rs
Normal file
@ -0,0 +1,274 @@
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
use crate::data::value::compute_values;
|
||||
use derive_new::new;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::hir::Operator;
|
||||
use nu_protocol::{UntaggedValue, Value};
|
||||
use nu_source::{SpannedItem, Tag, TaggedItem};
|
||||
use nu_value_ext::ValueExt;
|
||||
|
||||
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, new)]
|
||||
pub struct Labels {
|
||||
pub x: Vec<String>,
|
||||
pub y: Vec<String>,
|
||||
}
|
||||
|
||||
impl Labels {
|
||||
pub fn at(&self, idx: usize) -> Option<&str> {
|
||||
if let Some(k) = self.x.get(idx) {
|
||||
Some(&k[..])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn grouped(&self) -> impl Iterator<Item = &String> {
|
||||
self.x.iter()
|
||||
}
|
||||
|
||||
pub fn grouping_total(&self) -> Value {
|
||||
UntaggedValue::int(self.x.len()).into_untagged_value()
|
||||
}
|
||||
|
||||
pub fn splits(&self) -> impl Iterator<Item = &String> {
|
||||
self.y.iter()
|
||||
}
|
||||
|
||||
pub fn splits_total(&self) -> Value {
|
||||
UntaggedValue::int(self.y.len()).into_untagged_value()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, new)]
|
||||
pub struct Range {
|
||||
pub start: Value,
|
||||
pub end: Value,
|
||||
}
|
||||
|
||||
fn formula(
|
||||
acc_begin: Value,
|
||||
calculator: Box<dyn Fn(Vec<&Value>) -> Result<Value, ShellError> + Send + Sync + 'static>,
|
||||
) -> Box<dyn Fn(&Value, Vec<&Value>) -> Result<Value, ShellError> + Send + Sync + 'static> {
|
||||
Box::new(move |acc, datax| -> Result<Value, ShellError> {
|
||||
let result = match compute_values(Operator::Multiply, &acc, &acc_begin) {
|
||||
Ok(v) => v.into_untagged_value(),
|
||||
Err((left_type, right_type)) => {
|
||||
return Err(ShellError::coerce_error(
|
||||
left_type.spanned_unknown(),
|
||||
right_type.spanned_unknown(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
match calculator(datax) {
|
||||
Ok(total) => Ok(match compute_values(Operator::Plus, &result, &total) {
|
||||
Ok(v) => v.into_untagged_value(),
|
||||
Err((left_type, right_type)) => {
|
||||
return Err(ShellError::coerce_error(
|
||||
left_type.spanned_unknown(),
|
||||
right_type.spanned_unknown(),
|
||||
))
|
||||
}
|
||||
}),
|
||||
Err(reason) => Err(reason),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reducer_for(
|
||||
command: Reduction,
|
||||
) -> Box<dyn Fn(&Value, Vec<&Value>) -> Result<Value, ShellError> + Send + Sync + 'static> {
|
||||
match command {
|
||||
Reduction::Accumulate => Box::new(formula(
|
||||
UntaggedValue::int(1).into_untagged_value(),
|
||||
Box::new(sum),
|
||||
)),
|
||||
_ => Box::new(formula(
|
||||
UntaggedValue::int(0).into_untagged_value(),
|
||||
Box::new(sum),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max(values: &Value, tag: impl Into<Tag>) -> Result<&Value, ShellError> {
|
||||
let tag = tag.into();
|
||||
|
||||
values
|
||||
.table_entries()
|
||||
.filter_map(|dataset| dataset.table_entries().max())
|
||||
.max()
|
||||
.ok_or_else(|| ShellError::labeled_error("err", "err", &tag))
|
||||
}
|
||||
|
||||
pub fn sum(data: Vec<&Value>) -> Result<Value, ShellError> {
|
||||
let mut acc = UntaggedValue::int(0);
|
||||
|
||||
for value in data {
|
||||
match value.value {
|
||||
UntaggedValue::Primitive(_) => {
|
||||
acc = match compute_values(Operator::Plus, &acc, &value) {
|
||||
Ok(v) => v,
|
||||
Err((left_type, right_type)) => {
|
||||
return Err(ShellError::coerce_error(
|
||||
left_type.spanned_unknown(),
|
||||
right_type.spanned_unknown(),
|
||||
))
|
||||
}
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
return Err(ShellError::labeled_error(
|
||||
"Attempted to compute the sum of a value that cannot be summed.",
|
||||
"value appears here",
|
||||
value.tag.span,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(acc.into_untagged_value())
|
||||
}
|
||||
|
||||
pub fn sort_columns(
|
||||
values: &[String],
|
||||
format: &Option<Box<dyn Fn(&Value, String) -> Result<String, ShellError>>>,
|
||||
) -> Result<Vec<String>, ShellError> {
|
||||
let mut keys = vec![];
|
||||
|
||||
if let Some(fmt) = format {
|
||||
for k in values.iter() {
|
||||
let k = k.clone().tagged_unknown();
|
||||
let v =
|
||||
crate::data::value::Date::naive_from_str(k.borrow_tagged())?.into_untagged_value();
|
||||
keys.push(fmt(&v, k.to_string())?);
|
||||
}
|
||||
} else {
|
||||
keys = values.to_vec();
|
||||
}
|
||||
|
||||
keys.sort();
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
pub fn sort(planes: &Labels, values: &Value, tag: impl Into<Tag>) -> Result<Value, ShellError> {
|
||||
let tag = tag.into();
|
||||
|
||||
let mut x = vec![];
|
||||
for column in planes.splits() {
|
||||
let key = column.clone().tagged_unknown();
|
||||
let groups = values
|
||||
.get_data_by_key(key.borrow_spanned())
|
||||
.ok_or_else(|| {
|
||||
ShellError::labeled_error("unknown column", "unknown column", key.span())
|
||||
})?;
|
||||
|
||||
let mut y = vec![];
|
||||
for inner_column in planes.grouped() {
|
||||
let key = inner_column.clone().tagged_unknown();
|
||||
let grouped = groups.get_data_by_key(key.borrow_spanned());
|
||||
|
||||
if let Some(grouped) = grouped {
|
||||
y.push(grouped.table_entries().cloned().collect::<Vec<_>>());
|
||||
} else {
|
||||
let empty = UntaggedValue::table(&[]).into_value(&tag);
|
||||
y.push(empty.table_entries().cloned().collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
|
||||
x.push(
|
||||
UntaggedValue::table(&y.iter().cloned().flatten().collect::<Vec<Value>>())
|
||||
.into_value(&tag),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(UntaggedValue::table(&x).into_value(&tag))
|
||||
}
|
||||
|
||||
pub fn evaluate(
|
||||
values: &Value,
|
||||
evaluator: &Option<Box<dyn Fn(usize, &Value) -> Result<Value, ShellError> + Send>>,
|
||||
tag: impl Into<Tag>,
|
||||
) -> Result<Value, ShellError> {
|
||||
let tag = tag.into();
|
||||
|
||||
let mut x = vec![];
|
||||
for split in values.table_entries() {
|
||||
let mut y = vec![];
|
||||
|
||||
for (idx, subset) in split.table_entries().enumerate() {
|
||||
let mut set = vec![];
|
||||
|
||||
if let Some(ref evaluator) = evaluator {
|
||||
let value = evaluator(idx, subset)?;
|
||||
|
||||
set.push(value);
|
||||
} else {
|
||||
set.push(UntaggedValue::int(1).into_value(&tag));
|
||||
}
|
||||
|
||||
y.push(UntaggedValue::table(&set).into_value(&tag));
|
||||
}
|
||||
|
||||
x.push(UntaggedValue::table(&y).into_value(&tag));
|
||||
}
|
||||
|
||||
Ok(UntaggedValue::table(&x).into_value(&tag))
|
||||
}
|
||||
|
||||
pub enum Reduction {
|
||||
#[allow(dead_code)]
|
||||
Count,
|
||||
Accumulate,
|
||||
}
|
||||
|
||||
pub fn reduce(values: &Value, tag: impl Into<Tag>) -> Result<Value, ShellError> {
|
||||
let tag = tag.into();
|
||||
let reduce_with = reducer_for(Reduction::Accumulate);
|
||||
|
||||
let mut datasets = vec![];
|
||||
for dataset in values.table_entries() {
|
||||
let mut acc = UntaggedValue::int(0).into_value(&tag);
|
||||
|
||||
let mut subsets = vec![];
|
||||
for subset in dataset.table_entries() {
|
||||
acc = reduce_with(&acc, subset.table_entries().collect::<Vec<_>>())?;
|
||||
subsets.push(acc.clone());
|
||||
}
|
||||
datasets.push(UntaggedValue::table(&subsets).into_value(&tag));
|
||||
}
|
||||
|
||||
Ok(UntaggedValue::table(&datasets).into_value(&tag))
|
||||
}
|
||||
|
||||
pub fn percentages(
|
||||
maxima: &Value,
|
||||
values: &Value,
|
||||
tag: impl Into<Tag>,
|
||||
) -> Result<Value, ShellError> {
|
||||
let tag = tag.into();
|
||||
|
||||
let mut x = vec![];
|
||||
for split in values.table_entries() {
|
||||
x.push(
|
||||
UntaggedValue::table(
|
||||
&split
|
||||
.table_entries()
|
||||
.filter_map(|s| {
|
||||
let hundred = UntaggedValue::decimal(100);
|
||||
|
||||
match compute_values(Operator::Divide, &hundred, &maxima) {
|
||||
Ok(v) => match compute_values(Operator::Multiply, &s, &v) {
|
||||
Ok(v) => Some(v.into_untagged_value()),
|
||||
Err(_) => None,
|
||||
},
|
||||
Err(_) => None,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.into_value(&tag),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(UntaggedValue::table(&x).into_value(&tag))
|
||||
}
|
@ -1,5 +1,296 @@
|
||||
pub mod group;
|
||||
pub mod split;
|
||||
|
||||
mod internal;
|
||||
|
||||
pub use crate::utils::data::group::group;
|
||||
pub use crate::utils::data::split::split;
|
||||
|
||||
use crate::utils::data::internal::*;
|
||||
|
||||
use derive_new::new;
|
||||
use getset::Getters;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{UntaggedValue, Value};
|
||||
use nu_source::Tag;
|
||||
|
||||
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Getters, Clone, new)]
|
||||
pub struct Model {
|
||||
pub labels: Labels,
|
||||
pub ranges: (Range, Range),
|
||||
|
||||
pub data: Value,
|
||||
pub percentages: Value,
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct Operation<'a> {
|
||||
pub grouper: Option<Box<dyn Fn(usize, &Value) -> Result<String, ShellError> + Send>>,
|
||||
pub splitter: Option<Box<dyn Fn(usize, &Value) -> Result<String, ShellError> + Send>>,
|
||||
pub format: Option<Box<dyn Fn(&Value, String) -> Result<String, ShellError>>>,
|
||||
pub eval: &'a Option<Box<dyn Fn(usize, &Value) -> Result<Value, ShellError> + Send>>,
|
||||
}
|
||||
|
||||
pub fn report(
|
||||
values: &Value,
|
||||
options: Operation,
|
||||
tag: impl Into<Tag>,
|
||||
) -> Result<Model, ShellError> {
|
||||
let tag = tag.into();
|
||||
|
||||
let grouped = group(&values, &options.grouper, &tag)?;
|
||||
let splitted = split(&grouped, &options.splitter, &tag)?;
|
||||
|
||||
let x = grouped
|
||||
.row_entries()
|
||||
.map(|(key, _)| key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let x = if options.format.is_some() {
|
||||
sort_columns(&x, &options.format)
|
||||
} else {
|
||||
sort_columns(&x, &None)
|
||||
}?;
|
||||
|
||||
let mut y = splitted
|
||||
.row_entries()
|
||||
.map(|(key, _)| key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
y.sort();
|
||||
|
||||
let planes = Labels { x, y };
|
||||
let sorted = sort(&planes, &splitted, &tag)?;
|
||||
|
||||
let evaluated = evaluate(
|
||||
&sorted,
|
||||
if options.eval.is_some() {
|
||||
options.eval
|
||||
} else {
|
||||
&None
|
||||
},
|
||||
&tag,
|
||||
)?;
|
||||
|
||||
let group_labels = planes.grouping_total();
|
||||
|
||||
let reduced = reduce(&evaluated, &tag)?;
|
||||
|
||||
let max = max(&reduced, &tag)?.clone();
|
||||
let maxima = max.clone();
|
||||
|
||||
let percents = percentages(&maxima, &reduced, &tag)?;
|
||||
|
||||
Ok(Model {
|
||||
labels: planes,
|
||||
ranges: (
|
||||
Range {
|
||||
start: UntaggedValue::int(0).into_untagged_value(),
|
||||
end: group_labels,
|
||||
},
|
||||
Range {
|
||||
start: UntaggedValue::int(0).into_untagged_value(),
|
||||
end: max,
|
||||
},
|
||||
),
|
||||
data: reduced,
|
||||
percentages: percents,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod helpers {
|
||||
use super::{report, Labels, Model, Operation, Range};
|
||||
use bigdecimal::BigDecimal;
|
||||
use indexmap::indexmap;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{UntaggedValue, Value};
|
||||
use nu_source::{Tag, TaggedItem};
|
||||
use nu_value_ext::ValueExt;
|
||||
use num_bigint::BigInt;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
|
||||
pub fn int(s: impl Into<BigInt>) -> Value {
|
||||
UntaggedValue::int(s).into_untagged_value()
|
||||
}
|
||||
|
||||
pub fn decimal(f: impl Into<BigDecimal>) -> Value {
|
||||
UntaggedValue::decimal(f.into()).into_untagged_value()
|
||||
}
|
||||
|
||||
pub fn string(input: impl Into<String>) -> Value {
|
||||
UntaggedValue::string(input.into()).into_untagged_value()
|
||||
}
|
||||
|
||||
pub fn row(entries: IndexMap<String, Value>) -> Value {
|
||||
UntaggedValue::row(entries).into_untagged_value()
|
||||
}
|
||||
|
||||
pub fn table(list: &[Value]) -> Value {
|
||||
UntaggedValue::table(list).into_untagged_value()
|
||||
}
|
||||
|
||||
pub fn date(input: impl Into<String>) -> Value {
|
||||
let key = input.into().tagged_unknown();
|
||||
crate::data::value::Date::naive_from_str(key.borrow_tagged())
|
||||
.unwrap()
|
||||
.into_untagged_value()
|
||||
}
|
||||
|
||||
pub fn committers() -> Vec<Value> {
|
||||
vec![
|
||||
row(indexmap! {
|
||||
"date".into() => date("2019-07-23"),
|
||||
"name".into() => string("AR"),
|
||||
"country".into() => string("EC"),
|
||||
"chickens".into() => int(10),
|
||||
}),
|
||||
row(indexmap! {
|
||||
"date".into() => date("2019-07-23"),
|
||||
"name".into() => string("JT"),
|
||||
"country".into() => string("NZ"),
|
||||
"chickens".into() => int(5),
|
||||
}),
|
||||
row(indexmap! {
|
||||
"date".into() => date("2019-10-10"),
|
||||
"name".into() => string("YK"),
|
||||
"country".into() => string("US"),
|
||||
"chickens".into() => int(6),
|
||||
}),
|
||||
row(indexmap! {
|
||||
"date".into() => date("2019-09-24"),
|
||||
"name".into() => string("AR"),
|
||||
"country".into() => string("EC"),
|
||||
"chickens".into() => int(20),
|
||||
}),
|
||||
row(indexmap! {
|
||||
"date".into() => date("2019-10-10"),
|
||||
"name".into() => string("JT"),
|
||||
"country".into() => string("NZ"),
|
||||
"chickens".into() => int(15),
|
||||
}),
|
||||
row(indexmap! {
|
||||
"date".into() => date("2019-09-24"),
|
||||
"name".into() => string("YK"),
|
||||
"country".into() => string("US"),
|
||||
"chickens".into() => int(4),
|
||||
}),
|
||||
row(indexmap! {
|
||||
"date".into() => date("2019-10-10"),
|
||||
"name".into() => string("AR"),
|
||||
"country".into() => string("EC"),
|
||||
"chickens".into() => int(30),
|
||||
}),
|
||||
row(indexmap! {
|
||||
"date".into() => date("2019-09-24"),
|
||||
"name".into() => string("JT"),
|
||||
"country".into() => string("NZ"),
|
||||
"chickens".into() => int(10),
|
||||
}),
|
||||
row(indexmap! {
|
||||
"date".into() => date("2019-07-23"),
|
||||
"name".into() => string("YK"),
|
||||
"country".into() => string("US"),
|
||||
"chickens".into() => int(2),
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn committers_grouped_by_date() -> Value {
|
||||
let sample = table(&committers());
|
||||
|
||||
let grouper = Box::new(move |_, row: &Value| {
|
||||
let key = String::from("date").tagged_unknown();
|
||||
let group_key = row.get_data_by_key(key.borrow_spanned()).unwrap();
|
||||
|
||||
group_key.format("%Y-%m-%d")
|
||||
});
|
||||
|
||||
crate::utils::data::group(&sample, &Some(grouper), Tag::unknown()).unwrap()
|
||||
}
|
||||
|
||||
pub fn date_formatter(
|
||||
fmt: &'static str,
|
||||
) -> Box<dyn Fn(&Value, String) -> Result<String, ShellError>> {
|
||||
Box::new(move |date: &Value, _: String| date.format(&fmt))
|
||||
}
|
||||
|
||||
fn assert_without_checking_percentages(report_a: Model, report_b: Model) {
|
||||
assert_eq!(report_a.labels.x, report_b.labels.x);
|
||||
assert_eq!(report_a.labels.y, report_b.labels.y);
|
||||
assert_eq!(report_a.ranges, report_b.ranges);
|
||||
assert_eq!(report_a.data, report_b.data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepares_report_using_accumulating_value() {
|
||||
let committers = table(&committers());
|
||||
|
||||
let by_date = Box::new(move |_, row: &Value| {
|
||||
let key = String::from("date").tagged_unknown();
|
||||
let key = row.get_data_by_key(key.borrow_spanned()).unwrap();
|
||||
|
||||
let callback = date_formatter("%Y-%m-%d");
|
||||
callback(&key, "nothing".to_string())
|
||||
});
|
||||
|
||||
let by_country = Box::new(move |_, row: &Value| {
|
||||
let key = String::from("country").tagged_unknown();
|
||||
let key = row.get_data_by_key(key.borrow_spanned()).unwrap();
|
||||
nu_value_ext::as_string(&key)
|
||||
});
|
||||
|
||||
let options = Operation {
|
||||
grouper: Some(by_date),
|
||||
splitter: Some(by_country),
|
||||
format: Some(date_formatter("%Y-%m-%d")),
|
||||
eval: /* value to be used for accumulation */ &Some(Box::new(move |_, value: &Value| {
|
||||
let chickens_key = String::from("chickens").tagged_unknown();
|
||||
|
||||
value
|
||||
.get_data_by_key(chickens_key.borrow_spanned())
|
||||
.ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"unknown column",
|
||||
"unknown column",
|
||||
chickens_key.span(),
|
||||
)
|
||||
})
|
||||
})),
|
||||
};
|
||||
|
||||
assert_without_checking_percentages(
|
||||
report(&committers, options, Tag::unknown()).unwrap(),
|
||||
Model {
|
||||
labels: Labels {
|
||||
x: vec![
|
||||
String::from("2019-07-23"),
|
||||
String::from("2019-09-24"),
|
||||
String::from("2019-10-10"),
|
||||
],
|
||||
y: vec![String::from("EC"), String::from("NZ"), String::from("US")],
|
||||
},
|
||||
ranges: (
|
||||
Range {
|
||||
start: int(0),
|
||||
end: int(3),
|
||||
},
|
||||
Range {
|
||||
start: int(0),
|
||||
end: int(60),
|
||||
},
|
||||
),
|
||||
data: table(&[
|
||||
table(&[int(10), int(30), int(60)]),
|
||||
table(&[int(5), int(15), int(30)]),
|
||||
table(&[int(2), int(6), int(12)]),
|
||||
]),
|
||||
percentages: table(&[
|
||||
table(&[decimal(16.66), decimal(50), decimal(100)]),
|
||||
table(&[decimal(8.33), decimal(25), decimal(50)]),
|
||||
table(&[decimal(3.33), decimal(10), decimal(20)]),
|
||||
]),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user