之前已经花了不少篇幅学习了linux系统编程的很多知识点:文件与io、进程、信号、管道,而零散的知识点,怎么能够综合的串接起来是学习的一个很重要的目的,当然最好的方式就是用所学的知识点做一个项目了,所以接下来会实现一个小型的迷你shell程序,跟缩减版的系统shell程序,不要看着项目很小,但是五脏俱全,先来看一下我们要实现的功能:

如exit会退出程序等。

另外还能捕捉一些信号,如:ctrl+c,也能忽略一些信号,如:ctrl+\,禁止退出自己的程序,这里就不演示了。

下面就来从零开始一步步实现我们自己的shell程序,首先进行模块的划分:

-------main.c  //这个是一个主控程序

-------parse.c、parse.h  //这个主要是用来进行shell命令的解析的

再编写一个Makefile,由于项目中会由多个.c文件构成,所以很有必要进行整体编译,关于Makefile的写法,可以参考博文:http://www.cnblogs.com/webor2006/p/3789589.html

这个Makefile的编写是比较简单的,这里就不详述了,里面内容如下:

这样,总体的项目编译环境就已经搭建好了,另外说明一下实现的思路:先搭建好一个整体的框架,然后于对其每个模块进行一一细化,最终完善整个功能,所有功能的实现都会按这个思路来,而且很重要的一条就是:步步为营,也就是当写完一段代码后,就立马进行编译运行,来确保每小段代码都成功,这样的话,一点点功能进行拆分,最终实现一个项目,所以接下来,先要实现一个简单的框架功能:

shell循环:也就是当我们在执行完一个命令后,可以接着再进行敲下一个命令,而不是敲一个就立马进行程序退出了。
 
另外,还要说明一下,为了编译方便,这次项目的编写采用EditPlus编辑器,因为它会有一些提示功能,比直接用vim编译要来得方便一些,使用这种方法的前提,是需要Editpus连接到Linux虚拟机上,关于怎么连接这里就不多说了,网上文章一大堆,好了,下面正式进行第一个小功能的编写:
首先先在parse.h头文件中声明shell循环需要的函数:
然后在parse.c中实现这些函数,当然先都空实现,一步步来:
然后main.c中去调用主loop循环:
【说明】:这里都是面向函数进行编程,也就是通常都是有一个.h头文件,一个.c实现文件,学习一下c语言编写的一些规范~
这时,先不管具体实现,先编译一下:
可见木有问题,但是没有shell循环,所以接下来进行修改,当执行程序时,能够进行循环:
这时再次运行:
发生死循环了,这是为什么呢?因为read_command一直是0,还没有去实现任何代码,所以接下来,需要在这个函数中接收用户敲入的命令:
再次编译运行:
最后,是由于敲入了"ctrl+d"传入了一个结束符,导至fgets获取为null,则退出了整个循环,这样shell循环效果就出来了,也就是shell命令的基本框架就已经搭建好了!
但是如果现在按ctrl+c整个程序会退出,如下:
现在,我们不想按ctrl+c时,shell程序退出,则需要进行信号捕获
由于这种类似的操作是属于初始化的,所以将其实现放到专门的.c文件中进行:
下面来编写setup函数,里面注册ctrl+c信号来防止用户按其退出:
开始编译运行看看效果,在运行之前,需要将init.c文件加入下Makefile当中:
开始运行:
从中可以看到,ctrl+c,ctrl+\都不会让其程序退出了,只了按了ctrl+d才会,所以已经成功通过信号来改变了其默认行为
【说明】:由于我虚拟机的原因,在按下ctrl+c时会显示^c字符,按下ctrl+\会显示^\字符,实际上信号是起作用了。
接下来,要来进行命令的解析:
在实现之前,需要进行命令的读取,这个在parse.c中已经简单实现了:
那常量的定义,这里放到统一的头文件中,便于集中管理:
这时在main.c中包含其def.h头文件:
由于现在行只解析一个简单命令,而不包含多个管道命令,所以先在main.c中声明一下命令结构体:
这时,在parse.c中需要使用在main.c中声明的全局变量,当然得用extern关键字喽:
在项目中会用到很多extern的全局变量,如果不封装一下可能每个使用的.c文件都得要声明一下,所以这个做法不是太好,应该还是得跟常量定义的def.h文件一样,得用专门一个头文件来存放extern的全局变量,如下:
这样的话,对于想引用全局变量的地方,就只要包含这个externs.h头文件既可,所以parse.c包含它:
这时,需要修改一下parse.c中的读取命令的函数了:
这个初始化工作应该放在init.c中,于是定义一个初始化的函数,对其变量进行初使化:
这时,应该是在每次执行一次命令时,进行初始化,所以,需修改parse.c中的shell_loop():
并且将读取命令行至全局变量中:
已经改了这么多,这时先来检查一下是否能顺利编译:
原因是由于没有包含init.h头文件,修改下次编译:
下面,则正式开始对命令进行解析,也就是编写parse_command()函数了:
第一步,将我们输入带有参数的命令折分,如下效果:
怎么样来实现呢?下面一点点来实现:
首先将变量指针指向我们解析的总命令字串:
接着,在开始解析之前,需再定义一个全局变量,主要作用如下:
但是又不会去改变cmdline,所以需用另外一个变量来存放,所以在main.c中定义一个新的全局变量:
另外还是在externs.h中进行声明:
下面正式开始解析:
由于可以左边会有空格,所以先将左空格去掉:
下面解析一个命令,最终放到cmd中的args参数里:
为了看到折分命令的效果,每解析到一个命令参数,则将其打印一下:
好了,先编译运行一下:
下面查找一下程序,原来是一个逻辑写错了:
再次执行:
成功解析了第一步,接下来,得执行命令了,这时因为命令的参数都已经解析完了,所以转到执行函数来对这些命令进行调用:
【说明】:关于execvp替换函数,可以参考博文:http://www.cnblogs.com/webor2006/p/3507913.html
这时看下效果:
这是为什么呢?因为我们的shell进程被execvp给替换成执行系统命令了,而系统命令执行完则会退出整个进程,这时怎么解决这个问题呢?
关于这个函数创建进程,没有判断进程创建失败的情况,所以还需完善一下:
最后完整的解析程序如下:
parse.c:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "parse.h"
#include "externs.h"
#include "init.h" /*
* shell主循环
*/
void shell_loop(void){
while(){
printf("[myshell]$ ");
fflush(stdout);
/* 初始化环境 */
init();
if(read_command() == -)
break;
parse_command();
execute_command();
}
printf("\nexit\n");
} /*
* 读取命令
* 成功返回0,失败或者读取到文件结束符(EOF)返回-1
*/
int read_command(void){
if(fgets(cmdline,MAXLINE,stdin) == NULL){
return -;
}
return ;
} /*
* 解析命令
* 成功返回解析到的命令个数,失败返回-1
*/
int parse_command(void){
char *cp = cmdline;
char *avp = avline; int i = ;
while(*cp != '\0'){
/** 去除左空格 **/
while(*cp == ' ' || *cp == '\t')
cp++;
/* 如果到了行尾,跳出循环 */
if(*cp == '\0' || *cp == '\n')
break;
cmd.args[i] = avp; while (*cp != '\0'
&& *cp != ' '
&& *cp != '\t'
&& *cp != '\n')
*avp++ = *cp++;
//printf("[%s]\n",cmd.args[i]);
*avp++ = '\0';
i++;
} return ;
} /*
* 执行命令
* 成功返回0,失败返回-1
*/
int execute_command(void){
pid_t pid = fork();
if(pid == -){
//进程创建失败
ERR_EXIT("fork");
}
if(pid == ) {
//子进程去执行替换函数
execvp(cmd.args[],cmd.args);
}
//父进程等待子进程的退出,这样并不会改变父进程本身的行为,所以进程就不会退出了
wait(NULL);
return ;
}

这节的最终运行效果如下:

好了,这篇写得有点罗嗦,有点长,但是没关系,对我有帮助就成,呵呵,下个篇幅下见~~

linux系统编程综合练习-实现一个小型的shell程序(一)的更多相关文章

  1. linux系统编程综合练习-实现一个小型的shell程序(四)

    上节中已经对后台作业进行了简单处理,基本上要实现的功能已经完了,下面回过头来,对代码进行一个调整,把写得不好的地方梳理一下,给代码加入适当的注释,这种习惯其实是比较好了,由于在开发的时候时间都比较紧, ...

  2. linux系统编程综合练习-实现一个小型的shell程序(三)

    上节中已经实现了对普通命令的解析,包括输入重定向,输出重定向,管道,后台作业,这次就来执行已经解析好的命令,对应的函数为:execute_command(),首先对带有管道的命令进行执行: 比如:&q ...

  3. linux系统编程综合练习-实现一个小型的shell程序(二)

    上节minishell当中,已经初步实现了一个简单命令的解析,这节来继续对更加复杂命令进行解析,包含:输入重定向的解析.管道行的解析.输出重定向的解析以及是否有后台作业的解析,如下: 下面对其进行实现 ...

  4. Linux系统学习笔记之 1 一个简单的shell程序

    不看笔记,长时间不用自己都忘了,还是得经常看看笔记啊. 一个简单的shell程序 shell结构 1.#!指定执行脚本的shell 2.#注释行 3.命令和控制结构 创建shell程序的步骤 第一步: ...

  5. Linux系统编程(33)—— socket编程之TCP程序的错误处理

    上一篇的例子不仅功能简单,而且简单到几乎没有什么错误处理,我们知道,系统调用不能保证每次都成功,必须进行出错处理,这样一方面可以保证程序逻辑正常,另一方面可以迅速得到故障信息. 为使错误处理的代码不影 ...

  6. linux系统编程之进程(一)

    今天起,开始学习linux系统编程中的另一个新的知识点----进程,在学习进程之前,有很多关于进程的概念需要了解,但是,概念是很枯燥的,也是让人很容易迷糊的,所以,先抛开这些抽象的概念,以实际编码来熟 ...

  7. linux系统编程之管道(三)

    今天继续研究管道的内容,这次主要是研究一下命名管道,以及与之前学过的匿名管道的区别,话不多说,进入正题: 所以说,我们要知道命名管道的作用,可以进行毫无关系的两个进程间进行通讯,这是匿名管道所无法实现 ...

  8. linux系统编程之信号(七)

    今天继续学习信号,主要是学习关于时间和定时器相关的函数的使用,关于这个实际上有很多内容,这里先简要进行说明,等之后再慢慢进行相关深入,也主要是为接下来要做的一个综合linux系统编程的例子做准备,好了 ...

  9. Linux系统编程【2】——编写who命令

    学到的知识点 通过实现who命令,学到了: 1.使用man命令寻找相关信息 2.基于文件编程 3.体会到c库函数与系统调用的不同 4.加深对缓冲技术的理解 who命令的作用 who命令的使用 在控制终 ...

随机推荐

  1. socket支持ipv6

    转自:https://www.jianshu.com/p/9926b99a7fef 以前cocos2d-x的项目里都使用的是libwebsocket,如果想支持ipv6,只需要升级相应的库即可,现在有 ...

  2. SpringBoot系列教程JPA之delete使用姿势详解

    原文: 190702-SpringBoot系列教程JPA之delete使用姿势详解 常见db中的四个操作curd,前面的几篇博文分别介绍了insert,update,接下来我们看下delete的使用姿 ...

  3. AppCrawler安装使用

    百度网盘: https://pan.baidu.com/s/1bpmR3eJ mac下安装appium 真机或者模拟器均可. 确保adb devices可以看到就行 启动appium 启动appium ...

  4. mysql高并发配置

    mysql高并发配置 要在mysqld下设置1 修改back_log参数值:由默认的50修改为500.(每个连接256kb,占用:125M)back_log=500<pre> back_l ...

  5. PHP保留两位小数显示

    <?php $num = '10.4567'; //第一种:利用round()对浮点数进行四舍五入 echo round($num,2).PHP_EOL; //10.46 //第二种:利用spr ...

  6. git stash详解

        应用场景: 1 当正在dev分支上开发某个项目,这时项目中出现一个bug,需要紧急修复,但是正在开发的内容只是完成一半,还不想提交,这时可以用git stash命令将修改的内容保存至堆栈区,然 ...

  7. 函数的练习2——python编程从入门到实践

    8-9 魔术师:创建一个包含魔术师名字的列表,并将其传递一个名为show_magicians()的函数,这个函数打印列表中每个魔术师的名字. def show_magicians(magicians) ...

  8. python 之 前端开发( jQuery事件、动画效果、.each()、 .data())

    11.58 事件 11.581 事件绑定方法与解绑 绑定事件: // 绑定方式一: $('.box1').click(function () { alert('绑定方式一') }); ​ // 绑定方 ...

  9. 数据分析-numpy的用法

    一.jupyter notebook 两种安装和启动的方式: 第一种方式: 命令行安装:pip install jupyter 启动:cmd 中输入 jupyter notebook 缺点:必须手动去 ...

  10. ZFS文件系统及Freenas介绍

    一.简介 1.什么是zfs文件系统 ZFS文件系统的英文名称为Zettabyte File System,也叫动态文件系统(Dynamic File System),是第一个128位文件系统.最初是由 ...