mirror of
https://github.com/nushell/nushell.git
synced 2025-07-01 07:00:37 +02:00
Compare commits
55 Commits
Author | SHA1 | Date | |
---|---|---|---|
bd6556eee1 | |||
18d988d4c8 | |||
0f7c723672 | |||
afce2fd0f9 | |||
4fd9974204 | |||
71615f77a7 | |||
9bc5022c9c | |||
552848b8b9 | |||
8ae8ebd107 | |||
473e9f9422 | |||
96985aa692 | |||
0961da406d | |||
84927d52b5 | |||
73312b506f | |||
c1bec3b443 | |||
c0be02a434 | |||
2ab8d035e6 | |||
24094acee9 | |||
0b2be52bb5 | |||
6a371802b4 | |||
29ccb9f5cd | |||
20ab125861 | |||
fb532f3f4e | |||
a29d52158e | |||
dc50e61f26 | |||
a2668e3327 | |||
e606407d79 | |||
5f4fae5b06 | |||
3687603799 | |||
643b532537 | |||
ed86b1fbe8 | |||
44a114111e | |||
812a76d588 | |||
e3be849c2a | |||
ba1b67c072 | |||
fa910b95b7 | |||
427bde83f7 | |||
7a0bc6bc46 | |||
c6da56949c | |||
5b398d2ed2 | |||
dcdfa2a866 | |||
9474fa1ea5 | |||
49a1385543 | |||
6427ea2331 | |||
3610baa227 | |||
4e201d20ca | |||
1fa21ff056 | |||
0bbd12e37f | |||
7df8fdfb28 | |||
6a39cd8546 | |||
dc3370b103 | |||
ac5ad45783 | |||
8ef5c47515 | |||
5b19bebe7d | |||
2c529cd849 |
@ -38,8 +38,8 @@ steps:
|
||||
if [ "$(uname)" == "Darwin" ]; then
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y --no-modify-path --default-toolchain "stable"
|
||||
export PATH=$HOME/.cargo/bin:$PATH
|
||||
rustup update
|
||||
fi
|
||||
rustup update
|
||||
rustc -Vv
|
||||
echo "##vso[task.prependpath]$HOME/.cargo/bin"
|
||||
rustup component add rustfmt
|
||||
|
14
.gitpod.Dockerfile
vendored
14
.gitpod.Dockerfile
vendored
@ -1,7 +1,9 @@
|
||||
FROM gitpod/workspace-full
|
||||
USER root
|
||||
RUN apt-get update && apt-get install -y libssl-dev \
|
||||
libxcb-composite0-dev \
|
||||
pkg-config \
|
||||
curl \
|
||||
rustc
|
||||
|
||||
USER gitpod
|
||||
|
||||
RUN sudo apt-get update && \
|
||||
sudo apt-get install -y \
|
||||
libssl-dev \
|
||||
libxcb-composite0-dev \
|
||||
pkg-config
|
||||
|
656
Cargo.lock
generated
656
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
121
Cargo.toml
121
Cargo.toml
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nu"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
description = "A shell for the GitHub era"
|
||||
license = "MIT"
|
||||
@ -10,6 +10,7 @@ default-run = "nu"
|
||||
repository = "https://github.com/nushell/nushell"
|
||||
homepage = "https://www.nushell.sh"
|
||||
documentation = "https://www.nushell.sh/book/"
|
||||
exclude = ["images"]
|
||||
|
||||
[workspace]
|
||||
|
||||
@ -39,25 +40,25 @@ members = [
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
nu-source = {version = "0.9.0", path = "./crates/nu-source"}
|
||||
nu-plugin = {version = "0.9.0", path = "./crates/nu-plugin"}
|
||||
nu-protocol = {version = "0.9.0", path = "./crates/nu-protocol"}
|
||||
nu-errors = {version = "0.9.0", path = "./crates/nu-errors"}
|
||||
nu-parser = {version = "0.9.0", path = "./crates/nu-parser"}
|
||||
nu-value-ext = {version = "0.9.0", path = "./crates/nu-value-ext"}
|
||||
nu_plugin_average = {version = "0.9.0", path = "./crates/nu_plugin_average", optional=true}
|
||||
nu_plugin_binaryview = {version = "0.9.0", path = "./crates/nu_plugin_binaryview", optional=true}
|
||||
nu_plugin_fetch = {version = "0.9.0", path = "./crates/nu_plugin_fetch", optional=true}
|
||||
nu_plugin_inc = {version = "0.9.0", path = "./crates/nu_plugin_inc", optional=true}
|
||||
nu_plugin_match = {version = "0.9.0", path = "./crates/nu_plugin_match", optional=true}
|
||||
nu_plugin_post = {version = "0.9.0", path = "./crates/nu_plugin_post", optional=true}
|
||||
nu_plugin_ps = {version = "0.9.0", path = "./crates/nu_plugin_ps", optional=true}
|
||||
nu_plugin_str = {version = "0.9.0", path = "./crates/nu_plugin_str", optional=true}
|
||||
nu_plugin_sum = {version = "0.9.0", path = "./crates/nu_plugin_sum", optional=true}
|
||||
nu_plugin_sys = {version = "0.9.0", path = "./crates/nu_plugin_sys", optional=true}
|
||||
nu_plugin_textview = {version = "0.9.0", path = "./crates/nu_plugin_textview", optional=true}
|
||||
nu_plugin_tree = {version = "0.9.0", path = "./crates/nu_plugin_tree", optional=true}
|
||||
nu-macros = { version = "0.9.0", path = "./crates/nu-macros" }
|
||||
nu-source = { version = "0.10.0", path = "./crates/nu-source" }
|
||||
nu-plugin = { version = "0.10.0", path = "./crates/nu-plugin" }
|
||||
nu-protocol = { version = "0.10.0", path = "./crates/nu-protocol" }
|
||||
nu-errors = { version = "0.10.0", path = "./crates/nu-errors" }
|
||||
nu-parser = { version = "0.10.0", path = "./crates/nu-parser" }
|
||||
nu-value-ext = { version = "0.10.0", path = "./crates/nu-value-ext" }
|
||||
nu_plugin_average = { version = "0.10.0", path = "./crates/nu_plugin_average", optional=true }
|
||||
nu_plugin_binaryview = { version = "0.10.0", path = "./crates/nu_plugin_binaryview", optional=true }
|
||||
nu_plugin_fetch = { version = "0.10.0", path = "./crates/nu_plugin_fetch", optional=true }
|
||||
nu_plugin_inc = { version = "0.10.0", path = "./crates/nu_plugin_inc", optional=true }
|
||||
nu_plugin_match = { version = "0.10.0", path = "./crates/nu_plugin_match", optional=true }
|
||||
nu_plugin_post = { version = "0.10.0", path = "./crates/nu_plugin_post", optional=true }
|
||||
nu_plugin_ps = { version = "0.10.0", path = "./crates/nu_plugin_ps", optional=true }
|
||||
nu_plugin_str = { version = "0.10.0", path = "./crates/nu_plugin_str", optional=true }
|
||||
nu_plugin_sum = { version = "0.10.0", path = "./crates/nu_plugin_sum", optional=true }
|
||||
nu_plugin_sys = { version = "0.10.0", path = "./crates/nu_plugin_sys", optional=true }
|
||||
nu_plugin_textview = { version = "0.10.0", path = "./crates/nu_plugin_textview", optional=true }
|
||||
nu_plugin_tree = { version = "0.10.0", path = "./crates/nu_plugin_tree", optional=true }
|
||||
nu-macros = { version = "0.10.0", path = "./crates/nu-macros" }
|
||||
|
||||
query_interface = "0.3.5"
|
||||
typetag = "0.1.4"
|
||||
@ -69,20 +70,20 @@ itertools = "0.8.2"
|
||||
ansi_term = "0.12.1"
|
||||
nom = "5.0.1"
|
||||
dunce = "1.0.0"
|
||||
indexmap = { version = "1.3.1", features = ["serde-1"] }
|
||||
indexmap = { version = "1.3.2", features = ["serde-1"] }
|
||||
byte-unit = "3.0.3"
|
||||
base64 = "0.11"
|
||||
futures-preview = { version = "=0.3.0-alpha.19", features = ["compat", "io-compat"] }
|
||||
async-stream = "0.1.2"
|
||||
futures_codec = "0.2.5"
|
||||
futures = { version = "0.3", features = ["compat", "io-compat"] }
|
||||
async-stream = "0.2"
|
||||
futures_codec = "0.4"
|
||||
num-traits = "0.2.11"
|
||||
term = "0.5.2"
|
||||
bytes = "0.4.12"
|
||||
log = "0.4.8"
|
||||
pretty_env_logger = "0.3.1"
|
||||
pretty_env_logger = "0.4.0"
|
||||
serde = { version = "1.0.104", features = ["derive"] }
|
||||
bson = { version = "0.14.0", features = ["decimal128"] }
|
||||
serde_json = "1.0.44"
|
||||
serde_json = "1.0.47"
|
||||
serde-hjson = "0.9.1"
|
||||
serde_yaml = "0.8"
|
||||
serde_bytes = "0.11.3"
|
||||
@ -96,7 +97,7 @@ git2 = { version = "0.11.0", default_features = false }
|
||||
dirs = "2.0.2"
|
||||
glob = "0.3.0"
|
||||
ctrlc = "3.1.3"
|
||||
roxmltree = "0.9.0"
|
||||
roxmltree = "0.9.1"
|
||||
nom_locate = "1.0.0"
|
||||
nom-tracable = "0.4.1"
|
||||
unicode-xid = "0.2.0"
|
||||
@ -109,7 +110,7 @@ ichwh = "0.3"
|
||||
textwrap = {version = "0.11.0", features = ["term_size"]}
|
||||
shellexpand = "1.1.1"
|
||||
pin-utils = "0.1.0-alpha.4"
|
||||
num-bigint = { version = "0.2.5", features = ["serde"] }
|
||||
num-bigint = { version = "0.2.6", features = ["serde"] }
|
||||
bigdecimal = { version = "0.1.0", features = ["serde"] }
|
||||
serde_urlencoded = "0.6.1"
|
||||
trash = "1.0.0"
|
||||
@ -118,22 +119,21 @@ cfg-if = "0.1"
|
||||
strip-ansi-escapes = "0.1.0"
|
||||
calamine = "0.16"
|
||||
umask = "0.1"
|
||||
futures-util = "0.3.1"
|
||||
futures-util = "0.3.4"
|
||||
termcolor = "1.1.0"
|
||||
natural = "0.3.0"
|
||||
parking_lot = "0.10.0"
|
||||
futures-timer = "1.0.2"
|
||||
meval = "0.2"
|
||||
|
||||
clipboard = {version = "0.5", optional = true }
|
||||
ptree = {version = "0.2" }
|
||||
starship = { version = "0.33.1", optional = true}
|
||||
heim = {version = "0.0.9", optional = true}
|
||||
battery = {version = "0.7.5", optional = true}
|
||||
starship = { version = "0.35.1", optional = true}
|
||||
syntect = {version = "3.2.0", optional = true }
|
||||
onig_sys = {version = "=69.1.0", optional = true }
|
||||
crossterm = {version = "0.14.2", optional = true}
|
||||
crossterm = {version = "0.16.0", optional = true}
|
||||
url = {version = "2.1.1", optional = true}
|
||||
semver = {version = "0.9.0", optional = true}
|
||||
filesize = "0.1.0"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
users = "0.9"
|
||||
@ -146,10 +146,10 @@ default = ["sys", "ps", "textview", "inc", "str"]
|
||||
stable = ["default", "starship-prompt", "binaryview", "match", "tree", "average", "sum", "post", "fetch", "clipboard"]
|
||||
|
||||
# Default
|
||||
sys = ["heim", "battery"]
|
||||
ps = ["heim"]
|
||||
textview = ["crossterm", "syntect", "onig_sys", "url"]
|
||||
inc = ["nu_plugin_inc"]
|
||||
textview = ["crossterm", "syntect", "onig_sys", "url", "nu_plugin_textview"]
|
||||
sys = ["nu_plugin_sys"]
|
||||
ps = ["nu_plugin_ps"]
|
||||
inc = ["semver", "nu_plugin_inc"]
|
||||
str = ["nu_plugin_str"]
|
||||
|
||||
# Stable
|
||||
@ -169,12 +169,12 @@ features = ["bundled", "blob"]
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "0.6.1"
|
||||
nu-test-support = { version = "0.9.0", path = "./crates/nu-test-support" }
|
||||
nu-test-support = { version = "0.10.0", path = "./crates/nu-test-support" }
|
||||
|
||||
[build-dependencies]
|
||||
toml = "0.5.6"
|
||||
serde = { version = "1.0.104", features = ["derive"] }
|
||||
nu-build = { version = "0.9.0", path = "./crates/nu-build" }
|
||||
nu-build = { version = "0.10.0", path = "./crates/nu-build" }
|
||||
|
||||
[lib]
|
||||
name = "nu"
|
||||
@ -196,6 +196,11 @@ name = "cococo"
|
||||
path = "crates/nu-test-support/src/bins/cococo.rs"
|
||||
required-features = ["test-bins"]
|
||||
|
||||
[[bin]]
|
||||
name = "nonu"
|
||||
path = "crates/nu-test-support/src/bins/nonu.rs"
|
||||
required-features = ["test-bins"]
|
||||
|
||||
# Core plugins that ship with `cargo install nu` by default
|
||||
# Currently, Cargo limits us to installing only one binary
|
||||
# unless we use [[bin]], so we use this as a workaround
|
||||
@ -224,6 +229,42 @@ name = "nu_plugin_core_sys"
|
||||
path = "src/plugins/nu_plugin_core_sys.rs"
|
||||
required-features = ["sys"]
|
||||
|
||||
# Stable plugins
|
||||
[[bin]]
|
||||
name = "nu_plugin_stable_average"
|
||||
path = "src/plugins/nu_plugin_stable_average.rs"
|
||||
required-features = ["average"]
|
||||
|
||||
[[bin]]
|
||||
name = "nu_plugin_stable_fetch"
|
||||
path = "src/plugins/nu_plugin_stable_fetch.rs"
|
||||
required-features = ["fetch"]
|
||||
|
||||
[[bin]]
|
||||
name = "nu_plugin_stable_binaryview"
|
||||
path = "src/plugins/nu_plugin_stable_binaryview.rs"
|
||||
required-features = ["binaryview"]
|
||||
|
||||
[[bin]]
|
||||
name = "nu_plugin_stable_match"
|
||||
path = "src/plugins/nu_plugin_stable_match.rs"
|
||||
required-features = ["match"]
|
||||
|
||||
[[bin]]
|
||||
name = "nu_plugin_stable_post"
|
||||
path = "src/plugins/nu_plugin_stable_post.rs"
|
||||
required-features = ["post"]
|
||||
|
||||
[[bin]]
|
||||
name = "nu_plugin_stable_sum"
|
||||
path = "src/plugins/nu_plugin_stable_sum.rs"
|
||||
required-features = ["sum"]
|
||||
|
||||
[[bin]]
|
||||
name = "nu_plugin_stable_tree"
|
||||
path = "src/plugins/nu_plugin_stable_tree.rs"
|
||||
required-features = ["tree"]
|
||||
|
||||
# Main nu binary
|
||||
[[bin]]
|
||||
name = "nu"
|
||||
|
@ -58,7 +58,7 @@ cargo install nu
|
||||
You can also build Nu yourself with all the bells and whistles (be sure to have installed the [dependencies](https://www.nushell.sh/book/en/installation.html#dependencies) for your platform), once you have checked out this repo with git:
|
||||
|
||||
```
|
||||
cargo build --all --features=stable
|
||||
cargo build --workspace --features=stable
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nu-build"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "Core build system for nushell"
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nu-errors"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "Core error subsystem for Nushell"
|
||||
@ -10,7 +10,7 @@ license = "MIT"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
nu-source = { path = "../nu-source", version = "0.9.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.10.0" }
|
||||
|
||||
ansi_term = "0.12.1"
|
||||
bigdecimal = { version = "0.1.0", features = ["serde"] }
|
||||
@ -29,4 +29,4 @@ toml = "0.5.5"
|
||||
serde_json = "1.0.44"
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = { version = "0.9.0", path = "../nu-build" }
|
||||
nu-build = { version = "0.10.0", path = "../nu-build" }
|
||||
|
@ -133,6 +133,10 @@ pub enum ArgumentError {
|
||||
MissingMandatoryPositional(String),
|
||||
/// A flag was found, and it should have been followed by a value, but no value was found
|
||||
MissingValueForName(String),
|
||||
/// An argument was found, but the command does not recognize it
|
||||
UnexpectedArgument(Spanned<String>),
|
||||
/// An flag was found, but the command does not recognize it
|
||||
UnexpectedFlag(Spanned<String>),
|
||||
/// A sequence of characters was found that was not syntactically valid (but would have
|
||||
/// been valid if the command was an external command)
|
||||
InvalidExternalWord,
|
||||
@ -146,6 +150,16 @@ impl PrettyDebug for ArgumentError {
|
||||
+ b::description(flag)
|
||||
+ b::description("` as mandatory flag")
|
||||
}
|
||||
ArgumentError::UnexpectedArgument(name) => {
|
||||
b::description("unexpected `")
|
||||
+ b::description(&name.item)
|
||||
+ b::description("` is not supported")
|
||||
}
|
||||
ArgumentError::UnexpectedFlag(name) => {
|
||||
b::description("unexpected `")
|
||||
+ b::description(&name.item)
|
||||
+ b::description("` is not supported")
|
||||
}
|
||||
ArgumentError::MissingMandatoryPositional(pos) => {
|
||||
b::description("missing `")
|
||||
+ b::description(pos)
|
||||
@ -452,6 +466,30 @@ impl ShellError {
|
||||
Severity::Error,
|
||||
"Invalid bare word for Nu command (did you intend to invoke an external command?)".to_string())
|
||||
.with_label(Label::new_primary(command.span)),
|
||||
ArgumentError::UnexpectedArgument(argument) => Diagnostic::new(
|
||||
Severity::Error,
|
||||
format!(
|
||||
"{} unexpected {}",
|
||||
Color::Cyan.paint(&command.item),
|
||||
Color::Green.bold().paint(&argument.item)
|
||||
),
|
||||
)
|
||||
.with_label(
|
||||
Label::new_primary(argument.span).with_message(
|
||||
format!("unexpected argument (try {} -h)", &command.item))
|
||||
),
|
||||
ArgumentError::UnexpectedFlag(flag) => Diagnostic::new(
|
||||
Severity::Error,
|
||||
format!(
|
||||
"{} unexpected {}",
|
||||
Color::Cyan.paint(&command.item),
|
||||
Color::Green.bold().paint(&flag.item)
|
||||
),
|
||||
)
|
||||
.with_label(
|
||||
Label::new_primary(flag.span).with_message(
|
||||
format!("unexpected flag (try {} -h)", &command.item))
|
||||
),
|
||||
ArgumentError::MissingMandatoryFlag(name) => Diagnostic::new(
|
||||
Severity::Error,
|
||||
format!(
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nu-macros"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "Core macros for building Nushell"
|
||||
@ -10,4 +10,4 @@ license = "MIT"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.9.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.10.0" }
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nu-parser"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "Core parser used in Nushell"
|
||||
@ -10,9 +10,9 @@ license = "MIT"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
nu-errors = { path = "../nu-errors", version = "0.9.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.9.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.9.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.10.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.10.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.10.0" }
|
||||
|
||||
pretty_env_logger = "0.3.1"
|
||||
pretty = "0.5.2"
|
||||
@ -41,7 +41,7 @@ enumflags2 = "0.6.2"
|
||||
pretty_assertions = "0.6.1"
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = { version = "0.9.0", path = "../nu-build" }
|
||||
nu-build = { version = "0.10.0", path = "../nu-build" }
|
||||
|
||||
[features]
|
||||
stable = []
|
||||
|
@ -14,6 +14,18 @@ impl ExternalArg {
|
||||
pub fn is_it(&self) -> bool {
|
||||
self.has("$it")
|
||||
}
|
||||
|
||||
pub fn is_nu(&self) -> bool {
|
||||
self.has("$nu")
|
||||
}
|
||||
|
||||
pub fn looks_like_it(&self) -> bool {
|
||||
self.arg.starts_with("$it") && (self.arg.starts_with("$it.") || self.is_it())
|
||||
}
|
||||
|
||||
pub fn looks_like_nu(&self) -> bool {
|
||||
self.arg.starts_with("$nu") && (self.arg.starts_with("$nu.") || self.is_nu())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for ExternalArg {
|
||||
@ -54,7 +66,11 @@ pub struct ExternalCommand {
|
||||
|
||||
impl ExternalCommand {
|
||||
pub fn has_it_argument(&self) -> bool {
|
||||
self.args.iter().any(|arg| arg.has("$it"))
|
||||
self.args.iter().any(|arg| arg.looks_like_it())
|
||||
}
|
||||
|
||||
pub fn has_nu_argument(&self) -> bool {
|
||||
self.args.iter().any(|arg| arg.looks_like_nu())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -204,6 +204,12 @@ pub struct SpannedExpression {
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl SpannedExpression {
|
||||
pub fn new(expr: Expression, span: Span) -> SpannedExpression {
|
||||
SpannedExpression { expr, span }
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for SpannedExpression {
|
||||
type Target = Expression;
|
||||
|
||||
|
@ -183,7 +183,11 @@ fn with_empty_context(source: &Text, callback: impl FnOnce(ExpandContext)) {
|
||||
SyntaxShape::Pattern,
|
||||
"a path to get the directory contents from",
|
||||
)
|
||||
.switch("full", "list all available columns for each entry"),
|
||||
.switch(
|
||||
"full",
|
||||
"list all available columns for each entry",
|
||||
Some('f'),
|
||||
),
|
||||
);
|
||||
|
||||
callback(ExpandContext::new(Box::new(registry), source, None))
|
||||
|
@ -862,7 +862,7 @@ pub fn delimited_brace(input: NomSpan) -> IResult<NomSpan, SpannedToken> {
|
||||
|
||||
Ok((
|
||||
input,
|
||||
TokenTreeBuilder::spanned_square(tokens.item, (left, right), tokens.span),
|
||||
TokenTreeBuilder::spanned_brace(tokens.item, (left, right), tokens.span),
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -361,9 +361,24 @@ impl SpannedToken {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_flag(&self, value: &str, source: &Text) -> Option<Flag> {
|
||||
pub(crate) fn as_flag(&self, value: &str, short: Option<char>, source: &Text) -> Option<Flag> {
|
||||
match self.unspanned() {
|
||||
Token::Flag(flag @ Flag { .. }) if value == flag.name().slice(source) => Some(*flag),
|
||||
Token::Flag(flag) => {
|
||||
let name = flag.name().slice(source);
|
||||
|
||||
match flag.kind {
|
||||
FlagKind::Longhand if value == name => Some(*flag),
|
||||
FlagKind::Shorthand => {
|
||||
if let Some(short_hand) = short {
|
||||
if short_hand.to_string() == name {
|
||||
return Some(*flag);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,11 @@ use crate::hir::syntax_shape::{
|
||||
BackoffColoringMode, ExpandSyntax, MaybeSpaceShape, MaybeWhitespaceEof,
|
||||
};
|
||||
use crate::hir::SpannedExpression;
|
||||
use crate::TokensIterator;
|
||||
use crate::{
|
||||
hir::{self, NamedArguments},
|
||||
Flag,
|
||||
};
|
||||
use crate::{Token, TokensIterator};
|
||||
use log::trace;
|
||||
use nu_errors::{ArgumentError, ParseError};
|
||||
use nu_protocol::{NamedType, PositionalType, Signature, SyntaxShape};
|
||||
@ -30,23 +30,24 @@ pub fn parse_command_tail(
|
||||
for (name, kind) in &config.named {
|
||||
trace!(target: "nu::parse::trace_remaining", "looking for {} : {:?}", name, kind);
|
||||
|
||||
tail.move_to(0);
|
||||
|
||||
match &kind.0 {
|
||||
NamedType::Switch => {
|
||||
let switch = extract_switch(name, tail);
|
||||
NamedType::Switch(s) => {
|
||||
let switch = extract_switch(name, *s, tail);
|
||||
|
||||
match switch {
|
||||
None => named.insert_switch(name, None),
|
||||
Some((_, flag)) => {
|
||||
Some((pos, flag)) => {
|
||||
named.insert_switch(name, Some(*flag));
|
||||
rest_signature.remove_named(name);
|
||||
tail.color_shape(flag.color(flag.span));
|
||||
tail.move_to(pos);
|
||||
tail.expand_infallible(MaybeSpaceShape);
|
||||
tail.move_to(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
NamedType::Mandatory(syntax_type) => {
|
||||
match extract_mandatory(config, name, tail, command_span) {
|
||||
NamedType::Mandatory(s, syntax_type) => {
|
||||
match extract_mandatory(config, name, *s, tail, command_span) {
|
||||
Err(err) => {
|
||||
// remember this error, but continue coloring
|
||||
found_error = Some(err);
|
||||
@ -71,8 +72,8 @@ pub fn parse_command_tail(
|
||||
}
|
||||
}
|
||||
}
|
||||
NamedType::Optional(syntax_type) => {
|
||||
match extract_optional(name, tail) {
|
||||
NamedType::Optional(s, syntax_type) => {
|
||||
match extract_optional(name, *s, tail) {
|
||||
Err(err) => {
|
||||
// remember this error, but continue coloring
|
||||
found_error = Some(err);
|
||||
@ -86,6 +87,7 @@ pub fn parse_command_tail(
|
||||
Ok(expr) => {
|
||||
named.insert_optional(name, Some(expr));
|
||||
rest_signature.remove_named(name);
|
||||
tail.move_to(pos);
|
||||
}
|
||||
Err(_) => {
|
||||
found_error = Some(ParseError::argument_error(
|
||||
@ -148,6 +150,15 @@ pub fn parse_command_tail(
|
||||
positional.extend(out);
|
||||
}
|
||||
|
||||
trace_remaining("after rest", &tail);
|
||||
|
||||
if found_error.is_none() {
|
||||
if let Some(unexpected_argument_error) = find_unexpected_tokens(config, tail, command_span)
|
||||
{
|
||||
found_error = Some(unexpected_argument_error);
|
||||
}
|
||||
}
|
||||
|
||||
eat_any_whitespace(tail);
|
||||
|
||||
// Consume any remaining tokens with backoff coloring mode
|
||||
@ -157,12 +168,6 @@ pub fn parse_command_tail(
|
||||
// this solution.
|
||||
tail.sort_shapes();
|
||||
|
||||
if let Some(err) = found_error {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
trace_remaining("after rest", &tail);
|
||||
|
||||
trace!(target: "nu::parse::trace_remaining", "Constructed positional={:?} named={:?}", positional, named);
|
||||
|
||||
let positional = if positional.is_empty() {
|
||||
@ -171,8 +176,6 @@ pub fn parse_command_tail(
|
||||
Some(positional)
|
||||
};
|
||||
|
||||
// TODO: Error if extra unconsumed positional arguments
|
||||
|
||||
let named = if named.named.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@ -181,6 +184,10 @@ pub fn parse_command_tail(
|
||||
|
||||
trace!(target: "nu::parse::trace_remaining", "Normalized positional={:?} named={:?}", positional, named);
|
||||
|
||||
if let Some(err) = found_error {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
Ok(Some((positional, named)))
|
||||
}
|
||||
|
||||
@ -192,6 +199,8 @@ pub fn continue_parsing_positionals(
|
||||
) -> Result<Vec<SpannedExpression>, ParseError> {
|
||||
let mut positional = vec![];
|
||||
|
||||
eat_any_whitespace(tail);
|
||||
|
||||
for arg in &config.positional {
|
||||
trace!(target: "nu::parse::trace_remaining", "Processing positional {:?}", arg);
|
||||
|
||||
@ -270,20 +279,36 @@ fn expand_spaced_expr<
|
||||
|
||||
fn extract_switch(
|
||||
name: &str,
|
||||
short: Option<char>,
|
||||
tokens: &mut hir::TokensIterator<'_>,
|
||||
) -> Option<(usize, Spanned<Flag>)> {
|
||||
let source = tokens.source();
|
||||
tokens.extract(|t| t.as_flag(name, &source).map(|flag| flag.spanned(t.span())))
|
||||
let switch = tokens.extract(|t| {
|
||||
t.as_flag(name, short, &source)
|
||||
.map(|flag| flag.spanned(t.span()))
|
||||
});
|
||||
|
||||
match switch {
|
||||
None => None,
|
||||
Some((pos, flag)) => {
|
||||
tokens.remove(pos);
|
||||
Some((pos, flag))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_mandatory(
|
||||
config: &Signature,
|
||||
name: &str,
|
||||
short: Option<char>,
|
||||
tokens: &mut hir::TokensIterator<'_>,
|
||||
span: Span,
|
||||
) -> Result<(usize, Spanned<Flag>), ParseError> {
|
||||
let source = tokens.source();
|
||||
let flag = tokens.extract(|t| t.as_flag(name, &source).map(|flag| flag.spanned(t.span())));
|
||||
let flag = tokens.extract(|t| {
|
||||
t.as_flag(name, short, &source)
|
||||
.map(|flag| flag.spanned(t.span()))
|
||||
});
|
||||
|
||||
match flag {
|
||||
None => Err(ParseError::argument_error(
|
||||
@ -300,10 +325,14 @@ fn extract_mandatory(
|
||||
|
||||
fn extract_optional(
|
||||
name: &str,
|
||||
short: Option<char>,
|
||||
tokens: &mut hir::TokensIterator<'_>,
|
||||
) -> Result<Option<(usize, Spanned<Flag>)>, ParseError> {
|
||||
let source = tokens.source();
|
||||
let flag = tokens.extract(|t| t.as_flag(name, &source).map(|flag| flag.spanned(t.span())));
|
||||
let flag = tokens.extract(|t| {
|
||||
t.as_flag(name, short, &source)
|
||||
.map(|flag| flag.spanned(t.span()))
|
||||
});
|
||||
|
||||
match flag {
|
||||
None => Ok(None),
|
||||
@ -314,6 +343,48 @@ fn extract_optional(
|
||||
}
|
||||
}
|
||||
|
||||
fn find_unexpected_tokens(
|
||||
config: &Signature,
|
||||
tail: &hir::TokensIterator,
|
||||
command_span: Span,
|
||||
) -> Option<ParseError> {
|
||||
let mut tokens = tail.clone();
|
||||
let source = tail.source();
|
||||
|
||||
loop {
|
||||
tokens.move_to(0);
|
||||
|
||||
if let Some(node) = tokens.peek().commit() {
|
||||
match &node.unspanned() {
|
||||
Token::Whitespace => {}
|
||||
Token::Flag { .. } => {
|
||||
return Some(ParseError::argument_error(
|
||||
config.name.clone().spanned(command_span),
|
||||
ArgumentError::UnexpectedFlag(Spanned {
|
||||
item: node.span().slice(&source).to_string(),
|
||||
span: node.span(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
return Some(ParseError::argument_error(
|
||||
config.name.clone().spanned(command_span),
|
||||
ArgumentError::UnexpectedArgument(Spanned {
|
||||
item: node.span().slice(&source).to_string(),
|
||||
span: node.span(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tokens.at_end() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn trace_remaining(desc: &'static str, tail: &hir::TokensIterator<'_>) {
|
||||
let offset = tail.clone().span_at_cursor();
|
||||
let source = tail.source();
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nu-plugin"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "Nushell Plugin"
|
||||
@ -10,10 +10,10 @@ license = "MIT"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.9.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.9.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.9.0" }
|
||||
nu-value-ext = { path = "../nu-value-ext", version = "0.9.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.10.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.10.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.10.0" }
|
||||
nu-value-ext = { path = "../nu-value-ext", version = "0.10.0" }
|
||||
|
||||
indexmap = { version = "1.3.0", features = ["serde-1"] }
|
||||
serde = { version = "1.0.103", features = ["derive"] }
|
||||
@ -21,4 +21,4 @@ num-bigint = { version = "0.2.3", features = ["serde"] }
|
||||
serde_json = "1.0.44"
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = { version = "0.9.0", path = "../nu-build" }
|
||||
nu-build = { version = "0.10.0", path = "../nu-build" }
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nu-protocol"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "Core values and protocols for Nushell"
|
||||
@ -10,8 +10,8 @@ license = "MIT"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
nu-source = { path = "../nu-source", version = "0.9.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.9.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.10.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.10.0" }
|
||||
|
||||
serde = { version = "1.0.103", features = ["derive"] }
|
||||
indexmap = { version = "1.3.0", features = ["serde-1"] }
|
||||
@ -38,4 +38,4 @@ toml = "0.5.5"
|
||||
serde_json = "1.0.44"
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = { version = "0.9.0", path = "../nu-build" }
|
||||
nu-build = { version = "0.10.0", path = "../nu-build" }
|
||||
|
@ -7,18 +7,28 @@ use serde::{Deserialize, Serialize};
|
||||
/// The types of named parameter that a command can have
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub enum NamedType {
|
||||
/// A flag without any associated argument. eg) `foo --bar`
|
||||
Switch,
|
||||
/// A mandatory flag, with associated argument. eg) `foo --required xyz`
|
||||
Mandatory(SyntaxShape),
|
||||
/// An optional flag, with associated argument. eg) `foo --optional abc`
|
||||
Optional(SyntaxShape),
|
||||
/// A flag without any associated argument. eg) `foo --bar, foo -b`
|
||||
Switch(Option<char>),
|
||||
/// A mandatory flag, with associated argument. eg) `foo --required xyz, foo -r xyz`
|
||||
Mandatory(Option<char>, SyntaxShape),
|
||||
/// An optional flag, with associated argument. eg) `foo --optional abc, foo -o abc`
|
||||
Optional(Option<char>, SyntaxShape),
|
||||
}
|
||||
|
||||
impl NamedType {
|
||||
pub fn get_short(&self) -> Option<char> {
|
||||
match self {
|
||||
NamedType::Switch(s) => *s,
|
||||
NamedType::Mandatory(s, _) => *s,
|
||||
NamedType::Optional(s, _) => *s,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of positional arguments
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum PositionalType {
|
||||
/// A mandatory postional argument with the expected shape of the value
|
||||
/// A mandatory positional argument with the expected shape of the value
|
||||
Mandatory(String, SyntaxShape),
|
||||
/// An optional positional argument with the expected shape of the value
|
||||
Optional(String, SyntaxShape),
|
||||
@ -120,7 +130,10 @@ impl Signature {
|
||||
pub fn allowed(&self) -> Vec<String> {
|
||||
let mut allowed = indexmap::IndexSet::new();
|
||||
|
||||
for (name, _) in &self.named {
|
||||
for (name, (t, _)) in &self.named {
|
||||
if let Some(c) = t.get_short() {
|
||||
allowed.insert(format!("-{}", c));
|
||||
}
|
||||
allowed.insert(format!("--{}", name));
|
||||
}
|
||||
|
||||
@ -157,14 +170,14 @@ impl PrettyDebugWithSource for Signature {
|
||||
}
|
||||
|
||||
impl Signature {
|
||||
/// Create a new command signagure with the given name
|
||||
/// Create a new command signature with the given name
|
||||
pub fn new(name: impl Into<String>) -> Signature {
|
||||
Signature {
|
||||
name: name.into(),
|
||||
usage: String::new(),
|
||||
positional: vec![],
|
||||
rest_positional: None,
|
||||
named: indexmap::indexmap! {"help".into() => (NamedType::Switch, "Display this help message".into())},
|
||||
named: indexmap::indexmap! {"help".into() => (NamedType::Switch(Some('h')), "Display this help message".into())},
|
||||
is_filter: false,
|
||||
yields: None,
|
||||
input: None,
|
||||
@ -218,9 +231,16 @@ impl Signature {
|
||||
name: impl Into<String>,
|
||||
ty: impl Into<SyntaxShape>,
|
||||
desc: impl Into<String>,
|
||||
short: Option<char>,
|
||||
) -> Signature {
|
||||
self.named
|
||||
.insert(name.into(), (NamedType::Optional(ty.into()), desc.into()));
|
||||
let s = short.and_then(|c| {
|
||||
debug_assert!(!self.get_shorts().contains(&c));
|
||||
Some(c)
|
||||
});
|
||||
self.named.insert(
|
||||
name.into(),
|
||||
(NamedType::Optional(s, ty.into()), desc.into()),
|
||||
);
|
||||
|
||||
self
|
||||
}
|
||||
@ -231,17 +251,35 @@ impl Signature {
|
||||
name: impl Into<String>,
|
||||
ty: impl Into<SyntaxShape>,
|
||||
desc: impl Into<String>,
|
||||
short: Option<char>,
|
||||
) -> Signature {
|
||||
self.named
|
||||
.insert(name.into(), (NamedType::Mandatory(ty.into()), desc.into()));
|
||||
let s = short.and_then(|c| {
|
||||
debug_assert!(!self.get_shorts().contains(&c));
|
||||
Some(c)
|
||||
});
|
||||
|
||||
self.named.insert(
|
||||
name.into(),
|
||||
(NamedType::Mandatory(s, ty.into()), desc.into()),
|
||||
);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a switch to the signature
|
||||
pub fn switch(mut self, name: impl Into<String>, desc: impl Into<String>) -> Signature {
|
||||
pub fn switch(
|
||||
mut self,
|
||||
name: impl Into<String>,
|
||||
desc: impl Into<String>,
|
||||
short: Option<char>,
|
||||
) -> Signature {
|
||||
let s = short.and_then(|c| {
|
||||
debug_assert!(!self.get_shorts().contains(&c));
|
||||
Some(c)
|
||||
});
|
||||
|
||||
self.named
|
||||
.insert(name.into(), (NamedType::Switch, desc.into()));
|
||||
.insert(name.into(), (NamedType::Switch(s), desc.into()));
|
||||
self
|
||||
}
|
||||
|
||||
@ -268,4 +306,15 @@ impl Signature {
|
||||
self.input = Some(ty);
|
||||
self
|
||||
}
|
||||
|
||||
/// Get list of the short-hand flags
|
||||
pub fn get_shorts(&self) -> Vec<char> {
|
||||
let mut shorts = Vec::new();
|
||||
for (_, (t, _)) in &self.named {
|
||||
if let Some(c) = t.get_short() {
|
||||
shorts.push(c);
|
||||
}
|
||||
}
|
||||
shorts
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ use crate::value::primitive::Primitive;
|
||||
use crate::value::range::{Range, RangeInclusion};
|
||||
use crate::{ColumnPath, PathMember};
|
||||
use bigdecimal::BigDecimal;
|
||||
use chrono::{DateTime, Utc};
|
||||
use indexmap::IndexMap;
|
||||
use nu_errors::ShellError;
|
||||
use nu_source::{AnchorLocation, HasSpan, Span, Spanned, Tag};
|
||||
@ -201,6 +202,10 @@ impl UntaggedValue {
|
||||
UntaggedValue::Primitive(Primitive::Date(s.into()))
|
||||
}
|
||||
|
||||
pub fn date(d: impl Into<DateTime<Utc>>) -> UntaggedValue {
|
||||
UntaggedValue::Primitive(Primitive::Date(d.into()))
|
||||
}
|
||||
|
||||
/// Helper for creating the Nothing value
|
||||
pub fn nothing() -> UntaggedValue {
|
||||
UntaggedValue::Primitive(Primitive::Nothing)
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nu-source"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "A source string characterizer for Nushell"
|
||||
@ -20,4 +20,4 @@ termcolor = "1.0.5"
|
||||
pretty = "0.5.2"
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = { version = "0.9.0", path = "../nu-build" }
|
||||
nu-build = { version = "0.10.0", path = "../nu-build" }
|
||||
|
@ -185,18 +185,22 @@ impl<T> Tagged<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new `Tag` from the current `Tag`
|
||||
pub fn tag(&self) -> Tag {
|
||||
self.tag.clone()
|
||||
}
|
||||
|
||||
/// Retrieve the `Span` for the current `Tag`.
|
||||
pub fn span(&self) -> Span {
|
||||
self.tag.span
|
||||
}
|
||||
|
||||
/// Returns the `AnchorLocation` of the `Tag` if there is one.
|
||||
pub fn anchor(&self) -> Option<AnchorLocation> {
|
||||
self.tag.anchor.clone()
|
||||
}
|
||||
|
||||
/// Returns the underlying `AnchorLocation` variant type as a string.
|
||||
pub fn anchor_name(&self) -> Option<String> {
|
||||
match self.tag.anchor {
|
||||
Some(AnchorLocation::File(ref file)) => Some(file.clone()),
|
||||
@ -205,10 +209,12 @@ impl<T> Tagged<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the current `Tag`'s item.
|
||||
pub fn item(&self) -> &T {
|
||||
&self.item
|
||||
}
|
||||
|
||||
/// Returns a tuple of the `Tagged` item and `Tag`.
|
||||
pub fn into_parts(self) -> (T, Tag) {
|
||||
(self.item, self.tag)
|
||||
}
|
||||
@ -256,7 +262,18 @@ impl From<&std::ops::Range<usize>> for Span {
|
||||
|
||||
/// The set of metadata that can be associated with a value
|
||||
#[derive(
|
||||
Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize, Hash, Getters, new,
|
||||
Debug,
|
||||
Default,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Ord,
|
||||
PartialOrd,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Hash,
|
||||
Getters,
|
||||
new,
|
||||
)]
|
||||
pub struct Tag {
|
||||
/// The original source for this value
|
||||
@ -329,10 +346,20 @@ impl From<&Tag> for Span {
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
/// Creates a default `Tag' with unknown `Span` position and no `AnchorLocation`
|
||||
pub fn default() -> Self {
|
||||
Tag {
|
||||
anchor: None,
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `Tag` from the given `Span` with no `AnchorLocation`
|
||||
pub fn unknown_anchor(span: Span) -> Tag {
|
||||
Tag { anchor: None, span }
|
||||
}
|
||||
|
||||
/// Creates a `Tag` from the given `AnchorLocation` for a span with a length of 1.
|
||||
pub fn for_char(pos: usize, anchor: AnchorLocation) -> Tag {
|
||||
Tag {
|
||||
anchor: Some(anchor),
|
||||
@ -340,6 +367,7 @@ impl Tag {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `Tag` for the given `AnchorLocatrion` with unknown `Span` position.
|
||||
pub fn unknown_span(anchor: AnchorLocation) -> Tag {
|
||||
Tag {
|
||||
anchor: Some(anchor),
|
||||
@ -347,6 +375,7 @@ impl Tag {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `Tag` with no `AnchorLocation` and an unknown `Span` position.
|
||||
pub fn unknown() -> Tag {
|
||||
Tag {
|
||||
anchor: None,
|
||||
@ -354,10 +383,15 @@ impl Tag {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the `AnchorLocation` of the current `Tag`
|
||||
pub fn anchor(&self) -> Option<AnchorLocation> {
|
||||
self.anchor.clone()
|
||||
}
|
||||
|
||||
// Merges the current `Tag` with the given `Tag`.
|
||||
///
|
||||
/// Both Tags must share the same `AnchorLocation`.
|
||||
// The resulting `Tag` will have a `Span` that starts from the current `Tag` and ends at `Span` of the given `Tag`.
|
||||
pub fn until(&self, other: impl Into<Tag>) -> Tag {
|
||||
let other = other.into();
|
||||
debug_assert!(
|
||||
@ -371,6 +405,11 @@ impl Tag {
|
||||
}
|
||||
}
|
||||
|
||||
/// Merges the current `Tag` with the given optional `Tag`.
|
||||
///
|
||||
/// Both `Tag`s must share the same `AnchorLocation`.
|
||||
/// The resulting `Tag` will have a `Span` that starts from the current `Tag` and ends at `Span` of the given `Tag`.
|
||||
/// Should the `None` variant be passed in, a new `Tag` with the same `Span` and `Anchorlocation` will be returned.
|
||||
pub fn until_option(&self, other: Option<impl Into<Tag>>) -> Tag {
|
||||
match other {
|
||||
Some(other) => {
|
||||
@ -446,7 +485,13 @@ pub fn span_for_spanned_list(mut iter: impl Iterator<Item = Span>) -> Span {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize, Hash)]
|
||||
/// A `Span` is metadata which indicates the start and end positions.
|
||||
///
|
||||
/// `Span`s are combined with `AnchorLocation`s to form another type of metadata, a `Tag`.
|
||||
/// A `Span`'s end position must be greater than or equal to its start position.
|
||||
#[derive(
|
||||
Debug, Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize, Hash,
|
||||
)]
|
||||
pub struct Span {
|
||||
start: usize,
|
||||
end: usize,
|
||||
@ -468,10 +513,17 @@ impl From<Option<Span>> for Span {
|
||||
}
|
||||
|
||||
impl Span {
|
||||
/// Creates a default new `Span` that has 0 start and 0 end.
|
||||
pub fn default() -> Self {
|
||||
Span::unknown()
|
||||
}
|
||||
|
||||
/// Creates a new `Span` that has 0 start and 0 end.
|
||||
pub fn unknown() -> Span {
|
||||
Span::new(0, 0)
|
||||
}
|
||||
|
||||
/// Creates a new `Span` from start and end inputs. The end parameter must be greater than or equal to the start parameter.
|
||||
pub fn new(start: usize, end: usize) -> Span {
|
||||
assert!(
|
||||
end >= start,
|
||||
@ -483,6 +535,16 @@ impl Span {
|
||||
Span { start, end }
|
||||
}
|
||||
|
||||
/// Creates a `Span` with a length of 1 from the given position.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// let char_span = Span::for_char(5);
|
||||
///
|
||||
/// assert_eq!(char_span.start(), 5);
|
||||
/// assert_eq!(char_span.end(), 6);
|
||||
/// ```
|
||||
pub fn for_char(pos: usize) -> Span {
|
||||
Span {
|
||||
start: pos,
|
||||
@ -490,22 +552,64 @@ impl Span {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a bool indicating if the given position falls inside the current `Span`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// let span = Span::new(2, 8);
|
||||
///
|
||||
/// assert_eq!(span.contains(5), true);
|
||||
/// assert_eq!(span.contains(100), false);
|
||||
/// ```
|
||||
pub fn contains(&self, pos: usize) -> bool {
|
||||
self.start <= pos && self.end >= pos
|
||||
}
|
||||
|
||||
/// Returns a new Span by merging an earlier Span with the current Span.
|
||||
///
|
||||
/// The resulting Span will have the same start position as the given Span and same end as the current Span.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// let original_span = Span::new(4, 6);
|
||||
/// let earlier_span = Span::new(1, 3);
|
||||
/// let merged_span = origin_span.since(earlier_span);
|
||||
///
|
||||
/// assert_eq!(merged_span.start(), 1);
|
||||
/// assert_eq!(merged_span.end(), 6);
|
||||
/// ```
|
||||
pub fn since(&self, other: impl Into<Span>) -> Span {
|
||||
let other = other.into();
|
||||
|
||||
Span::new(other.start, self.end)
|
||||
}
|
||||
|
||||
/// Returns a new Span by merging a later Span with the current Span.
|
||||
///
|
||||
/// The resulting Span will have the same start position as the current Span and same end as the given Span.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// let original_span = Span::new(4, 6);
|
||||
/// let later_span = Span::new(9, 11);
|
||||
/// let merged_span = origin_span.until(later_span);
|
||||
///
|
||||
/// assert_eq!(merged_span.start(), 4);
|
||||
/// assert_eq!(merged_span.end(), 11);
|
||||
/// ```
|
||||
pub fn until(&self, other: impl Into<Span>) -> Span {
|
||||
let other = other.into();
|
||||
|
||||
Span::new(self.start, other.end)
|
||||
}
|
||||
|
||||
/// Returns a new Span by merging a later Span with the current Span.
|
||||
///
|
||||
/// If the given Span is of the None variant,
|
||||
/// A Span with the same values as the current Span is returned.
|
||||
pub fn until_option(&self, other: Option<impl Into<Span>>) -> Span {
|
||||
match other {
|
||||
Some(other) => {
|
||||
@ -529,18 +633,32 @@ impl Span {
|
||||
self.slice(source).to_string().spanned(*self)
|
||||
}
|
||||
|
||||
/// Returns the start position of the current Span.
|
||||
pub fn start(&self) -> usize {
|
||||
self.start
|
||||
}
|
||||
|
||||
/// Returns the end position of the current Span.
|
||||
pub fn end(&self) -> usize {
|
||||
self.end
|
||||
}
|
||||
|
||||
/// Returns a bool if the current Span indicates an "unknown" position.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// let unknown_span = Span::unknown();
|
||||
/// let known_span = Span::new(4, 6);
|
||||
///
|
||||
/// assert_eq!(unknown_span.is_unknown(), true);
|
||||
/// assert_eq!(known_span.is_unknown(), false);
|
||||
/// ```
|
||||
pub fn is_unknown(&self) -> bool {
|
||||
self.start == 0 && self.end == 0
|
||||
}
|
||||
|
||||
/// Returns a slice of the input that covers the start and end of the current Span.
|
||||
pub fn slice<'a>(&self, source: &'a str) -> &'a str {
|
||||
&source[self.start..self.end]
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nu-test-support"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "A source string characterizer for Nushell"
|
||||
@ -10,9 +10,9 @@ license = "MIT"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
nu-parser = { path = "../nu-parser", version = "0.9.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.9.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.9.0" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.10.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.10.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.10.0" }
|
||||
|
||||
app_dirs = "1.2.1"
|
||||
dunce = "1.0.0"
|
||||
@ -22,4 +22,4 @@ tempfile = "3.1.0"
|
||||
indexmap = { version = "1.3.0", features = ["serde-1"] }
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = { version = "0.9.0", path = "../nu-build" }
|
||||
nu-build = { version = "0.10.0", path = "../nu-build" }
|
||||
|
@ -8,12 +8,9 @@ fn main() {
|
||||
|
||||
// if no arguments given, chop from standard input and exit.
|
||||
let stdin = io::stdin();
|
||||
let mut input = stdin.lock().lines();
|
||||
|
||||
if let Some(Ok(given)) = input.next() {
|
||||
if !given.is_empty() {
|
||||
for line in stdin.lock().lines() {
|
||||
if let Ok(given) = line {
|
||||
println!("{}", chop(&given));
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,9 +18,12 @@ fn main() {
|
||||
}
|
||||
|
||||
fn chop(word: &str) -> &str {
|
||||
let to = word.len() - 1;
|
||||
|
||||
&word[..to]
|
||||
if word.is_empty() {
|
||||
word
|
||||
} else {
|
||||
let to = word.len() - 1;
|
||||
&word[..to]
|
||||
}
|
||||
}
|
||||
|
||||
fn did_chop_arguments() -> bool {
|
||||
|
3
crates/nu-test-support/src/bins/nonu.rs
Normal file
3
crates/nu-test-support/src/bins/nonu.rs
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
std::env::args().skip(1).for_each(|arg| print!("{}", arg));
|
||||
}
|
@ -21,7 +21,7 @@ macro_rules! nu {
|
||||
|
||||
let commands = &*format!(
|
||||
"
|
||||
cd {}
|
||||
cd \"{}\"
|
||||
{}
|
||||
exit",
|
||||
$crate::fs::in_directory($cwd),
|
||||
@ -103,7 +103,7 @@ macro_rules! nu_error {
|
||||
|
||||
let commands = &*format!(
|
||||
"
|
||||
cd {}
|
||||
cd \"{}\"
|
||||
{}
|
||||
exit",
|
||||
$crate::fs::in_directory($cwd),
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nu-value-ext"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "Extension traits for values in Nushell"
|
||||
@ -10,14 +10,14 @@ license = "MIT"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
nu-source = { path = "../nu-source", version = "0.9.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.9.0" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.9.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.9.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.10.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.10.0" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.10.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.10.0" }
|
||||
|
||||
num-traits = "0.2.10"
|
||||
itertools = "0.8.2"
|
||||
indexmap = { version = "1.3.0", features = ["serde-1"] }
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = { version = "0.9.0", path = "../nu-build" }
|
||||
nu-build = { version = "0.10.0", path = "../nu-build" }
|
||||
|
@ -1,16 +1,19 @@
|
||||
[package]
|
||||
name = "nu_plugin_average"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "An average value plugin for Nushell"
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.9.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.9.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.9.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.9.0" }
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.10.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.10.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.10.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.10.0" }
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = { version = "0.9.0", path = "../nu-build" }
|
||||
nu-build = { version = "0.10.0", path = "../nu-build" }
|
||||
|
68
crates/nu_plugin_average/src/average.rs
Normal file
68
crates/nu_plugin_average/src/average.rs
Normal file
@ -0,0 +1,68 @@
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Primitive, UntaggedValue, Value};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Average {
|
||||
pub total: Option<Value>,
|
||||
pub count: u64,
|
||||
}
|
||||
|
||||
impl Average {
|
||||
pub fn new() -> Average {
|
||||
Average {
|
||||
total: None,
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn average(&mut self, value: Value) -> Result<(), ShellError> {
|
||||
match &value.value {
|
||||
UntaggedValue::Primitive(Primitive::Nothing) => Ok(()),
|
||||
UntaggedValue::Primitive(Primitive::Int(i)) => match &self.total {
|
||||
Some(Value {
|
||||
value: UntaggedValue::Primitive(Primitive::Int(j)),
|
||||
tag,
|
||||
}) => {
|
||||
self.total = Some(UntaggedValue::int(i + j).into_value(tag));
|
||||
self.count += 1;
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
self.total = Some(value.clone());
|
||||
self.count += 1;
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ShellError::labeled_error(
|
||||
"Could calculate average of non-integer or unrelated types",
|
||||
"source",
|
||||
value.tag,
|
||||
)),
|
||||
},
|
||||
UntaggedValue::Primitive(Primitive::Bytes(b)) => match &self.total {
|
||||
Some(Value {
|
||||
value: UntaggedValue::Primitive(Primitive::Bytes(j)),
|
||||
tag,
|
||||
}) => {
|
||||
self.total = Some(UntaggedValue::bytes(b + j).into_value(tag));
|
||||
self.count += 1;
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
self.total = Some(value);
|
||||
self.count += 1;
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ShellError::labeled_error(
|
||||
"Could calculate average of non-integer or unrelated types",
|
||||
"source",
|
||||
value.tag,
|
||||
)),
|
||||
},
|
||||
x => Err(ShellError::labeled_error(
|
||||
format!("Unrecognized type in stream: {:?}", x),
|
||||
"source",
|
||||
value.tag,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
4
crates/nu_plugin_average/src/lib.rs
Normal file
4
crates/nu_plugin_average/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod average;
|
||||
mod nu;
|
||||
|
||||
pub use average::Average;
|
@ -1,117 +1,6 @@
|
||||
use nu_errors::{CoerceInto, ShellError};
|
||||
use nu_plugin::{serve_plugin, Plugin};
|
||||
use nu_protocol::{
|
||||
CallInfo, Primitive, ReturnSuccess, ReturnValue, Signature, UntaggedValue, Value,
|
||||
};
|
||||
use nu_source::TaggedItem;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Average {
|
||||
total: Option<Value>,
|
||||
count: u64,
|
||||
}
|
||||
|
||||
impl Average {
|
||||
fn new() -> Average {
|
||||
Average {
|
||||
total: None,
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn average(&mut self, value: Value) -> Result<(), ShellError> {
|
||||
match &value.value {
|
||||
UntaggedValue::Primitive(Primitive::Nothing) => Ok(()),
|
||||
UntaggedValue::Primitive(Primitive::Int(i)) => match &self.total {
|
||||
Some(Value {
|
||||
value: UntaggedValue::Primitive(Primitive::Int(j)),
|
||||
tag,
|
||||
}) => {
|
||||
self.total = Some(UntaggedValue::int(i + j).into_value(tag));
|
||||
self.count += 1;
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
self.total = Some(value.clone());
|
||||
self.count += 1;
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ShellError::labeled_error(
|
||||
"Could calculate average of non-integer or unrelated types",
|
||||
"source",
|
||||
value.tag,
|
||||
)),
|
||||
},
|
||||
UntaggedValue::Primitive(Primitive::Bytes(b)) => match &self.total {
|
||||
Some(Value {
|
||||
value: UntaggedValue::Primitive(Primitive::Bytes(j)),
|
||||
tag,
|
||||
}) => {
|
||||
self.total = Some(UntaggedValue::bytes(b + j).into_value(tag));
|
||||
self.count += 1;
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
self.total = Some(value);
|
||||
self.count += 1;
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ShellError::labeled_error(
|
||||
"Could calculate average of non-integer or unrelated types",
|
||||
"source",
|
||||
value.tag,
|
||||
)),
|
||||
},
|
||||
x => Err(ShellError::labeled_error(
|
||||
format!("Unrecognized type in stream: {:?}", x),
|
||||
"source",
|
||||
value.tag,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for Average {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("average")
|
||||
.desc("Compute the average of a column of numerical values.")
|
||||
.filter())
|
||||
}
|
||||
|
||||
fn begin_filter(&mut self, _: CallInfo) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn filter(&mut self, input: Value) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
self.average(input)?;
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn end_filter(&mut self) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
match self.total {
|
||||
None => Ok(vec![]),
|
||||
Some(ref inner) => match &inner.value {
|
||||
UntaggedValue::Primitive(Primitive::Int(i)) => {
|
||||
let total: u64 = i
|
||||
.tagged(inner.tag.clone())
|
||||
.coerce_into("converting for average")?;
|
||||
let avg = total as f64 / self.count as f64;
|
||||
let primitive_value: UntaggedValue = Primitive::from(avg).into();
|
||||
let value = primitive_value.into_value(inner.tag.clone());
|
||||
Ok(vec![ReturnSuccess::value(value)])
|
||||
}
|
||||
UntaggedValue::Primitive(Primitive::Bytes(bytes)) => {
|
||||
let avg = *bytes as f64 / self.count as f64;
|
||||
let primitive_value: UntaggedValue = Primitive::from(avg).into();
|
||||
let tagged_value = primitive_value.into_value(inner.tag.clone());
|
||||
Ok(vec![ReturnSuccess::value(tagged_value)])
|
||||
}
|
||||
_ => Ok(vec![]),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
use nu_plugin::serve_plugin;
|
||||
use nu_plugin_average::Average;
|
||||
|
||||
fn main() {
|
||||
serve_plugin(&mut Average::new());
|
||||
serve_plugin(&mut Average::new())
|
||||
}
|
||||
|
48
crates/nu_plugin_average/src/nu/mod.rs
Normal file
48
crates/nu_plugin_average/src/nu/mod.rs
Normal file
@ -0,0 +1,48 @@
|
||||
use crate::Average;
|
||||
use nu_errors::{CoerceInto, ShellError};
|
||||
use nu_plugin::Plugin;
|
||||
use nu_protocol::{
|
||||
CallInfo, Primitive, ReturnSuccess, ReturnValue, Signature, UntaggedValue, Value,
|
||||
};
|
||||
use nu_source::TaggedItem;
|
||||
|
||||
impl Plugin for Average {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("average")
|
||||
.desc("Compute the average of a column of numerical values.")
|
||||
.filter())
|
||||
}
|
||||
|
||||
fn begin_filter(&mut self, _: CallInfo) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn filter(&mut self, input: Value) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
self.average(input)?;
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn end_filter(&mut self) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
match self.total {
|
||||
None => Ok(vec![]),
|
||||
Some(ref inner) => match &inner.value {
|
||||
UntaggedValue::Primitive(Primitive::Int(i)) => {
|
||||
let total: u64 = i
|
||||
.tagged(inner.tag.clone())
|
||||
.coerce_into("converting for average")?;
|
||||
let avg = total as f64 / self.count as f64;
|
||||
let primitive_value: UntaggedValue = Primitive::from(avg).into();
|
||||
let value = primitive_value.into_value(inner.tag.clone());
|
||||
Ok(vec![ReturnSuccess::value(value)])
|
||||
}
|
||||
UntaggedValue::Primitive(Primitive::Bytes(bytes)) => {
|
||||
let avg = *bytes as f64 / self.count as f64;
|
||||
let primitive_value: UntaggedValue = UntaggedValue::bytes(avg as u64);
|
||||
let tagged_value = primitive_value.into_value(inner.tag.clone());
|
||||
Ok(vec![ReturnSuccess::value(tagged_value)])
|
||||
}
|
||||
_ => Ok(vec![]),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +1,25 @@
|
||||
[package]
|
||||
name = "nu_plugin_binaryview"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "A binary viewer plugin for Nushell"
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
ansi_term = "0.12.1"
|
||||
crossterm = { version = "0.14.2" }
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.9.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.9.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.9.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.9.0" }
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.10.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.10.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.10.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.10.0" }
|
||||
pretty-hex = "0.1.1"
|
||||
image = { version = "0.22.4", default_features = false, features = ["png_codec", "jpeg"] }
|
||||
rawkey = "0.1.2"
|
||||
neso = "0.5.0"
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = { version = "0.9.0", path = "../nu-build" }
|
||||
nu-build = { version = "0.10.0", path = "../nu-build" }
|
||||
|
402
crates/nu_plugin_binaryview/src/binaryview.rs
Normal file
402
crates/nu_plugin_binaryview/src/binaryview.rs
Normal file
@ -0,0 +1,402 @@
|
||||
use crossterm::{style::Attribute, ExecutableCommand};
|
||||
use nu_protocol::outln;
|
||||
use nu_source::AnchorLocation;
|
||||
use pretty_hex::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BinaryView;
|
||||
|
||||
impl BinaryView {
|
||||
pub fn new() -> BinaryView {
|
||||
BinaryView
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_binary(
|
||||
b: &[u8],
|
||||
source: Option<&AnchorLocation>,
|
||||
lores_mode: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if b.len() > 3 {
|
||||
if let (0x4e, 0x45, 0x53) = (b[0], b[1], b[2]) {
|
||||
view_contents_interactive(b, source, lores_mode)?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
view_contents(b, source, lores_mode)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct RenderContext {
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub frame_buffer: Vec<(u8, u8, u8)>,
|
||||
pub since_last_button: Vec<usize>,
|
||||
pub lores_mode: bool,
|
||||
}
|
||||
|
||||
impl RenderContext {
|
||||
pub fn blank(lores_mode: bool) -> RenderContext {
|
||||
RenderContext {
|
||||
width: 0,
|
||||
height: 0,
|
||||
frame_buffer: vec![],
|
||||
since_last_button: vec![0; 8],
|
||||
lores_mode,
|
||||
}
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
self.frame_buffer = vec![(0, 0, 0); self.width * self.height as usize];
|
||||
}
|
||||
|
||||
fn render_to_screen_lores(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut prev_color: Option<(u8, u8, u8)> = None;
|
||||
let mut prev_count = 1;
|
||||
|
||||
let _ = std::io::stdout().execute(crossterm::cursor::MoveTo(0, 0));
|
||||
|
||||
for pixel in &self.frame_buffer {
|
||||
match prev_color {
|
||||
Some(c) if c == *pixel => {
|
||||
prev_count += 1;
|
||||
}
|
||||
Some(c) => {
|
||||
print!(
|
||||
"{}",
|
||||
ansi_term::Colour::RGB(c.0, c.1, c.2)
|
||||
.paint((0..prev_count).map(|_| "█").collect::<String>())
|
||||
);
|
||||
prev_color = Some(*pixel);
|
||||
prev_count = 1;
|
||||
}
|
||||
_ => {
|
||||
prev_color = Some(*pixel);
|
||||
prev_count = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if prev_count > 0 {
|
||||
if let Some(color) = prev_color {
|
||||
print!(
|
||||
"{}",
|
||||
ansi_term::Colour::RGB(color.0, color.1, color.2)
|
||||
.paint((0..prev_count).map(|_| "█").collect::<String>())
|
||||
);
|
||||
}
|
||||
}
|
||||
outln!("{}", Attribute::Reset);
|
||||
Ok(())
|
||||
}
|
||||
fn render_to_screen_hires(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut prev_fg: Option<(u8, u8, u8)> = None;
|
||||
let mut prev_bg: Option<(u8, u8, u8)> = None;
|
||||
let mut prev_count = 1;
|
||||
|
||||
let mut pos = 0;
|
||||
let fb_len = self.frame_buffer.len();
|
||||
|
||||
let _ = std::io::stdout().execute(crossterm::cursor::MoveTo(0, 0));
|
||||
|
||||
while pos < (fb_len - self.width) {
|
||||
let top_pixel = self.frame_buffer[pos];
|
||||
let bottom_pixel = self.frame_buffer[pos + self.width];
|
||||
|
||||
match (prev_fg, prev_bg) {
|
||||
(Some(c), Some(d)) if c == top_pixel && d == bottom_pixel => {
|
||||
prev_count += 1;
|
||||
}
|
||||
(Some(c), Some(d)) => {
|
||||
print!(
|
||||
"{}",
|
||||
ansi_term::Colour::RGB(c.0, c.1, c.2)
|
||||
.on(ansi_term::Colour::RGB(d.0, d.1, d.2,))
|
||||
.paint((0..prev_count).map(|_| "▀").collect::<String>())
|
||||
);
|
||||
prev_fg = Some(top_pixel);
|
||||
prev_bg = Some(bottom_pixel);
|
||||
prev_count = 1;
|
||||
}
|
||||
_ => {
|
||||
prev_fg = Some(top_pixel);
|
||||
prev_bg = Some(bottom_pixel);
|
||||
prev_count = 1;
|
||||
}
|
||||
}
|
||||
pos += 1;
|
||||
if pos % self.width == 0 {
|
||||
pos += self.width;
|
||||
}
|
||||
}
|
||||
if prev_count > 0 {
|
||||
if let (Some(c), Some(d)) = (prev_fg, prev_bg) {
|
||||
print!(
|
||||
"{}",
|
||||
ansi_term::Colour::RGB(c.0, c.1, c.2)
|
||||
.on(ansi_term::Colour::RGB(d.0, d.1, d.2,))
|
||||
.paint((0..prev_count).map(|_| "▀").collect::<String>())
|
||||
);
|
||||
}
|
||||
}
|
||||
outln!("{}", Attribute::Reset);
|
||||
Ok(())
|
||||
}
|
||||
pub fn flush(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if self.lores_mode {
|
||||
self.render_to_screen_lores()
|
||||
} else {
|
||||
self.render_to_screen_hires()
|
||||
}
|
||||
}
|
||||
pub fn update(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let terminal_size = crossterm::terminal::size().unwrap_or_else(|_| (80, 24));
|
||||
|
||||
if (self.width != terminal_size.0 as usize) || (self.height != terminal_size.1 as usize) {
|
||||
let _ = std::io::stdout().execute(crossterm::cursor::Hide);
|
||||
|
||||
self.width = terminal_size.0 as usize;
|
||||
self.height = if self.lores_mode {
|
||||
terminal_size.1 as usize - 1
|
||||
} else {
|
||||
(terminal_size.1 as usize - 1) * 2
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RawImageBuffer {
|
||||
dimensions: (u64, u64),
|
||||
colortype: image::ColorType,
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
fn load_from_png_buffer(buffer: &[u8]) -> Result<RawImageBuffer, Box<dyn std::error::Error>> {
|
||||
use image::ImageDecoder;
|
||||
|
||||
let decoder = image::png::PNGDecoder::new(buffer)?;
|
||||
|
||||
let dimensions = decoder.dimensions();
|
||||
let colortype = decoder.colortype();
|
||||
let buffer = decoder.read_image()?;
|
||||
|
||||
Ok(RawImageBuffer {
|
||||
dimensions,
|
||||
colortype,
|
||||
buffer,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_from_jpg_buffer(buffer: &[u8]) -> Result<RawImageBuffer, Box<dyn std::error::Error>> {
|
||||
use image::ImageDecoder;
|
||||
|
||||
let decoder = image::jpeg::JPEGDecoder::new(buffer)?;
|
||||
|
||||
let dimensions = decoder.dimensions();
|
||||
let colortype = decoder.colortype();
|
||||
let buffer = decoder.read_image()?;
|
||||
|
||||
Ok(RawImageBuffer {
|
||||
dimensions,
|
||||
colortype,
|
||||
buffer,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn view_contents(
|
||||
buffer: &[u8],
|
||||
_source: Option<&AnchorLocation>,
|
||||
lores_mode: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut raw_image_buffer = load_from_png_buffer(buffer);
|
||||
|
||||
if raw_image_buffer.is_err() {
|
||||
raw_image_buffer = load_from_jpg_buffer(buffer);
|
||||
}
|
||||
|
||||
if raw_image_buffer.is_err() {
|
||||
//Not yet supported
|
||||
outln!("{:?}", buffer.hex_dump());
|
||||
return Ok(());
|
||||
}
|
||||
let raw_image_buffer = raw_image_buffer?;
|
||||
|
||||
let mut render_context: RenderContext = RenderContext::blank(lores_mode);
|
||||
let _ = render_context.update();
|
||||
render_context.clear();
|
||||
|
||||
match raw_image_buffer.colortype {
|
||||
image::ColorType::RGBA(8) => {
|
||||
let img = image::ImageBuffer::<image::Rgba<u8>, Vec<u8>>::from_vec(
|
||||
raw_image_buffer.dimensions.0 as u32,
|
||||
raw_image_buffer.dimensions.1 as u32,
|
||||
raw_image_buffer.buffer,
|
||||
)
|
||||
.ok_or("Cannot convert image data")?;
|
||||
|
||||
let resized_img = image::imageops::resize(
|
||||
&img,
|
||||
render_context.width as u32,
|
||||
render_context.height as u32,
|
||||
image::FilterType::Lanczos3,
|
||||
);
|
||||
|
||||
let mut count = 0;
|
||||
for pixel in resized_img.pixels() {
|
||||
use image::Pixel;
|
||||
let rgb = pixel.to_rgb();
|
||||
render_context.frame_buffer[count] = (rgb[0], rgb[1], rgb[2]);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
image::ColorType::RGB(8) => {
|
||||
let img = image::ImageBuffer::<image::Rgb<u8>, Vec<u8>>::from_vec(
|
||||
raw_image_buffer.dimensions.0 as u32,
|
||||
raw_image_buffer.dimensions.1 as u32,
|
||||
raw_image_buffer.buffer,
|
||||
)
|
||||
.ok_or("Cannot convert image data")?;
|
||||
|
||||
let resized_img = image::imageops::resize(
|
||||
&img,
|
||||
render_context.width as u32,
|
||||
render_context.height as u32,
|
||||
image::FilterType::Lanczos3,
|
||||
);
|
||||
|
||||
let mut count = 0;
|
||||
for pixel in resized_img.pixels() {
|
||||
use image::Pixel;
|
||||
let rgb = pixel.to_rgb();
|
||||
render_context.frame_buffer[count] = (rgb[0], rgb[1], rgb[2]);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
//Not yet supported
|
||||
outln!("{:?}", buffer.hex_dump());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
render_context.flush()?;
|
||||
|
||||
let _ = std::io::stdout().execute(crossterm::cursor::Show);
|
||||
|
||||
let _ = crossterm::terminal::disable_raw_mode();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn view_contents_interactive(
|
||||
buffer: &[u8],
|
||||
source: Option<&AnchorLocation>,
|
||||
lores_mode: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use rawkey::{KeyCode, RawKey};
|
||||
|
||||
let sav_path = if let Some(AnchorLocation::File(f)) = source {
|
||||
let mut path = std::path::PathBuf::from(f);
|
||||
path.set_extension("sav");
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut nes = neso::Nes::new(0.0);
|
||||
let rawkey = RawKey::new();
|
||||
nes.load_rom(&buffer);
|
||||
|
||||
if let Some(ref sav_path) = sav_path {
|
||||
if let Ok(contents) = std::fs::read(sav_path) {
|
||||
let _ = nes.load_state(&contents);
|
||||
}
|
||||
}
|
||||
|
||||
nes.reset();
|
||||
|
||||
if let Ok(_raw) = crossterm::terminal::enable_raw_mode() {
|
||||
let mut render_context: RenderContext = RenderContext::blank(lores_mode);
|
||||
|
||||
let buttons = vec![
|
||||
KeyCode::Alt,
|
||||
KeyCode::LeftControl,
|
||||
KeyCode::Tab,
|
||||
KeyCode::BackSpace,
|
||||
KeyCode::UpArrow,
|
||||
KeyCode::DownArrow,
|
||||
KeyCode::LeftArrow,
|
||||
KeyCode::RightArrow,
|
||||
];
|
||||
|
||||
let _ = std::io::stdout().execute(crossterm::cursor::Hide);
|
||||
|
||||
'gameloop: loop {
|
||||
let _ = render_context.update();
|
||||
nes.step_frame();
|
||||
|
||||
let image_buffer = nes.image_buffer();
|
||||
|
||||
let slice = unsafe { std::slice::from_raw_parts(image_buffer, 256 * 240 * 4) };
|
||||
let img = image::ImageBuffer::<image::Rgba<u8>, &[u8]>::from_raw(256, 240, slice)
|
||||
.ok_or("Cannot convert image data")?;
|
||||
let resized_img = image::imageops::resize(
|
||||
&img,
|
||||
render_context.width as u32,
|
||||
render_context.height as u32,
|
||||
image::FilterType::Lanczos3,
|
||||
);
|
||||
|
||||
render_context.clear();
|
||||
|
||||
let mut count = 0;
|
||||
for pixel in resized_img.pixels() {
|
||||
use image::Pixel;
|
||||
let rgb = pixel.to_rgb();
|
||||
|
||||
render_context.frame_buffer[count] = (rgb[0], rgb[1], rgb[2]);
|
||||
count += 1;
|
||||
}
|
||||
render_context.flush()?;
|
||||
|
||||
if rawkey.is_pressed(rawkey::KeyCode::Escape) {
|
||||
break 'gameloop;
|
||||
} else {
|
||||
for (idx, button) in buttons.iter().enumerate() {
|
||||
if rawkey.is_pressed(*button) {
|
||||
nes.press_button(0, idx as u8);
|
||||
} else {
|
||||
nes.release_button(0, idx as u8);
|
||||
}
|
||||
}
|
||||
loop {
|
||||
let x = crossterm::event::poll(std::time::Duration::from_secs(0));
|
||||
match x {
|
||||
Ok(true) => {
|
||||
// Swallow the events so we don't queue them into the line editor
|
||||
let _ = crossterm::event::read();
|
||||
}
|
||||
_ => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref sav_path) = sav_path {
|
||||
let buffer = nes.save_state();
|
||||
if let Ok(buffer) = buffer {
|
||||
let _ = std::fs::write(sav_path, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = std::io::stdout().execute(crossterm::cursor::Show);
|
||||
|
||||
let _screen = crossterm::terminal::disable_raw_mode();
|
||||
|
||||
Ok(())
|
||||
}
|
4
crates/nu_plugin_binaryview/src/lib.rs
Normal file
4
crates/nu_plugin_binaryview/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod binaryview;
|
||||
mod nu;
|
||||
|
||||
pub use binaryview::BinaryView;
|
@ -1,424 +1,6 @@
|
||||
use crossterm::{style::Attribute, ExecutableCommand};
|
||||
use nu_errors::ShellError;
|
||||
use nu_plugin::{serve_plugin, Plugin};
|
||||
use nu_protocol::{outln, CallInfo, Primitive, Signature, UntaggedValue, Value};
|
||||
use nu_source::AnchorLocation;
|
||||
use pretty_hex::*;
|
||||
|
||||
struct BinaryView;
|
||||
|
||||
impl BinaryView {
|
||||
fn new() -> BinaryView {
|
||||
BinaryView
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for BinaryView {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("binaryview")
|
||||
.desc("Autoview of binary data.")
|
||||
.switch("lores", "use low resolution output mode"))
|
||||
}
|
||||
|
||||
fn sink(&mut self, call_info: CallInfo, input: Vec<Value>) {
|
||||
for v in input {
|
||||
let value_anchor = v.anchor();
|
||||
if let UntaggedValue::Primitive(Primitive::Binary(b)) = &v.value {
|
||||
let _ = view_binary(&b, value_anchor.as_ref(), call_info.args.has("lores"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view_binary(
|
||||
b: &[u8],
|
||||
source: Option<&AnchorLocation>,
|
||||
lores_mode: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if b.len() > 3 {
|
||||
if let (0x4e, 0x45, 0x53) = (b[0], b[1], b[2]) {
|
||||
view_contents_interactive(b, source, lores_mode)?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
view_contents(b, source, lores_mode)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct RenderContext {
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub frame_buffer: Vec<(u8, u8, u8)>,
|
||||
pub since_last_button: Vec<usize>,
|
||||
pub lores_mode: bool,
|
||||
}
|
||||
|
||||
impl RenderContext {
|
||||
pub fn blank(lores_mode: bool) -> RenderContext {
|
||||
RenderContext {
|
||||
width: 0,
|
||||
height: 0,
|
||||
frame_buffer: vec![],
|
||||
since_last_button: vec![0; 8],
|
||||
lores_mode,
|
||||
}
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
self.frame_buffer = vec![(0, 0, 0); self.width * self.height as usize];
|
||||
}
|
||||
|
||||
fn render_to_screen_lores(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut prev_color: Option<(u8, u8, u8)> = None;
|
||||
let mut prev_count = 1;
|
||||
|
||||
let _ = std::io::stdout().execute(crossterm::cursor::MoveTo(0, 0));
|
||||
|
||||
for pixel in &self.frame_buffer {
|
||||
match prev_color {
|
||||
Some(c) if c == *pixel => {
|
||||
prev_count += 1;
|
||||
}
|
||||
Some(c) => {
|
||||
print!(
|
||||
"{}",
|
||||
ansi_term::Colour::RGB(c.0, c.1, c.2)
|
||||
.paint((0..prev_count).map(|_| "█").collect::<String>())
|
||||
);
|
||||
prev_color = Some(*pixel);
|
||||
prev_count = 1;
|
||||
}
|
||||
_ => {
|
||||
prev_color = Some(*pixel);
|
||||
prev_count = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if prev_count > 0 {
|
||||
if let Some(color) = prev_color {
|
||||
print!(
|
||||
"{}",
|
||||
ansi_term::Colour::RGB(color.0, color.1, color.2)
|
||||
.paint((0..prev_count).map(|_| "█").collect::<String>())
|
||||
);
|
||||
}
|
||||
}
|
||||
outln!("{}", Attribute::Reset);
|
||||
Ok(())
|
||||
}
|
||||
fn render_to_screen_hires(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut prev_fg: Option<(u8, u8, u8)> = None;
|
||||
let mut prev_bg: Option<(u8, u8, u8)> = None;
|
||||
let mut prev_count = 1;
|
||||
|
||||
let mut pos = 0;
|
||||
let fb_len = self.frame_buffer.len();
|
||||
|
||||
let _ = std::io::stdout().execute(crossterm::cursor::MoveTo(0, 0));
|
||||
|
||||
while pos < (fb_len - self.width) {
|
||||
let top_pixel = self.frame_buffer[pos];
|
||||
let bottom_pixel = self.frame_buffer[pos + self.width];
|
||||
|
||||
match (prev_fg, prev_bg) {
|
||||
(Some(c), Some(d)) if c == top_pixel && d == bottom_pixel => {
|
||||
prev_count += 1;
|
||||
}
|
||||
(Some(c), Some(d)) => {
|
||||
print!(
|
||||
"{}",
|
||||
ansi_term::Colour::RGB(c.0, c.1, c.2)
|
||||
.on(ansi_term::Colour::RGB(d.0, d.1, d.2,))
|
||||
.paint((0..prev_count).map(|_| "▀").collect::<String>())
|
||||
);
|
||||
prev_fg = Some(top_pixel);
|
||||
prev_bg = Some(bottom_pixel);
|
||||
prev_count = 1;
|
||||
}
|
||||
_ => {
|
||||
prev_fg = Some(top_pixel);
|
||||
prev_bg = Some(bottom_pixel);
|
||||
prev_count = 1;
|
||||
}
|
||||
}
|
||||
pos += 1;
|
||||
if pos % self.width == 0 {
|
||||
pos += self.width;
|
||||
}
|
||||
}
|
||||
if prev_count > 0 {
|
||||
if let (Some(c), Some(d)) = (prev_fg, prev_bg) {
|
||||
print!(
|
||||
"{}",
|
||||
ansi_term::Colour::RGB(c.0, c.1, c.2)
|
||||
.on(ansi_term::Colour::RGB(d.0, d.1, d.2,))
|
||||
.paint((0..prev_count).map(|_| "▀").collect::<String>())
|
||||
);
|
||||
}
|
||||
}
|
||||
outln!("{}", Attribute::Reset);
|
||||
Ok(())
|
||||
}
|
||||
pub fn flush(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if self.lores_mode {
|
||||
self.render_to_screen_lores()
|
||||
} else {
|
||||
self.render_to_screen_hires()
|
||||
}
|
||||
}
|
||||
pub fn update(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let terminal_size = crossterm::terminal::size().unwrap_or_else(|_| (80, 24));
|
||||
|
||||
if (self.width != terminal_size.0 as usize) || (self.height != terminal_size.1 as usize) {
|
||||
let _ = std::io::stdout().execute(crossterm::cursor::Hide);
|
||||
|
||||
self.width = terminal_size.0 as usize;
|
||||
self.height = if self.lores_mode {
|
||||
terminal_size.1 as usize - 1
|
||||
} else {
|
||||
(terminal_size.1 as usize - 1) * 2
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RawImageBuffer {
|
||||
dimensions: (u64, u64),
|
||||
colortype: image::ColorType,
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
fn load_from_png_buffer(buffer: &[u8]) -> Result<RawImageBuffer, Box<dyn std::error::Error>> {
|
||||
use image::ImageDecoder;
|
||||
|
||||
let decoder = image::png::PNGDecoder::new(buffer)?;
|
||||
|
||||
let dimensions = decoder.dimensions();
|
||||
let colortype = decoder.colortype();
|
||||
let buffer = decoder.read_image()?;
|
||||
|
||||
Ok(RawImageBuffer {
|
||||
dimensions,
|
||||
colortype,
|
||||
buffer,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_from_jpg_buffer(buffer: &[u8]) -> Result<RawImageBuffer, Box<dyn std::error::Error>> {
|
||||
use image::ImageDecoder;
|
||||
|
||||
let decoder = image::jpeg::JPEGDecoder::new(buffer)?;
|
||||
|
||||
let dimensions = decoder.dimensions();
|
||||
let colortype = decoder.colortype();
|
||||
let buffer = decoder.read_image()?;
|
||||
|
||||
Ok(RawImageBuffer {
|
||||
dimensions,
|
||||
colortype,
|
||||
buffer,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn view_contents(
|
||||
buffer: &[u8],
|
||||
_source: Option<&AnchorLocation>,
|
||||
lores_mode: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut raw_image_buffer = load_from_png_buffer(buffer);
|
||||
|
||||
if raw_image_buffer.is_err() {
|
||||
raw_image_buffer = load_from_jpg_buffer(buffer);
|
||||
}
|
||||
|
||||
if raw_image_buffer.is_err() {
|
||||
//Not yet supported
|
||||
outln!("{:?}", buffer.hex_dump());
|
||||
return Ok(());
|
||||
}
|
||||
let raw_image_buffer = raw_image_buffer?;
|
||||
|
||||
let mut render_context: RenderContext = RenderContext::blank(lores_mode);
|
||||
let _ = render_context.update();
|
||||
render_context.clear();
|
||||
|
||||
match raw_image_buffer.colortype {
|
||||
image::ColorType::RGBA(8) => {
|
||||
let img = image::ImageBuffer::<image::Rgba<u8>, Vec<u8>>::from_vec(
|
||||
raw_image_buffer.dimensions.0 as u32,
|
||||
raw_image_buffer.dimensions.1 as u32,
|
||||
raw_image_buffer.buffer,
|
||||
)
|
||||
.ok_or("Cannot convert image data")?;
|
||||
|
||||
let resized_img = image::imageops::resize(
|
||||
&img,
|
||||
render_context.width as u32,
|
||||
render_context.height as u32,
|
||||
image::FilterType::Lanczos3,
|
||||
);
|
||||
|
||||
let mut count = 0;
|
||||
for pixel in resized_img.pixels() {
|
||||
use image::Pixel;
|
||||
let rgb = pixel.to_rgb();
|
||||
render_context.frame_buffer[count] = (rgb[0], rgb[1], rgb[2]);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
image::ColorType::RGB(8) => {
|
||||
let img = image::ImageBuffer::<image::Rgb<u8>, Vec<u8>>::from_vec(
|
||||
raw_image_buffer.dimensions.0 as u32,
|
||||
raw_image_buffer.dimensions.1 as u32,
|
||||
raw_image_buffer.buffer,
|
||||
)
|
||||
.ok_or("Cannot convert image data")?;
|
||||
|
||||
let resized_img = image::imageops::resize(
|
||||
&img,
|
||||
render_context.width as u32,
|
||||
render_context.height as u32,
|
||||
image::FilterType::Lanczos3,
|
||||
);
|
||||
|
||||
let mut count = 0;
|
||||
for pixel in resized_img.pixels() {
|
||||
use image::Pixel;
|
||||
let rgb = pixel.to_rgb();
|
||||
render_context.frame_buffer[count] = (rgb[0], rgb[1], rgb[2]);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
//Not yet supported
|
||||
outln!("{:?}", buffer.hex_dump());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
render_context.flush()?;
|
||||
|
||||
let _ = std::io::stdout().execute(crossterm::cursor::Show);
|
||||
|
||||
let _ = crossterm::terminal::disable_raw_mode();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn view_contents_interactive(
|
||||
buffer: &[u8],
|
||||
source: Option<&AnchorLocation>,
|
||||
lores_mode: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use rawkey::{KeyCode, RawKey};
|
||||
|
||||
let sav_path = if let Some(AnchorLocation::File(f)) = source {
|
||||
let mut path = std::path::PathBuf::from(f);
|
||||
path.set_extension("sav");
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut nes = neso::Nes::new(0.0);
|
||||
let rawkey = RawKey::new();
|
||||
nes.load_rom(&buffer);
|
||||
|
||||
if let Some(ref sav_path) = sav_path {
|
||||
if let Ok(contents) = std::fs::read(sav_path) {
|
||||
let _ = nes.load_state(&contents);
|
||||
}
|
||||
}
|
||||
|
||||
nes.reset();
|
||||
|
||||
if let Ok(_raw) = crossterm::terminal::enable_raw_mode() {
|
||||
let mut render_context: RenderContext = RenderContext::blank(lores_mode);
|
||||
|
||||
let buttons = vec![
|
||||
KeyCode::Alt,
|
||||
KeyCode::LeftControl,
|
||||
KeyCode::Tab,
|
||||
KeyCode::BackSpace,
|
||||
KeyCode::UpArrow,
|
||||
KeyCode::DownArrow,
|
||||
KeyCode::LeftArrow,
|
||||
KeyCode::RightArrow,
|
||||
];
|
||||
|
||||
let _ = std::io::stdout().execute(crossterm::cursor::Hide);
|
||||
|
||||
'gameloop: loop {
|
||||
let _ = render_context.update();
|
||||
nes.step_frame();
|
||||
|
||||
let image_buffer = nes.image_buffer();
|
||||
|
||||
let slice = unsafe { std::slice::from_raw_parts(image_buffer, 256 * 240 * 4) };
|
||||
let img = image::ImageBuffer::<image::Rgba<u8>, &[u8]>::from_raw(256, 240, slice)
|
||||
.ok_or("Cannot convert image data")?;
|
||||
let resized_img = image::imageops::resize(
|
||||
&img,
|
||||
render_context.width as u32,
|
||||
render_context.height as u32,
|
||||
image::FilterType::Lanczos3,
|
||||
);
|
||||
|
||||
render_context.clear();
|
||||
|
||||
let mut count = 0;
|
||||
for pixel in resized_img.pixels() {
|
||||
use image::Pixel;
|
||||
let rgb = pixel.to_rgb();
|
||||
|
||||
render_context.frame_buffer[count] = (rgb[0], rgb[1], rgb[2]);
|
||||
count += 1;
|
||||
}
|
||||
render_context.flush()?;
|
||||
|
||||
if rawkey.is_pressed(rawkey::KeyCode::Escape) {
|
||||
break 'gameloop;
|
||||
} else {
|
||||
for (idx, button) in buttons.iter().enumerate() {
|
||||
if rawkey.is_pressed(*button) {
|
||||
nes.press_button(0, idx as u8);
|
||||
} else {
|
||||
nes.release_button(0, idx as u8);
|
||||
}
|
||||
}
|
||||
loop {
|
||||
let x = crossterm::event::poll(std::time::Duration::from_secs(0));
|
||||
match x {
|
||||
Ok(true) => {
|
||||
// Swallow the events so we don't queue them into the line editor
|
||||
let _ = crossterm::event::read();
|
||||
}
|
||||
_ => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref sav_path) = sav_path {
|
||||
let buffer = nes.save_state();
|
||||
if let Ok(buffer) = buffer {
|
||||
let _ = std::fs::write(sav_path, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = std::io::stdout().execute(crossterm::cursor::Show);
|
||||
|
||||
let _screen = crossterm::terminal::disable_raw_mode();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
use nu_plugin::serve_plugin;
|
||||
use nu_plugin_binaryview::BinaryView;
|
||||
|
||||
fn main() {
|
||||
serve_plugin(&mut BinaryView::new());
|
||||
serve_plugin(&mut BinaryView::new())
|
||||
}
|
||||
|
23
crates/nu_plugin_binaryview/src/nu/mod.rs
Normal file
23
crates/nu_plugin_binaryview/src/nu/mod.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use nu_errors::ShellError;
|
||||
use nu_plugin::Plugin;
|
||||
use nu_protocol::{CallInfo, Primitive, Signature, UntaggedValue, Value};
|
||||
|
||||
use crate::binaryview::view_binary;
|
||||
use crate::BinaryView;
|
||||
|
||||
impl Plugin for BinaryView {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("binaryview")
|
||||
.desc("Autoview of binary data.")
|
||||
.switch("lores", "use low resolution output mode", Some('l')))
|
||||
}
|
||||
|
||||
fn sink(&mut self, call_info: CallInfo, input: Vec<Value>) {
|
||||
for v in input {
|
||||
let value_anchor = v.anchor();
|
||||
if let UntaggedValue::Primitive(Primitive::Binary(b)) = &v.value {
|
||||
let _ = view_binary(&b, value_anchor.as_ref(), call_info.args.has("lores"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +1,22 @@
|
||||
[package]
|
||||
name = "nu_plugin_fetch"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "A URL fetch plugin for Nushell"
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.9.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.9.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.9.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.9.0" }
|
||||
futures-preview = { version = "=0.3.0-alpha.19", features = ["compat", "io-compat"] }
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.10.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.10.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.10.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.10.0" }
|
||||
futures = { version = "0.3", features = ["compat", "io-compat"] }
|
||||
surf = "1.0.3"
|
||||
url = "2.1.0"
|
||||
url = "2.1.1"
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = { version = "0.9.0", path = "../nu-build" }
|
||||
nu-build = { version = "0.10.0", path = "../nu-build" }
|
||||
|
258
crates/nu_plugin_fetch/src/fetch.rs
Normal file
258
crates/nu_plugin_fetch/src/fetch.rs
Normal file
@ -0,0 +1,258 @@
|
||||
use mime::Mime;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{CallInfo, CommandAction, ReturnSuccess, ReturnValue, UntaggedValue, Value};
|
||||
use nu_source::{AnchorLocation, Span, Tag};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use surf::mime;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Fetch {
|
||||
pub path: Option<Value>,
|
||||
pub has_raw: bool,
|
||||
}
|
||||
|
||||
impl Fetch {
|
||||
pub fn new() -> Fetch {
|
||||
Fetch {
|
||||
path: None,
|
||||
has_raw: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup(&mut self, call_info: CallInfo) -> ReturnValue {
|
||||
self.path = Some(
|
||||
match call_info.args.nth(0).ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"No file or directory specified",
|
||||
"for command",
|
||||
&call_info.name_tag,
|
||||
)
|
||||
})? {
|
||||
file => file.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
self.has_raw = call_info.args.has("raw");
|
||||
|
||||
ReturnSuccess::value(UntaggedValue::nothing().into_untagged_value())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_helper(path: &Value, has_raw: bool, row: Value) -> ReturnValue {
|
||||
let path_buf = path.as_path()?;
|
||||
let path_str = path_buf.display().to_string();
|
||||
|
||||
//FIXME: this is a workaround because plugins don't yet support per-item iteration
|
||||
let path_str = if path_str == "$it" {
|
||||
let path_buf = row.as_path()?;
|
||||
path_buf.display().to_string()
|
||||
} else {
|
||||
path_str
|
||||
};
|
||||
|
||||
let path_span = path.tag.span;
|
||||
|
||||
let result = fetch(&path_str, path_span).await;
|
||||
|
||||
if let Err(e) = result {
|
||||
return Err(e);
|
||||
}
|
||||
let (file_extension, contents, contents_tag) = result?;
|
||||
|
||||
let file_extension = if has_raw {
|
||||
None
|
||||
} else {
|
||||
// If the extension could not be determined via mimetype, try to use the path
|
||||
// extension. Some file types do not declare their mimetypes (such as bson files).
|
||||
file_extension.or_else(|| path_str.split('.').last().map(String::from))
|
||||
};
|
||||
|
||||
let tagged_contents = contents.retag(&contents_tag);
|
||||
|
||||
if let Some(extension) = file_extension {
|
||||
Ok(ReturnSuccess::Action(CommandAction::AutoConvert(
|
||||
tagged_contents,
|
||||
extension,
|
||||
)))
|
||||
} else {
|
||||
ReturnSuccess::value(tagged_contents)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch(
|
||||
location: &str,
|
||||
span: Span,
|
||||
) -> Result<(Option<String>, UntaggedValue, Tag), ShellError> {
|
||||
if url::Url::parse(location).is_err() {
|
||||
return Err(ShellError::labeled_error(
|
||||
"Incomplete or incorrect url",
|
||||
"expected a full url",
|
||||
span,
|
||||
));
|
||||
}
|
||||
|
||||
let response = surf::get(location).await;
|
||||
match response {
|
||||
Ok(mut r) => match r.headers().get("content-type") {
|
||||
Some(content_type) => {
|
||||
let content_type = Mime::from_str(content_type).map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
format!("MIME type unknown: {}", content_type),
|
||||
"given unknown MIME type",
|
||||
span,
|
||||
)
|
||||
})?;
|
||||
match (content_type.type_(), content_type.subtype()) {
|
||||
(mime::APPLICATION, mime::XML) => Ok((
|
||||
Some("xml".to_string()),
|
||||
UntaggedValue::string(r.body_string().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load text from remote url",
|
||||
"could not load",
|
||||
span,
|
||||
)
|
||||
})?),
|
||||
Tag {
|
||||
span,
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
},
|
||||
)),
|
||||
(mime::APPLICATION, mime::JSON) => Ok((
|
||||
Some("json".to_string()),
|
||||
UntaggedValue::string(r.body_string().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load text from remote url",
|
||||
"could not load",
|
||||
span,
|
||||
)
|
||||
})?),
|
||||
Tag {
|
||||
span,
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
},
|
||||
)),
|
||||
(mime::APPLICATION, mime::OCTET_STREAM) => {
|
||||
let buf: Vec<u8> = r.body_bytes().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load binary file",
|
||||
"could not load",
|
||||
span,
|
||||
)
|
||||
})?;
|
||||
Ok((
|
||||
None,
|
||||
UntaggedValue::binary(buf),
|
||||
Tag {
|
||||
span,
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
},
|
||||
))
|
||||
}
|
||||
(mime::IMAGE, mime::SVG) => Ok((
|
||||
Some("svg".to_string()),
|
||||
UntaggedValue::string(r.body_string().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load svg from remote url",
|
||||
"could not load",
|
||||
span,
|
||||
)
|
||||
})?),
|
||||
Tag {
|
||||
span,
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
},
|
||||
)),
|
||||
(mime::IMAGE, image_ty) => {
|
||||
let buf: Vec<u8> = r.body_bytes().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load image file",
|
||||
"could not load",
|
||||
span,
|
||||
)
|
||||
})?;
|
||||
Ok((
|
||||
Some(image_ty.to_string()),
|
||||
UntaggedValue::binary(buf),
|
||||
Tag {
|
||||
span,
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
},
|
||||
))
|
||||
}
|
||||
(mime::TEXT, mime::HTML) => Ok((
|
||||
Some("html".to_string()),
|
||||
UntaggedValue::string(r.body_string().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load text from remote url",
|
||||
"could not load",
|
||||
span,
|
||||
)
|
||||
})?),
|
||||
Tag {
|
||||
span,
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
},
|
||||
)),
|
||||
(mime::TEXT, mime::PLAIN) => {
|
||||
let path_extension = url::Url::parse(location)
|
||||
.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
format!("Cannot parse URL: {}", location),
|
||||
"cannot parse",
|
||||
span,
|
||||
)
|
||||
})?
|
||||
.path_segments()
|
||||
.and_then(|segments| segments.last())
|
||||
.and_then(|name| if name.is_empty() { None } else { Some(name) })
|
||||
.and_then(|name| {
|
||||
PathBuf::from(name)
|
||||
.extension()
|
||||
.map(|name| name.to_string_lossy().to_string())
|
||||
});
|
||||
|
||||
Ok((
|
||||
path_extension,
|
||||
UntaggedValue::string(r.body_string().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load text from remote url",
|
||||
"could not load",
|
||||
span,
|
||||
)
|
||||
})?),
|
||||
Tag {
|
||||
span,
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
},
|
||||
))
|
||||
}
|
||||
(ty, sub_ty) => Ok((
|
||||
None,
|
||||
UntaggedValue::string(format!(
|
||||
"Not yet supported MIME type: {} {}",
|
||||
ty, sub_ty
|
||||
)),
|
||||
Tag {
|
||||
span,
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
},
|
||||
)),
|
||||
}
|
||||
}
|
||||
None => Ok((
|
||||
None,
|
||||
UntaggedValue::string("No content type found".to_owned()),
|
||||
Tag {
|
||||
span,
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
},
|
||||
)),
|
||||
},
|
||||
Err(_) => Err(ShellError::labeled_error(
|
||||
"URL could not be opened",
|
||||
"url not found",
|
||||
span,
|
||||
)),
|
||||
}
|
||||
}
|
4
crates/nu_plugin_fetch/src/lib.rs
Normal file
4
crates/nu_plugin_fetch/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod fetch;
|
||||
mod nu;
|
||||
|
||||
pub use fetch::Fetch;
|
@ -1,299 +1,6 @@
|
||||
use futures::executor::block_on;
|
||||
use mime::Mime;
|
||||
use nu_errors::ShellError;
|
||||
use nu_plugin::{serve_plugin, Plugin};
|
||||
use nu_protocol::{
|
||||
CallInfo, CommandAction, ReturnSuccess, ReturnValue, Signature, SyntaxShape, UntaggedValue,
|
||||
Value,
|
||||
};
|
||||
use nu_source::{AnchorLocation, Span, Tag};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use surf::mime;
|
||||
|
||||
struct Fetch {
|
||||
path: Option<Value>,
|
||||
has_raw: bool,
|
||||
}
|
||||
|
||||
impl Fetch {
|
||||
fn new() -> Fetch {
|
||||
Fetch {
|
||||
path: None,
|
||||
has_raw: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(&mut self, call_info: CallInfo) -> ReturnValue {
|
||||
self.path = Some(
|
||||
match call_info.args.nth(0).ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"No file or directory specified",
|
||||
"for command",
|
||||
&call_info.name_tag,
|
||||
)
|
||||
})? {
|
||||
file => file.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
self.has_raw = call_info.args.has("raw");
|
||||
|
||||
ReturnSuccess::value(UntaggedValue::nothing().into_untagged_value())
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for Fetch {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("fetch")
|
||||
.desc("Load from a URL into a cell, convert to table if possible (avoid by appending '--raw')")
|
||||
.required(
|
||||
"path",
|
||||
SyntaxShape::Path,
|
||||
"the URL to fetch the contents from",
|
||||
)
|
||||
.switch("raw", "fetch contents as text rather than a table")
|
||||
.filter())
|
||||
}
|
||||
|
||||
fn begin_filter(&mut self, callinfo: CallInfo) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
self.setup(callinfo)?;
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn filter(&mut self, value: Value) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
Ok(vec![block_on(fetch_helper(
|
||||
&self.path.clone().ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"internal error: path not set",
|
||||
"path not set",
|
||||
&value.tag,
|
||||
)
|
||||
})?,
|
||||
self.has_raw,
|
||||
value,
|
||||
))])
|
||||
}
|
||||
}
|
||||
use nu_plugin::serve_plugin;
|
||||
use nu_plugin_fetch::Fetch;
|
||||
|
||||
fn main() {
|
||||
serve_plugin(&mut Fetch::new());
|
||||
}
|
||||
|
||||
async fn fetch_helper(path: &Value, has_raw: bool, row: Value) -> ReturnValue {
|
||||
let path_buf = path.as_path()?;
|
||||
let path_str = path_buf.display().to_string();
|
||||
|
||||
//FIXME: this is a workaround because plugins don't yet support per-item iteration
|
||||
let path_str = if path_str == "$it" {
|
||||
let path_buf = row.as_path()?;
|
||||
path_buf.display().to_string()
|
||||
} else {
|
||||
path_str
|
||||
};
|
||||
|
||||
let path_span = path.tag.span;
|
||||
|
||||
let result = fetch(&path_str, path_span).await;
|
||||
|
||||
if let Err(e) = result {
|
||||
return Err(e);
|
||||
}
|
||||
let (file_extension, contents, contents_tag) = result?;
|
||||
|
||||
let file_extension = if has_raw {
|
||||
None
|
||||
} else {
|
||||
// If the extension could not be determined via mimetype, try to use the path
|
||||
// extension. Some file types do not declare their mimetypes (such as bson files).
|
||||
file_extension.or_else(|| path_str.split('.').last().map(String::from))
|
||||
};
|
||||
|
||||
let tagged_contents = contents.retag(&contents_tag);
|
||||
|
||||
if let Some(extension) = file_extension {
|
||||
Ok(ReturnSuccess::Action(CommandAction::AutoConvert(
|
||||
tagged_contents,
|
||||
extension,
|
||||
)))
|
||||
} else {
|
||||
ReturnSuccess::value(tagged_contents)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch(
|
||||
location: &str,
|
||||
span: Span,
|
||||
) -> Result<(Option<String>, UntaggedValue, Tag), ShellError> {
|
||||
if url::Url::parse(location).is_err() {
|
||||
return Err(ShellError::labeled_error(
|
||||
"Incomplete or incorrect url",
|
||||
"expected a full url",
|
||||
span,
|
||||
));
|
||||
}
|
||||
|
||||
let response = surf::get(location).await;
|
||||
match response {
|
||||
Ok(mut r) => match r.headers().get("content-type") {
|
||||
Some(content_type) => {
|
||||
let content_type = Mime::from_str(content_type).map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
format!("MIME type unknown: {}", content_type),
|
||||
"given unknown MIME type",
|
||||
span,
|
||||
)
|
||||
})?;
|
||||
match (content_type.type_(), content_type.subtype()) {
|
||||
(mime::APPLICATION, mime::XML) => Ok((
|
||||
Some("xml".to_string()),
|
||||
UntaggedValue::string(r.body_string().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load text from remote url",
|
||||
"could not load",
|
||||
span,
|
||||
)
|
||||
})?),
|
||||
Tag {
|
||||
span,
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
},
|
||||
)),
|
||||
(mime::APPLICATION, mime::JSON) => Ok((
|
||||
Some("json".to_string()),
|
||||
UntaggedValue::string(r.body_string().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load text from remote url",
|
||||
"could not load",
|
||||
span,
|
||||
)
|
||||
})?),
|
||||
Tag {
|
||||
span,
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
},
|
||||
)),
|
||||
(mime::APPLICATION, mime::OCTET_STREAM) => {
|
||||
let buf: Vec<u8> = r.body_bytes().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load binary file",
|
||||
"could not load",
|
||||
span,
|
||||
)
|
||||
})?;
|
||||
Ok((
|
||||
None,
|
||||
UntaggedValue::binary(buf),
|
||||
Tag {
|
||||
span,
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
},
|
||||
))
|
||||
}
|
||||
(mime::IMAGE, mime::SVG) => Ok((
|
||||
Some("svg".to_string()),
|
||||
UntaggedValue::string(r.body_string().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load svg from remote url",
|
||||
"could not load",
|
||||
span,
|
||||
)
|
||||
})?),
|
||||
Tag {
|
||||
span,
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
},
|
||||
)),
|
||||
(mime::IMAGE, image_ty) => {
|
||||
let buf: Vec<u8> = r.body_bytes().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load image file",
|
||||
"could not load",
|
||||
span,
|
||||
)
|
||||
})?;
|
||||
Ok((
|
||||
Some(image_ty.to_string()),
|
||||
UntaggedValue::binary(buf),
|
||||
Tag {
|
||||
span,
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
},
|
||||
))
|
||||
}
|
||||
(mime::TEXT, mime::HTML) => Ok((
|
||||
Some("html".to_string()),
|
||||
UntaggedValue::string(r.body_string().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load text from remote url",
|
||||
"could not load",
|
||||
span,
|
||||
)
|
||||
})?),
|
||||
Tag {
|
||||
span,
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
},
|
||||
)),
|
||||
(mime::TEXT, mime::PLAIN) => {
|
||||
let path_extension = url::Url::parse(location)
|
||||
.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
format!("Cannot parse URL: {}", location),
|
||||
"cannot parse",
|
||||
span,
|
||||
)
|
||||
})?
|
||||
.path_segments()
|
||||
.and_then(|segments| segments.last())
|
||||
.and_then(|name| if name.is_empty() { None } else { Some(name) })
|
||||
.and_then(|name| {
|
||||
PathBuf::from(name)
|
||||
.extension()
|
||||
.map(|name| name.to_string_lossy().to_string())
|
||||
});
|
||||
|
||||
Ok((
|
||||
path_extension,
|
||||
UntaggedValue::string(r.body_string().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load text from remote url",
|
||||
"could not load",
|
||||
span,
|
||||
)
|
||||
})?),
|
||||
Tag {
|
||||
span,
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
},
|
||||
))
|
||||
}
|
||||
(ty, sub_ty) => Ok((
|
||||
None,
|
||||
UntaggedValue::string(format!(
|
||||
"Not yet supported MIME type: {} {}",
|
||||
ty, sub_ty
|
||||
)),
|
||||
Tag {
|
||||
span,
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
},
|
||||
)),
|
||||
}
|
||||
}
|
||||
None => Ok((
|
||||
None,
|
||||
UntaggedValue::string("No content type found".to_owned()),
|
||||
Tag {
|
||||
span,
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
},
|
||||
)),
|
||||
},
|
||||
Err(_) => Err(ShellError::labeled_error(
|
||||
"URL could not be opened",
|
||||
"url not found",
|
||||
span,
|
||||
)),
|
||||
}
|
||||
serve_plugin(&mut Fetch::new())
|
||||
}
|
||||
|
40
crates/nu_plugin_fetch/src/nu/mod.rs
Normal file
40
crates/nu_plugin_fetch/src/nu/mod.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use futures::executor::block_on;
|
||||
use nu_errors::ShellError;
|
||||
use nu_plugin::Plugin;
|
||||
use nu_protocol::{CallInfo, ReturnValue, Signature, SyntaxShape, Value};
|
||||
|
||||
use crate::fetch::fetch_helper;
|
||||
use crate::Fetch;
|
||||
|
||||
impl Plugin for Fetch {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("fetch")
|
||||
.desc("Load from a URL into a cell, convert to table if possible (avoid by appending '--raw')")
|
||||
.required(
|
||||
"path",
|
||||
SyntaxShape::Path,
|
||||
"the URL to fetch the contents from",
|
||||
)
|
||||
.switch("raw", "fetch contents as text rather than a table", Some('r'))
|
||||
.filter())
|
||||
}
|
||||
|
||||
fn begin_filter(&mut self, callinfo: CallInfo) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
self.setup(callinfo)?;
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn filter(&mut self, value: Value) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
Ok(vec![block_on(fetch_helper(
|
||||
&self.path.clone().ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"internal error: path not set",
|
||||
"path not set",
|
||||
&value.tag,
|
||||
)
|
||||
})?,
|
||||
self.has_raw,
|
||||
value,
|
||||
))])
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nu_plugin_inc"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "A version incrementer plugin for Nushell"
|
||||
@ -10,12 +10,13 @@ license = "MIT"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.9.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.9.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.9.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.9.0" }
|
||||
nu-value-ext = { path = "../nu-value-ext", version = "0.9.0" }
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.10.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.10.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.10.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.10.0" }
|
||||
nu-value-ext = { path = "../nu-value-ext", version = "0.10.0" }
|
||||
|
||||
semver = "0.9.0"
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = { version = "0.9.0", path = "../nu-build" }
|
||||
nu-build = { version = "0.10.0", path = "../nu-build" }
|
||||
|
@ -1,5 +1,5 @@
|
||||
mod inc;
|
||||
mod nu_plugin_inc;
|
||||
mod nu;
|
||||
|
||||
pub use inc::Inc;
|
||||
|
||||
|
@ -16,9 +16,21 @@ impl Plugin for Inc {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("inc")
|
||||
.desc("Increment a value or version. Optionally use the column of a table.")
|
||||
.switch("major", "increment the major version (eg 1.2.1 -> 2.0.0)")
|
||||
.switch("minor", "increment the minor version (eg 1.2.1 -> 1.3.0)")
|
||||
.switch("patch", "increment the patch version (eg 1.2.1 -> 1.2.2)")
|
||||
.switch(
|
||||
"major",
|
||||
"increment the major version (eg 1.2.1 -> 2.0.0)",
|
||||
Some('M'),
|
||||
)
|
||||
.switch(
|
||||
"minor",
|
||||
"increment the minor version (eg 1.2.1 -> 1.3.0)",
|
||||
Some('m'),
|
||||
)
|
||||
.switch(
|
||||
"patch",
|
||||
"increment the patch version (eg 1.2.1 -> 1.2.2)",
|
||||
Some('p'),
|
||||
)
|
||||
.rest(SyntaxShape::ColumnPath, "the column(s) to update")
|
||||
.filter())
|
||||
}
|
||||
@ -37,7 +49,9 @@ impl Plugin for Inc {
|
||||
if let Some(args) = call_info.args.positional {
|
||||
for arg in args {
|
||||
match arg {
|
||||
table @ Value {
|
||||
table
|
||||
@
|
||||
Value {
|
||||
value: UntaggedValue::Primitive(Primitive::ColumnPath(_)),
|
||||
..
|
||||
} => {
|
@ -1,18 +1,21 @@
|
||||
[package]
|
||||
name = "nu_plugin_match"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "A regex match plugin for Nushell"
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.9.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.9.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.9.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.9.0" }
|
||||
futures-preview = { version = "=0.3.0-alpha.19", features = ["compat", "io-compat"] }
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.10.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.10.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.10.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.10.0" }
|
||||
futures = { version = "0.3", features = ["compat", "io-compat"] }
|
||||
regex = "1"
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = { version = "0.9.0", path = "../nu-build" }
|
||||
nu-build = { version = "0.10.0", path = "../nu-build" }
|
||||
|
4
crates/nu_plugin_match/src/lib.rs
Normal file
4
crates/nu_plugin_match/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod match_;
|
||||
mod nu;
|
||||
|
||||
pub use match_::Match;
|
@ -1,112 +1,5 @@
|
||||
use nu_errors::ShellError;
|
||||
use nu_plugin::{serve_plugin, Plugin};
|
||||
use nu_protocol::{
|
||||
CallInfo, Primitive, ReturnSuccess, ReturnValue, Signature, SyntaxShape, UntaggedValue, Value,
|
||||
};
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
struct Match {
|
||||
column: String,
|
||||
regex: Regex,
|
||||
}
|
||||
|
||||
impl Match {
|
||||
#[allow(clippy::trivial_regex)]
|
||||
fn new() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
Ok(Match {
|
||||
column: String::new(),
|
||||
regex: Regex::new("")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for Match {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("match")
|
||||
.desc("filter rows by regex")
|
||||
.required("member", SyntaxShape::Member, "the column name to match")
|
||||
.required("regex", SyntaxShape::String, "the regex to match with")
|
||||
.filter())
|
||||
}
|
||||
fn begin_filter(&mut self, call_info: CallInfo) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
if let Some(args) = call_info.args.positional {
|
||||
match &args[0] {
|
||||
Value {
|
||||
value: UntaggedValue::Primitive(Primitive::String(s)),
|
||||
..
|
||||
} => {
|
||||
self.column = s.clone();
|
||||
}
|
||||
Value { tag, .. } => {
|
||||
return Err(ShellError::labeled_error(
|
||||
"Unrecognized type in params",
|
||||
"value",
|
||||
tag,
|
||||
));
|
||||
}
|
||||
}
|
||||
match &args[1] {
|
||||
Value {
|
||||
value: UntaggedValue::Primitive(Primitive::String(s)),
|
||||
tag,
|
||||
} => {
|
||||
self.regex = Regex::new(s).map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Internal error while creating regex",
|
||||
"internal error created by pattern",
|
||||
tag,
|
||||
)
|
||||
})?;
|
||||
}
|
||||
Value { tag, .. } => {
|
||||
return Err(ShellError::labeled_error(
|
||||
"Unrecognized type in params",
|
||||
"unexpected value",
|
||||
tag,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn filter(&mut self, input: Value) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
let flag: bool;
|
||||
match &input {
|
||||
Value {
|
||||
value: UntaggedValue::Row(dict),
|
||||
tag,
|
||||
} => {
|
||||
if let Some(val) = dict.entries.get(&self.column) {
|
||||
if let Ok(s) = val.as_string() {
|
||||
flag = self.regex.is_match(&s);
|
||||
} else {
|
||||
return Err(ShellError::labeled_error(
|
||||
"expected string",
|
||||
"value",
|
||||
val.tag(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(ShellError::labeled_error(
|
||||
format!("column not in row! {:?} {:?}", &self.column, dict),
|
||||
"row",
|
||||
tag,
|
||||
));
|
||||
}
|
||||
}
|
||||
Value { tag, .. } => {
|
||||
return Err(ShellError::labeled_error("Expected row", "value", tag));
|
||||
}
|
||||
}
|
||||
if flag {
|
||||
Ok(vec![Ok(ReturnSuccess::Value(input))])
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
}
|
||||
use nu_plugin::serve_plugin;
|
||||
use nu_plugin_match::Match;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
serve_plugin(&mut Match::new()?);
|
||||
|
16
crates/nu_plugin_match/src/match_.rs
Normal file
16
crates/nu_plugin_match/src/match_.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use regex::Regex;
|
||||
|
||||
pub struct Match {
|
||||
pub column: String,
|
||||
pub regex: Regex,
|
||||
}
|
||||
|
||||
impl Match {
|
||||
#[allow(clippy::trivial_regex)]
|
||||
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
Ok(Match {
|
||||
column: String::new(),
|
||||
regex: Regex::new("")?,
|
||||
})
|
||||
}
|
||||
}
|
96
crates/nu_plugin_match/src/nu/mod.rs
Normal file
96
crates/nu_plugin_match/src/nu/mod.rs
Normal file
@ -0,0 +1,96 @@
|
||||
use nu_errors::ShellError;
|
||||
use nu_plugin::Plugin;
|
||||
use nu_protocol::{
|
||||
CallInfo, Primitive, ReturnSuccess, ReturnValue, Signature, SyntaxShape, UntaggedValue, Value,
|
||||
};
|
||||
|
||||
use crate::Match;
|
||||
use regex::Regex;
|
||||
|
||||
impl Plugin for Match {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("match")
|
||||
.desc("filter rows by regex")
|
||||
.required("member", SyntaxShape::Member, "the column name to match")
|
||||
.required("regex", SyntaxShape::String, "the regex to match with")
|
||||
.filter())
|
||||
}
|
||||
|
||||
fn begin_filter(&mut self, call_info: CallInfo) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
if let Some(args) = call_info.args.positional {
|
||||
match &args[0] {
|
||||
Value {
|
||||
value: UntaggedValue::Primitive(Primitive::String(s)),
|
||||
..
|
||||
} => {
|
||||
self.column = s.clone();
|
||||
}
|
||||
Value { tag, .. } => {
|
||||
return Err(ShellError::labeled_error(
|
||||
"Unrecognized type in params",
|
||||
"value",
|
||||
tag,
|
||||
));
|
||||
}
|
||||
}
|
||||
match &args[1] {
|
||||
Value {
|
||||
value: UntaggedValue::Primitive(Primitive::String(s)),
|
||||
tag,
|
||||
} => {
|
||||
self.regex = Regex::new(s).map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Internal error while creating regex",
|
||||
"internal error created by pattern",
|
||||
tag,
|
||||
)
|
||||
})?;
|
||||
}
|
||||
Value { tag, .. } => {
|
||||
return Err(ShellError::labeled_error(
|
||||
"Unrecognized type in params",
|
||||
"unexpected value",
|
||||
tag,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn filter(&mut self, input: Value) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
let flag: bool;
|
||||
match &input {
|
||||
Value {
|
||||
value: UntaggedValue::Row(dict),
|
||||
tag,
|
||||
} => {
|
||||
if let Some(val) = dict.entries.get(&self.column) {
|
||||
if let Ok(s) = val.as_string() {
|
||||
flag = self.regex.is_match(&s);
|
||||
} else {
|
||||
return Err(ShellError::labeled_error(
|
||||
"expected string",
|
||||
"value",
|
||||
val.tag(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(ShellError::labeled_error(
|
||||
format!("column not in row! {:?} {:?}", &self.column, dict),
|
||||
"row",
|
||||
tag,
|
||||
));
|
||||
}
|
||||
}
|
||||
Value { tag, .. } => {
|
||||
return Err(ShellError::labeled_error("Expected row", "value", tag));
|
||||
}
|
||||
}
|
||||
if flag {
|
||||
Ok(vec![Ok(ReturnSuccess::Value(input))])
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +1,25 @@
|
||||
[package]
|
||||
name = "nu_plugin_post"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "An HTTP post plugin for Nushell"
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.9.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.9.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.9.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.9.0" }
|
||||
futures-preview = { version = "=0.3.0-alpha.19", features = ["compat", "io-compat"] }
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.10.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.10.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.10.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.10.0" }
|
||||
futures = { version = "0.3", features = ["compat", "io-compat"] }
|
||||
surf = "1.0.3"
|
||||
url = "2.1.0"
|
||||
serde_json = "1.0.44"
|
||||
url = "2.1.1"
|
||||
serde_json = "1.0.46"
|
||||
base64 = "0.11"
|
||||
num-traits = "0.2.10"
|
||||
num-traits = "0.2.11"
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = { version = "0.9.0", path = "../nu-build" }
|
||||
nu-build = { version = "0.10.0", path = "../nu-build" }
|
||||
|
4
crates/nu_plugin_post/src/lib.rs
Normal file
4
crates/nu_plugin_post/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod nu;
|
||||
mod post;
|
||||
|
||||
pub use post::Post;
|
@ -1,569 +1,6 @@
|
||||
use base64::encode;
|
||||
use futures::executor::block_on;
|
||||
use mime::Mime;
|
||||
use nu_errors::{CoerceInto, ShellError};
|
||||
use nu_plugin::{serve_plugin, Plugin};
|
||||
use nu_protocol::{
|
||||
CallInfo, CommandAction, Primitive, ReturnSuccess, ReturnValue, Signature, SyntaxShape,
|
||||
UnspannedPathMember, UntaggedValue, Value,
|
||||
};
|
||||
use nu_source::{AnchorLocation, Tag, TaggedItem};
|
||||
use num_traits::cast::ToPrimitive;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use surf::mime;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum HeaderKind {
|
||||
ContentType(String),
|
||||
ContentLength(String),
|
||||
}
|
||||
|
||||
struct Post {
|
||||
path: Option<Value>,
|
||||
has_raw: bool,
|
||||
body: Option<Value>,
|
||||
user: Option<String>,
|
||||
password: Option<String>,
|
||||
headers: Vec<HeaderKind>,
|
||||
tag: Tag,
|
||||
}
|
||||
|
||||
impl Post {
|
||||
fn new() -> Post {
|
||||
Post {
|
||||
path: None,
|
||||
has_raw: false,
|
||||
body: None,
|
||||
user: None,
|
||||
password: None,
|
||||
headers: vec![],
|
||||
tag: Tag::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(&mut self, call_info: CallInfo) -> ReturnValue {
|
||||
self.path = Some(
|
||||
match call_info.args.nth(0).ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"No file or directory specified",
|
||||
"for command",
|
||||
&call_info.name_tag,
|
||||
)
|
||||
})? {
|
||||
file => file.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
self.has_raw = call_info.args.has("raw");
|
||||
|
||||
self.body = match call_info.args.nth(1).ok_or_else(|| {
|
||||
ShellError::labeled_error("No body specified", "for command", &call_info.name_tag)
|
||||
})? {
|
||||
file => Some(file.clone()),
|
||||
};
|
||||
|
||||
self.user = match call_info.args.get("user") {
|
||||
Some(user) => Some(user.as_string()?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
self.password = match call_info.args.get("password") {
|
||||
Some(password) => Some(password.as_string()?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
self.headers = get_headers(&call_info)?;
|
||||
|
||||
self.tag = call_info.name_tag;
|
||||
|
||||
ReturnSuccess::value(UntaggedValue::nothing().into_untagged_value())
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for Post {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("post")
|
||||
.desc("Post content to a url and retrieve data as a table if possible.")
|
||||
.required("path", SyntaxShape::Any, "the URL to post to")
|
||||
.required("body", SyntaxShape::Any, "the contents of the post body")
|
||||
.named("user", SyntaxShape::Any, "the username when authenticating")
|
||||
.named(
|
||||
"password",
|
||||
SyntaxShape::Any,
|
||||
"the password when authenticating",
|
||||
)
|
||||
.named(
|
||||
"content-type",
|
||||
SyntaxShape::Any,
|
||||
"the MIME type of content to post",
|
||||
)
|
||||
.named(
|
||||
"content-length",
|
||||
SyntaxShape::Any,
|
||||
"the length of the content being posted",
|
||||
)
|
||||
.switch("raw", "return values as a string instead of a table")
|
||||
.filter())
|
||||
}
|
||||
|
||||
fn begin_filter(&mut self, call_info: CallInfo) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
self.setup(call_info)?;
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn filter(&mut self, row: Value) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
Ok(vec![block_on(post_helper(
|
||||
&self.path.clone().ok_or_else(|| {
|
||||
ShellError::labeled_error("expected a 'path'", "expected a 'path'", &self.tag)
|
||||
})?,
|
||||
self.has_raw,
|
||||
&self.body.clone().ok_or_else(|| {
|
||||
ShellError::labeled_error("expected a 'body'", "expected a 'body'", &self.tag)
|
||||
})?,
|
||||
self.user.clone(),
|
||||
self.password.clone(),
|
||||
&self.headers.clone(),
|
||||
row,
|
||||
))])
|
||||
}
|
||||
}
|
||||
use nu_plugin::serve_plugin;
|
||||
use nu_plugin_post::Post;
|
||||
|
||||
fn main() {
|
||||
serve_plugin(&mut Post::new());
|
||||
}
|
||||
|
||||
async fn post_helper(
|
||||
path: &Value,
|
||||
has_raw: bool,
|
||||
body: &Value,
|
||||
user: Option<String>,
|
||||
password: Option<String>,
|
||||
headers: &[HeaderKind],
|
||||
row: Value,
|
||||
) -> ReturnValue {
|
||||
let path_tag = path.tag.clone();
|
||||
let path_str = path.as_string()?.to_string();
|
||||
|
||||
//FIXME: this is a workaround because plugins don't yet support per-item iteration
|
||||
let path_str = if path_str == "$it" {
|
||||
let path_buf = row.as_path()?;
|
||||
path_buf.display().to_string()
|
||||
} else {
|
||||
path_str
|
||||
};
|
||||
|
||||
//FIXME: this is a workaround because plugins don't yet support per-item iteration
|
||||
let body = if let Ok(x) = body.as_string() {
|
||||
if x == "$it" {
|
||||
&row
|
||||
} else {
|
||||
body
|
||||
}
|
||||
} else {
|
||||
body
|
||||
};
|
||||
|
||||
let (file_extension, contents, contents_tag) =
|
||||
post(&path_str, &body, user, password, &headers, path_tag.clone()).await?;
|
||||
|
||||
let file_extension = if has_raw {
|
||||
None
|
||||
} else {
|
||||
// If the extension could not be determined via mimetype, try to use the path
|
||||
// extension. Some file types do not declare their mimetypes (such as bson files).
|
||||
file_extension.or_else(|| path_str.split('.').last().map(String::from))
|
||||
};
|
||||
|
||||
let tagged_contents = contents.into_value(&contents_tag);
|
||||
|
||||
if let Some(extension) = file_extension {
|
||||
Ok(ReturnSuccess::Action(CommandAction::AutoConvert(
|
||||
tagged_contents,
|
||||
extension,
|
||||
)))
|
||||
} else {
|
||||
ReturnSuccess::value(tagged_contents)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn post(
|
||||
location: &str,
|
||||
body: &Value,
|
||||
user: Option<String>,
|
||||
password: Option<String>,
|
||||
headers: &[HeaderKind],
|
||||
tag: Tag,
|
||||
) -> Result<(Option<String>, UntaggedValue, Tag), ShellError> {
|
||||
if location.starts_with("http:") || location.starts_with("https:") {
|
||||
let login = match (user, password) {
|
||||
(Some(user), Some(password)) => Some(encode(&format!("{}:{}", user, password))),
|
||||
(Some(user), _) => Some(encode(&format!("{}:", user))),
|
||||
_ => None,
|
||||
};
|
||||
let response = match body {
|
||||
Value {
|
||||
value: UntaggedValue::Primitive(Primitive::String(body_str)),
|
||||
..
|
||||
} => {
|
||||
let mut s = surf::post(location).body_string(body_str.to_string());
|
||||
if let Some(login) = login {
|
||||
s = s.set_header("Authorization", format!("Basic {}", login));
|
||||
}
|
||||
|
||||
for h in headers {
|
||||
s = match h {
|
||||
HeaderKind::ContentType(ct) => s.set_header("Content-Type", ct),
|
||||
HeaderKind::ContentLength(cl) => s.set_header("Content-Length", cl),
|
||||
};
|
||||
}
|
||||
s.await
|
||||
}
|
||||
Value {
|
||||
value: UntaggedValue::Primitive(Primitive::Binary(b)),
|
||||
..
|
||||
} => {
|
||||
let mut s = surf::post(location).body_bytes(b);
|
||||
if let Some(login) = login {
|
||||
s = s.set_header("Authorization", format!("Basic {}", login));
|
||||
}
|
||||
s.await
|
||||
}
|
||||
Value { value, tag } => {
|
||||
match value_to_json_value(&value.clone().into_untagged_value()) {
|
||||
Ok(json_value) => match serde_json::to_string(&json_value) {
|
||||
Ok(result_string) => {
|
||||
let mut s = surf::post(location).body_string(result_string);
|
||||
|
||||
if let Some(login) = login {
|
||||
s = s.set_header("Authorization", format!("Basic {}", login));
|
||||
}
|
||||
s.await
|
||||
}
|
||||
_ => {
|
||||
return Err(ShellError::labeled_error(
|
||||
"Could not automatically convert table",
|
||||
"needs manual conversion",
|
||||
tag,
|
||||
));
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return Err(ShellError::labeled_error(
|
||||
"Could not automatically convert table",
|
||||
"needs manual conversion",
|
||||
tag,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
match response {
|
||||
Ok(mut r) => match r.headers().get("content-type") {
|
||||
Some(content_type) => {
|
||||
let content_type = Mime::from_str(content_type).map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
format!("Unknown MIME type: {}", content_type),
|
||||
"unknown MIME type",
|
||||
&tag,
|
||||
)
|
||||
})?;
|
||||
match (content_type.type_(), content_type.subtype()) {
|
||||
(mime::APPLICATION, mime::XML) => Ok((
|
||||
Some("xml".to_string()),
|
||||
UntaggedValue::string(r.body_string().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load text from remote url",
|
||||
"could not load",
|
||||
&tag,
|
||||
)
|
||||
})?),
|
||||
Tag {
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
span: tag.span,
|
||||
},
|
||||
)),
|
||||
(mime::APPLICATION, mime::JSON) => Ok((
|
||||
Some("json".to_string()),
|
||||
UntaggedValue::string(r.body_string().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load text from remote url",
|
||||
"could not load",
|
||||
&tag,
|
||||
)
|
||||
})?),
|
||||
Tag {
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
span: tag.span,
|
||||
},
|
||||
)),
|
||||
(mime::APPLICATION, mime::OCTET_STREAM) => {
|
||||
let buf: Vec<u8> = r.body_bytes().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load binary file",
|
||||
"could not load",
|
||||
&tag,
|
||||
)
|
||||
})?;
|
||||
Ok((
|
||||
None,
|
||||
UntaggedValue::binary(buf),
|
||||
Tag {
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
span: tag.span,
|
||||
},
|
||||
))
|
||||
}
|
||||
(mime::IMAGE, image_ty) => {
|
||||
let buf: Vec<u8> = r.body_bytes().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load image file",
|
||||
"could not load",
|
||||
&tag,
|
||||
)
|
||||
})?;
|
||||
Ok((
|
||||
Some(image_ty.to_string()),
|
||||
UntaggedValue::binary(buf),
|
||||
Tag {
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
span: tag.span,
|
||||
},
|
||||
))
|
||||
}
|
||||
(mime::TEXT, mime::HTML) => Ok((
|
||||
Some("html".to_string()),
|
||||
UntaggedValue::string(r.body_string().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load text from remote url",
|
||||
"could not load",
|
||||
&tag,
|
||||
)
|
||||
})?),
|
||||
Tag {
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
span: tag.span,
|
||||
},
|
||||
)),
|
||||
(mime::TEXT, mime::PLAIN) => {
|
||||
let path_extension = url::Url::parse(location)
|
||||
.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
format!("could not parse URL: {}", location),
|
||||
"could not parse URL",
|
||||
&tag,
|
||||
)
|
||||
})?
|
||||
.path_segments()
|
||||
.and_then(|segments| segments.last())
|
||||
.and_then(|name| if name.is_empty() { None } else { Some(name) })
|
||||
.and_then(|name| {
|
||||
PathBuf::from(name)
|
||||
.extension()
|
||||
.map(|name| name.to_string_lossy().to_string())
|
||||
});
|
||||
|
||||
Ok((
|
||||
path_extension,
|
||||
UntaggedValue::string(r.body_string().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load text from remote url",
|
||||
"could not load",
|
||||
&tag,
|
||||
)
|
||||
})?),
|
||||
Tag {
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
span: tag.span,
|
||||
},
|
||||
))
|
||||
}
|
||||
(ty, sub_ty) => Ok((
|
||||
None,
|
||||
UntaggedValue::string(format!(
|
||||
"Not yet supported MIME type: {} {}",
|
||||
ty, sub_ty
|
||||
)),
|
||||
Tag {
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
span: tag.span,
|
||||
},
|
||||
)),
|
||||
}
|
||||
}
|
||||
None => Ok((
|
||||
None,
|
||||
UntaggedValue::string("No content type found".to_owned()),
|
||||
Tag {
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
span: tag.span,
|
||||
},
|
||||
)),
|
||||
},
|
||||
Err(_) => Err(ShellError::labeled_error(
|
||||
"URL could not be opened",
|
||||
"url not found",
|
||||
tag,
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
Err(ShellError::labeled_error(
|
||||
"Expected a url",
|
||||
"needs a url",
|
||||
tag,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME FIXME FIXME
|
||||
// Ultimately, we don't want to duplicate to-json here, but we need to because there isn't an easy way to call into it, yet
|
||||
pub fn value_to_json_value(v: &Value) -> Result<serde_json::Value, ShellError> {
|
||||
Ok(match &v.value {
|
||||
UntaggedValue::Primitive(Primitive::Boolean(b)) => serde_json::Value::Bool(*b),
|
||||
UntaggedValue::Primitive(Primitive::Bytes(b)) => serde_json::Value::Number(
|
||||
serde_json::Number::from(b.to_u64().expect("What about really big numbers")),
|
||||
),
|
||||
UntaggedValue::Primitive(Primitive::Duration(secs)) => {
|
||||
serde_json::Value::Number(serde_json::Number::from(*secs))
|
||||
}
|
||||
UntaggedValue::Primitive(Primitive::Date(d)) => serde_json::Value::String(d.to_string()),
|
||||
UntaggedValue::Primitive(Primitive::EndOfStream) => serde_json::Value::Null,
|
||||
UntaggedValue::Primitive(Primitive::BeginningOfStream) => serde_json::Value::Null,
|
||||
UntaggedValue::Primitive(Primitive::Decimal(f)) => serde_json::Value::Number(
|
||||
serde_json::Number::from_f64(
|
||||
f.to_f64().expect("TODO: What about really big decimals?"),
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"Can not convert big decimal to f64",
|
||||
"cannot convert big decimal to f64",
|
||||
&v.tag,
|
||||
)
|
||||
})?,
|
||||
),
|
||||
UntaggedValue::Primitive(Primitive::Int(i)) => {
|
||||
serde_json::Value::Number(serde_json::Number::from(CoerceInto::<i64>::coerce_into(
|
||||
i.tagged(&v.tag),
|
||||
"converting to JSON number",
|
||||
)?))
|
||||
}
|
||||
UntaggedValue::Primitive(Primitive::Nothing) => serde_json::Value::Null,
|
||||
UntaggedValue::Primitive(Primitive::Pattern(s)) => serde_json::Value::String(s.clone()),
|
||||
UntaggedValue::Primitive(Primitive::String(s)) => serde_json::Value::String(s.clone()),
|
||||
UntaggedValue::Primitive(Primitive::Line(s)) => serde_json::Value::String(s.clone()),
|
||||
UntaggedValue::Primitive(Primitive::ColumnPath(path)) => serde_json::Value::Array(
|
||||
path.iter()
|
||||
.map(|x| match &x.unspanned {
|
||||
UnspannedPathMember::String(string) => {
|
||||
Ok(serde_json::Value::String(string.clone()))
|
||||
}
|
||||
UnspannedPathMember::Int(int) => Ok(serde_json::Value::Number(
|
||||
serde_json::Number::from(CoerceInto::<i64>::coerce_into(
|
||||
int.tagged(&v.tag),
|
||||
"converting to JSON number",
|
||||
)?),
|
||||
)),
|
||||
})
|
||||
.collect::<Result<Vec<serde_json::Value>, ShellError>>()?,
|
||||
),
|
||||
UntaggedValue::Primitive(Primitive::Path(s)) => {
|
||||
serde_json::Value::String(s.display().to_string())
|
||||
}
|
||||
|
||||
UntaggedValue::Table(l) => serde_json::Value::Array(json_list(l)?),
|
||||
UntaggedValue::Error(e) => return Err(e.clone()),
|
||||
UntaggedValue::Block(_) | UntaggedValue::Primitive(Primitive::Range(_)) => {
|
||||
serde_json::Value::Null
|
||||
}
|
||||
UntaggedValue::Primitive(Primitive::Binary(b)) => {
|
||||
let mut output = vec![];
|
||||
|
||||
for item in b.iter() {
|
||||
output.push(serde_json::Value::Number(
|
||||
serde_json::Number::from_f64(*item as f64).ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"Cannot create number from from f64",
|
||||
"cannot created number from f64",
|
||||
&v.tag,
|
||||
)
|
||||
})?,
|
||||
));
|
||||
}
|
||||
serde_json::Value::Array(output)
|
||||
}
|
||||
UntaggedValue::Row(o) => {
|
||||
let mut m = serde_json::Map::new();
|
||||
for (k, v) in o.entries.iter() {
|
||||
m.insert(k.clone(), value_to_json_value(v)?);
|
||||
}
|
||||
serde_json::Value::Object(m)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn json_list(input: &[Value]) -> Result<Vec<serde_json::Value>, ShellError> {
|
||||
let mut out = vec![];
|
||||
|
||||
for value in input {
|
||||
out.push(value_to_json_value(value)?);
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn get_headers(call_info: &CallInfo) -> Result<Vec<HeaderKind>, ShellError> {
|
||||
let mut headers = vec![];
|
||||
|
||||
match extract_header_value(&call_info, "content-type") {
|
||||
Ok(h) => {
|
||||
if let Some(ct) = h {
|
||||
headers.push(HeaderKind::ContentType(ct))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
match extract_header_value(&call_info, "content-length") {
|
||||
Ok(h) => {
|
||||
if let Some(cl) = h {
|
||||
headers.push(HeaderKind::ContentLength(cl))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
fn extract_header_value(call_info: &CallInfo, key: &str) -> Result<Option<String>, ShellError> {
|
||||
if call_info.args.has(key) {
|
||||
let tagged = call_info.args.get(key);
|
||||
let val = match tagged {
|
||||
Some(Value {
|
||||
value: UntaggedValue::Primitive(Primitive::String(s)),
|
||||
..
|
||||
}) => s.clone(),
|
||||
Some(Value { tag, .. }) => {
|
||||
return Err(ShellError::labeled_error(
|
||||
format!("{} not in expected format. Expected string.", key),
|
||||
"post error",
|
||||
tag,
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
return Err(ShellError::labeled_error(
|
||||
format!("{} not in expected format. Expected string.", key),
|
||||
"post error",
|
||||
Tag::unknown(),
|
||||
));
|
||||
}
|
||||
};
|
||||
return Ok(Some(val));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
67
crates/nu_plugin_post/src/nu/mod.rs
Normal file
67
crates/nu_plugin_post/src/nu/mod.rs
Normal file
@ -0,0 +1,67 @@
|
||||
use futures::executor::block_on;
|
||||
use nu_errors::ShellError;
|
||||
use nu_plugin::Plugin;
|
||||
use nu_protocol::{CallInfo, ReturnValue, Signature, SyntaxShape, Value};
|
||||
|
||||
use crate::post::post_helper;
|
||||
use crate::Post;
|
||||
|
||||
impl Plugin for Post {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("post")
|
||||
.desc("Post content to a url and retrieve data as a table if possible.")
|
||||
.required("path", SyntaxShape::Any, "the URL to post to")
|
||||
.required("body", SyntaxShape::Any, "the contents of the post body")
|
||||
.named(
|
||||
"user",
|
||||
SyntaxShape::Any,
|
||||
"the username when authenticating",
|
||||
Some('u'),
|
||||
)
|
||||
.named(
|
||||
"password",
|
||||
SyntaxShape::Any,
|
||||
"the password when authenticating",
|
||||
Some('p'),
|
||||
)
|
||||
.named(
|
||||
"content-type",
|
||||
SyntaxShape::Any,
|
||||
"the MIME type of content to post",
|
||||
Some('t'),
|
||||
)
|
||||
.named(
|
||||
"content-length",
|
||||
SyntaxShape::Any,
|
||||
"the length of the content being posted",
|
||||
Some('l'),
|
||||
)
|
||||
.switch(
|
||||
"raw",
|
||||
"return values as a string instead of a table",
|
||||
Some('r'),
|
||||
)
|
||||
.filter())
|
||||
}
|
||||
|
||||
fn begin_filter(&mut self, call_info: CallInfo) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
self.setup(call_info)?;
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn filter(&mut self, row: Value) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
Ok(vec![block_on(post_helper(
|
||||
&self.path.clone().ok_or_else(|| {
|
||||
ShellError::labeled_error("expected a 'path'", "expected a 'path'", &self.tag)
|
||||
})?,
|
||||
self.has_raw,
|
||||
&self.body.clone().ok_or_else(|| {
|
||||
ShellError::labeled_error("expected a 'body'", "expected a 'body'", &self.tag)
|
||||
})?,
|
||||
self.user.clone(),
|
||||
self.password.clone(),
|
||||
&self.headers.clone(),
|
||||
row,
|
||||
))])
|
||||
}
|
||||
}
|
516
crates/nu_plugin_post/src/post.rs
Normal file
516
crates/nu_plugin_post/src/post.rs
Normal file
@ -0,0 +1,516 @@
|
||||
use base64::encode;
|
||||
use mime::Mime;
|
||||
use nu_errors::{CoerceInto, ShellError};
|
||||
use nu_protocol::{
|
||||
CallInfo, CommandAction, Primitive, ReturnSuccess, ReturnValue, UnspannedPathMember,
|
||||
UntaggedValue, Value,
|
||||
};
|
||||
use nu_source::{AnchorLocation, Tag, TaggedItem};
|
||||
use num_traits::cast::ToPrimitive;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use surf::mime;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum HeaderKind {
|
||||
ContentType(String),
|
||||
ContentLength(String),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Post {
|
||||
pub path: Option<Value>,
|
||||
pub has_raw: bool,
|
||||
pub body: Option<Value>,
|
||||
pub user: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub headers: Vec<HeaderKind>,
|
||||
pub tag: Tag,
|
||||
}
|
||||
|
||||
impl Post {
|
||||
pub fn new() -> Post {
|
||||
Post {
|
||||
path: None,
|
||||
has_raw: false,
|
||||
body: None,
|
||||
user: None,
|
||||
password: None,
|
||||
headers: vec![],
|
||||
tag: Tag::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup(&mut self, call_info: CallInfo) -> ReturnValue {
|
||||
self.path = Some(
|
||||
match call_info.args.nth(0).ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"No file or directory specified",
|
||||
"for command",
|
||||
&call_info.name_tag,
|
||||
)
|
||||
})? {
|
||||
file => file.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
self.has_raw = call_info.args.has("raw");
|
||||
|
||||
self.body = match call_info.args.nth(1).ok_or_else(|| {
|
||||
ShellError::labeled_error("No body specified", "for command", &call_info.name_tag)
|
||||
})? {
|
||||
file => Some(file.clone()),
|
||||
};
|
||||
|
||||
self.user = match call_info.args.get("user") {
|
||||
Some(user) => Some(user.as_string()?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
self.password = match call_info.args.get("password") {
|
||||
Some(password) => Some(password.as_string()?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
self.headers = get_headers(&call_info)?;
|
||||
|
||||
self.tag = call_info.name_tag;
|
||||
|
||||
ReturnSuccess::value(UntaggedValue::nothing().into_untagged_value())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn post_helper(
|
||||
path: &Value,
|
||||
has_raw: bool,
|
||||
body: &Value,
|
||||
user: Option<String>,
|
||||
password: Option<String>,
|
||||
headers: &[HeaderKind],
|
||||
row: Value,
|
||||
) -> ReturnValue {
|
||||
let path_tag = path.tag.clone();
|
||||
let path_str = path.as_string()?.to_string();
|
||||
|
||||
//FIXME: this is a workaround because plugins don't yet support per-item iteration
|
||||
let path_str = if path_str == "$it" {
|
||||
let path_buf = row.as_path()?;
|
||||
path_buf.display().to_string()
|
||||
} else {
|
||||
path_str
|
||||
};
|
||||
|
||||
//FIXME: this is a workaround because plugins don't yet support per-item iteration
|
||||
let body = if let Ok(x) = body.as_string() {
|
||||
if x == "$it" {
|
||||
&row
|
||||
} else {
|
||||
body
|
||||
}
|
||||
} else {
|
||||
body
|
||||
};
|
||||
|
||||
let (file_extension, contents, contents_tag) =
|
||||
post(&path_str, &body, user, password, &headers, path_tag.clone()).await?;
|
||||
|
||||
let file_extension = if has_raw {
|
||||
None
|
||||
} else {
|
||||
// If the extension could not be determined via mimetype, try to use the path
|
||||
// extension. Some file types do not declare their mimetypes (such as bson files).
|
||||
file_extension.or_else(|| path_str.split('.').last().map(String::from))
|
||||
};
|
||||
|
||||
let tagged_contents = contents.into_value(&contents_tag);
|
||||
|
||||
if let Some(extension) = file_extension {
|
||||
Ok(ReturnSuccess::Action(CommandAction::AutoConvert(
|
||||
tagged_contents,
|
||||
extension,
|
||||
)))
|
||||
} else {
|
||||
ReturnSuccess::value(tagged_contents)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn post(
|
||||
location: &str,
|
||||
body: &Value,
|
||||
user: Option<String>,
|
||||
password: Option<String>,
|
||||
headers: &[HeaderKind],
|
||||
tag: Tag,
|
||||
) -> Result<(Option<String>, UntaggedValue, Tag), ShellError> {
|
||||
if location.starts_with("http:") || location.starts_with("https:") {
|
||||
let login = match (user, password) {
|
||||
(Some(user), Some(password)) => Some(encode(&format!("{}:{}", user, password))),
|
||||
(Some(user), _) => Some(encode(&format!("{}:", user))),
|
||||
_ => None,
|
||||
};
|
||||
let response = match body {
|
||||
Value {
|
||||
value: UntaggedValue::Primitive(Primitive::String(body_str)),
|
||||
..
|
||||
} => {
|
||||
let mut s = surf::post(location).body_string(body_str.to_string());
|
||||
if let Some(login) = login {
|
||||
s = s.set_header("Authorization", format!("Basic {}", login));
|
||||
}
|
||||
|
||||
for h in headers {
|
||||
s = match h {
|
||||
HeaderKind::ContentType(ct) => s.set_header("Content-Type", ct),
|
||||
HeaderKind::ContentLength(cl) => s.set_header("Content-Length", cl),
|
||||
};
|
||||
}
|
||||
s.await
|
||||
}
|
||||
Value {
|
||||
value: UntaggedValue::Primitive(Primitive::Binary(b)),
|
||||
..
|
||||
} => {
|
||||
let mut s = surf::post(location).body_bytes(b);
|
||||
if let Some(login) = login {
|
||||
s = s.set_header("Authorization", format!("Basic {}", login));
|
||||
}
|
||||
s.await
|
||||
}
|
||||
Value { value, tag } => {
|
||||
match value_to_json_value(&value.clone().into_untagged_value()) {
|
||||
Ok(json_value) => match serde_json::to_string(&json_value) {
|
||||
Ok(result_string) => {
|
||||
let mut s = surf::post(location).body_string(result_string);
|
||||
|
||||
if let Some(login) = login {
|
||||
s = s.set_header("Authorization", format!("Basic {}", login));
|
||||
}
|
||||
s.await
|
||||
}
|
||||
_ => {
|
||||
return Err(ShellError::labeled_error(
|
||||
"Could not automatically convert table",
|
||||
"needs manual conversion",
|
||||
tag,
|
||||
));
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return Err(ShellError::labeled_error(
|
||||
"Could not automatically convert table",
|
||||
"needs manual conversion",
|
||||
tag,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
match response {
|
||||
Ok(mut r) => match r.headers().get("content-type") {
|
||||
Some(content_type) => {
|
||||
let content_type = Mime::from_str(content_type).map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
format!("Unknown MIME type: {}", content_type),
|
||||
"unknown MIME type",
|
||||
&tag,
|
||||
)
|
||||
})?;
|
||||
match (content_type.type_(), content_type.subtype()) {
|
||||
(mime::APPLICATION, mime::XML) => Ok((
|
||||
Some("xml".to_string()),
|
||||
UntaggedValue::string(r.body_string().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load text from remote url",
|
||||
"could not load",
|
||||
&tag,
|
||||
)
|
||||
})?),
|
||||
Tag {
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
span: tag.span,
|
||||
},
|
||||
)),
|
||||
(mime::APPLICATION, mime::JSON) => Ok((
|
||||
Some("json".to_string()),
|
||||
UntaggedValue::string(r.body_string().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load text from remote url",
|
||||
"could not load",
|
||||
&tag,
|
||||
)
|
||||
})?),
|
||||
Tag {
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
span: tag.span,
|
||||
},
|
||||
)),
|
||||
(mime::APPLICATION, mime::OCTET_STREAM) => {
|
||||
let buf: Vec<u8> = r.body_bytes().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load binary file",
|
||||
"could not load",
|
||||
&tag,
|
||||
)
|
||||
})?;
|
||||
Ok((
|
||||
None,
|
||||
UntaggedValue::binary(buf),
|
||||
Tag {
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
span: tag.span,
|
||||
},
|
||||
))
|
||||
}
|
||||
(mime::IMAGE, image_ty) => {
|
||||
let buf: Vec<u8> = r.body_bytes().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load image file",
|
||||
"could not load",
|
||||
&tag,
|
||||
)
|
||||
})?;
|
||||
Ok((
|
||||
Some(image_ty.to_string()),
|
||||
UntaggedValue::binary(buf),
|
||||
Tag {
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
span: tag.span,
|
||||
},
|
||||
))
|
||||
}
|
||||
(mime::TEXT, mime::HTML) => Ok((
|
||||
Some("html".to_string()),
|
||||
UntaggedValue::string(r.body_string().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load text from remote url",
|
||||
"could not load",
|
||||
&tag,
|
||||
)
|
||||
})?),
|
||||
Tag {
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
span: tag.span,
|
||||
},
|
||||
)),
|
||||
(mime::TEXT, mime::PLAIN) => {
|
||||
let path_extension = url::Url::parse(location)
|
||||
.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
format!("could not parse URL: {}", location),
|
||||
"could not parse URL",
|
||||
&tag,
|
||||
)
|
||||
})?
|
||||
.path_segments()
|
||||
.and_then(|segments| segments.last())
|
||||
.and_then(|name| if name.is_empty() { None } else { Some(name) })
|
||||
.and_then(|name| {
|
||||
PathBuf::from(name)
|
||||
.extension()
|
||||
.map(|name| name.to_string_lossy().to_string())
|
||||
});
|
||||
|
||||
Ok((
|
||||
path_extension,
|
||||
UntaggedValue::string(r.body_string().await.map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"Could not load text from remote url",
|
||||
"could not load",
|
||||
&tag,
|
||||
)
|
||||
})?),
|
||||
Tag {
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
span: tag.span,
|
||||
},
|
||||
))
|
||||
}
|
||||
(ty, sub_ty) => Ok((
|
||||
None,
|
||||
UntaggedValue::string(format!(
|
||||
"Not yet supported MIME type: {} {}",
|
||||
ty, sub_ty
|
||||
)),
|
||||
Tag {
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
span: tag.span,
|
||||
},
|
||||
)),
|
||||
}
|
||||
}
|
||||
None => Ok((
|
||||
None,
|
||||
UntaggedValue::string("No content type found".to_owned()),
|
||||
Tag {
|
||||
anchor: Some(AnchorLocation::Url(location.to_string())),
|
||||
span: tag.span,
|
||||
},
|
||||
)),
|
||||
},
|
||||
Err(_) => Err(ShellError::labeled_error(
|
||||
"URL could not be opened",
|
||||
"url not found",
|
||||
tag,
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
Err(ShellError::labeled_error(
|
||||
"Expected a url",
|
||||
"needs a url",
|
||||
tag,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME FIXME FIXME
|
||||
// Ultimately, we don't want to duplicate to-json here, but we need to because there isn't an easy way to call into it, yet
|
||||
pub fn value_to_json_value(v: &Value) -> Result<serde_json::Value, ShellError> {
|
||||
Ok(match &v.value {
|
||||
UntaggedValue::Primitive(Primitive::Boolean(b)) => serde_json::Value::Bool(*b),
|
||||
UntaggedValue::Primitive(Primitive::Bytes(b)) => serde_json::Value::Number(
|
||||
serde_json::Number::from(b.to_u64().expect("What about really big numbers")),
|
||||
),
|
||||
UntaggedValue::Primitive(Primitive::Duration(secs)) => {
|
||||
serde_json::Value::Number(serde_json::Number::from(*secs))
|
||||
}
|
||||
UntaggedValue::Primitive(Primitive::Date(d)) => serde_json::Value::String(d.to_string()),
|
||||
UntaggedValue::Primitive(Primitive::EndOfStream) => serde_json::Value::Null,
|
||||
UntaggedValue::Primitive(Primitive::BeginningOfStream) => serde_json::Value::Null,
|
||||
UntaggedValue::Primitive(Primitive::Decimal(f)) => serde_json::Value::Number(
|
||||
serde_json::Number::from_f64(
|
||||
f.to_f64().expect("TODO: What about really big decimals?"),
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"Can not convert big decimal to f64",
|
||||
"cannot convert big decimal to f64",
|
||||
&v.tag,
|
||||
)
|
||||
})?,
|
||||
),
|
||||
UntaggedValue::Primitive(Primitive::Int(i)) => {
|
||||
serde_json::Value::Number(serde_json::Number::from(CoerceInto::<i64>::coerce_into(
|
||||
i.tagged(&v.tag),
|
||||
"converting to JSON number",
|
||||
)?))
|
||||
}
|
||||
UntaggedValue::Primitive(Primitive::Nothing) => serde_json::Value::Null,
|
||||
UntaggedValue::Primitive(Primitive::Pattern(s)) => serde_json::Value::String(s.clone()),
|
||||
UntaggedValue::Primitive(Primitive::String(s)) => serde_json::Value::String(s.clone()),
|
||||
UntaggedValue::Primitive(Primitive::Line(s)) => serde_json::Value::String(s.clone()),
|
||||
UntaggedValue::Primitive(Primitive::ColumnPath(path)) => serde_json::Value::Array(
|
||||
path.iter()
|
||||
.map(|x| match &x.unspanned {
|
||||
UnspannedPathMember::String(string) => {
|
||||
Ok(serde_json::Value::String(string.clone()))
|
||||
}
|
||||
UnspannedPathMember::Int(int) => Ok(serde_json::Value::Number(
|
||||
serde_json::Number::from(CoerceInto::<i64>::coerce_into(
|
||||
int.tagged(&v.tag),
|
||||
"converting to JSON number",
|
||||
)?),
|
||||
)),
|
||||
})
|
||||
.collect::<Result<Vec<serde_json::Value>, ShellError>>()?,
|
||||
),
|
||||
UntaggedValue::Primitive(Primitive::Path(s)) => {
|
||||
serde_json::Value::String(s.display().to_string())
|
||||
}
|
||||
|
||||
UntaggedValue::Table(l) => serde_json::Value::Array(json_list(l)?),
|
||||
UntaggedValue::Error(e) => return Err(e.clone()),
|
||||
UntaggedValue::Block(_) | UntaggedValue::Primitive(Primitive::Range(_)) => {
|
||||
serde_json::Value::Null
|
||||
}
|
||||
UntaggedValue::Primitive(Primitive::Binary(b)) => {
|
||||
let mut output = vec![];
|
||||
|
||||
for item in b.iter() {
|
||||
output.push(serde_json::Value::Number(
|
||||
serde_json::Number::from_f64(*item as f64).ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"Cannot create number from from f64",
|
||||
"cannot created number from f64",
|
||||
&v.tag,
|
||||
)
|
||||
})?,
|
||||
));
|
||||
}
|
||||
serde_json::Value::Array(output)
|
||||
}
|
||||
UntaggedValue::Row(o) => {
|
||||
let mut m = serde_json::Map::new();
|
||||
for (k, v) in o.entries.iter() {
|
||||
m.insert(k.clone(), value_to_json_value(v)?);
|
||||
}
|
||||
serde_json::Value::Object(m)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn json_list(input: &[Value]) -> Result<Vec<serde_json::Value>, ShellError> {
|
||||
let mut out = vec![];
|
||||
|
||||
for value in input {
|
||||
out.push(value_to_json_value(value)?);
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn get_headers(call_info: &CallInfo) -> Result<Vec<HeaderKind>, ShellError> {
|
||||
let mut headers = vec![];
|
||||
|
||||
match extract_header_value(&call_info, "content-type") {
|
||||
Ok(h) => {
|
||||
if let Some(ct) = h {
|
||||
headers.push(HeaderKind::ContentType(ct))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
match extract_header_value(&call_info, "content-length") {
|
||||
Ok(h) => {
|
||||
if let Some(cl) = h {
|
||||
headers.push(HeaderKind::ContentLength(cl))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
fn extract_header_value(call_info: &CallInfo, key: &str) -> Result<Option<String>, ShellError> {
|
||||
if call_info.args.has(key) {
|
||||
let tagged = call_info.args.get(key);
|
||||
let val = match tagged {
|
||||
Some(Value {
|
||||
value: UntaggedValue::Primitive(Primitive::String(s)),
|
||||
..
|
||||
}) => s.clone(),
|
||||
Some(Value { tag, .. }) => {
|
||||
return Err(ShellError::labeled_error(
|
||||
format!("{} not in expected format. Expected string.", key),
|
||||
"post error",
|
||||
tag,
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
return Err(ShellError::labeled_error(
|
||||
format!("{} not in expected format. Expected string.", key),
|
||||
"post error",
|
||||
Tag::unknown(),
|
||||
));
|
||||
}
|
||||
};
|
||||
return Ok(Some(val));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
@ -1,22 +1,28 @@
|
||||
[package]
|
||||
name = "nu_plugin_ps"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "A process list plugin for Nushell"
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.9.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.9.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.9.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.9.0" }
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
futures-preview = { version = "=0.3.0-alpha.19", features = ["compat", "io-compat"] }
|
||||
heim = "0.0.9"
|
||||
futures-timer = "1.0.3"
|
||||
[dependencies]
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.10.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.10.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.10.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.10.0" }
|
||||
|
||||
futures = { version = "0.3", features = ["compat", "io-compat"] }
|
||||
futures-timer = "3.0.1"
|
||||
pin-utils = "0.1.0-alpha.4"
|
||||
futures-util = "0.3.1"
|
||||
|
||||
[dependencies.heim]
|
||||
version = "0.0.9"
|
||||
default-features = false
|
||||
features = ["process", "runtime-polyfill"]
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = { version = "0.9.0", path = "../nu-build" }
|
||||
nu-build = { version = "0.10.0", path = "../nu-build" }
|
||||
|
4
crates/nu_plugin_ps/src/lib.rs
Normal file
4
crates/nu_plugin_ps/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod nu;
|
||||
mod ps;
|
||||
|
||||
pub use ps::Ps;
|
@ -1,94 +1,6 @@
|
||||
use futures::executor::block_on;
|
||||
//use futures::stream::TryStreamExt;
|
||||
|
||||
use futures_util::{StreamExt, TryStreamExt};
|
||||
use heim::process::{self as process, Process, ProcessResult};
|
||||
use heim::units::{information, ratio, Ratio};
|
||||
use std::usize;
|
||||
|
||||
use nu_errors::ShellError;
|
||||
use nu_plugin::{serve_plugin, Plugin};
|
||||
use nu_protocol::{
|
||||
CallInfo, ReturnSuccess, ReturnValue, Signature, TaggedDictBuilder, UntaggedValue, Value,
|
||||
};
|
||||
use nu_source::Tag;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
struct Ps;
|
||||
impl Ps {
|
||||
fn new() -> Ps {
|
||||
Ps
|
||||
}
|
||||
}
|
||||
|
||||
async fn usage(process: Process) -> ProcessResult<(process::Process, Ratio, process::Memory)> {
|
||||
let usage_1 = process.cpu_usage().await?;
|
||||
futures_timer::Delay::new(Duration::from_millis(100)).await;
|
||||
let usage_2 = process.cpu_usage().await?;
|
||||
|
||||
let memory = process.memory().await?;
|
||||
|
||||
Ok((process, usage_2 - usage_1, memory))
|
||||
}
|
||||
|
||||
async fn ps(tag: Tag) -> Vec<Value> {
|
||||
let processes = process::processes()
|
||||
.map_ok(|process| {
|
||||
// Note that there is no `.await` here,
|
||||
// as we want to pass the returned future
|
||||
// into the `.try_buffer_unordered`.
|
||||
usage(process)
|
||||
})
|
||||
.try_buffer_unordered(usize::MAX);
|
||||
pin_utils::pin_mut!(processes);
|
||||
|
||||
let mut output = vec![];
|
||||
while let Some(res) = processes.next().await {
|
||||
if let Ok((process, usage, memory)) = res {
|
||||
let mut dict = TaggedDictBuilder::new(&tag);
|
||||
dict.insert_untagged("pid", UntaggedValue::int(process.pid()));
|
||||
if let Ok(name) = process.name().await {
|
||||
dict.insert_untagged("name", UntaggedValue::string(name));
|
||||
}
|
||||
if let Ok(status) = process.status().await {
|
||||
dict.insert_untagged("status", UntaggedValue::string(format!("{:?}", status)));
|
||||
}
|
||||
dict.insert_untagged("cpu", UntaggedValue::decimal(usage.get::<ratio::percent>()));
|
||||
dict.insert_untagged(
|
||||
"mem",
|
||||
UntaggedValue::bytes(memory.rss().get::<information::byte>()),
|
||||
);
|
||||
dict.insert_untagged(
|
||||
"virtual",
|
||||
UntaggedValue::bytes(memory.vms().get::<information::byte>()),
|
||||
);
|
||||
output.push(dict.into_value());
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
impl Plugin for Ps {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("ps")
|
||||
.desc("View information about system processes.")
|
||||
.filter())
|
||||
}
|
||||
|
||||
fn begin_filter(&mut self, callinfo: CallInfo) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
Ok(block_on(ps(callinfo.name_tag))
|
||||
.into_iter()
|
||||
.map(ReturnSuccess::value)
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn filter(&mut self, _: Value) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
use nu_plugin::serve_plugin;
|
||||
use nu_plugin_ps::Ps;
|
||||
|
||||
fn main() {
|
||||
serve_plugin(&mut Ps::new());
|
||||
serve_plugin(&mut Ps::new())
|
||||
}
|
||||
|
25
crates/nu_plugin_ps/src/nu/mod.rs
Normal file
25
crates/nu_plugin_ps/src/nu/mod.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use crate::ps::{ps, Ps};
|
||||
use nu_errors::ShellError;
|
||||
use nu_plugin::Plugin;
|
||||
use nu_protocol::{CallInfo, ReturnSuccess, ReturnValue, Signature, Value};
|
||||
|
||||
use futures::executor::block_on;
|
||||
|
||||
impl Plugin for Ps {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("ps")
|
||||
.desc("View information about system processes.")
|
||||
.filter())
|
||||
}
|
||||
|
||||
fn begin_filter(&mut self, callinfo: CallInfo) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
Ok(block_on(ps(callinfo.name_tag))
|
||||
.into_iter()
|
||||
.map(ReturnSuccess::value)
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn filter(&mut self, _: Value) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
66
crates/nu_plugin_ps/src/ps.rs
Normal file
66
crates/nu_plugin_ps/src/ps.rs
Normal file
@ -0,0 +1,66 @@
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use heim::process::{self as process, Process, ProcessResult};
|
||||
use heim::units::{information, ratio, Ratio};
|
||||
use std::usize;
|
||||
|
||||
use nu_protocol::{TaggedDictBuilder, UntaggedValue, Value};
|
||||
use nu_source::Tag;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Ps;
|
||||
|
||||
impl Ps {
|
||||
pub fn new() -> Ps {
|
||||
Ps
|
||||
}
|
||||
}
|
||||
|
||||
async fn usage(process: Process) -> ProcessResult<(process::Process, Ratio, process::Memory)> {
|
||||
let usage_1 = process.cpu_usage().await?;
|
||||
futures_timer::Delay::new(Duration::from_millis(100)).await;
|
||||
let usage_2 = process.cpu_usage().await?;
|
||||
|
||||
let memory = process.memory().await?;
|
||||
|
||||
Ok((process, usage_2 - usage_1, memory))
|
||||
}
|
||||
|
||||
pub async fn ps(tag: Tag) -> Vec<Value> {
|
||||
let processes = process::processes()
|
||||
.map_ok(|process| {
|
||||
// Note that there is no `.await` here,
|
||||
// as we want to pass the returned future
|
||||
// into the `.try_buffer_unordered`.
|
||||
usage(process)
|
||||
})
|
||||
.try_buffer_unordered(usize::MAX);
|
||||
pin_utils::pin_mut!(processes);
|
||||
|
||||
let mut output = vec![];
|
||||
while let Some(res) = processes.next().await {
|
||||
if let Ok((process, usage, memory)) = res {
|
||||
let mut dict = TaggedDictBuilder::new(&tag);
|
||||
dict.insert_untagged("pid", UntaggedValue::int(process.pid()));
|
||||
if let Ok(name) = process.name().await {
|
||||
dict.insert_untagged("name", UntaggedValue::string(name));
|
||||
}
|
||||
if let Ok(status) = process.status().await {
|
||||
dict.insert_untagged("status", UntaggedValue::string(format!("{:?}", status)));
|
||||
}
|
||||
dict.insert_untagged("cpu", UntaggedValue::decimal(usage.get::<ratio::percent>()));
|
||||
dict.insert_untagged(
|
||||
"mem",
|
||||
UntaggedValue::bytes(memory.rss().get::<information::byte>()),
|
||||
);
|
||||
dict.insert_untagged(
|
||||
"virtual",
|
||||
UntaggedValue::bytes(memory.vms().get::<information::byte>()),
|
||||
);
|
||||
output.push(dict.into_value());
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nu_plugin_str"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "A string manipulation plugin for Nushell"
|
||||
@ -10,13 +10,15 @@ license = "MIT"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.9.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.9.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.9.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.9.0" }
|
||||
nu-value-ext = { path = "../nu-value-ext", version = "0.9.0" }
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.10.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.10.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.10.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.10.0" }
|
||||
nu-value-ext = { path = "../nu-value-ext", version = "0.10.0" }
|
||||
chrono = { version = "0.4.10", features = ["serde"] }
|
||||
|
||||
regex = "1"
|
||||
num-bigint = "0.2.3"
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = { version = "0.9.0", path = "../nu-build" }
|
||||
nu-build = { version = "0.10.0", path = "../nu-build" }
|
||||
|
@ -1,4 +1,4 @@
|
||||
mod nu_plugin_str;
|
||||
mod nu;
|
||||
mod strutils;
|
||||
|
||||
pub use strutils::Str;
|
||||
|
@ -15,19 +15,32 @@ impl Plugin for Str {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("str")
|
||||
.desc("Apply string function. Optional use the column of a table")
|
||||
.switch("downcase", "convert string to lowercase")
|
||||
.switch("upcase", "convert string to uppercase")
|
||||
.switch("to-int", "convert string to integer")
|
||||
.named("replace", SyntaxShape::String, "replaces the string")
|
||||
.switch("downcase", "convert string to lowercase", Some('d'))
|
||||
.switch("upcase", "convert string to uppercase", Some('U'))
|
||||
.switch("to-int", "convert string to integer", Some('i'))
|
||||
.named(
|
||||
"replace",
|
||||
SyntaxShape::String,
|
||||
"replaces the string",
|
||||
Some('r'),
|
||||
)
|
||||
.named(
|
||||
"find-replace",
|
||||
SyntaxShape::Any,
|
||||
"finds and replaces [pattern replacement]",
|
||||
Some('f'),
|
||||
)
|
||||
.named(
|
||||
"substring",
|
||||
SyntaxShape::String,
|
||||
"convert string to portion of original, requires \"start,end\"",
|
||||
Some('s'),
|
||||
)
|
||||
.named(
|
||||
"to-date-time",
|
||||
SyntaxShape::String,
|
||||
"Convert string to Date/Time",
|
||||
Some('D'),
|
||||
)
|
||||
.rest(SyntaxShape::ColumnPath, "the column(s) to convert")
|
||||
.filter())
|
||||
@ -110,6 +123,11 @@ impl Plugin for Str {
|
||||
self.for_field(possible_field);
|
||||
}
|
||||
|
||||
if let Some(dt) = args.get("to-date-time") {
|
||||
let dt = dt.as_string()?;
|
||||
self.for_date_time(dt);
|
||||
}
|
||||
|
||||
match &self.error {
|
||||
Some(reason) => Err(ShellError::untagged_runtime_error(format!(
|
||||
"{}: {}",
|
@ -7,7 +7,37 @@ mod integration {
|
||||
unstructured_sample_record,
|
||||
};
|
||||
use nu_plugin::test_helpers::{expect_return_value_at, plugin, CallStub};
|
||||
use nu_protocol::UntaggedValue;
|
||||
use nu_protocol::{Primitive, UntaggedValue};
|
||||
|
||||
#[test]
|
||||
fn picks_up_date_time() {
|
||||
let run = plugin(&mut Str::new())
|
||||
.args(
|
||||
CallStub::new()
|
||||
.with_named_parameter("to-date-time", string("%d.%m.%Y %H:%M %P %z"))
|
||||
.create(),
|
||||
)
|
||||
.input(string("5.8.1994 8:00 am +0000"))
|
||||
.input(string("6.9.1995 10:00 am +0000"))
|
||||
.input(string("5.8.1994 20:00 pm +0000"))
|
||||
.input(string("20.4.2020 8:00 am +0000"))
|
||||
.setup(|_, _| {})
|
||||
.test();
|
||||
let ret_vals = run.unwrap();
|
||||
for r in ret_vals {
|
||||
let r = r
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.raw_value()
|
||||
.unwrap()
|
||||
.as_primitive()
|
||||
.unwrap();
|
||||
match r {
|
||||
Primitive::Date(_) => (),
|
||||
_ => assert!(false, "failed to convert string to date"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picks_up_one_action_flag_only() {
|
@ -1,3 +1,6 @@
|
||||
extern crate chrono;
|
||||
|
||||
use chrono::DateTime;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{did_you_mean, ColumnPath, Primitive, ShellTypeName, UntaggedValue, Value};
|
||||
use nu_source::{span_for_spanned_list, Tagged};
|
||||
@ -12,6 +15,7 @@ pub enum Action {
|
||||
ToInteger,
|
||||
Substring(usize, usize),
|
||||
Replace(ReplaceAction),
|
||||
ToDateTime(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
@ -70,6 +74,10 @@ impl Str {
|
||||
Err(_) => UntaggedValue::string(input),
|
||||
},
|
||||
},
|
||||
Some(Action::ToDateTime(dt)) => match DateTime::parse_from_str(input, dt) {
|
||||
Ok(d) => UntaggedValue::date(d),
|
||||
Err(_) => UntaggedValue::string(input),
|
||||
},
|
||||
None => UntaggedValue::string(input),
|
||||
};
|
||||
|
||||
@ -89,27 +97,15 @@ impl Str {
|
||||
}
|
||||
|
||||
pub fn for_to_int(&mut self) {
|
||||
if self.permit() {
|
||||
self.action = Some(Action::ToInteger);
|
||||
} else {
|
||||
self.log_error("can only apply one");
|
||||
}
|
||||
self.add_action(Action::ToInteger);
|
||||
}
|
||||
|
||||
pub fn for_downcase(&mut self) {
|
||||
if self.permit() {
|
||||
self.action = Some(Action::Downcase);
|
||||
} else {
|
||||
self.log_error("can only apply one");
|
||||
}
|
||||
self.add_action(Action::Downcase);
|
||||
}
|
||||
|
||||
pub fn for_upcase(&mut self) {
|
||||
if self.permit() {
|
||||
self.action = Some(Action::Upcase);
|
||||
} else {
|
||||
self.log_error("can only apply one");
|
||||
}
|
||||
self.add_action(Action::Upcase);
|
||||
}
|
||||
|
||||
pub fn for_substring(&mut self, s: String) -> Result<(), ShellError> {
|
||||
@ -130,18 +126,24 @@ impl Str {
|
||||
};
|
||||
if start > end {
|
||||
self.log_error("End must be greater than or equal to Start");
|
||||
} else if self.permit() {
|
||||
self.action = Some(Action::Substring(start, end));
|
||||
} else {
|
||||
self.log_error("can only apply one");
|
||||
self.add_action(Action::Substring(start, end));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn for_replace(&mut self, mode: ReplaceAction) {
|
||||
self.add_action(Action::Replace(mode));
|
||||
}
|
||||
|
||||
pub fn for_date_time(&mut self, dt: String) {
|
||||
self.add_action(Action::ToDateTime(dt));
|
||||
}
|
||||
|
||||
fn add_action(&mut self, act: Action) {
|
||||
if self.permit() {
|
||||
self.action = Some(Action::Replace(mode));
|
||||
self.action = Some(act);
|
||||
} else {
|
||||
self.log_error("can only apply one");
|
||||
}
|
||||
|
@ -1,16 +1,19 @@
|
||||
[package]
|
||||
name = "nu_plugin_sum"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "A simple summation plugin for Nushell"
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.9.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.9.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.9.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.9.0" }
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.10.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.10.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.10.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.10.0" }
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = { version = "0.9.0", path = "../nu-build" }
|
||||
nu-build = { version = "0.10.0", path = "../nu-build" }
|
||||
|
4
crates/nu_plugin_sum/src/lib.rs
Normal file
4
crates/nu_plugin_sum/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod nu;
|
||||
mod sum;
|
||||
|
||||
pub use sum::Sum;
|
@ -1,94 +1,5 @@
|
||||
use nu_errors::ShellError;
|
||||
use nu_plugin::{serve_plugin, Plugin};
|
||||
use nu_protocol::{
|
||||
CallInfo, Primitive, ReturnSuccess, ReturnValue, Signature, UntaggedValue, Value,
|
||||
};
|
||||
|
||||
struct Sum {
|
||||
total: Option<Value>,
|
||||
}
|
||||
impl Sum {
|
||||
fn new() -> Sum {
|
||||
Sum { total: None }
|
||||
}
|
||||
|
||||
fn sum(&mut self, value: Value) -> Result<(), ShellError> {
|
||||
match &value.value {
|
||||
UntaggedValue::Primitive(Primitive::Nothing) => Ok(()),
|
||||
UntaggedValue::Primitive(Primitive::Int(i)) => {
|
||||
match &self.total {
|
||||
Some(Value {
|
||||
value: UntaggedValue::Primitive(Primitive::Int(j)),
|
||||
tag,
|
||||
}) => {
|
||||
//TODO: handle overflow
|
||||
self.total = Some(UntaggedValue::int(i + j).into_value(tag));
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
self.total = Some(value.clone());
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ShellError::labeled_error(
|
||||
"Could not sum non-integer or unrelated types",
|
||||
"source",
|
||||
value.tag,
|
||||
)),
|
||||
}
|
||||
}
|
||||
UntaggedValue::Primitive(Primitive::Bytes(b)) => {
|
||||
match &self.total {
|
||||
Some(Value {
|
||||
value: UntaggedValue::Primitive(Primitive::Bytes(j)),
|
||||
tag,
|
||||
}) => {
|
||||
//TODO: handle overflow
|
||||
self.total = Some(UntaggedValue::bytes(b + j).into_value(tag));
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
self.total = Some(value);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ShellError::labeled_error(
|
||||
"Could not sum non-integer or unrelated types",
|
||||
"source",
|
||||
value.tag,
|
||||
)),
|
||||
}
|
||||
}
|
||||
x => Err(ShellError::labeled_error(
|
||||
format!("Unrecognized type in stream: {:?}", x),
|
||||
"source",
|
||||
value.tag,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for Sum {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("sum")
|
||||
.desc("Sum a column of values.")
|
||||
.filter())
|
||||
}
|
||||
|
||||
fn begin_filter(&mut self, _: CallInfo) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn filter(&mut self, input: Value) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
self.sum(input)?;
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn end_filter(&mut self) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
match self.total {
|
||||
None => Ok(vec![]),
|
||||
Some(ref v) => Ok(vec![ReturnSuccess::value(v.clone())]),
|
||||
}
|
||||
}
|
||||
}
|
||||
use nu_plugin::serve_plugin;
|
||||
use nu_plugin_sum::Sum;
|
||||
|
||||
fn main() {
|
||||
serve_plugin(&mut Sum::new());
|
||||
|
29
crates/nu_plugin_sum/src/nu/mod.rs
Normal file
29
crates/nu_plugin_sum/src/nu/mod.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use nu_errors::ShellError;
|
||||
use nu_plugin::Plugin;
|
||||
use nu_protocol::{CallInfo, ReturnSuccess, ReturnValue, Signature, Value};
|
||||
|
||||
use crate::Sum;
|
||||
|
||||
impl Plugin for Sum {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("sum")
|
||||
.desc("Sum a column of values.")
|
||||
.filter())
|
||||
}
|
||||
|
||||
fn begin_filter(&mut self, _: CallInfo) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn filter(&mut self, input: Value) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
self.sum(input)?;
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn end_filter(&mut self) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
match self.total {
|
||||
None => Ok(vec![]),
|
||||
Some(ref v) => Ok(vec![ReturnSuccess::value(v.clone())]),
|
||||
}
|
||||
}
|
||||
}
|
66
crates/nu_plugin_sum/src/sum.rs
Normal file
66
crates/nu_plugin_sum/src/sum.rs
Normal file
@ -0,0 +1,66 @@
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{Primitive, UntaggedValue, Value};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Sum {
|
||||
pub total: Option<Value>,
|
||||
}
|
||||
|
||||
impl Sum {
|
||||
pub fn new() -> Sum {
|
||||
Sum { total: None }
|
||||
}
|
||||
|
||||
pub fn sum(&mut self, value: Value) -> Result<(), ShellError> {
|
||||
match &value.value {
|
||||
UntaggedValue::Primitive(Primitive::Nothing) => Ok(()),
|
||||
UntaggedValue::Primitive(Primitive::Int(i)) => {
|
||||
match &self.total {
|
||||
Some(Value {
|
||||
value: UntaggedValue::Primitive(Primitive::Int(j)),
|
||||
tag,
|
||||
}) => {
|
||||
//TODO: handle overflow
|
||||
self.total = Some(UntaggedValue::int(i + j).into_value(tag));
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
self.total = Some(value.clone());
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ShellError::labeled_error(
|
||||
"Could not sum non-integer or unrelated types",
|
||||
"source",
|
||||
value.tag,
|
||||
)),
|
||||
}
|
||||
}
|
||||
UntaggedValue::Primitive(Primitive::Bytes(b)) => {
|
||||
match &self.total {
|
||||
Some(Value {
|
||||
value: UntaggedValue::Primitive(Primitive::Bytes(j)),
|
||||
tag,
|
||||
}) => {
|
||||
//TODO: handle overflow
|
||||
self.total = Some(UntaggedValue::bytes(b + j).into_value(tag));
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
self.total = Some(value);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ShellError::labeled_error(
|
||||
"Could not sum non-integer or unrelated types",
|
||||
"source",
|
||||
value.tag,
|
||||
)),
|
||||
}
|
||||
}
|
||||
x => Err(ShellError::labeled_error(
|
||||
format!("Unrecognized type in stream: {:?}", x),
|
||||
"source",
|
||||
value.tag,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,20 +1,29 @@
|
||||
[package]
|
||||
name = "nu_plugin_sys"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "A system info plugin for Nushell"
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.9.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.9.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.9.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.9.0" }
|
||||
heim = "0.0.9"
|
||||
futures-preview = { version = "=0.3.0-alpha.19", features = ["compat", "io-compat"] }
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.10.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.10.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.10.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.10.0" }
|
||||
|
||||
futures = { version = "0.3", features = ["compat", "io-compat"] }
|
||||
battery = "0.7.5"
|
||||
futures-util = "0.3.1"
|
||||
|
||||
[dependencies.heim]
|
||||
version = "0.0.9"
|
||||
default-features = false
|
||||
|
||||
features = ["host", "cpu", "memory", "disk", "net", "sensors", "runtime-polyfill"]
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = { version = "0.9.0", path = "../nu-build" }
|
||||
nu-build = { version = "0.10.0", path = "../nu-build" }
|
||||
|
4
crates/nu_plugin_sys/src/lib.rs
Normal file
4
crates/nu_plugin_sys/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod nu;
|
||||
mod sys;
|
||||
|
||||
pub use sys::Sys;
|
@ -1,354 +1,5 @@
|
||||
use std::ffi::OsStr;
|
||||
|
||||
use futures::executor::block_on;
|
||||
//use futures::stream::StreamExt;
|
||||
use futures_util::StreamExt;
|
||||
use heim::units::{frequency, information, thermodynamic_temperature, time};
|
||||
use heim::{disk, host, memory, net, sensors};
|
||||
use nu_errors::ShellError;
|
||||
use nu_plugin::{serve_plugin, Plugin};
|
||||
use nu_protocol::{
|
||||
CallInfo, ReturnSuccess, ReturnValue, Signature, TaggedDictBuilder, UntaggedValue, Value,
|
||||
};
|
||||
use nu_source::Tag;
|
||||
|
||||
struct Sys;
|
||||
impl Sys {
|
||||
fn new() -> Sys {
|
||||
Sys
|
||||
}
|
||||
}
|
||||
|
||||
async fn cpu(tag: Tag) -> Option<Value> {
|
||||
match futures::future::try_join(heim::cpu::logical_count(), heim::cpu::frequency()).await {
|
||||
Ok((num_cpu, cpu_speed)) => {
|
||||
let mut cpu_idx = TaggedDictBuilder::with_capacity(tag, 4);
|
||||
cpu_idx.insert_untagged("cores", UntaggedValue::int(num_cpu));
|
||||
|
||||
let current_speed =
|
||||
(cpu_speed.current().get::<frequency::hertz>() as f64 / 1_000_000_000.0 * 100.0)
|
||||
.round()
|
||||
/ 100.0;
|
||||
cpu_idx.insert_untagged("current ghz", UntaggedValue::decimal(current_speed));
|
||||
|
||||
if let Some(min_speed) = cpu_speed.min() {
|
||||
let min_speed =
|
||||
(min_speed.get::<frequency::hertz>() as f64 / 1_000_000_000.0 * 100.0).round()
|
||||
/ 100.0;
|
||||
cpu_idx.insert_untagged("min ghz", UntaggedValue::decimal(min_speed));
|
||||
}
|
||||
|
||||
if let Some(max_speed) = cpu_speed.max() {
|
||||
let max_speed =
|
||||
(max_speed.get::<frequency::hertz>() as f64 / 1_000_000_000.0 * 100.0).round()
|
||||
/ 100.0;
|
||||
cpu_idx.insert_untagged("max ghz", UntaggedValue::decimal(max_speed));
|
||||
}
|
||||
|
||||
Some(cpu_idx.into_value())
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn mem(tag: Tag) -> Value {
|
||||
let mut dict = TaggedDictBuilder::with_capacity(tag, 4);
|
||||
|
||||
let (memory_result, swap_result) =
|
||||
futures::future::join(memory::memory(), memory::swap()).await;
|
||||
|
||||
if let Ok(memory) = memory_result {
|
||||
dict.insert_untagged(
|
||||
"total",
|
||||
UntaggedValue::bytes(memory.total().get::<information::byte>()),
|
||||
);
|
||||
dict.insert_untagged(
|
||||
"free",
|
||||
UntaggedValue::bytes(memory.free().get::<information::byte>()),
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(swap) = swap_result {
|
||||
dict.insert_untagged(
|
||||
"swap total",
|
||||
UntaggedValue::bytes(swap.total().get::<information::byte>()),
|
||||
);
|
||||
dict.insert_untagged(
|
||||
"swap free",
|
||||
UntaggedValue::bytes(swap.free().get::<information::byte>()),
|
||||
);
|
||||
}
|
||||
|
||||
dict.into_value()
|
||||
}
|
||||
|
||||
async fn host(tag: Tag) -> Value {
|
||||
let mut dict = TaggedDictBuilder::with_capacity(&tag, 6);
|
||||
|
||||
let (platform_result, uptime_result) =
|
||||
futures::future::join(host::platform(), host::uptime()).await;
|
||||
|
||||
// OS
|
||||
if let Ok(platform) = platform_result {
|
||||
dict.insert_untagged("name", UntaggedValue::string(platform.system()));
|
||||
dict.insert_untagged("release", UntaggedValue::string(platform.release()));
|
||||
dict.insert_untagged("hostname", UntaggedValue::string(platform.hostname()));
|
||||
dict.insert_untagged(
|
||||
"arch",
|
||||
UntaggedValue::string(platform.architecture().as_str()),
|
||||
);
|
||||
}
|
||||
|
||||
// Uptime
|
||||
if let Ok(uptime) = uptime_result {
|
||||
let mut uptime_dict = TaggedDictBuilder::with_capacity(&tag, 4);
|
||||
|
||||
let uptime = uptime.get::<time::second>().round() as i64;
|
||||
let days = uptime / (60 * 60 * 24);
|
||||
let hours = (uptime - days * 60 * 60 * 24) / (60 * 60);
|
||||
let minutes = (uptime - days * 60 * 60 * 24 - hours * 60 * 60) / 60;
|
||||
let seconds = uptime % 60;
|
||||
|
||||
uptime_dict.insert_untagged("days", UntaggedValue::int(days));
|
||||
uptime_dict.insert_untagged("hours", UntaggedValue::int(hours));
|
||||
uptime_dict.insert_untagged("mins", UntaggedValue::int(minutes));
|
||||
uptime_dict.insert_untagged("secs", UntaggedValue::int(seconds));
|
||||
|
||||
dict.insert_value("uptime", uptime_dict);
|
||||
}
|
||||
|
||||
// Users
|
||||
let mut users = host::users();
|
||||
let mut user_vec = vec![];
|
||||
while let Some(user) = users.next().await {
|
||||
if let Ok(user) = user {
|
||||
user_vec.push(Value {
|
||||
value: UntaggedValue::string(user.username()),
|
||||
tag: tag.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
let user_list = UntaggedValue::Table(user_vec);
|
||||
dict.insert_untagged("users", user_list);
|
||||
|
||||
dict.into_value()
|
||||
}
|
||||
|
||||
async fn disks(tag: Tag) -> Option<UntaggedValue> {
|
||||
let mut output = vec![];
|
||||
let mut partitions = disk::partitions_physical();
|
||||
while let Some(part) = partitions.next().await {
|
||||
if let Ok(part) = part {
|
||||
let mut dict = TaggedDictBuilder::with_capacity(&tag, 6);
|
||||
dict.insert_untagged(
|
||||
"device",
|
||||
UntaggedValue::string(
|
||||
part.device()
|
||||
.unwrap_or_else(|| OsStr::new("N/A"))
|
||||
.to_string_lossy(),
|
||||
),
|
||||
);
|
||||
|
||||
dict.insert_untagged("type", UntaggedValue::string(part.file_system().as_str()));
|
||||
dict.insert_untagged(
|
||||
"mount",
|
||||
UntaggedValue::string(part.mount_point().to_string_lossy()),
|
||||
);
|
||||
|
||||
if let Ok(usage) = disk::usage(part.mount_point().to_path_buf()).await {
|
||||
dict.insert_untagged(
|
||||
"total",
|
||||
UntaggedValue::bytes(usage.total().get::<information::byte>()),
|
||||
);
|
||||
dict.insert_untagged(
|
||||
"used",
|
||||
UntaggedValue::bytes(usage.used().get::<information::byte>()),
|
||||
);
|
||||
dict.insert_untagged(
|
||||
"free",
|
||||
UntaggedValue::bytes(usage.free().get::<information::byte>()),
|
||||
);
|
||||
}
|
||||
|
||||
output.push(dict.into_value());
|
||||
}
|
||||
}
|
||||
|
||||
if !output.is_empty() {
|
||||
Some(UntaggedValue::Table(output))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn battery(tag: Tag) -> Option<UntaggedValue> {
|
||||
let mut output = vec![];
|
||||
|
||||
if let Ok(manager) = battery::Manager::new() {
|
||||
if let Ok(batteries) = manager.batteries() {
|
||||
for battery in batteries {
|
||||
if let Ok(battery) = battery {
|
||||
let mut dict = TaggedDictBuilder::new(&tag);
|
||||
if let Some(vendor) = battery.vendor() {
|
||||
dict.insert_untagged("vendor", UntaggedValue::string(vendor));
|
||||
}
|
||||
if let Some(model) = battery.model() {
|
||||
dict.insert_untagged("model", UntaggedValue::string(model));
|
||||
}
|
||||
if let Some(cycles) = battery.cycle_count() {
|
||||
dict.insert_untagged("cycles", UntaggedValue::int(cycles));
|
||||
}
|
||||
if let Some(time_to_full) = battery.time_to_full() {
|
||||
dict.insert_untagged(
|
||||
"mins to full",
|
||||
UntaggedValue::decimal(
|
||||
time_to_full.get::<battery::units::time::minute>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if let Some(time_to_empty) = battery.time_to_empty() {
|
||||
dict.insert_untagged(
|
||||
"mins to empty",
|
||||
UntaggedValue::decimal(
|
||||
time_to_empty.get::<battery::units::time::minute>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
output.push(dict.into_value());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !output.is_empty() {
|
||||
Some(UntaggedValue::Table(output))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn temp(tag: Tag) -> Option<UntaggedValue> {
|
||||
let mut output = vec![];
|
||||
|
||||
let mut sensors = sensors::temperatures();
|
||||
while let Some(sensor) = sensors.next().await {
|
||||
if let Ok(sensor) = sensor {
|
||||
let mut dict = TaggedDictBuilder::new(&tag);
|
||||
dict.insert_untagged("unit", UntaggedValue::string(sensor.unit()));
|
||||
if let Some(label) = sensor.label() {
|
||||
dict.insert_untagged("label", UntaggedValue::string(label));
|
||||
}
|
||||
dict.insert_untagged(
|
||||
"temp",
|
||||
UntaggedValue::decimal(
|
||||
sensor
|
||||
.current()
|
||||
.get::<thermodynamic_temperature::degree_celsius>(),
|
||||
),
|
||||
);
|
||||
if let Some(high) = sensor.high() {
|
||||
dict.insert_untagged(
|
||||
"high",
|
||||
UntaggedValue::decimal(high.get::<thermodynamic_temperature::degree_celsius>()),
|
||||
);
|
||||
}
|
||||
if let Some(critical) = sensor.critical() {
|
||||
dict.insert_untagged(
|
||||
"critical",
|
||||
UntaggedValue::decimal(
|
||||
critical.get::<thermodynamic_temperature::degree_celsius>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
output.push(dict.into_value());
|
||||
}
|
||||
}
|
||||
|
||||
if !output.is_empty() {
|
||||
Some(UntaggedValue::Table(output))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn net(tag: Tag) -> Option<UntaggedValue> {
|
||||
let mut output = vec![];
|
||||
let mut io_counters = net::io_counters();
|
||||
while let Some(nic) = io_counters.next().await {
|
||||
if let Ok(nic) = nic {
|
||||
let mut network_idx = TaggedDictBuilder::with_capacity(&tag, 3);
|
||||
network_idx.insert_untagged("name", UntaggedValue::string(nic.interface()));
|
||||
network_idx.insert_untagged(
|
||||
"sent",
|
||||
UntaggedValue::bytes(nic.bytes_sent().get::<information::byte>()),
|
||||
);
|
||||
network_idx.insert_untagged(
|
||||
"recv",
|
||||
UntaggedValue::bytes(nic.bytes_recv().get::<information::byte>()),
|
||||
);
|
||||
output.push(network_idx.into_value());
|
||||
}
|
||||
}
|
||||
if !output.is_empty() {
|
||||
Some(UntaggedValue::Table(output))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn sysinfo(tag: Tag) -> Vec<Value> {
|
||||
let mut sysinfo = TaggedDictBuilder::with_capacity(&tag, 7);
|
||||
|
||||
let (host, cpu, disks, memory, temp) = futures::future::join5(
|
||||
host(tag.clone()),
|
||||
cpu(tag.clone()),
|
||||
disks(tag.clone()),
|
||||
mem(tag.clone()),
|
||||
temp(tag.clone()),
|
||||
)
|
||||
.await;
|
||||
let (net, battery) = futures::future::join(net(tag.clone()), battery(tag.clone())).await;
|
||||
|
||||
sysinfo.insert_value("host", host);
|
||||
if let Some(cpu) = cpu {
|
||||
sysinfo.insert_value("cpu", cpu);
|
||||
}
|
||||
if let Some(disks) = disks {
|
||||
sysinfo.insert_untagged("disks", disks);
|
||||
}
|
||||
sysinfo.insert_value("mem", memory);
|
||||
if let Some(temp) = temp {
|
||||
sysinfo.insert_untagged("temp", temp);
|
||||
}
|
||||
if let Some(net) = net {
|
||||
sysinfo.insert_untagged("net", net);
|
||||
}
|
||||
if let Some(battery) = battery {
|
||||
sysinfo.insert_untagged("battery", battery);
|
||||
}
|
||||
|
||||
vec![sysinfo.into_value()]
|
||||
}
|
||||
|
||||
impl Plugin for Sys {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("sys")
|
||||
.desc("View information about the current system.")
|
||||
.filter())
|
||||
}
|
||||
|
||||
fn begin_filter(&mut self, callinfo: CallInfo) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
Ok(block_on(sysinfo(callinfo.name_tag))
|
||||
.into_iter()
|
||||
.map(ReturnSuccess::value)
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn filter(&mut self, _: Value) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
use nu_plugin::serve_plugin;
|
||||
use nu_plugin_sys::Sys;
|
||||
|
||||
fn main() {
|
||||
serve_plugin(&mut Sys::new());
|
||||
|
25
crates/nu_plugin_sys/src/nu/mod.rs
Normal file
25
crates/nu_plugin_sys/src/nu/mod.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use crate::sys::{sysinfo, Sys};
|
||||
use nu_errors::ShellError;
|
||||
use nu_plugin::Plugin;
|
||||
use nu_protocol::{CallInfo, ReturnSuccess, ReturnValue, Signature, Value};
|
||||
|
||||
use futures::executor::block_on;
|
||||
|
||||
impl Plugin for Sys {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("sys")
|
||||
.desc("View information about the current system.")
|
||||
.filter())
|
||||
}
|
||||
|
||||
fn begin_filter(&mut self, callinfo: CallInfo) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
Ok(block_on(sysinfo(callinfo.name_tag))
|
||||
.into_iter()
|
||||
.map(ReturnSuccess::value)
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn filter(&mut self, _: Value) -> Result<Vec<ReturnValue>, ShellError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
328
crates/nu_plugin_sys/src/sys.rs
Normal file
328
crates/nu_plugin_sys/src/sys.rs
Normal file
@ -0,0 +1,328 @@
|
||||
use futures_util::StreamExt;
|
||||
use heim::units::{frequency, information, thermodynamic_temperature, time};
|
||||
use heim::{disk, host, memory, net, sensors};
|
||||
use nu_protocol::{TaggedDictBuilder, UntaggedValue, Value};
|
||||
use nu_source::Tag;
|
||||
use std::ffi::OsStr;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Sys;
|
||||
|
||||
impl Sys {
|
||||
pub fn new() -> Sys {
|
||||
Sys
|
||||
}
|
||||
}
|
||||
|
||||
async fn cpu(tag: Tag) -> Option<Value> {
|
||||
match futures::future::try_join(heim::cpu::logical_count(), heim::cpu::frequency()).await {
|
||||
Ok((num_cpu, cpu_speed)) => {
|
||||
let mut cpu_idx = TaggedDictBuilder::with_capacity(tag, 4);
|
||||
cpu_idx.insert_untagged("cores", UntaggedValue::int(num_cpu));
|
||||
|
||||
let current_speed =
|
||||
(cpu_speed.current().get::<frequency::hertz>() as f64 / 1_000_000_000.0 * 100.0)
|
||||
.round()
|
||||
/ 100.0;
|
||||
cpu_idx.insert_untagged("current ghz", UntaggedValue::decimal(current_speed));
|
||||
|
||||
if let Some(min_speed) = cpu_speed.min() {
|
||||
let min_speed =
|
||||
(min_speed.get::<frequency::hertz>() as f64 / 1_000_000_000.0 * 100.0).round()
|
||||
/ 100.0;
|
||||
cpu_idx.insert_untagged("min ghz", UntaggedValue::decimal(min_speed));
|
||||
}
|
||||
|
||||
if let Some(max_speed) = cpu_speed.max() {
|
||||
let max_speed =
|
||||
(max_speed.get::<frequency::hertz>() as f64 / 1_000_000_000.0 * 100.0).round()
|
||||
/ 100.0;
|
||||
cpu_idx.insert_untagged("max ghz", UntaggedValue::decimal(max_speed));
|
||||
}
|
||||
|
||||
Some(cpu_idx.into_value())
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn mem(tag: Tag) -> Value {
|
||||
let mut dict = TaggedDictBuilder::with_capacity(tag, 4);
|
||||
|
||||
let (memory_result, swap_result) =
|
||||
futures::future::join(memory::memory(), memory::swap()).await;
|
||||
|
||||
if let Ok(memory) = memory_result {
|
||||
dict.insert_untagged(
|
||||
"total",
|
||||
UntaggedValue::bytes(memory.total().get::<information::byte>()),
|
||||
);
|
||||
dict.insert_untagged(
|
||||
"free",
|
||||
UntaggedValue::bytes(memory.free().get::<information::byte>()),
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(swap) = swap_result {
|
||||
dict.insert_untagged(
|
||||
"swap total",
|
||||
UntaggedValue::bytes(swap.total().get::<information::byte>()),
|
||||
);
|
||||
dict.insert_untagged(
|
||||
"swap free",
|
||||
UntaggedValue::bytes(swap.free().get::<information::byte>()),
|
||||
);
|
||||
}
|
||||
|
||||
dict.into_value()
|
||||
}
|
||||
|
||||
async fn host(tag: Tag) -> Value {
|
||||
let mut dict = TaggedDictBuilder::with_capacity(&tag, 6);
|
||||
|
||||
let (platform_result, uptime_result) =
|
||||
futures::future::join(host::platform(), host::uptime()).await;
|
||||
|
||||
// OS
|
||||
if let Ok(platform) = platform_result {
|
||||
dict.insert_untagged("name", UntaggedValue::string(platform.system()));
|
||||
dict.insert_untagged("release", UntaggedValue::string(platform.release()));
|
||||
dict.insert_untagged("version", UntaggedValue::string(platform.version()));
|
||||
dict.insert_untagged("hostname", UntaggedValue::string(platform.hostname()));
|
||||
dict.insert_untagged(
|
||||
"arch",
|
||||
UntaggedValue::string(platform.architecture().as_str()),
|
||||
);
|
||||
}
|
||||
|
||||
// Uptime
|
||||
if let Ok(uptime) = uptime_result {
|
||||
let mut uptime_dict = TaggedDictBuilder::with_capacity(&tag, 4);
|
||||
|
||||
let uptime = uptime.get::<time::second>().round() as i64;
|
||||
let days = uptime / (60 * 60 * 24);
|
||||
let hours = (uptime - days * 60 * 60 * 24) / (60 * 60);
|
||||
let minutes = (uptime - days * 60 * 60 * 24 - hours * 60 * 60) / 60;
|
||||
let seconds = uptime % 60;
|
||||
|
||||
uptime_dict.insert_untagged("days", UntaggedValue::int(days));
|
||||
uptime_dict.insert_untagged("hours", UntaggedValue::int(hours));
|
||||
uptime_dict.insert_untagged("mins", UntaggedValue::int(minutes));
|
||||
uptime_dict.insert_untagged("secs", UntaggedValue::int(seconds));
|
||||
|
||||
dict.insert_value("uptime", uptime_dict);
|
||||
}
|
||||
|
||||
// Users
|
||||
let mut users = host::users();
|
||||
let mut user_vec = vec![];
|
||||
while let Some(user) = users.next().await {
|
||||
if let Ok(user) = user {
|
||||
user_vec.push(Value {
|
||||
value: UntaggedValue::string(user.username()),
|
||||
tag: tag.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
let user_list = UntaggedValue::Table(user_vec);
|
||||
dict.insert_untagged("users", user_list);
|
||||
|
||||
dict.into_value()
|
||||
}
|
||||
|
||||
async fn disks(tag: Tag) -> Option<UntaggedValue> {
|
||||
let mut output = vec![];
|
||||
let mut partitions = disk::partitions_physical();
|
||||
while let Some(part) = partitions.next().await {
|
||||
if let Ok(part) = part {
|
||||
let mut dict = TaggedDictBuilder::with_capacity(&tag, 6);
|
||||
dict.insert_untagged(
|
||||
"device",
|
||||
UntaggedValue::string(
|
||||
part.device()
|
||||
.unwrap_or_else(|| OsStr::new("N/A"))
|
||||
.to_string_lossy(),
|
||||
),
|
||||
);
|
||||
|
||||
dict.insert_untagged("type", UntaggedValue::string(part.file_system().as_str()));
|
||||
dict.insert_untagged(
|
||||
"mount",
|
||||
UntaggedValue::string(part.mount_point().to_string_lossy()),
|
||||
);
|
||||
|
||||
if let Ok(usage) = disk::usage(part.mount_point().to_path_buf()).await {
|
||||
dict.insert_untagged(
|
||||
"total",
|
||||
UntaggedValue::bytes(usage.total().get::<information::byte>()),
|
||||
);
|
||||
dict.insert_untagged(
|
||||
"used",
|
||||
UntaggedValue::bytes(usage.used().get::<information::byte>()),
|
||||
);
|
||||
dict.insert_untagged(
|
||||
"free",
|
||||
UntaggedValue::bytes(usage.free().get::<information::byte>()),
|
||||
);
|
||||
}
|
||||
|
||||
output.push(dict.into_value());
|
||||
}
|
||||
}
|
||||
|
||||
if !output.is_empty() {
|
||||
Some(UntaggedValue::Table(output))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn battery(tag: Tag) -> Option<UntaggedValue> {
|
||||
let mut output = vec![];
|
||||
|
||||
if let Ok(manager) = battery::Manager::new() {
|
||||
if let Ok(batteries) = manager.batteries() {
|
||||
for battery in batteries {
|
||||
if let Ok(battery) = battery {
|
||||
let mut dict = TaggedDictBuilder::new(&tag);
|
||||
if let Some(vendor) = battery.vendor() {
|
||||
dict.insert_untagged("vendor", UntaggedValue::string(vendor));
|
||||
}
|
||||
if let Some(model) = battery.model() {
|
||||
dict.insert_untagged("model", UntaggedValue::string(model));
|
||||
}
|
||||
if let Some(cycles) = battery.cycle_count() {
|
||||
dict.insert_untagged("cycles", UntaggedValue::int(cycles));
|
||||
}
|
||||
if let Some(time_to_full) = battery.time_to_full() {
|
||||
dict.insert_untagged(
|
||||
"mins to full",
|
||||
UntaggedValue::decimal(
|
||||
time_to_full.get::<battery::units::time::minute>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if let Some(time_to_empty) = battery.time_to_empty() {
|
||||
dict.insert_untagged(
|
||||
"mins to empty",
|
||||
UntaggedValue::decimal(
|
||||
time_to_empty.get::<battery::units::time::minute>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
output.push(dict.into_value());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !output.is_empty() {
|
||||
Some(UntaggedValue::Table(output))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn temp(tag: Tag) -> Option<UntaggedValue> {
|
||||
let mut output = vec![];
|
||||
|
||||
let mut sensors = sensors::temperatures();
|
||||
while let Some(sensor) = sensors.next().await {
|
||||
if let Ok(sensor) = sensor {
|
||||
let mut dict = TaggedDictBuilder::new(&tag);
|
||||
dict.insert_untagged("unit", UntaggedValue::string(sensor.unit()));
|
||||
if let Some(label) = sensor.label() {
|
||||
dict.insert_untagged("label", UntaggedValue::string(label));
|
||||
}
|
||||
dict.insert_untagged(
|
||||
"temp",
|
||||
UntaggedValue::decimal(
|
||||
sensor
|
||||
.current()
|
||||
.get::<thermodynamic_temperature::degree_celsius>(),
|
||||
),
|
||||
);
|
||||
if let Some(high) = sensor.high() {
|
||||
dict.insert_untagged(
|
||||
"high",
|
||||
UntaggedValue::decimal(high.get::<thermodynamic_temperature::degree_celsius>()),
|
||||
);
|
||||
}
|
||||
if let Some(critical) = sensor.critical() {
|
||||
dict.insert_untagged(
|
||||
"critical",
|
||||
UntaggedValue::decimal(
|
||||
critical.get::<thermodynamic_temperature::degree_celsius>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
output.push(dict.into_value());
|
||||
}
|
||||
}
|
||||
|
||||
if !output.is_empty() {
|
||||
Some(UntaggedValue::Table(output))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn net(tag: Tag) -> Option<UntaggedValue> {
|
||||
let mut output = vec![];
|
||||
let mut io_counters = net::io_counters();
|
||||
while let Some(nic) = io_counters.next().await {
|
||||
if let Ok(nic) = nic {
|
||||
let mut network_idx = TaggedDictBuilder::with_capacity(&tag, 3);
|
||||
network_idx.insert_untagged("name", UntaggedValue::string(nic.interface()));
|
||||
network_idx.insert_untagged(
|
||||
"sent",
|
||||
UntaggedValue::bytes(nic.bytes_sent().get::<information::byte>()),
|
||||
);
|
||||
network_idx.insert_untagged(
|
||||
"recv",
|
||||
UntaggedValue::bytes(nic.bytes_recv().get::<information::byte>()),
|
||||
);
|
||||
output.push(network_idx.into_value());
|
||||
}
|
||||
}
|
||||
if !output.is_empty() {
|
||||
Some(UntaggedValue::Table(output))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sysinfo(tag: Tag) -> Vec<Value> {
|
||||
let mut sysinfo = TaggedDictBuilder::with_capacity(&tag, 7);
|
||||
|
||||
let (host, cpu, disks, memory, temp) = futures::future::join5(
|
||||
host(tag.clone()),
|
||||
cpu(tag.clone()),
|
||||
disks(tag.clone()),
|
||||
mem(tag.clone()),
|
||||
temp(tag.clone()),
|
||||
)
|
||||
.await;
|
||||
let (net, battery) = futures::future::join(net(tag.clone()), battery(tag.clone())).await;
|
||||
|
||||
sysinfo.insert_value("host", host);
|
||||
if let Some(cpu) = cpu {
|
||||
sysinfo.insert_value("cpu", cpu);
|
||||
}
|
||||
if let Some(disks) = disks {
|
||||
sysinfo.insert_untagged("disks", disks);
|
||||
}
|
||||
sysinfo.insert_value("mem", memory);
|
||||
if let Some(temp) = temp {
|
||||
sysinfo.insert_untagged("temp", temp);
|
||||
}
|
||||
if let Some(net) = net {
|
||||
sysinfo.insert_untagged("net", net);
|
||||
}
|
||||
if let Some(battery) = battery {
|
||||
sysinfo.insert_untagged("battery", battery);
|
||||
}
|
||||
|
||||
vec![sysinfo.into_value()]
|
||||
}
|
@ -1,16 +1,19 @@
|
||||
[package]
|
||||
name = "nu_plugin_textview"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "Text viewer plugin for Nushell"
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.9.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.9.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.9.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.9.0" }
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.10.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.10.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.10.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.10.0" }
|
||||
|
||||
crossterm = "0.14.2"
|
||||
syntect = "3.2.0"
|
||||
@ -19,4 +22,4 @@ ansi_term = "0.12.1"
|
||||
url = "2.1.1"
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = { version = "0.9.0", path = "../nu-build" }
|
||||
nu-build = { version = "0.10.0", path = "../nu-build" }
|
||||
|
4
crates/nu_plugin_textview/src/lib.rs
Normal file
4
crates/nu_plugin_textview/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod nu;
|
||||
mod textview;
|
||||
|
||||
pub use textview::TextView;
|
@ -1,304 +1,5 @@
|
||||
use crossterm::{
|
||||
event::{KeyCode, KeyEvent},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use nu_errors::ShellError;
|
||||
use nu_plugin::{serve_plugin, Plugin};
|
||||
use nu_protocol::{outln, CallInfo, Primitive, Signature, UntaggedValue, Value};
|
||||
use nu_source::AnchorLocation;
|
||||
|
||||
use syntect::easy::HighlightLines;
|
||||
use syntect::highlighting::{Style, ThemeSet};
|
||||
use syntect::parsing::SyntaxSet;
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
enum DrawCommand {
|
||||
DrawString(Style, String),
|
||||
NextLine,
|
||||
}
|
||||
|
||||
struct TextView;
|
||||
|
||||
impl TextView {
|
||||
fn new() -> TextView {
|
||||
TextView
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for TextView {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("textview").desc("Autoview of text data."))
|
||||
}
|
||||
|
||||
fn sink(&mut self, _call_info: CallInfo, input: Vec<Value>) {
|
||||
if !input.is_empty() {
|
||||
view_text_value(&input[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_textview(
|
||||
draw_commands: &[DrawCommand],
|
||||
starting_row: usize,
|
||||
use_color_buffer: bool,
|
||||
) -> usize {
|
||||
let size = crossterm::terminal::size().unwrap_or_else(|_| (80, 24));
|
||||
|
||||
// render
|
||||
let mut pos = 0;
|
||||
let width = size.0 as usize;
|
||||
let height = size.1 as usize - 1;
|
||||
let mut frame_buffer = vec![];
|
||||
|
||||
for command in draw_commands {
|
||||
match command {
|
||||
DrawCommand::DrawString(style, string) => {
|
||||
for chr in string.chars() {
|
||||
if chr == '\t' {
|
||||
for _ in 0..8 {
|
||||
frame_buffer.push((
|
||||
' ',
|
||||
style.foreground.r,
|
||||
style.foreground.g,
|
||||
style.foreground.b,
|
||||
));
|
||||
}
|
||||
pos += 8;
|
||||
} else {
|
||||
frame_buffer.push((
|
||||
chr,
|
||||
style.foreground.r,
|
||||
style.foreground.g,
|
||||
style.foreground.b,
|
||||
));
|
||||
pos += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
DrawCommand::NextLine => {
|
||||
for _ in 0..(width - pos % width) {
|
||||
frame_buffer.push((' ', 0, 0, 0));
|
||||
}
|
||||
pos += width - pos % width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let num_frame_buffer_rows = frame_buffer.len() / width;
|
||||
let buffer_needs_scrolling = num_frame_buffer_rows > height;
|
||||
|
||||
// display
|
||||
let mut ansi_strings = vec![];
|
||||
let mut normal_chars = vec![];
|
||||
|
||||
for c in
|
||||
&frame_buffer[starting_row * width..std::cmp::min(pos, (starting_row + height) * width)]
|
||||
{
|
||||
if use_color_buffer {
|
||||
ansi_strings.push(ansi_term::Colour::RGB(c.1, c.2, c.3).paint(format!("{}", c.0)));
|
||||
} else {
|
||||
normal_chars.push(c.0);
|
||||
}
|
||||
}
|
||||
|
||||
if buffer_needs_scrolling {
|
||||
let _ = std::io::stdout().execute(crossterm::cursor::MoveTo(0, 0));
|
||||
}
|
||||
|
||||
if use_color_buffer {
|
||||
print!("{}", ansi_term::ANSIStrings(&ansi_strings));
|
||||
} else {
|
||||
let s: String = normal_chars.into_iter().collect();
|
||||
print!("{}", s);
|
||||
}
|
||||
|
||||
if buffer_needs_scrolling {
|
||||
let _ = std::io::stdout().execute(crossterm::cursor::MoveTo(0, size.1));
|
||||
print!(
|
||||
"{}",
|
||||
ansi_term::Colour::Blue.paint("[ESC to quit, arrow keys to move]")
|
||||
);
|
||||
}
|
||||
|
||||
let _ = std::io::stdout().flush();
|
||||
|
||||
num_frame_buffer_rows
|
||||
}
|
||||
|
||||
fn scroll_view_lines_if_needed(draw_commands: Vec<DrawCommand>, use_color_buffer: bool) {
|
||||
let mut starting_row = 0;
|
||||
|
||||
if let Ok(_raw) = crossterm::terminal::enable_raw_mode() {
|
||||
let mut size = crossterm::terminal::size().unwrap_or_else(|_| (80, 24));
|
||||
let height = size.1 as usize - 1;
|
||||
|
||||
let mut max_bottom_line = paint_textview(&draw_commands, starting_row, use_color_buffer);
|
||||
|
||||
// Only scroll if needed
|
||||
if max_bottom_line > height as usize {
|
||||
let _ = std::io::stdout().execute(crossterm::cursor::Hide);
|
||||
|
||||
loop {
|
||||
if let Ok(ev) = crossterm::event::read() {
|
||||
if let crossterm::event::Event::Key(KeyEvent { code, modifiers }) = ev {
|
||||
match code {
|
||||
KeyCode::Esc => {
|
||||
break;
|
||||
}
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
if starting_row > 0 {
|
||||
starting_row -= 1;
|
||||
max_bottom_line = paint_textview(
|
||||
&draw_commands,
|
||||
starting_row,
|
||||
use_color_buffer,
|
||||
);
|
||||
}
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
if starting_row < (max_bottom_line - height) {
|
||||
starting_row += 1;
|
||||
}
|
||||
max_bottom_line =
|
||||
paint_textview(&draw_commands, starting_row, use_color_buffer);
|
||||
}
|
||||
KeyCode::Char('b')
|
||||
if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) =>
|
||||
{
|
||||
starting_row -= std::cmp::min(height, starting_row);
|
||||
max_bottom_line =
|
||||
paint_textview(&draw_commands, starting_row, use_color_buffer);
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
starting_row -= std::cmp::min(height, starting_row);
|
||||
max_bottom_line =
|
||||
paint_textview(&draw_commands, starting_row, use_color_buffer);
|
||||
}
|
||||
KeyCode::Char('f')
|
||||
if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) =>
|
||||
{
|
||||
if starting_row < (max_bottom_line - height) {
|
||||
starting_row += height;
|
||||
|
||||
if starting_row > (max_bottom_line - height) {
|
||||
starting_row = max_bottom_line - height;
|
||||
}
|
||||
}
|
||||
max_bottom_line =
|
||||
paint_textview(&draw_commands, starting_row, use_color_buffer);
|
||||
}
|
||||
KeyCode::PageDown | KeyCode::Char(' ') => {
|
||||
if starting_row < (max_bottom_line - height) {
|
||||
starting_row += height;
|
||||
|
||||
if starting_row > (max_bottom_line - height) {
|
||||
starting_row = max_bottom_line - height;
|
||||
}
|
||||
}
|
||||
max_bottom_line =
|
||||
paint_textview(&draw_commands, starting_row, use_color_buffer);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(new_size) = crossterm::terminal::size() {
|
||||
if size != new_size {
|
||||
size = new_size;
|
||||
let _ = std::io::stdout().execute(crossterm::terminal::Clear(
|
||||
crossterm::terminal::ClearType::All,
|
||||
));
|
||||
max_bottom_line =
|
||||
paint_textview(&draw_commands, starting_row, use_color_buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = std::io::stdout().execute(crossterm::cursor::Show);
|
||||
|
||||
let _ = crossterm::terminal::disable_raw_mode();
|
||||
}
|
||||
}
|
||||
|
||||
outln!("");
|
||||
}
|
||||
|
||||
fn scroll_view(s: &str) {
|
||||
let mut v = vec![];
|
||||
for line in s.lines() {
|
||||
v.push(DrawCommand::DrawString(Style::default(), line.to_string()));
|
||||
v.push(DrawCommand::NextLine);
|
||||
}
|
||||
scroll_view_lines_if_needed(v, false);
|
||||
}
|
||||
|
||||
fn view_text_value(value: &Value) {
|
||||
let value_anchor = value.anchor();
|
||||
if let UntaggedValue::Primitive(Primitive::String(ref s)) = &value.value {
|
||||
if let Some(source) = value_anchor {
|
||||
let extension: Option<String> = match source {
|
||||
AnchorLocation::File(file) => {
|
||||
let path = Path::new(&file);
|
||||
path.extension().map(|x| x.to_string_lossy().to_string())
|
||||
}
|
||||
AnchorLocation::Url(url) => {
|
||||
let url = url::Url::parse(&url);
|
||||
if let Ok(url) = url {
|
||||
if let Some(mut segments) = url.path_segments() {
|
||||
if let Some(file) = segments.next_back() {
|
||||
let path = Path::new(file);
|
||||
path.extension().map(|x| x.to_string_lossy().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
//FIXME: this probably isn't correct
|
||||
AnchorLocation::Source(_source) => None,
|
||||
};
|
||||
|
||||
match extension {
|
||||
Some(extension) => {
|
||||
// Load these once at the start of your program
|
||||
let ps: SyntaxSet =
|
||||
syntect::dumps::from_binary(include_bytes!("assets/syntaxes.bin"));
|
||||
|
||||
if let Some(syntax) = ps.find_syntax_by_extension(&extension) {
|
||||
let ts: ThemeSet =
|
||||
syntect::dumps::from_binary(include_bytes!("assets/themes.bin"));
|
||||
let mut h = HighlightLines::new(syntax, &ts.themes["OneHalfDark"]);
|
||||
|
||||
let mut v = vec![];
|
||||
for line in s.lines() {
|
||||
let ranges: Vec<(Style, &str)> = h.highlight(line, &ps);
|
||||
|
||||
for range in ranges {
|
||||
v.push(DrawCommand::DrawString(range.0, range.1.to_string()));
|
||||
}
|
||||
|
||||
v.push(DrawCommand::NextLine);
|
||||
}
|
||||
scroll_view_lines_if_needed(v, true);
|
||||
} else {
|
||||
scroll_view(s);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
scroll_view(s);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scroll_view(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
use nu_plugin::serve_plugin;
|
||||
use nu_plugin_textview::TextView;
|
||||
|
||||
fn main() {
|
||||
serve_plugin(&mut TextView::new());
|
||||
|
16
crates/nu_plugin_textview/src/nu/mod.rs
Normal file
16
crates/nu_plugin_textview/src/nu/mod.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use crate::textview::{view_text_value, TextView};
|
||||
use nu_errors::ShellError;
|
||||
use nu_plugin::Plugin;
|
||||
use nu_protocol::{CallInfo, Signature, Value};
|
||||
|
||||
impl Plugin for TextView {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("textview").desc("Autoview of text data."))
|
||||
}
|
||||
|
||||
fn sink(&mut self, _call_info: CallInfo, input: Vec<Value>) {
|
||||
if !input.is_empty() {
|
||||
view_text_value(&input[0]);
|
||||
}
|
||||
}
|
||||
}
|
289
crates/nu_plugin_textview/src/textview.rs
Normal file
289
crates/nu_plugin_textview/src/textview.rs
Normal file
@ -0,0 +1,289 @@
|
||||
use crossterm::{
|
||||
event::{KeyCode, KeyEvent},
|
||||
ExecutableCommand,
|
||||
};
|
||||
|
||||
use nu_protocol::{Primitive, UntaggedValue, Value};
|
||||
use nu_source::AnchorLocation;
|
||||
|
||||
use syntect::easy::HighlightLines;
|
||||
use syntect::highlighting::{Style, ThemeSet};
|
||||
use syntect::parsing::SyntaxSet;
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
enum DrawCommand {
|
||||
DrawString(Style, String),
|
||||
NextLine,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TextView;
|
||||
|
||||
impl TextView {
|
||||
pub fn new() -> TextView {
|
||||
TextView
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_textview(
|
||||
draw_commands: &[DrawCommand],
|
||||
starting_row: usize,
|
||||
use_color_buffer: bool,
|
||||
) -> usize {
|
||||
let size = crossterm::terminal::size().unwrap_or_else(|_| (80, 24));
|
||||
|
||||
// render
|
||||
let mut pos = 0;
|
||||
let width = size.0 as usize;
|
||||
let height = size.1 as usize - 1;
|
||||
let mut frame_buffer = vec![];
|
||||
|
||||
for command in draw_commands {
|
||||
match command {
|
||||
DrawCommand::DrawString(style, string) => {
|
||||
for chr in string.chars() {
|
||||
if chr == '\t' {
|
||||
for _ in 0..8 {
|
||||
frame_buffer.push((
|
||||
' ',
|
||||
style.foreground.r,
|
||||
style.foreground.g,
|
||||
style.foreground.b,
|
||||
));
|
||||
}
|
||||
pos += 8;
|
||||
} else {
|
||||
frame_buffer.push((
|
||||
chr,
|
||||
style.foreground.r,
|
||||
style.foreground.g,
|
||||
style.foreground.b,
|
||||
));
|
||||
pos += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
DrawCommand::NextLine => {
|
||||
for _ in 0..(width - pos % width) {
|
||||
frame_buffer.push((' ', 0, 0, 0));
|
||||
}
|
||||
pos += width - pos % width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let num_frame_buffer_rows = frame_buffer.len() / width;
|
||||
let buffer_needs_scrolling = num_frame_buffer_rows > height;
|
||||
|
||||
// display
|
||||
let mut ansi_strings = vec![];
|
||||
let mut normal_chars = vec![];
|
||||
|
||||
for c in
|
||||
&frame_buffer[starting_row * width..std::cmp::min(pos, (starting_row + height) * width)]
|
||||
{
|
||||
if use_color_buffer {
|
||||
ansi_strings.push(ansi_term::Colour::RGB(c.1, c.2, c.3).paint(format!("{}", c.0)));
|
||||
} else {
|
||||
normal_chars.push(c.0);
|
||||
}
|
||||
}
|
||||
|
||||
if buffer_needs_scrolling {
|
||||
let _ = std::io::stdout().execute(crossterm::cursor::MoveTo(0, 0));
|
||||
}
|
||||
|
||||
if use_color_buffer {
|
||||
print!("{}", ansi_term::ANSIStrings(&ansi_strings));
|
||||
} else {
|
||||
let s: String = normal_chars.into_iter().collect();
|
||||
print!("{}", s);
|
||||
}
|
||||
|
||||
if buffer_needs_scrolling {
|
||||
let _ = std::io::stdout().execute(crossterm::cursor::MoveTo(0, size.1));
|
||||
print!(
|
||||
"{}",
|
||||
ansi_term::Colour::Blue.paint("[ESC to quit, arrow keys to move]")
|
||||
);
|
||||
}
|
||||
|
||||
let _ = std::io::stdout().flush();
|
||||
|
||||
num_frame_buffer_rows
|
||||
}
|
||||
|
||||
fn scroll_view_lines_if_needed(draw_commands: Vec<DrawCommand>, use_color_buffer: bool) {
|
||||
let mut starting_row = 0;
|
||||
|
||||
if let Ok(_raw) = crossterm::terminal::enable_raw_mode() {
|
||||
let mut size = crossterm::terminal::size().unwrap_or_else(|_| (80, 24));
|
||||
let height = size.1 as usize - 1;
|
||||
|
||||
let mut max_bottom_line = paint_textview(&draw_commands, starting_row, use_color_buffer);
|
||||
|
||||
// Only scroll if needed
|
||||
if max_bottom_line > height as usize {
|
||||
let _ = std::io::stdout().execute(crossterm::cursor::Hide);
|
||||
|
||||
loop {
|
||||
if let Ok(ev) = crossterm::event::read() {
|
||||
if let crossterm::event::Event::Key(KeyEvent { code, modifiers }) = ev {
|
||||
match code {
|
||||
KeyCode::Esc => {
|
||||
break;
|
||||
}
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
if starting_row > 0 {
|
||||
starting_row -= 1;
|
||||
max_bottom_line = paint_textview(
|
||||
&draw_commands,
|
||||
starting_row,
|
||||
use_color_buffer,
|
||||
);
|
||||
}
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
if starting_row < (max_bottom_line - height) {
|
||||
starting_row += 1;
|
||||
}
|
||||
max_bottom_line =
|
||||
paint_textview(&draw_commands, starting_row, use_color_buffer);
|
||||
}
|
||||
KeyCode::Char('b')
|
||||
if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) =>
|
||||
{
|
||||
starting_row -= std::cmp::min(height, starting_row);
|
||||
max_bottom_line =
|
||||
paint_textview(&draw_commands, starting_row, use_color_buffer);
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
starting_row -= std::cmp::min(height, starting_row);
|
||||
max_bottom_line =
|
||||
paint_textview(&draw_commands, starting_row, use_color_buffer);
|
||||
}
|
||||
KeyCode::Char('f')
|
||||
if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) =>
|
||||
{
|
||||
if starting_row < (max_bottom_line - height) {
|
||||
starting_row += height;
|
||||
|
||||
if starting_row > (max_bottom_line - height) {
|
||||
starting_row = max_bottom_line - height;
|
||||
}
|
||||
}
|
||||
max_bottom_line =
|
||||
paint_textview(&draw_commands, starting_row, use_color_buffer);
|
||||
}
|
||||
KeyCode::PageDown | KeyCode::Char(' ') => {
|
||||
if starting_row < (max_bottom_line - height) {
|
||||
starting_row += height;
|
||||
|
||||
if starting_row > (max_bottom_line - height) {
|
||||
starting_row = max_bottom_line - height;
|
||||
}
|
||||
}
|
||||
max_bottom_line =
|
||||
paint_textview(&draw_commands, starting_row, use_color_buffer);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(new_size) = crossterm::terminal::size() {
|
||||
if size != new_size {
|
||||
size = new_size;
|
||||
let _ = std::io::stdout().execute(crossterm::terminal::Clear(
|
||||
crossterm::terminal::ClearType::All,
|
||||
));
|
||||
max_bottom_line =
|
||||
paint_textview(&draw_commands, starting_row, use_color_buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = std::io::stdout().execute(crossterm::cursor::Show);
|
||||
let _ = crossterm::terminal::disable_raw_mode();
|
||||
}
|
||||
|
||||
println!()
|
||||
}
|
||||
|
||||
fn scroll_view(s: &str) {
|
||||
let mut v = vec![];
|
||||
for line in s.lines() {
|
||||
v.push(DrawCommand::DrawString(Style::default(), line.to_string()));
|
||||
v.push(DrawCommand::NextLine);
|
||||
}
|
||||
scroll_view_lines_if_needed(v, false);
|
||||
}
|
||||
|
||||
pub fn view_text_value(value: &Value) {
|
||||
let value_anchor = value.anchor();
|
||||
if let UntaggedValue::Primitive(Primitive::String(ref s)) = &value.value {
|
||||
if let Some(source) = value_anchor {
|
||||
let extension: Option<String> = match source {
|
||||
AnchorLocation::File(file) => {
|
||||
let path = Path::new(&file);
|
||||
path.extension().map(|x| x.to_string_lossy().to_string())
|
||||
}
|
||||
AnchorLocation::Url(url) => {
|
||||
let url = url::Url::parse(&url);
|
||||
if let Ok(url) = url {
|
||||
if let Some(mut segments) = url.path_segments() {
|
||||
if let Some(file) = segments.next_back() {
|
||||
let path = Path::new(file);
|
||||
path.extension().map(|x| x.to_string_lossy().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
//FIXME: this probably isn't correct
|
||||
AnchorLocation::Source(_source) => None,
|
||||
};
|
||||
|
||||
match extension {
|
||||
Some(extension) => {
|
||||
// Load these once at the start of your program
|
||||
let ps: SyntaxSet =
|
||||
syntect::dumps::from_binary(include_bytes!("assets/syntaxes.bin"));
|
||||
|
||||
if let Some(syntax) = ps.find_syntax_by_extension(&extension) {
|
||||
let ts: ThemeSet =
|
||||
syntect::dumps::from_binary(include_bytes!("assets/themes.bin"));
|
||||
let mut h = HighlightLines::new(syntax, &ts.themes["OneHalfDark"]);
|
||||
|
||||
let mut v = vec![];
|
||||
for line in s.lines() {
|
||||
let ranges: Vec<(Style, &str)> = h.highlight(line, &ps);
|
||||
|
||||
for range in ranges {
|
||||
v.push(DrawCommand::DrawString(range.0, range.1.to_string()));
|
||||
}
|
||||
|
||||
v.push(DrawCommand::NextLine);
|
||||
}
|
||||
scroll_view_lines_if_needed(v, true);
|
||||
} else {
|
||||
scroll_view(s);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
scroll_view(s);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scroll_view(s);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +1,21 @@
|
||||
[package]
|
||||
name = "nu_plugin_tree"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Yehuda Katz <wycats@gmail.com>", "Jonathan Turner <jonathan.d.turner@gmail.com>", "Andrés N. Robalino <andres@androbtech.com>"]
|
||||
edition = "2018"
|
||||
description = "Tree viewer plugin for Nushell"
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.9.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.9.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.9.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.9.0" }
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.10.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.10.0" }
|
||||
nu-source = { path = "../nu-source", version = "0.10.0" }
|
||||
nu-errors = { path = "../nu-errors", version = "0.10.0" }
|
||||
ptree = {version = "0.2" }
|
||||
derive-new = "0.5.8"
|
||||
|
||||
[build-dependencies]
|
||||
nu-build = { version = "0.9.0", path = "../nu-build" }
|
||||
nu-build = { version = "0.10.0", path = "../nu-build" }
|
||||
|
4
crates/nu_plugin_tree/src/lib.rs
Normal file
4
crates/nu_plugin_tree/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod nu;
|
||||
mod tree;
|
||||
|
||||
pub use tree::TreeViewer;
|
@ -1,100 +1,5 @@
|
||||
use derive_new::new;
|
||||
use nu_errors::ShellError;
|
||||
use nu_plugin::{serve_plugin, Plugin};
|
||||
use nu_protocol::{format_primitive, CallInfo, Signature, UntaggedValue, Value};
|
||||
use ptree::item::StringItem;
|
||||
use ptree::output::print_tree_with;
|
||||
use ptree::print_config::PrintConfig;
|
||||
use ptree::style::{Color, Style};
|
||||
use ptree::TreeBuilder;
|
||||
|
||||
#[derive(new)]
|
||||
pub struct TreeView {
|
||||
tree: StringItem,
|
||||
}
|
||||
|
||||
impl TreeView {
|
||||
fn from_value_helper(value: &UntaggedValue, mut builder: &mut TreeBuilder) {
|
||||
match value {
|
||||
UntaggedValue::Primitive(p) => {
|
||||
let _ = builder.add_empty_child(format_primitive(p, None));
|
||||
}
|
||||
UntaggedValue::Row(o) => {
|
||||
for (k, v) in o.entries.iter() {
|
||||
builder = builder.begin_child(k.clone());
|
||||
Self::from_value_helper(v, builder);
|
||||
builder = builder.end_child();
|
||||
}
|
||||
}
|
||||
UntaggedValue::Table(l) => {
|
||||
for elem in l.iter() {
|
||||
Self::from_value_helper(elem, builder);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn from_value(value: &Value) -> TreeView {
|
||||
let descs = value.data_descriptors();
|
||||
|
||||
let mut tree = TreeBuilder::new("".to_string());
|
||||
let mut builder = &mut tree;
|
||||
|
||||
for desc in descs {
|
||||
let value = match &value.value {
|
||||
UntaggedValue::Row(d) => d.get_data(&desc).borrow().clone(),
|
||||
_ => value.clone(),
|
||||
};
|
||||
builder = builder.begin_child(desc.clone());
|
||||
Self::from_value_helper(&value, &mut builder);
|
||||
builder = builder.end_child();
|
||||
//entries.push((desc.name.clone(), value.borrow().copy()))
|
||||
}
|
||||
|
||||
TreeView::new(builder.build())
|
||||
}
|
||||
|
||||
fn render_view(&self) -> Result<(), ShellError> {
|
||||
// Set up the print configuration
|
||||
let config = {
|
||||
let mut config = PrintConfig::from_env();
|
||||
config.branch = Style {
|
||||
foreground: Some(Color::Green),
|
||||
dimmed: true,
|
||||
..Style::default()
|
||||
};
|
||||
config.leaf = Style {
|
||||
bold: true,
|
||||
..Style::default()
|
||||
};
|
||||
config.indent = 4;
|
||||
config
|
||||
};
|
||||
|
||||
// Print out the tree using custom formatting
|
||||
print_tree_with(&self.tree, &config)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct TreeViewer;
|
||||
|
||||
impl Plugin for TreeViewer {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("tree").desc("View the contents of the pipeline as a tree."))
|
||||
}
|
||||
|
||||
fn sink(&mut self, _call_info: CallInfo, input: Vec<Value>) {
|
||||
if !input.is_empty() {
|
||||
for i in input.iter() {
|
||||
let view = TreeView::from_value(&i);
|
||||
let _ = view.render_view();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
use nu_plugin::serve_plugin;
|
||||
use nu_plugin_tree::TreeViewer;
|
||||
|
||||
fn main() {
|
||||
serve_plugin(&mut TreeViewer);
|
||||
|
21
crates/nu_plugin_tree/src/nu/mod.rs
Normal file
21
crates/nu_plugin_tree/src/nu/mod.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use nu_errors::ShellError;
|
||||
use nu_plugin::Plugin;
|
||||
use nu_protocol::{CallInfo, Signature, Value};
|
||||
|
||||
use crate::tree::TreeView;
|
||||
use crate::TreeViewer;
|
||||
|
||||
impl Plugin for TreeViewer {
|
||||
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||
Ok(Signature::build("tree").desc("View the contents of the pipeline as a tree."))
|
||||
}
|
||||
|
||||
fn sink(&mut self, _call_info: CallInfo, input: Vec<Value>) {
|
||||
if !input.is_empty() {
|
||||
for i in input.iter() {
|
||||
let view = TreeView::from_value(&i);
|
||||
let _ = view.render_view();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
80
crates/nu_plugin_tree/src/tree.rs
Normal file
80
crates/nu_plugin_tree/src/tree.rs
Normal file
@ -0,0 +1,80 @@
|
||||
use derive_new::new;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{format_primitive, UntaggedValue, Value};
|
||||
use ptree::item::StringItem;
|
||||
use ptree::output::print_tree_with;
|
||||
use ptree::print_config::PrintConfig;
|
||||
use ptree::style::{Color, Style};
|
||||
use ptree::TreeBuilder;
|
||||
|
||||
pub struct TreeViewer;
|
||||
#[derive(new)]
|
||||
pub struct TreeView {
|
||||
tree: StringItem,
|
||||
}
|
||||
|
||||
impl TreeView {
|
||||
fn from_value_helper(value: &UntaggedValue, mut builder: &mut TreeBuilder) {
|
||||
match value {
|
||||
UntaggedValue::Primitive(p) => {
|
||||
let _ = builder.add_empty_child(format_primitive(p, None));
|
||||
}
|
||||
UntaggedValue::Row(o) => {
|
||||
for (k, v) in o.entries.iter() {
|
||||
builder = builder.begin_child(k.clone());
|
||||
Self::from_value_helper(v, builder);
|
||||
builder = builder.end_child();
|
||||
}
|
||||
}
|
||||
UntaggedValue::Table(l) => {
|
||||
for elem in l.iter() {
|
||||
Self::from_value_helper(elem, builder);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_value(value: &Value) -> TreeView {
|
||||
let descs = value.data_descriptors();
|
||||
|
||||
let mut tree = TreeBuilder::new("".to_string());
|
||||
let mut builder = &mut tree;
|
||||
|
||||
for desc in descs {
|
||||
let value = match &value.value {
|
||||
UntaggedValue::Row(d) => d.get_data(&desc).borrow().clone(),
|
||||
_ => value.clone(),
|
||||
};
|
||||
builder = builder.begin_child(desc.clone());
|
||||
Self::from_value_helper(&value, &mut builder);
|
||||
builder = builder.end_child();
|
||||
//entries.push((desc.name.clone(), value.borrow().copy()))
|
||||
}
|
||||
|
||||
TreeView::new(builder.build())
|
||||
}
|
||||
|
||||
pub fn render_view(&self) -> Result<(), ShellError> {
|
||||
// Set up the print configuration
|
||||
let config = {
|
||||
let mut config = PrintConfig::from_env();
|
||||
config.branch = Style {
|
||||
foreground: Some(Color::Green),
|
||||
dimmed: true,
|
||||
..Style::default()
|
||||
};
|
||||
config.leaf = Style {
|
||||
bold: true,
|
||||
..Style::default()
|
||||
};
|
||||
config.indent = 4;
|
||||
config
|
||||
};
|
||||
|
||||
// Print out the tree using custom formatting
|
||||
print_tree_with(&self.tree, &config)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
116
docs/commands/calc.md
Normal file
116
docs/commands/calc.md
Normal file
@ -0,0 +1,116 @@
|
||||
# calc
|
||||
|
||||
calc is a command that takes a math expression from the pipeline and calculates that into a number.
|
||||
|
||||
This command supports the following operations -
|
||||
|
||||
operations :
|
||||
* binary operators: +, -, *, /, % (remainder), ^ (power)
|
||||
* unary operators: +, -, ! (factorial)
|
||||
|
||||
functions :
|
||||
* sqrt, abs
|
||||
* exp, ln, log10
|
||||
* sin, cos, tan, asin, acos, atan, atan2
|
||||
* sinh, cosh, tanh, asinh, acosh, atanh
|
||||
* floor, ceil, round
|
||||
* signum
|
||||
* max(x, ...), min(x, ...): maximum and minimumum of 1 or more numbers
|
||||
|
||||
constants:
|
||||
* pi
|
||||
* e
|
||||
|
||||
## Examples -
|
||||
|
||||
```
|
||||
> echo "1+2+3" | calc
|
||||
6.000000000000000
|
||||
> echo "1-2+3" | calc
|
||||
2.000000000000000
|
||||
> echo "-(-23)" | calc
|
||||
23.00000000000000
|
||||
> echo "5^2" | calc
|
||||
25.00000000000000
|
||||
> echo "5^3" | calc
|
||||
125.0000000000000
|
||||
> echo "min(5,4,3,2,1,0,-100,45)" | calc
|
||||
-100.0000000000000
|
||||
> echo "max(5,4,3,2,1,0,-100,45)" | calc
|
||||
45.00000000000000
|
||||
> echo "sqrt(2) | calc"
|
||||
1.414213562373095
|
||||
> echo pi | calc
|
||||
3.141592653589793
|
||||
> echo e | calc
|
||||
2.718281828459045
|
||||
> echo "sin(pi / 2)" | calc
|
||||
1.000000000000000
|
||||
> echo "floor(5999/1000)" | calc
|
||||
5.000000000000000
|
||||
```
|
||||
|
||||
```
|
||||
❯ open abc.json
|
||||
───┬──────
|
||||
# │ size
|
||||
───┼──────
|
||||
0 │ 816
|
||||
1 │ 1627
|
||||
2 │ 1436
|
||||
3 │ 1573
|
||||
4 │ 935
|
||||
5 │ 52
|
||||
6 │ 999
|
||||
7 │ 1639
|
||||
───┴──────
|
||||
|
||||
❯ open abc.json | format "({size} + 500) * 4"
|
||||
───┬──────────────────
|
||||
# │ <value>
|
||||
───┼──────────────────
|
||||
0 │ (816 + 500) * 4
|
||||
1 │ (1627 + 500) * 4
|
||||
2 │ (1436 + 500) * 4
|
||||
3 │ (1573 + 500) * 4
|
||||
4 │ (935 + 500) * 4
|
||||
5 │ (52 + 500) * 4
|
||||
6 │ (999 + 500) * 4
|
||||
7 │ (1639 + 500) * 4
|
||||
───┴──────────────────
|
||||
|
||||
❯ open abc.json | format "({size} + 500) * 4" | calc
|
||||
───┬───────────
|
||||
# │ <value>
|
||||
───┼───────────
|
||||
0 │ 5264.0000
|
||||
1 │ 8508.0000
|
||||
2 │ 7744.0000
|
||||
3 │ 8292.0000
|
||||
4 │ 5740.0000
|
||||
5 │ 2208.0000
|
||||
6 │ 5996.0000
|
||||
7 │ 8556.0000
|
||||
───┴───────────
|
||||
|
||||
❯ open abc.json | format "({size} - 1000) * 4" | calc
|
||||
───┬────────────
|
||||
# │ <value>
|
||||
───┼────────────
|
||||
0 │ -736.0000
|
||||
1 │ 2508.0000
|
||||
2 │ 1744.0000
|
||||
3 │ 2292.0000
|
||||
4 │ -260.0000
|
||||
5 │ -3792.0000
|
||||
6 │ -4.0000
|
||||
7 │ 2556.0000
|
||||
───┴────────────
|
||||
```
|
||||
|
||||
Note that since `calc` uses floating-point numbers, the result may not always be precise.
|
||||
|
||||
```
|
||||
> echo "floor(5999999999999999999/1000000000000000000)" | calc
|
||||
6.000000000000000
|
||||
```
|
@ -1,11 +1,11 @@
|
||||
# add
|
||||
# insert
|
||||
|
||||
This command adds a column to any table output. The first parameter takes the heading, the second parameter takes the value for all the rows.
|
||||
|
||||
## Examples
|
||||
|
||||
```shell
|
||||
> ls | add is_on_a_computer yes_obviously
|
||||
> ls | insert is_on_a_computer yes_obviously
|
||||
━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━┯━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━━━━┯━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━
|
||||
# │ name │ type │ readonly │ size │ accessed │ modified │ is_on_a_computer
|
||||
───┼────────────────────────────┼──────┼──────────┼────────┼───────────┼───────────┼──────────────────
|
||||
@ -18,7 +18,7 @@ This command adds a column to any table output. The first parameter takes the he
|
||||
```
|
||||
|
||||
```shell
|
||||
> shells | add os linux_on_this_machine
|
||||
> shells | insert os linux_on_this_machine
|
||||
━━━┯━━━┯━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━
|
||||
# │ │ name │ path │ os
|
||||
───┼───┼────────────┼────────────────────────────────┼───────────────────────
|
@ -19,18 +19,18 @@ Yehuda,Katz,10/11/2013,A
|
||||
━━━┯━━━━━━━━━━━━┯━━━━━━━━━━━┯━━━━━━━━━━━━┯━━━━━━
|
||||
# │ first_name │ last_name │ rusty_at │ type
|
||||
───┼────────────┼───────────┼────────────┼──────
|
||||
0 │ Andrés │ Robalino │ 10/11/2013 │ A
|
||||
1 │ Jonathan │ Turner │ 10/12/2013 │ B
|
||||
2 │ Yehuda │ Katz │ 10/11/2013 │ A
|
||||
0 │ Andrés │ Robalino │ 10/11/2013 │ A
|
||||
1 │ Jonathan │ Turner │ 10/12/2013 │ B
|
||||
2 │ Yehuda │ Katz │ 10/11/2013 │ A
|
||||
━━━┷━━━━━━━━━━━━┷━━━━━━━━━━━┷━━━━━━━━━━━━┷━━━━━━
|
||||
```
|
||||
|
||||
```
|
||||
> `open test.csv | get type | uniq`
|
||||
━━━┯━━━━━━━━━
|
||||
# │ <value>
|
||||
# │ <value>
|
||||
───┼─────────
|
||||
0 │ A
|
||||
1 │ B
|
||||
0 │ A
|
||||
1 │ B
|
||||
━━━┷━━━━━━━━━
|
||||
```
|
162
src/cli.rs
162
src/cli.rs
@ -6,13 +6,10 @@ use crate::context::Context;
|
||||
#[cfg(not(feature = "starship-prompt"))]
|
||||
use crate::git::current_branch;
|
||||
use crate::prelude::*;
|
||||
use futures_codec::{FramedRead, LinesCodec};
|
||||
use nu_errors::ShellError;
|
||||
use nu_parser::hir::Expression;
|
||||
use nu_parser::{
|
||||
hir, ClassifiedCommand, ClassifiedPipeline, InternalCommand, PipelineShape, SpannedToken,
|
||||
TokensIterator,
|
||||
};
|
||||
use nu_protocol::{Signature, Value};
|
||||
use nu_parser::{ClassifiedPipeline, PipelineShape, SpannedToken, TokensIterator};
|
||||
use nu_protocol::{Primitive, ReturnSuccess, Signature, UntaggedValue, Value};
|
||||
|
||||
use log::{debug, log_enabled, trace};
|
||||
use rustyline::error::ReadlineError;
|
||||
@ -137,7 +134,7 @@ fn search_paths() -> Vec<std::path::PathBuf> {
|
||||
search_paths
|
||||
}
|
||||
|
||||
fn load_plugins(context: &mut Context) -> Result<(), ShellError> {
|
||||
pub fn load_plugins(context: &mut Context) -> Result<(), ShellError> {
|
||||
let opts = glob::MatchOptions {
|
||||
case_sensitive: false,
|
||||
require_literal_separator: false,
|
||||
@ -147,7 +144,7 @@ fn load_plugins(context: &mut Context) -> Result<(), ShellError> {
|
||||
for path in search_paths() {
|
||||
let mut pattern = path.to_path_buf();
|
||||
|
||||
pattern.push(std::path::Path::new("nu_plugin_[a-z]*"));
|
||||
pattern.push(std::path::Path::new("nu_plugin_[a-z0-9][a-z0-9]*"));
|
||||
|
||||
match glob::glob_with(&pattern.to_string_lossy(), opts) {
|
||||
Err(_) => {}
|
||||
@ -173,14 +170,14 @@ fn load_plugins(context: &mut Context) -> Result<(), ShellError> {
|
||||
{
|
||||
bin_name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphabetic() || c == '_' || c == '.')
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.')
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
bin_name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphabetic() || c == '_')
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_')
|
||||
}
|
||||
};
|
||||
|
||||
@ -240,10 +237,9 @@ fn create_default_starship_config() -> Option<toml::Value> {
|
||||
Some(toml::Value::Table(map))
|
||||
}
|
||||
|
||||
/// 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() -> Result<(), Box<dyn Error>> {
|
||||
let mut syncer = crate::env::environment_syncer::EnvironmentSyncer::new();
|
||||
|
||||
pub fn create_default_context(
|
||||
syncer: &mut crate::env::environment_syncer::EnvironmentSyncer,
|
||||
) -> Result<Context, Box<dyn Error>> {
|
||||
syncer.load_environment();
|
||||
|
||||
let mut context = Context::basic()?;
|
||||
@ -268,8 +264,10 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
|
||||
whole_stream_command(Save),
|
||||
per_item_command(Cpy),
|
||||
whole_stream_command(Date),
|
||||
per_item_command(Calc),
|
||||
per_item_command(Mkdir),
|
||||
per_item_command(Move),
|
||||
per_item_command(Kill),
|
||||
whole_stream_command(Version),
|
||||
whole_stream_command(Clear),
|
||||
whole_stream_command(What),
|
||||
@ -372,6 +370,55 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
pub async fn run_pipeline_standalone(
|
||||
pipeline: String,
|
||||
redirect_stdin: bool,
|
||||
context: &mut Context,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let line = process_line(Ok(pipeline), context, redirect_stdin).await;
|
||||
|
||||
match line {
|
||||
LineResult::Success(line) => {
|
||||
let error_code = {
|
||||
let errors = context.current_errors.clone();
|
||||
let errors = errors.lock();
|
||||
|
||||
if errors.len() > 0 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
|
||||
context.maybe_print_errors(Text::from(line));
|
||||
if error_code != 0 {
|
||||
std::process::exit(error_code);
|
||||
}
|
||||
}
|
||||
|
||||
LineResult::Error(line, err) => {
|
||||
context.with_host(|host| {
|
||||
print_err(err, host, &Text::from(line.clone()));
|
||||
});
|
||||
|
||||
context.maybe_print_errors(Text::from(line));
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
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() -> Result<(), Box<dyn Error>> {
|
||||
let mut syncer = crate::env::environment_syncer::EnvironmentSyncer::new();
|
||||
let mut context = create_default_context(&mut syncer)?;
|
||||
|
||||
let _ = load_plugins(&mut context);
|
||||
|
||||
let config = Config::builder().color_mode(ColorMode::Forced).build();
|
||||
@ -480,7 +527,7 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
|
||||
initial_command = None;
|
||||
}
|
||||
|
||||
let line = process_line(readline, &mut context).await;
|
||||
let line = process_line(readline, &mut context, false).await;
|
||||
|
||||
// Check the config to see if we need to update the path
|
||||
// TODO: make sure config is cached so we don't path this load every call
|
||||
@ -559,7 +606,11 @@ enum LineResult {
|
||||
}
|
||||
|
||||
/// Process the line by parsing the text to turn it into commands, classify those commands so that we understand what is being called in the pipeline, and then run this pipeline
|
||||
async fn process_line(readline: Result<String, ReadlineError>, ctx: &mut Context) -> LineResult {
|
||||
async fn process_line(
|
||||
readline: Result<String, ReadlineError>,
|
||||
ctx: &mut Context,
|
||||
redirect_stdin: bool,
|
||||
) -> LineResult {
|
||||
match &readline {
|
||||
Ok(line) if line.trim() == "" => LineResult::Success(line.clone()),
|
||||
|
||||
@ -577,37 +628,70 @@ async fn process_line(readline: Result<String, ReadlineError>, ctx: &mut Context
|
||||
debug!("=== Parsed ===");
|
||||
debug!("{:#?}", result);
|
||||
|
||||
let mut pipeline = classify_pipeline(&result, ctx, &Text::from(line));
|
||||
let pipeline = classify_pipeline(&result, ctx, &Text::from(line));
|
||||
|
||||
if let Some(failure) = pipeline.failed {
|
||||
return LineResult::Error(line.to_string(), failure.into());
|
||||
}
|
||||
|
||||
let should_push = match pipeline.commands.list.last() {
|
||||
Some(ClassifiedCommand::External(_)) => false,
|
||||
_ => true,
|
||||
let input_stream = if redirect_stdin {
|
||||
let file = futures::io::AllowStdIo::new(
|
||||
crate::commands::classified::external::StdoutWithNewline::new(std::io::stdin()),
|
||||
);
|
||||
let stream = FramedRead::new(file, LinesCodec).map(|line| {
|
||||
if let Ok(line) = line {
|
||||
Ok(Value {
|
||||
value: UntaggedValue::Primitive(Primitive::String(line)),
|
||||
tag: Tag::unknown(),
|
||||
})
|
||||
} else {
|
||||
panic!("Internal error: could not read lines of text from stdin")
|
||||
}
|
||||
});
|
||||
Some(stream.to_input_stream())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if should_push {
|
||||
pipeline
|
||||
.commands
|
||||
.list
|
||||
.push(ClassifiedCommand::Internal(InternalCommand {
|
||||
name: "autoview".to_string(),
|
||||
name_tag: Tag::unknown(),
|
||||
args: hir::Call::new(
|
||||
Box::new(
|
||||
Expression::synthetic_string("autoview").into_expr(Span::unknown()),
|
||||
),
|
||||
None,
|
||||
None,
|
||||
Span::unknown(),
|
||||
),
|
||||
}));
|
||||
}
|
||||
match run_pipeline(pipeline, ctx, input_stream, line).await {
|
||||
Ok(Some(input)) => {
|
||||
// Running a pipeline gives us back a stream that we can then
|
||||
// work through. At the top level, we just want to pull on the
|
||||
// values to compute them.
|
||||
use futures::stream::TryStreamExt;
|
||||
|
||||
match run_pipeline(pipeline, ctx, None, line).await {
|
||||
Ok(_) => LineResult::Success(line.to_string()),
|
||||
let context = RunnableContext {
|
||||
input,
|
||||
shell_manager: ctx.shell_manager.clone(),
|
||||
host: ctx.host.clone(),
|
||||
ctrl_c: ctx.ctrl_c.clone(),
|
||||
commands: ctx.registry.clone(),
|
||||
name: Tag::unknown(),
|
||||
source: Text::from(String::new()),
|
||||
};
|
||||
|
||||
if let Ok(mut output_stream) = crate::commands::autoview::autoview(context) {
|
||||
loop {
|
||||
match output_stream.try_next().await {
|
||||
Ok(Some(ReturnSuccess::Value(Value {
|
||||
value: UntaggedValue::Error(e),
|
||||
..
|
||||
}))) => return LineResult::Error(line.to_string(), e),
|
||||
Ok(Some(_item)) => {
|
||||
if ctx.ctrl_c.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LineResult::Success(line.to_string())
|
||||
}
|
||||
Ok(None) => LineResult::Success(line.to_string()),
|
||||
Err(err) => LineResult::Error(line.to_string(), err),
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ mod to_delimited_data;
|
||||
pub(crate) mod append;
|
||||
pub(crate) mod args;
|
||||
pub(crate) mod autoview;
|
||||
pub(crate) mod calc;
|
||||
pub(crate) mod cd;
|
||||
pub(crate) mod classified;
|
||||
pub(crate) mod clip;
|
||||
@ -102,11 +103,12 @@ pub(crate) mod wrap;
|
||||
pub(crate) use autoview::Autoview;
|
||||
pub(crate) use cd::Cd;
|
||||
pub(crate) use command::{
|
||||
per_item_command, whole_stream_command, Command, PerItemCommand, RawCommandArgs,
|
||||
UnevaluatedCallInfo, WholeStreamCommand,
|
||||
per_item_command, whole_stream_command, Command, PerItemCommand, UnevaluatedCallInfo,
|
||||
WholeStreamCommand,
|
||||
};
|
||||
|
||||
pub(crate) use append::Append;
|
||||
pub(crate) use calc::Calc;
|
||||
pub(crate) use compact::Compact;
|
||||
pub(crate) use config::Config;
|
||||
pub(crate) use count::Count;
|
||||
@ -117,6 +119,8 @@ pub(crate) use default::Default;
|
||||
pub(crate) use du::Du;
|
||||
pub(crate) use echo::Echo;
|
||||
pub(crate) use edit::Edit;
|
||||
pub(crate) mod kill;
|
||||
pub(crate) use kill::Kill;
|
||||
pub(crate) mod clear;
|
||||
pub(crate) use clear::Clear;
|
||||
pub(crate) use enter::Enter;
|
||||
|
@ -43,6 +43,7 @@ fn append(
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
let mut after: VecDeque<Value> = VecDeque::new();
|
||||
after.push_back(row);
|
||||
let after = futures::stream::iter(after);
|
||||
|
||||
Ok(OutputStream::from_input(input.values.chain(after)))
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
use crate::commands::{RawCommandArgs, WholeStreamCommand};
|
||||
use crate::commands::UnevaluatedCallInfo;
|
||||
use crate::commands::WholeStreamCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_parser::{hir, hir::Expression, hir::Literal, hir::SpannedExpression};
|
||||
use nu_protocol::{Primitive, ReturnSuccess, Signature, UntaggedValue, Value};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
pub struct Autoview;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AutoviewArgs {}
|
||||
|
||||
impl WholeStreamCommand for Autoview {
|
||||
fn name(&self) -> &str {
|
||||
"autoview"
|
||||
@ -27,21 +27,48 @@ impl WholeStreamCommand for Autoview {
|
||||
args: CommandArgs,
|
||||
registry: &CommandRegistry,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
Ok(args.process_raw(registry, autoview)?.run())
|
||||
autoview(RunnableContext {
|
||||
input: args.input,
|
||||
commands: registry.clone(),
|
||||
shell_manager: args.shell_manager,
|
||||
host: args.host,
|
||||
source: args.call_info.source,
|
||||
ctrl_c: args.ctrl_c,
|
||||
name: args.call_info.name_tag,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn autoview(
|
||||
AutoviewArgs {}: AutoviewArgs,
|
||||
context: RunnableContext,
|
||||
raw: RawCommandArgs,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
pub struct RunnableContextWithoutInput {
|
||||
pub shell_manager: ShellManager,
|
||||
pub host: Arc<parking_lot::Mutex<Box<dyn Host>>>,
|
||||
pub source: Text,
|
||||
pub ctrl_c: Arc<AtomicBool>,
|
||||
pub commands: CommandRegistry,
|
||||
pub name: Tag,
|
||||
}
|
||||
|
||||
impl RunnableContextWithoutInput {
|
||||
pub fn convert(context: RunnableContext) -> (InputStream, RunnableContextWithoutInput) {
|
||||
let new_context = RunnableContextWithoutInput {
|
||||
shell_manager: context.shell_manager,
|
||||
host: context.host,
|
||||
source: context.source,
|
||||
ctrl_c: context.ctrl_c,
|
||||
commands: context.commands,
|
||||
name: context.name,
|
||||
};
|
||||
(context.input, new_context)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn autoview(context: RunnableContext) -> Result<OutputStream, ShellError> {
|
||||
let binary = context.get_command("binaryview");
|
||||
let text = context.get_command("textview");
|
||||
let table = context.get_command("table");
|
||||
|
||||
Ok(OutputStream::new(async_stream! {
|
||||
let mut input_stream = context.input;
|
||||
let (mut input_stream, context) = RunnableContextWithoutInput::convert(context);
|
||||
|
||||
match input_stream.next().await {
|
||||
Some(x) => {
|
||||
@ -66,7 +93,7 @@ pub fn autoview(
|
||||
};
|
||||
let stream = stream.to_input_stream();
|
||||
if let Some(table) = table {
|
||||
let mut command_args = raw.with_input(stream);
|
||||
let command_args = create_default_command_args(&context).with_input(stream);
|
||||
let result = table.run(command_args, &context.commands);
|
||||
result.collect::<Vec<_>>().await;
|
||||
}
|
||||
@ -80,7 +107,8 @@ pub fn autoview(
|
||||
if let Some(text) = text {
|
||||
let mut stream = VecDeque::new();
|
||||
stream.push_back(UntaggedValue::string(s).into_value(Tag { anchor, span }));
|
||||
let result = text.run(raw.with_input(stream), &context.commands);
|
||||
let command_args = create_default_command_args(&context).with_input(stream);
|
||||
let result = text.run(command_args, &context.commands);
|
||||
result.collect::<Vec<_>>().await;
|
||||
} else {
|
||||
outln!("{}", s);
|
||||
@ -99,7 +127,8 @@ pub fn autoview(
|
||||
if let Some(text) = text {
|
||||
let mut stream = VecDeque::new();
|
||||
stream.push_back(UntaggedValue::string(s).into_value(Tag { anchor, span }));
|
||||
let result = text.run(raw.with_input(stream), &context.commands);
|
||||
let command_args = create_default_command_args(&context).with_input(stream);
|
||||
let result = text.run(command_args, &context.commands);
|
||||
result.collect::<Vec<_>>().await;
|
||||
} else {
|
||||
outln!("{}\n", s);
|
||||
@ -134,7 +163,8 @@ pub fn autoview(
|
||||
if let Some(binary) = binary {
|
||||
let mut stream = VecDeque::new();
|
||||
stream.push_back(x);
|
||||
let result = binary.run(raw.with_input(stream), &context.commands);
|
||||
let command_args = create_default_command_args(&context).with_input(stream);
|
||||
let result = binary.run(command_args, &context.commands);
|
||||
result.collect::<Vec<_>>().await;
|
||||
} else {
|
||||
use pretty_hex::*;
|
||||
@ -149,7 +179,8 @@ pub fn autoview(
|
||||
if let Some(table) = table {
|
||||
let mut stream = VecDeque::new();
|
||||
stream.push_back(x);
|
||||
let result = table.run(raw.with_input(stream), &context.commands);
|
||||
let command_args = create_default_command_args(&context).with_input(stream);
|
||||
let result = table.run(command_args, &context.commands);
|
||||
result.collect::<Vec<_>>().await;
|
||||
} else {
|
||||
outln!("{:?}", item);
|
||||
@ -170,3 +201,25 @@ pub fn autoview(
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn create_default_command_args(context: &RunnableContextWithoutInput) -> RawCommandArgs {
|
||||
let span = context.name.span;
|
||||
RawCommandArgs {
|
||||
host: context.host.clone(),
|
||||
ctrl_c: context.ctrl_c.clone(),
|
||||
shell_manager: context.shell_manager.clone(),
|
||||
call_info: UnevaluatedCallInfo {
|
||||
args: hir::Call {
|
||||
head: Box::new(SpannedExpression::new(
|
||||
Expression::Literal(Literal::String(span)),
|
||||
span,
|
||||
)),
|
||||
positional: None,
|
||||
named: None,
|
||||
span,
|
||||
},
|
||||
source: context.source.clone(),
|
||||
name_tag: context.name.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
63
src/commands/calc.rs
Normal file
63
src/commands/calc.rs
Normal file
@ -0,0 +1,63 @@
|
||||
use crate::commands::PerItemCommand;
|
||||
use crate::prelude::*;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{CallInfo, Primitive, ReturnSuccess, UntaggedValue, Value};
|
||||
|
||||
pub struct Calc;
|
||||
|
||||
impl PerItemCommand for Calc {
|
||||
fn name(&self) -> &str {
|
||||
"calc"
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Parse a math expression into a number"
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_call_info: &CallInfo,
|
||||
_registry: &CommandRegistry,
|
||||
raw_args: &RawCommandArgs,
|
||||
input: Value,
|
||||
) -> Result<OutputStream, ShellError> {
|
||||
calc(input, raw_args)
|
||||
}
|
||||
}
|
||||
|
||||
fn calc(input: Value, args: &RawCommandArgs) -> Result<OutputStream, ShellError> {
|
||||
let name_span = &args.call_info.name_tag.span;
|
||||
|
||||
let output = if let Ok(string) = input.as_string() {
|
||||
match parse(&string, &input.tag) {
|
||||
Ok(value) => ReturnSuccess::value(value),
|
||||
Err(err) => Err(ShellError::labeled_error(
|
||||
"Calculation error",
|
||||
err,
|
||||
&input.tag.span,
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
Err(ShellError::labeled_error(
|
||||
"Expected a string from pipeline",
|
||||
"requires string input",
|
||||
name_span,
|
||||
))
|
||||
};
|
||||
|
||||
Ok(vec![output].into())
|
||||
}
|
||||
|
||||
pub fn parse(math_expression: &str, tag: impl Into<Tag>) -> Result<Value, String> {
|
||||
use std::f64;
|
||||
let num = meval::eval_str(math_expression);
|
||||
match num {
|
||||
Ok(num) => {
|
||||
if num == f64::INFINITY || num == f64::NEG_INFINITY {
|
||||
return Err(String::from("cannot represent result"));
|
||||
}
|
||||
Ok(UntaggedValue::from(Primitive::from(num)).into_value(tag))
|
||||
}
|
||||
Err(error) => Err(error.to_string()),
|
||||
}
|
||||
}
|
@ -1,53 +1,17 @@
|
||||
use crate::prelude::*;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use futures::stream::StreamExt;
|
||||
use futures_codec::{Decoder, Encoder, FramedRead};
|
||||
use futures_codec::{FramedRead, LinesCodec};
|
||||
use log::trace;
|
||||
use nu_errors::ShellError;
|
||||
use nu_parser::commands::classified::external::ExternalArg;
|
||||
use nu_parser::ExternalCommand;
|
||||
use nu_protocol::{Primitive, ShellTypeName, UntaggedValue, Value};
|
||||
use std::io::{Error, ErrorKind, Write};
|
||||
use nu_protocol::{ColumnPath, Primitive, ShellTypeName, UntaggedValue, Value};
|
||||
use nu_source::{Tag, Tagged};
|
||||
use nu_value_ext::as_column_path;
|
||||
use std::io::Write;
|
||||
use std::ops::Deref;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
/// A simple `Codec` implementation that splits up data into lines.
|
||||
pub struct LinesCodec {}
|
||||
|
||||
impl Encoder for LinesCodec {
|
||||
type Item = String;
|
||||
type Error = Error;
|
||||
|
||||
fn encode(&mut self, item: Self::Item, dst: &mut BytesMut) -> Result<(), Self::Error> {
|
||||
dst.put(item);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for LinesCodec {
|
||||
type Item = nu_protocol::UntaggedValue;
|
||||
type Error = Error;
|
||||
|
||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||
match src.iter().position(|b| b == &b'\n') {
|
||||
Some(pos) if !src.is_empty() => {
|
||||
let buf = src.split_to(pos + 1);
|
||||
String::from_utf8(buf.to_vec())
|
||||
.map(UntaggedValue::line)
|
||||
.map(Some)
|
||||
.map_err(|e| Error::new(ErrorKind::InvalidData, e))
|
||||
}
|
||||
_ if !src.is_empty() => {
|
||||
let drained = src.take();
|
||||
String::from_utf8(drained.to_vec())
|
||||
.map(UntaggedValue::string)
|
||||
.map(Some)
|
||||
.map_err(|e| Error::new(ErrorKind::InvalidData, e))
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nu_value_to_string(command: &ExternalCommand, from: &Value) -> Result<String, ShellError> {
|
||||
match &from.value {
|
||||
UntaggedValue::Primitive(Primitive::Int(i)) => Ok(i.to_string()),
|
||||
@ -55,7 +19,7 @@ pub fn nu_value_to_string(command: &ExternalCommand, from: &Value) -> Result<Str
|
||||
| UntaggedValue::Primitive(Primitive::Line(s)) => Ok(s.clone()),
|
||||
UntaggedValue::Primitive(Primitive::Path(p)) => Ok(p.to_string_lossy().to_string()),
|
||||
unsupported => Err(ShellError::labeled_error(
|
||||
format!("$it needs string data (given: {})", unsupported.type_name()),
|
||||
format!("needs string data (given: {})", unsupported.type_name()),
|
||||
"expected a string",
|
||||
&command.name_tag,
|
||||
)),
|
||||
@ -97,13 +61,59 @@ pub(crate) async fn run_external_command(
|
||||
));
|
||||
}
|
||||
|
||||
if command.has_it_argument() {
|
||||
if command.has_it_argument() || command.has_nu_argument() {
|
||||
run_with_iterator_arg(command, context, input, is_last).await
|
||||
} else {
|
||||
run_with_stdin(command, context, input, is_last).await
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_column_path_for_fetching_it_variable(
|
||||
argument: &ExternalArg,
|
||||
) -> Result<Tagged<ColumnPath>, ShellError> {
|
||||
// We have "$it.[contents of interest]"
|
||||
// and start slicing from "$it.[member+]"
|
||||
// ^ here.
|
||||
let key = nu_source::Text::from(argument.deref()).slice(4..argument.len());
|
||||
|
||||
to_column_path(&key, &argument.tag)
|
||||
}
|
||||
|
||||
fn prepare_column_path_for_fetching_nu_variable(
|
||||
argument: &ExternalArg,
|
||||
) -> Result<Tagged<ColumnPath>, ShellError> {
|
||||
// We have "$nu.[contents of interest]"
|
||||
// and start slicing from "$nu.[member+]"
|
||||
// ^ here.
|
||||
let key = nu_source::Text::from(argument.deref()).slice(4..argument.len());
|
||||
|
||||
to_column_path(&key, &argument.tag)
|
||||
}
|
||||
|
||||
fn to_column_path(
|
||||
path_members: &str,
|
||||
tag: impl Into<Tag>,
|
||||
) -> Result<Tagged<ColumnPath>, ShellError> {
|
||||
let tag = tag.into();
|
||||
|
||||
as_column_path(
|
||||
&UntaggedValue::Table(
|
||||
path_members
|
||||
.split('.')
|
||||
.map(|x| {
|
||||
let member = match x.parse::<u64>() {
|
||||
Ok(v) => UntaggedValue::int(v),
|
||||
Err(_) => UntaggedValue::string(x),
|
||||
};
|
||||
|
||||
member.into_value(&tag)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.into_value(&tag),
|
||||
)
|
||||
}
|
||||
|
||||
async fn run_with_iterator_arg(
|
||||
command: ExternalCommand,
|
||||
context: &mut Context,
|
||||
@ -126,14 +136,136 @@ async fn run_with_iterator_arg(
|
||||
let path = &path;
|
||||
let args = command.args.clone();
|
||||
|
||||
let it_replacement = match nu_value_to_string(&command, &value) {
|
||||
Ok(value) => value,
|
||||
Err(reason) => {
|
||||
yield Ok(Value {
|
||||
value: UntaggedValue::Error(reason),
|
||||
tag: name_tag
|
||||
});
|
||||
return;
|
||||
let it_replacement = {
|
||||
if command.has_it_argument() {
|
||||
let empty_arg = ExternalArg {
|
||||
arg: "".to_string(),
|
||||
tag: name_tag.clone()
|
||||
};
|
||||
|
||||
let key = args.iter()
|
||||
.find(|arg| arg.looks_like_it())
|
||||
.unwrap_or_else(|| &empty_arg);
|
||||
|
||||
if args.iter().all(|arg| !arg.is_it()) {
|
||||
let key = match prepare_column_path_for_fetching_it_variable(&key) {
|
||||
Ok(keypath) => keypath,
|
||||
Err(reason) => {
|
||||
yield Ok(Value {
|
||||
value: UntaggedValue::Error(reason),
|
||||
tag: name_tag
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match crate::commands::get::get_column_path(&key, &value) {
|
||||
Ok(field) => {
|
||||
match nu_value_to_string(&command, &field) {
|
||||
Ok(val) => Some(val),
|
||||
Err(reason) => {
|
||||
yield Ok(Value {
|
||||
value: UntaggedValue::Error(reason),
|
||||
tag: name_tag
|
||||
});
|
||||
return;
|
||||
},
|
||||
}
|
||||
},
|
||||
Err(reason) => {
|
||||
yield Ok(Value {
|
||||
value: UntaggedValue::Error(reason),
|
||||
tag: name_tag
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match nu_value_to_string(&command, &value) {
|
||||
Ok(val) => Some(val),
|
||||
Err(reason) => {
|
||||
yield Ok(Value {
|
||||
value: UntaggedValue::Error(reason),
|
||||
tag: name_tag
|
||||
});
|
||||
return;
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let nu_replacement = {
|
||||
if command.has_nu_argument() {
|
||||
let empty_arg = ExternalArg {
|
||||
arg: "".to_string(),
|
||||
tag: name_tag.clone()
|
||||
};
|
||||
|
||||
let key = args.iter()
|
||||
.find(|arg| arg.looks_like_nu())
|
||||
.unwrap_or_else(|| &empty_arg);
|
||||
|
||||
let nu_var = match crate::evaluate::variables::nu(&name_tag) {
|
||||
Ok(variables) => variables,
|
||||
Err(reason) => {
|
||||
yield Ok(Value {
|
||||
value: UntaggedValue::Error(reason),
|
||||
tag: name_tag
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if args.iter().all(|arg| !arg.is_nu()) {
|
||||
let key = match prepare_column_path_for_fetching_nu_variable(&key) {
|
||||
Ok(keypath) => keypath,
|
||||
Err(reason) => {
|
||||
yield Ok(Value {
|
||||
value: UntaggedValue::Error(reason),
|
||||
tag: name_tag
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match crate::commands::get::get_column_path(&key, &nu_var) {
|
||||
Ok(field) => {
|
||||
match nu_value_to_string(&command, &field) {
|
||||
Ok(val) => Some(val),
|
||||
Err(reason) => {
|
||||
yield Ok(Value {
|
||||
value: UntaggedValue::Error(reason),
|
||||
tag: name_tag
|
||||
});
|
||||
return;
|
||||
},
|
||||
}
|
||||
},
|
||||
Err(reason) => {
|
||||
yield Ok(Value {
|
||||
value: UntaggedValue::Error(reason),
|
||||
tag: name_tag
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match nu_value_to_string(&command, &nu_var) {
|
||||
Ok(val) => Some(val),
|
||||
Err(reason) => {
|
||||
yield Ok(Value {
|
||||
value: UntaggedValue::Error(reason),
|
||||
tag: name_tag
|
||||
});
|
||||
return;
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
@ -141,26 +273,48 @@ async fn run_with_iterator_arg(
|
||||
if arg.chars().all(|c| c.is_whitespace()) {
|
||||
None
|
||||
} else {
|
||||
let arg = if arg.is_it() {
|
||||
let value = it_replacement.to_owned();
|
||||
let value = expand_tilde(&value, || home_dir.as_ref()).as_ref().to_string();
|
||||
let value = {
|
||||
if argument_contains_whitespace(&value) && !argument_is_quoted(&value) {
|
||||
add_quotes(&value)
|
||||
} else {
|
||||
value
|
||||
let arg = if arg.looks_like_it() {
|
||||
if let Some(mut value) = it_replacement.to_owned() {
|
||||
let mut value = expand_tilde(&value, || home_dir.as_ref()).as_ref().to_string();
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
value = {
|
||||
if argument_contains_whitespace(&value) && !argument_is_quoted(&value) {
|
||||
add_quotes(&value)
|
||||
} else {
|
||||
value
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
value
|
||||
Some(value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if arg.looks_like_nu() {
|
||||
if let Some(mut value) = nu_replacement.to_owned() {
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
value = {
|
||||
if argument_contains_whitespace(&value) && !argument_is_quoted(&value) {
|
||||
add_quotes(&value)
|
||||
} else {
|
||||
value
|
||||
}
|
||||
};
|
||||
}
|
||||
Some(value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
arg.to_string()
|
||||
Some(arg.to_string())
|
||||
};
|
||||
|
||||
Some(arg)
|
||||
arg
|
||||
}
|
||||
}).collect::<Vec<String>>();
|
||||
|
||||
match spawn(&command, &path, &process_args[..], None, is_last).await {
|
||||
match spawn(&command, &path, &process_args[..], None, is_last) {
|
||||
Ok(res) => {
|
||||
if let Some(mut res) = res {
|
||||
while let Some(item) = res.next().await {
|
||||
@ -190,83 +344,85 @@ async fn run_with_stdin(
|
||||
) -> Result<Option<InputStream>, ShellError> {
|
||||
let path = context.shell_manager.path();
|
||||
|
||||
let mut inputs: InputStream = if let Some(input) = input {
|
||||
trace_stream!(target: "nu::trace_stream::external::stdin", "input" = input)
|
||||
} else {
|
||||
InputStream::empty()
|
||||
};
|
||||
let input = input
|
||||
.map(|input| trace_stream!(target: "nu::trace_stream::external::stdin", "input" = input));
|
||||
|
||||
let stream = async_stream! {
|
||||
while let Some(value) = inputs.next().await {
|
||||
let name = command.name.clone();
|
||||
let name_tag = command.name_tag.clone();
|
||||
let home_dir = dirs::home_dir();
|
||||
let path = &path;
|
||||
let args = command.args.clone();
|
||||
let process_args = command
|
||||
.args
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
let arg = expand_tilde(arg.deref(), dirs::home_dir);
|
||||
|
||||
let value_for_stdin = match nu_value_to_string_for_stdin(&command, &value) {
|
||||
Ok(value) => value,
|
||||
Err(reason) => {
|
||||
yield Ok(Value {
|
||||
value: UntaggedValue::Error(reason),
|
||||
tag: name_tag
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let process_args = args.iter().map(|arg| {
|
||||
let arg = expand_tilde(arg.deref(), || home_dir.as_ref());
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
if argument_contains_whitespace(&arg) && argument_is_quoted(&arg) {
|
||||
if let Some(unquoted) = remove_quotes(&arg) {
|
||||
format!(r#""{}""#, unquoted)
|
||||
} else {
|
||||
arg.as_ref().to_string()
|
||||
}
|
||||
} else {
|
||||
arg.as_ref().to_string()
|
||||
}
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
if argument_contains_whitespace(&arg) && argument_is_quoted(&arg) {
|
||||
if let Some(unquoted) = remove_quotes(&arg) {
|
||||
unquoted.to_string()
|
||||
format!(r#""{}""#, unquoted)
|
||||
} else {
|
||||
arg.as_ref().to_string()
|
||||
}
|
||||
}
|
||||
}).collect::<Vec<String>>();
|
||||
|
||||
match spawn(&command, &path, &process_args[..], value_for_stdin, is_last).await {
|
||||
Ok(res) => {
|
||||
if let Some(mut res) = res {
|
||||
while let Some(item) = res.next().await {
|
||||
yield Ok(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(reason) => {
|
||||
yield Ok(Value {
|
||||
value: UntaggedValue::Error(reason),
|
||||
tag: name_tag
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
arg.as_ref().to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if let Some(unquoted) = remove_quotes(&arg) {
|
||||
unquoted.to_string()
|
||||
} else {
|
||||
arg.as_ref().to_string()
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
Ok(Some(stream.to_input_stream()))
|
||||
spawn(&command, &path, &process_args[..], input, is_last)
|
||||
}
|
||||
|
||||
async fn spawn(
|
||||
/// This is a wrapper for stdout-like readers that ensure a carriage return ends the stream
|
||||
pub struct StdoutWithNewline<T: std::io::Read> {
|
||||
stdout: T,
|
||||
ended_in_newline: bool,
|
||||
}
|
||||
|
||||
impl<T: std::io::Read> StdoutWithNewline<T> {
|
||||
pub fn new(stdout: T) -> StdoutWithNewline<T> {
|
||||
StdoutWithNewline {
|
||||
stdout,
|
||||
ended_in_newline: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T: std::io::Read> std::io::Read for StdoutWithNewline<T> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
match self.stdout.read(buf) {
|
||||
Err(e) => Err(e),
|
||||
Ok(0) => {
|
||||
if !self.ended_in_newline && !buf.is_empty() {
|
||||
self.ended_in_newline = true;
|
||||
buf[0] = b'\n';
|
||||
Ok(1)
|
||||
} else {
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
Ok(len) => {
|
||||
if buf[len - 1] == b'\n' {
|
||||
self.ended_in_newline = true;
|
||||
} else {
|
||||
self.ended_in_newline = false;
|
||||
}
|
||||
Ok(len)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn(
|
||||
command: &ExternalCommand,
|
||||
path: &str,
|
||||
args: &[String],
|
||||
stdin_contents: Option<String>,
|
||||
input: Option<InputStream>,
|
||||
is_last: bool,
|
||||
) -> Result<Option<InputStream>, ShellError> {
|
||||
let command = command.clone();
|
||||
@ -304,7 +460,7 @@ async fn spawn(
|
||||
}
|
||||
|
||||
// open since we have some contents for stdin
|
||||
if stdin_contents.is_some() {
|
||||
if input.is_some() {
|
||||
process.stdin(Stdio::piped());
|
||||
trace!(target: "nu::run::external", "set up stdin pipe");
|
||||
}
|
||||
@ -313,52 +469,37 @@ async fn spawn(
|
||||
|
||||
if let Ok(mut child) = process.spawn() {
|
||||
let stream = async_stream! {
|
||||
if let Some(mut input) = stdin_contents.as_ref() {
|
||||
if let Some(mut input) = input {
|
||||
let mut stdin_write = child.stdin
|
||||
.take()
|
||||
.expect("Internal error: could not get stdin pipe for external command");
|
||||
|
||||
if let Err(e) = stdin_write.write(input.as_bytes()) {
|
||||
let message = format!("Unable to write to stdin (error = {})", e);
|
||||
while let Some(value) = input.next().await {
|
||||
let input_string = match nu_value_to_string_for_stdin(&command, &value) {
|
||||
Ok(None) => continue,
|
||||
Ok(Some(v)) => v,
|
||||
Err(e) => {
|
||||
yield Ok(Value {
|
||||
value: UntaggedValue::Error(e),
|
||||
tag: name_tag
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
yield Ok(Value {
|
||||
value: UntaggedValue::Error(ShellError::labeled_error(
|
||||
message,
|
||||
"application may have closed before completing pipeline",
|
||||
&name_tag)),
|
||||
tag: name_tag
|
||||
});
|
||||
return;
|
||||
}
|
||||
if let Err(e) = stdin_write.write(input_string.as_bytes()) {
|
||||
let message = format!("Unable to write to stdin (error = {})", e);
|
||||
|
||||
drop(stdin_write);
|
||||
}
|
||||
|
||||
if is_last && command.has_it_argument() {
|
||||
if let Ok(status) = child.wait() {
|
||||
if status.success() {
|
||||
yield Ok(Value {
|
||||
value: UntaggedValue::Error(ShellError::labeled_error(
|
||||
message,
|
||||
"application may have closed before completing pipeline",
|
||||
&name_tag)),
|
||||
tag: name_tag
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// We can give an error when we see a non-zero exit code, but this is different
|
||||
// than what other shells will do.
|
||||
let cfg = crate::data::config::config(Tag::unknown());
|
||||
if let Ok(cfg) = cfg {
|
||||
if cfg.contains_key("nonzero_exit_errors") {
|
||||
yield Ok(Value {
|
||||
value: UntaggedValue::Error(
|
||||
ShellError::labeled_error(
|
||||
"External command failed",
|
||||
"command failed",
|
||||
&name_tag,
|
||||
)
|
||||
),
|
||||
tag: name_tag,
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if !is_last {
|
||||
@ -375,25 +516,33 @@ async fn spawn(
|
||||
return;
|
||||
};
|
||||
|
||||
let file = futures::io::AllowStdIo::new(stdout);
|
||||
let stream = FramedRead::new(file, LinesCodec {});
|
||||
let file = futures::io::AllowStdIo::new(StdoutWithNewline::new(stdout));
|
||||
let mut stream = FramedRead::new(file, LinesCodec);
|
||||
|
||||
let mut stream = stream.map(|line| {
|
||||
while let Some(line) = stream.next().await {
|
||||
if let Ok(line) = line {
|
||||
line.into_value(&name_tag)
|
||||
yield Ok(Value {
|
||||
value: UntaggedValue::Primitive(Primitive::Line(line)),
|
||||
tag: name_tag.clone(),
|
||||
});
|
||||
} else {
|
||||
panic!("Internal error: could not read lines of text from stdin")
|
||||
}
|
||||
});
|
||||
|
||||
loop {
|
||||
match stream.next().await {
|
||||
Some(item) => yield Ok(item),
|
||||
None => break,
|
||||
yield Ok(Value {
|
||||
value: UntaggedValue::Error(
|
||||
ShellError::labeled_error(
|
||||
"Unable to read lines from stdout. This usually happens when the output does not end with a newline.",
|
||||
"unable to read from stdout",
|
||||
&name_tag,
|
||||
)
|
||||
),
|
||||
tag: name_tag.clone(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We can give an error when we see a non-zero exit code, but this is different
|
||||
// than what other shells will do.
|
||||
if child.wait().is_err() {
|
||||
let cfg = crate::data::config::config(Tag::unknown());
|
||||
if let Ok(cfg) = cfg {
|
||||
@ -453,6 +602,7 @@ where
|
||||
shellexpand::tilde_with_context(input, home_dir)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn argument_contains_whitespace(argument: &str) -> bool {
|
||||
argument.chars().any(|c| c.is_whitespace())
|
||||
}
|
||||
@ -462,12 +612,13 @@ fn argument_is_quoted(argument: &str) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
(argument.starts_with('"') && argument.ends_with('"')
|
||||
((argument.starts_with('"') && argument.ends_with('"'))
|
||||
|| (argument.starts_with('\'') && argument.ends_with('\'')))
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn add_quotes(argument: &str) -> String {
|
||||
format!("'{}'", argument)
|
||||
format!("\"{}\"", argument)
|
||||
}
|
||||
|
||||
fn remove_quotes(argument: &str) -> Option<&str> {
|
||||
@ -588,8 +739,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn adds_quotes_to_argument_to_be_passed_in() {
|
||||
assert_eq!(add_quotes("andrés"), "'andrés'");
|
||||
assert_eq!(add_quotes("'andrés'"), "''andrés''");
|
||||
assert_eq!(add_quotes("andrés"), "\"andrés\"");
|
||||
//assert_eq!(add_quotes("\"andrés\""), "\"andrés\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -138,6 +138,14 @@ pub(crate) async fn run_internal_command(
|
||||
}
|
||||
},
|
||||
|
||||
Ok(ReturnSuccess::Value(Value {
|
||||
value: UntaggedValue::Error(err),
|
||||
..
|
||||
})) => {
|
||||
context.error(err);
|
||||
break;
|
||||
}
|
||||
|
||||
Ok(ReturnSuccess::Value(v)) => {
|
||||
yielded = true;
|
||||
yield Ok(v);
|
||||
|
@ -1,19 +1,17 @@
|
||||
use crate::commands::classified::external::run_external_command;
|
||||
use crate::commands::classified::internal::run_internal_command;
|
||||
use crate::context::Context;
|
||||
use crate::stream::{InputStream, OutputStream};
|
||||
use crate::stream::InputStream;
|
||||
use nu_errors::ShellError;
|
||||
use nu_parser::{ClassifiedCommand, ClassifiedPipeline};
|
||||
use nu_protocol::{ReturnSuccess, UntaggedValue, Value};
|
||||
use nu_source::Text;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
pub(crate) async fn run_pipeline(
|
||||
pipeline: ClassifiedPipeline,
|
||||
ctx: &mut Context,
|
||||
mut input: Option<InputStream>,
|
||||
line: &str,
|
||||
) -> Result<(), ShellError> {
|
||||
) -> Result<Option<InputStream>, ShellError> {
|
||||
let mut iter = pipeline.commands.list.into_iter().peekable();
|
||||
|
||||
loop {
|
||||
@ -48,26 +46,5 @@ pub(crate) async fn run_pipeline(
|
||||
};
|
||||
}
|
||||
|
||||
use futures::stream::TryStreamExt;
|
||||
if let Some(input) = input {
|
||||
let mut output_stream: OutputStream = input.into();
|
||||
loop {
|
||||
match output_stream.try_next().await {
|
||||
Ok(Some(ReturnSuccess::Value(Value {
|
||||
value: UntaggedValue::Error(e),
|
||||
..
|
||||
}))) => return Err(e),
|
||||
Ok(Some(_item)) => {
|
||||
if ctx.ctrl_c.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(input)
|
||||
}
|
||||
|
@ -525,9 +525,11 @@ impl Command {
|
||||
match call_info {
|
||||
Ok(call_info) => match command.run(&call_info, ®istry, &raw_args, x) {
|
||||
Ok(o) => o,
|
||||
Err(e) => VecDeque::from(vec![ReturnValue::Err(e)]).to_output_stream(),
|
||||
Err(e) => {
|
||||
futures::stream::iter(vec![ReturnValue::Err(e)]).to_output_stream()
|
||||
}
|
||||
},
|
||||
Err(e) => VecDeque::from(vec![ReturnValue::Err(e)]).to_output_stream(),
|
||||
Err(e) => futures::stream::iter(vec![ReturnValue::Err(e)]).to_output_stream(),
|
||||
}
|
||||
})
|
||||
.flatten();
|
||||
|
@ -31,21 +31,34 @@ impl WholeStreamCommand for Config {
|
||||
"load",
|
||||
SyntaxShape::Path,
|
||||
"load the config from the path give",
|
||||
Some('l'),
|
||||
)
|
||||
.named(
|
||||
"set",
|
||||
SyntaxShape::Any,
|
||||
"set a value in the config, eg) --set [key value]",
|
||||
Some('s'),
|
||||
)
|
||||
.named(
|
||||
"set_into",
|
||||
SyntaxShape::Member,
|
||||
"sets a variable from values in the pipeline",
|
||||
Some('i'),
|
||||
)
|
||||
.named("get", SyntaxShape::Any, "get a value from the config")
|
||||
.named("remove", SyntaxShape::Any, "remove a value from the config")
|
||||
.switch("clear", "clear the config")
|
||||
.switch("path", "return the path to the config file")
|
||||
.named(
|
||||
"get",
|
||||
SyntaxShape::Any,
|
||||
"get a value from the config",
|
||||
Some('g'),
|
||||
)
|
||||
.named(
|
||||
"remove",
|
||||
SyntaxShape::Any,
|
||||
"remove a value from the config",
|
||||
Some('r'),
|
||||
)
|
||||
.switch("clear", "clear the config", Some('c'))
|
||||
.switch("path", "return the path to the config file", Some('p'))
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
|
@ -24,7 +24,11 @@ impl PerItemCommand for Cpy {
|
||||
Signature::build("cp")
|
||||
.required("src", SyntaxShape::Pattern, "the place to copy from")
|
||||
.required("dst", SyntaxShape::Path, "the place to copy to")
|
||||
.switch("recursive", "copy recursively through subdirectories")
|
||||
.switch(
|
||||
"recursive",
|
||||
"copy recursively through subdirectories",
|
||||
Some('r'),
|
||||
)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user