Merge branch 'main' of https://github.com/nushell/nushell into patch/bump-tabled-try

This commit is contained in:
Maxim Zhiburt 2024-08-06 00:26:38 +03:00
commit 9f79750ac3
199 changed files with 3676 additions and 1978 deletions

View File

@ -161,7 +161,7 @@ jobs:
# REF: https://github.com/marketplace/actions/gh-release
# Create a release only in nushell/nightly repo
- name: Publish Archive
uses: softprops/action-gh-release@v2.0.6
uses: softprops/action-gh-release@v2.0.8
if: ${{ startsWith(github.repository, 'nushell/nightly') }}
with:
prerelease: true

View File

@ -161,8 +161,12 @@ if $os in ['macos-latest'] or $USE_UBUNTU {
let releaseStem = $'($bin)-($version)-($target)'
print $'(char nl)Download less related stuffs...'; hr-line
# todo: less-v661 is out but is released as a zip file. maybe we should switch to that and extract it?
aria2c https://github.com/jftuga/less-Windows/releases/download/less-v608/less.exe -o less.exe
aria2c https://raw.githubusercontent.com/jftuga/less-Windows/master/LICENSE -o LICENSE-for-less.txt
# the below was renamed because it was failing to download for darren. it should work but it wasn't
# todo: maybe we should get rid of this aria2c dependency and just use http get?
#aria2c https://raw.githubusercontent.com/jftuga/less-Windows/master/LICENSE -o LICENSE-for-less.txt
aria2c https://github.com/jftuga/less-Windows/blob/master/LICENSE -o LICENSE-for-less.txt
# Create Windows msi release package
if (get-env _EXTRA_) == 'msi' {

View File

@ -91,7 +91,7 @@ jobs:
# REF: https://github.com/marketplace/actions/gh-release
- name: Publish Archive
uses: softprops/action-gh-release@v2.0.6
uses: softprops/action-gh-release@v2.0.8
if: ${{ startsWith(github.ref, 'refs/tags/') }}
with:
draft: true

View File

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

428
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"
name = "nu"
repository = "https://github.com/nushell/nushell"
rust-version = "1.77.2"
version = "0.95.1"
rust-version = "1.78.0"
version = "0.96.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -116,7 +116,7 @@ mockito = { version = "1.4", default-features = false }
native-tls = "0.2"
nix = { version = "0.28", default-features = false }
notify-debouncer-full = { version = "0.3", default-features = false }
nu-ansi-term = "0.50.0"
nu-ansi-term = "0.50.1"
num-format = "0.4"
num-traits = "0.2"
omnipath = "0.1"
@ -138,7 +138,7 @@ quote = "1.0"
rand = "0.8"
ratatui = "0.26"
rayon = "1.10"
reedline = "0.32.0"
reedline = "0.33.0"
regex = "1.9.5"
rmp = "0.8"
rmp-serde = "1.3"
@ -183,22 +183,22 @@ windows-sys = "0.48"
winreg = "0.52"
[dependencies]
nu-cli = { path = "./crates/nu-cli", version = "0.95.1" }
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.95.1" }
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.95.1" }
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.95.1", optional = true }
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.95.1" }
nu-command = { path = "./crates/nu-command", version = "0.95.1" }
nu-engine = { path = "./crates/nu-engine", version = "0.95.1" }
nu-explore = { path = "./crates/nu-explore", version = "0.95.1" }
nu-lsp = { path = "./crates/nu-lsp/", version = "0.95.1" }
nu-parser = { path = "./crates/nu-parser", version = "0.95.1" }
nu-path = { path = "./crates/nu-path", version = "0.95.1" }
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.95.1" }
nu-protocol = { path = "./crates/nu-protocol", version = "0.95.1" }
nu-std = { path = "./crates/nu-std", version = "0.95.1" }
nu-system = { path = "./crates/nu-system", version = "0.95.1" }
nu-utils = { path = "./crates/nu-utils", version = "0.95.1" }
nu-cli = { path = "./crates/nu-cli", version = "0.96.2" }
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.96.2" }
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.96.2" }
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.96.2", optional = true }
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.96.2" }
nu-command = { path = "./crates/nu-command", version = "0.96.2" }
nu-engine = { path = "./crates/nu-engine", version = "0.96.2" }
nu-explore = { path = "./crates/nu-explore", version = "0.96.2" }
nu-lsp = { path = "./crates/nu-lsp/", version = "0.96.2" }
nu-parser = { path = "./crates/nu-parser", version = "0.96.2" }
nu-path = { path = "./crates/nu-path", version = "0.96.2" }
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.96.2" }
nu-protocol = { path = "./crates/nu-protocol", version = "0.96.2" }
nu-std = { path = "./crates/nu-std", version = "0.96.2" }
nu-system = { path = "./crates/nu-system", version = "0.96.2" }
nu-utils = { path = "./crates/nu-utils", version = "0.96.2" }
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
crossterm = { workspace = true }
@ -227,9 +227,9 @@ nix = { workspace = true, default-features = false, features = [
] }
[dev-dependencies]
nu-test-support = { path = "./crates/nu-test-support", version = "0.95.1" }
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.95.1" }
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.95.1" }
nu-test-support = { path = "./crates/nu-test-support", version = "0.96.2" }
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.96.2" }
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.96.2" }
assert_cmd = "2.0"
dirs = { workspace = true }
tango-bench = "0.5"

29
SECURITY.md Normal file
View File

@ -0,0 +1,29 @@
# Security Policy
As a shell and programming language Nushell provides you with great powers and the potential to do dangerous things to your computer and data. Whenever there is a risk that a malicious actor can abuse a bug or a violation of documented behavior/assumptions in Nushell to harm you this is a *security* risk.
We want to fix those issues without exposing our users to unnecessary risk. Thus we want to explain our security policy.
Additional issues may be part of *safety* where the behavior of Nushell as designed and implemented can cause unintended harm or a bug causes damage without the involvement of a third party.
## Supported Versions
As Nushell is still under very active pre-stable development, the only version the core team prioritizes for security and safety fixes is the [most recent version as published on GitHub](https://github.com/nushell/nushell/releases/latest).
Only if you provide a strong reasoning and the necessary resources, will we consider blessing a backported fix with an official patch release for a previous version.
## Reporting a Vulnerability
If you suspect that a bug or behavior of Nushell can affect security or may be potentially exploitable, please report the issue to us in private.
Either reach out to the core team on [our Discord server](https://discord.gg/NtAbbGn) to arrange a private channel or use the [GitHub vulnerability reporting form](https://github.com/nushell/nushell/security/advisories/new).
Please try to answer the following questions:
- How can we reach you for further questions?
- What is the bug? Which system of Nushell may be affected?
- Do you have proof-of-concept for a potential exploit or have you observed an exploit in the wild?
- What is your assessment of the severity based on what could be impacted should the bug be exploited?
- Are additional people aware of the issue or deserve credit for identifying the issue?
We will try to get back to you within a week with:
- acknowledging the receipt of the report
- an initial plan of how we want to address this including the primary points of contact for further communication
- our preliminary assessment of how severe we judge the issue
- a proposal for how we can coordinate responsible disclosure (e.g. how we ship the bugfix, if we need to coordinate with distribution maintainers, when you can release a blog post if you want to etc.)
For purely *safety* related issues where the impact is severe by direct user action instead of malicious input or third parties, feel free to open a regular issue. If we deem that there may be an additional *security* risk on a *safety* issue we may continue discussions in a restricted forum.

View File

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

View File

@ -67,7 +67,7 @@ impl Command for History {
} else {
let history_reader: Option<Box<dyn ReedlineHistory>> = match history.file_format {
HistoryFileFormat::Sqlite => {
SqliteBackedHistory::with_file(history_path.clone(), None, None)
SqliteBackedHistory::with_file(history_path.clone().into(), None, None)
.map(|inner| {
let boxed: Box<dyn ReedlineHistory> = Box::new(inner);
boxed
@ -77,7 +77,7 @@ impl Command for History {
HistoryFileFormat::PlainText => FileBackedHistory::with_file(
history.max_size as usize,
history_path.clone(),
history_path.clone().into(),
)
.map(|inner| {
let boxed: Box<dyn ReedlineHistory> = Box::new(inner);
@ -156,58 +156,34 @@ fn create_history_record(idx: usize, entry: HistoryItem, long: bool, head: Span)
//2. Create a record of either short or long columns and values
let item_id_value = Value::int(
match entry.id {
Some(id) => {
let ids = id.to_string();
match ids.parse::<i64>() {
Ok(i) => i,
_ => 0i64,
}
}
None => 0i64,
},
entry
.id
.and_then(|id| id.to_string().parse::<i64>().ok())
.unwrap_or_default(),
head,
);
let start_timestamp_value = Value::string(
match entry.start_timestamp {
Some(time) => time.to_string(),
None => "".into(),
},
entry
.start_timestamp
.map(|time| time.to_string())
.unwrap_or_default(),
head,
);
let command_value = Value::string(entry.command_line, head);
let session_id_value = Value::int(
match entry.session_id {
Some(sid) => {
let sids = sid.to_string();
match sids.parse::<i64>() {
Ok(i) => i,
_ => 0i64,
}
}
None => 0i64,
},
head,
);
let hostname_value = Value::string(
match entry.hostname {
Some(host) => host,
None => "".into(),
},
head,
);
let cwd_value = Value::string(
match entry.cwd {
Some(cwd) => cwd,
None => "".into(),
},
entry
.session_id
.and_then(|id| id.to_string().parse::<i64>().ok())
.unwrap_or_default(),
head,
);
let hostname_value = Value::string(entry.hostname.unwrap_or_default(), head);
let cwd_value = Value::string(entry.cwd.unwrap_or_default(), head);
let duration_value = Value::duration(
match entry.duration {
Some(d) => d.as_nanos().try_into().unwrap_or(0),
None => 0,
},
entry
.duration
.and_then(|d| d.as_nanos().try_into().ok())
.unwrap_or(0),
head,
);
let exit_status_value = Value::int(entry.exit_status.unwrap_or(0), head);

View File

@ -61,10 +61,12 @@ impl Command for KeybindingsList {
.map(|option| call.has_flag(engine_state, stack, option))
.collect::<Result<Vec<_>, ShellError>>()?;
let no_option_specified = presence.iter().all(|present| !*present);
let records = all_options
.iter()
.zip(presence)
.filter(|(_, present)| *present)
.filter(|(_, present)| no_option_specified || *present)
.flat_map(|(option, _)| get_records(option, call.head))
.collect();

View File

@ -99,10 +99,9 @@ impl CommandCompletion {
suggestion: Suggestion {
value: String::from_utf8_lossy(&x.0).to_string(),
description: x.1,
style: None,
extra: None,
span: reedline::Span::new(span.start - offset, span.end - offset),
append_whitespace: true,
..Suggestion::default()
},
kind: Some(SuggestionKind::Command(x.2)),
})
@ -118,11 +117,9 @@ impl CommandCompletion {
.map(move |x| SemanticSuggestion {
suggestion: Suggestion {
value: x,
description: None,
style: None,
extra: None,
span: reedline::Span::new(span.start - offset, span.end - offset),
append_whitespace: true,
..Suggestion::default()
},
// TODO: is there a way to create a test?
kind: None,
@ -136,11 +133,9 @@ impl CommandCompletion {
results.push(SemanticSuggestion {
suggestion: Suggestion {
value: format!("^{}", external.suggestion.value),
description: None,
style: None,
extra: None,
span: external.suggestion.span,
append_whitespace: true,
..Suggestion::default()
},
kind: external.kind,
})

View File

@ -443,14 +443,11 @@ pub fn map_value_completions<'a>(
return Some(SemanticSuggestion {
suggestion: Suggestion {
value: s,
description: None,
style: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
append_whitespace: false,
..Suggestion::default()
},
kind: Some(SuggestionKind::Type(x.get_type())),
});
@ -460,14 +457,11 @@ pub fn map_value_completions<'a>(
if let Ok(record) = x.as_record() {
let mut suggestion = Suggestion {
value: String::from(""), // Initialize with empty string
description: None,
style: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
append_whitespace: false,
..Suggestion::default()
};
// Iterate the cols looking for `value` and `description`

View File

@ -10,11 +10,9 @@ use nu_protocol::{
levenshtein_distance, Span,
};
use nu_utils::get_ls_colors;
use std::path::{
is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR,
};
use std::path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP};
use super::SortBy;
use super::{MatchAlgorithm, SortBy};
#[derive(Clone, Default)]
pub struct PathBuiltFromString {
@ -22,12 +20,21 @@ pub struct PathBuiltFromString {
isdir: bool,
}
fn complete_rec(
/// Recursively goes through paths that match a given `partial`.
/// built: State struct for a valid matching path built so far.
///
/// `isdir`: whether the current partial path has a trailing slash.
/// Parsing a path string into a pathbuf loses that bit of information.
///
/// 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.
pub fn complete_rec(
partial: &[&str],
built: &PathBuiltFromString,
cwd: &Path,
options: &CompletionOptions,
dir: bool,
want_directory: bool,
isdir: bool,
) -> Vec<PathBuiltFromString> {
let mut completions = vec![];
@ -37,7 +44,7 @@ fn complete_rec(
let mut built = built.clone();
built.parts.push(base.to_string());
built.isdir = true;
return complete_rec(rest, &built, cwd, options, dir, isdir);
return complete_rec(rest, &built, cwd, options, want_directory, isdir);
}
}
@ -58,7 +65,7 @@ fn complete_rec(
built.parts.push(entry_name.clone());
built.isdir = entry_isdir;
if !dir || entry_isdir {
if !want_directory || entry_isdir {
entries.push((entry_name, built));
}
}
@ -70,12 +77,29 @@ fn complete_rec(
match partial.split_first() {
Some((base, rest)) => {
if matches(base, &entry_name, options) {
// We use `isdir` to confirm that the current component has
// at least one next component or a slash.
// Serves as confirmation to ignore longer completions for
// components in between.
if !rest.is_empty() || isdir {
completions.extend(complete_rec(rest, &built, cwd, options, dir, isdir));
completions.extend(complete_rec(
rest,
&built,
cwd,
options,
want_directory,
isdir,
));
} else {
completions.push(built);
}
}
if entry_name.eq(base)
&& matches!(options.match_algorithm, MatchAlgorithm::Prefix)
&& isdir
{
break;
}
}
None => {
completions.push(built);
@ -93,16 +117,16 @@ enum OriginalCwd {
}
impl OriginalCwd {
fn apply(&self, mut p: PathBuiltFromString) -> String {
fn apply(&self, mut p: PathBuiltFromString, path_separator: char) -> String {
match self {
Self::None => {}
Self::Home => p.parts.insert(0, "~".to_string()),
Self::Prefix(s) => p.parts.insert(0, s.clone()),
};
let mut ret = p.parts.join(MAIN_SEPARATOR_STR);
let mut ret = p.parts.join(&path_separator.to_string());
if p.isdir {
ret.push(SEP);
ret.push(path_separator);
}
ret
}
@ -133,6 +157,14 @@ pub fn complete_item(
) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
let partial = surround_remove(partial);
let isdir = partial.ends_with(is_separator);
#[cfg(unix)]
let path_separator = SEP;
#[cfg(windows)]
let path_separator = partial
.chars()
.rfind(|c: &char| is_separator(*c))
.unwrap_or(SEP);
let cwd_pathbuf = Path::new(cwd).to_path_buf();
let ls_colors = (engine_state.config.use_ls_colors_completions
&& engine_state.config.use_ansi_coloring)
@ -170,7 +202,7 @@ pub fn complete_item(
}
Some(Component::Normal(home)) if home.to_string_lossy() == "~" => {
components.next();
cwd = home_dir().unwrap_or(cwd_pathbuf);
cwd = home_dir().map(Into::into).unwrap_or(cwd_pathbuf);
prefix_len = 1;
original_cwd = OriginalCwd::Home;
}
@ -195,7 +227,7 @@ pub fn complete_item(
)
.into_iter()
.map(|p| {
let path = original_cwd.apply(p);
let path = original_cwd.apply(p, path_separator);
let style = ls_colors.as_ref().map(|lsc| {
lsc.style_for_path_with_metadata(
&path,

View File

@ -48,14 +48,12 @@ impl Completer for DirectoryCompletion {
.map(move |x| SemanticSuggestion {
suggestion: Suggestion {
value: x.1,
description: None,
style: x.2,
extra: None,
span: reedline::Span {
start: x.0.start - offset,
end: x.0.end - offset,
},
append_whitespace: false,
..Suggestion::default()
},
// TODO????
kind: None,

View File

@ -116,14 +116,13 @@ impl Completer for DotNuCompletion {
.map(move |x| SemanticSuggestion {
suggestion: Suggestion {
value: x.1,
description: None,
style: x.2,
extra: None,
span: reedline::Span {
start: x.0.start - offset,
end: x.0.end - offset,
},
append_whitespace: true,
..Suggestion::default()
},
// TODO????
kind: None,

View File

@ -53,14 +53,12 @@ impl Completer for FileCompletion {
.map(move |x| SemanticSuggestion {
suggestion: Suggestion {
value: x.1,
description: None,
style: x.2,
extra: None,
span: reedline::Span {
start: x.0.start - offset,
end: x.0.end - offset,
},
append_whitespace: false,
..Suggestion::default()
},
// TODO????
kind: None,

View File

@ -51,13 +51,12 @@ impl Completer for FlagCompletion {
suggestion: Suggestion {
value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()),
style: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
append_whitespace: true,
..Suggestion::default()
},
// TODO????
kind: None,
@ -78,13 +77,12 @@ impl Completer for FlagCompletion {
suggestion: Suggestion {
value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()),
style: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
append_whitespace: true,
..Suggestion::default()
},
// TODO????
kind: None,

View File

@ -85,11 +85,8 @@ impl Completer for VariableCompletion {
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: env_var.0,
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
..Suggestion::default()
},
kind: Some(SuggestionKind::Type(env_var.1.get_type())),
});
@ -157,11 +154,8 @@ impl Completer for VariableCompletion {
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: builtin.to_string(),
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
..Suggestion::default()
},
// TODO is there a way to get the VarId to get the type???
kind: None,
@ -184,11 +178,8 @@ impl Completer for VariableCompletion {
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(v.0).to_string(),
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
..Suggestion::default()
},
kind: Some(SuggestionKind::Type(
working_set.get_variable(*v.1).ty.clone(),
@ -215,11 +206,8 @@ impl Completer for VariableCompletion {
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(v.0).to_string(),
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
..Suggestion::default()
},
kind: Some(SuggestionKind::Type(
working_set.get_variable(*v.1).ty.clone(),
@ -255,11 +243,8 @@ fn nested_suggestions(
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: col.clone(),
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
..Suggestion::default()
},
kind: Some(kind.clone()),
});
@ -272,11 +257,8 @@ fn nested_suggestions(
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: column_name,
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
..Suggestion::default()
},
kind: Some(kind.clone()),
});

View File

@ -192,7 +192,8 @@ pub fn add_plugin_file(
} else if let Some(mut plugin_path) = nu_path::config_dir() {
// Path to store plugins signatures
plugin_path.push(storage_path);
let mut plugin_path = canonicalize_with(&plugin_path, &cwd).unwrap_or(plugin_path);
let mut plugin_path =
canonicalize_with(&plugin_path, &cwd).unwrap_or(plugin_path.into());
plugin_path.push(PLUGIN_FILE);
let plugin_path = canonicalize_with(&plugin_path, &cwd).unwrap_or(plugin_path);
engine_state.plugin_path = Some(plugin_path);
@ -247,7 +248,7 @@ pub(crate) fn get_history_path(storage_path: &str, mode: HistoryFileFormat) -> O
HistoryFileFormat::PlainText => HISTORY_FILE_TXT,
HistoryFileFormat::Sqlite => HISTORY_FILE_SQLITE,
});
history_path
history_path.into()
})
}

View File

@ -1,4 +1,4 @@
use nu_engine::documentation::get_flags_section;
use nu_engine::documentation::{get_flags_section, HelpStyle};
use nu_protocol::{engine::EngineState, levenshtein_distance, Config};
use nu_utils::IgnoreCaseExt;
use reedline::{Completer, Suggestion};
@ -20,6 +20,9 @@ impl NuHelpCompleter {
fn completion_helper(&self, line: &str, pos: usize) -> Vec<Suggestion> {
let folded_line = line.to_folded_case();
let mut help_style = HelpStyle::default();
help_style.update_from_config(&self.engine_state, &self.config);
let mut commands = self
.engine_state
.get_decls_sorted(false)
@ -60,12 +63,9 @@ impl NuHelpCompleter {
let _ = write!(long_desc, "Usage:\r\n > {}\r\n", sig.call_signature());
if !sig.named.is_empty() {
long_desc.push_str(&get_flags_section(
Some(&self.engine_state),
Some(&self.config),
&sig,
|v| v.to_parsable_string(", ", &self.config),
))
long_desc.push_str(&get_flags_section(&sig, &help_style, |v| {
v.to_parsable_string(", ", &self.config)
}))
}
if !sig.required_positional.is_empty()
@ -110,13 +110,12 @@ impl NuHelpCompleter {
Suggestion {
value: decl.name().into(),
description: Some(long_desc),
style: None,
extra: Some(extra),
span: reedline::Span {
start: pos - line.len(),
end: pos,
},
append_whitespace: false,
..Suggestion::default()
}
})
.collect()

View File

@ -142,10 +142,9 @@ fn convert_to_suggestions(
vec![Suggestion {
value: text,
description,
style: None,
extra,
span,
append_whitespace: false,
..Suggestion::default()
}]
}
Value::List { vals, .. } => vals
@ -154,9 +153,6 @@ fn convert_to_suggestions(
.collect(),
_ => vec![Suggestion {
value: format!("Not a record: {value:?}"),
description: None,
style: None,
extra: None,
span: reedline::Span {
start: if only_buffer_difference {
pos - line.len()
@ -169,7 +165,7 @@ fn convert_to_suggestions(
line.len()
},
},
append_whitespace: false,
..Suggestion::default()
}],
}
}

View File

@ -193,6 +193,29 @@ fn get_style(record: &Record, name: &str, span: Span) -> Option<Style> {
})
}
fn set_menu_style<M: MenuBuilder>(mut menu: M, style: &Value) -> M {
let span = style.span();
let Value::Record { val, .. } = &style else {
return menu;
};
if let Some(style) = get_style(val, "text", span) {
menu = menu.with_text_style(style);
}
if let Some(style) = get_style(val, "selected_text", span) {
menu = menu.with_selected_text_style(style);
}
if let Some(style) = get_style(val, "description_text", span) {
menu = menu.with_description_text_style(style);
}
if let Some(style) = get_style(val, "match_text", span) {
menu = menu.with_match_text_style(style);
}
if let Some(style) = get_style(val, "selected_match_text", span) {
menu = menu.with_selected_match_text_style(style);
}
menu
}
// Adds a columnar menu to the editor engine
pub(crate) fn add_columnar_menu(
line_editor: Reedline,
@ -231,24 +254,7 @@ pub(crate) fn add_columnar_menu(
};
}
let span = menu.style.span();
if let Value::Record { val, .. } = &menu.style {
if let Some(style) = get_style(val, "text", span) {
columnar_menu = columnar_menu.with_text_style(style);
}
if let Some(style) = get_style(val, "selected_text", span) {
columnar_menu = columnar_menu.with_selected_text_style(style);
}
if let Some(style) = get_style(val, "description_text", span) {
columnar_menu = columnar_menu.with_description_text_style(style);
}
if let Some(style) = get_style(val, "match_text", span) {
columnar_menu = columnar_menu.with_match_text_style(style);
}
if let Some(style) = get_style(val, "selected_match_text", span) {
columnar_menu = columnar_menu.with_selected_match_text_style(style);
}
}
columnar_menu = set_menu_style(columnar_menu, &menu.style);
let marker = menu.marker.to_expanded_string("", config);
columnar_menu = columnar_menu.with_marker(&marker);
@ -304,18 +310,7 @@ pub(crate) fn add_list_menu(
};
}
let span = menu.style.span();
if let Value::Record { val, .. } = &menu.style {
if let Some(style) = get_style(val, "text", span) {
list_menu = list_menu.with_text_style(style);
}
if let Some(style) = get_style(val, "selected_text", span) {
list_menu = list_menu.with_selected_text_style(style);
}
if let Some(style) = get_style(val, "description_text", span) {
list_menu = list_menu.with_description_text_style(style);
}
}
list_menu = set_menu_style(list_menu, &menu.style);
let marker = menu.marker.to_expanded_string("", &config);
list_menu = list_menu.with_marker(&marker);
@ -496,24 +491,7 @@ pub(crate) fn add_ide_menu(
};
}
let span = menu.style.span();
if let Value::Record { val, .. } = &menu.style {
if let Some(style) = get_style(val, "text", span) {
ide_menu = ide_menu.with_text_style(style);
}
if let Some(style) = get_style(val, "selected_text", span) {
ide_menu = ide_menu.with_selected_text_style(style);
}
if let Some(style) = get_style(val, "description_text", span) {
ide_menu = ide_menu.with_description_text_style(style);
}
if let Some(style) = get_style(val, "match_text", span) {
ide_menu = ide_menu.with_match_text_style(style);
}
if let Some(style) = get_style(val, "selected_match_text", span) {
ide_menu = ide_menu.with_selected_match_text_style(style);
}
}
ide_menu = set_menu_style(ide_menu, &menu.style);
let marker = menu.marker.to_expanded_string("", &config);
ide_menu = ide_menu.with_marker(&marker);
@ -601,18 +579,7 @@ pub(crate) fn add_description_menu(
};
}
let span = menu.style.span();
if let Value::Record { val, .. } = &menu.style {
if let Some(style) = get_style(val, "text", span) {
description_menu = description_menu.with_text_style(style);
}
if let Some(style) = get_style(val, "selected_text", span) {
description_menu = description_menu.with_selected_text_style(style);
}
if let Some(style) = get_style(val, "description_text", span) {
description_menu = description_menu.with_description_text_style(style);
}
}
description_menu = set_menu_style(description_menu, &menu.style);
let marker = menu.marker.to_expanded_string("", &config);
description_menu = description_menu.with_marker(&marker);

View File

@ -1337,20 +1337,26 @@ fn are_session_ids_in_sync() {
#[cfg(test)]
mod test_auto_cd {
use super::{do_auto_cd, parse_operation, ReplOperation};
use nu_path::AbsolutePath;
use nu_protocol::engine::{EngineState, Stack};
use std::path::Path;
use tempfile::tempdir;
/// Create a symlink. Works on both Unix and Windows.
#[cfg(any(unix, windows))]
fn symlink(original: impl AsRef<Path>, link: impl AsRef<Path>) -> std::io::Result<()> {
fn symlink(
original: impl AsRef<AbsolutePath>,
link: impl AsRef<AbsolutePath>,
) -> std::io::Result<()> {
let original = original.as_ref();
let link = link.as_ref();
#[cfg(unix)]
{
std::os::unix::fs::symlink(original, link)
}
#[cfg(windows)]
{
if original.as_ref().is_dir() {
if original.is_dir() {
std::os::windows::fs::symlink_dir(original, link)
} else {
std::os::windows::fs::symlink_file(original, link)
@ -1362,11 +1368,11 @@ mod test_auto_cd {
/// `before`, and after `input` is parsed and evaluated, PWD should be
/// changed to `after`.
#[track_caller]
fn check(before: impl AsRef<Path>, input: &str, after: impl AsRef<Path>) {
fn check(before: impl AsRef<AbsolutePath>, input: &str, after: impl AsRef<AbsolutePath>) {
// Setup EngineState and Stack.
let mut engine_state = EngineState::new();
let mut stack = Stack::new();
stack.set_cwd(before).unwrap();
stack.set_cwd(before.as_ref()).unwrap();
// Parse the input. It must be an auto-cd operation.
let op = parse_operation(input.to_string(), &engine_state, &stack).unwrap();
@ -1382,54 +1388,66 @@ mod test_auto_cd {
// don't have to be byte-wise equal (on Windows, the 8.3 filename
// conversion messes things up),
let updated_cwd = std::fs::canonicalize(updated_cwd).unwrap();
let after = std::fs::canonicalize(after).unwrap();
let after = std::fs::canonicalize(after.as_ref()).unwrap();
assert_eq!(updated_cwd, after);
}
#[test]
fn auto_cd_root() {
let tempdir = tempdir().unwrap();
let root = if cfg!(windows) { r"C:\" } else { "/" };
check(&tempdir, root, root);
let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
let input = if cfg!(windows) { r"C:\" } else { "/" };
let root = AbsolutePath::try_new(input).unwrap();
check(tempdir, input, root);
}
#[test]
fn auto_cd_tilde() {
let tempdir = tempdir().unwrap();
let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
let home = nu_path::home_dir().unwrap();
check(&tempdir, "~", home);
check(tempdir, "~", home);
}
#[test]
fn auto_cd_dot() {
let tempdir = tempdir().unwrap();
check(&tempdir, ".", &tempdir);
let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
check(tempdir, ".", tempdir);
}
#[test]
fn auto_cd_double_dot() {
let tempdir = tempdir().unwrap();
let dir = tempdir.path().join("foo");
let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
let dir = tempdir.join("foo");
std::fs::create_dir_all(&dir).unwrap();
check(dir, "..", &tempdir);
check(dir, "..", tempdir);
}
#[test]
fn auto_cd_triple_dot() {
let tempdir = tempdir().unwrap();
let dir = tempdir.path().join("foo").join("bar");
let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
let dir = tempdir.join("foo").join("bar");
std::fs::create_dir_all(&dir).unwrap();
check(dir, "...", &tempdir);
check(dir, "...", tempdir);
}
#[test]
fn auto_cd_relative() {
let tempdir = tempdir().unwrap();
let foo = tempdir.path().join("foo");
let bar = tempdir.path().join("bar");
let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
let foo = tempdir.join("foo");
let bar = tempdir.join("bar");
std::fs::create_dir_all(&foo).unwrap();
std::fs::create_dir_all(&bar).unwrap();
let input = if cfg!(windows) { r"..\bar" } else { "../bar" };
check(foo, input, bar);
}
@ -1437,32 +1455,35 @@ mod test_auto_cd {
#[test]
fn auto_cd_trailing_slash() {
let tempdir = tempdir().unwrap();
let dir = tempdir.path().join("foo");
std::fs::create_dir_all(&dir).unwrap();
let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
let dir = tempdir.join("foo");
std::fs::create_dir_all(&dir).unwrap();
let input = if cfg!(windows) { r"foo\" } else { "foo/" };
check(&tempdir, input, dir);
check(tempdir, input, dir);
}
#[test]
fn auto_cd_symlink() {
let tempdir = tempdir().unwrap();
let dir = tempdir.path().join("foo");
std::fs::create_dir_all(&dir).unwrap();
let link = tempdir.path().join("link");
symlink(&dir, &link).unwrap();
let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
let dir = tempdir.join("foo");
std::fs::create_dir_all(&dir).unwrap();
let link = tempdir.join("link");
symlink(&dir, &link).unwrap();
let input = if cfg!(windows) { r".\link" } else { "./link" };
check(&tempdir, input, link);
check(tempdir, input, link);
}
#[test]
#[should_panic(expected = "was not parsed into an auto-cd operation")]
fn auto_cd_nonexistent_directory() {
let tempdir = tempdir().unwrap();
let dir = tempdir.path().join("foo");
let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
let dir = tempdir.join("foo");
let input = if cfg!(windows) { r"foo\" } else { "foo/" };
check(&tempdir, input, dir);
check(tempdir, input, dir);
}
}

View File

@ -321,16 +321,10 @@ mod test {
let env = engine_state.render_env_vars();
assert!(
matches!(env.get(&"FOO".to_string()), Some(&Value::String { val, .. }) if val == "foo")
);
assert!(
matches!(env.get(&"SYMBOLS".to_string()), Some(&Value::String { val, .. }) if val == symbols)
);
assert!(
matches!(env.get(&symbols.to_string()), Some(&Value::String { val, .. }) if val == "symbols")
);
assert!(env.get(&"PWD".to_string()).is_some());
assert!(matches!(env.get("FOO"), Some(&Value::String { val, .. }) if val == "foo"));
assert!(matches!(env.get("SYMBOLS"), Some(&Value::String { val, .. }) if val == symbols));
assert!(matches!(env.get(symbols), Some(&Value::String { val, .. }) if val == "symbols"));
assert!(env.contains_key("PWD"));
assert_eq!(env.len(), 4);
}
}

View File

@ -0,0 +1,7 @@
use nu_test_support::nu;
#[test]
fn not_empty() {
let result = nu!("keybindings list | is-not-empty");
assert_eq!(result.out, "true");
}

View File

@ -1 +1,2 @@
mod keybindings_list;
mod nu_highlight;

View File

@ -32,7 +32,6 @@ fn completer() -> NuCompleter {
fn completer_strings() -> NuCompleter {
// Create a new engine
let (dir, _, mut engine, mut stack) = new_engine();
// Add record value as example
let record = r#"def animals [] { ["cat", "dog", "eel" ] }
def my-command [animal: string@animals] { print $animal }"#;
@ -123,28 +122,28 @@ fn variables_double_dash_argument_with_flagcompletion(mut completer: NuCompleter
let suggestions = completer.complete("tst --", 6);
let expected: Vec<String> = vec!["--help".into(), "--mod".into()];
// dbg!(&expected, &suggestions);
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn variables_single_dash_argument_with_flagcompletion(mut completer: NuCompleter) {
let suggestions = completer.complete("tst -", 5);
let expected: Vec<String> = vec!["--help".into(), "--mod".into(), "-h".into(), "-s".into()];
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn variables_command_with_commandcompletion(mut completer_strings: NuCompleter) {
let suggestions = completer_strings.complete("my-c ", 4);
let expected: Vec<String> = vec!["my-command".into()];
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn variables_subcommands_with_customcompletion(mut completer_strings: NuCompleter) {
let suggestions = completer_strings.complete("my-command ", 11);
let expected: Vec<String> = vec!["cat".into(), "dog".into(), "eel".into()];
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[rstest]
@ -153,7 +152,7 @@ fn variables_customcompletion_subcommands_with_customcompletion_2(
) {
let suggestions = completer_strings.complete("my-command ", 11);
let expected: Vec<String> = vec!["cat".into(), "dog".into(), "eel".into()];
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[test]
@ -182,19 +181,19 @@ fn dotnu_completions() {
let completion_str = "source-env ".to_string();
let suggestions = completer.complete(&completion_str, completion_str.len());
match_suggestions(expected.clone(), suggestions);
match_suggestions(&expected, &suggestions);
// Test use completion
let completion_str = "use ".to_string();
let suggestions = completer.complete(&completion_str, completion_str.len());
match_suggestions(expected.clone(), suggestions);
match_suggestions(&expected, &suggestions);
// Test overlay use completion
let completion_str = "overlay use ".to_string();
let suggestions = completer.complete(&completion_str, completion_str.len());
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[test]
@ -258,8 +257,22 @@ fn file_completions() {
folder(dir.join(".hidden_folder")),
];
#[cfg(windows)]
{
let separator = '/';
let target_dir = format!("cp {dir_str}{separator}");
let slash_suggestions = completer.complete(&target_dir, target_dir.len());
let expected_slash_paths: Vec<String> = expected_paths
.iter()
.map(|s| s.replace('\\', "/"))
.collect();
match_suggestions(&expected_slash_paths, &slash_suggestions);
}
// Match the results
match_suggestions(expected_paths, suggestions);
match_suggestions(&expected_paths, &suggestions);
// Test completions for a file
let target_dir = format!("cp {}", folder(dir.join("another")));
@ -269,17 +282,91 @@ fn file_completions() {
let expected_paths: Vec<String> = vec![file(dir.join("another").join("newfile"))];
// Match the results
match_suggestions(expected_paths, suggestions);
match_suggestions(&expected_paths, &suggestions);
// Test completions for hidden files
let target_dir = format!("ls {}/.", folder(dir.join(".hidden_folder")));
let target_dir = format!("ls {}{MAIN_SEPARATOR}.", folder(dir.join(".hidden_folder")));
let suggestions = completer.complete(&target_dir, target_dir.len());
let expected_paths: Vec<String> =
vec![file(dir.join(".hidden_folder").join(".hidden_subfile"))];
#[cfg(windows)]
{
let target_dir = format!("ls {}/.", folder(dir.join(".hidden_folder")));
let slash_suggestions = completer.complete(&target_dir, target_dir.len());
let expected_slash: Vec<String> = expected_paths
.iter()
.map(|s| s.replace('\\', "/"))
.collect();
match_suggestions(&expected_slash, &slash_suggestions);
}
// Match the results
match_suggestions(expected_paths, suggestions);
match_suggestions(&expected_paths, &suggestions);
}
#[cfg(windows)]
#[test]
fn file_completions_with_mixed_separators() {
// Create a new engine
let (dir, dir_str, engine, stack) = new_dotnu_engine();
// Instantiate a new completer
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
// Create Expected values
let expected_paths: Vec<String> = vec![
file(dir.join("lib-dir1").join("bar.nu")),
file(dir.join("lib-dir1").join("baz.nu")),
file(dir.join("lib-dir1").join("xyzzy.nu")),
];
let expecetd_slash_paths: Vec<String> = expected_paths
.iter()
.map(|s| s.replace(MAIN_SEPARATOR, "/"))
.collect();
let target_dir = format!("ls {dir_str}/lib-dir1/");
let suggestions = completer.complete(&target_dir, target_dir.len());
match_suggestions(&expecetd_slash_paths, &suggestions);
let target_dir = format!("cp {dir_str}\\lib-dir1/");
let suggestions = completer.complete(&target_dir, target_dir.len());
match_suggestions(&expecetd_slash_paths, &suggestions);
let target_dir = format!("ls {dir_str}/lib-dir1\\/");
let suggestions = completer.complete(&target_dir, target_dir.len());
match_suggestions(&expecetd_slash_paths, &suggestions);
let target_dir = format!("ls {dir_str}\\lib-dir1\\/");
let suggestions = completer.complete(&target_dir, target_dir.len());
match_suggestions(&expecetd_slash_paths, &suggestions);
let target_dir = format!("ls {dir_str}\\lib-dir1\\");
let suggestions = completer.complete(&target_dir, target_dir.len());
match_suggestions(&expected_paths, &suggestions);
let target_dir = format!("ls {dir_str}/lib-dir1\\");
let suggestions = completer.complete(&target_dir, target_dir.len());
match_suggestions(&expected_paths, &suggestions);
let target_dir = format!("ls {dir_str}/lib-dir1/\\");
let suggestions = completer.complete(&target_dir, target_dir.len());
match_suggestions(&expected_paths, &suggestions);
let target_dir = format!("ls {dir_str}\\lib-dir1/\\");
let suggestions = completer.complete(&target_dir, target_dir.len());
match_suggestions(&expected_paths, &suggestions);
}
#[test]
@ -303,7 +390,7 @@ fn partial_completions() {
];
// Match the results
match_suggestions(expected_paths, suggestions);
match_suggestions(&expected_paths, &suggestions);
// Test completions for the files whose name begin with "h"
// and are present under directories whose names begin with "pa"
@ -324,7 +411,7 @@ fn partial_completions() {
];
// Match the results
match_suggestions(expected_paths, suggestions);
match_suggestions(&expected_paths, &suggestions);
// Test completion for all files under directories whose names begin with "pa"
let dir_str = folder(dir.join("pa"));
@ -345,7 +432,7 @@ fn partial_completions() {
];
// Match the results
match_suggestions(expected_paths, suggestions);
match_suggestions(&expected_paths, &suggestions);
// Test completion for a single file
let dir_str = file(dir.join("fi").join("so"));
@ -356,7 +443,7 @@ fn partial_completions() {
let expected_paths: Vec<String> = vec![file(dir.join("final_partial").join("somefile"))];
// Match the results
match_suggestions(expected_paths, suggestions);
match_suggestions(&expected_paths, &suggestions);
// Test completion where there is a sneaky `..` in the path
let dir_str = file(dir.join("par").join("..").join("fi").join("so"));
@ -392,7 +479,7 @@ fn partial_completions() {
];
// Match the results
match_suggestions(expected_paths, suggestions);
match_suggestions(&expected_paths, &suggestions);
// Test completion for all files under directories whose names begin with "pa"
let file_str = file(dir.join("partial-a").join("have"));
@ -406,7 +493,7 @@ fn partial_completions() {
];
// Match the results
match_suggestions(expected_paths, suggestions);
match_suggestions(&expected_paths, &suggestions);
// Test completion for all files under directories whose names begin with "pa"
let file_str = file(dir.join("partial-a").join("have_ext."));
@ -420,7 +507,7 @@ fn partial_completions() {
];
// Match the results
match_suggestions(expected_paths, suggestions);
match_suggestions(&expected_paths, &suggestions);
}
#[test]
@ -455,15 +542,16 @@ fn command_ls_with_filecompletion() {
".hidden_folder/".to_string(),
];
match_suggestions(expected_paths, suggestions);
match_suggestions(&expected_paths, &suggestions);
let target_dir = "ls custom_completion.";
let suggestions = completer.complete(target_dir, target_dir.len());
let expected_paths: Vec<String> = vec!["custom_completion.nu".to_string()];
match_suggestions(expected_paths, suggestions)
match_suggestions(&expected_paths, &suggestions);
}
#[test]
fn command_open_with_filecompletion() {
let (_, _, engine, stack) = new_engine();
@ -496,14 +584,14 @@ fn command_open_with_filecompletion() {
".hidden_folder/".to_string(),
];
match_suggestions(expected_paths, suggestions);
match_suggestions(&expected_paths, &suggestions);
let target_dir = "open custom_completion.";
let suggestions = completer.complete(target_dir, target_dir.len());
let expected_paths: Vec<String> = vec!["custom_completion.nu".to_string()];
match_suggestions(expected_paths, suggestions)
match_suggestions(&expected_paths, &suggestions);
}
#[test]
@ -538,7 +626,7 @@ fn command_rm_with_globcompletion() {
".hidden_folder/".to_string(),
];
match_suggestions(expected_paths, suggestions)
match_suggestions(&expected_paths, &suggestions)
}
#[test]
@ -573,7 +661,7 @@ fn command_cp_with_globcompletion() {
".hidden_folder/".to_string(),
];
match_suggestions(expected_paths, suggestions)
match_suggestions(&expected_paths, &suggestions)
}
#[test]
@ -608,7 +696,7 @@ fn command_save_with_filecompletion() {
".hidden_folder/".to_string(),
];
match_suggestions(expected_paths, suggestions)
match_suggestions(&expected_paths, &suggestions)
}
#[test]
@ -643,7 +731,7 @@ fn command_touch_with_filecompletion() {
".hidden_folder/".to_string(),
];
match_suggestions(expected_paths, suggestions)
match_suggestions(&expected_paths, &suggestions)
}
#[test]
@ -678,7 +766,7 @@ fn command_watch_with_filecompletion() {
".hidden_folder/".to_string(),
];
match_suggestions(expected_paths, suggestions)
match_suggestions(&expected_paths, &suggestions)
}
#[rstest]
@ -686,19 +774,19 @@ fn subcommand_completions(mut subcommand_completer: NuCompleter) {
let prefix = "foo br";
let suggestions = subcommand_completer.complete(prefix, prefix.len());
match_suggestions(
vec!["foo bar".to_string(), "foo aabrr".to_string()],
suggestions,
&vec!["foo bar".to_string(), "foo aabrr".to_string()],
&suggestions,
);
let prefix = "foo b";
let suggestions = subcommand_completer.complete(prefix, prefix.len());
match_suggestions(
vec![
&vec![
"foo bar".to_string(),
"foo abaz".to_string(),
"foo aabrr".to_string(),
],
suggestions,
&suggestions,
);
}
@ -724,7 +812,7 @@ fn file_completion_quoted() {
format!("`{}`", folder("test dir")),
];
match_suggestions(expected_paths, suggestions);
match_suggestions(&expected_paths, &suggestions);
let dir: PathBuf = "test dir".into();
let target_dir = format!("open '{}'", folder(dir.clone()));
@ -735,7 +823,7 @@ fn file_completion_quoted() {
format!("`{}`", file(dir.join("single quote"))),
];
match_suggestions(expected_paths, suggestions)
match_suggestions(&expected_paths, &suggestions)
}
#[test]
@ -770,7 +858,7 @@ fn flag_completions() {
];
// Match results
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[test]
@ -794,8 +882,21 @@ fn folder_with_directorycompletions() {
folder(dir.join(".hidden_folder")),
];
#[cfg(windows)]
{
let target_dir = format!("cd {dir_str}/");
let slash_suggestions = completer.complete(&target_dir, target_dir.len());
let expected_slash_paths: Vec<String> = expected_paths
.iter()
.map(|s| s.replace('\\', "/"))
.collect();
match_suggestions(&expected_slash_paths, &slash_suggestions);
}
// Match the results
match_suggestions(expected_paths, suggestions);
match_suggestions(&expected_paths, &suggestions);
}
#[test]
@ -837,7 +938,7 @@ fn variables_completions() {
];
// Match results
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
// Test completions for $nu.h (filter)
let suggestions = completer.complete("$nu.h", 5);
@ -851,7 +952,7 @@ fn variables_completions() {
];
// Match results
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
// Test completions for $nu.os-info
let suggestions = completer.complete("$nu.os-info.", 12);
@ -863,7 +964,7 @@ fn variables_completions() {
"name".into(),
];
// Match results
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
// Test completions for custom var
let suggestions = completer.complete("$actor.", 7);
@ -873,7 +974,7 @@ fn variables_completions() {
let expected: Vec<String> = vec!["age".into(), "name".into()];
// Match results
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
// Test completions for custom var (filtering)
let suggestions = completer.complete("$actor.n", 8);
@ -883,7 +984,7 @@ fn variables_completions() {
let expected: Vec<String> = vec!["name".into()];
// Match results
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
// Test completions for $env
let suggestions = completer.complete("$env.", 5);
@ -896,7 +997,7 @@ fn variables_completions() {
let expected: Vec<String> = vec!["PATH".into(), "PWD".into(), "TEST".into()];
// Match results
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
// Test completions for $env
let suggestions = completer.complete("$env.T", 6);
@ -906,12 +1007,12 @@ fn variables_completions() {
let expected: Vec<String> = vec!["TEST".into()];
// Match results
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
let suggestions = completer.complete("$", 1);
let expected: Vec<String> = vec!["$actor".into(), "$env".into(), "$in".into(), "$nu".into()];
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[test]
@ -930,7 +1031,7 @@ fn alias_of_command_and_flags() {
#[cfg(not(windows))]
let expected_paths: Vec<String> = vec!["test_a/".to_string(), "test_b/".to_string()];
match_suggestions(expected_paths, suggestions)
match_suggestions(&expected_paths, &suggestions)
}
#[test]
@ -949,7 +1050,7 @@ fn alias_of_basic_command() {
#[cfg(not(windows))]
let expected_paths: Vec<String> = vec!["test_a/".to_string(), "test_b/".to_string()];
match_suggestions(expected_paths, suggestions)
match_suggestions(&expected_paths, &suggestions)
}
#[test]
@ -971,7 +1072,7 @@ fn alias_of_another_alias() {
#[cfg(not(windows))]
let expected_paths: Vec<String> = vec!["test_a/".to_string(), "test_b/".to_string()];
match_suggestions(expected_paths, suggestions)
match_suggestions(&expected_paths, &suggestions)
}
fn run_external_completion(completer: &str, input: &str) -> Vec<Suggestion> {
@ -1034,35 +1135,35 @@ fn unknown_command_completion() {
".hidden_folder/".to_string(),
];
match_suggestions(expected_paths, suggestions)
match_suggestions(&expected_paths, &suggestions)
}
#[rstest]
fn flagcompletion_triggers_after_cursor(mut completer: NuCompleter) {
let suggestions = completer.complete("tst -h", 5);
let expected: Vec<String> = vec!["--help".into(), "--mod".into(), "-h".into(), "-s".into()];
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn customcompletion_triggers_after_cursor(mut completer_strings: NuCompleter) {
let suggestions = completer_strings.complete("my-command c", 11);
let expected: Vec<String> = vec!["cat".into(), "dog".into(), "eel".into()];
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn customcompletion_triggers_after_cursor_piped(mut completer_strings: NuCompleter) {
let suggestions = completer_strings.complete("my-command c | ls", 11);
let expected: Vec<String> = vec!["cat".into(), "dog".into(), "eel".into()];
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn flagcompletion_triggers_after_cursor_piped(mut completer: NuCompleter) {
let suggestions = completer.complete("tst -h | ls", 5);
let expected: Vec<String> = vec!["--help".into(), "--mod".into(), "-h".into(), "-s".into()];
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[test]
@ -1096,77 +1197,77 @@ fn filecompletions_triggers_after_cursor() {
".hidden_folder/".to_string(),
];
match_suggestions(expected_paths, suggestions);
match_suggestions(&expected_paths, &suggestions);
}
#[rstest]
fn extern_custom_completion_positional(mut extern_completer: NuCompleter) {
let suggestions = extern_completer.complete("spam ", 5);
let expected: Vec<String> = vec!["cat".into(), "dog".into(), "eel".into()];
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn extern_custom_completion_long_flag_1(mut extern_completer: NuCompleter) {
let suggestions = extern_completer.complete("spam --foo=", 11);
let expected: Vec<String> = vec!["cat".into(), "dog".into(), "eel".into()];
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn extern_custom_completion_long_flag_2(mut extern_completer: NuCompleter) {
let suggestions = extern_completer.complete("spam --foo ", 11);
let expected: Vec<String> = vec!["cat".into(), "dog".into(), "eel".into()];
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn extern_custom_completion_long_flag_short(mut extern_completer: NuCompleter) {
let suggestions = extern_completer.complete("spam -f ", 8);
let expected: Vec<String> = vec!["cat".into(), "dog".into(), "eel".into()];
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn extern_custom_completion_short_flag(mut extern_completer: NuCompleter) {
let suggestions = extern_completer.complete("spam -b ", 8);
let expected: Vec<String> = vec!["cat".into(), "dog".into(), "eel".into()];
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn extern_complete_flags(mut extern_completer: NuCompleter) {
let suggestions = extern_completer.complete("spam -", 6);
let expected: Vec<String> = vec!["--foo".into(), "-b".into(), "-f".into()];
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn custom_completer_triggers_cursor_before_word(mut custom_completer: NuCompleter) {
let suggestions = custom_completer.complete("cmd foo bar", 8);
let expected: Vec<String> = vec!["cmd".into(), "foo".into(), "".into()];
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn custom_completer_triggers_cursor_on_word_left_boundary(mut custom_completer: NuCompleter) {
let suggestions = custom_completer.complete("cmd foo bar", 8);
let expected: Vec<String> = vec!["cmd".into(), "foo".into(), "".into()];
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn custom_completer_triggers_cursor_next_to_word(mut custom_completer: NuCompleter) {
let suggestions = custom_completer.complete("cmd foo bar", 11);
let expected: Vec<String> = vec!["cmd".into(), "foo".into(), "bar".into()];
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn custom_completer_triggers_cursor_after_word(mut custom_completer: NuCompleter) {
let suggestions = custom_completer.complete("cmd foo bar ", 12);
let expected: Vec<String> = vec!["cmd".into(), "foo".into(), "bar".into(), "".into()];
match_suggestions(expected, suggestions);
match_suggestions(&expected, &suggestions);
}
#[ignore = "was reverted, still needs fixing"]

View File

@ -186,7 +186,7 @@ pub fn new_partial_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
}
// match a list of suggestions with the expected values
pub fn match_suggestions(expected: Vec<String>, suggestions: Vec<Suggestion>) {
pub fn match_suggestions(expected: &Vec<String>, suggestions: &Vec<Suggestion>) {
let expected_len = expected.len();
let suggestions_len = suggestions.len();
if expected_len != suggestions_len {
@ -196,13 +196,13 @@ pub fn match_suggestions(expected: Vec<String>, suggestions: Vec<Suggestion>) {
Expected: {expected:#?}\n"
)
}
assert_eq!(
expected,
suggestions
.into_iter()
.map(|it| it.value)
.collect::<Vec<_>>()
);
let suggestoins_str = suggestions
.iter()
.map(|it| it.value.clone())
.collect::<Vec<_>>();
assert_eq!(expected, &suggestoins_str);
}
// append the separator to the converted path

View File

@ -5,15 +5,15 @@ edition = "2021"
license = "MIT"
name = "nu-cmd-base"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-base"
version = "0.95.1"
version = "0.96.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-parser = { path = "../nu-parser", version = "0.95.1" }
nu-path = { path = "../nu-path", version = "0.95.1" }
nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
nu-engine = { path = "../nu-engine", version = "0.96.2" }
nu-parser = { path = "../nu-parser", version = "0.96.2" }
nu-path = { path = "../nu-path", version = "0.96.2" }
nu-protocol = { path = "../nu-protocol", version = "0.96.2" }
indexmap = { workspace = true }
miette = { workspace = true }

View File

@ -3,21 +3,26 @@ use nu_protocol::{
engine::{EngineState, Stack},
Range, ShellError, Span, Value,
};
use std::{ops::Bound, path::PathBuf};
use std::ops::Bound;
pub fn get_init_cwd() -> PathBuf {
std::env::current_dir().unwrap_or_else(|_| {
std::env::var("PWD")
.map(Into::into)
.unwrap_or_else(|_| nu_path::home_dir().unwrap_or_default())
})
pub fn get_init_cwd() -> AbsolutePathBuf {
std::env::current_dir()
.ok()
.and_then(|path| AbsolutePathBuf::try_from(path).ok())
.or_else(|| {
std::env::var("PWD")
.ok()
.and_then(|path| AbsolutePathBuf::try_from(path).ok())
})
.or_else(nu_path::home_dir)
.expect("Failed to get current working directory")
}
pub fn get_guaranteed_cwd(engine_state: &EngineState, stack: &Stack) -> PathBuf {
pub fn get_guaranteed_cwd(engine_state: &EngineState, stack: &Stack) -> AbsolutePathBuf {
engine_state
.cwd(Some(stack))
.map(AbsolutePathBuf::into_std_path_buf)
.unwrap_or(crate::util::get_init_cwd())
.ok()
.unwrap_or_else(get_init_cwd)
}
type MakeRangeError = fn(&str, Span) -> ShellError;

View File

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

View File

@ -6,22 +6,22 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-lang"
edition = "2021"
license = "MIT"
name = "nu-cmd-lang"
version = "0.95.1"
version = "0.96.2"
[lib]
bench = false
[dependencies]
nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-parser = { path = "../nu-parser", version = "0.95.1" }
nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
nu-utils = { path = "../nu-utils", version = "0.95.1" }
nu-engine = { path = "../nu-engine", version = "0.96.2" }
nu-parser = { path = "../nu-parser", version = "0.96.2" }
nu-protocol = { path = "../nu-protocol", version = "0.96.2" }
nu-utils = { path = "../nu-utils", version = "0.96.2" }
itertools = { workspace = true }
shadow-rs = { version = "0.29", default-features = false }
shadow-rs = { version = "0.30", default-features = false }
[build-dependencies]
shadow-rs = { version = "0.29", default-features = false }
shadow-rs = { version = "0.30", default-features = false }
[features]
mimalloc = []

View File

@ -72,7 +72,7 @@ pub fn check_example_input_and_output_types_match_command_signature(
witnessed_type_transformations
}
fn eval_pipeline_without_terminal_expression(
pub fn eval_pipeline_without_terminal_expression(
src: &str,
cwd: &std::path::Path,
engine_state: &mut Box<EngineState>,

View File

@ -5,15 +5,15 @@ edition = "2021"
license = "MIT"
name = "nu-cmd-plugin"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-plugin"
version = "0.95.1"
version = "0.96.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-path = { path = "../nu-path", version = "0.95.1" }
nu-protocol = { path = "../nu-protocol", version = "0.95.1", features = ["plugin"] }
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.95.1" }
nu-engine = { path = "../nu-engine", version = "0.96.2" }
nu-path = { path = "../nu-path", version = "0.96.2" }
nu-protocol = { path = "../nu-protocol", version = "0.96.2", features = ["plugin"] }
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.96.2" }
itertools = { workspace = true }

View File

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

View File

@ -5,7 +5,7 @@ edition = "2021"
license = "MIT"
name = "nu-command"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-command"
version = "0.95.1"
version = "0.96.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -13,21 +13,21 @@ version = "0.95.1"
bench = false
[dependencies]
nu-cmd-base = { path = "../nu-cmd-base", version = "0.95.1" }
nu-color-config = { path = "../nu-color-config", version = "0.95.1" }
nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-glob = { path = "../nu-glob", version = "0.95.1" }
nu-json = { path = "../nu-json", version = "0.95.1" }
nu-parser = { path = "../nu-parser", version = "0.95.1" }
nu-path = { path = "../nu-path", version = "0.95.1" }
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.95.1" }
nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
nu-system = { path = "../nu-system", version = "0.95.1" }
nu-table = { path = "../nu-table", version = "0.95.1" }
nu-term-grid = { path = "../nu-term-grid", version = "0.95.1" }
nu-utils = { path = "../nu-utils", version = "0.95.1" }
nu-cmd-base = { path = "../nu-cmd-base", version = "0.96.2" }
nu-color-config = { path = "../nu-color-config", version = "0.96.2" }
nu-engine = { path = "../nu-engine", version = "0.96.2" }
nu-glob = { path = "../nu-glob", version = "0.96.2" }
nu-json = { path = "../nu-json", version = "0.96.2" }
nu-parser = { path = "../nu-parser", version = "0.96.2" }
nu-path = { path = "../nu-path", version = "0.96.2" }
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.96.2" }
nu-protocol = { path = "../nu-protocol", version = "0.96.2" }
nu-system = { path = "../nu-system", version = "0.96.2" }
nu-table = { path = "../nu-table", version = "0.96.2" }
nu-term-grid = { path = "../nu-term-grid", version = "0.96.2" }
nu-utils = { path = "../nu-utils", version = "0.96.2" }
nu-ansi-term = { workspace = true }
nuon = { path = "../nuon", version = "0.95.1" }
nuon = { path = "../nuon", version = "0.96.2" }
alphanumeric-sort = { workspace = true }
base64 = { workspace = true }
@ -137,8 +137,8 @@ sqlite = ["rusqlite"]
trash-support = ["trash"]
[dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.95.1" }
nu-test-support = { path = "../nu-test-support", version = "0.95.1" }
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.96.2" }
nu-test-support = { path = "../nu-test-support", version = "0.96.2" }
dirs = { workspace = true }
mockito = { workspace = true, default-features = false }
@ -146,4 +146,4 @@ quickcheck = { workspace = true }
quickcheck_macros = { workspace = true }
rstest = { workspace = true, default-features = false }
pretty_assertions = { workspace = true }
tempfile = { workspace = true }
tempfile = { workspace = true }

View File

@ -177,11 +177,9 @@ fn run_histogram(
match v {
// parse record, and fill valid value to actual input.
Value::Record { val, .. } => {
for (c, v) in val.iter() {
if c == col_name {
if let Ok(v) = HashableValue::from_value(v.clone(), head_span) {
inputs.push(v);
}
if let Some(v) = val.get(col_name) {
if let Ok(v) = HashableValue::from_value(v.clone(), head_span) {
inputs.push(v);
}
}
}

View File

@ -150,13 +150,9 @@ fn fill(
FillAlignment::Left
};
let width = if let Some(arg) = width_arg { arg } else { 1 };
let width = width_arg.unwrap_or(1);
let character = if let Some(arg) = character_arg {
arg
} else {
" ".to_string()
};
let character = character_arg.unwrap_or_else(|| " ".to_string());
let arg = Arguments {
width,

View File

@ -424,11 +424,7 @@ pub fn value_to_sql(value: Value) -> Result<Box<dyn rusqlite::ToSql>, ShellError
Value::Filesize { val, .. } => Box::new(val),
Value::Duration { val, .. } => Box::new(val),
Value::Date { val, .. } => Box::new(val),
Value::String { val, .. } => {
// don't store ansi escape sequences in the database
// escape single quotes
Box::new(nu_utils::strip_ansi_unlikely(&val).into_owned())
}
Value::String { val, .. } => Box::new(val),
Value::Binary { val, .. } => Box::new(val),
Value::Nothing { .. } => Box::new(rusqlite::types::Null),
val => {

View File

@ -397,6 +397,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
RandomFloat,
RandomInt,
RandomUuid,
RandomBinary
};
// Generators

View File

@ -1,6 +1,5 @@
use nu_cmd_base::util::get_init_cwd;
use nu_engine::command_prelude::*;
use nu_path::AbsolutePathBuf;
use nu_utils::filesystem::{have_permission, PermissionResult};
#[derive(Clone)]
@ -46,8 +45,8 @@ impl Command for Cd {
// user can use `cd` to recover PWD to a good state.
let cwd = engine_state
.cwd(Some(stack))
.map(AbsolutePathBuf::into_std_path_buf)
.unwrap_or(get_init_cwd());
.ok()
.unwrap_or_else(get_init_cwd);
let path_val = {
if let Some(path) = path_val {
@ -66,7 +65,7 @@ impl Command for Cd {
if let Some(oldpwd) = stack.get_env_var(engine_state, "OLDPWD") {
oldpwd.to_path()?
} else {
cwd
cwd.into()
}
} else {
// Trim whitespace from the end of path.
@ -135,7 +134,7 @@ impl Command for Cd {
result: None,
},
Example {
description: "Change to the previous working directory ($OLDPWD)",
description: r#"Change to the previous working directory (same as "cd $env.OLDPWD")"#,
example: r#"cd -"#,
result: None,
},
@ -144,6 +143,16 @@ impl Command for Cd {
example: r#"def --env gohome [] { cd ~ }"#,
result: None,
},
Example {
description: "Move two directories up in the tree (the parent directory's parent). Additional dots can be added for additional levels.",
example: r#"cd ..."#,
result: None,
},
Example {
description: "The cd command itself is often optional. Simply entering a path to a directory will cd to it.",
example: r#"/home"#,
result: None,
},
]
}
}

View File

@ -135,12 +135,9 @@ fn rm(
let home: Option<String> = nu_path::home_dir().map(|path| {
{
if path.exists() {
match nu_path::canonicalize_with(&path, &currentdir_path) {
Ok(canon_path) => canon_path,
Err(_) => path,
}
nu_path::canonicalize_with(&path, &currentdir_path).unwrap_or(path.into())
} else {
path
path.into()
}
}
.to_string_lossy()

View File

@ -121,9 +121,11 @@ impl Command for Save {
} else {
match stderr {
ChildPipe::Pipe(mut pipe) => {
io::copy(&mut pipe, &mut io::sink())
io::copy(&mut pipe, &mut io::stderr())
}
ChildPipe::Tee(mut tee) => {
io::copy(&mut tee, &mut io::stderr())
}
ChildPipe::Tee(mut tee) => io::copy(&mut tee, &mut io::sink()),
}
.err_span(span)?;
}

View File

@ -86,17 +86,22 @@ impl Command for UCp {
},
Example {
description: "Copy only if source file is newer than target file",
example: "cp -u a b",
example: "cp -u myfile newfile",
result: None,
},
Example {
description: "Copy file preserving mode and timestamps attributes",
example: "cp --preserve [ mode timestamps ] a b",
example: "cp --preserve [ mode timestamps ] myfile newfile",
result: None,
},
Example {
description: "Copy file erasing all attributes",
example: "cp --preserve [] a b",
example: "cp --preserve [] myfile newfile",
result: None,
},
Example {
description: "Copy file to a directory three levels above its current location",
example: "cp myfile ....",
result: None,
},
]
@ -235,7 +240,7 @@ impl Command for UCp {
for (sources, need_expand_tilde) in sources.iter_mut() {
for src in sources.iter_mut() {
if !src.is_absolute() {
*src = nu_path::expand_path_with(&src, &cwd, *need_expand_tilde);
*src = nu_path::expand_path_with(&*src, &cwd, *need_expand_tilde);
}
}
}

View File

@ -30,11 +30,21 @@ impl Command for UMv {
example: "mv test.txt my/subdirectory",
result: None,
},
Example {
description: "Move only if source file is newer than target file",
example: "mv -u new/test.txt old/",
result: None,
},
Example {
description: "Move many files into a directory",
example: "mv *.txt my/subdirectory",
result: None,
},
Example {
description: r#"Move a file into the "my" directory two levels up in the directory tree"#,
example: "mv test.txt .../my/",
result: None,
},
]
}
@ -49,6 +59,11 @@ impl Command for UMv {
.switch("verbose", "explain what is being done.", Some('v'))
.switch("progress", "display a progress bar", Some('p'))
.switch("interactive", "prompt before overwriting", Some('i'))
.switch(
"update",
"move and overwrite only when the SOURCE file is newer than the destination file or when the destination file is missing",
Some('u')
)
.switch("no-clobber", "do not overwrite an existing file", Some('n'))
.rest(
"paths",
@ -77,6 +92,11 @@ impl Command for UMv {
} else {
uu_mv::OverwriteMode::Force
};
let update = if call.has_flag(engine_state, stack, "update")? {
UpdateMode::ReplaceIfOlder
} else {
UpdateMode::ReplaceAll
};
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?;
@ -141,7 +161,7 @@ impl Command for UMv {
for (files, need_expand_tilde) in files.iter_mut() {
for src in files.iter_mut() {
if !src.is_absolute() {
*src = nu_path::expand_path_with(&src, &cwd, *need_expand_tilde);
*src = nu_path::expand_path_with(&*src, &cwd, *need_expand_tilde);
}
}
}
@ -164,7 +184,7 @@ impl Command for UMv {
verbose,
suffix: String::from("~"),
backup: BackupMode::NoBackup,
update: UpdateMode::ReplaceAll,
update,
target_dir: None,
no_target_dir: false,
strip_slashes: false,

View File

@ -27,7 +27,7 @@ impl Command for Default {
}
fn usage(&self) -> &str {
"Sets a default row's column if missing."
"Sets a default value if a row's column is missing or null."
}
fn run(
@ -66,6 +66,20 @@ impl Command for Default {
Span::test_data(),
)),
},
Example {
description: r#"Replace the missing value in the "a" column of a list"#,
example: "[{a:1 b:2} {b:1}] | default 'N/A' a",
result: Some(Value::test_list(vec![
Value::test_record(record! {
"a" => Value::test_int(1),
"b" => Value::test_int(2),
}),
Value::test_record(record! {
"a" => Value::test_string("N/A"),
"b" => Value::test_int(1),
}),
])),
},
]
}
}
@ -88,19 +102,13 @@ fn default(
val: ref mut record,
..
} => {
let mut found = false;
for (col, val) in record.to_mut().iter_mut() {
if *col == column.item {
found = true;
if matches!(val, Value::Nothing { .. }) {
*val = value.clone();
}
let record = record.to_mut();
if let Some(val) = record.get_mut(&column.item) {
if matches!(val, Value::Nothing { .. }) {
*val = value.clone();
}
}
if !found {
record.to_mut().push(column.item.clone(), value.clone());
} else {
record.push(column.item.clone(), value.clone());
}
item

View File

@ -120,8 +120,7 @@ impl Command for Reduce {
engine_state.signals().check(head)?;
acc = closure
.add_arg(value)
.add_arg(acc)
.run_with_input(PipelineData::Empty)?
.run_with_value(acc)?
.into_value(head)?;
}

View File

@ -1,7 +1,7 @@
use std::io::{BufRead, Cursor};
use nu_engine::command_prelude::*;
use nu_protocol::{ListStream, PipelineMetadata, Signals};
use nu_protocol::{ListStream, Signals};
#[derive(Clone)]
pub struct FromJson;
@ -83,7 +83,7 @@ impl Command for FromJson {
strict,
engine_state.signals().clone(),
),
update_metadata(metadata),
metadata,
))
}
PipelineData::ByteStream(stream, metadata)
@ -92,7 +92,7 @@ impl Command for FromJson {
if let Some(reader) = stream.reader() {
Ok(PipelineData::ListStream(
read_json_lines(reader, span, strict, Signals::empty()),
update_metadata(metadata),
metadata,
))
} else {
Ok(PipelineData::Empty)
@ -115,10 +115,10 @@ impl Command for FromJson {
if strict {
Ok(convert_string_to_value_strict(&string_input, span)?
.into_pipeline_data_with_metadata(update_metadata(metadata)))
.into_pipeline_data_with_metadata(metadata))
} else {
Ok(convert_string_to_value(&string_input, span)?
.into_pipeline_data_with_metadata(update_metadata(metadata)))
.into_pipeline_data_with_metadata(metadata))
}
}
}
@ -265,14 +265,6 @@ fn convert_string_to_value_strict(string_input: &str, span: Span) -> Result<Valu
}
}
fn update_metadata(metadata: Option<PipelineMetadata>) -> Option<PipelineMetadata> {
metadata
.map(|md| md.with_content_type(Some("application/json".into())))
.or_else(|| {
Some(PipelineMetadata::default().with_content_type(Some("application/json".into())))
})
}
#[cfg(test)]
mod test {
use super::*;

View File

@ -4,6 +4,8 @@ use crate::formats::to::delimited::to_delimited_data;
use nu_engine::command_prelude::*;
use nu_protocol::Config;
use super::delimited::ToDelimitedDataArgs;
#[derive(Clone)]
pub struct ToCsv;
@ -116,17 +118,62 @@ fn to_csv(
},
};
to_delimited_data(noheaders, sep, columns, "CSV", input, head, config)
to_delimited_data(
ToDelimitedDataArgs {
noheaders,
separator: sep,
columns,
format_name: "CSV",
input,
head,
content_type: Some(mime::TEXT_CSV.to_string()),
},
config,
)
}
#[cfg(test)]
mod test {
use nu_cmd_lang::eval_pipeline_without_terminal_expression;
use crate::Metadata;
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(ToCsv {})
}
#[test]
fn test_content_type_metadata() {
let mut engine_state = Box::new(EngineState::new());
let delta = {
// Base functions that are needed for testing
// Try to keep this working set small to keep tests running as fast as possible
let mut working_set = StateWorkingSet::new(&engine_state);
working_set.add_decl(Box::new(ToCsv {}));
working_set.add_decl(Box::new(Metadata {}));
working_set.render()
};
engine_state
.merge_delta(delta)
.expect("Error merging delta");
let cmd = "{a: 1 b: 2} | to csv | metadata | get content_type";
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),
&mut engine_state,
);
assert_eq!(
Value::test_record(record!("content_type" => Value::test_string("text/csv"))),
result.expect("There should be a result")
);
}
}

View File

@ -69,18 +69,36 @@ fn make_cant_convert_error(value: &Value, format_name: &'static str) -> ShellErr
}
}
pub struct ToDelimitedDataArgs {
pub noheaders: bool,
pub separator: Spanned<char>,
pub columns: Option<Vec<String>>,
pub format_name: &'static str,
pub input: PipelineData,
pub head: Span,
pub content_type: Option<String>,
}
pub fn to_delimited_data(
noheaders: bool,
separator: Spanned<char>,
columns: Option<Vec<String>>,
format_name: &'static str,
input: PipelineData,
head: Span,
ToDelimitedDataArgs {
noheaders,
separator,
columns,
format_name,
input,
head,
content_type,
}: ToDelimitedDataArgs,
config: Arc<Config>,
) -> Result<PipelineData, ShellError> {
let mut input = input;
let span = input.span().unwrap_or(head);
let metadata = input.metadata();
let metadata = Some(
input
.metadata()
.unwrap_or_default()
.with_content_type(content_type),
);
let separator = u8::try_from(separator.item).map_err(|_| ShellError::IncorrectValue {
msg: "separator must be an ASCII character".into(),

View File

@ -64,7 +64,7 @@ impl Command for ToJson {
let res = Value::string(serde_json_string, span);
let metadata = PipelineMetadata {
data_source: nu_protocol::DataSource::None,
content_type: Some("application/json".to_string()),
content_type: Some(mime::APPLICATION_JSON.to_string()),
};
Ok(PipelineData::Value(res, Some(metadata)))
}
@ -159,6 +159,10 @@ fn json_list(input: &[Value]) -> Result<Vec<nu_json::Value>, ShellError> {
#[cfg(test)]
mod test {
use nu_cmd_lang::eval_pipeline_without_terminal_expression;
use crate::Metadata;
use super::*;
#[test]
@ -167,4 +171,34 @@ mod test {
test_examples(ToJson {})
}
#[test]
fn test_content_type_metadata() {
let mut engine_state = Box::new(EngineState::new());
let delta = {
// Base functions that are needed for testing
// Try to keep this working set small to keep tests running as fast as possible
let mut working_set = StateWorkingSet::new(&engine_state);
working_set.add_decl(Box::new(ToJson {}));
working_set.add_decl(Box::new(Metadata {}));
working_set.render()
};
engine_state
.merge_delta(delta)
.expect("Error merging delta");
let cmd = "{a: 1 b: 2} | to json | metadata | get content_type";
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),
&mut engine_state,
);
assert_eq!(
Value::test_record(record!("content_type" => Value::test_string("application/json"))),
result.expect("There should be a result")
);
}
}

View File

@ -82,6 +82,12 @@ fn to_md(
config: &Config,
head: Span,
) -> Result<PipelineData, ShellError> {
// text/markdown became a valid mimetype with rfc7763
let metadata = input
.metadata()
.unwrap_or_default()
.with_content_type(Some("text/markdown".into()));
let (grouped_input, single_list) = group_by(input, head, config);
if per_element || single_list {
return Ok(Value::string(
@ -95,9 +101,10 @@ fn to_md(
.join(""),
head,
)
.into_pipeline_data());
.into_pipeline_data_with_metadata(Some(metadata)));
}
Ok(Value::string(table(grouped_input, pretty, config), head).into_pipeline_data())
Ok(Value::string(table(grouped_input, pretty, config), head)
.into_pipeline_data_with_metadata(Some(metadata)))
}
fn fragment(input: Value, pretty: bool, config: &Config) -> String {
@ -328,7 +335,10 @@ fn get_padded_string(text: String, desired_length: usize, padding_character: cha
#[cfg(test)]
mod tests {
use crate::Metadata;
use super::*;
use nu_cmd_lang::eval_pipeline_without_terminal_expression;
use nu_protocol::{record, Config, IntoPipelineData, Value};
fn one(string: &str) -> String {
@ -453,4 +463,35 @@ mod tests {
"#)
);
}
#[test]
fn test_content_type_metadata() {
let mut engine_state = Box::new(EngineState::new());
let state_delta = {
// Base functions that are needed for testing
// Try to keep this working set small to keep tests running as fast as possible
let mut working_set = StateWorkingSet::new(&engine_state);
working_set.add_decl(Box::new(ToMd {}));
working_set.add_decl(Box::new(Metadata {}));
working_set.render()
};
let delta = state_delta;
engine_state
.merge_delta(delta)
.expect("Error merging delta");
let cmd = "{a: 1 b: 2} | to md | metadata | get content_type";
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),
&mut engine_state,
);
assert_eq!(
Value::test_record(record!("content_type" => Value::test_string("text/markdown"))),
result.expect("There should be a result")
);
}
}

View File

@ -74,13 +74,18 @@ MessagePack: https://msgpack.org/
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let metadata = input
.metadata()
.unwrap_or_default()
.with_content_type(Some("application/x-msgpack".into()));
let value_span = input.span().unwrap_or(call.head);
let value = input.into_value(value_span)?;
let mut out = vec![];
write_value(&mut out, &value, 0)?;
Ok(Value::binary(out, call.head).into_pipeline_data())
Ok(Value::binary(out, call.head).into_pipeline_data_with_metadata(Some(metadata)))
}
}
@ -268,6 +273,10 @@ where
#[cfg(test)]
mod test {
use nu_cmd_lang::eval_pipeline_without_terminal_expression;
use crate::Metadata;
use super::*;
#[test]
@ -276,4 +285,36 @@ mod test {
test_examples(ToMsgpack {})
}
#[test]
fn test_content_type_metadata() {
let mut engine_state = Box::new(EngineState::new());
let delta = {
// Base functions that are needed for testing
// Try to keep this working set small to keep tests running as fast as possible
let mut working_set = StateWorkingSet::new(&engine_state);
working_set.add_decl(Box::new(ToMsgpack {}));
working_set.add_decl(Box::new(Metadata {}));
working_set.render()
};
engine_state
.merge_delta(delta)
.expect("Error merging delta");
let cmd = "{a: 1 b: 2} | to msgpack | metadata | get content_type";
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),
&mut engine_state,
);
assert_eq!(
Value::test_record(
record!("content_type" => Value::test_string("application/x-msgpack"))
),
result.expect("There should be a result")
);
}
}

View File

@ -42,6 +42,11 @@ impl Command for ToNuon {
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let metadata = input
.metadata()
.unwrap_or_default()
.with_content_type(Some("application/x-nuon".into()));
let style = if call.has_flag(engine_state, stack, "raw")? {
nuon::ToStyle::Raw
} else if let Some(t) = call.get_flag(engine_state, stack, "tabs")? {
@ -56,9 +61,8 @@ impl Command for ToNuon {
let value = input.into_value(span)?;
match nuon::to_nuon(&value, style, Some(span)) {
Ok(serde_nuon_string) => {
Ok(Value::string(serde_nuon_string, span).into_pipeline_data())
}
Ok(serde_nuon_string) => Ok(Value::string(serde_nuon_string, span)
.into_pipeline_data_with_metadata(Some(metadata))),
_ => Ok(Value::error(
ShellError::CantConvert {
to_type: "NUON".into(),
@ -68,7 +72,7 @@ impl Command for ToNuon {
},
span,
)
.into_pipeline_data()),
.into_pipeline_data_with_metadata(Some(metadata))),
}
}
@ -100,10 +104,45 @@ impl Command for ToNuon {
#[cfg(test)]
mod test {
use super::*;
use nu_cmd_lang::eval_pipeline_without_terminal_expression;
use crate::Metadata;
#[test]
fn test_examples() {
use super::ToNuon;
use crate::test_examples;
test_examples(ToNuon {})
}
#[test]
fn test_content_type_metadata() {
let mut engine_state = Box::new(EngineState::new());
let delta = {
// Base functions that are needed for testing
// Try to keep this working set small to keep tests running as fast as possible
let mut working_set = StateWorkingSet::new(&engine_state);
working_set.add_decl(Box::new(ToNuon {}));
working_set.add_decl(Box::new(Metadata {}));
working_set.render()
};
engine_state
.merge_delta(delta)
.expect("Error merging delta");
let cmd = "{a: 1 b: 2} | to nuon | metadata | get content_type";
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),
&mut engine_state,
);
assert_eq!(
Value::test_record(record!("content_type" => Value::test_string("application/x-nuon"))),
result.expect("There should be a result")
);
}
}

View File

@ -134,14 +134,18 @@ fn local_into_string(value: Value, separator: &str, config: &Config) -> String {
fn update_metadata(metadata: Option<PipelineMetadata>) -> Option<PipelineMetadata> {
metadata
.map(|md| md.with_content_type(Some("text/plain".to_string())))
.map(|md| md.with_content_type(Some(mime::TEXT_PLAIN.to_string())))
.or_else(|| {
Some(PipelineMetadata::default().with_content_type(Some("text/plain".to_string())))
Some(PipelineMetadata::default().with_content_type(Some(mime::TEXT_PLAIN.to_string())))
})
}
#[cfg(test)]
mod test {
use nu_cmd_lang::eval_pipeline_without_terminal_expression;
use crate::Metadata;
use super::*;
#[test]
@ -150,4 +154,34 @@ mod test {
test_examples(ToText {})
}
#[test]
fn test_content_type_metadata() {
let mut engine_state = Box::new(EngineState::new());
let delta = {
// Base functions that are needed for testing
// Try to keep this working set small to keep tests running as fast as possible
let mut working_set = StateWorkingSet::new(&engine_state);
working_set.add_decl(Box::new(ToText {}));
working_set.add_decl(Box::new(Metadata {}));
working_set.render()
};
engine_state
.merge_delta(delta)
.expect("Error merging delta");
let cmd = "{a: 1 b: 2} | to text | metadata | get content_type";
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),
&mut engine_state,
);
assert_eq!(
Value::test_record(record!("content_type" => Value::test_string("text/plain"))),
result.expect("There should be a result")
);
}
}

View File

@ -4,6 +4,8 @@ use crate::formats::to::delimited::to_delimited_data;
use nu_engine::command_prelude::*;
use nu_protocol::Config;
use super::delimited::ToDelimitedDataArgs;
#[derive(Clone)]
pub struct ToTsv;
@ -82,11 +84,26 @@ fn to_tsv(
item: '\t',
span: head,
};
to_delimited_data(noheaders, sep, columns, "TSV", input, head, config)
to_delimited_data(
ToDelimitedDataArgs {
noheaders,
separator: sep,
columns,
format_name: "TSV",
input,
head,
content_type: Some(mime::TEXT_TAB_SEPARATED_VALUES.to_string()),
},
config,
)
}
#[cfg(test)]
mod test {
use nu_cmd_lang::eval_pipeline_without_terminal_expression;
use crate::Metadata;
use super::*;
#[test]
@ -95,4 +112,36 @@ mod test {
test_examples(ToTsv {})
}
#[test]
fn test_content_type_metadata() {
let mut engine_state = Box::new(EngineState::new());
let delta = {
// Base functions that are needed for testing
// Try to keep this working set small to keep tests running as fast as possible
let mut working_set = StateWorkingSet::new(&engine_state);
working_set.add_decl(Box::new(ToTsv {}));
working_set.add_decl(Box::new(Metadata {}));
working_set.render()
};
engine_state
.merge_delta(delta)
.expect("Error merging delta");
let cmd = "{a: 1 b: 2} | to tsv | metadata | get content_type";
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),
&mut engine_state,
);
assert_eq!(
Value::test_record(
record!("content_type" => Value::test_string("text/tab-separated-values"))
),
result.expect("There should be a result")
);
}
}

View File

@ -132,6 +132,10 @@ impl Job {
}
fn run(mut self, input: PipelineData, head: Span) -> Result<PipelineData, ShellError> {
let metadata = input
.metadata()
.unwrap_or_default()
.with_content_type(Some("application/xml".into()));
let value = input.into_value(head)?;
self.write_xml_entry(value, true).and_then(|_| {
@ -141,7 +145,7 @@ impl Job {
} else {
return Err(ShellError::NonUtf8 { span: head });
};
Ok(Value::string(s, head).into_pipeline_data())
Ok(Value::string(s, head).into_pipeline_data_with_metadata(Some(metadata)))
})
}
@ -508,6 +512,10 @@ impl Job {
#[cfg(test)]
mod test {
use nu_cmd_lang::eval_pipeline_without_terminal_expression;
use crate::Metadata;
use super::*;
#[test]
@ -516,4 +524,34 @@ mod test {
test_examples(ToXml {})
}
#[test]
fn test_content_type_metadata() {
let mut engine_state = Box::new(EngineState::new());
let delta = {
// Base functions that are needed for testing
// Try to keep this working set small to keep tests running as fast as possible
let mut working_set = StateWorkingSet::new(&engine_state);
working_set.add_decl(Box::new(ToXml {}));
working_set.add_decl(Box::new(Metadata {}));
working_set.render()
};
engine_state
.merge_delta(delta)
.expect("Error merging delta");
let cmd = "{tag: note attributes: {} content : [{tag: remember attributes: {} content : [{tag: null attributes: null content : Event}]}]} | to xml | metadata | get content_type";
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),
&mut engine_state,
);
assert_eq!(
Value::test_record(record!("content_type" => Value::test_string("application/xml"))),
result.expect("There should be a result")
);
}
}

View File

@ -95,11 +95,18 @@ pub fn value_to_yaml_value(v: &Value) -> Result<serde_yaml::Value, ShellError> {
}
fn to_yaml(input: PipelineData, head: Span) -> Result<PipelineData, ShellError> {
let metadata = input
.metadata()
.unwrap_or_default()
.with_content_type(Some("application/yaml".into()));
let value = input.into_value(head)?;
let yaml_value = value_to_yaml_value(&value)?;
match serde_yaml::to_string(&yaml_value) {
Ok(serde_yaml_string) => Ok(Value::string(serde_yaml_string, head).into_pipeline_data()),
Ok(serde_yaml_string) => {
Ok(Value::string(serde_yaml_string, head)
.into_pipeline_data_with_metadata(Some(metadata)))
}
_ => Ok(Value::error(
ShellError::CantConvert {
to_type: "YAML".into(),
@ -109,12 +116,16 @@ fn to_yaml(input: PipelineData, head: Span) -> Result<PipelineData, ShellError>
},
head,
)
.into_pipeline_data()),
.into_pipeline_data_with_metadata(Some(metadata))),
}
}
#[cfg(test)]
mod test {
use nu_cmd_lang::eval_pipeline_without_terminal_expression;
use crate::Metadata;
use super::*;
#[test]
@ -123,4 +134,34 @@ mod test {
test_examples(ToYaml {})
}
#[test]
fn test_content_type_metadata() {
let mut engine_state = Box::new(EngineState::new());
let delta = {
// Base functions that are needed for testing
// Try to keep this working set small to keep tests running as fast as possible
let mut working_set = StateWorkingSet::new(&engine_state);
working_set.add_decl(Box::new(ToYaml {}));
working_set.add_decl(Box::new(Metadata {}));
working_set.render()
};
engine_state
.merge_delta(delta)
.expect("Error merging delta");
let cmd = "{a: 1 b: 2} | to yaml | metadata | get content_type";
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),
&mut engine_state,
);
assert_eq!(
Value::test_record(record!("content_type" => Value::test_string("application/yaml"))),
result.expect("There should be a result")
);
}
}

View File

@ -29,6 +29,10 @@ impl Command for SubCommand {
vec!["square", "root"]
}
fn is_const(&self) -> bool {
true
}
fn run(
&self,
engine_state: &EngineState,
@ -44,6 +48,23 @@ impl Command for SubCommand {
input.map(move |value| operate(value, head), engine_state.signals())
}
fn run_const(
&self,
working_set: &StateWorkingSet,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
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| operate(value, head),
working_set.permanent().signals(),
)
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Compute the square root of each number in a list",

View File

@ -141,17 +141,17 @@ pub fn request_add_authorization_header(
let login = match (user, password) {
(Some(user), Some(password)) => {
let mut enc_str = String::new();
base64_engine.encode_string(&format!("{user}:{password}"), &mut enc_str);
base64_engine.encode_string(format!("{user}:{password}"), &mut enc_str);
Some(enc_str)
}
(Some(user), _) => {
let mut enc_str = String::new();
base64_engine.encode_string(&format!("{user}:"), &mut enc_str);
base64_engine.encode_string(format!("{user}:"), &mut enc_str);
Some(enc_str)
}
(_, Some(password)) => {
let mut enc_str = String::new();
base64_engine.encode_string(&format!(":{password}"), &mut enc_str);
base64_engine.encode_string(format!(":{password}"), &mut enc_str);
Some(enc_str)
}
_ => None,

View File

@ -0,0 +1,64 @@
use nu_engine::command_prelude::*;
use rand::{thread_rng, RngCore};
#[derive(Clone)]
pub struct SubCommand;
impl Command for SubCommand {
fn name(&self) -> &str {
"random binary"
}
fn signature(&self) -> Signature {
Signature::build("random binary")
.input_output_types(vec![(Type::Nothing, Type::Binary)])
.allow_variants_without_examples(true)
.required("length", SyntaxShape::Int, "Length of the output binary.")
.category(Category::Random)
}
fn usage(&self) -> &str {
"Generate random bytes."
}
fn search_terms(&self) -> Vec<&str> {
vec!["generate", "bytes"]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let length = call.req(engine_state, stack, 0)?;
let mut rng = thread_rng();
let mut out = vec![0u8; length];
rng.fill_bytes(&mut out);
Ok(Value::binary(out, call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Generate 16 random bytes",
example: "random bytes 16",
result: None,
}]
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(SubCommand {})
}
}

View File

@ -19,12 +19,17 @@ impl Command for SubCommand {
Signature::build("random chars")
.input_output_types(vec![(Type::Nothing, Type::String)])
.allow_variants_without_examples(true)
.named("length", SyntaxShape::Int, "Number of chars", Some('l'))
.named(
"length",
SyntaxShape::Int,
"Number of chars (default 25)",
Some('l'),
)
.category(Category::Random)
}
fn usage(&self) -> &str {
"Generate random chars."
"Generate random chars uniformly distributed over ASCII letters and numbers: a-z, A-Z and 0-9."
}
fn search_terms(&self) -> Vec<&str> {
@ -44,7 +49,7 @@ impl Command for SubCommand {
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Generate random chars",
description: "Generate a string with 25 random chars",
example: "random chars",
result: None,
},

View File

@ -15,7 +15,11 @@ impl Command for SubCommand {
Signature::build("random int")
.input_output_types(vec![(Type::Nothing, Type::Int)])
.allow_variants_without_examples(true)
.optional("range", SyntaxShape::Range, "Range of values.")
.optional(
"range",
SyntaxShape::Range,
"Range of potential values, inclusive of both start and end values.",
)
.category(Category::Random)
}
@ -40,12 +44,12 @@ impl Command for SubCommand {
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Generate an unconstrained random integer",
description: "Generate a non-negative random integer",
example: "random int",
result: None,
},
Example {
description: "Generate a random integer less than or equal to 500",
description: "Generate a random integer between 0 (inclusive) and 500 (inclusive)",
example: "random int ..500",
result: None,
},
@ -55,8 +59,8 @@ impl Command for SubCommand {
result: None,
},
Example {
description: "Generate a random integer between 1 and 10",
example: "random int 1..10",
description: "Generate a random integer between -10 (inclusive) and 10 (inclusive)",
example: "random int (-10)..10",
result: None,
},
]

View File

@ -1,3 +1,4 @@
mod binary;
mod bool;
mod chars;
mod dice;
@ -6,6 +7,7 @@ mod int;
mod random_;
mod uuid;
pub use self::binary::SubCommand as RandomBinary;
pub use self::bool::SubCommand as RandomBool;
pub use self::chars::SubCommand as RandomChars;
pub use self::dice::SubCommand as RandomDice;

View File

@ -187,7 +187,7 @@ fn split_words_helper(v: &Value, word_length: Option<usize>, span: Span, graphem
// [^[:alpha:]\'] = do not match any uppercase or lowercase letters or apostrophes
// [^\p{L}\'] = do not match any unicode uppercase or lowercase letters or apostrophes
// Let's go with the unicode one in hopes that it works on more than just ascii characters
let regex_replace = Regex::new(r"[^\p{L}\']").expect("regular expression error");
let regex_replace = Regex::new(r"[^\p{L}\p{N}\']").expect("regular expression error");
let v_span = v.span();
match v {
@ -422,4 +422,9 @@ mod test {
test_examples(SubCommand {})
}
#[test]
fn mixed_letter_number() {
let actual = nu!(r#"echo "a1 b2 c3" | split words | str join ','"#);
assert_eq!(actual.out, "a1,b2,c3");
}
}

View File

@ -1,7 +1,7 @@
use nu_path::Path;
use nu_test_support::fs::Stub::EmptyFile;
use nu_test_support::nu;
use nu_test_support::playground::Playground;
use std::path::PathBuf;
#[test]
fn cd_works_with_in_var() {
@ -22,7 +22,7 @@ fn filesystem_change_from_current_directory_using_relative_path() {
Playground::setup("cd_test_1", |dirs, _| {
let actual = nu!( cwd: dirs.root(), "cd cd_test_1; $env.PWD");
assert_eq!(PathBuf::from(actual.out), *dirs.test());
assert_eq!(Path::new(&actual.out), dirs.test());
})
}
@ -32,7 +32,7 @@ fn filesystem_change_from_current_directory_using_relative_path_with_trailing_sl
// Intentionally not using correct path sep because this should work on Windows
let actual = nu!( cwd: dirs.root(), "cd cd_test_1_slash/; $env.PWD");
assert_eq!(PathBuf::from(actual.out), *dirs.test());
assert_eq!(Path::new(&actual.out), *dirs.test());
})
}
@ -48,7 +48,7 @@ fn filesystem_change_from_current_directory_using_absolute_path() {
dirs.formats().display()
);
assert_eq!(PathBuf::from(actual.out), dirs.formats());
assert_eq!(Path::new(&actual.out), dirs.formats());
})
}
@ -65,7 +65,7 @@ fn filesystem_change_from_current_directory_using_absolute_path_with_trailing_sl
std::path::MAIN_SEPARATOR_STR,
);
assert_eq!(PathBuf::from(actual.out), dirs.formats());
assert_eq!(Path::new(&actual.out), dirs.formats());
})
}
@ -84,7 +84,7 @@ fn filesystem_switch_back_to_previous_working_directory() {
dirs.test().display()
);
assert_eq!(PathBuf::from(actual.out), dirs.test().join("odin"));
assert_eq!(Path::new(&actual.out), dirs.test().join("odin"));
})
}
@ -101,10 +101,7 @@ fn filesystem_change_from_current_directory_using_relative_path_and_dash() {
"
);
assert_eq!(
PathBuf::from(actual.out),
dirs.test().join("odin").join("-")
);
assert_eq!(Path::new(&actual.out), dirs.test().join("odin").join("-"));
})
}
@ -119,7 +116,7 @@ fn filesystem_change_current_directory_to_parent_directory() {
"
);
assert_eq!(PathBuf::from(actual.out), *dirs.root());
assert_eq!(Path::new(&actual.out), *dirs.root());
})
}
@ -136,7 +133,7 @@ fn filesystem_change_current_directory_to_two_parents_up_using_multiple_dots() {
"
);
assert_eq!(PathBuf::from(actual.out), *dirs.test());
assert_eq!(Path::new(&actual.out), *dirs.test());
})
}
@ -151,7 +148,7 @@ fn filesystem_change_to_home_directory() {
"
);
assert_eq!(Some(PathBuf::from(actual.out)), dirs::home_dir());
assert_eq!(Path::new(&actual.out), dirs::home_dir().unwrap());
})
}
@ -169,7 +166,7 @@ fn filesystem_change_to_a_directory_containing_spaces() {
);
assert_eq!(
PathBuf::from(actual.out),
Path::new(&actual.out),
dirs.test().join("robalino turner katz")
);
})
@ -234,7 +231,7 @@ fn filesystem_change_directory_to_symlink_relative() {
$env.PWD
"
);
assert_eq!(PathBuf::from(actual.out), dirs.test().join("foo_link"));
assert_eq!(Path::new(&actual.out), dirs.test().join("foo_link"));
let actual = nu!(
cwd: dirs.test().join("boo"),
@ -243,7 +240,7 @@ fn filesystem_change_directory_to_symlink_relative() {
$env.PWD
"
);
assert_eq!(PathBuf::from(actual.out), dirs.test().join("foo"));
assert_eq!(Path::new(&actual.out), dirs.test().join("foo"));
})
}

View File

@ -95,13 +95,13 @@ fn capture_error_with_both_stdout_stderr_messages_not_hang_nushell() {
#[test]
fn combined_pipe_redirection() {
let actual = nu!("$env.FOO = hello; $env.BAR = world; nu --testbin echo_env_mixed out-err FOO BAR o+e>| complete | get stdout");
let actual = nu!("$env.FOO = 'hello'; $env.BAR = 'world'; nu --testbin echo_env_mixed out-err FOO BAR o+e>| complete | get stdout");
assert_eq!(actual.out, "helloworld");
}
#[test]
fn err_pipe_redirection() {
let actual =
nu!("$env.FOO = hello; nu --testbin echo_env_stderr FOO e>| complete | get stdout");
nu!("$env.FOO = 'hello'; nu --testbin echo_env_stderr FOO e>| complete | get stdout");
assert_eq!(actual.out, "hello");
}

View File

@ -1,6 +1,5 @@
use std::{io::Write, path::PathBuf};
use chrono::{DateTime, FixedOffset};
use nu_path::AbsolutePathBuf;
use nu_protocol::{ast::PathMember, record, Span, Value};
use nu_test_support::{
fs::{line_ending, Stub},
@ -13,6 +12,7 @@ use rand::{
rngs::StdRng,
Rng, SeedableRng,
};
use std::io::Write;
#[test]
fn into_sqlite_schema() {
@ -453,7 +453,7 @@ impl Distribution<TestRow> for Standard {
}
}
fn make_sqlite_db(dirs: &Dirs, nu_table: &str) -> PathBuf {
fn make_sqlite_db(dirs: &Dirs, nu_table: &str) -> AbsolutePathBuf {
let testdir = dirs.test();
let testdb_path =
testdir.join(testdir.file_name().unwrap().to_str().unwrap().to_owned() + ".db");
@ -465,7 +465,7 @@ fn make_sqlite_db(dirs: &Dirs, nu_table: &str) -> PathBuf {
);
assert!(nucmd.status.success());
testdb_path.into()
testdb_path
}
fn insert_test_rows(dirs: &Dirs, nu_table: &str, sql_query: Option<&str>, expected: Vec<TestRow>) {

View File

@ -23,6 +23,13 @@ fn let_takes_pipeline() {
assert_eq!(actual.out, "11");
}
#[test]
fn let_takes_pipeline_with_declared_type() {
let actual = nu!(r#"let x: list<string> = [] | append "hello world"; print $x.0"#);
assert_eq!(actual.out, "hello world");
}
#[test]
fn let_pipeline_allows_in() {
let actual =
@ -38,6 +45,13 @@ fn mut_takes_pipeline() {
assert_eq!(actual.out, "11");
}
#[test]
fn mut_takes_pipeline_with_declared_type() {
let actual = nu!(r#"mut x: list<string> = [] | append "hello world"; print $x.0"#);
assert_eq!(actual.out, "hello world");
}
#[test]
fn mut_pipeline_allows_in() {
let actual =

View File

@ -1,6 +1,6 @@
use nu_path::AbsolutePath;
use nu_test_support::nu;
use nu_test_support::playground::Playground;
use std::path::PathBuf;
#[test]
fn creates_temp_file() {
@ -9,7 +9,7 @@ fn creates_temp_file() {
cwd: dirs.test(),
"mktemp"
);
let loc = PathBuf::from(output.out.clone());
let loc = AbsolutePath::try_new(&output.out).unwrap();
println!("{:?}", loc);
assert!(loc.exists());
})
@ -22,7 +22,7 @@ fn creates_temp_file_with_suffix() {
cwd: dirs.test(),
"mktemp --suffix .txt tempfileXXX"
);
let loc = PathBuf::from(output.out.clone());
let loc = AbsolutePath::try_new(&output.out).unwrap();
assert!(loc.exists());
assert!(loc.is_file());
assert!(output.out.ends_with(".txt"));
@ -37,8 +37,7 @@ fn creates_temp_directory() {
cwd: dirs.test(),
"mktemp -d"
);
let loc = PathBuf::from(output.out);
let loc = AbsolutePath::try_new(&output.out).unwrap();
assert!(loc.exists());
assert!(loc.is_dir());
})

View File

@ -2,7 +2,6 @@ use nu_test_support::fs::{files_exist_at, Stub::EmptyFile, Stub::FileWithContent
use nu_test_support::nu;
use nu_test_support::playground::Playground;
use rstest::rstest;
use std::path::Path;
#[test]
fn moves_a_file() {
@ -96,7 +95,7 @@ fn moves_the_directory_inside_directory_if_path_to_move_is_existing_directory()
assert!(!original_dir.exists());
assert!(expected.exists());
assert!(files_exist_at(vec!["jttxt"], expected))
assert!(files_exist_at(&["jttxt"], expected))
})
}
@ -125,7 +124,7 @@ fn moves_using_path_with_wildcard() {
nu!(cwd: work_dir, "mv ../originals/*.ini ../expected");
assert!(files_exist_at(
vec!["yehuda.ini", "jt.ini", "sample.ini", "andres.ini",],
&["yehuda.ini", "jt.ini", "sample.ini", "andres.ini",],
expected
));
})
@ -152,7 +151,7 @@ fn moves_using_a_glob() {
assert!(meal_dir.exists());
assert!(files_exist_at(
vec!["arepa.txt", "empanada.txt", "taquiza.txt",],
&["arepa.txt", "empanada.txt", "taquiza.txt",],
expected
));
})
@ -184,7 +183,7 @@ fn moves_a_directory_with_files() {
assert!(!original_dir.exists());
assert!(expected_dir.exists());
assert!(files_exist_at(
vec![
&[
"car/car1.txt",
"car/car2.txt",
"bicycle/bicycle1.txt",
@ -322,7 +321,7 @@ fn move_files_using_glob_two_parents_up_using_multiple_dots() {
"#
);
let files = vec![
let files = &[
"yehuda.yaml",
"jtjson",
"andres.xml",
@ -333,7 +332,7 @@ fn move_files_using_glob_two_parents_up_using_multiple_dots() {
let original_dir = dirs.test().join("foo/bar");
let destination_dir = dirs.test();
assert!(files_exist_at(files.clone(), destination_dir));
assert!(files_exist_at(files, destination_dir));
assert!(!files_exist_at(files, original_dir))
})
}
@ -440,10 +439,7 @@ fn mv_change_case_of_directory() {
);
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
assert!(files_exist_at(
vec!["somefile.txt",],
dirs.test().join(new_dir)
));
assert!(files_exist_at(&["somefile.txt"], dirs.test().join(new_dir)));
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
_actual.err.contains("to a subdirectory of itself");
@ -647,10 +643,10 @@ fn test_cp_inside_glob_metachars_dir() {
assert!(actual.err.is_empty());
assert!(!files_exist_at(
vec!["test_file.txt"],
&["test_file.txt"],
dirs.test().join(sub_dir)
));
assert!(files_exist_at(vec!["test_file.txt"], dirs.test()));
assert!(files_exist_at(&["test_file.txt"], dirs.test()));
});
}
@ -667,19 +663,13 @@ fn mv_with_tilde() {
// mv file
let actual = nu!(cwd: dirs.test(), "mv '~tilde/f1.txt' ./");
assert!(actual.err.is_empty());
assert!(!files_exist_at(
vec![Path::new("f1.txt")],
dirs.test().join("~tilde")
));
assert!(files_exist_at(vec![Path::new("f1.txt")], dirs.test()));
assert!(!files_exist_at(&["f1.txt"], dirs.test().join("~tilde")));
assert!(files_exist_at(&["f1.txt"], dirs.test()));
// pass variable
let actual = nu!(cwd: dirs.test(), "let f = '~tilde/f2.txt'; mv $f ./");
assert!(actual.err.is_empty());
assert!(!files_exist_at(
vec![Path::new("f2.txt")],
dirs.test().join("~tilde")
));
assert!(files_exist_at(vec![Path::new("f1.txt")], dirs.test()));
assert!(!files_exist_at(&["f2.txt"], dirs.test().join("~tilde")));
assert!(files_exist_at(&["f1.txt"], dirs.test()));
})
}

View File

@ -1,9 +1,8 @@
use nu_path::Path;
use nu_test_support::fs::Stub::EmptyFile;
use nu_test_support::playground::Playground;
use nu_test_support::{nu, pipeline};
use std::path::PathBuf;
#[test]
fn expands_path_with_dot() {
Playground::setup("path_expand_1", |dirs, sandbox| {
@ -18,7 +17,7 @@ fn expands_path_with_dot() {
));
let expected = dirs.test.join("menu").join("spam.txt");
assert_eq!(PathBuf::from(actual.out), expected);
assert_eq!(Path::new(&actual.out), expected);
})
}
@ -38,7 +37,7 @@ fn expands_path_without_follow_symlink() {
));
let expected = dirs.test.join("menu").join("spam_link.ln");
assert_eq!(PathBuf::from(actual.out), expected);
assert_eq!(Path::new(&actual.out), expected);
})
}
@ -56,7 +55,7 @@ fn expands_path_with_double_dot() {
));
let expected = dirs.test.join("menu").join("spam.txt");
assert_eq!(PathBuf::from(actual.out), expected);
assert_eq!(Path::new(&actual.out), expected);
})
}
@ -74,7 +73,7 @@ fn const_path_expand() {
));
let expected = dirs.test.join("menu").join("spam.txt");
assert_eq!(PathBuf::from(actual.out), expected);
assert_eq!(Path::new(&actual.out), expected);
})
}
@ -92,7 +91,7 @@ mod windows {
"#
));
assert!(!PathBuf::from(actual.out).starts_with("~"));
assert!(!Path::new(&actual.out).starts_with("~"));
})
}
@ -106,7 +105,7 @@ mod windows {
"#
));
assert!(!PathBuf::from(actual.out).starts_with("~"));
assert!(!Path::new(&actual.out).starts_with("~"));
})
}
@ -131,7 +130,7 @@ mod windows {
));
let expected = dirs.test.join("menu").join("spam_link.ln");
assert_eq!(PathBuf::from(actual.out), expected);
assert_eq!(Path::new(&actual.out), expected);
})
}
}

View File

@ -3,5 +3,4 @@ mod chars;
mod dice;
mod float;
mod int;
#[cfg(feature = "uuid_crate")]
mod uuid;

View File

@ -1,5 +1,5 @@
use nu_test_support::nu;
use uuid_crate::Uuid;
use uuid::Uuid;
#[test]
fn generates_valid_uuid4() {

View File

@ -1,10 +1,11 @@
#[cfg(not(windows))]
use nu_path::AbsolutePath;
use nu_test_support::fs::{files_exist_at, Stub::EmptyFile};
use nu_test_support::nu;
use nu_test_support::playground::Playground;
use rstest::rstest;
#[cfg(not(windows))]
use std::fs;
use std::path::Path;
#[test]
fn removes_a_file() {
@ -48,7 +49,7 @@ fn removes_files_with_wildcard() {
);
assert!(!files_exist_at(
vec![
&[
"src/parser/parse/token_tree.rs",
"src/parser/hir/baseline_parse.rs",
"src/parser/hir/baseline_parse_tokens.rs"
@ -89,7 +90,7 @@ fn removes_deeply_nested_directories_with_wildcard_and_recursive_flag() {
);
assert!(!files_exist_at(
vec!["src/parser/parse", "src/parser/hir"],
&["src/parser/parse", "src/parser/hir"],
dirs.test()
));
})
@ -144,7 +145,7 @@ fn errors_if_attempting_to_delete_home() {
Playground::setup("rm_test_8", |dirs, _| {
let actual = nu!(
cwd: dirs.root(),
"$env.HOME = myhome ; rm -rf ~"
"$env.HOME = 'myhome' ; rm -rf ~"
);
assert!(actual.err.contains("please use -I or -i"));
@ -275,7 +276,7 @@ fn remove_files_from_two_parents_up_using_multiple_dots_and_glob() {
);
assert!(!files_exist_at(
vec!["yehuda.txt", "jttxt", "kevin.txt"],
&["yehuda.txt", "jttxt", "kevin.txt"],
dirs.test()
));
})
@ -303,8 +304,8 @@ fn rm_wildcard_keeps_dotfiles() {
r#"rm *"#
);
assert!(!files_exist_at(vec!["foo"], dirs.test()));
assert!(files_exist_at(vec![".bar"], dirs.test()));
assert!(!files_exist_at(&["foo"], dirs.test()));
assert!(files_exist_at(&[".bar"], dirs.test()));
})
}
@ -318,8 +319,8 @@ fn rm_wildcard_leading_dot_deletes_dotfiles() {
"rm .*"
);
assert!(files_exist_at(vec!["foo"], dirs.test()));
assert!(!files_exist_at(vec![".bar"], dirs.test()));
assert!(files_exist_at(&["foo"], dirs.test()));
assert!(!files_exist_at(&[".bar"], dirs.test()));
})
}
@ -405,16 +406,19 @@ fn removes_file_after_cd() {
})
}
#[cfg(not(windows))]
struct Cleanup<'a> {
dir_to_clean: &'a AbsolutePath,
}
#[cfg(not(windows))]
fn set_dir_read_only(directory: &AbsolutePath, read_only: bool) {
let mut permissions = fs::metadata(directory).unwrap().permissions();
permissions.set_readonly(read_only);
fs::set_permissions(directory, permissions).expect("failed to set directory permissions");
}
#[cfg(not(windows))]
impl<'a> Drop for Cleanup<'a> {
/// Restores write permissions to the given directory so that the Playground can be successfully
/// cleaned up.
@ -448,7 +452,7 @@ fn rm_prints_filenames_on_error() {
// This rm is expected to fail, and stderr output indicating so is also expected.
let actual = nu!(cwd: test_dir, "rm test*.txt");
assert!(files_exist_at(file_names.clone(), test_dir));
assert!(files_exist_at(&file_names, test_dir));
for file_name in file_names {
let path = test_dir.join(file_name);
let substr = format!("Could not delete {}", path.to_string_lossy());
@ -477,7 +481,7 @@ fn rm_files_inside_glob_metachars_dir() {
assert!(actual.err.is_empty());
assert!(!files_exist_at(
vec!["test_file.txt"],
&["test_file.txt"],
dirs.test().join(sub_dir)
));
});
@ -551,22 +555,16 @@ fn rm_with_tilde() {
let actual = nu!(cwd: dirs.test(), "rm '~tilde/f1.txt'");
assert!(actual.err.is_empty());
assert!(!files_exist_at(
vec![Path::new("f1.txt")],
dirs.test().join("~tilde")
));
assert!(!files_exist_at(&["f1.txt"], dirs.test().join("~tilde")));
// pass variable
let actual = nu!(cwd: dirs.test(), "let f = '~tilde/f2.txt'; rm $f");
assert!(actual.err.is_empty());
assert!(!files_exist_at(
vec![Path::new("f2.txt")],
dirs.test().join("~tilde")
));
assert!(!files_exist_at(&["f2.txt"], dirs.test().join("~tilde")));
// remove directory
let actual = nu!(cwd: dirs.test(), "let f = '~tilde'; rm -r $f");
assert!(actual.err.is_empty());
assert!(!files_exist_at(vec![Path::new("~tilde")], dirs.test()));
assert!(!files_exist_at(&["~tilde"], dirs.test()));
})
}

View File

@ -463,3 +463,65 @@ fn save_same_file_with_collect_and_filter() {
assert_eq!("helloworld", actual.out);
})
}
#[test]
fn save_from_child_process_dont_sink_stderr() {
Playground::setup("save_test_22", |dirs, sandbox| {
sandbox.with_files(&[
Stub::FileWithContent("log.txt", "Old"),
Stub::FileWithContent("err.txt", "Old Err"),
]);
let expected_file = dirs.test().join("log.txt");
let expected_stderr_file = dirs.test().join("err.txt");
let actual = nu!(
cwd: dirs.root(),
r#"
$env.FOO = " New";
$env.BAZ = " New Err";
do -i {nu -n -c 'nu --testbin echo_env FOO; nu --testbin echo_env_stderr BAZ'} | save -a -r save_test_22/log.txt"#,
);
assert_eq!(actual.err.trim_end(), " New Err");
let actual = file_contents(expected_file);
assert_eq!(actual.trim_end(), "Old New");
let actual = file_contents(expected_stderr_file);
assert_eq!(actual.trim_end(), "Old Err");
})
}
#[test]
fn parent_redirection_doesnt_affect_save() {
Playground::setup("save_test_23", |dirs, sandbox| {
sandbox.with_files(&[
Stub::FileWithContent("log.txt", "Old"),
Stub::FileWithContent("err.txt", "Old Err"),
]);
let expected_file = dirs.test().join("log.txt");
let expected_stderr_file = dirs.test().join("err.txt");
let actual = nu!(
cwd: dirs.root(),
r#"
$env.FOO = " New";
$env.BAZ = " New Err";
def tttt [] {
do -i {nu -n -c 'nu --testbin echo_env FOO; nu --testbin echo_env_stderr BAZ'} | save -a -r save_test_23/log.txt
};
tttt e> ("save_test_23" | path join empty_file)"#
);
assert_eq!(actual.err.trim_end(), " New Err");
let actual = file_contents(expected_file);
assert_eq!(actual.trim_end(), "Old New");
let actual = file_contents(expected_stderr_file);
assert_eq!(actual.trim_end(), "Old Err");
let actual = file_contents(dirs.test().join("empty_file"));
assert_eq!(actual.trim_end(), "");
})
}

View File

@ -2567,7 +2567,7 @@ fn theme_cmd(theme: &str, footer: bool, then: &str) -> String {
with_footer = "$env.config.footer_mode = \"always\"".to_string();
}
format!("$env.config.table.mode = {theme}; $env.config.table.header_on_separator = true; {with_footer}; {then}")
format!("$env.config.table.mode = \"{theme}\"; $env.config.table.header_on_separator = true; {with_footer}; {then}")
}
#[test]

View File

@ -2,7 +2,6 @@ use chrono::{DateTime, Local};
use nu_test_support::fs::{files_exist_at, Stub};
use nu_test_support::nu;
use nu_test_support::playground::Playground;
use std::path::Path;
// Use 1 instead of 0 because 0 has a special meaning in Windows
const TIME_ONE: filetime::FileTime = filetime::FileTime::from_unix_time(1, 0);
@ -494,12 +493,12 @@ fn create_a_file_with_tilde() {
Playground::setup("touch with tilde", |dirs, _| {
let actual = nu!(cwd: dirs.test(), "touch '~tilde'");
assert!(actual.err.is_empty());
assert!(files_exist_at(vec![Path::new("~tilde")], dirs.test()));
assert!(files_exist_at(&["~tilde"], dirs.test()));
// pass variable
let actual = nu!(cwd: dirs.test(), "let f = '~tilde2'; touch $f");
assert!(actual.err.is_empty());
assert!(files_exist_at(vec![Path::new("~tilde2")], dirs.test()));
assert!(files_exist_at(&["~tilde2"], dirs.test()));
})
}

View File

@ -7,7 +7,6 @@ use nu_test_support::nu;
use nu_test_support::playground::Playground;
use rstest::rstest;
use std::path::Path;
#[cfg(not(target_os = "windows"))]
const PATH_SEPARATOR: &str = "/";
@ -131,11 +130,7 @@ fn copies_the_directory_inside_directory_if_path_to_copy_is_directory_and_with_r
assert!(expected_dir.exists());
assert!(files_exist_at(
vec![
Path::new("yehuda.txt"),
Path::new("jttxt"),
Path::new("andres.txt")
],
&["yehuda.txt", "jttxt", "andres.txt"],
&expected_dir
));
})
@ -181,15 +176,15 @@ fn deep_copies_with_recursive_flag_impl(progress: bool) {
assert!(expected_dir.exists());
assert!(files_exist_at(
vec![Path::new("errors.txt"), Path::new("multishells.txt")],
&["errors.txt", "multishells.txt"],
jts_expected_copied_dir
));
assert!(files_exist_at(
vec![Path::new("coverage.txt"), Path::new("commands.txt")],
&["coverage.txt", "commands.txt"],
andres_expected_copied_dir
));
assert!(files_exist_at(
vec![Path::new("defer-evaluation.txt")],
&["defer-evaluation.txt"],
yehudas_expected_copied_dir
));
})
@ -220,13 +215,13 @@ fn copies_using_path_with_wildcard_impl(progress: bool) {
);
assert!(files_exist_at(
vec![
Path::new("caco3_plastics.csv"),
Path::new("cargo_sample.toml"),
Path::new("jt.xml"),
Path::new("sample.ini"),
Path::new("sgml_description.json"),
Path::new("utf16.ini"),
&[
"caco3_plastics.csv",
"cargo_sample.toml",
"jt.xml",
"sample.ini",
"sgml_description.json",
"utf16.ini",
],
dirs.test()
));
@ -265,13 +260,13 @@ fn copies_using_a_glob_impl(progress: bool) {
);
assert!(files_exist_at(
vec![
Path::new("caco3_plastics.csv"),
Path::new("cargo_sample.toml"),
Path::new("jt.xml"),
Path::new("sample.ini"),
Path::new("sgml_description.json"),
Path::new("utf16.ini"),
&[
"caco3_plastics.csv",
"cargo_sample.toml",
"jt.xml",
"sample.ini",
"sgml_description.json",
"utf16.ini",
],
dirs.test()
));
@ -341,7 +336,7 @@ fn copy_files_using_glob_two_parents_up_using_multiple_dots_imp(progress: bool)
);
assert!(files_exist_at(
vec![
&[
"yehuda.yaml",
"jtjson",
"andres.xml",
@ -377,7 +372,7 @@ fn copy_file_and_dir_from_two_parents_up_using_multiple_dots_to_current_dir_recu
let expected = dirs.test().join("foo/bar");
assert!(files_exist_at(vec!["hello_there", "hello_again"], expected));
assert!(files_exist_at(&["hello_there", "hello_again"], expected));
})
}
@ -428,7 +423,7 @@ fn copy_dir_contains_symlink_ignored_impl(progress: bool) {
// check hello_there exists inside `tmp_dir_2`, and `dangle_symlink` don't exists inside `tmp_dir_2`.
let expected = sandbox.cwd().join("tmp_dir_2");
assert!(files_exist_at(vec!["hello_there"], expected));
assert!(files_exist_at(&["hello_there"], expected));
// GNU cp will copy the broken symlink, so following their behavior
// thus commenting out below
// let path = expected.join("dangle_symlink");
@ -461,7 +456,7 @@ fn copy_dir_contains_symlink_impl(progress: bool) {
// check hello_there exists inside `tmp_dir_2`, and `dangle_symlink` also exists inside `tmp_dir_2`.
let expected = sandbox.cwd().join("tmp_dir_2");
assert!(files_exist_at(vec!["hello_there"], expected.clone()));
assert!(files_exist_at(&["hello_there"], expected.clone()));
let path = expected.join("dangle_symlink");
assert!(path.is_symlink());
});
@ -1151,10 +1146,10 @@ fn test_cp_inside_glob_metachars_dir() {
assert!(actual.err.is_empty());
assert!(files_exist_at(
vec!["test_file.txt"],
&["test_file.txt"],
dirs.test().join(sub_dir)
));
assert!(files_exist_at(vec!["test_file.txt"], dirs.test()));
assert!(files_exist_at(&["test_file.txt"], dirs.test()));
});
}
@ -1167,10 +1162,7 @@ fn test_cp_to_customized_home_directory() {
let actual = nu!(cwd: dirs.test(), "mkdir test; cp test_file.txt ~/test/");
assert!(actual.err.is_empty());
assert!(files_exist_at(
vec!["test_file.txt"],
dirs.test().join("test")
));
assert!(files_exist_at(&["test_file.txt"], dirs.test().join("test")));
})
}
@ -1193,20 +1185,14 @@ fn cp_with_tilde() {
// cp file
let actual = nu!(cwd: dirs.test(), "cp '~tilde/f1.txt' ./");
assert!(actual.err.is_empty());
assert!(files_exist_at(
vec![Path::new("f1.txt")],
dirs.test().join("~tilde")
));
assert!(files_exist_at(vec![Path::new("f1.txt")], dirs.test()));
assert!(files_exist_at(&["f1.txt"], dirs.test().join("~tilde")));
assert!(files_exist_at(&["f1.txt"], dirs.test()));
// pass variable
let actual = nu!(cwd: dirs.test(), "let f = '~tilde/f2.txt'; cp $f ./");
assert!(actual.err.is_empty());
assert!(files_exist_at(
vec![Path::new("f2.txt")],
dirs.test().join("~tilde")
));
assert!(files_exist_at(vec![Path::new("f1.txt")], dirs.test()));
assert!(files_exist_at(&["f2.txt"], dirs.test().join("~tilde")));
assert!(files_exist_at(&["f1.txt"], dirs.test()));
})
}

View File

@ -1,7 +1,6 @@
use nu_test_support::fs::files_exist_at;
use nu_test_support::playground::Playground;
use nu_test_support::{nu, pipeline};
use std::path::Path;
#[test]
fn creates_directory() {
@ -25,10 +24,7 @@ fn accepts_and_creates_directories() {
"mkdir dir_1 dir_2 dir_3"
);
assert!(files_exist_at(
vec![Path::new("dir_1"), Path::new("dir_2"), Path::new("dir_3")],
dirs.test()
));
assert!(files_exist_at(&["dir_1", "dir_2", "dir_3"], dirs.test()));
})
}
@ -70,10 +66,7 @@ fn print_created_paths() {
pipeline("mkdir -v dir_1 dir_2 dir_3")
);
assert!(files_exist_at(
vec![Path::new("dir_1"), Path::new("dir_2"), Path::new("dir_3")],
dirs.test()
));
assert!(files_exist_at(&["dir_1", "dir_2", "dir_3"], dirs.test()));
assert!(actual.out.contains("dir_1"));
assert!(actual.out.contains("dir_2"));
@ -165,11 +158,11 @@ fn mkdir_with_tilde() {
Playground::setup("mkdir with tilde", |dirs, _| {
let actual = nu!(cwd: dirs.test(), "mkdir '~tilde'");
assert!(actual.err.is_empty());
assert!(files_exist_at(vec![Path::new("~tilde")], dirs.test()));
assert!(files_exist_at(&["~tilde"], dirs.test()));
// pass variable
let actual = nu!(cwd: dirs.test(), "let f = '~tilde2'; mkdir $f");
assert!(actual.err.is_empty());
assert!(files_exist_at(vec![Path::new("~tilde2")], dirs.test()));
assert!(files_exist_at(&["~tilde2"], dirs.test()));
})
}

View File

@ -5,7 +5,7 @@ edition = "2021"
license = "MIT"
name = "nu-derive-value"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-derive-value"
version = "0.95.1"
version = "0.96.2"
[lib]
proc-macro = true

View File

@ -5,17 +5,18 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-engine"
edition = "2021"
license = "MIT"
name = "nu-engine"
version = "0.95.1"
version = "0.96.2"
[lib]
bench = false
[dependencies]
nu-protocol = { path = "../nu-protocol", features = ["plugin"], version = "0.95.1" }
nu-path = { path = "../nu-path", version = "0.95.1" }
nu-glob = { path = "../nu-glob", version = "0.95.1" }
nu-utils = { path = "../nu-utils", version = "0.95.1" }
nu-protocol = { path = "../nu-protocol", features = ["plugin"], version = "0.96.2" }
nu-path = { path = "../nu-path", version = "0.96.2" }
nu-glob = { path = "../nu-glob", version = "0.96.2" }
nu-utils = { path = "../nu-utils", version = "0.96.2" }
log = { workspace = true }
terminal_size = { workspace = true }
[features]
plugin = []

View File

@ -423,7 +423,7 @@ impl BlockBuilder {
self.push(Instruction::Jump { index: label_id.0 }.into_spanned(span))
}
/// The index that the next instruction [`.push()`]ed will have.
/// The index that the next instruction [`.push()`](Self::push)ed will have.
pub(crate) fn here(&self) -> usize {
self.instructions.len()
}

View File

@ -444,7 +444,15 @@ pub(crate) fn compile_expression(
working_set,
builder,
&full_cell_path.head,
RedirectModes::capture_out(expr.span),
// Only capture the output if there is a tail. This was a bit of a headscratcher
// as the parser emits a FullCellPath with no tail for subexpressions in
// general, which shouldn't be captured any differently than they otherwise
// would be.
if !full_cell_path.tail.is_empty() {
RedirectModes::capture_out(expr.span)
} else {
redirect_modes
},
in_reg,
out_reg,
)?;

View File

@ -3,24 +3,28 @@ use nu_protocol::{
ast::{Argument, Call, Expr, Expression, RecordItem},
debugger::WithoutDebug,
engine::{Command, EngineState, Stack, UNKNOWN_SPAN_ID},
record, Category, Config, Example, IntoPipelineData, PipelineData, Signature, Span, SpanId,
Spanned, SyntaxShape, Type, Value,
record, Category, Config, Example, IntoPipelineData, PipelineData, PositionalArg, Signature,
Span, SpanId, Spanned, SyntaxShape, Type, Value,
};
use std::{collections::HashMap, fmt::Write};
use terminal_size::{Height, Width};
/// ANSI style reset
const RESET: &str = "\x1b[0m";
/// ANSI set default color (as set in the terminal)
const DEFAULT_COLOR: &str = "\x1b[39m";
pub fn get_full_help(
command: &dyn Command,
engine_state: &EngineState,
stack: &mut Stack,
) -> String {
let config = stack.get_config(engine_state);
let doc_config = DocumentationConfig {
no_subcommands: false,
no_color: !config.use_ansi_coloring,
brief: false,
};
// Precautionary step to capture any command output generated during this operation. We
// internally call several commands (`table`, `ansi`, `nu-highlight`) and get their
// `PipelineData` using this `Stack`, any other output should not be redirected like the main
// execution.
let stack = &mut stack.start_capture();
let signature = command.signature().update_from_command(command);
get_documentation(
@ -28,19 +32,11 @@ pub fn get_full_help(
&command.examples(),
engine_state,
stack,
&doc_config,
command.is_keyword(),
)
}
#[derive(Default)]
struct DocumentationConfig {
no_subcommands: bool,
no_color: bool,
brief: bool,
}
// Utility returns nu-highlighted string
/// Syntax highlight code using the `nu-highlight` command if available
fn nu_highlight_string(code_string: &str, engine_state: &EngineState, stack: &mut Stack) -> String {
if let Some(highlighter) = engine_state.find_decl(b"nu-highlight", &[]) {
let decl = engine_state.get_decl(highlighter);
@ -67,35 +63,15 @@ fn get_documentation(
examples: &[Example],
engine_state: &EngineState,
stack: &mut Stack,
config: &DocumentationConfig,
is_parser_keyword: bool,
) -> String {
let nu_config = stack.get_config(engine_state);
// Create ansi colors
//todo make these configurable -- pull from enginestate.config
let help_section_name: String = get_ansi_color_for_component_or_default(
engine_state,
&nu_config,
"shape_string",
"\x1b[32m",
); // default: green
let help_subcolor_one: String = get_ansi_color_for_component_or_default(
engine_state,
&nu_config,
"shape_external",
"\x1b[36m",
); // default: cyan
// was const bb: &str = "\x1b[1;34m"; // bold blue
let help_subcolor_two: String = get_ansi_color_for_component_or_default(
engine_state,
&nu_config,
"shape_block",
"\x1b[94m",
); // default: light blue (nobold, should be bolding the *names*)
const RESET: &str = "\x1b[0m"; // reset
let mut help_style = HelpStyle::default();
help_style.update_from_config(engine_state, &nu_config);
let help_section_name = &help_style.section_name;
let help_subcolor_one = &help_style.subcolor_one;
let cmd_name = &sig.name;
let mut long_desc = String::new();
@ -106,44 +82,46 @@ fn get_documentation(
long_desc.push_str("\n\n");
}
let extra_usage = if config.brief { "" } else { &sig.extra_usage };
let extra_usage = &sig.extra_usage;
if !extra_usage.is_empty() {
long_desc.push_str(extra_usage);
long_desc.push_str("\n\n");
}
let mut subcommands = vec![];
if !config.no_subcommands {
let signatures = engine_state.get_signatures(true);
for sig in signatures {
if sig.name.starts_with(&format!("{cmd_name} "))
// Don't display removed/deprecated commands in the Subcommands list
&& !matches!(sig.category, Category::Removed)
{
subcommands.push(format!(
" {help_subcolor_one}{}{RESET} - {}",
sig.name, sig.usage
));
}
}
}
if !sig.search_terms.is_empty() {
let text = format!(
"{help_section_name}Search terms{RESET}: {help_subcolor_one}{}{}\n\n",
let _ = write!(
long_desc,
"{help_section_name}Search terms{RESET}: {help_subcolor_one}{}{RESET}\n\n",
sig.search_terms.join(", "),
RESET
);
let _ = write!(long_desc, "{text}");
}
let text = format!(
"{}Usage{}:\n > {}\n",
help_section_name,
RESET,
let _ = write!(
long_desc,
"{help_section_name}Usage{RESET}:\n > {}\n",
sig.call_signature()
);
let _ = write!(long_desc, "{text}");
// TODO: improve the subcommand name resolution
// issues:
// - Aliases are included
// - https://github.com/nushell/nushell/issues/11657
// - Subcommands are included violating module scoping
// - https://github.com/nushell/nushell/issues/11447
// - https://github.com/nushell/nushell/issues/11625
let mut subcommands = vec![];
let signatures = engine_state.get_signatures(true);
for sig in signatures {
// Don't display removed/deprecated commands in the Subcommands list
if sig.name.starts_with(&format!("{cmd_name} "))
&& !matches!(sig.category, Category::Removed)
{
subcommands.push(format!(
" {help_subcolor_one}{}{RESET} - {}",
sig.name, sig.usage
));
}
}
if !subcommands.is_empty() {
let _ = write!(long_desc, "\n{help_section_name}Subcommands{RESET}:\n");
@ -153,12 +131,9 @@ fn get_documentation(
}
if !sig.named.is_empty() {
long_desc.push_str(&get_flags_section(
Some(engine_state),
Some(&nu_config),
sig,
|v| nu_highlight_string(&v.to_parsable_string(", ", &nu_config), engine_state, stack),
))
long_desc.push_str(&get_flags_section(sig, &help_style, |v| {
nu_highlight_string(&v.to_parsable_string(", ", &nu_config), engine_state, stack)
}))
}
if !sig.required_positional.is_empty()
@ -167,70 +142,46 @@ fn get_documentation(
{
let _ = write!(long_desc, "\n{help_section_name}Parameters{RESET}:\n");
for positional in &sig.required_positional {
let text = match &positional.shape {
SyntaxShape::Keyword(kw, shape) => {
format!(
" {help_subcolor_one}\"{}\" + {RESET}<{help_subcolor_two}{}{RESET}>: {}",
String::from_utf8_lossy(kw),
document_shape(*shape.clone()),
positional.desc
)
}
_ => {
format!(
" {help_subcolor_one}{}{RESET} <{help_subcolor_two}{}{RESET}>: {}",
positional.name,
document_shape(positional.shape.clone()),
positional.desc
)
}
};
let _ = writeln!(long_desc, "{text}");
write_positional(
&mut long_desc,
positional,
PositionalKind::Required,
&help_style,
&nu_config,
engine_state,
stack,
);
}
for positional in &sig.optional_positional {
let text = match &positional.shape {
SyntaxShape::Keyword(kw, shape) => {
format!(
" {help_subcolor_one}\"{}\" + {RESET}<{help_subcolor_two}{}{RESET}>: {} (optional)",
String::from_utf8_lossy(kw),
document_shape(*shape.clone()),
positional.desc
)
}
_ => {
let opt_suffix = if let Some(value) = &positional.default_value {
format!(
" (optional, default: {})",
nu_highlight_string(
&value.to_parsable_string(", ", &nu_config),
engine_state,
stack
)
)
} else {
(" (optional)").to_string()
};
format!(
" {help_subcolor_one}{}{RESET} <{help_subcolor_two}{}{RESET}>: {}{}",
positional.name,
document_shape(positional.shape.clone()),
positional.desc,
opt_suffix,
)
}
};
let _ = writeln!(long_desc, "{text}");
write_positional(
&mut long_desc,
positional,
PositionalKind::Optional,
&help_style,
&nu_config,
engine_state,
stack,
);
}
if let Some(rest_positional) = &sig.rest_positional {
let text = format!(
" ...{help_subcolor_one}{}{RESET} <{help_subcolor_two}{}{RESET}>: {}",
rest_positional.name,
document_shape(rest_positional.shape.clone()),
rest_positional.desc
write_positional(
&mut long_desc,
rest_positional,
PositionalKind::Rest,
&help_style,
&nu_config,
engine_state,
stack,
);
let _ = writeln!(long_desc, "{text}");
}
}
fn get_term_width() -> usize {
if let Some((Width(w), Height(_))) = terminal_size::terminal_size() {
w as usize
} else {
80
}
}
@ -256,7 +207,18 @@ fn get_documentation(
&Call {
decl_id,
head: span,
arguments: vec![],
arguments: vec![Argument::Named((
Spanned {
item: "width".to_string(),
span: Span::unknown(),
},
None,
Some(Expression::new_unknown(
Expr::Int(get_term_width() as i64 - 2), // padding, see below
Span::unknown(),
Type::Int,
)),
))],
parser_info: HashMap::new(),
},
PipelineData::Value(Value::list(vals, span), None),
@ -280,36 +242,12 @@ fn get_documentation(
long_desc.push_str(" ");
long_desc.push_str(example.description);
if config.no_color {
if !nu_config.use_ansi_coloring {
let _ = write!(long_desc, "\n > {}\n", example.example);
} else if let Some(highlighter) = engine_state.find_decl(b"nu-highlight", &[]) {
let decl = engine_state.get_decl(highlighter);
let call = Call::new(Span::unknown());
match decl.run(
engine_state,
stack,
&(&call).into(),
Value::string(example.example, Span::unknown()).into_pipeline_data(),
) {
Ok(output) => {
let result = output.into_value(Span::unknown());
match result.and_then(Value::coerce_into_string) {
Ok(s) => {
let _ = write!(long_desc, "\n > {s}\n");
}
_ => {
let _ = write!(long_desc, "\n > {}\n", example.example);
}
}
}
Err(_) => {
let _ = write!(long_desc, "\n > {}\n", example.example);
}
}
} else {
let _ = write!(long_desc, "\n > {}\n", example.example);
}
let code_string = nu_highlight_string(example.example, engine_state, stack);
let _ = write!(long_desc, "\n > {code_string}\n");
};
if let Some(result) = &example.result {
let mut table_call = Call::new(Span::unknown());
@ -334,6 +272,19 @@ fn get_documentation(
None,
))
}
table_call.add_named((
Spanned {
item: "width".to_string(),
span: Span::unknown(),
},
None,
Some(Expression::new_unknown(
Expr::Int(get_term_width() as i64 - 2),
Span::unknown(),
Type::Int,
)),
));
let table = engine_state
.find_decl("table".as_bytes(), &[])
.and_then(|decl_id| {
@ -362,19 +313,19 @@ fn get_documentation(
long_desc.push('\n');
if config.no_color {
if !nu_config.use_ansi_coloring {
nu_utils::strip_ansi_string_likely(long_desc)
} else {
long_desc
}
}
fn get_ansi_color_for_component_or_default(
fn update_ansi_from_config(
ansi_code: &mut String,
engine_state: &EngineState,
nu_config: &Config,
theme_component: &str,
default: &str,
) -> String {
) {
if let Some(color) = &nu_config.color_config.get(theme_component) {
let caller_stack = &mut Stack::new().capture();
let span = Span::unknown();
@ -397,14 +348,12 @@ fn get_ansi_color_for_component_or_default(
PipelineData::Empty,
) {
if let Ok((str, ..)) = result.collect_string_strict(span) {
return str;
*ansi_code = str;
}
}
}
}
}
default.to_string()
}
fn get_argument_for_color_value(
@ -458,151 +407,174 @@ fn get_argument_for_color_value(
}
}
// document shape helps showing more useful information
pub fn document_shape(shape: SyntaxShape) -> SyntaxShape {
/// Contains the settings for ANSI colors in help output
///
/// By default contains a fixed set of (4-bit) colors
///
/// Can reflect configuration using [`HelpStyle::update_from_config`]
pub struct HelpStyle {
section_name: String,
subcolor_one: String,
subcolor_two: String,
}
impl Default for HelpStyle {
fn default() -> Self {
HelpStyle {
// default: green
section_name: "\x1b[32m".to_string(),
// default: cyan
subcolor_one: "\x1b[36m".to_string(),
// default: light blue
subcolor_two: "\x1b[94m".to_string(),
}
}
}
impl HelpStyle {
/// Pull colors from the [`Config`]
///
/// Uses some arbitrary `shape_*` settings, assuming they are well visible in the terminal theme.
///
/// Implementation detail: currently executes `ansi` command internally thus requiring the
/// [`EngineState`] for execution.
/// See <https://github.com/nushell/nushell/pull/10623> for details
pub fn update_from_config(&mut self, engine_state: &EngineState, nu_config: &Config) {
update_ansi_from_config(
&mut self.section_name,
engine_state,
nu_config,
"shape_string",
);
update_ansi_from_config(
&mut self.subcolor_one,
engine_state,
nu_config,
"shape_external",
);
update_ansi_from_config(
&mut self.subcolor_two,
engine_state,
nu_config,
"shape_block",
);
}
}
/// Make syntax shape presentable by stripping custom completer info
fn document_shape(shape: &SyntaxShape) -> &SyntaxShape {
match shape {
SyntaxShape::CompleterWrapper(inner_shape, _) => *inner_shape,
SyntaxShape::CompleterWrapper(inner_shape, _) => inner_shape,
_ => shape,
}
}
#[derive(PartialEq)]
enum PositionalKind {
Required,
Optional,
Rest,
}
fn write_positional(
long_desc: &mut String,
positional: &PositionalArg,
arg_kind: PositionalKind,
help_style: &HelpStyle,
nu_config: &Config,
engine_state: &EngineState,
stack: &mut Stack,
) {
let help_subcolor_one = &help_style.subcolor_one;
let help_subcolor_two = &help_style.subcolor_two;
// Indentation
long_desc.push_str(" ");
if arg_kind == PositionalKind::Rest {
long_desc.push_str("...");
}
match &positional.shape {
SyntaxShape::Keyword(kw, shape) => {
let _ = write!(
long_desc,
"{help_subcolor_one}\"{}\" + {RESET}<{help_subcolor_two}{}{RESET}>",
String::from_utf8_lossy(kw),
document_shape(shape),
);
}
_ => {
let _ = write!(
long_desc,
"{help_subcolor_one}{}{RESET} <{help_subcolor_two}{}{RESET}>",
positional.name,
document_shape(&positional.shape),
);
}
};
if !positional.desc.is_empty() || arg_kind == PositionalKind::Optional {
let _ = write!(long_desc, ": {}", positional.desc);
}
if arg_kind == PositionalKind::Optional {
if let Some(value) = &positional.default_value {
let _ = write!(
long_desc,
" (optional, default: {})",
nu_highlight_string(
&value.to_parsable_string(", ", nu_config),
engine_state,
stack
)
);
} else {
long_desc.push_str(" (optional)");
};
}
long_desc.push('\n');
}
pub fn get_flags_section<F>(
engine_state_opt: Option<&EngineState>,
nu_config_opt: Option<&Config>,
signature: &Signature,
help_style: &HelpStyle,
mut value_formatter: F, // format default Value (because some calls cant access config or nu-highlight)
) -> String
where
F: FnMut(&nu_protocol::Value) -> String,
{
//todo make these configurable -- pull from enginestate.config
let help_section_name: String;
let help_subcolor_one: String;
let help_subcolor_two: String;
// Sometimes we want to get the flags without engine_state
// For example, in nu-plugin. In that case, we fall back on default values
if let Some(engine_state) = engine_state_opt {
let nu_config = nu_config_opt.unwrap_or_else(|| engine_state.get_config());
help_section_name = get_ansi_color_for_component_or_default(
engine_state,
nu_config,
"shape_string",
"\x1b[32m",
); // default: green
help_subcolor_one = get_ansi_color_for_component_or_default(
engine_state,
nu_config,
"shape_external",
"\x1b[36m",
); // default: cyan
// was const bb: &str = "\x1b[1;34m"; // bold blue
help_subcolor_two = get_ansi_color_for_component_or_default(
engine_state,
nu_config,
"shape_block",
"\x1b[94m",
);
// default: light blue (nobold, should be bolding the *names*)
} else {
help_section_name = "\x1b[32m".to_string();
help_subcolor_one = "\x1b[36m".to_string();
help_subcolor_two = "\x1b[94m".to_string();
}
const RESET: &str = "\x1b[0m"; // reset
const D: &str = "\x1b[39m"; // default
let help_section_name = &help_style.section_name;
let help_subcolor_one = &help_style.subcolor_one;
let help_subcolor_two = &help_style.subcolor_two;
let mut long_desc = String::new();
let _ = write!(long_desc, "\n{help_section_name}Flags{RESET}:\n");
for flag in &signature.named {
let default_str = if let Some(value) = &flag.default_value {
format!(
" (default: {help_subcolor_two}{}{RESET})",
&value_formatter(value)
)
} else {
"".to_string()
};
let msg = if let Some(arg) = &flag.arg {
if let Some(short) = flag.short {
if flag.required {
format!(
" {help_subcolor_one}-{}{}{RESET} (required parameter) {:?} - {}{}\n",
short,
if !flag.long.is_empty() {
format!("{D},{RESET} {help_subcolor_one}--{}", flag.long)
} else {
"".into()
},
arg,
flag.desc,
default_str,
)
} else {
format!(
" {help_subcolor_one}-{}{}{RESET} <{help_subcolor_two}{:?}{RESET}> - {}{}\n",
short,
if !flag.long.is_empty() {
format!("{D},{RESET} {help_subcolor_one}--{}", flag.long)
} else {
"".into()
},
arg,
flag.desc,
default_str,
)
}
} else if flag.required {
format!(
" {help_subcolor_one}--{}{RESET} (required parameter) <{help_subcolor_two}{:?}{RESET}> - {}{}\n",
flag.long, arg, flag.desc, default_str,
)
} else {
format!(
" {help_subcolor_one}--{}{RESET} <{help_subcolor_two}{:?}{RESET}> - {}{}\n",
flag.long, arg, flag.desc, default_str,
)
// Indentation
long_desc.push_str(" ");
// Short flag shown before long flag
if let Some(short) = flag.short {
let _ = write!(long_desc, "{help_subcolor_one}-{}{RESET}", short);
if !flag.long.is_empty() {
let _ = write!(long_desc, "{DEFAULT_COLOR},{RESET} ");
}
} else if let Some(short) = flag.short {
if flag.required {
format!(
" {help_subcolor_one}-{}{}{RESET} (required parameter) - {}{}\n",
short,
if !flag.long.is_empty() {
format!("{D},{RESET} {help_subcolor_one}--{}", flag.long)
} else {
"".into()
},
flag.desc,
default_str,
)
} else {
format!(
" {help_subcolor_one}-{}{}{RESET} - {}{}\n",
short,
if !flag.long.is_empty() {
format!("{D},{RESET} {help_subcolor_one}--{}", flag.long)
} else {
"".into()
},
flag.desc,
default_str
)
}
} else if flag.required {
format!(
" {help_subcolor_one}--{}{RESET} (required parameter) - {}{}\n",
flag.long, flag.desc, default_str,
)
} else {
format!(
" {help_subcolor_one}--{}{RESET} - {}\n",
flag.long, flag.desc
)
};
long_desc.push_str(&msg);
}
if !flag.long.is_empty() {
let _ = write!(long_desc, "{help_subcolor_one}--{}{RESET}", flag.long);
}
if flag.required {
long_desc.push_str(" (required parameter)")
}
// Type/Syntax shape info
if let Some(arg) = &flag.arg {
let _ = write!(
long_desc,
" <{help_subcolor_two}{}{RESET}>",
document_shape(arg)
);
}
let _ = write!(long_desc, " - {}", flag.desc);
if let Some(value) = &flag.default_value {
let _ = write!(long_desc, " (default: {})", &value_formatter(value));
}
long_desc.push('\n');
}
long_desc
}

View File

@ -198,7 +198,7 @@ pub fn redirect_env(engine_state: &EngineState, caller_stack: &mut Stack, callee
}
// set config to callee config, to capture any updates to that
caller_stack.config = callee_stack.config.clone();
caller_stack.config.clone_from(&callee_stack.config);
}
fn eval_external(

View File

@ -5,21 +5,21 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-explore"
edition = "2021"
license = "MIT"
name = "nu-explore"
version = "0.95.1"
version = "0.96.2"
[lib]
bench = false
[dependencies]
nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
nu-parser = { path = "../nu-parser", version = "0.95.1" }
nu-color-config = { path = "../nu-color-config", version = "0.95.1" }
nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-table = { path = "../nu-table", version = "0.95.1" }
nu-json = { path = "../nu-json", version = "0.95.1" }
nu-utils = { path = "../nu-utils", version = "0.95.1" }
nu-protocol = { path = "../nu-protocol", version = "0.96.2" }
nu-parser = { path = "../nu-parser", version = "0.96.2" }
nu-color-config = { path = "../nu-color-config", version = "0.96.2" }
nu-engine = { path = "../nu-engine", version = "0.96.2" }
nu-table = { path = "../nu-table", version = "0.96.2" }
nu-json = { path = "../nu-json", version = "0.96.2" }
nu-utils = { path = "../nu-utils", version = "0.96.2" }
nu-ansi-term = { workspace = true }
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.95.1" }
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.96.2" }
anyhow = { workspace = true }
log = { workspace = true }

View File

@ -1,6 +1,6 @@
[package]
name = "nu-glob"
version = "0.95.1"
version = "0.96.2"
authors = ["The Nushell Project Developers", "The Rust Project Developers"]
license = "MIT/Apache-2.0"
description = """

View File

@ -8,7 +8,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-json"
edition = "2021"
license = "MIT"
name = "nu-json"
version = "0.95.1"
version = "0.96.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -26,7 +26,7 @@ serde = { workspace = true }
serde_json = { workspace = true }
[dev-dependencies]
nu-test-support = { path = "../nu-test-support", version = "0.95.1" }
nu-path = { path = "../nu-path", version = "0.95.1" }
nu-test-support = { path = "../nu-test-support", version = "0.96.2" }
nu-path = { path = "../nu-path", version = "0.96.2" }
serde_json = "1.0"
fancy-regex = "0.13.0"
fancy-regex = "0.13.0"

View File

@ -3,14 +3,14 @@ authors = ["The Nushell Project Developers"]
description = "Nushell's integrated LSP server"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-lsp"
name = "nu-lsp"
version = "0.95.1"
version = "0.96.2"
edition = "2021"
license = "MIT"
[dependencies]
nu-cli = { path = "../nu-cli", version = "0.95.1" }
nu-parser = { path = "../nu-parser", version = "0.95.1" }
nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
nu-cli = { path = "../nu-cli", version = "0.96.2" }
nu-parser = { path = "../nu-parser", version = "0.96.2" }
nu-protocol = { path = "../nu-protocol", version = "0.96.2" }
reedline = { workspace = true }
@ -23,8 +23,8 @@ serde = { workspace = true }
serde_json = { workspace = true }
[dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.95.1" }
nu-command = { path = "../nu-command", version = "0.95.1" }
nu-test-support = { path = "../nu-test-support", version = "0.95.1" }
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.96.2" }
nu-command = { path = "../nu-command", version = "0.96.2" }
nu-test-support = { path = "../nu-test-support", version = "0.96.2" }
assert-json-diff = "2.0"

View File

@ -5,17 +5,17 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-parser"
edition = "2021"
license = "MIT"
name = "nu-parser"
version = "0.95.1"
version = "0.96.2"
exclude = ["/fuzz"]
[lib]
bench = false
[dependencies]
nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-path = { path = "../nu-path", version = "0.95.1" }
nu-plugin-engine = { path = "../nu-plugin-engine", optional = true, version = "0.95.1" }
nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
nu-engine = { path = "../nu-engine", version = "0.96.2" }
nu-path = { path = "../nu-path", version = "0.96.2" }
nu-plugin-engine = { path = "../nu-plugin-engine", optional = true, version = "0.96.2" }
nu-protocol = { path = "../nu-protocol", version = "0.96.2" }
bytesize = { workspace = true }
chrono = { default-features = false, features = ['std'], workspace = true }

View File

@ -6,6 +6,7 @@ pub enum TokenContents {
Comment,
Pipe,
PipePipe,
AssignmentOperator,
ErrGreaterPipe,
OutErrGreaterPipe,
Semicolon,
@ -69,6 +70,12 @@ fn is_item_terminator(
|| special_tokens.contains(&c))
}
/// Assignment operators have special handling distinct from math expressions, as they cause the
/// rest of the pipeline to be consumed.
pub fn is_assignment_operator(bytes: &[u8]) -> bool {
matches!(bytes, b"=" | b"+=" | b"++=" | b"-=" | b"*=" | b"/=")
}
// A special token is one that is a byte that stands alone as its own token. For example
// when parsing a signature you may want to have `:` be able to separate tokens and also
// to be handled as its own token to notify you you're about to parse a type in the example
@ -297,6 +304,10 @@ pub fn lex_item(
let mut err = None;
let output = match &input[(span.start - span_offset)..(span.end - span_offset)] {
bytes if is_assignment_operator(bytes) => Token {
contents: TokenContents::AssignmentOperator,
span,
},
b"out>" | b"o>" => Token {
contents: TokenContents::OutGreaterThan,
span,

View File

@ -196,10 +196,43 @@ pub fn lite_parse(tokens: &[Token]) -> (LiteBlock, Option<ParseError>) {
let mut last_token = TokenContents::Eol;
let mut file_redirection = None;
let mut curr_comment: Option<Vec<Span>> = None;
let mut is_assignment = false;
let mut error = None;
for (idx, token) in tokens.iter().enumerate() {
if let Some((source, append, span)) = file_redirection.take() {
if is_assignment {
match &token.contents {
// Consume until semicolon or terminating EOL. Assignments absorb pipelines and
// redirections.
TokenContents::Eol => {
// Handle `[Command] [Pipe] ([Comment] | [Eol])+ [Command]`
//
// `[Eol]` branch checks if previous token is `[Pipe]` to construct pipeline
// and so `[Comment] | [Eol]` should be ignore to make it work
let actual_token = last_non_comment_token(tokens, idx);
if actual_token != Some(TokenContents::Pipe) {
is_assignment = false;
pipeline.push(&mut command);
block.push(&mut pipeline);
}
if last_token == TokenContents::Eol {
// Clear out the comment as we're entering a new comment
curr_comment = None;
}
}
TokenContents::Semicolon => {
is_assignment = false;
pipeline.push(&mut command);
block.push(&mut pipeline);
}
TokenContents::Comment => {
command.comments.push(token.span);
curr_comment = None;
}
_ => command.push(token.span),
}
} else if let Some((source, append, span)) = file_redirection.take() {
match &token.contents {
TokenContents::PipePipe => {
error = error.or(Some(ParseError::ShellOrOr(token.span)));
@ -218,6 +251,11 @@ pub fn lite_parse(tokens: &[Token]) -> (LiteBlock, Option<ParseError>) {
command.push(token.span)
}
}
TokenContents::AssignmentOperator => {
error = error.or(Some(ParseError::Expected("redirection target", token.span)));
command.push(span);
command.push(token.span);
}
TokenContents::OutGreaterThan
| TokenContents::OutGreaterGreaterThan
| TokenContents::ErrGreaterThan
@ -280,6 +318,15 @@ pub fn lite_parse(tokens: &[Token]) -> (LiteBlock, Option<ParseError>) {
}
command.push(token.span);
}
TokenContents::AssignmentOperator => {
// When in assignment mode, we'll just consume pipes or redirections as part of
// the command.
is_assignment = true;
if let Some(curr_comment) = curr_comment.take() {
command.comments = curr_comment;
}
command.push(token.span);
}
TokenContents::OutGreaterThan => {
error = error.or(command.check_accepts_redirection(token.span));
file_redirection = Some((RedirectionSource::Stdout, false, token.span));

View File

@ -35,8 +35,8 @@ use crate::{
lite_parser::{lite_parse, LiteCommand},
parser::{
check_call, garbage, garbage_pipeline, parse, parse_call, parse_expression,
parse_full_signature, parse_import_pattern, parse_internal_call, parse_multispan_value,
parse_string, parse_value, parse_var_with_opt_type, trim_quotes, ParsedInternalCall,
parse_full_signature, parse_import_pattern, parse_internal_call, parse_string, parse_value,
parse_var_with_opt_type, trim_quotes, ParsedInternalCall,
},
unescape_unquote_string, Token, TokenContents,
};
@ -169,11 +169,7 @@ pub fn parse_def_predecl(working_set: &mut StateWorkingSet, spans: &[Span]) {
// Now, pos should point at the next span after the def-like call.
// Skip all potential flags, like --env, --wrapped or --help:
while pos < spans.len()
&& working_set
.get_span_contents(spans[pos])
.starts_with(&[b'-'])
{
while pos < spans.len() && working_set.get_span_contents(spans[pos]).starts_with(b"-") {
pos += 1;
}
@ -202,12 +198,8 @@ pub fn parse_def_predecl(working_set: &mut StateWorkingSet, spans: &[Span]) {
let mut signature_pos = None;
while pos < spans.len() {
if working_set
.get_span_contents(spans[pos])
.starts_with(&[b'['])
|| working_set
.get_span_contents(spans[pos])
.starts_with(&[b'('])
if working_set.get_span_contents(spans[pos]).starts_with(b"[")
|| working_set.get_span_contents(spans[pos]).starts_with(b"(")
{
signature_pos = Some(pos);
break;
@ -308,21 +300,21 @@ pub fn parse_for(working_set: &mut StateWorkingSet, lite_command: &LiteCommand)
}
// Let's get our block and make sure it has the right signature
if let Some(arg) = call.positional_nth(2) {
match arg {
Expression {
expr: Expr::Block(block_id),
..
}
| Expression {
expr: Expr::RowCondition(block_id),
..
} => {
let block = working_set.get_block_mut(*block_id);
if let Some(
Expression {
expr: Expr::Block(block_id),
..
}
| Expression {
expr: Expr::RowCondition(block_id),
..
},
) = call.positional_nth(2)
{
{
let block = working_set.get_block_mut(*block_id);
block.signature = Box::new(sig);
}
_ => {}
block.signature = Box::new(sig);
}
}
@ -424,7 +416,7 @@ pub fn parse_def(
let mut decl_name_span = None;
for span in rest_spans {
if !working_set.get_span_contents(*span).starts_with(&[b'-']) {
if !working_set.get_span_contents(*span).starts_with(b"-") {
decl_name_span = Some(*span);
break;
}
@ -554,7 +546,7 @@ pub fn parse_def(
for arg_name in &signature.optional_positional {
verify_not_reserved_variable_name(working_set, &arg_name.name, sig.span);
}
for arg_name in &signature.rest_positional {
if let Some(arg_name) = &signature.rest_positional {
verify_not_reserved_variable_name(working_set, &arg_name.name, sig.span);
}
for flag_name in &signature.get_names() {
@ -3171,9 +3163,6 @@ pub fn parse_const(working_set: &mut StateWorkingSet, spans: &[Span]) -> Pipelin
// }
if let Some(decl_id) = working_set.find_decl(b"const") {
let cmd = working_set.get_decl(decl_id);
let call_signature = cmd.signature().call_signature();
if spans.len() >= 4 {
// This is a bit of by-hand parsing to get around the issue where we want to parse in the reverse order
// so that the var-id created by the variable isn't visible in the expression that init it
@ -3181,18 +3170,29 @@ pub fn parse_const(working_set: &mut StateWorkingSet, spans: &[Span]) -> Pipelin
let item = working_set.get_span_contents(*span.1);
// const x = 'f', = at least start from index 2
if item == b"=" && spans.len() > (span.0 + 1) && span.0 > 1 {
let mut idx = span.0;
// Parse the rvalue as a subexpression
let rvalue_span = Span::concat(&spans[(span.0 + 1)..]);
let rvalue = parse_multispan_value(
working_set,
spans,
&mut idx,
&SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::MathExpression)),
let (rvalue_tokens, rvalue_error) = lex(
working_set.get_span_contents(rvalue_span),
rvalue_span.start,
&[],
&[],
false,
);
working_set.parse_errors.extend(rvalue_error);
trace!("parsing: const right-hand side subexpression");
let rvalue_block =
parse_block(working_set, &rvalue_tokens, rvalue_span, false, true);
let rvalue_ty = rvalue_block.output_type();
let rvalue_block_id = working_set.add_block(Arc::new(rvalue_block));
let rvalue = Expression::new(
working_set,
Expr::Subexpression(rvalue_block_id),
rvalue_span,
rvalue_ty,
);
if idx < (spans.len() - 1) {
working_set
.error(ParseError::ExtraPositional(call_signature, spans[idx + 1]));
}
let mut idx = 0;

View File

@ -1,5 +1,5 @@
use crate::{
lex::{lex, lex_signature},
lex::{is_assignment_operator, lex, lex_signature},
lite_parser::{lite_parse, LiteCommand, LitePipeline, LiteRedirection, LiteRedirectionTarget},
parse_keywords::*,
parse_patterns::parse_pattern,
@ -1458,7 +1458,8 @@ fn parse_binary_with_base(
| TokenContents::ErrGreaterThan
| TokenContents::ErrGreaterGreaterThan
| TokenContents::OutErrGreaterThan
| TokenContents::OutErrGreaterGreaterThan => {
| TokenContents::OutErrGreaterGreaterThan
| TokenContents::AssignmentOperator => {
working_set.error(ParseError::Expected("binary", span));
return garbage(working_set, span);
}
@ -3409,7 +3410,7 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
for token in &output {
match token {
Token {
contents: crate::TokenContents::Item,
contents: crate::TokenContents::Item | crate::TokenContents::AssignmentOperator,
span,
} => {
let span = *span;
@ -4829,7 +4830,7 @@ pub fn parse_value(
}
}
pub fn parse_operator(working_set: &mut StateWorkingSet, span: Span) -> Expression {
pub fn parse_assignment_operator(working_set: &mut StateWorkingSet, span: Span) -> Expression {
let contents = working_set.get_span_contents(span);
let operator = match contents {
@ -4839,6 +4840,95 @@ pub fn parse_operator(working_set: &mut StateWorkingSet, span: Span) -> Expressi
b"-=" => Operator::Assignment(Assignment::MinusAssign),
b"*=" => Operator::Assignment(Assignment::MultiplyAssign),
b"/=" => Operator::Assignment(Assignment::DivideAssign),
_ => {
working_set.error(ParseError::Expected("assignment operator", span));
return garbage(working_set, span);
}
};
Expression::new(working_set, Expr::Operator(operator), span, Type::Any)
}
pub fn parse_assignment_expression(
working_set: &mut StateWorkingSet,
spans: &[Span],
) -> Expression {
trace!("parsing: assignment expression");
let expr_span = Span::concat(spans);
// Assignment always has the most precedence, and its right-hand side can be a pipeline
let Some(op_index) = spans
.iter()
.position(|span| is_assignment_operator(working_set.get_span_contents(*span)))
else {
working_set.error(ParseError::Expected("assignment expression", expr_span));
return garbage(working_set, expr_span);
};
let lhs_spans = &spans[0..op_index];
let op_span = spans[op_index];
let rhs_spans = &spans[(op_index + 1)..];
if lhs_spans.is_empty() {
working_set.error(ParseError::Expected(
"left hand side of assignment",
op_span,
));
return garbage(working_set, expr_span);
}
if rhs_spans.is_empty() {
working_set.error(ParseError::Expected(
"right hand side of assignment",
op_span,
));
return garbage(working_set, expr_span);
}
// Parse the lhs and operator as usual for a math expression
let mut lhs = parse_expression(working_set, lhs_spans);
let mut operator = parse_assignment_operator(working_set, op_span);
// Re-parse the right-hand side as a subexpression
let rhs_span = Span::concat(rhs_spans);
let (rhs_tokens, rhs_error) = lex(
working_set.get_span_contents(rhs_span),
rhs_span.start,
&[],
&[],
true,
);
working_set.parse_errors.extend(rhs_error);
trace!("parsing: assignment right-hand side subexpression");
let rhs_block = parse_block(working_set, &rhs_tokens, rhs_span, false, true);
let rhs_ty = rhs_block.output_type();
let rhs_block_id = working_set.add_block(Arc::new(rhs_block));
let mut rhs = Expression::new(
working_set,
Expr::Subexpression(rhs_block_id),
rhs_span,
rhs_ty,
);
let (result_ty, err) = math_result_type(working_set, &mut lhs, &mut operator, &mut rhs);
if let Some(err) = err {
working_set.parse_errors.push(err);
}
Expression::new(
working_set,
Expr::BinaryOp(Box::new(lhs), Box::new(operator), Box::new(rhs)),
expr_span,
result_ty,
)
}
pub fn parse_operator(working_set: &mut StateWorkingSet, span: Span) -> Expression {
let contents = working_set.get_span_contents(span);
let operator = match contents {
b"==" => Operator::Comparison(Comparison::Equal),
b"!=" => Operator::Comparison(Comparison::NotEqual),
b"<" => Operator::Comparison(Comparison::LessThan),
@ -4954,6 +5044,10 @@ pub fn parse_operator(working_set: &mut StateWorkingSet, span: Span) -> Expressi
));
return garbage(working_set, span);
}
op if is_assignment_operator(op) => {
working_set.error(ParseError::Expected("a non-assignment operator", span));
return garbage(working_set, span);
}
_ => {
working_set.error(ParseError::Expected("operator", span));
return garbage(working_set, span);
@ -5258,7 +5352,12 @@ pub fn parse_expression(working_set: &mut StateWorkingSet, spans: &[Span]) -> Ex
return garbage(working_set, Span::concat(spans));
}
let output = if is_math_expression_like(working_set, spans[pos]) {
let output = if spans[pos..]
.iter()
.any(|span| is_assignment_operator(working_set.get_span_contents(*span)))
{
parse_assignment_expression(working_set, &spans[pos..])
} else if is_math_expression_like(working_set, spans[pos]) {
parse_math_expression(working_set, &spans[pos..], None)
} else {
let bytes = working_set.get_span_contents(spans[pos]).to_vec();
@ -5690,69 +5789,24 @@ pub(crate) fn redirecting_builtin_error(
}
pub fn parse_pipeline(working_set: &mut StateWorkingSet, pipeline: &LitePipeline) -> Pipeline {
let first_command = pipeline.commands.first();
let first_command_name = first_command
.and_then(|command| command.parts.first())
.map(|span| working_set.get_span_contents(*span));
if pipeline.commands.len() > 1 {
// Special case: allow "let" or "mut" to consume the whole pipeline, if this is a pipeline
// with multiple commands
if matches!(first_command_name, Some(b"let" | b"mut")) {
// Merge the pipeline into one command
let first_command = first_command.expect("must be Some");
// Parse a normal multi command pipeline
let elements: Vec<_> = pipeline
.commands
.iter()
.enumerate()
.map(|(index, element)| {
let element = parse_pipeline_element(working_set, element);
// Handle $in for pipeline elements beyond the first one
if index > 0 && element.has_in_variable(working_set) {
wrap_element_with_collect(working_set, element.clone())
} else {
element
}
})
.collect();
let remainder_span = first_command
.parts_including_redirection()
.skip(3)
.chain(
pipeline.commands[1..]
.iter()
.flat_map(|command| command.parts_including_redirection()),
)
.reduce(Span::append);
let parts = first_command
.parts
.iter()
.take(3) // the let/mut start itself
.copied()
.chain(remainder_span) // everything else
.collect();
let comments = pipeline
.commands
.iter()
.flat_map(|command| command.comments.iter())
.copied()
.collect();
let new_command = LiteCommand {
pipe: None,
comments,
parts,
redirection: None,
};
parse_builtin_commands(working_set, &new_command)
} else {
// Parse a normal multi command pipeline
let elements: Vec<_> = pipeline
.commands
.iter()
.enumerate()
.map(|(index, element)| {
let element = parse_pipeline_element(working_set, element);
// Handle $in for pipeline elements beyond the first one
if index > 0 && element.has_in_variable(working_set) {
wrap_element_with_collect(working_set, element.clone())
} else {
element
}
})
.collect();
Pipeline { elements }
}
Pipeline { elements }
} else {
// If there's only one command in the pipeline, this could be a builtin command
parse_builtin_commands(working_set, &pipeline.commands[0])
@ -5872,7 +5926,7 @@ pub fn discover_captures_in_closure(
seen.push(var_id);
}
}
for positional in &block.signature.rest_positional {
if let Some(positional) = &block.signature.rest_positional {
if let Some(var_id) = positional.var_id {
seen.push(var_id);
}

View File

@ -1177,9 +1177,9 @@ fn test_nothing_comparison_eq() {
#[rstest]
#[case(b"let a = 1 err> /dev/null")]
#[case(b"let a = 1 out> /dev/null")]
#[case(b"let a = 1 out+err> /dev/null")]
#[case(b"mut a = 1 err> /dev/null")]
#[case(b"mut a = 1 out> /dev/null")]
#[case(b"let a = 1 out+err> /dev/null")]
#[case(b"mut a = 1 out+err> /dev/null")]
fn test_redirection_with_letmut(#[case] phase: &[u8]) {
let engine_state = EngineState::new();

View File

@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-path"
edition = "2021"
license = "MIT"
name = "nu-path"
version = "0.95.1"
version = "0.96.2"
exclude = ["/fuzz"]
[lib]
@ -18,4 +18,4 @@ dirs = { workspace = true }
omnipath = { workspace = true }
[target.'cfg(all(unix, not(target_os = "macos"), not(target_os = "android")))'.dependencies]
pwd = { workspace = true }
pwd = { workspace = true }

View File

@ -1,4 +1,5 @@
use super::helpers;
#[cfg(windows)]
use omnipath::WinPathExt;
use std::path::{Component, Path, PathBuf};
/// Normalize the path, expanding occurrences of n-dots.
@ -63,7 +64,18 @@ pub fn expand_dots(path: impl AsRef<Path>) -> PathBuf {
}
}
helpers::simiplified(&result)
simiplified(&result)
}
#[cfg(windows)]
fn simiplified(path: &std::path::Path) -> PathBuf {
path.to_winuser_path()
.unwrap_or_else(|_| path.to_path_buf())
}
#[cfg(not(windows))]
fn simiplified(path: &std::path::Path) -> PathBuf {
path.to_path_buf()
}
#[cfg(test)]

View File

@ -1,8 +1,9 @@
#[cfg(windows)]
use omnipath::WinPathExt;
use std::io;
use std::path::{Path, PathBuf};
use super::dots::{expand_dots, expand_ndots};
use super::helpers;
use super::tilde::expand_tilde;
// Join a path relative to another path. Paths starting with tilde are considered as absolute.
@ -30,8 +31,17 @@ where
fn canonicalize(path: impl AsRef<Path>) -> io::Result<PathBuf> {
let path = expand_tilde(path);
let path = expand_ndots(path);
canonicalize_path(&path)
}
helpers::canonicalize(&path)
#[cfg(windows)]
fn canonicalize_path(path: &std::path::Path) -> std::io::Result<std::path::PathBuf> {
path.canonicalize()?.to_winuser_path()
}
#[cfg(not(windows))]
fn canonicalize_path(path: &std::path::Path) -> std::io::Result<std::path::PathBuf> {
path.canonicalize()
}
/// Resolve all symbolic links and all components (tilde, ., .., ...+) and return the path in its

View File

@ -1,59 +1,32 @@
#[cfg(windows)]
use omnipath::WinPathExt;
use std::path::PathBuf;
use crate::AbsolutePathBuf;
pub fn home_dir() -> Option<PathBuf> {
dirs::home_dir()
pub fn home_dir() -> Option<AbsolutePathBuf> {
dirs::home_dir().and_then(|home| AbsolutePathBuf::try_from(home).ok())
}
/// Return the data directory for the current platform or XDG_DATA_HOME if specified.
pub fn data_dir() -> Option<PathBuf> {
match std::env::var("XDG_DATA_HOME").map(PathBuf::from) {
Ok(xdg_data) if xdg_data.is_absolute() => Some(canonicalize(&xdg_data).unwrap_or(xdg_data)),
_ => get_canonicalized_path(dirs::data_dir()),
}
pub fn data_dir() -> Option<AbsolutePathBuf> {
std::env::var("XDG_DATA_HOME")
.ok()
.and_then(|path| AbsolutePathBuf::try_from(path).ok())
.or_else(|| dirs::data_dir().and_then(|path| AbsolutePathBuf::try_from(path).ok()))
.map(|path| path.canonicalize().map(Into::into).unwrap_or(path))
}
/// Return the cache directory for the current platform or XDG_CACHE_HOME if specified.
pub fn cache_dir() -> Option<PathBuf> {
match std::env::var("XDG_CACHE_HOME").map(PathBuf::from) {
Ok(xdg_cache) if xdg_cache.is_absolute() => {
Some(canonicalize(&xdg_cache).unwrap_or(xdg_cache))
}
_ => get_canonicalized_path(dirs::cache_dir()),
}
pub fn cache_dir() -> Option<AbsolutePathBuf> {
std::env::var("XDG_CACHE_HOME")
.ok()
.and_then(|path| AbsolutePathBuf::try_from(path).ok())
.or_else(|| dirs::cache_dir().and_then(|path| AbsolutePathBuf::try_from(path).ok()))
.map(|path| path.canonicalize().map(Into::into).unwrap_or(path))
}
/// Return the config directory for the current platform or XDG_CONFIG_HOME if specified.
pub fn config_dir() -> Option<PathBuf> {
match std::env::var("XDG_CONFIG_HOME").map(PathBuf::from) {
Ok(xdg_config) if xdg_config.is_absolute() => {
Some(canonicalize(&xdg_config).unwrap_or(xdg_config))
}
_ => get_canonicalized_path(dirs::config_dir()),
}
}
pub fn get_canonicalized_path(path: Option<PathBuf>) -> Option<PathBuf> {
let path = path?;
Some(canonicalize(&path).unwrap_or(path))
}
#[cfg(windows)]
pub fn canonicalize(path: &std::path::Path) -> std::io::Result<std::path::PathBuf> {
path.canonicalize()?.to_winuser_path()
}
#[cfg(not(windows))]
pub fn canonicalize(path: &std::path::Path) -> std::io::Result<std::path::PathBuf> {
path.canonicalize()
}
#[cfg(windows)]
pub fn simiplified(path: &std::path::Path) -> PathBuf {
path.to_winuser_path()
.unwrap_or_else(|_| path.to_path_buf())
}
#[cfg(not(windows))]
pub fn simiplified(path: &std::path::Path) -> PathBuf {
path.to_path_buf()
pub fn config_dir() -> Option<AbsolutePathBuf> {
std::env::var("XDG_CONFIG_HOME")
.ok()
.and_then(|path| AbsolutePathBuf::try_from(path).ok())
.or_else(|| dirs::config_dir().and_then(|path| AbsolutePathBuf::try_from(path).ok()))
.map(|path| path.canonicalize().map(Into::into).unwrap_or(path))
}

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