返回

Windows批处理:告别硬编码,交互式选择COM端口

windows

Windows 批处理脚本:交互式选择 COM 端口

搞硬件或者跑一些老程序的时候,常常要和串口(COM Port)打交道。要是程序启动时需要指定用哪个串口,写死在脚本里就太不灵活了,设备一换或者插拔顺序变了,端口号可能就变了。所以,写一个批处理脚本,让它能自动检测可用的 COM 端口,列出来让用户选,这就方便多了。

假设你有个启动程序的老批处理脚本,里面有个硬编码的 COM 端口号参数,像这样:

my_program.exe --port COM5 --baud 9600

现在目标是改造它,让脚本运行时动态显示可用的 COM 端口列表,用户输入选择后,再把选中的端口号(比如 5)传给 my_program.exe

问题在哪儿?

直接用系统命令获取 COM 端口信息,拿到的数据不是直接能用的列表。比如,用 WMI (Windows Management Instrumentation) 命令:

wmic path Win32_SerialPort get DeviceID,Caption /format:list

输出可能是这样的:

Caption=USB Serial Port
DeviceID=COM5

Caption=Communications Port
DeviceID=COM1

Caption=Communications Port
DeviceID=COM2

Caption=Realtek RealManage COM1
DeviceID=COMRealtek RealManage COM1

Caption=Realtek RealManage COM2
DeviceID=COMRealtek RealManage COM2

这输出有几个问题:

  1. 格式:键=值 的形式,而且每个端口的信息分散在多行。
  2. 顺序: 不一定按 COM 端口号排序。
  3. 混杂: 可能包含一些虚拟端口或者看起来重复的端口(比如例子里的 Realtek RealManage COM)。
  4. 需要提取: 我们需要的是 COM 后面的那个 数字,而不是 COMx 这个字符串。

所以,需要写点脚本逻辑来处理这些原始数据,整理成用户友好的选择列表,并能处理用户的输入。

解决方案:编写交互式选择脚本

下面是一个批处理脚本,它能实现我们想要的功能:检测、过滤、显示、选择、验证并提取端口号。

核心思路:

  1. 使用 wmic 命令获取原始端口信息。
  2. 使用 for /f 循环逐行解析 wmic 的输出。
  3. 在循环中识别 DeviceIDCaption 行。
  4. 当获取到一个完整的端口信息(DeviceIDCaption 都有了)时,进行判断:
    • 检查 DeviceID 是否是我们想要的格式(比如以 COM 开头,后面跟数字)。
    • (可选)根据 CaptionDeviceID 过滤掉不需要的端口。
  5. 将有效的端口信息存起来,并为其分配一个选择序号。
  6. 显示带有序号的端口列表给用户。
  7. 使用 set /p 获取用户输入。
  8. 验证用户输入是否是有效的序号。如果无效,重新提示输入。
  9. 如果输入有效,根据序号找到对应的 DeviceID (如 COM5)。
  10. DeviceID 中提取出数字(5),存入变量。
  11. 脚本结束,后续可以使用这个存好的端口号变量。

代码示例:

@echo off
setlocal enabledelayedexpansion

echo Detecting available COM ports...

set _portCount=0
set "_device_id="
set "_caption="

REM 清理旧的选择变量 (可选, 良好习惯)
for /L %%i in (1 1 99) do (
    set "_selCOM[%%i]="
    set "_selDesc[%%i]="
)

REM 使用 wmic 获取端口信息, 并用 for /f 解析
for /f "tokens=1,* delims==" %%a in ('wmic path Win32_SerialPort get DeviceID^,Caption /format:list ^| findstr /r /c:"^DeviceID=" /c:"^Caption="') do (
    set "_line=%%a=%%b"
    REM 处理键值对前的空格 (wmic 输出有时会有)
    for /f "tokens=1,* delims==" %%x in ("!_line!") do (
        set "_key=%%x"
        set "_value=%%y"
        REM 去掉键名可能带的尾随空格
        for /l %%z in (1 1 10) do if "!_key:~-1!"==" " set "_key=!_key:~0,-1!"

        if /i "!_key!"=="DeviceID" set "_device_id=!_value!"
        if /i "!_key!"=="Caption" set "_caption=!_value!"
    )

    REM 当 DeviceID 和 Caption 都获取到后, 进行处理
    if defined _device_id if defined _caption (
        REM --- 端口过滤逻辑 ---
        REM 1. 检查 DeviceID 是否以 COM 开头且后面是数字
        echo "!_device_id!" | findstr /r /b /c:"^COM[0-9][0-9]*
@echo off
setlocal enabledelayedexpansion

echo Detecting available COM ports...

set _portCount=0
set "_device_id="
set "_caption="

REM 清理旧的选择变量 (可选, 良好习惯)
for /L %%i in (1 1 99) do (
    set "_selCOM[%%i]="
    set "_selDesc[%%i]="
)

REM 使用 wmic 获取端口信息, 并用 for /f 解析
for /f "tokens=1,* delims==" %%a in ('wmic path Win32_SerialPort get DeviceID^,Caption /format:list ^| findstr /r /c:"^DeviceID=" /c:"^Caption="') do (
    set "_line=%%a=%%b"
    REM 处理键值对前的空格 (wmic 输出有时会有)
    for /f "tokens=1,* delims==" %%x in ("!_line!") do (
        set "_key=%%x"
        set "_value=%%y"
        REM 去掉键名可能带的尾随空格
        for /l %%z in (1 1 10) do if "!_key:~-1!"==" " set "_key=!_key:~0,-1!"

        if /i "!_key!"=="DeviceID" set "_device_id=!_value!"
        if /i "!_key!"=="Caption" set "_caption=!_value!"
    )

    REM 当 DeviceID 和 Caption 都获取到后, 进行处理
    if defined _device_id if defined _caption (
        REM --- 端口过滤逻辑 ---
        REM 1. 检查 DeviceID 是否以 COM 开头且后面是数字
        echo "!_device_id!" | findstr /r /b /c:"^COM[0-9][0-9]*$" > nul
        if !errorlevel! equ 0 (
            REM 2. (可选) 进一步过滤: 比如排除包含特定文字的 Caption
            echo "!_caption!" | findstr /i /c:"Virtual" > nul
            if !errorlevel! neq 0 (
                REM --- 过滤通过, 存储端口信息 ---
                set /a _portCount+=1
                set "_selCOM[!_portCount!]=!_device_id!"
                set "_selDesc[!_portCount!]=!_caption!"
                REM echo Found port: !_device_id! - !_caption! (Debug)
            )
        )

        REM 处理完一组信息后, 重置变量以接收下一组
        set "_device_id="
        set "_caption="
    )
)

REM --- 显示选择列表 ---
if %_portCount% equ 0 (
    echo No suitable COM ports found. Exiting.
    pause
    exit /b 1
)

echo.
echo Please select what COM port to use:
for /l %%i in (1 1 %_portCount%) do (
    echo   %%i - !_selCOM[%%i]! = !_selDesc[%%i]!
)
echo or type 'exit' to quit.
echo.

:askChoice
set /p "_choice=Enter your choice (1-%_portCount% or exit): "

REM 转换为小写方便比较
set "_choiceLower=!_choice!"
if /i "!_choiceLower!"=="exit" (
    echo User chose to exit.
    exit /b 1
)

REM --- 输入验证 ---
REM 1. 检查是否是数字
set "_numCheck="
set /a _numCheck=!_choice! 2>nul
if not defined _numCheck (
    echo Invalid input. Please enter a number between 1 and %_portCount% or 'exit'.
    goto askChoice
)
REM 2. 检查数字范围
if !_choice! LSS 1 (
    echo Invalid input. Number too small. Please enter a number between 1 and %_portCount% or 'exit'.
    goto askChoice
)
if !_choice! GTR %_portCount% (
    echo Invalid input. Number too large. Please enter a number between 1 and %_portCount% or 'exit'.
    goto askChoice
)

REM --- 输入有效, 提取端口号 ---
set "_selectedCOM=!_selCOM[%_choice%]!"
REM 提取 COM 后面的数字
set "selectedComPortNumber=!_selectedCOM:~3!"

echo.
echo You selected: Port !_selectedCOM! (Number: %selectedComPortNumber%)
echo Description: !_selDesc[%_choice%]!
echo.

REM --- 后续操作 ---
echo Now you can use the variable 'selectedComPortNumber' which holds the value: %selectedComPortNumber%
REM 示例: 启动程序
REM my_program.exe --port %selectedComPortNumber% --some-other-param value

REM 清理并退出 (根据需要可以保留 selectedComPortNumber 变量)
endlocal & set "selectedComPortNumber=%selectedComPortNumber%"

echo Script finished.
pause
exit /b 0
quot; > nul if !errorlevel! equ 0 ( REM 2. (可选) 进一步过滤: 比如排除包含特定文字的 Caption echo "!_caption!" | findstr /i /c:"Virtual" > nul if !errorlevel! neq 0 ( REM --- 过滤通过, 存储端口信息 --- set /a _portCount+=1 set "_selCOM[!_portCount!]=!_device_id!" set "_selDesc[!_portCount!]=!_caption!" REM echo Found port: !_device_id! - !_caption! (Debug) ) ) REM 处理完一组信息后, 重置变量以接收下一组 set "_device_id=" set "_caption=" ) ) REM --- 显示选择列表 --- if %_portCount% equ 0 ( echo No suitable COM ports found. Exiting. pause exit /b 1 ) echo. echo Please select what COM port to use: for /l %%i in (1 1 %_portCount%) do ( echo %%i - !_selCOM[%%i]! = !_selDesc[%%i]! ) echo or type 'exit' to quit. echo. :askChoice set /p "_choice=Enter your choice (1-%_portCount% or exit): " REM 转换为小写方便比较 set "_choiceLower=!_choice!" if /i "!_choiceLower!"=="exit" ( echo User chose to exit. exit /b 1 ) REM --- 输入验证 --- REM 1. 检查是否是数字 set "_numCheck=" set /a _numCheck=!_choice! 2>nul if not defined _numCheck ( echo Invalid input. Please enter a number between 1 and %_portCount% or 'exit'. goto askChoice ) REM 2. 检查数字范围 if !_choice! LSS 1 ( echo Invalid input. Number too small. Please enter a number between 1 and %_portCount% or 'exit'. goto askChoice ) if !_choice! GTR %_portCount% ( echo Invalid input. Number too large. Please enter a number between 1 and %_portCount% or 'exit'. goto askChoice ) REM --- 输入有效, 提取端口号 --- set "_selectedCOM=!_selCOM[%_choice%]!" REM 提取 COM 后面的数字 set "selectedComPortNumber=!_selectedCOM:~3!" echo. echo You selected: Port !_selectedCOM! (Number: %selectedComPortNumber%) echo Description: !_selDesc[%_choice%]! echo. REM --- 后续操作 --- echo Now you can use the variable 'selectedComPortNumber' which holds the value: %selectedComPortNumber% REM 示例: 启动程序 REM my_program.exe --port %selectedComPortNumber% --some-other-param value REM 清理并退出 (根据需要可以保留 selectedComPortNumber 变量) endlocal & set "selectedComPortNumber=%selectedComPortNumber%" echo Script finished. pause exit /b 0

代码详解:

  1. @echo off : 关闭命令回显,让输出更干净。
  2. setlocal enabledelayedexpansion : 启用延迟环境变量扩展。这在 for 循环内部修改和读取变量时非常重要,否则你可能会读到循环开始前的值,而不是当前迭代的值。变量要用 !var! 而不是 %var% 来访问。
  3. 初始化变量 : _portCount 计数器清零,_device_id_caption 用于临时存储解析出的值。
  4. 清理旧变量 (可选) : for /L %%i in (1 1 99) do set "_selCOM[%%i]=" 这个循环是为了确保之前的运行没有留下 _selCOM_selDesc 的变量,虽然 setlocal 会在结束时清理,但在复杂脚本中这是个好习惯。
  5. wmic ... | findstr ... :
    • wmic path Win32_SerialPort get DeviceID,Caption /format:list: 获取 WMI 信息,指定输出格式为列表。
    • ^|: 管道符 | 需要用 ^ 转义,因为它在 for /f 的命令字符串里。
    • findstr /r /c:"^DeviceID=" /c:"^Caption=": 只保留以 DeviceID=Caption= 开头的行 (^ 表示行首,/r 使用正则表达式,/c:"string" 查找字符串)。这可以初步过滤掉 wmic 输出中的空行或其他无关信息。
  6. for /f "tokens=1,* delims==" %%a in ('...') do ... :
    • 这是核心解析循环。它处理 wmic 命令的每一行输出。
    • tokens=1,* delims==: 按 = 分割行。第一个部分 (=之前) 存入 %%a,其余所有部分 (=之后) 存入 %%b
    • set "_line=%%a=%%b": 重组行为后续处理,防止值里包含 = 导致问题。
    • 处理前导/尾随空格 : wmic 的输出可能不太规整,for /f 的额外嵌套和 !_key:~0,-1! 用来去除键名可能包含的意外空格。
    • if /i "!_key!"=="DeviceID" ...: 判断当前行是 DeviceID 还是 Caption,并将值存入临时变量 _device_id_caption/i 表示忽略大小写比较。
  7. 处理完整端口信息 :
    • if defined _device_id if defined _caption ...: 当 _device_id_caption 都被赋值后,意味着关于一个端口的两条关键信息都收到了。
    • 端口过滤逻辑 :
      • echo "!_device_id!" | findstr /r /b /c:"^COM[0-9][0-9]*$" > nul: 检查 _device_id 是否以 COM 开头 (/b),后面跟着一个或多个数字 ([0-9][0-9]*)。> nul 隐藏 findstr 的输出,我们只关心它的 errorlevel
      • if !errorlevel! equ 0: 如果 findstr 找到了匹配项 (errorlevel 为 0),说明 DeviceID 格式有效。
      • (可选过滤) echo "!_caption!" | findstr /i /c:"Virtual" > nul: 检查 Caption 是否包含 "Virtual" (忽略大小写)。
      • if !errorlevel! neq 0: 如果 Caption 包含 "Virtual" (errorlevel 不为 0),则认为这个端口是有效的。你可以根据需要修改这里的过滤条件,比如排除包含 "Bluetooth" 的端口等。
      • 存储端口 : 如果通过所有过滤,_portCount 加 1,然后用 !_portCount! 作为索引,把 _device_id_caption 存入类似数组的变量 _selCOM[...]_selDesc[...]
    • 重置变量 : set "_device_id="set "_caption=" 清空临时变量,准备接收下一个端口的信息。
  8. 显示选择列表 :
    • 检查 _portCount 是否大于 0。如果没有找到端口,就提示并退出。
    • for /l %%i in (1 1 %_portCount%) do ... 循环,根据存储的信息打印带序号的列表。
  9. 获取用户输入 :
    • :askChoice 是一个标签,用于无效输入时跳回这里。
    • set /p "_choice=...": 显示提示符并等待用户输入,将输入内容存入 _choice 变量。
  10. 输入验证 :
    • 检查输入是否为 'exit' (忽略大小写)。
    • set /a _numCheck=!_choice! 2>nul 尝试把输入当作数字做运算。如果输入不是纯数字,运算会失败,_numCheck 就不会被定义。2>nul 隐藏可能的错误信息。
    • 检查数字是否在有效范围 (1 到 _portCount) 内。
    • 如果任何检查失败,显示错误消息并 goto askChoice 返回提示。
  11. 提取端口号 :
    • set "_selectedCOM=!_selCOM[%_choice%]!": 根据用户选择的有效序号 _choice,从 _selCOM "数组" 中取出对应的 DeviceID (例如 COM5)。
    • set "selectedComPortNumber=!_selectedCOM:~3!": 这是关键!使用字符串截取功能。!_selectedCOM:~3! 的意思是:取变量 _selectedCOM 的值,从第 3 个字符 之后 (索引从 0 开始,所以是跳过 C, O, M 这三个字符) 开始的所有字符。对于 COM5,结果就是 5。对于 COM12,结果就是 12
  12. 显示结果和后续 :
    • 显示用户选择的完整信息和提取出的端口号。
    • 注释掉了 my_program.exe 的示例调用,你可以取消注释并修改成你的实际程序和参数。
  13. endlocal & set "selectedComPortNumber=%selectedComPortNumber%" :
    • endlocal: 结束 setlocal 创建的本地环境,恢复之前的环境变量。默认情况下,selectedComPortNumber 这样的变量会丢失。
    • & set "selectedComPortNumber=%selectedComPortNumber%": 这是一个小技巧。& 是命令连接符。在 endlocal 生效 之前,它先把本地环境中的 selectedComPortNumber 的值(用 %...% 读取) 设置回了 全局 环境(或者说是 setlocal 之前的环境)的同名变量。这样,脚本退出后,你还能在命令行或者父脚本中访问 %selectedComPortNumber% 的值。如果不需要在脚本结束后保留这个值,可以省略 & set ... 部分。
  14. pause : 暂停脚本,让用户能看到输出结果。
  15. exit /b 0 : 正常退出脚本。exit /b 只退出当前脚本,不会关闭整个 cmd 窗口,0 表示成功。

进阶使用技巧

  • 更复杂的过滤 :
    • 你可以组合 findstr 命令来实现更复杂的 "与"、"或"、"非" 逻辑。
    • 例如,排除所有包含 "Virtual" 或 "Bluetooth" 的
      echo "!_caption!" | findstr /i /c:"Virtual" /c:"Bluetooth" > nul
      if !errorlevel! neq 0 (
          REM 这个端口既不包含 Virtual 也不包含 Bluetooth
          REM ... 存储端口 ...
      )
      
    • 或者,只保留 USB 转串口设备(假设它们的都包含 "USB Serial"):
      echo "!_caption!" | findstr /i /c:"USB Serial" > nul
      if !errorlevel! equ 0 (
          REM 这是一个 USB 串口
          REM ... 存储端口 ...
      )
      
  • 错误处理 : 可以在 wmic 命令后检查 %errorlevel%,如果 wmic 执行失败(例如 WMI 服务没运行),则给出提示。
  • 替代方案: reg query : 有些情况下,也可以通过查询注册表 HKLM\HARDWARE\DEVICEMAP\SERIALCOMM 来获取 COM 端口列表。但 wmic 通常能提供更详细的信息(如),并且可能更稳定一些。reg query 的输出格式也需要不同的解析方法。
reg query HKLM\HARDWARE\DEVICEMAP\SERIALCOMM

这个命令输出类似:

HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM
    \Device\Serial0    REG_SZ    COM1
    \Device\USBSER000    REG_SZ    COM5

解析这个需要不同的 for /f 参数。

安全建议

  • 输入验证 : 脚本中已经包含了对用户输入的数字范围验证,这是个好习惯,可以防止因无效输入导致后续命令出错。增加对 'exit' 的处理也是一种基本的健壮性措施。
  • 命令执行 : 这个脚本主要运行 wmic 和内部命令,相对安全。如果你的脚本需要调用外部 .exe 文件,请确保来源可靠。
  • 权限 : wmic 读取硬件信息通常不需要管理员权限。但如果脚本要修改系统设置或写入受保护区域,则需要考虑以管理员身份运行,并谨慎操作。对于仅仅是读取 COM 端口列表并传递给另一个程序,普通用户权限通常足够。

这个脚本提供了一个灵活的方式来处理需要用户选择 COM 端口的场景,避免了硬编码带来的麻烦,增强了批处理脚本的可用性。