F# pattern matching for cleaner systems administration scripts
Administration scripts often begin as a small success check and end as a thicket of exit codes, temporary files and conditional branches. F# pattern matching provides a clearer shape: describe the outcomes a tool can produce, then handle each one explicitly.
F# is practical on Windows, Linux and macOS because it runs on .NET and can orchestrate PowerShell, SSH and command-line tools. Its type-safe discriminated unions and exhaustive matching are especially useful when a check may pass, warn, fail or return no usable data.
Consider a script that verifies a backup, web service or storage pool. Instead of flattening every result to a Boolean, a union such as Healthy, Warning or Failed preserves the distinction. The compiler identifies missing branches, while unit tests turn operational states into readable examples.
Clean logic matters for Australian teams working in Sydney, Melbourne and Brisbane, who must also account for time zones and daylight-saving changes that complicate overnight schedules. Local cloud regions, Australian support arrangements and privacy obligations also shape what a trustworthy script should do. The goal is maintainable automation, not academic elegance.
Model operational outcomes as explicit states
A useful first step is to separate command execution from interpretation. Model what the check means rather than passing raw exit codes through every layer.
type ServiceState =
| Running
| Stopped
| Degraded of reason: string
| Unknown
let describe state =
match state with
| Running -> "healthy"
| Stopped -> "not running"
| Degraded reason -> $"degraded: {reason}"
| Unknown -> "no reliable answer"
The benefit is not shorter code alone. A new state forces a decision about behaviour, and an unhandled state becomes visible during compilation. Keep Unknown separate from Failed; treating missing telemetry as a confirmed outage can trigger unnecessary restarts.
Match command results without defensive clutter
Wrapping tools in a small function lets the rest of the script match a standard result. A command can return Ok output or Error details, with the wrapper responsible for timeouts, stderr and exit-code translation. This keeps vendor-specific quirks at the boundary.
That explicit contract improves handovers as much as maintainability. The emphasis on clear expectations in Elizabeth Mitchell’s interview insights fits the same operational need: automation should report what happened, not merely that it finished. A named failure is more useful than an ambiguous empty string.
Use Option when absence is legitimate, such as no matching hosts, but keep genuine execution failures as Error. Pattern matching then forces callers to distinguish those cases. For cloud APIs, active patterns can extract a status field while ignoring irrelevant JSON properties.
Turn deployments into testable decisions
Deployment checks often parse JSON, CSV or command output before deciding what to do. Parse that data into a union first, then match on it. A rollout might have Ready, RollingBack, NeedsApproval and Completed states, each with different consequences.
This arrangement makes tests straightforward. A table or list of named cases can assert that Ready starts the next region, NeedsApproval pauses and RollingBack raises an alert. Unusual inputs belong in the model too, rather than appearing as mysterious fallback branches.
Keep irreversible actions behind small functions with clear names. Isolate stopping a service from evaluating its health. It becomes obvious during code review that a match cannot accidentally reboot a host. Error handling also stays close to the operation that failed.
Connect software checks to real hardware
Storage monitoring is where clean abstractions meet messy devices. SMART attributes, controller tools and vendor-specific output can expose the same disk problem in different formats. Before writing the parser, compare hardware vendor comparisons to see which tools and capabilities apply to your actual equipment.
Normalise the relevant readings into a small union, then match on them. A disk can be Healthy, Failing, Unreadable or Unchecked. The last state matters: a failed query is not evidence that the disk has failed and should not be folded into either answer.
For an Australian home lab built around a spare mini PC or retired business server, this separation is especially valuable. Consumer disks and second-hand controllers may report differently. Local resellers and managed service providers can also influence the choice through warranty support, spare-part availability and repair turnaround.
Weigh RAID choices for Australian workloads
RAID decisions need the same care. The trade-offs summarised in hardware versus software RAID show why there is no universal winner. A hardware controller can provide dedicated processing, while software RAID or ZFS offers flexibility and useful diagnostics. Existing skills often matter more than a small benchmark difference.
For Australian organisations, data location and recovery time deserve equal attention. A copy held in an overseas cloud region may satisfy some backup goals but still be awkward to reach during a local outage. Local-region infrastructure can improve latency, although residency alone does not prove recoverability.
Logs can contain personal information, so storing every diagnostic dump indefinitely is not a neutral choice. The Privacy Act 1988 and the Australian Privacy Principles require governance of covered information. Restrict access, define retention and avoid putting customer identifiers into routine alert messages.
Put the patterns into a reliable workflow
These practices fit into a workflow that works beyond a single script:
- Model operational outcomes as named states instead of raw exit codes.
- Keep command execution separate from interpretation and decision-making.
- Handle missing data explicitly; never let Unknown become success by default.
- Test normal, warning, failed and incomplete scenarios as separate cases.
- Log structured context and correlation identifiers without exposing secrets.
- Make retries idempotent and verify timeouts around daylight-saving changes.
These checks are easier to review when they live beside the scripts they protect. Start with read-only reporting, compare its output with the current manual process, and add actions only after the states and ownership are clear.
The next step is concrete: choose one read-only service check, convert its outcomes into a discriminated union, and add a test for each branch. Run it in report-only mode through an Australian business day, then automate a response once the logs and owners are agreed. That small cycle turns a brittle shell script into dependable operational tooling.
Karl Katzke