Linuxで簡単なシェルを自作してみた(4)|組み込みコマンド
目次
Linuxで簡単なシェルを自作してみた(3)|wait実行中の割り込みを回避するのつづき。
組み込みコマンドをシェルに追加するには、組み込みコマンドが呼ばれたときにシェル自身のプロセスで実行する必要がある。(わざわざ子プロセスを生成して外部コマンドとして実行するのは、組み込みとは言わない)
今回は、組み込みコマンドとして、ディレクトリを変更するための cd
コマンドと、カレントディレクトリを表示する pwd
を追加してみる。
cd
コマンドの特徴
cd
コマンドは、シェルプロセス内で実行され、シェルのカレントディレクトリを変更する。新たなプロセスを作成して実行するのではなく、現在実行中のシェルのプロセスでディレクトリ変更を行う。- 引数に指定されたパスに移動する。引数がない場合は、ホームディレクトリに移動する。
- 引数が無効な場合(例えば、存在しないディレクトリを指定した場合)は、エラーメッセージを表示する。
pwd
コマンドの特徴
pwd
コマンドは、シェルプロセス内で実行され、現在のカレントディレクトリの絶対パスを表示する。
修正したシェルのソースコード
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <unistd.h> // fork, execvp
5#include <sys/wait.h> // waitpid
6#include <glob.h> // glob
7#include <errno.h> // errno
8#include <limits.h> // PATH_MAX
9
10#define MAX_CMD_LEN 1024
11#define MAX_ARGS 100
12
13// 組み込みコマンド関連の宣言
14int cd (char *argv[]);
15int pwd(char *argv[]);
16struct builtin {
17 char cmd[256]; // コマンド名
18 int (*func)(char *argv[]); // 関数へのポインタ
19};
20struct builtin blt_in[] = {{"cd",cd}, {"pwd",pwd}, {"",0}};
21
22// コマンドをスペースで分割する関数
23void parse_command(char *cmd, char *argv[]) {
24 char *token = strtok(cmd, " \n");
25 int i = 0;
26 while (token != NULL) {
27 argv[i++] = token;
28 token = strtok(NULL, " \n");
29 }
30 argv[i] = NULL; // 最後の引数をNULLで終わらせる
31}
32
33// 引用符を削除する関数
34void remove_quotes(char *str) {
35 char *src = str, *dst = str;
36 while (*src) {
37 if (*src == '"' || *src == '\'') {
38 src++; // 引用符をスキップ
39 } else {
40 *dst++ = *src++;
41 }
42 }
43 *dst = '\0'; // 文字列の終端を追加
44}
45
46// 引数リストにパス名展開を適用する関数
47int expand_arguments(char *argv[], char **expanded_argv) {
48 glob_t glob_result;
49 int argc = 0; // 展開後の引数数
50
51 for (int i = 0; argv[i] != NULL; i++) {
52 // 引用符が含まれている場合は展開しない
53 if ((strchr(argv[i], '*') || strchr(argv[i], '?') || strchr(argv[i], '[')) &&
54 (argv[i][0] != '"' && argv[i][0] != '\'')) { // 引用符外でパス名展開
55 if (glob(argv[i], GLOB_NOCHECK | GLOB_TILDE, NULL, &glob_result) != 0) {
56 perror("glob failed");
57 return -1;
58 }
59 // 展開された結果を追加
60 for (size_t j = 0; j < glob_result.gl_pathc; j++) {
61 if (argc >= MAX_ARGS - 1) {
62 fprintf(stderr, "Too many arguments after glob expansion\n");
63 globfree(&glob_result);
64 return -1;
65 }
66 expanded_argv[argc++] = strdup(glob_result.gl_pathv[j]);
67 }
68 globfree(&glob_result); // メモリを解放
69 } else {
70 // 引用符を取り除く
71 remove_quotes(argv[i]);
72
73 // パス名展開が不要な場合
74 if (argc >= MAX_ARGS - 1) {
75 fprintf(stderr, "Too many arguments\n");
76 return -1;
77 }
78 expanded_argv[argc++] = strdup(argv[i]);
79 }
80 }
81 expanded_argv[argc] = NULL; // 最後にNULLを追加
82 return argc;
83}
84
85// 組み込みコマンドの判定・実行
86int built_in(char *argv[]){
87 struct builtin *p;
88 // 組み込みコマンド配列(blt_in)の走査
89 for (p = blt_in; *p->cmd != '\0'; p++) {
90 // 組み込みコマンドでない場合は次をチェック
91 if (strcmp(p->cmd, argv[0]) != 0) continue;
92 // 組み込みコマンドの関数を呼び出し
93 (p->func)(argv);
94 return 0;
95 }
96 return -1;
97}
98
99// cd コマンド
100int cd(char *argv[]) {
101 if (argv[1] == NULL) {
102 // 引数なしの場合はホームディレクトリに移動
103 const char *home_dir = getenv("HOME");
104 if (home_dir == NULL) {
105 perror("cd: HOME not set");
106 return -1;
107 } else {
108 if (chdir(home_dir) != 0) {
109 perror("cd failed");
110 return -1;
111 }
112 }
113 } else {
114 // 引数が指定された場合、そのディレクトリに移動
115 if (chdir(argv[1]) != 0) {
116 perror("cd failed");
117 return -1;
118 }
119 }
120 return 0;
121}
122
123// pwd コマンド
124int pwd(char *argv[]){
125 char buf[PATH_MAX+1];
126 if (getcwd(buf, sizeof(buf)) == NULL) {
127 perror("getcwd failed");
128 return -1;
129 }
130 printf("%s\n", buf);
131 return 0;
132}
133
134int main() {
135 char cmd[MAX_CMD_LEN]; // コマンド入力用のバッファ
136 char *argv[MAX_ARGS]; // コマンド引数
137 char *expanded_argv[MAX_ARGS]; // 展開後の引数
138 pid_t pid;
139 int status;
140
141 // シェルのメインループ
142 while (1) {
143 // プロンプトを表示
144 fprintf(stderr, "mysh> ");
145
146 // コマンドを読み込む
147 if (fgets(cmd, sizeof(cmd), stdin) == NULL) {
148 if (feof(stdin)) {
149 break; // EOFが入力された場合、終了
150 }
151 perror("fgets failed");
152 continue;
153 }
154
155 // コマンドを解析
156 parse_command(cmd, argv);
157
158 // 空のコマンドの場合は次の入力へスキップ
159 if (argv[0] == NULL) {
160 continue;
161 }
162
163 // 終了コマンドの場合
164 if (strcmp(argv[0], "exit") == 0) {
165 break;
166 }
167
168 // 引数を展開
169 if (expand_arguments(argv, expanded_argv) == -1) {
170 fprintf(stderr, "Argument expansion failed\n");
171 continue;
172 }
173
174 // 組み込みコマンド判定・実行
175 if (built_in(expanded_argv) == 0) {
176 continue;
177 }
178
179 // 子プロセスを作成してコマンドを実行
180 pid = fork();
181 if (pid == -1) {
182 perror("fork failed");
183 exit(1);
184 }
185
186 // 子プロセス
187 if (pid == 0) {
188 // execvpを使用してコマンドを実行
189 if (execvp(expanded_argv[0], expanded_argv) == -1) {
190 perror("exec failed");
191 exit(1);
192 }
193 }
194
195 // 親プロセス
196 while (1) {
197 if (waitpid(pid, &status, 0) == (pid_t)-1) { // 子プロセスの終了を待つ
198 if (errno == EINTR) {
199 continue; // 割り込み発生時はリトライする
200 } else {
201 perror("waitpid failed");
202 exit(1);
203 }
204 }
205 break;
206 }
207
208 // メモリを解放
209 for (int i = 0; expanded_argv[i] != NULL; i++) {
210 free(expanded_argv[i]);
211 }
212 }
213
214 printf("Exiting shell...\n");
215 return 0;
216}
Linuxで簡単なシェルを自作してみた(3)|wait実行中の割り込みを回避するのソースコードと
今回修正したソースコードの差異(diff)を、以下リンク先に示す。
修正前後の比較1
主な変更点
main()
関数main()
関数内にbuilt_in()
関数を追加した。 この関数では、入力された文字列(コマンド)が組み込みコマンドか否かの判定が行われ、組み込みコマンドの場合はそのコマンドが実行される。- 組み込みコマンドの場合は、戻り値が 0 となり、
main()
関数内のループの先頭に戻って次のコマンド入力を待ち受ける。
built_in()
関数- グローバル変数
blt_in[]
は、組み込みコマンドの構造体配列である。 for ループでこの配列を走査して、シェルに入力されたコマンド名を探している。 見つかったら、入力されたコマンドは組み込みコマンドと判定され、組み込みコマンドごとに定義されている関数が呼び出される。 ここでは関数ポインタを使って個別の組み込みコマンドの関数を呼び出す形式とした。 - 組み込みコマンドと判定された場合、戻り値は 0 、そうでない場合は -1 。
- グローバル変数
cd
コマンドの関数- 引数がない場合は
getenv("HOME")
でホームディレクトリのパスを取得して、そこに移動する。 - 引数が指定されている場合は、システムコール
chdir()
を使って指定されたディレクトリに移動する。
- 引数がない場合は
pwd
コマンドの関数getcwd()
を呼び出してカレントディレクトリを取得し、端末に表示する。
使用方法
- 上記のコードをファイルに保存(例えば
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...