目录
一、前言
[root@hecs-215580 cpp]# ls
1,17.cpp
[root@hecs-215580 cpp]# ps
PID TTY TIME CMD
24190 pts/0 00:00:00 bash
24493 pts/0 00:00:00 ps
上面是在Xshell上的经典基本互动和指令操作。
![](https://i-blog.csdnimg.cn/blog_migrate/8ea7f0adf4ab1a3b2a3a55032e56572e.png)
二、shell模拟实现
框架编写
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define SIZE 1024
#define MAX_ARGC 64
#define SEP " "
char *argv[MAX_ARGC];//方便代码运行时父子进程都可以看到
char pwd[SIZE];
char env[SIZE]; // for test
int lastcode = 0;
int main()
{
while(1)
{
char commandline[SIZE];
// 1. 打印命令行提示符,获取用户输入的命令字符串
int n = Interactive(commandline, SIZE);
if(n == 0) continue;
// 2. 对命令行字符串进行切割
Split(commandline);
// 3. 处理内建命令
n = BuildinCmd();
if(n) continue;
// 4. 执行这个命令
Execute();
}
// for(int i=0; argv[i]; i++)
// {
// printf("argv[%d]: %s\n", i, argv[i]);
// }
return 0;
}
首先包含所需要用到的头文件,创建需要用到的全局变量,并创建主函数main,将shell所具有的功能进行函数封装,然后来逐步实现。
2.1打印命令行提示符,获取用户输入命令字符串
const char* HostName()
{
char *hostname = getenv("HOSTNAME");
if(hostname) return hostname;
else return "None";
}
const char* UserName()
{
char *hostname = getenv("USER");
if(hostname) return hostname;
else return "None";
}
const char *CurrentWorkDir()
{
char *hostname = getenv("PWD");
if(hostname) return hostname;
else return "None";
}
int Interactive(char out[], int size)
{
// 输出提示符并获取用户输入的命令字符串"ls -a -l"
printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
fgets(out, size, stdin);
out[strlen(out)-1] = 0; //'\0', commandline是空串的情况?
return strlen(out);
}
通过getenv获取到当前用户的信息并打印。
而在获取用户命令行指令时,一条指令里往往都包含有空格,所以用scanf就会导致缓冲区被刷新导致拿不到完整的命令字符串,所以此处用fgets更好一些,使用fgets获取到字符串后,编译器默认会自动在字符串最后添加\0。
因为在输入完成后需要回车来换行结束,所以回车也会被fgets获取,所以最后一个字符就是\n,从而导致在打印时多打印出一个空行,所以在获取字符串后通过strlen计算长度,并把\n改为0,就避免了多打印空行的问题。
2.2对命令行字符串进行切割
void Split(char in[])
{
int i = 0;
argv[i++] = strtok(in, SEP); // "ls -a -l" SEP为空格
while(argv[i++] = strtok(NULL, SEP));
// 故意将== 写成 = 切割完成后将argv再往后一个成员赋值为null
//注:为方便满足使用其他函数接口的要求,所以将argv最后一个成员赋值为null
//最后while判断argv条件不满足推出循环
if(strcmp(argv[0], "ls") ==0)
{
argv[i-1] = (char*)"--color";
argv[i] = NULL;
}
}
char *strtok(char s[], const char *delim);
这里用strtok函数分解字符串。s为要分解的目标字符传,delim为分隔符字符(如果传入字符串,则传入的字符串中每个字符均为分割符)。首次调用时,s指向要分解的字符串,之后再次调用要把s设成NULL。在头文件#include<string.h>中。
当strtok()在s字符串中找到参数delim中所包含的分割字符时,则会将该字符改为\0 字符。在第一次调用时,strtok()必需给予参数s字符串,往后的调用继续分割历史字符串则需要将参数s设置成NULL。继续往后分割。
从s开头开始的一个个被分割的串。当s中的字符查找到末尾时,返回NULL。如果查找不到delim中的字符时,返回当前strtok的字符串的指针。所有delim中包含的字符都会被滤掉,并将被滤掉的地方设为一处分割的节点。
将用户输入的指令行以空格作为分割进行分割后,将分割出的命令的指针依次存入argv指针数组中。
2.3执行命令
void Execute()
{
pid_t id = fork();
if(id == 0)//通过id判断是否为子进程
{
// 让子进程执行命令
execvp(argv[0], argv);
exit(1);
}
//父进程
int status = 0;
pid_t rid = waitpid(id, &status, 0);//等待子进程结束
if(rid == id) lastcode = WEXITSTATUS(status); //获取退出码
}
因为命令行shell的运行是持续性的,所以不能让shell自己去进行程序替换,所以需要fork创建子进程来不断进行单次命令的执行。
2.4处理内建命令
像cd,echo,export这种内建命令单纯让子进程直接去执行是行不通的,只会改变子进程的路径,而子进程在执行完毕后就会进行销毁,父进程无法直接打印出切换后的用户路径,需要bash自己去切换。
int BuildinCmd()
{
int ret = 0;
// 1. 检测是否是内建命令, 是 1, 否 0
if(strcmp("cd", argv[0]) == 0)
{
// 2. 执行
ret = 1;
char *target = argv[1]; //cd XXX or cd
if(!target) target = Home();
chdir(target);
char temp[1024];
getcwd(temp, 1024);//获取当前路径
snprintf(pwd, SIZE, "PWD=%s", temp);//用绝对路径更新环境变量
//snprintf:按照指定格式,把指定内容格式化写入到指定长度的字符串中
putenv(pwd);//导入到环境变量中
}
else if(strcmp("export", argv[0]) == 0)
{
ret = 1;
if(argv[1])
{
strcpy(env, argv[1]);
putenv(env);
}
}
else if(strcmp("echo", argv[0]) == 0)
{
ret = 1;
if(argv[1] == NULL) {
printf("\n");
}
else{
if(argv[1][0] == '$')
{
if(argv[1][1] == '?')
{
printf("%d\n", lastcode);//打印退出码
lastcode = 0;
}
else{
char *e = getenv(argv[1]+1);
if(e) printf("%s\n", e);
}
}
else{
printf("%s\n", argv[1]);
}
}
}
return ret;
}