nushell/crates/nu_plugin_nu_example/nu_plugin_nu_example.nu

267 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.
2024-08-22 11:36:32 +02:00
const NUSHELL_VERSION = "0.97.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,
description: "Signature test for Nushell plugin in Nushell",
extra_description: "",
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,
}
}
}) |
Change behavior of `into record` on lists to be more useful (#13637) # Description The previous behaviour of `into record` on lists was to create a new record with each list index as the key. This was not very useful for creating meaningful records, though, and most people would end up using commands like `headers` or `transpose` to turn a list of keys and values into a record. This PR changes that instead to do what I think the most ergonomic thing is, and instead: - A list of records is merged into one record. - A list of pairs (two element lists) is folded into a record with the first element of each pair being the key, and the second being the value. The former is just generally more useful than having to use `reduce` with `merge` for such a common operation, and the latter is useful because it means that `$a | zip $b | into record` *just works* in the way that seems most obvious. Example: ```nushell [[foo bar] [baz quux]] | into record # => {foo: bar, baz: quux} [{foo: bar} {baz: quux}] | into record # => {foo: bar, baz: quux} [foo baz] | zip [bar quux] | into record # => {foo: bar, baz: quux} ``` The support for range input has been removed, as it would no longer reflect the treatment of an equivalent list. The following is equivalent to the old behavior, in case that's desired: ``` 0.. | zip [a b c] | into record # => {0: a, 1: b, 2: c} ``` # User-Facing Changes - `into record` changed as described above (breaking) - `into record` no longer supports range input (breaking) # Tests + Formatting Examples changed to match, everything works. Some usage in stdlib and `nu_plugin_nu_example` had to be changed. # After Submitting - [ ] release notes (commands, breaking change)
2024-08-22 11:38:43 +02:00
into record
),
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
}