Compare commits

..

No commits in common. "main" and "0.103.0" have entirely different histories.

352 changed files with 4857 additions and 13800 deletions

40
.github/labeler.yml vendored
View File

@ -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_*/**

View File

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

View File

@ -8,7 +8,6 @@
name: Nightly Build name: Nightly Build
on: on:
workflow_dispatch:
push: push:
branches: branches:
- nightly # Just for test purpose only with the nightly repo - nightly # Just for test purpose only with the nightly repo
@ -40,7 +39,7 @@ jobs:
uses: hustcer/setup-nu@v3 uses: hustcer/setup-nu@v3
if: github.repository == 'nushell/nightly' if: github.repository == 'nushell/nightly'
with: with:
version: 0.103.0 version: 0.101.0
# Synchronize the main branch of nightly repo with the main branch of Nushell official repo # Synchronize the main branch of nightly repo with the main branch of Nushell official repo
- name: Prepare for Nightly Release - name: Prepare for Nightly Release
@ -140,7 +139,7 @@ jobs:
- name: Setup Nushell - name: Setup Nushell
uses: hustcer/setup-nu@v3 uses: hustcer/setup-nu@v3
with: with:
version: 0.103.0 version: 0.101.0
- name: Release Nu Binary - name: Release Nu Binary
id: nu id: nu
@ -198,7 +197,7 @@ jobs:
- name: Setup Nushell - name: Setup Nushell
uses: hustcer/setup-nu@v3 uses: hustcer/setup-nu@v3
with: with:
version: 0.103.0 version: 0.101.0
# Keep the last a few releases # Keep the last a few releases
- name: Delete Older Releases - name: Delete Older Releases

View File

@ -117,14 +117,14 @@ if $os in ['macos-latest'] or $USE_UBUNTU {
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# Build for Windows without static-link-openssl feature # Build for Windows without static-link-openssl feature
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
if $os =~ 'windows' { if $os in ['windows-latest'] {
cargo-build-nu cargo-build-nu
} }
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# Prepare for the release archive # Prepare for the release archive
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
let suffix = if $os =~ 'windows' { '.exe' } let suffix = if $os == 'windows-latest' { '.exe' }
# nu, nu_plugin_* were all included # nu, nu_plugin_* were all included
let executable = $'target/($target)/release/($bin)*($suffix)' let executable = $'target/($target)/release/($bin)*($suffix)'
print $'Current executable file: ($executable)' 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 [LICENSE ...(glob $executable)] | each {|it| cp -rv $it $dist } | flatten
print $'(char nl)Check binary release version detail:'; hr-line print $'(char nl)Check binary release version detail:'; hr-line
let ver = if $os =~ 'windows' { let ver = if $os == 'windows-latest' {
(do -i { .\output\nu.exe -c 'version' }) | default '' | str join (do -i { .\output\nu.exe -c 'version' }) | str join
} else { } else {
(do -i { ./output/nu -c 'version' }) | default '' | str join (do -i { ./output/nu -c 'version' }) | str join
} }
if ($ver | str trim | is-empty) { if ($ver | str trim | is-empty) {
print $'(ansi r)Incompatible Nu binary: The binary cross compiled is not runnable on current arch...(ansi reset)' 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/ # 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 echo $"archive=($archive)" | save --append $env.GITHUB_OUTPUT
} else if $os =~ 'windows' { } else if $os == 'windows-latest' {
let releaseStem = $'($bin)-($version)-($target)' let releaseStem = $'($bin)-($version)-($target)'
@ -221,7 +221,7 @@ if $os in ['macos-latest'] or $USE_UBUNTU {
} }
def 'cargo-build-nu' [] { def 'cargo-build-nu' [] {
if $os =~ 'windows' { if $os == 'windows-latest' {
cargo build --release --all --target $target cargo build --release --all --target $target
} else { } else {
cargo build --release --all --target $target --features=static-link-openssl cargo build --release --all --target $target --features=static-link-openssl

View File

@ -89,7 +89,7 @@ jobs:
- name: Setup Nushell - name: Setup Nushell
uses: hustcer/setup-nu@v3 uses: hustcer/setup-nu@v3
with: with:
version: 0.103.0 version: 0.101.0
- name: Release Nu Binary - name: Release Nu Binary
id: nu id: nu

View File

@ -10,4 +10,4 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Check spelling - name: Check spelling
uses: crate-ci/typos@v1.31.1 uses: crate-ci/typos@v1.29.10

507
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,8 +10,8 @@ homepage = "https://www.nushell.sh"
license = "MIT" license = "MIT"
name = "nu" name = "nu"
repository = "https://github.com/nushell/nushell" repository = "https://github.com/nushell/nushell"
rust-version = "1.84.1" rust-version = "1.83.0"
version = "0.104.1" version = "0.103.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # 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" ansi-str = "0.8"
anyhow = "1.0.82" anyhow = "1.0.82"
base64 = "0.22.1" base64 = "0.22.1"
bracoxide = "0.1.6" bracoxide = "0.1.5"
brotli = "7.0" brotli = "7.0"
byteorder = "1.5" byteorder = "1.5"
bytes = "1" bytes = "1"
bytesize = "1.3.3" bytesize = "1.3.1"
calamine = "0.27" calamine = "0.26.1"
chardetng = "0.1.17" chardetng = "0.1.17"
chrono = { default-features = false, version = "0.4.34" } chrono = { default-features = false, version = "0.4.34" }
chrono-humanize = "0.2.3" chrono-humanize = "0.2.3"
@ -91,8 +91,8 @@ fancy-regex = "0.14"
filesize = "0.2" filesize = "0.2"
filetime = "0.2" filetime = "0.2"
heck = "0.5.0" heck = "0.5.0"
human-date-parser = "0.3.0" human-date-parser = "0.2.0"
indexmap = "2.9" indexmap = "2.7"
indicatif = "0.17" indicatif = "0.17"
interprocess = "2.2.0" interprocess = "2.2.0"
is_executable = "1.0" is_executable = "1.0"
@ -110,7 +110,7 @@ md5 = { version = "0.10", package = "md-5" }
miette = "7.5" miette = "7.5"
mime = "0.3.17" mime = "0.3.17"
mime_guess = "2.0" mime_guess = "2.0"
mockito = { version = "1.7", default-features = false } mockito = { version = "1.6", default-features = false }
multipart-rs = "0.1.13" multipart-rs = "0.1.13"
native-tls = "0.2" native-tls = "0.2"
nix = { version = "0.29", default-features = false } nix = { version = "0.29", default-features = false }
@ -135,22 +135,22 @@ quick-xml = "0.37.0"
quickcheck = "1.0" quickcheck = "1.0"
quickcheck_macros = "1.0" quickcheck_macros = "1.0"
quote = "1.0" quote = "1.0"
rand = "0.9" rand = "0.8"
getrandom = "0.2" # pick same version that rand requires getrandom = "0.2" # pick same version that rand requires
rand_chacha = "0.9" rand_chacha = "0.3.1"
ratatui = "0.29" ratatui = "0.29"
rayon = "1.10" rayon = "1.10"
reedline = "0.40.0" reedline = "0.39.0"
rmp = "0.8" rmp = "0.8"
rmp-serde = "1.3" rmp-serde = "1.3"
roxmltree = "0.20" roxmltree = "0.20"
rstest = { version = "0.23", default-features = false } rstest = { version = "0.23", default-features = false }
rstest_reuse = "0.7" rstest_reuse = "0.7"
rusqlite = "0.31" rusqlite = "0.31"
rust-embed = "8.7.0" rust-embed = "8.6.0"
scopeguard = { version = "1.2.0" } scopeguard = { version = "1.2.0" }
serde = { version = "1.0" } serde = { version = "1.0" }
serde_json = "1.0.97" serde_json = "1.0"
serde_urlencoded = "0.7.1" serde_urlencoded = "0.7.1"
serde_yaml = "0.9.33" serde_yaml = "0.9.33"
sha2 = "0.10" sha2 = "0.10"
@ -161,24 +161,24 @@ syn = "2.0"
sysinfo = "0.33" sysinfo = "0.33"
tabled = { version = "0.17.0", default-features = false } tabled = { version = "0.17.0", default-features = false }
tempfile = "3.15" tempfile = "3.15"
titlecase = "3.5" titlecase = "3.4"
toml = "0.8" toml = "0.8"
trash = "5.2" trash = "5.2"
update-informer = { version = "1.2.0", default-features = false, features = ["github", "native-tls", "ureq"] } update-informer = { version = "1.2.0", default-features = false, features = ["github", "native-tls", "ureq"] }
umask = "2.1" umask = "2.1"
unicode-segmentation = "1.12" unicode-segmentation = "1.12"
unicode-width = "0.2" unicode-width = "0.2"
ureq = { version = "2.12", default-features = false, features = ["socks-proxy"] } ureq = { version = "2.12", default-features = false }
url = "2.2" url = "2.2"
uu_cp = "0.0.30" uu_cp = "0.0.29"
uu_mkdir = "0.0.30" uu_mkdir = "0.0.29"
uu_mktemp = "0.0.30" uu_mktemp = "0.0.29"
uu_mv = "0.0.30" uu_mv = "0.0.29"
uu_touch = "0.0.30" uu_touch = "0.0.29"
uu_whoami = "0.0.30" uu_whoami = "0.0.29"
uu_uname = "0.0.30" uu_uname = "0.0.29"
uucore = "0.0.30" uucore = "0.0.29"
uuid = "1.16.0" uuid = "1.12.0"
v_htmlescape = "0.15.0" v_htmlescape = "0.15.0"
wax = "0.6" wax = "0.6"
web-time = "1.1.0" web-time = "1.1.0"
@ -197,22 +197,22 @@ unchecked_duration_subtraction = "warn"
workspace = true workspace = true
[dependencies] [dependencies]
nu-cli = { path = "./crates/nu-cli", version = "0.104.1" } nu-cli = { path = "./crates/nu-cli", version = "0.103.0" }
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.104.1" } nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.103.0" }
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.104.1" } nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.103.0" }
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.104.1", optional = true } nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.103.0", optional = true }
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.104.1" } nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.103.0" }
nu-command = { path = "./crates/nu-command", version = "0.104.1" } nu-command = { path = "./crates/nu-command", version = "0.103.0" }
nu-engine = { path = "./crates/nu-engine", version = "0.104.1" } nu-engine = { path = "./crates/nu-engine", version = "0.103.0" }
nu-explore = { path = "./crates/nu-explore", version = "0.104.1" } nu-explore = { path = "./crates/nu-explore", version = "0.103.0" }
nu-lsp = { path = "./crates/nu-lsp/", version = "0.104.1" } nu-lsp = { path = "./crates/nu-lsp/", version = "0.103.0" }
nu-parser = { path = "./crates/nu-parser", version = "0.104.1" } nu-parser = { path = "./crates/nu-parser", version = "0.103.0" }
nu-path = { path = "./crates/nu-path", version = "0.104.1" } nu-path = { path = "./crates/nu-path", version = "0.103.0" }
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.104.1" } nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.103.0" }
nu-protocol = { path = "./crates/nu-protocol", version = "0.104.1" } nu-protocol = { path = "./crates/nu-protocol", version = "0.103.0" }
nu-std = { path = "./crates/nu-std", version = "0.104.1" } nu-std = { path = "./crates/nu-std", version = "0.103.0" }
nu-system = { path = "./crates/nu-system", version = "0.104.1" } nu-system = { path = "./crates/nu-system", version = "0.103.0" }
nu-utils = { path = "./crates/nu-utils", version = "0.104.1" } nu-utils = { path = "./crates/nu-utils", version = "0.103.0" }
reedline = { workspace = true, features = ["bashisms", "sqlite"] } reedline = { workspace = true, features = ["bashisms", "sqlite"] }
crossterm = { workspace = true } crossterm = { workspace = true }
@ -241,9 +241,9 @@ nix = { workspace = true, default-features = false, features = [
] } ] }
[dev-dependencies] [dev-dependencies]
nu-test-support = { path = "./crates/nu-test-support", 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.104.1" } nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.103.0" }
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.104.1" } nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.103.0" }
assert_cmd = "2.0" assert_cmd = "2.0"
dirs = { workspace = true } dirs = { workspace = true }
tango-bench = "0.6" tango-bench = "0.6"

View File

@ -222,7 +222,6 @@ Please submit an issue or PR to be added to this list.
- [Dorothy](http://github.com/bevry/dorothy) - [Dorothy](http://github.com/bevry/dorothy)
- [Direnv](https://github.com/direnv/direnv/blob/master/docs/hook.md#nushell) - [Direnv](https://github.com/direnv/direnv/blob/master/docs/hook.md#nushell)
- [x-cmd](https://x-cmd.com/mod/nu) - [x-cmd](https://x-cmd.com/mod/nu)
- [vfox](https://github.com/version-fox/vfox)
## Contributing ## Contributing

View File

@ -5,29 +5,28 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cli"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "nu-cli" name = "nu-cli"
version = "0.104.1" version = "0.103.0"
[lib] [lib]
bench = false bench = false
[dev-dependencies] [dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.104.1" } nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.103.0" }
nu-command = { path = "../nu-command", version = "0.104.1" } nu-command = { path = "../nu-command", version = "0.103.0" }
nu-std = { path = "../nu-std", version = "0.104.1" } nu-test-support = { path = "../nu-test-support", version = "0.103.0" }
nu-test-support = { path = "../nu-test-support", version = "0.104.1" }
rstest = { workspace = true, default-features = false } rstest = { workspace = true, default-features = false }
tempfile = { workspace = true } tempfile = { workspace = true }
[dependencies] [dependencies]
nu-cmd-base = { path = "../nu-cmd-base", version = "0.104.1" } nu-cmd-base = { path = "../nu-cmd-base", version = "0.103.0" }
nu-engine = { path = "../nu-engine", version = "0.104.1", features = ["os"] } nu-engine = { path = "../nu-engine", version = "0.103.0", features = ["os"] }
nu-glob = { path = "../nu-glob", version = "0.104.1" } nu-glob = { path = "../nu-glob", version = "0.103.0" }
nu-path = { path = "../nu-path", version = "0.104.1" } nu-path = { path = "../nu-path", version = "0.103.0" }
nu-parser = { path = "../nu-parser", version = "0.104.1" } nu-parser = { path = "../nu-parser", version = "0.103.0" }
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.104.1", optional = true } nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.103.0", optional = true }
nu-protocol = { path = "../nu-protocol", version = "0.104.1", features = ["os"] } nu-protocol = { path = "../nu-protocol", version = "0.103.0", features = ["os"] }
nu-utils = { path = "../nu-utils", version = "0.104.1" } nu-utils = { path = "../nu-utils", version = "0.103.0" }
nu-color-config = { path = "../nu-color-config", version = "0.104.1" } nu-color-config = { path = "../nu-color-config", version = "0.103.0" }
nu-ansi-term = { workspace = true } nu-ansi-term = { workspace = true }
reedline = { workspace = true, features = ["bashisms", "sqlite"] } reedline = { workspace = true, features = ["bashisms", "sqlite"] }

View File

@ -105,9 +105,10 @@ impl Command for History {
.ok() .ok()
}) })
.map(move |entries| { .map(move |entries| {
entries.into_iter().enumerate().map(move |(idx, entry)| { entries
create_sqlite_history_record(idx, entry, long, head) .into_iter()
}) .enumerate()
.map(move |(idx, entry)| create_history_record(idx, entry, long, head))
}) })
.ok_or(IoError::new( .ok_or(IoError::new(
std::io::ErrorKind::NotFound, 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 //1. Format all the values
//2. Create a record of either short or long columns and 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(), .unwrap_or_default(),
head, head,
); );
let start_timestamp_value = Value::date( let start_timestamp_value = Value::string(
entry.start_timestamp.unwrap_or_default().fixed_offset(), entry
.start_timestamp
.map(|time| time.to_string())
.unwrap_or_default(),
head, head,
); );
let command_value = Value::string(entry.command_line, head); let command_value = Value::string(entry.command_line, head);

View File

@ -26,7 +26,7 @@ impl Command for HistoryImport {
fn extra_description(&self) -> &str { 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: 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. 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.

View File

@ -27,7 +27,7 @@ impl Completer for AttributeCompletion {
let attr_commands = let attr_commands =
working_set.find_commands_by_predicate(|s| s.starts_with(b"attr "), true); 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); let name = name.strip_prefix(b"attr ").unwrap_or(&name);
matcher.add_semantic_suggestion(SemanticSuggestion { matcher.add_semantic_suggestion(SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
@ -41,7 +41,7 @@ impl Completer for AttributeCompletion {
}, },
append_whitespace: false, 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, append_whitespace: false,
}, },
kind: Some(SuggestionKind::Command(cmd.command_type(), None)), kind: Some(SuggestionKind::Command(cmd.command_type())),
}); });
} }

View File

@ -1,7 +1,7 @@
use crate::completions::CompletionOptions; use crate::completions::CompletionOptions;
use nu_protocol::{ use nu_protocol::{
engine::{Stack, StateWorkingSet}, engine::{Stack, StateWorkingSet},
DeclId, Span, Span,
}; };
use reedline::Suggestion; use reedline::Suggestion;
@ -28,7 +28,7 @@ pub struct SemanticSuggestion {
// TODO: think about name: maybe suggestion context? // TODO: think about name: maybe suggestion context?
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum SuggestionKind { pub enum SuggestionKind {
Command(nu_protocol::engine::CommandType, Option<DeclId>), Command(nu_protocol::engine::CommandType),
Value(nu_protocol::Type), Value(nu_protocol::Type),
CellPath, CellPath,
Directory, Directory,

View File

@ -1,5 +1,3 @@
use std::borrow::Cow;
use crate::completions::{Completer, CompletionOptions, SemanticSuggestion, SuggestionKind}; use crate::completions::{Completer, CompletionOptions, SemanticSuggestion, SuggestionKind};
use nu_engine::{column::get_columns, eval_variable}; use nu_engine::{column::get_columns, eval_variable};
use nu_protocol::{ use nu_protocol::{
@ -19,14 +17,14 @@ pub struct CellPathCompletion<'a> {
fn prefix_from_path_member(member: &PathMember, pos: usize) -> (String, Span) { fn prefix_from_path_member(member: &PathMember, pos: usize) -> (String, Span) {
let (prefix_str, start) = match member { let (prefix_str, start) = match member {
PathMember::String { val, span, .. } => (val, span.start), PathMember::String { val, span, .. } => (val.clone(), span.start),
PathMember::Int { val, span, .. } => (&val.to_string(), span.start), PathMember::Int { val, span, .. } => (val.to_string(), span.start),
}; };
let prefix_str = prefix_str.get(..pos + 1 - start).unwrap_or(prefix_str); let prefix_str = prefix_str
// strip wrapping quotes .get(..pos + 1 - start)
let quotations = ['"', '\'', '`']; .map(str::to_string)
let prefix_str = prefix_str.strip_prefix(quotations).unwrap_or(prefix_str); .unwrap_or(prefix_str);
(prefix_str.to_string(), Span::new(start, pos + 1)) (prefix_str, Span::new(start, pos + 1))
} }
impl Completer for CellPathCompletion<'_> { impl Completer for CellPathCompletion<'_> {
@ -103,35 +101,21 @@ pub(crate) fn eval_cell_path(
} else { } else {
eval_constant(working_set, head) eval_constant(working_set, head)
}?; }?;
head_value head_value.follow_cell_path(path_members, false)
.follow_cell_path(path_members, false)
.map(Cow::into_owned)
} }
fn get_suggestions_by_value( fn get_suggestions_by_value(
value: &Value, value: &Value,
current_span: reedline::Span, current_span: reedline::Span,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let to_suggestion = |s: String, v: Option<&Value>| { let to_suggestion = |s: String, v: Option<&Value>| SemanticSuggestion {
// Check if the string needs quoting suggestion: Suggestion {
let value = if s.is_empty() value: s,
|| s.chars() span: current_span,
.any(|c: char| !(c.is_ascii_alphabetic() || ['_', '-'].contains(&c))) description: v.map(|v| v.get_type().to_string()),
{ ..Suggestion::default()
format!("{:?}", s) },
} else { kind: Some(SuggestionKind::CellPath),
s
};
SemanticSuggestion {
suggestion: Suggestion {
value,
span: current_span,
description: v.map(|v| v.get_type().to_string()),
..Suggestion::default()
},
kind: Some(SuggestionKind::CellPath),
}
}; };
match value { match value {
Value::Record { val, .. } => val Value::Record { val, .. } => val

View File

@ -75,10 +75,7 @@ impl CommandCompletion {
append_whitespace: true, append_whitespace: true,
..Default::default() ..Default::default()
}, },
kind: Some(SuggestionKind::Command( kind: Some(SuggestionKind::Command(CommandType::External)),
CommandType::External,
None,
)),
}, },
); );
} }
@ -115,7 +112,7 @@ impl Completer for CommandCompletion {
}, },
true, true,
); );
for (decl_id, name, description, typ) in filtered_commands { for (name, description, typ) in filtered_commands {
let name = String::from_utf8_lossy(&name); let name = String::from_utf8_lossy(&name);
internal_suggs.insert( internal_suggs.insert(
name.to_string(), name.to_string(),
@ -127,7 +124,7 @@ impl Completer for CommandCompletion {
append_whitespace: true, append_whitespace: true,
..Suggestion::default() ..Suggestion::default()
}, },
kind: Some(SuggestionKind::Command(typ, Some(decl_id))), kind: Some(SuggestionKind::Command(typ)),
}, },
); );
} }

View File

@ -1,20 +1,21 @@
use crate::completions::{ use crate::completions::{
base::{SemanticSuggestion, SuggestionKind},
AttributableCompletion, AttributeCompletion, CellPathCompletion, CommandCompletion, Completer, AttributableCompletion, AttributeCompletion, CellPathCompletion, CommandCompletion, Completer,
CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion, CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion, FileCompletion,
ExportableCompletion, FileCompletion, FlagCompletion, OperatorCompletion, VariableCompletion, FlagCompletion, OperatorCompletion, VariableCompletion,
}; };
use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style}; use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style};
use nu_engine::eval_block; 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::{ use nu_protocol::{
ast::{Argument, Block, Expr, Expression, FindMapResult, ListItem, Traverse}, ast::{Argument, Block, Expr, Expression, FindMapResult, Traverse},
debugger::WithoutDebug, debugger::WithoutDebug,
engine::{Closure, EngineState, Stack, StateWorkingSet}, engine::{Closure, EngineState, Stack, StateWorkingSet},
PipelineData, Span, Type, Value, PipelineData, Span, Type, Value,
}; };
use reedline::{Completer as ReedlineCompleter, Suggestion}; 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 /// Used as the function `f` in find_map Traverse
/// ///
@ -56,13 +57,8 @@ fn find_pipeline_element_by_position<'a>(
Expr::FullCellPath(fcp) => fcp Expr::FullCellPath(fcp) => fcp
.head .head
.find_map(working_set, &closure) .find_map(working_set, &closure)
.or(Some(expr))
.map(FindMapResult::Found) .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(), .unwrap_or_default(),
Expr::Var(_) => FindMapResult::Found(expr), Expr::Var(_) => FindMapResult::Found(expr),
Expr::AttributeBlock(ab) => ab Expr::AttributeBlock(ab) => ab
@ -131,18 +127,6 @@ struct Context<'a> {
offset: usize, 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<'_> { impl Context<'_> {
fn new<'a>( fn new<'a>(
working_set: &'a StateWorkingSet, working_set: &'a StateWorkingSet,
@ -344,8 +328,7 @@ impl NuCompleter {
// NOTE: the argument to complete is not necessarily the last one // NOTE: the argument to complete is not necessarily the last one
// for lsp completion, we don't trim the text, // for lsp completion, we don't trim the text,
// so that `def`s after pos can be completed // so that `def`s after pos can be completed
let mut positional_arg_indices = Vec::new(); for arg in call.arguments.iter() {
for (arg_idx, arg) in call.arguments.iter().enumerate() {
let span = arg.span(); let span = arg.span();
if span.contains(pos) { if span.contains(pos) {
// if customized completion specified, it has highest priority // if customized completion specified, it has highest priority
@ -395,16 +378,10 @@ impl NuCompleter {
Argument::Positional(_) if prefix == b"-" => flag_completion_helper(), Argument::Positional(_) if prefix == b"-" => flag_completion_helper(),
// complete according to expression type and command head // complete according to expression type and command head
Argument::Positional(expr) => { Argument::Positional(expr) => {
let command_head = working_set.get_decl(call.decl_id).name(); let command_head = working_set.get_span_contents(call.head);
positional_arg_indices.push(arg_idx);
self.argument_completion_helper( self.argument_completion_helper(
PositionalArguments { command_head,
command_head, expr,
positional_arg_indices,
arguments: &call.arguments,
expr,
},
pos,
&ctx, &ctx,
suggestions.is_empty(), suggestions.is_empty(),
) )
@ -412,8 +389,6 @@ impl NuCompleter {
_ => vec![], _ => vec![],
}); });
break; break;
} else if !matches!(arg, Argument::Named(_)) {
positional_arg_indices.push(arg_idx);
} }
} }
} }
@ -511,10 +486,9 @@ impl NuCompleter {
externals: bool, externals: bool,
strip: bool, strip: bool,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let config = self.engine_state.get_config();
let mut command_completions = CommandCompletion { let mut command_completions = CommandCompletion {
internals, internals,
externals: !internals || (externals && config.completions.external.enable), externals,
}; };
let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip); let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
let ctx = Context::new(working_set, new_span, prefix, offset); let ctx = Context::new(working_set, new_span, prefix, offset);
@ -523,97 +497,20 @@ impl NuCompleter {
fn argument_completion_helper( fn argument_completion_helper(
&self, &self,
argument_info: PositionalArguments, command_head: &[u8],
pos: usize, expr: &Expression,
ctx: &Context, ctx: &Context,
need_fallback: bool, need_fallback: bool,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let PositionalArguments {
command_head,
positional_arg_indices,
arguments,
expr,
} = argument_info;
// special commands // special commands
match command_head { match command_head {
// complete module file/directory // complete module file/directory
"use" | "export use" | "overlay use" | "source-env" // TODO: if module file already specified,
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,
// should parse it to get modules/commands/consts to complete // should parse it to get modules/commands/consts to complete
"use" | "export use" => { b"use" | b"export use" | b"overlay use" | b"source-env" => {
let Some(Argument::Positional(Expression { return self.process_completion(&mut DotNuCompletion, ctx);
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![],
}
} }
"which" => { b"which" => {
let mut completer = CommandCompletion { let mut completer = CommandCompletion {
internals: true, internals: true,
externals: true, externals: true,
@ -646,6 +543,7 @@ impl NuCompleter {
case_sensitive: config.completions.case_sensitive, case_sensitive: config.completions.case_sensitive,
match_algorithm: config.completions.algorithm.into(), match_algorithm: config.completions.algorithm.into(),
sort: config.completions.sort, sort: config.completions.sort,
..Default::default()
}; };
completer.fetch( completer.fetch(

View File

@ -22,22 +22,18 @@ pub struct PathBuiltFromString {
/// Recursively goes through paths that match a given `partial`. /// Recursively goes through paths that match a given `partial`.
/// built: State struct for a valid matching path built so far. /// 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. /// `isdir`: whether the current partial path has a trailing slash.
/// Parsing a path string into a pathbuf loses that bit of information. /// Parsing a path string into a pathbuf loses that bit of information.
/// ///
/// `enable_exact_match`: Whether match algorithm is Prefix and all previous components /// want_directory: Whether we want only directories as completion matches.
/// of the path matched a directory exactly. /// 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( fn complete_rec(
partial: &[&str], partial: &[&str],
built_paths: &[PathBuiltFromString], built_paths: &[PathBuiltFromString],
options: &CompletionOptions, options: &CompletionOptions,
want_directory: bool, want_directory: bool,
isdir: bool, isdir: bool,
enable_exact_match: bool,
) -> Vec<PathBuiltFromString> { ) -> Vec<PathBuiltFromString> {
if let Some((&base, rest)) = partial.split_first() { if let Some((&base, rest)) = partial.split_first() {
if base.chars().all(|c| c == '.') && (isdir || !rest.is_empty()) { if base.chars().all(|c| c == '.') && (isdir || !rest.is_empty()) {
@ -50,14 +46,7 @@ fn complete_rec(
built built
}) })
.collect(); .collect();
return complete_rec( return complete_rec(rest, &built_paths, options, want_directory, isdir);
rest,
&built_paths,
options,
want_directory,
isdir,
enable_exact_match,
);
} }
} }
@ -97,26 +86,27 @@ fn complete_rec(
// Serves as confirmation to ignore longer completions for // Serves as confirmation to ignore longer completions for
// components in between. // components in between.
if !rest.is_empty() || isdir { 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( completions.extend(complete_rec(
rest, rest,
&[built], &[built],
options, options,
want_directory, want_directory,
isdir, 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 { if exact_match {
break; break;
} }
} else {
completions.push(built);
} }
} }
None => { None => {
@ -150,7 +140,7 @@ impl OriginalCwd {
} }
} }
pub fn surround_remove(partial: &str) -> String { fn surround_remove(partial: &str) -> String {
for c in ['`', '"', '\''] { for c in ['`', '"', '\''] {
if partial.starts_with(c) { if partial.starts_with(c) {
let ret = partial.strip_prefix(c).unwrap_or(partial); 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 let ls_colors = (engine_state.config.completions.use_ls_colors
&& engine_state.config.use_ansi_coloring.get(engine_state)) && engine_state.config.use_ansi_coloring.get(engine_state))
.then(|| { .then(|| {
let ls_colors_env_str = stack let ls_colors_env_str = match stack.get_env_var(engine_state, "LS_COLORS") {
.get_env_var(engine_state, "LS_COLORS") Some(v) => env_to_string("LS_COLORS", v, engine_state, stack).ok(),
.and_then(|v| env_to_string("LS_COLORS", v, engine_state, stack).ok()); None => None,
};
get_ls_colors(ls_colors_env_str) get_ls_colors(ls_colors_env_str)
}); });
@ -265,7 +256,6 @@ pub fn complete_item(
options, options,
want_directory, want_directory,
isdir, isdir,
options.match_algorithm == MatchAlgorithm::Prefix,
) )
.into_iter() .into_iter()
.map(|mut p| { .map(|mut p| {
@ -274,12 +264,15 @@ pub fn complete_item(
} }
let is_dir = p.isdir; let is_dir = p.isdir;
let path = original_cwd.apply(p, path_separator); 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| { let style = ls_colors.as_ref().map(|lsc| {
lsc.style_for_path_with_metadata(&real_path, metadata.as_ref()) lsc.style_for_path_with_metadata(
.map(lscolors::Style::to_nu_ansi_term_style) &path,
.unwrap_or_default() std::fs::symlink_metadata(expand_to_real_path(&path))
.ok()
.as_ref(),
)
.map(lscolors::Style::to_nu_ansi_term_style)
.unwrap_or_default()
}); });
FileSuggestion { FileSuggestion {
span, span,

View File

@ -18,12 +18,6 @@ pub enum MatchAlgorithm {
/// "git switch" is matched by "git sw" /// "git switch" is matched by "git sw"
Prefix, 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 /// Only show suggestions which contain the input chars at any place
/// ///
/// Example: /// Example:
@ -42,10 +36,6 @@ enum State<T> {
/// Holds (haystack, item) /// Holds (haystack, item)
items: Vec<(String, T)>, items: Vec<(String, T)>,
}, },
Substring {
/// Holds (haystack, item)
items: Vec<(String, T)>,
},
Fuzzy { Fuzzy {
matcher: Matcher, matcher: Matcher,
atom: Atom, atom: Atom,
@ -74,18 +64,6 @@ impl<T> NuMatcher<'_, T> {
state: State::Prefix { items: Vec::new() }, 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 => { MatchAlgorithm::Fuzzy => {
let atom = Atom::new( let atom = Atom::new(
needle, needle,
@ -124,21 +102,11 @@ impl<T> NuMatcher<'_, T> {
} else { } else {
Cow::Owned(haystack.to_folded_case()) Cow::Owned(haystack.to_folded_case())
}; };
let matches = haystack_folded.starts_with(self.needle.as_str()); let matches = if self.options.positional {
if matches { haystack_folded.starts_with(self.needle.as_str())
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)
} else { } 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 matches {
if let Some(item) = item { if let Some(item) = item {
items.push((haystack.to_string(), item)); items.push((haystack.to_string(), item));
@ -180,7 +148,7 @@ impl<T> NuMatcher<'_, T> {
/// Get all the items that matched (sorted) /// Get all the items that matched (sorted)
pub fn results(self) -> Vec<T> { pub fn results(self) -> Vec<T> {
match self.state { match self.state {
State::Prefix { mut items, .. } | State::Substring { mut items, .. } => { State::Prefix { mut items, .. } => {
items.sort_by(|(haystack1, _), (haystack2, _)| { items.sort_by(|(haystack1, _), (haystack2, _)| {
let cmp_sensitive = haystack1.cmp(haystack2); let cmp_sensitive = haystack1.cmp(haystack2);
if self.options.case_sensitive { if self.options.case_sensitive {
@ -227,7 +195,6 @@ impl From<CompletionAlgorithm> for MatchAlgorithm {
fn from(value: CompletionAlgorithm) -> Self { fn from(value: CompletionAlgorithm) -> Self {
match value { match value {
CompletionAlgorithm::Prefix => MatchAlgorithm::Prefix, CompletionAlgorithm::Prefix => MatchAlgorithm::Prefix,
CompletionAlgorithm::Substring => MatchAlgorithm::Substring,
CompletionAlgorithm::Fuzzy => MatchAlgorithm::Fuzzy, CompletionAlgorithm::Fuzzy => MatchAlgorithm::Fuzzy,
} }
} }
@ -239,7 +206,6 @@ impl TryFrom<String> for MatchAlgorithm {
fn try_from(value: String) -> Result<Self, Self::Error> { fn try_from(value: String) -> Result<Self, Self::Error> {
match value.as_str() { match value.as_str() {
"prefix" => Ok(Self::Prefix), "prefix" => Ok(Self::Prefix),
"substring" => Ok(Self::Substring),
"fuzzy" => Ok(Self::Fuzzy), "fuzzy" => Ok(Self::Fuzzy),
_ => Err(InvalidMatchAlgorithm::Unknown), _ => Err(InvalidMatchAlgorithm::Unknown),
} }
@ -264,6 +230,7 @@ impl std::error::Error for InvalidMatchAlgorithm {}
#[derive(Clone)] #[derive(Clone)]
pub struct CompletionOptions { pub struct CompletionOptions {
pub case_sensitive: bool, pub case_sensitive: bool,
pub positional: bool,
pub match_algorithm: MatchAlgorithm, pub match_algorithm: MatchAlgorithm,
pub sort: CompletionSort, pub sort: CompletionSort,
} }
@ -272,6 +239,7 @@ impl Default for CompletionOptions {
fn default() -> Self { fn default() -> Self {
Self { Self {
case_sensitive: true, case_sensitive: true,
positional: true,
match_algorithm: MatchAlgorithm::Prefix, match_algorithm: MatchAlgorithm::Prefix,
sort: Default::default(), sort: Default::default(),
} }
@ -288,9 +256,6 @@ mod test {
#[case(MatchAlgorithm::Prefix, "example text", "", true)] #[case(MatchAlgorithm::Prefix, "example text", "", true)]
#[case(MatchAlgorithm::Prefix, "example text", "examp", true)] #[case(MatchAlgorithm::Prefix, "example text", "examp", true)]
#[case(MatchAlgorithm::Prefix, "example text", "text", false)] #[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", "", true)]
#[case(MatchAlgorithm::Fuzzy, "example text", "examp", true)] #[case(MatchAlgorithm::Fuzzy, "example text", "examp", true)]
#[case(MatchAlgorithm::Fuzzy, "example text", "ext", true)] #[case(MatchAlgorithm::Fuzzy, "example text", "ext", true)]

View File

@ -1,12 +1,11 @@
use crate::completions::{ use crate::completions::{
completer::map_value_completions, Completer, CompletionOptions, MatchAlgorithm, completer::map_value_completions, Completer, CompletionOptions, SemanticSuggestion,
SemanticSuggestion,
}; };
use nu_engine::eval_call; use nu_engine::eval_call;
use nu_protocol::{ use nu_protocol::{
ast::{Argument, Call, Expr, Expression}, ast::{Argument, Call, Expr, Expression},
debugger::WithoutDebug, debugger::WithoutDebug,
engine::{EngineState, Stack, StateWorkingSet}, engine::{Stack, StateWorkingSet},
DeclId, PipelineData, Span, Type, Value, DeclId, PipelineData, Span, Type, Value,
}; };
use std::collections::HashMap; use std::collections::HashMap;
@ -43,37 +42,28 @@ impl<T: Completer> Completer for CustomCompletion<T> {
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
// Call custom declaration // Call custom declaration
let mut stack_mut = stack.clone(); let mut stack_mut = stack.clone();
let mut eval = |engine_state: &EngineState| { let result = eval_call::<WithoutDebug>(
eval_call::<WithoutDebug>( working_set.permanent_state,
engine_state, &mut stack_mut,
&mut stack_mut, &Call {
&Call { decl_id: self.decl_id,
decl_id: self.decl_id, head: span,
head: span, arguments: vec![
arguments: vec![ Argument::Positional(Expression::new_unknown(
Argument::Positional(Expression::new_unknown( Expr::String(self.line.clone()),
Expr::String(self.line.clone()), Span::unknown(),
Span::unknown(), Type::String,
Type::String, )),
)), Argument::Positional(Expression::new_unknown(
Argument::Positional(Expression::new_unknown( Expr::Int(self.line_pos as i64),
Expr::Int(self.line_pos as i64), Span::unknown(),
Span::unknown(), Type::Int,
Type::Int, )),
)), ],
], parser_info: HashMap::new(),
parser_info: HashMap::new(), },
}, PipelineData::empty(),
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 mut completion_options = orig_options.clone(); let mut completion_options = orig_options.clone();
let mut should_sort = true; let mut should_sort = true;
@ -103,10 +93,10 @@ impl<T: Completer> Completer for CustomCompletion<T> {
{ {
completion_options.case_sensitive = case_sensitive; completion_options.case_sensitive = case_sensitive;
} }
let positional = if let Some(positional) =
options.get("positional").and_then(|val| val.as_bool().ok()); 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."); completion_options.positional = positional;
} }
if let Some(algorithm) = options if let Some(algorithm) = options
.get("completion_algorithm") .get("completion_algorithm")
@ -114,11 +104,6 @@ impl<T: Completer> Completer for CustomCompletion<T> {
.and_then(|option| option.try_into().ok()) .and_then(|option| option.try_into().ok())
{ {
completion_options.match_algorithm = algorithm; completion_options.match_algorithm = algorithm;
if let Some(false) = positional {
if completion_options.match_algorithm == MatchAlgorithm::Prefix {
completion_options.match_algorithm = MatchAlgorithm::Substring
}
}
} }
} }

View File

@ -1,23 +1,18 @@
use crate::completions::{ use crate::completions::{file_path_completion, Completer, CompletionOptions};
completion_common::{surround_remove, FileSuggestion},
completion_options::NuMatcher,
file_path_completion, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind,
};
use nu_path::expand_tilde; use nu_path::expand_tilde;
use nu_protocol::{ use nu_protocol::{
engine::{Stack, StateWorkingSet, VirtualPath}, engine::{Stack, StateWorkingSet},
Span, Span,
}; };
use reedline::Suggestion; use reedline::Suggestion;
use std::{ use std::{
collections::HashSet, collections::HashSet,
path::{is_separator, PathBuf, MAIN_SEPARATOR_STR}, path::{is_separator, PathBuf, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR},
}; };
pub struct DotNuCompletion { use super::{SemanticSuggestion, SuggestionKind};
/// e.g. use std/a<tab>
pub std_virtual_path: bool, pub struct DotNuCompletion;
}
impl Completer for DotNuCompletion { impl Completer for DotNuCompletion {
fn fetch( fn fetch(
@ -107,7 +102,7 @@ impl Completer for DotNuCompletion {
// Fetch the files filtering the ones that ends with .nu // Fetch the files filtering the ones that ends with .nu
// and transform them into suggestions // and transform them into suggestions
let mut completions = file_path_completion( let completions = file_path_completion(
span, span,
partial, partial,
&search_dirs &search_dirs
@ -118,60 +113,17 @@ impl Completer for DotNuCompletion {
working_set.permanent_state, working_set.permanent_state,
stack, 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 completions
.into_iter() .into_iter()
// Different base dir, so we list the .nu files or folders // Different base dir, so we list the .nu files or folders
.filter(|it| { .filter(|it| {
// for paths with spaces in them // for paths with spaces in them
let path = it.path.trim_end_matches('`'); let path = it.path.trim_end_matches('`');
path.ends_with(".nu") || it.is_dir path.ends_with(".nu") || path.ends_with(SEP)
}) })
.map(|x| { .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 // Re-calculate the span to replace
let mut span_offset = 0; let mut span_offset = 0;
let mut value = x.path.to_string(); let mut value = x.path.to_string();

View File

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

View File

@ -1,12 +1,12 @@
use crate::completions::{ use crate::completions::{completion_options::NuMatcher, Completer, CompletionOptions};
completion_options::NuMatcher, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind,
};
use nu_protocol::{ use nu_protocol::{
engine::{Stack, StateWorkingSet}, engine::{Stack, StateWorkingSet},
DeclId, Span, DeclId, Span,
}; };
use reedline::Suggestion; use reedline::Suggestion;
use super::{SemanticSuggestion, SuggestionKind};
#[derive(Clone)] #[derive(Clone)]
pub struct FlagCompletion { pub struct FlagCompletion {
pub decl_id: DeclId, pub decl_id: DeclId,

View File

@ -8,7 +8,6 @@ mod completion_options;
mod custom_completions; mod custom_completions;
mod directory_completions; mod directory_completions;
mod dotnu_completions; mod dotnu_completions;
mod exportable_completions;
mod file_completions; mod file_completions;
mod flag_completions; mod flag_completions;
mod operator_completions; mod operator_completions;
@ -23,7 +22,6 @@ pub use completion_options::{CompletionOptions, MatchAlgorithm};
pub use custom_completions::CustomCompletion; pub use custom_completions::CustomCompletion;
pub use directory_completions::DirectoryCompletion; pub use directory_completions::DirectoryCompletion;
pub use dotnu_completions::DotNuCompletion; pub use dotnu_completions::DotNuCompletion;
pub use exportable_completions::ExportableCompletion;
pub use file_completions::{file_path_completion, FileCompletion}; pub use file_completions::{file_path_completion, FileCompletion};
pub use flag_completions::FlagCompletion; pub use flag_completions::FlagCompletion;
pub use operator_completions::OperatorCompletion; pub use operator_completions::OperatorCompletion;

View File

@ -864,7 +864,7 @@ fn do_auto_cd(
path.to_string_lossy().to_string() 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( report_shell_error(
engine_state, engine_state,
&ShellError::Io(IoError::new_with_additional_context( &ShellError::Io(IoError::new_with_additional_context(

View File

@ -10,8 +10,7 @@ use nu_cli::NuCompleter;
use nu_engine::eval_block; use nu_engine::eval_block;
use nu_parser::parse; use nu_parser::parse;
use nu_path::expand_tilde; use nu_path::expand_tilde;
use nu_protocol::{debugger::WithoutDebug, engine::StateWorkingSet, Config, PipelineData}; use nu_protocol::{debugger::WithoutDebug, engine::StateWorkingSet, PipelineData};
use nu_std::load_standard_library;
use reedline::{Completer, Suggestion}; use reedline::{Completer, Suggestion};
use rstest::{fixture, rstest}; use rstest::{fixture, rstest};
use support::{ use support::{
@ -228,12 +227,14 @@ fn customcompletions_override_options() {
let mut completer = custom_completer_with_options( let mut completer = custom_completer_with_options(
r#"$env.config.completions.algorithm = "fuzzy" r#"$env.config.completions.algorithm = "fuzzy"
$env.config.completions.case_sensitive = false"#, $env.config.completions.case_sensitive = false"#,
r#"completion_algorithm: "substring", r#"completion_algorithm: "prefix",
positional: false,
case_sensitive: true, case_sensitive: true,
sort: true"#, sort: true"#,
&["Foo Abcdef", "Abcdef", "Acd Bar"], &["Foo Abcdef", "Abcdef", "Acd Bar"],
); );
// positional: false should make it do substring matching
// sort: true should force sorting // sort: true should force sorting
let expected: Vec<_> = vec!["Abcdef", "Foo Abcdef"]; let expected: Vec<_> = vec!["Abcdef", "Foo Abcdef"];
let suggestions = completer.complete("my-command Abcd", 15); let suggestions = completer.complete("my-command Abcd", 15);
@ -349,24 +350,9 @@ fn custom_arguments_vs_subcommands() {
match_suggestions(&expected, &suggestions); 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 `^` /// External command only if starts with `^`
#[test] #[test]
fn external_commands() { fn external_commands_only() {
let engine = new_external_engine(); let engine = new_external_engine();
let mut completer = NuCompleter::new( let mut completer = NuCompleter::new(
Arc::new(engine), Arc::new(engine),
@ -389,31 +375,6 @@ fn external_commands() {
match_suggestions(&expected, &suggestions); 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 /// Which completes both internals and externals
#[test] #[test]
fn which_command_completions() { fn which_command_completions() {
@ -512,7 +473,7 @@ fn dotnu_completions() {
match_suggestions(&vec!["sub.nu`"], &suggestions); match_suggestions(&vec!["sub.nu`"], &suggestions);
let mut expected = vec![ let expected = vec![
"asdf.nu", "asdf.nu",
"bar.nu", "bar.nu",
"bat.nu", "bat.nu",
@ -545,8 +506,6 @@ fn dotnu_completions() {
match_suggestions(&expected, &suggestions); match_suggestions(&expected, &suggestions);
// Test use completion // Test use completion
expected.push("std");
expected.push("std-rfc");
let completion_str = "use "; let completion_str = "use ";
let suggestions = completer.complete(completion_str, completion_str.len()); let suggestions = completer.complete(completion_str, completion_str.len());
@ -578,66 +537,6 @@ fn dotnu_completions() {
match_dir_content_for_dotnu(dir_content, &suggestions); 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] #[test]
fn dotnu_completions_const_nu_lib_dirs() { fn dotnu_completions_const_nu_lib_dirs() {
let (_, _, engine, stack) = new_dotnu_engine(); let (_, _, engine, stack) = new_dotnu_engine();
@ -1012,11 +911,10 @@ fn partial_completions() {
// Create the expected values // Create the expected values
let expected_paths = [ let expected_paths = [
file(dir.join("partial").join("hello.txt")), 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.exe")),
file(dir.join("partial-a").join("have_ext.txt")), file(dir.join("partial-a").join("have_ext.txt")),
file(dir.join("partial-a").join("hello")), 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("hello_b")),
file(dir.join("partial-b").join("hi_b")), file(dir.join("partial-b").join("hi_b")),
file(dir.join("partial-c").join("hello_c")), file(dir.join("partial-c").join("hello_c")),
@ -1033,12 +931,11 @@ fn partial_completions() {
// Create the expected values // Create the expected values
let expected_paths = [ let expected_paths = [
file(dir.join("partial").join("hello.txt")), 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("anotherfile")),
file(dir.join("partial-a").join("have_ext.exe")), file(dir.join("partial-a").join("have_ext.exe")),
file(dir.join("partial-a").join("have_ext.txt")), file(dir.join("partial-a").join("have_ext.txt")),
file(dir.join("partial-a").join("hello")), 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("hello_b")),
file(dir.join("partial-b").join("hi_b")), file(dir.join("partial-b").join("hi_b")),
file(dir.join("partial-c").join("hello_c")), file(dir.join("partial-c").join("hello_c")),
@ -2005,35 +1902,6 @@ fn table_cell_path_completions() {
match_suggestions(&expected, &suggestions); 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] #[test]
fn alias_of_command_and_flags() { fn alias_of_command_and_flags() {
let (_, _, mut engine, mut stack) = new_engine(); 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)); 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 target_dir = format!("open {}", folder(dir.join("pArTiAL")));
let suggestions = completer.complete(&target_dir, target_dir.len()); 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( match_suggestions(
&vec![ &vec![file(dir.join("partial").join("hello.txt")).as_str()],
file(dir.join("partial").join("hello.txt")).as_str(),
folder(dir.join("partial").join("hol")).as_str(),
],
&suggestions, &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"] #[ignore = "was reverted, still needs fixing"]

View File

@ -5,7 +5,7 @@ edition = "2021"
license = "MIT" license = "MIT"
name = "nu-cmd-base" name = "nu-cmd-base"
repository = "https://github.com/nushell/nushell/tree/main/crates/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 # 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 workspace = true
[dependencies] [dependencies]
nu-engine = { path = "../nu-engine", 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.104.1" } nu-parser = { path = "../nu-parser", version = "0.103.0" }
nu-path = { path = "../nu-path", version = "0.104.1" } nu-path = { path = "../nu-path", version = "0.103.0" }
nu-protocol = { path = "../nu-protocol", version = "0.104.1", default-features = false } nu-protocol = { path = "../nu-protocol", version = "0.103.0", default-features = false }
indexmap = { workspace = true } indexmap = { workspace = true }
miette = { workspace = true } miette = { workspace = true }

View File

@ -5,7 +5,7 @@ edition = "2021"
license = "MIT" license = "MIT"
name = "nu-cmd-extra" name = "nu-cmd-extra"
repository = "https://github.com/nushell/nushell/tree/main/crates/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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -16,13 +16,13 @@ bench = false
workspace = true workspace = true
[dependencies] [dependencies]
nu-cmd-base = { path = "../nu-cmd-base", version = "0.104.1" } nu-cmd-base = { path = "../nu-cmd-base", version = "0.103.0" }
nu-engine = { path = "../nu-engine", version = "0.104.1", default-features = false } nu-engine = { path = "../nu-engine", version = "0.103.0", default-features = false }
nu-json = { version = "0.104.1", path = "../nu-json" } nu-json = { version = "0.103.0", path = "../nu-json" }
nu-parser = { path = "../nu-parser", version = "0.104.1" } nu-parser = { path = "../nu-parser", version = "0.103.0" }
nu-pretty-hex = { version = "0.104.1", path = "../nu-pretty-hex" } nu-pretty-hex = { version = "0.103.0", path = "../nu-pretty-hex" }
nu-protocol = { path = "../nu-protocol", version = "0.104.1", default-features = false } nu-protocol = { path = "../nu-protocol", version = "0.103.0", default-features = false }
nu-utils = { path = "../nu-utils", version = "0.104.1", default-features = false } nu-utils = { path = "../nu-utils", version = "0.103.0", default-features = false }
# Potential dependencies for extras # Potential dependencies for extras
heck = { workspace = true } heck = { workspace = true }
@ -37,6 +37,6 @@ itertools = { workspace = true }
mime = { workspace = true } mime = { workspace = true }
[dev-dependencies] [dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.104.1" } nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.103.0" }
nu-command = { path = "../nu-command", version = "0.104.1" } nu-command = { path = "../nu-command", version = "0.103.0" }
nu-test-support = { path = "../nu-test-support", version = "0.104.1" } nu-test-support = { path = "../nu-test-support", version = "0.103.0" }

View File

@ -135,7 +135,7 @@ where
(min, max) => (rhs, lhs, max, min), (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 a;
let mut b; let mut b;
@ -159,10 +159,9 @@ where
} }
(Value::Binary { .. }, Value::Int { .. }) | (Value::Int { .. }, Value::Binary { .. }) => { (Value::Binary { .. }, Value::Int { .. }) | (Value::Int { .. }, Value::Binary { .. }) => {
Value::error( Value::error(
ShellError::OnlySupportsThisInputType { ShellError::PipelineMismatch {
exp_input_type: "input, and argument, to be both int or both binary" exp_input_type: "input, and argument, to be both int or both binary"
.to_string(), .to_string(),
wrong_type: "int and binary".to_string(),
dst_span: rhs.span(), dst_span: rhs.span(),
src_span: span, src_span: span,
}, },

View File

@ -249,7 +249,7 @@ fn shift_bytes_and_bits_left(data: &[u8], byte_shift: usize, bit_shift: usize) -
Last | Only => lhs << bit_shift, Last | Only => lhs << bit_shift,
_ => (lhs << bit_shift) | (rhs >> (8 - 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>>() .collect::<Vec<u8>>()
} }

View File

@ -253,11 +253,12 @@ fn format_record(
optional: false, optional: false,
}) })
.collect(); .collect();
match data_as_value.clone().follow_cell_path(&path_members, false) {
let expanded_string = data_as_value Ok(value_at_column) => {
.follow_cell_path(&path_members, false)? output.push_str(value_at_column.to_expanded_string(", ", config).as_str())
.to_expanded_string(", ", config); }
output.push_str(expanded_string.as_str()) Err(se) => return Err(se),
}
} }
} }
} }

View File

@ -6,7 +6,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-lang"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "nu-cmd-lang" name = "nu-cmd-lang"
version = "0.104.1" version = "0.103.0"
[lib] [lib]
bench = false bench = false
@ -15,16 +15,16 @@ bench = false
workspace = true workspace = true
[dependencies] [dependencies]
nu-engine = { path = "../nu-engine", 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.104.1" } nu-parser = { path = "../nu-parser", version = "0.103.0" }
nu-protocol = { path = "../nu-protocol", version = "0.104.1", default-features = false } nu-protocol = { path = "../nu-protocol", version = "0.103.0", default-features = false }
nu-utils = { path = "../nu-utils", version = "0.104.1", default-features = false } nu-utils = { path = "../nu-utils", version = "0.103.0", default-features = false }
itertools = { workspace = true } itertools = { workspace = true }
shadow-rs = { version = "1.1", default-features = false } shadow-rs = { version = "0.38", default-features = false }
[build-dependencies] [build-dependencies]
shadow-rs = { version = "1.1", default-features = false, features = ["build"] } shadow-rs = { version = "0.38", default-features = false }
[dev-dependencies] [dev-dependencies]
quickcheck = { workspace = true } quickcheck = { workspace = true }

View File

@ -18,4 +18,4 @@ A base crate is one with minimal dependencies in our system so that other develo
### Background on nu-cmd-lang ### 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*.

View File

@ -1,9 +1,6 @@
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use nu_protocol::{ use nu_protocol::{engine::StateWorkingSet, ByteStreamSource, PipelineMetadata};
engine::{Closure, StateWorkingSet},
BlockId, ByteStreamSource, Category, PipelineMetadata, Signature,
};
use std::any::type_name;
#[derive(Clone)] #[derive(Clone)]
pub struct Describe; 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", "{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!( result: Some(Value::test_record(record!(
"type" => Value::test_string("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!( "columns" => Value::test_record(record!(
"shell" => Value::test_record(record!( "shell" => Value::test_string("string"),
"type" => Value::test_string("string"), "uwu" => Value::test_string("bool"),
"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),
)),
"features" => Value::test_record(record!( "features" => Value::test_record(record!(
"type" => Value::test_string("record"), "type" => Value::test_string("record"),
"detailed_type" => Value::test_string("record<bugs: bool, multiplatform: bool, speed: int>"),
"columns" => Value::test_record(record!( "columns" => Value::test_record(record!(
"bugs" => Value::test_record(record!( "bugs" => Value::test_string("bool"),
"type" => Value::test_string("bool"), "multiplatform" => Value::test_string("bool"),
"detailed_type" => Value::test_string("bool"), "speed" => Value::test_string("int"),
"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),
)),
)), )),
"rust_type" => Value::test_string("&nu_utils::shared_cow::SharedCow<nu_protocol::value::record::Record>"),
)), )),
"fib" => Value::test_record(record!( "fib" => Value::test_record(record!(
"type" => Value::test_string("list"), "type" => Value::test_string("list"),
"detailed_type" => Value::test_string("list<int>"),
"length" => Value::test_int(6), "length" => Value::test_int(6),
"rust_type" => Value::test_string("&mut alloc::vec::Vec<nu_protocol::value::Value>"), "values" => Value::test_list(vec![
"value" => Value::test_list(vec![ Value::test_string("int"),
Value::test_record(record!( Value::test_string("int"),
"type" => Value::test_string("int"), Value::test_string("int"),
"detailed_type" => Value::test_string("int"), Value::test_string("int"),
"rust_type" => Value::test_string("i64"), Value::test_string("int"),
"value" => Value::test_int(1), Value::test_string("int"),
)), ]),
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),
))]
),
)), )),
"on_save" => Value::test_record(record!( "on_save" => Value::test_record(record!(
"type" => Value::test_string("closure"), "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!( "signature" => Value::test_record(record!(
"name" => Value::test_string(""), "name" => Value::test_string(""),
"category" => Value::test_string("default"), "category" => Value::test_string("default"),
)), )),
)), )),
"first_commit" => Value::test_record(record!( "first_commit" => Value::test_string("date"),
"type" => Value::test_string("datetime"), "my_duration" => Value::test_string("duration"),
"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),
))
)), )),
"rust_type" => Value::test_string("&nu_utils::shared_cow::SharedCow<nu_protocol::value::record::Record>"),
))), ))),
}, },
Example { Example {
@ -255,9 +175,7 @@ fn run(
Value::record( Value::record(
record! { record! {
"type" => Value::string("bytestream", head), "type" => Value::string(type_, head),
"detailed_type" => Value::string(type_, head),
"rust_type" => Value::string(type_of(&stream), head),
"origin" => Value::string(origin, head), "origin" => Value::string(origin, head),
"metadata" => metadata_to_value(metadata, head), "metadata" => metadata_to_value(metadata, head),
}, },
@ -274,7 +192,6 @@ fn run(
description description
} }
PipelineData::ListStream(stream, ..) => { PipelineData::ListStream(stream, ..) => {
let type_ = type_of(&stream);
if options.detailed { if options.detailed {
let subtype = if options.no_collect { let subtype = if options.no_collect {
Value::string("any", head) Value::string("any", head)
@ -284,8 +201,6 @@ fn run(
Value::record( Value::record(
record! { record! {
"type" => Value::string("stream", head), "type" => Value::string("stream", head),
"detailed_type" => Value::string("list stream", head),
"rust_type" => Value::string(type_, head),
"origin" => Value::string("nushell", head), "origin" => Value::string("nushell", head),
"subtype" => subtype, "subtype" => subtype,
"metadata" => metadata_to_value(metadata, head), "metadata" => metadata_to_value(metadata, head),
@ -314,95 +229,45 @@ fn run(
} }
enum Description { enum Description {
String(String),
Record(Record), Record(Record),
} }
impl Description { impl Description {
fn into_value(self, span: Span) -> Value { fn into_value(self, span: Span) -> Value {
match self { match self {
Description::String(ty) => Value::string(ty, span),
Description::Record(record) => Value::record(record, span), Description::Record(record) => Value::record(record, span),
} }
} }
} }
fn describe_value(value: Value, head: Span, engine_state: Option<&EngineState>) -> Value { 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) Value::record(record, head)
} }
fn type_of<T>(_: &T) -> String {
type_name::<T>().to_string()
}
fn describe_value_inner( fn describe_value_inner(
mut value: Value, value: Value,
head: Span, head: Span,
engine_state: Option<&EngineState>, engine_state: Option<&EngineState>,
) -> Description { ) -> Description {
let value_type = value.get_type().to_string();
match value { match value {
Value::Bool { val, .. } => Description::Record(record! { Value::Bool { .. }
"type" => Value::string("bool", head), | Value::Int { .. }
"detailed_type" => Value::string(value_type, head), | Value::Float { .. }
"rust_type" => Value::string(type_of(&val), head), | Value::Filesize { .. }
"value" => value, | Value::Duration { .. }
}), | Value::Date { .. }
Value::Int { val, .. } => Description::Record(record! { | Value::Range { .. }
"type" => Value::string("int", head), | Value::String { .. }
"detailed_type" => Value::string(value_type, head), | Value::Glob { .. }
"rust_type" => Value::string(type_of(&val), head), | Value::Nothing { .. } => Description::String(value.get_type().to_string()),
"value" => value, Value::Record { val, .. } => {
}), let mut columns = val.into_owned();
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();
for (_, val) in &mut columns { for (_, val) in &mut columns {
*val = *val =
describe_value_inner(std::mem::take(val), head, engine_state).into_value(head); describe_value_inner(std::mem::take(val), head, engine_state).into_value(head);
@ -410,34 +275,25 @@ fn describe_value_inner(
Description::Record(record! { Description::Record(record! {
"type" => Value::string("record", head), "type" => Value::string("record", head),
"detailed_type" => Value::string(value_type, head), "columns" => Value::record(columns, head),
"columns" => Value::record(columns.clone(), head),
"rust_type" => Value::string(type_of(&val), head),
}) })
} }
Value::List { ref mut vals, .. } => { Value::List { mut vals, .. } => {
for val in &mut *vals { for val in &mut vals {
*val = *val =
describe_value_inner(std::mem::take(val), head, engine_state).into_value(head); describe_value_inner(std::mem::take(val), head, engine_state).into_value(head);
} }
Description::Record(record! { Description::Record(record! {
"type" => Value::string("list", head), "type" => Value::string("list", head),
"detailed_type" => Value::string(value_type, head),
"length" => Value::int(vals.len() as i64, head), "length" => Value::int(vals.len() as i64, head),
"rust_type" => Value::string(type_of(&vals), head), "values" => Value::list(vals, head),
"value" => value,
}) })
} }
Value::Closure { ref val, .. } => { Value::Closure { val, .. } => {
let block = engine_state.map(|engine_state| engine_state.get_block(val.block_id)); let block = engine_state.map(|engine_state| engine_state.get_block(val.block_id));
let mut record = record! { let mut record = record! { "type" => Value::string("closure", head) };
"type" => Value::string("closure", head),
"detailed_type" => Value::string(value_type, head),
"rust_type" => Value::string(type_of(&val), head),
"value" => value,
};
if let Some(block) = block { if let Some(block) = block {
record.push( record.push(
"signature", "signature",
@ -452,37 +308,21 @@ fn describe_value_inner(
} }
Description::Record(record) Description::Record(record)
} }
Value::Error { ref error, .. } => Description::Record(record! { Value::Error { error, .. } => Description::Record(record! {
"type" => Value::string("error", head), "type" => Value::string("error", head),
"detailed_type" => Value::string(value_type, head),
"subtype" => Value::string(error.to_string(), 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), "type" => Value::string("binary", head),
"detailed_type" => Value::string(value_type, head),
"length" => Value::int(val.len() as i64, 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), "type" => Value::string("cell-path", head),
"detailed_type" => Value::string(value_type, head),
"length" => Value::int(val.members.len() as i64, 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), "type" => Value::string("custom", head),
"detailed_type" => Value::string(value_type, head),
"subtype" => Value::string(val.type_name(), 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),
}
}), }),
} }
} }

View File

@ -31,6 +31,16 @@ impl Command for Do {
"ignore errors as the closure runs", "ignore errors as the closure runs",
Some('i'), 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( .switch(
"capture-errors", "capture-errors",
"catch errors as the closure runs, and return them", "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 rest: Vec<Value> = call.rest(engine_state, caller_stack, 1)?;
let ignore_all_errors = call.has_flag(engine_state, caller_stack, "ignore-errors")?; 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 capture_errors = call.has_flag(engine_state, caller_stack, "capture-errors")?;
let has_env = call.has_flag(engine_state, caller_stack, "env")?; 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)) Ok(PipelineData::ByteStream(mut stream, metadata))
if ignore_all_errors if ignore_program_errors
&& !matches!( && !matches!(
caller_stack.stdout(), caller_stack.stdout(),
OutDest::Pipe | OutDest::PipeSeparate | OutDest::Value OutDest::Pipe | OutDest::PipeSeparate | OutDest::Value
@ -178,10 +218,10 @@ impl Command for Do {
} }
Ok(PipelineData::ByteStream(stream, metadata)) 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::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| { let stream = stream.map(move |value| {
if let Value::Error { .. } = value { if let Value::Error { .. } = value {
Value::nothing(head) Value::nothing(head)

View File

@ -116,8 +116,6 @@ impl Command for OverlayUse {
// b) refreshing an active overlay (the origin module changed) // b) refreshing an active overlay (the origin module changed)
let module = engine_state.get_module(module_id); 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 // Evaluate the export-env block (if any) and keep its environment
if let Some(block_id) = module.env_block { 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 // The export-env block should see the env vars *before* activating this overlay
caller_stack.add_overlay(overlay_name); 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 // Merge the block's environment to the current stack
redirect_env(engine_state, caller_stack, &callee_stack); redirect_env(engine_state, caller_stack, &callee_stack);
} else { } else {
caller_stack.add_overlay(overlay_name); 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 { } else {
caller_stack.add_overlay(overlay_name); caller_stack.add_overlay(overlay_name);

View File

@ -5,7 +5,7 @@ edition = "2021"
license = "MIT" license = "MIT"
name = "nu-cmd-plugin" name = "nu-cmd-plugin"
repository = "https://github.com/nushell/nushell/tree/main/crates/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 # 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 workspace = true
[dependencies] [dependencies]
nu-engine = { path = "../nu-engine", version = "0.104.1" } nu-engine = { path = "../nu-engine", version = "0.103.0" }
nu-path = { path = "../nu-path", version = "0.104.1" } nu-path = { path = "../nu-path", version = "0.103.0" }
nu-protocol = { path = "../nu-protocol", version = "0.104.1", features = ["plugin"] } nu-protocol = { path = "../nu-protocol", version = "0.103.0", features = ["plugin"] }
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.104.1" } nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.103.0" }
itertools = { workspace = true } itertools = { workspace = true }

View File

@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-color-confi
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "nu-color-config" name = "nu-color-config"
version = "0.104.1" version = "0.103.0"
[lib] [lib]
bench = false bench = false
@ -14,12 +14,12 @@ bench = false
workspace = true workspace = true
[dependencies] [dependencies]
nu-protocol = { path = "../nu-protocol", version = "0.104.1", default-features = false } nu-protocol = { path = "../nu-protocol", version = "0.103.0", default-features = false }
nu-engine = { path = "../nu-engine", version = "0.104.1", default-features = false } nu-engine = { path = "../nu-engine", version = "0.103.0", default-features = false }
nu-json = { path = "../nu-json", version = "0.104.1" } nu-json = { path = "../nu-json", version = "0.103.0" }
nu-ansi-term = { workspace = true } nu-ansi-term = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
[dev-dependencies] [dev-dependencies]
nu-test-support = { path = "../nu-test-support", version = "0.104.1" } nu-test-support = { path = "../nu-test-support", version = "0.103.0" }

View File

@ -120,7 +120,7 @@ impl<'a> StyleComputer<'a> {
("int".to_string(), ComputableStyle::Static(Color::White.normal())), ("int".to_string(), ComputableStyle::Static(Color::White.normal())),
("filesize".to_string(), ComputableStyle::Static(Color::Cyan.normal())), ("filesize".to_string(), ComputableStyle::Static(Color::Cyan.normal())),
("duration".to_string(), ComputableStyle::Static(Color::White.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())), ("range".to_string(), ComputableStyle::Static(Color::White.normal())),
("float".to_string(), ComputableStyle::Static(Color::White.normal())), ("float".to_string(), ComputableStyle::Static(Color::White.normal())),
("string".to_string(), ComputableStyle::Static(Color::White.normal())), ("string".to_string(), ComputableStyle::Static(Color::White.normal())),

View File

@ -5,7 +5,7 @@ edition = "2021"
license = "MIT" license = "MIT"
name = "nu-command" name = "nu-command"
repository = "https://github.com/nushell/nushell/tree/main/crates/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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -16,21 +16,21 @@ bench = false
workspace = true workspace = true
[dependencies] [dependencies]
nu-cmd-base = { path = "../nu-cmd-base", version = "0.104.1" } nu-cmd-base = { path = "../nu-cmd-base", version = "0.103.0" }
nu-color-config = { path = "../nu-color-config", version = "0.104.1" } nu-color-config = { path = "../nu-color-config", version = "0.103.0" }
nu-engine = { path = "../nu-engine", version = "0.104.1", default-features = false } nu-engine = { path = "../nu-engine", version = "0.103.0", default-features = false }
nu-glob = { path = "../nu-glob", version = "0.104.1" } nu-glob = { path = "../nu-glob", version = "0.103.0" }
nu-json = { path = "../nu-json", version = "0.104.1" } nu-json = { path = "../nu-json", version = "0.103.0" }
nu-parser = { path = "../nu-parser", version = "0.104.1" } nu-parser = { path = "../nu-parser", version = "0.103.0" }
nu-path = { path = "../nu-path", version = "0.104.1" } nu-path = { path = "../nu-path", version = "0.103.0" }
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.104.1" } nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.103.0" }
nu-protocol = { path = "../nu-protocol", version = "0.104.1", default-features = false } nu-protocol = { path = "../nu-protocol", version = "0.103.0", default-features = false }
nu-system = { path = "../nu-system", version = "0.104.1" } nu-system = { path = "../nu-system", version = "0.103.0" }
nu-table = { path = "../nu-table", version = "0.104.1" } nu-table = { path = "../nu-table", version = "0.103.0" }
nu-term-grid = { path = "../nu-term-grid", version = "0.104.1" } nu-term-grid = { path = "../nu-term-grid", version = "0.103.0" }
nu-utils = { path = "../nu-utils", version = "0.104.1", default-features = false } nu-utils = { path = "../nu-utils", version = "0.103.0", default-features = false }
nu-ansi-term = { workspace = true } nu-ansi-term = { workspace = true }
nuon = { path = "../nuon", version = "0.104.1" } nuon = { path = "../nuon", version = "0.103.0" }
alphanumeric-sort = { workspace = true } alphanumeric-sort = { workspace = true }
base64 = { workspace = true } base64 = { workspace = true }
@ -129,7 +129,7 @@ v_htmlescape = { workspace = true }
wax = { workspace = true } wax = { workspace = true }
which = { workspace = true, optional = true } which = { workspace = true, optional = true }
unicode-width = { workspace = 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 } web-time = { workspace = true }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
@ -209,8 +209,8 @@ sqlite = ["rusqlite"]
trash-support = ["trash"] trash-support = ["trash"]
[dev-dependencies] [dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.104.1" } nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.103.0" }
nu-test-support = { path = "../nu-test-support", version = "0.104.1" } nu-test-support = { path = "../nu-test-support", version = "0.103.0" }
dirs = { workspace = true } dirs = { workspace = true }
mockito = { workspace = true, default-features = false } mockito = { workspace = true, default-features = false }

View File

@ -1,29 +1,12 @@
use crate::{generate_strftime_list, parse_date_from_string}; use crate::{generate_strftime_list, parse_date_from_string};
use chrono::{ use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, TimeZone, Utc};
DateTime, Datelike, FixedOffset, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, use human_date_parser::{from_human_time, ParseResult};
Timelike, Utc,
};
use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_cmd_base::input_handler::{operate, CmdArgument};
use nu_engine::command_prelude::*; 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 { struct Arguments {
zone_options: Option<Spanned<Zone>>, zone_options: Option<Spanned<Zone>>,
format_options: Option<Spanned<DatetimeFormat>>, format_options: Option<DatetimeFormat>,
cell_paths: Option<Vec<CellPath>>, cell_paths: Option<Vec<CellPath>>,
} }
@ -81,12 +64,8 @@ impl Command for IntoDatetime {
(Type::String, Type::Date), (Type::String, Type::Date),
(Type::List(Box::new(Type::String)), Type::List(Box::new(Type::Date))), (Type::List(Box::new(Type::String)), Type::List(Box::new(Type::Date))),
(Type::table(), Type::table()), (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::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 // 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 // which aren't caught by the parser. see https://github.com/nushell/nushell/pull/14922 for more details
// only applicable for --list flag // only applicable for --list flag
@ -116,6 +95,11 @@ impl Command for IntoDatetime {
"Show all possible variables for use in --format flag", "Show all possible variables for use in --format flag",
Some('l'), Some('l'),
) )
.switch(
"list-human",
"Show human-readable datetime parsing examples",
Some('n'),
)
.rest( .rest(
"rest", "rest",
SyntaxShape::CellPath, SyntaxShape::CellPath,
@ -133,6 +117,8 @@ impl Command for IntoDatetime {
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
if call.has_flag(engine_state, stack, "list")? { if call.has_flag(engine_state, stack, "list")? {
Ok(generate_strftime_list(call.head, true).into_pipeline_data()) 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 { } else {
let cell_paths = call.rest(engine_state, stack, 0)?; let cell_paths = call.rest(engine_state, stack, 0)?;
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
@ -152,16 +138,13 @@ impl Command for IntoDatetime {
}; };
let format_options = call let format_options = call
.get_flag::<Spanned<String>>(engine_state, stack, "format")? .get_flag::<String>(engine_state, stack, "format")?
.as_ref() .as_ref()
.map(|fmt| Spanned { .map(|fmt| DatetimeFormat(fmt.to_string()));
item: DatetimeFormat(fmt.item.to_string()),
span: fmt.span,
});
let args = Arguments { let args = Arguments {
zone_options,
format_options, format_options,
zone_options,
cell_paths, cell_paths,
}; };
operate(action, args, input, call.head, engine_state.signals()) operate(action, args, input, call.head, engine_state.signals())
@ -237,12 +220,6 @@ impl Command for IntoDatetime {
#[allow(clippy::inconsistent_digit_grouping)] #[allow(clippy::inconsistent_digit_grouping)]
result: example_result_1(1614434140_000000000), 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 { Example {
description: "Convert list of timestamps to datetimes", 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"#, 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(), 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); struct DatetimeFormat(String);
fn action(input: &Value, args: &Arguments, head: Span) -> Value { 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(); 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 // Let's try dtparse first
if matches!(input, Value::String { .. }) && dateformat.is_none() { if matches!(input, Value::String { .. }) && dateformat.is_none() {
let span = input.span(); let span = input.span();
if let Ok(input_val) = input.coerce_str() { if let Ok(input_val) = input.coerce_str() {
if let Ok(date) = parse_date_from_string(&input_val, span) { match parse_date_from_string(&input_val, span) {
return Value::date(date, 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?) // Check to see if input looks like a Unix timestamp (i.e. can it be parsed to an int?)
let timestamp = match input { let timestamp = match input {
@ -410,59 +403,23 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
let parse_as_string = |val: &str| { let parse_as_string = |val: &str| {
match dateformat { match dateformat {
Some(dt_format) => match DateTime::parse_from_str(val, &dt_format.item.0) { Some(dt) => match DateTime::parse_from_str(val, &dt.0) {
Ok(dt) => { Ok(d) => Value::date ( d, head ),
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,
),
},
}
},
Err(reason) => { Err(reason) => {
parse_with_format(val, &dt_format.item.0, head).unwrap_or_else(|_| Value::error ( match NaiveDateTime::parse_from_str(val, &dt.0) {
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()) }, 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, 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> { fn list_human_readable_examples(span: Span) -> Value {
if let Some(invalid_col) = record let examples: Vec<String> = vec![
.columns() "Today 18:30".into(),
.find(|key| !ALLOWED_COLUMNS.contains(&key.as_str())) "2022-11-07 13:25:30".into(),
{ "15:20 Friday".into(),
let allowed_cols = ALLOWED_COLUMNS.join(", "); "This Friday 17:00".into(),
return Err(ShellError::UnsupportedInput { "13:25, Next Tuesday".into(),
msg: format!( "Last Friday at 19:45".into(),
"Column '{invalid_col}' is not valid for a structured datetime. Allowed columns are: {allowed_cols}" "In 3 days".into(),
), "In 2 hours".into(),
input: "value originates from here".into(), "10 hours and 5 minutes ago".into(),
msg_span: head, "1 years ago".into(),
input_span: span "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. let records = examples
// And local timezone is used if not provided. .iter()
#[derive(Debug)] .map(|s| {
enum RecordColumnDefault { Value::record(
Now, record! {
Zero, "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)
let mut record_column_default = RecordColumnDefault::Now; },
span,
)
})
.collect::<Vec<Value>>();
let now = Local::now(); Value::list(records, span)
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(())
} }
#[cfg(test)] #[cfg(test)]
@ -769,10 +508,7 @@ mod tests {
#[test] #[test]
fn takes_a_date_format_with_timezone() { fn takes_a_date_format_with_timezone() {
let date_str = Value::test_string("16.11.1984 8:00 am +0000"); let date_str = Value::test_string("16.11.1984 8:00 am +0000");
let fmt_options = Some(Spanned { let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()));
item: DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()),
span: Span::test_data(),
});
let args = Arguments { let args = Arguments {
zone_options: None, zone_options: None,
format_options: fmt_options, format_options: fmt_options,
@ -787,12 +523,16 @@ mod tests {
} }
#[test] #[test]
#[ignore]
fn takes_a_date_format_without_timezone() { 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 date_str = Value::test_string("16.11.1984 8:00 am");
let fmt_options = Some(Spanned { let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P".to_string()));
item: DatetimeFormat("%d.%m.%Y %H:%M %P".to_string()),
span: Span::test_data(),
});
let args = Arguments { let args = Arguments {
zone_options: None, zone_options: None,
format_options: fmt_options, format_options: fmt_options,
@ -874,10 +614,7 @@ mod tests {
#[test] #[test]
fn takes_int_with_formatstring() { fn takes_int_with_formatstring() {
let date_int = Value::test_int(1_614_434_140); let date_int = Value::test_int(1_614_434_140);
let fmt_options = Some(Spanned { let fmt_options = Some(DatetimeFormat("%s".to_string()));
item: DatetimeFormat("%s".to_string()),
span: Span::test_data(),
});
let args = Arguments { let args = Arguments {
zone_options: None, zone_options: None,
format_options: fmt_options, format_options: fmt_options,
@ -892,55 +629,6 @@ mod tests {
assert_eq!(actual, expected) 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] #[test]
fn takes_timestamp() { fn takes_timestamp() {
let date_str = Value::test_string("1614434140000000000"); let date_str = Value::test_string("1614434140000000000");
@ -955,7 +643,7 @@ mod tests {
}; };
let actual = action(&date_str, &args, Span::test_data()); let actual = action(&date_str, &args, Span::test_data());
let expected = Value::date( let expected = Value::date(
Local.timestamp_opt(1_614_434_140, 0).unwrap().into(), Local.timestamp_opt(1614434140, 0).unwrap().into(),
Span::test_data(), Span::test_data(),
); );
@ -974,7 +662,7 @@ mod tests {
cell_paths: None, cell_paths: None,
}; };
let expected = Value::date( let expected = Value::date(
Local.timestamp_opt(1_614_434_140, 0).unwrap().into(), Local.timestamp_opt(1614434140, 0).unwrap().into(),
Span::test_data(), Span::test_data(),
); );
let actual = action(&expected, &args, 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 actual = action(&date_str, &args, Span::test_data());
let expected = Value::date( let expected = Value::date(
Utc.timestamp_opt(1_614_434_140, 0).unwrap().into(), Utc.timestamp_opt(1614434140, 0).unwrap().into(),
Span::test_data(), Span::test_data(),
); );
@ -1003,10 +691,7 @@ mod tests {
#[test] #[test]
fn communicates_parsing_error_given_an_invalid_datetimelike_string() { fn communicates_parsing_error_given_an_invalid_datetimelike_string() {
let date_str = Value::test_string("16.11.1984 8:00 am Oops0000"); let date_str = Value::test_string("16.11.1984 8:00 am Oops0000");
let fmt_options = Some(Spanned { let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()));
item: DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()),
span: Span::test_data(),
});
let args = Arguments { let args = Arguments {
zone_options: None, zone_options: None,
format_options: fmt_options, format_options: fmt_options,

View File

@ -1,41 +1,8 @@
use nu_cmd_base::input_handler::{operate, CmdArgument};
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use nu_parser::{parse_unit_value, DURATION_UNIT_GROUPS}; use nu_parser::{parse_unit_value, DURATION_UNIT_GROUPS};
use nu_protocol::{ast::Expr, Unit}; 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_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)] #[derive(Clone)]
pub struct IntoDuration; pub struct IntoDuration;
@ -48,17 +15,13 @@ impl Command for IntoDuration {
Signature::build("into duration") Signature::build("into duration")
.input_output_types(vec![ .input_output_types(vec![
(Type::Int, Type::Duration), (Type::Int, Type::Duration),
(Type::Float, Type::Duration),
(Type::String, Type::Duration), (Type::String, Type::Duration),
(Type::Duration, 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()), (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( .named(
"unit", "unit",
SyntaxShape::String, SyntaxShape::String,
@ -92,35 +55,7 @@ impl Command for IntoDuration {
call: &Call, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let cell_paths = call.rest(engine_state, stack, 0)?; into_duration(engine_state, stack, call, input)
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())
} }
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
@ -174,23 +109,67 @@ impl Command for IntoDuration {
example: "1_234 | into duration --unit ms", example: "1_234 | into duration --unit ms",
result: Some(Value::test_duration(1_234 * 1_000_000)), 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)> { fn split_whitespace_indices(s: &str, span: Span) -> impl Iterator<Item = (&str, Span)> {
s.split_whitespace().map(move |sub| { s.split_whitespace().map(move |sub| {
// Gets the offset of the `sub` substring inside the string `s`. // 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 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 { match input {
Value::Duration { .. } => input.clone(), Value::Duration { .. } => input.clone(),
Value::Record { val, .. } => { Value::String { val, .. } => match compound_to_duration(val, value_span) {
merge_record(val, head, value_span).unwrap_or_else(|err| Value::error(err, value_span)) Ok(val) => Value::duration(val, span),
} Err(error) => Value::error(error, 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::Int { val, .. } => { Value::Int { val, .. } => {
let ns = unit_to_ns_factor(unit); let ns = match unit {
Value::duration(*val * ns, head) "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. // Propagate errors by explicitly matching them before the final case.
Value::Error { .. } => input.clone(), Value::Error { .. } => input.clone(),
@ -305,134 +260,14 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
ShellError::OnlySupportsThisInputType { ShellError::OnlySupportsThisInputType {
exp_input_type: "string or duration".into(), exp_input_type: "string or duration".into(),
wrong_type: other.get_type().to_string(), wrong_type: other.get_type().to_string(),
dst_span: head, dst_span: span,
src_span: other.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)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
@ -449,27 +284,24 @@ mod test {
#[rstest] #[rstest]
#[case("3ns", 3)] #[case("3ns", 3)]
#[case("4us", 4 * NS_PER_US)] #[case("4us", 4*1000)]
#[case("4\u{00B5}s", 4 * NS_PER_US)] // micro sign #[case("4\u{00B5}s", 4*1000)] // micro sign
#[case("4\u{03BC}s", 4 * NS_PER_US)] // mu symbol #[case("4\u{03BC}s", 4*1000)] // mu symbol
#[case("5ms", 5 * NS_PER_MS)] #[case("5ms", 5 * 1000 * 1000)]
#[case("1sec", NS_PER_SEC)] #[case("1sec", NS_PER_SEC)]
#[case("7min", 7 * NS_PER_MINUTE)] #[case("7min", 7 * 60 * NS_PER_SEC)]
#[case("42hr", 42 * NS_PER_HOUR)] #[case("42hr", 42 * 60 * 60 * NS_PER_SEC)]
#[case("123day", 123 * NS_PER_DAY)] #[case("123day", 123 * 24 * 60 * 60 * NS_PER_SEC)]
#[case("3wk", 3 * NS_PER_WEEK)] #[case("3wk", 3 * 7 * 24 * 60 * 60 * NS_PER_SEC)]
#[case("86hr 26ns", 86 * 3600 * NS_PER_SEC + 26)] // compound duration string #[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) { fn turns_string_to_duration(#[case] phrase: &str, #[case] expected_duration_val: i64) {
let args = Arguments { let actual = action(
unit: Some(Spanned { &Value::test_string(phrase),
item: "ns".to_string(), "ns",
span: Span::test_data(), Span::new(0, phrase.len()),
}), );
cell_paths: None,
};
let actual = action(&Value::test_string(phrase), &args, Span::test_data());
match actual { match actual {
Value::Duration { Value::Duration {
val: observed_val, .. val: observed_val, ..

View File

@ -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) { } 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 { 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(), from_type: "string".to_string(),
span, span,
help: Some(format!( 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)) Ok(Value::date(dt, span))
} else if DATETIME_YMD_RE.is_match(&val_str).unwrap_or(false) { } 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 { 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(), from_type: "string".to_string(),
span, span,
help: Some(format!( 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)) Ok(Value::date(dt, span))
} else if DATETIME_YMDZ_RE.is_match(&val_str).unwrap_or(false) { } 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 { 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(), from_type: "string".to_string(),
span, span,
help: Some(format!( help: Some(format!(

View File

@ -40,7 +40,6 @@ impl Command for SplitCellPath {
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; let head = call.head;
let input_type = input.get_type();
let src_span = match input { let src_span = match input {
// Early return on correct type and empty pipeline // Early return on correct type and empty pipeline
@ -55,9 +54,8 @@ impl Command for SplitCellPath {
PipelineData::ListStream(stream, ..) => stream.span(), PipelineData::ListStream(stream, ..) => stream.span(),
PipelineData::ByteStream(stream, ..) => stream.span(), PipelineData::ByteStream(stream, ..) => stream.span(),
}; };
Err(ShellError::OnlySupportsThisInputType { Err(ShellError::PipelineMismatch {
exp_input_type: "cell-path".into(), exp_input_type: "cell-path".into(),
wrong_type: input_type.to_string(),
dst_span: head, dst_span: head,
src_span, src_span,
}) })

View File

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

View File

@ -1,5 +1,4 @@
mod date_; mod date_;
mod from_human;
mod humanize; mod humanize;
mod list_timezone; mod list_timezone;
mod now; mod now;
@ -8,7 +7,6 @@ mod to_timezone;
mod utils; mod utils;
pub use date_::Date; pub use date_::Date;
pub use from_human::DateFromHuman;
pub use humanize::DateHumanize; pub use humanize::DateHumanize;
pub use list_timezone::DateListTimezones; pub use list_timezone::DateListTimezones;
pub use now::DateNow; pub use now::DateNow;

View File

@ -38,16 +38,10 @@ impl Command for DateNow {
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![
Example { 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""#, example: r#"date now | format date "%Y-%m-%d %H:%M:%S""#,
result: None, 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 { Example {
description: "Get the time duration since 2019-04-30.", description: "Get the time duration since 2019-04-30.",
example: r#"(date now) - 2019-05-01"#, example: r#"(date now) - 2019-05-01"#,
@ -59,8 +53,7 @@ impl Command for DateNow {
result: None, result: None,
}, },
Example { Example {
description: description: "Get current time in full RFC 3339 format with time zone.",
"Get current time and format it in the debug format (RFC 2822 with timezone)",
example: r#"date now | debug"#, example: r#"date now | debug"#,
result: None, result: None,
}, },

View File

@ -9,11 +9,7 @@ pub(crate) fn parse_date_from_string(
Ok((native_dt, fixed_offset)) => { Ok((native_dt, fixed_offset)) => {
let offset = match fixed_offset { let offset = match fixed_offset {
Some(offset) => offset, Some(offset) => offset,
None => *Local None => *(Local::now().offset()),
.from_local_datetime(&native_dt)
.single()
.unwrap_or_default()
.offset(),
}; };
match offset.from_local_datetime(&native_dt) { match offset.from_local_datetime(&native_dt) {
LocalResult::Single(d) => Ok(d), LocalResult::Single(d) => Ok(d),

View File

@ -23,11 +23,6 @@ impl Command for Debug {
]) ])
.category(Category::Debug) .category(Category::Debug)
.switch("raw", "Prints the raw value representation", Some('r')) .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( fn run(
@ -40,7 +35,6 @@ impl Command for Debug {
let head = call.head; let head = call.head;
let config = stack.get_config(engine_state); let config = stack.get_config(engine_state);
let raw = call.has_flag(engine_state, stack, "raw")?; 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? // Should PipelineData::Empty result in an error here?
@ -48,11 +42,6 @@ impl Command for Debug {
move |x| { move |x| {
if raw { if raw {
Value::string(x.to_debug_string(), head) 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 { } else {
Value::string(x.to_expanded_string(", ", &config), head) Value::string(x.to_expanded_string(", ", &config), head)
} }
@ -89,75 +78,10 @@ impl Command for Debug {
Span::test_data(), 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)] #[cfg(test)]
mod test { mod test {
#[test] #[test]

View File

@ -118,7 +118,7 @@ fn increase_string_width(text: &mut String, total: usize) {
let rest = total - width; let rest = total - width;
if rest > 0 { if rest > 0 {
text.extend(std::iter::repeat_n(' ', rest)); text.extend(std::iter::repeat(' ').take(rest));
} }
} }

View File

@ -272,7 +272,6 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
// Date // Date
bind_command! { bind_command! {
Date, Date,
DateFromHuman,
DateHumanize, DateHumanize,
DateListTimezones, DateListTimezones,
DateNow, DateNow,
@ -452,18 +451,9 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
JobSpawn, JobSpawn,
JobList, JobList,
JobKill, JobKill,
JobId,
JobTag,
Job, Job,
}; };
#[cfg(not(target_family = "wasm"))]
bind_command! {
JobSend,
JobRecv,
JobFlush,
}
#[cfg(all(unix, feature = "os"))] #[cfg(all(unix, feature = "os"))]
bind_command! { bind_command! {
JobUnfreeze, JobUnfreeze,

View File

@ -1,11 +1,7 @@
use nu_cmd_base::util::get_editor; use nu_cmd_base::util::get_editor;
use nu_engine::{command_prelude::*, env_to_strings, get_full_help}; use nu_engine::{command_prelude::*, env_to_strings, get_full_help};
use nu_protocol::shell_error::io::IoError;
use nu_system::ForegroundChild; use nu_system::ForegroundChild;
#[cfg(feature = "os")]
use nu_protocol::process::PostWaitCallback;
#[derive(Clone)] #[derive(Clone)]
pub struct ConfigMeta; pub struct ConfigMeta;
@ -65,6 +61,7 @@ pub(super) fn start_editor(
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
// Find the editor executable. // Find the editor executable.
use nu_protocol::shell_error::io::IoError;
let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?; 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 paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
let cwd = engine_state.cwd(Some(stack))?; 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`. // Wrap the output into a `PipelineData::ByteStream`.
let child = nu_protocol::process::ChildProcess::new( let child = nu_protocol::process::ChildProcess::new(child, None, false, call.head, None)?;
child,
None,
false,
call.head,
Some(post_wait_callback),
)?;
Ok(PipelineData::ByteStream( Ok(PipelineData::ByteStream(
ByteStream::child(child, call.head), ByteStream::child(child, call.head),
None, None,

View File

@ -10,7 +10,7 @@ impl Command for Job {
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build("job") Signature::build("job")
.category(Category::Experimental) .category(Category::Strings)
.input_output_types(vec![(Type::Nothing, Type::String)]) .input_output_types(vec![(Type::Nothing, Type::String)])
} }

View File

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

View File

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

View File

@ -37,7 +37,7 @@ impl Command for JobList {
let values = jobs let values = jobs
.iter() .iter()
.map(|(id, job)| { .map(|(id, job)| {
let mut record = record! { let record = record! {
"id" => Value::int(id.get() as i64, head), "id" => Value::int(id.get() as i64, head),
"type" => match job { "type" => match job {
Job::Thread(_) => Value::string("thread", head), Job::Thread(_) => Value::string("thread", head),
@ -52,16 +52,12 @@ impl Command for JobList {
head, head,
), ),
Job::Frozen(FrozenJob { unfreeze, .. }) => { Job::Frozen(FrozenJob { unfreeze }) => {
Value::list(vec![ Value::int(unfreeze.pid() as i64, head) ], head) 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) Value::record(record, head)
}) })
.collect::<Vec<Value>>(); .collect::<Vec<Value>>();

View File

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

View File

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

View File

@ -1,15 +1,15 @@
use std::{ use std::{
sync::{ sync::{
atomic::{AtomicBool, AtomicU32}, atomic::{AtomicBool, AtomicU32},
mpsc, Arc, Mutex, Arc,
}, },
thread, thread,
}; };
use nu_engine::{command_prelude::*, ClosureEvalOnce}; use nu_engine::{command_prelude::*, ClosureEvalOnce};
use nu_protocol::{ use nu_protocol::{
engine::{Closure, CurrentJob, Job, Mailbox, Redirection, ThreadJob}, engine::{Closure, Job, ThreadJob},
report_shell_error, OutDest, Signals, report_shell_error, Signals,
}; };
#[derive(Clone)] #[derive(Clone)]
@ -28,12 +28,6 @@ impl Command for JobSpawn {
Signature::build("job spawn") Signature::build("job spawn")
.category(Category::Experimental) .category(Category::Experimental)
.input_output_types(vec![(Type::Nothing, Type::Int)]) .input_output_types(vec![(Type::Nothing, Type::Int)])
.named(
"tag",
SyntaxShape::String,
"An optional description tag for this job",
Some('t'),
)
.required( .required(
"closure", "closure",
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), SyntaxShape::Closure(Some(vec![SyntaxShape::Any])),
@ -56,12 +50,11 @@ impl Command for JobSpawn {
let closure: Closure = call.req(engine_state, stack, 0)?; 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(); let mut job_state = engine_state.clone();
job_state.is_interactive = false; job_state.is_interactive = false;
let job_stack = stack.clone();
// the new job should have its ctrl-c independent of foreground // the new job should have its ctrl-c independent of foreground
let job_signals = Signals::new(Arc::new(AtomicBool::new(false))); let job_signals = Signals::new(Arc::new(AtomicBool::new(false)));
job_state.set_signals(job_signals.clone()); job_state.set_signals(job_signals.clone());
@ -74,37 +67,24 @@ impl Command for JobSpawn {
let jobs = job_state.jobs.clone(); let jobs = job_state.jobs.clone();
let mut jobs = jobs.lock().expect("jobs lock is poisoned!"); let mut jobs = jobs.lock().expect("jobs lock is poisoned!");
let (send, recv) = mpsc::channel();
let id = { let id = {
let thread_job = ThreadJob::new(job_signals, tag, send); let thread_job = ThreadJob::new(job_signals);
job_state.current_thread_job = Some(thread_job.clone());
let id = jobs.add_job(Job::Thread(thread_job.clone())); jobs.add_job(Job::Thread(thread_job))
job_state.current_job = CurrentJob {
id,
background_thread_job: Some(thread_job),
mailbox: Arc::new(Mutex::new(Mailbox::new(recv))),
};
id
}; };
let result = thread::Builder::new() let result = thread::Builder::new()
.name(format!("background job {}", id.get())) .name(format!("background job {}", id.get()))
.spawn(move || { .spawn(move || {
let mut stack = job_stack.reset_pipes(); ClosureEvalOnce::new(&job_state, &job_stack, closure)
let stack = stack.push_redirection(
Some(Redirection::Pipe(OutDest::Null)),
Some(Redirection::Pipe(OutDest::Null)),
);
ClosureEvalOnce::new_preserve_out_dest(&job_state, &stack, closure)
.run_with_input(Value::nothing(head).into_pipeline_data()) .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| { .unwrap_or_else(|err| {
if !job_state.signals().interrupted() { if !job_state.signals().interrupted() {
report_shell_error(&job_state, &err); report_shell_error(&job_state, &err);
} }
Value::nothing(head)
}); });
{ {

View File

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

View File

@ -112,13 +112,10 @@ fn unfreeze_job(
span, span,
}), }),
Job::Frozen(FrozenJob { Job::Frozen(FrozenJob { unfreeze: handle }) => {
unfreeze: handle,
tag,
}) => {
let pid = handle.pid(); 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) { if !thread_job.try_add_pid(pid) {
kill_by_pid(pid.into()).map_err(|err| { kill_by_pid(pid.into()).map_err(|err| {
ShellError::Io(IoError::new_internal( ShellError::Io(IoError::new_internal(
@ -136,7 +133,7 @@ fn unfreeze_job(
.then(|| state.pipeline_externals_state.clone()), .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); thread_job.remove_pid(pid);
} }
@ -144,14 +141,8 @@ fn unfreeze_job(
Ok(ForegroundWaitStatus::Frozen(handle)) => { Ok(ForegroundWaitStatus::Frozen(handle)) => {
let mut jobs = state.jobs.lock().expect("jobs lock is poisoned!"); let mut jobs = state.jobs.lock().expect("jobs lock is poisoned!");
jobs.add_job_with_id( jobs.add_job_with_id(old_id, Job::Frozen(FrozenJob { unfreeze: handle }))
old_id, .expect("job was supposed to be removed");
Job::Frozen(FrozenJob {
unfreeze: handle,
tag,
}),
)
.expect("job was supposed to be removed");
if state.is_interactive { if state.is_interactive {
println!("\nJob {} is re-frozen", old_id.get()); println!("\nJob {} is re-frozen", old_id.get());

View File

@ -1,35 +1,18 @@
mod is_admin; mod is_admin;
mod job; mod job;
mod job_id;
mod job_kill; mod job_kill;
mod job_list; mod job_list;
mod job_spawn; mod job_spawn;
mod job_tag;
#[cfg(all(unix, feature = "os"))] #[cfg(all(unix, feature = "os"))]
mod job_unfreeze; 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 is_admin::IsAdmin;
pub use job::Job; pub use job::Job;
pub use job_id::JobId;
pub use job_kill::JobKill; pub use job_kill::JobKill;
pub use job_list::JobList; pub use job_list::JobList;
pub use job_spawn::JobSpawn;
pub use job_tag::JobTag;
#[cfg(not(target_family = "wasm"))] pub use job_spawn::JobSpawn;
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;
#[cfg(all(unix, feature = "os"))] #[cfg(all(unix, feature = "os"))]
pub use job_unfreeze::JobUnfreeze; pub use job_unfreeze::JobUnfreeze;

View File

@ -132,7 +132,7 @@ impl Command for Cd {
stack.set_cwd(path)?; stack.set_cwd(path)?;
Ok(PipelineData::empty()) Ok(PipelineData::empty())
} }
PermissionResult::PermissionDenied => { PermissionResult::PermissionDenied(_) => {
Err(IoError::new(std::io::ErrorKind::PermissionDenied, call.head, path).into()) Err(IoError::new(std::io::ErrorKind::PermissionDenied, call.head, path).into())
} }
} }

View File

@ -35,11 +35,6 @@ impl Command for Glob {
"Whether to filter out symlinks from the returned paths", "Whether to filter out symlinks from the returned paths",
Some('S'), Some('S'),
) )
.switch(
"follow-symlinks",
"Whether to follow symbolic links to their targets",
Some('l'),
)
.named( .named(
"exclude", "exclude",
SyntaxShape::List(Box::new(SyntaxShape::String)), SyntaxShape::List(Box::new(SyntaxShape::String)),
@ -116,11 +111,6 @@ impl Command for Glob {
example: r#"glob **/* --exclude [**/target/** **/.git/** */]"#, example: r#"glob **/* --exclude [**/target/** **/.git/** */]"#,
result: None, 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_dirs = call.has_flag(engine_state, stack, "no-dir")?;
let no_files = call.has_flag(engine_state, stack, "no-file")?; let no_files = call.has_flag(engine_state, stack, "no-file")?;
let no_symlinks = call.has_flag(engine_state, stack, "no-symlink")?; 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 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 { 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 result = if !not_patterns.is_empty() {
let np: Vec<&str> = not_patterns.iter().map(|s| s as &str).collect(); let np: Vec<&str> = not_patterns.iter().map(|s| s as &str).collect();
let glob_results = glob let glob_results = glob
@ -236,7 +220,7 @@ impl Command for Glob {
path, path,
WalkBehavior { WalkBehavior {
depth: folder_depth, depth: folder_depth,
link: link_behavior, ..Default::default()
}, },
) )
.into_owned() .into_owned()
@ -263,7 +247,7 @@ impl Command for Glob {
path, path,
WalkBehavior { WalkBehavior {
depth: folder_depth, depth: folder_depth,
link: link_behavior, ..Default::default()
}, },
) )
.into_owned() .into_owned()

View File

@ -378,7 +378,10 @@ fn ls_for_one_pattern(
.par_bridge() .par_bridge()
.filter_map(move |x| match x { .filter_map(move |x| match x {
Ok(path) => { 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 hidden_dir_clone = Arc::clone(&hidden_dirs);
let mut hidden_dir_mutex = hidden_dir_clone let mut hidden_dir_mutex = hidden_dir_clone
.lock() .lock()

View File

@ -1,15 +1,11 @@
#[allow(deprecated)] #[allow(deprecated)]
use nu_engine::{command_prelude::*, current_dir, eval_call}; use nu_engine::{command_prelude::*, current_dir, get_eval_block};
use nu_protocol::{ use nu_protocol::{
ast, ast,
debugger::{WithDebug, WithoutDebug},
shell_error::{self, io::IoError}, shell_error::{self, io::IoError},
DataSource, NuGlob, PipelineMetadata, DataSource, NuGlob, PipelineMetadata,
}; };
use std::{ use std::path::{Path, PathBuf};
collections::HashMap,
path::{Path, PathBuf},
};
#[cfg(feature = "sqlite")] #[cfg(feature = "sqlite")]
use crate::database::SQLiteDatabase; use crate::database::SQLiteDatabase;
@ -34,14 +30,7 @@ impl Command for Open {
} }
fn search_terms(&self) -> Vec<&str> { fn search_terms(&self) -> Vec<&str> {
vec![ vec!["load", "read", "load_file", "read_file"]
"load",
"read",
"load_file",
"read_file",
"cat",
"get-content",
]
} }
fn signature(&self) -> nu_protocol::Signature { fn signature(&self) -> nu_protocol::Signature {
@ -74,6 +63,7 @@ impl Command for Open {
#[allow(deprecated)] #[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?; let cwd = current_dir(engine_state, stack)?;
let mut paths = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?; 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) { 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 // 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 { match converter {
Some((converter_id, ext)) => { Some((converter_id, ext)) => {
let open_call = ast::Call { let decl = engine_state.get_decl(converter_id);
decl_id: converter_id, let command_output = if let Some(block_id) = decl.block_id() {
head: call_span, let block = engine_state.get_block(block_id);
arguments: vec![], eval_block(engine_state, stack, block, stream)
parser_info: HashMap::new(),
};
let command_output = if engine_state.is_debugging() {
eval_call::<WithDebug>(engine_state, stack, &open_call, stream)
} else { } 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| { output.push(command_output.map_err(|inner| {
ShellError::GenericError{ ShellError::GenericError{
@ -281,16 +268,6 @@ impl Command for Open {
example: r#"def "from ndjson" [] { from json -o }; open myfile.ndjson"#, example: r#"def "from ndjson" [] { from json -o }; open myfile.ndjson"#,
result: None, 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,
}
] ]
} }
} }

View File

@ -10,7 +10,11 @@ use nu_protocol::{
}; };
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::prelude::FileTypeExt; 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( const TRASH_SUPPORTED: bool = cfg!(all(
feature = "trash-support", feature = "trash-support",
@ -375,7 +379,7 @@ fn rm(
); );
let result = if let Err(e) = interaction { 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 { } else if interactive && !confirmed {
Ok(()) Ok(())
} else if TRASH_SUPPORTED && (trash || (rm_always_trash && !permanent)) { } else if TRASH_SUPPORTED && (trash || (rm_always_trash && !permanent)) {
@ -385,7 +389,7 @@ fn rm(
))] ))]
{ {
trash::delete(&f).map_err(|e: trash::Error| { trash::delete(&f).map_err(|e: trash::Error| {
Error::other(format!("{e:?}\nTry '--permanent' flag")) Error::new(ErrorKind::Other, format!("{e:?}\nTry '--permanent' flag"))
}) })
} }

View File

@ -263,17 +263,6 @@ impl Command for Save {
example: r#"do -i {} | save foo.txt --stderr bar.txt"#, example: r#"do -i {} | save foo.txt --stderr bar.txt"#,
result: None, 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,
},
] ]
} }

View File

@ -158,15 +158,17 @@ impl Command for UTouch {
continue; continue;
} }
let mut expanded_globs = let mut expanded_globs = glob(
glob(&file_path.to_string_lossy(), engine_state.signals().clone()) &file_path.to_string_lossy(),
.unwrap_or_else(|_| { Some(engine_state.signals().clone()),
panic!( )
"Failed to process file path: {}", .unwrap_or_else(|_| {
&file_path.to_string_lossy() panic!(
) "Failed to process file path: {}",
}) &file_path.to_string_lossy()
.peekable(); )
})
.peekable();
if expanded_globs.peek().is_none() { if expanded_globs.peek().is_none() {
let file_name = file_path.file_name().unwrap_or_else(|| { let file_name = file_path.file_name().unwrap_or_else(|| {

View File

@ -30,11 +30,6 @@ impl Command for All {
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![
Example {
description: "Check if a list contains only true values",
example: "[false true true false] | all {}",
result: Some(Value::test_bool(false)),
},
Example { Example {
description: "Check if each row's status is the string 'UP'", description: "Check if each row's status is the string 'UP'",
example: "[[status]; [UP] [UP]] | all {|el| $el.status == UP }", example: "[[status]; [UP] [UP]] | all {|el| $el.status == UP }",

View File

@ -30,11 +30,6 @@ impl Command for Any {
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![
Example {
description: "Check if a list contains any true values",
example: "[false true true false] | any {}",
result: Some(Value::test_bool(true)),
},
Example { Example {
description: "Check if any row's status is the string 'DOWN'", description: "Check if any row's status is the string 'DOWN'",
example: "[[status]; [UP] [DOWN] [UP]] | any {|el| $el.status == DOWN }", example: "[[status]; [UP] [DOWN] [UP]] | any {|el| $el.status == DOWN }",

View File

@ -243,7 +243,7 @@ mod test {
let chunks = chunk_read.map(|e| e.unwrap()).collect::<Vec<_>>(); let chunks = chunk_read.map(|e| e.unwrap()).collect::<Vec<_>>();
assert_eq!( assert_eq!(
chunks, 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<_>>(); let chunks = chunk_read.map(|e| e.unwrap()).collect::<Vec<_>>();
assert_eq!( assert_eq!(
chunks, 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()]
); );
} }

View File

@ -1,5 +1,4 @@
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use nu_protocol::{ListStream, Signals};
#[derive(Clone)] #[derive(Clone)]
pub struct Default; pub struct Default;
@ -147,20 +146,6 @@ fn default(
&& matches!(input, PipelineData::Value(ref value, _) if value.is_empty())) && matches!(input, PipelineData::Value(ref value, _) if value.is_empty()))
{ {
Ok(value.into_pipeline_data()) 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 { } else {
Ok(input) Ok(input)
} }

View File

@ -15,8 +15,17 @@ pub fn empty(
if !columns.is_empty() { if !columns.is_empty() {
for val in input { for val in input {
for column in &columns { for column in &columns {
if !val.follow_cell_path(&column.members, false)?.is_nothing() { let val = val.clone();
return Ok(Value::bool(negate, head).into_pipeline_data()); 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),
} }
} }
} }

View File

@ -1,5 +1,3 @@
use std::borrow::Cow;
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use nu_protocol::{ast::PathMember, Signals}; use nu_protocol::{ast::PathMember, Signals};
@ -190,11 +188,9 @@ fn action(
let input = input.into_value(span)?; let input = input.into_value(span)?;
for path in paths { for path in paths {
output.push( let val = input.clone().follow_cell_path(&path.members, !sensitive);
input
.follow_cell_path(&path.members, !sensitive)? output.push(val?);
.into_owned(),
);
} }
Ok(output.into_iter().into_pipeline_data(span, signals)) Ok(output.into_iter().into_pipeline_data(span, signals))
@ -227,10 +223,10 @@ pub fn follow_cell_path_into_stream(
.map(move |value| { .map(move |value| {
let span = value.span(); let span = value.span();
value match value.follow_cell_path(&cell_path, insensitive) {
.follow_cell_path(&cell_path, insensitive) Ok(v) => v,
.map(Cow::into_owned) Err(error) => Value::error(error, span),
.unwrap_or_else(|error| Value::error(error, span)) }
}) })
.into_pipeline_data(head, signals); .into_pipeline_data(head, signals);

View File

@ -322,9 +322,11 @@ fn group_cell_path(
let mut groups = IndexMap::<_, Vec<_>>::new(); let mut groups = IndexMap::<_, Vec<_>>::new();
for value in values.into_iter() { 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 continue; // likely the result of a failed optional access, ignore this value
} }

View File

@ -1,5 +1,3 @@
use std::borrow::Cow;
use nu_engine::{command_prelude::*, ClosureEval, ClosureEvalOnce}; use nu_engine::{command_prelude::*, ClosureEval, ClosureEvalOnce};
use nu_protocol::ast::PathMember; use nu_protocol::ast::PathMember;
@ -301,8 +299,8 @@ fn insert_value_by_closure(
) -> Result<(), ShellError> { ) -> Result<(), ShellError> {
let value_at_path = if first_path_member_int { let value_at_path = if first_path_member_int {
value value
.clone()
.follow_cell_path(cell_path, false) .follow_cell_path(cell_path, false)
.map(Cow::into_owned)
.unwrap_or(Value::nothing(span)) .unwrap_or(Value::nothing(span))
} else { } else {
value.clone() value.clone()
@ -321,8 +319,8 @@ fn insert_single_value_by_closure(
) -> Result<(), ShellError> { ) -> Result<(), ShellError> {
let value_at_path = if first_path_member_int { let value_at_path = if first_path_member_int {
value value
.clone()
.follow_cell_path(cell_path, false) .follow_cell_path(cell_path, false)
.map(Cow::into_owned)
.unwrap_or(Value::nothing(span)) .unwrap_or(Value::nothing(span))
} else { } else {
value.clone() value.clone()

View File

@ -255,16 +255,6 @@ fn join_rows(
config: &Config, config: &Config,
span: Span, 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 { for this_row in this {
if let Value::Record { if let Value::Record {
val: this_record, .. val: this_record, ..
@ -291,40 +281,39 @@ fn join_rows(
result.push(Value::record(record, span)) 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))
} }
} } // else { a row is missing a value for the join column }
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))
}
}; };
} }
} }

View File

@ -42,9 +42,8 @@ pub(crate) fn typecheck_merge(lhs: &Value, rhs: &Value, head: Span) -> Result<()
match (lhs.get_type(), rhs.get_type()) { match (lhs.get_type(), rhs.get_type()) {
(Type::Record { .. }, Type::Record { .. }) => Ok(()), (Type::Record { .. }, Type::Record { .. }) => Ok(()),
(_, _) if is_list_of_records(lhs) && is_list_of_records(rhs) => 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(), 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, dst_span: head,
src_span: lhs.span(), src_span: lhs.span(),
}), }),

View File

@ -174,9 +174,8 @@ impl Command for Move {
PipelineData::Value(Value::Record { val, .. }, ..) => { PipelineData::Value(Value::Record { val, .. }, ..) => {
Ok(move_record_columns(&val, &columns, &location, head)?.into_pipeline_data()) 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(), exp_input_type: "record or table".to_string(),
wrong_type: other.get_type().to_string(),
dst_span: head, dst_span: head,
src_span: Span::new(head.start, head.start), src_span: Span::new(head.start, head.start),
}), }),

View File

@ -229,37 +229,45 @@ fn select(
match v { match v {
Value::List { Value::List {
vals: input_vals, .. vals: input_vals, ..
} => Ok(input_vals } => {
.into_iter() Ok(input_vals
.map(move |input_val| { .into_iter()
if !columns.is_empty() { .map(move |input_val| {
let mut record = Record::new(); if !columns.is_empty() {
for path in &columns { let mut record = Record::new();
match input_val.follow_cell_path(&path.members, false) { for path in &columns {
Ok(fetcher) => { //FIXME: improve implementation to not clone
record.push(path.to_column_name(), fetcher.into_owned()); 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) Value::record(record, span)
} else { } else {
input_val.clone() input_val.clone()
} }
}) })
.into_pipeline_data_with_metadata( .into_pipeline_data_with_metadata(
call_span, call_span,
engine_state.signals().clone(), engine_state.signals().clone(),
metadata, metadata,
)), ))
}
_ => { _ => {
if !columns.is_empty() { if !columns.is_empty() {
let mut record = Record::new(); let mut record = Record::new();
for cell_path in columns { for cell_path in columns {
let result = v.follow_cell_path(&cell_path.members, false)?; // FIXME: remove clone
record.push(cell_path.to_column_name(), result.into_owned()); 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) Ok(Value::record(record, call_span)
@ -270,24 +278,31 @@ fn select(
} }
} }
} }
PipelineData::ListStream(stream, metadata, ..) => Ok(stream PipelineData::ListStream(stream, metadata, ..) => {
.map(move |x| { Ok(stream
if !columns.is_empty() { .map(move |x| {
let mut record = Record::new(); if !columns.is_empty() {
for path in &columns { let mut record = Record::new();
match x.follow_cell_path(&path.members, false) { for path in &columns {
Ok(value) => { //FIXME: improve implementation to not clone
record.push(path.to_column_name(), value.into_owned()); 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 { .into_pipeline_data_with_metadata(
x call_span,
} engine_state.signals().clone(),
}) metadata,
.into_pipeline_data_with_metadata(call_span, engine_state.signals().clone(), metadata)), ))
}
_ => Ok(PipelineData::empty()), _ => Ok(PipelineData::empty()),
} }
} }

View File

@ -1,5 +1,5 @@
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use rand::{prelude::SliceRandom, rng}; use rand::{prelude::SliceRandom, thread_rng};
#[derive(Clone)] #[derive(Clone)]
pub struct Shuffle; pub struct Shuffle;
@ -31,7 +31,7 @@ impl Command for Shuffle {
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let metadata = input.metadata(); let metadata = input.metadata();
let mut values = input.into_iter_strict(call.head)?.collect::<Vec<_>>(); 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(); let iter = values.into_iter();
Ok(iter.into_pipeline_data_with_metadata( Ok(iter.into_pipeline_data_with_metadata(
call.head, call.head,

View File

@ -184,10 +184,9 @@ impl Command for Sort {
dst_span: value.span(), dst_span: value.span(),
}) })
} }
ref other => { _ => {
return Err(ShellError::OnlySupportsThisInputType { return Err(ShellError::PipelineMismatch {
exp_input_type: "record or list".to_string(), exp_input_type: "record or list".to_string(),
wrong_type: other.get_type().to_string(),
dst_span: call.head, dst_span: call.head,
src_span: value.span(), src_span: value.span(),
}) })

View File

@ -394,7 +394,7 @@ impl<R: Read> Read for IoTee<R> {
if let Some(thread) = self.thread.take() { if let Some(thread) = self.thread.take() {
if thread.is_finished() { if thread.is_finished() {
if let Err(err) = thread.join().unwrap_or_else(|_| Err(panic_error())) { 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 { } else {
self.thread = Some(thread) self.thread = Some(thread)
@ -405,7 +405,7 @@ impl<R: Read> Read for IoTee<R> {
self.sender = None; self.sender = None;
if let Some(thread) = self.thread.take() { if let Some(thread) = self.thread.take() {
if let Err(err) = thread.join().unwrap_or_else(|_| Err(panic_error())) { 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() { } else if let Some(sender) = self.sender.as_mut() {

View File

@ -243,17 +243,17 @@ fn update_value_by_closure(
cell_path: &[PathMember], cell_path: &[PathMember],
first_path_member_int: bool, first_path_member_int: bool,
) -> Result<(), ShellError> { ) -> 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 { let arg = if first_path_member_int {
value_at_path.as_ref() &value_at_path
} else { } else {
&*value &*value
}; };
let new_value = closure let new_value = closure
.add_arg(arg.clone()) .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)?; .into_value(span)?;
value.update_data_at_cell_path(cell_path, new_value) value.update_data_at_cell_path(cell_path, new_value)
@ -266,17 +266,17 @@ fn update_single_value_by_closure(
cell_path: &[PathMember], cell_path: &[PathMember],
first_path_member_int: bool, first_path_member_int: bool,
) -> Result<(), ShellError> { ) -> 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 { let arg = if first_path_member_int {
value_at_path.as_ref() &value_at_path
} else { } else {
&*value &*value
}; };
let new_value = closure let new_value = closure
.add_arg(arg.clone()) .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)?; .into_value(span)?;
value.update_data_at_cell_path(cell_path, new_value) value.update_data_at_cell_path(cell_path, new_value)

View File

@ -1,5 +1,3 @@
use std::borrow::Cow;
use nu_engine::{command_prelude::*, ClosureEval, ClosureEvalOnce}; use nu_engine::{command_prelude::*, ClosureEval, ClosureEvalOnce};
use nu_protocol::ast::PathMember; use nu_protocol::ast::PathMember;
@ -321,19 +319,15 @@ fn upsert_value_by_closure(
cell_path: &[PathMember], cell_path: &[PathMember],
first_path_member_int: bool, first_path_member_int: bool,
) -> Result<(), ShellError> { ) -> 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 { let arg = if first_path_member_int {
value_at_path value_at_path.clone().unwrap_or(Value::nothing(span))
.as_deref()
.cloned()
.unwrap_or(Value::nothing(span))
} else { } else {
value.clone() value.clone()
}; };
let input = value_at_path let input = value_at_path
.map(Cow::into_owned)
.map(IntoPipelineData::into_pipeline_data) .map(IntoPipelineData::into_pipeline_data)
.unwrap_or(PipelineData::Empty); .unwrap_or(PipelineData::Empty);
@ -352,19 +346,15 @@ fn upsert_single_value_by_closure(
cell_path: &[PathMember], cell_path: &[PathMember],
first_path_member_int: bool, first_path_member_int: bool,
) -> Result<(), ShellError> { ) -> 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 { let arg = if first_path_member_int {
value_at_path value_at_path.clone().unwrap_or(Value::nothing(span))
.as_deref()
.cloned()
.unwrap_or(Value::nothing(span))
} else { } else {
value.clone() value.clone()
}; };
let input = value_at_path let input = value_at_path
.map(Cow::into_owned)
.map(IntoPipelineData::into_pipeline_data) .map(IntoPipelineData::into_pipeline_data)
.unwrap_or(PipelineData::Empty); .unwrap_or(PipelineData::Empty);

View File

@ -36,13 +36,13 @@ impl Command for ToMd {
Example { Example {
description: "Outputs an MD string representing the contents of this table", description: "Outputs an MD string representing the contents of this table",
example: "[[foo bar]; [1 2]] | to md", 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 { Example {
description: "Optionally, output a formatted markdown string", description: "Optionally, output a formatted markdown string",
example: "[[foo bar]; [1 2]] | to md --pretty", example: "[[foo bar]; [1 2]] | to md --pretty",
result: Some(Value::test_string( result: Some(Value::test_string(
"| foo | bar |\n| --- | --- |\n| 1 | 2 |", "| foo | bar |\n| --- | --- |\n| 1 | 2 |\n",
)), )),
}, },
Example { Example {
@ -57,13 +57,6 @@ impl Command for ToMd {
example: "[0 1 2] | to md --pretty", example: "[0 1 2] | to md --pretty",
result: Some(Value::test_string("0\n1\n2")), 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 grouped_input
.into_iter() .into_iter()
.map(move |val| match val { .map(move |val| match val {
Value::List { .. } => { Value::List { .. } => table(val.into_pipeline_data(), pretty, config),
format!("{}\n", table(val.into_pipeline_data(), pretty, config))
}
other => fragment(other, pretty, config), other => fragment(other, pretty, config),
}) })
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("") .join(""),
.trim(),
head, head,
) )
.into_pipeline_data_with_metadata(Some(metadata))); .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 { fn table(input: PipelineData, pretty: bool, config: &Config) -> String {
let vec_of_values = input let vec_of_values = input.into_iter().collect::<Vec<Value>>();
.into_iter()
.flat_map(|val| match val {
Value::List { vals, .. } => vals,
other => vec![other],
})
.collect::<Vec<Value>>();
let mut headers = merge_descriptors(&vec_of_values); let mut headers = merge_descriptors(&vec_of_values);
let mut empty_header_index = 0; 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] #[test]
fn test_content_type_metadata() { fn test_content_type_metadata() {
let mut engine_state = Box::new(EngineState::new()); let mut engine_state = Box::new(EngineState::new());

View File

@ -1,4 +1,3 @@
use chrono::Datelike;
use chrono_humanize::HumanTime; use chrono_humanize::HumanTime;
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use nu_protocol::{format_duration, shell_error::io::IoError, ByteStream, PipelineMetadata}; 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::Filesize { val, .. } => val.to_string(),
Value::Duration { val, .. } => format_duration(val), Value::Duration { val, .. } => format_duration(val),
Value::Date { val, .. } => { Value::Date { val, .. } => {
format!( format!("{} ({})", val.to_rfc2822(), HumanTime::from(val))
"{} ({})",
{
if val.year() >= 0 {
val.to_rfc2822()
} else {
val.to_rfc3339()
}
},
HumanTime::from(val)
)
} }
Value::Range { val, .. } => val.to_string(), Value::Range { val, .. } => val.to_string(),
Value::String { val, .. } => val, Value::String { val, .. } => val,

View File

@ -1,4 +1,3 @@
use crate::math::utils::ensure_bounded;
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
#[derive(Clone)] #[derive(Clone)]
@ -22,7 +21,6 @@ impl Command for MathAbs {
Type::List(Box::new(Type::Duration)), Type::List(Box::new(Type::Duration)),
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) .allow_variants_without_examples(true)
.category(Category::Math) .category(Category::Math)
@ -48,16 +46,6 @@ impl Command for MathAbs {
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; 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()) input.map(move |value| abs_helper(value, head), engine_state.signals())
} }
@ -68,16 +56,6 @@ impl Command for MathAbs {
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; 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( input.map(
move |value| abs_helper(value, head), move |value| abs_helper(value, head),
working_set.permanent().signals(), working_set.permanent().signals(),

View File

@ -1,4 +1,3 @@
use crate::math::utils::ensure_bounded;
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
#[derive(Clone)] #[derive(Clone)]
@ -17,7 +16,6 @@ impl Command for MathCeil {
Type::List(Box::new(Type::Number)), Type::List(Box::new(Type::Number)),
Type::List(Box::new(Type::Int)), Type::List(Box::new(Type::Int)),
), ),
(Type::Range, Type::List(Box::new(Type::Number))),
]) ])
.allow_variants_without_examples(true) .allow_variants_without_examples(true)
.category(Category::Math) .category(Category::Math)
@ -47,16 +45,6 @@ impl Command for MathCeil {
if matches!(input, PipelineData::Empty) { if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head }); 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()) input.map(move |value| operate(value, head), engine_state.signals())
} }
@ -71,16 +59,6 @@ impl Command for MathCeil {
if matches!(input, PipelineData::Empty) { if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head }); 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( input.map(
move |value| operate(value, head), move |value| operate(value, head),
working_set.permanent().signals(), working_set.permanent().signals(),

View File

@ -1,4 +1,3 @@
use crate::math::utils::ensure_bounded;
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
#[derive(Clone)] #[derive(Clone)]
@ -17,7 +16,6 @@ impl Command for MathFloor {
Type::List(Box::new(Type::Number)), Type::List(Box::new(Type::Number)),
Type::List(Box::new(Type::Int)), Type::List(Box::new(Type::Int)),
), ),
(Type::Range, Type::List(Box::new(Type::Number))),
]) ])
.allow_variants_without_examples(true) .allow_variants_without_examples(true)
.category(Category::Math) .category(Category::Math)
@ -47,16 +45,6 @@ impl Command for MathFloor {
if matches!(input, PipelineData::Empty) { if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head }); 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()) input.map(move |value| operate(value, head), engine_state.signals())
} }
@ -71,16 +59,6 @@ impl Command for MathFloor {
if matches!(input, PipelineData::Empty) { if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head }); 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( input.map(
move |value| operate(value, head), move |value| operate(value, head),
working_set.permanent().signals(), working_set.permanent().signals(),

View File

@ -1,4 +1,3 @@
use crate::math::utils::ensure_bounded;
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use nu_protocol::Signals; use nu_protocol::Signals;
@ -23,7 +22,6 @@ impl Command for MathLog {
Type::List(Box::new(Type::Number)), Type::List(Box::new(Type::Number)),
Type::List(Box::new(Type::Float)), Type::List(Box::new(Type::Float)),
), ),
(Type::Range, Type::List(Box::new(Type::Number))),
]) ])
.allow_variants_without_examples(true) .allow_variants_without_examples(true)
.category(Category::Math) .category(Category::Math)
@ -48,18 +46,7 @@ impl Command for MathLog {
call: &Call, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head;
let base: Spanned<f64> = call.req(engine_state, stack, 0)?; 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()) log(base, call.head, input, engine_state.signals())
} }
@ -69,18 +56,7 @@ impl Command for MathLog {
call: &Call, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head;
let base: Spanned<f64> = call.req_const(working_set, 0)?; 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()) log(base, call.head, input, working_set.permanent().signals())
} }

View File

@ -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 //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 // But f64 doesn't implement Hash, so we get the binary representation to use as
// key in the HashMap // key in the HashMap
@ -130,11 +130,11 @@ pub fn mode(values: &[Value], span: Span, head: Span) -> Result<Value, ShellErro
NumberTypes::Filesize, NumberTypes::Filesize,
)), )),
Value::Error { error, .. } => Err(*error.clone()), Value::Error { error, .. } => Err(*error.clone()),
_ => Err(ShellError::UnsupportedInput { other => Err(ShellError::UnsupportedInput {
msg: "Unable to give a result with this input".to_string(), msg: "Unable to give a result with this input".to_string(),
input: "value originates from here".into(), input: "value originates from here".into(),
msg_span: head, msg_span: head,
input_span: span, input_span: other.span(),
}), }),
}) })
.collect::<Result<Vec<HashableType>, ShellError>>()?; .collect::<Result<Vec<HashableType>, ShellError>>()?;

View File

@ -1,4 +1,3 @@
use crate::math::utils::ensure_bounded;
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
#[derive(Clone)] #[derive(Clone)]
@ -17,7 +16,6 @@ impl Command for MathRound {
Type::List(Box::new(Type::Number)), Type::List(Box::new(Type::Number)),
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) .allow_variants_without_examples(true)
.named( .named(
@ -54,16 +52,6 @@ impl Command for MathRound {
if matches!(input, PipelineData::Empty) { if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head }); 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( input.map(
move |value| operate(value, head, precision_param), move |value| operate(value, head, precision_param),
engine_state.signals(), engine_state.signals(),
@ -82,16 +70,6 @@ impl Command for MathRound {
if matches!(input, PipelineData::Empty) { if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head }); 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( input.map(
move |value| operate(value, head, precision_param), move |value| operate(value, head, precision_param),
working_set.permanent().signals(), working_set.permanent().signals(),

View File

@ -1,4 +1,3 @@
use crate::math::utils::ensure_bounded;
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
#[derive(Clone)] #[derive(Clone)]
@ -17,7 +16,6 @@ impl Command for MathSqrt {
Type::List(Box::new(Type::Number)), Type::List(Box::new(Type::Number)),
Type::List(Box::new(Type::Float)), Type::List(Box::new(Type::Float)),
), ),
(Type::Range, Type::List(Box::new(Type::Number))),
]) ])
.allow_variants_without_examples(true) .allow_variants_without_examples(true)
.category(Category::Math) .category(Category::Math)
@ -47,16 +45,6 @@ impl Command for MathSqrt {
if matches!(input, PipelineData::Empty) { if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head }); 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()) input.map(move |value| operate(value, head), engine_state.signals())
} }
@ -71,16 +59,6 @@ impl Command for MathSqrt {
if matches!(input, PipelineData::Empty) { if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head }); 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( input.map(
move |value| operate(value, head), move |value| operate(value, head),
working_set.permanent().signals(), working_set.permanent().signals(),

View File

@ -14,7 +14,6 @@ impl Command for MathStddev {
Signature::build("math stddev") Signature::build("math stddev")
.input_output_types(vec![ .input_output_types(vec![
(Type::List(Box::new(Type::Number)), Type::Number), (Type::List(Box::new(Type::Number)), Type::Number),
(Type::Range, Type::Number),
(Type::table(), Type::record()), (Type::table(), Type::record()),
(Type::record(), Type::record()), (Type::record(), Type::record()),
]) ])
@ -54,18 +53,6 @@ impl Command for MathStddev {
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let sample = call.has_flag(engine_state, stack, "sample")?; 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)) run_with_function(call, input, compute_stddev(sample))
} }
@ -76,18 +63,6 @@ impl Command for MathStddev {
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let sample = call.has_flag_const(working_set, "sample")?; 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)) run_with_function(call, input, compute_stddev(sample))
} }

View File

@ -1,4 +1,4 @@
use core::slice; use core::{ops::Bound, slice};
use indexmap::IndexMap; use indexmap::IndexMap;
use nu_protocol::{ use nu_protocol::{
engine::Call, IntoPipelineData, PipelineData, Range, ShellError, Signals, Span, Value, engine::Call, IntoPipelineData, PipelineData, Range, ShellError, Signals, Span, Value,
@ -93,7 +93,10 @@ pub fn calculate(
Ok(Value::record(record, span)) Ok(Value::record(record, span))
} }
PipelineData::Value(Value::Range { val, .. }, ..) => { 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 let new_vals: Result<Vec<Value>, ShellError> = val
.into_range_iter(span, Signals::empty()) .into_range_iter(span, Signals::empty())
.map(|val| mf(&[val], span, name)) .map(|val| mf(&[val], span, name))
@ -102,7 +105,7 @@ pub fn calculate(
mf(&new_vals?, span, name) mf(&new_vals?, span, name)
} }
PipelineData::Value(val, ..) => mf(&[val], 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 { val => Err(ShellError::UnsupportedInput {
msg: "Only ints, floats, lists, records, or ranges are supported".into(), msg: "Only ints, floats, lists, records, or ranges are supported".into(),
input: "value originates from here".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> { pub fn ensure_bounded<T>(
if range.is_bounded() { bound: Bound<T>,
return Ok(()); 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,
})
} }

View File

@ -13,7 +13,6 @@ impl Command for MathVariance {
Signature::build("math variance") Signature::build("math variance")
.input_output_types(vec![ .input_output_types(vec![
(Type::List(Box::new(Type::Number)), Type::Number), (Type::List(Box::new(Type::Number)), Type::Number),
(Type::Range, Type::Number),
(Type::table(), Type::record()), (Type::table(), Type::record()),
(Type::record(), Type::record()), (Type::record(), Type::record()),
]) ])
@ -46,18 +45,6 @@ impl Command for MathVariance {
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let sample = call.has_flag(engine_state, stack, "sample")?; 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)) run_with_function(call, input, compute_variance(sample))
} }
@ -68,18 +55,6 @@ impl Command for MathVariance {
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let sample = call.has_flag_const(working_set, "sample")?; 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)) run_with_function(call, input, compute_variance(sample))
} }

View File

@ -723,7 +723,7 @@ fn transform_response_using_content_type(
) )
})? })?
.path_segments() .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| if name.is_empty() { None } else { Some(name) })
.and_then(|name| { .and_then(|name| {
PathBuf::from(name) PathBuf::from(name)

View File

@ -117,13 +117,8 @@ impl Command for HttpDelete {
result: None, result: None,
}, },
Example { Example {
description: "http delete from example.com, with custom header using a record", description: "http delete from example.com, with custom header",
example: "http delete --headers {my-header-key: my-header-value} https://www.example.com", 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",
result: None, result: None,
}, },
Example { Example {

Some files were not shown because too many files have changed in this diff Show More