vimdiffの謎
ファイルの差分を確認するためのコマンドとしてdiff
コマンドがありますが、
vimのインタフェースを用いて差分を確認できるvimdiff
というコマンドもあります。
$ vimdiff a.txt b.txt
のように使います。 vimdiffはdiffよりも見やすくて便利なのでよく使っています。
ある日、ふとvimdiffの実体が気になって確認してみると、
/usr/bin/vim
へのシンボリックリンクになっていることに気がつきました。
$ type vimdiff
vimdiff is /usr/bin/vimdiff
$ ls -l /usr/bin/vimdiff
lrwxrwxrwx 1 root root 3 Jan 16 21:09 /usr/bin/vimdiff -> vim*
vimには-d
というオプションがあり、このオプションを渡すとvimdiffと同じように差分表示モードで起動します。
vimdiff
はvim -d
をラップしているスクリプトファイルか何かかと予想していたのですが、
実体はvimへのシンボリックリンクでした。
そうなるとvimdiff
とvim
は何が違うんだと疑問に思うのですが、
$ vim a.txt b.txt
と
$ vimdiff a.txt b.txt
は明らかに挙動が異なります。 シンボリックリンクに「特定のオプションを追加して起動する」なんていう機能は存在しないよなあと 思いつつ調べてみると、stackoverflowに同じ全く同じ質問が見つかりました。
https://stackoverflow.com/questions/8876323/how-does-the-softlink-vimdiff-be-implemented
結局これはどういう仕組みなのかというと、
vimがargv[0]
を見て挙動を変えているということでした。
C言語でユーザプログラムを書く際に、main関数の引数としてargv
をとることができます。
普通、シェルはargv[0]
に実行ファイルの名前を設定し、argv[1]
以降にコマンドライン引数を格納します。
実行ファイルへのシンボリックリンクの場合、argv[0]
にはシンボリックリンクの名前が渡されることになります。
vimdiff
を起動するとargv[0]
にはvimdiff
が渡され、
普通にvimを起動したときにはvim
が渡されることになります。
vimは「main関数に渡されたargv[0]
がvimdiff
だったら、-d
オプションが指定されたときと同じように振る舞う」というようなことをやっているわけですね。
以下のようなC言語のプログラムを書いてこの挙動を確認してみました。
my-vim.c:
1#include <libgen.h>
2#include <stdio.h>
3#include <string.h>
4
5int main(int argc, char *argv[]) {
6 if (argc < 1) return 1;
7 printf("my-vim.c: argv[0] is %s\n", argv[0]);
8
9 // argv[0] のファイル名が "vimdiff" の場合
10 if (strcmp(basename(argv[0]), "vimdiff") == 0) {
11 printf("diff mode\n");
12 } else {
13 printf("normal mode\n");
14 }
15}
my-vimdiff.c:
1#include <stdio.h>
2#include <unistd.h>
3
4extern char **environ;
5
6int main(int argc, char **argv) {
7 if (argc < 1) return 1;
8 printf("my-vimdiff.c: argv[0] is %s\n", argv[0]);
9
10 argv[0] = "vimdiff";
11 execve("./my-vim", argv, environ);
12 return 1;
13}
実行結果:
$ make my-vim
cc my-vim.c -o my-vim
$ make my-vimdiff
cc my-vimdiff.c -o my-vimdiff
$
$ ./my-vim
my-vim.c: argv[0] is ./my-vim
normal mode
$
$ ./my-vimdiff
my-vimdiff.c: argv[0] is ./my-vimdiff
my-vim.c: argv[0] is vimdiff
diff mode
$
$ ln -s my-vim vimdiff # シンボリックリンクにした場合
$ ./vimdiff
my-vim.c: argv[0] is ./vimdiff
diff mode
my-vimdiff
ではシェルを使わずに、execve
を直接呼んでmy-vim
を起動しています。
このときにargv[0]
に好きな文字列を渡すことができます。
起動されるmy-vim
側では、これを見て挙動を変えています。
分かってしまえばなんだーという感じですが、argv[0]
を見るという発想がなかったので驚きました。
最後に、本物のvimのソースコードを確認してみると、
parse_command_name
という関数でargv[0]
を見ていました。
vimdiff
以外にもview
、diff
、viewdiff
などの名前でも挙動が変わるようです。
https://github.com/vim/vim/blob/8e5ba693ad9377fbf4b047093624248b81eac854/src/main.c#L1835-L1924
ちなみに、この方法はbusyboxでも使われていて、
例えばls
という名前で/usr/bin/busybox
へのシンボリックリンク作ると、このファイルはbusybox ls
のように振る舞います。
$ ln -s /usr/bin/busybox ls
$ busybox ls
a.txt b.txt ls
$ ./ls
a.txt b.txt ls
軽量Linuxディストリビューションとして有名なAlpine Linuxでは
/bin
以下の (/bin/busybox
を除く) 全てのファイルはbusybox
へのシンボリックリンクになっています。