Keywords: CLI, error handling, modules, tooling
อ่านแบบคน Python:
- ถ้าอยากเอา “ภาพรวม” ก่อน: จำ pattern
parse → Command enum → execute - ถ้าอยาก “ลงมือทำ”: ทำให้ parse คืน
Commandก่อน แล้วค่อยเติม behavior ทีละคำสั่ง - ถ้าติด: เปิด 12-learning-playbook.md
CLI เป็นสนามฝึก Rust ที่คุ้มมากสำหรับคนมาจาก Python เพราะมันบังคับให้คุณจัดระบบความคิดแบบ “รับ input ที่ไม่แน่นอน → แปลงเป็นข้อมูลที่ชัด → dispatch”
แก่นของบทนี้:
- Args ในโลกจริง “ไม่ครบ/เกิน/พิมพ์ผิด” เป็นเรื่องปกติ
- ใช้
enum+matchเพื่อ encode “คำสั่งที่ถูกต้อง” ลงใน type - แยก parse ออกจาก execute เพื่อให้ test ได้ง่าย
Python (เทียบแนวคิด):
import sys
print("args=", sys.argv)Output (example):
args= ['app.py', 'status']
Rust:
fn main() {
let args: Vec<String> = std::env::args().collect();
println!("args={args:?}");
}Output (example):
args=["target/debug/app", "status"]
สิ่งที่ควรรู้ตั้งแต่แรก:
args[0]โดยทั่วไปคือ path ของโปรแกรม (ไม่ใช่ subcommand)- อย่าใช้
args[1]ตรง ๆ ถ้าคุณยังไม่ได้เช็กความยาว (จะ panic ได้)
ถ้ารันผ่าน cargo run แล้วอยากส่ง args ให้โปรแกรมจริง ต้องคั่นด้วย --
cargo run -- statuscargo run -- set greeting hello
std::env::args()ให้String(UTF-8)- ถ้ามี arg ที่ไม่ใช่ UTF-8 อาจทำให้โปรแกรมพัง
- ถ้าต้อง robust มาก ๆ ค่อยขยับไป
std::env::args_os()(ได้OsString) แล้วค่อยแปลง/validate เอง
สำหรับบทนี้ เราใช้ args() เพื่อโฟกัส “pattern การจัดโครง” ก่อน
ให้คิดแบบสายงานจริง:
- รับ
Vec<String>(raw input) - parse เป็น
Command(ชนิดข้อมูลที่ชัด) - execute ตาม
Command(logic ไม่ต้องมานั่งเช็ก args ซ้ำ)
ข้อดีเทียบกับ if/elif ยาว ๆ:
- “คำสั่งที่ถูกต้อง” ถูกบังคับด้วย type
- เพิ่มคำสั่งใหม่: เพิ่ม enum + เพิ่ม parse + เพิ่ม match
- แยกส่วนได้: parse/test, execute/test
enum Command {
Help,
Version,
Status,
}
fn usage() -> &'static str {
"usage: app [help|version|status]"
}
fn parse_command(args: &[String]) -> Command {
let sub = args.get(1).map(|s| s.as_str());
match sub {
None | Some("help") => Command::Help,
Some("version") => Command::Version,
Some("status") => Command::Status,
_ => Command::Help,
}
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let cmd = parse_command(&args);
match cmd {
Command::Help => println!("{}", usage()),
Command::Version => println!("v0"),
Command::Status => println!("ok"),
}
}Output (example):
ok
ทำไม args.get(1) ถึงสำคัญ:
- ผู้ใช้พิมพ์แค่
app(ไม่มี subcommand) - ผู้ใช้พิมพ์
app status extra(มีของเกิน) - ผู้ใช้พิมพ์
app stauts(typo)
ถ้าใช้ args[1] จะ panic ในเคสแรกทันที แต่ get(1) จะให้ Option แล้วคุณตัดสินใจ fallback ได้เอง
เวอร์ชันก่อนอ่านง่าย แต่ “unknown command” ถูกกลืนเป็น help เงียบ ๆ ซึ่ง UX ไม่ดี (ผู้ใช้ไม่รู้ว่าพิมพ์ผิดตรงไหน)
แนวที่โตได้:
parse_command(&[String]) -> Result<Command, String>- ถ้า parse ไม่ได้: คืนข้อความ error ที่ชัด + พิมพ์ usage
enum Command {
Help,
Version,
Status,
}
fn usage() -> &'static str {
"usage: app [help|version|status]"
}
fn parse_command(args: &[String]) -> Result<Command, String> {
let sub = args.get(1).map(|s| s.as_str());
match sub {
None | Some("help") | Some("--help") | Some("-h") => Ok(Command::Help),
Some("version") => Ok(Command::Version),
Some("status") => Ok(Command::Status),
Some(other) => Err(format!("unknown command: {other}")),
}
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let cmd = match parse_command(&args) {
Ok(c) => c,
Err(e) => {
eprintln!("error: {e}");
eprintln!("{}", usage());
return;
}
};
match cmd {
Command::Help => println!("{}", usage()),
Command::Version => println!("v0"),
Command::Status => println!("ok"),
}
}ประโยชน์ที่ได้ทันที:
- พิมพ์ผิดแล้ว “ดัง”
- parse กับ execute แยกชัด ทำให้ unit test ง่าย
CLI จริงมักมีคำสั่งที่ต้องการข้อมูลเพิ่ม เช่น set ที่รับ NAME กับ VALUE
ให้ encode payload ลงใน enum:
enum Command {
Help,
Status,
Set { name: String, value: String },
}
fn usage() -> &'static str {
"usage: app [help|status|set NAME VALUE]"
}
fn parse_command(args: &[String]) -> Result<Command, String> {
let sub = args.get(1).map(|s| s.as_str());
match sub {
None | Some("help") | Some("--help") | Some("-h") => Ok(Command::Help),
Some("status") => Ok(Command::Status),
Some("set") => {
let name = args.get(2).ok_or_else(|| "missing NAME for 'set'".to_string())?;
let value = args.get(3).ok_or_else(|| "missing VALUE for 'set'".to_string())?;
Ok(Command::Set {
name: name.clone(),
value: value.clone(),
})
}
Some(other) => Err(format!("unknown command: {other}")),
}
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let cmd = match parse_command(&args) {
Ok(c) => c,
Err(e) => {
eprintln!("error: {e}");
eprintln!("{}", usage());
return;
}
};
match cmd {
Command::Help => println!("{}", usage()),
Command::Status => println!("ok"),
Command::Set { name, value } => println!("set {name}={value}"),
}
}จุดสำคัญ:
- ถ้าผ่าน
parse_commandแล้ว แปลว่าsetมี name/value ครบ - ส่วน execute ไม่ต้องเช็ก
args.len()ซ้ำอีก (logic สะอาดขึ้น)
set NAME VALUE ถือว่า VALUE เป็น “หนึ่ง token”
ถ้าจะส่งข้อความที่มีช่องว่าง ผู้ใช้ต้อง quote ตาม shell:
app set greeting "hello world"
ถ้าคุณอยากรองรับ “รับค่าทั้งหมดที่เหลือเป็นข้อความเดียว” (rest-of-args):
- เก็บ
args[3..]แล้วjoin(" ")
พอ CLI โตมาก ๆ ค่อยพิจารณาใช้ crate อย่าง clap เพื่อจัดการ quoting/flags/usage ให้ดีขึ้น
เพราะเราออกแบบให้ parse รับ &[String] เราเขียน test ได้โดยไม่ต้องรันโปรแกรมจริง
#[test]
fn parse_status() {
let args = vec!["app".to_string(), "status".to_string()];
let cmd = parse_command(&args).unwrap();
match cmd {
Command::Status => {}
_ => panic!("expected status"),
}
}
#[test]
fn parse_set_missing_value() {
let args = vec!["app".to_string(), "set".to_string(), "greeting".to_string()];
let err = parse_command(&args).unwrap_err();
assert!(err.contains("missing VALUE"));
}Output (example):
(no output — tests pass silently)
หมายเหตุ: ตัวอย่างนี้ assume ว่าคุณอยู่ในไฟล์เดียวกันและ parse_command/Command visible กับ test ได้ (เช่นอยู่ใน main.rs เดียวกัน หรือย้าย logic ไป lib.rs ตามบท 05)
- Pattern A:
Vec<String>→parse_command(&args)→match cmd - Pattern B: Parse ให้เป็น
enumที่มี payload แล้วค่อย execute - Pattern C: Unknown/missing args ควร
Err(...)+ พิมพ์usage - Pattern D: แยก parse/execute เพื่อ test ได้ และลดการ panic
- เพิ่ม command
version(พิมพ์v0) - เพิ่ม command
set NAME VALUEแล้วให้ parse เป็นCommand::Set { name, value } - เพิ่ม command
get NAME(เพื่อเตรียมไปบท mini project) - ปรับ unknown command ให้พิมพ์
error: ...และตามด้วยusage: ... - (ต่อยอด) ลองแยกไฟล์/โมดูล:
src/main.rsทำแค่ parse args แล้วเรียก executesrc/lib.rsเก็บCommand,parse_command, และ logic ที่ test ได้