PowerShell模块开发指南
前言
PowerShell模块是构建复杂自动化解决方案的核心组件。通过模块化设计,你可以将脚本和命令封装为可重用的组件,实现代码复用、版本控制和团队协作。本文将深入探讨PowerShell模块开发的全生命周期,从基础架构到高级应用,帮助你掌握企业级模块开发技能。
1. 模块基础架构
1.1 模块类型与结构
PowerShell支持多种模块类型,适用于不同场景需求:
| 模块类型 | 扩展名 | 特点 | 适用场景 |
|---|---|---|---|
| 脚本模块 | .psm1 | 纯PowerShell脚本实现,易于编写调试 | 快速功能迭代、跨平台兼容需求 |
| 二进制模块 | .dll | 编译型模块,基于C#等.NET语言开发 | 高性能要求、复杂逻辑实现 |
| 清单模块 | .psd1 | 包含模块元数据的清单文件 | 打包分发、版本控制、依赖管理 |
| 动态模块 | 内存中创建 | 运行时动态生成,无需文件系统 | 临时功能、动态适配场景 |
标准模块目录结构遵循"一模块一目录"原则,典型结构如下:
MyModule/
├── MyModule.psd1 # 模块清单文件
├── MyModule.psm1 # 主脚本文件
├── Public/ # 公开函数目录
│ ├── Get-Data.ps1
│ └── Set-Config.ps1
├── Private/ # 私有函数目录
│ ├── Convert-Input.ps1
│ └── Validate-Params.ps1
├── Classes/ # 自定义类定义
│ └── DataModel.ps1
├── Enums/ # 枚举类型定义
│ └── StatusCodes.ps1
├── Formats/ # 格式化文件
│ └── MyModule.Format.ps1xml
├── Help/ # 帮助文档
│ └── MyModule-Help.xml
└── Tests/ # 测试文件
├── MyModule.Tests.ps1
└── Integration/
1.2 模块清单(.psd1)详解
模块清单是模块的"身份证",包含关键元数据和配置信息。以下是一个企业级模块清单示例:
@{
# 基本标识信息
RootModule = 'MyModule.psm1'
ModuleVersion = '2.1.0'
GUID = 'a1b2c3d4-e5f6-7890-abcd-1234567890ab'
Author = 'Contoso Dev Team'
CompanyName = 'Contoso Corporation'
Copyright = '(c) 2025 Contoso. All rights reserved.'
# 版本控制与兼容性
CompatiblePSEditions = @('Desktop', 'Core')
PowerShellVersion = '5.1'
ProcessorArchitecture = @('x64', 'x86', 'Arm64')
# 导出定义
FunctionsToExport = @('Get-Data', 'Set-Config', 'Test-Connection')
CmdletsToExport = @('Invoke-Action', 'Register-Event')
VariablesToExport = @('$Global:MyModuleConfig')
AliasesToExport = @('gdt', 'stc')
# 依赖管理
RequiredModules = @(
@{ModuleName='PSScriptAnalyzer'; ModuleVersion='1.21.0'},
@{ModuleName='SqlServer'; ModuleVersion='21.1.18256'; MaximumVersion='21.99.99999'}
)
RequiredAssemblies = @('Newtonsoft.Json.dll', 'MyModule.Types.dll')
ScriptsToProcess = @('Initialize.ps1', 'RegisterEvents.ps1')
# 资源与文档
FormatsToProcess = @('MyModule.Format.ps1xml')
TypesToProcess = @('MyModule.Types.ps1xml')
HelpInfoURI = 'https://docs.contoso.com/powershell/mymodule'
}
关键元数据字段解析:
- GUID:全球唯一标识符,确保模块唯一性
- ModuleVersion:遵循语义化版本(SemVer)规范:主版本.次版本.修订号
- CompatiblePSEditions:指定支持的PowerShell版本系列(Desktop/Core)
- FunctionsToExport:显式列出公开函数,避免意外导出内部函数
最佳实践:始终显式指定导出内容,避免使用
*通配符。这不仅提高性能,还能防止内部实现细节泄露。
2. 脚本模块开发
2.1 函数设计与实现
优质函数设计遵循"单一职责"原则,每个函数专注解决特定问题。以下是企业级函数开发的标准模板:
function Get-Data {
<#
.SYNOPSIS
从数据源获取并处理数据
.DESCRIPTION
此命令连接到指定数据源,执行查询并返回格式化结果。支持多种输出格式和筛选条件。
.PARAMETER Server
数据源服务器名称或IP地址
.PARAMETER Database
目标数据库名称
.PARAMETER Query
要执行的查询字符串
.PARAMETER Credential
用于身份验证的凭据对象
.PARAMETER OutputFormat
指定输出格式,支持Table、List、Json和Csv
.EXAMPLE
Get-Data -Server "sql01" -Database "Inventory" -Query "SELECT * FROM Assets"
获取所有资产信息并以表格形式显示
#>
[CmdletBinding(DefaultParameterSetName='Default', SupportsShouldProcess=$true)]
[OutputType([PSCustomObject], [System.Management.Automation.PSObject])]
param (
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string]$Server,
[Parameter(Mandatory=$true, Position=1)]
[ValidateNotNullOrEmpty()]
[string]$Database,
[Parameter(Mandatory=$true)]
[ValidateScript({
if ($_ -match '^(SELECT|EXEC|WITH)\s+') {
$true
}
else {
throw "只允许SELECT、EXEC和WITH开头的查询语句"
}
})]
[string]$Query,
[Parameter()]
[pscredential]$Credential,
[Parameter()]
[ValidateSet('Table', 'List', 'Json', 'Csv')]
[string]$OutputFormat = 'Table'
)
begin {
# 初始化代码,只执行一次
Write-Verbose "开始执行Get-Data命令,参数: Server=$Server, Database=$Database"
# 加载依赖项
try {
Import-Module SqlServer -ErrorAction Stop
}
catch {
throw "缺少依赖模块SqlServer,请运行Install-Module SqlServer安装"
}
# 初始化连接字符串
$connectionParams = @{
ServerInstance = $Server
Database = $Database
Query = $Query
}
# 添加凭据(如果提供)
if ($PSBoundParameters.ContainsKey('Credential')) {
$connectionParams['Credential'] = $Credential
}
}
process {
# 处理逻辑,对每个输入对象执行
if ($PSCmdlet.ShouldProcess($Server, "查询数据库 $Database")) {
try {
# 执行查询
$data = Invoke-SqlCmd @connectionParams -ErrorAction Stop
# 根据输出格式处理结果
switch ($OutputFormat) {
'Table' { $data | Format-Table -AutoSize }
'List' { $data | Format-List }
'Json' { $data | ConvertTo-Json -Depth 10 }
'Csv' { $data | ConvertTo-Csv -NoTypeInformation }
}
}
catch {
Write-Error "查询执行失败: $_"
throw
}
}
}
end {
Write-Verbose "Get-Data命令执行完成"
}
}
2.2 参数验证与转换
PowerShell提供丰富的参数验证机制,确保输入数据符合预期:
function New-Resource {
[CmdletBinding()]
param (
# 基本验证
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]$Name,
# 范围验证
[ValidateRange(1, 100)]
[int]$Count = 10,
# 正则表达式验证
[ValidatePattern('^[A-Za-z0-9_-]+$')]
[string]$Code,
# 脚本验证
[ValidateScript({
if (-not (Test-Path $_ -PathType Container)) {
throw "路径 $_ 不存在"
}
$true
})]
[string]$Path
)
# 函数实现...
}
2.3 并行处理
利用PowerShell的并行处理能力提升性能:
function Invoke-ParallelProcess {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
[string[]]$Items,
[Parameter(Mandatory=$true)]
[scriptblock]$ScriptBlock,
[int]$ThrottleLimit = 5
)
end {
# 并行处理并立即返回结果
return $inputItems | ForEach-Object -Parallel {
& $using:ScriptBlock $_
} -ThrottleLimit $ThrottleLimit
}
}
# 使用示例
$results = 1..100 | Invoke-ParallelProcess -ScriptBlock {
param($number)
[PSCustomObject]@{
Number = $number
Square = $number * $number
ProcessId = $PID
}
} -ThrottleLimit 8
3. 二进制模块开发
3.1 C# Cmdlet开发基础
二进制模块基于.NET框架开发,提供更高性能和更强的类型安全。以下是一个C# Cmdlet示例:
using System;
using System.Management.Automation;
using System.Net;
namespace MyModule.Cmdlets
{
[Cmdlet(VerbsCommunications.Send, "Data",
SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.Medium)]
[OutputType(typeof(DataResponse))]
public class SendDataCommand : PSCmdlet
{
[Parameter(Mandatory = true, Position = 0)]
[ValidateNotNullOrEmpty]
public string Server { get; set; }
[Parameter(Mandatory = true)]
[ValidateRange(1, 65535)]
public int Port { get; set; } = 8080;
[Parameter(Mandatory = true, ValueFromPipeline = true)]
public string[] Data { get; set; }
[Parameter()]
public SwitchParameter UseHttps { get; set; }
private WebClient client;
private string baseUri;
protected override void BeginProcessing()
{
base.BeginProcessing();
client = new WebClient();
string protocol = UseHttps ? "https" : "http";
baseUri = $"{protocol}://{Server}:{Port}/api/data";
WriteVerbose($"初始化连接: {baseUri}");
}
protected override void ProcessRecord()
{
foreach (var item in Data)
{
if (ShouldProcess(Server, $"发送数据: {item}"))
{
try
{
string response = client.UploadString(baseUri, item);
var result = new DataResponse
{
Timestamp = DateTime.Now,
Server = Server,
DataLength = item.Length,
Success = true,
Response = response
};
WriteObject(result);
}
catch (WebException ex)
{
WriteError(new ErrorRecord(
ex,
"SendDataFailed",
ErrorCategory.ConnectionError,
item));
}
}
}
}
protected override void EndProcessing()
{
client.Dispose();
base.EndProcessing();
}
}
public class DataResponse
{
public DateTime Timestamp { get; set; }
public string Server { get; set; }
public int DataLength { get; set; }
public bool Success { get; set; }
public string Response { get; set; }
}
}
3.2 项目配置与编译
二进制模块项目需要正确的配置才能与PowerShell兼容。以下是一个典型的.csproj文件配置:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>MyModule.Cmdlets</AssemblyName>
<RootNamespace>MyModule.Cmdlets</RootNamespace>
<OutputType>Library</OutputType>
<Version>2.1.0</Version>
</PropertyGroup>
<ItemGroup>
<!-- PowerShell SDK引用 -->
<PackageReference Include="PowerShellStandard.Library" Version="5.1.0" />
<!-- 其他依赖项 -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<!-- 生成PowerShell模块清单 -->
<Target Name="GenerateModuleManifest" AfterTargets="Build">
<Exec Command="pwsh -Command "New-ModuleManifest -Path $(OutputPath)MyModule.Cmdlets.psd1 -RootModule MyModule.Cmdlets.dll -ModuleVersion $(Version) -Author 'Contoso' -CompanyName 'Contoso' -CmdletsToExport @('Send-Data')"" />
</Target>
</Project>
4. 跨平台兼容性
4.1 平台检测与适配
确保模块在不同操作系统上正常工作的核心策略是平台检测与条件执行:
function Get-PlatformInfo {
[CmdletBinding()]
[OutputType([PSCustomObject])]
param()
$platform = [PSCustomObject]@{
OS = switch ($true) {
$IsWindows { 'Windows' }
$IsmacOS { 'macOS' }
$IsLinux { 'Linux' }
default { 'Unknown' }
}
IsCoreCLR = $IsCoreCLR
PowerShellVersion = $PSVersionTable.PSVersion
Architecture = $env:PROCESSOR_ARCHITECTURE ?? 'Unknown'
HomeDirectory = $HOME
FileSystemSeparator = if ($IsWindows) { '\' } else { '/' }
}
return $platform
}
# 平台特定操作示例
function Invoke-PlatformSpecificOperation {
[CmdletBinding()]
param()
$platform = Get-PlatformInfo
Write-Verbose "运行平台: $($platform.OS) $($platform.PowerShellVersion)"
switch ($platform.OS) {
'Windows' {
# Windows特定实现
$registryPath = 'HKLM:\Software\Contoso\MyModule'
if (-not (Test-Path $registryPath)) {
New-Item -Path $registryPath -Force | Out-Null
}
}
'macOS' {
# macOS特定实现
$plistPath = "$($platform.HomeDirectory)/Library/Preferences/com.contoso.MyModule.plist"
defaults write $plistPath LastRun "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
}
'Linux' {
# Linux特定实现
$configDir = "$($platform.HomeDirectory)/.config/MyModule"
if (-not (Test-Path $configDir)) {
New-Item -Path $configDir -ItemType Directory -Force | Out-Null
}
}
}
}
4.2 文件系统路径处理
跨平台路径处理的关键是避免硬编码路径分隔符,使用PowerShell的内置路径处理 cmdlet:
function Resolve-CrossPlatformPath {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[string[]]$Path,
[switch]$Relative,
[switch]$EnsureExists
)
$resolvedPath = Join-Path -Path $Path
if ($IsWindows) {
$resolvedPath = $resolvedPath -replace '/', '\'
}
else {
$resolvedPath = $resolvedPath -replace '\\', '/'
if (-not $Relative -and -not $resolvedPath.StartsWith('/')) {
$resolvedPath = "/$resolvedPath"
}
}
if ($EnsureExists -and -not (Test-Path $resolvedPath)) {
$null = New-Item -Path $resolvedPath -ItemType Directory -Force
}
return $resolvedPath
}
5. 模块测试与调试
5.1 Pester测试框架应用
Pester是PowerShell的官方测试框架,支持单元测试、集成测试和验收测试:
# MyModule.Tests.ps1
$moduleName = "MyModule"
$modulePath = Join-Path -Path $PSScriptRoot -ChildPath ".." -Resolve
Import-Module $modulePath -Force
Describe "MyModule 基本功能测试" -Tag "Unit" {
Context "模块导入验证" {
It "应成功导入模块" {
Get-Module $moduleName | Should -Not -BeNullOrEmpty
}
It "应导出预期命令" {
$exportedCommands = (Get-Module $moduleName).ExportedCommands.Keys
$expectedCommands = @('Get-Data', 'Set-Config')
foreach ($cmd in $expectedCommands) {
$exportedCommands | Should -Contain $cmd
}
}
}
Context "Get-Data 命令测试" {
BeforeAll {
$testServer = "testserver"
$testDatabase = "testdb"
}
It "当提供无效服务器时应抛出错误" {
{ Get-Data -Server "invalid.server" -Database $testDatabase -Query "SELECT 1" } | Should -Throw
}
It "应返回预期格式的对象" {
Mock Invoke-SqlCmd {
[PSCustomObject]@{
ID = 1
Name = "Test Data"
Value = "Sample"
}
} -ParameterFilter { $ServerInstance -eq $testServer }
$result = Get-Data -Server $testServer -Database $testDatabase -Query "SELECT * FROM Test" -OutputFormat Table
$result | Should -Not -BeNullOrEmpty
$result.PSObject.Properties.Name | Should -Contain "ID"
}
}
}
5.2 调试技术与工具
PowerShell提供多种调试工具,帮助诊断模块问题:
# 启用模块调试
function Start-ModuleDebug {
[CmdletBinding()]
param (
[string]$ModuleName = "MyModule"
)
Import-Module $ModuleName -Force -Debug
Set-PSBreakpoint -Command "Get-Data" -Line 25
$DebugPreference = "Continue"
}
# 使用示例
Start-ModuleDebug
Get-Data -Server prodserver -Database inventory -Query "SELECT * FROM Assets"
6. 模块发布与部署
6.1 打包与版本管理
使用PowerShellGet模块管理模块打包和发布:
function Publish-ModulePackage {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[string]$ModulePath,
[Parameter(Mandatory=$true)]
[string]$OutputPath,
[string]$Version,
[switch]$PublishToGallery,
[string]$NuGetApiKey
)
# 确保输出目录存在
if (-not (Test-Path $OutputPath)) {
New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null
}
# 获取模块清单
$manifestPath = Get-ChildItem -Path $ModulePath -Filter "*.psd1" -Recurse | Select-Object -First 1
$manifest = Test-ModuleManifest -Path $manifestPath
# 如果指定了版本,更新模块版本
if ($Version) {
$manifest.Version = $Version
Update-ModuleManifest -Path $manifestPath -ModuleVersion $Version
}
# 打包模块
$packagePath = Join-Path -Path $OutputPath -ChildPath "$($manifest.Name)_$($manifest.Version).nupkg"
Publish-Module -Path $ModulePath -DestinationPath $OutputPath -Force -PassThru
Write-Host "模块包已创建: $packagePath"
# 如果需要发布到PowerShell Gallery
if ($PublishToGallery) {
Publish-Module -Path $ModulePath -NuGetApiKey $NuGetApiKey -Force
}
return $packagePath
}
6.2 CI/CD自动化流程
使用GitHub Actions实现模块的自动测试和发布:
# .github/workflows/module-pipeline.yml
name: PowerShell Module CI/CD
on:
push:
branches: [ main ]
paths:
- 'src/**'
- 'tests/**'
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
powershell: [7.2, 7.3]
steps:
- uses: actions/checkout@v3
- name: Install PowerShell ${{ matrix.powershell }}
uses: PowerShell/Install-PowerShell@v1
with:
version: ${{ matrix.powershell }}
- name: Install Dependencies
shell: pwsh
run: |
Install-Module Pester, PSScriptAnalyzer -Force -Scope CurrentUser
- name: Run Pester Tests
shell: pwsh
run: Invoke-Pester -Path ./tests -OutputFile test-results.xml -OutputFormat NUnitXml
- name: Upload Test Results
uses: actions/upload-artifact@v3
with:
name: test-results-${{ matrix.os }}-${{ matrix.powershell }}
path: test-results.xml
7. 性能优化与安全加固
7.1 性能优化策略
# 性能基准测试脚本
function Test-ModulePerformance {
[CmdletBinding()]
param (
[string]$ModuleName,
[int]$Iterations = 10
)
Import-Module $ModuleName -Force
$results = [System.Collections.Generic.List[PSObject]]::new()
$testCommands = @(
@{ Name = "Get-Data"; Params = @{ Server = "localhost"; Database = "test"; Query = "SELECT TOP 100 * FROM Data" } },
@{ Name = "Set-Config"; Params = @{ Key = "Test"; Value = "Performance" } }
)
foreach ($cmd in $testCommands) {
$totalDuration = [TimeSpan]::Zero
for ($i = 0; $i -lt $Iterations; $i++) {
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
& $cmd.Name @($cmd.Params) | Out-Null
$stopwatch.Stop()
$totalDuration += $stopwatch.Elapsed
}
$results.Add([PSCustomObject]@{
Command = $cmd.Name
AverageMs = $totalDuration.TotalMilliseconds / $Iterations
TotalMs = $totalDuration.TotalMilliseconds
})
}
return $results | Format-Table -AutoSize
}
7.2 安全最佳实践
# 安全配置验证函数
function Test-ModuleSecurity {
[CmdletBinding()]
param (
[string]$ModulePath
)
$issues = [System.Collections.Generic.List[string]]::new()
# 检查脚本签名
$scriptFiles = Get-ChildItem -Path $ModulePath -Filter "*.ps1" -Recurse
foreach ($file in $scriptFiles) {
$signature = Get-AuthenticodeSignature -FilePath $file.FullName
if ($signature.Status -ne "Valid") {
$issues.Add("未签名的脚本文件: $($file.FullName)")
}
}
# 检查敏感信息
$sensitivePatterns = @(
[regex]::new('(?i)password\s*=\s*["''].*?["'']'),
[regex]::new('(?i)secret\s*=\s*["''].*?["'']')
)
foreach ($file in $scriptFiles) {
$content = Get-Content -Path $file.FullName -Raw
foreach ($pattern in $sensitivePatterns) {
if ($pattern.IsMatch($content)) {
$issues.Add("可能包含敏感信息的文件: $($file.FullName)")
break
}
}
}
if ($issues.Count -eq 0) {
Write-Host "模块安全检查通过,未发现问题"
return $true
}
else {
Write-Host "模块安全检查发现 $($issues.Count) 个问题:"
$issues | ForEach-Object { Write-Host "- $_" }
return $false
}
}
8. 高级应用与最佳实践
8.1 模块依赖管理
function Initialize-ModuleDependencies {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[string]$ModulePath
)
$manifestPath = Get-ChildItem -Path $ModulePath -Filter "*.psd1" -Recurse | Select-Object -First 1
$manifest = Test-ModuleManifest -Path $manifestPath
# 创建依赖目录
$dependenciesPath = Join-Path -Path $ModulePath -ChildPath "Dependencies"
if (-not (Test-Path $dependenciesPath)) {
New-Item -Path $dependenciesPath -ItemType Directory -Force | Out-Null
}
$env:PSModulePath = "$dependenciesPath;$env:PSModulePath"
# 处理RequiredModules
foreach ($required in $manifest.RequiredModules) {
if ($required -is [string]) {
$moduleName = $required
$moduleVersion = $null
}
else {
$moduleName = $required.ModuleName
$moduleVersion = $required.ModuleVersion
}
# 检查模块是否已安装
$installed = Get-Module -ListAvailable $moduleName |
Where-Object { (-not $moduleVersion) -or ($_.Version -ge [version]$moduleVersion) } |
Sort-Object Version -Descending |
Select-Object -First 1
if (-not $installed) {
Install-Module -Name $moduleName -Scope CurrentUser -Force -ErrorAction Stop
}
}
Write-Host "所有依赖模块已处理完成"
}
8.2 插件架构实现
$script:Plugins = @{}
# 插件注册函数
function Register-Plugin {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[string]$Name,
[Parameter(Mandatory=$true)]
[scriptblock]$Handler,
[string[]]$Categories = @(),
[string]$Description
)
$script:Plugins[$Name] = [PSCustomObject]@{
Name = $Name
Handler = $Handler
Categories = $Categories
Description = $Description
}
}
# 插件执行函数
function Invoke-Plugin {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[string]$Name,
[hashtable]$Parameters = @{}
)
if (-not $script:Plugins.ContainsKey($Name)) {
throw "插件不存在: $Name"
}
$plugin = $script:Plugins[$Name]
Write-Verbose "执行插件: $Name"
$result = & $plugin.Handler @Parameters
return $result
}
# 插件示例
Register-Plugin -Name "JsonExporter" -Categories "Export", "Json" -Description "将数据导出为JSON格式" -Handler {
param($InputObject, $Parameters)
$outputPath = $Parameters.OutputPath ?? "output.json"
$InputObject | ConvertTo-Json | Out-File $outputPath -Encoding utf8
return [PSCustomObject]@{
Plugin = $Parameters.PluginName
OutputPath = $outputPath
RecordCount = @($InputObject).Count
}
}
9. 总结与展望
PowerShell模块开发是系统管理和自动化任务的核心技能。通过掌握模块化架构、命令设计、跨平台兼容、测试部署等关键技术,你可以构建专业、高效且可维护的PowerShell扩展。
未来发展方向
- 云原生集成:与Azure、AWS等云平台更深度的集成能力
- AI辅助开发:利用PowerShell的AI功能实现智能数据处理
- 容器化部署:模块的容器化打包和Kubernetes集成
- 低代码开发:通过可视化工具辅助模块设计和生成
PowerShell不断发展,建议持续关注官方文档和社区动态,参与开源项目贡献,不断提升模块化开发技能。
学习资源推荐
- PowerShell官方文档: https://learn.microsoft.com/powershell/
- PowerShell GitHub仓库: https://github.com/PowerShell/PowerShell
- PowerShell模块最佳实践: GitHub.com/PowerShell/DscResources
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



