有一个小型游戏和回声计划并不是真的为我们提供了太多的功能。我们需要有几个元项目或可以帮助我们创建新计划的程序。什么是最小的元节目套件?对我们来说,它看起来我们可以使用汇编程序和链接器来获得。 C编译器会有所帮助。但我们可以在没有文本编辑器的情况下创建这些东西。文本编辑器将允许我们创建我们想要的任何程序。但是什么是文本编辑器?
根据维基百科的说法,文本编辑器是"一种编辑纯文本的计算机程序类型。"所以,写作可以编辑纯文本的最小程序。您可能听说过的一些文本编辑包括ED,VI,EMAC和Nano。我们还知道Visual Studio代码,Sublime Text和Atom等编辑器。其中,除了ED的所有内容都是视觉的,或面向屏幕的文本编辑器。除了是标准文本编辑器之外,还是面向线路的文本编辑器。 1我提出了一个简短的功能列表,即我认为代表可以编辑纯文本的程序的绝对裸露的最小值:
请注意,重点是线条,而不是字符。另一个重点是行动地点的任意性。文本编辑器不应该刚刚在顶部开始并在最后连续地附加行,但也应该能够在现有文本行或文件的顶部插入文本行。
另一件重要的是要注意的是,没有考虑将文件的内容打印到终端或其他输出设备。我们可以绝对可以编写一个不显示文本的文本编辑器,可以使用像CAT(1)这样的另一个程序来显示文本。 2但是,我认为这不仅是合理的,而且对于我们令人难以置信的稀疏UI更好,以便将至少打印到终端的活动方式。
我们需要的第一件事是一种吸引人的名字。我在普朗克定居,测量单位并表示你可以' t比这更小(至少,我可以' t;也许你可以)。此时我们应该知道钻取:复制_start.s,_syscall.s和crt.s并创建一个名为planck.c的新C文件。我们只需要五个Syscalls:_exit,读取,写,打开和关闭。根据打开(2)的手动页面,它需要一个变量的参数。由于我们打算潜在地创建全新文件,我们将有一个签名的int打开(const char * path,int标志,int模式),因此我们可以使用适当000644模式处理创建新文件。另外四个Syscalls我们将在手动页面中声明。我们还希望保持我们的Strlen功能。
我们还有一些进一步的识别来处理。对于一个,我们没有Malloc函数,因此我们将粘贴到静态大小的缓冲区供文本文件。 3也许我们应该在未来的博文中写一个;动态内存分配将是一个很好的功能。但另一天'秒。今天,让'挑选固定的缓冲区尺寸,仍然很小但合理。如果i' m在普朗克思考被用作代码的文本编辑器方面,我尝试用80列文本保留。它'我喜欢的风格。所以让'给自己128个字符(它' ll真的是126个字符,因为每个人都以换行符结束,并且是一个稍后会清楚的原因)。并假定我们可以拥有最多1024行的文本文件。谈到C代码时,我们可以始终拥有多个文本文件。我们不需要一个文件。我认为1024是合理的。如果没有,嗯,它' s所有开源软件我们可以简单地使它更大并重新编译。我们将调用此阵列LineCol并使其成为二维数组:Char LineCol [1024] [128];
既然我们知道如何将argc和argv传递给我们的程序并且我们这样做,我们可以拥有一个可选的参数:如果用户指定命令调用中的文件,我们将尝试打开该文件并使用它。这将让我们在文本文件中进行一些工作,保存它,并以后重新打开进一步工作。这似乎是绝对的要求。如果出于某种原因,我们可以' t打开文件,可能是因为它不存在,让' s假设,' s用户想要创建的新文件的名称。如果文件ISN' t在命令行上,我们可以要求用户在保存文件时提供文件名。我们还需要一个缓冲区来存储文件名,缓冲区来存储当前正在编辑的行,以及一些变量来存储当前行号,键入的字母和一些循环变量。设置我们变量并处理用户指定命令行上文件的情况的代码如下所示。
intmain(int argc,char * argv []){char linecol [1024] [128]; char文件[1024],行[128]; char buf [5],c; int co = 0,fd,i,j,li = 0,save_name = 0; if(argc> 2){ovputs("使用:" 2);托管(argv [0],2);托管(" [文件] \ n" 2); _EXIT(1); for(i = 0; i< 1024; i ++){for(j = 0; j< 128; j ++)linecol [i] [j] =' \ 0&#39 ;; }如果(argc == 2){for(i = 0; i< strlen(argv [1]); i ++)文件[i] = argv [1] [i];文件[i] =' \ 0&#39 ;; save_name = 1; if((fd =打开(文件,0x0000,0))== -1){ovputs("普朗克:错误:无法打开",2);托管(文件,2);托管(" \ n",2);转到开始; } co = 0; li = 0;而(读取(fd,& c,1)> 0){linecol [li] [co] = c; if(++ co> 126){dupts("木板:错误:线",2); Dputi(Li,2);托管("长于127个字符\ n",2); }如果(c ==' \ n'){if(++ li> 1023){dutps(" plank:错误:",2);呕吐物(Argv [1],2);托管("大于1024线\ n" 2); _EXIT(1); } co = 0; }}如果(关闭(fd)== -1){ovputs("普朗克:错误:无法关闭",2);托管(文件,2);托管(" \ n",2); _EXIT(1); }}
我想要一个方便函数来向终端写入弦;托管是该功能。它遵循类似的函数签名,而是使用文件描述符而不是文件流。
即使使用稀疏UI,我们也应该向用户提供有关启动的一些信息。 ED为您打开的文件中的字符数。我不确定为我们有多有意义。我认为一个更好的信息是文件中包含的行数。所以,如果我们使用新文件,请打印' s或0如果我们使用新文件。
我们还需要一种将数字打印到控制台的方法。不幸的是,它并不像印刷字符串那么简单。但它仍然不是太难。您可以从SnakeQR游戏中记住此代码。
静态voiddputi(int n,int fd){char num [5]; INT I = 0; do {num [i ++] = n%10 +' 0&#39 ;; }而((n / = 10)> 0); for(i--; i> = 0; i--)写(fd,& num [i],1);}
现在我们准备写主循环。我们的主循环将是这样的。
根据我们提前提出的功能列表,我们决定绝对必须实施,我们有以下命令。
get_command:托管("线:" 1); if((li = dgeti(buf,sizeof(buf),0))> 1024){dgts("?\ n" 1); goto get_command; Qupts("命令:",1); (void)dgets(Buf,sizeof(buf),0);
我们要打印字符串和数字是不够的,我们还需要能够阅读它们。所以我们需要两个职能来实现这一目标。
让' s从dgets开头,这将让我们在字符串中读取。我们将从文件描述符中读取大小-1字节我们指定到我们指定的缓冲区。这样,如果我们使用静态缓冲区和尺寸(缓冲区)的大小,我们将始终安全,永远不会超越我们的缓冲区。即使是更好,我们将始终能够返回有效字符串,因为我们将在读取循环的末尾Nul-Terminate,并保证NUL字节在缓冲区内。这是不出所料的,OpenBSD函数strlcat(3)和strlcpy(3)做什么。我已经完成了尽我所能,以确保这是一个安全的功能,尽管我知道有人会出现并使用它不用利。那是关于安全的有趣的事情。
静态intdgets(char * s,int size,int fd){int i; for(i = 0; i< size - 1; i ++){if(读取(fd,& s [i],1)< 1)返回0; if(s [i] ==' \ n')休息; } s [i] =' \ 0&#39 ;;返回strlen;}
获取数字也需要仔细考虑; DGETI是生成的函数。我们在数字中读入缓冲区,确保它们是数字,然后执行一些基本算术并返回我们算术的结果。如果我们没有输入所有数字,我们将返回故意大于1024的数字,这将发出指定无效行号的主循环。
静态Intdgeti(char * s,int size,int fd){int i,n = 0; for(i = 0; i< size - 1; i ++){if(读取(fd,& s [i],1)< 1)返回0; if(s [i] ==' \ n')休息; if(s [i]' 0' || s [i]>' 9')n = 1; } s [i] =' \ 0&#39 ;; if(s [0] ==' \ 0')返回0; if(n == 1)返回1025; n = 0; for(i = 0; i< strlen; i ++)n =(n * 10)+(s [i] - ' 0');返回n;}
既然我们拥有我们要工作的行号且我们要执行的命令,我们的其余文本编辑器都只是一个大型交换机语句,其中包含每个命令的逻辑。
切换(Buf [0]){案例' d':for(i = 0; i< 128; i ++)inececol [li - 1] [i] =' \ 0' ; for(i = li; i< 1024; i ++){for(j = 0; j< 128; j ++)inececol [i-1] [j] = linecol [i] [j]; for(i = 0; i< 128; i ++)linecol [1023] [i] =' \ 0&#39 ;;休息;案例' i&#39 ;: if(li == 0){ovputs("?\ n" 1);休息; } --li; if(inececol [li] [0]!=' \ 0'&& linecol [li] [0]!=' \ n'){dinecol [li] ,1);托管(" \ n" 1); }如果(dgets(行,sizeof(行) - 1,0)== 0)中断; for(i = 0; i< 128; i ++)inececol [li] [i] =' \ 0&#39 ;; for(i = 0; i< strlen(线); i ++)inececol [li] [i] =行[i]; Linecol [Li] [i] =' \ n&#39 ;; for(i = 0; inececol [i] [0]!=' \ 0&#39 ;; i ++); if(i< li){for(j = 0; j< 128; j ++)inececol [i] [j] = linecol [li] [j]; for(j = 0; j< 128; j ++)inececol [li] [j] =' \ 0&#39 ;; } 休息;案例' n&#39 ;: if(inececol [1023] [0]!=' \ 0'){duts("普朗克:错误:无法添加行,已经在限制\ N&#34 ;,2);休息; for(i = 1022; i> li - 1; i--){if(linecol [i] [0]!=' \ 0'){for(j = 0; j< 128; j ++)inececol [i + 1] [j] = linecol [i] [j]; for(j = 0; j< 128; j ++)inececol [i] [j] =' \ 0&#39 ;; }} inececol [li] [0] =' \ n&#39 ;;休息;案例' p'如果(li == 0){for(i = 0; linecol [i] [0]!=' \ 0' i ++)托管(Linecol [i ],1); }否则if(inececol [li - 1] [0] ==' \ 0'){ovputs("?\ n" 1); } else {dputs(linecol [li - 1],1); } 休息;案例' q&#39 ;: goto完成了;案例' s&#39 ;: if(save_name == 0){dutps("文件:" 1); Dgets(文件,sizeof(文件),0); save_name = 1; }如果((fd =打开(文件,0x0001 | 0x0200,000644))== -1){托管("普朗克:错误:无法打开",2);托管(文件,2);托管(" \ n",2);休息; for(li = 0; li< 1024; li ++){if(linecol [li] [0] ==' \ 0')休息;托管(Linecol [Li],FD); }如果(关闭(fd)== -1){dutps("普朗克:错误:无法关闭",2);托管(文件,2);托管(" \ n",2); _EXIT(1); } 休息;默认值:托管("?\ n" 1); } goto get_command;完成:返回0;}
当删除一条线时,我们将删除的行下方的所有行换成一个。同样,当添加一条线时,我们将下面的所有线推下来(用检查,确保我们不超过我们的阵列限制)。
当我们插入文本时,我们打印要编辑的行。无论用户类型如何完全更换该行。所以要小心!那个'我们为什么要打印线路,所以用户可以制作小调整,如修复错字,并拥有所需的所有信息。
如果用户在插入模式下没有文本,则不会替换该行。这具有不能通过插入模式制作空线的副作用。这就是我们需要换行符的原因。我认为这是一个好的权衡,因为这种方式要求错误的行,然后去插入模式并不是你必须重新输入整行。
我们在Insert模式下的大多数尺寸(BUF)-1字符内读到。这是如此,这些线路总是可以以换行符和nul字符结束。
创建新的非空行文本需要两个命令:首先,n-1行上的n命令将新行插入行n;其次,我在线上的命令。第0行可用于在文件顶部插入新行。如果您想要一个空行,单独的n命令就足够了。
保存文件与我们文件的文件描述符的每个行都是简单的,我们达到从nul字符开始的第一行,这意味着我们已经用完了文本。
Warning: Can only detect less than 5000 characters
2我用我们在这篇文章中创建的文本编辑器写了那种猫版本。 这将是未来博客文章的主题。 3是的,我们可以使用MMAP(2)和Munmap(2),但我们应该专注于每个博客帖子的一两件事。 我们可以随时编写更多博客文章。