笨方法写Quine
2024-09-21什么是Quine
Quine是一类很有意思的程序,它运行之后所输出的结果是它自身。这种程序又被称为“自我复制程序”,具有某种类似生物一样的奇妙的自指特征,很有意思。
下面两个quine程序的例子是我从维基百科上抄下来的。
示例1: Python
c = 'c = %r; print(c %% c)'; print(c % c)
示例2: Java
public class Quine
{
public static void main(String[] args)
{
char q = 34; // Quotation mark character
String[] l = { // Array of source code
"public class Quine",
"{",
" public static void main(String[] args)",
" {",
" char q = 34; // Quotation mark character",
" String[] l = { // Array of source code",
" ",
" };",
" for (int i = 0; i < 6; i++) // Print opening code",
" System.out.println(l[i]);",
" for (int i = 0; i < l.length; i++) // Print string array",
" System.out.println(l[6] + q + l[i] + q + ',');",
" for (int i = 7; i < l.length; i++) // Print this code",
" System.out.println(l[i]);",
" }",
"}",
};
for (int i = 0; i < 6; i++) // Print opening code
System.out.println(l[i]);
for (int i = 0; i < l.length; i++) // Print string array
System.out.println(l[6] + q + l[i] + q + ',');
for (int i = 7; i < l.length; i++) // Print this code
System.out.println(l[i]);
}
}
但是这两个例子里面,代码都不是很好理解。假如我想要用第三种语言,比如C++或者JavaScript,写一个quine,还需要重新构思。
因此,我希望找到一种通用的方式,可以以最直观最容易的方式写出quine。虽然这种quine不一定是最短、最高效的,但是最直观、最容易举一反三。
Quine的规律
观察上一节的两个quine,虽然看起来区别很大,但是其实总结起来大概分这么几步:
- 开头,可能有一些import或者类定义之类的东西;
- 一个字符串,因为其作用有点像是遗传物质,所以这里我就记作DNA,其内容应当包含这个字符串前面的程序,加上这个字符串后面的程序;
- 结尾:
- 从字符串DNA中取出开头,并打印;
- 打印这个字符串本身;
- 从字符串DNA中取出结尾,并打印。
虽然看上去很简单,但是常用的编程语言中,在表达字符串的时候,换行符、引号都需要额外的转义。例如,字符串中换行符要写作“\n”,引号前面要加上反斜杠。这些内容带来了不必要的麻烦,这也是上面的代码难以看懂的原因。
但是,这些转义符号,其本质都是一种编码。所以,我的思路是,干脆直接把字符串DNA编码成最简单的16进制形式,然后需要打印的时候再解码。
这样,就不需要奇怪的hack了。
而且,也不一定非得是16进制编码,用base64、base32,或者跟真正的DNA一样,用碱基字母编码,都是可以的。
妙妙小工具
为了把字符串编码成16进制,我用Python写了一个小工具,可以读入字符串,输出其16进制形式:
import sys
s = sys.stdin.read()
if s[-1] == '\n':
s = s[:-1]
print(s.encode('utf-8').hex())
把上述代码保存为hexencode.py,然后在Shell中可以这样使用:
cat input.txt | python3 hexencode.py
开始写代码
Python写起来比较简单,就先用Python吧。
首先定义字符串DNA。这里我们不知道“DNA”的内容,所以先用emoji符号代替。因为这个字符串里面包含了两部分内容:头和尾,我们假设头是老虎,尾巴是蛇:
dna = '🐱,🐍'
然后把头和尾巴取出来:
head, tail = dna.split(',')
因为我们打算用16进制编码,所以这里要把头和尾都用16进制解码恢复成原来的样子:
head = bytes.fromhex(head).decode('utf-8')
tail = bytes.fromhex(tail).decode('utf-8')
最后,我们把头、DNA、尾巴,这三部分拼接起来,一起输出:
print(head + dna + tail)
现在的程序是这样的:
dna = '🐱,🐍'
head, tail = dna.split(',')
head = bytes.fromhex(head).decode('utf-8')
tail = bytes.fromhex(tail).decode('utf-8')
print(head + dna + tail)
老虎🐱头前面的部分是:
dna = '
使用妙妙小工具编码,得到:
646e61203d2027
把🐱替换成这个字符串。
接着,蛇🐍尾巴后面的部分是:
'
head, tail = dna.split(',')
head = bytes.fromhex(head).decode('utf-8')
tail = bytes.fromhex(tail).decode('utf-8')
print(head + dna + tail)
使用妙妙小工具编码,得到:
270a686561642c207461696c203d20646e612e73706c697428272c27290a68656164203d2062797465732e66726f6d6865782868656164292e6465636f646528277574662d3827290a7461696c203d2062797465732e66726f6d686578287461696c292e6465636f646528277574662d3827290a7072696e742868656164202b20646e61202b207461696c29
把🐍替换成这个字符串。
最后得到了这样的代码:
dna = '646e61203d2027,270a686561642c207461696c203d20646e612e73706c697428272c27290a68656164203d2062797465732e66726f6d6865782868656164292e6465636f646528277574662d3827290a7461696c203d2062797465732e66726f6d686578287461696c292e6465636f646528277574662d3827290a7072696e742868656164202b20646e61202b207461696c29'
head, tail = dna.split(',')
head = bytes.fromhex(head).decode('utf-8')
tail = bytes.fromhex(tail).decode('utf-8')
print(head + dna + tail)
到这里,一个quine就已经完成了。运行一下试试吧。
推广到其他语言
这里就用C++举例好了:
先写出差不多的模板:
#include <iostream>
#include <string>
void split(std::string input, std::string &first, std::string &second);
std::string hex_decode(std::string hex);
int main() {
std::string dna = "🐱,🐍";
std::string head, tail;
split(dna, head, tail);
head = hex_decode(head);
tail = hex_decode(tail);
std::cout << head << dna << tail << std::endl;
}
// 麻烦的地方是C++标准库里面不提供分割和16进制解码函数
// 我也懒得写了,直接AI生成一下:
void split(std::string input, std::string &first, std::string &second) {
size_t commaPos = input.find(',');
if (commaPos != std::string::npos) {
first = input.substr(0, commaPos);
second = input.substr(commaPos + 1);
} else {
first = input;
second = "";
}
}
std::string hex_decode(std::string input) {
std::string output;
output.reserve(input.size() / 2);
for (size_t i = 0; i < input.size(); i += 2) {
std::string byteString = input.substr(i, 2);
char byte = static_cast<char>(std::strtol(byteString.c_str(), nullptr, 16));
output.push_back(byte);
}
return output;
}
然后把🐱前面的程序文本和🐍后面的程序文本用妙妙小工具编码成16进制并替换,得到:
#include <iostream>
#include <string>
void split(std::string input, std::string &first, std::string &second);
std::string hex_decode(std::string hex);
int main() {
std::string dna = "23696e636c756465203c696f73747265616d3e0a23696e636c756465203c737472696e673e0a0a766f69642073706c6974287374643a3a737472696e6720696e7075742c207374643a3a737472696e67202666697273742c207374643a3a737472696e6720267365636f6e64293b0a7374643a3a737472696e67206865785f6465636f6465287374643a3a737472696e6720686578293b0a0a696e74206d61696e2829207b0a20207374643a3a737472696e6720646e61203d2022,223b0a20200a20207374643a3a737472696e6720686561642c207461696c3b0a202073706c697428646e612c20686561642c207461696c293b0a202068656164203d206865785f6465636f64652868656164293b0a20207461696c203d206865785f6465636f6465287461696c293b0a20200a20207374643a3a636f7574203c3c2068656164203c3c20646e61203c3c207461696c203c3c207374643a3a656e646c3b0a7d0a0a2f2f20e9babbe783a6e79a84e59cb0e696b9e698af432b2be6a087e58786e5ba93e9878ce99da2e4b88de68f90e4be9be58886e589b2e5928c3136e8bf9be588b6e8a7a3e7a081e587bde695b00a2f2f20e68891e4b99fe68792e5be97e58699e4ba86efbc8ce79bb4e68ea54149e7949fe68890e4b880e4b88b3a0a0a766f69642073706c6974287374643a3a737472696e6720696e7075742c207374643a3a737472696e67202666697273742c207374643a3a737472696e6720267365636f6e6429207b0a2020202073697a655f7420636f6d6d61506f73203d20696e7075742e66696e6428272c27293b0a2020202069662028636f6d6d61506f7320213d207374643a3a737472696e673a3a6e706f7329207b0a20202020202020206669727374203d20696e7075742e73756273747228302c20636f6d6d61506f73293b0a20202020202020207365636f6e64203d20696e7075742e73756273747228636f6d6d61506f73202b2031293b0a202020207d20656c7365207b0a20202020202020206669727374203d20696e7075743b0a20202020202020207365636f6e64203d2022223b0a202020207d0a7d0a0a7374643a3a737472696e67206865785f6465636f6465287374643a3a737472696e6720696e70757429207b0a202020207374643a3a737472696e67206f75747075743b0a202020206f75747075742e7265736572766528696e7075742e73697a652829202f2032293b0a20202020666f72202873697a655f742069203d20303b2069203c20696e7075742e73697a6528293b2069202b3d203229207b0a20202020202020207374643a3a737472696e672062797465537472696e67203d20696e7075742e73756273747228692c2032293b0a2020202020202020636861722062797465203d207374617469635f636173743c636861723e287374643a3a737472746f6c2862797465537472696e672e635f73747228292c206e756c6c7074722c20313629293b0a20202020202020206f75747075742e707573685f6261636b2862797465293b0a202020207d0a2020202072657475726e206f75747075743b0a7d";
std::string head, tail;
split(dna, head, tail);
head = hex_decode(head);
tail = hex_decode(tail);
std::cout << head << dna << tail << std::endl;
}
// 麻烦的地方是C++标准库里面不提供分割和16进制解码函数
// 我也懒得写了,直接AI生成一下:
void split(std::string input, std::string &first, std::string &second) {
size_t commaPos = input.find(',');
if (commaPos != std::string::npos) {
first = input.substr(0, commaPos);
second = input.substr(commaPos + 1);
} else {
first = input;
second = "";
}
}
std::string hex_decode(std::string input) {
std::string output;
output.reserve(input.size() / 2);
for (size_t i = 0; i < input.size(); i += 2) {
std::string byteString = input.substr(i, 2);
char byte = static_cast<char>(std::strtol(byteString.c_str(), nullptr, 16));
output.push_back(byte);
}
return output;
}
把这个C++源代码文件保存为quine.cpp,然后可以用以下命令验证其输出结果是否和原来的源代码一致:
g++ quine.cpp && ./a.out | diff quine.cpp -
多语言的Quine
还有一种quine是这样的:A语言写出来的程序输出B语言的源代码,这段B语言的源代码又输出一开始的A语言的代码,形成了一个A到B回到A的循环。这个过程甚至可以包括更多语言,例如A到B到C到D到E再回到A。
GitHub上有个项目就构造了一个100多种语言的循环。
我们用了这种16进制编码的“笨方法”之后,因为不需要再考虑转义字符的问题了,无论多少种语言都可以轻松构造。
接下来我们就把上面的Python和C++结合起来,构造一个C++程序输出Python程序,Python程序运行又输出原来的C++程序的例子。
先写出Python版本的模板,还是和上面一样:
dna = '🐱,🐍'
head, tail = dna.split(',')
head = bytes.fromhex(head).decode('utf-8')
tail = bytes.fromhex(tail).decode('utf-8')
print(head + dna + tail)
然后,写一个C++程序,输出上面的模板:
#include <iostream>
#include <string>
std::string py =
"dna = '🐱,🐍'\n"
"head, tail = dna.split(',')\n"
"head = bytes.fromhex(head).decode('utf-8')\n"
"tail = bytes.fromhex(tail).decode('utf-8')\n"
"print(head + dna + tail)\n";
int main() {
std::cout << py;
return 0;
}
然后把这段C++代码中,🐱前面的程序文本和🐍后面的程序文本用妙妙小工具编码成16进制并替换,得到:
#include <iostream>
#include <string>
std::string py =
"dna = '23696e636c756465203c696f73747265616d3e0a23696e636c756465203c737472696e673e0a0a7374643a3a737472696e67207079203d0a2020202022646e61203d2027,275c6e220a2020202022686561642c207461696c203d20646e612e73706c697428272c27295c6e220a202020202268656164203d2062797465732e66726f6d6865782868656164292e6465636f646528277574662d3827295c6e220a20202020227461696c203d2062797465732e66726f6d686578287461696c292e6465636f646528277574662d3827295c6e220a20202020227072696e742868656164202b20646e61202b207461696c295c6e223b0a0a696e74206d61696e2829207b0a202020207374643a3a636f7574203c3c2070793b0a2020202072657475726e20303b0a7d'\n"
"head, tail = dna.split(',')\n"
"head = bytes.fromhex(head).decode('utf-8')\n"
"tail = bytes.fromhex(tail).decode('utf-8')\n"
"print(head + dna + tail)\n";
int main() {
std::cout << py;
return 0;
}
把这段代码保存为quine.cpp,并验证结果:
g++ quine.cpp
./a.out > quine.py
python3 quine.py > quine2.cpp
diff quine.cpp quine2.cpp
完工。
Email: i (at) mistivia (dot) com