IT运维

如何构建一个Linux Shell(二)

2020-07-24 17:14:17 | 来源:中培企业IT培训网

在如何构建一个Linux Shell(一)中,我们构建了一个简单的Linux shell,该shell打印提示字符串,读取输入,然后将输入回显到屏幕上。现在这不是很令人印象深刻,不是吗?在如何构建一个Linux Shell(二)中,我们将更新代码,以使Shell能够解析和执行简单命令。首先让我们看一下什么是简单的命令。

  什么是简单命令?

一个简单的命令 由单词列表组成,这些单词列表由空格字符(空格,制表符,换行符)分隔。第一个单词是命令名,并且是必需的(否则,shell将没有解析和执行命令!)。第二个和后续单词是可选的。如果存在,它们形成的论点,我们希望shell传递到执行的命令。

例如,以下命令: ls -l 由两个词组成: ls (命令名称),以及 -l(第一个也是唯一的参数)。同样,命令:gcc -o shell main.c prompt.c(在第一部分中,我们用它来编译我们的shell)由5个词组成:一个命令名和一个4个参数的列表。

为了能够执行简单的命令,我们的外壳程序需要执行以下步骤:

扫描输入,一次输入一个字符,以查找下一个标记。我们称此过程为词法扫描,而执行此任务的外壳部分称为词法扫描器,或简称为扫描器。

提取输入令牌。我们称这种标记化输入。

解析标记并创建抽象语法树或AST。Shell负责执行此操作的部分称为解析器。

执行AST。这是执行者的工作。

下图显示了Shell为了解析和执行命令而执行的步骤。您可以看到图中包含的步骤比上面列表中显示的步骤更多,这很好。随着外壳的增长和变得越来越复杂,我们将在需要时添加其他步骤。

现在,让我们详细查看上述四个步骤,并查看在shell中实现这些功能所需的代码。

  扫描输入

为了获得下一个令牌,我们需要能够一次扫描一个字符的输入,以便我们可以识别可以作为令牌一部分的字符和作为定界符的字符。甲分隔符是一个标记的令牌(以及可能的另一令牌的开始)的端部。通常,分隔符是空格字符(空格,制表符,换行符),但也可以包含其他字符,例如; 和 &。

通常,扫描输入意味着我们应该能够:

.从输入中检索下一个字符。

.返回我们读回的最后一个字符作为输入。

.前瞻(或窥视)以检查下一个字符,而无需实际检索它。

.跳过空白字符。

我们将在一分钟内定义执行所有这些任务的功能。但是首先,让我们谈谈抽象输入。

记住 read_cmd()函数,这是我们在本教程的第一部分中定义的?那就是我们用来读取用户输入并将其作为malloc的字符串。我们可以将此字符串直接传递给我们的扫描仪,但这会使扫描过程有点麻烦。特别是,扫描器很难记住它给我们的最后一个字符,以便它可以越过该字符并给我们后面的字符。

为了简化扫描仪的工作,我们通过将输入字符串作为 struct source_s 结构,我们将在源文件中定义 source.h。继续在源目录中创建该文件,然后在您喜欢的文本编辑器中将其打开并添加以下代码:

#ifndef SOURCE_H

#define SOURCE_H

#define EOF (-1)

#define ERRCHAR ( 0)

#define INIT_SRC_POS (-2)

struct source_s

{

char *buffer; /* the input text */

long bufsize; /* size of the input text */

long curpos; /* absolute char position in source */

};

char next_char(struct source_s *src);

void unget_char(struct source_s *src);

char peek_char(struct source_s *src);

void skip_white_spaces(struct source_s *src);

#endif

关注结构的定义,您可以看到 struct source_s 除了两个以外,还包含指向输入字符串的指针 long 包含有关字符串长度和我们当前在字符串中的位置(将从中获取下一个字符)的信息的字段。

现在创建另一个名为 source.c,您应在其中添加以下代码:

#include

#include "shell.h"

#include "source.h"

void unget_char(struct source_s *src)

{

if(src->curpos < 0)

{

return;

}

src->curpos--;

}

char next_char(struct source_s *src)

{

if(!src || !src->buffer)

{

errno = ENODATA;

return ERRCHAR;

}

char c1 = 0;

if(src->curpos == INIT_SRC_POS)

{

src->curpos = -1;

}

else

{

c1 = src->buffer[src->curpos];

}

if(++src->curpos >= src->bufsize)

{

src->curpos = src->bufsize;

return EOF;

}

return src->buffer[src->curpos];

}

char peek_char(struct source_s *src)

{

if(!src || !src->buffer)

{

errno = ENODATA;

return ERRCHAR;

}

long pos = src->curpos;

if(pos == INIT_SRC_POS)

{

pos++;

}

pos++;

if(pos >= src->bufsize)

{

return EOF;

}

return src->buffer[pos];

}

void skip_white_spaces(struct source_s *src)

{

char c;

if(!src || !src->buffer)

{

return;

}

while(((c = peek_char(src)) != EOF) && (c == ' ' || c == ' '))

{

next_char(src);

}

}

的 unget_char()函数将(我们从输入中检索到的)最后一个字符返回(或取消保护)到输入源。它只是通过操纵源结构的指针来做到这一点。在本系列后面的部分中,您将看到此功能的好处。

的 next_char() 函数返回输入的下一个字符并更新源指针,以便下一次调用 next_char()返回以下输入字符。当我们到达输入中的最后一个字符时,该函数将返回特殊字符EOF,我们在其中将其定义为-1 source.h 以上。

的 peek_char() 功能类似于 next_char()它返回输入的下一个字符。唯一的区别是peek_char() 不会更新源指针,因此下一次调用 next_char()返回我们刚刚偷看的相同输入字符。在本系列的后面部分,您将看到输入偷看的好处。

最后, skip_white_spaces()函数将跳过所有空格字符。这将在完成读取令牌后为我们提供帮助,并且在读取下一个令牌之前希望跳过定界符空白。

  标记输入

现在我们已经有了扫描仪的功能,我们将使用这些功能来提取输入令牌。我们将首先定义一个新结构,该结构将用于表示令牌。

继续创建一个名为 scanner.h 在您的源目录中,然后将其打开并添加以下代码:

#ifndef SCANNER_H

#define SCANNER_H

struct token_s

{

struct source_s *src; /* source of input */

int text_len; /* length of token text */

char *text; /* token text */

};

/* the special EOF token, which indicates the end of input */

extern struct token_s eof_token;

struct token_s *tokenize(struct source_s *src);

void free_token(struct token_s *tok);

#endif

专注于结构定义, struct token_s 包含一个指向 struct source_s保留了我们的投入。该结构还包含一个指向令牌文本的指针,以及一个告诉我们该文本长度的字段(这样我们就无需重复调用strlen() 在令牌的文本上)。

接下来,我们将编写 tokenize()函数,它将从输入中检索下一个标记。我们还将编写一些帮助程序功能,以帮助我们使用输入令牌。

在源目录中,创建一个名为 scanner.c,然后输入以下代码:

#include

#include

#include

#include

#include "shell.h"

#include "scanner.h"

#include "source.h"

char *tok_buf = NULL;

int tok_bufsize = 0;

int tok_bufindex = -1;

/* special token to indicate end of input */

struct token_s eof_token =

{

.text_len = 0,

};

void add_to_buf(char c)

{

tok_buf[tok_bufindex++] = c;

if(tok_bufindex >= tok_bufsize)

{

char *tmp = realloc(tok_buf, tok_bufsize*2);

if(!tmp)

{

errno = ENOMEM;

return;

}

tok_buf = tmp;

tok_bufsize *= 2;

}

}

struct token_s *create_token(char *str)

{

struct token_s *tok = malloc(sizeof(struct token_s));

if(!tok)

{

return NULL;

}

memset(tok, 0, sizeof(struct token_s));

tok->text_len = strlen(str);

char *nstr = malloc(tok->text_len+1);

if(!nstr)

{

free(tok);

return NULL;

}

strcpy(nstr, str);

tok->text = nstr;

return tok;

}

void free_token(struct token_s *tok)

{

if(tok->text)

{

free(tok->text);

}

free(tok);

}

struct token_s *tokenize(struct source_s *src)

{

int endloop = 0;

if(!src || !src->buffer || !src->bufsize)

{

errno = ENODATA;

return &eof_token;

}

if(!tok_buf)

{

tok_bufsize = 1024;

tok_buf = malloc(tok_bufsize);

if(!tok_buf)

{

errno = ENOMEM;

return &eof_token;

}

}

tok_bufindex = 0;

tok_buf[0] = '';

char nc = next_char(src);

if(nc == ERRCHAR || nc == EOF)

{

return &eof_token;

}

do

{

switch(nc)

{

case ' ':

case ' ':

if(tok_bufindex > 0)

{

endloop = 1;

}

break;

case ' ':

if(tok_bufindex > 0)

{

unget_char(src);

}

else

{

add_to_buf(nc);

}

endloop = 1;

break;

default:

add_to_buf(nc);

break;

}

if(endloop)

{

break;

}

} while((nc = next_char(src)) != EOF);

if(tok_bufindex == 0)

{

return &eof_token;

}

if(tok_bufindex >= tok_bufsize)

{

tok_bufindex--;

}

tok_buf[tok_bufindex] = '';

struct token_s *tok = create_token(tok_buf);

if(!tok)

{

fprintf(stderr, "error: failed to alloc buffer: %s ", strerror(errno));

return &eof_token;

}

tok->src = src;

return tok;

}

我们在此文件中定义的全局变量用于以下目的:

.tok_buf 是指向我们将在其中存储当前令牌的缓冲区的指针。

.tok_bufsize 是我们分配给缓冲区的字节数。

.tok_bufindex 是当前缓冲区的索引(即它告诉我们缓冲区中下一个输入字符的添加位置)。

.eof_token是一个特殊的令牌,我们将用它来表示文件/输入的结尾(EOF)。

现在让我们看一下在此文件中定义的功能。

的 add_to_buf()函数将单个字符添加到令牌缓冲区。如果缓冲区已满,则函数将对其进行扩展。

的 create_token() 函数接受一个字符串并将其转换为 struct token_s结构体。它负责为令牌的结构和文本分配内存,并填充结构的成员字段。

的 free_token() 函数释放令牌结构使用的内存以及用于存储令牌文本的内存。

的 tokenize()函数是我们词法扫描器的心脏和灵魂,并且函数的代码相当简单。首先,为令牌缓冲区分配内存(如果尚未完成),然后初始化令牌缓冲区和源指针。然后调用next_char()检索下一个输入字符。当我们到达输入结尾时,tokenize() 返回特殊 eof_token,表示输入结束。

然后,该函数循环读取一次输入的字符。如果遇到空白字符,它将检查令牌缓冲区以查看其是否为空。如果缓冲区不为空,则定界当前令牌并中断循环。否则,我们将跳过空格字符,并移至下一个标记的开头。

获得令牌后, tokenize() 来电 create_token(),并向其传递令牌文本(我们将其存储在缓冲区中)。令牌文本将转换为令牌结构,tokenize() 然后返回到呼叫者。

我们确实在这部分上取得了很大的进步,但是我们的shell仍未准备好解析和执行命令。因此,我们现在不会编译该shell。在实现解析器和执行器之后,我们的下一个编译将在第三部分的结尾。想了解更多关于Linux的信息,请继续关注中培伟业吧。

标签: IT运维