LESSON 3 OF 4
A complete validated CSV report
Prepare the source file
Save the following fixture as services.csv in a practice directory. Save the script after it as Report-Services.ps1. Do not use a production export for your first run.
Host,Service,Status,UptimeHours
APP01,BackupAgent,Running,12
APP02,BackupAgent,Stopped,0
APP03,BackupAgent,Running,1
APP04,BackupAgent,Running,2
APP05,BackupAgent,Unknown,
APP06,BackupAgent,Running,offline
Before running the script, predict the output for each row. You should get six output rows: two Review, two NoFinding, and two Invalid. Explain each classification before comparing your answer with the exported file. Reconcile the rows rather than accepting a plausible total from a tutorial or a generated answer.
The following implementation makes no endpoint changes. It writes a report to a new file and refuses to overwrite an existing file.
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$InputPath,
[Parameter(Mandatory)][string]$OutputPath
)
function Get-ServiceAssessment {
param([Parameter(Mandatory)]$Row)
$hostName = ([string]$Row.Host).Trim()
$serviceName = ([string]$Row.Service).Trim()
$state = ([string]$Row.Status).Trim()
$rawUptime = ([string]$Row.UptimeHours).Trim()
$reason = ''
$assessment = 'Invalid'
$uptime = 0
if ($hostName -eq '' -or $serviceName -eq '') {
$reason = 'Missing identity'
} elseif ($state -notin @('Running','Stopped')) {
$reason = 'Unsupported status'
} elseif ($rawUptime -notmatch '^[0-9]+$' -or
-not [int]::TryParse($rawUptime, [ref]$uptime)) {
$reason = 'Uptime must be a non-negative 32-bit whole number'
} else {
$state = if ($state -eq 'Running') { 'Running' } else { 'Stopped' }
if ($state -eq 'Stopped') {
$assessment = 'Review'
$reason = 'Service is stopped in the observation'
} elseif ($uptime -lt 2) {
$assessment = 'Review'
$reason = 'Running service has less than 2 hours uptime'
} else {
$assessment = 'NoFinding'
$reason = 'No finding under this rule'
}
}
[pscustomobject]@{
Host = $hostName
Service = $serviceName
Status = $state
SourceUptimeHours = [string]$Row.UptimeHours
Assessment = $assessment
Reason = $reason
}
}
try {
$source = (Resolve-Path -LiteralPath $InputPath -ErrorAction Stop).Path
$destination = [System.IO.Path]::GetFullPath($OutputPath)
if ($source -eq $destination) { throw 'Input and output must differ' }
if (Test-Path -LiteralPath $destination) { throw 'Output already exists' }
$rows = @(Import-Csv -LiteralPath $source -Encoding utf8 -ErrorAction Stop)
if ($rows.Count -eq 0) { throw 'Input must contain observations' }
$actual = @($rows[0].PSObject.Properties.Name)
$required = @('Host','Service','Status','UptimeHours')
if (@(Compare-Object $required $actual).Count -ne 0) {
throw 'CSV columns do not match the expected schema'
}
$report = @($rows | ForEach-Object { Get-ServiceAssessment -Row $_ })
$report | Select-Object Host, Service, Status, SourceUptimeHours, Assessment, Reason |
Export-Csv -LiteralPath $destination -NoTypeInformation -Encoding utf8 -NoClobber -ErrorAction Stop
$report | Group-Object Assessment | Select-Object Name, Count
} catch {
Write-Error -Message $_.Exception.Message -ErrorAction Continue
exit 2
}
Run it in a child PowerShell process so a deliberate failing test cannot exit your interactive terminal:
pwsh -NoProfile -File ./Report-Services.ps1 -InputPath ./services.csv -OutputPath ./report.csv
$LASTEXITCODE
Import-Csv ./report.csv | Select-Object Host, Assessment, Reason
The normal exit code is 0. Read the exported file and reconcile its six observations with the fixture. APP02 and APP03 need review. APP01 and APP04 have no finding. APP05 and APP06 are invalid. The original raw uptime appears in SourceUptimeHours, which helps a reviewer diagnose why a row was rejected.
Why these choices matter
Resolve-Path -LiteralPath interprets the given path as a literal name and makes a missing source an explicit error. The script should not treat a file that never arrived as a healthy population of zero services. GetFullPath computes a destination path; it does not prove that its directory exists or that the caller can write there. Export remains an operation that can fail.
-ErrorAction Stop promotes relevant non-terminating errors to a terminating error that the catch block can handle. PowerShell has more than one error behavior. A failed command can emit an error and continue unless you intentionally establish a stopping boundary. Test that behavior rather than assuming that the existence of try guarantees every failure is caught.
-NoClobber protects an existing report at the export operation, not just at the preceding check. A file could appear between the check and the write. The final command must preserve the no-overwrite contract too.
TryParse avoids accepting a value outside the chosen 32-bit range. The regular expression narrows accepted text to digits: negative signs, decimal points and a word such as offline are invalid. This is a policy choice for this fixture, not a universal rule for all numeric data. A future schema could reasonably permit decimals; it would need a changed contract and independent tests.
The code checks the property names produced by Import-Csv. It does not implement a strict raw CSV parser that rejects every malformed row length or duplicate header before import. Do not describe it as complete file-format validation. Add a parser or an upstream validation step before operational use if those failure modes matter. A dependable course explains its example's limits instead of hiding them behind a “production ready” label.
Inject failures safely
Keep the original fixture unchanged and make separate test copies. Change APP04 uptime from 2 to 1; one result must move from NoFinding to Review. Change it to -1; it must become Invalid, not Review. Change APP06 to 2147483648; it exceeds the chosen integer range and must stay invalid. Delete the Service column; the script must fail with exit 2 rather than create a misleading report.
Run with an output file that already exists and compare its bytes afterward. It must remain unchanged. Run with a missing input file and confirm no report was created. Finally, pass the same file for both paths and verify the source remains unchanged. These are meaningful protections, not cosmetic tests of whether a button can be clicked.
The error stream is separate from the normal object output, but a caller can merge streams deliberately. A scheduler should inspect the exit code and capture diagnostics separately. It should not treat any text on standard output as proof that all rows succeeded.
Worked laboratory solution
Compare your reasoning with the worked solution
A is 9/100 × 100 = 9%, so Alert. B is exactly 10%, so OK under the strict rule. C cannot be divided by zero and must be Invalid. D is missing evidence and must also be Invalid, not 0% or healthy. The function should preserve that distinction. Use structured objects for later CSV export; applying Format-Table before Export-Csv exports formatting objects rather than the intended report data. Test your specific PowerShell version and culture-sensitive numeric input in a local lab.
YOUR NEXT STEP
Keep building your skills
No directly related CogniSkill book is mapped to this course yet. Continue with the free learning, or compare the publishing catalogue by your next goal.
Explore books by topic and goal →Sign in to save lesson completion. Reading position is separate from completion.
Practice notes — your working space
Save predictions, outputs and explanations as you work. Notes stay on this browser, not your account; people sharing this browser can access them. Avoid passwords and sensitive data. Export to keep a portable copy, or clear your notes before leaving a shared device.
How was this lesson?
Help us improve this lesson.