什么是shell?
Shell(壳)是指命令行界面(CLI)或脚本语言,它为用户提供了与操作系统交互的方式。它是一个程序,从用户那里接收命令,并通过与操作系统内核交互来执行这些命令。Shell充当用户和操作系统之间的中介,允许用户执行各种任务,运行程序,操作文件和目录,并通过脚本实现任务自动化。
在Xshell上使用一下shell
在使用Shell时,您在Shell提示符中键入命令,Shell执行这些命令并返回输出。
下图中红圈中的字符串都是linux系统通过shell来执行的一些命令
模拟实现shell
思路
主要的程序逻辑就是在main()函数中。通过一个无限循环,不断等待用户输入命令并执行。
- getUserCommand()获取用户输入的命令字符串。
- commandSplit()将命令字符串按照分隔符分割成参数列表。
- doBuildin()检查是否为内建命令,如果是,则执行相应的操作。
如果不是内建命令,则调用execute()函数执行命令。 - execute()函数使用fork()创建子进程,在子进程中调用execvp()函数执行命令,父进程等待子进程执行完毕。
这样,代码就完成了一个简单的Shell程序,能够处理用户输入的命令,并执行相应的操作。
代码实现
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define NUM 1024
#define SIZE 64
#define SEP " "
char usercommand[NUM];
char cwd[1024];
char enval[100][100];//测试
int count_enval=0;
int lastcode;
const char *getUsername()
{
const char *name = getenv("USER");
if(name) return name;
else return "none";
}
const char *getHostname()
{
const char *hostname = getenv("HOSTNAME");
if(hostname) return hostname;
else return "none";
}
const char *getCwd()
{
const char *cwd = getenv("PWD");
if(cwd) return cwd;
else return "none";
}
//打印命令行提示符,获取用户命令字符串
int getUserCommand(char *command, int num)
{
printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());
char *r = fgets(command, num, stdin); // 最终你还是会输入\n
if(r == NULL) return -1;
// "abcd\n" "\n"
command[strlen(command) - 1] = '\0'; // 有没有可能越界?不会
return strlen(command);
}
//分割字符串
void commandSplit(char *in,char **out)
{
int argc=0;
out[argc++]=strtok(in,SEP);
while(out[argc++]=strtok(NULL,SEP));
//for (int i=0;out[i];i++)
//{
// printf("%d:%s\n",i,out[i]);
//}
}
int execute(char *argv[])
{
pid_t id=fork();
if(id<0)
{
return -1;
}
if(id==0)
{
execvp(argv[0],argv);
exit(1);
}
else
{
int status=0;
pid_t rid=waitpid(-1,&status,0);
if(rid>0)
{
lastcode=WEXITSTATUS(status);
}
}
return 0;
}
void cd(const char* path )
{
chdir(path);
char tmp[1024];
getcwd(tmp,sizeof(tmp));
sprintf(cwd,"PWD=%s",tmp);
putenv(cwd);
}
//内键情况
int doBuildin(char *argv[])
{
if(strcmp(argv[0],"cd")==0)
{
char * path=NULL;
if(argv[1]==NULL)
{
path=".";
}
else path=argv[1];
cd(path);
return 1;
}
else if(strcmp(argv[0],"exprot")==0)
{
if(argv==NULL)return 1;
strcpy(enval[count_enval],argv[1]);
putenv(enval[count_enval]);
count_enval++;
return 1;
}
else if(strcmp(argv[0],"echo")==0)
{
char *val =argv[1]+1;
if(strcmp(val,"?")==0)
{
printf("%d\n",lastcode);
lastcode=0;
}
else
{
printf("%s\n",getenv(val));
}
return 1;
}
return 0;
}
int main()
{
while(1)
{
char *argv[SIZE];
//1.打印命令行提示符,获取用户命令字符串
int n=getUserCommand(usercommand,sizeof(usercommand));
if(n<0)continue;
//2.分割字符串
commandSplit(usercommand,argv);
//3. 内键情况(cd,exprot...)
n=doBuildin(argv);
if(n) continue;
//4.执行对应的命令
execute(argv);
}
return 0;
}
运行结果:
代码讲解
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
这些头文件包含了在程序中使用的各种函数和类型的声明。
cpp
定义常量和全局变量:
#define NUM 1024
#define SIZE 64
#define SEP " "
char usercommand[NUM];
char cwd[1024];
char enval[100][100];//测试
int count_enval=0;
int lastcode;
stdio.h、stdlib.h、string.h、unistd.h、sys/types.h、sys/wait.h:包含了一些标准库函数和系统调用的声明。
NUM和SIZE:定义了一些常量,用于数组的大小。
SEP:定义了命令参数的分隔符。
usercommand[NUM]:存储用户输入的命令字符串。
cwd[1024]:存储当前工作目录的路径。
enval[100][100]:存储环境变量的字符串数组。
count_enval:环境变量计数器。
lastcode:上一个命令的退出状态码。
- getUsername(), getHostname(), getCwd(): 这些函数用于获取当前用户名、主机名和工作目录。
- getUserCommand(char *command, int num): 这个函数用于获取用户输入的命令,并返回命令的长度。
- commandSplit(char *in, char *out[]): 这个函数将输入的命令字符串按照分隔符分割成一个个参数,存储在out数组中。
- execute(char *argv[]): 这个函数使用fork()创建子进程,并在子进程中执行命令。
- cd(const char *path): 这个函数用于实现cd命令,改变当前工作目录。
- doBuildin(char *argv[]): 这个函数用于执行内建命令(如cd、export、echo)。
下面是对每一个函数的详细解释:
cpp
getUsername()
cpp
const char *getUsername()
{
const char *name = getenv("USER");
if (name)
return name;
else
return "none";
}
该函数用于获取当前用户名。它通过调用getenv()函数获取环境变量"USER"的值,并返回该值作为用户名。如果环境变量不存在,则返回字符串"none"。
cpp
getHostname()
cpp
const char *getHostname()
{
const char *hostname = getenv("HOSTNAME");
if (hostname)
return hostname;
else
return "none";
}
该函数用于获取当前主机名。它通过调用getenv()函数获取环境变量"HOSTNAME"的值,并返回该值作为主机名。如果环境变量不存在,则返回字符串"none"。
cpp
getCwd()
cpp
const char *getCwd()
{
const char *cwd = getenv("PWD");
if (cwd)
return cwd;
else
return "none";
}
该函数用于获取当前工作目录。它通过调用getenv()函数获取环境变量"PWD"的值,并返回该值作为工作目录。如果环境变量不存在,则返回字符串"none"。
cpp
getUserCommand(char *command, int num)
cpp
int getUserCommand(char *command, int num)
{
printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());
char *r = fgets(command, num, stdin);
if (r == NULL)
return -1;
command[strlen(command) - 1] = '\0';
return strlen(command);
}
该函数用于获取用户输入的命令。它首先打印出类似"[username@hostname cwd]#"的提示符,然后使用fgets()函数从标准输入中读取用户输入的命令字符串。读取成功后,函数会去掉字符串末尾的换行符,并返回命令的长度。如果读取失败,函数返回-1。
cpp
commandSplit(char *in, char *out[])
cpp
void commandSplit(char *in, char *out[])
{
int argc = 0;
out[argc++] = strtok(in, SEP);
while (out[argc++] = strtok(NULL, SEP));
}
该函数将输入的命令字符串按照分隔符进行分割,并将分割得到的参数存储在out数组中。分割过程使用strtok()函数,第一次调用时传入原始字符串和分隔符,后续调用传入NULL和分隔符。分割完成后,out数组中存储了每个参数的地址。函数没有返回值,通过参数传递分割得到的参数列表。
cpp
execute(char *argv[])
cpp
int execute(char *argv[])
{
pid_t id = fork();
if (id < 0)
return -1;
else if (id == 0) // child
{
execvp(argv[0], argv);
exit(1);
}
else // father
{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
lastcode = WEXITSTATUS(status);
}
}
return 0;
}
该函数用于执行命令。它首先调用fork()函数创建一个子进程,然后在子进程中调用execvp()函数执行指定的命令。如果fork()失败,函数返回-1。如果是子进程,调用execvp()执行命令,并在执行完毕后调用exit(1)退出子进程。如果是父进程,使用waitpid()函数等待子进程的结束,并获取子进程的退出状态码。最后,将子进程的退出码存储在lastcode变量中。
cpp
cd(const char* path )
cpp
void cd(const char* path )
{
chdir(path);
char tmp[1024];
getcwd(tmp,sizeof(tmp));
sprintf(cwd,"PWD=%s",tmp);
putenv(cwd);
}
调用chdir()函数改变当前工作目录为传入的路径。
使用getcwd()函数获取当前工作目录的路径,并存储在临时数组tmp中。
使用sprintf()函数将路径格式化为"PWD=路径"的形式,并存储在cwd字符串中。
使用putenv()函数将cwd设置为环境变量。
cpp
doBuildin(char *argv[])
cpp
//内键情况
int doBuildin(char *argv[])
{
if(strcmp(argv[0],"cd")==0)
{
char * path=NULL;
if(argv[1]==NULL)
{
path=".";
}
else path=argv[1];
cd(path);
return 1;
}
else if(strcmp(argv[0],"exprot")==0)
{
if(argv==NULL)return 1;
strcpy(enval[count_enval],argv[1]);
putenv(enval[count_enval]);
count_enval++;
return 1;
}
else if(strcmp(argv[0],"echo")==0)
{
char *val =argv[1]+1;
if(strcmp(val,"?")==0)
{
printf("%d\n",lastcode);
lastcode=0;
}
else
{
printf("%s\n",getenv(val));
}
return 1;
}
return 0;
}
根据传入的参数数组argv[]判断是否是内键命令。
如果是cd命令,判断是否提供了路径参数,如果没有,则将路径设置为当前目录;否则,使用cd()函数切换到指定路径。
如果是export命令,判断是否提供了参数,如果没有,则返回;否则,将参数复制到enval数组中,并使用putenv()函数将其设置为环境变量,同时递增count_enval计数器。
如果是echo命令,从参数中提取变量名,并判断变量名是否为"?",如果是,则打印上一个命令的退出状态码;否则,使用getenv()函数获取指定变量的值,并打印出来。
如果是以上内键命令之一,返回1;否则,返回0。
cpp
int main()
{
while(1)
{
char *argv[SIZE];
//1.打印命令行提示符,获取用户命令字符串
int n=getUserCommand(usercommand,sizeof(usercommand));
if(n<0)continue;
//2.分割字符串
commandSplit(usercommand,argv);
//3. 内键情况(cd,exprot...)
n=doBuildin(argv);
if(n) continue;
//4.执行对应的命令
execute(argv);
}
return 0;
}
main()函数:
进入一个无限循环,用于不断接收用户的命令并执行。
定义参数数组argv[SIZE]。
调用getUserCommand()函数获取用户命令字符串。
如果获取失败,则继续下一次循环。
调用commandSplit()函数将命令字符串分割为参数数组argv[]。
调用doBuildin()函数判断是否是内建命令并执行。
如果是内键命令,则继续下一次循环。
调用execute()函数执行命令。
返回0,结束程序运行。
总结
代码实现了一个简单的交互式shell,能够解析用户输入的命令并执行相应的操作。内键命令包括cd切换目录,export设置环境变量,echo打印变量值。其他命令会通过fork()创建子进程并调用execvp()执行外部命令。
(本章完)