mirror of
https://github.com/nushell/nushell
synced 2024-12-25 04:23:10 +00:00
Merge branch 'nushell:main' into main
This commit is contained in:
commit
8f298cd1ca
131 changed files with 5277 additions and 3470 deletions
2
.github/workflows/typos.yml
vendored
2
.github/workflows/typos.yml
vendored
|
@ -10,4 +10,4 @@ jobs:
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Check spelling
|
- name: Check spelling
|
||||||
uses: crate-ci/typos@v1.27.3
|
uses: crate-ci/typos@v1.28.1
|
||||||
|
|
1299
Cargo.lock
generated
1299
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
27
Cargo.toml
27
Cargo.toml
|
@ -71,7 +71,7 @@ brotli = "6.0"
|
||||||
byteorder = "1.5"
|
byteorder = "1.5"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
bytesize = "1.3"
|
bytesize = "1.3"
|
||||||
calamine = "0.24.0"
|
calamine = "0.26.1"
|
||||||
chardetng = "0.1.17"
|
chardetng = "0.1.17"
|
||||||
chrono = { default-features = false, version = "0.4.34" }
|
chrono = { default-features = false, version = "0.4.34" }
|
||||||
chrono-humanize = "0.2.3"
|
chrono-humanize = "0.2.3"
|
||||||
|
@ -106,7 +106,7 @@ lsp-server = "0.7.5"
|
||||||
lsp-types = { version = "0.95.0", features = ["proposed"] }
|
lsp-types = { version = "0.95.0", features = ["proposed"] }
|
||||||
mach2 = "0.4"
|
mach2 = "0.4"
|
||||||
md5 = { version = "0.10", package = "md-5" }
|
md5 = { version = "0.10", package = "md-5" }
|
||||||
miette = "7.2"
|
miette = "7.3"
|
||||||
mime = "0.3.17"
|
mime = "0.3.17"
|
||||||
mime_guess = "2.0"
|
mime_guess = "2.0"
|
||||||
mockito = { version = "1.6", default-features = false }
|
mockito = { version = "1.6", default-features = false }
|
||||||
|
@ -129,7 +129,7 @@ proc-macro-error = { version = "1.0", default-features = false }
|
||||||
proc-macro2 = "1.0"
|
proc-macro2 = "1.0"
|
||||||
procfs = "0.16.0"
|
procfs = "0.16.0"
|
||||||
pwd = "1.3"
|
pwd = "1.3"
|
||||||
quick-xml = "0.32.0"
|
quick-xml = "0.37.0"
|
||||||
quickcheck = "1.0"
|
quickcheck = "1.0"
|
||||||
quickcheck_macros = "1.0"
|
quickcheck_macros = "1.0"
|
||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
|
@ -156,22 +156,23 @@ syn = "2.0"
|
||||||
sysinfo = "0.32"
|
sysinfo = "0.32"
|
||||||
tabled = { version = "0.16.0", default-features = false }
|
tabled = { version = "0.16.0", default-features = false }
|
||||||
tempfile = "3.14"
|
tempfile = "3.14"
|
||||||
terminal_size = "0.3"
|
terminal_size = "0.4"
|
||||||
titlecase = "2.0"
|
titlecase = "2.0"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
trash = "5.2"
|
trash = "5.2"
|
||||||
umask = "2.1"
|
umask = "2.1"
|
||||||
unicode-segmentation = "1.12"
|
unicode-segmentation = "1.12"
|
||||||
unicode-width = "0.1"
|
unicode-width = "0.2"
|
||||||
ureq = { version = "2.10", default-features = false }
|
ureq = { version = "2.10", default-features = false }
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
uu_cp = "0.0.27"
|
uu_cp = "0.0.28"
|
||||||
uu_mkdir = "0.0.27"
|
uu_mkdir = "0.0.28"
|
||||||
uu_mktemp = "0.0.27"
|
uu_mktemp = "0.0.28"
|
||||||
uu_mv = "0.0.27"
|
uu_mv = "0.0.28"
|
||||||
uu_whoami = "0.0.27"
|
uu_touch = "0.0.28"
|
||||||
uu_uname = "0.0.27"
|
uu_whoami = "0.0.28"
|
||||||
uucore = "0.0.27"
|
uu_uname = "0.0.28"
|
||||||
|
uucore = "0.0.28"
|
||||||
uuid = "1.11.0"
|
uuid = "1.11.0"
|
||||||
v_htmlescape = "0.15.0"
|
v_htmlescape = "0.15.0"
|
||||||
wax = "0.6"
|
wax = "0.6"
|
||||||
|
@ -313,7 +314,7 @@ bench = false
|
||||||
# To use a development version of a dependency please use a global override here
|
# To use a development version of a dependency please use a global override here
|
||||||
# changing versions in each sub-crate of the workspace is tedious
|
# changing versions in each sub-crate of the workspace is tedious
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# reedline = { git = "https://github.com/nushell/reedline", branch = "main" }
|
reedline = { git = "https://github.com/nushell/reedline", branch = "main" }
|
||||||
# nu-ansi-term = {git = "https://github.com/nushell/nu-ansi-term.git", branch = "main"}
|
# nu-ansi-term = {git = "https://github.com/nushell/nu-ansi-term.git", branch = "main"}
|
||||||
|
|
||||||
# Run all benchmarks with `cargo bench`
|
# Run all benchmarks with `cargo bench`
|
||||||
|
|
|
@ -58,7 +58,7 @@ For details about which platforms the Nushell team actively supports, see [our p
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
The default configurations can be found at [sample_config](crates/nu-utils/src/sample_config)
|
The default configurations can be found at [sample_config](crates/nu-utils/src/default_files)
|
||||||
which are the configuration files one gets when they startup Nushell for the first time.
|
which are the configuration files one gets when they startup Nushell for the first time.
|
||||||
|
|
||||||
It sets all of the default configuration to run Nushell. From here one can
|
It sets all of the default configuration to run Nushell. From here one can
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
completions::{Completer, CompletionOptions, MatchAlgorithm},
|
completions::{Completer, CompletionOptions},
|
||||||
SuggestionKind,
|
SuggestionKind,
|
||||||
};
|
};
|
||||||
use nu_parser::FlatShape;
|
use nu_parser::FlatShape;
|
||||||
|
@ -9,7 +11,7 @@ use nu_protocol::{
|
||||||
};
|
};
|
||||||
use reedline::Suggestion;
|
use reedline::Suggestion;
|
||||||
|
|
||||||
use super::{completion_common::sort_suggestions, SemanticSuggestion};
|
use super::{completion_options::NuMatcher, SemanticSuggestion};
|
||||||
|
|
||||||
pub struct CommandCompletion {
|
pub struct CommandCompletion {
|
||||||
flattened: Vec<(Span, FlatShape)>,
|
flattened: Vec<(Span, FlatShape)>,
|
||||||
|
@ -33,10 +35,11 @@ impl CommandCompletion {
|
||||||
fn external_command_completion(
|
fn external_command_completion(
|
||||||
&self,
|
&self,
|
||||||
working_set: &StateWorkingSet,
|
working_set: &StateWorkingSet,
|
||||||
prefix: &str,
|
sugg_span: reedline::Span,
|
||||||
match_algorithm: MatchAlgorithm,
|
matched_internal: impl Fn(&str) -> bool,
|
||||||
) -> Vec<String> {
|
matcher: &mut NuMatcher<String>,
|
||||||
let mut executables = vec![];
|
) -> HashMap<String, SemanticSuggestion> {
|
||||||
|
let mut suggs = HashMap::new();
|
||||||
|
|
||||||
// os agnostic way to get the PATH env var
|
// os agnostic way to get the PATH env var
|
||||||
let paths = working_set.permanent_state.get_path_env_var();
|
let paths = working_set.permanent_state.get_path_env_var();
|
||||||
|
@ -54,24 +57,38 @@ impl CommandCompletion {
|
||||||
.completions
|
.completions
|
||||||
.external
|
.external
|
||||||
.max_results
|
.max_results
|
||||||
> executables.len() as i64
|
<= suggs.len() as i64
|
||||||
&& !executables.contains(
|
|
||||||
&item
|
|
||||||
.path()
|
|
||||||
.file_name()
|
|
||||||
.map(|x| x.to_string_lossy().to_string())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
)
|
|
||||||
&& matches!(
|
|
||||||
item.path().file_name().map(|x| match_algorithm
|
|
||||||
.matches_str(&x.to_string_lossy(), prefix)),
|
|
||||||
Some(true)
|
|
||||||
)
|
|
||||||
&& is_executable::is_executable(item.path())
|
|
||||||
{
|
{
|
||||||
if let Ok(name) = item.file_name().into_string() {
|
break;
|
||||||
executables.push(name);
|
}
|
||||||
}
|
let Ok(name) = item.file_name().into_string() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let value = if matched_internal(&name) {
|
||||||
|
format!("^{}", name)
|
||||||
|
} else {
|
||||||
|
name.clone()
|
||||||
|
};
|
||||||
|
if suggs.contains_key(&value) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if matcher.matches(&name) && is_executable::is_executable(item.path()) {
|
||||||
|
// If there's an internal command with the same name, adds ^cmd to the
|
||||||
|
// matcher so that both the internal and external command are included
|
||||||
|
matcher.add(&name, value.clone());
|
||||||
|
suggs.insert(
|
||||||
|
value.clone(),
|
||||||
|
SemanticSuggestion {
|
||||||
|
suggestion: Suggestion {
|
||||||
|
value,
|
||||||
|
span: sugg_span,
|
||||||
|
append_whitespace: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
// TODO: is there a way to create a test?
|
||||||
|
kind: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,7 +96,7 @@ impl CommandCompletion {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
executables
|
suggs
|
||||||
}
|
}
|
||||||
|
|
||||||
fn complete_commands(
|
fn complete_commands(
|
||||||
|
@ -88,68 +105,59 @@ impl CommandCompletion {
|
||||||
span: Span,
|
span: Span,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
find_externals: bool,
|
find_externals: bool,
|
||||||
match_algorithm: MatchAlgorithm,
|
options: &CompletionOptions,
|
||||||
) -> Vec<SemanticSuggestion> {
|
) -> Vec<SemanticSuggestion> {
|
||||||
let partial = working_set.get_span_contents(span);
|
let partial = working_set.get_span_contents(span);
|
||||||
|
let mut matcher = NuMatcher::new(String::from_utf8_lossy(partial), options.clone());
|
||||||
|
|
||||||
let filter_predicate = |command: &[u8]| match_algorithm.matches_u8(command, partial);
|
let sugg_span = reedline::Span::new(span.start - offset, span.end - offset);
|
||||||
|
|
||||||
let mut results = working_set
|
let mut internal_suggs = HashMap::new();
|
||||||
.find_commands_by_predicate(filter_predicate, true)
|
let filtered_commands = working_set.find_commands_by_predicate(
|
||||||
.into_iter()
|
|name| {
|
||||||
.map(move |x| SemanticSuggestion {
|
let name = String::from_utf8_lossy(name);
|
||||||
suggestion: Suggestion {
|
matcher.add(&name, name.to_string())
|
||||||
value: String::from_utf8_lossy(&x.0).to_string(),
|
},
|
||||||
description: x.1,
|
true,
|
||||||
span: reedline::Span::new(span.start - offset, span.end - offset),
|
);
|
||||||
append_whitespace: true,
|
for (name, description, typ) in filtered_commands {
|
||||||
..Suggestion::default()
|
let name = String::from_utf8_lossy(&name);
|
||||||
},
|
internal_suggs.insert(
|
||||||
kind: Some(SuggestionKind::Command(x.2)),
|
name.to_string(),
|
||||||
})
|
SemanticSuggestion {
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let partial = working_set.get_span_contents(span);
|
|
||||||
let partial = String::from_utf8_lossy(partial).to_string();
|
|
||||||
|
|
||||||
if find_externals {
|
|
||||||
let results_external = self
|
|
||||||
.external_command_completion(working_set, &partial, match_algorithm)
|
|
||||||
.into_iter()
|
|
||||||
.map(move |x| SemanticSuggestion {
|
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
value: x,
|
value: name.to_string(),
|
||||||
span: reedline::Span::new(span.start - offset, span.end - offset),
|
description,
|
||||||
|
span: sugg_span,
|
||||||
append_whitespace: true,
|
append_whitespace: true,
|
||||||
..Suggestion::default()
|
..Suggestion::default()
|
||||||
},
|
},
|
||||||
// TODO: is there a way to create a test?
|
kind: Some(SuggestionKind::Command(typ)),
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
&mut self,
|
||||||
working_set: &StateWorkingSet,
|
working_set: &StateWorkingSet,
|
||||||
_stack: &Stack,
|
_stack: &Stack,
|
||||||
prefix: &[u8],
|
_prefix: &[u8],
|
||||||
span: Span,
|
span: Span,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
pos: usize,
|
pos: usize,
|
||||||
|
@ -188,18 +196,18 @@ impl Completer for CommandCompletion {
|
||||||
Span::new(last.0.start, pos),
|
Span::new(last.0.start, pos),
|
||||||
offset,
|
offset,
|
||||||
false,
|
false,
|
||||||
options.match_algorithm,
|
options,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
};
|
};
|
||||||
|
|
||||||
if !subcommands.is_empty() {
|
if !subcommands.is_empty() {
|
||||||
return sort_suggestions(&String::from_utf8_lossy(prefix), subcommands, options);
|
return subcommands;
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = working_set.get_config();
|
let config = working_set.get_config();
|
||||||
let commands = if matches!(self.flat_shape, nu_parser::FlatShape::External)
|
if matches!(self.flat_shape, nu_parser::FlatShape::External)
|
||||||
|| matches!(self.flat_shape, nu_parser::FlatShape::InternalCall(_))
|
|| matches!(self.flat_shape, nu_parser::FlatShape::InternalCall(_))
|
||||||
|| ((span.end - span.start) == 0)
|
|| ((span.end - span.start) == 0)
|
||||||
|| is_passthrough_command(working_set.delta.get_file_contents())
|
|| is_passthrough_command(working_set.delta.get_file_contents())
|
||||||
|
@ -214,13 +222,11 @@ impl Completer for CommandCompletion {
|
||||||
span,
|
span,
|
||||||
offset,
|
offset,
|
||||||
config.completions.external.enable,
|
config.completions.external.enable,
|
||||||
options.match_algorithm,
|
options,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
};
|
}
|
||||||
|
|
||||||
sort_suggestions(&String::from_utf8_lossy(prefix), commands, options)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,20 @@
|
||||||
use super::MatchAlgorithm;
|
use super::{completion_options::NuMatcher, MatchAlgorithm};
|
||||||
use crate::{
|
use crate::completions::CompletionOptions;
|
||||||
completions::{matches, CompletionOptions},
|
|
||||||
SemanticSuggestion,
|
|
||||||
};
|
|
||||||
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
|
|
||||||
use nu_ansi_term::Style;
|
use nu_ansi_term::Style;
|
||||||
use nu_engine::env_to_string;
|
use nu_engine::env_to_string;
|
||||||
use nu_path::dots::expand_ndots;
|
use nu_path::dots::expand_ndots;
|
||||||
use nu_path::{expand_to_real_path, home_dir};
|
use nu_path::{expand_to_real_path, home_dir};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{EngineState, Stack, StateWorkingSet},
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
CompletionSort, Span,
|
Span,
|
||||||
};
|
};
|
||||||
use nu_utils::get_ls_colors;
|
use nu_utils::get_ls_colors;
|
||||||
|
use nu_utils::IgnoreCaseExt;
|
||||||
use std::path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP};
|
use std::path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP};
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct PathBuiltFromString {
|
pub struct PathBuiltFromString {
|
||||||
|
cwd: PathBuf,
|
||||||
parts: Vec<String>,
|
parts: Vec<String>,
|
||||||
isdir: bool,
|
isdir: bool,
|
||||||
}
|
}
|
||||||
|
@ -30,76 +28,84 @@ pub struct PathBuiltFromString {
|
||||||
/// want_directory: Whether we want only directories as completion matches.
|
/// want_directory: Whether we want only directories as completion matches.
|
||||||
/// Some commands like `cd` can only be run on directories whereas others
|
/// Some commands like `cd` can only be run on directories whereas others
|
||||||
/// like `ls` can be run on regular files as well.
|
/// like `ls` can be run on regular files as well.
|
||||||
pub fn complete_rec(
|
fn complete_rec(
|
||||||
partial: &[&str],
|
partial: &[&str],
|
||||||
built: &PathBuiltFromString,
|
built_paths: &[PathBuiltFromString],
|
||||||
cwd: &Path,
|
|
||||||
options: &CompletionOptions,
|
options: &CompletionOptions,
|
||||||
want_directory: bool,
|
want_directory: bool,
|
||||||
isdir: bool,
|
isdir: bool,
|
||||||
) -> Vec<PathBuiltFromString> {
|
) -> Vec<PathBuiltFromString> {
|
||||||
let mut completions = vec![];
|
|
||||||
|
|
||||||
if let Some((&base, rest)) = partial.split_first() {
|
if let Some((&base, rest)) = partial.split_first() {
|
||||||
if base.chars().all(|c| c == '.') && (isdir || !rest.is_empty()) {
|
if base.chars().all(|c| c == '.') && (isdir || !rest.is_empty()) {
|
||||||
let mut built = built.clone();
|
let built_paths: Vec<_> = built_paths
|
||||||
built.parts.push(base.to_string());
|
.iter()
|
||||||
built.isdir = true;
|
.map(|built| {
|
||||||
return complete_rec(rest, &built, cwd, options, want_directory, isdir);
|
let mut built = built.clone();
|
||||||
}
|
built.parts.push(base.to_string());
|
||||||
}
|
built.isdir = true;
|
||||||
|
built
|
||||||
let mut built_path = cwd.to_path_buf();
|
})
|
||||||
for part in &built.parts {
|
.collect();
|
||||||
built_path.push(part);
|
return complete_rec(rest, &built_paths, options, want_directory, isdir);
|
||||||
}
|
|
||||||
|
|
||||||
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 prefix = partial.first().unwrap_or(&"");
|
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() {
|
match partial.split_first() {
|
||||||
Some((base, rest)) => {
|
Some((base, rest)) => {
|
||||||
if matches(base, &entry_name, options) {
|
// We use `isdir` to confirm that the current component has
|
||||||
// We use `isdir` to confirm that the current component has
|
// at least one next component or a slash.
|
||||||
// at least one next component or a slash.
|
// Serves as confirmation to ignore longer completions for
|
||||||
// Serves as confirmation to ignore longer completions for
|
// components in between.
|
||||||
// components in between.
|
if !rest.is_empty() || isdir {
|
||||||
if !rest.is_empty() || isdir {
|
completions.extend(complete_rec(
|
||||||
completions.extend(complete_rec(
|
rest,
|
||||||
rest,
|
&[built],
|
||||||
&built,
|
options,
|
||||||
cwd,
|
want_directory,
|
||||||
options,
|
isdir,
|
||||||
want_directory,
|
));
|
||||||
isdir,
|
} else {
|
||||||
));
|
completions.push(built);
|
||||||
} else {
|
|
||||||
completions.push(built);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if entry_name.eq(base)
|
|
||||||
&& matches!(options.match_algorithm, MatchAlgorithm::Prefix)
|
// For https://github.com/nushell/nushell/issues/13204
|
||||||
&& isdir
|
if isdir && options.match_algorithm == MatchAlgorithm::Prefix {
|
||||||
{
|
let exact_match = if options.case_sensitive {
|
||||||
break;
|
entry_name.eq(base)
|
||||||
|
} else {
|
||||||
|
entry_name.to_folded_case().eq(&base.to_folded_case())
|
||||||
|
};
|
||||||
|
if exact_match {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
@ -147,15 +153,25 @@ fn surround_remove(partial: &str) -> String {
|
||||||
partial.to_string()
|
partial.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct FileSuggestion {
|
||||||
|
pub span: nu_protocol::Span,
|
||||||
|
pub path: String,
|
||||||
|
pub style: Option<Style>,
|
||||||
|
pub cwd: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Parameters
|
||||||
|
/// * `cwds` - A list of directories in which to search. The only reason this isn't a single string
|
||||||
|
/// is because dotnu_completions searches in multiple directories at once
|
||||||
pub fn complete_item(
|
pub fn complete_item(
|
||||||
want_directory: bool,
|
want_directory: bool,
|
||||||
span: nu_protocol::Span,
|
span: nu_protocol::Span,
|
||||||
partial: &str,
|
partial: &str,
|
||||||
cwd: &str,
|
cwds: &[impl AsRef<str>],
|
||||||
options: &CompletionOptions,
|
options: &CompletionOptions,
|
||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
stack: &Stack,
|
stack: &Stack,
|
||||||
) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
|
) -> Vec<FileSuggestion> {
|
||||||
let cleaned_partial = surround_remove(partial);
|
let cleaned_partial = surround_remove(partial);
|
||||||
let isdir = cleaned_partial.ends_with(is_separator);
|
let isdir = cleaned_partial.ends_with(is_separator);
|
||||||
let expanded_partial = expand_ndots(Path::new(&cleaned_partial));
|
let expanded_partial = expand_ndots(Path::new(&cleaned_partial));
|
||||||
|
@ -175,7 +191,10 @@ pub fn complete_item(
|
||||||
partial.push_str(&format!("{path_separator}."));
|
partial.push_str(&format!("{path_separator}."));
|
||||||
}
|
}
|
||||||
|
|
||||||
let cwd_pathbuf = Path::new(cwd).to_path_buf();
|
let cwd_pathbufs: Vec<_> = cwds
|
||||||
|
.iter()
|
||||||
|
.map(|cwd| Path::new(cwd.as_ref()).to_path_buf())
|
||||||
|
.collect();
|
||||||
let ls_colors = (engine_state.config.completions.use_ls_colors
|
let ls_colors = (engine_state.config.completions.use_ls_colors
|
||||||
&& engine_state.config.use_ansi_coloring)
|
&& engine_state.config.use_ansi_coloring)
|
||||||
.then(|| {
|
.then(|| {
|
||||||
|
@ -186,7 +205,7 @@ pub fn complete_item(
|
||||||
get_ls_colors(ls_colors_env_str)
|
get_ls_colors(ls_colors_env_str)
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut cwd = cwd_pathbuf.clone();
|
let mut cwds = cwd_pathbufs.clone();
|
||||||
let mut prefix_len = 0;
|
let mut prefix_len = 0;
|
||||||
let mut original_cwd = OriginalCwd::None;
|
let mut original_cwd = OriginalCwd::None;
|
||||||
|
|
||||||
|
@ -194,19 +213,21 @@ pub fn complete_item(
|
||||||
match components.peek().cloned() {
|
match components.peek().cloned() {
|
||||||
Some(c @ Component::Prefix(..)) => {
|
Some(c @ Component::Prefix(..)) => {
|
||||||
// windows only by definition
|
// windows only by definition
|
||||||
cwd = [c, Component::RootDir].iter().collect();
|
cwds = vec![[c, Component::RootDir].iter().collect()];
|
||||||
prefix_len = c.as_os_str().len();
|
prefix_len = c.as_os_str().len();
|
||||||
original_cwd = OriginalCwd::Prefix(c.as_os_str().to_string_lossy().into_owned());
|
original_cwd = OriginalCwd::Prefix(c.as_os_str().to_string_lossy().into_owned());
|
||||||
}
|
}
|
||||||
Some(c @ Component::RootDir) => {
|
Some(c @ Component::RootDir) => {
|
||||||
// This is kind of a hack. When joining an empty string with the rest,
|
// This is kind of a hack. When joining an empty string with the rest,
|
||||||
// we add the slash automagically
|
// we add the slash automagically
|
||||||
cwd = PathBuf::from(c.as_os_str());
|
cwds = vec![PathBuf::from(c.as_os_str())];
|
||||||
prefix_len = 1;
|
prefix_len = 1;
|
||||||
original_cwd = OriginalCwd::Prefix(String::new());
|
original_cwd = OriginalCwd::Prefix(String::new());
|
||||||
}
|
}
|
||||||
Some(Component::Normal(home)) if home.to_string_lossy() == "~" => {
|
Some(Component::Normal(home)) if home.to_string_lossy() == "~" => {
|
||||||
cwd = home_dir().map(Into::into).unwrap_or(cwd_pathbuf);
|
cwds = home_dir()
|
||||||
|
.map(|dir| vec![dir.into()])
|
||||||
|
.unwrap_or(cwd_pathbufs);
|
||||||
prefix_len = 1;
|
prefix_len = 1;
|
||||||
original_cwd = OriginalCwd::Home;
|
original_cwd = OriginalCwd::Home;
|
||||||
}
|
}
|
||||||
|
@ -223,8 +244,14 @@ pub fn complete_item(
|
||||||
|
|
||||||
complete_rec(
|
complete_rec(
|
||||||
partial.as_slice(),
|
partial.as_slice(),
|
||||||
&PathBuiltFromString::default(),
|
&cwds
|
||||||
&cwd,
|
.into_iter()
|
||||||
|
.map(|cwd| PathBuiltFromString {
|
||||||
|
cwd,
|
||||||
|
parts: Vec::new(),
|
||||||
|
isdir: false,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
options,
|
options,
|
||||||
want_directory,
|
want_directory,
|
||||||
isdir,
|
isdir,
|
||||||
|
@ -234,6 +261,7 @@ pub fn complete_item(
|
||||||
if should_collapse_dots {
|
if should_collapse_dots {
|
||||||
p = collapse_ndots(p);
|
p = collapse_ndots(p);
|
||||||
}
|
}
|
||||||
|
let cwd = p.cwd.clone();
|
||||||
let path = original_cwd.apply(p, path_separator);
|
let path = original_cwd.apply(p, path_separator);
|
||||||
let style = ls_colors.as_ref().map(|lsc| {
|
let style = ls_colors.as_ref().map(|lsc| {
|
||||||
lsc.style_for_path_with_metadata(
|
lsc.style_for_path_with_metadata(
|
||||||
|
@ -245,7 +273,12 @@ pub fn complete_item(
|
||||||
.map(lscolors::Style::to_nu_ansi_term_style)
|
.map(lscolors::Style::to_nu_ansi_term_style)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
});
|
});
|
||||||
(span, escape_path(path, want_directory), style)
|
FileSuggestion {
|
||||||
|
span,
|
||||||
|
path: escape_path(path, want_directory),
|
||||||
|
style,
|
||||||
|
cwd,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
@ -310,45 +343,6 @@ pub fn adjust_if_intermediate(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience function to sort suggestions using [`sort_completions`]
|
|
||||||
pub fn sort_suggestions(
|
|
||||||
prefix: &str,
|
|
||||||
items: Vec<SemanticSuggestion>,
|
|
||||||
options: &CompletionOptions,
|
|
||||||
) -> Vec<SemanticSuggestion> {
|
|
||||||
sort_completions(prefix, items, options, |it| &it.suggestion.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # Arguments
|
|
||||||
/// * `prefix` - What the user's typed, for sorting by fuzzy matcher score
|
|
||||||
pub fn sort_completions<T>(
|
|
||||||
prefix: &str,
|
|
||||||
mut items: Vec<T>,
|
|
||||||
options: &CompletionOptions,
|
|
||||||
get_value: fn(&T) -> &str,
|
|
||||||
) -> Vec<T> {
|
|
||||||
// Sort items
|
|
||||||
if options.sort == CompletionSort::Smart && options.match_algorithm == MatchAlgorithm::Fuzzy {
|
|
||||||
let mut matcher = SkimMatcherV2::default();
|
|
||||||
if options.case_sensitive {
|
|
||||||
matcher = matcher.respect_case();
|
|
||||||
} else {
|
|
||||||
matcher = matcher.ignore_case();
|
|
||||||
};
|
|
||||||
items.sort_unstable_by(|a, b| {
|
|
||||||
let a_str = get_value(a);
|
|
||||||
let b_str = get_value(b);
|
|
||||||
let a_score = matcher.fuzzy_match(a_str, prefix).unwrap_or_default();
|
|
||||||
let b_score = matcher.fuzzy_match(b_str, prefix).unwrap_or_default();
|
|
||||||
b_score.cmp(&a_score).then(a_str.cmp(b_str))
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
items.sort_unstable_by(|a, b| get_value(a).cmp(get_value(b)));
|
|
||||||
}
|
|
||||||
|
|
||||||
items
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collapse multiple ".." components into n-dots.
|
/// Collapse multiple ".." components into n-dots.
|
||||||
///
|
///
|
||||||
/// It performs the reverse operation of `expand_ndots`, collapsing sequences of ".." into n-dots,
|
/// It performs the reverse operation of `expand_ndots`, collapsing sequences of ".." into n-dots,
|
||||||
|
@ -359,6 +353,7 @@ fn collapse_ndots(path: PathBuiltFromString) -> PathBuiltFromString {
|
||||||
let mut result = PathBuiltFromString {
|
let mut result = PathBuiltFromString {
|
||||||
parts: Vec::with_capacity(path.parts.len()),
|
parts: Vec::with_capacity(path.parts.len()),
|
||||||
isdir: path.isdir,
|
isdir: path.isdir,
|
||||||
|
cwd: path.cwd,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut dot_count = 0;
|
let mut dot_count = 0;
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
|
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
|
||||||
use nu_parser::trim_quotes_str;
|
use nu_parser::trim_quotes_str;
|
||||||
use nu_protocol::{CompletionAlgorithm, CompletionSort};
|
use nu_protocol::{CompletionAlgorithm, CompletionSort};
|
||||||
use std::fmt::Display;
|
use nu_utils::IgnoreCaseExt;
|
||||||
|
use std::{borrow::Cow, fmt::Display};
|
||||||
|
|
||||||
|
use super::SemanticSuggestion;
|
||||||
|
|
||||||
/// Describes how suggestions should be matched.
|
/// Describes how suggestions should be matched.
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
|
@ -19,33 +22,154 @@ pub enum MatchAlgorithm {
|
||||||
Fuzzy,
|
Fuzzy,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MatchAlgorithm {
|
pub struct NuMatcher<T> {
|
||||||
/// Returns whether the `needle` search text matches the given `haystack`.
|
options: CompletionOptions,
|
||||||
pub fn matches_str(&self, haystack: &str, needle: &str) -> bool {
|
needle: String,
|
||||||
let haystack = trim_quotes_str(haystack);
|
state: State<T>,
|
||||||
let needle = trim_quotes_str(needle);
|
}
|
||||||
match *self {
|
|
||||||
MatchAlgorithm::Prefix => haystack.starts_with(needle),
|
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 => {
|
MatchAlgorithm::Fuzzy => {
|
||||||
let matcher = SkimMatcherV2::default();
|
let mut matcher = SkimMatcherV2::default();
|
||||||
matcher.fuzzy_match(haystack, needle).is_some()
|
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`.
|
/// Returns whether or not the haystack matches the needle. If it does, `item` is added
|
||||||
pub fn matches_u8(&self, haystack: &[u8], needle: &[u8]) -> bool {
|
/// to the list of matches (if given).
|
||||||
match *self {
|
///
|
||||||
MatchAlgorithm::Prefix => haystack.starts_with(needle),
|
/// Helper to avoid code duplication between [NuMatcher::add] and [NuMatcher::matches].
|
||||||
MatchAlgorithm::Fuzzy => {
|
fn matches_aux(&mut self, haystack: &str, item: Option<T>) -> bool {
|
||||||
let haystack_str = String::from_utf8_lossy(haystack);
|
let haystack = trim_quotes_str(haystack);
|
||||||
let needle_str = String::from_utf8_lossy(needle);
|
match &mut self.state {
|
||||||
|
State::Prefix { items } => {
|
||||||
let matcher = SkimMatcherV2::default();
|
let haystack_folded = if self.options.case_sensitive {
|
||||||
matcher.fuzzy_match(&haystack_str, &needle_str).is_some()
|
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 {
|
impl From<CompletionAlgorithm> for MatchAlgorithm {
|
||||||
|
@ -105,35 +229,49 @@ impl Default for CompletionOptions {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::MatchAlgorithm;
|
use rstest::rstest;
|
||||||
|
|
||||||
#[test]
|
use super::{CompletionOptions, MatchAlgorithm, NuMatcher};
|
||||||
fn match_algorithm_prefix() {
|
|
||||||
let algorithm = MatchAlgorithm::Prefix;
|
|
||||||
|
|
||||||
assert!(algorithm.matches_str("example text", ""));
|
#[rstest]
|
||||||
assert!(algorithm.matches_str("example text", "examp"));
|
#[case(MatchAlgorithm::Prefix, "example text", "", true)]
|
||||||
assert!(!algorithm.matches_str("example text", "text"));
|
#[case(MatchAlgorithm::Prefix, "example text", "examp", true)]
|
||||||
|
#[case(MatchAlgorithm::Prefix, "example text", "text", false)]
|
||||||
assert!(algorithm.matches_u8(&[1, 2, 3], &[]));
|
#[case(MatchAlgorithm::Fuzzy, "example text", "", true)]
|
||||||
assert!(algorithm.matches_u8(&[1, 2, 3], &[1, 2]));
|
#[case(MatchAlgorithm::Fuzzy, "example text", "examp", true)]
|
||||||
assert!(!algorithm.matches_u8(&[1, 2, 3], &[2, 3]));
|
#[case(MatchAlgorithm::Fuzzy, "example text", "ext", true)]
|
||||||
|
#[case(MatchAlgorithm::Fuzzy, "example text", "mplxt", true)]
|
||||||
|
#[case(MatchAlgorithm::Fuzzy, "example text", "mpp", false)]
|
||||||
|
fn match_algorithm_simple(
|
||||||
|
#[case] match_algorithm: MatchAlgorithm,
|
||||||
|
#[case] haystack: &str,
|
||||||
|
#[case] needle: &str,
|
||||||
|
#[case] should_match: bool,
|
||||||
|
) {
|
||||||
|
let options = CompletionOptions {
|
||||||
|
match_algorithm,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut matcher = NuMatcher::new(needle, options);
|
||||||
|
matcher.add(haystack, haystack);
|
||||||
|
if should_match {
|
||||||
|
assert_eq!(vec![haystack], matcher.results());
|
||||||
|
} else {
|
||||||
|
assert_ne!(vec![haystack], matcher.results());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn match_algorithm_fuzzy() {
|
fn match_algorithm_fuzzy_sort_score() {
|
||||||
let algorithm = MatchAlgorithm::Fuzzy;
|
let options = CompletionOptions {
|
||||||
|
match_algorithm: MatchAlgorithm::Fuzzy,
|
||||||
assert!(algorithm.matches_str("example text", ""));
|
..Default::default()
|
||||||
assert!(algorithm.matches_str("example text", "examp"));
|
};
|
||||||
assert!(algorithm.matches_str("example text", "ext"));
|
let mut matcher = NuMatcher::new("fob", options);
|
||||||
assert!(algorithm.matches_str("example text", "mplxt"));
|
for item in ["foo/bar", "fob", "foo bar"] {
|
||||||
assert!(!algorithm.matches_str("example text", "mpp"));
|
matcher.add(item, item);
|
||||||
|
}
|
||||||
assert!(algorithm.matches_u8(&[1, 2, 3], &[]));
|
// Sort by score, then in alphabetical order
|
||||||
assert!(algorithm.matches_u8(&[1, 2, 3], &[1, 2]));
|
assert_eq!(vec!["fob", "foo bar", "foo/bar"], matcher.results());
|
||||||
assert!(algorithm.matches_u8(&[1, 2, 3], &[2, 3]));
|
|
||||||
assert!(algorithm.matches_u8(&[1, 2, 3], &[1, 3]));
|
|
||||||
assert!(!algorithm.matches_u8(&[1, 2, 3], &[2, 2]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,9 @@ use nu_protocol::{
|
||||||
engine::{Stack, StateWorkingSet},
|
engine::{Stack, StateWorkingSet},
|
||||||
CompletionSort, DeclId, PipelineData, Span, Type, Value,
|
CompletionSort, DeclId, PipelineData, Span, Type, Value,
|
||||||
};
|
};
|
||||||
use nu_utils::IgnoreCaseExt;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use super::completion_common::sort_suggestions;
|
use super::completion_options::NuMatcher;
|
||||||
|
|
||||||
pub struct CustomCompletion {
|
pub struct CustomCompletion {
|
||||||
stack: Stack,
|
stack: Stack,
|
||||||
|
@ -123,41 +122,11 @@ impl Completer for CustomCompletion {
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let options = custom_completion_options
|
let options = custom_completion_options.unwrap_or(completion_options.clone());
|
||||||
.as_ref()
|
let mut matcher = NuMatcher::new(String::from_utf8_lossy(prefix), options);
|
||||||
.unwrap_or(completion_options);
|
for sugg in suggestions {
|
||||||
let suggestions = filter(prefix, suggestions, options);
|
matcher.add_semantic_suggestion(sugg);
|
||||||
sort_suggestions(&String::from_utf8_lossy(prefix), suggestions, options)
|
}
|
||||||
|
matcher.results()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn filter(
|
|
||||||
prefix: &[u8],
|
|
||||||
items: Vec<SemanticSuggestion>,
|
|
||||||
options: &CompletionOptions,
|
|
||||||
) -> Vec<SemanticSuggestion> {
|
|
||||||
items
|
|
||||||
.into_iter()
|
|
||||||
.filter(|it| match options.match_algorithm {
|
|
||||||
MatchAlgorithm::Prefix => match (options.case_sensitive, options.positional) {
|
|
||||||
(true, true) => it.suggestion.value.as_bytes().starts_with(prefix),
|
|
||||||
(true, false) => it
|
|
||||||
.suggestion
|
|
||||||
.value
|
|
||||||
.contains(std::str::from_utf8(prefix).unwrap_or("")),
|
|
||||||
(false, positional) => {
|
|
||||||
let value = it.suggestion.value.to_folded_case();
|
|
||||||
let prefix = std::str::from_utf8(prefix).unwrap_or("").to_folded_case();
|
|
||||||
if positional {
|
|
||||||
value.starts_with(&prefix)
|
|
||||||
} else {
|
|
||||||
value.contains(&prefix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
MatchAlgorithm::Fuzzy => options
|
|
||||||
.match_algorithm
|
|
||||||
.matches_u8(it.suggestion.value.as_bytes(), prefix),
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ use crate::completions::{
|
||||||
completion_common::{adjust_if_intermediate, complete_item, AdjustView},
|
completion_common::{adjust_if_intermediate, complete_item, AdjustView},
|
||||||
Completer, CompletionOptions,
|
Completer, CompletionOptions,
|
||||||
};
|
};
|
||||||
use nu_ansi_term::Style;
|
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{EngineState, Stack, StateWorkingSet},
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
Span,
|
Span,
|
||||||
|
@ -10,7 +9,7 @@ use nu_protocol::{
|
||||||
use reedline::Suggestion;
|
use reedline::Suggestion;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use super::SemanticSuggestion;
|
use super::{completion_common::FileSuggestion, SemanticSuggestion};
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct DirectoryCompletion {}
|
pub struct DirectoryCompletion {}
|
||||||
|
@ -47,11 +46,11 @@ impl Completer for DirectoryCompletion {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(move |x| SemanticSuggestion {
|
.map(move |x| SemanticSuggestion {
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
value: x.1,
|
value: x.path,
|
||||||
style: x.2,
|
style: x.style,
|
||||||
span: reedline::Span {
|
span: reedline::Span {
|
||||||
start: x.0.start - offset,
|
start: x.span.start - offset,
|
||||||
end: x.0.end - offset,
|
end: x.span.end - offset,
|
||||||
},
|
},
|
||||||
..Suggestion::default()
|
..Suggestion::default()
|
||||||
},
|
},
|
||||||
|
@ -92,6 +91,6 @@ pub fn directory_completion(
|
||||||
options: &CompletionOptions,
|
options: &CompletionOptions,
|
||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
stack: &Stack,
|
stack: &Stack,
|
||||||
) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
|
) -> Vec<FileSuggestion> {
|
||||||
complete_item(true, span, partial, cwd, options, engine_state, stack)
|
complete_item(true, span, partial, &[cwd], options, engine_state, stack)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ use nu_protocol::{
|
||||||
use reedline::Suggestion;
|
use reedline::Suggestion;
|
||||||
use std::path::{is_separator, Path, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR};
|
use std::path::{is_separator, Path, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR};
|
||||||
|
|
||||||
use super::{completion_common::sort_suggestions, SemanticSuggestion};
|
use super::SemanticSuggestion;
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct DotNuCompletion {}
|
pub struct DotNuCompletion {}
|
||||||
|
@ -87,49 +87,44 @@ impl Completer for DotNuCompletion {
|
||||||
|
|
||||||
// Fetch the files filtering the ones that ends with .nu
|
// Fetch the files filtering the ones that ends with .nu
|
||||||
// and transform them into suggestions
|
// and transform them into suggestions
|
||||||
let output: Vec<SemanticSuggestion> = search_dirs
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|search_dir| {
|
|
||||||
let completions = file_path_completion(
|
|
||||||
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<_>>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,16 +2,14 @@ use crate::completions::{
|
||||||
completion_common::{adjust_if_intermediate, complete_item, AdjustView},
|
completion_common::{adjust_if_intermediate, complete_item, AdjustView},
|
||||||
Completer, CompletionOptions,
|
Completer, CompletionOptions,
|
||||||
};
|
};
|
||||||
use nu_ansi_term::Style;
|
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{EngineState, Stack, StateWorkingSet},
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
Span,
|
Span,
|
||||||
};
|
};
|
||||||
use nu_utils::IgnoreCaseExt;
|
|
||||||
use reedline::Suggestion;
|
use reedline::Suggestion;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use super::SemanticSuggestion;
|
use super::{completion_common::FileSuggestion, SemanticSuggestion};
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct FileCompletion {}
|
pub struct FileCompletion {}
|
||||||
|
@ -44,7 +42,7 @@ impl Completer for FileCompletion {
|
||||||
readjusted,
|
readjusted,
|
||||||
span,
|
span,
|
||||||
&prefix,
|
&prefix,
|
||||||
&working_set.permanent_state.current_work_dir(),
|
&[&working_set.permanent_state.current_work_dir()],
|
||||||
options,
|
options,
|
||||||
working_set.permanent_state,
|
working_set.permanent_state,
|
||||||
stack,
|
stack,
|
||||||
|
@ -52,11 +50,11 @@ impl Completer for FileCompletion {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(move |x| SemanticSuggestion {
|
.map(move |x| SemanticSuggestion {
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
value: x.1,
|
value: x.path,
|
||||||
style: x.2,
|
style: x.style,
|
||||||
span: reedline::Span {
|
span: reedline::Span {
|
||||||
start: x.0.start - offset,
|
start: x.span.start - offset,
|
||||||
end: x.0.end - offset,
|
end: x.span.end - offset,
|
||||||
},
|
},
|
||||||
..Suggestion::default()
|
..Suggestion::default()
|
||||||
},
|
},
|
||||||
|
@ -95,21 +93,10 @@ impl Completer for FileCompletion {
|
||||||
pub fn file_path_completion(
|
pub fn file_path_completion(
|
||||||
span: nu_protocol::Span,
|
span: nu_protocol::Span,
|
||||||
partial: &str,
|
partial: &str,
|
||||||
cwd: &str,
|
cwds: &[impl AsRef<str>],
|
||||||
options: &CompletionOptions,
|
options: &CompletionOptions,
|
||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
stack: &Stack,
|
stack: &Stack,
|
||||||
) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
|
) -> Vec<FileSuggestion> {
|
||||||
complete_item(false, span, partial, cwd, options, engine_state, stack)
|
complete_item(false, span, partial, cwds, options, engine_state, stack)
|
||||||
}
|
|
||||||
|
|
||||||
pub fn matches(partial: &str, from: &str, options: &CompletionOptions) -> bool {
|
|
||||||
// Check for case sensitive
|
|
||||||
if !options.case_sensitive {
|
|
||||||
return options
|
|
||||||
.match_algorithm
|
|
||||||
.matches_str(&from.to_folded_case(), &partial.to_folded_case());
|
|
||||||
}
|
|
||||||
|
|
||||||
options.match_algorithm.matches_str(from, partial)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::completions::{completion_common::sort_suggestions, Completer, CompletionOptions};
|
use crate::completions::{completion_options::NuMatcher, Completer, CompletionOptions};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::{Expr, Expression},
|
ast::{Expr, Expression},
|
||||||
engine::{Stack, StateWorkingSet},
|
engine::{Stack, StateWorkingSet},
|
||||||
|
@ -35,7 +35,7 @@ impl Completer for FlagCompletion {
|
||||||
let decl = working_set.get_decl(call.decl_id);
|
let decl = working_set.get_decl(call.decl_id);
|
||||||
let sig = decl.signature();
|
let sig = decl.signature();
|
||||||
|
|
||||||
let mut output = vec![];
|
let mut matcher = NuMatcher::new(String::from_utf8_lossy(prefix), options.clone());
|
||||||
|
|
||||||
for named in &sig.named {
|
for named in &sig.named {
|
||||||
let flag_desc = &named.desc;
|
let flag_desc = &named.desc;
|
||||||
|
@ -44,34 +44,7 @@ impl Completer for FlagCompletion {
|
||||||
short.encode_utf8(&mut named);
|
short.encode_utf8(&mut named);
|
||||||
named.insert(0, b'-');
|
named.insert(0, b'-');
|
||||||
|
|
||||||
if options.match_algorithm.matches_u8(&named, prefix) {
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
output.push(SemanticSuggestion {
|
|
||||||
suggestion: Suggestion {
|
|
||||||
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 {
|
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
value: String::from_utf8_lossy(&named).to_string(),
|
value: String::from_utf8_lossy(&named).to_string(),
|
||||||
description: Some(flag_desc.to_string()),
|
description: Some(flag_desc.to_string()),
|
||||||
|
@ -86,9 +59,32 @@ impl Completer for FlagCompletion {
|
||||||
kind: None,
|
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![]
|
vec![]
|
||||||
|
|
|
@ -18,7 +18,7 @@ pub use completion_options::{CompletionOptions, MatchAlgorithm};
|
||||||
pub use custom_completions::CustomCompletion;
|
pub use custom_completions::CustomCompletion;
|
||||||
pub use directory_completions::DirectoryCompletion;
|
pub use directory_completions::DirectoryCompletion;
|
||||||
pub use dotnu_completions::DotNuCompletion;
|
pub use dotnu_completions::DotNuCompletion;
|
||||||
pub use file_completions::{file_path_completion, matches, FileCompletion};
|
pub use file_completions::{file_path_completion, FileCompletion};
|
||||||
pub use flag_completions::FlagCompletion;
|
pub use flag_completions::FlagCompletion;
|
||||||
pub use operator_completions::OperatorCompletion;
|
pub use operator_completions::OperatorCompletion;
|
||||||
pub use variable_completions::VariableCompletion;
|
pub use variable_completions::VariableCompletion;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::completions::{
|
use crate::completions::{
|
||||||
Completer, CompletionOptions, MatchAlgorithm, SemanticSuggestion, SuggestionKind,
|
completion_options::NuMatcher, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind,
|
||||||
};
|
};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::{Expr, Expression},
|
ast::{Expr, Expression},
|
||||||
|
@ -28,7 +28,7 @@ impl Completer for OperatorCompletion {
|
||||||
span: Span,
|
span: Span,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
_pos: usize,
|
_pos: usize,
|
||||||
_options: &CompletionOptions,
|
options: &CompletionOptions,
|
||||||
) -> Vec<SemanticSuggestion> {
|
) -> Vec<SemanticSuggestion> {
|
||||||
//Check if int, float, or string
|
//Check if int, float, or string
|
||||||
let partial = std::str::from_utf8(working_set.get_span_contents(span)).unwrap_or("");
|
let partial = std::str::from_utf8(working_set.get_span_contents(span)).unwrap_or("");
|
||||||
|
@ -60,10 +60,6 @@ impl Completer for OperatorCompletion {
|
||||||
("bit-shr", "Bitwise shift right"),
|
("bit-shr", "Bitwise shift right"),
|
||||||
("in", "Is a member of (doesn't use regex)"),
|
("in", "Is a member of (doesn't use regex)"),
|
||||||
("not-in", "Is not a member of (doesn't use regex)"),
|
("not-in", "Is not a member of (doesn't use regex)"),
|
||||||
(
|
|
||||||
"++",
|
|
||||||
"Appends two lists, a list and a value, two strings, or two binary values",
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
Expr::String(_) => vec![
|
Expr::String(_) => vec![
|
||||||
("=~", "Contains regex match"),
|
("=~", "Contains regex match"),
|
||||||
|
@ -72,7 +68,7 @@ impl Completer for OperatorCompletion {
|
||||||
("not-like", "Does not contain regex match"),
|
("not-like", "Does not contain regex match"),
|
||||||
(
|
(
|
||||||
"++",
|
"++",
|
||||||
"Appends two lists, a list and a value, two strings, or two binary values",
|
"Concatenates two lists, two strings, or two binary values",
|
||||||
),
|
),
|
||||||
("in", "Is a member of (doesn't use regex)"),
|
("in", "Is a member of (doesn't use regex)"),
|
||||||
("not-in", "Is not a member of (doesn't use regex)"),
|
("not-in", "Is not a member of (doesn't use regex)"),
|
||||||
|
@ -95,10 +91,6 @@ impl Completer for OperatorCompletion {
|
||||||
("**", "Power of"),
|
("**", "Power of"),
|
||||||
("in", "Is a member of (doesn't use regex)"),
|
("in", "Is a member of (doesn't use regex)"),
|
||||||
("not-in", "Is not a member of (doesn't use regex)"),
|
("not-in", "Is not a member of (doesn't use regex)"),
|
||||||
(
|
|
||||||
"++",
|
|
||||||
"Appends two lists, a list and a value, two strings, or two binary values",
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
Expr::Bool(_) => vec![
|
Expr::Bool(_) => vec![
|
||||||
(
|
(
|
||||||
|
@ -113,15 +105,11 @@ impl Completer for OperatorCompletion {
|
||||||
("not", "Negates a value or expression"),
|
("not", "Negates a value or expression"),
|
||||||
("in", "Is a member of (doesn't use regex)"),
|
("in", "Is a member of (doesn't use regex)"),
|
||||||
("not-in", "Is not a member of (doesn't use regex)"),
|
("not-in", "Is not a member of (doesn't use regex)"),
|
||||||
(
|
|
||||||
"++",
|
|
||||||
"Appends two lists, a list and a value, two strings, or two binary values",
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
Expr::FullCellPath(path) => match path.head.expr {
|
Expr::FullCellPath(path) => match path.head.expr {
|
||||||
Expr::List(_) => vec![(
|
Expr::List(_) => vec![(
|
||||||
"++",
|
"++",
|
||||||
"Appends two lists, a list and a value, two strings, or two binary values",
|
"Concatenates two lists, two strings, or two binary values",
|
||||||
)],
|
)],
|
||||||
Expr::Var(id) => get_variable_completions(id, working_set),
|
Expr::Var(id) => get_variable_completions(id, working_set),
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
|
@ -129,17 +117,12 @@ impl Completer for OperatorCompletion {
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
let match_algorithm = MatchAlgorithm::Prefix;
|
let mut matcher = NuMatcher::new(partial, options.clone());
|
||||||
let input_fuzzy_search =
|
for (symbol, desc) in possible_operations.into_iter() {
|
||||||
|(operator, _): &(&str, &str)| match_algorithm.matches_str(operator, partial);
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
|
|
||||||
possible_operations
|
|
||||||
.into_iter()
|
|
||||||
.filter(input_fuzzy_search)
|
|
||||||
.map(move |x| SemanticSuggestion {
|
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
value: x.0.to_string(),
|
value: symbol.to_string(),
|
||||||
description: Some(x.1.to_string()),
|
description: Some(desc.to_string()),
|
||||||
span: reedline::Span::new(span.start - offset, span.end - offset),
|
span: reedline::Span::new(span.start - offset, span.end - offset),
|
||||||
append_whitespace: true,
|
append_whitespace: true,
|
||||||
..Suggestion::default()
|
..Suggestion::default()
|
||||||
|
@ -147,8 +130,9 @@ impl Completer for OperatorCompletion {
|
||||||
kind: Some(SuggestionKind::Command(
|
kind: Some(SuggestionKind::Command(
|
||||||
nu_protocol::engine::CommandType::Builtin,
|
nu_protocol::engine::CommandType::Builtin,
|
||||||
)),
|
)),
|
||||||
})
|
});
|
||||||
.collect()
|
}
|
||||||
|
matcher.results()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,7 +149,7 @@ pub fn get_variable_completions<'a>(
|
||||||
Type::List(_) | Type::String | Type::Binary => vec![
|
Type::List(_) | Type::String | Type::Binary => vec![
|
||||||
(
|
(
|
||||||
"++=",
|
"++=",
|
||||||
"Appends a list, a value, a string, or a binary value to a variable.",
|
"Concatenates two lists, two strings, or two binary values",
|
||||||
),
|
),
|
||||||
("=", "Assigns a value to a variable."),
|
("=", "Assigns a value to a variable."),
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
use crate::completions::{
|
use crate::completions::{Completer, CompletionOptions, SemanticSuggestion, SuggestionKind};
|
||||||
Completer, CompletionOptions, MatchAlgorithm, SemanticSuggestion, SuggestionKind,
|
|
||||||
};
|
|
||||||
use nu_engine::{column::get_columns, eval_variable};
|
use nu_engine::{column::get_columns, eval_variable};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{Stack, StateWorkingSet},
|
engine::{Stack, StateWorkingSet},
|
||||||
|
@ -9,7 +7,7 @@ use nu_protocol::{
|
||||||
use reedline::Suggestion;
|
use reedline::Suggestion;
|
||||||
use std::str;
|
use std::str;
|
||||||
|
|
||||||
use super::completion_common::sort_suggestions;
|
use super::completion_options::NuMatcher;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct VariableCompletion {
|
pub struct VariableCompletion {
|
||||||
|
@ -33,7 +31,6 @@ impl Completer for VariableCompletion {
|
||||||
_pos: usize,
|
_pos: usize,
|
||||||
options: &CompletionOptions,
|
options: &CompletionOptions,
|
||||||
) -> Vec<SemanticSuggestion> {
|
) -> Vec<SemanticSuggestion> {
|
||||||
let mut output = vec![];
|
|
||||||
let builtins = ["$nu", "$in", "$env"];
|
let builtins = ["$nu", "$in", "$env"];
|
||||||
let var_str = std::str::from_utf8(&self.var_context.0).unwrap_or("");
|
let var_str = std::str::from_utf8(&self.var_context.0).unwrap_or("");
|
||||||
let var_id = working_set.find_variable(&self.var_context.0);
|
let var_id = working_set.find_variable(&self.var_context.0);
|
||||||
|
@ -43,6 +40,7 @@ impl Completer for VariableCompletion {
|
||||||
};
|
};
|
||||||
let sublevels_count = self.var_context.1.len();
|
let sublevels_count = self.var_context.1.len();
|
||||||
let prefix_str = String::from_utf8_lossy(prefix);
|
let prefix_str = String::from_utf8_lossy(prefix);
|
||||||
|
let mut matcher = NuMatcher::new(prefix_str, options.clone());
|
||||||
|
|
||||||
// Completions for the given variable
|
// Completions for the given variable
|
||||||
if !var_str.is_empty() {
|
if !var_str.is_empty() {
|
||||||
|
@ -63,37 +61,25 @@ impl Completer for VariableCompletion {
|
||||||
|
|
||||||
if let Some(val) = env_vars.get(&target_var_str) {
|
if let Some(val) = env_vars.get(&target_var_str) {
|
||||||
for suggestion in nested_suggestions(val, &nested_levels, current_span) {
|
for suggestion in nested_suggestions(val, &nested_levels, current_span) {
|
||||||
if options.match_algorithm.matches_u8_insensitive(
|
matcher.add_semantic_suggestion(suggestion);
|
||||||
options.case_sensitive,
|
|
||||||
suggestion.suggestion.value.as_bytes(),
|
|
||||||
prefix,
|
|
||||||
) {
|
|
||||||
output.push(suggestion);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sort_suggestions(&prefix_str, output, options);
|
return matcher.results();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No nesting provided, return all env vars
|
// No nesting provided, return all env vars
|
||||||
for env_var in env_vars {
|
for env_var in env_vars {
|
||||||
if options.match_algorithm.matches_u8_insensitive(
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
options.case_sensitive,
|
suggestion: Suggestion {
|
||||||
env_var.0.as_bytes(),
|
value: env_var.0,
|
||||||
prefix,
|
span: current_span,
|
||||||
) {
|
..Suggestion::default()
|
||||||
output.push(SemanticSuggestion {
|
},
|
||||||
suggestion: Suggestion {
|
kind: Some(SuggestionKind::Type(env_var.1.get_type())),
|
||||||
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)
|
for suggestion in nested_suggestions(&nuval, &self.var_context.1, current_span)
|
||||||
{
|
{
|
||||||
if options.match_algorithm.matches_u8_insensitive(
|
matcher.add_semantic_suggestion(suggestion);
|
||||||
options.case_sensitive,
|
|
||||||
suggestion.suggestion.value.as_bytes(),
|
|
||||||
prefix,
|
|
||||||
) {
|
|
||||||
output.push(suggestion);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sort_suggestions(&prefix_str, output, options);
|
return matcher.results();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,37 +110,25 @@ impl Completer for VariableCompletion {
|
||||||
if let Ok(value) = var {
|
if let Ok(value) = var {
|
||||||
for suggestion in nested_suggestions(&value, &self.var_context.1, current_span)
|
for suggestion in nested_suggestions(&value, &self.var_context.1, current_span)
|
||||||
{
|
{
|
||||||
if options.match_algorithm.matches_u8_insensitive(
|
matcher.add_semantic_suggestion(suggestion);
|
||||||
options.case_sensitive,
|
|
||||||
suggestion.suggestion.value.as_bytes(),
|
|
||||||
prefix,
|
|
||||||
) {
|
|
||||||
output.push(suggestion);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sort_suggestions(&prefix_str, output, options);
|
return matcher.results();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Variable completion (e.g: $en<tab> to complete $env)
|
// Variable completion (e.g: $en<tab> to complete $env)
|
||||||
for builtin in builtins {
|
for builtin in builtins {
|
||||||
if options.match_algorithm.matches_u8_insensitive(
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
options.case_sensitive,
|
suggestion: Suggestion {
|
||||||
builtin.as_bytes(),
|
value: builtin.to_string(),
|
||||||
prefix,
|
span: current_span,
|
||||||
) {
|
..Suggestion::default()
|
||||||
output.push(SemanticSuggestion {
|
},
|
||||||
suggestion: Suggestion {
|
// TODO is there a way to get the VarId to get the type???
|
||||||
value: builtin.to_string(),
|
kind: None,
|
||||||
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
|
// 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 scope_frame in working_set.delta.scope.iter().rev() {
|
||||||
for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() {
|
for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() {
|
||||||
for v in &overlay_frame.vars {
|
for v in &overlay_frame.vars {
|
||||||
if options.match_algorithm.matches_u8_insensitive(
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
options.case_sensitive,
|
|
||||||
v.0,
|
|
||||||
prefix,
|
|
||||||
) {
|
|
||||||
output.push(SemanticSuggestion {
|
|
||||||
suggestion: Suggestion {
|
|
||||||
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 {
|
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
value: String::from_utf8_lossy(v.0).to_string(),
|
value: String::from_utf8_lossy(v.0).to_string(),
|
||||||
span: current_span,
|
span: current_span,
|
||||||
|
@ -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?
|
matcher.results()
|
||||||
|
|
||||||
output
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -302,13 +254,3 @@ fn recursive_value(val: &Value, sublevels: &[Vec<u8>]) -> Result<Value, Span> {
|
||||||
Ok(val.clone())
|
Ok(val.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MatchAlgorithm {
|
|
||||||
pub fn matches_u8_insensitive(&self, sensitive: bool, haystack: &[u8], needle: &[u8]) -> bool {
|
|
||||||
if sensitive {
|
|
||||||
self.matches_u8(haystack, needle)
|
|
||||||
} else {
|
|
||||||
self.matches_u8(&haystack.to_ascii_lowercase(), &needle.to_ascii_lowercase())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -9,6 +9,8 @@ use nu_protocol::{
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::util::print_pipeline;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct EvaluateCommandsOpts {
|
pub struct EvaluateCommandsOpts {
|
||||||
pub table_mode: Option<Value>,
|
pub table_mode: Option<Value>,
|
||||||
|
@ -72,7 +74,7 @@ pub fn evaluate_commands(
|
||||||
|
|
||||||
if let Some(err) = working_set.compile_errors.first() {
|
if let Some(err) = working_set.compile_errors.first() {
|
||||||
report_compile_error(&working_set, err);
|
report_compile_error(&working_set, err);
|
||||||
// Not a fatal error, for now
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
(output, working_set.render())
|
(output, working_set.render())
|
||||||
|
@ -93,7 +95,7 @@ pub fn evaluate_commands(
|
||||||
t_mode.coerce_str()?.parse().unwrap_or_default();
|
t_mode.coerce_str()?.parse().unwrap_or_default();
|
||||||
}
|
}
|
||||||
|
|
||||||
pipeline.print(engine_state, stack, no_newline, false)?;
|
print_pipeline(engine_state, stack, pipeline, no_newline)?;
|
||||||
|
|
||||||
info!("evaluate {}:{}:{}", file!(), line!(), column!());
|
info!("evaluate {}:{}:{}", file!(), line!(), column!());
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::util::eval_source;
|
use crate::util::{eval_source, print_pipeline};
|
||||||
use log::{info, trace};
|
use log::{info, trace};
|
||||||
use nu_engine::{convert_env_values, eval_block};
|
use nu_engine::{convert_env_values, eval_block};
|
||||||
use nu_parser::parse;
|
use nu_parser::parse;
|
||||||
|
@ -89,7 +89,7 @@ pub fn evaluate_file(
|
||||||
|
|
||||||
if let Some(err) = working_set.compile_errors.first() {
|
if let Some(err) = working_set.compile_errors.first() {
|
||||||
report_compile_error(&working_set, err);
|
report_compile_error(&working_set, err);
|
||||||
// Not a fatal error, for now
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for blocks whose name starts with "main" and replace it with the filename.
|
// Look for blocks whose name starts with "main" and replace it with the filename.
|
||||||
|
@ -119,7 +119,7 @@ pub fn evaluate_file(
|
||||||
};
|
};
|
||||||
|
|
||||||
// Print the pipeline output of the last command of the file.
|
// Print the pipeline output of the last command of the file.
|
||||||
pipeline.print(engine_state, stack, true, false)?;
|
print_pipeline(engine_state, stack, pipeline, true)?;
|
||||||
|
|
||||||
// Invoke the main command with arguments.
|
// Invoke the main command with arguments.
|
||||||
// Arguments with whitespaces are quoted, thus can be safely concatenated by whitespace.
|
// Arguments with whitespaces are quoted, thus can be safely concatenated by whitespace.
|
||||||
|
|
|
@ -65,8 +65,12 @@ Since this command has no output, there is no point in piping it with other comm
|
||||||
arg.into_pipeline_data()
|
arg.into_pipeline_data()
|
||||||
.print_raw(engine_state, no_newline, to_stderr)?;
|
.print_raw(engine_state, no_newline, to_stderr)?;
|
||||||
} else {
|
} else {
|
||||||
arg.into_pipeline_data()
|
arg.into_pipeline_data().print_table(
|
||||||
.print(engine_state, stack, no_newline, to_stderr)?;
|
engine_state,
|
||||||
|
stack,
|
||||||
|
no_newline,
|
||||||
|
to_stderr,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if !input.is_nothing() {
|
} else if !input.is_nothing() {
|
||||||
|
@ -78,7 +82,7 @@ Since this command has no output, there is no point in piping it with other comm
|
||||||
if raw {
|
if raw {
|
||||||
input.print_raw(engine_state, no_newline, to_stderr)?;
|
input.print_raw(engine_state, no_newline, to_stderr)?;
|
||||||
} else {
|
} else {
|
||||||
input.print(engine_state, stack, no_newline, to_stderr)?;
|
input.print_table(engine_state, stack, no_newline, to_stderr)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -201,6 +201,35 @@ fn gather_env_vars(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Print a pipeline with formatting applied based on display_output hook.
|
||||||
|
///
|
||||||
|
/// This function should be preferred when printing values resulting from a completed evaluation.
|
||||||
|
/// For values printed as part of a command's execution, such as values printed by the `print` command,
|
||||||
|
/// the `PipelineData::print_table` function should be preferred instead as it is not config-dependent.
|
||||||
|
///
|
||||||
|
/// `no_newline` controls if we need to attach newline character to output.
|
||||||
|
pub fn print_pipeline(
|
||||||
|
engine_state: &mut EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
pipeline: PipelineData,
|
||||||
|
no_newline: bool,
|
||||||
|
) -> Result<(), ShellError> {
|
||||||
|
if let Some(hook) = engine_state.get_config().hooks.display_output.clone() {
|
||||||
|
let pipeline = eval_hook(
|
||||||
|
engine_state,
|
||||||
|
stack,
|
||||||
|
Some(pipeline),
|
||||||
|
vec![],
|
||||||
|
&hook,
|
||||||
|
"display_output",
|
||||||
|
)?;
|
||||||
|
pipeline.print_raw(engine_state, no_newline, false)
|
||||||
|
} else {
|
||||||
|
// if display_output isn't set, we should still prefer to print with some formatting
|
||||||
|
pipeline.print_table(engine_state, stack, no_newline, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn eval_source(
|
pub fn eval_source(
|
||||||
engine_state: &mut EngineState,
|
engine_state: &mut EngineState,
|
||||||
stack: &mut Stack,
|
stack: &mut Stack,
|
||||||
|
@ -267,7 +296,7 @@ fn evaluate_source(
|
||||||
|
|
||||||
if let Some(err) = working_set.compile_errors.first() {
|
if let Some(err) = working_set.compile_errors.first() {
|
||||||
report_compile_error(&working_set, err);
|
report_compile_error(&working_set, err);
|
||||||
// Not a fatal error, for now
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
(output, working_set.render())
|
(output, working_set.render())
|
||||||
|
@ -281,36 +310,12 @@ fn evaluate_source(
|
||||||
eval_block::<WithoutDebug>(engine_state, stack, &block, input)
|
eval_block::<WithoutDebug>(engine_state, stack, &block, input)
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
if let PipelineData::ByteStream(..) = pipeline {
|
let no_newline = matches!(&pipeline, &PipelineData::ByteStream(..));
|
||||||
// run the display hook on bytestreams too
|
print_pipeline(engine_state, stack, pipeline, no_newline)?;
|
||||||
run_display_hook(engine_state, stack, pipeline, false)
|
|
||||||
} else {
|
|
||||||
run_display_hook(engine_state, stack, pipeline, true)
|
|
||||||
}?;
|
|
||||||
|
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_display_hook(
|
|
||||||
engine_state: &mut EngineState,
|
|
||||||
stack: &mut Stack,
|
|
||||||
pipeline: PipelineData,
|
|
||||||
no_newline: bool,
|
|
||||||
) -> Result<(), ShellError> {
|
|
||||||
if let Some(hook) = engine_state.get_config().hooks.display_output.clone() {
|
|
||||||
let pipeline = eval_hook(
|
|
||||||
engine_state,
|
|
||||||
stack,
|
|
||||||
Some(pipeline),
|
|
||||||
vec![],
|
|
||||||
&hook,
|
|
||||||
"display_output",
|
|
||||||
)?;
|
|
||||||
pipeline.print(engine_state, stack, no_newline, false)
|
|
||||||
} else {
|
|
||||||
pipeline.print(engine_state, stack, no_newline, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -890,8 +890,8 @@ fn subcommand_completions(mut subcommand_completer: NuCompleter) {
|
||||||
match_suggestions(
|
match_suggestions(
|
||||||
&vec![
|
&vec![
|
||||||
"foo bar".to_string(),
|
"foo bar".to_string(),
|
||||||
"foo aabcrr".to_string(),
|
|
||||||
"foo abaz".to_string(),
|
"foo abaz".to_string(),
|
||||||
|
"foo aabcrr".to_string(),
|
||||||
],
|
],
|
||||||
&suggestions,
|
&suggestions,
|
||||||
);
|
);
|
||||||
|
@ -955,8 +955,8 @@ fn flag_completions() {
|
||||||
"--mime-type".into(),
|
"--mime-type".into(),
|
||||||
"--short-names".into(),
|
"--short-names".into(),
|
||||||
"--threads".into(),
|
"--threads".into(),
|
||||||
"-D".into(),
|
|
||||||
"-a".into(),
|
"-a".into(),
|
||||||
|
"-D".into(),
|
||||||
"-d".into(),
|
"-d".into(),
|
||||||
"-f".into(),
|
"-f".into(),
|
||||||
"-h".into(),
|
"-h".into(),
|
||||||
|
@ -1287,7 +1287,7 @@ fn variables_completions() {
|
||||||
assert_eq!(3, suggestions.len());
|
assert_eq!(3, suggestions.len());
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let expected: Vec<String> = vec!["PWD".into(), "Path".into(), "TEST".into()];
|
let expected: Vec<String> = vec!["Path".into(), "PWD".into(), "TEST".into()];
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
let expected: Vec<String> = vec!["PATH".into(), "PWD".into(), "TEST".into()];
|
let expected: Vec<String> = vec!["PATH".into(), "PWD".into(), "TEST".into()];
|
||||||
|
|
||||||
|
@ -1576,6 +1576,23 @@ fn sort_fuzzy_completions_in_alphabetical_order(mut fuzzy_alpha_sort_completer:
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exact_match() {
|
||||||
|
let (dir, _, engine, stack) = new_partial_engine();
|
||||||
|
|
||||||
|
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||||
|
|
||||||
|
let target_dir = format!("open {}", folder(dir.join("pArTiAL")));
|
||||||
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
|
// Since it's an exact match, only 'partial' should be suggested, not
|
||||||
|
// 'partial-a' and stuff. Implemented in #13302
|
||||||
|
match_suggestions(
|
||||||
|
&vec![file(dir.join("partial").join("hello.txt"))],
|
||||||
|
&suggestions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[ignore = "was reverted, still needs fixing"]
|
#[ignore = "was reverted, still needs fixing"]
|
||||||
#[rstest]
|
#[rstest]
|
||||||
fn alias_offset_bug_7648() {
|
fn alias_offset_bug_7648() {
|
||||||
|
|
|
@ -21,10 +21,10 @@ nu-protocol = { path = "../nu-protocol", version = "0.100.1" }
|
||||||
nu-utils = { path = "../nu-utils", version = "0.100.1" }
|
nu-utils = { path = "../nu-utils", version = "0.100.1" }
|
||||||
|
|
||||||
itertools = { workspace = true }
|
itertools = { workspace = true }
|
||||||
shadow-rs = { version = "0.35", default-features = false }
|
shadow-rs = { version = "0.36", default-features = false }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
shadow-rs = { version = "0.35", default-features = false }
|
shadow-rs = { version = "0.36", default-features = false }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
mimalloc = []
|
mimalloc = []
|
||||||
|
|
|
@ -69,6 +69,33 @@ impl Command for Do {
|
||||||
let block: Closure = call.req(engine_state, caller_stack, 0)?;
|
let block: Closure = call.req(engine_state, caller_stack, 0)?;
|
||||||
let rest: Vec<Value> = call.rest(engine_state, caller_stack, 1)?;
|
let rest: Vec<Value> = call.rest(engine_state, caller_stack, 1)?;
|
||||||
let ignore_all_errors = call.has_flag(engine_state, caller_stack, "ignore-errors")?;
|
let ignore_all_errors = call.has_flag(engine_state, caller_stack, "ignore-errors")?;
|
||||||
|
|
||||||
|
if call.has_flag(engine_state, caller_stack, "ignore-shell-errors")? {
|
||||||
|
nu_protocol::report_shell_warning(
|
||||||
|
engine_state,
|
||||||
|
&ShellError::GenericError {
|
||||||
|
error: "Deprecated option".into(),
|
||||||
|
msg: "`--ignore-shell-errors` is deprecated and will be removed in 0.102.0."
|
||||||
|
.into(),
|
||||||
|
span: Some(call.head),
|
||||||
|
help: Some("Please use the `--ignore-errors(-i)`".into()),
|
||||||
|
inner: vec![],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if call.has_flag(engine_state, caller_stack, "ignore-program-errors")? {
|
||||||
|
nu_protocol::report_shell_warning(
|
||||||
|
engine_state,
|
||||||
|
&ShellError::GenericError {
|
||||||
|
error: "Deprecated option".into(),
|
||||||
|
msg: "`--ignore-program-errors` is deprecated and will be removed in 0.102.0."
|
||||||
|
.into(),
|
||||||
|
span: Some(call.head),
|
||||||
|
help: Some("Please use the `--ignore-errors(-i)`".into()),
|
||||||
|
inner: vec![],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
let ignore_shell_errors = ignore_all_errors
|
let ignore_shell_errors = ignore_all_errors
|
||||||
|| call.has_flag(engine_state, caller_stack, "ignore-shell-errors")?;
|
|| call.has_flag(engine_state, caller_stack, "ignore-shell-errors")?;
|
||||||
let ignore_program_errors = ignore_all_errors
|
let ignore_program_errors = ignore_all_errors
|
||||||
|
@ -208,16 +235,6 @@ impl Command for Do {
|
||||||
example: r#"do --ignore-errors { thisisnotarealcommand }"#,
|
example: r#"do --ignore-errors { thisisnotarealcommand }"#,
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
Example {
|
|
||||||
description: "Run the closure and ignore shell errors",
|
|
||||||
example: r#"do --ignore-shell-errors { thisisnotarealcommand }"#,
|
|
||||||
result: None,
|
|
||||||
},
|
|
||||||
Example {
|
|
||||||
description: "Run the closure and ignore external program errors",
|
|
||||||
example: r#"do --ignore-program-errors { nu --commands 'exit 1' }; echo "I'll still run""#,
|
|
||||||
result: None,
|
|
||||||
},
|
|
||||||
Example {
|
Example {
|
||||||
description: "Abort the pipeline if a program returns a non-zero exit code",
|
description: "Abort the pipeline if a program returns a non-zero exit code",
|
||||||
example: r#"do --capture-errors { nu --commands 'exit 1' } | myscarycommand"#,
|
example: r#"do --capture-errors { nu --commands 'exit 1' } | myscarycommand"#,
|
||||||
|
|
|
@ -86,7 +86,6 @@ serde_yaml = { workspace = true }
|
||||||
sha2 = { workspace = true }
|
sha2 = { workspace = true }
|
||||||
sysinfo = { workspace = true }
|
sysinfo = { workspace = true }
|
||||||
tabled = { workspace = true, features = ["ansi"], default-features = false }
|
tabled = { workspace = true, features = ["ansi"], default-features = false }
|
||||||
terminal_size = { workspace = true }
|
|
||||||
titlecase = { workspace = true }
|
titlecase = { workspace = true }
|
||||||
toml = { workspace = true, features = ["preserve_order"] }
|
toml = { workspace = true, features = ["preserve_order"] }
|
||||||
unicode-segmentation = { workspace = true }
|
unicode-segmentation = { workspace = true }
|
||||||
|
@ -96,6 +95,7 @@ uu_cp = { workspace = true }
|
||||||
uu_mkdir = { workspace = true }
|
uu_mkdir = { workspace = true }
|
||||||
uu_mktemp = { workspace = true }
|
uu_mktemp = { workspace = true }
|
||||||
uu_mv = { workspace = true }
|
uu_mv = { workspace = true }
|
||||||
|
uu_touch = { workspace = true }
|
||||||
uu_uname = { workspace = true }
|
uu_uname = { workspace = true }
|
||||||
uu_whoami = { workspace = true }
|
uu_whoami = { workspace = true }
|
||||||
uuid = { workspace = true, features = ["v4"] }
|
uuid = { workspace = true, features = ["v4"] }
|
||||||
|
|
|
@ -359,7 +359,6 @@ fn nu_value_to_sqlite_type(val: &Value) -> Result<&'static str, ShellError> {
|
||||||
| Type::Custom(_)
|
| Type::Custom(_)
|
||||||
| Type::Error
|
| Type::Error
|
||||||
| Type::List(_)
|
| Type::List(_)
|
||||||
| Type::ListStream
|
|
||||||
| Type::Range
|
| Type::Range
|
||||||
| Type::Record(_)
|
| Type::Record(_)
|
||||||
| Type::Signature
|
| Type::Signature
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_parser::parse;
|
use nu_parser::{flatten_block, parse};
|
||||||
use nu_protocol::engine::StateWorkingSet;
|
use nu_protocol::{engine::StateWorkingSet, record};
|
||||||
|
use serde_json::{json, Value as JsonValue};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Ast;
|
pub struct Ast;
|
||||||
|
@ -16,109 +17,23 @@ impl Command for Ast {
|
||||||
|
|
||||||
fn signature(&self) -> Signature {
|
fn signature(&self) -> Signature {
|
||||||
Signature::build("ast")
|
Signature::build("ast")
|
||||||
.input_output_types(vec![(Type::String, Type::record())])
|
.input_output_types(vec![
|
||||||
|
(Type::Nothing, Type::table()),
|
||||||
|
(Type::Nothing, Type::record()),
|
||||||
|
(Type::Nothing, Type::String),
|
||||||
|
])
|
||||||
.required(
|
.required(
|
||||||
"pipeline",
|
"pipeline",
|
||||||
SyntaxShape::String,
|
SyntaxShape::String,
|
||||||
"The pipeline to print the ast for.",
|
"The pipeline to print the ast for.",
|
||||||
)
|
)
|
||||||
.switch("json", "serialize to json", Some('j'))
|
.switch("json", "Serialize to json", Some('j'))
|
||||||
.switch("minify", "minify the nuon or json output", Some('m'))
|
.switch("minify", "Minify the nuon or json output", Some('m'))
|
||||||
|
.switch("flatten", "An easier to read version of the ast", Some('f'))
|
||||||
.allow_variants_without_examples(true)
|
.allow_variants_without_examples(true)
|
||||||
.category(Category::Debug)
|
.category(Category::Debug)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn 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> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
vec![
|
vec![
|
||||||
Example {
|
Example {
|
||||||
|
@ -147,8 +62,247 @@ impl Command for Ast {
|
||||||
example: "ast 'for x in 1..10 { echo $x ' --json --minify",
|
example: "ast 'for x in 1..10 { echo $x ' --json --minify",
|
||||||
result: None,
|
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)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use super::inspect_table;
|
use super::inspect_table;
|
||||||
|
use crossterm::terminal::size;
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use terminal_size::{terminal_size, Height, Width};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Inspect;
|
pub struct Inspect;
|
||||||
|
@ -38,12 +38,9 @@ impl Command for Inspect {
|
||||||
let original_input = input_val.clone();
|
let original_input = input_val.clone();
|
||||||
let description = input_val.get_type().to_string();
|
let description = input_val.get_type().to_string();
|
||||||
|
|
||||||
let (cols, _rows) = match terminal_size() {
|
let (cols, _rows) = size().unwrap_or((0, 0));
|
||||||
Some((w, h)) => (Width(w.0), Height(h.0)),
|
|
||||||
None => (Width(0), Height(0)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let table = inspect_table::build_table(input_val, description, cols.0 as usize);
|
let table = inspect_table::build_table(input_val, description, cols as usize);
|
||||||
|
|
||||||
// Note that this is printed to stderr. The reason for this is so it doesn't disrupt the regular nushell
|
// Note that this is printed to stderr. The reason for this is so it doesn't disrupt the regular nushell
|
||||||
// tabular output. If we printed to stdout, nushell would get confused with two outputs.
|
// tabular output. If we printed to stdout, nushell would get confused with two outputs.
|
||||||
|
|
|
@ -230,6 +230,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
||||||
Rm,
|
Rm,
|
||||||
Save,
|
Save,
|
||||||
Touch,
|
Touch,
|
||||||
|
UTouch,
|
||||||
Glob,
|
Glob,
|
||||||
Watch,
|
Watch,
|
||||||
};
|
};
|
||||||
|
@ -247,7 +248,9 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
||||||
IsTerminal,
|
IsTerminal,
|
||||||
Kill,
|
Kill,
|
||||||
Sleep,
|
Sleep,
|
||||||
|
Term,
|
||||||
TermSize,
|
TermSize,
|
||||||
|
TermQuery,
|
||||||
Whoami,
|
Whoami,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
37
crates/nu-command/src/env/config/config_env.rs
vendored
37
crates/nu-command/src/env/config/config_env.rs
vendored
|
@ -15,7 +15,16 @@ impl Command for ConfigEnv {
|
||||||
Signature::build(self.name())
|
Signature::build(self.name())
|
||||||
.category(Category::Env)
|
.category(Category::Env)
|
||||||
.input_output_types(vec![(Type::Nothing, Type::Any)])
|
.input_output_types(vec![(Type::Nothing, Type::Any)])
|
||||||
.switch("default", "Print default `env.nu` file instead.", Some('d'))
|
.switch(
|
||||||
|
"default",
|
||||||
|
"Print the internal default `env.nu` file instead.",
|
||||||
|
Some('d'),
|
||||||
|
)
|
||||||
|
.switch(
|
||||||
|
"sample",
|
||||||
|
"Print a commented, sample `env.nu` file instead.",
|
||||||
|
Some('s'),
|
||||||
|
)
|
||||||
// TODO: Signature narrower than what run actually supports theoretically
|
// TODO: Signature narrower than what run actually supports theoretically
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,18 +35,18 @@ impl Command for ConfigEnv {
|
||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
vec![
|
vec![
|
||||||
Example {
|
Example {
|
||||||
description: "allow user to open and update nu env",
|
description: "open user's env.nu in the default editor",
|
||||||
example: "config env",
|
example: "config env",
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "allow user to print default `env.nu` file",
|
description: "pretty-print a commented, sample `env.nu` that explains common settings",
|
||||||
example: "config env --default,",
|
example: "config env --sample | nu-highlight,",
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "allow saving the default `env.nu` locally",
|
description: "pretty-print the internal `env.nu` file which is loaded before the user's environment",
|
||||||
example: "config env --default | save -f ~/.config/nushell/default_env.nu",
|
example: "config env --default | nu-highlight,",
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -50,12 +59,28 @@ impl Command for ConfigEnv {
|
||||||
call: &Call,
|
call: &Call,
|
||||||
_input: PipelineData,
|
_input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let default_flag = call.has_flag(engine_state, stack, "default")?;
|
||||||
|
let sample_flag = call.has_flag(engine_state, stack, "sample")?;
|
||||||
|
if default_flag && sample_flag {
|
||||||
|
return Err(ShellError::IncompatibleParameters {
|
||||||
|
left_message: "can't use `--default` at the same time".into(),
|
||||||
|
left_span: call.get_flag_span(stack, "default").expect("has flag"),
|
||||||
|
right_message: "because of `--sample`".into(),
|
||||||
|
right_span: call.get_flag_span(stack, "sample").expect("has flag"),
|
||||||
|
});
|
||||||
|
}
|
||||||
// `--default` flag handling
|
// `--default` flag handling
|
||||||
if call.has_flag(engine_state, stack, "default")? {
|
if call.has_flag(engine_state, stack, "default")? {
|
||||||
let head = call.head;
|
let head = call.head;
|
||||||
return Ok(Value::string(nu_utils::get_default_env(), head).into_pipeline_data());
|
return Ok(Value::string(nu_utils::get_default_env(), head).into_pipeline_data());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `--sample` flag handling
|
||||||
|
if sample_flag {
|
||||||
|
let head = call.head;
|
||||||
|
return Ok(Value::string(nu_utils::get_sample_env(), head).into_pipeline_data());
|
||||||
|
}
|
||||||
|
|
||||||
// Find the editor executable.
|
// Find the editor executable.
|
||||||
let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?;
|
let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?;
|
||||||
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
||||||
|
|
37
crates/nu-command/src/env/config/config_nu.rs
vendored
37
crates/nu-command/src/env/config/config_nu.rs
vendored
|
@ -17,9 +17,14 @@ impl Command for ConfigNu {
|
||||||
.input_output_types(vec![(Type::Nothing, Type::Any)])
|
.input_output_types(vec![(Type::Nothing, Type::Any)])
|
||||||
.switch(
|
.switch(
|
||||||
"default",
|
"default",
|
||||||
"Print default `config.nu` file instead.",
|
"Print the internal default `config.nu` file instead.",
|
||||||
Some('d'),
|
Some('d'),
|
||||||
)
|
)
|
||||||
|
.switch(
|
||||||
|
"sample",
|
||||||
|
"Print a commented, sample `config.nu` file instead.",
|
||||||
|
Some('s'),
|
||||||
|
)
|
||||||
// TODO: Signature narrower than what run actually supports theoretically
|
// TODO: Signature narrower than what run actually supports theoretically
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,18 +35,19 @@ impl Command for ConfigNu {
|
||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
vec![
|
vec![
|
||||||
Example {
|
Example {
|
||||||
description: "allow user to open and update nu config",
|
description: "open user's config.nu in the default editor",
|
||||||
example: "config nu",
|
example: "config nu",
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "allow user to print default `config.nu` file",
|
description: "pretty-print a commented, sample `config.nu` that explains common settings",
|
||||||
example: "config nu --default,",
|
example: "config nu --sample | nu-highlight",
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "allow saving the default `config.nu` locally",
|
description:
|
||||||
example: "config nu --default | save -f ~/.config/nushell/default_config.nu",
|
"pretty-print the internal `config.nu` file which is loaded before user's config",
|
||||||
|
example: "config nu --default | nu-highlight",
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -54,12 +60,29 @@ impl Command for ConfigNu {
|
||||||
call: &Call,
|
call: &Call,
|
||||||
_input: PipelineData,
|
_input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let default_flag = call.has_flag(engine_state, stack, "default")?;
|
||||||
|
let sample_flag = call.has_flag(engine_state, stack, "sample")?;
|
||||||
|
if default_flag && sample_flag {
|
||||||
|
return Err(ShellError::IncompatibleParameters {
|
||||||
|
left_message: "can't use `--default` at the same time".into(),
|
||||||
|
left_span: call.get_flag_span(stack, "default").expect("has flag"),
|
||||||
|
right_message: "because of `--sample`".into(),
|
||||||
|
right_span: call.get_flag_span(stack, "sample").expect("has flag"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// `--default` flag handling
|
// `--default` flag handling
|
||||||
if call.has_flag(engine_state, stack, "default")? {
|
if default_flag {
|
||||||
let head = call.head;
|
let head = call.head;
|
||||||
return Ok(Value::string(nu_utils::get_default_config(), head).into_pipeline_data());
|
return Ok(Value::string(nu_utils::get_default_config(), head).into_pipeline_data());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `--sample` flag handling
|
||||||
|
if sample_flag {
|
||||||
|
let head = call.head;
|
||||||
|
return Ok(Value::string(nu_utils::get_sample_config(), head).into_pipeline_data());
|
||||||
|
}
|
||||||
|
|
||||||
// Find the editor executable.
|
// Find the editor executable.
|
||||||
let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?;
|
let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?;
|
||||||
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
||||||
|
|
|
@ -12,6 +12,7 @@ mod ucp;
|
||||||
mod umkdir;
|
mod umkdir;
|
||||||
mod umv;
|
mod umv;
|
||||||
mod util;
|
mod util;
|
||||||
|
mod utouch;
|
||||||
mod watch;
|
mod watch;
|
||||||
|
|
||||||
pub use self::open::Open;
|
pub use self::open::Open;
|
||||||
|
@ -27,4 +28,5 @@ pub use touch::Touch;
|
||||||
pub use ucp::UCp;
|
pub use ucp::UCp;
|
||||||
pub use umkdir::UMkdir;
|
pub use umkdir::UMkdir;
|
||||||
pub use umv::UMv;
|
pub use umv::UMv;
|
||||||
|
pub use utouch::UTouch;
|
||||||
pub use watch::Watch;
|
pub use watch::Watch;
|
||||||
|
|
|
@ -188,6 +188,7 @@ impl Command for UMv {
|
||||||
target_dir: None,
|
target_dir: None,
|
||||||
no_target_dir: false,
|
no_target_dir: false,
|
||||||
strip_slashes: false,
|
strip_slashes: false,
|
||||||
|
debug: false,
|
||||||
};
|
};
|
||||||
if let Err(error) = uu_mv::mv(&files, &options) {
|
if let Err(error) = uu_mv::mv(&files, &options) {
|
||||||
return Err(ShellError::GenericError {
|
return Err(ShellError::GenericError {
|
||||||
|
|
268
crates/nu-command/src/filesystem/utouch.rs
Normal file
268
crates/nu-command/src/filesystem/utouch.rs
Normal 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,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -194,7 +194,7 @@ impl Command for Watch {
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(val) => {
|
Ok(val) => {
|
||||||
val.print(engine_state, stack, false, false)?;
|
val.print_table(engine_state, stack, false, false)?;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let working_set = StateWorkingSet::new(engine_state);
|
let working_set = StateWorkingSet::new(engine_state);
|
||||||
|
|
|
@ -129,6 +129,8 @@ fn insert(
|
||||||
let replacement: Value = call.req(engine_state, stack, 1)?;
|
let replacement: Value = call.req(engine_state, stack, 1)?;
|
||||||
|
|
||||||
match input {
|
match input {
|
||||||
|
// Propagate errors in the pipeline
|
||||||
|
PipelineData::Value(Value::Error { error, .. }, ..) => Err(*error),
|
||||||
PipelineData::Value(mut value, metadata) => {
|
PipelineData::Value(mut value, metadata) => {
|
||||||
if let Value::Closure { val, .. } = replacement {
|
if let Value::Closure { val, .. } = replacement {
|
||||||
match (cell_path.members.first(), &mut value) {
|
match (cell_path.members.first(), &mut value) {
|
||||||
|
|
|
@ -120,6 +120,8 @@ repeating this process with row 1, and so on."#
|
||||||
PipelineData::Value(Value::Record { val: inp, .. }, ..),
|
PipelineData::Value(Value::Record { val: inp, .. }, ..),
|
||||||
Value::Record { val: to_merge, .. },
|
Value::Record { val: to_merge, .. },
|
||||||
) => Ok(Value::record(do_merge(inp, &to_merge), head).into_pipeline_data()),
|
) => Ok(Value::record(do_merge(inp, &to_merge), head).into_pipeline_data()),
|
||||||
|
// Propagate errors in the pipeline
|
||||||
|
(PipelineData::Value(Value::Error { error, .. }, ..), _) => Err(*error.clone()),
|
||||||
(PipelineData::Value(val, ..), ..) => {
|
(PipelineData::Value(val, ..), ..) => {
|
||||||
// Only point the "value originates here" arrow at the merge value
|
// Only point the "value originates here" arrow at the merge value
|
||||||
// if it was generated from a block. Otherwise, point at the pipeline value. -Leon 2022-10-27
|
// if it was generated from a block. Otherwise, point at the pipeline value. -Leon 2022-10-27
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
|
use nu_protocol::report_shell_warning;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SplitBy;
|
pub struct SplitBy;
|
||||||
|
@ -27,6 +28,15 @@ impl Command for SplitBy {
|
||||||
call: &Call,
|
call: &Call,
|
||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
report_shell_warning(
|
||||||
|
engine_state,
|
||||||
|
&ShellError::Deprecated {
|
||||||
|
deprecated: "The `split_by` command",
|
||||||
|
suggestion: "Please use the `group-by` command instead.",
|
||||||
|
span: call.head,
|
||||||
|
help: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
split_by(engine_state, stack, call, input)
|
split_by(engine_state, stack, call, input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,10 @@ impl Command for FromCsv {
|
||||||
|
|
||||||
fn signature(&self) -> Signature {
|
fn signature(&self) -> Signature {
|
||||||
Signature::build("from csv")
|
Signature::build("from csv")
|
||||||
.input_output_types(vec![(Type::String, Type::table())])
|
.input_output_types(vec![
|
||||||
|
(Type::String, Type::table()),
|
||||||
|
(Type::String, Type::list(Type::Any)),
|
||||||
|
])
|
||||||
.named(
|
.named(
|
||||||
"separator",
|
"separator",
|
||||||
SyntaxShape::String,
|
SyntaxShape::String,
|
||||||
|
@ -82,6 +85,26 @@ impl Command for FromCsv {
|
||||||
})],
|
})],
|
||||||
))
|
))
|
||||||
},
|
},
|
||||||
|
Example {
|
||||||
|
description: "Convert comma-separated data to a table, allowing variable number of columns per row",
|
||||||
|
example: "\"ColA,ColB\n1,2\n3,4,5\n6\" | from csv --flexible",
|
||||||
|
result: Some(Value::test_list (
|
||||||
|
vec![
|
||||||
|
Value::test_record(record! {
|
||||||
|
"ColA" => Value::test_int(1),
|
||||||
|
"ColB" => Value::test_int(2),
|
||||||
|
}),
|
||||||
|
Value::test_record(record! {
|
||||||
|
"ColA" => Value::test_int(3),
|
||||||
|
"ColB" => Value::test_int(4),
|
||||||
|
"column2" => Value::test_int(5),
|
||||||
|
}),
|
||||||
|
Value::test_record(record! {
|
||||||
|
"ColA" => Value::test_int(6),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "Convert comma-separated data to a table, ignoring headers",
|
description: "Convert comma-separated data to a table, ignoring headers",
|
||||||
example: "open data.txt | from csv --noheaders",
|
example: "open data.txt | from csv --noheaders",
|
||||||
|
|
|
@ -39,12 +39,7 @@ fn from_delimited_stream(
|
||||||
.from_reader(input_reader);
|
.from_reader(input_reader);
|
||||||
|
|
||||||
let headers = if noheaders {
|
let headers = if noheaders {
|
||||||
(0..reader
|
vec![]
|
||||||
.headers()
|
|
||||||
.map_err(|err| from_csv_error(err, span))?
|
|
||||||
.len())
|
|
||||||
.map(|i| format!("column{i}"))
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
} else {
|
} else {
|
||||||
reader
|
reader
|
||||||
.headers()
|
.headers()
|
||||||
|
@ -54,32 +49,28 @@ fn from_delimited_stream(
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let n = headers.len();
|
||||||
|
let columns = headers
|
||||||
|
.into_iter()
|
||||||
|
.chain((n..).map(|i| format!("column{i}")));
|
||||||
let iter = reader.into_records().map(move |row| {
|
let iter = reader.into_records().map(move |row| {
|
||||||
let row = match row {
|
let row = match row {
|
||||||
Ok(row) => row,
|
Ok(row) => row,
|
||||||
Err(err) => return Value::error(from_csv_error(err, span), span),
|
Err(err) => return Value::error(from_csv_error(err, span), span),
|
||||||
};
|
};
|
||||||
let columns = headers.iter().cloned();
|
let columns = columns.clone();
|
||||||
let values = row
|
let values = row.into_iter().map(|s| {
|
||||||
.into_iter()
|
if no_infer {
|
||||||
.map(|s| {
|
Value::string(s, span)
|
||||||
if no_infer {
|
} else if let Ok(i) = s.parse() {
|
||||||
Value::string(s, span)
|
Value::int(i, span)
|
||||||
} else if let Ok(i) = s.parse() {
|
} else if let Ok(f) = s.parse() {
|
||||||
Value::int(i, span)
|
Value::float(f, span)
|
||||||
} else if let Ok(f) = s.parse() {
|
} else {
|
||||||
Value::float(f, span)
|
Value::string(s, span)
|
||||||
} else {
|
}
|
||||||
Value::string(s, span)
|
});
|
||||||
}
|
|
||||||
})
|
|
||||||
.chain(std::iter::repeat(Value::nothing(span)));
|
|
||||||
|
|
||||||
// If there are more values than the number of headers,
|
|
||||||
// then the remaining values are ignored.
|
|
||||||
//
|
|
||||||
// Otherwise, if there are less values than headers,
|
|
||||||
// then `Value::nothing(span)` is used to fill the remaining columns.
|
|
||||||
Value::record(columns.zip(values).collect(), span)
|
Value::record(columns.zip(values).collect(), span)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,10 @@ impl Command for FromTsv {
|
||||||
|
|
||||||
fn signature(&self) -> Signature {
|
fn signature(&self) -> Signature {
|
||||||
Signature::build("from tsv")
|
Signature::build("from tsv")
|
||||||
.input_output_types(vec![(Type::String, Type::table())])
|
.input_output_types(vec![
|
||||||
|
(Type::String, Type::table()),
|
||||||
|
(Type::String, Type::list(Type::Any)),
|
||||||
|
])
|
||||||
.named(
|
.named(
|
||||||
"comment",
|
"comment",
|
||||||
SyntaxShape::String,
|
SyntaxShape::String,
|
||||||
|
@ -76,6 +79,21 @@ impl Command for FromTsv {
|
||||||
})],
|
})],
|
||||||
))
|
))
|
||||||
},
|
},
|
||||||
|
Example {
|
||||||
|
description: "Convert comma-separated data to a table, allowing variable number of columns per row and ignoring headers",
|
||||||
|
example: "\"value 1\nvalue 2\tdescription 2\" | from tsv --flexible --noheaders",
|
||||||
|
result: Some(Value::test_list (
|
||||||
|
vec![
|
||||||
|
Value::test_record(record! {
|
||||||
|
"column0" => Value::test_string("value 1"),
|
||||||
|
}),
|
||||||
|
Value::test_record(record! {
|
||||||
|
"column0" => Value::test_string("value 2"),
|
||||||
|
"column1" => Value::test_string("description 2"),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "Create a tsv file with header columns and open it",
|
description: "Create a tsv file with header columns and open it",
|
||||||
example: r#"$'c1(char tab)c2(char tab)c3(char nl)1(char tab)2(char tab)3' | save tsv-data | open tsv-data | from tsv"#,
|
example: r#"$'c1(char tab)c2(char tab)c3(char nl)1(char tab)2(char tab)3' | save tsv-data | open tsv-data | from tsv"#,
|
||||||
|
|
|
@ -4,7 +4,7 @@ use nu_engine::command_prelude::*;
|
||||||
|
|
||||||
use quick_xml::{
|
use quick_xml::{
|
||||||
escape,
|
escape,
|
||||||
events::{BytesEnd, BytesStart, BytesText, Event},
|
events::{BytesEnd, BytesPI, BytesStart, BytesText, Event},
|
||||||
};
|
};
|
||||||
use std::{borrow::Cow, io::Cursor};
|
use std::{borrow::Cow, io::Cursor};
|
||||||
|
|
||||||
|
@ -406,7 +406,7 @@ impl Job {
|
||||||
let content_text = format!("{} {}", tag, content);
|
let content_text = format!("{} {}", tag, content);
|
||||||
// PI content must NOT be escaped
|
// PI content must NOT be escaped
|
||||||
// https://www.w3.org/TR/xml/#sec-pi
|
// https://www.w3.org/TR/xml/#sec-pi
|
||||||
let pi_content = BytesText::from_escaped(content_text.as_str());
|
let pi_content = BytesPI::new(content_text.as_str());
|
||||||
|
|
||||||
self.writer
|
self.writer
|
||||||
.write_event(Event::PI(pi_content))
|
.write_event(Event::PI(pi_content))
|
||||||
|
|
|
@ -31,7 +31,7 @@ impl Command for HelpOperators {
|
||||||
let mut operators = [
|
let mut operators = [
|
||||||
Operator::Assignment(Assignment::Assign),
|
Operator::Assignment(Assignment::Assign),
|
||||||
Operator::Assignment(Assignment::PlusAssign),
|
Operator::Assignment(Assignment::PlusAssign),
|
||||||
Operator::Assignment(Assignment::AppendAssign),
|
Operator::Assignment(Assignment::ConcatAssign),
|
||||||
Operator::Assignment(Assignment::MinusAssign),
|
Operator::Assignment(Assignment::MinusAssign),
|
||||||
Operator::Assignment(Assignment::MultiplyAssign),
|
Operator::Assignment(Assignment::MultiplyAssign),
|
||||||
Operator::Assignment(Assignment::DivideAssign),
|
Operator::Assignment(Assignment::DivideAssign),
|
||||||
|
@ -48,7 +48,7 @@ impl Command for HelpOperators {
|
||||||
Operator::Comparison(Comparison::StartsWith),
|
Operator::Comparison(Comparison::StartsWith),
|
||||||
Operator::Comparison(Comparison::EndsWith),
|
Operator::Comparison(Comparison::EndsWith),
|
||||||
Operator::Math(Math::Plus),
|
Operator::Math(Math::Plus),
|
||||||
Operator::Math(Math::Append),
|
Operator::Math(Math::Concat),
|
||||||
Operator::Math(Math::Minus),
|
Operator::Math(Math::Minus),
|
||||||
Operator::Math(Math::Multiply),
|
Operator::Math(Math::Multiply),
|
||||||
Operator::Math(Math::Divide),
|
Operator::Math(Math::Divide),
|
||||||
|
@ -144,8 +144,8 @@ fn description(operator: &Operator) -> &'static str {
|
||||||
Operator::Comparison(Comparison::StartsWith) => "Checks if a string starts with another.",
|
Operator::Comparison(Comparison::StartsWith) => "Checks if a string starts with another.",
|
||||||
Operator::Comparison(Comparison::EndsWith) => "Checks if a string ends with another.",
|
Operator::Comparison(Comparison::EndsWith) => "Checks if a string ends with another.",
|
||||||
Operator::Math(Math::Plus) => "Adds two values.",
|
Operator::Math(Math::Plus) => "Adds two values.",
|
||||||
Operator::Math(Math::Append) => {
|
Operator::Math(Math::Concat) => {
|
||||||
"Appends two lists, a list and a value, two strings, or two binary values."
|
"Concatenates two lists, two strings, or two binary values."
|
||||||
}
|
}
|
||||||
Operator::Math(Math::Minus) => "Subtracts two values.",
|
Operator::Math(Math::Minus) => "Subtracts two values.",
|
||||||
Operator::Math(Math::Multiply) => "Multiplies two values.",
|
Operator::Math(Math::Multiply) => "Multiplies two values.",
|
||||||
|
@ -163,8 +163,8 @@ fn description(operator: &Operator) -> &'static str {
|
||||||
Operator::Bits(Bits::ShiftRight) => "Bitwise shifts a value right by another.",
|
Operator::Bits(Bits::ShiftRight) => "Bitwise shifts a value right by another.",
|
||||||
Operator::Assignment(Assignment::Assign) => "Assigns a value to a variable.",
|
Operator::Assignment(Assignment::Assign) => "Assigns a value to a variable.",
|
||||||
Operator::Assignment(Assignment::PlusAssign) => "Adds a value to a variable.",
|
Operator::Assignment(Assignment::PlusAssign) => "Adds a value to a variable.",
|
||||||
Operator::Assignment(Assignment::AppendAssign) => {
|
Operator::Assignment(Assignment::ConcatAssign) => {
|
||||||
"Appends a list, a value, a string, or a binary value to a variable."
|
"Concatenates two lists, two strings, or two binary values."
|
||||||
}
|
}
|
||||||
Operator::Assignment(Assignment::MinusAssign) => "Subtracts a value from a variable.",
|
Operator::Assignment(Assignment::MinusAssign) => "Subtracts a value from a variable.",
|
||||||
Operator::Assignment(Assignment::MultiplyAssign) => "Multiplies a variable by a value.",
|
Operator::Assignment(Assignment::MultiplyAssign) => "Multiplies a variable by a value.",
|
||||||
|
|
|
@ -376,7 +376,7 @@ fn send_multipart_request(
|
||||||
format!("Content-Length: {}", val.len()),
|
format!("Content-Length: {}", val.len()),
|
||||||
];
|
];
|
||||||
builder
|
builder
|
||||||
.add(&mut Cursor::new(val), &headers.join("\n"))
|
.add(&mut Cursor::new(val), &headers.join("\r\n"))
|
||||||
.map_err(err)?;
|
.map_err(err)?;
|
||||||
} else {
|
} else {
|
||||||
let headers = format!(r#"Content-Disposition: form-data; name="{}""#, col);
|
let headers = format!(r#"Content-Disposition: form-data; name="{}""#, col);
|
||||||
|
|
|
@ -43,6 +43,12 @@ impl Command for Input {
|
||||||
"number of characters to read; suppresses output",
|
"number of characters to read; suppresses output",
|
||||||
Some('n'),
|
Some('n'),
|
||||||
)
|
)
|
||||||
|
.named(
|
||||||
|
"default",
|
||||||
|
SyntaxShape::String,
|
||||||
|
"default value if no input is provided",
|
||||||
|
Some('d'),
|
||||||
|
)
|
||||||
.switch("suppress-output", "don't print keystroke values", Some('s'))
|
.switch("suppress-output", "don't print keystroke values", Some('s'))
|
||||||
.category(Category::Platform)
|
.category(Category::Platform)
|
||||||
}
|
}
|
||||||
|
@ -72,8 +78,12 @@ impl Command for Input {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let default_val: Option<String> = call.get_flag(engine_state, stack, "default")?;
|
||||||
if let Some(prompt) = &prompt {
|
if let Some(prompt) = &prompt {
|
||||||
print!("{prompt}");
|
match &default_val {
|
||||||
|
None => print!("{prompt}"),
|
||||||
|
Some(val) => print!("{prompt} (default: {val})"),
|
||||||
|
}
|
||||||
let _ = std::io::stdout().flush();
|
let _ = std::io::stdout().flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,7 +159,10 @@ impl Command for Input {
|
||||||
if !suppress_output {
|
if !suppress_output {
|
||||||
std::io::stdout().write_all(b"\n")?;
|
std::io::stdout().write_all(b"\n")?;
|
||||||
}
|
}
|
||||||
Ok(Value::string(buf, call.head).into_pipeline_data())
|
match default_val {
|
||||||
|
Some(val) if buf.is_empty() => Ok(Value::string(val, call.head).into_pipeline_data()),
|
||||||
|
_ => Ok(Value::string(buf, call.head).into_pipeline_data()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
@ -164,6 +177,11 @@ impl Command for Input {
|
||||||
example: "let user_input = (input --numchar 2)",
|
example: "let user_input = (input --numchar 2)",
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
|
Example {
|
||||||
|
description: "Get input from the user with default value, and assign to a variable",
|
||||||
|
example: "let user_input = (input --default 10)",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ mod input;
|
||||||
mod is_terminal;
|
mod is_terminal;
|
||||||
mod kill;
|
mod kill;
|
||||||
mod sleep;
|
mod sleep;
|
||||||
mod term_size;
|
mod term;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
mod ulimit;
|
mod ulimit;
|
||||||
mod whoami;
|
mod whoami;
|
||||||
|
@ -19,7 +19,7 @@ pub use input::InputListen;
|
||||||
pub use is_terminal::IsTerminal;
|
pub use is_terminal::IsTerminal;
|
||||||
pub use kill::Kill;
|
pub use kill::Kill;
|
||||||
pub use sleep::Sleep;
|
pub use sleep::Sleep;
|
||||||
pub use term_size::TermSize;
|
pub use term::{Term, TermQuery, TermSize};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
pub use ulimit::ULimit;
|
pub use ulimit::ULimit;
|
||||||
pub use whoami::Whoami;
|
pub use whoami::Whoami;
|
||||||
|
|
7
crates/nu-command/src/platform/term/mod.rs
Normal file
7
crates/nu-command/src/platform/term/mod.rs
Normal 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;
|
34
crates/nu-command/src/platform/term/term_.rs
Normal file
34
crates/nu-command/src/platform/term/term_.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
137
crates/nu-command/src/platform/term/term_query.rs
Normal file
137
crates/nu-command/src/platform/term/term_query.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
|
use crossterm::terminal::size;
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use terminal_size::{terminal_size, Height, Width};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TermSize;
|
pub struct TermSize;
|
||||||
|
@ -51,15 +51,12 @@ impl Command for TermSize {
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let head = call.head;
|
let head = call.head;
|
||||||
|
|
||||||
let (cols, rows) = match terminal_size() {
|
let (cols, rows) = size().unwrap_or((0, 0));
|
||||||
Some((w, h)) => (Width(w.0), Height(h.0)),
|
|
||||||
None => (Width(0), Height(0)),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Value::record(
|
Ok(Value::record(
|
||||||
record! {
|
record! {
|
||||||
"columns" => Value::int(cols.0 as i64, head),
|
"columns" => Value::int(cols as i64, head),
|
||||||
"rows" => Value::int(rows.0 as i64, head),
|
"rows" => Value::int(rows as i64, head),
|
||||||
},
|
},
|
||||||
head,
|
head,
|
||||||
)
|
)
|
|
@ -12,7 +12,7 @@ impl Command for SubCommand {
|
||||||
|
|
||||||
fn signature(&self) -> Signature {
|
fn signature(&self) -> Signature {
|
||||||
Signature::build("random dice")
|
Signature::build("random dice")
|
||||||
.input_output_types(vec![(Type::Nothing, Type::ListStream)])
|
.input_output_types(vec![(Type::Nothing, Type::list(Type::Int))])
|
||||||
.allow_variants_without_examples(true)
|
.allow_variants_without_examples(true)
|
||||||
.named(
|
.named(
|
||||||
"dice",
|
"dice",
|
||||||
|
|
|
@ -77,20 +77,6 @@ impl Command for SubCommand {
|
||||||
call: &Call,
|
call: &Call,
|
||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
if call.has_flag_const(working_set, "not")? {
|
|
||||||
nu_protocol::report_shell_error(
|
|
||||||
working_set.permanent(),
|
|
||||||
&ShellError::GenericError {
|
|
||||||
error: "Deprecated option".into(),
|
|
||||||
msg: "`str contains --not {string}` is deprecated and will be removed in 0.95."
|
|
||||||
.into(),
|
|
||||||
span: Some(call.head),
|
|
||||||
help: Some("Please use the `not` operator instead.".into()),
|
|
||||||
inner: vec![],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let cell_paths: Vec<CellPath> = call.rest_const(working_set, 1)?;
|
let cell_paths: Vec<CellPath> = call.rest_const(working_set, 1)?;
|
||||||
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
|
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
|
||||||
let args = Arguments {
|
let args = Arguments {
|
||||||
|
|
|
@ -307,7 +307,7 @@ fn test_one_newline() {
|
||||||
correct_counts.insert(Counter::GraphemeClusters, 1);
|
correct_counts.insert(Counter::GraphemeClusters, 1);
|
||||||
correct_counts.insert(Counter::Bytes, 1);
|
correct_counts.insert(Counter::Bytes, 1);
|
||||||
correct_counts.insert(Counter::CodePoints, 1);
|
correct_counts.insert(Counter::CodePoints, 1);
|
||||||
correct_counts.insert(Counter::UnicodeWidth, 0);
|
correct_counts.insert(Counter::UnicodeWidth, 1);
|
||||||
|
|
||||||
assert_eq!(correct_counts, counts);
|
assert_eq!(correct_counts, counts);
|
||||||
}
|
}
|
||||||
|
@ -347,7 +347,7 @@ fn test_count_counts_lines() {
|
||||||
|
|
||||||
// one more than grapheme clusters because of \r\n
|
// one more than grapheme clusters because of \r\n
|
||||||
correct_counts.insert(Counter::CodePoints, 24);
|
correct_counts.insert(Counter::CodePoints, 24);
|
||||||
correct_counts.insert(Counter::UnicodeWidth, 17);
|
correct_counts.insert(Counter::UnicodeWidth, 23);
|
||||||
|
|
||||||
assert_eq!(correct_counts, counts);
|
assert_eq!(correct_counts, counts);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@ impl Command for NuCheck {
|
||||||
Signature::build("nu-check")
|
Signature::build("nu-check")
|
||||||
.input_output_types(vec![
|
.input_output_types(vec![
|
||||||
(Type::String, Type::Bool),
|
(Type::String, Type::Bool),
|
||||||
(Type::ListStream, Type::Bool),
|
|
||||||
(Type::List(Box::new(Type::Any)), Type::Bool),
|
(Type::List(Box::new(Type::Any)), Type::Bool),
|
||||||
])
|
])
|
||||||
// type is string to avoid automatically canonicalizing the path
|
// type is string to avoid automatically canonicalizing the path
|
||||||
|
|
|
@ -5,6 +5,8 @@ use nu_protocol::{did_you_mean, process::ChildProcess, ByteStream, NuGlob, OutDe
|
||||||
use nu_system::ForegroundChild;
|
use nu_system::ForegroundChild;
|
||||||
use nu_utils::IgnoreCaseExt;
|
use nu_utils::IgnoreCaseExt;
|
||||||
use pathdiff::diff_paths;
|
use pathdiff::diff_paths;
|
||||||
|
#[cfg(windows)]
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
ffi::{OsStr, OsString},
|
ffi::{OsStr, OsString},
|
||||||
|
@ -91,6 +93,22 @@ impl Command for External {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// let's make sure it's a .ps1 script, but only on Windows
|
||||||
|
let potential_powershell_script = if cfg!(windows) {
|
||||||
|
if let Some(executable) = which(&expanded_name, "", cwd.as_ref()) {
|
||||||
|
let ext = executable
|
||||||
|
.extension()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_uppercase();
|
||||||
|
ext == "PS1"
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
// Find the absolute path to the executable. On Windows, set the
|
// Find the absolute path to the executable. On Windows, set the
|
||||||
// executable to "cmd.exe" if it's a CMD internal command. If the
|
// executable to "cmd.exe" if it's a CMD internal command. If the
|
||||||
// command is not found, display a helpful error message.
|
// command is not found, display a helpful error message.
|
||||||
|
@ -98,11 +116,16 @@ impl Command for External {
|
||||||
&& (is_cmd_internal_command(&name_str) || potential_nuscript_in_windows)
|
&& (is_cmd_internal_command(&name_str) || potential_nuscript_in_windows)
|
||||||
{
|
{
|
||||||
PathBuf::from("cmd.exe")
|
PathBuf::from("cmd.exe")
|
||||||
|
} else if cfg!(windows) && potential_powershell_script {
|
||||||
|
// If we're on Windows and we're trying to run a PowerShell script, we'll use
|
||||||
|
// `powershell.exe` to run it. We shouldn't have to check for powershell.exe because
|
||||||
|
// it's automatically installed on all modern windows systems.
|
||||||
|
PathBuf::from("powershell.exe")
|
||||||
} else {
|
} else {
|
||||||
// Determine the PATH to be used and then use `which` to find it - though this has no
|
// Determine the PATH to be used and then use `which` to find it - though this has no
|
||||||
// effect if it's an absolute path already
|
// effect if it's an absolute path already
|
||||||
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
||||||
let Some(executable) = which(expanded_name, &paths, cwd.as_ref()) else {
|
let Some(executable) = which(&expanded_name, &paths, cwd.as_ref()) else {
|
||||||
return Err(command_not_found(&name_str, call.head, engine_state, stack));
|
return Err(command_not_found(&name_str, call.head, engine_state, stack));
|
||||||
};
|
};
|
||||||
executable
|
executable
|
||||||
|
@ -123,15 +146,29 @@ impl Command for External {
|
||||||
let args = eval_arguments_from_call(engine_state, stack, call)?;
|
let args = eval_arguments_from_call(engine_state, stack, call)?;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
if is_cmd_internal_command(&name_str) || potential_nuscript_in_windows {
|
if is_cmd_internal_command(&name_str) || potential_nuscript_in_windows {
|
||||||
use std::os::windows::process::CommandExt;
|
|
||||||
|
|
||||||
// The /D flag disables execution of AutoRun commands from registry.
|
// The /D flag disables execution of AutoRun commands from registry.
|
||||||
// The /C flag followed by a command name instructs CMD to execute
|
// The /C flag followed by a command name instructs CMD to execute
|
||||||
// that command and quit.
|
// that command and quit.
|
||||||
command.args(["/D", "/C", &name_str]);
|
command.args(["/D", "/C", &expanded_name.to_string_lossy()]);
|
||||||
for arg in &args {
|
for arg in &args {
|
||||||
command.raw_arg(escape_cmd_argument(arg)?);
|
command.raw_arg(escape_cmd_argument(arg)?);
|
||||||
}
|
}
|
||||||
|
} else if potential_powershell_script {
|
||||||
|
use nu_path::canonicalize_with;
|
||||||
|
|
||||||
|
// canonicalize the path to the script so that tests pass
|
||||||
|
let canon_path = if let Ok(cwd) = engine_state.cwd_as_string(None) {
|
||||||
|
canonicalize_with(&expanded_name, cwd)?
|
||||||
|
} else {
|
||||||
|
// If we can't get the current working directory, just provide the expanded name
|
||||||
|
expanded_name
|
||||||
|
};
|
||||||
|
// The -Command flag followed by a script name instructs PowerShell to
|
||||||
|
// execute that script and quit.
|
||||||
|
command.args(["-Command", &canon_path.to_string_lossy()]);
|
||||||
|
for arg in &args {
|
||||||
|
command.raw_arg(arg.item.clone());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
command.args(args.into_iter().map(|s| s.item));
|
command.args(args.into_iter().map(|s| s.item));
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,8 +44,29 @@ fn net(span: Span) -> Value {
|
||||||
let networks = Networks::new_with_refreshed_list()
|
let networks = Networks::new_with_refreshed_list()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(iface, data)| {
|
.map(|(iface, data)| {
|
||||||
|
let ip_addresses = data
|
||||||
|
.ip_networks()
|
||||||
|
.iter()
|
||||||
|
.map(|ip| {
|
||||||
|
let protocol = match ip.addr {
|
||||||
|
std::net::IpAddr::V4(_) => "ipv4",
|
||||||
|
std::net::IpAddr::V6(_) => "ipv6",
|
||||||
|
};
|
||||||
|
Value::record(
|
||||||
|
record! {
|
||||||
|
"address" => Value::string(ip.addr.to_string(), span),
|
||||||
|
"protocol" => Value::string(protocol, span),
|
||||||
|
"loop" => Value::bool(ip.addr.is_loopback(), span),
|
||||||
|
"multicast" => Value::bool(ip.addr.is_multicast(), span),
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
let record = record! {
|
let record = record! {
|
||||||
"name" => Value::string(trim_cstyle_null(iface), span),
|
"name" => Value::string(trim_cstyle_null(iface), span),
|
||||||
|
"mac" => Value::string(data.mac_address().to_string(), span),
|
||||||
|
"ip" => Value::list(ip_addresses, span),
|
||||||
"sent" => Value::filesize(data.total_transmitted() as i64, span),
|
"sent" => Value::filesize(data.total_transmitted() as i64, span),
|
||||||
"recv" => Value::filesize(data.total_received() as i64, span),
|
"recv" => Value::filesize(data.total_received() as i64, span),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
// use super::icons::{icon_for_file, iconify_style_ansi_to_nu};
|
// use super::icons::{icon_for_file, iconify_style_ansi_to_nu};
|
||||||
use super::icons::icon_for_file;
|
use super::icons::icon_for_file;
|
||||||
|
use crossterm::terminal::size;
|
||||||
use lscolors::Style;
|
use lscolors::Style;
|
||||||
use nu_engine::{command_prelude::*, env_to_string};
|
use nu_engine::{command_prelude::*, env_to_string};
|
||||||
use nu_protocol::Config;
|
use nu_protocol::Config;
|
||||||
use nu_term_grid::grid::{Alignment, Cell, Direction, Filling, Grid, GridOptions};
|
use nu_term_grid::grid::{Alignment, Cell, Direction, Filling, Grid, GridOptions};
|
||||||
use nu_utils::get_ls_colors;
|
use nu_utils::get_ls_colors;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use terminal_size::{Height, Width};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Griddle;
|
pub struct Griddle;
|
||||||
|
@ -192,7 +192,7 @@ fn create_grid_output(
|
||||||
|
|
||||||
let cols = if let Some(col) = width_param {
|
let cols = if let Some(col) = width_param {
|
||||||
col as u16
|
col as u16
|
||||||
} else if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() {
|
} else if let Ok((w, _h)) = size() {
|
||||||
w
|
w
|
||||||
} else {
|
} else {
|
||||||
80u16
|
80u16
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// overall reduce the redundant calls to StyleComputer etc.
|
// overall reduce the redundant calls to StyleComputer etc.
|
||||||
// the goal is to configure it once...
|
// the goal is to configure it once...
|
||||||
|
|
||||||
|
use crossterm::terminal::size;
|
||||||
use lscolors::{LsColors, Style};
|
use lscolors::{LsColors, Style};
|
||||||
use nu_color_config::{color_from_hex, StyleComputer, TextStyle};
|
use nu_color_config::{color_from_hex, StyleComputer, TextStyle};
|
||||||
use nu_engine::{command_prelude::*, env_to_string};
|
use nu_engine::{command_prelude::*, env_to_string};
|
||||||
|
@ -22,7 +23,6 @@ use std::{
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
use terminal_size::{Height, Width};
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
const STREAM_PAGE_SIZE: usize = 1000;
|
const STREAM_PAGE_SIZE: usize = 1000;
|
||||||
|
@ -30,7 +30,7 @@ const STREAM_PAGE_SIZE: usize = 1000;
|
||||||
fn get_width_param(width_param: Option<i64>) -> usize {
|
fn get_width_param(width_param: Option<i64>) -> usize {
|
||||||
if let Some(col) = width_param {
|
if let Some(col) = width_param {
|
||||||
col as usize
|
col as usize
|
||||||
} else if let Some((Width(w), Height(_))) = terminal_size::terminal_size() {
|
} else if let Ok((w, _h)) = size() {
|
||||||
w as usize
|
w as usize
|
||||||
} else {
|
} else {
|
||||||
80
|
80
|
||||||
|
@ -1088,7 +1088,7 @@ fn create_empty_placeholder(
|
||||||
let data = vec![vec![cell]];
|
let data = vec![vec![cell]];
|
||||||
let mut table = NuTable::from(data);
|
let mut table = NuTable::from(data);
|
||||||
table.set_data_style(TextStyle::default().dimmed());
|
table.set_data_style(TextStyle::default().dimmed());
|
||||||
let out = TableOutput::new(table, false, false, false);
|
let out = TableOutput::new(table, false, false, 1);
|
||||||
|
|
||||||
let style_computer = &StyleComputer::from_config(engine_state, stack);
|
let style_computer = &StyleComputer::from_config(engine_state, stack);
|
||||||
let config = create_nu_table_config(&config, style_computer, &out, false, TableMode::default());
|
let config = create_nu_table_config(&config, style_computer, &out, false, TableMode::default());
|
||||||
|
|
|
@ -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"));
|
|
||||||
}
|
|
76
crates/nu-command/tests/commands/assignment/concat.rs
Normal file
76
crates/nu-command/tests/commands/assignment/concat.rs
Normal 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"));
|
||||||
|
}
|
|
@ -1 +1 @@
|
||||||
mod append_assign;
|
mod concat;
|
||||||
|
|
|
@ -2,6 +2,13 @@ use nu_test_support::nu;
|
||||||
use nu_test_support::playground::Playground;
|
use nu_test_support::playground::Playground;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn def_with_trailing_comma() {
|
||||||
|
let actual = nu!("def test-command [ foo: int, ] { $foo }; test-command 1");
|
||||||
|
|
||||||
|
assert!(actual.out == "1");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn def_with_comment() {
|
fn def_with_comment() {
|
||||||
Playground::setup("def_with_comment", |dirs, _| {
|
Playground::setup("def_with_comment", |dirs, _| {
|
||||||
|
@ -72,6 +79,13 @@ fn def_errors_with_comma_before_equals() {
|
||||||
assert!(actual.err.contains("expected parameter"));
|
assert!(actual.err.contains("expected parameter"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn def_errors_with_colon_before_equals() {
|
||||||
|
let actual = nu!("def test-command [ foo: = 1 ] {}");
|
||||||
|
|
||||||
|
assert!(actual.err.contains("expected type"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn def_errors_with_comma_before_colon() {
|
fn def_errors_with_comma_before_colon() {
|
||||||
let actual = nu!("def test-command [ foo, : int ] {}");
|
let actual = nu!("def test-command [ foo, : int ] {}");
|
||||||
|
@ -85,7 +99,6 @@ fn def_errors_with_multiple_colons() {
|
||||||
assert!(actual.err.contains("expected type"));
|
assert!(actual.err.contains("expected type"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ignore = "This error condition is not implemented yet"]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn def_errors_with_multiple_types() {
|
fn def_errors_with_multiple_types() {
|
||||||
let actual = nu!("def test-command [ foo:int:string ] {}");
|
let actual = nu!("def test-command [ foo:int:string ] {}");
|
||||||
|
@ -93,6 +106,20 @@ fn def_errors_with_multiple_types() {
|
||||||
assert!(actual.err.contains("expected parameter"));
|
assert!(actual.err.contains("expected parameter"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn def_errors_with_trailing_colon() {
|
||||||
|
let actual = nu!("def test-command [ foo: int: ] {}");
|
||||||
|
|
||||||
|
assert!(actual.err.contains("expected parameter"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn def_errors_with_trailing_default_value() {
|
||||||
|
let actual = nu!("def test-command [ foo: int = ] {}");
|
||||||
|
|
||||||
|
assert!(actual.err.contains("expected default value"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn def_errors_with_multiple_commas() {
|
fn def_errors_with_multiple_commas() {
|
||||||
let actual = nu!("def test-command [ foo,,bar ] {}");
|
let actual = nu!("def test-command [ foo,,bar ] {}");
|
||||||
|
|
|
@ -44,7 +44,7 @@ fn do_with_semicolon_break_on_failed_external() {
|
||||||
fn ignore_shell_errors_works_for_external_with_semicolon() {
|
fn ignore_shell_errors_works_for_external_with_semicolon() {
|
||||||
let actual = nu!(r#"do -s { open asdfasdf.txt }; "text""#);
|
let actual = nu!(r#"do -s { open asdfasdf.txt }; "text""#);
|
||||||
|
|
||||||
assert_eq!(actual.err, "");
|
assert!(actual.err.contains("Deprecated option"));
|
||||||
assert_eq!(actual.out, "text");
|
assert_eq!(actual.out, "text");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ fn ignore_shell_errors_works_for_external_with_semicolon() {
|
||||||
fn ignore_program_errors_works_for_external_with_semicolon() {
|
fn ignore_program_errors_works_for_external_with_semicolon() {
|
||||||
let actual = nu!(r#"do -p { nu -n -c 'exit 1' }; "text""#);
|
let actual = nu!(r#"do -p { nu -n -c 'exit 1' }; "text""#);
|
||||||
|
|
||||||
assert_eq!(actual.err, "");
|
assert!(actual.err.contains("Deprecated option"));
|
||||||
assert_eq!(actual.out, "text");
|
assert_eq!(actual.out, "text");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,6 +80,7 @@ fn run_closure_with_it_using() {
|
||||||
#[test]
|
#[test]
|
||||||
fn waits_for_external() {
|
fn waits_for_external() {
|
||||||
let actual = nu!(r#"do -p { nu -c 'sleep 1sec; print before; exit 1'}; print after"#);
|
let actual = nu!(r#"do -p { nu -c 'sleep 1sec; print before; exit 1'}; print after"#);
|
||||||
assert!(actual.err.is_empty());
|
|
||||||
|
assert!(actual.err.contains("Deprecated option"));
|
||||||
assert_eq!(actual.out, "beforeafter");
|
assert_eq!(actual.out, "beforeafter");
|
||||||
}
|
}
|
||||||
|
|
|
@ -483,7 +483,7 @@ fn compound_where_paren() {
|
||||||
// TODO: these ++ tests are not really testing *math* functionality, maybe find another place for them
|
// TODO: these ++ tests are not really testing *math* functionality, maybe find another place for them
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn adding_lists() {
|
fn concat_lists() {
|
||||||
let actual = nu!(pipeline(
|
let actual = nu!(pipeline(
|
||||||
r#"
|
r#"
|
||||||
[1 3] ++ [5 6] | to nuon
|
[1 3] ++ [5 6] | to nuon
|
||||||
|
@ -494,29 +494,7 @@ fn adding_lists() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn adding_list_and_value() {
|
fn concat_tables() {
|
||||||
let actual = nu!(pipeline(
|
|
||||||
r#"
|
|
||||||
[1 3] ++ 5 | to nuon
|
|
||||||
"#
|
|
||||||
));
|
|
||||||
|
|
||||||
assert_eq!(actual.out, "[1, 3, 5]");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn adding_value_and_list() {
|
|
||||||
let actual = nu!(pipeline(
|
|
||||||
r#"
|
|
||||||
1 ++ [3 5] | to nuon
|
|
||||||
"#
|
|
||||||
));
|
|
||||||
|
|
||||||
assert_eq!(actual.out, "[1, 3, 5]");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn adding_tables() {
|
|
||||||
let actual = nu!(pipeline(
|
let actual = nu!(pipeline(
|
||||||
r#"
|
r#"
|
||||||
[[a b]; [1 2]] ++ [[c d]; [10 11]] | to nuon
|
[[a b]; [1 2]] ++ [[c d]; [10 11]] | to nuon
|
||||||
|
@ -526,7 +504,7 @@ fn adding_tables() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn append_strings() {
|
fn concat_strings() {
|
||||||
let actual = nu!(pipeline(
|
let actual = nu!(pipeline(
|
||||||
r#"
|
r#"
|
||||||
"foo" ++ "bar"
|
"foo" ++ "bar"
|
||||||
|
@ -536,7 +514,7 @@ fn append_strings() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn append_binary_values() {
|
fn concat_binary_values() {
|
||||||
let actual = nu!(pipeline(
|
let actual = nu!(pipeline(
|
||||||
r#"
|
r#"
|
||||||
0x[01 02] ++ 0x[03 04] | to nuon
|
0x[01 02] ++ 0x[03 04] | to nuon
|
||||||
|
|
|
@ -127,6 +127,7 @@ mod update;
|
||||||
mod upsert;
|
mod upsert;
|
||||||
mod url;
|
mod url;
|
||||||
mod use_;
|
mod use_;
|
||||||
|
mod utouch;
|
||||||
mod where_;
|
mod where_;
|
||||||
mod which;
|
mod which;
|
||||||
mod while_;
|
mod while_;
|
||||||
|
|
|
@ -513,13 +513,18 @@ fn test_mv_no_clobber() {
|
||||||
sandbox.with_files(&[EmptyFile(file_a)]);
|
sandbox.with_files(&[EmptyFile(file_a)]);
|
||||||
sandbox.with_files(&[EmptyFile(file_b)]);
|
sandbox.with_files(&[EmptyFile(file_b)]);
|
||||||
|
|
||||||
let actual = nu!(
|
let _ = nu!(
|
||||||
cwd: dirs.test(),
|
cwd: dirs.test(),
|
||||||
"mv -n {} {}",
|
"mv -n {} {}",
|
||||||
file_a,
|
file_a,
|
||||||
file_b,
|
file_b,
|
||||||
);
|
);
|
||||||
assert!(actual.err.contains("not replacing"));
|
|
||||||
|
let file_count = nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"ls test_mv* | length | to nuon"
|
||||||
|
);
|
||||||
|
assert_eq!(file_count.out, "2");
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -355,9 +355,9 @@ fn external_command_receives_raw_binary_data() {
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
#[test]
|
#[test]
|
||||||
fn can_run_batch_files() {
|
fn can_run_cmd_files() {
|
||||||
use nu_test_support::fs::Stub::FileWithContent;
|
use nu_test_support::fs::Stub::FileWithContent;
|
||||||
Playground::setup("run a Windows batch file", |dirs, sandbox| {
|
Playground::setup("run a Windows cmd file", |dirs, sandbox| {
|
||||||
sandbox.with_files(&[FileWithContent(
|
sandbox.with_files(&[FileWithContent(
|
||||||
"foo.cmd",
|
"foo.cmd",
|
||||||
r#"
|
r#"
|
||||||
|
@ -371,12 +371,30 @@ fn can_run_batch_files() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
#[test]
|
||||||
|
fn can_run_batch_files() {
|
||||||
|
use nu_test_support::fs::Stub::FileWithContent;
|
||||||
|
Playground::setup("run a Windows batch file", |dirs, sandbox| {
|
||||||
|
sandbox.with_files(&[FileWithContent(
|
||||||
|
"foo.bat",
|
||||||
|
r#"
|
||||||
|
@echo off
|
||||||
|
echo Hello World
|
||||||
|
"#,
|
||||||
|
)]);
|
||||||
|
|
||||||
|
let actual = nu!(cwd: dirs.test(), pipeline("foo.bat"));
|
||||||
|
assert!(actual.out.contains("Hello World"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
#[test]
|
#[test]
|
||||||
fn can_run_batch_files_without_cmd_extension() {
|
fn can_run_batch_files_without_cmd_extension() {
|
||||||
use nu_test_support::fs::Stub::FileWithContent;
|
use nu_test_support::fs::Stub::FileWithContent;
|
||||||
Playground::setup(
|
Playground::setup(
|
||||||
"run a Windows batch file without specifying the extension",
|
"run a Windows cmd file without specifying the extension",
|
||||||
|dirs, sandbox| {
|
|dirs, sandbox| {
|
||||||
sandbox.with_files(&[FileWithContent(
|
sandbox.with_files(&[FileWithContent(
|
||||||
"foo.cmd",
|
"foo.cmd",
|
||||||
|
@ -440,3 +458,20 @@ fn redirect_combine() {
|
||||||
assert_eq!(actual.out, "FooBar");
|
assert_eq!(actual.out, "FooBar");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
#[test]
|
||||||
|
fn can_run_ps1_files() {
|
||||||
|
use nu_test_support::fs::Stub::FileWithContent;
|
||||||
|
Playground::setup("run_a_windows_ps_file", |dirs, sandbox| {
|
||||||
|
sandbox.with_files(&[FileWithContent(
|
||||||
|
"foo.ps1",
|
||||||
|
r#"
|
||||||
|
Write-Host Hello World
|
||||||
|
"#,
|
||||||
|
)]);
|
||||||
|
|
||||||
|
let actual = nu!(cwd: dirs.test(), pipeline("foo.ps1"));
|
||||||
|
assert!(actual.out.contains("Hello World"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -2941,3 +2941,123 @@ fn table_footer_inheritance() {
|
||||||
assert_eq!(actual.out.match_indices("x2").count(), 1);
|
assert_eq!(actual.out.match_indices("x2").count(), 1);
|
||||||
assert_eq!(actual.out.match_indices("x3").count(), 1);
|
assert_eq!(actual.out.match_indices("x3").count(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_footer_inheritance_kv_rows() {
|
||||||
|
let actual = nu!(
|
||||||
|
concat!(
|
||||||
|
"$env.config.table.footer_inheritance = true;",
|
||||||
|
"$env.config.footer_mode = 7;",
|
||||||
|
"[[a b]; ['kv' {0: 0, 1: 1, 2: 2, 3: 3, 4: 4} ], ['data' 0], ['data' 0] ] | table --expand --width=80",
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
actual.out,
|
||||||
|
"╭───┬──────┬───────────╮\
|
||||||
|
│ # │ a │ b │\
|
||||||
|
├───┼──────┼───────────┤\
|
||||||
|
│ 0 │ kv │ ╭───┬───╮ │\
|
||||||
|
│ │ │ │ 0 │ 0 │ │\
|
||||||
|
│ │ │ │ 1 │ 1 │ │\
|
||||||
|
│ │ │ │ 2 │ 2 │ │\
|
||||||
|
│ │ │ │ 3 │ 3 │ │\
|
||||||
|
│ │ │ │ 4 │ 4 │ │\
|
||||||
|
│ │ │ ╰───┴───╯ │\
|
||||||
|
│ 1 │ data │ 0 │\
|
||||||
|
│ 2 │ data │ 0 │\
|
||||||
|
╰───┴──────┴───────────╯"
|
||||||
|
);
|
||||||
|
|
||||||
|
let actual = nu!(
|
||||||
|
concat!(
|
||||||
|
"$env.config.table.footer_inheritance = true;",
|
||||||
|
"$env.config.footer_mode = 7;",
|
||||||
|
"[[a b]; ['kv' {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5} ], ['data' 0], ['data' 0] ] | table --expand --width=80",
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
actual.out,
|
||||||
|
"╭───┬──────┬───────────╮\
|
||||||
|
│ # │ a │ b │\
|
||||||
|
├───┼──────┼───────────┤\
|
||||||
|
│ 0 │ kv │ ╭───┬───╮ │\
|
||||||
|
│ │ │ │ 0 │ 0 │ │\
|
||||||
|
│ │ │ │ 1 │ 1 │ │\
|
||||||
|
│ │ │ │ 2 │ 2 │ │\
|
||||||
|
│ │ │ │ 3 │ 3 │ │\
|
||||||
|
│ │ │ │ 4 │ 4 │ │\
|
||||||
|
│ │ │ │ 5 │ 5 │ │\
|
||||||
|
│ │ │ ╰───┴───╯ │\
|
||||||
|
│ 1 │ data │ 0 │\
|
||||||
|
│ 2 │ data │ 0 │\
|
||||||
|
├───┼──────┼───────────┤\
|
||||||
|
│ # │ a │ b │\
|
||||||
|
╰───┴──────┴───────────╯"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_footer_inheritance_list_rows() {
|
||||||
|
let actual = nu!(
|
||||||
|
concat!(
|
||||||
|
"$env.config.table.footer_inheritance = true;",
|
||||||
|
"$env.config.footer_mode = 7;",
|
||||||
|
"[[a b]; ['kv' {0: [[field]; [0] [1] [2] [3] [4]]} ], ['data' 0], ['data' 0] ] | table --expand --width=80",
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
actual.out,
|
||||||
|
"╭───┬──────┬───────────────────────╮\
|
||||||
|
│ # │ a │ b │\
|
||||||
|
├───┼──────┼───────────────────────┤\
|
||||||
|
│ 0 │ kv │ ╭───┬───────────────╮ │\
|
||||||
|
│ │ │ │ │ ╭───┬───────╮ │ │\
|
||||||
|
│ │ │ │ 0 │ │ # │ field │ │ │\
|
||||||
|
│ │ │ │ │ ├───┼───────┤ │ │\
|
||||||
|
│ │ │ │ │ │ 0 │ 0 │ │ │\
|
||||||
|
│ │ │ │ │ │ 1 │ 1 │ │ │\
|
||||||
|
│ │ │ │ │ │ 2 │ 2 │ │ │\
|
||||||
|
│ │ │ │ │ │ 3 │ 3 │ │ │\
|
||||||
|
│ │ │ │ │ │ 4 │ 4 │ │ │\
|
||||||
|
│ │ │ │ │ ╰───┴───────╯ │ │\
|
||||||
|
│ │ │ ╰───┴───────────────╯ │\
|
||||||
|
│ 1 │ data │ 0 │\
|
||||||
|
│ 2 │ data │ 0 │\
|
||||||
|
╰───┴──────┴───────────────────────╯"
|
||||||
|
);
|
||||||
|
|
||||||
|
let actual = nu!(
|
||||||
|
concat!(
|
||||||
|
"$env.config.table.footer_inheritance = true;",
|
||||||
|
"$env.config.footer_mode = 7;",
|
||||||
|
"[[a b]; ['kv' {0: [[field]; [0] [1] [2] [3] [4] [5]]} ], ['data' 0], ['data' 0] ] | table --expand --width=80",
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
actual.out,
|
||||||
|
"╭───┬──────┬───────────────────────╮\
|
||||||
|
│ # │ a │ b │\
|
||||||
|
├───┼──────┼───────────────────────┤\
|
||||||
|
│ 0 │ kv │ ╭───┬───────────────╮ │\
|
||||||
|
│ │ │ │ │ ╭───┬───────╮ │ │\
|
||||||
|
│ │ │ │ 0 │ │ # │ field │ │ │\
|
||||||
|
│ │ │ │ │ ├───┼───────┤ │ │\
|
||||||
|
│ │ │ │ │ │ 0 │ 0 │ │ │\
|
||||||
|
│ │ │ │ │ │ 1 │ 1 │ │ │\
|
||||||
|
│ │ │ │ │ │ 2 │ 2 │ │ │\
|
||||||
|
│ │ │ │ │ │ 3 │ 3 │ │ │\
|
||||||
|
│ │ │ │ │ │ 4 │ 4 │ │ │\
|
||||||
|
│ │ │ │ │ │ 5 │ 5 │ │ │\
|
||||||
|
│ │ │ │ │ ╰───┴───────╯ │ │\
|
||||||
|
│ │ │ ╰───┴───────────────╯ │\
|
||||||
|
│ 1 │ data │ 0 │\
|
||||||
|
│ 2 │ data │ 0 │\
|
||||||
|
├───┼──────┼───────────────────────┤\
|
||||||
|
│ # │ a │ b │\
|
||||||
|
╰───┴──────┴───────────────────────╯"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -841,14 +841,13 @@ fn test_cp_arg_no_clobber() {
|
||||||
let target = dirs.fixtures.join("cp").join(TEST_HOW_ARE_YOU_SOURCE);
|
let target = dirs.fixtures.join("cp").join(TEST_HOW_ARE_YOU_SOURCE);
|
||||||
let target_hash = get_file_hash(target.display());
|
let target_hash = get_file_hash(target.display());
|
||||||
|
|
||||||
let actual = nu!(
|
let _ = nu!(
|
||||||
cwd: dirs.root(),
|
cwd: dirs.root(),
|
||||||
"cp {} {} --no-clobber",
|
"cp {} {} --no-clobber",
|
||||||
src.display(),
|
src.display(),
|
||||||
target.display()
|
target.display()
|
||||||
);
|
);
|
||||||
let after_cp_hash = get_file_hash(target.display());
|
let after_cp_hash = get_file_hash(target.display());
|
||||||
assert!(actual.err.contains("not replacing"));
|
|
||||||
// Check content was not clobbered
|
// Check content was not clobbered
|
||||||
assert_eq!(after_cp_hash, target_hash);
|
assert_eq!(after_cp_hash, target_hash);
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,7 +27,7 @@ fn url_join_with_only_user() {
|
||||||
"password": "",
|
"password": "",
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": "",
|
"port": "",
|
||||||
} | url join
|
} | url join
|
||||||
"#
|
"#
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ fn url_join_with_only_pwd() {
|
||||||
"password": "pwd",
|
"password": "pwd",
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": "",
|
"port": "",
|
||||||
} | url join
|
} | url join
|
||||||
"#
|
"#
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ fn url_join_with_user_and_pwd() {
|
||||||
"password": "pwd",
|
"password": "pwd",
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": "",
|
"port": "",
|
||||||
} | url join
|
} | url join
|
||||||
"#
|
"#
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ fn url_join_with_query() {
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"query": "par_1=aaa&par_2=bbb"
|
"query": "par_1=aaa&par_2=bbb"
|
||||||
"port": "",
|
"port": "",
|
||||||
} | url join
|
} | url join
|
||||||
"#
|
"#
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -411,12 +411,9 @@ fn url_join_with_params_invalid_table() {
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"params": (
|
"params": (
|
||||||
[
|
[
|
||||||
["key", "value"];
|
{ key: foo, value: bar }
|
||||||
["par_1", "aaa"],
|
"not a record"
|
||||||
["par_2", "bbb"],
|
]
|
||||||
["par_1", "ccc"],
|
|
||||||
["par_2", "ddd"],
|
|
||||||
] ++ ["not a record"]
|
|
||||||
),
|
),
|
||||||
"port": "1234",
|
"port": "1234",
|
||||||
} | url join
|
} | url join
|
||||||
|
|
740
crates/nu-command/tests/commands/utouch.rs
Normal file
740
crates/nu-command/tests/commands/utouch.rs
Normal 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));
|
||||||
|
})
|
||||||
|
}
|
|
@ -469,7 +469,7 @@ fn from_csv_test_flexible_extra_vals() {
|
||||||
echo "a,b\n1,2,3" | from csv --flexible | first | values | to nuon
|
echo "a,b\n1,2,3" | from csv --flexible | first | values | to nuon
|
||||||
"#
|
"#
|
||||||
));
|
));
|
||||||
assert_eq!(actual.out, "[1, 2]");
|
assert_eq!(actual.out, "[1, 2, 3]");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -479,5 +479,5 @@ fn from_csv_test_flexible_missing_vals() {
|
||||||
echo "a,b\n1" | from csv --flexible | first | values | to nuon
|
echo "a,b\n1" | from csv --flexible | first | values | to nuon
|
||||||
"#
|
"#
|
||||||
));
|
));
|
||||||
assert_eq!(actual.out, "[1, null]");
|
assert_eq!(actual.out, "[1]");
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,3 +29,20 @@ fn from_ods_file_to_table_select_sheet() {
|
||||||
|
|
||||||
assert_eq!(actual.out, "SalesOrders");
|
assert_eq!(actual.out, "SalesOrders");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_ods_file_to_table_select_sheet_with_annotations() {
|
||||||
|
let actual = nu!(
|
||||||
|
cwd: "tests/fixtures/formats", pipeline(
|
||||||
|
r#"
|
||||||
|
open sample_data_with_annotation.ods --raw
|
||||||
|
| from ods --sheets ["SalesOrders"]
|
||||||
|
| get SalesOrders
|
||||||
|
| get column4
|
||||||
|
| get 0
|
||||||
|
"#
|
||||||
|
));
|
||||||
|
|
||||||
|
// The Units column in the sheet SalesOrders has an annotation and should be ignored.
|
||||||
|
assert_eq!(actual.out, "Units");
|
||||||
|
}
|
||||||
|
|
|
@ -155,7 +155,7 @@ pub(crate) fn decompose_assignment(assignment: Assignment) -> Option<Operator> {
|
||||||
match assignment {
|
match assignment {
|
||||||
Assignment::Assign => None,
|
Assignment::Assign => None,
|
||||||
Assignment::PlusAssign => Some(Operator::Math(Math::Plus)),
|
Assignment::PlusAssign => Some(Operator::Math(Math::Plus)),
|
||||||
Assignment::AppendAssign => Some(Operator::Math(Math::Append)),
|
Assignment::ConcatAssign => Some(Operator::Math(Math::Concat)),
|
||||||
Assignment::MinusAssign => Some(Operator::Math(Math::Minus)),
|
Assignment::MinusAssign => Some(Operator::Math(Math::Minus)),
|
||||||
Assignment::MultiplyAssign => Some(Operator::Math(Math::Multiply)),
|
Assignment::MultiplyAssign => Some(Operator::Math(Math::Multiply)),
|
||||||
Assignment::DivideAssign => Some(Operator::Math(Math::Divide)),
|
Assignment::DivideAssign => Some(Operator::Math(Math::Divide)),
|
||||||
|
|
|
@ -547,9 +547,9 @@ impl Eval for EvalRuntime {
|
||||||
let lhs = eval_expression::<D>(engine_state, stack, lhs)?;
|
let lhs = eval_expression::<D>(engine_state, stack, lhs)?;
|
||||||
lhs.div(op_span, &rhs, op_span)?
|
lhs.div(op_span, &rhs, op_span)?
|
||||||
}
|
}
|
||||||
Assignment::AppendAssign => {
|
Assignment::ConcatAssign => {
|
||||||
let lhs = eval_expression::<D>(engine_state, stack, lhs)?;
|
let lhs = eval_expression::<D>(engine_state, stack, lhs)?;
|
||||||
lhs.append(op_span, &rhs, op_span)?
|
lhs.concat(op_span, &rhs, op_span)?
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -956,7 +956,7 @@ fn binary_op(
|
||||||
},
|
},
|
||||||
Operator::Math(mat) => match mat {
|
Operator::Math(mat) => match mat {
|
||||||
Math::Plus => lhs_val.add(op_span, &rhs_val, span)?,
|
Math::Plus => lhs_val.add(op_span, &rhs_val, span)?,
|
||||||
Math::Append => lhs_val.append(op_span, &rhs_val, span)?,
|
Math::Concat => lhs_val.concat(op_span, &rhs_val, span)?,
|
||||||
Math::Minus => lhs_val.sub(op_span, &rhs_val, span)?,
|
Math::Minus => lhs_val.sub(op_span, &rhs_val, span)?,
|
||||||
Math::Multiply => lhs_val.mul(op_span, &rhs_val, span)?,
|
Math::Multiply => lhs_val.mul(op_span, &rhs_val, span)?,
|
||||||
Math::Divide => lhs_val.div(op_span, &rhs_val, span)?,
|
Math::Divide => lhs_val.div(op_span, &rhs_val, span)?,
|
||||||
|
|
|
@ -27,7 +27,6 @@ nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.100.1" }
|
||||||
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
terminal_size = { workspace = true }
|
|
||||||
strip-ansi-escapes = { workspace = true }
|
strip-ansi-escapes = { workspace = true }
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true }
|
||||||
ratatui = { workspace = true }
|
ratatui = { workspace = true }
|
||||||
|
|
|
@ -9,6 +9,7 @@ mod views;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use commands::{ExpandCmd, HelpCmd, NuCmd, QuitCmd, TableCmd, TryCmd};
|
use commands::{ExpandCmd, HelpCmd, NuCmd, QuitCmd, TableCmd, TryCmd};
|
||||||
|
use crossterm::terminal::size;
|
||||||
pub use default_context::add_explore_context;
|
pub use default_context::add_explore_context;
|
||||||
pub use explore::Explore;
|
pub use explore::Explore;
|
||||||
use explore::ExploreConfig;
|
use explore::ExploreConfig;
|
||||||
|
@ -19,7 +20,6 @@ use nu_protocol::{
|
||||||
};
|
};
|
||||||
use pager::{Page, Pager, PagerConfig};
|
use pager::{Page, Pager, PagerConfig};
|
||||||
use registry::CommandRegistry;
|
use registry::CommandRegistry;
|
||||||
use terminal_size::{Height, Width};
|
|
||||||
use views::{BinaryView, Orientation, Preview, RecordView};
|
use views::{BinaryView, Orientation, Preview, RecordView};
|
||||||
|
|
||||||
mod util {
|
mod util {
|
||||||
|
@ -80,7 +80,7 @@ fn create_record_view(
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.tail {
|
if config.tail {
|
||||||
if let Some((Width(w), Height(h))) = terminal_size::terminal_size() {
|
if let Ok((w, h)) = size() {
|
||||||
view.tail(w, h);
|
view.tail(w, h);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -772,6 +772,51 @@ fn calculate_end_span(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_oneof(
|
||||||
|
working_set: &mut StateWorkingSet,
|
||||||
|
spans: &[Span],
|
||||||
|
spans_idx: &mut usize,
|
||||||
|
possible_shapes: &Vec<SyntaxShape>,
|
||||||
|
multispan: bool,
|
||||||
|
) -> Expression {
|
||||||
|
for shape in possible_shapes {
|
||||||
|
let starting_error_count = working_set.parse_errors.len();
|
||||||
|
let value = match multispan {
|
||||||
|
true => parse_multispan_value(working_set, spans, spans_idx, shape),
|
||||||
|
false => parse_value(working_set, spans[*spans_idx], shape),
|
||||||
|
};
|
||||||
|
|
||||||
|
if starting_error_count == working_set.parse_errors.len() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// while trying the possible shapes, ignore Expected type errors
|
||||||
|
// unless they're inside a block, closure, or expression
|
||||||
|
let propagate_error = match working_set.parse_errors.last() {
|
||||||
|
Some(ParseError::Expected(_, error_span))
|
||||||
|
| Some(ParseError::ExpectedWithStringMsg(_, error_span)) => {
|
||||||
|
matches!(
|
||||||
|
shape,
|
||||||
|
SyntaxShape::Block | SyntaxShape::Closure(_) | SyntaxShape::Expression
|
||||||
|
) && *error_span != spans[*spans_idx]
|
||||||
|
}
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
|
if !propagate_error {
|
||||||
|
working_set.parse_errors.truncate(starting_error_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if working_set.parse_errors.is_empty() {
|
||||||
|
working_set.error(ParseError::ExpectedWithStringMsg(
|
||||||
|
format!("one of a list of accepted shapes: {possible_shapes:?}"),
|
||||||
|
spans[*spans_idx],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::garbage(working_set, spans[*spans_idx])
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_multispan_value(
|
pub fn parse_multispan_value(
|
||||||
working_set: &mut StateWorkingSet,
|
working_set: &mut StateWorkingSet,
|
||||||
spans: &[Span],
|
spans: &[Span],
|
||||||
|
@ -800,54 +845,10 @@ pub fn parse_multispan_value(
|
||||||
|
|
||||||
arg
|
arg
|
||||||
}
|
}
|
||||||
SyntaxShape::OneOf(shapes) => {
|
SyntaxShape::OneOf(possible_shapes) => {
|
||||||
// handle for `if` command.
|
parse_oneof(working_set, spans, spans_idx, possible_shapes, true)
|
||||||
//let block_then_exp = shapes.as_slice() == [SyntaxShape::Block, SyntaxShape::Expression];
|
|
||||||
for shape in shapes.iter() {
|
|
||||||
let starting_error_count = working_set.parse_errors.len();
|
|
||||||
let s = parse_multispan_value(working_set, spans, spans_idx, shape);
|
|
||||||
|
|
||||||
if starting_error_count == working_set.parse_errors.len() {
|
|
||||||
return s;
|
|
||||||
} else if let Some(
|
|
||||||
ParseError::Expected(..) | ParseError::ExpectedWithStringMsg(..),
|
|
||||||
) = working_set.parse_errors.last()
|
|
||||||
{
|
|
||||||
working_set.parse_errors.truncate(starting_error_count);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// `if` is parsing block first and then expression.
|
|
||||||
// when we're writing something like `else if $a`, parsing as a
|
|
||||||
// block will result to error(because it's not a block)
|
|
||||||
//
|
|
||||||
// If parse as a expression also failed, user is more likely concerned
|
|
||||||
// about expression failure rather than "expect block failure"".
|
|
||||||
|
|
||||||
// FIXME FIXME FIXME
|
|
||||||
// if block_then_exp {
|
|
||||||
// match &err {
|
|
||||||
// Some(ParseError::Expected(expected, _)) => {
|
|
||||||
// if expected.starts_with("block") {
|
|
||||||
// err = e
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// _ => err = err.or(e),
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// err = err.or(e)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
let span = spans[*spans_idx];
|
|
||||||
|
|
||||||
if working_set.parse_errors.is_empty() {
|
|
||||||
working_set.error(ParseError::ExpectedWithStringMsg(
|
|
||||||
format!("one of a list of accepted shapes: {shapes:?}"),
|
|
||||||
span,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Expression::garbage(working_set, span)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SyntaxShape::Expression => {
|
SyntaxShape::Expression => {
|
||||||
trace!("parsing: expression");
|
trace!("parsing: expression");
|
||||||
|
|
||||||
|
@ -3392,6 +3393,7 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
|
||||||
Arg,
|
Arg,
|
||||||
AfterCommaArg,
|
AfterCommaArg,
|
||||||
Type,
|
Type,
|
||||||
|
AfterType,
|
||||||
DefaultValue,
|
DefaultValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3425,7 +3427,9 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
|
||||||
let mut args: Vec<Arg> = vec![];
|
let mut args: Vec<Arg> = vec![];
|
||||||
let mut parse_mode = ParseMode::Arg;
|
let mut parse_mode = ParseMode::Arg;
|
||||||
|
|
||||||
for token in &output {
|
for (index, token) in output.iter().enumerate() {
|
||||||
|
let last_token = index == output.len() - 1;
|
||||||
|
|
||||||
match token {
|
match token {
|
||||||
Token {
|
Token {
|
||||||
contents: crate::TokenContents::Item | crate::TokenContents::AssignmentOperator,
|
contents: crate::TokenContents::Item | crate::TokenContents::AssignmentOperator,
|
||||||
|
@ -3437,10 +3441,12 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
|
||||||
// The : symbol separates types
|
// The : symbol separates types
|
||||||
if contents == b":" {
|
if contents == b":" {
|
||||||
match parse_mode {
|
match parse_mode {
|
||||||
|
ParseMode::Arg if last_token => working_set
|
||||||
|
.error(ParseError::Expected("type", Span::new(span.end, span.end))),
|
||||||
ParseMode::Arg => {
|
ParseMode::Arg => {
|
||||||
parse_mode = ParseMode::Type;
|
parse_mode = ParseMode::Type;
|
||||||
}
|
}
|
||||||
ParseMode::AfterCommaArg => {
|
ParseMode::AfterCommaArg | ParseMode::AfterType => {
|
||||||
working_set.error(ParseError::Expected("parameter or flag", span));
|
working_set.error(ParseError::Expected("parameter or flag", span));
|
||||||
}
|
}
|
||||||
ParseMode::Type | ParseMode::DefaultValue => {
|
ParseMode::Type | ParseMode::DefaultValue => {
|
||||||
|
@ -3452,9 +3458,15 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
|
||||||
// The = symbol separates a variable from its default value
|
// The = symbol separates a variable from its default value
|
||||||
else if contents == b"=" {
|
else if contents == b"=" {
|
||||||
match parse_mode {
|
match parse_mode {
|
||||||
ParseMode::Type | ParseMode::Arg => {
|
ParseMode::Arg | ParseMode::AfterType if last_token => working_set.error(
|
||||||
|
ParseError::Expected("default value", Span::new(span.end, span.end)),
|
||||||
|
),
|
||||||
|
ParseMode::Arg | ParseMode::AfterType => {
|
||||||
parse_mode = ParseMode::DefaultValue;
|
parse_mode = ParseMode::DefaultValue;
|
||||||
}
|
}
|
||||||
|
ParseMode::Type => {
|
||||||
|
working_set.error(ParseError::Expected("type", span));
|
||||||
|
}
|
||||||
ParseMode::AfterCommaArg => {
|
ParseMode::AfterCommaArg => {
|
||||||
working_set.error(ParseError::Expected("parameter or flag", span));
|
working_set.error(ParseError::Expected("parameter or flag", span));
|
||||||
}
|
}
|
||||||
|
@ -3467,7 +3479,9 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
|
||||||
// The , symbol separates params only
|
// The , symbol separates params only
|
||||||
else if contents == b"," {
|
else if contents == b"," {
|
||||||
match parse_mode {
|
match parse_mode {
|
||||||
ParseMode::Arg => parse_mode = ParseMode::AfterCommaArg,
|
ParseMode::Arg | ParseMode::AfterType => {
|
||||||
|
parse_mode = ParseMode::AfterCommaArg
|
||||||
|
}
|
||||||
ParseMode::AfterCommaArg => {
|
ParseMode::AfterCommaArg => {
|
||||||
working_set.error(ParseError::Expected("parameter or flag", span));
|
working_set.error(ParseError::Expected("parameter or flag", span));
|
||||||
}
|
}
|
||||||
|
@ -3480,7 +3494,7 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match parse_mode {
|
match parse_mode {
|
||||||
ParseMode::Arg | ParseMode::AfterCommaArg => {
|
ParseMode::Arg | ParseMode::AfterCommaArg | ParseMode::AfterType => {
|
||||||
// Long flag with optional short form following with no whitespace, e.g. --output, --age(-a)
|
// Long flag with optional short form following with no whitespace, e.g. --output, --age(-a)
|
||||||
if contents.starts_with(b"--") && contents.len() > 2 {
|
if contents.starts_with(b"--") && contents.len() > 2 {
|
||||||
// Split the long flag from the short flag with the ( character as delimiter.
|
// Split the long flag from the short flag with the ( character as delimiter.
|
||||||
|
@ -3790,7 +3804,7 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
parse_mode = ParseMode::Arg;
|
parse_mode = ParseMode::AfterType;
|
||||||
}
|
}
|
||||||
ParseMode::DefaultValue => {
|
ParseMode::DefaultValue => {
|
||||||
if let Some(last) = args.last_mut() {
|
if let Some(last) = args.last_mut() {
|
||||||
|
@ -4813,29 +4827,7 @@ pub fn parse_value(
|
||||||
|
|
||||||
SyntaxShape::ExternalArgument => parse_regular_external_arg(working_set, span),
|
SyntaxShape::ExternalArgument => parse_regular_external_arg(working_set, span),
|
||||||
SyntaxShape::OneOf(possible_shapes) => {
|
SyntaxShape::OneOf(possible_shapes) => {
|
||||||
for s in possible_shapes {
|
parse_oneof(working_set, &[span], &mut 0, possible_shapes, false)
|
||||||
let starting_error_count = working_set.parse_errors.len();
|
|
||||||
let value = parse_value(working_set, span, s);
|
|
||||||
|
|
||||||
if starting_error_count == working_set.parse_errors.len() {
|
|
||||||
return value;
|
|
||||||
} else if let Some(
|
|
||||||
ParseError::Expected(..) | ParseError::ExpectedWithStringMsg(..),
|
|
||||||
) = working_set.parse_errors.last()
|
|
||||||
{
|
|
||||||
working_set.parse_errors.truncate(starting_error_count);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if working_set.parse_errors.is_empty() {
|
|
||||||
working_set.error(ParseError::ExpectedWithStringMsg(
|
|
||||||
format!("one of a list of accepted shapes: {possible_shapes:?}"),
|
|
||||||
span,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Expression::garbage(working_set, span)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SyntaxShape::Any => {
|
SyntaxShape::Any => {
|
||||||
|
@ -4895,7 +4887,7 @@ pub fn parse_assignment_operator(working_set: &mut StateWorkingSet, span: Span)
|
||||||
let operator = match contents {
|
let operator = match contents {
|
||||||
b"=" => Operator::Assignment(Assignment::Assign),
|
b"=" => Operator::Assignment(Assignment::Assign),
|
||||||
b"+=" => Operator::Assignment(Assignment::PlusAssign),
|
b"+=" => Operator::Assignment(Assignment::PlusAssign),
|
||||||
b"++=" => Operator::Assignment(Assignment::AppendAssign),
|
b"++=" => Operator::Assignment(Assignment::ConcatAssign),
|
||||||
b"-=" => Operator::Assignment(Assignment::MinusAssign),
|
b"-=" => Operator::Assignment(Assignment::MinusAssign),
|
||||||
b"*=" => Operator::Assignment(Assignment::MultiplyAssign),
|
b"*=" => Operator::Assignment(Assignment::MultiplyAssign),
|
||||||
b"/=" => Operator::Assignment(Assignment::DivideAssign),
|
b"/=" => Operator::Assignment(Assignment::DivideAssign),
|
||||||
|
@ -5021,7 +5013,7 @@ pub fn parse_operator(working_set: &mut StateWorkingSet, span: Span) -> Expressi
|
||||||
b"=~" | b"like" => Operator::Comparison(Comparison::RegexMatch),
|
b"=~" | b"like" => Operator::Comparison(Comparison::RegexMatch),
|
||||||
b"!~" | b"not-like" => Operator::Comparison(Comparison::NotRegexMatch),
|
b"!~" | b"not-like" => Operator::Comparison(Comparison::NotRegexMatch),
|
||||||
b"+" => Operator::Math(Math::Plus),
|
b"+" => Operator::Math(Math::Plus),
|
||||||
b"++" => Operator::Math(Math::Append),
|
b"++" => Operator::Math(Math::Concat),
|
||||||
b"-" => Operator::Math(Math::Minus),
|
b"-" => Operator::Math(Math::Minus),
|
||||||
b"*" => Operator::Math(Math::Multiply),
|
b"*" => Operator::Math(Math::Multiply),
|
||||||
b"/" => Operator::Math(Math::Divide),
|
b"/" => Operator::Math(Math::Divide),
|
||||||
|
|
|
@ -29,8 +29,6 @@ pub fn type_compatible(lhs: &Type, rhs: &Type) -> bool {
|
||||||
|
|
||||||
match (lhs, rhs) {
|
match (lhs, rhs) {
|
||||||
(Type::List(c), Type::List(d)) => type_compatible(c, d),
|
(Type::List(c), Type::List(d)) => type_compatible(c, d),
|
||||||
(Type::ListStream, Type::List(_)) => true,
|
|
||||||
(Type::List(_), Type::ListStream) => true,
|
|
||||||
(Type::List(c), Type::Table(table_fields)) => {
|
(Type::List(c), Type::Table(table_fields)) => {
|
||||||
if matches!(**c, Type::Any) {
|
if matches!(**c, Type::Any) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -132,7 +130,7 @@ pub fn math_result_type(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Operator::Math(Math::Append) => check_append(working_set, lhs, rhs, op),
|
Operator::Math(Math::Concat) => check_concat(working_set, lhs, rhs, op),
|
||||||
Operator::Math(Math::Minus) => match (&lhs.ty, &rhs.ty) {
|
Operator::Math(Math::Minus) => match (&lhs.ty, &rhs.ty) {
|
||||||
(Type::Int, Type::Int) => (Type::Int, None),
|
(Type::Int, Type::Int) => (Type::Int, None),
|
||||||
(Type::Float, Type::Int) => (Type::Float, None),
|
(Type::Float, Type::Int) => (Type::Float, None),
|
||||||
|
@ -935,8 +933,8 @@ pub fn math_result_type(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Operator::Assignment(Assignment::AppendAssign) => {
|
Operator::Assignment(Assignment::ConcatAssign) => {
|
||||||
check_append(working_set, lhs, rhs, op)
|
check_concat(working_set, lhs, rhs, op)
|
||||||
}
|
}
|
||||||
Operator::Assignment(_) => match (&lhs.ty, &rhs.ty) {
|
Operator::Assignment(_) => match (&lhs.ty, &rhs.ty) {
|
||||||
(x, y) if x == y => (Type::Nothing, None),
|
(x, y) if x == y => (Type::Nothing, None),
|
||||||
|
@ -1085,7 +1083,7 @@ pub fn check_block_input_output(working_set: &StateWorkingSet, block: &Block) ->
|
||||||
output_errors
|
output_errors
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_append(
|
fn check_concat(
|
||||||
working_set: &mut StateWorkingSet,
|
working_set: &mut StateWorkingSet,
|
||||||
lhs: &Expression,
|
lhs: &Expression,
|
||||||
rhs: &Expression,
|
rhs: &Expression,
|
||||||
|
@ -1099,23 +1097,17 @@ fn check_append(
|
||||||
(Type::List(Box::new(Type::Any)), None)
|
(Type::List(Box::new(Type::Any)), None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(Type::List(a), b) | (b, Type::List(a)) => {
|
|
||||||
if a == &Box::new(b.clone()) {
|
|
||||||
(Type::List(a.clone()), None)
|
|
||||||
} else {
|
|
||||||
(Type::List(Box::new(Type::Any)), None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(Type::Table(a), Type::Table(_)) => (Type::Table(a.clone()), None),
|
(Type::Table(a), Type::Table(_)) => (Type::Table(a.clone()), None),
|
||||||
(Type::String, Type::String) => (Type::String, None),
|
(Type::String, Type::String) => (Type::String, None),
|
||||||
(Type::Binary, Type::Binary) => (Type::Binary, None),
|
(Type::Binary, Type::Binary) => (Type::Binary, None),
|
||||||
(Type::Any, _) | (_, Type::Any) => (Type::Any, None),
|
(Type::Any, _) | (_, Type::Any) => (Type::Any, None),
|
||||||
(Type::Table(_) | Type::String | Type::Binary, _) => {
|
(Type::Table(_) | Type::List(_) | Type::String | Type::Binary, _)
|
||||||
|
| (_, Type::Table(_) | Type::List(_) | Type::String | Type::Binary) => {
|
||||||
*op = Expression::garbage(working_set, op.span);
|
*op = Expression::garbage(working_set, op.span);
|
||||||
(
|
(
|
||||||
Type::Any,
|
Type::Any,
|
||||||
Some(ParseError::UnsupportedOperationRHS(
|
Some(ParseError::UnsupportedOperationRHS(
|
||||||
"append".into(),
|
"concatenation".into(),
|
||||||
op.span,
|
op.span,
|
||||||
lhs.span,
|
lhs.span,
|
||||||
lhs.ty.clone(),
|
lhs.ty.clone(),
|
||||||
|
@ -1129,7 +1121,7 @@ fn check_append(
|
||||||
(
|
(
|
||||||
Type::Any,
|
Type::Any,
|
||||||
Some(ParseError::UnsupportedOperationLHS(
|
Some(ParseError::UnsupportedOperationLHS(
|
||||||
"append".into(),
|
"concatenation".into(),
|
||||||
op.span,
|
op.span,
|
||||||
lhs.span,
|
lhs.span,
|
||||||
lhs.ty.clone(),
|
lhs.ty.clone(),
|
||||||
|
|
|
@ -1485,7 +1485,7 @@ fn prepare_plugin_call_custom_value_op() {
|
||||||
span,
|
span,
|
||||||
},
|
},
|
||||||
CustomValueOp::Operation(
|
CustomValueOp::Operation(
|
||||||
Operator::Math(Math::Append).into_spanned(span),
|
Operator::Math(Math::Concat).into_spanned(span),
|
||||||
cv_ok_val.clone(),
|
cv_ok_val.clone(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1498,7 +1498,7 @@ fn prepare_plugin_call_custom_value_op() {
|
||||||
span,
|
span,
|
||||||
},
|
},
|
||||||
CustomValueOp::Operation(
|
CustomValueOp::Operation(
|
||||||
Operator::Math(Math::Append).into_spanned(span),
|
Operator::Math(Math::Concat).into_spanned(span),
|
||||||
cv_bad_val.clone(),
|
cv_bad_val.clone(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -21,7 +21,7 @@ nu-plugin-core = { path = "../nu-plugin-core", version = "0.100.1", default-feat
|
||||||
nu-utils = { path = "../nu-utils", version = "0.100.1" }
|
nu-utils = { path = "../nu-utils", version = "0.100.1" }
|
||||||
|
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
thiserror = "1.0"
|
thiserror = "2.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
|
|
@ -36,7 +36,7 @@ num-format = { workspace = true }
|
||||||
rmp-serde = { workspace = true, optional = true }
|
rmp-serde = { workspace = true, optional = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
thiserror = "1.0"
|
thiserror = "2.0"
|
||||||
typetag = "0.2"
|
typetag = "0.2"
|
||||||
os_pipe = { workspace = true, features = ["io_safety"] }
|
os_pipe = { workspace = true, features = ["io_safety"] }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
|
|
|
@ -24,7 +24,7 @@ pub enum Comparison {
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum Math {
|
pub enum Math {
|
||||||
Plus,
|
Plus,
|
||||||
Append,
|
Concat,
|
||||||
Minus,
|
Minus,
|
||||||
Multiply,
|
Multiply,
|
||||||
Divide,
|
Divide,
|
||||||
|
@ -53,7 +53,7 @@ pub enum Bits {
|
||||||
pub enum Assignment {
|
pub enum Assignment {
|
||||||
Assign,
|
Assign,
|
||||||
PlusAssign,
|
PlusAssign,
|
||||||
AppendAssign,
|
ConcatAssign,
|
||||||
MinusAssign,
|
MinusAssign,
|
||||||
MultiplyAssign,
|
MultiplyAssign,
|
||||||
DivideAssign,
|
DivideAssign,
|
||||||
|
@ -90,7 +90,7 @@ impl Operator {
|
||||||
| Self::Comparison(Comparison::NotEqual)
|
| Self::Comparison(Comparison::NotEqual)
|
||||||
| Self::Comparison(Comparison::In)
|
| Self::Comparison(Comparison::In)
|
||||||
| Self::Comparison(Comparison::NotIn)
|
| Self::Comparison(Comparison::NotIn)
|
||||||
| Self::Math(Math::Append) => 80,
|
| Self::Math(Math::Concat) => 80,
|
||||||
Self::Bits(Bits::BitAnd) => 75,
|
Self::Bits(Bits::BitAnd) => 75,
|
||||||
Self::Bits(Bits::BitXor) => 70,
|
Self::Bits(Bits::BitXor) => 70,
|
||||||
Self::Bits(Bits::BitOr) => 60,
|
Self::Bits(Bits::BitOr) => 60,
|
||||||
|
@ -107,7 +107,7 @@ impl Display for Operator {
|
||||||
match self {
|
match self {
|
||||||
Operator::Assignment(Assignment::Assign) => write!(f, "="),
|
Operator::Assignment(Assignment::Assign) => write!(f, "="),
|
||||||
Operator::Assignment(Assignment::PlusAssign) => write!(f, "+="),
|
Operator::Assignment(Assignment::PlusAssign) => write!(f, "+="),
|
||||||
Operator::Assignment(Assignment::AppendAssign) => write!(f, "++="),
|
Operator::Assignment(Assignment::ConcatAssign) => write!(f, "++="),
|
||||||
Operator::Assignment(Assignment::MinusAssign) => write!(f, "-="),
|
Operator::Assignment(Assignment::MinusAssign) => write!(f, "-="),
|
||||||
Operator::Assignment(Assignment::MultiplyAssign) => write!(f, "*="),
|
Operator::Assignment(Assignment::MultiplyAssign) => write!(f, "*="),
|
||||||
Operator::Assignment(Assignment::DivideAssign) => write!(f, "/="),
|
Operator::Assignment(Assignment::DivideAssign) => write!(f, "/="),
|
||||||
|
@ -124,7 +124,7 @@ impl Display for Operator {
|
||||||
Operator::Comparison(Comparison::In) => write!(f, "in"),
|
Operator::Comparison(Comparison::In) => write!(f, "in"),
|
||||||
Operator::Comparison(Comparison::NotIn) => write!(f, "not-in"),
|
Operator::Comparison(Comparison::NotIn) => write!(f, "not-in"),
|
||||||
Operator::Math(Math::Plus) => write!(f, "+"),
|
Operator::Math(Math::Plus) => write!(f, "+"),
|
||||||
Operator::Math(Math::Append) => write!(f, "++"),
|
Operator::Math(Math::Concat) => write!(f, "++"),
|
||||||
Operator::Math(Math::Minus) => write!(f, "-"),
|
Operator::Math(Math::Minus) => write!(f, "-"),
|
||||||
Operator::Math(Math::Multiply) => write!(f, "*"),
|
Operator::Math(Math::Multiply) => write!(f, "*"),
|
||||||
Operator::Math(Math::Divide) => write!(f, "/"),
|
Operator::Math(Math::Divide) => write!(f, "/"),
|
||||||
|
|
|
@ -698,7 +698,7 @@ impl EngineState {
|
||||||
|
|
||||||
pub fn find_commands_by_predicate(
|
pub fn find_commands_by_predicate(
|
||||||
&self,
|
&self,
|
||||||
predicate: impl Fn(&[u8]) -> bool,
|
mut predicate: impl FnMut(&[u8]) -> bool,
|
||||||
ignore_deprecated: bool,
|
ignore_deprecated: bool,
|
||||||
) -> Vec<(Vec<u8>, Option<String>, CommandType)> {
|
) -> Vec<(Vec<u8>, Option<String>, CommandType)> {
|
||||||
let mut output = vec![];
|
let mut output = vec![];
|
||||||
|
|
|
@ -724,7 +724,7 @@ impl<'a> StateWorkingSet<'a> {
|
||||||
|
|
||||||
pub fn find_commands_by_predicate(
|
pub fn find_commands_by_predicate(
|
||||||
&self,
|
&self,
|
||||||
predicate: impl Fn(&[u8]) -> bool,
|
mut predicate: impl FnMut(&[u8]) -> bool,
|
||||||
ignore_deprecated: bool,
|
ignore_deprecated: bool,
|
||||||
) -> Vec<(Vec<u8>, Option<String>, CommandType)> {
|
) -> Vec<(Vec<u8>, Option<String>, CommandType)> {
|
||||||
let mut output = vec![];
|
let mut output = vec![];
|
||||||
|
|
|
@ -1330,14 +1330,15 @@ This is an internal Nushell error, please file an issue https://github.com/nushe
|
||||||
span: Span,
|
span: Span,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("Deprecated: {old_command}")]
|
#[error("{deprecated} is deprecated and will be removed in a future release")]
|
||||||
#[diagnostic(help("for more info see {url}"))]
|
#[diagnostic()]
|
||||||
Deprecated {
|
Deprecated {
|
||||||
old_command: String,
|
deprecated: &'static str,
|
||||||
new_suggestion: String,
|
suggestion: &'static str,
|
||||||
#[label("`{old_command}` is deprecated and will be removed in a future release. Please {new_suggestion} instead.")]
|
#[label("{deprecated} is deprecated. {suggestion}")]
|
||||||
span: Span,
|
span: Span,
|
||||||
url: String,
|
#[help]
|
||||||
|
help: Option<&'static str>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Invalid glob pattern
|
/// Invalid glob pattern
|
||||||
|
|
|
@ -238,7 +238,7 @@ pub trait Eval {
|
||||||
Math::Minus => lhs.sub(op_span, &rhs, expr_span),
|
Math::Minus => lhs.sub(op_span, &rhs, expr_span),
|
||||||
Math::Multiply => lhs.mul(op_span, &rhs, expr_span),
|
Math::Multiply => lhs.mul(op_span, &rhs, expr_span),
|
||||||
Math::Divide => lhs.div(op_span, &rhs, expr_span),
|
Math::Divide => lhs.div(op_span, &rhs, expr_span),
|
||||||
Math::Append => lhs.append(op_span, &rhs, expr_span),
|
Math::Concat => lhs.concat(op_span, &rhs, expr_span),
|
||||||
Math::Modulo => lhs.modulo(op_span, &rhs, expr_span),
|
Math::Modulo => lhs.modulo(op_span, &rhs, expr_span),
|
||||||
Math::FloorDivision => lhs.floor_div(op_span, &rhs, expr_span),
|
Math::FloorDivision => lhs.floor_div(op_span, &rhs, expr_span),
|
||||||
Math::Pow => lhs.pow(op_span, &rhs, expr_span),
|
Math::Pow => lhs.pow(op_span, &rhs, expr_span),
|
||||||
|
|
|
@ -109,7 +109,7 @@ impl PipelineData {
|
||||||
/// than would be returned by [`Value::get_type()`] on the result of
|
/// than would be returned by [`Value::get_type()`] on the result of
|
||||||
/// [`.into_value()`](Self::into_value).
|
/// [`.into_value()`](Self::into_value).
|
||||||
///
|
///
|
||||||
/// Specifically, a `ListStream` results in [`list stream`](Type::ListStream) rather than
|
/// Specifically, a `ListStream` results in `list<any>` rather than
|
||||||
/// the fully complete [`list`](Type::List) type (which would require knowing the contents),
|
/// the fully complete [`list`](Type::List) type (which would require knowing the contents),
|
||||||
/// and a `ByteStream` with [unknown](crate::ByteStreamType::Unknown) type results in
|
/// and a `ByteStream` with [unknown](crate::ByteStreamType::Unknown) type results in
|
||||||
/// [`any`](Type::Any) rather than [`string`](Type::String) or [`binary`](Type::Binary).
|
/// [`any`](Type::Any) rather than [`string`](Type::String) or [`binary`](Type::Binary).
|
||||||
|
@ -117,7 +117,7 @@ impl PipelineData {
|
||||||
match self {
|
match self {
|
||||||
PipelineData::Empty => Type::Nothing,
|
PipelineData::Empty => Type::Nothing,
|
||||||
PipelineData::Value(value, _) => value.get_type(),
|
PipelineData::Value(value, _) => value.get_type(),
|
||||||
PipelineData::ListStream(_, _) => Type::ListStream,
|
PipelineData::ListStream(_, _) => Type::list(Type::Any),
|
||||||
PipelineData::ByteStream(stream, _) => stream.type_().into(),
|
PipelineData::ByteStream(stream, _) => stream.type_().into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -203,7 +203,7 @@ impl PipelineData {
|
||||||
) -> Result<Self, ShellError> {
|
) -> Result<Self, ShellError> {
|
||||||
match stack.pipe_stdout().unwrap_or(&OutDest::Inherit) {
|
match stack.pipe_stdout().unwrap_or(&OutDest::Inherit) {
|
||||||
OutDest::Print => {
|
OutDest::Print => {
|
||||||
self.print(engine_state, stack, false, false)?;
|
self.print_table(engine_state, stack, false, false)?;
|
||||||
Ok(Self::Empty)
|
Ok(Self::Empty)
|
||||||
}
|
}
|
||||||
OutDest::Pipe | OutDest::PipeSeparate => Ok(self),
|
OutDest::Pipe | OutDest::PipeSeparate => Ok(self),
|
||||||
|
@ -534,11 +534,14 @@ impl PipelineData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Consume and print self data immediately.
|
/// Consume and print self data immediately, formatted using table command.
|
||||||
|
///
|
||||||
|
/// This does not respect the display_output hook. If a value is being printed out by a command,
|
||||||
|
/// this function should be used. Otherwise, `nu_cli::util::print_pipeline` should be preferred.
|
||||||
///
|
///
|
||||||
/// `no_newline` controls if we need to attach newline character to output.
|
/// `no_newline` controls if we need to attach newline character to output.
|
||||||
/// `to_stderr` controls if data is output to stderr, when the value is false, the data is output to stdout.
|
/// `to_stderr` controls if data is output to stderr, when the value is false, the data is output to stdout.
|
||||||
pub fn print(
|
pub fn print_table(
|
||||||
self,
|
self,
|
||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
stack: &mut Stack,
|
stack: &mut Stack,
|
||||||
|
|
|
@ -23,7 +23,6 @@ pub enum Type {
|
||||||
Float,
|
Float,
|
||||||
Int,
|
Int,
|
||||||
List(Box<Type>),
|
List(Box<Type>),
|
||||||
ListStream,
|
|
||||||
#[default]
|
#[default]
|
||||||
Nothing,
|
Nothing,
|
||||||
Number,
|
Number,
|
||||||
|
@ -121,7 +120,6 @@ impl Type {
|
||||||
Type::Nothing => SyntaxShape::Nothing,
|
Type::Nothing => SyntaxShape::Nothing,
|
||||||
Type::Record(entries) => SyntaxShape::Record(mk_shape(entries)),
|
Type::Record(entries) => SyntaxShape::Record(mk_shape(entries)),
|
||||||
Type::Table(columns) => SyntaxShape::Table(mk_shape(columns)),
|
Type::Table(columns) => SyntaxShape::Table(mk_shape(columns)),
|
||||||
Type::ListStream => SyntaxShape::List(Box::new(SyntaxShape::Any)),
|
|
||||||
Type::Any => SyntaxShape::Any,
|
Type::Any => SyntaxShape::Any,
|
||||||
Type::Error => SyntaxShape::Any,
|
Type::Error => SyntaxShape::Any,
|
||||||
Type::Binary => SyntaxShape::Binary,
|
Type::Binary => SyntaxShape::Binary,
|
||||||
|
@ -151,7 +149,6 @@ impl Type {
|
||||||
Type::Nothing => String::from("nothing"),
|
Type::Nothing => String::from("nothing"),
|
||||||
Type::Number => String::from("number"),
|
Type::Number => String::from("number"),
|
||||||
Type::String => String::from("string"),
|
Type::String => String::from("string"),
|
||||||
Type::ListStream => String::from("list-stream"),
|
|
||||||
Type::Any => String::from("any"),
|
Type::Any => String::from("any"),
|
||||||
Type::Error => String::from("error"),
|
Type::Error => String::from("error"),
|
||||||
Type::Binary => String::from("binary"),
|
Type::Binary => String::from("binary"),
|
||||||
|
@ -209,7 +206,6 @@ impl Display for Type {
|
||||||
Type::Nothing => write!(f, "nothing"),
|
Type::Nothing => write!(f, "nothing"),
|
||||||
Type::Number => write!(f, "number"),
|
Type::Number => write!(f, "number"),
|
||||||
Type::String => write!(f, "string"),
|
Type::String => write!(f, "string"),
|
||||||
Type::ListStream => write!(f, "list-stream"),
|
|
||||||
Type::Any => write!(f, "any"),
|
Type::Any => write!(f, "any"),
|
||||||
Type::Error => write!(f, "error"),
|
Type::Error => write!(f, "error"),
|
||||||
Type::Binary => write!(f, "binary"),
|
Type::Binary => write!(f, "binary"),
|
||||||
|
|
|
@ -2503,34 +2503,20 @@ impl Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn append(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
|
pub fn concat(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
|
||||||
match (self, rhs) {
|
match (self, rhs) {
|
||||||
(Value::List { vals: lhs, .. }, Value::List { vals: rhs, .. }) => {
|
(Value::List { vals: lhs, .. }, Value::List { vals: rhs, .. }) => {
|
||||||
let mut lhs = lhs.clone();
|
Ok(Value::list([lhs.as_slice(), rhs.as_slice()].concat(), span))
|
||||||
let mut rhs = rhs.clone();
|
|
||||||
lhs.append(&mut rhs);
|
|
||||||
Ok(Value::list(lhs, span))
|
|
||||||
}
|
|
||||||
(Value::List { vals: lhs, .. }, val) => {
|
|
||||||
let mut lhs = lhs.clone();
|
|
||||||
lhs.push(val.clone());
|
|
||||||
Ok(Value::list(lhs, span))
|
|
||||||
}
|
|
||||||
(val, Value::List { vals: rhs, .. }) => {
|
|
||||||
let mut rhs = rhs.clone();
|
|
||||||
rhs.insert(0, val.clone());
|
|
||||||
Ok(Value::list(rhs, span))
|
|
||||||
}
|
}
|
||||||
(Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => {
|
(Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => {
|
||||||
Ok(Value::string(lhs.to_string() + rhs, span))
|
Ok(Value::string([lhs.as_str(), rhs.as_str()].join(""), span))
|
||||||
}
|
|
||||||
(Value::Binary { val: lhs, .. }, Value::Binary { val: rhs, .. }) => {
|
|
||||||
let mut val = lhs.clone();
|
|
||||||
val.extend(rhs);
|
|
||||||
Ok(Value::binary(val, span))
|
|
||||||
}
|
}
|
||||||
|
(Value::Binary { val: lhs, .. }, Value::Binary { val: rhs, .. }) => Ok(Value::binary(
|
||||||
|
[lhs.as_slice(), rhs.as_slice()].concat(),
|
||||||
|
span,
|
||||||
|
)),
|
||||||
(Value::Custom { val: lhs, .. }, rhs) => {
|
(Value::Custom { val: lhs, .. }, rhs) => {
|
||||||
lhs.operation(self.span(), Operator::Math(Math::Append), op, rhs)
|
lhs.operation(self.span(), Operator::Math(Math::Concat), op, rhs)
|
||||||
}
|
}
|
||||||
_ => Err(ShellError::OperatorMismatch {
|
_ => Err(ShellError::OperatorMismatch {
|
||||||
op_span: op,
|
op_span: op,
|
||||||
|
|
|
@ -35,7 +35,7 @@ fn config_affected_when_mutated() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn config_affected_when_deep_mutated() {
|
fn config_affected_when_deep_mutated() {
|
||||||
let actual = nu!(cwd: "crates/nu-utils/src/sample_config", nu_repl_code(&[
|
let actual = nu!(cwd: "crates/nu-utils/src/default_files", nu_repl_code(&[
|
||||||
r#"source default_config.nu"#,
|
r#"source default_config.nu"#,
|
||||||
r#"$env.config.filesize.metric = true"#,
|
r#"$env.config.filesize.metric = true"#,
|
||||||
r#"20mib | into string"#]));
|
r#"20mib | into string"#]));
|
||||||
|
@ -45,7 +45,7 @@ fn config_affected_when_deep_mutated() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn config_add_unsupported_key() {
|
fn config_add_unsupported_key() {
|
||||||
let actual = nu!(cwd: "crates/nu-utils/src/sample_config", nu_repl_code(&[
|
let actual = nu!(cwd: "crates/nu-utils/src/default_files", nu_repl_code(&[
|
||||||
r#"source default_config.nu"#,
|
r#"source default_config.nu"#,
|
||||||
r#"$env.config.foo = 2"#,
|
r#"$env.config.foo = 2"#,
|
||||||
r#";"#]));
|
r#";"#]));
|
||||||
|
@ -57,7 +57,7 @@ fn config_add_unsupported_key() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn config_add_unsupported_type() {
|
fn config_add_unsupported_type() {
|
||||||
let actual = nu!(cwd: "crates/nu-utils/src/sample_config", nu_repl_code(&[r#"source default_config.nu"#,
|
let actual = nu!(cwd: "crates/nu-utils/src/default_files", nu_repl_code(&[r#"source default_config.nu"#,
|
||||||
r#"$env.config.ls = '' "#,
|
r#"$env.config.ls = '' "#,
|
||||||
r#";"#]));
|
r#";"#]));
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ fn config_add_unsupported_type() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn config_add_unsupported_value() {
|
fn config_add_unsupported_value() {
|
||||||
let actual = nu!(cwd: "crates/nu-utils/src/sample_config", nu_repl_code(&[r#"source default_config.nu"#,
|
let actual = nu!(cwd: "crates/nu-utils/src/default_files", nu_repl_code(&[r#"source default_config.nu"#,
|
||||||
r#"$env.config.history.file_format = ''"#,
|
r#"$env.config.history.file_format = ''"#,
|
||||||
r#";"#]));
|
r#";"#]));
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ fn config_add_unsupported_value() {
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "Figure out how to make test_bins::nu_repl() continue execution after shell errors"]
|
#[ignore = "Figure out how to make test_bins::nu_repl() continue execution after shell errors"]
|
||||||
fn config_unsupported_key_reverted() {
|
fn config_unsupported_key_reverted() {
|
||||||
let actual = nu!(cwd: "crates/nu-utils/src/sample_config", nu_repl_code(&[r#"source default_config.nu"#,
|
let actual = nu!(cwd: "crates/nu-utils/src/default_files", nu_repl_code(&[r#"source default_config.nu"#,
|
||||||
r#"$env.config.foo = 1"#,
|
r#"$env.config.foo = 1"#,
|
||||||
r#"'foo' in $env.config"#]));
|
r#"'foo' in $env.config"#]));
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ fn config_unsupported_key_reverted() {
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "Figure out how to make test_bins::nu_repl() continue execution after shell errors"]
|
#[ignore = "Figure out how to make test_bins::nu_repl() continue execution after shell errors"]
|
||||||
fn config_unsupported_type_reverted() {
|
fn config_unsupported_type_reverted() {
|
||||||
let actual = nu!(cwd: "crates/nu-utils/src/sample_config", nu_repl_code(&[r#" source default_config.nu"#,
|
let actual = nu!(cwd: "crates/nu-utils/src/default_files", nu_repl_code(&[r#" source default_config.nu"#,
|
||||||
r#"$env.config.ls = ''"#,
|
r#"$env.config.ls = ''"#,
|
||||||
r#"$env.config.ls | describe"#]));
|
r#"$env.config.ls | describe"#]));
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ fn config_unsupported_type_reverted() {
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "Figure out how to make test_bins::nu_repl() continue execution after errors"]
|
#[ignore = "Figure out how to make test_bins::nu_repl() continue execution after errors"]
|
||||||
fn config_unsupported_value_reverted() {
|
fn config_unsupported_value_reverted() {
|
||||||
let actual = nu!(cwd: "crates/nu-utils/src/sample_config", nu_repl_code(&[r#" source default_config.nu"#,
|
let actual = nu!(cwd: "crates/nu-utils/src/default_files", nu_repl_code(&[r#" source default_config.nu"#,
|
||||||
r#"$env.config.history.file_format = 'plaintext'"#,
|
r#"$env.config.history.file_format = 'plaintext'"#,
|
||||||
r#"$env.config.history.file_format = ''"#,
|
r#"$env.config.history.file_format = ''"#,
|
||||||
r#"$env.config.history.file_format | to json"#]));
|
r#"$env.config.history.file_format | to json"#]));
|
||||||
|
|
|
@ -69,7 +69,7 @@ fn fancy_default_errors() {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
actual.err,
|
actual.err,
|
||||||
"Error: \u{1b}[31m×\u{1b}[0m oh no!\n ╭─[\u{1b}[36;1;4mline1\u{1b}[0m:1:13]\n \u{1b}[2m1\u{1b}[0m │ force_error \"My error\"\n · \u{1b}[35;1m ─────┬────\u{1b}[0m\n · \u{1b}[35;1m╰── \u{1b}[35;1mhere's the error\u{1b}[0m\u{1b}[0m\n ╰────\n\n\n"
|
"Error: \n \u{1b}[31m×\u{1b}[0m oh no!\n ╭─[\u{1b}[36;1;4mline1:1:13\u{1b}[0m]\n \u{1b}[2m1\u{1b}[0m │ force_error \"My error\"\n · \u{1b}[35;1m ─────┬────\u{1b}[0m\n · \u{1b}[35;1m╰── \u{1b}[35;1mhere's the error\u{1b}[0m\u{1b}[0m\n ╰────\n\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,7 @@ pub fn load_standard_library(
|
||||||
("mod.nu", "std/math", include_str!("../std/math/mod.nu")),
|
("mod.nu", "std/math", include_str!("../std/math/mod.nu")),
|
||||||
("mod.nu", "std/util", include_str!("../std/util/mod.nu")),
|
("mod.nu", "std/util", include_str!("../std/util/mod.nu")),
|
||||||
("mod.nu", "std/xml", include_str!("../std/xml/mod.nu")),
|
("mod.nu", "std/xml", include_str!("../std/xml/mod.nu")),
|
||||||
|
("mod.nu", "std/config", include_str!("../std/config/mod.nu")),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (filename, std_subdir_name, content) in std_submodules.drain(..) {
|
for (filename, std_subdir_name, content) in std_submodules.drain(..) {
|
||||||
|
|
139
crates/nu-std/std/config/mod.nu
Normal file
139
crates/nu-std/std/config/mod.nu
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,7 +43,7 @@ export def --env add [
|
||||||
}
|
}
|
||||||
|
|
||||||
# Make the next directory on the list the active directory.
|
# 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.
|
# then cycle to the top of the list.
|
||||||
export def --env next [
|
export def --env next [
|
||||||
N:int = 1 # number of positions to move.
|
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.
|
# 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.
|
# then cycle to the end of the list.
|
||||||
export def --env prev [
|
export def --env prev [
|
||||||
N:int = 1 # number of positions to move.
|
N:int = 1 # number of positions to move.
|
||||||
|
|
|
@ -28,3 +28,13 @@ export def "to ndjson" []: any -> string {
|
||||||
export def "to jsonl" []: any -> string {
|
export def "to jsonl" []: any -> string {
|
||||||
each { to json --raw } | to text
|
each { to json --raw } | to text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Convert from NDNUON (newline-delimited NUON), to structured data
|
||||||
|
export def "from ndnuon" []: [string -> any] {
|
||||||
|
lines | each { from nuon }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert structured data to NDNUON, i.e. newline-delimited NUON
|
||||||
|
export def "to ndnuon" []: [any -> string] {
|
||||||
|
each { to nuon --raw } | to text
|
||||||
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ def get-all-operators [] { return [
|
||||||
|
|
||||||
[Assignment, =, Assign, "Assigns a value to a variable.", 10]
|
[Assignment, =, Assign, "Assigns a value to a variable.", 10]
|
||||||
[Assignment, +=, PlusAssign, "Adds a value to a variable.", 10]
|
[Assignment, +=, PlusAssign, "Adds a value to a variable.", 10]
|
||||||
[Assignment, ++=, AppendAssign, "Appends a list or a value to a variable.", 10]
|
[Assignment, ++=, ConcatAssign, "Concatenate two lists, two strings, or two binary values.", 10]
|
||||||
[Assignment, -=, MinusAssign, "Subtracts a value from a variable.", 10]
|
[Assignment, -=, MinusAssign, "Subtracts a value from a variable.", 10]
|
||||||
[Assignment, *=, MultiplyAssign, "Multiplies a variable by a value.", 10]
|
[Assignment, *=, MultiplyAssign, "Multiplies a variable by a value.", 10]
|
||||||
[Assignment, /=, DivideAssign, "Divides a variable by a value.", 10]
|
[Assignment, /=, DivideAssign, "Divides a variable by a value.", 10]
|
||||||
|
@ -55,7 +55,7 @@ def get-all-operators [] { return [
|
||||||
[Comparison, ends-with, EndsWith, "Checks if a string ends with another.", 80]
|
[Comparison, ends-with, EndsWith, "Checks if a string ends with another.", 80]
|
||||||
[Comparison, not, UnaryNot, "Negates a value or expression.", 0]
|
[Comparison, not, UnaryNot, "Negates a value or expression.", 0]
|
||||||
[Math, +, Plus, "Adds two values.", 90]
|
[Math, +, Plus, "Adds two values.", 90]
|
||||||
[Math, ++, Append, "Appends two lists or a list and a value.", 80]
|
[Math, ++, Concat, "Concatenate two lists, two strings, or two binary values.", 80]
|
||||||
[Math, -, Minus, "Subtracts two values.", 90]
|
[Math, -, Minus, "Subtracts two values.", 90]
|
||||||
[Math, *, Multiply, "Multiplies two values.", 95]
|
[Math, *, Multiply, "Multiplies two values.", 95]
|
||||||
[Math, /, Divide, "Divides two values.", 95]
|
[Math, /, Divide, "Divides two values.", 95]
|
||||||
|
@ -684,8 +684,7 @@ def build-command-page [command: record] {
|
||||||
] | flatten | str join "\n"
|
] | flatten | str join "\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Show help on commands.
|
def scope-commands [
|
||||||
export def commands [
|
|
||||||
...command: string@"nu-complete list-commands" # the name of command to get help on
|
...command: string@"nu-complete list-commands" # the name of command to get help on
|
||||||
--find (-f): string # string to find in command names and description
|
--find (-f): string # string to find in command names and description
|
||||||
] {
|
] {
|
||||||
|
@ -699,20 +698,35 @@ export def commands [
|
||||||
let found_command = ($commands | where name == $target_command)
|
let found_command = ($commands | where name == $target_command)
|
||||||
|
|
||||||
if ($found_command | is-empty) {
|
if ($found_command | is-empty) {
|
||||||
try {
|
command-not-found-error (metadata $command | get span)
|
||||||
print $"(ansi default_italic)Help pages from external command ($target_command | pretty-cmd):(ansi reset)"
|
} else {
|
||||||
^($env.NU_HELPER? | default "man") $target_command
|
build-command-page ($found_command | get 0)
|
||||||
} catch {
|
|
||||||
command-not-found-error (metadata $command | get span)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
build-command-page ($found_command | get 0)
|
|
||||||
} else {
|
} else {
|
||||||
$commands | select name category description signatures search_terms
|
$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 [] {
|
def pretty-cmd [] {
|
||||||
let cmd = $in
|
let cmd = $in
|
||||||
$"(ansi default_dimmed)(ansi default_italic)($cmd)(ansi reset)"
|
$"(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 target_item = ($item | str join " ")
|
||||||
|
|
||||||
let commands = (try { commands $target_item --find $find })
|
let commands = (try { scope-commands $target_item --find $find })
|
||||||
if not ($commands | is-empty) { return $commands }
|
if not ($commands | is-empty) { return $commands }
|
||||||
|
|
||||||
let aliases = (try { aliases $target_item --find $find })
|
let aliases = (try { aliases $target_item --find $find })
|
||||||
|
@ -776,13 +790,7 @@ You can also learn more at (ansi default_italic)(ansi light_cyan_underline)https
|
||||||
print -e $"No help results found mentioning: ($find)"
|
print -e $"No help results found mentioning: ($find)"
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
# use external tool (e.g: `man`) to search help for $target_item
|
||||||
let span = (metadata $item | get span)
|
# the stdout and stderr of external tool will follow `main` call.
|
||||||
error make {
|
external-commands $target_item
|
||||||
msg: ("std::help::item_not_found" | error-fmt)
|
|
||||||
label: {
|
|
||||||
text: "item not found"
|
|
||||||
span: $span
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ export module std/iter
|
||||||
export module std/log
|
export module std/log
|
||||||
export module std/math
|
export module std/math
|
||||||
export module std/xml
|
export module std/xml
|
||||||
|
export module std/config
|
||||||
|
|
||||||
# Load main dirs command and all subcommands
|
# Load main dirs command and all subcommands
|
||||||
export use std/dirs main
|
export use std/dirs main
|
||||||
|
|
|
@ -2,15 +2,26 @@
|
||||||
use std/assert
|
use std/assert
|
||||||
use std/formats *
|
use std/formats *
|
||||||
|
|
||||||
def test_data_multiline [] {
|
def test_data_multiline [--nuon] {
|
||||||
let lines = [
|
let lines = if $nuon {
|
||||||
"{\"a\":1}",
|
[
|
||||||
"{\"a\":2}",
|
"{a: 1}",
|
||||||
"{\"a\":3}",
|
"{a: 2}",
|
||||||
"{\"a\":4}",
|
"{a: 3}",
|
||||||
"{\"a\":5}",
|
"{a: 4}",
|
||||||
"{\"a\":6}",
|
"{a: 5}",
|
||||||
]
|
"{a: 6}",
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
[
|
||||||
|
"{\"a\":1}",
|
||||||
|
"{\"a\":2}",
|
||||||
|
"{\"a\":3}",
|
||||||
|
"{\"a\":4}",
|
||||||
|
"{\"a\":5}",
|
||||||
|
"{\"a\":6}",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
if $nu.os-info.name == "windows" {
|
if $nu.os-info.name == "windows" {
|
||||||
$lines | str join "\r\n"
|
$lines | str join "\r\n"
|
||||||
|
@ -84,3 +95,36 @@ def to_jsonl_single_object [] {
|
||||||
let expect = "{\"a\":1}"
|
let expect = "{\"a\":1}"
|
||||||
assert equal $result $expect "could not convert to JSONL"
|
assert equal $result $expect "could not convert to JSONL"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
def from_ndnuon_multiple_objects [] {
|
||||||
|
let result = test_data_multiline | from ndnuon
|
||||||
|
let expect = [{a:1},{a:2},{a:3},{a:4},{a:5},{a:6}]
|
||||||
|
assert equal $result $expect "could not convert from NDNUON"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
def from_ndnuon_single_object [] {
|
||||||
|
let result = '{a: 1}' | from ndnuon
|
||||||
|
let expect = [{a:1}]
|
||||||
|
assert equal $result $expect "could not convert from NDNUON"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
def from_ndnuon_invalid_object [] {
|
||||||
|
assert error { '{"a":1' | formats from ndnuon }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
def to_ndnuon_multiple_objects [] {
|
||||||
|
let result = [{a:1},{a:2},{a:3},{a:4},{a:5},{a:6}] | to ndnuon | str trim
|
||||||
|
let expect = test_data_multiline --nuon
|
||||||
|
assert equal $result $expect "could not convert to NDNUON"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
def to_ndnuon_single_object [] {
|
||||||
|
let result = [{a:1}] | to ndnuon | str trim
|
||||||
|
let expect = "{a: 1}"
|
||||||
|
assert equal $result $expect "could not convert to NDNUON"
|
||||||
|
}
|
||||||
|
|
|
@ -1,16 +1,27 @@
|
||||||
# Test std/formats when importing `use std *`
|
# Test std/formats when importing `use std *`
|
||||||
use std *
|
use std *
|
||||||
|
|
||||||
def test_data_multiline [] {
|
def test_data_multiline [--nuon] {
|
||||||
use std *
|
use std *
|
||||||
let lines = [
|
let lines = if $nuon {
|
||||||
"{\"a\":1}",
|
[
|
||||||
"{\"a\":2}",
|
"{a: 1}",
|
||||||
"{\"a\":3}",
|
"{a: 2}",
|
||||||
"{\"a\":4}",
|
"{a: 3}",
|
||||||
"{\"a\":5}",
|
"{a: 4}",
|
||||||
"{\"a\":6}",
|
"{a: 5}",
|
||||||
]
|
"{a: 6}",
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
[
|
||||||
|
"{\"a\":1}",
|
||||||
|
"{\"a\":2}",
|
||||||
|
"{\"a\":3}",
|
||||||
|
"{\"a\":4}",
|
||||||
|
"{\"a\":5}",
|
||||||
|
"{\"a\":6}",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
if $nu.os-info.name == "windows" {
|
if $nu.os-info.name == "windows" {
|
||||||
$lines | str join "\r\n"
|
$lines | str join "\r\n"
|
||||||
|
@ -84,3 +95,36 @@ def to_jsonl_single_object [] {
|
||||||
let expect = "{\"a\":1}"
|
let expect = "{\"a\":1}"
|
||||||
assert equal $result $expect "could not convert to JSONL"
|
assert equal $result $expect "could not convert to JSONL"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
def from_ndnuon_multiple_objects [] {
|
||||||
|
let result = test_data_multiline | formats from ndnuon
|
||||||
|
let expect = [{a:1},{a:2},{a:3},{a:4},{a:5},{a:6}]
|
||||||
|
assert equal $result $expect "could not convert from NDNUON"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
def from_ndnuon_single_object [] {
|
||||||
|
let result = '{a: 1}' | formats from ndnuon
|
||||||
|
let expect = [{a:1}]
|
||||||
|
assert equal $result $expect "could not convert from NDNUON"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
def from_ndnuon_invalid_object [] {
|
||||||
|
assert error { '{"a":1' | formats from ndnuon }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
def to_ndnuon_multiple_objects [] {
|
||||||
|
let result = [{a:1},{a:2},{a:3},{a:4},{a:5},{a:6}] | formats to ndnuon | str trim
|
||||||
|
let expect = test_data_multiline --nuon
|
||||||
|
assert equal $result $expect "could not convert to NDNUON"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
def to_ndnuon_single_object [] {
|
||||||
|
let result = [{a:1}] | formats to ndnuon | str trim
|
||||||
|
let expect = "{a: 1}"
|
||||||
|
assert equal $result $expect "could not convert to NDNUON"
|
||||||
|
}
|
||||||
|
|
|
@ -18,8 +18,12 @@ pub fn create_nu_table_config(
|
||||||
expand: bool,
|
expand: bool,
|
||||||
mode: TableMode,
|
mode: TableMode,
|
||||||
) -> NuTableConfig {
|
) -> NuTableConfig {
|
||||||
let with_footer = (config.table.footer_inheritance && out.with_footer)
|
let mut count_rows = out.table.count_rows();
|
||||||
|| with_footer(config, out.with_header, out.table.count_rows());
|
if config.table.footer_inheritance {
|
||||||
|
count_rows = out.count_rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
let with_footer = with_footer(config, out.with_header, count_rows);
|
||||||
|
|
||||||
NuTableConfig {
|
NuTableConfig {
|
||||||
theme: load_theme(mode),
|
theme: load_theme(mode),
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue