標準入力からプログレスバーを利用する仕組み

cocoaDialogというアプリケーションがあって、インストールするとコマンド呼び出しから、GUIのダイアログ環境を提供してくれる。

例:
msgbox(メッセージボックス)
inputbox(インプットボックス)
secure-inputbox(セキュア インプットボックス)
dropdown(ドロップダウン)
fileselect(ファイルセレクト)
textbox(テキストボックス)


progressbar(プログレスバー
progressbar(プログレスバー

ひと通りのGUIが揃っていて、たいへん便利である。その中でも気になるのがプログレスバーの存在である。時間のかかる処理の進捗状況を教えてくれるあの棒グラフみたいなGUI。今まで単機能のコマンドツールを作っても、進捗状況を的確にフィードバックする方法がなくて困っていた。窮余の策として、Growlや通知センターを使って、「開始しました」「終了しました」なんてメッセージを出力して、しのいでいた...。

そんな時はCocoaDoalogのプログレスバーを利用すれば、快適な待ち時間を演出できそうである!

作業環境

インストール

  • 安定板と開発版がある
    • バージョン 2.1.1(stable=安定板)
    • バージョン 3.0.0-beta7(development=開発版)
  • ここではバージョン 3.0.0-beta7の開発版を試してみた。

  • ダウンロードしたCocoaDialogは、アプリケーションフォルダにコピーしてみた。

プログレスバーの特殊性

  • プログレスバー以外のダイアログは...
    • ダイアログが表示されている間は、処理を中断して、ユーザーの入力を待っている。
    • ユーザーの入力が完了したらダイアログを閉じて、入力値を受け取って、処理を再開する。
  • 一方、プログレスバーの場合は...
    • ダイアログが表示されている間も、平行して処理を継続している。
    • 処理の進捗状況に応じて、バーの長さやメッセージを随時更新する。
    • 処理が完了したら、連動してプログレスバーも閉じる。
  • つまり、プログレスバーはメインの処理とマルチタクスで実行される処理なのである。
  • しかも、メインの処理と連動して、プログレスバーの表示を随時更新する必要もある。

CocoaDialogのプログレスバーの仕組み

  • このマルチタスクと随時更新をどのように実現させているのか?
  • CocoaDialogのプログレスバーでは、更新に必要なデータを標準入力から受け取る仕様になっていた。
  • 更新に必要なデータとは、プログレスバーの割合(0-100の数値)とメッセージとしてのテキスト。
  • 半角スペースで区切って、以下のように指定するのだ。
$ echo 10 running... | /Applications/CocoaDialog.app/Contents/MacOS/CocoaDialog progressbar
  • しかーし、上記コマンドを実行しても、そのプログレスバーを確認する間もなく一瞬にして終了してしまう...。
  • なぜか?それはechoコマンドがすぐ終了してしまうので、それに連携したプログレスバーも終了しているのだ。
  • そこで、5秒ほど時間を稼いでみる。
$ { echo 10 running...; sleep 5; } | /Applications/CocoaDialog.app/Contents/MacOS/CocoaDialog progressbar
あるいは...
$ ( echo 10 running...; sleep 5; ) | /Applications/CocoaDialog.app/Contents/MacOS/CocoaDialog progressbar
  • 括弧で囲いsleepコマンドとグループ化して、それをパイプでCocoaDialogに渡してみた。

見えた!

{ 
  echo 10 running...; sleep 2; 
  echo 50 running...; sleep 3; 
  echo 100 running...; sleep 1; 
} | /Applications/CocoaDialog.app/Contents/MacOS/CocoaDialog progressbar
  • もうちょっと工夫して、forループで繰り返してみる。
for (( i = 1; i <= 100; i++ )); do 
  echo $i $i/100 running...; sleep .05; 
done | /Applications/CocoaDialog.app/Contents/MacOS/CocoaDialog progressbar

  • 終了までの時間が予測できない場合は、--indeterminateオプションでこんな表示にできる。
$ sleep 5 | /Applications/CocoaDialog.app/Contents/MacOS/CocoaDialog progressbar --indeterminate


ひとまずこれだけ知っていれば、プログレスバーを便利に使えそう!

まめ知識 ()と{}の違い

  • ちなみに、{ コマンド; ...; }と( コマンド; ...; )の違いは...
    • どちらも括弧内のコマンドをグループ化するが、(上例では括弧内のコマンドの標準出力は、すべてCocoaDialogに送信される)
    • ()では、サブシェルを起動して、サブシェルの環境でコマンドを実行する。
    • {}では、現在のシェル環境で、括弧内のコマンドを実行する。
    • ちなみにサブシェルとは、現在のシェルの子プロセスとして起動したシェルのこと。
$ ( sleep 5; )

$ { sleep 5; }
  • 上記を実行して、アクティビティモニタで観察してみると...
    • ( sleep 5; ) では、bashとsleepが起動した。
    • { sleep 5; } では、sleepのみ起動した。
  • つまり、()でグループ化して実行する場合には、現在のシェル環境に影響を与えない、という利点がある。
    • ()内でcdコマンドを実行するとか、
    • ()内外で同じ名前の変数を利用しているとか、
    • そんな場合でも、()外のカレントディレクトリや変数の内容に影響を与えないのだ。
  • 一方、{}でグループ化して実行した場合は、現在のシェル環境にそのまま影響してしまう...。
    • 但し、自分のbash環境 version 3.2.48(1)-release (x86_64-apple-darwin12) では...
    • パイプを利用した場合には、たとえ{}でグループ化してもサブシェル環境で実行された。
    • つまり、`{コマンド} | コマンド` == `(コマンド) | コマンド`なのである。

まめ知識 パイプのマルチタスク

  • パイプとは、あるコマンドの標準出力を、次のコマンドの標準入力に接続する方法である。
  • つまり、あるコマンドの処理結果を、次のコマンドでさらに加工できるのだ。
  • 単純なコマンドもパイプで組み合わせることによって、複雑な処理をこなせるようになる。

素晴らしい仕組みである!

  • ところで、コマンドA | コマンドB というパイプ処理があった場合、
  • まずコマンドAを実行して、その処理が終了したらコマンドBを起動して処理を引き継ぐ、ようなイメージを抱いてしまう。

しかし、現実は違う。

  • 現実は、コマンドAの終了を待たずに、コマンドBも起動する。
  • 順序としてはコマンドA、コマンドBの順だが、ほぼ同時に起動するのだ。
$ sleep 10 | cat | grep .
  • そして、以下のようなコマンドを実行すると、毎秒ごとに1から10までカウントが進行する。
  • つまり、forブロック、cat、grepがすべて起動した状態でパイプライン接続され、マルチタスク的に処理されているのだ。
$ for (( i=1; i<=10; i++ )); do echo $i; sleep 1; done | cat | grep .
1
2
3
4
5
6
7
8
9
10