Help us understand the problem. What is going on with this article?

tokioで外部コマンド実行

はじめに

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::prelude::*;
use tokio::process::Command;
use tokio_util::codec::{FramedRead, LinesCodec, LinesCodecError};
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 mut 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::prelude::*;
use tokio::process::Command;
use tokio::time::delay_for;
use tokio_util::codec::{FramedRead, LinesCodec, LinesCodecError};
use futures::prelude::*;
use futures::future;

// 途中でコマンドの実行を中止します
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 mut stdout = child.stdout().take().unwrap();
let mut reader = FramedRead::new(stdout, LinesCodec::new());

tokio::spawn(async move {
    // 5 秒後にコマンドの実行を中止します
    delay_for(Duration::from_secs(5)).await;
    drop(child);
});

while let Some(line) = reader.next().await {
    println!("{}", line?);
}

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした