Merge branch 'main' into fix-run-external-quoting

This commit is contained in:
Devyn Cairns 2024-06-19 18:45:18 -07:00
commit 640889e8aa
No known key found for this signature in database
108 changed files with 3508 additions and 1321 deletions

View File

@ -36,7 +36,7 @@ jobs:
- uses: actions/checkout@v4.1.6
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
- name: cargo fmt
run: cargo fmt --all -- --check
@ -69,7 +69,7 @@ jobs:
- uses: actions/checkout@v4.1.6
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
- name: Tests
run: cargo test --workspace --profile ci --exclude nu_plugin_* ${{ matrix.default-flags }}
@ -98,7 +98,7 @@ jobs:
- uses: actions/checkout@v4.1.6
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
- name: Install Nushell
run: cargo install --path . --locked --no-default-features
@ -149,7 +149,7 @@ jobs:
- uses: actions/checkout@v4.1.6
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
- name: Clippy
run: cargo clippy --package nu_plugin_* -- $CLIPPY_OPTIONS

View File

@ -122,7 +122,7 @@ jobs:
echo "targets = ['${{matrix.target}}']" >> rust-toolchain.toml
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
# WARN: Keep the rustflags to prevent from the winget submission error: `CAQuietExec: Error 0xc0000135`
with:
rustflags: ''

View File

@ -69,7 +69,7 @@ jobs:
echo "targets = ['${{matrix.target}}']" >> rust-toolchain.toml
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
# WARN: Keep the rustflags to prevent from the winget submission error: `CAQuietExec: Error 0xc0000135`
with:
cache: false

View File

@ -10,4 +10,4 @@ jobs:
uses: actions/checkout@v4.1.6
- name: Check spelling
uses: crate-ci/typos@v1.22.1
uses: crate-ci/typos@v1.22.7

84
Cargo.lock generated
View File

@ -920,6 +920,15 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@ -1678,9 +1687,9 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "git2"
version = "0.18.3"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70"
checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
dependencies = [
"bitflags 2.5.0",
"libc",
@ -2279,9 +2288,9 @@ dependencies = [
[[package]]
name = "libgit2-sys"
version = "0.16.2+1.7.2"
version = "0.17.0+1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8"
checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224"
dependencies = [
"cc",
"libc",
@ -3011,7 +3020,7 @@ dependencies = [
"uu_mv",
"uu_uname",
"uu_whoami",
"uucore 0.0.25",
"uucore",
"uuid",
"v_htmlescape",
"wax",
@ -3020,6 +3029,17 @@ dependencies = [
"winreg",
]
[[package]]
name = "nu-derive-value"
version = "0.94.3"
dependencies = [
"convert_case",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.60",
]
[[package]]
name = "nu-engine"
version = "0.94.3"
@ -3209,11 +3229,13 @@ dependencies = [
"byte-unit",
"chrono",
"chrono-humanize",
"convert_case",
"fancy-regex",
"indexmap",
"lru",
"miette",
"nix",
"nu-derive-value",
"nu-path",
"nu-system",
"nu-test-support",
@ -6352,93 +6374,77 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uu_cp"
version = "0.0.25"
version = "0.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcbe045dc92209114afdfd366bd18f7b95dbf999f3eaa85ad6dca910b0be3d56"
checksum = "c31fc5c95f7668999e129464a29e9080f69ba01ccf7a0ae43ff2cfdb15baa340"
dependencies = [
"clap",
"filetime",
"indicatif",
"libc",
"quick-error 2.0.1",
"uucore 0.0.26",
"uucore",
"walkdir",
"xattr",
]
[[package]]
name = "uu_mkdir"
version = "0.0.25"
version = "0.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "040aa4584036b2f65e05387b0ea9ac468afce1db325743ce5f350689fd9ce4ae"
checksum = "496d95e0e3121e4d424ba62019eb84a6f1102213ca8ca16c0a2f8c652c7236c3"
dependencies = [
"clap",
"uucore 0.0.26",
"uucore",
]
[[package]]
name = "uu_mktemp"
version = "0.0.25"
version = "0.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f240a99c36d768153874d198c43605a45c86996b576262689a0f18248cc3bc57"
checksum = "a28a0d9744bdc28ceaf13f70b959bacded91aedfd008402d72fa1e3224158653"
dependencies = [
"clap",
"rand",
"tempfile",
"uucore 0.0.26",
"uucore",
]
[[package]]
name = "uu_mv"
version = "0.0.25"
version = "0.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c99fd7c75e6e85553c92537314be3d9a64b4927051aa1608513feea2f933022"
checksum = "53680908b01c5ac3cc0ee8a376de3e51a36dde2c5a5227a115a3d0977cc4539b"
dependencies = [
"clap",
"fs_extra",
"indicatif",
"uucore 0.0.26",
"uucore",
]
[[package]]
name = "uu_uname"
version = "0.0.25"
version = "0.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5951832d73199636bde6c0d61cf960932b3c4450142c290375bc10c7abed6db5"
checksum = "a7f4125fb4f286313bca8f222abaefe39db54d65179ea788c91ebd3162345f4e"
dependencies = [
"clap",
"platform-info",
"uucore 0.0.26",
"uucore",
]
[[package]]
name = "uu_whoami"
version = "0.0.25"
version = "0.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b44166eb6335aeac42744ea368cc4c32d3f2287a4ff765a5ce44d927ab8bb4"
checksum = "7f7b313901a15cfde2d88f434fcd077903d690f73cc36d1cec20f47906960aec"
dependencies = [
"clap",
"libc",
"uucore 0.0.26",
"uucore",
"windows-sys 0.48.0",
]
[[package]]
name = "uucore"
version = "0.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23994a722acb43dbc56877e271c9723f167ae42c4c089f909b2d7dd106c3a9b4"
dependencies = [
"clap",
"glob",
"libc",
"nix",
"once_cell",
"os_display",
"uucore_procs",
"wild",
]
[[package]]
name = "uucore"
version = "0.0.26"

View File

@ -39,6 +39,7 @@ members = [
"crates/nu-lsp",
"crates/nu-pretty-hex",
"crates/nu-protocol",
"crates/nu-derive-value",
"crates/nu-plugin",
"crates/nu-plugin-core",
"crates/nu-plugin-engine",
@ -74,6 +75,7 @@ chardetng = "0.1.17"
chrono = { default-features = false, version = "0.4.34" }
chrono-humanize = "0.2.3"
chrono-tz = "0.8"
convert_case = "0.6"
crossbeam-channel = "0.5.8"
crossterm = "0.27"
csv = "1.3"
@ -123,11 +125,14 @@ pathdiff = "0.2"
percent-encoding = "2"
pretty_assertions = "1.4"
print-positions = "0.6"
proc-macro-error = { version = "1.0", default-features = false }
proc-macro2 = "1.0"
procfs = "0.16.0"
pwd = "1.3"
quick-xml = "0.31.0"
quickcheck = "1.0"
quickcheck_macros = "1.0"
quote = "1.0"
rand = "0.8"
ratatui = "0.26"
rayon = "1.10"
@ -147,6 +152,7 @@ serde_urlencoded = "0.7.1"
serde_yaml = "0.9"
sha2 = "0.10"
strip-ansi-escapes = "0.2.0"
syn = "2.0"
sysinfo = "0.30"
tabled = { version = "0.14.0", default-features = false }
tempfile = "3.10"
@ -159,13 +165,13 @@ unicode-segmentation = "1.11"
unicode-width = "0.1"
ureq = { version = "2.9", default-features = false }
url = "2.2"
uu_cp = "0.0.25"
uu_mkdir = "0.0.25"
uu_mktemp = "0.0.25"
uu_mv = "0.0.25"
uu_whoami = "0.0.25"
uu_uname = "0.0.25"
uucore = "0.0.25"
uu_cp = "0.0.26"
uu_mkdir = "0.0.26"
uu_mktemp = "0.0.26"
uu_mv = "0.0.26"
uu_whoami = "0.0.26"
uu_uname = "0.0.26"
uucore = "0.0.26"
uuid = "1.8.0"
v_htmlescape = "0.15.0"
wax = "0.6"
@ -195,6 +201,7 @@ reedline = { workspace = true, features = ["bashisms", "sqlite"] }
crossterm = { workspace = true }
ctrlc = { workspace = true }
dirs-next = { workspace = true }
log = { workspace = true }
miette = { workspace = true, features = ["fancy-no-backtrace", "fancy"] }
mimalloc = { version = "0.1.42", default-features = false, optional = true }
@ -244,7 +251,6 @@ default = ["default-no-clipboard", "system-clipboard"]
# See https://github.com/nushell/nushell/pull/11535
default-no-clipboard = [
"plugin",
"which-support",
"trash-support",
"sqlite",
"mimalloc",
@ -264,7 +270,6 @@ system-clipboard = [
]
# Stable (Default)
which-support = ["nu-command/which-support", "nu-cmd-lang/which-support"]
trash-support = ["nu-command/trash-support", "nu-cmd-lang/trash-support"]
# SQLite commands for nushell

View File

@ -52,7 +52,7 @@ To use `Nu` in GitHub Action, check [setup-nu](https://github.com/marketplace/ac
Detailed installation instructions can be found in the [installation chapter of the book](https://www.nushell.sh/book/installation.html). Nu is available via many package managers:
[![Packaging status](https://repology.org/badge/vertical-allrepos/nushell.svg)](https://repology.org/project/nushell/versions)
[![Packaging status](https://repology.org/badge/vertical-allrepos/nushell.svg?columns=3)](https://repology.org/project/nushell/versions)
For details about which platforms the Nushell team actively supports, see [our platform support policy](devdocs/PLATFORM_SUPPORT.md).
@ -222,6 +222,7 @@ Please submit an issue or PR to be added to this list.
- [clap](https://github.com/clap-rs/clap/tree/master/clap_complete_nushell)
- [Dorothy](http://github.com/bevry/dorothy)
- [Direnv](https://github.com/direnv/direnv/blob/master/docs/hook.md#nushell)
- [x-cmd](https://x-cmd.com/mod/nu)
## Contributing

View File

@ -47,8 +47,7 @@ fn setup_stack_and_engine_from_command(command: &str) -> (Stack, EngineState) {
&mut engine,
&mut stack,
PipelineData::empty(),
None,
false,
Default::default(),
)
.unwrap();
@ -90,8 +89,7 @@ fn bench_command(
&mut engine,
&mut stack,
PipelineData::empty(),
None,
false,
Default::default(),
)
.unwrap(),
);

View File

@ -1,7 +1,7 @@
use crate::completions::{matches, CompletionOptions};
use nu_ansi_term::Style;
use nu_engine::env_to_string;
use nu_path::home_dir;
use nu_path::{expand_to_real_path, home_dir};
use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet},
Span,
@ -185,9 +185,14 @@ pub fn complete_item(
.map(|p| {
let path = original_cwd.apply(p);
let style = ls_colors.as_ref().map(|lsc| {
lsc.style_for_path_with_metadata(&path, std::fs::symlink_metadata(&path).ok().as_ref())
.map(lscolors::Style::to_nu_ansi_term_style)
.unwrap_or_default()
lsc.style_for_path_with_metadata(
&path,
std::fs::symlink_metadata(expand_to_real_path(&path))
.ok()
.as_ref(),
)
.map(lscolors::Style::to_nu_ansi_term_style)
.unwrap_or_default()
});
(span, escape_path(path, want_directory), style)
})

View File

@ -8,15 +8,45 @@ use nu_protocol::{
};
use std::sync::Arc;
#[derive(Default)]
pub struct EvaluateCommandsOpts {
pub table_mode: Option<Value>,
pub error_style: Option<Value>,
pub no_newline: bool,
}
/// Run a command (or commands) given to us by the user
pub fn evaluate_commands(
commands: &Spanned<String>,
engine_state: &mut EngineState,
stack: &mut Stack,
input: PipelineData,
table_mode: Option<Value>,
no_newline: bool,
opts: EvaluateCommandsOpts,
) -> Result<(), ShellError> {
let EvaluateCommandsOpts {
table_mode,
error_style,
no_newline,
} = opts;
// Handle the configured error style early
if let Some(e_style) = error_style {
match e_style.coerce_str()?.parse() {
Ok(e_style) => {
Arc::make_mut(&mut engine_state.config).error_style = e_style;
}
Err(err) => {
return Err(ShellError::GenericError {
error: "Invalid value for `--error-style`".into(),
msg: err.into(),
span: Some(e_style.span()),
help: None,
inner: vec![],
});
}
}
}
// Translate environment variables from Strings to Values
convert_env_values(engine_state, stack)?;

View File

@ -17,7 +17,7 @@ mod validation;
pub use commands::add_cli_context;
pub use completions::{FileCompletion, NuCompleter, SemanticSuggestion, SuggestionKind};
pub use config_files::eval_config_contents;
pub use eval_cmds::evaluate_commands;
pub use eval_cmds::{evaluate_commands, EvaluateCommandsOpts};
pub use eval_file::evaluate_file;
pub use menus::NuHelpCompleter;
pub use nu_cmd_base::util::get_init_cwd;

View File

@ -75,7 +75,7 @@ const DEFAULT_HELP_MENU: &str = r#"
// Adds all menus to line editor
pub(crate) fn add_menus(
mut line_editor: Reedline,
engine_state: Arc<EngineState>,
engine_state_ref: Arc<EngineState>,
stack: &Stack,
config: &Config,
) -> Result<Reedline, ShellError> {
@ -83,7 +83,7 @@ pub(crate) fn add_menus(
line_editor = line_editor.clear_menus();
for menu in &config.menus {
line_editor = add_menu(line_editor, menu, engine_state.clone(), stack, config)?
line_editor = add_menu(line_editor, menu, engine_state_ref.clone(), stack, config)?
}
// Checking if the default menus have been added from the config file
@ -93,13 +93,16 @@ pub(crate) fn add_menus(
("help_menu", DEFAULT_HELP_MENU),
];
let mut engine_state = (*engine_state_ref).clone();
let mut menu_eval_results = vec![];
for (name, definition) in default_menus {
if !config
.menus
.iter()
.any(|menu| menu.name.to_expanded_string("", config) == name)
{
let (block, _) = {
let (block, delta) = {
let mut working_set = StateWorkingSet::new(&engine_state);
let output = parse(
&mut working_set,
@ -111,15 +114,31 @@ pub(crate) fn add_menus(
(output, working_set.render())
};
engine_state.merge_delta(delta)?;
let mut temp_stack = Stack::new().capture();
let input = PipelineData::Empty;
let res = eval_block::<WithoutDebug>(&engine_state, &mut temp_stack, &block, input)?;
menu_eval_results.push(eval_block::<WithoutDebug>(
&engine_state,
&mut temp_stack,
&block,
input,
)?);
}
}
if let PipelineData::Value(value, None) = res {
for menu in create_menus(&value)? {
line_editor =
add_menu(line_editor, &menu, engine_state.clone(), stack, config)?;
}
let new_engine_state_ref = Arc::new(engine_state);
for res in menu_eval_results.into_iter() {
if let PipelineData::Value(value, None) = res {
for menu in create_menus(&value)? {
line_editor = add_menu(
line_editor,
&menu,
new_engine_state_ref.clone(),
stack,
config,
)?;
}
}
}

View File

@ -763,11 +763,13 @@ fn variables_completions() {
// Test completions for $nu
let suggestions = completer.complete("$nu.", 4);
assert_eq!(15, suggestions.len());
assert_eq!(17, suggestions.len());
let expected: Vec<String> = vec![
"cache-dir".into(),
"config-path".into(),
"current-exe".into(),
"data-dir".into(),
"default-config-dir".into(),
"env-path".into(),
"history-enabled".into(),

View File

@ -194,7 +194,7 @@ pub fn eval_hook(
let Some(follow) = val.get("code") else {
return Err(ShellError::CantFindColumn {
col_name: "code".into(),
span,
span: Some(span),
src_span: span,
});
};

View File

@ -20,13 +20,14 @@ pub fn get_guaranteed_cwd(engine_state: &EngineState, stack: &Stack) -> PathBuf
type MakeRangeError = fn(&str, Span) -> ShellError;
/// Returns a inclusive pair of boundary in given `range`.
pub fn process_range(range: &Range) -> Result<(isize, isize), MakeRangeError> {
match range {
Range::IntRange(range) => {
let start = range.start().try_into().unwrap_or(0);
let end = match range.end() {
Bound::Included(v) => (v + 1) as isize,
Bound::Excluded(v) => v as isize,
Bound::Included(v) => v as isize,
Bound::Excluded(v) => (v - 1) as isize,
Bound::Unbounded => isize::MAX,
};
Ok((start, end))

View File

@ -32,10 +32,6 @@ serde_urlencoded = { workspace = true }
v_htmlescape = { workspace = true }
itertools = { workspace = true }
[features]
extra = ["default"]
default = []
[dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.94.3" }
nu-command = { path = "../nu-command", version = "0.94.3" }

View File

@ -1,5 +1,4 @@
use nu_engine::{command_prelude::*, get_eval_expression};
use nu_parser::parse_expression;
use nu_engine::command_prelude::*;
use nu_protocol::{ast::PathMember, engine::StateWorkingSet, ListStream};
#[derive(Clone)]
@ -57,14 +56,7 @@ impl Command for FormatPattern {
string_span.start + 1,
)?;
format(
input_val,
&ops,
engine_state,
&mut working_set,
stack,
call.head,
)
format(input_val, &ops, engine_state, call.head)
}
}
}
@ -100,8 +92,6 @@ enum FormatOperation {
FixedText(String),
// raw input is something like {column1.column2}
ValueFromColumn(String, Span),
// raw input is something like {$it.column1.column2} or {$var}.
ValueNeedEval(String, Span),
}
/// Given a pattern that is fed into the Format command, we can process it and subdivide it
@ -110,7 +100,6 @@ enum FormatOperation {
/// there without any further processing.
/// FormatOperation::ValueFromColumn contains the name of a column whose values will be
/// formatted according to the input pattern.
/// FormatOperation::ValueNeedEval contains expression which need to eval, it has the following form:
/// "$it.column1.column2" or "$variable"
fn extract_formatting_operations(
input: String,
@ -161,10 +150,17 @@ fn extract_formatting_operations(
if !column_name.is_empty() {
if column_need_eval {
output.push(FormatOperation::ValueNeedEval(
column_name.clone(),
Span::new(span_start + column_span_start, span_start + column_span_end),
));
return Err(ShellError::GenericError {
error: "Removed functionality".into(),
msg: "The ability to use variables ($it) in `format pattern` has been removed."
.into(),
span: Some(error_span),
help: Some(
"You can use other formatting options, such as string interpolation."
.into(),
),
inner: vec![],
});
} else {
output.push(FormatOperation::ValueFromColumn(
column_name.clone(),
@ -185,8 +181,6 @@ fn format(
input_data: Value,
format_operations: &[FormatOperation],
engine_state: &EngineState,
working_set: &mut StateWorkingSet,
stack: &mut Stack,
head_span: Span,
) -> Result<PipelineData, ShellError> {
let data_as_value = input_data;
@ -194,13 +188,7 @@ fn format(
// We can only handle a Record or a List of Records
match data_as_value {
Value::Record { .. } => {
match format_record(
format_operations,
&data_as_value,
engine_state,
working_set,
stack,
) {
match format_record(format_operations, &data_as_value, engine_state) {
Ok(value) => Ok(PipelineData::Value(Value::string(value, head_span), None)),
Err(value) => Err(value),
}
@ -211,13 +199,7 @@ fn format(
for val in vals.iter() {
match val {
Value::Record { .. } => {
match format_record(
format_operations,
val,
engine_state,
working_set,
stack,
) {
match format_record(format_operations, val, engine_state) {
Ok(value) => {
list.push(Value::string(value, head_span));
}
@ -256,12 +238,9 @@ fn format_record(
format_operations: &[FormatOperation],
data_as_value: &Value,
engine_state: &EngineState,
working_set: &mut StateWorkingSet,
stack: &mut Stack,
) -> Result<String, ShellError> {
let config = engine_state.get_config();
let mut output = String::new();
let eval_expression = get_eval_expression(engine_state);
for op in format_operations {
match op {
@ -283,23 +262,6 @@ fn format_record(
Err(se) => return Err(se),
}
}
FormatOperation::ValueNeedEval(_col_name, span) => {
let exp = parse_expression(working_set, &[*span]);
match working_set.parse_errors.first() {
None => {
let parsed_result = eval_expression(engine_state, stack, &exp);
if let Ok(val) = parsed_result {
output.push_str(&val.to_abbreviated_string(config))
}
}
Some(err) => {
return Err(ShellError::TypeMismatch {
err_message: format!("expression is invalid, detail message: {err:?}"),
span: *span,
})
}
}
}
}
}
Ok(output)

View File

@ -25,7 +25,6 @@ shadow-rs = { version = "0.28", default-features = false }
[features]
mimalloc = []
which-support = []
trash-support = []
sqlite = []
static-link-openssl = []

View File

@ -67,8 +67,8 @@ impl Command for Def {
},
Example {
description: "Define a custom wrapper for an external command",
example: r#"def --wrapped my-echo [...rest] { echo $rest }; my-echo spam"#,
result: Some(Value::test_list(vec![Value::test_string("spam")])),
example: r#"def --wrapped my-echo [...rest] { ^echo ...$rest }; my-echo -e 'spam\tspam'"#,
result: Some(Value::test_string("spam\tspam")),
},
]
}

View File

@ -1,5 +1,6 @@
use nu_engine::{command_prelude::*, get_eval_block, get_eval_expression};
use nu_protocol::engine::CommandType;
use nu_protocol::ParseWarning;
#[derive(Clone)]
pub struct For;
@ -30,7 +31,7 @@ impl Command for For {
.required("block", SyntaxShape::Block, "The block to run.")
.switch(
"numbered",
"return a numbered item ($it.index and $it.item)",
"DEPRECATED: return a numbered item ($it.index and $it.item)",
Some('n'),
)
.creates_scope()
@ -78,6 +79,20 @@ impl Command for For {
let value = eval_expression(engine_state, stack, keyword_expr)?;
let numbered = call.has_flag(engine_state, stack, "numbered")?;
if numbered {
nu_protocol::report_error_new(
engine_state,
&ParseWarning::DeprecatedWarning {
old_command: "--numbered/-n".into(),
new_suggestion: "use `enumerate`".into(),
span: call
.get_named_arg("numbered")
.expect("`get_named_arg` found `--numbered` but still failed")
.span,
url: "See `help for` examples".into(),
},
);
}
let ctrlc = engine_state.ctrlc.clone();
let engine_state = engine_state.clone();
@ -198,8 +213,7 @@ impl Command for For {
},
Example {
description: "Number each item and print a message",
example:
"for $it in ['bob' 'fred'] --numbered { print $\"($it.index) is ($it.item)\" }",
example: r#"for $it in (['bob' 'fred'] | enumerate) { print $"($it.index) is ($it.item)" }"#,
result: None,
},
]

View File

@ -122,6 +122,10 @@ impl Command for If {
}
}
fn search_terms(&self) -> Vec<&str> {
vec!["else", "conditional"]
}
fn examples(&self) -> Vec<Example> {
vec![
Example {

View File

@ -10,7 +10,7 @@ impl Command for Try {
}
fn usage(&self) -> &str {
"Try to run a block, if it fails optionally run a catch block."
"Try to run a block, if it fails optionally run a catch closure."
}
fn signature(&self) -> nu_protocol::Signature {
@ -18,7 +18,7 @@ impl Command for Try {
.input_output_types(vec![(Type::Any, Type::Any)])
.required("try_block", SyntaxShape::Block, "Block to run.")
.optional(
"catch_block",
"catch_closure",
SyntaxShape::Keyword(
b"catch".to_vec(),
Box::new(SyntaxShape::OneOf(vec![
@ -26,7 +26,7 @@ impl Command for Try {
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])),
])),
),
"Block to run if try block fails.",
"Closure to run if try block fails.",
)
.category(Category::Core)
}
@ -85,9 +85,14 @@ impl Command for Try {
},
Example {
description: "Try to run a missing command",
example: "try { asdfasdf } catch { 'missing' } ",
example: "try { asdfasdf } catch { 'missing' }",
result: Some(Value::test_string("missing")),
},
Example {
description: "Try to run a missing command and report the message",
example: "try { asdfasdf } catch { |err| $err.msg }",
result: None,
},
]
}
}

View File

@ -160,11 +160,6 @@ fn features_enabled() -> Vec<String> {
// NOTE: There should be another way to know features on.
#[cfg(feature = "which-support")]
{
names.push("which".to_string());
}
#[cfg(feature = "trash-support")]
{
names.push("trash".to_string());

View File

@ -134,7 +134,6 @@ workspace = true
plugin = ["nu-parser/plugin"]
sqlite = ["rusqlite"]
trash-support = ["trash"]
which-support = []
[dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.94.3" }

View File

@ -128,48 +128,24 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
let range = &args.indexes;
match input {
Value::Binary { val, .. } => {
use std::cmp::{self, Ordering};
let len = val.len() as isize;
let start = if range.0 < 0 { range.0 + len } else { range.0 };
let end = if range.1 < 0 { range.1 + len } else { range.1 };
let end = if range.1 < 0 {
cmp::max(range.1 + len, 0)
} else {
range.1
};
if start < len && end >= 0 {
match start.cmp(&end) {
Ordering::Equal => Value::binary(vec![], head),
Ordering::Greater => Value::error(
ShellError::TypeMismatch {
err_message: "End must be greater than or equal to Start".to_string(),
span: head,
},
head,
),
Ordering::Less => Value::binary(
if end == isize::MAX {
val.iter()
.skip(start as usize)
.copied()
.collect::<Vec<u8>>()
} else {
val.iter()
.skip(start as usize)
.take((end - start) as usize)
.copied()
.collect()
},
head,
),
}
} else {
if start > end {
Value::binary(vec![], head)
} else {
let val_iter = val.iter().skip(start as usize);
Value::binary(
if end == isize::MAX {
val_iter.copied().collect::<Vec<u8>>()
} else {
val_iter.take((end - start + 1) as usize).copied().collect()
},
head,
)
}
}
Value::Error { .. } => input.clone(),
other => Value::error(

View File

@ -194,7 +194,7 @@ fn run_histogram(
if inputs.is_empty() {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: head_span,
span: Some(head_span),
src_span: list_span,
});
}

View File

@ -154,7 +154,7 @@ fn record_to_path_member(
let Some(value) = record.get("value") else {
return Err(ShellError::CantFindColumn {
col_name: "value".into(),
span: val_span,
span: Some(val_span),
src_span: span,
});
};

View File

@ -122,7 +122,7 @@ pub fn action(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value {
Value::Filesize { .. } => input.clone(),
Value::Int { val, .. } => Value::filesize(*val, value_span),
Value::Float { val, .. } => Value::filesize(*val as i64, value_span),
Value::String { val, .. } => match int_from_string(val, value_span) {
Value::String { val, .. } => match i64_from_string(val, value_span) {
Ok(val) => Value::filesize(val, value_span),
Err(error) => Value::error(error, value_span),
},
@ -139,7 +139,7 @@ pub fn action(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value {
}
}
fn int_from_string(a_string: &str, span: Span) -> Result<i64, ShellError> {
fn i64_from_string(a_string: &str, span: Span) -> Result<i64, ShellError> {
// Get the Locale so we know what the thousands separator is
let locale = get_system_locale();
@ -151,24 +151,35 @@ fn int_from_string(a_string: &str, span: Span) -> Result<i64, ShellError> {
// Hadle negative file size
if let Some(stripped_negative_string) = clean_string.strip_prefix('-') {
match stripped_negative_string.parse::<bytesize::ByteSize>() {
Ok(n) => Ok(-(n.as_u64() as i64)),
Ok(n) => i64_from_byte_size(n, true, span),
Err(_) => Err(string_convert_error(span)),
}
} else if let Some(stripped_positive_string) = clean_string.strip_prefix('+') {
match stripped_positive_string.parse::<bytesize::ByteSize>() {
Ok(n) if stripped_positive_string.starts_with(|c: char| c.is_ascii_digit()) => {
Ok(n.as_u64() as i64)
i64_from_byte_size(n, false, span)
}
_ => Err(string_convert_error(span)),
}
} else {
match clean_string.parse::<bytesize::ByteSize>() {
Ok(n) => Ok(n.as_u64() as i64),
Ok(n) => i64_from_byte_size(n, false, span),
Err(_) => Err(string_convert_error(span)),
}
}
}
fn i64_from_byte_size(
byte_size: bytesize::ByteSize,
is_negative: bool,
span: Span,
) -> Result<i64, ShellError> {
match i64::try_from(byte_size.as_u64()) {
Ok(n) => Ok(if is_negative { -n } else { n }),
Err(_) => Err(string_convert_error(span)),
}
}
fn string_convert_error(span: Span) -> ShellError {
ShellError::CantConvert {
to_type: "filesize".into(),

View File

@ -127,7 +127,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
SysTemp,
SysUsers,
UName,
Which,
};
// Help
@ -172,9 +172,6 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
))]
bind_command! { Ps };
#[cfg(feature = "which-support")]
bind_command! { Which };
// Strings
bind_command! {
Char,

View File

@ -35,12 +35,12 @@ impl Command for Touch {
)
.switch(
"modified",
"change the modification time of the file or directory. If no timestamp, date or reference file/directory is given, the current time is used",
"change the modification time of the file or directory. If no reference file/directory is given, the current time is used",
Some('m'),
)
.switch(
"access",
"change the access time of the file or directory. If no timestamp, date or reference file/directory is given, the current time is used",
"change the access time of the file or directory. If no reference file/directory is given, the current time is used",
Some('a'),
)
.switch(
@ -189,11 +189,6 @@ impl Command for Touch {
example: r#"touch -m -r fixture.json d e"#,
result: None,
},
Example {
description: r#"Changes the last accessed time of "fixture.json" to a date"#,
example: r#"touch -a -d "August 24, 2019; 12:30:30" fixture.json"#,
result: None,
},
]
}
}

View File

@ -130,7 +130,7 @@ pub fn split(
Some(group_key) => Ok(group_key.coerce_string()?),
None => Err(ShellError::CantFindColumn {
col_name: column_name.item.to_string(),
span: column_name.span,
span: Some(column_name.span),
src_span: row.span(),
}),
}

View File

@ -123,7 +123,7 @@ fn validate(vec: &[Value], columns: &[String], span: Span) -> Result<(), ShellEr
if let Some(nonexistent) = nonexistent_column(columns, record.columns()) {
return Err(ShellError::CantFindColumn {
col_name: nonexistent,
span,
span: Some(span),
src_span: val_span,
});
}

View File

@ -1,4 +1,4 @@
use chrono::SecondsFormat;
use chrono::{DateTime, Datelike, FixedOffset, Timelike};
use nu_engine::command_prelude::*;
use nu_protocol::ast::PathMember;
@ -49,9 +49,7 @@ fn helper(engine_state: &EngineState, v: &Value) -> Result<toml::Value, ShellErr
Value::Int { val, .. } => toml::Value::Integer(*val),
Value::Filesize { val, .. } => toml::Value::Integer(*val),
Value::Duration { val, .. } => toml::Value::String(val.to_string()),
Value::Date { val, .. } => {
toml::Value::String(val.to_rfc3339_opts(SecondsFormat::AutoSi, false))
}
Value::Date { val, .. } => toml::Value::Datetime(to_toml_datetime(val)),
Value::Range { .. } => toml::Value::String("<Range>".to_string()),
Value::Float { val, .. } => toml::Value::Float(*val),
Value::String { val, .. } | Value::Glob { val, .. } => toml::Value::String(val.clone()),
@ -157,6 +155,43 @@ fn to_toml(
}
}
/// Convert chrono datetime into a toml::Value datetime. The latter uses its
/// own ad-hoc datetime types, which makes this somewhat convoluted.
fn to_toml_datetime(datetime: &DateTime<FixedOffset>) -> toml::value::Datetime {
let date = toml::value::Date {
// TODO: figure out what to do with BC dates, because the toml
// crate doesn't support them. Same for large years, which
// don't fit in u16.
year: datetime.year_ce().1 as u16,
// Panic: this is safe, because chrono guarantees that the month
// value will be between 1 and 12 and the day will be between 1
// and 31
month: datetime.month() as u8,
day: datetime.day() as u8,
};
let time = toml::value::Time {
// Panic: same as before, chorono guarantees that all of the following 3
// methods return values less than 65'000
hour: datetime.hour() as u8,
minute: datetime.minute() as u8,
second: datetime.second() as u8,
nanosecond: datetime.nanosecond(),
};
let offset = toml::value::Offset::Custom {
// Panic: minute timezone offset fits into i16 (that's more than
// 1000 hours)
minutes: (-datetime.timezone().utc_minus_local() / 60) as i16,
};
toml::value::Datetime {
date: Some(date),
time: Some(time),
offset: Some(offset),
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -181,7 +216,20 @@ mod tests {
Span::test_data(),
);
let reference_date = toml::Value::String(String::from("1980-10-12T10:12:44+02:00"));
let reference_date = toml::Value::Datetime(toml::value::Datetime {
date: Some(toml::value::Date {
year: 1980,
month: 10,
day: 12,
}),
time: Some(toml::value::Time {
hour: 10,
minute: 12,
second: 44,
nanosecond: 0,
}),
offset: Some(toml::value::Offset::Custom { minutes: 120 }),
});
let result = helper(&engine_state, &test_date);

View File

@ -122,7 +122,7 @@ fn build_help_commands(engine_state: &EngineState, span: Span) -> Vec<Value> {
let usage = sig.usage;
let search_terms = sig.search_terms;
let command_type = format!("{:?}", decl.command_type()).to_ascii_lowercase();
let command_type = decl.command_type().to_string();
// Build table of parameters
let param_table = {

View File

@ -1,7 +1,10 @@
use super::PathSubcommandArguments;
use nu_engine::command_prelude::*;
use nu_protocol::engine::StateWorkingSet;
use std::path::{Path, PathBuf};
use std::{
io,
path::{Path, PathBuf},
};
struct Arguments {
pwd: PathBuf,
@ -36,7 +39,7 @@ impl Command for SubCommand {
fn extra_usage(&self) -> &str {
r#"This checks the file system to confirm the path's object type.
If nothing is found, an empty string will be returned."#
If the path does not exist, null will be returned."#
}
fn is_const(&self) -> bool {
@ -104,9 +107,11 @@ If nothing is found, an empty string will be returned."#
fn path_type(path: &Path, span: Span, args: &Arguments) -> Value {
let path = nu_path::expand_path_with(path, &args.pwd, true);
let meta = path.symlink_metadata();
let ty = meta.as_ref().map(get_file_type).unwrap_or("");
Value::string(ty, span)
match path.symlink_metadata() {
Ok(metadata) => Value::string(get_file_type(&metadata), span),
Err(err) if err.kind() == io::ErrorKind::NotFound => Value::nothing(span),
Err(err) => Value::error(err.into_spanned(span).into(), span),
}
}
fn get_file_type(md: &std::fs::Metadata) -> &str {

View File

@ -81,7 +81,7 @@ pub fn sort(
if let Some(nonexistent) = nonexistent_column(&sort_columns, record.columns()) {
return Err(ShellError::CantFindColumn {
col_name: nonexistent,
span,
span: Some(span),
src_span: val_span,
});
}

View File

@ -10,7 +10,6 @@ struct Arguments {
substring: String,
cell_paths: Option<Vec<CellPath>>,
case_insensitive: bool,
not_contain: bool,
}
impl CmdArgument for Arguments {
@ -40,7 +39,6 @@ impl Command for SubCommand {
"For a data structure input, check strings at the given cell paths, and replace with result.",
)
.switch("ignore-case", "search is case insensitive", Some('i'))
.switch("not", "DEPRECATED OPTION: does not contain", Some('n'))
.category(Category::Strings)
}
@ -63,27 +61,12 @@ impl Command for SubCommand {
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
if call.has_flag(engine_state, stack, "not")? {
nu_protocol::report_error_new(
engine_state,
&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(engine_state, stack, 1)?;
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
let args = Arguments {
substring: call.req::<String>(engine_state, stack, 0)?,
cell_paths,
case_insensitive: call.has_flag(engine_state, stack, "ignore-case")?,
not_contain: call.has_flag(engine_state, stack, "not")?,
};
operate(action, args, input, call.head, engine_state.ctrlc.clone())
}
@ -114,7 +97,6 @@ impl Command for SubCommand {
substring: call.req_const::<String>(working_set, 0)?,
cell_paths,
case_insensitive: call.has_flag_const(working_set, "ignore-case")?,
not_contain: call.has_flag_const(working_set, "not")?,
};
operate(
action,
@ -183,7 +165,6 @@ fn action(
input: &Value,
Arguments {
case_insensitive,
not_contain,
substring,
..
}: &Arguments,
@ -191,23 +172,11 @@ fn action(
) -> Value {
match input {
Value::String { val, .. } => Value::bool(
match case_insensitive {
true => {
if *not_contain {
!val.to_folded_case()
.contains(substring.to_folded_case().as_str())
} else {
val.to_folded_case()
.contains(substring.to_folded_case().as_str())
}
}
false => {
if *not_contain {
!val.contains(substring)
} else {
val.contains(substring)
}
}
if *case_insensitive {
val.to_folded_case()
.contains(substring.to_folded_case().as_str())
} else {
val.contains(substring)
},
head,
),

View File

@ -5,7 +5,6 @@ use nu_cmd_base::{
};
use nu_engine::command_prelude::*;
use nu_protocol::{engine::StateWorkingSet, Range};
use std::cmp::Ordering;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Clone)]
@ -151,6 +150,11 @@ impl Command for SubCommand {
example: " '🇯🇵ほげ ふが ぴよ' | str substring --grapheme-clusters 4..5",
result: Some(Value::test_string("ふが")),
},
Example {
description: "sub string by negative index",
example: " 'good nushell' | str substring 5..-2",
result: Some(Value::test_string("nushel")),
},
]
}
}
@ -167,56 +171,46 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
options.0
};
let end: isize = if options.1 < 0 {
std::cmp::max(len + options.1, 0)
options.1 + len
} else {
options.1
};
if start < len && end >= 0 {
match start.cmp(&end) {
Ordering::Equal => Value::string("", head),
Ordering::Greater => Value::error(
ShellError::TypeMismatch {
err_message: "End must be greater than or equal to Start".to_string(),
span: head,
},
head,
),
Ordering::Less => Value::string(
{
if end == isize::MAX {
if args.graphemes {
s.graphemes(true)
.skip(start as usize)
.collect::<Vec<&str>>()
.join("")
} else {
String::from_utf8_lossy(
&s.bytes().skip(start as usize).collect::<Vec<_>>(),
)
.to_string()
}
} else if args.graphemes {
if start > end {
Value::string("", head)
} else {
Value::string(
{
if end == isize::MAX {
if args.graphemes {
s.graphemes(true)
.skip(start as usize)
.take((end - start) as usize)
.collect::<Vec<&str>>()
.join("")
} else {
String::from_utf8_lossy(
&s.bytes()
.skip(start as usize)
.take((end - start) as usize)
.collect::<Vec<_>>(),
&s.bytes().skip(start as usize).collect::<Vec<_>>(),
)
.to_string()
}
},
head,
),
}
} else {
Value::string("", head)
} else if args.graphemes {
s.graphemes(true)
.skip(start as usize)
.take((end - start + 1) as usize)
.collect::<Vec<&str>>()
.join("")
} else {
String::from_utf8_lossy(
&s.bytes()
.skip(start as usize)
.take((end - start + 1) as usize)
.collect::<Vec<_>>(),
)
.to_string()
}
},
head,
)
}
}
// Propagate errors by explicitly matching them before the final case.
@ -243,6 +237,7 @@ mod tests {
test_examples(SubCommand {})
}
#[derive(Debug)]
struct Expectation<'a> {
options: (isize, isize),
expected: &'a str,
@ -266,18 +261,19 @@ mod tests {
let word = Value::test_string("andres");
let cases = vec![
expectation("a", (0, 1)),
expectation("an", (0, 2)),
expectation("and", (0, 3)),
expectation("andr", (0, 4)),
expectation("andre", (0, 5)),
expectation("a", (0, 0)),
expectation("an", (0, 1)),
expectation("and", (0, 2)),
expectation("andr", (0, 3)),
expectation("andre", (0, 4)),
expectation("andres", (0, 5)),
expectation("andres", (0, 6)),
expectation("", (0, -6)),
expectation("a", (0, -5)),
expectation("an", (0, -4)),
expectation("and", (0, -3)),
expectation("andr", (0, -2)),
expectation("andre", (0, -1)),
expectation("a", (0, -6)),
expectation("an", (0, -5)),
expectation("and", (0, -4)),
expectation("andr", (0, -3)),
expectation("andre", (0, -2)),
expectation("andres", (0, -1)),
// str substring [ -4 , _ ]
// str substring -4 ,
expectation("dres", (-4, isize::MAX)),
@ -292,6 +288,7 @@ mod tests {
];
for expectation in &cases {
println!("{:?}", expectation);
let expected = expectation.expected;
let actual = action(
&word,

View File

@ -1,4 +1,6 @@
use super::trim_cstyle_null;
use nu_engine::command_prelude::*;
use sysinfo::{CpuRefreshKind, System, MINIMUM_CPU_UPDATE_INTERVAL};
#[derive(Clone)]
pub struct SysCpu;
@ -26,7 +28,7 @@ impl Command for SysCpu {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::cpu(call.head).into_pipeline_data())
Ok(cpu(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -37,3 +39,42 @@ impl Command for SysCpu {
}]
}
}
fn cpu(span: Span) -> Value {
let mut sys = System::new();
sys.refresh_cpu_specifics(CpuRefreshKind::everything());
// We must refresh the CPU twice a while apart to get valid usage data.
// In theory we could just sleep MINIMUM_CPU_UPDATE_INTERVAL, but I've noticed that
// that gives poor results (error of ~5%). Decided to wait 2x that long, somewhat arbitrarily
std::thread::sleep(MINIMUM_CPU_UPDATE_INTERVAL * 2);
sys.refresh_cpu_specifics(CpuRefreshKind::new().with_cpu_usage());
let cpus = sys
.cpus()
.iter()
.map(|cpu| {
// sysinfo CPU usage numbers are not very precise unless you wait a long time between refreshes.
// Round to 1DP (chosen somewhat arbitrarily) so people aren't misled by high-precision floats.
let rounded_usage = (cpu.cpu_usage() * 10.0).round() / 10.0;
let load_avg = System::load_average();
let load_avg = format!(
"{:.2}, {:.2}, {:.2}",
load_avg.one, load_avg.five, load_avg.fifteen
);
let record = record! {
"name" => Value::string(trim_cstyle_null(cpu.name()), span),
"brand" => Value::string(trim_cstyle_null(cpu.brand()), span),
"freq" => Value::int(cpu.frequency() as i64, span),
"cpu_usage" => Value::float(rounded_usage.into(), span),
"load_average" => Value::string(load_avg, span),
"vendor_id" => Value::string(trim_cstyle_null(cpu.vendor_id()), span),
};
Value::record(record, span)
})
.collect();
Value::list(cpus, span)
}

View File

@ -1,4 +1,6 @@
use super::trim_cstyle_null;
use nu_engine::command_prelude::*;
use sysinfo::Disks;
#[derive(Clone)]
pub struct SysDisks;
@ -26,7 +28,7 @@ impl Command for SysDisks {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::disks(call.head).into_pipeline_data())
Ok(disks(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -37,3 +39,27 @@ impl Command for SysDisks {
}]
}
}
fn disks(span: Span) -> Value {
let disks = Disks::new_with_refreshed_list()
.iter()
.map(|disk| {
let device = trim_cstyle_null(disk.name().to_string_lossy());
let typ = trim_cstyle_null(disk.file_system().to_string_lossy());
let record = record! {
"device" => Value::string(device, span),
"type" => Value::string(typ, span),
"mount" => Value::string(disk.mount_point().to_string_lossy(), span),
"total" => Value::filesize(disk.total_space() as i64, span),
"free" => Value::filesize(disk.available_space() as i64, span),
"removable" => Value::bool(disk.is_removable(), span),
"kind" => Value::string(disk.kind().to_string(), span),
};
Value::record(record, span)
})
.collect();
Value::list(disks, span)
}

View File

@ -1,4 +1,7 @@
use super::trim_cstyle_null;
use chrono::{DateTime, FixedOffset, Local};
use nu_engine::command_prelude::*;
use sysinfo::System;
#[derive(Clone)]
pub struct SysHost;
@ -26,8 +29,7 @@ impl Command for SysHost {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let host = super::host(call.head);
Ok(Value::record(host, call.head).into_pipeline_data())
Ok(host(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -38,3 +40,53 @@ impl Command for SysHost {
}]
}
}
fn host(span: Span) -> Value {
let mut record = Record::new();
if let Some(name) = System::name() {
record.push("name", Value::string(trim_cstyle_null(name), span));
}
if let Some(version) = System::os_version() {
record.push("os_version", Value::string(trim_cstyle_null(version), span));
}
if let Some(long_version) = System::long_os_version() {
record.push(
"long_os_version",
Value::string(trim_cstyle_null(long_version), span),
);
}
if let Some(version) = System::kernel_version() {
record.push(
"kernel_version",
Value::string(trim_cstyle_null(version), span),
);
}
if let Some(hostname) = System::host_name() {
record.push("hostname", Value::string(trim_cstyle_null(hostname), span));
}
let uptime = System::uptime()
.saturating_mul(1_000_000_000)
.try_into()
.unwrap_or(i64::MAX);
record.push("uptime", Value::duration(uptime, span));
let boot_time = boot_time()
.map(|time| Value::date(time, span))
.unwrap_or(Value::nothing(span));
record.push("boot_time", boot_time);
Value::record(record, span)
}
fn boot_time() -> Option<DateTime<FixedOffset>> {
// Broken systems can apparently return really high values.
// See: https://github.com/nushell/nushell/issues/10155
// First, try to convert u64 to i64, and then try to create a `DateTime`.
let secs = System::boot_time().try_into().ok()?;
let time = DateTime::from_timestamp(secs, 0)?;
Some(time.with_timezone(&Local).fixed_offset())
}

View File

@ -1,4 +1,5 @@
use nu_engine::command_prelude::*;
use sysinfo::System;
#[derive(Clone)]
pub struct SysMem;
@ -26,7 +27,7 @@ impl Command for SysMem {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::mem(call.head).into_pipeline_data())
Ok(mem(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -37,3 +38,20 @@ impl Command for SysMem {
}]
}
}
fn mem(span: Span) -> Value {
let mut sys = System::new();
sys.refresh_memory();
let record = record! {
"total" => Value::filesize(sys.total_memory() as i64, span),
"free" => Value::filesize(sys.free_memory() as i64, span),
"used" => Value::filesize(sys.used_memory() as i64, span),
"available" => Value::filesize(sys.available_memory() as i64, span),
"swap total" => Value::filesize(sys.total_swap() as i64, span),
"swap free" => Value::filesize(sys.free_swap() as i64, span),
"swap used" => Value::filesize(sys.used_swap() as i64, span),
};
Value::record(record, span)
}

View File

@ -16,202 +16,6 @@ pub use sys_::Sys;
pub use temp::SysTemp;
pub use users::SysUsers;
use chrono::{DateTime, FixedOffset, Local};
use nu_protocol::{record, Record, Span, Value};
use sysinfo::{
Components, CpuRefreshKind, Disks, Networks, System, Users, MINIMUM_CPU_UPDATE_INTERVAL,
};
pub fn trim_cstyle_null(s: impl AsRef<str>) -> String {
fn trim_cstyle_null(s: impl AsRef<str>) -> String {
s.as_ref().trim_matches('\0').into()
}
pub fn disks(span: Span) -> Value {
let disks = Disks::new_with_refreshed_list()
.iter()
.map(|disk| {
let device = trim_cstyle_null(disk.name().to_string_lossy());
let typ = trim_cstyle_null(disk.file_system().to_string_lossy());
let record = record! {
"device" => Value::string(device, span),
"type" => Value::string(typ, span),
"mount" => Value::string(disk.mount_point().to_string_lossy(), span),
"total" => Value::filesize(disk.total_space() as i64, span),
"free" => Value::filesize(disk.available_space() as i64, span),
"removable" => Value::bool(disk.is_removable(), span),
"kind" => Value::string(disk.kind().to_string(), span),
};
Value::record(record, span)
})
.collect();
Value::list(disks, span)
}
pub fn net(span: Span) -> Value {
let networks = Networks::new_with_refreshed_list()
.iter()
.map(|(iface, data)| {
let record = record! {
"name" => Value::string(trim_cstyle_null(iface), span),
"sent" => Value::filesize(data.total_transmitted() as i64, span),
"recv" => Value::filesize(data.total_received() as i64, span),
};
Value::record(record, span)
})
.collect();
Value::list(networks, span)
}
pub fn cpu(span: Span) -> Value {
let mut sys = System::new();
sys.refresh_cpu_specifics(CpuRefreshKind::everything());
// We must refresh the CPU twice a while apart to get valid usage data.
// In theory we could just sleep MINIMUM_CPU_UPDATE_INTERVAL, but I've noticed that
// that gives poor results (error of ~5%). Decided to wait 2x that long, somewhat arbitrarily
std::thread::sleep(MINIMUM_CPU_UPDATE_INTERVAL * 2);
sys.refresh_cpu_specifics(CpuRefreshKind::new().with_cpu_usage());
let cpus = sys
.cpus()
.iter()
.map(|cpu| {
// sysinfo CPU usage numbers are not very precise unless you wait a long time between refreshes.
// Round to 1DP (chosen somewhat arbitrarily) so people aren't misled by high-precision floats.
let rounded_usage = (cpu.cpu_usage() * 10.0).round() / 10.0;
let load_avg = System::load_average();
let load_avg = format!(
"{:.2}, {:.2}, {:.2}",
load_avg.one, load_avg.five, load_avg.fifteen
);
let record = record! {
"name" => Value::string(trim_cstyle_null(cpu.name()), span),
"brand" => Value::string(trim_cstyle_null(cpu.brand()), span),
"freq" => Value::int(cpu.frequency() as i64, span),
"cpu_usage" => Value::float(rounded_usage.into(), span),
"load_average" => Value::string(load_avg, span),
"vendor_id" => Value::string(trim_cstyle_null(cpu.vendor_id()), span),
};
Value::record(record, span)
})
.collect();
Value::list(cpus, span)
}
pub fn mem(span: Span) -> Value {
let mut sys = System::new();
sys.refresh_memory();
let record = record! {
"total" => Value::filesize(sys.total_memory() as i64, span),
"free" => Value::filesize(sys.free_memory() as i64, span),
"used" => Value::filesize(sys.used_memory() as i64, span),
"available" => Value::filesize(sys.available_memory() as i64, span),
"swap total" => Value::filesize(sys.total_swap() as i64, span),
"swap free" => Value::filesize(sys.free_swap() as i64, span),
"swap used" => Value::filesize(sys.used_swap() as i64, span),
};
Value::record(record, span)
}
pub fn users(span: Span) -> Value {
let users = Users::new_with_refreshed_list()
.iter()
.map(|user| {
let groups = user
.groups()
.iter()
.map(|group| Value::string(trim_cstyle_null(group.name()), span))
.collect();
let record = record! {
"name" => Value::string(trim_cstyle_null(user.name()), span),
"groups" => Value::list(groups, span),
};
Value::record(record, span)
})
.collect();
Value::list(users, span)
}
pub fn host(span: Span) -> Record {
let mut record = Record::new();
if let Some(name) = System::name() {
record.push("name", Value::string(trim_cstyle_null(name), span));
}
if let Some(version) = System::os_version() {
record.push("os_version", Value::string(trim_cstyle_null(version), span));
}
if let Some(long_version) = System::long_os_version() {
record.push(
"long_os_version",
Value::string(trim_cstyle_null(long_version), span),
);
}
if let Some(version) = System::kernel_version() {
record.push(
"kernel_version",
Value::string(trim_cstyle_null(version), span),
);
}
if let Some(hostname) = System::host_name() {
record.push("hostname", Value::string(trim_cstyle_null(hostname), span));
}
let uptime = System::uptime()
.saturating_mul(1_000_000_000)
.try_into()
.unwrap_or(i64::MAX);
record.push("uptime", Value::duration(uptime, span));
let boot_time = boot_time()
.map(|time| Value::date(time, span))
.unwrap_or(Value::nothing(span));
record.push("boot_time", boot_time);
record
}
fn boot_time() -> Option<DateTime<FixedOffset>> {
// Broken systems can apparently return really high values.
// See: https://github.com/nushell/nushell/issues/10155
// First, try to convert u64 to i64, and then try to create a `DateTime`.
let secs = System::boot_time().try_into().ok()?;
let time = DateTime::from_timestamp(secs, 0)?;
Some(time.with_timezone(&Local).fixed_offset())
}
pub fn temp(span: Span) -> Value {
let components = Components::new_with_refreshed_list()
.iter()
.map(|component| {
let mut record = record! {
"unit" => Value::string(component.label(), span),
"temp" => Value::float(component.temperature().into(), span),
"high" => Value::float(component.max().into(), span),
};
if let Some(critical) = component.critical() {
record.push("critical", Value::float(critical.into(), span));
}
Value::record(record, span)
})
.collect();
Value::list(components, span)
}

View File

@ -1,4 +1,6 @@
use super::trim_cstyle_null;
use nu_engine::command_prelude::*;
use sysinfo::Networks;
#[derive(Clone)]
pub struct SysNet;
@ -26,7 +28,7 @@ impl Command for SysNet {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::net(call.head).into_pipeline_data())
Ok(net(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -37,3 +39,20 @@ impl Command for SysNet {
}]
}
}
fn net(span: Span) -> Value {
let networks = Networks::new_with_refreshed_list()
.iter()
.map(|(iface, data)| {
let record = record! {
"name" => Value::string(trim_cstyle_null(iface), span),
"sent" => Value::filesize(data.total_transmitted() as i64, span),
"recv" => Value::filesize(data.total_received() as i64, span),
};
Value::record(record, span)
})
.collect();
Value::list(networks, span)
}

View File

@ -1,4 +1,4 @@
use nu_engine::command_prelude::*;
use nu_engine::{command_prelude::*, get_full_help};
#[derive(Clone)]
pub struct Sys;
@ -20,41 +20,17 @@ impl Command for Sys {
}
fn extra_usage(&self) -> &str {
"Note that this command may take a noticeable amount of time to run. To reduce the time taken, you can use the various `sys` sub commands to get the subset of information you are interested in."
"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,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
nu_protocol::report_error_new(
engine_state,
&ShellError::GenericError {
error: "Deprecated command".into(),
msg: "the `sys` command is deprecated, please use the new subcommands (`sys host`, `sys mem`, etc.)."
.into(),
span: Some(call.head),
help: None,
inner: vec![],
},
);
let head = call.head;
let mut host = super::host(head);
host.push("sessions", super::users(head));
let record = record! {
"host" => Value::record(host, head),
"cpu" => super::cpu(head),
"disks" => super::disks(head),
"mem" => super::mem(head),
"temp" => super::temp(head),
"net" => super::net(head),
};
Ok(Value::record(record, head).into_pipeline_data())
Ok(Value::string(get_full_help(self, engine_state, stack), call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {

View File

@ -1,4 +1,5 @@
use nu_engine::command_prelude::*;
use sysinfo::Components;
#[derive(Clone)]
pub struct SysTemp;
@ -30,7 +31,7 @@ impl Command for SysTemp {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::temp(call.head).into_pipeline_data())
Ok(temp(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -41,3 +42,24 @@ impl Command for SysTemp {
}]
}
}
fn temp(span: Span) -> Value {
let components = Components::new_with_refreshed_list()
.iter()
.map(|component| {
let mut record = record! {
"unit" => Value::string(component.label(), span),
"temp" => Value::float(component.temperature().into(), span),
"high" => Value::float(component.max().into(), span),
};
if let Some(critical) = component.critical() {
record.push("critical", Value::float(critical.into(), span));
}
Value::record(record, span)
})
.collect();
Value::list(components, span)
}

View File

@ -1,4 +1,6 @@
use super::trim_cstyle_null;
use nu_engine::command_prelude::*;
use sysinfo::Users;
#[derive(Clone)]
pub struct SysUsers;
@ -11,7 +13,7 @@ impl Command for SysUsers {
fn signature(&self) -> Signature {
Signature::build("sys users")
.category(Category::System)
.input_output_types(vec![(Type::Nothing, Type::record())])
.input_output_types(vec![(Type::Nothing, Type::table())])
}
fn usage(&self) -> &str {
@ -25,7 +27,7 @@ impl Command for SysUsers {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::users(call.head).into_pipeline_data())
Ok(users(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -36,3 +38,25 @@ impl Command for SysUsers {
}]
}
}
fn users(span: Span) -> Value {
let users = Users::new_with_refreshed_list()
.iter()
.map(|user| {
let groups = user
.groups()
.iter()
.map(|group| Value::string(trim_cstyle_null(group.name()), span))
.collect();
let record = record! {
"name" => Value::string(trim_cstyle_null(user.name()), span),
"groups" => Value::list(groups, span),
};
Value::record(record, span)
})
.collect();
Value::list(users, span)
}

View File

@ -92,7 +92,6 @@ fn get_entries_in_nu(
all_entries
}
#[cfg(feature = "which-support")]
fn get_first_entry_in_path(
item: &str,
span: Span,
@ -104,17 +103,6 @@ fn get_first_entry_in_path(
.ok()
}
#[cfg(not(feature = "which-support"))]
fn get_first_entry_in_path(
_item: &str,
_span: Span,
_cwd: impl AsRef<Path>,
_paths: impl AsRef<OsStr>,
) -> Option<Value> {
None
}
#[cfg(feature = "which-support")]
fn get_all_entries_in_path(
item: &str,
span: Span,
@ -129,16 +117,6 @@ fn get_all_entries_in_path(
.unwrap_or_default()
}
#[cfg(not(feature = "which-support"))]
fn get_all_entries_in_path(
_item: &str,
_span: Span,
_cwd: impl AsRef<Path>,
_paths: impl AsRef<OsStr>,
) -> Vec<Value> {
vec![]
}
#[derive(Debug)]
struct WhichArgs {
applications: Vec<Spanned<String>>,

View File

@ -170,7 +170,10 @@ impl Command for Table {
}),
Value::test_record(record! {
"a" => Value::test_int(3),
"b" => Value::test_int(4),
"b" => Value::test_list(vec![
Value::test_int(4),
Value::test_int(4),
])
}),
])),
},
@ -184,7 +187,10 @@ impl Command for Table {
}),
Value::test_record(record! {
"a" => Value::test_int(3),
"b" => Value::test_int(4),
"b" => Value::test_list(vec![
Value::test_int(4),
Value::test_int(4),
])
}),
])),
},

View File

@ -31,12 +31,12 @@ fn detect_columns_with_legacy_and_flag_c() {
(
"$\"c1 c2 c3 c4 c5(char nl)a b c d e\"",
"[[c1,c3,c4,c5]; ['a b',c,d,e]]",
"0..0",
"0..1",
),
(
"$\"c1 c2 c3 c4 c5(char nl)a b c d e\"",
"[[c1,c2,c3,c4]; [a,b,c,'d e']]",
"(-2)..(-2)",
"(-2)..(-1)",
),
(
"$\"c1 c2 c3 c4 c5(char nl)a b c d e\"",
@ -72,10 +72,10 @@ drwxr-xr-x 2 root root 4.0K Mar 20 08:28 =(char nl)
drwxr-xr-x 4 root root 4.0K Mar 20 08:18 ~(char nl)
-rw-r--r-- 1 root root 3.0K Mar 20 07:23 ~asdf(char nl)\"";
let expected = "[
['column0', 'column1', 'column2', 'column3', 'column4', 'column5', 'column8'];
['drwxr-xr-x', '2', 'root', 'root', '4.0K', 'Mar 20 08:28', '='],
['drwxr-xr-x', '4', 'root', 'root', '4.0K', 'Mar 20 08:18', '~'],
['-rw-r--r--', '1', 'root', 'root', '3.0K', 'Mar 20 07:23', '~asdf']
['column0', 'column1', 'column2', 'column3', 'column4', 'column5', 'column7', 'column8'];
['drwxr-xr-x', '2', 'root', 'root', '4.0K', 'Mar 20', '08:28', '='],
['drwxr-xr-x', '4', 'root', 'root', '4.0K', 'Mar 20', '08:18', '~'],
['-rw-r--r--', '1', 'root', 'root', '3.0K', 'Mar 20', '07:23', '~asdf']
]";
let range = "5..6";
let cmd = format!(

View File

@ -4,8 +4,9 @@ use nu_test_support::nu;
fn error_label_works() {
let actual = nu!("error make {msg:foo label:{text:unseen}}");
assert!(actual.err.contains("unseen"));
assert!(actual.err.contains("╰──"));
assert!(actual
.err
.contains("label at line 1, columns 1 to 10: unseen"));
}
#[test]

View File

@ -37,7 +37,7 @@ fn given_fields_can_be_column_paths() {
}
#[test]
fn can_use_variables() {
fn cant_use_variables() {
let actual = nu!(
cwd: "tests/fixtures/formats", pipeline(
r#"
@ -46,7 +46,8 @@ fn can_use_variables() {
"#
));
assert_eq!(actual.out, "nu is a new type of shell");
// TODO SPAN: This has been removed during SpanId refactor
assert!(actual.err.contains("Removed functionality"));
}
#[test]
@ -55,7 +56,7 @@ fn error_unmatched_brace() {
cwd: "tests/fixtures/formats", pipeline(
r#"
open cargo_sample.toml
| format pattern "{$it.package.name"
| format pattern "{package.name"
"#
));

View File

@ -76,6 +76,13 @@ fn into_filesize_wrong_negative_str_filesize() {
assert!(actual.err.contains("can't convert string to filesize"));
}
#[test]
fn into_filesize_large_negative_str_filesize() {
let actual = nu!("'-10000PiB' | into filesize");
assert!(actual.err.contains("can't convert string to filesize"));
}
#[test]
fn into_filesize_negative_str() {
let actual = nu!("'-1' | into filesize");
@ -104,6 +111,13 @@ fn into_filesize_wrong_positive_str_filesize() {
assert!(actual.err.contains("can't convert string to filesize"));
}
#[test]
fn into_filesize_large_positive_str_filesize() {
let actual = nu!("'+10000PiB' | into filesize");
assert!(actual.err.contains("can't convert string to filesize"));
}
#[test]
fn into_filesize_positive_str() {
let actual = nu!("'+1' | into filesize");

View File

@ -125,7 +125,6 @@ mod upsert;
mod url;
mod use_;
mod where_;
#[cfg(feature = "which-support")]
mod which;
mod while_;
mod with_env;

View File

@ -255,7 +255,7 @@ fn substrings_the_input() {
}
#[test]
fn substring_errors_if_start_index_is_greater_than_end_index() {
fn substring_empty_if_start_index_is_greater_than_end_index() {
Playground::setup("str_test_9", |dirs, sandbox| {
sandbox.with_files(&[FileWithContent(
"sample.toml",
@ -270,12 +270,10 @@ fn substring_errors_if_start_index_is_greater_than_end_index() {
r#"
open sample.toml
| str substring 6..4 fortune.teller.phone
| get fortune.teller.phone
"#
));
assert!(actual
.err
.contains("End must be greater than or equal to Start"))
assert_eq!(actual.out, "")
})
}
@ -375,6 +373,21 @@ fn substrings_the_input_and_treats_end_index_as_length_if_blank_end_index_given(
})
}
#[test]
fn substring_by_negative_index() {
Playground::setup("str_test_13", |dirs, _| {
let actual = nu!(
cwd: dirs.test(), "'apples' | str substring 0..-1",
);
assert_eq!(actual.out, "apples");
let actual = nu!(
cwd: dirs.test(), "'apples' | str substring 0..<-1",
);
assert_eq!(actual.out, "apple");
})
}
#[test]
fn str_reverse() {
let actual = nu!(r#"

View File

@ -906,7 +906,7 @@ fn test_cp_debug_default() {
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if !actual
.out
.contains("copy offload: unknown, reflink: unsupported, sparse detection: no")
.contains("copy offload: yes, reflink: unsupported, sparse detection: no")
{
panic!("{}", format!("Failure: stdout was \n{}", actual.out));
}

View File

@ -0,0 +1,21 @@
[package]
authors = ["The Nushell Project Developers"]
description = "Macros implementation of #[derive(FromValue, IntoValue)]"
edition = "2021"
license = "MIT"
name = "nu-derive-value"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-derive-value"
version = "0.94.3"
[lib]
proc-macro = true
# we can only use exposed macros in doctests really,
# so we cannot test anything useful in a doctest
doctest = false
[dependencies]
proc-macro2 = { workspace = true }
syn = { workspace = true }
quote = { workspace = true }
proc-macro-error = { workspace = true }
convert_case = { workspace = true }

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 - 2023 The Nushell Project Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,116 @@
use convert_case::Case;
use syn::{spanned::Spanned, Attribute, Fields, LitStr};
use crate::{error::DeriveError, HELPER_ATTRIBUTE};
#[derive(Debug)]
pub struct ContainerAttributes {
pub rename_all: Case,
}
impl Default for ContainerAttributes {
fn default() -> Self {
Self {
rename_all: Case::Snake,
}
}
}
impl ContainerAttributes {
pub fn parse_attrs<'a, M>(
iter: impl Iterator<Item = &'a Attribute>,
) -> Result<Self, DeriveError<M>> {
let mut container_attrs = ContainerAttributes::default();
for attr in filter(iter) {
// This is a container to allow returning derive errors inside the parse_nested_meta fn.
let mut err = Ok(());
attr.parse_nested_meta(|meta| {
let ident = meta.path.require_ident()?;
match ident.to_string().as_str() {
"rename_all" => {
// The matched case are all useful variants from `convert_case` with aliases
// that `serde` uses.
let case: LitStr = meta.value()?.parse()?;
let case = match case.value().as_str() {
"UPPER CASE" | "UPPER WITH SPACES CASE" => Case::Upper,
"lower case" | "lower with spaces case" => Case::Lower,
"Title Case" => Case::Title,
"camelCase" => Case::Camel,
"PascalCase" | "UpperCamelCase" => Case::Pascal,
"snake_case" => Case::Snake,
"UPPER_SNAKE_CASE" | "SCREAMING_SNAKE_CASE" => Case::UpperSnake,
"kebab-case" => Case::Kebab,
"COBOL-CASE" | "UPPER-KEBAB-CASE" | "SCREAMING-KEBAB-CASE" => {
Case::Cobol
}
"Train-Case" => Case::Train,
"flatcase" | "lowercase" => Case::Flat,
"UPPERFLATCASE" | "UPPERCASE" => Case::UpperFlat,
// Although very funny, we don't support `Case::{Toggle, Alternating}`,
// as we see no real benefit.
c => {
err = Err(DeriveError::InvalidAttributeValue {
value_span: case.span(),
value: Box::new(c.to_string()),
});
return Ok(()); // We stored the err in `err`.
}
};
container_attrs.rename_all = case;
}
ident => {
err = Err(DeriveError::UnexpectedAttribute {
meta_span: ident.span(),
});
}
}
Ok(())
})
.map_err(DeriveError::Syn)?;
err?; // Shortcircuit here if `err` is holding some error.
}
Ok(container_attrs)
}
}
pub fn filter<'a>(
iter: impl Iterator<Item = &'a Attribute>,
) -> impl Iterator<Item = &'a Attribute> {
iter.filter(|attr| attr.path().is_ident(HELPER_ATTRIBUTE))
}
// The deny functions are built to easily deny the use of the helper attribute if used incorrectly.
// As the usage of it gets more complex, these functions might be discarded or replaced.
/// Deny any attribute that uses the helper attribute.
pub fn deny<M>(attrs: &[Attribute]) -> Result<(), DeriveError<M>> {
match filter(attrs.iter()).next() {
Some(attr) => Err(DeriveError::InvalidAttributePosition {
attribute_span: attr.span(),
}),
None => Ok(()),
}
}
/// Deny any attributes that uses the helper attribute on any field.
pub fn deny_fields<M>(fields: &Fields) -> Result<(), DeriveError<M>> {
match fields {
Fields::Named(fields) => {
for field in fields.named.iter() {
deny(&field.attrs)?;
}
}
Fields::Unnamed(fields) => {
for field in fields.unnamed.iter() {
deny(&field.attrs)?;
}
}
Fields::Unit => (),
}
Ok(())
}

View File

@ -0,0 +1,82 @@
use std::{any, fmt::Debug, marker::PhantomData};
use proc_macro2::Span;
use proc_macro_error::{Diagnostic, Level};
#[derive(Debug)]
pub enum DeriveError<M> {
/// Marker variant, makes the `M` generic parameter valid.
_Marker(PhantomData<M>),
/// Parsing errors thrown by `syn`.
Syn(syn::parse::Error),
/// `syn::DeriveInput` was a union, currently not supported
UnsupportedUnions,
/// Only plain enums are supported right now.
UnsupportedEnums { fields_span: Span },
/// Found a `#[nu_value(x)]` attribute where `x` is unexpected.
UnexpectedAttribute { meta_span: Span },
/// Found a `#[nu_value(x)]` attribute at a invalid position.
InvalidAttributePosition { attribute_span: Span },
/// Found a valid `#[nu_value(x)]` attribute but the passed values is invalid.
InvalidAttributeValue {
value_span: Span,
value: Box<dyn Debug>,
},
}
impl<M> From<DeriveError<M>> for Diagnostic {
fn from(value: DeriveError<M>) -> Self {
let derive_name = any::type_name::<M>().split("::").last().expect("not empty");
match value {
DeriveError::_Marker(_) => panic!("used marker variant"),
DeriveError::Syn(e) => Diagnostic::spanned(e.span(), Level::Error, e.to_string()),
DeriveError::UnsupportedUnions => Diagnostic::new(
Level::Error,
format!("`{derive_name}` cannot be derived from unions"),
)
.help("consider refactoring to a struct".to_string())
.note("if you really need a union, consider opening an issue on Github".to_string()),
DeriveError::UnsupportedEnums { fields_span } => Diagnostic::spanned(
fields_span,
Level::Error,
format!("`{derive_name}` can only be derived from plain enums"),
)
.help(
"consider refactoring your data type to a struct with a plain enum as a field"
.to_string(),
)
.note("more complex enums could be implemented in the future".to_string()),
DeriveError::InvalidAttributePosition { attribute_span } => Diagnostic::spanned(
attribute_span,
Level::Error,
"invalid attribute position".to_string(),
)
.help(format!(
"check documentation for `{derive_name}` for valid placements"
)),
DeriveError::UnexpectedAttribute { meta_span } => {
Diagnostic::spanned(meta_span, Level::Error, "unknown attribute".to_string()).help(
format!("check documentation for `{derive_name}` for valid attributes"),
)
}
DeriveError::InvalidAttributeValue { value_span, value } => {
Diagnostic::spanned(value_span, Level::Error, format!("invalid value {value:?}"))
.help(format!(
"check documentation for `{derive_name}` for valid attribute values"
))
}
}
}
}

View File

@ -0,0 +1,539 @@
use convert_case::Casing;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
use syn::{
spanned::Spanned, Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Generics, Ident,
};
use crate::attributes::{self, ContainerAttributes};
#[derive(Debug)]
pub struct FromValue;
type DeriveError = super::error::DeriveError<FromValue>;
/// Inner implementation of the `#[derive(FromValue)]` macro for structs and enums.
///
/// Uses `proc_macro2::TokenStream` for better testing support, unlike `proc_macro::TokenStream`.
///
/// This function directs the `FromValue` trait derivation to the correct implementation based on
/// the input type:
/// - For structs: [`derive_struct_from_value`]
/// - For enums: [`derive_enum_from_value`]
/// - Unions are not supported and will return an error.
pub fn derive_from_value(input: TokenStream2) -> Result<TokenStream2, DeriveError> {
let input: DeriveInput = syn::parse2(input).map_err(DeriveError::Syn)?;
match input.data {
Data::Struct(data_struct) => Ok(derive_struct_from_value(
input.ident,
data_struct,
input.generics,
input.attrs,
)?),
Data::Enum(data_enum) => Ok(derive_enum_from_value(
input.ident,
data_enum,
input.generics,
input.attrs,
)?),
Data::Union(_) => Err(DeriveError::UnsupportedUnions),
}
}
/// Implements the `#[derive(FromValue)]` macro for structs.
///
/// This function ensures that the helper attribute is not used anywhere, as it is not supported for
/// structs.
/// Other than this, this function provides the impl signature for `FromValue`.
/// The implementation for `FromValue::from_value` is handled by [`struct_from_value`] and the
/// `FromValue::expected_type` is handled by [`struct_expected_type`].
fn derive_struct_from_value(
ident: Ident,
data: DataStruct,
generics: Generics,
attrs: Vec<Attribute>,
) -> Result<TokenStream2, DeriveError> {
attributes::deny(&attrs)?;
attributes::deny_fields(&data.fields)?;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let from_value_impl = struct_from_value(&data);
let expected_type_impl = struct_expected_type(&data.fields);
Ok(quote! {
#[automatically_derived]
impl #impl_generics nu_protocol::FromValue for #ident #ty_generics #where_clause {
#from_value_impl
#expected_type_impl
}
})
}
/// Implements `FromValue::from_value` for structs.
///
/// This function constructs the `from_value` function for structs.
/// The implementation is straightforward as most of the heavy lifting is handled by
/// `parse_value_via_fields`, and this function only needs to construct the signature around it.
///
/// For structs with named fields, this constructs a large return type where each field
/// contains the implementation for that specific field.
/// In structs with unnamed fields, a [`VecDeque`](std::collections::VecDeque) is used to load each
/// field one after another, and the result is used to construct the tuple.
/// For unit structs, this only checks if the input value is `Value::Nothing`.
///
/// # Examples
///
/// These examples show what the macro would generate.
///
/// Struct with named fields:
/// ```rust
/// #[derive(IntoValue)]
/// struct Pet {
/// name: String,
/// age: u8,
/// favorite_toy: Option<String>,
/// }
///
/// impl nu_protocol::FromValue for Pet {
/// fn from_value(
/// v: nu_protocol::Value
/// ) -> std::result::Result<Self, nu_protocol::ShellError> {
/// let span = v.span();
/// let mut record = v.into_record()?;
/// std::result::Result::Ok(Pet {
/// name: <String as nu_protocol::FromValue>::from_value(
/// record
/// .remove("name")
/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
/// col_name: std::string::ToString::to_string("name"),
/// span: std::option::Option::None,
/// src_span: span
/// })?,
/// )?,
/// age: <u8 as nu_protocol::FromValue>::from_value(
/// record
/// .remove("age")
/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
/// col_name: std::string::ToString::to_string("age"),
/// span: std::option::Option::None,
/// src_span: span
/// })?,
/// )?,
/// favorite_toy: <Option<String> as nu_protocol::FromValue>::from_value(
/// record
/// .remove("favorite_toy")
/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
/// col_name: std::string::ToString::to_string("favorite_toy"),
/// span: std::option::Option::None,
/// src_span: span
/// })?,
/// )?,
/// })
/// }
/// }
/// ```
///
/// Struct with unnamed fields:
/// ```rust
/// #[derive(IntoValue)]
/// struct Color(u8, u8, u8);
///
/// impl nu_protocol::FromValue for Color {
/// fn from_value(
/// v: nu_protocol::Value
/// ) -> std::result::Result<Self, nu_protocol::ShellError> {
/// let span = v.span();
/// let list = v.into_list()?;
/// let mut deque: std::collections::VecDeque<_> = std::convert::From::from(list);
/// std::result::Result::Ok(Self(
/// {
/// <u8 as nu_protocol::FromValue>::from_value(
/// deque
/// .pop_front()
/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
/// col_name: std::string::ToString::to_string(&0),
/// span: std::option::Option::None,
/// src_span: span
/// })?,
/// )?
/// },
/// {
/// <u8 as nu_protocol::FromValue>::from_value(
/// deque
/// .pop_front()
/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
/// col_name: std::string::ToString::to_string(&1),
/// span: std::option::Option::None,
/// src_span: span
/// })?,
/// )?
/// },
/// {
/// <u8 as nu_protocol::FromValue>::from_value(
/// deque
/// .pop_front()
/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
/// col_name: std::string::ToString::to_string(&2),
/// span: std::option::Option::None,
/// src_span: span
/// })?,
/// )?
/// }
/// ))
/// }
/// }
/// ```
///
/// Unit struct:
/// ```rust
/// #[derive(IntoValue)]
/// struct Unicorn;
///
/// impl nu_protocol::FromValue for Unicorn {
/// fn from_value(
/// v: nu_protocol::Value
/// ) -> std::result::Result<Self, nu_protocol::ShellError> {
/// match v {
/// nu_protocol::Value::Nothing {..} => Ok(Self),
/// v => std::result::Result::Err(nu_protocol::ShellError::CantConvert {
/// to_type: std::string::ToString::to_string(&<Self as nu_protocol::FromValue>::expected_type()),
/// from_type: std::string::ToString::to_string(&v.get_type()),
/// span: v.span(),
/// help: std::option::Option::None
/// })
/// }
/// }
/// }
/// ```
fn struct_from_value(data: &DataStruct) -> TokenStream2 {
let body = parse_value_via_fields(&data.fields, quote!(Self));
quote! {
fn from_value(
v: nu_protocol::Value
) -> std::result::Result<Self, nu_protocol::ShellError> {
#body
}
}
}
/// Implements `FromValue::expected_type` for structs.
///
/// This function constructs the `expected_type` function for structs.
/// The type depends on the `fields`: named fields construct a record type with every key and type
/// laid out.
/// Unnamed fields construct a custom type with the name in the format like
/// `list[type0, type1, type2]`.
/// No fields expect the `Type::Nothing`.
///
/// # Examples
///
/// These examples show what the macro would generate.
///
/// Struct with named fields:
/// ```rust
/// #[derive(IntoValue)]
/// struct Pet {
/// name: String,
/// age: u8,
/// favorite_toy: Option<String>,
/// }
///
/// impl nu_protocol::FromValue for Pet {
/// fn expected_type() -> nu_protocol::Type {
/// nu_protocol::Type::Record(
/// std::vec![
/// (
/// std::string::ToString::to_string("name"),
/// <String as nu_protocol::FromValue>::expected_type(),
/// ),
/// (
/// std::string::ToString::to_string("age"),
/// <u8 as nu_protocol::FromValue>::expected_type(),
/// ),
/// (
/// std::string::ToString::to_string("favorite_toy"),
/// <Option<String> as nu_protocol::FromValue>::expected_type(),
/// )
/// ].into_boxed_slice()
/// )
/// }
/// }
/// ```
///
/// Struct with unnamed fields:
/// ```rust
/// #[derive(IntoValue)]
/// struct Color(u8, u8, u8);
///
/// impl nu_protocol::FromValue for Color {
/// fn expected_type() -> nu_protocol::Type {
/// nu_protocol::Type::Custom(
/// std::format!(
/// "[{}, {}, {}]",
/// <u8 as nu_protocol::FromValue>::expected_type(),
/// <u8 as nu_protocol::FromValue>::expected_type(),
/// <u8 as nu_protocol::FromValue>::expected_type()
/// )
/// .into_boxed_str()
/// )
/// }
/// }
/// ```
///
/// Unit struct:
/// ```rust
/// #[derive(IntoValue)]
/// struct Unicorn;
///
/// impl nu_protocol::FromValue for Color {
/// fn expected_type() -> nu_protocol::Type {
/// nu_protocol::Type::Nothing
/// }
/// }
/// ```
fn struct_expected_type(fields: &Fields) -> TokenStream2 {
let ty = match fields {
Fields::Named(fields) => {
let fields = fields.named.iter().map(|field| {
let ident = field.ident.as_ref().expect("named has idents");
let ident_s = ident.to_string();
let ty = &field.ty;
quote! {(
std::string::ToString::to_string(#ident_s),
<#ty as nu_protocol::FromValue>::expected_type(),
)}
});
quote!(nu_protocol::Type::Record(
std::vec![#(#fields),*].into_boxed_slice()
))
}
Fields::Unnamed(fields) => {
let mut iter = fields.unnamed.iter();
let fields = fields.unnamed.iter().map(|field| {
let ty = &field.ty;
quote!(<#ty as nu_protocol::FromValue>::expected_type())
});
let mut template = String::new();
template.push('[');
if iter.next().is_some() {
template.push_str("{}")
}
iter.for_each(|_| template.push_str(", {}"));
template.push(']');
quote! {
nu_protocol::Type::Custom(
std::format!(
#template,
#(#fields),*
)
.into_boxed_str()
)
}
}
Fields::Unit => quote!(nu_protocol::Type::Nothing),
};
quote! {
fn expected_type() -> nu_protocol::Type {
#ty
}
}
}
/// Implements the `#[derive(FromValue)]` macro for enums.
///
/// This function constructs the implementation of the `FromValue` trait for enums.
/// It is designed to be on the same level as [`derive_struct_from_value`], even though this
/// function only provides the impl signature for `FromValue`.
/// The actual implementation for `FromValue::from_value` is handled by [`enum_from_value`].
///
/// Since variants are difficult to type with the current type system, this function uses the
/// default implementation for `expected_type`.
fn derive_enum_from_value(
ident: Ident,
data: DataEnum,
generics: Generics,
attrs: Vec<Attribute>,
) -> Result<TokenStream2, DeriveError> {
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let from_value_impl = enum_from_value(&data, &attrs)?;
Ok(quote! {
#[automatically_derived]
impl #impl_generics nu_protocol::FromValue for #ident #ty_generics #where_clause {
#from_value_impl
}
})
}
/// Implements `FromValue::from_value` for enums.
///
/// This function constructs the `from_value` implementation for enums.
/// It only accepts enums with unit variants, as it is currently unclear how other types of enums
/// should be represented via a `Value`.
/// This function checks that every field is a unit variant and constructs a match statement over
/// all possible variants.
/// The input value is expected to be a `Value::String` containing the name of the variant formatted
/// as defined by the `#[nu_value(rename_all = "...")]` attribute.
/// If no attribute is given, [`convert_case::Case::Snake`] is expected.
///
/// If no matching variant is found, `ShellError::CantConvert` is returned.
///
/// This is how such a derived implementation looks:
/// ```rust
/// #[derive(IntoValue)]
/// enum Weather {
/// Sunny,
/// Cloudy,
/// Raining
/// }
///
/// impl nu_protocol::IntoValue for Weather {
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
/// let span = v.span();
/// let ty = v.get_type();
///
/// let s = v.into_string()?;
/// match s.as_str() {
/// "sunny" => std::result::Ok(Self::Sunny),
/// "cloudy" => std::result::Ok(Self::Cloudy),
/// "raining" => std::result::Ok(Self::Raining),
/// _ => std::result::Result::Err(nu_protocol::ShellError::CantConvert {
/// to_type: std::string::ToString::to_string(
/// &<Self as nu_protocol::FromValue>::expected_type()
/// ),
/// from_type: std::string::ToString::to_string(&ty),
/// span: span,help: std::option::Option::None,
/// }),
/// }
/// }
/// }
/// ```
fn enum_from_value(data: &DataEnum, attrs: &[Attribute]) -> Result<TokenStream2, DeriveError> {
let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?;
let arms: Vec<TokenStream2> = data
.variants
.iter()
.map(|variant| {
attributes::deny(&variant.attrs)?;
let ident = &variant.ident;
let ident_s = format!("{ident}")
.as_str()
.to_case(container_attrs.rename_all);
match &variant.fields {
Fields::Named(fields) => Err(DeriveError::UnsupportedEnums {
fields_span: fields.span(),
}),
Fields::Unnamed(fields) => Err(DeriveError::UnsupportedEnums {
fields_span: fields.span(),
}),
Fields::Unit => Ok(quote!(#ident_s => std::result::Result::Ok(Self::#ident))),
}
})
.collect::<Result<_, _>>()?;
Ok(quote! {
fn from_value(
v: nu_protocol::Value
) -> std::result::Result<Self, nu_protocol::ShellError> {
let span = v.span();
let ty = v.get_type();
let s = v.into_string()?;
match s.as_str() {
#(#arms,)*
_ => std::result::Result::Err(nu_protocol::ShellError::CantConvert {
to_type: std::string::ToString::to_string(
&<Self as nu_protocol::FromValue>::expected_type()
),
from_type: std::string::ToString::to_string(&ty),
span: span,
help: std::option::Option::None,
}),
}
}
})
}
/// Parses a `Value` into self.
///
/// This function handles the actual parsing of a `Value` into self.
/// It takes two parameters: `fields` and `self_ident`.
/// The `fields` parameter determines the expected type of `Value`: named fields expect a
/// `Value::Record`, unnamed fields expect a `Value::List`, and a unit expects `Value::Nothing`.
///
/// For named fields, the `fields` parameter indicates which field in the record corresponds to
/// which struct field.
/// For both named and unnamed fields, it also helps cast the type into a `FromValue` type.
/// This approach maintains
/// [hygiene](https://doc.rust-lang.org/reference/macros-by-example.html#hygiene).
///
/// The `self_ident` parameter is used to describe the identifier of the returned value.
/// For structs, `Self` is usually sufficient, but for enums, `Self::Variant` may be needed in the
/// future.
///
/// This function is more complex than the equivalent for `IntoValue` due to error handling
/// requirements.
/// For missing fields, `ShellError::CantFindColumn` is used, and for unit structs,
/// `ShellError::CantConvert` is used.
/// The implementation avoids local variables for fields to prevent accidental shadowing, ensuring
/// that poorly named fields don't cause issues.
/// While this style is not typically recommended in handwritten Rust, it is acceptable for code
/// generation.
fn parse_value_via_fields(fields: &Fields, self_ident: impl ToTokens) -> TokenStream2 {
match fields {
Fields::Named(fields) => {
let fields = fields.named.iter().map(|field| {
// TODO: handle missing fields for Options as None
let ident = field.ident.as_ref().expect("named has idents");
let ident_s = ident.to_string();
let ty = &field.ty;
quote! {
#ident: <#ty as nu_protocol::FromValue>::from_value(
record
.remove(#ident_s)
.ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
col_name: std::string::ToString::to_string(#ident_s),
span: std::option::Option::None,
src_span: span
})?,
)?
}
});
quote! {
let span = v.span();
let mut record = v.into_record()?;
std::result::Result::Ok(#self_ident {#(#fields),*})
}
}
Fields::Unnamed(fields) => {
let fields = fields.unnamed.iter().enumerate().map(|(i, field)| {
let ty = &field.ty;
quote! {{
<#ty as nu_protocol::FromValue>::from_value(
deque
.pop_front()
.ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
col_name: std::string::ToString::to_string(&#i),
span: std::option::Option::None,
src_span: span
})?,
)?
}}
});
quote! {
let span = v.span();
let list = v.into_list()?;
let mut deque: std::collections::VecDeque<_> = std::convert::From::from(list);
std::result::Result::Ok(#self_ident(#(#fields),*))
}
}
Fields::Unit => quote! {
match v {
nu_protocol::Value::Nothing {..} => Ok(#self_ident),
v => std::result::Result::Err(nu_protocol::ShellError::CantConvert {
to_type: std::string::ToString::to_string(&<Self as nu_protocol::FromValue>::expected_type()),
from_type: std::string::ToString::to_string(&v.get_type()),
span: v.span(),
help: std::option::Option::None
})
}
},
}
}

View File

@ -0,0 +1,266 @@
use convert_case::Casing;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
use syn::{
spanned::Spanned, Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Generics, Ident,
Index,
};
use crate::attributes::{self, ContainerAttributes};
#[derive(Debug)]
pub struct IntoValue;
type DeriveError = super::error::DeriveError<IntoValue>;
/// Inner implementation of the `#[derive(IntoValue)]` macro for structs and enums.
///
/// Uses `proc_macro2::TokenStream` for better testing support, unlike `proc_macro::TokenStream`.
///
/// This function directs the `IntoValue` trait derivation to the correct implementation based on
/// the input type:
/// - For structs: [`struct_into_value`]
/// - For enums: [`enum_into_value`]
/// - Unions are not supported and will return an error.
pub fn derive_into_value(input: TokenStream2) -> Result<TokenStream2, DeriveError> {
let input: DeriveInput = syn::parse2(input).map_err(DeriveError::Syn)?;
match input.data {
Data::Struct(data_struct) => Ok(struct_into_value(
input.ident,
data_struct,
input.generics,
input.attrs,
)?),
Data::Enum(data_enum) => Ok(enum_into_value(
input.ident,
data_enum,
input.generics,
input.attrs,
)?),
Data::Union(_) => Err(DeriveError::UnsupportedUnions),
}
}
/// Implements the `#[derive(IntoValue)]` macro for structs.
///
/// Automatically derives the `IntoValue` trait for any struct where each field implements
/// `IntoValue`.
/// For structs with named fields, the derived implementation creates a `Value::Record` using the
/// struct fields as keys.
/// Each field value is converted using the `IntoValue::into_value` method.
/// For structs with unnamed fields, this generates a `Value::List` with each field in the list.
/// For unit structs, this generates `Value::Nothing`, because there is no data.
///
/// Note: The helper attribute `#[nu_value(...)]` is currently not allowed on structs.
///
/// # Examples
///
/// These examples show what the macro would generate.
///
/// Struct with named fields:
/// ```rust
/// #[derive(IntoValue)]
/// struct Pet {
/// name: String,
/// age: u8,
/// favorite_toy: Option<String>,
/// }
///
/// impl nu_protocol::IntoValue for Pet {
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
/// nu_protocol::Value::record(nu_protocol::record! {
/// "name" => nu_protocol::IntoValue::into_value(self.name, span),
/// "age" => nu_protocol::IntoValue::into_value(self.age, span),
/// "favorite_toy" => nu_protocol::IntoValue::into_value(self.favorite_toy, span),
/// }, span)
/// }
/// }
/// ```
///
/// Struct with unnamed fields:
/// ```rust
/// #[derive(IntoValue)]
/// struct Color(u8, u8, u8);
///
/// impl nu_protocol::IntoValue for Color {
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
/// nu_protocol::Value::list(vec![
/// nu_protocol::IntoValue::into_value(self.0, span),
/// nu_protocol::IntoValue::into_value(self.1, span),
/// nu_protocol::IntoValue::into_value(self.2, span),
/// ], span)
/// }
/// }
/// ```
///
/// Unit struct:
/// ```rust
/// #[derive(IntoValue)]
/// struct Unicorn;
///
/// impl nu_protocol::IntoValue for Unicorn {
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
/// nu_protocol::Value::nothing(span)
/// }
/// }
/// ```
fn struct_into_value(
ident: Ident,
data: DataStruct,
generics: Generics,
attrs: Vec<Attribute>,
) -> Result<TokenStream2, DeriveError> {
attributes::deny(&attrs)?;
attributes::deny_fields(&data.fields)?;
let record = match &data.fields {
Fields::Named(fields) => {
let accessor = fields
.named
.iter()
.map(|field| field.ident.as_ref().expect("named has idents"))
.map(|ident| quote!(self.#ident));
fields_return_value(&data.fields, accessor)
}
Fields::Unnamed(fields) => {
let accessor = fields
.unnamed
.iter()
.enumerate()
.map(|(n, _)| Index::from(n))
.map(|index| quote!(self.#index));
fields_return_value(&data.fields, accessor)
}
Fields::Unit => quote!(nu_protocol::Value::nothing(span)),
};
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
Ok(quote! {
#[automatically_derived]
impl #impl_generics nu_protocol::IntoValue for #ident #ty_generics #where_clause {
fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
#record
}
}
})
}
/// Implements the `#[derive(IntoValue)]` macro for enums.
///
/// This function implements the derive macro `IntoValue` for enums.
/// Currently, only unit enum variants are supported as it is not clear how other types of enums
/// should be represented in a `Value`.
/// For simple enums, we represent the enum as a `Value::String`. For other types of variants, we return an error.
/// The variant name will be case-converted as described by the `#[nu_value(rename_all = "...")]` helper attribute.
/// If no attribute is used, the default is `case_convert::Case::Snake`.
/// The implementation matches over all variants, uses the appropriate variant name, and constructs a `Value::String`.
///
/// This is how such a derived implementation looks:
/// ```rust
/// #[derive(IntoValue)]
/// enum Weather {
/// Sunny,
/// Cloudy,
/// Raining
/// }
///
/// impl nu_protocol::IntoValue for Weather {
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
/// match self {
/// Self::Sunny => nu_protocol::Value::string("sunny", span),
/// Self::Cloudy => nu_protocol::Value::string("cloudy", span),
/// Self::Raining => nu_protocol::Value::string("raining", span),
/// }
/// }
/// }
/// ```
fn enum_into_value(
ident: Ident,
data: DataEnum,
generics: Generics,
attrs: Vec<Attribute>,
) -> Result<TokenStream2, DeriveError> {
let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?;
let arms: Vec<TokenStream2> = data
.variants
.into_iter()
.map(|variant| {
attributes::deny(&variant.attrs)?;
let ident = variant.ident;
let ident_s = format!("{ident}")
.as_str()
.to_case(container_attrs.rename_all);
match &variant.fields {
// In the future we can implement more complexe enums here.
Fields::Named(fields) => Err(DeriveError::UnsupportedEnums {
fields_span: fields.span(),
}),
Fields::Unnamed(fields) => Err(DeriveError::UnsupportedEnums {
fields_span: fields.span(),
}),
Fields::Unit => {
Ok(quote!(Self::#ident => nu_protocol::Value::string(#ident_s, span)))
}
}
})
.collect::<Result<_, _>>()?;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
Ok(quote! {
impl #impl_generics nu_protocol::IntoValue for #ident #ty_generics #where_clause {
fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
match self {
#(#arms,)*
}
}
}
})
}
/// Constructs the final `Value` that the macro generates.
///
/// This function handles the construction of the final `Value` that the macro generates.
/// It is currently only used for structs but may be used for enums in the future.
/// The function takes two parameters: the `fields`, which allow iterating over each field of a data
/// type, and the `accessor`.
/// The fields determine whether we need to generate a `Value::Record`, `Value::List`, or
/// `Value::Nothing`.
/// For named fields, they are also directly used to generate the record key.
///
/// The `accessor` parameter generalizes how the data is accessed.
/// For named fields, this is usually the name of the fields preceded by `self` in a struct, and
/// maybe something else for enums.
/// For unnamed fields, this should be an iterator similar to the one with named fields, but
/// accessing tuple fields, so we get `self.n`.
/// For unit structs, this parameter is ignored.
/// By using the accessor like this, we can have the same code for structs and enums with data
/// variants in the future.
fn fields_return_value(
fields: &Fields,
accessor: impl Iterator<Item = impl ToTokens>,
) -> TokenStream2 {
match fields {
Fields::Named(fields) => {
let items: Vec<TokenStream2> = fields
.named
.iter()
.zip(accessor)
.map(|(field, accessor)| {
let ident = field.ident.as_ref().expect("named has idents");
let field = ident.to_string();
quote!(#field => nu_protocol::IntoValue::into_value(#accessor, span))
})
.collect();
quote! {
nu_protocol::Value::record(nu_protocol::record! {
#(#items),*
}, span)
}
}
Fields::Unnamed(fields) => {
let items =
fields.unnamed.iter().zip(accessor).map(
|(_, accessor)| quote!(nu_protocol::IntoValue::into_value(#accessor, span)),
);
quote!(nu_protocol::Value::list(std::vec![#(#items),*], span))
}
Fields::Unit => quote!(nu_protocol::Value::nothing(span)),
}
}

View File

@ -0,0 +1,69 @@
//! Macro implementations of `#[derive(FromValue, IntoValue)]`.
//!
//! As this crate is a [`proc_macro`] crate, it is only allowed to export
//! [procedural macros](https://doc.rust-lang.org/reference/procedural-macros.html).
//! Therefore, it only exports [`IntoValue`] and [`FromValue`].
//!
//! To get documentation for other functions and types used in this crate, run
//! `cargo doc -p nu-derive-value --document-private-items`.
//!
//! This crate uses a lot of
//! [`proc_macro2::TokenStream`](https://docs.rs/proc-macro2/1.0.24/proc_macro2/struct.TokenStream.html)
//! as `TokenStream2` to allow testing the behavior of the macros directly, including the output
//! token stream or if the macro errors as expected.
//! The tests for functionality can be found in `nu_protocol::value::test_derive`.
//!
//! This documentation is often less reference-heavy than typical Rust documentation.
//! This is because this crate is a dependency for `nu_protocol`, and linking to it would create a
//! cyclic dependency.
//! Also all examples in the documentation aren't tested as this crate cannot be compiled as a
//! normal library very easily.
//! This might change in the future if cargo allows building a proc-macro crate differently for
//! `cfg(doctest)` as they are already doing for `cfg(test)`.
//!
//! The generated code from the derive macros tries to be as
//! [hygienic](https://doc.rust-lang.org/reference/macros-by-example.html#hygiene) as possible.
//! This ensures that the macro can be called anywhere without requiring specific imports.
//! This results in obtuse code, which isn't recommended for manual, handwritten Rust
//! but ensures that no other code may influence this generated code or vice versa.
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use proc_macro_error::{proc_macro_error, Diagnostic};
mod attributes;
mod error;
mod from;
mod into;
#[cfg(test)]
mod tests;
const HELPER_ATTRIBUTE: &str = "nu_value";
/// Derive macro generating an impl of the trait `IntoValue`.
///
/// For further information, see the docs on the trait itself.
#[proc_macro_derive(IntoValue, attributes(nu_value))]
#[proc_macro_error]
pub fn derive_into_value(input: TokenStream) -> TokenStream {
let input = TokenStream2::from(input);
let output = match into::derive_into_value(input) {
Ok(output) => output,
Err(e) => Diagnostic::from(e).abort(),
};
TokenStream::from(output)
}
/// Derive macro generating an impl of the trait `FromValue`.
///
/// For further information, see the docs on the trait itself.
#[proc_macro_derive(FromValue, attributes(nu_value))]
#[proc_macro_error]
pub fn derive_from_value(input: TokenStream) -> TokenStream {
let input = TokenStream2::from(input);
let output = match from::derive_from_value(input) {
Ok(output) => output,
Err(e) => Diagnostic::from(e).abort(),
};
TokenStream::from(output)
}

View File

@ -0,0 +1,157 @@
// These tests only check that the derive macros throw the relevant errors.
// Functionality of the derived types is tested in nu_protocol::value::test_derive.
use crate::error::DeriveError;
use crate::from::derive_from_value;
use crate::into::derive_into_value;
use quote::quote;
#[test]
fn unsupported_unions() {
let input = quote! {
#[nu_value]
union SomeUnion {
f1: u32,
f2: f32,
}
};
let from_res = derive_from_value(input.clone());
assert!(
matches!(from_res, Err(DeriveError::UnsupportedUnions)),
"expected `DeriveError::UnsupportedUnions`, got {:?}",
from_res
);
let into_res = derive_into_value(input);
assert!(
matches!(into_res, Err(DeriveError::UnsupportedUnions)),
"expected `DeriveError::UnsupportedUnions`, got {:?}",
into_res
);
}
#[test]
fn unsupported_enums() {
let input = quote! {
#[nu_value(rename_all = "SCREAMING_SNAKE_CASE")]
enum ComplexEnum {
Unit,
Unnamed(u32, f32),
Named {
u: u32,
f: f32,
}
}
};
let from_res = derive_from_value(input.clone());
assert!(
matches!(from_res, Err(DeriveError::UnsupportedEnums { .. })),
"expected `DeriveError::UnsupportedEnums`, got {:?}",
from_res
);
let into_res = derive_into_value(input);
assert!(
matches!(into_res, Err(DeriveError::UnsupportedEnums { .. })),
"expected `DeriveError::UnsupportedEnums`, got {:?}",
into_res
);
}
#[test]
fn unexpected_attribute() {
let input = quote! {
#[nu_value(what)]
enum SimpleEnum {
A,
B,
}
};
let from_res = derive_from_value(input.clone());
assert!(
matches!(from_res, Err(DeriveError::UnexpectedAttribute { .. })),
"expected `DeriveError::UnexpectedAttribute`, got {:?}",
from_res
);
let into_res = derive_into_value(input);
assert!(
matches!(into_res, Err(DeriveError::UnexpectedAttribute { .. })),
"expected `DeriveError::UnexpectedAttribute`, got {:?}",
into_res
);
}
#[test]
fn deny_attribute_on_structs() {
let input = quote! {
#[nu_value]
struct SomeStruct;
};
let from_res = derive_from_value(input.clone());
assert!(
matches!(from_res, Err(DeriveError::InvalidAttributePosition { .. })),
"expected `DeriveError::InvalidAttributePosition`, got {:?}",
from_res
);
let into_res = derive_into_value(input);
assert!(
matches!(into_res, Err(DeriveError::InvalidAttributePosition { .. })),
"expected `DeriveError::InvalidAttributePosition`, got {:?}",
into_res
);
}
#[test]
fn deny_attribute_on_fields() {
let input = quote! {
struct SomeStruct {
#[nu_value]
field: ()
}
};
let from_res = derive_from_value(input.clone());
assert!(
matches!(from_res, Err(DeriveError::InvalidAttributePosition { .. })),
"expected `DeriveError::InvalidAttributePosition`, got {:?}",
from_res
);
let into_res = derive_into_value(input);
assert!(
matches!(into_res, Err(DeriveError::InvalidAttributePosition { .. })),
"expected `DeriveError::InvalidAttributePosition`, got {:?}",
into_res
);
}
#[test]
fn invalid_attribute_value() {
let input = quote! {
#[nu_value(rename_all = "CrazY-CasE")]
enum SimpleEnum {
A,
B
}
};
let from_res = derive_from_value(input.clone());
assert!(
matches!(from_res, Err(DeriveError::InvalidAttributeValue { .. })),
"expected `DeriveError::InvalidAttributeValue`, got {:?}",
from_res
);
let into_res = derive_into_value(input);
assert!(
matches!(into_res, Err(DeriveError::InvalidAttributeValue { .. })),
"expected `DeriveError::InvalidAttributeValue`, got {:?}",
into_res
);
}

View File

@ -3,7 +3,7 @@ use nu_protocol::{
ast::{Argument, Call, Expr, Expression, RecordItem},
debugger::WithoutDebug,
engine::{Command, EngineState, Stack, UNKNOWN_SPAN_ID},
record, Category, Example, IntoPipelineData, PipelineData, Signature, Span, SpanId,
record, Category, Example, IntoPipelineData, PipelineData, Signature, Span, SpanId, Spanned,
SyntaxShape, Type, Value,
};
use std::{collections::HashMap, fmt::Write};
@ -296,6 +296,28 @@ fn get_documentation(
}
if let Some(result) = &example.result {
let mut table_call = Call::new(Span::unknown());
if example.example.ends_with("--collapse") {
// collapse the result
table_call.add_named((
Spanned {
item: "collapse".to_string(),
span: Span::unknown(),
},
None,
None,
))
} else {
// expand the result
table_call.add_named((
Spanned {
item: "expand".to_string(),
span: Span::unknown(),
},
None,
None,
))
}
let table = engine_state
.find_decl("table".as_bytes(), &[])
.and_then(|decl_id| {
@ -304,7 +326,7 @@ fn get_documentation(
.run(
engine_state,
stack,
&Call::new(Span::new(0, 0)),
&table_call,
PipelineData::Value(result.clone(), None),
)
.ok()

View File

@ -208,11 +208,13 @@ fn eval_external(
) -> Result<PipelineData, ShellError> {
let decl_id = engine_state
.find_decl("run-external".as_bytes(), &[])
.ok_or(ShellError::ExternalNotSupported { span: head.span })?;
.ok_or(ShellError::ExternalNotSupported {
span: head.span(&engine_state),
})?;
let command = engine_state.get_decl(decl_id);
let mut call = Call::new(head.span);
let mut call = Call::new(head.span(&engine_state));
call.add_positional(head.clone());
@ -712,7 +714,7 @@ impl Eval for EvalRuntime {
args: &[ExternalArgument],
_: Span,
) -> Result<Value, ShellError> {
let span = head.span;
let span = head.span(&engine_state);
// FIXME: protect this collect with ctrl-c
eval_external(engine_state, stack, head, args, PipelineData::empty())?.into_value(span)
}
@ -779,9 +781,11 @@ impl Eval for EvalRuntime {
let var_info = engine_state.get_var(*var_id);
if var_info.mutable {
stack.add_var(*var_id, rhs);
Ok(Value::nothing(lhs.span))
Ok(Value::nothing(lhs.span(&engine_state)))
} else {
Err(ShellError::AssignmentRequiresMutableVar { lhs_span: lhs.span })
Err(ShellError::AssignmentRequiresMutableVar {
lhs_span: lhs.span(&engine_state),
})
}
}
Expr::FullCellPath(cell_path) => {
@ -797,7 +801,7 @@ impl Eval for EvalRuntime {
// Reject attempts to assign to the entire $env
if cell_path.tail.is_empty() {
return Err(ShellError::CannotReplaceEnv {
span: cell_path.head.span,
span: cell_path.head.span(&engine_state),
});
}
@ -837,15 +841,21 @@ impl Eval for EvalRuntime {
lhs.upsert_data_at_cell_path(&cell_path.tail, rhs)?;
stack.add_var(*var_id, lhs);
}
Ok(Value::nothing(cell_path.head.span))
Ok(Value::nothing(cell_path.head.span(&engine_state)))
} else {
Err(ShellError::AssignmentRequiresMutableVar { lhs_span: lhs.span })
Err(ShellError::AssignmentRequiresMutableVar {
lhs_span: lhs.span(&engine_state),
})
}
}
_ => Err(ShellError::AssignmentRequiresVar { lhs_span: lhs.span }),
_ => Err(ShellError::AssignmentRequiresVar {
lhs_span: lhs.span(&engine_state),
}),
}
}
_ => Err(ShellError::AssignmentRequiresVar { lhs_span: lhs.span }),
_ => Err(ShellError::AssignmentRequiresVar {
lhs_span: lhs.span(&engine_state),
}),
}
}
@ -882,8 +892,8 @@ impl Eval for EvalRuntime {
Ok(Value::string(name, span))
}
fn unreachable(expr: &Expression) -> Result<Value, ShellError> {
Ok(Value::nothing(expr.span))
fn unreachable(engine_state: &EngineState, expr: &Expression) -> Result<Value, ShellError> {
Ok(Value::nothing(expr.span(&engine_state)))
}
}

View File

@ -27,7 +27,7 @@ impl NuCmd {
}
impl ViewCommand for NuCmd {
type View = NuView<'static>;
type View = NuView;
fn name(&self) -> &'static str {
Self::NAME
@ -72,12 +72,12 @@ impl ViewCommand for NuCmd {
}
}
pub enum NuView<'a> {
Records(RecordView<'a>),
pub enum NuView {
Records(RecordView),
Preview(Preview),
}
impl View for NuView<'_> {
impl View for NuView {
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) {
match self {
NuView::Records(v) => v.draw(f, area, cfg, layout),

View File

@ -30,7 +30,7 @@ impl TableCmd {
}
impl ViewCommand for TableCmd {
type View = RecordView<'static>;
type View = RecordView;
fn name(&self) -> &'static str {
Self::NAME

View File

@ -22,7 +22,7 @@ impl TryCmd {
}
impl ViewCommand for TryCmd {
type View = TryView<'static>;
type View = TryView;
fn name(&self) -> &'static str {
Self::NAME

View File

@ -23,23 +23,20 @@ use nu_protocol::{
Config, Record, Span, Value,
};
use ratatui::{layout::Rect, widgets::Block};
use std::{borrow::Cow, collections::HashMap};
use std::collections::HashMap;
pub use self::table_widget::Orientation;
#[derive(Debug, Clone)]
pub struct RecordView<'a> {
layer_stack: Vec<RecordLayer<'a>>,
pub struct RecordView {
layer_stack: Vec<RecordLayer>,
mode: UIMode,
orientation: Orientation,
cfg: ExploreConfig,
}
impl<'a> RecordView<'a> {
pub fn new(
columns: impl Into<Cow<'a, [String]>>,
records: impl Into<Cow<'a, [Vec<Value>]>>,
) -> Self {
impl RecordView {
pub fn new(columns: Vec<String>, records: Vec<Vec<Value>>) -> Self {
Self {
layer_stack: vec![RecordLayer::new(columns, records)],
mode: UIMode::View,
@ -64,13 +61,13 @@ impl<'a> RecordView<'a> {
}
// todo: rename to get_layer
pub fn get_layer_last(&self) -> &RecordLayer<'a> {
pub fn get_layer_last(&self) -> &RecordLayer {
self.layer_stack
.last()
.expect("we guarantee that 1 entry is always in a list")
}
pub fn get_layer_last_mut(&mut self) -> &mut RecordLayer<'a> {
pub fn get_layer_last_mut(&mut self) -> &mut RecordLayer {
self.layer_stack
.last_mut()
.expect("we guarantee that 1 entry is always in a list")
@ -134,32 +131,39 @@ impl<'a> RecordView<'a> {
Orientation::Left => (column, row),
};
if layer.records.len() > row && layer.records[row].len() > column {
layer.records[row][column].clone()
} else {
Value::nothing(Span::unknown())
if row >= layer.count_rows() || column >= layer.count_columns() {
// actually must never happen; unless cursor works incorrectly
// if being sure about cursor it can be deleted;
return Value::nothing(Span::unknown());
}
layer.record_values[row][column].clone()
}
/// Create a table widget.
/// WARNING: this is currently really slow on large data sets.
/// It creates a string representation of every cell in the table and looks at every row for lscolorize.
fn create_table_widget(&'a self, cfg: ViewConfig<'a>) -> TableWidget<'a> {
let layer = self.get_layer_last();
let mut data = convert_records_to_string(&layer.records, cfg.nu_config, cfg.style_computer);
lscolorize(&layer.columns, &mut data, cfg.lscolors);
let headers = layer.columns.as_ref();
fn create_table_widget<'a>(&'a mut self, cfg: ViewConfig<'a>) -> TableWidget<'a> {
let style = self.cfg.table;
let style_computer = cfg.style_computer;
let Position { row, column } = self.get_window_origin();
let layer = self.get_layer_last_mut();
if layer.record_text.is_none() {
let mut data =
convert_records_to_string(&layer.record_values, cfg.nu_config, cfg.style_computer);
lscolorize(&layer.column_names, &mut data, cfg.lscolors);
layer.record_text = Some(data);
}
let headers = &layer.column_names;
let data = layer.record_text.as_ref().expect("always ok");
TableWidget::new(
headers,
data,
style_computer,
row,
column,
self.cfg.table,
style,
layer.orientation,
)
}
@ -197,7 +201,7 @@ impl<'a> RecordView<'a> {
}
}
impl View for RecordView<'_> {
impl View for RecordView {
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) {
let mut table_layout = TableWidgetState::default();
// TODO: creating the table widget is O(N) where N is the number of cells in the grid.
@ -265,7 +269,7 @@ impl View for RecordView<'_> {
let style_computer = StyleComputer::new(&dummy_engine_state, &dummy_stack, HashMap::new());
let data = convert_records_to_string(
&self.get_layer_last().records,
&self.get_layer_last().record_values,
&nu_protocol::Config::default(),
&style_computer,
);
@ -274,7 +278,7 @@ impl View for RecordView<'_> {
}
fn show_data(&mut self, pos: usize) -> bool {
let data = &self.get_layer_last().records;
let data = &self.get_layer_last().record_values;
let mut i = 0;
for (row, cells) in data.iter().enumerate() {
@ -332,30 +336,35 @@ enum UIMode {
}
#[derive(Debug, Clone)]
pub struct RecordLayer<'a> {
columns: Cow<'a, [String]>,
records: Cow<'a, [Vec<Value>]>,
pub struct RecordLayer {
column_names: Vec<String>,
// These are the raw records in the current layer. The sole reason we keep this around is so we can return the original value
// if it's being peeked. Otherwise we could accept an iterator over it.
// or if it could be Clonable we could do that anyway;
// cause it would keep memory footprint lower while keep everything working
// (yee would make return O(n); we would need to traverse iterator once again; but maybe worth it)
record_values: Vec<Vec<Value>>,
// This is the text representation of the record values (the actual text that will be displayed to users).
// It's an Option because we need configuration to set it and we (currently) don't have access to configuration when things are created.
record_text: Option<Vec<Vec<NuText>>>,
orientation: Orientation,
name: Option<String>,
was_transposed: bool,
cursor: WindowCursor2D,
}
impl<'a> RecordLayer<'a> {
fn new(
columns: impl Into<Cow<'a, [String]>>,
records: impl Into<Cow<'a, [Vec<Value>]>>,
) -> Self {
let columns = columns.into();
let records = records.into();
impl RecordLayer {
fn new(columns: Vec<String>, records: Vec<Vec<Value>>) -> Self {
// TODO: refactor so this is fallible and returns a Result instead of panicking
let cursor =
WindowCursor2D::new(records.len(), columns.len()).expect("Failed to create cursor");
let column_names = columns.iter().map(|s| strip_string(s)).collect();
Self {
columns,
records,
column_names,
record_values: records,
record_text: None,
cursor,
orientation: Orientation::Top,
name: None,
@ -369,21 +378,21 @@ impl<'a> RecordLayer<'a> {
fn count_rows(&self) -> usize {
match self.orientation {
Orientation::Top => self.records.len(),
Orientation::Left => self.columns.len(),
Orientation::Top => self.record_values.len(),
Orientation::Left => self.column_names.len(),
}
}
fn count_columns(&self) -> usize {
match self.orientation {
Orientation::Top => self.columns.len(),
Orientation::Left => self.records.len(),
Orientation::Top => self.column_names.len(),
Orientation::Left => self.record_values.len(),
}
}
fn get_column_header(&self) -> Option<String> {
let col = self.cursor.column();
self.columns.get(col).map(|header| header.to_string())
self.column_names.get(col).map(|header| header.to_string())
}
fn reset_cursor(&mut self) {
@ -570,13 +579,13 @@ fn handle_key_event_cursor_mode(
}
}
fn create_layer(value: Value) -> Result<RecordLayer<'static>> {
fn create_layer(value: Value) -> Result<RecordLayer> {
let (columns, values) = collect_input(value)?;
Ok(RecordLayer::new(columns, values))
}
fn push_layer(view: &mut RecordView<'_>, mut next_layer: RecordLayer<'static>) {
fn push_layer(view: &mut RecordView, mut next_layer: RecordLayer) {
let layer = view.get_layer_last();
let header = layer.get_column_header();
@ -599,9 +608,9 @@ fn estimate_page_size(area: Rect, show_head: bool) -> u16 {
}
/// scroll to the end of the data
fn tail_data(state: &mut RecordView<'_>, page_size: usize) {
fn tail_data(state: &mut RecordView, page_size: usize) {
let layer = state.get_layer_last_mut();
let count_rows = layer.records.len();
let count_rows = layer.count_rows();
if count_rows > page_size {
layer
.cursor
@ -620,6 +629,7 @@ fn convert_records_to_string(
row.iter()
.map(|value| {
let text = value.clone().to_abbreviated_string(cfg);
let text = strip_string(&text);
let float_precision = cfg.float_precision as usize;
make_styled_string(style_computer, text, Some(value), float_precision)
@ -649,12 +659,16 @@ fn build_last_value(v: &RecordView) -> Value {
fn build_table_as_list(v: &RecordView) -> Value {
let layer = v.get_layer_last();
let cols = &layer.columns;
let vals = layer
.records
.record_values
.iter()
.map(|vals| {
let record = cols.iter().cloned().zip(vals.iter().cloned()).collect();
let record = layer
.column_names
.iter()
.cloned()
.zip(vals.iter().cloned())
.collect();
Value::record(record, NuSpan::unknown())
})
.collect();
@ -665,16 +679,15 @@ fn build_table_as_list(v: &RecordView) -> Value {
fn build_table_as_record(v: &RecordView) -> Value {
let layer = v.get_layer_last();
let record = if let Some(row) = layer.records.first() {
layer
.columns
let mut record = Record::new();
if let Some(row) = layer.record_values.first() {
record = layer
.column_names
.iter()
.cloned()
.zip(row.iter().cloned())
.collect()
} else {
Record::new()
};
.collect();
}
Value::record(record, NuSpan::unknown())
}
@ -708,17 +721,12 @@ fn get_percentage(value: usize, max: usize) -> usize {
((value as f32 / max as f32) * 100.0).floor() as usize
}
fn transpose_table(layer: &mut RecordLayer<'_>) {
let count_rows = layer.records.len();
let count_columns = layer.columns.len();
fn transpose_table(layer: &mut RecordLayer) {
let count_rows = layer.record_values.len();
let count_columns = layer.column_names.len();
if layer.was_transposed {
let data = match &mut layer.records {
Cow::Owned(data) => data,
Cow::Borrowed(_) => unreachable!("must never happen"),
};
let headers = pop_first_column(data);
let headers = pop_first_column(&mut layer.record_values);
let headers = headers
.into_iter()
.map(|value| match value {
@ -727,23 +735,25 @@ fn transpose_table(layer: &mut RecordLayer<'_>) {
})
.collect();
let data = _transpose_table(data, count_rows, count_columns - 1);
let data = _transpose_table(&layer.record_values, count_rows, count_columns - 1);
layer.records = Cow::Owned(data);
layer.columns = Cow::Owned(headers);
} else {
let mut data = _transpose_table(&layer.records, count_rows, count_columns);
layer.record_values = data;
layer.column_names = headers;
for (column, column_name) in layer.columns.iter().enumerate() {
let value = Value::string(column_name, NuSpan::unknown());
data[column].insert(0, value);
}
layer.records = Cow::Owned(data);
layer.columns = (1..count_rows + 1 + 1).map(|i| i.to_string()).collect();
return;
}
let mut data = _transpose_table(&layer.record_values, count_rows, count_columns);
for (column, column_name) in layer.column_names.iter().enumerate() {
let value = Value::string(column_name, NuSpan::unknown());
data[column].insert(0, value);
}
layer.record_values = data;
layer.column_names = (1..count_rows + 1 + 1).map(|i| i.to_string()).collect();
layer.was_transposed = !layer.was_transposed;
}
@ -770,3 +780,9 @@ fn _transpose_table(
data
}
fn strip_string(text: &str) -> String {
String::from_utf8(strip_ansi_escapes::strip(text))
.map_err(|_| ())
.unwrap_or_else(|_| text.to_owned())
}

View File

@ -13,15 +13,12 @@ use ratatui::{
text::Span,
widgets::{Block, Borders, Paragraph, StatefulWidget, Widget},
};
use std::{
borrow::Cow,
cmp::{max, Ordering},
};
use std::cmp::{max, Ordering};
#[derive(Debug, Clone)]
pub struct TableWidget<'a> {
columns: Cow<'a, [String]>,
data: Cow<'a, [Vec<NuText>]>,
columns: &'a [String],
data: &'a [Vec<NuText>],
index_row: usize,
index_column: usize,
config: TableConfig,
@ -39,8 +36,8 @@ pub enum Orientation {
impl<'a> TableWidget<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
columns: impl Into<Cow<'a, [String]>>,
data: impl Into<Cow<'a, [Vec<NuText>]>>,
columns: &'a [String],
data: &'a [Vec<NuText>],
style_computer: &'a StyleComputer<'a>,
index_row: usize,
index_column: usize,
@ -48,8 +45,8 @@ impl<'a> TableWidget<'a> {
header_position: Orientation,
) -> Self {
Self {
columns: columns.into(),
data: data.into(),
columns,
data,
style_computer,
index_row,
index_column,
@ -197,22 +194,25 @@ impl<'a> TableWidget<'a> {
}
if show_head {
let mut header = [head_row_text(&head, self.style_computer)];
let head_style = head_style(&head, self.style_computer);
if head_width > use_space as usize {
truncate_str(&mut header[0].0, use_space as usize)
truncate_str(&mut head, use_space as usize)
}
let head_iter = [(&head, head_style)].into_iter();
let mut w = width;
w += render_space(buf, w, head_y, 1, padding_l);
w += render_column(buf, w, head_y, use_space, &header);
w += render_column(buf, w, head_y, use_space, head_iter);
w += render_space(buf, w, head_y, 1, padding_r);
let x = w - padding_r - use_space;
state.layout.push(&header[0].0, x, head_y, use_space, 1);
state.layout.push(&head, x, head_y, use_space, 1);
}
let head_rows = column.iter().map(|(t, s)| (t, *s));
width += render_space(buf, width, data_y, data_height, padding_l);
width += render_column(buf, width, data_y, use_space, &column);
width += render_column(buf, width, data_y, use_space, head_rows);
width += render_space(buf, width, data_y, data_height, padding_r);
for (row, (text, _)) in column.iter().enumerate() {
@ -305,10 +305,9 @@ impl<'a> TableWidget<'a> {
return;
}
let columns = columns
let columns_iter = columns
.iter()
.map(|s| head_row_text(s, self.style_computer))
.collect::<Vec<_>>();
.map(|s| (s.clone(), head_style(s, self.style_computer)));
if !show_index {
let x = area.x + left_w;
@ -326,12 +325,12 @@ impl<'a> TableWidget<'a> {
let x = area.x + left_w;
left_w += render_space(buf, x, area.y, 1, padding_l);
let x = area.x + left_w;
left_w += render_column(buf, x, area.y, columns_width as u16, &columns);
left_w += render_column(buf, x, area.y, columns_width as u16, columns_iter);
let x = area.x + left_w;
left_w += render_space(buf, x, area.y, 1, padding_r);
let layout_x = left_w - padding_r - columns_width as u16;
for (i, (text, _)) in columns.iter().enumerate() {
for (i, text) in columns.iter().enumerate() {
state
.layout
.push(text, layout_x, area.y + i as u16, columns_width as u16, 1);
@ -377,10 +376,12 @@ impl<'a> TableWidget<'a> {
break;
}
let head_rows = column.iter().map(|(t, s)| (t, *s));
let x = area.x + left_w;
left_w += render_space(buf, x, area.y, area.height, padding_l);
let x = area.x + left_w;
left_w += render_column(buf, x, area.y, column_width, &column);
left_w += render_column(buf, x, area.y, column_width, head_rows);
let x = area.x + left_w;
left_w += render_space(buf, x, area.y, area.height, padding_r);
@ -619,14 +620,14 @@ fn repeat_vertical(
c: char,
style: TextStyle,
) {
let text = std::iter::repeat(c)
.take(width as usize)
.collect::<String>();
let text = String::from(c);
let style = text_style_to_tui_style(style);
let span = Span::styled(text, style);
let span = Span::styled(&text, style);
for row in 0..height {
buf.set_span(x, y + row, &span, width);
for col in 0..width {
buf.set_span(x + col, y + row, &span, 1);
}
}
}
@ -676,35 +677,28 @@ fn calculate_column_width(column: &[NuText]) -> usize {
.unwrap_or(0)
}
fn render_column(
fn render_column<T, S>(
buf: &mut ratatui::buffer::Buffer,
x: u16,
y: u16,
available_width: u16,
rows: &[NuText],
) -> u16 {
for (row, (text, style)) in rows.iter().enumerate() {
let style = text_style_to_tui_style(*style);
let text = strip_string(text);
let span = Span::styled(text, style);
rows: impl Iterator<Item = (T, S)>,
) -> u16
where
T: AsRef<str>,
S: Into<TextStyle>,
{
for (row, (text, style)) in rows.enumerate() {
let style = text_style_to_tui_style(style.into());
let span = Span::styled(text.as_ref(), style);
buf.set_span(x, y + row as u16, &span, available_width);
}
available_width
}
fn strip_string(text: &str) -> String {
String::from_utf8(strip_ansi_escapes::strip(text))
.map_err(|_| ())
.unwrap_or_else(|_| text.to_owned())
}
fn head_row_text(head: &str, style_computer: &StyleComputer) -> NuText {
(
String::from(head),
TextStyle::with_style(
Alignment::Center,
style_computer.compute("header", &Value::string(head, nu_protocol::Span::unknown())),
),
)
fn head_style(head: &str, style_computer: &StyleComputer) -> TextStyle {
let style =
style_computer.compute("header", &Value::string(head, nu_protocol::Span::unknown()));
TextStyle::with_style(Alignment::Center, style)
}

View File

@ -16,21 +16,21 @@ use ratatui::{
};
use std::cmp::min;
pub struct TryView<'a> {
pub struct TryView {
input: Value,
command: String,
reactive: bool,
table: Option<RecordView<'a>>,
immediate: bool,
table: Option<RecordView>,
view_mode: bool,
border_color: Style,
}
impl<'a> TryView<'a> {
impl TryView {
pub fn new(input: Value) -> Self {
Self {
input,
table: None,
reactive: false,
immediate: false,
border_color: Style::default(),
view_mode: false,
command: String::new(),
@ -48,7 +48,7 @@ impl<'a> TryView<'a> {
}
}
impl View for TryView<'_> {
impl View for TryView {
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) {
let border_color = self.border_color;
@ -178,7 +178,7 @@ impl View for TryView<'_> {
if !self.command.is_empty() {
self.command.pop();
if self.reactive {
if self.immediate {
match self.try_run(engine_state, stack) {
Ok(_) => info.report = Some(Report::default()),
Err(err) => info.report = Some(Report::error(format!("Error: {err}"))),
@ -191,7 +191,7 @@ impl View for TryView<'_> {
KeyCode::Char(c) => {
self.command.push(*c);
if self.reactive {
if self.immediate {
match self.try_run(engine_state, stack) {
Ok(_) => info.report = Some(Report::default()),
Err(err) => info.report = Some(Report::error(format!("Error: {err}"))),
@ -235,7 +235,7 @@ impl View for TryView<'_> {
fn setup(&mut self, config: ViewConfig<'_>) {
self.border_color = nu_style_to_tui(config.explore_config.table.separator_style);
self.reactive = config.explore_config.try_reactive;
self.immediate = config.explore_config.try_reactive;
let mut r = RecordView::new(vec![], vec![]);
r.setup(config);
@ -253,7 +253,7 @@ fn run_command(
input: &Value,
engine_state: &EngineState,
stack: &mut Stack,
) -> Result<RecordView<'static>> {
) -> Result<RecordView> {
let pipeline = run_command_with_value(command, input, engine_state, stack)?;
let is_record = matches!(pipeline, PipelineData::Value(Value::Record { .. }, ..));

View File

@ -6,18 +6,36 @@ pub fn home_dir() -> Option<PathBuf> {
dirs_next::home_dir()
}
/// Return the data directory for the current platform or XDG_DATA_HOME if specified.
pub fn data_dir() -> Option<PathBuf> {
match std::env::var("XDG_DATA_HOME").map(PathBuf::from) {
Ok(xdg_data) if xdg_data.is_absolute() => Some(canonicalize(&xdg_data).unwrap_or(xdg_data)),
_ => get_canonicalized_path(dirs_next::data_dir()),
}
}
/// Return the cache directory for the current platform or XDG_CACHE_HOME if specified.
pub fn cache_dir() -> Option<PathBuf> {
match std::env::var("XDG_CACHE_HOME").map(PathBuf::from) {
Ok(xdg_cache) if xdg_cache.is_absolute() => {
Some(canonicalize(&xdg_cache).unwrap_or(xdg_cache))
}
_ => get_canonicalized_path(dirs_next::cache_dir()),
}
}
/// Return the config directory for the current platform or XDG_CONFIG_HOME if specified.
pub fn config_dir() -> Option<PathBuf> {
match std::env::var("XDG_CONFIG_HOME").map(PathBuf::from) {
Ok(xdg_config) if xdg_config.is_absolute() => {
Some(canonicalize(&xdg_config).unwrap_or(xdg_config))
}
_ => config_dir_old(),
_ => get_canonicalized_path(dirs_next::config_dir()),
}
}
/// Get the old default config directory. Outside of Linux, this will ignore `XDG_CONFIG_HOME`
pub fn config_dir_old() -> Option<PathBuf> {
let path = dirs_next::config_dir()?;
pub fn get_canonicalized_path(path: Option<PathBuf>) -> Option<PathBuf> {
let path = path?;
Some(canonicalize(&path).unwrap_or(path))
}

View File

@ -8,6 +8,6 @@ mod trailing_slash;
pub use components::components;
pub use expansions::{canonicalize_with, expand_path_with, expand_to_real_path, locate_in_dirs};
pub use helpers::{config_dir, config_dir_old, home_dir};
pub use helpers::{cache_dir, config_dir, data_dir, get_canonicalized_path, home_dir};
pub use tilde::expand_tilde;
pub use trailing_slash::{has_trailing_slash, strip_trailing_slash};

View File

@ -16,11 +16,13 @@ bench = false
nu-utils = { path = "../nu-utils", version = "0.94.3" }
nu-path = { path = "../nu-path", version = "0.94.3" }
nu-system = { path = "../nu-system", version = "0.94.3" }
nu-derive-value = { path = "../nu-derive-value", version = "0.94.3" }
brotli = { workspace = true, optional = true }
byte-unit = { version = "5.1", features = [ "serde" ] }
chrono = { workspace = true, features = [ "serde", "std", "unstable-locales" ], default-features = false }
chrono-humanize = { workspace = true }
convert_case = { workspace = true }
fancy-regex = { workspace = true }
indexmap = { workspace = true }
lru = { workspace = true }

View File

@ -1,7 +1,7 @@
use crate::{
ast::{Argument, Block, Expr, ExternalArgument, ImportPattern, MatchPattern, RecordItem},
engine::{EngineState, StateWorkingSet},
BlockId, DeclId, Signature, Span, SpanId, Type, VarId, IN_VARIABLE_ID,
engine::StateWorkingSet,
BlockId, DeclId, GetSpan, Signature, Span, SpanId, Type, VarId, IN_VARIABLE_ID,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
@ -516,7 +516,7 @@ impl Expression {
}
}
pub fn span(&self, engine_state: &EngineState) -> Span {
engine_state.get_span(self.span_id)
pub fn span(&self, state: &impl GetSpan) -> Span {
state.get_span(self.span_id)
}
}

View File

@ -7,7 +7,7 @@ use crate::{
Variable, Visibility, DEFAULT_OVERLAY_NAME,
},
eval_const::create_nu_constant,
BlockId, Category, Config, DeclId, FileId, HistoryConfig, Module, ModuleId, OverlayId,
BlockId, Category, Config, DeclId, FileId, GetSpan, HistoryConfig, Module, ModuleId, OverlayId,
ShellError, Signature, Span, SpanId, Type, Value, VarId, VirtualPathId,
};
use fancy_regex::Regex;
@ -1035,18 +1035,20 @@ impl EngineState {
SpanId(self.num_spans() - 1)
}
/// Find ID of a span (should be avoided if possible)
pub fn find_span_id(&self, span: Span) -> Option<SpanId> {
self.spans.iter().position(|sp| sp == &span).map(SpanId)
}
}
impl<'a> GetSpan for &'a EngineState {
/// Get existing span
pub fn get_span(&self, span_id: SpanId) -> Span {
fn get_span(&self, span_id: SpanId) -> Span {
*self
.spans
.get(span_id.0)
.expect("internal error: missing span")
}
/// Find ID of a span (should be avoided if possible)
pub fn find_span_id(&self, span: Span) -> Option<SpanId> {
self.spans.iter().position(|sp| sp == &span).map(SpanId)
}
}
impl Default for EngineState {

View File

@ -4,8 +4,8 @@ use crate::{
usage::build_usage, CachedFile, Command, CommandType, EngineState, OverlayFrame,
StateDelta, Variable, VirtualPath, Visibility,
},
BlockId, Category, Config, DeclId, FileId, Module, ModuleId, ParseError, ParseWarning, Span,
SpanId, Type, Value, VarId, VirtualPathId,
BlockId, Category, Config, DeclId, FileId, GetSpan, Module, ModuleId, ParseError, ParseWarning,
Span, SpanId, Type, Value, VarId, VirtualPathId,
};
use core::panic;
use std::{
@ -1019,8 +1019,10 @@ impl<'a> StateWorkingSet<'a> {
self.delta.spans.push(span);
SpanId(num_permanent_spans + self.delta.spans.len() - 1)
}
}
pub fn get_span(&self, span_id: SpanId) -> Span {
impl<'a> GetSpan for &'a StateWorkingSet<'a> {
fn get_span(&self, span_id: SpanId) -> Span {
let num_permanent_spans = self.permanent_state.num_spans();
if span_id.0 < num_permanent_spans {
self.permanent_state.get_span(span_id)

View File

@ -10,7 +10,7 @@ pub enum ParseWarning {
DeprecatedWarning {
old_command: String,
new_suggestion: String,
#[label("`{old_command}` is deprecated and will be removed in 0.94. Please {new_suggestion} instead")]
#[label("`{old_command}` is deprecated and will be removed in a future release. Please {new_suggestion} instead")]
span: Span,
url: String,
},

View File

@ -557,12 +557,12 @@ pub enum ShellError {
/// ## Resolution
///
/// Check the spelling of your column name. Did you forget to rename a column somewhere?
#[error("Cannot find column")]
#[error("Cannot find column '{col_name}'")]
#[diagnostic(code(nu::shell::column_not_found))]
CantFindColumn {
col_name: String,
#[label = "cannot find column '{col_name}'"]
span: Span,
span: Option<Span>,
#[label = "value originates here"]
src_span: Span,
},
@ -876,6 +876,21 @@ pub enum ShellError {
span: Span,
},
#[cfg(unix)]
/// An I/O operation failed.
///
/// ## Resolution
///
/// This is a generic error. Refer to the specific error message for further details.
#[error("program coredump error")]
#[diagnostic(code(nu::shell::coredump_error))]
CoredumpErrorSpanned {
msg: String,
signal: i32,
#[label("{msg}")]
span: Span,
},
/// Tried to `cd` to a path that isn't a directory.
///
/// ## Resolution

View File

@ -4,7 +4,7 @@ use crate::{
ExternalArgument, ListItem, Math, Operator, RecordItem,
},
debugger::DebugContext,
Config, Range, Record, ShellError, Span, Type, Value, VarId, ENV_VARIABLE_ID,
Config, GetSpan, Range, Record, ShellError, Span, Type, Value, VarId, ENV_VARIABLE_ID,
};
use std::{borrow::Cow, collections::HashMap};
@ -12,7 +12,7 @@ use std::{borrow::Cow, collections::HashMap};
pub trait Eval {
/// State that doesn't need to be mutated.
/// EngineState for regular eval and StateWorkingSet for const eval
type State<'a>: Copy;
type State<'a>: Copy + GetSpan;
/// State that needs to be mutated.
/// This is the stack for regular eval, and unused by const eval
@ -23,17 +23,19 @@ pub trait Eval {
mut_state: &mut Self::MutState,
expr: &Expression,
) -> Result<Value, ShellError> {
let expr_span = expr.span(&state);
match &expr.expr {
Expr::Bool(b) => Ok(Value::bool(*b, expr.span)),
Expr::Int(i) => Ok(Value::int(*i, expr.span)),
Expr::Float(f) => Ok(Value::float(*f, expr.span)),
Expr::Binary(b) => Ok(Value::binary(b.clone(), expr.span)),
Expr::Filepath(path, quoted) => Self::eval_filepath(state, mut_state, path.clone(), *quoted, expr.span),
Expr::Bool(b) => Ok(Value::bool(*b, expr_span)),
Expr::Int(i) => Ok(Value::int(*i, expr_span)),
Expr::Float(f) => Ok(Value::float(*f, expr_span)),
Expr::Binary(b) => Ok(Value::binary(b.clone(), expr_span)),
Expr::Filepath(path, quoted) => Self::eval_filepath(state, mut_state, path.clone(), *quoted, expr_span),
Expr::Directory(path, quoted) => {
Self::eval_directory(state, mut_state, path.clone(), *quoted, expr.span)
Self::eval_directory(state, mut_state, path.clone(), *quoted, expr_span)
}
Expr::Var(var_id) => Self::eval_var(state, mut_state, *var_id, expr.span),
Expr::CellPath(cell_path) => Ok(Value::cell_path(cell_path.clone(), expr.span)),
Expr::Var(var_id) => Self::eval_var(state, mut_state, *var_id, expr_span),
Expr::CellPath(cell_path) => Ok(Value::cell_path(cell_path.clone(), expr_span)),
Expr::FullCellPath(cell_path) => {
let value = Self::eval::<D>(state, mut_state, &cell_path.head)?;
@ -45,7 +47,7 @@ pub trait Eval {
value.follow_cell_path(&cell_path.tail, false)
}
}
Expr::DateTime(dt) => Ok(Value::date(*dt, expr.span)),
Expr::DateTime(dt) => Ok(Value::date(*dt, expr_span)),
Expr::List(list) => {
let mut output = vec![];
for item in list {
@ -53,11 +55,11 @@ pub trait Eval {
ListItem::Item(expr) => output.push(Self::eval::<D>(state, mut_state, expr)?),
ListItem::Spread(_, expr) => match Self::eval::<D>(state, mut_state, expr)? {
Value::List { vals, .. } => output.extend(vals),
_ => return Err(ShellError::CannotSpreadAsList { span: expr.span }),
_ => return Err(ShellError::CannotSpreadAsList { span: expr_span }),
},
}
}
Ok(Value::list(output, expr.span))
Ok(Value::list(output, expr_span))
}
Expr::Record(items) => {
let mut record = Record::new();
@ -67,36 +69,38 @@ pub trait Eval {
RecordItem::Pair(col, val) => {
// avoid duplicate cols
let col_name = Self::eval::<D>(state, mut_state, col)?.coerce_into_string()?;
let col_span = col.span(&state);
if let Some(orig_span) = col_names.get(&col_name) {
return Err(ShellError::ColumnDefinedTwice {
col_name,
second_use: col.span,
second_use: col_span,
first_use: *orig_span,
});
} else {
col_names.insert(col_name.clone(), col.span);
col_names.insert(col_name.clone(), col_span);
record.push(col_name, Self::eval::<D>(state, mut_state, val)?);
}
}
RecordItem::Spread(_, inner) => {
let inner_span = inner.span(&state);
match Self::eval::<D>(state, mut_state, inner)? {
Value::Record { val: inner_val, .. } => {
for (col_name, val) in inner_val.into_owned() {
if let Some(orig_span) = col_names.get(&col_name) {
return Err(ShellError::ColumnDefinedTwice {
col_name,
second_use: inner.span,
second_use: inner_span,
first_use: *orig_span,
});
} else {
col_names.insert(col_name.clone(), inner.span);
col_names.insert(col_name.clone(), inner_span);
record.push(col_name, val);
}
}
}
_ => {
return Err(ShellError::CannotSpreadAsRecord {
span: inner.span,
span: inner_span,
})
}
}
@ -104,7 +108,7 @@ pub trait Eval {
}
}
Ok(Value::record(record, expr.span))
Ok(Value::record(record, expr_span))
}
Expr::Table(table) => {
let mut output_headers = vec![];
@ -114,10 +118,11 @@ pub trait Eval {
.iter()
.position(|existing| existing == &header)
{
let first_use = table.columns[idx].span(&state);
return Err(ShellError::ColumnDefinedTwice {
col_name: header,
second_use: expr.span,
first_use: table.columns[idx].span,
second_use: expr_span,
first_use,
});
} else {
output_headers.push(header);
@ -132,66 +137,66 @@ pub trait Eval {
output_rows.push(Value::record(
record,
expr.span,
expr_span,
));
}
Ok(Value::list(output_rows, expr.span))
Ok(Value::list(output_rows, expr_span))
}
Expr::Keyword(kw) => Self::eval::<D>(state, mut_state, &kw.expr),
Expr::String(s) | Expr::RawString(s) => Ok(Value::string(s.clone(), expr.span)),
Expr::Nothing => Ok(Value::nothing(expr.span)),
Expr::String(s) | Expr::RawString(s) => Ok(Value::string(s.clone(), expr_span)),
Expr::Nothing => Ok(Value::nothing(expr_span)),
Expr::ValueWithUnit(value) => match Self::eval::<D>(state, mut_state, &value.expr)? {
Value::Int { val, .. } => value.unit.item.build_value(val, value.unit.span),
x => Err(ShellError::CantConvert {
to_type: "unit value".into(),
from_type: x.get_type().to_string(),
span: value.expr.span,
span: value.expr.span(&state),
help: None,
}),
},
Expr::Call(call) => Self::eval_call::<D>(state, mut_state, call, expr.span),
Expr::Call(call) => Self::eval_call::<D>(state, mut_state, call, expr_span),
Expr::ExternalCall(head, args) => {
Self::eval_external_call(state, mut_state, head, args, expr.span)
Self::eval_external_call(state, mut_state, head, args, expr_span)
}
Expr::Subexpression(block_id) => {
Self::eval_subexpression::<D>(state, mut_state, *block_id, expr.span)
Self::eval_subexpression::<D>(state, mut_state, *block_id, expr_span)
}
Expr::Range(range) => {
let from = if let Some(f) = &range.from {
Self::eval::<D>(state, mut_state, f)?
} else {
Value::nothing(expr.span)
Value::nothing(expr_span)
};
let next = if let Some(s) = &range.next {
Self::eval::<D>(state, mut_state, s)?
} else {
Value::nothing(expr.span)
Value::nothing(expr_span)
};
let to = if let Some(t) = &range.to {
Self::eval::<D>(state, mut_state, t)?
} else {
Value::nothing(expr.span)
Value::nothing(expr_span)
};
Ok(Value::range(
Range::new(from, next, to, range.operator.inclusion, expr.span)?,
expr.span,
Range::new(from, next, to, range.operator.inclusion, expr_span)?,
expr_span,
))
}
Expr::UnaryNot(expr) => {
let lhs = Self::eval::<D>(state, mut_state, expr)?;
match lhs {
Value::Bool { val, .. } => Ok(Value::bool(!val, expr.span)),
Value::Bool { val, .. } => Ok(Value::bool(!val, expr_span)),
other => Err(ShellError::TypeMismatch {
err_message: format!("expected bool, found {}", other.get_type()),
span: expr.span,
span: expr_span,
}),
}
}
Expr::BinaryOp(lhs, op, rhs) => {
let op_span = op.span;
let op_span = op.span(&state);
let op = eval_operator(op)?;
match op {
@ -200,23 +205,23 @@ pub trait Eval {
match boolean {
Boolean::And => {
if lhs.is_false() {
Ok(Value::bool(false, expr.span))
Ok(Value::bool(false, expr_span))
} else {
let rhs = Self::eval::<D>(state, mut_state, rhs)?;
lhs.and(op_span, &rhs, expr.span)
lhs.and(op_span, &rhs, expr_span)
}
}
Boolean::Or => {
if lhs.is_true() {
Ok(Value::bool(true, expr.span))
Ok(Value::bool(true, expr_span))
} else {
let rhs = Self::eval::<D>(state, mut_state, rhs)?;
lhs.or(op_span, &rhs, expr.span)
lhs.or(op_span, &rhs, expr_span)
}
}
Boolean::Xor => {
let rhs = Self::eval::<D>(state, mut_state, rhs)?;
lhs.xor(op_span, &rhs, expr.span)
lhs.xor(op_span, &rhs, expr_span)
}
}
}
@ -225,35 +230,35 @@ pub trait Eval {
let rhs = Self::eval::<D>(state, mut_state, rhs)?;
match math {
Math::Plus => lhs.add(op_span, &rhs, expr.span),
Math::Minus => lhs.sub(op_span, &rhs, expr.span),
Math::Multiply => lhs.mul(op_span, &rhs, expr.span),
Math::Divide => lhs.div(op_span, &rhs, expr.span),
Math::Append => lhs.append(op_span, &rhs, expr.span),
Math::Modulo => lhs.modulo(op_span, &rhs, expr.span),
Math::FloorDivision => lhs.floor_div(op_span, &rhs, expr.span),
Math::Pow => lhs.pow(op_span, &rhs, expr.span),
Math::Plus => lhs.add(op_span, &rhs, expr_span),
Math::Minus => lhs.sub(op_span, &rhs, expr_span),
Math::Multiply => lhs.mul(op_span, &rhs, expr_span),
Math::Divide => lhs.div(op_span, &rhs, expr_span),
Math::Append => lhs.append(op_span, &rhs, expr_span),
Math::Modulo => lhs.modulo(op_span, &rhs, expr_span),
Math::FloorDivision => lhs.floor_div(op_span, &rhs, expr_span),
Math::Pow => lhs.pow(op_span, &rhs, expr_span),
}
}
Operator::Comparison(comparison) => {
let lhs = Self::eval::<D>(state, mut_state, lhs)?;
let rhs = Self::eval::<D>(state, mut_state, rhs)?;
match comparison {
Comparison::LessThan => lhs.lt(op_span, &rhs, expr.span),
Comparison::LessThanOrEqual => lhs.lte(op_span, &rhs, expr.span),
Comparison::GreaterThan => lhs.gt(op_span, &rhs, expr.span),
Comparison::GreaterThanOrEqual => lhs.gte(op_span, &rhs, expr.span),
Comparison::Equal => lhs.eq(op_span, &rhs, expr.span),
Comparison::NotEqual => lhs.ne(op_span, &rhs, expr.span),
Comparison::In => lhs.r#in(op_span, &rhs, expr.span),
Comparison::NotIn => lhs.not_in(op_span, &rhs, expr.span),
Comparison::StartsWith => lhs.starts_with(op_span, &rhs, expr.span),
Comparison::EndsWith => lhs.ends_with(op_span, &rhs, expr.span),
Comparison::LessThan => lhs.lt(op_span, &rhs, expr_span),
Comparison::LessThanOrEqual => lhs.lte(op_span, &rhs, expr_span),
Comparison::GreaterThan => lhs.gt(op_span, &rhs, expr_span),
Comparison::GreaterThanOrEqual => lhs.gte(op_span, &rhs, expr_span),
Comparison::Equal => lhs.eq(op_span, &rhs, expr_span),
Comparison::NotEqual => lhs.ne(op_span, &rhs, expr_span),
Comparison::In => lhs.r#in(op_span, &rhs, expr_span),
Comparison::NotIn => lhs.not_in(op_span, &rhs, expr_span),
Comparison::StartsWith => lhs.starts_with(op_span, &rhs, expr_span),
Comparison::EndsWith => lhs.ends_with(op_span, &rhs, expr_span),
Comparison::RegexMatch => {
Self::regex_match(state, op_span, &lhs, &rhs, false, expr.span)
Self::regex_match(state, op_span, &lhs, &rhs, false, expr_span)
}
Comparison::NotRegexMatch => {
Self::regex_match(state, op_span, &lhs, &rhs, true, expr.span)
Self::regex_match(state, op_span, &lhs, &rhs, true, expr_span)
}
}
}
@ -261,20 +266,20 @@ pub trait Eval {
let lhs = Self::eval::<D>(state, mut_state, lhs)?;
let rhs = Self::eval::<D>(state, mut_state, rhs)?;
match bits {
Bits::BitAnd => lhs.bit_and(op_span, &rhs, expr.span),
Bits::BitOr => lhs.bit_or(op_span, &rhs, expr.span),
Bits::BitXor => lhs.bit_xor(op_span, &rhs, expr.span),
Bits::ShiftLeft => lhs.bit_shl(op_span, &rhs, expr.span),
Bits::ShiftRight => lhs.bit_shr(op_span, &rhs, expr.span),
Bits::BitAnd => lhs.bit_and(op_span, &rhs, expr_span),
Bits::BitOr => lhs.bit_or(op_span, &rhs, expr_span),
Bits::BitXor => lhs.bit_xor(op_span, &rhs, expr_span),
Bits::ShiftLeft => lhs.bit_shl(op_span, &rhs, expr_span),
Bits::ShiftRight => lhs.bit_shr(op_span, &rhs, expr_span),
}
}
Operator::Assignment(assignment) => Self::eval_assignment::<D>(
state, mut_state, lhs, rhs, assignment, op_span, expr.span
state, mut_state, lhs, rhs, assignment, op_span, expr_span
),
}
}
Expr::RowCondition(block_id) | Expr::Closure(block_id) => {
Self::eval_row_condition_or_closure(state, mut_state, *block_id, expr.span)
Self::eval_row_condition_or_closure(state, mut_state, *block_id, expr_span)
}
Expr::StringInterpolation(exprs, quoted) => {
let config = Self::get_config(state, mut_state);
@ -286,16 +291,16 @@ pub trait Eval {
// For supporting bare string interpolation to create globs, currently only
// implemented for external calls
if expr.ty == Type::Glob {
Ok(Value::glob(str, *quoted, expr.span))
Ok(Value::glob(str, *quoted, expr_span))
} else {
Ok(Value::string(str, expr.span))
Ok(Value::string(str, expr_span))
}
}
Expr::Overlay(_) => Self::eval_overlay(state, expr.span),
Expr::Overlay(_) => Self::eval_overlay(state, expr_span),
Expr::GlobPattern(pattern, quoted) => {
// GlobPattern is similar to Filepath
// But we don't want to expand path during eval time, it's required for `nu_engine::glob_from` to run correctly
Ok(Value::glob(pattern, *quoted, expr.span))
Ok(Value::glob(pattern, *quoted, expr_span))
}
Expr::MatchBlock(_) // match blocks are handled by `match`
| Expr::Block(_) // blocks are handled directly by core commands
@ -303,7 +308,7 @@ pub trait Eval {
| Expr::ImportPattern(_)
| Expr::Signature(_)
| Expr::Operator(_)
| Expr::Garbage => Self::unreachable(expr),
| Expr::Garbage => Self::unreachable(state, expr),
}
}
@ -384,5 +389,5 @@ pub trait Eval {
fn eval_overlay(state: Self::State<'_>, span: Span) -> Result<Value, ShellError>;
/// For expressions that should never actually be evaluated
fn unreachable(expr: &Expression) -> Result<Value, ShellError>;
fn unreachable(state: Self::State<'_>, expr: &Expression) -> Result<Value, ShellError>;
}

View File

@ -149,6 +149,38 @@ pub(crate) fn create_nu_constant(engine_state: &EngineState, span: Span) -> Valu
},
);
record.push(
"data-dir",
if let Some(path) = nu_path::data_dir() {
let mut canon_data_path = canonicalize_path(engine_state, &path);
canon_data_path.push("nushell");
Value::string(canon_data_path.to_string_lossy(), span)
} else {
Value::error(
ShellError::IOError {
msg: "Could not get data path".into(),
},
span,
)
},
);
record.push(
"cache-dir",
if let Some(path) = nu_path::cache_dir() {
let mut canon_cache_path = canonicalize_path(engine_state, &path);
canon_cache_path.push("nushell");
Value::string(canon_cache_path.to_string_lossy(), span)
} else {
Value::error(
ShellError::IOError {
msg: "Could not get cache path".into(),
},
span,
)
},
);
record.push("temp-path", {
let canon_temp_path = canonicalize_path(engine_state, &std::env::temp_dir());
Value::string(canon_temp_path.to_string_lossy(), span)
@ -251,7 +283,7 @@ pub fn eval_constant_with_input(
Expr::Call(call) => eval_const_call(working_set, call, input),
Expr::Subexpression(block_id) => {
let block = working_set.get_block(*block_id);
eval_const_subexpression(working_set, block, input, expr.span)
eval_const_subexpression(working_set, block, input, expr.span(&working_set))
}
_ => eval_constant(working_set, expr).map(|v| PipelineData::Value(v, None)),
}
@ -379,7 +411,9 @@ impl Eval for EvalConst {
Err(ShellError::NotAConstant { span })
}
fn unreachable(expr: &Expression) -> Result<Value, ShellError> {
Err(ShellError::NotAConstant { span: expr.span })
fn unreachable(working_set: &StateWorkingSet, expr: &Expression) -> Result<Value, ShellError> {
Err(ShellError::NotAConstant {
span: expr.span(&working_set),
})
}
}

View File

@ -39,3 +39,5 @@ pub use span::*;
pub use syntax_shape::*;
pub use ty::*;
pub use value::*;
pub use nu_derive_value::*;

View File

@ -23,7 +23,21 @@ impl ExitStatusFuture {
ExitStatusFuture::Finished(Err(err)) => Err(err.as_ref().clone()),
ExitStatusFuture::Running(receiver) => {
let code = match receiver.recv() {
Ok(Ok(status)) => Ok(status),
Ok(Ok(status)) => {
#[cfg(unix)]
if let ExitStatus::Signaled {
signal,
core_dumped: true,
} = status
{
return Err(ShellError::CoredumpErrorSpanned {
msg: format!("coredump detected. received signal: {signal}"),
signal,
span,
});
}
Ok(status)
}
Ok(Err(err)) => Err(ShellError::IOErrorSpanned {
msg: format!("failed to get exit code: {err:?}"),
span,

View File

@ -1,7 +1,12 @@
use crate::SpanId;
use miette::SourceSpan;
use serde::{Deserialize, Serialize};
use std::ops::Deref;
pub trait GetSpan {
fn get_span(&self, span_id: SpanId) -> Span;
}
/// A spanned area of interest, generic over what kind of thing is of interest
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Spanned<T> {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,196 @@
use std::collections::HashMap;
use crate::{Record, ShellError, Span, Value};
/// A trait for converting a value into a [`Value`].
///
/// This conversion is infallible, for fallible conversions use [`TryIntoValue`].
///
/// # Derivable
/// This trait can be used with `#[derive]`.
/// When derived on structs with named fields, the resulting value representation will use
/// [`Value::Record`], where each field of the record corresponds to a field of the struct.
/// For structs with unnamed fields, the value representation will be [`Value::List`], with all
/// fields inserted into a list.
/// Unit structs will be represented as [`Value::Nothing`] since they contain no data.
///
/// Only enums with no fields may derive this trait.
/// The resulting value representation will be the name of the variant as a [`Value::String`].
/// By default, variant names will be converted to ["snake_case"](convert_case::Case::Snake).
/// You can customize the case conversion using `#[nu_value(rename_all = "kebab-case")]` on the enum.
/// All deterministic and useful case conversions provided by [`convert_case::Case`] are supported
/// by specifying the case name followed by "case".
/// Also all values for
/// [`#[serde(rename_all = "...")]`](https://serde.rs/container-attrs.html#rename_all) are valid
/// here.
///
/// ```
/// # use nu_protocol::{IntoValue, Value, Span};
/// #[derive(IntoValue)]
/// #[nu_value(rename_all = "COBOL-CASE")]
/// enum Bird {
/// MountainEagle,
/// ForestOwl,
/// RiverDuck,
/// }
///
/// assert_eq!(
/// Bird::RiverDuck.into_value(Span::unknown()),
/// Value::test_string("RIVER-DUCK")
/// );
/// ```
pub trait IntoValue: Sized {
/// Converts the given value to a [`Value`].
fn into_value(self, span: Span) -> Value;
}
// Primitive Types
impl<T, const N: usize> IntoValue for [T; N]
where
T: IntoValue,
{
fn into_value(self, span: Span) -> Value {
Vec::from(self).into_value(span)
}
}
macro_rules! primitive_into_value {
($type:ty, $method:ident) => {
primitive_into_value!($type => $type, $method);
};
($type:ty => $as_type:ty, $method:ident) => {
impl IntoValue for $type {
fn into_value(self, span: Span) -> Value {
Value::$method(<$as_type>::from(self), span)
}
}
};
}
primitive_into_value!(bool, bool);
primitive_into_value!(char, string);
primitive_into_value!(f32 => f64, float);
primitive_into_value!(f64, float);
primitive_into_value!(i8 => i64, int);
primitive_into_value!(i16 => i64, int);
primitive_into_value!(i32 => i64, int);
primitive_into_value!(i64, int);
primitive_into_value!(u8 => i64, int);
primitive_into_value!(u16 => i64, int);
primitive_into_value!(u32 => i64, int);
// u64 and usize may be truncated as Value only supports i64.
impl IntoValue for isize {
fn into_value(self, span: Span) -> Value {
Value::int(self as i64, span)
}
}
impl IntoValue for () {
fn into_value(self, span: Span) -> Value {
Value::nothing(span)
}
}
macro_rules! tuple_into_value {
($($t:ident:$n:tt),+) => {
impl<$($t),+> IntoValue for ($($t,)+) where $($t: IntoValue,)+ {
fn into_value(self, span: Span) -> Value {
let vals = vec![$(self.$n.into_value(span)),+];
Value::list(vals, span)
}
}
}
}
// Tuples in std are implemented for up to 12 elements, so we do it here too.
tuple_into_value!(T0:0);
tuple_into_value!(T0:0, T1:1);
tuple_into_value!(T0:0, T1:1, T2:2);
tuple_into_value!(T0:0, T1:1, T2:2, T3:3);
tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4);
tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5);
tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6);
tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7);
tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8);
tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9);
tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9, T10:10);
tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9, T10:10, T11:11);
// Other std Types
impl IntoValue for String {
fn into_value(self, span: Span) -> Value {
Value::string(self, span)
}
}
impl<T> IntoValue for Vec<T>
where
T: IntoValue,
{
fn into_value(self, span: Span) -> Value {
Value::list(self.into_iter().map(|v| v.into_value(span)).collect(), span)
}
}
impl<T> IntoValue for Option<T>
where
T: IntoValue,
{
fn into_value(self, span: Span) -> Value {
match self {
Some(v) => v.into_value(span),
None => Value::nothing(span),
}
}
}
impl<V> IntoValue for HashMap<String, V>
where
V: IntoValue,
{
fn into_value(self, span: Span) -> Value {
let mut record = Record::new();
for (k, v) in self.into_iter() {
// Using `push` is fine as a hashmaps have unique keys.
// To ensure this uniqueness, we only allow hashmaps with strings as
// keys and not keys which implement `Into<String>` or `ToString`.
record.push(k, v.into_value(span));
}
Value::record(record, span)
}
}
// Nu Types
impl IntoValue for Value {
fn into_value(self, span: Span) -> Value {
self.with_span(span)
}
}
// TODO: use this type for all the `into_value` methods that types implement but return a Result
/// A trait for trying to convert a value into a `Value`.
///
/// Types like streams may fail while collecting the `Value`,
/// for these types it is useful to implement a fallible variant.
///
/// This conversion is fallible, for infallible conversions use [`IntoValue`].
/// All types that implement `IntoValue` will automatically implement this trait.
pub trait TryIntoValue: Sized {
// TODO: instead of ShellError, maybe we could have a IntoValueError that implements Into<ShellError>
/// Tries to convert the given value into a `Value`.
fn try_into_value(self, span: Span) -> Result<Value, ShellError>;
}
impl<T> TryIntoValue for T
where
T: IntoValue,
{
fn try_into_value(self, span: Span) -> Result<Value, ShellError> {
Ok(self.into_value(span))
}
}

View File

@ -4,7 +4,10 @@ mod filesize;
mod from;
mod from_value;
mod glob;
mod into_value;
mod range;
#[cfg(test)]
mod test_derive;
pub mod record;
pub use custom_value::CustomValue;
@ -12,6 +15,7 @@ pub use duration::*;
pub use filesize::*;
pub use from_value::FromValue;
pub use glob::*;
pub use into_value::{IntoValue, TryIntoValue};
pub use range::{FloatRange, IntRange, Range};
pub use record::Record;
@ -1089,7 +1093,7 @@ impl Value {
} else {
return Err(ShellError::CantFindColumn {
col_name: column_name.clone(),
span: *origin_span,
span: Some(*origin_span),
src_span: span,
});
}
@ -1126,7 +1130,7 @@ impl Value {
} else {
Err(ShellError::CantFindColumn {
col_name: column_name.clone(),
span: *origin_span,
span: Some(*origin_span),
src_span: val_span,
})
}
@ -1136,7 +1140,7 @@ impl Value {
}
_ => Err(ShellError::CantFindColumn {
col_name: column_name.clone(),
span: *origin_span,
span: Some(*origin_span),
src_span: val_span,
}),
}
@ -1237,7 +1241,7 @@ impl Value {
v => {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: *span,
span: Some(*span),
src_span: v.span(),
});
}
@ -1262,7 +1266,7 @@ impl Value {
v => {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: *span,
span: Some(*span),
src_span: v.span(),
});
}
@ -1342,7 +1346,7 @@ impl Value {
} else {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: *span,
span: Some(*span),
src_span: v_span,
});
}
@ -1351,7 +1355,7 @@ impl Value {
v => {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: *span,
span: Some(*span),
src_span: v.span(),
});
}
@ -1364,7 +1368,7 @@ impl Value {
} else {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: *span,
span: Some(*span),
src_span: v_span,
});
}
@ -1373,7 +1377,7 @@ impl Value {
v => {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: *span,
span: Some(*span),
src_span: v.span(),
});
}
@ -1427,7 +1431,7 @@ impl Value {
if record.to_mut().remove(col_name).is_none() && !optional {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: *span,
span: Some(*span),
src_span: v_span,
});
}
@ -1435,7 +1439,7 @@ impl Value {
v => {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: *span,
span: Some(*span),
src_span: v.span(),
});
}
@ -1447,7 +1451,7 @@ impl Value {
if record.to_mut().remove(col_name).is_none() && !optional {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: *span,
span: Some(*span),
src_span: v_span,
});
}
@ -1455,7 +1459,7 @@ impl Value {
}
v => Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: *span,
span: Some(*span),
src_span: v.span(),
}),
},
@ -1504,7 +1508,7 @@ impl Value {
} else if !optional {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: *span,
span: Some(*span),
src_span: v_span,
});
}
@ -1512,7 +1516,7 @@ impl Value {
v => {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: *span,
span: Some(*span),
src_span: v.span(),
});
}
@ -1526,7 +1530,7 @@ impl Value {
} else if !optional {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: *span,
span: Some(*span),
src_span: v_span,
});
}
@ -1534,7 +1538,7 @@ impl Value {
}
v => Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: *span,
span: Some(*span),
src_span: v.span(),
}),
},

View File

@ -0,0 +1,386 @@
use crate::{record, FromValue, IntoValue, Record, Span, Value};
use std::collections::HashMap;
// Make nu_protocol available in this namespace, consumers of this crate will
// have this without such an export.
// The derive macro fully qualifies paths to "nu_protocol".
use crate as nu_protocol;
trait IntoTestValue {
fn into_test_value(self) -> Value;
}
impl<T> IntoTestValue for T
where
T: IntoValue,
{
fn into_test_value(self) -> Value {
self.into_value(Span::test_data())
}
}
#[derive(IntoValue, FromValue, Debug, PartialEq)]
struct NamedFieldsStruct<T>
where
T: IntoValue + FromValue,
{
array: [u16; 4],
bool: bool,
char: char,
f32: f32,
f64: f64,
i8: i8,
i16: i16,
i32: i32,
i64: i64,
isize: isize,
u16: u16,
u32: u32,
unit: (),
tuple: (u32, bool),
some: Option<u32>,
none: Option<u32>,
vec: Vec<T>,
string: String,
hashmap: HashMap<String, u32>,
nested: Nestee,
}
#[derive(IntoValue, FromValue, Debug, PartialEq)]
struct Nestee {
u32: u32,
some: Option<u32>,
none: Option<u32>,
}
impl NamedFieldsStruct<u32> {
fn make() -> Self {
Self {
array: [1, 2, 3, 4],
bool: true,
char: 'a',
f32: std::f32::consts::PI,
f64: std::f64::consts::E,
i8: 127,
i16: -32768,
i32: 2147483647,
i64: -9223372036854775808,
isize: 2,
u16: 65535,
u32: 4294967295,
unit: (),
tuple: (1, true),
some: Some(123),
none: None,
vec: vec![10, 20, 30],
string: "string".to_string(),
hashmap: HashMap::from_iter([("a".to_string(), 10), ("b".to_string(), 20)]),
nested: Nestee {
u32: 3,
some: Some(42),
none: None,
},
}
}
fn value() -> Value {
Value::test_record(record! {
"array" => Value::test_list(vec![
Value::test_int(1),
Value::test_int(2),
Value::test_int(3),
Value::test_int(4)
]),
"bool" => Value::test_bool(true),
"char" => Value::test_string('a'),
"f32" => Value::test_float(std::f32::consts::PI.into()),
"f64" => Value::test_float(std::f64::consts::E),
"i8" => Value::test_int(127),
"i16" => Value::test_int(-32768),
"i32" => Value::test_int(2147483647),
"i64" => Value::test_int(-9223372036854775808),
"isize" => Value::test_int(2),
"u16" => Value::test_int(65535),
"u32" => Value::test_int(4294967295),
"unit" => Value::test_nothing(),
"tuple" => Value::test_list(vec![
Value::test_int(1),
Value::test_bool(true)
]),
"some" => Value::test_int(123),
"none" => Value::test_nothing(),
"vec" => Value::test_list(vec![
Value::test_int(10),
Value::test_int(20),
Value::test_int(30)
]),
"string" => Value::test_string("string"),
"hashmap" => Value::test_record(record! {
"a" => Value::test_int(10),
"b" => Value::test_int(20)
}),
"nested" => Value::test_record(record! {
"u32" => Value::test_int(3),
"some" => Value::test_int(42),
"none" => Value::test_nothing(),
})
})
}
}
#[test]
fn named_fields_struct_into_value() {
let expected = NamedFieldsStruct::value();
let actual = NamedFieldsStruct::make().into_test_value();
assert_eq!(expected, actual);
}
#[test]
fn named_fields_struct_from_value() {
let expected = NamedFieldsStruct::make();
let actual = NamedFieldsStruct::from_value(NamedFieldsStruct::value()).unwrap();
assert_eq!(expected, actual);
}
#[test]
fn named_fields_struct_roundtrip() {
let expected = NamedFieldsStruct::make();
let actual =
NamedFieldsStruct::from_value(NamedFieldsStruct::make().into_test_value()).unwrap();
assert_eq!(expected, actual);
let expected = NamedFieldsStruct::value();
let actual = NamedFieldsStruct::<u32>::from_value(NamedFieldsStruct::value())
.unwrap()
.into_test_value();
assert_eq!(expected, actual);
}
#[test]
fn named_fields_struct_missing_value() {
let value = Value::test_record(Record::new());
let res: Result<NamedFieldsStruct<u32>, _> = NamedFieldsStruct::from_value(value);
assert!(res.is_err());
}
#[test]
fn named_fields_struct_incorrect_type() {
// Should work for every type that is not a record.
let value = Value::test_nothing();
let res: Result<NamedFieldsStruct<u32>, _> = NamedFieldsStruct::from_value(value);
assert!(res.is_err());
}
#[derive(IntoValue, FromValue, Debug, PartialEq)]
struct UnnamedFieldsStruct<T>(u32, String, T)
where
T: IntoValue + FromValue;
impl UnnamedFieldsStruct<f64> {
fn make() -> Self {
UnnamedFieldsStruct(420, "Hello, tuple!".to_string(), 33.33)
}
fn value() -> Value {
Value::test_list(vec![
Value::test_int(420),
Value::test_string("Hello, tuple!"),
Value::test_float(33.33),
])
}
}
#[test]
fn unnamed_fields_struct_into_value() {
let expected = UnnamedFieldsStruct::value();
let actual = UnnamedFieldsStruct::make().into_test_value();
assert_eq!(expected, actual);
}
#[test]
fn unnamed_fields_struct_from_value() {
let expected = UnnamedFieldsStruct::make();
let value = UnnamedFieldsStruct::value();
let actual = UnnamedFieldsStruct::from_value(value).unwrap();
assert_eq!(expected, actual);
}
#[test]
fn unnamed_fields_struct_roundtrip() {
let expected = UnnamedFieldsStruct::make();
let actual =
UnnamedFieldsStruct::from_value(UnnamedFieldsStruct::make().into_test_value()).unwrap();
assert_eq!(expected, actual);
let expected = UnnamedFieldsStruct::value();
let actual = UnnamedFieldsStruct::<f64>::from_value(UnnamedFieldsStruct::value())
.unwrap()
.into_test_value();
assert_eq!(expected, actual);
}
#[test]
fn unnamed_fields_struct_missing_value() {
let value = Value::test_list(vec![]);
let res: Result<UnnamedFieldsStruct<f64>, _> = UnnamedFieldsStruct::from_value(value);
assert!(res.is_err());
}
#[test]
fn unnamed_fields_struct_incorrect_type() {
// Should work for every type that is not a record.
let value = Value::test_nothing();
let res: Result<UnnamedFieldsStruct<f64>, _> = UnnamedFieldsStruct::from_value(value);
assert!(res.is_err());
}
#[derive(IntoValue, FromValue, Debug, PartialEq)]
struct UnitStruct;
#[test]
fn unit_struct_into_value() {
let expected = Value::test_nothing();
let actual = UnitStruct.into_test_value();
assert_eq!(expected, actual);
}
#[test]
fn unit_struct_from_value() {
let expected = UnitStruct;
let actual = UnitStruct::from_value(Value::test_nothing()).unwrap();
assert_eq!(expected, actual);
}
#[test]
fn unit_struct_roundtrip() {
let expected = UnitStruct;
let actual = UnitStruct::from_value(UnitStruct.into_test_value()).unwrap();
assert_eq!(expected, actual);
let expected = Value::test_nothing();
let actual = UnitStruct::from_value(Value::test_nothing())
.unwrap()
.into_test_value();
assert_eq!(expected, actual);
}
#[derive(IntoValue, FromValue, Debug, PartialEq)]
enum Enum {
AlphaOne,
BetaTwo,
CharlieThree,
}
impl Enum {
fn make() -> [Self; 3] {
[Enum::AlphaOne, Enum::BetaTwo, Enum::CharlieThree]
}
fn value() -> Value {
Value::test_list(vec![
Value::test_string("alpha_one"),
Value::test_string("beta_two"),
Value::test_string("charlie_three"),
])
}
}
#[test]
fn enum_into_value() {
let expected = Enum::value();
let actual = Enum::make().into_test_value();
assert_eq!(expected, actual);
}
#[test]
fn enum_from_value() {
let expected = Enum::make();
let actual = <[Enum; 3]>::from_value(Enum::value()).unwrap();
assert_eq!(expected, actual);
}
#[test]
fn enum_roundtrip() {
let expected = Enum::make();
let actual = <[Enum; 3]>::from_value(Enum::make().into_test_value()).unwrap();
assert_eq!(expected, actual);
let expected = Enum::value();
let actual = <[Enum; 3]>::from_value(Enum::value())
.unwrap()
.into_test_value();
assert_eq!(expected, actual);
}
#[test]
fn enum_unknown_variant() {
let value = Value::test_string("delta_four");
let res = Enum::from_value(value);
assert!(res.is_err());
}
#[test]
fn enum_incorrect_type() {
// Should work for every type that is not a record.
let value = Value::test_nothing();
let res = Enum::from_value(value);
assert!(res.is_err());
}
// Generate the `Enum` from before but with all possible `rename_all` variants.
macro_rules! enum_rename_all {
($($ident:ident: $case:literal => [$a1:literal, $b2:literal, $c3:literal]),*) => {
$(
#[derive(Debug, PartialEq, IntoValue, FromValue)]
#[nu_value(rename_all = $case)]
enum $ident {
AlphaOne,
BetaTwo,
CharlieThree
}
impl $ident {
fn make() -> [Self; 3] {
[Self::AlphaOne, Self::BetaTwo, Self::CharlieThree]
}
fn value() -> Value {
Value::test_list(vec![
Value::test_string($a1),
Value::test_string($b2),
Value::test_string($c3),
])
}
}
)*
#[test]
fn enum_rename_all_into_value() {$({
let expected = $ident::value();
let actual = $ident::make().into_test_value();
assert_eq!(expected, actual);
})*}
#[test]
fn enum_rename_all_from_value() {$({
let expected = $ident::make();
let actual = <[$ident; 3]>::from_value($ident::value()).unwrap();
assert_eq!(expected, actual);
})*}
}
}
enum_rename_all! {
Upper: "UPPER CASE" => ["ALPHA ONE", "BETA TWO", "CHARLIE THREE"],
Lower: "lower case" => ["alpha one", "beta two", "charlie three"],
Title: "Title Case" => ["Alpha One", "Beta Two", "Charlie Three"],
Camel: "camelCase" => ["alphaOne", "betaTwo", "charlieThree"],
Pascal: "PascalCase" => ["AlphaOne", "BetaTwo", "CharlieThree"],
Snake: "snake_case" => ["alpha_one", "beta_two", "charlie_three"],
UpperSnake: "UPPER_SNAKE_CASE" => ["ALPHA_ONE", "BETA_TWO", "CHARLIE_THREE"],
Kebab: "kebab-case" => ["alpha-one", "beta-two", "charlie-three"],
Cobol: "COBOL-CASE" => ["ALPHA-ONE", "BETA-TWO", "CHARLIE-THREE"],
Train: "Train-Case" => ["Alpha-One", "Beta-Two", "Charlie-Three"],
Flat: "flatcase" => ["alphaone", "betatwo", "charliethree"],
UpperFlat: "UPPERFLATCASE" => ["ALPHAONE", "BETATWO", "CHARLIETHREE"]
}

View File

@ -658,7 +658,7 @@ def build-command-page [command: record] {
$" > ($example.example | nu-highlight)"
(if not ($example.result | is-empty) {
$example.result
| table
| table -e
| to text
| if ($example.result | describe) == "binary" { str join } else { lines }
| each {|line|
@ -771,6 +771,11 @@ You can also learn more at (ansi default_italic)(ansi light_cyan_underline)https
let modules = (try { modules $target_item --find $find })
if not ($modules | is-empty) { return $modules }
if ($find | is-not-empty) {
print -e $"No help results found mentioning: ($find)"
return []
}
let span = (metadata $item | get span)
error make {

View File

@ -161,10 +161,7 @@ export def bench [
}
}
# print a banner for nushell, with information about the project
#
# Example:
# an example can be found in [this asciinema recording](https://asciinema.org/a/566513)
# Print a banner for nushell with information about the project
export def banner [] {
let dt = (datetime-diff (date now) 2019-05-10T09:59:12-07:00)
$"(ansi green) __ ,(ansi reset)

View File

@ -259,9 +259,7 @@ fn draw_table(
table.with(width_ctrl);
}
let tablewidth = table.total_width();
table_to_string(table, tablewidth, termwidth)
table_to_string(table, termwidth)
}
fn set_indent(table: &mut Table, left: usize, right: usize) {
@ -289,7 +287,9 @@ fn set_border_head(table: &mut Table, with_footer: bool, wctrl: TableWidthCtrl)
}
}
fn table_to_string(table: Table, total_width: usize, termwidth: usize) -> Option<String> {
fn table_to_string(table: Table, termwidth: usize) -> Option<String> {
let total_width = table.total_width();
if total_width > termwidth {
None
} else {
@ -318,6 +318,7 @@ impl TableOption<NuRecords, CompleteDimensionVecRecords<'_>, ColoredConfig> for
dim: &mut CompleteDimensionVecRecords<'_>,
) {
let total_width = get_total_width2(&self.width, cfg);
if total_width > self.max {
TableTrim {
max: self.max,

View File

@ -289,6 +289,8 @@ pub fn nu_run_test(opts: NuOpts, commands: impl AsRef<str>, with_std: bool) -> O
if !with_std {
command.arg("--no-std-lib");
}
// Use plain errors to help make error text matching more consistent
command.args(["--error-style", "plain"]);
command
.arg(format!("-c {}", escape_quote_string(&commands)))
.stdout(Stdio::piped())
@ -375,6 +377,8 @@ where
.envs(envs)
.arg("--commands")
.arg(command)
// Use plain errors to help make error text matching more consistent
.args(["--error-style", "plain"])
.arg("--config")
.arg(temp_config_file)
.arg("--env-config")

View File

@ -77,6 +77,7 @@ $env.ENV_CONVERSIONS = {
# The default for this is $nu.default-config-dir/scripts
$env.NU_LIB_DIRS = [
($nu.default-config-dir | path join 'scripts') # add <nushell-config-dir>/scripts
($nu.data-dir | path join 'completions') # default home for nushell completions
]
# Directories to search for plugin binaries when calling register

View File

@ -87,7 +87,7 @@ impl CustomValue for CoolCustomValue {
} else {
Err(ShellError::CantFindColumn {
col_name: column_name,
span: path_span,
span: Some(path_span),
src_span: self_span,
})
}

View File

@ -19,4 +19,4 @@ bench = false
nu-plugin = { path = "../nu-plugin", version = "0.94.3" }
nu-protocol = { path = "../nu-protocol", version = "0.94.3" }
git2 = "0.18"
git2 = "0.19"

View File

@ -35,6 +35,7 @@ impl PluginCommand for ToNu {
Some('n'),
)
.switch("tail", "shows tail rows", Some('t'))
.switch("index", "add an index column", Some('i'))
.input_output_types(vec![
(Type::Custom("expression".into()), Type::Any),
(Type::Custom("dataframe".into()), Type::table()),
@ -62,18 +63,18 @@ impl PluginCommand for ToNu {
vec![
Example {
description: "Shows head rows from dataframe",
example: "[[a b]; [1 2] [3 4]] | polars into-df | polars into-nu",
example: "[[a b]; [1 2] [3 4]] | polars into-df | polars into-nu --index",
result: Some(Value::list(vec![rec_1, rec_2], Span::test_data())),
},
Example {
description: "Shows tail rows from dataframe",
example:
"[[a b]; [1 2] [5 6] [3 4]] | polars into-df | polars into-nu --tail --rows 1",
"[[a b]; [1 2] [5 6] [3 4]] | polars into-df | polars into-nu --tail --rows 1 --index",
result: Some(Value::list(vec![rec_3], Span::test_data())),
},
Example {
description: "Convert a col expression into a nushell value",
example: "polars col a | polars into-nu",
example: "polars col a | polars into-nu --index",
result: Some(Value::test_record(record! {
"expr" => Value::test_string("column"),
"value" => Value::test_string("a"),
@ -106,17 +107,18 @@ fn dataframe_command(
) -> Result<PipelineData, ShellError> {
let rows: Option<usize> = call.get_flag("rows")?;
let tail: bool = call.has_flag("tail")?;
let index: bool = call.has_flag("index")?;
let df = NuDataFrame::try_from_value_coerce(plugin, &input, call.head)?;
let values = if tail {
df.tail(rows, call.head)?
df.tail(rows, index, call.head)?
} else {
// if rows is specified, return those rows, otherwise return everything
if rows.is_some() {
df.head(rows, call.head)?
df.head(rows, index, call.head)?
} else {
df.head(Some(df.height()), call.head)?
df.head(Some(df.height()), index, call.head)?
}
};

View File

@ -326,22 +326,22 @@ impl NuDataFrame {
}
// Print is made out a head and if the dataframe is too large, then a tail
pub fn print(&self, span: Span) -> Result<Vec<Value>, ShellError> {
pub fn print(&self, include_index: bool, span: Span) -> Result<Vec<Value>, ShellError> {
let df = &self.df;
let size: usize = 20;
if df.height() > size {
let sample_size = size / 2;
let mut values = self.head(Some(sample_size), span)?;
let mut values = self.head(Some(sample_size), include_index, span)?;
conversion::add_separator(&mut values, df, self.has_index(), span);
let remaining = df.height() - sample_size;
let tail_size = remaining.min(sample_size);
let mut tail_values = self.tail(Some(tail_size), span)?;
let mut tail_values = self.tail(Some(tail_size), include_index, span)?;
values.append(&mut tail_values);
Ok(values)
} else {
Ok(self.head(Some(size), span)?)
Ok(self.head(Some(size), include_index, span)?)
}
}
@ -349,26 +349,38 @@ impl NuDataFrame {
self.df.height()
}
pub fn head(&self, rows: Option<usize>, span: Span) -> Result<Vec<Value>, ShellError> {
pub fn head(
&self,
rows: Option<usize>,
include_index: bool,
span: Span,
) -> Result<Vec<Value>, ShellError> {
let to_row = rows.unwrap_or(5);
let values = self.to_rows(0, to_row, span)?;
let values = self.to_rows(0, to_row, include_index, span)?;
Ok(values)
}
pub fn tail(&self, rows: Option<usize>, span: Span) -> Result<Vec<Value>, ShellError> {
pub fn tail(
&self,
rows: Option<usize>,
include_index: bool,
span: Span,
) -> Result<Vec<Value>, ShellError> {
let df = &self.df;
let to_row = df.height();
let size = rows.unwrap_or(DEFAULT_ROWS);
let from_row = to_row.saturating_sub(size);
let values = self.to_rows(from_row, to_row, span)?;
let values = self.to_rows(from_row, to_row, include_index, span)?;
Ok(values)
}
/// Converts the dataframe to a nushell list of values
pub fn to_rows(
&self,
from_row: usize,
to_row: usize,
include_index: bool,
span: Span,
) -> Result<Vec<Value>, ShellError> {
let df = &self.df;
@ -400,7 +412,7 @@ impl NuDataFrame {
.map(|i| {
let mut record = Record::new();
if !has_index {
if !has_index && include_index {
record.push("index", Value::int((i + from_row) as i64, span));
}
@ -602,7 +614,7 @@ impl CustomValueSupport for NuDataFrame {
}
fn base_value(self, span: Span) -> Result<Value, ShellError> {
let vals = self.print(span)?;
let vals = self.print(true, span)?;
Ok(Value::list(vals, span))
}

View File

@ -63,45 +63,16 @@ fn compute_with_value(
op: Span,
right: &Value,
) -> Result<Value, ShellError> {
let rhs_span = right.span();
match right {
Value::Custom { val: rhs, .. } => {
let rhs = rhs.as_any().downcast_ref::<NuExpression>().ok_or_else(|| {
ShellError::TypeMismatch {
err_message: "Right hand side not a dataframe expression".into(),
span: rhs_span,
}
})?;
match rhs.as_ref() {
polars::prelude::Expr::Literal(..) => with_operator(
(plugin, engine),
operator,
left,
rhs,
lhs_span,
right.span(),
op,
),
_ => Err(ShellError::TypeMismatch {
err_message: "Only literal expressions or number".into(),
span: right.span(),
}),
}
}
_ => {
let rhs = NuExpression::try_from_value(plugin, right)?;
with_operator(
(plugin, engine),
operator,
left,
&rhs,
lhs_span,
right.span(),
op,
)
}
}
let rhs = NuExpression::try_from_value(plugin, right)?;
with_operator(
(plugin, engine),
operator,
left,
&rhs,
lhs_span,
right.span(),
op,
)
}
fn with_operator(

View File

@ -29,8 +29,10 @@ pub(crate) fn gather_commandline_args() -> (Vec<String>, String, Vec<String>) {
}
let flag_value = match arg.as_ref() {
"--commands" | "-c" | "--table-mode" | "-m" | "-e" | "--execute" | "--config"
| "--env-config" | "-I" | "ide-ast" => args.next().map(|a| escape_quote_string(&a)),
"--commands" | "-c" | "--table-mode" | "-m" | "--error-style" | "-e" | "--execute"
| "--config" | "--env-config" | "-I" | "ide-ast" => {
args.next().map(|a| escape_quote_string(&a))
}
#[cfg(feature = "plugin")]
"--plugin-config" => args.next().map(|a| escape_quote_string(&a)),
"--log-level" | "--log-target" | "--log-include" | "--log-exclude" | "--testbin"
@ -102,6 +104,8 @@ pub(crate) fn parse_commandline_args(
let execute = call.get_flag_expr("execute");
let table_mode: Option<Value> =
call.get_flag(engine_state, &mut stack, "table-mode")?;
let error_style: Option<Value> =
call.get_flag(engine_state, &mut stack, "error-style")?;
let no_newline = call.get_named_arg("no-newline");
// ide flags
@ -245,6 +249,7 @@ pub(crate) fn parse_commandline_args(
ide_check,
ide_ast,
table_mode,
error_style,
no_newline,
});
}
@ -278,6 +283,7 @@ pub(crate) struct NushellCliArgs {
pub(crate) log_exclude: Option<Vec<Spanned<String>>>,
pub(crate) execute: Option<Spanned<String>>,
pub(crate) table_mode: Option<Value>,
pub(crate) error_style: Option<Value>,
pub(crate) no_newline: Option<Spanned<String>>,
pub(crate) include_path: Option<Spanned<String>>,
pub(crate) lsp: bool,
@ -325,6 +331,12 @@ impl Command for Nu {
"the table mode to use. rounded is default.",
Some('m'),
)
.named(
"error-style",
SyntaxShape::String,
"the error style to use (fancy or plain). default: fancy",
None,
)
.switch("no-newline", "print the result for --commands(-c) without a newline", None)
.switch(
"no-config-file",

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