This chapter covers generating PowerShell cmdlets from Prolog predicates. You’ll learn how to create advanced functions with proper [CmdletBinding()] attributes, parameter validation, pipeline support, verbose output, and error handling.
In Chapter 2, we generated basic PowerShell functions. Now we’ll generate advanced functions (also called “script cmdlets”) that behave like native PowerShell cmdlets.
A PowerShell cmdlet has:
-Verbose, -Debug, etc.)Write-Error, $ErrorActionPreferenceBasic function (Chapter 2):
function parent {
param([string]$X, [string]$Y)
# ... implementation
}
Advanced function (cmdlet):
function Get-Parent {
[CmdletBinding()]
param(
[Parameter(Position=0)]
[ValidateNotNullOrEmpty()]
[string]$Parent,
[Parameter(Position=1, ValueFromPipeline=$true)]
[string]$Child
)
begin {
Write-Verbose "Loading parent facts..."
$facts = # ... load facts
}
process {
Write-Verbose "Processing: Parent=$Parent, Child=$Child"
# ... implementation
}
end {
Write-Verbose "Query complete"
}
}
PowerShell uses Verb-Noun naming. The compiler maps predicates:
| Prolog Predicate | PowerShell Cmdlet |
|---|---|
parent/2 |
Get-Parent |
ancestor/2 |
Get-Ancestor |
add_user/3 |
Add-User |
remove_item/1 |
Remove-Item |
?- compile_to_powershell(parent/2, [
cmdlet_name('Get-Parent'), % Override name
cmdlet_binding(true), % Add [CmdletBinding()]
verbose_output(true), % Add Write-Verbose calls
parameter_sets(['ByParent', 'ByChild'])
], Code).
% Prolog: query(search_term, max_results)
query(Term, Max) :- ...
Generates:
function Search-Query {
[CmdletBinding()]
param(
[Parameter(Position=0, Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]$SearchTerm,
[Parameter(Position=1)]
[ValidateRange(1, 1000)]
[int]$MaxResults = 100
)
# Implementation
}
For predicates that accept input from a previous command:
function Get-Ancestor {
[CmdletBinding()]
param(
[Parameter(Position=0)]
[string]$Ancestor,
[Parameter(Position=1, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[Alias('Name', 'Person')]
[string]$Descendant
)
process {
# Called once per pipeline item
if ($Descendant) {
# Query for this specific descendant
}
}
}
Usage:
# Direct call
Get-Ancestor -Ancestor 'abraham' -Descendant 'jacob'
# Pipeline input
'jacob', 'esau' | Get-Ancestor -Ancestor 'abraham'
# From another command
Get-Person | Get-Ancestor -Ancestor 'abraham'
The compiler can generate validation attributes based on Prolog constraints:
% Prolog with constraints
age(Person, Age) :-
person(Person),
Age > 0,
Age < 150.
Generated:
function Get-Age {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[ValidateScript({ Test-Person $_ })]
[string]$Person,
[ValidateRange(1, 149)]
[int]$Age
)
# Implementation
}
| Attribute | Purpose |
|---|---|
[ValidateNotNullOrEmpty()] |
Reject null/empty strings |
[ValidateRange(min, max)] |
Numeric bounds |
[ValidateSet('a','b','c')] |
Allowed values |
[ValidatePattern('^[A-Z]')] |
Regex match |
[ValidateScript({...})] |
Custom validation |
[ValidateLength(min, max)] |
String length |
Advanced functions support three execution phases:
Runs once before processing any pipeline input:
begin {
Write-Verbose "Initializing..."
# Load facts once (expensive operation)
$script:facts = Load-Facts
# Build indexes
$script:index = @{}
foreach ($f in $script:facts) {
$script:index[$f.Key] = $f
}
}
Runs once per pipeline item:
process {
Write-Verbose "Processing: $_"
# Use prebuilt index for fast lookup
if ($script:index.ContainsKey($_)) {
$script:index[$_]
}
}
Runs after all pipeline input is processed:
end {
Write-Verbose "Cleanup..."
# Emit aggregated results
# Clean up resources
}
% Prolog
ancestor(X, Y) :- parent(X, Y).
ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
function Get-Ancestor {
[CmdletBinding()]
param(
[Parameter(Position=0)]
[string]$Ancestor,
[Parameter(Position=1, ValueFromPipeline=$true)]
[string]$Descendant
)
begin {
Write-Verbose "[Get-Ancestor] Loading parent facts..."
$parentFacts = Get-Parent
Write-Verbose "[Get-Ancestor] Building ancestor closure..."
$ancestors = @{}
# Base case: direct parents
foreach ($p in $parentFacts) {
$key = "$($p.X):$($p.Y)"
$ancestors[$key] = [PSCustomObject]@{
Ancestor = $p.X
Descendant = $p.Y
}
}
# Fixpoint: transitive closure
do {
$newCount = 0
foreach ($a in @($ancestors.Values)) {
foreach ($p in $parentFacts) {
if ($a.Descendant -eq $p.X) {
$key = "$($a.Ancestor):$($p.Y)"
if (-not $ancestors.ContainsKey($key)) {
$ancestors[$key] = [PSCustomObject]@{
Ancestor = $a.Ancestor
Descendant = $p.Y
}
$newCount++
}
}
}
}
Write-Verbose "[Get-Ancestor] Added $newCount new relationships"
} while ($newCount -gt 0)
Write-Verbose "[Get-Ancestor] Total: $($ancestors.Count) ancestor relationships"
}
process {
if ($Ancestor -and $Descendant) {
$key = "$Ancestor`:$Descendant"
if ($ancestors.ContainsKey($key)) {
$ancestors[$key]
}
} elseif ($Ancestor) {
$ancestors.Values | Where-Object { $_.Ancestor -eq $Ancestor }
} elseif ($Descendant) {
$ancestors.Values | Where-Object { $_.Descendant -eq $Descendant }
} else {
$ancestors.Values
}
}
end {
Write-Verbose "[Get-Ancestor] Query complete"
}
}
Add diagnostic messages visible with -Verbose:
function Get-Parent {
[CmdletBinding()]
param([string]$Name)
Write-Verbose "Querying parent for: $Name"
# ...
Write-Verbose "Found $count results"
}
Get-Parent -Name 'bob' -Verbose
Output:
VERBOSE: Querying parent for: bob
VERBOSE: Found 2 results
charles
diana
For developer-level diagnostics:
function Get-Parent {
[CmdletBinding()]
param([string]$Name)
Write-Debug "Index lookup key: parent:$Name"
Write-Debug "Hash: $($Name.GetHashCode())"
# ...
}
$DebugPreference = 'Continue'
Get-Parent -Name 'bob' -Debug
?- compile_to_powershell(parent/2, [
verbose_output(true), % Add Write-Verbose
debug_output(true) % Add Write-Debug
], Code).
Write-Error - Non-terminating error (continues execution):
function Get-Parent {
[CmdletBinding()]
param([string]$Name)
if (-not (Test-PersonExists $Name)) {
Write-Error "Person not found: $Name"
return
}
# ... continue
}
Throw - Terminating error (stops execution):
function Get-Parent {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$Name
)
if (-not (Test-PersonExists $Name)) {
throw "Person not found: $Name"
}
# ... never reached if person doesn't exist
}
[CmdletBinding()] automatically enables -ErrorAction:
# Stop on first error
Get-Parent -Name 'unknown' -ErrorAction Stop
# Silently continue
Get-Parent -Name 'unknown' -ErrorAction SilentlyContinue
# Ask what to do
Get-Parent -Name 'unknown' -ErrorAction Inquire
function Get-Parent {
[CmdletBinding()]
param([string]$Name)
try {
$result = Invoke-ExpensiveOperation -Name $Name
$result
}
catch [System.IO.FileNotFoundException] {
Write-Error "Data file not found: $_"
}
catch {
Write-Error "Unexpected error: $_"
}
}
Add type names for custom formatting:
function Get-Parent {
[CmdletBinding()]
[OutputType([UnifyWeaver.ParentRelation])]
param([string]$Name)
process {
$result = [PSCustomObject]@{
Parent = $p.X
Child = $p.Y
}
$result.PSObject.TypeNames.Insert(0, 'UnifyWeaver.ParentRelation')
$result
}
}
Create UnifyWeaver.format.ps1xml:
<?xml version="1.0" encoding="utf-8"?>
<Configuration>
<ViewDefinitions>
<View>
<Name>ParentRelation</Name>
<ViewSelectedBy>
<TypeName>UnifyWeaver.ParentRelation</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader><Label>Parent</Label></TableColumnHeader>
<TableColumnHeader><Label>Child</Label></TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem><PropertyName>Parent</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>Child</PropertyName></TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
Load with:
Update-FormatData -AppendPath UnifyWeaver.format.ps1xml
Let’s generate a complete cmdlet module from our family tree predicates.
% parent facts
parent(abraham, ishmael).
parent(abraham, isaac).
parent(sarah, isaac).
parent(isaac, esau).
parent(isaac, jacob).
% grandparent rule
grandparent(GP, GC) :-
parent(GP, P),
parent(P, GC).
% ancestor rule
ancestor(A, D) :- parent(A, D).
ancestor(A, D) :- parent(A, P), ancestor(P, D).
?- compile_to_powershell(parent/2, [
cmdlet_name('Get-FamilyParent'),
cmdlet_binding(true),
verbose_output(true),
output_file('FamilyTree/Get-FamilyParent.ps1')
], _).
?- compile_to_powershell(grandparent/2, [
cmdlet_name('Get-FamilyGrandparent'),
cmdlet_binding(true),
verbose_output(true),
output_file('FamilyTree/Get-FamilyGrandparent.ps1')
], _).
?- compile_to_powershell(ancestor/2, [
cmdlet_name('Get-FamilyAncestor'),
cmdlet_binding(true),
verbose_output(true),
output_file('FamilyTree/Get-FamilyAncestor.ps1')
], _).
# FamilyTree/FamilyTree.psd1
@{
ModuleVersion = '1.0.0'
GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
Author = 'UnifyWeaver'
Description = 'Family tree queries generated from Prolog'
FunctionsToExport = @(
'Get-FamilyParent',
'Get-FamilyGrandparent',
'Get-FamilyAncestor'
)
NestedModules = @(
'Get-FamilyParent.ps1',
'Get-FamilyGrandparent.ps1',
'Get-FamilyAncestor.ps1'
)
}
Import-Module ./FamilyTree
# Who are abraham's descendants?
Get-FamilyAncestor -Ancestor abraham -Verbose
# Pipeline: check if these people are descendants of isaac
'jacob', 'esau', 'ishmael' | Get-FamilyAncestor -Ancestor isaac
# Find all grandparents
Get-FamilyGrandparent | Format-Table
In Chapter 4, you’ll learn about .NET integration:
Add-Typedotnet_source plugin[CmdletBinding(
SupportsShouldProcess=$true, # Enable -WhatIf, -Confirm
DefaultParameterSetName='Set1', # Default parameter set
PositionalBinding=$false # Require named parameters
)]
| Parameter | Purpose |
|---|---|
-Verbose |
Show verbose messages |
-Debug |
Show debug messages |
-ErrorAction |
Control error behavior |
-WarningAction |
Control warning behavior |
-InformationAction |
Control info messages |
-ErrorVariable |
Store errors in variable |
-OutVariable |
Store output in variable |
[Parameter(
Mandatory=$true, # Required
Position=0, # Positional index
ValueFromPipeline=$true, # Accept pipeline input
ValueFromPipelineByPropertyName=$true, # By property name
ParameterSetName='Set1', # Belongs to set
HelpMessage='Enter a name' # Help text
)]
[Alias('N', 'PersonName')] # Alternative names
[ValidateNotNullOrEmpty()] # Validation
[string]$Name
| Option | Values | Description |
|---|---|---|
cmdlet_name(Name) |
String | Override function name |
cmdlet_binding(Bool) |
true/false | Add CmdletBinding |
verbose_output(Bool) |
true/false | Add Write-Verbose |
debug_output(Bool) |
true/false | Add Write-Debug |
output_type(Type) |
Type name | Add OutputType attribute |
| ← Previous: Chapter 2: Facts and Rules | 📖 Book 12: PowerShell Target | Next: Chapter 4: .NET Integration → |