Merge remote-tracking branch 'origin/main' into plugin-ctrlc
This commit is contained in:
commit
6797008430
2
.github/workflows/audit.yml
vendored
2
.github/workflows/audit.yml
vendored
|
@ -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 }}
|
||||
|
|
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
|
@ -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
|
||||
|
|
6
.github/workflows/nightly-build.yml
vendored
6
.github/workflows/nightly-build.yml
vendored
|
@ -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
|
||||
|
||||
|
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -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: |
|
||||
|
|
2
.github/workflows/typos.yml
vendored
2
.github/workflows/typos.yml
vendored
|
@ -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
11
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(())
|
||||
})?;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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),
|
||||
])
|
||||
}),
|
||||
])),
|
||||
},
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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]),
|
||||
)?;
|
||||
|
|
|
@ -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("e_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)?;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)?),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
//! }
|
||||
|
|
|
@ -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)]
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
//! }
|
||||
|
|
|
@ -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)]
|
||||
/// # }
|
||||
|
|
|
@ -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()`.
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
38
crates/nu-protocol/src/plugin/metadata.rs
Normal file
38
crates/nu-protocol/src/plugin/metadata.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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::*;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>,
|
||||
},
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)]
|
||||
}
|
||||
|
|
|
@ -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())]
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
41
crates/nu_plugin_polars/src/cache/mod.rs
vendored
41
crates/nu_plugin_polars/src/cache/mod.rs
vendored
|
@ -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,
|
||||
|
|
2
crates/nu_plugin_polars/src/cache/rm.rs
vendored
2
crates/nu_plugin_polars/src/cache/rm.rs
vendored
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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!({
|
||||
|
|
|
@ -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(),
|
||||
|
|
12
src/main.rs
12
src/main.rs
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue
Block a user