Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu
std::process::Commandlà builder pattern để cấu hình và chạy tiến trình con bằng Rust. - Phân biệt 3 cách chạy:
output()chờ và capture,status()chờ và lấy exit code,spawn()chạy background trả vềChildhandle. - Đọc và xử lý
Output:stdout,stderr(Vec<u8>), vàstatus(ExitStatus). - Điều khiển pipe stdin/stdout/stderr qua
Stdio::piped(),Stdio::inherit(),Stdio::null(). - Quản lý vòng đời tiến trình con với
Child::wait(),Child::kill(),Child::try_wait(). - Set biến môi trường với
env()và đổi working directory vớicurrent_dir(). - Biết khi nào nên chuyển sang
tokio::processcho async I/O.
Command::new(...).arg(...) Builder
std::process::Command là builder pattern: tạo bằng Command::new(program), chain method để cấu hình, kết thúc bằng một trong các method "chạy". Mỗi setter trả về &mut Command nên có thể nối chuỗi:
use std::process::Command;
let output = Command::new("git")
.args(["log", "--oneline", "-n", "5"])
.output()
.expect("failed to run git");
println!("{}", String::from_utf8_lossy(&output.stdout));
Vài điểm cần để ý:
Command::new("git")— tên chương trình. Nếu là tên đơn (không có/), OS sẽ tìm theo biến môi trườngPATH. Đường dẫn tuyệt đối (/usr/bin/git) thì gọi trực tiếp..arg("foo")thêm một argument;.args([...])thêm nhiều cùng lúc. Mỗi argument là một string riêng — không phải dòng lệnh shell. Bạn không thể truyền"log --oneline"như một string duy nhất — phải tách["log", "--oneline"].- Không có shell mặc định. Glob (
*.txt), pipe (|), redirect (>) không được expand. Muốn dùng shell phải gọiCommand::new("sh").args(["-c", "ls *.txt | wc -l"])tường minh.
Quyết định không qua shell là cố ý — giảm rủi ro shell injection. Argument tách rõ ràng nghĩa là dữ liệu user nhập không trở thành code shell.
3 Cách Chạy: output, status, spawn
Sau khi cấu hình Command, có 3 method tận cùng — chọn cái nào phụ thuộc bạn cần gì.
output() — chờ tiến trình kết thúc, capture toàn bộ stdout và stderr vào buffer. Phù hợp khi cần đọc kết quả:
let out = Command::new("hostname").output()?;
let name = String::from_utf8_lossy(&out.stdout);
println!("Hostname: {}", name.trim());
status() — chờ tiến trình kết thúc, chỉ lấy exit code. stdout và stderr mặc định inherit (đổ thẳng ra terminal của parent). Phù hợp khi chỉ quan tâm thành công hay thất bại:
let status = Command::new("cargo")
.args(["test", "--release"])
.status()?;
if status.success() {
println!("Test passed");
} else {
eprintln!("Test failed with code {:?}", status.code());
}
spawn() — trả về Child handle ngay lập tức, không chờ. Tiến trình con chạy song song với parent. Phù hợp khi cần kiểm soát thủ công (đọc stdout streaming, kill khi quá thời gian, chạy nhiều process đồng thời):
let mut child = Command::new("sleep")
.arg("30")
.spawn()?;
println!("Spawned PID {}", child.id());
// ...làm việc khác ở parent...
let status = child.wait()?;
println!("Child exited: {:?}", status);
Quy tắc nhớ: cần dữ liệu → output; chỉ cần kết quả pass/fail → status; cần kiểm soát chi tiết → spawn.
Output: stdout, stderr, status
Method output() trả về std::io::Result<Output>. Output là struct đơn giản:
pub struct Output {
pub status: ExitStatus,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
Lưu ý stdout và stderr là Vec<u8> — bytes thô, không đảm bảo UTF-8. Có 2 cách chuyển sang String:
// Lossy: byte không hợp lệ thay bằng U+FFFD, không bao giờ fail
let s = String::from_utf8_lossy(&output.stdout);
// Strict: fail nếu không hợp lệ UTF-8
let s = String::from_utf8(output.stdout)?;
Dùng from_utf8_lossy khi không chắc encoding (output của ls, log, …). Dùng from_utf8 khi format đã spec UTF-8 (vd git log --format=%H).
Kiểm tra status:
let out = Command::new("grep").args(["needle", "haystack.txt"]).output()?;
if out.status.success() {
println!("Found: {}", String::from_utf8_lossy(&out.stdout));
} else {
// grep exit code 1 nghĩa là không tìm thấy (không phải lỗi thật)
match out.status.code() {
Some(1) => println!("Not found"),
Some(code) => eprintln!("grep error {}: {}", code, String::from_utf8_lossy(&out.stderr)),
None => eprintln!("grep killed by signal"),
}
}
status.code() trả Option<i32>: Some(code) nếu tiến trình tự exit, None nếu bị OS signal kill (vd SIGTERM). Trên Unix có thể dùng trait ExitStatusExt để lấy signal number cụ thể.
Stdio Pipe: piped, inherit, null
Mỗi tiến trình con có 3 stream chuẩn: stdin, stdout, stderr. Mặc định output() dùng piped() cho cả 3, còn status()/spawn() dùng inherit(). Bạn override qua method .stdin(), .stdout(), .stderr() với enum Stdio:
Stdio::piped()— tạo OS pipe, parent đọc/ghi qua handle trênChild.Stdio::inherit()— dùng chung stream của parent (output đổ thẳng terminal).Stdio::null()— chuyển hướng vào/dev/null(Unix) hoặcNUL(Windows), bỏ qua hoàn toàn.
Ví dụ ghi dữ liệu vào stdin của tiến trình con — pipe input vào wc -w để đếm từ:
use std::io::Write;
use std::process::{Command, Stdio};
let mut child = Command::new("wc")
.arg("-w")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
// Lấy handle stdin, ghi dữ liệu, drop để close pipe
child.stdin.take().unwrap().write_all(b"hello world from rust")?;
let out = child.wait_with_output()?;
println!("Word count: {}", String::from_utf8_lossy(&out.stdout).trim());
Hai chi tiết quan trọng: (1) phải take() stdin ra khỏi Child rồi drop để pipe đóng — nếu không, wc chờ EOF mãi mãi và hệ thống deadlock; (2) dùng wait_with_output() để gộp wait + đọc stdout còn lại, an toàn hơn gọi tay wait() trước rồi đọc sau.
Dùng Stdio::null() khi muốn "fire and forget" — chạy background daemon, không quan tâm output:
Command::new("rust-analyzer")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
Child::wait, Child::kill
spawn() trả về Child — handle để điều khiển tiến trình con. Các method chính:
child.id() -> u32— PID, dùng cho log hoặc gửi signal ngoài.child.wait() -> io::Result<ExitStatus>— block tới khi tiến trình kết thúc.child.try_wait() -> io::Result<Option<ExitStatus>>— non-blocking poll.Ok(None)nghĩa là vẫn đang chạy.child.kill() -> io::Result<()>— gửiSIGKILL(Unix) hoặcTerminateProcess(Windows). Sau kill vẫn nên gọiwait()để thu hồi zombie process.child.wait_with_output()— consumeChild, đóngstdin, đọc hếtstdout/stderr, trảOutput.
Pattern timeout phổ biến — chờ tối đa N giây rồi kill:
use std::process::{Command, Stdio};
use std::thread;
use std::time::{Duration, Instant};
let mut child = Command::new("ffmpeg")
.args(["-i", "in.mp4", "out.webm"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let deadline = Instant::now() + Duration::from_secs(60);
loop {
match child.try_wait()? {
Some(status) => {
println!("Finished: {:?}", status);
break;
}
None if Instant::now() > deadline => {
eprintln!("Timeout, killing process");
child.kill()?;
child.wait()?; // thu zombie
break;
}
None => thread::sleep(Duration::from_millis(200)),
}
}
Lưu ý: Child không tự kill khi drop. Nếu bạn quên wait() hoặc kill(), tiến trình con tiếp tục chạy ngầm sau khi parent exit — có thể là điều bạn muốn (daemon) hoặc không (resource leak). Tự quản lý vòng đời tường minh.
env() Và current_dir()
Mặc định tiến trình con thừa kế toàn bộ biến môi trường và working directory của parent. Override khi cần qua các method:
let output = Command::new("./build.sh")
.current_dir("/tmp/workspace")
.env("CARGO_TARGET_DIR", "/tmp/target")
.env("RUST_LOG", "debug")
.env_remove("HTTP_PROXY")
.output()?;
env(key, value)— set hoặc override một biến.envs(iter)— set hàng loạt từHashMaphoặc array tuple.env_remove(key)— bỏ một biến khỏi child env (vẫn còn ở parent).env_clear()— wipe toàn bộ, chỉ giữ biến set sau đó. Hữu ích khi muốn môi trường sạch tuyệt đối (vd test isolation, sandbox).current_dir(path)— đổi working directory của tiến trình con. Path có thể tương đối (so với CWD của parent) hoặc tuyệt đối.
Lưu ý current_dir() chỉ ảnh hưởng tiến trình con — CWD của parent không đổi. Đây là khác biệt với std::env::set_current_dir() (đổi CWD của chính process hiện tại, ảnh hưởng mọi thao tác file tiếp theo của parent).
Pattern thực tế: chạy git ở repo khác:
let out = Command::new("git")
.current_dir("/path/to/other/repo")
.args(["rev-parse", "HEAD"])
.output()?;
let commit = String::from_utf8_lossy(&out.stdout);
println!("HEAD: {}", commit.trim());
Preview tokio::process Async
std::process là blocking — wait(), output(), đọc/ghi pipe đều dừng cả thread. Trong async runtime (axum, actix), điều này phá vỡ scheduler. Giải pháp: tokio::process::Command, API gần như identical nhưng trả về Future:
use tokio::process::Command;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let out = Command::new("git")
.args(["log", "--oneline", "-n", "3"])
.output()
.await?;
println!("{}", String::from_utf8_lossy(&out.stdout));
Ok(())
}
Khác biệt chính: thêm .await, và pipe stdin/stdout là tokio::io::AsyncRead/AsyncWrite — read/write không block runtime. Khi viết web service mà cần exec subprocess, dùng tokio::process, đừng gọi std::process bên trong async fn.
Tổng Kết
std::process::Commandlà builder pattern:new+ chain setter + một method chạy tận cùng.- Argument tách từng phần qua
.arg()/.args()— không đi qua shell, không bị shell injection. - 3 cách chạy:
output()chờ + capture stdout/stderr,status()chờ + exit code,spawn()background trả vềChild. Outputchứastatus,stdout,stderr(kiểuVec<u8>) — dùngString::from_utf8_lossyđể hiển thị an toàn.Stdio::piped()/inherit()/null()điều khiển 3 stream stdin/stdout/stderr độc lập.Child::wait()block,try_wait()non-blocking,kill()gửi SIGKILL — luôn nhớwait()saukill()để thu zombie.env(),env_remove(),env_clear(),current_dir()cấu hình môi trường tiến trình con; không ảnh hưởng parent.- Trong async runtime, dùng
tokio::process::Commandthay vìstd::processđể tránh block executor.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Vì sao
Command::new("ls").arg("*.txt").output()không list được file.txtnhư shell? Làm sao để fix mà vẫn dùngstd::process? - Phân biệt
output(),status(),spawn(): mỗi cái chờ hay không, capture stdout hay không, return type là gì? - Vì sao khi pipe data vào
stdincủa child cầntake()và drop handle? Điều gì xảy ra nếu giữ handle stdin sống mà gọichild.wait()? - Bạn cần chạy
cargo buildtrong directory/projects/myappvới biến môi trườngRUST_LOG=trace, capture cả stdout và stderr. Viết code Rust thực hiện. - Khác biệt giữa
Stdio::inherit()vàStdio::piped()? Khi nào nên dùng cái nào? - Trong async function (vd axum handler), bạn cần exec
ffmpegđể convert video. Dùngstd::process::Commandtrực tiếp có vấn đề gì? Cách đúng là gì?
Đáp án
std::process::Commandkhông qua shell, nên glob pattern*.txtkhông được expand —lsnhận literal string"*.txt"và báo "no such file". Fix: gọi shell tường minh:Command::new("sh").args(["-c", "ls *.txt"]). Nhưng cách an toàn hơn (tránh shell injection) là dùng crateglobđể expand pattern bên Rust, rồi pass danh sách file cụ thể qua.args().output(): block chờ tiến trình kết thúc, capture cả stdout và stderr vào buffer, returnio::Result<Output>.status(): block chờ, stdout/stderr inherit ra terminal parent (không capture), returnio::Result<ExitStatus>.spawn(): không block, return ngayio::Result<Child>— child chạy song song với parent, caller tự quản lý wait/kill.- Tiến trình như
wc,cat,sortđọc stdin tới khi nhận EOF. EOF được gửi khi đầu ghi của pipe đóng — tức là khi handle stdin trong Rust bị drop. Nếu giữ handle sống và gọichild.wait(), child chờ EOF mãi (chưa đóng pipe), parent chờ child exit — deadlock. Pattern đúng:child.stdin.take().unwrap().write_all(...)?rồi để handle drop cuối scope, hoặc dùngwait_with_output()tự xử lý đúng thứ tự. use std::process::Command; let out = Command::new("cargo") .arg("build") .current_dir("/projects/myapp") .env("RUST_LOG", "trace") .output()?; println!("stdout: {}", String::from_utf8_lossy(&out.stdout)); eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr)); println!("exit: {:?}", out.status.code());Stdio::inherit()chia sẻ stream của parent — output của child đổ thẳng terminal/log của parent, không qua buffer. Dùng khi muốn user thấy output realtime (CLI tool wrap command khác, build script).Stdio::piped()tạo OS pipe — parent đọc/ghi qua handleChildStdout/ChildStdin. Dùng khi cần programmatic access (parse output, feed input từ Rust). Mặc địnhoutput()dùng piped,status()dùng inherit.std::process::Commandblocking —.output().awaitsẽ không compile (không phải Future), còn.output()không có await sẽ block toàn bộ thread của tokio runtime, freeze các task khác đang chạy trên cùng worker. Cách đúng: dùngtokio::process::Command— API gần giống nhưngoutput(),wait(), đọc pipe đều async, không chặn executor. Trong handler axum hoặc tokio task, luôn dùngtokio::process.
Bài Tiếp Theo
Bài 293: std::time — Duration & Instant — bài tiếp theo của Group 36 Standard Library. Tìm hiểu Duration::from_secs/from_millis/from_micros/from_nanos để đại diện khoảng thời gian, Instant::now() để đo elapsed time chính xác cao (monotonic clock), hiệu giữa hai Instant trả về Duration, và khác biệt với SystemTime dùng cho wall clock (kém chính xác hơn, có thể nhảy ngược khi NTP sync).
