概要
Rust には CUI 上で可視性の良いプログレスバーを表示できる Indicatif というライブラリがある。mise で Indicatif を使用するプログラムを実行するとき、タスクの依存がない、または単一の依存の場合はプログレスバーが表示されるが、複数に依存する場合はプログレスバーが表示されないという挙動を見つけた。
結果的に、単一タスクか複数(並列)タスクかで mise が自動で挙動を変えているため、複数タスク時に TTY 端末が割り当てられないという動きが原因だった。-o interleave
で内部的なパイプをしないようにすれば正しく表示されるようになる。
バグではないが、mise の挙動を知らないとハマりそうなので、検索で引っかかるように Qiita に記録しておく。Rust かどうかにかかわらず、カラー出力 (colored, termcolor, ansi_term など) を伴うプログレスバー (pbr, progress-bar など)、スピナー、死活表示、シンタックスハイライト、ログフォーマット、また端末制御 (crossterm や termion など) を行うカーソル位置制御、画面クリア機能など、TTY を検出して CUI を制御しているプログラムは同じような影響を受けるだろう。
挙動
以下のようなプログラムがある。
use indicatif::{ProgressBar, ProgressStyle};
use std::thread;
use std::time::Duration;
fn main() {
let pb = ProgressBar::new(100);
pb.set_style(
ProgressStyle::default_bar()
.template("Preparing: {spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})")
.unwrap()
.progress_chars("#>-"),
);
if pb.is_hidden() {
eprintln!("### progress bar is hidden");
}
for i in 0..=100 {
pb.set_position(i);
thread::sleep(Duration::from_millis(50));
}
pb.finish_with_message("finished");
let is_tty = console::user_attended_stderr();
eprintln!("TTY detection: {}", is_tty);
eprintln!("Is stderr a TTY?: {}", atty::is(atty::Stream::Stderr));
}
[package]
name = "mise_indicatif"
version = "0.1.0"
edition = "2024"
[dependencies]
indicatif = "0.18.0"
console = "0.16"
atty = "0.2"
Indicatif は画面制御のために大量のエスケープシーケンスを出力する。出力先が TTY 端末であればそれで良いが、CI 環境のようにパイプやリダイレクトされている場合はゴミ情報となるためプログレスバーを表示しないようになっている。
以下では、通常の起動ではプロセスに TTY が割り当てられているためプログレスバーが表示される。しかし、パイプやリダイレクトのような TTY が検出できない実行では何も表示しない。
$ cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.15s
Running `target/debug/mise_indicatif`
Preparing: [00:00:05] [########################################] 100/100 (0s) ✅
TTY detection: true ✅
Is stderr a TTY?: true ✅
$ cargo run 2>&1 | more
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
Running `target/debug/mise_indicatif`
### progress bar is hidden ✅
TTY detection: false ✅
Is stderr a TTY?: false ✅
さて、mise で次のような タスクを作成したとする。
[tools]
rust = "1.89"
[tasks.setup]
run = "echo setup"
[tasks.format]
run = "cargo fmt"
[tasks.single1]
depends = ["setup"]
run = "cargo run"
[tasks.single2]
depends = ["format"]
run = "cargo run"
[tasks.multiple]
depends = ["setup", "format"]
run = "cargo run"
2 つの単一実行タスクはそれぞれプログレスバーが表示されるが、それらを組み合わせた複数タスクのケースでは TTY が検出できなくなってプログレスバーが表示されない。
$ mise run single1
[setup] $ echo setup
setup
[single1] $ cargo run
Preparing: [00:00:05] [########################################] 100/100 (0s) ✅
TTY detection: true ✅
Is stderr a TTY?: true ✅
Finished in 5.33s
$ mise run single2
[format] $ cargo fmt
[single2] $ cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
Running `target/debug/mise_indicatif`
Preparing: [00:00:05] [########################################] 100/100 (0s) ✅
TTY detection: true ✅
Is stderr a TTY?: true ✅
Finished in 5.46s
$ mise run multiple
[format] $ cargo fmt
[setup] $ echo setup
[setup] setup
[setup] Finished in 2.6ms
[format] Finished in 127.9ms
[multiple] $ cargo run
[multiple] ### progress bar is hidden ❌
[multiple] TTY detection: false ❌
[multiple] Is stderr a TTY?: false ❌
[multiple] Finished in 5.28s
Finished in 5.41s
解決策
mise の出力オプション -o
または --output
に interleave
を指定する。
$ mise run multiple -o interleave
[setup] $ echo setup
[format] $ cargo fmt
setup
[multiple] $ cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.18s
Running `target/debug/mise_indicatif`
Preparing: [00:00:05] [########################################] 100/100 (0s) ✅
TTY detection: true ✅
Is stderr is TTY?: true ✅
Finished in 5.46s
または mise.toml
に以下のような出力設定を追加しておくか、環境変数 MISE_TASK_OUTPUT
も利用できる。
[settings]
task_output = "interleave"
問題の根本原因は mise の出力モード選択ロジックにある。単一タスクの場合は stdout/stderr へ直接出力する interleave モードで動作するので起動プロセスから TTY が検出できる。複数タスクの場合は並行する出力がどのタスクのものかを識別できるように prefix モードで動作する。この prefix モードは起動プロセスの出力をパイプしてバッファリングし、行単位で [setup] などのプレフィクスを付けて stdout/stderr へ出力する。このため prefix モードで動作するとプログラムからは TTY が検出できず、結果的に Indicatif は出力がパイプやリダイレクトされていると見なしてプログレスバーを非表示にする。