LoginSignup
400

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-03-21

はじめに

以前書いたエントリー、重大な脆弱性(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は制御文字で、単純にテキストをバッファリングする処理ではなく、補完機能という重要な機能を担いますので、次回はこのへんの説明をしたいと思います。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
400