zoukankan      html  css  js  c++  java
  • 【译】使用 Rust 构建你自己的 Shell

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

    shell 是什么?

    • shell 是一个程序,它可以用于控制你的计算机。这在很大程度上简化了启动应用程序。但 shell 本身并不是一个交互式应用程序。
    • 大多数用户通过终端模拟器来和 shell 交互。Ubuntu 问答社区的用户 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
        // read_line 会在最后留下一个换行符,在处理用户的输入后会被删除
        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
            // 需要显式地刷新它,这样确保它在 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" => {
                    // 如果没有提供路径参数,则默认 '/' 路径
                    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();
                }
            }
        }
    }
    

    错误处理

    • 如果你看到这儿,你可能会发现,如果你输入一个不存在的命令,上面的 shell 将会崩溃。在下面的版本中,通过给用户输出报错提示,然后允许他们输入一个新的命令,可以很好地解决这个问题。
    • 由于输入一个错误的命令是退出 shell 的一个简单方法,所以我还实现了另一个 shell 内建功能,也就是 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();
    
                    // 优雅地处理非正常输入
                    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
            // 必须是可以 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();
            }
    
        }
    }
    

    结语

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

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

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

  • 相关阅读:
    bootstrap-图片样式记录
    关于json数据中的多反斜杆转译--StringEscapeUtils.unescapeJava(踩过的坑)
    Nginx与tomcat组合的简单使用
    多进程之间的互斥信号量的实现(Linux和windows跨平台)
    跨平台(win和unix)的线程封装类
    linux 静态库、共享库
    WinMain与wWinMain,win32的字符集问题
    linux下添加动态链接库路径、动态库加载等方法
    win系统动态载入DLL所需要的三个函数详解(LoadLibrary,GetProcAddress,FreeLibrary)
    dll程序开发总结
  • 原文地址:https://www.cnblogs.com/ishenghuo/p/12550142.html
Copyright © 2011-2022 走看看