├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── README.md.liquid ├── cargo-generate.toml ├── init.rhai ├── pre.rhai ├── src ├── commands │ ├── mod.rs │ └── {{command_module}}.rs └── main.rs └── test.nu /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | {% if github_username != empty -%} 4 | 5 | {% endif -%} 6 | name = "{{ project-name }}" 7 | version = "0.1.0" 8 | authors = ["{{authors}}"] 9 | edition = "2021" 10 | description = "a nushell plugin called {{ plugin_name }}" 11 | repository = "https://github.com/{{ github_username }}/{{ project-name }}" 12 | license = "MIT" 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | # for local development, you can use a path dependency 18 | # nu-plugin = { path = "../nushell/crates/nu-plugin" } 19 | # nu-protocol = { path = "../nushell/crates/nu-protocol", features = ["plugin"] } 20 | nu-plugin = "0.104.0" 21 | nu-protocol = { version = "0.104.0", features = ["plugin"] } 22 | 23 | [dev-dependencies] 24 | # nu-plugin-test-support = { path = "../nushell/crates/nu-plugin-test-support" } 25 | nu-plugin-test-support = { version = "0.104.0" } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 - 2022 The Nushell Project Developers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nu_plugin_template 2 | 3 | This template is intended to be used with [cargo-generate](https://github.com/cargo-generate/cargo-generate) in order to quickly bootstrap nushell plugin projects. 4 | 5 | You must run `cargo generate` with `--force`. 6 | 7 | ## Usage: 8 | 9 | ``` 10 | > cargo generate --force --git https://github.com/nushell/nu_plugin_template 11 | 🤷 What will this plugin be named?: foo 12 | Creating a new plugin named "foo" 13 | Your plugin crate will be named "nu_plugin_foo". 14 | 15 | Note that the MIT license is used by default, to reflect the majority of 16 | Nushell projects. You can change this manually if you'd like to. 17 | 18 | !!! IMPORTANT !!! 19 | You must run cargo generate with --force, or it will rename your project to 20 | something that is non-standard for Nushell plugins and this will fail. 21 | 22 | If you see a message after this about renaming your project, please abort and 23 | try again with --force. 24 | 25 | 🔧 Destination: /var/home/devyn/Projects/nushell/nu_plugin_foo ... 26 | 🔧 project-name: nu_plugin_foo ... 27 | 🔧 Generating template ... 28 | 🤷 What should your first command be called? (spaces are okay): foo 29 | ✔ 🤷 Do you intend to create more than one command / subcommand? · No 30 | ✔ 🤷 Would you like a simple command? Say no if you would like to use streaming. · Yes 31 | 🤷 What is your GitHub username? (Leave blank if you don't want to publish to GitHub) [default: ]: 32 | 🔧 Moving generated files into: `/var/home/devyn/Projects/nushell/nu_plugin_foo`... 33 | 🔧 Initializing a fresh Git repository 34 | ✨ Done! New project created /var/home/devyn/Projects/nushell/nu_plugin_foo 35 | > cd nu_plugin_foo 36 | > cargo build 37 | > plugin add target/debug/nu_plugin_foo 38 | > plugin use foo 39 | > foo Ellie 40 | Hello, Ellie. How are you today? 41 | ``` 42 | 43 | ## Config values 44 | 45 | - `plugin_name` - all nushell plugins are binaries with the name format 46 | `nu_plugin_SOMETHING`. This is how nushell discovers them. You need to tell this 47 | generator what that `SOMETHING` is. If you enter `random` as the plugin name, 48 | your binary will be called `nu_plugin_random`. 49 | 50 | - `command_name` - the name of your first/only command. This can be any valid nushell command name, 51 | and can contain spaces. For example, if you're creating a format plugin for `FORMAT` files, you 52 | might choose to go with `from FORMAT` or `to FORMAT`. 53 | 54 | - `multi_commmand` - set to `Yes` if you expect that you'll be creating more than one command, in 55 | which case we'll create a `commands` module for you and put the command in there. Set to `No` if you 56 | would rather just have everything in `src/main.rs`. 57 | 58 | - `command_is_simple` - set to `Yes` if you want a `SimplePluginCommand` with no streaming support, 59 | or `No` if you want `PluginCommand` with a streaming example. 60 | 61 | - `github_username` - we'll use this to set the repository field in `Cargo.toml` if you set it. 62 | 63 | -------------------------------------------------------------------------------- /README.md.liquid: -------------------------------------------------------------------------------- 1 | # {{ project-name }} 2 | 3 | This is a [Nushell](https://nushell.sh/) plugin called "{{ plugin_name }}". 4 | 5 | ## Installing 6 | 7 | ```nushell 8 | > cargo install --path . 9 | ``` 10 | 11 | ## Usage 12 | 13 | FIXME: This reflects the demo functionality generated with the template. Update this documentation 14 | once you have implemented the actual plugin functionality. 15 | 16 | ```nushell 17 | > plugin add ~/.cargo/bin/{{ project-name }} 18 | > plugin use {{ plugin_name }} 19 | {% if command_is_simple -%} 20 | > {{ command_name }} Ellie 21 | Hello, Ellie. How are you today? 22 | > {{ command_name }} --shout Ellie 23 | HELLO, ELLIE. HOW ARE YOU TODAY? 24 | {%- else -%} 25 | > [ Ellie ] | {{ command_name }} 26 | ╭───┬──────────────────────────────────╮ 27 | │ 0 │ Hello, Ellie. How are you today? │ 28 | ╰───┴──────────────────────────────────╯ 29 | > [ Ellie ] | {{ command_name }} --shout 30 | ╭───┬──────────────────────────────────╮ 31 | │ 0 │ HELLO, ELLIE. HOW ARE YOU TODAY? │ 32 | ╰───┴──────────────────────────────────╯ 33 | {%- endif %} 34 | ``` 35 | -------------------------------------------------------------------------------- /cargo-generate.toml: -------------------------------------------------------------------------------- 1 | [template] 2 | ignore = ["test.nu"] 3 | 4 | [placeholders.command_name] 5 | type = "string" 6 | prompt = "What should your first command be called? (spaces are okay)" 7 | 8 | [placeholders.multi_command] 9 | type = "string" 10 | prompt = "Do you intend to create more than one command / subcommand?" 11 | choices = [ 12 | "Yes", 13 | "No" 14 | ] 15 | default = "No" 16 | 17 | [placeholders.command_is_simple] 18 | type = "string" 19 | prompt = "Would you like a simple command? Say no if you would like to use streaming." 20 | choices = [ 21 | "Yes", 22 | "No" 23 | ] 24 | 25 | [placeholders.github_username] 26 | type = "string" 27 | prompt = "What is your GitHub username? (Leave blank if you don't want to publish to GitHub)" 28 | default = "" 29 | 30 | [hooks] 31 | init = ["init.rhai"] 32 | pre = ["pre.rhai"] 33 | 34 | [conditional.'multi_command == "No"'] 35 | ignore = ["src/commands"] 36 | -------------------------------------------------------------------------------- /init.rhai: -------------------------------------------------------------------------------- 1 | let plugin_name = variable::get("project-name"); 2 | 3 | if plugin_name.is_empty() { 4 | plugin_name = variable::prompt("What will this plugin be named?").to_snake_case(); 5 | } 6 | 7 | if plugin_name.starts_with("nu_plugin_") { 8 | plugin_name.replace("nu_plugin_", ""); 9 | } 10 | 11 | if plugin_name.is_empty() { 12 | abort("Plugin name must not be empty."); 13 | } 14 | 15 | variable::set("plugin_name", plugin_name); 16 | variable::set("project-name", "nu_plugin_" + plugin_name); 17 | 18 | print(`Creating a new plugin named "${variable::get("plugin_name")}"`); 19 | print(`Your plugin crate will be named "${variable::get("project-name")}".`); 20 | print(""); 21 | print("Note that the MIT license is used by default, to reflect the majority of"); 22 | print("Nushell projects. You can change this manually if you'd like to."); 23 | print(""); 24 | print("!!! IMPORTANT !!!"); 25 | print("You must run cargo generate with --force, or it will rename your project to"); 26 | print("something that is non-standard for Nushell plugins and this will fail."); 27 | print(""); 28 | print("If you see a message after this about renaming your project, please abort and"); 29 | print("try again with --force."); 30 | print(""); 31 | -------------------------------------------------------------------------------- /pre.rhai: -------------------------------------------------------------------------------- 1 | if variable::get("project-name").contains("-") { 2 | abort("Please run cargo generate with --force, so that we can set the crate name to\n\ 3 | something starting with `nu_plugin_`."); 4 | } 5 | 6 | variable::set("plugin_struct", variable::get("plugin_name").to_pascal_case() + "Plugin"); 7 | variable::set("command_struct", variable::get("command_name").to_pascal_case()); 8 | variable::set("command_module", variable::get("command_name").to_snake_case()); 9 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | // Command modules should be added here 2 | mod {{ command_module }}; 3 | 4 | // Command structs should be exported here 5 | pub use {{ command_module }}::{{ command_struct }}; 6 | -------------------------------------------------------------------------------- /src/commands/{{command_module}}.rs: -------------------------------------------------------------------------------- 1 | {% if command_is_simple == "Yes" -%} 2 | use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand}; 3 | use nu_protocol::{Category, Example, LabeledError, Signature, SyntaxShape, Value}; 4 | {%- else -%} 5 | use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand}; 6 | use nu_protocol::{Category, Example, LabeledError, PipelineData, Signals, Signature, Type, Value}; 7 | {%- endif %} 8 | 9 | use crate::{{ plugin_struct }}; 10 | 11 | pub struct {{ command_struct }}; 12 | 13 | {% if command_is_simple == "Yes" -%} 14 | impl SimplePluginCommand for {{ command_struct }} { 15 | type Plugin = {{ plugin_struct }}; 16 | 17 | fn name(&self) -> &str { 18 | "{{ command_name }}" 19 | } 20 | 21 | fn signature(&self) -> Signature { 22 | Signature::build(self.name()) 23 | .required("name", SyntaxShape::String, "(FIXME) A demo parameter - your name") 24 | .switch("shout", "(FIXME) Yell it instead", None) 25 | .category(Category::Experimental) 26 | } 27 | 28 | fn description(&self) -> &str { 29 | "(FIXME) help text for {{ command_name }}" 30 | } 31 | 32 | fn examples(&self) -> Vec { 33 | vec![ 34 | Example { 35 | example: "{{ command_name }} Ellie", 36 | description: "Say hello to Ellie", 37 | result: Some(Value::test_string("Hello, Ellie. How are you today?")), 38 | }, 39 | Example { 40 | example: "{{ command_name }} --shout Ellie", 41 | description: "Shout hello to Ellie", 42 | result: Some(Value::test_string("HELLO, ELLIE. HOW ARE YOU TODAY?")), 43 | }, 44 | ] 45 | } 46 | 47 | fn run( 48 | &self, 49 | _plugin: &{{ plugin_struct }}, 50 | _engine: &EngineInterface, 51 | call: &EvaluatedCall, 52 | _input: &Value, 53 | ) -> Result { 54 | let name: String = call.req(0)?; 55 | let mut greeting = format!("Hello, {name}. How are you today?"); 56 | if call.has_flag("shout")? { 57 | greeting = greeting.to_uppercase(); 58 | } 59 | Ok(Value::string(greeting, call.head)) 60 | } 61 | } 62 | {%- else -%} 63 | impl PluginCommand for {{ command_struct }} { 64 | type Plugin = {{ plugin_struct }}; 65 | 66 | fn name(&self) -> &str { 67 | "{{ command_name }}" 68 | } 69 | 70 | fn signature(&self) -> Signature { 71 | Signature::build(self.name()) 72 | .switch("shout", "(FIXME) Yell it instead", None) 73 | .input_output_type(Type::List(Type::String.into()), Type::List(Type::String.into())) 74 | .category(Category::Experimental) 75 | } 76 | 77 | fn description(&self) -> &str { 78 | "(FIXME) help text for {{ command_name }}" 79 | } 80 | 81 | fn examples(&self) -> Vec { 82 | vec![ 83 | Example { 84 | example: "[ Ellie ] | {{ command_name }}", 85 | description: "Say hello to Ellie", 86 | result: Some(Value::test_list(vec![ 87 | Value::test_string("Hello, Ellie. How are you today?") 88 | ])), 89 | }, 90 | Example { 91 | example: "[ Ellie ] | {{ command_name }} --shout", 92 | description: "Shout hello to Ellie", 93 | result: Some(Value::test_list(vec![ 94 | Value::test_string("HELLO, ELLIE. HOW ARE YOU TODAY?") 95 | ])), 96 | }, 97 | ] 98 | } 99 | 100 | fn run( 101 | &self, 102 | _plugin: &{{ plugin_struct }}, 103 | _engine: &EngineInterface, 104 | call: &EvaluatedCall, 105 | input: PipelineData, 106 | ) -> Result { 107 | let span = call.head; 108 | let shout = call.has_flag("shout")?; 109 | Ok(input.map(move |name| { 110 | match name.as_str() { 111 | Ok(name) => { 112 | let mut greeting = format!("Hello, {name}. How are you today?"); 113 | if shout { 114 | greeting = greeting.to_uppercase(); 115 | } 116 | Value::string(greeting, span) 117 | } 118 | Err(err) => Value::error(err, span), 119 | } 120 | }, &Signals::empty())?) 121 | } 122 | } 123 | {%- endif %} 124 | 125 | #[test] 126 | fn test_examples() -> Result<(), nu_protocol::ShellError> { 127 | use nu_plugin_test_support::PluginTest; 128 | 129 | // This will automatically run the examples specified in your command and compare their actual 130 | // output against what was specified in the example. 131 | // 132 | // We recommend you add this test to any other commands you create, or remove it if the examples 133 | // can't be tested this way. 134 | 135 | PluginTest::new("{{ plugin_name }}", {{ plugin_struct }}.into())? 136 | .test_command_examples(&{{ command_struct }}) 137 | } 138 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use nu_plugin::{MsgPackSerializer, Plugin, PluginCommand, serve_plugin}; 2 | {% if multi_command == "No" -%} 3 | {%- if command_is_simple == "Yes" -%} 4 | use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand}; 5 | use nu_protocol::{Category, Example, LabeledError, Signature, SyntaxShape, Value}; 6 | {%- else -%} 7 | use nu_plugin::{EngineInterface, EvaluatedCall}; 8 | use nu_protocol::{Category, Example, LabeledError, PipelineData, Signals, Signature, Type, Value}; 9 | {%- endif %} 10 | {%- else %} 11 | mod commands; 12 | pub use commands::*; 13 | {%- endif %} 14 | 15 | pub struct {{ plugin_struct }}; 16 | 17 | impl Plugin for {{ plugin_struct }} { 18 | fn version(&self) -> String { 19 | // This automatically uses the version of your package from Cargo.toml as the plugin version 20 | // sent to Nushell 21 | env!("CARGO_PKG_VERSION").into() 22 | } 23 | 24 | fn commands(&self) -> Vec>> { 25 | vec![ 26 | // Commands should be added here 27 | Box::new({{ command_struct }}), 28 | ] 29 | } 30 | } 31 | 32 | {% if multi_command == "No" -%} 33 | pub struct {{ command_struct }}; 34 | 35 | {% if command_is_simple == "Yes" -%} 36 | impl SimplePluginCommand for {{ command_struct }} { 37 | type Plugin = {{ plugin_struct }}; 38 | 39 | fn name(&self) -> &str { 40 | "{{ command_name }}" 41 | } 42 | 43 | fn signature(&self) -> Signature { 44 | Signature::build(PluginCommand::name(self)) 45 | .required("name", SyntaxShape::String, "(FIXME) A demo parameter - your name") 46 | .switch("shout", "(FIXME) Yell it instead", None) 47 | .category(Category::Experimental) 48 | } 49 | 50 | fn description(&self) -> &str { 51 | "(FIXME) help text for {{ command_name }}" 52 | } 53 | 54 | fn examples(&self) -> Vec { 55 | vec![ 56 | Example { 57 | example: "{{ command_name }} Ellie", 58 | description: "Say hello to Ellie", 59 | result: Some(Value::test_string("Hello, Ellie. How are you today?")), 60 | }, 61 | Example { 62 | example: "{{ command_name }} --shout Ellie", 63 | description: "Shout hello to Ellie", 64 | result: Some(Value::test_string("HELLO, ELLIE. HOW ARE YOU TODAY?")), 65 | }, 66 | ] 67 | } 68 | 69 | fn run( 70 | &self, 71 | _plugin: &{{ plugin_struct }}, 72 | _engine: &EngineInterface, 73 | call: &EvaluatedCall, 74 | _input: &Value, 75 | ) -> Result { 76 | let name: String = call.req(0)?; 77 | let mut greeting = format!("Hello, {name}. How are you today?"); 78 | if call.has_flag("shout")? { 79 | greeting = greeting.to_uppercase(); 80 | } 81 | Ok(Value::string(greeting, call.head)) 82 | } 83 | } 84 | {%- else -%} 85 | impl PluginCommand for {{ command_struct }} { 86 | type Plugin = {{ plugin_struct }}; 87 | 88 | fn name(&self) -> &str { 89 | "{{ command_name }}" 90 | } 91 | 92 | fn signature(&self) -> Signature { 93 | Signature::build(self.name()) 94 | .switch("shout", "(FIXME) Yell it instead", None) 95 | .input_output_type(Type::List(Type::String.into()), Type::List(Type::String.into())) 96 | .category(Category::Experimental) 97 | } 98 | 99 | fn description(&self) -> &str { 100 | "(FIXME) help text for {{ command_name }}" 101 | } 102 | 103 | fn examples(&self) -> Vec { 104 | vec![ 105 | Example { 106 | example: "[ Ellie ] | {{ command_name }}", 107 | description: "Say hello to Ellie", 108 | result: Some(Value::test_list(vec![ 109 | Value::test_string("Hello, Ellie. How are you today?") 110 | ])), 111 | }, 112 | Example { 113 | example: "[ Ellie ] | {{ command_name }} --shout", 114 | description: "Shout hello to Ellie", 115 | result: Some(Value::test_list(vec![ 116 | Value::test_string("HELLO, ELLIE. HOW ARE YOU TODAY?") 117 | ])), 118 | }, 119 | ] 120 | } 121 | 122 | fn run( 123 | &self, 124 | _plugin: &{{ plugin_struct }}, 125 | _engine: &EngineInterface, 126 | call: &EvaluatedCall, 127 | input: PipelineData, 128 | ) -> Result { 129 | let span = call.head; 130 | let shout = call.has_flag("shout")?; 131 | Ok(input.map(move |name| { 132 | match name.as_str() { 133 | Ok(name) => { 134 | let mut greeting = format!("Hello, {name}. How are you today?"); 135 | if shout { 136 | greeting = greeting.to_uppercase(); 137 | } 138 | Value::string(greeting, span) 139 | } 140 | Err(err) => Value::error(err, span), 141 | } 142 | }, &Signals::empty())?) 143 | } 144 | } 145 | {%- endif %} 146 | 147 | #[test] 148 | fn test_examples() -> Result<(), nu_protocol::ShellError> { 149 | use nu_plugin_test_support::PluginTest; 150 | 151 | // This will automatically run the examples specified in your command and compare their actual 152 | // output against what was specified in the example. You can remove this test if the examples 153 | // can't be tested this way, but we recommend including it if possible. 154 | 155 | PluginTest::new("{{ plugin_name }}", {{ plugin_struct }}.into())? 156 | .test_command_examples(&{{ command_struct }}) 157 | } 158 | 159 | {% endif -%} 160 | fn main() { 161 | serve_plugin(&{{ plugin_struct }}, MsgPackSerializer); 162 | } 163 | -------------------------------------------------------------------------------- /test.nu: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nu 2 | let tempdir = (mktemp --directory) 3 | let template = $env.PWD 4 | 5 | for command_is_simple in [Yes, No] { 6 | for multi_command in [Yes, No] { 7 | print ($"Testing with command_is_simple=($command_is_simple), " ++ 8 | $"multi_command=($multi_command)") 9 | try { 10 | do --capture-errors { 11 | cd $tempdir 12 | ( 13 | ^cargo generate 14 | --path $template 15 | --force 16 | --silent 17 | --name nu_plugin_test_plugin 18 | --define command_name="test command" 19 | --define $"command_is_simple=($command_is_simple)" 20 | --define $"multi_command=($multi_command)" 21 | --define github_username= 22 | ) 23 | do { cd nu_plugin_test_plugin; ^cargo test } 24 | rm -r nu_plugin_test_plugin 25 | } 26 | } catch { |err| 27 | print -e ($"Failed with command_is_simple=($command_is_simple), " ++ 28 | $"multi_command=($multi_command)") 29 | rm -rf $tempdir 30 | $err.raw 31 | } 32 | } 33 | } 34 | 35 | rm -rf $tempdir 36 | print "All tests passed." 37 | --------------------------------------------------------------------------------