Manifest.psm1

function Update-Manifest {
    #.Synopsis
    # Update a PowerShell module manifest
    #.Description
    # By default Update-Manifest increments the ModuleVersion, but it can set any key in the Module Manifest, its PrivateData, or the PSData in PrivateData.
    #
    # NOTE: This cannot currently create new keys, or uncomment keys.
    #.Example
    # Update-Manifest .\Configuration.psd1
    #
    # Increments the Build part of the ModuleVersion in the Configuration.psd1 file
    #.Example
    # Update-Manifest .\Configuration.psd1 -Increment Major
    #
    # Increments the Major version part of the ModuleVersion in the Configuration.psd1 file
    #.Example
    # Update-Manifest .\Configuration.psd1 -Value '0.4'
    #
    # Sets the ModuleVersion in the Configuration.psd1 file to 0.4
    #.Example
    # Update-Manifest .\Configuration.psd1 -Property ReleaseNotes -Value 'Add the awesome Update-Manifest function!'
    #
    # Sets the PrivateData.PSData.ReleaseNotes value in the Configuration.psd1 file!
    [CmdletBinding()]
    param(
        # The path to the module manifest file
        [Parameter(ValueFromPipelineByPropertyName="True", Position=0)]
        [Alias("PSPath")]
        [string]$Manifest,

        # The property to be set in the manifest. It must already exist in the file (and not be commented out)
        # This searches the Manifest root properties, then the properties PrivateData, then the PSData
        [Parameter(ParameterSetName="Overwrite")]
        [string]$PropertyName = 'ModuleVersion',

        # A new value for the property
        [Parameter(ParameterSetName="Overwrite", Mandatory)]
        $Value,

        # By default Update-Manifest increments ModuleVersion; this controls which part of the version number is incremented
        [Parameter(ParameterSetName="Increment")]
        [ValidateSet("Major","Minor","Build","Revision")]
        [string]$Increment = "Build",

        # When set, and incrementing the ModuleVersion, output the new version number.
        [Parameter(ParameterSetName="Increment")]
        [switch]$Passthru
    )

    $KeyValue = Get-ManifestValue $Manifest -PropertyName $PropertyName -Passthru

    if($PSCmdlet.ParameterSetName -eq "Increment") {
        $Version = [Version]$KeyValue.SafeGetValue()

        $Version = switch($Increment) {
            "Major" {
                [Version]::new($Version.Major + 1, 0)
            }
            "Minor" {
                $Minor = if($Version.Minor -le 0) { 1 } else { $Version.Minor + 1 }
                [Version]::new($Version.Major, $Minor)
            }
            "Build" {
                $Build = if($Version.Build -le 0) { 1 } else { $Version.Build + 1 }
                [Version]::new($Version.Major, $Version.Minor, $Build)
            }
            "Revision" {
                $Build = if($Version.Build -le 0) { 0 } else { $Version.Build }
                $Revision = if($Version.Revision -le 0) { 1 } else { $Version.Revision + 1 }
                [Version]::new($Version.Major, $Version.Minor, $Build, $Revision)
            }
        }

        $Value = $Version

        if($Passthru) { $Value }
    }

    $Value = ConvertTo-Metadata $Value

    $Extent = $KeyValue.Extent
    while($KeyValue.parent) { $KeyValue = $KeyValue.parent }

    $ManifestContent = $KeyValue.Extent.Text.Remove(
                                               $Extent.StartOffset, 
                                               ($Extent.EndOffset - $Extent.StartOffset)
                                           ).Insert($Extent.StartOffset, $Value)

    if(Test-Path $Manifest) {
        Set-Content $Manifest $ManifestContent
    } else {
        $ManifestContent
    }
}


function Get-ManifestValue {
    #.Synopsis
    # Reads a specific value from a module manifest
    #.Description
    # By default Get-ManifestValue gets the ModuleVersion, but it can read any key in the Module Manifest, including the PrivateData, or the PSData inside the PrivateData.
    #.Example
    # Get-ManifestValue .\Configuration.psd1
    #
    # Returns the module version number (as a string)
    #.Example
    # Get-ManifestValue .\Configuration.psd1 ReleaseNotes
    #
    # Returns the release notes!
    [CmdletBinding()]
    param(
        # The path to the module manifest file
        [Parameter(ValueFromPipelineByPropertyName="True", Position=0)]
        [Alias("PSPath")]
        [string]$Manifest,

        # The property to be read from the manifest. Get-ManifestValue searches the Manifest root properties, then the properties PrivateData, then the PSData
        [Parameter(ParameterSetName="Overwrite", Position=1)]
        [string]$PropertyName = 'ModuleVersion',

        [switch]$Passthru
    )
    $ErrorActionPreference = "Stop"

    if(Test-Path $Manifest) {
        $ManifestContent = Get-Content $Manifest -Raw
    } else { 
        $ManifestContent = $Manifest
    }

    $Tokens = $Null; $ParseErrors = $Null
    $AST = [System.Management.Automation.Language.Parser]::ParseInput( $ManifestContent, $Manifest, [ref]$Tokens, [ref]$ParseErrors )
    $ManifestHash = $AST.Find( {$args[0] -is [System.Management.Automation.Language.HashtableAst]}, $true )
    $KeyValue = $ManifestHash.KeyValuePairs.Where{ $_.Item1.Value -eq $PropertyName }.Item2

    # Recursively search for PropertyName in the PrivateData and PrivateData.PSData
    if(!$KeyValue) {
        $global:PrivateData = $ManifestHash.KeyValuePairs.Where{ $_.Item1.Value -eq 'PrivateData' }.Item2.PipelineElements.Expression
        $KeyValue = $PrivateData.KeyValuePairs.Where{ $_.Item1.Value -eq $PropertyName }.Item2
        if(!$KeyValue) {
            $global:PSData = $PrivateData.KeyValuePairs.Where{ $_.Item1.Value -eq 'PSData' }.Item2.PipelineElements.Expression
            $KeyValue = $PSData.KeyValuePairs.Where{ $(Write-Verbose "'$($_.Item1.Value)' -eq '$PropertyName'"); $_.Item1.Value -eq $PropertyName }.Item2
            if(!$KeyValue) {
                Write-Error "Couldn't find '$PropertyName' in that manifest!"
                return
            }
        }
    }

    if($Passthru) { $KeyValue } else { 
        Write-Verbose "Start $($KeyValue.Extent.StartLineNumber) : $($KeyValue.Extent.StartColumnNumber) (char $($KeyValue.Extent.StartOffset))"
        Write-Verbose "End $($KeyValue.Extent.EndLineNumber) : $($KeyValue.Extent.EndColumnNumber) (char $($KeyValue.Extent.EndOffset))"
        $KeyValue.SafeGetValue()
    }
}