2020-5-20(Wed)
whiptail はターミナルに制御コードをふんだんに使ったダイアログを出し、ユーザー入力を得るプログラムで、色々な環境で使うことができる。whiptail を使ってとまどうのは、おそらく出力のリダイレクトなので、ちょっと復習してから whiptail に話に移る。なお、以下で whiptail にかんする部分は、主に私の環境で man whiptail から得た情報をまとめただけのメモである。その他の部分も含めて、間違えたこと書いてたらごめん。
目次
whiptail はダイアログを描き出すための制御コードてんこ盛りなテキストを標準出力に出力し、ユーザー入力を標準エラーに出力する。
だからうっかり whiptail にパイプにつないでユーザー入力を得ようとすると奇妙なことになる。
$ whiptail --inputbox "type your text" 10 30 | cat -v ^[[?1049h^[[1;24r^[[4l^[[?25l^[(B^[[m^[[37m^[ (以下略)
(ここで --inputbox はダイアログの種類、10 は表示高、30 は表示幅)
これは、ダイアログ画面表示のための制御文字たっぷりな文字列をパイプから cat に送ってしまったのだ。
次のようにすれば、ダイアログ画面のための出力でなくユーザー入力がパイプに流れ込むようになる。
$ whiptail --inputbox "type your text" 10 30 3>&1 1>&2 2>&3 | ユーザー入力の処理
また、次のようにして変数 a にユーザー入力をおさめることができる。
$ a=$(whiptail --inputbox "type your text" 10 30 3>&1 1>&2 2>&3)
3>&1 1>&2 2>&3 は一体何をやっているのかというと、標準出力と標準エラーの行き先の交換をしているのである。つまり、ふつうならパイプの入口に向かう標準出力を端末に向け、ふつうなら端末に向かう標準エラーをパイプの入口に向けてしまおうというわけだ。
$ whiptail --inputbox "type your text" 10 30 3>&1 1>&2 2>&3 | ユーザー入力の処理
とやるとき使われる標準出力と標準エラーの行き先の交換をするための呪文
3>&1 1>&2 2>&3
は何をやっているのか。3&>1 1>&2 2>&3 は、左から順に解釈されるので、順番に見ていくことにする。
はじめ、whiptail プロセスのファイルディスクリプタ 1 はパイプの入口を、ファイルディスクリプタ 2 は端末を指しているとする。(上で例示したコマンドラインを実行すると 3&>1 1>&2 2>&3 が解釈される直前には実際にこうなっているのだが、その理由は次節参照)
①まず、3&>1 だ。ファイルディスクリプタ3を新たに開き、これが「ファイルディスクリプタ1が指すところ」すなわちパイプの入口を指すようにする。
②次に、1>&2 だ。これは、ファイルディスクリプタ1が「ファイルディスクリプタ2が指すところ」すなわち端末を指すようにする。これによって、whiptail の標準出力が端末に送られるようになる。
③最後に、2>&3 だ。これは、ファイルディスクリプタ2が「ファイルディスクリプタ3が指すところ」すなわちパイプの入口を指すようにする。これで、whiptail の標準エラーがパイプに送られるようになる。
これにて取り替えっこ完了である。
ファイルディスクリプタ3は、ファイルディスクリプタ1が指している場所を一時的に指し示しておくために使われただけなので、最後に 3>&- をやって閉じてしまっても構わない。もっとも、whiptail プロセスが終了すればこの不要なファイルディスクリプタも消えるから、あえてやらないでもよい。
$ whiptail --inputbox "type your text" 10 30 3>&1 1>&2 2>&3 | ユーザー入力の処理
とすることで、うまく whiptail からユーザー入力をパイプに流せることはすでに述べた。このとき whiptail プログラムが起動するまでに起こることを見てみることにする。
(1) fork によって(将来 whiptail プロセスとなる予定の)新たなプロセスが作られる。このときこのプロセスのファイルディスクリプタは1番も2番もいつも通り、端末(私の場合は /dev/pts/1 みたいな仮想端末だ)を指している。
(2) パイプが用意され、(1) で作ったプロセスのファイルディスクリプタ1番がこのパイプの入口に向けられる。ファイルディスクリプタ2番は相変わらず端末を向いたままだ。ここまでは普通のパイプラインと変わらない。
(3) 3>&1 1>&2 2>&3 が解釈され、ファイルディスクリプタ1番とファイルディスクリプタ2番の向かう先が交換される。
(4) exec によってこのプロセスが whiptail プロセスに変わる。
(2)の後に(3)が行われたというところ、つまり「|」が「3>&1 1>&2 2>&3」より先に解釈されたということが重要だ。もしこの順序が逆であったならば、「3>&1 1>&2 2>&3」が何をしようとも、後で「|」によってファイルディスクリプタ1番は強制的にパイプの入口に向けられることになる。(ついでに言うなら、このとき 3>&1 1>&2 2>&3 は行き先が同じ端末になっているディスクリプタ1 と 2 に対して、その行き先を交換するという空しいことをやることになる)
なぜコマンドライン中で先に書いてある 3>&1 1>&2 2>&3 が「|」より後に効果を発揮するのかということは、仕様といえばそれまでだが、実装の様子を少し想像すると理解しやすいかもしれない。まずシェルはコマンドライン中に | を発見すると popen システムコールを呼び、'whiptail --inputbox "type your text" 10 30 3>&1 1>&2 2>&3' を渡す。popen はパイプをカーネルにあつらえさせて、ファイルディスクリプタ1番をその入口に向け、それから execve システムコールを呼び 'whiptail --inputbox "type your text" 10 30 3>&1 1>&2 2>&3' を渡す。ここではじめて 3>&1 1>&2 2>&3 が解釈され、ファイルディスクリプタ1番の行き先とファイルディスクリプタ2番の行き先が交換される。(と私は想像しているが、違うかもしれない)
もしあなたがへそ曲がりで、どうしても 3>&1 1>&2 2>&3 をパイプの処理より先にやりたいというのなら、
$ exec 3>&1 1>&2 2>&3 $ whiptail --inputbox "type your text" 10 30 | ユーザー入力の処理
とやってみれば、空しさを味わうことができるだろう。
$ whiptail --yesno "タマネギ食べれる?" 10 30
〈はい〉を押すと、終了ステータスが 0、〈いいえ〉を押すと終了ステータスが 1 となる。ようするに、直後に $? を調べればよい。この手のダイアログは、みなその伝でいく。
$ --msgbox "ぞーさんぞーさん" 10 30
$ echo $term xterm-256color $ old=$term $ TERM=vt100 $ whiptail --infobox "ぞーさんぞーさん" 10 30 $ TERM=$old
ターミナルのことわからんけど、xterm や gnome-term の初期設定では駄目だそうだ。しらんけど。
$ whiptail --inputbox "氏名を入力" 10 30 "野原しんのすけ" 3>&1 1>&2 2>&3
このスクリプトは、ユーザー入力をパイプで捕えられるようにディスクリプタの向きを変えている。改行は付加されない
whiptail --passwordbox "パスワードを入力" 10 30 abc 3>&1 1>&2 2>&3
このスクリプトは、ユーザー入力をパイプで捕えられるようにディスクリプタの向きを変えている。。改行は付加されない
$ cat kazehakase.txt 諸君は、東京市某町某番地なる風博士の邸宅を御存じであろう乎? (以下略) $ whiptail --textbox kazehakase.txt 20 40
ちなみにこれは坂口安吾「風博士」の冒頭。
$ whiptail --menu "好きな歴史上の人物を選んでください" 14 30 5 \ yoshinobu 徳川慶喜 katsu 勝海舟 yamaoka 山岡鉄舟 \ oguri 小栗忠順 ishimitsu 石光真清 3>&1 1>&2 2>&3
第一コラムはタグと呼ばれるもので、whiptail 終了時には選択された行のタグが標準エラーとして出力される。この例では、それをパイプで捕えられるようにディスクリプタの向きを変えている。
--notags オプションを用いれば、タグを表示しないようにできる。
$ whiptail --checklist "好きな大リーグボールを選んでください" 10 30 2 \ one 大リーグボール1号 two 大リーグボール2号 3>&1 1>&2 2>&3
選択はスペースキー入力で行う。複数解答可。
第一コラムはタグと呼ばれるもので、whiptail 終了時には選択された行のタグが標準エラーとして出力される。この例では、それをパイプで捕えられるようにディスクリプタの向きを変えている。
複数の選択肢が選ばれた場合はタグが "one" "two" のようにクオートされて出力される。--separate-output オプションをつけた場合は、複数のタグは改行で区切られ、クオートは行われない。
$ whiptail --radiolist "鎌倉幕府開幕はa,b,cのうちどれ?" 10 30 3 \ a 11年 on b 1192年 off c 1192296年 off 3>&1 1>&2 2>&3
ファイルから進捗を読取ってプログレス・バーを表示せむ
$ cat input.txt 10 20 30 40 XXX 50 半分まできたよ XXX 60 70 80 XXX 90 あと少しだ XXX 100 $ while read P do echo $P sleep 1 done < input.txt | whiptail --gauge 進捗 10 30 0
このスクリプトはじつはちょっと問題ありだ。というのは、XXX やプロンプト(「半分まできたよ」など)を読み込んだあとにも 1 秒待ってしまうから、そこでもたつく。
プログラムは(あるいはプロセスは)、ファイルに直接出力しないで、ファイルディスクリプタに向けて出力する。ファイルディスクリプタはカーネルに管理されていて、自分のところに来たテキストストリームをファイルに向ける。ファイルディスクリプタがどこを指しているかしだいで、テキストストリームは端末に表示されたり通常のファイルにおさまったりする(端末なんかもファイルの一種と考えられている)。何番のファイルディスクリプタがどのファイルを指すかは、各プロセスごとに変更できる。これを利用して、われわれはプログラムの出力先を変更することができる。たとえば 1>&2 は、ファイル・ディスクリプタ1番を、「現在ファイル・ディスクリプタ2番が指しているファイル」を指すようにせよという指示だ。
——と、ここまではいい。問題は標準入力・標準出力・標準エラーとは何かということだ。それはファイル(端末を含めて)なのか、ファイルディスクリプタなのか、テキスト・データそのものなのか、テキスト・データが流れる道筋なのか。
まず古い本を見てみる。新しいものだけ見ればいいのだろうけれど、ちょっと気になったのだ。1984年に英語版が出ている『UNIXプログラミング環境』 では「プログラムはオープンされたファイルを 3 個受継ぐ。その 3 つのファイルには 0, 1, 2 のファイル指定子がついていて、それぞれ、標準入力、標準出力、標準エラー出力と呼ばれている。」(p.302)のようにあり、標準入力・標準出力・標準エラーはファイル(端末を含めて)であるように読めるし、指定子(ファイルディスクリプタ)であるようにも読める。(原文を見てない)。
また1988年に英語版が出ている『プログラミング言語C第2版』 では
「テキストの入力あるい出力は、それがどこで発生したか、あるいはどこへ出力するかにはよらずに、文字のストリーム(流れ)として扱われる。このテキスト・ストリームは、行に分割された文字の列である(p.19)」、「stderr と呼ばれる第 2 の出力ストリームを……プログラムに割り当てればよい。……stderr に書かれる出力は……普通は画面に出力される(p.199)」のようにあり、標準入力・標準出力・標準エラーがストリームであると読める箇所がある。
そして 2017年に書かれたISO/IEC 9899:2018(C18)のドラフト(pdf)の7.21.3には、
At program startup, three text streams are predefined and need not be opened explicitly —standard input(for reading conventional input),standard output(for writing conventional output), and standard error(for writing diagnostic output).
プログラムが起動したとき、3 つのテキスト・ストリームはあらかじめ定義されていて、明示的にオープンしなくてもよい。標準入力(慣習的な入力を読みとるためのもの)、標準出力(慣習的な出力のためのもの)、標準エラー(診断的出力を書くためのもの)である。
とある。これからすると、標準入力、標準出力、標準エラーはテキスト・ストリームであり、「開く(open)」対象となりうるものであるということになる。
また、2013年の Advanced Programming in the UNIX Environment Third Edition には
Three streams are predefined and automatically available to a process: standard input, standard output, and standard error. These streams refer to the same files as the file descriptors STDIN_FILENO, STDOUT_FILENO, and STDERR_FILENO...(p.145)
とある。「標準入力、標準出力、標準エラーは事前に定義されたストリームであり、プロセスが使えるように自動的に用意され」、「0,1,2 番のディスクリプタが指し示す(refer to)ファイルと同じファイルを指し示している」というのである。
どうやら「標準入力はファイルディスクリプタ0番を通って入るデータの流れ道」「標準出力はファイルディスクリプタ1番を通って出るデータの流れ道」「標準エラーはファイルディスクリプタ2番を通って出るデータの流れ道」と理解しておいて、さほど問題はなさそうである。
おわり