PowerShell 技能连载 - Windows 开发环境配置即代码

适用于 PowerShell 7.0 及以上版本

在 Linux 和 macOS 世界里,”Dotfiles” 文化早已深入人心——开发者把 shell 配置、编辑器偏好、软件包清单统统放进 Git 仓库,一条命令就能在新机器上完整还原工作环境。这种”配置即代码”(Configuration as Code)的理念不仅节省时间,更重要的是保证了多台设备之间的一致性,也让灾难恢复变得轻而易举。

Windows 平台长期以来缺少类似的标准化方案。开发者通常需要手动下载安装包、逐一点击安装向导、手动配置环境变量和注册表项。整个过程既繁琐又容易遗漏。随着 Windows Package Manager(winget)的成熟和 PowerShell 7 的普及,Windows 上也可以实现与 Unix 系统媲美的自动化环境配置流程。

本文将介绍如何使用 PowerShell 结合 winget 构建一套完整的”Windows Dotfiles”方案:自动安装常用开发工具链、管理系统配置与偏好设置,以及通过 Git 仓库实现多机同步和一键恢复。

开发工具链安装脚本

第一步是将日常开发所需的工具清单化。通过 winget 的命令行接口,我们可以用 PowerShell 脚本批量安装 VS Code、Git、Node.js、Python 等常用工具,并自动处理依赖关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# DevToolkit.ps1 - 开发工具链一键安装脚本
# 定义工具清单:包名 + 可选版本约束
$tools = @(
@{ Id = 'Git.Git'; Name = 'Git'; Source = 'winget' }
@{ Id = 'Microsoft.VisualStudioCode'; Name = 'VS Code'; Source = 'winget' }
@{ Id = 'OpenJS.NodeJS.LTS'; Name = 'Node.js LTS'; Source = 'winget' }
@{ Id = 'Python.Python.3.12'; Name = 'Python 3.12'; Source = 'winget' }
@{ Id = 'Docker.DockerDesktop'; Name = 'Docker Desktop'; Source = 'winget' }
@{ Id = 'Microsoft.WindowsTerminal'; Name = 'Windows Terminal'; Source = 'winget' }
@{ Id = 'JanDeDobbeleer.OhMyPosh'; Name = 'Oh My Posh'; Source = 'winget' }
)

function Install-DevTool {
param(
[Parameter(Mandatory)]
[hashtable]$Tool
)

$installed = winget list --id $Tool.Id --accept-source-agreements 2>$null
if ($installed -match $Tool.Id) {
Write-Host "[已安装] $($Tool.Name)" -ForegroundColor Green
return
}

Write-Host "[安装中] $($Tool.Name) ($($Tool.Id))" -ForegroundColor Cyan
$result = winget install --id $Tool.Id `
--accept-package-agreements `
--accept-source-agreements `
--silent 2>&1

if ($LASTEXITCODE -eq 0) {
Write-Host "[完成] $($Tool.Name) 安装成功" -ForegroundColor Green
} else {
Write-Host "[失败] $($Tool.Name): $result" -ForegroundColor Red
}
}

# 批量安装并统计结果
$stats = @{ Success = 0; Skipped = 0; Failed = 0 }

foreach ($tool in $tools) {
$before = $stats.Success + $stats.Failed
Install-DevTool -Tool $tool

$after = winget list --id $tool.Id --accept-source-agreements 2>$null
if ($after -match $tool.Id) {
if ($before -eq ($stats.Success + $stats.Failed)) {
$stats.Skipped++
} else {
$stats.Success++
}
} else {
$stats.Failed++
}
}

Write-Host "`n--- 安装报告 ---" -ForegroundColor Yellow
Write-Host "新安装: $($stats.Success) | 已存在: $($stats.Skipped) | 失败: $($stats.Failed)"

上面的脚本会逐个检查每个工具是否已经安装,避免重复安装,并给出清晰的彩色输出和最终统计报告。你可以根据个人需求增删 $tools 数组中的条目。

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[已安装] Git
[已安装] VS Code
[安装中] Node.js LTS (OpenJS.NodeJS.LTS)
[完成] Node.js LTS 安装成功
[安装中] Python 3.12 (Python.Python.3.12)
[完成] Python 3.12 安装成功
[已安装] Docker Desktop
[安装中] Windows Terminal (Microsoft.WindowsTerminal)
[完成] Windows Terminal 安装成功
[安装中] Oh My Posh (JanDeDobbeleer.OhMyPosh)
[完成] Oh My Posh 安装成功

--- 安装报告 ---
新安装: 4 | 已存在: 3 | 失败: 0

系统配置与偏好设置

安装完工具只是第一步,更关键的是将各项配置也纳入版本管理。下面的脚本演示了如何通过 PowerShell 管理注册表项、PowerShell Profile 文件和环境变量,把这些偏好设置也变成可追踪、可复现的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# SystemConfig.ps1 - 系统配置与偏好设置脚本
# 定义 Dotfiles 仓库路径
$DotfilesRoot = Join-Path $HOME 'dotfiles'
$BackupDir = Join-Path $DotfilesRoot 'backups'

# 创建备份目录
if (-not (Test-Path $BackupDir)) {
New-Item -Path $BackupDir -ItemType Directory -Force | Out-Null
}

# --- 1. Windows 注册表配置 ---
$regSettings = @{
# 显示文件扩展名
'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced\HideFileExt' = 0
# 显示隐藏文件
'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced\Hidden' = 1
# 关闭 Bing 搜索
'HKCU:\Software\Microsoft\Windows\CurrentVersion\Search\BingSearchEnabled' = 0
}

foreach ($entry in $regSettings.GetEnumerator()) {
$path = Split-Path $entry.Key
$name = Split-Path $entry.Key -Leaf
if (-not (Test-Path $path)) {
New-Item -Path $path -Force | Out-Null
}
Set-ItemProperty -Path $path -Name $name -Value $entry.Value -Type DWord
Write-Host "[注册表] $name = $($entry.Value)" -ForegroundColor Cyan
}

# --- 2. PowerShell Profile 管理 ---
$profileContent = @'
# ===== PowerShell Profile - 由 dotfiles 管理 =====
# Oh My Posh 主题
oh-my-posh init pwsh --config "$env:POSH_THEMES_PATH\jandebuhr.omp.json" | Invoke-Expression

# 常用别名
Set-Alias -Name ll -Value Get-ChildItem
Set-Alias -Name which -Value Get-Command

# 自定义函数:快速进入项目目录
function proj { Set-Location "$env:USERPROFILE\Projects\$args" }

# PSReadLine 配置
Set-PSReadLineOption -PredictiveSource History
Set-PSReadLineOption -EditMode Windows
'@

$profileDir = Split-Path $PROFILE
if (-not (Test-Path $profileDir)) {
New-Item -Path $profileDir -ItemType Directory -Force | Out-Null
}

# 备份现有 Profile
if (Test-Path $PROFILE) {
$backupFile = Join-Path $BackupDir 'Microsoft.PowerShell_profile.ps1.bak'
Copy-Item $PROFILE $backupFile -Force
Write-Host "[备份] Profile 已备份到 $backupFile" -ForegroundColor Yellow
}

Set-Content -Path $PROFILE -Value $profileContent -Encoding UTF8
Write-Host "[Profile] 已写入 $PROFILE" -ForegroundColor Green

# --- 3. 环境变量设置 ---
$envConfig = @{
'DOTFILES_ROOT' = $DotfilesRoot
'PROJECTS_HOME' = Join-Path $HOME 'Projects'
'EDITOR' = 'code'
}

foreach ($entry in $envConfig.GetEnumerator()) {
[Environment]::SetEnvironmentVariable(
$entry.Key, $entry.Value, 'User'
)
Write-Host "[环境变量] $($entry.Key) = $($entry.Value)" -ForegroundColor Cyan
}

Write-Host "`n[完成] 系统配置已全部应用,重启终端生效" -ForegroundColor Green

这段脚本把注册表修改、Profile 文件生成和环境变量设置集中在一起。每次执行都会自动备份旧配置,保证操作可回滚。将这段脚本放入 dotfiles 仓库后,只要 git pull 再执行一次就能在新机器上还原所有偏好。

执行结果示例:

1
2
3
4
5
6
7
8
9
10
[注册表] HideFileExt = 0
[注册表] Hidden = 1
[注册表] BingSearchEnabled = 0
[备份] Profile 已备份到 C:\Users\dev\dotfiles\backups\Microsoft.PowerShell_profile.ps1.bak
[Profile] 已写入 C:\Users\dev\Documents\PowerShell\Microsoft.PowerShell_profile.ps1
[环境变量] DOTFILES_ROOT = C:\Users\dev\dotfiles
[环境变量] PROJECTS_HOME = C:\Users\dev\Projects
[环境变量] EDITOR = code

[完成] 系统配置已全部应用,重启终端生效

配置恢复与多机同步

前两个脚本解决了安装和配置的问题,但真正的价值在于多机同步。下面的脚本实现了配置导出和一键恢复功能,配合 Git 仓库可以在任何 Windows 机器上快速还原完整开发环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# SyncDotfiles.ps1 - 配置导出、同步与恢复
# Dotfiles 仓库路径
$DotfilesRoot = Join-Path $HOME 'dotfiles'
$ExportsDir = Join-Path $DotfilesRoot 'exports'

function Export-CurrentConfig {
<# 将当前环境导出为可追踪的清单文件 #>

if (-not (Test-Path $ExportsDir)) {
New-Item -Path $ExportsDir -ItemType Directory -Force | Out-Null
}

# 导出 winget 已安装软件列表
Write-Host "[导出] 正在生成 winget 软件清单..." -ForegroundColor Cyan
winget export --output (Join-Path $ExportsDir 'winget-packages.json') `
--accept-source-agreements 2>$null

# 同时生成可读的文本版本
winget list --accept-source-agreements |
Out-File (Join-Path $ExportsDir 'winget-list.txt') -Encoding UTF8

# 导出环境变量
$userEnv = [Environment]::GetEnvironmentVariables('User') |
GetEnumerator() | Sort-Object Name
$userEnv | ConvertTo-Json |
Out-File (Join-Path $ExportsDir 'user-env.json') -Encoding UTF8

# 导出 PowerShell 模块列表
Get-Module -ListAvailable |
Select-Object Name, Version, ModuleBase |
Sort-Object Name |
ConvertTo-Json |
Out-File (Join-Path $ExportsDir 'ps-modules.json') -Encoding UTF8

# 导出 Profile 文件
if (Test-Path $PROFILE) {
Copy-Item $PROFILE (Join-Path $ExportsDir 'profile.ps1') -Force
}

Write-Host "[完成] 配置已导出到 $ExportsDir" -ForegroundColor Green
}

function Restore-DevEnvironment {
<# 从 dotfiles 仓库一键恢复开发环境 #>

# 1. 从 winget 清单批量安装
$manifestPath = Join-Path $ExportsDir 'winget-packages.json'
if (Test-Path $manifestPath) {
Write-Host "[恢复] 从 winget 清单安装软件..." -ForegroundColor Cyan
winget import --import-file $manifestPath `
--accept-package-agreements `
--accept-source-agreements
}

# 2. 恢复 Profile
$profileBackup = Join-Path $ExportsDir 'profile.ps1'
if (Test-Path $profileBackup) {
$profileDir = Split-Path $PROFILE
if (-not (Test-Path $profileDir)) {
New-Item -Path $profileDir -ItemType Directory -Force | Out-Null
}
Copy-Item $profileBackup $PROFILE -Force
Write-Host "[恢复] PowerShell Profile 已还原" -ForegroundColor Green
}

# 3. 恢复环境变量
$envPath = Join-Path $ExportsDir 'user-env.json'
if (Test-Path $envPath) {
$envVars = Get-Content $envPath | ConvertFrom-Json
foreach ($prop in $envVars.PSObject.Properties) {
[Environment]::SetEnvironmentVariable(
$prop.Name, $prop.Value.ToString(), 'User'
)
}
Write-Host "[恢复] 环境变量已还原" -ForegroundColor Green
}

# 4. 安装 PowerShell 模块
$requiredModules = @('PSReadLine', 'Terminal-Icons', 'z')
foreach ($mod in $requiredModules) {
if (-not (Get-Module -ListAvailable -Name $mod)) {
Install-Module -Name $mod -Force -Scope CurrentUser
Write-Host "[模块] 已安装 $mod" -ForegroundColor Cyan
}
}

Write-Host "`n[完成] 开发环境已完整恢复,请重启终端" -ForegroundColor Green
}

# 使用方式:
# 导出: . .\SyncDotfiles.ps1; Export-CurrentConfig
# 恢复: . .\SyncDotfiles.ps1; Restore-DevEnvironment

这个脚本提供了两个核心函数:Export-CurrentConfig 将当前环境的所有关键配置导出为 JSON 清单文件,Restore-DevEnvironment 则从这些清单文件一键还原。配合 Git 仓库,你在任何 Windows 机器上只需 git clone 你的 dotfiles 仓库,然后运行恢复函数即可。

执行结果示例(导出模式):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[导出] 正在生成 winget 软件清单...
[完成] 配置已导出到 C:\Users\dev\dotfiles\exports

> Get-ChildItem C:\Users\dev\dotfiles\exports

Directory: C:\Users\dev\dotfiles\exports

Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2026/4/15 10:30 24576 winget-packages.json
-a--- 2026/4/15 10:30 8192 winget-list.txt
-a--- 2026/4/15 10:30 1024 user-env.json
-a--- 2026/4/15 10:30 3072 ps-modules.json
-a--- 2026/4/15 10:30 2048 profile.ps1

执行结果示例(恢复模式):

1
2
3
4
5
6
7
8
9
[恢复] 从 winget 清单安装软件...
Found 12 packages in the import file.
已安装 7 个包, 已跳过 5 个包。
[恢复] PowerShell Profile 已还原
[恢复] 环境变量已还原
[模块] 已安装 Terminal-Icons
[模块] 已安装 z

[完成] 开发环境已完整恢复,请重启终端

注意事项

  1. winget 前置条件:winget 随 Windows 11 和 Windows 10 (1809+) 的 App Installer 分发。如果系统没有 winget,需要先从 Microsoft Store 安装”应用安装程序”,或在 GitHub 的 microsoft/winget-cli 仓库手动下载 MSIX 包。

  2. 管理员权限:部分软件(如 Docker Desktop、某些注册表项的修改)需要以管理员身份运行 PowerShell。建议在脚本开头加入 #Requires -RunAsAdministrator 声明,或在启动时检测权限并提示用户提权。

  3. 网络与代理:winget 和 PowerShell Gallery 在国内网络环境下可能较慢。建议提前配置代理:$env:HTTPS_PROXY = 'http://127.0.0.1:7890',或将 NuGet 源替换为国内镜像。

  4. 版本锁定:winget-packages.json 导出的清单会锁定具体版本号。如果希望在恢复时始终获取最新版,可以改为从文本清单逐条 winget install 而非使用 winget import,以获取最新可用版本。

  5. 敏感信息处理:dotfiles 仓库中不要存放 API Key、Token 等敏感信息。环境变量中涉及密钥的部分应使用 Windows Credential Manager 或 .env 文件管理,并在 .gitignore 中排除。

  6. 幂等性设计:所有脚本都应设计为可重复执行而不产生副作用——安装前先检测是否已存在,配置前先备份旧值。这样即使执行失败也可以安全地重新运行。

PowerShell 技能连载 - WinGet 高级包管理

适用于 PowerShell 7.0 及以上版本,Windows 10/11 内置 WinGet

从安装工具到企业级管理平台

随着 Windows Package Manager(WinGet)的持续迭代,它已经从一个简单的命令行安装工具成长为 Windows 平台软件生命周期管理的核心组件。微软在 2025 年为 WinGet 引入了 winget configurewinget export 等新能力,使其在配置即代码(Configuration as Code)方向上迈出了关键一步。对于运维团队而言,WinGet 不再只是”装软件的工具”,而是可以纳入 CI/CD 流水线的基础设施自动化组件。

在企业环境中,软件版本管理、环境一致性和更新回滚是最让人头疼的三件事。开发团队抱怨”我本地能跑但你那里不行”,安全团队要求所有软件必须及时打补丁,运维团队则担心某个更新导致大面积故障。这些需求都可以通过 PowerShell 与 WinGet 的深度集成来解决。

本文将从三个高级场景出发:软件版本审计与对比、YAML 配置驱动的声明式部署、更新自动化与安全回滚,展示如何用 PowerShell 构建企业级的 WinGet 包管理方案。

软件清单与版本审计

在多机环境中,了解每台机器安装了哪些软件、版本是否一致,是运维工作的起点。下面的脚本会扫描本机所有 WinGet 管理的软件,并与基准版本清单进行对比,快速识别版本偏差。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
function Compare-WinGetBaseline {
<#
.SYNOPSIS
将本机已安装软件与基准版本清单对比,输出偏差报告
.PARAMETER BaselinePath
基准 JSON 文件路径,包含期望的软件版本
.PARAMETER ExportCurrent
将当前已安装软件导出为基准文件(用于首次建立基准)
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$BaselinePath,
[switch]$ExportCurrent
)

# 采集当前已安装软件
$rawOutput = winget list --accept-source-agreements 2>&1
$installed = $rawOutput | Where-Object {
$_ -match '^\S' -and
$_ -notmatch '^Name\s+Id' -and
$_ -notmatch '^\d+ packages'
} | ForEach-Object {
$parts = $_ -split '\s{2,}'
if ($parts.Count -ge 3) {
[PSCustomObject]@{
Name = $parts[0].Trim()
Id = $parts[1].Trim()
Version = if ($parts.Count -ge 4) { $parts[2].Trim() } else { '未知' }
Available = if ($parts.Count -ge 4) { $parts[3].Trim() } else {
if ($parts.Count -eq 3) { $parts[2].Trim() } else { '' }
}
}
}
}

# 导出模式:将当前状态保存为基准
if ($ExportCurrent) {
$baseline = $installed | ForEach-Object {
[ordered]@{ Id = $_.Id; Version = $_.Version }
}
$baseline | ConvertTo-Json -Depth 3 | Set-Content $BaselinePath -Encoding UTF8
Write-Host "基准已导出至 $BaselinePath ($($baseline.Count) 个软件包)" `
-ForegroundColor Green
return
}

# 对比模式:加载基准并与当前状态比较
$baselineRaw = Get-Content $BaselinePath -Raw | ConvertFrom-Json
$baselineMap = @{}
foreach ($item in $baselineRaw) {
$baselineMap[$item.Id] = $item.Version
}

$results = foreach ($pkg in $installed) {
if ($baselineMap.ContainsKey($pkg.Id)) {
$expected = $baselineMap[$pkg.Id]
$status = if ($pkg.Version -eq $expected) { '匹配' }
elseif ([version]::TryParse($pkg.Version, [ref]$null) -and
[version]::TryParse($expected, [ref]$null)) {
if ([version]$pkg.Version -gt [version]$expected) { '更新' }
else { '过旧' }
} else { '不同' }
[PSCustomObject]@{
Name = $pkg.Name
Id = $pkg.Id
Current = $pkg.Version
Expected = $expected
Status = $status
}
}
}

$results | Sort-Object Status | Format-Table -AutoSize
$summary = $results | Group-Object Status | ForEach-Object {
"$($_.Name): $($_.Count)"
}
Write-Host "版本偏差汇总: $($summary -join ', ')" -ForegroundColor Cyan
}

# 首次建立基准
# Compare-WinGetBaseline -BaselinePath './baseline.json' -ExportCurrent

# 后续对比检查
# Compare-WinGetBaseline -BaselinePath './baseline.json'

上面的脚本核心逻辑是:先用 winget list 采集当前已安装软件,再将结果与基准 JSON 文件逐项对比。首次使用时通过 -ExportCurrent 参数将当前状态保存为基准文件,后续巡检时直接运行对比即可。状态分为”匹配”、”更新”、”过旧”和”不同”四种,其中”更新”表示本机版本比基准还新(可能是自行升级的未记录变更),”过旧”则是需要关注的版本偏差。

1
2
3
4
5
6
7
8
9
10
Name                Id                            Current  Expected Status
---- -- ------- -------- ------
7-Zip 7zip.7zip 24.09 24.09 匹配
Visual Studio Code Microsoft.VisualStudioCode 1.98.2 1.97.0 更新
Git Git.Git 2.47.1 2.48.0 过旧
PowerShell Microsoft.PowerShell 7.5.0 7.5.0 匹配
Node.js LTS OpenJS.NodeJS.LTS 22.14.0 22.12.0 更新
Docker Desktop Docker.DockerDesktop 4.34.0 4.35.0 过旧

版本偏差汇总: 匹配: 2, 更新: 2, 过旧: 2

YAML 配置驱动的声明式部署

在企业环境中,软件部署配置通常需要纳入版本控制,并与团队共享。YAML 格式比 JSON 更易读、支持注释,非常适合作为声明式部署配置的载体。下面的脚本读取 YAML 配置文件,确保目标机器的软件状态与声明的期望状态一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
function Invoke-WinGetDesiredState {
<#
.SYNOPSIS
读取 YAML 声明式配置,确保软件状态与期望一致
.PARAMETER ConfigPath
YAML 配置文件路径
.PARAMETER ReportOnly
仅生成偏差报告,不执行实际安装或卸载
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$ConfigPath,
[switch]$ReportOnly
)

# 读取 YAML 配置(需要 powershell-yaml 模块或手动解析)
$configContent = Get-Content $ConfigPath -Raw

# 简易 YAML 解析:提取 packages 节点下的条目
$lines = $configContent -split "`n"
$desiredPackages = @{}
$currentSection = ''

foreach ($line in $lines) {
if ($line -match '^\s*#') { continue }
if ($line -match '^packages:') {
$currentSection = 'packages'
continue
}
if ($currentSection -eq 'packages' -and
$line -match '^\s{2}(\S+):\s*$') {
$pkgId = $Matches[1]
$desiredPackages[$pkgId] = @{ State = 'present'; Version = '' }
$currentSection = "pkg_$pkgId"
}
if ($line -match '^\s{4}state:\s*(\S+)') {
$desiredPackages[$pkgId]['State'] = $Matches[1]
}
if ($line -match '^\s{4}version:\s*(\S+)') {
$desiredPackages[$pkgId]['Version'] = $Matches[1]
}
if ($line -match '^[^\s]') {
$currentSection = ''
}
}

# 获取当前已安装软件
$rawOutput = winget list --accept-source-agreements 2>&1
$installedIds = @()
$installedMap = @{}

$rawOutput | Where-Object {
$_ -match '^\S' -and
$_ -notmatch '^Name\s+Id' -and
$_ -notmatch '^\d+ packages'
} | ForEach-Object {
$parts = $_ -split '\s{2,}'
if ($parts.Count -ge 2) {
$id = $parts[1].Trim()
$installedIds += $id
$installedMap[$id] = if ($parts.Count -ge 4) {
$parts[2].Trim()
} else { '未知' }
}
}

# 计算偏差并执行收敛
$actions = @()

foreach ($id in $desiredPackages.Keys) {
$desired = $desiredPackages[$id]
if ($desired.State -eq 'present') {
if ($id -notin $installedIds) {
$actions += [PSCustomObject]@{
Action = '安装'
Id = $id
Target = $desired.Version
}
} elseif ($desired.Version -and
$installedMap[$id] -ne $desired.Version) {
$actions += [PSCustomObject]@{
Action = '版本调整'
Id = $id
Target = "$($installedMap[$id]) -> $($desired.Version)"
}
}
} elseif ($desired.State -eq 'absent' -and $id -in $installedIds) {
$actions += [PSCustomObject]@{
Action = '卸载'
Id = $id
Target = $installedMap[$id]
}
}
}

Write-Host "配置偏差分析完成,共 $($actions.Count) 项操作:" `
-ForegroundColor Cyan
$actions | Format-Table -AutoSize

if ($ReportOnly) {
Write-Host "[报告模式] 不执行实际操作" -ForegroundColor DarkYellow
return
}

foreach ($act in $actions) {
switch ($act.Action) {
'安装' {
$args = @('install', '--id', $act.Id,
'--accept-package-agreements',
'--accept-source-agreements')
if ($desiredPackages[$act.Id].Version) {
$args += @('--version', $desiredPackages[$act.Id].Version)
}
Write-Host "安装 $($act.Id)..." -ForegroundColor Yellow -NoNewline
winget @args 2>&1 | Out-Null
}
'卸载' {
Write-Host "卸载 $($act.Id)..." -ForegroundColor Yellow -NoNewline
winget uninstall --id $act.Id 2>&1 | Out-Null
}
'版本调整' {
Write-Host "调整 $($act.Id) 版本..." -ForegroundColor Yellow -NoNewline
winget install --id $act.Id --version `
$desiredPackages[$act.Id].Version `
--force --accept-package-agreements `
--accept-source-agreements 2>&1 | Out-Null
}
}
if ($LASTEXITCODE -eq 0) {
Write-Host " 完成" -ForegroundColor Green
} else {
Write-Host " 失败 (退出码: $LASTEXITCODE)" -ForegroundColor Red
}
}
Write-Host "`n状态收敛完成" -ForegroundColor Green
}

这段脚本实现了一个声明式配置引擎:读取 YAML 配置文件中声明的期望状态(presentabsent),与本机实际状态对比,计算出需要执行的操作列表(安装、卸载、版本调整),然后逐一收敛。-ReportOnly 模式下仅展示偏差报告,不执行实际操作,适合在 CI 流水线中作为审计步骤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
配置偏差分析完成,共 4 项操作:

Action Id Target
------ -- ------
安装 Microsoft.VisualStudioCode 1.98.2
安装 Docker.DockerDesktop 4.35.0
卸载 SomeApp.Legacy 2.1.0
版本调整 Git.Git 2.47.1 -> 2.48.0

安装 Microsoft.VisualStudioCode... 完成
安装 Docker.DockerDesktop... 完成
卸载 SomeApp.Legacy... 完成
调整 Git.Git 版本... 完成

状态收敛完成

更新自动化与安全回滚

自动更新是双刃剑:及时打补丁能堵住安全漏洞,但一次不兼容的更新可能让整个团队停工。下面的脚本在执行自动更新前会先创建软件快照,如果更新后出现问题,可以从快照恢复到更新前的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
function Invoke-WinGetSafeUpdate {
<#
.SYNOPSIS
安全更新:更新前快照,支持回滚
.PARAMETER SnapshotDir
快照存储目录
.PARAMETER MaxRetries
单个软件包更新失败后的最大重试次数
.PARAMETER RollbackOnFailure
任一软件更新失败时自动回滚所有已更新的软件
#>
[CmdletBinding()]
param(
[string]$SnapshotDir = (Join-Path $env:TEMP 'WinGetSnapshots'),
[int]$MaxRetries = 2,
[switch]$RollbackOnFailure
)

if (-not (Test-Path $SnapshotDir)) {
New-Item -ItemType Directory -Path $SnapshotDir | Out-Null
}

$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$snapshotPath = Join-Path $SnapshotDir "snapshot_$timestamp.json"

# 第一步:创建当前软件版本快照
Write-Host "[1/4] 正在创建软件版本快照..." -ForegroundColor Cyan
$rawOutput = winget list --accept-source-agreements 2>&1
$snapshot = $rawOutput | Where-Object {
$_ -match '^\S' -and
$_ -notmatch '^Name\s+Id' -and
$_ -notmatch '^\d+ packages'
} | ForEach-Object {
$parts = $_ -split '\s{2,}'
if ($parts.Count -ge 3) {
[ordered]@{
Name = $parts[0].Trim()
Id = $parts[1].Trim()
Version = if ($parts.Count -ge 4) {
$parts[2].Trim()
} else { '未知' }
}
}
}

$snapshot | ConvertTo-Json -Depth 3 | Set-Content $snapshotPath -Encoding UTF8
Write-Host "快照已保存: $snapshotPath ($($snapshot.Count) 个软件包)" `
-ForegroundColor Green

# 第二步:扫描可更新的软件
Write-Host "`n[2/4] 正在扫描可更新的软件..." -ForegroundColor Cyan
$upgradeOutput = winget upgrade 2>&1
$upgradable = $upgradeOutput | Where-Object {
$_ -match '^\S' -and
$_ -notmatch '^Name\s+Id' -and
$_ -notmatch '^\d+ upgrades'
} | ForEach-Object {
$parts = $_ -split '\s{2,}'
if ($parts.Count -ge 4) {
[PSCustomObject]@{
Name = $parts[0].Trim()
Id = $parts[1].Trim()
OldVer = $parts[2].Trim()
NewVer = $parts[3].Trim()
}
}
}

if (-not $upgradable) {
Write-Host "所有软件已是最新版本,无需更新" -ForegroundColor Green
return
}

Write-Host "发现 $($upgradable.Count) 个可更新软件包:" -ForegroundColor Yellow
$upgradable | Format-Table Name, Id, OldVer, NewVer -AutoSize

# 第三步:逐个执行更新并记录结果
Write-Host "`n[3/4] 开始安全更新..." -ForegroundColor Cyan
$results = @()
$failed = @()

foreach ($pkg in $upgradable) {
$success = $false
$attempt = 0

while (-not $success -and $attempt -lt $MaxRetries) {
$attempt++
Write-Host " 更新 $($pkg.Id) ($($pkg.OldVer) -> $($pkg.NewVer))" `
-ForegroundColor Yellow -NoNewline
if ($attempt -gt 1) {
Write-Host " [重试 $attempt]" -ForegroundColor DarkYellow -NoNewline
}

winget upgrade --id $pkg.Id --accept-package-agreements `
--accept-source-agreements 2>&1 | Out-Null

if ($LASTEXITCODE -eq 0) {
Write-Host " 完成" -ForegroundColor Green
$success = $true
$results += [PSCustomObject]@{
Id = $pkg.Id
OldVer = $pkg.OldVer
NewVer = $pkg.NewVer
Status = '成功'
Attempts = $attempt
}
} else {
Write-Host " 失败" -ForegroundColor Red
}
}

if (-not $success) {
$failed += $pkg
$results += [PSCustomObject]@{
Id = $pkg.Id
OldVer = $pkg.OldVer
NewVer = $pkg.NewVer
Status = '失败'
Attempts = $MaxRetries
}
}
}

# 第四步:失败时自动回滚
Write-Host "`n[4/4] 更新结果汇总:" -ForegroundColor Cyan
$results | Format-Table Id, OldVer, NewVer, Status, Attempts -AutoSize

if ($failed -and $RollbackOnFailure) {
Write-Host "`n检测到更新失败,正在回滚到快照版本..." -ForegroundColor Red
$snapshotData = Get-Content $snapshotPath -Raw | ConvertFrom-Json
$updatedIds = $results | Where-Object { $_.Status -eq '成功' } |
Select-Object -ExpandProperty Id

foreach ($id in $updatedIds) {
$entry = $snapshotData | Where-Object { $_.Id -eq $id }
if ($entry -and $entry.Version -ne '未知') {
Write-Host " 回滚 $($entry.Id) 到 $($entry.Version)..." `
-ForegroundColor Yellow -NoNewline
winget install --id $entry.Id --version $entry.Version `
--force --accept-package-agreements `
--accept-source-agreements 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Host " 完成" -ForegroundColor Green
} else {
Write-Host " 回滚失败" -ForegroundColor Red
}
}
}
Write-Host "回滚流程结束" -ForegroundColor Cyan
} elseif ($failed) {
Write-Host "`n提示: 更新失败的软件可使用以下命令手动回滚:" -ForegroundColor Yellow
Write-Host " 回滚快照文件: $snapshotPath" -ForegroundColor DarkYellow
foreach ($f in $failed) {
Write-Host " winget install --id $($f.Id) " + `
"--version $($f.OldVer) --force" -ForegroundColor DarkYellow
}
}

Write-Host "`n快照文件: $snapshotPath" -ForegroundColor DarkGray
Write-Host "可使用以下命令查看历史快照: Get-ChildItem $SnapshotDir" `
-ForegroundColor DarkGray
}

这个脚本的四步流程设计体现了”先快照、再更新、后回滚”的安全策略。第一步将当前所有软件版本写入 JSON 快照文件;第二步扫描可更新软件列表;第三步逐个执行更新,每个软件包失败后自动重试最多 MaxRetries 次;第四步如果开启了 -RollbackOnFailure,则将所有已成功更新的软件回滚到快照版本。快照文件以时间戳命名,保存在临时目录下,可用于后续手动恢复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
[1/4] 正在创建软件版本快照...
快照已保存: /tmp/WinGetSnapshots/snapshot_20260409_080000.json (48 个软件包)

[2/4] 正在扫描可更新的软件...
发现 5 个可更新软件包:

Name Id OldVer NewVer
---- -- ------ ------
7-Zip 7zip.7zip 24.09 24.10
Visual Studio Code Microsoft.VisualStudioCode 1.98.2 1.99.0
Git Git.Git 2.48.0 2.49.0
PowerShell Microsoft.PowerShell 7.5.0 7.5.1
Docker Desktop Docker.DockerDesktop 4.35.0 4.36.0

[3/4] 开始安全更新...
更新 7zip.7zip (24.09 -> 24.10) 完成
更新 Microsoft.VisualStudioCode (1.98.2 -> 1.99.0) 完成
更新 Git.Git (2.48.0 -> 2.49.0) 完成
更新 Microsoft.PowerShell (7.5.0 -> 7.5.1) 完成
更新 Docker.DockerDesktop (4.35.0 -> 4.36.0) 失败
更新 Docker.DockerDesktop (4.35.0 -> 4.36.0) [重试 1] 失败
更新 Docker.DockerDesktop (4.35.0 -> 4.36.0) [重试 2] 失败

[4/4] 更新结果汇总:

Id OldVer NewVer Status Attempts
-- ------ ------ ------ --------
7zip.7zip 24.09 24.10 成功 1
Microsoft.VisualStudioCode 1.98.2 1.99.0 成功 1
Git.Git 2.48.0 2.49.0 成功 1
Microsoft.PowerShell 7.5.0 7.5.1 成功 1
Docker.DockerDesktop 4.35.0 4.36.0 失败 2

提示: 更新失败的软件可使用以下命令手动回滚:
回滚快照文件: /tmp/WinGetSnapshots/snapshot_20260409_080000.json
winget install --id Docker.DockerDesktop --version 4.35.0 --force

快照文件: /tmp/WinGetSnapshots/snapshot_20260409_080000.json
可使用以下命令查看历史快照: Get-ChildItem /tmp/WinGetSnapshots

注意事项

  • 版本号解析:部分软件使用非标准版本号格式(如 1.0.0-beta.3),直接用 [version] 类型转换会失败,对比时需要做容错处理。示例代码中对解析失败的版本使用了字符串直接比较
  • YAML 解析依赖:完整的生产级 YAML 解析建议安装 powershell-yaml 模块(Install-Module powershell-yaml),示例中使用了简易的行解析逻辑以避免外部依赖,但不支持复杂的嵌套结构
  • 快照存储位置:示例中快照保存在 $env:TEMP 目录,系统清理时可能被删除。生产环境建议将快照目录设置为持久化路径,如 $HOME\WinGetSnapshots,或推送到远程存储(如 S3、Azure Blob)
  • 回滚局限性winget install --version 回滚依赖软件源保留旧版本安装包,部分软件(如 Chrome、Firefox)的旧版本可能已从源中移除,导致回滚失败。关键软件建议在快照时同时下载离线安装包
  • 并发安全:多台机器同时运行大规模更新可能给内部软件源带来压力,建议在脚本中加入随机延迟(Start-Sleep -Seconds (Get-Random -Max 300))进行错峰
  • 日志与审计:建议将每次更新操作的结果追加写入日志文件(Export-CsvAdd-Content),定期归档,方便事后审计和问题追溯

PowerShell 技能连载 - WinGet 包管理自动化

适用于 PowerShell 7.0 及以上版本,需要 winget CLI

背景介绍

在企业 IT 运维中,软件安装和更新是一项高频且重复的工作。新员工入职时需要配置开发环境,安全合规要求定期更新终端上的软件版本,这些都给运维团队带来了巨大的工作量。传统的手动安装方式不仅效率低下,还容易出现版本不一致、配置遗漏等问题。

WinGet(Windows Package Manager)是微软官方推出的命令行包管理工具,从 Windows 10 1709 版本开始内置。它提供了类似 Linux 下 apt 或 yum 的软件管理体验,支持搜索、安装、更新和卸载应用程序。结合 PowerShell 的脚本能力,我们可以将 WinGet 的操作封装为可复用的自动化流程。

本文将通过三个实际场景,演示如何使用 PowerShell 调用 WinGet 实现软件搜索安装、批量部署以及自动化更新,帮助运维人员构建完整的 Windows 软件生命周期管理体系。

WinGet 基础操作

WinGet 提供了丰富的子命令来管理软件包。下面的脚本展示了最常用的三个操作:搜索软件包、安装软件以及列出已安装的软件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 搜索软件包
function Find-WinGetPackage {
param(
[Parameter(Mandatory)]
[string]$Keyword
)

$result = winget search $Keyword --accept-source-agreements 2>&1
$result
}

# 安装软件包
function Install-WinGetPackage {
param(
[Parameter(Mandatory)]
[string]$PackageId,

[switch]$Silent
)

$args = @(
'install'
'--id', $PackageId
'--accept-source-agreements'
'--accept-package-agreements'
)

if ($Silent) {
$args += '--silent'
}

Write-Host "正在安装: $PackageId" -ForegroundColor Cyan
winget @args 2>&1
}

# 列出已安装的软件
function Get-WinGetInstalled {
$output = winget list --accept-source-agreements 2>&1
$output
}

# 示例:搜索 VS Code
Find-WinGetPackage -Keyword "Visual Studio Code"

# 示例:安装 VS Code(静默模式)
Install-WinGetPackage -PackageId 'Microsoft.VisualStudioCode' -Silent

# 示例:查看已安装的软件列表
Get-WinGetInstalled

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
名称                         ID                                版本        来源
---------------------------------------------------------------------------
Visual Studio Code Microsoft.VisualStudioCode 1.96.2 winget
Visual Studio Code Insiders Microsoft.VisualStudioCode.Insiders 1.97.0 winget

正在安装: Microsoft.VisualStudioCode
已成功完成安装

名称 ID 版本
---------------------------------------------------------------------------
Visual Studio Code Microsoft.VisualStudioCode 1.96.2
Git for Windows Git.Git 2.47.1
PowerShell 7 Microsoft.PowerShell 7.4.6

通过上面的函数封装,我们可以将 WinGet 的命令行操作转化为结构化的 PowerShell 函数,便于后续在脚本中组合调用。--accept-source-agreements--accept-package-agreements 参数可以跳过交互式确认,实现无人值守安装。

批量安装与软件清单管理

在企业环境中,我们通常需要根据角色或团队批量安装一组软件。下面的脚本展示了如何通过 JSON 清单文件管理软件列表,并实现一键批量部署。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# 定义软件清单(可以保存为外部 JSON 文件)
$softwareList = @(
@{
Id = 'Microsoft.VisualStudioCode'
Name = 'Visual Studio Code'
Category = 'Editor'
}
@{
Id = 'Git.Git'
Name = 'Git for Windows'
Category = 'VCS'
}
@{
Id = 'Microsoft.PowerShell'
Name = 'PowerShell 7'
Category = 'Runtime'
}
@{
Id = 'OpenJS.NodeJS.LTS'
Name = 'Node.js LTS'
Category = 'Runtime'
}
@{
Id = 'Docker.DockerDesktop'
Name = 'Docker Desktop'
Category = 'Container'
}
) | ConvertTo-Json -Depth 3

# 将清单保存到文件
$listPath = Join-Path $HOME 'winget-software-list.json'
$softwareList | Out-File -FilePath $listPath -Encoding utf8
Write-Host "软件清单已保存到: $listPath" -ForegroundColor Green

# 从文件加载并批量安装
function Install-WinGetFromList {
param(
[Parameter(Mandatory)]
[string]$ListFile,

[string[]]$Categories
)

$packages = Get-Content $ListFile -Raw | ConvertFrom-Json

# 按类别过滤
if ($Categories) {
$packages = $packages | Where-Object { $_.Category -in $Categories }
}

$total = $packages.Count
$success = 0
$failed = 0

foreach ($pkg in $packages) {
Write-Host "`n[$($success + $failed + 1)/$total] 安装: $($pkg.Name)" -ForegroundColor Cyan

$output = winget install --id $pkg.Id `
--accept-source-agreements `
--accept-package-agreements `
--silent 2>&1

$lastLine = ($output | Select-Object -Last 1).ToString()
if ($lastLine -match '成功|successfully|已安装') {
Write-Host " -> 成功" -ForegroundColor Green
$success++
} else {
Write-Host " -> 失败: $lastLine" -ForegroundColor Red
$failed++
}
}

# 输出汇总报告
Write-Host "`n===== 安装汇总 =====" -ForegroundColor Yellow
Write-Host "总计: $total | 成功: $success | 失败: $failed"
}

# 示例:仅安装 Runtime 类别的软件
Install-WinGetFromList -ListFile $listPath -Categories 'Runtime', 'VCS'

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
软件清单已保存到: C:\Users\admin\winget-software-list.json

[1/3] 安装: Git for Windows
-> 成功

[2/3] 安装: PowerShell 7
-> 成功

[3/3] 安装: Node.js LTS
-> 成功

===== 安装汇总 =====
总计: 3 | 成功: 3 | 失败: 0

使用 JSON 清单文件管理软件列表的好处是:不同团队可以维护各自的清单文件,新员工入职时只需指定对应的清单即可完成环境搭建。同时,清单文件可以纳入 Git 版本管理,便于审计和追溯软件配置变更。

软件更新检查与自动化更新

保持软件版本最新是安全运维的基本要求。下面的脚本实现了自动检测可更新的软件,并在确认后执行批量更新,同时支持将更新任务注册为 Windows 计划任务实现定时自动执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# 检查可更新的软件
function Get-WinGetUpdate {
$output = winget upgrade --accept-source-agreements 2>&1

# 解析输出获取可更新的包列表
$lines = $output -split "`n"
$updates = @()

# 找到数据行(跳过标题和分隔线)
$dataStarted = $false
foreach ($line in $lines) {
if ($line -match '^[-\s]+$') {
$dataStarted = $true
continue
}
if ($dataStarted -and $line.Trim() -and $line -notmatch '^\s*$') {
$updates += $line.Trim()
}
}

return $updates
}

# 执行更新
function Start-WinGetUpdate {
param(
[string]$PackageId,

[switch]$All,

[switch]$WhatIf
)

if ($All) {
Write-Host "检查所有可更新的软件..." -ForegroundColor Cyan
$updates = Get-WinGetUpdate

if ($updates.Count -eq 0) {
Write-Host "所有软件均为最新版本" -ForegroundColor Green
return
}

Write-Host "发现 $($updates.Count) 个可更新的软件:`n"
$updates | ForEach-Object { Write-Host " - $_" }

if ($WhatIf) {
Write-Host "`n[WhatIf 模式] 跳过实际更新" -ForegroundColor Yellow
return
}

$confirm = Read-Host "`n确认更新全部? (Y/N)"
if ($confirm -eq 'Y') {
winget upgrade --all `
--accept-source-agreements `
--accept-package-agreements 2>&1
}
}
elseif ($PackageId) {
if ($WhatIf) {
Write-Host "[WhatIf 模式] 将更新: $PackageId" -ForegroundColor Yellow
return
}

winget upgrade --id $PackageId `
--accept-source-agreements `
--accept-package-agreements 2>&1
}
}

# 定时任务:每天检查更新并记录日志
function Register-WinGetUpdateTask {
param(
[string]$Time = '03:00'
)

$scriptPath = Join-Path $HOME 'winget-auto-update.ps1'

# 生成自动更新脚本
$scriptContent = @"
`$logDir = Join-Path `$HOME 'WinGetUpdateLogs'
if (-not (Test-Path `$logDir)) {
New-Item -Path `$logDir -ItemType Directory | Out-Null
}

`$logFile = Join-Path `$logDir "update-$(Get-Date -Format 'yyyy-MM-dd').log"
"[$((Get-Date))] 开始检查更新" | Out-File `$logFile -Append

`$output = winget upgrade --all --accept-source-agreements --accept-package-agreements --silent 2>&1
`$output | Out-File `$logFile -Append

"[$((Get-Date))] 更新完成" | Out-File `$logFile -Append
"@

Set-Content -Path $scriptPath -Value $scriptContent -Encoding utf8

# 注册计划任务
$action = New-ScheduledTaskAction -Execute 'pwsh.exe' `
-Argument "-NoProfile -File `"$scriptPath`""
$trigger = New-ScheduledTaskTrigger -Daily -At $Time
$settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries

Register-ScheduledTask `
-TaskName 'WinGet-AutoUpdate' `
-Action $action `
-Trigger $trigger `
-Settings $settings `
-Force

Write-Host "已注册每日更新任务,执行时间: $Time" -ForegroundColor Green
}

# 示例:检查更新(仅查看,不执行)
Start-WinGetUpdate -All -WhatIf

# 示例:注册每日凌晨 3 点自动更新任务
# Register-WinGetUpdateTask -Time '03:00'

执行结果示例:

1
2
3
4
5
6
7
8
检查所有可更新的软件...
发现 3 个可更新的软件:

- Git.Git 2.43.0 2.47.1 winget
- Microsoft.VisualStudioCode 1.95.0 1.96.2 winget
- Docker.DockerDesktop 4.34.0 4.35.1 winget

[WhatIf 模式] 跳过实际更新

将更新检查注册为 Windows 计划任务后,系统会在每天凌晨自动检查并安装更新,同时将操作日志写入文件。运维人员可以通过查看日志目录下的文件,快速了解每台机器的软件更新情况,确保安全补丁及时到位。

注意事项

  1. WinGet 版本要求:建议使用 WinGet v1.6 或更高版本,旧版本可能不支持 --accept-package-agreements 等参数。可以通过 winget --version 查看当前版本,通过 Microsoft Store 自动更新 WinGet。

  2. 管理员权限:部分软件的安装需要管理员权限。在脚本中可以使用 #requires -RunAsAdministrator 声明,或者以管理员身份运行 PowerShell 来执行安装操作,否则可能会遇到权限不足的错误。

  3. 网络环境:在企业代理环境下,WinGet 可能需要额外配置才能正常访问软件源。可以通过 winget settings 命令打开配置文件,设置代理和网络相关参数,或者配置企业内部源(如 Azure Artifacts)作为替代。

  4. 安装失败处理:某些软件不支持静默安装或存在安装依赖冲突。建议在批量部署前先在测试环境中验证清单文件,对于失败的安装记录错误日志并单独处理,避免一个失败阻塞整个部署流程。

  5. 计划任务兼容性Register-ScheduledTask 需要 Windows 8.1 及以上系统。在 Windows Server 环境中,需要确认 Task Scheduler 服务正常运行,且执行账户具有安装软件的权限。对于不支持计划任务的场景,可以考虑使用 Group Policy 的启动脚本替代。

  6. PATH 环境变量刷新:WinGet 安装完软件后可能修改了系统 PATH,但当前 PowerShell 会话不会自动感知变更。部署完成后需要重启 PowerShell 或手动刷新环境变量:$env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'User'),否则后续命令可能找不到新安装的可执行文件。

PowerShell 技能连载 - Windows 包管理自动化

适用于 PowerShell 5.1 及以上版本(Windows)

在 Windows 上管理软件安装是运维工作中最繁琐的环节之一——每台服务器需要安装几十个工具和运行时,手动安装耗时且容易遗漏版本不一致。Windows Package Manager(winget)和 Chocolatey 的出现改变了这一局面,它们让 Windows 也拥有了类似 Linux apt-get / yum 的包管理体验。结合 PowerShell,可以实现软件的批量安装、版本锁定和自动更新。

本文将讲解 winget 和 Chocolatey 的 PowerShell 集成,以及如何构建标准化的软件清单。

WinGet 基础操作

WinGet 是微软官方的 Windows 包管理器,内置在 Windows 11 和 Windows 10 1809+ 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 检查 winget 版本
winget --version

# 搜索软件包
winget search "Visual Studio Code" --source winget

# 安装软件包
winget install --id Microsoft.VisualStudioCode --accept-package-agreements --accept-source-agreements

# 安装特定版本
winget install --id Git.Git --version 2.45.0 --accept-package-agreements

# 查看已安装的软件
winget list

# 升级所有可更新的软件
winget upgrade --all --accept-package-agreements

# 导出已安装软件列表
winget export -o C:\Config\installed-packages.json

# 从清单导入安装
winget import -i C:\Config\installed-packages.json --accept-package-agreements

执行结果示例:

1
2
3
4
5
6
7
8
9
10
v1.7.11261

名称 ID 版本 可用 源
---------------------------------------------------------------------------------------
Visual Studio Code Microsoft.VisualStudioCode 1.89.1 winget
Git Git.Git 2.45.0 winget
PowerShell Microsoft.PowerShell 7.4.2 7.5.0 winget
7-Zip 7zip.7zip 24.08 winget

已成功安装的包: 4, 已跳过: 0, 失败: 0

用 PowerShell 封装 WinGet

将 winget 命令封装为 PowerShell 函数,可以更好地集成到自动化脚本中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
function Get-WinGetPackageInfo {
<#
.SYNOPSIS
查询 winget 软件包信息
#>
param([string]$PackageId)

$output = winget show --id $PackageId --accept-source-agreements 2>&1
$info = [ordered]@{}

foreach ($line in $output) {
if ($line -match '^\s*(.+?)\s{2,}(.+)$') {
$key = $Matches[1].Trim()
$value = $Matches[2].Trim()
$info[$key] = $value
}
}

return [PSCustomObject]$info
}

function Install-WinGetPackages {
<#
.SYNOPSIS
批量安装 winget 软件包
#>
param(
[Parameter(Mandatory)]
[string[]]$PackageIds,

[switch]$Upgrade
)

$results = @()

foreach ($id in $PackageIds) {
Write-Host "正在处理:$id" -ForegroundColor Cyan
$args = @('install', '--id', $id, '--accept-package-agreements', '--accept-source-agreements', '--silent')

if ($Upgrade) {
$args = @('upgrade', '--id', $id, '--accept-package-agreements', '--accept-source-agreements', '--silent')
}

$process = Start-Process -FilePath winget -ArgumentList $args -Wait -PassThru -NoNewWindow

$results += [PSCustomObject]@{
Package = $id
Action = if ($Upgrade) { 'Upgrade' } else { 'Install' }
Success = $process.ExitCode -eq 0
ExitCode = $process.ExitCode
}
}

$results | Format-Table -AutoSize
$success = ($results | Where-Object Success).Count
Write-Host "`n完成:$success/$($results.Count) 成功" -ForegroundColor $(if ($success -eq $results.Count) { 'Green' } else { 'Yellow' })
}

# 批量安装常用开发工具
$devTools = @(
'Git.Git',
'Microsoft.VisualStudioCode',
'Microsoft.WindowsTerminal',
'7zip.7zip',
'jq.jq',
'BurntSushi.ripgrep.MSVC',
'sharkdp.fd'
)

Install-WinGetPackages -PackageIds $devTools

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
正在处理:Git.Git
正在处理:Microsoft.VisualStudioCode
...

Package Action Success ExitCode
------- ------ ------- --------
Git.Git Install True 0
Microsoft.VisualStudioCode Install True 0
Microsoft.WindowsTerminal Install True 0
7zip.7zip Install True 0
jq.jq Install True 0

完成:7/7 成功

Chocolatey 集成

Chocolatey 是 Windows 上最成熟的第三方包管理器,拥有超过 9000 个软件包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 安装 Chocolatey(需要管理员权限)
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))

# 常用 Chocolatey 命令
choco search nodejs
choco install nodejs-lts -y
choco install python -y
choco install docker-desktop -y

# 查看已安装的包
choco list --local-only

# 升级所有包
choco upgrade all -y

# 导出已安装包列表
choco export -o C:\Config\choco-packages.config

# 从配置文件安装
choco install C:\Config\choco-packages.config -y

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
Chocolatey v2.2.2
1 validations passed
nodejs 22.2.0 [Approved]
nodejs-lts 20.14.0 [Approved]

Chocolatey installed 3/3 packages successfully

Name Version
---- -------
chocolatey 2.2.2
nodejs-lts 20.14.0
python 3.12.3
7zip 24.08

构建标准软件清单

对于团队或服务器群,维护一份标准软件清单可以确保环境一致性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
function New-SoftwareManifest {
<#
.SYNOPSIS
生成标准软件安装清单
#>
param(
[string]$OutputPath = "C:\Config\software-manifest.json"
)

# 定义标准软件列表
$manifest = @{
metadata = @{
created = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
author = $env:USERNAME
machine = $env:COMPUTERNAME
}
packages = @(
@{ id = 'Git.Git'; version = 'latest'; required = $true; category = 'DevTools' }
@{ id = 'Microsoft.VisualStudioCode'; version = 'latest'; required = $true; category = 'DevTools' }
@{ id = 'Microsoft.PowerShell'; version = '7.4.2'; required = $true; category = 'Runtime' }
@{ id = '7zip.7zip'; version = 'latest'; required = $true; category = 'Utilities' }
@{ id = 'Docker.DockerDesktop'; version = 'latest'; required = $false; category = 'Containers' }
@{ id = 'Python.Python.3.12'; version = 'latest'; required = $false; category = 'Runtime' }
)
}

$manifest | ConvertTo-Json -Depth 5 | Set-Content $OutputPath -Encoding UTF8
Write-Host "软件清单已生成:$OutputPath" -ForegroundColor Green
}

function Install-FromManifest {
<#
.SYNOPSIS
从清单文件安装所有软件
#>
param(
[Parameter(Mandatory)]
[string]$ManifestPath
)

$manifest = Get-Content $ManifestPath | ConvertFrom-Json
Write-Host "清单创建于:$($manifest.metadata.created)" -ForegroundColor Cyan
Write-Host "软件包数量:$($manifest.packages.Count)" -ForegroundColor Cyan

$results = foreach ($pkg in $manifest.packages) {
$args = @('install', '--id', $pkg.id, '--accept-package-agreements', '--accept-source-agreements', '--silent')

if ($pkg.version -ne 'latest') {
$args += @('--version', $pkg.version)
}

$process = Start-Process winget -ArgumentList $args -Wait -PassThru -NoNewWindow 2>$null

[PSCustomObject]@{
Package = $pkg.id
Version = $pkg.version
Required = $pkg.Required
Category = $pkg.Category
Success = $process.ExitCode -eq 0
}
}

$results | Format-Table -AutoSize
}

# 使用示例
New-SoftwareManifest
Install-FromManifest -ManifestPath "C:\Config\software-manifest.json"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
软件清单已生成:C:\Config\software-manifest.json
清单创建于:2025-05-19 08:30:00
软件包数量:7

Package Version Required Category Success
------- ------- -------- -------- -------
Git.Git latest True DevTools True
Microsoft.VisualStudioCode latest True DevTools True
Microsoft.PowerShell 7.4.2 True Runtime True
7zip.7zip latest True Utilities True

注意事项

  1. 管理员权限:winget 和 Chocolatey 的系统级安装需要管理员权限。Chocolatey 的 --user 模式可以安装到用户目录
  2. 安装顺序:某些软件有依赖关系(如 VS Code 扩展依赖 VS Code),清单中应按依赖顺序排列
  3. 版本锁定:生产环境建议锁定版本号,避免自动更新引入不兼容变更
  4. MSI/EXE 退出码:不同安装程序的退出码含义不同,0 通常表示成功,3010 表示需要重启
  5. 离线安装:无外网环境可以预先下载安装包,使用 winget install --manifest 或 Chocolatey 的 --source 参数指定本地路径
  6. 幂等性:重复安装同一软件通常是安全的(已安装则跳过),但建议在脚本中检查安装状态

PowerShell 技能连载 - WinGet 包管理自动化

适用于 Windows 10/11,PowerShell 7.0 及以上版本

WinGet:Windows 包管理的新时代

在 Linux 世界中,aptyumpacman 等包管理器早已是系统管理的标配工具。而在 Windows 平台上,长期以来我们只能依赖图形界面的安装向导,或者各自为政的第三方管理工具(如 Chocolatey、Scoop)。2020 年微软推出 WinGet(Windows Package Manager)后,这一局面终于开始改变。

如今 WinGet 已经相当成熟,软件源中收录了超过 6000 款常用软件,覆盖开发工具、浏览器、办公套件等几乎所有类别。更重要的是,WinGet 是 Windows 10/11 的内置组件,无需额外安装即可使用。将 WinGet 与 PowerShell 结合,我们可以实现软件安装、升级、卸载的完全自动化——无论是日常维护还是新机初始化,都能一键搞定。

本文将介绍如何用 PowerShell 封装 WinGet 命令、批量安装软件清单、自动检测升级、生成软件报告,以及编写新机初始化脚本。

封装 WinGet 常用命令

WinGet 的命令行输出是纯文本格式,不便于在脚本中进一步处理。我们先将常用的 WinGet 操作封装成 PowerShell 函数,输出结构化对象,方便后续链式调用和自动化编排。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
function Get-WinGetPackage {
<#
.SYNOPSIS
获取已安装的 WinGet 软件包列表
#>
[CmdletBinding()]
param(
[string]$Name
)
$args = @('list')
if ($Name) {
$args += @('--name', $Name)
}
$output = winget @args 2>&1
# 跳过表头和末尾的摘要行
$lines = $output | Where-Object {
$_ -notmatch '^Name\s+Id' -and
$_ -notmatch '^\s*$' -and
$_ -notmatch '^\d+ packages'
}
foreach ($line in $lines) {
$parts = $line -split '\s{2,}'
if ($parts.Count -ge 3) {
[PSCustomObject]@{
Name = $parts[0]
Id = $parts[1]
Version = $parts[2]
}
}
}
}

function Install-WinGetPackage {
<#
.SYNOPSIS
安装 WinGet 软件包
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[string]$Id,
[switch]$Force
)
if ($PSCmdlet.ShouldProcess($Id, 'Install WinGet package')) {
$installArgs = @('install', '--id', $Id, '--accept-package-agreements',
'--accept-source-agreements')
if ($Force) {
$installArgs += '--force'
}
winget @installArgs 2>&1
}
}

function Update-WinGetPackage {
<#
.SYNOPSIS
升级指定的 WinGet 软件包
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[string]$Id,
[switch]$All
)
if ($PSCmdlet.ShouldProcess($Id, 'Update WinGet package')) {
$upgradeArgs = @('upgrade')
if ($All) {
$upgradeArgs += '--all'
} elseif ($Id) {
$upgradeArgs += @('--id', $Id)
}
$upgradeArgs += @('--accept-package-agreements', '--accept-source-agreements')
winget @upgradeArgs 2>&1
}
}

上面的代码定义了三个核心函数:Get-WinGetPackage 解析 winget list 的纯文本输出为结构化对象,Install-WinGetPackage 封装安装命令并自动接受协议,Update-WinGetPackage 封装升级命令。每个函数都支持 ShouldProcess,可以通过 -WhatIf 参数进行干运行预览。

1
2
3
4
5
6
7
Name                Id                             Version
---- -- -------
PowerShell Microsoft.PowerShell 7.4.6
Visual Studio Code Microsoft.VisualStudioCode 1.96.2
Windows Terminal Microsoft.WindowsTerminal 1.21.3231
Git Git.Git 2.47.1
7-Zip 7zip.7zip 24.08

批量安装软件清单

重装系统后手动安装几十个软件是一件耗时且容易遗漏的事情。将常用软件整理成 JSON 清单文件,然后用脚本批量安装,可以确保每次安装的软件集一致且不遗漏。

首先,创建一个软件清单 JSON 文件,将需要的软件按类别分组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# 定义软件安装清单
$manifest = @'
{
"categories": [
{
"name": "开发工具",
"packages": [
"Microsoft.VisualStudioCode",
"Git.Git",
"Microsoft.PowerShell",
"OpenJS.NodeJS.LTS",
"Python.Python.3.12"
]
},
{
"name": "浏览器",
"packages": [
"Google.Chrome",
"Mozilla.Firefox"
]
},
{
"name": "系统工具",
"packages": [
"7zip.7zip",
"Microsoft.WindowsTerminal",
"Microsoft.Powertoys",
"voidtools.Everything"
]
},
{
"name": "效率工具",
"packages": [
"Obsidian.Obsidian",
"Telegram.TelegramDesktop"
]
}
]
}
'@

$pkgList = $manifest | ConvertFrom-Json

foreach ($category in $pkgList.categories) {
Write-Host "`n=== 安装类别: $($category.name) ===" -ForegroundColor Cyan
foreach ($pkgId in $category.packages) {
Write-Host "正在安装: $pkgId ..." -ForegroundColor Yellow -NoNewline
$result = winget install --id $pkgId `
--accept-package-agreements `
--accept-source-agreements 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host " 完成" -ForegroundColor Green
} else {
Write-Host " 失败 (退出码: $LASTEXITCODE)" -ForegroundColor Red
Write-Host " 错误信息: $($result | Select-Object -Last 2)" -ForegroundColor DarkRed
}
}
}

这段代码将软件清单以 JSON 格式定义,按类别分组管理。安装时逐类别遍历,每个软件包单独安装并报告结果。JSON 格式的清单文件也可以单独保存到磁盘,纳入版本控制,方便团队共享。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
=== 安装类别: 开发工具 ===
正在安装: Microsoft.VisualStudioCode ... 完成
正在安装: Git.Git ... 完成
正在安装: Microsoft.PowerShell ... 完成
正在安装: OpenJS.NodeJS.LTS ... 完成
正在安装: Python.Python.3.12 ... 完成

=== 安装类别: 浏览器 ===
正在安装: Google.Chrome ... 完成
正在安装: Mozilla.Firefox ... 完成

=== 安装类别: 系统工具 ===
正在安装: 7zip.7zip ... 完成
正在安装: Microsoft.WindowsTerminal ... 完成
正在安装: Microsoft.Powertoys ... 完成
正在安装: voidtools.Everything ... 完成

=== 安装类别: 效率工具 ===
正在安装: Obsidian.Obsidian ... 完成
正在安装: Telegram.TelegramDesktop ... 完成

检测已安装软件并自动升级

软件更新往往包含安全补丁和功能改进,但手动逐个检查升级非常繁琐。下面的脚本会扫描所有可通过 WinGet 升级的软件,筛选出有可用更新的,然后自动执行升级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
function Invoke-WinGetAutoUpgrade {
<#
.SYNOPSIS
检测所有可升级的 WinGet 软件包并执行升级
.PARAMETER DryRun
仅显示可升级的软件,不实际执行升级
.PARAMETER ExcludeIds
排除不需要升级的软件包 ID 列表
#>
[CmdletBinding()]
param(
[switch]$DryRun,
[string[]]$ExcludeIds
)
Write-Host "正在扫描可升级的软件包..." -ForegroundColor Cyan
$upgradeList = winget upgrade 2>&1
$packages = $upgradeList | Where-Object {
$_ -match '^\S' -and
$_ -notmatch '^Name\s+Id' -and
$_ -notmatch '^\d+ upgrades'
} | ForEach-Object {
$parts = $_ -split '\s{2,}'
if ($parts.Count -ge 4) {
[PSCustomObject]@{
Name = $parts[0]
Id = $parts[1]
InstalledVer = $parts[2]
AvailableVer = $parts[3]
}
}
}
if ($ExcludeIds) {
$packages = $packages | Where-Object { $_.Id -notin $ExcludeIds }
}
if (-not $packages) {
Write-Host "所有软件均为最新版本" -ForegroundColor Green
return
}
Write-Host "发现 $($packages.Count) 个可升级软件包:" -ForegroundColor Yellow
$packages | Format-Table -Property Name, Id, InstalledVer, AvailableVer -AutoSize
if ($DryRun) {
Write-Host "[DryRun] 跳过实际升级" -ForegroundColor DarkYellow
return
}
foreach ($pkg in $packages) {
Write-Host "升级 $($pkg.Name) ($($pkg.InstalledVer) -> $($pkg.AvailableVer))..." `
-ForegroundColor Yellow -NoNewline
$result = winget upgrade --id $pkg.Id `
--accept-package-agreements `
--accept-source-agreements 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host " 完成" -ForegroundColor Green
} else {
Write-Host " 失败" -ForegroundColor Red
}
}
Write-Host "`n升级完成" -ForegroundColor Green
}

# 示例:排除已知不兼容升级的软件,先干运行查看
Invoke-WinGetAutoUpgrade -DryRun -ExcludeIds @('SomeApp.Legacy')

# 确认后执行实际升级
# Invoke-WinGetAutoUpgrade -ExcludeIds @('SomeApp.Legacy')

这个函数支持 -DryRun 参数先预览可升级列表而不实际执行,也支持 -ExcludeIds 排除某些已知升级后会出问题的软件。对于生产环境,建议先运行干运行模式确认,再执行实际升级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
正在扫描可升级的软件包...
发现 4 个可升级软件包:

Name Id InstalledVer AvailableVer
---- -- ------------ ------------
7-Zip 7zip.7zip 24.06 24.08
Visual Studio Code Microsoft.VisualStudioCode 1.95.3 1.96.2
Git Git.Git 2.46.0 2.47.1
PowerShell Microsoft.PowerShell 7.4.5 7.4.6

升级 7-Zip (24.06 -> 24.08)... 完成
升级 Visual Studio Code (1.95.3 -> 1.96.2)... 完成
升级 Git (2.46.0 -> 2.47.1)... 完成
升级 PowerShell (7.4.5 -> 7.4.6)... 完成

升级完成

生成软件清单报告

定期生成系统已安装软件的报告,有助于审计和资产盘点。下面的脚本将 WinGet 管理的软件信息导出为 CSV 和 HTML 两种格式,便于存档和查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
function Export-WinGetReport {
<#
.SYNOPSIS
生成已安装软件清单报告,输出 CSV 和 HTML 格式
.PARAMETER OutputDir
报告输出目录,默认为当前用户的桌面
#>
[CmdletBinding()]
param(
[string]$OutputDir = [Environment]::GetFolderPath('Desktop')
)
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$csvPath = Join-Path $OutputDir "WinGet_Inventory_${timestamp}.csv"
$htmlPath = Join-Path $OutputDir "WinGet_Inventory_${timestamp}.html"
Write-Host "正在收集已安装软件信息..." -ForegroundColor Cyan
$rawOutput = winget list 2>&1
$packages = $rawOutput | Where-Object {
$_ -match '^\S' -and
$_ -notmatch '^Name\s+Id' -and
$_ -notmatch '^\d+ packages'
} | ForEach-Object {
$parts = $_ -split '\s{2,}'
if ($parts.Count -ge 3) {
[PSCustomObject]@{
Name = $parts[0]
Id = $parts[1]
Version = $parts[2]
ScanDate = (Get-Date -Format 'yyyy-MM-dd')
}
}
}
# 导出 CSV
$packages | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8
Write-Host "CSV 报告已保存: $csvPath" -ForegroundColor Green
# 导出 HTML
$htmlHeader = @"
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WinGet 软件清单报告 - $timestamp</title>
<style>
body { font-family: 'Segoe UI', sans-serif; margin: 20px; }
h1 { color: #0078d4; }
table { border-collapse: collapse; width: 100%; }
th { background: #0078d4; color: white; padding: 8px 12px; text-align: left; }
td { border-bottom: 1px solid #e0e0e0; padding: 8px 12px; }
tr:nth-child(even) { background: #f5f5f5; }
.total { margin-top: 16px; font-weight: bold; color: #333; }
</style>
</head>
<body>
<h1>WinGet 软件清单报告</h1>
<p>生成时间: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')</p>
"@
$htmlBody = $packages | ConvertTo-Html -Fragment
$htmlFooter = @"
<p class="total">共 $($packages.Count) 个软件包</p>
</body></html>
"@
$htmlHeader + $htmlBody + $htmlFooter | Set-Content -Path $htmlPath -Encoding UTF8
Write-Host "HTML 报告已保存: $htmlPath" -ForegroundColor Green
Write-Host "共发现 $($packages.Count) 个已安装软件包" -ForegroundColor Cyan
}

Export-WinGetReport

脚本会同时输出 CSV 和 HTML 两种格式。CSV 适合导入 Excel 进行进一步分析,HTML 报告则可以直接在浏览器中查看,样式更友好。两种文件都以时间戳命名,方便归档对比。

1
2
3
4
正在收集已安装软件信息...
CSV 报告已保存: C:\Users\admin\Desktop\WinGet_Inventory_20250429_080000.csv
HTML 报告已保存: C:\Users\admin\Desktop\WinGet_Inventory_20250429_080000.html
共发现 42 个已安装软件包

新机初始化脚本

将前面的功能组合起来,我们可以编写一个完整的新机初始化脚本。这个脚本会安装基础软件、配置 PowerShell 环境、生成初始软件报告,让一台全新的 Windows 电脑在几分钟内进入可工作状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#Requires -RunAsAdministrator
<#
.SYNOPSIS
Windows 新机初始化脚本
.DESCRIPTION
通过 WinGet 批量安装常用软件,配置 PowerShell 环境,
生成软件清单报告,快速搭建工作环境。
.NOTES
需要以管理员权限运行
#>
param(
[string]$ManifestPath,
[switch]$SkipUpgrade,
[switch]$SkipReport
)
$ErrorActionPreference = 'Stop'
Write-Host @"

============================================
Windows 新机初始化脚本
$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
============================================

"@ -ForegroundColor Cyan
# 步骤 1: 确认 WinGet 可用
Write-Host "[1/5] 检查 WinGet 可用性..." -ForegroundColor Yellow
$wingetCmd = Get-Command winget -ErrorAction SilentlyContinue
if (-not $wingetCmd) {
Write-Host "WinGet 未安装,正在通过 App Installer 安装..." -ForegroundColor Yellow
# Windows 11 通常自带,Windows 10 可能需要从 Microsoft Store 安装
Write-Host "请从 Microsoft Store 安装 '应用安装程序' 后重试" -ForegroundColor Red
return
}
Write-Host "WinGet 版本: $(winget --version)" -ForegroundColor Green
# 步骤 2: 加载软件清单
Write-Host "`n[2/5] 加载软件安装清单..." -ForegroundColor Yellow
if (-not $ManifestPath) {
$ManifestPath = Join-Path $PSScriptRoot 'software-manifest.json'
}
if (-not (Test-Path $ManifestPath)) {
Write-Host "清单文件不存在: $ManifestPath" -ForegroundColor Red
Write-Host "请创建清单文件或通过 -ManifestPath 参数指定路径" -ForegroundColor Yellow
return
}
$manifest = Get-Content $ManifestPath -Raw | ConvertFrom-Json
$totalPkgs = ($manifest.categories | ForEach-Object { $_.packages.Count } |
Measure-Object -Sum).Sum
Write-Host "清单包含 $($manifest.categories.Count) 个类别,共 $totalPkgs 个软件包" `
-ForegroundColor Green
# 步骤 3: 批量安装
Write-Host "`n[3/5] 开始批量安装..." -ForegroundColor Yellow
$success = 0
$failed = 0
foreach ($category in $manifest.categories) {
Write-Host "`n--- $($category.name) ---" -ForegroundColor Cyan
foreach ($pkgId in $category.packages) {
try {
winget install --id $pkgId `
--accept-package-agreements `
--accept-source-agreements 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Host " [OK] $pkgId" -ForegroundColor Green
$success++
} else {
Write-Host " [FAIL] $pkgId (退出码: $LASTEXITCODE)" -ForegroundColor Red
$failed++
}
} catch {
Write-Host " [ERROR] $pkgId : $_" -ForegroundColor Red
$failed++
}
}
}
Write-Host "`n安装统计: 成功 $success / 失败 $failed / 总计 $totalPkgs" -ForegroundColor Cyan
# 步骤 4: 升级已安装软件
if (-not $SkipUpgrade) {
Write-Host "`n[4/5] 升级所有已安装软件到最新版..." -ForegroundColor Yellow
winget upgrade --all --accept-package-agreements --accept-source-agreements 2>&1
Write-Host "升级完成" -ForegroundColor Green
} else {
Write-Host "`n[4/5] 跳过软件升级" -ForegroundColor DarkYellow
}
# 步骤 5: 生成报告
if (-not $SkipReport) {
Write-Host "`n[5/5] 生成软件清单报告..." -ForegroundColor Yellow
Export-WinGetReport
} else {
Write-Host "`n[5/5] 跳过报告生成" -ForegroundColor DarkYellow
}
Write-Host @"

============================================
初始化完成!
成功安装: $success
安装失败: $failed
$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
============================================

"@ -ForegroundColor Green

这个脚本设计了五个步骤:检查 WinGet 可用性、加载软件清单、批量安装、升级已安装软件、生成报告。每一步都有清晰的进度提示,支持通过参数跳过升级和报告步骤。脚本开头使用 #Requires -RunAsAdministrator 确保以管理员权限运行,因为部分软件安装需要提升权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
============================================
Windows 新机初始化脚本
2025-04-29 08:00:00
============================================

[1/5] 检查 WinGet 可用性...
WinGet 版本: v1.9.25200

[2/5] 加载软件安装清单...
清单包含 4 个类别,共 13 个软件包

[3/5] 开始批量安装...

--- 开发工具 ---
[OK] Microsoft.VisualStudioCode
[OK] Git.Git
[OK] Microsoft.PowerShell
[OK] OpenJS.NodeJS.LTS
[OK] Python.Python.3.12

--- 浏览器 ---
[OK] Google.Chrome
[OK] Mozilla.Firefox

--- 系统工具 ---
[OK] 7zip.7zip
[OK] Microsoft.WindowsTerminal
[OK] Microsoft.Powertoys
[OK] voidtools.Everything

--- 效率工具 ---
[OK] Obsidian.Obsidian
[OK] Telegram.TelegramDesktop

安装统计: 成功 13 / 失败 0 / 总计 13

[4/5] 升级所有已安装软件到最新版...
升级完成

[5/5] 生成软件清单报告...
CSV 报告已保存: C:\Users\admin\Desktop\WinGet_Inventory_20250429_080000.csv
HTML 报告已保存: C:\Users\admin\Desktop\WinGet_Inventory_20250429_080000.html
共发现 42 个已安装软件包

============================================
初始化完成!
成功安装: 13
安装失败: 0
2025-04-29 08:05:32
============================================

注意事项

  • 管理员权限:部分软件(如系统级安装的程序)需要以管理员身份运行 PowerShell,脚本中已通过 #Requires -RunAsAdministrator 确保这一点
  • WinGet 输出解析:WinGet 的命令行输出格式可能在版本更新后变化,解析逻辑需要相应调整。如果遇到解析异常,建议检查 WinGet 版本并更新解析正则
  • 软件源一致性:WinGet 有两个源(winget 和 msstore),某些软件同时存在于两个源中,安装时建议使用 --id 精确指定软件包 ID,避免歧义
  • 安装顺序:部分软件有依赖关系(如 VS Code 扩展依赖 VS Code 本体),清单中应确保依赖项排在前面
  • 网络环境:在企业网络中,可能需要配置代理或使用内部软件源,WinGet 支持通过 --source 参数指定自定义源
  • 错误处理:批量安装时某个软件失败不应阻断整个流程,脚本中对每个软件单独 try/catch 处理,最终汇总成功和失败数量