標準入力から文字列を受け取るコマンドラインツールを開発しているとき、場合によってはパイプラインの存在も意識しなければなりません。

基本的にパイプライン入力は標準入力として扱われますが、入力がコマンドラインへの直接入力なのか、あるいはパイプラインからの入力なのかについて判定するにはひと工夫が必要です。判別する必要がないなら問題ありませんが、判別が必要な場合これでは不便です。

例えば:

  • パイプラインからの入力がない場合に、標準入力の入力待ちになるようなツールを作成したい場合。

    • この場合、標準入力からの入力は改行コードを以て1行分の入力として扱われる。

    • パイプラインからの入力があった場合は、全入力内容がパイプから標準入力へと入力される。

    • しかし、入力の境界は改行コードで判別されるため、パイプラインから入力された文字列の末尾が改行コードで終了していない場合、Enter キーの入力待ちになってしまう。

  • 標準入力からの入力の場合のみ、1行ずつ対話形式で処理を進めるツールを作成したい場合。

    • パイプラインからの入力に対しても同様に1行ずつ処理が走ってしまう。

    • 複数行まとめて処理させたいが、入力の境界がどこまでなのかが判別できない。

これまでは標準入力への入力を async-std などを使用して1~2秒程度待機し、「起動直後に複数行入力されている & 一定期間キー入力がない」といった状況の場合に、強制的に標準入力待ちを打ち切って処理を続行する、といった荒業で対応していました。

しかし、これを簡潔に解決してくれる便利なクレートがあったので紹介したいと思います。

解決策:atty

atty というクレートを利用します。

are you or are you not a tty?

の略らしいです。

要は入力に対して「お前はコマンドラインからの入力か?違うのか?」を判別してくれるもので、ソースコードを見る限り、

  • UNIX の場合は libc::isatty()

  • Windows の場合は Windows API の GetConsoleMode()

それぞれ判別しているようです。

GitHub - softprops/atty: are you or are you not a tty?

いずれも libc や OS 依存のハンドラ等を利用するため unsafe code を含みますが、実装は至ってシンプルでした。

利用方法

まずは Cargo.toml に追記(要:最新バージョンのチェック)

[dependencies]
atty = { version = "0.2.14" }

次に、atty::is(Stream::Stdin) で判定(bool)

use atty::Stream;
...
  if atty::is(Stream::Stdin) {
    ...
  }
...

簡単ですね。

動作確認

動作確認のため、パイプラインからの入力なら起動時に全文字列を取得し、そうでない場合は対話形式で1行ずつ文字列を表示するプログラムを作ってみました。

use atty::Stream;
use std::io::{self, Read};

fn main() {
    if atty::is(Stream::Stdin) {
        // パイプラインからの入力ではない
        println!("This is atty.");
        // 1行ずつ文字列を表示する
        loop {
            let mut buffer = String::new();
            io::stdin().read_line(&mut buffer).unwrap();
            print!("You typed: {}", buffer);
        }
    }
    else {
        // パイプラインからの入力である
        println!("This is not atty.");
        // 一気に全文字列を取得し表示する
        let mut buffer = String::new();
        io::stdin().read_to_string(&mut buffer).unwrap();
        print!("You typed: {}", buffer);
    }
}

まずはパイプライン入力無しのパターン:

$ cargo run
This is atty.
hi!
You typed: hi!
hello
You typed: hello
...

つづいてパイプラインから入力してみるパターン:

$ echo hello | cargo run --
This is not atty.
You typed: hello

このように、引数とパイプライン入力を区別することができました。

活用方法

前述の通り、パイプラインからの入力があれば起動時に全文字列を取得して処理し、そうでなければ標準入力からの入力待ち、とした場合に有用です。

今回は atty::is(Stream::Stdin) しか利用していませんが、Stream::StdoutStream::Stderr も指定可能です。その場合、出力先が標準(エラー)出力なのか否かを判別してくれます。

参考文献