Reinventing Square Wheels - algon's blog

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と同じように差分表示モードで起動します。 vimdiffvim -dをラップしているスクリプトファイルか何かかと予想していたのですが、 実体はvimへのシンボリックリンクでした。

そうなるとvimdiffvimは何が違うんだと疑問に思うのですが、

$ 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以外にもviewdiffviewdiffなどの名前でも挙動が変わるようです。 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へのシンボリックリンクになっています。