Add an example Nushell plugin written in Nushell itself (#12574)

# Description

As suggested by @fdncred.

It's neat that this is possible, but the particularly useful part of
this is that we can actually
test it because it doesn't have any external dependencies, unlike the
python plugin.

Right now this just implements exactly the same behavior as the python
plugin, but we could have it
exercise a few more things.

Also fixes a couple of bugs:

- `.nu` plugins were not run with `nu --stdin`, so they couldn't take
input.
- `register` couldn't be called if `--no-config-file` was set, because
it would error on trying to
  update the plugin file.

# User-Facing Changes

- `nu_plugin_nu_example` plugin added.
- `register` now works in `--no-config-file` mode.

# Tests + Formatting
Tests added for `nu_plugin_nu_example`.

- 🟢 `toolkit fmt`
- 🟢 `toolkit clippy`
- 🟢 `toolkit test`
- 🟢 `toolkit test stdlib`

# After Submitting

- [ ] Add the version bump to the release script just like for python
This commit is contained in:
Devyn Cairns 2024-04-18 23:53:30 -07:00 committed by GitHub
parent 6d2cb4382a
commit fac2f43aa4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 294 additions and 2 deletions

View file

@ -113,7 +113,10 @@ fn create_command(path: &Path, mut shell: Option<&Path>, mode: &CommunicationMod
Some(Path::new("sh"))
}
}
Some("nu") => Some(Path::new("nu")),
Some("nu") => {
shell_args.push("--stdin");
Some(Path::new("nu"))
}
Some("py") => Some(Path::new("python")),
Some("rb") => Some(Path::new("ruby")),
Some("jar") => {

View file

@ -269,7 +269,9 @@ impl EngineState {
#[cfg(feature = "plugin")]
if delta.plugins_changed {
// Update the plugin file with the new signatures.
self.update_plugin_file()?;
if self.plugin_signatures.is_some() {
self.update_plugin_file()?;
}
}
Ok(())

View file

@ -0,0 +1,260 @@
#!/usr/bin/env -S nu --stdin
# Example of using a Nushell script as a Nushell plugin
#
# This is a port of the nu_plugin_python_example plugin to Nushell itself. There is probably not
# really any reason to write a Nushell plugin in Nushell, but this is a fun proof of concept, and
# it also allows us to test the plugin interface with something manually implemented in a scripting
# language without adding any extra dependencies to our tests.
const NUSHELL_VERSION = "0.92.3"
def main [--stdio] {
if ($stdio) {
start_plugin
} else {
print -e "Run me from inside nushell!"
exit 1
}
}
const SIGNATURES = [
{
sig: {
name: nu_plugin_nu_example,
usage: "Signature test for Nushell plugin in Nushell",
extra_usage: "",
required_positional: [
[
name,
desc,
shape
];
[
a,
"required integer value",
Int
],
[
b,
"required string value",
String
]
],
optional_positional: [
[
name,
desc,
shape
];
[
opt,
"Optional number",
Int
]
],
rest_positional: {
name: rest,
desc: "rest value string",
shape: String
},
named: [
[
long,
short,
arg,
required,
desc
];
[
help,
h,
null,
false,
"Display the help message for this command"
],
[
flag,
f,
null,
false,
"a flag for the signature"
],
[
named,
n,
String,
false,
"named string"
]
],
input_output_types: [
[Any, Any]
],
allow_variants_without_examples: true,
search_terms: [
Example
],
is_filter: false,
creates_scope: false,
allows_unknown_args: false,
category: Experimental
},
examples: []
}
]
def process_call [
id: int,
plugin_call: record<
name: string,
call: record<
head: record<start: int, end: int>,
positional: list,
named: list,
>,
input: any
>
] {
# plugin_call is a dictionary with the information from the call
# It should contain:
# - The name of the call
# - The call data which includes the positional and named values
# - The input from the pipeline
# Use this information to implement your plugin logic
# Print the call to stderr, in raw nuon and as a table
$plugin_call | to nuon --raw | print -e
$plugin_call | table -e | print -e
# Get the span from the call
let span = $plugin_call.call.head
# Create a Value of type List that will be encoded and sent to Nushell
let value = {
Value: {
List: {
vals: (0..9 | each { |x|
{
Record: {
val: (
[one two three] |
zip (0..2 | each { |y|
{
Int: {
val: ($x * $y),
span: $span,
}
}
}) |
each { into record } |
transpose --as-record --header-row
),
span: $span
}
}
}),
span: $span
}
}
}
write_response $id { PipelineData: $value }
}
def tell_nushell_encoding [] {
print -n "\u{0004}json"
}
def tell_nushell_hello [] {
# A `Hello` message is required at startup to inform nushell of the protocol capabilities and
# compatibility of the plugin. The version specified should be the version of nushell that this
# plugin was tested and developed against.
let hello = {
Hello: {
protocol: "nu-plugin", # always this value
version: $NUSHELL_VERSION,
features: []
}
}
$hello | to json --raw | print
}
def write_response [id: int, response: record] {
# Use this format to send a response to a plugin call. The ID of the plugin call is required.
let wrapped_response = {
CallResponse: [
$id,
$response,
]
}
$wrapped_response | to json --raw | print
}
def write_error [id: int, text: string, span?: record<start: int, end: int>] {
# Use this error format to send errors to nushell in response to a plugin call. The ID of the
# plugin call is required.
let error = if ($span | is-not-empty) {
{
Error: {
msg: "ERROR from plugin",
labels: [
{
text: $text,
span: $span,
}
],
}
}
} else {
{
Error: {
msg: "ERROR from plugin",
help: $text,
}
}
}
write_response $id $error
}
def handle_input []: any -> nothing {
match $in {
{ Hello: $hello } => {
if ($hello.version != $NUSHELL_VERSION) {
exit 1
}
}
"Goodbye" => {
exit 0
}
{ Call: [$id, $plugin_call] } => {
match $plugin_call {
"Signature" => {
write_response $id { Signature: $SIGNATURES }
}
{ Run: $call_info } => {
process_call $id $call_info
}
_ => {
write_error $id $"Operation not supported: ($plugin_call | to json --raw)"
}
}
}
$other => {
print -e $"Unknown message: ($other | to json --raw)"
exit 1
}
}
}
def start_plugin [] {
lines |
prepend (do {
# This is a hack so that we do this first, but we can also take input as a stream
tell_nushell_encoding
tell_nushell_hello
[]
}) |
each { from json | handle_input } |
ignore
}

View file

@ -3,6 +3,7 @@ mod core_inc;
mod custom_values;
mod env;
mod formats;
mod nu_plugin_nu_example;
mod register;
mod stream;
mod stress_internals;

View file

@ -0,0 +1,26 @@
use nu_test_support::nu;
#[test]
fn register() {
let out = nu!("register crates/nu_plugin_nu_example/nu_plugin_nu_example.nu");
assert!(out.status.success());
assert!(out.out.trim().is_empty());
assert!(out.err.trim().is_empty());
}
#[test]
fn call() {
let out = nu!(r#"
register crates/nu_plugin_nu_example/nu_plugin_nu_example.nu
nu_plugin_nu_example 4242 teststring
"#);
assert!(out.status.success());
assert!(out.err.contains("name: nu_plugin_nu_example"));
assert!(out.err.contains("4242"));
assert!(out.err.contains("teststring"));
assert!(out.out.contains("one"));
assert!(out.out.contains("two"));
assert!(out.out.contains("three"));
}