nushell/docs/implementing_a_command.md
2021-04-16 08:11:26 -05:00

10 KiB

Implementing a Command

We will be learning how to write a command and see what's required to do so in small steps. For this example we are interested writing a command named take-two that takes two lucky values coming in.

Let's generate a table with row literal syntax first, like so:

> echo [[repository, LOC]; [nushell, 11161984] [scripts, 500] [nushell_book, 200]]
──────────────┬──────────
 repository   │ LOC
──────────────┼──────────
 nushell      │ 11161984
 scripts      │      500
 nushell_book │      200
──────────────┴──────────

Note: values are passed around in the stream. A generated table like the example above is three row values. See the values chapter to learn more about row value types.

We want to take-two rows from the generated table. Here is what we would like to do:

> echo [[repository, LOC]; [nushell, 11161984] [scripts, 500] [nushell_book, 200]] | take-two
──────────────┬──────────
 repository   │ LOC
──────────────┼──────────
 nushell      │ 11161984
 scripts      │      500
──────────────┴──────────

See the take-two command in the pipeline above? That's what we want.

Before starting writing the command it's important to understand that everything in the stream are values. Sometimes you will see a table with no columns and the reason is that none of the values coming in are row value types. For this case we call it simply a list, like so:

> echo [[repository, LOC]; [nushell, 11161984] [scripts, 500] [nushell_book, 200]] | get repository
──────────────
 nushell
 scripts
 nushell_book
──────────────

The example above Nu sees three values of type String (the call get repository returns them).

What happens if the stream has row value types and other value types? Let's find out:

> echo [[repository, LOC]; [nushell, 11161984] [scripts, 500] [nushell_book, 200]] | append 10
──────────────┬──────────
 repository   │ LOC
──────────────┼──────────
 nushell      │ 11161984
 scripts      │      500
 nushell_book │      200
──────────────┴──────────
────
 10
────

When the pipeline finishes, as soon as Nu sees a value that is of type row it will try to display it as a table with columns. The reason is that a value of type row represents rows having column cell headers and cell values.

We appended the value 10 using append. Nu sees four values in the stream, three of them being row types and one being an int value type. Therefore we see a table printed (the three row values) and a list (one int value) from the stream. This matters because some commands expect and assume all values coming in are row types and some do not.

For the command take-two we are about to implement we don't care what kind of values are coming in since we just want to take two values.

Command implementation steps

To guide us in our command implementation journey we will start registering the command, as we do so we will be driving out the command.

Register the command

All the internal commands reside in crates/nu-command. For our command named take-two we can create the source file take_two.rs and place it in src/commands/take_two.rs. Go ahead and create it:

> touch crates/nu-command/src/commands/take_two.rs

Edit the source file and add the following:

pub struct Command;

Before registering it to the context you need to make it available by declaring the module and re-export it in crates/nu-command/src/commands.rs. The commands we write are modules already (take_two.rs is the take_two module). In case you are not familiar with rust modules read this refresher. Let's go ahead and declare it and then re-export the command's Command struct as TakeTwo, like so:

nu-command/src/commands.rs

# ...
pub(crate) mod take_two;
# ...

# ...
pub(crate) use take_two::Command as TakeTwo;
# ...

We should be ready now to register it. You need to add the command (in this case the struct will be in scope named as TakeTwo from the previous snippet) where they are added one by one to the context.

src/commands/default_context.rs

context.add_commands(vec![
    # ...
    whole_stream_command(TakeTwo),
    # ...
);

We will now resort to the rust compiler to make sure things are wired up correctly. Under crates/nu-command run:

> cargo build
   Compiling nu-command v0.29.2 (/Users/nu/nushell/crates/nu-command)
error[E0277]: the trait bound `take_two::Command: nu_engine::WholeStreamCommand` is not satisfied
   --> crates/nu-command/src/commands/default_context.rs:13:34
    |
13  |             whole_stream_command(TakeTwo),
    |                                  ^^^^^^^ the trait `nu_engine::WholeStreamCommand` is not implemented for `take_two::Command`
    |
   ::: /Users/nu/nushell/crates/nu-engine/src/whole_stream_command.rs:273:43
    |
273 | pub fn whole_stream_command(command: impl WholeStreamCommand + 'static) -> Command {
    |                                           ------------------ required by this bound in `whole_stream_command`

error[E0277]: the trait bound `take_two::Command: nu_engine::WholeStreamCommand` is not satisfied
   --> crates/nu-command/src/commands/default_context.rs:13:34
    |
13  |             whole_stream_command(TakeTwo),
    |                                  ^^^^^^^ the trait `nu_engine::WholeStreamCommand` is not implemented for `take_two::Command`
    |
   ::: /Users/nu/nushell/crates/nu-engine/src/whole_stream_command.rs:273:43
    |
273 | pub fn whole_stream_command(command: impl WholeStreamCommand + 'static) -> Command {
    |                                           ------------------ required by this bound in `whole_stream_command`

error: aborting due to previous error

That's not certainly what we expected.

It happens that Nu needs to know more about the command, including it's name and signature. We didn't do any of that and the compiler is telling us so. We seem to be missing implementing the WholeStreamCommand trait. This is the trait that, when implemented, allows us to state it's name, signature, and the like.

WholeStreamCommand

Every internal command must implement the WholeStreamCommand trait. Let's take a look at the required functions:

pub trait WholeStreamCommand: Send + Sync {
    fn name(&self) -> &str;
    fn signature(&self) -> Signature;
    fn usage(&self) -> &str;
}

Note: For simplicity, we've removed the default implementations of the trait. If you want to read more about the trait you can read the code here

We will need to implement the required functions name for telling Nu the name of the command, signature to tell the command's signature, and usage. Before we start, we will also write an example of the usage implementing examples (not shown above).

Ready? Go ahead and edit crates/nu-command/src/commands/take_two.rs:

use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_protocol::Signature;

pub struct Command;

impl WholeStreamCommand for Command {
    fn name(&self) -> &str {
        "take-two"
    }

    fn usage(&self) -> &str {
        "takes two values"
    }

    fn signature(&self) -> Signature {
        Signature::build("take-two")
    }

    fn examples(&self) -> Vec<Example> {
        vec![
            Example {
                description: "Takes two values",
                example: "echo [11161984 500 200] | take-two",
                result: None,
            },
        ]
    }
}

Let's try again by running cargo build:

cargo build
   Compiling nu-command v0.29.2 (/Users/nu/nushell/crates/nu-command)
    Finished dev [unoptimized + debuginfo] target(s) in 10.56s

Great!

Since we have been working in crates/nu-command and building there to shorten the build time as we work on the command, you may want to run a full build now by doing cargo build --all --features=extra:

Let's try entering help take-two:

> help take-two
takes two values

Usage:
  > take-two {flags}

Flags:
  -h, --help: Display this help message

Examples:
  Takes two values
  > echo [11161984 500 200] | take-two

We get a help description of our newly implemented take-two command for free. This works because we implemented examples. It's good practice to do so for documentation (and as we will soon see, unit testing) purposes.

Now let's run echo [11161984 500 200] | take-two

> echo [11161984 500 200] | take-two
error: Error: Unimplemented: take-two does not implement run or run_with_actions

That was unexpected. What happened?

We did not implement take-two's logic anywhere. Nu is telling us the command does not implement run or run_with_actions. These functions are in the WholeStreamCommand trait and the default implementation gives the error. Since our command won't be doing any actions we can go ahead and implement run for the logic, let's do that:

use nu_errors::ShellError;
# ...

    fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
        Ok(args.input.take(2).to_output_stream())
    }

# ...

Compile and try one more time:

> echo [11161984 500 200] | take-two
───┬──────────
 0 │ 11161984
 1 │      500
───┴──────────

Not bad. We created first a list of int values and took two of them. Let's create row type values and take two of them now:

> echo [[LOC]; [11161984] [500] [200]] | take-two
───┬──────────
 # │ LOC
───┼──────────
 0 │ 11161984
 1 │      500
───┴──────────

The command is ready.