Rendre la prévisualisation et la confirmation honnêtes

ShellBeginner
Pratiquer maintenant

Introduction

Une commande de changement de salle doit mériter la confiance de l'organisateur : la prévisualisation ne doit laisser aucun travail, le refus ne doit rien faire et l'approbation doit appliquer exactement le plan admissible complet. Déclarer la prise en charge de WhatIf ne suffit pas à protéger toutes les méthodes d'écriture de fichiers.

Ce laboratoire indépendant démarre avec son propre événement et un script incomplet. Utilisez les compétences de validation par ID, JSON, paramètres et gestion des erreurs du laboratoire guidé précédent ; aucun fichier d'une ancienne VM n'est nécessaire. PowerShell et les ressources synthétiques sont préparés. Vous utiliserez le Terminal par défaut, effectuerez de véritables confirmations natives et rouvrirez l'événement réellement enregistré. La durée est une estimation de l'auteur comprenant la lecture et la récupération après erreur.

Placer toute écriture derrière une seule décision

Dans cette étape, vous corrigerez une implémentation trompeuse de WhatIf afin que la prévisualisation et le refus de confirmation laissent l'espace de travail inchangé.

Comprendre pourquoi le script initial est dangereux

Ouvrez Terminal, initialement un shell Linux. Démarrez une session PowerShell propre avec des paramètres UTF-8 limités à la session de ce rôle. env transmet ces paramètres uniquement à ce programme ; -NoLogo masque la bannière et -NoProfile ignore les profils personnels :

env LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 pwsh -NoLogo -NoProfile

Attendez l'invite PS. Toutes les commandes suivantes sont des commandes PowerShell. Ce laboratoire possède son propre scénario préparé, indépendant des VM précédentes :

Set-Location /home/labex/project/room-changes

L'événement source et les demandes utilisent le même format que dans le laboratoire précédent. Lisez-les avant de modifier votre script :

Get-Content ./requests.json -Raw
./Show-RoomSchedule.ps1 -Path ./event.json

Le script initial apply-room-changes.ps1 est volontairement incomplet. Il déclare prendre en charge WhatIf mais écrit via .NET sans demander l'autorisation, et n'implémente pas les modifications complètes des salles. Essayez-le uniquement avec cette nouvelle sortie de démonstration jetable :

pwsh -NoLogo -NoProfile -File ./apply-room-changes.ps1 -EventPath ./event.json -PlanPath ./requests.json -OutputPath ./unsafe-preview.json -WhatIf

Il affiche Plan applied.. Ce message ne prouve ni que le bon plan a été appliqué ni que la prévisualisation a été respectée. Vérifiez le véritable fichier :

Test-Path ./unsafe-preview.json
True

Déclarer un paramètre commun ne protège pas automatiquement les écritures .NET arbitraires. L'événement d'origine est toujours intact, mais cette prévisualisation n'aurait dû créer aucune sortie. Supprimez uniquement ce fichier jetable avec Remove-Item -LiteralPath, sans toucher aux entrées ni aux notes :

Remove-Item -LiteralPath ./unsafe-preview.json

Conserver l'événement complet

Les entrées event.json, requests.json, small-request.json, bad-request.json et le fichier sans rapport notes.txt doivent conserver leurs octets, mode de fichier, propriétaire et mtime. Ne modifiez pas le lecteur préparé. Vous pouvez remplacer votre apply-room-changes.ps1 et créer le nouveau résultat nommé. L'événement et les demandes sont fournis en JSON UTF-8 bien formé. L'événement contient des ID exacts uniques, des valeurs entières non négatives pour les participants et les capacités, ainsi qu'une liste d'approbations. Les demandes forment un tableau non vide d'ID uniques de sessions et d'ID de salles cibles. Les noms sont des libellés d'affichage ; les ID sont sensibles à la casse.

Les valeurs inconnues peuvent comprendre des objets imbriqués, des tableaux ordonnés, null, des booléens, des chaînes et des nombres JSON finis bornés, jusqu'à 20 niveaux. Les clés d'objet exactement dupliquées et les schémas mal formés arbitraires sortent du périmètre. Utilisez ConvertFrom-Json -AsHashtable -DateKind String pour conserver les clés inconnues distinctes par la casse et maintenir les chaînes ressemblant à des dates avec leurs décalages d'origine exacts. Ces options ont été présentées dans le laboratoire précédent. La source contient à la fois When et when ; ne les fusionnez pas. Utilisez ConvertTo-Json -Depth 100 pour l'événement complet afin de préserver ses objets imbriqués inconnus. La profondeur par défaut de deux ne suffit pas. Des écritures numériques finies égales comme 1 et 1.0 sont acceptables ; les booléens doivent rester booléens, l'ordre des tableaux doit être conservé et l'ordre des propriétés d'objet est cosmétique.

La sortie doit être un nouveau fichier sous un répertoire parent réel déjà existant, distinct de chaque entrée. La chaîne de répertoires du cours est réelle ; aucun autre processus n'écrit simultanément dans ces fichiers. Refusez les fichiers, répertoires et liens existants, y compris les liens cassés, sans les modifier. Un plan non valide doit refuser l'invocation entière avant toute écriture : une ligne valide précédente ne justifie pas un ID inconnu ultérieur, une session non approuvée, un doublon ou un dépassement de capacité. L'égalité avec la capacité est valide. Les salles cibles sont déjà libres aux horaires concernés.

Utiliser ShouldProcess natif

Votre appel réutilisable accepte EventPath, PlanPath et OutputPath, comme le validateur. Il doit être autonome avec ces arguments et les commandes installées, sans auxiliaires voisins. Conservez toute la validation avant la décision d'autorisation, puis enregistrez l'événement complet en ne changeant que les champs RoomId demandés.

Lisez ces ajouts avant d'enregistrer le code :

  • [CmdletBinding(SupportsShouldProcess)] ajoute les paramètres communs natifs -WhatIf et -Confirm. N'inventez pas un paramètre booléen qui se contente d'afficher une prévisualisation.
  • $PSCmdlet est l'objet de commande automatique de PowerShell. $PSCmdlet.ShouldProcess(target, action) indique si l'opération doit être effectuée. Avec WhatIf, il affiche l'action proposée et renvoie false ; avec Confirm, il pose une question et ne renvoie true qu'en cas d'approbation.
  • Appelez-le une fois pour le plan validé complet. La cible identifie la sortie et l'action nomme chaque ID exact de session et les ID d'ancienne et de nouvelle salle. ForEach-Object construit cette description ; -join '; ' assemble ses chaînes. Seule la branche true peut modifier des objets ou écrire des fichiers. Un refus ne doit créer ni répertoires de sortie, ni fichiers, ni travail temporaire, ni modifier le travail existant.
  • Il s'agit d'une confirmation globale, pas d'une question par session. Une méthode .NET ne génère pas de seconde question native ici. Si vous utilisez ensuite un cmdlet interne prenant en charge la confirmation, supprimez sa question supplémentaire uniquement dans la branche déjà autorisée ; ne contournez jamais la décision extérieure.
  • Après approbation, chaque Session mémorisée désigne l'objet correspondant dans l'événement complet. Mettre à jour son RoomId conserve tous les autres champs. L'appel .NET WriteAllText et l'encodage UTF-8 explicite sont le même mécanisme que dans le laboratoire précédent, désormais protégé par ShouldProcess.

La résolution des chemins, l'analyse de l'objet original, les tableaux de correspondances, les comparaisons sensibles à la casse, la gestion des erreurs et la collecte des modifications avant leur utilisation suivent le laboratoire précédent. Enregistrez le remplacement complet avec la here-string littérale ; il ne remplace que votre script :

Set-Content -Path ./apply-room-changes.ps1 -Encoding utf8 -Value @'
[CmdletBinding(SupportsShouldProcess)]
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]@{
            Session = $sessions[0]
            SessionId = $request.SessionId
            OldRoomId = $sessions[0].RoomId
            NewRoomId = $request.RoomId
        }
    }
    $description = ($changes | ForEach-Object { "$($_.SessionId): $($_.OldRoomId) -> $($_.NewRoomId)" }) -join '; '
    if ($PSCmdlet.ShouldProcess($fullOutput, "Apply complete room plan [$description]")) {
        foreach ($change in $changes) { $change.Session.RoomId = $change.NewRoomId }
        $json = ConvertTo-Json -InputObject $event -Depth 100
        [System.IO.File]::WriteAllText($fullOutput, $json + [Environment]::NewLine, [System.Text.UTF8Encoding]::new($false))
        Write-Output "Saved: $fullOutput"
    }
}
catch {
    Write-Error ("Room change failed: {0}" -f $_.Exception.Message) -ErrorAction Continue
    exit 1
}
'@

Prévisualiser, puis refuser l'opération entière

La commande suivante doit émettre un message WhatIf natif décrivant à la fois s-a: r-old -> r-equal et s-c: r-old -> r-large, avec le nouveau chemin de sortie comme cible :

pwsh -NoLogo -NoProfile -File ./apply-room-changes.ps1 -EventPath ./event.json -PlanPath ./requests.json -OutputPath ./event-updated.json -WhatIf

Lisez immédiatement le code de sortie natif de l'enfant :

$LASTEXITCODE
0

Demandez maintenant une confirmation native. Attendez la véritable question, lisez son action complète, saisissez N, puis appuyez sur Entrée. Ne collez pas N avant l'apparition de la question :

pwsh -NoLogo -NoProfile -File ./apply-room-changes.ps1 -EventPath ./event.json -PlanPath ./requests.json -OutputPath ./event-updated.json -Confirm

Vous devez voir Confirm, l'action complète et des options dont [Y] Yes et [N] No. Il n'y a qu'une question pour les deux modifications. No ignore l'opération ; ce n'est pas une erreur de validation, donc l'enfant se termine avec succès sans effectuer de travail :

$LASTEXITCODE
0
Test-Path ./event-updated.json
False

Relisez l'original au lieu de faire confiance à une variable en mémoire :

Get-Content ./event.json -Raw

Tous les RoomId des sessions restent à r-old et les notes imbriquées sont toujours présentes. Ni WhatIf ni No ne doivent laisser de fichier, de répertoire ou de candidat temporaire. La vérification fonctionnelle lit vos fichiers originaux et teste le script exact dans des PTY PowerShell jetables, avec notamment de véritables entrées N/Y. Ces tests vérifient le comportement du programme ; ils ne créent ni ne revendiquent un historique de vos confirmations dans le scénario réel.

Si un chemin est erroné, inspectez Get-Location et Get-ChildItem, puis revenez avec Set-Location /home/labex/project/room-changes. Corrigez une faute dans le script en enregistrant à nouveau la here-string complète corrigée. Si vous approuvez trop tôt par accident, conservez la source et supprimez uniquement votre event-updated.json avec Remove-Item -LiteralPath ./event-updated.json avant de vous entraîner à nouveau au refus. Ne supprimez ni ne modifiez jamais l'événement, les demandes ou les notes d'origine pour masquer une erreur.

Approuver et rouvrir le véritable programme

Dans cette étape, vous approuverez le plan validé complet, utiliserez l'événement enregistré et vérifierez le refus d'une demande ultérieure non valide.

Commencez dans la même session PowerShell de Terminal, dans /home/labex/project/room-changes. Votre script corrigé est toujours enregistré depuis l'étape précédente ; la sortie finale n'existe pas encore. Si vous avez ouvert un autre Terminal, utilisez le même lancement PowerShell explicite en UTF-8 et le même Set-Location qu'à cette étape. Ce script ne nécessite pas de variables de session, car chaque chemin est un argument.

Appliquer un plan approuvé complet

Relancez l'appel de confirmation. Attendez la question native, lisez les deux correspondances d'ID, saisissez Y, puis appuyez sur Entrée :

pwsh -NoLogo -NoProfile -File ./apply-room-changes.ps1 -EventPath ./event.json -PlanPath ./requests.json -OutputPath ./event-updated.json -Confirm

Attendez une question suivie de Saved: et du chemin de sortie. Lisez immédiatement son code de sortie natif :

$LASTEXITCODE
0

Cela confirme une fois le plan validé entier. Ce n'est pas une garantie de transaction en cas de plantage, de coupure de courant ou d'écritures concurrentes. L'entrée reste l'original ; le résultat est un événement complet distinct. Un échec d'écriture est une erreur, pas une autorisation d'affirmer que le programme a été appliqué.

Le lecteur préparé lit exactement le fichier passé à -Path. Il ne régénère pas un résultat manquant et ne se rabat pas sur event.json :

./Show-RoomSchedule.ps1 -Path ./event-updated.json

Les lignes réellement enregistrées indiquent désormais s-a dans r-equal, s-b toujours dans r-old et s-c dans r-large. Deux sessions portent toujours Shared maps et deux salles portent toujours Studio ; les ID les distinguent.

Rouvrez la sortie complète comme dictionnaires en maintenant les textes ressemblant à des horodatages sous forme de chaînes :

$saved = Get-Content ./event-updated.json -Raw | ConvertFrom-Json -AsHashtable -DateKind String

Un point descend dans les propriétés imbriquées. Les métadonnées originales de la première session contiennent une chaîne ressemblant à une date et une clé dont la casse est différente. L'accès entre crochets utilise chaque clé exacte du dictionnaire. Lisez-les séparément :

$saved.Sessions[0].Metadata.contact['When']
2026-11-07T09:30:00+05:30
$saved.Sessions[0].Metadata.contact['when']
Keep this distinct key

Lisez une autre valeur inconnue et la note originale sans rapport avec la tâche :

$saved.Unknown.offset
2027-01-02T12:34:56-07:00
Get-Content ./notes.txt
Venue manager: preserve this personal access note.

Ces observations accompagnent la comparaison sémantique complète ; deux champs conservés ne suffiraient pas à établir que tous les objets inconnus ont survécu. Ne reconstruisez pas l'événement à partir d'une projection ne contenant que les colonnes affichées.

Empêcher une demande ultérieure non valide de publier une modification partielle

Le fichier préparé bad-request.json possède une première demande valide et une salle inconnue dans la seconde. Utilisez une autre sortie nouvelle pour préserver le résultat terminé. L'appel suivant échoue volontairement avant toute confirmation :

pwsh -NoLogo -NoProfile -File ./apply-room-changes.ps1 -EventPath ./event.json -PlanPath ./bad-request.json -OutputPath ./rejected-event.json -Confirm

Attendez Room change failed: et une explication concernant la salle inconnue, sans question de confirmation. Lisez immédiatement le résultat :

$LASTEXITCODE
1
Test-Path ./rejected-event.json
False

La demande valide précédente n'a pas été appliquée ni publiée seule. Remettez l'argument sur requests.json pour les futurs travaux légitimes ; ne modifiez pas l'exemple incorrect fourni. L'événement d'origine et le fichier terminé event-updated.json restent disponibles.

Une destination existante constitue également un refus volontaire. Vous pouvez l'observer avec WhatIf, qui valide toujours les préconditions avant toute autorisation :

pwsh -NoLogo -NoProfile -File ./apply-room-changes.ps1 -EventPath ./event.json -PlanPath ./requests.json -OutputPath ./event-updated.json -WhatIf
$LASTEXITCODE
1

Attendez Output already exists; choose a new path.. La prévisualisation ne permet pas d'ignorer des arguments non valides. Conservez le résultat existant ; une nouvelle invocation nécessite un nouveau chemin de sortie. Si vous voulez volontairement reconstruire le résultat canonique du laboratoire après avoir corrigé votre script, supprimez uniquement votre sortie et répétez la confirmation valide. Les originaux restent la référence de récupération.

La vérification finale lit le véritable résultat complet et la référence de conservation. Elle teste les octets originaux de votre script sur des événements isolés avec d'autres ID, une capacité égale, des enregistrements de même titre, des données imbriquées et des chemins d'échec. Les boucles et implémentations d'objets équivalentes sont acceptées. Elle compare les valeurs et types JSON, pas l'ordre des propriétés d'objet, l'écriture des commandes, l'éditeur choisi ni les messages de réussite. Répéter la vérification n'applique, ne réinitialise ni ne répare votre événement réel.

Résumé

Vous avez corrigé une prévisualisation trompeuse, validé le plan complet avant toute écriture et utilisé une seule décision ShouldProcess native. WhatIf et No ont laissé le travail inchangé ; Yes a enregistré un événement complet avec les relations d'ID demandées et toutes les autres valeurs imbriquées préservées. Vous avez rouvert ce véritable fichier et confirmé qu'une demande ultérieure non valide ou une sortie existante sont refusées plutôt que publiées ou remplacées silencieusement.