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)を、以下リンク先に示す。
修正前後の比較
主な変更点
main()
関数
- コマンドの実行前に
handle_redirection()
関数を呼び出して、リダイレクションの記号がある場合に入出力先の切り替えを行う。
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_RDONLY
、O_WRONLY
、またはO_RDWR
これらは、それぞれファイルを読み取り専用、書き込み専用、または読み書き可能で開くことを示す。さらに、
flags
引数には、 ファイル作成フラグ や ファイル状態フラグ を 0個以上ビット単位のOR演算で組み合わせて指定する。 これらの2つのフラググループの違いは、ファイル作成フラグがopen
操作そのものの動作に影響を与えるのに対し、ファイル状態フラグはその後の入出力操作の動作に影響を与える点である。
O_APPEND
(ファイル状態フラグ)
ファイルを追記モードで開く。 このモードでは、各write(2)
呼び出しの前にファイルオフセットが自動的にファイルの末尾に移動する。O_CREAT
(ファイル作成フラグ)
指定されたパス名が存在しない場合、新しい通常ファイルを作成する。mode
引数は、新しいファイルが作成される際に適用されるファイルモードビットを指定する。 ただし、flags
にO_CREAT
またはO_TMPFILE
が指定されていない場合、mode
は無視される(そのため、0
を指定するか、単に省略しても問題ない)。
一方、flags
にO_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
にエラー内容が設定される。
built_in()
関数の修正
組み込みコマンドはシェルのプロセス内で実行されるため、 リダイレクションによる標準入力/出力/エラー出力の変更がシェル全体に影響を与える可能性がある。 これを防ぐには、以下の手順が必要となる:- 実行前に現在の標準入力/出力/エラー出力のファイル記述子をバックアップする。
manage_file_descriptors()
関数を第一引数 SAVE で呼び出す。 - リダイレクションを実行する。
handle_redirection()
関数を呼び出す。 - 組み込みコマンドを実行する。
- 実行後、バックアップしたファイル記述子を復元する。
manage_file_descriptors()
関数を第一引数 RESTORE で呼び出す。
- 実行前に現在の標準入力/出力/エラー出力のファイル記述子をバックアップする。
manage_file_descriptors()
関数を追加- 第一引数が SAVE の場合は、
dup()
システムコールを使って、標準入力/出力/エラー出力のファイル記述子をバックアップする。 - 第一引数が RESTORE の場合は、
dup2()
システムコールを使って、バックアップしていた標準入力/出力/エラー出力のファイル記述子を元に戻す。 復元後は、バックアップ用のファイル記述子をclose()
システムコールを使って全てクローズする。
- 第一引数が SAVE の場合は、
使用方法
- 上記のコードをファイルに保存(例えば
myshell.c
)する。 - コンパイルする:
1gcc myshell.c -o myshell
- 実行する:
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
を使うことで標準エラー出力への出力もファイルに吐き出される。
期待通りの結果だ。