nushell/crates/nu_plugin_nu_example/nu_plugin_nu_example.nu

269 lines
5.9 KiB
Plaintext
Raw Normal View History

#!/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.96.2"
const PLUGIN_VERSION = "0.1.1" # bump if you change commands!
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
}
}, null]
}
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 {
Allow plugins to report their own version and store it in the registry (#12883) # Description This allows plugins to report their version (and potentially other metadata in the future). The version is shown in `plugin list` and in `version`. The metadata is stored in the registry file, and reflects whatever was retrieved on `plugin add`, not necessarily the running binary. This can help you to diagnose if there's some kind of mismatch with what you expect. We could potentially use this functionality to show a warning or error if a plugin being run does not have the same version as what was in the cache file, suggesting `plugin add` be run again, but I haven't done that at this point. It is optional, and it requires the plugin author to make some code changes if they want to provide it, since I can't automatically determine the version of the calling crate or anything tricky like that to do it. Example: ``` > plugin list | select name version is_running pid ╭───┬────────────────┬─────────┬────────────┬─────╮ │ # │ name │ version │ is_running │ pid │ ├───┼────────────────┼─────────┼────────────┼─────┤ │ 0 │ example │ 0.93.1 │ false │ │ │ 1 │ gstat │ 0.93.1 │ false │ │ │ 2 │ inc │ 0.93.1 │ false │ │ │ 3 │ python_example │ 0.1.0 │ false │ │ ╰───┴────────────────┴─────────┴────────────┴─────╯ ``` cc @maxim-uvarov (he asked for it) # User-Facing Changes - `plugin list` gets a `version` column - `version` shows plugin versions when available - plugin authors *should* add `fn metadata()` to their `impl Plugin`, but don't have to # Tests + Formatting Tested the low level stuff and also the `plugin list` column. # After Submitting - [ ] update plugin guide docs - [ ] update plugin protocol docs (`Metadata` call & response) - [ ] update plugin template (`fn metadata()` should be easy) - [ ] release notes
2024-06-21 13:27:09 +02:00
"Metadata" => {
write_response $id {
Metadata: {
version: $PLUGIN_VERSION
}
}
}
"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
}