[Translation] Rust build your own using Shell

  • This is a tutorial using Rust build their own shell, and has been included in the build-your-own-x list. Create yourself a shell is a good way to understand the shell, terminal emulator, such as OS and work together.

A shell is what?

  • A shell is a program that can be used to control your computer. This greatly simplifies start the application. But the shell itself is not an interactive application.
  • Most users to interact through a shell and a terminal emulator. Q Ubuntu user community geirha definition of the terminal emulator as follows:

Terminal emulator (commonly referred to as terminal) is a "window", yes, it runs a text-based application, by default, it is your login shell (ie under Ubuntu bash). When you type characters in the window, in addition to stdin terminal sends these characters to the shell (or other programs), but also to draw these characters in the window. shell to stdout and stderr character is sent to the terminal, the terminal to draw the characters in the window.

  • In this tutorial, we will write your own shell, and run it (the place is usually run in the cargo) in the common terminal emulator.

Start simple

  • The most simple shell Rust only a few lines of code. Here we create a new string to save user input. stdin().read_lineUser input will block until the user presses the Enter key, then the user will input the entire content (including blank lines Enter key) is written string. Use input.trim()delete line breaks and other whitespace, we try it.
fn main(){
    let mut input = String::new();
    stdin().read_line(&mut input).unwrap();

    // read_line leaves a trailing newline, which trim removes
    // read_line 会在最后留下一个换行符,在处理用户的输入后会被删除
    let command = input.trim(); 

    Command::new(command)
        .spawn()
        .unwrap();
}
  • After running this operation, you should see your terminal is waiting for a blinking cursor input. Try typing ls and press Enter, you will see the ls command to print the contents of the current directory, then the shell will be launched.
  • Note: This example is not in Rust Playground running, because it currently does not support running and processing stdin and so takes a long time to wait.

Receiving a plurality of commands

  • We do not want to exit the shell when the user enters a single command. Support for multiple command mainly to the above code in a package loopand add a call waitto wait for the processing of each sub-command to ensure that we are not before the current deal is completed, the user is prompted to enter additional information. I also added a few lines to print characters >, to make it easier to distinguish between the input and output area of his command during processing.
fn main(){
    loop {
        // use the `>` character as the prompt
        // 使用 `>` 作为提示
        // need to explicitly flush this to ensure it prints before read_line
        // 需要显式地刷新它,这样确保它在 read_line 之前打印
        print!("> ");
        stdout().flush();

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

        let command = input.trim();

        let mut child = Command::new(command)
            .spawn()
            .unwrap();

        // don't accept another command until this one completes
        // 在这个命令处理完之前不再接受新的命令
        child.wait(); 
    }
}
  • After running this code, you will see that after running the first command, displays a prompt so that you can enter the second command. Use lsand pwdcommands to try it.

Parameter Handling

  • If you try to run shell commands on the above ls -a, it will collapse. Because it does not know how to handle parameters, it attempts to run a named ls -acommand, but the correct behavior is to use parameters -ato run a named lscommand.
  • The user input is split by a space character, and the contents before the first name space as a command (e.g. ls), and the contents of a space after the first pass to the command (e.g. as a parameter -a), the following problem it will be resolved.
fn main(){
    loop {
        print!("> ");
        stdout().flush();

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

        // everything after the first whitespace character 
        //     is interpreted as args to the command
        // 第一个空白符之后的所有内容都视为命令的参数
        let mut parts = input.trim().split_whitespace();
        let command = parts.next().unwrap();
        let args = parts;

        let mut child = Command::new(command)
            .args(args)
            .spawn()
            .unwrap();

        child.wait();
    }
}

shell built-in features

  • It turns out, shell can not simply be assigned certain commands to another process. There is some logic required within the shell provides, therefore, must be implemented by the shell itself.
  • Perhaps the most common example is the cdcommand. To understand why cd must be a shell built-in features, please see this link . Processing built-in command, in fact, it is called cda program. Here explanation about this duality.
  • Let's add a shell built-in features to our cd function of the shell
fn main(){
    loop {
        print!("> ");
        stdout().flush();

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

        let mut parts = input.trim().split_whitespace();
        let command = parts.next().unwrap();
        let args = parts;

        match command {
            "cd" => {
                // 如果没有提供路径参数,则默认 '/' 路径
                let new_dir = args.peekable().peek().map_or("/", |x| *x);
                let root = Path::new(new_dir);
                if let Err(e) = env::set_current_dir(&root) {
                    eprintln!("{}", e);
                }
            },
            command => {
                let mut child = Command::new(command)
                    .args(args)
                    .spawn()
                    .unwrap();

                child.wait();
            }
        }
    }
}

Error Handling

  • If you see here, you may find that if you enter a command that does not exist, the above shell will collapse. In the following version, the output to the user via an error prompt, and then allow them to enter a new command, can solve this problem.
  • Due to enter a wrong command is a simple way to exit the shell, so I also realized another shell built-in function, which is the exitcommand.
fn main(){
    loop {
        print!("> ");
        stdout().flush();

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

        let mut parts = input.trim().split_whitespace();
        let command = parts.next().unwrap();
        let args = parts;

        match command {
            "cd" => {
                let new_dir = args.peekable().peek().map_or("/", |x| *x);
                let root = Path::new(new_dir);
                if let Err(e) = env::set_current_dir(&root) {
                    eprintln!("{}", e);
                }
            },
            "exit" => return,
            command => {
                let child = Command::new(command)
                    .args(args)
                    .spawn();

                // 优雅地处理非正常输入
                match child {
                    Ok(mut child) => { child.wait(); },
                    Err(e) => eprintln!("{}", e),
                };
            }
        }
    }
}

Pipe character

  • If the shell is not the function of the pipeline operator, it is difficult for the actual production environment. If you are not familiar with this feature, you can use |the character to tell the shell to the output of the first command to redirect the input of the second command. For example, running ls | grep Cargowill trigger the following actions:

    • ls It will list all files and directories in the current directory
    • The shell above the list of files and directories to enter through the pipeline grep
    • grepThis list will be filtered, and only the output file name contains the character Cargofile
  • Again we were the last iteration of the shell, including basic support for the pipeline. To understand the IO pipes and redirection of other features, you can refer to this article

fn main(){
    loop {
        print!("> ");
        stdout().flush();

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

        // must be peekable so we know when we are on the last command
        // 必须是可以 peek 的,这样我们才能确定何时结束
        let mut commands = input.trim().split(" | ").peekable();
        let mut previous_command = None;

        while let Some(command) = commands.next()  {

            let mut parts = command.trim().split_whitespace();
            let command = parts.next().unwrap();
            let args = parts;

            match command {
                "cd" => {
                    let new_dir = args.peekable().peek()
                        .map_or("/", |x| *x);
                    let root = Path::new(new_dir);
                    if let Err(e) = env::set_current_dir(&root) {
                        eprintln!("{}", e);
                    }

                    previous_command = None;
                },
                "exit" => return,
                command => {
                    let stdin = previous_command
                        .map_or(
                            Stdio::inherit(),
                            |output: Child| Stdio::from(output.stdout.unwrap())
                        );

                    let stdout = if commands.peek().is_some() {
                        // there is another command piped behind this one
                        // prepare to send output to the next command
                        // 在这个命令后还有另一个命令,准备将其输出到下一个命令
                        Stdio::piped()
                    } else {
                        // there are no more commands piped behind this one
                        // send output to shell stdout
                        // 在发送输出到 shell 的 stdout 之后,就没有命令要执行了
                        Stdio::inherit()
                    };

                    let output = Command::new(command)
                        .args(args)
                        .stdin(stdin)
                        .stdout(stdout)
                        .spawn();

                    match output {
                        Ok(output) => { previous_command = Some(output); },
                        Err(e) => {
                            previous_command = None;
                            eprintln!("{}", e);
                        },
                    };
                }
            }
        }

        if let Some(mut final_command) = previous_command {
            // block until the final command has finished
            // 阻塞一直到命令执行完成
            final_command.wait();
        }

    }
}

Epilogue

  • In less than 100 lines of code, we created a shell, which can be used in many daily operations, but a real shell will have more features and functionality. GNU website has an online manual on the bash shell, including shell characteristic of the list, which is started studying more advanced features of a good place.

  • Please note that this is a learning project for me, between simplicity and robustness need to weigh the case, I chose simplicity.

  • The shell can project my GitHub found on. As of this writing, the latest submission is a47640. Another study Rust shell items you might be interested in Rush

Guess you like

Origin www.cnblogs.com/ishenghuo/p/12550142.html