Adding a Nushell command
I recently made a contribution to Nushell, adding the ability to read .eml files from the shell. I had been looking for an excuse to contribute to some Rust projects, and this proved a good place to start. It first required that I write an .eml parser crate (which I won’t go into here). With that external dependency filled, I dived into how to implement a new Nushell component. At least for my from-eml
command, I found that I had to make the following changes (presented here in what I think is a logical order):
Modify crates/nu-cli/Cargo.toml
- Add a reference to my crate to make it available to Nushell.
Add crates/nu-cli/src/commands/from_eml.rs
- Create my command, presented as the
FromEML
struct - Create ancillary
FromEMLArgs
struct with arguments relevant to my command. In my case, I want to be able to pass in how many bytes of the email body I want to be able to preview. This corresponds directly to the builder functionwith_body_preview
in my crate. - Implement
WholeStreamCommand
for my struct, conforming to the contract that hooks my built-in command into the Nu ecosystem. - The core logic for adding my command went into a
from_eml
function – the name isn’t required to conform to the command, as it gets called in theWholeStreamCommand::run
impl.
There are a number things to understand about what’s going on here, and a number of confusing points that one needs to map between the domain-specific data to Nu’s data model:
- The
from-
commands – invoked asopen filename.json --raw | from-json
(explicitly passing raw JSON data to thefrom-json
command) or directly asopen filename.json
(implicitly callingfrom-json
) will read in the entire file contents and pass it to you in aRunnableContext
. You can collect the string for your own data manipulation. - Command arguments, such as my
FromEMLArgs
appear to require SerdeDeserialize
, and if the arg contains a hyphen, you should use Serde’srename(deserialize = "arg-name")
to change it to your field value (eg,arg_name
). - I believe also that the underlying Rust type is wrapped in
Tagged<T>
, though I can’t explain why. This gets extracted by accessing the.item
- For my command, I wanted to return a dictionary of values – an email should tell you that there’s a contained
Subject
with its corresponding value; it has aTo
field that may contain one or more structured values; and so on. So I’m using theTaggedDictBuilder
to collect these values. This collection uses key-value pairs, but it also maintains ordering of values inserted.TaggedDictBuilder
maps a RustString
to a NushellValue
– be it a primitive, a row of dictionaries, etc. - I found the concept of tags, untagged values, and tagged values a bit wonky. A
Tag
is effectively just a reference back to the command that invoked your custom command. I assume there’s valid reason for threading this through various results, but for my use case, there seems no reason to keep track of this information. Using theTaggedDictBuilder
and my structured data, I’m effectively linking the Nushell user’s invocation offrom-eml
and the content of the email they’re opening (eg,To:
->John Smith <jsmith@example.com>
). - It took me a while to figure out that when I parse out multiple email addresses and want to present them together, I needed to create a
Table
. The resulting code is very straightforward and easy to grok, but getting there took a lot of experimentation.
Modify crates/nu-cli/src/cli.rs
- Indicate
FromEML
is a whole stream command
Modify crates/nu-cli/src/commands.rs
- Expose the
from_eml
module and theFromEML
struct as a usable command
Add tests/fixtures/formats/sample.eml
- Sample .eml file that will be used for testing
Modify crates/nu-cli/src/utils.rs
- Appears to add the above
sample.eml
file as a resource to build-time tests.
Add crates/nu-cli/tests/format_conversions/eml.rs
- Create the tests that get run against my command. This includes actual Nushell commands to be run, testing integration of
from-eml
into Nu. For example,open sample.eml | get To
.
Modify crates/nu-cli/tests/format_conversions/mod.rs
- Expose the above
eml.rs
format conversion tests