Merge remote-tracking branch 'origin/main' into plugin-ctrlc

This commit is contained in:
Andy Gayton 2024-06-23 15:55:51 -04:00
commit 6797008430
76 changed files with 1875 additions and 713 deletions

View File

@ -19,7 +19,7 @@ jobs:
# Prevent sudden announcement of a new advisory from failing ci:
continue-on-error: true
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
- uses: rustsec/audit-check@v1.4.1
with:
token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -33,7 +33,7 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
@ -66,7 +66,7 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
@ -95,7 +95,7 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
@ -146,7 +146,7 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0

View File

@ -27,7 +27,7 @@ jobs:
# if: github.repository == 'nushell/nightly'
steps:
- name: Checkout
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
if: github.repository == 'nushell/nightly'
with:
ref: main
@ -112,7 +112,7 @@ jobs:
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
with:
ref: main
fetch-depth: 0
@ -181,7 +181,7 @@ jobs:
- name: Waiting for Release
run: sleep 1800
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
with:
ref: main

View File

@ -62,7 +62,7 @@ jobs:
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
- name: Update Rust Toolchain Target
run: |

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Check spelling
uses: crate-ci/typos@v1.22.7

11
Cargo.lock generated
View File

@ -1227,6 +1227,12 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "doctest-file"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562"
[[package]]
name = "downcast-rs"
version = "1.2.1"
@ -2026,10 +2032,11 @@ dependencies = [
[[package]]
name = "interprocess"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4d0250d41da118226e55b3d50ca3f0d9e0a0f6829b92f543ac0054aeea1572"
checksum = "67bafc2f5dbdad79a6d925649758d5472647b416028099f0b829d1b67fdd47d3"
dependencies = [
"doctest-file",
"libc",
"recvmsg",
"widestring",

View File

@ -95,7 +95,7 @@ heck = "0.5.0"
human-date-parser = "0.1.1"
indexmap = "2.2"
indicatif = "0.17"
interprocess = "2.1.0"
interprocess = "2.2.0"
is_executable = "1.0"
itertools = "0.12"
libc = "0.2"

View File

@ -344,7 +344,10 @@ pub fn migrate_old_plugin_file(engine_state: &EngineState, storage_path: &str) -
name: identity.name().to_owned(),
filename: identity.filename().to_owned(),
shell: identity.shell().map(|p| p.to_owned()),
data: PluginRegistryItemData::Valid { commands },
data: PluginRegistryItemData::Valid {
metadata: Default::default(),
commands,
},
});
}

View File

@ -138,6 +138,7 @@ impl Highlighter for NuHighlighter {
FlatShape::Filepath => add_colored_token(&shape.1, next_token),
FlatShape::Directory => add_colored_token(&shape.1, next_token),
FlatShape::GlobInterpolation => add_colored_token(&shape.1, next_token),
FlatShape::GlobPattern => add_colored_token(&shape.1, next_token),
FlatShape::Variable(_) | FlatShape::VarDecl(_) => {
add_colored_token(&shape.1, next_token)
@ -452,15 +453,17 @@ fn find_matching_block_end_in_expr(
}
}
Expr::StringInterpolation(exprs) => exprs.iter().find_map(|expr| {
find_matching_block_end_in_expr(
line,
working_set,
expr,
global_span_offset,
global_cursor_offset,
)
}),
Expr::StringInterpolation(exprs) | Expr::GlobInterpolation(exprs, _) => {
exprs.iter().find_map(|expr| {
find_matching_block_end_in_expr(
line,
working_set,
expr,
global_span_offset,
global_cursor_offset,
)
})
}
Expr::List(list) => {
if expr_last == global_cursor_offset {

View File

@ -763,7 +763,7 @@ fn variables_completions() {
// Test completions for $nu
let suggestions = completer.complete("$nu.", 4);
assert_eq!(17, suggestions.len());
assert_eq!(18, suggestions.len());
let expected: Vec<String> = vec![
"cache-dir".into(),
@ -783,6 +783,7 @@ fn variables_completions() {
"plugin-path".into(),
"startup-time".into(),
"temp-path".into(),
"vendor-autoload-dir".into(),
];
// Match results

View File

@ -229,14 +229,24 @@ impl Command for Do {
result: None,
},
Example {
description: "Run the closure, with a positional parameter",
example: r#"do {|x| 100 + $x } 77"#,
description: "Run the closure with a positional, type-checked parameter",
example: r#"do {|x:int| 100 + $x } 77"#,
result: Some(Value::test_int(177)),
},
Example {
description: "Run the closure, with input",
example: r#"77 | do {|x| 100 + $in }"#,
result: None, // TODO: returns 177
description: "Run the closure with pipeline input",
example: r#"77 | do { 100 + $in }"#,
result: Some(Value::test_int(177)),
},
Example {
description: "Run the closure with a default parameter value",
example: r#"77 | do {|x=100| $x + $in }"#,
result: Some(Value::test_int(177)),
},
Example {
description: "Run the closure with two positional parameters",
example: r#"do {|x,y| $x + $y } 77 100"#,
result: Some(Value::test_int(177)),
},
Example {
description: "Run the closure and keep changes to the environment",

View File

@ -116,11 +116,18 @@ pub fn version(engine_state: &EngineState, span: Span) -> Result<PipelineData, S
Value::string(features_enabled().join(", "), span),
);
// Get a list of plugin names
// Get a list of plugin names and versions if present
let installed_plugins = engine_state
.plugins()
.iter()
.map(|x| x.identity().name())
.map(|x| {
let name = x.identity().name();
if let Some(version) = x.metadata().and_then(|m| m.version) {
format!("{name} {version}")
} else {
name.into()
}
})
.collect::<Vec<_>>();
record.push(

View File

@ -118,11 +118,12 @@ apparent the next time `nu` is next launched with that plugin registry file.
},
));
let interface = plugin.clone().get_plugin(Some((engine_state, stack)))?;
let metadata = interface.get_metadata()?;
let commands = interface.get_signature()?;
modify_plugin_file(engine_state, stack, call.head, custom_path, |contents| {
// Update the file with the received signatures
let item = PluginRegistryItem::new(plugin.identity(), commands);
// Update the file with the received metadata and signatures
let item = PluginRegistryItem::new(plugin.identity(), metadata, commands);
contents.upsert_plugin(item);
Ok(())
})?;

View File

@ -16,6 +16,7 @@ impl Command for PluginList {
Type::Table(
[
("name".into(), Type::String),
("version".into(), Type::String),
("is_running".into(), Type::Bool),
("pid".into(), Type::Int),
("filename".into(), Type::String),
@ -43,6 +44,7 @@ impl Command for PluginList {
description: "List installed plugins.",
result: Some(Value::test_list(vec![Value::test_record(record! {
"name" => Value::test_string("inc"),
"version" => Value::test_string(env!("CARGO_PKG_VERSION")),
"is_running" => Value::test_bool(true),
"pid" => Value::test_int(106480),
"filename" => if cfg!(windows) {
@ -98,8 +100,15 @@ impl Command for PluginList {
.map(|s| Value::string(s.to_string_lossy(), head))
.unwrap_or(Value::nothing(head));
let metadata = plugin.metadata();
let version = metadata
.and_then(|m| m.version)
.map(|s| Value::string(s, head))
.unwrap_or(Value::nothing(head));
let record = record! {
"name" => Value::string(plugin.identity().name(), head),
"version" => version,
"is_running" => Value::bool(plugin.is_running(), head),
"pid" => pid,
"filename" => Value::string(plugin.identity().filename().to_string_lossy(), head),

View File

@ -20,6 +20,7 @@ pub fn default_shape_color(shape: &str) -> Style {
"shape_flag" => Style::new().fg(Color::Blue).bold(),
"shape_float" => Style::new().fg(Color::Purple).bold(),
"shape_garbage" => Style::new().fg(Color::White).on(Color::Red).bold(),
"shape_glob_interpolation" => Style::new().fg(Color::Cyan).bold(),
"shape_globpattern" => Style::new().fg(Color::Cyan).bold(),
"shape_int" => Style::new().fg(Color::Purple).bold(),
"shape_internalcall" => Style::new().fg(Color::Cyan).bold(),

View File

@ -1,6 +1,7 @@
use chrono::{Datelike, Local, NaiveDate};
use nu_color_config::StyleComputer;
use nu_engine::command_prelude::*;
use nu_protocol::ast::{Expr, Expression};
use std::collections::VecDeque;
@ -14,6 +15,7 @@ struct Arguments {
month_names: bool,
full_year: Option<Spanned<i64>>,
week_start: Option<Spanned<String>>,
as_table: bool,
}
impl Command for Cal {
@ -26,6 +28,7 @@ impl Command for Cal {
.switch("year", "Display the year column", Some('y'))
.switch("quarter", "Display the quarter column", Some('q'))
.switch("month", "Display the month column", Some('m'))
.switch("as-table", "output as a table", Some('t'))
.named(
"full-year",
SyntaxShape::Int,
@ -43,7 +46,10 @@ impl Command for Cal {
"Display the month names instead of integers",
None,
)
.input_output_types(vec![(Type::Nothing, Type::table())])
.input_output_types(vec![
(Type::Nothing, Type::table()),
(Type::Nothing, Type::String),
])
.allow_variants_without_examples(true) // TODO: supply exhaustive examples
.category(Category::Generators)
}
@ -75,10 +81,15 @@ impl Command for Cal {
result: None,
},
Example {
description: "This month's calendar with the week starting on monday",
description: "This month's calendar with the week starting on Monday",
example: "cal --week-start mo",
result: None,
},
Example {
description: "How many 'Friday the Thirteenths' occurred in 2015?",
example: "cal --as-table --full-year 2015 | where fr == 13 | length",
result: None,
},
]
}
}
@ -101,6 +112,7 @@ pub fn cal(
quarter: call.has_flag(engine_state, stack, "quarter")?,
full_year: call.get_flag(engine_state, stack, "full-year")?,
week_start: call.get_flag(engine_state, stack, "week-start")?,
as_table: call.has_flag(engine_state, stack, "as-table")?,
};
let style_computer = &StyleComputer::from_config(engine_state, stack);
@ -131,7 +143,27 @@ pub fn cal(
style_computer,
)?;
Ok(Value::list(calendar_vec_deque.into_iter().collect(), tag).into_pipeline_data())
let mut table_no_index = Call::new(Span::unknown());
table_no_index.add_named((
Spanned {
item: "index".to_string(),
span: Span::unknown(),
},
None,
Some(Expression::new_unknown(
Expr::Bool(false),
Span::unknown(),
Type::Bool,
)),
));
let cal_table_output =
Value::list(calendar_vec_deque.into_iter().collect(), tag).into_pipeline_data();
if !arguments.as_table {
crate::Table.run(engine_state, stack, &table_no_index, cal_table_output)
} else {
Ok(cal_table_output)
}
}
fn get_invalid_year_shell_error(head: Span) -> ShellError {

View File

@ -12,13 +12,7 @@ impl Command for Generate {
fn signature(&self) -> Signature {
Signature::build("generate")
.input_output_types(vec![
(Type::Nothing, Type::List(Box::new(Type::Any))),
(
Type::List(Box::new(Type::Any)),
Type::List(Box::new(Type::Any)),
),
])
.input_output_types(vec![(Type::Nothing, Type::List(Box::new(Type::Any)))])
.required("initial", SyntaxShape::Any, "Initial value.")
.required(
"closure",
@ -63,23 +57,10 @@ used as the next argument to the closure, otherwise generation stops.
)),
},
Example {
example: "generate [0, 1] {|fib| {out: $fib.0, next: [$fib.1, ($fib.0 + $fib.1)]} } | first 10",
description: "Generate a stream of fibonacci numbers",
result: Some(Value::list(
vec![
Value::test_int(0),
Value::test_int(1),
Value::test_int(1),
Value::test_int(2),
Value::test_int(3),
Value::test_int(5),
Value::test_int(8),
Value::test_int(13),
Value::test_int(21),
Value::test_int(34),
],
Span::test_data(),
)),
example:
"generate [0, 1] {|fib| {out: $fib.0, next: [$fib.1, ($fib.0 + $fib.1)]} }",
description: "Generate a continuous stream of Fibonacci numbers",
result: None,
},
]
}

View File

@ -1,16 +1,15 @@
use nu_cmd_base::hook::eval_hook;
use nu_engine::{command_prelude::*, env_to_strings, get_eval_expression};
use nu_path::{dots::expand_ndots, expand_tilde};
use nu_protocol::{
ast::{Expr, Expression},
did_you_mean,
process::ChildProcess,
ByteStream, NuGlob, OutDest,
ast::Expression, did_you_mean, process::ChildProcess, ByteStream, NuGlob, OutDest,
};
use nu_system::ForegroundChild;
use nu_utils::IgnoreCaseExt;
use pathdiff::diff_paths;
use std::{
borrow::Cow,
ffi::{OsStr, OsString},
io::Write,
path::{Path, PathBuf},
process::Stdio,
@ -33,8 +32,16 @@ impl Command for External {
fn signature(&self) -> nu_protocol::Signature {
Signature::build(self.name())
.input_output_types(vec![(Type::Any, Type::Any)])
.required("command", SyntaxShape::String, "External command to run.")
.rest("args", SyntaxShape::Any, "Arguments for external command.")
.required(
"command",
SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]),
"External command to run.",
)
.rest(
"args",
SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::Any]),
"Arguments for external command.",
)
.category(Category::System)
}
@ -47,42 +54,31 @@ impl Command for External {
) -> Result<PipelineData, ShellError> {
let cwd = engine_state.cwd(Some(stack))?;
// Evaluate the command name in the same way the arguments are evaluated. Since this isn't
// a spread, it should return a one-element vec.
let name_expr = call
.positional_nth(0)
.ok_or_else(|| ShellError::MissingParameter {
param_name: "command".into(),
span: call.head,
})?;
let name = eval_argument(engine_state, stack, name_expr, false)?
.pop()
.expect("eval_argument returned zero-element vec")
.into_spanned(name_expr.span);
let name: Value = call.req(engine_state, stack, 0)?;
let name_str: Cow<str> = match &name {
Value::Glob { val, .. } => Cow::Borrowed(val),
Value::String { val, .. } => Cow::Borrowed(val),
_ => Cow::Owned(name.clone().coerce_into_string()?),
};
let expanded_name = match &name {
// Expand tilde and ndots on the name if it's a bare string / glob (#13000)
Value::Glob { no_expand, .. } if !*no_expand => expand_ndots(expand_tilde(&*name_str)),
_ => Path::new(&*name_str).to_owned(),
};
// Find the absolute path to the executable. On Windows, set the
// executable to "cmd.exe" if it's is a CMD internal command. If the
// command is not found, display a helpful error message.
let executable = if cfg!(windows) && is_cmd_internal_command(&name.item) {
let executable = if cfg!(windows) && is_cmd_internal_command(&name_str) {
PathBuf::from("cmd.exe")
} else {
// Expand tilde on the name if it's a bare string (#13000)
let expanded_name = if is_bare_string(name_expr) {
expand_tilde(&name.item)
} else {
name.item.clone()
};
// Determine the PATH to be used and then use `which` to find it - though this has no
// effect if it's an absolute path already
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
let Some(executable) = which(&expanded_name, &paths, &cwd) else {
return Err(command_not_found(
&name.item,
call.head,
engine_state,
stack,
));
let Some(executable) = which(expanded_name, &paths, &cwd) else {
return Err(command_not_found(&name_str, call.head, engine_state, stack));
};
executable
};
@ -101,15 +97,15 @@ impl Command for External {
// Configure args.
let args = eval_arguments_from_call(engine_state, stack, call)?;
#[cfg(windows)]
if is_cmd_internal_command(&name.item) {
if is_cmd_internal_command(&name_str) {
use std::os::windows::process::CommandExt;
// The /D flag disables execution of AutoRun commands from registry.
// The /C flag followed by a command name instructs CMD to execute
// that command and quit.
command.args(["/D", "/C", &name.item]);
command.args(["/D", "/C", &name_str]);
for arg in &args {
command.raw_arg(escape_cmd_argument(arg)?.as_ref());
command.raw_arg(escape_cmd_argument(arg)?);
}
} else {
command.args(args.into_iter().map(|s| s.item));
@ -217,76 +213,54 @@ impl Command for External {
}
}
/// Removes surrounding quotes from a string. Doesn't remove quotes from raw
/// strings. Returns the original string if it doesn't have matching quotes.
fn remove_quotes(s: &str) -> Cow<'_, str> {
let quoted_by_double_quotes = s.len() >= 2 && s.starts_with('"') && s.ends_with('"');
let quoted_by_single_quotes = s.len() >= 2 && s.starts_with('\'') && s.ends_with('\'');
let quoted_by_backticks = s.len() >= 2 && s.starts_with('`') && s.ends_with('`');
if quoted_by_double_quotes {
Cow::Owned(s[1..s.len() - 1].to_string().replace(r#"\""#, "\""))
} else if quoted_by_single_quotes || quoted_by_backticks {
Cow::Borrowed(&s[1..s.len() - 1])
} else {
Cow::Borrowed(s)
}
}
/// Evaluate all arguments from a call, performing expansions when necessary.
pub fn eval_arguments_from_call(
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
) -> Result<Vec<Spanned<String>>, ShellError> {
) -> Result<Vec<Spanned<OsString>>, ShellError> {
let ctrlc = &engine_state.ctrlc;
let cwd = engine_state.cwd(Some(stack))?;
let mut args: Vec<Spanned<String>> = vec![];
let mut args: Vec<Spanned<OsString>> = vec![];
for (expr, spread) in call.rest_iter(1) {
if is_bare_string(expr) {
// If `expr` is a bare string, perform tilde-expansion,
// glob-expansion, and inner-quotes-removal, in that order.
for arg in eval_argument(engine_state, stack, expr, spread)? {
let tilde_expanded = expand_tilde(&arg);
for glob_expanded in expand_glob(&tilde_expanded, &cwd, expr.span, ctrlc)? {
let inner_quotes_removed = remove_inner_quotes(&glob_expanded);
args.push(inner_quotes_removed.into_owned().into_spanned(expr.span));
for arg in eval_argument(engine_state, stack, expr, spread)? {
match arg {
// Expand globs passed to run-external
Value::Glob { val, no_expand, .. } if !no_expand => args.extend(
expand_glob(&val, &cwd, expr.span, ctrlc)?
.into_iter()
.map(|s| s.into_spanned(expr.span)),
),
other => {
args.push(OsString::from(coerce_into_string(other)?).into_spanned(expr.span))
}
}
} else {
for arg in eval_argument(engine_state, stack, expr, spread)? {
args.push(arg.into_spanned(expr.span));
}
}
}
Ok(args)
}
/// Evaluates an expression, coercing the values to strings.
///
/// Note: The parser currently has a special hack that retains surrounding
/// quotes for string literals in `Expression`, so that we can decide whether
/// the expression is considered a bare string. The hack doesn't affect string
/// literals within lists or records. This function will remove the quotes
/// before evaluating the expression.
/// Custom `coerce_into_string()`, including globs, since those are often args to `run-external`
/// as well
fn coerce_into_string(val: Value) -> Result<String, ShellError> {
match val {
Value::Glob { val, .. } => Ok(val),
_ => val.coerce_into_string(),
}
}
/// Evaluate an argument, returning more than one value if it was a list to be spread.
fn eval_argument(
engine_state: &EngineState,
stack: &mut Stack,
expr: &Expression,
spread: bool,
) -> Result<Vec<String>, ShellError> {
// Remove quotes from string literals.
let mut expr = expr.clone();
if let Expr::String(s) = &expr.expr {
expr.expr = Expr::String(remove_quotes(s).into());
}
) -> Result<Vec<Value>, ShellError> {
let eval = get_eval_expression(engine_state);
match eval(engine_state, stack, &expr)? {
match eval(engine_state, stack, expr)? {
Value::List { vals, .. } => {
if spread {
vals.into_iter()
.map(|val| val.coerce_into_string())
.collect()
Ok(vals)
} else {
Err(ShellError::CannotPassListToExternal {
arg: String::from_utf8_lossy(engine_state.get_span_contents(expr.span)).into(),
@ -298,31 +272,12 @@ fn eval_argument(
if spread {
Err(ShellError::CannotSpreadAsList { span: expr.span })
} else {
Ok(vec![value.coerce_into_string()?])
Ok(vec![value])
}
}
}
}
/// Returns whether an expression is considered a bare string.
///
/// Bare strings are defined as string literals that are either unquoted or
/// quoted by backticks. Raw strings or string interpolations don't count.
fn is_bare_string(expr: &Expression) -> bool {
let Expr::String(s) = &expr.expr else {
return false;
};
let quoted_by_double_quotes = s.len() >= 2 && s.starts_with('"') && s.ends_with('"');
let quoted_by_single_quotes = s.len() >= 2 && s.starts_with('\'') && s.ends_with('\'');
!quoted_by_double_quotes && !quoted_by_single_quotes
}
/// Performs tilde expansion on `arg`. Returns the original string if `arg`
/// doesn't start with tilde.
fn expand_tilde(arg: &str) -> String {
nu_path::expand_tilde(arg).to_string_lossy().to_string()
}
/// Performs glob expansion on `arg`. If the expansion found no matches or the pattern
/// is not a valid glob, then this returns the original string as the expansion result.
///
@ -333,19 +288,21 @@ fn expand_glob(
cwd: &Path,
span: Span,
interrupt: &Option<Arc<AtomicBool>>,
) -> Result<Vec<String>, ShellError> {
) -> Result<Vec<OsString>, ShellError> {
const GLOB_CHARS: &[char] = &['*', '?', '['];
// Don't expand something that doesn't include the GLOB_CHARS
// For an argument that doesn't include the GLOB_CHARS, just do the `expand_tilde`
// and `expand_ndots` expansion
if !arg.contains(GLOB_CHARS) {
return Ok(vec![arg.into()]);
let path = expand_ndots(expand_tilde(arg));
return Ok(vec![path.into()]);
}
// We must use `nu_engine::glob_from` here, in order to ensure we get paths from the correct
// dir
let glob = NuGlob::Expand(arg.to_owned()).into_spanned(span);
if let Ok((prefix, matches)) = nu_engine::glob_from(&glob, cwd, span, None) {
let mut result = vec![];
let mut result: Vec<OsString> = vec![];
for m in matches {
if nu_utils::ctrl_c::was_pressed(interrupt) {
@ -353,7 +310,7 @@ fn expand_glob(
}
if let Ok(arg) = m {
let arg = resolve_globbed_path_to_cwd_relative(arg, prefix.as_ref(), cwd);
result.push(arg.to_string_lossy().to_string());
result.push(arg.into());
} else {
result.push(arg.into());
}
@ -392,23 +349,6 @@ fn resolve_globbed_path_to_cwd_relative(
}
}
/// Transforms `--option="value"` into `--option=value`. `value` can be quoted
/// with double quotes, single quotes, or backticks. Only removes the outermost
/// pair of quotes after the equal sign.
fn remove_inner_quotes(arg: &str) -> Cow<'_, str> {
// Split `arg` on the first `=`.
let Some((option, value)) = arg.split_once('=') else {
return Cow::Borrowed(arg);
};
// Check that `option` doesn't contain quotes.
if option.contains('"') || option.contains('\'') || option.contains('`') {
return Cow::Borrowed(arg);
}
// Remove the outermost pair of quotes from `value`.
let value = remove_quotes(value);
Cow::Owned(format!("{option}={value}"))
}
/// Write `PipelineData` into `writer`. If `PipelineData` is not binary, it is
/// first rendered using the `table` command.
///
@ -577,7 +517,7 @@ pub fn command_not_found(
/// Note: the `which.rs` crate always uses PATHEXT from the environment. As
/// such, changing PATHEXT within Nushell doesn't work without updating the
/// actual environment of the Nushell process.
pub fn which(name: &str, paths: &str, cwd: &Path) -> Option<PathBuf> {
pub fn which(name: impl AsRef<OsStr>, paths: &str, cwd: &Path) -> Option<PathBuf> {
#[cfg(windows)]
let paths = format!("{};{}", cwd.display(), paths);
which::which_in(name, Some(paths), cwd).ok()
@ -593,17 +533,18 @@ fn is_cmd_internal_command(name: &str) -> bool {
}
/// Returns true if a string contains CMD special characters.
#[cfg(windows)]
fn has_cmd_special_character(s: &str) -> bool {
const SPECIAL_CHARS: &[char] = &['<', '>', '&', '|', '^'];
SPECIAL_CHARS.iter().any(|c| s.contains(*c))
fn has_cmd_special_character(s: impl AsRef<[u8]>) -> bool {
s.as_ref()
.iter()
.any(|b| matches!(b, b'<' | b'>' | b'&' | b'|' | b'^'))
}
/// Escape an argument for CMD internal commands. The result can be safely passed to `raw_arg()`.
#[cfg(windows)]
fn escape_cmd_argument(arg: &Spanned<String>) -> Result<Cow<'_, str>, ShellError> {
#[cfg_attr(not(windows), allow(dead_code))]
fn escape_cmd_argument(arg: &Spanned<OsString>) -> Result<Cow<'_, OsStr>, ShellError> {
let Spanned { item: arg, span } = arg;
if arg.contains(['\r', '\n', '%']) {
let bytes = arg.as_encoded_bytes();
if bytes.iter().any(|b| matches!(b, b'\r' | b'\n' | b'%')) {
// \r and \n trunacte the rest of the arguments and % can expand environment variables
Err(ShellError::ExternalCommand {
label:
@ -612,12 +553,12 @@ fn escape_cmd_argument(arg: &Spanned<String>) -> Result<Cow<'_, str>, ShellError
help: "some characters currently cannot be securely escaped".into(),
span: *span,
})
} else if arg.contains('"') {
} else if bytes.contains(&b'"') {
// If `arg` is already quoted by double quotes, confirm there's no
// embedded double quotes, then leave it as is.
if arg.chars().filter(|c| *c == '"').count() == 2
&& arg.starts_with('"')
&& arg.ends_with('"')
if bytes.iter().filter(|b| **b == b'"').count() == 2
&& bytes.starts_with(b"\"")
&& bytes.ends_with(b"\"")
{
Ok(Cow::Borrowed(arg))
} else {
@ -628,9 +569,13 @@ fn escape_cmd_argument(arg: &Spanned<String>) -> Result<Cow<'_, str>, ShellError
span: *span,
})
}
} else if arg.contains(' ') || has_cmd_special_character(arg) {
} else if bytes.contains(&b' ') || has_cmd_special_character(bytes) {
// If `arg` contains space or special characters, quote the entire argument by double quotes.
Ok(Cow::Owned(format!("\"{arg}\"")))
let mut new_str = OsString::new();
new_str.push("\"");
new_str.push(arg);
new_str.push("\"");
Ok(Cow::Owned(new_str))
} else {
// FIXME?: what if `arg.is_empty()`?
Ok(Cow::Borrowed(arg))
@ -640,64 +585,8 @@ fn escape_cmd_argument(arg: &Spanned<String>) -> Result<Cow<'_, str>, ShellError
#[cfg(test)]
mod test {
use super::*;
use nu_protocol::ast::ListItem;
use nu_test_support::{fs::Stub, playground::Playground};
#[test]
fn test_remove_quotes() {
assert_eq!(remove_quotes(r#""#), r#""#);
assert_eq!(remove_quotes(r#"'"#), r#"'"#);
assert_eq!(remove_quotes(r#"''"#), r#""#);
assert_eq!(remove_quotes(r#""foo""#), r#"foo"#);
assert_eq!(remove_quotes(r#"`foo '"' bar`"#), r#"foo '"' bar"#);
assert_eq!(remove_quotes(r#"'foo' bar"#), r#"'foo' bar"#);
assert_eq!(remove_quotes(r#"r#'foo'#"#), r#"r#'foo'#"#);
assert_eq!(remove_quotes(r#""foo\" bar""#), r#"foo" bar"#);
}
#[test]
fn test_eval_argument() {
fn expression(expr: Expr) -> Expression {
Expression::new_unknown(expr, Span::unknown(), Type::Any)
}
fn eval(expr: Expr, spread: bool) -> Result<Vec<String>, ShellError> {
let engine_state = EngineState::new();
let mut stack = Stack::new();
eval_argument(&engine_state, &mut stack, &expression(expr), spread)
}
let actual = eval(Expr::String("".into()), false).unwrap();
let expected = &[""];
assert_eq!(actual, expected);
let actual = eval(Expr::String("'foo'".into()), false).unwrap();
let expected = &["foo"];
assert_eq!(actual, expected);
let actual = eval(Expr::RawString("'foo'".into()), false).unwrap();
let expected = &["'foo'"];
assert_eq!(actual, expected);
let actual = eval(Expr::List(vec![]), true).unwrap();
let expected: &[&str] = &[];
assert_eq!(actual, expected);
let actual = eval(
Expr::List(vec![
ListItem::Item(expression(Expr::String("'foo'".into()))),
ListItem::Item(expression(Expr::String("bar".into()))),
]),
true,
)
.unwrap();
let expected = &["'foo'", "bar"];
assert_eq!(actual, expected);
eval(Expr::String("".into()), true).unwrap_err();
eval(Expr::List(vec![]), false).unwrap_err();
}
#[test]
fn test_expand_glob() {
Playground::setup("test_expand_glob", |dirs, play| {
@ -721,46 +610,20 @@ mod test {
assert_eq!(actual, expected);
let actual = expand_glob("./a.txt", cwd, Span::unknown(), &None).unwrap();
let expected = &["./a.txt"];
let expected: Vec<OsString> = vec![Path::new(".").join("a.txt").into()];
assert_eq!(actual, expected);
let actual = expand_glob("[*.txt", cwd, Span::unknown(), &None).unwrap();
let expected = &["[*.txt"];
assert_eq!(actual, expected);
let actual = expand_glob("~/foo.txt", cwd, Span::unknown(), &None).unwrap();
let home = dirs_next::home_dir().expect("failed to get home dir");
let expected: Vec<OsString> = vec![home.join("foo.txt").into()];
assert_eq!(actual, expected);
})
}
#[test]
fn test_remove_inner_quotes() {
let actual = remove_inner_quotes(r#"--option=value"#);
let expected = r#"--option=value"#;
assert_eq!(actual, expected);
let actual = remove_inner_quotes(r#"--option="value""#);
let expected = r#"--option=value"#;
assert_eq!(actual, expected);
let actual = remove_inner_quotes(r#"--option='value'"#);
let expected = r#"--option=value"#;
assert_eq!(actual, expected);
let actual = remove_inner_quotes(r#"--option "value""#);
let expected = r#"--option "value""#;
assert_eq!(actual, expected);
let actual = remove_inner_quotes(r#"-option="value""#);
let expected = r#"-option=value"#;
assert_eq!(actual, expected);
let actual = remove_inner_quotes(r#"option="value""#);
let expected = r#"option=value"#;
assert_eq!(actual, expected);
let actual = remove_inner_quotes(r#"option="v\"value""#);
let expected = r#"option=v"value"#;
assert_eq!(actual, expected);
}
#[test]
fn test_write_pipeline_data() {
let engine_state = EngineState::new();

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

@ -1,8 +1,9 @@
use nu_test_support::{nu, pipeline};
// Tests against table/structured data
#[test]
fn cal_full_year() {
let actual = nu!("cal -y --full-year 2010 | first | to json -r");
let actual = nu!("cal -t -y --full-year 2010 | first | to json -r");
let first_week_2010_json =
r#"{"year":2010,"su":null,"mo":null,"tu":null,"we":null,"th":null,"fr":1,"sa":2}"#;
@ -14,7 +15,7 @@ fn cal_full_year() {
fn cal_february_2020_leap_year() {
let actual = nu!(pipeline(
r#"
cal -ym --full-year 2020 --month-names | where month == "february" | to json -r
cal --as-table -ym --full-year 2020 --month-names | where month == "february" | to json -r
"#
));
@ -27,7 +28,7 @@ fn cal_february_2020_leap_year() {
fn cal_fr_the_thirteenths_in_2015() {
let actual = nu!(pipeline(
r#"
cal --full-year 2015 | default 0 fr | where fr == 13 | length
cal --as-table --full-year 2015 | default 0 fr | where fr == 13 | length
"#
));
@ -38,7 +39,7 @@ fn cal_fr_the_thirteenths_in_2015() {
fn cal_rows_in_2020() {
let actual = nu!(pipeline(
r#"
cal --full-year 2020 | length
cal --as-table --full-year 2020 | length
"#
));
@ -49,7 +50,7 @@ fn cal_rows_in_2020() {
fn cal_week_day_start_mo() {
let actual = nu!(pipeline(
r#"
cal --full-year 2020 -m --month-names --week-start mo | where month == january | to json -r
cal --as-table --full-year 2020 -m --month-names --week-start mo | where month == january | to json -r
"#
));
@ -62,9 +63,43 @@ fn cal_week_day_start_mo() {
fn cal_sees_pipeline_year() {
let actual = nu!(pipeline(
r#"
cal --full-year 1020 | get mo | first 4 | to json -r
cal --as-table --full-year 1020 | get mo | first 4 | to json -r
"#
));
assert_eq!(actual.out, "[null,3,10,17]");
}
// Tests against default string output
#[test]
fn cal_is_string() {
let actual = nu!(pipeline(
r#"
cal | describe
"#
));
assert_eq!(actual.out, "string (stream)");
}
#[test]
fn cal_year_num_lines() {
let actual = nu!(pipeline(
r#"
cal --full-year 2024 | lines | length
"#
));
assert_eq!(actual.out, "68");
}
#[test]
fn cal_week_start_string() {
let actual = nu!(pipeline(
r#"
cal --week-start fr | lines | get 1 | split row '│' | get 2 | ansi strip | str trim
"#
));
assert_eq!(actual.out, "sa");
}

View File

@ -2,7 +2,7 @@ use nu_test_support::nu;
#[test]
fn length_columns_in_cal_table() {
let actual = nu!("cal | columns | length");
let actual = nu!("cal --as-table | columns | length");
assert_eq!(actual.out, "7");
}

View File

@ -1,4 +1,3 @@
#[cfg(not(windows))]
use nu_test_support::fs::Stub::EmptyFile;
use nu_test_support::playground::Playground;
use nu_test_support::{nu, pipeline};
@ -17,7 +16,6 @@ fn better_empty_redirection() {
assert!(!actual.out.contains('2'));
}
#[cfg(not(windows))]
#[test]
fn explicit_glob() {
Playground::setup("external with explicit glob", |dirs, sandbox| {
@ -30,15 +28,15 @@ fn explicit_glob() {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
^ls | glob '*.txt' | length
^nu --testbin cococo ('*.txt' | into glob)
"#
));
assert_eq!(actual.out, "2");
assert!(actual.out.contains("D&D_volume_1.txt"));
assert!(actual.out.contains("D&D_volume_2.txt"));
})
}
#[cfg(not(windows))]
#[test]
fn bare_word_expand_path_glob() {
Playground::setup("bare word should do the expansion", |dirs, sandbox| {
@ -51,7 +49,7 @@ fn bare_word_expand_path_glob() {
let actual = nu!(
cwd: dirs.test(), pipeline(
"
^ls *.txt
^nu --testbin cococo *.txt
"
));
@ -60,7 +58,6 @@ fn bare_word_expand_path_glob() {
})
}
#[cfg(not(windows))]
#[test]
fn backtick_expand_path_glob() {
Playground::setup("backtick should do the expansion", |dirs, sandbox| {
@ -73,7 +70,7 @@ fn backtick_expand_path_glob() {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
^ls `*.txt`
^nu --testbin cococo `*.txt`
"#
));
@ -82,7 +79,6 @@ fn backtick_expand_path_glob() {
})
}
#[cfg(not(windows))]
#[test]
fn single_quote_does_not_expand_path_glob() {
Playground::setup("single quote do not run the expansion", |dirs, sandbox| {
@ -95,15 +91,14 @@ fn single_quote_does_not_expand_path_glob() {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
^ls '*.txt'
^nu --testbin cococo '*.txt'
"#
));
assert!(actual.err.contains("No such file or directory"));
assert_eq!(actual.out, "*.txt");
})
}
#[cfg(not(windows))]
#[test]
fn double_quote_does_not_expand_path_glob() {
Playground::setup("double quote do not run the expansion", |dirs, sandbox| {
@ -116,22 +111,21 @@ fn double_quote_does_not_expand_path_glob() {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
^ls "*.txt"
^nu --testbin cococo "*.txt"
"#
));
assert!(actual.err.contains("No such file or directory"));
assert_eq!(actual.out, "*.txt");
})
}
#[cfg(not(windows))]
#[test]
fn failed_command_with_semicolon_will_not_execute_following_cmds() {
Playground::setup("external failed command with semicolon", |dirs, _| {
let actual = nu!(
cwd: dirs.test(), pipeline(
"
^ls *.abc; echo done
nu --testbin fail; echo done
"
));
@ -155,16 +149,51 @@ fn external_args_with_quoted() {
#[cfg(not(windows))]
#[test]
fn external_arg_with_long_flag_value_quoted() {
Playground::setup("external failed command with semicolon", |dirs, _| {
fn external_arg_with_option_like_embedded_quotes() {
// TODO: would be nice to make this work with cococo, but arg parsing interferes
Playground::setup(
"external arg with option like embedded quotes",
|dirs, _| {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
^echo --foo='bar' -foo='bar'
"#
));
assert_eq!(actual.out, "--foo=bar -foo=bar");
},
)
}
#[test]
fn external_arg_with_non_option_like_embedded_quotes() {
Playground::setup(
"external arg with non option like embedded quotes",
|dirs, _| {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
^nu --testbin cococo foo='bar' 'foo'=bar
"#
));
assert_eq!(actual.out, "foo=bar foo=bar");
},
)
}
#[test]
fn external_arg_with_string_interpolation() {
Playground::setup("external arg with string interpolation", |dirs, _| {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
^echo --foo='bar'
^nu --testbin cococo foo=(2 + 2) $"foo=(2 + 2)" foo=$"(2 + 2)"
"#
));
assert_eq!(actual.out, "--foo=bar");
assert_eq!(actual.out, "foo=4 foo=4 foo=4");
})
}
@ -200,6 +229,67 @@ fn external_command_escape_args() {
})
}
#[test]
#[cfg_attr(
not(target_os = "linux"),
ignore = "only runs on Linux, where controlling the HOME var is reliable"
)]
fn external_command_expand_tilde() {
Playground::setup("external command expand tilde", |dirs, _| {
// Make a copy of the nu executable that we can use
let mut src = std::fs::File::open(nu_test_support::fs::binaries().join("nu"))
.expect("failed to open nu");
let mut dst = std::fs::File::create_new(dirs.test().join("test_nu"))
.expect("failed to create test_nu file");
std::io::copy(&mut src, &mut dst).expect("failed to copy data for nu binary");
// Make test_nu have the same permissions so that it's executable
dst.set_permissions(
src.metadata()
.expect("failed to get nu metadata")
.permissions(),
)
.expect("failed to set permissions on test_nu");
// Close the files
drop(dst);
drop(src);
let actual = nu!(
envs: vec![
("HOME".to_string(), dirs.test().to_string_lossy().into_owned()),
],
r#"
^~/test_nu --testbin cococo hello
"#
);
assert_eq!(actual.out, "hello");
})
}
#[test]
fn external_arg_expand_tilde() {
Playground::setup("external arg expand tilde", |dirs, _| {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
^nu --testbin cococo ~/foo ~/(2 + 2)
"#
));
let home = dirs_next::home_dir().expect("failed to find home dir");
assert_eq!(
actual.out,
format!(
"{} {}",
home.join("foo").display(),
home.join("4").display()
)
);
})
}
#[test]
fn external_command_not_expand_tilde_with_quotes() {
Playground::setup(
@ -231,21 +321,6 @@ fn external_command_receives_raw_binary_data() {
})
}
#[cfg(windows)]
#[test]
fn failed_command_with_semicolon_will_not_execute_following_cmds_windows() {
Playground::setup("external failed command with semicolon", |dirs, _| {
let actual = nu!(
cwd: dirs.test(), pipeline(
"
^cargo asdf; echo done
"
));
assert!(!actual.out.contains("done"));
})
}
#[cfg(windows)]
#[test]
fn can_run_batch_files() {

View File

@ -3,6 +3,7 @@ use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
use syn::{
spanned::Spanned, Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Generics, Ident,
Type,
};
use crate::attributes::{self, ContainerAttributes};
@ -116,15 +117,11 @@ fn derive_struct_from_value(
/// 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
/// })?,
/// )?,
/// favorite_toy: record
/// .remove("favorite_toy")
/// .map(|v| <#ty as nu_protocol::FromValue>::from_value(v))
/// .transpose()?
/// .flatten(),
/// })
/// }
/// }
@ -480,20 +477,29 @@ fn parse_value_via_fields(fields: &Fields, self_ident: impl ToTokens) -> TokenSt
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
match type_is_option(ty) {
true => quote! {
#ident: 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
})?,
)?
.map(|v| <#ty as nu_protocol::FromValue>::from_value(v))
.transpose()?
.flatten()
},
false => 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! {
@ -537,3 +543,25 @@ fn parse_value_via_fields(fields: &Fields, self_ident: impl ToTokens) -> TokenSt
},
}
}
const FULLY_QUALIFIED_OPTION: &str = "std::option::Option";
const PARTIALLY_QUALIFIED_OPTION: &str = "option::Option";
const PRELUDE_OPTION: &str = "Option";
/// Check if the field type is an `Option`.
///
/// This function checks if a given type is an `Option`.
/// We assume that an `Option` is [`std::option::Option`] because we can't see the whole code and
/// can't ask the compiler itself.
/// If the `Option` type isn't `std::option::Option`, the user will get a compile error due to a
/// type mismatch.
/// It's very unusual for people to override `Option`, so this should rarely be an issue.
///
/// When [rust#63084](https://github.com/rust-lang/rust/issues/63084) is resolved, we can use
/// [`std::any::type_name`] for a static assertion check to get a more direct error messages.
fn type_is_option(ty: &Type) -> bool {
let s = ty.to_token_stream().to_string();
s.starts_with(PRELUDE_OPTION)
|| s.starts_with(PARTIALLY_QUALIFIED_OPTION)
|| s.starts_with(FULLY_QUALIFIED_OPTION)
}

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

@ -131,7 +131,7 @@ impl RecordView {
Orientation::Left => (column, row),
};
if row >= layer.count_rows() || column >= layer.count_columns() {
if row >= layer.record_values.len() || column >= layer.column_names.len() {
// actually must never happen; unless cursor works incorrectly
// if being sure about cursor it can be deleted;
return Value::nothing(Span::unknown());
@ -610,7 +610,7 @@ 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) {
let layer = state.get_layer_last_mut();
let count_rows = layer.count_rows();
let count_rows = layer.record_values.len();
if count_rows > page_size {
layer
.cursor
@ -722,43 +722,66 @@ fn get_percentage(value: usize, max: usize) -> usize {
}
fn transpose_table(layer: &mut RecordLayer) {
if layer.was_transposed {
transpose_from(layer);
} else {
transpose_to(layer);
}
layer.was_transposed = !layer.was_transposed;
}
fn transpose_from(layer: &mut RecordLayer) {
let count_rows = layer.record_values.len();
let count_columns = layer.column_names.len();
if layer.was_transposed {
let headers = pop_first_column(&mut layer.record_values);
let headers = headers
.into_iter()
.map(|value| match value {
Value::String { val, .. } => val,
_ => unreachable!("must never happen"),
})
.collect();
if let Some(data) = &mut layer.record_text {
pop_first_column(data);
*data = _transpose_table(data, count_rows, count_columns - 1);
}
let data = _transpose_table(&layer.record_values, count_rows, count_columns - 1);
let headers = pop_first_column(&mut layer.record_values);
let headers = headers
.into_iter()
.map(|value| match value {
Value::String { val, .. } => val,
_ => unreachable!("must never happen"),
})
.collect();
layer.record_values = data;
layer.column_names = headers;
let data = _transpose_table(&layer.record_values, count_rows, count_columns - 1);
return;
layer.record_values = data;
layer.column_names = headers;
}
fn transpose_to(layer: &mut RecordLayer) {
let count_rows = layer.record_values.len();
let count_columns = layer.column_names.len();
if let Some(data) = &mut layer.record_text {
*data = _transpose_table(data, count_rows, count_columns);
for (column, column_name) in layer.column_names.iter().enumerate() {
let value = (column_name.to_owned(), Default::default());
data[column].insert(0, value);
}
}
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;
}
fn pop_first_column(values: &mut [Vec<Value>]) -> Vec<Value> {
let mut data = vec![Value::default(); values.len()];
fn pop_first_column<T>(values: &mut [Vec<T>]) -> Vec<T>
where
T: Default + Clone,
{
let mut data = vec![T::default(); values.len()];
for (row, values) in values.iter_mut().enumerate() {
data[row] = values.remove(0);
}
@ -766,12 +789,11 @@ fn pop_first_column(values: &mut [Vec<Value>]) -> Vec<Value> {
data
}
fn _transpose_table(
values: &[Vec<Value>],
count_rows: usize,
count_columns: usize,
) -> Vec<Vec<Value>> {
let mut data = vec![vec![Value::default(); count_rows]; count_columns];
fn _transpose_table<T>(values: &[Vec<T>], count_rows: usize, count_columns: usize) -> Vec<Vec<T>>
where
T: Clone + Default,
{
let mut data = vec![vec![T::default(); count_rows]; count_columns];
for (row, values) in values.iter().enumerate() {
for (column, value) in values.iter().enumerate() {
data[column][row].clone_from(value);

View File

@ -88,6 +88,7 @@ impl StatefulWidget for TableWidget<'_> {
// todo: refactoring these to methods as they have quite a bit in common.
impl<'a> TableWidget<'a> {
// header at the top; header is always 1 line
fn render_table_horizontal(self, area: Rect, buf: &mut Buffer, state: &mut TableWidgetState) {
let padding_l = self.config.column_padding_left as u16;
let padding_r = self.config.column_padding_right as u16;
@ -130,25 +131,16 @@ impl<'a> TableWidget<'a> {
}
if show_index {
let area = Rect::new(width, data_y, area.width, data_height);
width += render_index(
buf,
area,
Rect::new(width, data_y, area.width, data_height),
self.style_computer,
self.index_row,
padding_l,
padding_r,
);
width += render_vertical_line_with_split(
buf,
width,
data_y,
data_height,
show_head,
false,
separator_s,
);
width += render_split_line(buf, width, area.y, area.height, show_head, separator_s);
}
// if there is more data than we can show, add an ellipsis to the column headers to hint at that
@ -162,6 +154,11 @@ impl<'a> TableWidget<'a> {
}
for col in self.index_column..self.columns.len() {
let need_split_line = state.count_columns > 0 && width < area.width;
if need_split_line {
width += render_split_line(buf, width, area.y, area.height, show_head, separator_s);
}
let mut column = create_column(data, col);
let column_width = calculate_column_width(&column);
@ -200,6 +197,7 @@ impl<'a> TableWidget<'a> {
}
let head_iter = [(&head, head_style)].into_iter();
// we don't change width here cause the whole column have the same width; so we add it when we print data
let mut w = width;
w += render_space(buf, w, head_y, 1, padding_l);
w += render_column(buf, w, head_y, use_space, head_iter);
@ -209,10 +207,10 @@ impl<'a> TableWidget<'a> {
state.layout.push(&head, x, head_y, use_space, 1);
}
let head_rows = column.iter().map(|(t, s)| (t, *s));
let column_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, head_rows);
width += render_column(buf, width, data_y, use_space, column_rows);
width += render_space(buf, width, data_y, data_height, padding_r);
for (row, (text, _)) in column.iter().enumerate() {
@ -235,15 +233,7 @@ impl<'a> TableWidget<'a> {
}
if width < area.width {
width += render_vertical_line_with_split(
buf,
width,
data_y,
data_height,
show_head,
false,
separator_s,
);
width += render_split_line(buf, width, area.y, area.height, show_head, separator_s);
}
let rest = area.width.saturating_sub(width);
@ -255,6 +245,7 @@ impl<'a> TableWidget<'a> {
}
}
// header at the left; header is always 1 line
fn render_table_vertical(self, area: Rect, buf: &mut Buffer, state: &mut TableWidgetState) {
if area.width == 0 || area.height == 0 {
return;
@ -353,6 +344,9 @@ impl<'a> TableWidget<'a> {
state.count_rows = columns.len();
state.count_columns = 0;
// note: is there a time where we would have more then 1 column?
// seems like not really; cause it's literally KV table, or am I wrong?
for col in self.index_column..self.data.len() {
let mut column =
self.data[col][self.index_row..self.index_row + columns.len()].to_vec();
@ -361,6 +355,13 @@ impl<'a> TableWidget<'a> {
break;
}
// see KV comment; this block might never got used
let need_split_line = state.count_columns > 0 && left_w < area.width;
if need_split_line {
render_vertical_line(buf, area.x + left_w, area.y, area.height, separator_s);
left_w += 1;
}
let column_width = column_width as u16;
let available = area.width - left_w;
let is_last = col + 1 == self.data.len();
@ -555,6 +556,51 @@ fn render_index(
width
}
fn render_split_line(
buf: &mut Buffer,
x: u16,
y: u16,
height: u16,
has_head: bool,
style: NuStyle,
) -> u16 {
if has_head {
render_vertical_split_line(buf, x, y, height, &[0], &[2], &[], style);
} else {
render_vertical_split_line(buf, x, y, height, &[], &[], &[], style);
}
1
}
#[allow(clippy::too_many_arguments)]
fn render_vertical_split_line(
buf: &mut Buffer,
x: u16,
y: u16,
height: u16,
top_slit: &[u16],
inner_slit: &[u16],
bottom_slit: &[u16],
style: NuStyle,
) -> u16 {
render_vertical_line(buf, x, y, height, style);
for &y in top_slit {
render_top_connector(buf, x, y, style);
}
for &y in inner_slit {
render_inner_connector(buf, x, y, style);
}
for &y in bottom_slit {
render_bottom_connector(buf, x, y, style);
}
1
}
fn render_vertical_line_with_split(
buf: &mut Buffer,
x: u16,
@ -668,6 +714,12 @@ fn render_bottom_connector(buf: &mut Buffer, x: u16, y: u16, style: NuStyle) {
buf.set_span(x, y, &span, 1);
}
fn render_inner_connector(buf: &mut Buffer, x: u16, y: u16, style: NuStyle) {
let style = nu_style_to_tui(style);
let span = Span::styled("", style);
buf.set_span(x, y, &span, 1);
}
fn calculate_column_width(column: &[NuText]) -> usize {
column
.iter()

View File

@ -26,6 +26,7 @@ pub enum FlatShape {
Flag,
Float,
Garbage,
GlobInterpolation,
GlobPattern,
Int,
InternalCall(DeclId),
@ -67,6 +68,7 @@ impl FlatShape {
FlatShape::Flag => "shape_flag",
FlatShape::Float => "shape_float",
FlatShape::Garbage => "shape_garbage",
FlatShape::GlobInterpolation => "shape_glob_interpolation",
FlatShape::GlobPattern => "shape_globpattern",
FlatShape::Int => "shape_int",
FlatShape::InternalCall(_) => "shape_internalcall",
@ -277,7 +279,7 @@ fn flatten_expression_into(
output[arg_start..].sort();
}
Expr::ExternalCall(head, args) => {
if let Expr::String(..) = &head.expr {
if let Expr::String(..) | Expr::GlobPattern(..) = &head.expr {
output.push((head.span, FlatShape::External));
} else {
flatten_expression_into(working_set, head, output);
@ -286,7 +288,7 @@ fn flatten_expression_into(
for arg in args.as_ref() {
match arg {
ExternalArgument::Regular(expr) => {
if let Expr::String(..) = &expr.expr {
if let Expr::String(..) | Expr::GlobPattern(..) = &expr.expr {
output.push((expr.span, FlatShape::ExternalArg));
} else {
flatten_expression_into(working_set, expr, output);
@ -431,6 +433,25 @@ fn flatten_expression_into(
}
output.extend(flattened);
}
Expr::GlobInterpolation(exprs, quoted) => {
let mut flattened = vec![];
for expr in exprs {
flatten_expression_into(working_set, expr, &mut flattened);
}
if *quoted {
// If we aren't a bare word interpolation, also highlight the outer quotes
output.push((
Span::new(expr.span.start, expr.span.start + 2),
FlatShape::GlobInterpolation,
));
flattened.push((
Span::new(expr.span.end - 1, expr.span.end),
FlatShape::GlobInterpolation,
));
}
output.extend(flattened);
}
Expr::Record(list) => {
let outer_span = expr.span;
let mut last_end = outer_span.start;

View File

@ -3740,28 +3740,37 @@ pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteComm
)
})?;
let signatures = plugin
let metadata_and_signatures = plugin
.clone()
.get(get_envs, None)
.and_then(|p| p.get_signature())
.and_then(|p| {
let meta = p.get_metadata()?;
let sigs = p.get_signature()?;
Ok((meta, sigs))
})
.map_err(|err| {
log::warn!("Error getting signatures: {err:?}");
log::warn!("Error getting metadata and signatures: {err:?}");
ParseError::LabeledError(
"Error getting signatures".into(),
"Error getting metadata and signatures".into(),
err.to_string(),
spans[0],
)
});
if let Ok(ref signatures) = signatures {
// Add the loaded plugin to the delta
working_set.update_plugin_registry(PluginRegistryItem::new(
&identity,
signatures.clone(),
));
match metadata_and_signatures {
Ok((meta, sigs)) => {
// Set the metadata on the plugin
plugin.set_metadata(Some(meta.clone()));
// Add the loaded plugin to the delta
working_set.update_plugin_registry(PluginRegistryItem::new(
&identity,
meta,
sigs.clone(),
));
Ok(sigs)
}
Err(err) => Err(err),
}
signatures
},
|sig| sig.map(|sig| vec![sig]),
)?;

View File

@ -16,7 +16,6 @@ use nu_protocol::{
IN_VARIABLE_ID,
};
use std::{
borrow::Cow,
collections::{HashMap, HashSet},
num::ParseIntError,
str,
@ -222,6 +221,209 @@ pub(crate) fn check_call(
}
}
/// Parses a string in the arg or head position of an external call.
///
/// If the string begins with `r#`, it is parsed as a raw string. If it doesn't contain any quotes
/// or parentheses, it is parsed as a glob pattern so that tilde and glob expansion can be handled
/// by `run-external`. Otherwise, we use a custom state machine to put together an interpolated
/// string, where each balanced pair of quotes is parsed as a separate part of the string, and then
/// concatenated together.
///
/// For example, `-foo="bar\nbaz"` becomes `$"-foo=bar\nbaz"`
fn parse_external_string(working_set: &mut StateWorkingSet, span: Span) -> Expression {
let contents = &working_set.get_span_contents(span);
if contents.starts_with(b"r#") {
parse_raw_string(working_set, span)
} else if contents
.iter()
.any(|b| matches!(b, b'"' | b'\'' | b'(' | b')'))
{
enum State {
Bare {
from: usize,
},
Quote {
from: usize,
quote_char: u8,
escaped: bool,
depth: i32,
},
}
// Find the spans of parts of the string that can be parsed as their own strings for
// concatenation.
//
// By passing each of these parts to `parse_string()`, we can eliminate the quotes and also
// handle string interpolation.
let make_span = |from: usize, index: usize| Span {
start: span.start + from,
end: span.start + index,
};
let mut spans = vec![];
let mut state = State::Bare { from: 0 };
let mut index = 0;
while index < contents.len() {
let ch = contents[index];
match &mut state {
State::Bare { from } => match ch {
b'"' | b'\'' => {
// Push bare string
if index != *from {
spans.push(make_span(*from, index));
}
// then transition to other state
state = State::Quote {
from: index,
quote_char: ch,
escaped: false,
depth: 1,
};
}
b'$' => {
if let Some(&quote_char @ (b'"' | b'\'')) = contents.get(index + 1) {
// Start a dollar quote (interpolated string)
if index != *from {
spans.push(make_span(*from, index));
}
state = State::Quote {
from: index,
quote_char,
escaped: false,
depth: 1,
};
// Skip over two chars (the dollar sign and the quote)
index += 2;
continue;
}
}
// Continue to consume
_ => (),
},
State::Quote {
from,
quote_char,
escaped,
depth,
} => match ch {
ch if ch == *quote_char && !*escaped => {
// Count if there are more than `depth` quotes remaining
if contents[index..]
.iter()
.filter(|b| *b == quote_char)
.count() as i32
> *depth
{
// Increment depth to be greedy
*depth += 1;
} else {
// Decrement depth
*depth -= 1;
}
if *depth == 0 {
// End of string
spans.push(make_span(*from, index + 1));
// go back to Bare state
state = State::Bare { from: index + 1 };
}
}
b'\\' if !*escaped && *quote_char == b'"' => {
// The next token is escaped so it doesn't count (only for double quote)
*escaped = true;
}
_ => {
*escaped = false;
}
},
}
index += 1;
}
// Add the final span
match state {
State::Bare { from } | State::Quote { from, .. } => {
if from < contents.len() {
spans.push(make_span(from, contents.len()));
}
}
}
// Log the spans that will be parsed
if log::log_enabled!(log::Level::Trace) {
let contents = spans
.iter()
.map(|span| String::from_utf8_lossy(working_set.get_span_contents(*span)))
.collect::<Vec<_>>();
trace!("parsing: external string, parts: {contents:?}")
}
// Check if the whole thing is quoted. If not, it should be a glob
let quoted =
(contents.len() >= 3 && contents.starts_with(b"$\"") && contents.ends_with(b"\""))
|| is_quoted(contents);
// Parse each as its own string
let exprs: Vec<Expression> = spans
.into_iter()
.map(|span| parse_string(working_set, span))
.collect();
if exprs
.iter()
.all(|expr| matches!(expr.expr, Expr::String(..)))
{
// If the exprs are all strings anyway, just collapse into a single string.
let string = exprs
.into_iter()
.map(|expr| {
let Expr::String(contents) = expr.expr else {
unreachable!("already checked that this was a String")
};
contents
})
.collect::<String>();
if quoted {
Expression::new(working_set, Expr::String(string), span, Type::String)
} else {
Expression::new(
working_set,
Expr::GlobPattern(string, false),
span,
Type::Glob,
)
}
} else {
// Flatten any string interpolations contained with the exprs.
let exprs = exprs
.into_iter()
.flat_map(|expr| match expr.expr {
Expr::StringInterpolation(subexprs) => subexprs,
_ => vec![expr],
})
.collect();
// Make an interpolation out of the expressions. Use `GlobInterpolation` if it's a bare
// word, so that the unquoted state can get passed through to `run-external`.
if quoted {
Expression::new(
working_set,
Expr::StringInterpolation(exprs),
span,
Type::String,
)
} else {
Expression::new(
working_set,
Expr::GlobInterpolation(exprs, false),
span,
Type::Glob,
)
}
}
} else {
parse_glob_pattern(working_set, span)
}
}
fn parse_external_arg(working_set: &mut StateWorkingSet, span: Span) -> ExternalArgument {
let contents = working_set.get_span_contents(span);
@ -229,8 +431,6 @@ fn parse_external_arg(working_set: &mut StateWorkingSet, span: Span) -> External
ExternalArgument::Regular(parse_dollar_expr(working_set, span))
} else if contents.starts_with(b"[") {
ExternalArgument::Regular(parse_list_expression(working_set, span, &SyntaxShape::Any))
} else if contents.starts_with(b"r#") {
ExternalArgument::Regular(parse_raw_string(working_set, span))
} else if contents.len() > 3
&& contents.starts_with(b"...")
&& (contents[3] == b'$' || contents[3] == b'[' || contents[3] == b'(')
@ -241,18 +441,7 @@ fn parse_external_arg(working_set: &mut StateWorkingSet, span: Span) -> External
&SyntaxShape::List(Box::new(SyntaxShape::Any)),
))
} else {
// Eval stage trims the quotes, so we don't have to do the same thing when parsing.
let (contents, err) = unescape_string_preserving_quotes(contents, span);
if let Some(err) = err {
working_set.error(err);
}
ExternalArgument::Regular(Expression::new(
working_set,
Expr::String(contents),
span,
Type::String,
))
ExternalArgument::Regular(parse_external_string(working_set, span))
}
}
@ -274,18 +463,7 @@ pub fn parse_external_call(working_set: &mut StateWorkingSet, spans: &[Span]) ->
let arg = parse_expression(working_set, &[head_span]);
Box::new(arg)
} else {
// Eval stage will unquote the string, so we don't bother with that here
let (contents, err) = unescape_string_preserving_quotes(&head_contents, head_span);
if let Some(err) = err {
working_set.error(err)
}
Box::new(Expression::new(
working_set,
Expr::String(contents),
head_span,
Type::String,
))
Box::new(parse_external_string(working_set, head_span))
};
let args = spans[1..]
@ -2639,23 +2817,6 @@ pub fn unescape_unquote_string(bytes: &[u8], span: Span) -> (String, Option<Pars
}
}
/// XXX: This is here temporarily as a patch, but we should replace this with properly representing
/// the quoted state of a string in the AST
fn unescape_string_preserving_quotes(bytes: &[u8], span: Span) -> (String, Option<ParseError>) {
let (bytes, err) = if bytes.starts_with(b"\"") {
let (bytes, err) = unescape_string(bytes, span);
(Cow::Owned(bytes), err)
} else {
(Cow::Borrowed(bytes), None)
};
// The original code for args used lossy conversion here, even though that's not what we
// typically use for strings. Revisit whether that's actually desirable later, but don't
// want to introduce a breaking change for this patch.
let token = String::from_utf8_lossy(&bytes).into_owned();
(token, err)
}
pub fn parse_string(working_set: &mut StateWorkingSet, span: Span) -> Expression {
trace!("parsing: string");
@ -6012,7 +6173,7 @@ pub fn discover_captures_in_expr(
}
Expr::String(_) => {}
Expr::RawString(_) => {}
Expr::StringInterpolation(exprs) => {
Expr::StringInterpolation(exprs) | Expr::GlobInterpolation(exprs, _) => {
for expr in exprs {
discover_captures_in_expr(working_set, expr, seen, seen_blocks, output)?;
}

View File

@ -1,8 +1,8 @@
use nu_parser::*;
use nu_protocol::{
ast::{Argument, Call, Expr, ExternalArgument, PathMember, Range},
ast::{Argument, Call, Expr, Expression, ExternalArgument, PathMember, Range},
engine::{Command, EngineState, Stack, StateWorkingSet},
ParseError, PipelineData, ShellError, Signature, Span, SyntaxShape,
ParseError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type,
};
use rstest::rstest;
@ -182,7 +182,7 @@ pub fn multi_test_parse_int() {
Test(
"ranges or relative paths not confused for int",
b"./a/b",
Expr::String("./a/b".into()),
Expr::GlobPattern("./a/b".into(), false),
None,
),
Test(
@ -694,6 +694,50 @@ pub fn parse_call_missing_req_flag() {
));
}
fn test_external_call(input: &str, tag: &str, f: impl FnOnce(&Expression, &[ExternalArgument])) {
let engine_state = EngineState::new();
let mut working_set = StateWorkingSet::new(&engine_state);
let block = parse(&mut working_set, None, input.as_bytes(), true);
assert!(
working_set.parse_errors.is_empty(),
"{tag}: errors: {:?}",
working_set.parse_errors
);
let pipeline = &block.pipelines[0];
assert_eq!(1, pipeline.len());
let element = &pipeline.elements[0];
match &element.expr.expr {
Expr::ExternalCall(name, args) => f(name, args),
other => {
panic!("{tag}: Unexpected expression in pipeline: {other:?}");
}
}
}
fn check_external_call_interpolation(
tag: &str,
subexpr_count: usize,
quoted: bool,
expr: &Expression,
) -> bool {
match &expr.expr {
Expr::StringInterpolation(exprs) => {
assert!(quoted, "{tag}: quoted");
assert_eq!(expr.ty, Type::String, "{tag}: expr.ty");
assert_eq!(subexpr_count, exprs.len(), "{tag}: subexpr_count");
true
}
Expr::GlobInterpolation(exprs, is_quoted) => {
assert_eq!(quoted, *is_quoted, "{tag}: quoted");
assert_eq!(expr.ty, Type::Glob, "{tag}: expr.ty");
assert_eq!(subexpr_count, exprs.len(), "{tag}: subexpr_count");
true
}
_ => false,
}
}
#[rstest]
#[case("foo-external-call", "foo-external-call", "bare word")]
#[case("^foo-external-call", "foo-external-call", "bare word with caret")]
@ -713,200 +757,370 @@ pub fn parse_call_missing_req_flag() {
r"foo\external-call",
"bare word with backslash and caret"
)]
#[case(
"^'foo external call'",
"'foo external call'",
"single quote with caret"
)]
#[case(
"^'foo/external call'",
"'foo/external call'",
"single quote with forward slash and caret"
)]
#[case(
r"^'foo\external call'",
r"'foo\external call'",
"single quote with backslash and caret"
)]
#[case(
r#"^"foo external call""#,
r#""foo external call""#,
"double quote with caret"
)]
#[case(
r#"^"foo/external call""#,
r#""foo/external call""#,
"double quote with forward slash and caret"
)]
#[case(
r#"^"foo\\external call""#,
r#""foo\external call""#,
"double quote with backslash and caret"
)]
#[case("`foo external call`", "`foo external call`", "backtick quote")]
#[case("`foo external call`", "foo external call", "backtick quote")]
#[case(
"^`foo external call`",
"`foo external call`",
"foo external call",
"backtick quote with caret"
)]
#[case(
"`foo/external call`",
"`foo/external call`",
"foo/external call",
"backtick quote with forward slash"
)]
#[case(
"^`foo/external call`",
"`foo/external call`",
"foo/external call",
"backtick quote with forward slash and caret"
)]
#[case(
r"^`foo\external call`",
r"`foo\external call`",
r"foo\external call",
"backtick quote with backslash"
)]
#[case(
r"^`foo\external call`",
r"`foo\external call`",
r"foo\external call",
"backtick quote with backslash and caret"
)]
fn test_external_call_name(#[case] input: &str, #[case] expected: &str, #[case] tag: &str) {
let engine_state = EngineState::new();
let mut working_set = StateWorkingSet::new(&engine_state);
let block = parse(&mut working_set, None, input.as_bytes(), true);
assert!(
working_set.parse_errors.is_empty(),
"{tag}: errors: {:?}",
working_set.parse_errors
);
let pipeline = &block.pipelines[0];
assert_eq!(1, pipeline.len());
let element = &pipeline.elements[0];
match &element.expr.expr {
Expr::ExternalCall(name, args) => {
match &name.expr {
Expr::String(string) => {
assert_eq!(expected, string);
}
other => {
panic!("{tag}: Unexpected expression in command name position: {other:?}");
}
}
assert_eq!(0, args.len());
}
other => {
panic!("{tag}: Unexpected expression in pipeline: {other:?}");
}
}
}
#[rstest]
#[case("^foo bar-baz", "bar-baz", "bare word")]
#[case("^foo bar/baz", "bar/baz", "bare word with forward slash")]
#[case(r"^foo bar\baz", r"bar\baz", "bare word with backslash")]
#[case("^foo 'bar baz'", "'bar baz'", "single quote")]
#[case("foo 'bar/baz'", "'bar/baz'", "single quote with forward slash")]
#[case(r"foo 'bar\baz'", r"'bar\baz'", "single quote with backslash")]
#[case(r#"^foo "bar baz""#, r#""bar baz""#, "double quote")]
#[case(r#"^foo "bar/baz""#, r#""bar/baz""#, "double quote with forward slash")]
#[case(r#"^foo "bar\\baz""#, r#""bar\baz""#, "double quote with backslash")]
#[case("^foo `bar baz`", "`bar baz`", "backtick quote")]
#[case("^foo `bar/baz`", "`bar/baz`", "backtick quote with forward slash")]
#[case(r"^foo `bar\baz`", r"`bar\baz`", "backtick quote with backslash")]
fn test_external_call_argument_regular(
pub fn test_external_call_head_glob(
#[case] input: &str,
#[case] expected: &str,
#[case] tag: &str,
) {
let engine_state = EngineState::new();
let mut working_set = StateWorkingSet::new(&engine_state);
let block = parse(&mut working_set, None, input.as_bytes(), true);
assert!(
working_set.parse_errors.is_empty(),
"{tag}: errors: {:?}",
working_set.parse_errors
);
test_external_call(input, tag, |name, args| {
match &name.expr {
Expr::GlobPattern(string, is_quoted) => {
assert_eq!(expected, string, "{tag}: incorrect name");
assert!(!*is_quoted);
}
other => {
panic!("{tag}: Unexpected expression in command name position: {other:?}");
}
}
assert_eq!(0, args.len());
})
}
let pipeline = &block.pipelines[0];
assert_eq!(1, pipeline.len());
let element = &pipeline.elements[0];
match &element.expr.expr {
Expr::ExternalCall(name, args) => {
match &name.expr {
Expr::String(string) => {
assert_eq!("foo", string, "{tag}: incorrect name");
#[rstest]
#[case(
r##"^r#'foo-external-call'#"##,
"foo-external-call",
"raw string with caret"
)]
#[case(
r##"^r#'foo/external-call'#"##,
"foo/external-call",
"raw string with forward slash and caret"
)]
#[case(
r##"^r#'foo\external-call'#"##,
r"foo\external-call",
"raw string with backslash and caret"
)]
pub fn test_external_call_head_raw_string(
#[case] input: &str,
#[case] expected: &str,
#[case] tag: &str,
) {
test_external_call(input, tag, |name, args| {
match &name.expr {
Expr::RawString(string) => {
assert_eq!(expected, string, "{tag}: incorrect name");
}
other => {
panic!("{tag}: Unexpected expression in command name position: {other:?}");
}
}
assert_eq!(0, args.len());
})
}
#[rstest]
#[case("^'foo external call'", "foo external call", "single quote with caret")]
#[case(
"^'foo/external call'",
"foo/external call",
"single quote with forward slash and caret"
)]
#[case(
r"^'foo\external call'",
r"foo\external call",
"single quote with backslash and caret"
)]
#[case(
r#"^"foo external call""#,
r#"foo external call"#,
"double quote with caret"
)]
#[case(
r#"^"foo/external call""#,
r#"foo/external call"#,
"double quote with forward slash and caret"
)]
#[case(
r#"^"foo\\external call""#,
r#"foo\external call"#,
"double quote with backslash and caret"
)]
pub fn test_external_call_head_string(
#[case] input: &str,
#[case] expected: &str,
#[case] tag: &str,
) {
test_external_call(input, tag, |name, args| {
match &name.expr {
Expr::String(string) => {
assert_eq!(expected, string);
}
other => {
panic!("{tag}: Unexpected expression in command name position: {other:?}");
}
}
assert_eq!(0, args.len());
})
}
#[rstest]
#[case(r"~/.foo/(1)", 2, false, "unquoted interpolated string")]
#[case(
r"~\.foo(2)\(1)",
4,
false,
"unquoted interpolated string with backslash"
)]
#[case(r"^~/.foo/(1)", 2, false, "unquoted interpolated string with caret")]
#[case(r#"^$"~/.foo/(1)""#, 2, true, "quoted interpolated string with caret")]
pub fn test_external_call_head_interpolated_string(
#[case] input: &str,
#[case] subexpr_count: usize,
#[case] quoted: bool,
#[case] tag: &str,
) {
test_external_call(input, tag, |name, args| {
if !check_external_call_interpolation(tag, subexpr_count, quoted, name) {
panic!("{tag}: Unexpected expression in command name position: {name:?}");
}
assert_eq!(0, args.len());
})
}
#[rstest]
#[case("^foo foo-external-call", "foo-external-call", "bare word")]
#[case(
"^foo foo/external-call",
"foo/external-call",
"bare word with forward slash"
)]
#[case(
r"^foo foo\external-call",
r"foo\external-call",
"bare word with backslash"
)]
#[case(
"^foo `foo external call`",
"foo external call",
"backtick quote with caret"
)]
#[case(
"^foo `foo/external call`",
"foo/external call",
"backtick quote with forward slash"
)]
#[case(
r"^foo `foo\external call`",
r"foo\external call",
"backtick quote with backslash"
)]
pub fn test_external_call_arg_glob(#[case] input: &str, #[case] expected: &str, #[case] tag: &str) {
test_external_call(input, tag, |name, args| {
match &name.expr {
Expr::GlobPattern(string, _) => {
assert_eq!("foo", string, "{tag}: incorrect name");
}
other => {
panic!("{tag}: Unexpected expression in command name position: {other:?}");
}
}
assert_eq!(1, args.len());
match &args[0] {
ExternalArgument::Regular(expr) => match &expr.expr {
Expr::GlobPattern(string, is_quoted) => {
assert_eq!(expected, string, "{tag}: incorrect arg");
assert!(!*is_quoted);
}
other => {
panic!("{tag}: Unexpected expression in command name position: {other:?}");
}
}
assert_eq!(1, args.len());
match &args[0] {
ExternalArgument::Regular(expr) => match &expr.expr {
Expr::String(string) => {
assert_eq!(expected, string, "{tag}: incorrect arg");
}
other => {
panic!("Unexpected expression in command arg position: {other:?}")
}
},
other @ ExternalArgument::Spread(..) => {
panic!("Unexpected external spread argument in command arg position: {other:?}")
panic!("Unexpected expression in command arg position: {other:?}")
}
},
other @ ExternalArgument::Spread(..) => {
panic!("Unexpected external spread argument in command arg position: {other:?}")
}
}
other => {
panic!("{tag}: Unexpected expression in pipeline: {other:?}");
})
}
#[rstest]
#[case(r##"^foo r#'foo-external-call'#"##, "foo-external-call", "raw string")]
#[case(
r##"^foo r#'foo/external-call'#"##,
"foo/external-call",
"raw string with forward slash"
)]
#[case(
r##"^foo r#'foo\external-call'#"##,
r"foo\external-call",
"raw string with backslash"
)]
pub fn test_external_call_arg_raw_string(
#[case] input: &str,
#[case] expected: &str,
#[case] tag: &str,
) {
test_external_call(input, tag, |name, args| {
match &name.expr {
Expr::GlobPattern(string, _) => {
assert_eq!("foo", string, "{tag}: incorrect name");
}
other => {
panic!("{tag}: Unexpected expression in command name position: {other:?}");
}
}
}
assert_eq!(1, args.len());
match &args[0] {
ExternalArgument::Regular(expr) => match &expr.expr {
Expr::RawString(string) => {
assert_eq!(expected, string, "{tag}: incorrect arg");
}
other => {
panic!("Unexpected expression in command arg position: {other:?}")
}
},
other @ ExternalArgument::Spread(..) => {
panic!("Unexpected external spread argument in command arg position: {other:?}")
}
}
})
}
#[rstest]
#[case("^foo 'foo external call'", "foo external call", "single quote")]
#[case(
"^foo 'foo/external call'",
"foo/external call",
"single quote with forward slash"
)]
#[case(
r"^foo 'foo\external call'",
r"foo\external call",
"single quote with backslash"
)]
#[case(r#"^foo "foo external call""#, r#"foo external call"#, "double quote")]
#[case(
r#"^foo "foo/external call""#,
r#"foo/external call"#,
"double quote with forward slash"
)]
#[case(
r#"^foo "foo\\external call""#,
r#"foo\external call"#,
"double quote with backslash"
)]
pub fn test_external_call_arg_string(
#[case] input: &str,
#[case] expected: &str,
#[case] tag: &str,
) {
test_external_call(input, tag, |name, args| {
match &name.expr {
Expr::GlobPattern(string, _) => {
assert_eq!("foo", string, "{tag}: incorrect name");
}
other => {
panic!("{tag}: Unexpected expression in command name position: {other:?}");
}
}
assert_eq!(1, args.len());
match &args[0] {
ExternalArgument::Regular(expr) => match &expr.expr {
Expr::String(string) => {
assert_eq!(expected, string, "{tag}: incorrect arg");
}
other => {
panic!("{tag}: Unexpected expression in command arg position: {other:?}")
}
},
other @ ExternalArgument::Spread(..) => {
panic!(
"{tag}: Unexpected external spread argument in command arg position: {other:?}"
)
}
}
})
}
#[rstest]
#[case(r"^foo ~/.foo/(1)", 2, false, "unquoted interpolated string")]
#[case(r#"^foo $"~/.foo/(1)""#, 2, true, "quoted interpolated string")]
pub fn test_external_call_arg_interpolated_string(
#[case] input: &str,
#[case] subexpr_count: usize,
#[case] quoted: bool,
#[case] tag: &str,
) {
test_external_call(input, tag, |name, args| {
match &name.expr {
Expr::GlobPattern(string, _) => {
assert_eq!("foo", string, "{tag}: incorrect name");
}
other => {
panic!("{tag}: Unexpected expression in command name position: {other:?}");
}
}
assert_eq!(1, args.len());
match &args[0] {
ExternalArgument::Regular(expr) => {
if !check_external_call_interpolation(tag, subexpr_count, quoted, expr) {
panic!("Unexpected expression in command arg position: {expr:?}")
}
}
other @ ExternalArgument::Spread(..) => {
panic!("Unexpected external spread argument in command arg position: {other:?}")
}
}
})
}
#[test]
fn test_external_call_argument_spread() {
let engine_state = EngineState::new();
let mut working_set = StateWorkingSet::new(&engine_state);
let block = parse(&mut working_set, None, b"^foo ...[a b c]", true);
assert!(
working_set.parse_errors.is_empty(),
"errors: {:?}",
working_set.parse_errors
);
let input = r"^foo ...[a b c]";
let tag = "spread";
let pipeline = &block.pipelines[0];
assert_eq!(1, pipeline.len());
let element = &pipeline.elements[0];
match &element.expr.expr {
Expr::ExternalCall(name, args) => {
match &name.expr {
Expr::String(string) => {
assert_eq!("foo", string, "incorrect name");
test_external_call(input, tag, |name, args| {
match &name.expr {
Expr::GlobPattern(string, _) => {
assert_eq!("foo", string, "incorrect name");
}
other => {
panic!("Unexpected expression in command name position: {other:?}");
}
}
assert_eq!(1, args.len());
match &args[0] {
ExternalArgument::Spread(expr) => match &expr.expr {
Expr::List(items) => {
assert_eq!(3, items.len());
// that's good enough, don't really need to go so deep into it...
}
other => {
panic!("Unexpected expression in command name position: {other:?}");
}
}
assert_eq!(1, args.len());
match &args[0] {
ExternalArgument::Spread(expr) => match &expr.expr {
Expr::List(items) => {
assert_eq!(3, items.len());
// that's good enough, don't really need to go so deep into it...
}
other => {
panic!("Unexpected expression in command arg position: {other:?}")
}
},
other @ ExternalArgument::Regular(..) => {
panic!(
"Unexpected external regular argument in command arg position: {other:?}"
)
panic!("Unexpected expression in command arg position: {other:?}")
}
},
other @ ExternalArgument::Regular(..) => {
panic!("Unexpected external regular argument in command arg position: {other:?}")
}
}
other => {
panic!("Unexpected expression in pipeline: {other:?}");
}
}
})
}
#[test]
@ -1132,6 +1346,44 @@ mod string {
assert_eq!(subexprs[0], &Expr::String("(1 + 3)(7 - 5)".to_string()));
}
#[test]
pub fn parse_string_interpolation_bare() {
let engine_state = EngineState::new();
let mut working_set = StateWorkingSet::new(&engine_state);
let block = parse(
&mut working_set,
None,
b"\"\" ++ foo(1 + 3)bar(7 - 5)",
true,
);
assert!(working_set.parse_errors.is_empty());
assert_eq!(block.len(), 1);
let pipeline = &block.pipelines[0];
assert_eq!(pipeline.len(), 1);
let element = &pipeline.elements[0];
assert!(element.redirection.is_none());
let subexprs: Vec<&Expr> = match &element.expr.expr {
Expr::BinaryOp(_, _, rhs) => match &rhs.expr {
Expr::StringInterpolation(expressions) => {
expressions.iter().map(|e| &e.expr).collect()
}
_ => panic!("Expected an `Expr::StringInterpolation`"),
},
_ => panic!("Expected an `Expr::BinaryOp`"),
};
assert_eq!(subexprs.len(), 4);
assert_eq!(subexprs[0], &Expr::String("foo".to_string()));
assert!(matches!(subexprs[1], &Expr::FullCellPath(..)));
assert_eq!(subexprs[2], &Expr::String("bar".to_string()));
assert!(matches!(subexprs[3], &Expr::FullCellPath(..)));
}
#[test]
pub fn parse_nested_expressions() {
let engine_state = EngineState::new();

View File

@ -252,7 +252,7 @@ pub fn load_plugin_registry_item(
})?;
match &plugin.data {
PluginRegistryItemData::Valid { commands } => {
PluginRegistryItemData::Valid { metadata, commands } => {
let plugin = add_plugin_to_working_set(working_set, &identity)?;
// Ensure that the plugin is reset. We're going to load new signatures, so we want to
@ -260,6 +260,9 @@ pub fn load_plugin_registry_item(
// doesn't.
plugin.reset()?;
// Set the plugin metadata from the file
plugin.set_metadata(Some(metadata.clone()));
// Create the declarations from the commands
for signature in commands {
let decl = PluginDeclaration::new(plugin.clone(), signature.clone());

View File

@ -11,8 +11,8 @@ use nu_plugin_protocol::{
PluginOutput, ProtocolInfo, StreamId, StreamMessage,
};
use nu_protocol::{
ast::Operator, CustomValue, IntoSpanned, PipelineData, PluginSignature, ShellError, Span,
Spanned, Value,
ast::Operator, CustomValue, IntoSpanned, PipelineData, PluginMetadata, PluginSignature,
ShellError, Span, Spanned, Value,
};
use std::{
collections::{btree_map, BTreeMap},
@ -722,6 +722,7 @@ impl PluginInterface {
// Convert the call into one with a header and handle the stream, if necessary
let (call, writer) = match call {
PluginCall::Metadata => (PluginCall::Metadata, Default::default()),
PluginCall::Signature => (PluginCall::Signature, Default::default()),
PluginCall::CustomValueOp(value, op) => {
(PluginCall::CustomValueOp(value, op), Default::default())
@ -919,6 +920,17 @@ impl PluginInterface {
self.receive_plugin_call_response(result.receiver, context, result.state)
}
/// Get the metadata from the plugin.
pub fn get_metadata(&self) -> Result<PluginMetadata, ShellError> {
match self.plugin_call(PluginCall::Metadata, None)? {
PluginCallResponse::Metadata(meta) => Ok(meta),
PluginCallResponse::Error(err) => Err(err.into()),
_ => Err(ShellError::PluginFailedToDecode {
msg: "Received unexpected response to plugin Metadata call".into(),
}),
}
}
/// Get the command signatures from the plugin.
pub fn get_signature(&self) -> Result<Vec<PluginSignature>, ShellError> {
match self.plugin_call(PluginCall::Signature, None)? {
@ -1212,6 +1224,7 @@ impl CurrentCallState {
source: &PluginSource,
) -> Result<(), ShellError> {
match call {
PluginCall::Metadata => Ok(()),
PluginCall::Signature => Ok(()),
PluginCall::Run(CallInfo { call, .. }) => self.prepare_call_args(call, source),
PluginCall::CustomValueOp(_, op) => {

View File

@ -18,7 +18,7 @@ use nu_protocol::{
ast::{Math, Operator},
engine::Closure,
ByteStreamType, CustomValue, IntoInterruptiblePipelineData, IntoSpanned, PipelineData,
PluginSignature, ShellError, Span, Spanned, Value,
PluginMetadata, PluginSignature, ShellError, Span, Spanned, Value,
};
use serde::{Deserialize, Serialize};
use std::{
@ -1019,6 +1019,25 @@ fn start_fake_plugin_call_responder(
.expect("failed to spawn thread");
}
#[test]
fn interface_get_metadata() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.plugin("test");
let interface = manager.get_interface();
start_fake_plugin_call_responder(manager, 1, |_| {
vec![ReceivedPluginCallMessage::Response(
PluginCallResponse::Metadata(PluginMetadata::new().with_version("test")),
)]
});
let metadata = interface.get_metadata()?;
assert_eq!(Some("test"), metadata.version.as_deref());
assert!(test.has_unconsumed_write());
Ok(())
}
#[test]
fn interface_get_signature() -> Result<(), ShellError> {
let test = TestCase::new();

View File

@ -7,7 +7,7 @@ use super::{PluginInterface, PluginSource};
use nu_plugin_core::CommunicationMode;
use nu_protocol::{
engine::{ctrlc, EngineState, Stack},
PluginGcConfig, PluginIdentity, RegisteredPlugin, ShellError,
PluginGcConfig, PluginIdentity, PluginMetadata, RegisteredPlugin, ShellError,
};
use std::{
collections::HashMap,
@ -31,6 +31,8 @@ pub struct PersistentPlugin {
struct MutableState {
/// Reference to the plugin if running
running: Option<RunningPlugin>,
/// Metadata for the plugin, e.g. version.
metadata: Option<PluginMetadata>,
/// Plugin's preferred communication mode (if known)
preferred_mode: Option<PreferredCommunicationMode>,
/// Garbage collector config
@ -61,6 +63,7 @@ impl PersistentPlugin {
identity,
mutable: Mutex::new(MutableState {
running: None,
metadata: None,
preferred_mode: None,
gc_config,
}),
@ -221,7 +224,11 @@ impl PersistentPlugin {
}))
});
mutable.running = Some(RunningPlugin { interface, gc, _ctrlc_guard: guard});
mutable.running = Some(RunningPlugin {
interface,
gc,
_ctrlc_guard: guard,
});
Ok(())
}
@ -281,6 +288,16 @@ impl RegisteredPlugin for PersistentPlugin {
self.stop_internal(true)
}
fn metadata(&self) -> Option<PluginMetadata> {
self.mutable.lock().ok().and_then(|m| m.metadata.clone())
}
fn set_metadata(&self, metadata: Option<PluginMetadata>) {
if let Ok(mut mutable) = self.mutable.lock() {
mutable.metadata = metadata;
}
}
fn set_gc_config(&self, gc_config: &PluginGcConfig) {
if let Ok(mut mutable) = self.mutable.lock() {
// Save the new config for future calls

View File

@ -23,7 +23,7 @@ pub mod test_util;
use nu_protocol::{
ast::Operator, engine::Closure, ByteStreamType, Config, LabeledError, PipelineData,
PluginSignature, ShellError, Span, Spanned, Value,
PluginMetadata, PluginSignature, ShellError, Span, Spanned, Value,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@ -119,6 +119,7 @@ pub struct ByteStreamInfo {
/// Calls that a plugin can execute. The type parameter determines the input type.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum PluginCall<D> {
Metadata,
Signature,
Run(CallInfo<D>),
CustomValueOp(Spanned<PluginCustomValue>, CustomValueOp),
@ -132,6 +133,7 @@ impl<D> PluginCall<D> {
f: impl FnOnce(D) -> Result<T, ShellError>,
) -> Result<PluginCall<T>, ShellError> {
Ok(match self {
PluginCall::Metadata => PluginCall::Metadata,
PluginCall::Signature => PluginCall::Signature,
PluginCall::Run(call) => PluginCall::Run(call.map_data(f)?),
PluginCall::CustomValueOp(custom_value, op) => {
@ -143,6 +145,7 @@ impl<D> PluginCall<D> {
/// The span associated with the call.
pub fn span(&self) -> Option<Span> {
match self {
PluginCall::Metadata => None,
PluginCall::Signature => None,
PluginCall::Run(CallInfo { call, .. }) => Some(call.head),
PluginCall::CustomValueOp(val, _) => Some(val.span),
@ -311,6 +314,7 @@ pub enum StreamMessage {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum PluginCallResponse<D> {
Error(LabeledError),
Metadata(PluginMetadata),
Signature(Vec<PluginSignature>),
Ordering(Option<Ordering>),
PipelineData(D),
@ -325,6 +329,7 @@ impl<D> PluginCallResponse<D> {
) -> Result<PluginCallResponse<T>, ShellError> {
Ok(match self {
PluginCallResponse::Error(err) => PluginCallResponse::Error(err),
PluginCallResponse::Metadata(meta) => PluginCallResponse::Metadata(meta),
PluginCallResponse::Signature(sigs) => PluginCallResponse::Signature(sigs),
PluginCallResponse::Ordering(ordering) => PluginCallResponse::Ordering(ordering),
PluginCallResponse::PipelineData(input) => PluginCallResponse::PipelineData(f(input)?),

View File

@ -6,7 +6,7 @@ use std::{
use nu_plugin_engine::{GetPlugin, PluginInterface};
use nu_protocol::{
engine::{EngineState, Stack},
PluginGcConfig, PluginIdentity, RegisteredPlugin, ShellError,
PluginGcConfig, PluginIdentity, PluginMetadata, RegisteredPlugin, ShellError,
};
pub struct FakePersistentPlugin {
@ -42,6 +42,12 @@ impl RegisteredPlugin for FakePersistentPlugin {
None
}
fn metadata(&self) -> Option<PluginMetadata> {
None
}
fn set_metadata(&self, _metadata: Option<PluginMetadata>) {}
fn set_gc_config(&self, _gc_config: &PluginGcConfig) {
// We don't have a GC
}

View File

@ -66,6 +66,10 @@
//! }
//!
//! impl Plugin for LowercasePlugin {
//! fn version(&self) -> String {
//! env!("CARGO_PKG_VERSION").into()
//! }
//!
//! fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
//! vec![Box::new(Lowercase)]
//! }

View File

@ -53,6 +53,10 @@ struct IntoU32;
struct IntoIntFromU32;
impl Plugin for CustomU32Plugin {
fn version(&self) -> String {
"0.0.0".into()
}
fn commands(&self) -> Vec<Box<dyn nu_plugin::PluginCommand<Plugin = Self>>> {
vec![Box::new(IntoU32), Box::new(IntoIntFromU32)]
}

View File

@ -8,6 +8,10 @@ struct HelloPlugin;
struct Hello;
impl Plugin for HelloPlugin {
fn version(&self) -> String {
"0.0.0".into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
vec![Box::new(Hello)]
}

View File

@ -59,6 +59,10 @@ impl PluginCommand for Lowercase {
}
impl Plugin for LowercasePlugin {
fn version(&self) -> String {
"0.0.0".into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
vec![Box::new(Lowercase)]
}

View File

@ -24,6 +24,10 @@
//! struct MyCommand;
//!
//! impl Plugin for MyPlugin {
//! fn version(&self) -> String {
//! env!("CARGO_PKG_VERSION").into()
//! }
//!
//! fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
//! vec![Box::new(MyCommand)]
//! }

View File

@ -60,6 +60,9 @@ use crate::{EngineInterface, EvaluatedCall, Plugin};
/// }
///
/// # impl Plugin for LowercasePlugin {
/// # fn version(&self) -> String {
/// # "0.0.0".into()
/// # }
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
/// # vec![Box::new(Lowercase)]
/// # }
@ -195,6 +198,9 @@ pub trait PluginCommand: Sync {
/// }
///
/// # impl Plugin for HelloPlugin {
/// # fn version(&self) -> String {
/// # "0.0.0".into()
/// # }
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
/// # vec![Box::new(Hello)]
/// # }

View File

@ -11,8 +11,8 @@ use nu_plugin_protocol::{
ProtocolInfo,
};
use nu_protocol::{
engine::{ctrlc, Closure}, Config, LabeledError, PipelineData, PluginSignature, ShellError, Span,
Spanned, Value,
engine::{ctrlc, Closure}, Config, LabeledError, PipelineData, PluginMetadata, PluginSignature,
ShellError, Span, Spanned, Value,
};
use std::{
collections::{btree_map, BTreeMap, HashMap},
@ -29,6 +29,9 @@ use std::{
#[derive(Debug)]
#[doc(hidden)]
pub enum ReceivedPluginCall {
Metadata {
engine: EngineInterface,
},
Signature {
engine: EngineInterface,
},
@ -283,8 +286,11 @@ impl InterfaceManager for EngineInterfaceManager {
}
};
match call {
// We just let the receiver handle it rather than trying to store signature here
// or something
// Ask the plugin for metadata
PluginCall::Metadata => {
self.send_plugin_call(ReceivedPluginCall::Metadata { engine: interface })
}
// Ask the plugin for signatures
PluginCall::Signature => {
self.send_plugin_call(ReceivedPluginCall::Signature { engine: interface })
}
@ -423,6 +429,13 @@ impl EngineInterface {
}
}
/// Write a call response of plugin metadata.
pub(crate) fn write_metadata(&self, metadata: PluginMetadata) -> Result<(), ShellError> {
let response = PluginCallResponse::Metadata(metadata);
self.write(PluginOutput::CallResponse(self.context()?, response))?;
self.flush()
}
/// Write a call response of plugin signatures.
///
/// Any custom values in the examples will be rendered using `to_base_value()`.

View File

@ -322,6 +322,26 @@ fn manager_consume_goodbye_closes_plugin_call_channel() -> Result<(), ShellError
Ok(())
}
#[test]
fn manager_consume_call_metadata_forwards_to_receiver_with_context() -> Result<(), ShellError> {
let mut manager = TestCase::new().engine();
set_default_protocol_info(&mut manager)?;
let rx = manager
.take_plugin_call_receiver()
.expect("couldn't take receiver");
manager.consume(PluginInput::Call(0, PluginCall::Metadata))?;
match rx.try_recv().expect("call was not forwarded to receiver") {
ReceivedPluginCall::Metadata { engine } => {
assert_eq!(Some(0), engine.context);
Ok(())
}
call => panic!("wrong call type: {call:?}"),
}
}
#[test]
fn manager_consume_call_signature_forwards_to_receiver_with_context() -> Result<(), ShellError> {
let mut manager = TestCase::new().engine();

View File

@ -16,7 +16,8 @@ use nu_plugin_core::{
};
use nu_plugin_protocol::{CallInfo, CustomValueOp, PluginCustomValue, PluginInput, PluginOutput};
use nu_protocol::{
ast::Operator, CustomValue, IntoSpanned, LabeledError, PipelineData, ShellError, Spanned, Value,
ast::Operator, CustomValue, IntoSpanned, LabeledError, PipelineData, PluginMetadata,
ShellError, Spanned, Value,
};
use thiserror::Error;
@ -52,6 +53,10 @@ pub(crate) const OUTPUT_BUFFER_SIZE: usize = 16384;
/// struct Hello;
///
/// impl Plugin for HelloPlugin {
/// fn version(&self) -> String {
/// env!("CARGO_PKG_VERSION").into()
/// }
///
/// fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
/// vec![Box::new(Hello)]
/// }
@ -89,6 +94,23 @@ pub(crate) const OUTPUT_BUFFER_SIZE: usize = 16384;
/// # }
/// ```
pub trait Plugin: Sync {
/// The version of the plugin.
///
/// The recommended implementation, which will use the version from your crate's `Cargo.toml`
/// file:
///
/// ```no_run
/// # use nu_plugin::{Plugin, PluginCommand};
/// # struct MyPlugin;
/// # impl Plugin for MyPlugin {
/// fn version(&self) -> String {
/// env!("CARGO_PKG_VERSION").into()
/// }
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> { vec![] }
/// # }
/// ```
fn version(&self) -> String;
/// The commands supported by the plugin
///
/// Each [`PluginCommand`] contains both the signature of the command and the functionality it
@ -216,6 +238,7 @@ pub trait Plugin: Sync {
/// # struct MyPlugin;
/// # impl MyPlugin { fn new() -> Self { Self }}
/// # impl Plugin for MyPlugin {
/// # fn version(&self) -> String { "0.0.0".into() }
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {todo!();}
/// # }
/// fn main() {
@ -504,6 +527,12 @@ where
}
match plugin_call {
// Send metadata back to nushell so it can be stored with the plugin signatures
ReceivedPluginCall::Metadata { engine } => {
engine
.write_metadata(PluginMetadata::new().with_version(plugin.version()))
.try_to_report(&engine)?;
}
// Sending the signature back to nushell to create the declaration definition
ReceivedPluginCall::Signature { engine } => {
let sigs = commands

View File

@ -32,8 +32,11 @@ pub enum Expr {
Keyword(Box<Keyword>),
ValueWithUnit(Box<ValueWithUnit>),
DateTime(chrono::DateTime<FixedOffset>),
/// The boolean is `true` if the string is quoted.
Filepath(String, bool),
/// The boolean is `true` if the string is quoted.
Directory(String, bool),
/// The boolean is `true` if the string is quoted.
GlobPattern(String, bool),
String(String),
RawString(String),
@ -43,6 +46,8 @@ pub enum Expr {
Overlay(Option<BlockId>), // block ID of the overlay's origin module
Signature(Box<Signature>),
StringInterpolation(Vec<Expression>),
/// The boolean is `true` if the string is quoted.
GlobInterpolation(Vec<Expression>, bool),
Nothing,
Garbage,
}
@ -84,6 +89,7 @@ impl Expr {
| Expr::RawString(_)
| Expr::CellPath(_)
| Expr::StringInterpolation(_)
| Expr::GlobInterpolation(_, _)
| Expr::Nothing => {
// These expressions do not use the output of the pipeline in any meaningful way,
// so we can discard the previous output by redirecting it to `Null`.

View File

@ -232,7 +232,7 @@ impl Expression {
}
false
}
Expr::StringInterpolation(items) => {
Expr::StringInterpolation(items) | Expr::GlobInterpolation(items, _) => {
for i in items {
if i.has_in_variable(working_set) {
return true;
@ -441,7 +441,7 @@ impl Expression {
Expr::Signature(_) => {}
Expr::String(_) => {}
Expr::RawString(_) => {}
Expr::StringInterpolation(items) => {
Expr::StringInterpolation(items) | Expr::GlobInterpolation(items, _) => {
for i in items {
i.replace_span(working_set, replaced, new_span)
}

View File

@ -258,6 +258,7 @@ fn expr_to_string(engine_state: &EngineState, expr: &Expr) -> String {
Expr::Signature(_) => "signature".to_string(),
Expr::String(_) | Expr::RawString(_) => "string".to_string(),
Expr::StringInterpolation(_) => "string interpolation".to_string(),
Expr::GlobInterpolation(_, _) => "glob interpolation".to_string(),
Expr::Subexpression(_) => "subexpression".to_string(),
Expr::Table(_) => "table".to_string(),
Expr::UnaryNot(_) => "unary not".to_string(),

View File

@ -290,6 +290,15 @@ pub trait Eval {
Ok(Value::string(str, expr_span))
}
Expr::GlobInterpolation(exprs, quoted) => {
let config = Self::get_config(state, mut_state);
let str = exprs
.iter()
.map(|expr| Self::eval::<D>(state, mut_state, expr).map(|v| v.to_expanded_string(", ", &config)))
.collect::<Result<String, _>>()?;
Ok(Value::glob(str, *quoted, expr_span))
}
Expr::Overlay(_) => Self::eval_overlay(state, expr_span),
Expr::GlobPattern(pattern, quoted) => {
// GlobPattern is similar to Filepath

View File

@ -181,6 +181,46 @@ pub(crate) fn create_nu_constant(engine_state: &EngineState, span: Span) -> Valu
},
);
// Create a system level directory for nushell scripts, modules, completions, etc
// that can be changed by setting the NU_VENDOR_AUTOLOAD_DIR env var on any platform
// before nushell is compiled OR if NU_VENDOR_AUTOLOAD_DIR is not set for non-windows
// systems, the PREFIX env var can be set before compile and used as PREFIX/nushell/vendor/autoload
record.push(
"vendor-autoload-dir",
// pseudo code
// if env var NU_VENDOR_AUTOLOAD_DIR is set, in any platform, use it
// if not, if windows, use ALLUSERPROFILE\nushell\vendor\autoload
// if not, if non-windows, if env var PREFIX is set, use PREFIX/share/nushell/vendor/autoload
// if not, use the default /usr/share/nushell/vendor/autoload
// check to see if NU_VENDOR_AUTOLOAD_DIR env var is set, if not, use the default
Value::string(
option_env!("NU_VENDOR_AUTOLOAD_DIR")
.map(String::from)
.unwrap_or_else(|| {
if cfg!(windows) {
let all_user_profile = match engine_state.get_env_var("ALLUSERPROFILE") {
Some(v) => format!(
"{}\\nushell\\vendor\\autoload",
v.coerce_string().unwrap_or("C:\\ProgramData".into())
),
None => "C:\\ProgramData\\nushell\\vendor\\autoload".into(),
};
all_user_profile
} else {
// In non-Windows environments, if NU_VENDOR_AUTOLOAD_DIR is not set
// check to see if PREFIX env var is set, and use it as PREFIX/nushell/vendor/autoload
// otherwise default to /usr/share/nushell/vendor/autoload
option_env!("PREFIX").map(String::from).map_or_else(
|| "/usr/local/share/nushell/vendor/autoload".into(),
|prefix| format!("{}/share/nushell/vendor/autoload", prefix),
)
}
}),
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)

View File

@ -0,0 +1,38 @@
use serde::{Deserialize, Serialize};
/// Metadata about the installed plugin. This is cached in the registry file along with the
/// signatures. None of the metadata fields are required, and more may be added in the future.
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[non_exhaustive]
pub struct PluginMetadata {
/// The version of the plugin itself, as self-reported.
pub version: Option<String>,
}
impl PluginMetadata {
/// Create empty metadata.
pub const fn new() -> PluginMetadata {
PluginMetadata { version: None }
}
/// Set the version of the plugin on the metadata. A suggested way to construct this is:
///
/// ```no_run
/// # use nu_protocol::PluginMetadata;
/// # fn example() -> PluginMetadata {
/// PluginMetadata::new().with_version(env!("CARGO_PKG_VERSION"))
/// # }
/// ```
///
/// which will use the version of your plugin's crate from its `Cargo.toml` file.
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
}
impl Default for PluginMetadata {
fn default() -> Self {
Self::new()
}
}

View File

@ -1,9 +1,11 @@
mod identity;
mod metadata;
mod registered;
mod registry_file;
mod signature;
pub use identity::*;
pub use metadata::*;
pub use registered::*;
pub use registry_file::*;
pub use signature::*;

View File

@ -1,6 +1,6 @@
use std::{any::Any, sync::Arc};
use crate::{PluginGcConfig, PluginIdentity, ShellError};
use crate::{PluginGcConfig, PluginIdentity, PluginMetadata, ShellError};
/// Trait for plugins registered in the [`EngineState`](crate::engine::EngineState).
pub trait RegisteredPlugin: Send + Sync {
@ -13,6 +13,12 @@ pub trait RegisteredPlugin: Send + Sync {
/// Process ID of the plugin executable, if running.
fn pid(&self) -> Option<u32>;
/// Get metadata for the plugin, if set.
fn metadata(&self) -> Option<PluginMetadata>;
/// Set metadata for the plugin.
fn set_metadata(&self, metadata: Option<PluginMetadata>);
/// Set garbage collection config for the plugin.
fn set_gc_config(&self, gc_config: &PluginGcConfig);

View File

@ -5,7 +5,7 @@ use std::{
use serde::{Deserialize, Serialize};
use crate::{PluginIdentity, PluginSignature, ShellError, Span};
use crate::{PluginIdentity, PluginMetadata, PluginSignature, ShellError, Span};
// This has a big impact on performance
const BUFFER_SIZE: usize = 65536;
@ -121,9 +121,10 @@ pub struct PluginRegistryItem {
}
impl PluginRegistryItem {
/// Create a [`PluginRegistryItem`] from an identity and signatures.
/// Create a [`PluginRegistryItem`] from an identity, metadata, and signatures.
pub fn new(
identity: &PluginIdentity,
metadata: PluginMetadata,
mut commands: Vec<PluginSignature>,
) -> PluginRegistryItem {
// Sort the commands for consistency
@ -133,7 +134,7 @@ impl PluginRegistryItem {
name: identity.name().to_owned(),
filename: identity.filename().to_owned(),
shell: identity.shell().map(|p| p.to_owned()),
data: PluginRegistryItemData::Valid { commands },
data: PluginRegistryItemData::Valid { metadata, commands },
}
}
}
@ -144,6 +145,9 @@ impl PluginRegistryItem {
#[serde(untagged)]
pub enum PluginRegistryItemData {
Valid {
/// Metadata for the plugin, including its version.
#[serde(default)]
metadata: PluginMetadata,
/// Signatures and examples for each command provided by the plugin.
commands: Vec<PluginSignature>,
},

View File

@ -1,6 +1,7 @@
use super::{PluginRegistryFile, PluginRegistryItem, PluginRegistryItemData};
use crate::{
Category, PluginExample, PluginSignature, ShellError, Signature, SyntaxShape, Type, Value,
Category, PluginExample, PluginMetadata, PluginSignature, ShellError, Signature, SyntaxShape,
Type, Value,
};
use pretty_assertions::assert_eq;
use std::io::Cursor;
@ -11,6 +12,9 @@ fn foo_plugin() -> PluginRegistryItem {
filename: "/path/to/nu_plugin_foo".into(),
shell: None,
data: PluginRegistryItemData::Valid {
metadata: PluginMetadata {
version: Some("0.1.0".into()),
},
commands: vec![PluginSignature {
sig: Signature::new("foo")
.input_output_type(Type::Int, Type::List(Box::new(Type::Int)))
@ -36,6 +40,9 @@ fn bar_plugin() -> PluginRegistryItem {
filename: "/path/to/nu_plugin_bar".into(),
shell: None,
data: PluginRegistryItemData::Valid {
metadata: PluginMetadata {
version: Some("0.2.0".into()),
},
commands: vec![PluginSignature {
sig: Signature::new("bar")
.usage("overwrites files with random data")

View File

@ -171,6 +171,62 @@ fn named_fields_struct_incorrect_type() {
assert!(res.is_err());
}
#[derive(IntoValue, FromValue, Debug, PartialEq, Default)]
struct ALotOfOptions {
required: bool,
float: Option<f64>,
int: Option<i64>,
value: Option<Value>,
nested: Option<Nestee>,
}
#[test]
fn missing_options() {
let value = Value::test_record(Record::new());
let res: Result<ALotOfOptions, _> = ALotOfOptions::from_value(value);
assert!(res.is_err());
let value = Value::test_record(record! {"required" => Value::test_bool(true)});
let expected = ALotOfOptions {
required: true,
..Default::default()
};
let actual = ALotOfOptions::from_value(value).unwrap();
assert_eq!(expected, actual);
let value = Value::test_record(record! {
"required" => Value::test_bool(true),
"float" => Value::test_float(std::f64::consts::PI),
});
let expected = ALotOfOptions {
required: true,
float: Some(std::f64::consts::PI),
..Default::default()
};
let actual = ALotOfOptions::from_value(value).unwrap();
assert_eq!(expected, actual);
let value = Value::test_record(record! {
"required" => Value::test_bool(true),
"int" => Value::test_int(12),
"nested" => Value::test_record(record! {
"u32" => Value::test_int(34),
}),
});
let expected = ALotOfOptions {
required: true,
int: Some(12),
nested: Some(Nestee {
u32: 34,
some: None,
none: None,
}),
..Default::default()
};
let actual = ALotOfOptions::from_value(value).unwrap();
assert_eq!(expected, actual);
}
#[derive(IntoValue, FromValue, Debug, PartialEq)]
struct UnnamedFieldsStruct<T>(u32, String, T)
where

View File

@ -1,6 +1,7 @@
use itertools::{EitherOrBoth, Itertools};
use libc::{
kinfo_proc, sysctl, CTL_HW, CTL_KERN, KERN_PROC, KERN_PROC_ALL, KERN_PROC_ARGS, TDF_IDLETD,
c_char, kinfo_proc, sysctl, CTL_HW, CTL_KERN, KERN_PROC, KERN_PROC_ALL, KERN_PROC_ARGS,
TDF_IDLETD,
};
use std::{
ffi::CStr,
@ -16,7 +17,7 @@ pub struct ProcessInfo {
pub ppid: i32,
pub name: String,
pub argv: Vec<u8>,
pub stat: i8,
pub stat: c_char,
pub percent_cpu: f64,
pub mem_resident: u64, // in bytes
pub mem_virtual: u64, // in bytes

View File

@ -245,6 +245,7 @@ use tempfile::tempdir;
pub struct NuOpts {
pub cwd: Option<String>,
pub locale: Option<String>,
pub envs: Option<Vec<(String, String)>>,
pub collapse_output: Option<bool>,
}
@ -278,6 +279,11 @@ pub fn nu_run_test(opts: NuOpts, commands: impl AsRef<str>, with_std: bool) -> O
command
.env(nu_utils::locale::LOCALE_OVERRIDE_ENV_VAR, locale)
.env(NATIVE_PATH_ENV_VAR, paths_joined);
if let Some(envs) = opts.envs {
command.envs(envs);
}
// Ensure that the user's config doesn't interfere with the tests
command.arg("--no-config-file");
if !with_std {

View File

@ -48,6 +48,7 @@ let dark_theme = {
shape_float: purple_bold
# shapes are used to change the cli syntax highlighting
shape_garbage: { fg: white bg: red attr: b}
shape_glob_interpolation: cyan_bold
shape_globpattern: cyan_bold
shape_int: purple_bold
shape_internalcall: cyan_bold

View File

@ -42,6 +42,10 @@ impl CustomValuePlugin {
}
impl Plugin for CustomValuePlugin {
fn version(&self) -> String {
env!("CARGO_PKG_VERSION").into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
vec![
Box::new(Generate),

View File

@ -7,6 +7,10 @@ pub use commands::*;
pub use example::ExamplePlugin;
impl Plugin for ExamplePlugin {
fn version(&self) -> String {
env!("CARGO_PKG_VERSION").into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
// This is a list of all of the commands you would like Nu to register when your plugin is
// loaded.

View File

@ -10,6 +10,10 @@ pub use from::vcf::FromVcf;
pub struct FromCmds;
impl Plugin for FromCmds {
fn version(&self) -> String {
env!("CARGO_PKG_VERSION").into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
vec![
Box::new(FromEml),

View File

@ -5,6 +5,10 @@ use nu_protocol::{Category, LabeledError, Signature, Spanned, SyntaxShape, Value
pub struct GStatPlugin;
impl Plugin for GStatPlugin {
fn version(&self) -> String {
env!("CARGO_PKG_VERSION").into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
vec![Box::new(GStat)]
}

View File

@ -5,6 +5,10 @@ use nu_protocol::{ast::CellPath, LabeledError, Signature, SyntaxShape, Value};
pub struct IncPlugin;
impl Plugin for IncPlugin {
fn version(&self) -> String {
env!("CARGO_PKG_VERSION").into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
vec![Box::new(Inc::new())]
}

View File

@ -7,6 +7,7 @@
# language without adding any extra dependencies to our tests.
const NUSHELL_VERSION = "0.94.3"
const PLUGIN_VERSION = "0.1.0" # bump if you change commands!
def main [--stdio] {
if ($stdio) {
@ -229,6 +230,13 @@ def handle_input []: any -> nothing {
}
{ Call: [$id, $plugin_call] } => {
match $plugin_call {
"Metadata" => {
write_response $id {
Metadata: {
version: $PLUGIN_VERSION
}
}
}
"Signature" => {
write_response $id { Signature: $SIGNATURES }
}

View File

@ -13,7 +13,7 @@ use nu_plugin::{EngineInterface, PluginCommand};
use nu_protocol::{LabeledError, ShellError, Span};
use uuid::Uuid;
use crate::{plugin_debug, values::PolarsPluginObject, PolarsPlugin};
use crate::{plugin_debug, values::PolarsPluginObject, EngineWrapper, PolarsPlugin};
#[derive(Debug, Clone)]
pub struct CacheValue {
@ -47,7 +47,7 @@ impl Cache {
/// * `force` - Delete even if there are multiple references
pub fn remove(
&self,
maybe_engine: Option<&EngineInterface>,
engine: impl EngineWrapper,
key: &Uuid,
force: bool,
) -> Result<Option<CacheValue>, ShellError> {
@ -60,22 +60,23 @@ impl Cache {
let removed = if force || reference_count.unwrap_or_default() < 1 {
let removed = lock.remove(key);
plugin_debug!("PolarsPlugin: removing {key} from cache: {removed:?}");
plugin_debug!(
engine,
"PolarsPlugin: removing {key} from cache: {removed:?}"
);
removed
} else {
plugin_debug!("PolarsPlugin: decrementing reference count for {key}");
plugin_debug!(
engine,
"PolarsPlugin: decrementing reference count for {key}"
);
None
};
// Once there are no more entries in the cache
// we can turn plugin gc back on
match maybe_engine {
Some(engine) if lock.is_empty() => {
plugin_debug!("PolarsPlugin: Cache is empty enabling GC");
engine.set_gc_disabled(false).map_err(LabeledError::from)?;
}
_ => (),
};
plugin_debug!(engine, "PolarsPlugin: Cache is empty enabling GC");
engine.set_gc_disabled(false).map_err(LabeledError::from)?;
drop(lock);
Ok(removed)
}
@ -84,23 +85,21 @@ impl Cache {
/// The maybe_engine parameter is required outside of testing
pub fn insert(
&self,
maybe_engine: Option<&EngineInterface>,
engine: impl EngineWrapper,
uuid: Uuid,
value: PolarsPluginObject,
span: Span,
) -> Result<Option<CacheValue>, ShellError> {
let mut lock = self.lock()?;
plugin_debug!("PolarsPlugin: Inserting {uuid} into cache: {value:?}");
plugin_debug!(
engine,
"PolarsPlugin: Inserting {uuid} into cache: {value:?}"
);
// turn off plugin gc the first time an entry is added to the cache
// as we don't want the plugin to be garbage collected if there
// is any live data
match maybe_engine {
Some(engine) if lock.is_empty() => {
plugin_debug!("PolarsPlugin: Cache has values disabling GC");
engine.set_gc_disabled(true).map_err(LabeledError::from)?;
}
_ => (),
};
plugin_debug!(engine, "PolarsPlugin: Cache has values disabling GC");
engine.set_gc_disabled(true).map_err(LabeledError::from)?;
let cache_value = CacheValue {
uuid,
value,
@ -154,7 +153,7 @@ pub trait Cacheable: Sized + Clone {
span: Span,
) -> Result<Self, ShellError> {
plugin.cache.insert(
Some(engine),
engine,
self.cache_id().to_owned(),
self.to_cache_value()?,
span,

View File

@ -63,7 +63,7 @@ fn remove_cache_entry(
let key = as_uuid(key, span)?;
let msg = plugin
.cache
.remove(Some(engine), &key, true)?
.remove(engine, &key, true)?
.map(|_| format!("Removed: {key}"))
.unwrap_or_else(|| format!("No value found for key: {key}"));
Ok(Value::string(msg, span))

View File

@ -1,5 +1,6 @@
use crate::{
dataframe::values::NuSchema,
perf,
values::{CustomValueSupport, NuLazyFrame},
PolarsPlugin,
};
@ -19,15 +20,20 @@ use std::{
sync::Arc,
};
use polars::prelude::{
CsvEncoding, IpcReader, JsonFormat, JsonReader, LazyCsvReader, LazyFileListReader, LazyFrame,
ParquetReader, ScanArgsIpc, ScanArgsParquet, SerReader,
use polars::{
lazy::frame::LazyJsonLineReader,
prelude::{
CsvEncoding, IpcReader, JsonFormat, JsonReader, LazyCsvReader, LazyFileListReader,
LazyFrame, ParquetReader, ScanArgsIpc, ScanArgsParquet, SerReader,
},
};
use polars_io::{
avro::AvroReader, csv::read::CsvReadOptions, prelude::ParallelStrategy, HiveOptions,
};
const DEFAULT_INFER_SCHEMA: usize = 100;
#[derive(Clone)]
pub struct OpenDataFrame;
@ -370,41 +376,82 @@ fn from_jsonl(
file_path: &Path,
file_span: Span,
) -> Result<Value, ShellError> {
let infer_schema: Option<usize> = call.get_flag("infer-schema")?;
let infer_schema: usize = call
.get_flag("infer-schema")?
.unwrap_or(DEFAULT_INFER_SCHEMA);
let maybe_schema = call
.get_flag("schema")?
.map(|schema| NuSchema::try_from(&schema))
.transpose()?;
let file = File::open(file_path).map_err(|e| ShellError::GenericError {
error: "Error opening file".into(),
msg: e.to_string(),
span: Some(file_span),
help: None,
inner: vec![],
})?;
let buf_reader = BufReader::new(file);
let reader = JsonReader::new(buf_reader)
.with_json_format(JsonFormat::JsonLines)
.infer_schema_len(infer_schema);
if call.has_flag("lazy")? {
let start_time = std::time::Instant::now();
let reader = match maybe_schema {
Some(schema) => reader.with_schema(schema.into()),
None => reader,
};
let df = LazyJsonLineReader::new(file_path)
.with_infer_schema_length(Some(infer_schema))
.with_schema(maybe_schema.map(|s| s.into()))
.finish()
.map_err(|e| ShellError::GenericError {
error: format!("Json lines reader error: {e}"),
msg: "".into(),
span: Some(call.head),
help: None,
inner: vec![],
})?;
let df: NuDataFrame = reader
.finish()
.map_err(|e| ShellError::GenericError {
error: "Json lines reader error".into(),
msg: format!("{e:?}"),
span: Some(call.head),
perf(
engine,
"Lazy json lines dataframe open",
start_time,
file!(),
line!(),
column!(),
);
let df = NuLazyFrame::new(false, df);
df.cache_and_to_value(plugin, engine, call.head)
} else {
let file = File::open(file_path).map_err(|e| ShellError::GenericError {
error: "Error opening file".into(),
msg: e.to_string(),
span: Some(file_span),
help: None,
inner: vec![],
})?
.into();
})?;
let buf_reader = BufReader::new(file);
let reader = JsonReader::new(buf_reader)
.with_json_format(JsonFormat::JsonLines)
.infer_schema_len(Some(infer_schema));
df.cache_and_to_value(plugin, engine, call.head)
let reader = match maybe_schema {
Some(schema) => reader.with_schema(schema.into()),
None => reader,
};
let start_time = std::time::Instant::now();
let df: NuDataFrame = reader
.finish()
.map_err(|e| ShellError::GenericError {
error: "Json lines reader error".into(),
msg: format!("{e:?}"),
span: Some(call.head),
help: None,
inner: vec![],
})?
.into();
perf(
engine,
"Eager json lines dataframe open",
start_time,
file!(),
line!(),
column!(),
);
df.cache_and_to_value(plugin, engine, call.head)
}
}
fn from_csv(
@ -416,7 +463,9 @@ fn from_csv(
) -> Result<Value, ShellError> {
let delimiter: Option<Spanned<String>> = call.get_flag("delimiter")?;
let no_header: bool = call.has_flag("no-header")?;
let infer_schema: Option<usize> = call.get_flag("infer-schema")?;
let infer_schema: usize = call
.get_flag("infer-schema")?
.unwrap_or(DEFAULT_INFER_SCHEMA);
let skip_rows: Option<usize> = call.get_flag("skip-rows")?;
let columns: Option<Vec<String>> = call.get_flag("columns")?;
@ -456,16 +505,14 @@ fn from_csv(
None => csv_reader,
};
let csv_reader = match infer_schema {
None => csv_reader,
Some(r) => csv_reader.with_infer_schema_length(Some(r)),
};
let csv_reader = csv_reader.with_infer_schema_length(Some(infer_schema));
let csv_reader = match skip_rows {
None => csv_reader,
Some(r) => csv_reader.with_skip_rows(r),
};
let start_time = std::time::Instant::now();
let df: NuLazyFrame = csv_reader
.finish()
.map_err(|e| ShellError::GenericError {
@ -477,11 +524,21 @@ fn from_csv(
})?
.into();
perf(
engine,
"Lazy CSV dataframe open",
start_time,
file!(),
line!(),
column!(),
);
df.cache_and_to_value(plugin, engine, call.head)
} else {
let start_time = std::time::Instant::now();
let df = CsvReadOptions::default()
.with_has_header(!no_header)
.with_infer_schema_length(infer_schema)
.with_infer_schema_length(Some(infer_schema))
.with_skip_rows(skip_rows.unwrap_or_default())
.with_schema(maybe_schema.map(|s| s.into()))
.with_columns(columns.map(Arc::new))
@ -511,6 +568,16 @@ fn from_csv(
help: None,
inner: vec![],
})?;
perf(
engine,
"Eager CSV dataframe open",
start_time,
file!(),
line!(),
column!(),
);
let df = NuDataFrame::new(false, df);
df.cache_and_to_value(plugin, engine, call.head)
}

View File

@ -8,25 +8,89 @@ use nu_plugin::{EngineInterface, Plugin, PluginCommand};
mod cache;
pub mod dataframe;
pub use dataframe::*;
use nu_protocol::{ast::Operator, CustomValue, LabeledError, Spanned, Value};
use nu_protocol::{ast::Operator, CustomValue, LabeledError, ShellError, Span, Spanned, Value};
use crate::{
eager::eager_commands, expressions::expr_commands, lazy::lazy_commands,
series::series_commands, values::PolarsPluginCustomValue,
};
pub trait EngineWrapper {
fn get_env_var(&self, key: &str) -> Option<String>;
fn use_color(&self) -> bool;
fn set_gc_disabled(&self, disabled: bool) -> Result<(), ShellError>;
}
impl EngineWrapper for &EngineInterface {
fn get_env_var(&self, key: &str) -> Option<String> {
EngineInterface::get_env_var(self, key)
.ok()
.flatten()
.map(|x| match x {
Value::String { val, .. } => val,
_ => "".to_string(),
})
}
fn use_color(&self) -> bool {
self.get_config()
.ok()
.and_then(|config| config.color_config.get("use_color").cloned())
.unwrap_or(Value::bool(false, Span::unknown()))
.is_true()
}
fn set_gc_disabled(&self, disabled: bool) -> Result<(), ShellError> {
EngineInterface::set_gc_disabled(self, disabled)
}
}
#[macro_export]
macro_rules! plugin_debug {
($($arg:tt)*) => {{
if std::env::var("POLARS_PLUGIN_DEBUG")
.ok()
.filter(|x| x == "1" || x == "true")
($env_var_provider:tt, $($arg:tt)*) => {{
if $env_var_provider.get_env_var("POLARS_PLUGIN_DEBUG")
.filter(|s| s == "1" || s == "true")
.is_some() {
eprintln!($($arg)*);
}
}};
}
pub fn perf(
env: impl EngineWrapper,
msg: &str,
dur: std::time::Instant,
file: &str,
line: u32,
column: u32,
) {
if env
.get_env_var("POLARS_PLUGIN_PERF")
.filter(|s| s == "1" || s == "true")
.is_some()
{
if env.use_color() {
eprintln!(
"perf: {}:{}:{} \x1b[32m{}\x1b[0m took \x1b[33m{:?}\x1b[0m",
file,
line,
column,
msg,
dur.elapsed(),
);
} else {
eprintln!(
"perf: {}:{}:{} {} took {:?}",
file,
line,
column,
msg,
dur.elapsed(),
);
}
}
}
#[derive(Default)]
pub struct PolarsPlugin {
pub(crate) cache: Cache,
@ -35,6 +99,10 @@ pub struct PolarsPlugin {
}
impl Plugin for PolarsPlugin {
fn version(&self) -> String {
env!("CARGO_PKG_VERSION").into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
let mut commands: Vec<Box<dyn PluginCommand<Plugin = Self>>> = vec![Box::new(PolarsCmd)];
commands.append(&mut eager_commands());
@ -52,7 +120,7 @@ impl Plugin for PolarsPlugin {
) -> Result<(), LabeledError> {
if !self.disable_cache_drop {
let id = CustomValueType::try_from_custom_value(custom_value)?.id();
let _ = self.cache.remove(Some(engine), &id, false);
let _ = self.cache.remove(engine, &id, false);
}
Ok(())
}
@ -193,6 +261,22 @@ pub mod test {
}
}
struct TestEngineWrapper;
impl EngineWrapper for TestEngineWrapper {
fn get_env_var(&self, key: &str) -> Option<String> {
std::env::var(key).ok()
}
fn use_color(&self) -> bool {
false
}
fn set_gc_disabled(&self, _disabled: bool) -> Result<(), ShellError> {
Ok(())
}
}
pub fn test_polars_plugin_command(command: &impl PluginCommand) -> Result<(), ShellError> {
test_polars_plugin_command_with_decls(command, vec![])
}
@ -212,7 +296,7 @@ pub mod test {
let id = obj.id();
plugin
.cache
.insert(None, id, obj, Span::test_data())
.insert(TestEngineWrapper {}, id, obj, Span::test_data())
.unwrap();
}
}

View File

@ -28,6 +28,7 @@ import json
NUSHELL_VERSION = "0.94.3"
PLUGIN_VERSION = "0.1.0" # bump if you change commands!
def signatures():
@ -228,7 +229,13 @@ def handle_input(input):
exit(0)
elif "Call" in input:
[id, plugin_call] = input["Call"]
if plugin_call == "Signature":
if plugin_call == "Metadata":
write_response(id, {
"Metadata": {
"version": PLUGIN_VERSION,
}
})
elif plugin_call == "Signature":
write_response(id, signatures())
elif "Run" in plugin_call:
process_call(id, plugin_call["Run"])

View File

@ -16,6 +16,10 @@ impl Query {
}
impl Plugin for Query {
fn version(&self) -> String {
env!("CARGO_PKG_VERSION").into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
vec![
Box::new(QueryCommand),

View File

@ -136,7 +136,21 @@ fn handle_message(
) -> Result<(), Box<dyn Error>> {
if let Some(plugin_call) = message.get("Call") {
let (id, plugin_call) = (&plugin_call[0], &plugin_call[1]);
if plugin_call.as_str() == Some("Signature") {
if plugin_call.as_str() == Some("Metadata") {
write(
output,
&json!({
"CallResponse": [
id,
{
"Metadata": {
"version": env!("CARGO_PKG_VERSION"),
}
}
]
}),
)
} else if plugin_call.as_str() == Some("Signature") {
write(
output,
&json!({

View File

@ -323,6 +323,12 @@ fn convert_to_value(
msg: "string interpolation not supported in nuon".into(),
span: expr.span,
}),
Expr::GlobInterpolation(..) => Err(ShellError::OutsideSpannedLabeledError {
src: original_text.to_string(),
error: "Error when loading".into(),
msg: "glob interpolation not supported in nuon".into(),
span: expr.span,
}),
Expr::Subexpression(..) => Err(ShellError::OutsideSpannedLabeledError {
src: original_text.to_string(),
error: "Error when loading".into(),

View File

@ -401,7 +401,7 @@ fn main() -> Result<()> {
#[cfg(feature = "plugin")]
if let Some(plugins) = &parsed_nu_cli_args.plugins {
use nu_plugin_engine::{GetPlugin, PluginDeclaration};
use nu_protocol::{engine::StateWorkingSet, ErrSpan, PluginIdentity};
use nu_protocol::{engine::StateWorkingSet, ErrSpan, PluginIdentity, RegisteredPlugin};
// Load any plugins specified with --plugins
start_time = std::time::Instant::now();
@ -420,8 +420,14 @@ fn main() -> Result<()> {
// Create the plugin and add it to the working set
let plugin = nu_plugin_engine::add_plugin_to_working_set(&mut working_set, &identity)?;
// Spawn the plugin to get its signatures, and then add the commands to the working set
for signature in plugin.clone().get_plugin(None)?.get_signature()? {
// Spawn the plugin to get the metadata and signatures
let interface = plugin.clone().get_plugin(None)?;
// Set its metadata
plugin.set_metadata(Some(interface.get_metadata()?));
// Add the commands from the signature to the working set
for signature in interface.get_signature()? {
let decl = PluginDeclaration::new(plugin.clone(), signature);
working_set.add_decl(Box::new(decl));
}

View File

@ -16,6 +16,17 @@ fn plugin_list_shows_installed_plugins() {
assert!(out.status.success());
}
#[test]
fn plugin_list_shows_installed_plugin_version() {
let out = nu_with_plugins!(
cwd: ".",
plugin: ("nu_plugin_inc"),
r#"(plugin list).version.0"#
);
assert_eq!(env!("CARGO_PKG_VERSION"), out.out);
assert!(out.status.success());
}
#[test]
fn plugin_keeps_running_after_calling_it() {
let out = nu_with_plugins!(

View File

@ -18,6 +18,13 @@ fn example_plugin_path() -> PathBuf {
.expect("nu_plugin_example not found")
}
fn valid_plugin_item_data() -> PluginRegistryItemData {
PluginRegistryItemData::Valid {
metadata: Default::default(),
commands: vec![],
}
}
#[test]
fn plugin_add_then_restart_nu() {
let result = nu_with_plugins!(
@ -149,7 +156,7 @@ fn plugin_rm_then_restart_nu() {
name: "example".into(),
filename: example_plugin_path,
shell: None,
data: PluginRegistryItemData::Valid { commands: vec![] },
data: valid_plugin_item_data(),
});
contents.upsert_plugin(PluginRegistryItem {
@ -157,7 +164,7 @@ fn plugin_rm_then_restart_nu() {
// this doesn't exist, but it should be ok
filename: dirs.test().join("nu_plugin_foo"),
shell: None,
data: PluginRegistryItemData::Valid { commands: vec![] },
data: valid_plugin_item_data(),
});
contents
@ -225,7 +232,7 @@ fn plugin_rm_from_custom_path() {
name: "example".into(),
filename: example_plugin_path,
shell: None,
data: PluginRegistryItemData::Valid { commands: vec![] },
data: valid_plugin_item_data(),
});
contents.upsert_plugin(PluginRegistryItem {
@ -233,7 +240,7 @@ fn plugin_rm_from_custom_path() {
// this doesn't exist, but it should be ok
filename: dirs.test().join("nu_plugin_foo"),
shell: None,
data: PluginRegistryItemData::Valid { commands: vec![] },
data: valid_plugin_item_data(),
});
contents
@ -273,7 +280,7 @@ fn plugin_rm_using_filename() {
name: "example".into(),
filename: example_plugin_path.clone(),
shell: None,
data: PluginRegistryItemData::Valid { commands: vec![] },
data: valid_plugin_item_data(),
});
contents.upsert_plugin(PluginRegistryItem {
@ -281,7 +288,7 @@ fn plugin_rm_using_filename() {
// this doesn't exist, but it should be ok
filename: dirs.test().join("nu_plugin_foo"),
shell: None,
data: PluginRegistryItemData::Valid { commands: vec![] },
data: valid_plugin_item_data(),
});
contents
@ -331,7 +338,7 @@ fn warning_on_invalid_plugin_item() {
name: "example".into(),
filename: example_plugin_path,
shell: None,
data: PluginRegistryItemData::Valid { commands: vec![] },
data: valid_plugin_item_data(),
});
contents.upsert_plugin(PluginRegistryItem {