mirror of
https://github.com/nushell/nushell.git
synced 2025-05-05 23:42:56 +00:00
Compare commits
No commits in common. "main" and "0.103.0" have entirely different histories.
40
.github/labeler.yml
vendored
40
.github/labeler.yml
vendored
@ -1,40 +0,0 @@
|
||||
# A bot for automatically labelling pull requests
|
||||
# See https://github.com/actions/labeler
|
||||
|
||||
dataframe:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- crates/nu_plugin_polars/**
|
||||
|
||||
std-library:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- crates/nu-std/**
|
||||
|
||||
ci:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- .github/workflows/**
|
||||
|
||||
|
||||
LSP:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- crates/nu-lsp/**
|
||||
|
||||
parser:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- crates/nu-parser/**
|
||||
|
||||
pr:plugins:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
# plugins API
|
||||
- crates/nu-plugin/**
|
||||
- crates/nu-plugin-core/**
|
||||
- crates/nu-plugin-engine/**
|
||||
- crates/nu-plugin-protocol/**
|
||||
- crates/nu-plugin-test-support/**
|
||||
# specific plugins (like polars)
|
||||
- crates/nu_plugin_*/**
|
19
.github/workflows/labels.yml
vendored
19
.github/workflows/labels.yml
vendored
@ -1,19 +0,0 @@
|
||||
# Automatically labels PRs based on the configuration file
|
||||
# you are probably looking for 👉 `.github/labeler.yml`
|
||||
name: Label PRs
|
||||
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'nushell'
|
||||
steps:
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
sync-labels: true
|
7
.github/workflows/nightly-build.yml
vendored
7
.github/workflows/nightly-build.yml
vendored
@ -8,7 +8,6 @@
|
||||
name: Nightly Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- nightly # Just for test purpose only with the nightly repo
|
||||
@ -40,7 +39,7 @@ jobs:
|
||||
uses: hustcer/setup-nu@v3
|
||||
if: github.repository == 'nushell/nightly'
|
||||
with:
|
||||
version: 0.103.0
|
||||
version: 0.101.0
|
||||
|
||||
# Synchronize the main branch of nightly repo with the main branch of Nushell official repo
|
||||
- name: Prepare for Nightly Release
|
||||
@ -140,7 +139,7 @@ jobs:
|
||||
- name: Setup Nushell
|
||||
uses: hustcer/setup-nu@v3
|
||||
with:
|
||||
version: 0.103.0
|
||||
version: 0.101.0
|
||||
|
||||
- name: Release Nu Binary
|
||||
id: nu
|
||||
@ -198,7 +197,7 @@ jobs:
|
||||
- name: Setup Nushell
|
||||
uses: hustcer/setup-nu@v3
|
||||
with:
|
||||
version: 0.103.0
|
||||
version: 0.101.0
|
||||
|
||||
# Keep the last a few releases
|
||||
- name: Delete Older Releases
|
||||
|
14
.github/workflows/release-pkg.nu
vendored
14
.github/workflows/release-pkg.nu
vendored
@ -117,14 +117,14 @@ if $os in ['macos-latest'] or $USE_UBUNTU {
|
||||
# ----------------------------------------------------------------------------
|
||||
# Build for Windows without static-link-openssl feature
|
||||
# ----------------------------------------------------------------------------
|
||||
if $os =~ 'windows' {
|
||||
if $os in ['windows-latest'] {
|
||||
cargo-build-nu
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Prepare for the release archive
|
||||
# ----------------------------------------------------------------------------
|
||||
let suffix = if $os =~ 'windows' { '.exe' }
|
||||
let suffix = if $os == 'windows-latest' { '.exe' }
|
||||
# nu, nu_plugin_* were all included
|
||||
let executable = $'target/($target)/release/($bin)*($suffix)'
|
||||
print $'Current executable file: ($executable)'
|
||||
@ -148,10 +148,10 @@ For more information, refer to https://www.nushell.sh/book/plugins.html
|
||||
[LICENSE ...(glob $executable)] | each {|it| cp -rv $it $dist } | flatten
|
||||
|
||||
print $'(char nl)Check binary release version detail:'; hr-line
|
||||
let ver = if $os =~ 'windows' {
|
||||
(do -i { .\output\nu.exe -c 'version' }) | default '' | str join
|
||||
let ver = if $os == 'windows-latest' {
|
||||
(do -i { .\output\nu.exe -c 'version' }) | str join
|
||||
} else {
|
||||
(do -i { ./output/nu -c 'version' }) | default '' | str join
|
||||
(do -i { ./output/nu -c 'version' }) | str join
|
||||
}
|
||||
if ($ver | str trim | is-empty) {
|
||||
print $'(ansi r)Incompatible Nu binary: The binary cross compiled is not runnable on current arch...(ansi reset)'
|
||||
@ -177,7 +177,7 @@ if $os in ['macos-latest'] or $USE_UBUNTU {
|
||||
# REF: https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/
|
||||
echo $"archive=($archive)" | save --append $env.GITHUB_OUTPUT
|
||||
|
||||
} else if $os =~ 'windows' {
|
||||
} else if $os == 'windows-latest' {
|
||||
|
||||
let releaseStem = $'($bin)-($version)-($target)'
|
||||
|
||||
@ -221,7 +221,7 @@ if $os in ['macos-latest'] or $USE_UBUNTU {
|
||||
}
|
||||
|
||||
def 'cargo-build-nu' [] {
|
||||
if $os =~ 'windows' {
|
||||
if $os == 'windows-latest' {
|
||||
cargo build --release --all --target $target
|
||||
} else {
|
||||
cargo build --release --all --target $target --features=static-link-openssl
|
||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -89,7 +89,7 @@ jobs:
|
||||
- name: Setup Nushell
|
||||
uses: hustcer/setup-nu@v3
|
||||
with:
|
||||
version: 0.103.0
|
||||
version: 0.101.0
|
||||
|
||||
- name: Release Nu Binary
|
||||
id: nu
|
||||
|
2
.github/workflows/typos.yml
vendored
2
.github/workflows/typos.yml
vendored
@ -10,4 +10,4 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Check spelling
|
||||
uses: crate-ci/typos@v1.31.1
|
||||
uses: crate-ci/typos@v1.29.10
|
||||
|
507
Cargo.lock
generated
507
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
86
Cargo.toml
86
Cargo.toml
@ -10,8 +10,8 @@ homepage = "https://www.nushell.sh"
|
||||
license = "MIT"
|
||||
name = "nu"
|
||||
repository = "https://github.com/nushell/nushell"
|
||||
rust-version = "1.84.1"
|
||||
version = "0.104.1"
|
||||
rust-version = "1.83.0"
|
||||
version = "0.103.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@ -66,12 +66,12 @@ alphanumeric-sort = "1.5"
|
||||
ansi-str = "0.8"
|
||||
anyhow = "1.0.82"
|
||||
base64 = "0.22.1"
|
||||
bracoxide = "0.1.6"
|
||||
bracoxide = "0.1.5"
|
||||
brotli = "7.0"
|
||||
byteorder = "1.5"
|
||||
bytes = "1"
|
||||
bytesize = "1.3.3"
|
||||
calamine = "0.27"
|
||||
bytesize = "1.3.1"
|
||||
calamine = "0.26.1"
|
||||
chardetng = "0.1.17"
|
||||
chrono = { default-features = false, version = "0.4.34" }
|
||||
chrono-humanize = "0.2.3"
|
||||
@ -91,8 +91,8 @@ fancy-regex = "0.14"
|
||||
filesize = "0.2"
|
||||
filetime = "0.2"
|
||||
heck = "0.5.0"
|
||||
human-date-parser = "0.3.0"
|
||||
indexmap = "2.9"
|
||||
human-date-parser = "0.2.0"
|
||||
indexmap = "2.7"
|
||||
indicatif = "0.17"
|
||||
interprocess = "2.2.0"
|
||||
is_executable = "1.0"
|
||||
@ -110,7 +110,7 @@ md5 = { version = "0.10", package = "md-5" }
|
||||
miette = "7.5"
|
||||
mime = "0.3.17"
|
||||
mime_guess = "2.0"
|
||||
mockito = { version = "1.7", default-features = false }
|
||||
mockito = { version = "1.6", default-features = false }
|
||||
multipart-rs = "0.1.13"
|
||||
native-tls = "0.2"
|
||||
nix = { version = "0.29", default-features = false }
|
||||
@ -135,22 +135,22 @@ quick-xml = "0.37.0"
|
||||
quickcheck = "1.0"
|
||||
quickcheck_macros = "1.0"
|
||||
quote = "1.0"
|
||||
rand = "0.9"
|
||||
rand = "0.8"
|
||||
getrandom = "0.2" # pick same version that rand requires
|
||||
rand_chacha = "0.9"
|
||||
rand_chacha = "0.3.1"
|
||||
ratatui = "0.29"
|
||||
rayon = "1.10"
|
||||
reedline = "0.40.0"
|
||||
reedline = "0.39.0"
|
||||
rmp = "0.8"
|
||||
rmp-serde = "1.3"
|
||||
roxmltree = "0.20"
|
||||
rstest = { version = "0.23", default-features = false }
|
||||
rstest_reuse = "0.7"
|
||||
rusqlite = "0.31"
|
||||
rust-embed = "8.7.0"
|
||||
rust-embed = "8.6.0"
|
||||
scopeguard = { version = "1.2.0" }
|
||||
serde = { version = "1.0" }
|
||||
serde_json = "1.0.97"
|
||||
serde_json = "1.0"
|
||||
serde_urlencoded = "0.7.1"
|
||||
serde_yaml = "0.9.33"
|
||||
sha2 = "0.10"
|
||||
@ -161,24 +161,24 @@ syn = "2.0"
|
||||
sysinfo = "0.33"
|
||||
tabled = { version = "0.17.0", default-features = false }
|
||||
tempfile = "3.15"
|
||||
titlecase = "3.5"
|
||||
titlecase = "3.4"
|
||||
toml = "0.8"
|
||||
trash = "5.2"
|
||||
update-informer = { version = "1.2.0", default-features = false, features = ["github", "native-tls", "ureq"] }
|
||||
umask = "2.1"
|
||||
unicode-segmentation = "1.12"
|
||||
unicode-width = "0.2"
|
||||
ureq = { version = "2.12", default-features = false, features = ["socks-proxy"] }
|
||||
ureq = { version = "2.12", default-features = false }
|
||||
url = "2.2"
|
||||
uu_cp = "0.0.30"
|
||||
uu_mkdir = "0.0.30"
|
||||
uu_mktemp = "0.0.30"
|
||||
uu_mv = "0.0.30"
|
||||
uu_touch = "0.0.30"
|
||||
uu_whoami = "0.0.30"
|
||||
uu_uname = "0.0.30"
|
||||
uucore = "0.0.30"
|
||||
uuid = "1.16.0"
|
||||
uu_cp = "0.0.29"
|
||||
uu_mkdir = "0.0.29"
|
||||
uu_mktemp = "0.0.29"
|
||||
uu_mv = "0.0.29"
|
||||
uu_touch = "0.0.29"
|
||||
uu_whoami = "0.0.29"
|
||||
uu_uname = "0.0.29"
|
||||
uucore = "0.0.29"
|
||||
uuid = "1.12.0"
|
||||
v_htmlescape = "0.15.0"
|
||||
wax = "0.6"
|
||||
web-time = "1.1.0"
|
||||
@ -197,22 +197,22 @@ unchecked_duration_subtraction = "warn"
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
nu-cli = { path = "./crates/nu-cli", version = "0.104.1" }
|
||||
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.104.1" }
|
||||
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.104.1" }
|
||||
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.104.1", optional = true }
|
||||
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.104.1" }
|
||||
nu-command = { path = "./crates/nu-command", version = "0.104.1" }
|
||||
nu-engine = { path = "./crates/nu-engine", version = "0.104.1" }
|
||||
nu-explore = { path = "./crates/nu-explore", version = "0.104.1" }
|
||||
nu-lsp = { path = "./crates/nu-lsp/", version = "0.104.1" }
|
||||
nu-parser = { path = "./crates/nu-parser", version = "0.104.1" }
|
||||
nu-path = { path = "./crates/nu-path", version = "0.104.1" }
|
||||
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.104.1" }
|
||||
nu-protocol = { path = "./crates/nu-protocol", version = "0.104.1" }
|
||||
nu-std = { path = "./crates/nu-std", version = "0.104.1" }
|
||||
nu-system = { path = "./crates/nu-system", version = "0.104.1" }
|
||||
nu-utils = { path = "./crates/nu-utils", version = "0.104.1" }
|
||||
nu-cli = { path = "./crates/nu-cli", version = "0.103.0" }
|
||||
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.103.0" }
|
||||
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.103.0" }
|
||||
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.103.0", optional = true }
|
||||
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.103.0" }
|
||||
nu-command = { path = "./crates/nu-command", version = "0.103.0" }
|
||||
nu-engine = { path = "./crates/nu-engine", version = "0.103.0" }
|
||||
nu-explore = { path = "./crates/nu-explore", version = "0.103.0" }
|
||||
nu-lsp = { path = "./crates/nu-lsp/", version = "0.103.0" }
|
||||
nu-parser = { path = "./crates/nu-parser", version = "0.103.0" }
|
||||
nu-path = { path = "./crates/nu-path", version = "0.103.0" }
|
||||
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.103.0" }
|
||||
nu-protocol = { path = "./crates/nu-protocol", version = "0.103.0" }
|
||||
nu-std = { path = "./crates/nu-std", version = "0.103.0" }
|
||||
nu-system = { path = "./crates/nu-system", version = "0.103.0" }
|
||||
nu-utils = { path = "./crates/nu-utils", version = "0.103.0" }
|
||||
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
|
||||
|
||||
crossterm = { workspace = true }
|
||||
@ -241,9 +241,9 @@ nix = { workspace = true, default-features = false, features = [
|
||||
] }
|
||||
|
||||
[dev-dependencies]
|
||||
nu-test-support = { path = "./crates/nu-test-support", version = "0.104.1" }
|
||||
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.104.1" }
|
||||
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.104.1" }
|
||||
nu-test-support = { path = "./crates/nu-test-support", version = "0.103.0" }
|
||||
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.103.0" }
|
||||
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.103.0" }
|
||||
assert_cmd = "2.0"
|
||||
dirs = { workspace = true }
|
||||
tango-bench = "0.6"
|
||||
|
@ -222,7 +222,6 @@ Please submit an issue or PR to be added to this list.
|
||||
- [Dorothy](http://github.com/bevry/dorothy)
|
||||
- [Direnv](https://github.com/direnv/direnv/blob/master/docs/hook.md#nushell)
|
||||
- [x-cmd](https://x-cmd.com/mod/nu)
|
||||
- [vfox](https://github.com/version-fox/vfox)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
@ -5,29 +5,28 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cli"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "nu-cli"
|
||||
version = "0.104.1"
|
||||
version = "0.103.0"
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
|
||||
[dev-dependencies]
|
||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.104.1" }
|
||||
nu-command = { path = "../nu-command", version = "0.104.1" }
|
||||
nu-std = { path = "../nu-std", version = "0.104.1" }
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.104.1" }
|
||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.103.0" }
|
||||
nu-command = { path = "../nu-command", version = "0.103.0" }
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.103.0" }
|
||||
rstest = { workspace = true, default-features = false }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
nu-cmd-base = { path = "../nu-cmd-base", version = "0.104.1" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.104.1", features = ["os"] }
|
||||
nu-glob = { path = "../nu-glob", version = "0.104.1" }
|
||||
nu-path = { path = "../nu-path", version = "0.104.1" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.104.1" }
|
||||
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.104.1", optional = true }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.104.1", features = ["os"] }
|
||||
nu-utils = { path = "../nu-utils", version = "0.104.1" }
|
||||
nu-color-config = { path = "../nu-color-config", version = "0.104.1" }
|
||||
nu-cmd-base = { path = "../nu-cmd-base", version = "0.103.0" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.103.0", features = ["os"] }
|
||||
nu-glob = { path = "../nu-glob", version = "0.103.0" }
|
||||
nu-path = { path = "../nu-path", version = "0.103.0" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.103.0" }
|
||||
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.103.0", optional = true }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.103.0", features = ["os"] }
|
||||
nu-utils = { path = "../nu-utils", version = "0.103.0" }
|
||||
nu-color-config = { path = "../nu-color-config", version = "0.103.0" }
|
||||
nu-ansi-term = { workspace = true }
|
||||
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
|
||||
|
||||
|
@ -105,9 +105,10 @@ impl Command for History {
|
||||
.ok()
|
||||
})
|
||||
.map(move |entries| {
|
||||
entries.into_iter().enumerate().map(move |(idx, entry)| {
|
||||
create_sqlite_history_record(idx, entry, long, head)
|
||||
})
|
||||
entries
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(move |(idx, entry)| create_history_record(idx, entry, long, head))
|
||||
})
|
||||
.ok_or(IoError::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
@ -139,7 +140,7 @@ impl Command for History {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_sqlite_history_record(idx: usize, entry: HistoryItem, long: bool, head: Span) -> Value {
|
||||
fn create_history_record(idx: usize, entry: HistoryItem, long: bool, head: Span) -> Value {
|
||||
//1. Format all the values
|
||||
//2. Create a record of either short or long columns and values
|
||||
|
||||
@ -150,8 +151,11 @@ fn create_sqlite_history_record(idx: usize, entry: HistoryItem, long: bool, head
|
||||
.unwrap_or_default(),
|
||||
head,
|
||||
);
|
||||
let start_timestamp_value = Value::date(
|
||||
entry.start_timestamp.unwrap_or_default().fixed_offset(),
|
||||
let start_timestamp_value = Value::string(
|
||||
entry
|
||||
.start_timestamp
|
||||
.map(|time| time.to_string())
|
||||
.unwrap_or_default(),
|
||||
head,
|
||||
);
|
||||
let command_value = Value::string(entry.command_line, head);
|
||||
|
@ -26,7 +26,7 @@ impl Command for HistoryImport {
|
||||
|
||||
fn extra_description(&self) -> &str {
|
||||
r#"Can import history from input, either successive command lines or more detailed records. If providing records, available fields are:
|
||||
command, start_timestamp, hostname, cwd, duration, exit_status.
|
||||
command_line, id, start_timestamp, hostname, cwd, duration, exit_status.
|
||||
|
||||
If no input is provided, will import all history items from existing history in the other format: if current history is stored in sqlite, it will store it in plain text and vice versa.
|
||||
|
||||
|
@ -27,7 +27,7 @@ impl Completer for AttributeCompletion {
|
||||
let attr_commands =
|
||||
working_set.find_commands_by_predicate(|s| s.starts_with(b"attr "), true);
|
||||
|
||||
for (decl_id, name, desc, ty) in attr_commands {
|
||||
for (name, desc, ty) in attr_commands {
|
||||
let name = name.strip_prefix(b"attr ").unwrap_or(&name);
|
||||
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
@ -41,7 +41,7 @@ impl Completer for AttributeCompletion {
|
||||
},
|
||||
append_whitespace: false,
|
||||
},
|
||||
kind: Some(SuggestionKind::Command(ty, Some(decl_id))),
|
||||
kind: Some(SuggestionKind::Command(ty)),
|
||||
});
|
||||
}
|
||||
|
||||
@ -78,7 +78,7 @@ impl Completer for AttributableCompletion {
|
||||
},
|
||||
append_whitespace: false,
|
||||
},
|
||||
kind: Some(SuggestionKind::Command(cmd.command_type(), None)),
|
||||
kind: Some(SuggestionKind::Command(cmd.command_type())),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::completions::CompletionOptions;
|
||||
use nu_protocol::{
|
||||
engine::{Stack, StateWorkingSet},
|
||||
DeclId, Span,
|
||||
Span,
|
||||
};
|
||||
use reedline::Suggestion;
|
||||
|
||||
@ -28,7 +28,7 @@ pub struct SemanticSuggestion {
|
||||
// TODO: think about name: maybe suggestion context?
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum SuggestionKind {
|
||||
Command(nu_protocol::engine::CommandType, Option<DeclId>),
|
||||
Command(nu_protocol::engine::CommandType),
|
||||
Value(nu_protocol::Type),
|
||||
CellPath,
|
||||
Directory,
|
||||
|
@ -1,5 +1,3 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::completions::{Completer, CompletionOptions, SemanticSuggestion, SuggestionKind};
|
||||
use nu_engine::{column::get_columns, eval_variable};
|
||||
use nu_protocol::{
|
||||
@ -19,14 +17,14 @@ pub struct CellPathCompletion<'a> {
|
||||
|
||||
fn prefix_from_path_member(member: &PathMember, pos: usize) -> (String, Span) {
|
||||
let (prefix_str, start) = match member {
|
||||
PathMember::String { val, span, .. } => (val, span.start),
|
||||
PathMember::Int { val, span, .. } => (&val.to_string(), span.start),
|
||||
PathMember::String { val, span, .. } => (val.clone(), span.start),
|
||||
PathMember::Int { val, span, .. } => (val.to_string(), span.start),
|
||||
};
|
||||
let prefix_str = prefix_str.get(..pos + 1 - start).unwrap_or(prefix_str);
|
||||
// strip wrapping quotes
|
||||
let quotations = ['"', '\'', '`'];
|
||||
let prefix_str = prefix_str.strip_prefix(quotations).unwrap_or(prefix_str);
|
||||
(prefix_str.to_string(), Span::new(start, pos + 1))
|
||||
let prefix_str = prefix_str
|
||||
.get(..pos + 1 - start)
|
||||
.map(str::to_string)
|
||||
.unwrap_or(prefix_str);
|
||||
(prefix_str, Span::new(start, pos + 1))
|
||||
}
|
||||
|
||||
impl Completer for CellPathCompletion<'_> {
|
||||
@ -103,35 +101,21 @@ pub(crate) fn eval_cell_path(
|
||||
} else {
|
||||
eval_constant(working_set, head)
|
||||
}?;
|
||||
head_value
|
||||
.follow_cell_path(path_members, false)
|
||||
.map(Cow::into_owned)
|
||||
head_value.follow_cell_path(path_members, false)
|
||||
}
|
||||
|
||||
fn get_suggestions_by_value(
|
||||
value: &Value,
|
||||
current_span: reedline::Span,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
let to_suggestion = |s: String, v: Option<&Value>| {
|
||||
// Check if the string needs quoting
|
||||
let value = if s.is_empty()
|
||||
|| s.chars()
|
||||
.any(|c: char| !(c.is_ascii_alphabetic() || ['_', '-'].contains(&c)))
|
||||
{
|
||||
format!("{:?}", s)
|
||||
} else {
|
||||
s
|
||||
};
|
||||
|
||||
SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value,
|
||||
span: current_span,
|
||||
description: v.map(|v| v.get_type().to_string()),
|
||||
..Suggestion::default()
|
||||
},
|
||||
kind: Some(SuggestionKind::CellPath),
|
||||
}
|
||||
let to_suggestion = |s: String, v: Option<&Value>| SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value: s,
|
||||
span: current_span,
|
||||
description: v.map(|v| v.get_type().to_string()),
|
||||
..Suggestion::default()
|
||||
},
|
||||
kind: Some(SuggestionKind::CellPath),
|
||||
};
|
||||
match value {
|
||||
Value::Record { val, .. } => val
|
||||
|
@ -75,10 +75,7 @@ impl CommandCompletion {
|
||||
append_whitespace: true,
|
||||
..Default::default()
|
||||
},
|
||||
kind: Some(SuggestionKind::Command(
|
||||
CommandType::External,
|
||||
None,
|
||||
)),
|
||||
kind: Some(SuggestionKind::Command(CommandType::External)),
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -115,7 +112,7 @@ impl Completer for CommandCompletion {
|
||||
},
|
||||
true,
|
||||
);
|
||||
for (decl_id, name, description, typ) in filtered_commands {
|
||||
for (name, description, typ) in filtered_commands {
|
||||
let name = String::from_utf8_lossy(&name);
|
||||
internal_suggs.insert(
|
||||
name.to_string(),
|
||||
@ -127,7 +124,7 @@ impl Completer for CommandCompletion {
|
||||
append_whitespace: true,
|
||||
..Suggestion::default()
|
||||
},
|
||||
kind: Some(SuggestionKind::Command(typ, Some(decl_id))),
|
||||
kind: Some(SuggestionKind::Command(typ)),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -1,20 +1,21 @@
|
||||
use crate::completions::{
|
||||
base::{SemanticSuggestion, SuggestionKind},
|
||||
AttributableCompletion, AttributeCompletion, CellPathCompletion, CommandCompletion, Completer,
|
||||
CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion,
|
||||
ExportableCompletion, FileCompletion, FlagCompletion, OperatorCompletion, VariableCompletion,
|
||||
CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion, FileCompletion,
|
||||
FlagCompletion, OperatorCompletion, VariableCompletion,
|
||||
};
|
||||
use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style};
|
||||
use nu_engine::eval_block;
|
||||
use nu_parser::{flatten_expression, parse, parse_module_file_or_dir};
|
||||
use nu_parser::{flatten_expression, parse};
|
||||
use nu_protocol::{
|
||||
ast::{Argument, Block, Expr, Expression, FindMapResult, ListItem, Traverse},
|
||||
ast::{Argument, Block, Expr, Expression, FindMapResult, Traverse},
|
||||
debugger::WithoutDebug,
|
||||
engine::{Closure, EngineState, Stack, StateWorkingSet},
|
||||
PipelineData, Span, Type, Value,
|
||||
};
|
||||
use reedline::{Completer as ReedlineCompleter, Suggestion};
|
||||
use std::sync::Arc;
|
||||
use std::{str, sync::Arc};
|
||||
|
||||
use super::base::{SemanticSuggestion, SuggestionKind};
|
||||
|
||||
/// Used as the function `f` in find_map Traverse
|
||||
///
|
||||
@ -56,13 +57,8 @@ fn find_pipeline_element_by_position<'a>(
|
||||
Expr::FullCellPath(fcp) => fcp
|
||||
.head
|
||||
.find_map(working_set, &closure)
|
||||
.or(Some(expr))
|
||||
.map(FindMapResult::Found)
|
||||
// e.g. use std/util [<tab>
|
||||
.or_else(|| {
|
||||
(fcp.head.span.contains(pos) && matches!(fcp.head.expr, Expr::List(_)))
|
||||
.then_some(FindMapResult::Continue)
|
||||
})
|
||||
.or(Some(FindMapResult::Found(expr)))
|
||||
.unwrap_or_default(),
|
||||
Expr::Var(_) => FindMapResult::Found(expr),
|
||||
Expr::AttributeBlock(ab) => ab
|
||||
@ -131,18 +127,6 @@ struct Context<'a> {
|
||||
offset: usize,
|
||||
}
|
||||
|
||||
/// For argument completion
|
||||
struct PositionalArguments<'a> {
|
||||
/// command name
|
||||
command_head: &'a str,
|
||||
/// indices of positional arguments
|
||||
positional_arg_indices: Vec<usize>,
|
||||
/// argument list
|
||||
arguments: &'a [Argument],
|
||||
/// expression of current argument
|
||||
expr: &'a Expression,
|
||||
}
|
||||
|
||||
impl Context<'_> {
|
||||
fn new<'a>(
|
||||
working_set: &'a StateWorkingSet,
|
||||
@ -344,8 +328,7 @@ impl NuCompleter {
|
||||
// NOTE: the argument to complete is not necessarily the last one
|
||||
// for lsp completion, we don't trim the text,
|
||||
// so that `def`s after pos can be completed
|
||||
let mut positional_arg_indices = Vec::new();
|
||||
for (arg_idx, arg) in call.arguments.iter().enumerate() {
|
||||
for arg in call.arguments.iter() {
|
||||
let span = arg.span();
|
||||
if span.contains(pos) {
|
||||
// if customized completion specified, it has highest priority
|
||||
@ -395,16 +378,10 @@ impl NuCompleter {
|
||||
Argument::Positional(_) if prefix == b"-" => flag_completion_helper(),
|
||||
// complete according to expression type and command head
|
||||
Argument::Positional(expr) => {
|
||||
let command_head = working_set.get_decl(call.decl_id).name();
|
||||
positional_arg_indices.push(arg_idx);
|
||||
let command_head = working_set.get_span_contents(call.head);
|
||||
self.argument_completion_helper(
|
||||
PositionalArguments {
|
||||
command_head,
|
||||
positional_arg_indices,
|
||||
arguments: &call.arguments,
|
||||
expr,
|
||||
},
|
||||
pos,
|
||||
command_head,
|
||||
expr,
|
||||
&ctx,
|
||||
suggestions.is_empty(),
|
||||
)
|
||||
@ -412,8 +389,6 @@ impl NuCompleter {
|
||||
_ => vec![],
|
||||
});
|
||||
break;
|
||||
} else if !matches!(arg, Argument::Named(_)) {
|
||||
positional_arg_indices.push(arg_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -511,10 +486,9 @@ impl NuCompleter {
|
||||
externals: bool,
|
||||
strip: bool,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
let config = self.engine_state.get_config();
|
||||
let mut command_completions = CommandCompletion {
|
||||
internals,
|
||||
externals: !internals || (externals && config.completions.external.enable),
|
||||
externals,
|
||||
};
|
||||
let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
|
||||
let ctx = Context::new(working_set, new_span, prefix, offset);
|
||||
@ -523,97 +497,20 @@ impl NuCompleter {
|
||||
|
||||
fn argument_completion_helper(
|
||||
&self,
|
||||
argument_info: PositionalArguments,
|
||||
pos: usize,
|
||||
command_head: &[u8],
|
||||
expr: &Expression,
|
||||
ctx: &Context,
|
||||
need_fallback: bool,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
let PositionalArguments {
|
||||
command_head,
|
||||
positional_arg_indices,
|
||||
arguments,
|
||||
expr,
|
||||
} = argument_info;
|
||||
// special commands
|
||||
match command_head {
|
||||
// complete module file/directory
|
||||
"use" | "export use" | "overlay use" | "source-env"
|
||||
if positional_arg_indices.len() == 1 =>
|
||||
{
|
||||
return self.process_completion(
|
||||
&mut DotNuCompletion {
|
||||
std_virtual_path: command_head != "source-env",
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
// NOTE: if module file already specified,
|
||||
// TODO: if module file already specified,
|
||||
// should parse it to get modules/commands/consts to complete
|
||||
"use" | "export use" => {
|
||||
let Some(Argument::Positional(Expression {
|
||||
expr: Expr::String(module_name),
|
||||
span,
|
||||
..
|
||||
})) = positional_arg_indices
|
||||
.first()
|
||||
.and_then(|i| arguments.get(*i))
|
||||
else {
|
||||
return vec![];
|
||||
};
|
||||
let module_name = module_name.as_bytes();
|
||||
let (module_id, temp_working_set) = match ctx.working_set.find_module(module_name) {
|
||||
Some(module_id) => (module_id, None),
|
||||
None => {
|
||||
let mut temp_working_set =
|
||||
StateWorkingSet::new(ctx.working_set.permanent_state);
|
||||
let Some(module_id) = parse_module_file_or_dir(
|
||||
&mut temp_working_set,
|
||||
module_name,
|
||||
*span,
|
||||
None,
|
||||
) else {
|
||||
return vec![];
|
||||
};
|
||||
(module_id, Some(temp_working_set))
|
||||
}
|
||||
};
|
||||
let mut exportable_completion = ExportableCompletion {
|
||||
module_id,
|
||||
temp_working_set,
|
||||
};
|
||||
let mut complete_on_list_items = |items: &[ListItem]| -> Vec<SemanticSuggestion> {
|
||||
for item in items {
|
||||
let span = item.expr().span;
|
||||
if span.contains(pos) {
|
||||
let offset = span.start.saturating_sub(ctx.span.start);
|
||||
let end_offset =
|
||||
ctx.prefix.len().min(pos.min(span.end) - ctx.span.start + 1);
|
||||
let new_ctx = Context::new(
|
||||
ctx.working_set,
|
||||
Span::new(span.start, ctx.span.end.min(span.end)),
|
||||
ctx.prefix.get(offset..end_offset).unwrap_or_default(),
|
||||
ctx.offset,
|
||||
);
|
||||
return self.process_completion(&mut exportable_completion, &new_ctx);
|
||||
}
|
||||
}
|
||||
vec![]
|
||||
};
|
||||
|
||||
match &expr.expr {
|
||||
Expr::String(_) => {
|
||||
return self.process_completion(&mut exportable_completion, ctx);
|
||||
}
|
||||
Expr::FullCellPath(fcp) => match &fcp.head.expr {
|
||||
Expr::List(items) => {
|
||||
return complete_on_list_items(items);
|
||||
}
|
||||
_ => return vec![],
|
||||
},
|
||||
_ => return vec![],
|
||||
}
|
||||
b"use" | b"export use" | b"overlay use" | b"source-env" => {
|
||||
return self.process_completion(&mut DotNuCompletion, ctx);
|
||||
}
|
||||
"which" => {
|
||||
b"which" => {
|
||||
let mut completer = CommandCompletion {
|
||||
internals: true,
|
||||
externals: true,
|
||||
@ -646,6 +543,7 @@ impl NuCompleter {
|
||||
case_sensitive: config.completions.case_sensitive,
|
||||
match_algorithm: config.completions.algorithm.into(),
|
||||
sort: config.completions.sort,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
completer.fetch(
|
||||
|
@ -22,22 +22,18 @@ pub struct PathBuiltFromString {
|
||||
/// Recursively goes through paths that match a given `partial`.
|
||||
/// built: State struct for a valid matching path built so far.
|
||||
///
|
||||
/// `want_directory`: Whether we want only directories as completion matches.
|
||||
/// Some commands like `cd` can only be run on directories whereas others
|
||||
/// like `ls` can be run on regular files as well.
|
||||
///
|
||||
/// `isdir`: whether the current partial path has a trailing slash.
|
||||
/// Parsing a path string into a pathbuf loses that bit of information.
|
||||
///
|
||||
/// `enable_exact_match`: Whether match algorithm is Prefix and all previous components
|
||||
/// of the path matched a directory exactly.
|
||||
/// want_directory: Whether we want only directories as completion matches.
|
||||
/// Some commands like `cd` can only be run on directories whereas others
|
||||
/// like `ls` can be run on regular files as well.
|
||||
fn complete_rec(
|
||||
partial: &[&str],
|
||||
built_paths: &[PathBuiltFromString],
|
||||
options: &CompletionOptions,
|
||||
want_directory: bool,
|
||||
isdir: bool,
|
||||
enable_exact_match: bool,
|
||||
) -> Vec<PathBuiltFromString> {
|
||||
if let Some((&base, rest)) = partial.split_first() {
|
||||
if base.chars().all(|c| c == '.') && (isdir || !rest.is_empty()) {
|
||||
@ -50,14 +46,7 @@ fn complete_rec(
|
||||
built
|
||||
})
|
||||
.collect();
|
||||
return complete_rec(
|
||||
rest,
|
||||
&built_paths,
|
||||
options,
|
||||
want_directory,
|
||||
isdir,
|
||||
enable_exact_match,
|
||||
);
|
||||
return complete_rec(rest, &built_paths, options, want_directory, isdir);
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,26 +86,27 @@ fn complete_rec(
|
||||
// Serves as confirmation to ignore longer completions for
|
||||
// components in between.
|
||||
if !rest.is_empty() || isdir {
|
||||
// Don't show longer completions if we have an exact match (#13204, #14794)
|
||||
let exact_match = enable_exact_match
|
||||
&& (if options.case_sensitive {
|
||||
entry_name.eq(base)
|
||||
} else {
|
||||
entry_name.eq_ignore_case(base)
|
||||
});
|
||||
completions.extend(complete_rec(
|
||||
rest,
|
||||
&[built],
|
||||
options,
|
||||
want_directory,
|
||||
isdir,
|
||||
exact_match,
|
||||
));
|
||||
} else {
|
||||
completions.push(built);
|
||||
}
|
||||
|
||||
// For https://github.com/nushell/nushell/issues/13204
|
||||
if isdir && options.match_algorithm == MatchAlgorithm::Prefix {
|
||||
let exact_match = if options.case_sensitive {
|
||||
entry_name.eq(base)
|
||||
} else {
|
||||
entry_name.to_folded_case().eq(&base.to_folded_case())
|
||||
};
|
||||
if exact_match {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
completions.push(built);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
@ -150,7 +140,7 @@ impl OriginalCwd {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn surround_remove(partial: &str) -> String {
|
||||
fn surround_remove(partial: &str) -> String {
|
||||
for c in ['`', '"', '\''] {
|
||||
if partial.starts_with(c) {
|
||||
let ret = partial.strip_prefix(c).unwrap_or(partial);
|
||||
@ -209,9 +199,10 @@ pub fn complete_item(
|
||||
let ls_colors = (engine_state.config.completions.use_ls_colors
|
||||
&& engine_state.config.use_ansi_coloring.get(engine_state))
|
||||
.then(|| {
|
||||
let ls_colors_env_str = stack
|
||||
.get_env_var(engine_state, "LS_COLORS")
|
||||
.and_then(|v| env_to_string("LS_COLORS", v, engine_state, stack).ok());
|
||||
let ls_colors_env_str = match stack.get_env_var(engine_state, "LS_COLORS") {
|
||||
Some(v) => env_to_string("LS_COLORS", v, engine_state, stack).ok(),
|
||||
None => None,
|
||||
};
|
||||
get_ls_colors(ls_colors_env_str)
|
||||
});
|
||||
|
||||
@ -265,7 +256,6 @@ pub fn complete_item(
|
||||
options,
|
||||
want_directory,
|
||||
isdir,
|
||||
options.match_algorithm == MatchAlgorithm::Prefix,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|mut p| {
|
||||
@ -274,12 +264,15 @@ pub fn complete_item(
|
||||
}
|
||||
let is_dir = p.isdir;
|
||||
let path = original_cwd.apply(p, path_separator);
|
||||
let real_path = expand_to_real_path(&path);
|
||||
let metadata = std::fs::symlink_metadata(&real_path).ok();
|
||||
let style = ls_colors.as_ref().map(|lsc| {
|
||||
lsc.style_for_path_with_metadata(&real_path, metadata.as_ref())
|
||||
.map(lscolors::Style::to_nu_ansi_term_style)
|
||||
.unwrap_or_default()
|
||||
lsc.style_for_path_with_metadata(
|
||||
&path,
|
||||
std::fs::symlink_metadata(expand_to_real_path(&path))
|
||||
.ok()
|
||||
.as_ref(),
|
||||
)
|
||||
.map(lscolors::Style::to_nu_ansi_term_style)
|
||||
.unwrap_or_default()
|
||||
});
|
||||
FileSuggestion {
|
||||
span,
|
||||
|
@ -18,12 +18,6 @@ pub enum MatchAlgorithm {
|
||||
/// "git switch" is matched by "git sw"
|
||||
Prefix,
|
||||
|
||||
/// Only show suggestions which have a substring matching with the given input
|
||||
///
|
||||
/// Example:
|
||||
/// "git checkout" is matched by "checkout"
|
||||
Substring,
|
||||
|
||||
/// Only show suggestions which contain the input chars at any place
|
||||
///
|
||||
/// Example:
|
||||
@ -42,10 +36,6 @@ enum State<T> {
|
||||
/// Holds (haystack, item)
|
||||
items: Vec<(String, T)>,
|
||||
},
|
||||
Substring {
|
||||
/// Holds (haystack, item)
|
||||
items: Vec<(String, T)>,
|
||||
},
|
||||
Fuzzy {
|
||||
matcher: Matcher,
|
||||
atom: Atom,
|
||||
@ -74,18 +64,6 @@ impl<T> NuMatcher<'_, T> {
|
||||
state: State::Prefix { items: Vec::new() },
|
||||
}
|
||||
}
|
||||
MatchAlgorithm::Substring => {
|
||||
let lowercase_needle = if options.case_sensitive {
|
||||
needle.to_owned()
|
||||
} else {
|
||||
needle.to_folded_case()
|
||||
};
|
||||
NuMatcher {
|
||||
options,
|
||||
needle: lowercase_needle,
|
||||
state: State::Substring { items: Vec::new() },
|
||||
}
|
||||
}
|
||||
MatchAlgorithm::Fuzzy => {
|
||||
let atom = Atom::new(
|
||||
needle,
|
||||
@ -124,21 +102,11 @@ impl<T> NuMatcher<'_, T> {
|
||||
} else {
|
||||
Cow::Owned(haystack.to_folded_case())
|
||||
};
|
||||
let matches = haystack_folded.starts_with(self.needle.as_str());
|
||||
if matches {
|
||||
if let Some(item) = item {
|
||||
items.push((haystack.to_string(), item));
|
||||
}
|
||||
}
|
||||
matches
|
||||
}
|
||||
State::Substring { items } => {
|
||||
let haystack_folded = if self.options.case_sensitive {
|
||||
Cow::Borrowed(haystack)
|
||||
let matches = if self.options.positional {
|
||||
haystack_folded.starts_with(self.needle.as_str())
|
||||
} else {
|
||||
Cow::Owned(haystack.to_folded_case())
|
||||
haystack_folded.contains(self.needle.as_str())
|
||||
};
|
||||
let matches = haystack_folded.contains(self.needle.as_str());
|
||||
if matches {
|
||||
if let Some(item) = item {
|
||||
items.push((haystack.to_string(), item));
|
||||
@ -180,7 +148,7 @@ impl<T> NuMatcher<'_, T> {
|
||||
/// Get all the items that matched (sorted)
|
||||
pub fn results(self) -> Vec<T> {
|
||||
match self.state {
|
||||
State::Prefix { mut items, .. } | State::Substring { mut items, .. } => {
|
||||
State::Prefix { mut items, .. } => {
|
||||
items.sort_by(|(haystack1, _), (haystack2, _)| {
|
||||
let cmp_sensitive = haystack1.cmp(haystack2);
|
||||
if self.options.case_sensitive {
|
||||
@ -227,7 +195,6 @@ impl From<CompletionAlgorithm> for MatchAlgorithm {
|
||||
fn from(value: CompletionAlgorithm) -> Self {
|
||||
match value {
|
||||
CompletionAlgorithm::Prefix => MatchAlgorithm::Prefix,
|
||||
CompletionAlgorithm::Substring => MatchAlgorithm::Substring,
|
||||
CompletionAlgorithm::Fuzzy => MatchAlgorithm::Fuzzy,
|
||||
}
|
||||
}
|
||||
@ -239,7 +206,6 @@ impl TryFrom<String> for MatchAlgorithm {
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
match value.as_str() {
|
||||
"prefix" => Ok(Self::Prefix),
|
||||
"substring" => Ok(Self::Substring),
|
||||
"fuzzy" => Ok(Self::Fuzzy),
|
||||
_ => Err(InvalidMatchAlgorithm::Unknown),
|
||||
}
|
||||
@ -264,6 +230,7 @@ impl std::error::Error for InvalidMatchAlgorithm {}
|
||||
#[derive(Clone)]
|
||||
pub struct CompletionOptions {
|
||||
pub case_sensitive: bool,
|
||||
pub positional: bool,
|
||||
pub match_algorithm: MatchAlgorithm,
|
||||
pub sort: CompletionSort,
|
||||
}
|
||||
@ -272,6 +239,7 @@ impl Default for CompletionOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
case_sensitive: true,
|
||||
positional: true,
|
||||
match_algorithm: MatchAlgorithm::Prefix,
|
||||
sort: Default::default(),
|
||||
}
|
||||
@ -288,9 +256,6 @@ mod test {
|
||||
#[case(MatchAlgorithm::Prefix, "example text", "", true)]
|
||||
#[case(MatchAlgorithm::Prefix, "example text", "examp", true)]
|
||||
#[case(MatchAlgorithm::Prefix, "example text", "text", false)]
|
||||
#[case(MatchAlgorithm::Substring, "example text", "", true)]
|
||||
#[case(MatchAlgorithm::Substring, "example text", "text", true)]
|
||||
#[case(MatchAlgorithm::Substring, "example text", "mplxt", false)]
|
||||
#[case(MatchAlgorithm::Fuzzy, "example text", "", true)]
|
||||
#[case(MatchAlgorithm::Fuzzy, "example text", "examp", true)]
|
||||
#[case(MatchAlgorithm::Fuzzy, "example text", "ext", true)]
|
||||
|
@ -1,12 +1,11 @@
|
||||
use crate::completions::{
|
||||
completer::map_value_completions, Completer, CompletionOptions, MatchAlgorithm,
|
||||
SemanticSuggestion,
|
||||
completer::map_value_completions, Completer, CompletionOptions, SemanticSuggestion,
|
||||
};
|
||||
use nu_engine::eval_call;
|
||||
use nu_protocol::{
|
||||
ast::{Argument, Call, Expr, Expression},
|
||||
debugger::WithoutDebug,
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
engine::{Stack, StateWorkingSet},
|
||||
DeclId, PipelineData, Span, Type, Value,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
@ -43,37 +42,28 @@ impl<T: Completer> Completer for CustomCompletion<T> {
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
// Call custom declaration
|
||||
let mut stack_mut = stack.clone();
|
||||
let mut eval = |engine_state: &EngineState| {
|
||||
eval_call::<WithoutDebug>(
|
||||
engine_state,
|
||||
&mut stack_mut,
|
||||
&Call {
|
||||
decl_id: self.decl_id,
|
||||
head: span,
|
||||
arguments: vec![
|
||||
Argument::Positional(Expression::new_unknown(
|
||||
Expr::String(self.line.clone()),
|
||||
Span::unknown(),
|
||||
Type::String,
|
||||
)),
|
||||
Argument::Positional(Expression::new_unknown(
|
||||
Expr::Int(self.line_pos as i64),
|
||||
Span::unknown(),
|
||||
Type::Int,
|
||||
)),
|
||||
],
|
||||
parser_info: HashMap::new(),
|
||||
},
|
||||
PipelineData::empty(),
|
||||
)
|
||||
};
|
||||
let result = if self.decl_id.get() < working_set.permanent_state.num_decls() {
|
||||
eval(working_set.permanent_state)
|
||||
} else {
|
||||
let mut engine_state = working_set.permanent_state.clone();
|
||||
let _ = engine_state.merge_delta(working_set.delta.clone());
|
||||
eval(&engine_state)
|
||||
};
|
||||
let result = eval_call::<WithoutDebug>(
|
||||
working_set.permanent_state,
|
||||
&mut stack_mut,
|
||||
&Call {
|
||||
decl_id: self.decl_id,
|
||||
head: span,
|
||||
arguments: vec![
|
||||
Argument::Positional(Expression::new_unknown(
|
||||
Expr::String(self.line.clone()),
|
||||
Span::unknown(),
|
||||
Type::String,
|
||||
)),
|
||||
Argument::Positional(Expression::new_unknown(
|
||||
Expr::Int(self.line_pos as i64),
|
||||
Span::unknown(),
|
||||
Type::Int,
|
||||
)),
|
||||
],
|
||||
parser_info: HashMap::new(),
|
||||
},
|
||||
PipelineData::empty(),
|
||||
);
|
||||
|
||||
let mut completion_options = orig_options.clone();
|
||||
let mut should_sort = true;
|
||||
@ -103,10 +93,10 @@ impl<T: Completer> Completer for CustomCompletion<T> {
|
||||
{
|
||||
completion_options.case_sensitive = case_sensitive;
|
||||
}
|
||||
let positional =
|
||||
options.get("positional").and_then(|val| val.as_bool().ok());
|
||||
if positional.is_some() {
|
||||
log::warn!("Use of the positional option is deprecated. Use the substring match algorithm instead.");
|
||||
if let Some(positional) =
|
||||
options.get("positional").and_then(|val| val.as_bool().ok())
|
||||
{
|
||||
completion_options.positional = positional;
|
||||
}
|
||||
if let Some(algorithm) = options
|
||||
.get("completion_algorithm")
|
||||
@ -114,11 +104,6 @@ impl<T: Completer> Completer for CustomCompletion<T> {
|
||||
.and_then(|option| option.try_into().ok())
|
||||
{
|
||||
completion_options.match_algorithm = algorithm;
|
||||
if let Some(false) = positional {
|
||||
if completion_options.match_algorithm == MatchAlgorithm::Prefix {
|
||||
completion_options.match_algorithm = MatchAlgorithm::Substring
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,23 +1,18 @@
|
||||
use crate::completions::{
|
||||
completion_common::{surround_remove, FileSuggestion},
|
||||
completion_options::NuMatcher,
|
||||
file_path_completion, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind,
|
||||
};
|
||||
use crate::completions::{file_path_completion, Completer, CompletionOptions};
|
||||
use nu_path::expand_tilde;
|
||||
use nu_protocol::{
|
||||
engine::{Stack, StateWorkingSet, VirtualPath},
|
||||
engine::{Stack, StateWorkingSet},
|
||||
Span,
|
||||
};
|
||||
use reedline::Suggestion;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
path::{is_separator, PathBuf, MAIN_SEPARATOR_STR},
|
||||
path::{is_separator, PathBuf, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR},
|
||||
};
|
||||
|
||||
pub struct DotNuCompletion {
|
||||
/// e.g. use std/a<tab>
|
||||
pub std_virtual_path: bool,
|
||||
}
|
||||
use super::{SemanticSuggestion, SuggestionKind};
|
||||
|
||||
pub struct DotNuCompletion;
|
||||
|
||||
impl Completer for DotNuCompletion {
|
||||
fn fetch(
|
||||
@ -107,7 +102,7 @@ impl Completer for DotNuCompletion {
|
||||
|
||||
// Fetch the files filtering the ones that ends with .nu
|
||||
// and transform them into suggestions
|
||||
let mut completions = file_path_completion(
|
||||
let completions = file_path_completion(
|
||||
span,
|
||||
partial,
|
||||
&search_dirs
|
||||
@ -118,60 +113,17 @@ impl Completer for DotNuCompletion {
|
||||
working_set.permanent_state,
|
||||
stack,
|
||||
);
|
||||
|
||||
if self.std_virtual_path {
|
||||
let mut matcher = NuMatcher::new(partial, options);
|
||||
let base_dir = surround_remove(&base_dir);
|
||||
if base_dir == "." {
|
||||
let surround_prefix = partial
|
||||
.chars()
|
||||
.take_while(|c| "`'\"".contains(*c))
|
||||
.collect::<String>();
|
||||
for path in ["std", "std-rfc"] {
|
||||
let path = format!("{}{}", surround_prefix, path);
|
||||
matcher.add(
|
||||
path.clone(),
|
||||
FileSuggestion {
|
||||
span,
|
||||
path,
|
||||
style: None,
|
||||
is_dir: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
} else if let Some(VirtualPath::Dir(sub_paths)) =
|
||||
working_set.find_virtual_path(&base_dir)
|
||||
{
|
||||
for sub_vp_id in sub_paths {
|
||||
let (path, sub_vp) = working_set.get_virtual_path(*sub_vp_id);
|
||||
let path = path
|
||||
.strip_prefix(&format!("{}/", base_dir))
|
||||
.unwrap_or(path)
|
||||
.to_string();
|
||||
matcher.add(
|
||||
path.clone(),
|
||||
FileSuggestion {
|
||||
path,
|
||||
span,
|
||||
style: None,
|
||||
is_dir: matches!(sub_vp, VirtualPath::Dir(_)),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
completions.extend(matcher.results());
|
||||
}
|
||||
|
||||
completions
|
||||
.into_iter()
|
||||
// Different base dir, so we list the .nu files or folders
|
||||
.filter(|it| {
|
||||
// for paths with spaces in them
|
||||
let path = it.path.trim_end_matches('`');
|
||||
path.ends_with(".nu") || it.is_dir
|
||||
path.ends_with(".nu") || path.ends_with(SEP)
|
||||
})
|
||||
.map(|x| {
|
||||
let append_whitespace = !x.is_dir && (!start_with_backquote || end_with_backquote);
|
||||
let append_whitespace =
|
||||
x.path.ends_with(".nu") && (!start_with_backquote || end_with_backquote);
|
||||
// Re-calculate the span to replace
|
||||
let mut span_offset = 0;
|
||||
let mut value = x.path.to_string();
|
||||
|
@ -1,112 +0,0 @@
|
||||
use crate::completions::{
|
||||
completion_common::surround_remove, completion_options::NuMatcher, Completer,
|
||||
CompletionOptions, SemanticSuggestion, SuggestionKind,
|
||||
};
|
||||
use nu_protocol::{
|
||||
engine::{Stack, StateWorkingSet},
|
||||
ModuleId, Span,
|
||||
};
|
||||
use reedline::Suggestion;
|
||||
|
||||
pub struct ExportableCompletion<'a> {
|
||||
pub module_id: ModuleId,
|
||||
pub temp_working_set: Option<StateWorkingSet<'a>>,
|
||||
}
|
||||
|
||||
/// If name contains space, wrap it in quotes
|
||||
fn wrapped_name(name: String) -> String {
|
||||
if !name.contains(' ') {
|
||||
return name;
|
||||
}
|
||||
if name.contains('\'') {
|
||||
format!("\"{}\"", name.replace('"', r#"\""#))
|
||||
} else {
|
||||
format!("'{name}'")
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for ExportableCompletion<'_> {
|
||||
fn fetch(
|
||||
&mut self,
|
||||
working_set: &StateWorkingSet,
|
||||
_stack: &Stack,
|
||||
prefix: impl AsRef<str>,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
options: &CompletionOptions,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
let mut matcher = NuMatcher::<()>::new(surround_remove(prefix.as_ref()), options);
|
||||
let mut results = Vec::new();
|
||||
let span = reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
};
|
||||
// TODO: use matcher.add_lazy to lazy evaluate an item if it matches the prefix
|
||||
let mut add_suggestion = |value: String,
|
||||
description: Option<String>,
|
||||
extra: Option<Vec<String>>,
|
||||
kind: SuggestionKind| {
|
||||
results.push(SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value,
|
||||
span,
|
||||
description,
|
||||
extra,
|
||||
..Suggestion::default()
|
||||
},
|
||||
kind: Some(kind),
|
||||
});
|
||||
};
|
||||
|
||||
let working_set = self.temp_working_set.as_ref().unwrap_or(working_set);
|
||||
let module = working_set.get_module(self.module_id);
|
||||
|
||||
for (name, decl_id) in &module.decls {
|
||||
let name = String::from_utf8_lossy(name).to_string();
|
||||
if matcher.matches(&name) {
|
||||
let cmd = working_set.get_decl(*decl_id);
|
||||
add_suggestion(
|
||||
wrapped_name(name),
|
||||
Some(cmd.description().to_string()),
|
||||
None,
|
||||
// `None` here avoids arguments being expanded by snippet edit style for lsp
|
||||
SuggestionKind::Command(cmd.command_type(), None),
|
||||
);
|
||||
}
|
||||
}
|
||||
for (name, module_id) in &module.submodules {
|
||||
let name = String::from_utf8_lossy(name).to_string();
|
||||
if matcher.matches(&name) {
|
||||
let comments = working_set.get_module_comments(*module_id).map(|spans| {
|
||||
spans
|
||||
.iter()
|
||||
.map(|sp| {
|
||||
String::from_utf8_lossy(working_set.get_span_contents(*sp)).into()
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
});
|
||||
add_suggestion(
|
||||
wrapped_name(name),
|
||||
Some("Submodule".into()),
|
||||
comments,
|
||||
SuggestionKind::Module,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (name, var_id) in &module.constants {
|
||||
let name = String::from_utf8_lossy(name).to_string();
|
||||
if matcher.matches(&name) {
|
||||
let var = working_set.get_variable(*var_id);
|
||||
add_suggestion(
|
||||
wrapped_name(name),
|
||||
var.const_val
|
||||
.as_ref()
|
||||
.and_then(|v| v.clone().coerce_into_string().ok()),
|
||||
None,
|
||||
SuggestionKind::Variable,
|
||||
);
|
||||
}
|
||||
}
|
||||
results
|
||||
}
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
use crate::completions::{
|
||||
completion_options::NuMatcher, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind,
|
||||
};
|
||||
use crate::completions::{completion_options::NuMatcher, Completer, CompletionOptions};
|
||||
use nu_protocol::{
|
||||
engine::{Stack, StateWorkingSet},
|
||||
DeclId, Span,
|
||||
};
|
||||
use reedline::Suggestion;
|
||||
|
||||
use super::{SemanticSuggestion, SuggestionKind};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FlagCompletion {
|
||||
pub decl_id: DeclId,
|
||||
|
@ -8,7 +8,6 @@ mod completion_options;
|
||||
mod custom_completions;
|
||||
mod directory_completions;
|
||||
mod dotnu_completions;
|
||||
mod exportable_completions;
|
||||
mod file_completions;
|
||||
mod flag_completions;
|
||||
mod operator_completions;
|
||||
@ -23,7 +22,6 @@ pub use completion_options::{CompletionOptions, MatchAlgorithm};
|
||||
pub use custom_completions::CustomCompletion;
|
||||
pub use directory_completions::DirectoryCompletion;
|
||||
pub use dotnu_completions::DotNuCompletion;
|
||||
pub use exportable_completions::ExportableCompletion;
|
||||
pub use file_completions::{file_path_completion, FileCompletion};
|
||||
pub use flag_completions::FlagCompletion;
|
||||
pub use operator_completions::OperatorCompletion;
|
||||
|
@ -864,7 +864,7 @@ fn do_auto_cd(
|
||||
path.to_string_lossy().to_string()
|
||||
};
|
||||
|
||||
if let PermissionResult::PermissionDenied = have_permission(path.clone()) {
|
||||
if let PermissionResult::PermissionDenied(_) = have_permission(path.clone()) {
|
||||
report_shell_error(
|
||||
engine_state,
|
||||
&ShellError::Io(IoError::new_with_additional_context(
|
||||
|
@ -10,8 +10,7 @@ use nu_cli::NuCompleter;
|
||||
use nu_engine::eval_block;
|
||||
use nu_parser::parse;
|
||||
use nu_path::expand_tilde;
|
||||
use nu_protocol::{debugger::WithoutDebug, engine::StateWorkingSet, Config, PipelineData};
|
||||
use nu_std::load_standard_library;
|
||||
use nu_protocol::{debugger::WithoutDebug, engine::StateWorkingSet, PipelineData};
|
||||
use reedline::{Completer, Suggestion};
|
||||
use rstest::{fixture, rstest};
|
||||
use support::{
|
||||
@ -228,12 +227,14 @@ fn customcompletions_override_options() {
|
||||
let mut completer = custom_completer_with_options(
|
||||
r#"$env.config.completions.algorithm = "fuzzy"
|
||||
$env.config.completions.case_sensitive = false"#,
|
||||
r#"completion_algorithm: "substring",
|
||||
r#"completion_algorithm: "prefix",
|
||||
positional: false,
|
||||
case_sensitive: true,
|
||||
sort: true"#,
|
||||
&["Foo Abcdef", "Abcdef", "Acd Bar"],
|
||||
);
|
||||
|
||||
// positional: false should make it do substring matching
|
||||
// sort: true should force sorting
|
||||
let expected: Vec<_> = vec!["Abcdef", "Foo Abcdef"];
|
||||
let suggestions = completer.complete("my-command Abcd", 15);
|
||||
@ -349,24 +350,9 @@ fn custom_arguments_vs_subcommands() {
|
||||
match_suggestions(&expected, &suggestions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_completions_defined_inline() {
|
||||
let (_, _, engine, stack) = new_engine();
|
||||
|
||||
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||
let completion_str = "def animals [] { [cat dog] }
|
||||
export def say [
|
||||
animal: string@animals
|
||||
] { }; say ";
|
||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||
// including only subcommand completions
|
||||
let expected: Vec<_> = vec!["cat", "dog"];
|
||||
match_suggestions(&expected, &suggestions);
|
||||
}
|
||||
|
||||
/// External command only if starts with `^`
|
||||
#[test]
|
||||
fn external_commands() {
|
||||
fn external_commands_only() {
|
||||
let engine = new_external_engine();
|
||||
let mut completer = NuCompleter::new(
|
||||
Arc::new(engine),
|
||||
@ -389,31 +375,6 @@ fn external_commands() {
|
||||
match_suggestions(&expected, &suggestions);
|
||||
}
|
||||
|
||||
/// Disable external commands except for those start with `^`
|
||||
#[test]
|
||||
fn external_commands_disabled() {
|
||||
let mut engine = new_external_engine();
|
||||
|
||||
let mut config = Config::default();
|
||||
config.completions.external.enable = false;
|
||||
engine.set_config(config);
|
||||
|
||||
let stack = nu_protocol::engine::Stack::new();
|
||||
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||
let completion_str = "ls; ^sleep";
|
||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||
#[cfg(windows)]
|
||||
let expected: Vec<_> = vec!["sleep.exe"];
|
||||
#[cfg(not(windows))]
|
||||
let expected: Vec<_> = vec!["sleep"];
|
||||
match_suggestions(&expected, &suggestions);
|
||||
|
||||
let completion_str = "sleep";
|
||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||
let expected: Vec<_> = vec!["sleep"];
|
||||
match_suggestions(&expected, &suggestions);
|
||||
}
|
||||
|
||||
/// Which completes both internals and externals
|
||||
#[test]
|
||||
fn which_command_completions() {
|
||||
@ -512,7 +473,7 @@ fn dotnu_completions() {
|
||||
|
||||
match_suggestions(&vec!["sub.nu`"], &suggestions);
|
||||
|
||||
let mut expected = vec![
|
||||
let expected = vec![
|
||||
"asdf.nu",
|
||||
"bar.nu",
|
||||
"bat.nu",
|
||||
@ -545,8 +506,6 @@ fn dotnu_completions() {
|
||||
match_suggestions(&expected, &suggestions);
|
||||
|
||||
// Test use completion
|
||||
expected.push("std");
|
||||
expected.push("std-rfc");
|
||||
let completion_str = "use ";
|
||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||
|
||||
@ -578,66 +537,6 @@ fn dotnu_completions() {
|
||||
match_dir_content_for_dotnu(dir_content, &suggestions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dotnu_stdlib_completions() {
|
||||
let (_, _, mut engine, stack) = new_dotnu_engine();
|
||||
assert!(load_standard_library(&mut engine).is_ok());
|
||||
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||
|
||||
// `export use` should be recognized as command `export use`
|
||||
let completion_str = "export use std/ass";
|
||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||
match_suggestions(&vec!["assert"], &suggestions);
|
||||
|
||||
let completion_str = "use `std-rfc/cli";
|
||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||
match_suggestions(&vec!["clip"], &suggestions);
|
||||
|
||||
let completion_str = "use \"std";
|
||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||
match_suggestions(&vec!["\"std", "\"std-rfc"], &suggestions);
|
||||
|
||||
let completion_str = "overlay use \'std-rfc/cli";
|
||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||
match_suggestions(&vec!["clip"], &suggestions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exportable_completions() {
|
||||
let (_, _, mut engine, mut stack) = new_dotnu_engine();
|
||||
let code = r#"export module "🤔🐘" {
|
||||
export const foo = "🤔🐘";
|
||||
}"#;
|
||||
assert!(support::merge_input(code.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||
assert!(load_standard_library(&mut engine).is_ok());
|
||||
|
||||
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||
|
||||
let completion_str = "use std null";
|
||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||
match_suggestions(&vec!["null-device", "null_device"], &suggestions);
|
||||
|
||||
let completion_str = "export use std/assert eq";
|
||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||
match_suggestions(&vec!["equal"], &suggestions);
|
||||
|
||||
let completion_str = "use std/assert \"not eq";
|
||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||
match_suggestions(&vec!["'not equal'"], &suggestions);
|
||||
|
||||
let completion_str = "use std-rfc/clip ['prefi";
|
||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||
match_suggestions(&vec!["prefix"], &suggestions);
|
||||
|
||||
let completion_str = "use std/math [E, `TAU";
|
||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||
match_suggestions(&vec!["TAU"], &suggestions);
|
||||
|
||||
let completion_str = "use 🤔🐘 'foo";
|
||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||
match_suggestions(&vec!["foo"], &suggestions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dotnu_completions_const_nu_lib_dirs() {
|
||||
let (_, _, engine, stack) = new_dotnu_engine();
|
||||
@ -1012,11 +911,10 @@ fn partial_completions() {
|
||||
// Create the expected values
|
||||
let expected_paths = [
|
||||
file(dir.join("partial").join("hello.txt")),
|
||||
folder(dir.join("partial").join("hol")),
|
||||
file(dir.join("partial-a").join("have_ext.exe")),
|
||||
file(dir.join("partial-a").join("have_ext.txt")),
|
||||
file(dir.join("partial-a").join("hello")),
|
||||
folder(dir.join("partial-a").join("hola")),
|
||||
file(dir.join("partial-a").join("hola")),
|
||||
file(dir.join("partial-b").join("hello_b")),
|
||||
file(dir.join("partial-b").join("hi_b")),
|
||||
file(dir.join("partial-c").join("hello_c")),
|
||||
@ -1033,12 +931,11 @@ fn partial_completions() {
|
||||
// Create the expected values
|
||||
let expected_paths = [
|
||||
file(dir.join("partial").join("hello.txt")),
|
||||
folder(dir.join("partial").join("hol")),
|
||||
file(dir.join("partial-a").join("anotherfile")),
|
||||
file(dir.join("partial-a").join("have_ext.exe")),
|
||||
file(dir.join("partial-a").join("have_ext.txt")),
|
||||
file(dir.join("partial-a").join("hello")),
|
||||
folder(dir.join("partial-a").join("hola")),
|
||||
file(dir.join("partial-a").join("hola")),
|
||||
file(dir.join("partial-b").join("hello_b")),
|
||||
file(dir.join("partial-b").join("hi_b")),
|
||||
file(dir.join("partial-c").join("hello_c")),
|
||||
@ -2005,35 +1902,6 @@ fn table_cell_path_completions() {
|
||||
match_suggestions(&expected, &suggestions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quoted_cell_path_completions() {
|
||||
let (_, _, mut engine, mut stack) = new_engine();
|
||||
let command = r#"let foo = {'foo bar':1 'foo\\"bar"': 1 '.': 1 '|': 1 1: 1 "": 1}"#;
|
||||
assert!(support::merge_input(command.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||
|
||||
let expected: Vec<_> = vec![
|
||||
"\"\"",
|
||||
"\".\"",
|
||||
"\"1\"",
|
||||
"\"foo bar\"",
|
||||
"\"foo\\\\\\\\\\\"bar\\\"\"",
|
||||
"\"|\"",
|
||||
];
|
||||
let completion_str = "$foo.";
|
||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||
match_suggestions(&expected, &suggestions);
|
||||
|
||||
let expected: Vec<_> = vec!["\"foo bar\"", "\"foo\\\\\\\\\\\"bar\\\"\""];
|
||||
let completion_str = "$foo.`foo";
|
||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||
match_suggestions(&expected, &suggestions);
|
||||
|
||||
let completion_str = "$foo.foo";
|
||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||
match_suggestions(&expected, &suggestions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_of_command_and_flags() {
|
||||
let (_, _, mut engine, mut stack) = new_engine();
|
||||
@ -2307,43 +2175,15 @@ fn exact_match() {
|
||||
|
||||
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||
|
||||
// Troll case to test if exact match logic works case insensitively
|
||||
let target_dir = format!("open {}", folder(dir.join("pArTiAL")));
|
||||
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||
|
||||
// Since it's an exact match, only 'partial' should be suggested, not
|
||||
// 'partial-a' and stuff. Implemented in #13302
|
||||
match_suggestions(
|
||||
&vec![
|
||||
file(dir.join("partial").join("hello.txt")).as_str(),
|
||||
folder(dir.join("partial").join("hol")).as_str(),
|
||||
],
|
||||
&vec![file(dir.join("partial").join("hello.txt")).as_str()],
|
||||
&suggestions,
|
||||
);
|
||||
|
||||
let target_dir = format!("open {}", file(dir.join("partial").join("h")));
|
||||
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||
match_suggestions(
|
||||
&vec![
|
||||
file(dir.join("partial").join("hello.txt")).as_str(),
|
||||
folder(dir.join("partial").join("hol")).as_str(),
|
||||
],
|
||||
&suggestions,
|
||||
);
|
||||
|
||||
// Even though "hol" is an exact match, the first component ("part") wasn't an
|
||||
// exact match, so we include partial-a/hola
|
||||
let target_dir = format!("open {}", file(dir.join("part").join("hol")));
|
||||
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||
match_suggestions(
|
||||
&vec![
|
||||
folder(dir.join("partial").join("hol")).as_str(),
|
||||
folder(dir.join("partial-a").join("hola")).as_str(),
|
||||
],
|
||||
&suggestions,
|
||||
);
|
||||
|
||||
// Exact match behavior shouldn't be enabled if the path has no slashes
|
||||
let target_dir = format!("open {}", file(dir.join("partial")));
|
||||
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||
assert!(suggestions.len() > 1);
|
||||
}
|
||||
|
||||
#[ignore = "was reverted, still needs fixing"]
|
||||
|
@ -5,7 +5,7 @@ edition = "2021"
|
||||
license = "MIT"
|
||||
name = "nu-cmd-base"
|
||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-base"
|
||||
version = "0.104.1"
|
||||
version = "0.103.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@ -13,10 +13,10 @@ version = "0.104.1"
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
nu-engine = { path = "../nu-engine", version = "0.104.1", default-features = false }
|
||||
nu-parser = { path = "../nu-parser", version = "0.104.1" }
|
||||
nu-path = { path = "../nu-path", version = "0.104.1" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.104.1", default-features = false }
|
||||
nu-engine = { path = "../nu-engine", version = "0.103.0", default-features = false }
|
||||
nu-parser = { path = "../nu-parser", version = "0.103.0" }
|
||||
nu-path = { path = "../nu-path", version = "0.103.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.103.0", default-features = false }
|
||||
|
||||
indexmap = { workspace = true }
|
||||
miette = { workspace = true }
|
||||
|
@ -5,7 +5,7 @@ edition = "2021"
|
||||
license = "MIT"
|
||||
name = "nu-cmd-extra"
|
||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-extra"
|
||||
version = "0.104.1"
|
||||
version = "0.103.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@ -16,13 +16,13 @@ bench = false
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
nu-cmd-base = { path = "../nu-cmd-base", version = "0.104.1" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.104.1", default-features = false }
|
||||
nu-json = { version = "0.104.1", path = "../nu-json" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.104.1" }
|
||||
nu-pretty-hex = { version = "0.104.1", path = "../nu-pretty-hex" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.104.1", default-features = false }
|
||||
nu-utils = { path = "../nu-utils", version = "0.104.1", default-features = false }
|
||||
nu-cmd-base = { path = "../nu-cmd-base", version = "0.103.0" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.103.0", default-features = false }
|
||||
nu-json = { version = "0.103.0", path = "../nu-json" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.103.0" }
|
||||
nu-pretty-hex = { version = "0.103.0", path = "../nu-pretty-hex" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.103.0", default-features = false }
|
||||
nu-utils = { path = "../nu-utils", version = "0.103.0", default-features = false }
|
||||
|
||||
# Potential dependencies for extras
|
||||
heck = { workspace = true }
|
||||
@ -37,6 +37,6 @@ itertools = { workspace = true }
|
||||
mime = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.104.1" }
|
||||
nu-command = { path = "../nu-command", version = "0.104.1" }
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.104.1" }
|
||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.103.0" }
|
||||
nu-command = { path = "../nu-command", version = "0.103.0" }
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.103.0" }
|
||||
|
@ -135,7 +135,7 @@ where
|
||||
(min, max) => (rhs, lhs, max, min),
|
||||
};
|
||||
|
||||
let pad = iter::repeat_n(0, max_len - min_len);
|
||||
let pad = iter::repeat(0).take(max_len - min_len);
|
||||
|
||||
let mut a;
|
||||
let mut b;
|
||||
@ -159,10 +159,9 @@ where
|
||||
}
|
||||
(Value::Binary { .. }, Value::Int { .. }) | (Value::Int { .. }, Value::Binary { .. }) => {
|
||||
Value::error(
|
||||
ShellError::OnlySupportsThisInputType {
|
||||
ShellError::PipelineMismatch {
|
||||
exp_input_type: "input, and argument, to be both int or both binary"
|
||||
.to_string(),
|
||||
wrong_type: "int and binary".to_string(),
|
||||
dst_span: rhs.span(),
|
||||
src_span: span,
|
||||
},
|
||||
|
@ -249,7 +249,7 @@ fn shift_bytes_and_bits_left(data: &[u8], byte_shift: usize, bit_shift: usize) -
|
||||
Last | Only => lhs << bit_shift,
|
||||
_ => (lhs << bit_shift) | (rhs >> (8 - bit_shift)),
|
||||
})
|
||||
.chain(iter::repeat_n(0, byte_shift))
|
||||
.chain(iter::repeat(0).take(byte_shift))
|
||||
.collect::<Vec<u8>>()
|
||||
}
|
||||
|
||||
|
@ -253,11 +253,12 @@ fn format_record(
|
||||
optional: false,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let expanded_string = data_as_value
|
||||
.follow_cell_path(&path_members, false)?
|
||||
.to_expanded_string(", ", config);
|
||||
output.push_str(expanded_string.as_str())
|
||||
match data_as_value.clone().follow_cell_path(&path_members, false) {
|
||||
Ok(value_at_column) => {
|
||||
output.push_str(value_at_column.to_expanded_string(", ", config).as_str())
|
||||
}
|
||||
Err(se) => return Err(se),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-lang"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "nu-cmd-lang"
|
||||
version = "0.104.1"
|
||||
version = "0.103.0"
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
@ -15,16 +15,16 @@ bench = false
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
nu-engine = { path = "../nu-engine", version = "0.104.1", default-features = false }
|
||||
nu-parser = { path = "../nu-parser", version = "0.104.1" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.104.1", default-features = false }
|
||||
nu-utils = { path = "../nu-utils", version = "0.104.1", default-features = false }
|
||||
nu-engine = { path = "../nu-engine", version = "0.103.0", default-features = false }
|
||||
nu-parser = { path = "../nu-parser", version = "0.103.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.103.0", default-features = false }
|
||||
nu-utils = { path = "../nu-utils", version = "0.103.0", default-features = false }
|
||||
|
||||
itertools = { workspace = true }
|
||||
shadow-rs = { version = "1.1", default-features = false }
|
||||
shadow-rs = { version = "0.38", default-features = false }
|
||||
|
||||
[build-dependencies]
|
||||
shadow-rs = { version = "1.1", default-features = false, features = ["build"] }
|
||||
shadow-rs = { version = "0.38", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
quickcheck = { workspace = true }
|
||||
|
@ -18,4 +18,4 @@ A base crate is one with minimal dependencies in our system so that other develo
|
||||
|
||||
### Background on nu-cmd-lang
|
||||
|
||||
This crate was designed to be a small, concise set of tools or commands that serve as the *foundation layer* of both nu and nushell. These are the core commands needed to have a nice working version of the *nu language* without all of the support that the other commands provide inside nushell. Prior to the launch of this crate all of our commands were housed in the crate *nu-command*. Moving forward we would like to *slowly* break out the commands in nu-command into different crates; the naming and how this will work and where all the commands will be located is a "work in progress" especially now that the *standard library* is starting to become more popular as a location for commands. As time goes on some of our commands written in rust will be migrated to nu and when this happens they will be moved into the *standard library*.
|
||||
This crate was designed to be a small, concise set of tools or commands that serve as the *foundation layer* of both nu and nushell. These are the core commands needed to have a nice working version of the *nu language* without all of the support that the other commands provide inside nushell. Prior to the launch of this crate all of our commands were housed in the crate *nu-command*. Moving forward we would like to *slowly* break out the commands in nu-command into different crates; the naming and how this will work and where all the commands will be located is a "work in progress" especially now that the *standard library* is starting to become more popular as a location for commands. As time goes on some of our commands written in rust will be migrated to nu and when this happens they will be moved into the *standard library*.
|
||||
|
@ -1,9 +1,6 @@
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::{
|
||||
engine::{Closure, StateWorkingSet},
|
||||
BlockId, ByteStreamSource, Category, PipelineMetadata, Signature,
|
||||
};
|
||||
use std::any::type_name;
|
||||
use nu_protocol::{engine::StateWorkingSet, ByteStreamSource, PipelineMetadata};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Describe;
|
||||
|
||||
@ -76,116 +73,39 @@ impl Command for Describe {
|
||||
"{shell:'true', uwu:true, features: {bugs:false, multiplatform:true, speed: 10}, fib: [1 1 2 3 5 8], on_save: {|x| $'Saving ($x)'}, first_commit: 2019-05-10, my_duration: (4min + 20sec)} | describe -d",
|
||||
result: Some(Value::test_record(record!(
|
||||
"type" => Value::test_string("record"),
|
||||
"detailed_type" => Value::test_string("record<shell: string, uwu: bool, features: record<bugs: bool, multiplatform: bool, speed: int>, fib: list<int>, on_save: closure, first_commit: datetime, my_duration: duration>"),
|
||||
"columns" => Value::test_record(record!(
|
||||
"shell" => Value::test_record(record!(
|
||||
"type" => Value::test_string("string"),
|
||||
"detailed_type" => Value::test_string("string"),
|
||||
"rust_type" => Value::test_string("&alloc::string::String"),
|
||||
"value" => Value::test_string("true"),
|
||||
)),
|
||||
"uwu" => Value::test_record(record!(
|
||||
"type" => Value::test_string("bool"),
|
||||
"detailed_type" => Value::test_string("bool"),
|
||||
"rust_type" => Value::test_string("bool"),
|
||||
"value" => Value::test_bool(true),
|
||||
)),
|
||||
"shell" => Value::test_string("string"),
|
||||
"uwu" => Value::test_string("bool"),
|
||||
"features" => Value::test_record(record!(
|
||||
"type" => Value::test_string("record"),
|
||||
"detailed_type" => Value::test_string("record<bugs: bool, multiplatform: bool, speed: int>"),
|
||||
"columns" => Value::test_record(record!(
|
||||
"bugs" => Value::test_record(record!(
|
||||
"type" => Value::test_string("bool"),
|
||||
"detailed_type" => Value::test_string("bool"),
|
||||
"rust_type" => Value::test_string("bool"),
|
||||
"value" => Value::test_bool(false),
|
||||
)),
|
||||
"multiplatform" => Value::test_record(record!(
|
||||
"type" => Value::test_string("bool"),
|
||||
"detailed_type" => Value::test_string("bool"),
|
||||
"rust_type" => Value::test_string("bool"),
|
||||
"value" => Value::test_bool(true),
|
||||
)),
|
||||
"speed" => Value::test_record(record!(
|
||||
"type" => Value::test_string("int"),
|
||||
"detailed_type" => Value::test_string("int"),
|
||||
"rust_type" => Value::test_string("i64"),
|
||||
"value" => Value::test_int(10),
|
||||
)),
|
||||
"bugs" => Value::test_string("bool"),
|
||||
"multiplatform" => Value::test_string("bool"),
|
||||
"speed" => Value::test_string("int"),
|
||||
)),
|
||||
"rust_type" => Value::test_string("&nu_utils::shared_cow::SharedCow<nu_protocol::value::record::Record>"),
|
||||
)),
|
||||
"fib" => Value::test_record(record!(
|
||||
"type" => Value::test_string("list"),
|
||||
"detailed_type" => Value::test_string("list<int>"),
|
||||
"length" => Value::test_int(6),
|
||||
"rust_type" => Value::test_string("&mut alloc::vec::Vec<nu_protocol::value::Value>"),
|
||||
"value" => Value::test_list(vec![
|
||||
Value::test_record(record!(
|
||||
"type" => Value::test_string("int"),
|
||||
"detailed_type" => Value::test_string("int"),
|
||||
"rust_type" => Value::test_string("i64"),
|
||||
"value" => Value::test_int(1),
|
||||
)),
|
||||
Value::test_record(record!(
|
||||
"type" => Value::test_string("int"),
|
||||
"detailed_type" => Value::test_string("int"),
|
||||
"rust_type" => Value::test_string("i64"),
|
||||
"value" => Value::test_int(1),
|
||||
)),
|
||||
Value::test_record(record!(
|
||||
"type" => Value::test_string("int"),
|
||||
"detailed_type" => Value::test_string("int"),
|
||||
"rust_type" => Value::test_string("i64"),
|
||||
"value" => Value::test_int(2),
|
||||
)),
|
||||
Value::test_record(record!(
|
||||
"type" => Value::test_string("int"),
|
||||
"detailed_type" => Value::test_string("int"),
|
||||
"rust_type" => Value::test_string("i64"),
|
||||
"value" => Value::test_int(3),
|
||||
)),
|
||||
Value::test_record(record!(
|
||||
"type" => Value::test_string("int"),
|
||||
"detailed_type" => Value::test_string("int"),
|
||||
"rust_type" => Value::test_string("i64"),
|
||||
"value" => Value::test_int(5),
|
||||
)),
|
||||
Value::test_record(record!(
|
||||
"type" => Value::test_string("int"),
|
||||
"detailed_type" => Value::test_string("int"),
|
||||
"rust_type" => Value::test_string("i64"),
|
||||
"value" => Value::test_int(8),
|
||||
))]
|
||||
),
|
||||
"values" => Value::test_list(vec![
|
||||
Value::test_string("int"),
|
||||
Value::test_string("int"),
|
||||
Value::test_string("int"),
|
||||
Value::test_string("int"),
|
||||
Value::test_string("int"),
|
||||
Value::test_string("int"),
|
||||
]),
|
||||
)),
|
||||
"on_save" => Value::test_record(record!(
|
||||
"type" => Value::test_string("closure"),
|
||||
"detailed_type" => Value::test_string("closure"),
|
||||
"rust_type" => Value::test_string("&alloc::boxed::Box<nu_protocol::engine::closure::Closure>"),
|
||||
"value" => Value::test_closure(Closure {
|
||||
block_id: BlockId::new(1),
|
||||
captures: vec![],
|
||||
}),
|
||||
"signature" => Value::test_record(record!(
|
||||
"name" => Value::test_string(""),
|
||||
"category" => Value::test_string("default"),
|
||||
)),
|
||||
)),
|
||||
"first_commit" => Value::test_record(record!(
|
||||
"type" => Value::test_string("datetime"),
|
||||
"detailed_type" => Value::test_string("datetime"),
|
||||
"rust_type" => Value::test_string("chrono::datetime::DateTime<chrono::offset::fixed::FixedOffset>"),
|
||||
"value" => Value::test_date("2019-05-10 00:00:00Z".parse().unwrap_or_default()),
|
||||
)),
|
||||
"my_duration" => Value::test_record(record!(
|
||||
"type" => Value::test_string("duration"),
|
||||
"detailed_type" => Value::test_string("duration"),
|
||||
"rust_type" => Value::test_string("i64"),
|
||||
"value" => Value::test_duration(260_000_000_000),
|
||||
))
|
||||
"first_commit" => Value::test_string("date"),
|
||||
"my_duration" => Value::test_string("duration"),
|
||||
)),
|
||||
"rust_type" => Value::test_string("&nu_utils::shared_cow::SharedCow<nu_protocol::value::record::Record>"),
|
||||
))),
|
||||
},
|
||||
Example {
|
||||
@ -255,9 +175,7 @@ fn run(
|
||||
|
||||
Value::record(
|
||||
record! {
|
||||
"type" => Value::string("bytestream", head),
|
||||
"detailed_type" => Value::string(type_, head),
|
||||
"rust_type" => Value::string(type_of(&stream), head),
|
||||
"type" => Value::string(type_, head),
|
||||
"origin" => Value::string(origin, head),
|
||||
"metadata" => metadata_to_value(metadata, head),
|
||||
},
|
||||
@ -274,7 +192,6 @@ fn run(
|
||||
description
|
||||
}
|
||||
PipelineData::ListStream(stream, ..) => {
|
||||
let type_ = type_of(&stream);
|
||||
if options.detailed {
|
||||
let subtype = if options.no_collect {
|
||||
Value::string("any", head)
|
||||
@ -284,8 +201,6 @@ fn run(
|
||||
Value::record(
|
||||
record! {
|
||||
"type" => Value::string("stream", head),
|
||||
"detailed_type" => Value::string("list stream", head),
|
||||
"rust_type" => Value::string(type_, head),
|
||||
"origin" => Value::string("nushell", head),
|
||||
"subtype" => subtype,
|
||||
"metadata" => metadata_to_value(metadata, head),
|
||||
@ -314,95 +229,45 @@ fn run(
|
||||
}
|
||||
|
||||
enum Description {
|
||||
String(String),
|
||||
Record(Record),
|
||||
}
|
||||
|
||||
impl Description {
|
||||
fn into_value(self, span: Span) -> Value {
|
||||
match self {
|
||||
Description::String(ty) => Value::string(ty, span),
|
||||
Description::Record(record) => Value::record(record, span),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn describe_value(value: Value, head: Span, engine_state: Option<&EngineState>) -> Value {
|
||||
let Description::Record(record) = describe_value_inner(value, head, engine_state);
|
||||
let record = match describe_value_inner(value, head, engine_state) {
|
||||
Description::String(ty) => record! { "type" => Value::string(ty, head) },
|
||||
Description::Record(record) => record,
|
||||
};
|
||||
Value::record(record, head)
|
||||
}
|
||||
|
||||
fn type_of<T>(_: &T) -> String {
|
||||
type_name::<T>().to_string()
|
||||
}
|
||||
|
||||
fn describe_value_inner(
|
||||
mut value: Value,
|
||||
value: Value,
|
||||
head: Span,
|
||||
engine_state: Option<&EngineState>,
|
||||
) -> Description {
|
||||
let value_type = value.get_type().to_string();
|
||||
match value {
|
||||
Value::Bool { val, .. } => Description::Record(record! {
|
||||
"type" => Value::string("bool", head),
|
||||
"detailed_type" => Value::string(value_type, head),
|
||||
"rust_type" => Value::string(type_of(&val), head),
|
||||
"value" => value,
|
||||
}),
|
||||
Value::Int { val, .. } => Description::Record(record! {
|
||||
"type" => Value::string("int", head),
|
||||
"detailed_type" => Value::string(value_type, head),
|
||||
"rust_type" => Value::string(type_of(&val), head),
|
||||
"value" => value,
|
||||
}),
|
||||
Value::Float { val, .. } => Description::Record(record! {
|
||||
"type" => Value::string("float", head),
|
||||
"detailed_type" => Value::string(value_type, head),
|
||||
"rust_type" => Value::string(type_of(&val), head),
|
||||
"value" => value,
|
||||
}),
|
||||
Value::Filesize { val, .. } => Description::Record(record! {
|
||||
"type" => Value::string("filesize", head),
|
||||
"detailed_type" => Value::string(value_type, head),
|
||||
"rust_type" => Value::string(type_of(&val), head),
|
||||
"value" => value,
|
||||
}),
|
||||
Value::Duration { val, .. } => Description::Record(record! {
|
||||
"type" => Value::string("duration", head),
|
||||
"detailed_type" => Value::string(value_type, head),
|
||||
"rust_type" => Value::string(type_of(&val), head),
|
||||
"value" => value,
|
||||
}),
|
||||
Value::Date { val, .. } => Description::Record(record! {
|
||||
"type" => Value::string("datetime", head),
|
||||
"detailed_type" => Value::string(value_type, head),
|
||||
"rust_type" => Value::string(type_of(&val), head),
|
||||
"value" => value,
|
||||
}),
|
||||
Value::Range { ref val, .. } => Description::Record(record! {
|
||||
"type" => Value::string("range", head),
|
||||
"detailed_type" => Value::string(value_type, head),
|
||||
"rust_type" => Value::string(type_of(&val), head),
|
||||
"value" => value,
|
||||
}),
|
||||
Value::String { ref val, .. } => Description::Record(record! {
|
||||
"type" => Value::string("string", head),
|
||||
"detailed_type" => Value::string(value_type, head),
|
||||
"rust_type" => Value::string(type_of(&val), head),
|
||||
"value" => value,
|
||||
}),
|
||||
Value::Glob { ref val, .. } => Description::Record(record! {
|
||||
"type" => Value::string("glob", head),
|
||||
"detailed_type" => Value::string(value_type, head),
|
||||
"rust_type" => Value::string(type_of(&val), head),
|
||||
"value" => value,
|
||||
}),
|
||||
Value::Nothing { .. } => Description::Record(record! {
|
||||
"type" => Value::string("nothing", head),
|
||||
"detailed_type" => Value::string(value_type, head),
|
||||
"rust_type" => Value::string("", head),
|
||||
"value" => value,
|
||||
}),
|
||||
Value::Record { ref val, .. } => {
|
||||
let mut columns = val.clone().into_owned();
|
||||
Value::Bool { .. }
|
||||
| Value::Int { .. }
|
||||
| Value::Float { .. }
|
||||
| Value::Filesize { .. }
|
||||
| Value::Duration { .. }
|
||||
| Value::Date { .. }
|
||||
| Value::Range { .. }
|
||||
| Value::String { .. }
|
||||
| Value::Glob { .. }
|
||||
| Value::Nothing { .. } => Description::String(value.get_type().to_string()),
|
||||
Value::Record { val, .. } => {
|
||||
let mut columns = val.into_owned();
|
||||
for (_, val) in &mut columns {
|
||||
*val =
|
||||
describe_value_inner(std::mem::take(val), head, engine_state).into_value(head);
|
||||
@ -410,34 +275,25 @@ fn describe_value_inner(
|
||||
|
||||
Description::Record(record! {
|
||||
"type" => Value::string("record", head),
|
||||
"detailed_type" => Value::string(value_type, head),
|
||||
"columns" => Value::record(columns.clone(), head),
|
||||
"rust_type" => Value::string(type_of(&val), head),
|
||||
"columns" => Value::record(columns, head),
|
||||
})
|
||||
}
|
||||
Value::List { ref mut vals, .. } => {
|
||||
for val in &mut *vals {
|
||||
Value::List { mut vals, .. } => {
|
||||
for val in &mut vals {
|
||||
*val =
|
||||
describe_value_inner(std::mem::take(val), head, engine_state).into_value(head);
|
||||
}
|
||||
|
||||
Description::Record(record! {
|
||||
"type" => Value::string("list", head),
|
||||
"detailed_type" => Value::string(value_type, head),
|
||||
"length" => Value::int(vals.len() as i64, head),
|
||||
"rust_type" => Value::string(type_of(&vals), head),
|
||||
"value" => value,
|
||||
"values" => Value::list(vals, head),
|
||||
})
|
||||
}
|
||||
Value::Closure { ref val, .. } => {
|
||||
Value::Closure { val, .. } => {
|
||||
let block = engine_state.map(|engine_state| engine_state.get_block(val.block_id));
|
||||
|
||||
let mut record = record! {
|
||||
"type" => Value::string("closure", head),
|
||||
"detailed_type" => Value::string(value_type, head),
|
||||
"rust_type" => Value::string(type_of(&val), head),
|
||||
"value" => value,
|
||||
};
|
||||
let mut record = record! { "type" => Value::string("closure", head) };
|
||||
if let Some(block) = block {
|
||||
record.push(
|
||||
"signature",
|
||||
@ -452,37 +308,21 @@ fn describe_value_inner(
|
||||
}
|
||||
Description::Record(record)
|
||||
}
|
||||
Value::Error { ref error, .. } => Description::Record(record! {
|
||||
Value::Error { error, .. } => Description::Record(record! {
|
||||
"type" => Value::string("error", head),
|
||||
"detailed_type" => Value::string(value_type, head),
|
||||
"subtype" => Value::string(error.to_string(), head),
|
||||
"rust_type" => Value::string(type_of(&error), head),
|
||||
"value" => value,
|
||||
}),
|
||||
Value::Binary { ref val, .. } => Description::Record(record! {
|
||||
Value::Binary { val, .. } => Description::Record(record! {
|
||||
"type" => Value::string("binary", head),
|
||||
"detailed_type" => Value::string(value_type, head),
|
||||
"length" => Value::int(val.len() as i64, head),
|
||||
"rust_type" => Value::string(type_of(&val), head),
|
||||
"value" => value,
|
||||
}),
|
||||
Value::CellPath { ref val, .. } => Description::Record(record! {
|
||||
Value::CellPath { val, .. } => Description::Record(record! {
|
||||
"type" => Value::string("cell-path", head),
|
||||
"detailed_type" => Value::string(value_type, head),
|
||||
"length" => Value::int(val.members.len() as i64, head),
|
||||
"rust_type" => Value::string(type_of(&val), head),
|
||||
"value" => value
|
||||
}),
|
||||
Value::Custom { ref val, .. } => Description::Record(record! {
|
||||
Value::Custom { val, .. } => Description::Record(record! {
|
||||
"type" => Value::string("custom", head),
|
||||
"detailed_type" => Value::string(value_type, head),
|
||||
"subtype" => Value::string(val.type_name(), head),
|
||||
"rust_type" => Value::string(type_of(&val), head),
|
||||
"value" =>
|
||||
match val.to_base_value(head) {
|
||||
Ok(base_value) => base_value,
|
||||
Err(err) => Value::error(err, head),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,16 @@ impl Command for Do {
|
||||
"ignore errors as the closure runs",
|
||||
Some('i'),
|
||||
)
|
||||
.switch(
|
||||
"ignore-shell-errors",
|
||||
"ignore shell errors as the closure runs",
|
||||
Some('s'),
|
||||
)
|
||||
.switch(
|
||||
"ignore-program-errors",
|
||||
"ignore external program errors as the closure runs",
|
||||
Some('p'),
|
||||
)
|
||||
.switch(
|
||||
"capture-errors",
|
||||
"catch errors as the closure runs, and return them",
|
||||
@ -61,6 +71,36 @@ impl Command for Do {
|
||||
let rest: Vec<Value> = call.rest(engine_state, caller_stack, 1)?;
|
||||
let ignore_all_errors = call.has_flag(engine_state, caller_stack, "ignore-errors")?;
|
||||
|
||||
if call.has_flag(engine_state, caller_stack, "ignore-shell-errors")? {
|
||||
nu_protocol::report_shell_warning(
|
||||
engine_state,
|
||||
&ShellError::GenericError {
|
||||
error: "Deprecated option".into(),
|
||||
msg: "`--ignore-shell-errors` is deprecated and will be removed in 0.102.0."
|
||||
.into(),
|
||||
span: Some(call.head),
|
||||
help: Some("Please use the `--ignore-errors(-i)`".into()),
|
||||
inner: vec![],
|
||||
},
|
||||
);
|
||||
}
|
||||
if call.has_flag(engine_state, caller_stack, "ignore-program-errors")? {
|
||||
nu_protocol::report_shell_warning(
|
||||
engine_state,
|
||||
&ShellError::GenericError {
|
||||
error: "Deprecated option".into(),
|
||||
msg: "`--ignore-program-errors` is deprecated and will be removed in 0.102.0."
|
||||
.into(),
|
||||
span: Some(call.head),
|
||||
help: Some("Please use the `--ignore-errors(-i)`".into()),
|
||||
inner: vec![],
|
||||
},
|
||||
);
|
||||
}
|
||||
let ignore_shell_errors = ignore_all_errors
|
||||
|| call.has_flag(engine_state, caller_stack, "ignore-shell-errors")?;
|
||||
let ignore_program_errors = ignore_all_errors
|
||||
|| call.has_flag(engine_state, caller_stack, "ignore-program-errors")?;
|
||||
let capture_errors = call.has_flag(engine_state, caller_stack, "capture-errors")?;
|
||||
let has_env = call.has_flag(engine_state, caller_stack, "env")?;
|
||||
|
||||
@ -166,7 +206,7 @@ impl Command for Do {
|
||||
}
|
||||
}
|
||||
Ok(PipelineData::ByteStream(mut stream, metadata))
|
||||
if ignore_all_errors
|
||||
if ignore_program_errors
|
||||
&& !matches!(
|
||||
caller_stack.stdout(),
|
||||
OutDest::Pipe | OutDest::PipeSeparate | OutDest::Value
|
||||
@ -178,10 +218,10 @@ impl Command for Do {
|
||||
}
|
||||
Ok(PipelineData::ByteStream(stream, metadata))
|
||||
}
|
||||
Ok(PipelineData::Value(Value::Error { .. }, ..)) | Err(_) if ignore_all_errors => {
|
||||
Ok(PipelineData::Value(Value::Error { .. }, ..)) | Err(_) if ignore_shell_errors => {
|
||||
Ok(PipelineData::empty())
|
||||
}
|
||||
Ok(PipelineData::ListStream(stream, metadata)) if ignore_all_errors => {
|
||||
Ok(PipelineData::ListStream(stream, metadata)) if ignore_shell_errors => {
|
||||
let stream = stream.map(move |value| {
|
||||
if let Value::Error { .. } = value {
|
||||
Value::nothing(head)
|
||||
|
@ -116,8 +116,6 @@ impl Command for OverlayUse {
|
||||
// b) refreshing an active overlay (the origin module changed)
|
||||
|
||||
let module = engine_state.get_module(module_id);
|
||||
// in such case, should also make sure that PWD is not restored in old overlays.
|
||||
let cwd = caller_stack.get_env_var(engine_state, "PWD").cloned();
|
||||
|
||||
// Evaluate the export-env block (if any) and keep its environment
|
||||
if let Some(block_id) = module.env_block {
|
||||
@ -162,19 +160,11 @@ impl Command for OverlayUse {
|
||||
|
||||
// The export-env block should see the env vars *before* activating this overlay
|
||||
caller_stack.add_overlay(overlay_name);
|
||||
// make sure that PWD is not restored in old overlays.
|
||||
if let Some(cwd) = cwd {
|
||||
caller_stack.add_env_var("PWD".to_string(), cwd);
|
||||
}
|
||||
|
||||
// Merge the block's environment to the current stack
|
||||
redirect_env(engine_state, caller_stack, &callee_stack);
|
||||
} else {
|
||||
caller_stack.add_overlay(overlay_name);
|
||||
// make sure that PWD is not restored in old overlays.
|
||||
if let Some(cwd) = cwd {
|
||||
caller_stack.add_env_var("PWD".to_string(), cwd);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
caller_stack.add_overlay(overlay_name);
|
||||
|
@ -5,7 +5,7 @@ edition = "2021"
|
||||
license = "MIT"
|
||||
name = "nu-cmd-plugin"
|
||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-plugin"
|
||||
version = "0.104.1"
|
||||
version = "0.103.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@ -13,10 +13,10 @@ version = "0.104.1"
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
nu-engine = { path = "../nu-engine", version = "0.104.1" }
|
||||
nu-path = { path = "../nu-path", version = "0.104.1" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.104.1", features = ["plugin"] }
|
||||
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.104.1" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.103.0" }
|
||||
nu-path = { path = "../nu-path", version = "0.103.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.103.0", features = ["plugin"] }
|
||||
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.103.0" }
|
||||
|
||||
itertools = { workspace = true }
|
||||
|
||||
|
@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-color-confi
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "nu-color-config"
|
||||
version = "0.104.1"
|
||||
version = "0.103.0"
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
@ -14,12 +14,12 @@ bench = false
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.104.1", default-features = false }
|
||||
nu-engine = { path = "../nu-engine", version = "0.104.1", default-features = false }
|
||||
nu-json = { path = "../nu-json", version = "0.104.1" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.103.0", default-features = false }
|
||||
nu-engine = { path = "../nu-engine", version = "0.103.0", default-features = false }
|
||||
nu-json = { path = "../nu-json", version = "0.103.0" }
|
||||
nu-ansi-term = { workspace = true }
|
||||
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.104.1" }
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.103.0" }
|
@ -120,7 +120,7 @@ impl<'a> StyleComputer<'a> {
|
||||
("int".to_string(), ComputableStyle::Static(Color::White.normal())),
|
||||
("filesize".to_string(), ComputableStyle::Static(Color::Cyan.normal())),
|
||||
("duration".to_string(), ComputableStyle::Static(Color::White.normal())),
|
||||
("datetime".to_string(), ComputableStyle::Static(Color::Purple.normal())),
|
||||
("date".to_string(), ComputableStyle::Static(Color::Purple.normal())),
|
||||
("range".to_string(), ComputableStyle::Static(Color::White.normal())),
|
||||
("float".to_string(), ComputableStyle::Static(Color::White.normal())),
|
||||
("string".to_string(), ComputableStyle::Static(Color::White.normal())),
|
||||
|
@ -5,7 +5,7 @@ edition = "2021"
|
||||
license = "MIT"
|
||||
name = "nu-command"
|
||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-command"
|
||||
version = "0.104.1"
|
||||
version = "0.103.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@ -16,21 +16,21 @@ bench = false
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
nu-cmd-base = { path = "../nu-cmd-base", version = "0.104.1" }
|
||||
nu-color-config = { path = "../nu-color-config", version = "0.104.1" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.104.1", default-features = false }
|
||||
nu-glob = { path = "../nu-glob", version = "0.104.1" }
|
||||
nu-json = { path = "../nu-json", version = "0.104.1" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.104.1" }
|
||||
nu-path = { path = "../nu-path", version = "0.104.1" }
|
||||
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.104.1" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.104.1", default-features = false }
|
||||
nu-system = { path = "../nu-system", version = "0.104.1" }
|
||||
nu-table = { path = "../nu-table", version = "0.104.1" }
|
||||
nu-term-grid = { path = "../nu-term-grid", version = "0.104.1" }
|
||||
nu-utils = { path = "../nu-utils", version = "0.104.1", default-features = false }
|
||||
nu-cmd-base = { path = "../nu-cmd-base", version = "0.103.0" }
|
||||
nu-color-config = { path = "../nu-color-config", version = "0.103.0" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.103.0", default-features = false }
|
||||
nu-glob = { path = "../nu-glob", version = "0.103.0" }
|
||||
nu-json = { path = "../nu-json", version = "0.103.0" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.103.0" }
|
||||
nu-path = { path = "../nu-path", version = "0.103.0" }
|
||||
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.103.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.103.0", default-features = false }
|
||||
nu-system = { path = "../nu-system", version = "0.103.0" }
|
||||
nu-table = { path = "../nu-table", version = "0.103.0" }
|
||||
nu-term-grid = { path = "../nu-term-grid", version = "0.103.0" }
|
||||
nu-utils = { path = "../nu-utils", version = "0.103.0", default-features = false }
|
||||
nu-ansi-term = { workspace = true }
|
||||
nuon = { path = "../nuon", version = "0.104.1" }
|
||||
nuon = { path = "../nuon", version = "0.103.0" }
|
||||
|
||||
alphanumeric-sort = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
@ -129,7 +129,7 @@ v_htmlescape = { workspace = true }
|
||||
wax = { workspace = true }
|
||||
which = { workspace = true, optional = true }
|
||||
unicode-width = { workspace = true }
|
||||
data-encoding = { version = "2.9.0", features = ["alloc"] }
|
||||
data-encoding = { version = "2.8.0", features = ["alloc"] }
|
||||
web-time = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
@ -209,8 +209,8 @@ sqlite = ["rusqlite"]
|
||||
trash-support = ["trash"]
|
||||
|
||||
[dev-dependencies]
|
||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.104.1" }
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.104.1" }
|
||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.103.0" }
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.103.0" }
|
||||
|
||||
dirs = { workspace = true }
|
||||
mockito = { workspace = true, default-features = false }
|
||||
|
@ -1,29 +1,12 @@
|
||||
use crate::{generate_strftime_list, parse_date_from_string};
|
||||
use chrono::{
|
||||
DateTime, Datelike, FixedOffset, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone,
|
||||
Timelike, Utc,
|
||||
};
|
||||
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, TimeZone, Utc};
|
||||
use human_date_parser::{from_human_time, ParseResult};
|
||||
use nu_cmd_base::input_handler::{operate, CmdArgument};
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
const HOUR: i32 = 60 * 60;
|
||||
const ALLOWED_COLUMNS: [&str; 10] = [
|
||||
"year",
|
||||
"month",
|
||||
"day",
|
||||
"hour",
|
||||
"minute",
|
||||
"second",
|
||||
"millisecond",
|
||||
"microsecond",
|
||||
"nanosecond",
|
||||
"timezone",
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Arguments {
|
||||
zone_options: Option<Spanned<Zone>>,
|
||||
format_options: Option<Spanned<DatetimeFormat>>,
|
||||
format_options: Option<DatetimeFormat>,
|
||||
cell_paths: Option<Vec<CellPath>>,
|
||||
}
|
||||
|
||||
@ -81,12 +64,8 @@ impl Command for IntoDatetime {
|
||||
(Type::String, Type::Date),
|
||||
(Type::List(Box::new(Type::String)), Type::List(Box::new(Type::Date))),
|
||||
(Type::table(), Type::table()),
|
||||
(Type::Nothing, Type::table()),
|
||||
// FIXME: https://github.com/nushell/nushell/issues/15485
|
||||
// 'record -> any' was added as a temporary workaround to avoid type inference issues. The Any arm needs to be appear first.
|
||||
(Type::record(), Type::Any),
|
||||
(Type::record(), Type::record()),
|
||||
(Type::record(), Type::Date),
|
||||
(Type::Nothing, Type::table()),
|
||||
// FIXME Type::Any input added to disable pipeline input type checking, as run-time checks can raise undesirable type errors
|
||||
// which aren't caught by the parser. see https://github.com/nushell/nushell/pull/14922 for more details
|
||||
// only applicable for --list flag
|
||||
@ -116,6 +95,11 @@ impl Command for IntoDatetime {
|
||||
"Show all possible variables for use in --format flag",
|
||||
Some('l'),
|
||||
)
|
||||
.switch(
|
||||
"list-human",
|
||||
"Show human-readable datetime parsing examples",
|
||||
Some('n'),
|
||||
)
|
||||
.rest(
|
||||
"rest",
|
||||
SyntaxShape::CellPath,
|
||||
@ -133,6 +117,8 @@ impl Command for IntoDatetime {
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
if call.has_flag(engine_state, stack, "list")? {
|
||||
Ok(generate_strftime_list(call.head, true).into_pipeline_data())
|
||||
} else if call.has_flag(engine_state, stack, "list-human")? {
|
||||
Ok(list_human_readable_examples(call.head).into_pipeline_data())
|
||||
} else {
|
||||
let cell_paths = call.rest(engine_state, stack, 0)?;
|
||||
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
|
||||
@ -152,16 +138,13 @@ impl Command for IntoDatetime {
|
||||
};
|
||||
|
||||
let format_options = call
|
||||
.get_flag::<Spanned<String>>(engine_state, stack, "format")?
|
||||
.get_flag::<String>(engine_state, stack, "format")?
|
||||
.as_ref()
|
||||
.map(|fmt| Spanned {
|
||||
item: DatetimeFormat(fmt.item.to_string()),
|
||||
span: fmt.span,
|
||||
});
|
||||
.map(|fmt| DatetimeFormat(fmt.to_string()));
|
||||
|
||||
let args = Arguments {
|
||||
zone_options,
|
||||
format_options,
|
||||
zone_options,
|
||||
cell_paths,
|
||||
};
|
||||
operate(action, args, input, call.head, engine_state.signals())
|
||||
@ -237,12 +220,6 @@ impl Command for IntoDatetime {
|
||||
#[allow(clippy::inconsistent_digit_grouping)]
|
||||
result: example_result_1(1614434140_000000000),
|
||||
},
|
||||
Example {
|
||||
description: "Using a record as input",
|
||||
example: "{year: 2025, month: 3, day: 30, hour: 12, minute: 15, second: 59, timezone: '+02:00'} | into datetime",
|
||||
#[allow(clippy::inconsistent_digit_grouping)]
|
||||
result: example_result_1(1743329759_000000000),
|
||||
},
|
||||
Example {
|
||||
description: "Convert list of timestamps to datetimes",
|
||||
example: r#"["2023-03-30 10:10:07 -05:00", "2023-05-05 13:43:49 -05:00", "2023-06-05 01:37:42 -05:00"] | into datetime"#,
|
||||
@ -276,11 +253,26 @@ impl Command for IntoDatetime {
|
||||
Span::test_data(),
|
||||
)),
|
||||
},
|
||||
Example {
|
||||
description: "Parsing human readable datetimes",
|
||||
example: "'Today at 18:30' | into datetime",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Parsing human readable datetimes",
|
||||
example: "'Last Friday at 19:45' | into datetime",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Parsing human readable datetimes",
|
||||
example: "'In 5 minutes and 30 seconds' | into datetime",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
struct DatetimeFormat(String);
|
||||
|
||||
fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
||||
@ -292,44 +284,45 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
||||
return input.clone();
|
||||
}
|
||||
|
||||
if let Value::Record { val: record, .. } = input {
|
||||
if let Some(tz) = timezone {
|
||||
return Value::error(
|
||||
ShellError::IncompatibleParameters {
|
||||
left_message: "got a record as input".into(),
|
||||
left_span: head,
|
||||
right_message: "the timezone should be included in the record".into(),
|
||||
right_span: tz.span,
|
||||
},
|
||||
head,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(dt) = dateformat {
|
||||
return Value::error(
|
||||
ShellError::IncompatibleParameters {
|
||||
left_message: "got a record as input".into(),
|
||||
left_span: head,
|
||||
right_message: "cannot be used with records".into(),
|
||||
right_span: dt.span,
|
||||
},
|
||||
head,
|
||||
);
|
||||
}
|
||||
|
||||
let span = input.span();
|
||||
return merge_record(record, head, span).unwrap_or_else(|err| Value::error(err, span));
|
||||
}
|
||||
|
||||
// Let's try dtparse first
|
||||
if matches!(input, Value::String { .. }) && dateformat.is_none() {
|
||||
let span = input.span();
|
||||
if let Ok(input_val) = input.coerce_str() {
|
||||
if let Ok(date) = parse_date_from_string(&input_val, span) {
|
||||
return Value::date(date, span);
|
||||
}
|
||||
match parse_date_from_string(&input_val, span) {
|
||||
Ok(date) => return Value::date(date, span),
|
||||
Err(_) => {
|
||||
if let Ok(date) = from_human_time(&input_val) {
|
||||
match date {
|
||||
ParseResult::Date(date) => {
|
||||
let time = Local::now().time();
|
||||
let combined = date.and_time(time);
|
||||
let local_offset = *Local::now().offset();
|
||||
let dt_fixed =
|
||||
TimeZone::from_local_datetime(&local_offset, &combined)
|
||||
.single()
|
||||
.unwrap_or_default();
|
||||
return Value::date(dt_fixed, span);
|
||||
}
|
||||
ParseResult::DateTime(date) => {
|
||||
return Value::date(date.fixed_offset(), span)
|
||||
}
|
||||
ParseResult::Time(time) => {
|
||||
let date = Local::now().date_naive();
|
||||
let combined = date.and_time(time);
|
||||
let local_offset = *Local::now().offset();
|
||||
let dt_fixed =
|
||||
TimeZone::from_local_datetime(&local_offset, &combined)
|
||||
.single()
|
||||
.unwrap_or_default();
|
||||
return Value::date(dt_fixed, span);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
const HOUR: i32 = 60 * 60;
|
||||
|
||||
// Check to see if input looks like a Unix timestamp (i.e. can it be parsed to an int?)
|
||||
let timestamp = match input {
|
||||
@ -410,59 +403,23 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
||||
|
||||
let parse_as_string = |val: &str| {
|
||||
match dateformat {
|
||||
Some(dt_format) => match DateTime::parse_from_str(val, &dt_format.item.0) {
|
||||
Ok(dt) => {
|
||||
match timezone {
|
||||
None => {
|
||||
Value::date ( dt, head )
|
||||
},
|
||||
Some(Spanned { item, span }) => match item {
|
||||
Zone::Utc => {
|
||||
Value::date ( dt, head )
|
||||
}
|
||||
Zone::Local => {
|
||||
Value::date(dt.with_timezone(&Local).into(), *span)
|
||||
}
|
||||
Zone::East(i) => match FixedOffset::east_opt((*i as i32) * HOUR) {
|
||||
Some(eastoffset) => {
|
||||
Value::date(dt.with_timezone(&eastoffset), *span)
|
||||
}
|
||||
None => Value::error(
|
||||
ShellError::DatetimeParseError {
|
||||
msg: input.to_abbreviated_string(&nu_protocol::Config::default()),
|
||||
span: *span,
|
||||
},
|
||||
*span,
|
||||
),
|
||||
},
|
||||
Zone::West(i) => match FixedOffset::west_opt((*i as i32) * HOUR) {
|
||||
Some(westoffset) => {
|
||||
Value::date(dt.with_timezone(&westoffset), *span)
|
||||
}
|
||||
None => Value::error(
|
||||
ShellError::DatetimeParseError {
|
||||
msg: input.to_abbreviated_string(&nu_protocol::Config::default()),
|
||||
span: *span,
|
||||
},
|
||||
*span,
|
||||
),
|
||||
},
|
||||
Zone::Error => Value::error(
|
||||
// This is an argument error, not an input error
|
||||
ShellError::TypeMismatch {
|
||||
err_message: "Invalid timezone or offset".to_string(),
|
||||
span: *span,
|
||||
},
|
||||
*span,
|
||||
),
|
||||
},
|
||||
}
|
||||
},
|
||||
Some(dt) => match DateTime::parse_from_str(val, &dt.0) {
|
||||
Ok(d) => Value::date ( d, head ),
|
||||
Err(reason) => {
|
||||
parse_with_format(val, &dt_format.item.0, head).unwrap_or_else(|_| Value::error (
|
||||
ShellError::CantConvert { to_type: format!("could not parse as datetime using format '{}'", dt_format.item.0), from_type: reason.to_string(), span: head, help: Some("you can use `into datetime` without a format string to enable flexible parsing".to_string()) },
|
||||
match NaiveDateTime::parse_from_str(val, &dt.0) {
|
||||
Ok(d) => {
|
||||
let dt_fixed =
|
||||
Local.from_local_datetime(&d).single().unwrap_or_default();
|
||||
|
||||
Value::date(dt_fixed.into(),head)
|
||||
}
|
||||
Err(_) => {
|
||||
Value::error (
|
||||
ShellError::CantConvert { to_type: format!("could not parse as datetime using format '{}'", dt.0), from_type: reason.to_string(), span: head, help: Some("you can use `into datetime` without a format string to enable flexible parsing".to_string()) },
|
||||
head,
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -497,260 +454,42 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_record(record: &Record, head: Span, span: Span) -> Result<Value, ShellError> {
|
||||
if let Some(invalid_col) = record
|
||||
.columns()
|
||||
.find(|key| !ALLOWED_COLUMNS.contains(&key.as_str()))
|
||||
{
|
||||
let allowed_cols = ALLOWED_COLUMNS.join(", ");
|
||||
return Err(ShellError::UnsupportedInput {
|
||||
msg: format!(
|
||||
"Column '{invalid_col}' is not valid for a structured datetime. Allowed columns are: {allowed_cols}"
|
||||
),
|
||||
input: "value originates from here".into(),
|
||||
msg_span: head,
|
||||
input_span: span
|
||||
}
|
||||
);
|
||||
};
|
||||
fn list_human_readable_examples(span: Span) -> Value {
|
||||
let examples: Vec<String> = vec![
|
||||
"Today 18:30".into(),
|
||||
"2022-11-07 13:25:30".into(),
|
||||
"15:20 Friday".into(),
|
||||
"This Friday 17:00".into(),
|
||||
"13:25, Next Tuesday".into(),
|
||||
"Last Friday at 19:45".into(),
|
||||
"In 3 days".into(),
|
||||
"In 2 hours".into(),
|
||||
"10 hours and 5 minutes ago".into(),
|
||||
"1 years ago".into(),
|
||||
"A year ago".into(),
|
||||
"A month ago".into(),
|
||||
"A week ago".into(),
|
||||
"A day ago".into(),
|
||||
"An hour ago".into(),
|
||||
"A minute ago".into(),
|
||||
"A second ago".into(),
|
||||
"Now".into(),
|
||||
];
|
||||
|
||||
// Empty fields are filled in a specific way: the time units bigger than the biggest provided fields are assumed to be current and smaller ones are zeroed.
|
||||
// And local timezone is used if not provided.
|
||||
#[derive(Debug)]
|
||||
enum RecordColumnDefault {
|
||||
Now,
|
||||
Zero,
|
||||
}
|
||||
let mut record_column_default = RecordColumnDefault::Now;
|
||||
let records = examples
|
||||
.iter()
|
||||
.map(|s| {
|
||||
Value::record(
|
||||
record! {
|
||||
"parseable human datetime examples" => Value::test_string(s.to_string()),
|
||||
"result" => action(&Value::test_string(s.to_string()), &Arguments { zone_options: None, format_options: None, cell_paths: None }, span)
|
||||
},
|
||||
span,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<Value>>();
|
||||
|
||||
let now = Local::now();
|
||||
let mut now_nanosecond = now.nanosecond();
|
||||
let now_millisecond = now_nanosecond / 1_000_000;
|
||||
now_nanosecond %= 1_000_000;
|
||||
let now_microsecond = now_nanosecond / 1_000;
|
||||
now_nanosecond %= 1_000;
|
||||
|
||||
let year: i32 = match record.get("year") {
|
||||
Some(val) => {
|
||||
record_column_default = RecordColumnDefault::Zero;
|
||||
match val {
|
||||
Value::Int { val, .. } => *val as i32,
|
||||
other => {
|
||||
return Err(ShellError::OnlySupportsThisInputType {
|
||||
exp_input_type: "int".to_string(),
|
||||
wrong_type: other.get_type().to_string(),
|
||||
dst_span: head,
|
||||
src_span: other.span(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
None => now.year(),
|
||||
};
|
||||
let month = match record.get("month") {
|
||||
Some(col_val) => {
|
||||
record_column_default = RecordColumnDefault::Zero;
|
||||
parse_value_from_record_as_u32("month", col_val, &head, &span)?
|
||||
}
|
||||
None => match record_column_default {
|
||||
RecordColumnDefault::Now => now.month(),
|
||||
RecordColumnDefault::Zero => 1,
|
||||
},
|
||||
};
|
||||
let day = match record.get("day") {
|
||||
Some(col_val) => {
|
||||
record_column_default = RecordColumnDefault::Zero;
|
||||
parse_value_from_record_as_u32("day", col_val, &head, &span)?
|
||||
}
|
||||
None => match record_column_default {
|
||||
RecordColumnDefault::Now => now.day(),
|
||||
RecordColumnDefault::Zero => 1,
|
||||
},
|
||||
};
|
||||
let hour = match record.get("hour") {
|
||||
Some(col_val) => {
|
||||
record_column_default = RecordColumnDefault::Zero;
|
||||
parse_value_from_record_as_u32("hour", col_val, &head, &span)?
|
||||
}
|
||||
None => match record_column_default {
|
||||
RecordColumnDefault::Now => now.hour(),
|
||||
RecordColumnDefault::Zero => 0,
|
||||
},
|
||||
};
|
||||
let minute = match record.get("minute") {
|
||||
Some(col_val) => {
|
||||
record_column_default = RecordColumnDefault::Zero;
|
||||
parse_value_from_record_as_u32("minute", col_val, &head, &span)?
|
||||
}
|
||||
None => match record_column_default {
|
||||
RecordColumnDefault::Now => now.minute(),
|
||||
RecordColumnDefault::Zero => 0,
|
||||
},
|
||||
};
|
||||
let second = match record.get("second") {
|
||||
Some(col_val) => {
|
||||
record_column_default = RecordColumnDefault::Zero;
|
||||
parse_value_from_record_as_u32("second", col_val, &head, &span)?
|
||||
}
|
||||
None => match record_column_default {
|
||||
RecordColumnDefault::Now => now.second(),
|
||||
RecordColumnDefault::Zero => 0,
|
||||
},
|
||||
};
|
||||
let millisecond = match record.get("millisecond") {
|
||||
Some(col_val) => {
|
||||
record_column_default = RecordColumnDefault::Zero;
|
||||
parse_value_from_record_as_u32("millisecond", col_val, &head, &span)?
|
||||
}
|
||||
None => match record_column_default {
|
||||
RecordColumnDefault::Now => now_millisecond,
|
||||
RecordColumnDefault::Zero => 0,
|
||||
},
|
||||
};
|
||||
let microsecond = match record.get("microsecond") {
|
||||
Some(col_val) => {
|
||||
record_column_default = RecordColumnDefault::Zero;
|
||||
parse_value_from_record_as_u32("microsecond", col_val, &head, &span)?
|
||||
}
|
||||
None => match record_column_default {
|
||||
RecordColumnDefault::Now => now_microsecond,
|
||||
RecordColumnDefault::Zero => 0,
|
||||
},
|
||||
};
|
||||
|
||||
let nanosecond = match record.get("nanosecond") {
|
||||
Some(col_val) => parse_value_from_record_as_u32("nanosecond", col_val, &head, &span)?,
|
||||
None => match record_column_default {
|
||||
RecordColumnDefault::Now => now_nanosecond,
|
||||
RecordColumnDefault::Zero => 0,
|
||||
},
|
||||
};
|
||||
|
||||
let offset: FixedOffset = match record.get("timezone") {
|
||||
Some(timezone) => parse_timezone_from_record(timezone, &head, &timezone.span())?,
|
||||
None => now.offset().to_owned(),
|
||||
};
|
||||
|
||||
let total_nanoseconds = nanosecond + microsecond * 1_000 + millisecond * 1_000_000;
|
||||
|
||||
let date = match NaiveDate::from_ymd_opt(year, month, day) {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
return Err(ShellError::IncorrectValue {
|
||||
msg: "one of more values are incorrect and do not represent valid date".to_string(),
|
||||
val_span: head,
|
||||
call_span: span,
|
||||
})
|
||||
}
|
||||
};
|
||||
let time = match NaiveTime::from_hms_nano_opt(hour, minute, second, total_nanoseconds) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return Err(ShellError::IncorrectValue {
|
||||
msg: "one of more values are incorrect and do not represent valid time".to_string(),
|
||||
val_span: head,
|
||||
call_span: span,
|
||||
})
|
||||
}
|
||||
};
|
||||
let date_time = NaiveDateTime::new(date, time);
|
||||
|
||||
let date_time_fixed = match offset.from_local_datetime(&date_time).single() {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
return Err(ShellError::IncorrectValue {
|
||||
msg: "Ambiguous or invalid timezone conversion".to_string(),
|
||||
val_span: head,
|
||||
call_span: span,
|
||||
})
|
||||
}
|
||||
};
|
||||
Ok(Value::date(date_time_fixed, span))
|
||||
}
|
||||
|
||||
fn parse_value_from_record_as_u32(
|
||||
col: &str,
|
||||
col_val: &Value,
|
||||
head: &Span,
|
||||
span: &Span,
|
||||
) -> Result<u32, ShellError> {
|
||||
let value: u32 = match col_val {
|
||||
Value::Int { val, .. } => {
|
||||
if *val < 0 || *val > u32::MAX as i64 {
|
||||
return Err(ShellError::IncorrectValue {
|
||||
msg: format!("incorrect value for {}", col),
|
||||
val_span: *head,
|
||||
call_span: *span,
|
||||
});
|
||||
}
|
||||
*val as u32
|
||||
}
|
||||
other => {
|
||||
return Err(ShellError::OnlySupportsThisInputType {
|
||||
exp_input_type: "int".to_string(),
|
||||
wrong_type: other.get_type().to_string(),
|
||||
dst_span: *head,
|
||||
src_span: other.span(),
|
||||
});
|
||||
}
|
||||
};
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn parse_timezone_from_record(
|
||||
timezone: &Value,
|
||||
head: &Span,
|
||||
span: &Span,
|
||||
) -> Result<FixedOffset, ShellError> {
|
||||
match timezone {
|
||||
Value::String { val, .. } => {
|
||||
let offset: FixedOffset = match val.parse() {
|
||||
Ok(offset) => offset,
|
||||
Err(_) => {
|
||||
return Err(ShellError::IncorrectValue {
|
||||
msg: "invalid timezone".to_string(),
|
||||
val_span: *span,
|
||||
call_span: *head,
|
||||
})
|
||||
}
|
||||
};
|
||||
Ok(offset)
|
||||
}
|
||||
other => Err(ShellError::OnlySupportsThisInputType {
|
||||
exp_input_type: "string".to_string(),
|
||||
wrong_type: other.get_type().to_string(),
|
||||
dst_span: *head,
|
||||
src_span: other.span(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_with_format(val: &str, fmt: &str, head: Span) -> Result<Value, ()> {
|
||||
// try parsing at date + time
|
||||
if let Ok(dt) = NaiveDateTime::parse_from_str(val, fmt) {
|
||||
let dt_native = Local.from_local_datetime(&dt).single().unwrap_or_default();
|
||||
return Ok(Value::date(dt_native.into(), head));
|
||||
}
|
||||
|
||||
// try parsing at date only
|
||||
if let Ok(date) = NaiveDate::parse_from_str(val, fmt) {
|
||||
if let Some(dt) = date.and_hms_opt(0, 0, 0) {
|
||||
let dt_native = Local.from_local_datetime(&dt).single().unwrap_or_default();
|
||||
return Ok(Value::date(dt_native.into(), head));
|
||||
}
|
||||
}
|
||||
|
||||
// try parsing at time only
|
||||
if let Ok(time) = NaiveTime::parse_from_str(val, fmt) {
|
||||
let now = Local::now().naive_local().date();
|
||||
let dt_native = Local
|
||||
.from_local_datetime(&now.and_time(time))
|
||||
.single()
|
||||
.unwrap_or_default();
|
||||
return Ok(Value::date(dt_native.into(), head));
|
||||
}
|
||||
|
||||
Err(())
|
||||
Value::list(records, span)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -769,10 +508,7 @@ mod tests {
|
||||
#[test]
|
||||
fn takes_a_date_format_with_timezone() {
|
||||
let date_str = Value::test_string("16.11.1984 8:00 am +0000");
|
||||
let fmt_options = Some(Spanned {
|
||||
item: DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()),
|
||||
span: Span::test_data(),
|
||||
});
|
||||
let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()));
|
||||
let args = Arguments {
|
||||
zone_options: None,
|
||||
format_options: fmt_options,
|
||||
@ -787,12 +523,16 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn takes_a_date_format_without_timezone() {
|
||||
// Ignoring this test for now because we changed the human-date-parser to use
|
||||
// the users timezone instead of UTC. We may continue to tweak this behavior.
|
||||
// Another hacky solution is to set the timezone to UTC in the test, which works
|
||||
// on MacOS and Linux but hasn't been tested on Windows. Plus it kind of defeats
|
||||
// the purpose of a "without_timezone" test.
|
||||
// std::env::set_var("TZ", "UTC");
|
||||
let date_str = Value::test_string("16.11.1984 8:00 am");
|
||||
let fmt_options = Some(Spanned {
|
||||
item: DatetimeFormat("%d.%m.%Y %H:%M %P".to_string()),
|
||||
span: Span::test_data(),
|
||||
});
|
||||
let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P".to_string()));
|
||||
let args = Arguments {
|
||||
zone_options: None,
|
||||
format_options: fmt_options,
|
||||
@ -874,10 +614,7 @@ mod tests {
|
||||
#[test]
|
||||
fn takes_int_with_formatstring() {
|
||||
let date_int = Value::test_int(1_614_434_140);
|
||||
let fmt_options = Some(Spanned {
|
||||
item: DatetimeFormat("%s".to_string()),
|
||||
span: Span::test_data(),
|
||||
});
|
||||
let fmt_options = Some(DatetimeFormat("%s".to_string()));
|
||||
let args = Arguments {
|
||||
zone_options: None,
|
||||
format_options: fmt_options,
|
||||
@ -892,55 +629,6 @@ mod tests {
|
||||
assert_eq!(actual, expected)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn takes_timestamp_offset_as_int_with_formatting() {
|
||||
let date_int = Value::test_int(1_614_434_140);
|
||||
let timezone_option = Some(Spanned {
|
||||
item: Zone::East(8),
|
||||
span: Span::test_data(),
|
||||
});
|
||||
let fmt_options = Some(Spanned {
|
||||
item: DatetimeFormat("%s".to_string()),
|
||||
span: Span::test_data(),
|
||||
});
|
||||
let args = Arguments {
|
||||
zone_options: timezone_option,
|
||||
format_options: fmt_options,
|
||||
cell_paths: None,
|
||||
};
|
||||
let actual = action(&date_int, &args, Span::test_data());
|
||||
let expected = Value::date(
|
||||
DateTime::parse_from_str("2021-02-27 21:55:40 +08:00", "%Y-%m-%d %H:%M:%S %z").unwrap(),
|
||||
Span::test_data(),
|
||||
);
|
||||
|
||||
assert_eq!(actual, expected)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn takes_timestamp_offset_as_int_with_local_timezone() {
|
||||
let date_int = Value::test_int(1_614_434_140);
|
||||
let timezone_option = Some(Spanned {
|
||||
item: Zone::Local,
|
||||
span: Span::test_data(),
|
||||
});
|
||||
let fmt_options = Some(Spanned {
|
||||
item: DatetimeFormat("%s".to_string()),
|
||||
span: Span::test_data(),
|
||||
});
|
||||
let args = Arguments {
|
||||
zone_options: timezone_option,
|
||||
format_options: fmt_options,
|
||||
cell_paths: None,
|
||||
};
|
||||
let actual = action(&date_int, &args, Span::test_data());
|
||||
let expected = Value::date(
|
||||
Utc.timestamp_opt(1_614_434_140, 0).unwrap().into(),
|
||||
Span::test_data(),
|
||||
);
|
||||
assert_eq!(actual, expected)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn takes_timestamp() {
|
||||
let date_str = Value::test_string("1614434140000000000");
|
||||
@ -955,7 +643,7 @@ mod tests {
|
||||
};
|
||||
let actual = action(&date_str, &args, Span::test_data());
|
||||
let expected = Value::date(
|
||||
Local.timestamp_opt(1_614_434_140, 0).unwrap().into(),
|
||||
Local.timestamp_opt(1614434140, 0).unwrap().into(),
|
||||
Span::test_data(),
|
||||
);
|
||||
|
||||
@ -974,7 +662,7 @@ mod tests {
|
||||
cell_paths: None,
|
||||
};
|
||||
let expected = Value::date(
|
||||
Local.timestamp_opt(1_614_434_140, 0).unwrap().into(),
|
||||
Local.timestamp_opt(1614434140, 0).unwrap().into(),
|
||||
Span::test_data(),
|
||||
);
|
||||
let actual = action(&expected, &args, Span::test_data());
|
||||
@ -993,7 +681,7 @@ mod tests {
|
||||
let actual = action(&date_str, &args, Span::test_data());
|
||||
|
||||
let expected = Value::date(
|
||||
Utc.timestamp_opt(1_614_434_140, 0).unwrap().into(),
|
||||
Utc.timestamp_opt(1614434140, 0).unwrap().into(),
|
||||
Span::test_data(),
|
||||
);
|
||||
|
||||
@ -1003,10 +691,7 @@ mod tests {
|
||||
#[test]
|
||||
fn communicates_parsing_error_given_an_invalid_datetimelike_string() {
|
||||
let date_str = Value::test_string("16.11.1984 8:00 am Oops0000");
|
||||
let fmt_options = Some(Spanned {
|
||||
item: DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()),
|
||||
span: Span::test_data(),
|
||||
});
|
||||
let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()));
|
||||
let args = Arguments {
|
||||
zone_options: None,
|
||||
format_options: fmt_options,
|
||||
|
@ -1,41 +1,8 @@
|
||||
use nu_cmd_base::input_handler::{operate, CmdArgument};
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_parser::{parse_unit_value, DURATION_UNIT_GROUPS};
|
||||
use nu_protocol::{ast::Expr, Unit};
|
||||
|
||||
const NS_PER_US: i64 = 1_000;
|
||||
const NS_PER_MS: i64 = 1_000_000;
|
||||
const NS_PER_SEC: i64 = 1_000_000_000;
|
||||
const NS_PER_MINUTE: i64 = 60 * NS_PER_SEC;
|
||||
const NS_PER_HOUR: i64 = 60 * NS_PER_MINUTE;
|
||||
const NS_PER_DAY: i64 = 24 * NS_PER_HOUR;
|
||||
const NS_PER_WEEK: i64 = 7 * NS_PER_DAY;
|
||||
|
||||
const ALLOWED_COLUMNS: [&str; 9] = [
|
||||
"week",
|
||||
"day",
|
||||
"hour",
|
||||
"minute",
|
||||
"second",
|
||||
"millisecond",
|
||||
"microsecond",
|
||||
"nanosecond",
|
||||
"sign",
|
||||
];
|
||||
const ALLOWED_SIGNS: [&str; 2] = ["+", "-"];
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Arguments {
|
||||
unit: Option<Spanned<String>>,
|
||||
cell_paths: Option<Vec<CellPath>>,
|
||||
}
|
||||
|
||||
impl CmdArgument for Arguments {
|
||||
fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
|
||||
self.cell_paths.take()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IntoDuration;
|
||||
|
||||
@ -48,17 +15,13 @@ impl Command for IntoDuration {
|
||||
Signature::build("into duration")
|
||||
.input_output_types(vec![
|
||||
(Type::Int, Type::Duration),
|
||||
(Type::Float, Type::Duration),
|
||||
(Type::String, Type::Duration),
|
||||
(Type::Duration, Type::Duration),
|
||||
// FIXME: https://github.com/nushell/nushell/issues/15485
|
||||
// 'record -> any' was added as a temporary workaround to avoid type inference issues. The Any arm needs to be appear first.
|
||||
(Type::record(), Type::Any),
|
||||
(Type::record(), Type::record()),
|
||||
(Type::record(), Type::Duration),
|
||||
(Type::table(), Type::table()),
|
||||
//todo: record<hour,minute,sign> | into duration -> Duration
|
||||
//(Type::record(), Type::record()),
|
||||
])
|
||||
.allow_variants_without_examples(true)
|
||||
//.allow_variants_without_examples(true)
|
||||
.named(
|
||||
"unit",
|
||||
SyntaxShape::String,
|
||||
@ -92,35 +55,7 @@ impl Command for IntoDuration {
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let cell_paths = call.rest(engine_state, stack, 0)?;
|
||||
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
|
||||
|
||||
let span = match input.span() {
|
||||
Some(t) => t,
|
||||
None => call.head,
|
||||
};
|
||||
let unit = match call.get_flag::<Spanned<String>>(engine_state, stack, "unit")? {
|
||||
Some(spanned_unit) => {
|
||||
if ["ns", "us", "µs", "ms", "sec", "min", "hr", "day", "wk"]
|
||||
.contains(&spanned_unit.item.as_str())
|
||||
{
|
||||
Some(spanned_unit)
|
||||
} else {
|
||||
return Err(ShellError::CantConvertToDuration {
|
||||
details: spanned_unit.item,
|
||||
dst_span: span,
|
||||
src_span: span,
|
||||
help: Some(
|
||||
"supported units are ns, us/µs, ms, sec, min, hr, day, and wk"
|
||||
.to_string(),
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let args = Arguments { unit, cell_paths };
|
||||
operate(action, args, input, call.head, engine_state.signals())
|
||||
into_duration(engine_state, stack, call, input)
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
@ -174,23 +109,67 @@ impl Command for IntoDuration {
|
||||
example: "1_234 | into duration --unit ms",
|
||||
result: Some(Value::test_duration(1_234 * 1_000_000)),
|
||||
},
|
||||
Example {
|
||||
description: "Convert a floating point number of an arbitrary unit to duration",
|
||||
example: "1.234 | into duration --unit sec",
|
||||
result: Some(Value::test_duration(1_234 * 1_000_000)),
|
||||
},
|
||||
Example {
|
||||
description: "Convert a record to a duration",
|
||||
example: "{day: 10, hour: 2, minute: 6, second: 50, sign: '+'} | into duration",
|
||||
result: Some(Value::duration(
|
||||
10 * NS_PER_DAY + 2 * NS_PER_HOUR + 6 * NS_PER_MINUTE + 50 * NS_PER_SEC,
|
||||
Span::test_data(),
|
||||
)),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn into_duration(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let span = match input.span() {
|
||||
Some(t) => t,
|
||||
None => call.head,
|
||||
};
|
||||
let column_paths: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
|
||||
|
||||
let unit = match call.get_flag::<String>(engine_state, stack, "unit")? {
|
||||
Some(sep) => {
|
||||
if ["ns", "us", "µs", "ms", "sec", "min", "hr", "day", "wk"]
|
||||
.iter()
|
||||
.any(|d| d == &sep)
|
||||
{
|
||||
sep
|
||||
} else {
|
||||
return Err(ShellError::CantConvertToDuration {
|
||||
details: sep,
|
||||
dst_span: span,
|
||||
src_span: span,
|
||||
help: Some(
|
||||
"supported units are ns, us/µs, ms, sec, min, hr, day, and wk".to_string(),
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
None => "ns".to_string(),
|
||||
};
|
||||
|
||||
input.map(
|
||||
move |v| {
|
||||
if column_paths.is_empty() {
|
||||
action(&v, &unit.clone(), span)
|
||||
} else {
|
||||
let unitclone = &unit.clone();
|
||||
let mut ret = v;
|
||||
for path in &column_paths {
|
||||
let r = ret.update_cell_path(
|
||||
&path.members,
|
||||
Box::new(move |old| action(old, unitclone, span)),
|
||||
);
|
||||
if let Err(error) = r {
|
||||
return Value::error(error, span);
|
||||
}
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
},
|
||||
engine_state.signals(),
|
||||
)
|
||||
}
|
||||
|
||||
fn split_whitespace_indices(s: &str, span: Span) -> impl Iterator<Item = (&str, Span)> {
|
||||
s.split_whitespace().map(move |sub| {
|
||||
// Gets the offset of the `sub` substring inside the string `s`.
|
||||
@ -253,51 +232,27 @@ fn string_to_duration(s: &str, span: Span) -> Result<i64, ShellError> {
|
||||
})
|
||||
}
|
||||
|
||||
fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
||||
fn action(input: &Value, unit: &str, span: Span) -> Value {
|
||||
let value_span = input.span();
|
||||
let unit_option = &args.unit;
|
||||
|
||||
if let Value::Record { .. } | Value::Duration { .. } = input {
|
||||
if let Some(unit) = unit_option {
|
||||
return Value::error(
|
||||
ShellError::IncompatibleParameters {
|
||||
left_message: "got a record as input".into(),
|
||||
left_span: head,
|
||||
right_message: "the units should be included in the record".into(),
|
||||
right_span: unit.span,
|
||||
},
|
||||
head,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let unit: &str = match unit_option {
|
||||
Some(unit) => &unit.item,
|
||||
None => "ns",
|
||||
};
|
||||
|
||||
match input {
|
||||
Value::Duration { .. } => input.clone(),
|
||||
Value::Record { val, .. } => {
|
||||
merge_record(val, head, value_span).unwrap_or_else(|err| Value::error(err, value_span))
|
||||
}
|
||||
Value::String { val, .. } => {
|
||||
if let Ok(num) = val.parse::<f64>() {
|
||||
let ns = unit_to_ns_factor(unit);
|
||||
return Value::duration((num * (ns as f64)) as i64, head);
|
||||
}
|
||||
match compound_to_duration(val, value_span) {
|
||||
Ok(val) => Value::duration(val, head),
|
||||
Err(error) => Value::error(error, head),
|
||||
}
|
||||
}
|
||||
Value::Float { val, .. } => {
|
||||
let ns = unit_to_ns_factor(unit);
|
||||
Value::duration((*val * (ns as f64)) as i64, head)
|
||||
}
|
||||
Value::String { val, .. } => match compound_to_duration(val, value_span) {
|
||||
Ok(val) => Value::duration(val, span),
|
||||
Err(error) => Value::error(error, span),
|
||||
},
|
||||
Value::Int { val, .. } => {
|
||||
let ns = unit_to_ns_factor(unit);
|
||||
Value::duration(*val * ns, head)
|
||||
let ns = match unit {
|
||||
"ns" => 1,
|
||||
"us" | "µs" => 1_000,
|
||||
"ms" => 1_000_000,
|
||||
"sec" => NS_PER_SEC,
|
||||
"min" => NS_PER_SEC * 60,
|
||||
"hr" => NS_PER_SEC * 60 * 60,
|
||||
"day" => NS_PER_SEC * 60 * 60 * 24,
|
||||
"wk" => NS_PER_SEC * 60 * 60 * 24 * 7,
|
||||
_ => 0,
|
||||
};
|
||||
Value::duration(*val * ns, span)
|
||||
}
|
||||
// Propagate errors by explicitly matching them before the final case.
|
||||
Value::Error { .. } => input.clone(),
|
||||
@ -305,134 +260,14 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
||||
ShellError::OnlySupportsThisInputType {
|
||||
exp_input_type: "string or duration".into(),
|
||||
wrong_type: other.get_type().to_string(),
|
||||
dst_span: head,
|
||||
dst_span: span,
|
||||
src_span: other.span(),
|
||||
},
|
||||
head,
|
||||
span,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_record(record: &Record, head: Span, span: Span) -> Result<Value, ShellError> {
|
||||
if let Some(invalid_col) = record
|
||||
.columns()
|
||||
.find(|key| !ALLOWED_COLUMNS.contains(&key.as_str()))
|
||||
{
|
||||
let allowed_cols = ALLOWED_COLUMNS.join(", ");
|
||||
return Err(ShellError::UnsupportedInput {
|
||||
msg: format!(
|
||||
"Column '{invalid_col}' is not valid for a structured duration. Allowed columns are: {allowed_cols}"
|
||||
),
|
||||
input: "value originates from here".into(),
|
||||
msg_span: head,
|
||||
input_span: span
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
let mut duration: i64 = 0;
|
||||
|
||||
if let Some(col_val) = record.get("week") {
|
||||
let week = parse_number_from_record(col_val, &head)?;
|
||||
duration += week * NS_PER_WEEK;
|
||||
};
|
||||
if let Some(col_val) = record.get("day") {
|
||||
let day = parse_number_from_record(col_val, &head)?;
|
||||
duration += day * NS_PER_DAY;
|
||||
};
|
||||
if let Some(col_val) = record.get("hour") {
|
||||
let hour = parse_number_from_record(col_val, &head)?;
|
||||
duration += hour * NS_PER_HOUR;
|
||||
};
|
||||
if let Some(col_val) = record.get("minute") {
|
||||
let minute = parse_number_from_record(col_val, &head)?;
|
||||
duration += minute * NS_PER_MINUTE;
|
||||
};
|
||||
if let Some(col_val) = record.get("second") {
|
||||
let second = parse_number_from_record(col_val, &head)?;
|
||||
duration += second * NS_PER_SEC;
|
||||
};
|
||||
if let Some(col_val) = record.get("millisecond") {
|
||||
let millisecond = parse_number_from_record(col_val, &head)?;
|
||||
duration += millisecond * NS_PER_MS;
|
||||
};
|
||||
if let Some(col_val) = record.get("microsecond") {
|
||||
let microsecond = parse_number_from_record(col_val, &head)?;
|
||||
duration += microsecond * NS_PER_US;
|
||||
};
|
||||
if let Some(col_val) = record.get("nanosecond") {
|
||||
let nanosecond = parse_number_from_record(col_val, &head)?;
|
||||
duration += nanosecond;
|
||||
};
|
||||
|
||||
if let Some(sign) = record.get("sign") {
|
||||
match sign {
|
||||
Value::String { val, .. } => {
|
||||
if !ALLOWED_SIGNS.contains(&val.as_str()) {
|
||||
let allowed_signs = ALLOWED_SIGNS.join(", ");
|
||||
return Err(ShellError::IncorrectValue {
|
||||
msg: format!("Invalid sign. Allowed signs are {}", allowed_signs)
|
||||
.to_string(),
|
||||
val_span: sign.span(),
|
||||
call_span: head,
|
||||
});
|
||||
}
|
||||
if val == "-" {
|
||||
duration = -duration;
|
||||
}
|
||||
}
|
||||
other => {
|
||||
return Err(ShellError::OnlySupportsThisInputType {
|
||||
exp_input_type: "int".to_string(),
|
||||
wrong_type: other.get_type().to_string(),
|
||||
dst_span: head,
|
||||
src_span: other.span(),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Value::duration(duration, span))
|
||||
}
|
||||
|
||||
fn parse_number_from_record(col_val: &Value, head: &Span) -> Result<i64, ShellError> {
|
||||
let value = match col_val {
|
||||
Value::Int { val, .. } => {
|
||||
if *val < 0 {
|
||||
return Err(ShellError::IncorrectValue {
|
||||
msg: "number should be positive".to_string(),
|
||||
val_span: col_val.span(),
|
||||
call_span: *head,
|
||||
});
|
||||
}
|
||||
*val
|
||||
}
|
||||
other => {
|
||||
return Err(ShellError::OnlySupportsThisInputType {
|
||||
exp_input_type: "int".to_string(),
|
||||
wrong_type: other.get_type().to_string(),
|
||||
dst_span: *head,
|
||||
src_span: other.span(),
|
||||
});
|
||||
}
|
||||
};
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn unit_to_ns_factor(unit: &str) -> i64 {
|
||||
match unit {
|
||||
"ns" => 1,
|
||||
"us" | "µs" => NS_PER_US,
|
||||
"ms" => NS_PER_MS,
|
||||
"sec" => NS_PER_SEC,
|
||||
"min" => NS_PER_MINUTE,
|
||||
"hr" => NS_PER_HOUR,
|
||||
"day" => NS_PER_DAY,
|
||||
"wk" => NS_PER_WEEK,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
@ -449,27 +284,24 @@ mod test {
|
||||
|
||||
#[rstest]
|
||||
#[case("3ns", 3)]
|
||||
#[case("4us", 4 * NS_PER_US)]
|
||||
#[case("4\u{00B5}s", 4 * NS_PER_US)] // micro sign
|
||||
#[case("4\u{03BC}s", 4 * NS_PER_US)] // mu symbol
|
||||
#[case("5ms", 5 * NS_PER_MS)]
|
||||
#[case("4us", 4*1000)]
|
||||
#[case("4\u{00B5}s", 4*1000)] // micro sign
|
||||
#[case("4\u{03BC}s", 4*1000)] // mu symbol
|
||||
#[case("5ms", 5 * 1000 * 1000)]
|
||||
#[case("1sec", NS_PER_SEC)]
|
||||
#[case("7min", 7 * NS_PER_MINUTE)]
|
||||
#[case("42hr", 42 * NS_PER_HOUR)]
|
||||
#[case("123day", 123 * NS_PER_DAY)]
|
||||
#[case("3wk", 3 * NS_PER_WEEK)]
|
||||
#[case("7min", 7 * 60 * NS_PER_SEC)]
|
||||
#[case("42hr", 42 * 60 * 60 * NS_PER_SEC)]
|
||||
#[case("123day", 123 * 24 * 60 * 60 * NS_PER_SEC)]
|
||||
#[case("3wk", 3 * 7 * 24 * 60 * 60 * NS_PER_SEC)]
|
||||
#[case("86hr 26ns", 86 * 3600 * NS_PER_SEC + 26)] // compound duration string
|
||||
#[case("14ns 3hr 17sec", 14 + 3 * NS_PER_HOUR + 17 * NS_PER_SEC)] // compound string with units in random order
|
||||
#[case("14ns 3hr 17sec", 14 + 3 * 3600 * NS_PER_SEC + 17 * NS_PER_SEC)] // compound string with units in random order
|
||||
|
||||
fn turns_string_to_duration(#[case] phrase: &str, #[case] expected_duration_val: i64) {
|
||||
let args = Arguments {
|
||||
unit: Some(Spanned {
|
||||
item: "ns".to_string(),
|
||||
span: Span::test_data(),
|
||||
}),
|
||||
cell_paths: None,
|
||||
};
|
||||
let actual = action(&Value::test_string(phrase), &args, Span::test_data());
|
||||
let actual = action(
|
||||
&Value::test_string(phrase),
|
||||
"ns",
|
||||
Span::new(0, phrase.len()),
|
||||
);
|
||||
match actual {
|
||||
Value::Duration {
|
||||
val: observed_val, ..
|
||||
|
@ -208,7 +208,7 @@ fn process_cell(val: Value, display_as_filesizes: bool, span: Span) -> Result<Va
|
||||
}
|
||||
} else if DATETIME_DMY_RE.is_match(&val_str).unwrap_or(false) {
|
||||
let dt = parse_date_from_string(&val_str, span).map_err(|_| ShellError::CantConvert {
|
||||
to_type: "datetime".to_string(),
|
||||
to_type: "date".to_string(),
|
||||
from_type: "string".to_string(),
|
||||
span,
|
||||
help: Some(format!(
|
||||
@ -219,7 +219,7 @@ fn process_cell(val: Value, display_as_filesizes: bool, span: Span) -> Result<Va
|
||||
Ok(Value::date(dt, span))
|
||||
} else if DATETIME_YMD_RE.is_match(&val_str).unwrap_or(false) {
|
||||
let dt = parse_date_from_string(&val_str, span).map_err(|_| ShellError::CantConvert {
|
||||
to_type: "datetime".to_string(),
|
||||
to_type: "date".to_string(),
|
||||
from_type: "string".to_string(),
|
||||
span,
|
||||
help: Some(format!(
|
||||
@ -230,7 +230,7 @@ fn process_cell(val: Value, display_as_filesizes: bool, span: Span) -> Result<Va
|
||||
Ok(Value::date(dt, span))
|
||||
} else if DATETIME_YMDZ_RE.is_match(&val_str).unwrap_or(false) {
|
||||
let dt = parse_date_from_string(&val_str, span).map_err(|_| ShellError::CantConvert {
|
||||
to_type: "datetime".to_string(),
|
||||
to_type: "date".to_string(),
|
||||
from_type: "string".to_string(),
|
||||
span,
|
||||
help: Some(format!(
|
||||
|
@ -40,7 +40,6 @@ impl Command for SplitCellPath {
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
let input_type = input.get_type();
|
||||
|
||||
let src_span = match input {
|
||||
// Early return on correct type and empty pipeline
|
||||
@ -55,9 +54,8 @@ impl Command for SplitCellPath {
|
||||
PipelineData::ListStream(stream, ..) => stream.span(),
|
||||
PipelineData::ByteStream(stream, ..) => stream.span(),
|
||||
};
|
||||
Err(ShellError::OnlySupportsThisInputType {
|
||||
Err(ShellError::PipelineMismatch {
|
||||
exp_input_type: "cell-path".into(),
|
||||
wrong_type: input_type.to_string(),
|
||||
dst_span: head,
|
||||
src_span,
|
||||
})
|
||||
|
@ -1,261 +0,0 @@
|
||||
use chrono::{Local, TimeZone};
|
||||
use human_date_parser::{from_human_time, ParseResult};
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DateFromHuman;
|
||||
|
||||
impl Command for DateFromHuman {
|
||||
fn name(&self) -> &str {
|
||||
"date from-human"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("date from-human")
|
||||
.input_output_types(vec![
|
||||
(Type::String, Type::Date),
|
||||
(Type::Nothing, Type::table()),
|
||||
])
|
||||
.allow_variants_without_examples(true)
|
||||
.switch(
|
||||
"list",
|
||||
"Show human-readable datetime parsing examples",
|
||||
Some('l'),
|
||||
)
|
||||
.category(Category::Date)
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Convert a human readable datetime string to a datetime."
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec![
|
||||
"relative",
|
||||
"now",
|
||||
"today",
|
||||
"tomorrow",
|
||||
"yesterday",
|
||||
"weekday",
|
||||
"weekday_name",
|
||||
"timezone",
|
||||
]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
if call.has_flag(engine_state, stack, "list")? {
|
||||
return Ok(list_human_readable_examples(call.head).into_pipeline_data());
|
||||
}
|
||||
let head = call.head;
|
||||
// This doesn't match explicit nulls
|
||||
if matches!(input, PipelineData::Empty) {
|
||||
return Err(ShellError::PipelineEmpty { dst_span: head });
|
||||
}
|
||||
input.map(move |value| helper(value, head), engine_state.signals())
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Parsing human readable datetime",
|
||||
example: "'Today at 18:30' | date from-human",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Parsing human readable datetime",
|
||||
example: "'Last Friday at 19:45' | date from-human",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Parsing human readable datetime",
|
||||
example: "'In 5 minutes and 30 seconds' | date from-human",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "PShow human-readable datetime parsing examples",
|
||||
example: "date from-human --list",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn helper(value: Value, head: Span) -> Value {
|
||||
let span = value.span();
|
||||
let input_val = match value {
|
||||
Value::String { val, .. } => val,
|
||||
other => {
|
||||
return Value::error(
|
||||
ShellError::OnlySupportsThisInputType {
|
||||
exp_input_type: "string".to_string(),
|
||||
wrong_type: other.get_type().to_string(),
|
||||
dst_span: head,
|
||||
src_span: span,
|
||||
},
|
||||
span,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let now = Local::now();
|
||||
|
||||
if let Ok(date) = from_human_time(&input_val, now.naive_local()) {
|
||||
match date {
|
||||
ParseResult::Date(date) => {
|
||||
let time = now.time();
|
||||
let combined = date.and_time(time);
|
||||
let local_offset = *now.offset();
|
||||
let dt_fixed = TimeZone::from_local_datetime(&local_offset, &combined)
|
||||
.single()
|
||||
.unwrap_or_default();
|
||||
return Value::date(dt_fixed, span);
|
||||
}
|
||||
ParseResult::DateTime(date) => {
|
||||
let local_offset = *now.offset();
|
||||
let dt_fixed = match local_offset.from_local_datetime(&date) {
|
||||
chrono::LocalResult::Single(dt) => dt,
|
||||
chrono::LocalResult::Ambiguous(_, _) => {
|
||||
return Value::error(
|
||||
ShellError::DatetimeParseError {
|
||||
msg: "Ambiguous datetime".to_string(),
|
||||
span,
|
||||
},
|
||||
span,
|
||||
);
|
||||
}
|
||||
chrono::LocalResult::None => {
|
||||
return Value::error(
|
||||
ShellError::DatetimeParseError {
|
||||
msg: "Invalid datetime".to_string(),
|
||||
span,
|
||||
},
|
||||
span,
|
||||
);
|
||||
}
|
||||
};
|
||||
return Value::date(dt_fixed, span);
|
||||
}
|
||||
ParseResult::Time(time) => {
|
||||
let date = now.date_naive();
|
||||
let combined = date.and_time(time);
|
||||
let local_offset = *now.offset();
|
||||
let dt_fixed = TimeZone::from_local_datetime(&local_offset, &combined)
|
||||
.single()
|
||||
.unwrap_or_default();
|
||||
return Value::date(dt_fixed, span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match from_human_time(&input_val, now.naive_local()) {
|
||||
Ok(date) => match date {
|
||||
ParseResult::Date(date) => {
|
||||
let time = now.time();
|
||||
let combined = date.and_time(time);
|
||||
let local_offset = *now.offset();
|
||||
let dt_fixed = TimeZone::from_local_datetime(&local_offset, &combined)
|
||||
.single()
|
||||
.unwrap_or_default();
|
||||
Value::date(dt_fixed, span)
|
||||
}
|
||||
ParseResult::DateTime(date) => {
|
||||
let local_offset = *now.offset();
|
||||
let dt_fixed = match local_offset.from_local_datetime(&date) {
|
||||
chrono::LocalResult::Single(dt) => dt,
|
||||
chrono::LocalResult::Ambiguous(_, _) => {
|
||||
return Value::error(
|
||||
ShellError::DatetimeParseError {
|
||||
msg: "Ambiguous datetime".to_string(),
|
||||
span,
|
||||
},
|
||||
span,
|
||||
);
|
||||
}
|
||||
chrono::LocalResult::None => {
|
||||
return Value::error(
|
||||
ShellError::DatetimeParseError {
|
||||
msg: "Invalid datetime".to_string(),
|
||||
span,
|
||||
},
|
||||
span,
|
||||
);
|
||||
}
|
||||
};
|
||||
Value::date(dt_fixed, span)
|
||||
}
|
||||
ParseResult::Time(time) => {
|
||||
let date = now.date_naive();
|
||||
let combined = date.and_time(time);
|
||||
let local_offset = *now.offset();
|
||||
let dt_fixed = TimeZone::from_local_datetime(&local_offset, &combined)
|
||||
.single()
|
||||
.unwrap_or_default();
|
||||
Value::date(dt_fixed, span)
|
||||
}
|
||||
},
|
||||
Err(_) => Value::error(
|
||||
ShellError::IncorrectValue {
|
||||
msg: "Cannot parse as humanized date".to_string(),
|
||||
val_span: head,
|
||||
call_span: span,
|
||||
},
|
||||
span,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn list_human_readable_examples(span: Span) -> Value {
|
||||
let examples: Vec<String> = vec![
|
||||
"Today 18:30".into(),
|
||||
"2022-11-07 13:25:30".into(),
|
||||
"15:20 Friday".into(),
|
||||
"This Friday 17:00".into(),
|
||||
"13:25, Next Tuesday".into(),
|
||||
"Last Friday at 19:45".into(),
|
||||
"In 3 days".into(),
|
||||
"In 2 hours".into(),
|
||||
"10 hours and 5 minutes ago".into(),
|
||||
"1 years ago".into(),
|
||||
"A year ago".into(),
|
||||
"A month ago".into(),
|
||||
"A week ago".into(),
|
||||
"A day ago".into(),
|
||||
"An hour ago".into(),
|
||||
"A minute ago".into(),
|
||||
"A second ago".into(),
|
||||
"Now".into(),
|
||||
];
|
||||
|
||||
let records = examples
|
||||
.iter()
|
||||
.map(|s| {
|
||||
Value::record(
|
||||
record! {
|
||||
"parseable human datetime examples" => Value::test_string(s.to_string()),
|
||||
"result" => helper(Value::test_string(s.to_string()), span),
|
||||
},
|
||||
span,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<Value>>();
|
||||
|
||||
Value::list(records, span)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_examples() {
|
||||
use crate::test_examples;
|
||||
|
||||
test_examples(DateFromHuman {})
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
mod date_;
|
||||
mod from_human;
|
||||
mod humanize;
|
||||
mod list_timezone;
|
||||
mod now;
|
||||
@ -8,7 +7,6 @@ mod to_timezone;
|
||||
mod utils;
|
||||
|
||||
pub use date_::Date;
|
||||
pub use from_human::DateFromHuman;
|
||||
pub use humanize::DateHumanize;
|
||||
pub use list_timezone::DateListTimezones;
|
||||
pub use now::DateNow;
|
||||
|
@ -38,16 +38,10 @@ impl Command for DateNow {
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Get the current date and format it in a given format string.",
|
||||
description: "Get the current date and display it in a given format string.",
|
||||
example: r#"date now | format date "%Y-%m-%d %H:%M:%S""#,
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description:
|
||||
"Get the current date and format it according to the RFC 3339 standard.",
|
||||
example: r#"date now | format date "%+""#,
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Get the time duration since 2019-04-30.",
|
||||
example: r#"(date now) - 2019-05-01"#,
|
||||
@ -59,8 +53,7 @@ impl Command for DateNow {
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description:
|
||||
"Get current time and format it in the debug format (RFC 2822 with timezone)",
|
||||
description: "Get current time in full RFC 3339 format with time zone.",
|
||||
example: r#"date now | debug"#,
|
||||
result: None,
|
||||
},
|
||||
|
@ -9,11 +9,7 @@ pub(crate) fn parse_date_from_string(
|
||||
Ok((native_dt, fixed_offset)) => {
|
||||
let offset = match fixed_offset {
|
||||
Some(offset) => offset,
|
||||
None => *Local
|
||||
.from_local_datetime(&native_dt)
|
||||
.single()
|
||||
.unwrap_or_default()
|
||||
.offset(),
|
||||
None => *(Local::now().offset()),
|
||||
};
|
||||
match offset.from_local_datetime(&native_dt) {
|
||||
LocalResult::Single(d) => Ok(d),
|
||||
|
@ -23,11 +23,6 @@ impl Command for Debug {
|
||||
])
|
||||
.category(Category::Debug)
|
||||
.switch("raw", "Prints the raw value representation", Some('r'))
|
||||
.switch(
|
||||
"raw-value",
|
||||
"Prints the raw value representation but not the nushell value part",
|
||||
Some('v'),
|
||||
)
|
||||
}
|
||||
|
||||
fn run(
|
||||
@ -40,7 +35,6 @@ impl Command for Debug {
|
||||
let head = call.head;
|
||||
let config = stack.get_config(engine_state);
|
||||
let raw = call.has_flag(engine_state, stack, "raw")?;
|
||||
let raw_value = call.has_flag(engine_state, stack, "raw-value")?;
|
||||
|
||||
// Should PipelineData::Empty result in an error here?
|
||||
|
||||
@ -48,11 +42,6 @@ impl Command for Debug {
|
||||
move |x| {
|
||||
if raw {
|
||||
Value::string(x.to_debug_string(), head)
|
||||
} else if raw_value {
|
||||
match x.coerce_into_string_all() {
|
||||
Ok(s) => Value::string(format!("{s:#?}"), head),
|
||||
Err(e) => Value::error(e, head),
|
||||
}
|
||||
} else {
|
||||
Value::string(x.to_expanded_string(", ", &config), head)
|
||||
}
|
||||
@ -89,75 +78,10 @@ impl Command for Debug {
|
||||
Span::test_data(),
|
||||
)),
|
||||
},
|
||||
Example {
|
||||
description: "Debug print an ansi escape encoded string and get the raw value",
|
||||
example: "$'(ansi red)nushell(ansi reset)' | debug -v",
|
||||
result: Some(Value::test_string("\"\\u{1b}[31mnushell\\u{1b}[0m\"")),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// This is just a local Value Extension trait to avoid having to
|
||||
// put another *_to_string() converter in nu_protocol
|
||||
trait ValueExt {
|
||||
fn coerce_into_string_all(&self) -> Result<String, ShellError>;
|
||||
fn cant_convert_to<T>(&self, typ: &str) -> Result<T, ShellError>;
|
||||
}
|
||||
|
||||
impl ValueExt for Value {
|
||||
fn cant_convert_to<T>(&self, typ: &str) -> Result<T, ShellError> {
|
||||
Err(ShellError::CantConvert {
|
||||
to_type: typ.into(),
|
||||
from_type: self.get_type().to_string(),
|
||||
span: self.span(),
|
||||
help: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn coerce_into_string_all(&self) -> Result<String, ShellError> {
|
||||
let span = self.span();
|
||||
match self {
|
||||
Value::Bool { val, .. } => Ok(val.to_string()),
|
||||
Value::Int { val, .. } => Ok(val.to_string()),
|
||||
Value::Float { val, .. } => Ok(val.to_string()),
|
||||
Value::String { val, .. } => Ok(val.to_string()),
|
||||
Value::Glob { val, .. } => Ok(val.to_string()),
|
||||
Value::Filesize { val, .. } => Ok(val.get().to_string()),
|
||||
Value::Duration { val, .. } => Ok(val.to_string()),
|
||||
Value::Date { val, .. } => Ok(val.to_rfc3339_opts(chrono::SecondsFormat::Nanos, true)),
|
||||
Value::Range { val, .. } => Ok(val.to_string()),
|
||||
Value::Record { val, .. } => Ok(format!(
|
||||
"{{{}}}",
|
||||
val.iter()
|
||||
.map(|(x, y)| match y.coerce_into_string_all() {
|
||||
Ok(value) => format!("{x}: {value}"),
|
||||
Err(err) => format!("Error: {err}"),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)),
|
||||
Value::List { vals, .. } => Ok(format!(
|
||||
"[{}]",
|
||||
vals.iter()
|
||||
.map(|x| match x.coerce_into_string_all() {
|
||||
Ok(value) => value,
|
||||
Err(err) => format!("Error: {err}"),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)),
|
||||
Value::Binary { val, .. } => match String::from_utf8(val.to_vec()) {
|
||||
Ok(s) => Ok(s),
|
||||
Err(err) => Value::binary(err.into_bytes(), span).cant_convert_to("string"),
|
||||
},
|
||||
Value::CellPath { val, .. } => Ok(val.to_string()),
|
||||
Value::Nothing { .. } => Ok("nothing".to_string()),
|
||||
val => val.cant_convert_to("string"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[test]
|
||||
|
@ -118,7 +118,7 @@ fn increase_string_width(text: &mut String, total: usize) {
|
||||
let rest = total - width;
|
||||
|
||||
if rest > 0 {
|
||||
text.extend(std::iter::repeat_n(' ', rest));
|
||||
text.extend(std::iter::repeat(' ').take(rest));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -272,7 +272,6 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
||||
// Date
|
||||
bind_command! {
|
||||
Date,
|
||||
DateFromHuman,
|
||||
DateHumanize,
|
||||
DateListTimezones,
|
||||
DateNow,
|
||||
@ -452,18 +451,9 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
||||
JobSpawn,
|
||||
JobList,
|
||||
JobKill,
|
||||
JobId,
|
||||
JobTag,
|
||||
Job,
|
||||
};
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
bind_command! {
|
||||
JobSend,
|
||||
JobRecv,
|
||||
JobFlush,
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "os"))]
|
||||
bind_command! {
|
||||
JobUnfreeze,
|
||||
|
16
crates/nu-command/src/env/config/config_.rs
vendored
16
crates/nu-command/src/env/config/config_.rs
vendored
@ -1,11 +1,7 @@
|
||||
use nu_cmd_base::util::get_editor;
|
||||
use nu_engine::{command_prelude::*, env_to_strings, get_full_help};
|
||||
use nu_protocol::shell_error::io::IoError;
|
||||
use nu_system::ForegroundChild;
|
||||
|
||||
#[cfg(feature = "os")]
|
||||
use nu_protocol::process::PostWaitCallback;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConfigMeta;
|
||||
|
||||
@ -65,6 +61,7 @@ pub(super) fn start_editor(
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
// Find the editor executable.
|
||||
|
||||
use nu_protocol::shell_error::io::IoError;
|
||||
let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?;
|
||||
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
||||
let cwd = engine_state.cwd(Some(stack))?;
|
||||
@ -122,17 +119,8 @@ pub(super) fn start_editor(
|
||||
)
|
||||
})?;
|
||||
|
||||
let post_wait_callback = PostWaitCallback::for_job_control(engine_state, None, None);
|
||||
|
||||
// Wrap the output into a `PipelineData::ByteStream`.
|
||||
let child = nu_protocol::process::ChildProcess::new(
|
||||
child,
|
||||
None,
|
||||
false,
|
||||
call.head,
|
||||
Some(post_wait_callback),
|
||||
)?;
|
||||
|
||||
let child = nu_protocol::process::ChildProcess::new(child, None, false, call.head, None)?;
|
||||
Ok(PipelineData::ByteStream(
|
||||
ByteStream::child(child, call.head),
|
||||
None,
|
||||
|
@ -10,7 +10,7 @@ impl Command for Job {
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("job")
|
||||
.category(Category::Experimental)
|
||||
.category(Category::Strings)
|
||||
.input_output_types(vec![(Type::Nothing, Type::String)])
|
||||
}
|
||||
|
||||
|
@ -1,58 +0,0 @@
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JobFlush;
|
||||
|
||||
impl Command for JobFlush {
|
||||
fn name(&self) -> &str {
|
||||
"job flush"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Clear this job's mailbox."
|
||||
}
|
||||
|
||||
fn extra_description(&self) -> &str {
|
||||
r#"
|
||||
This command removes all messages in the mailbox of the current job.
|
||||
If a message is received while this command is executing, it may also be discarded.
|
||||
"#
|
||||
}
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("job flush")
|
||||
.category(Category::Experimental)
|
||||
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
|
||||
.allow_variants_without_examples(true)
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
_stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let mut mailbox = engine_state
|
||||
.current_job
|
||||
.mailbox
|
||||
.lock()
|
||||
.expect("failed to acquire lock");
|
||||
|
||||
mailbox.clear();
|
||||
|
||||
Ok(Value::nothing(call.head).into_pipeline_data())
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
example: "job flush",
|
||||
description: "Clear the mailbox of the current job.",
|
||||
result: None,
|
||||
}]
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JobId;
|
||||
|
||||
impl Command for JobId {
|
||||
fn name(&self) -> &str {
|
||||
"job id"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Get id of current job."
|
||||
}
|
||||
|
||||
fn extra_description(&self) -> &str {
|
||||
"This command returns the job id for the current background job.
|
||||
The special id 0 indicates that this command was not called from a background job thread, and
|
||||
was instead spawned by main nushell execution thread."
|
||||
}
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("job id")
|
||||
.category(Category::Experimental)
|
||||
.input_output_types(vec![(Type::Nothing, Type::Int)])
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["self", "this", "my-id", "this-id"]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
_stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
|
||||
Ok(Value::int(engine_state.current_job.id.get() as i64, head).into_pipeline_data())
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
example: "job id",
|
||||
description: "Get id of current job",
|
||||
result: None,
|
||||
}]
|
||||
}
|
||||
}
|
@ -37,7 +37,7 @@ impl Command for JobList {
|
||||
let values = jobs
|
||||
.iter()
|
||||
.map(|(id, job)| {
|
||||
let mut record = record! {
|
||||
let record = record! {
|
||||
"id" => Value::int(id.get() as i64, head),
|
||||
"type" => match job {
|
||||
Job::Thread(_) => Value::string("thread", head),
|
||||
@ -52,16 +52,12 @@ impl Command for JobList {
|
||||
head,
|
||||
),
|
||||
|
||||
Job::Frozen(FrozenJob { unfreeze, .. }) => {
|
||||
Job::Frozen(FrozenJob { unfreeze }) => {
|
||||
Value::list(vec![ Value::int(unfreeze.pid() as i64, head) ], head)
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(tag) = job.tag() {
|
||||
record.push("tag", Value::string(tag, head));
|
||||
}
|
||||
|
||||
Value::record(record, head)
|
||||
})
|
||||
.collect::<Vec<Value>>();
|
||||
|
@ -1,181 +0,0 @@
|
||||
use std::{
|
||||
sync::mpsc::{RecvTimeoutError, TryRecvError},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
use nu_protocol::{
|
||||
engine::{FilterTag, Mailbox},
|
||||
Signals,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JobRecv;
|
||||
|
||||
const CTRL_C_CHECK_INTERVAL: Duration = Duration::from_millis(100);
|
||||
|
||||
impl Command for JobRecv {
|
||||
fn name(&self) -> &str {
|
||||
"job recv"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Read a message from the mailbox."
|
||||
}
|
||||
|
||||
fn extra_description(&self) -> &str {
|
||||
r#"When messages are sent to the current process, they get stored in what is called the "mailbox".
|
||||
This commands reads and returns a message from the mailbox, in a first-in-first-out fashion.
|
||||
|
||||
Messages may have numeric flags attached to them. This commands supports filtering out messages that do not satisfy a given tag, by using the `tag` flag.
|
||||
If no tag is specified, this command will accept any message.
|
||||
|
||||
If no message with the specified tag (if any) is available in the mailbox, this command will block the current thread until one arrives.
|
||||
By default this command block indefinitely until a matching message arrives, but a timeout duration can be specified.
|
||||
If a timeout duration of zero is specified, it will succeed only if there already is a message in the mailbox.
|
||||
|
||||
Note: When using par-each, only one thread at a time can utilize this command.
|
||||
In the case of two or more threads running this command, they will wait until other threads are done using it,
|
||||
in no particular order, regardless of the specified timeout parameter.
|
||||
"#
|
||||
}
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("job recv")
|
||||
.category(Category::Experimental)
|
||||
.named("tag", SyntaxShape::Int, "A tag for the message", None)
|
||||
.named(
|
||||
"timeout",
|
||||
SyntaxShape::Duration,
|
||||
"The maximum time duration to wait for.",
|
||||
None,
|
||||
)
|
||||
.input_output_types(vec![(Type::Nothing, Type::Any)])
|
||||
.allow_variants_without_examples(true)
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["receive"]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
|
||||
let tag_arg: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "tag")?;
|
||||
|
||||
if let Some(tag) = tag_arg {
|
||||
if tag.item < 0 {
|
||||
return Err(ShellError::NeedsPositiveValue { span: tag.span });
|
||||
}
|
||||
}
|
||||
|
||||
let tag = tag_arg.map(|it| it.item as FilterTag);
|
||||
|
||||
let duration: Option<i64> = call.get_flag(engine_state, stack, "timeout")?;
|
||||
|
||||
let timeout = duration.map(|it| Duration::from_nanos(it as u64));
|
||||
|
||||
let mut mailbox = engine_state
|
||||
.current_job
|
||||
.mailbox
|
||||
.lock()
|
||||
.expect("failed to acquire lock");
|
||||
|
||||
if let Some(timeout) = timeout {
|
||||
if timeout == Duration::ZERO {
|
||||
recv_instantly(&mut mailbox, tag, head)
|
||||
} else {
|
||||
recv_with_time_limit(&mut mailbox, tag, engine_state.signals(), head, timeout)
|
||||
}
|
||||
} else {
|
||||
recv_without_time_limit(&mut mailbox, tag, engine_state.signals(), head)
|
||||
}
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
example: "job recv",
|
||||
description: "Block the current thread while no message arrives",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
example: "job recv --timeout 10sec",
|
||||
description: "Receive a message, wait for at most 10 seconds.",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
example: "job recv --timeout 0sec",
|
||||
description: "Get a message or fail if no message is available immediately",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn recv_without_time_limit(
|
||||
mailbox: &mut Mailbox,
|
||||
tag: Option<FilterTag>,
|
||||
signals: &Signals,
|
||||
span: Span,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
loop {
|
||||
if signals.interrupted() {
|
||||
return Err(ShellError::Interrupted { span });
|
||||
}
|
||||
match mailbox.recv_timeout(tag, CTRL_C_CHECK_INTERVAL) {
|
||||
Ok(value) => return Ok(value),
|
||||
Err(RecvTimeoutError::Timeout) => {} // try again
|
||||
Err(RecvTimeoutError::Disconnected) => return Err(ShellError::Interrupted { span }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn recv_instantly(
|
||||
mailbox: &mut Mailbox,
|
||||
tag: Option<FilterTag>,
|
||||
span: Span,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
match mailbox.try_recv(tag) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(TryRecvError::Empty) => Err(ShellError::RecvTimeout { span }),
|
||||
Err(TryRecvError::Disconnected) => Err(ShellError::Interrupted { span }),
|
||||
}
|
||||
}
|
||||
|
||||
fn recv_with_time_limit(
|
||||
mailbox: &mut Mailbox,
|
||||
tag: Option<FilterTag>,
|
||||
signals: &Signals,
|
||||
span: Span,
|
||||
timeout: Duration,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let deadline = Instant::now() + timeout;
|
||||
|
||||
loop {
|
||||
if signals.interrupted() {
|
||||
return Err(ShellError::Interrupted { span });
|
||||
}
|
||||
|
||||
let time_until_deadline = deadline.saturating_duration_since(Instant::now());
|
||||
|
||||
let time_to_sleep = time_until_deadline.min(CTRL_C_CHECK_INTERVAL);
|
||||
|
||||
match mailbox.recv_timeout(tag, time_to_sleep) {
|
||||
Ok(value) => return Ok(value),
|
||||
Err(RecvTimeoutError::Timeout) => {} // try again
|
||||
Err(RecvTimeoutError::Disconnected) => return Err(ShellError::Interrupted { span }),
|
||||
}
|
||||
|
||||
if time_until_deadline.is_zero() {
|
||||
return Err(ShellError::RecvTimeout { span });
|
||||
}
|
||||
}
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::{engine::FilterTag, JobId};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JobSend;
|
||||
|
||||
impl Command for JobSend {
|
||||
fn name(&self) -> &str {
|
||||
"job send"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Send a message to the mailbox of a job."
|
||||
}
|
||||
|
||||
fn extra_description(&self) -> &str {
|
||||
r#"
|
||||
This command sends a message to a background job, which can then read sent messages
|
||||
in a first-in-first-out fashion with `job recv`. When it does so, it may additionally specify a numeric filter tag,
|
||||
in which case it will only read messages sent with the exact same filter tag.
|
||||
In particular, the id 0 refers to the main/initial nushell thread.
|
||||
|
||||
A message can be any nushell value, and streams are always collected before being sent.
|
||||
|
||||
This command never blocks.
|
||||
"#
|
||||
}
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("job send")
|
||||
.category(Category::Experimental)
|
||||
.required(
|
||||
"id",
|
||||
SyntaxShape::Int,
|
||||
"The id of the job to send the message to.",
|
||||
)
|
||||
.named("tag", SyntaxShape::Int, "A tag for the message", None)
|
||||
.input_output_types(vec![(Type::Any, Type::Nothing)])
|
||||
.allow_variants_without_examples(true)
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
|
||||
let id_arg: Spanned<i64> = call.req(engine_state, stack, 0)?;
|
||||
let tag_arg: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "tag")?;
|
||||
|
||||
let id = id_arg.item;
|
||||
|
||||
if id < 0 {
|
||||
return Err(ShellError::NeedsPositiveValue { span: id_arg.span });
|
||||
}
|
||||
|
||||
if let Some(tag) = tag_arg {
|
||||
if tag.item < 0 {
|
||||
return Err(ShellError::NeedsPositiveValue { span: tag.span });
|
||||
}
|
||||
}
|
||||
|
||||
let tag = tag_arg.map(|it| it.item as FilterTag);
|
||||
|
||||
if id == 0 {
|
||||
engine_state
|
||||
.root_job_sender
|
||||
.send((tag, input))
|
||||
.expect("this should NEVER happen.");
|
||||
} else {
|
||||
let jobs = engine_state.jobs.lock().expect("failed to acquire lock");
|
||||
|
||||
if let Some(job) = jobs.lookup(JobId::new(id as usize)) {
|
||||
match job {
|
||||
nu_protocol::engine::Job::Thread(thread_job) => {
|
||||
// it is ok to send this value while holding the lock, because
|
||||
// mail channels are always unbounded, so this send never blocks
|
||||
let _ = thread_job.sender.send((tag, input));
|
||||
}
|
||||
nu_protocol::engine::Job::Frozen(_) => {
|
||||
return Err(ShellError::JobIsFrozen {
|
||||
id: id as usize,
|
||||
span: id_arg.span,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(ShellError::JobNotFound {
|
||||
id: id as usize,
|
||||
span: id_arg.span,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Value::nothing(head).into_pipeline_data())
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
example: "let id = job spawn { job recv | save sent.txt }; 'hi' | job send $id",
|
||||
description: "Send a message to a newly spawned job",
|
||||
result: None,
|
||||
}]
|
||||
}
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU32},
|
||||
mpsc, Arc, Mutex,
|
||||
Arc,
|
||||
},
|
||||
thread,
|
||||
};
|
||||
|
||||
use nu_engine::{command_prelude::*, ClosureEvalOnce};
|
||||
use nu_protocol::{
|
||||
engine::{Closure, CurrentJob, Job, Mailbox, Redirection, ThreadJob},
|
||||
report_shell_error, OutDest, Signals,
|
||||
engine::{Closure, Job, ThreadJob},
|
||||
report_shell_error, Signals,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -28,12 +28,6 @@ impl Command for JobSpawn {
|
||||
Signature::build("job spawn")
|
||||
.category(Category::Experimental)
|
||||
.input_output_types(vec![(Type::Nothing, Type::Int)])
|
||||
.named(
|
||||
"tag",
|
||||
SyntaxShape::String,
|
||||
"An optional description tag for this job",
|
||||
Some('t'),
|
||||
)
|
||||
.required(
|
||||
"closure",
|
||||
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])),
|
||||
@ -56,12 +50,11 @@ impl Command for JobSpawn {
|
||||
|
||||
let closure: Closure = call.req(engine_state, stack, 0)?;
|
||||
|
||||
let tag: Option<String> = call.get_flag(engine_state, stack, "tag")?;
|
||||
let job_stack = stack.clone();
|
||||
|
||||
let mut job_state = engine_state.clone();
|
||||
job_state.is_interactive = false;
|
||||
|
||||
let job_stack = stack.clone();
|
||||
|
||||
// the new job should have its ctrl-c independent of foreground
|
||||
let job_signals = Signals::new(Arc::new(AtomicBool::new(false)));
|
||||
job_state.set_signals(job_signals.clone());
|
||||
@ -74,37 +67,24 @@ impl Command for JobSpawn {
|
||||
let jobs = job_state.jobs.clone();
|
||||
let mut jobs = jobs.lock().expect("jobs lock is poisoned!");
|
||||
|
||||
let (send, recv) = mpsc::channel();
|
||||
|
||||
let id = {
|
||||
let thread_job = ThreadJob::new(job_signals, tag, send);
|
||||
|
||||
let id = jobs.add_job(Job::Thread(thread_job.clone()));
|
||||
|
||||
job_state.current_job = CurrentJob {
|
||||
id,
|
||||
background_thread_job: Some(thread_job),
|
||||
mailbox: Arc::new(Mutex::new(Mailbox::new(recv))),
|
||||
};
|
||||
|
||||
id
|
||||
let thread_job = ThreadJob::new(job_signals);
|
||||
job_state.current_thread_job = Some(thread_job.clone());
|
||||
jobs.add_job(Job::Thread(thread_job))
|
||||
};
|
||||
|
||||
let result = thread::Builder::new()
|
||||
.name(format!("background job {}", id.get()))
|
||||
.spawn(move || {
|
||||
let mut stack = job_stack.reset_pipes();
|
||||
let stack = stack.push_redirection(
|
||||
Some(Redirection::Pipe(OutDest::Null)),
|
||||
Some(Redirection::Pipe(OutDest::Null)),
|
||||
);
|
||||
ClosureEvalOnce::new_preserve_out_dest(&job_state, &stack, closure)
|
||||
ClosureEvalOnce::new(&job_state, &job_stack, closure)
|
||||
.run_with_input(Value::nothing(head).into_pipeline_data())
|
||||
.and_then(|data| data.drain())
|
||||
.and_then(|data| data.into_value(head))
|
||||
.unwrap_or_else(|err| {
|
||||
if !job_state.signals().interrupted() {
|
||||
report_shell_error(&job_state, &err);
|
||||
}
|
||||
|
||||
Value::nothing(head)
|
||||
});
|
||||
|
||||
{
|
||||
|
@ -1,81 +0,0 @@
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::JobId;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JobTag;
|
||||
|
||||
impl Command for JobTag {
|
||||
fn name(&self) -> &str {
|
||||
"job tag"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Add a description tag to a background job."
|
||||
}
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("job tag")
|
||||
.category(Category::Experimental)
|
||||
.required("id", SyntaxShape::Int, "The id of the job to tag.")
|
||||
.required(
|
||||
"tag",
|
||||
SyntaxShape::OneOf(vec![SyntaxShape::String, SyntaxShape::Nothing]),
|
||||
"The tag to assign to the job.",
|
||||
)
|
||||
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["describe", "desc"]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
|
||||
let id_arg: Spanned<i64> = call.req(engine_state, stack, 0)?;
|
||||
|
||||
if id_arg.item < 0 {
|
||||
return Err(ShellError::NeedsPositiveValue { span: id_arg.span });
|
||||
}
|
||||
|
||||
let id: JobId = JobId::new(id_arg.item as usize);
|
||||
|
||||
let tag: Option<String> = call.req(engine_state, stack, 1)?;
|
||||
|
||||
let mut jobs = engine_state.jobs.lock().expect("jobs lock is poisoned!");
|
||||
|
||||
match jobs.lookup_mut(id) {
|
||||
None => {
|
||||
return Err(ShellError::JobNotFound {
|
||||
id: id.get(),
|
||||
span: head,
|
||||
});
|
||||
}
|
||||
|
||||
Some(job) => job.assign_tag(tag),
|
||||
}
|
||||
|
||||
Ok(Value::nothing(head).into_pipeline_data())
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
example: "let id = job spawn { sleep 10sec }; job tag $id abc ",
|
||||
description: "Tag a newly spawned job",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
example: "let id = job spawn { sleep 10sec }; job tag $id abc; job tag $id null",
|
||||
description: "Remove the tag of a job",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
@ -112,13 +112,10 @@ fn unfreeze_job(
|
||||
span,
|
||||
}),
|
||||
|
||||
Job::Frozen(FrozenJob {
|
||||
unfreeze: handle,
|
||||
tag,
|
||||
}) => {
|
||||
Job::Frozen(FrozenJob { unfreeze: handle }) => {
|
||||
let pid = handle.pid();
|
||||
|
||||
if let Some(thread_job) = &state.current_thread_job() {
|
||||
if let Some(thread_job) = &state.current_thread_job {
|
||||
if !thread_job.try_add_pid(pid) {
|
||||
kill_by_pid(pid.into()).map_err(|err| {
|
||||
ShellError::Io(IoError::new_internal(
|
||||
@ -136,7 +133,7 @@ fn unfreeze_job(
|
||||
.then(|| state.pipeline_externals_state.clone()),
|
||||
);
|
||||
|
||||
if let Some(thread_job) = &state.current_thread_job() {
|
||||
if let Some(thread_job) = &state.current_thread_job {
|
||||
thread_job.remove_pid(pid);
|
||||
}
|
||||
|
||||
@ -144,14 +141,8 @@ fn unfreeze_job(
|
||||
Ok(ForegroundWaitStatus::Frozen(handle)) => {
|
||||
let mut jobs = state.jobs.lock().expect("jobs lock is poisoned!");
|
||||
|
||||
jobs.add_job_with_id(
|
||||
old_id,
|
||||
Job::Frozen(FrozenJob {
|
||||
unfreeze: handle,
|
||||
tag,
|
||||
}),
|
||||
)
|
||||
.expect("job was supposed to be removed");
|
||||
jobs.add_job_with_id(old_id, Job::Frozen(FrozenJob { unfreeze: handle }))
|
||||
.expect("job was supposed to be removed");
|
||||
|
||||
if state.is_interactive {
|
||||
println!("\nJob {} is re-frozen", old_id.get());
|
||||
|
@ -1,35 +1,18 @@
|
||||
mod is_admin;
|
||||
mod job;
|
||||
mod job_id;
|
||||
mod job_kill;
|
||||
mod job_list;
|
||||
mod job_spawn;
|
||||
mod job_tag;
|
||||
|
||||
#[cfg(all(unix, feature = "os"))]
|
||||
mod job_unfreeze;
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
mod job_flush;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
mod job_recv;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
mod job_send;
|
||||
|
||||
pub use is_admin::IsAdmin;
|
||||
pub use job::Job;
|
||||
pub use job_id::JobId;
|
||||
pub use job_kill::JobKill;
|
||||
pub use job_list::JobList;
|
||||
pub use job_spawn::JobSpawn;
|
||||
pub use job_tag::JobTag;
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub use job_flush::JobFlush;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub use job_recv::JobRecv;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub use job_send::JobSend;
|
||||
pub use job_spawn::JobSpawn;
|
||||
|
||||
#[cfg(all(unix, feature = "os"))]
|
||||
pub use job_unfreeze::JobUnfreeze;
|
||||
|
@ -132,7 +132,7 @@ impl Command for Cd {
|
||||
stack.set_cwd(path)?;
|
||||
Ok(PipelineData::empty())
|
||||
}
|
||||
PermissionResult::PermissionDenied => {
|
||||
PermissionResult::PermissionDenied(_) => {
|
||||
Err(IoError::new(std::io::ErrorKind::PermissionDenied, call.head, path).into())
|
||||
}
|
||||
}
|
||||
|
@ -35,11 +35,6 @@ impl Command for Glob {
|
||||
"Whether to filter out symlinks from the returned paths",
|
||||
Some('S'),
|
||||
)
|
||||
.switch(
|
||||
"follow-symlinks",
|
||||
"Whether to follow symbolic links to their targets",
|
||||
Some('l'),
|
||||
)
|
||||
.named(
|
||||
"exclude",
|
||||
SyntaxShape::List(Box::new(SyntaxShape::String)),
|
||||
@ -116,11 +111,6 @@ impl Command for Glob {
|
||||
example: r#"glob **/* --exclude [**/target/** **/.git/** */]"#,
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Search for files following symbolic links to their targets",
|
||||
example: r#"glob "**/*.txt" --follow-symlinks"#,
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@ -142,7 +132,6 @@ impl Command for Glob {
|
||||
let no_dirs = call.has_flag(engine_state, stack, "no-dir")?;
|
||||
let no_files = call.has_flag(engine_state, stack, "no-file")?;
|
||||
let no_symlinks = call.has_flag(engine_state, stack, "no-symlink")?;
|
||||
let follow_symlinks = call.has_flag(engine_state, stack, "follow-symlinks")?;
|
||||
let paths_to_exclude: Option<Value> = call.get_flag(engine_state, stack, "exclude")?;
|
||||
|
||||
let (not_patterns, not_pattern_span): (Vec<String>, Span) = match paths_to_exclude {
|
||||
@ -224,11 +213,6 @@ impl Command for Glob {
|
||||
}
|
||||
};
|
||||
|
||||
let link_behavior = match follow_symlinks {
|
||||
true => wax::LinkBehavior::ReadTarget,
|
||||
false => wax::LinkBehavior::ReadFile,
|
||||
};
|
||||
|
||||
let result = if !not_patterns.is_empty() {
|
||||
let np: Vec<&str> = not_patterns.iter().map(|s| s as &str).collect();
|
||||
let glob_results = glob
|
||||
@ -236,7 +220,7 @@ impl Command for Glob {
|
||||
path,
|
||||
WalkBehavior {
|
||||
depth: folder_depth,
|
||||
link: link_behavior,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.into_owned()
|
||||
@ -263,7 +247,7 @@ impl Command for Glob {
|
||||
path,
|
||||
WalkBehavior {
|
||||
depth: folder_depth,
|
||||
link: link_behavior,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.into_owned()
|
||||
|
@ -378,7 +378,10 @@ fn ls_for_one_pattern(
|
||||
.par_bridge()
|
||||
.filter_map(move |x| match x {
|
||||
Ok(path) => {
|
||||
let metadata = std::fs::symlink_metadata(&path).ok();
|
||||
let metadata = match std::fs::symlink_metadata(&path) {
|
||||
Ok(metadata) => Some(metadata),
|
||||
Err(_) => None,
|
||||
};
|
||||
let hidden_dir_clone = Arc::clone(&hidden_dirs);
|
||||
let mut hidden_dir_mutex = hidden_dir_clone
|
||||
.lock()
|
||||
|
@ -1,15 +1,11 @@
|
||||
#[allow(deprecated)]
|
||||
use nu_engine::{command_prelude::*, current_dir, eval_call};
|
||||
use nu_engine::{command_prelude::*, current_dir, get_eval_block};
|
||||
use nu_protocol::{
|
||||
ast,
|
||||
debugger::{WithDebug, WithoutDebug},
|
||||
shell_error::{self, io::IoError},
|
||||
DataSource, NuGlob, PipelineMetadata,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
use crate::database::SQLiteDatabase;
|
||||
@ -34,14 +30,7 @@ impl Command for Open {
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec![
|
||||
"load",
|
||||
"read",
|
||||
"load_file",
|
||||
"read_file",
|
||||
"cat",
|
||||
"get-content",
|
||||
]
|
||||
vec!["load", "read", "load_file", "read_file"]
|
||||
}
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
@ -74,6 +63,7 @@ impl Command for Open {
|
||||
#[allow(deprecated)]
|
||||
let cwd = current_dir(engine_state, stack)?;
|
||||
let mut paths = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
|
||||
let eval_block = get_eval_block(engine_state);
|
||||
|
||||
if paths.is_empty() && !call.has_positional_args(stack, 0) {
|
||||
// try to use path from pipeline input if there were no positional or spread args
|
||||
@ -202,16 +192,13 @@ impl Command for Open {
|
||||
|
||||
match converter {
|
||||
Some((converter_id, ext)) => {
|
||||
let open_call = ast::Call {
|
||||
decl_id: converter_id,
|
||||
head: call_span,
|
||||
arguments: vec![],
|
||||
parser_info: HashMap::new(),
|
||||
};
|
||||
let command_output = if engine_state.is_debugging() {
|
||||
eval_call::<WithDebug>(engine_state, stack, &open_call, stream)
|
||||
let decl = engine_state.get_decl(converter_id);
|
||||
let command_output = if let Some(block_id) = decl.block_id() {
|
||||
let block = engine_state.get_block(block_id);
|
||||
eval_block(engine_state, stack, block, stream)
|
||||
} else {
|
||||
eval_call::<WithoutDebug>(engine_state, stack, &open_call, stream)
|
||||
let call = ast::Call::new(call_span);
|
||||
decl.run(engine_state, stack, &(&call).into(), stream)
|
||||
};
|
||||
output.push(command_output.map_err(|inner| {
|
||||
ShellError::GenericError{
|
||||
@ -281,16 +268,6 @@ impl Command for Open {
|
||||
example: r#"def "from ndjson" [] { from json -o }; open myfile.ndjson"#,
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Show the extensions for which the `open` command will automatically parse",
|
||||
example: r#"scope commands
|
||||
| where name starts-with "from "
|
||||
| insert extension { get name | str replace -r "^from " "" | $"*.($in)" }
|
||||
| select extension name
|
||||
| rename extension command
|
||||
"#,
|
||||
result: None,
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,11 @@ use nu_protocol::{
|
||||
};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::prelude::FileTypeExt;
|
||||
use std::{collections::HashMap, io::Error, path::PathBuf};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::{Error, ErrorKind},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
const TRASH_SUPPORTED: bool = cfg!(all(
|
||||
feature = "trash-support",
|
||||
@ -375,7 +379,7 @@ fn rm(
|
||||
);
|
||||
|
||||
let result = if let Err(e) = interaction {
|
||||
Err(Error::other(&*e.to_string()))
|
||||
Err(Error::new(ErrorKind::Other, &*e.to_string()))
|
||||
} else if interactive && !confirmed {
|
||||
Ok(())
|
||||
} else if TRASH_SUPPORTED && (trash || (rm_always_trash && !permanent)) {
|
||||
@ -385,7 +389,7 @@ fn rm(
|
||||
))]
|
||||
{
|
||||
trash::delete(&f).map_err(|e: trash::Error| {
|
||||
Error::other(format!("{e:?}\nTry '--permanent' flag"))
|
||||
Error::new(ErrorKind::Other, format!("{e:?}\nTry '--permanent' flag"))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -263,17 +263,6 @@ impl Command for Save {
|
||||
example: r#"do -i {} | save foo.txt --stderr bar.txt"#,
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description:
|
||||
"Show the extensions for which the `save` command will automatically serialize",
|
||||
example: r#"scope commands
|
||||
| where name starts-with "to "
|
||||
| insert extension { get name | str replace -r "^to " "" | $"*.($in)" }
|
||||
| select extension name
|
||||
| rename extension command
|
||||
"#,
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -158,15 +158,17 @@ impl Command for UTouch {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut expanded_globs =
|
||||
glob(&file_path.to_string_lossy(), engine_state.signals().clone())
|
||||
.unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"Failed to process file path: {}",
|
||||
&file_path.to_string_lossy()
|
||||
)
|
||||
})
|
||||
.peekable();
|
||||
let mut expanded_globs = glob(
|
||||
&file_path.to_string_lossy(),
|
||||
Some(engine_state.signals().clone()),
|
||||
)
|
||||
.unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"Failed to process file path: {}",
|
||||
&file_path.to_string_lossy()
|
||||
)
|
||||
})
|
||||
.peekable();
|
||||
|
||||
if expanded_globs.peek().is_none() {
|
||||
let file_name = file_path.file_name().unwrap_or_else(|| {
|
||||
|
@ -30,11 +30,6 @@ impl Command for All {
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Check if a list contains only true values",
|
||||
example: "[false true true false] | all {}",
|
||||
result: Some(Value::test_bool(false)),
|
||||
},
|
||||
Example {
|
||||
description: "Check if each row's status is the string 'UP'",
|
||||
example: "[[status]; [UP] [UP]] | all {|el| $el.status == UP }",
|
||||
|
@ -30,11 +30,6 @@ impl Command for Any {
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Check if a list contains any true values",
|
||||
example: "[false true true false] | any {}",
|
||||
result: Some(Value::test_bool(true)),
|
||||
},
|
||||
Example {
|
||||
description: "Check if any row's status is the string 'DOWN'",
|
||||
example: "[[status]; [UP] [DOWN] [UP]] | any {|el| $el.status == DOWN }",
|
||||
|
@ -243,7 +243,7 @@ mod test {
|
||||
let chunks = chunk_read.map(|e| e.unwrap()).collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
chunks,
|
||||
[&s.as_bytes()[..4], &s.as_bytes()[4..8], &s.as_bytes()[8..]]
|
||||
[s[..4].as_bytes(), s[4..8].as_bytes(), s[8..].as_bytes()]
|
||||
);
|
||||
}
|
||||
|
||||
@ -260,7 +260,7 @@ mod test {
|
||||
let chunks = chunk_read.map(|e| e.unwrap()).collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
chunks,
|
||||
[&s.as_bytes()[..4], &s.as_bytes()[4..8], &s.as_bytes()[8..]]
|
||||
[s[..4].as_bytes(), s[4..8].as_bytes(), s[8..].as_bytes()]
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::{ListStream, Signals};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Default;
|
||||
@ -147,20 +146,6 @@ fn default(
|
||||
&& matches!(input, PipelineData::Value(ref value, _) if value.is_empty()))
|
||||
{
|
||||
Ok(value.into_pipeline_data())
|
||||
} else if default_when_empty && matches!(input, PipelineData::ListStream(..)) {
|
||||
let PipelineData::ListStream(ls, metadata) = input else {
|
||||
unreachable!()
|
||||
};
|
||||
let span = ls.span();
|
||||
let mut stream = ls.into_inner().peekable();
|
||||
if stream.peek().is_none() {
|
||||
return Ok(value.into_pipeline_data());
|
||||
}
|
||||
|
||||
// stream's internal state already preserves the original signals config, so if this
|
||||
// Signals::empty list stream gets interrupted it will be caught by the underlying iterator
|
||||
let ls = ListStream::new(stream, span, Signals::empty());
|
||||
Ok(PipelineData::ListStream(ls, metadata))
|
||||
} else {
|
||||
Ok(input)
|
||||
}
|
||||
|
@ -15,8 +15,17 @@ pub fn empty(
|
||||
if !columns.is_empty() {
|
||||
for val in input {
|
||||
for column in &columns {
|
||||
if !val.follow_cell_path(&column.members, false)?.is_nothing() {
|
||||
return Ok(Value::bool(negate, head).into_pipeline_data());
|
||||
let val = val.clone();
|
||||
match val.follow_cell_path(&column.members, false) {
|
||||
Ok(Value::Nothing { .. }) => {}
|
||||
Ok(_) => {
|
||||
if negate {
|
||||
return Ok(Value::bool(true, head).into_pipeline_data());
|
||||
} else {
|
||||
return Ok(Value::bool(false, head).into_pipeline_data());
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::{ast::PathMember, Signals};
|
||||
|
||||
@ -190,11 +188,9 @@ fn action(
|
||||
let input = input.into_value(span)?;
|
||||
|
||||
for path in paths {
|
||||
output.push(
|
||||
input
|
||||
.follow_cell_path(&path.members, !sensitive)?
|
||||
.into_owned(),
|
||||
);
|
||||
let val = input.clone().follow_cell_path(&path.members, !sensitive);
|
||||
|
||||
output.push(val?);
|
||||
}
|
||||
|
||||
Ok(output.into_iter().into_pipeline_data(span, signals))
|
||||
@ -227,10 +223,10 @@ pub fn follow_cell_path_into_stream(
|
||||
.map(move |value| {
|
||||
let span = value.span();
|
||||
|
||||
value
|
||||
.follow_cell_path(&cell_path, insensitive)
|
||||
.map(Cow::into_owned)
|
||||
.unwrap_or_else(|error| Value::error(error, span))
|
||||
match value.follow_cell_path(&cell_path, insensitive) {
|
||||
Ok(v) => v,
|
||||
Err(error) => Value::error(error, span),
|
||||
}
|
||||
})
|
||||
.into_pipeline_data(head, signals);
|
||||
|
||||
|
@ -322,9 +322,11 @@ fn group_cell_path(
|
||||
let mut groups = IndexMap::<_, Vec<_>>::new();
|
||||
|
||||
for value in values.into_iter() {
|
||||
let key = value.follow_cell_path(&column_name.members, false)?;
|
||||
let key = value
|
||||
.clone()
|
||||
.follow_cell_path(&column_name.members, false)?;
|
||||
|
||||
if key.is_nothing() {
|
||||
if matches!(key, Value::Nothing { .. }) {
|
||||
continue; // likely the result of a failed optional access, ignore this value
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use nu_engine::{command_prelude::*, ClosureEval, ClosureEvalOnce};
|
||||
use nu_protocol::ast::PathMember;
|
||||
|
||||
@ -301,8 +299,8 @@ fn insert_value_by_closure(
|
||||
) -> Result<(), ShellError> {
|
||||
let value_at_path = if first_path_member_int {
|
||||
value
|
||||
.clone()
|
||||
.follow_cell_path(cell_path, false)
|
||||
.map(Cow::into_owned)
|
||||
.unwrap_or(Value::nothing(span))
|
||||
} else {
|
||||
value.clone()
|
||||
@ -321,8 +319,8 @@ fn insert_single_value_by_closure(
|
||||
) -> Result<(), ShellError> {
|
||||
let value_at_path = if first_path_member_int {
|
||||
value
|
||||
.clone()
|
||||
.follow_cell_path(cell_path, false)
|
||||
.map(Cow::into_owned)
|
||||
.unwrap_or(Value::nothing(span))
|
||||
} else {
|
||||
value.clone()
|
||||
|
@ -255,16 +255,6 @@ fn join_rows(
|
||||
config: &Config,
|
||||
span: Span,
|
||||
) {
|
||||
if !this
|
||||
.iter()
|
||||
.any(|this_record| match this_record.as_record() {
|
||||
Ok(record) => record.contains(this_join_key),
|
||||
Err(_) => false,
|
||||
})
|
||||
{
|
||||
// `this` table does not contain the join column; do nothing
|
||||
return;
|
||||
}
|
||||
for this_row in this {
|
||||
if let Value::Record {
|
||||
val: this_record, ..
|
||||
@ -291,40 +281,39 @@ fn join_rows(
|
||||
result.push(Value::record(record, span))
|
||||
}
|
||||
}
|
||||
continue;
|
||||
} else if !matches!(join_type, JoinType::Inner) {
|
||||
// `other` table did not contain any rows matching
|
||||
// `this` row on the join column; emit a single joined
|
||||
// row with null values for columns not present,
|
||||
let other_record = other_keys
|
||||
.iter()
|
||||
.map(|&key| {
|
||||
let val = if Some(key.as_ref()) == shared_join_key {
|
||||
this_record
|
||||
.get(key)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::nothing(span))
|
||||
} else {
|
||||
Value::nothing(span)
|
||||
};
|
||||
|
||||
(key.clone(), val)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let record = match join_type {
|
||||
JoinType::Inner | JoinType::Right => {
|
||||
merge_records(&other_record, this_record, shared_join_key)
|
||||
}
|
||||
JoinType::Left => {
|
||||
merge_records(this_record, &other_record, shared_join_key)
|
||||
}
|
||||
_ => panic!("not implemented"),
|
||||
};
|
||||
|
||||
result.push(Value::record(record, span))
|
||||
}
|
||||
}
|
||||
if !matches!(join_type, JoinType::Inner) {
|
||||
// Either `this` row is missing a value for the join column or
|
||||
// `other` table did not contain any rows matching
|
||||
// `this` row on the join column; emit a single joined
|
||||
// row with null values for columns not present
|
||||
let other_record = other_keys
|
||||
.iter()
|
||||
.map(|&key| {
|
||||
let val = if Some(key.as_ref()) == shared_join_key {
|
||||
this_record
|
||||
.get(key)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::nothing(span))
|
||||
} else {
|
||||
Value::nothing(span)
|
||||
};
|
||||
|
||||
(key.clone(), val)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let record = match join_type {
|
||||
JoinType::Inner | JoinType::Right => {
|
||||
merge_records(&other_record, this_record, shared_join_key)
|
||||
}
|
||||
JoinType::Left => merge_records(this_record, &other_record, shared_join_key),
|
||||
_ => panic!("not implemented"),
|
||||
};
|
||||
|
||||
result.push(Value::record(record, span))
|
||||
}
|
||||
} // else { a row is missing a value for the join column }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -42,9 +42,8 @@ pub(crate) fn typecheck_merge(lhs: &Value, rhs: &Value, head: Span) -> Result<()
|
||||
match (lhs.get_type(), rhs.get_type()) {
|
||||
(Type::Record { .. }, Type::Record { .. }) => Ok(()),
|
||||
(_, _) if is_list_of_records(lhs) && is_list_of_records(rhs) => Ok(()),
|
||||
other => Err(ShellError::OnlySupportsThisInputType {
|
||||
_ => Err(ShellError::PipelineMismatch {
|
||||
exp_input_type: "input and argument to be both record or both table".to_string(),
|
||||
wrong_type: format!("{} and {}", other.0, other.1).to_string(),
|
||||
dst_span: head,
|
||||
src_span: lhs.span(),
|
||||
}),
|
||||
|
@ -174,9 +174,8 @@ impl Command for Move {
|
||||
PipelineData::Value(Value::Record { val, .. }, ..) => {
|
||||
Ok(move_record_columns(&val, &columns, &location, head)?.into_pipeline_data())
|
||||
}
|
||||
other => Err(ShellError::OnlySupportsThisInputType {
|
||||
_ => Err(ShellError::PipelineMismatch {
|
||||
exp_input_type: "record or table".to_string(),
|
||||
wrong_type: other.get_type().to_string(),
|
||||
dst_span: head,
|
||||
src_span: Span::new(head.start, head.start),
|
||||
}),
|
||||
|
@ -229,37 +229,45 @@ fn select(
|
||||
match v {
|
||||
Value::List {
|
||||
vals: input_vals, ..
|
||||
} => Ok(input_vals
|
||||
.into_iter()
|
||||
.map(move |input_val| {
|
||||
if !columns.is_empty() {
|
||||
let mut record = Record::new();
|
||||
for path in &columns {
|
||||
match input_val.follow_cell_path(&path.members, false) {
|
||||
Ok(fetcher) => {
|
||||
record.push(path.to_column_name(), fetcher.into_owned());
|
||||
} => {
|
||||
Ok(input_vals
|
||||
.into_iter()
|
||||
.map(move |input_val| {
|
||||
if !columns.is_empty() {
|
||||
let mut record = Record::new();
|
||||
for path in &columns {
|
||||
//FIXME: improve implementation to not clone
|
||||
match input_val.clone().follow_cell_path(&path.members, false) {
|
||||
Ok(fetcher) => {
|
||||
record.push(path.to_column_name(), fetcher);
|
||||
}
|
||||
Err(e) => return Value::error(e, call_span),
|
||||
}
|
||||
Err(e) => return Value::error(e, call_span),
|
||||
}
|
||||
}
|
||||
|
||||
Value::record(record, span)
|
||||
} else {
|
||||
input_val.clone()
|
||||
}
|
||||
})
|
||||
.into_pipeline_data_with_metadata(
|
||||
call_span,
|
||||
engine_state.signals().clone(),
|
||||
metadata,
|
||||
)),
|
||||
Value::record(record, span)
|
||||
} else {
|
||||
input_val.clone()
|
||||
}
|
||||
})
|
||||
.into_pipeline_data_with_metadata(
|
||||
call_span,
|
||||
engine_state.signals().clone(),
|
||||
metadata,
|
||||
))
|
||||
}
|
||||
_ => {
|
||||
if !columns.is_empty() {
|
||||
let mut record = Record::new();
|
||||
|
||||
for cell_path in columns {
|
||||
let result = v.follow_cell_path(&cell_path.members, false)?;
|
||||
record.push(cell_path.to_column_name(), result.into_owned());
|
||||
// FIXME: remove clone
|
||||
match v.clone().follow_cell_path(&cell_path.members, false) {
|
||||
Ok(result) => {
|
||||
record.push(cell_path.to_column_name(), result);
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Value::record(record, call_span)
|
||||
@ -270,24 +278,31 @@ fn select(
|
||||
}
|
||||
}
|
||||
}
|
||||
PipelineData::ListStream(stream, metadata, ..) => Ok(stream
|
||||
.map(move |x| {
|
||||
if !columns.is_empty() {
|
||||
let mut record = Record::new();
|
||||
for path in &columns {
|
||||
match x.follow_cell_path(&path.members, false) {
|
||||
Ok(value) => {
|
||||
record.push(path.to_column_name(), value.into_owned());
|
||||
PipelineData::ListStream(stream, metadata, ..) => {
|
||||
Ok(stream
|
||||
.map(move |x| {
|
||||
if !columns.is_empty() {
|
||||
let mut record = Record::new();
|
||||
for path in &columns {
|
||||
//FIXME: improve implementation to not clone
|
||||
match x.clone().follow_cell_path(&path.members, false) {
|
||||
Ok(value) => {
|
||||
record.push(path.to_column_name(), value);
|
||||
}
|
||||
Err(e) => return Value::error(e, call_span),
|
||||
}
|
||||
Err(e) => return Value::error(e, call_span),
|
||||
}
|
||||
Value::record(record, call_span)
|
||||
} else {
|
||||
x
|
||||
}
|
||||
Value::record(record, call_span)
|
||||
} else {
|
||||
x
|
||||
}
|
||||
})
|
||||
.into_pipeline_data_with_metadata(call_span, engine_state.signals().clone(), metadata)),
|
||||
})
|
||||
.into_pipeline_data_with_metadata(
|
||||
call_span,
|
||||
engine_state.signals().clone(),
|
||||
metadata,
|
||||
))
|
||||
}
|
||||
_ => Ok(PipelineData::empty()),
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use nu_engine::command_prelude::*;
|
||||
use rand::{prelude::SliceRandom, rng};
|
||||
use rand::{prelude::SliceRandom, thread_rng};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Shuffle;
|
||||
@ -31,7 +31,7 @@ impl Command for Shuffle {
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let metadata = input.metadata();
|
||||
let mut values = input.into_iter_strict(call.head)?.collect::<Vec<_>>();
|
||||
values.shuffle(&mut rng());
|
||||
values.shuffle(&mut thread_rng());
|
||||
let iter = values.into_iter();
|
||||
Ok(iter.into_pipeline_data_with_metadata(
|
||||
call.head,
|
||||
|
@ -184,10 +184,9 @@ impl Command for Sort {
|
||||
dst_span: value.span(),
|
||||
})
|
||||
}
|
||||
ref other => {
|
||||
return Err(ShellError::OnlySupportsThisInputType {
|
||||
_ => {
|
||||
return Err(ShellError::PipelineMismatch {
|
||||
exp_input_type: "record or list".to_string(),
|
||||
wrong_type: other.get_type().to_string(),
|
||||
dst_span: call.head,
|
||||
src_span: value.span(),
|
||||
})
|
||||
|
@ -394,7 +394,7 @@ impl<R: Read> Read for IoTee<R> {
|
||||
if let Some(thread) = self.thread.take() {
|
||||
if thread.is_finished() {
|
||||
if let Err(err) = thread.join().unwrap_or_else(|_| Err(panic_error())) {
|
||||
return Err(io::Error::other(err));
|
||||
return Err(io::Error::new(io::ErrorKind::Other, err));
|
||||
}
|
||||
} else {
|
||||
self.thread = Some(thread)
|
||||
@ -405,7 +405,7 @@ impl<R: Read> Read for IoTee<R> {
|
||||
self.sender = None;
|
||||
if let Some(thread) = self.thread.take() {
|
||||
if let Err(err) = thread.join().unwrap_or_else(|_| Err(panic_error())) {
|
||||
return Err(io::Error::other(err));
|
||||
return Err(io::Error::new(io::ErrorKind::Other, err));
|
||||
}
|
||||
}
|
||||
} else if let Some(sender) = self.sender.as_mut() {
|
||||
|
@ -243,17 +243,17 @@ fn update_value_by_closure(
|
||||
cell_path: &[PathMember],
|
||||
first_path_member_int: bool,
|
||||
) -> Result<(), ShellError> {
|
||||
let value_at_path = value.follow_cell_path(cell_path, false)?;
|
||||
let value_at_path = value.clone().follow_cell_path(cell_path, false)?;
|
||||
|
||||
let arg = if first_path_member_int {
|
||||
value_at_path.as_ref()
|
||||
&value_at_path
|
||||
} else {
|
||||
&*value
|
||||
};
|
||||
|
||||
let new_value = closure
|
||||
.add_arg(arg.clone())
|
||||
.run_with_input(value_at_path.into_owned().into_pipeline_data())?
|
||||
.run_with_input(value_at_path.into_pipeline_data())?
|
||||
.into_value(span)?;
|
||||
|
||||
value.update_data_at_cell_path(cell_path, new_value)
|
||||
@ -266,17 +266,17 @@ fn update_single_value_by_closure(
|
||||
cell_path: &[PathMember],
|
||||
first_path_member_int: bool,
|
||||
) -> Result<(), ShellError> {
|
||||
let value_at_path = value.follow_cell_path(cell_path, false)?;
|
||||
let value_at_path = value.clone().follow_cell_path(cell_path, false)?;
|
||||
|
||||
let arg = if first_path_member_int {
|
||||
value_at_path.as_ref()
|
||||
&value_at_path
|
||||
} else {
|
||||
&*value
|
||||
};
|
||||
|
||||
let new_value = closure
|
||||
.add_arg(arg.clone())
|
||||
.run_with_input(value_at_path.into_owned().into_pipeline_data())?
|
||||
.run_with_input(value_at_path.into_pipeline_data())?
|
||||
.into_value(span)?;
|
||||
|
||||
value.update_data_at_cell_path(cell_path, new_value)
|
||||
|
@ -1,5 +1,3 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use nu_engine::{command_prelude::*, ClosureEval, ClosureEvalOnce};
|
||||
use nu_protocol::ast::PathMember;
|
||||
|
||||
@ -321,19 +319,15 @@ fn upsert_value_by_closure(
|
||||
cell_path: &[PathMember],
|
||||
first_path_member_int: bool,
|
||||
) -> Result<(), ShellError> {
|
||||
let value_at_path = value.follow_cell_path(cell_path, false);
|
||||
let value_at_path = value.clone().follow_cell_path(cell_path, false);
|
||||
|
||||
let arg = if first_path_member_int {
|
||||
value_at_path
|
||||
.as_deref()
|
||||
.cloned()
|
||||
.unwrap_or(Value::nothing(span))
|
||||
value_at_path.clone().unwrap_or(Value::nothing(span))
|
||||
} else {
|
||||
value.clone()
|
||||
};
|
||||
|
||||
let input = value_at_path
|
||||
.map(Cow::into_owned)
|
||||
.map(IntoPipelineData::into_pipeline_data)
|
||||
.unwrap_or(PipelineData::Empty);
|
||||
|
||||
@ -352,19 +346,15 @@ fn upsert_single_value_by_closure(
|
||||
cell_path: &[PathMember],
|
||||
first_path_member_int: bool,
|
||||
) -> Result<(), ShellError> {
|
||||
let value_at_path = value.follow_cell_path(cell_path, false);
|
||||
let value_at_path = value.clone().follow_cell_path(cell_path, false);
|
||||
|
||||
let arg = if first_path_member_int {
|
||||
value_at_path
|
||||
.as_deref()
|
||||
.cloned()
|
||||
.unwrap_or(Value::nothing(span))
|
||||
value_at_path.clone().unwrap_or(Value::nothing(span))
|
||||
} else {
|
||||
value.clone()
|
||||
};
|
||||
|
||||
let input = value_at_path
|
||||
.map(Cow::into_owned)
|
||||
.map(IntoPipelineData::into_pipeline_data)
|
||||
.unwrap_or(PipelineData::Empty);
|
||||
|
||||
|
@ -36,13 +36,13 @@ impl Command for ToMd {
|
||||
Example {
|
||||
description: "Outputs an MD string representing the contents of this table",
|
||||
example: "[[foo bar]; [1 2]] | to md",
|
||||
result: Some(Value::test_string("|foo|bar|\n|-|-|\n|1|2|")),
|
||||
result: Some(Value::test_string("|foo|bar|\n|-|-|\n|1|2|\n")),
|
||||
},
|
||||
Example {
|
||||
description: "Optionally, output a formatted markdown string",
|
||||
example: "[[foo bar]; [1 2]] | to md --pretty",
|
||||
result: Some(Value::test_string(
|
||||
"| foo | bar |\n| --- | --- |\n| 1 | 2 |",
|
||||
"| foo | bar |\n| --- | --- |\n| 1 | 2 |\n",
|
||||
)),
|
||||
},
|
||||
Example {
|
||||
@ -57,13 +57,6 @@ impl Command for ToMd {
|
||||
example: "[0 1 2] | to md --pretty",
|
||||
result: Some(Value::test_string("0\n1\n2")),
|
||||
},
|
||||
Example {
|
||||
description: "Separate list into markdown tables",
|
||||
example: "[ {foo: 1, bar: 2} {foo: 3, bar: 4} {foo: 5}] | to md --per-element",
|
||||
result: Some(Value::test_string(
|
||||
"|foo|bar|\n|-|-|\n|1|2|\n|3|4|\n|foo|\n|-|\n|5|",
|
||||
)),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@ -101,14 +94,11 @@ fn to_md(
|
||||
grouped_input
|
||||
.into_iter()
|
||||
.map(move |val| match val {
|
||||
Value::List { .. } => {
|
||||
format!("{}\n", table(val.into_pipeline_data(), pretty, config))
|
||||
}
|
||||
Value::List { .. } => table(val.into_pipeline_data(), pretty, config),
|
||||
other => fragment(other, pretty, config),
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("")
|
||||
.trim(),
|
||||
.join(""),
|
||||
head,
|
||||
)
|
||||
.into_pipeline_data_with_metadata(Some(metadata)));
|
||||
@ -162,13 +152,7 @@ fn collect_headers(headers: &[String]) -> (Vec<String>, Vec<usize>) {
|
||||
}
|
||||
|
||||
fn table(input: PipelineData, pretty: bool, config: &Config) -> String {
|
||||
let vec_of_values = input
|
||||
.into_iter()
|
||||
.flat_map(|val| match val {
|
||||
Value::List { vals, .. } => vals,
|
||||
other => vec![other],
|
||||
})
|
||||
.collect::<Vec<Value>>();
|
||||
let vec_of_values = input.into_iter().collect::<Vec<Value>>();
|
||||
let mut headers = merge_descriptors(&vec_of_values);
|
||||
|
||||
let mut empty_header_index = 0;
|
||||
@ -480,39 +464,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_row_value() {
|
||||
let value = Value::test_list(vec![
|
||||
Value::test_record(record! {
|
||||
"foo" => Value::test_string("1"),
|
||||
"bar" => Value::test_string("2"),
|
||||
}),
|
||||
Value::test_record(record! {
|
||||
"foo" => Value::test_string("3"),
|
||||
"bar" => Value::test_string("4"),
|
||||
}),
|
||||
Value::test_record(record! {
|
||||
"foo" => Value::test_string("5"),
|
||||
"bar" => Value::test_string(""),
|
||||
}),
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
table(
|
||||
value.clone().into_pipeline_data(),
|
||||
false,
|
||||
&Config::default()
|
||||
),
|
||||
one(r#"
|
||||
|foo|bar|
|
||||
|-|-|
|
||||
|1|2|
|
||||
|3|4|
|
||||
|5||
|
||||
"#)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_content_type_metadata() {
|
||||
let mut engine_state = Box::new(EngineState::new());
|
||||
|
@ -1,4 +1,3 @@
|
||||
use chrono::Datelike;
|
||||
use chrono_humanize::HumanTime;
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::{format_duration, shell_error::io::IoError, ByteStream, PipelineMetadata};
|
||||
@ -168,17 +167,7 @@ fn local_into_string(
|
||||
Value::Filesize { val, .. } => val.to_string(),
|
||||
Value::Duration { val, .. } => format_duration(val),
|
||||
Value::Date { val, .. } => {
|
||||
format!(
|
||||
"{} ({})",
|
||||
{
|
||||
if val.year() >= 0 {
|
||||
val.to_rfc2822()
|
||||
} else {
|
||||
val.to_rfc3339()
|
||||
}
|
||||
},
|
||||
HumanTime::from(val)
|
||||
)
|
||||
format!("{} ({})", val.to_rfc2822(), HumanTime::from(val))
|
||||
}
|
||||
Value::Range { val, .. } => val.to_string(),
|
||||
Value::String { val, .. } => val,
|
||||
|
@ -1,4 +1,3 @@
|
||||
use crate::math::utils::ensure_bounded;
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -22,7 +21,6 @@ impl Command for MathAbs {
|
||||
Type::List(Box::new(Type::Duration)),
|
||||
Type::List(Box::new(Type::Duration)),
|
||||
),
|
||||
(Type::Range, Type::List(Box::new(Type::Number))),
|
||||
])
|
||||
.allow_variants_without_examples(true)
|
||||
.category(Category::Math)
|
||||
@ -48,16 +46,6 @@ impl Command for MathAbs {
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
if let PipelineData::Value(
|
||||
Value::Range {
|
||||
ref val,
|
||||
internal_span,
|
||||
},
|
||||
..,
|
||||
) = input
|
||||
{
|
||||
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||
}
|
||||
input.map(move |value| abs_helper(value, head), engine_state.signals())
|
||||
}
|
||||
|
||||
@ -68,16 +56,6 @@ impl Command for MathAbs {
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
if let PipelineData::Value(
|
||||
Value::Range {
|
||||
ref val,
|
||||
internal_span,
|
||||
},
|
||||
..,
|
||||
) = input
|
||||
{
|
||||
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||
}
|
||||
input.map(
|
||||
move |value| abs_helper(value, head),
|
||||
working_set.permanent().signals(),
|
||||
|
@ -1,4 +1,3 @@
|
||||
use crate::math::utils::ensure_bounded;
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -17,7 +16,6 @@ impl Command for MathCeil {
|
||||
Type::List(Box::new(Type::Number)),
|
||||
Type::List(Box::new(Type::Int)),
|
||||
),
|
||||
(Type::Range, Type::List(Box::new(Type::Number))),
|
||||
])
|
||||
.allow_variants_without_examples(true)
|
||||
.category(Category::Math)
|
||||
@ -47,16 +45,6 @@ impl Command for MathCeil {
|
||||
if matches!(input, PipelineData::Empty) {
|
||||
return Err(ShellError::PipelineEmpty { dst_span: head });
|
||||
}
|
||||
if let PipelineData::Value(
|
||||
Value::Range {
|
||||
ref val,
|
||||
internal_span,
|
||||
},
|
||||
..,
|
||||
) = input
|
||||
{
|
||||
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||
}
|
||||
input.map(move |value| operate(value, head), engine_state.signals())
|
||||
}
|
||||
|
||||
@ -71,16 +59,6 @@ impl Command for MathCeil {
|
||||
if matches!(input, PipelineData::Empty) {
|
||||
return Err(ShellError::PipelineEmpty { dst_span: head });
|
||||
}
|
||||
if let PipelineData::Value(
|
||||
Value::Range {
|
||||
ref val,
|
||||
internal_span,
|
||||
},
|
||||
..,
|
||||
) = input
|
||||
{
|
||||
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||
}
|
||||
input.map(
|
||||
move |value| operate(value, head),
|
||||
working_set.permanent().signals(),
|
||||
|
@ -1,4 +1,3 @@
|
||||
use crate::math::utils::ensure_bounded;
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -17,7 +16,6 @@ impl Command for MathFloor {
|
||||
Type::List(Box::new(Type::Number)),
|
||||
Type::List(Box::new(Type::Int)),
|
||||
),
|
||||
(Type::Range, Type::List(Box::new(Type::Number))),
|
||||
])
|
||||
.allow_variants_without_examples(true)
|
||||
.category(Category::Math)
|
||||
@ -47,16 +45,6 @@ impl Command for MathFloor {
|
||||
if matches!(input, PipelineData::Empty) {
|
||||
return Err(ShellError::PipelineEmpty { dst_span: head });
|
||||
}
|
||||
if let PipelineData::Value(
|
||||
Value::Range {
|
||||
ref val,
|
||||
internal_span,
|
||||
},
|
||||
..,
|
||||
) = input
|
||||
{
|
||||
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||
}
|
||||
input.map(move |value| operate(value, head), engine_state.signals())
|
||||
}
|
||||
|
||||
@ -71,16 +59,6 @@ impl Command for MathFloor {
|
||||
if matches!(input, PipelineData::Empty) {
|
||||
return Err(ShellError::PipelineEmpty { dst_span: head });
|
||||
}
|
||||
if let PipelineData::Value(
|
||||
Value::Range {
|
||||
ref val,
|
||||
internal_span,
|
||||
},
|
||||
..,
|
||||
) = input
|
||||
{
|
||||
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||
}
|
||||
input.map(
|
||||
move |value| operate(value, head),
|
||||
working_set.permanent().signals(),
|
||||
|
@ -1,4 +1,3 @@
|
||||
use crate::math::utils::ensure_bounded;
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::Signals;
|
||||
|
||||
@ -23,7 +22,6 @@ impl Command for MathLog {
|
||||
Type::List(Box::new(Type::Number)),
|
||||
Type::List(Box::new(Type::Float)),
|
||||
),
|
||||
(Type::Range, Type::List(Box::new(Type::Number))),
|
||||
])
|
||||
.allow_variants_without_examples(true)
|
||||
.category(Category::Math)
|
||||
@ -48,18 +46,7 @@ impl Command for MathLog {
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
let base: Spanned<f64> = call.req(engine_state, stack, 0)?;
|
||||
if let PipelineData::Value(
|
||||
Value::Range {
|
||||
ref val,
|
||||
internal_span,
|
||||
},
|
||||
..,
|
||||
) = input
|
||||
{
|
||||
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||
}
|
||||
log(base, call.head, input, engine_state.signals())
|
||||
}
|
||||
|
||||
@ -69,18 +56,7 @@ impl Command for MathLog {
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
let base: Spanned<f64> = call.req_const(working_set, 0)?;
|
||||
if let PipelineData::Value(
|
||||
Value::Range {
|
||||
ref val,
|
||||
internal_span,
|
||||
},
|
||||
..,
|
||||
) = input
|
||||
{
|
||||
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||
}
|
||||
log(base, call.head, input, working_set.permanent().signals())
|
||||
}
|
||||
|
||||
|
@ -110,7 +110,7 @@ impl Command for MathMode {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mode(values: &[Value], span: Span, head: Span) -> Result<Value, ShellError> {
|
||||
pub fn mode(values: &[Value], _span: Span, head: Span) -> Result<Value, ShellError> {
|
||||
//In e-q, Value doesn't implement Hash or Eq, so we have to get the values inside
|
||||
// But f64 doesn't implement Hash, so we get the binary representation to use as
|
||||
// key in the HashMap
|
||||
@ -130,11 +130,11 @@ pub fn mode(values: &[Value], span: Span, head: Span) -> Result<Value, ShellErro
|
||||
NumberTypes::Filesize,
|
||||
)),
|
||||
Value::Error { error, .. } => Err(*error.clone()),
|
||||
_ => Err(ShellError::UnsupportedInput {
|
||||
other => Err(ShellError::UnsupportedInput {
|
||||
msg: "Unable to give a result with this input".to_string(),
|
||||
input: "value originates from here".into(),
|
||||
msg_span: head,
|
||||
input_span: span,
|
||||
input_span: other.span(),
|
||||
}),
|
||||
})
|
||||
.collect::<Result<Vec<HashableType>, ShellError>>()?;
|
||||
|
@ -1,4 +1,3 @@
|
||||
use crate::math::utils::ensure_bounded;
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -17,7 +16,6 @@ impl Command for MathRound {
|
||||
Type::List(Box::new(Type::Number)),
|
||||
Type::List(Box::new(Type::Number)),
|
||||
),
|
||||
(Type::Range, Type::List(Box::new(Type::Number))),
|
||||
])
|
||||
.allow_variants_without_examples(true)
|
||||
.named(
|
||||
@ -54,16 +52,6 @@ impl Command for MathRound {
|
||||
if matches!(input, PipelineData::Empty) {
|
||||
return Err(ShellError::PipelineEmpty { dst_span: head });
|
||||
}
|
||||
if let PipelineData::Value(
|
||||
Value::Range {
|
||||
ref val,
|
||||
internal_span,
|
||||
},
|
||||
..,
|
||||
) = input
|
||||
{
|
||||
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||
}
|
||||
input.map(
|
||||
move |value| operate(value, head, precision_param),
|
||||
engine_state.signals(),
|
||||
@ -82,16 +70,6 @@ impl Command for MathRound {
|
||||
if matches!(input, PipelineData::Empty) {
|
||||
return Err(ShellError::PipelineEmpty { dst_span: head });
|
||||
}
|
||||
if let PipelineData::Value(
|
||||
Value::Range {
|
||||
ref val,
|
||||
internal_span,
|
||||
},
|
||||
..,
|
||||
) = input
|
||||
{
|
||||
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||
}
|
||||
input.map(
|
||||
move |value| operate(value, head, precision_param),
|
||||
working_set.permanent().signals(),
|
||||
|
@ -1,4 +1,3 @@
|
||||
use crate::math::utils::ensure_bounded;
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -17,7 +16,6 @@ impl Command for MathSqrt {
|
||||
Type::List(Box::new(Type::Number)),
|
||||
Type::List(Box::new(Type::Float)),
|
||||
),
|
||||
(Type::Range, Type::List(Box::new(Type::Number))),
|
||||
])
|
||||
.allow_variants_without_examples(true)
|
||||
.category(Category::Math)
|
||||
@ -47,16 +45,6 @@ impl Command for MathSqrt {
|
||||
if matches!(input, PipelineData::Empty) {
|
||||
return Err(ShellError::PipelineEmpty { dst_span: head });
|
||||
}
|
||||
if let PipelineData::Value(
|
||||
Value::Range {
|
||||
ref val,
|
||||
internal_span,
|
||||
},
|
||||
..,
|
||||
) = input
|
||||
{
|
||||
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||
}
|
||||
input.map(move |value| operate(value, head), engine_state.signals())
|
||||
}
|
||||
|
||||
@ -71,16 +59,6 @@ impl Command for MathSqrt {
|
||||
if matches!(input, PipelineData::Empty) {
|
||||
return Err(ShellError::PipelineEmpty { dst_span: head });
|
||||
}
|
||||
if let PipelineData::Value(
|
||||
Value::Range {
|
||||
ref val,
|
||||
internal_span,
|
||||
},
|
||||
..,
|
||||
) = input
|
||||
{
|
||||
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||
}
|
||||
input.map(
|
||||
move |value| operate(value, head),
|
||||
working_set.permanent().signals(),
|
||||
|
@ -14,7 +14,6 @@ impl Command for MathStddev {
|
||||
Signature::build("math stddev")
|
||||
.input_output_types(vec![
|
||||
(Type::List(Box::new(Type::Number)), Type::Number),
|
||||
(Type::Range, Type::Number),
|
||||
(Type::table(), Type::record()),
|
||||
(Type::record(), Type::record()),
|
||||
])
|
||||
@ -54,18 +53,6 @@ impl Command for MathStddev {
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let sample = call.has_flag(engine_state, stack, "sample")?;
|
||||
let name = call.head;
|
||||
let span = input.span().unwrap_or(name);
|
||||
let input: PipelineData = match input.try_expand_range() {
|
||||
Err(_) => {
|
||||
return Err(ShellError::IncorrectValue {
|
||||
msg: "Range must be bounded".to_string(),
|
||||
val_span: span,
|
||||
call_span: name,
|
||||
});
|
||||
}
|
||||
Ok(val) => val,
|
||||
};
|
||||
run_with_function(call, input, compute_stddev(sample))
|
||||
}
|
||||
|
||||
@ -76,18 +63,6 @@ impl Command for MathStddev {
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let sample = call.has_flag_const(working_set, "sample")?;
|
||||
let name = call.head;
|
||||
let span = input.span().unwrap_or(name);
|
||||
let input: PipelineData = match input.try_expand_range() {
|
||||
Err(_) => {
|
||||
return Err(ShellError::IncorrectValue {
|
||||
msg: "Range must be bounded".to_string(),
|
||||
val_span: span,
|
||||
call_span: name,
|
||||
});
|
||||
}
|
||||
Ok(val) => val,
|
||||
};
|
||||
run_with_function(call, input, compute_stddev(sample))
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
use core::slice;
|
||||
use core::{ops::Bound, slice};
|
||||
use indexmap::IndexMap;
|
||||
use nu_protocol::{
|
||||
engine::Call, IntoPipelineData, PipelineData, Range, ShellError, Signals, Span, Value,
|
||||
@ -93,7 +93,10 @@ pub fn calculate(
|
||||
Ok(Value::record(record, span))
|
||||
}
|
||||
PipelineData::Value(Value::Range { val, .. }, ..) => {
|
||||
ensure_bounded(val.as_ref(), span, name)?;
|
||||
match *val {
|
||||
Range::IntRange(range) => ensure_bounded(range.end(), span, name)?,
|
||||
Range::FloatRange(range) => ensure_bounded(range.end(), span, name)?,
|
||||
}
|
||||
let new_vals: Result<Vec<Value>, ShellError> = val
|
||||
.into_range_iter(span, Signals::empty())
|
||||
.map(|val| mf(&[val], span, name))
|
||||
@ -102,7 +105,7 @@ pub fn calculate(
|
||||
mf(&new_vals?, span, name)
|
||||
}
|
||||
PipelineData::Value(val, ..) => mf(&[val], span, name),
|
||||
PipelineData::Empty => Err(ShellError::PipelineEmpty { dst_span: name }),
|
||||
PipelineData::Empty { .. } => Err(ShellError::PipelineEmpty { dst_span: name }),
|
||||
val => Err(ShellError::UnsupportedInput {
|
||||
msg: "Only ints, floats, lists, records, or ranges are supported".into(),
|
||||
input: "value originates from here".into(),
|
||||
@ -114,13 +117,17 @@ pub fn calculate(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_bounded(range: &Range, val_span: Span, call_span: Span) -> Result<(), ShellError> {
|
||||
if range.is_bounded() {
|
||||
return Ok(());
|
||||
pub fn ensure_bounded<T>(
|
||||
bound: Bound<T>,
|
||||
val_span: Span,
|
||||
call_span: Span,
|
||||
) -> Result<(), ShellError> {
|
||||
match bound {
|
||||
Bound::<T>::Unbounded => Err(ShellError::IncorrectValue {
|
||||
msg: "Range must be bounded".to_string(),
|
||||
val_span,
|
||||
call_span,
|
||||
}),
|
||||
_ => Ok(()),
|
||||
}
|
||||
Err(ShellError::IncorrectValue {
|
||||
msg: "Range must be bounded".to_string(),
|
||||
val_span,
|
||||
call_span,
|
||||
})
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ impl Command for MathVariance {
|
||||
Signature::build("math variance")
|
||||
.input_output_types(vec![
|
||||
(Type::List(Box::new(Type::Number)), Type::Number),
|
||||
(Type::Range, Type::Number),
|
||||
(Type::table(), Type::record()),
|
||||
(Type::record(), Type::record()),
|
||||
])
|
||||
@ -46,18 +45,6 @@ impl Command for MathVariance {
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let sample = call.has_flag(engine_state, stack, "sample")?;
|
||||
let name = call.head;
|
||||
let span = input.span().unwrap_or(name);
|
||||
let input: PipelineData = match input.try_expand_range() {
|
||||
Err(_) => {
|
||||
return Err(ShellError::IncorrectValue {
|
||||
msg: "Range must be bounded".to_string(),
|
||||
val_span: span,
|
||||
call_span: name,
|
||||
});
|
||||
}
|
||||
Ok(val) => val,
|
||||
};
|
||||
run_with_function(call, input, compute_variance(sample))
|
||||
}
|
||||
|
||||
@ -68,18 +55,6 @@ impl Command for MathVariance {
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let sample = call.has_flag_const(working_set, "sample")?;
|
||||
let name = call.head;
|
||||
let span = input.span().unwrap_or(name);
|
||||
let input: PipelineData = match input.try_expand_range() {
|
||||
Err(_) => {
|
||||
return Err(ShellError::IncorrectValue {
|
||||
msg: "Range must be bounded".to_string(),
|
||||
val_span: span,
|
||||
call_span: name,
|
||||
});
|
||||
}
|
||||
Ok(val) => val,
|
||||
};
|
||||
run_with_function(call, input, compute_variance(sample))
|
||||
}
|
||||
|
||||
|
@ -723,7 +723,7 @@ fn transform_response_using_content_type(
|
||||
)
|
||||
})?
|
||||
.path_segments()
|
||||
.and_then(|mut segments| segments.next_back())
|
||||
.and_then(|segments| segments.last())
|
||||
.and_then(|name| if name.is_empty() { None } else { Some(name) })
|
||||
.and_then(|name| {
|
||||
PathBuf::from(name)
|
||||
|
@ -117,13 +117,8 @@ impl Command for HttpDelete {
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "http delete from example.com, with custom header using a record",
|
||||
example: "http delete --headers {my-header-key: my-header-value} https://www.example.com",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "http delete from example.com, with custom header using a list",
|
||||
example: "http delete --headers [my-header-key-A my-header-value-A my-header-key-B my-header-value-B] https://www.example.com",
|
||||
description: "http delete from example.com, with custom header",
|
||||
example: "http delete --headers [my-header-key my-header-value] https://www.example.com",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user