PowerShell 技能连载 - Kubernetes Helm 包管理

适用于 PowerShell 7.0 及以上版本,需要 kubectl 和 helm CLI

背景

Helm 是 Kubernetes 生态中最主流的包管理工具,被誉为 “Kubernetes 的 apt/yum”。它通过 Chart 模板将复杂的应用部署抽象为一组可版本化、可复用的配置文件,极大降低了 Kubernetes 应用的分发和运维门槛。在实际的 DevOps 工作流中,团队往往需要同时管理数十个 Chart 仓库、多套环境配置以及频繁的版本升级与回滚。

PowerShell 凭借强大的对象管道和丰富的字符串处理能力,非常适合用来编排 Helm 工作流。相比 Bash 脚本,PowerShell 能够将 helmkubectl 的输出直接转化为结构化对象,便于筛选、比较和批量操作。本文将围绕 Chart 仓库管理、Values 文件动态生成和批量部署回滚三个场景,展示如何用 PowerShell 打造高效的 Helm 自动化工具链。

Chart 仓库与版本管理

在生产环境中,团队通常会订阅多个 Helm 仓库(如 Bitnami、官方稳定仓库、企业私有仓库等),并且需要追踪 Chart 版本的变化。下面的脚本展示了如何用 PowerShell 统一管理仓库、搜索 Chart 并对比版本差异:

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
# 定义仓库列表
$repos = @(
@{ Name = 'bitnami'; Url = 'https://charts.bitnami.com/bitnami' }
@{ Name = 'ingress-nginx'; Url = 'https://kubernetes.github.io/ingress-nginx' }
@{ Name = 'jetstack'; Url = 'https://charts.jetstack.io' }
)

# 批量添加并更新仓库
foreach ($repo in $repos) {
helm repo add $repo.Name $repo.Url 2>$null
Write-Host "已添加仓库: $($repo.Name)" -ForegroundColor Green
}

helm repo update

# 搜索指定关键词的 Chart,并按版本排序
$searchKeyword = 'nginx'
$charts = helm search repo $searchKeyword --output json |
ConvertFrom-Json

$charts |
Select-Object name, @{N='version';E={ $_.chart_version }}, app_version, description |
Sort-Object name |
Format-Table -AutoSize

# 对比已安装版本与仓库最新版本
$releases = helm list --all-namespaces --output json | ConvertFrom-Json

foreach ($rel in $releases) {
$chartName = $rel.chart -replace '-\d+.*$', ''
$installed = $rel.chart_version
$latest = (helm search repo $chartName --output json |
ConvertFrom-Json |
Select-Object -First 1).chart_version

$status = if ($installed -eq $latest) { '已是最新' } else { "可升级: $installed -> $latest" }
Write-Host ("[{0}] {1} ({2}): {3}" -f $rel.namespace, $rel.name, $chartName, $status)
}

# 更新 Chart 依赖(针对本地 Chart)
$localChartPath = './charts/my-application'
if (Test-Path "$localChartPath/Chart.yaml") {
helm dependency update $localChartPath
Write-Host "依赖已更新: $localChartPath" -ForegroundColor Cyan
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
已添加仓库: bitnami
已添加仓库: ingress-nginx
已添加仓库: jetstack
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "bitnami" chart repository
...Successfully got an update from the "ingress-nginx" chart repository
...Successfully got an update from the "jetstack" chart repository
Update Complete.

name version app_version description
---- ------- ----------- -----------
bitnami/nginx 18.2.5 1.25.4 NGINX Open Source...
ingress-nginx/ingress-nginx 4.11.3 1.11.3 Ingress controller...

[default] api-gateway (api-gateway): 可升级: 1.2.0 -> 1.3.1
[production] cert-manager (cert-manager): 已是最新
[staging] my-app (my-app): 可升级: 2.0.1 -> 2.1.0
依赖已更新: ./charts/my-application

Values 文件动态生成

Helm 的灵活性很大程度上来自 values.yaml 文件。在多环境(开发、预发布、生产)部署中,为每个环境维护独立的 Values 文件既繁琐又容易出错。PowerShell 可以基于模板和数据表动态生成 Values 文件,还能安全地处理密码等敏感值:

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
# 定义环境配置矩阵
$environments = @{
dev = @{
replicas = 1
imageTag = 'latest'
resources = @{ cpuRequest = '100m'; memRequest = '128Mi' }
ingress = @{ enabled = $false }
}
staging = @{
replicas = 2
imageTag = 'v2.1.0-rc.1'
resources = @{ cpuRequest = '250m'; memRequest = '256Mi' }
ingress = @{ enabled = $true; host = 'staging.example.com' }
}
production = @{
replicas = 3
imageTag = 'v2.0.1'
resources = @{ cpuRequest = '500m'; memRequest = '512Mi' }
ingress = @{ enabled = $true; host = 'app.example.com'; tls = $true }
}
}

# 为每个环境生成 values 文件
foreach ($envName in $environments.Keys) {
$env = $environments[$envName]
$values = [ordered]@{
replicaCount = $env.replicas
image = [ordered]@{
repository = 'registry.example.com/my-app'
tag = $env.imageTag
pullPolicy = if ($env.imageTag -eq 'latest') { 'Always' } else { 'IfNotPresent' }
}
resources = [ordered]@{
requests = [ordered]@{
cpu = $env.resources.cpuRequest
memory = $env.resources.memRequest
}
}
ingress = $env.ingress
}

$outputPath = "values-$envName.yaml"

# 使用 ConvertTo-Yaml(或手动构建 YAML 字符串)
# 这里用 helm 的 --set 语法验证生成结果
$jsonContent = $values | ConvertTo-Json -Depth 10

# 将 JSON 转为 YAML(利用 yq 或简单字符串替换)
$jsonContent | python3 -c "import sys,yaml,json; yaml.dump(json.load(sys.stdin),open('$outputPath','w'),default_flow_style=False)"

Write-Host "已生成: $outputPath" -ForegroundColor Green
}

# 敏感值处理:从外部密钥管理器获取
function Get-SecretForHelm {
param([string]$SecretName)

# 从环境变量或 Azure Key Vault / HashiCorp Vault 获取
$value = [System.Environment]::GetEnvironmentVariable("HELM_SECRET_$SecretName")
if (-not $value) {
# 尝试从 kubectl secret 获取
$value = kubectl get secret app-secrets -o jsonpath="{.data.$SecretName}" |
base64 -d 2>$null
}
return $value
}

# 合并基础 values 与环境覆盖
function Merge-HelmValues {
param($BasePath, $OverridePath, $OutputPath)

# 使用 yq 合并 YAML 文件
yq eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' `
$BasePath $OverridePath -o yaml > $OutputPath

Write-Host "合并完成: $OutputPath" -ForegroundColor Cyan
}

# 生成包含敏感值的最终 values
$dbPassword = Get-SecretForHelm -SecretName 'DB_PASSWORD'
if ($dbPassword) {
$secretValues = "database:`n password: $dbPassword"
$secretValues | Out-File -FilePath 'values-secrets.yaml' -Encoding utf8
Merge-HelmValues -BasePath 'values-production.yaml' `
-OverridePath 'values-secrets.yaml' `
-OutputPath 'values-production-final.yaml'
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
已生成: values-dev.yaml
已生成: values-staging.yaml
已生成: values-production.yaml
合并完成: values-production-final.yaml

# 生成的 values-dev.yaml 内容摘要:
replicaCount: 1
image:
repository: registry.example.com/my-app
tag: latest
pullPolicy: Always
resources:
requests:
cpu: 100m
memory: 128Mi
ingress:
enabled: false

批量部署与回滚

在微服务架构下,一次发布可能涉及多个 Helm Release。手动逐个执行 helm upgrade 既耗时又容易遗漏。下面的脚本实现了多命名空间批量部署、发布状态追踪以及一键回滚功能:

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
# 定义部署清单
$deployments = @(
@{
Name = 'api-gateway'
Namespace = 'production'
Chart = 'bitnami/nginx'
ValuesFile = 'values-production.yaml'
Wait = $true
}
@{
Name = 'auth-service'
Namespace = 'production'
Chart = './charts/auth-service'
ValuesFile = 'values-auth-production.yaml'
Wait = $true
}
@{
Name = 'frontend'
Namespace = 'production'
Chart = './charts/frontend'
ValuesFile = 'values-frontend-production.yaml'
Wait = $false
}
)

# 确保 namespace 存在
$namespaces = $deployments | Select-Object -ExpandProperty Namespace -Unique
foreach ($ns in $namespaces) {
$exists = kubectl get namespace $ns --ignore-not-found 2>$null
if (-not $exists) {
kubectl create namespace $ns
Write-Host "已创建命名空间: $ns" -ForegroundColor Yellow
}
}

# 批量部署并记录结果
$results = [System.Collections.Generic.List[PSObject]]::new()

foreach ($dep in $deployments) {
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
Write-Host "`n部署: $($dep.Name) -> $($dep.Namespace)" -ForegroundColor Cyan

$waitFlag = if ($dep.Wait) { '--wait' } else { '' }
$command = "helm upgrade --install $($dep.Name) $($dep.Chart) " +
"--namespace $($dep.Namespace) " +
"-f $($dep.ValuesFile) $waitFlag --timeout 5m"

$output = Invoke-Expression $command 2>&1
$stopwatch.Stop()

$success = $LASTEXITCODE -eq 0
$results.Add([PSCustomObject]@{
Name = $dep.Name
Namespace = $dep.Namespace
Status = if ($success) { 'deployed' } else { 'failed' }
Duration = '{0:N1}s' -f $stopwatch.Elapsed.TotalSeconds
Output = $output | Select-Object -First 3
})
}

# 输出部署报告
Write-Host "`n===== 部署报告 =====" -ForegroundColor Yellow
$results |
Select-Object Name, Namespace, Status, Duration |
Format-Table -AutoSize

# 检查是否有失败的部署
$failed = $results | Where-Object { $_.Status -eq 'failed' }
if ($failed) {
Write-Host "`n以下部署失败,准备回滚:" -ForegroundColor Red
$failed | ForEach-Object { Write-Host " - $($_.Name) ($($_.Namespace))" }

# 一键回滚所有失败项
foreach ($f in $failed) {
Write-Host "`n回滚: $($f.Name)" -ForegroundColor Red
helm rollback $f.Name --namespace $f.Namespace
$rollbackStatus = if ($LASTEXITCODE -eq 0) { '回滚成功' } else { '回滚失败' }
Write-Host " $rollbackStatus"
}
}

# 生成当前所有 Release 状态摘要
$allReleases = helm list --all-namespaces --output json | ConvertFrom-Json
$report = $allReleases |
Select-Object name, namespace, @{N='chart';E={ $_.chart -replace '-\d+.*$', '' }},
chart_version, status, updated |
Sort-Object namespace, name

Write-Host "`n===== 当前 Release 状态 =====" -ForegroundColor Green
$report | Format-Table -AutoSize

# 导出报告为 CSV(可选)
$report | Export-Csv -Path "helm-deploy-report-$(Get-Date -Format 'yyyyMMdd-HHmmss').csv" `
-NoTypeInformation -Encoding utf8
Write-Host "报告已导出到 CSV 文件" -ForegroundColor Cyan

执行结果示例:

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
已创建命名空间: production

部署: api-gateway -> production
Release "api-gateway" has been upgraded. Happy Helming!
NAME: api-gateway
LAST DEPLOYED: Wed Apr 23 10:30:00 2026

部署: auth-service -> production
Release "auth-service" has been upgraded. Happy Helming!
NAME: auth-service
LAST DEPLOYED: Wed Apr 23 10:30:45 2026

部署: frontend -> production
Release "frontend" has been upgraded. Happy Helming!

===== 部署报告 =====
Name Namespace Status Duration
---- --------- ------ --------
api-gateway production deployed 32.5s
auth-service production deployed 41.2s
frontend production deployed 3.1s

===== 当前 Release 状态 =====
name namespace chart chart_version status updated
---- --------- ----- ------------- ------ -------
api-gateway production nginx 18.2.5 deployed 2026-04-23 10:30:00
auth-service production auth-service 1.5.0 deployed 2026-04-23 10:30:45
frontend production frontend 2.1.0 deployed 2026-04-23 10:31:02
报告已导出到 CSV 文件

注意事项

  • Helm 版本兼容性:本文示例基于 Helm 3.x,Helm 2 已停止维护,请确保使用 helm version 确认当前版本为 3.0 以上。Helm 3 移除了 Tiller,安全性显著提升。
  • JSON 输出解析helm 命令支持 --output json 参数,配合 ConvertFrom-Json 可以将结果转为 PowerShell 对象。但不同 Helm 版本的 JSON 字段名可能略有差异(如 chart_versionapp_version),使用前建议先检查输出结构。
  • 敏感值管理:不要将密码、Token 等敏感信息硬编码在 Values 文件或脚本中。应结合 Kubernetes Secret、外部密钥管理器(如 HashiCorp Vault、Azure Key Vault)或 Helm Secrets 插件来管理敏感配置。
  • 回滚策略helm rollback 默认回滚到上一个修订版本。如果需要回滚到更早的版本,先用 helm history <release> 查看修订记录,再指定 helm rollback <release> <revision> 回滚到目标版本。
  • 超时与等待:生产环境部署建议始终使用 --wait--timeout 参数,确保 Helm 等待所有 Pod 就绪后才认为部署成功。合理的超时值通常为 5-10 分钟,具体取决于应用的启动时间。
  • 并发部署限制:虽然 PowerShell 支持并发执行(如 ForEach-Object -Parallel),但 Helm 对同一 Release 的并发操作并不安全。建议对同一 Release 的操作串行执行,不同 Release 之间可以安全地并发部署。

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 技能连载 - 包管理与依赖控制

适用于 PowerShell 5.1 及以上版本

PowerShell 模块生态在过去几年里蓬勃发展,PowerShell Gallery 上已经托管了数以万计的模块。从日常运维的 Active Directory 管理,到云端自动化的 Az 模块,再到新兴的 AI 交互工具,几乎每种场景都有对应的模块可用。然而,模块数量的增长也带来了管理上的挑战:不同项目依赖同一个模块的不同版本、私有环境的离线分发需求、以及供应链安全对模块来源的审计要求,都是实际工作中必须面对的问题。

PowerShell 7 引入了 PSResourceGet(Microsoft.PowerShell.PSResourceGet)作为新一代包管理引擎,替代了沿用多年的 PowerShellGet v2。PSResourceGet 基于 NuGet 协议重新实现了仓库交互,在性能、安全性和功能覆盖面上都有显著提升。同时,它保留了与 PowerShellGet 类似的命令风格,降低了迁移成本。对于仍然运行在 Windows PowerShell 5.1 环境中的系统,PowerShellGet 依然可用,但强烈建议尽早迁移到 PSResourceGet。

本文将从基础操作入手,逐步介绍模块搜索与安装、版本锁定与依赖分析,以及私有仓库的搭建方法,帮助你在不同规模的自动化环境中实现可靠的包管理与依赖控制。

PSResourceGet 基础操作

PSResourceGet 的核心操作围绕”仓库(Repository)”展开。仓库是模块的存储和分发端点,默认连接到 PowerShell Gallery。下面的代码展示了从注册仓库到搜索、安装、更新模块的完整流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 安装 PSResourceGet 模块(如果尚未安装)
Install-Module Microsoft.PowerShell.PSResourceGet -Force -Scope CurrentUser

# 查看已注册的仓库
Get-PSResourceRepository

# 注册额外的仓库,例如自定义的 NuGet 源
Register-PSResourceRepository -Name 'MyCompanyFeed' -Uri 'https://nuget.mycompany.com/v3/index.json'

# 搜索模块:在所有仓库中查找包含 "Az" 关键字的模块
Find-PSResource -Name '*Az*' -Type Module -Repository 'PSGallery' | Select-Object Name, Version, Description | Format-Table -AutoSize

# 安装指定模块的最新稳定版本
Install-PSResource -Name 'Pester' -Scope CurrentUser -TrustRepository

# 安装指定版本的模块
Install-PSResource -Name 'Pester' -Version '5.5.0' -Scope CurrentUser

# 更新已安装的模块到最新版本
Update-PSResource -Name 'Pester' -Scope CurrentUser

# 查看本地已安装的模块信息
Get-InstalledPSResource -Name 'Pester'

上述代码中,Register-PSResourceRepository 用于注册自定义仓库,Find-PSResource 支持通配符搜索并可以限定仓库范围,Install-PSResourceUpdate-PSResource 分别完成安装与升级操作。-TrustRepository 参数表示信任该仓库,避免每次安装时都弹出确认提示。

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Name            Uri                                     Trusted
---- --- -------
PSGallery https://www.powershellgallery.com/api/… False
MyCompanyFeed https://nuget.mycompany.com/v3/indexTrue

Name Version Description
---- ------- -----------
Az 12.0.0 Microsoft Azure PowerShell
Az.Accounts 3.0.0 Microsoft Azure PowerShell - Accounts
Az.Compute 7.0.0 Microsoft Azure PowerShell - Compute

Name Version Prerelease Repository Description
---- ------- ---------- ---------- -----------
Pester 5.7.1 PSGallery Pester is the ubiquitous test…

版本锁定与依赖管理

在自动化管道和多人协作的项目中,模块版本的一致性至关重要。不同开发者或不同环境如果安装了不兼容的模块版本,可能导致脚本行为不一致甚至运行失败。PSResourceGet 提供了多种版本控制机制来应对这一问题。

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
# 安装精确版本(版本锁定)
Install-PSResource -Name 'Az.Accounts' -Version '3.0.0' -Scope CurrentUser

# 安装版本范围内的最新版本(支持 NuGet 版本范围语法)
# [3.0.0, 4.0.0) 表示大于等于 3.0.0 且小于 4.0.0
Install-PSResource -Name 'Az.Accounts' -Version '[3.0.0, 4.0.0)' -Scope CurrentUser

# 安装预发布版本
Install-PSResource -Name 'Pester' -Version '6.0.0' -Prerelease -Scope CurrentUser

# 查看模块的依赖关系
$resource = Find-PSResource -Name 'Az.Compute' -Repository 'PSGallery'
$resource.Dependencies | Format-Table Name, VersionRange -AutoSize

# 导出当前环境的模块清单(用于版本锁定文件)
$installed = Get-InstalledPSResource
$lockData = $installed | ForEach-Object {
[PSCustomObject]@{
Name = $_.Name
Version = $_.Version
}
}
$lockData | ConvertTo-Json -Depth 3 | Set-Content -Path './modules.lock.json' -Encoding UTF8

# 根据锁定文件批量恢复模块
$lock = Get-Content -Path './modules.lock.json' -Raw | ConvertFrom-Json
foreach ($entry in $lock) {
Install-PSResource -Name $entry.Name -Version $entry.Version -Scope CurrentUser -ErrorAction SilentlyContinue
}

这段代码展示了几个关键的版本管理技巧:使用 RequiredVersion 精确锁定版本,使用 NuGet 版本范围语法控制兼容范围,通过 Dependencies 属性分析模块的依赖树,以及将环境状态导出为 JSON 锁定文件以便在另一台机器上复现。

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Name       VersionRange
---- ------------
Az.Accounts [3.0.0, 4.0.0)

modules.lock.json 内容示例:
[
{
"Name": "Pester",
"Version": "5.7.1"
},
{
"Name": "Az.Accounts",
"Version": "3.0.0"
},
{
"Name": "Az.Compute",
"Version": "7.0.0"
}
]

私有仓库搭建与内网分发

在企业环境中,出于安全审计或网络隔离的需要,通常不希望服务器直接访问公网的 PowerShell Gallery。搭建私有仓库可以解决这个问题,同时也能用于分发团队内部开发的模块。NuGet.Server 是一种轻量级的私有仓库方案,配合 PSResourceGet 可以实现完整的内网分发流程。

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
# ========== 服务端:搭建 NuGet.Server ==========

# 安装 NuGet.Server 包(在一台有 IIS 的 Windows 服务器上执行)
# 首先确保 NuGet 包源已注册
Register-PackageSource -Name 'NuGetGallery' -Location 'https://www.nuget.org/api/v2' -ProviderName NuGet -Trusted

# 创建 NuGet.Server 站点的目录
$sitePath = 'C:\PSGallery\Private'
New-Item -ItemType Directory -Path $sitePath -Force

# 使用 dotnet 方式创建 NuGet.Server(需要 .NET SDK)
# 或者直接下载 NuGet.Server 包并解压
Save-Package -Name NuGet.Server -Source 'NuGetGallery' -Path $sitePath

# 将自定义模块打包为 nupkg 并发布到私有仓库
$modulePath = 'C:\Modules\MyUtils'
$nupkgOutput = 'C:\PSGallery\Packages'

# 使用 PSResourceGet 发布模块
Publish-PSResource -Path $modulePath -Repository 'MyCompanyFeed' -ApiKey 'your-api-key-here'

# ========== 客户端:从私有仓库安装 ==========

# 在客户端机器上注册私有仓库
Register-PSResourceRepository -Name 'CompanyGallery' -Uri 'https://psgallery.mycompany.com/v3/index.json' -Trusted

# 设置默认仓库优先级(私有仓库优先于公网)
Set-PSResourceRepository -Name 'CompanyGallery' -Priority 1

# 从私有仓库搜索和安装模块
Find-PSResource -Name 'MyUtils' -Repository 'CompanyGallery'
Install-PSResource -Name 'MyUtils' -Repository 'CompanyGallery' -Scope CurrentUser

# 验证模块来源
Get-InstalledPSResource -Name 'MyUtils' | Select-Object Name, Version, Repository

上述代码分为服务端和客户端两部分。服务端负责搭建 NuGet.Server 并发布内部模块,客户端则注册私有仓库、设置优先级,并从中安装模块。通过设置 Priority 参数,可以确保私有仓库中的模块优先于公网同名模块被使用,这在覆盖公网模块的场景中非常有用。

执行结果示例:

1
2
3
4
5
6
7
Name    Version Repository       Description
---- ------- ---------- -----------
MyUtils 1.2.0 CompanyGallery Company internal utility module…

Name Version Repository
---- ------- ----------
MyUtils 1.2.0 CompanyGallery

注意事项

  1. PSResourceGet 与 PowerShellGet 的兼容性:PSResourceGet 可以管理由 PowerShellGet v2/v3 安装的模块,但反向操作可能存在兼容性问题。建议在团队内统一使用 PSResourceGet,避免混用导致的版本记录混乱。

  2. 版本范围语法:PSResourceGet 使用 NuGet 版本范围语法,方括号表示包含边界、圆括号表示排除边界。例如 [1.0.0, 2.0.0) 表示大于等于 1.0.0 且小于 2.0.0。务必仔细检查范围表达式,避免因语法错误导致意外安装了不兼容的版本。

  3. 模块锁定文件应纳入版本控制modules.lock.json 类似于 npm 的 package-lock.json,应与项目代码一起提交到 Git 仓库。这样可以确保 CI/CD 管道中的模块版本与开发环境完全一致。

  4. 私有仓库的安全加固:生产环境的私有仓库应启用 HTTPS、配置 API Key 认证,并定期审计已发布的模块内容。对于高安全要求的环境,可以考虑使用 Azure Artifacts 或 JFrog Artifactory 等企业级制品仓库。

  5. 离线环境的模块分发:对于完全隔离的网络环境,可以使用 Save-PSResource 将模块及其依赖下载到本地目录,然后通过 U 盘或内部文件共享拷贝到目标机器,再用 Install-PSResource 从本地路径安装。这种方式不需要搭建完整的 NuGet 服务。

  6. 模块依赖冲突排查:当遇到模块加载冲突时,可以使用 Get-Module -ListAvailable 查看所有可用版本,结合 $env:PSModulePath 分析模块搜索路径的优先级。对于冲突严重的环境,考虑使用 PowerShell 的 Assembly Load Context(ALC)隔离机制,或在不同项目中使用独立的模块安装路径。

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 技能连载 - PowerShellGet 包管理

适用于 PowerShell 5.1 及以上版本

背景

在现代运维和开发工作中,模块化管理代码已成为主流趋势。Python 有 pip,Node.js 有 npm,而 PowerShell 的官方包管理工具就是 PowerShellGet。它提供了一组标准 cmdlet,让用户可以从 PowerShell Gallery 或私有 NuGet 源搜索、安装、更新和发布模块及脚本。自 PowerShell 5.0 起,PowerShellGet 已内置于系统中,开箱即用。

在日常工作中,我们经常面临以下场景:团队共享了一批自定义运维模块,需要统一分发和版本控制;某个第三方模块发布了安全补丁,需要快速检查并升级所有服务器上的旧版本;或者我们需要将内部开发的工具模块发布到私有仓库,供多个环境使用。PowerShellGet 正是解决这些问题的利器。

本文将从仓库管理、模块搜索与安装、批量更新、以及私有源搭建等方面,全面介绍 PowerShellGet 的实际用法,帮助你构建高效的 PowerShell 模块管理工作流。

查看和管理仓库源

PowerShellGet 依赖 NuGet 协议与仓库交互。默认情况下,系统已注册 PowerShell Gallery 作为公共源。我们可以先查看当前注册的所有仓库,确认源是否可用。

1
2
# 查看已注册的仓库列表
Get-PSRepository
1
2
3
Name                      InstallationPolicy   SourceLocation
---- ----------------- --------------
PSGallery Untrusted https://www.powershellgallery.com/api/v2

如果默认的 PSGallery 仓库丢失(某些精简系统可能出现),可以使用以下命令恢复:

1
2
3
4
5
# 恢复默认的 PSGallery 仓库
Register-PSRepository -Default

# 验证仓库是否恢复成功
Get-PSRepository
1
2
3
Name                      InstallationPolicy   SourceLocation
---- ----------------- --------------
PSGallery Untrusted https://www.powershellgallery.com/api/v2

在生产环境中,建议将仓库的 InstallationPolicy 设为 Trusted,避免每次安装时都需要手动确认:

1
2
# 将 PSGallery 设为受信任源
Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted

搜索和安装模块

PowerShellGet 提供了 Find-ModuleInstall-Module 两个核心 cmdlet,分别用于搜索和安装模块。下面演示如何按关键词搜索模块,并查看模块的详细信息。

1
2
3
4
5
6
# 按标签搜索安全相关的模块
$modules = Find-Module -Tag 'Security' | Select-Object -First 5

foreach ($m in $modules) {
Write-Output ("模块名: {0}, 版本: {1}, 描述: {2}" -f $m.Name, $m.Version, $m.Description)
}
1
2
3
4
5
模块名: Carbon, 版本: 2.5.0, 描述: Carbon is a PowerShell module for automating the configuration of Windows.
模块名: ACMESharp, 版本: 0.8.1, 描述: Client library for the ACME protocol for certificate management.
模块名: DSInternals, 版本: 2.22, 描述: The DSInternals PowerShell Module exposes several internal features of Active Directory.
模块名: DSCEA, 版本: 1.2.0.0, 描述: DSCEA is a scanning engine for processing Test-DscConfiguration results.
模块名: PoshACME, 版本: 3.10.0, 描述: PowerShell module for ACME certificate management.

找到需要的模块后,使用 Install-Module 进行安装。建议指定 -Scope CurrentUser 避免需要管理员权限:

1
2
3
4
5
# 安装指定模块到当前用户范围
Install-Module -Name 'PSScriptAnalyzer' -Scope CurrentUser -Repository PSGallery

# 确认安装成功
Get-Module -ListAvailable -Name 'PSScriptAnalyzer' | Select-Object Name, Version, ModuleBase
1
2
3
Name               Version    ModuleBase
---- ------- ----------
PSScriptAnalyzer 1.22.0 C:\Users\user\Documents\PowerShell\Modules\PSScriptAnalyzer\1.22.0

有时候我们需要安装特定版本的模块,比如为了兼容性而回退到旧版本:

1
2
# 查看模块的所有可用版本
Find-Module -Name 'PSScriptAnalyzer' -AllVersions | Select-Object Name, Version | Sort-Object Version -Descending | Select-Object -First 5
1
2
3
4
5
6
7
Name               Version
---- -------
PSScriptAnalyzer 1.22.0
PSScriptAnalyzer 1.21.0
PSScriptAnalyzer 1.20.0
PSScriptAnalyzer 1.19.1
PSScriptAnalyzer 1.19.0
1
2
# 安装指定版本
Install-Module -Name 'PSScriptAnalyzer' -RequiredVersion '1.21.0' -Scope CurrentUser -Force

批量检查和更新模块

在维护多台服务器时,定期检查已安装模块是否有更新是一项重要的运维任务。下面的脚本会扫描所有已安装模块,对比 PowerShell Gallery 上的最新版本,并生成更新报告。

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
# 获取所有已安装模块的当前版本和最新版本
$installedModules = Get-InstalledModule
$updateReport = @()

foreach ($mod in $installedModules) {
$latest = Find-Module -Name $mod.Name -Repository PSGallery -ErrorAction SilentlyContinue

if ($latest -and $latest.Version -ne $mod.Version) {
$updateReport += [PSCustomObject]@{
Name = $mod.Name
CurrentVersion = $mod.Version
LatestVersion = $latest.Version
NeedsUpdate = $true
}
}
else {
$updateReport += [PSCustomObject]@{
Name = $mod.Name
CurrentVersion = $mod.Version
LatestVersion = $mod.Version
NeedsUpdate = $false
}
}
}

$updateReport | Format-Table -AutoSize
1
2
3
4
5
6
Name                CurrentVersion  LatestVersion  NeedsUpdate
---- -------------- ------------- -----------
PSScriptAnalyzer 1.21.0 1.22.0 True
PSSqlite 1.1.0 1.1.0 False
PowerShellGet 2.2.5 2.2.5 False
Pester 5.4.0 5.5.0 True

确认需要更新的模块后,可以使用 Update-Module 进行升级:

1
2
3
4
5
6
7
8
# 更新所有有新版本的模块
foreach ($mod in $updateReport) {
if ($mod.NeedsUpdate) {
Write-Output ("正在更新 {0} 从 {1} 到 {2}..." -f $mod.Name, $mod.CurrentVersion, $mod.LatestVersion)
Update-Module -Name $mod.Name -Scope CurrentUser
Write-Output ("{0} 更新完成。" -f $mod.Name)
}
}
1
2
3
4
正在更新 PSScriptAnalyzer 从 1.21.0 到 1.22.0...
PSScriptAnalyzer 更新完成。
正在更新 Pester 从 5.4.0 到 5.5.0...
Pester 更新完成。

注册私有 NuGet 仓库

在企业环境中,公共的 PowerShell Gallery 并不总是可用(比如受限网络环境),或者我们需要分发内部开发的专用模块。这时可以注册私有 NuGet 仓库,实现内部模块的统一管理。

1
2
3
4
5
6
7
8
9
10
11
12
# 注册企业内部的 NuGet 仓库
$repoParams = @{
Name = 'CompanyPSRepo'
SourceLocation = 'https://nuget.company.com/v3/index.json'
PublishLocation = 'https://nuget.company.com/v3/index.json'
InstallationPolicy = 'Trusted'
}

Register-PSRepository @repoParams

# 验证注册结果
Get-PSRepository -Name 'CompanyPSRepo'
1
2
3
Name                InstallationPolicy   SourceLocation
---- ----------------- --------------
CompanyPSRepo Trusted https://nuget.company.com/v3/index.json

注册完成后,就可以像使用 PSGallery 一样搜索和安装私有仓库中的模块:

1
2
3
4
5
# 搜索私有仓库中的模块
Find-Module -Repository 'CompanyPSRepo'

# 从私有仓库安装模块
Install-Module -Name 'Company.Utils' -Repository 'CompanyPSRepo' -Scope CurrentUser
1
2
3
4
5
Version    Name              Repository      Description
------- ---- ---------- -----------
1.5.0 Company.Utils CompanyPSRepo 公司内部通用运维工具模块
2.0.1 Company.Security CompanyPSRepo 公司安全基线检查模块
1.0.0 Company.Logging CompanyPSRepo 统一日志记录模块

发布模块到仓库

开发完一个 PowerShell 模块后,可以使用 Publish-Module 将其发布到 PowerShell Gallery 或私有仓库。发布前需要准备好模块清单(.psd1 文件),确保元数据完整。

1
2
3
# 查看模块清单信息
$modulePath = 'C:\Modules\MyToolKit'
Test-ModuleManifest -Path "$modulePath\MyToolKit.psd1" | Select-Object Name, Version, Author, Description
1
2
3
4
Name        : MyToolKit
Version : 1.0.0
Author : admin
Description : 日常运维工具集
1
2
3
4
5
6
7
8
9
# 发布模块到私有仓库(使用 API Key 认证)
$publishParams = @{
Path = $modulePath
Repository = 'CompanyPSRepo'
NuGetApiKey = 'your-api-key-here'
Verbose = $true
}

Publish-Module @publishParams
1
2
3
4
VERBOSE: 正在准备要发布的模块 'MyToolKit' (版本 1.0.0)...
VERBOSE: 正在创建 NuGet 包...
VERBOSE: 正在将包推送到 'https://nuget.company.com/v3/index.json'...
VERBOSE: 模块 'MyToolKit' (版本 1.0.0) 发布成功。

发布完成后,其他团队成员就可以通过 Find-ModuleInstall-Module 获取最新版本。

注意事项

  1. PowerShellGet 版本差异:PowerShellGet v2(随 Windows 自带)和 v3(需要单独安装)在 cmdlet 命名和参数上有较大变化。v3 的包名为 Microsoft.PowerShell.PSResourceGet,计划在未来替代 v2。迁移前请确认兼容性。

  2. NuGet 提供程序依赖:首次使用 Install-Module 时,系统可能会提示安装 NuGet 提供程序。在企业环境中建议提前部署,可以通过 Install-PackageProvider -Name NuGet -Force 预装。

  3. 执行策略限制:如果系统执行策略设置为 Restricted,模块安装和脚本执行都会被阻止。建议将执行策略设为 RemoteSignedSet-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

  4. 网络代理问题:在需要通过代理访问外网的环境中,PowerShellGet 默认不会自动使用系统代理。需要显式配置 PowerShellGet 的代理设置,或者使用 -Proxy 参数指定代理地址。

  5. 模块冲突处理:如果系统中存在多个版本的同一模块,PowerShell 默认加载路径中排在最前面的版本。使用 Import-Module 时通过 -RequiredVersion 参数明确指定版本,避免意外加载旧版本导致兼容性问题。

  6. 私有仓库安全:发布模块时使用的 API Key 应妥善保管,切勿硬编码在脚本中。建议使用环境变量或 Azure Key Vault 等安全存储来管理凭据,例如 $apiKey = $env:PSGALLERY_API_KEY

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 技能连载 - PowerShell Gallery 与模块管理

适用于 PowerShell 5.1 及以上版本

PowerShell 的强大很大程度上归功于其丰富的模块生态。PowerShell Gallery 是微软官方的模块仓库,目前托管了超过 4000 个模块,涵盖 Azure 管理、AWS 集成、安全审计、数据库操作等几乎所有领域。掌握模块的查找、安装、发布和版本管理,是每个 PowerShell 用户提升效率的必经之路。

本文将讲解 PowerShell Gallery 的使用技巧、模块的安装与更新策略、私有仓库搭建,以及如何发布自己的模块。

查找与安装模块

PowerShell Gallery 是最大的公共模块仓库,通过 Find-ModuleInstall-Module 命令即可搜索和安装模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 搜索模块(支持通配符)
Find-Module -Name *azure* | Select-Object Name, Version, Description |
Sort-Object Downloads -Descending | Select-Object -First 10 |
Format-Table -AutoSize

# 安装模块(首次安装会提示信任仓库)
Install-Module -Name Az.Compute -Scope CurrentUser

# 安装特定版本
Install-Module -Name Pester -RequiredVersion 5.5.0 -Scope CurrentUser

# 安装预发布版本
Install-Module -Name PSScriptAnalyzer -AllowPrerelease -Scope CurrentUser

# 查看已安装的模块
Get-InstalledModule | Select-Object Name, Version, Repository |
Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
Name                Version  Description
---- ------- -----------
Az 12.0.0 Microsoft Azure PowerShell
Az.Compute 7.0.0 Microsoft Azure PowerShell - Compute ...
AzureAD 2.0.2. Azure Active Directory V2 PowerShell...

Name Version Repository
---- ------- ----------
Az.Compute 7.0.0 PSGallery
Pester 5.5.0 PSGallery

注意:首次使用 Install-Module 时会提示是否信任 PowerShell Gallery 仓库。可以通过 -Force 参数跳过提示,但建议先了解模块内容再安装。

模块版本管理

在团队协作中,统一的模块版本非常重要。不同版本的模块可能有不同的命令签名和行为,导致脚本在不同机器上表现不一致。

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
# 查看已安装模块的所有版本
Get-InstalledModule -Name Az | Select-Object Name, Version, InstalledLocation

# 查看模块可用的所有版本
Find-Module -Name Pester -AllVersions |
Select-Object Version, Description |
Format-Table -AutoSize

# 更新模块到最新版
Update-Module -Name Pester

# 更新所有已安装模块
Get-InstalledModule | ForEach-Object {
try {
Update-Module -Name $_.Name -ErrorAction Stop
Write-Host "已更新:$($_.Name)" -ForegroundColor Green
} catch {
Write-Host "更新失败:$($_.Name) - $($_.Exception.Message)" -ForegroundColor Red
}
}

# 卸载特定版本
Uninstall-Module -Name Pester -RequiredVersion 4.10.0

# 强制安装指定版本(降级)
Install-Module -Name Pester -RequiredVersion 5.4.0 -Force -Scope CurrentUser

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Name  Version InstalledLocation
---- ------- ------------------
Az 11.3.0 C:\Users\admin\Documents\PowerShell\Modules\Az\11.3.0
Az 12.0.0 C:\Users\admin\Documents\PowerShell\Modules\Az\12.0.0

Version Description
------- -----------
5.5.0 Pester provides a framework for running BDD style tests...
5.4.1 Pester provides a framework for running BDD style tests...
5.4.0 Pester provides a framework for running BDD style tests...

已更新:Pester
已更新:PSScriptAnalyzer
更新失败:Az - Module 'Az' is already at the latest version

模块依赖与安装策略

复杂模块通常有大量依赖。例如 Az 模块包含了 70+ 个子模块。理解依赖关系有助于解决安装冲突:

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
# 查看模块的依赖关系
Find-Module -Name Az.Automation |
Select-Object Name, Version,
@{N='Dependencies'; E={$_.Dependencies | ForEach-Object { "$($_.Name)@$($_.Version)" } | Join-String -Separator ', '}}

# 安装时自动处理依赖
Install-Module -Name Az.Automation -Scope CurrentUser
# 会自动安装 Az.Accounts、Az.Resources 等依赖模块

# 检查已安装模块的依赖链
function Get-ModuleDependencyTree {
param([string]$ModuleName)

$module = Get-Module -ListAvailable -Name $ModuleName |
Sort-Object Version -Descending | Select-Object -First 1

$result = [System.Collections.Generic.List[PSObject]]::new()

function ResolveDeps {
param($Mod, [int]$Depth = 0)
$result.Add([PSCustomObject]@{
Module = $Mod.Name
Version = $Mod.Version.ToString()
Depth = $Depth
})
if ($Mod.RequiredModules) {
foreach ($dep in $Mod.RequiredModules) {
ResolveDeps -Mod $dep -Depth ($Depth + 1)
}
}
}

ResolveDeps -Mod $module
$result | Format-Table -AutoSize
}

Get-ModuleDependencyTree -ModuleName Pester

执行结果示例:

1
2
3
4
5
6
7
8
9
10
Module            Version Depth
------ ------- -----
Pester 5.5.0 0
Microsoft.PowerShell.ScriptAnalyzer 1.22.0 1

Module Version Depth
------ ------- -----
Az.Automation 2.0.0 0
Az.Accounts 4.0.0 1
Az.Resources 6.0.0 1

保存模块离线安装

在无外网连接的生产环境中,可以先下载模块,再离线部署:

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
# 下载模块到指定目录(不安装)
Save-Module -Name Az.Compute -Path "C:\OfflineModules" -Repository PSGallery

# 下载特定版本
Save-Module -Name Pester -RequiredVersion 5.5.0 -Path "C:\OfflineModules"

# 批量下载常用模块
$modules = @(
'Az.Accounts', 'Az.Compute', 'Az.Storage', 'Az.Network',
'Pester', 'PSScriptAnalyzer', 'PSReadLine', 'Terminal-Icons'
)

foreach ($mod in $modules) {
Write-Host "下载:$mod" -ForegroundColor Cyan
Save-Module -Name $mod -Path "C:\OfflineModules" -Force
}

# 列出已下载的模块
Get-ChildItem "C:\OfflineModules" -Directory |
ForEach-Object {
$mod = Get-ChildItem $_.FullName -Directory | Select-Object -First 1
[PSCustomObject]@{
Module = $_.Name
Version = $mod.Name
SizeMB = [math]::Round((Get-ChildItem $_.FullName -Recurse | Measure-Object Length -Sum).Sum / 1MB, 2)
}
} | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
下载:Az.Accounts
下载:Az.Compute
下载:Pester

Module Version SizeMB
------ ------- ------
Az.Accounts 4.0.0 12.35
Az.Compute 7.0.0 8.72
Pester 5.5.0 3.45

注意:离线安装时,将下载的模块目录复制到目标机器的 $env:PSModulePath 中的任一路径下(如 $HOME\Documents\PowerShell\Modules\)即可。

注册私有仓库

企业环境中通常需要搭建私有模块仓库,用于存放内部工具模块。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
# 注册基于 NuGet 的私有仓库
$repoParams = @{
Name = 'InternalPSGallery'
SourceLocation = 'https://psgallery.internal.company.com/nuget'
PublishLocation = 'https://psgallery.internal.company.com/nuget'
InstallationPolicy = 'Trusted'
}
Register-PSRepository @repoParams

# 查看所有已注册的仓库
Get-PSRepository | Format-Table Name, SourceLocation, Trusted -AutoSize

# 从私有仓库搜索和安装模块
Find-Module -Repository InternalPSGallery |
Select-Object Name, Version, Description |
Format-Table -AutoSize

Install-Module -Name Company.Utils -Repository InternalPSGallery

# 也可以使用本地文件共享作为仓库
Register-PSRepository -Name LocalShare `
-SourceLocation '\\fileserver\PSModules' `
-PublishLocation '\\fileserver\PSModules' `
-InstallationPolicy Trusted

执行结果示例:

1
2
3
4
5
6
7
8
9
Name                 SourceLocation                           Trusted
---- -------------- -------
PSGallery https://www.powershellgallery.com/api/ False
InternalPSGallery https://psgallery.internal.company.co... True

Name Version Description
---- ------- -----------
Company.Utils 1.5.0 内部通用工具函数库
Company.Logging 2.0.1 统一日志模块

发布自己的模块

开发完模块后,可以发布到 PowerShell Gallery 或私有仓库。发布前需要准备 API Key:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 检查模块清单是否完整
$modulePath = "C:\Projects\MyModule"
Test-ModuleManifest -Path "$modulePath\MyModule.psd1"

# 发布前验证
$publishParams = @{
Path = $modulePath
NuGetApiKey = 'your-api-key-here'
Repository = 'PSGallery'
Verbose = $true
}

# 先做 Dry Run(不实际上传)
# 可以用 -WhatIf(如果模块支持)
Publish-Module @publishParams

# 发布到私有仓库
Publish-Module -Path $modulePath `
-Repository InternalPSGallery `
-NuGetApiKey 'internal-key'

执行结果示例:

1
2
3
4
5
ModuleType Version    Name          ExportedCommands
---------- ------- ---- ----------------
Manifest 1.0.0 MyModule {Get-MyData, Set-MyConfig, ...}

VERBOSE: Successfully published module 'MyModule' to PSGallery

模块自动加载优化

PowerShell 3.0 及以上版本支持模块自动加载——首次使用模块中的命令时自动加载对应模块。但在模块数量众多时,这可能影响启动性能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 查看当前 PSModulePath
$env:PSModulePath -split [System.IO.Path]::PathSeparator

# 禁用自动加载(提高启动速度)
$PSModuleAutoLoadingPreference = 'None'

# 手动导入需要的模块
Import-Module Az.Compute, Pester

# 查看当前已加载的模块
Get-Module | Select-Object Name, Version, ModuleType |
Format-Table -AutoSize

# 查看模块导出的命令
Get-Command -Module Az.Compute |
Select-Object Name, CommandType |
Sort-Object Name |
Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
C:\Users\admin\Documents\PowerShell\Modules
C:\Program Files\PowerShell\Modules
C:\Windows\system32\WindowsPowerShell\v1.0\Modules

Name Version ModuleType
---- ------- ----------
Az.Accounts 4.0.0 Script
Az.Compute 7.0.0 Script
Pester 5.5.0 Script

Name CommandType
---- -----------
Get-AzVM Function
Get-AzVMSize Function
New-AzVM Function
Remove-AzVM Function

注意事项

  1. 安装范围:使用 -Scope CurrentUser 安装到用户目录,无需管理员权限;-Scope AllUsers 安装到系统目录,所有用户可用但需要管理员权限
  2. 仓库信任策略InstallationPolicy 设为 Trusted 时跳过安装确认,仅对可信仓库使用
  3. 版本锁定:团队项目建议在 requirements.psd1 中锁定模块版本,确保所有成员使用相同版本
  4. 预发布版本-AllowPrerelease 参数用于安装测试版本,生产环境不应使用
  5. 模块冲突:不同模块可能导出同名命令,使用 Import-Module -Prefix 添加前缀避免冲突
  6. 清理旧版本:更新模块后旧版本仍保留在磁盘上,定期使用 Uninstall-Module 清理不再需要的版本

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 处理,最终汇总成功和失败数量