
于此止?:个人主页
❄️个人专栏传送门《Linux篇》 《QT百日筑基篇》 《算法篇》
《C++与STL剖析篇》 《数据结构篇》 《Python基础篇》
⭐️纵有狂风拔地起,我亦乘风破万里⭐️
Shell命令行的编写
前言:
欢迎来到《数字世界·Linux内核探秘》系列。
如果说计算机是一个庞大的数字世界,那么进程就是其中鲜活的“生命体”,环境变量是它们寻找资源的“指南针”,而调度机制则是维持整个世界秩序的“总指挥”。
本系列将从地基开始,一步步带你深入Linux内核的腹地,理解进程从诞生、运行到终止的完整生命周期。无论你是刚入门的初学者,还是希望夯实基础的开发者,希望这三讲能帮你建立起对Linux进程管理清晰、系统的认知框架。
我们知识实现对应的操作。
目录/索引:
目录
命令行的样式:
我们这次实现的命令行就是我们的xshell我们输入命令的那个地方。
像我们下面的那种形式我们实现主要实现以下的几种方法:
1.打印出我们对应的命令行。
2.在这个命令行实现普通的命令(形如ls,pwd)。
3.在这个命令行实现我们的内建命令。(cd, echo)

技术栈的要求:
1.我们的c++的string的一些关于字符串的操作。
推荐链接:《STL 时空漫游指南》栈上的精灵:SSO 如何让 std::string 在时间的夹缝中起舞——string使用接口详解-CSDN博客
2.我们c语言的一些简单的操作。
3.我们环境变量的运用:
推荐链接:
【Linux篇】数字世界程序运行寻找地址的指南针——环境变量的详解 -CSDN博客
4.我们在shell中会使用一些程序替换的知识,我们需要了解对应的execl家族的一些函数。
推荐链接:
【Linux篇】数字世界的“总指挥”——进程控制与调度机制详解-CSDN博客
5.make等编译工具的使用,能让我们更简单的进行对应的编译和删除:
推荐链接:
编写我们的Shell命令行:
1.命令行的样式:
首先我们拆解我们对应的命令行:
前面的zb是我们的用户名, 我们获取他的方式有好几种, 这里我们采取环境变量的方式进行获取,我们先看一些我们的环境变量有什么:
![]()
查看所有的环境变量:
env
每个机器的环境变量都不同,这里我们拿我的来进行说明,我们下面有对应的环境变量在代码中我们可以使用getenv()来解析对应的环境变量。他的返回值是这个环境变量的值


我们获取对应的我们需要的环境变量:
我们要将这个环境变量以命令行的形式进行输出:
const char *USERNAME() {
const char *name = getenv("USER");
return name == NULL ? "None" : name;
}
const char *PWD() {
const char *pwd = getenv("PWD");
return pwd == NULL ? "None" : pwd;
}
我们也进行函数封装的形式进行操作:
这里我们
#define COM_LINE_PRI "%s@VM-0-13-ubuntu:%s*" //如果不喜欢这种形式我们后续还能进行更改
#define COM_line_MAX 1024
void _COM_line(char *com_lin, int size) {
snprintf(com_lin, size, COM_LINE_PRI, USERNAME(), PWD());//将我们要输出的内容格式化到我们 //的com_lin数组中
}
void COM_line() {
char Com_line_P[COM_line_MAX]; //将我们的内容定义为1024后续内存不够在进行修改
_COM_line(Com_line_P, sizeof(Com_line_P));//我们调用函数将我们的命令行进行填写
printf("%s", Com_line_P);
fflush(stdout);//保险起见进行刷新缓冲区
}
这里为了方便我们进行编译我们设置对应的makefile文件
shell:shell.cc
g++ -o $@ $^
.PHONY:clean
clean:
rm -f shell
我们将我们上面的内容进行对应的运行操作:
我们发现命令行果然和我们预想的一样,但是好像哪里又不对。
![]()
我们对比一下我们的系统的命令行参数好像家目录是 ~我们的则是对应的路径。使用*是为了区分我们自己的和系统的区别。
当然这里我们要使用我们的getcwd来获取对应当前的路径。
将我们获得路径放入对应的buf中。大小为size。
为什么使用getcwd: 因为在一个路径进行切换的时候我们的进程先进行切换,然后环境变量在进行对应的切换。这里我们的cd的时候会有影响。

所以这里我们要使用striing对我们的字符串进行切分。只是使用string使用起来给更加的简单:

std::string spwd;//定义我们的string我一般喜欢把他定义全局的
#define PWD_MAX 1024
const char *PWD() {
//const char *pwd = getenv("PWD");
//spwd = pwd;
char pwd[PWD_MAX];
getcwd(pwd, PWD_MAX);
spwd = pwd;
if (spwd.find(getenv("HOME")) >= 0)
spwd.replace(0, strlen(getenv("HOME")), 1, '~');//如果我们现在是/home/zb那就替换为~
return spwd.c_str();
}
我们运行:
发现我们的命令行果然是一样的了。所以我们完成了我们的第一步。

这里我们注意我们的命令行只能运行一次我想让他一直进行运行不退出我们要加上循环。
while(1)
{
COM_line(); // 获取命令行的格式
}
这样我们就能一致的进行运行了。
2.输入我们对应的命令:
其实我们输入的ls -l也就是一个个的字符串了。所以我们在输入的时候我们也输入一个个字符串。
我们需要在main函数中定义一个数组,或者全局中定义一个数组,
#define COM_line_MAX 1024 //习惯空间给1kb
char Com_line[COM_line_MAX];
然后我们要给这个字符串进行输入,怎么输入???
像我们的命令那样 ls -l -a,要加上空格。
我们这里的输入方式有很多种,我使用我们的fgets。
这里我给了一个bool的类型,这里要注意的话我们输入的时候会手动的敲一个回车, 我们要将最后一个回车进行置为0。
如果我只写了一个回车我的程序不执行任何的命令,可以直接的往下走。所以我们加上bool类型,有助于我们后续的判断。
bool Input(char *Com_line) {
if (fgets(Com_line, sizeof(Com_line), stdin) == NULL)
return false;
Com_line[strlen(Com_line) - 1] = 0;
if (strlen(Com_line) == 0)
return false; // 我是敲了一个\n
return true;
}
这里我们需要进行判断如果我们失败了我们就continue结束这次的循环,再次输入。
if(Input(Com_line)==false)
continue;
3.切分字符串。
我们在上面输入了对应的字符串的时候,我们后来程序替换的时候也不能进行对应的替换呀,所以这里我们要对我们刚才输入的字符串的形式进行切分,注意我们刚才输入的时候有” “,所以我们可以以” “作为切分的符号进行对应的切分。
这里切分的函数主要使用strtok。
我们传入的第一个参数是要进行切分的字符串, 第二个参数是我们以什么符号进行切割。
这里注意我们第一次切完了下一次再次切分这个字符串的时候我们直接传NULL即可再次切割上一次切分的字符串。

我们要把我们切出来的字符串进行保存。我们可以创建一个数组。我们将他命名为c_argv
还要有一个计数, 计算有几个指令。这里要经常的使用最好定义为全局的。
#define C_ARGV_MAX 128
char *c_argv[C_ARGV_MAX];
size_t c_argc;
下面我们将字符串进行切割。
bool CommandParse(char *Com_line) {
#define SEP " "
c_argc = 0;
c_argv[c_argc++] = strtok(Com_line, SEP);
while (c_argv[c_argc++] = strtok(NULL, " "))
;
c_argc--;
return true;
}
我们可以打印以下看以下里面的内容
for(size_t i=0;i<c_argc;i++)
{
std::cout<< c_argv[i]<< std::endl;
}
我们发现我们刚才输入的命令果然差分出来了,这第三步我们就完成了。

4.执行对应的普通命令:
这里我们要执行对应的命令需要将函数进行程序的替换。
这里我们的命令行列表有了,我们要使用那个函数进行呢???
我们使用的命令最好不带路径,我们有对应的命令名(c_argv[0]),所以这里使用带p的。
我们也有了对应的命令行参数表所我们一定使用带v的。
这里的环境变量对我们没有太大的作用。我们不用带e的
所以我们最后使用execvp
这里我们要创建子进程来完成这件工作。如果使用父进程的话我们想要执行的后续代码都将被替换掉。
bool Par_input() {
pid_t id = fork();
if (id == 0) {
// 子进程进行程序的替换
//环境变量的填写
execvp(c_argv[0], c_argv);
exit(1);
} else if (id > 0) {
Inf_Exit= 0;
pid_t wi = waitpid(id, &Inf_Exit, 0);
if (wi == -1)
exit(1);
return true;
} else {
perror("进程创建失败\n");
exit(1);
}
}
写到这里我们对应的已经能运行一部分的命令了。

但是像我们的cd命令还是不能进行对应的使用的,因为我们的cd命令属于是内建命令。
所以下面我们要进行添加的就是我们对应的内建命令的使用。
5.内建命令的使用:
cd命令
像我们的普通命令已经能够执行了,但是像我们的内建命令,还是不能执行的, 那是因为我们少了一部,我们应该判断我们对应的命令是不是我们对应的内建命令, 如果不是我们对应的内建命令我们就执行创建子进程执行对应的普通命令。
所以判断的时机是什么???
我们应该把字符串拆分出来, 并且先判断对应的命令是不是我们对应的内建命令。
我们这里设置为对应的bool类型
这里的cd主要有两种:
1.我们直接进行cd这样是跳转我们对应的家目录, 或者我们对用的cd ~也是回到我们的家目录。所以我们要获得对应的家目录对应的信息。我们可以使用getenv来进行获得,这里我们注意的是我们对应的环境变量的HOME是我们对应进程的家目录。
所以
当我们对应的只输入一个cd也就是我们的c_argv[0]==cd并且c_argc==1的时候或者我们的c_argv==2并且我们的c_argv[1]==~的时候我们回到对应的家目录。
这里我们要实现cd -,就要定义一个旧的路径。
所以这里我们定义一个之前的路径。定义成全局的。
我们要认识两个系统调用,一个是chdir: 这个函数是改变我们当前的路径。
我们只要传入对应的路径就能给我们进行更改了。

getcwd:获得我们当前的路径。所以我们对应OldPwd需要这个函数来进行实现。
#define PWD_MAX 1024
char OldPwd[PWD_MAX];
std::string home = getenv("HOME");
if (c_argc == 1 || (c_argc == 2 && strcmp(c_argv[1], "~") == 0)) // 只输入了一个cd
{
if (getcwd(OldPwd, PWD_MAX) == nullptr) {
perror("getcwd failed");
return false;
}
if (chdir(home.c_str()) != 0)
return false;
}
我们有对应的oldpwd了所以我们就能实现我们的cd -操作了。
这里也只是先将我们的oldpwd给我们的newpwd。然后在获得当前的路径给我们对应的oldpwd就可以了。
这里我们cd路径的情况:
1.我们直接cd 绝对路径。
2. 我们cd 文件名(这里可以直接的进行写文件名, 因为我们进程对补充我们对应的路径的)。
else if (c_argc == 2) {
std::string newpwd;
if (strcmp(c_argv[1], "-") == 0) {
newpwd = OldPwd;
if (getcwd(OldPwd, PWD_MAX) == nullptr) {
perror("getcwd failed");
return false;
}
} else {
if (getcwd(OldPwd, PWD_MAX) == nullptr) {
perror("getcwd failed");
return false;
}
// 其他路径问题(绝对)
newpwd = c_argv[1];
}
if (chdir(newpwd.c_str()) != 0)
return false;
} else {
// 输多了retun false;
return false;
}
return true;
}
bool Command_built_Execute() {
std::string com = c_argv[0];
if (strcmp(com.c_str(), "cd") == 0) {
std::string home = getenv("HOME");
if (c_argc == 1 || (c_argc == 2 && strcmp(c_argv[1], "~") == 0)) // 只输入了一个cd
{
if (getcwd(OldPwd, PWD_MAX) == nullptr) {
perror("getcwd failed");
return false;
}
if (chdir(home.c_str()) != 0)
return false;
} else if (c_argc == 2) {
std::string newpwd;
if (strcmp(c_argv[1], "-") == 0) {
newpwd = OldPwd;
if (getcwd(OldPwd, PWD_MAX) == nullptr) {
perror("getcwd failed");
return false;
}
} else {
if (getcwd(OldPwd, PWD_MAX) == nullptr) {
perror("getcwd failed");
return false;
}
// 其他路径问题(绝对)
newpwd = c_argv[1];
}
if (chdir(newpwd.c_str()) != 0)
return false;
} else {
// 输多了retun false;
return false;
}
return true;
}
else {
// 不是内建命令
return false;
}
return true;
}
当然这种写法也可以但是不美观我们把他封装为对应的函数。

到这里我们就能直接的运行我们对应的cd命令了
echo命令:
这里我们echo主要实现3个功能:
1.我们的echo 字符串
2.echo 环境变量
3.echo $? 查看对应的提出码
我们对应的退出码是我们上一次进程的退出吗,所以我们要在waitt那里进行创建,要注意这里的创建最好是定义成全局的。
我们记录这个退出码, 还要注意使用完要归0因为我们对应的echo也是一个对应的进程, 也会有自己的退出码。
int Inf_Exit=0;
//这个在创建子进程的时候父进程的回收
pid_t wi = waitpid(id, &Inf_Exit, 0);
if(c_argc <= 1) return false;
std::string es = c_argv[1];
if(es[0]=='$')
{
if(strcmp(es.c_str(),"$?"))
{
std::cout<<((Inf_Exit>>8)&0xff)<<std::endl;//echo $?
Inf_Exit=0;
}
}
这里我们像要进行输入一个string打印出来:
std::string es = c_argv[1];
std::cout<<es<<std::endl;
我们对应的环境变量:
这里要使用一个substr函数, 他是为了剪切我们的$符号。这样我们直接取出对应的环境,变量就可以了。
es = es.substr(1);
std::cout<< getenv(es.c_str())<<std::endl;
bool Echo()
{
if(c_argc <= 1) return false;
std::string es = c_argv[1];
std::cout<<es<<std::endl;
if(es[0]=='$')
{
if(strcmp(es.c_str(),"$?")==0)
{
std::cout<<((Inf_Exit>>8)&0xff)<<std::endl;//echo $?
Inf_Exit=0;
}
else
{
//取出对应的环境变量
es = es.substr(1);
std::cout<< getenv(es.c_str())<<std::endl;
}
}
else
{
std::cout<<es<<std::endl;
}
return true;
}
这里我们测试一下:
我们发现我们都能进行对应的操作。

我们还可以给子进程导入对应的环境变量。其实子进程的环境环境变量是和父进程相同的

总代码展示:
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <unordered_map>
#define COM_LINE_PRI "%s@VM-0-13-ubuntu:%s*"
#define COM_line_MAX 1024
#define C_ARGV_MAX 128
#define PWD_MAX 1024
#define C_ENV_MAX 100
char *c_argv[C_ARGV_MAX];
size_t c_argc;
std::string spwd;
int Inf_Exit = 0;
char OldPwd[PWD_MAX];
char *c_env[C_ENV_MAX];
std::unordered_map<std::string,std::string>mp;
const char *USERNAME() {
const char *name = getenv("USER");
return name == NULL ? "None" : name;
}
const char *PWD() {
// const char *pwd = getenv("PWD");
// spwd = pwd;
char pwd[PWD_MAX];
getcwd(pwd, PWD_MAX);
spwd = pwd;
if (spwd.find(getenv("HOME")) >= 0)
spwd.replace(0, strlen(getenv("HOME")), 1, '~');
return spwd.c_str();
}
void _COM_line(char *com_lin, int size) {
snprintf(com_lin, size, COM_LINE_PRI, USERNAME(), PWD());
}
void COM_line() {
char Com_line_P[COM_line_MAX];
_COM_line(Com_line_P, sizeof(Com_line_P));
printf("%s", Com_line_P);
fflush(stdout);
}
bool Input(char *Com_line) {
if (fgets(Com_line, COM_line_MAX, stdin) == NULL)
return false;
Com_line[strlen(Com_line) - 1] = 0;
if (strlen(Com_line) == 0)
return false; // 我是敲了一个\n
return true;
}
void CommandParse(char *Com_line) {
#define SEP " "
c_argc = 0;
c_argv[c_argc++] = strtok(Com_line, SEP);
while (c_argv[c_argc++] = strtok(NULL, " "))
;
c_argc--;
c_argv[c_argc] = 0;
}
bool Par_input() {
pid_t id = fork();
if (id == 0) {
// 子进程进行程序的替换
// 环境变量的填写
execvp(c_argv[0], c_argv);
exit(1);
} else if (id > 0) {
Inf_Exit = 0;
pid_t wi = waitpid(id, &Inf_Exit, 0);
if (wi == -1)
exit(1);
return true;
} else {
perror("进程创建失败\n");
exit(1);
}
}
bool Cd() {
std::string home = getenv("HOME");
if (c_argc == 1 || (c_argc == 2 && strcmp(c_argv[1], "~") == 0)) // 只输入了一个cd
{
if (getcwd(OldPwd, PWD_MAX) == nullptr) {
perror("getcwd failed");
return false;
}
if (chdir(home.c_str()) != 0)
return false;
} else if (c_argc == 2) {
std::string newpwd;
if (strcmp(c_argv[1], "-") == 0) {
newpwd = OldPwd;
if (getcwd(OldPwd, PWD_MAX) == nullptr) {
perror("getcwd failed");
return false;
}
} else {
if (getcwd(OldPwd, PWD_MAX) == nullptr) {
perror("getcwd failed");
return false;
}
// 其他路径问题(绝对)
newpwd = c_argv[1];
}
if (chdir(newpwd.c_str()) != 0)
return false;
} else {
// 输多了retun false;
return false;
}
return true;
}
bool Echo() {
if (c_argc <= 1)
return false;
std::string es = c_argv[1];
if (es[0] == '$') {
if (strcmp(es.c_str(), "$?") == 0) {
std::cout << ((Inf_Exit >> 8) & 0xff) << std::endl; // echo $?
Inf_Exit = 0;
} else {
// 取出对应的环境变量
es = es.substr(1);
std::cout << getenv(es.c_str()) << std::endl;
}
}
else {
std::cout << es << std::endl;
}
return true;
}
bool Command_built_Execute() {
std::string com = c_argv[0];
if (strcmp(com.c_str(), "cd") == 0) {
if (Cd() == false)
return false;
}
else if (strcmp(com.c_str(), "echo") == 0) {
if (Echo() == false)
return false;
}
else {
// 不是内建命令
return false;
}
return true;
}
int main() {
while (1) {
COM_line(); // 获取命令行的格式
char Com_line[COM_line_MAX]; // 输入命令数组
if (Input(Com_line) == false)
continue;
CommandParse(Com_line); // 切分字符串。
// 执行内建命令
if (Command_built_Execute())
continue;
//创建我们对应的别名
CTAStable();
Par_input(); // 执行子进程
// for(size_t i=0;i<c_argc;i++)
// {
// std::cout<< c_argv[i]<< std::endl;
// }
}
return 0;
}

转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/buhuizhiyuci/article/details/163191451




