Linuxで簡単なシェルを自作してみた(2)|パス名展開と引用符削除

Linuxで簡単なシェルを自作してみた(1)|外部コマンドの実行で作成したシェルを強化していく。まずはパス名展開の実装から。

パス名展開とは、例えば拡張子が .txt のファイルをリストアップするときに ls *.txt を実行するが、 * をワイルドカードとして扱い、 *.txt のパターンにマッチするファイルに展開するようなことだ。

この展開をシステムコールを駆使して作ると骨が折れそうなので、glob という標準ライブラリを利用する。

パス名展開を追加したソースコード

(以下のソースコードは基本的な考慮漏れがある。後述する。)

  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
  8#define MAX_CMD_LEN 1024
  9#define MAX_ARGS 100
 10
 11// コマンドをスペースで分割する関数
 12void parse_command(char *cmd, char *argv[]) {
 13    char *token = strtok(cmd, " \n");
 14    int i = 0;
 15    while (token != NULL) {
 16        argv[i++] = token;
 17        token = strtok(NULL, " \n");
 18    }
 19    argv[i] = NULL; // 最後の引数をNULLで終わらせる
 20}
 21
 22// 引数リストにパス名展開を適用する関数
 23int expand_arguments(char *argv[], char **expanded_argv) {
 24    glob_t glob_result;
 25    int argc = 0; // 展開後の引数数
 26
 27    for (int i = 0; argv[i] != NULL; i++) {
 28        if (strchr(argv[i], '*') || strchr(argv[i], '?') || strchr(argv[i], '[')) {
 29            // パス名展開が必要な場合
 30            if (glob(argv[i], GLOB_NOCHECK | GLOB_TILDE, NULL, &glob_result) != 0) {
 31                perror("glob failed");
 32                return -1;
 33            }
 34            // 展開された結果を追加
 35            for (size_t j = 0; j < glob_result.gl_pathc; j++) {
 36                if (argc >= MAX_ARGS - 1) {
 37                    fprintf(stderr, "Too many arguments after glob expansion\n");
 38                    globfree(&glob_result);
 39                    return -1;
 40                }
 41                expanded_argv[argc++] = strdup(glob_result.gl_pathv[j]);
 42            }
 43            globfree(&glob_result); // メモリを解放
 44        } else {
 45            // パス名展開が不要な場合
 46            if (argc >= MAX_ARGS - 1) {
 47                fprintf(stderr, "Too many arguments\n");
 48                return -1;
 49            }
 50            expanded_argv[argc++] = strdup(argv[i]);
 51        }
 52    }
 53    expanded_argv[argc] = NULL; // 最後にNULLを追加
 54    return argc;
 55}
 56
 57int main() {
 58    char cmd[MAX_CMD_LEN]; // コマンド入力用のバッファ
 59    char *argv[MAX_ARGS];  // コマンド引数
 60    char *expanded_argv[MAX_ARGS]; // 展開後の引数
 61    pid_t pid;
 62    int status;
 63
 64    // シェルのメインループ
 65    while (1) {
 66        // プロンプトを表示
 67        fprintf(stderr, "mysh> ");
 68
 69        // コマンドを読み込む
 70        if (fgets(cmd, sizeof(cmd), stdin) == NULL) {
 71            if (feof(stdin)) {
 72                break; // EOFが入力された場合、終了
 73            }
 74            perror("fgets failed");
 75            continue;
 76        }
 77
 78        // コマンドを解析
 79        parse_command(cmd, argv);
 80
 81        // 空のコマンドの場合は次の入力へスキップ
 82        if (argv[0] == NULL) {
 83            continue;
 84        }
 85
 86        // 終了コマンドの場合
 87        if (strcmp(argv[0], "exit") == 0) {
 88            break;
 89        }
 90
 91        // 引数を展開
 92        if (expand_arguments(argv, expanded_argv) == -1) {
 93            fprintf(stderr, "Argument expansion failed\n");
 94            continue;
 95        }
 96
 97        // 子プロセスを作成してコマンドを実行
 98        pid = fork();
 99        if (pid == -1) {
100            perror("fork failed");
101            exit(1);
102        }
103
104        // 子プロセス
105        if (pid == 0) {
106            // execvpを使用してコマンドを実行
107            if (execvp(expanded_argv[0], expanded_argv) == -1) {
108                perror("exec failed");
109                exit(1);
110            }
111        }
112
113        // 親プロセス
114        if (waitpid(pid, &status, 0) == (pid_t)-1) { // 子プロセスの終了を待つ
115            perror("waitpid failed");
116            exit(1);
117        }
118
119        // メモリを解放
120        for (int i = 0; expanded_argv[i] != NULL; i++) {
121            free(expanded_argv[i]);
122        }
123    }
124
125    printf("Exiting shell...\n");
126    return 0;
127}

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

説明

argv には解析後のコマンド引数へのポインタが設定されていることは Linuxで簡単なシェルを自作してみた(1)|外部コマンドの実行で説明済み。 今回は、その引数にワイルドカードが含まれている場合に、それを展開して、その展開結果を expanded_argv に設定する。

  • expand_arguments() の概念図

例えば、コマンドとして echo *.txt を入力した場合で示す。

  • 主な関数のリファレンス

glob() のリファレンスより抜粋)

1#include <glob.h>
2int glob (const char *restrict pattern, int flags,
3          int (*errfunc)(const char *epath, int eerrno),
4          glob_t *restrict pglob);
5void globfree (glob_t *pglob);

glob() は、シェルで使用される規則に従って、第一引数 pattern に一致するすべてのパス名を検索する。
globfree() は、以前に glob() を呼び出した際に動的に確保されたメモリを解放する。

glob() 呼び出しの結果は、第四引数 pglob が指す構造体に格納される。この構造体は glob_t 型(<glob.h> で宣言されている)であり、以下の要素を含む:

1typedef struct {
2    size_t   gl_pathc;    /* これまでに一致したパスの数 */
3    char   **gl_pathv;    /* 一致したパス名のリスト */
4    size_t   gl_offs;     /* gl_pathv に予約するスロット数 */
5} glob_t;

結果は動的に確保されたメモリに格納される。

第二引数 flags は、glob() の動作を変更する以下のシンボリック定数のいずれか、または、複数を、ビット単位の論理和で組み合わせたものを指定する。
GLOB_NOCHECK: パターンに一致するものがない場合、元のパターンを返す。(デフォルトは GLOB_NOMATCH を返す)
GLOB_TILDE: チルダ展開を実行する。

第三引数 errfunc が NULL でない場合、エラーが発生すると、エラーが起きたパスを指すポインタ (epath) と、opendir(3), readdir(3), または stat(2) の呼び出しによって返された errno の値 (eerrno) を引数として呼び出される。

glob() が成功すると、pglob->gl_pathc には一致したパス名の数が、pglob->gl_pathv には一致したパス名へのポインタリストが格納される。このリストは NULL ポインタで終了する。

戻り値:
glob() が正常に完了すると、戻り値は 0 になりる。 それ以外では、GLOB_NOSPACE(メモリ不足の場合)、GLOB_ABORTED(読み取りエラーが発生した場合)、GLOB_NOMATCH(一致するものが見つからなかった場合)がある。

strdup() のリファレンスより抜粋)
#include <string.h>
char *strdup(const char *s);

strdup() は、指定された文字列 s を複製し、その複製へのポインタを返す。この複製は malloc(3) によって確保されるため、使用後は free(3) を使って解放する必要がある。

戻り値:
strdup() は成功すると、複製された文字列へのポインタを返す。メモリが不足している場合は NULL を返し、その際にエラーを示すために errno が設定される。

引用符内のワイルドカードは展開しない

例えば、find . -name '*.txt' というコマンドを考えてみる。 ここまで説明したソースコードだと、引用符を含めた文字列 '*.txt' にマッチするファイル名は 通常は存在しないので、展開されない。 よって、引用符付きの '*.txt' がそのまま find の引数となるが、find でも引用符付きで マッチするファイル名は探せないので、結局、find の実行結果としては何も返らない。 (期待した動作は *.txt にマッチするファイル名を返して欲しいのに)

よって、シェルの内部で引用符を除去する必要がある。これにより find の引数として引用符がついていない *.txt が渡り、a.txt などのファイルがマッチするようになる。

ただし、* などのワイルドカードは、これまで説明してきたようにパス名展開されてしまう。 '*.txt' が展開された結果が find の引数として渡ってしまうと、 例えば find . -name a.txt b.txt などと実行することになり、find の引数違反のためエラーとなる。 よって引用符内のワールドカードを展開するのはNGだ。

これらを考慮したソースコードは以下となる。 引用符を考慮する前後のソースコードの差異(diff)を、以下リンク先に示す。
修正前後の比較2

  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
  8#define MAX_CMD_LEN 1024
  9#define MAX_ARGS 100
 10
 11// コマンドをスペースで分割する関数
 12void parse_command(char *cmd, char *argv[]) {
 13    char *token = strtok(cmd, " \n");
 14    int i = 0;
 15    while (token != NULL) {
 16        argv[i++] = token;
 17        token = strtok(NULL, " \n");
 18    }
 19    argv[i] = NULL; // 最後の引数をNULLで終わらせる
 20}
 21
 22// 引用符を削除する関数
 23void remove_quotes(char *str) {
 24    char *src = str, *dst = str;
 25    while (*src) {
 26        if (*src == '"' || *src == '\'') {
 27            src++;  // 引用符をスキップ
 28        } else {
 29            *dst++ = *src++;
 30        }
 31    }
 32    *dst = '\0';  // 文字列の終端を追加
 33}
 34
 35// 引数リストにパス名展開を適用する関数
 36int expand_arguments(char *argv[], char **expanded_argv) {
 37    glob_t glob_result;
 38    int argc = 0; // 展開後の引数数
 39
 40    for (int i = 0; argv[i] != NULL; i++) {
 41        // 引用符が含まれている場合は展開しない
 42        if ((strchr(argv[i], '*') || strchr(argv[i], '?') || strchr(argv[i], '[')) &&
 43            (argv[i][0] != '"' && argv[i][0] != '\'')) { // 引用符外でパス名展開
 44            if (glob(argv[i], GLOB_NOCHECK | GLOB_TILDE, NULL, &glob_result) != 0) {
 45                perror("glob failed");
 46                return -1;
 47            }
 48            // 展開された結果を追加
 49            for (size_t j = 0; j < glob_result.gl_pathc; j++) {
 50                if (argc >= MAX_ARGS - 1) {
 51                    fprintf(stderr, "Too many arguments after glob expansion\n");
 52                    globfree(&glob_result);
 53                    return -1;
 54                }
 55                expanded_argv[argc++] = strdup(glob_result.gl_pathv[j]);
 56            }
 57            globfree(&glob_result); // メモリを解放
 58        } else {
 59            // 引用符を取り除く
 60            remove_quotes(argv[i]);
 61
 62            // パス名展開が不要な場合
 63            if (argc >= MAX_ARGS - 1) {
 64                fprintf(stderr, "Too many arguments\n");
 65                return -1;
 66            }
 67            expanded_argv[argc++] = strdup(argv[i]);
 68        }
 69    }
 70    expanded_argv[argc] = NULL; // 最後にNULLを追加
 71    return argc;
 72}
 73
 74int main() {
 75    char cmd[MAX_CMD_LEN]; // コマンド入力用のバッファ
 76    char *argv[MAX_ARGS];  // コマンド引数
 77    char *expanded_argv[MAX_ARGS]; // 展開後の引数
 78    pid_t pid;
 79    int status;
 80
 81    // シェルのメインループ
 82    while (1) {
 83        // プロンプトを表示
 84        fprintf(stderr, "mysh> ");
 85
 86        // コマンドを読み込む
 87        if (fgets(cmd, sizeof(cmd), stdin) == NULL) {
 88            if (feof(stdin)) {
 89                break; // EOFが入力された場合、終了
 90            }
 91            perror("fgets failed");
 92            continue;
 93        }
 94
 95        // コマンドを解析
 96        parse_command(cmd, argv);
 97
 98        // 空のコマンドの場合は次の入力へスキップ
 99        if (argv[0] == NULL) {
100            continue;
101        }
102
103        // 終了コマンドの場合
104        if (strcmp(argv[0], "exit") == 0) {
105            break;
106        }
107
108        // 引数を展開
109        if (expand_arguments(argv, expanded_argv) == -1) {
110            fprintf(stderr, "Argument expansion failed\n");
111            continue;
112        }
113
114        // 子プロセスを作成してコマンドを実行
115        pid = fork();
116        if (pid == -1) {
117            perror("fork failed");
118            exit(1);
119        }
120
121        // 子プロセス
122        if (pid == 0) {
123            // execvpを使用してコマンドを実行
124            if (execvp(expanded_argv[0], expanded_argv) == -1) {
125                perror("exec failed");
126                exit(1);
127            }
128        }
129
130        // 親プロセス
131        if (waitpid(pid, &status, 0) == (pid_t)-1) { // 子プロセスの終了を待つ
132            perror("waitpid failed");
133            exit(1);
134        }
135
136        // メモリを解放
137        for (int i = 0; expanded_argv[i] != NULL; i++) {
138            free(expanded_argv[i]);
139        }
140    }
141
142    printf("Exiting shell...\n");
143    return 0;
144}

関連ページ