43
15

More than 3 years have passed since last update.

tokioで外部コマンド実行

Last updated at Posted at 2019-12-22

この記事は 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::Commandls などのすぐ終わるコマンドを実行するときは向いていますが、コマンドの実行や結果の取得時に処理をブロックしてしまうので、実行に時間がかかるようなコマンドを実行するときには不向きだと思います。そこで、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::Commandstd::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() することで、子プロセスで実行してくれます。下記の例では stdoutFramedRead1 に渡しています。こうすることで、コマンドの出力結果を 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?);
}

  1. tokio 0.2 から tokio-util クレート に分離されました 

43
15
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
15