在使用Linux的过程中,相信大家最熟悉的就是Linux的命令行使用方式了,我们可以给命令行输入任意有效指令, 然后命令行会根据我们输入的指令来完成相应的操作。 今天我们尝试在Linux使用C语言自己实现一个简单的shell命令行程序,它可以像真的命令行那样执行命令, 与程序员交互, 话不多说, 先来看看实现效果吧: myshell功能测试
用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。
然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。所以要写一个shell,需要循环以下过程:
该部分只讲功能实现的代码逻辑, 故可能不会包含宏定义和全局变量等实现细节,如需完整的项目代码,请移步本文第四部分.
我们将获取命令行做成一个循环,除非用户主动退出,否则一直保持命令行接收指令的状态:
int main()
{
while(!quit)
{
//2.交互问题,获取命令行内容
interact(commandline,sizeof(commandline));
//3.分割命令字符串strtok(),解析命令行
int argc = splitstring(commandline, argv);
if(argc == 0) continue;
//4.指令的判断
int n = buildCommand(argv,argc);
//5.普通命令的执行
if(!n) NormalExcute(argv);
}
return 0;
}
具体的获取命令行逻辑如下函数:
const char* getusername()
{
//通过getenv()获取环境变量中的用户名
return getenv("USER");
}
void getpwd()
{
//通过getcwd系统接口获取并更新pwd
getcwd(pwd,sizeof(pwd));
}
void interact(char *cline, int size)
{
//需要环境变量相关的系统调用函数来获取命令行提示信息
//获取主机名
char hostname[64];
gethostname(hostname,sizeof(hostname));
//1.打印bash命令行前面的提示信息
getpwd();
printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),hostname,pwd);
//2.接收用户输入信息
fgets(cline, size,stdin);
assert(cline != NULL);
(void)cline; //防止编译器报错定义而未使用的变量(假装用一下)
cline[strlen(cline)-1] = '\0';
}
解析命令行主要就是将获取到的字符串按空格切分开来放入一个新数组中,我们使用strtok()来完成这个工作, 具体实现代码如下:
int splitstring(char cline[], char *_argv[])
{
int i = 0;
_argv[i++] = strtok(cline, DELIM);
while(_argv[i++] = strtok(NULL, DELIM));
return i-1;
}
我们虽然可以借助fork()创建子进程来代替我们实现诸多普通命令, 但是对于很多内建命令来说, 创建子进程执行命令的结果并不会影响父进程, 这会导致父进程命令无效, 因此对于内建命令我们要先判断,再让父进程自主完成这些内建命令, 代码如下:
int buildCommand(char *_argv[],int _argc)
{
//4.指令的判断
//cd命令
if(_argc == 2 && strcmp(_argv[0],"cd") == 0)
{
//更改目录
chdir(_argv[1]);
getpwd();
//更改环境变量
sprintf(getenv("PWD"),"%s",pwd);
return 1;
}
//export命令
else if(_argc == 2 && strcmp(_argv[0],"export") == 0)
{
//因为_argv一直被我们用来存新的指令,环境变量会因此被覆盖
//所以需要一个固定的存环境变量的地方来保存环境变量
strcpy(myenv,_argv[1]);
putenv(myenv);
return 1;
}
//echo命令
else if(_argc == 2 && strcmp(_argv[0],"echo") == 0)
{
if(strcmp(_argv[1],"$?") == 0)
{
printf("%d\n",lastcode);
lastcode = 0;
}
else if(*_argv[1] == '$')
{
char *val = getenv(_argv[1]+1);
if(val) printf("%s\n",val);
}
else
{
printf("%s\n",_argv[1]);
}
return 1;
}
//ls命令
if(strcmp(_argv[0],"ls") == 0)
{
_argv[_argc++] = "--color";
_argv[_argc] = NULL;
}
return 0;
}
普通命令的执行不会影响父进程,因此我们可以使用fork()创建子进程,然后使用exec*系列进程替换函数来完成相关操作, 代码如下:
void NormalExcute(char *_argv[])
{
//5.普通命令的执行
pid_t id = fork();
if(id < 0)
{
perror("fork error");
return;
}
else if(id == 0)
{
//让子进程执行普通命令
//execvpe(_argv[0],_argv,environ);
execvp(_argv[0],_argv);
exit(EXIT_CODE);
}
else
{
//父进程等待子进程返回值
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid == id)
{
lastcode = WEXITSTATUS(status);
}
}
}
完整项目代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#define LEFT "["
#define RIGHT "]"
#define LABLE "$"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 55
int lastcode = 0;
int quit = 0;
char commandline[LINE_SIZE];
char *argv[ARGC_SIZE];
char pwd[LINE_SIZE];
//自定义环境变量表,做成二维数组就需要维护了
char myenv[LINE_SIZE];
//自定义本地变量表
const char* getusername()
{
//通过getenv()获取环境变量中的用户名
return getenv("USER");
}
void getpwd()
{
//通过getenv()获取环境变量中的路径
//return getenv("PWD");
getcwd(pwd,sizeof(pwd));
}
void interact(char *cline, int size)
{
//1.打印bash命令行前面的提示信息
//需要环境变量相关的系统调用函数来获取命令行提示信息
char hostname[64];
gethostname(hostname,sizeof(hostname));
getpwd();
printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),hostname,pwd);
//2.接收用户输入信息
fgets(cline, size,stdin);
assert(cline != NULL);
(void)cline; //防止编译器报错定义而未使用的变量(假装用一下)
cline[strlen(cline)-1] = '\0';
}
int splitstring(char cline[], char *_argv[])
{
int i = 0;
_argv[i++] = strtok(cline, DELIM);
while(_argv[i++] = strtok(NULL, DELIM));
return i-1;
}
void NormalExcute(char *_argv[])
{
//5.普通命令的执行
pid_t id = fork();
if(id < 0)
{
perror("fork error");
return;
}
else if(id == 0)
{
//让子进程执行普通命令
//execvpe(_argv[0],_argv,environ);
execvp(_argv[0],_argv);
exit(EXIT_CODE);
}
else
{
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid == id)
{
lastcode = WEXITSTATUS(status);
}
}
}
int buildCommand(char *_argv[],int _argc)
{
//4.指令的判断
if(_argc == 2 && strcmp(_argv[0],"cd") == 0)
{
//更改目录
chdir(_argv[1]);
getpwd();
//更改环境变量
sprintf(getenv("PWD"),"%s",pwd);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0],"export") == 0)
{
//因为_argv一直被我们用来存新的指令,环境变量会因此被覆盖
//所以需要一个固定的存环境变量的地方来保存环境变量
strcpy(myenv,_argv[1]);
putenv(myenv);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0],"echo") == 0)
{
if(strcmp(_argv[1],"$?") == 0)
{
printf("%d\n",lastcode);
lastcode = 0;
}
else if(*_argv[1] == '$')
{
char *val = getenv(_argv[1]+1);
if(val) printf("%s\n",val);
}
else
{
printf("%s\n",_argv[1]);
}
return 1;
}
if(strcmp(_argv[0],"ls") == 0)
{
_argv[_argc++] = "--color";
_argv[_argc] = NULL;
}
return 0;
}
int main()
{
while(!quit)
{
//2.交互问题,获取命令行内容
interact(commandline,sizeof(commandline));
//printf("echo:%s\n",commandline);
//3.分割命令字符串strtok(),解析命令行
int argc = splitstring(commandline, argv);
if(argc == 0) continue;
//4.指令的判断
int n = buildCommand(argv,argc);
//5.普通命令的执行
if(!n) NormalExcute(argv);
}
return 0;
}
希望这篇关于 在Linux中实现一个简易的shell命令行 的博客能对大家有所帮助,欢迎大佬们留言或私信与我交流.
学海漫浩浩,我亦苦作舟!关注我,大家一起学习,一起进步!
今天是2024.10.24, 祝广大程序员们: "编"出未来,"程"就梦想!