【译】使用 Rust 构建你自己的 Shell

栏目: 编程语言 · Rust · 发布时间: 6年前

内容简介:正文开始终端模拟器(通常简称为终端)就是一个“窗口”,是的,它运行一个基于文本的程序,默认情况下,它就是你登陆的 shell (也就是 Ubuntu 下的 bash)。当你在窗口中键入字符时,终端除了将这些字符发送到 shell (或其他程序)的 stdin 之外,还会在窗口中绘制这些字符。 shell 输出到 stdout 和 stderr 的字符被发送到终端,终端在窗口中绘制这些字符。

正文开始

  • 这是一个使用 Rust 构建自己的 shell 的教程,已经被收录在 build-your-own-x 列表中。自己创建一个 shell 是理解 shell、终端模拟器、以及 OS 等协同工作的好办法。

shell 是什么?

  • shell 是一个程序,它可以用于控制你的计算机。这在很大程度上简化了启动应用程序。但 shell 本身并不是一个交互式应用程序。
  • 大多数用户通过终端模拟器来和 shell 交互。用户geirha 对终端模拟器的形容如下:

终端模拟器(通常简称为终端)就是一个“窗口”,是的,它运行一个基于文本的程序,默认情况下,它就是你登陆的 shell (也就是 Ubuntu 下的 bash)。当你在窗口中键入字符时,终端除了将这些字符发送到 shell (或其他程序)的 stdin 之外,还会在窗口中绘制这些字符。 shell 输出到 stdout 和 stderr 的字符被发送到终端,终端在窗口中绘制这些字符。

  • 在本教程中,我们将编写自己的 shell ,并在普通的终端模拟器(通常在 cargo 运行的地方)中运行它。

从简单开始

  • 最简单的 shell 只需要几行 Rust 代码。这里我们创建一个新字符串,用于保存用户输入。 stdin().read_line 将会在用户输入处阻塞,直到用户按下回车键,然后它将整个用户输入的内容(包括回车键的空行)写入字符串。使用 input.trim() 删除换行符等空行,我们尝试在命令行中运行它。
fn main(){
    let mut input = String::new();
    stdin().read_line(&mut input).unwrap();

    // read_line leaves a trailing newline, which trim removes
    let command = input.trim(); 

    Command::new(command)
        .spawn()
        .unwrap();
}
复制代码
  • 运行此操作后,你应该会在你的终端中看到一个正在等待输入的闪烁光标。尝试键入 ls 并回车,你将看到 ls 命令打印当前目录的内容,然后 shell 将推出。
  • 注意:这个例子不能在Rust Playground 上运行,因为它目前不支持 stdin 等需要长时间等待的运行和处理。

接收多个命令

  • 我们不希望在用户输入单个命令后退出 shell。支持多个命令主要是将上面的代码封装在一个 loop 中,并添加调用 wait 来等待每个子命令的处理,以确保我们不会在当前处理完成之前,提示用户输入额外的信息。我还添加了几行来打印字符 > ,以便用户更容易的将他的输入与处理命令过程中的输出区分开来。
fn main(){
    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();

        let command = input.trim();

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

        // don't accept another command until this one completes
        child.wait(); 
    }
}
复制代码
  • 运行这段代码后,你将看到在运行第一个命令之后,会显示一个提示符,以便你可以输入第二个命令。使用 lspwd 命令来尝试一下吧。

参数处理

  • 如果你尝试在上面的 shell 上运行命令 ls -a ,它将会崩溃。因为它不知道怎么处理参数,它尝试运行一个名为 ls -a 的命令,但正确的行为是使用参数 -a 运行一个名为 ls 的命令。
  • 通过将用户输入拆分为空格字符,并将第一个空格之前的内容作为命令的名称(例如 ls ),而将第一个空格之后的内容作为参数传递给该命令(例如 -a ),这个问题在下面就会解决。
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 的内建功能

  • 事实证明, shell 不能简单的将某些命令分派给另一个进程。这些都是影响 shell 内部,所以,必须由 shell 本身实现。
  • 最常见的例子可能就是 cd 命令。要了解为什么 cd 必须是 shell 的内建功能,请查看这个链接。处理内建的命令,实际上是一个名为 cd 的程序。这里有关于这种二象性的解释。
  • 下面我们添加 shell 内建功能 cd 功能到我们的 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" => {
                // default to '/' as new directory if one was not provided
                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();
            }
        }
    }
}
复制代码

错误处理

exit
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();

                // gracefully handle malformed user input
                match child {
                    Ok(mut child) => { child.wait(); },
                    Err(e) => eprintln!("{}", e),
                };
            }
        }
    }
}
复制代码

管道符

  • 如果没有管道操作符的功能的 shell 是很难用于实际生产环境的。如果你不熟悉这个特性,可以使用 | 字符告诉 shell 将第一个命令的结果输出重定向到第二个命令的输入。例如,运行 ls | grep Cargo 会触发以下操作:

    • ls 将列出当前目录中的所有文件和目录
    • shell 将通过管道将以上的文件和目录列表输入到 grep
    • grep 将过滤这个列表,并只输出文件名包含字符 Cargo 的文件
  • shell 的最后一次迭代包括了对管道的基础支持。要了解管道和 IO 重定向的其他功能,可以参考这个文章

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
        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
                        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();
        }

    }
}
复制代码

结语

  • 在不到 100 行的代码中,我们创建了一个 shell ,它可以用于许多日常操作,但是一个真正的 shell 会有更多的特性和功能。GNU 网站有一个关于 bash shell 的在线手册,其中包括了shell 特性的列表,这是着手研究更高级功能的好地方。

  • 请注意,这对我来说是一个学习的项目,在简单性和健壮性之间需要权衡的情况下,我选择简单性。

  • 这个 shell 项目可以在我的 GitHub 上找到。在撰写本文时,最新提交是 a47640 。另一个你可能感兴趣的学习 Rust shell 项目是 Rush


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Where Wizards Stay Up Late

Where Wizards Stay Up Late

Katie Hafner / Simon & Schuster / 1998-1-21 / USD 16.00

Twenty five years ago, it didn't exist. Today, twenty million people worldwide are surfing the Net. "Where Wizards Stay Up Late" is the exciting story of the pioneers responsible for creating the most......一起来看看 《Where Wizards Stay Up Late》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具