返回
Windows批处理:告别硬编码,交互式选择COM端口
windows
2025-04-11 02:59:25
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
这输出有几个问题:
- 格式: 是
键=值
的形式,而且每个端口的信息分散在多行。 - 顺序: 不一定按 COM 端口号排序。
- 混杂: 可能包含一些虚拟端口或者看起来重复的端口(比如例子里的
Realtek RealManage COM
)。 - 需要提取: 我们需要的是
COM
后面的那个 数字,而不是COMx
这个字符串。
所以,需要写点脚本逻辑来处理这些原始数据,整理成用户友好的选择列表,并能处理用户的输入。
解决方案:编写交互式选择脚本
下面是一个批处理脚本,它能实现我们想要的功能:检测、过滤、显示、选择、验证并提取端口号。
核心思路:
- 使用
wmic
命令获取原始端口信息。 - 使用
for /f
循环逐行解析wmic
的输出。 - 在循环中识别
DeviceID
和Caption
行。 - 当获取到一个完整的端口信息(
DeviceID
和Caption
都有了)时,进行判断:- 检查
DeviceID
是否是我们想要的格式(比如以COM
开头,后面跟数字)。 - (可选)根据
Caption
或DeviceID
过滤掉不需要的端口。
- 检查
- 将有效的端口信息存起来,并为其分配一个选择序号。
- 显示带有序号的端口列表给用户。
- 使用
set /p
获取用户输入。 - 验证用户输入是否是有效的序号。如果无效,重新提示输入。
- 如果输入有效,根据序号找到对应的
DeviceID
(如COM5
)。 - 从
DeviceID
中提取出数字(5
),存入变量。 - 脚本结束,后续可以使用这个存好的端口号变量。
代码示例:
@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
代码详解:
@echo off
: 关闭命令回显,让输出更干净。setlocal enabledelayedexpansion
: 启用延迟环境变量扩展。这在for
循环内部修改和读取变量时非常重要,否则你可能会读到循环开始前的值,而不是当前迭代的值。变量要用!var!
而不是%var%
来访问。- 初始化变量 :
_portCount
计数器清零,_device_id
和_caption
用于临时存储解析出的值。 - 清理旧变量 (可选) :
for /L %%i in (1 1 99) do set "_selCOM[%%i]="
这个循环是为了确保之前的运行没有留下_selCOM
或_selDesc
的变量,虽然setlocal
会在结束时清理,但在复杂脚本中这是个好习惯。 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
输出中的空行或其他无关信息。
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
表示忽略大小写比较。
- 这是核心解析循环。它处理
- 处理完整端口信息 :
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="
清空临时变量,准备接收下一个端口的信息。
- 显示选择列表 :
- 检查
_portCount
是否大于 0。如果没有找到端口,就提示并退出。 - 用
for /l %%i in (1 1 %_portCount%) do ...
循环,根据存储的信息打印带序号的列表。
- 检查
- 获取用户输入 :
:askChoice
是一个标签,用于无效输入时跳回这里。set /p "_choice=..."
: 显示提示符并等待用户输入,将输入内容存入_choice
变量。
- 输入验证 :
- 检查输入是否为 'exit' (忽略大小写)。
- 用
set /a _numCheck=!_choice! 2>nul
尝试把输入当作数字做运算。如果输入不是纯数字,运算会失败,_numCheck
就不会被定义。2>nul
隐藏可能的错误信息。 - 检查数字是否在有效范围 (1 到
_portCount
) 内。 - 如果任何检查失败,显示错误消息并
goto askChoice
返回提示。
- 提取端口号 :
set "_selectedCOM=!_selCOM[%_choice%]!"
: 根据用户选择的有效序号_choice
,从_selCOM
"数组" 中取出对应的DeviceID
(例如COM5
)。set "selectedComPortNumber=!_selectedCOM:~3!"
: 这是关键!使用字符串截取功能。!_selectedCOM:~3!
的意思是:取变量_selectedCOM
的值,从第 3 个字符 之后 (索引从 0 开始,所以是跳过 C, O, M 这三个字符) 开始的所有字符。对于COM5
,结果就是5
。对于COM12
,结果就是12
。
- 显示结果和后续 :
- 显示用户选择的完整信息和提取出的端口号。
- 注释掉了
my_program.exe
的示例调用,你可以取消注释并修改成你的实际程序和参数。
endlocal & set "selectedComPortNumber=%selectedComPortNumber%"
:endlocal
: 结束setlocal
创建的本地环境,恢复之前的环境变量。默认情况下,selectedComPortNumber
这样的变量会丢失。& set "selectedComPortNumber=%selectedComPortNumber%"
: 这是一个小技巧。&
是命令连接符。在endlocal
生效 之前,它先把本地环境中的selectedComPortNumber
的值(用%...%
读取) 设置回了 全局 环境(或者说是setlocal
之前的环境)的同名变量。这样,脚本退出后,你还能在命令行或者父脚本中访问%selectedComPortNumber%
的值。如果不需要在脚本结束后保留这个值,可以省略& set ...
部分。
pause
: 暂停脚本,让用户能看到输出结果。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 端口的场景,避免了硬编码带来的麻烦,增强了批处理脚本的可用性。