muttの:sourceでファイル名も補完できるようにするpatchを書いた時のメモ

今回書いたmuttのpatch

https://raw.github.com/silenvx/PKGBUILD/master/mutt/source_complete.patch
ここにおいてますが、archlinuxユーザーなどのmakepkgが使える環境下の人は

    % git clone https://github.com/silenvx/PKGBUILD.git
    % cd PKGBUILD/mutt
    % makepkg -i

これで楽にインストールができます

なんでこんなpatchを書いたのか

特定のファイルをsourceした後に、cキーの後に?キーを入力してメールのアカウントを切り替えられるようにしていて、そのsourceのファイル名を毎回打つのが煩わしくなったので。
この切り替えるやり方は私のmuttrcにコメントで書いてあります
今はhttps://github.com/silenvx/dotfiles/blob/master/misc/default/HOME/.muttrcここにおいてるはず
この記事でも簡単に書いておくと
.muttrcには

    source ~/.mutt/.この後用意するアカウントごとのファイル名

このように、muttの起動時にログインしたいアカウントに関するファイルをsourceするように書いておいて
上記のアカウントごとのファイルの場所に

    set my_account_name='@より前の部分'
    source ~/.mutt/この後用意するメールサーバごとの設定ファイルが書かれたファイル名

    set imap_pass='パスワード'
    set smtp_pass='パスワード'

このように書く。このアカウントごとのファイルを、アカウントを切り替える時にsourceします
次にメールサーバごとの設定ファイルを書きますが、gmailを例にすると

    mailboxes imaps://imap.gmail.com/inbox
    set hostname="gmail.com"
    set folder='imaps://imap.gmail.com/'
    set imap_user="${my_account_name}@gmail.com"
    set spoolfile='+INBOX'

    set sendmail=
    set smtp_url="smtp://${my_account_name}@smtp.gmail.com:587/"

    set realname="${my_account_name}@gmail.com"
    set use_from="yes"
    set from="${my_account_name}@gmail.com"

    set postponed='+[Gmail]/下書き'
    set record=

だいたいこんな感じでできます。変数はシェルスクリプトのように${変数名}で表現ができるので後は書きたいように書いてください

ここより下はmuttのpatchを書いた時のメモというか思考というか手順みたいなもの (読む必要はありません)

まず始めに、補完がどのように実装されているか調べるために
muttソースコードを落としてきて*1

    % find mutt-1.5.21/ -type f -regex '.*\.\(c\|h\)' -exec grep -nH 'create-alias' {} \;

こんな感じで調べる。なぜcreate-aliasで絞るかというと
muttにてindexの画面で:execをした後に補完をするとcreate-aliasが出るからです
ちなみにgrepのオプションの-nは行数、-Hでファイル名が出るのでfindと組み合わせた場合に便利です
すると

    mutt-1.5.21/functions.h:86:  { "create-alias",          OP_CREATE_ALIAS,       "a" },
    mutt-1.5.21/functions.h:177:  { "create-alias", OP_CREATE_ALIAS,               "a" },
    mutt-1.5.21/functions.h:392:  { "create-alias", OP_CREATE_ALIAS,        "a" },
    mutt-1.5.21/init.h:106:  ** \fC$<create-alias>\fP function. Entries added to this file are

こんな感じの結果が返ってきますのでmutt-1.5.21/functions.hを見ることにします
mutt-1.5.21/init.hはコメントなので今回は見ません
するとこれらの値は

     85 struct binding_t OpMain[] = { /* map: index */

    175 struct binding_t OpPager[] = { /* map: pager */

    368 struct binding_t OpBrowser[] = { /* map: browser */

に書いてあることがわかります。最初に書いてある数値は行数なので実際には書いてません。
今回はcreate-aliasで絞ったので、この3つしかヒットしませんでしたが、実際にはmap毎に書いてあるみたいです
今回はindexのところで補完を行ったのでとりあえず、85行目の

    struct binding_t OpMain[] = { /* map: index */

この構造体を使っている処理を探して、そこから考えていくとします。
というわけで最初と同じように絞ります

    % find mutt-1.5.21/ -type f -regex '.*\.\(c\|h\)' -exec grep -nH 'OpMain' {} \;
    mutt-1.5.21/functions.h:85:struct binding_t OpMain[] = { /* map: index */
    mutt-1.5.21/keymap.c:709:  create_bindings (OpMain, MENU_MAIN);
    mutt-1.5.21/keymap.c:928:      return OpMain;
    mutt-1.5.21/keymap.h:105:extern struct binding_t OpMain[];

mutt-1.5.21/keymap.cにヒントが書かれていそうなので見ます
最初のcreate_bindings (OpMain, MENU_MAIN);は

    702 void km_init (void)
    703 {

この関数の中にあって、この関数自体はキーバインドの初期化をしているようなので今回はあまり関係がなさそうなので無視します
なので注目すべきなのは return OpMain; これですね。

    923 struct binding_t *km_get_table (int menu)

この関数で、戻り値として利用されていますので、さらにこの関数を使っている処理を探すことにしますので、再び絞ります

    % find mutt-1.5.21/ -type f -regex '.*\.\(c\|h\)' -exec grep -nH 'km_get_table' {} \;
    mutt-1.5.21/keymap.c:368:         struct binding_t *binding = km_get_table (Menus[i].value);
    mutt-1.5.21/keymap.c:467:      if ((bindings = km_get_table (menu)) &&
    mutt-1.5.21/keymap.c:486:       bindings = km_get_table (Menus[i].value);
    mutt-1.5.21/keymap.c:923:struct binding_t *km_get_table (int menu)
    mutt-1.5.21/keymap.c:1002:        bindings = km_get_table (menu[i]);
    mutt-1.5.21/keymap.c:1085:    if ((bindings = km_get_table (CurrentMenu)) == NULL
    mutt-1.5.21/help.c:47:  if ((map = km_get_table(menu)))
    mutt-1.5.21/help.c:345:  funcs = km_get_table (menu);
    mutt-1.5.21/init.c:2536:    struct binding_t *menu = km_get_table (CurrentMenu);
    mutt-1.5.21/keymap.h:101:struct binding_t *km_get_table (int menu);

後は、それぞれの処理をしている関数名から何をしている処理なのかを予測していくと
mutt-1.5.21/init.cに補完関連らしき関数を見つけることができました

    2430 int mutt_command_complete (char *buffer, size_t len, int pos, int numtabs)

名前的に考えて、恐らくこの関数で補完関連を司っているのでしょう。
注目すべきなのは

    2534   else if (!mutt_strncmp (buffer, "exec", 4))
    2535   {
    2536     struct binding_t *menu = km_get_table (CurrentMenu); 

この部分ですね。どうやらmutt_strncmp関数で入力途中のコマンドがexecなのか判定できるのでしょう。
引数から考えて、bufferがその入力途中のコマンドが入っていて、"exec"が検索したい文字、最後の引数の4が何文字まで検索するか、なのでしょう。きっと。
念の為、この関数の処理を調べるとしますので、再びこのように絞ります

    % find mutt-1.5.21/ -type f -regex '.*\.\(c\|h\)' -exec grep -nH 'mutt_strncmp' {} \;
    (中略)
    mutt-1.5.21/lib.c:878:int mutt_strncmp(const char *a, const char *b, size_t l)

というわけで、途中で略しましたが関数名の前に型があるのがきっと宣言しているところなのでmutt-1.5.21/lib.cを見ることにします

    878 int mutt_strncmp(const char *a, const char *b, size_t l)
    879 {
    880   return strncmp(NONULL(a), NONULL(b), l);
    881 }

strncmpのラッパーのようなので、さっきの考え方はあっていたようです。
さて、問題はこのif文の中の処理がどうなっているか、です。

    2534   else if (!mutt_strncmp (buffer, "exec", 4))

buffer変数に入力途中の文字列が入っているということはだいたい察しがつくので、この変数を書き換えている箇所を見ればよさそうです
そういうわけで、書き換えてるのは

    2430 int mutt_command_complete (char *buffer, size_t len, int pos, int numtabs)

    2432   char *pt = buffer;

    2579     strncpy (pt, Completed, buffer + len - pt - spaces);

この3行から考えるに

    2579     strncpy (pt, Completed, buffer + len - pt - spaces);

この処理で、他の補完も同じようになっているのでこれを使えばよさそうです
補完したい文字はCompleted変数に書き込んでおけばいいみたいですね
さて、このCompletedの文字列がどこで書き換えられているか、というと

    2572     if (numtabs == 1 && Num_matched == 2)
    2573       snprintf(Completed, sizeof(Completed),"%s", Matches[0]);
    2574     else if (numtabs > 1 && Num_matched > 2)
    2575     /* cycle thru all the matches */
    2576       snprintf(Completed, sizeof(Completed), "%s",
    2577            Matches[(numtabs - 2) % Num_matched]);

だいたいこんな感じで書き換えているみたいです
さて、このnumtabs変数とNum_matched変数はどのように生成されているかというと
numtabs変数はこの関数を呼び出した時の引数として使われていてmutt_command_completeの呼び出し元を追うと

    % find mutt-1.5.21/ -type f -regex '.*\.\(c\|h\)' -exec grep -nH 'mutt_command_complete' {} \;
    mutt-1.5.21/protos.h:281:int mutt_command_complete (char *, size_t, int, int);
    mutt-1.5.21/init.c:2430:int mutt_command_complete (char *buffer, size_t len, int pos, int numtabs)
    mutt-1.5.21/enter.c:557:            else if (!mutt_command_complete (buf, buflen, i, state->tabs))

mutt-1.5.21/enter.cでmutt_command_complete関数を呼び出しており、
numtabsの値はstate->tabsに入っていることがわかるので、今度はこの変数の生成方法を追います

    % find mutt-1.5.21/ -type f -regex '.*\.\(c\|h\)' -exec grep -nH 'state->tabs' {} \;
    mutt-1.5.21/enter.c:299:        state->tabs = 0;
    mutt-1.5.21/enter.c:488:          state->tabs++;
    mutt-1.5.21/enter.c:556:              state->tabs = 0;
    mutt-1.5.21/enter.c:557:            else if (!mutt_command_complete (buf, buflen, i, state->tabs))
    mutt-1.5.21/enter.c:640:      state->tabs = 0;

ヒットした5行のうち4行は値の初期化と関数の呼び出しに使われているので、残りのインクリメントをしているところを見ることにします

    486     case OP_EDITOR_COMPLETE:
    487     case OP_EDITOR_COMPLETE_QUERY:
    488       state->tabs++;

こうなっていたので、上にさかのぼってswitchを探すと

    298       if (ch != OP_EDITOR_COMPLETE && ch != OP_EDITOR_COMPLETE_QUERY)
    299         state->tabs = 0;

    301       switch (ch)

どうやらchの値がOP_EDITOR_COMPLETE(_QUERY)?であればインクリメントされ、そうでなければ初期化されるようです
では、このchの生成方法を調べますが変数名が短すぎて関係ないものが多数ヒットしてしまう恐れがあるので
この処理をしている関数を上から順に見ていきますと

    222   int ch, w, r;

    289     if ((ch = km_dokey (MENU_EDITOR)) == -1)

ここで代入されていることがわかります
km_dokey関数は名前的に考えて、キー入力関連だと思うので定数らしきOP_EDITOR_COMPLETEについて調べるとします

    % find mutt-1.5.21/ -type f -regex '.*\.\(c\|h\)' -exec grep -nH 'OP_EDITOR_COMPLETE' {} \;
    mutt-1.5.21/functions.h:416:  { "complete",             OP_EDITOR_COMPLETE,             "\t"   },
    mutt-1.5.21/functions.h:417:  { "complete-query",       OP_EDITOR_COMPLETE_QUERY,       "\024" },
    (後略)

どうやらascii文字で定義されているようです。補完に使うキーはtabキーで、それは'\t'なのでさっきの仮説はきっと合っていたのでしょう。
ここまでの情報を統合すると、タブを押して補完の結果を切り替えた回数がnumtabs変数に入ると考えられます

次に
Num_matched変数は

    2383 static int  Num_matched = 0; /* Number of matches for completion */

最初にこのようにグローバル変数でstatic変数として宣言されており

    2501     if (numtabs == 1)
    2502     {
    2503       Num_matched = 0;

1回目の補完時に必ず0に初期化されて

    2512       Matches[Num_matched++] = User_typed;

先ほどと同じif文の中でインクリメントされてるので必ず1以上になりますが、それだけだと

    2525     if (numtabs == 1 && Num_matched == 2)

    2527     else if (numtabs > 1 && Num_matched > 2)

これらのif文に入ることができないので他にNum_matched変数が変動する箇所を探します

    % find mutt-1.5.21/ -type f -regex '.*\.\(c\|h\)' -exec grep -nH 'Num_matched' {} \;
    (前略)
    mutt-1.5.21/init.c:2419:    Matches[Num_matched++] = src;
    (中略)
    mutt-1.5.21/init.c:2456:      Matches[Num_matched++] = User_typed;
    (中略)
    mutt-1.5.21/init.c:2512:      Matches[Num_matched++] = User_typed;
    (中略)
    mutt-1.5.21/init.c:2559:      Matches[Num_matched++] = User_typed;
    (後略)

変動していない、もしくは0を代入している箇所は省きました
これを見ると、1つだけ違うのがあって、それが怪しいのでまずそれを最初に見ます。
するとこの処理はこの関数で行われていることがわかります

    2412 static void candidate (char *dest, char *try, const char *src, int len)

このcandidate関数はnumtabs変数が1の時、つまりは初めての補完の時に

    2536     struct binding_t *menu = km_get_table (CurrentMenu);

    2538     if (!menu && CurrentMenu != MENU_PAGER)
    2539       menu = OpGeneric;
    (中略)
    2549       for (num = 0; menu[num].name; num++)
    2550     candidate (Completed, User_typed, menu[num].name, sizeof (Completed));
    2551       /* try the generic menu */
    2552       if (Completed[0] == 0 && CurrentMenu != MENU_PAGER)_
    2553       {
    2554     menu = OpGeneric;
    2555     for (num = 0; menu[num].name; num++)
    2556     candidate (Completed, User_typed, menu[num].name, sizeof (Completed));
    2557       }

このようにfor文を使って、呼ばれているので2以上になることはありそうです
問題は、何回このfor文を繰り返すか、です
ではこのfor文を見ていきましょう

    2549       for (num = 0; menu[num].name; num++)

これは、きっとmenu配列の最後に終わりのデータとして0が代入されているのでしょう。
このmenu配列はkm_get_table関数の戻り値でなので
最初の方に書いたようにmutt-1.5.21/functions.hにあるOp何とかかんとかの配列が代入されるのでしょう。
それらの配列の最後にはこのようなbinding_t構造体があって

    { NULL, 0, NULL }

binding_t構造体のnameはどれなのか調べるために

    % find mutt-1.5.21/ -type f -regex '.*\.\(c\|h\)' -exec grep -nH 'binding_t' {} \;
    (前略)
    mutt-1.5.21/keymap.h:94:struct binding_t
    (後略)

で、見てみると

    94 struct binding_t
    95 {
    96   char *name;   /* name of the function */
    97   int op;   /* function id number */
    98   char *seq;   /* default key binding */
    99 };

仮説は正しかったようです。

というわけでmenuの配列の数-1だけcandidate関数が呼び出されるようです
このcandidate関数の上にコメントがあるので訳します

    補完を助けるための関数です
    可能な補完を支援するためにdestバッファを変更します
    dest = 補完の結果はここで取得
    try  = ユーザーが補完するために入力したデータ
    src  = 補完の候補
    len  = destバッファの長さ

らしいので、呼び出し元と照らし合わせます

    2412 static void candidate (char *dest, char *try, const char *src, int len)

    2550     candidate (Completed, User_typed, menu[num].name, sizeof (Completed));

ここに出てくるUser_typedはこのように定義されており

    2546       strfcpy (User_typed, pt, sizeof (User_typed));

ptのコピーのようなものみたいです。

それで、candidate関数の一部を書くと
tryの文字列がsrcにあったらMatches関数の領域を増やして、そこにsrcのアドレスがはいります
その時、Num_matched変数はインクリメントされるので少なくとも2以上にはなります
そういうわけで

    2572     if (numtabs == 1 && Num_matched == 2)
    2573       snprintf(Completed, sizeof(Completed),"%s", Matches[0]);
    2574     else if (numtabs > 1 && Num_matched > 2)
    2575     /* cycle thru all the matches */
    2576       snprintf(Completed, sizeof(Completed), "%s",
    2577            Matches[(numtabs - 2) % Num_matched]);

候補が1つだけしかない場合は上のif文で、候補が複数あった場合は下のelse if文が呼び出されるみたいですね
else if文のこの計算式は

    (numtabs - 2) % Num_matched

tabキーを一定回数以上押したら最初に戻るようになってるみたいです

というわけで補完の追加方法はわかったので後はどのようにファイル名を取得するかです
調べたところ、readdir関数を使えばいいみたいなのでそれを使用して実装するだけなので後は端折ります

後書き

goto文を使ったり若干、スパゲッティソースになった気がするので気が向いたら改良しますが今のところ特に不満はないのでしばらくはこのまま。
ただ、ファイル名にスペースがあれば区切りと勘違いされるバグがあるので、注意。そんなファイル名は使わないので今すぐは直しませんが、時間ができた時に直します。

*1:archlinuxなら % yaourt -G mutt;cd mutt;makepkg -o をすれば楽にmutt/src/に展開できます