blog.anqou.net
rss
author
tags

OCaml の Eio を使って外部コマンドをパイプでつなぎ出力を得る

Eio(ocaml-multicore/eio) は OCaml で非同期処理を行うためのフレームワークです。古くからある Lwt や Async とは異なり OCaml 5.0 で導入された algebraic effect を使っているため、direct style で非同期 I/O の処理を記述することができるという特徴があります。

Eio.Process モジュールを使うと、Eio を使い外部コマンドを起動することができます。Eio の README には以下のような単純な例が示されています:

# Eio_main.run @@ fun env ->
  let proc_mgr = Eio.Stdenv.process_mgr env in
  Eio.Process.parse_out proc_mgr Eio.Buf_read.line ["echo"; "hello"];;
- : string = "hello"

Eio.Process には pipe(2) のサポートもあるため、より複雑なコマンド実行も行うことができます。ここでは、2 つのコマンドをパイプでつなぎ、後側のコマンドの出力を一行ずつ取り出すようなコードを Eio 1.3 を使い書いてみます。基本的に通常の Linux プログラミングを行えば良く[1]、syscall に対応する Eio の関数を呼び出していく形になります:

Eio_main.run @@ fun env ->
Eio.Switch.run @@ fun sw ->

(* 1 つ目のコマンド用のパイプ(pipe1)を作る。*)
let r1, w1 = Eio.Process.pipe ~sw env#process_mgr in
(* 1 つ目のコマンドを実行する。標準出力をパイプに流す。*)
let com1 = Eio.Process.spawn ~sw ~stdout:w1 env#process_mgr [ "ls" ] in
(* pipe1 の書き込み口は不要になるので閉じる。*)
Eio.Flow.close w1;
(* 2 つ目のコマンド用のパイプ(pipe2)を作る。*)
let r2, w2 = Eio.Process.pipe ~sw env#process_mgr in
(* 2 つ目のコマンドを実行する。標準入力には pipe1 の読み出し口を、
   標準出力には pipe2 の書き出し口を指定する。*)
let com2 = Eio.Process.spawn ~sw ~stdin:r1 ~stdout:w2 env#process_mgr [ "cat" ] in
(* pipe1 の読み出し口・pipe2 の書き出し口は不要になるので閉じる。*)
Eio.Flow.close r1;
Eio.Flow.close w2;

(* pipe2 の読み出し口から一行ずつ読む。行長の上限が予め分かっている場合は
   ~max_size にその値を指定する。*)
let reader = Eio.Buf_read.of_flow ~max_size:max_int r2 in
let rec loop () =
  match Eio.Buf_read.line reader with
  | exception End_of_file -> Ok ()
  | exception Failure msg -> Error msg
  | exception Eio.Buf_read.Buffer_limit_exceeded -> assert false
  | line ->
      print_endline line; (* line を使って好きな処理を書く。*)
      loop ()
in
loop () |> Result.error_to_failure;

(* 読み出しが終わると pipe2 の読み出し口が不要になるので閉じる。*)
Eio.Flow.close r2;

(* コマンドの実行を待機する。コマンドが異常終了すると例外が飛ぶ。*)
Eio.Process.await_exn com1;
Eio.Process.await_exn com2;
()

Eio.Process のリファレンスも参考にしてください。また、関数の呼び出し方は parse_out の実装を読むと参考になります。

注釈

  1. 「通常の Linux プログラミング」と書きましたが、自分は通常の Linux プログラミングをあまり真面目にやったことがないので、初めてこのコードを書いたときは見事にパイプを閉じるタイミングを間違え、コマンドが終了しなくなりました。