【譯】使用 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM