✨个人主页: 熬夜学编程的小林
💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】
目录
1、简易的shell
1.1、输出一个命令行
1.2、获取用户命令字符串
1.3、命令行字符串分割
1.4、检查命令是否是内建命令
1.5、执行命令
1.6、完整代码
1、简易的shell
考虑下面这个与shell典型的互动:
[jkl@host shell]$ ls
makefile myshell myshell.c
[jkl@host shell]$ ps
PID TTY TIME CMD
20980 pts/0 00:00:00 bash
26709 pts/0 00:00:00 ps
用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。
然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序并等待这个进程结束。
所以要写一个shell,需要循环以下过程:
- 1. 输出一个命令行
- 2. 获取命令行
- 3. 解析命令行
- 4. 检查是否为内建命令
- 5. 建立一个子进程(fork)
- 6. 替换子进程(execvp)
- 7. 父进程等待子进程退出(wait)
根据这些思路,和我们前面的学的技术,就可以自己来实现一个shell了。
会用到的头文件
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<errno.h>
1.1、输出一个命令行
命令行的内容包含用户名,主机名,当前路径。
环境变量中包含三种信息,因此我们可以使用获取环境变量的 getenv() 函数获取。
#include <stdlib.h>
char *getenv(const char *name);
获取到三个字符串的内容之后,我们需要将三个字符串合并成一个字符串,此时我们可以用到snprintf()函数。
#include<stdio.h>
int snprintf(char *str, size_t size, const char *format, ...);
代码演示
#define SIZE 512
const char* GetUserName()
{
const char* name = getenv("USER");
if(name == NULL) return "None";
return name;
}
const char* GetHostName()
{
const char* hostname = getenv("HOSTNAME");
if(hostname == NULL) return "None";
return hostname;
}
const char* GetCwd()
{
const char* cwd = getenv("PWD");
if(cwd == NULL) return "None";
return cwd;
}
void MakeCommandLineAndPrint()
{
char line[SIZE];
// 用户名@主机名 当前路径
const char* username = GetUserName();
const char* hostname = GetHostName();
const char* cwd = GetCwd();
snprintf(line,sizeof(line),"[%s@%s %s]> ",username,hostname,cwd);
printf("%s",line);
sleep(2);
}
int main()
{
// 1、自己输出一个命令行
MakeCommandLineAndPrint();
return 0;
}
运行结果
两个问题:1、程序结束才刷新缓冲区(刷新缓冲区即可) 2、打印的是绝对路径(实现一个算法即可)
优化
// 找最后一个/ ,宏是替换可以不用传二级指针,do while 不加分号,为了后面加分号
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)
// command : output
void MakeCommandLineAndPrint()
{
char line[SIZE];
// 用户名@主机名 当前路径
const char* username = GetUserName();
const char* hostname = GetHostName();
const char* cwd = GetCwd();
SkipPath(cwd);// 处理绝对路径问题
// 需要处理第一个/,长度为1用/
snprintf(line,sizeof(line),"[%s@%s %s]> ",username,hostname,strlen(cwd) == 1 ? "/" : cwd + 1);
printf("%s",line);
fflush(stdout);// 处理缓冲区问题
}
运行结果
1.2、获取用户命令字符串
从标准输入流中获取字符串,并存放在数组中。
此处需要用到 fgets() , strlen()函数。
#include<stdio.h>
char *fgets(char *s, int size, FILE *stream);
计算字符串长度
#include <string.h>
size_t strlen(const char *s);
代码演示
#define ZERO '\0'
#define SIZE 512
int GetUserCommand(char command[],size_t n)
{
char* s = fgets(command,n,stdin);
if(s == NULL) return -1;// 字符串为空直接返回
command[strlen(command)-1]=ZERO;// 设置结尾标志
return strlen(command);// 返回字符串长度
}
int main()
{
// 1、自己输出一个命令行
MakeCommandLineAndPrint();
2、获取用户命令字符串
char usercommand[SIZE];
int n = GetUserCommand(usercommand,sizeof(usercommand));
if(n<=0) return 1;// 没有获取到字符串则结束程序
printf("%s\n",usercommand);// 打印命令行字符串
return 0;
}
运行结果
1.3、命令行字符串分割
将以空格分割的字符串,分割成全部是单独的字符串。
此处需要用到 strtok() 函数。
#include<string.h>
char *strtok(char *str, const char *delim);
代码演示
#define SEP " "
char* gArgv[NUM];
void SplitCommand(char command[])
{
// "ls -a -l -n" -> "ls" "-a" "-l" "-n"
gArgv[0]=strtok(command,SEP);
int index = 1;
// 故意写成赋值,表示先赋值再判断,分割之后返回NULL,刚好让gArgv的最后一个元素为空,循环结束
while((gArgv[index++] = strtok(NULL,SEP)));
}
int main()
{
// 1、自己输出一个命令行
MakeCommandLineAndPrint();
//2、获取用户命令字符串
char usercommand[SIZE];
int n = GetUserCommand(usercommand,sizeof(usercommand));
if(n<=0) return 1;// 没有获取到字符串则结束程序
printf("%s\n",usercommand);
// 3、命令行字符串分割
SplitCommand(usercommand);
// 打印分割之后的字符串
for(int i=0;gArgv[i];i++)
{
printf("%s\n",gArgv[i]);
}
return 0;
}
运行结果
1.4、检查命令是否是内建命令
判断gArgv[0]是否是内建命令,是内建命令则做特殊处理。
此处需要用到 strcmp() chdir() getcwd() snprintf() putenv() 函数。
#include <string.h>
int strcmp(const char *s1, const char *s2);
比较两个字符串是否相同,相同返回0。
#include <unistd.h>
int chdir(const char *path);
修改当前工作目录。
#include <unistd.h>
char *getcwd(char *buf, size_t size);
获取当前工作目录。
#include <stdlib.h>
int putenv(char *string);
添加环境变量。
代码演示
#define SIZE 512
char* gArgv[NUM];
char cwd[SIZE*2];
int lastcode = 0;
const char* GetHome()
{
const char* home = getenv("HOME");
if(home == NULL) return "None";
return home;
}
void Cd()
{
const char* path = gArgv[1];
if(path == NULL) path = GetHome();
// path 一定存在
chdir(path);// 修改当前工作目录
// 刷新环境变量
char temp[SIZE*2];
getcwd(temp,sizeof(temp));// 将工作目录保存到temp中
snprintf(cwd,sizeof(cwd),"PWD=%s",temp);// 以格式化形式将PWD=目录保存到cwd中
putenv(cwd);// 将cwd导入环境变量
}
int CheckBuildin()
{
int yes = 0;
const char* enter_cmd = gArgv[0];
if(strcmp(enter_cmd,"cd") == 0)
{
yes = 1;
Cd();
}
else if(strcmp(enter_cmd,"echo") == 0 && strcmp(gArgv[1],"$?") == 0)
{
yes = 1;
printf("%d\n",lastcode);
lastcode = 0;
}
return yes;
}
int main()
{
// 1、自己输出一个命令行
MakeCommandLineAndPrint();
//2、获取用户命令字符串
char usercommand[SIZE];
int n = GetUserCommand(usercommand,sizeof(usercommand));
if(n<=0) return 1;// 没有获取到字符串则结束程序
printf("%s\n",usercommand);
// 3、命令行字符串分割
SplitCommand(usercommand);
for(int i=0;gArgv[i];i++)
{
printf("%s\n",gArgv[i]);
}
// 4、检测命令是否是内建命令
n = CheckBuildin();// 不是0则是内建命令
printf("n = %d\n",n);
return 0;
}
运行结果
1.5、执行命令
创建子进程,使用进程替换,让子进程执行命令。
此处需要用到 fork() execvp() exit() waitpid() strerror() 函数。
#include <unistd.h>
pid_t fork(void);
创建子进程,子进程返回0,父进程返回子进程pid。
#include<unistd.h>
int execvp(const char *file, char *const argv[]);
进程替换(只需传文件名,argv为命令行参数表)。
#include <stdlib.h>
void exit(int status);
退出进程。
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
等待子进程。
#include <string.h>
char *strerror(int errnum);
根据错误码打印错误信息。
errno为全局的错误码变量。
代码演示
char* gArgv[NUM];
int lastcode = 0;
void Die()
{
exit(1);
}
void ExecuteCommand()
{
pid_t id = fork();
if(id < 0) Die();
else if(id == 0)
{
// child 程序替换
execvp(gArgv[0],gArgv);
exit(errno);
}
else
{
// father
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
if(lastcode != 0) printf("%s:%s:%d\n",gArgv[0],strerror(lastcode),lastcode);
}
}
}
运行结果
命令行是能够一直解释命令的,因此我们还需要加一个循环,在完整代码里面。
1.6、完整代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<errno.h>
#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32
// 找最后一个/ ,宏是替换可以不用传二级指针,do while 不加分号,为了后面加分号
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)
char* gArgv[NUM];
int lastcode = 0;
char cwd[SIZE*2];
const char* GetUserName()
{
const char* name = getenv("USER");
if(name == NULL) return "None";
return name;
}
const char* GetHostName()
{
const char* hostname = getenv("HOSTNAME");
if(hostname == NULL) return "None";
return hostname;
}
const char* GetCwd()
{
const char* cwd = getenv("PWD");
if(cwd == NULL) return "None";
return cwd;
}
const char* GetHome()
{
const char* home = getenv("HOME");
if(home == NULL) return "None";
return home;
}
// command : output
void MakeCommandLineAndPrint()
{
char line[SIZE];
// 用户名@主机名 当前路径
const char* username = GetUserName();
const char* hostname = GetHostName();
const char* cwd = GetCwd();
SkipPath(cwd);
// 需要处理第一个/,长度为1用/
snprintf(line,sizeof(line),"[%s@%s %s]> ",username,hostname,strlen(cwd) == 1 ? "/" : cwd + 1);
printf("%s",line);
fflush(stdout);
}
int GetUserCommand(char command[],size_t n)
{
char* s = fgets(command,n,stdin);
if(s == NULL) return -1;
command[strlen(command)-1]=ZERO;
return strlen(command);
}
void SplitCommand(char command[])
{
// "ls -a -l -n" -> "ls" "-a" "-l" "-n"
gArgv[0]=strtok(command,SEP);
int index = 1;
// 故意写成赋值,表示先赋值再判断,分割之后返回NULL,刚好让gArgv的最后一个元素为空,循环结束
while((gArgv[index++] = strtok(NULL,SEP)));
}
void Die()
{
exit(1);
}
void ExecuteCommand()
{
pid_t id = fork();
if(id < 0) Die();
else if(id == 0)
{
// child 程序替换
execvp(gArgv[0],gArgv);
exit(errno);
}
else
{
// father
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
if(lastcode != 0) printf("%s:%s:%d\n",gArgv[0],strerror(lastcode),lastcode);
}
}
}
void Cd()
{
const char* path = gArgv[1];
if(path == NULL) path = GetHome();
// path 一定存在
chdir(path);
// 刷新环境变量
char temp[SIZE*2];
getcwd(temp,sizeof(temp));
snprintf(cwd,sizeof(cwd),"PWD=%s",temp);
putenv(cwd);
}
int CheckBuildin()
{
int yes = 0;
const char* enter_cmd = gArgv[0];
if(strcmp(enter_cmd,"cd") == 0)
{
yes = 1;
Cd();
}
else if(strcmp(enter_cmd,"echo") == 0 && strcmp(gArgv[1],"$?") == 0)
{
yes = 1;
printf("%d\n",lastcode);
lastcode = 0;
}
return yes;
}
int main()
{
int quit = 0;
while(!quit)
{
// 1、自己输出一个命令行
MakeCommandLineAndPrint();
// 2、获取用户命令字符串
char usercommand[SIZE];
int n = GetUserCommand(usercommand,sizeof(usercommand));
if(n<=0) return 1;
// 3、命令行字符串分割
SplitCommand(usercommand);
// 4、检测命令是否是内建命令
n = CheckBuildin();
if(n) continue;
// 5、执行命名
ExecuteCommand();
}
return 0;
}
运行结果