Merge branch 'nushell:main' into main

This commit is contained in:
anomius 2024-11-29 23:01:28 +05:30 committed by GitHub
commit 8f298cd1ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
131 changed files with 5277 additions and 3470 deletions

View file

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

1299
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -71,7 +71,7 @@ brotli = "6.0"
byteorder = "1.5" byteorder = "1.5"
bytes = "1" bytes = "1"
bytesize = "1.3" bytesize = "1.3"
calamine = "0.24.0" calamine = "0.26.1"
chardetng = "0.1.17" chardetng = "0.1.17"
chrono = { default-features = false, version = "0.4.34" } chrono = { default-features = false, version = "0.4.34" }
chrono-humanize = "0.2.3" chrono-humanize = "0.2.3"
@ -106,7 +106,7 @@ lsp-server = "0.7.5"
lsp-types = { version = "0.95.0", features = ["proposed"] } lsp-types = { version = "0.95.0", features = ["proposed"] }
mach2 = "0.4" mach2 = "0.4"
md5 = { version = "0.10", package = "md-5" } md5 = { version = "0.10", package = "md-5" }
miette = "7.2" miette = "7.3"
mime = "0.3.17" mime = "0.3.17"
mime_guess = "2.0" mime_guess = "2.0"
mockito = { version = "1.6", default-features = false } mockito = { version = "1.6", default-features = false }
@ -129,7 +129,7 @@ proc-macro-error = { version = "1.0", default-features = false }
proc-macro2 = "1.0" proc-macro2 = "1.0"
procfs = "0.16.0" procfs = "0.16.0"
pwd = "1.3" pwd = "1.3"
quick-xml = "0.32.0" quick-xml = "0.37.0"
quickcheck = "1.0" quickcheck = "1.0"
quickcheck_macros = "1.0" quickcheck_macros = "1.0"
quote = "1.0" quote = "1.0"
@ -156,22 +156,23 @@ syn = "2.0"
sysinfo = "0.32" sysinfo = "0.32"
tabled = { version = "0.16.0", default-features = false } tabled = { version = "0.16.0", default-features = false }
tempfile = "3.14" tempfile = "3.14"
terminal_size = "0.3" terminal_size = "0.4"
titlecase = "2.0" titlecase = "2.0"
toml = "0.8" toml = "0.8"
trash = "5.2" trash = "5.2"
umask = "2.1" umask = "2.1"
unicode-segmentation = "1.12" unicode-segmentation = "1.12"
unicode-width = "0.1" unicode-width = "0.2"
ureq = { version = "2.10", default-features = false } ureq = { version = "2.10", default-features = false }
url = "2.2" url = "2.2"
uu_cp = "0.0.27" uu_cp = "0.0.28"
uu_mkdir = "0.0.27" uu_mkdir = "0.0.28"
uu_mktemp = "0.0.27" uu_mktemp = "0.0.28"
uu_mv = "0.0.27" uu_mv = "0.0.28"
uu_whoami = "0.0.27" uu_touch = "0.0.28"
uu_uname = "0.0.27" uu_whoami = "0.0.28"
uucore = "0.0.27" uu_uname = "0.0.28"
uucore = "0.0.28"
uuid = "1.11.0" uuid = "1.11.0"
v_htmlescape = "0.15.0" v_htmlescape = "0.15.0"
wax = "0.6" wax = "0.6"
@ -313,7 +314,7 @@ bench = false
# To use a development version of a dependency please use a global override here # To use a development version of a dependency please use a global override here
# changing versions in each sub-crate of the workspace is tedious # changing versions in each sub-crate of the workspace is tedious
[patch.crates-io] [patch.crates-io]
# reedline = { git = "https://github.com/nushell/reedline", branch = "main" } reedline = { git = "https://github.com/nushell/reedline", branch = "main" }
# nu-ansi-term = {git = "https://github.com/nushell/nu-ansi-term.git", branch = "main"} # nu-ansi-term = {git = "https://github.com/nushell/nu-ansi-term.git", branch = "main"}
# Run all benchmarks with `cargo bench` # Run all benchmarks with `cargo bench`

View file

@ -58,7 +58,7 @@ For details about which platforms the Nushell team actively supports, see [our p
## Configuration ## Configuration
The default configurations can be found at [sample_config](crates/nu-utils/src/sample_config) The default configurations can be found at [sample_config](crates/nu-utils/src/default_files)
which are the configuration files one gets when they startup Nushell for the first time. which are the configuration files one gets when they startup Nushell for the first time.
It sets all of the default configuration to run Nushell. From here one can It sets all of the default configuration to run Nushell. From here one can

View file

@ -1,5 +1,7 @@
use std::collections::HashMap;
use crate::{ use crate::{
completions::{Completer, CompletionOptions, MatchAlgorithm}, completions::{Completer, CompletionOptions},
SuggestionKind, SuggestionKind,
}; };
use nu_parser::FlatShape; use nu_parser::FlatShape;
@ -9,7 +11,7 @@ use nu_protocol::{
}; };
use reedline::Suggestion; use reedline::Suggestion;
use super::{completion_common::sort_suggestions, SemanticSuggestion}; use super::{completion_options::NuMatcher, SemanticSuggestion};
pub struct CommandCompletion { pub struct CommandCompletion {
flattened: Vec<(Span, FlatShape)>, flattened: Vec<(Span, FlatShape)>,
@ -33,10 +35,11 @@ impl CommandCompletion {
fn external_command_completion( fn external_command_completion(
&self, &self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
prefix: &str, sugg_span: reedline::Span,
match_algorithm: MatchAlgorithm, matched_internal: impl Fn(&str) -> bool,
) -> Vec<String> { matcher: &mut NuMatcher<String>,
let mut executables = vec![]; ) -> HashMap<String, SemanticSuggestion> {
let mut suggs = HashMap::new();
// os agnostic way to get the PATH env var // os agnostic way to get the PATH env var
let paths = working_set.permanent_state.get_path_env_var(); let paths = working_set.permanent_state.get_path_env_var();
@ -54,24 +57,38 @@ impl CommandCompletion {
.completions .completions
.external .external
.max_results .max_results
> executables.len() as i64 <= suggs.len() as i64
&& !executables.contains(
&item
.path()
.file_name()
.map(|x| x.to_string_lossy().to_string())
.unwrap_or_default(),
)
&& matches!(
item.path().file_name().map(|x| match_algorithm
.matches_str(&x.to_string_lossy(), prefix)),
Some(true)
)
&& is_executable::is_executable(item.path())
{ {
if let Ok(name) = item.file_name().into_string() { break;
executables.push(name);
} }
let Ok(name) = item.file_name().into_string() else {
continue;
};
let value = if matched_internal(&name) {
format!("^{}", name)
} else {
name.clone()
};
if suggs.contains_key(&value) {
continue;
}
if matcher.matches(&name) && is_executable::is_executable(item.path()) {
// If there's an internal command with the same name, adds ^cmd to the
// matcher so that both the internal and external command are included
matcher.add(&name, value.clone());
suggs.insert(
value.clone(),
SemanticSuggestion {
suggestion: Suggestion {
value,
span: sugg_span,
append_whitespace: true,
..Default::default()
},
// TODO: is there a way to create a test?
kind: None,
},
);
} }
} }
} }
@ -79,7 +96,7 @@ impl CommandCompletion {
} }
} }
executables suggs
} }
fn complete_commands( fn complete_commands(
@ -88,69 +105,60 @@ impl CommandCompletion {
span: Span, span: Span,
offset: usize, offset: usize,
find_externals: bool, find_externals: bool,
match_algorithm: MatchAlgorithm, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let partial = working_set.get_span_contents(span); let partial = working_set.get_span_contents(span);
let mut matcher = NuMatcher::new(String::from_utf8_lossy(partial), options.clone());
let filter_predicate = |command: &[u8]| match_algorithm.matches_u8(command, partial); let sugg_span = reedline::Span::new(span.start - offset, span.end - offset);
let mut results = working_set let mut internal_suggs = HashMap::new();
.find_commands_by_predicate(filter_predicate, true) let filtered_commands = working_set.find_commands_by_predicate(
.into_iter() |name| {
.map(move |x| SemanticSuggestion { let name = String::from_utf8_lossy(name);
matcher.add(&name, name.to_string())
},
true,
);
for (name, description, typ) in filtered_commands {
let name = String::from_utf8_lossy(&name);
internal_suggs.insert(
name.to_string(),
SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: String::from_utf8_lossy(&x.0).to_string(), value: name.to_string(),
description: x.1, description,
span: reedline::Span::new(span.start - offset, span.end - offset), span: sugg_span,
append_whitespace: true, append_whitespace: true,
..Suggestion::default() ..Suggestion::default()
}, },
kind: Some(SuggestionKind::Command(x.2)), kind: Some(SuggestionKind::Command(typ)),
})
.collect::<Vec<_>>();
let partial = working_set.get_span_contents(span);
let partial = String::from_utf8_lossy(partial).to_string();
if find_externals {
let results_external = self
.external_command_completion(working_set, &partial, match_algorithm)
.into_iter()
.map(move |x| SemanticSuggestion {
suggestion: Suggestion {
value: x,
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, }
});
let results_strings: Vec<String> = let mut external_suggs = if find_externals {
results.iter().map(|x| x.suggestion.value.clone()).collect(); self.external_command_completion(
working_set,
for external in results_external { sugg_span,
if results_strings.contains(&external.suggestion.value) { |name| internal_suggs.contains_key(name),
results.push(SemanticSuggestion { &mut matcher,
suggestion: Suggestion { )
value: format!("^{}", external.suggestion.value),
span: external.suggestion.span,
append_whitespace: true,
..Suggestion::default()
},
kind: external.kind,
})
} else { } else {
results.push(external) HashMap::new()
} };
}
results let mut res = Vec::new();
} else { for cmd_name in matcher.results() {
results if let Some(sugg) = internal_suggs
.remove(&cmd_name)
.or_else(|| external_suggs.remove(&cmd_name))
{
res.push(sugg);
} }
} }
res
}
} }
impl Completer for CommandCompletion { impl Completer for CommandCompletion {
@ -158,7 +166,7 @@ impl Completer for CommandCompletion {
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
_stack: &Stack, _stack: &Stack,
prefix: &[u8], _prefix: &[u8],
span: Span, span: Span,
offset: usize, offset: usize,
pos: usize, pos: usize,
@ -188,18 +196,18 @@ impl Completer for CommandCompletion {
Span::new(last.0.start, pos), Span::new(last.0.start, pos),
offset, offset,
false, false,
options.match_algorithm, options,
) )
} else { } else {
vec![] vec![]
}; };
if !subcommands.is_empty() { if !subcommands.is_empty() {
return sort_suggestions(&String::from_utf8_lossy(prefix), subcommands, options); return subcommands;
} }
let config = working_set.get_config(); let config = working_set.get_config();
let commands = if matches!(self.flat_shape, nu_parser::FlatShape::External) if matches!(self.flat_shape, nu_parser::FlatShape::External)
|| matches!(self.flat_shape, nu_parser::FlatShape::InternalCall(_)) || matches!(self.flat_shape, nu_parser::FlatShape::InternalCall(_))
|| ((span.end - span.start) == 0) || ((span.end - span.start) == 0)
|| is_passthrough_command(working_set.delta.get_file_contents()) || is_passthrough_command(working_set.delta.get_file_contents())
@ -214,13 +222,11 @@ impl Completer for CommandCompletion {
span, span,
offset, offset,
config.completions.external.enable, config.completions.external.enable,
options.match_algorithm, options,
) )
} else { } else {
vec![] vec![]
}; }
sort_suggestions(&String::from_utf8_lossy(prefix), commands, options)
} }
} }

View file

@ -1,22 +1,20 @@
use super::MatchAlgorithm; use super::{completion_options::NuMatcher, MatchAlgorithm};
use crate::{ use crate::completions::CompletionOptions;
completions::{matches, CompletionOptions},
SemanticSuggestion,
};
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use nu_ansi_term::Style; use nu_ansi_term::Style;
use nu_engine::env_to_string; use nu_engine::env_to_string;
use nu_path::dots::expand_ndots; use nu_path::dots::expand_ndots;
use nu_path::{expand_to_real_path, home_dir}; use nu_path::{expand_to_real_path, home_dir};
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
CompletionSort, Span, Span,
}; };
use nu_utils::get_ls_colors; use nu_utils::get_ls_colors;
use nu_utils::IgnoreCaseExt;
use std::path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP}; use std::path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP};
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct PathBuiltFromString { pub struct PathBuiltFromString {
cwd: PathBuf,
parts: Vec<String>, parts: Vec<String>,
isdir: bool, isdir: bool,
} }
@ -30,35 +28,41 @@ pub struct PathBuiltFromString {
/// want_directory: Whether we want only directories as completion matches. /// want_directory: Whether we want only directories as completion matches.
/// Some commands like `cd` can only be run on directories whereas others /// Some commands like `cd` can only be run on directories whereas others
/// like `ls` can be run on regular files as well. /// like `ls` can be run on regular files as well.
pub fn complete_rec( fn complete_rec(
partial: &[&str], partial: &[&str],
built: &PathBuiltFromString, built_paths: &[PathBuiltFromString],
cwd: &Path,
options: &CompletionOptions, options: &CompletionOptions,
want_directory: bool, want_directory: bool,
isdir: bool, isdir: bool,
) -> Vec<PathBuiltFromString> { ) -> Vec<PathBuiltFromString> {
let mut completions = vec![];
if let Some((&base, rest)) = partial.split_first() { if let Some((&base, rest)) = partial.split_first() {
if base.chars().all(|c| c == '.') && (isdir || !rest.is_empty()) { if base.chars().all(|c| c == '.') && (isdir || !rest.is_empty()) {
let built_paths: Vec<_> = built_paths
.iter()
.map(|built| {
let mut built = built.clone(); let mut built = built.clone();
built.parts.push(base.to_string()); built.parts.push(base.to_string());
built.isdir = true; built.isdir = true;
return complete_rec(rest, &built, cwd, options, want_directory, isdir); built
})
.collect();
return complete_rec(rest, &built_paths, options, want_directory, isdir);
} }
} }
let mut built_path = cwd.to_path_buf(); let prefix = partial.first().unwrap_or(&"");
let mut matcher = NuMatcher::new(prefix, options.clone());
for built in built_paths {
let mut path = built.cwd.clone();
for part in &built.parts { for part in &built.parts {
built_path.push(part); path.push(part);
} }
let Ok(result) = built_path.read_dir() else { let Ok(result) = path.read_dir() else {
return completions; continue;
}; };
let mut entries = Vec::new();
for entry in result.filter_map(|e| e.ok()) { for entry in result.filter_map(|e| e.ok()) {
let entry_name = entry.file_name().to_string_lossy().into_owned(); let entry_name = entry.file_name().to_string_lossy().into_owned();
let entry_isdir = entry.path().is_dir(); let entry_isdir = entry.path().is_dir();
@ -67,17 +71,15 @@ pub fn complete_rec(
built.isdir = entry_isdir; built.isdir = entry_isdir;
if !want_directory || entry_isdir { if !want_directory || entry_isdir {
entries.push((entry_name, built)); matcher.add(entry_name.clone(), (entry_name, built));
}
} }
} }
let prefix = partial.first().unwrap_or(&""); let mut completions = vec![];
let sorted_entries = sort_completions(prefix, entries, options, |(entry, _)| entry); for (entry_name, built) in matcher.results() {
for (entry_name, built) in sorted_entries {
match partial.split_first() { match partial.split_first() {
Some((base, rest)) => { Some((base, rest)) => {
if matches(base, &entry_name, options) {
// We use `isdir` to confirm that the current component has // We use `isdir` to confirm that the current component has
// at least one next component or a slash. // at least one next component or a slash.
// Serves as confirmation to ignore longer completions for // Serves as confirmation to ignore longer completions for
@ -85,8 +87,7 @@ pub fn complete_rec(
if !rest.is_empty() || isdir { if !rest.is_empty() || isdir {
completions.extend(complete_rec( completions.extend(complete_rec(
rest, rest,
&built, &[built],
cwd,
options, options,
want_directory, want_directory,
isdir, isdir,
@ -94,14 +95,19 @@ pub fn complete_rec(
} else { } else {
completions.push(built); completions.push(built);
} }
}
if entry_name.eq(base) // For https://github.com/nushell/nushell/issues/13204
&& matches!(options.match_algorithm, MatchAlgorithm::Prefix) if isdir && options.match_algorithm == MatchAlgorithm::Prefix {
&& isdir let exact_match = if options.case_sensitive {
{ entry_name.eq(base)
} else {
entry_name.to_folded_case().eq(&base.to_folded_case())
};
if exact_match {
break; break;
} }
} }
}
None => { None => {
completions.push(built); completions.push(built);
} }
@ -147,15 +153,25 @@ fn surround_remove(partial: &str) -> String {
partial.to_string() partial.to_string()
} }
pub struct FileSuggestion {
pub span: nu_protocol::Span,
pub path: String,
pub style: Option<Style>,
pub cwd: PathBuf,
}
/// # Parameters
/// * `cwds` - A list of directories in which to search. The only reason this isn't a single string
/// is because dotnu_completions searches in multiple directories at once
pub fn complete_item( pub fn complete_item(
want_directory: bool, want_directory: bool,
span: nu_protocol::Span, span: nu_protocol::Span,
partial: &str, partial: &str,
cwd: &str, cwds: &[impl AsRef<str>],
options: &CompletionOptions, options: &CompletionOptions,
engine_state: &EngineState, engine_state: &EngineState,
stack: &Stack, stack: &Stack,
) -> Vec<(nu_protocol::Span, String, Option<Style>)> { ) -> Vec<FileSuggestion> {
let cleaned_partial = surround_remove(partial); let cleaned_partial = surround_remove(partial);
let isdir = cleaned_partial.ends_with(is_separator); let isdir = cleaned_partial.ends_with(is_separator);
let expanded_partial = expand_ndots(Path::new(&cleaned_partial)); let expanded_partial = expand_ndots(Path::new(&cleaned_partial));
@ -175,7 +191,10 @@ pub fn complete_item(
partial.push_str(&format!("{path_separator}.")); partial.push_str(&format!("{path_separator}."));
} }
let cwd_pathbuf = Path::new(cwd).to_path_buf(); let cwd_pathbufs: Vec<_> = cwds
.iter()
.map(|cwd| Path::new(cwd.as_ref()).to_path_buf())
.collect();
let ls_colors = (engine_state.config.completions.use_ls_colors let ls_colors = (engine_state.config.completions.use_ls_colors
&& engine_state.config.use_ansi_coloring) && engine_state.config.use_ansi_coloring)
.then(|| { .then(|| {
@ -186,7 +205,7 @@ pub fn complete_item(
get_ls_colors(ls_colors_env_str) get_ls_colors(ls_colors_env_str)
}); });
let mut cwd = cwd_pathbuf.clone(); let mut cwds = cwd_pathbufs.clone();
let mut prefix_len = 0; let mut prefix_len = 0;
let mut original_cwd = OriginalCwd::None; let mut original_cwd = OriginalCwd::None;
@ -194,19 +213,21 @@ pub fn complete_item(
match components.peek().cloned() { match components.peek().cloned() {
Some(c @ Component::Prefix(..)) => { Some(c @ Component::Prefix(..)) => {
// windows only by definition // windows only by definition
cwd = [c, Component::RootDir].iter().collect(); cwds = vec![[c, Component::RootDir].iter().collect()];
prefix_len = c.as_os_str().len(); prefix_len = c.as_os_str().len();
original_cwd = OriginalCwd::Prefix(c.as_os_str().to_string_lossy().into_owned()); original_cwd = OriginalCwd::Prefix(c.as_os_str().to_string_lossy().into_owned());
} }
Some(c @ Component::RootDir) => { Some(c @ Component::RootDir) => {
// This is kind of a hack. When joining an empty string with the rest, // This is kind of a hack. When joining an empty string with the rest,
// we add the slash automagically // we add the slash automagically
cwd = PathBuf::from(c.as_os_str()); cwds = vec![PathBuf::from(c.as_os_str())];
prefix_len = 1; prefix_len = 1;
original_cwd = OriginalCwd::Prefix(String::new()); original_cwd = OriginalCwd::Prefix(String::new());
} }
Some(Component::Normal(home)) if home.to_string_lossy() == "~" => { Some(Component::Normal(home)) if home.to_string_lossy() == "~" => {
cwd = home_dir().map(Into::into).unwrap_or(cwd_pathbuf); cwds = home_dir()
.map(|dir| vec![dir.into()])
.unwrap_or(cwd_pathbufs);
prefix_len = 1; prefix_len = 1;
original_cwd = OriginalCwd::Home; original_cwd = OriginalCwd::Home;
} }
@ -223,8 +244,14 @@ pub fn complete_item(
complete_rec( complete_rec(
partial.as_slice(), partial.as_slice(),
&PathBuiltFromString::default(), &cwds
&cwd, .into_iter()
.map(|cwd| PathBuiltFromString {
cwd,
parts: Vec::new(),
isdir: false,
})
.collect::<Vec<_>>(),
options, options,
want_directory, want_directory,
isdir, isdir,
@ -234,6 +261,7 @@ pub fn complete_item(
if should_collapse_dots { if should_collapse_dots {
p = collapse_ndots(p); p = collapse_ndots(p);
} }
let cwd = p.cwd.clone();
let path = original_cwd.apply(p, path_separator); let path = original_cwd.apply(p, path_separator);
let style = ls_colors.as_ref().map(|lsc| { let style = ls_colors.as_ref().map(|lsc| {
lsc.style_for_path_with_metadata( lsc.style_for_path_with_metadata(
@ -245,7 +273,12 @@ pub fn complete_item(
.map(lscolors::Style::to_nu_ansi_term_style) .map(lscolors::Style::to_nu_ansi_term_style)
.unwrap_or_default() .unwrap_or_default()
}); });
(span, escape_path(path, want_directory), style) FileSuggestion {
span,
path: escape_path(path, want_directory),
style,
cwd,
}
}) })
.collect() .collect()
} }
@ -310,45 +343,6 @@ pub fn adjust_if_intermediate(
} }
} }
/// Convenience function to sort suggestions using [`sort_completions`]
pub fn sort_suggestions(
prefix: &str,
items: Vec<SemanticSuggestion>,
options: &CompletionOptions,
) -> Vec<SemanticSuggestion> {
sort_completions(prefix, items, options, |it| &it.suggestion.value)
}
/// # Arguments
/// * `prefix` - What the user's typed, for sorting by fuzzy matcher score
pub fn sort_completions<T>(
prefix: &str,
mut items: Vec<T>,
options: &CompletionOptions,
get_value: fn(&T) -> &str,
) -> Vec<T> {
// Sort items
if options.sort == CompletionSort::Smart && options.match_algorithm == MatchAlgorithm::Fuzzy {
let mut matcher = SkimMatcherV2::default();
if options.case_sensitive {
matcher = matcher.respect_case();
} else {
matcher = matcher.ignore_case();
};
items.sort_unstable_by(|a, b| {
let a_str = get_value(a);
let b_str = get_value(b);
let a_score = matcher.fuzzy_match(a_str, prefix).unwrap_or_default();
let b_score = matcher.fuzzy_match(b_str, prefix).unwrap_or_default();
b_score.cmp(&a_score).then(a_str.cmp(b_str))
});
} else {
items.sort_unstable_by(|a, b| get_value(a).cmp(get_value(b)));
}
items
}
/// Collapse multiple ".." components into n-dots. /// Collapse multiple ".." components into n-dots.
/// ///
/// It performs the reverse operation of `expand_ndots`, collapsing sequences of ".." into n-dots, /// It performs the reverse operation of `expand_ndots`, collapsing sequences of ".." into n-dots,
@ -359,6 +353,7 @@ fn collapse_ndots(path: PathBuiltFromString) -> PathBuiltFromString {
let mut result = PathBuiltFromString { let mut result = PathBuiltFromString {
parts: Vec::with_capacity(path.parts.len()), parts: Vec::with_capacity(path.parts.len()),
isdir: path.isdir, isdir: path.isdir,
cwd: path.cwd,
}; };
let mut dot_count = 0; let mut dot_count = 0;

View file

@ -1,7 +1,10 @@
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use nu_parser::trim_quotes_str; use nu_parser::trim_quotes_str;
use nu_protocol::{CompletionAlgorithm, CompletionSort}; use nu_protocol::{CompletionAlgorithm, CompletionSort};
use std::fmt::Display; use nu_utils::IgnoreCaseExt;
use std::{borrow::Cow, fmt::Display};
use super::SemanticSuggestion;
/// Describes how suggestions should be matched. /// Describes how suggestions should be matched.
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
@ -19,32 +22,153 @@ pub enum MatchAlgorithm {
Fuzzy, Fuzzy,
} }
impl MatchAlgorithm { pub struct NuMatcher<T> {
/// Returns whether the `needle` search text matches the given `haystack`. options: CompletionOptions,
pub fn matches_str(&self, haystack: &str, needle: &str) -> bool { needle: String,
state: State<T>,
}
enum State<T> {
Prefix {
/// Holds (haystack, item)
items: Vec<(String, T)>,
},
Fuzzy {
matcher: Box<SkimMatcherV2>,
/// Holds (haystack, item, score)
items: Vec<(String, T, i64)>,
},
}
/// Filters and sorts suggestions
impl<T> NuMatcher<T> {
/// # Arguments
///
/// * `needle` - The text to search for
pub fn new(needle: impl AsRef<str>, options: CompletionOptions) -> NuMatcher<T> {
let orig_needle = trim_quotes_str(needle.as_ref());
let lowercase_needle = if options.case_sensitive {
orig_needle.to_owned()
} else {
orig_needle.to_folded_case()
};
match options.match_algorithm {
MatchAlgorithm::Prefix => NuMatcher {
options,
needle: lowercase_needle,
state: State::Prefix { items: Vec::new() },
},
MatchAlgorithm::Fuzzy => {
let mut matcher = SkimMatcherV2::default();
if options.case_sensitive {
matcher = matcher.respect_case();
} else {
matcher = matcher.ignore_case();
};
NuMatcher {
options,
needle: orig_needle.to_owned(),
state: State::Fuzzy {
matcher: Box::new(matcher),
items: Vec::new(),
},
}
}
}
}
/// Returns whether or not the haystack matches the needle. If it does, `item` is added
/// to the list of matches (if given).
///
/// Helper to avoid code duplication between [NuMatcher::add] and [NuMatcher::matches].
fn matches_aux(&mut self, haystack: &str, item: Option<T>) -> bool {
let haystack = trim_quotes_str(haystack); let haystack = trim_quotes_str(haystack);
let needle = trim_quotes_str(needle); match &mut self.state {
match *self { State::Prefix { items } => {
MatchAlgorithm::Prefix => haystack.starts_with(needle), let haystack_folded = if self.options.case_sensitive {
MatchAlgorithm::Fuzzy => { Cow::Borrowed(haystack)
let matcher = SkimMatcherV2::default(); } else {
matcher.fuzzy_match(haystack, needle).is_some() Cow::Owned(haystack.to_folded_case())
};
let matches = if self.options.positional {
haystack_folded.starts_with(self.needle.as_str())
} else {
haystack_folded.contains(self.needle.as_str())
};
if matches {
if let Some(item) = item {
items.push((haystack.to_string(), item));
}
}
matches
}
State::Fuzzy { items, matcher } => {
let Some(score) = matcher.fuzzy_match(haystack, &self.needle) else {
return false;
};
if let Some(item) = item {
items.push((haystack.to_string(), item, score));
}
true
} }
} }
} }
/// Returns whether the `needle` search text matches the given `haystack`. /// Add the given item if the given haystack matches the needle.
pub fn matches_u8(&self, haystack: &[u8], needle: &[u8]) -> bool { ///
match *self { /// Returns whether the item was added.
MatchAlgorithm::Prefix => haystack.starts_with(needle), pub fn add(&mut self, haystack: impl AsRef<str>, item: T) -> bool {
MatchAlgorithm::Fuzzy => { self.matches_aux(haystack.as_ref(), Some(item))
let haystack_str = String::from_utf8_lossy(haystack); }
let needle_str = String::from_utf8_lossy(needle);
let matcher = SkimMatcherV2::default(); /// Returns whether the haystack matches the needle.
matcher.fuzzy_match(&haystack_str, &needle_str).is_some() pub fn matches(&mut self, haystack: &str) -> bool {
self.matches_aux(haystack, None)
}
/// Get all the items that matched (sorted)
pub fn results(self) -> Vec<T> {
match self.state {
State::Prefix { mut items, .. } => {
items.sort_by(|(haystack1, _), (haystack2, _)| {
let cmp_sensitive = haystack1.cmp(haystack2);
if self.options.case_sensitive {
cmp_sensitive
} else {
haystack1
.to_folded_case()
.cmp(&haystack2.to_folded_case())
.then(cmp_sensitive)
}
});
items.into_iter().map(|(_, item)| item).collect::<Vec<_>>()
}
State::Fuzzy { mut items, .. } => {
match self.options.sort {
CompletionSort::Alphabetical => {
items.sort_by(|(haystack1, _, _), (haystack2, _, _)| {
haystack1.cmp(haystack2)
});
}
CompletionSort::Smart => {
items.sort_by(|(haystack1, _, score1), (haystack2, _, score2)| {
score2.cmp(score1).then(haystack1.cmp(haystack2))
});
} }
} }
items
.into_iter()
.map(|(_, item, _)| item)
.collect::<Vec<_>>()
}
}
}
}
impl NuMatcher<SemanticSuggestion> {
pub fn add_semantic_suggestion(&mut self, sugg: SemanticSuggestion) -> bool {
let value = sugg.suggestion.value.to_string();
self.add(value, sugg)
} }
} }
@ -105,35 +229,49 @@ impl Default for CompletionOptions {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::MatchAlgorithm; use rstest::rstest;
#[test] use super::{CompletionOptions, MatchAlgorithm, NuMatcher};
fn match_algorithm_prefix() {
let algorithm = MatchAlgorithm::Prefix;
assert!(algorithm.matches_str("example text", "")); #[rstest]
assert!(algorithm.matches_str("example text", "examp")); #[case(MatchAlgorithm::Prefix, "example text", "", true)]
assert!(!algorithm.matches_str("example text", "text")); #[case(MatchAlgorithm::Prefix, "example text", "examp", true)]
#[case(MatchAlgorithm::Prefix, "example text", "text", false)]
assert!(algorithm.matches_u8(&[1, 2, 3], &[])); #[case(MatchAlgorithm::Fuzzy, "example text", "", true)]
assert!(algorithm.matches_u8(&[1, 2, 3], &[1, 2])); #[case(MatchAlgorithm::Fuzzy, "example text", "examp", true)]
assert!(!algorithm.matches_u8(&[1, 2, 3], &[2, 3])); #[case(MatchAlgorithm::Fuzzy, "example text", "ext", true)]
#[case(MatchAlgorithm::Fuzzy, "example text", "mplxt", true)]
#[case(MatchAlgorithm::Fuzzy, "example text", "mpp", false)]
fn match_algorithm_simple(
#[case] match_algorithm: MatchAlgorithm,
#[case] haystack: &str,
#[case] needle: &str,
#[case] should_match: bool,
) {
let options = CompletionOptions {
match_algorithm,
..Default::default()
};
let mut matcher = NuMatcher::new(needle, options);
matcher.add(haystack, haystack);
if should_match {
assert_eq!(vec![haystack], matcher.results());
} else {
assert_ne!(vec![haystack], matcher.results());
}
} }
#[test] #[test]
fn match_algorithm_fuzzy() { fn match_algorithm_fuzzy_sort_score() {
let algorithm = MatchAlgorithm::Fuzzy; let options = CompletionOptions {
match_algorithm: MatchAlgorithm::Fuzzy,
assert!(algorithm.matches_str("example text", "")); ..Default::default()
assert!(algorithm.matches_str("example text", "examp")); };
assert!(algorithm.matches_str("example text", "ext")); let mut matcher = NuMatcher::new("fob", options);
assert!(algorithm.matches_str("example text", "mplxt")); for item in ["foo/bar", "fob", "foo bar"] {
assert!(!algorithm.matches_str("example text", "mpp")); matcher.add(item, item);
}
assert!(algorithm.matches_u8(&[1, 2, 3], &[])); // Sort by score, then in alphabetical order
assert!(algorithm.matches_u8(&[1, 2, 3], &[1, 2])); assert_eq!(vec!["fob", "foo bar", "foo/bar"], matcher.results());
assert!(algorithm.matches_u8(&[1, 2, 3], &[2, 3]));
assert!(algorithm.matches_u8(&[1, 2, 3], &[1, 3]));
assert!(!algorithm.matches_u8(&[1, 2, 3], &[2, 2]));
} }
} }

View file

@ -9,10 +9,9 @@ use nu_protocol::{
engine::{Stack, StateWorkingSet}, engine::{Stack, StateWorkingSet},
CompletionSort, DeclId, PipelineData, Span, Type, Value, CompletionSort, DeclId, PipelineData, Span, Type, Value,
}; };
use nu_utils::IgnoreCaseExt;
use std::collections::HashMap; use std::collections::HashMap;
use super::completion_common::sort_suggestions; use super::completion_options::NuMatcher;
pub struct CustomCompletion { pub struct CustomCompletion {
stack: Stack, stack: Stack,
@ -123,41 +122,11 @@ impl Completer for CustomCompletion {
}) })
.unwrap_or_default(); .unwrap_or_default();
let options = custom_completion_options let options = custom_completion_options.unwrap_or(completion_options.clone());
.as_ref() let mut matcher = NuMatcher::new(String::from_utf8_lossy(prefix), options);
.unwrap_or(completion_options); for sugg in suggestions {
let suggestions = filter(prefix, suggestions, options); matcher.add_semantic_suggestion(sugg);
sort_suggestions(&String::from_utf8_lossy(prefix), suggestions, options) }
matcher.results()
} }
} }
fn filter(
prefix: &[u8],
items: Vec<SemanticSuggestion>,
options: &CompletionOptions,
) -> Vec<SemanticSuggestion> {
items
.into_iter()
.filter(|it| match options.match_algorithm {
MatchAlgorithm::Prefix => match (options.case_sensitive, options.positional) {
(true, true) => it.suggestion.value.as_bytes().starts_with(prefix),
(true, false) => it
.suggestion
.value
.contains(std::str::from_utf8(prefix).unwrap_or("")),
(false, positional) => {
let value = it.suggestion.value.to_folded_case();
let prefix = std::str::from_utf8(prefix).unwrap_or("").to_folded_case();
if positional {
value.starts_with(&prefix)
} else {
value.contains(&prefix)
}
}
},
MatchAlgorithm::Fuzzy => options
.match_algorithm
.matches_u8(it.suggestion.value.as_bytes(), prefix),
})
.collect()
}

View file

@ -2,7 +2,6 @@ use crate::completions::{
completion_common::{adjust_if_intermediate, complete_item, AdjustView}, completion_common::{adjust_if_intermediate, complete_item, AdjustView},
Completer, CompletionOptions, Completer, CompletionOptions,
}; };
use nu_ansi_term::Style;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
Span, Span,
@ -10,7 +9,7 @@ use nu_protocol::{
use reedline::Suggestion; use reedline::Suggestion;
use std::path::Path; use std::path::Path;
use super::SemanticSuggestion; use super::{completion_common::FileSuggestion, SemanticSuggestion};
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct DirectoryCompletion {} pub struct DirectoryCompletion {}
@ -47,11 +46,11 @@ impl Completer for DirectoryCompletion {
.into_iter() .into_iter()
.map(move |x| SemanticSuggestion { .map(move |x| SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: x.1, value: x.path,
style: x.2, style: x.style,
span: reedline::Span { span: reedline::Span {
start: x.0.start - offset, start: x.span.start - offset,
end: x.0.end - offset, end: x.span.end - offset,
}, },
..Suggestion::default() ..Suggestion::default()
}, },
@ -92,6 +91,6 @@ pub fn directory_completion(
options: &CompletionOptions, options: &CompletionOptions,
engine_state: &EngineState, engine_state: &EngineState,
stack: &Stack, stack: &Stack,
) -> Vec<(nu_protocol::Span, String, Option<Style>)> { ) -> Vec<FileSuggestion> {
complete_item(true, span, partial, cwd, options, engine_state, stack) complete_item(true, span, partial, &[cwd], options, engine_state, stack)
} }

View file

@ -6,7 +6,7 @@ use nu_protocol::{
use reedline::Suggestion; use reedline::Suggestion;
use std::path::{is_separator, Path, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR}; use std::path::{is_separator, Path, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR};
use super::{completion_common::sort_suggestions, SemanticSuggestion}; use super::SemanticSuggestion;
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct DotNuCompletion {} pub struct DotNuCompletion {}
@ -87,13 +87,11 @@ impl Completer for DotNuCompletion {
// Fetch the files filtering the ones that ends with .nu // Fetch the files filtering the ones that ends with .nu
// and transform them into suggestions // and transform them into suggestions
let output: Vec<SemanticSuggestion> = search_dirs
.into_iter()
.flat_map(|search_dir| {
let completions = file_path_completion( let completions = file_path_completion(
span, span,
&partial, &partial,
&search_dir, &search_dirs.iter().map(|d| d.as_str()).collect::<Vec<_>>(),
options, options,
working_set.permanent_state, working_set.permanent_state,
stack, stack,
@ -103,23 +101,23 @@ impl Completer for DotNuCompletion {
.filter(move |it| { .filter(move |it| {
// Different base dir, so we list the .nu files or folders // Different base dir, so we list the .nu files or folders
if !is_current_folder { if !is_current_folder {
it.1.ends_with(".nu") || it.1.ends_with(SEP) it.path.ends_with(".nu") || it.path.ends_with(SEP)
} else { } else {
// Lib dirs, so we filter only the .nu files or directory modules // Lib dirs, so we filter only the .nu files or directory modules
if it.1.ends_with(SEP) { if it.path.ends_with(SEP) {
Path::new(&search_dir).join(&it.1).join("mod.nu").exists() Path::new(&it.cwd).join(&it.path).join("mod.nu").exists()
} else { } else {
it.1.ends_with(".nu") it.path.ends_with(".nu")
} }
} }
}) })
.map(move |x| SemanticSuggestion { .map(move |x| SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: x.1, value: x.path,
style: x.2, style: x.style,
span: reedline::Span { span: reedline::Span {
start: x.0.start - offset, start: x.span.start - offset,
end: x.0.end - offset, end: x.span.end - offset,
}, },
append_whitespace: true, append_whitespace: true,
..Suggestion::default() ..Suggestion::default()
@ -127,9 +125,6 @@ impl Completer for DotNuCompletion {
// TODO???? // TODO????
kind: None, kind: None,
}) })
}) .collect::<Vec<_>>()
.collect();
sort_suggestions(&prefix_str, output, options)
} }
} }

View file

@ -2,16 +2,14 @@ use crate::completions::{
completion_common::{adjust_if_intermediate, complete_item, AdjustView}, completion_common::{adjust_if_intermediate, complete_item, AdjustView},
Completer, CompletionOptions, Completer, CompletionOptions,
}; };
use nu_ansi_term::Style;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
Span, Span,
}; };
use nu_utils::IgnoreCaseExt;
use reedline::Suggestion; use reedline::Suggestion;
use std::path::Path; use std::path::Path;
use super::SemanticSuggestion; use super::{completion_common::FileSuggestion, SemanticSuggestion};
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct FileCompletion {} pub struct FileCompletion {}
@ -44,7 +42,7 @@ impl Completer for FileCompletion {
readjusted, readjusted,
span, span,
&prefix, &prefix,
&working_set.permanent_state.current_work_dir(), &[&working_set.permanent_state.current_work_dir()],
options, options,
working_set.permanent_state, working_set.permanent_state,
stack, stack,
@ -52,11 +50,11 @@ impl Completer for FileCompletion {
.into_iter() .into_iter()
.map(move |x| SemanticSuggestion { .map(move |x| SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: x.1, value: x.path,
style: x.2, style: x.style,
span: reedline::Span { span: reedline::Span {
start: x.0.start - offset, start: x.span.start - offset,
end: x.0.end - offset, end: x.span.end - offset,
}, },
..Suggestion::default() ..Suggestion::default()
}, },
@ -95,21 +93,10 @@ impl Completer for FileCompletion {
pub fn file_path_completion( pub fn file_path_completion(
span: nu_protocol::Span, span: nu_protocol::Span,
partial: &str, partial: &str,
cwd: &str, cwds: &[impl AsRef<str>],
options: &CompletionOptions, options: &CompletionOptions,
engine_state: &EngineState, engine_state: &EngineState,
stack: &Stack, stack: &Stack,
) -> Vec<(nu_protocol::Span, String, Option<Style>)> { ) -> Vec<FileSuggestion> {
complete_item(false, span, partial, cwd, options, engine_state, stack) complete_item(false, span, partial, cwds, options, engine_state, stack)
}
pub fn matches(partial: &str, from: &str, options: &CompletionOptions) -> bool {
// Check for case sensitive
if !options.case_sensitive {
return options
.match_algorithm
.matches_str(&from.to_folded_case(), &partial.to_folded_case());
}
options.match_algorithm.matches_str(from, partial)
} }

View file

@ -1,4 +1,4 @@
use crate::completions::{completion_common::sort_suggestions, Completer, CompletionOptions}; use crate::completions::{completion_options::NuMatcher, Completer, CompletionOptions};
use nu_protocol::{ use nu_protocol::{
ast::{Expr, Expression}, ast::{Expr, Expression},
engine::{Stack, StateWorkingSet}, engine::{Stack, StateWorkingSet},
@ -35,7 +35,7 @@ impl Completer for FlagCompletion {
let decl = working_set.get_decl(call.decl_id); let decl = working_set.get_decl(call.decl_id);
let sig = decl.signature(); let sig = decl.signature();
let mut output = vec![]; let mut matcher = NuMatcher::new(String::from_utf8_lossy(prefix), options.clone());
for named in &sig.named { for named in &sig.named {
let flag_desc = &named.desc; let flag_desc = &named.desc;
@ -44,8 +44,7 @@ impl Completer for FlagCompletion {
short.encode_utf8(&mut named); short.encode_utf8(&mut named);
named.insert(0, b'-'); named.insert(0, b'-');
if options.match_algorithm.matches_u8(&named, prefix) { matcher.add_semantic_suggestion(SemanticSuggestion {
output.push(SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: String::from_utf8_lossy(&named).to_string(), value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()), description: Some(flag_desc.to_string()),
@ -60,7 +59,6 @@ impl Completer for FlagCompletion {
kind: None, kind: None,
}); });
} }
}
if named.long.is_empty() { if named.long.is_empty() {
continue; continue;
@ -70,8 +68,7 @@ impl Completer for FlagCompletion {
named.insert(0, b'-'); named.insert(0, b'-');
named.insert(0, b'-'); named.insert(0, b'-');
if options.match_algorithm.matches_u8(&named, prefix) { matcher.add_semantic_suggestion(SemanticSuggestion {
output.push(SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: String::from_utf8_lossy(&named).to_string(), value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()), description: Some(flag_desc.to_string()),
@ -86,9 +83,8 @@ impl Completer for FlagCompletion {
kind: None, kind: None,
}); });
} }
}
return sort_suggestions(&String::from_utf8_lossy(prefix), output, options); return matcher.results();
} }
vec![] vec![]

View file

@ -18,7 +18,7 @@ pub use completion_options::{CompletionOptions, MatchAlgorithm};
pub use custom_completions::CustomCompletion; pub use custom_completions::CustomCompletion;
pub use directory_completions::DirectoryCompletion; pub use directory_completions::DirectoryCompletion;
pub use dotnu_completions::DotNuCompletion; pub use dotnu_completions::DotNuCompletion;
pub use file_completions::{file_path_completion, matches, FileCompletion}; pub use file_completions::{file_path_completion, FileCompletion};
pub use flag_completions::FlagCompletion; pub use flag_completions::FlagCompletion;
pub use operator_completions::OperatorCompletion; pub use operator_completions::OperatorCompletion;
pub use variable_completions::VariableCompletion; pub use variable_completions::VariableCompletion;

View file

@ -1,5 +1,5 @@
use crate::completions::{ use crate::completions::{
Completer, CompletionOptions, MatchAlgorithm, SemanticSuggestion, SuggestionKind, completion_options::NuMatcher, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind,
}; };
use nu_protocol::{ use nu_protocol::{
ast::{Expr, Expression}, ast::{Expr, Expression},
@ -28,7 +28,7 @@ impl Completer for OperatorCompletion {
span: Span, span: Span,
offset: usize, offset: usize,
_pos: usize, _pos: usize,
_options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
//Check if int, float, or string //Check if int, float, or string
let partial = std::str::from_utf8(working_set.get_span_contents(span)).unwrap_or(""); let partial = std::str::from_utf8(working_set.get_span_contents(span)).unwrap_or("");
@ -60,10 +60,6 @@ impl Completer for OperatorCompletion {
("bit-shr", "Bitwise shift right"), ("bit-shr", "Bitwise shift right"),
("in", "Is a member of (doesn't use regex)"), ("in", "Is a member of (doesn't use regex)"),
("not-in", "Is not a member of (doesn't use regex)"), ("not-in", "Is not a member of (doesn't use regex)"),
(
"++",
"Appends two lists, a list and a value, two strings, or two binary values",
),
], ],
Expr::String(_) => vec![ Expr::String(_) => vec![
("=~", "Contains regex match"), ("=~", "Contains regex match"),
@ -72,7 +68,7 @@ impl Completer for OperatorCompletion {
("not-like", "Does not contain regex match"), ("not-like", "Does not contain regex match"),
( (
"++", "++",
"Appends two lists, a list and a value, two strings, or two binary values", "Concatenates two lists, two strings, or two binary values",
), ),
("in", "Is a member of (doesn't use regex)"), ("in", "Is a member of (doesn't use regex)"),
("not-in", "Is not a member of (doesn't use regex)"), ("not-in", "Is not a member of (doesn't use regex)"),
@ -95,10 +91,6 @@ impl Completer for OperatorCompletion {
("**", "Power of"), ("**", "Power of"),
("in", "Is a member of (doesn't use regex)"), ("in", "Is a member of (doesn't use regex)"),
("not-in", "Is not a member of (doesn't use regex)"), ("not-in", "Is not a member of (doesn't use regex)"),
(
"++",
"Appends two lists, a list and a value, two strings, or two binary values",
),
], ],
Expr::Bool(_) => vec![ Expr::Bool(_) => vec![
( (
@ -113,15 +105,11 @@ impl Completer for OperatorCompletion {
("not", "Negates a value or expression"), ("not", "Negates a value or expression"),
("in", "Is a member of (doesn't use regex)"), ("in", "Is a member of (doesn't use regex)"),
("not-in", "Is not a member of (doesn't use regex)"), ("not-in", "Is not a member of (doesn't use regex)"),
(
"++",
"Appends two lists, a list and a value, two strings, or two binary values",
),
], ],
Expr::FullCellPath(path) => match path.head.expr { Expr::FullCellPath(path) => match path.head.expr {
Expr::List(_) => vec![( Expr::List(_) => vec![(
"++", "++",
"Appends two lists, a list and a value, two strings, or two binary values", "Concatenates two lists, two strings, or two binary values",
)], )],
Expr::Var(id) => get_variable_completions(id, working_set), Expr::Var(id) => get_variable_completions(id, working_set),
_ => vec![], _ => vec![],
@ -129,17 +117,12 @@ impl Completer for OperatorCompletion {
_ => vec![], _ => vec![],
}; };
let match_algorithm = MatchAlgorithm::Prefix; let mut matcher = NuMatcher::new(partial, options.clone());
let input_fuzzy_search = for (symbol, desc) in possible_operations.into_iter() {
|(operator, _): &(&str, &str)| match_algorithm.matches_str(operator, partial); matcher.add_semantic_suggestion(SemanticSuggestion {
possible_operations
.into_iter()
.filter(input_fuzzy_search)
.map(move |x| SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: x.0.to_string(), value: symbol.to_string(),
description: Some(x.1.to_string()), description: Some(desc.to_string()),
span: reedline::Span::new(span.start - offset, span.end - offset), span: reedline::Span::new(span.start - offset, span.end - offset),
append_whitespace: true, append_whitespace: true,
..Suggestion::default() ..Suggestion::default()
@ -147,8 +130,9 @@ impl Completer for OperatorCompletion {
kind: Some(SuggestionKind::Command( kind: Some(SuggestionKind::Command(
nu_protocol::engine::CommandType::Builtin, nu_protocol::engine::CommandType::Builtin,
)), )),
}) });
.collect() }
matcher.results()
} }
} }
@ -165,7 +149,7 @@ pub fn get_variable_completions<'a>(
Type::List(_) | Type::String | Type::Binary => vec![ Type::List(_) | Type::String | Type::Binary => vec![
( (
"++=", "++=",
"Appends a list, a value, a string, or a binary value to a variable.", "Concatenates two lists, two strings, or two binary values",
), ),
("=", "Assigns a value to a variable."), ("=", "Assigns a value to a variable."),
], ],

View file

@ -1,6 +1,4 @@
use crate::completions::{ use crate::completions::{Completer, CompletionOptions, SemanticSuggestion, SuggestionKind};
Completer, CompletionOptions, MatchAlgorithm, SemanticSuggestion, SuggestionKind,
};
use nu_engine::{column::get_columns, eval_variable}; use nu_engine::{column::get_columns, eval_variable};
use nu_protocol::{ use nu_protocol::{
engine::{Stack, StateWorkingSet}, engine::{Stack, StateWorkingSet},
@ -9,7 +7,7 @@ use nu_protocol::{
use reedline::Suggestion; use reedline::Suggestion;
use std::str; use std::str;
use super::completion_common::sort_suggestions; use super::completion_options::NuMatcher;
#[derive(Clone)] #[derive(Clone)]
pub struct VariableCompletion { pub struct VariableCompletion {
@ -33,7 +31,6 @@ impl Completer for VariableCompletion {
_pos: usize, _pos: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let mut output = vec![];
let builtins = ["$nu", "$in", "$env"]; let builtins = ["$nu", "$in", "$env"];
let var_str = std::str::from_utf8(&self.var_context.0).unwrap_or(""); let var_str = std::str::from_utf8(&self.var_context.0).unwrap_or("");
let var_id = working_set.find_variable(&self.var_context.0); let var_id = working_set.find_variable(&self.var_context.0);
@ -43,6 +40,7 @@ impl Completer for VariableCompletion {
}; };
let sublevels_count = self.var_context.1.len(); let sublevels_count = self.var_context.1.len();
let prefix_str = String::from_utf8_lossy(prefix); let prefix_str = String::from_utf8_lossy(prefix);
let mut matcher = NuMatcher::new(prefix_str, options.clone());
// Completions for the given variable // Completions for the given variable
if !var_str.is_empty() { if !var_str.is_empty() {
@ -63,26 +61,15 @@ impl Completer for VariableCompletion {
if let Some(val) = env_vars.get(&target_var_str) { if let Some(val) = env_vars.get(&target_var_str) {
for suggestion in nested_suggestions(val, &nested_levels, current_span) { for suggestion in nested_suggestions(val, &nested_levels, current_span) {
if options.match_algorithm.matches_u8_insensitive( matcher.add_semantic_suggestion(suggestion);
options.case_sensitive,
suggestion.suggestion.value.as_bytes(),
prefix,
) {
output.push(suggestion);
}
} }
return sort_suggestions(&prefix_str, output, options); return matcher.results();
} }
} else { } else {
// No nesting provided, return all env vars // No nesting provided, return all env vars
for env_var in env_vars { for env_var in env_vars {
if options.match_algorithm.matches_u8_insensitive( matcher.add_semantic_suggestion(SemanticSuggestion {
options.case_sensitive,
env_var.0.as_bytes(),
prefix,
) {
output.push(SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: env_var.0, value: env_var.0,
span: current_span, span: current_span,
@ -91,9 +78,8 @@ impl Completer for VariableCompletion {
kind: Some(SuggestionKind::Type(env_var.1.get_type())), kind: Some(SuggestionKind::Type(env_var.1.get_type())),
}); });
} }
}
return sort_suggestions(&prefix_str, output, options); return matcher.results();
} }
} }
@ -108,16 +94,10 @@ impl Completer for VariableCompletion {
) { ) {
for suggestion in nested_suggestions(&nuval, &self.var_context.1, current_span) for suggestion in nested_suggestions(&nuval, &self.var_context.1, current_span)
{ {
if options.match_algorithm.matches_u8_insensitive( matcher.add_semantic_suggestion(suggestion);
options.case_sensitive,
suggestion.suggestion.value.as_bytes(),
prefix,
) {
output.push(suggestion);
}
} }
return sort_suggestions(&prefix_str, output, options); return matcher.results();
} }
} }
@ -130,28 +110,17 @@ impl Completer for VariableCompletion {
if let Ok(value) = var { if let Ok(value) = var {
for suggestion in nested_suggestions(&value, &self.var_context.1, current_span) for suggestion in nested_suggestions(&value, &self.var_context.1, current_span)
{ {
if options.match_algorithm.matches_u8_insensitive( matcher.add_semantic_suggestion(suggestion);
options.case_sensitive,
suggestion.suggestion.value.as_bytes(),
prefix,
) {
output.push(suggestion);
}
} }
return sort_suggestions(&prefix_str, output, options); return matcher.results();
} }
} }
} }
// Variable completion (e.g: $en<tab> to complete $env) // Variable completion (e.g: $en<tab> to complete $env)
for builtin in builtins { for builtin in builtins {
if options.match_algorithm.matches_u8_insensitive( matcher.add_semantic_suggestion(SemanticSuggestion {
options.case_sensitive,
builtin.as_bytes(),
prefix,
) {
output.push(SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: builtin.to_string(), value: builtin.to_string(),
span: current_span, span: current_span,
@ -161,7 +130,6 @@ impl Completer for VariableCompletion {
kind: None, kind: None,
}); });
} }
}
// TODO: The following can be refactored (see find_commands_by_predicate() used in // TODO: The following can be refactored (see find_commands_by_predicate() used in
// command_completions). // command_completions).
@ -170,12 +138,7 @@ impl Completer for VariableCompletion {
for scope_frame in working_set.delta.scope.iter().rev() { for scope_frame in working_set.delta.scope.iter().rev() {
for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() { for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() {
for v in &overlay_frame.vars { for v in &overlay_frame.vars {
if options.match_algorithm.matches_u8_insensitive( matcher.add_semantic_suggestion(SemanticSuggestion {
options.case_sensitive,
v.0,
prefix,
) {
output.push(SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: String::from_utf8_lossy(v.0).to_string(), value: String::from_utf8_lossy(v.0).to_string(),
span: current_span, span: current_span,
@ -188,7 +151,6 @@ impl Completer for VariableCompletion {
} }
} }
} }
}
// Permanent state vars // Permanent state vars
// for scope in &self.engine_state.scope { // for scope in &self.engine_state.scope {
@ -198,12 +160,7 @@ impl Completer for VariableCompletion {
.rev() .rev()
{ {
for v in &overlay_frame.vars { for v in &overlay_frame.vars {
if options.match_algorithm.matches_u8_insensitive( matcher.add_semantic_suggestion(SemanticSuggestion {
options.case_sensitive,
v.0,
prefix,
) {
output.push(SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: String::from_utf8_lossy(v.0).to_string(), value: String::from_utf8_lossy(v.0).to_string(),
span: current_span, span: current_span,
@ -215,13 +172,8 @@ impl Completer for VariableCompletion {
}); });
} }
} }
}
output = sort_suggestions(&prefix_str, output, options); matcher.results()
output.dedup(); // TODO: Removes only consecutive duplicates, is it intended?
output
} }
} }
@ -302,13 +254,3 @@ fn recursive_value(val: &Value, sublevels: &[Vec<u8>]) -> Result<Value, Span> {
Ok(val.clone()) Ok(val.clone())
} }
} }
impl MatchAlgorithm {
pub fn matches_u8_insensitive(&self, sensitive: bool, haystack: &[u8], needle: &[u8]) -> bool {
if sensitive {
self.matches_u8(haystack, needle)
} else {
self.matches_u8(&haystack.to_ascii_lowercase(), &needle.to_ascii_lowercase())
}
}
}

View file

@ -9,6 +9,8 @@ use nu_protocol::{
}; };
use std::sync::Arc; use std::sync::Arc;
use crate::util::print_pipeline;
#[derive(Default)] #[derive(Default)]
pub struct EvaluateCommandsOpts { pub struct EvaluateCommandsOpts {
pub table_mode: Option<Value>, pub table_mode: Option<Value>,
@ -72,7 +74,7 @@ pub fn evaluate_commands(
if let Some(err) = working_set.compile_errors.first() { if let Some(err) = working_set.compile_errors.first() {
report_compile_error(&working_set, err); report_compile_error(&working_set, err);
// Not a fatal error, for now std::process::exit(1);
} }
(output, working_set.render()) (output, working_set.render())
@ -93,7 +95,7 @@ pub fn evaluate_commands(
t_mode.coerce_str()?.parse().unwrap_or_default(); t_mode.coerce_str()?.parse().unwrap_or_default();
} }
pipeline.print(engine_state, stack, no_newline, false)?; print_pipeline(engine_state, stack, pipeline, no_newline)?;
info!("evaluate {}:{}:{}", file!(), line!(), column!()); info!("evaluate {}:{}:{}", file!(), line!(), column!());

View file

@ -1,4 +1,4 @@
use crate::util::eval_source; use crate::util::{eval_source, print_pipeline};
use log::{info, trace}; use log::{info, trace};
use nu_engine::{convert_env_values, eval_block}; use nu_engine::{convert_env_values, eval_block};
use nu_parser::parse; use nu_parser::parse;
@ -89,7 +89,7 @@ pub fn evaluate_file(
if let Some(err) = working_set.compile_errors.first() { if let Some(err) = working_set.compile_errors.first() {
report_compile_error(&working_set, err); report_compile_error(&working_set, err);
// Not a fatal error, for now std::process::exit(1);
} }
// Look for blocks whose name starts with "main" and replace it with the filename. // Look for blocks whose name starts with "main" and replace it with the filename.
@ -119,7 +119,7 @@ pub fn evaluate_file(
}; };
// Print the pipeline output of the last command of the file. // Print the pipeline output of the last command of the file.
pipeline.print(engine_state, stack, true, false)?; print_pipeline(engine_state, stack, pipeline, true)?;
// Invoke the main command with arguments. // Invoke the main command with arguments.
// Arguments with whitespaces are quoted, thus can be safely concatenated by whitespace. // Arguments with whitespaces are quoted, thus can be safely concatenated by whitespace.

View file

@ -65,8 +65,12 @@ Since this command has no output, there is no point in piping it with other comm
arg.into_pipeline_data() arg.into_pipeline_data()
.print_raw(engine_state, no_newline, to_stderr)?; .print_raw(engine_state, no_newline, to_stderr)?;
} else { } else {
arg.into_pipeline_data() arg.into_pipeline_data().print_table(
.print(engine_state, stack, no_newline, to_stderr)?; engine_state,
stack,
no_newline,
to_stderr,
)?;
} }
} }
} else if !input.is_nothing() { } else if !input.is_nothing() {
@ -78,7 +82,7 @@ Since this command has no output, there is no point in piping it with other comm
if raw { if raw {
input.print_raw(engine_state, no_newline, to_stderr)?; input.print_raw(engine_state, no_newline, to_stderr)?;
} else { } else {
input.print(engine_state, stack, no_newline, to_stderr)?; input.print_table(engine_state, stack, no_newline, to_stderr)?;
} }
} }

View file

@ -201,6 +201,35 @@ fn gather_env_vars(
} }
} }
/// Print a pipeline with formatting applied based on display_output hook.
///
/// This function should be preferred when printing values resulting from a completed evaluation.
/// For values printed as part of a command's execution, such as values printed by the `print` command,
/// the `PipelineData::print_table` function should be preferred instead as it is not config-dependent.
///
/// `no_newline` controls if we need to attach newline character to output.
pub fn print_pipeline(
engine_state: &mut EngineState,
stack: &mut Stack,
pipeline: PipelineData,
no_newline: bool,
) -> Result<(), ShellError> {
if let Some(hook) = engine_state.get_config().hooks.display_output.clone() {
let pipeline = eval_hook(
engine_state,
stack,
Some(pipeline),
vec![],
&hook,
"display_output",
)?;
pipeline.print_raw(engine_state, no_newline, false)
} else {
// if display_output isn't set, we should still prefer to print with some formatting
pipeline.print_table(engine_state, stack, no_newline, false)
}
}
pub fn eval_source( pub fn eval_source(
engine_state: &mut EngineState, engine_state: &mut EngineState,
stack: &mut Stack, stack: &mut Stack,
@ -267,7 +296,7 @@ fn evaluate_source(
if let Some(err) = working_set.compile_errors.first() { if let Some(err) = working_set.compile_errors.first() {
report_compile_error(&working_set, err); report_compile_error(&working_set, err);
// Not a fatal error, for now return Ok(true);
} }
(output, working_set.render()) (output, working_set.render())
@ -281,36 +310,12 @@ fn evaluate_source(
eval_block::<WithoutDebug>(engine_state, stack, &block, input) eval_block::<WithoutDebug>(engine_state, stack, &block, input)
}?; }?;
if let PipelineData::ByteStream(..) = pipeline { let no_newline = matches!(&pipeline, &PipelineData::ByteStream(..));
// run the display hook on bytestreams too print_pipeline(engine_state, stack, pipeline, no_newline)?;
run_display_hook(engine_state, stack, pipeline, false)
} else {
run_display_hook(engine_state, stack, pipeline, true)
}?;
Ok(false) Ok(false)
} }
fn run_display_hook(
engine_state: &mut EngineState,
stack: &mut Stack,
pipeline: PipelineData,
no_newline: bool,
) -> Result<(), ShellError> {
if let Some(hook) = engine_state.get_config().hooks.display_output.clone() {
let pipeline = eval_hook(
engine_state,
stack,
Some(pipeline),
vec![],
&hook,
"display_output",
)?;
pipeline.print(engine_state, stack, no_newline, false)
} else {
pipeline.print(engine_state, stack, no_newline, false)
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

View file

@ -890,8 +890,8 @@ fn subcommand_completions(mut subcommand_completer: NuCompleter) {
match_suggestions( match_suggestions(
&vec![ &vec![
"foo bar".to_string(), "foo bar".to_string(),
"foo aabcrr".to_string(),
"foo abaz".to_string(), "foo abaz".to_string(),
"foo aabcrr".to_string(),
], ],
&suggestions, &suggestions,
); );
@ -955,8 +955,8 @@ fn flag_completions() {
"--mime-type".into(), "--mime-type".into(),
"--short-names".into(), "--short-names".into(),
"--threads".into(), "--threads".into(),
"-D".into(),
"-a".into(), "-a".into(),
"-D".into(),
"-d".into(), "-d".into(),
"-f".into(), "-f".into(),
"-h".into(), "-h".into(),
@ -1287,7 +1287,7 @@ fn variables_completions() {
assert_eq!(3, suggestions.len()); assert_eq!(3, suggestions.len());
#[cfg(windows)] #[cfg(windows)]
let expected: Vec<String> = vec!["PWD".into(), "Path".into(), "TEST".into()]; let expected: Vec<String> = vec!["Path".into(), "PWD".into(), "TEST".into()];
#[cfg(not(windows))] #[cfg(not(windows))]
let expected: Vec<String> = vec!["PATH".into(), "PWD".into(), "TEST".into()]; let expected: Vec<String> = vec!["PATH".into(), "PWD".into(), "TEST".into()];
@ -1576,6 +1576,23 @@ fn sort_fuzzy_completions_in_alphabetical_order(mut fuzzy_alpha_sort_completer:
); );
} }
#[test]
fn exact_match() {
let (dir, _, engine, stack) = new_partial_engine();
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
let target_dir = format!("open {}", folder(dir.join("pArTiAL")));
let suggestions = completer.complete(&target_dir, target_dir.len());
// Since it's an exact match, only 'partial' should be suggested, not
// 'partial-a' and stuff. Implemented in #13302
match_suggestions(
&vec![file(dir.join("partial").join("hello.txt"))],
&suggestions,
);
}
#[ignore = "was reverted, still needs fixing"] #[ignore = "was reverted, still needs fixing"]
#[rstest] #[rstest]
fn alias_offset_bug_7648() { fn alias_offset_bug_7648() {

View file

@ -21,10 +21,10 @@ nu-protocol = { path = "../nu-protocol", version = "0.100.1" }
nu-utils = { path = "../nu-utils", version = "0.100.1" } nu-utils = { path = "../nu-utils", version = "0.100.1" }
itertools = { workspace = true } itertools = { workspace = true }
shadow-rs = { version = "0.35", default-features = false } shadow-rs = { version = "0.36", default-features = false }
[build-dependencies] [build-dependencies]
shadow-rs = { version = "0.35", default-features = false } shadow-rs = { version = "0.36", default-features = false }
[features] [features]
mimalloc = [] mimalloc = []

View file

@ -69,6 +69,33 @@ impl Command for Do {
let block: Closure = call.req(engine_state, caller_stack, 0)?; let block: Closure = call.req(engine_state, caller_stack, 0)?;
let rest: Vec<Value> = call.rest(engine_state, caller_stack, 1)?; let rest: Vec<Value> = call.rest(engine_state, caller_stack, 1)?;
let ignore_all_errors = call.has_flag(engine_state, caller_stack, "ignore-errors")?; let ignore_all_errors = call.has_flag(engine_state, caller_stack, "ignore-errors")?;
if call.has_flag(engine_state, caller_stack, "ignore-shell-errors")? {
nu_protocol::report_shell_warning(
engine_state,
&ShellError::GenericError {
error: "Deprecated option".into(),
msg: "`--ignore-shell-errors` is deprecated and will be removed in 0.102.0."
.into(),
span: Some(call.head),
help: Some("Please use the `--ignore-errors(-i)`".into()),
inner: vec![],
},
);
}
if call.has_flag(engine_state, caller_stack, "ignore-program-errors")? {
nu_protocol::report_shell_warning(
engine_state,
&ShellError::GenericError {
error: "Deprecated option".into(),
msg: "`--ignore-program-errors` is deprecated and will be removed in 0.102.0."
.into(),
span: Some(call.head),
help: Some("Please use the `--ignore-errors(-i)`".into()),
inner: vec![],
},
);
}
let ignore_shell_errors = ignore_all_errors let ignore_shell_errors = ignore_all_errors
|| call.has_flag(engine_state, caller_stack, "ignore-shell-errors")?; || call.has_flag(engine_state, caller_stack, "ignore-shell-errors")?;
let ignore_program_errors = ignore_all_errors let ignore_program_errors = ignore_all_errors
@ -208,16 +235,6 @@ impl Command for Do {
example: r#"do --ignore-errors { thisisnotarealcommand }"#, example: r#"do --ignore-errors { thisisnotarealcommand }"#,
result: None, result: None,
}, },
Example {
description: "Run the closure and ignore shell errors",
example: r#"do --ignore-shell-errors { thisisnotarealcommand }"#,
result: None,
},
Example {
description: "Run the closure and ignore external program errors",
example: r#"do --ignore-program-errors { nu --commands 'exit 1' }; echo "I'll still run""#,
result: None,
},
Example { Example {
description: "Abort the pipeline if a program returns a non-zero exit code", description: "Abort the pipeline if a program returns a non-zero exit code",
example: r#"do --capture-errors { nu --commands 'exit 1' } | myscarycommand"#, example: r#"do --capture-errors { nu --commands 'exit 1' } | myscarycommand"#,

View file

@ -86,7 +86,6 @@ serde_yaml = { workspace = true }
sha2 = { workspace = true } sha2 = { workspace = true }
sysinfo = { workspace = true } sysinfo = { workspace = true }
tabled = { workspace = true, features = ["ansi"], default-features = false } tabled = { workspace = true, features = ["ansi"], default-features = false }
terminal_size = { workspace = true }
titlecase = { workspace = true } titlecase = { workspace = true }
toml = { workspace = true, features = ["preserve_order"] } toml = { workspace = true, features = ["preserve_order"] }
unicode-segmentation = { workspace = true } unicode-segmentation = { workspace = true }
@ -96,6 +95,7 @@ uu_cp = { workspace = true }
uu_mkdir = { workspace = true } uu_mkdir = { workspace = true }
uu_mktemp = { workspace = true } uu_mktemp = { workspace = true }
uu_mv = { workspace = true } uu_mv = { workspace = true }
uu_touch = { workspace = true }
uu_uname = { workspace = true } uu_uname = { workspace = true }
uu_whoami = { workspace = true } uu_whoami = { workspace = true }
uuid = { workspace = true, features = ["v4"] } uuid = { workspace = true, features = ["v4"] }

View file

@ -359,7 +359,6 @@ fn nu_value_to_sqlite_type(val: &Value) -> Result<&'static str, ShellError> {
| Type::Custom(_) | Type::Custom(_)
| Type::Error | Type::Error
| Type::List(_) | Type::List(_)
| Type::ListStream
| Type::Range | Type::Range
| Type::Record(_) | Type::Record(_)
| Type::Signature | Type::Signature

View file

@ -1,6 +1,7 @@
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use nu_parser::parse; use nu_parser::{flatten_block, parse};
use nu_protocol::engine::StateWorkingSet; use nu_protocol::{engine::StateWorkingSet, record};
use serde_json::{json, Value as JsonValue};
#[derive(Clone)] #[derive(Clone)]
pub struct Ast; pub struct Ast;
@ -16,18 +17,120 @@ impl Command for Ast {
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build("ast") Signature::build("ast")
.input_output_types(vec![(Type::String, Type::record())]) .input_output_types(vec![
(Type::Nothing, Type::table()),
(Type::Nothing, Type::record()),
(Type::Nothing, Type::String),
])
.required( .required(
"pipeline", "pipeline",
SyntaxShape::String, SyntaxShape::String,
"The pipeline to print the ast for.", "The pipeline to print the ast for.",
) )
.switch("json", "serialize to json", Some('j')) .switch("json", "Serialize to json", Some('j'))
.switch("minify", "minify the nuon or json output", Some('m')) .switch("minify", "Minify the nuon or json output", Some('m'))
.switch("flatten", "An easier to read version of the ast", Some('f'))
.allow_variants_without_examples(true) .allow_variants_without_examples(true)
.category(Category::Debug) .category(Category::Debug)
} }
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Print the ast of a string",
example: "ast 'hello'",
result: None,
},
Example {
description: "Print the ast of a pipeline",
example: "ast 'ls | where name =~ README'",
result: None,
},
Example {
description: "Print the ast of a pipeline with an error",
example: "ast 'for x in 1..10 { echo $x '",
result: None,
},
Example {
description:
"Print the ast of a pipeline with an error, as json, in a nushell table",
example: "ast 'for x in 1..10 { echo $x ' --json | get block | from json",
result: None,
},
Example {
description: "Print the ast of a pipeline with an error, as json, minified",
example: "ast 'for x in 1..10 { echo $x ' --json --minify",
result: None,
},
Example {
description: "Print the ast of a string flattened",
example: r#"ast "'hello'" --flatten"#,
result: Some(Value::test_list(vec![Value::test_record(record! {
"content" => Value::test_string("'hello'"),
"shape" => Value::test_string("shape_string"),
"span" => Value::test_record(record! {
"start" => Value::test_int(0),
"end" => Value::test_int(7),}),
})])),
},
Example {
description: "Print the ast of a string flattened, as json, minified",
example: r#"ast "'hello'" --flatten --json --minify"#,
result: Some(Value::test_string(
r#"[{"content":"'hello'","shape":"shape_string","span":{"start":0,"end":7}}]"#,
)),
},
Example {
description: "Print the ast of a pipeline flattened",
example: r#"ast 'ls | sort-by type name -i' --flatten"#,
result: Some(Value::test_list(vec![
Value::test_record(record! {
"content" => Value::test_string("ls"),
"shape" => Value::test_string("shape_external"),
"span" => Value::test_record(record! {
"start" => Value::test_int(0),
"end" => Value::test_int(2),}),
}),
Value::test_record(record! {
"content" => Value::test_string("|"),
"shape" => Value::test_string("shape_pipe"),
"span" => Value::test_record(record! {
"start" => Value::test_int(3),
"end" => Value::test_int(4),}),
}),
Value::test_record(record! {
"content" => Value::test_string("sort-by"),
"shape" => Value::test_string("shape_internalcall"),
"span" => Value::test_record(record! {
"start" => Value::test_int(5),
"end" => Value::test_int(12),}),
}),
Value::test_record(record! {
"content" => Value::test_string("type"),
"shape" => Value::test_string("shape_string"),
"span" => Value::test_record(record! {
"start" => Value::test_int(13),
"end" => Value::test_int(17),}),
}),
Value::test_record(record! {
"content" => Value::test_string("name"),
"shape" => Value::test_string("shape_string"),
"span" => Value::test_record(record! {
"start" => Value::test_int(18),
"end" => Value::test_int(22),}),
}),
Value::test_record(record! {
"content" => Value::test_string("-i"),
"shape" => Value::test_string("shape_flag"),
"span" => Value::test_record(record! {
"start" => Value::test_int(23),
"end" => Value::test_int(25),}),
}),
])),
},
]
}
fn run( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
@ -38,19 +141,81 @@ impl Command for Ast {
let pipeline: Spanned<String> = call.req(engine_state, stack, 0)?; let pipeline: Spanned<String> = call.req(engine_state, stack, 0)?;
let to_json = call.has_flag(engine_state, stack, "json")?; let to_json = call.has_flag(engine_state, stack, "json")?;
let minify = call.has_flag(engine_state, stack, "minify")?; let minify = call.has_flag(engine_state, stack, "minify")?;
let flatten = call.has_flag(engine_state, stack, "flatten")?;
let mut working_set = StateWorkingSet::new(engine_state); let mut working_set = StateWorkingSet::new(engine_state);
let block_output = parse(&mut working_set, None, pipeline.item.as_bytes(), false); let offset = working_set.next_span_start();
let parsed_block = parse(&mut working_set, None, pipeline.item.as_bytes(), false);
if flatten {
let flat = flatten_block(&working_set, &parsed_block);
if to_json {
let mut json_val: JsonValue = json!([]);
for (span, shape) in flat {
let content =
String::from_utf8_lossy(working_set.get_span_contents(span)).to_string();
let json = json!(
{
"content": content,
"shape": shape.to_string(),
"span": {
"start": span.start.checked_sub(offset),
"end": span.end.checked_sub(offset),
},
}
);
json_merge(&mut json_val, &json);
}
let json_string = if minify {
if let Ok(json_str) = serde_json::to_string(&json_val) {
json_str
} else {
"{}".to_string()
}
} else if let Ok(json_str) = serde_json::to_string_pretty(&json_val) {
json_str
} else {
"{}".to_string()
};
Ok(Value::string(json_string, pipeline.span).into_pipeline_data())
} else {
// let mut rec: Record = Record::new();
let mut rec = vec![];
for (span, shape) in flat {
let content =
String::from_utf8_lossy(working_set.get_span_contents(span)).to_string();
let each_rec = record! {
"content" => Value::test_string(content),
"shape" => Value::test_string(shape.to_string()),
"span" => Value::test_record(record!{
"start" => Value::test_int(match span.start.checked_sub(offset) {
Some(start) => start as i64,
None => 0
}),
"end" => Value::test_int(match span.end.checked_sub(offset) {
Some(end) => end as i64,
None => 0
}),
}),
};
rec.push(Value::test_record(each_rec));
}
Ok(Value::list(rec, pipeline.span).into_pipeline_data())
}
} else {
let error_output = working_set.parse_errors.first(); let error_output = working_set.parse_errors.first();
let block_span = match &block_output.span { let block_span = match &parsed_block.span {
Some(span) => span, Some(span) => span,
None => &pipeline.span, None => &pipeline.span,
}; };
if to_json { if to_json {
// Get the block as json // Get the block as json
let serde_block_str = if minify { let serde_block_str = if minify {
serde_json::to_string(&*block_output) serde_json::to_string(&*parsed_block)
} else { } else {
serde_json::to_string_pretty(&*block_output) serde_json::to_string_pretty(&*parsed_block)
}; };
let block_json = match serde_block_str { let block_json = match serde_block_str {
Ok(json) => json, Ok(json) => json,
@ -59,7 +224,7 @@ impl Command for Ast {
from_type: "block".to_string(), from_type: "block".to_string(),
span: *block_span, span: *block_span,
help: Some(format!( help: Some(format!(
"Error: {e}\nCan't convert {block_output:?} to string" "Error: {e}\nCan't convert {parsed_block:?} to string"
)), )),
})?, })?,
}; };
@ -94,9 +259,9 @@ impl Command for Ast {
} else { } else {
let block_value = Value::string( let block_value = Value::string(
if minify { if minify {
format!("{block_output:?}") format!("{parsed_block:?}")
} else { } else {
format!("{block_output:#?}") format!("{parsed_block:#?}")
}, },
pipeline.span, pipeline.span,
); );
@ -118,36 +283,25 @@ impl Command for Ast {
Ok(output_record.into_pipeline_data()) Ok(output_record.into_pipeline_data())
} }
} }
}
}
fn examples(&self) -> Vec<Example> { fn json_merge(a: &mut JsonValue, b: &JsonValue) {
vec![ match (a, b) {
Example { (JsonValue::Object(ref mut a), JsonValue::Object(b)) => {
description: "Print the ast of a string", for (k, v) in b {
example: "ast 'hello'", json_merge(a.entry(k).or_insert(JsonValue::Null), v);
result: None, }
}, }
Example { (JsonValue::Array(ref mut a), JsonValue::Array(b)) => {
description: "Print the ast of a pipeline", a.extend(b.clone());
example: "ast 'ls | where name =~ README'", }
result: None, (JsonValue::Array(ref mut a), JsonValue::Object(b)) => {
}, a.extend([JsonValue::Object(b.clone())]);
Example { }
description: "Print the ast of a pipeline with an error", (a, b) => {
example: "ast 'for x in 1..10 { echo $x '", *a = b.clone();
result: None, }
},
Example {
description:
"Print the ast of a pipeline with an error, as json, in a nushell table",
example: "ast 'for x in 1..10 { echo $x ' --json | get block | from json",
result: None,
},
Example {
description: "Print the ast of a pipeline with an error, as json, minified",
example: "ast 'for x in 1..10 { echo $x ' --json --minify",
result: None,
},
]
} }
} }

View file

@ -1,6 +1,6 @@
use super::inspect_table; use super::inspect_table;
use crossterm::terminal::size;
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use terminal_size::{terminal_size, Height, Width};
#[derive(Clone)] #[derive(Clone)]
pub struct Inspect; pub struct Inspect;
@ -38,12 +38,9 @@ impl Command for Inspect {
let original_input = input_val.clone(); let original_input = input_val.clone();
let description = input_val.get_type().to_string(); let description = input_val.get_type().to_string();
let (cols, _rows) = match terminal_size() { let (cols, _rows) = size().unwrap_or((0, 0));
Some((w, h)) => (Width(w.0), Height(h.0)),
None => (Width(0), Height(0)),
};
let table = inspect_table::build_table(input_val, description, cols.0 as usize); let table = inspect_table::build_table(input_val, description, cols as usize);
// Note that this is printed to stderr. The reason for this is so it doesn't disrupt the regular nushell // Note that this is printed to stderr. The reason for this is so it doesn't disrupt the regular nushell
// tabular output. If we printed to stdout, nushell would get confused with two outputs. // tabular output. If we printed to stdout, nushell would get confused with two outputs.

View file

@ -230,6 +230,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
Rm, Rm,
Save, Save,
Touch, Touch,
UTouch,
Glob, Glob,
Watch, Watch,
}; };
@ -247,7 +248,9 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
IsTerminal, IsTerminal,
Kill, Kill,
Sleep, Sleep,
Term,
TermSize, TermSize,
TermQuery,
Whoami, Whoami,
}; };

View file

@ -15,7 +15,16 @@ impl Command for ConfigEnv {
Signature::build(self.name()) Signature::build(self.name())
.category(Category::Env) .category(Category::Env)
.input_output_types(vec![(Type::Nothing, Type::Any)]) .input_output_types(vec![(Type::Nothing, Type::Any)])
.switch("default", "Print default `env.nu` file instead.", Some('d')) .switch(
"default",
"Print the internal default `env.nu` file instead.",
Some('d'),
)
.switch(
"sample",
"Print a commented, sample `env.nu` file instead.",
Some('s'),
)
// TODO: Signature narrower than what run actually supports theoretically // TODO: Signature narrower than what run actually supports theoretically
} }
@ -26,18 +35,18 @@ impl Command for ConfigEnv {
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![
Example { Example {
description: "allow user to open and update nu env", description: "open user's env.nu in the default editor",
example: "config env", example: "config env",
result: None, result: None,
}, },
Example { Example {
description: "allow user to print default `env.nu` file", description: "pretty-print a commented, sample `env.nu` that explains common settings",
example: "config env --default,", example: "config env --sample | nu-highlight,",
result: None, result: None,
}, },
Example { Example {
description: "allow saving the default `env.nu` locally", description: "pretty-print the internal `env.nu` file which is loaded before the user's environment",
example: "config env --default | save -f ~/.config/nushell/default_env.nu", example: "config env --default | nu-highlight,",
result: None, result: None,
}, },
] ]
@ -50,12 +59,28 @@ impl Command for ConfigEnv {
call: &Call, call: &Call,
_input: PipelineData, _input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let default_flag = call.has_flag(engine_state, stack, "default")?;
let sample_flag = call.has_flag(engine_state, stack, "sample")?;
if default_flag && sample_flag {
return Err(ShellError::IncompatibleParameters {
left_message: "can't use `--default` at the same time".into(),
left_span: call.get_flag_span(stack, "default").expect("has flag"),
right_message: "because of `--sample`".into(),
right_span: call.get_flag_span(stack, "sample").expect("has flag"),
});
}
// `--default` flag handling // `--default` flag handling
if call.has_flag(engine_state, stack, "default")? { if call.has_flag(engine_state, stack, "default")? {
let head = call.head; let head = call.head;
return Ok(Value::string(nu_utils::get_default_env(), head).into_pipeline_data()); return Ok(Value::string(nu_utils::get_default_env(), head).into_pipeline_data());
} }
// `--sample` flag handling
if sample_flag {
let head = call.head;
return Ok(Value::string(nu_utils::get_sample_env(), head).into_pipeline_data());
}
// Find the editor executable. // Find the editor executable.
let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?; let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?;
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?; let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;

View file

@ -17,9 +17,14 @@ impl Command for ConfigNu {
.input_output_types(vec![(Type::Nothing, Type::Any)]) .input_output_types(vec![(Type::Nothing, Type::Any)])
.switch( .switch(
"default", "default",
"Print default `config.nu` file instead.", "Print the internal default `config.nu` file instead.",
Some('d'), Some('d'),
) )
.switch(
"sample",
"Print a commented, sample `config.nu` file instead.",
Some('s'),
)
// TODO: Signature narrower than what run actually supports theoretically // TODO: Signature narrower than what run actually supports theoretically
} }
@ -30,18 +35,19 @@ impl Command for ConfigNu {
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![
Example { Example {
description: "allow user to open and update nu config", description: "open user's config.nu in the default editor",
example: "config nu", example: "config nu",
result: None, result: None,
}, },
Example { Example {
description: "allow user to print default `config.nu` file", description: "pretty-print a commented, sample `config.nu` that explains common settings",
example: "config nu --default,", example: "config nu --sample | nu-highlight",
result: None, result: None,
}, },
Example { Example {
description: "allow saving the default `config.nu` locally", description:
example: "config nu --default | save -f ~/.config/nushell/default_config.nu", "pretty-print the internal `config.nu` file which is loaded before user's config",
example: "config nu --default | nu-highlight",
result: None, result: None,
}, },
] ]
@ -54,12 +60,29 @@ impl Command for ConfigNu {
call: &Call, call: &Call,
_input: PipelineData, _input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let default_flag = call.has_flag(engine_state, stack, "default")?;
let sample_flag = call.has_flag(engine_state, stack, "sample")?;
if default_flag && sample_flag {
return Err(ShellError::IncompatibleParameters {
left_message: "can't use `--default` at the same time".into(),
left_span: call.get_flag_span(stack, "default").expect("has flag"),
right_message: "because of `--sample`".into(),
right_span: call.get_flag_span(stack, "sample").expect("has flag"),
});
}
// `--default` flag handling // `--default` flag handling
if call.has_flag(engine_state, stack, "default")? { if default_flag {
let head = call.head; let head = call.head;
return Ok(Value::string(nu_utils::get_default_config(), head).into_pipeline_data()); return Ok(Value::string(nu_utils::get_default_config(), head).into_pipeline_data());
} }
// `--sample` flag handling
if sample_flag {
let head = call.head;
return Ok(Value::string(nu_utils::get_sample_config(), head).into_pipeline_data());
}
// Find the editor executable. // Find the editor executable.
let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?; let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?;
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?; let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;

View file

@ -12,6 +12,7 @@ mod ucp;
mod umkdir; mod umkdir;
mod umv; mod umv;
mod util; mod util;
mod utouch;
mod watch; mod watch;
pub use self::open::Open; pub use self::open::Open;
@ -27,4 +28,5 @@ pub use touch::Touch;
pub use ucp::UCp; pub use ucp::UCp;
pub use umkdir::UMkdir; pub use umkdir::UMkdir;
pub use umv::UMv; pub use umv::UMv;
pub use utouch::UTouch;
pub use watch::Watch; pub use watch::Watch;

View file

@ -188,6 +188,7 @@ impl Command for UMv {
target_dir: None, target_dir: None,
no_target_dir: false, no_target_dir: false,
strip_slashes: false, strip_slashes: false,
debug: false,
}; };
if let Err(error) = uu_mv::mv(&files, &options) { if let Err(error) = uu_mv::mv(&files, &options) {
return Err(ShellError::GenericError { return Err(ShellError::GenericError {

View file

@ -0,0 +1,268 @@
use std::io::ErrorKind;
use std::path::PathBuf;
use chrono::{DateTime, FixedOffset};
use filetime::FileTime;
use nu_engine::CallExt;
use nu_path::expand_path_with;
use nu_protocol::engine::{Call, Command, EngineState, Stack};
use nu_protocol::{
Category, Example, NuGlob, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type,
};
use uu_touch::error::TouchError;
use uu_touch::{ChangeTimes, InputFile, Options, Source};
use super::util::get_rest_for_glob_pattern;
#[derive(Clone)]
pub struct UTouch;
impl Command for UTouch {
fn name(&self) -> &str {
"utouch"
}
fn search_terms(&self) -> Vec<&str> {
vec!["create", "file"]
}
fn signature(&self) -> Signature {
Signature::build("utouch")
.input_output_types(vec![ (Type::Nothing, Type::Nothing) ])
.rest(
"files",
SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::Filepath]),
"The file(s) to create. '-' is used to represent stdout."
)
.named(
"reference",
SyntaxShape::Filepath,
"Use the access and modification times of the reference file/directory instead of the current time",
Some('r'),
)
.named(
"timestamp",
SyntaxShape::DateTime,
"Use the given timestamp instead of the current time",
Some('t')
)
.named(
"date",
SyntaxShape::String,
"Use the given time instead of the current time. This can be a full timestamp or it can be relative to either the current time or reference file time (if given). For more information, see https://www.gnu.org/software/coreutils/manual/html_node/touch-invocation.html",
Some('d')
)
.switch(
"modified",
"Change only the modification time (if used with -a, access time is changed too)",
Some('m'),
)
.switch(
"access",
"Change only the access time (if used with -m, modification time is changed too)",
Some('a'),
)
.switch(
"no-create",
"Don't create the file if it doesn't exist",
Some('c'),
)
.switch(
"no-deref",
"Affect each symbolic link instead of any referenced file (only for systems that can change the timestamps of a symlink). Ignored if touching stdout",
Some('s'),
)
.category(Category::FileSystem)
}
fn description(&self) -> &str {
"Creates one or more files."
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let change_mtime: bool = call.has_flag(engine_state, stack, "modified")?;
let change_atime: bool = call.has_flag(engine_state, stack, "access")?;
let no_create: bool = call.has_flag(engine_state, stack, "no-create")?;
let no_deref: bool = call.has_flag(engine_state, stack, "no-dereference")?;
let file_globs: Vec<Spanned<NuGlob>> =
get_rest_for_glob_pattern(engine_state, stack, call, 0)?;
let cwd = engine_state.cwd(Some(stack))?;
if file_globs.is_empty() {
return Err(ShellError::MissingParameter {
param_name: "requires file paths".to_string(),
span: call.head,
});
}
let (reference_file, reference_span) = if let Some(reference) =
call.get_flag::<Spanned<PathBuf>>(engine_state, stack, "reference")?
{
(Some(reference.item), Some(reference.span))
} else {
(None, None)
};
let (date_str, date_span) =
if let Some(date) = call.get_flag::<Spanned<String>>(engine_state, stack, "date")? {
(Some(date.item), Some(date.span))
} else {
(None, None)
};
let timestamp: Option<Spanned<DateTime<FixedOffset>>> =
call.get_flag(engine_state, stack, "timestamp")?;
let source = if let Some(timestamp) = timestamp {
if let Some(reference_span) = reference_span {
return Err(ShellError::IncompatibleParameters {
left_message: "timestamp given".to_string(),
left_span: timestamp.span,
right_message: "reference given".to_string(),
right_span: reference_span,
});
}
if let Some(date_span) = date_span {
return Err(ShellError::IncompatibleParameters {
left_message: "timestamp given".to_string(),
left_span: timestamp.span,
right_message: "date given".to_string(),
right_span: date_span,
});
}
Source::Timestamp(FileTime::from_unix_time(
timestamp.item.timestamp(),
timestamp.item.timestamp_subsec_nanos(),
))
} else if let Some(reference_file) = reference_file {
let reference_file = expand_path_with(reference_file, &cwd, true);
Source::Reference(reference_file)
} else {
Source::Now
};
let change_times = if change_atime && !change_mtime {
ChangeTimes::AtimeOnly
} else if change_mtime && !change_atime {
ChangeTimes::MtimeOnly
} else {
ChangeTimes::Both
};
let mut input_files = Vec::new();
for file_glob in &file_globs {
if file_glob.item.as_ref() == "-" {
input_files.push(InputFile::Stdout);
} else {
let path =
expand_path_with(file_glob.item.as_ref(), &cwd, file_glob.item.is_expand());
input_files.push(InputFile::Path(path));
}
}
if let Err(err) = uu_touch::touch(
&input_files,
&Options {
no_create,
no_deref,
source,
date: date_str,
change_times,
strict: true,
},
) {
let nu_err = match err {
TouchError::TouchFileError { path, index, error } => ShellError::GenericError {
error: format!("Could not touch {}", path.display()),
msg: error.to_string(),
span: Some(file_globs[index].span),
help: None,
inner: Vec::new(),
},
TouchError::InvalidDateFormat(date) => ShellError::IncorrectValue {
msg: format!("Invalid date: {}", date),
val_span: date_span.expect("utouch should've been given a date"),
call_span: call.head,
},
TouchError::ReferenceFileInaccessible(reference_path, io_err) => {
let span =
reference_span.expect("utouch should've been given a reference file");
if io_err.kind() == ErrorKind::NotFound {
ShellError::FileNotFound {
span,
file: reference_path.display().to_string(),
}
} else {
ShellError::GenericError {
error: io_err.to_string(),
msg: format!("Failed to read metadata of {}", reference_path.display()),
span: Some(span),
help: None,
inner: Vec::new(),
}
}
}
_ => ShellError::GenericError {
error: err.to_string(),
msg: err.to_string(),
span: Some(call.head),
help: None,
inner: Vec::new(),
},
};
return Err(nu_err);
}
Ok(PipelineData::empty())
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Creates \"fixture.json\"",
example: "utouch fixture.json",
result: None,
},
Example {
description: "Creates files a, b and c",
example: "utouch a b c",
result: None,
},
Example {
description: r#"Changes the last modified time of "fixture.json" to today's date"#,
example: "utouch -m fixture.json",
result: None,
},
Example {
description: "Changes the last accessed and modified times of files a, b and c to the current time but yesterday",
example: r#"utouch -d "yesterday" a b c"#,
result: None,
},
Example {
description: r#"Changes the last modified time of files d and e to "fixture.json"'s last modified time"#,
example: r#"utouch -m -r fixture.json d e"#,
result: None,
},
Example {
description: r#"Changes the last accessed time of "fixture.json" to a datetime"#,
example: r#"utouch -a -t 2019-08-24T12:30:30 fixture.json"#,
result: None,
},
Example {
description: r#"Change the last accessed and modified times of stdout"#,
example: r#"utouch -"#,
result: None,
},
Example {
description: r#"Changes the last accessed and modified times of file a to 1 month before "fixture.json"'s last modified time"#,
example: r#"utouch -r fixture.json -d "-1 month" a"#,
result: None,
},
]
}
}

View file

@ -194,7 +194,7 @@ impl Command for Watch {
match result { match result {
Ok(val) => { Ok(val) => {
val.print(engine_state, stack, false, false)?; val.print_table(engine_state, stack, false, false)?;
} }
Err(err) => { Err(err) => {
let working_set = StateWorkingSet::new(engine_state); let working_set = StateWorkingSet::new(engine_state);

View file

@ -129,6 +129,8 @@ fn insert(
let replacement: Value = call.req(engine_state, stack, 1)?; let replacement: Value = call.req(engine_state, stack, 1)?;
match input { match input {
// Propagate errors in the pipeline
PipelineData::Value(Value::Error { error, .. }, ..) => Err(*error),
PipelineData::Value(mut value, metadata) => { PipelineData::Value(mut value, metadata) => {
if let Value::Closure { val, .. } = replacement { if let Value::Closure { val, .. } = replacement {
match (cell_path.members.first(), &mut value) { match (cell_path.members.first(), &mut value) {

View file

@ -120,6 +120,8 @@ repeating this process with row 1, and so on."#
PipelineData::Value(Value::Record { val: inp, .. }, ..), PipelineData::Value(Value::Record { val: inp, .. }, ..),
Value::Record { val: to_merge, .. }, Value::Record { val: to_merge, .. },
) => Ok(Value::record(do_merge(inp, &to_merge), head).into_pipeline_data()), ) => Ok(Value::record(do_merge(inp, &to_merge), head).into_pipeline_data()),
// Propagate errors in the pipeline
(PipelineData::Value(Value::Error { error, .. }, ..), _) => Err(*error.clone()),
(PipelineData::Value(val, ..), ..) => { (PipelineData::Value(val, ..), ..) => {
// Only point the "value originates here" arrow at the merge value // Only point the "value originates here" arrow at the merge value
// if it was generated from a block. Otherwise, point at the pipeline value. -Leon 2022-10-27 // if it was generated from a block. Otherwise, point at the pipeline value. -Leon 2022-10-27

View file

@ -1,5 +1,6 @@
use indexmap::IndexMap; use indexmap::IndexMap;
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use nu_protocol::report_shell_warning;
#[derive(Clone)] #[derive(Clone)]
pub struct SplitBy; pub struct SplitBy;
@ -27,6 +28,15 @@ impl Command for SplitBy {
call: &Call, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
report_shell_warning(
engine_state,
&ShellError::Deprecated {
deprecated: "The `split_by` command",
suggestion: "Please use the `group-by` command instead.",
span: call.head,
help: None,
},
);
split_by(engine_state, stack, call, input) split_by(engine_state, stack, call, input)
} }

View file

@ -11,7 +11,10 @@ impl Command for FromCsv {
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build("from csv") Signature::build("from csv")
.input_output_types(vec![(Type::String, Type::table())]) .input_output_types(vec![
(Type::String, Type::table()),
(Type::String, Type::list(Type::Any)),
])
.named( .named(
"separator", "separator",
SyntaxShape::String, SyntaxShape::String,
@ -82,6 +85,26 @@ impl Command for FromCsv {
})], })],
)) ))
}, },
Example {
description: "Convert comma-separated data to a table, allowing variable number of columns per row",
example: "\"ColA,ColB\n1,2\n3,4,5\n6\" | from csv --flexible",
result: Some(Value::test_list (
vec![
Value::test_record(record! {
"ColA" => Value::test_int(1),
"ColB" => Value::test_int(2),
}),
Value::test_record(record! {
"ColA" => Value::test_int(3),
"ColB" => Value::test_int(4),
"column2" => Value::test_int(5),
}),
Value::test_record(record! {
"ColA" => Value::test_int(6),
}),
],
))
},
Example { Example {
description: "Convert comma-separated data to a table, ignoring headers", description: "Convert comma-separated data to a table, ignoring headers",
example: "open data.txt | from csv --noheaders", example: "open data.txt | from csv --noheaders",

View file

@ -39,12 +39,7 @@ fn from_delimited_stream(
.from_reader(input_reader); .from_reader(input_reader);
let headers = if noheaders { let headers = if noheaders {
(0..reader vec![]
.headers()
.map_err(|err| from_csv_error(err, span))?
.len())
.map(|i| format!("column{i}"))
.collect::<Vec<String>>()
} else { } else {
reader reader
.headers() .headers()
@ -54,15 +49,17 @@ fn from_delimited_stream(
.collect() .collect()
}; };
let n = headers.len();
let columns = headers
.into_iter()
.chain((n..).map(|i| format!("column{i}")));
let iter = reader.into_records().map(move |row| { let iter = reader.into_records().map(move |row| {
let row = match row { let row = match row {
Ok(row) => row, Ok(row) => row,
Err(err) => return Value::error(from_csv_error(err, span), span), Err(err) => return Value::error(from_csv_error(err, span), span),
}; };
let columns = headers.iter().cloned(); let columns = columns.clone();
let values = row let values = row.into_iter().map(|s| {
.into_iter()
.map(|s| {
if no_infer { if no_infer {
Value::string(s, span) Value::string(s, span)
} else if let Ok(i) = s.parse() { } else if let Ok(i) = s.parse() {
@ -72,14 +69,8 @@ fn from_delimited_stream(
} else { } else {
Value::string(s, span) Value::string(s, span)
} }
}) });
.chain(std::iter::repeat(Value::nothing(span)));
// If there are more values than the number of headers,
// then the remaining values are ignored.
//
// Otherwise, if there are less values than headers,
// then `Value::nothing(span)` is used to fill the remaining columns.
Value::record(columns.zip(values).collect(), span) Value::record(columns.zip(values).collect(), span)
}); });

View file

@ -11,7 +11,10 @@ impl Command for FromTsv {
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build("from tsv") Signature::build("from tsv")
.input_output_types(vec![(Type::String, Type::table())]) .input_output_types(vec![
(Type::String, Type::table()),
(Type::String, Type::list(Type::Any)),
])
.named( .named(
"comment", "comment",
SyntaxShape::String, SyntaxShape::String,
@ -76,6 +79,21 @@ impl Command for FromTsv {
})], })],
)) ))
}, },
Example {
description: "Convert comma-separated data to a table, allowing variable number of columns per row and ignoring headers",
example: "\"value 1\nvalue 2\tdescription 2\" | from tsv --flexible --noheaders",
result: Some(Value::test_list (
vec![
Value::test_record(record! {
"column0" => Value::test_string("value 1"),
}),
Value::test_record(record! {
"column0" => Value::test_string("value 2"),
"column1" => Value::test_string("description 2"),
}),
],
))
},
Example { Example {
description: "Create a tsv file with header columns and open it", description: "Create a tsv file with header columns and open it",
example: r#"$'c1(char tab)c2(char tab)c3(char nl)1(char tab)2(char tab)3' | save tsv-data | open tsv-data | from tsv"#, example: r#"$'c1(char tab)c2(char tab)c3(char nl)1(char tab)2(char tab)3' | save tsv-data | open tsv-data | from tsv"#,

View file

@ -4,7 +4,7 @@ use nu_engine::command_prelude::*;
use quick_xml::{ use quick_xml::{
escape, escape,
events::{BytesEnd, BytesStart, BytesText, Event}, events::{BytesEnd, BytesPI, BytesStart, BytesText, Event},
}; };
use std::{borrow::Cow, io::Cursor}; use std::{borrow::Cow, io::Cursor};
@ -406,7 +406,7 @@ impl Job {
let content_text = format!("{} {}", tag, content); let content_text = format!("{} {}", tag, content);
// PI content must NOT be escaped // PI content must NOT be escaped
// https://www.w3.org/TR/xml/#sec-pi // https://www.w3.org/TR/xml/#sec-pi
let pi_content = BytesText::from_escaped(content_text.as_str()); let pi_content = BytesPI::new(content_text.as_str());
self.writer self.writer
.write_event(Event::PI(pi_content)) .write_event(Event::PI(pi_content))

View file

@ -31,7 +31,7 @@ impl Command for HelpOperators {
let mut operators = [ let mut operators = [
Operator::Assignment(Assignment::Assign), Operator::Assignment(Assignment::Assign),
Operator::Assignment(Assignment::PlusAssign), Operator::Assignment(Assignment::PlusAssign),
Operator::Assignment(Assignment::AppendAssign), Operator::Assignment(Assignment::ConcatAssign),
Operator::Assignment(Assignment::MinusAssign), Operator::Assignment(Assignment::MinusAssign),
Operator::Assignment(Assignment::MultiplyAssign), Operator::Assignment(Assignment::MultiplyAssign),
Operator::Assignment(Assignment::DivideAssign), Operator::Assignment(Assignment::DivideAssign),
@ -48,7 +48,7 @@ impl Command for HelpOperators {
Operator::Comparison(Comparison::StartsWith), Operator::Comparison(Comparison::StartsWith),
Operator::Comparison(Comparison::EndsWith), Operator::Comparison(Comparison::EndsWith),
Operator::Math(Math::Plus), Operator::Math(Math::Plus),
Operator::Math(Math::Append), Operator::Math(Math::Concat),
Operator::Math(Math::Minus), Operator::Math(Math::Minus),
Operator::Math(Math::Multiply), Operator::Math(Math::Multiply),
Operator::Math(Math::Divide), Operator::Math(Math::Divide),
@ -144,8 +144,8 @@ fn description(operator: &Operator) -> &'static str {
Operator::Comparison(Comparison::StartsWith) => "Checks if a string starts with another.", Operator::Comparison(Comparison::StartsWith) => "Checks if a string starts with another.",
Operator::Comparison(Comparison::EndsWith) => "Checks if a string ends with another.", Operator::Comparison(Comparison::EndsWith) => "Checks if a string ends with another.",
Operator::Math(Math::Plus) => "Adds two values.", Operator::Math(Math::Plus) => "Adds two values.",
Operator::Math(Math::Append) => { Operator::Math(Math::Concat) => {
"Appends two lists, a list and a value, two strings, or two binary values." "Concatenates two lists, two strings, or two binary values."
} }
Operator::Math(Math::Minus) => "Subtracts two values.", Operator::Math(Math::Minus) => "Subtracts two values.",
Operator::Math(Math::Multiply) => "Multiplies two values.", Operator::Math(Math::Multiply) => "Multiplies two values.",
@ -163,8 +163,8 @@ fn description(operator: &Operator) -> &'static str {
Operator::Bits(Bits::ShiftRight) => "Bitwise shifts a value right by another.", Operator::Bits(Bits::ShiftRight) => "Bitwise shifts a value right by another.",
Operator::Assignment(Assignment::Assign) => "Assigns a value to a variable.", Operator::Assignment(Assignment::Assign) => "Assigns a value to a variable.",
Operator::Assignment(Assignment::PlusAssign) => "Adds a value to a variable.", Operator::Assignment(Assignment::PlusAssign) => "Adds a value to a variable.",
Operator::Assignment(Assignment::AppendAssign) => { Operator::Assignment(Assignment::ConcatAssign) => {
"Appends a list, a value, a string, or a binary value to a variable." "Concatenates two lists, two strings, or two binary values."
} }
Operator::Assignment(Assignment::MinusAssign) => "Subtracts a value from a variable.", Operator::Assignment(Assignment::MinusAssign) => "Subtracts a value from a variable.",
Operator::Assignment(Assignment::MultiplyAssign) => "Multiplies a variable by a value.", Operator::Assignment(Assignment::MultiplyAssign) => "Multiplies a variable by a value.",

View file

@ -376,7 +376,7 @@ fn send_multipart_request(
format!("Content-Length: {}", val.len()), format!("Content-Length: {}", val.len()),
]; ];
builder builder
.add(&mut Cursor::new(val), &headers.join("\n")) .add(&mut Cursor::new(val), &headers.join("\r\n"))
.map_err(err)?; .map_err(err)?;
} else { } else {
let headers = format!(r#"Content-Disposition: form-data; name="{}""#, col); let headers = format!(r#"Content-Disposition: form-data; name="{}""#, col);

View file

@ -43,6 +43,12 @@ impl Command for Input {
"number of characters to read; suppresses output", "number of characters to read; suppresses output",
Some('n'), Some('n'),
) )
.named(
"default",
SyntaxShape::String,
"default value if no input is provided",
Some('d'),
)
.switch("suppress-output", "don't print keystroke values", Some('s')) .switch("suppress-output", "don't print keystroke values", Some('s'))
.category(Category::Platform) .category(Category::Platform)
} }
@ -72,8 +78,12 @@ impl Command for Input {
}); });
} }
let default_val: Option<String> = call.get_flag(engine_state, stack, "default")?;
if let Some(prompt) = &prompt { if let Some(prompt) = &prompt {
print!("{prompt}"); match &default_val {
None => print!("{prompt}"),
Some(val) => print!("{prompt} (default: {val})"),
}
let _ = std::io::stdout().flush(); let _ = std::io::stdout().flush();
} }
@ -149,7 +159,10 @@ impl Command for Input {
if !suppress_output { if !suppress_output {
std::io::stdout().write_all(b"\n")?; std::io::stdout().write_all(b"\n")?;
} }
Ok(Value::string(buf, call.head).into_pipeline_data()) match default_val {
Some(val) if buf.is_empty() => Ok(Value::string(val, call.head).into_pipeline_data()),
_ => Ok(Value::string(buf, call.head).into_pipeline_data()),
}
} }
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
@ -164,6 +177,11 @@ impl Command for Input {
example: "let user_input = (input --numchar 2)", example: "let user_input = (input --numchar 2)",
result: None, result: None,
}, },
Example {
description: "Get input from the user with default value, and assign to a variable",
example: "let user_input = (input --default 10)",
result: None,
},
] ]
} }
} }

View file

@ -5,7 +5,7 @@ mod input;
mod is_terminal; mod is_terminal;
mod kill; mod kill;
mod sleep; mod sleep;
mod term_size; mod term;
#[cfg(unix)] #[cfg(unix)]
mod ulimit; mod ulimit;
mod whoami; mod whoami;
@ -19,7 +19,7 @@ pub use input::InputListen;
pub use is_terminal::IsTerminal; pub use is_terminal::IsTerminal;
pub use kill::Kill; pub use kill::Kill;
pub use sleep::Sleep; pub use sleep::Sleep;
pub use term_size::TermSize; pub use term::{Term, TermQuery, TermSize};
#[cfg(unix)] #[cfg(unix)]
pub use ulimit::ULimit; pub use ulimit::ULimit;
pub use whoami::Whoami; pub use whoami::Whoami;

View file

@ -0,0 +1,7 @@
mod term_;
mod term_query;
mod term_size;
pub use term_::Term;
pub use term_query::TermQuery;
pub use term_size::TermSize;

View file

@ -0,0 +1,34 @@
use nu_engine::{command_prelude::*, get_full_help};
#[derive(Clone)]
pub struct Term;
impl Command for Term {
fn name(&self) -> &str {
"term"
}
fn signature(&self) -> Signature {
Signature::build("term")
.category(Category::Platform)
.input_output_types(vec![(Type::Nothing, Type::String)])
}
fn description(&self) -> &str {
"Commands for querying information about the terminal."
}
fn extra_description(&self) -> &str {
"You must use one of the following subcommands. Using this command as-is will only produce this help message."
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(Value::string(get_full_help(self, engine_state, stack), call.head).into_pipeline_data())
}
}

View file

@ -0,0 +1,137 @@
use std::{
io::{Read, Write},
time::Duration,
};
use nu_engine::command_prelude::*;
const CTRL_C: u8 = 3;
#[derive(Clone)]
pub struct TermQuery;
impl Command for TermQuery {
fn name(&self) -> &str {
"term query"
}
fn description(&self) -> &str {
"Query the terminal for information."
}
fn extra_description(&self) -> &str {
"Print the given query, and read the immediate result from stdin.
The standard input will be read right after `query` is printed, and consumed until the `terminator`
sequence is encountered. The `terminator` is not removed from the output.
If `terminator` is not supplied, input will be read until Ctrl-C is pressed."
}
fn signature(&self) -> Signature {
Signature::build("term query")
.category(Category::Platform)
.input_output_types(vec![(Type::Nothing, Type::Binary)])
.allow_variants_without_examples(true)
.required(
"query",
SyntaxShape::OneOf(vec![SyntaxShape::Binary, SyntaxShape::String]),
"The query that will be printed to stdout.",
)
.named(
"terminator",
SyntaxShape::OneOf(vec![SyntaxShape::Binary, SyntaxShape::String]),
"Terminator sequence for the expected reply.",
Some('t'),
)
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Get cursor position.",
example: r#"term query (ansi cursor_position) --terminator 'R'"#,
result: None,
},
Example {
description: "Get terminal background color.",
example: r#"term query $'(ansi osc)10;?(ansi st)' --terminator (ansi st)"#,
result: None,
},
Example {
description: "Read clipboard content on terminals supporting OSC-52.",
example: r#"term query $'(ansi osc)52;c;?(ansi st)' --terminator (ansi st)"#,
result: None,
},
]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let query: Vec<u8> = call.req(engine_state, stack, 0)?;
let terminator: Option<Vec<u8>> = call.get_flag(engine_state, stack, "terminator")?;
crossterm::terminal::enable_raw_mode()?;
// clear terminal events
while crossterm::event::poll(Duration::from_secs(0))? {
// If there's an event, read it to remove it from the queue
let _ = crossterm::event::read()?;
}
let mut b = [0u8; 1];
let mut buf = vec![];
let mut stdin = std::io::stdin().lock();
{
let mut stdout = std::io::stdout().lock();
stdout.write_all(&query)?;
stdout.flush()?;
}
let out = if let Some(terminator) = terminator {
loop {
if let Err(err) = stdin.read_exact(&mut b) {
break Err(ShellError::from(err));
}
if b[0] == CTRL_C {
break Err(ShellError::Interrupted { span: call.head });
}
buf.push(b[0]);
if buf.ends_with(&terminator) {
break Ok(Value::Binary {
val: buf,
internal_span: call.head,
}
.into_pipeline_data());
}
}
} else {
loop {
if let Err(err) = stdin.read_exact(&mut b) {
break Err(ShellError::from(err));
}
if b[0] == CTRL_C {
break Ok(Value::Binary {
val: buf,
internal_span: call.head,
}
.into_pipeline_data());
}
buf.push(b[0]);
}
};
crossterm::terminal::disable_raw_mode()?;
out
}
}

View file

@ -1,5 +1,5 @@
use crossterm::terminal::size;
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use terminal_size::{terminal_size, Height, Width};
#[derive(Clone)] #[derive(Clone)]
pub struct TermSize; pub struct TermSize;
@ -51,15 +51,12 @@ impl Command for TermSize {
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; let head = call.head;
let (cols, rows) = match terminal_size() { let (cols, rows) = size().unwrap_or((0, 0));
Some((w, h)) => (Width(w.0), Height(h.0)),
None => (Width(0), Height(0)),
};
Ok(Value::record( Ok(Value::record(
record! { record! {
"columns" => Value::int(cols.0 as i64, head), "columns" => Value::int(cols as i64, head),
"rows" => Value::int(rows.0 as i64, head), "rows" => Value::int(rows as i64, head),
}, },
head, head,
) )

View file

@ -12,7 +12,7 @@ impl Command for SubCommand {
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build("random dice") Signature::build("random dice")
.input_output_types(vec![(Type::Nothing, Type::ListStream)]) .input_output_types(vec![(Type::Nothing, Type::list(Type::Int))])
.allow_variants_without_examples(true) .allow_variants_without_examples(true)
.named( .named(
"dice", "dice",

View file

@ -77,20 +77,6 @@ impl Command for SubCommand {
call: &Call, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
if call.has_flag_const(working_set, "not")? {
nu_protocol::report_shell_error(
working_set.permanent(),
&ShellError::GenericError {
error: "Deprecated option".into(),
msg: "`str contains --not {string}` is deprecated and will be removed in 0.95."
.into(),
span: Some(call.head),
help: Some("Please use the `not` operator instead.".into()),
inner: vec![],
},
);
}
let cell_paths: Vec<CellPath> = call.rest_const(working_set, 1)?; let cell_paths: Vec<CellPath> = call.rest_const(working_set, 1)?;
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
let args = Arguments { let args = Arguments {

View file

@ -307,7 +307,7 @@ fn test_one_newline() {
correct_counts.insert(Counter::GraphemeClusters, 1); correct_counts.insert(Counter::GraphemeClusters, 1);
correct_counts.insert(Counter::Bytes, 1); correct_counts.insert(Counter::Bytes, 1);
correct_counts.insert(Counter::CodePoints, 1); correct_counts.insert(Counter::CodePoints, 1);
correct_counts.insert(Counter::UnicodeWidth, 0); correct_counts.insert(Counter::UnicodeWidth, 1);
assert_eq!(correct_counts, counts); assert_eq!(correct_counts, counts);
} }
@ -347,7 +347,7 @@ fn test_count_counts_lines() {
// one more than grapheme clusters because of \r\n // one more than grapheme clusters because of \r\n
correct_counts.insert(Counter::CodePoints, 24); correct_counts.insert(Counter::CodePoints, 24);
correct_counts.insert(Counter::UnicodeWidth, 17); correct_counts.insert(Counter::UnicodeWidth, 23);
assert_eq!(correct_counts, counts); assert_eq!(correct_counts, counts);
} }

View file

@ -15,7 +15,6 @@ impl Command for NuCheck {
Signature::build("nu-check") Signature::build("nu-check")
.input_output_types(vec![ .input_output_types(vec![
(Type::String, Type::Bool), (Type::String, Type::Bool),
(Type::ListStream, Type::Bool),
(Type::List(Box::new(Type::Any)), Type::Bool), (Type::List(Box::new(Type::Any)), Type::Bool),
]) ])
// type is string to avoid automatically canonicalizing the path // type is string to avoid automatically canonicalizing the path

View file

@ -5,6 +5,8 @@ use nu_protocol::{did_you_mean, process::ChildProcess, ByteStream, NuGlob, OutDe
use nu_system::ForegroundChild; use nu_system::ForegroundChild;
use nu_utils::IgnoreCaseExt; use nu_utils::IgnoreCaseExt;
use pathdiff::diff_paths; use pathdiff::diff_paths;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
use std::{ use std::{
borrow::Cow, borrow::Cow,
ffi::{OsStr, OsString}, ffi::{OsStr, OsString},
@ -91,6 +93,22 @@ impl Command for External {
false false
}; };
// let's make sure it's a .ps1 script, but only on Windows
let potential_powershell_script = if cfg!(windows) {
if let Some(executable) = which(&expanded_name, "", cwd.as_ref()) {
let ext = executable
.extension()
.unwrap_or_default()
.to_string_lossy()
.to_uppercase();
ext == "PS1"
} else {
false
}
} else {
false
};
// Find the absolute path to the executable. On Windows, set the // Find the absolute path to the executable. On Windows, set the
// executable to "cmd.exe" if it's a CMD internal command. If the // executable to "cmd.exe" if it's a CMD internal command. If the
// command is not found, display a helpful error message. // command is not found, display a helpful error message.
@ -98,11 +116,16 @@ impl Command for External {
&& (is_cmd_internal_command(&name_str) || potential_nuscript_in_windows) && (is_cmd_internal_command(&name_str) || potential_nuscript_in_windows)
{ {
PathBuf::from("cmd.exe") PathBuf::from("cmd.exe")
} else if cfg!(windows) && potential_powershell_script {
// If we're on Windows and we're trying to run a PowerShell script, we'll use
// `powershell.exe` to run it. We shouldn't have to check for powershell.exe because
// it's automatically installed on all modern windows systems.
PathBuf::from("powershell.exe")
} else { } else {
// Determine the PATH to be used and then use `which` to find it - though this has no // Determine the PATH to be used and then use `which` to find it - though this has no
// effect if it's an absolute path already // effect if it's an absolute path already
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?; let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
let Some(executable) = which(expanded_name, &paths, cwd.as_ref()) else { let Some(executable) = which(&expanded_name, &paths, cwd.as_ref()) else {
return Err(command_not_found(&name_str, call.head, engine_state, stack)); return Err(command_not_found(&name_str, call.head, engine_state, stack));
}; };
executable executable
@ -123,15 +146,29 @@ impl Command for External {
let args = eval_arguments_from_call(engine_state, stack, call)?; let args = eval_arguments_from_call(engine_state, stack, call)?;
#[cfg(windows)] #[cfg(windows)]
if is_cmd_internal_command(&name_str) || potential_nuscript_in_windows { if is_cmd_internal_command(&name_str) || potential_nuscript_in_windows {
use std::os::windows::process::CommandExt;
// The /D flag disables execution of AutoRun commands from registry. // The /D flag disables execution of AutoRun commands from registry.
// The /C flag followed by a command name instructs CMD to execute // The /C flag followed by a command name instructs CMD to execute
// that command and quit. // that command and quit.
command.args(["/D", "/C", &name_str]); command.args(["/D", "/C", &expanded_name.to_string_lossy()]);
for arg in &args { for arg in &args {
command.raw_arg(escape_cmd_argument(arg)?); command.raw_arg(escape_cmd_argument(arg)?);
} }
} else if potential_powershell_script {
use nu_path::canonicalize_with;
// canonicalize the path to the script so that tests pass
let canon_path = if let Ok(cwd) = engine_state.cwd_as_string(None) {
canonicalize_with(&expanded_name, cwd)?
} else {
// If we can't get the current working directory, just provide the expanded name
expanded_name
};
// The -Command flag followed by a script name instructs PowerShell to
// execute that script and quit.
command.args(["-Command", &canon_path.to_string_lossy()]);
for arg in &args {
command.raw_arg(arg.item.clone());
}
} else { } else {
command.args(args.into_iter().map(|s| s.item)); command.args(args.into_iter().map(|s| s.item));
} }

View file

@ -44,8 +44,29 @@ fn net(span: Span) -> Value {
let networks = Networks::new_with_refreshed_list() let networks = Networks::new_with_refreshed_list()
.iter() .iter()
.map(|(iface, data)| { .map(|(iface, data)| {
let ip_addresses = data
.ip_networks()
.iter()
.map(|ip| {
let protocol = match ip.addr {
std::net::IpAddr::V4(_) => "ipv4",
std::net::IpAddr::V6(_) => "ipv6",
};
Value::record(
record! {
"address" => Value::string(ip.addr.to_string(), span),
"protocol" => Value::string(protocol, span),
"loop" => Value::bool(ip.addr.is_loopback(), span),
"multicast" => Value::bool(ip.addr.is_multicast(), span),
},
span,
)
})
.collect();
let record = record! { let record = record! {
"name" => Value::string(trim_cstyle_null(iface), span), "name" => Value::string(trim_cstyle_null(iface), span),
"mac" => Value::string(data.mac_address().to_string(), span),
"ip" => Value::list(ip_addresses, span),
"sent" => Value::filesize(data.total_transmitted() as i64, span), "sent" => Value::filesize(data.total_transmitted() as i64, span),
"recv" => Value::filesize(data.total_received() as i64, span), "recv" => Value::filesize(data.total_received() as i64, span),
}; };

View file

@ -1,12 +1,12 @@
// use super::icons::{icon_for_file, iconify_style_ansi_to_nu}; // use super::icons::{icon_for_file, iconify_style_ansi_to_nu};
use super::icons::icon_for_file; use super::icons::icon_for_file;
use crossterm::terminal::size;
use lscolors::Style; use lscolors::Style;
use nu_engine::{command_prelude::*, env_to_string}; use nu_engine::{command_prelude::*, env_to_string};
use nu_protocol::Config; use nu_protocol::Config;
use nu_term_grid::grid::{Alignment, Cell, Direction, Filling, Grid, GridOptions}; use nu_term_grid::grid::{Alignment, Cell, Direction, Filling, Grid, GridOptions};
use nu_utils::get_ls_colors; use nu_utils::get_ls_colors;
use std::path::Path; use std::path::Path;
use terminal_size::{Height, Width};
#[derive(Clone)] #[derive(Clone)]
pub struct Griddle; pub struct Griddle;
@ -192,7 +192,7 @@ fn create_grid_output(
let cols = if let Some(col) = width_param { let cols = if let Some(col) = width_param {
col as u16 col as u16
} else if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() { } else if let Ok((w, _h)) = size() {
w w
} else { } else {
80u16 80u16

View file

@ -2,6 +2,7 @@
// overall reduce the redundant calls to StyleComputer etc. // overall reduce the redundant calls to StyleComputer etc.
// the goal is to configure it once... // the goal is to configure it once...
use crossterm::terminal::size;
use lscolors::{LsColors, Style}; use lscolors::{LsColors, Style};
use nu_color_config::{color_from_hex, StyleComputer, TextStyle}; use nu_color_config::{color_from_hex, StyleComputer, TextStyle};
use nu_engine::{command_prelude::*, env_to_string}; use nu_engine::{command_prelude::*, env_to_string};
@ -22,7 +23,6 @@ use std::{
str::FromStr, str::FromStr,
time::Instant, time::Instant,
}; };
use terminal_size::{Height, Width};
use url::Url; use url::Url;
const STREAM_PAGE_SIZE: usize = 1000; const STREAM_PAGE_SIZE: usize = 1000;
@ -30,7 +30,7 @@ const STREAM_PAGE_SIZE: usize = 1000;
fn get_width_param(width_param: Option<i64>) -> usize { fn get_width_param(width_param: Option<i64>) -> usize {
if let Some(col) = width_param { if let Some(col) = width_param {
col as usize col as usize
} else if let Some((Width(w), Height(_))) = terminal_size::terminal_size() { } else if let Ok((w, _h)) = size() {
w as usize w as usize
} else { } else {
80 80
@ -1088,7 +1088,7 @@ fn create_empty_placeholder(
let data = vec![vec![cell]]; let data = vec![vec![cell]];
let mut table = NuTable::from(data); let mut table = NuTable::from(data);
table.set_data_style(TextStyle::default().dimmed()); table.set_data_style(TextStyle::default().dimmed());
let out = TableOutput::new(table, false, false, false); let out = TableOutput::new(table, false, false, 1);
let style_computer = &StyleComputer::from_config(engine_state, stack); let style_computer = &StyleComputer::from_config(engine_state, stack);
let config = create_nu_table_config(&config, style_computer, &out, false, TableMode::default()); let config = create_nu_table_config(&config, style_computer, &out, false, TableMode::default());

View file

@ -1,88 +0,0 @@
use nu_test_support::nu;
#[test]
fn append_assign_int() {
let actual = nu!(r#"
mut a = [1 2];
$a ++= [3 4];
$a == [1 2 3 4]
"#);
assert_eq!(actual.out, "true")
}
#[test]
fn append_assign_string() {
let actual = nu!(r#"
mut a = [a b];
$a ++= [c d];
$a == [a b c d]
"#);
assert_eq!(actual.out, "true")
}
#[test]
fn append_assign_any() {
let actual = nu!(r#"
mut a = [1 2 a];
$a ++= [b 3];
$a == [1 2 a b 3]
"#);
assert_eq!(actual.out, "true")
}
#[test]
fn append_assign_both_empty() {
let actual = nu!(r#"
mut a = [];
$a ++= [];
$a == []
"#);
assert_eq!(actual.out, "true")
}
#[test]
fn append_assign_type_mismatch() {
let actual = nu!(r#"
mut a = [1 2];
$a ++= [a];
$a == [1 2 "a"]
"#);
assert_eq!(actual.out, "true")
}
#[test]
fn append_assign_single_element() {
let actual = nu!(r#"
mut a = ["list" "and"];
$a ++= "a single element";
$a == ["list" "and" "a single element"]
"#);
assert_eq!(actual.out, "true")
}
#[test]
fn append_assign_to_single_element() {
let actual = nu!(r#"
mut a = "string";
$a ++= ["and" "the" "list"];
$a == ["string" "and" "the" "list"]
"#);
assert_eq!(actual.out, "true")
}
#[test]
fn append_assign_single_to_single() {
let actual = nu!(r#"
mut a = 1;
$a ++= "and a single element";
"#);
assert!(actual.err.contains("nu::parser::unsupported_operation"));
}

View file

@ -0,0 +1,76 @@
use nu_test_support::nu;
#[test]
fn concat_assign_list_int() {
let actual = nu!(r#"
mut a = [1 2];
$a ++= [3 4];
$a == [1 2 3 4]
"#);
assert_eq!(actual.out, "true")
}
#[test]
fn concat_assign_list_string() {
let actual = nu!(r#"
mut a = [a b];
$a ++= [c d];
$a == [a b c d]
"#);
assert_eq!(actual.out, "true")
}
#[test]
fn concat_assign_any() {
let actual = nu!(r#"
mut a = [1 2 a];
$a ++= [b 3];
$a == [1 2 a b 3]
"#);
assert_eq!(actual.out, "true")
}
#[test]
fn concat_assign_both_empty() {
let actual = nu!(r#"
mut a = [];
$a ++= [];
$a == []
"#);
assert_eq!(actual.out, "true")
}
#[test]
fn concat_assign_string() {
let actual = nu!(r#"
mut a = 'hello';
$a ++= ' world';
$a == 'hello world'
"#);
assert_eq!(actual.out, "true")
}
#[test]
fn concat_assign_type_mismatch() {
let actual = nu!(r#"
mut a = [];
$a ++= 'str'
"#);
assert!(actual.err.contains("nu::parser::unsupported_operation"));
}
#[test]
fn concat_assign_runtime_type_mismatch() {
let actual = nu!(r#"
mut a = [];
$a ++= if true { 'str' }
"#);
assert!(actual.err.contains("nu::shell::type_mismatch"));
}

View file

@ -1 +1 @@
mod append_assign; mod concat;

View file

@ -2,6 +2,13 @@ use nu_test_support::nu;
use nu_test_support::playground::Playground; use nu_test_support::playground::Playground;
use std::fs; use std::fs;
#[test]
fn def_with_trailing_comma() {
let actual = nu!("def test-command [ foo: int, ] { $foo }; test-command 1");
assert!(actual.out == "1");
}
#[test] #[test]
fn def_with_comment() { fn def_with_comment() {
Playground::setup("def_with_comment", |dirs, _| { Playground::setup("def_with_comment", |dirs, _| {
@ -72,6 +79,13 @@ fn def_errors_with_comma_before_equals() {
assert!(actual.err.contains("expected parameter")); assert!(actual.err.contains("expected parameter"));
} }
#[test]
fn def_errors_with_colon_before_equals() {
let actual = nu!("def test-command [ foo: = 1 ] {}");
assert!(actual.err.contains("expected type"));
}
#[test] #[test]
fn def_errors_with_comma_before_colon() { fn def_errors_with_comma_before_colon() {
let actual = nu!("def test-command [ foo, : int ] {}"); let actual = nu!("def test-command [ foo, : int ] {}");
@ -85,7 +99,6 @@ fn def_errors_with_multiple_colons() {
assert!(actual.err.contains("expected type")); assert!(actual.err.contains("expected type"));
} }
#[ignore = "This error condition is not implemented yet"]
#[test] #[test]
fn def_errors_with_multiple_types() { fn def_errors_with_multiple_types() {
let actual = nu!("def test-command [ foo:int:string ] {}"); let actual = nu!("def test-command [ foo:int:string ] {}");
@ -93,6 +106,20 @@ fn def_errors_with_multiple_types() {
assert!(actual.err.contains("expected parameter")); assert!(actual.err.contains("expected parameter"));
} }
#[test]
fn def_errors_with_trailing_colon() {
let actual = nu!("def test-command [ foo: int: ] {}");
assert!(actual.err.contains("expected parameter"));
}
#[test]
fn def_errors_with_trailing_default_value() {
let actual = nu!("def test-command [ foo: int = ] {}");
assert!(actual.err.contains("expected default value"));
}
#[test] #[test]
fn def_errors_with_multiple_commas() { fn def_errors_with_multiple_commas() {
let actual = nu!("def test-command [ foo,,bar ] {}"); let actual = nu!("def test-command [ foo,,bar ] {}");

View file

@ -44,7 +44,7 @@ fn do_with_semicolon_break_on_failed_external() {
fn ignore_shell_errors_works_for_external_with_semicolon() { fn ignore_shell_errors_works_for_external_with_semicolon() {
let actual = nu!(r#"do -s { open asdfasdf.txt }; "text""#); let actual = nu!(r#"do -s { open asdfasdf.txt }; "text""#);
assert_eq!(actual.err, ""); assert!(actual.err.contains("Deprecated option"));
assert_eq!(actual.out, "text"); assert_eq!(actual.out, "text");
} }
@ -52,7 +52,7 @@ fn ignore_shell_errors_works_for_external_with_semicolon() {
fn ignore_program_errors_works_for_external_with_semicolon() { fn ignore_program_errors_works_for_external_with_semicolon() {
let actual = nu!(r#"do -p { nu -n -c 'exit 1' }; "text""#); let actual = nu!(r#"do -p { nu -n -c 'exit 1' }; "text""#);
assert_eq!(actual.err, ""); assert!(actual.err.contains("Deprecated option"));
assert_eq!(actual.out, "text"); assert_eq!(actual.out, "text");
} }
@ -80,6 +80,7 @@ fn run_closure_with_it_using() {
#[test] #[test]
fn waits_for_external() { fn waits_for_external() {
let actual = nu!(r#"do -p { nu -c 'sleep 1sec; print before; exit 1'}; print after"#); let actual = nu!(r#"do -p { nu -c 'sleep 1sec; print before; exit 1'}; print after"#);
assert!(actual.err.is_empty());
assert!(actual.err.contains("Deprecated option"));
assert_eq!(actual.out, "beforeafter"); assert_eq!(actual.out, "beforeafter");
} }

View file

@ -483,7 +483,7 @@ fn compound_where_paren() {
// TODO: these ++ tests are not really testing *math* functionality, maybe find another place for them // TODO: these ++ tests are not really testing *math* functionality, maybe find another place for them
#[test] #[test]
fn adding_lists() { fn concat_lists() {
let actual = nu!(pipeline( let actual = nu!(pipeline(
r#" r#"
[1 3] ++ [5 6] | to nuon [1 3] ++ [5 6] | to nuon
@ -494,29 +494,7 @@ fn adding_lists() {
} }
#[test] #[test]
fn adding_list_and_value() { fn concat_tables() {
let actual = nu!(pipeline(
r#"
[1 3] ++ 5 | to nuon
"#
));
assert_eq!(actual.out, "[1, 3, 5]");
}
#[test]
fn adding_value_and_list() {
let actual = nu!(pipeline(
r#"
1 ++ [3 5] | to nuon
"#
));
assert_eq!(actual.out, "[1, 3, 5]");
}
#[test]
fn adding_tables() {
let actual = nu!(pipeline( let actual = nu!(pipeline(
r#" r#"
[[a b]; [1 2]] ++ [[c d]; [10 11]] | to nuon [[a b]; [1 2]] ++ [[c d]; [10 11]] | to nuon
@ -526,7 +504,7 @@ fn adding_tables() {
} }
#[test] #[test]
fn append_strings() { fn concat_strings() {
let actual = nu!(pipeline( let actual = nu!(pipeline(
r#" r#"
"foo" ++ "bar" "foo" ++ "bar"
@ -536,7 +514,7 @@ fn append_strings() {
} }
#[test] #[test]
fn append_binary_values() { fn concat_binary_values() {
let actual = nu!(pipeline( let actual = nu!(pipeline(
r#" r#"
0x[01 02] ++ 0x[03 04] | to nuon 0x[01 02] ++ 0x[03 04] | to nuon

View file

@ -127,6 +127,7 @@ mod update;
mod upsert; mod upsert;
mod url; mod url;
mod use_; mod use_;
mod utouch;
mod where_; mod where_;
mod which; mod which;
mod while_; mod while_;

View file

@ -513,13 +513,18 @@ fn test_mv_no_clobber() {
sandbox.with_files(&[EmptyFile(file_a)]); sandbox.with_files(&[EmptyFile(file_a)]);
sandbox.with_files(&[EmptyFile(file_b)]); sandbox.with_files(&[EmptyFile(file_b)]);
let actual = nu!( let _ = nu!(
cwd: dirs.test(), cwd: dirs.test(),
"mv -n {} {}", "mv -n {} {}",
file_a, file_a,
file_b, file_b,
); );
assert!(actual.err.contains("not replacing"));
let file_count = nu!(
cwd: dirs.test(),
"ls test_mv* | length | to nuon"
);
assert_eq!(file_count.out, "2");
}) })
} }

View file

@ -355,9 +355,9 @@ fn external_command_receives_raw_binary_data() {
#[cfg(windows)] #[cfg(windows)]
#[test] #[test]
fn can_run_batch_files() { fn can_run_cmd_files() {
use nu_test_support::fs::Stub::FileWithContent; use nu_test_support::fs::Stub::FileWithContent;
Playground::setup("run a Windows batch file", |dirs, sandbox| { Playground::setup("run a Windows cmd file", |dirs, sandbox| {
sandbox.with_files(&[FileWithContent( sandbox.with_files(&[FileWithContent(
"foo.cmd", "foo.cmd",
r#" r#"
@ -371,12 +371,30 @@ fn can_run_batch_files() {
}); });
} }
#[cfg(windows)]
#[test]
fn can_run_batch_files() {
use nu_test_support::fs::Stub::FileWithContent;
Playground::setup("run a Windows batch file", |dirs, sandbox| {
sandbox.with_files(&[FileWithContent(
"foo.bat",
r#"
@echo off
echo Hello World
"#,
)]);
let actual = nu!(cwd: dirs.test(), pipeline("foo.bat"));
assert!(actual.out.contains("Hello World"));
});
}
#[cfg(windows)] #[cfg(windows)]
#[test] #[test]
fn can_run_batch_files_without_cmd_extension() { fn can_run_batch_files_without_cmd_extension() {
use nu_test_support::fs::Stub::FileWithContent; use nu_test_support::fs::Stub::FileWithContent;
Playground::setup( Playground::setup(
"run a Windows batch file without specifying the extension", "run a Windows cmd file without specifying the extension",
|dirs, sandbox| { |dirs, sandbox| {
sandbox.with_files(&[FileWithContent( sandbox.with_files(&[FileWithContent(
"foo.cmd", "foo.cmd",
@ -440,3 +458,20 @@ fn redirect_combine() {
assert_eq!(actual.out, "FooBar"); assert_eq!(actual.out, "FooBar");
}); });
} }
#[cfg(windows)]
#[test]
fn can_run_ps1_files() {
use nu_test_support::fs::Stub::FileWithContent;
Playground::setup("run_a_windows_ps_file", |dirs, sandbox| {
sandbox.with_files(&[FileWithContent(
"foo.ps1",
r#"
Write-Host Hello World
"#,
)]);
let actual = nu!(cwd: dirs.test(), pipeline("foo.ps1"));
assert!(actual.out.contains("Hello World"));
});
}

View file

@ -2941,3 +2941,123 @@ fn table_footer_inheritance() {
assert_eq!(actual.out.match_indices("x2").count(), 1); assert_eq!(actual.out.match_indices("x2").count(), 1);
assert_eq!(actual.out.match_indices("x3").count(), 1); assert_eq!(actual.out.match_indices("x3").count(), 1);
} }
#[test]
fn table_footer_inheritance_kv_rows() {
let actual = nu!(
concat!(
"$env.config.table.footer_inheritance = true;",
"$env.config.footer_mode = 7;",
"[[a b]; ['kv' {0: 0, 1: 1, 2: 2, 3: 3, 4: 4} ], ['data' 0], ['data' 0] ] | table --expand --width=80",
)
);
assert_eq!(
actual.out,
"╭───┬──────┬───────────╮\
# a b \
\
0 kv \
0 0 \
1 1 \
2 2 \
3 3 \
4 4 \
\
1 data 0 \
2 data 0 \
"
);
let actual = nu!(
concat!(
"$env.config.table.footer_inheritance = true;",
"$env.config.footer_mode = 7;",
"[[a b]; ['kv' {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5} ], ['data' 0], ['data' 0] ] | table --expand --width=80",
)
);
assert_eq!(
actual.out,
"╭───┬──────┬───────────╮\
# a b \
\
0 kv \
0 0 \
1 1 \
2 2 \
3 3 \
4 4 \
5 5 \
\
1 data 0 \
2 data 0 \
\
# a b \
"
);
}
#[test]
fn table_footer_inheritance_list_rows() {
let actual = nu!(
concat!(
"$env.config.table.footer_inheritance = true;",
"$env.config.footer_mode = 7;",
"[[a b]; ['kv' {0: [[field]; [0] [1] [2] [3] [4]]} ], ['data' 0], ['data' 0] ] | table --expand --width=80",
)
);
assert_eq!(
actual.out,
"╭───┬──────┬───────────────────────╮\
# a b \
\
0 kv \
\
0 # field \
\
0 0 \
1 1 \
2 2 \
3 3 \
4 4 \
\
\
1 data 0 \
2 data 0 \
"
);
let actual = nu!(
concat!(
"$env.config.table.footer_inheritance = true;",
"$env.config.footer_mode = 7;",
"[[a b]; ['kv' {0: [[field]; [0] [1] [2] [3] [4] [5]]} ], ['data' 0], ['data' 0] ] | table --expand --width=80",
)
);
assert_eq!(
actual.out,
"╭───┬──────┬───────────────────────╮\
# a b \
\
0 kv \
\
0 # field \
\
0 0 \
1 1 \
2 2 \
3 3 \
4 4 \
5 5 \
\
\
1 data 0 \
2 data 0 \
\
# a b \
"
);
}

View file

@ -841,14 +841,13 @@ fn test_cp_arg_no_clobber() {
let target = dirs.fixtures.join("cp").join(TEST_HOW_ARE_YOU_SOURCE); let target = dirs.fixtures.join("cp").join(TEST_HOW_ARE_YOU_SOURCE);
let target_hash = get_file_hash(target.display()); let target_hash = get_file_hash(target.display());
let actual = nu!( let _ = nu!(
cwd: dirs.root(), cwd: dirs.root(),
"cp {} {} --no-clobber", "cp {} {} --no-clobber",
src.display(), src.display(),
target.display() target.display()
); );
let after_cp_hash = get_file_hash(target.display()); let after_cp_hash = get_file_hash(target.display());
assert!(actual.err.contains("not replacing"));
// Check content was not clobbered // Check content was not clobbered
assert_eq!(after_cp_hash, target_hash); assert_eq!(after_cp_hash, target_hash);
}); });

View file

@ -411,12 +411,9 @@ fn url_join_with_params_invalid_table() {
"host": "localhost", "host": "localhost",
"params": ( "params": (
[ [
["key", "value"]; { key: foo, value: bar }
["par_1", "aaa"], "not a record"
["par_2", "bbb"], ]
["par_1", "ccc"],
["par_2", "ddd"],
] ++ ["not a record"]
), ),
"port": "1234", "port": "1234",
} | url join } | url join

View file

@ -0,0 +1,740 @@
use chrono::{DateTime, Days, Local, TimeDelta, Utc};
use filetime::FileTime;
use nu_test_support::fs::{files_exist_at, Stub};
use nu_test_support::nu;
use nu_test_support::playground::{Dirs, Playground};
use std::path::Path;
// Use 1 instead of 0 because 0 has a special meaning in Windows
const TIME_ONE: FileTime = FileTime::from_unix_time(1, 0);
fn file_times(file: impl AsRef<Path>) -> (FileTime, FileTime) {
(
file.as_ref().metadata().unwrap().accessed().unwrap().into(),
file.as_ref().metadata().unwrap().modified().unwrap().into(),
)
}
fn symlink_times(path: &nu_path::AbsolutePath) -> (filetime::FileTime, filetime::FileTime) {
let metadata = path.symlink_metadata().unwrap();
(
filetime::FileTime::from_system_time(metadata.accessed().unwrap()),
filetime::FileTime::from_system_time(metadata.modified().unwrap()),
)
}
// From https://github.com/nushell/nushell/pull/14214
fn setup_symlink_fs(dirs: &Dirs, sandbox: &mut Playground<'_>) {
sandbox.mkdir("d");
sandbox.with_files(&[Stub::EmptyFile("f"), Stub::EmptyFile("d/f")]);
sandbox.symlink("f", "fs");
sandbox.symlink("d", "ds");
sandbox.symlink("d/f", "fds");
// sandbox.symlink does not handle symlinks to missing files well. It panics
// But they are useful, and they should be tested.
#[cfg(unix)]
{
std::os::unix::fs::symlink(dirs.test().join("m"), dirs.test().join("fms")).unwrap();
}
#[cfg(windows)]
{
std::os::windows::fs::symlink_file(dirs.test().join("m"), dirs.test().join("fms")).unwrap();
}
// Change the file times to a known "old" value for comparison
filetime::set_symlink_file_times(dirs.test().join("f"), TIME_ONE, TIME_ONE).unwrap();
filetime::set_symlink_file_times(dirs.test().join("d"), TIME_ONE, TIME_ONE).unwrap();
filetime::set_symlink_file_times(dirs.test().join("d/f"), TIME_ONE, TIME_ONE).unwrap();
filetime::set_symlink_file_times(dirs.test().join("ds"), TIME_ONE, TIME_ONE).unwrap();
filetime::set_symlink_file_times(dirs.test().join("fs"), TIME_ONE, TIME_ONE).unwrap();
filetime::set_symlink_file_times(dirs.test().join("fds"), TIME_ONE, TIME_ONE).unwrap();
filetime::set_symlink_file_times(dirs.test().join("fms"), TIME_ONE, TIME_ONE).unwrap();
}
#[test]
fn creates_a_file_when_it_doesnt_exist() {
Playground::setup("create_test_1", |dirs, _sandbox| {
nu!(
cwd: dirs.test(),
"utouch i_will_be_created.txt"
);
let path = dirs.test().join("i_will_be_created.txt");
assert!(path.exists());
})
}
#[test]
fn creates_two_files() {
Playground::setup("create_test_2", |dirs, _sandbox| {
nu!(
cwd: dirs.test(),
"utouch a b"
);
let path = dirs.test().join("a");
assert!(path.exists());
let path2 = dirs.test().join("b");
assert!(path2.exists());
})
}
#[test]
fn change_modified_time_of_file_to_today() {
Playground::setup("change_time_test_9", |dirs, sandbox| {
sandbox.with_files(&[Stub::EmptyFile("file.txt")]);
let path = dirs.test().join("file.txt");
// Set file.txt's times to the past before the test to make sure `utouch` actually changes the mtime to today
filetime::set_file_times(&path, TIME_ONE, TIME_ONE).unwrap();
nu!(
cwd: dirs.test(),
"utouch -m file.txt"
);
let metadata = path.metadata().unwrap();
// Check only the date since the time may not match exactly
let today = Local::now().date_naive();
let mtime_day = DateTime::<Local>::from(metadata.modified().unwrap()).date_naive();
assert_eq!(today, mtime_day);
// Check that atime remains unchanged
assert_eq!(
TIME_ONE,
FileTime::from_system_time(metadata.accessed().unwrap())
);
})
}
#[test]
fn change_access_time_of_file_to_today() {
Playground::setup("change_time_test_18", |dirs, sandbox| {
sandbox.with_files(&[Stub::EmptyFile("file.txt")]);
let path = dirs.test().join("file.txt");
// Set file.txt's times to the past before the test to make sure `utouch` actually changes the atime to today
filetime::set_file_times(&path, TIME_ONE, TIME_ONE).unwrap();
nu!(
cwd: dirs.test(),
"utouch -a file.txt"
);
let metadata = path.metadata().unwrap();
// Check only the date since the time may not match exactly
let today = Local::now().date_naive();
let atime_day = DateTime::<Local>::from(metadata.accessed().unwrap()).date_naive();
assert_eq!(today, atime_day);
// Check that mtime remains unchanged
assert_eq!(
TIME_ONE,
FileTime::from_system_time(metadata.modified().unwrap())
);
})
}
#[test]
fn change_modified_and_access_time_of_file_to_today() {
Playground::setup("change_time_test_27", |dirs, sandbox| {
sandbox.with_files(&[Stub::EmptyFile("file.txt")]);
let path = dirs.test().join("file.txt");
filetime::set_file_times(&path, TIME_ONE, TIME_ONE).unwrap();
nu!(
cwd: dirs.test(),
"utouch -a -m file.txt"
);
let metadata = path.metadata().unwrap();
// Check only the date since the time may not match exactly
let today = Local::now().date_naive();
let mtime_day = DateTime::<Local>::from(metadata.modified().unwrap()).date_naive();
let atime_day = DateTime::<Local>::from(metadata.accessed().unwrap()).date_naive();
assert_eq!(today, mtime_day);
assert_eq!(today, atime_day);
})
}
#[test]
fn not_create_file_if_it_not_exists() {
Playground::setup("change_time_test_28", |dirs, _sandbox| {
let outcome = nu!(
cwd: dirs.test(),
"utouch -c file.txt"
);
let path = dirs.test().join("file.txt");
assert!(!path.exists());
// If --no-create is improperly handled `utouch` may error when trying to change the times of a nonexistent file
assert!(outcome.status.success())
})
}
#[test]
fn change_file_times_if_exists_with_no_create() {
Playground::setup(
"change_file_times_if_exists_with_no_create",
|dirs, sandbox| {
sandbox.with_files(&[Stub::EmptyFile("file.txt")]);
let path = dirs.test().join("file.txt");
filetime::set_file_times(&path, TIME_ONE, TIME_ONE).unwrap();
nu!(
cwd: dirs.test(),
"utouch -c file.txt"
);
let metadata = path.metadata().unwrap();
// Check only the date since the time may not match exactly
let today = Local::now().date_naive();
let mtime_day = DateTime::<Local>::from(metadata.modified().unwrap()).date_naive();
let atime_day = DateTime::<Local>::from(metadata.accessed().unwrap()).date_naive();
assert_eq!(today, mtime_day);
assert_eq!(today, atime_day);
},
)
}
#[test]
fn creates_file_three_dots() {
Playground::setup("create_test_1", |dirs, _sandbox| {
nu!(
cwd: dirs.test(),
"utouch file..."
);
let path = dirs.test().join("file...");
assert!(path.exists());
})
}
#[test]
fn creates_file_four_dots() {
Playground::setup("create_test_1", |dirs, _sandbox| {
nu!(
cwd: dirs.test(),
"utouch file...."
);
let path = dirs.test().join("file....");
assert!(path.exists());
})
}
#[test]
fn creates_file_four_dots_quotation_marks() {
Playground::setup("create_test_1", |dirs, _sandbox| {
nu!(
cwd: dirs.test(),
"utouch 'file....'"
);
let path = dirs.test().join("file....");
assert!(path.exists());
})
}
#[test]
fn change_file_times_to_reference_file() {
Playground::setup("change_dir_times_to_reference_dir", |dirs, sandbox| {
sandbox.with_files(&[
Stub::EmptyFile("reference_file"),
Stub::EmptyFile("target_file"),
]);
let reference = dirs.test().join("reference_file");
let target = dirs.test().join("target_file");
// Change the times for reference
filetime::set_file_times(&reference, FileTime::from_unix_time(1337, 0), TIME_ONE).unwrap();
// target should have today's date since it was just created, but reference should be different
assert_ne!(
reference.metadata().unwrap().accessed().unwrap(),
target.metadata().unwrap().accessed().unwrap()
);
assert_ne!(
reference.metadata().unwrap().modified().unwrap(),
target.metadata().unwrap().modified().unwrap()
);
nu!(
cwd: dirs.test(),
"utouch -r reference_file target_file"
);
assert_eq!(
reference.metadata().unwrap().accessed().unwrap(),
target.metadata().unwrap().accessed().unwrap()
);
assert_eq!(
reference.metadata().unwrap().modified().unwrap(),
target.metadata().unwrap().modified().unwrap()
);
})
}
#[test]
fn change_file_mtime_to_reference() {
Playground::setup("change_file_mtime_to_reference", |dirs, sandbox| {
sandbox.with_files(&[
Stub::EmptyFile("reference_file"),
Stub::EmptyFile("target_file"),
]);
let reference = dirs.test().join("reference_file");
let target = dirs.test().join("target_file");
// Change the times for reference
filetime::set_file_times(&reference, TIME_ONE, FileTime::from_unix_time(1337, 0)).unwrap();
// target should have today's date since it was just created, but reference should be different
assert_ne!(file_times(&reference), file_times(&target));
// Save target's current atime to make sure it is preserved
let target_original_atime = target.metadata().unwrap().accessed().unwrap();
nu!(
cwd: dirs.test(),
"utouch -mr reference_file target_file"
);
assert_eq!(
reference.metadata().unwrap().modified().unwrap(),
target.metadata().unwrap().modified().unwrap()
);
assert_ne!(
reference.metadata().unwrap().accessed().unwrap(),
target.metadata().unwrap().accessed().unwrap()
);
assert_eq!(
target_original_atime,
target.metadata().unwrap().accessed().unwrap()
);
})
}
// TODO when https://github.com/uutils/coreutils/issues/6629 is fixed,
// unignore this test
#[test]
#[ignore]
fn change_file_times_to_reference_file_with_date() {
Playground::setup(
"change_file_times_to_reference_file_with_date",
|dirs, sandbox| {
sandbox.with_files(&[
Stub::EmptyFile("reference_file"),
Stub::EmptyFile("target_file"),
]);
let reference = dirs.test().join("reference_file");
let target = dirs.test().join("target_file");
let now = Utc::now();
let ref_atime = now;
let ref_mtime = now.checked_sub_days(Days::new(5)).unwrap();
// Change the times for reference
filetime::set_file_times(
reference,
FileTime::from_unix_time(ref_atime.timestamp(), ref_atime.timestamp_subsec_nanos()),
FileTime::from_unix_time(ref_mtime.timestamp(), ref_mtime.timestamp_subsec_nanos()),
)
.unwrap();
nu!(
cwd: dirs.test(),
r#"utouch -r reference_file -d "yesterday" target_file"#
);
let (got_atime, got_mtime) = file_times(target);
let got = (
DateTime::from_timestamp(got_atime.seconds(), got_atime.nanoseconds()).unwrap(),
DateTime::from_timestamp(got_mtime.seconds(), got_mtime.nanoseconds()).unwrap(),
);
assert_eq!(
(
now.checked_sub_days(Days::new(1)).unwrap(),
now.checked_sub_days(Days::new(6)).unwrap()
),
got
);
},
)
}
#[test]
fn change_file_times_to_timestamp() {
Playground::setup("change_file_times_to_timestamp", |dirs, sandbox| {
sandbox.with_files(&[Stub::EmptyFile("target_file")]);
let target = dirs.test().join("target_file");
let timestamp = DateTime::from_timestamp(TIME_ONE.unix_seconds(), TIME_ONE.nanoseconds())
.unwrap()
.to_rfc3339();
nu!(cwd: dirs.test(), format!("utouch --timestamp {} target_file", timestamp));
assert_eq!((TIME_ONE, TIME_ONE), file_times(target));
})
}
#[test]
fn change_modified_time_of_dir_to_today() {
Playground::setup("change_dir_mtime", |dirs, sandbox| {
sandbox.mkdir("test_dir");
let path = dirs.test().join("test_dir");
filetime::set_file_mtime(&path, TIME_ONE).unwrap();
nu!(
cwd: dirs.test(),
"utouch -m test_dir"
);
// Check only the date since the time may not match exactly
let today = Local::now().date_naive();
let mtime_day =
DateTime::<Local>::from(path.metadata().unwrap().modified().unwrap()).date_naive();
assert_eq!(today, mtime_day);
})
}
#[test]
fn change_access_time_of_dir_to_today() {
Playground::setup("change_dir_atime", |dirs, sandbox| {
sandbox.mkdir("test_dir");
let path = dirs.test().join("test_dir");
filetime::set_file_atime(&path, TIME_ONE).unwrap();
nu!(
cwd: dirs.test(),
"utouch -a test_dir"
);
// Check only the date since the time may not match exactly
let today = Local::now().date_naive();
let atime_day =
DateTime::<Local>::from(path.metadata().unwrap().accessed().unwrap()).date_naive();
assert_eq!(today, atime_day);
})
}
#[test]
fn change_modified_and_access_time_of_dir_to_today() {
Playground::setup("change_dir_times", |dirs, sandbox| {
sandbox.mkdir("test_dir");
let path = dirs.test().join("test_dir");
filetime::set_file_times(&path, TIME_ONE, TIME_ONE).unwrap();
nu!(
cwd: dirs.test(),
"utouch -a -m test_dir"
);
let metadata = path.metadata().unwrap();
// Check only the date since the time may not match exactly
let today = Local::now().date_naive();
let mtime_day = DateTime::<Local>::from(metadata.modified().unwrap()).date_naive();
let atime_day = DateTime::<Local>::from(metadata.accessed().unwrap()).date_naive();
assert_eq!(today, mtime_day);
assert_eq!(today, atime_day);
})
}
// TODO when https://github.com/uutils/coreutils/issues/6629 is fixed,
// unignore this test
#[test]
#[ignore]
fn change_file_times_to_date() {
Playground::setup("change_file_times_to_date", |dirs, sandbox| {
sandbox.with_files(&[Stub::EmptyFile("target_file")]);
let expected = Utc::now().checked_sub_signed(TimeDelta::hours(2)).unwrap();
nu!(cwd: dirs.test(), "utouch -d '-2 hours' target_file");
let (got_atime, got_mtime) = file_times(dirs.test().join("target_file"));
let got_atime =
DateTime::from_timestamp(got_atime.seconds(), got_atime.nanoseconds()).unwrap();
let got_mtime =
DateTime::from_timestamp(got_mtime.seconds(), got_mtime.nanoseconds()).unwrap();
let threshold = TimeDelta::minutes(1);
assert!(
got_atime.signed_duration_since(expected).lt(&threshold)
&& got_mtime.signed_duration_since(expected).lt(&threshold),
"Expected: {}. Got: atime={}, mtime={}",
expected,
got_atime,
got_mtime
);
assert!(got_mtime.signed_duration_since(expected).lt(&threshold));
})
}
#[test]
fn change_dir_three_dots_times() {
Playground::setup("change_dir_three_dots_times", |dirs, sandbox| {
sandbox.mkdir("test_dir...");
let path = dirs.test().join("test_dir...");
filetime::set_file_times(&path, TIME_ONE, TIME_ONE).unwrap();
nu!(
cwd: dirs.test(),
"utouch test_dir..."
);
let metadata = path.metadata().unwrap();
// Check only the date since the time may not match exactly
let today = Local::now().date_naive();
let mtime_day = DateTime::<Local>::from(metadata.modified().unwrap()).date_naive();
let atime_day = DateTime::<Local>::from(metadata.accessed().unwrap()).date_naive();
assert_eq!(today, mtime_day);
assert_eq!(today, atime_day);
})
}
#[test]
fn change_dir_times_to_reference_dir() {
Playground::setup("change_dir_times_to_reference_dir", |dirs, sandbox| {
sandbox.mkdir("reference_dir");
sandbox.mkdir("target_dir");
let reference = dirs.test().join("reference_dir");
let target = dirs.test().join("target_dir");
// Change the times for reference
filetime::set_file_times(&reference, FileTime::from_unix_time(1337, 0), TIME_ONE).unwrap();
// target should have today's date since it was just created, but reference should be different
assert_ne!(
reference.metadata().unwrap().accessed().unwrap(),
target.metadata().unwrap().accessed().unwrap()
);
assert_ne!(
reference.metadata().unwrap().modified().unwrap(),
target.metadata().unwrap().modified().unwrap()
);
nu!(
cwd: dirs.test(),
"utouch -r reference_dir target_dir"
);
assert_eq!(
reference.metadata().unwrap().accessed().unwrap(),
target.metadata().unwrap().accessed().unwrap()
);
assert_eq!(
reference.metadata().unwrap().modified().unwrap(),
target.metadata().unwrap().modified().unwrap()
);
})
}
#[test]
fn change_dir_atime_to_reference() {
Playground::setup("change_dir_atime_to_reference", |dirs, sandbox| {
sandbox.mkdir("reference_dir");
sandbox.mkdir("target_dir");
let reference = dirs.test().join("reference_dir");
let target = dirs.test().join("target_dir");
// Change the times for reference
filetime::set_file_times(&reference, FileTime::from_unix_time(1337, 0), TIME_ONE).unwrap();
// target should have today's date since it was just created, but reference should be different
assert_ne!(
reference.metadata().unwrap().accessed().unwrap(),
target.metadata().unwrap().accessed().unwrap()
);
assert_ne!(
reference.metadata().unwrap().modified().unwrap(),
target.metadata().unwrap().modified().unwrap()
);
// Save target's current mtime to make sure it is preserved
let target_original_mtime = target.metadata().unwrap().modified().unwrap();
nu!(
cwd: dirs.test(),
"utouch -ar reference_dir target_dir"
);
assert_eq!(
reference.metadata().unwrap().accessed().unwrap(),
target.metadata().unwrap().accessed().unwrap()
);
assert_ne!(
reference.metadata().unwrap().modified().unwrap(),
target.metadata().unwrap().modified().unwrap()
);
assert_eq!(
target_original_mtime,
target.metadata().unwrap().modified().unwrap()
);
})
}
#[test]
fn create_a_file_with_tilde() {
Playground::setup("utouch with tilde", |dirs, _| {
let actual = nu!(cwd: dirs.test(), "utouch '~tilde'");
assert!(actual.err.is_empty());
assert!(files_exist_at(&[Path::new("~tilde")], dirs.test()));
// pass variable
let actual = nu!(cwd: dirs.test(), "let f = '~tilde2'; utouch $f");
assert!(actual.err.is_empty());
assert!(files_exist_at(&[Path::new("~tilde2")], dirs.test()));
})
}
#[test]
fn respects_cwd() {
Playground::setup("utouch_respects_cwd", |dirs, _sandbox| {
nu!(
cwd: dirs.test(),
"mkdir 'dir'; cd 'dir'; utouch 'i_will_be_created.txt'"
);
let path = dirs.test().join("dir/i_will_be_created.txt");
assert!(path.exists());
})
}
#[test]
fn reference_respects_cwd() {
Playground::setup("utouch_reference_respects_cwd", |dirs, _sandbox| {
nu!(
cwd: dirs.test(),
"mkdir 'dir'; cd 'dir'; utouch 'ref.txt'; utouch --reference 'ref.txt' 'foo.txt'"
);
let path = dirs.test().join("dir/foo.txt");
assert!(path.exists());
})
}
#[test]
fn recognizes_stdout() {
Playground::setup("utouch_recognizes_stdout", |dirs, _sandbox| {
nu!(cwd: dirs.test(), "utouch -");
assert!(!dirs.test().join("-").exists());
})
}
#[test]
fn follow_symlinks() {
Playground::setup("touch_follows_symlinks", |dirs, sandbox| {
setup_symlink_fs(&dirs, sandbox);
let missing = dirs.test().join("m");
assert!(!missing.exists());
nu!(
cwd: dirs.test(),
"
touch fds
touch ds
touch fs
touch fms
"
);
// We created the missing symlink target
assert!(missing.exists());
// The timestamps for files and directories were changed from TIME_ONE
let file_times = symlink_times(&dirs.test().join("f"));
let dir_times = symlink_times(&dirs.test().join("d"));
let dir_file_times = symlink_times(&dirs.test().join("d/f"));
assert_ne!(file_times, (TIME_ONE, TIME_ONE));
assert_ne!(dir_times, (TIME_ONE, TIME_ONE));
assert_ne!(dir_file_times, (TIME_ONE, TIME_ONE));
// For symlinks, they remain (mostly) the same
// We can't test accessed times, since to reach the target file, the symlink must be accessed!
let file_symlink_times = symlink_times(&dirs.test().join("fs"));
let dir_symlink_times = symlink_times(&dirs.test().join("ds"));
let dir_file_symlink_times = symlink_times(&dirs.test().join("fds"));
let file_missing_symlink_times = symlink_times(&dirs.test().join("fms"));
assert_eq!(file_symlink_times.1, TIME_ONE);
assert_eq!(dir_symlink_times.1, TIME_ONE);
assert_eq!(dir_file_symlink_times.1, TIME_ONE);
assert_eq!(file_missing_symlink_times.1, TIME_ONE);
})
}
#[test]
fn no_follow_symlinks() {
Playground::setup("touch_touches_symlinks", |dirs, sandbox| {
setup_symlink_fs(&dirs, sandbox);
let missing = dirs.test().join("m");
assert!(!missing.exists());
nu!(
cwd: dirs.test(),
"
touch fds -s
touch ds -s
touch fs -s
touch fms -s
"
);
// We did not create the missing symlink target
assert!(!missing.exists());
// The timestamps for files and directories remain the same
let file_times = symlink_times(&dirs.test().join("f"));
let dir_times = symlink_times(&dirs.test().join("d"));
let dir_file_times = symlink_times(&dirs.test().join("d/f"));
assert_eq!(file_times, (TIME_ONE, TIME_ONE));
assert_eq!(dir_times, (TIME_ONE, TIME_ONE));
assert_eq!(dir_file_times, (TIME_ONE, TIME_ONE));
// For symlinks, everything changed. (except their targets, and paths, and personality)
let file_symlink_times = symlink_times(&dirs.test().join("fs"));
let dir_symlink_times = symlink_times(&dirs.test().join("ds"));
let dir_file_symlink_times = symlink_times(&dirs.test().join("fds"));
let file_missing_symlink_times = symlink_times(&dirs.test().join("fms"));
assert_ne!(file_symlink_times, (TIME_ONE, TIME_ONE));
assert_ne!(dir_symlink_times, (TIME_ONE, TIME_ONE));
assert_ne!(dir_file_symlink_times, (TIME_ONE, TIME_ONE));
assert_ne!(file_missing_symlink_times, (TIME_ONE, TIME_ONE));
})
}

View file

@ -469,7 +469,7 @@ fn from_csv_test_flexible_extra_vals() {
echo "a,b\n1,2,3" | from csv --flexible | first | values | to nuon echo "a,b\n1,2,3" | from csv --flexible | first | values | to nuon
"# "#
)); ));
assert_eq!(actual.out, "[1, 2]"); assert_eq!(actual.out, "[1, 2, 3]");
} }
#[test] #[test]
@ -479,5 +479,5 @@ fn from_csv_test_flexible_missing_vals() {
echo "a,b\n1" | from csv --flexible | first | values | to nuon echo "a,b\n1" | from csv --flexible | first | values | to nuon
"# "#
)); ));
assert_eq!(actual.out, "[1, null]"); assert_eq!(actual.out, "[1]");
} }

View file

@ -29,3 +29,20 @@ fn from_ods_file_to_table_select_sheet() {
assert_eq!(actual.out, "SalesOrders"); assert_eq!(actual.out, "SalesOrders");
} }
#[test]
fn from_ods_file_to_table_select_sheet_with_annotations() {
let actual = nu!(
cwd: "tests/fixtures/formats", pipeline(
r#"
open sample_data_with_annotation.ods --raw
| from ods --sheets ["SalesOrders"]
| get SalesOrders
| get column4
| get 0
"#
));
// The Units column in the sheet SalesOrders has an annotation and should be ignored.
assert_eq!(actual.out, "Units");
}

View file

@ -155,7 +155,7 @@ pub(crate) fn decompose_assignment(assignment: Assignment) -> Option<Operator> {
match assignment { match assignment {
Assignment::Assign => None, Assignment::Assign => None,
Assignment::PlusAssign => Some(Operator::Math(Math::Plus)), Assignment::PlusAssign => Some(Operator::Math(Math::Plus)),
Assignment::AppendAssign => Some(Operator::Math(Math::Append)), Assignment::ConcatAssign => Some(Operator::Math(Math::Concat)),
Assignment::MinusAssign => Some(Operator::Math(Math::Minus)), Assignment::MinusAssign => Some(Operator::Math(Math::Minus)),
Assignment::MultiplyAssign => Some(Operator::Math(Math::Multiply)), Assignment::MultiplyAssign => Some(Operator::Math(Math::Multiply)),
Assignment::DivideAssign => Some(Operator::Math(Math::Divide)), Assignment::DivideAssign => Some(Operator::Math(Math::Divide)),

View file

@ -547,9 +547,9 @@ impl Eval for EvalRuntime {
let lhs = eval_expression::<D>(engine_state, stack, lhs)?; let lhs = eval_expression::<D>(engine_state, stack, lhs)?;
lhs.div(op_span, &rhs, op_span)? lhs.div(op_span, &rhs, op_span)?
} }
Assignment::AppendAssign => { Assignment::ConcatAssign => {
let lhs = eval_expression::<D>(engine_state, stack, lhs)?; let lhs = eval_expression::<D>(engine_state, stack, lhs)?;
lhs.append(op_span, &rhs, op_span)? lhs.concat(op_span, &rhs, op_span)?
} }
}; };

View file

@ -956,7 +956,7 @@ fn binary_op(
}, },
Operator::Math(mat) => match mat { Operator::Math(mat) => match mat {
Math::Plus => lhs_val.add(op_span, &rhs_val, span)?, Math::Plus => lhs_val.add(op_span, &rhs_val, span)?,
Math::Append => lhs_val.append(op_span, &rhs_val, span)?, Math::Concat => lhs_val.concat(op_span, &rhs_val, span)?,
Math::Minus => lhs_val.sub(op_span, &rhs_val, span)?, Math::Minus => lhs_val.sub(op_span, &rhs_val, span)?,
Math::Multiply => lhs_val.mul(op_span, &rhs_val, span)?, Math::Multiply => lhs_val.mul(op_span, &rhs_val, span)?,
Math::Divide => lhs_val.div(op_span, &rhs_val, span)?, Math::Divide => lhs_val.div(op_span, &rhs_val, span)?,

View file

@ -27,7 +27,6 @@ nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.100.1" }
anyhow = { workspace = true } anyhow = { workspace = true }
log = { workspace = true } log = { workspace = true }
terminal_size = { workspace = true }
strip-ansi-escapes = { workspace = true } strip-ansi-escapes = { workspace = true }
crossterm = { workspace = true } crossterm = { workspace = true }
ratatui = { workspace = true } ratatui = { workspace = true }

View file

@ -9,6 +9,7 @@ mod views;
use anyhow::Result; use anyhow::Result;
use commands::{ExpandCmd, HelpCmd, NuCmd, QuitCmd, TableCmd, TryCmd}; use commands::{ExpandCmd, HelpCmd, NuCmd, QuitCmd, TableCmd, TryCmd};
use crossterm::terminal::size;
pub use default_context::add_explore_context; pub use default_context::add_explore_context;
pub use explore::Explore; pub use explore::Explore;
use explore::ExploreConfig; use explore::ExploreConfig;
@ -19,7 +20,6 @@ use nu_protocol::{
}; };
use pager::{Page, Pager, PagerConfig}; use pager::{Page, Pager, PagerConfig};
use registry::CommandRegistry; use registry::CommandRegistry;
use terminal_size::{Height, Width};
use views::{BinaryView, Orientation, Preview, RecordView}; use views::{BinaryView, Orientation, Preview, RecordView};
mod util { mod util {
@ -80,7 +80,7 @@ fn create_record_view(
} }
if config.tail { if config.tail {
if let Some((Width(w), Height(h))) = terminal_size::terminal_size() { if let Ok((w, h)) = size() {
view.tail(w, h); view.tail(w, h);
} }
} }

View file

@ -772,6 +772,51 @@ fn calculate_end_span(
} }
} }
fn parse_oneof(
working_set: &mut StateWorkingSet,
spans: &[Span],
spans_idx: &mut usize,
possible_shapes: &Vec<SyntaxShape>,
multispan: bool,
) -> Expression {
for shape in possible_shapes {
let starting_error_count = working_set.parse_errors.len();
let value = match multispan {
true => parse_multispan_value(working_set, spans, spans_idx, shape),
false => parse_value(working_set, spans[*spans_idx], shape),
};
if starting_error_count == working_set.parse_errors.len() {
return value;
}
// while trying the possible shapes, ignore Expected type errors
// unless they're inside a block, closure, or expression
let propagate_error = match working_set.parse_errors.last() {
Some(ParseError::Expected(_, error_span))
| Some(ParseError::ExpectedWithStringMsg(_, error_span)) => {
matches!(
shape,
SyntaxShape::Block | SyntaxShape::Closure(_) | SyntaxShape::Expression
) && *error_span != spans[*spans_idx]
}
_ => true,
};
if !propagate_error {
working_set.parse_errors.truncate(starting_error_count);
}
}
if working_set.parse_errors.is_empty() {
working_set.error(ParseError::ExpectedWithStringMsg(
format!("one of a list of accepted shapes: {possible_shapes:?}"),
spans[*spans_idx],
));
}
Expression::garbage(working_set, spans[*spans_idx])
}
pub fn parse_multispan_value( pub fn parse_multispan_value(
working_set: &mut StateWorkingSet, working_set: &mut StateWorkingSet,
spans: &[Span], spans: &[Span],
@ -800,54 +845,10 @@ pub fn parse_multispan_value(
arg arg
} }
SyntaxShape::OneOf(shapes) => { SyntaxShape::OneOf(possible_shapes) => {
// handle for `if` command. parse_oneof(working_set, spans, spans_idx, possible_shapes, true)
//let block_then_exp = shapes.as_slice() == [SyntaxShape::Block, SyntaxShape::Expression];
for shape in shapes.iter() {
let starting_error_count = working_set.parse_errors.len();
let s = parse_multispan_value(working_set, spans, spans_idx, shape);
if starting_error_count == working_set.parse_errors.len() {
return s;
} else if let Some(
ParseError::Expected(..) | ParseError::ExpectedWithStringMsg(..),
) = working_set.parse_errors.last()
{
working_set.parse_errors.truncate(starting_error_count);
continue;
}
// `if` is parsing block first and then expression.
// when we're writing something like `else if $a`, parsing as a
// block will result to error(because it's not a block)
//
// If parse as a expression also failed, user is more likely concerned
// about expression failure rather than "expect block failure"".
// FIXME FIXME FIXME
// if block_then_exp {
// match &err {
// Some(ParseError::Expected(expected, _)) => {
// if expected.starts_with("block") {
// err = e
// }
// }
// _ => err = err.or(e),
// }
// } else {
// err = err.or(e)
// }
}
let span = spans[*spans_idx];
if working_set.parse_errors.is_empty() {
working_set.error(ParseError::ExpectedWithStringMsg(
format!("one of a list of accepted shapes: {shapes:?}"),
span,
));
} }
Expression::garbage(working_set, span)
}
SyntaxShape::Expression => { SyntaxShape::Expression => {
trace!("parsing: expression"); trace!("parsing: expression");
@ -3392,6 +3393,7 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
Arg, Arg,
AfterCommaArg, AfterCommaArg,
Type, Type,
AfterType,
DefaultValue, DefaultValue,
} }
@ -3425,7 +3427,9 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
let mut args: Vec<Arg> = vec![]; let mut args: Vec<Arg> = vec![];
let mut parse_mode = ParseMode::Arg; let mut parse_mode = ParseMode::Arg;
for token in &output { for (index, token) in output.iter().enumerate() {
let last_token = index == output.len() - 1;
match token { match token {
Token { Token {
contents: crate::TokenContents::Item | crate::TokenContents::AssignmentOperator, contents: crate::TokenContents::Item | crate::TokenContents::AssignmentOperator,
@ -3437,10 +3441,12 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
// The : symbol separates types // The : symbol separates types
if contents == b":" { if contents == b":" {
match parse_mode { match parse_mode {
ParseMode::Arg if last_token => working_set
.error(ParseError::Expected("type", Span::new(span.end, span.end))),
ParseMode::Arg => { ParseMode::Arg => {
parse_mode = ParseMode::Type; parse_mode = ParseMode::Type;
} }
ParseMode::AfterCommaArg => { ParseMode::AfterCommaArg | ParseMode::AfterType => {
working_set.error(ParseError::Expected("parameter or flag", span)); working_set.error(ParseError::Expected("parameter or flag", span));
} }
ParseMode::Type | ParseMode::DefaultValue => { ParseMode::Type | ParseMode::DefaultValue => {
@ -3452,9 +3458,15 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
// The = symbol separates a variable from its default value // The = symbol separates a variable from its default value
else if contents == b"=" { else if contents == b"=" {
match parse_mode { match parse_mode {
ParseMode::Type | ParseMode::Arg => { ParseMode::Arg | ParseMode::AfterType if last_token => working_set.error(
ParseError::Expected("default value", Span::new(span.end, span.end)),
),
ParseMode::Arg | ParseMode::AfterType => {
parse_mode = ParseMode::DefaultValue; parse_mode = ParseMode::DefaultValue;
} }
ParseMode::Type => {
working_set.error(ParseError::Expected("type", span));
}
ParseMode::AfterCommaArg => { ParseMode::AfterCommaArg => {
working_set.error(ParseError::Expected("parameter or flag", span)); working_set.error(ParseError::Expected("parameter or flag", span));
} }
@ -3467,7 +3479,9 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
// The , symbol separates params only // The , symbol separates params only
else if contents == b"," { else if contents == b"," {
match parse_mode { match parse_mode {
ParseMode::Arg => parse_mode = ParseMode::AfterCommaArg, ParseMode::Arg | ParseMode::AfterType => {
parse_mode = ParseMode::AfterCommaArg
}
ParseMode::AfterCommaArg => { ParseMode::AfterCommaArg => {
working_set.error(ParseError::Expected("parameter or flag", span)); working_set.error(ParseError::Expected("parameter or flag", span));
} }
@ -3480,7 +3494,7 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
} }
} else { } else {
match parse_mode { match parse_mode {
ParseMode::Arg | ParseMode::AfterCommaArg => { ParseMode::Arg | ParseMode::AfterCommaArg | ParseMode::AfterType => {
// Long flag with optional short form following with no whitespace, e.g. --output, --age(-a) // Long flag with optional short form following with no whitespace, e.g. --output, --age(-a)
if contents.starts_with(b"--") && contents.len() > 2 { if contents.starts_with(b"--") && contents.len() > 2 {
// Split the long flag from the short flag with the ( character as delimiter. // Split the long flag from the short flag with the ( character as delimiter.
@ -3790,7 +3804,7 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
} }
} }
} }
parse_mode = ParseMode::Arg; parse_mode = ParseMode::AfterType;
} }
ParseMode::DefaultValue => { ParseMode::DefaultValue => {
if let Some(last) = args.last_mut() { if let Some(last) = args.last_mut() {
@ -4813,29 +4827,7 @@ pub fn parse_value(
SyntaxShape::ExternalArgument => parse_regular_external_arg(working_set, span), SyntaxShape::ExternalArgument => parse_regular_external_arg(working_set, span),
SyntaxShape::OneOf(possible_shapes) => { SyntaxShape::OneOf(possible_shapes) => {
for s in possible_shapes { parse_oneof(working_set, &[span], &mut 0, possible_shapes, false)
let starting_error_count = working_set.parse_errors.len();
let value = parse_value(working_set, span, s);
if starting_error_count == working_set.parse_errors.len() {
return value;
} else if let Some(
ParseError::Expected(..) | ParseError::ExpectedWithStringMsg(..),
) = working_set.parse_errors.last()
{
working_set.parse_errors.truncate(starting_error_count);
continue;
}
}
if working_set.parse_errors.is_empty() {
working_set.error(ParseError::ExpectedWithStringMsg(
format!("one of a list of accepted shapes: {possible_shapes:?}"),
span,
));
}
Expression::garbage(working_set, span)
} }
SyntaxShape::Any => { SyntaxShape::Any => {
@ -4895,7 +4887,7 @@ pub fn parse_assignment_operator(working_set: &mut StateWorkingSet, span: Span)
let operator = match contents { let operator = match contents {
b"=" => Operator::Assignment(Assignment::Assign), b"=" => Operator::Assignment(Assignment::Assign),
b"+=" => Operator::Assignment(Assignment::PlusAssign), b"+=" => Operator::Assignment(Assignment::PlusAssign),
b"++=" => Operator::Assignment(Assignment::AppendAssign), b"++=" => Operator::Assignment(Assignment::ConcatAssign),
b"-=" => Operator::Assignment(Assignment::MinusAssign), b"-=" => Operator::Assignment(Assignment::MinusAssign),
b"*=" => Operator::Assignment(Assignment::MultiplyAssign), b"*=" => Operator::Assignment(Assignment::MultiplyAssign),
b"/=" => Operator::Assignment(Assignment::DivideAssign), b"/=" => Operator::Assignment(Assignment::DivideAssign),
@ -5021,7 +5013,7 @@ pub fn parse_operator(working_set: &mut StateWorkingSet, span: Span) -> Expressi
b"=~" | b"like" => Operator::Comparison(Comparison::RegexMatch), b"=~" | b"like" => Operator::Comparison(Comparison::RegexMatch),
b"!~" | b"not-like" => Operator::Comparison(Comparison::NotRegexMatch), b"!~" | b"not-like" => Operator::Comparison(Comparison::NotRegexMatch),
b"+" => Operator::Math(Math::Plus), b"+" => Operator::Math(Math::Plus),
b"++" => Operator::Math(Math::Append), b"++" => Operator::Math(Math::Concat),
b"-" => Operator::Math(Math::Minus), b"-" => Operator::Math(Math::Minus),
b"*" => Operator::Math(Math::Multiply), b"*" => Operator::Math(Math::Multiply),
b"/" => Operator::Math(Math::Divide), b"/" => Operator::Math(Math::Divide),

View file

@ -29,8 +29,6 @@ pub fn type_compatible(lhs: &Type, rhs: &Type) -> bool {
match (lhs, rhs) { match (lhs, rhs) {
(Type::List(c), Type::List(d)) => type_compatible(c, d), (Type::List(c), Type::List(d)) => type_compatible(c, d),
(Type::ListStream, Type::List(_)) => true,
(Type::List(_), Type::ListStream) => true,
(Type::List(c), Type::Table(table_fields)) => { (Type::List(c), Type::Table(table_fields)) => {
if matches!(**c, Type::Any) { if matches!(**c, Type::Any) {
return true; return true;
@ -132,7 +130,7 @@ pub fn math_result_type(
) )
} }
}, },
Operator::Math(Math::Append) => check_append(working_set, lhs, rhs, op), Operator::Math(Math::Concat) => check_concat(working_set, lhs, rhs, op),
Operator::Math(Math::Minus) => match (&lhs.ty, &rhs.ty) { Operator::Math(Math::Minus) => match (&lhs.ty, &rhs.ty) {
(Type::Int, Type::Int) => (Type::Int, None), (Type::Int, Type::Int) => (Type::Int, None),
(Type::Float, Type::Int) => (Type::Float, None), (Type::Float, Type::Int) => (Type::Float, None),
@ -935,8 +933,8 @@ pub fn math_result_type(
) )
} }
}, },
Operator::Assignment(Assignment::AppendAssign) => { Operator::Assignment(Assignment::ConcatAssign) => {
check_append(working_set, lhs, rhs, op) check_concat(working_set, lhs, rhs, op)
} }
Operator::Assignment(_) => match (&lhs.ty, &rhs.ty) { Operator::Assignment(_) => match (&lhs.ty, &rhs.ty) {
(x, y) if x == y => (Type::Nothing, None), (x, y) if x == y => (Type::Nothing, None),
@ -1085,7 +1083,7 @@ pub fn check_block_input_output(working_set: &StateWorkingSet, block: &Block) ->
output_errors output_errors
} }
fn check_append( fn check_concat(
working_set: &mut StateWorkingSet, working_set: &mut StateWorkingSet,
lhs: &Expression, lhs: &Expression,
rhs: &Expression, rhs: &Expression,
@ -1099,23 +1097,17 @@ fn check_append(
(Type::List(Box::new(Type::Any)), None) (Type::List(Box::new(Type::Any)), None)
} }
} }
(Type::List(a), b) | (b, Type::List(a)) => {
if a == &Box::new(b.clone()) {
(Type::List(a.clone()), None)
} else {
(Type::List(Box::new(Type::Any)), None)
}
}
(Type::Table(a), Type::Table(_)) => (Type::Table(a.clone()), None), (Type::Table(a), Type::Table(_)) => (Type::Table(a.clone()), None),
(Type::String, Type::String) => (Type::String, None), (Type::String, Type::String) => (Type::String, None),
(Type::Binary, Type::Binary) => (Type::Binary, None), (Type::Binary, Type::Binary) => (Type::Binary, None),
(Type::Any, _) | (_, Type::Any) => (Type::Any, None), (Type::Any, _) | (_, Type::Any) => (Type::Any, None),
(Type::Table(_) | Type::String | Type::Binary, _) => { (Type::Table(_) | Type::List(_) | Type::String | Type::Binary, _)
| (_, Type::Table(_) | Type::List(_) | Type::String | Type::Binary) => {
*op = Expression::garbage(working_set, op.span); *op = Expression::garbage(working_set, op.span);
( (
Type::Any, Type::Any,
Some(ParseError::UnsupportedOperationRHS( Some(ParseError::UnsupportedOperationRHS(
"append".into(), "concatenation".into(),
op.span, op.span,
lhs.span, lhs.span,
lhs.ty.clone(), lhs.ty.clone(),
@ -1129,7 +1121,7 @@ fn check_append(
( (
Type::Any, Type::Any,
Some(ParseError::UnsupportedOperationLHS( Some(ParseError::UnsupportedOperationLHS(
"append".into(), "concatenation".into(),
op.span, op.span,
lhs.span, lhs.span,
lhs.ty.clone(), lhs.ty.clone(),

View file

@ -1485,7 +1485,7 @@ fn prepare_plugin_call_custom_value_op() {
span, span,
}, },
CustomValueOp::Operation( CustomValueOp::Operation(
Operator::Math(Math::Append).into_spanned(span), Operator::Math(Math::Concat).into_spanned(span),
cv_ok_val.clone(), cv_ok_val.clone(),
), ),
), ),
@ -1498,7 +1498,7 @@ fn prepare_plugin_call_custom_value_op() {
span, span,
}, },
CustomValueOp::Operation( CustomValueOp::Operation(
Operator::Math(Math::Append).into_spanned(span), Operator::Math(Math::Concat).into_spanned(span),
cv_bad_val.clone(), cv_bad_val.clone(),
), ),
), ),

View file

@ -21,7 +21,7 @@ nu-plugin-core = { path = "../nu-plugin-core", version = "0.100.1", default-feat
nu-utils = { path = "../nu-utils", version = "0.100.1" } nu-utils = { path = "../nu-utils", version = "0.100.1" }
log = { workspace = true } log = { workspace = true }
thiserror = "1.0" thiserror = "2.0"
[dev-dependencies] [dev-dependencies]
serde = { workspace = true } serde = { workspace = true }

View file

@ -36,7 +36,7 @@ num-format = { workspace = true }
rmp-serde = { workspace = true, optional = true } rmp-serde = { workspace = true, optional = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
thiserror = "1.0" thiserror = "2.0"
typetag = "0.2" typetag = "0.2"
os_pipe = { workspace = true, features = ["io_safety"] } os_pipe = { workspace = true, features = ["io_safety"] }
log = { workspace = true } log = { workspace = true }

View file

@ -24,7 +24,7 @@ pub enum Comparison {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Math { pub enum Math {
Plus, Plus,
Append, Concat,
Minus, Minus,
Multiply, Multiply,
Divide, Divide,
@ -53,7 +53,7 @@ pub enum Bits {
pub enum Assignment { pub enum Assignment {
Assign, Assign,
PlusAssign, PlusAssign,
AppendAssign, ConcatAssign,
MinusAssign, MinusAssign,
MultiplyAssign, MultiplyAssign,
DivideAssign, DivideAssign,
@ -90,7 +90,7 @@ impl Operator {
| Self::Comparison(Comparison::NotEqual) | Self::Comparison(Comparison::NotEqual)
| Self::Comparison(Comparison::In) | Self::Comparison(Comparison::In)
| Self::Comparison(Comparison::NotIn) | Self::Comparison(Comparison::NotIn)
| Self::Math(Math::Append) => 80, | Self::Math(Math::Concat) => 80,
Self::Bits(Bits::BitAnd) => 75, Self::Bits(Bits::BitAnd) => 75,
Self::Bits(Bits::BitXor) => 70, Self::Bits(Bits::BitXor) => 70,
Self::Bits(Bits::BitOr) => 60, Self::Bits(Bits::BitOr) => 60,
@ -107,7 +107,7 @@ impl Display for Operator {
match self { match self {
Operator::Assignment(Assignment::Assign) => write!(f, "="), Operator::Assignment(Assignment::Assign) => write!(f, "="),
Operator::Assignment(Assignment::PlusAssign) => write!(f, "+="), Operator::Assignment(Assignment::PlusAssign) => write!(f, "+="),
Operator::Assignment(Assignment::AppendAssign) => write!(f, "++="), Operator::Assignment(Assignment::ConcatAssign) => write!(f, "++="),
Operator::Assignment(Assignment::MinusAssign) => write!(f, "-="), Operator::Assignment(Assignment::MinusAssign) => write!(f, "-="),
Operator::Assignment(Assignment::MultiplyAssign) => write!(f, "*="), Operator::Assignment(Assignment::MultiplyAssign) => write!(f, "*="),
Operator::Assignment(Assignment::DivideAssign) => write!(f, "/="), Operator::Assignment(Assignment::DivideAssign) => write!(f, "/="),
@ -124,7 +124,7 @@ impl Display for Operator {
Operator::Comparison(Comparison::In) => write!(f, "in"), Operator::Comparison(Comparison::In) => write!(f, "in"),
Operator::Comparison(Comparison::NotIn) => write!(f, "not-in"), Operator::Comparison(Comparison::NotIn) => write!(f, "not-in"),
Operator::Math(Math::Plus) => write!(f, "+"), Operator::Math(Math::Plus) => write!(f, "+"),
Operator::Math(Math::Append) => write!(f, "++"), Operator::Math(Math::Concat) => write!(f, "++"),
Operator::Math(Math::Minus) => write!(f, "-"), Operator::Math(Math::Minus) => write!(f, "-"),
Operator::Math(Math::Multiply) => write!(f, "*"), Operator::Math(Math::Multiply) => write!(f, "*"),
Operator::Math(Math::Divide) => write!(f, "/"), Operator::Math(Math::Divide) => write!(f, "/"),

View file

@ -698,7 +698,7 @@ impl EngineState {
pub fn find_commands_by_predicate( pub fn find_commands_by_predicate(
&self, &self,
predicate: impl Fn(&[u8]) -> bool, mut predicate: impl FnMut(&[u8]) -> bool,
ignore_deprecated: bool, ignore_deprecated: bool,
) -> Vec<(Vec<u8>, Option<String>, CommandType)> { ) -> Vec<(Vec<u8>, Option<String>, CommandType)> {
let mut output = vec![]; let mut output = vec![];

View file

@ -724,7 +724,7 @@ impl<'a> StateWorkingSet<'a> {
pub fn find_commands_by_predicate( pub fn find_commands_by_predicate(
&self, &self,
predicate: impl Fn(&[u8]) -> bool, mut predicate: impl FnMut(&[u8]) -> bool,
ignore_deprecated: bool, ignore_deprecated: bool,
) -> Vec<(Vec<u8>, Option<String>, CommandType)> { ) -> Vec<(Vec<u8>, Option<String>, CommandType)> {
let mut output = vec![]; let mut output = vec![];

View file

@ -1330,14 +1330,15 @@ This is an internal Nushell error, please file an issue https://github.com/nushe
span: Span, span: Span,
}, },
#[error("Deprecated: {old_command}")] #[error("{deprecated} is deprecated and will be removed in a future release")]
#[diagnostic(help("for more info see {url}"))] #[diagnostic()]
Deprecated { Deprecated {
old_command: String, deprecated: &'static str,
new_suggestion: String, suggestion: &'static str,
#[label("`{old_command}` is deprecated and will be removed in a future release. Please {new_suggestion} instead.")] #[label("{deprecated} is deprecated. {suggestion}")]
span: Span, span: Span,
url: String, #[help]
help: Option<&'static str>,
}, },
/// Invalid glob pattern /// Invalid glob pattern

View file

@ -238,7 +238,7 @@ pub trait Eval {
Math::Minus => lhs.sub(op_span, &rhs, expr_span), Math::Minus => lhs.sub(op_span, &rhs, expr_span),
Math::Multiply => lhs.mul(op_span, &rhs, expr_span), Math::Multiply => lhs.mul(op_span, &rhs, expr_span),
Math::Divide => lhs.div(op_span, &rhs, expr_span), Math::Divide => lhs.div(op_span, &rhs, expr_span),
Math::Append => lhs.append(op_span, &rhs, expr_span), Math::Concat => lhs.concat(op_span, &rhs, expr_span),
Math::Modulo => lhs.modulo(op_span, &rhs, expr_span), Math::Modulo => lhs.modulo(op_span, &rhs, expr_span),
Math::FloorDivision => lhs.floor_div(op_span, &rhs, expr_span), Math::FloorDivision => lhs.floor_div(op_span, &rhs, expr_span),
Math::Pow => lhs.pow(op_span, &rhs, expr_span), Math::Pow => lhs.pow(op_span, &rhs, expr_span),

View file

@ -109,7 +109,7 @@ impl PipelineData {
/// than would be returned by [`Value::get_type()`] on the result of /// than would be returned by [`Value::get_type()`] on the result of
/// [`.into_value()`](Self::into_value). /// [`.into_value()`](Self::into_value).
/// ///
/// Specifically, a `ListStream` results in [`list stream`](Type::ListStream) rather than /// Specifically, a `ListStream` results in `list<any>` rather than
/// the fully complete [`list`](Type::List) type (which would require knowing the contents), /// the fully complete [`list`](Type::List) type (which would require knowing the contents),
/// and a `ByteStream` with [unknown](crate::ByteStreamType::Unknown) type results in /// and a `ByteStream` with [unknown](crate::ByteStreamType::Unknown) type results in
/// [`any`](Type::Any) rather than [`string`](Type::String) or [`binary`](Type::Binary). /// [`any`](Type::Any) rather than [`string`](Type::String) or [`binary`](Type::Binary).
@ -117,7 +117,7 @@ impl PipelineData {
match self { match self {
PipelineData::Empty => Type::Nothing, PipelineData::Empty => Type::Nothing,
PipelineData::Value(value, _) => value.get_type(), PipelineData::Value(value, _) => value.get_type(),
PipelineData::ListStream(_, _) => Type::ListStream, PipelineData::ListStream(_, _) => Type::list(Type::Any),
PipelineData::ByteStream(stream, _) => stream.type_().into(), PipelineData::ByteStream(stream, _) => stream.type_().into(),
} }
} }
@ -203,7 +203,7 @@ impl PipelineData {
) -> Result<Self, ShellError> { ) -> Result<Self, ShellError> {
match stack.pipe_stdout().unwrap_or(&OutDest::Inherit) { match stack.pipe_stdout().unwrap_or(&OutDest::Inherit) {
OutDest::Print => { OutDest::Print => {
self.print(engine_state, stack, false, false)?; self.print_table(engine_state, stack, false, false)?;
Ok(Self::Empty) Ok(Self::Empty)
} }
OutDest::Pipe | OutDest::PipeSeparate => Ok(self), OutDest::Pipe | OutDest::PipeSeparate => Ok(self),
@ -534,11 +534,14 @@ impl PipelineData {
} }
} }
/// Consume and print self data immediately. /// Consume and print self data immediately, formatted using table command.
///
/// This does not respect the display_output hook. If a value is being printed out by a command,
/// this function should be used. Otherwise, `nu_cli::util::print_pipeline` should be preferred.
/// ///
/// `no_newline` controls if we need to attach newline character to output. /// `no_newline` controls if we need to attach newline character to output.
/// `to_stderr` controls if data is output to stderr, when the value is false, the data is output to stdout. /// `to_stderr` controls if data is output to stderr, when the value is false, the data is output to stdout.
pub fn print( pub fn print_table(
self, self,
engine_state: &EngineState, engine_state: &EngineState,
stack: &mut Stack, stack: &mut Stack,

View file

@ -23,7 +23,6 @@ pub enum Type {
Float, Float,
Int, Int,
List(Box<Type>), List(Box<Type>),
ListStream,
#[default] #[default]
Nothing, Nothing,
Number, Number,
@ -121,7 +120,6 @@ impl Type {
Type::Nothing => SyntaxShape::Nothing, Type::Nothing => SyntaxShape::Nothing,
Type::Record(entries) => SyntaxShape::Record(mk_shape(entries)), Type::Record(entries) => SyntaxShape::Record(mk_shape(entries)),
Type::Table(columns) => SyntaxShape::Table(mk_shape(columns)), Type::Table(columns) => SyntaxShape::Table(mk_shape(columns)),
Type::ListStream => SyntaxShape::List(Box::new(SyntaxShape::Any)),
Type::Any => SyntaxShape::Any, Type::Any => SyntaxShape::Any,
Type::Error => SyntaxShape::Any, Type::Error => SyntaxShape::Any,
Type::Binary => SyntaxShape::Binary, Type::Binary => SyntaxShape::Binary,
@ -151,7 +149,6 @@ impl Type {
Type::Nothing => String::from("nothing"), Type::Nothing => String::from("nothing"),
Type::Number => String::from("number"), Type::Number => String::from("number"),
Type::String => String::from("string"), Type::String => String::from("string"),
Type::ListStream => String::from("list-stream"),
Type::Any => String::from("any"), Type::Any => String::from("any"),
Type::Error => String::from("error"), Type::Error => String::from("error"),
Type::Binary => String::from("binary"), Type::Binary => String::from("binary"),
@ -209,7 +206,6 @@ impl Display for Type {
Type::Nothing => write!(f, "nothing"), Type::Nothing => write!(f, "nothing"),
Type::Number => write!(f, "number"), Type::Number => write!(f, "number"),
Type::String => write!(f, "string"), Type::String => write!(f, "string"),
Type::ListStream => write!(f, "list-stream"),
Type::Any => write!(f, "any"), Type::Any => write!(f, "any"),
Type::Error => write!(f, "error"), Type::Error => write!(f, "error"),
Type::Binary => write!(f, "binary"), Type::Binary => write!(f, "binary"),

View file

@ -2503,34 +2503,20 @@ impl Value {
} }
} }
pub fn append(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> { pub fn concat(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) { match (self, rhs) {
(Value::List { vals: lhs, .. }, Value::List { vals: rhs, .. }) => { (Value::List { vals: lhs, .. }, Value::List { vals: rhs, .. }) => {
let mut lhs = lhs.clone(); Ok(Value::list([lhs.as_slice(), rhs.as_slice()].concat(), span))
let mut rhs = rhs.clone();
lhs.append(&mut rhs);
Ok(Value::list(lhs, span))
}
(Value::List { vals: lhs, .. }, val) => {
let mut lhs = lhs.clone();
lhs.push(val.clone());
Ok(Value::list(lhs, span))
}
(val, Value::List { vals: rhs, .. }) => {
let mut rhs = rhs.clone();
rhs.insert(0, val.clone());
Ok(Value::list(rhs, span))
} }
(Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => { (Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => {
Ok(Value::string(lhs.to_string() + rhs, span)) Ok(Value::string([lhs.as_str(), rhs.as_str()].join(""), span))
}
(Value::Binary { val: lhs, .. }, Value::Binary { val: rhs, .. }) => {
let mut val = lhs.clone();
val.extend(rhs);
Ok(Value::binary(val, span))
} }
(Value::Binary { val: lhs, .. }, Value::Binary { val: rhs, .. }) => Ok(Value::binary(
[lhs.as_slice(), rhs.as_slice()].concat(),
span,
)),
(Value::Custom { val: lhs, .. }, rhs) => { (Value::Custom { val: lhs, .. }, rhs) => {
lhs.operation(self.span(), Operator::Math(Math::Append), op, rhs) lhs.operation(self.span(), Operator::Math(Math::Concat), op, rhs)
} }
_ => Err(ShellError::OperatorMismatch { _ => Err(ShellError::OperatorMismatch {
op_span: op, op_span: op,

View file

@ -35,7 +35,7 @@ fn config_affected_when_mutated() {
#[test] #[test]
fn config_affected_when_deep_mutated() { fn config_affected_when_deep_mutated() {
let actual = nu!(cwd: "crates/nu-utils/src/sample_config", nu_repl_code(&[ let actual = nu!(cwd: "crates/nu-utils/src/default_files", nu_repl_code(&[
r#"source default_config.nu"#, r#"source default_config.nu"#,
r#"$env.config.filesize.metric = true"#, r#"$env.config.filesize.metric = true"#,
r#"20mib | into string"#])); r#"20mib | into string"#]));
@ -45,7 +45,7 @@ fn config_affected_when_deep_mutated() {
#[test] #[test]
fn config_add_unsupported_key() { fn config_add_unsupported_key() {
let actual = nu!(cwd: "crates/nu-utils/src/sample_config", nu_repl_code(&[ let actual = nu!(cwd: "crates/nu-utils/src/default_files", nu_repl_code(&[
r#"source default_config.nu"#, r#"source default_config.nu"#,
r#"$env.config.foo = 2"#, r#"$env.config.foo = 2"#,
r#";"#])); r#";"#]));
@ -57,7 +57,7 @@ fn config_add_unsupported_key() {
#[test] #[test]
fn config_add_unsupported_type() { fn config_add_unsupported_type() {
let actual = nu!(cwd: "crates/nu-utils/src/sample_config", nu_repl_code(&[r#"source default_config.nu"#, let actual = nu!(cwd: "crates/nu-utils/src/default_files", nu_repl_code(&[r#"source default_config.nu"#,
r#"$env.config.ls = '' "#, r#"$env.config.ls = '' "#,
r#";"#])); r#";"#]));
@ -66,7 +66,7 @@ fn config_add_unsupported_type() {
#[test] #[test]
fn config_add_unsupported_value() { fn config_add_unsupported_value() {
let actual = nu!(cwd: "crates/nu-utils/src/sample_config", nu_repl_code(&[r#"source default_config.nu"#, let actual = nu!(cwd: "crates/nu-utils/src/default_files", nu_repl_code(&[r#"source default_config.nu"#,
r#"$env.config.history.file_format = ''"#, r#"$env.config.history.file_format = ''"#,
r#";"#])); r#";"#]));
@ -77,7 +77,7 @@ fn config_add_unsupported_value() {
#[test] #[test]
#[ignore = "Figure out how to make test_bins::nu_repl() continue execution after shell errors"] #[ignore = "Figure out how to make test_bins::nu_repl() continue execution after shell errors"]
fn config_unsupported_key_reverted() { fn config_unsupported_key_reverted() {
let actual = nu!(cwd: "crates/nu-utils/src/sample_config", nu_repl_code(&[r#"source default_config.nu"#, let actual = nu!(cwd: "crates/nu-utils/src/default_files", nu_repl_code(&[r#"source default_config.nu"#,
r#"$env.config.foo = 1"#, r#"$env.config.foo = 1"#,
r#"'foo' in $env.config"#])); r#"'foo' in $env.config"#]));
@ -87,7 +87,7 @@ fn config_unsupported_key_reverted() {
#[test] #[test]
#[ignore = "Figure out how to make test_bins::nu_repl() continue execution after shell errors"] #[ignore = "Figure out how to make test_bins::nu_repl() continue execution after shell errors"]
fn config_unsupported_type_reverted() { fn config_unsupported_type_reverted() {
let actual = nu!(cwd: "crates/nu-utils/src/sample_config", nu_repl_code(&[r#" source default_config.nu"#, let actual = nu!(cwd: "crates/nu-utils/src/default_files", nu_repl_code(&[r#" source default_config.nu"#,
r#"$env.config.ls = ''"#, r#"$env.config.ls = ''"#,
r#"$env.config.ls | describe"#])); r#"$env.config.ls | describe"#]));
@ -97,7 +97,7 @@ fn config_unsupported_type_reverted() {
#[test] #[test]
#[ignore = "Figure out how to make test_bins::nu_repl() continue execution after errors"] #[ignore = "Figure out how to make test_bins::nu_repl() continue execution after errors"]
fn config_unsupported_value_reverted() { fn config_unsupported_value_reverted() {
let actual = nu!(cwd: "crates/nu-utils/src/sample_config", nu_repl_code(&[r#" source default_config.nu"#, let actual = nu!(cwd: "crates/nu-utils/src/default_files", nu_repl_code(&[r#" source default_config.nu"#,
r#"$env.config.history.file_format = 'plaintext'"#, r#"$env.config.history.file_format = 'plaintext'"#,
r#"$env.config.history.file_format = ''"#, r#"$env.config.history.file_format = ''"#,
r#"$env.config.history.file_format | to json"#])); r#"$env.config.history.file_format | to json"#]));

View file

@ -69,7 +69,7 @@ fn fancy_default_errors() {
assert_eq!( assert_eq!(
actual.err, actual.err,
"Error: \u{1b}[31m×\u{1b}[0m oh no!\n ╭─[\u{1b}[36;1;4mline1\u{1b}[0m:1:13]\n \u{1b}[2m1\u{1b}[0m │ force_error \"My error\"\n · \u{1b}[35;1m ─────┬────\u{1b}[0m\n · \u{1b}[35;1m╰── \u{1b}[35;1mhere's the error\u{1b}[0m\u{1b}[0m\n ╰────\n\n\n" "Error: \n \u{1b}[31m×\u{1b}[0m oh no!\n ╭─[\u{1b}[36;1;4mline1:1:13\u{1b}[0m]\n \u{1b}[2m1\u{1b}[0m │ force_error \"My error\"\n · \u{1b}[35;1m ─────┬────\u{1b}[0m\n · \u{1b}[35;1m╰── \u{1b}[35;1mhere's the error\u{1b}[0m\u{1b}[0m\n ╰────\n\n"
); );
} }

View file

@ -54,6 +54,7 @@ pub fn load_standard_library(
("mod.nu", "std/math", include_str!("../std/math/mod.nu")), ("mod.nu", "std/math", include_str!("../std/math/mod.nu")),
("mod.nu", "std/util", include_str!("../std/util/mod.nu")), ("mod.nu", "std/util", include_str!("../std/util/mod.nu")),
("mod.nu", "std/xml", include_str!("../std/xml/mod.nu")), ("mod.nu", "std/xml", include_str!("../std/xml/mod.nu")),
("mod.nu", "std/config", include_str!("../std/config/mod.nu")),
]; ];
for (filename, std_subdir_name, content) in std_submodules.drain(..) { for (filename, std_subdir_name, content) in std_submodules.drain(..) {

View file

@ -0,0 +1,139 @@
# Returns a dark-mode theme that can be assigned to $env.config.color_config
export def dark-theme [] {
{
# color for nushell primitives
separator: white
leading_trailing_space_bg: { attr: n } # no fg, no bg, attr none effectively turns this off
header: green_bold
empty: blue
# Closures can be used to choose colors for specific values.
# The value (in this case, a bool) is piped into the closure.
# eg) {|| if $in { 'light_cyan' } else { 'light_gray' } }
bool: light_cyan
int: white
filesize: cyan
duration: white
date: purple
range: white
float: white
string: white
nothing: white
binary: white
cell-path: white
row_index: green_bold
record: white
list: white
block: white
hints: dark_gray
search_result: { bg: red fg: white }
shape_and: purple_bold
shape_binary: purple_bold
shape_block: blue_bold
shape_bool: light_cyan
shape_closure: green_bold
shape_custom: green
shape_datetime: cyan_bold
shape_directory: cyan
shape_external: cyan
shape_externalarg: green_bold
shape_external_resolved: light_yellow_bold
shape_filepath: cyan
shape_flag: blue_bold
shape_float: purple_bold
# shapes are used to change the cli syntax highlighting
shape_garbage: { fg: white bg: red attr: b }
shape_glob_interpolation: cyan_bold
shape_globpattern: cyan_bold
shape_int: purple_bold
shape_internalcall: cyan_bold
shape_keyword: cyan_bold
shape_list: cyan_bold
shape_literal: blue
shape_match_pattern: green
shape_matching_brackets: { attr: u }
shape_nothing: light_cyan
shape_operator: yellow
shape_or: purple_bold
shape_pipe: purple_bold
shape_range: yellow_bold
shape_record: cyan_bold
shape_redirection: purple_bold
shape_signature: green_bold
shape_string: green
shape_string_interpolation: cyan_bold
shape_table: blue_bold
shape_variable: purple
shape_vardecl: purple
shape_raw_string: light_purple
}
}
# Returns a light-mode theme that can be assigned to $env.config.color_config
export def light-theme [] {
{
# color for nushell primitives
separator: dark_gray
leading_trailing_space_bg: { attr: n } # no fg, no bg, attr none effectively turns this off
header: green_bold
empty: blue
# Closures can be used to choose colors for specific values.
# The value (in this case, a bool) is piped into the closure.
# eg) {|| if $in { 'dark_cyan' } else { 'dark_gray' } }
bool: dark_cyan
int: dark_gray
filesize: cyan_bold
duration: dark_gray
date: purple
range: dark_gray
float: dark_gray
string: dark_gray
nothing: dark_gray
binary: dark_gray
cell-path: dark_gray
row_index: green_bold
record: dark_gray
list: dark_gray
block: dark_gray
hints: dark_gray
search_result: { fg: white bg: red }
shape_and: purple_bold
shape_binary: purple_bold
shape_block: blue_bold
shape_bool: light_cyan
shape_closure: green_bold
shape_custom: green
shape_datetime: cyan_bold
shape_directory: cyan
shape_external: cyan
shape_externalarg: green_bold
shape_external_resolved: light_purple_bold
shape_filepath: cyan
shape_flag: blue_bold
shape_float: purple_bold
# shapes are used to change the cli syntax highlighting
shape_garbage: { fg: white bg: red attr: b }
shape_glob_interpolation: cyan_bold
shape_globpattern: cyan_bold
shape_int: purple_bold
shape_internalcall: cyan_bold
shape_keyword: cyan_bold
shape_list: cyan_bold
shape_literal: blue
shape_match_pattern: green
shape_matching_brackets: { attr: u }
shape_nothing: light_cyan
shape_operator: yellow
shape_or: purple_bold
shape_pipe: purple_bold
shape_range: yellow_bold
shape_record: cyan_bold
shape_redirection: purple_bold
shape_signature: green_bold
shape_string: green
shape_string_interpolation: cyan_bold
shape_table: blue_bold
shape_variable: purple
shape_vardecl: purple
shape_raw_string: light_purple
}
}

View file

@ -28,3 +28,13 @@ export def "to ndjson" []: any -> string {
export def "to jsonl" []: any -> string { export def "to jsonl" []: any -> string {
each { to json --raw } | to text each { to json --raw } | to text
} }
# Convert from NDNUON (newline-delimited NUON), to structured data
export def "from ndnuon" []: [string -> any] {
lines | each { from nuon }
}
# Convert structured data to NDNUON, i.e. newline-delimited NUON
export def "to ndnuon" []: [any -> string] {
each { to nuon --raw } | to text
}

View file

@ -37,7 +37,7 @@ def get-all-operators [] { return [
[Assignment, =, Assign, "Assigns a value to a variable.", 10] [Assignment, =, Assign, "Assigns a value to a variable.", 10]
[Assignment, +=, PlusAssign, "Adds a value to a variable.", 10] [Assignment, +=, PlusAssign, "Adds a value to a variable.", 10]
[Assignment, ++=, AppendAssign, "Appends a list or a value to a variable.", 10] [Assignment, ++=, ConcatAssign, "Concatenate two lists, two strings, or two binary values.", 10]
[Assignment, -=, MinusAssign, "Subtracts a value from a variable.", 10] [Assignment, -=, MinusAssign, "Subtracts a value from a variable.", 10]
[Assignment, *=, MultiplyAssign, "Multiplies a variable by a value.", 10] [Assignment, *=, MultiplyAssign, "Multiplies a variable by a value.", 10]
[Assignment, /=, DivideAssign, "Divides a variable by a value.", 10] [Assignment, /=, DivideAssign, "Divides a variable by a value.", 10]
@ -55,7 +55,7 @@ def get-all-operators [] { return [
[Comparison, ends-with, EndsWith, "Checks if a string ends with another.", 80] [Comparison, ends-with, EndsWith, "Checks if a string ends with another.", 80]
[Comparison, not, UnaryNot, "Negates a value or expression.", 0] [Comparison, not, UnaryNot, "Negates a value or expression.", 0]
[Math, +, Plus, "Adds two values.", 90] [Math, +, Plus, "Adds two values.", 90]
[Math, ++, Append, "Appends two lists or a list and a value.", 80] [Math, ++, Concat, "Concatenate two lists, two strings, or two binary values.", 80]
[Math, -, Minus, "Subtracts two values.", 90] [Math, -, Minus, "Subtracts two values.", 90]
[Math, *, Multiply, "Multiplies two values.", 95] [Math, *, Multiply, "Multiplies two values.", 95]
[Math, /, Divide, "Divides two values.", 95] [Math, /, Divide, "Divides two values.", 95]
@ -684,8 +684,7 @@ def build-command-page [command: record] {
] | flatten | str join "\n" ] | flatten | str join "\n"
} }
# Show help on commands. def scope-commands [
export def commands [
...command: string@"nu-complete list-commands" # the name of command to get help on ...command: string@"nu-complete list-commands" # the name of command to get help on
--find (-f): string # string to find in command names and description --find (-f): string # string to find in command names and description
] { ] {
@ -699,17 +698,32 @@ export def commands [
let found_command = ($commands | where name == $target_command) let found_command = ($commands | where name == $target_command)
if ($found_command | is-empty) { if ($found_command | is-empty) {
try {
print $"(ansi default_italic)Help pages from external command ($target_command | pretty-cmd):(ansi reset)"
^($env.NU_HELPER? | default "man") $target_command
} catch {
command-not-found-error (metadata $command | get span) command-not-found-error (metadata $command | get span)
} else {
build-command-page ($found_command | get 0)
}
} else {
$commands | select name category description signatures search_terms
} }
} }
build-command-page ($found_command | get 0) def external-commands [
} else { ...command: string@"nu-complete list-commands",
$commands | select name category description signatures search_terms ] {
let target_command = $command | str join " "
print $"(ansi default_italic)Help pages from external command ($target_command | pretty-cmd):(ansi reset)"
^($env.NU_HELPER? | default "man") $target_command
}
# Show help on commands.
export def commands [
...command: string@"nu-complete list-commands" # the name of command to get help on
--find (-f): string # string to find in command names and description
] {
try {
scope-commands ...$command --find=$find
} catch {
external-commands ...$command
} }
} }
@ -763,7 +777,7 @@ You can also learn more at (ansi default_italic)(ansi light_cyan_underline)https
let target_item = ($item | str join " ") let target_item = ($item | str join " ")
let commands = (try { commands $target_item --find $find }) let commands = (try { scope-commands $target_item --find $find })
if not ($commands | is-empty) { return $commands } if not ($commands | is-empty) { return $commands }
let aliases = (try { aliases $target_item --find $find }) let aliases = (try { aliases $target_item --find $find })
@ -776,13 +790,7 @@ You can also learn more at (ansi default_italic)(ansi light_cyan_underline)https
print -e $"No help results found mentioning: ($find)" print -e $"No help results found mentioning: ($find)"
return [] return []
} }
# use external tool (e.g: `man`) to search help for $target_item
let span = (metadata $item | get span) # the stdout and stderr of external tool will follow `main` call.
error make { external-commands $target_item
msg: ("std::help::item_not_found" | error-fmt)
label: {
text: "item not found"
span: $span
}
}
} }

View file

@ -14,6 +14,7 @@ export module std/iter
export module std/log export module std/log
export module std/math export module std/math
export module std/xml export module std/xml
export module std/config
# Load main dirs command and all subcommands # Load main dirs command and all subcommands
export use std/dirs main export use std/dirs main

View file

@ -2,8 +2,18 @@
use std/assert use std/assert
use std/formats * use std/formats *
def test_data_multiline [] { def test_data_multiline [--nuon] {
let lines = [ let lines = if $nuon {
[
"{a: 1}",
"{a: 2}",
"{a: 3}",
"{a: 4}",
"{a: 5}",
"{a: 6}",
]
} else {
[
"{\"a\":1}", "{\"a\":1}",
"{\"a\":2}", "{\"a\":2}",
"{\"a\":3}", "{\"a\":3}",
@ -11,6 +21,7 @@ def test_data_multiline [] {
"{\"a\":5}", "{\"a\":5}",
"{\"a\":6}", "{\"a\":6}",
] ]
}
if $nu.os-info.name == "windows" { if $nu.os-info.name == "windows" {
$lines | str join "\r\n" $lines | str join "\r\n"
@ -84,3 +95,36 @@ def to_jsonl_single_object [] {
let expect = "{\"a\":1}" let expect = "{\"a\":1}"
assert equal $result $expect "could not convert to JSONL" assert equal $result $expect "could not convert to JSONL"
} }
#[test]
def from_ndnuon_multiple_objects [] {
let result = test_data_multiline | from ndnuon
let expect = [{a:1},{a:2},{a:3},{a:4},{a:5},{a:6}]
assert equal $result $expect "could not convert from NDNUON"
}
#[test]
def from_ndnuon_single_object [] {
let result = '{a: 1}' | from ndnuon
let expect = [{a:1}]
assert equal $result $expect "could not convert from NDNUON"
}
#[test]
def from_ndnuon_invalid_object [] {
assert error { '{"a":1' | formats from ndnuon }
}
#[test]
def to_ndnuon_multiple_objects [] {
let result = [{a:1},{a:2},{a:3},{a:4},{a:5},{a:6}] | to ndnuon | str trim
let expect = test_data_multiline --nuon
assert equal $result $expect "could not convert to NDNUON"
}
#[test]
def to_ndnuon_single_object [] {
let result = [{a:1}] | to ndnuon | str trim
let expect = "{a: 1}"
assert equal $result $expect "could not convert to NDNUON"
}

View file

@ -1,9 +1,19 @@
# Test std/formats when importing `use std *` # Test std/formats when importing `use std *`
use std * use std *
def test_data_multiline [] { def test_data_multiline [--nuon] {
use std * use std *
let lines = [ let lines = if $nuon {
[
"{a: 1}",
"{a: 2}",
"{a: 3}",
"{a: 4}",
"{a: 5}",
"{a: 6}",
]
} else {
[
"{\"a\":1}", "{\"a\":1}",
"{\"a\":2}", "{\"a\":2}",
"{\"a\":3}", "{\"a\":3}",
@ -11,6 +21,7 @@ def test_data_multiline [] {
"{\"a\":5}", "{\"a\":5}",
"{\"a\":6}", "{\"a\":6}",
] ]
}
if $nu.os-info.name == "windows" { if $nu.os-info.name == "windows" {
$lines | str join "\r\n" $lines | str join "\r\n"
@ -84,3 +95,36 @@ def to_jsonl_single_object [] {
let expect = "{\"a\":1}" let expect = "{\"a\":1}"
assert equal $result $expect "could not convert to JSONL" assert equal $result $expect "could not convert to JSONL"
} }
#[test]
def from_ndnuon_multiple_objects [] {
let result = test_data_multiline | formats from ndnuon
let expect = [{a:1},{a:2},{a:3},{a:4},{a:5},{a:6}]
assert equal $result $expect "could not convert from NDNUON"
}
#[test]
def from_ndnuon_single_object [] {
let result = '{a: 1}' | formats from ndnuon
let expect = [{a:1}]
assert equal $result $expect "could not convert from NDNUON"
}
#[test]
def from_ndnuon_invalid_object [] {
assert error { '{"a":1' | formats from ndnuon }
}
#[test]
def to_ndnuon_multiple_objects [] {
let result = [{a:1},{a:2},{a:3},{a:4},{a:5},{a:6}] | formats to ndnuon | str trim
let expect = test_data_multiline --nuon
assert equal $result $expect "could not convert to NDNUON"
}
#[test]
def to_ndnuon_single_object [] {
let result = [{a:1}] | formats to ndnuon | str trim
let expect = "{a: 1}"
assert equal $result $expect "could not convert to NDNUON"
}

View file

@ -18,8 +18,12 @@ pub fn create_nu_table_config(
expand: bool, expand: bool,
mode: TableMode, mode: TableMode,
) -> NuTableConfig { ) -> NuTableConfig {
let with_footer = (config.table.footer_inheritance && out.with_footer) let mut count_rows = out.table.count_rows();
|| with_footer(config, out.with_header, out.table.count_rows()); if config.table.footer_inheritance {
count_rows = out.count_rows;
}
let with_footer = with_footer(config, out.with_header, count_rows);
NuTableConfig { NuTableConfig {
theme: load_theme(mode), theme: load_theme(mode),

View file

@ -615,12 +615,15 @@ fn load_theme(
if let Some(style) = sep_color { if let Some(style) = sep_color {
let color = convert_style(style); let color = convert_style(style);
let color = ANSIBuf::from(color); let color = ANSIBuf::from(color);
// todo: use .modify(Segment::all(), color) --> it has this optimization
table.get_config_mut().set_border_color_default(color); table.get_config_mut().set_border_color_default(color);
} }
if !with_header { if !with_header {
// todo: remove and use theme.remove_horizontal_lines();
table.with(RemoveHorizontalLine); table.with(RemoveHorizontalLine);
} else if with_footer { } else if with_footer {
// todo: remove and set it on theme rather then here...
table.with(CopyFirstHorizontalLineAtLast); table.with(CopyFirstHorizontalLineAtLast);
} }
} }
@ -1257,6 +1260,7 @@ fn remove_row(recs: &mut NuRecords, row: usize) -> Vec<String> {
columns columns
} }
// todo; use Format?
struct StripColorFromRow(usize); struct StripColorFromRow(usize);
impl TableOption<NuRecords, ColoredConfig, CompleteDimensionVecRecords<'_>> for StripColorFromRow { impl TableOption<NuRecords, ColoredConfig, CompleteDimensionVecRecords<'_>> for StripColorFromRow {

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