1 简介
用C语言实现的一个简易的shell,能够接受用户输入的命令并执行操作,支持多重管道及重定向。
程序运行后,会模拟shell用绿色字体显示当前的用户名、主机名和路径,等待用户输入命令。程序逐次读取用户输入的指令后,将指令按空格拆分成多个字符串命令,然后判断该命令的类型。若命令有误,则用红色字体打印出错误信息。
- 若命令为exit,则调用自定义的exit函数,向该程序进程发送terminal信号结束该进程。
- 若命令为cd,则判断参数,调用chdir()函数修改当前的路径,并返回相应的结果。若修改成功,则使用getcwd()函数更新当前路径。
- 若为其它命令,则先判断是否有合法的管道。若有管道,则在子进程中执行管道符号前面的命令,父进程等待子进程结束后,递归处理管道符号后面的命令。若没有管道,则直接执行命令。在执行命令的时候,先判断该命令是否存在,以及是否有合法的重定向,再使用execvp()执行相应的操作。
2 功能
- 显示当前用户名、主机名和工作路径
- exit命令
- cd命令
- 判断命令是否存在
- 执行外部命令
- 实现输入、输出重定向
- 递归实现多重管道
3 效果展示
3.1 启动myshell
图中第一行为系统shell,为了与系统区分开,我将自定义的shell的默认信息显示全部设为绿色。由于该路径是个链接,链接到/mnt/g/os_homework/myshell,因此显示出来的路径是实际路径。
3.2 执行cd命令
cd命令的参数可以是相对路径,也可以是绝对路径。当参数出错时,会根据情况用红色字体打印出相应的错误信息。
3.3 执行外部命令
图中演示了“ls -al”、“rm”,以及自定义的可执行程序sum。sum程序要求输入一个整数n,然后求1~n的和。当命令不存在时,返回错误信息。
3.4 重定向
图中展示了输入、输出重定向,程序还会判断重定向是否合法。
3.5 管道
图中展示了多重管道的演示结果,管道与重定向也可以混用。
3.6 exit命令
程序接收到terminal信号后,退出。
4 关键代码
扯了这么多,总得讲一下代码吧。
程序代码已上传到GitHub中~~
4.1 获取用户名、主机名及当前工作路径
void getUsername() { // 获取当前登录的用户名
struct passwd* pwd = getpwuid(getuid());
strcpy(username, pwd->pw_name);
}
void getHostname() { // 获取主机名
gethostname(hostname, BUF_SZ);
}
int getCurWorkDir() { // 获取当前的工作目录
char* result = getcwd(curPath, BUF_SZ);
if (result == NULL)
return ERROR_SYSTEM;
else return RESULT_NORMAL;
}4.2 以空格分割命令
int splitCommands(char command[BUF_SZ]) { // 以空格分割命令, 返回分割得到的字符串个数
int num = 0;
int i, j;
int len = strlen(command);
for (i=0, j=0; i<len; ++i) {
if (command[i] != ' ') {
commands[num][j++] = command[i];
} else {
if (j != 0) {
commands[num][j] = '\0';
++num;
j = 0;
}
}
}
if (j != 0) {
commands[num][j] = '\0';
++num;
}
return num;
}其中字符串数组commands的全局变量,保存分割后的命令。
4.3 执行exit命令
int callExit() { // 发送terminal信号退出进程
pid_t pid = getpid();
if (kill(pid, SIGTERM) == -1)
return ERROR_EXIT;
else return RESULT_NORMAL;
}4.4 执行cd命令
int callCd(int commandNum) { // 执行cd命令
int result = RESULT_NORMAL;
if (commandNum < 2) {
result = ERROR_MISS_PARAMETER;
} else if (commandNum > 2) {
result = ERROR_TOO_MANY_PARAMETER;
} else {
int ret = chdir(commands[1]);
if (ret) result = ERROR_WRONG_PARAMETER;
}
return result;
}4.5 判断命令是否存在
Linux系统中,判断命令是否存在有多种方法。本程序使用”command -v xxx”来判断命令xxx是否存在。若命令存在,则会返回该命令的路径信息;否则,不会返回信息。因此可以用这一点来判断命令是否存在。
程序中使用管道,在子进程中将程序执行后的输出重定向到输出文件标识符,在父进程中将输入重定向到输入文件标识符,读取子进程返回的信息。若父进程读取的第一个字符就是EOF,则表示子进程没有返回信息,意味着命令不存在。执行完毕后,还原输入输出重定向。
int isCommandExist(const char* command) { // 判断指令是否存在
if (command == NULL || strlen(command) == 0) return FALSE;
int result = TRUE;
int fds[2];
if (pipe(fds) == -1) {
result = FALSE;
} else {
/* 暂存输入输出重定向标志 */
int inFd = dup(STDIN_FILENO);
int outFd = dup(STDOUT_FILENO);
pid_t pid = vfork();
if (pid == -1) {
result = FALSE;
} else if (pid == 0) {
/* 将结果输出重定向到文件标识符 */
close(fds[0]);
dup2(fds[1], STDOUT_FILENO);
close(fds[1]);
char tmp[BUF_SZ];
sprintf(tmp, "command -v %s", command);
system(tmp);
exit(1);
} else {
waitpid(pid, NULL, 0);
/* 输入重定向 */
close(fds[1]);
dup2(fds[0], STDIN_FILENO);
close(fds[0]);
if (getchar() == EOF) { // 没有数据,意味着命令不存在
result = FALSE;
}
/* 恢复输入、输出重定向 */
dup2(inFd, STDIN_FILENO);
dup2(outFd, STDOUT_FILENO);
}
}
return result;
}4.6 执行外部命令 ——callCommand()函数
该程序是给主函数调用的,参数是命令的长度,主要作用是创建子进程,在子进程中调用callCommandWithPipe()函数,该函数可以处理包含管道的命令,父进程获取子进程的返回码,并返回给主函数。
int callCommand(int commandNum) { // 给用户使用的函数,用以执行用户输入的命令
pid_t pid = fork();
if (pid == -1) {
return ERROR_FORK;
} else if (pid == 0) {
/* 获取标准输入、输出的文件标识符 */
int inFds = dup(STDIN_FILENO);
int outFds = dup(STDOUT_FILENO);
int result = callCommandWithPipe(0, commandNum);
/* 还原标准输入、输出重定向 */
dup2(inFds, STDIN_FILENO);
dup2(outFds, STDOUT_FILENO);
exit(result);
} else {
int status;
waitpid(pid, &status, 0);
return WEXITSTATUS(status);
}
}4.7 可处理多重管道的callCommandWithPipe()函数
因为要递归处理多重管道,因此将参数设为左闭右开的指令区间。先判断有没有管道符号,若没有,则直接调用callCommandWithRedi()函数去执行命令,该函数可以处理包含重定向信息的命令。若有管道符号,则先判断管道符号后续是否有指令,若没有,则返回错误信息,若有,则执行。
执行时,先启动管道,在子进程中执行管道符号前半部分的命令,并返回执行后的状态结果。父进程等待子进程退出后,获取子进程的返回码。若子进程没有正常执行,则读取子进程输出的错误信息,并打印到控制台。否则,递归执行管道符号后半部分的命令,并将结果返回给主函数。
int callCommandWithPipe(int left, int right) { // 所要执行的指令区间[left, right),可能含有管道
if (left >= right) return RESULT_NORMAL;
/* 判断是否有管道命令 */
int pipeIdx = -1;
for (int i=left; i<right; ++i) {
if (strcmp(commands[i], COMMAND_PIPE) == 0) {
pipeIdx = i;
break;
}
}
if (pipeIdx == -1) { // 不含有管道命令
return callCommandWithRedi(left, right);
} else if (pipeIdx+1 == right) { // 管道命令'|'后续没有指令,参数缺失
return ERROR_PIPE_MISS_PARAMETER;
}
/* 执行命令 */
int fds[2];
if (pipe(fds) == -1) {
return ERROR_PIPE;
}
int result = RESULT_NORMAL;
pid_t pid = vfork();
if (pid == -1) {
result = ERROR_FORK;
} else if (pid == 0) { // 子进程执行单个命令
close(fds[0]);
dup2(fds[1], STDOUT_FILENO); // 将标准输出重定向到fds[1]
close(fds[1]);
result = callCommandWithRedi(left, pipeIdx);
exit(result);
} else { // 父进程递归执行后续命令
int status;
waitpid(pid, &status, 0);
int exitCode = WEXITSTATUS(status);
if (exitCode != RESULT_NORMAL) { // 子进程的指令没有正常退出,打印错误信息
char info[4096] = {0};
char line[BUF_SZ];
close(fds[1]);
dup2(fds[0], STDIN_FILENO); // 将标准输入重定向到fds[0]
close(fds[0]);
while(fgets(line, BUF_SZ, stdin) != NULL) { // 读取子进程的错误信息
strcat(info, line);
}
printf("%s", info); // 打印错误信息
result = exitCode;
} else if (pipeIdx+1 < right){
close(fds[1]);
dup2(fds[0], STDIN_FILENO); // 将标准输入重定向到fds[0]
close(fds[0]);
result = callCommandWithPipe(pipeIdx+1, right); // 递归执行后续指令
}
}
return result;
}
4.8 可处理重定向的callCommandWithRedi()函数
函数首先判断指令是否存在。若指令不存在,则直接返回错误信息,不需要再继续执行。
当指令存在时,先判断是否有合法的重定向,再进行下一步处理。同样,在子进程中执行程序。C语言对于重定向的处理,可以使用文件读写的方式,也可以使用其它。为了使得代码比较简洁,我使用freopen()这一神器来实现输入、输出重定向。随后使用execvp()函数执行命令。若执行失败,则会把错误编号存在errno中,返回errno;若执行成功,则会返回0。
父进程等待子进程结束,并读取子进程的返回码。若不为0,则使用strerror()函数获取对应的错误信息,并打印到控制台。
int callCommandWithRedi(int left, int right) { // 所要执行的指令区间[left, right),不含管道,可能含有重定向
if (!isCommandExist(commands[left])) { // 指令不存在
return ERROR_COMMAND;
}
/* 判断是否有重定向 */
int inNum = 0, outNum = 0;
char *inFile = NULL, *outFile = NULL;
int endIdx = right; // 指令在重定向前的终止下标
for (int i=left; i<right; ++i) {
if (strcmp(commands[i], COMMAND_IN) == 0) { // 输入重定向
++inNum;
if (i+1 < right)
inFile = commands[i+1];
else return ERROR_MISS_PARAMETER; // 重定向符号后缺少文件名
if (endIdx == right) endIdx = i;
} else if (strcmp(commands[i], COMMAND_OUT) == 0) { // 输出重定向
++outNum;
if (i+1 < right)
outFile = commands[i+1];
else return ERROR_MISS_PARAMETER; // 重定向符号后缺少文件名
if (endIdx == right) endIdx = i;
}
}
/* 处理重定向 */
if (inNum == 1) {
FILE* fp = fopen(inFile, "r");
if (fp == NULL) // 输入重定向文件不存在
return ERROR_FILE_NOT_EXIST;
fclose(fp);
}
if (inNum > 1) { // 输入重定向符超过一个
return ERROR_MANY_IN;
} else if (outNum > 1) { // 输出重定向符超过一个
return ERROR_MANY_OUT;
}
int result = RESULT_NORMAL;
pid_t pid = vfork();
if (pid == -1) {
result = ERROR_FORK;
} else if (pid == 0) {
/* 输入输出重定向 */
if (inNum == 1)
freopen(inFile, "r", stdin);
if (outNum == 1)
freopen(outFile, "w", stdout);
/* 执行命令 */
char* comm[BUF_SZ];
for (int i=left; i<endIdx; ++i)
comm[i] = commands[i];
comm[endIdx] = NULL;
execvp(comm[left], comm+left);
exit(errno); // 执行出错,返回errno
} else {
int status;
waitpid(pid, &status, 0);
int err = WEXITSTATUS(status); // 读取子进程的返回码
if (err) { // 返回码不为0,意味着子进程执行出错,用红色字体打印出错信息
printf("\e[31;1mError: %s\n\e[0m", strerror(err));
}
}
return result;
}5 结尾
以上就是简易shell程序的相关内容啦,如果有发现什么问题,欢迎大家一起探讨。