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 function with_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 the WholeStreamCommand::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 as open filename.json --raw | from-json (explicitly passing raw JSON data to the from-json command) or directly as open filename.json (implicitly calling from-json) will read in the entire file contents and pass it to you in a RunnableContext. You can collect the string for your own data manipulation.
  • Command arguments, such as my FromEMLArgs appear to require Serde Deserialize, and if the arg contains a hyphen, you should use Serde’s rename(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 a To field that may contain one or more structured values; and so on. So I’m using the TaggedDictBuilder to collect these values. This collection uses key-value pairs, but it also maintains ordering of values inserted. TaggedDictBuilder maps a Rust String to a Nushell Value – 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 the TaggedDictBuilder and my structured data, I’m effectively linking the Nushell user’s invocation of from-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 the FromEML 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