Loading...
Loading...
Guide for creating Nushell plugins in Rust using nu_plugin and nu_protocol crates. Use when users want to build custom Nushell commands, extend Nushell with new functionality, create data transformations, or integrate external tools/APIs into Nushell. Covers project setup, command implementation, streaming data, custom values, and testing.
npx skill4agent add ypares/agent-skills nushell-plugin-buildercargo new nu_plugin_<name>
cd nu_plugin_<name>
cargo add nu-plugin nu-protocoluse nu_plugin::{EvaluatedCall, MsgPackSerializer, serve_plugin};
use nu_plugin::{EngineInterface, Plugin, PluginCommand, SimplePluginCommand};
use nu_protocol::{LabeledError, Signature, Type, Value};
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![Box::new(MyCommand)]
}
}
struct MyCommand;
impl SimplePluginCommand for MyCommand {
type Plugin = MyPlugin;
fn name(&self) -> &str {
"my-command"
}
fn signature(&self) -> Signature {
Signature::build("my-command")
.input_output_type(Type::String, Type::Int)
}
fn run(
&self,
_plugin: &MyPlugin,
_engine: &EngineInterface,
call: &EvaluatedCall,
input: &Value,
) -> Result<Value, LabeledError> {
match input {
Value::String { val, .. } => {
Ok(Value::int(val.len() as i64, call.head))
}
_ => Err(LabeledError::new("Expected string input")
.with_label("requires string", call.head))
}
}
}
fn main() {
serve_plugin(&MyPlugin, MsgPackSerializer)
}# Build
cargo build --release
# Install to cargo bin
cargo install --path . --locked
# Register with nushell
plugin add ~/.cargo/bin/nu_plugin_<name> # Add .exe on Windows
plugin use <name>
# Test
"hello" | my-command&ValueResult<Value, LabeledError>PipelineDataResult<PipelineData, LabeledError>references/advanced-features.mduse nu_protocol::{Signature, Type};
Signature::build("my-command")
.input_output_type(Type::String, Type::Int)StringIntFloatBoolList(Box<Type>)Record(...)AnySignature::build("my-command")
// Named flags
.named("output", SyntaxShape::Filepath, "output file", Some('o'))
.switch("verbose", "enable verbose output", Some('v'))
// Positional arguments
.required("input", SyntaxShape::String, "input value")
.optional("count", SyntaxShape::Int, "repeat count")
.rest("files", SyntaxShape::Filepath, "files to process")fn run(&self, call: &EvaluatedCall, ...) -> Result<Value, LabeledError> {
let output: Option<String> = call.get_flag("output")?;
let verbose: bool = call.has_flag("verbose")?;
let input: String = call.req(0)?; // First positional
let count: Option<i64> = call.opt(1)?; // Second positional
let files: Vec<String> = call.rest(2)?; // Remaining args
}LabeledErrorErr(LabeledError::new("Error message")
.with_label("specific issue", call.head))main()serve_plugin(&MyPlugin, MsgPackSerializer) // Production
// serve_plugin(&MyPlugin, JsonSerializer) // DebugValue::String { val, .. } => {
Ok(Value::string(val.to_uppercase(), call.head))
}let items = vec![
Value::string("a", call.head),
Value::string("b", call.head),
];
Ok(Value::list(items, call.head))use nu_protocol::record;
Ok(Value::record(
record! {
"name" => Value::string("example", call.head),
"size" => Value::int(42, call.head),
},
call.head,
))let records = vec![
Value::record(record! { "name" => Value::string("a", span) }, span),
Value::record(record! { "name" => Value::string("b", span) }, span),
];
Ok(Value::list(records, call.head))references/examples.md# Build
cargo build
# Test (debug build)
plugin add target/debug/nu_plugin_<name>
plugin use <name>
"test" | my-command
# After changes, reload
plugin rm <name>
plugin add target/debug/nu_plugin_<name>
plugin use <name>[dev-dependencies]
nu-plugin-test-support = "0.109.1"#[cfg(test)]
mod tests {
use nu_plugin_test_support::PluginTest;
#[test]
fn test_command() -> Result<(), nu_protocol::ShellError> {
PluginTest::new("myplugin", MyPlugin.into())?
.test_examples(&MyCommand)
}
}references/testing-debugging.mdPipelineDataimpl PluginCommand for MyCommand {
fn run(&self, input: PipelineData, ...) -> Result<PipelineData, LabeledError> {
let filtered = input.into_iter().filter(|v| /* condition */);
Ok(PipelineData::ListStream(ListStream::new(filtered, span, None), None))
}
}// Get environment variables
let home = engine.get_env_var("HOME")?;
// Set environment variables (before response)
engine.add_env_var("MY_VAR", Value::string("value", span))?;
// Get plugin config from $env.config.plugins.<name>
let config = engine.get_plugin_config()?;
// Get current directory for path resolution
let cwd = engine.get_current_dir()?;references/advanced-features.mdengine.is_using_stdio()engine.get_current_dir()nu-pluginnu-protocolreferences/plugin-protocol.mdreferences/advanced-features.mdreferences/examples.mdreferences/testing-debugging.mdscripts/init_plugin.pypython3 scripts/init_plugin.py <plugin-name> [--output-dir <path>]