この記事は tokio 0.3.x に対応しています
はじめに
Rust で ls
等の外部コマンドを実行したいときは std::process::Command が使えます。
下記の例では spawn()
を使ってコマンドを子プロセスで実行します。標準入力と標準出力は inherit されます。つまり、cargo run
を実行しているターミナルに結果が出力されます。
use std::process::Command;
// 子プロセスとして実行します
Command::new("ls")
.args(&["-l", "-a"])
.spawn()
.expect("failed to start `ls`");
コマンドの出力結果をプログラム中で使いたい場合は output()
を使ってコマンドを実行すると便利です。このときは output()
でコマンドの出力を待機します。
use std::process::Command;
// コマンドの結果を待機します
let output = Command::new("ls")
.args(&["-l", "-a"])
.output()
.expect("failed to start `ls`");
println!("{}", String::from_utf8_lossy(&output.stdout));
spawn()
を使ってコマンドを実行する場合でコマンドの出力結果をプログラム中で使いたければ、std::process::ChildStdout を取得しましょう。
use std::process::{Command, Stdio};
use std::io::prelude::*;
use std::io::BufReader
// 子プロセスとして実行します
let mut child = Command::new("ls")
.args(&["-l", "-a"])
.stdout(Stdio::piped())
.spawn()
.expect("failed to start `ls`");
// stdout への出力のハンドラを取得します
let mut stdout = child.stdout.take().unwrap();
// BufReader をかませることで 1 行ずつ実行結果を取り出します
let reader = BufReader::new(stdout);
for line in reader.lines() {
println!("{}", line?);
}
std::process::Command
は ls
などのすぐ終わるコマンドを実行するときは向いていますが、コマンドの実行や結果の取得時に処理をブロックしてしまうので、実行に時間がかかるようなコマンドを実行するときには不向きだと思います。そこで、tokio::process::Command を使って非同期 Rust で書き直してみましょう。
tokio::process::Command
によるコマンドの実行
bash -c 'for i in {0..10}; do echo $i; sleep 1; done'
というコマンドを実行することを考えましょう。理想的には 1 秒おきに
0
1
2
3
4
5
6
7
8
9
と出力されるはずです。
コマンドの出力結果を待機する
tokio::process::Command
は std::process::Command
と同じような API を持ちますが、コマンドの出力結果をみる系のメソッドは Future
を返すようになっています。ひとまず、 output()
を使ってみましょう。
use tokio::process::Command;
// コマンドの結果を待機します
let output = Command::new("bash")
.arg("-c")
.arg("for i in {0..10}; do echo $i; sleep 1; done")
.output()
.await
.expect("failed to execute sleep");
println!("{:?}", String::from_utf8_lossy(&output.stdout));
使い心地は std::process::Command
と同じです。コマンドの実行結果全体の出力を await
しているので、10 秒後に結果が表示されます。
コマンドの出力結果を1行ずつ取得する
std::process::Command
でやったものをもとに tokio で書き直してみましょう。コマンドを spawn()
することで、子プロセスで実行してくれます。下記の例では stdout
を FramedRead
1 に渡しています。こうすることで、コマンドの出力結果を 1 行ずつ yield してくれるような Stream として扱うことができるようになります。
use std::process::Stdio;
use tokio::process::Command;
use tokio_util::codec::{FramedRead, LinesCodec};
use futures::prelude::*;
// 1 行ずつ結果を読み取ります
let mut child = Command::new("bash")
.arg("-c")
.arg("for i in {0..10}; do echo $i; sleep 1; done")
.stdout(Stdio::piped())
.spawn()
.expect("failed to start sleep");
// stdout への出力のハンドラを取得します
let stdout = child.stdout.take().unwrap();
// stdout の出力を Stream に変換します
let mut reader = FramedRead::new(stdout, LinesCodec::new());
while let Some(line) = reader.next().await {
println!("{}", line?);
}
コマンドの実行を中断する
コマンドの実行を途中で中止したいときもあるかと思います。Future の処理を中断する場合、Future を単に drop してあげれば OK です。しかし、Command
の場合はこれだけだと不十分です。一般的な Future は drop されたときにリソースの解放をしてくれるため、Command
の実行も中止してくれそうな感じがしますが、std::process
の挙動に合わせるため drop してもコマンドの実行は中止されなくなりました。drop したときに子プロセスで動いてるコマンドを kill するには、kill_on_drop(true)
とします。
use std::process::Stdio;
use std::time::Duration;
use tokio::process::Command;
use tokio::time::sleep;
use tokio_util::codec::{FramedRead, LinesCodec};
use futures::prelude::*;
// 途中でコマンドの実行を中止します
let mut child = Command::new("bash")
.kill_on_drop(true)
.arg("-c")
.arg("for i in {0..10}; do echo $i; sleep 1; done")
.stdout(Stdio::piped())
.spawn()
.expect("failed to start sleep");
let stdout = child.stdout.take().unwrap();
let mut reader = FramedRead::new(stdout, LinesCodec::new());
tokio::spawn(async move {
// 5 秒後にコマンドの実行を中止します
sleep(Duration::from_secs(5)).await;
drop(child);
});
while let Some(line) = reader.next().await {
println!("{}", line?);
}
-
tokio 0.2 から
tokio-util
クレート に分離されました ↩