介绍
参加人数增加后,Field Notes 批准了两项工作坊房间变更。多个场次标题和房间名称重复,因此安全的计划需要精确身份、批准范围和足够的目标容量。
你将预览一个可工作的简单变更,再构建可复用的 PowerShell 计划验证器。本实验以前置 PowerShell for Beginners 中的对象、JSON、筛选、参数和错误处理为基础;不要求 Bash 脚本知识。工具和原始合成活动数据已在独立工作区中准备好。时长为作者估计,包含阅读和普通错误处理。
构建完整的基于 ID 的计划
本步骤先预览一项准备好的变更,再构建可复用的验证器,在不改变排期的前提下保存完整业务计划。
进入 PowerShell 并预览一项请求
打开 Terminal,初始运行的是 Linux shell。下面的启动命令仅为本次 PowerShell 会话设置 UTF-8,不会改变全局 shell。env 将这些环境值传给程序。-NoLogo 隐藏启动横幅,-NoProfile 跳过个人启动脚本:
env LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 pwsh -NoLogo -NoProfile
等待以 PS 开头的提示符。后续所有命令都在 PowerShell 内运行。Set-Location 更改其工作目录:
Set-Location /home/labex/project/room-changes
准备好的 preview-one.ps1 只处理提供的单项演练请求。它支持 PowerShell 原生的 -WhatIf:描述动作而不执行。这是可工作的简单示例,还不是你要构建的可复用多请求验证器。./ 表示当前目录中的脚本:
./preview-one.ps1 -WhatIf
预期出现包含 Move s-a: r-old -> r-equal 和目标 ./rehearsal-only.json 的原生消息。ID 标明场次、原房间和拟迁入的房间。它没有移动所有同标题的场次。用 Get-Content -Raw 读取真实源 JSON,该命令将整个文件作为一个字符串返回:
Get-Content ./event.json -Raw
s-a 记录的 RoomId 仍为 r-old。Test-Path 只检查是否存在,不会创建任何内容:
Test-Path ./rehearsal-only.json
False
如果误漏了 -WhatIf,演练可能创建那个独立文件,但不会覆盖输入。Remove-Item -LiteralPath ./rehearsal-only.json 只删除演练输出;仅在该文件存在时使用,然后正确重试预览。-LiteralPath 按字面解释路径,不展开通配符。不要为了从错误中恢复而删除原始活动数据或请求。
构建工具前先读懂关系
场地方已经确认目标房间在这些时间可用。你要检查的是批准范围和容量,而不是解决日历冲突。读取请求映射,并用准备好的只读读取器查看真实原始排期:
Get-Content ./requests.json -Raw
./Show-RoomSchedule.ps1 -Path ./event.json
请求将 s-a 移至 r-equal,将 s-c 移至 r-large。s-a 和 s-b 的标题都是 Shared maps;本次请求中只有 s-a 获准。还有两个房间都叫 Studio。务必按 SessionId 和 RoomId 关联,不要使用显示名称。第一项请求的场次有 24 人,目标容量恰好为 24:相等是允许的。
event.json、requests.json、small-request.json、bad-request.json 及无关的 notes.txt 都是必须保留的原始文件,包括字节、文件模式、所有者和修改时间(mtime)。不要编辑准备好的工具。可以创建自己的脚本及本实验指定的新结果文件。课程路径及其整条目录链都是真实目录。输出必须是新文件,与输入不同,且其真实父目录必须已经存在;对于已有文件、目录或链接,即使是悬空链接,也必须拒绝且不改变它。操作时没有其他并发写入者。
提供的 JSON 是格式正确的 UTF-8 数据:一个包含 Rooms、Sessions 和 ApprovedSessionIds 的活动对象,以及一个非空请求数组,其中包含唯一的精确场次 ID 和目标房间 ID。ID 是区分大小写的字符串。人数和容量是非负整数值。未知元数据可以包含嵌套对象、有序数组、字符串、布尔值、null 及有界有限 JSON 数值,嵌套不超过 20 层。完全重复的对象键、格式不符的数据结构和任意恶意输入不在本课程范围内。
保存任何计划前验证每一项请求
你将创建 Build-RoomPlan.ps1,带有三个命名参数:EventPath、PlanPath 和 OutputPath。它必须通过这些参数和已安装命令处理其他提供的活动和 ID,不依赖相邻辅助文件。结果是按请求顺序排列的 JSON 数组。每行恰好包含 SessionId、OldRoomId、NewRoomId、Attendees 和 Capacity。这是描述当前对象关系的业务计划,不是自称完成工作的标记。它不会改变排期。下一个实验才会写出完整的更新后活动数据。
保存脚本前,先了解以下新增内容:
[CmdletBinding()]将其声明为参数化高级脚本;[Parameter(Mandatory)]要求提供参数,[string]声明文本类型。try、catch、throw和exit 1沿用前置错误处理实验。$ErrorActionPreference = 'Stop'使命令错误终止当前操作。catch用Write-Error -ErrorAction Continue显示原因,随后执行明确的exit 1。- PowerShell 的
$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath(...)根据当前 PowerShell 目录解析输出。Split-Path -LiteralPath返回其父路径。Get-Item -Force也能发现隐藏对象和悬空链接。这里的-ErrorAction SilentlyContinue允许新输出按预期尚不存在。Test-Path -PathType Container要求父目录存在。 ConvertFrom-Json -AsHashtable将 JSON 对象保留为字典,包括仅大小写不同的未知键。-DateKind String将类似日期的字符串保持为字符串,保留原始偏移量;准备好的 PowerShell 版本支持该选项。即使请求数组只有一个成员,-NoEnumerate也会将其作为完整数组保留。@(...)将匹配项收集为数组,即使匹配数为零或一。-ceq区分大小写比较。-ccontains检查精确成员关系;-cnotcontains是其反向判断。-ne表示不等于,-gt表示大于,-or表示任一条件成立。-isnot [array]拒绝非数组值。$seen += ...和$changes += ...向集合追加内容。未知 ID、未获准场次、重复请求或人数过多,都会在写入任何输出前抛出错误。-gt允许容量恰好等于人数。[pscustomobject]@{...}收集计划的已知字段。ConvertTo-Json -InputObject $changes -Depth 5序列化完整数组;足够的深度可避免将嵌套对象转换为显示字符串。JSON 属性顺序和缩进不重要,数组顺序则必须保留。布尔值不同于数值,而1和1.0这样的相等有限数值写法代表同一个 JSON 数值。- PowerShell 可以用
[Type]::Method(...)调用 .NET 方法。[System.IO.File]::WriteAllText将文本写入选定路径。[Environment]::NewLine添加换行,[System.Text.UTF8Encoding]::new($false)创建不带字节顺序标记的 UTF-8 编码。此方法不会自动遵守 WhatIf;下一个实验会将它放入明确的授权分支中。
用字面 here-string 保存完整脚本:@' 开始,独占一行的 '@ 结束。单引号使保存时保留脚本中的变量。Set-Content -Encoding utf8 只替换此脚本文件:
Set-Content -Path ./Build-RoomPlan.ps1 -Encoding utf8 -Value @'
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$EventPath,
[Parameter(Mandatory)][string]$PlanPath,
[Parameter(Mandatory)][string]$OutputPath
)
try {
$ErrorActionPreference = 'Stop'
$fullOutput = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath)
if (Get-Item -LiteralPath $fullOutput -Force -ErrorAction SilentlyContinue) {
throw 'Output already exists; choose a new path.'
}
$parent = Split-Path -LiteralPath $fullOutput
if (-not (Test-Path -LiteralPath $parent -PathType Container)) {
throw 'Output parent must already exist.'
}
$event = Get-Content -LiteralPath $EventPath -Raw | ConvertFrom-Json -AsHashtable -DateKind String
$requests = Get-Content -LiteralPath $PlanPath -Raw | ConvertFrom-Json -AsHashtable -DateKind String -NoEnumerate
if ($requests -isnot [array] -or $requests.Count -eq 0) { throw 'Plan must be a nonempty array.' }
$changes = @()
$seen = @()
foreach ($request in $requests) {
if ($seen -ccontains $request.SessionId) { throw "Duplicate session ID: $($request.SessionId)" }
$seen += $request.SessionId
$sessions = @($event.Sessions | Where-Object { $_.SessionId -ceq $request.SessionId })
$rooms = @($event.Rooms | Where-Object { $_.RoomId -ceq $request.RoomId })
if ($sessions.Count -ne 1) { throw "Unknown or ambiguous session ID: $($request.SessionId)" }
if ($rooms.Count -ne 1) { throw "Unknown or ambiguous room ID: $($request.RoomId)" }
if ($event.ApprovedSessionIds -cnotcontains $request.SessionId) { throw "Unapproved session ID: $($request.SessionId)" }
if ($sessions[0].Attendees -gt $rooms[0].Capacity) { throw "Capacity exceeded: $($request.SessionId) -> $($request.RoomId)" }
$changes += [pscustomobject]@{
SessionId = $request.SessionId
OldRoomId = $sessions[0].RoomId
NewRoomId = $request.RoomId
Attendees = $sessions[0].Attendees
Capacity = $rooms[0].Capacity
}
}
$json = ConvertTo-Json -InputObject $changes -Depth 5
[System.IO.File]::WriteAllText($fullOutput, $json + [Environment]::NewLine, [System.Text.UTF8Encoding]::new($false))
Write-Output "Saved plan: $fullOutput"
}
catch {
Write-Error ("Plan failed: {0}" -f $_.Exception.Message) -ErrorAction Continue
exit 1
}
'@
在子 PowerShell 进程中运行验证器。-NonInteractive 禁止无人值守的提示,-File 指定脚本。子进程的 exit 1 不会关闭你的交互会话:
pwsh -NoLogo -NoProfile -NonInteractive -File ./Build-RoomPlan.ps1 -EventPath ./event.json -PlanPath ./requests.json -OutputPath ./validated-plan.json
$LASTEXITCODE 是最近一个外部程序的原生退出状态。立即读取:
$LASTEXITCODE
0
重新打开真实产物:
Get-Content ./validated-plan.json -Raw
其中包含两行请求:s-a、r-old、r-equal、24、24;以及 s-c、r-old、r-large、18、40。原始排期保持不变。已有输出会被明确拒绝,不会被静默替换。如果需要重新构建自己的计划,仅用 Remove-Item -LiteralPath ./validated-plan.json 删除 validated-plan.json,然后再次运行修正后的验证器。
将后续无效请求作为整体拒绝
bad-request.json 的第一项变更合法,后面却指定了未知目标房间。下一条命令预期失败:
pwsh -NoLogo -NoProfile -NonInteractive -File ./Build-RoomPlan.ps1 -EventPath ./event.json -PlanPath ./bad-request.json -OutputPath ./bad-plan-result.json
预期出现 Plan failed: 和未知房间的说明。立即读取原生结果:
$LASTEXITCODE
1
Test-Path ./bad-plan-result.json
False
前面合法的那一行没有作为部分计划发布。未知场次 ID、未批准 ID、重复请求和超出容量,也必须在写入前拒绝整次调用。请求文件为空或缺失,不是成功的空任务。
遇到普通路径拼写错误,用 Get-Location 和 Get-ChildItem 检查目录与名称,再用 Set-Location /home/labex/project/room-changes 返回。遇到脚本拼写错误,重新保存完整修正后的 here-string,仅替换自己的脚本。不要编辑源数据来让错误消失。最终检查会读取真实业务计划和原始属性,并在使用不同 ID 的隔离活动数据中运行你的原样脚本字节。它不会接受固定的初始答案列表,也不会更改你的实际排期。
总结
你使用了诚实的原生预览,通过精确 ID 关联场次与房间,检查批准和容量,并保存完整且已验证的业务计划。容量相等被接受,同标题记录保持区分,后续无效请求没有生成部分计划。原始活动数据和笔记保持不变;下一个实验将通过原生确认应用整个已验证计划。



