跳至正文
LNN的博客!

“文言”编程语言能读取标准输入了

前不久我给“文言”写了个扩展库,让它能够通过 Node.js 读取标准输入。现已被收入“文言”的包管理系统“文淵閣”

研究过这门深奥编程语言的朋友可能知道,“文言”中没有原生的办法来读取标准输入。我猜测这大概也是洛谷网不再支持这门语言的原因之一。

“文言”被洛谷网移除前,不乏有人用这门语言来解算法题,这就不得不用到嵌入 JavaScript 代码的 hack。由于“文言”代码需要先编译成 JavaScript代码才能运行,而编译器没有很严格地检查代码是否符合正常语法,我们可以轻松地注入 JavaScript 表达式:

「(str=>process.stdout.write(str))」『問天地好在』

在这句代码中直接嵌入了一个 JavaScript 箭头函数 str => process.stdout.write(str)。利用这种方法,我们可以调用 Node.js 环境下的标准库来实现读取输入。但问题是,如果直接读取 /dev/stdin 的内容,就必须一次读取完整个输入数据,而无法在命令行进行人机交互。

如果是 Node.js 开发,一般会采用原生的 readline 模块来读取用户输入。但它是异步运行的,要想用它来给“文言”程序读取输入,大概需要修改整个“文言”编译器的代码,让它编译出支持异步的程序,这似乎不太现实。于是我通过查阅各种资料,摸索出来了不使用异步操作读取命令行输入的办法:

// gets.js

const fs = require("fs")
const SEGMENT_LEN = 1024
const EOL_BUFFER = Buffer.from(require("os").EOL)

function gets() {
  // 缓冲区,以及已读入的字节数
  let buffer = Buffer.alloc(SEGMENT_LEN)
  let len = 0

  while (true) {
    // 读取一字节,如果 EOF 就停止读入
    if (fs.readSync(0, buffer, len, 1) === 0) break
    ++len

    // 如果已经换行就停止读入
    if (buffer.subarray(len - EOL_BUFFER.length, len).equals(EOL_BUFFER)) break

    // 如果缓冲区已经写满就扩容
    if (len === buffer.length) {
      const oldBuffer = buffer
      buffer = Buffer.alloc(oldBuffer.length + SEGMENT_LEN)
      buffer.set(oldBuffer)
    }
  }

  return buffer.subarray(0, len).toString()
}

这里声明了一个 gets() 函数,可以读取一行用户输入。方法稍显笨拙:每次用 fs.readSync(0) 读取一个字节,这里如果用户还没有输入完成并按下回车,就会阻塞程序,等待用户输入;用户按下回车后,程序就会逐字节地读取输入的内容,直到遇到换行符为止。

原型有了,就可以用“文言”来实现了。我定义了一个 閱行 函数,并给它创建了语法糖 ……一直到 ,分别对应调用函数 閱行 一次至九次。这样一来,我们就可以很方便地连续读取多行输入:

名之曰」。

这就相当于:

閱行」。閱行」。閱行」。名之曰」。

不过,在解算法题的时候,我们往往需要从输入中读入数字、字符、单词,而不是读取一整行,所以我还添加了这些读取特定类型数据的方法:閱數閱字閱言,并为它们定义了相应的语法糖。

这样一来,用“文言”来解 A+B Problem 就可以这样写:

名之曰」。
」。書之

当然,要想得到正确的输出,我们不能直接用“文言”的解释器来运行这个程序,因为这样会输出中文数字;需要先把程序编译成 JavaScript,再调用 Node.js 运行编译出的代码。

wenyan -c program.wy > compiled.js
node compiled.js < input.txt > output.txt

在“文言”编程中,输出被称作“”,那么读取输入不妨叫做 “”;许多第三方的扩展库都叫做“某某秘術”,因此我决定把我的这个库命名为“閱文秘術”。

这里是“閱文秘術”的 GitHub 仓库。


评论区

加载基于 GitHub issues 的 utteranc.es 评论区组件……