Compare commits

...

60 Commits

Author SHA1 Message Date
87d71604ad Bump to 0.18.1 (#2335) 2020-08-12 15:59:28 +12:00
e372e7c448 Display built features. Long/Short commit hashes display removed due to cargo publishing. (#2333)
Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
Co-authored-by: Jonathan Turner <jonathandturner@users.noreply.github.com>

Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
Co-authored-by: Jonathan Turner <jonathandturner@users.noreply.github.com>
2020-08-12 15:13:33 +12:00
0194dee3a6 Updated version hashing and bumped nu-cli to 0.18.1 (#2331)
* Updated version hashing and bumped nu-cli to 0.18.1

* made code pertyer

* Update version.rs

* Update version.rs

Co-authored-by: Jonathan Turner <jonathandturner@users.noreply.github.com>
2020-08-12 09:43:23 +12:00
cc3c10867c Histogram no longer requires a wrap command before it on unnamed columns (#2332) 2020-08-12 09:42:59 +12:00
3c18169f63 Autoenv fix: Exitscripts incorrectly running when visiting a subdirectory (#2326)
* Add test case for issue

* Preliminary fix

* fmt

* Reorder asserts

* move insertion

* Touch nu-env.toml

* Cleanup

* touch nu-env toml

* Remove touch

* Change feature flags
2020-08-12 07:54:49 +12:00
43e9c89125 moved theme assets local to crate (#2329)
* moved theme assets local to crate

* remove the TODO comment
2020-08-11 13:57:03 -05:00
2ad07912d9 Bump to 0.18 (#2325) 2020-08-11 18:44:53 +12:00
51ad019495 Update Cargo.lock (#2322) 2020-08-11 15:00:07 +12:00
9264325e57 Make history file location configurable (#2320)
* Make history location configurable

Add history-path to your config if you want an alternate history file
location

* use IndexMap.get() instead of index

Co-authored-by: Amanita Muscaria <nope>
2020-08-11 13:58:53 +12:00
901157341b Bumped which-rs to 4.0.2 (#2321)
This should fix #1541
2020-08-11 13:55:43 +12:00
eb766b80c1 added pkg_mgr/winget + yaml (#2297) 2020-08-11 05:45:53 +12:00
f0dbffd761 Add 228 json html themes for to html (#2308)
* add 228 json html themes
removed old assets, added new zipped asset
added --list to get a list of the theme names
reworked some older theme code
added rust-embed and zip crate
removed the dark tests

* fmt

* Updated, removed excess comments
Changed usage a bit
Updated the error handling
Added some helper items in --list
2020-08-11 05:43:16 +12:00
f14c0df582 Allow disabling welcome message on launch (#2314)
* Implements #2313
2020-08-09 11:38:21 +12:00
362bb1bea3 Add commit hash to version command (#2312)
* Add commit to version command

* Replace unwrap with expect.
2020-08-08 17:39:34 +12:00
724b177c97 Sample variance and Sample standard deviation. (#2310) 2020-08-06 23:56:19 -05:00
50343f2d6a Add stderr back when using do -i (#2309)
* Add stderr back when using do -i

* Add stderr back when using do -i
2020-08-07 16:53:37 +12:00
3122525b96 removed rustyline config duplication (#2306)
* removed rustyline config duplication
set other rustyline defaults if line_editor section doesn't exist
updated keyseq_timeout to -1 if emacs mode is chosen

* change checking rustyline config to if lets

* removed some unneccessary code
2020-08-05 16:34:28 -05:00
8232c6f185 Update rustyline defaults (#2305)
Use rustyline defaults if no config exists for line_editor (for most options)
2020-08-05 13:05:13 -05:00
6202705eb6 parse most common date formats using dtparse crate (#2303)
* use dtparse crate to parse most common date formats

* use dtparse crate to parse most common date formats - cargo fmt
2020-08-05 12:44:52 +12:00
e1c5940b04 Add command "reduce" (#2292)
* initial

* fold working

* tests and cleanup

* change command to reduce, with fold flag

* move complex example to tests

* add --numbered flag
2020-08-05 05:16:19 +12:00
7f35bfc005 histogram: support regular values. (#2300) 2020-08-04 04:57:25 -05:00
c48c092125 String funcs - Contains and IndexOf (#2298)
* Contains and index of string functions

* Clippy and fmt
2020-08-04 18:36:51 +12:00
028fc9b9cd Data summarize reporting overhaul. (#2299)
Refactored out most of internal work for summarizing data opening
the door for generating charts from it. A model is introduced
to hold information needed for a summary, Histogram command is
an example of a partial usage. This is the beginning.

Removed implicit arithmetic traits on Value and Primitive to avoid
mixed types panics. The std operations traits can't fail and we
can't guarantee that. We can handle gracefully now since compute_values
was introduced after the parser changes four months ago. The handling
logic should be taken care of either explicitly or in compute_values.

The zero identity trait was also removed (and implementing this forced
us to also implement Add, Mult, etc)

Also: the `math` operations now remove in the output if a given column is not computable:

```
> ls | math sum
──────┬──────────
 size │ 150.9 KB
──────┴──────────
```
2020-08-03 17:47:19 -05:00
eeb9b4edcb Match cleanup (#2294)
* Delete unnecessary match

* Use `unwrap_or_else()`

* Whitespace was trim on file save

* Use `map_or_else()`

* Use a default to group all match arms with same output

* Clippy made me do it
2020-08-04 05:43:27 +12:00
3a7869b422 Switch to maintained app_dirs (#2293)
* Switch to maintained app_dirs

* Update app_dirs under old name
2020-08-04 05:41:57 +12:00
c48ea46c4f Match cleanup (#2290) 2020-08-02 18:34:33 -04:00
f33da33626 Add --partial to 'to html' (#2291) 2020-08-03 08:47:54 +12:00
a88f5c7ae7 Make str collect take an optional separator value (#2289)
* Make `str collect` take an optional separator value

* Make `str collect` take an optional separator value

* Add some tests

* Fix my tests
2020-08-02 19:29:29 +12:00
cda53b6cda Return incomplete parse from lite_parse (#2284)
* Move lite_parse tests into a submodule

* Have lite_parse return partial parses when error encountered.

Although a parse fails, we can generally still return what was successfully
parsed. This is useful, for example, when figuring out completions at some
cursor position, because we can map the cursor to something more structured
(e.g., cursor is at a flag name).
2020-08-02 06:39:55 +12:00
ee734873ba Fix no longer working histogram example (#2271)
* Fix no longer working histogram example

* Oops
2020-08-02 06:38:45 +12:00
9fb6f5cd09 Change f/full flag to l/long for ls and ps commands (#2283)
* Change `f`/`full` flag to `l`/`long` for `ls` and `ps` commands

* Fix a few more `--full` instances
2020-08-02 06:30:45 +12:00
4ef15b5f80 docs/alias: simplify the 'persistent' section, using --save (#2285)
All the workarounds using `config` aren't necessary anymore. Only `config path` is still of interest.
2020-08-01 08:11:26 -04:00
ba81278ffd Remove build.rs and nu-build (#2282) 2020-08-01 09:21:10 +12:00
10fbed3808 updated cmd builtin commands (#2266)
* updated cmd builtin commands

* removed cd, chdir, exit, prompt, rem

* remove more commands, what remains is useful

* cargo fmt is so finicky
2020-07-31 09:51:42 +12:00
16cfc36aec set default edit_mode to emacs instead of vi (#2278) 2020-07-30 14:59:20 -05:00
aca7f71737 🐛 Fix path command error messages (#2261). (#2276) 2020-07-31 06:51:14 +12:00
3282a509a9 Make insert take in a block (#2265)
* Make insert take in a block

* Add some tests
2020-07-30 16:58:54 +12:00
878b748a41 Add list output for to html (#2273) 2020-07-30 16:54:55 +12:00
18a4505b9b starts_with ends_with match functions for string (#2269) 2020-07-30 16:51:20 +12:00
26e77a4b05 Add url commands (#2274)
* scheme
* path
* query
* host
2020-07-30 08:56:56 +12:00
37f10cf273 Add two further path cmds - type and exists (#2264)
* Add two further path cmds - type and exists

* Update type.rs

Try a more universal directory

* Update type.rs

Co-authored-by: Jonathan Turner <jonathandturner@users.noreply.github.com>
2020-07-27 14:12:07 +12:00
5e0a9aecaa ltrim and rtrim for string (#2262)
* Trim string from left and right

* Move trim to folder

* fmt

* Clippy
2020-07-27 06:09:35 +12:00
7e2c627044 WIP: Path utility commands (#2255)
* Add new path commands

basename, expand and extension. Currently there is no real error
handling. expand returns the initial path if it didn't work, the others
return empty string

* Optionally apply to path
2020-07-26 07:29:15 +12:00
4347339e9a Make all bullet point items uppercase (#2257) 2020-07-26 06:15:12 +12:00
e66a8258ec add example to parse command, with row output (#2256) 2020-07-26 06:14:29 +12:00
e4b42b54ad Simplify NuCompleter. (#2254)
- Removing old code for dealing with escaping, since that has moved elsewhere.
- Eliminating some match statements in favour of result/option methods.
- Fix an issue where completing inside quotes could remove the quote at the
  beginning, if one already existed on the line but the replacement didn't have
  a quote at the beginning.
2020-07-25 10:41:14 -04:00
de18b9ca2c Match cleanup (#2248) 2020-07-25 08:40:35 -04:00
a77f0f7b41 to-xml.md documentation update (#2253)
* Update to-xml.md documentation to be consistent

* Capitalize bullet point items

* Add link to this document wthin `to.md`
2020-07-25 20:19:15 +12:00
6b31a006b8 Refactor all completion logic into NuCompleter (#2252)
* Refactor all completion logic into `NuCompleter`

This is the next step to improving completions. Previously, completion logic was
scattered about (`FilesystemShell`, `NuCompleter`, `Helper`, and `ShellManager`).
By unifying the core logic into a central location, it will be easier to take the
next steps in improving completion.

* Update context.rs

Co-authored-by: Jonathan Turner <jonathandturner@users.noreply.github.com>
2020-07-25 11:39:12 +12:00
2db4fe83d8 Remove unnecessary peekable iterator (#2251) 2020-07-24 12:06:12 -04:00
55a2f284d9 Add to xml command (#2141) (#2155) 2020-07-24 19:41:22 +12:00
2d3b1e090a Remove piping of stderr. (#2247)
In any other shell, stderr is inherited like normal, and only piped if you
request it explicitly (e.g., `2>/dev/null`). In the case of a command like
`fzf`, stderr is used for the interactive selection of files. By piping it,
something like

    fzf | xargs echo

does not work. By removing all stderr piping we eliminate this issue. We can
return later with a way to deal with stderr piping when an actual use case
arises.
2020-07-24 17:56:50 +12:00
ed0c1038e3 Step 1 for to html theme-ing (#2245)
* reworked theming step 1.
added theme parameter.
hard coded 4 themes

* forgot about blulocolight

* aarrrrg! test are in another place. fixed i think.
2020-07-23 13:21:58 -05:00
0c20282200 added documentation of available binding options (#2246)
straight from the rustyline source code
2020-07-23 13:13:06 -05:00
e71f44d26f if config file doesn't exist, set defaults. (#2244)
if line_editor in config doesn't exist, set defaults.
2020-07-23 08:27:45 -05:00
e3d7e46855 added defaults. fixed but of not loading history. (#2243) 2020-07-23 07:19:05 -05:00
9b35aae5e8 update sample configs (#2242)
* update sample configs

* change rustyline to line_editor
2020-07-23 06:49:25 -05:00
7e9f87c57f Expose all rustyline configuration points (#2238)
* Added all rustyline config points

* comments cleanup

* my good friend fmt keeps changing his mind
2020-07-23 09:43:52 +12:00
5d17b72852 update config documentation (#2178)
* update config documentation

* update config syntax

* update config syntax

* Update alias.md

Co-authored-by: Jonathan Turner <jonathandturner@users.noreply.github.com>
2020-07-23 09:42:04 +12:00
6b4634b293 Convert table of primitives to positional arguments for external cmd (#2232)
* Convert table of primitives to positional arguments for external cmd

* Multiple file test, fix for cococo
2020-07-23 09:41:34 +12:00
191 changed files with 6805 additions and 3686 deletions

View File

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

View File

@ -1 +0,0 @@
[build]

846
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,3 +0,0 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
nu_build::build()
}

View File

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

View File

@ -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(())
}

View File

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

Binary file not shown.

View File

@ -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")
}

View File

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

View File

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

View File

@ -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]))
}

View File

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

View File

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

View File

@ -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(())
}

View File

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

View File

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

View File

@ -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,
},
]

View File

@ -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,
}]
}

View File

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

View File

@ -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()) {

View File

@ -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, &registry);
let scope = raw_args.call_info.scope.clone();
@ -72,7 +74,26 @@ async fn do_(
},
input,
) = raw_args.process(&registry).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;

View File

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

View File

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

View File

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

View File

@ -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) }),
]),
})
);

View File

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

View File

@ -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(&registry).await?;
let (HistogramArgs { rest: mut columns }, input) = args.process(&registry).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)]

View File

@ -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(&registry).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, &registry));
let (InsertArgs { column, value }, input) = raw_args.process(&registry).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())
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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(),
})
}

View File

@ -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(&registry).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(),
_ => {

View File

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

View File

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

View File

@ -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(&registry).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",

View File

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

View 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(&registry).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 {})
}
}

View 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, &registry))
.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 {})
}
}

View 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(&registry).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())
}

View 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(&registry).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(),
})
}

View 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(&registry).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 {})
}
}

View 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())
}

View 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(&registry).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 {})
}
}

View 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, &registry));
let (reduce_args, mut input): (ReduceArgs, _) = raw_args.process(&registry).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 {})
}
}

View File

@ -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, &registry);

View File

@ -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())
}
}

View File

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

View File

@ -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")})

View File

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

View 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(&registry).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);
}
}

View 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(&registry).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);
}
}

View File

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

View 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(&registry).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);
}
}

View File

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

View 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(&registry).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);
}
}

View File

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

View File

@ -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(&registry).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);
}
}

View 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(&registry).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,
))
}
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View File

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

View File

@ -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(&registry).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(&regex_hm, &output_string);
} else if no_color {
setup_no_color_regexes(&mut regex_hm);
output_string = run_regexes(&regex_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(&regex_hm, &output_string);
} else if no_color {
setup_no_color_regexes(&mut regex_hm);
output_string = run_regexes(&regex_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,

View 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 &current.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 ",
&current.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(&registry).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 {})
}
}

View File

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

View 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, &registry))
.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 {})
}
}

View 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(&registry).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 {})
}
}

View 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())
}

View 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(&registry).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 {})
}
}

View 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(&registry).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 {})
}
}

View 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(&registry).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 {})
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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!(

View File

@ -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()?;

View File

@ -41,7 +41,6 @@ pub(crate) async fn evaluate_args(
.await?,
);
}
_ => {}
};
}

View File

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

View File

@ -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")]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
pub mod data;
pub mod data_processing;
pub mod test_bins;
use crate::path::canonicalize;

View 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))
}

View File

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