🐚

[課題振り返り] minishell 前編

2025/01/27に公開

はじめに

今回は42の一番の山場ともいわれるminishellについて振り返っていきます。

課題概要

C言語で基本的な機能を持ったshell(bash)を実装します。

実装方法

一番参考にするのはBash Reference Manualだと思います。(terminalでman bashでも見れます)
https://www.gnu.org/software/bash/manual/bash.html#What-is-Bash_003f

bashの実際のソースコードじゃないの?と思った方もいると思います。が、正直bashのソースコードを全て読もうと思うと果てしなく時間がかかると思います。
また、今回の課題は最小限の機能しか実装しないので実際のコードで余計な部分も見ていくのは課題をクリアする観点ではかなり大変になります。

bashの利用方法

自作shellの挙動がbashと一致するかどうかを確かめるためにbashを利用しますが、コマンドラインでbashと入力することでbashシェルに切り替えることができます。

学んだこと

shellの実行順序

以下はリファレンスの引用です。

(原文)
3.1.1 Shell Operation
The following is a brief description of the shell’s operation when it reads and executes a command. Basically, the shell does the following:
1.Reads its input from a file (see Shell Scripts), from a string supplied as an argument to the -c invocation option (see Invoking Bash), or from the user’s terminal.
2.Breaks the input into words and operators, obeying the quoting rules described in Quoting. These tokens are separated by metacharacters. Alias expansion is performed by this step (see Aliases).
3.Parses the tokens into simple and compound commands (see Shell Commands).
4.Performs the various shell expansions (see Shell Expansions), breaking the expanded tokens into lists of filenames (see Filename Expansion) and commands and arguments.
5.Performs any necessary redirections (see Redirections) and removes the redirection operators and their operands from the argument list.
6.Executes the command (see Executing Commands).
7.Optionally waits for the command to complete and collects its exit status (see Exit Status).

(Web日本語翻訳)
3.1.1 シェル操作
以下は、シェルがコマンドを読み取って実行するときの動作の簡単な説明です。基本的に、シェルは次のことを行います。
1.ファイルから入力を読み取ります(シェルスクリプトを参照)、引数として指定された文字列から-c呼び出しオプション(「Bash の呼び出し」を参照)、またはユーザーの端末から実行します。
2.引用符で説明されている引用符の規則に従って、入力を単語と演算子に分割します。これらのトークンはメタ文字で区切られます。エイリアスの展開はこのステップで実行されます (エイリアスを参照)。
3.トークンを単純コマンドと複合コマンドに解析します ( 「シェル コマンド」を参照)。
4.さまざまなシェル拡張 ( 「シェル拡張」を参照) を実行し、拡張されたトークンをファイル名 ( 「ファイル名拡張」を参照) とコマンドおよび引数のリストに分割します。
5.必要なリダイレクト (リダイレクトを参照) を実行し、リダイレクト演算子とそのオペランドを引数リストから削除します。
6.コマンドを実行します ( 「コマンドの実行」を参照)。
7.オプションで、コマンドが完了するまで待機し、終了ステータスを収集します ( 「終了ステータス」を参照)。

なるべくこれに沿って振り返り、説明をしていきます。

1.入力の受け取り方

bashが入力を受け取る方法は3種類あります。

  1. ファイルから
bash script.sh

このようにシェルスクリプトを指定することでそのファイルを上から順に実行します。

  1. -cで文字列を指定
bash -c "echo Hello, World!"

-cで指定した文字列がコマンドとして処理されます。

  1. 標準入力から
$ echo Hello, World!

プロンプトを表示して標準入力から受け付けます。
これが最もよく使われるshellの使い方だと思います。

また、今回の課題では3のみ対応すれば良いです。
そして3に対応する方法としてreadline関数を使用します。

readline

以下manualの日本語翻訳をしてくれているサイトです
https://nxmnpg.lemoda.net/ja/3/readline
shell再実装の入り口になるような関数です。
標準入力からの入力を受け付け続けることができます。

MacOSでの使用方法

brew install readline

をして、コンパイル時に"-lreadline"を指定してください。
コンパイル時に"-L(ライブラリを検索するパス)"を指定しないと、システムの標準ライブラリパス(/libや/usr/lib)などから検索してくれます。

以下がコード例になります。

#include <readline/readline.h>
#include <readline/history.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    while (1)
    {
        char *line = readline("prompt> ");
        if (*line)
            add_history(line);
        printf("line: %s\n", line);
        if (!line)
            break;
        free(line);
    }
    return (0);
}
(実行結果)
$ cc readline.c -lreadline // -lreadlineが必要
$ ./a.out 
prompt> hello
line: hello
prompt> world
line: world
prompt> ^Dline: (null)

readlineの引数に文字列を与えることでそれをプロンプトとして表示してくれます。(bashでいうbash-3.2$ のようなもの)
このコードは受け取った入力をそのまま出力するコードです。
manualにもあるようにreadlineはmallocを用いてメモリを確保しているのでfreeする必要があります。
また、add_history(line)とすることで履歴機能を使用することができます。上下矢印キーを使用することで前の入力に遡ることができます。
また、例ではCtrl+Dを押すことでプログラムが終了しています。

Ctrl+D

Ctrl+Dは何をしているのでしょうか?
minitalk編でCtrl+Cはプログラムに対してSIGINTを送っていると説明しました。(デフォルトでSIGINTはプログラムを強制終了させる)
では、同様にCtrl+Dはプログラムに何かシグナルを送っているのでしょうか?
実はCtrl+Dはプログラムに対して特別なEOF(End Of File)を送ります。これはシグナルではなく、入力が終了したことを知らせるものです。
readlineはこのEOFを受け取るとNULLを返します。(manual参照)
なので、if(!line)の条件がtrueとなり、breakしプログラムが終了するということです。
逆にいえばif(!line)の部分がないとプログラムは終了しません。

2.tokenize

入力を最小単位に分割していく作業です。
基本的にはshellは単語同士はスペースで分割されますが、後の作業のためにそのtokenの種類を持つリスト構造体を作ります。
bashのマニュアルにはtokenは以下のように説明があります。

DEFINITIONS
       The following definitions are used throughout the rest of this document.
       blank  A space or tab.
       word   A sequence of characters considered as a single unit by the shell.  Also
              known as a token.
       name   A word consisting only of alphanumeric characters and underscores, and
              beginning with an alphabetic character or an underscore.  Also referred to
              as an identifier.
       metacharacter
              A character that, when unquoted, separates words.  One of the following:
              |  & ; ( ) < > space tab
       control operator
              A token that performs a control function.  It is one of the following
              symbols:
              || & && ; ;; ( ) | <newline>

これがtokenの種類です。

3.トークンを単純コマンドと複合コマンドに解析します

これは今回の課題要件では単純コマンドのみに対応すれば良いのでここでは説明しません。
(ループ構造や条件分岐など)

4.トークンをファイル名とコマンドおよび引数のリストに分割する

これは簡単に言うと2で分割したtokenに意味を持たせて処理しやすい構造にするパートです。
細かな構造の違いは置いておいて、僕は以下のような構造にしました。

<pipeline> = <simple_command> ('|' <pipeline>)
<simple_command> = <command_element>+
<command_element> = <word> | <redirection>
<redirection> = '>' <word>
              | '<' <word>
              | '>>' <word>
              | '<<' <word>

これはEBNFという構文規則を記述するためのフォーマットで詳しくは他のサイトなどで勉強して欲しいのですが、簡単に説明すると
1行目: <pipeline>は1つの<simple_command>を要し、'|'で繋いで次の<pipeline>を繋げることができる。
2行目: <simple_command>は<command_element>の1回以上の繰り返し。
3行目: <command_element>は<word>または<redirection>である。
4~行目: <redirection>は'>'または'<'または'>>'または'<<'で繋いで<word>を繋ぐ。

これに基づいてリスト構造を作成していきます。

おわりに

今回はここまでにします。続きは後編を書くので書き次第ここにもリンクを貼ります。

参考文献

Bash Reference Manual: https://www.gnu.org/software/bash/manual/bash.html#What-is-Bash_003f
readline関数 manualの日本語翻訳をしてくれているサイト: https://nxmnpg.lemoda.net/ja/3/readline

Discussion