Rust 提供了強大的系統程式設計能力,使開發 shell 程式成為可能。透過 std::process::Command 可以建立和管理行程,nix 套件則提供了訊號處理功能,而 Rust 的 I/O 函式庫則支援讀取使用者輸入和檔案操作。結合這些功能,我們可以逐步建構一個功能完善的 shell 程式。需要注意的是,錯誤處理和訊號處理在系統程式設計中至關重要,Rust 的錯誤處理機制和 nix 套件提供的訊號處理功能可以幫助我們編寫更穩健的程式碼。

使用 Rust 編寫基本 Shell 程式

透過本章的學習,您將具備使用 Rust 編寫基本 Shell 程式的能力。Shell 程式是一種特殊的程式,它提供了一個使用者介面來與作業系統互動,允許使用者執行命令、管理檔案和控制程式。

Rust 中的行程管理與 I/O 操作

在系統程式設計中,建立新的行程是一項基本且重要的功能。Rust 語言透過 std::process::Command 結構體提供了一個方便且跨平台的方式來與行程互動。本文將詳細介紹如何在 Rust 中建立新的行程並管理其 I/O 操作。

建立新行程

要建立新的行程,首先需要建立一個 Command 物件。這物件代表了要執行的程式及其引數。可以使用 Command::new 方法來建立新的 Command 物件,並指定程式名稱和必要的引數。

use std::process::Command;

fn main() {
    let mut cmd = Command::new("ls");
    cmd.arg("-l");
    // ...
}

內容解密:

  • Command::new("ls") 建立了一個新的 Command 物件,代表要執行的 ls 程式。
  • cmd.arg("-l")ls 程式增加了一個 -l 引數,用於顯示詳細的檔案列表。

組態行程

可以透過新增引數、設定環境變數、組態工作目錄等方式來修改 Command 物件,以自定義子行程的行為。

use std::process::Command;

fn main() {
    let mut cmd = Command::new("ls");
    cmd.arg("-l").current_dir("/path/to/directory");
    // ...
}

內容解密:

  • cmd.arg("-l") 增加了 -l 引數。
  • current_dir("/path/to/directory") 設定了子行程的工作目錄。

建立子行程

組態好 Command 物件後,可以呼叫 spawn 方法來建立子行程。此方法傳回一個 std::process::Child 物件,代表正在執行的子行程。

use std::process::Command;

fn main() {
    let mut cmd = Command::new("ls");
    cmd.arg("-l").current_dir("/path/to/directory");
    let child = cmd.spawn().expect("Failed to start child process");
    // ...
}

內容解密:

  • cmd.spawn() 建立了子行程,並傳回一個 Child 物件。
  • .expect("Failed to start child process") 用於處理建立子行程失敗的情況。

管理子行程

可以透過等待子行程完成或與其進行標準輸入、輸出和錯誤串流的通訊來與子行程互動。Child 物件提供瞭如 waitstdinstdoutstderr 等方法來實作這些功能。

use std::process::{Command, Stdio};

fn main() {
    let mut cmd = Command::new("echo");
    cmd.arg("Hello, Rust!").stdout(Stdio::piped());
    let child = cmd.spawn().expect("Failed to start child process");
    let output = child.wait_with_output().expect("Failed to wait for child process");
    println!("Child process exited with: {:?}", output.status);
    println!("Child process output: {}", String::from_utf8_lossy(&output.stdout));
}

內容解密:

  • cmd.arg("Hello, Rust!") 設定了 echo 程式的引數。
  • .stdout(Stdio::piped()) 將子行程的標準輸出導向管道,以便父行程讀取。
  • child.wait_with_output() 等待子行程完成,並捕捉其輸出。

I/O 操作

I/O 操作是系統程式設計中的基本導向。Rust 提供了強大的工具來管理各種 I/O 操作,包括標準串流、檔案操作等。

標準串流

Rust 提供了三個標準 I/O 串流:stdin 用於讀取輸入、stdout 用於寫入正常輸出、stderr 用於寫入錯誤輸出。這些串流分別由 std::io::Stdinstd::io::Stdoutstd::io::Stderr 型別代表。

use std::io;

fn main() {
    let mut input = String::new();
    println!("Please enter your name: ");
    io::stdin().read_line(&mut input).expect("Failed to read line");
    println!("Hello, {}!", input.trim());
}

內容解密:

  • io::stdin().read_line(&mut input) 從標準輸入讀取一行文字。
  • .expect("Failed to read line") 處理讀取失敗的情況。

寫入輸出

可以使用 std::io::stdout()std::io::stderr() 函式來寫入輸出或錯誤訊息。

use std::io::{self, Write};

fn main() {
    let stdout = io::stdout();
    let mut handle = stdout.lock();
    writeln!(handle, "This is a line of text").expect("Failed to write to stdout");
}

內容解密:

  • let stdout = io::stdout(); 取得標準輸出的控制程式碼。
  • let mut handle = stdout.lock(); 鎖定標準輸出,以確保同步寫入。
  • writeln!(handle, "This is a line of text") 寫入一行文字到標準輸出。

Rust 中的輸入/輸出處理與錯誤管理

Rust 語言提供了強大的輸入/輸出(I/O)處理能力,包括檔案操作、行程間通訊以及錯誤處理等。本章節將探討 Rust 中的 I/O 處理機制,並介紹如何有效地管理錯誤和訊號,以建立穩健可靠的應用程式。

同步輸出與鎖定機制

在多執行緒環境中,為了避免多個執行緒同時寫入標準輸出(stdout)而導致輸出的混亂,Rust 提供了鎖定機制來確保輸出的一致性。當使用鎖定機制時,其他執行緒無法寫入 stdout,直到鎖定被釋放。

程式碼範例:使用鎖定機制寫入 stdout

use std::io::{self, Write};

fn main() -> io::Result<()> {
    let stdout = io::stdout();
    let mut handle = stdout.lock();
    handle.write_all(b"Hello, Rust!")?;
    Ok(())
}

內容解密:

  1. io::stdout():取得標準輸出的控制程式碼。
  2. stdout.lock():對標準輸出進行鎖定,確保在鎖定期間,其他執行緒無法寫入。
  3. handle.write_all(b"Hello, Rust!")?:將位元組字串寫入標準輸出。

檔案輸入/輸出

Rust 的 std::fs 模組提供了豐富的檔案操作功能,包括開啟、讀取、寫入和操作檔案。透過 File::openFile::create 方法,可以開啟檔案進行讀取或寫入操作。

程式碼範例:基本檔案操作

use std::fs::File;
use std::io::{self, Read, Write};

fn main() -> io::Result<()> {
    let mut file = File::create("example.txt")?;
    file.write_all(b"Hello, Rust!")?;
    
    let mut contents = String::new();
    File::open("example.txt")?.read_to_string(&mut contents)?;
    
    println!("File contents: {}", contents);
    Ok(())
}

內容解密:

  1. File::create("example.txt")?:建立一個新的檔案 example.txt,若失敗則傳回錯誤。
  2. file.write_all(b"Hello, Rust!")?:將字串寫入檔案。
  3. File::open("example.txt")?.read_to_string(&mut contents)?;:開啟檔案並讀取其內容至字串變數 contents

行程間通訊

Rust 允許透過 std::process::Command 建立子行程,並與其進行通訊,包括讀取子行程的輸出。

程式碼範例:捕捉子行程輸出

use std::process::{Command, Stdio};
use std::io::{self, Read};

fn main() -> io::Result<()> {
    let mut cmd = Command::new("echo")
        .arg("Hello, Rust!")
        .stdout(Stdio::piped())
        .spawn()?;
    
    let mut output = String::new();
    cmd.stdout.take().unwrap().read_to_string(&mut output)?;
    
    println!("Child process output: {}", output);
    Ok(())
}

內容解密:

  1. Command::new("echo").arg("Hello, Rust!"):建立一個執行 echo "Hello, Rust!" 的命令。
  2. .stdout(Stdio::piped()):設定子行程的 stdout 為管道,以便父行程讀取。
  3. cmd.stdout.take().unwrap().read_to_string(&mut output)?;:讀取子行程的輸出至字串變數 output

錯誤處理

Rust 使用 ResultOption 型別來處理可能發生的錯誤。透過 matchunwrap 方法,可以有效地管理錯誤。

程式碼範例:讀取檔案內容並處理錯誤

use std::fs::File;
use std::io::{self, Read};

fn read_file_contents(filename: &str) -> Result<String, io::Error> {
    let mut file = File::open(filename)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

fn main() {
    match read_file_contents("example.txt") {
        Ok(contents) => println!("File contents: {}", contents),
        Err(err) => eprintln!("Error: {}", err),
    }
}

內容解密:

  1. File::open(filename)?:嘗試開啟檔案,若失敗則傳播錯誤。
  2. match read_file_contents("example.txt"):根據 read_file_contents 的結果進行匹配,若成功則列印檔案內容,若失敗則列印錯誤訊息。

訊號處理

在 Unix-like 系統中,可以使用訊號來通知行程有關事件。Rust 提供了相關函式庫(如 nix)來處理訊號。

程式碼範例:處理 SIGINT 訊號

#[cfg(unix)]
extern crate nix;
#[cfg(unix)]
use nix::sys::signal;

#[cfg(unix)]
fn main() {
    unsafe {
        signal::signal(signal::Signal::SIGINT, signal::SigHandler::Handler(signal_handler)).unwrap();
    }
    loop {
        // 應用程式主迴圈
    }
}

#[cfg(unix)]
extern "C" fn signal_handler(_signal: i32) {
    println!("Received Ctrl+C signal");
    // 自定義訊號處理邏輯
}

內容解密:

  1. signal::signal(signal::Signal::SIGINT, signal::SigHandler::Handler(signal_handler)):註冊訊號處理函式,當接收到 SIGINT 訊號(Ctrl+C)時呼叫 signal_handler
  2. signal_handler 函式:在接收到 SIGINT 訊號時被呼叫,執行自定義的訊號處理邏輯。

Rust 中的行程管理、訊號處理與 I/O 作業

Rust 語言提供了豐富的功能來處理行程管理、訊號處理和 I/O 作業。這些功能使得開發者能夠建立高效、穩健的系統程式。在本章中,我們將探討這些主題,並建立一個簡易的 shell 程式。

行程管理

在系統程式設計中,行程是獨立的程式單元,擁有自己的記憶體和資源。Rust 使用 std::process::Command 結構體來管理行程和執行系統命令。

建立新行程

使用 Command 結構體可以建立新的行程。下面是一個範例:

use std::process::Command;

fn main() {
    let output = Command::new("ls")
        .arg("-l")
        .output()
        .expect("Failed to execute command");

    println!("{}", String::from_utf8_lossy(&output.stdout));
}

程式碼解密:

  1. 使用 Command::new("ls") 建立一個新的 ls 命令列程。
  2. 使用 .arg("-l") 新增 -l 引數來列出詳細的檔案資訊。
  3. 使用 .output() 執行命令並捕捉輸出。
  4. 使用 expect 處理可能的錯誤。
  5. 將輸出的 stdout 轉換為字串並列印出來。

訊號處理

訊號是用於非同步通知行程的機制,通常用於表示事件或錯誤。Rust 中的 nix 套件提供了訊號處理的功能。

處理訊號

下面是一個使用 nix 套件處理訊號的範例:

use nix::sys::signal::{self, SigHandler, Signal};
use nix::unistd::getpid;

fn main() {
    let pid = getpid();
    println!("Process ID: {}", pid);

    unsafe {
        signal::signal(Signal::SIGINT, SigHandler::Handler(handle_sigint))
            .expect("Failed to set signal handler");
    }

    loop {
        // 保持程式執行以接收訊號
    }
}

fn handle_sigint(_: nix::sys::signal::Signal) {
    println!("Received SIGINT signal");
}

程式碼解密:

  1. 使用 nix::sys::signal 模組來設定訊號處理器。
  2. 使用 signal::signal 設定 SIGINT 訊號的處理器為 handle_sigint 函式。
  3. 在無窮迴圈中保持程式執行,以便接收訊號。
  4. 當接收到 SIGINT 訊號(例如按下 Ctrl+C)時,呼叫 handle_sigint 函式。

I/O 作業

Rust 提供了多種方法來進行 I/O 作業,包括讀取使用者輸入、讀寫檔案等。

讀取使用者輸入

下面是一個讀取使用者輸入的範例:

use std::io;

fn main() {
    println!("Enter your name:");

    let mut input = String::new();
    io::stdin().read_line(&mut input)
        .expect("Failed to read line");

    println!("Hello, {}!", input.trim());
}

程式碼解密:

  1. 使用 io::stdin().read_line 讀取使用者的輸入。
  2. 使用 expect 處理可能的錯誤。
  3. 將輸入的字串修剪後輸出問候訊息。

建立簡易 Shell 程式

綜合上述知識,我們可以建立一個簡易的 shell 程式。

use std::io::{self, Write};
use std::process::{Command, Stdio};

fn main() {
    loop {
        print!("> ");
        io::stdout().flush().expect("Failed to flush stdout");

        let mut input = String::new();
        io::stdin().read_line(&mut input)
            .expect("Failed to read line");

        let input = input.trim();
        if input.is_empty() {
            continue;
        }
        if input == "exit" {
            break;
        }
        execute_command(input);
    }
}

fn execute_command(command: &str) {
    let mut parts = command.split_whitespace();
    let cmd = parts.next().expect("No command specified");
    let output = Command::new(cmd)
        .args(parts)
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .output()
        .expect("Failed to execute command");

    if !output.status.success() {
        eprintln!("Error: Command failed with code {:?}", output.status.code());
    }
}

程式碼解密:

  1. 主迴圈中不斷讀取使用者輸入並執行命令。
  2. 使用 execute_command 函式執行使用者輸入的命令。
  3. 使用 Command::new 建立命令,並使用 .args 新增引數。
  4. 設定 stdout 和 stderr 繼承父行程,以便在 shell 中顯示輸出和錯誤。
  5. 檢查命令是否成功執行,如果失敗則列印錯誤訊息。