目录
一.全部代码
cpp
#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;
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";
}
char *Home()
{
return getenv("HOME");
}
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);
}
void Split(char in[])
{
int i = 0;
argv[i++] = strtok(in, SEP); // "ls -a -l"
while(argv[i++] = strtok(NULL, SEP)); // 故意将== 写成 =
if(strcmp(argv[0], "ls") ==0)
{
argv[i-1] = (char*)"--color";
argv[i] = NULL;
}
}
void Execute()
{
pid_t id = fork();
if(id == 0)
{
// 让子进程执行命名
execvp(argv[0], argv);
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id) lastcode = WEXITSTATUS(status);
//printf("run done, rid: %d\n", rid);
}
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);
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;
}
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;
}
二.自定义shell
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define SIZE 1024
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 main()
{
// 输出提示符并获取用户输入的命令字符串"ls -a -l"
printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
char commandline[SIZE];
scanf("%s", commandline);
printf("test: %s\n", commandline);
return 0;
}
首先我们先来模拟一下最开始的命令行及其输入~
已经有点味道了~ 就是功能有点单一,只能输入输出~
我们再执行一次模拟写入指令后发现当前缓冲区只能获取ls,后面遇到空格就刷新出来了。那要如何获取完整的指令呢?(不获取到完整的指令又怎么知道用户具体要干啥呢?)
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define SIZE 1024
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 main()
{
// 输出提示符并获取用户输入的命令字符串"ls -a -l"
printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
char commandline[SIZE];
fgets(commandline,SIZE,stdin);
commandline[strlen(commandline)-1] = 0;
printf("test: %s\n", commandline);
return 0;
}
这里我们借助函数fgets来获取行内容~因为fgets结尾会自带/n(换行),所以我们把结尾处的'/n'给换掉~
既然能够获取完整的输入命令,接下来我们就需要以空格为分隔符拆除若干个字符串存放在argv数组中~
cpp
#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 " "
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 main() {
// 输出提示符并获取用户输入的命令字符串"ls -a -l"
printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
char commandline[SIZE];
fgets(commandline,SIZE,stdin);
commandline[strlen(commandline)-1] = 0;
//对命令行字符串进行切割
char* argv[MAX_ARGC];
int i = 0;
argv[i++] = strtok(commandline,SEP);
while(argv[i++]=strtok(NULL,SEP));
for(int i = 0;argv[i];i++)
{
printf("argv[%d]: %s\n",i,argv[i]);
}
return 0;
}
我们利用strtok进行字符串按空格(SEP)进行分割,若要继续分割则需要把参数换成NULL~
然后利用分割到最后剩下NULL的特性把所有输入命令都挨个存储到argv数组中~
cpp
#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 " "
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 main() {
// 输出提示符并获取用户输入的命令字符串"ls -a -l"
printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
char commandline[SIZE];
fgets(commandline,SIZE,stdin);
commandline[strlen(commandline)-1] = 0;
//对命令行字符串进行切割
char* argv[MAX_ARGC];
int i = 0;
argv[i++] = strtok(commandline,SEP);
while(argv[i++]=strtok(NULL,SEP));
//执行命令
pid_t id = fork();
if(id == 0)
{ // 让子进程执行命名
execvp(argv[0], argv);
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, NULL, 0);
printf("run done,rid:%d\n", rid);
return 0;
}
我们创建一个子进程,然后把存储好命名行指令的数组argv通过execvp函数进行进程替换~
就好比我们输入了ls -a -l,那么它们就会作为替换进程所需要执行的参数,通过这些参数找到真正的ls -a -l指令进而去调用~
最后再套上无限循环就可以一直使用我们的自定义shell了~
我们发现了一个问题:为什么cd指令无法生效呢? 明明其他命令都生效了~
因为我们这些命令最终都是子进程在执行的,所以当前执行cd命令的是子进程而不是当前的bash,
而子进程一执行就退出了,切换路径还有什么意义?所以是要让bash去切换才有用,然后我们才能看到路径的变化~
这种命令称为内建命令,不需要由子进程去执行,让其父进程去执行~
我们对内建指令cd进行特殊处理,如何获取到的指令中有cd那就标记为内建指令,并记录后面要去的路径,然后用chdir改变子进程的路径。若无则为NULL,cd直接到家目录下。
不过当前有一个问题,用pwd可以查到更新后的路径,但shell中打印的路径却没有更新~然后就是直接按回车会有空串的问题~
我们记录获取字符串的长度,如果空串0那就不要继续执行了~
至于路径变化我们只需要做到环境变量的更新就好了~
我们设置一个大小为SIZE的数组pwd,用来存储环境变量PWD~
通过snprintf函数把获取到的路径存储进pwd数组中,再通过putenv把这个环境变量进行更新或添加~
不过还是有点瑕疵,当执行cd ..的时候路径也会变成 .. 但这个明明是上一级路径~
我们可以借助函数getcwd来获取当前路径,因为路径我们已经切换过了,所以根据获取更新后的路径作为环境变量~
为什么我们在自定义shell中导入新的环境变量会看不到呢?因为子进程会退出,没必要~所以export也是内建命令
如果这样修改会发现env只有在第一次才可以查到新的环境变量,后面就看不到了。
因为argv是每一次都要去获取指令的,你虽然在这一次输入指令export被成功导入,但下一次的命令就没有了,这样就会覆盖argv,argv表又回归原状,找不到原来有export指令的表了,自然就找不到环境变量了
所以我们再定义一个关于环境变量的数组emv,然后导出的时候有env这个表在就不会因为argc覆盖问题而看不了环境变量了
我们再来获取一下进程的退出码~
cpp
void Execute()
{
pid_t id = fork();
if(id == 0)
{
// 让子进程执行命名
execvp(argv[0], argv);
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id) lastcode = WEXITSTATUS(status);
//printf("run done, rid: %d\n", rid);
}
然后对内建命令echo进行编写
cpp
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]);
}
}
}