正確な変更セットを作成する

ShellBeginner
オンラインで実践に進む

はじめに

参加者の増加を受け、Field Notes はワークショップの部屋変更を 2 件承認しました。複数のセッションタイトルと部屋名が重複するため、安全な計画には正確な識別、承認範囲、十分な変更先の定員が必要です。

動作する小さな変更をプレビューしてから、再利用可能な PowerShell 計画検証スクリプトを作ります。この実験は PowerShell for Beginners のオブジェクト、JSON、フィルター、パラメーター、エラー処理を前提とし、Bash スクリプトは不要です。ツールと元の合成イベントデータは、独立した作業領域に準備されています。所要時間は、読む時間と通常の間違いを含む作者の見積もりです。

ID に基づく完全な計画を作成する

このステップでは、準備済みの変更を 1 件プレビューしてから、スケジュールを変更せずに完全な業務計画を保存する再利用可能な検証スクリプトを作成します。

PowerShell に入り、1 件の要求をプレビューする

Terminal を開きます。最初は Linux シェルが動作しています。次の起動コマンドは、この PowerShell セッションだけに UTF-8 を設定し、グローバルなシェルは変更しません。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 は、提供された 1 件の練習用要求だけを処理します。PowerShell ネイティブの -WhatIf に対応し、操作を実行する代わりにその内容を表示します。これは動作する小さな例であり、これから作る複数要求用の再利用可能な検証スクリプトではありません。./ は現在のディレクトリにあるスクリプトを示します。

./preview-one.ps1 -WhatIf

Move s-a: r-old -> r-equal と対象 ./rehearsal-only.json を含むネイティブメッセージが表示されるはずです。ID はセッションと、現在の部屋および変更先の部屋を識別します。同じタイトルのセッションすべてを移動したわけではありません。実際の元 JSON を Get-Content -Raw で読みます。このコマンドはファイル全体を 1 つの文字列として返します。

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 という部屋も 2 つあります。表示名ではなく、必ず 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 階層以内です。完全に重複するオブジェクトキー、不正なスキーマ、任意の悪意ある入力は、このコースの対象外です。

計画を保存する前にすべての要求を検証する

EventPath、PlanPath、OutputPath の 3 つの名前付きパラメーターを持つ Build-RoomPlan.ps1 を作成します。これらの引数とインストール済みのコマンドを使い、隣接する補助ファイルに依存せず、別の提供イベントや 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 は、要素が 1 つだけでも要求配列を配列のまま維持します。
  • @(...) は、一致がゼロまたは 1 件でも配列にまとめます。-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

要求された 2 行、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 でセッションと部屋を関連付け、承認と定員を検証して、完全な検証済み業務計画を保存しました。定員との一致は受け入れられ、同タイトルのレコードは区別され、後続の無効な要求から部分計画は生成されませんでした。元イベントとノートは変更されていません。次の実験では、ネイティブ確認を通して検証済みの計画全体を適用します。