- Build Your Own Shell using Rust 譯文
- 原文地址:https://www.joshmcguigan.com/blog/build-your-own-shell-rust/
- 原文作者:Josh Mcguigan
- 譯文出自:https://github.com/suhanyujie/article-transfer-rs
- 本文永久鏈接: https://github.com/suhanyujie/article-transfer-rs/blob/master/src/2019/Build_Your_Own_Shell_using_Rust.md
- 譯者:suhanyujie
- tips:水平有限,翻譯不當之處,還請指正,謝謝!
- 這是一個使用 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();
}
}
- 運行這段代碼后,你將看到在運行第一個命令之后,會顯示一個提示符,以便你可以輸入第二個命令。使用
ls
和pwd
命令來嘗試一下吧。
參數處理
- 如果你嘗試在上面的 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();
}
}
}