上篇:如虎添翼:Console Matlab Vim完美结合[Matlab视角]
首先,为何要大费周章的做这些事呢?一方面是个人喜好,一方面借应用来检验我的“混合编程插件”的扩展性和健壮性,或者……如果你想一直写代码,那花个几天时间打造整合个称手的开发环境那是相当值得的!加油!
环境:Fedora 10+Vim7.2
项目主页:http://code.google.com/p/vimhybridevel
在上篇里虽然实现了在Matlab Console里运行vim,但关键问题还没解决:vim没法直接跟matlab通信。试想一下(实际情况就是如此),写了一段m script,然后运行没有得到自己的期望或要做参数调整,改吧,于是每改一次都(在matlab console)运行、退出vim,这个看上去没什么问题,但是所有的Undo和Redo信息都丢失了……所以很有必要解决vim直接与matlab通信的问题。
在继续之前先啰嗦一些题外话。08年年底的时候我写了个vim插件hybridevel.vim,目的是方便混合编程,自己使用一直很满意,这半年来也陆续修正了一些小bug,但是一直没有完善的文档,故此插件到现在也没有发布。既然是“混合编程”,那包含对m script的支持也就理所当然了,但是上次的设计并未考虑到matlab这样的特殊情况,所以花了一点时间来改造那个插件。(实际上它们都共同依赖于另一个插件shellinsidevim.vim,最新版本并未上传给vim.org)
本解决方案针对的问题是在不离开vim的情况下执行m script,由于vim对m scrippt的支持本身由hybridevel.vim插件负责,因此vim代码只需考虑最核心的部分。具体来说:
1.使用mlint评估m script,以便发现“编译期”错误和获得提升性能的修改建议:
方便起见,这里用来matlab的外部工具mlint命令,因此实现起来没有什么技术难度。但是这里有两个很郁闷的问题,vim自带了compiler/mlint.vim文件用于根据其输出生成quickfixlist,但是很奇怪的是在我这里总是匹配不到任何errorformat,为了弄清楚原因特意一点点对照errorformat规范仔细检查,未果。估计不是文件参数的问题,怀疑是vim的bug(我这个vim在分析ant输出时也无法正常解析出错文件名)。所以不再浪费时间,转而根据其原理和vim api手动实现了quickfixlist的生成。顺便也说一点,如果你的vim解析mlint不成问题,也有一个问题需要考虑,mlint的输出中不包含文件名,除非你一次传递多个文件作为参数,因此在处理单个文件是需要手动规范输出以配合errorformat,不然即使生成了quickfixlist也不会自动定位,还不如干脆都自己编码来的方便。
function! g
o_matlab_mlint()
call g:ExecuteCommand(g
arseCommand(get(g
roject.current.command,'mlint',"")))
let qfix={'filename':bufname("%"),'lnum':0,'col':0,'vcol':0,'nr':-1,'text':''}
let quickfixes=[]
let hasError=0
for line in split(@+,"n")
if g:Trim(line)==''
continue
endif
if match(line,'^(={10}) .+ 1$')>-1
let qfix['filename']=match(line,'[^= ]+')
continue
endif
let qfix['lnum']=matchstr(line,'^L d+')[2:-1]
let qfix['col']=matchstr(line,'(C d+')[3:-1]
let qfix['text']=matchstr(line,'): u+:.*$')[3:-1]
call add(quickfixes,copy(qfix))
endfor
call setqflist(quickfixes)
cc!
endfunction
2.vim与matlab通信,执行m script并获取执行结果,包括标准输出和figure显示:
实现这个功能的方法也有很多,但是考虑到兼容性、方面性和移植性,这里没有采用诸如“写辅助shell脚本”或“使用vim的编程语言接口(它可能要求重现编译vim以支持所需的语言)”等很多大型vimscript插件的做法。其实这个需求很简答,不要求与matlab交互,只是将一系列命令发给matlab然后获取其执行结果而已,使用shell的“文件重定向”特性足以,不过缺点是,即使vim是如上篇所述在matlab里启动的,此方法在实施时仍然会再启动一个“临时”的matlab。不过由于宿主和子matlab都是“-nodesktop”模式的,系统开销的增加相对于其便捷性也就可以忽略不计了。
“重定向”的实现有一些小细节,比如“文件”可能是file或named pipe或named socket,这里用的是file,由于file是“非阻塞”的通道,或者说这个通道的另一端一定有接收者,所以得考虑一个顺序,matlab一旦启动就会输出欢迎信息,然后被stdout接受,而matlab又没有接收到stdin,于是它就直接退出了,显然这样得不到运行结果。而如果是选择named pipe类“阻塞”的通道,只要无视stdout或stderr(如果有的话),matlab就会一直等待,此时无论什么时候给stdin都没问题,当然由于vimscript是单线程的,自然在matlab等待时没机会给它输入;即使在启动matlab时指名其在后台运行,可以有交互式输入,但是仍然无法检测stdout或stderr(除非之后不用再交互了)。因此,单凭基本vim script(使用vim编程语言接口比如python的不算)来实现的交互能力是很有限的。
这么一来思路就清晰了,在启动matlab之前先准备好要执行的matlab命令,这里又有一个问题,上篇里我们提到,现代版本的matlab搞得像个小操作系统,在“-nodesktop”模式下启动的matlab相当于以操作系统桌面为桌面,因此如果执行过程中需要创建小窗口,比如显示图像的figure,则它相当于一个独立的子程序异步执行。也就是说stdin里的命令会继续执行,等到matlab使命完成退出,然后……失去父进程的小窗口也跟着强行关闭(好在stdout跑不了,可以少操点心了)。这是个很棘手的问题,大大超出了vim的控制范围,不过既然matlab把自己整的这么伟大,那是不是它本身能够同步处理小窗口呢?嗯,我猜对了,顺着其exit的help一路找到了uiwait这样的函数。一目了然,stdin的最后一个/组命令应该是循环“uiwait(get(0,'CurrentFigure'))”,等待所有figure退出(如果没有则直接退出),好了,可以慢慢欣赏绘制的图像了。
向完美主义看齐继续处理最后的一些小问题:格式化stdout和stderr,去掉那些乱七八糟的输出和matlab command line控制符,这个就是基本的List和String操作。不过有一点需要说明,stderr即使是在matlab command line里显示也都是干净的,但是其实它的每条错误都被“{^H??? ”和“}^H”包围着(“^H”是ASCII的8)。
function! g
o_matlab_interpret()
let arguments='('.substitute(g
arseCommand("ARGUMENTS"),' ',',','g').')'
let maincmd=fnamemodify(g
roject.current.main,":t:r").substitute(arguments,'^()$','','g')
let matlabcmds=[]
call add(matlabcmds,"cd ".fnamemodify(bufname("%"),":p:h"))
call add(matlabcmds,maincmd)
call add(matlabcmds,"display(' ')")
call add(matlabcmds,"while get(0,'CurrentFigure'),")
call add(matlabcmds,"uiwait(get(0,'CurrentFigure'))")
call add(matlabcmds,'end')
try
call writefile(matlabcmds,'.MATLABIN')
let launcher=g
arseCommand(get(g
roject.current.command,'interpret',""))
call g:ExecuteCommand(launcher,'0< .MATLABIN','1>.MATLABOUT','2>.MATLABERR','&')
while 1
let stdout=readfile('.MATLABOUT')
let head=match(stdout,'^>> >> ')
if head==-1
continue
elseif !exists('startpos')
let startpos=head
call g:AddShellCommandResult(['>> '.maincmd,''])
call g
isplayOutput()
endif
let stdout[head]=substitute(g:Trim(stdout[head][5:-1]),'(>>s*)*$','','g')
let endpos=len(stdout)-2
if (endpos>=startpos)
call g:AddShellCommandResult(stdout[startpos :endpos]+[''])
call g
isplayOutput()
let startpos=endpos+1
endif
if g:Trim(stdout[-1])=~'^>>$'
break
endif
endwhile
let H=nr2char(
let stderr=join(filter(readfile('.MATLABERR'),'len(v:val)>len("")')," ")
let startpos=match(stderr,'{'.H.'???',0)
while startpos>=0
let endpos=match(stderr,'}'.H,startpos)
call g:AddShellCommandResult(["ExecuteCommand failed: ".stderr[startpos+6:endpos-2],''])
let startpos=match(stderr,'{'.H.'???',endpos)
endwhile
call g
isplayOutput()
finally
call delete('.MATLABIN')
call delete('.MATLABOUT')
call delete('.MATLABERR')
endtry
endfunction
uiwait(get(0,'CurrentFigure'))
至此console、matlab和vim三者的整合告一段落了,根据这次折腾的收获,根据上述思路可以更进一步的完善vim和matlab或其他进程的交互。