【譯】使用 Rust 構建你本身的 Shell

正文開始html

  • 這是一個使用 Rust 構建本身的 shell 的教程,已經被收錄在 build-your-own-x 列表中。本身建立一個 shell 是理解 shell、終端模擬器、以及 OS 等協同工做的好辦法。

shell 是什麼?

  • shell 是一個程序,它能夠用於控制你的計算機。這在很大程度上簡化了啓動應用程序。但 shell 自己並非一個交互式應用程序。
  • 大多數用戶經過終端模擬器來和 shell 交互。用戶 geirha 對終端模擬器的形容以下:

終端模擬器(一般簡稱爲終端)就是一個「窗口」,是的,它運行一個基於文本的程序,默認狀況下,它就是你登錄的 shell (也就是 Ubuntu 下的 bash)。當你在窗口中鍵入字符時,終端除了將這些字符發送到 shell (或其餘程序)的 stdin 以外,還會在窗口中繪製這些字符。 shell 輸出到 stdout 和 stderr 的字符被髮送到終端,終端在窗口中繪製這些字符。node

  • 在本教程中,咱們將編寫本身的 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();
            }
        }
    }
}
複製代碼

錯誤處理

  • 若是你看到這兒,你可能會發現,若是你輸入一個不存在的命令,上面的 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();

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

管道符

  • 若是沒有管道操做符的功能的 shell 是很難用於實際生產環境的。若是你不熟悉這個特性,可使用 | 字符告訴 shell 將第一個命令的結果輸出重定向到第二個命令的輸入。例如,運行 ls | grep Cargo 會觸發如下操做:git

    • ls 將列出當前目錄中的全部文件和目錄
    • shell 將經過管道將以上的文件和目錄列表輸入到 grep
    • grep 將過濾這個列表,並只輸出文件名包含字符 Cargo 的文件
  • shell 的最後一次迭代包括了對管道的基礎支持。要了解管道和 IO 重定向的其餘功能,能夠參考這個文章github

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

  • 請注意,這對我來講是一個學習的項目,在簡單性和健壯性之間須要權衡的狀況下,我選擇簡單性。ubuntu

  • 這個 shell 項目能夠在個人 GitHub 上找到。在撰寫本文時,最新提交是 a47640 。另外一個你可能感興趣的學習 Rust shell 項目是 Rushbash

相關文章
相關標籤/搜索