Linuxで簡単なシェルを自作してみた(6)|リダイレクション

Linuxで簡単なシェルを自作してみた(5)|バックグラウンド実行のつづき。

リダイレクション(入力リダイレクション、出力リダイレクション)の機能をシェルに追加することで、ユーザーがコマンドの入力や出力をファイルにリダイレクトできるようになる。

以下の代表的なリダイレクション機能を追加する。

  • > : コマンドの標準出力を指定したファイルに書き込む。
  • < : コマンドの標準入力を指定したファイルから読み込む。
  • >> : コマンドの標準出力を指定したファイルに追記する。
  • 2>&1 : 標準エラー出力を標準出力にリダイレクトし、両方の出力を同じファイルに送る。

修正したシェルのソースコード

  1#include <stdio.h>
  2#include <stdlib.h>
  3#include <string.h>
  4#include <unistd.h>   // fork, execvp, dup2
  5#include <sys/wait.h> // waitpid
  6#include <glob.h>     // glob
  7#include <errno.h>    // errno
  8#include <limits.h>   // PATH_MAX
  9#include <fcntl.h>    // open
 10
 11#define MAX_CMD_LEN 1024
 12#define MAX_ARGS 100
 13
 14// 組み込みコマンド関連の宣言
 15int cd (char *argv[]);
 16int pwd(char *argv[]);
 17struct builtin {
 18    char cmd[256];              // コマンド名
 19    int (*func)(char *argv[]);  // 関数へのポインタ
 20};
 21struct builtin blt_in[] = {{"cd",cd}, {"pwd",pwd}, {"",0}};
 22
 23// リダイレクション関連の宣言
 24void handle_redirection(char *argv[]);
 25enum Action { SAVE, RESTORE };
 26typedef enum { // リダイレクションのタイプを表す列挙型
 27    REDIR_INPUT,
 28    REDIR_OUTPUT,
 29    REDIR_APPEND,
 30    REDIR_MERGE_STDERR
 31} RedirectionType;
 32typedef struct { // リダイレクションの設定を格納する構造体
 33    const char *symbol;      // リダイレクション記号
 34    RedirectionType type;    // リダイレクションのタイプ
 35    int target_fd;           // 対象のファイル記述子
 36    int flags;               // ファイルオープンフラグ
 37} RedirectionRule;
 38static const RedirectionRule redirection_rules[] = { // リダイレクションルールの定義
 39    {"<",    REDIR_INPUT,  STDIN_FILENO,  O_RDONLY},
 40    {">",    REDIR_OUTPUT, STDOUT_FILENO, O_WRONLY | O_CREAT | O_TRUNC},
 41    {">>",   REDIR_APPEND, STDOUT_FILENO, O_WRONLY | O_CREAT | O_APPEND},
 42    {"2>&1", REDIR_MERGE_STDERR, STDERR_FILENO, 0} // ファイルフラグ不要
 43};
 44
 45// コマンドをスペースで分割する関数
 46void parse_command(char *cmd, char *argv[]) {
 47    char *token = strtok(cmd, " \n");
 48    int i = 0;
 49    while (token != NULL) {
 50        argv[i++] = token;
 51        token = strtok(NULL, " \n");
 52    }
 53    argv[i] = NULL; // 最後の引数をNULLで終わらせる
 54}
 55
 56// 引用符を削除する関数
 57void remove_quotes(char *str) {
 58    char *src = str, *dst = str;
 59    while (*src) {
 60        if (*src == '"' || *src == '\'') {
 61            src++;  // 引用符をスキップ
 62        } else {
 63            *dst++ = *src++;
 64        }
 65    }
 66    *dst = '\0';  // 文字列の終端を追加
 67}
 68
 69// 引数リストにパス名展開を適用する関数
 70int expand_arguments(char *argv[], char **expanded_argv) {
 71    glob_t glob_result;
 72    int argc = 0; // 展開後の引数数
 73
 74    for (int i = 0; argv[i] != NULL; i++) {
 75        // 引用符が含まれている場合は展開しない
 76        if ((strchr(argv[i], '*') || strchr(argv[i], '?') || strchr(argv[i], '[')) &&
 77            (argv[i][0] != '"' && argv[i][0] != '\'')) { // 引用符外でパス名展開
 78            if (glob(argv[i], GLOB_NOCHECK | GLOB_TILDE, NULL, &glob_result) != 0) {
 79                perror("glob failed");
 80                return -1;
 81            }
 82            // 展開された結果を追加
 83            for (size_t j = 0; j < glob_result.gl_pathc; j++) {
 84                if (argc >= MAX_ARGS - 1) {
 85                    fprintf(stderr, "Too many arguments after glob expansion\n");
 86                    globfree(&glob_result);
 87                    return -1;
 88                }
 89                expanded_argv[argc++] = strdup(glob_result.gl_pathv[j]);
 90            }
 91            globfree(&glob_result); // メモリを解放
 92        } else {
 93            // 引用符を取り除く
 94            remove_quotes(argv[i]);
 95
 96            // パス名展開が不要な場合
 97            if (argc >= MAX_ARGS - 1) {
 98                fprintf(stderr, "Too many arguments\n");
 99                return -1;
100            }
101            expanded_argv[argc++] = strdup(argv[i]);
102        }
103    }
104    expanded_argv[argc] = NULL; // 最後にNULLを追加
105    return argc;
106}
107
108// ファイル記述子の保存または復元を行う関数
109void manage_file_descriptors(enum Action action, int *saved_fds) {
110    if (action == SAVE) {
111        // 現在の標準入力、標準出力、標準エラー出力を保存
112        saved_fds[0] = dup(STDIN_FILENO);
113        saved_fds[1] = dup(STDOUT_FILENO);
114        saved_fds[2] = dup(STDERR_FILENO);
115        if (saved_fds[0] == -1 || saved_fds[1] == -1 || saved_fds[2] == -1) {
116            perror("Failed to save file descriptors");
117            exit(1);
118        }
119    } else if (action == RESTORE) {
120        // 保存されたファイル記述子を復元
121        if (dup2(saved_fds[0], STDIN_FILENO) == -1 ||
122            dup2(saved_fds[1], STDOUT_FILENO) == -1 ||
123            dup2(saved_fds[2], STDERR_FILENO) == -1) {
124            perror("Failed to restore file descriptors");
125            exit(1);
126        }
127        // 保存したファイル記述子を閉じる
128        close(saved_fds[0]);
129        close(saved_fds[1]);
130        close(saved_fds[2]);
131    }
132}
133
134// 組み込みコマンドの判定・実行
135int built_in(char *argv[], int bg){
136    struct builtin *p;
137    int saved_fds[3];
138    // 組み込みコマンド配列(blt_in)の走査
139    for (p = blt_in; *p->cmd != '\0'; p++) {
140        // 組み込みコマンドでない場合は次をチェック
141        if (strcmp(p->cmd, argv[0]) != 0) continue;
142        // 標準入力/出力/エラーのファイル記述子をバックアップ
143        manage_file_descriptors(SAVE, saved_fds);
144        // リダイレクションの処理
145        handle_redirection(argv);
146        // バックグラウンド実行の場合
147        if (bg) {
148            pid_t pid = fork();
149            if (pid == -1) {
150                perror("fork failed");
151                exit(1);
152            }
153            // 子プロセス
154            if (pid == 0) exit ((p->func)(argv)); // 組み込みコマンドの関数を呼び出し
155            // 親プロセス
156            fprintf(stderr, "Background process started with PID: %d\n", pid);
157            // ファイル記述子をバックアップから復元
158            manage_file_descriptors(RESTORE, saved_fds);
159            return 0;
160        }
161        // バックグラウンド実行でない場合
162        // 組み込みコマンドの関数を呼び出し
163        (p->func)(argv);
164        // ファイル記述子をバックアップから復元
165        manage_file_descriptors(RESTORE, saved_fds);
166        return 0;
167    }
168    return -1;
169}
170
171// cd コマンド
172int cd(char *argv[]) {
173    if (argv[1] == NULL) {
174        // 引数なしの場合はホームディレクトリに移動
175        const char *home_dir = getenv("HOME");
176        if (home_dir == NULL) {
177            perror("cd: HOME not set");
178            return -1;
179        } else {
180            if (chdir(home_dir) != 0) {
181                perror("cd failed");
182                return -1;
183            }
184        }
185    } else {
186        // 引数が指定された場合、そのディレクトリに移動
187        if (chdir(argv[1]) != 0) {
188            perror("cd failed");
189            return -1;
190        }
191    }
192    return 0;
193}
194
195// pwd コマンド
196int pwd(char *argv[]){
197    char buf[PATH_MAX+1];
198    if (getcwd(buf, sizeof(buf)) == NULL) {
199        perror("getcwd failed");
200        return -1;
201    }
202    printf("%s\n", buf);
203    return 0;
204}
205
206// バックグラウンド実行判定
207// コマンドの末尾が '&' かどうかを確認し、'&' を取り除いて戻り値として返す
208int is_background_command(char *argv[]) {
209    int i = 0;
210    while (argv[i] != NULL) i++;
211    if (i > 0 && strcmp(argv[i - 1], "&") == 0) {
212        argv[i - 1] = NULL; // '&' をコマンド引数から削除
213        return 1;
214    }
215    return 0;
216}
217
218// リダイレクションを処理する関数
219void handle_redirection(char *argv[]) {
220    int i = 0;
221
222    while (argv[i] != NULL) {
223        const RedirectionRule *rule = NULL; // 該当するリダイレクションルール
224        char *redirect_file = NULL;
225
226        // リダイレクション記号を検索
227        for (size_t j = 0; j < sizeof(redirection_rules) / sizeof(redirection_rules[0]); j++) {
228            if (strcmp(argv[i], redirection_rules[j].symbol) == 0) {
229                rule = &redirection_rules[j];
230                break;
231            }
232        }
233
234        if (rule) {
235            // リダイレクション記号が見つかった場合の処理
236            switch (rule->type) {
237                case REDIR_INPUT:
238                case REDIR_OUTPUT:
239                case REDIR_APPEND:
240                    // リダイレクト先のファイル名を取得
241                    redirect_file = argv[i + 1];
242                    if (!redirect_file) {
243                        fprintf(stderr, "Syntax error: No file specified for redirection\n");
244                        exit(1);
245                    }
246
247                    // ファイルを開き、リダイレクションを設定
248                    int fd = open(redirect_file, rule->flags, 0644);
249                    if (fd == -1) {
250                        perror("Redirection failed");
251                        exit(1);
252                    }
253                    if (dup2(fd, rule->target_fd) == -1) {
254                        perror("dup2 failed");
255                        close(fd);
256                        exit(1);
257                    }
258                    close(fd);
259                    break;
260
261                case REDIR_MERGE_STDERR:
262                    // 標準エラー出力を標準出力にリダイレクト
263                    if (dup2(STDOUT_FILENO, STDERR_FILENO) == -1) {
264                        perror("dup2 failed");
265                        exit(1);
266                    }
267                    break;
268            }
269
270            // リダイレクション記号と関連引数を削除
271            argv[i] = NULL;
272            if (redirect_file) {
273                argv[i + 1] = NULL;
274                i++; // ファイル名をスキップ
275            }
276        }
277        i++;
278    }
279}
280
281int main() {
282    char cmd[MAX_CMD_LEN]; // コマンド入力用のバッファ
283    char *argv[MAX_ARGS];  // コマンド引数
284    char *expanded_argv[MAX_ARGS]; // 展開後の引数
285    pid_t pid, wpid;
286    int status, bg;
287
288    // シェルのメインループ
289    while (1) {
290        // ゾンビを消滅させる
291        while ((wpid = waitpid(-1, &status, WNOHANG)) > 0)
292            fprintf(stderr, "process %d done\n", wpid);
293
294        // プロンプトを表示
295        fprintf(stderr, "mysh> ");
296
297        // コマンドを読み込む
298        if (fgets(cmd, sizeof(cmd), stdin) == NULL) {
299            if (feof(stdin)) {
300                break; // EOFが入力された場合、終了
301            }
302            perror("fgets failed");
303            continue;
304        }
305
306        // コマンドを解析
307        parse_command(cmd, argv);
308
309        // 空のコマンドの場合は次の入力へスキップ
310        if (argv[0] == NULL) {
311            continue;
312        }
313
314        // 終了コマンドの場合
315        if (strcmp(argv[0], "exit") == 0) {
316            break;
317        }
318
319        // バックグラウンド実行判定
320        bg = is_background_command(argv);
321
322        // 引数を展開
323        if (expand_arguments(argv, expanded_argv) == -1) {
324            fprintf(stderr, "Argument expansion failed\n");
325            continue;
326        }
327
328        // 組み込みコマンド判定・実行
329        if (built_in(expanded_argv, bg) == 0) {
330            continue;
331        }
332
333        // 子プロセスを作成してコマンドを実行
334        pid = fork();
335        if (pid == -1) {
336            perror("fork failed");
337            exit(1);
338        }
339
340        // 子プロセス
341        if (pid == 0) {
342            // リダイレクションの処理
343            handle_redirection(expanded_argv);
344
345            // execvpを使用してコマンドを実行
346            if (execvp(expanded_argv[0], expanded_argv) == -1) {
347                perror("exec failed");
348                exit(1);
349            }
350        }
351
352        // 親プロセス
353        while (1) {
354            // バックグラウンド実行の場合
355            if (bg) {
356                fprintf(stderr, "Background process started with PID: %d\n", pid);
357                break; // バックグラウンド実行の場合は待機しない
358            }
359            // バックグラウンド実行でない場合
360            if (waitpid(pid, &status, 0) == (pid_t)-1) { // 子プロセスの終了を待つ
361                if (errno == EINTR) {
362                    continue; // 割り込み発生時はリトライする
363                } else {
364                    perror("waitpid failed");
365                    exit(1);
366                }
367            }
368            break;
369        }
370
371        // メモリを解放
372        for (int i = 0; expanded_argv[i] != NULL; i++) {
373            free(expanded_argv[i]);
374        }
375    }
376
377    printf("Exiting shell...\n");
378    return 0;
379}

Linuxで簡単なシェルを自作してみた(5)|バックグラウンド実行のソースコードと 今回修正したソースコードの差異(diff)を、以下リンク先に示す。
修正前後の比較

主な変更点

  1. main() 関数
  • コマンドの実行前に handle_redirection() 関数を呼び出して、リダイレクションの記号がある場合に入出力先の切り替えを行う。
  1. handle_redirection() 関数の追加
    • 引数として渡されたコマンドにリダイレクション記号(<, >, >>, 2>&1)が含まれているかをチェックする。
    • リダイレクション記号が含まれていて、その記号が <, >, >> のいずれかであった場合
      • リダイレクト先のファイル名を取得してファイルオープンする。 ファイル記述子を扱うため open() システムコールを使う。
      • dup2() システムコールを使って、オープンしたファイルのファイル記述子を、 リダイレクション記号に応じた標準入力/出力/エラー出力のファイル記述子にコピーする(リダイレクション)。 これにより、標準入力/出力/エラー出力の対象が端末からオープンしたファイルに変わる。
      • リダイレクト後は、オープンしたファイルのファイル記述子はお役御免なので、 close() システムコールでクローズする。
    • リダイレクション記号が含まれていて、その記号が 2>&1 の場合
      • 標準出力のファイル記述子を標準エラー出力のファイル記述子にコピーする。 これにより、標準エラー出力への出力は、標準出力の出力先と同じになる。
    • 以上でリダイレクションの処理は完了のため、後のコマンド実行時に邪魔になるリダイレクション記号やファイル名をコマンド引数配列から削除する。

open(2) リファレンスより抜粋)
#include <fcntl.h>
int open(const char *pathname, int flags, ... /* mode_t mode */ );

open() システムコールは、指定されたパス名のファイルを開く。 open() の戻り値はファイル記述子と呼ばれる小さな非負整数で、 プロセス内で管理されている「オープンファイル記述子テーブル」のエントリを指すインデックスとなる。 このファイル記述子を使うことで、read(2)write(2)lseek(2)fcntl(2) などの システムコールを通じて開かれたファイルを操作する。 呼び出しが成功した場合、返されるファイル記述子は、 そのプロセスで現在未使用の中で最も小さい番号が割り当てられる。

open() の呼び出しにより、新しい「オープンファイル記述(open file description)」を作成する。 これはシステム全体で管理されるオープンファイルのテーブルに記録されるエントリの一つ。 このオープンファイル記述には、ファイルオフセットやファイル状態フラグ(後述)が 記録される。 一方、ファイル記述子はオープンファイル記述への参照であり、 その参照は指定したパス名が後から削除されたり、 別のファイルを指すように変更された場合でも影響を受けない。

flags 引数には、次のいずれかの アクセスモード を指定する必要がある:
O_RDONLYO_WRONLY、または O_RDWR
これらは、それぞれファイルを読み取り専用、書き込み専用、または読み書き可能で開くことを示す。

さらに、flags 引数には、 ファイル作成フラグファイル状態フラグ を 0個以上ビット単位のOR演算で組み合わせて指定する。 これらの2つのフラググループの違いは、ファイル作成フラグが open 操作そのものの動作に影響を与えるのに対し、ファイル状態フラグはその後の入出力操作の動作に影響を与える点である。

  • O_APPEND (ファイル状態フラグ)
    ファイルを追記モードで開く。 このモードでは、各 write(2) 呼び出しの前にファイルオフセットが自動的にファイルの末尾に移動する。
  • O_CREAT (ファイル作成フラグ)
    指定されたパス名が存在しない場合、新しい通常ファイルを作成する。 mode 引数は、新しいファイルが作成される際に適用されるファイルモードビットを指定する。 ただし、flagsO_CREAT または O_TMPFILE が指定されていない場合、 mode は無視される(そのため、0 を指定するか、単に省略しても問題ない)。
    一方、flagsO_CREAT または O_TMPFILE が指定されている場合、 mode 引数は必須。
  • O_TRUNC (ファイル作成フラグ)
    指定されたファイルが既に存在し、通常ファイルであり、 アクセスモードが書き込み可能(O_RDWR または O_WRONLY)である場合、 そのファイルの内容は長さ0に切り詰められる(中身が削除される)。

dup(2) リファレンスより抜粋)
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);

dup() システムコールは、指定されたファイル記述子 oldfd と同じオープンファイル記述を参照する新しいファイル記述子を割り当てる(オープンファイル記述については open(2) を参照)。
新しいファイル記述子の番号は、呼び出し元プロセスで未使用の中から最も小さい番号が割り当てられる。

dup2() システムコールは、dup() と同様の処理を行うが、 未使用の最小のファイル記述子を割り当てる代わりに、 指定されたファイル記述子番号 newfd を使用する。 つまり、newfd は調整され、oldfd と同じオープンファイル記述を参照するようになる。
もし newfd がすでにオープンされていた場合、そのファイル記述子は再利用される前に自動的に閉じられる。

戻り値
これらのシステムコールが成功した場合、新しいファイル記述子が返される。 エラーが発生した場合は -1 が返され、errno にエラー内容が設定される。

close(2) リファレンスより抜粋)
#include <unistd.h>
int close(int fd);

close() はファイル記述子を閉じ、その記述子がファイルを参照しない状態にする。これにより、閉じられたファイル記述子は再利用可能になる。

戻り値
これらのシステムコールが成功した場合、新しいファイル記述子が返される。 エラーが発生した場合は -1 が返され、errno にエラー内容が設定される。

  1. built_in() 関数の修正
    組み込みコマンドはシェルのプロセス内で実行されるため、 リダイレクションによる標準入力/出力/エラー出力の変更がシェル全体に影響を与える可能性がある。 これを防ぐには、以下の手順が必要となる:

    • 実行前に現在の標準入力/出力/エラー出力のファイル記述子をバックアップする。 manage_file_descriptors() 関数を第一引数 SAVE で呼び出す。
    • リダイレクションを実行する。 handle_redirection() 関数を呼び出す。
    • 組み込みコマンドを実行する。
    • 実行後、バックアップしたファイル記述子を復元する。 manage_file_descriptors() 関数を第一引数 RESTORE で呼び出す。
  2. manage_file_descriptors() 関数を追加

    • 第一引数が SAVE の場合は、 dup() システムコールを使って、標準入力/出力/エラー出力のファイル記述子をバックアップする。
    • 第一引数が RESTORE の場合は、 dup2() システムコールを使って、バックアップしていた標準入力/出力/エラー出力のファイル記述子を元に戻す。 復元後は、バックアップ用のファイル記述子を close() システムコールを使って全てクローズする。

使用方法

  1. 上記のコードをファイルに保存(例えば myshell.c)する。
  2. コンパイルする:
    1gcc myshell.c -o myshell
    
  3. 実行する:
    1./myshell
    

サンプル実行

サンプル1
 1mysh> cat input.txt
 2This sentence is in the input.txt file.
 3
 4mysh> cat output.txt
 5cat: output.txt: No such file or directory
 6
 7mysh> cat log.txt
 8This sentence is in the log.txt file.
 9
10mysh> cat < input.txt > output.txt >> log.txt
11
12mysh> cat input.txt
13This sentence is in the input.txt file.
14
15mysh> cat output.txt
16
17mysh> cat log.txt
18This sentence is in the log.txt file.
19This sentence is in the input.txt file.
20
21mysh> 

リダイレクションを含むコマンドの実行前後で、各ファイルの内容を出力して比較する。 output.txt が作成されているが、右側の >> により標準出力が log.txt にリダイレクトされるので、output.txt には何も吐かれていない。 期待通りの結果だ。

サンプル2
 1mysh> ls exist_file not_exist_file
 2ls: not_exist_file: No such file or directory
 3exist_file
 4
 5mysh> ls exist_file not_exist_file > output.txt
 6ls: not_exist_file: No such file or directory
 7
 8mysh> cat output.txt
 9exist_file
10
11mysh> ls exist_file not_exist_file > output.txt 2>&1
12
13mysh> cat output.txt
14ls: not_exist_file: No such file or directory
15exist_file
16
17mysh>

exist_file は存在し、not_exist_file は存在しない。 この二つのファイルに対して ls を実行すると、exist_file は標準出力へ出力され、 not_exist_file に対しては存在しない旨のメッセージが標準エラー出力に出力される。
> を使うと、標準出力への出力分だけがファイルに残るが、併せて 2>&1 を使うことで標準エラー出力への出力もファイルに吐き出される。 期待通りの結果だ。

関連ページ