python subprocess 中关于 executable 的想当然陷阱
背景:gpp 通用前处理器
GPP is a general-purpose preprocessor with customizable syntax, suitable for a wide range of preprocessing tasks. Its independence from any one programming language makes it much more versatile than the C preprocessor (cpp), while its syntax is lighter and more flexible than that of GNU m4. There are built-in macros for use with C/C++, LaTeX, HTML, XHTML, and Prolog files.
简单来说就是可以让你在任意文本文件之间使用#define
、#include
等命令,适合构建一些文本处理体系。
没搜到中文资料,Ubuntu 下可以直接 apt 安装,Windows下则需要自己拿MSYS2来编译,事实上这篇文章说的就是gpp
的编译。
今天的坑发生在尝试用python
调用gpp
的时候。
问题的排查处理
不是很熟悉subprocess
的用法,这是事件的前提。简单看了看文档后,我写了这样的代码:
subprocess.check_call(
executable=rpEnv.gppExe, # rpEnv.gppExe 存放着 gpp 程序的绝对路径
args=["-H", "-I{}".format(gemsPath), "-o", workingMD, inputMD],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
因为有个参数叫executable
,我很自然的就认为应该在这里传递可执行文件的绝对地址,然后args
里面放参数。也许有经验一看就知道问题,但关键是,这玩意儿能运行!不报错!结果也似乎挺好的!
然后我就这么用了一周,直到今天,我开始真正往模板文件里面加<#define>
了,然后这货就开始报错,还不详细,就是返回值为1
,输出只到第一个<
就结束了。
于是我就尝试自己在命令行里面写./bin/gpp/gpp -H -I/home/gems /home/sim.md -O /home/1.md
这样的命令,然后发现输出是正常的……
也就是说,不加<#define>
这样的实质性命令,subprocess
调用和手敲命令都正常;加<#define>
这样的实质性命令之后,subprocess
调用gpp
就出错了,但手敲命令正常。于是就猜想什么stdin
一类的问题,但仔细想想又实在不像。
再去仔细看subprocess
的文档,发现怎么很多地方都是只写args
不写executable
啊,程序名直接写在args
第一个,然后还有这样一句:
executable
参数指定一个要执行的替换程序。这很少需要。
不管啥道理,试一下:
subprocess.check_call(
args=[rpEnv.gppExe, "-H", "-I{}".format(gemsPath), "-o", workingMD, inputMD],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
这就不报错了。仔细看executable
参数相关文档,可以发现这是个高级用法,我理解是用于希望修改程序参数列表第一项的情况,也就是C/C++接受的的零号参数,一般都等于可执行文件名,正常情况下不用的那个。印象中似乎gcc
会用软链接配合这个机制来区分你调用的是gcc
还是g++
。同时,这个选项的行为跟shell=True
选项还有耦合,这个用到的时候再研究吧。
然后就清楚了,如果全用args
来调用,跟手敲命令是等价的。如果用上面那一版带executable
参数的代码,则系统实际上调用的是executable
指定的那个gpp
,但这个gpp
收到的参数中,第零项,本该是类似~/bin/gpp/gpp
这样的参数,被替换成了-H
,但替换了也没啥鸟用,gpp
解析命令行参数的时候会把忽略掉,因为正常参数不会放到第零个位置。
那为啥一开始用的好好的?这同时推断出了gpp
的一个内部机制:它应该是遇到了<#define>
这样的预处理命令之后,才去命令行里面找如何处理,所以直到第一次遇到#define
的时候,缺失参数-H
的后果才显出来。之前我没开始做这一块,输入全是普通文本,就没显出来。
为了验证,回到第一版代码,再在args
的第一位加一个绝对与-H
冲突的命令-T
,变成args=["-T", "-H", "-I{}".format(gemsPath), "-o", workingMD, inputMD]
,结果是无任何报错,输出完全正常。
再次验证,手敲命令,但故意把-H
参数漏掉变成./bin/gpp/gpp -I/home/gems /home/sim.md -O /home/1.md
,发现如果输入文件中没有#define
则一切正常,存在#define
则报错:
error: #define/#defeval requires an identifier or a single macro call
与猜想完全一致。
悲伤的后文
写完上文几天之后,再次需要更改调用命令,但这次比较特殊,加了一大堆双引号,还有反斜杠\
啥的。
然后就发现,直接改上面的subprocess.check_call
调用,怎么改都会出错,想要调试大概只能自己写一个东西输出实际接收到的命令行选项才行。
然后就还是用了懒办法:
gppUDef = ["-U", '"<#"', '">"', r'"\B"', '"|"', '">"', '"<"', '">"', '"#"', '""']
argArray = (
[rpEnv.gppExe] + gppUDef + ["-I{}".format(gemsPath), "-o", workingMD, inputMD]
)
gppCmd = " ".join(argArray)
gppR = os.system(gppCmd)
if not gppR == 0:
print("GPP call error: " + gppCmd)
用os.system(gppCmd)
,一点毛病都没有。
不过,据说,如果有更改cwd
的需求,os.system
会麻烦一些。