Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般

CLIツールのUX設計ベストプラクティス: プログレス表示を改善する3つのパターン(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

CLIツールのUX設計ベストプラクティス: プログレス表示を改善する3つのパターン(翻訳)

CLIツールを作るときは、進行状況をユーザーに表示する方法をプログレス表示で強化しましょう。世の中にあるコマンドラインアプリのほとんどは、開発エクスペリエンスを改善する余地をたくさん残しているものですが、ことプログレス表示については私は絶対なくてはならないと考えています。ターミナルエミュレータで実行するアプリやシェルスクリプトを書いていて、あと1つ何か改善したら時間切れという状況になったら、せめてプロセスが長時間実行されるときのプログレス表示はぜひ実装しておきましょう。

コンピュータがどれほど高速化されようと、かなり時間のかかるタスクが消えてなくなることはありません。たとえばソースコードのコンパイルは、Go言語で書かれた小規模アプリケーションなら一瞬で終わることもありますが、C++コードベースの大規模アプリだとたっぷり数時間かかる可能性もあります。ダウンロード時間も、ネット接続の帯域幅次第でいくらでもブレます。というぐらいに時間のかかるタスクはいくらでもリストアップできます。

本記事では、進行状況を知らせる一般的なユーザーインターフェイスとして「スピナー」「X of Y」「プログレスバー」という3つのパターンを見ていきます。それぞれのメリットとデメリットを検討して、自分たちのニーズに合うものを選ぶコツを紹介します。

🔗 プログレス表示が重要な理由: 掌握感とユーザーの多様性

最初に、人間は「自分が対象を掌握できている」という感覚を好みますが、これは心理的なものです。コードのコンパイルやファイルのダウンロードといったタスクでは、そこで行われている操作を人間が自分の目で確かめられるようにすると、満足感が高まります。人間は、たとえ結果を変更できなくても、進行状況を自分の目で確かめられるようになっていれば、対象を自分が掌握しているという感覚を得られるものなのです。

動物研究や臨床研究、神経画像学から得られたエビデンスを総合した結果、何かを思い通りにしようとする欲求は、生存のために生物学的に不可欠であることが示されました。
Born to Choose: The Origins and Value of the Need for Controlより

次に、「コマンドラインアプリを使うのは、いかにもそれっぽいヒゲを生やしたLinuxユーザーしかいないはず」と決めつけてはいけません。新世代の若い開発者たちは、アプリのコンパイルやビルドやデプロイで普通にCLIツールを使いこなしていますし、生まれたときからノートPCやスマホに囲まれて育った世代は、昔よりもソフトウェアのUXに多くのことを期待するものです。

だからこそ、最新の進行状況を刻々と表示することの重要性は、GUIであってもCLIであっても変わらず、優れたUXが当然実践すべき機能の1つです。それでは、プログレス表示の実際を見ていくことにしましょう。

🔗 「何も表示しないまま放置」は避けること

ただし、コマンドラインアプリが何も表示しない方が適切な場合ももちろんあります。ビルドシステムから呼び出されることが多いコンパイラはその典型で、たとえばRustのコンパイラであるrustcは多くの場合cargoから実行されます。このようなシナリオであれば、コンパイラがほとんど何も表示しなくても問題ありません。

CLIを作るときは、アプリをサイレントモードで実行できる--quietフラグの追加も検討すること。
CLI Guidelinesより

しかしそのような場合でなければ、ユーザーにとって有用な何らかのステータス更新を表示することを強くおすすめします。ユーザーがじっと見つめている暗いターミナル画面で、カーソルを空しく点滅させたまま放っておいてはいけません。さらに、アプリがジョブを完了したら、人間が読み返してすぐ理解できるログが画面上に残るようにしましょう。

コマンドラインアプリを作るなら、こういうそっけない振る舞いは避けること

このアドバイスを軽く受け流したりせず、どうか真剣に受け止めてください。ユーザーをないがしろにしないために、以下で提案しているUIパターンのどれかを実装して、CLIのユーザーエクスペリエンスを強化することをお忘れなく。

🔗 1: スピナーはユーザーを安心させる「闇夜の灯り」

CLIでステータスを表示するときに、くるくる回転するスピナーアニメーションも追加することで、特に時間のかかる処理で「私は止まっていませんよ、動いていますよ」というステータスを手軽に示せるようになります。

CLIで使えるシンプルなプログレスインジケータ

以下は、Go言語でCharmのhuh?というライブラリを用いて実装できるスピナーの例です。

// import ("github.com/charmbracelet/huh/spinner")

_ = spinner.New().
  Title("Saving config file...").
  Action(longOperation).
  Run()

charmbracelet/huh - GitHub

ただし、スピナーにはシンプルであるがゆえの欠点もあります。スピナーは「ただいま処理中です」以上の情報を伝えられません。さらに重要なのは、アプリが無限ループに陥った場合でも表示が変わらないので、ユーザーがループに気づけなくなってしまうことです。

コマンドラインでは、この最後の問題についてはプログラムで特定の操作が実際に完了したらスピナーを更新する、という解決方法が一般に使われます。
たとえば、アプリが複数のファイルを処理しているのであれば、個別のファイル処理が終了するたびにスピナーを更新する方法が使えるでしょう。こうすることで、スピナーは単に「現在処理中です」というシグナルをユーザーに伝えるのみならず、処理に時間がかかりすぎている場合にスピナーが更新されなくなることで、何か潜在的な問題が生じている可能性をユーザーに伝えることも可能になります。

点字アルファベット文字はCLIのスピナーで多用されます

点字(braille)アルファベット文字はCLIのスピナーで多用されます。

では、プログレス表示にスピナーが最適なのはどんな場合でしょうか?

  • 1個のタスクやタスクのシーケンスが短時間(せいぜい数秒以内)に終了すべき場合
  • スピナー以外の情報を表示する必要がない場合(詳しくは次のセクションで後述します↓)

🔗 2:「X of Y」パターンは表示するデータがある場合に最適

私は、誰もがこの「X of Y」パターンを選択できるようにしたいと本気で願っています。このパターンで表示するデータを取得できない場合やデータ取得の負荷が大きすぎる場合もないわけではありませんが、開発者は進行中の処理に関する何らかのメトリクスを取得できるのが普通です。

標準的なフォーマットは「X / Y」です。Xには処理が完了した項目の件数や量を表示し、可能であれば処理すべき全体の件数や量をYに表示します。


現在の行の末尾に(X / Y KB)を表示することで、ユーザーの不安を和らげるのに十分な情報が得られます。

Go言語用のuiliveライブラリを使うと、このパターンをわずか数行で手軽に実装できます。

// import ("github.com/gosuri/uilive")

fmt.Println("Downloading libs...")
writer := uilive.New()
writer.Start()

for lib, count := range libs {
  for i := 0; i <= count; i++ {
    _, _ = fmt.Fprintf(writer, "  %s (%d / %dKB)\n", lib, i, count)
    // time.Sleep(time.Millisecond * 25)
  }
  _, _ = fmt.Fprintf(writer.Bypass(), "✓ %s\n", lib)
}

writer.Stop()

gosuri/uilive - GitHub

この「X of Y」パターンは、進捗を表示するのにうってつけです。数値が上昇していれば処理が順調であることがわかりますし、数値が動かなくなったら何か問題が生じている可能性があることがわかります。さらに、Yの値を表示できれば、ユーザーが残り時間をざっくり見積もるのにも有用です。
さらにイケてる感じにするには、この「X of Y」パターンにスピナーも加えてみましょう。この合せ技でCLIを究極の仕上がりにできるかもしれません。

まとめ: CLIでステップバイステップの処理を行う場合で、かつ進行状況のメトリクスを得られる場合は、常に「X of Y」パターンを選びましょう。そしてそれを標準にしましょう。

🔗 3: プログレスバーは同時処理される複数プロセスのプログレス表示に有用

プログレスバーは、「X of Y」パターンと組み合わせると俄然光り輝き、CLIのエクスペリエンスが1ランクアップします。1や2で得られるすべての情報に加えて、進捗がビジュアル表示されることで残り時間を一目で大づかみできるようになります。

複数行にわたる項目で更新される数値を人間が目で追いかけるのは大変なので、ビジュアル表示が有用

以下は、Go言語用のMulti Progress Barライブラリを用いて同時に動く複数のプログレスバーを表示するサンプルコードです。

// import (
//  "github.com/vbauerster/mpb/v8"
//  "github.com/vbauerster/mpb/v8/decor"
// )

fmt.Println("Downloading libs...")
var wg sync.WaitGroup
p := mpb.New(mpb.WithWaitGroup(&wg))
wg.Add(numBars)

for lib, count := range libs {
  name := fmt.Sprintf("  %s", lib)
  bar := p.New(int64(count),
    mpb.BarStyle().Lbound(" ").Filler("█").Tip("█").Padding("░").Rbound(" "),
    mpb.PrependDecorators(
      decor.Name(name, decor.WCSyncSpaceR),
      decor.CurrentNoUnit("%d /", decor.WCSyncSpace),
      decor.TotalNoUnit("%dKB", decor.WCSyncSpace),
    ),
    mpb.AppendDecorators(
      decor.OnComplete(
        decor.EwmaETA(decor.ET_STYLE_GO, 30, decor.WCSyncWidth), "Done",
      ),
    ),
  )

  // 何らかの処理をシミュレートする
  go func() {
    defer wg.Done()
    // ...
  }()
}

p.Wait()

vbauerster/mpb - GitHub

プログレスバーはGUIの世界では標準ですが、CLIの世界では場合によってはやりすぎの可能性があるかもしれません。私から言えることは、プログレスバーを採用するかどうかを決める前に十分考えておくことです。

プログレスバーは、「同じような複数のプロセス」が「かなり長い間」「並行して」実行される場合に最適です。このような場合、「X of Y」パターンだけだと複数の数値を追いかけるのがつらくなるので、プログレスバーをビジュアル表示するのが有効です。
逆に、単一のタスクを処理する場合や、複数のタスクを(並行ではなく)順次処理する場合は、わざわざプログレスバーを表示しない方がよいかもしれません。

プログレスバーを増やしすぎるとユーザーがかえって混乱する可能性もあります。これを解決する一般的な方法として、プログレスバーをアクションごとに表示するのではなく、以下のように全体の進行状況を1個のプログレスバーで代表させる方法も使えます。

全体の進行状況を1個のプログレスバーで表示する

全体の進行状況を1個のプログレスバーで表示する

なお、この例はCharmのpackage managerのデモにインスパイアされました。

🔗 P.S. 実行結果のログは散らからないように整頓しよう

ログの話をする前に一言ご注意申し上げます。
CLIのアクションが完了したら、スピナーやプログレスバーを忘れず消すことを習慣づけてください。ほとんどのライブラリはスピナーやプログレスバーを自動削除してくれますが、確実に削除されるようにしておきましょう。

次に、コマンドの詳しい実行結果がはっきりと伝わるように出力しましょう。どの項目が成功したかが一目でわかるよう、成功した項目にはチェックマークアイコン✅を緑色で表示します。

ユーザーが出力に色を含めたくない場合に備えて、--no-colorフラグやNO_COLOR環境変数などで色を無効にできるようにしておくこと。
CLI Guidelinesより

CLIアプリのstdio(標準出力)ストリームを>でリダイレクトして、テキストファイルに何が出力されるかを事前にチェックしておきましょう。

$ app > file.txt

あるコマンドの出力が、パイプ演算子|で別のコマンドの入力として使われる可能性についても考慮しておきましょう。CLIアプリの出力をgrepしたくなる状況は非常に多いので、ユーザーがgrepしたときに混乱しないようにしておきましょう。

プログラムで読み取り可能な出力を他の方法で提供できない場合は、CLIプログラムに--plainフラグを追加することを検討せよ。
CLI Guidelinesより

最後に私個人から皆さんにお願いがあります。
ステータスメッセージに含まれる進行形...ingは、実行が終わったら完了形や過去形...edに更新するのをどうか忘れないでください。

CLIでは、実行中のアクションをdownload-ingやcreat-ingなどのように動名詞(gerund)で表現することがよくあります。処理が完了するまではそれでよいのですが、処理が完了したあとも...ingのままでは誤解の元になります。アクションが完了したことをログで正確に伝えるには、...ingを単純過去形なり現在完了形なりに変更する必要があります。

本記事に含まれている動画サンプルをもう一度見返していただければ、どの動画サンプルもこの決まりを守っていることがわかります。


CLIの世界はUXデザイナーからあまり注目されておらず、無視されていると私は確信しています。多くのスタートアップ企業はWebアプリのUX強化に力を注いでいますが、開発者の日常業務になくてはならないはずのCLIにはそこまで力を注いでいないようです。

コマンドラインアプリで実現したい目標や課題がおありでしたら、元記事のフォームまでご相談をお寄せください。御社のプロダクト改善に役立つ実践的かつ実証済みのアドバイスを無償にて提供いたします。

関連記事

フロントエンド開発者がデザインに向き合うための厳選ベストプラクティス7選(翻訳)

AnyCable 1.0: RubyとGoによるリアルタイムWebの4年間(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。