第零四章 • 交互

读取-求值-输出

在编写我们的 Lisp 之前,我们需要寻找一种和它交互的方式。最简单的方法,我们可以修改代码,重新编译,然后再次运行。这个方案虽然理论上可行,但是太为繁琐。如果可以动态地和程序进行交互,我们就可以快速地测试程序在各种条件下的行为。我们称这种模式为交互提示。

这种模式下的程序读取用户的输入,在程序内部进行处理,然后返回一些信息给用户。这种系统也被叫做 REPL,是 read-evaluate-print loop (读取-求值-输出循环) 的简写。这种技术被广泛地应用在各种编程语言的解释器中,如果你学过 Python,那你一定不会陌生。

在编写一个完整的 REPL 之前,我们先实现一个简单的程序:读取用户的输入,简单处理后返回给用户。在后面的章节中,我们会对这个程序不断扩展,最后能够正确地读取并解析一个真正的 Lisp 程序,并将结果返回给用户。

交互提示

为了实现这个简单的想法,可以使用一个循环不断打印信息并等待用户输入。为了获取用户输入的内容,我们可以使用 stdio.h 中的 fgets 函数。这个函数可以一直读取直到遇到换行符为止。我们需要找个地方存储用户的输入。为此可以声明一个固定大小的数组缓冲区。

一旦获取到用户输入的字符串,就可以使用 printf 将它打印到命令行中。

#include <stdio.h>

/* Declare a buffer for user input of size 2048 */
static char input[2048];

int main(int argc, char** argv) {

  /* Print Version and Exit Information */
  puts("Lispy Version 0.0.0.0.1");
  puts("Press Ctrl+c to Exit\n");

  /* In a never ending loop */
  while (1) {

    /* Output our prompt */
    fputs("lispy> ", stdout);

    /* Read a line of user input of maximum size 2048 */
    fgets(input, 2048, stdin);

    /* Echo input back to user */
    printf("No you're a %s", input);
  }

  return 0;
}

代码中的 /*...*/ 是什么?

这是 C 语言中的注释,是为了向其它阅读代码的人解释代码作用的。在编译的时候,会被编译器忽略掉。

现在来深入解读一下这个程序。

static char input[2048]; 这行代码声明了一个拥有 2048 个字符长度的全局数组。这个数组中存储的数据可以在程序的任何地方获取到。我们会把用户在命令中输入的语句保存到这里面来。static 关键字标明这个数组仅在本文件中可见。[2048] 表明了数组的大小。

我们使用 while(1) 来构造一个无限循环,条件语句 1 永远都为真,所以这个循环会一直执行下去。

我们使用 fputs 打印提示信息。这个函数和前面介绍过的 puts 函数区别是 fputs 不会在末尾自动加换行符。我们使用 fgets 函数来获取用户在命令行中输入的字符串。这两个函数都需要指定写入或读取的文件。在这里,我们使用 stdinstdout 作为输入和输出。这两个变量都是在 <stdio.h> 中定义的,用来表示向命令行进行输入和输出。当我们把 stdin 传给 fgets 后,它就会等待用户输入一串字符,并按下回车键。如果得到了字串,就会把字串连同换行符存放到 input 数组中。为了不让获取到的数据太大数组装不下,我们还要指定一下可以获取的最大长度为 2048

我们使用 printf 函数将处理后的信息返回给用户。printf 允许我们同时打印多个不同类型的值。它会自动对第一个字符串参数中的模式进行匹配。例如,在上面的例子中,可以在第一个参数中看到 %s 字样。printf 将自动把 %s 替换为后面的参数中的值。s 代表字符串(string)。

更多关于 printf 的模式种类及其用法,可以参考文档

我怎么才能知道一些类似于 fgetsprintf 的函数的用法?

很明显你不可能一开始就知道这些标准库函数的作用和用法,这些都需要经验。幸运的是,C 语言的标准库非常精炼。绝大多数的库函数都可以在平时的练习中了解并学会使用。如果你想要解决的是底层的、基本的问题,关注一下参考文档会大有裨益,因为很可能标准库中的某个函数所做的事情正是你想要的!

编译

你可以使用在第二章介绍过的命令来编译上面的程序:

cc -std=c99 -Wall prompt.c -o prompt

编译通过之后,你应该试着运行并测试一下这个程序。测试完成后,可以使用 Ctrl+c 快捷键来退出程序。如果一切正常,你会得到类似于下面的结果:

Lispy Version 0.0.0.0.1
Press Ctrl+c to Exit

lispy> hello
No You're a hello
lispy> my name is Dan
No You're a my name is Dan
lispy> Stop being so rude!
No You're a Stop being so rude!
lispy>

编辑输入

如果你用的是 Mac 或 Linux,当你用左右箭头键编辑在程序中的输入时,你会遇到一个奇怪的问题:

Lispy Version 0.0.0.0.3
Press Ctrl+c to Exit

lispy> hel^[[D^[[C

使用箭头键不会前后移动输入的光标,而是会产生像 ^[[D^[[C 这种奇怪的字符。很明显这不是我们想要的结果。

而在 Windows 上则不会有这个现象。

在 Mac 和 Linux 上,我们需要用到 editline 库来解决这个问题。并把 fputsfgets 替换为这个库提供的相同功能的函数。

如果你用的是 Windows 系统,则可以直接跳到本章的最后。因为接下来的几个小节都是和安装与配置 editline 相关的内容。

使用 editline 库

我们会用到 editline 库提供的两个函数:readlineadd_historyreadlinefgets 一样,从命令行读取一行输入,并且允许用户使用左右箭头进行编辑。add_history 可以纪录下我们之前输入过的命令,并使用上下箭头来获取。新的程序如下所示:

#include <stdio.h>
#include <stdlib.h>

#include <editline/readline.h>
#include <editline/history.h>

int main(int argc, char** argv) {

  /* Print Version and Exit Information */
  puts("Lispy Version 0.0.0.0.1");
  puts("Press Ctrl+c to Exit\n");

  /* In a never ending loop */
  while (1) {

    /* Output our prompt and get input */
    char* input = readline("lispy> ");

    /* Add input to history */
    add_history(input);

    /* Echo input back to user */    
    printf("No you're a %s\n", input);

    /* Free retrieved input */
    free(input);

  }

  return 0;
}

我们增加了一些新的头文件。<stdlib.h> 提供了 free 函数。<editline/readline.h><editline/history.h> 提供了 editline 库中的 readlineadd_history 函数。

在上面的程序中,我们使用 readline 读取用户输入,使用 add_history 将该输入添加到历史纪录当中,最后使用 printf 将其加工并打印出来。

fgets 不同的是,readline 并不在结尾添加换行符。所以我们在 printf 函数中添加了一个换行符。另外,我们还需要使用 free 函数手动释放 readline 函数返回给我们的缓冲区 input。这是因为 readline 不同于 fgets 函数,后者使用已经存在的空间,而前者会申请一块新的内存,所以需要手动释放。内存的申请与释放问题我们会在后面的章节中深入讨论。

链接 editline 并编译

如果你使用前面我们提供的命令行来编译这个程序,你会得到类似于下面的错误,因为在使用之前,你必须先在电脑上安装 editline 库。

fatal error: editline/readline.h: No such file or directory #include <editline/readline.h>

在 Mac 上,editline 包含在 Command Line Tools 中,安装方法我们在第二章有说明。安装完后,可能还是会出现头文件不存在的编译错误。这时,可以移除 #include <editline/history.h> 这行代码,再试一次。

在 Linux 上,可以使用 sudo apt-get install libedit-dev 来安装 editline。在 Fedora 上,使用 su -c "yum install libedit-dev*" 命令安装。

一旦你安装好了 editline,你可以再次编译试一下。然后将会得到如下的错误:

undefined reference to `readline'
undefined reference to `add_history'

这是因为没有将 editline 链接到程序中。我们需要使用 -ledit 标记来完成链接,用法如下:

cc -std=c99 -Wall prompt.c -ledit -o prompt

再次运行程序,就可以自由地编辑输入的文字了!

为什么我的程序还是不能编译?

在有些系统上,editline 的安装、包含、链接的方式可能会有些许差别,请善用搜索引擎哦。

预处理器

对于这样的一个小程序而言,我们针对不同的系统编写不同的代码是可以的。但是如果我把我的代码发给一个使用不同的操作系统的朋友,让他帮我完善一下代码,可能就会出问题了。理想情况下,我希望我的代码可以在任何操作系统上编译并运行。这在 C 语言中是个很普遍的问题,叫做可移植性(portability)。这通常都是个很棘手的问题。

但是 C 提供了一个机制来帮助我们,叫做预处理器(preprocessor)。

预处理器也是一个程序,它在编译之前运行。它有很多作用。而我们之前就已经悄悄地用过预处理器了。任何以井号 # 开头的语句都是一个预处理命令。为了使用标准库中的函数,我们已经用它来包含(include)过头文件了。

预处理的另一个作用是检测当前的代码在哪个操作系统中运行,从而来产生平台相关的代码。而这也正是我们做可移植性工作时所需要的。

在 Windows 上,我们可以伪造一个 readlineadd_history 函数,而在其他系统上就使用 editline 库提供给我们的真正有作用的函数。

为了达到这个目的,我们需要把平台相关的代码包在#ifdef#else#endif 预处理命令中。如果条件为真,包裹在 #ifdef#else 之间的代码就会被执行,否则,#elseendif 之间的代码被执行。通过这个特性,我们就能写出在 Windows、Linux 和 Mac 三大平台上都能正确编译的代码了:

#include <stdio.h>
#include <stdlib.h>

/* If we are compiling on Windows compile these functions */
#ifdef _WIN32
#include <string.h>

static char buffer[2048];

/* Fake readline function */
char* readline(char* prompt) {
  fputs(prompt, stdout);
  fgets(buffer, 2048, stdin);
  char* cpy = malloc(strlen(buffer)+1);
  strcpy(cpy, buffer);
  cpy[strlen(cpy)-1] = '\0';
  return cpy;
}

/* Fake add_history function */
void add_history(char* unused) {}

/* Otherwise include the editline headers */
#else
#include <editline/readline.h>
#include <editline/history.h>
#endif

int main(int argc, char** argv) {

  puts("Lispy Version 0.0.0.0.1");
  puts("Press Ctrl+c to Exit\n");

  while (1) {

    /* Now in either case readline will be correctly defined */
    char* input = readline("lispy> ");
    add_history(input);

    printf("No you're a %s\n", input);
    free(input);

  }

  return 0;
}

参考

prompt_unix.c

#include <stdio.h>
#include <stdlib.h>

#include <editline/readline.h>
#include <editline/history.h>

int main(int argc, char** argv) {

  /* Print Version and Exit Information */
  puts("Lispy Version 0.0.0.0.1");
  puts("Press Ctrl+c to Exit\n");

  /* In a never ending loop */
  while (1) {

    /* Output our prompt and get input */
    char* input = readline("lispy> ");

    /* Add input to history */
    add_history(input);

    /* Echo input back to user */    
    printf("No you're a %s\n", input);

    /* Free retrived input */
    free(input);

  }

  return 0;
}

prompt_windows.c

#include <stdio.h>

/* Declare a buffer for user input of size 2048 */
static char input[2048];

int main(int argc, char** argv) {

  /* Print Version and Exit Information */
  puts("Lispy Version 0.0.0.0.1");
  puts("Press Ctrl+c to Exit\n");

  /* In a never ending loop */
  while (1) {

    /* Output our prompt */
    fputs("lispy> ", stdout);

    /* Read a line of user input of maximum size 2048 */
    fgets(input, 2048, stdin);

    /* Echo input back to user */
    printf("No you're a %s", input);
  }

  return 0;
}

prompt.c

#include <stdio.h>
#include <stdlib.h>

/* If we are compiling on Windows compile these functions */
#ifdef _WIN32
#include <string.h>

static char buffer[2048];

/* Fake readline function */
char* readline(char* prompt) {
  fputs(prompt, stdout);
  fgets(buffer, 2048, stdin);
  char* cpy = malloc(strlen(buffer)+1);
  strcpy(cpy, buffer);
  cpy[strlen(cpy)-1] = '\0';
  return cpy;
}

/* Fake add_history function */
void add_history(char* unused) {}

/* Otherwise include the editline headers */
#else
#include <editline/readline.h>
#include <editline/history.h>
#endif

int main(int argc, char** argv) {

  puts("Lispy Version 0.0.0.0.1");
  puts("Press Ctrl+c to Exit\n");

  while (1) {

    /* Now in either case readline will be correctly defined */
    char* input = readline("lispy> ");
    add_history(input);

    printf("No you're a %s\n", input);
    free(input);

  }

  return 0;
}

彩蛋

  • 将提示信息 lispy> 换成其他你喜欢的。
  • 修改打印的信息。
  • 在程序开头的提示信息中添加一些其他的信息。
  • 在字符串中,\n 表示什么?
  • printf 还有哪些输出模式?
  • 如果你向 printf 传递一个与模式不匹配的值会怎样?
  • 预处理器 #ifndef 有什么用?
  • 预处理器 #define 有什么用?
  • _WIN32 在 Windows 中有定义,那在 Linux 和 Mac 中定义了什么呢?

results matching ""

    No results matching ""