写一个自己的Unix Shell(1)搭建一个框架

        熟悉 Unix/Linux 系统的朋友都清楚,我们通常都通过 Shell 程序和 Unix/Linux 系统打交道,比如查看文件列表,我们会输入ls命令,切换目录,我们会输入cd命令等等,那么 Shell 程序是如何实现的呢?下面就让我们用C语言写一个自己的 Shell 程序。

第一步:搭建一个框架

        我们写一个框架,实现下图的样子。

        从上面的图中我们可以看到:当执行我们的myshell程序后,当我们只键入回车键,空格等这些空白内容时,程序不断输出 myshell# 字符串,当我们输入一行非空白内容再按回车时,程序会打印我们输入的内容,然后再打印myshell# 字符串后等待下一次输入。

        接下来我们实现上面的程序,首先我们写一个循环,在这个循环中打印 myshell# 字符串后等待用户输入数据。当用户输入空白内容时,程序接着打印myshell# 字符串等待用户下次输入,当用户输入非空白内容时,程序打印用户输入的内容,接着打印myshell# 字符串等待用户下次输入。

        要实现上面的流程,我们需要两个重要功能,就是从终端读取一行字符和判断字符串是否是空白字符串。

        我们用getline()函数来实现从终端中读取一行字符的功能。下面是getline()函数的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/*
des:
从给定文件中读取一行字符串
param:
returnStr[IN, OUT]: 用来存放从fp中读取到的一行字符串
returnStrBufSz[IN, OUT]: 用来保存returnStr的缓冲区大小,如果为NULL,
表示调用者不关心returnStr的缓冲区大小,就不会设置它。
fp[IN]: 待读取的文件
return:
成功: 读取到的字符串的实际大小
失败: -1
*/
ssize_t getline(char **returnStr, size_t *returnStrBufSz, FILE *fp) {
int ret = -1;
ssize_t nread = 0;
char c;
size_t bufSz = 512;
if(NULL == returnStr || NULL == fp) {
goto __finish;
}
*returnStr = malloc(bufSz);
if(*returnStr == NULL) {
goto __finish;
}
while((c = getc(fp)) != EOF) {
// 当开辟的空间不够用的时候, 将空间扩为原来的一倍
if(nread >= bufSz) {
bufSz = bufSz * 2;
*returnStr = realloc(*returnStr, bufSz);
if(*returnStr == NULL) {
return -1;
}
}
(*returnStr)[nread++] = c;
// 当读到换行符时, 跳出循环, 读取一行完毕
if(c == '\n') {
break;
}
}
(*returnStr)[nread] = '\0';
if(returnStrBufSz != NULL) {
*returnStrBufSz = bufSz;
}
ret = 0;
__finish:
if(ret < 0 && *returnStr != NULL) {
free(*returnStr);
*returnStr = NULL;
}
return ret == 0 ? nread : ret;
}

        我们结合例子来看一下getline()函数的原理和使用方法,下面是一个使用getline()函数的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void getlineUsage(void) {
char *strBuf = NULL;
size_t strBufSz = 0;
ssize_t nread = 0;
nread = getline(&strBuf, &strBufSz, stdin);
if(nread < 0) {
fprintf(stderr, "getlineUsage() getline() error\n");
if(strBufSz != NULL) {
free(strBufSz);
}
return;
}
if(nread == 0) {
printf("read end\n");
return;
}
printf("strBuf: %s\n", strBuf);
printf("strBufSz: %zu\n", strBufSz);
printf("nread: %zd\n", nread);
if(strBuf != NULL) {
free(strBuf);
}
return;
}

        在getline()函数的使用例子getlineUsage()函数中,我们定义了一个指针strBuf,一个size_t型的变量strBufSz,一个ssize_t型的变量 nread。接着将strBuf的地址,strBufSz的地址和stdin(即标准输入)传入了getline()函数中,在getline()函数中,会用第一个参数returnStr来接收strBuf的地址,对strBuf的地址进行解引用可以拿到strBuf指针指向的内容,一开始strBuf指向NULL(因为我们在getlineUsage()函数中对strBuf指针的初始值赋值为NULL),然后为strBuf开辟默认512字节大小的内存,用来存放从标准输入stdin读取的字符。接着用标准库函数getc()来不断从stdin中读取一个字符放入strBuf开辟的内存中。当strBuf开辟的内存不够用时,我们用realloc()函数来扩容内存,直到读到一个换行符\n,我们结束对stdin的读取。此时,strBuf指向的内存中已经存放了一行字符,于是我们为strBuf指向的内存放一个结束标志\0,将strBuf开辟的内存大小记录在strBufSz中(getline()函数的第二个参数returnStrBufSz接收了我们传入的strBufSz变量的地址,对returnStrBufSz解引用就可以拿到strBufSz变量的内容并可以重新赋值),返回读取到的字符数。当getline()函数返回后,我们用nread接收读取的字符数量,当读到的字符数小于0时,说明出现了错误,当读到的字符数等于0时,说明在标准输入stdin读取到了EOF文件结束标志,我们打印读取结束,然后返回,当读到的字符数大于0,我们打印读到的字符,strBuf开辟的内存大小和读到的字符数,接着释放strBuf的内存,例子结束。

        通过上面的描述,我们可以明白如何实现读取一行字符的功能,下面我们来看一下第二个重要功能:判断一个字符串是否为空白字符串。

        我们用isSpaceStr()函数来判断一个字符串是否为空白字符串。下面是isSpaceStr()函数的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
des:
判断是否是空白字符串,
(如果给定自符串仅包含 空格, tab符号, 换行, 回车, 我们就认为是空白字符串)
param:
str[IN]: 给定字符串
return:
是空白字符串: 1
不是空白字符串: 0
出错: -1
*/
int isSpaceStr(char *str) {
int spaceStr = 1;
if(str == NULL) {
return -1;
}
while(*str != '\0') {
if(*str != ' ' && *str != '\t' && *str != '\n' && *str != '\r') {
spaceStr = 0;
break;
}
str++;
}
return spaceStr;
}

        判断是否为空白字符串的函数非常简单,就是遍历给定的字符串,当给定的字符串含有空格、制表符、换行符和回车符之外的字符时,我们就认为它不是一个空白的字符串,否则就认为它是一个空白的字符串。

        到目前为止,我们实现了读取一行字符和判断给定字符串是否为空白字符串的功能,接着就在一个循环中从标准输入读取一行字符,判断一下是否为空白字符串,如果是空白字符串就继续从标准输入中读取,否则就打印非空白字符串后继续下一次的读取。在这个过程中打印一下字符串myshell# 就完成了制作自己的Shell的第一步:框架的搭建。

        实现上段描述的代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int main(int argc, char **argv) {

while(1) {
char *cmdString = NULL;
ssize_t nread = 0;
printf("myshell# ");
nread = getline(&cmdString, NULL, stdin);
if(nread < 0) {
fprintf(stderr, "main() getline()\n");
if(cmdString != NULL) {
free(cmdString);
}
exit(EXIT_FAILURE);
}
if(nread == 0) {
printf("\nBey~\n");
exit(EXIT_SUCCESS);
}
if(cmdString == NULL) {
fprintf(stderr, "main() cmdString = NULL\n");
exit(EXIT_FAILURE);
}
if(isSpaceStr(cmdString) == 1) {
free(cmdString);
continue;
}
printf("%s", cmdString);
if(cmdString != NULL) {
free(cmdString);
}

}
return 0;
}

        至此,我们完成了制作自己的Shell的第一步“搭建一个框架”的全部步骤。

附:

        欢迎关注我的微信公众号^_^。

dark

sans