
前阵子给一屋子测试机统一搭建运行环境几十台机器要挨个启动采集程序、拉起数据服务再配合串口设备做自检。手动一台台点开点到最后往往记不清哪台漏了哪台重复了。当时我就靠几段VBS脚本把这些杂活全包了——VBS 运行外部程序说白了就是一行 CreateObject 加一行 Run 的事但真要处理参数传递、等待进程结束、捕获输出、按需提权、配合串口设备通信这些场景中间其实有相当多值得记一笔的细节。这篇文章把我这些年用 VBS 拉起外部程序的经验、踩过的坑和绕路的方案整理出来适合系统管理员、测试工程师也适合想用 Windows 脚本做自动化的新手朋友参考。1. 先认识 WScript.ShellVBS 唤启外部程序的底层入口1.1 为什么所有 VBS 启动外部程序的例子里都有它VBScript 本身并没有直接启动一个程序的内置函数能帮你干这件事的是 Windows 提供的一个 COM 对象注册名叫做 WScript.Shell。可以把它理解为系统给脚本准备的一个万能工具箱里面放着操作窗口、读写注册表、创建快捷方式、发送按键以及我们最关心的运行外部程序这一类功能。在这个思路上VBS 里启动外部程序的标准起手式就是Set objShell CreateObject(WScript.Shell) objShell.Run notepad.exe第一句的作用是向系统请求一个 WScript.Shell 对象第二句是调用这个对象的 Run 方法去启动记事本。很多新手以为 Run 就是全部但实际使用中你会发现Run 只是这个工具箱里最基础的起子后面要讲的 Exec、ShellExecute 才是应对复杂场景的进阶工具。这个对象还有一个变体叫做 WshShell在 HTA、WSF 等宿主里同样存在。日常写 .vbs 文件时统一用 CreateObject(WScript.Shell) 就行两种写法不影响功能。值得注意的一点是这个 COM 组件在 Windows 2000 之后的内核版本里都是系统自带的不需要额外安装依赖这也是 VBS 能在老机器上依然被人捡起来用的原因之一。1.2 Run 方法的几个参数不搞懂会出幺蛾子Run 方法的完整签名实际上是这样的object.Run(strCommand, [intWindowStyle], [bWaitOnReturn])其中 strCommand 是必填的后面两个可选参数才是真正影响行为的关键。intWindowStyle 控制启动程序的窗口显示方式常用的值包括常量/数值窗口状态适用场景0隐藏窗口静默执行批处理、后台任务1正常显示启动图形界面程序3最大化全屏展示类工具7最小化需要最小化运行减少干扰4普通窗口但无焦点不想抢占用户注意力的程序bWaitOnReturn 是布尔值默认为 False也就是 Run 一发出指令立即返回、继续执行后面的代码。如果设为 True脚本会傻傻地等到被启动程序退出后才继续走下一行。这里有个经常被误解的点很多人以为 Run 方法返回的是进程 ID其实它返回的只是一个数值绝大多数情况下是 0。想拿到真正的进程 ID得走后面要讲的 Exec 路线。另一个高频坑是路径带空格。Run 方法内部走的命令行解析逻辑和你在运行框里输入命令是类似的所以如果程序路径里含有空格必须自己加一对双引号objShell.Run C:\Program Files\SomeApp\app.exe --init反过来如果路径里没有空格却强行加引号反而可能因为引号被当成命令的一部分而出错。建议固定习惯凡是路径变量都先用双引号包好再传给 Run宁可多包也不要漏包。2. Run、Exec、ShellExecute三种唤启姿势的取舍2.1 Run最简单但别指望它拿到程序输出Run 适合的场景是启动后就不管了打开记事本、拉起一个服务、执行一个安装包。这种姿势的优点是代码量极少也不容易出错。实际操作中我经常用 Run 来处理并行启动的场景。比如要一次性拉起三个服务端程序Set shell CreateObject(WScript.Shell) shell.Run srv_a.exe, 1, False shell.Run srv_b.exe, 1, False shell.Run srv_c.exe, 1, False由于 bWaitOnReturn 全部是 False三个程序会近乎同时启动比起一个个双击、一个个等待效率高了一个量级。尤其在做批量测试环境准备时这套发令枪式的启动方式帮我省了大量时间。但 Run 的天花板也很明显你拿不到标准输出拿不到退出码更无法实时判断程序是否启动成功。如果只需要开个门Run 恰到好处如果还需要听里面的人在说什么就必须换 Exec 了。2.2 Exec能接 StdOut/StdErr但小心读到死等Exec 方法在 WScript.Shell 对象上是和 Run 平行的存在实际运行的是 cmd 解释器来执行命令所以几乎所有的命令行工具都能交给它。最关键的区别是Exec 返回一个 WshScriptExec 对象这个对象带有 StdOut、StdErr 两个管道还有 Status、ExitCode 属性让你能跟被启动的程序对话。一个典型的读取输出的代码Set shell CreateObject(WScript.Shell) Set exec shell.Exec(ping 127.0.0.1 -n 3) WScript.StdOut.Write exec.StdOut.ReadAll这段代码能完整输出 ping 的结果。但请记住一个教训当被启动程序持续输出大量内容时直接在脚本里调用 ReadAll 会让脚本挂在那里死等因为 ReadAll 要读到管道关闭才返回。如果那个程序不退出管道就不会关闭你的脚本也就永远停在那一步。更稳的做法是逐行读取Do While Not exec.StdOut.AtEndOfStream WScript.Echo exec.StdOut.ReadLine Loop逐行处理的另一个好处是能实时看到程序进度配合 WScript.Sleep 做轮询日志效果和你在命令行里看到的几乎一致。对于 ping、tracert、robocopy 这类会持续输出、又不会立刻结束的命令用 Exec 比用 Run 加重定向可靠得多。2.3 需要管理员权限时用 Shell.Application 的 ShellExecuteRun 和 Exec 都没法直接指定以管理员身份运行。日常双击脚本触发的进程继承的是当前用户的权限如果目标程序需要管理员权限就会触发 UAC 弹窗或者干脆启动失败。这时候我习惯走 Shell.Application 对象的 ShellExecute 方法通过 verb 参数指定 runas就能主动申请提权Set app CreateObject(Shell.Application) app.ShellExecute regedit.exe, , , runas, 1ShellExecute 还有一重妙用它能直接调用系统文件关联。比如我想让一个 .pdf 用默认阅读器打开命令行里很难写得干净ShellExecute 一行就够了app.ShellExecute report.pdf这种方式本质上就是模拟你在资源管理器里双击文件非常省心。不过要注意ShellExecute 的调用是异步的它不会等待程序退出如果你同时需要等待和捕获输出那就只能回到 Exec 的路线上或者用 Run 配合 WaitOnReturn。2.4 三种方式对比小结特性RunExecShellExecute启动外部程序支持支持支持等待程序退出通过 bWaitOnReturn自带 WaitOnReturn 逻辑不支持获取退出码不可靠支持 ExitCode不支持捕获标准输出不支持支持不支持指定提权动词不支持不支持支持 runas调用系统文件关联不支持不支持支持典型场景快速启动、批量并行命令工具、日志捕获提权安装、打开文件根据需求选方案是我写这类脚本时始终坚持的原则。能 Run 的绝不硬上 Exec需要提权的直接走 ShellExecute需要交互输出的才动用 Exec。方案选对了后面能少踩一半的坑。3. 等进程结束、判退出码、按需清理启动之后那些操心事3.1 WaitOnReturn 与轮询循环怎么优雅地等一个程序结束有些场景要求等上一个程序跑完才能启动下一个比如安装包必须先执行完毕再验证版本。Run 的 bWaitOnReturn 参数是最直接的实现方式Set shell CreateObject(WScript.Shell) shell.Run setup.exe /silent, 1, True WScript.Echo 安装完成继续下一步但 bWaitOnReturn True 有个隐藏风险如果被启动程序因为某些原因挂住不退出你的脚本也会永远卡死在这里。碰到这种场景我宁可自己写一个带超时控制的轮询循环用 Exec 方式启动然后轮询 StatusSet shell CreateObject(WScript.Shell) Set exec shell.Exec(long_running_task.exe) startTime Timer timeoutSeconds 60 Do While exec.Status 0 If Timer - startTime timeoutSeconds Then WScript.Echo 超时了强制结束 exec.Terminate Exit Do End If WScript.Sleep 500 Loop注意 Status 为 0 表示正在运行为 1 表示已结束。Terminate 可以强制结束进程这个在测试环境里尤其常用。轮询间隔我一般选 300 到 500 毫秒既不会让 CPU 空转太厉害也不至于错过进程状态变化。3.2 拿退出码判断成功失败比看弹窗可靠一线运维里判断一个程序是否成功不能靠肉眼退出码才是最诚实的。Exec 对象的 ExitCode 属性能在程序结束后返回进程退出码0 通常表示成功非 0 则对应不同错误语义。我常写一个通用函数来跑外部命令并把退出码抛出来Function RunCommand(cmdLine) Set shell CreateObject(WScript.Shell) Set exec shell.Exec(cmd /c cmdLine) Do While exec.Status 0 WScript.Sleep 200 Loop RunCommand exec.ExitCode End Function code RunCommand(xcopy D:\data E:\backup /E /Y) If code 0 Then WScript.Echo 备份成功 Else WScript.Echo 备份失败退出码 code End If多提一句cmd /c 外面包一层可以确保复杂命令里的重定向、管道符号能被正常解析。用 Exec 直接执行带 或 | 的命令时这些符号可能被 VBS 当作脚本语法处理导致莫名其妙的报错。走了 cmd /c 之后就等同于把整条命令扔给命令行解释器兼容性最好。3.3 顺带看看怎么按进程名杀掉多余的程序和运行外部程序配套的头痛问题之一是进程清理。测试环境跑完一轮后经常残留一堆僵尸进程VBS 也能通过 WMI 按名称查杀Set wmi GetObject(winmgmts:\\.\root\cimv2) Set procs wmi.ExecQuery(SELECT ProcessId FROM Win32_Process WHERE Nameapp.exe) For Each proc In procs wmi.Get(Win32_Process.Handle proc.ProcessId ).Terminate Next这套方法虽然比 TaskKill 慢一点但好处是纯 WMI 调用不受命令行转义影响而且在脚本里便于镶入条件判断逻辑比如只清理特定用户启动的进程实例。4. 访问 COM 口热词背后VBS 和串口设备通信的可行路径4.1 MSComm 控件VBS 连串口设备的经典路线在热词里看到vbs 访问 com 口时我一点不意外因为做设备自动化测试的人确实常遇到这个需求门禁控制器、工业采集板、电子秤、某些老式仪器很多都走 RS232 串口而上位机工具五花八门。VBS 要访问串口最经典的路线是通过 MSComm 这个 ActiveX 控件。MSComm 控件在较老的 Windows 系统里随系统附带如果环境里没有注册先手动注册一下regsvr32 C:\Windows\SysWOW64\mscomm32.ocx注意这里我特意写了 SysWOW64是因为在 64 位系统里32 位的 MSComm 控件需要放在 32 位子系统目录下注册如果直接去 System32 里找往往找不到。注册之后VBS 里就能这样创建对象Set com CreateObject(MSComm.MSComm)不同系统里控件的 ProgID 可能略有差异有的环境要写MSCommLib.MSComm。稳妥的做法是注册后顺手用注册表编辑器看一下 HKEY_CLASSES_ROOT 里实际登记的 ProgID以实际为准。4.2 一个可复用的串口读写小模板下面给出我实际跑过的最小可用模板实现了打开串口、发送指令、读取返回、关闭串口四个步骤Set com CreateObject(MSComm.MSComm) 串口号COM3波特率 9600无校验8 数据位1 停止位 com.CommPort 3 com.Settings 9600,N,8,1 com.InputMode 0 0 表示文本模式1 表示二进制模式 com.RThreshold 1 收到 1 个字符就触发接收事件 com.SThreshold 0 com.PortOpen True If com.PortOpen Then 发送一条 AT 指令示例 com.Output AT vbCr 等待设备响应轮询缓冲区字节数 WScript.Sleep 500 If com.InBufferCount 0 Then response com.Input WScript.Echo 收到设备数据 response End If com.PortOpen False End If这段代码需要特别注意的是 Input 属性的使用时机必须先检查 InBufferCount 不为 0再调 Input 读取否则读不到数据甚至可能报错。Input 读取后缓冲区会自动清空所以不要想着再读一次试试。4.3 常见坑输入模式、缓冲区、OnComm 事件响应不了怎么办坑一InputMode 决定 Input 返回的数据类型。文本模式下返回字符串二进制模式返回字节数组。如果设备回传的是二进制帧必须用 InputMode 1然后对字节数组逐字节解析否则中文、控制字符都会被转义打乱。坑二VBS 不是事件驱动的宿主环境。MSComm 控件的 OnComm 事件在 VB6、Delphi 里能自动触发但在 VBS 脚本里没有办法像那样响应事件只能靠轮询 InBufferCount 来判断是否有数据到达。所以在 VBS 里做串口通信本质上就是一个发指令—延时—查缓冲—读数据的循环千万别试图在脚本里挂事件回调。坑三串口号大于 9 的时候CommPort 必须带上端口名的前缀写法例如 COM10否则控件不识别。这个属于老控件对端口枚举的兼容性历史遗留问题碰到就打这个补丁。坑四regsvr32 注册失败时先确认下载的 mscomm32.ocx 位数是否和脚本宿主一致。64 位系统上默认 WScript.exe 跑在 64 位上下文去调用 32 位控件会有兼容问题必要时用 32 位版本的 WScript 或 cscript 来执行脚本。5. .vbs 文件扩展服务文件关联、默认宿主与开机自启的实操5.1 wscript 与 cscript两种宿主决定你的脚本怎么跑.vbs 文件本身只是文本真正执行它的是 Windows 自带的两个脚本宿主WScript.exe 和 Cscript.exe。双击 .vbs 默认走 WScript.exe此时所有弹出窗口、WScript.Echo 输出会以对话框形式显示适合纯交互式脚本。但如果你需要把脚本输出重定向、在命令行窗口里看日志WScript 就很不方便了。这时候用 cscript 加 //nologo 参数cscript //nologo test.vbsCscript 会把 WScript.Echo 的内容直接打到控制台标准输出配合管道可以继续交给 findstr、more 等命令进一步处理。我自己写需要排障的脚本时一律用 cscript 跑避免一个弹窗一个点关。5.2 文件关联和发送到让 vbs 脚本像普通程序一样方便启停有人问 .vbs 文件扩展服务的含义其实核心就是文件关联。正常情况下 Windows 已经内置了 .vbs 到 VBSFile 文件类型的关联但如果你想指定用 cscript 作为默认执行器可以这样手动建立assoc .vbsVBSFile ftype VBSFile%SystemRoot%\System32\Cscript.exe //nologo %1 %*或者直接在注册表里定位到这些键值HKEY_CLASSES_ROOT.vbsHKEY_CLASSES_ROOT\VBSFile\Shell\Open\Command注册表的值写的就是默认执行的命令行模板。这个思路可以在需要批量部署脚本宿主的机器上快速统一环境。另一个很实用的扩展服务是发送到。把某个 vbs 脚本的快捷方式放进当前用户的 SendTo 目录就能在资源管理器里右键任意文件通过发送到直接交给脚本处理。%APPDATA%\Microsoft\Windows\SendTo比如我放了一个转码.vbs的快捷方式进去选中一批视频文件右键发送到转码脚本就会逐个处理。这个玩法本质上没有改 .vbs 的文件关联但把脚本挂进了系统右键菜单体系使用体验和原生程序几乎没区别。5.3 开机自启的几种挂法想让 VBS 脚本开机自动执行我最常用的有两种挂法。一是启动文件夹。把脚本或其快捷方式放到以下任一目录即可%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp第一种只对当前用户生效第二种对全部用户生效但需要写入 ProgramData 的权限实际部署时要留意。二是注册表 Run 键这是服务型脚本更常见的挂载点HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run用 VBS 自己写自己也很有意思Set shell CreateObject(WScript.Shell) shell.RegWrite HKCU\Software\Microsoft\Windows\CurrentVersion\Run\MyTask, _ wscript.exe D:\scripts\monitor.vbs, REG_SZ注意引号嵌套路径中带空格时外部必须用双引号把整条命令行包起来内部路径再用一对双引号保护。类似前面 Run 方法处理路径空格的规则这里也要保持一致。最后再说一个实战小技巧如果要通过 .vbs 脚本启动外部程序并且同时完成文件打开权限修复、注册缺失组件、等待程序退出后清理日志这一整套动作我一般会把它拆成几个独立的小 vbs再写一个主控 vbs 按顺序调用每个小脚本的职责单一排障时能精确定位到哪一步出了问题。VBS 这东西功能上限不算高但把组合拳打好在日常系统管理里完全够用。