public/Properties.ps1

function Properties {
    <#
    .SYNOPSIS
    Define a scriptblock that contains assignments to variables that will be
    available within all tasks in the build script
 
    .DESCRIPTION
    A build script may declare a "Properties" function which allows you to
    define variables that will be available within all the "Task" functions in
    the build script.
 
    .PARAMETER Properties
    The script block containing all the variable assignment statements
 
    .PARAMETER Hashtable
    An alternative to the scriptblock parameter, you can provide a hashtable of
    key-value pairs that will be converted into variables. This is useful when
    you want to define properties programmatically or from an external source.
 
    .EXAMPLE
    Properties {
        $build_dir = "c:\build"
        $connection_string = "datasource=localhost;initial catalog=northwind;integrated security=sspi"
    }
    Task default -depends Test
    Task Test -depends Compile, Clean {
    }
    Task Compile -depends Clean {
    }
    Task Clean {
    }
 
    Note: You can have more than one "Properties" function defined in the build script.
    .EXAMPLE
    Properties {
        $script:build_dir = "c:\build"
        $script:connection_string = "datasource=localhost;initial catalog=northwind;integrated security=sspi"
    }
    Task Compile {
        "Building to: $build_dir" # No PSScriptAnalyzer warning, variable is recognized
    }
 
    Recommended: Use script-scoped variables to avoid PSScriptAnalyzer warnings
    The $script: prefix has identical runtime behavior but satisfies
    PSScriptAnalyzer's static analysis requirements.
    .EXAMPLE
    Properties {
        $build_dir = "c:\build" # Warning: PSUseDeclaredVarsMoreThanAssignments
    }
    Task Compile {
        "Building to: $build_dir" # Works at runtime, but PSScriptAnalyzer warns
    }
 
    Alternative: Non-scoped variables (generates PSScriptAnalyzer warnings)
 
    Variables still work correctly at runtime, but PSScriptAnalyzer cannot detect
    that they will be used in tasks.
    .NOTES
    This works by defining a script block that is pushed onto the
    $psake.Context.Peek().properties stack. This allows the properties to be
    accessed within all tasks in the build script.
    This means that the variables defined in the script block will be
    available in the scope of the tasks, but not in the global scope of the
    build script.
 
    PSScriptAnalyzer may warn about variables assigned but not used
    (PSUseDeclaredVarsMoreThanAssignments) when variables are declared in
    Properties blocks. This is a false positive - the variables ARE used
    in tasks when the Properties scriptblock is dot-sourced at runtime.
 
    To suppress this warning, use script-scoped variables:
 
    Properties {
        $script:build_dir = "c:\build"
        $script:connection_string = "datasource=..."
    }
 
    This has identical runtime behavior but satisfies PSScriptAnalyzer's
    static analysis requirements. See the examples above for more details.
    #>

    [CmdletBinding(DefaultParameterSetName = 'ScriptBlock')]
    param(
        [Parameter(
            Mandatory = $true,
            Position = 0,
            ParameterSetName = 'ScriptBlock')]
        [scriptblock]$Properties,

        [Parameter(
            Mandatory = $true,
            Position = 0,
            ParameterSetName = 'Hashtable')]
        [hashtable]$Hashtable
    )

    Write-Debug "Registering Properties block (ParameterSet='$($PSCmdlet.ParameterSetName)')"
    if ($PSCmdlet.ParameterSetName -eq 'Hashtable') {
        # Validate that all keys are legal PowerShell variable names before storing.
        foreach ($key in $Hashtable.Keys) {
            if ($key -notmatch '^[A-Za-z_][A-Za-z0-9_]*$') {
                throw "Properties hashtable key '$key' is not a valid variable name."
            }
        }
        # Store the hashtable in $psake so the deferred scriptblock can access it.
        # (closures via GetNewClosure break dot-sourcing into caller's scope)
        # Use Set-Variable at execution time instead of string interpolation to
        # avoid any code-injection risk from key or value content.
        $storageKey = "_propHash_$(Get-Random)"
        $psake[$storageKey] = $Hashtable.Clone()
        $Properties = [scriptblock]::Create("
            foreach (`$_key in `$psake['$storageKey'].Keys) {
                Set-Variable -Name `$_key -Value `$psake['$storageKey'][`$_key]
            }
        "
)
    }

    $psake.Context.Peek().properties.Push($Properties)
}