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

主な変更点

  1. main() 関数

    • main() 関数内に built_in() 関数を追加した。 この関数では、入力された文字列(コマンド)が組み込みコマンドか否かの判定が行われ、組み込みコマンドの場合はそのコマンドが実行される。
    • 組み込みコマンドの場合は、戻り値が 0 となり、main() 関数内のループの先頭に戻って次のコマンド入力を待ち受ける。
  2. built_in() 関数

    • グローバル変数 blt_in[] は、組み込みコマンドの構造体配列である。 for ループでこの配列を走査して、シェルに入力されたコマンド名を探している。 見つかったら、入力されたコマンドは組み込みコマンドと判定され、組み込みコマンドごとに定義されている関数が呼び出される。 ここでは関数ポインタを使って個別の組み込みコマンドの関数を呼び出す形式とした。
    • 組み込みコマンドと判定された場合、戻り値は 0 、そうでない場合は -1 。
  3. cd コマンドの関数

    • 引数がない場合は getenv("HOME") でホームディレクトリのパスを取得して、そこに移動する。
    • 引数が指定されている場合は、システムコール chdir() を使って指定されたディレクトリに移動する。
  4. pwd コマンドの関数

    • getcwd() を呼び出してカレントディレクトリを取得し、端末に表示する。

使用方法

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

関連ページ