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
FromEMLstruct - Create ancillary
FromEMLArgsstruct 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_previewin my crate. - Implement
WholeStreamCommandfor 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_emlfunction – the name isn’t required to conform to the command, as it gets called in theWholeStreamCommand::runimpl.
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-jsoncommand) 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
FromEMLArgsappear 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
Subjectwith its corresponding value; it has aTofield that may contain one or more structured values; and so on. So I’m using theTaggedDictBuilderto collect these values. This collection uses key-value pairs, but it also maintains ordering of values inserted.TaggedDictBuildermaps a RustStringto 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
Tagis 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 theTaggedDictBuilderand my structured data, I’m effectively linking the Nushell user’s invocation offrom-emland 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
FromEMLis a whole stream command
Modify crates/nu-cli/src/commands.rs
- Expose the
from_emlmodule and theFromEMLstruct 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.emlfile 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-emlinto Nu. For example,open sample.eml | get To.
Modify crates/nu-cli/tests/format_conversions/mod.rs
- Expose the above
eml.rsformat conversion tests