“文言”编程语言能读取标准输入了
前不久我给“文言”写了个扩展库,让它能够通过 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 仓库。