第6章: ビルトインコマンドとシグナル処理の理論

6.1 ビルトインコマンドの理論

なぜビルトインが必要か

UNIXシェルにおいて、一部のコマンドはビルトイン(Built-in)として実装される必要がある。これは、プロセスモデルの本質的な制約に起因する。

プロセスアドレス空間の独立性:

親プロセス(シェル)          子プロセス
┌─────────────────────┐      ┌─────────────────────┐
│ 環境変数            │      │ 環境変数(コピー)  │
│ カレントディレクトリ│      │ カレントディレクトリ│
│ シェル状態          │      │ 独立した状態        │
└─────────────────────┘      └─────────────────────┘
         ↑                              ↑
         │                              │
    変更が永続化              exit時に破棄

子プロセスでchdir()を実行しても、親プロセスのカレントディレクトリには影響しない。これはfork()によりアドレス空間がコピーされるためである。

POSIXビルトイン要件

POSIX.1-2017(IEEE Std 1003.1-2017)では、シェルが実装すべきビルトインを規定している:

Special Built-In Utilities(特殊ビルトイン):

break, :, continue, ., eval, exec, exit, export,
readonly, return, set, shift, times, trap, unset

Regular Built-In Utilities(通常ビルトイン):

alias, bg, cd, command, false, fc, fg, getopts,
hash, jobs, kill, newgrp, pwd, read, true,
umask, unalias, wait

minishellで実装するビルトイン:

  • echo: 引数の出力
  • cd: ディレクトリ変更
  • pwd: 現在のディレクトリ表示
  • export: 環境変数の設定
  • unset: 環境変数の削除
  • env: 環境変数の一覧
  • exit: シェルの終了

ビルトインの実行フロー

コマンド入力
     │
     ↓
┌─────────────────┐
│ ビルトインか?   │
└─────────────────┘
     │         │
    YES       NO
     │         │
     ↓         ↓
┌─────────┐  ┌─────────┐
│ 親で実行│  │ fork()  │
│         │  │ execve()│
└─────────┘  └─────────┘

6.2 環境変数の理論

環境変数の歴史

環境変数(Environment Variables)は、Version 7 UNIX(1979年)で導入された。Bourneシェルの設計者Stephen Bourneが、プロセス間で設定を継承するメカニズムとして考案した。

環境変数の構造

POSIX規定の環境変数形式:

NAME=value

制約:
- NAMEは英字またはアンダースコアで始まる
- NAMEは英数字とアンダースコアのみを含む
- 大文字小文字を区別する
- valueは任意の文字列(NULLを除く)

プロセスと環境変数

execve()システムコールは環境変数を受け取る:

int execve(const char *pathname, char *const argv[], char *const envp[]);

envp"KEY=value"形式の文字列配列で、NULLで終端される。

execve時の環境変数:

envp[0] → "PATH=/usr/bin:/bin"
envp[1] → "HOME=/home/user"
envp[2] → "USER=username"
envp[3] → "PWD=/current/dir"
envp[4] → NULL

環境変数のデータ構造

typedef struct s_env
{
    char            *key;       /* 変数名 */
    char            *value;     /* 値 */
    int             exported;   /* export済みフラグ */
    struct s_env    *next;      /* 次のノード */
}   t_env;

連結リストを使用する理由:

  • 動的な追加・削除が容易
  • サイズの変更が柔軟
  • 順序の維持が可能

環境変数の操作

/* 環境変数の取得 */
char *get_env_value(t_env *env, const char *key)
{
    while (env)
    {
        if (ft_strcmp(env->key, key) == 0)
            return (env->value);
        env = env->next;
    }
    return (NULL);
}

/* 環境変数の設定 */
int set_env_value(t_env **env, const char *key, const char *value)
{
    t_env *current;
    t_env *new_node;

    /* 既存の変数を探す */
    current = *env;
    while (current)
    {
        if (ft_strcmp(current->key, key) == 0)
        {
            free(current->value);
            current->value = ft_strdup(value);
            return (0);
        }
        current = current->next;
    }

    /* 新しい変数を追加 */
    new_node = create_env_node(key, value);
    if (!new_node)
        return (-1);

    add_env_node(env, new_node);
    return (0);
}

6.3 echoコマンドの実装

POSIX仕様

POSIXではechoの動作を以下のように規定している:

echo [-n] [string ...]

-n: 末尾の改行を出力しない

注意: POSIXでは-nの動作は実装依存としている。bashでは-nを認識するが、一部のシステムでは文字列として扱う。

実装

int builtin_echo(char **argv)
{
    int     i;
    int     newline;
    int     j;

    newline = 1;
    i = 1;

    /* -nオプションの処理(複数可: -n -nn -nnn等) */
    while (argv[i] && argv[i][0] == '-')
    {
        j = 1;
        while (argv[i][j] == 'n')
            j++;

        /* -nの後に他の文字がなければオプション */
        if (argv[i][j] == '\0' && j > 1)
        {
            newline = 0;
            i++;
        }
        else
            break;
    }

    /* 引数を出力 */
    while (argv[i])
    {
        ft_putstr_fd(argv[i], STDOUT_FILENO);
        if (argv[i + 1])
            ft_putchar_fd(' ', STDOUT_FILENO);
        i++;
    }

    /* 改行(-nがなければ) */
    if (newline)
        ft_putchar_fd('\n', STDOUT_FILENO);

    return (0);  /* echoは常に成功 */
}

6.4 cdコマンドの実装

cdの理論

cd(change directory)は、プロセスのカレントディレクトリを変更する。内部的にはchdir()システムコールを使用する。

#include <unistd.h>

int chdir(const char *path);

POSIX仕様

cd [-L|-P] [directory]

-L: シンボリックリンクを論理的にたどる(デフォルト)
-P: シンボリックリンクを物理的にたどる

特殊なケース:

  • cd: $HOMEに移動
  • cd -: $OLDPWDに移動し、そのパスを出力
  • cd ~: $HOMEに移動

実装

int builtin_cd(char **argv, t_shell *shell)
{
    char    *path;
    char    *oldpwd;
    char    *newpwd;

    /* 引数なし: HOMEへ移動 */
    if (!argv[1])
    {
        path = get_env_value(shell->env, "HOME");
        if (!path)
        {
            print_error("cd", "HOME not set");
            return (1);
        }
    }
    /* cd -: OLDPWDへ移動 */
    else if (ft_strcmp(argv[1], "-") == 0)
    {
        path = get_env_value(shell->env, "OLDPWD");
        if (!path)
        {
            print_error("cd", "OLDPWD not set");
            return (1);
        }
        /* パスを表示 */
        ft_putendl_fd(path, STDOUT_FILENO);
    }
    /* 通常のパス */
    else
    {
        path = argv[1];
    }

    /* 現在のディレクトリを保存 */
    oldpwd = getcwd(NULL, 0);

    /* ディレクトリ変更 */
    if (chdir(path) == -1)
    {
        print_error_with_arg("cd", path, strerror(errno));
        free(oldpwd);
        return (1);
    }

    /* OLDPWD更新 */
    if (oldpwd)
    {
        set_env_value(&shell->env, "OLDPWD", oldpwd);
        free(oldpwd);
    }

    /* PWD更新 */
    newpwd = getcwd(NULL, 0);
    if (newpwd)
    {
        set_env_value(&shell->env, "PWD", newpwd);
        free(newpwd);
    }

    return (0);
}

引数の検証

int validate_cd_args(char **argv)
{
    struct stat st;

    /* 引数が多すぎる */
    if (argv[1] && argv[2])
    {
        print_error("cd", "too many arguments");
        return (1);
    }

    if (argv[1] && ft_strcmp(argv[1], "-") != 0)
    {
        /* パスの存在確認 */
        if (stat(argv[1], &st) == -1)
        {
            print_error_with_arg("cd", argv[1], strerror(errno));
            return (1);
        }

        /* ディレクトリかどうか */
        if (!S_ISDIR(st.st_mode))
        {
            print_error_with_arg("cd", argv[1], "Not a directory");
            return (1);
        }

        /* 実行権限(検索権限)の確認 */
        if (access(argv[1], X_OK) == -1)
        {
            print_error_with_arg("cd", argv[1], strerror(errno));
            return (1);
        }
    }

    return (0);
}

6.5 pwdコマンドの実装

getcwd()システムコール

#include <unistd.h>

char *getcwd(char *buf, size_t size);

bufがNULLの場合、getcwd()は必要なサイズのメモリを動的に確保する(POSIX.1-2001拡張)。

実装

int builtin_pwd(void)
{
    char *cwd;

    cwd = getcwd(NULL, 0);
    if (!cwd)
    {
        perror("pwd");
        return (1);
    }

    ft_putendl_fd(cwd, STDOUT_FILENO);
    free(cwd);

    return (0);
}

6.6 exportコマンドの実装

動作仕様

export [name[=value] ...]

引数なし: 全てのexport済み変数を表示
name=value: 変数を設定しexport
name: 変数をexport(値なし)

変数名の検証

POSIX規定の識別子規則:

int is_valid_identifier(const char *str)
{
    int i;

    if (!str || !*str)
        return (0);

    /* 最初の文字: 英字またはアンダースコア */
    if (!ft_isalpha(str[0]) && str[0] != '_')
        return (0);

    /* 残りの文字: 英数字またはアンダースコア */
    i = 1;
    while (str[i] && str[i] != '=')
    {
        if (!ft_isalnum(str[i]) && str[i] != '_')
            return (0);
        i++;
    }

    return (1);
}

実装

int builtin_export(char **argv, t_shell *shell)
{
    int i;
    int status;

    /* 引数なし: 環境変数を表示 */
    if (!argv[1])
    {
        print_exported_vars(shell->env);
        return (0);
    }

    status = 0;
    i = 1;
    while (argv[i])
    {
        if (!is_valid_identifier(argv[i]))
        {
            print_export_error(argv[i]);
            status = 1;
        }
        else
        {
            export_variable(shell, argv[i]);
        }
        i++;
    }

    return (status);
}

void export_variable(t_shell *shell, char *arg)
{
    char    *key;
    char    *value;
    char    *equals;

    equals = ft_strchr(arg, '=');

    if (equals)
    {
        /* KEY=value形式 */
        key = ft_substr(arg, 0, equals - arg);
        value = ft_strdup(equals + 1);

        set_env_value(&shell->env, key, value);
        mark_exported(shell->env, key);

        free(key);
        free(value);
    }
    else
    {
        /* KEYのみ(exportマークを付ける) */
        mark_exported(shell->env, arg);
    }
}

export時の表示

void print_exported_vars(t_env *env)
{
    t_env   **sorted;
    int     count;
    int     i;

    /* 環境変数をソート */
    count = count_env_vars(env);
    sorted = sort_env_vars(env, count);
    if (!sorted)
        return;

    /* 表示 */
    i = 0;
    while (i < count)
    {
        ft_putstr_fd("declare -x ", STDOUT_FILENO);
        ft_putstr_fd(sorted[i]->key, STDOUT_FILENO);

        if (sorted[i]->value)
        {
            ft_putstr_fd("=\"", STDOUT_FILENO);
            ft_putstr_fd(sorted[i]->value, STDOUT_FILENO);
            ft_putchar_fd('"', STDOUT_FILENO);
        }

        ft_putchar_fd('\n', STDOUT_FILENO);
        i++;
    }

    free(sorted);
}

6.7 unsetコマンドの実装

動作仕様

unset [name ...]

指定された変数を削除する。

実装

int builtin_unset(char **argv, t_shell *shell)
{
    int i;
    int status;

    if (!argv[1])
        return (0);

    status = 0;
    i = 1;
    while (argv[i])
    {
        if (!is_valid_env_name(argv[i]))
        {
            print_unset_error(argv[i]);
            status = 1;
        }
        else
        {
            remove_env_var(&shell->env, argv[i]);
        }
        i++;
    }

    return (status);
}

void remove_env_var(t_env **env, const char *key)
{
    t_env   *current;
    t_env   *prev;

    current = *env;
    prev = NULL;

    while (current)
    {
        if (ft_strcmp(current->key, key) == 0)
        {
            /* ノードを削除 */
            if (prev)
                prev->next = current->next;
            else
                *env = current->next;

            free(current->key);
            free(current->value);
            free(current);
            return;
        }

        prev = current;
        current = current->next;
    }
}

6.8 envコマンドの実装

動作仕様

env

値が設定されている全ての環境変数を表示する。

実装

int builtin_env(t_env *env)
{
    while (env)
    {
        /* 値がある変数のみ表示 */
        if (env->value && env->exported)
        {
            ft_putstr_fd(env->key, STDOUT_FILENO);
            ft_putchar_fd('=', STDOUT_FILENO);
            ft_putendl_fd(env->value, STDOUT_FILENO);
        }

        env = env->next;
    }

    return (0);
}

6.9 exitコマンドの実装

動作仕様

exit [n]

終了ステータスnでシェルを終了する。
nが省略された場合、直前のコマンドの終了ステータスを使用する。

終了ステータスの理論

POSIX規定の終了ステータス:

  • 0: 成功
  • 1-125: コマンド固有のエラー
  • 126: コマンドは見つかったが実行不可
  • 127: コマンドが見つからない
  • 128+N: シグナルNによる終了

実装

int builtin_exit(char **argv, t_shell *shell)
{
    long long   exit_code;

    ft_putendl_fd("exit", STDERR_FILENO);

    /* 引数なし: 直前の終了ステータス */
    if (!argv[1])
    {
        cleanup_shell(shell);
        exit(shell->last_exit);
    }

    /* 数値でない場合 */
    if (!is_numeric_argument(argv[1]))
    {
        print_exit_error(argv[1], "numeric argument required");
        cleanup_shell(shell);
        exit(2);
    }

    /* 引数が多すぎる */
    if (argv[2])
    {
        print_error("exit", "too many arguments");
        return (1);  /* シェルは終了しない */
    }

    /* オーバーフローチェック */
    if (!is_valid_exit_value(argv[1], &exit_code))
    {
        print_exit_error(argv[1], "numeric argument required");
        cleanup_shell(shell);
        exit(2);
    }

    cleanup_shell(shell);
    exit((unsigned char)exit_code);
}

数値引数の検証

int is_numeric_argument(const char *str)
{
    int i;

    if (!str || !*str)
        return (0);

    i = 0;

    /* 符号 */
    if (str[i] == '+' || str[i] == '-')
        i++;

    /* 少なくとも1つの数字が必要 */
    if (!str[i])
        return (0);

    /* 全て数字 */
    while (str[i])
    {
        if (!ft_isdigit(str[i]))
            return (0);
        i++;
    }

    return (1);
}

int is_valid_exit_value(const char *str, long long *result)
{
    long long   num;
    int         sign;
    int         i;

    num = 0;
    sign = 1;
    i = 0;

    if (str[i] == '-')
    {
        sign = -1;
        i++;
    }
    else if (str[i] == '+')
        i++;

    while (str[i])
    {
        /* オーバーフローチェック */
        if (num > (LLONG_MAX - (str[i] - '0')) / 10)
            return (0);

        num = num * 10 + (str[i] - '0');
        i++;
    }

    *result = num * sign;
    return (1);
}

6.10 シグナル処理の理論

シグナルの歴史

シグナルは、UNIX Version 4(1973年)で導入されたソフトウェア割り込みメカニズムである。初期のシグナルは信頼性に欠けており、BSD信頼シグナル(4.2BSD, 1983年)で改善された。

POSIX.1-1990で標準化され、現在はPOSIX.1-2017が最新規格である。

シグナルの分類

1. プロセス制御シグナル
   SIGINT  (2)  - 端末割り込み(Ctrl+C)
   SIGQUIT (3)  - 端末終了(Ctrl+\)
   SIGTERM (15) - 終了要求
   SIGKILL (9)  - 強制終了(捕捉不可)
   SIGSTOP (19) - 停止(捕捉不可)

2. 端末シグナル
   SIGTSTP (20) - 端末停止(Ctrl+Z)
   SIGTTIN (21) - バックグラウンド読み取り
   SIGTTOU (22) - バックグラウンド書き込み

3. パイプシグナル
   SIGPIPE (13) - 壊れたパイプへの書き込み

4. 子プロセスシグナル
   SIGCHLD (17) - 子プロセスの状態変化

sigaction()システムコール

POSIXでは、signal()よりもsigaction()の使用を推奨している:

#include <signal.h>

int sigaction(int signum, const struct sigaction *act,
              struct sigaction *oldact);

struct sigaction {
    void     (*sa_handler)(int);           /* ハンドラ関数 */
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;                    /* ブロックするシグナル */
    int        sa_flags;                   /* オプションフラグ */
};

非同期シグナルセーフ関数

シグナルハンドラ内で安全に呼び出せる関数は限られている(POSIX.1-2017 表2-4):

write(), _exit(), signal()
(その他多数、ただしprintf(), malloc(), free()は含まれない)

重要: シグナルハンドラ内ではwrite()を使用し、printf()は使用しない。

6.11 シェルのシグナル処理

インタラクティブモード

プロンプト表示中の動作:

void setup_interactive_signals(void)
{
    struct sigaction sa_int;
    struct sigaction sa_quit;

    /* SIGINT: 新しいプロンプトを表示 */
    sigemptyset(&sa_int.sa_mask);
    sa_int.sa_flags = 0;
    sa_int.sa_handler = handle_sigint_interactive;
    sigaction(SIGINT, &sa_int, NULL);

    /* SIGQUIT: 無視 */
    sigemptyset(&sa_quit.sa_mask);
    sa_quit.sa_flags = 0;
    sa_quit.sa_handler = SIG_IGN;
    sigaction(SIGQUIT, &sa_quit, NULL);
}

void handle_sigint_interactive(int sig)
{
    (void)sig;

    write(STDOUT_FILENO, "\n", 1);

    /* readlineの状態をリセット */
    rl_on_new_line();
    rl_replace_line("", 0);
    rl_redisplay();

    /* 終了ステータスを130に設定 */
    g_signal_status = 130;
}

コマンド実行モード

子プロセス実行中の動作:

void setup_execution_signals(void)
{
    struct sigaction sa;

    /* デフォルト動作に戻す */
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sa.sa_handler = SIG_DFL;

    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGQUIT, &sa, NULL);
}

ヒアドキュメントモード

ヒアドキュメント入力中の動作:

void setup_heredoc_signals(void)
{
    struct sigaction sa;

    /* SIGINT: 入力をキャンセル */
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sa.sa_handler = handle_sigint_heredoc;
    sigaction(SIGINT, &sa, NULL);

    /* SIGQUIT: 無視 */
    signal(SIGQUIT, SIG_IGN);
}

void handle_sigint_heredoc(int sig)
{
    (void)sig;

    write(STDOUT_FILENO, "\n", 1);

    /* ヒアドキュメントを中断 */
    g_heredoc_interrupted = 1;

    /* readlineを終了させる */
    rl_done = 1;

    g_signal_status = 130;
}

6.12 グローバル変数とシグナル

volatile sig_atomic_t

シグナルハンドラとメインプログラム間で共有する変数は、volatile sig_atomic_t型を使用する:

volatile sig_atomic_t g_signal_status = 0;

volatile: コンパイラの最適化を防ぐ sig_atomic_t: アトミックなアクセスを保証

グローバル変数の設計

/* 最小限のグローバル変数 */
typedef struct s_signal_state
{
    volatile sig_atomic_t received_signal;
    volatile sig_atomic_t in_heredoc;
}   t_signal_state;

t_signal_state g_signal;

POSIXシグナルハンドラでは、グローバル変数への書き込みはsig_atomic_t型のみが安全である。

6.13 ビルトインのディスパッチ

コマンドテーブル

typedef int (*t_builtin_func)(char **argv, t_shell *shell);

typedef struct s_builtin
{
    const char      *name;
    t_builtin_func  func;
}   t_builtin;

static const t_builtin g_builtins[] = {
    {"echo",   builtin_echo_wrapper},
    {"cd",     builtin_cd},
    {"pwd",    builtin_pwd_wrapper},
    {"export", builtin_export},
    {"unset",  builtin_unset},
    {"env",    builtin_env_wrapper},
    {"exit",   builtin_exit},
    {NULL,     NULL}
};

ビルトインの判定と実行

int is_builtin(const char *cmd)
{
    int i;

    if (!cmd)
        return (0);

    i = 0;
    while (g_builtins[i].name)
    {
        if (ft_strcmp(cmd, g_builtins[i].name) == 0)
            return (1);
        i++;
    }

    return (0);
}

int execute_builtin(t_cmd *cmd, t_shell *shell)
{
    int i;

    i = 0;
    while (g_builtins[i].name)
    {
        if (ft_strcmp(cmd->argv[0], g_builtins[i].name) == 0)
            return (g_builtins[i].func(cmd->argv, shell));
        i++;
    }

    return (1);  /* 到達しないはず */
}

6.14 パイプ内のビルトイン

問題

ビルトインがパイプ内にある場合、子プロセスで実行する必要がある:

echo hello | cat    # echoは子プロセスで実行
cd /tmp | pwd       # cdは子プロセスで実行(親には影響なし)

実装

int execute_builtin_in_pipeline(t_cmd *cmd, t_shell *shell)
{
    pid_t   pid;
    int     status;

    pid = fork();
    if (pid == -1)
        return (1);

    if (pid == 0)
    {
        /* 子プロセス */
        setup_execution_signals();

        if (cmd->redirects)
        {
            if (setup_redirections(cmd->redirects) != 0)
                exit(1);
        }

        exit(execute_builtin(cmd, shell));
    }

    waitpid(pid, &status, 0);
    return (WEXITSTATUS(status));
}

6.15 特殊な環境変数

SHLVL

シェルのネストレベルを追跡:

void init_shlvl(t_shell *shell)
{
    char    *shlvl_str;
    int     level;
    char    *new_level;

    shlvl_str = get_env_value(shell->env, "SHLVL");

    if (shlvl_str)
        level = ft_atoi(shlvl_str) + 1;
    else
        level = 1;

    if (level < 1)
        level = 1;
    else if (level > 999)
        level = 1;  /* リセット */

    new_level = ft_itoa(level);
    set_env_value(&shell->env, "SHLVL", new_level);
    free(new_level);
}

PWDとOLDPWD

void init_pwd_vars(t_shell *shell)
{
    char *cwd;

    cwd = getcwd(NULL, 0);
    if (cwd)
    {
        set_env_value(&shell->env, "PWD", cwd);
        free(cwd);
    }

    /* OLDPWDは初期化しない(未定義のまま) */
}

6.16 まとめ

本章では、ビルトインコマンドとシグナル処理について学んだ:

  • ビルトインの必要性: プロセスアドレス空間の独立性
  • 環境変数: 構造と操作
  • 各ビルトイン: echo, cd, pwd, export, unset, env, exit
  • シグナル理論: POSIX規格とsigaction()
  • シグナル処理: インタラクティブ/実行/ヒアドキュメントモード
  • グローバル変数: volatile sig_atomic_t
  • これでminishellの実装に必要な全ての理論的基盤を学んだ。各コンポーネントを統合し、完全に動作するシェルを構築することができる。

    ---

    参考文献

  • IEEE (2017). "POSIX.1-2017: Shell & Utilities", IEEE Std 1003.1-2017
  • Bourne, S. R. (1978). "The UNIX Shell", Bell System Technical Journal, 57(6)
  • Stevens, W. R., & Rago, S. A. (2013). "Advanced Programming in the UNIX Environment", 3rd Edition, Addison-Wesley
  • Kerrisk, M. (2010). "The Linux Programming Interface", No Starch Press
  • Rochkind, M. J. (2004). "Advanced UNIX Programming", 2nd Edition, Addison-Wesley
  • McKusick, M. K., et al. (1996). "The Design and Implementation of the 4.4BSD Operating System", Addison-Wesley
  • Bach, M. J. (1986). "The Design of the UNIX Operating System", Prentice Hall