精华内容
下载资源
问答
  • c语言编写简单shell解释器

    千次阅读 2016-04-14 14:45:59
    在windows开发环境下写一个简单shell解释器 1、输入一个exe可执行文件路径或命令后能启动该程序 2、输入txt文本文件的路径,能打开该文本文件中所指定的若干exe程序路径及命令打开程序首先要弄清shell解释器其实...

    在windows开发环境下写一个简单shell解释器
    1、输入一个exe可执行文件路径或命令后能启动该程序
    2、输入txt文本文件的路径,能打开该文本文件中所指定的若干exe程序路径及命令打开程序

    首先要弄清shell解释器其实就是一个exe文件,在这个程序输入正确
    命令,就有相应操作执行。

    接着启动程序其实就是在系统中创建进程,
    用win32的api CreateaProcess()函数借口实现。
    该函数原型如下

    CreateProcess(
    
    LPCWSTR lpszImageName, //指向可执行的模块的指针
    
    LPCWSTR lpszCmdLine, //指向可执行命令行字符串的指针
    
    LPSECURITY_ATTRIBUTES lpsaProcess, //CE 不支持
    
    LPSECURITY_ATTRIBUTES lpsaThread, //CE 不支持
    
    BOOL fInheritHandles, //CE 不支持
    
    DWORD fdwCreate, //创建标志
    
    LPVOID lpvEnvironment, //CE 不支持
    
    LPWSTR lpszCurDir, //CE 不支持
    
    LPSTARTUPINFOW lpsiStartInfo, //CE 不支持
    
    LPPROCESS_INFORMATION lppiProcInfo //指向进程信息结构体的指针
    
    );

    该函数最重要的两个参数是lpszImageName和lpszCmdLine。
    lpszImageName代表应用名
    可以直接设为null,只需设定lpszCmdLine为正确的命令行参数即可(即在cmd上运行该程序时需要的命令)。

    当输入字符串后
    该min_shell.exe首先判断是否是直接打开exe可执行文件,还是需通过txt文本内容打开程序,接着创建程序就好。

    #include <windows.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <conio.h>
    #include <string.h>
    //判断输入字符串为txt还是执行exe命令
    int check(char *s, int len) {
        int j = 0, flag = 0;
        for (int i = 0; i < len; i++) {
            if (s[i] == '.' && len - i > 3 && s[i + 1] == 'e' && s[i + 2] == 'x' && s[i + 3] == 'e')
                flag = 1;
        }
        return flag;
    }
    //创建程序
    void CreateProc(char* s) {
        STARTUPINFO si;
        PROCESS_INFORMATION pi;
        ZeroMemory(&si, sizeof(si));
        si.cb = sizeof(si);
        ZeroMemory(&pi, sizeof(pi));
        if (!CreateProcess(NULL, s, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
            fprintf(stderr, "creat Process Failed.\n");
        }
        else {
            fprintf(stderr, "Successfully created.\n");
        }
        WaitForSingleObject(pi.hProcess, 0);
    }
    //读取txt文本内容,并创建程序
    void ReadTxt(char * argv) {
        // get file
        FILE * pFile;
        fopen_s( &pFile, argv,"rb");
        if (pFile == NULL) {
            fputs("File error", stderr);
            exit(1);
        }
    
        // obtain file size:
        fseek(pFile, 0, SEEK_END);
        long lSize = ftell(pFile);
        rewind(pFile);
    
        // allocate memory to contain the whole file:
        char *buffer = (char*)malloc(sizeof(char)*lSize);
        if (buffer == NULL) {
            fputs("Memory error", stderr);
            exit(2);
        }
    
        // copy the file into the buffer:
        size_t result = fread(buffer, 1, lSize, pFile);
        if (result != lSize) {
            fputs("Reading error", stderr);
            exit(3);
        }
    
        char *pBegin = buffer;
        char *pEnd = strchr(pBegin, '\r');//每行都以\r\n结尾
        pBegin = pBegin + 3;
        int lens = 0;
        while (pEnd) {
            char s[2010];
            lens = 0;
            while (pBegin != pEnd) {
                s[lens++] = *pBegin;
                pBegin++;
            }
            s[lens++] = '\0';
            CreateProc(s);
            pBegin = pEnd + 2;//每行都以\r\n结尾所以+2
            pEnd = strchr(pBegin, '\r');
        }
    }
    int main() {
        while (true) {
            printf("$min_shell>");
            char s[2010];
            gets_s(s);
            int len = strlen(s);
            int flag = check(s, len);//判断输入字符串为txt 还是exe
            if (flag == 1) {
                CreateProc(s);
            }
            else {
                ReadTxt(s);
            }
        }
    }
    展开全文
  • 编写自己的Shell解释器——操作系统例子。本期的目的是向大家介绍shell的概念和基本原理,并且在此基础上动手做一个简单shell解释器。同时,还将就用到的一些 linux环境编程的知识做一定讲解
  • 编写自己的Shell解释器

    千次阅读 2012-09-13 12:30:35
    编写自己的Shell解释器 摘要:本期的目的是向大家介绍shell的概念和基本原理,并且在此基础上动手做一个简单shell解释器。同时,还将就用到的一些 linux环境编程的知识做一定讲解。 本文适合的读者对象  对...

    编写自己的Shell解释器

    摘要本期的目的是向大家介绍shell的概念和基本原理,并且在此基础上动手做一个简单shell解释器。同时,还将就用到的一些 linux环境编程的知识做一定讲解。

    本文适合的读者对象

           linux环境上的c语言开发有一定经验;

    linux环境编程(比如进程、管道)有一点了解。

    概述

    本章的目的是带大家了解shell的基本原理,并且自己动手做一个shell解释器。为此,

    首先,我们解释什么是shell解释器。

    其次,我们要大致了解shell解释器具有哪些功能;

    最后,我们具体讲解如何实现一个简单的 shell 解释器,并对需要用到一些 linux环境编程的知识做一定讲解,并提醒你如果想深入掌握,应该去看哪些资料。

          

    Shell解释器是什么?

    Shell解释器是一个程序。对,是一个程序,而且,它就在我们的身边。在linux系统中,当我们输入用户名和密码登陆之后,我们就开始执行一个shell解释器程序,通常是 /bin/bash,当然也可以是别的,比如/bin/sh。(详细概念请看第一期中的shell有关部分)

    提示:在 /etc/passwd 文件中,每个用户对应的最后一项,就指定了该用户登陆之后,要执行的shell解释器程序。

    linux 字符界面下,输入

    man bash

    调出 bash 的帮助页面

    帮助的最开始就对bash下了一个定义:

     

    bash 是一个兼容于 sh 的命令语言解释器,它从标准输入或者文件中读取命令并执行。它的意图是实现 IEEE POSIX标准中对 shell和工具所规范的内容。

     

    Shell解释器的作用

    在登陆 linux 系统之后,屏幕上就会出现一行提示符,在我的机器上,是这样的:

           [root@stevens root]#

          

    这行提示符就是由bash解释器打印出来的,这说明,现在已经处于 bash 的控制之下了,也同时提示用户,可以输入命令。用户输入命令,并回车确认后,bash分析用户的命令,如果用户的命令格式正确,那么bash就按照用户的意思去做一些事情。

    比如,用户输入:

    [root@stevens root]#  echo “hello, world”

    那么,bash就负责在屏幕上打印一行“hello world”。

     

    如果,用户输入:

    [root@stevens root]#  cd /tmp

    那么,bash就把用户的当前目录改变为 /tmp

    所以,shell解释器的作用就是对用户输入的命令进行“解释”,有了它,用户才可以在 linux 系统中任意挥洒。没有它的帮助,你纵然十八般本领在身,也施展不出。

    bash每次在“解释”完用户命令之后,又打印出一行提示符,然后继续等待用户的下一个命令。这种循环式的设计,使得用户可以始终处于 bash 的控制之下。除非你输入 exitlogout明确表示要退出 bash

     

    Shell语法梗概

    我们不停的命令 bash 做这做那,一般情况下它都很听话,按你的吩咐去做。可有时候,它会对你说:“嗨,老兄,你的命令我理解不了,无法执行”。例如,你输入这样的命令:

     

    [root@stevesn root]# aaaaaa

     

    bash会告诉你:

    bash: aaaaaa: command not found

     

    是的,你必须说的让它能听懂,否则它就给你这么一句抱怨,当然也还会有其它的牢骚。

    那么,什么样格式的命令,它才能正确理解执行了?这就要引出shell 的语言规范了。

    Shell作为一个命令语言解释器,有一套自己的语言规范,凡是符合这个规范的命令,它就可以正确执行,否则就会报错。这个语言规范是在 IEEE POSIX的第二部分:“shelltools规范”中定义的。关于这份规范,可以在这里看到。

    官方的东西,总是冗长而且晦涩,因为它要做到面面俱到且不能有破绽。如果读者有兴趣,可以仔细研究这份规范。而我们的目的只是理解shell的实现思想,然后去实现一个简单的 shell 解释器,所以没必要陷入枯燥的概念之中。

    现在请继续在 linux 字符界面下输入 man bash,调出 bash 的帮助页面,然后找到 shell语法”那一部分,我们就是以这里的描述作为实现的依据。

    bash帮助的“shell 语法”一节,是这样来定义shell 语法的:

    l         简单命令

    简单命令是(可选的)一系列变量赋值, 紧接着是空白字符分隔的词和重定向符号, 最后以一个控制操作符结束. 第一个词指明了要执行的命令, 它被作为第 0 个参数. 其余词被作为这个命令的参数.

           这个定义可以这样来理解:

    1、  可以有变量赋值,例如

    a=10 b=20 export a b

    2、  “词”是以空白字符分隔开的,空白字符包括制表符(tab)和空格,例如:

    ls /tmp

    就是两个词,一个 ls,一个 /tmp

    3、可以出现重定向符号,重定向符号是“>”和“<”,例如:

    echo “hello world” > /tmp/log

    4、  简单命令结束于控制操作符,控制操作符包括:

    ||  &   &&     ;   ;;  ( )   |  <newline>

    例如,用户输入:

    ls /tmp

    用户最后敲的回车键就是控制操作符 newline,表示要结束这个简单命令。

    如果用户输入:

    echo “100” ; echo “200”

    那么这是两个简单命令,第一个结束于“;”,第二个结束于newline

    5、  简单命令的第一个词是要执行的命令,其余的词都是这个命令的参数,例如:

    echo “hello world” echo

    第一个echo 是命令,第二个词“hello world”是参数1,第三个词 echo 是参数2,而不再作为一个命令了。

    简单命令是 shell 语法中最小的命令,通过简单命令的组合,又可以得到管道命令和列表命令。

    l         管道(命令)

    管道是一个或多个简单命令的序列,两个简单命令之间通过管道符号(“|”)来分隔

    例如

    echo “hello world” | wc –l

    就是一个管道,它由两个简单命令组成,两个简单命令之间用管道符号分隔开。

    我们可以看到,管道符号“|”也是属于上面提到的控制操作符。

    根据这个定义,一个简单命令也同时是一个管道。

    管道的作用是把它前面的那个简单命令的输出作为后面那个简单命令的输入,就上面这个例子来说:

    echo “hello world” 本来是要在标准输出(屏幕)上打印 “hello world” 的,但是管道现在不让结果输出到屏幕上,而是“流”到 wc –l 这个简单命令,wc –l 就把“流”过来的数据作为它的标准输入进行计算,从而统计出结果是 1 行。

    关于管道更详细的内容,我们在后面具体实现管道的时候再说明。

    l         列表(命令):

    列表是一个或多个管道组成的序列,两个管道之间用操作符 ;, &, &&, || 分隔。我们看到,这几个操作符都属于控制操作符。

    例如

    echo “hello world” | wc –l ; echo “nice to meet you”

    就是一个列表,它由两个管道组成,管道之间用分号(;)隔开

    分号这种控制操作符仅仅表示一种执行上的先后顺序。

    l         复合命令

           这个定义比较复杂,实现起来也有相当难度,在咱们这个示例程序中,就不实现了。

    以上是 shell 语法规范的定义,我们的 shell 程序就是要以此规范为依据,实现对简单命令、管道和列表的解释。对于列表中的控制操作符,我们只支持分号(;),其它的留给读者自己来实现。

           接下来,我们具体介绍如何实现一个简单的 shell解释器。

    实现shell实例

    程序主框架

           主程序很简单,它在做一些必要的初始化工作之后,进入这样一个循环:

    u       打印提示符并等待用户输入

    u       获取用户输入

    u       分析用户输入

    u       解释执行;

    如果用户输入 logout或者 exit 之后,才退出这个循环。

    用类似伪代码的形式表示如下:

    while(1) {

           print_prompt();

           get_input();

           parse_input();

           if(“logout” || “exit”)

                  break;

           do_cmd();

    }

     

    读取用户输入

    如何获取用户输入?一种方法是通过 getchar() 从标准输入每次读一个字符,如果读到的字符是 ‘/n’,说明用户键入了回车键,那么就把此前读到的字符串作为用户输入的命令。

    代码如下:

     

    int len = 0;

    int ch;

    char buf[300];

     

    ch = getchar();

    while(len < BUFSIZ && ch != '/n') {

           buf[len++] = ch;

           ch = getchar();

    }

    if(len == BUFSIZ) {

           printf("command is too long/n");

           break;

    }

    buf[len] = '/n';

    len++;

    buf[len] = 0;

     

    但是,我们注意到,在 bash 中,可以用“<-”和“->”键在命令行中左右移动,可以用上下键调用以前使用的命令,可以用退格键来删除一个字符,还可以用 tab 键来进行命令行补全。我们的shell如果也要支持这些功能,那么就必须对这些键进行处理。这样仅仅对用户输入的读取就非常麻烦了。

    实际上,任何需要一个获取用户输入的程序,都会涉及到同样的问题,如何象bash 那样处理键盘?GNU readline 库就是专门解决这个问题的,它把对键盘的操作完全封装起来,对外只提供一个简单的调用接口。有了它,对键盘的处理就不再让人头疼了。

    关于 readline 库的详细信息,可以通过 man readline 来看它的帮助页面。在我们的 shell 程序中,我是这样来使用 readline的。

     

    char* line;

    char prompt[200];

    while(1) {

           set_prompt(prompt);

           if(!(line = readline(prompt)))

                  break;

           。。。。。。

    }

     

    首先通过 set_prompt() 来设置要输出的提示符,然后以提示符作为参数调用 readline(),这个函数等待用户输入,并动态创建一块内存来保存用户输入的数据,可以通过返回的指针 line 得到这块内存。在每次处理完用户输入的命令之后,我们必须自己负责来释放这块内存。

    有了 readline 之后,我们就可以象 bash 那样使用键盘了。

    在通过 readline 获取用户输入之后,下一步就是对用户输入的命令进行分析。

     

    命令行分析

    对命令行的分析,实际上是一个词法分析过程。学过编译原理的朋友,都听说过 lex yacc 的大名,它们分别是词法分析和语法分析工具。Lex yacc 都有GNU的版本(open source 的思想实在是太伟大了,什么好东东都有免费的用),分别是 flex bison

    所谓“工欲善其事,必先利其器”,既然有这么好的工具,那我们就不必辛辛苦苦自己进行词法分析了。对,我们要用 lex 来完成枯燥的命令行词法分析工作。

    “去买本《lexyacc》(中国电力出版社)来看吧。第一次学当然稍微有点难度,不过一旦掌握了,以后再碰到类似问题,就可以多一个利器,可以节省劳动力了

    在我们的这个 shell 程序中,用 flex 来完成词法分析工作。相对语法分析来说,词法分析要简单的多。由于我们只是做一个简单的 shell,因此并没有用到语法分析,而实际上在 bash 的实现代码中,就用到了语法分析和 yacc

    关于 lex 的细节,在这里我就不能多说了。Lex程序,通常分为三个部分,其中进行语法分析工作的就是它的第二部分: “规则”。规则定义了在词法分析过程中,遇到什么样的情况,应该如何处理。

    词法分析的思路,就是根据前面定义的“shell语法规范”来把用户输入的命令行拆解成

    首先,我们要把用户输入的命令,以空白字符(tab键或者空格)分隔成一个个的参数,并把这些参数保存到一个参数数组中。但是,这其中有几种特殊情况。

    一、如果遇到的字符是;”、“>”、“<”或“|,由于这些符号是管道或者列表中所用到的分隔符,因此必须把它们当作一个单独的参数。

    二、以双引号(括起来的字符串要作为一个单独的参数,即使其中出现了空白字符、“;”、“>”、“<”、“|”。其实,在POSIX标准中,对引号的处理相当复杂,不仅包括双引号(),还有单引号()、反引号(`),在什么情况下,应该用什么样的引号以及对引号中的字符串应该如何解释,都有一大堆的条款。我们这里只是处理一种极简单的情况。

     

    其次,如果我们遇到换行符(’/n’),那么就结束本次命令行分析。根据前面定义的 shell 语法规范,最上层的是列表命令,因此下一步是把所有的参数作为一个列表命令来处理。

     

    根据这个思路,我们来看对应的 lex 规则。

    %%

     

    "/""            {BEGIN QUOTE;}

    <QUOTE>[^/n"]+  {add_arg(yytext);}

    <QUOTE>"/""     {BEGIN 0;}

    <QUOTE>/n       {BEGIN 0; do_list_cmd(); reset_args();}

    ";"             {add_simple_arg(yytext);}

    ">"             {add_simple_arg(yytext);}

    "<"             {add_simple_arg(yytext);}

    "|"             {add_simple_arg(yytext);}

    [^ /t/n|<>;"]+  {add_arg(yytext);}

    /n              {do_list_cmd(); reset_args();}

    .               ;

    %%

     

     

    我们对这些规则逐条解释:

    144条规则,目的是为了在命令行中支持引号,它们用到了 lex 规则的状态特性。

    1"/""            {BEGIN QUOTE;}

    2<QUOTE>[^/n"]+  {add_arg(yytext);}

    3<QUOTE>"/""     {BEGIN 0;}

    4<QUOTE>/n       {BEGIN 0; do_list_cmd(); reset_args();}

     

    1、  如果扫描到引号(),那么进入 QUOTE 状态。在这个状态下,即使扫描到空白字符或“;”、“>”、“<”、“|”,也要当作普通的字符。

    2、  如果处于 QUOTE状态,扫描到除引号和回车以外的字符,那么调用 add_arg()函数,把整个字符串加入到参数数组中。

    3、  如果处于QUOTE状态,扫描到引号,那么表示匹配了前面的引号,于是恢复到默认状态。

    4、  如果处于QUOTE状态,扫描到回车,那么结束了本次扫描,恢复到默认状态,并执行 do_list_cmd(),来执行对列表命令的处理。

     

    以下几条规则,是在处于默认状态的情况下的处理。

    5";"              {add_simple_arg(yytext);}

    6">"              {add_simple_arg(yytext);}

    7"<"              {add_simple_arg(yytext);}

    8"|"               {add_simple_arg(yytext);}

    9[^ /t/n|<>;"]+      {add_arg(yytext);}

    10/n               {do_list_cmd(); reset_args();}

     

     

    5、  如果遇到分号(;),因为这是一个列表命令结束的操作符,所以作为一个单独的参数,执行 add_simple_arg(),将它加入参数数组。

    6、  如果遇到 >,因为这是一个简单命令结束的操作符,所以作为一个单独的参数,执行 add_simple_arg(),将它加入参数数组。

    7、  如果遇到 <,因为这是一个简单命令结束的操作符,所以作为一个单独的参数,执行 add_simple_arg(),将它加入参数数组。

    8、  如果遇到管道符号(|),因为这是一个管道命令结束的操作符,所以作为一个单独的参数,执行 add_simple_arg(),将它加入参数数组。

    9、  对于不是制表符(tab)、换行符(’/n’)、| <>和分号(;)以外的字符序列,作为一个普通的参数,加入参数数组。

    10、              如果遇到换行符,那么结束本次扫描,执行 do_list_cmd(),来执行对列表命令的处理。

    11、              对于任意其它字符,忽略

    通过 lex 的“规则”把用户输入的命令行分解成一个个的参数之后,都要执行 do_list_cmd() 来执行对列表命令的处理。

     

    命令处理

    首先是对处于“shell语法规范”中最上层的列表命令的处理。

    l         列表命令的处理过程:

    依次检查参数数组中的每一个参数,如果是分号(;),那么就认为分号前面的所有参数组成了一个管道命令,调用 do_pipe_cmd() 来执行对管道命令的处理。如果扫描到最后,不再有分号出现,那么把剩下的所有参数作为一个管道命令处理。

    代码很简单:

    static void do_list_cmd()

    {

           int i = 0;

           int j = 0;

           char* p;

           while(argbuf[i]) {

                  if(strcmp(argbuf[i], ";") == 0) {//  ;

                         p = argbuf[i];

                         argbuf[i] = 0;

                         do_pipe_cmd(i-j, argbuf+j);

                         argbuf[i] = p;

                         j = ++i;

                  } else

                         i++;

           }

           do_pipe_cmd(i-j, argbuf+j);

    }

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     


    接下来是对管道命令的处理。

    管道命令的处理

    管道是进程间通信(IPC)的一种形式,关于管道的详细解释在《unix高级环境编程》第14章:进程间通信以及《unix网络编程:第2卷:进程间通信》第4章:管道和FIFO中可以看到。

    我们还是来看一个管道的例子:

    [root@stevens root]#  echo “hello world”|wc –c |wc –l

    在这个例子中,有三个简单命令和两个管道。

    第一个命令是 echo “hello world”,它在屏幕上输出 hello world。由于它后面是一个管道,因此,它并不在屏幕上输出结果,而是把它的输出重定向到管道的写入端。

    第二个命令是 wc –c,它本来需要指定输入源,由于它前面是一个管道,因此它就从这个管道的读出端读数据。也就是说读到的是 hello worldwc –c 是统计读到的字符数,结果应该是12。由于它后面又出现一个管道,因此这个结果不能输出到屏幕上,而是重定向到第二个管道的写入端。

    第三个命令是 wc –l。它同样从第二个管道的读出端读数据,读到的是12,然后它统计读到了几行数据,结果是1行,于是在屏幕上输出的最终结果是1

    在这个例子中,第一个命令只有一个“后”管道,第三个命令只有一个“前”管道,而第二个命令既有“前”管道,又有“后”管道。

    在我们处理管道命令的do_pipe_cmd()函数中,它的处理过程是:

    首先定义两个管道 prefd postfd,它们分别用来保存“前”管道和“后”管道。此外,还有一个变量 prepipe 来指示“前”管道是否有效。

    然后依次检查参数数组中每一个参数,如果是管道符号(|),那么就认为管道符号前面所有的参数组成了一个简单命令,并创建一个“后”管道。如果没有“前”管道(管道中第一个简单命令是没有“前”管道的),那么只传递“后”管道来调用do_simple_cmd(),否则,同时传递“前”管道和“后”管道来调用 do_simple_cmd()

    执行完以后,用“前”管道来保存当前的“后”管道,并设置“前”管道有效标识prepipe,继续往后扫描。如果扫描到最后,不再有管道符号出现,那么只传递“前”管道来调用do_simple_cmd()

    代码如下:

    int i = 0, j = 0, prepipe = 0;

    int prefd[2], postfd[2];

    char* p;

    while(argv[i]) {

           if(strcmp(argv[i], "|") == 0) { // pipe

                  p = argv[i];

                  argv[i] = 0;

                  pipe(postfd);         //create the post pipe

                  if(prepipe)     

                         do_simple_cmd(i-j, argv+j, prefd, postfd);

                  else

                         do_simple_cmd(i-j, argv+j, 0, postfd);

                  argv[i] = p;

                  prepipe = 1;

                  prefd[0] = postfd[0];

                  prefd[1] = postfd[1];

                  j = ++i;

           } else

                  i++;

    }

    if(prepipe)

           do_simple_cmd(i-j, argv+j, prefd, 0);

    else

           do_simple_cmd(i-j, argv+j, 0, 0);

     

    最后,我们分析简单命令的处理过程。

    简单命令处理过程

    我们已经看到,对列表命令和管道命令的处理,实际只是一个分解过程,最终命令的执行还是要由简单命令来完成。

    在简单命令的处理过程中,必须考虑以下情况:

    1、区分内部命令和外部命令

    根据简单命令的定义,它的第一个参数是要执行的命令,后面的参数作为该命令的参数。要执行的命令有两种情况:

    一种是外部命令,也就是对应着磁盘上的某个程序,例如 wcls等等。对这种外部命令,我们首先要到指定的路径下找到它,然后再执行它。

    二是内部命令,内部命令并不对应磁盘上的程序,例如cdecho等等,它需要shell自己来决定该如何执行。例如对 cd 命令,shell就应该根据它后面的参数改变当前路径。

    对于外部命令,需要创建一个子进程来执行它,而对于内部命令,则没有这个必要。

    外部命令的执行,是通过 exec 函数来完成的。有六种不同形式的 exec 函数,它们可以统称为 exec 函数。我们使用的是 execv()。关于 exec的细节,请看《unix环境高级编程》第8章:进程控制。

    对于内部命令,我们目前支持五种,分别是:

    exit:退出shell解释器

    cd:改变目录

    echo:回显

    export:导入或显示环境变量

    history:显示命令历史信息

    这几个内部命令分别由 do_exit()do_cd()do_echo()do_export()do_history()来实现。

    2、处理重定向

    在简单命令的定义中,包括了对重定向的支持。重定向有多种情况,最简单的是输入重定向和输出重定向,分别对应着“<”和“>”。

    输入重定向,就是把“<”后面指定的文件作为标准输入,例如:

    wc < xxx

           表示把 xxx 这个文件的内容作为 wc 命令的输入。

    输出重定向,就是把“>”后面指定的文件作为标准输出,例如:

    echo “hello world” > xxx

    表示把 echo “hello world” 的结果输入到 xxx 文件中,而不是屏幕上。

    为了支持重定向,我们首先对简单命令的参数进行扫描,如果遇到“<”或者“>”那么就认为遇到了重定向,并把“<”或者“>”符号后面的参数作为重定向的文件名称。

    对于输入重定向,首先是以只读方式打开“<”后面的文件,并获得文件描述符,然后将该文件描述符复制给标准输入。

    对于输出重定向,首先是以写方式打开“>”后面的文件,并获得文件描述符,然后将该文件描述符复制给标准输出。

    具体实现在 predo_for_redirect() 函数中:

    3、管道的实现

    管道的实现实际上也是一种重定向的处理。对于“前”管道,类似于输入重定向,不同的是,它是把一个指定的描述符(“前”管道的输出端)复制给标准输入。对于“后”管道,类似于输出重定向,不同的是,它把一个指定的描述符(“后”管道的输入端)复制给标准输出。

    在对管道的处理上,还必须要注意管道和输入或输出重定向同时出现的情况,如果是一个“前”管道和一个输入重定向同时出现,那么优先处理输入重定向,不再从“前”管道中读取数据了。同样,如果一个“后”管道和一个输出重定向同时出现,那么优先处理输出重定向,不再把数据输出到“后”管道中。

           至此,我们已经描述了实现一个简单的 shell 解释器的全部过程,相应的代码和 makefile 在我们的网站上可以下载。希望大家能够结合代码和这篇文章,亲自动手做一次,以加深对shell 解释器的理解

    展开全文
  • 编写自己的Shell解释器,让你有更多的实践机会
  • Linux中编写Shell脚本

    万次阅读 多人点赞 2018-10-08 11:22:35
    Shell脚本编写规范 Shell 中的变量 变量的算术运算 双小括号 (()) 数值运算命令的用法 let 运算命令的用法 expr 命令的用法 br 命令的用法 $[]符号的运算示例 Shell脚本的条件测试 几种条件测试语句 文件...

    目录

    Shell

    Shell脚本的执行

    Shell脚本编写规范

    Shell 中的变量

    变量的算术运算

    双小括号 (())  数值运算命令的用法

    let 运算命令的用法

    expr 命令的用法

    br 命令的用法

    $[] 符号的运算示例

    Shell脚本的条件测试

    几种条件测试语句

    文件测试操作符

    字符串测试操作符

    整数二元比较操作符

    逻辑操作符

    测试表达式 test 、[] 、[[]] 、 (()) 的区别

    if 条件判断语句

    case 条件判断语句

    for循环语句

    while循环语句

    Break、Continue、exit 循环控制语句

    Shell脚本执行scrapy爬虫和python脚本


    Shell

    Shell是一个命令解释器,它的作用是解释执行用户输入的命令及程序等。 用户每输入一条命令,Shell就执行一条。这种从键盘输入命令,就可以立即得到回应的对话方式,称为交互的方式。

    当命令或程序语句不在命令行下执行,而是通过一个程序文件来执行时,该程序文件就被称为Shell脚本。 在Shell脚本里内置了很多命令、语句及循环控制,然后将这些命令一次性执行完毕,这种通过文件执行脚本的方式称为非交互的方式。 Shell脚本语言很适合用于处理纯文本型的数据,而Linux系统中几乎所有的配置文件、日志文件,以及绝大对数的启动文件都是纯文本类型的文件。
    Shell 脚本的类型
    Shell脚本语言是弱类型语言(无须定义变量的类型即可使用),在Unix/Linux中主要有两大类shell: 
    一类是 Bourne  shell ,另一类是 C shell

    1. Bourne shell 包括 Bourne shell (sh)、Korn shell(ksh)、Bourne Again Shell 三种类型。 
    2. C shell包括csh、tcsh两种类型

    查看系统默认的shell: echo  $SHELL
    查看系统支持的shell: cat  /etc/shells

    Shell脚本的执行

    Shell脚本的执行通常可以采用以下几种方式。

    1、bash script-namesh script-name     这是当脚本文件本身没有执行权限时常使用的方法

    2、path/script-name./script-name    在当前路径下执行脚本,需要将脚本文件的权限改为可执行。然后使用脚本的绝对路径或相对路径就可以直接执行脚本了。

    3、source script-name.  script-name 这种方法通常是使用source或 “.”(点号)读入或加载指定的Shell脚本文件,然后依次执行指定的Shell脚本中的语句。这些语句将在当前父 shell 脚本进程中执行(其他几种模式都会启用新的进程执行该脚本进程)。

    Shell脚本编写规范

    Shell脚本的开发规范及习惯非常重要,虽然这些规范不是必须要遵守的,但有了好的规范和习惯,可以大大提升开发效率,并能在后期降低对脚本的维护成本。

    1、一个规范的Shell脚本在第一行会指出由哪个程序(解释器)来执行脚本中的内容,这一行内容在Linux bash的编程一般为:#!/bin/bash  或 #!  /bin/sh     bash 与 sh 的区别 , sh 为 bash的软连接,大多数情况下,脚本使用“#!/bin/bash”和“#!/bin/sh”是没有区别的,但更规范的写法是在脚本的开头使用    #!/bin/bash 

    2、在shell脚本中,跟在 # 后面的内容表示注释,用来对脚本进行注释说明,注释部分不会被当做程序来执行,仅仅是给开发者和使用者看的,系统解释器是看不到的,更不会执行。注释可以自成一行,也可以跟在脚本命令的后面与命令在同一行。 注释尽量不要使用中文,在脚本中最好也不要有中文。

    3、Shell脚本的开头会加版本、版权等信息

    # Date:16:29 2018-10-20 
    # Author: Create by xiaoxie
    # Description: This script function is …… 
    # Version: 1.1 

    4、在shell脚本中尽量不用中文注释,尽量用英文注释,防止本机或切换系统环境后中文乱码的困扰。

    5、Shell脚本的命名应以.sh为扩展名 例如:1.sh

    6、成对的符号应尽量一次性写出来,然后退格在符号内增加内容,以防止遗漏。这些成对的符号包括: {}、[]、‘’、“”  等

    7、中括号[]两端至少要有1个空格,因此,键入中括号时即留出空格[  ],然后在退格键入中间内容,并确保两端都至少由一个空格。

    8、对于流程控制语句,应一次性将格式写完,再添加内容。 如:一次性完成for循环语句的格式

    for
    do
         内容
    done

    9、通过缩进让代码更易读,如:

    if 条件内容     
           then         
                内容 
    fi 

    10、对于常规变量的字符串定义变量值应加双引号,并且等号前后不能有空格,需要强引用的,则用单引号(‘’),如果是命令的引用,则用反引号(``)。

    11、脚本中的单引号、双引号及反引号必须为英文状态下的符号。

    Shell 中的变量

    简单地说,变量就是用一个固定的字符串(也可能是字符、数字等的组合)代替更多、更复杂的内容,该内容里可能还会包含变量、路径、字符串等其他内容。 变量是暂时存储数据的地方及数据标记,所存储的数据存在于内存空间中,通过正确地调用内存中变量的名字就可以读取出与变量对应的数据。

    变量的赋值方法为: 先写变量名称,紧接着是 "=" ,最后是值。中间无任何空格。 通过echo命令加上  $变量名,即可输出变量的值。 双引号,以防止出错变量的值一般要加上。

    定义变量时变量名建议用大写,如  A=xie     B=99

    read  -p  “提示信息”   变量名      #交互式赋值方法

    查看变量内容 echo $A  或  echo ${A}

    赋值时使用引号的作用

    • 双引号:允许通过$符号引用其他变量值
    • 单引号:禁止引用其他变量值,$视为普通字符
    • 反撇号:命令替换,提取命令执行后的输出结果 全局变量的定义方法 export 变量名

    位置参数

    位置参数是一种在调用 Shell 程序的命令行中按照各自的位置决定的变量,是在程序名之后输入的参数。 位置参数之间用空格分隔,Shell取第一个位置参数替换程序文件中的 $1,第二个替换 $2 , 依次类推。$0 是一个特殊变量,它的内容是当前这个shell程序的文件名,所以 $0 不是一个位置参数。

    假如我现在有一个 1.sh脚本文件,内容如下

    #! /bin/bash
    echo $1
    echo $(($2+$3))
    

    当我执行时,我在文件名后加3个参数

    预定义变量

    预定义变量和环境变量相类似,也是在Shell一开始就定义的变量,不同的是,用户只能根据shell的定义来使用这些变量,所有预定义变量都是由符号“$”和另一个符号组成。 常见的Shell预定义变量有以下几种。

    • $# :位置参数的数量
    • $* :所有位置参数的内容
    • $? :命令执行后返回的状态,0表示没有错误,非0表示有错误
    • $$ :当前进程的进程号
    • $! :后台运行的最后一个进程号
    • $0 :当前执行的进程名

    假如我现在有一个 1.sh脚本文件,内容如下

    #! /bin/bash
    
    echo $1
    echo ${2}+${3}
    
    echo $#             #打印出位置参数的数量
    echo $*             #打印出位置参数的内容
    echo $?             #打印出命令执行后返回的状态
    echo $$             #打印出当前进程的进程号
    echo $0             #打印出当前进程的进程名
    

    当我执行时,我在文件名后加3个参数

    参考文章:Linux中环境变量的设置

    变量的算术运算

    Shell中常见的算术运算符

    Shell 中常见的算术运算命令

    双小括号 (())  数值运算命令的用法

    双小括号 (()) 的作用是进行数值运算与数值比较,它的效率很高,用法灵活,是Linux下常用的运算操作符。其操作方法如下:

    let 运算命令的用法

    let运算命令的语法格式为: let 赋值表达式  

    let 赋值表达式的功能等同于“((赋值表达式))” 范例:  给变量 i 加8

     

    expr 命令的用法

    1、expr 用于运算 
       语法:expr 表达式 
       范例:expr 2 + 2 ; expr 2 – 2 ; expr 2  \*  2 ;  expr 2  /  2    
       注意:运算符号和数字之间要有空格!!

    2、expr配合变量计算

    expr在Shell中可配合变量进行计算,但需要用反引号将计算表达式括起来。

    3、利用 expr 计算字符串的长度

    br 命令的用法

    bc 是UNIX/Linux下的计算器,除了作为计算器来使用,还可以作为命令行计算工具使用

    交互模式 在shell命令行直接输入 bc 及能进入bc语言的交互模式

    bc也可以进行非交互式的运算,方法是与 echo 一起使用,所以我们就可以写在脚本里面

    $[] 符号的运算示例

    Shell脚本的条件测试

    通常,在shell的各种条件结构和流程控制结构中都要进行各种测试,然后根据测试结果执行不同的操作,有时候也会与 if 等条件语句相结合,来完成测试判断,以减少程序运行错误。

    几种条件测试语句

    文件测试操作符

    常用文件测试操作符说明
    -d   , d的全拼为 directory文件存在且为目录则为真
    -f   ,   f的全拼为  file文件存在且为文件则为真
    -e , e的全拼为 exists文件存在则为真
    -s ,s的全拼为 size文件存在且大小不为0则为真
    -r ,r的全拼为 read文件存在且可读则为真
    -w ,w的全拼为write文件存在且可写则为真
    -x ,x的全拼为executable文件存在且可执行则为真
    -L ,L的全拼为link文件存在且为链接文件则为真
    f1 -nt  f2  ,nt的全拼为 newer than文件f1比文件f2新则为真
    f1 -ot f2 ,ot的全拼为older than文件f1比文件f2旧则为真

    注:(())不能用于文件测试,文件测试一般常用的是 []

    字符串测试操作符

    常用字符串测试操作符说明
    -n  若字符串长度不为0,则为真
    -z若字符串长度为0,则为真
    “字符串1”  ==  “字符串2”若字符串1等于字符串2,则为真
    “字符串1”  !=  “字符串2”若字符串1不等于字符串2,则为真

    注: == 和 !=  两端要有空格  ,(())不能用于字符测试

    整数二元比较操作符

    在[]以及test中使用的比较符号在(())和[[]]中使用的比较符号说明
    -eq== 或 =相等,全拼为  equal
    -ne!=

    不相等,全拼为 not equal

    -gt>大于,全拼为 greater than
    -ge>=大于等于,全拼为 greater equal
    -lt<小于,全拼为 less than
    -le<=小于等于 ,全拼为less equal
    • "="和"!="也可以在[]中作比较使用,但在[]中使用包含"<"和">"的符号时,需要用反斜线转义,有时不转义虽然语法不会报错,但是结果可能不对。
    • 也可以在[[]]中使用包含“-gt”和“-lt”的符号,但是不建议使用
    • 比较符号两端也要有空格,[] (())  [[]]  两端都要有空格

    逻辑操作符

    在[]和test中使用的操作符在[[]]和(())中使用的操作符说明
    -a&&and ,与,两端都为真,才为真
    -o||or ,或, 两端有一个为真,就为真
    !!not ,非, 两端相反,则结果为真

    测试表达式 test 、[] 、[[]] 、 (()) 的区别

    测试表达式符号test[][[]](())
    边界是否需要空格需要需要需要不需要
    逻辑操作符! 、-a、 -o! 、-a、 -o! 、&& 、 ||! 、&& 、 ||
    整数比较操作符-eq 、 -gt 、-lt、-ge 、-le-eq 、 -gt 、-lt、-ge 、-le-eq 、 -gt 、-lt、-ge 、-le 或  = 、>  、< 、 >= 、 <== 、>  、< 、 >= 、 <=
    字符串比较操作符= 、 == 、!== 、 == 、!== 、 == 、!=不支持
    文件操作-d、-f、-e、-r、-s、-w、-x、-L、-nt、-ot-d、-f、-e、-r、-s、-w、-x、-L、-nt、-ot-d、-f、-e、-r、-s、-w、-x、-L、-nt、-ot不支持
    是否支持通配符匹配

    不支持

    不支持支持不支持

    if 条件判断语句

    #####单条件判断##############
    if  条件判断
      then 
          命令
    else
          命令
    fi
    
    #或
    
    if  条件判断;then 
         命令
    else
         命令
    fi

    ###双条件判断#####
    if 条件判断
      then
          命令
    elif 条件判断
      then 
          命令
    else
       命令
    fi
    
    ##或
    if 条件判断;then
        命令
    elif 条件判断;then 
        命令
    else
        命令
    fi
    

    if语句对于字符串的匹配

    case 条件判断语句

    case条件语句相当于多分支的if/elif/ellse条件语句,但是它比这些条件语句看起来更规范更工整,常被应用于实现系统服务启动脚本等企业应用场景中。

    case  变量  in
            one)
                命令
    ;;
            two)
                 命令
    ;;
             *) 
                 命令
    esac

    for循环语句

    for  条件
    do
       命令
    done
    
    ##或
    
    for  条件;do
       命令
    done

     

    while循环语句

    while  条件
    do
       命令
    done

    Break、Continue、exit 循环控制语句

    break 、continue在条件语句及循环语句(for、while、if等)中用于控制程序走向;而exit则用于终止所有语句并退出当前脚本。

    命令说明
    break   n如果省略 n ,则表示跳出整个循环,n 表示跳出循环的成熟
    continue n如果省略 n ,则表示跳过本次循环,忽略本次循环的剩余代码,进行循环的下一次循环。n表示退到第 n 层继续循环
    exit n退出当前 shell 程序,n 为上一次程序执行的状态返回值。n 也可以省略,在下一个 shell 里可通过  $?  接收 exit  n 的n值

    Shell脚本执行scrapy爬虫和python脚本

    #! /bin/bash
    cd /opt/project/
    scrapy crawl xx
    python3 test.py

     

    展开全文
  • 写的这个伪shell能够运行不带参数的命令,除了cd不支持其他的shell内建命令,不过cd命令不能支持进入主目录,根目录等,支持IO重定向和管道,输入exit退出程序。
  • Shell解释器

    2020-12-10 09:48:14
    shell解释器,用户和操作系统内核之间的桥梁 一、Shell常见种类 就像不同地区有不同方言一样,不同的Linux/Unix系统使用着不同类型的shell Bsh:由贝尔实验室编写。Bsh是产生较早的UNIX Shell程序,实现了最基本的...

    shell解释器,用户和操作系统内核之间的桥梁

    一、Shell常见种类

    就像不同地区有不同方言一样,不同的Linux/Unix系统使用着不同类型的shell

    • Bsh:由贝尔实验室编写。Bsh是产生较早的UNIX Shell程序,实现了最基本的命令解释器的功能,同时也可以作为脚本编程语言
    • Csh:是因使用C语言的语法风格而得名,在用户的命令行交互界面上进行了很多改进,并增加了历史,别名,文件名替换,作业掏等功能,相比Bsh,Csh在更加适用为  用户提供命令交互操作
    • Ksh:在Bsh和Csh之后出现的,结合了两都的功能优势,兼具Bsh的语法和Csh的交互特性.
    • Bash:从名称可以看出是Bsh的升级版本,是著名的开源软件项目,目前大多数的Linux版本(包括Red Hat公司的Linux系统)都使用Bash 作为默认的Shell程序当运行,Shell程序时,实际运行的是Bash程序
    • Zsh:更多地基于交互式操作考虑进行设计的Shell程序,集成了Bash,Ksh等多种Shell程序的优点

    二、Bash

    2.1、Linux默认使用的Shell程序

    命令文件位置:/bin/bash文件中

    [root@Carlota /]# ls /bin/bash 
    /bin/bash
    [root@Carlota /]# 
    [root@Carlota /]# ll /bin/bash 
    -rwxr-xr-x. 1 root root 964600 8月   8 2019 /bin/bash
    [root@Carlota /]# 
    

    3.2、主要功能

    • 命令历史: 是BaSh中用于提高命令输入效率的一项功能,能够让用户快速地重复执行已经输入过的命令,减少重复输入工作,向上的方向键等,history可以查看已经输入过哪些命令

    • **命令别名:**可以将频繁使用的复杂命令定义为简短的别名,当用记需要执行该复杂命令时,只需要使用别名即可完成对应的操作,降低和操作复杂性,提高了输入效率

    • **标准输入输出和重定向:**Linux使用文件来描述系统的硬件,设备等资源。

    • **管道操作:**在Bash环境中,为不同命令之间的协同工作提供了一种机制,为于管道符号左侧的命令输出结果,将作为右侧命令的输入,同一行命令中可以有多个管道

    三、相关Shell命令

    • 查看当前Shell解释器
    $ echo $SHELL
    /bin/bash
    
    • 查看系统支持Shell解释器
    $ cat /etc/shells 
    /bin/bash
    /bin/csh
    /bin/ksh
    /bin/sh
    /bin/tcsh
    /bin/zsh
    
    • 切换系统当前默认的Shell解释器
    $ chsh
    

    四、关于首行#!的理解

    脚本文件首行#!(sha-bang)一方面标志着脚本文件的magic number为脚本文件类型,另一方面告知系统此脚本文件需要使用何种命令解释器来执行

    #!/bin/sh            --使用sh来解释执行
    #!/bin/bash          --使用bash来解释执行
    #!/bin/python        --使用python来解释执行
    #!/bin/perl          --使用perl来解释执行
    #!/bin/env bash      --使用env来适配某些操作系统中bash并不安装在/bin目录下的情况
    
    展开全文
  • 编写shell解释器之前,先来分析几个知识点: (1)shell执行命令时步骤:(如下图) (2)shell执行脚本时的步骤:(如下图)  在这里,有一个知识点必须知道,那就是shell命令下的内建命令,(内建命令在...
  • 希望了解shell实现的朋友可以看看
  • 编写自己的Shell解释器摘要:本期的目的是向大家介绍shell的概念和基本原理,并且在此基础上动手做一个简单shell解释器。同时,还将就用到的一些 linux环境编程的知识做一定讲解。本文适合的读者对象 对linux环境上...
  • 该软件是用符合POSIX shellshell脚本编写的Pure LISP解释器,其灵感来自于, , , , 和 本软件的目的 易于在基础LISP语言处理的教育和研究中使用 通过在符合POSIX的外壳上运行在所有计算机环境中使用 如何使用 ...
  • Linux下实现简单的shell解释器

    千次阅读 2017-02-28 16:35:42
    实际上shell就是一个命令解释器,它解释有用户输入的命令并且把它们送到内核。不仅如此,shell有自己的编程语言用于对命令的编辑,它允许用户编写shell命令组成的程序。shell变成语言具有普通编程语言的很多特点,...
  • 编写简单的网络版shell命令解释器

    千次阅读 2009-04-30 19:00:00
    项目名称:网络版shell命令解释器主要目的:练习socket 编程实现功能:可利用windows 的telnet远程登录服务进到 linux 系统,运行自己编写shell 命令解析器,进行一些简单的内部命令和外部命令的操作,并把...
  • bash 是一个为GNU计划编写的Unix shell。它的名字是一系列缩写:Bourne-Again Shell — 这是关于Bourne shell(sh)的一个双关语(Bourne again / born again)。 bash是大多数Linux系统以及Mac OS X默认的shell,它...
  • shell命令解释器

    2013-06-01 23:21:48
    shell命令解释器。重定向,管道等等。
  • 编写Shell脚本

    2018-04-15 09:48:00
    1、脚本的编写  Shell脚本本身是一个文本文件,这里编写一个简单的程序,在屏幕上显示一行helloworld! 脚本内容如下: ... 编写Shell程序首先要了解Shell脚本的基本构成。下面为一个复杂一些的...
  • 如何编写Shell脚本

    千次阅读 2019-06-06 17:19:00
    什么是shell脚本 Shell 脚本(shell script),是一种为 shell 编写的脚本程序。...这个解释器就是Shell,它是一个用 C 语言编写的程序。常见的Shell有Bourne Shell(/usr/bin/sh或/bin/sh)和Bourne Again Shell...
  • Linux操作系统中shell是用户与系统内核沟通的中介,它为用户使用操作系统的服务提供了一个命令行界面,用户在shell提示符下输入的每个命令都由shell解释,然后传给内核执行。本实验要求用C语言编写一个简单的shell...
  • 文章目录shell解释器的送一shell的分类shcshtcshashbashbash 的优点 shell解释器的送一 Unix/Linux上常见的Shell脚本解释器有bash、sh、csh、ksh等,习惯上把它们称作一种Shell。我们常说有多少种Shell,其实说的是...
  • PHP编写shell

    万次阅读 2014-06-09 17:03:32
    先说php php的目录底下有解释器 早就注意到了 那么他就可以像py那样来用了 首先添加环境变量 名称:path 值就是php目录 例如我的就是C:\php-5.5.10 然后在cmd里输入"php -v" 会显示版本 据说明设置成功了 ...
  • shell命令解释器代码框架(原版),C语言编写,适合初学者
  • Linux外壳 Linux 操作系统的命令行解释器,用 C 编写。 代码组织: 1)您从 Linux 终端调用 shell。 根据您决定将其设为批处理模式还是交互模式,它将相应地运行
  • 在minix系统下,用C编写的代码,实现Minix系统shell命令解释器功能。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 56,146
精华内容 22,458
关键字:

编写shell解释器