Linuxで簡単なシェルを自作してみた|外部コマンド、リダイレクション、パイプラインの実装方法
目次
自作したLinuxシェルのソースコードを通じて、シェルの基本的な動作から、外部コマンドの実行、バックグラウンド処理、リダイレクション、パイプラインの実装方法までを説明する。
基本的なシェル(外部コマンドの実行)
基本的なシェルのソースコードは、シェルとしてコマンドラインを解釈し、ユーザーから入力されたコマンドを実行する簡単なプログラムである。ここでは、C 言語を使った簡単なシェルの例を示す。
このシェルは、ユーザーからコマンドを読み込み、システムの fork() と exec() を使ってそのコマンドを実行する。
基本的なシェルのソースコード (C言語)
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <unistd.h>
5#include <sys/wait.h>
6
7#define MAX_CMD_LEN 1024
8#define MAX_ARGS 100
9
10// コマンドをスペースで分割する関数
11void parse_command(char *cmd, char **args) {
12 char *token = strtok(cmd, " \n");
13 int i = 0;
14 while (token != NULL) {
15 args[i++] = token;
16 token = strtok(NULL, " \n");
17 }
18 args[i] = NULL; // 最後の引数をNULLで終わらせる
19}
20
21int main() {
22 char cmd[MAX_CMD_LEN]; // コマンド入力用のバッファ
23 char *args[MAX_ARGS]; // コマンド引数
24 pid_t pid;
25 int status;
26
27 // シェルのメインループ
28 while (1) {
29 // プロンプトを表示
30 printf("mysh> ");
31 fflush(stdout);
32
33 // コマンドを読み込む
34 if (fgets(cmd, sizeof(cmd), stdin) == NULL) {
35 if (feof(stdin)) {
36 break; // EOFが入力された場合、終了
37 }
38 perror("fgets failed");
39 continue;
40 }
41
42 // コマンドを解析
43 parse_command(cmd, args);
44
45 // 終了コマンドの場合
46 if (args[0] != NULL && strcmp(args[0], "exit") == 0) {
47 break;
48 }
49
50 // 子プロセスを作成してコマンドを実行
51 pid = fork();
52 if (pid == -1) {
53 perror("fork failed");
54 exit(1);
55 }
56
57 if (pid == 0) { // 子プロセス
58 // execvpを使用してコマンドを実行
59 if (execvp(args[0], args) == -1) {
60 perror("exec failed");
61 exit(1);
62 }
63 } else { // 親プロセス
64 waitpid(pid, &status, 0); // 子プロセスの終了を待つ
65 }
66 }
67
68 printf("Exiting shell...\n");
69 return 0;
70}
説明
コマンドの入力と解析
fgets()
関数を使ってユーザーの入力を受け取り、その後、strtok()
関数でコマンドをスペースで分割して引数に変換する。fork()
とexecvp()
fork()
を使って新しいプロセス(子プロセス)を作成する。子プロセス内でexecvp()
を使って実際のコマンドを実行する。execvp()
は、指定されたコマンドをシステムで実行するための関数で、引数リストを渡すことができる。waitpid()
親プロセスはwaitpid()
を使って子プロセスが終了するのを待つ。exit
コマンドexit
コマンドが入力された場合、シェルは終了する。
使用方法
- 上記のコードをファイルに保存(例えば
myshell.c
)する。 - コンパイルする:
1gcc myshell.c -o myshell
- 実行する:
1./myshell
サンプル実行
1mysh> ls
2Desktop Documents Downloads myshell.c
3mysh> echo Hello, world!
4Hello, world!
5mysh> exit
6Exiting shell...
このシェルは、シンプルですが、基本的なコマンド実行の仕組みを理解するのに役立つ。
バックグラウンド実行の機能を追加
バックグラウンド実行の機能を追加するには、ユーザーがコマンドの最後に &
を入力した場合、そのコマンドをバックグラウンドで実行するようにシェルを変更する。
バックグラウンドで実行する際、シェルは新しいプロセスを生成し、そのプロセスが終了するのを待機せずに次のコマンドを受け付けるようにする。
ただしバックグラウンドプロセスが終了したときに親プロセスが waitpid()
を呼び出す必要はある(放置したままだとゾンビプロセスとして残ってしまうため)。
SIGCHLD
シグナルをハンドリングして、親プロセスがバックグラウンドプロセスの終了の通知を受け取り、waitpid()
を呼び出すようにする。
バックグラウンド実行をサポートするために、シェルに以下の変更を加える:
- コマンドの末尾に
&
があるかどうかを確認。 - バックグラウンド実行したプロセスから
SIGCHLD
シグナルを受け取り、バックグラウンドプロセスに対してwaitpid()
を実行して消滅させる。 &
があれば、プロセスをバックグラウンドで実行し、親プロセスはその終了を待たずに次のコマンドを実行可能とする。&
がなければ、通常通りフォアグラウンドで実行し、終了を待つ。
修正したシェルのコード
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <unistd.h>
5#include <sys/wait.h>
6#include <signal.h>
7
8#define MAX_CMD_LEN 1024
9#define MAX_ARGS 100
10
11// コマンドをスペースで分割する関数
12void parse_command(char *cmd, char **args) {
13 char *token = strtok(cmd, " \n");
14 int i = 0;
15 while (token != NULL) {
16 args[i++] = token;
17 token = strtok(NULL, " \n");
18 }
19 args[i] = NULL; // 最後の引数をNULLで終わらせる
20}
21
22// コマンドの末尾が '&' かどうかを確認し、'&' を取り除いて戻り値として返す
23int is_background_command(char **args) {
24 int i = 0;
25 while (args[i] != NULL) {
26 i++;
27 }
28 if (i > 0 && strcmp(args[i - 1], "&") == 0) {
29 args[i - 1] = NULL; // '&' をコマンド引数から削除
30 return 1; // バックグラウンド実行フラグを返す
31 }
32 return 0;
33}
34
35// SIGCHLD シグナルハンドラ(バックグラウンドプロセスの終了を処理)
36void sigchld_handler(int signo) {
37 int status;
38 // 終了した子プロセスの状態を取得
39 while (waitpid(-1, &status, WNOHANG) > 0) {
40 // 子プロセスがシグナルで終了した場合はそのシグナル番号を出力
41 if (WIFSIGNALED(status)) {
42 printf("Child process terminated by signal %d\n", WTERMSIG(status));
43 }
44 }
45}
46
47int main() {
48 char cmd[MAX_CMD_LEN]; // コマンド入力用のバッファ
49 char *args[MAX_ARGS]; // コマンド引数
50 pid_t pid;
51 int status;
52
53 // SIGCHLD シグナルのハンドラを設定
54 signal(SIGCHLD, sigchld_handler);
55
56 // シェルのメインループ
57 while (1) {
58 // プロンプトを表示
59 printf("mysh> ");
60 fflush(stdout);
61
62 // コマンドを読み込む
63 if (fgets(cmd, sizeof(cmd), stdin) == NULL) {
64 if (feof(stdin)) {
65 break; // EOFが入力された場合、終了
66 }
67 perror("fgets failed");
68 continue;
69 }
70
71 // コマンドを解析
72 parse_command(cmd, args);
73
74 // 空のコマンドの場合は次の入力へスキップ
75 if (args[0] == NULL) {
76 continue; // 空コマンドは何もせず次に進む
77 }
78
79 // 終了コマンドの場合
80 if (args[0] != NULL && strcmp(args[0], "exit") == 0) {
81 break;
82 }
83
84 // バックグラウンド実行かどうかを判定
85 int background = is_background_command(args);
86
87 // 子プロセスを作成してコマンドを実行
88 pid = fork();
89 if (pid == -1) {
90 perror("fork failed");
91 exit(1);
92 }
93
94 if (pid == 0) { // 子プロセス
95 // execvpを使用してコマンドを実行
96 if (execvp(args[0], args) == -1) {
97 perror("exec failed");
98 exit(1);
99 }
100 } else { // 親プロセス
101 if (!background) {
102 // バックグラウンドでなければ、親は子プロセスの終了を待つ
103 waitpid(pid, &status, 0);
104 } else {
105 // バックグラウンドの場合、親は終了を待たずに次のコマンドを受け付ける
106 printf("Background process started with PID: %d\n", pid);
107 }
108 }
109 }
110
111 printf("Exiting shell...\n");
112 return 0;
113}
主な変更点
バックグラウンド実行の判定
is_background_command()
関数を追加し、コマンドの最後に&
があるかどうかを確認する。&
があれば、その部分を削除し、コマンドをバックグラウンドで実行するフラグを返す。**
SIGCHLD
シグナルハンドラの追加signal()
関数を追加し、SIGCHLD
シグナルを受け取ったらsigchld_handler
関数(シグナルハンドラ)を呼び出す。シグナルハンドラでは子プロセス(バックグラウンドプロセス)に対してwaitpid()
を実行して、子プロセスを消滅させる。バックグラウンド実行の処理 バックグラウンドで実行する場合、親プロセスは
waitpid()
を呼ばず、すぐに次のコマンドを受け付ける。また、バックグラウンドで実行されるプロセスの PID を表示する。バックグラウンド実行の判定 コマンドがバックグラウンドで実行される場合(
&
がコマンドの最後にある場合)は、親プロセスはwaitpid()
を使わずに、次のコマンドをすぐに受け付ける。
使用方法
- 上記のコードをファイルに保存(例えば
myshell.c
)する。 - コンパイルする:
1gcc myshell.c -o myshell
- 実行する:
1./myshell
サンプル実行
1mysh> ls
2Desktop Documents Downloads myshell.c
3mysh> sleep 5 &
4Background process started with PID: 12345
5mysh> echo Hello, world!
6Hello, world!
7mysh> exit
8Exiting shell...
上記の例では、sleep 5 &
をバックグラウンドで実行し、その後、すぐに次のコマンド(echo Hello, world!
)を実行することができる。
バックグラウンドプロセスが終了するのをシェルが待つことなく、シェルは次のコマンドを受け付けることができる。
組み込みコマンド cd を追加
組み込みコマンド cd
の機能をシェルに追加するには、cd
コマンドが呼ばれたときに、プロセスを新たにフォークして実行するのではなく、シェル自身のプロセスでディレクトリを変更する必要がある。
このためには、chdir()
システムコールを使用してカレントディレクトリを変更する。
cd
コマンドの特徴
cd
コマンドは、シェルプロセス内で実行され、シェルのカレントディレクトリを変更する。新たなプロセスを作成して実行するのではなく、現在実行中のシェルのプロセスでディレクトリ変更を行う。- 引数に指定されたパスに移動する。引数がない場合は、通常ホームディレクトリ (
~
) に移動する。 - 引数が無効な場合(例えば、存在しないディレクトリを指定した場合)は、エラーメッセージを表示する。
修正したシェルのコード
以下に、cd
コマンドを組み込んだシェルのコードを示す。
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <unistd.h>
5#include <sys/wait.h>
6#include <signal.h>
7
8#define MAX_CMD_LEN 1024
9#define MAX_ARGS 100
10
11// コマンドをスペースで分割する関数
12void parse_command(char *cmd, char **args) {
13 char *token = strtok(cmd, " \n");
14 int i = 0;
15 while (token != NULL) {
16 args[i++] = token;
17 token = strtok(NULL, " \n");
18 }
19 args[i] = NULL; // 最後の引数をNULLで終わらせる
20}
21
22// コマンドの末尾が '&' かどうかを確認し、'&' を取り除いて戻り値として返す
23int is_background_command(char **args) {
24 int i = 0;
25 while (args[i] != NULL) {
26 i++;
27 }
28 if (i > 0 && strcmp(args[i - 1], "&") == 0) {
29 args[i - 1] = NULL; // '&' をコマンド引数から削除
30 return 1; // バックグラウンド実行フラグを返す
31 }
32 return 0;
33}
34
35// SIGCHLD シグナルハンドラ(バックグラウンドプロセスの終了を処理)
36void sigchld_handler(int signo) {
37 int status;
38 // 終了した子プロセスの状態を取得
39 while (waitpid(-1, &status, WNOHANG) > 0) {
40 // 子プロセスがシグナルで終了した場合はそのシグナル番号を出力
41 if (WIFSIGNALED(status)) {
42 printf("Child process terminated by signal %d\n", WTERMSIG(status));
43 }
44 }
45}
46
47// 組み込みコマンド cd を処理する関数
48void execute_cd(char **args) {
49 if (args[1] == NULL) {
50 // 引数なしの場合はホームディレクトリに移動
51 const char *home_dir = getenv("HOME");
52 if (home_dir == NULL) {
53 perror("cd: HOME not set");
54 } else {
55 if (chdir(home_dir) != 0) {
56 perror("cd failed");
57 }
58 }
59 } else {
60 // 引数が指定された場合、そのディレクトリに移動
61 if (chdir(args[1]) != 0) {
62 perror("cd failed");
63 }
64 }
65}
66
67int main() {
68 char cmd[MAX_CMD_LEN]; // コマンド入力用のバッファ
69 char *args[MAX_ARGS]; // コマンド引数
70 pid_t pid;
71 int status;
72
73 // SIGCHLD シグナルのハンドラを設定
74 signal(SIGCHLD, sigchld_handler);
75
76 // シェルのメインループ
77 while (1) {
78 // プロンプトを表示
79 printf("mysh> ");
80 fflush(stdout);
81
82 // コマンドを読み込む
83 if (fgets(cmd, sizeof(cmd), stdin) == NULL) {
84 if (feof(stdin)) {
85 break; // EOFが入力された場合、終了
86 }
87 perror("fgets failed");
88 continue;
89 }
90
91 // コマンドを解析
92 parse_command(cmd, args);
93
94 // 空のコマンドの場合は次の入力へスキップ
95 if (args[0] == NULL) {
96 continue; // 空コマンドは何もせず次に進む
97 }
98
99 // 終了コマンドの場合
100 if (args[0] != NULL && strcmp(args[0], "exit") == 0) {
101 break;
102 }
103
104 // cd コマンドの場合、組み込みで処理
105 if (args[0] != NULL && strcmp(args[0], "cd") == 0) {
106 execute_cd(args);
107 continue;
108 }
109
110 // バックグラウンド実行かどうかを判定
111 int background = is_background_command(args);
112
113 // 子プロセスを作成してコマンドを実行
114 pid = fork();
115 if (pid == -1) {
116 perror("fork failed");
117 exit(1);
118 }
119
120 if (pid == 0) { // 子プロセス
121 // execvpを使用してコマンドを実行
122 if (execvp(args[0], args) == -1) {
123 perror("exec failed");
124 exit(1);
125 }
126 } else { // 親プロセス
127 if (!background) {
128 // バックグラウンドでなければ、親は子プロセスの終了を待つ
129 waitpid(pid, &status, 0);
130 } else {
131 // バックグラウンドの場合、親は終了を待たずに次のコマンドを受け付ける
132 printf("Background process started with PID: %d\n", pid);
133 }
134 }
135 }
136
137 printf("Exiting shell...\n");
138 return 0;
139}
主な変更点
cd
コマンドの処理execute_cd()
関数を追加した。この関数は、引数がない場合はホームディレクトリに移動し、引数が指定されている場合はそのディレクトリに移動する。ディレクトリ変更にはchdir()
システムコールを使用する。- ホームディレクトリは
getenv("HOME")
を使って取得します。
cd
コマンドの判定- ユーザーが
cd
を入力した場合、シェルはexecute_cd()
を呼び出してディレクトリを変更する。cd
コマンドが実行された場合、fork()
とexecvp()
の処理はスキップされる。
- ユーザーが
ディレクトリ変更のエラーハンドリング
- 指定されたディレクトリが存在しない場合や、
HOME
環境変数が設定されていない場合にエラーメッセージを表示する。
- 指定されたディレクトリが存在しない場合や、
使用方法
- 上記のコードをファイルに保存(例えば
myshell.c
)する。 - コンパイルする:
1gcc myshell.c -o myshell
- 実行する:
1./myshell
サンプル実行
1mysh> cd /home/user
2mysh> pwd
3/home/user
4mysh> cd
5mysh> cd /nonexistent
6cd failed: No such file or directory
7mysh> exit
8Exiting shell...
このように、cd
コマンドはシェル内で動作し、引数に指定されたディレクトリに移動する。引数なしの場合はホームディレクトリに移動し、無効なディレクトリが指定された場合はエラーメッセージを表示する。
リダイレクション機能を追加
リダイレクション(入力リダイレクション、出力リダイレクション)の機能をシェルに追加することで、ユーザーがコマンドの入力や出力をファイルにリダイレクトできるようになる。 ここでは、シェルに以下のリダイレクション機能を追加する。
追加するリダイレクション機能
- 出力リダイレクション (
>
): コマンドの標準出力を指定したファイルに書き込む。 - 入力リダイレクション (
<
): コマンドの標準入力を指定したファイルから読み込む。 - 追記リダイレクション (
>>
): コマンドの標準出力を指定したファイルに追記する。
実装の概要
コマンド引数の解析 コマンドを解析して、リダイレクション記号(
>
,<
,>>
)が含まれているかを確認する。これらの記号が見つかれば、対応するファイルの開き方を決定する。fork()
とexecvp()
の使用fork()
で子プロセスを作成し、リダイレクションが指定された場合、子プロセス内で標準入力や標準出力をファイルにリダイレクトする。リダイレクションの実装
- 出力リダイレクションは
freopen()
またはopen()
を使用してファイルにリダイレクトする。 - 入力リダイレクションは
freopen()
またはopen()
を使用してファイルから入力をリダイレクトする。
- 出力リダイレクションは
修正したシェルのコード
以下に、リダイレクション機能を追加したシェルのコードを示す。
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <unistd.h>
5#include <sys/wait.h>
6#include <signal.h>
7#include <fcntl.h>
8
9#define MAX_CMD_LEN 1024
10#define MAX_ARGS 100
11
12// コマンドをスペースで分割する関数
13void parse_command(char *cmd, char **args) {
14 char *token = strtok(cmd, " \n");
15 int i = 0;
16 while (token != NULL) {
17 args[i++] = token;
18 token = strtok(NULL, " \n");
19 }
20 args[i] = NULL; // 最後の引数をNULLで終わらせる
21}
22
23// コマンドの末尾が '&' かどうかを確認し、'&' を取り除いて戻り値として返す
24int is_background_command(char **args) {
25 int i = 0;
26 while (args[i] != NULL) {
27 i++;
28 }
29 if (i > 0 && strcmp(args[i - 1], "&") == 0) {
30 args[i - 1] = NULL; // '&' をコマンド引数から削除
31 return 1; // バックグラウンド実行フラグを返す
32 }
33 return 0;
34}
35
36// SIGCHLD シグナルハンドラ(バックグラウンドプロセスの終了を処理)
37void sigchld_handler(int signo) {
38 int status;
39 // 終了した子プロセスの状態を取得
40 while (waitpid(-1, &status, WNOHANG) > 0) {
41 // 子プロセスがシグナルで終了した場合はそのシグナル番号を出力
42 if (WIFSIGNALED(status)) {
43 printf("Child process terminated by signal %d\n", WTERMSIG(status));
44 }
45 }
46}
47
48// 組み込みコマンド cd を処理する関数
49void execute_cd(char **args) {
50 if (args[1] == NULL) {
51 // 引数なしの場合はホームディレクトリに移動
52 const char *home_dir = getenv("HOME");
53 if (home_dir == NULL) {
54 perror("cd: HOME not set");
55 } else {
56 if (chdir(home_dir) != 0) {
57 perror("cd failed");
58 }
59 }
60 } else {
61 // 引数が指定された場合、そのディレクトリに移動
62 if (chdir(args[1]) != 0) {
63 perror("cd failed");
64 }
65 }
66}
67
68// リダイレクションのための関数
69void handle_redirection(char **args) {
70 int i = 0;
71 int input_redirect = 0;
72 int output_redirect = 0;
73 int append_redirect = 0;
74 char *input_file = NULL;
75 char *output_file = NULL;
76
77 // リダイレクションを探す
78 while (args[i] != NULL) {
79 if (strcmp(args[i], "<") == 0) {
80 input_redirect = 1;
81 input_file = args[i + 1];
82 args[i] = NULL; // リダイレクション記号を取り除く
83 } else if (strcmp(args[i], ">") == 0) {
84 output_redirect = 1;
85 output_file = args[i + 1];
86 args[i] = NULL;
87 } else if (strcmp(args[i], ">>") == 0) {
88 append_redirect = 1;
89 output_file = args[i + 1];
90 args[i] = NULL;
91 }
92 i++;
93 }
94
95 // 入力リダイレクション
96 if (input_redirect) {
97 int fd = open(input_file, O_RDONLY);
98 if (fd == -1) {
99 perror("Input redirection failed");
100 exit(1);
101 }
102 dup2(fd, STDIN_FILENO); // 標準入力をファイルにリダイレクト
103 close(fd);
104 }
105
106 // 出力リダイレクション(上書き)
107 if (output_redirect) {
108 int fd = open(output_file, O_WRONLY | O_CREAT | O_TRUNC, 0644);
109 if (fd == -1) {
110 perror("Output redirection failed");
111 exit(1);
112 }
113 dup2(fd, STDOUT_FILENO); // 標準出力をファイルにリダイレクト
114 close(fd);
115 }
116
117 // 出力リダイレクション(追記)
118 if (append_redirect) {
119 int fd = open(output_file, O_WRONLY | O_CREAT | O_APPEND, 0644);
120 if (fd == -1) {
121 perror("Append redirection failed");
122 exit(1);
123 }
124 dup2(fd, STDOUT_FILENO); // 標準出力をファイルに追記リダイレクト
125 close(fd);
126 }
127}
128
129int main() {
130 char cmd[MAX_CMD_LEN]; // コマンド入力用のバッファ
131 char *args[MAX_ARGS]; // コマンド引数
132 pid_t pid;
133 int status;
134
135 // SIGCHLD シグナルのハンドラを設定
136 signal(SIGCHLD, sigchld_handler);
137
138 // シェルのメインループ
139 while (1) {
140 // プロンプトを表示
141 printf("mysh> ");
142 fflush(stdout);
143
144 // コマンドを読み込む
145 if (fgets(cmd, sizeof(cmd), stdin) == NULL) {
146 if (feof(stdin)) {
147 break; // EOFが入力された場合、終了
148 }
149 perror("fgets failed");
150 continue;
151 }
152
153 // コマンドを解析
154 parse_command(cmd, args);
155
156 // 空のコマンドの場合は次の入力へスキップ
157 if (args[0] == NULL) {
158 continue; // 空コマンドは何もせず次に進む
159 }
160
161 // 終了コマンドの場合
162 if (args[0] != NULL && strcmp(args[0], "exit") == 0) {
163 break;
164 }
165
166 // cd コマンドの場合、組み込みで処理
167 if (args[0] != NULL && strcmp(args[0], "cd") == 0) {
168 execute_cd(args);
169 continue;
170 }
171
172 // バックグラウンド実行かどうかを判定
173 int background = is_background_command(args);
174
175 // 子プロセスを作成してコマンドを実行
176 pid = fork();
177 if (pid == -1) {
178 perror("fork failed");
179 exit(1);
180 }
181
182 if (pid == 0) { // 子プロセス
183 // リダイレクションの処理
184 handle_redirection(args);
185
186 // execvpを使用してコマンドを実行
187 if (execvp(args[0], args) == -1) {
188 perror("exec failed");
189 exit(1);
190 }
191 } else { // 親プロセス
192 if (!background) {
193 // バックグラウンドでなければ、親は子プロセスの終了を待つ
194 waitpid(pid, &status, 0);
195 } else {
196 // バックグラウンドの場合、親は終了を待たずに次のコマンドを受け付ける
197 printf("Background process started with PID: %d\n", pid);
198 }
199 }
200 }
201
202 printf("Exiting shell...\n");
203 return 0;
204}
主な変更点
handle_redirection()
関数の追加- リダイレクション(入力、出力、追記)を処理するための関数である。
- 引数として渡されたコマンドにリダイレクション記号(
<
,>
,>>
)が含まれているかをチェックし、該当する処理を行う。 dup2()
を使用して標準入力または標準出力をファイルにリダイレクトする。
入力リダイレクション (
<
): ファイルから標準入力を読み込みむ。open()
でファイルを読み込み専用で開き、dup2()
で標準入力に設定する。
出力リダイレクション (
>
): 標準出力を指定したファイルに書き込む。open()
でファイルを開き、dup2()
で標準出力に設定する。
追記リダイレクション (
>>
): 標準出力をファイルに追記する。open()
でファイルを追記モードで開き、dup2()
で標準出力に設定する。
使用方法
- 上記のコードをファイルに保存(例えば
myshell.c
)する。 - コンパイルする:
1gcc myshell.c -o myshell
- 実行する:
1./myshell
サンプル実行
1mysh> echo Hello, world! > output.txt
2mysh> cat output.txt
3Hello, world!
4mysh> echo Another line >> output.txt
5mysh> cat output.txt
6Hello, world!
7Another line
8mysh> cat < input.txt
9< input.txt の内容が表示される >
10mysh> exit
11Exiting shell...
リダイレクションを使うことで、コマンドの入力や出力をファイルにリダイレクトしたり、ファイルから入力を受け取ったりすることができる。
パイプライン機能を追加
パイプライン機能をシェルに追加することで、複数のコマンドを |
でつなげて、前のコマンドの標準出力を次のコマンドの標準入力として渡すことができるようになる。
シェル内でパイプラインを処理するには、次のようなステップが必要である:
パイプラインの基本的な処理
コマンドの解析 ユーザーが入力したコマンドに含まれるパイプライン(
|
)を探し、コマンドを分割して各コマンドを独立して実行できるようにする。pipe()
とfork()
の使用 パイプラインで複数のコマンドを接続するために、pipe()
を使用してパイプを作成し、fork()
で複数の子プロセスを生成する。各子プロセスは、自分の標準入力または標準出力をパイプに接続する。プロセス間でのデータ転送 最初のコマンドの標準出力をパイプの書き込み端に接続し、次のコマンドの標準入力をパイプの読み取り端に接続する。これにより、コマンド間でデータを渡すことができる。
パイプライン機能を追加したシェルのコード
以下に、パイプライン機能を追加したシェルのコードを示す。
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <unistd.h>
5#include <sys/wait.h>
6#include <signal.h>
7#include <fcntl.h>
8
9#define MAX_CMD_LEN 1024
10#define MAX_ARGS 100
11
12// コマンドをスペースで分割する関数
13void parse_command(char *cmd, char **args) {
14 int i = 0;
15 char *token = strtok(cmd, " \n");
16
17 // トークン化して args 配列に格納
18 while (token != NULL) {
19 args[i++] = token;
20 token = strtok(NULL, " \n");
21 }
22 args[i] = NULL; // 最後の引数をNULLで終わらせる
23}
24
25// コマンドをパイプで分割する関数
26int parse_pipeline(char *cmd, char **cmds) {
27 char *token = strtok(cmd, "|");
28 int i = 0;
29 while (token != NULL) {
30 cmds[i++] = token;
31 token = strtok(NULL, "|");
32 }
33 cmds[i] = NULL; // 最後をNULLで終端
34 return i; // パイプの数を返す
35}
36
37// コマンドの末尾が '&' かどうかを確認し、'&' を取り除いて戻り値として返す
38int is_background_command(char **args) {
39 int i = 0;
40 while (args[i] != NULL) {
41 i++;
42 }
43 if (i > 0 && strcmp(args[i - 1], "&") == 0) {
44 args[i - 1] = NULL; // '&' をコマンド引数から削除
45 return 1; // バックグラウンド実行フラグを返す
46 }
47 return 0;
48}
49
50// SIGCHLD シグナルハンドラ(バックグラウンドプロセスの終了を処理)
51void sigchld_handler(int signo) {
52 int status;
53 // 終了した子プロセスの状態を取得
54 while (waitpid(-1, &status, WNOHANG) > 0) {
55 // 子プロセスがシグナルで終了した場合はそのシグナル番号を出力
56 if (WIFSIGNALED(status)) {
57 printf("Child process terminated by signal %d\n", WTERMSIG(status));
58 }
59 }
60}
61
62// 組み込みコマンド cd を処理する関数
63void execute_cd(char **args) {
64 if (args[1] == NULL) {
65 // 引数なしの場合はホームディレクトリに移動
66 const char *home_dir = getenv("HOME");
67 if (home_dir == NULL) {
68 perror("cd: HOME not set");
69 } else {
70 if (chdir(home_dir) != 0) {
71 perror("cd failed");
72 }
73 }
74 } else {
75 // 引数が指定された場合、そのディレクトリに移動
76 if (chdir(args[1]) != 0) {
77 perror("cd failed");
78 }
79 }
80}
81
82// リダイレクションのための関数
83void handle_redirection(char **args) {
84 int i = 0;
85 while (args[i] != NULL) {
86 if (strcmp(args[i], "<") == 0) {
87 int fd = open(args[i + 1], O_RDONLY);
88 if (fd == -1) {
89 perror("Input redirection failed");
90 exit(1);
91 }
92 dup2(fd, STDIN_FILENO);
93 close(fd);
94 args[i] = NULL; // リダイレクション記号を取り除く
95 } else if (strcmp(args[i], ">") == 0) {
96 int fd = open(args[i + 1], O_WRONLY | O_CREAT | O_TRUNC, 0644);
97 if (fd == -1) {
98 perror("Output redirection failed");
99 exit(1);
100 }
101 dup2(fd, STDOUT_FILENO);
102 close(fd);
103 args[i] = NULL; // リダイレクション記号を取り除く
104 } else if (strcmp(args[i], ">>") == 0) {
105 int fd = open(args[i + 1], O_WRONLY | O_CREAT | O_APPEND, 0644);
106 if (fd == -1) {
107 perror("Append redirection failed");
108 exit(1);
109 }
110 dup2(fd, STDOUT_FILENO);
111 close(fd);
112 args[i] = NULL; // リダイレクション記号を取り除く
113 }
114 i++;
115 }
116}
117
118int main() {
119 char cmd[MAX_CMD_LEN]; // コマンド入力用のバッファ
120 char *cmds[MAX_ARGS]; // パイプで分割されたコマンド用
121 char *args[MAX_ARGS]; // コマンド引数
122 pid_t pid;
123 int status;
124
125 // SIGCHLD シグナルのハンドラを設定
126 signal(SIGCHLD, sigchld_handler);
127
128 // シェルのメインループ
129 while (1) {
130 // プロンプトを表示
131 printf("mysh> ");
132 fflush(stdout);
133
134 // コマンドを読み込む
135 if (fgets(cmd, sizeof(cmd), stdin) == NULL) {
136 if (feof(stdin)) break; // EOFが入力された場合、終了
137 perror("fgets failed");
138 continue;
139 }
140
141 // パイプで分割
142 int num_cmds = parse_pipeline(cmd, cmds);
143
144 if (num_cmds > 1) { // パイプが含まれる場合
145 int pipes[num_cmds - 1][2];
146 pid_t pids[num_cmds];
147
148 // 必要なパイプを作成
149 for (int i = 0; i < num_cmds - 1; i++) {
150 if (pipe(pipes[i]) == -1) {
151 perror("pipe failed");
152 exit(1);
153 }
154 }
155
156 for (int i = 0; i < num_cmds; i++) {
157 parse_command(cmds[i], args);
158
159 if ((pids[i] = fork()) == -1) {
160 perror("fork failed");
161 exit(1);
162 }
163
164 if (pids[i] == 0) { // 子プロセス
165 if (i > 0) { // パイプの入力
166 dup2(pipes[i - 1][0], STDIN_FILENO);
167 }
168 if (i < num_cmds - 1) { // パイプの出力
169 dup2(pipes[i][1], STDOUT_FILENO);
170 }
171 for (int j = 0; j < num_cmds - 1; j++) { // 全てのパイプを閉じる
172 close(pipes[j][0]);
173 close(pipes[j][1]);
174 }
175
176 handle_redirection(args); // リダイレクション処理
177
178 if (execvp(args[0], args) == -1) {
179 perror("exec failed");
180 exit(1);
181 }
182 }
183 }
184
185 // 親プロセスでパイプを閉じる
186 for (int i = 0; i < num_cmds - 1; i++) {
187 close(pipes[i][0]);
188 close(pipes[i][1]);
189 }
190
191 // 全ての子プロセスの終了を待つ
192 for (int i = 0; i < num_cmds; i++) {
193 waitpid(pids[i], &status, 0);
194 }
195 } else { // パイプが含まれない通常のコマンド
196 parse_command(cmds[0], args);
197
198 // 空のコマンドの場合は次の入力へスキップ
199 if (args[0] == NULL) continue;
200
201 // 終了コマンドの場合
202 if (strcmp(args[0], "exit") == 0) break;
203
204 // cd コマンドの場合、組み込みで処理
205 if (strcmp(args[0], "cd") == 0) {
206 execute_cd(args);
207 continue;
208 }
209
210 // バックグラウンド実行かどうかを判定
211 int background = is_background_command(args);
212
213 // 子プロセスを作成してコマンドを実行
214 pid = fork();
215 if (pid == -1) {
216 perror("fork failed");
217 exit(1);
218 }
219
220 if (pid == 0) { // 子プロセス
221 // リダイレクションの処理
222 handle_redirection(args);
223
224 // execvpを使用してコマンドを実行
225 if (execvp(args[0], args) == -1) {
226 perror("exec failed");
227 exit(1);
228 }
229 } else { // 親プロセス
230 if (!background) {
231 // バックグラウンドでなければ、親は子プロセスの終了を待つ
232 waitpid(pid, &status, 0);
233 } else {
234 // バックグラウンドの場合、親は終了を待たずに次のコマンドを受け付ける
235 printf("Background process started with PID: %d\n", pid);
236 }
237 }
238 }
239 }
240
241 printf("Exiting shell...\n");
242 return 0;
243}
主な変更点
parse_pipeline()
関数- コマンドを
|
で分割するための関数。
- コマンドを
プロセス間のパイプ接続
pipe()
を使ってパイプを作成する。- 各コマンドを実行する子プロセスを
fork()
で生成し、標準入力や標準出力をパイプにリダイレクトする。
複数コマンドのパイプライン処理
- 複数のコマンドがパイプでつながれた場合、順番に実行され、前のコマンドの標準出力が次のコマンドの標準入力として渡される。
使用例
1mysh> cat sample.txt
2aaa
3aaa
4aaa
5bbb
6aaa
7ccc
8aaa
9mysh> cat sample.txt | grep aaa | wc -l
10 5
このコードでは、複数のコマンドをパイプでつなげて実行することができる。 最初のコマンドの出力が次のコマンドの入力として渡され、最終的な結果が表示される。