Abstract
- 理由
- GnuWin版のwget(1.11.4-1)のコーディングミスにより、グローバル設定ファイルのパス情報が標準エラー出力(stderr)に常に出力されており、auto-install.elがwgetの標準出力(stdout)から取り込むelispファイルの末尾にそれが追加された状態で保存されるため。
- 対策
- 以下のいずれかを行なう
- 解説(というか余談)
- wgetの標準エラー出力を抑制しようとしてハマった。
理由
Emacsのauto-install.elは、EmacsWikiやgithubなどに置かれた各種elispファイルを、簡単な操作で選択→ダウンロード→インストール→バイトコンパイルしてくれる機能を提供するelispファイルだ。ダウンロードに関しては、Emacsのurl-retrieveで行なうか、外部コマンドのwgetを使うかを選択できる。現在は、「wgetが存在するならwgetを使う設定」がデフォルトだ。
Windows上のEmacs(NTEmacs)でも、GnuWin版のwgetやCygwin版のwgetを別途用意すればauto-install.elでwgetを利用できる。筆者は当初、(wgetのためだけにCygwinを導入するのは大げさなので)GnuWin版のwgetをインストールしていた。
ところが、GnuWin版のwget(1.11.4-1)には、auto-install.elと組み合わせる際にひとつ困った点がある。以下に示すような、"SYSTEM_GETWC
"や"syswgetrc
"で始まる文字列を常に標準エラー出力に吐き出すことだ。
これらは、ユーザーごとの設定ファイル(~/.wgetrc)が見つからないときに使われる、グローバルな設定ファイル(wgetrc)のパス情報を示している。SYSTEM_WGETRCで始まる行には、ビルド時に指定されたデフォルトインストールフォルダ以下のetc/wgetrc(GnuWin版ではc:/progra~1/wget/etc/wgetc
)が、syswgetrcで始まる行には、実際のインストールフォルダ(上の例では"c:\Users\hogehoge\Apps\wget
")以下のetc/wgetrcがそれぞれ表示される。
GnuWin版wget(1.11.4-1)のソースコードを調べたところ、これらのメッセージを出力しているのは、init.c
に含まれる関数initialize()内の2箇所のfprintf()だ。なお、オリジナルであるGnu Project Archivesのwget(1.11.4)のinit.cには該当部分が見あたらないので、GnuWin側で追加したものと思われる。
コードを一見すると、opt.verbose
を0(冗長出力の抑制を意味する値)に設定しておくこと、すなわち、wget起動時に--quiet
または-q
オプションを指定することで、メッセージを抑制できるように見える(このコードを書いたヤツも間違いなくそのつもりだ)。ところが、実際には、--quietオプションを付けたとしても、標準エラー出力(stderr)に問題のメッセージが常に出力されてしまう。
なぜなら、opt.verbose
には、ここより前の処理で初期値-1(未変更を意味する値)が設定されており、上記のコードを含むinitialize()が呼び出された後に、起動時オプションの解釈が行なわれるからだ。つまり、wgetが--quietオプションなどを解釈してopt.verbose
の値を適切(0または1)に設定するのは、上記のコードを実行した後なのだ。このため、上記のコードに2箇所含まれているif文の条件式opt.verbose
は、--quietオプションの有無にかかわらず常に成立する。
通常、wgetはダウンロードした内容を(標準出力ではなく)個々のファイルとして保存するため、この冗長なメッセージが常に出力される症状は、「なんかいつも変なメッセージがでるんだけど」程度のささいな問題に過ぎない。しかし、auto-install.elでは、wgetでダウンロードしたelispファイルを標準出力(stdout)経由でバッファに取り込み、ユーザーにインストールするかどうかを確認させる。GnuWin版のwgetを利用する場合、標準エラー出力(stderr)に吐き出された例のパス情報が、auto-install.elが利用するバッファの末尾に追加されてしまうのだ(このバッファはreadonly)。気がつかずにそのままelispファイルをインストールするとバイトコンパイルに失敗するし、elispファイルをロードする際にはエラーメッセージが表示される。もちろん、インストール前にreadonlyを解除して末尾2行を削除したり、インストール後のelispファイルから末尾2行を削除してバイトコンパイルしなおしてもいいのだが、いずれにしても毎回手作業で行なうのは面倒だ。
対策
対策としては、(手間のかからない順に)「auto-install.elでwgetを使わないようにする」か「auto-install.elでwgetが吐き出す余分な末尾2行を削除する」、「GnuWin版のwgetの代わりにCygWin版のwgetを使う」のいずれかだ。
ほかにも、wgetを呼び出している関数を再定義して、「auto-install.elでwgetを呼び出す際に標準エラー出力を抑制する」方法もある(詳細は「解説」を参照)。
-
auto-install.elでwgetを使わないようにする方法
auto-install.elの設定を変更して、wgetの代わりにurl-retrieve(標準添付のUrlパッケージの一部)を利用する。具体的には、"
~/.emacs.d/init.el
"に以下の内容を追加する。なお、url-retrieveには、プロキシ・SSLがらみの問題があるらしい(筆者は詳細を知らない)ので、これらの問題にぶつかった場合はwgetを利用するしかない。
-
auto-install.elでwgetが吐き出す余分な末尾2行を削除する方法
場当たり的な方法だが、GnuWin版のwgetのバイナリを現状のまま利用できる。
wgetを利用する設定では、auto-install.elは関数
auto-install-download-by-wget
の中で、wgetを非同期プロセスとして開始する。auto-install-download-by-wgetが終了しても、非同期プロセスはまだ実行中である(可能性が高い)ので、非同期プロセスの終了時に呼び出される関数auto-install-download-callback-continue
の挙動を変更して、バッファの末尾にある標準エラー出力由来の2行を削除する。具体的には、Emacsの初期化ファイル("~/.emacs.d/init.el
"や"~/.emacs
"など)で、auto-install.elをロードしている部分より後に、以下のアドバイスを追加する。Emacsのアドバイスは、関数そのものを再定義することなく、関数呼び出しの前・後(あるいは前後両方)にコードをかぶせる機能だ。今回は、beforeアドバイスを使って、元のauto-install-download-callback-continueを呼び出す直前に、「バッファ末尾に移動してから、ファイル先頭方向に"
SYSTEM_WGETRC =
"で始まる行を検索し、もし見つかった場合にはそこからバッファ末尾までを削除する」という処理を付け加えている。問題の起きないwgetに入れ替えた場合は、上記のアドバイスを削除すればいい。まあ、elispファイルの内容そのものに、"
SYSTEM_WGETRC =
"で始まる行が含まれていなければ(普通はないよね)、そのまま残しておいても問題は起きないはずだ。 -
GnuWin版のwgetの代わりにCygWin版のwgetを使う方法
GnuWin版のwgetの代わりに、問題のコードを含んでいないCygWin版のwgetを利用する。実は、CygWin版のwgetのほうがバージョンが新しい(この記事の執筆時は1.13.4)。bashを使いたいとか、Emacsから呼び出すツールがほかにいくつかあるといった場合には、Windows上で単独動作させることを目的としたGnuWinより、CygWin環境の導入を検討した方がいいだろう。
「CygWinは導入が面倒なんだよね」という人には、gnupackがオススメだ。これは、Windows版のEmacs(NTEmacs、CygWin版のEmacsではない)とgvim、CygWin環境とそこで動作する各種ツール(wgetを含む)、Window版のGhostScriptなどをまとめたもの。ダウンロードした実行形式のアーカイブを適当なフォルダに展開するだけでインストールが完了する(CygWinパッケージを後で追加することも可能)。詳細はgnupack Users Guideを参照されたい。
解説(というか余談)
この問題が発生したとき、筆者が最初に行なった対処法は、auto-install.elがwgetを呼び出す際に使われる関数auto-install-download-by-wget
を再定義して、標準エラー出力を抑制することだった。具体的には以下のようなコードを書いた。
ハイライト部分が元の関数との相違点で、関数start-process-shell-command
を利用して、シェルを介してwgetを非同期プロセスとして実行している(元の関数ではシェルを介さないstart-process
を利用)。標準エラー出力の抑制は、Windowsのコマンドプロンプトを想定して"2>nul
"と書いている。これは、UNIX系シェルでの"2>/dev/null
"に相当する。
このコードの問題点は、シェルにbashを使うと、"nul"という名前のファイルが作られてしまうことにある。Windowsでは"nul"はヌルデバイスを意味する特殊名なので、コマンドプロンプトやエクスプローラーからはこのファイルを削除できない(bashからなら削除できる)。また、"nul"が作成されたのが(偶然にも)Dropboxで管理しているフォルダだったので、Dropboxの同期処理がいつまで経っても完了しないという問題まで発生した。
もちろん、start-process-shell-commandが利用するシェル(shell-file-name)の値を見て場合分けしてもよかったのだが、「そこまでするなら、バッファの末尾を調べて削る方がずっとシンプルだよなぁ」と思い直して、前述のアドバイスで対処することにした次第。現在はgnupackに移行したので、そのアドバイスもお役ご免となっている。
0 件のコメント:
コメントを投稿