第5章: ヒアドキュメントとテキストストリーム処理 - シェル言語設計の計算機科学的基盤

5.1 シェル言語の歴史と設計哲学

UNIXシェルの進化

UNIXシェルは、1971年のThompson Shell(V1-V6 UNIX)から始まり、複数の世代を経て発展しました:

UNIXシェルの系譜:

1971  Thompson Shell (sh) - Ken Thompson
       │
       │ 限定的な機能、パイプなし
       │
1973  Thompson Shell + Pipes
       │
       │ パイプ演算子 | の追加
       │
1977  Bourne Shell (sh) - Stephen Bourne
       │
       │ ★ ヒアドキュメント登場 <<
       │ ★ 制御構造(if, while, for)
       │ ★ 変数とパラメータ展開
       │
1978  C Shell (csh) - Bill Joy
       │
       │ ヒストリ機能、ジョブ制御
       │
1983  Korn Shell (ksh) - David Korn
       │
       │ Bourne Shell + C Shell の統合
       │
1989  Bash - Brian Fox (GNU)
       │
       │ Bourne Again Shell
       │ POSIX準拠 + 拡張機能
       │
2005  Zsh, Fish など
       現代のシェル

Stephen Bourneの設計思想

Bourne Shell(1977)の設計者Stephen Bourneは、シェルを「プログラミング言語」として再設計しました:

Bourneの設計原則:

1. シェルスクリプトは正式なプログラム
   ├─ 制御構造(if-then-else, while, for)
   ├─ 関数定義
   └─ 変数と演算

2. テキストストリームの統一的処理
   ├─ 標準入出力の一貫した扱い
   ├─ パイプラインの自然な記述
   └─ リダイレクションの柔軟性

3. ヒアドキュメント:インラインデータ
   ├─ スクリプト内に直接データを埋め込む
   ├─ 外部ファイル依存の削減
   └─ 可読性の向上

5.2 ヒアドキュメントの理論的基盤

ヒアドキュメントとは何か

ヒアドキュメント(Here-Document)は、シェルスクリプト内でインライン複数行テキストを記述する構文です:

# 基本構文
command << DELIMITER
line 1
line 2
...
DELIMITER

計算機科学的には、これはリテラルブロックまたはヒアストリングと呼ばれる概念の実装です:

ヒアドキュメントの構文論的位置づけ:

プログラミング言語における類似概念:
┌────────────────┬────────────────────────────────────┐
│ 言語           │ 同等の機能                          │
├────────────────┼────────────────────────────────────┤
│ Shell          │ << DELIMITER ... DELIMITER         │
│ Perl           │ <<EOF ... EOF                      │
│ Ruby           │ <<~HEREDOC ... HEREDOC             │
│ Python         │ """ ... """ (docstring)            │
│ JavaScript     │ ` ... ` (template literal)         │
└────────────────┴────────────────────────────────────┘

これらはすべて「複数行リテラル」の異なる表現

レキシカル解析とヒアドキュメント

シェルのパーサーがヒアドキュメントを処理する方法は、コンパイラ理論の観点から興味深いです:

ヒアドキュメントのパース処理:

入力: cat << EOF
      Hello
      World
      EOF

レキサー(字句解析器)の動作:

1. "cat" → トークン: WORD
2. "<<" → トークン: HEREDOC_OP
3. "EOF" → トークン: DELIMITER
4. 改行 → モード切り替え: HEREDOC_BODY

ヒアドキュメント本体の収集:
┌────────────────────────────────────┐
│ 行を読み込み                       │
│ while (行 != DELIMITER)           │
│     本体に追加                     │
│ end while                         │
└────────────────────────────────────┘

5. "Hello\nWorld\n" → ヒアドキュメント本体
6. "EOF" → DELIMITER(終端)

ヒアドキュメントの変種

POSIXシェルでは、いくつかのヒアドキュメント変種が定義されています:

# 1. 基本形(展開あり)
cat << EOF
Variable is $VAR
Command output is $(date)
EOF

# 2. クォート形(展開なし)
cat << 'EOF'
Variable is $VAR (literally)
Command output is $(date) (literally)
EOF

# 3. タブストリップ形(<<-)
    cat <<- EOF
	Leading tabs are stripped
	EOF

# 4. ヒアストリング(<<<)- Bash拡張
cat <<< "Single line input"

5.3 一時ファイル vs パイプ:設計上のトレードオフ

実装アプローチの比較

ヒアドキュメントの実装には、主に2つのアプローチがあります:

アプローチ1: 一時ファイル方式

ユーザ入力
    │
    ↓
┌─────────────────┐
│ 一時ファイル作成 │ ← /tmp/heredoc_XXX
│ (write → close) │
└────────┬────────┘
         │
         ↓ open(O_RDONLY)
┌─────────────────┐
│ コマンドに渡す   │
│ (stdin = tmpfile)│
└─────────────────┘


アプローチ2: パイプ方式

ユーザ入力
    │
    ↓ fork()
┌─────────────────┐      ┌─────────────────┐
│ 親プロセス       │      │ 子プロセス       │
│ (入力を読む)     │ pipe │ (コマンド実行)   │
│ write(pipe[1])  │─────→│ stdin = pipe[0] │
└─────────────────┘      └─────────────────┘

トレードオフ分析

┌────────────────┬─────────────────────┬─────────────────────┐
│ 観点           │ 一時ファイル方式     │ パイプ方式           │
├────────────────┼─────────────────────┼─────────────────────┤
│ メモリ使用     │ 低(ディスクに保存)  │ 高(メモリ内バッファ)│
│ I/O速度        │ 遅(ディスクI/O)     │ 速(メモリI/O)      │
│ 大容量データ   │ 対応可能             │ バッファ制限あり     │
│ 実装複雑度     │ 低                   │ 高(fork必要)       │
│ セキュリティ   │ 低(ファイル残存)    │ 高(データ残らず)   │
│ シーク可能     │ 可能                 │ 不可能              │
└────────────────┴─────────────────────┴─────────────────────┘

実用的な選択:
- 小〜中規模データ: パイプ方式が効率的
- 大規模データ: 一時ファイル方式が安全
- セキュリティ重視: パイプ方式(データ残存なし)

POSIX一時ファイルの安全な使用

一時ファイル方式を使う場合、セキュリティ上の考慮が必要です:

/* 安全な一時ファイル作成のベストプラクティス */

/* 方法1: mkstemp() - 推奨 */
char template[] = "/tmp/heredoc_XXXXXX";
int fd = mkstemp(template);  // 安全なランダム名を生成
// 使用後即座にunlink()
unlink(template);  // ファイル名を削除(fdは有効)

/* 方法2: O_TMPFILE (Linux 3.11+) */
int fd = open("/tmp", O_TMPFILE | O_RDWR, 0600);
// ファイル名なしで直接fd取得

/* 危険な方法(避けるべき) */
// 固定名を使用 - 競合状態の脆弱性あり
open("/tmp/heredoc_tmp", O_CREAT | O_WRONLY, 0644);

5.4 I/Oリダイレクションの意味論

リダイレクション演算子の分類

POSIXシェルは複数のリダイレクション演算子を定義しています:

リダイレクション演算子の体系:

出力リダイレクション:
┌─────────┬────────────────────────────────────────┐
│ 演算子   │ 意味                                   │
├─────────┼────────────────────────────────────────┤
│ >       │ 上書き(O_WRONLY | O_CREAT | O_TRUNC)  │
│ >>      │ 追記(O_WRONLY | O_CREAT | O_APPEND)  │
│ >|      │ noclobber無視で上書き                  │
│ >&n     │ FD nへ複製                             │
│ &>      │ stdout と stderr を同じファイルへ      │
└─────────┴────────────────────────────────────────┘

入力リダイレクション:
┌─────────┬────────────────────────────────────────┐
│ 演算子   │ 意味                                   │
├─────────┼────────────────────────────────────────┤
│ <       │ ファイルから読み込み(O_RDONLY)        │
│ <<      │ ヒアドキュメント                        │
│ <<<     │ ヒアストリング(Bash拡張)              │
│ <&n     │ FD nから複製                           │
└─────────┴────────────────────────────────────────┘

Pipexにおける出力モードの違い

通常モード(>)vs ヒアドキュメントモード(>>):

通常のpipex:
./pipex infile cmd1 cmd2 outfile
                          ↓
           open(outfile, O_WRONLY | O_CREAT | O_TRUNC, 0644)
                          ↓
           既存の内容は消去される

here_doc pipex:
./pipex here_doc LIMITER cmd1 cmd2 outfile
                                    ↓
           open(outfile, O_WRONLY | O_CREAT | O_APPEND, 0644)
                                    ↓
           既存の内容の後に追記される

この違いの理由:

シェルでの等価コマンド:

通常: < infile cmd1 | cmd2 > outfile
      ファイルの内容を処理して新しいファイルを作成

heredoc: << LIMITER cmd1 | cmd2 >> outfile
         対話的入力を既存ファイルに追加

ヒアドキュメントは通常、ログやデータの追加に使用されるため
追記モード(>>)がデフォルトとなっている

5.5 ヒアドキュメントの実装

一時ファイル方式の実装

#include "pipex.h"

#define HEREDOC_TMP ".heredoc_tmp"

/* デリミタまで標準入力を読み込み、一時ファイルに保存 */
int handle_heredoc(char *limiter)
{
    int     tmp_fd;
    char    *line;
    size_t  limiter_len;

    /* 一時ファイルを作成 */
    tmp_fd = open(HEREDOC_TMP, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (tmp_fd == -1)
    {
        perror("heredoc: open");
        return (-1);
    }

    limiter_len = ft_strlen(limiter);

    /* プロンプトを表示 */
    ft_putstr_fd("heredoc> ", STDOUT_FILENO);

    while (1)
    {
        line = get_next_line(STDIN_FILENO);

        /* EOFの処理(Ctrl+D) */
        if (!line)
        {
            ft_putstr_fd("\nWarning: here-document delimited by ", 2);
            ft_putstr_fd("end-of-file (wanted `", 2);
            ft_putstr_fd(limiter, 2);
            ft_putendl_fd("')", 2);
            break;
        }

        /* デリミタをチェック */
        if (ft_strncmp(line, limiter, limiter_len) == 0 &&
            (line[limiter_len] == '\n' || line[limiter_len] == '\0'))
        {
            free(line);
            break;
        }

        /* 一時ファイルに書き込み */
        write(tmp_fd, line, ft_strlen(line));

        free(line);
        ft_putstr_fd("heredoc> ", STDOUT_FILENO);
    }

    close(tmp_fd);

    /* 一時ファイルを読み込みモードで再オープン */
    tmp_fd = open(HEREDOC_TMP, O_RDONLY);
    if (tmp_fd == -1)
    {
        perror("heredoc: reopen");
        unlink(HEREDOC_TMP);
        return (-1);
    }

    /* ファイル名を削除(FDは有効なまま) */
    unlink(HEREDOC_TMP);

    return (tmp_fd);
}

unlink()後もFDが有効な理由

UNIXファイルシステムの参照カウント機構:

unlink()の動作:
1. ディレクトリエントリを削除(ファイル名がなくなる)
2. ファイルへの参照カウントを減らす

ファイルが実際に削除されるのは:
参照カウント = 0 になった時のみ

┌─────────────────────────────────────────────────────────┐
│ 操作                        │ 参照カウント │ ファイル状態 │
├─────────────────────────────┼──────────────┼──────────────┤
│ open("file") → fd=3        │ 1            │ 存在         │
│ unlink("file")             │ 1            │ 存在(名前なし)│
│ 他プロセスからアクセス不可   │              │              │
│ close(fd=3)                │ 0            │ 削除         │
└─────────────────────────────┴──────────────┴──────────────┘

この仕組みにより:
- プログラム異常終了時も一時ファイルは自動削除
- 他プロセスからの読み取りを防止(セキュリティ)

パイプ方式の実装

int handle_heredoc_pipe(char *limiter, int *result_fd)
{
    int     pipe_fd[2];
    pid_t   pid;
    char    *line;
    size_t  limiter_len;

    /* パイプを作成 */
    if (pipe(pipe_fd) == -1)
    {
        perror("heredoc: pipe");
        return (-1);
    }

    pid = fork();
    if (pid == -1)
    {
        perror("heredoc: fork");
        close(pipe_fd[0]);
        close(pipe_fd[1]);
        return (-1);
    }

    if (pid == 0)
    {
        /* 子プロセス: 入力を読んでパイプに書き込む */
        close(pipe_fd[0]);  /* 読み込み側を閉じる */

        limiter_len = ft_strlen(limiter);
        ft_putstr_fd("heredoc> ", STDOUT_FILENO);

        while (1)
        {
            line = get_next_line(STDIN_FILENO);
            if (!line)
                break;

            if (ft_strncmp(line, limiter, limiter_len) == 0 &&
                (line[limiter_len] == '\n' || line[limiter_len] == '\0'))
            {
                free(line);
                break;
            }

            write(pipe_fd[1], line, ft_strlen(line));
            free(line);
            ft_putstr_fd("heredoc> ", STDOUT_FILENO);
        }

        close(pipe_fd[1]);
        exit(0);
    }
    else
    {
        /* 親プロセス */
        close(pipe_fd[1]);  /* 書き込み側を閉じる */
        waitpid(pid, NULL, 0);  /* 子の完了を待つ */
        *result_fd = pipe_fd[0];
        return (0);
    }
}

5.6 完全なヒアドキュメント対応Pipex

データ構造の拡張

typedef struct s_pipex_heredoc
{
    int     input_fd;      /* heredoc一時ファイル or 入力ファイル */
    int     outfile;       /* 出力ファイル */
    int     pipe_fd[2];    /* プロセス間パイプ */
    char    **envp;        /* 環境変数 */
    int     is_heredoc;    /* heredocモードフラグ */
}   t_pipex_heredoc;

引数解析

int parse_args_heredoc(int argc, char **argv, t_pipex_heredoc *data)
{
    /* here_docモードの判定 */
    if (argc == 6 && ft_strcmp(argv[1], "here_doc") == 0)
    {
        data->is_heredoc = 1;

        /* ヒアドキュメントから入力を収集 */
        data->input_fd = handle_heredoc(argv[2]);
        if (data->input_fd == -1)
            return (-1);

        /* 出力ファイルを追記モードで開く */
        data->outfile = open(argv[5],
                             O_WRONLY | O_CREAT | O_APPEND, 0644);
        if (data->outfile == -1)
        {
            perror(argv[5]);
            close(data->input_fd);
            return (-1);
        }

        return (0);
    }

    /* 通常モード */
    if (argc != 5)
    {
        ft_putstr_fd("Usage:\n", 2);
        ft_putstr_fd("  ./pipex file1 cmd1 cmd2 file2\n", 2);
        ft_putstr_fd("  ./pipex here_doc LIMITER cmd1 cmd2 file\n", 2);
        return (-1);
    }

    data->is_heredoc = 0;

    /* 入力ファイルを開く */
    data->input_fd = open(argv[1], O_RDONLY);
    if (data->input_fd == -1)
    {
        perror(argv[1]);
        return (-1);
    }

    /* 出力ファイルを上書きモードで開く */
    data->outfile = open(argv[4], O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (data->outfile == -1)
    {
        perror(argv[4]);
        close(data->input_fd);
        return (-1);
    }

    return (0);
}

メイン関数

int main(int argc, char **argv, char **envp)
{
    t_pipex_heredoc data;
    pid_t           pid1;
    pid_t           pid2;
    int             cmd1_idx;
    int             cmd2_idx;
    int             status;

    data.envp = envp;

    if (parse_args_heredoc(argc, argv, &data) == -1)
        return (1);

    /* コマンドのインデックスを設定 */
    cmd1_idx = data.is_heredoc ? 3 : 2;
    cmd2_idx = data.is_heredoc ? 4 : 3;

    /* パイプを作成 */
    if (pipe(data.pipe_fd) == -1)
    {
        perror("pipe");
        close(data.input_fd);
        close(data.outfile);
        return (1);
    }

    /* 第1子プロセス */
    pid1 = fork();
    if (pid1 == 0)
    {
        dup2(data.input_fd, STDIN_FILENO);
        dup2(data.pipe_fd[1], STDOUT_FILENO);
        close(data.pipe_fd[0]);
        close(data.pipe_fd[1]);
        close(data.input_fd);
        close(data.outfile);
        execute_cmd(argv[cmd1_idx], envp);
    }

    /* 第2子プロセス */
    pid2 = fork();
    if (pid2 == 0)
    {
        dup2(data.pipe_fd[0], STDIN_FILENO);
        dup2(data.outfile, STDOUT_FILENO);
        close(data.pipe_fd[0]);
        close(data.pipe_fd[1]);
        close(data.input_fd);
        close(data.outfile);
        execute_cmd(argv[cmd2_idx], envp);
    }

    /* 親プロセス: クリーンアップ */
    close(data.pipe_fd[0]);
    close(data.pipe_fd[1]);
    close(data.input_fd);
    close(data.outfile);

    waitpid(pid1, &status, 0);
    waitpid(pid2, &status, 0);

    return (WEXITSTATUS(status));
}

5.7 複数パイプの実装(ボーナス)

N個のコマンドパイプライン

任意個数のコマンドをパイプ接続:

./pipex infile cmd1 cmd2 cmd3 ... cmdN outfile

等価なシェルコマンド:
< infile cmd1 | cmd2 | cmd3 | ... | cmdN > outfile

必要なパイプ数: N - 1(コマンド数 - 1)

データフロー:
          pipe[0]    pipe[1]    pipe[2]
             │          │          │
infile ──→ cmd1 ──→ cmd2 ──→ cmd3 ──→ ... ──→ cmdN ──→ outfile

拡張データ構造

typedef struct s_multi_pipex
{
    int     infile;
    int     outfile;
    int     **pipes;       /* パイプの2次元配列 */
    int     pipe_count;    /* パイプの数 = cmd_count - 1 */
    int     cmd_count;     /* コマンドの数 */
    char    **cmds;        /* コマンド配列へのポインタ */
    char    **envp;
    int     is_heredoc;
}   t_multi_pipex;

パイプ配列の作成

int create_pipes(t_multi_pipex *data)
{
    int i;

    /* (コマンド数 - 1)個のパイプが必要 */
    data->pipe_count = data->cmd_count - 1;

    data->pipes = malloc(sizeof(int *) * data->pipe_count);
    if (!data->pipes)
        return (-1);

    for (i = 0; i < data->pipe_count; i++)
    {
        data->pipes[i] = malloc(sizeof(int) * 2);
        if (!data->pipes[i])
        {
            /* 確保済みのメモリを解放 */
            while (--i >= 0)
                free(data->pipes[i]);
            free(data->pipes);
            return (-1);
        }

        if (pipe(data->pipes[i]) == -1)
        {
            perror("pipe");
            /* パイプと配列を解放 */
            while (--i >= 0)
            {
                close(data->pipes[i][0]);
                close(data->pipes[i][1]);
                free(data->pipes[i]);
            }
            free(data->pipes[i]);
            free(data->pipes);
            return (-1);
        }
    }

    return (0);
}

リダイレクション設定

void    setup_redirections(t_multi_pipex *data, int index)
{
    /*
     * index = 0(最初のコマンド):
     *   stdin  ← infile
     *   stdout → pipes[0][1]
     *
     * index = N-1(最後のコマンド):
     *   stdin  ← pipes[N-2][0]
     *   stdout → outfile
     *
     * それ以外(中間):
     *   stdin  ← pipes[index-1][0]
     *   stdout → pipes[index][1]
     */

    if (index == 0)
    {
        /* 最初のコマンド */
        dup2(data->infile, STDIN_FILENO);
        dup2(data->pipes[0][1], STDOUT_FILENO);
    }
    else if (index == data->cmd_count - 1)
    {
        /* 最後のコマンド */
        dup2(data->pipes[index - 1][0], STDIN_FILENO);
        dup2(data->outfile, STDOUT_FILENO);
    }
    else
    {
        /* 中間のコマンド */
        dup2(data->pipes[index - 1][0], STDIN_FILENO);
        dup2(data->pipes[index][1], STDOUT_FILENO);
    }
}

パイプラインの実行

void    close_all_pipes(t_multi_pipex *data)
{
    int i;

    for (i = 0; i < data->pipe_count; i++)
    {
        close(data->pipes[i][0]);
        close(data->pipes[i][1]);
    }
}

void    free_pipes(t_multi_pipex *data)
{
    int i;

    for (i = 0; i < data->pipe_count; i++)
        free(data->pipes[i]);
    free(data->pipes);
}

int execute_pipeline(t_multi_pipex *data)
{
    pid_t   *pids;
    int     i;
    int     status;
    int     last_status;

    pids = malloc(sizeof(pid_t) * data->cmd_count);
    if (!pids)
        return (-1);

    /* 各コマンドのプロセスを作成 */
    for (i = 0; i < data->cmd_count; i++)
    {
        pids[i] = fork();

        if (pids[i] == -1)
        {
            perror("fork");
            free(pids);
            return (-1);
        }

        if (pids[i] == 0)
        {
            /* 子プロセス */
            setup_redirections(data, i);
            close_all_pipes(data);
            close(data->infile);
            close(data->outfile);

            execute_cmd(data->cmds[i], data->envp);
            exit(127);  /* execveが失敗した場合 */
        }
    }

    /* 親プロセス: すべてのFDを閉じる */
    close_all_pipes(data);
    close(data->infile);
    close(data->outfile);

    /* すべての子プロセスを待つ */
    last_status = 0;
    for (i = 0; i < data->cmd_count; i++)
    {
        waitpid(pids[i], &status, 0);
        if (i == data->cmd_count - 1)
            last_status = status;
    }

    free(pids);
    return (WEXITSTATUS(last_status));
}

5.8 終了ステータスの意味論

POSIX終了ステータス規約

終了ステータスの意味(POSIX.1-2017):

┌─────────────┬─────────────────────────────────────────┐
│ 終了コード   │ 意味                                    │
├─────────────┼─────────────────────────────────────────┤
│ 0           │ 成功                                    │
│ 1-125       │ コマンド固有のエラー                     │
│ 126         │ 実行権限なし(permission denied)        │
│ 127         │ コマンドが見つからない(not found)      │
│ 128+n       │ シグナルnで終了(128+SIGTERM = 143)    │
│ 255         │ 終了コードが範囲外                       │
└─────────────┴─────────────────────────────────────────┘

パイプラインの終了コード:
- デフォルト: 最後のコマンドの終了コード
- set -o pipefail: いずれかのコマンドがエラーなら非ゼロ

Pipexでの終了コード処理

/* シェルと同じ動作: 最後のコマンドの終了コードを返す */
int main(int argc, char **argv, char **envp)
{
    /* ... */

    /* 最後のコマンドの終了ステータスを取得 */
    waitpid(pid1, NULL, 0);
    waitpid(pid2, &status, 0);

    /* WEXITSTATUSマクロで終了コードを抽出 */
    if (WIFEXITED(status))
        return (WEXITSTATUS(status));
    else if (WIFSIGNALED(status))
        return (128 + WTERMSIG(status));
    else
        return (1);
}

5.9 まとめ

学習した理論

  • シェルの歴史
- Thompson Shell (1971) から Bash (1989) への進化 - Stephen Bourneによる言語としてのシェル設計 - ヒアドキュメントの導入(1977)

  • ヒアドキュメントの構文論
- レキシカル解析における特殊処理 - 変種(<<, <<-, <<<)の意味論 - 他言語の複数行リテラルとの対応

  • 実装のトレードオフ
- 一時ファイル vs パイプ方式 - unlink()とファイル参照カウント - セキュリティの考慮

実装のポイント

  • ヒアドキュメント
- デリミタの正確な検出 - EOF(Ctrl+D)の適切な処理 - 一時ファイルの安全な削除

  • 複数パイプ
- 動的なパイプ配列管理 - 位置に応じたリダイレクション設定 - 全FDの確実なクローズ

  • 終了ステータス
- POSIX規約に従った終了コード - 最後のコマンドのステータスを返す

次章では、テストとデバッグについて学びます。