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 技能连载 - JSON Schema 验证

适用于 PowerShell 7.0 及以上版本

JSON 已经成为现代配置管理和数据交换的事实标准格式。无论是应用配置文件、CI/CD 流水线定义,还是 REST API 的请求与响应体,JSON 无处不在。然而 JSON 本身并不携带结构约束信息——一个字段是字符串还是数字、哪些字段是必填的、数值的合法范围是什么,这些都需要额外的手段来保证。

JSON Schema(RFC 8927)是一套标准化的 JSON 结构描述规范,可以用声明式的方式定义数据的形状、类型、约束和关系。相比于手写验证逻辑,使用标准的 JSON Schema 具有可复用、可共享、可版本化的优势,而且生态中有大量现成的工具和库。将 JSON Schema 验证集成到 PowerShell 脚本中,可以在自动化流水线的入口处拦截无效数据,避免后续环节出现难以排查的问题。

本文将介绍如何在 PowerShell 中利用 .NET 生态的 JsonSchema.NET 库,实现标准 JSON Schema 验证、高级条件约束以及批量配置文件的验证与报告生成。

定义 Schema 并验证配置文件

首先安装 JsonSchema.NET 库,然后用它来加载标准的 JSON Schema 定义,对配置文件进行结构验证。JsonSchema.NET 是一个纯 .NET 实现的 JSON Schema 验证器,支持 Draft 6、Draft 7 和 Draft 2020-12 版本。

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
# 安装 JsonSchema.NET 库
Install-Module -Name JsonSchema.NET -Scope CurrentUser -Force

# 或者通过 NuGet 安装(适合 CI/CD 环境)
# dotnet add package JsonSchema.NET

# 定义标准的 JSON Schema(Draft 2020-12)
$schemaJson = @"
{
"`$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"title": "应用配置 Schema",
"required": ["appName", "version", "environment", "server"],
"properties": {
"appName": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "应用名称"
},
"version": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
"description": "语义化版本号"
},
"environment": {
"type": "string",
"enum": ["development", "staging", "production"],
"description": "部署环境"
},
"server": {
"type": "object",
"required": ["host", "port"],
"properties": {
"host": {
"type": "string",
"format": "hostname"
},
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"ssl": {
"type": "boolean",
"default": false
}
},
"additionalProperties": false
},
"logging": {
"type": "object",
"properties": {
"level": {
"type": "string",
"enum": ["debug", "info", "warn", "error"],
"default": "info"
},
"path": {
"type": "string",
"format": "uri-reference"
}
}
}
},
"additionalProperties": false
}
"@

# 加载 Schema
$schema = [JsonSchema.JsonSchema]::FromText($schemaJson)

# 准备一份有效的配置
$validConfig = @"
{
"appName": "OrderService",
"version": "3.2.1",
"environment": "production",
"server": {
"host": "api.example.com",
"port": 443,
"ssl": true
},
"logging": {
"level": "warn",
"path": "/var/log/orderservice.log"
}
}
"@

# 验证并输出结果
$results = $schema.Evaluate($validConfig)
Write-Host "有效配置验证结果:$($results.IsValid)" -ForegroundColor Green

# 准备一份无效的配置——故意制造多个问题
$invalidConfig = @"
{
"appName": "",
"version": "3.2",
"environment": "testing",
"server": {
"host": "api.example.com",
"port": 99999,
"extraField": "not-allowed"
}
}
"@

$results = $schema.Evaluate($invalidConfig)
Write-Host "`n无效配置验证结果:$($results.IsValid)" -ForegroundColor Red

# 逐条输出错误详情
foreach ($detail in $results.Details) {
Write-Host (" 路径: {0,-25} 错误: {1}" -f $detail.InstanceLocation, $detail.Message) -ForegroundColor Yellow
}

执行结果示例:

1
2
3
4
5
6
7
8
有效配置验证结果:True

无效配置验证结果:False
路径: /appName 错误: 字符串长度不足,最少需要 1 个字符
路径: /version 错误: 字符串不匹配模式 ^[0-9]+\.[0-9]+\.[0-9]+$
路径: /environment 错误: 值不在枚举列表中
路径: /server/port 错误: 整数值 99999 超过最大值 65535
路径: /server 错误: 存在不允许的额外属性 extraField

Schema 高级特性——条件验证与组合模式

实际业务中,配置规则往往不是扁平的。比如生产环境必须启用 SSL、数据库连接字符串在特定环境下有不同的格式要求。JSON Schema 提供了 if/then/elseallOfoneOfanyOf 等条件组合机制,可以表达复杂的业务约束。

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
# 定义包含条件逻辑的复杂 Schema
$advancedSchemaJson = @"
{
"`$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"title": "部署配置 Schema(含条件规则)",
"required": ["environment", "database", "features"],
"properties": {
"environment": {
"type": "string",
"enum": ["dev", "staging", "production"]
},
"database": { "type": "object" },
"features": { "type": "object" }
},
"allOf": [
{
"if": {
"properties": {
"environment": { "const": "production" }
}
},
"then": {
"properties": {
"database": {
"required": ["connectionString", "maxPoolSize", "sslMode"],
"properties": {
"connectionString": {
"type": "string",
"minLength": 10
},
"maxPoolSize": {
"type": "integer",
"minimum": 10,
"maximum": 200
},
"sslMode": {
"type": "string",
"enum": ["require", "verify-ca", "verify-full"]
}
}
},
"features": {
"required": ["monitoring", "errorReporting"],
"properties": {
"monitoring": { "type": "boolean", "const": true },
"errorReporting": { "type": "boolean", "const": true }
}
}
}
}
},
{
"if": {
"properties": {
"environment": { "const": "dev" }
}
},
"then": {
"properties": {
"database": {
"required": ["connectionString"],
"properties": {
"connectionString": { "type": "string" },
"maxPoolSize": { "type": "integer", "default": 5 }
}
}
}
}
}
],
"oneOf": [
{
"properties": {
"features": {
"properties": {
"cacheProvider": { "const": "redis" }
},
"required": ["redisHost"]
}
}
},
{
"properties": {
"features": {
"properties": {
"cacheProvider": { "const": "memory" }
}
}
}
}
]
}
"@

$advSchema = [JsonSchema.JsonSchema]::FromText($advancedSchemaJson)

# 测试:生产环境缺少必填字段和监控开关
$prodConfig = @"
{
"environment": "production",
"database": {
"connectionString": "Host=localhost;Database=mydb",
"maxPoolSize": 5
},
"features": {
"monitoring": false,
"cacheProvider": "redis"
}
}
"@

$results = $advSchema.Evaluate($prodConfig)

Write-Host "=== 生产环境配置验证报告 ===" -ForegroundColor Cyan
Write-Host "是否通过:$($results.IsValid)"
Write-Host "`n验证错误明细:"
$index = 0
foreach ($detail in $results.Details) {
$index++
Write-Host (" [{0}] 路径: {1}" -f $index, $detail.InstanceLocation) -ForegroundColor Yellow
Write-Host (" 描述: {0}" -f $detail.Message) -ForegroundColor Gray
}

# 测试:开发环境的简化配置(应该通过)
$devConfig = @"
{
"environment": "dev",
"database": {
"connectionString": "Host=localhost;Database=devdb"
},
"features": {
"cacheProvider": "memory"
}
}
"@

$devResults = $advSchema.Evaluate($devConfig)
Write-Host "`n=== 开发环境配置验证报告 ===" -ForegroundColor Cyan
Write-Host "是否通过:$($devResults.IsValid)" -ForegroundColor Green

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
=== 生产环境配置验证报告 ===
是否通过:False

验证错误明细:
[1] 路径: /database
描述: 缺少必填属性 sslMode
[2] 路径: /database/maxPoolSize
描述: 整数值 5 低于最小值 10
[3] 路径: /features/monitoring
描述: 值必须为 true
[4] 路径: /features
描述: 缺少必填属性 errorReporting
[5] 路径: /features
描述: 缺少必填属性 redisHost(cacheProvider 为 redis 时必需)

=== 开发环境配置验证报告 ===
是否通过:True

批量验证与错误报告生成

在管理多个环境、多个微服务配置文件的场景下,手动逐个验证效率太低。下面的脚本实现了批量扫描指定目录下的 JSON 配置文件,使用统一的 Schema 进行验证,并生成结构化的验证报告。

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
function Invoke-BatchSchemaValidation {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$ConfigDirectory,

[Parameter(Mandatory)]
[string]$SchemaFile,

[string]$ReportPath
)

# 加载 Schema
$schemaText = Get-Content $SchemaFile -Raw
$schema = [JsonSchema.JsonSchema]::FromText($schemaText)

# 收集所有 JSON 配置文件
$configFiles = Get-ChildItem -Path $ConfigDirectory -Filter "*.json" -Recurse
Write-Host ("发现 {0} 个配置文件,开始批量验证..." -f $configFiles.Count) -ForegroundColor Cyan

# 验证结果集合
$report = [System.Collections.Generic.List[PSObject]]::new()
$passCount = 0
$failCount = 0

foreach ($file in $configFiles) {
$relativePath = $file.FullName.Replace($ConfigDirectory, "").TrimStart("\", "/")
Write-Host (" 验证: {0} ... " -f $relativePath) -NoNewline

try {
$configText = Get-Content $file.FullName -Raw
$results = $schema.Evaluate($configText)

if ($results.IsValid) {
Write-Host "通过" -ForegroundColor Green
$passCount++
$report += [PSCustomObject]@{
File = $relativePath
Status = "Pass"
ErrorCount = 0
Errors = ""
ValidatedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
}
} else {
Write-Host "失败" -ForegroundColor Red
$failCount++
$errorList = ($results.Details | ForEach-Object {
"{0} => {1}" -f $_.InstanceLocation, $_.Message
}) -join "; "

$report += [PSCustomObject]@{
File = $relativePath
Status = "Fail"
ErrorCount = $results.Details.Count
Errors = $errorList
ValidatedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
}
}
} catch {
Write-Host "异常" -ForegroundColor Magenta
$failCount++
$report += [PSCustomObject]@{
File = $relativePath
Status = "Error"
ErrorCount = 1
Errors = $_.Exception.Message
ValidatedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
}
}
}

# 输出汇总信息
Write-Host "`n========== 验证汇总 ==========" -ForegroundColor Cyan
Write-Host (" 总文件数:{0}" -f $configFiles.Count)
Write-Host (" 通过:{0}" -f $passCount) -ForegroundColor Green
Write-Host (" 失败:{0}" -f $failCount) -ForegroundColor Red
Write-Host (" 通过率:{0:P1}" -f ($passCount / $configFiles.Count))

# 导出报告
if ($ReportPath) {
$report | ConvertTo-Json -Depth 5 | Out-File $ReportPath -Encoding UTF8
Write-Host ("`n验证报告已保存至:{0}" -f $ReportPath) -ForegroundColor Green
}

return $report
}

# 执行批量验证
$validationResults = Invoke-BatchSchemaValidation `
-ConfigDirectory "C:\MyApp\Configs" `
-SchemaFile "C:\MyApp\Schemas\app-config.schema.json" `
-ReportPath "C:\MyApp\Reports\validation-report.json"

# 针对失败项生成修复建议
$failures = $validationResults | Where-Object { $_.Status -ne "Pass" }
if ($failures) {
Write-Host "`n========== 需要修复的文件 ==========" -ForegroundColor Yellow
foreach ($failure in $failures) {
Write-Host (" [{0}] {1}" -f $failure.Status, $failure.File) -ForegroundColor Yellow
Write-Host (" 错误数: {0}" -f $failure.ErrorCount)
foreach ($err in $failure.Errors -split "; ") {
Write-Host (" - {0}" -f $err) -ForegroundColor DarkGray
}
}
}

执行结果示例:

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
发现 6 个配置文件,开始批量验证...
验证: dev\app-settings.json ... 通过
验证: dev\cache-config.json ... 通过
验证: staging\app-settings.json ... 通过
验证: staging\cache-config.json ... 失败
验证: production\app-settings.json ... 失败
验证: production\cache-config.json ... 通过

========== 验证汇总 ==========
总文件数:6
通过:4
失败:2
通过率:66.7%

验证报告已保存至:C:\MyApp\Reports\validation-report.json

========== 需要修复的文件 ==========
[Fail] staging\cache-config.json
错误数: 1
- /server/port => 整数值 99999 超过最大值 65535
[Fail] production\app-settings.json
错误数: 3
- /database/sslMode => 缺少必填属性 sslMode
- /features/monitoring => 值必须为 true
- /features/errorReporting => 缺少必填属性 errorReporting

注意事项

  1. 库的选择:JsonSchema.NET 是纯 C# 实现,跨平台兼容性好。如果项目中已经依赖了 Newtonsoft.Json,也可以选择 Newtonsoft.Json.Schema,但它需要商业授权才能在生产环境免受限制
  2. Schema 版本兼容:不同版本的 JSON Schema(Draft-04 / Draft-07 / 2020-12)在特性支持上有差异,编写 Schema 时务必在 $schema 字段中声明正确的版本标识
  3. 大文件性能:对超过 10MB 的 JSON 文件进行完整 Schema 验证可能耗时较长,建议在 CI/CD 中仅对关键字段做校验,或采用增量验证策略
  4. 错误路径解读:验证错误中的 InstanceLocation 使用 JSON Pointer 格式(如 /server/port),可以精确定位到出错的节点,编写自动化修复脚本时应充分利用此信息
  5. Schema 即文档:维护良好的 JSON Schema 不仅是验证工具,还可以配合文档生成器(如 @adobe/jsonschema2md)自动生成配置参考手册,减少人工维护成本
  6. CI/CD 集成:建议将 Schema 验证作为部署流水线的前置步骤(gate check),任何配置文件变更必须通过验证才能合并到主分支,从源头杜绝无效配置上线

PowerShell 技能连载 - DSC v3 配置管理

适用于 PowerShell 7.0 及以上版本(跨平台)

Desired State Configuration(DSC)一直是 Windows 生态中基础设施即代码(IaC)的重要支柱。从 DSC v1 基于 MOF 的推送模式,到 DSC v2 引入的拉取服务,再到如今全新的 DSC v3,微软对配置管理的理念发生了根本性的转变。DSC v3 完全抛弃了对 WMF(Windows Management Framework)的依赖,转而成为一个独立的跨平台工具,支持 Windows、Linux 甚至 macOS。

DSC v3 的核心变化在于它不再绑定 PowerShell 作为运行时。新的 DSC 引擎(dsc)是原生可执行文件,配置文档采用 YAML 或 JSON 格式编写,资源可以通过任何语言实现(不再局限于 PowerShell 模块)。这使得 DSC v3 能够更好地融入现代 DevOps 流水线,与 Ansible、Chef、Terraform 等工具协同工作。同时,DSC v3 还引入了基于 JSON Schema 的配置验证机制,在应用配置之前就能捕获语法和结构错误。

对于 PowerShell 用户而言,DSC v3 带来的好消息是你仍然可以用 PowerShell 编写自定义 DSC 资源,同时享受新版引擎带来的性能提升和更好的错误报告。下面我们通过几个实际示例来体验 DSC v3 的核心用法。

安装 DSC v3

DSC v3 是独立发布的工具,不随 PowerShell 自带。你可以通过多种方式安装。

1
2
3
4
5
6
7
8
9
10
11
12
# 使用 winget 安装 DSC v3(Windows)
winget install Microsoft.DSC v3

# 或者手动下载安装
$release = Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/DSC/releases/latest'
$asset = $release.assets | Where-Object { $_.name -like '*-x64*' -and $_.name -like '*.msi' }
$downloadPath = Join-Path $env:TEMP $asset.name
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $downloadPath
Write-Host "已下载到: $downloadPath"

# 验证安装
dsc --version

安装完成后,可以通过以下输出确认版本信息:

1
3.0.0

编写 YAML 配置文档

DSC v3 采用声明式的配置文档,用 YAML 或 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
# 创建 DSC v3 配置文档
$configContent = @"
schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
resources:
- name: Ensure temp directory
type: Microsoft.DSC/File
properties:
destinationPath: /tmp/dsc-demo
ensure: present
attributes:
- directory

- name: Set editor environment variable
type: Microsoft.DSC/Environment
properties:
name: EDITOR
value: code
ensure: present

- name: Install Git
type: Microsoft.DSC/Package
properties:
name: git
ensure: present
"@

$configPath = Join-Path $env:TEMP 'dsc-config.dsc.yaml'
$configContent | Set-Content -Path $configPath -Encoding UTF8
Write-Host "配置文档已写入: $configPath"

执行结果示例:

1
配置文档已写入: /tmp/dsc-config.dsc.yaml

验证和测试配置

在真正应用配置之前,先用 dsc config test 检查当前系统是否已经符合期望状态。这是一个安全且不会产生副作用的操作。

1
2
3
4
5
6
7
8
9
10
11
# 验证配置文档的语法正确性
dsc config validate --path $configPath

# 测试当前系统状态与期望配置的差异
$testResult = dsc config test --path $configPath 2>&1
$resultObj = $testResult | ConvertFrom-Json

foreach ($resource in $resultObj.results) {
$status = if ($resource.inDesiredState) { '符合' } else { '不符合' }
Write-Host ("资源 [{0}] 状态: {1}" -f $resource.name, $status)
}

执行结果示例:

1
2
3
资源 [Ensure temp directory] 状态: 不符合
资源 [Set editor environment variable] 状态: 不符合
资源 [Install Git] 状态: 符合

应用配置并获取状态

确认测试结果后,使用 dsc config set 将系统推进到期望状态,再用 dsc config get 确认最终结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 应用配置(将系统设置为期望状态)
$setResult = dsc config set --path $configPath 2>&1
$setObj = $setResult | ConvertFrom-Json

foreach ($op in $setObj.operations) {
Write-Host ("操作: {0} -> {1}" -f $op.resourceName, $op.operation)
}

Write-Host "`n--- 获取当前配置状态 ---"

# 获取当前系统的实际状态
$getResult = dsc config get --path $configPath 2>&1
$getObj = $getResult | ConvertFrom-Json

foreach ($resource in $getObj.results) {
$state = if ($resource.inDesiredState) { 'OK' } else { 'DRIFT' }
Write-Host ("{0}: {1}" -f $resource.name, $state)
}

执行结果示例:

1
2
3
4
5
6
7
操作: Ensure temp directory -> set
操作: Set editor environment variable -> set

--- 获取当前配置状态 ---
Ensure temp directory: OK
Set editor environment variable: OK
Install Git: OK

编写基于 PowerShell 的自定义 DSC 资源

DSC v3 的一大改进是自定义资源不再需要实现复杂的 Cmdlet 类。你只需要提供一个导出 JSON 清单的 PowerShell 脚本,然后实现 getsettest 三个操作即可。

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
# 创建自定义 DSC 资源的目录结构
$resourceDir = Join-Path $env:TEMP 'DSCResources/MyCustomService'
New-Item -ItemType Directory -Path $resourceDir -Force | Out-Null

# 资源清单文件 (resource.manifest.json)
$manifest = @{
schemaVersion = '2024/04'
type = 'MyCustom/Service'
version = '1.0.0'
description = '管理 Windows 服务的自定义 DSC 资源'
capabilities = @('get', 'set', 'test')
filePath = './service-resource.ps1'
} | ConvertTo-Json -Depth 5

$manifest | Set-Content (Join-Path $resourceDir 'resource.manifest.json')

# 资源实现脚本 (service-resource.ps1)
$scriptContent = @'
param(
[string]$Operation,
[string]$InputData
)

$config = $InputData | ConvertFrom-Json

switch ($Operation) {
'get' {
$service = Get-Service -Name $config.name -ErrorAction SilentlyContinue
if ($service) {
@{ name = $config.name; status = $service.Status; startType = $service.StartType } |
ConvertTo-Json -Compress
} else {
@{ name = $config.name; status = 'NotFound'; startType = 'Unknown' } |
ConvertTo-Json -Compress
}
}
'test' {
$service = Get-Service -Name $config.name -ErrorAction SilentlyContinue
$desiredState = $config.ensure -eq 'present'
$actualState = ($null -ne $service -and $service.Status -eq 'Running')
@{ inDesiredState = ($desiredState -eq $actualState) } |
ConvertTo-Json -Compress
}
'set' {
if ($config.ensure -eq 'present') {
Set-Service -Name $config.name -StartupType Automatic
Start-Service -Name $config.name
} else {
Stop-Service -Name $config.name -Force
Set-Service -Name $config.name -StartupType Disabled
}
@{ changed = $true } | ConvertTo-Json -Compress
}
}
'@

$scriptContent | Set-Content (Join-Path $resourceDir 'service-resource.ps1')
Write-Host "自定义资源已创建于: $resourceDir"
Write-Host "清单文件: resource.manifest.json"
Write-Host "实现脚本: service-resource.ps1"

执行结果示例:

1
2
3
自定义资源已创建于: /tmp/DSCResources/MyCustomService
清单文件: resource.manifest.json
实现脚本: service-resource.ps1

注意事项

  1. DSC v3 与旧版不兼容:DSC v3 使用全新的配置格式(YAML/JSON),无法直接复用 DSC v1/v2 的 MOF 配置。迁移时需要重写配置文档,但资源逻辑通常可以保留。

  2. 资源清单是必须的:每个自定义资源都必须提供 resource.manifest.json,其中声明资源类型、版本、支持的操作(getsettestdelete)以及入口脚本的路径。缺少清单会导致 DSC 引擎无法发现该资源。

  3. 跨平台差异:虽然 DSC v3 支持多平台,但并非所有内置资源在每个操作系统上都可用。例如 Microsoft.DSC/Package 在 Linux 上使用系统包管理器(apt/yum),在 Windows 上则使用 winget 或 MSI。编写配置时要注意目标平台。

  4. 幂等性是关键set 操作必须是幂等的——多次执行 set 不应产生副作用。确保你的 test 操作能准确检测当前状态,set 操作只在需要变更时才执行修改。

  5. JSON 输出格式:自定义资源的 PowerShell 脚本必须输出有效的 JSON(使用 ConvertTo-Json),且不能有多余的控制台输出(Write-Host 等),否则会导致 DSC 引擎解析失败。调试时可以重定向到文件。

  6. Schema 验证要先于部署:始终先运行 dsc config validate 检查配置文档的语法和结构,再执行 testset。这可以在早期捕获拼写错误和结构问题,避免运行时意外。

PowerShell 技能连载 - 配置管理模式

适用于 PowerShell 5.1 及以上版本

几乎所有脚本都涉及配置管理——数据库连接字符串、API 端点、超时设置、日志级别。如何存储、加载、验证、合并配置,直接影响脚本的可维护性和灵活性。硬编码的配置难以适应不同环境,缺少验证的配置导致运行时错误,没有合并机制的配置无法覆盖默认值。

本文将讲解 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
# 方式 1:JSON 配置文件
$jsonConfig = @"
{
"appName": "MyApp",
"version": "2.5.0",
"database": {
"server": "prod-db01",
"port": 5432,
"name": "myapp_production",
"timeout": 30
},
"logging": {
"level": "Warning",
"path": "C:\\Logs\\MyApp",
"maxSizeMB": 100
},
"features": {
"enableCache": true,
"enableMetrics": true,
"maxRetries": 3
}
}
"@

$jsonConfig | Set-Content "C:\MyApp\config.json" -Encoding UTF8

# 加载 JSON 配置
$config = Get-Content "C:\MyApp\config.json" -Raw | ConvertFrom-Json
Write-Host "应用名:$($config.appName)"
Write-Host "数据库:$($config.database.server):$($config.database.port)"
Write-Host "日志级别:$($config.logging.level)"

# 方式 2:PSD1 配置文件(PowerShell 数据文件)
$psd1Config = @"
@{
AppName = 'MyApp'
Version = '2.5.0'
Database = @{
Server = 'prod-db01'
Port = 5432
Name = 'myapp_production'
Timeout = 30
}
Logging = @{
Level = 'Warning'
Path = 'C:\Logs\MyApp'
MaxSizeMB = 100
}
}
"@

$psd1Config | Set-Content "C:\MyApp\config.psd1" -Encoding UTF8

# 加载 PSD1 配置
$psd1Data = Invoke-Expression (Get-Content "C:\MyApp\config.psd1" -Raw)
Write-Host "`nPSD1 数据库配置:$($psd1Data.Database.Server)"

# 方式 3:环境变量配置
$env:MYAPP_DB_SERVER = "prod-db01"
$env:MYAPP_DB_PORT = "5432"
$env:MYAPP_LOG_LEVEL = "Debug"
Write-Host "环境变量配置:$env:MYAPP_DB_SERVER`:$env:MYAPP_DB_PORT"

执行结果示例:

1
2
3
4
5
6
应用名:MyApp
数据库:prod-db01:5432
日志级别:Warning

PSD1 数据库配置:prod-db01
环境变量配置:prod-db01:5432

多层配置合并

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
# 多层配置:默认值 < 配置文件 < 环境变量 < 命令行参数
function Get-ApplicationConfig {
param(
[string]$ConfigPath = "C:\MyApp\config.json",
[string]$Environment = "production"
)

# 第一层:默认配置
$defaults = @{
appName = "MyApp"
version = "1.0.0"
database = @{
server = "localhost"
port = 5432
name = "myapp"
timeout = 30
}
logging = @{
level = "Info"
path = "C:\Logs\MyApp"
maxSizeMB = 50
}
features = @{
enableCache = $true
enableMetrics = $false
maxRetries = 3
}
}

# 第二层:配置文件
if (Test-Path $ConfigPath) {
$fileConfig = Get-Content $ConfigPath -Raw | ConvertFrom-Json
$defaults = Merge-Config $defaults (ConvertTo-Hashtable $fileConfig)
Write-Host "已加载配置文件:$ConfigPath" -ForegroundColor Green
}

# 第三层:环境特定配置
$envConfigPath = $ConfigPath -replace '\.json$', ".$Environment.json"
if (Test-Path $envConfigPath) {
$envConfig = Get-Content $envConfigPath -Raw | ConvertFrom-Json
$defaults = Merge-Config $defaults (ConvertTo-Hashtable $envConfig)
Write-Host "已加载环境配置:$envConfigPath" -ForegroundColor Green
}

# 第四层:环境变量覆盖
$envPrefix = "MYAPP_"
Get-ChildItem Env: | Where-Object { $_.Name -like "$envPrefix*" } | ForEach-Object {
$key = $_.Name.Substring($envPrefix.Length).Replace("_", ".")
Set-NestedValue -Config $defaults -Path $key -Value $_.Value
}

return $defaults
}

# 辅助函数:深度合并
function Merge-Config {
param($Base, $Override)

$result = @{}
foreach ($key in $Base.Keys) { $result[$key] = $Base[$key] }

foreach ($key in $Override.Keys) {
if ($result[$key] -is [hashtable] -and $Override[$key] -is [hashtable]) {
$result[$key] = Merge-Config $result[$key] $Override[$key]
} else {
$result[$key] = $Override[$key]
}
}
return $result
}

function ConvertTo-Hashtable {
param([Parameter(Mandatory)][object]$InputObject)
if ($InputObject -is [System.Management.Automation.PSCustomObject]) {
$hash = @{}
$InputObject.PSObject.Properties | ForEach-Object {
$hash[$_.Name] = ConvertTo-Hashtable $_.Value
}
return $hash
}
return $InputObject
}

function Set-NestedValue {
param($Config, [string]$Path, $Value)

$keys = $Path -split '\.'
$current = $Config
for ($i = 0; $i -lt $keys.Count - 1; $i++) {
if (-not $current[$keys[$i]]) {
$current[$keys[$i]] = @{}
}
$current = $current[$keys[$i]]
}
# 尝试类型转换
$lastKey = $keys[-1]
if ($Value -match '^\d+$') { $Value = [int]$Value }
elseif ($Value -match '^(true|false)$') { $Value = $Value -eq 'true' }
$current[$lastKey] = $Value
}

# 使用多层配置
$config = Get-ApplicationConfig -ConfigPath "C:\MyApp\config.json" -Environment "production"
Write-Host "`n最终配置:" -ForegroundColor Cyan
Write-Host " 应用:$($config.appName) v$($config.version)"
Write-Host " 数据库:$($config.database.server):$($config.database.port)/$($config.database.name)"
Write-Host " 日志级别:$($config.logging.level)"

执行结果示例:

1
2
3
4
5
已加载配置文件:C:\MyApp\config.json
最终配置:
应用:MyApp v2.5.0
数据库:prod-db01:5432/myapp_production
日志级别:Warning

配置验证

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
# 配置验证器
function Test-ConfigSchema {
param(
[Parameter(Mandatory)]
[hashtable]$Config,

[hashtable]$Schema
)

$errors = @()

foreach ($key in $Schema.Keys) {
$rule = $Schema[$key]

# 检查必填项
if ($rule.Required -and -not $Config.ContainsKey($key)) {
$errors += "缺少必填配置:$key"
continue
}

if (-not $Config.ContainsKey($key)) { continue }

$value = $Config[$key]

# 类型检查
if ($rule.Type -and $value -isnot $rule.Type) {
$errors += "$key 类型错误:期望 $($rule.Type.Name),实际 $($value.GetType().Name)"
}

# 范围检查
if ($rule.Min -and $value -lt $rule.Min) {
$errors += "$key$value 小于最小值 $($rule.Min)"
}
if ($rule.Max -and $value -gt $rule.Max) {
$errors += "$key$value 大于最大值 $($rule.Max)"
}

# 枚举检查
if ($rule.ValidValues -and $value -notin $rule.ValidValues) {
$errors += "$key 值 '$value' 不在允许列表中:$($rule.ValidValues -join ', ')"
}

# 正则检查
if ($rule.Pattern -and $value -notmatch $rule.Pattern) {
$errors += "$key 值 '$value' 不匹配模式:$($rule.Pattern)"
}
}

if ($errors) {
Write-Host "配置验证失败:" -ForegroundColor Red
$errors | ForEach-Object { Write-Host " ! $_" -ForegroundColor Red }
return $false
}

Write-Host "配置验证通过" -ForegroundColor Green
return $true
}

# 定义配置模式
$schema = @{
appName = @{ Required = $true; Type = [string] }
version = @{ Required = $true; Pattern = '^\d+\.\d+\.\d+$' }
database = @{
Required = $true
}
logging = @{
Required = $false
}
}

$validConfig = @{
appName = "MyApp"
version = "2.5.0"
database = @{ server = "localhost" }
}

Test-ConfigSchema -Config $validConfig -Schema $schema

$invalidConfig = @{
appName = 123
version = "invalid"
}

Test-ConfigSchema -Config $invalidConfig -Schema $schema

执行结果示例:

1
2
3
4
5
配置验证通过
配置验证失败:
! appName 类型错误:期望 String,实际 Int32
! version 值 'invalid' 不匹配模式:^\d+\.\d+\.\d+$
! 缺少必填配置:database

注意事项

  1. 敏感配置:密码、Token 等敏感配置不应存储在配置文件中,使用环境变量或密钥管理服务
  2. 配置优先级:明确文档化配置层的优先级,让用户知道哪个配置源优先生效
  3. 默认值:所有配置都应有合理的默认值,确保脚本在缺少配置文件时仍能运行
  4. 配置热加载:长时间运行的服务应支持配置热加载,无需重启
  5. PSD1 安全Invoke-Expression 加载 PSD1 存在安全风险,生产环境使用 Import-PowerShellDataFile
  6. 环境隔离:不同环境(开发、测试、生产)的配置应完全独立,避免误操作

PowerShell 技能连载 - Desired State Configuration 实战

适用于 PowerShell 5.1 及以上版本(Windows),DSC 需要 Windows 管理框架

Desired State Configuration(DSC)是 PowerShell 的声明式配置管理框架——你定义系统”应该是什么状态”,DSC 负责将系统调整到目标状态。与命令式脚本(”执行这些步骤”)不同,DSC 声明期望结果(”安装这些角色、创建这些文件、启动这些服务”),引擎自动判断需要做什么。这使得配置管理具有幂等性和可审计性。

本文将讲解 DSC 的核心概念、配置编写,以及实用的服务器配置场景。

DSC 配置基础

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
# DSC 配置块
Configuration WebServerSetup {
param([string[]]$MachineName = "localhost")

Node $MachineName {
# 安装 IIS 角色
WindowsFeature IIS {
Ensure = "Present"
Name = "Web-Server"
IncludeAllSubFeature = $true
}

# 确保 Web 管理工具安装
WindowsFeature WebMgmtTools {
Ensure = "Present"
Name = "Web-Mgmt-Tools"
DependsOn = "[WindowsFeature]IIS"
}

# 创建网站目录
File WebsiteRoot {
Ensure = "Present"
DestinationPath = "C:\inetpub\MyApp"
Type = "Directory"
}

# 复制默认页面
File DefaultPage {
Ensure = "Present"
DestinationPath = "C:\inetpub\MyApp\default.htm"
Contents = @"
<!DOCTYPE html>
<html>
<head><title>MyApp</title></head>
<body><h1>MyApp Running</h1><p>DSC Configured</p></body>
</html>
"@
DependsOn = "[File]WebsiteRoot"
}

# 启动 W3SVC 服务
Service W3SVC {
Name = "W3SVC"
State = "Running"
StartupType = "Automatic"
DependsOn = "[WindowsFeature]IIS"
}
}
}

# 编译配置(生成 MOF 文件)
WebServerSetup -OutputPath "C:\DSC\WebServer"

Write-Host "配置已编译,MOF 文件位于 C:\DSC\WebServer" -ForegroundColor Green

执行结果示例:

1
2
3
4
5
6
配置已编译,MOF 文件位于 C:\DSC\WebServer
Directory: C:\DSC\WebServer

Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2025-07-24 08:30:15 3456 localhost.mof

应用与测试配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 应用 DSC 配置(推送模式)
Start-DscConfiguration -Path "C:\DSC\WebServer" -Wait -Verbose -Force

# 检查配置状态
Test-DscConfiguration -Path "C:\DSC\WebServer"

# 查看当前配置状态详情
Get-DscConfigurationStatus |
Select-Object Status, StartDate, Type, Mode |
Format-List

# 获取当前 DSC 配置的实际值
Get-DscConfiguration |
Select-Object ConfigurationName, ResourceInstanceName, ResourceModuleName |
Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
PSComputerName  ResourceName  InDesiredState
localhost WindowsFeature True
localhost WindowsFeature True
localhost File True
localhost File True
localhost Service True

Status : Success
StartDate : 2025-07-24 08:30:15
Type : Initial
Mode : Push

完整服务器配置

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
# 综合服务器配置 DSC
Configuration ProductionWebServer {
param(
[string[]]$NodeName = "localhost",
[string]$WebsitePath = "C:\inetpub\MyApp",
[string]$LogPath = "C:\Logs\MyApp"
)

Import-DscResource -ModuleName PSDesiredStateConfiguration
Import-DscResource -ModuleName NetworkingDsc
Import-DscResource -ModuleName xWebAdministration

Node $NodeName {
# Windows 功能
WindowsFeature WebServer {
Ensure = "Present"
Name = "Web-Server"
}

WindowsFeature NetFramework {
Ensure = "Present"
Name = "NET-Framework-45-Core"
}

# 目录结构
File WebsiteDir {
Ensure = "Present"
DestinationPath = $WebsitePath
Type = "Directory"
}

File LogDir {
Ensure = "Present"
DestinationPath = $LogPath
Type = "Directory"
}

# 注册表设置
Registry MaxUrlLength {
Ensure = "Present"
Key = "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\HTTP\Parameters"
ValueName = "UrlSegmentMaxLength"
ValueData = "0"
ValueType = "Dword"
}

# Windows 防火墙规则
Script FirewallRuleHTTP {
GetScript = { @{ Result = (Get-NetFirewallRule -DisplayName "HTTP" -ErrorAction SilentlyContinue).DisplayName } }
SetScript = { New-NetFirewallRule -DisplayName "HTTP" -Direction Inbound -Protocol TCP -LocalPort 80 -Action Allow }
TestScript = { (Get-NetFirewallRule -DisplayName "HTTP" -ErrorAction SilentlyContinue) -ne $null }
}

# 服务配置
Service W3SVC {
Name = "W3SVC"
State = "Running"
StartupType = "Automatic"
}

Service WAS {
Name = "WAS"
State = "Running"
StartupType = "Automatic"
}

# 本地配置管理器设置
LocalConfigurationManager {
ConfigurationMode = "ApplyAndAutoCorrect"
ConfigurationModeFrequencyMins = 30
RefreshMode = "Push"
RebootNodeIfNeeded = $true
}
}
}

# 编译
ProductionWebServer -OutputPath "C:\DSC\ProductionWeb" -NodeName "SRV-WEB-01"

# 应用
Start-DscConfiguration -Path "C:\DSC\ProductionWeb" -Wait -Force

Write-Host "生产服务器配置已应用" -ForegroundColor Green

执行结果示例:

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
28
29
30
31
32
33
34
35
36
# 定期检查配置漂移
function Test-DscDrift {
param([string]$ComputerName = "localhost")

$result = Test-DscConfiguration -ComputerName $ComputerName -Detailed

$inDesired = ($result.ResourcesInDesiredState | Measure-Object).Count
$notInDesired = ($result.ResourcesNotInDesiredState | Measure-Object).Count

$report = [PSCustomObject]@{
Computer = $ComputerName
Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
InDesired = $inDesired
NotInDesired = $notInDesired
Compliant = $notInDesired -eq 0
}

if ($notInDesired -gt 0) {
Write-Host "[$($report.Timestamp)] $ComputerName : 漂移!$notInDesired 项不符合" -ForegroundColor Red
$result.ResourcesNotInDesiredState | ForEach-Object {
Write-Host " - $($_.ResourceId)" -ForegroundColor Yellow
}
} else {
Write-Host "[$($report.Timestamp)] $ComputerName : 合规($inDesired 项)" -ForegroundColor Green
}

return $report
}

# 批量检查
$servers = @("SRV-WEB-01", "SRV-WEB-02", "SRV-WEB-03")
$reports = foreach ($server in $servers) {
Test-DscDrift -ComputerName $server
}

$reports | Format-Table Computer, InDesired, NotInDesired, Compliant -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
[2025-07-24 09:00:00] SRV-WEB-01 : 合规(8 项)
[2025-07-24 09:00:02] SRV-WEB-02 : 漂移!2 项不符合
- [Service]W3SVC
- [File]DefaultPage
[2025-07-24 09:00:04] SRV-WEB-03 : 合规(8 项)

Computer InDesired NotInDesired Compliant
-------- --------- ------------ ---------
SRV-WEB-01 8 0 True
SRV-WEB-02 6 2 False
SRV-WEB-03 8 0 True

注意事项

  1. 推送 vs 拉取:推送模式(Push)适合小规模环境,拉取模式(Pull)适合大规模环境
  2. 幂等性:DSC 配置应该可以重复执行而不产生副作用,使用 Test 脚本检测当前状态
  3. MOF 文件:MOF 文件可能包含敏感信息(密码),注意安全存储和传输
  4. 依赖关系:使用 DependsOn 控制资源配置顺序,避免依赖循环
  5. 自定义资源:内置资源不满足需求时,可以创建自定义 DSC 资源模块
  6. AutoCorrect 模式ApplyAndAutoCorrect 模式会自动修复漂移,ApplyOnly 模式只检测不修复

PowerShell 技能连载 - 注册表管理

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

Windows 注册表是系统配置的核心存储——从系统服务配置到用户偏好,从已安装软件列表到启动项管理,几乎所有 Windows 配置都存储在注册表中。虽然图形界面提供了部分设置入口,但大量高级配置只能通过注册表修改。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
# PowerShell 内置的注册表驱动器
Get-PSDrive -PSProvider Registry | Format-Table Name, Root -AutoSize

# 浏览注册表(像文件系统一样)
Set-Location HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion
Get-ChildItem | Select-Object Name -First 10

# 读取注册表值
$productName = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ProductName).ProductName
Write-Host "操作系统:$productName"

$currentBuild = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name CurrentBuild).CurrentBuild
Write-Host "构建号:$currentBuild"

# 读取所有值
$uninstall = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue
$uninstall | Where-Object { $_.DisplayName } |
Select-Object DisplayName, DisplayVersion, Publisher |
Sort-Object DisplayName |
Format-Table -AutoSize | Out-String -Width 100 | Write-Host

# 搜索注册表
function Find-RegistryValue {
param(
[string]$Path = "HKLM:\SOFTWARE",
[string]$SearchValue,
[int]$MaxDepth = 3
)

$results = @()

function Search-Reg {
param([string]$CurrentPath, [int]$Depth)

if ($Depth -gt $MaxDepth) { return }

try {
$item = Get-Item $CurrentPath -ErrorAction Stop
foreach ($val in $item.GetValueNames()) {
$data = $item.GetValue($val)
if ($data -is [string] -and $data -match [regex]::Escape($SearchValue)) {
$results += [PSCustomObject]@{
Path = $CurrentPath
Name = $val
Value = $data
}
}
}

Get-ChildItem $CurrentPath -ErrorAction SilentlyContinue |
ForEach-Object { Search-Reg $_.PSPath ($Depth + 1) }
} catch {}
}

Search-Reg -CurrentPath $Path -Depth 0
return $results
}

# 搜索 PowerShell 相关的注册表值
Find-RegistryValue -Path "HKLM:\SOFTWARE" -SearchValue "PowerShell" -MaxDepth 2 |
Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Name  Root
---- ----
HKLM HKEY_LOCAL_MACHINE
HKCU HKEY_CURRENT_USER

操作系统:Microsoft Windows Server 2022 Standard
构建号:20348

DisplayName DisplayVersion Publisher
------------ -------------- ---------
7-Zip 23.01 (x64) 23.01 Igor Pavlov
Git for Windows 2.45.0 The Git Development Community
PowerShell 7-x64 7.4.2 Microsoft Corporation

Path Name Value
HKLM:\SOFTWARE\Microsoft\PowerShell InstallDir C:\Program Files\PowerShell\7\

注册表修改

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
# 创建注册表项
$newKeyPath = "HKLM:\SOFTWARE\MyApp"
if (-not (Test-Path $newKeyPath)) {
New-Item -Path $newKeyPath -Force | Out-Null
Write-Host "已创建注册表项:$newKeyPath" -ForegroundColor Green
}

# 设置注册表值
New-ItemProperty -Path $newKeyPath -Name "InstallPath" -Value "C:\Program Files\MyApp" -PropertyType String -Force | Out-Null
New-ItemProperty -Path $newKeyPath -Name "Version" -Value "2.5.0" -PropertyType String -Force | Out-Null
New-ItemProperty -Path $newKeyPath -Name "Port" -Value 8080 -PropertyType DWord -Force | Out-Null
New-ItemProperty -Path $newKeyPath -Name "Enabled" -Value 1 -PropertyType DWord -Force | Out-Null

Write-Host "已设置注册表值" -ForegroundColor Green

# 验证
Get-ItemProperty -Path $newKeyPath | Format-List

# 修改现有值
Set-ItemProperty -Path $newKeyPath -Name "Port" -Value 9090
Write-Host "端口已修改为 9090" -ForegroundColor Yellow

# 删除注册表值
Remove-ItemProperty -Path $newKeyPath -Name "Enabled" -ErrorAction SilentlyContinue
Write-Host "已删除 Enabled 值" -ForegroundColor Yellow

# 删除注册表项
Remove-Item -Path $newKeyPath -Recurse -Force
Write-Host "已删除注册表项:$newKeyPath" -ForegroundColor Red

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
已创建注册表项:HKLM:\SOFTWARE\MyApp
已设置注册表值

InstallPath : C:\Program Files\MyApp
Version : 2.5.0
Port : 9090
PSPath : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\MyApp

端口已修改为 9090
已删除 Enabled
已删除注册表项:HKLM:\SOFTWARE\MyApp

系统配置管理

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
# 管理启动项
function Get-StartupItems {
$items = @()

# 当前用户启动项
$userRun = Get-ItemProperty "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" -ErrorAction SilentlyContinue
if ($userRun) {
$userRun.PSObject.Properties | Where-Object {
$_.Name -notin @('PSPath', 'PSParentPath', 'PSChildName', 'PSDrive', 'PSProvider')
} | ForEach-Object {
$items += [PSCustomObject]@{
Scope = "User"
Name = $_.Name
Value = $_.Value
}
}
}

# 系统启动项
$sysRun = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" -ErrorAction SilentlyContinue
if ($sysRun) {
$sysRun.PSObject.Properties | Where-Object {
$_.Name -notin @('PSPath', 'PSParentPath', 'PSChildName', 'PSDrive', 'PSProvider')
} | ForEach-Object {
$items += [PSCustomObject]@{
Scope = "System"
Name = $_.Name
Value = $_.Value
}
}
}

return $items
}

$startupItems = Get-StartupItems
Write-Host "启动项($($startupItems.Count) 个):" -ForegroundColor Cyan
$startupItems | Format-Table -AutoSize

# 禁用/启用 Windows 功能
function Set-WindowsFeatureViaRegistry {
param(
[string]$Feature,
[bool]$Enabled
)

$featureMap = @{
"RemoteDesktop" = @{
Path = "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server"
Name = "fDenyTSConnections"
On = 0
Off = 1
}
"UAC" = @{
Path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"
Name = "EnableLUA"
On = 1
Off = 0
}
"Firewall" = @{
Path = "HKLM:\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\StandardProfile"
Name = "EnableFirewall"
On = 1
Off = 0
}
}

$config = $featureMap[$Feature]
if (-not $config) {
throw "未知功能:$Feature(可用:$($featureMap.Keys -join ', '))"
}

$value = if ($Enabled) { $config.On } else { $config.Off }
Set-ItemProperty -Path $config.Path -Name $config.Name -Value $value -Force
$state = if ($Enabled) { "启用" } else { "禁用" }
Write-Host "已${state} $Feature" -ForegroundColor Green
}

# 查看当前状态
$rdp = (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server").fDenyTSConnections
Write-Host "远程桌面:$(if ($rdp -eq 0) { '已启用' } else { '已禁用' })"

执行结果示例:

1
2
3
4
5
6
7
8
启动项(5 个):
Scope Name Value
----- ---- -----
User OneDrive "C:\Program Files\Microsoft OneDrive\OneDrive.exe"
System SecurityHealth C:\Program Files\Windows Defender\MSASCuiL.exe
System VMware Tray "C:\Program Files\VMware\VMware Tools\trayicon.exe"

远程桌面:已禁用

注册表备份与恢复

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
# 导出注册表项
function Export-RegistryKey {
param(
[Parameter(Mandatory)]
[string]$KeyPath,

[string]$OutputDir = "C:\Backup\Registry"
)

New-Item $OutputDir -ItemType Directory -Force | Out-Null

$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$safeName = ($KeyPath -replace '[\\:]', '_') -replace '^HKLM_', 'HKLM-' -replace '^HKCU_', 'HKCU-'
$outputFile = Join-Path $OutputDir "${safeName}_${timestamp}.reg"

# 转换 PowerShell 路径为注册表路径
$regPath = $KeyPath -replace '^HKLM:\\', 'HKEY_LOCAL_MACHINE\' -replace '^HKCU:\\', 'HKEY_CURRENT_USER\'

$result = reg export $regPath $outputFile /y 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "已导出:$outputFile ($((Get-Item $outputFile).Length / 1KB -as [int]) KB)" -ForegroundColor Green
return $outputFile
} else {
Write-Host "导出失败:$result" -ForegroundColor Red
return $null
}
}

# 备份重要注册表项
$importantKeys = @(
"HKLM:\SYSTEM\CurrentControlSet\Services"
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
"HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion"
)

foreach ($key in $importantKeys) {
Export-RegistryKey -KeyPath $key
}

# 恢复注册表
function Restore-RegistryKey {
param([Parameter(Mandatory)][string]$RegFile)

if (-not (Test-Path $RegFile)) {
throw "文件不存在:$RegFile"
}

Write-Host "即将导入注册表:$RegFile" -ForegroundColor Yellow
$result = reg import $RegFile 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "导入成功" -ForegroundColor Green
} else {
Write-Host "导入失败:$result" -ForegroundColor Red
}
}

执行结果示例:

1
2
3
已导出:C:\Backup\Registry\HKLM-SYSTEM-CurrentControlSet-Services_20250716_083015.reg (256 KB)
已导出:C:\Backup\Registry\HKLM-SOFTWARE-Microsoft-Windows-CurrentVersion-Run_20250716_083015.reg (4 KB)
已导出:C:\Backup\Registry\HKLM-SOFTWARE-Microsoft-Windows-NT-CurrentVersion_20250716_083015.reg (12 KB)

注意事项

  1. 备份先行:修改注册表前务必备份相关键值,错误修改可能导致系统不稳定
  2. 管理员权限:修改 HKLM:\ 下的注册表需要管理员权限,HKCU:\ 不需要
  3. 数据类型:注册表值有多种类型(String、DWord、QWord、Binary、ExpandString、MultiString),设置时指定正确的类型
  4. 系统关键键HKLM:\SYSTEMHKLM:\SOFTWARE\Microsoft\Windows NT 下的键值直接影响系统运行,谨慎修改
  5. 64 位重定向:32 位程序访问 HKLM:\SOFTWARE 会被重定向到 HKLM:\SOFTWARE\WOW6432Node,注意区分
  6. 远程注册表:使用 Invoke-Command 可以远程操作其他机器的注册表(需要 WinRM 和 Remote Registry 服务)

PowerShell 技能连载 - 环境变量管理

适用于 PowerShell 5.1 及以上版本

环境变量是操作系统级别的配置机制,几乎影响所有程序的行为——PATH 决定命令搜索路径、JAVA_HOME 指定 Java 运行时、HTTP_PROXY 配置代理。在运维场景中,合理管理环境变量可以控制程序行为、隔离开发/测试/生产环境配置、管理工具链路径。PowerShell 对环境变量的操作比 CMD 更加强大和直观。

本文将讲解 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
# 读取环境变量
Write-Host "计算机名:$env:COMPUTERNAME"
Write-Host "用户名:$env:USERNAME"
Write-Host "系统路径:$env:PATH"
Write-Host "临时目录:$env:TEMP"
Write-Host "处理器架构:$env:PROCESSOR_ARCHITECTURE"
Write-Host "PowerShell 版本:$($PSVersionTable.PSVersion)"

# .NET 方式读取(可指定默认值)
$dbServer = [System.Environment]::GetEnvironmentVariable("DB_SERVER")
if (-not $dbServer) {
$dbServer = "localhost"
}
Write-Host "数据库服务器:$dbServer"

# 列出所有环境变量
Get-ChildItem Env: | Sort-Object Name | Format-Table Name, Value -AutoSize

# 搜索环境变量
Get-ChildItem Env: | Where-Object { $_.Name -match "JAVA|PYTHON|NODE" } |
Format-Table Name, Value -AutoSize

# 设置临时环境变量(仅当前会话)
$env:MYAPP_ENV = "development"
$env:MYAPP_DEBUG = "true"
Write-Host "已设置 MYAPP_ENV=$env:MYAPP_ENV"

# 删除环境变量
Remove-Item Env:MYAPP_DEBUG
Write-Host "MYAPP_DEBUG 已删除:'$env:MYAPP_DEBUG'"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
计算机名:WORKSTATION01
用户名:admin
系统路径:C:\Windows\system32;C:\Windows;C:\Program Files\PowerShell\7
临时目录:C:\Users\admin\AppData\Local\Temp
处理器架构:AMD64
PowerShell 版本:7.4.2
数据库服务器:localhost

Name Value
---- -----
COMPUTERNAME WORKSTATION01
JAVA_HOME C:\Program Files\Java\jdk-17
NODE_PATH C:\Program Files\nodejs
PATH C:\Windows\system32;C:\Windows;...
PYTHONPATH C:\Python312
已设置 MYAPP_ENV=development
MYAPP_DEBUG 已删除:''

持久化环境变量

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
# 持久化到用户级别(注册表 HKCU\Environment)
[System.Environment]::SetEnvironmentVariable("MYAPP_HOME", "C:\MyApp", "User")

# 持久化到系统级别(需要管理员权限)
# [System.Environment]::SetEnvironmentVariable("MYAPP_HOME", "C:\MyApp", "Machine")

# 验证持久化
$persistent = [System.Environment]::GetEnvironmentVariable("MYAPP_HOME", "User")
Write-Host "持久化值:$persistent"

# 读取所有用户级环境变量
$userVars = [System.Environment]::GetEnvironmentVariables("User")
$userVars.GetEnumerator() | Sort-Object Key |
Format-Table @{N='Name';E={$_.Key}}, @{N='Value';E={$_.Value}} -AutoSize

# 设置后同步到当前会话
function Set-EnvPersistent {
param(
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][string]$Value,
[ValidateSet("User", "Machine")]
[string]$Scope = "User"
)

[System.Environment]::SetEnvironmentVariable($Name, $Value, $Scope)

# 同步到当前会话
Set-Item -Path "Env:$Name" -Value $Value

Write-Host "已设置 $Name=$Value ($Scope)" -ForegroundColor Green
}

Set-EnvPersistent -Name "APP_LOG_LEVEL" -Value "Debug" -Scope User

# 删除持久化变量
[System.Environment]::SetEnvironmentVariable("APP_LOG_LEVEL", $null, "User")
Write-Host "已删除 APP_LOG_LEVEL"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
持久化值:C:\MyApp

Name Value
---- -----
APP_LOG_LEVEL Debug
HOME C:\Users\admin
MYAPP_HOME C:\MyApp
PATH C:\Users\admin\bin;C:\Python312\Scripts

已设置 APP_LOG_LEVEL=Debug (User)
已删除 APP_LOG_LEVEL

PATH 管理

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
# 安全的 PATH 管理
function Add-ToPath {
param(
[Parameter(Mandatory)]
[string]$Directory,

[ValidateSet("User", "Machine")]
[string]$Scope = "User"
)

# 验证目录存在
if (-not (Test-Path $Directory)) {
Write-Host "目录不存在:$Directory" -ForegroundColor Red
return
}

$currentPath = [System.Environment]::GetEnvironmentVariable("PATH", $Scope)
$paths = $currentPath -split ';' | Where-Object { $_ }

# 检查是否已存在
if ($paths -contains $Directory) {
Write-Host "已在 PATH 中:$Directory" -ForegroundColor Yellow
return
}

# 添加并持久化
$newPath = $paths + $Directory
[System.Environment]::SetEnvironmentVariable("PATH", ($newPath -join ';'), $Scope)

# 同步当前会话
$env:PATH = $newPath -join ';'

Write-Host "已添加到 PATH:$Directory" -ForegroundColor Green
}

function Remove-FromPath {
param(
[Parameter(Mandatory)]
[string]$Directory,

[ValidateSet("User", "Machine")]
[string]$Scope = "User"
)

$currentPath = [System.Environment]::GetEnvironmentVariable("PATH", $Scope)
$paths = $currentPath -split ';' | Where-Object { $_ -ne $Directory }

[System.Environment]::SetEnvironmentVariable("PATH", ($paths -join ';'), $Scope)
$env:PATH = $paths -join ';'

Write-Host "已从 PATH 移除:$Directory" -ForegroundColor Green
}

# 清理 PATH 中不存在的目录
function Repair-Path {
param([ValidateSet("User", "Machine")][string]$Scope = "User")

$currentPath = [System.Environment]::GetEnvironmentVariable("PATH", $Scope)
$paths = $currentPath -split ';' | Where-Object { $_ }

$validPaths = @()
$removed = @()

foreach ($p in $paths) {
if (Test-Path $p) {
$validPaths += $p
} else {
$removed += $p
}
}

if ($removed.Count -gt 0) {
[System.Environment]::SetEnvironmentVariable("PATH", ($validPaths -join ';'), $Scope)
$env:PATH = $validPaths -join ';'
Write-Host "清理了 $($removed.Count) 个无效路径:" -ForegroundColor Yellow
$removed | ForEach-Object { Write-Host " [已移除] $_" -ForegroundColor DarkGray }
} else {
Write-Host "PATH 无需清理" -ForegroundColor Green
}
}

Repair-Path -Scope User

执行结果示例:

1
2
3
4
5
6
已在 PATH 中:C:\Python312
已从 PATH 移除:C:\OldTool\bin
清理了 3 个无效路径:
[已移除] C:\DeletedApp\bin
[已移除] D:\NoLongerExists\tools
[已移除] C:\Temp\scripts

环境配置文件管理

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
# .env 文件加载器
function Import-DotEnv {
param(
[Parameter(Mandatory)]
[string]$Path,

[switch]$Persist
)

if (-not (Test-Path $Path)) {
throw ".env 文件不存在:$Path"
}

$lines = Get-Content $Path -ErrorAction Stop
$loaded = 0

foreach ($line in $lines) {
# 跳过注释和空行
if ($line -match '^\s*#' -or $line -match '^\s*$') { continue }

if ($line -match '^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)\s*$') {
$name = $Matches[1]
$value = $Matches[2].Trim('"').Trim("'")

Set-Item -Path "Env:$name" -Value $value
if ($Persist) {
[System.Environment]::SetEnvironmentVariable($name, $value, "User")
}

$loaded++
}
}

Write-Host "从 $Path 加载了 $loaded 个环境变量" -ForegroundColor Green
}

# .env 文件示例
$envContent = @"
# 数据库配置
DB_HOST=prod-db01.example.com
DB_PORT=5432
DB_NAME=myapp_production
DB_USER=app_user

# 应用配置
APP_ENV=production
APP_DEBUG=false
APP_PORT=8080

# 日志配置
LOG_LEVEL=Warning
LOG_PATH=C:\Logs\MyApp
"@

$envContent | Set-Content "C:\MyApp\.env" -Encoding UTF8
Import-DotEnv -Path "C:\MyApp\.env"

# 验证加载
Write-Host "数据库:$env:DB_HOST`:$env:DB_PORT/$env:DB_NAME"
Write-Host "环境:$env:APP_ENV"

# 导出当前环境变量到 .env 文件
function Export-DotEnv {
param(
[string]$Path = ".env",
[string[]]$Prefixes = @("MYAPP_", "DB_", "APP_")
)

$lines = @("# 自动生成 $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")

Get-ChildItem Env: | Where-Object {
$name = $_.Name
$Prefixes | ForEach-Object { $name -like "$_*" } | Where-Object { $_ }
} | Sort-Object Name | ForEach-Object {
$lines += "$($_.Name)=$($_.Value)"
}

$lines | Set-Content $Path -Encoding UTF8
Write-Host "已导出 $($lines.Count - 1) 个变量到 $Path" -ForegroundColor Green
}

执行结果示例:

1
2
3
从 C:\MyApp\.env 加载了 8 个环境变量
数据库:prod-db01.example.com:5432/myapp_production
环境:production

注意事项

  1. 作用域优先级:Machine < User < Process,后者覆盖前者。$env:VAR 修改的是 Process 级别
  2. 会话隔离$env:VAR 的修改只在当前会话生效,新开窗口不会看到变化
  3. PATH 分隔符:Windows 用 ;,Linux/macOS 用 :,跨平台脚本注意区分
  4. 敏感信息:不要将密码、Token 存入环境变量后再持久化到注册表。使用凭据管理器或密钥库
  5. 特殊变量PATHPATHEXTPSModulePath 等系统变量修改需谨慎,建议先备份原值
  6. 环境继承:子进程继承父进程的环境变量快照,修改不会双向传播