目录
介绍:
Linux下,Shell是一个用户与操作系统之间的接口,而命令行则是这个接口的表现形式。具体来说Shell是一个命令行解释器,它读取用户输入的命令(或来自脚本的命令),然后解释并执行这些命令。这里需说明一下,Shell程序在处理用户输入的指令时很多时候都是创建子进程来实现,但有些指令功能子进程无法对其做出相应功能的处理,必须依靠父进程完成,比如:cd、export、alias等,这种命令称为内建命令。内建命令是Shell程序的一部分,这种命令程序是嵌入到Shell的源码中,运用时直接由shell解释并执。
Shell程序的实现,我们可以分为三大步骤:
1,命令行的编写以及获取用户输入的命令串。这里需考虑特殊情况——指令串为空串。
2,指令串的处理。这里要考虑指令中的选项参数。
3,指令的执行。这里的指令依靠子进程来完成。执行指令时要考虑特殊指令,比如内 建指令。
一,命令行的编写
当编写shell程序时,首要考虑的是打印命令行。命令行中有用户名、主机名、当前的工作目录消息。由于用户名和工作目录会根据需要而变化,因此这里需使用系统下的环境变量来获取这些信息。这里我们分别设置三个函数接口来调用环境变量实现。
//以下的获取中,通过调用获取指定Shell环境变量的getenv接口来获取我们需要的信息,一旦获取失败同一用 None 表示
const char* HostName() //获取主机名
{
char* hostname = getenv("HOSTNAME");
if (hostname)
return hostname;
else
return "None";
}
const char* UserName() //获取用户名
{
char* username = getenv("USER");
if (username)
return username;
else
return "None";
}
const char* WorkDir() //获取当前目录
{
char* workdir = getenv("PWD");
if (workdir)
return workdir;
else
return "None";
}
命令行输出完之后下面就要获取输入流中的命令串。这里需注意输入的指令串为空串的情况。当输入的指令串不为空串时进行下一步操作,当输入的指令串为空串时不做任何处理。
int Interact(char* out, int size) //out:存储的指令串 size:指令串的长度大小
{
// 输出命令行提示符并获取用户输入的命令字符串
fprintf(stdout, "[%s@%s %s]$ ", UserName(), HostName(), WorkDir());
fgets(out, size, stdin); // 从输入流中获取命令串,要考虑空格字符
out[strlen(out) - 1] = NULL; // fgets也会把'\n'字符存储,这里去掉此多余字符
return strlen(out); // 这里要在后面执行中处理空串的问题
}
当函数返回值为0时,表明输入的串为空串,此时Shell程序需要从头开始,即输入命令行,存储用户输入的指令,指令的处理。
二,命令的串的处理
由于输入的命令存在选项参数,为了后面指令程序方便实现,这里需要用指针数组把指令以及参数分别存储下来。
#define MAX_SIZE 64
#define SEP " " //以空格作为分割符char* argv[MAX_SIZE]; //存储处理后的指令串
void Split(char* in) // in:存储的指令串
{
int i = 0;
//strtok接口函数对输入的字符串指令进行切割
argv[i++] = strtok(in, SEP);
while (argv[i++] = strtok(NULL, SEP))
{ }// 下面是针对于 ls 指针的处理,让ls指令显示的目录和文件添加上该有的颜色
if (strcmp(argv[0], "ls") == 0)
{
argv[i - 1] = (char*)"--color"; // 这里把ls指令的颜色选项功能带上
argv[i] = NULL;
}
}
三,命令的执行
在指令的指令前,我们要先考虑内建命令。由于内建命令的在Shell程序中处理的方式各种各样,所以这里我们只实现对cd、export、echo的处理。
除了内建命令外,其它命令进程的执行是依靠子进程来实现。系统一旦处理到内建命令时,就不能正常当作普通命令来处理,需要单独进行。
cd指令切换目录时,若让子进程让子进程处理,发现cd指令无法正常切换,因为此时执行指令的不是当前父进程的shell/bash,而是子进程的bash/shell。每当子进程执行完相应的进程后就退出了,无法正常进行切换,所以这里必须看成内建命令进行处理。当处理cd指令时,我们可先使用函数 chdir() 来改变当前的工作路径(目前环境变量中的所在路径没有变换。chidir不会影响环境变量),然后 getcwd 和 putenv 来获取当前进程的绝对路径改变环境变量。这样,再一次进行命令行的输出时就会正常得到工作路径的切换。
export导入环境变量时,若正常让子进程处理,只会导入到子进程的环境变量表中,父进程存储的环境变量表中根本没有此数据,导致处理失败,因此,需要将export导入环境变量的操作在父进程中执行。当export没有任何参数时不做任何处理,后面一旦跟环境变量串时,我们只需利用putenv导入环境变量表中即可。
echo命令形式复杂多样,这里我们只对 echo [信息] 和 echo $[环境变量] 进行处理。处理的方式很简单,当使用 echo [信息] 时只需输出信息即可;当 echo $[环境变量] 时只需输出获取的环境变量即可。
在实现时,我们先认识一下有关系统和C库中的专有函数接口。
chdir接口:更改当前工作目录时,但不会影响任何环境变量
头文件
#include <unistd.h>
接口形式
int chdir(const char *path);
参数
path
是一个指向要切换到的目录路径的字符串。如果函数执行成功,它将返回0。如果执行失败,它将返回-1
getcwd接口:获取当前工作目录的完整路径
头文件
#include <unistd.h>
接口形式
char *getcwd(char *buf, size_t size);
参数buf指向一个字符数组的指针,该数组用于存储当前工作目录的路径。如果此参数为NULL,则
getcwd
会动态分配所需的内存。size是数组buf的大小。如果成功,getcwd
返回指向buf
的指针。如果失败,返回NULL,并设置全局变量errno
以指示错误类型。
putenv接口:用于添加或修改环境变量
头文件
#include <stdlib.h>
接口形式
int putenv(char *string);putenv
接受一个指向以name=value
格式表示的字符串的指针,其中name
是环境变量的名称,value
是该变量的值。如果环境变量已经存在,putenv
会修改它的值;如果不存在,它会添加新的环境变量。如果putenv
成功,它会返回0;如果失败(比如内存不足),则返回非零值。
getenv:从环境中取字符串,获取环境变量的值
头文件
#include <stdlib.h>
接口形式
char *getenv(const char *name);
name
参数是一个指向要检索的环境变量名称的字符串的指针。getenv
函数返回的指针指向一个字符串,这个字符串就是环境变量的值。如果getenv
找不到指定的环境变量,它会返回NULL
snprintf接口:将格式化的数据写入一个字符串中
头文件
#include <stdio.h>
接口形式
int snprintf(char *str, size_t size, const char *format, ...);
此函数跟printf效果类似,将指定的数据串输出到大小为size的str字符数组中
下面是对内建命令做出的处理函数。
char* Home()
{
return getenv("HOME");
}int BuildCommand()
{
int ret = 0;
//检测输入的指令程序是否是内建指令,ret ==1表示是,ret == 0 表示不是
if (strcmp("cd", argv[0]) == 0)
{ret = 1;
char* target = argv[1]; // 这里cd内建指令可能存在选项参数
if (!target) target = Home(); // cd无参形式的情况
chdir(target); // 改变当前的工作路径,但目前环境变量中的所在路径没有变换。chidir不会影响环境变量
// 这里要注意类似于cd ..的情况
char temp[1024];
getcwd(temp, 1024); // 获取当前进程的绝对路径
snprintf(pwd, SIZE, "PWD=%s", temp); //将获得的路径存放到pwd中
putenv(pwd); //添加或修改当前进程PCB存储中的环境变量
}
// 处理导入系统下的环境变量
else if(strcmp("export", argv[0]) == 0)
{
ret = 1;
if (argv[1]) // export使用时,参数可能为空
{
strcpy(env, argv[1]);
putenv(env);
}
}
else if(strcmp("echo", argv[0]) == 0)
{
ret = 1;
if (argv[1] == NULL)
{
cout << endl;
}
else
{
if (argv[1][0] == '$')
{
if (argv[1][1] == '?') //输出进程退出状态
{
cout << lastcode << endl;
lastcode = 0;
}
else //输出指定环境变量
{
char* e = getenv(argv[1] + 1);
if (e) cout << e << endl;
}
}
else //输出指定信息串
{
cout << argv[1] << endl;
}
}
}
return ret;
}
系统下的普通命令,我们只需创建子进程,运用进程替换去实现即可。代码如下:
void Execute()
{
pid_t id = fork();
if (id == 0)
{
// 让子进程执行命令
execvp(argv[0], argv);
exit(1);
}//为防止僵尸进程,这里需进行进程等待
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid == id) lastcode = WEXITSTATUS(status); // 保存子进程的退出码
}
四,shell程序总代码
具体的接口功能已经全部实现,下面我们需进行逻辑的关联。这里面的逻辑很简单,由于shell程序总是存在的,因此这里我们只需分别套用无限循环对以上三大步骤进行实现即可。若是发现步骤一中的空串情况或步骤三中内建命令,就不在进行以下步骤。
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;#define SIZE 1024
#define MAX_SIZE 64
#define SEP " "char* argv[MAX_SIZE];
char pwd[SIZE];
char env[SIZE];
int lastcode = 0;const char* HostName()
{
char* hostname = getenv("HOSTNAME");
if (hostname)
return hostname;
else
return "None";
}
const char* UserName()
{
char* username = getenv("USER");
if (username)
return username;
else
return "None";
}
const char* WorkDir()
{
char* workdir = getenv("PWD");
if (workdir)
return workdir;
else
return "None";
}char* Home()
{
return getenv("HOME");
}int Interact(char* out, int size)
{
// 输出命令行提示符并获取用户输入的命令字符串
fprintf(stdout, "[%s@%s %s]$ ", UserName(), HostName(), WorkDir());
fgets(out, size, stdin);
out[strlen(out) - 1] = NULL; // fgets也会把'\n'字符存储,这里去掉getcwd此多余字符
return strlen(out);
}void Split(char* in)
{
int i = 0;
//strtok 对输入的字符串指令进行切割
argv[i++] = strtok(in, SEP);
while (argv[i++] = strtok(NULL, SEP))
{ }
// 让ls指令显示的目录和文件添加上该有的颜色
if (strcmp(argv[0], "ls") == 0)
{
argv[i - 1] = (char*)"--color"; // 这里把ls指令的颜色选项功能带上
argv[i] = NULL;
}
}void Execute()
{
pid_t id = fork();
if (id == 0)
{
// 让子进程执行命令
execvp(argv[0], argv);
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid == id) lastcode = WEXITSTATUS(status); // 保存子进程的退出码
}int BuildCommand()
{
int ret = 0;
//1, 检测输入的指令程序是否是内建指令,1表示是,0表示不是
if (strcmp("cd", argv[0]) == 0)
{
ret = 1;
char* target = argv[1]; // 这里cd内建指令可能存在选项参数
if (!target) target = Home(); // cd无参形式的情况
chdir(target); // 改变当前的工作路径,但目前环境变量中的所在路径没有变换。chidir不会影响环境变量
// 这里要注意类似于cd ..的情况
char temp[1024];
getcwd(temp, 1024); // 获取当前进程的绝对路径
snprintf(pwd, SIZE, "PWD=%s", temp); //将获得的路径存放到pwd中
putenv(pwd); //添加或修改当前进程PCB存储中的环境变量
}
// 处理导入系统下的环境变量
else if(strcmp("export", argv[0]) == 0)
{
ret = 1;
if (argv[1]) // export使用时,参数可能为空
{
strcpy(env, argv[1]);
putenv(env);
}
}
else if(strcmp("echo", argv[0]) == 0)
{
ret = 1;
if (argv[1] == NULL) cout << endl;
else
{
if (argv[1][0] == '$')
{
if (argv[1][1] == '?')
{
cout << lastcode << endl;
lastcode = 0;
}
else
{
char* e = getenv(argv[1] + 1);
if (e) cout << e << endl;
}
}
else cout << argv[1] << endl;
}
}
return ret;
}int main()
{
while (true)
{
//1, 输出命令行提示符并获取用户输入的命令字符串
char commandline[SIZE];
int n = Interact(commandline, SIZE);
if (n == 0) continue; // 处理空串的问题
//2, 对命令行字符串进行切割
Split(commandline);
//3, 处理内建命令
n = BuildCommand(); if (n) continue;
//4, 开始执行输入的程序指令
Execute();
}
return 0;
}