一、前言 #
嗨,這裡是拍拍君!🦀
如果你跟拍拍君一樣,是個寫慣 Python 的工程師,那你一定用過 Typer 來做 CLI 工具——三行程式碼就有漂亮的 --help,超方便。
但有沒有遇過這種情況:你的 CLI 工具要處理幾十萬個檔案、或是要打包給沒裝 Python 的同事用?這時候 Rust 就是你的好朋友了。今天拍拍君帶你用 Rust 的 clap 套件來打造 CLI 工具,而且會一路跟 Python Typer 對照,讓你無痛上手!
二、安裝 Rust 與建立專案 #
如果你還沒裝 Rust,一行搞定:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
安裝完成後,建立新專案:
cargo new pypy-cli
cd pypy-cli
加入 clap 依賴:
cargo add clap --features derive
這會在 Cargo.toml 加上:
[dependencies]
clap = { version = "4", features = ["derive"] }
💡
cargo add就像 Python 的uv add或pip install,但它直接改Cargo.toml(相當於pyproject.toml)。
三、Hello World:最簡單的 CLI #
Python Typer 版 #
# hello.py
import typer
app = typer.Typer()
@app.command()
def hello(name: str):
"""跟你打招呼!"""
print(f"哈囉,{name}!拍拍君向你問好 🎉")
if __name__ == "__main__":
app()
python hello.py 拍拍醬
# 哈囉,拍拍醬!拍拍君向你問好 🎉
Rust clap 版 #
// src/main.rs
use clap::Parser;
/// 拍拍君的 CLI 工具
#[derive(Parser)]
#[command(name = "pypy-cli", version, about)]
struct Cli {
/// 你的名字
name: String,
}
fn main() {
let cli = Cli::parse();
println!("哈囉,{}!拍拍君向你問好 🎉", cli.name);
}
cargo run -- 拍拍醬
# 哈囉,拍拍醬!拍拍君向你問好 🎉
看出來了嗎?結構超像的:
| 概念 | Python Typer | Rust clap |
|---|---|---|
| 定義參數 | 函式參數 + type hint | struct 欄位 + type |
| 說明文字 | docstring | /// 文件註解 |
| 執行解析 | app() |
Cli::parse() |
四、選擇性參數與旗標 #
Python 版 #
@app.command()
def greet(
name: str,
greeting: str = typer.Option("哈囉", help="問候語"),
excited: bool = typer.Option(False, "--excited", "-e", help="加驚嘆號"),
):
"""自訂問候語"""
msg = f"{greeting},{name}"
if excited:
msg += "!!!🔥"
print(msg)
Rust 版 #
use clap::Parser;
#[derive(Parser)]
#[command(name = "pypy-cli", version, about = "拍拍君的 CLI 工具")]
struct Cli {
/// 你的名字
name: String,
/// 問候語
#[arg(short, long, default_value = "哈囉")]
greeting: String,
/// 加驚嘆號
#[arg(short, long)]
excited: bool,
}
fn main() {
let cli = Cli::parse();
let mut msg = format!("{},{}", cli.greeting, cli.name);
if cli.excited {
msg.push_str("!!!🔥");
}
println!("{msg}");
}
cargo run -- 拍拍醬 --greeting 嗨嗨 -e
# 嗨嗨,拍拍醬!!!🔥
重點整理:
#[arg(short, long)]→ 自動產生-g和--greetingbool型別自動變成旗標(flag),不用給值default_value就是 Typer 的typer.Option("預設值")
五、子命令(Subcommands) #
真正的 CLI 工具通常有子命令,像 git add、docker run。這是 clap 最強大的地方。
Python 版 #
import typer
app = typer.Typer()
@app.command()
def count(path: str, extension: str = typer.Option(".py", help="副檔名")):
"""計算檔案數量"""
from pathlib import Path
files = list(Path(path).rglob(f"*{extension}"))
print(f"找到 {len(files)} 個 {extension} 檔案 📂")
@app.command()
def search(path: str, pattern: str):
"""搜尋檔案內容"""
from pathlib import Path
for f in Path(path).rglob("*.py"):
content = f.read_text(errors="ignore")
if pattern in content:
print(f"✅ {f}")
if __name__ == "__main__":
app()
Rust 版 #
use clap::{Parser, Subcommand};
use std::fs;
use std::path::Path;
#[derive(Parser)]
#[command(name = "pypy-cli", version, about = "拍拍君的檔案工具")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// 計算檔案數量
Count {
/// 目標路徑
path: String,
/// 副檔名
#[arg(short, long, default_value = ".py")]
extension: String,
},
/// 搜尋檔案內容
Search {
/// 目標路徑
path: String,
/// 搜尋關鍵字
pattern: String,
},
}
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Count { path, extension } => {
let count = count_files(&path, &extension);
println!("找到 {count} 個 {extension} 檔案 📂");
}
Commands::Search { path, pattern } => {
search_files(&path, &pattern);
}
}
}
fn count_files(dir: &str, ext: &str) -> usize {
let mut count = 0;
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
count += count_files(path.to_str().unwrap_or(""), ext);
} else if path.to_str().map_or(false, |s| s.ends_with(ext)) {
count += 1;
}
}
}
count
}
fn search_files(dir: &str, pattern: &str) {
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
search_files(path.to_str().unwrap_or(""), pattern);
} else if path.to_str().map_or(false, |s| s.ends_with(".py")) {
if let Ok(content) = fs::read_to_string(&path) {
if content.contains(pattern) {
println!("✅ {}", path.display());
}
}
}
}
}
}
cargo run -- count ./src --extension .rs
# 找到 1 個 .rs 檔案 📂
cargo run -- search ./src "fn main"
# ✅ ./src/main.rs
子命令在 Rust 裡用 enum 表示——每個 variant 就是一個子命令,超直覺!
六、錯誤處理:Result 與 anyhow #
Python 裡我們習慣 try/except,Rust 用 Result 型別。搭配 anyhow 套件可以讓錯誤處理跟 Python 一樣輕鬆。
cargo add anyhow
use anyhow::{Context, Result};
use clap::Parser;
use std::fs;
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "pypy-cli")]
struct Cli {
/// 要讀取的檔案
file: PathBuf,
/// 搜尋關鍵字
pattern: String,
}
fn main() -> Result<()> {
let cli = Cli::parse();
let content = fs::read_to_string(&cli.file)
.with_context(|| format!("讀不到檔案:{}", cli.file.display()))?;
let matches: Vec<(usize, &str)> = content
.lines()
.enumerate()
.filter(|(_, line)| line.contains(&cli.pattern))
.collect();
if matches.is_empty() {
println!("找不到「{}」😢", cli.pattern);
} else {
println!("找到 {} 筆符合的行:", matches.len());
for (i, line) in matches {
println!(" L{}: {}", i + 1, line);
}
}
Ok(())
}
cargo run -- Cargo.toml "clap"
# 找到 1 筆符合的行:
# L7: clap = { version = "4", features = ["derive"] }
? 運算子就像 Python 的 raise——遇到錯誤就往上拋,但 Rust 在編譯時就會檢查你有沒有處理所有可能的錯誤,安全感滿滿。
七、打包與發佈 #
Python CLI 工具要發佈,你得搞 setup.py、pyproject.toml、entry_points⋯⋯
Rust?一行:
cargo build --release
產出的二進位檔在 target/release/pypy-cli,直接丟給別人就能用!不需要裝 Rust、不需要虛擬環境。
# 檔案大小比較
$ ls -lh target/release/pypy-cli
# -rwxr-xr-x 1 user staff 1.2M pypy-cli
# Python 打包成執行檔(用 PyInstaller)
$ ls -lh dist/pypy-cli
# -rwxr-xr-x 1 user staff 45M pypy-cli
1.2 MB vs 45 MB,差距超級大!
八、Typer vs clap 完整比較 #
| 功能 | Python Typer | Rust clap |
|---|---|---|
| 參數定義 | type hints | struct + derive |
| 選項 | typer.Option() |
#[arg()] |
| 子命令 | @app.command() |
enum + Subcommand |
| 自動補全 | ✅ 內建 | ✅ clap_complete |
| 錯誤處理 | try/except | Result + ? |
| 打包 | PyInstaller (~45MB) | cargo build (~1MB) |
| 啟動速度 | ~100ms | ~1ms |
| 學習曲線 | 🟢 低 | 🟡 中 |
結語 #
今天我們從 Python Typer 的角度出發,學了 Rust clap 的核心功能:
- 基本參數 — struct 欄位就是參數
- 選項與旗標 —
#[arg()]標註 - 子命令 — enum 表達不同命令
- 錯誤處理 —
Result+anyhow - 打包 —
cargo build --release一行搞定
如果你已經會 Typer,學 clap 的門檻真的不高。而且 Rust CLI 的啟動速度和檔案大小,真的會讓你回不去 😏
下次我們來看 Rust 的 Ownership 和 Borrowing——這是 Rust 最獨特(也最讓人抓狂)的概念。拍拍君會用 Python 工程師聽得懂的方式來講,敬請期待!
Happy coding!🦀✨