mirror of
https://github.com/nushell/nushell
synced 2024-11-10 15:14:14 +00:00
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:
parent
6d2cb4382a
commit
fac2f43aa4
5 changed files with 294 additions and 2 deletions
|
@ -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") => {
|
||||
|
|
|
@ -269,8 +269,10 @@ impl EngineState {
|
|||
#[cfg(feature = "plugin")]
|
||||
if delta.plugins_changed {
|
||||
// Update the plugin file with the new signatures.
|
||||
if self.plugin_signatures.is_some() {
|
||||
self.update_plugin_file()?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
260
crates/nu_plugin_nu_example/nu_plugin_nu_example.nu
Executable file
260
crates/nu_plugin_nu_example/nu_plugin_nu_example.nu
Executable 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
|
||||
}
|
|
@ -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;
|
||||
|
|
26
tests/plugins/nu_plugin_nu_example.rs
Normal file
26
tests/plugins/nu_plugin_nu_example.rs
Normal 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"));
|
||||
}
|
Loading…
Reference in a new issue