logo
Published on

Building a Unix-shell in Rust - Part 2

Authors

In the first post of this series, we laid the groundwork by discussing what a shell is and how it works. If you missed it, I suggest starting from there to get the full picture. Now, let’s dive into the next steps: reading user input, parsing that input into commands, and classifying those commands to execute the appropriate actions.

Reading User Input

To start, we need our shell to read user input. This is the first step in our REPL (Read-Eval-Print-Loop) process. Here’s how we can achieve this in Rust:

use std::io::{stdin, stdout, Write};

use anyhow::Result;

fn main() -> Result<()> {
    loop {
        // use the `>` character as the prompt
        // need to explicitly flush this to ensure it prints before read_line
        print!("> ");
        stdout().flush()?;

        let mut input = String::new();
        stdin().read_line(&mut input).unwrap();
    }

    Ok(())
}

Breaking It Down

  1. Prompting the User: We use the > character as the prompt. This signals to the user that the shell is ready for input.

  2. Flushing stdout: stdout().flush()?; ensures that the prompt is printed immediately, not buffered. This is crucial for a good user experience.

  3. Reading Input: We read the user input into a mutable String variable called input using stdin().read_line(&mut input).unwrap();. Even though this aproach of reading the user input is very bare bones, as the user cannot edit it while typing, not having the command history, etc. It is sufficient for now and it will be expanded in the future.

By this point, we can display a prompt and capture user input. Let’s move on to parsing this input.

Parsing the Input into Commands

Once we have the user input, we need to parse it into a command and its arguments. This helps us determine what action to take.

First, we trim the input using input.trim(), which removes any leading or trailing whitespace from the input string. Next, we split the input using split_ascii_whitespace(), which splits the input into an iterator of substrings separated by ASCII whitespace. To extract the command, we use parts.next(), which retrieves the first substring. If there’s no command (empty input), the loop continues. Finally, we extract the arguments by capturing the remaining substrings as the command’s arguments with let args = parts;.

Here is the code that does this process:

let mut parts = input.trim().split_ascii_whitespace();
let command;

match parts.next() {
    Some(com) => command = com,
    None => continue,  // continue if no command provided
}

let args = parts;

Classifying Commands

With the command and arguments parsed, we need to classify and handle them appropriately.

To do this, we pass the command into a match block and check if it matches a built-in command. If it doesn’t, we assume the command is external and execute it. If it turns out not to be a correct command, we display an error message and restart the loop.

match command {
    "cd" => cd(args),
    "exit" => break,
    command => {
        let child = Command::new(command)
            .args(args)
            .spawn();

        match child {
            Ok(mut child) => { child.wait()?; },
            Err(e) => eprintln!("{}", e)
        };
    }
}

For external commands, Command::new(command).args(args).spawn() tries to execute them. If the command executes successfully, child.wait()? waits for it to finish.

If the command is a built-in one, it is necessary to create a function that implements its functionality and call it with the arguments as parameters, like the cd function in the code above.

Conclusion

We’ve covered a lot in this post, from reading user input to parsing it into commands and arguments, and finally classifying and executing those commands. Our shell is taking shape, and we now have a basic REPL loop that can handle both built-in and external commands.

In the next post, we’ll dive deeper into implementing more built-in commands and improving the user experience. Stay tuned, and happy coding!

Next Steps

In the next post, we’ll focus on refactoring the code to improve readability, maintainability, and extend functionality. Stay tuned!