目录
1.命令行提示符
2.命令行参数
2.1 获取命令行参数
2.2 解析命令行参数
3.判断指令类型
3.1 模拟cd命令
3.2 模拟export和echo
bash的环境变量来源
4.外部指令的执行
1.命令行提示符
在我们输入指令前,终端界面一般有一个命令行提示符, 我们先实现这个功能,
我们这里把当前工作路径的全路径打印出来了,如果只要打印当前工作目录名,可以分割路径提取。
2.命令行参数
2.1 获取命令行参数
如果我们用scanf读取输入的命令行参数,只会读到一个不含空白符的字符串,因为scanf会以空白符为结尾。我们可以看看,
所以我们不能用scanf读取命令行参数,可以用fgets, fgets - cppreference.com
注意:
puts遇到空字符停止输出,在输出字符串时会自动在字符串末尾(\0)加一个换行符。
gets()丢弃输入中的换行符,puts()在输出中添加换行符。fgets()保留输入中的换行符,fputs()不在输出中添加换行符。
我们编写函数Interact,用来实现用户和命令行交互的功能,使程序获取用户输入的命令行参数,
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <assert.h>
4 #include <string.h>
5 #define LEFT "["
6 #define RIGHT "]"
7 #define LABEL "#"//root用户是#,普通用户是@
8 #define LINE_SIZE 1024
9
10 char commandline[LINE_SIZE];
11
12 //获取用户名
13 const char* getUsername()
14 {
15 return getenv("USER");
16 }
17
18 //获取主机名
19 const char* getHostname()
20 {
21 return getenv("HOSTNAME");
22 }
23
24 //获取当前工作路径
25 const char* getPwd()
26 {
27 return getenv("PWD");
28 }
29
30 void Interact(char* cline,int size)
31 {
E> 32 printf(LEFT"%s@%s %s"RIGHT""LABEL" ",getUsername(),getHostname(),getPwd());
33 char *s = fgets(cline,size,stdin);
34 assert(s);//assert只在debug模式下作用,release版本下会被优化掉
35 //所以变量s在release版本下,可能只定义而没有被使用。
36 //编译器对于定义了但没有使用的变量可能会报warning
37 //所以为了让编译器编过,我们加下面一句代码
38 (void)s;//抵消编译器的一些报警
39
40 //"ls -a -l\n\0"
41 //fgets会将最后用作结束的换行符保存,我们要删除这个换行符
42 cline[strlen(cline) - 1] = '\0';//"ls -a -l\0\0"
43 }
44 int main()
45 {
46 char commandline[LINE_SIZE];
47 int quit = 0;
48 while(!quit)
49 {
50 Interact(commandline,sizeof(commandline));
51 printf("echo:%s\n",commandline);
52 }
53 return 0;
54 }
运行
我们可以一直向命令行中输入参数。
2.2 解析命令行参数
用户输入参数(指令)后,shell会对该行参数进行解析,一般会将字符串进行分割,我们这里用strtok函数分割字符串。
我们用splitstring接口实现字符串解析的功能, 同时用一个for循环检测解析后的结果。
10 #define ARGC_SIZE 32
11 #define DELIMIT " \t"
//其余部分代码同上
47 int splitstring(char cline[],char *argv[])
48 {
49 //分割字符串cline
50 int i = 0;
51 argv[i++] = strtok(cline,DELIMIT);
W> 52 while(argv[i++] = strtok(NULL,DELIMIT));
53 return i - 1;//返回分割后字符串个数
54 }
55 int main()
56 {
57 char commandline[LINE_SIZE];//
58 char *argv[ARGC_SIZE];
59 int quit = 0;
60 while(!quit)
61 {
62 //1.打印命令行标识符并等待参数输入
63 Interact(commandline,sizeof(commandline));
64 printf("echo:%s\n",commandline);
65
66 //commandline -> "ls -a -l\0" -> "ls" "-a" "-l"
67 //2.解析参数,对子串进行分割
68 int argc = splitstring(commandline,argv);
69 if(argc == 0) continue;
70 //检测分割后的结果
71 for(int i = 0;argv[i];i++) printf("[%d]:%s\n",i,argv[i]);
72 }
73 return 0;
74 }
运行, 命令行提示符、用户与命令行交互、解析参数的功能实现了,现在我们需要根据用户输入的参数(指令),执行程序。
但在执行程序之前,我们必须对指令进行判断。
3.判断指令类型
我们之前学习过指令,Linux系统的指令一般可以分为两类 ,
一类是内部指令(builtin shell command),内部指令是指内建在shell中的指令,但我们执行该类指令时,不需要额外创建进程,所以内部指令执行的效率高。
另一类是外部指令(external shell command)。外部指令是指非内建于shell的指令,我们执行该类指令时,会额外创建一个进程。
我们可以通过指令type判断一个指令是否是内建指令。
type命令来自英文单词“类型”,其功能是用于查看命令类型,如需区分某个命令是Shell内部指令还是外部命令,则可以使用type命令进行查看。
原文链接:type命令 – 查看命令类型 – Linux命令大全(手册)
显示出文件路径的一般是外部命令,显示“is a shell builtin”是内部命令。
我们判断是否是内建命令,是一条一条判断的,
3.1 模拟cd命令
拿“cd”举例,cd + 目标路径,我们可以通过chdir系统调用,将当前的工作路径换为“目标路径”,
注意,chdir只会更改进程的当前工作目录,只会影响当前进程及其子进程的工作目录,不会影响环境变量中的PWD,PWD 是由 shell 自动维护,而不是由内核直接管理。
对于典型的 shell,PWD 在你使用命令(如 cd)改变目录时会被更新,但是,直接用系统调用 chdir 不会自动更新 PWD。linux环境 调用chdir函数虽然更改了工作目录,但是加载动态库还是之前的目录下 - CSDN文库
所以当我们调用chdir更改当前工作目录后,还要对环境变量PWD进行修改。先通过调用getcwd()使全局变量pwd获取当前工作目录的绝对路径,然后将全局变量pwd拷贝到环境变量PWD中,可以通过getenv()的返回值获取对应环境变量的地址,再将字符串pwd写入getenv返回的指针指向的内存区域。
返回值为1,表示内部指令的执行。
同样的,我们还可以实现几个内建命令,
3.2 模拟export和echo
我们检验一下内建命令export,没有问题,我们先把代码中的检测注释掉,再检验一下echo,
看上去没有问题,我们用export增加一个环境变量,然后用echo打印该环境变量,
很奇怪,没有打印新增的环境变量val_env,我们输入env指令检查一下,发现也没有新增的环境变量,但我们之前export新增命令后输入env,可以查看到新增环境变量,这是为什么呢?为什么中间输入echo指令查看环境变量,env就不显示新增环境变量了?
这是因为,当进程启动时,会专门开辟一块空间存储命令行参数和环境变量,同时用一个字符串指针数组管理这些环境变量,这个管理环境变量的字符串指针数组就叫做环境变量表(char* envrion[]),
当我们用putenv新增一个环境变量_argv[1],这时环境变量表会分配一个元素,也就是字符串指针指向这个新增的环境变量,这个新增的环境变量并没有添加到专门存储环境变量的内存空间中,而是在栈区(因为_argv[1]是一个全局变量),
当我们重新在命令行输入指令时,会刷新命令行参数表_argv(char* argv[]),如果argv会分割成两个字符串,第二个字符串就会覆盖原来的_argv[1],“echo $val_env”中的“$val_env”,就会覆盖原来的“export val_env=1111111”中的“val_env=1111111”,这样环境变量表environ就找不到原来新增的环境变量了val_env=1111111,因为被覆盖了。
为了防止再次输入命令会覆盖环境变量的问题,我们要自己维护一个存放环境变量的空间myenv,将新增的环境变量存放在myenv中,
这样就不会覆盖了。 (但再添加一个新的环境变量会覆盖旧的环境变量,大家也可以把自己维护的环境变量设置成二维数组的形式,在堆上申请空间)
上面的myenv属于自定义的环境变量表,具有全局属性,我们还可以自定义一个本地变量表,【Linux】:环境变量-CSDN博客 当我们用echo检查最近一个进程的退出码时,可增加一个if语句,
bash的环境变量来源
当我们成功登录linux系统时,会启动一个shell进程(bash进程),我们在命令行运行的程序,实例化的进程的环境变量都是继承bash进程。bash进程的环境变量保存在用户目录的".bash.profile"文件中,该文件中保存了导入环境变量的方式。
4.外部指令的执行
我们用externalExecute接口实现外部指令的执行,
5.完整代码
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define LEFT "["
#define RIGHT "]"
#define LABEL "#"//root用户是#,普通用户是@
#define LINE_SIZE 1024//命令行标识符的大小
#define ARGC_SIZE 32//字符串指针数组的字符串个数
#define DELIMIT " \t"//strtok()的分割符
#define EXIT_CODE 303//子进程程序替换失败后的退出码
extern char **environ;//环境变量
char commandline[LINE_SIZE];//命令行标识符
int lastcode = 0;//退出码
int quit = 0;//充当bash进程结束条件的判断
char *argv[ARGC_SIZE];//存储分割后的参数序列
char pwd[LINE_SIZE];//获取当前目录的工作路径
char myenv[LINE_SIZE];//环境变量存放空间
//获取用户名
const char* getUsername()
{
return getenv("USER");
}
//获取主机名
const char* getHostname()
{
return getenv("HOSTNAME");
}
//获取当前工作目录路径
void getpwd()
{
getcwd(pwd,1024);
}
void Interact(char* cline,int size)
{
getpwd();
printf(LEFT"%s@%s %s"RIGHT""LABEL" ",getUsername(),getHostname(),pwd);
char *s = fgets(cline,size,stdin);
assert(s);//assert只在debug模式下作用,release版本下会被优化掉
//所以变量s在release版本下,可能只定义而没有被使用。
//编译器对于定义了但没有使用的变量可能会报warning
//所以为了让编译器编过,我们加下面一句代码
(void)s;//抵消编译器的一些报警
//"ls -a -l\n\0"
//fgets会将最后用作结束的换行符保存,我们要删除这个换行符
cline[strlen(cline) - 1] = '\0';//"ls -a -l\0\0"
}
int splitstring(char cline[],char *_argv[])
{
//分割字符串cline
int i = 0;
_argv[i++] = strtok(cline,DELIMIT);
while(_argv[i++] = strtok(NULL,DELIMIT));
return i - 1;//返回分割后字符串个数
}
void externalExecute(char* _argv[])
{
pid_t id = fork();
if(id < 0)
{
//子进程创建失败
perror("fork");
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 buildCmd(char* _argv[],int _argc)
{
if(_argc == 2 && strcmp(_argv[0],"cd") == 0)
{
printf("检验环境变量PWD是否改变:%s\n",getenv("PWD"));
chdir(_argv[1]);
getpwd();
sprintf(getenv("PWD"),"%s",pwd);//修改环境变量PWD
printf("检验环境变量PWD是否改变:%s\n",getenv("PWD"));
return 1;
}
else if(_argc == 2 && strcmp(_argv[0],"export") == 0)
{
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;
}
// 特殊处理一下ls
if(strcmp(_argv[0], "ls") == 0)
{
_argv[_argc++] = "--color";
_argv[_argc] = NULL;
}
return 0;
}
int main()
{
while(!quit)
{
//1.
//2.获取命令行参数(指令)
Interact(commandline,sizeof(commandline));
//printf("echo:%s\n",commandline);
//commandline -> "ls -a -l\0" -> "ls" "-a" "-l"
//3.解析参数,对子串进行分割
int argc = splitstring(commandline,argv);
if(argc == 0) continue;
//检测分割后的结果
//for(int i = 0;argv[i];i++) printf("[%d]:%s\n",i,argv[i]);
//4.判断指令
int n = buildCmd(argv,argc);
//5.外部指令的执行
if(!n) externalExecute(argv);
}
return 0;
}