在这里插入图片描述

shell的基本框架

要写一个命令行我们首先要写出基本框架。

  1. 打印命令行
  2. 获取用户输入的命令
  3. 分析命令
  4. 执行命令

基本框架的代码:

int main()
{
  //because shell must keep running
  while(true)
  {
    PrintCommandLine();  //1.use this func to print command line
    //only get <command_buffer ->output
    GetCommandLine();    //2.get user's command 

    //"is -a -b -c -d"--->"ls" "-a" "-b" "-d"
    ParseCommandLine();  //3.analyze command

    ExecuteCommand();    //4.implement command
  }
  return 0;
}

因为命令要时刻保持运行,所以我们还要加上一层循环保证时时刻刻在刷新命令行。

PrintCommandLine

接下来就需要实现打印命令行的函数了,首先看一下我们打印命令行时需要什么?
在这里插入图片描述
需要用户名,主机名,文件路径最后还需要一个$或者#

void PrintCommandLine()  //1.use this func to print command line
{
  //create command line 
  //this printf have no \n,so this result is won't be display immediately
  printf("%s",MakeCommandLine().c_str());
}

打印的这个函数,MakeCommandLine()负责返回一个string类型的表。
由于我们要获取主机名,所以需要用到获取环境变量的函数,getenv()

//get user name 
string GetUserName()
{
  string name =getenv("USER");
  return name.empty()?"None":name;
}

//get hostname 
string GetHostName()
{
  string HostName=getenv("HOSTNAME");
  return HostName.empty()?"None":HostName;
}
const int basesize=1024;
//overall situation's The working path of the current shell
char Pwd[basesize];
//overall situation's Pwd envitonment variable
char Pwdenv[basesize];
//get pwd 
string GetPwd()
{
  //string Pwd=getenv("PWD");
  if(nullptr == getcwd(Pwd,sizeof(Pwd))) return "None";
  snprintf(Pwdenv,sizeof(Pwdenv),"PWD=%s",Pwd);
  putenv(Pwdenv);
  return Pwd;
}

由于这里不能直接使用getenv(“PWD”),因为这里获取的是shell的pwd,shell的pwd一直都处在当前路径下,也就是我们的自己的shell运行的那个目录下,所以这里,这里前两个函数都很容易理解,只需要解释一下最后一个,我们来看看snprintf这个函数
在这里插入图片描述
这个函数是将后面的一个字符串以某种格式打印到前面的s当中,需要写出前面s的大小。
在这里插入图片描述
回到获取路径这个函数当中,第一个if是用来判断获取当前工作路径是否成功。如果获取成功,当前工作路径将存储在Pwd当中,snprintf,这个函数我们将Pwd这个字符串以PWD="Pwd"这样的格式打印这个,所以这里Pwdenv已经存储了环境变量的那个格式,获取了环境变量瞬时应该将环境变量表中的环境变量更新一下,所以putenv,最后将当前工作路径返回即可。
下面函数只需要调用上面获取好的接口即可,然后按照格式化打印,将对应的用户名,型号名,还有工作路径,都存储在Command_Line这个缓冲区中,然后返回到打印的函数

string MakeCommandLine()
{
  //[newuser@hcss-ecs-e091 myshell]$
  char Command_Line[basesize];
  //output
  snprintf(Command_Line,basesize,"[%s@%s %s]# ",GetUserName().c_str(),GetHostName().c_str(),GetPwd().c_str());
  return Command_Line;
}

PrintCommandLine
这里打印的时候需要刷新的一下屏幕,保证一直都是在一行

void PrintCommandLine()  //1.use this func to print command line
{
  //create command line 
  //this printf have no \n,so this result is won't be display immediately
  printf("%s",MakeCommandLine().c_str());
  //refresh screen
  fflush(stdout);
}

GetCommandLine

获取命令行,首先需要一个数组,将输入的命令先存放在这个函数中,然后由下一步来执行将字符串拆分为单个命令和选项即可。

//after only call this commandline,put string to this buffer
bool GetCommandLine(char Command_Buffer[],int size)    //2.get user's command 
{
  //cannot be use scanf and cin 
  //we think:We need to treat the command line entered by the user as a complete string 
  //"ls -a -l -n" this is a complete string 
  //              array        size   stadard input
  char *result = fgets(Command_Buffer,size,stdin);
  if(!result) return false;
  //we should delete last str,because it is enter key
  Command_Buffer[strlen(Command_Buffer)-1] = 0;//currently, it is OK
  if(strlen(Command_Buffer) == 0) return false;
  return true;
}

这里不能用scanf,因为scanf和cin不能使用空格,所以我们选择用fgets,用fgets获取字符串,然后将这个字符串存在Command_Buffer中,获取完之后判断一下是否获取成功,就是检查一下获取之后的变量是否是nullptr。

注意:最后一个位置的字符需要改为0,因为我们输入的时候,会回车也是一个字符,所以应该将这个回车给去掉
当获取的命令中只有回车的时候,将回车去掉strlen就变为0了,所以只有回车时不需要解析命令,所以直接返回false。

ParseCommandLine

获取成功后我们就需要将整个字符串以空格为分隔符将其分为各个字符串来进行解析了。
这里转化为的表在shell中就是传给main参数的argc和argv,所以这里我们也需要一个变量argc和一个argv一个来计数一个来存储解析出来的表。

const int argvnum = 64;
//command line parameter list 
char *gargv[argvnum];
//for counting
int gargc;

由于我们使用的是全局变量,所以每次进入这个函数的时候都需要重置这两个全局变量

void ParseCommandLine(char Command_Buffer[],int len)  //3.analyze command
{
  (void)len;
  memset(gargv,0,sizeof(gargv));
  gargc=0;
  //"ls -a -l -n"---->ls
  //finally cut to Cogargv 
  const char *sep = " ";
  //post++
  gargv[gargc++] = strtok(Command_Buffer,sep);
  //Form a table and stop looping when the return value is nullptr 
  while((bool)(gargv[gargc++] = strtok(nullptr,sep)));
  gargc--;
}

在这里插入图片描述
这个函数是将str以delimiters为分隔符来分割字符串,分出来的字符串会返回首地址,第一个参数只有第一次才传入对应的字符串的首地址,往后调用这个函数对同一个字符串做分割,只需要传入nullptr,所以第一个较为特殊,我们只需要对第一个做特殊处理,将其第一个分割,然后存储在gargv中,然后对应的计数++,往后都是nullptr作为第一个参数,往后分割之后返回的是nullptr就证明分割完了,所以这里我们要使用一个循环,但是由于nullptr那次也进行了++,所以实际上计数多记了一次,下面要进行–。
在这里插入图片描述

ExecuteCommand

将对应的字符串根据空格分隔符翻译为表之后,接下来就需要执行命令了,为了确保shell的稳定,所以我们用子进程来执行命令,这里创建子进程,然后判断子进程是否创建成功,创建成功后,子进程执行任务,这里最开始其实可以用execvp来进行进程替换,gargv是指令,gargv是整个选项。
在这里插入图片描述
由于进行进程替换之后就不可能执行exit了,所以exit是用来判断替换失败还是替换成功的,如果替换成功就不会exit了,如果失败就会退出,并且退出码是1,父进程等待子进程结束,回收子进程即可,如果rid为正说明回收成功,如果rid小于零等待失败直接返回false

bool ExecuteCommand()    //4.implement command
{
  //implement command 
  //let the child process execute 
  //because parent process execute the process ,if this process is failed ,myshell is hangs
  pid_t id = fork();//create child
  if(id < 0) return false;
  if(id == 0)
  {
    //child process
    //implement command 
    execvpe(gargv[0],gargv,genv);
    
    //Exit
    exit(1);//fail return 1
  }

  int status = 0;
  pid_t rid = waitpid(id,&status,0);//blocking wait 
  if(rid > 0)
  {
    //wait success
    return true;
  }
  else return false;
}

这里其实已经差不多了,但是我们还需要初始化我们的环境变量表。

InitEnv

需要顶一个全局的环境变量表

//my env array 
const int envnum = 64;
char *genv[envnum];

拥有我们自己的环境变量只需要将父进程的环境变量拷贝下来即可,进行深拷贝。

//as a shell,to get a evironment variable should from system to get 
//today, we direct get environment variable from parent process
void InitEnv()
{
  //get environment variables from parent process 
  extern char **environ;
  int index = 0;
  while(environ[index] != nullptr)
  {
    //open up the same space as environment variable 
    genv[index] =(char*)malloc(strlen(environ[index])+1);
    //copy element in genv to environ 
    strncpy(genv[index],environ[index],strlen(environ[index]+1));
    index++;
  }
  genv[index]=nullptr;
}

我们还需要对一些命令进行特殊处理,比如一些内建命令。

CheckAndExecBuildCommand

因为我们是用子进程执行的命令,所以如果我们cd的话,是子进程cd,影响不了父进程,子进程执行完cd直接退出了,所以我们需要一个函数来判断这个命令是否是内建命令
内建命令是指直接在 shell 内部实现的命令,而不是外部可执行文件。内建命令是由 shell 本身提供和执行的,因此它们不需要创建新的进程来执行。相比于外部命令,内建命令的执行速度更快,因为它们不需要通过系统调用加载可执行文件。内建命令通常用于控制 shell 的行为或执行与 shell 相关的任务。

//shell execute command by itself,the essence is shell call itself's func
bool CheckAndExecBuildCommand()//check build-in command and execute command 
{
  if(strcmp(gargv[0],"cd") == 0)
  {
    //build-in command
    if(gargc == 2)
    {
      //change path 
      chdir(gargv[1]);
    }
    return true;
  }
  //export is also a build-in command 
  else if(strcmp(gargv[0],"export") == 0)
  {
    if(gargc == 2)
    {
      AddEnv(gargv[1]);
    }
    return true;
  }
  //env is also a build-in command 
  else if(strcmp(gargv[0],"env") == 0)
  {
    for(int i = 0;genv[i];i++)
    {
      printf("%s\n",genv[i]);
    }
    return true;
  }
  return false;
}

代码总览

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<cstdlib>
using namespace std;

const int basesize=1024;
const int argvnum = 64;
//command line parameter list 
char *gargv[argvnum];

//for counting
int gargc;

//overall situation's The working path of the current shell
char Pwd[basesize];
//overall situation's Pwd envitonment variable
char Pwdenv[basesize];

//my env array 
const int envnum = 64;
char *genv[envnum];


//get user name 
string GetUserName()
{
  string name =getenv("USER");
  return name.empty()?"None":name;
}

//get hostname 
string GetHostName()
{
  string HostName=getenv("HOSTNAME");
  return HostName.empty()?"None":HostName;
}

//get pwd 
string GetPwd()
{
  //string Pwd=getenv("PWD");
  if(nullptr == getcwd(Pwd,sizeof(Pwd))) return "None";
  snprintf(Pwdenv,sizeof(Pwdenv),"PWD=%s",Pwd);
  putenv(Pwdenv);
  return Pwd;
}

//Create output format 
string MakeCommandLine()
{
  //[newuser@hcss-ecs-e091 myshell]$
  char Command_Line[basesize];
  //output
  snprintf(Command_Line,basesize,"[%s@%s %s]# ",GetUserName().c_str(),GetHostName().c_str(),GetPwd().c_str());
  return Command_Line;
}

void PrintCommandLine()  //1.use this func to print command line
{
  //create command line 
  //this printf have no \n,so this result is won't be display immediately
  printf("%s",MakeCommandLine().c_str());
  //refresh screen
  fflush(stdout);
}

//after only call this commandline,put string to this buffer
bool GetCommandLine(char Command_Buffer[],int size)    //2.get user's command 
{
  //cannot be use scanf and cin 
  //we think:We need to treat the command line entered by the user as a complete string 
  //"ls -a -l -n" this is a complete string 
  //              array        size   stadard input
  char *result = fgets(Command_Buffer,size,stdin);
  if(!result) return false;
  //we should delete last str,because it is enter key
  Command_Buffer[strlen(Command_Buffer)-1] = 0;//currently, it is OK
  if(strlen(Command_Buffer) == 0) return false;
  return true;
}

void ParseCommandLine(char Command_Buffer[],int len)  //3.analyze command
{
  (void)len;
  memset(gargv,0,sizeof(gargv));
  gargc=0;
  //"ls -a -l -n"---->ls
  //finally cut to Cogargv 
  const char *sep = " ";
  //post++
  gargv[gargc++] = strtok(Command_Buffer,sep);
  //Form a table and stop looping when the return value is nullptr 
  while((bool)(gargv[gargc++] = strtok(nullptr,sep)));
  gargc--;
}


//in my command line 
//have some command must be child process to implement
//but have some command not be child process to implement----built-in command 
bool ExecuteCommand()    //4.implement command
{
  //implement command 
  //let the child process execute 
  //because parent process execute the process ,if this process is failed ,myshell is hangs
  pid_t id = fork();//create child
  if(id < 0) return false;
  if(id == 0)
  {
    //child process
    //implement command 
    execvpe(gargv[0],gargv,genv);
    
    //Exit
    exit(1);//fail return 1
  }

  int status = 0;
  pid_t rid = waitpid(id,&status,0);//blocking wait 
  if(rid > 0)
  {
    //wait success
    return true;
  }
  else return false;
}

//add a environment variable 
void AddEnv(const char *item)
{
  int index = 0;
  while(genv[index]) index++;

  //find last location 
  genv[index] = (char*)malloc(strlen(item)+1);
  strncpy(genv[index],item,strlen(item)+1);
  genv[++index] = nullptr;

}

//shell execute command by itself,the essence is shell call itself's func
bool CheckAndExecBuildCommand()//check build-in command and execute command 
{
  if(strcmp(gargv[0],"cd") == 0)
  {
    //build-in command
    if(gargc == 2)
    {
      //change path 
      chdir(gargv[1]);
    }
    return true;
  }
  //export is also a build-in command 
  else if(strcmp(gargv[0],"export") == 0)
  {
    if(gargc == 2)
    {
      AddEnv(gargv[1]);
    }
    return true;
  }
  //env is also a build-in command 
  else if(strcmp(gargv[0],"env") == 0)
  {
    for(int i = 0;genv[i];i++)
    {
      printf("%s\n",genv[i]);
    }
    return true;
  }
  return false;
}


//as a shell,to get a evironment variable should from system to get 
//today, we direct get environment variable from parent process
void InitEnv()
{
  //get environment variables from parent process 
  extern char **environ;
  int index = 0;
  while(environ[index] != nullptr)
  {
    //open up the same space as environment variable 
    genv[index] =(char*)malloc(strlen(environ[index])+1);
    //copy element in genv to environ 
    strncpy(genv[index],environ[index],strlen(environ[index]+1));
    index++;
  }
  genv[index]=nullptr;
}


int main()
{
  //my shell's environment variable 
  InitEnv();
  //new buffer 
  char Command_Buffer[basesize];
  //because shell must keep running
  while(true)
  {
    PrintCommandLine();  //1.use this func to print command line
    //only get <command_buffer ->output
    if(!GetCommandLine(Command_Buffer,basesize))    //2.get user's command 
    {
      //get fail 
      continue; 
    }
    //"is -a -b -c -d"--->"ls" "-a" "-b" "-d"
    ParseCommandLine(Command_Buffer,strlen(Command_Buffer));  //3.analyze command
    if(CheckAndExecBuildCommand())
    {
      continue;
    }
    ExecuteCommand();    //4.implement command
  }
  return 0;
}

运行效果

在这里插入图片描述

总结

通过编写一个简易版的Linux命令行shell,我们掌握了在命令行环境中解析并运行指令的基础知识。这一项目帮助我们理解了如何通过系统调用执行外部程序、处理输入和输出,以及如何让shell与用户交互。尽管功能较为基础,但它包含了命令读取、解析和执行等关键流程,为后续学习更复杂的shell实现和系统编程提供了扎实的基础。如果有兴趣进一步扩展,可以尝试加入更多特性,如命令历史记录、自动补全、管道和重定向支持等,使这个shell更加功能丰富。

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐