Ubuntu Weekly Recipe

第422回whiptailでCUIをグラフィカルにする

CUIでシェルスクリプトや設定コマンドを書いていると、自分以外の誰かが使う可能性を考えて、よりユーザーフレンドリーなインターフェースにしたいケースが稀にあります。今回はそんなケースで使える、⁠whiptail」を紹介します。

whiptailとは?

whiptailはnewtと呼ばれるテキストモード(CUI)用のウィンドウツールキットに同梱されているプログラムです[1]⁠。具体的にはDebianパッケージの設定システムであるdebconfや、Xが動いていない環境でim-configが設定ダイアログを表示するときに使用しています。

このwhiptailをシェルスクリプトと組み合わせて使うと、CUI上でも簡単に問い合わせダイアログや選択用のチェックボックス、進捗バーの表示を作れることができるのです。しかも、whiptailパッケージはdebconfが依存している都合上、どのようなUbuntuマシンでも最初からインストールされています。よって環境依存性をあまり考慮しなくても使えるというメリットも存在します。

図1 ⁠どっちだよ!」とツッコミたくなるダイアログも簡単に作れる
画像

whiptailの使い方

whiptailはただのコマンドです。whiptailコマンドを実行したら、戻り値や標準出力・標準エラー出力に表示される値を元に次の処理を行うようにシェルスクリプトを書くだけです。シェルスクリプトを書いたことがある人であれば、あとはmanページを読めば、やりたいことを実現できるでしょう。

そこでこのRecipeでは「どんなことができるの?」という観点で、すべての機能をスクリーンショット付きでお届けします。

whiptailの基本的な書式

whiptailの基本的な書式は以下のとおりです。

$ whiptail レイアウトオプション ボックスオプション

レイアウトオプションにはボックスタイトルやボックスの位置といった、ボックス共通のレイアウトに関するオプションを設定します。たとえば「--backtitle」は画面左上に文字列を表示し、⁠--topleft」はダイアログを画面中央ではなく左上隅に表示します。

ボックスオプションにはボックスの種類や種類ごとのオプションについて設定します。たとえばチェックボックスなら選択肢の一覧を指定しますし、入力ボックスなら初期値を設定します。

ちなみにlibnewtはgettextによる翻訳をサポートしています。その都合上、⁠Ok/Cancel/Yes/No」は日本語ロケールならすべて「了解/取消/はい/いいえ」と翻訳されます。今回の記事でOKボタンやYesボタンと言及していても、ボタンのラベルはロケールによって異なることがあるので注意してください。

Yes/Noダイアログ

Yes/Noダイアログはその名のとおり、Yes/Noボタンを表示するダイアログです。

$ whiptail --yesno 説明 ダイアログの高さ ダイアログの幅

高さと幅は数値で指定します。説明に指定した文字列が幅を超えると自動的に改行します。両方とも0にすると、適切な高さと幅を自動的に設定します。

たとえば次のコマンドは、図1のキャンセルダイアログを表示するコマンドです。

$ whiptail --title "キャンセルしますか?" --no-button "キャンセル" --yesno "本当にキャンセルしますか?" 8 50

「--title」はダイアログのタイトルになります。⁠--no-button」はNoボタンのラベルを変更するオプションです。⁠--yes-button」オプションを使えば「はい」も変更できますし、⁠--cance-button」「--ok-button」オプションも存在します。

メッセージボックス

メッセージボックスは、メッセージとOKボタンのみを表示するダイアログです。ユーザーに何か情報を通知し、ユーザーがそれを確認するまで待つ場合に使用します。

$ whiptail --ok-button "はい" --msgbox "もし わしの みかたになれば\nせかいの はんぶんを\nおまえ に やろう。" 10 25

上記のように文字列の中に\nによって改行文字を入れることも可能です。ただしANSIエスケープコードなどは埋め込めません。

図2 ⁠どうじゃ? わしの みかたに なるか?」
画像

インフォボックス

インフォボックスはメッセージボックスと似ていますが、ユーザーの操作を待たずに即座に終了します。表示されたメッセージは画面に残るので、ノンインタラクティブなシェルスクリプトで通知だけ残したい場合に便利です。

ただしxtermやGNOME端末では問答無用でメッセージがクリアされるため、そのままでは使えません。もしこれらの端末ソフトウェア上で使用したい場合は、次のように環境変数TERMにxterm系以外を設定しましょう。

$ TERM=vt100 whiptail --title "たかしへ" --infobox "おやつはVT100の中にあります。" 7 45
図3 VT100を冷蔵庫代わりに使うかーちゃん
画像

入力ボックス

入力ボックスは、ユーザーが自由に入力できるダイアログを表示します。

$ whiptail --inputbox \
"\n 何について調べますか?\n ────────────────v─────\n                  🐬" \
0 0 "お前を消す方法"

入力した内容は「標準エラー出力」に表示されます。コマンド置換などで結果を取得したい場合は、後述の「メニューボックス」の項目を参照してください。キャンセルボタンを押した場合は、戻り値が1となり標準エラー出力には何も表示されません。

入力ボックスの場合は、高さと幅の数字の後ろに入力ダイアログの「初期値」を指定できます。

図4 おはようからおやすみまであなたを見つめるカイルくん
画像

パスワードボックス

パスワードボックスも入力ボックスと同様にユーザーの入力を受け付けます。パスワードボックスの場合、入力された文字が*でマスクされます。

$ whiptail --title "バズワードのセットアップ" \
--passwordbox "\nカタカナと英数字を利用し、技術的な用語と一般的な用語を組み合わせることで、\
それっぽいバズワードになります。\n\n新しいバズワード:\n" 13 70

当たり前のことではありますが、マスクされるのは表示部分だけです。入力した文字列は、入力ボックスと同様に標準エラー出力にマスクされずに表示されます。

ちなみに入力ボックスと同様に初期値を指定できます。ただしマスクされているため、ユーザーは初期値が何であるかはわかりません。さらにシェルコマンドとして記述する都合上、初期値はマスクされずにスクリプトやコマンド履歴に残ることになります。よってパスワードボックスで初期値を使うことはまずないでしょう。

図5 毎年新たなバズワードが生まれては消えていく
画像

テキストボックス

テキストボックスは任意のファイルの内容を表示するダイアログです。ファイル名を指定すれば、それを表示します。またファイル名に/dev/stdinを指定すれば、ヒアドキュメント経由でコマンドの結果を渡せます。

$ whiptail --textbox /dev/stdin 0 0 \
<<<"$(curl http://changelogs.ubuntu.com/changelogs/pool/main/n/newt/newt_0.52.18-1ubuntu2/changelog)"

ボックスの幅と高さを指定した場合は、その領域に入るサイズでしかファイルを表示しません。幅と高さを指定しなかった場合は、ボックスのサイズをあふれる時に、カーソルキーやPageUp/PageDownキーでスクロールできます。幅と高さを指定しつつスクロールしたい場合は「--scrolltext」オプションを使用してください。

図6 ダブルクオーテーションでくくれば改行も表示される
画像

メニューボックス

メニューボックスはその名のとおり、選択式のメニューを表示するダイアログです。ボックスの高さと幅の後ろに、メニュー領域の高さを指定した上で、⁠タグ 項目」の文字列をメニューの数だけ繰り返してしていきます。

選択肢を選んだ上でEnterキーを押すか、OKボタンを押すと標準エラー出力に「タグ」の方が出力されます。Cancelボタンを押すと戻り値が1となり、標準エラー出力には何も表示されません。

シェルスクリプト内部でwhiptailを使う場合、その選択結果をコマンド置換を用いて変数に代入したいことがあるでしょう。単純に標準エラー出力を標準出力にリダイレクトする方法だと、whiptailのUIまで変数に入ってしまいます。そこで以下のコマンド3>&1 1>&2 2>&3では、標準出力と標準エラー出力を入れ替えています。

$ choise=$(whiptail --title "選ばれたのは" --menu "選ばれたのは?" 0 0 0 \
"綾鷹" "コカ・コーラ" "伊右衛門" "サントリー" "お〜いお茶" "伊藤園" "生茶" "キリンビバレッジ" \
3>&1 1>&2 2>&3) && echo "選ばれたのは「${choise}」でした。"

この方法は、入力ボックスやパスワードボックス、チェックリスト、ラジオボタンなどでも使用できます。なお「--output-fd」オプションを使えば、出力先のファイルディスクリプタを直接指定できます。

上記コマンドでは高さと幅だけでなく、メニューの高さも0に設定しているため、メニューが収まる適切なサイズのボックスが作成されます。

「--notags」を使うと、タグを非表示にできます。どれを選んでも結果的に「綾鷹」にしたい場合に便利です。

$ choise=$(whiptail --notags --title "選ばれたのは" --menu "選ばれたのは?" 0 0 0 \
"綾鷹" "綾鷹" "綾鷹" "伊右衛門" "綾鷹" "お〜いお茶" "綾鷹" "生茶" \
3>&1 1>&2 2>&3) && echo "選ばれたのは「${choise}」でした。"
図7 ⁠選ばれたのは綾鷹でした。」
画像

チェックリスト

チェックリストは複数選択可能な選択ボックスです。メニューボックスが一つを選択すれば終了なのに対して、チェックリストではどれを選ぶか、選ばないかを試行錯誤した上で、確定できます。

$ whiptail --title "好きなお菓子" --checklist "あなたが好きなお菓子は?" 0 0 0 \
"きのこの山" "" OFF \
"たけのこの里" "" OFF \
"すぎのこ村" "" OFF \
"コアラのマーチ" "" OFF \
"バームロール" "" OFF \
"ホワイトロリータ" "" ON

チェックリストではタグと項目に加えて、初期状態を示すフラグも指定します。OFFならチェックが外れている状態で、ONならチェックがついています。大文字小文字は区別しません。なお、項目については上記のように""と指定することで省略できます。⁠--noitem」オプションを使うと、""すら不要になります。

標準エラー出力には選択したタグのリストが出力されます。たとえば上記のコマンドで「きのこの山」⁠バームロール」⁠ホワイトロリータ」を選択すると、結果的に以下のような出力になります。

"きのこの山" "バームロール" "ホワイトロリータ"

シェルスクリプト側はこれを受け取って、何が選択されたかを判断します。

図8 平和の象徴「ホワイトロリータ」
画像

ラジオボタン

ラジオボタンは一つしか選択できないチェックリストです。コマンドの使い方はほぼ同じです。ユーザーはどれか一つを選択すると、他の選択項目がOFFになります。

$ whiptail --title "好きなお菓子" --radiolist "あなたが **最も** 好きなお菓子は?" 0 0 0 \
"きのこの山" "" OFF \
"たけのこの里" "" OFF \
"すぎのこ村" "" OFF \
"コアラのマーチ" "" OFF \
"バームロール" "" OFF \
"ホワイトロリータ" "" ON
図9 トロイアの王子パリスをも悩ませる究極の問い
画像

進捗バー

進捗バーは進捗状態にあわせてアニメーションするバーです。また、バーの内側にはパーセンテージの表示も行われます。

アニメーションをさせるためには、継続的に標準入力から進捗状態のパーセンテージを通知する必要があります。さらに数字ではなく「XXXnパーセンテージn文字列nXXX」という文字列を送ると、パーセンテージの更新とともに、パーセンテージの次の行の文字列が、ダイアログの中の文字列で置き換わります。

このあたりは若干ややこしいので実際にやってみると良いでしょう。

$ for i in `seq 0 10 90` `seq 90 -10 0`; do sleep 0.5; \
if [ $i -eq 90 ]; then echo "XXX"; error=1; echo $i; echo "致命的なエラー";echo "XXX"; \
elif [ $i -gt 50 ]; then echo "XXX"; echo $i; \
  if [ "$error" != "1" ]; then echo "おや、いけそう?"; else echo "あっ、あっ、あっ"; fi; echo "XXX"; \
elif [ $i -le 30 ]; then echo "XXX"; echo $i; echo "進捗ダメです。"; echo "XXX"; \
else echo $i; fi; done \
| whiptail --title "進捗どうですか?" --gauge "進捗ダメです。" 0 0 0

whiptailのオプションそのものはシンプルです。最後の数字はパーセンテージの初期値です。

上記をシェルスクリプトとして書き直すと次のようになります。

#!/bin/bash

for i in `seq 0 10 90` `seq 90 -10 0`; do
    sleep 0.5;

    if [ $i -eq 90 ]; then
        echo "XXX"; error=1; echo $i;
        echo "致命的なエラー";echo "XXX";
    elif [ $i -gt 50 ]; then
        echo "XXX"; echo $i;
        if [ "$error" != "1" ]; then
            echo "おや、いけそう?";
        else
            echo "あっ、あっ、あっ";
        fi;
        echo "XXX";
    elif [ $i -le 30 ]; then
        echo "XXX"; echo $i; echo "進捗ダメです。"; echo "XXX"; else echo $i
    fi;
done | whiptail --title "進捗どうですか?" --gauge "進捗ダメです。" 0 0 0
図10 人生には進捗が巻き戻ることだってある
画像

おすすめ記事

記事・ニュース一覧