Emacs
WSL

NTEmacs から WSL のコマンドを利用する

はじめに

Emacs はPosix 系の外部コマンドを利用することで高度な機能を実現しているので。NTEmacs は使う場合は MSys や Cygwin などと併用することが多いと思います。

本稿では、その Posix 環境として Msys や Cygwin の代りに WSL (Windows Subsystem for Linux) を利用できないか検討しました。

WSLのコマンド

MsysやCygwinと違って、WSLではLinux側のファイルをWindows側から見ることができません。1
WSL側のコマンドをWindowsから使うには、全てを bash.exe 経由で起動する必要があります。

bash -lc "command ..."

bash.exe 自体は、%windir%\System32 にあるWindowsアプリ です。

Emacs が外部プログラムを起動する方法

Emacs が外部のプログラムを起動する時、シェルにコマンドラインを渡して起動する場合と、call-process などから直接プログラムを起動する場合があります。

M-x grep, M-x find などは、シェル経由の起動になります。

M-x grep などでWSLのコマンドを利用可能にする

シェル経由で起動されるプログラムについては、 shell-file-name に WSLのbash.exeを指定するだけで利用可能になります。

(setq shell-file-name (executable-find "bash"))
(setq grep-use-null-device nil)

Windows側のPATH が bash.exe に伝わっているので、Windows のコマンドもこの方法で起動できます。
ただし、プログラムの拡張子まで明示的に指定する必要があります。

M-! attrib.exe * /S RET

PATHにない場所のコマンドを使うためにフルパスで指定する時は、WSLから見たフルパスを用います。

M-! "/mnt/c/Program Files/dotnet/dotnet.exe" nuget --help RET

monky (Mercurial)

monky は EmacsからMercurial を利用するためのパッケージです。ELPAからインストールできます。

monky は、call-process で直接 hg コマンドを起動するので、 shll-file-name に bash.exe を指定しても効きません。WSL の hg コマンドを emacs から使うために、ラッパースクリプトを用意します。

hg.bat
@echo off

bash -cl "hg %*"

単純すぎて、hgの引数に特殊文字が来たら破綻しそうですが、monky からhg を呼ぶ分には問題なさそうです。2

ラッパースクリプトの置き場所は、PATHの通っている場所ならどこでも良いですが、Emacsからしか使わないスクリプトなので、Emacs の exec-directory (私の環境だと c:/emacs/libexec/emacs/25.3/x86_64-w64-mingw32/) に置くと邪魔にならないと思います。

これで、Emacsから WSLのhg を呼べるようになりました。monky-status を実行すると、

Setting current directory: Permission denied, /mnt/c/..../foo/

と怒られます。hg の出力に含まれるフルパスは WSL から見たものなので、Emacs が理解できないからです。こういう時は、file-name-handler を設定します。

Emcas-lisp のプログラムをGitHub に置きました。 ( wsl-file-name-handler )

wsl-file-name.el をロードして、M-x wsl-enable-file-name-handler すると、/mnt/X/... 形式のファイル名を X:/... に変換するようになります。

WSLのhg を使う上でもう一つの問題は、WSL 側からWindows のファイルシステムを見ると、全てのファイルがowner=root group=root mode=0777 の状態になっていることです。読み書きには問題ありませんが、Mercurial が

not trusting file /mnt/X/.../foo

と文句を言うせいで、monky が hg の出力を解析し損ないます。これを防ぐには、WSL側の ~/.hgrc に

[trusted]
users = root

を追加するのが簡単です。
他の解決方法として、この記事によれば、 -o metadata 付きで /mnt/X をマウントすれば良いようですが、Insider Build での話なので私の環境ではまだ利用できませんでした。

Egg (Git)

Egg (Emacs Got Git) は、Emacs からgitを利用するためのパッケージです。 ここから クローンして使っています。

Egg も monky と同様の簡単なラッパーと file-name-handler で使えるようになりました。3

magit (Git)

注意: この項は未完成です。

Magitは Eggと同様にEmacsからgit を利用するためのパッケージです。ELPA からインストールできます。

Magit は、 Egg よりもややこしい引数をgit に渡します。例えば、"--format=%h %s" という空白を含む引数を使ってくるので、単純なラッパーでは破綻します。そこで空白のある引数に対応したラッパーを書いてみました。

git.bat
@if (@Code == @Batch) @then
@echo off

for /f "usebackq delims=" %%t in (`bash -c "mktemp tmpXXXXXX.bat"`) do set "tmpbat=%%t"
CScript //E:JScript //Nologo "%~f0" %tmpbat% git %*
call %tmpbat%
if exist %tmpbat% del %tmpbat%
goto :EOF
@end

var shell = WScript.CreateObject("WScript.Shell")
var args = WScript.Arguments;
var param = [];

var batchfile= args(0);

var fso = new ActiveXObject("Scripting.FileSystemObject");
var f = fso.CreateTextFile(batchfile, true);

for (i=1; i < args.length; i++) {
    var s = args(i);
    if (s.indexOf(" ") > 0 || s.indexOf("\t") > 0)
    s = '\\"' + s + '\\"';
    param.push(s);
}

f.writeLine('bash -lc "' + param.join(' ') + '"');

バッチコマンドとJScript を一つのファイルに入れられるんですね。今回初めて知りました。
一時ファイルにバッチコマンドを書いてから実行するのはダサいですが、for /f "usebackq" で華麗にコマンドラインの変換をしようとしたら、foo.bat=true という引数が foot.bartrue の二つに分解されるという謎現象にみまわれ、回避方法が見付からなかったので、苦肉の策です。

これで空白を含む引数は正しくgitに渡るようになりましたが、今度は master^{commit} という引数が master{commit} に変換されていました。 「^」はバッチファイルでのエスケープ文字なんですね。これも初めて知りました。バッチファイルなんて普段書かないからなあ。

これは、バッチファイルに引数が渡された時点で既に変っているので、emacs-lisp 側で何とかしないとダメなようです。

という所まで見て、ここまで苦労するならWindows版Git をインストールした方が早いな、と心が折れたので、この項は未完です。

その他の問題

このケースにはまだ出喰わしていませんが、 emacs-lisp から外部コマンドにフルパスが渡る場合も変換が必要になると思われます。

まとめ

  1. shell-file-name に WSLのbash.exe を設定すると幸せになれます。
  2. call-process で起動するプログラムについては、ラッパーバッチファイルと file-name-handler で何とかなる場合があります。
  3. magit は手強い


  1. %localappdata% 下のどこかにあるので、全く見えないわけではないですが、Windowsからの操作は推奨されません。触ると壊れる場合もあるようです。詳しくは Do not change Linux files using Windows apps and tools を参照。 

  2. 全てのケースで確認したわけではないので、絶対にないとは言いきれませんが 

  3. これも簡単な使い方しか試してないので、引数関係で問題が出ないとは限りません。