Edited at

原理原則で理解するbashの仕組み

More than 1 year has passed since last update.


はじめに

以前書いたエントリー、重大な脆弱性(CVE-2017-5932)で少し話題になったbash4.4の補完機能の便利な点で、bash4.4からでないとタブの補完機能のソート処理が制御できないという問題について、ソースコードレベルで調べた結果をまとめていたのですが、bashの実装そのものを深く掘り下げ過ぎてしまい、内容が膨大になったので、何回かに分けて書こうと思います。

今回はbashが起動されてからインタラクティブモードでキーボードの入力を待ち受けるまでのお話です。普段使っているbashがどのような処理を行っているのか一緒に覗いてみませんか?


検証ソースコード

Bash version 4.1.0(1) release GNU


bashの生誕

bashのプロセスが起動されるのはOSへのログイン時にユーザーのログインシェルがbashに設定されている場合、あるいはログイン後に明示的にbashコマンドを実行した時の2通りが考えられます。

しかし、その違いは実はそれほど重要ではなく、どのようなオプションで起動されたかによってbashの処理は大きく別れることになります。

いずれにしても、全てのストーリーは、ここから始まります。

$ bash # ログイン後に明示的な実行

bashのプロセスが起動すると、shell.c内のmain関数が実行され、必要な初期化処理を行います。初期化処理はbashの起動コンテキストや設定ファイル(/etc/bash.bashrc、/etc/profile、~/.profile、~/.bash_profile、~/.bash_login、~/.bashrc等)の内容によって異なります。


共通初期化処理後

起動オプションによらない共通の初期化処理を行った後、-cオプションとともに特定のコマンドが実行される場合はそのコマンドを実行してbashは即時終了してしまうのですが、それ以外の場合は最後にreader_loopという関数が実行されて、命令されたコマンドが逐次実行されていきます。

とりわけ、インタラクティブモードで起動された時は、半永久的にこの処理を繰り返します


shell.c

341 #if defined (NO_MAIN_ENV_ARG)

342 /* systems without third argument to main() */
343 int
344 main (argc, argv)
345 int argc;
346 char **argv;
347 #else /* !NO_MAIN_ENV_ARG */
348 int
349 main (argc, argv, env)
350 int argc;
351 char **argv, **env;
352 #endif /* !NO_MAIN_ENV_ARG */
353 {
354 register int i;
# 省略
746 shell_initialized = 1;
747
748 /* Read commands until exit condition. */
749 reader_loop ();
750 exit_shell (last_command_exit_value);

なお、厳密な話をするとbashが以下のいずれかの状態で起動される場合はインタラクティブモードと定義され、それ以外は非インタラクティブモードと定義されます。


  • iオプション指定されている。

  • cオプションが指定されていない。かつ、bashがwordexp(3)によって呼び出されていない。かつ、bashで処理すべき引数が残っていないか、-sオプションが指定されている。かつ、カレントプロセスの端末が標準入力と標準エラー出力に紐付いている。

私自身の日本語力の問題もありますが、日本語で書くとあまりにもわかりにくいので、下記の実際のコードの処理を参照して下さい😅


shell.c

 517   if (forced_interactive ||     /* -i flag */

518 (!command_execution_string && /* No -c command and ... */
519 wordexp_only == 0 && /* No --wordexp and ... */
520 ((arg_index == argc) || /* no remaining args or... */
521 read_from_stdin) && /* -s flag with args, and */
522 isatty (fileno (stdin)) && /* Input is a terminal and */
523 isatty (fileno (stderr)))) /* error output is a terminal. */
524 init_interactive ();
525 else
526 init_noninteractive ();

直感的な話をすれば、キーボードからコマンドを入力して繰り返し実行できる状態であればbashはインタラクティブモードで起動していると言えます。

bashを利用するほとんどのケースがこのインタラクティブモードでの起動で、非インタラクティブモードで起動するケースがあるとすればシェルスクリプトのインタプリタ、あるいはメインシェルからサブシェルとして起動するくらいでしょう。

#!/bin/bash

# do something

reader_loopの処理ではbashが入力文字列を解釈して実行する必要のない状態(EOF_Reachedが0でない)になるまで、以下の処理が繰り返されます。実際のソースコードの行数と照らし合わせて見てみましょう。


  1. ループに突入 74行目

  2. read_command関数で入力された文字列の構文解釈 137行目

  3. 解釈した結果実行すべき命令を実行 152行目

  4. 終了のフラグ(EOF_Reachedが0でない)がなければ、1に戻る


eval.c

62 int

63 reader_loop ()
64 {
# 省略
74 while (EOF_Reached == 0)
75 {
76 int code;
77
# 省略
137 if (read_command () == 0)
138 {
139 if (interactive_shell == 0 && read_but_dont_execute)
140 {
141 last_command_exit_value = EXECUTION_SUCCESS;
142 dispose_command (global_command);
143 global_command = (COMMAND *)NULL;
144 }
145 else if (current_command = global_command)
146 {
147 global_command = (COMMAND *)NULL;
148 current_command_number++;
149
150 executing = 1;
151 stdin_redir = 0;
152 execute_command (current_command);
153
154 exec_done:
155 QUIT;
156
157 if (current_command)
158 {
159 dispose_command (current_command);
160 current_command = (COMMAND *)NULL;
161 }
162 }
163 }
164 else
165 {
166 /* Parse error, maybe discard rest of stream if not interactive. */
167 if (interactive == 0)
168 EOF_Reached = EOF;
169 }
170 if (just_one_command)
171 EOF_Reached = EOF;
172 }
173 indirection_level--;
174 return (last_command_exit_value);
175 }

read_command関数は入力される文字列をパースする一連の処理の基点なのですが、シェル変数TMOUTの内容を読み取る指定もここで行われます。


シェル変数

シェル変数とは、実行中のコマンドライン上や、シェルスクリプト内で記述される変数を指します。ご存知の通りユーザーが自由に定義することもできますし、TMOUTのようにbashの内部で予約されているものもあります。


eval.c

239 int

240 read_command ()
241 {
242 SHELL_VAR *tmout_var;
243 int tmout_len, result;
244 SigHandler *old_alrm;
245
# 省略
254 if (interactive)
255 {
256 tmout_var = find_variable ("TMOUT");
# 省略
267 }
# 省略
272 result = parse_command ();
# 省略

シェル変数TMOUTがbash内部でどのように利用されているかというと、たとえばbashの設定ファイルに下記のような変数をセットしておくと、1分間何も入力がないと自動的にログアウトされます。

export TMOUT=60

シェル変数はfind_variable関数、シェル関数はfind_functionというように、bashに対してテキストインターフェイスで入力されたデータを取得する関数が用意されています。


variables.c


1794 /* Look up the variable entry named NAME. Returns the entry or NULL. */
1795 SHELL_VAR *
1796 find_variable (name)
1797 const char *name;
1798 {
1799 return (find_variable_internal (name, (expanding_redir == 0 && (assigning_in_environment || executing_builtin))));
1800 }

1802 /* Look up the function entry whose name matches STRING.
1803 Returns the entry or NULL. */

1804 SHELL_VAR *
1805 find_function (name)
1806 const char *name;
1807 {
1808 return (hash_lookup (name, shell_functions));
1809 }


実装上はいずれもSHELL_VARというC言語の構造体で管理され、ハッシュテーブルを通して取得するという実装になっています。

型を気にしなくていいプログラミング言語のデータ構造にあるあるな実装ですね。


variables.h

 82 typedef struct variable {

83 char *name; /* Symbol that the user types. */
84 char *value; /* Value that is returned. */
85 char *exportstr; /* String for the environment. */
86 sh_var_value_func_t *dynamic_value; /* Function called to return a `dynamic'
87 value for a variable, like $SECONDS
88 or $RANDOM. */

89 sh_var_assign_func_t *assign_func; /* Function called when this `special
90 variable' is assigned a value in
91 bind_variable. */

92 int attributes; /* export, readonly, array, invisible... */
93 int context; /* Which context this variable belongs to. */
94 } SHELL_VAR;


インタプリタとしてのbash

その処理が終わった後は、parse_command関数が実行されます。parse_command関数はインタプリタとしての役割を果たすbashの要となる処理です。

設定ファイルの文字列をパースするreader_loop以前の初期化処理の中で何度も呼ばれていましたが、reader_loop関数が実行されてからはここで初めて実行されます。

なお、プロンプトが表示されてユーザーの入力を待ち受ける状態になった時は、エンターキーを叩くまで(入力文字列がbash内部で字句解析されるまで)はしばらくここまで戻ってきません。

下記のソースコードの分岐を確認してください。

parse_command関数をこの時点で呼んだ時はbash_input.typeにst_stdinが指定されているので、シェル変数PROMPT_COMMANDからプロンプトに文字列を表示する為のコマンドを取得して、execute_variable_commandに渡します。


eval.c

205 int

206 parse_command ()
207 {
208 int r;
209 char *command_to_execute;
210
# 省略
217 if (interactive && bash_input.type != st_string)
218 {
219 command_to_execute = get_string_value ("PROMPT_COMMAND");
220 if (command_to_execute)
221 execute_variable_command (command_to_execute, "PROMPT_COMMAND");
222
223 if (running_under_emacs == 2)
224 send_pwd_to_eterm (); /* Yuck */
225 }
226
227 current_command_line_count = 0;
228 r = yyparse ();
# 省略

PROMPT_COMMANDが定義されていない場合は、PROMPT_COMMANDで定義されたコマンドは実行されず、シェル変数PS1からプロンプトの文字列を取得するのみとなります。PS1は特にユーザーが設定していない場合は、\\s-\\v\\$というフォーマットになります。


config-top.h

68 #define PPROMPT "\\s-\\v\\$ "


PROMPT_COMMANDはbashから命令が実行されるたび(インタラクティブモードでエンターキーを入力するたび)に実行されますが、端末の画面幅に変化があった時などには実行されません。

対して、PS1の内容は端末の画面を広げたり、縮小するなど端末に何か少しでも変化が起きるたびに常にデコードされた内容が表示されます。


4つの入力タイプ

さきほど、bash_input.typeという言葉が出てきましたが、これはbashの現在の入力コンテキストを管理する構造体BASH_INPUTのtypeメンバーです。

ここは、bashの字句解析処理を理解するにあたって大事なポイントです。

bashの入力タイプ(bash_input.type)は以下の4つが列挙型によって定義されており、この4つのタイプによって異なる構文解析アプローチ(getterメンバー)をとる実装になっています。


input.h

38 enum stream_type {st_none, st_stdin, st_stream, st_string, st_bstream};

72 typedef union {
73 FILE *file;
74 char *string;
75 #if defined (BUFFERED_INPUT)
76 int buffered_fd;
77 #endif
78 } INPUT_STREAM;

80 typedef struct {
81 enum stream_type type;
82 char *name;
83 INPUT_STREAM location;
84 sh_cget_func_t *getter;
85 sh_cunget_func_t *ungetter;
86 } BASH_INPUT;


実際にシェル変数PROMPT_COMMANDを私の環境で表示してみました。

$ echo $PROMPT_COMMAND # 本当は$の部分がプロンプトで表示される部分ですが$にしています

printf "\033]0;%s@%s:%s\007" "${USER}" "${HOSTNAME%%.*}" "${PWD/#$HOME/~}"

この文字列がexecute_variable_command関数に渡され、parse_and_execute関数によって解釈され実行されます。

そして、parse_and_execute関数内のwith_input_from_string関数では、入力タイプが文字列だった場合の構文解析処理ルーチンが設定されます。


builtins/evalstring.c

169 int

170 parse_and_execute (string, from_file, flags)
171 char *string;
172 const char *from_file;
173 int flags;
174 {

# 省略

198 with_input_from_string (string, from_file);
199 while (*(bash_input.location.string))
200 {

# 省略

252 if (parse_command () == 0)
253 {


with_input_from_string関数では入力対象が文字列だった場合の字句解析ルーチン(bash_input.getter)をinit_yy_io関数によって設定します。この場合はyy_string_get関数が設定されます。


y.tab.c

3865 void

3866 with_input_from_string (string, name)
3867 char *string;
3868 const char *name;
3869 {
3870 INPUT_STREAM location;
3871
3872 location.string = string;
3873 init_yy_io (yy_string_get, yy_string_unget, st_string, name, location);
3874 }

そしてparse_and_execute関数から、先程も実行されたparse_command関数が実行されますが、init_yy_io関数の第3引数にst_stringが指定されているので今度はPROMPT_COMMANDの部分はスルーしてその後のyyparse関数が実行されます。

ちなみに、このあたりの処理はy.tab.cというソースコード内に記述されていて、Yaccに触れたことがある方は、名前からピンと来ると思いますがparser.yから生成された構文解析器としてのソースコードです。


bashの構文解析

y.tab.c内のyyparse関数ではbash内部でバッファリングされた文字列に対して、構文解析処理を行います。

yyparse関数内でyylex関数が繰り返し呼ばれます。yylex関数は字句解析関数として作用します。

今回の字句解析する対象の文字列はprintf "\033]0;%s@%s:%s\007" "${USER}" "${HOSTNAME%%.*}" "${PWD/#$HOME/~}"です。

この例でいうと、字句解析の処理の流れは下記の流れです。といっても、このあたりの処理はbash独自というわけではなくYaccを使ったプログラミング言語の処理系であればおおむね共通の処理となるので、詳しい解説はBisonやYaccについてのドキュメントをご参照下さい。


  1. ルックアヘッドシンボル(yychar)が空だった場合yylex関数を実行(2029行目)して\033]0;%s@%s:%s\007" "${USER}" "${HOSTNAME%%.*}" "${PWD/#$HOME/~}までを一度、shell_input_line変数に格納する。

  2. トークンに分ける処理(read_token_word関数)でprintfまで解析して、結果をyycharに格納する。(2029行目)

  3. yycharをトークンシンボルとなるyytokenに変換する(2039行目)。yytokenからアクションとなるyynを求めて(2045-2048行目)、該当の処理を実行する(2107行目)。

  4. 改行(0x0a)に出会うまで1から繰り返す。


y.tab.c

1382 #ifdef YYLEX_PARAM

1383 # define YYLEX yylex (YYLEX_PARAM)
1384 #else
1385 # define YYLEX yylex ()
1386 #endif
# 省略
1812 #ifdef YYPARSE_PARAM
1813 #if defined __STDC__ || defined __cplusplus
1814 int yyparse (void *YYPARSE_PARAM);
1815 #else
1816 int yyparse ();
1817 #endif
1818 #else /* ! YYPARSE_PARAM */
1819 #if defined __STDC__ || defined __cplusplus
1820 int yyparse (void);
1821 #else
1822 int yyparse ();
1823 #endif
1824 #endif /* ! YYPARSE_PARAM */
# 省略
2025 /* YYCHAR is either YYEMPTY or YYEOF or a valid look-ahead symbol. */
2026 if (yychar == YYEMPTY)
2027 {
2028 YYDPRINTF ((stderr, "Reading a token: "));
2029 yychar = YYLEX;
2030 }
2031
2032 if (yychar <= YYEOF)
2033 {
2034 yychar = yytoken = YYEOF;
2035 YYDPRINTF ((stderr, "Now at end of input.\n"));
2036 }
2037 else
2038 {
2039 yytoken = YYTRANSLATE (yychar);
2040 YY_SYMBOL_PRINT ("Next token is", yytoken, &yylval, &yylloc);
2041 }
# 省略
2045 yyn += yytoken;
2046 if (yyn < 0 || YYLAST < yyn || yycheck[yyn] != yytoken)
2047 goto yydefault;
2048 yyn = yytable[yyn];
# 省略
2106 YY_REDUCE_PRINT (yyn);
2107 switch (yyn)

構文解析の全体の処理を眺めたところで、引き続きyylex関数内を見ていきます。yylex関数内ではread_token関数が呼ばれます。


y.tab.c

4798 static int

4799 yylex ()
4800 {
4818 # 省略
4819 two_tokens_ago = token_before_that;
4820 token_before_that = last_read_token;
4821 last_read_token = current_token;
4822 current_token = read_token (READ);
4823 # 省略
4831
4832 return (current_token);
4833 }

read_token内でshell_getc関数がbashに解釈させる文字を1文字ずつ、EOFか改行に達するまで取得するのですが、実際の処理はshell_getc関数内のyy_getc関数で処理されてます。


y.tab.c

5150 static int

5151 read_token (command)
5152 int command;
5153 {
# 省略
5199 /* Read a single word from input. Start by skipping blanks. */
5200 while ((character = shell_getc (1)) != EOF && shellblank (character))
5201 ;


y.tab.c

4471 static int

4472 shell_getc (remove_quoted_newline)
4473 int remove_quoted_newline;
4474 {
# 省略
4542 while (1)
4543 {
4544 c = yy_getc ();
# 省略

shell_getc関数内のyy_getc関数が、メインで字句を取得していく関数になるのですが、この関数の処理の実体は先程説明した入力タイプの違いにより切り替わる関数(bash_input.getter)です。


y.tab.c

3692 static int

3693 yy_getc ()
3694 {
3695 return (*(bash_input.getter)) ();
3696 }

たとえば.bashrcに書かれた、export PATHという文字列を解釈する時は、yy_string_get関数が実行されます。yy_string_get関数はご覧の通り、文字が存在する限りその文字を取得する単純な実装になっています。


y.tab.c

3838 static int

3839 yy_string_get ()
3840 {
3841 register char *string;
3842 register unsigned char c;
3843
3844 string = bash_input.location.string;
3845
3846 /* If the string doesn't exist, or is empty, EOF found. */
3847 if (string && *string)
3848 {
3849 c = *string++;
3850 bash_input.location.string = string;
3851 return (c);
3852 }
3853 else
3854 return (EOF);

基本的にシェルの関数や変数を解釈する時の初期化処理は、文字列を読み込んでいくことになるので、yy_string_get関数によって字句や構文が解釈されていきます。その後、parse_and_execute関数に戻され、execute_command_internal関数を基点とする実行処理が行われます。

それらの処理が終わると、bash_input.getterはpop_stream関数によりyy_readline_getに設定されます。構文解析関数を含む入力ストリーム管理データの切り替えはpopという名前からわかるようにスタックで管理されています。


y.tab.c

4000 void

4001 pop_stream ()
4002 {
4003 if (!stream_list)
4004 EOF_Reached = 1;
4005 else
4006 {
4007 STREAM_SAVER *saver = stream_list;
4008
4009 EOF_Reached = 0;
4010 stream_list = stream_list->next;
4011
4012 init_yy_io (saver->bash_input.getter,
4013 saver->bash_input.ungetter,
4014 saver->bash_input.type,
4015 saver->bash_input.name,
4016 saver->bash_input.location);
# 省略

もともとreader_loop関数前の初期化処理の中で、set_bash_input関数よって、入力の構文解析関数はyy_readline_getに設定されるのですが、途中でyy_string_getが呼び出された都合上、push_stream関数でスタックに積み上げられていました。

いよいよ端末からの入力を待ち受ける段階の直前で再びyy_readline_get関数に復帰するという実装になっていて、bashの入力タイプを柔軟に切り替えられる実装になっています。


コマンドライン入力の為の処理へ

bash_input.getterがyy_readline_get関数に設定されてから、はじめてyy_getcが実行される時は、キーボードで入力する環境にする為に必要な初期化処理がinitialize_readline関数で行われます。

その後、readline関数が実行され、コマンドラインで待ち受ける為のメインの処理に入っていきます。


y.tab.c

3737 static int

3738 yy_readline_get ()
3739 {
# 省略
3744 if (!current_readline_line)
3745 {
3746 if (!bash_readline_initialized)
3747 initialize_readline ();
# 省略
3761
3762 current_readline_line = readline (current_readline_prompt ?
3763 current_readline_prompt : "");

readline関数から呼ばれるreadline_internal関数の中で、readline_internal_charloop関数の処理に移り、readline_internal_charloopの中でwhileループに突入し、rl_read_key関数から(*rl_getc_function)関数(rl_getc関数)内のread関数に処理に達したところでユーザーの入力を待ち続ける状態に入ります。


lib/readline/readline.c

307 char *

308 readline (prompt)
309 const char *prompt;
310 {
311 char *value;
# 省略
342 value = readline_internal ();
# 省略


lib/readline/readline.c

587 static char *

588 readline_internal ()
589 {
590 int eof;
591
592 readline_internal_setup ();
593 eof = readline_internal_charloop ();
594 return (readline_internal_teardown (eof));
595 }


lib/readline/readlinek.c

476 STATIC_CALLBACK int

477 #if defined (READLINE_CALLBACKS)
478 readline_internal_char ()
479 #else
480 readline_internal_charloop ()
481 #endif
482 {
483 static int lastc, eof_found;
484 int c, code, lk;
485
# 省略
490 while (rl_done == 0)
491 {
# 省略
515
516 RL_SETSTATE(RL_STATE_READCMD);
517 c = rl_read_key ();
518 RL_UNSETSTATE(RL_STATE_READCMD);


lib/readline/input.c

409 int

410 rl_read_key ()
411 {
412 int c;
413
# 省略
445 if (rl_get_char (&c) == 0)
446 c = (*rl_getc_function) (rl_instream);
447 RL_CHECK_SIGNALS ();


lib/readline/input.c

454 int

455 rl_getc (stream)
456 FILE *stream;
457 {
458 int result;
459 unsigned char c;
# 省略
469 result = read (fileno (stream), &c, sizeof (unsigned char));

この状態になると、下記のようにbashのプロンプトが表示されてユーザーの入力を待ち受けるという、いつもの見慣れた画面が表示されています。

$

この状態でたとえば、キーボードでpwdの後に続いて改行を入力すると、pwdという文字列がyylexやyyparse関数内の処理によって解析された後、execute_command関数を基点とする処理で該当する命令が実行されます。

$ pwd # カレントディレクトリを表示する命令

/home/hoge # カレントディレクトリが標準出力に出力


プロンプトが表示された状態からの処理

プロンプトでの入力待受の状態から、コマンド実行までを箇条書きにすると以下のフローです。冒頭のreader_loop関数のフローの中で入力待受状態を切り出したものになっています。


  1. 1文字入力する

  2. その1文字に対応する処理関数が実行される(基本的に制御文字以外の文字は、bashのバッファに文字が追加格納されるだけの処理※後述)。

  3. 1と2を繰り返す

  4. 改行が入力されると、字句解析が行われてexecute_command関数を基点とする処理が実行される。

  5. bashのプロセスを終了させる状態でなければ(reader_loop内のwhileループが終了しなければ)、1に戻る。

補足すると、4でexecute_command関数が実行される前に一度bashの処理は、read_command関数からreader_loop関数に戻ってきています。

reader_loop内から実際のコマンド実行処理(execute_command関数)が呼び出されて、reader_loopのループの冒頭に戻り、今度はコマンドラインに関する初期化処理を省略して、ユーザーからの入力を待ち受ける状態になります。冒頭のreader_loop関数のソースコードを今一度ご参照下さい。

入力文字列がbashの構文解析処理において、どのように解釈され実行されるかはここまでで大体確認できましたので、今度はその前段階、プロンプトが表示された段階で文字を入力した時の内部処理を確認していきましょう。

プロンプトが表示された時に、キーボードで何か文字を入力するたびに、表示上はほとんど変化ありませんが、bashの中で対応する処理がしっかり行われています。

たとえばpを入力します。

$ p

ユーザーからの入力待ちによって停止していたbashの処理は、pの入力によって動き出します。

入力値がpなので、rl_read_keyからの戻り値はcにpのASCII文字コード0x70が代入され(517行目)、その値は_rl_dispatch関数へ渡されます(552行目)。ちなみに、ここでunsigned charにキャストしているのが重要な意味を持ってきます。


lib/readline/readline.c

476 STATIC_CALLBACK int

477 #if defined (READLINE_CALLBACKS)
478 readline_internal_char ()
479 #else
480 readline_internal_charloop ()
481 #endif
482 {
# 略
516 RL_SETSTATE(RL_STATE_READCMD);
517 c = rl_read_key ();
518 RL_UNSETSTATE(RL_STATE_READCMD);
# 略
551 lastc = c;
552 _rl_dispatch ((unsigned char)c, _rl_keymap);
553 RL_CHECK_SIGNALS ();

_rl_dispatch関数から、更に_rl_dispatch_subseq関数へrl_read_keyからの戻り値が渡され、(*map[key].function)関数(774行目)が実行されます。

727 int

728 _rl_dispatch_subseq (key, map, got_subseq)
729 register int key;
730 Keymap map;
731 int got_subseq;
732 {
# 省略
760 switch (map[key].type)
761 {
762 case ISFUNC:
763 func = map[key].function;
764 if (func)
765 {
# 省略
774 (*map[key].function)(rl_numeric_arg * rl_arg_sign, key);
# 省略

関数ポインタによって実現されているこの関数がどこで定義されているかというと、現在のbashのキーマップ設定がemacsかviのどちらに設定されているかで異なります。

特に設定を変えていない場合は、emacsのキーマップ設定が適用されているはずなので、lib/readline/emacs_keymap.cのemacs_standard_keymapの配列変数を参照します。

viモードの場合は、lib/readline/vi_keymap.cファイルを確認して下さい。


lib/readline/emacs_keymap.c

32 KEYMAP_ENTRY_ARRAY emacs_standard_keymap = {

33
34 /* Control keys. */
35 { ISFUNC, rl_set_mark }, /* Control-@ */
36 { ISFUNC, rl_beg_of_line }, /* Control-a */
37 { ISFUNC, rl_backward_char }, /* Control-b */
38 { ISFUNC, (rl_command_func_t *)0x0 }, /* Control-c */
39 { ISFUNC, rl_delete }, /* Control-d */
40 { ISFUNC, rl_end_of_line }, /* Control-e */
41 { ISFUNC, rl_forward_char }, /* Control-f */
42 { ISFUNC, rl_abort }, /* Control-g */
43 { ISFUNC, rl_rubout }, /* Control-h */
44 { ISFUNC, rl_complete }, /* Control-i */
45 { ISFUNC, rl_newline }, /* Control-j */
46 { ISFUNC, rl_kill_line }, /* Control-k */
47 { ISFUNC, rl_clear_screen }, /* Control-l */
48 { ISFUNC, rl_newline }, /* Control-m */
49 { ISFUNC, rl_get_next_history }, /* Control-n */
50 { ISFUNC, (rl_command_func_t *)0x0 }, /* Control-o */
51 { ISFUNC, rl_get_previous_history }, /* Control-p */
52 { ISFUNC, rl_quoted_insert }, /* Control-q */
53 { ISFUNC, rl_reverse_search_history }, /* Control-r */
54 { ISFUNC, rl_forward_search_history }, /* Control-s */
55 { ISFUNC, rl_transpose_chars }, /* Control-t */
56 { ISFUNC, rl_unix_line_discard }, /* Control-u */
57 { ISFUNC, rl_quoted_insert }, /* Control-v */
58 { ISFUNC, rl_unix_word_rubout }, /* Control-w */
59 { ISKMAP, (rl_command_func_t *)emacs_ctlx_keymap }, /* Control-x */
60 { ISFUNC, rl_yank }, /* Control-y */
61 { ISFUNC, (rl_command_func_t *)0x0 }, /* Control-z */
62 { ISKMAP, (rl_command_func_t *)emacs_meta_keymap }, /* Control-[ */
63 { ISFUNC, (rl_command_func_t *)0x0 }, /* Control-\ */
64 { ISFUNC, rl_char_search }, /* Control-] */
65 { ISFUNC, (rl_command_func_t *)0x0 }, /* Control-^ */
66 { ISFUNC, rl_undo_command }, /* Control-_ */
67
68 /* The start of printing characters. */
69 { ISFUNC, rl_insert }, /* SPACE */
70 { ISFUNC, rl_insert }, /* ! */
71 { ISFUNC, rl_insert }, /* " */
72 { ISFUNC, rl_insert }, /* # */
73 { ISFUNC, rl_insert }, /* $ */
74 { ISFUNC, rl_insert }, /* % */
75 { ISFUNC, rl_insert }, /* & */
76 { ISFUNC, rl_insert }, /* ' */
77 { ISFUNC, rl_insert }, /* ( */
78 { ISFUNC, rl_insert }, /* ) */
79 { ISFUNC, rl_insert }, /* * */
80 { ISFUNC, rl_insert }, /* + */
81 { ISFUNC, rl_insert }, /* , */
82 { ISFUNC, rl_insert }, /* - */
83 { ISFUNC, rl_insert }, /* . */
84 { ISFUNC, rl_insert }, /* / */
# 以後ずっと最大254番目の要素までrl_insertが続く

入力したキーのASCII文字コードの整数に対応する要素の関数が、実行される実装になっています。pの場合は、配列の112番目の要素rl_insert_textという入力文字をバッファリングしておくための関数が実行されます。

何となく察しがつくと思いますが、最初の方の配列要素は制御文字に対応しているので、入力文字が制御文字の場合、何らかの目的をもった関数が実行されて、それ以外の通常文字の場合は、文字をbash内のメモリ(rl_line_buffer変数)にバッファリングしておくための、rl_insert_text関数が実行されているのがわかると思います。


マルチバイトの入力で問題が起きないのだろうか?

ここで入力が想定されているのはあくまで、ASCIIコードに基づいているのでそれ以外のマルチバイトの文字コード、たとえば符号化方式がUTF-8における"あ"を入力したらどうなるのでしょうか?

入力文字がunsigned char型にキャストされてたのを思い出して下さい。

"あ"はUTF-8のバイト列では0xe38182ですが1バイトにキャストされて0x82になります。この対応もrl_insert_text関数が実行されるので、入力文字をバッファリング関数に追加するだけの処理になっています。

UTF-8の2バイト目以降は0x80(128)以上で定義されているので、キーに対応する関数が配列の127番目以内に定義されていれば、UTF-8のマルチバイト文字を入力して制御文字で実行されるべき関数が実行されてしまうという可能性はありません。

よって、ただの文字列を意図した入力ならば、シングルバイトだろうがマルチバイトだろうが実行すべきその時が来るまで、バッファリングする処理が行われます。

ただ、それでは問題がおこる文字の符号化方式が存在する可能性も捨てきれませんが、少なくともUTF-8である限り問題は起こりません。

そしてpに続けてwdまでの入力の時点で、rl_line_buffer変数にはpwdまでが格納されています。

$ pwd # ここでエンターを押す。

pwdが入力された時点でエンターキーを押すと、emacs_standard_keymap変数の10番目の要素の、rl_newline関数が実行され、この関数内で、rl_done変数に1が代入されます。


lib/readline/text.c

949 int

950 rl_newline (count, key)
951 int count, key;
952 {
953 rl_done = 1;
# 省略

rl_doneが0以外だと、readline_internal_charloop関数内のwhileループが終了する条件になるので、処理はyylexやyyparseに戻っていき、先述した命令を実行する処理に移っていきます。

先述した通り、文字列入力の処理は全てyy_getc関数によってラップされているので、入力コンテキストが文字列の場合でも、キーボードの場合でも入力から実行までの処理は基本的に同じです。

命令を実行した後は、再びreader_loopのループの冒頭まで戻り、readline_internal_charloop内の入力待受処理まで実行されます。この繰り返しです。

冒頭で説明した、bashがインタラクティブモードで起動された時は、半永久的にreader_loopの処理を繰り返すとはこのことを意味しています。

要点をだいぶ絞って説明したので、細かい部分で説明していない箇所もたくさんありますが、bashが起動してから入力を待ち受けるまでの重要な部分の処理は以上になります。

TABによる補完機能の処理を担っているのは、emacs_standard_keymap関数の9番目の要素、rl_complete関数です。

TABは制御文字で、単純にテキストをバッファリングする処理ではなく、補完機能という重要な機能を担いますので、次回はこのへんの説明をしたいと思います。