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
- 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"
bytes = "1"
bytesize = "1.3"
calamine = "0.24.0"
calamine = "0.26.1"
chardetng = "0.1.17"
chrono = { default-features = false, version = "0.4.34" }
chrono-humanize = "0.2.3"
@ -106,7 +106,7 @@ lsp-server = "0.7.5"
lsp-types = { version = "0.95.0", features = ["proposed"] }
mach2 = "0.4"
md5 = { version = "0.10", package = "md-5" }
miette = "7.2"
miette = "7.3"
mime = "0.3.17"
mime_guess = "2.0"
mockito = { version = "1.6", default-features = false }
@ -129,7 +129,7 @@ proc-macro-error = { version = "1.0", default-features = false }
proc-macro2 = "1.0"
procfs = "0.16.0"
pwd = "1.3"
quick-xml = "0.32.0"
quick-xml = "0.37.0"
quickcheck = "1.0"
quickcheck_macros = "1.0"
quote = "1.0"
@ -156,22 +156,23 @@ syn = "2.0"
sysinfo = "0.32"
tabled = { version = "0.16.0", default-features = false }
tempfile = "3.14"
terminal_size = "0.3"
terminal_size = "0.4"
titlecase = "2.0"
toml = "0.8"
trash = "5.2"
umask = "2.1"
unicode-segmentation = "1.12"
unicode-width = "0.1"
unicode-width = "0.2"
ureq = { version = "2.10", default-features = false }
url = "2.2"
uu_cp = "0.0.27"
uu_mkdir = "0.0.27"
uu_mktemp = "0.0.27"
uu_mv = "0.0.27"
uu_whoami = "0.0.27"
uu_uname = "0.0.27"
uucore = "0.0.27"
uu_cp = "0.0.28"
uu_mkdir = "0.0.28"
uu_mktemp = "0.0.28"
uu_mv = "0.0.28"
uu_touch = "0.0.28"
uu_whoami = "0.0.28"
uu_uname = "0.0.28"
uucore = "0.0.28"
uuid = "1.11.0"
v_htmlescape = "0.15.0"
wax = "0.6"
@ -313,7 +314,7 @@ bench = false
# 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
[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"}
# 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
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.
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::{
completions::{Completer, CompletionOptions, MatchAlgorithm},
completions::{Completer, CompletionOptions},
SuggestionKind,
};
use nu_parser::FlatShape;
@ -9,7 +11,7 @@ use nu_protocol::{
};
use reedline::Suggestion;
use super::{completion_common::sort_suggestions, SemanticSuggestion};
use super::{completion_options::NuMatcher, SemanticSuggestion};
pub struct CommandCompletion {
flattened: Vec<(Span, FlatShape)>,
@ -33,10 +35,11 @@ impl CommandCompletion {
fn external_command_completion(
&self,
working_set: &StateWorkingSet,
prefix: &str,
match_algorithm: MatchAlgorithm,
) -> Vec<String> {
let mut executables = vec![];
sugg_span: reedline::Span,
matched_internal: impl Fn(&str) -> bool,
matcher: &mut NuMatcher<String>,
) -> HashMap<String, SemanticSuggestion> {
let mut suggs = HashMap::new();
// os agnostic way to get the PATH env var
let paths = working_set.permanent_state.get_path_env_var();
@ -54,24 +57,38 @@ impl CommandCompletion {
.completions
.external
.max_results
> executables.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())
<= suggs.len() as i64
{
if let Ok(name) = item.file_name().into_string() {
executables.push(name);
}
break;
}
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(
@ -88,68 +105,59 @@ impl CommandCompletion {
span: Span,
offset: usize,
find_externals: bool,
match_algorithm: MatchAlgorithm,
options: &CompletionOptions,
) -> Vec<SemanticSuggestion> {
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
.find_commands_by_predicate(filter_predicate, true)
.into_iter()
.map(move |x| SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(&x.0).to_string(),
description: x.1,
span: reedline::Span::new(span.start - offset, span.end - offset),
append_whitespace: true,
..Suggestion::default()
},
kind: Some(SuggestionKind::Command(x.2)),
})
.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 {
let mut internal_suggs = HashMap::new();
let filtered_commands = working_set.find_commands_by_predicate(
|name| {
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 {
value: x,
span: reedline::Span::new(span.start - offset, span.end - offset),
value: name.to_string(),
description,
span: sugg_span,
append_whitespace: true,
..Suggestion::default()
},
// TODO: is there a way to create a test?
kind: None,
});
let results_strings: Vec<String> =
results.iter().map(|x| x.suggestion.value.clone()).collect();
for external in results_external {
if results_strings.contains(&external.suggestion.value) {
results.push(SemanticSuggestion {
suggestion: Suggestion {
value: format!("^{}", external.suggestion.value),
span: external.suggestion.span,
append_whitespace: true,
..Suggestion::default()
},
kind: external.kind,
})
} else {
results.push(external)
}
}
results
} else {
results
kind: Some(SuggestionKind::Command(typ)),
},
);
}
let mut external_suggs = if find_externals {
self.external_command_completion(
working_set,
sugg_span,
|name| internal_suggs.contains_key(name),
&mut matcher,
)
} else {
HashMap::new()
};
let mut res = Vec::new();
for cmd_name in matcher.results() {
if let Some(sugg) = internal_suggs
.remove(&cmd_name)
.or_else(|| external_suggs.remove(&cmd_name))
{
res.push(sugg);
}
}
res
}
}
@ -158,7 +166,7 @@ impl Completer for CommandCompletion {
&mut self,
working_set: &StateWorkingSet,
_stack: &Stack,
prefix: &[u8],
_prefix: &[u8],
span: Span,
offset: usize,
pos: usize,
@ -188,18 +196,18 @@ impl Completer for CommandCompletion {
Span::new(last.0.start, pos),
offset,
false,
options.match_algorithm,
options,
)
} else {
vec![]
};
if !subcommands.is_empty() {
return sort_suggestions(&String::from_utf8_lossy(prefix), subcommands, options);
return subcommands;
}
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(_))
|| ((span.end - span.start) == 0)
|| is_passthrough_command(working_set.delta.get_file_contents())
@ -214,13 +222,11 @@ impl Completer for CommandCompletion {
span,
offset,
config.completions.external.enable,
options.match_algorithm,
options,
)
} else {
vec![]
};
sort_suggestions(&String::from_utf8_lossy(prefix), commands, options)
}
}
}

View file

@ -1,22 +1,20 @@
use super::MatchAlgorithm;
use crate::{
completions::{matches, CompletionOptions},
SemanticSuggestion,
};
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use super::{completion_options::NuMatcher, MatchAlgorithm};
use crate::completions::CompletionOptions;
use nu_ansi_term::Style;
use nu_engine::env_to_string;
use nu_path::dots::expand_ndots;
use nu_path::{expand_to_real_path, home_dir};
use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet},
CompletionSort, Span,
Span,
};
use nu_utils::get_ls_colors;
use nu_utils::IgnoreCaseExt;
use std::path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP};
#[derive(Clone, Default)]
pub struct PathBuiltFromString {
cwd: PathBuf,
parts: Vec<String>,
isdir: bool,
}
@ -30,76 +28,84 @@ pub struct PathBuiltFromString {
/// want_directory: Whether we want only directories as completion matches.
/// Some commands like `cd` can only be run on directories whereas others
/// like `ls` can be run on regular files as well.
pub fn complete_rec(
fn complete_rec(
partial: &[&str],
built: &PathBuiltFromString,
cwd: &Path,
built_paths: &[PathBuiltFromString],
options: &CompletionOptions,
want_directory: bool,
isdir: bool,
) -> Vec<PathBuiltFromString> {
let mut completions = vec![];
if let Some((&base, rest)) = partial.split_first() {
if base.chars().all(|c| c == '.') && (isdir || !rest.is_empty()) {
let mut built = built.clone();
built.parts.push(base.to_string());
built.isdir = true;
return complete_rec(rest, &built, cwd, options, want_directory, isdir);
}
}
let mut built_path = cwd.to_path_buf();
for part in &built.parts {
built_path.push(part);
}
let Ok(result) = built_path.read_dir() else {
return completions;
};
let mut entries = Vec::new();
for entry in result.filter_map(|e| e.ok()) {
let entry_name = entry.file_name().to_string_lossy().into_owned();
let entry_isdir = entry.path().is_dir();
let mut built = built.clone();
built.parts.push(entry_name.clone());
built.isdir = entry_isdir;
if !want_directory || entry_isdir {
entries.push((entry_name, built));
let built_paths: Vec<_> = built_paths
.iter()
.map(|built| {
let mut built = built.clone();
built.parts.push(base.to_string());
built.isdir = true;
built
})
.collect();
return complete_rec(rest, &built_paths, options, want_directory, isdir);
}
}
let prefix = partial.first().unwrap_or(&"");
let sorted_entries = sort_completions(prefix, entries, options, |(entry, _)| entry);
let mut matcher = NuMatcher::new(prefix, options.clone());
for (entry_name, built) in sorted_entries {
for built in built_paths {
let mut path = built.cwd.clone();
for part in &built.parts {
path.push(part);
}
let Ok(result) = path.read_dir() else {
continue;
};
for entry in result.filter_map(|e| e.ok()) {
let entry_name = entry.file_name().to_string_lossy().into_owned();
let entry_isdir = entry.path().is_dir();
let mut built = built.clone();
built.parts.push(entry_name.clone());
built.isdir = entry_isdir;
if !want_directory || entry_isdir {
matcher.add(entry_name.clone(), (entry_name, built));
}
}
}
let mut completions = vec![];
for (entry_name, built) in matcher.results() {
match partial.split_first() {
Some((base, rest)) => {
if matches(base, &entry_name, options) {
// We use `isdir` to confirm that the current component has
// at least one next component or a slash.
// Serves as confirmation to ignore longer completions for
// components in between.
if !rest.is_empty() || isdir {
completions.extend(complete_rec(
rest,
&built,
cwd,
options,
want_directory,
isdir,
));
} else {
completions.push(built);
}
// We use `isdir` to confirm that the current component has
// at least one next component or a slash.
// Serves as confirmation to ignore longer completions for
// components in between.
if !rest.is_empty() || isdir {
completions.extend(complete_rec(
rest,
&[built],
options,
want_directory,
isdir,
));
} else {
completions.push(built);
}
if entry_name.eq(base)
&& matches!(options.match_algorithm, MatchAlgorithm::Prefix)
&& isdir
{
break;
// For https://github.com/nushell/nushell/issues/13204
if isdir && options.match_algorithm == MatchAlgorithm::Prefix {
let exact_match = if options.case_sensitive {
entry_name.eq(base)
} else {
entry_name.to_folded_case().eq(&base.to_folded_case())
};
if exact_match {
break;
}
}
}
None => {
@ -147,15 +153,25 @@ fn surround_remove(partial: &str) -> 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(
want_directory: bool,
span: nu_protocol::Span,
partial: &str,
cwd: &str,
cwds: &[impl AsRef<str>],
options: &CompletionOptions,
engine_state: &EngineState,
stack: &Stack,
) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
) -> Vec<FileSuggestion> {
let cleaned_partial = surround_remove(partial);
let isdir = cleaned_partial.ends_with(is_separator);
let expanded_partial = expand_ndots(Path::new(&cleaned_partial));
@ -175,7 +191,10 @@ pub fn complete_item(
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
&& engine_state.config.use_ansi_coloring)
.then(|| {
@ -186,7 +205,7 @@ pub fn complete_item(
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 original_cwd = OriginalCwd::None;
@ -194,19 +213,21 @@ pub fn complete_item(
match components.peek().cloned() {
Some(c @ Component::Prefix(..)) => {
// windows only by definition
cwd = [c, Component::RootDir].iter().collect();
cwds = vec![[c, Component::RootDir].iter().collect()];
prefix_len = c.as_os_str().len();
original_cwd = OriginalCwd::Prefix(c.as_os_str().to_string_lossy().into_owned());
}
Some(c @ Component::RootDir) => {
// This is kind of a hack. When joining an empty string with the rest,
// we add the slash automagically
cwd = PathBuf::from(c.as_os_str());
cwds = vec![PathBuf::from(c.as_os_str())];
prefix_len = 1;
original_cwd = OriginalCwd::Prefix(String::new());
}
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;
original_cwd = OriginalCwd::Home;
}
@ -223,8 +244,14 @@ pub fn complete_item(
complete_rec(
partial.as_slice(),
&PathBuiltFromString::default(),
&cwd,
&cwds
.into_iter()
.map(|cwd| PathBuiltFromString {
cwd,
parts: Vec::new(),
isdir: false,
})
.collect::<Vec<_>>(),
options,
want_directory,
isdir,
@ -234,6 +261,7 @@ pub fn complete_item(
if should_collapse_dots {
p = collapse_ndots(p);
}
let cwd = p.cwd.clone();
let path = original_cwd.apply(p, path_separator);
let style = ls_colors.as_ref().map(|lsc| {
lsc.style_for_path_with_metadata(
@ -245,7 +273,12 @@ pub fn complete_item(
.map(lscolors::Style::to_nu_ansi_term_style)
.unwrap_or_default()
});
(span, escape_path(path, want_directory), style)
FileSuggestion {
span,
path: escape_path(path, want_directory),
style,
cwd,
}
})
.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.
///
/// 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 {
parts: Vec::with_capacity(path.parts.len()),
isdir: path.isdir,
cwd: path.cwd,
};
let mut dot_count = 0;

View file

@ -1,7 +1,10 @@
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use nu_parser::trim_quotes_str;
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.
#[derive(Copy, Clone, Debug, PartialEq)]
@ -19,33 +22,154 @@ pub enum MatchAlgorithm {
Fuzzy,
}
impl MatchAlgorithm {
/// Returns whether the `needle` search text matches the given `haystack`.
pub fn matches_str(&self, haystack: &str, needle: &str) -> bool {
let haystack = trim_quotes_str(haystack);
let needle = trim_quotes_str(needle);
match *self {
MatchAlgorithm::Prefix => haystack.starts_with(needle),
pub struct NuMatcher<T> {
options: CompletionOptions,
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 matcher = SkimMatcherV2::default();
matcher.fuzzy_match(haystack, needle).is_some()
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 the `needle` search text matches the given `haystack`.
pub fn matches_u8(&self, haystack: &[u8], needle: &[u8]) -> bool {
match *self {
MatchAlgorithm::Prefix => haystack.starts_with(needle),
MatchAlgorithm::Fuzzy => {
let haystack_str = String::from_utf8_lossy(haystack);
let needle_str = String::from_utf8_lossy(needle);
let matcher = SkimMatcherV2::default();
matcher.fuzzy_match(&haystack_str, &needle_str).is_some()
/// 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);
match &mut self.state {
State::Prefix { items } => {
let haystack_folded = if self.options.case_sensitive {
Cow::Borrowed(haystack)
} else {
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
}
}
}
/// Add the given item if the given haystack matches the needle.
///
/// Returns whether the item was added.
pub fn add(&mut self, haystack: impl AsRef<str>, item: T) -> bool {
self.matches_aux(haystack.as_ref(), Some(item))
}
/// Returns whether the haystack matches the needle.
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)
}
}
impl From<CompletionAlgorithm> for MatchAlgorithm {
@ -105,35 +229,49 @@ impl Default for CompletionOptions {
#[cfg(test)]
mod test {
use super::MatchAlgorithm;
use rstest::rstest;
#[test]
fn match_algorithm_prefix() {
let algorithm = MatchAlgorithm::Prefix;
use super::{CompletionOptions, MatchAlgorithm, NuMatcher};
assert!(algorithm.matches_str("example text", ""));
assert!(algorithm.matches_str("example text", "examp"));
assert!(!algorithm.matches_str("example text", "text"));
assert!(algorithm.matches_u8(&[1, 2, 3], &[]));
assert!(algorithm.matches_u8(&[1, 2, 3], &[1, 2]));
assert!(!algorithm.matches_u8(&[1, 2, 3], &[2, 3]));
#[rstest]
#[case(MatchAlgorithm::Prefix, "example text", "", true)]
#[case(MatchAlgorithm::Prefix, "example text", "examp", true)]
#[case(MatchAlgorithm::Prefix, "example text", "text", false)]
#[case(MatchAlgorithm::Fuzzy, "example text", "", true)]
#[case(MatchAlgorithm::Fuzzy, "example text", "examp", true)]
#[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]
fn match_algorithm_fuzzy() {
let algorithm = MatchAlgorithm::Fuzzy;
assert!(algorithm.matches_str("example text", ""));
assert!(algorithm.matches_str("example text", "examp"));
assert!(algorithm.matches_str("example text", "ext"));
assert!(algorithm.matches_str("example text", "mplxt"));
assert!(!algorithm.matches_str("example text", "mpp"));
assert!(algorithm.matches_u8(&[1, 2, 3], &[]));
assert!(algorithm.matches_u8(&[1, 2, 3], &[1, 2]));
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]));
fn match_algorithm_fuzzy_sort_score() {
let options = CompletionOptions {
match_algorithm: MatchAlgorithm::Fuzzy,
..Default::default()
};
let mut matcher = NuMatcher::new("fob", options);
for item in ["foo/bar", "fob", "foo bar"] {
matcher.add(item, item);
}
// Sort by score, then in alphabetical order
assert_eq!(vec!["fob", "foo bar", "foo/bar"], matcher.results());
}
}

View file

@ -9,10 +9,9 @@ use nu_protocol::{
engine::{Stack, StateWorkingSet},
CompletionSort, DeclId, PipelineData, Span, Type, Value,
};
use nu_utils::IgnoreCaseExt;
use std::collections::HashMap;
use super::completion_common::sort_suggestions;
use super::completion_options::NuMatcher;
pub struct CustomCompletion {
stack: Stack,
@ -123,41 +122,11 @@ impl Completer for CustomCompletion {
})
.unwrap_or_default();
let options = custom_completion_options
.as_ref()
.unwrap_or(completion_options);
let suggestions = filter(prefix, suggestions, options);
sort_suggestions(&String::from_utf8_lossy(prefix), suggestions, options)
let options = custom_completion_options.unwrap_or(completion_options.clone());
let mut matcher = NuMatcher::new(String::from_utf8_lossy(prefix), options);
for sugg in suggestions {
matcher.add_semantic_suggestion(sugg);
}
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},
Completer, CompletionOptions,
};
use nu_ansi_term::Style;
use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet},
Span,
@ -10,7 +9,7 @@ use nu_protocol::{
use reedline::Suggestion;
use std::path::Path;
use super::SemanticSuggestion;
use super::{completion_common::FileSuggestion, SemanticSuggestion};
#[derive(Clone, Default)]
pub struct DirectoryCompletion {}
@ -47,11 +46,11 @@ impl Completer for DirectoryCompletion {
.into_iter()
.map(move |x| SemanticSuggestion {
suggestion: Suggestion {
value: x.1,
style: x.2,
value: x.path,
style: x.style,
span: reedline::Span {
start: x.0.start - offset,
end: x.0.end - offset,
start: x.span.start - offset,
end: x.span.end - offset,
},
..Suggestion::default()
},
@ -92,6 +91,6 @@ pub fn directory_completion(
options: &CompletionOptions,
engine_state: &EngineState,
stack: &Stack,
) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
complete_item(true, span, partial, cwd, options, engine_state, stack)
) -> Vec<FileSuggestion> {
complete_item(true, span, partial, &[cwd], options, engine_state, stack)
}

View file

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

View file

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

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::{
ast::{Expr, Expression},
engine::{Stack, StateWorkingSet},
@ -35,7 +35,7 @@ impl Completer for FlagCompletion {
let decl = working_set.get_decl(call.decl_id);
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 {
let flag_desc = &named.desc;
@ -44,34 +44,7 @@ impl Completer for FlagCompletion {
short.encode_utf8(&mut named);
named.insert(0, b'-');
if options.match_algorithm.matches_u8(&named, prefix) {
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()),
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
append_whitespace: true,
..Suggestion::default()
},
// TODO????
kind: None,
});
}
}
if named.long.is_empty() {
continue;
}
let mut named = named.long.as_bytes().to_vec();
named.insert(0, b'-');
named.insert(0, b'-');
if options.match_algorithm.matches_u8(&named, prefix) {
output.push(SemanticSuggestion {
matcher.add_semantic_suggestion(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()),
@ -86,9 +59,32 @@ impl Completer for FlagCompletion {
kind: None,
});
}
if named.long.is_empty() {
continue;
}
let mut named = named.long.as_bytes().to_vec();
named.insert(0, b'-');
named.insert(0, b'-');
matcher.add_semantic_suggestion(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()),
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
append_whitespace: true,
..Suggestion::default()
},
// TODO????
kind: None,
});
}
return sort_suggestions(&String::from_utf8_lossy(prefix), output, options);
return matcher.results();
}
vec![]

View file

@ -18,7 +18,7 @@ pub use completion_options::{CompletionOptions, MatchAlgorithm};
pub use custom_completions::CustomCompletion;
pub use directory_completions::DirectoryCompletion;
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 operator_completions::OperatorCompletion;
pub use variable_completions::VariableCompletion;

View file

@ -1,5 +1,5 @@
use crate::completions::{
Completer, CompletionOptions, MatchAlgorithm, SemanticSuggestion, SuggestionKind,
completion_options::NuMatcher, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind,
};
use nu_protocol::{
ast::{Expr, Expression},
@ -28,7 +28,7 @@ impl Completer for OperatorCompletion {
span: Span,
offset: usize,
_pos: usize,
_options: &CompletionOptions,
options: &CompletionOptions,
) -> Vec<SemanticSuggestion> {
//Check if int, float, or string
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"),
("in", "Is 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![
("=~", "Contains regex match"),
@ -72,7 +68,7 @@ impl Completer for OperatorCompletion {
("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)"),
("not-in", "Is not a member of (doesn't use regex)"),
@ -95,10 +91,6 @@ impl Completer for OperatorCompletion {
("**", "Power of"),
("in", "Is 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![
(
@ -113,15 +105,11 @@ impl Completer for OperatorCompletion {
("not", "Negates a value or expression"),
("in", "Is 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::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),
_ => vec![],
@ -129,17 +117,12 @@ impl Completer for OperatorCompletion {
_ => vec![],
};
let match_algorithm = MatchAlgorithm::Prefix;
let input_fuzzy_search =
|(operator, _): &(&str, &str)| match_algorithm.matches_str(operator, partial);
possible_operations
.into_iter()
.filter(input_fuzzy_search)
.map(move |x| SemanticSuggestion {
let mut matcher = NuMatcher::new(partial, options.clone());
for (symbol, desc) in possible_operations.into_iter() {
matcher.add_semantic_suggestion(SemanticSuggestion {
suggestion: Suggestion {
value: x.0.to_string(),
description: Some(x.1.to_string()),
value: symbol.to_string(),
description: Some(desc.to_string()),
span: reedline::Span::new(span.start - offset, span.end - offset),
append_whitespace: true,
..Suggestion::default()
@ -147,8 +130,9 @@ impl Completer for OperatorCompletion {
kind: Some(SuggestionKind::Command(
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![
(
"++=",
"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."),
],

View file

@ -1,6 +1,4 @@
use crate::completions::{
Completer, CompletionOptions, MatchAlgorithm, SemanticSuggestion, SuggestionKind,
};
use crate::completions::{Completer, CompletionOptions, SemanticSuggestion, SuggestionKind};
use nu_engine::{column::get_columns, eval_variable};
use nu_protocol::{
engine::{Stack, StateWorkingSet},
@ -9,7 +7,7 @@ use nu_protocol::{
use reedline::Suggestion;
use std::str;
use super::completion_common::sort_suggestions;
use super::completion_options::NuMatcher;
#[derive(Clone)]
pub struct VariableCompletion {
@ -33,7 +31,6 @@ impl Completer for VariableCompletion {
_pos: usize,
options: &CompletionOptions,
) -> Vec<SemanticSuggestion> {
let mut output = vec![];
let builtins = ["$nu", "$in", "$env"];
let var_str = std::str::from_utf8(&self.var_context.0).unwrap_or("");
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 prefix_str = String::from_utf8_lossy(prefix);
let mut matcher = NuMatcher::new(prefix_str, options.clone());
// Completions for the given variable
if !var_str.is_empty() {
@ -63,37 +61,25 @@ impl Completer for VariableCompletion {
if let Some(val) = env_vars.get(&target_var_str) {
for suggestion in nested_suggestions(val, &nested_levels, current_span) {
if options.match_algorithm.matches_u8_insensitive(
options.case_sensitive,
suggestion.suggestion.value.as_bytes(),
prefix,
) {
output.push(suggestion);
}
matcher.add_semantic_suggestion(suggestion);
}
return sort_suggestions(&prefix_str, output, options);
return matcher.results();
}
} else {
// No nesting provided, return all env vars
for env_var in env_vars {
if options.match_algorithm.matches_u8_insensitive(
options.case_sensitive,
env_var.0.as_bytes(),
prefix,
) {
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: env_var.0,
span: current_span,
..Suggestion::default()
},
kind: Some(SuggestionKind::Type(env_var.1.get_type())),
});
}
matcher.add_semantic_suggestion(SemanticSuggestion {
suggestion: Suggestion {
value: env_var.0,
span: current_span,
..Suggestion::default()
},
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)
{
if options.match_algorithm.matches_u8_insensitive(
options.case_sensitive,
suggestion.suggestion.value.as_bytes(),
prefix,
) {
output.push(suggestion);
}
matcher.add_semantic_suggestion(suggestion);
}
return sort_suggestions(&prefix_str, output, options);
return matcher.results();
}
}
@ -130,37 +110,25 @@ impl Completer for VariableCompletion {
if let Ok(value) = var {
for suggestion in nested_suggestions(&value, &self.var_context.1, current_span)
{
if options.match_algorithm.matches_u8_insensitive(
options.case_sensitive,
suggestion.suggestion.value.as_bytes(),
prefix,
) {
output.push(suggestion);
}
matcher.add_semantic_suggestion(suggestion);
}
return sort_suggestions(&prefix_str, output, options);
return matcher.results();
}
}
}
// Variable completion (e.g: $en<tab> to complete $env)
for builtin in builtins {
if options.match_algorithm.matches_u8_insensitive(
options.case_sensitive,
builtin.as_bytes(),
prefix,
) {
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: builtin.to_string(),
span: current_span,
..Suggestion::default()
},
// TODO is there a way to get the VarId to get the type???
kind: None,
});
}
matcher.add_semantic_suggestion(SemanticSuggestion {
suggestion: Suggestion {
value: builtin.to_string(),
span: current_span,
..Suggestion::default()
},
// TODO is there a way to get the VarId to get the type???
kind: None,
});
}
// TODO: The following can be refactored (see find_commands_by_predicate() used in
@ -170,40 +138,7 @@ impl Completer for VariableCompletion {
for scope_frame in working_set.delta.scope.iter().rev() {
for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() {
for v in &overlay_frame.vars {
if options.match_algorithm.matches_u8_insensitive(
options.case_sensitive,
v.0,
prefix,
) {
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(v.0).to_string(),
span: current_span,
..Suggestion::default()
},
kind: Some(SuggestionKind::Type(
working_set.get_variable(*v.1).ty.clone(),
)),
});
}
}
}
}
// Permanent state vars
// for scope in &self.engine_state.scope {
for overlay_frame in working_set
.permanent_state
.active_overlays(&removed_overlays)
.rev()
{
for v in &overlay_frame.vars {
if options.match_algorithm.matches_u8_insensitive(
options.case_sensitive,
v.0,
prefix,
) {
output.push(SemanticSuggestion {
matcher.add_semantic_suggestion(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(v.0).to_string(),
span: current_span,
@ -217,11 +152,28 @@ impl Completer for VariableCompletion {
}
}
output = sort_suggestions(&prefix_str, output, options);
// Permanent state vars
// for scope in &self.engine_state.scope {
for overlay_frame in working_set
.permanent_state
.active_overlays(&removed_overlays)
.rev()
{
for v in &overlay_frame.vars {
matcher.add_semantic_suggestion(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(v.0).to_string(),
span: current_span,
..Suggestion::default()
},
kind: Some(SuggestionKind::Type(
working_set.get_variable(*v.1).ty.clone(),
)),
});
}
}
output.dedup(); // TODO: Removes only consecutive duplicates, is it intended?
output
matcher.results()
}
}
@ -302,13 +254,3 @@ fn recursive_value(val: &Value, sublevels: &[Vec<u8>]) -> Result<Value, Span> {
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 crate::util::print_pipeline;
#[derive(Default)]
pub struct EvaluateCommandsOpts {
pub table_mode: Option<Value>,
@ -72,7 +74,7 @@ pub fn evaluate_commands(
if let Some(err) = working_set.compile_errors.first() {
report_compile_error(&working_set, err);
// Not a fatal error, for now
std::process::exit(1);
}
(output, working_set.render())
@ -93,7 +95,7 @@ pub fn evaluate_commands(
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!());

View file

@ -1,4 +1,4 @@
use crate::util::eval_source;
use crate::util::{eval_source, print_pipeline};
use log::{info, trace};
use nu_engine::{convert_env_values, eval_block};
use nu_parser::parse;
@ -89,7 +89,7 @@ pub fn evaluate_file(
if let Some(err) = working_set.compile_errors.first() {
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.
@ -119,7 +119,7 @@ pub fn evaluate_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.
// 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()
.print_raw(engine_state, no_newline, to_stderr)?;
} else {
arg.into_pipeline_data()
.print(engine_state, stack, no_newline, to_stderr)?;
arg.into_pipeline_data().print_table(
engine_state,
stack,
no_newline,
to_stderr,
)?;
}
}
} 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 {
input.print_raw(engine_state, no_newline, to_stderr)?;
} 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(
engine_state: &mut EngineState,
stack: &mut Stack,
@ -267,7 +296,7 @@ fn evaluate_source(
if let Some(err) = working_set.compile_errors.first() {
report_compile_error(&working_set, err);
// Not a fatal error, for now
return Ok(true);
}
(output, working_set.render())
@ -281,36 +310,12 @@ fn evaluate_source(
eval_block::<WithoutDebug>(engine_state, stack, &block, input)
}?;
if let PipelineData::ByteStream(..) = pipeline {
// run the display hook on bytestreams too
run_display_hook(engine_state, stack, pipeline, false)
} else {
run_display_hook(engine_state, stack, pipeline, true)
}?;
let no_newline = matches!(&pipeline, &PipelineData::ByteStream(..));
print_pipeline(engine_state, stack, pipeline, no_newline)?;
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)]
mod test {
use super::*;

View file

@ -890,8 +890,8 @@ fn subcommand_completions(mut subcommand_completer: NuCompleter) {
match_suggestions(
&vec![
"foo bar".to_string(),
"foo aabcrr".to_string(),
"foo abaz".to_string(),
"foo aabcrr".to_string(),
],
&suggestions,
);
@ -955,8 +955,8 @@ fn flag_completions() {
"--mime-type".into(),
"--short-names".into(),
"--threads".into(),
"-D".into(),
"-a".into(),
"-D".into(),
"-d".into(),
"-f".into(),
"-h".into(),
@ -1287,7 +1287,7 @@ fn variables_completions() {
assert_eq!(3, suggestions.len());
#[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))]
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"]
#[rstest]
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" }
itertools = { workspace = true }
shadow-rs = { version = "0.35", default-features = false }
shadow-rs = { version = "0.36", default-features = false }
[build-dependencies]
shadow-rs = { version = "0.35", default-features = false }
shadow-rs = { version = "0.36", default-features = false }
[features]
mimalloc = []

View file

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

View file

@ -86,7 +86,6 @@ serde_yaml = { workspace = true }
sha2 = { workspace = true }
sysinfo = { workspace = true }
tabled = { workspace = true, features = ["ansi"], default-features = false }
terminal_size = { workspace = true }
titlecase = { workspace = true }
toml = { workspace = true, features = ["preserve_order"] }
unicode-segmentation = { workspace = true }
@ -96,6 +95,7 @@ uu_cp = { workspace = true }
uu_mkdir = { workspace = true }
uu_mktemp = { workspace = true }
uu_mv = { workspace = true }
uu_touch = { workspace = true }
uu_uname = { workspace = true }
uu_whoami = { workspace = true }
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::Error
| Type::List(_)
| Type::ListStream
| Type::Range
| Type::Record(_)
| Type::Signature

View file

@ -1,6 +1,7 @@
use nu_engine::command_prelude::*;
use nu_parser::parse;
use nu_protocol::engine::StateWorkingSet;
use nu_parser::{flatten_block, parse};
use nu_protocol::{engine::StateWorkingSet, record};
use serde_json::{json, Value as JsonValue};
#[derive(Clone)]
pub struct Ast;
@ -16,109 +17,23 @@ impl Command for Ast {
fn signature(&self) -> Signature {
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(
"pipeline",
SyntaxShape::String,
"The pipeline to print the ast for.",
)
.switch("json", "serialize to json", Some('j'))
.switch("minify", "minify the nuon or json output", Some('m'))
.switch("json", "Serialize to json", Some('j'))
.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)
.category(Category::Debug)
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let pipeline: Spanned<String> = call.req(engine_state, stack, 0)?;
let to_json = call.has_flag(engine_state, stack, "json")?;
let minify = call.has_flag(engine_state, stack, "minify")?;
let mut working_set = StateWorkingSet::new(engine_state);
let block_output = parse(&mut working_set, None, pipeline.item.as_bytes(), false);
let error_output = working_set.parse_errors.first();
let block_span = match &block_output.span {
Some(span) => span,
None => &pipeline.span,
};
if to_json {
// Get the block as json
let serde_block_str = if minify {
serde_json::to_string(&*block_output)
} else {
serde_json::to_string_pretty(&*block_output)
};
let block_json = match serde_block_str {
Ok(json) => json,
Err(e) => Err(ShellError::CantConvert {
to_type: "string".to_string(),
from_type: "block".to_string(),
span: *block_span,
help: Some(format!(
"Error: {e}\nCan't convert {block_output:?} to string"
)),
})?,
};
// Get the error as json
let serde_error_str = if minify {
serde_json::to_string(&error_output)
} else {
serde_json::to_string_pretty(&error_output)
};
let error_json = match serde_error_str {
Ok(json) => json,
Err(e) => Err(ShellError::CantConvert {
to_type: "string".to_string(),
from_type: "error".to_string(),
span: *block_span,
help: Some(format!(
"Error: {e}\nCan't convert {error_output:?} to string"
)),
})?,
};
// Create a new output record, merging the block and error
let output_record = Value::record(
record! {
"block" => Value::string(block_json, *block_span),
"error" => Value::string(error_json, Span::test_data()),
},
pipeline.span,
);
Ok(output_record.into_pipeline_data())
} else {
let block_value = Value::string(
if minify {
format!("{block_output:?}")
} else {
format!("{block_output:#?}")
},
pipeline.span,
);
let error_value = Value::string(
if minify {
format!("{error_output:?}")
} else {
format!("{error_output:#?}")
},
pipeline.span,
);
let output_record = Value::record(
record! {
"block" => block_value,
"error" => error_value
},
pipeline.span,
);
Ok(output_record.into_pipeline_data())
}
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
@ -147,8 +62,247 @@ impl Command for Ast {
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(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let pipeline: Spanned<String> = call.req(engine_state, stack, 0)?;
let to_json = call.has_flag(engine_state, stack, "json")?;
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 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 block_span = match &parsed_block.span {
Some(span) => span,
None => &pipeline.span,
};
if to_json {
// Get the block as json
let serde_block_str = if minify {
serde_json::to_string(&*parsed_block)
} else {
serde_json::to_string_pretty(&*parsed_block)
};
let block_json = match serde_block_str {
Ok(json) => json,
Err(e) => Err(ShellError::CantConvert {
to_type: "string".to_string(),
from_type: "block".to_string(),
span: *block_span,
help: Some(format!(
"Error: {e}\nCan't convert {parsed_block:?} to string"
)),
})?,
};
// Get the error as json
let serde_error_str = if minify {
serde_json::to_string(&error_output)
} else {
serde_json::to_string_pretty(&error_output)
};
let error_json = match serde_error_str {
Ok(json) => json,
Err(e) => Err(ShellError::CantConvert {
to_type: "string".to_string(),
from_type: "error".to_string(),
span: *block_span,
help: Some(format!(
"Error: {e}\nCan't convert {error_output:?} to string"
)),
})?,
};
// Create a new output record, merging the block and error
let output_record = Value::record(
record! {
"block" => Value::string(block_json, *block_span),
"error" => Value::string(error_json, Span::test_data()),
},
pipeline.span,
);
Ok(output_record.into_pipeline_data())
} else {
let block_value = Value::string(
if minify {
format!("{parsed_block:?}")
} else {
format!("{parsed_block:#?}")
},
pipeline.span,
);
let error_value = Value::string(
if minify {
format!("{error_output:?}")
} else {
format!("{error_output:#?}")
},
pipeline.span,
);
let output_record = Value::record(
record! {
"block" => block_value,
"error" => error_value
},
pipeline.span,
);
Ok(output_record.into_pipeline_data())
}
}
}
}
fn json_merge(a: &mut JsonValue, b: &JsonValue) {
match (a, b) {
(JsonValue::Object(ref mut a), JsonValue::Object(b)) => {
for (k, v) in b {
json_merge(a.entry(k).or_insert(JsonValue::Null), v);
}
}
(JsonValue::Array(ref mut a), JsonValue::Array(b)) => {
a.extend(b.clone());
}
(JsonValue::Array(ref mut a), JsonValue::Object(b)) => {
a.extend([JsonValue::Object(b.clone())]);
}
(a, b) => {
*a = b.clone();
}
}
}
#[cfg(test)]

View file

@ -1,6 +1,6 @@
use super::inspect_table;
use crossterm::terminal::size;
use nu_engine::command_prelude::*;
use terminal_size::{terminal_size, Height, Width};
#[derive(Clone)]
pub struct Inspect;
@ -38,12 +38,9 @@ impl Command for Inspect {
let original_input = input_val.clone();
let description = input_val.get_type().to_string();
let (cols, _rows) = match terminal_size() {
Some((w, h)) => (Width(w.0), Height(h.0)),
None => (Width(0), Height(0)),
};
let (cols, _rows) = size().unwrap_or((0, 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
// 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,
Save,
Touch,
UTouch,
Glob,
Watch,
};
@ -247,7 +248,9 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
IsTerminal,
Kill,
Sleep,
Term,
TermSize,
TermQuery,
Whoami,
};

View file

@ -15,7 +15,16 @@ impl Command for ConfigEnv {
Signature::build(self.name())
.category(Category::Env)
.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
}
@ -26,18 +35,18 @@ impl Command for ConfigEnv {
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "allow user to open and update nu env",
description: "open user's env.nu in the default editor",
example: "config env",
result: None,
},
Example {
description: "allow user to print default `env.nu` file",
example: "config env --default,",
description: "pretty-print a commented, sample `env.nu` that explains common settings",
example: "config env --sample | nu-highlight,",
result: None,
},
Example {
description: "allow saving the default `env.nu` locally",
example: "config env --default | save -f ~/.config/nushell/default_env.nu",
description: "pretty-print the internal `env.nu` file which is loaded before the user's environment",
example: "config env --default | nu-highlight,",
result: None,
},
]
@ -50,12 +59,28 @@ impl Command for ConfigEnv {
call: &Call,
_input: PipelineData,
) -> 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
if call.has_flag(engine_state, stack, "default")? {
let head = call.head;
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.
let (editor_name, editor_args) = get_editor(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)])
.switch(
"default",
"Print default `config.nu` file instead.",
"Print the internal default `config.nu` file instead.",
Some('d'),
)
.switch(
"sample",
"Print a commented, sample `config.nu` file instead.",
Some('s'),
)
// TODO: Signature narrower than what run actually supports theoretically
}
@ -30,18 +35,19 @@ impl Command for ConfigNu {
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "allow user to open and update nu config",
description: "open user's config.nu in the default editor",
example: "config nu",
result: None,
},
Example {
description: "allow user to print default `config.nu` file",
example: "config nu --default,",
description: "pretty-print a commented, sample `config.nu` that explains common settings",
example: "config nu --sample | nu-highlight",
result: None,
},
Example {
description: "allow saving the default `config.nu` locally",
example: "config nu --default | save -f ~/.config/nushell/default_config.nu",
description:
"pretty-print the internal `config.nu` file which is loaded before user's config",
example: "config nu --default | nu-highlight",
result: None,
},
]
@ -54,12 +60,29 @@ impl Command for ConfigNu {
call: &Call,
_input: PipelineData,
) -> 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
if call.has_flag(engine_state, stack, "default")? {
if default_flag {
let head = call.head;
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.
let (editor_name, editor_args) = get_editor(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 umv;
mod util;
mod utouch;
mod watch;
pub use self::open::Open;
@ -27,4 +28,5 @@ pub use touch::Touch;
pub use ucp::UCp;
pub use umkdir::UMkdir;
pub use umv::UMv;
pub use utouch::UTouch;
pub use watch::Watch;

View file

@ -188,6 +188,7 @@ impl Command for UMv {
target_dir: None,
no_target_dir: false,
strip_slashes: false,
debug: false,
};
if let Err(error) = uu_mv::mv(&files, &options) {
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 {
Ok(val) => {
val.print(engine_state, stack, false, false)?;
val.print_table(engine_state, stack, false, false)?;
}
Err(err) => {
let working_set = StateWorkingSet::new(engine_state);

View file

@ -129,6 +129,8 @@ fn insert(
let replacement: Value = call.req(engine_state, stack, 1)?;
match input {
// Propagate errors in the pipeline
PipelineData::Value(Value::Error { error, .. }, ..) => Err(*error),
PipelineData::Value(mut value, metadata) => {
if let Value::Closure { val, .. } = replacement {
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, .. }, ..),
Value::Record { val: to_merge, .. },
) => 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, ..), ..) => {
// 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

View file

@ -1,5 +1,6 @@
use indexmap::IndexMap;
use nu_engine::command_prelude::*;
use nu_protocol::report_shell_warning;
#[derive(Clone)]
pub struct SplitBy;
@ -27,6 +28,15 @@ impl Command for SplitBy {
call: &Call,
input: PipelineData,
) -> 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)
}

View file

@ -11,7 +11,10 @@ impl Command for FromCsv {
fn signature(&self) -> Signature {
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(
"separator",
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 {
description: "Convert comma-separated data to a table, ignoring headers",
example: "open data.txt | from csv --noheaders",

View file

@ -39,12 +39,7 @@ fn from_delimited_stream(
.from_reader(input_reader);
let headers = if noheaders {
(0..reader
.headers()
.map_err(|err| from_csv_error(err, span))?
.len())
.map(|i| format!("column{i}"))
.collect::<Vec<String>>()
vec![]
} else {
reader
.headers()
@ -54,32 +49,28 @@ fn from_delimited_stream(
.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 row = match row {
Ok(row) => row,
Err(err) => return Value::error(from_csv_error(err, span), span),
};
let columns = headers.iter().cloned();
let values = row
.into_iter()
.map(|s| {
if no_infer {
Value::string(s, span)
} else if let Ok(i) = s.parse() {
Value::int(i, span)
} else if let Ok(f) = s.parse() {
Value::float(f, span)
} else {
Value::string(s, span)
}
})
.chain(std::iter::repeat(Value::nothing(span)));
let columns = columns.clone();
let values = row.into_iter().map(|s| {
if no_infer {
Value::string(s, span)
} else if let Ok(i) = s.parse() {
Value::int(i, span)
} else if let Ok(f) = s.parse() {
Value::float(f, span)
} else {
Value::string(s, 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)
});

View file

@ -11,7 +11,10 @@ impl Command for FromTsv {
fn signature(&self) -> Signature {
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(
"comment",
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 {
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"#,

View file

@ -4,7 +4,7 @@ use nu_engine::command_prelude::*;
use quick_xml::{
escape,
events::{BytesEnd, BytesStart, BytesText, Event},
events::{BytesEnd, BytesPI, BytesStart, BytesText, Event},
};
use std::{borrow::Cow, io::Cursor};
@ -406,7 +406,7 @@ impl Job {
let content_text = format!("{} {}", tag, content);
// PI content must NOT be escaped
// 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
.write_event(Event::PI(pi_content))

View file

@ -31,7 +31,7 @@ impl Command for HelpOperators {
let mut operators = [
Operator::Assignment(Assignment::Assign),
Operator::Assignment(Assignment::PlusAssign),
Operator::Assignment(Assignment::AppendAssign),
Operator::Assignment(Assignment::ConcatAssign),
Operator::Assignment(Assignment::MinusAssign),
Operator::Assignment(Assignment::MultiplyAssign),
Operator::Assignment(Assignment::DivideAssign),
@ -48,7 +48,7 @@ impl Command for HelpOperators {
Operator::Comparison(Comparison::StartsWith),
Operator::Comparison(Comparison::EndsWith),
Operator::Math(Math::Plus),
Operator::Math(Math::Append),
Operator::Math(Math::Concat),
Operator::Math(Math::Minus),
Operator::Math(Math::Multiply),
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::EndsWith) => "Checks if a string ends with another.",
Operator::Math(Math::Plus) => "Adds two values.",
Operator::Math(Math::Append) => {
"Appends two lists, a list and a value, two strings, or two binary values."
Operator::Math(Math::Concat) => {
"Concatenates two lists, two strings, or two binary values."
}
Operator::Math(Math::Minus) => "Subtracts 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::Assignment(Assignment::Assign) => "Assigns a value to a variable.",
Operator::Assignment(Assignment::PlusAssign) => "Adds a value to a variable.",
Operator::Assignment(Assignment::AppendAssign) => {
"Appends a list, a value, a string, or a binary value to a variable."
Operator::Assignment(Assignment::ConcatAssign) => {
"Concatenates two lists, two strings, or two binary values."
}
Operator::Assignment(Assignment::MinusAssign) => "Subtracts a value from a variable.",
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()),
];
builder
.add(&mut Cursor::new(val), &headers.join("\n"))
.add(&mut Cursor::new(val), &headers.join("\r\n"))
.map_err(err)?;
} else {
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",
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'))
.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 {
print!("{prompt}");
match &default_val {
None => print!("{prompt}"),
Some(val) => print!("{prompt} (default: {val})"),
}
let _ = std::io::stdout().flush();
}
@ -149,7 +159,10 @@ impl Command for Input {
if !suppress_output {
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> {
@ -164,6 +177,11 @@ impl Command for Input {
example: "let user_input = (input --numchar 2)",
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 kill;
mod sleep;
mod term_size;
mod term;
#[cfg(unix)]
mod ulimit;
mod whoami;
@ -19,7 +19,7 @@ pub use input::InputListen;
pub use is_terminal::IsTerminal;
pub use kill::Kill;
pub use sleep::Sleep;
pub use term_size::TermSize;
pub use term::{Term, TermQuery, TermSize};
#[cfg(unix)]
pub use ulimit::ULimit;
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 terminal_size::{terminal_size, Height, Width};
#[derive(Clone)]
pub struct TermSize;
@ -51,15 +51,12 @@ impl Command for TermSize {
) -> Result<PipelineData, ShellError> {
let head = call.head;
let (cols, rows) = match terminal_size() {
Some((w, h)) => (Width(w.0), Height(h.0)),
None => (Width(0), Height(0)),
};
let (cols, rows) = size().unwrap_or((0, 0));
Ok(Value::record(
record! {
"columns" => Value::int(cols.0 as i64, head),
"rows" => Value::int(rows.0 as i64, head),
"columns" => Value::int(cols as i64, head),
"rows" => Value::int(rows as i64, head),
},
head,
)

View file

@ -12,7 +12,7 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
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)
.named(
"dice",

View file

@ -77,20 +77,6 @@ impl Command for SubCommand {
call: &Call,
input: PipelineData,
) -> 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 = (!cell_paths.is_empty()).then_some(cell_paths);
let args = Arguments {

View file

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

View file

@ -15,7 +15,6 @@ impl Command for NuCheck {
Signature::build("nu-check")
.input_output_types(vec![
(Type::String, Type::Bool),
(Type::ListStream, Type::Bool),
(Type::List(Box::new(Type::Any)), Type::Bool),
])
// 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_utils::IgnoreCaseExt;
use pathdiff::diff_paths;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
use std::{
borrow::Cow,
ffi::{OsStr, OsString},
@ -91,6 +93,22 @@ impl Command for External {
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
// executable to "cmd.exe" if it's a CMD internal command. If the
// 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)
{
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 {
// 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
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));
};
executable
@ -123,15 +146,29 @@ impl Command for External {
let args = eval_arguments_from_call(engine_state, stack, call)?;
#[cfg(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 /C flag followed by a command name instructs CMD to execute
// that command and quit.
command.args(["/D", "/C", &name_str]);
command.args(["/D", "/C", &expanded_name.to_string_lossy()]);
for arg in &args {
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 {
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()
.iter()
.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! {
"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),
"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;
use crossterm::terminal::size;
use lscolors::Style;
use nu_engine::{command_prelude::*, env_to_string};
use nu_protocol::Config;
use nu_term_grid::grid::{Alignment, Cell, Direction, Filling, Grid, GridOptions};
use nu_utils::get_ls_colors;
use std::path::Path;
use terminal_size::{Height, Width};
#[derive(Clone)]
pub struct Griddle;
@ -192,7 +192,7 @@ fn create_grid_output(
let cols = if let Some(col) = width_param {
col as u16
} else if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() {
} else if let Ok((w, _h)) = size() {
w
} else {
80u16

View file

@ -2,6 +2,7 @@
// overall reduce the redundant calls to StyleComputer etc.
// the goal is to configure it once...
use crossterm::terminal::size;
use lscolors::{LsColors, Style};
use nu_color_config::{color_from_hex, StyleComputer, TextStyle};
use nu_engine::{command_prelude::*, env_to_string};
@ -22,7 +23,6 @@ use std::{
str::FromStr,
time::Instant,
};
use terminal_size::{Height, Width};
use url::Url;
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 {
if let Some(col) = width_param {
col as usize
} else if let Some((Width(w), Height(_))) = terminal_size::terminal_size() {
} else if let Ok((w, _h)) = size() {
w as usize
} else {
80
@ -1088,7 +1088,7 @@ fn create_empty_placeholder(
let data = vec![vec![cell]];
let mut table = NuTable::from(data);
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 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 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]
fn def_with_comment() {
Playground::setup("def_with_comment", |dirs, _| {
@ -72,6 +79,13 @@ fn def_errors_with_comma_before_equals() {
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]
fn def_errors_with_comma_before_colon() {
let actual = nu!("def test-command [ foo, : int ] {}");
@ -85,7 +99,6 @@ fn def_errors_with_multiple_colons() {
assert!(actual.err.contains("expected type"));
}
#[ignore = "This error condition is not implemented yet"]
#[test]
fn def_errors_with_multiple_types() {
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"));
}
#[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]
fn def_errors_with_multiple_commas() {
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() {
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");
}
@ -52,7 +52,7 @@ fn ignore_shell_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""#);
assert_eq!(actual.err, "");
assert!(actual.err.contains("Deprecated option"));
assert_eq!(actual.out, "text");
}
@ -80,6 +80,7 @@ fn run_closure_with_it_using() {
#[test]
fn waits_for_external() {
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");
}

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
#[test]
fn adding_lists() {
fn concat_lists() {
let actual = nu!(pipeline(
r#"
[1 3] ++ [5 6] | to nuon
@ -494,29 +494,7 @@ fn adding_lists() {
}
#[test]
fn adding_list_and_value() {
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() {
fn concat_tables() {
let actual = nu!(pipeline(
r#"
[[a b]; [1 2]] ++ [[c d]; [10 11]] | to nuon
@ -526,7 +504,7 @@ fn adding_tables() {
}
#[test]
fn append_strings() {
fn concat_strings() {
let actual = nu!(pipeline(
r#"
"foo" ++ "bar"
@ -536,7 +514,7 @@ fn append_strings() {
}
#[test]
fn append_binary_values() {
fn concat_binary_values() {
let actual = nu!(pipeline(
r#"
0x[01 02] ++ 0x[03 04] | to nuon

View file

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

View file

@ -513,13 +513,18 @@ fn test_mv_no_clobber() {
sandbox.with_files(&[EmptyFile(file_a)]);
sandbox.with_files(&[EmptyFile(file_b)]);
let actual = nu!(
let _ = nu!(
cwd: dirs.test(),
"mv -n {} {}",
file_a,
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)]
#[test]
fn can_run_batch_files() {
fn can_run_cmd_files() {
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(
"foo.cmd",
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)]
#[test]
fn can_run_batch_files_without_cmd_extension() {
use nu_test_support::fs::Stub::FileWithContent;
Playground::setup(
"run a Windows batch file without specifying the extension",
"run a Windows cmd file without specifying the extension",
|dirs, sandbox| {
sandbox.with_files(&[FileWithContent(
"foo.cmd",
@ -440,3 +458,20 @@ fn redirect_combine() {
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("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_hash = get_file_hash(target.display());
let actual = nu!(
cwd: dirs.root(),
"cp {} {} --no-clobber",
src.display(),
target.display()
let _ = nu!(
cwd: dirs.root(),
"cp {} {} --no-clobber",
src.display(),
target.display()
);
let after_cp_hash = get_file_hash(target.display());
assert!(actual.err.contains("not replacing"));
// Check content was not clobbered
assert_eq!(after_cp_hash, target_hash);
});

View file

@ -27,7 +27,7 @@ fn url_join_with_only_user() {
"password": "",
"host": "localhost",
"port": "",
} | url join
} | url join
"#
));
@ -44,7 +44,7 @@ fn url_join_with_only_pwd() {
"password": "pwd",
"host": "localhost",
"port": "",
} | url join
} | url join
"#
));
@ -61,7 +61,7 @@ fn url_join_with_user_and_pwd() {
"password": "pwd",
"host": "localhost",
"port": "",
} | url join
} | url join
"#
));
@ -79,7 +79,7 @@ fn url_join_with_query() {
"host": "localhost",
"query": "par_1=aaa&par_2=bbb"
"port": "",
} | url join
} | url join
"#
));
@ -411,12 +411,9 @@ fn url_join_with_params_invalid_table() {
"host": "localhost",
"params": (
[
["key", "value"];
["par_1", "aaa"],
["par_2", "bbb"],
["par_1", "ccc"],
["par_2", "ddd"],
] ++ ["not a record"]
{ key: foo, value: bar }
"not a record"
]
),
"port": "1234",
} | 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
"#
));
assert_eq!(actual.out, "[1, 2]");
assert_eq!(actual.out, "[1, 2, 3]");
}
#[test]
@ -479,5 +479,5 @@ fn from_csv_test_flexible_missing_vals() {
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");
}
#[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 {
Assignment::Assign => None,
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::MultiplyAssign => Some(Operator::Math(Math::Multiply)),
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)?;
lhs.div(op_span, &rhs, op_span)?
}
Assignment::AppendAssign => {
Assignment::ConcatAssign => {
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 {
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::Multiply => lhs_val.mul(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 }
log = { workspace = true }
terminal_size = { workspace = true }
strip-ansi-escapes = { workspace = true }
crossterm = { workspace = true }
ratatui = { workspace = true }

View file

@ -9,6 +9,7 @@ mod views;
use anyhow::Result;
use commands::{ExpandCmd, HelpCmd, NuCmd, QuitCmd, TableCmd, TryCmd};
use crossterm::terminal::size;
pub use default_context::add_explore_context;
pub use explore::Explore;
use explore::ExploreConfig;
@ -19,7 +20,6 @@ use nu_protocol::{
};
use pager::{Page, Pager, PagerConfig};
use registry::CommandRegistry;
use terminal_size::{Height, Width};
use views::{BinaryView, Orientation, Preview, RecordView};
mod util {
@ -80,7 +80,7 @@ fn create_record_view(
}
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 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(
working_set: &mut StateWorkingSet,
spans: &[Span],
@ -800,54 +845,10 @@ pub fn parse_multispan_value(
arg
}
SyntaxShape::OneOf(shapes) => {
// handle for `if` command.
//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::OneOf(possible_shapes) => {
parse_oneof(working_set, spans, spans_idx, possible_shapes, true)
}
SyntaxShape::Expression => {
trace!("parsing: expression");
@ -3392,6 +3393,7 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
Arg,
AfterCommaArg,
Type,
AfterType,
DefaultValue,
}
@ -3425,7 +3427,9 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
let mut args: Vec<Arg> = vec![];
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 {
Token {
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
if contents == b":" {
match parse_mode {
ParseMode::Arg if last_token => working_set
.error(ParseError::Expected("type", Span::new(span.end, span.end))),
ParseMode::Arg => {
parse_mode = ParseMode::Type;
}
ParseMode::AfterCommaArg => {
ParseMode::AfterCommaArg | ParseMode::AfterType => {
working_set.error(ParseError::Expected("parameter or flag", span));
}
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
else if contents == b"=" {
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;
}
ParseMode::Type => {
working_set.error(ParseError::Expected("type", span));
}
ParseMode::AfterCommaArg => {
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
else if contents == b"," {
match parse_mode {
ParseMode::Arg => parse_mode = ParseMode::AfterCommaArg,
ParseMode::Arg | ParseMode::AfterType => {
parse_mode = ParseMode::AfterCommaArg
}
ParseMode::AfterCommaArg => {
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 {
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)
if contents.starts_with(b"--") && contents.len() > 2 {
// 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 => {
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::OneOf(possible_shapes) => {
for s in possible_shapes {
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)
parse_oneof(working_set, &[span], &mut 0, possible_shapes, false)
}
SyntaxShape::Any => {
@ -4895,7 +4887,7 @@ pub fn parse_assignment_operator(working_set: &mut StateWorkingSet, span: Span)
let operator = match contents {
b"=" => Operator::Assignment(Assignment::Assign),
b"+=" => Operator::Assignment(Assignment::PlusAssign),
b"++=" => Operator::Assignment(Assignment::AppendAssign),
b"++=" => Operator::Assignment(Assignment::ConcatAssign),
b"-=" => Operator::Assignment(Assignment::MinusAssign),
b"*=" => Operator::Assignment(Assignment::MultiplyAssign),
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"not-like" => Operator::Comparison(Comparison::NotRegexMatch),
b"+" => Operator::Math(Math::Plus),
b"++" => Operator::Math(Math::Append),
b"++" => Operator::Math(Math::Concat),
b"-" => Operator::Math(Math::Minus),
b"*" => Operator::Math(Math::Multiply),
b"/" => Operator::Math(Math::Divide),

View file

@ -29,8 +29,6 @@ pub fn type_compatible(lhs: &Type, rhs: &Type) -> bool {
match (lhs, rhs) {
(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)) => {
if matches!(**c, Type::Any) {
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) {
(Type::Int, Type::Int) => (Type::Int, None),
(Type::Float, Type::Int) => (Type::Float, None),
@ -935,8 +933,8 @@ pub fn math_result_type(
)
}
},
Operator::Assignment(Assignment::AppendAssign) => {
check_append(working_set, lhs, rhs, op)
Operator::Assignment(Assignment::ConcatAssign) => {
check_concat(working_set, lhs, rhs, op)
}
Operator::Assignment(_) => match (&lhs.ty, &rhs.ty) {
(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
}
fn check_append(
fn check_concat(
working_set: &mut StateWorkingSet,
lhs: &Expression,
rhs: &Expression,
@ -1099,23 +1097,17 @@ fn check_append(
(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::String, Type::String) => (Type::String, None),
(Type::Binary, Type::Binary) => (Type::Binary, 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);
(
Type::Any,
Some(ParseError::UnsupportedOperationRHS(
"append".into(),
"concatenation".into(),
op.span,
lhs.span,
lhs.ty.clone(),
@ -1129,7 +1121,7 @@ fn check_append(
(
Type::Any,
Some(ParseError::UnsupportedOperationLHS(
"append".into(),
"concatenation".into(),
op.span,
lhs.span,
lhs.ty.clone(),

View file

@ -1485,7 +1485,7 @@ fn prepare_plugin_call_custom_value_op() {
span,
},
CustomValueOp::Operation(
Operator::Math(Math::Append).into_spanned(span),
Operator::Math(Math::Concat).into_spanned(span),
cv_ok_val.clone(),
),
),
@ -1498,7 +1498,7 @@ fn prepare_plugin_call_custom_value_op() {
span,
},
CustomValueOp::Operation(
Operator::Math(Math::Append).into_spanned(span),
Operator::Math(Math::Concat).into_spanned(span),
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" }
log = { workspace = true }
thiserror = "1.0"
thiserror = "2.0"
[dev-dependencies]
serde = { workspace = true }

View file

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

View file

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

View file

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

View file

@ -724,7 +724,7 @@ impl<'a> StateWorkingSet<'a> {
pub fn find_commands_by_predicate(
&self,
predicate: impl Fn(&[u8]) -> bool,
mut predicate: impl FnMut(&[u8]) -> bool,
ignore_deprecated: bool,
) -> Vec<(Vec<u8>, Option<String>, CommandType)> {
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,
},
#[error("Deprecated: {old_command}")]
#[diagnostic(help("for more info see {url}"))]
#[error("{deprecated} is deprecated and will be removed in a future release")]
#[diagnostic()]
Deprecated {
old_command: String,
new_suggestion: String,
#[label("`{old_command}` is deprecated and will be removed in a future release. Please {new_suggestion} instead.")]
deprecated: &'static str,
suggestion: &'static str,
#[label("{deprecated} is deprecated. {suggestion}")]
span: Span,
url: String,
#[help]
help: Option<&'static str>,
},
/// Invalid glob pattern

View file

@ -238,7 +238,7 @@ pub trait Eval {
Math::Minus => lhs.sub(op_span, &rhs, expr_span),
Math::Multiply => lhs.mul(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::FloorDivision => lhs.floor_div(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
/// [`.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),
/// and a `ByteStream` with [unknown](crate::ByteStreamType::Unknown) type results in
/// [`any`](Type::Any) rather than [`string`](Type::String) or [`binary`](Type::Binary).
@ -117,7 +117,7 @@ impl PipelineData {
match self {
PipelineData::Empty => Type::Nothing,
PipelineData::Value(value, _) => value.get_type(),
PipelineData::ListStream(_, _) => Type::ListStream,
PipelineData::ListStream(_, _) => Type::list(Type::Any),
PipelineData::ByteStream(stream, _) => stream.type_().into(),
}
}
@ -203,7 +203,7 @@ impl PipelineData {
) -> Result<Self, ShellError> {
match stack.pipe_stdout().unwrap_or(&OutDest::Inherit) {
OutDest::Print => {
self.print(engine_state, stack, false, false)?;
self.print_table(engine_state, stack, false, false)?;
Ok(Self::Empty)
}
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.
/// `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,
engine_state: &EngineState,
stack: &mut Stack,

View file

@ -23,7 +23,6 @@ pub enum Type {
Float,
Int,
List(Box<Type>),
ListStream,
#[default]
Nothing,
Number,
@ -121,7 +120,6 @@ impl Type {
Type::Nothing => SyntaxShape::Nothing,
Type::Record(entries) => SyntaxShape::Record(mk_shape(entries)),
Type::Table(columns) => SyntaxShape::Table(mk_shape(columns)),
Type::ListStream => SyntaxShape::List(Box::new(SyntaxShape::Any)),
Type::Any => SyntaxShape::Any,
Type::Error => SyntaxShape::Any,
Type::Binary => SyntaxShape::Binary,
@ -151,7 +149,6 @@ impl Type {
Type::Nothing => String::from("nothing"),
Type::Number => String::from("number"),
Type::String => String::from("string"),
Type::ListStream => String::from("list-stream"),
Type::Any => String::from("any"),
Type::Error => String::from("error"),
Type::Binary => String::from("binary"),
@ -209,7 +206,6 @@ impl Display for Type {
Type::Nothing => write!(f, "nothing"),
Type::Number => write!(f, "number"),
Type::String => write!(f, "string"),
Type::ListStream => write!(f, "list-stream"),
Type::Any => write!(f, "any"),
Type::Error => write!(f, "error"),
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) {
(Value::List { vals: lhs, .. }, Value::List { vals: rhs, .. }) => {
let mut lhs = lhs.clone();
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))
Ok(Value::list([lhs.as_slice(), rhs.as_slice()].concat(), span))
}
(Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => {
Ok(Value::string(lhs.to_string() + rhs, span))
}
(Value::Binary { val: lhs, .. }, Value::Binary { val: rhs, .. }) => {
let mut val = lhs.clone();
val.extend(rhs);
Ok(Value::binary(val, span))
Ok(Value::string([lhs.as_str(), rhs.as_str()].join(""), 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) => {
lhs.operation(self.span(), Operator::Math(Math::Append), op, rhs)
lhs.operation(self.span(), Operator::Math(Math::Concat), op, rhs)
}
_ => Err(ShellError::OperatorMismatch {
op_span: op,

View file

@ -35,7 +35,7 @@ fn config_affected_when_mutated() {
#[test]
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#"$env.config.filesize.metric = true"#,
r#"20mib | into string"#]));
@ -45,7 +45,7 @@ fn config_affected_when_deep_mutated() {
#[test]
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#"$env.config.foo = 2"#,
r#";"#]));
@ -57,7 +57,7 @@ fn config_add_unsupported_key() {
#[test]
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#";"#]));
@ -66,7 +66,7 @@ fn config_add_unsupported_type() {
#[test]
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#";"#]));
@ -77,7 +77,7 @@ fn config_add_unsupported_value() {
#[test]
#[ignore = "Figure out how to make test_bins::nu_repl() continue execution after shell errors"]
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#"'foo' in $env.config"#]));
@ -87,7 +87,7 @@ fn config_unsupported_key_reverted() {
#[test]
#[ignore = "Figure out how to make test_bins::nu_repl() continue execution after shell errors"]
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 | describe"#]));
@ -97,7 +97,7 @@ fn config_unsupported_type_reverted() {
#[test]
#[ignore = "Figure out how to make test_bins::nu_repl() continue execution after errors"]
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 = ''"#,
r#"$env.config.history.file_format | to json"#]));

View file

@ -69,7 +69,7 @@ fn fancy_default_errors() {
assert_eq!(
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/util", include_str!("../std/util/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(..) {

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

@ -43,7 +43,7 @@ export def --env add [
}
# Make the next directory on the list the active directory.
# If the currenta ctive directory is the last in the list,
# If the current active directory is the last in the list,
# then cycle to the top of the list.
export def --env next [
N:int = 1 # number of positions to move.
@ -52,7 +52,7 @@ export def --env next [
}
# Make the previous directory on the list the active directory.
# If the current active directory is the first in the list,
# If the current active directory is the first in the list,
# then cycle to the end of the list.
export def --env prev [
N:int = 1 # number of positions to move.

View file

@ -28,3 +28,13 @@ export def "to ndjson" []: any -> string {
export def "to jsonl" []: any -> string {
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, +=, 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, *=, MultiplyAssign, "Multiplies 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, not, UnaryNot, "Negates a value or expression.", 0]
[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, *, Multiply, "Multiplies two values.", 95]
[Math, /, Divide, "Divides two values.", 95]
@ -684,8 +684,7 @@ def build-command-page [command: record] {
] | flatten | str join "\n"
}
# Show help on commands.
export def commands [
def scope-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
] {
@ -699,20 +698,35 @@ export def commands [
let found_command = ($commands | where name == $target_command)
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)
}
build-command-page ($found_command | get 0)
} else {
$commands | select name category description signatures search_terms
}
}
def external-commands [
...command: string@"nu-complete list-commands",
] {
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
}
}
def pretty-cmd [] {
let cmd = $in
$"(ansi default_dimmed)(ansi default_italic)($cmd)(ansi reset)"
@ -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 commands = (try { commands $target_item --find $find })
let commands = (try { scope-commands $target_item --find $find })
if not ($commands | is-empty) { return $commands }
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)"
return []
}
let span = (metadata $item | get span)
error make {
msg: ("std::help::item_not_found" | error-fmt)
label: {
text: "item not found"
span: $span
}
}
# use external tool (e.g: `man`) to search help for $target_item
# the stdout and stderr of external tool will follow `main` call.
external-commands $target_item
}

View file

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

View file

@ -2,15 +2,26 @@
use std/assert
use std/formats *
def test_data_multiline [] {
let lines = [
"{\"a\":1}",
"{\"a\":2}",
"{\"a\":3}",
"{\"a\":4}",
"{\"a\":5}",
"{\"a\":6}",
]
def test_data_multiline [--nuon] {
let lines = if $nuon {
[
"{a: 1}",
"{a: 2}",
"{a: 3}",
"{a: 4}",
"{a: 5}",
"{a: 6}",
]
} else {
[
"{\"a\":1}",
"{\"a\":2}",
"{\"a\":3}",
"{\"a\":4}",
"{\"a\":5}",
"{\"a\":6}",
]
}
if $nu.os-info.name == "windows" {
$lines | str join "\r\n"
@ -84,3 +95,36 @@ def to_jsonl_single_object [] {
let expect = "{\"a\":1}"
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,16 +1,27 @@
# Test std/formats when importing `use std *`
use std *
def test_data_multiline [] {
def test_data_multiline [--nuon] {
use std *
let lines = [
"{\"a\":1}",
"{\"a\":2}",
"{\"a\":3}",
"{\"a\":4}",
"{\"a\":5}",
"{\"a\":6}",
]
let lines = if $nuon {
[
"{a: 1}",
"{a: 2}",
"{a: 3}",
"{a: 4}",
"{a: 5}",
"{a: 6}",
]
} else {
[
"{\"a\":1}",
"{\"a\":2}",
"{\"a\":3}",
"{\"a\":4}",
"{\"a\":5}",
"{\"a\":6}",
]
}
if $nu.os-info.name == "windows" {
$lines | str join "\r\n"
@ -84,3 +95,36 @@ def to_jsonl_single_object [] {
let expect = "{\"a\":1}"
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,
mode: TableMode,
) -> NuTableConfig {
let with_footer = (config.table.footer_inheritance && out.with_footer)
|| with_footer(config, out.with_header, out.table.count_rows());
let mut count_rows = 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 {
theme: load_theme(mode),

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