第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 まとめ
学習した理論
- シェルの歴史
- ヒアドキュメントの構文論
- 実装のトレードオフ
実装のポイント
- ヒアドキュメント
- 複数パイプ
- 終了ステータス
次章では、テストとデバッグについて学びます。