1. 目前实现的功能
- 打印仿Shell的命令行提示符,有当前的工作目录
- 基本Shell命令的执行
- 重定向符的执行
- 多管道符的执行
- 内建命令:cd
2. 动图预览
3. 源码
#include <stdio.h>
#include <stdlib.h> // getenv()
#include <unistd.h> // usleep() duip2() fork()
#include <string.h> // memset()
#include <ctype.h> // isspace()
#include <fcntl.h> // open()
#include <sys/wait.h> // waitpid()
// 打印命令行提示符
void print_prompt() {
// 登陆用户名通过环境变量 LOGNAME 得到
char* username = getenv("LOGNAME");
// 当前路径使用popen执行命令行命令pwd来获得
char* pwd = (char *)malloc(sizeof(char) * 1024);
FILE* fp = popen("pwd", "r");
fscanf(fp, "%s", pwd);
printf("%s@%s$ ", username, pwd);
fflush(stdout);
free(pwd);
}
// 获取命令行输入
char* get_command() {
char* input = (char *)malloc(sizeof(char) * 1024);
memset(input, 0, 1024);
// %[^\n] : 从标准输入缓冲区取数据直到遇到\n
// %*c : 从缓冲区取出一个字符并丢掉
// 目的是消耗掉剩余的\n
if(scanf("%[^\n]%*c", input) != 1) { // scanf 返回成功读取到了几个格式化数据,即几个%s之类
getchar();
}
return input;
}
// 解析输入中是否有重定向符
// redirect为输出型参数,0表示没有,1表示覆盖重定向,2表示追加重定向
// 返回值为重定向后的文件名
char* parse_redirection(char* input, int* redirect) {
char* ptr = input;
*redirect = 0;
char* filename = (char *)malloc(sizeof(char) * 1024);
memset(filename, 0, 1024);
while(*ptr != '\0' && *ptr != '>') {
ptr++;
}
if(*ptr == '>') { // >
*redirect = 1;
*ptr++ = '\0';
if(*ptr == '>') { // >>
*redirect = 2;
*ptr++ = '\0';
}
// parse file name
while(*ptr != '\0' && isspace(*ptr)) {
ptr++;
}
if(*ptr == '\0') { // there is no file name
*redirect = 0;
return NULL;
}
else {
filename = ptr;
while(*ptr != '\0' && !isspace(*ptr)) {
ptr++;
}
*ptr = '\0';
}
return filename;
}
return NULL;
}
// 解析输入中的命令部分的所有参数,返回二级指针argv,表示所有参数
char** parse_command(char* input) {
// 申请命令行参数数组,大小为32
char** argv = (char **)malloc(sizeof(char *) * 32);
int argc = 0;
char* ptr = input;
while(*ptr != '\0') {
if(isspace(*ptr)) {
*ptr = '\0';
ptr++;
}
else {
argv[argc++] = ptr;
// 跳过这个参数
while(*ptr != '\0' && !isspace(*ptr)) {
ptr++;
}
// 此时ptr要么指向一个空白字符,要么'\0'
}
}
argv[argc] = NULL;
return argv;
}
// 将输入根据管道符分割成不同的命令
int divide_input(char* input, char* commands[]) {
int cnt = 0;
char* ptr = input;
commands[cnt++] = ptr++;
while(*ptr != '\0') {
if(*ptr == '|') {
*ptr++ = '\0';
commands[cnt++] = ptr;
continue;
}
ptr++;
}
return cnt;
}
// 内置命令cd的实现
void built_in_cd(const char* path) {
if(chdir(path) == -1) {
perror("chdir error");
}
}
int main() {
int ret; // 承载所有系统调用的返回值
while(1) {
// print prompt
print_prompt();
// get_command
char* input = get_command();
if(strlen(input) == 0) { // 如果用户只输入了一个回车
continue;
}
// 根据管道符将命令分割成不同的子命令
char** commands = (char **)malloc(sizeof(char*) * 10);
int cnt = divide_input(input, commands); // cnt表示子命令的个数
// 申请多个不同命令间通信用的管道,暂时一次性申请十个
int pipefd[10][2] = { { 0 } };
for(int i = 0; i < cnt - 1; i++) { // cnt表示子命令的个数,所以只需要cnt-1个管道
pipe(pipefd[i]);
}
// 循环cnt次执行所有cnt个子命令
for(int i = 0; i < cnt; i++) {
// 解析当前子命令中是否有重定向符,如果有就在函数内部做好重定向的相关操作
// 可知如果既有管道符又有重定向符,则按我的代码的逻辑最终标准输入或标准输出会
// 重定向到文件上而不是管道上,最终管道符会失效。
int redirect;
char* filename = parse_redirection(commands[i], &redirect);
// 将当前子命令拆分成不同的命令行参数
char** argv = parse_command(commands[i]);
// 处理Shell内建命令,现已实现 cd
if(!strcmp(argv[0], "cd")) {
built_in_cd(argv[1]);
continue;
}
// 创建子进程
pid_t pid = fork();
if(pid < 0) {
perror("fork error");
exit(-1);
}
else if(pid == 0) {
// 子进程中首先处理重定向符
int fd;
if(redirect == 1) {
fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
dup2(fd, 1);
}
else if(redirect == 2) {
fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
dup2(fd, 1);
}
// 第二步处理管道符
if(i != 0) {
// 将子进程的标准输入重定向到上一对管道读端
ret = dup2(pipefd[i - 1][0], STDIN_FILENO);
if(ret == -1) {
perror("dup2 error");
exit(-1);
}
// 关闭上一对管道不需要的写端
close(pipefd[i - 1][1]);
}
if(i != cnt - 1) {
// 将子进程的标准输出重定向到这一对管道的写端
ret = dup2(pipefd[i][1], STDOUT_FILENO);
if(ret == -1) {
perror("dup2 error");
exit(-1);
}
// 关闭这一对管道不需要的读端
close(pipefd[i][0]);
}
// 执行程序替换
ret = execvp(argv[0], argv);
if(ret == -1) {
perror("execvp error");
exit(-1);
}
}
else {
// 关闭父进程的管道。注意,不能在fork()第一个子进程后就关闭父进程第一对管道,这样做的话
// 第一个子进程退出后第一对管道就丢失了,而fork()的第二个子进程还需要从第一对管道中读取
// 输入。所以正确的关闭逻辑应该是在fork()第二个子进程后才关闭第一对管道。这样在父进程中
// 关闭多余管道的目的是为了让grep等阻塞命令退出,关闭所有管道的写端,grep的读操作就会立
// 刻退出。
if(i != 0) {
close(pipefd[i - 1][0]);
close(pipefd[i - 1][1]);
}
waitpid(pid, NULL, 0);
}
}
}
}