PowerShell 技能连载 - Plaster 项目脚手架

适用于 PowerShell 5.1 及以上版本

每次创建新的 PowerShell 模块项目,都需要手动建立目录结构、编写模块清单(.psd1)、创建入口脚本(.psm1)、添加 Pester 测试文件,甚至还要配置 CI/CD 流水线。这些重复劳动不仅耗时,而且容易出现结构不一致的问题,尤其在多人协作的团队中更为突出。

Plaster 是由 PowerShell 社区开发的正式项目脚手架工具,它的灵感来源于 Node.js 的 Yeoman 和 Python 的 Cookiecutter。Plaster 使用 XML 模板来定义完整的项目结构,通过声明式的方式描述文件、目录、参数和条件逻辑,一键生成标准化的项目骨架。

借助 Plaster,团队可以将最佳实践固化到模板中——无论是代码规范、测试框架配置,还是 Git 钩子和 CI 流水线——每次创建新项目时都能保持结构一致,大幅降低遗漏关键配置的风险。

Plaster 模板基础

Plaster 模板的核心是一个 plasterManifest.xml 文件,它定义了模板的元数据、用户参数和文件生成规则。下面是一个最基础的模块模板示例。

首先安装 Plaster 模块:

1
Install-Module -Name Plaster -Scope CurrentUser -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
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
# 创建模板目录
$templatePath = "$HOME\PlasterTemplates\BasicModule"
New-Item -Path $templatePath -ItemType Directory -Force

# 创建 plasterManifest.xml
$manifestContent = @'
<?xml version="1.0" encoding="utf-8"?>
<plasterManifest
schemaVersion="1.1"
templateType="Project"
xmlns="http://www.microsoft.com/schemas/PowerShell/Plaster/v1">

<metadata>
<name>BasicModuleTemplate</name>
<id>9a4e5d6f-7b8c-4a3b-2c1d-0e9f8a7b6c5d</id>
<version>1.0.0</version>
<title>基础 PowerShell 模块模板</title>
<description>生成一个标准结构的 PowerShell 模块项目</description>
<author>DevOps Team</author>
<tags>Module, Pester, CI</tags>
</metadata>

<parameters>
<parameter name="ModuleName"
type="text"
prompt="请输入模块名称" />

<parameter name="ModuleDescription"
type="text"
prompt="请输入模块描述" />

<parameter name="AuthorName"
type="text"
prompt="请输入作者名称"
default="$env:USERNAME" />

<parameter name="ModuleVersion"
type="text"
prompt="请输入初始版本号"
default="0.1.0" />
</parameters>

<content>
<message>&#10;&#10;正在创建基础模块项目...&#10;</message>

<!-- 创建目录结构 -->
<file source=""
destination="src\Functions\Public" />
<file source=""
destination="src\Functions\Private" />
<file source=""
destination="tests" />
<file source=""
destination="docs" />

<!-- 生成模块入口文件 -->
<templateFile
source="module.psm1.t"
destination="src\{{PLASTER_PARAM_ModuleName}}.psm1" />

<!-- 生成模块清单 -->
<templateFile
source="manifest.psd1.t"
destination="src\{{PLASTER_PARAM_ModuleName}}.psd1" />

<!-- 生成测试文件 -->
<templateFile
source="test.Tests.ps1.t"
destination="tests\{{PLASTER_PARAM_ModuleName}}.Tests.ps1" />

<message>&#10;模块项目已成功创建!&#10;</message>
</content>
</plasterManifest>
'@

$manifestContent | Set-Content -Path "$templatePath\plasterManifest.xml" -Encoding UTF8

同时需要创建模板文件。以模块入口文件模板 module.psm1.t 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 创建模块入口模板
$psm1Template = @'
# {{PLASTER_PARAM_ModuleName}} 模块入口
# 由 Plaster 于 {{PLASTER_Date}} 生成

$PublicFunctions = @( Get-ChildItem -Path "$PSScriptRoot\Functions\Public\*.ps1" -ErrorAction SilentlyContinue )
$PrivateFunctions = @( Get-ChildItem -Path "$PSScriptRoot\Functions\Private\*.ps1" -ErrorAction SilentlyContinue )

foreach ($import in @($PublicFunctions + $PrivateFunctions)) {
try {
. $import.FullName
} catch {
Write-Error "无法导入函数 $($import.FullName): $_"
}
}

Export-ModuleMember -Function $PublicFunctions.BaseName
'@

$psm1Template | Set-Content -Path "$templatePath\module.psm1.t" -Encoding UTF8

调用模板创建项目:

1
2
$destPath = "$HOME\Projects\MyNewModule"
Invoke-Plaster -TemplatePath $templatePath -DestinationPath $destPath

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
请输入模块名称: MyStorageToolkit
请输入模块描述: 存储管理工具集
请输入作者名称 [victorwoo]:
请输入初始版本号 [0.1.0]:

正在创建基础模块项目...

创建目录: src\Functions\Public
创建目录: src\Functions\Private
创建目录: tests
创建目录: docs
创建文件: src\MyStorageToolkit.psm1
创建文件: src\MyStorageToolkit.psd1
创建文件: tests\MyStorageToolkit.Tests.ps1

模块项目已成功创建!

高级模板功能

基础模板能满足简单需求,但实际团队项目中往往需要根据用户选择来决定生成哪些内容。Plaster 支持 choice(单选)、multichoice(多选)和条件判断,让模板具备灵活的定制能力。

下面是一个带有高级功能的模板清单片段:

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
# 高级模板的 parameters 和 content 部分
$advancedManifest = @'
<parameters>
<parameter name="ModuleName"
type="text"
prompt="请输入模块名称" />

<parameter name="ModuleDescription"
type="text"
prompt="请输入模块描述" />

<parameter name="License"
type="choice"
prompt="请选择许可证类型"
default="1"
store="text">
<choice label="&amp;MIT"
help="MIT 许可证"
value="MIT" />
<choice label="&amp;Apache"
help="Apache 2.0 许可证"
value="Apache" />
<choice label="&amp;None"
help="不添加许可证"
value="None" />
</parameter>

<parameter name="Options"
type="multichoice"
prompt="请选择要包含的功能(空格选择,回车确认)"
default="0,1"
store="text">
<choice label="&amp;Pester 测试框架"
help="添加 Pester 测试"
value="Pester" />
<choice label="&amp;GitHub Actions CI"
help="添加 GitHub Actions 工作流"
value="GitHubActions" />
<choice label="&amp;VS Code 配置"
help="添加 .vscode 目录"
value="VSCode" />
<choice label="&amp;帮助文档"
help="添加帮助文档模板"
value="HelpDocs" />
</parameter>

<parameter name="Editor"
type="choice"
prompt="请选择编辑器配置"
default="0"
store="text">
<choice label="&amp;VS Code"
help="Visual Studio Code"
value="VSCode" />
<choice label="&amp;其他"
help="不生成编辑器配置"
value="None" />
</parameter>
</parameters>
'@

$advancedManifest | Set-Content -Path "$templatePath\advanced_params.xml" -Encoding UTF8

使用条件逻辑控制文件生成:

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
# 条件生成的 content 片段
$contentSnippet = @'
<content>
<!-- 条件生成:仅当选择 Pester 时创建测试 -->
<condition condition="$PLASTER_PARAM_Options -contains 'Pester'">
<file source="" destination="tests" />
<templateFile source="test.Tests.ps1.t"
destination="tests\{{PLASTER_PARAM_ModuleName}}.Tests.ps1" />
<message>已添加 Pester 测试框架配置</message>
</condition>

<!-- 条件生成:仅当选择 GitHub Actions 时创建 CI 配置 -->
<condition condition="$PLASTER_PARAM_Options -contains 'GitHubActions'">
<file source="" destination=".github\workflows" />
<templateFile source="ci.yml.t"
destination=".github\workflows\ci.yml" />
<message>已添加 GitHub Actions CI 配置</message>
</condition>

<!-- 条件生成:仅当选择 VS Code 时创建编辑器配置 -->
<condition condition="$PLASTER_PARAM_Editor -eq 'VSCode'">
<file source="" destination=".vscode" />
<templateFile source="settings.json.t"
destination=".vscode\settings.json" />
<templateFile source="extensions.json.t"
destination=".vscode\extensions.json" />
<message>已添加 VS Code 配置</message>
</condition>

<!-- 条件生成:仅当选择帮助文档时创建 -->
<condition condition="$PLASTER_PARAM_Options -contains 'HelpDocs'">
<file source="" destination="docs" />
<templateFile source="readme.md.t"
destination="docs\{{PLASTER_PARAM_ModuleName}}.md" />
<message>已添加帮助文档模板</message>
</condition>
</content>
'@

$contentSnippet | Set-Content -Path "$templatePath\content_snippet.xml" -Encoding UTF8

运行高级模板时,交互界面如下:

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
请输入模块名称: MyStorageToolkit
请输入模块描述: 存储管理工具集

请选择许可证类型
[1] MIT
[2] Apache
[3] None
选择 [1]: 1

请选择要包含的功能(空格选择,回车确认)
[x] 1. Pester 测试框架
[x] 2. GitHub Actions CI
[ ] 3. VS Code 配置
[ ] 4. 帮助文档
选择 [0,1]:

请选择编辑器配置
[1] VS Code
[2] 其他
选择 [1]: 1

创建目录: tests
创建文件: tests\MyStorageToolkit.Tests.ps1
已添加 Pester 测试框架配置
创建目录: .github\workflows
创建文件: .github\workflows\ci.yml
已添加 GitHub Actions CI 配置
创建目录: .vscode
创建文件: .vscode\settings.json
创建文件: .vscode\extensions.json
已添加 VS Code 配置

团队模板实战

将模板纳入团队协作流程的关键在于模板的版本管理和共享分发。下面展示一个完整的团队模块模板,并将模板打包发布到内部 PowerShell 仓库。

首先创建完整的模板项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 团队模板项目初始化
$teamTemplatePath = "$HOME\PlasterTemplates\TeamStandardModule"
$directories = @(
"$teamTemplatePath\src\Functions\Public"
"$teamTemplatePath\src\Functions\Private"
"$teamTemplatePath\src\Classes"
"$teamTemplatePath\tests\Unit"
"$teamTemplatePath\tests\Integration"
"$teamTemplatePath\examples"
"$teamTemplatePath\docs"
"$teamTemplatePath\.github\workflows"
"$teamTemplatePath\.vscode"
)

foreach ($dir in $directories) {
New-Item -Path $dir -ItemType Directory -Force | Out-Null
}

创建 GitHub Actions CI 工作流模板文件 ci.yml.t

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
$ciTemplate = @'
name: CI - {{PLASTER_PARAM_ModuleName}}

on:
push:
branches: [main, develop]
pull_request:
branches: [main]

jobs:
test:
runs-on: {{PLASTER_PARAM RunnerType}}
strategy:
matrix:
powershell-version: ['7.2', '7.4']

steps:
- uses: actions/checkout@v4

- name: 安装 PowerShell ${{ '{{' }} matrix.powershell-version {{ '}}' }}
uses: PowerShell/setup-pwsh@v1

- name: 安装依赖模块
shell: pwsh
run: |
Install-Module -Name Pester -Force -Scope CurrentUser
Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser

- name: 执行 Pester 测试
shell: pwsh
run: |
$config = New-PesterConfiguration
$config.Run.Path = './tests'
$config.Output.Verbosity = 'Detailed'
$config.CodeCoverage.Enabled = $true
$config.CodeCoverage.Path = './src/*.psm1'
Invoke-Pester -Configuration $config

- name: 执行 Script Analyzer
shell: pwsh
run: |
Invoke-ScriptAnalyzer -Path './src' -Severity Warning
'@

$ciTemplate | Set-Content -Path "$teamTemplatePath\ci.yml.t" -Encoding UTF8

将模板打包为 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
# 创建模板分发模块的清单
$moduleDir = "$HOME\Projects\TeamPlasterTemplates"
New-Item -Path $moduleDir -ItemType Directory -Force | Out-Null

# 将模板文件复制到模块目录中
Copy-Item -Path $teamTemplatePath -Destination "$moduleDir\Templates" -Recurse -Force

# 创建模块入口脚本,提供快速调用函数
$moduleScript = @'
function Install-TeamModuleTemplate {
<#
.SYNOPSIS
使用团队标准模板创建新的 PowerShell 模块项目
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$DestinationPath,

[string]$TemplatePath = "$PSScriptRoot\Templates\TeamStandardModule"
)

if (-not (Get-Module -ListAvailable -Name Plaster)) {
throw "请先安装 Plaster 模块: Install-Module -Name Plaster -Scope CurrentUser"
}

Invoke-Plaster -TemplatePath $TemplatePath -DestinationPath $DestinationPath
}

Export-ModuleMember -Function Install-TeamModuleTemplate
'@

$moduleScript | Set-Content -Path "$moduleDir\TeamPlasterTemplates.psm1" -Encoding UTF8

# 生成模块清单
$manifestParams = @{
Path = "$moduleDir\TeamPlasterTemplates.psd1"
RootModule = "TeamPlasterTemplates.psm1"
ModuleVersion = "1.0.0"
Author = "DevOps Team"
Description = "团队标准 PowerShell 模块 Plaster 模板"
FunctionsToExport = @("Install-TeamModuleTemplate")
RequiredModules = @("Plaster")
PowerShellVersion = "5.1"
}
New-ModuleManifest @manifestParams

Write-Host "团队模板模块已创建: $moduleDir" -ForegroundColor Green

发布到内部 NuGet 仓库或复制到团队共享路径后,团队成员即可一键使用:

1
2
3
4
5
6
# 安装团队模板模块
Install-Module -Name TeamPlasterTemplates -Repository InternalPSGallery -Scope CurrentUser

# 使用团队模板创建新项目
Import-Module TeamPlasterTemplates
Install-TeamModuleTemplate -DestinationPath "$HOME\Projects\NewProject"

执行结果示例:

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
请输入模块名称: NetworkMonitor
请输入模块描述: 网络监控与分析工具集
请输入作者名称 [victorwoo]:
请输入初始版本号 [0.1.0]:

请选择许可证类型
选择 [1]: 1 (MIT)

请选择要包含的功能(空格选择,回车确认)
[x] 1. Pester 测试框架
[x] 2. GitHub Actions CI
[x] 3. VS Code 配置
[x] 4. 帮助文档

创建目录: src\Functions\Public
创建目录: src\Functions\Private
创建目录: src\Classes
创建目录: tests\Unit
创建目录: tests\Integration
创建目录: examples
创建目录: .github\workflows
创建文件: .github\workflows\ci.yml
创建目录: .vscode
创建文件: .vscode\settings.json
创建目录: docs
创建文件: docs\NetworkMonitor.md

模块项目 NetworkMonitor 已创建完成!
目录结构:
src/
Functions/
Public/
Private/
Classes/
NetworkMonitor.psm1
NetworkMonitor.psd1
tests/
Unit/
Integration/
NetworkMonitor.Tests.ps1
examples/
docs/
.github/workflows/ci.yml
.vscode/

注意事项

  1. 模板清单必须使用 UTF-8 编码。Plaster 解析 XML 时对编码敏感,如果清单文件包含中文且未使用 UTF-8 编码,会导致解析失败或乱码。保存文件时务必指定 -Encoding UTF8

  2. 模板变量语法为双花括号。Plaster 使用 {{PLASTER_PARAM_Name}} 引用参数,{{PLASTER_Date}} 获取当前日期。注意这与 PowerShell 本身的 $variable 语法不同,不要混淆。

  3. 条件表达式使用 PowerShell 语法<condition> 标签中的 condition 属性值会被当作 PowerShell 表达式求值,因此可以使用 -contains-eq-and-or 等运算符组合复杂条件。

  4. 模板文件扩展名建议使用 .t 后缀。例如 module.psm1.tci.yml.t。这样可以让编辑器识别它们为模板而非实际脚本,避免 IDE 的语法检查报错。

  5. 测试模板时使用 -Force 参数Invoke-Plaster 在目标目录已存在文件时会跳过覆盖。开发调试模板时加上 -Force 参数可以强制覆盖,方便反复测试。

  6. 团队共享模板建议打包为 PowerShell 模块。将模板文件嵌入到 .psm1 模块中并通过内部 NuGet 仓库分发,可以统一版本管理,避免团队成员使用过时模板。结合 RequiredModules 声明 Plaster 依赖,确保安装即用。

PowerShell 技能连载 - 模板引擎与代码生成

适用于 PowerShell 5.1 及以上版本

代码生成是提高效率的利器——从配置文件模板、项目脚手架、API 客户端,到重复性的 CRUD 代码,都可以用模板引擎自动生成。PowerShell 的字符串插值和 Here-String 天然适合模板渲染,结合哈希表的对象展开能力,可以构建灵活的模板引擎。无论是生成批量配置文件、搭建项目骨架,还是生成重复性代码,模板引擎都能大幅减少手工劳动。

本文将讲解 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
# 简单变量替换
function Expand-Template {
param(
[Parameter(Mandatory)]
[string]$Template,

[hashtable]$Variables
)

$result = $Template
foreach ($key in $Variables.Keys) {
$result = $result -replace "\{\{$key\}\}", $Variables[$key]
}
return $result
}

$configTemplate = @"
server {
listen {{port}};
server_name {{hostname}};

location / {
proxy_pass http://{{backend_host}}:{{backend_port}};
proxy_set_header Host `$host;
}

access_log /var/log/nginx/{{hostname}}.access.log;
}
"@

$nginxConfig = Expand-Template -Template $configTemplate -Variables @{
port = "8080"
hostname = "api.example.com"
backend_host = "127.0.0.1"
backend_port = "3000"
}

Write-Host $nginxConfig

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
server {
listen 8080;
server_name api.example.com;

location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
}

access_log /var/log/nginx/api.example.com.access.log;
}

高级模板引擎

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 Invoke-TemplateEngine {
param(
[Parameter(Mandatory)]
[string]$Template,

[Parameter(Mandatory)]
[hashtable]$Context
)

# 处理条件块 {{#if variable}}...{{/if}}
$Template = [regex]::Replace($Template, '\{\{#if\s+(\w+)\}\}(.*?)\{\{/if\}\}', {
param($m)
$varName = $m.Groups[1].Value
$content = $m.Groups[2].Value
if ($Context[$varName]) { $content } else { "" }
}, [System.Text.RegularExpressions.RegexOptions]::Singleline)

# 处理循环 {{#each items}}...{{/each}}
$Template = [regex]::Replace($Template, '\{\{#each\s+(\w+)\}\}(.*?)\{\{/each\}\}', {
param($m)
$varName = $m.Groups[1].Value
$body = $m.Groups[2].Value
$items = $Context[$varName]
if ($items -is [array]) {
($items | ForEach-Object {
$item = $_
$result = $body
if ($item -is [hashtable]) {
foreach ($key in $item.Keys) {
$result = $result -replace "\{\{this\.$key\}\}", $item[$key]
}
}
$result -replace '\{\{this\}\}', $item
}) -join ''
} else { "" }
}, [System.Text.RegularExpressions.RegexOptions]::Singleline)

# 处理简单变量
foreach ($key in $Context.Keys) {
if ($Context[$key] -isnot [array] -and $Context[$key] -isnot [hashtable]) {
$Template = $template -replace "\{\{$key\}\}", $Context[$key]
}
}

return $Template
}

# 使用高级模板
$dockerComposeTemplate = @"
version: '3.8'
services:
webapp:
image: {{registry}}/{{app_name}}:{{version}}
ports:
- "{{port}}:80"
environment:
- ASPNETCORE_ENVIRONMENT={{environment}}
{{#if debug}}
- ASPNETCORE_DETAILEDERRORS=true
{{/if}}
depends_on:{{#each services}}
- {{this.name}}
{{/each}}
"@

$composed = Invoke-TemplateEngine -Template $dockerComposeTemplate -Context @{
registry = "registry.example.com"
app_name = "myapp"
version = "2.5.0"
port = "8080"
environment = "production"
debug = $false
services = @(
@{ name = "redis" },
@{ name = "postgres" },
@{ name = "rabbitmq" }
)
}

Write-Host $composed

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
version: '3.8'
services:
webapp:
image: registry.example.com/myapp:2.5.0
ports:
- "8080:80"
environment:
- ASPNETCORE_ENVIRONMENT=production

depends_on:
- redis
- postgres
- rabbitmq

项目脚手架生成器

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
# 项目脚手架生成器
function New-ProjectScaffold {
param(
[Parameter(Mandatory)]
[string]$ProjectName,

[string]$BasePath = ".",

[string]$Author = $env:USERNAME,

[string]$Description = "PowerShell module project"
)

$projectDir = Join-Path $BasePath $ProjectName
$context = @{
ProjectName = $ProjectName
Author = $Author
Description = $Description
Year = (Get-Date).Year
Version = "0.1.0"
Guid = [guid]::NewGuid().ToString()
}

Write-Host "创建项目:$ProjectName" -ForegroundColor Cyan

# 创建目录结构
$dirs = @(
"source",
"source\Public",
"source\Private",
"tests",
"docs",
"examples"
)

foreach ($dir in $dirs) {
$fullPath = Join-Path $projectDir $dir
New-Item $fullPath -ItemType Directory -Force | Out-Null
Write-Host " 创建目录:$dir" -ForegroundColor DarkGray
}

# 生成模块清单
$manifest = @"
@{
RootModule = '$ProjectName.psm1'
ModuleVersion = '$($context.Version)'
GUID = '$($context.Guid)'
Author = '$($context.Author)'
Description = '$($context.Description)'
FunctionsToExport = @()
CmdletsToExport = @()
VariablesToExport = @()
AliasesToExport = @()
PrivateData = @{
PSData = @{
Tags = @('$ProjectName')
ProjectUri = 'https://github.com/user/$ProjectName'
}
}
}
"@
$manifest | Set-Content (Join-Path $projectDir "source\$ProjectName.psd1") -Encoding UTF8

# 生成模块入口
$module = @"
# 导入公共函数
`$public = Get-ChildItem -Path "`$PSScriptRoot\Public\*.ps1" -ErrorAction SilentlyContinue
foreach (`$func in `$public) {
. `$func.FullName
Export-ModuleMember -Function `$func.BaseName
}

# 导入私有函数
`$private = Get-ChildItem -Path "`$PSScriptRoot\Private\*.ps1" -ErrorAction SilentlyContinue
foreach (`$func in `$private) {
. `$func.FullName
}

Write-Host '$ProjectName 模块已加载' -ForegroundColor Green
"@
$module | Set-Content (Join-Path $projectDir "source\$ProjectName.psm1") -Encoding UTF8

# 生成 README
$sb = [System.Text.StringBuilder]::new()
$null = $sb.AppendLine("# $ProjectName")
$null = $sb.AppendLine("")
$null = $sb.AppendLine($context.Description)
$null = $sb.AppendLine("")
$null = $sb.AppendLine("## 安装")
$null = $sb.AppendLine("")
$null = $sb.AppendLine("``````powershell")
$null = $sb.AppendLine("Install-Module $ProjectName -Scope CurrentUser")
$null = $sb.AppendLine("``````")
$null = $sb.AppendLine("")
$null = $sb.AppendLine("## 使用")
$null = $sb.AppendLine("")
$null = $sb.AppendLine("``````powershell")
$null = $sb.AppendLine("Import-Module $ProjectName")
$null = $sb.AppendLine("``````")
$null = $sb.AppendLine("")
$null = $sb.AppendLine("## 许可证")
$null = $sb.AppendLine("")
$null = $sb.AppendLine("MIT License - $($context.Author)")
$sb.ToString() | Set-Content (Join-Path $projectDir "README.md") -Encoding UTF8

Write-Host "`n项目脚手架已生成:$projectDir" -ForegroundColor Green
Write-Host " 目录:$($dirs.Count) 个" -ForegroundColor Cyan
Write-Host " 文件:3 个(psd1, psm1, README.md)" -ForegroundColor Cyan
}

New-ProjectScaffold -ProjectName "MyUtils" -Description "实用的 PowerShell 工具函数模块"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
创建项目:MyUtils
创建目录:source
创建目录:source\Public
创建目录:source\Private
创建目录:tests
创建目录:docs
创建目录:examples

项目脚手架已生成:.\MyUtils
目录:6
文件:3 个(psd1, psm1, README.md)

批量配置文件生成

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
# 从 CSV 数据批量生成配置文件
function New-ConfigFromTemplate {
param(
[Parameter(Mandatory)]
[string]$TemplatePath,

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

[Parameter(Mandatory)]
[string]$OutputDir
)

$template = Get-Content $TemplatePath -Raw
$data = Import-Csv $DataPath -Encoding UTF8

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

foreach ($row in $data) {
$context = @{}
$row.PSObject.Properties | ForEach-Object { $context[$_.Name] = $_.Value }

$output = Invoke-TemplateEngine -Template $template -Context $context
$fileName = "$($row.Name -replace '\s', '-').config"
$outputPath = Join-Path $OutputDir $fileName

$output | Set-Content $outputPath -Encoding UTF8
Write-Host "已生成:$fileName" -ForegroundColor Green
}

Write-Host "`n批量生成完成:$($data.Count) 个文件" -ForegroundColor Cyan
}

# 使用模板批量生成 Nginx 配置
$nginxTemplate = @"
server {
listen {{port}};
server_name {{domain}};

root /var/www/{{name}};
index index.html;

location / {
try_files `$uri `$uri/ =404;
}

access_log /var/log/nginx/{{name}}.log;
}
"@

$nginxTemplate | Set-Content "C:\Templates\nginx.txt" -Encoding UTF8

$sites = @"
Name,Domain,Port
site1,api.example.com,443
site2,web.example.com,80
site3,docs.example.com,8080
"@ | ConvertFrom-Csv

$sites | Export-Csv "C:\Data\sites.csv" -NoTypeInformation -Encoding UTF8
New-ConfigFromTemplate -TemplatePath "C:\Templates\nginx.txt" -DataPath "C:\Data\sites.csv" -OutputDir "C:\Output\nginx"

执行结果示例:

1
2
3
4
5
已生成:site1.config
已生成:site2.config
已生成:site3.config

批量生成完成:3 个文件

注意事项

  1. 转义字符:模板中包含 PowerShell 特殊字符($、反引号)时需要正确转义
  2. 安全性:不要用 Invoke-Expression 渲染模板,存在代码注入风险。使用字符串替换或正则
  3. 编码:生成的文件始终指定 UTF-8 编码,避免中文乱码
  4. 幂等性:脚手架生成器应该可以重复执行,已存在的文件不覆盖或提示用户
  5. 模板来源:模板可以存储在文件、数据库或远程仓库中,方便版本管理
  6. 复杂逻辑:如果模板需要复杂条件判断,考虑使用专门的模板引擎模块(如 PSTemplates