最近帮朋友整理一批Windows运维脚本,发现一个特别普遍的问题:脚本本身功能没毛病,但用起来是真难受。要么双击直接跑完、一条命令把所有事全干了,想单独执行其中某一步就得打开编辑器去改代码;要么是一大串参数,过两周自己都记不清哪个参数是干嘛的。后来我把这些脚本统一改成了终端里选数字的交互式菜单——双击bat文件,先是一个清爽的菜单,输入1就清理系统临时文件,输入2就切换高性能电源模式,输入3查看当前网络连接,输入错了还有提示,不用重开窗口。这套思路用了大半年,自己维护省心,朋友拿去用也基本不用教。
这篇文章就把Windows终端菜单的构建思路完整拆一遍。从最低成本、最稳妥的批处理方案讲起,到更适合复杂项目、输出更漂亮的PowerShell方案,再穿插编码乱码、按键校验、动态生成菜单、静默隐藏运行这些实操细节。内容不追求华丽,只保证每段代码你都能直接复制去改。
1. 终端菜单在Windows环境里的真实价值
1.1 脚本“能用”和“好用”之间隔着一个菜单
很多人写批处理或PowerShell脚本,默认的使用方式是“写死逻辑然后双击”。比如热词里经常有人搜“生成一段bat批处理代码,用于优化Windows系统游戏性能,包括关闭不必要的后台服务、调整电源模式为高性能、优化网络延迟、清理系统临时文件”——这种把多个功能揉在一个脚本里的需求非常典型。脚本写完,功能都有,但问题也来了:每次想只执行“清理临时文件”,就得临时注释掉其他段落;想临时加一个功能,又担心把原有流程搞坏。
交互式菜单解决的就是这个“记忆成本”和“误操作成本”。菜单把脚本里的可选项全部展示在屏幕上,用户只需要做两件事:看屏幕、按数字。不需要记命令,不需要改代码,不需要担心误执行了哪个操作。菜单在这里承担的是一个“人机接口”的职责,把脚本从“一次性自动化工具”升级成“可反复使用的工具箱”。
1.2 终端菜单最适用的几种场景
根据我自己的使用经验,下面这几类场景特别适合用终端菜单:
- 系统维护工具箱:清理临时文件、磁盘检查、网络重置、电源模式切换、关闭后台服务等,每个功能独立成项。
- 开发环境切换:启动或停止本地服务(比如Elasticsearch、Redis、MySQL、Docker容器),不同项目需要不同环境组合。
- 批量脚本选择器:一个目录下有多个独立的bat或PowerShell脚本,用菜单统一调度,避免到处找文件。
- 部署发布流程:测试环境构建、打包、上传、重启服务,用菜单提示操作者分步执行,防止漏步骤。
这些场景的共性是“选项多、每个选项独立、使用者不一定是脚本作者”。菜单一上,脚本的可交付性直接上一个台阶。
1.3 三条实现路线怎么选
在Windows里做终端菜单,主流方案有三个,各有各的适用场景:
| 方案 | 核心命令 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|---|
| 纯批处理 + choice | choice |
兼容性好,Win7到Win11都能跑,单键响应,原生支持超时 | 界面朴素,逻辑复杂后难维护 | 系统维护、快速工具箱 |
| 批处理 + set/p | set /p |
方法简单,任何Windows版本都支持 | 需要按回车,输入校验麻烦 | 临时用的小脚本 |
| PowerShell + Read-Host | Read-Host/switch |
输出漂亮,支持颜色、Unicode,逻辑清晰,易扩展 | 依赖PowerShell环境,旧系统可能没有 | 工具链复杂、需要持续迭代的脚本 |
我个人建议:日常维护脚本优先用纯批处理加choice,稳、快、无依赖;如果脚本涉及到注册表操作、服务管理、网络配置这些需要PowerShell能力的场景,或者菜单层级超过两级,直接上PowerShell,别硬用批处理去拼。判断标准就一条——这个脚本预计要长期维护吗?要长期维护,选维护成本低的那个。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 批处理菜单:choice命令和errorlevel的配合逻辑
2.1 为什么首选choice而不是set /p
很多新手写菜单,第一反应是用set /p var=让用户输入数字再回车。这个写法不是不行,但有几个硬伤:
- 用户输完必须按回车,交互多一步,感觉不干脆。
- 输入校验麻烦。用户可能输入字母、空格、多个字符,你得用一堆
if去判断。 - 没有超时机制。脚本挂在那里等输入,如果人走开了就一直等。
choice命令正好解决这几个问题。它只接受单个按键,按完立刻响应,不用回车,而且原生支持超时和默认选项。它的作用是“等待用户从指定字符集合中选择一个”,返回值是“所选字符在集合中的序号”,从1开始。
2.2 choice的核心参数速查
写菜单时最常用的choice参数就这么几个:
| 参数 | 作用 | 示例 |
|---|---|---|
/C 字符集合 |
指定可选按键 | choice /c 1234 表示接受1、2、3、4 |
/N |
不显示选项列表 | choice /n /c 1234 屏幕上不显示“是/否”那行提示 |
/M 提示文本 |
显示自定义提示 | choice /m "请选择:" |
/T 秒数 |
等待时间,需要配合/D使用 | choice /t 10 /d 4 /c 1234 等10秒 |
/D 默认选项 |
超时后默认选择第几个选项 | choice /t 10 /d 4 /c 1234 超时后选第4个 |
/CS |
区分大小写 | choice /cs /c YN N和n不一样 |
这里有一个特别容易踩的坑:/T和/D必须成对出现,只写/t 10不写/d,命令直接报错“不能使用默认选项”。而且/D后面跟的是“选项序号”,不是按键本身。比如choice /c 1234 /t 5 /d 2,意思是5秒超时后自动选择第2个选项,也就是按键“2”。
2.3 errorlevel的倒序判断陷阱
choice返回的是序号,想要根据用户按的键走不同分支,就得判断errorlevel。但批处理里有一个特别容易理解反的规则:
if errorlevel N 的真实含义是“如果errorlevel大于等于N”。
举个例子,用户按了“1”,choice返回errorlevel=1。这时候你写:
code复制if errorlevel 1 goto :one
if errorlevel 2 goto :two
第一个条件“errorlevel>=1”直接成立,后面的判断永远不会执行,菜单永远只能执行第一项。
标准写法必须倒序判断,从最大值往最小值写:
code复制if errorlevel 4 goto :four
if errorlevel 3 goto :three
if errorlevel 2 goto :two
if errorlevel 1 goto :one
这样用户按“3”时,errorlevel=3,先被if errorlevel 3接住,走到three分支。按“1”时,errorlevel=1,跳过前面的4、3、2,落到if errorlevel 1。这个顺序问题,是批处理菜单里最常见的翻车点,我见过太多人在这里调半天。
2.4 一个最小可用菜单的完整拆解
下面这个示例把前面说的知识点全串起来了,就是很多人网上搜的那种“游戏性能优化批处理”,我把功能抽象成几个菜单项:
batch复制@echo off
chcp 65001 >nul
title 系统维护工具
:menu
cls
echo ==========================================
echo 系统维护工具菜单
echo ==========================================
echo 1. 清理系统临时文件
echo 2. 切换高性能电源模式
echo 3. 查看当前网络连接状态
echo 4. 退出
echo ==========================================
choice /c 1234 /n /m "请选择操作(1-4): "
if errorlevel 4 exit /b
if errorlevel 3 goto :network
if errorlevel 2 goto :power
if errorlevel 1 goto :cleanup
:cleanup
echo 正在清理临时文件...
del /f /s /q "%TEMP%\*" >nul 2>&1
echo 临时文件清理完成。
pause
goto :menu
:power
echo 正在切换到高性能电源模式...
powercfg /setactive 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
echo 电源模式切换完成。
pause
goto :menu
:network
echo 当前活动的TCP连接前10条:
netstat -ano | findstr "ESTABLISHED" | findstr /n "^" | findstr "^[0-9]:"
powershell -NoProfile -Command "Get-NetTCPConnection -State Established | Select-Object -First 10 LocalAddress,LocalPort,RemoteAddress,RemotePort,OwningProcess | Format-Table -AutoSize"
pause
goto :menu
拆开看几个关键点:
chcp 65001 >nul:先把代码页切到UTF-8,解决中文乱码。这个下一节详细讲。:menu标签和goto :menu形成循环,每次执行完一个功能回到菜单重新显示。choice /c 1234 /n /m:/n把choice自带的“是/否”提示隐藏掉,/m显示自己的中文提示。pause是必须的,否则执行完功能提示一闪而过,菜单界面直接回到开头,用户看不清结果。- 高性能电源模式的GUID是固定的
8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,这是Windows内置的卓越性能或高性能方案,不同系统版本可能要先用powercfg /list查一下。
这套写法虽然朴素,但已经足够支撑一个日常维护工具箱了。
3. 编码与乱码:中英文菜单的第一道坎
3.1 乱码的根源:代码页和文件编码不一致
写中文菜单,第一关就是乱码。Windows的cmd窗口默认使用系统代码页,中文系统是GBK(代码页936)。如果你把bat文件保存成UTF-8编码,cmd读取文件时按照GBK去解码,中文字符就全变成了“鈥滃”这类乱码。
这就是网上“windows乱码的乱码大全”这类词能成热搜的根本原因——几乎每个写Windows脚本的人都会碰上。解决思路只有两类:要么让cmd用UTF-8解码,要么让文件用GBK编码。
3.2 方案一:chcp 65001 + UTF-8保存
这是当前最推荐的方案,尤其是你的脚本要在不同语言版本的Windows之间分发时。
batch复制@echo off
chcp 65001 >nul
title 测试菜单
echo 中文菜单测试
pause
注意几个细节:
chcp 65001 >nul必须放在@echo off之后、任何中文输出之前。如果中文文本出现在chcp之前,cmd已经按GBK把那一行读坏了。- 保存bat文件时,编码要选“UTF-8”,但这里有个分叉:带BOM还是不带BOM。实测经验是,bat文件带UTF-8 BOM有时会在第一行报“系统找不到指定的文件”,因为cmd会把BOM当成本文件名的首字符。所以稳妥做法是保存为“UTF-8无BOM”。
- 如果保存成UTF-8无BOM后,偶发中文显示异常,可以试试“UTF-8 with BOM”,但第一行必须是
@echo off。两种都试一下,以你的Windows版本实际显示为准。
3.3 方案二:GBK/ANSI编码 + 默认代码页
如果脚本只在自己机器上跑,或者只在内网的中文Windows环境里分发,那最省事的方案是:把bat文件保存为“ANSI”编码(在中文Windows里就是GBK),不在脚本里写chcp,直接用默认代码页。
这个方案的优势是零设置、零报错,cmd怎么读都对。缺点是文件拿到英文系统或繁体中文系统上就会乱成一团,通用性差。所以我一般只会在临时脚本上这么干,凡是可能要分享出去的脚本,一律UTF-8无BOM + chcp 65001。
3.4 闪退和乱码的排查清单
菜单脚本最常见的几个症状,我整理成一个清单方便排查:
| 症状 | 原因 | 处理方式 |
|---|---|---|
| 双击脚本闪一下就消失 | 脚本语法错误,或第一行编码异常 | 在cmd里运行cmd /k 脚本路径看报错;检查文件编码 |
| 中文菜单变乱码 | 文件编码和代码页不一致 | 统一用UTF-8无BOM + chcp 65001 |
| choice报“无法使用默认选项” | /T和/D没成对出现 | 补上/D参数 |
| 菜单永远只执行第一项 | errorlevel判断没有倒序 | 从高到低排列if errorlevel |
| 执行完某个功能后窗口直接关闭 | 缺少pause | 在功能分支末尾加pause |
这套排查思路我也用在批处理之外的PowerShell脚本上,逻辑是一样的:先确定编码,再检查语法,最后看变量值。
4. PowerShell版本:菜单的现代形态
4.1 为什么很多场景值得升级到PowerShell
批处理选择choice方案虽然稳,但功能复杂到一定程度,比如要处理网络配置、系统服务、注册表、或者菜单项超过两级,批处理的代码可读性和维护性会急剧下降。这时候PowerShell是更好的选择。
PowerShell菜单的核心优势:
- 原生支持Unicode,不乱码,代码用UTF-8 with BOM保存即可。
switch、do-while、函数、脚本块,写复杂逻辑比批处理舒服太多。- 彩色输出用
Write-Host -ForegroundColor一行搞定,菜单美观度完全碾压批处理。 Read-Host虽然要回车,但配合校验分支,体验也不差;想要单键响应也可以用[Console]::ReadKey()。
4.2 一个可直接运行的PowerShell菜单骨架
下面这个例子我直接贴上来了,功能是常规的系统维护菜单,但结构上你可以随意扩展:
powershell复制# 保存为 menu.ps1,右键"使用PowerShell运行"或控制台执行
$title = "系统维护工具"
$menuItems = @(
"清理系统临时文件",
"切换高性能电源模式",
"查看网络连接状态",
"退出"
)
function Show-Menu {
Clear-Host
Write-Host ("=" * 42) -ForegroundColor Cyan
Write-Host " $title" -ForegroundColor Green
Write-Host ("=" * 42) -ForegroundColor Cyan
for ($i = 0; $i -lt $menuItems.Count; $i++) {
Write-Host (" {0}. {1}" -f ($i + 1), $menuItems[$i]) -ForegroundColor Yellow
}
Write-Host ("=" * 42) -ForegroundColor Cyan
}
function Invoke-Action {
param([int]$Action)
switch ($Action) {
1 {
Write-Host "正在清理临时文件..." -ForegroundColor Gray
Remove-Item "$env:TEMP\*" -Recurse -Force -ErrorAction SilentlyContinue
Write-Host "临时文件清理完成。" -ForegroundColor Green
}
2 {
Write-Host "正在切换高性能电源模式..." -ForegroundColor Gray
powercfg /setactive 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
Write-Host "电源模式切换完成。" -ForegroundColor Green
}
3 {
Write-Host "当前TCP连接状态:" -ForegroundColor Gray
Get-NetTCPConnection -State Established |
Select-Object -First 10 LocalAddress, LocalPort, RemoteAddress, RemotePort, OwningProcess |
Format-Table -AutoSize
}
4 { return $false }
default {
Write-Warning "无效选项,请重新输入。"
Start-Sleep -Milliseconds 800
}
}
return $true
}
do {
Show-Menu
$input = Read-Host "请选择操作(1-$($menuItems.Count))"
$choice = 0
[int]::TryParse($input, [ref]$choice) | Out-Null
$continue = Invoke-Action -Action $choice
} while ($continue -ne $false)
几个细节值得说明:
$menuItems数组集中管理菜单项,Show-Menu和Invoke-Action都从数组取数,增加一个菜单项只需要在数组里加一行、在switch里加一个分支。[int]::TryParse把输入转成数字,转换失败时$choice保持0,会进入default分支提示无效选项,而不是脚本报错。Invoke-Action返回布尔值,决定是继续循环还是退出,退出条件是明确的return $false,比起在循环里用break更清晰。
4.3 输出着色与单键读取
PowerShell菜单的体验可以做得比批处理好一个档次,关键在于两个点:颜色和按键。
颜色用Write-Host -ForegroundColor控制,常用颜色有:
| 颜色 | 适用场景 |
|---|---|
| Cyan | 分隔线、标题框 |
| Green | 成功提示、当前状态 |
| Yellow | 菜单项、可选项 |
| Red | 错误提示 |
| Gray | 日志、说明文本 |
单键读取方面,Read-Host必须按回车,如果你想要批处理choice那种“按一个键立刻响应”的手感,可以用[Console]::ReadKey():
powershell复制$key = [Console]::ReadKey($true)
switch ($key.KeyChar.ToString().ToLower()) {
'1' { Invoke-Action -Action 1 }
'q' { return }
}
这里$true表示不把按键显示在屏幕上。需要注意[Console]::ReadKey()在控制台宿主下正常,在VS Code的集成终端里也基本没问题,但在某些嵌入式PowerShell环境里可能不支持。对Windows日用的菜单脚本来说,这个限制不构成问题。
4.4 批处理和PowerShell混用一个入口
实际项目里没必要非此即彼。我常用的方式是:外层用bat启动,内层跑PowerShell。
batch复制@echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0menu.ps1"
这样做的原因是双击bat文件体验最直接,而且可以用-ExecutionPolicy Bypass绕过执行策略限制,避免用户机器上因为PowerShell执行策略报错跑不起来。
5. 多级菜单与状态传递:菜单体系的扩展设计
5.1 call和goto的区别,决定了菜单怎么返回
批处理做多级菜单时,最核心的问题是“子菜单执行完怎么回上一级”。关键在于理解call和goto的区别。
goto是直接跳转,跳过去了就回不来了,想回上一级必须在子菜单末尾再goto回主菜单标签。call则是调起一个子过程/子脚本,子过程执行完用exit /b返回时,会回到call的下一行继续执行。
对应到菜单设计:
- 主菜单到子菜单,推荐用
call :submenu,子菜单可以自然返回主菜单。 - 如果只是简单的功能选项而不是子菜单,用
goto+功能标签就行。
5.2 一个二级菜单的批处理写法
batch复制@echo off
chcp 65001 >nul
:main
cls
echo 1. 系统维护
echo 2. 网络工具
echo 3. 退出
choice /c 123 /n /m "请选择: "
if errorlevel 3 exit /b
if errorlevel 2 call :netmenu
if errorlevel 1 call :sysmenu
goto :main
:sysmenu
cls
echo 1. 清理临时文件
echo 2. 磁盘检查
echo 3. 返回上级
choice /c 123 /n /m "请选择: "
if errorlevel 3 goto :eof
if errorlevel 2 goto :chkdsk
if errorlevel 1 goto :cleanup
goto :sysmenu
:cleanup
del /f /s /q "%TEMP%\*" >nul 2>&1
echo 清理完成
pause
goto :sysmenu
:chkdsk
chkdsk C:
pause
goto :sysmenu
:netmenu
cls
echo 1. 查看IP配置
echo 2. 测试网关连通性
echo 3. 返回上级
choice /c 123 /n /m "请选择: "
if errorlevel 3 goto :eof
if errorlevel 2 goto :ping
if errorlevel 1 goto :ipconfig
goto :netmenu
:ipconfig
ipconfig /all
pause
goto :netmenu
:ping
ping -n 4 192.168.1.1
pause
goto :netmenu
这个例子里,子菜单用call :netmenu进入,子菜单里选“返回上级”时goto :eof,这个:eof是批处理内置标签,表示“返回到call的下一行”。于是流程回到主菜单的goto :main,重新显示主菜单。整个跳转逻辑是闭环的。
5.3 用setlocal和临时文件传递状态
多级菜单还有一个常见需求:在子菜单里做了一些选择,返回主菜单后主菜单要知道状态。批处理的变量作用域默认是全局的,但如果你用了setlocal,局部变量会在endlocal或脚本退出时消失,子过程中用call :某标签时要注意变量作用域。
我常用的方案是:主脚本开头用setlocal enabledelayedexpansion,所有需要跨菜单传递的变量放到一个临时文件存储,子菜单切换时读取。
batch复制:choose_language
echo 1. 中文
echo 2. English
choice /c 12 /n /m "请选择: "
if errorlevel 2 set "lang=en" & echo en> "%TEMP%\menu_lang.txt"
if errorlevel 1 set "lang=zh" & echo zh> "%TEMP%\menu_lang.txt"
下次任意子菜单启动时,set /p lang=< "%TEMP%\menu_lang.txt" 就能恢复到之前的选择。这个方案虽然土,但胜在简单可靠,尤其在多层菜单、多脚本联动时特别好用。
6. 动态菜单:扫描目录、服务和应用列表生成选项
6.1 目录扫描生成菜单
有些场景下,菜单的选项不是写死的,而是依赖环境变化的。比如我一个目录下放了很多独立的运维脚本,希望菜单自动列出这些脚本,选了就执行。这就叫动态菜单。
批处理里用for /f配合dir扫描目录:
batch复制@echo off
setlocal enabledelayedexpansion
chcp 65001 >nul
set /a count=0
echo 可用的脚本列表:
echo ================================
for /f "delims=" %%i in ('dir /b /a-d "%~dp0scripts\*.bat"') do (
set /a count+=1
set "file_!count!=%%i"
echo !count!. %%i
)
echo ================================
choice /c 123456789 /n /m "请选择脚本序号: "
if errorlevel 1 (
set "selected=!file_%errorlevel%!"
echo 正在执行脚本: !selected!
call "%~dp0scripts\!selected!"
)
这个写法有几个关键点:
setlocal enabledelayedexpansion必须开,否则for循环里set /a count+=1的!count!取不到实时值。dir /b /a-d只列出文件名不列目录,"%~dp0scripts\*.bat"限定目录和扩展名。choice /c 123456789最多支持9个选项,如果脚本超过9个,需要拼更长的/c字符串。!file_%errorlevel%!这种间接引用能按序号取到文件名。注意外层%errorlevel%在if errorlevel 1的块里读取有些版本会滞后,更稳的做法是直接用变量保存错误码:set "sel=%errorlevel%",再!file_%sel%!。
6.2 延迟变量展开的坑
上面代码里!count!和!file_!count!!是延迟展开语法。新手经常在这里踩坑,最常见的问题是for循环里%count%写成了普通百分号形式,然后发现count永远是1,因为%count%在for执行之前就被整体替换成初始值了。记住一条规则:凡是在for或if块内部要动态读取的变量,必须用!变量名!,并且脚本开头要setlocal enabledelayedexpansion。
6.3 用动态菜单管理多个服务的启动和停止
这个思路放到服务管理上特别实用。比如你本地装了Elasticsearch、Redis、Docker Desktop,每次都记不住启动命令,可以写一个服务管理菜单:
batch复制@echo off
setlocal enabledelayedexpansion
chcp 65001 >nul
:menu
cls
echo 1. 启动 Elasticsearch
echo 2. 启动 Redis
echo 3. 启动 Docker Desktop
echo 4. 查看所有服务状态
echo 5. 退出
choice /c 12345 /n /m "请选择: "
if errorlevel 5 exit /b
if errorlevel 4 goto :status
if errorlevel 3 start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe"
if errorlevel 2 start "" "C:\Program Files\Redis\redis-server.exe"
if errorlevel 1 call "D:\services\elasticsearch\bin\elasticsearch.bat"
pause
goto :menu
:status
tasklist | findstr /i "java redis docker"
pause
goto :menu
这样的菜单把一堆零散命令整合成一个入口,省去记忆成本。类似的场景还能用到“开启关闭开发环境”、“切换多套配置文件”上,逻辑都是一样的:先列选项,再映射命令。
7. 静默运行、参数化入口与自动化调度
7.1 用VBS隐藏窗口启动菜单脚本
有些菜单脚本需要放到计划任务里跑,不想弹出一个黑框打扰用户;或者开机启动一个维护菜单,但不希望用户直接被终端界面砸在脸上。批处理本身没法隐藏自己的窗口,但可以通过VBScript启动器来实现。
vbs复制Set WshShell = CreateObject("WScript.Shell")
WshShell.Run "cmd /c D:\scripts\menu.bat", 0, False
.Run的第二个参数0表示窗口隐藏,第三个参数False表示不等待脚本执行完就返回。把这段保存成run_menu.vbs,双击VBS就能让menu.bat静默启动。
这一招也回应了很多人搜的“windows实现cmd静默运行”,核心就是这个参数。注意:窗口隐藏不等于权限提升,如果脚本本身需要管理员权限,还是得配合右键“以管理员身份运行”或任务计划的“最高权限”选项。
7.2 参数化入口:同一套脚本支持交互和静默两种模式
比单纯隐藏窗口更高级的玩法是参数化。给菜单脚本加一个参数,凡是从计划任务或自动化工具调起来的时候,直接走静默模式,不再显示菜单、不再等待输入。
批处理的参数用%1判断:
batch复制@echo off
chcp 65001 >nul
if /i "%~1"=="-silent" goto :silent
:menu
cls
echo 1. 清理临时文件
echo 2. 切换高性能电源模式
echo 3. 退出
choice /c 123 /n /m "请选择: "
if errorlevel 3 exit /b
if errorlevel 2 goto :power
if errorlevel 1 goto :cleanup
:silent
echo 进入静默模式,执行默认维护...
call :cleanup
call :power
exit /b
:cleanup
del /f /s /q "%TEMP%\*" >nul 2>&1
exit /b
:power
powercfg /setactive 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
exit /b
call :cleanup这种方式把功能封装成子过程,交互模式调一次,静默模式调一次,代码不重复。脚本就可以同时被手动双击使用和计划任务调度。
7.3 计划任务和日志记录
配合静默模式,计划任务就是很自然的下一步。创建计划任务的PowerShell命令:
powershell复制$action = New-ScheduledTaskAction -Execute "cmd.exe" -Argument '/c D:\scripts\menu.bat -silent'
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 9:00am
Register-ScheduledTask -TaskName "系统维护-周清理" -Action $action -Trigger $trigger -RunLevel Highest
自动化执行时,日志很重要。批处理里用重定向把所有输出追加到一个文件:
batch复制call :cleanup >> "%USERPROFILE%\Desktop\维护日志.txt" 2>&1
PowerShell里更推荐Start-Transcript:
powershell复制Start-Transcript -Path "$env:TEMP\menu_log.txt" -Append
# 脚本主体...
Stop-Transcript
有了日志,脚本出了问题能快速定位,不至于盲猜。
8. 实测中踩过的坑和最终优化习惯
8.1 最容易翻车的小细节
写作过程中大部分坑前面已经提到了,这里再把实测中反复遇到的、最隐蔽的几个集中说一遍:
timeout /t 3在输入被重定向时(比如从管道或计划任务启动)会立刻跳过,不会真的等3秒。需要强制延时可以换成ping -n 4 127.0.0.1 >nul,或者用PowerShell的Start-Sleep。choice /c参数里的字符集合不要加逗号或空格,/c 1,2,3会把逗号当成第4个可选按键,结果用户按逗号也能触发。- bat文件保存为UTF-8 with BOM时,部分Windows版本会把BOM当文件名首字符,第一行报错闪退。遇到双击闪退第一件事就是检查编码。
if errorlevel 1判断和%errorlevel%判断语义不同。if errorlevel 1是“大于等于1”,if "%errorlevel%"=="1"是“精确等于1”。如果设置了setlocal enabledelayedexpansion,某些块的%errorlevel%取的是初始值,需要改成!errorlevel!。- 路径里带空格但没加引号时,
call和start经常报“找不到文件”。凡涉及文件路径的变量,复制时都用双引号包住。
8.2 菜单脚本的目录组织习惯
我个人的习惯是建一个D:\scripts\目录,目录下按脚本类型分文件夹,菜单入口统一放在根目录。menu.bat只负责展示菜单和调度,真正的功能逻辑封装成独立的bat或ps1文件放在modules\子目录里。这样每个功能文件都可以单独测试、单独维护,菜单只是入口。
目录结构大致长这样:
code复制D:\scripts\
├── menu.bat
├── menu.ps1
├── modules\
│ ├── clean_temp.bat
│ ├── set_power.bat
│ └── network_info.ps1
└── logs\
└── menu_log.txt
入口菜单里用call "%~dp0modules\clean_temp.bat"调用功能脚本,%~dp0能自动取得当前bat所在目录,这样整个目录随便移动路径也不会失效。
8.3 我的几个固定习惯
最后分享几个我长期用下来的习惯,可能不适用于所有场景,但对终端菜单这种工具型脚本很友好。
第一,所有需要交互的菜单脚本,尽量在菜单里提供“退出”选项,不要用窗口右上角的叉来结束。因为脚本可能从计划任务或另一个脚本调起来,叉掉的方式不可控,exit /b能保证干净退出。
第二,菜单项不超过9个。超过9个是choice的天然上限,用PowerShell的话可以多一些,但人眼在一屏里能快速扫描的选项也就是10个以内。选项一多,就该考虑分类做二级菜单。
第三,菜单标题和分隔线用变量保存,放在脚本顶部。这样改标题只改一个地方,不用全文搜索替换。批处理里变量写起来有点啰嗦,但长期维护时很值。
第四,每次改完菜单脚本,先在cmd里跑一遍cmd /k 脚本路径,不要直接双击。双击时窗口一关,报错信息全没了,排查效率极低。有了cmd /k,窗口即使出错也会停在原地,报错一目了然。
终端菜单这件事,技术含量不算高,但它实打实把脚本从“自己用的半成品”变成了“能交付给别人的工具”。如果你手头正攒了一堆零散的bat或PowerShell脚本,花一个下午把它们收拢到一个菜单里,之后每次使用节省的时间和精力,绝对值得。
