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}