LeetABit.Build.Help.psm1

#requires -version 6
using namespace System.Collections

Set-StrictMode -Version 3.0
Import-LocalizedData -BindingVariable LocalizedData -FileName LeetABit.Build.Help.Resources.psd1

$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
    if (Get-Module 'LeetABit.Build.Extensibility') {
        LeetABit.Build.Extensibility\Unregister-BuildExtension "LeetABit.Build.Help" -ErrorAction SilentlyContinue
    }
}

$Regex_ScriptBlockSyntax_FunctionName = '(?<={0})(.+?)(?=\[(-WhatIf|-Confirm|\<CommonParameters\>))'


##################################################################################################################
# Target Handlers
##################################################################################################################

Register-BuildTask "help" -Jobs {
    <#
    .SYNOPSIS
        Gets help for the build script or one of its targets.
    #>

    [CmdletBinding(PositionalBinding = $False)]

    param (
        # Optional name of the build extension for which help shall be obtained.
        [Parameter(Position = 0,
                   Mandatory = $False,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $True)]
        [String]
        $ExtensionTopic,

        # Optional name of the build task for which help shall be obtained.
        [Parameter(Position = 1,
                   Mandatory = $False,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $True)]
        [String]
        $TaskTopic
    )

    begin {
        LeetABit.Build.Common\Import-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }

    process {
        Get-BuildHelp $ExtensionTopic $TaskTopic | Out-String | Write-Information -InformationAction Continue
    }
}


##################################################################################################################
# Public Commands
##################################################################################################################


function Get-BuildHelp {
    <#
    .SYNOPSIS
        Gets help about build scripts usage.
    .DESCRIPTION
        Get-BuildHelp cmdlet provides a concise documentation about each of the loaded extensions and build tasks.
    .EXAMPLE
        PC> Get-BuildHelp
 
        Gets help about all registered build extensions and tasks.
    .EXAMPLE
        PC> Get-BuildHelp -ExtensionTopic "PowerShell"
 
        Gets a detailed help about all tasks provided by "PowerShell" extension.
    .EXAMPLE
        PC> Get-BuildHelp -TaskTopic "build"
 
        Gets a detailed help about all build commands provided by different extensions.
    .EXAMPLE
        PC> Get-BuildHelp -ExtensionTopic "PowerShell" -TaskTopic "build"
 
        Gets a detailed help about "build" task provided by "PowerShell" extension.
    #>

    [CmdletBinding(PositionalBinding = $False)]

    param (
        # Optional name of the build extension for which help shall be obtained.
        [Parameter(Position = 0,
                   Mandatory = $False,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $True)]
        [String]
        $ExtensionTopic,

        # Optional name of the build task for which help shall be obtained.
        [Parameter(Position = 1,
                   Mandatory = $False,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $True)]
        [String]
        $TaskTopic
    )

    begin {
        LeetABit.Build.Common\Import-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $scriptName = Join-Path '.' 'run.ps1'
    }

    process {
        $typeNameSuffix = if ($ExtensionTopic) {
            if ($TaskTopic) { 'DetailedView' } else { 'ExtensionView' }
        }
        else {
            if ($TaskTopic) { 'TaskView' } else { 'GeneralView' }
        }

        $helpInfo = @{}
        $helpInfo.ScriptName = $scriptName
        $helpInfo.Synopsis = $LocalizedData.Get_BuildHelp_Buildstrapper_Synopsis
        $helpInfo.TaskTopic = $TaskTopic
        $helpInfo.ExtensionTopic = $ExtensionTopic
        $helpInfo.Extensions = @()

        foreach ($currentExtension in LeetABit.Build.Extensibility\Get-BuildExtension) {
            if ($ExtensionTopic -and $ExtensionTopic -ne $currentExtension.Name) {
                continue
            }

            $extension = @{}
            $extension.Name = $currentExtension.Name
            $extension.Description = ''

            if ($currentExtension.Resolver.Module -and $currentExtension.Resolver.Module.Name -ne 'LeetABit.Build.Extensibility') {
                $extension.Description = $currentExtension.Resolver.Module.Description
            }

            $extension.Tasks = @{}

            foreach ($currentTask in $currentExtension.Tasks.Values) {
                if ($TaskTopic -and $TaskTopic -ne $currentTask.Name) {
                    continue
                }

                $task = @{}
                $task.Name = $currentTask.Name
                $task.IsDefault = $currentTask.IsDefault
                $task.Jobs = @()
                $task.Description = @()
                $task.Parameters = @()

                $currentTask.Jobs | ForEach-Object {
                    if ($_ -is [String]) {
                        $task.Jobs += $_
                        $task.Description += $extension.Tasks[$_].Description
                        $task.Parameters += $extension.Tasks[$_].Parameters
                    }
                    else {
                        if (-not $extension.Description) {
                            if ($_.Module) {
                                $extension.Description = $_.Module.Description
                            }
                        }

                        $jobScriptBlock = [String]$_
                        $function:private:ScriptBlockCommand = $jobScriptBlock
                        $helpObject = Get-Help 'ScriptBlockCommand'
                        $helpString = (Get-Help 'ScriptBlockCommand' -Full | Out-String)

                        $job = @{}
                        $job.Description = $helpObject.Synopsis
                        $task.Description += $helpObject.Synopsis

                        $nextSection = if ($helpObject.PSObject.TypeNames -contains 'ExtendedCmdletHelpInfo') {
                            'PARAMETERS'
                        }
                        else {
                            'DESCRIPTION'
                        }

                        $startIndex = $helpString.IndexOf("SYNTAX") + "SYNTAX".Length
                        $endIndex   = $helpString.IndexOf($nextSection, $startIndex)
                        $syntaxText = $helpString.Substring($startIndex, $endIndex - $startIndex).Trim()

                        $syntaxString = ($syntaxText -split [Environment]::NewLine) -join ''
                        $syntax = "$scriptName $($task.Name)"
                        $regex = $Regex_ScriptBlockSyntax_FunctionName -f 'ScriptBlockCommand'

                        if ($syntaxString -match $regex) {
                            $syntax += "$($matches[1])"
                        }

                        $job.Syntax = $syntax.Trim()
                        $job.Parameters = @()

                        if ($helpObject.parameters.PSObject.Properties.Name -contains 'parameter') {
                            foreach ($parameterObject in $helpObject.parameters.parameter) {
                                $parameter = @{}
                                $parameter.Name = $parameterObject.Name
                                if ($parameterObject.PSObject.Properties.Name -contains "type") {
                                    $parameter.Type = $parameterObject.type.name
                                }

                                if ($parameterObject.PSObject.Properties.Name -contains "description") {
                                    if ($parameterObject.description -is [String]) {
                                        $parameter.Description = $parameterObject.description
                                    }
                                    else {
                                        $parameter.Description = $parameterObject.description.Text
                                    }
                                }
                                else {
                                    $parameter.Description = ""
                                }

                                $parameter.Mandatory = [System.Convert]::ToBoolean($parameterObject.required)
                                $parameterObject = Convert-DictionaryToHelpObject -Properties $parameter -HelpObjectName 'Parameter' -HelpView $typeNameSuffix
                                $task.Parameters += $parameterObject
                                $job.Parameters += $parameterObject
                            }
                        }

                        $task.Jobs += Convert-DictionaryToHelpObject -Properties $job -HelpObjectName 'Job' -HelpView $typeNameSuffix
                    }
                }

                $parametersDictionary = @{}

                if ($task.Parameters) {
                    $task.Parameters | ForEach-Object {
                        if ($parametersDictionary.ContainsKey($_.Name)) {
                            $alreadyStored = $parametersDictionary[$_.Name]
                            $parametersDictionary.Remove($_.Name)

                            if ($alreadyStored.Type -ne $_.Type) {
                                $alreadyStored.Type = "String"
                            }

                            if ($alreadyStored.Description -notcontains $_.Description) {
                                $alreadyStored.Description += $_.Description
                            }

                            $alreadyStored.Mandatory = $alreadyStored.Mandatory -or $_.Mandatory
                            $parametersDictionary.Add($alreadyStored.Name, $alreadyStored)
                        }
                        else {
                            $_.Description = @($_.Description)
                            $parametersDictionary.Add($_.Name, $_)
                        }
                    }
                }

                $task.Description = (($task.Description | Get-Unique) -join " ").Trim()
                $task.Parameters = $parametersDictionary.Values | Sort-Object -Property Name
                $extension.Tasks.Add($task.Name, (Convert-DictionaryToHelpObject -Properties $task -HelpObjectName 'Task' -HelpView $typeNameSuffix))
            }

            if (-not $TaskTopic -or $extension.Tasks) {
                $extension.Tasks = $extension.Tasks.Values;
                $helpInfo.Extensions += Convert-DictionaryToHelpObject -Properties $extension -HelpObjectName 'Extension' -HelpView $typeNameSuffix
            }
        }

        Convert-DictionaryToHelpObject -Properties $helpInfo -HelpObjectName 'HelpInfo' -HelpView $typeNameSuffix
    }
}


##################################################################################################################
# Private Commands
##################################################################################################################



function Convert-DictionaryToHelpObject {
    <#
    .SYNOPSIS
        Converts a hashtable to a PSObject using keys as property names with associated values.
    #>

    [CmdletBinding(PositionalBinding = $False)]
    [OutputType([String])]

    param (
        # A hashtable with desired object's properties.
        [Parameter(Position = 0,
                   Mandatory = $True,
                   ValueFromPipeline = $True,
                   ValueFromPipelineByPropertyName = $True)]
        [IDictionary]
        $Properties,

        # A name of the help object's type that shall be assigned to the object.
        [Parameter(Position = 1,
                   Mandatory = $True,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $True)]
        [String]
        $HelpObjectName,

        # A name of the help object's type suffix that shall be assigned to the object as a secondary type.
        [Parameter(Position = 2,
                   Mandatory = $True,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $True)]
        [String]
        $HelpView
    )

    begin {
        $typeNameNamespace = 'LeetABit.Build.'
    }

    process {
        LeetABit.Build.Common\New-PSObject (($typeNameNamespace + $HelpObjectName + ".$HelpView"), ($typeNameNamespace + $HelpObjectName)) $Properties
    }
}


Export-ModuleMember -Function '*' -Variable '*' -Alias '*' -Cmdlet '*'

# SIG # Begin signature block
# MIIM3wYJKoZIhvcNAQcCoIIM0DCCDMwCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBhjxPxaYFM0GdK
# +VJfWwEOtYhSJtjU0h73jToO6iZr3KCCCe0wggTeMIIDxqADAgECAhBrMmoPAyjT
# eh1TC/0jvUjiMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK
# ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2Vy
# dGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5l
# dHdvcmsgQ0EwHhcNMTUxMDI5MTEzMDI5WhcNMjcwNjA5MTEzMDI5WjCBgDELMAkG
# A1UEBhMCUEwxIjAgBgNVBAoMGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAl
# BgNVBAsMHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAwwb
# Q2VydHVtIENvZGUgU2lnbmluZyBDQSBTSEEyMIIBIjANBgkqhkiG9w0BAQEFAAOC
# AQ8AMIIBCgKCAQEAt9uo2MjjvNrag7q5v9bVV1NBt0C6FwxEldTpZjt/tL6Qo5QJ
# pa0hIBeARrRDJj6OSxpk7A5AMkP8gp//Si3qlN1aETaLYe/sFtRJA9jnXcNlW/JO
# CyvDwVP6QC3CqzMkBYFwfsiHTJ/RgMIYew4UvU4DQ8soSLAt5jbfGz2Lw4ydN57h
# BtclUN95Pdq3X+tGvnYoNrgCAEYD0DQbeLQox1HHyJU/bo2JGNxJ8cIPGvSBgcdt
# 1AR3xSGjLlP5d8/cqZvDweXVZy8xvMDCaJxKluUf8fNINQ725LHF74eAOuKADDSd
# +hRkceQcoaqyzwCn4zdy+UCtniiVAg3OkONbxQIDAQABo4IBUzCCAU8wDwYDVR0T
# AQH/BAUwAwEB/zAdBgNVHQ4EFgQUwHu0yLduVqcJSJr4ck/X1yQsNj4wHwYDVR0j
# BBgwFoAUCHbNywf/JPbFze27kLzihDdGdfcwDgYDVR0PAQH/BAQDAgEGMBMGA1Ud
# JQQMMAoGCCsGAQUFBwMDMC8GA1UdHwQoMCYwJKAioCCGHmh0dHA6Ly9jcmwuY2Vy
# dHVtLnBsL2N0bmNhLmNybDBrBggrBgEFBQcBAQRfMF0wKAYIKwYBBQUHMAGGHGh0
# dHA6Ly9zdWJjYS5vY3NwLWNlcnR1bS5jb20wMQYIKwYBBQUHMAKGJWh0dHA6Ly9y
# ZXBvc2l0b3J5LmNlcnR1bS5wbC9jdG5jYS5jZXIwOQYDVR0gBDIwMDAuBgRVHSAA
# MCYwJAYIKwYBBQUHAgEWGGh0dHA6Ly93d3cuY2VydHVtLnBsL0NQUzANBgkqhkiG
# 9w0BAQsFAAOCAQEAquU/dlQCTHAOKak5lgYPMbcL8aaLUvsQj09CW4y9MSMBZp3o
# KaFNw1D69/hFDh2C1/z+pjIEc/1x7MyID6OSCMWBWAL9C2k7zbg/ST3QjRwTFGgu
# mw2arbAZ4p7SfDl3iG8j/XuE/ERttbprcJJVbJSx2Df9qVkdtGOy3BPNeI4lNcGa
# jzeELtRFzOP1zI1zqOM6beeVlHBXkVC2be9zck8vAodg4uoioe0+/dGLZo0ucm1P
# xl017pOomNJnaunaGc0Cg/l0/F96GAQoHt0iMzt2bEcFXdVS/g66dvODEMduMF+n
# YMf6dCcxmyiD7SGKG/EjUoTtlbytOqWjQgGdvDCCBQcwggPvoAMCAQICECxWDYHo
# gPTFxULdYYZu+b0wDQYJKoZIhvcNAQELBQAwgYAxCzAJBgNVBAYTAlBMMSIwIAYD
# VQQKDBlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLDB5DZXJ0dW0g
# Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkxJDAiBgNVBAMMG0NlcnR1bSBDb2RlIFNp
# Z25pbmcgQ0EgU0hBMjAeFw0yMDAzMTgwNjMzMDdaFw0yMTAzMTgwNjMzMDdaMHAx
# CzAJBgNVBAYTAlBMMRAwDgYDVQQHDAdLcmFrw7N3MR4wHAYDVQQKDBVPcGVuIFNv
# dXJjZSBEZXZlbG9wZXIxLzAtBgNVBAMMJk9wZW4gU291cmNlIERldmVsb3Blciwg
# SHViZXJ0IEJ1a293c2tpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
# 3B0pw0zPe4KG0FR7q4ZrHDWpELuc3KyfaaaDkF6EXpbX5bCByq97QrQ4YJjru9UW
# JK+u45hyUpiUXWPfeKHKKz3bLMCmIMaPq+FpfSh2xzB3lFnj/1LlO7htUpfKZ6Ks
# FpCkrKk6ZiPs8PxXpfmoiOzxryySbcqVZr7ZEQnRKfl6Op4IKiZQ54lZOy4ORRMu
# ghxlxJfK49XQ5gUrV1dRL3blFSIrfOl1K6wB0/5QxxWeO4WxHzD9WtfpSTs2/gML
# fj4xe80QjaGGShpN+qtPRaa+2qEdm2Dm+Btto8Gy9eVcxUXAQNZFTZPMd2Sf71yl
# whZIsqEChoTawyrnZTMwgwIDAQABo4IBijCCAYYwDAYDVR0TAQH/BAIwADAyBgNV
# HR8EKzApMCegJaAjhiFodHRwOi8vY3JsLmNlcnR1bS5wbC9jc2Nhc2hhMi5jcmww
# cQYIKwYBBQUHAQEEZTBjMCsGCCsGAQUFBzABhh9odHRwOi8vY3NjYXNoYTIub2Nz
# cC1jZXJ0dW0uY29tMDQGCCsGAQUFBzAChihodHRwOi8vcmVwb3NpdG9yeS5jZXJ0
# dW0ucGwvY3NjYXNoYTIuY2VyMB8GA1UdIwQYMBaAFMB7tMi3blanCUia+HJP19ck
# LDY+MB0GA1UdDgQWBBSqIB8Yxug7KjAkRKhvS62xLJPR1zAdBgNVHRIEFjAUgRJj
# c2Nhc2hhMkBjZXJ0dW0ucGwwDgYDVR0PAQH/BAQDAgeAMEsGA1UdIAREMEIwCAYG
# Z4EMAQQBMDYGCyqEaAGG9ncCBQEEMCcwJQYIKwYBBQUHAgEWGWh0dHBzOi8vd3d3
# LmNlcnR1bS5wbC9DUFMwEwYDVR0lBAwwCgYIKwYBBQUHAwMwDQYJKoZIhvcNAQEL
# BQADggEBAKkWEKsxeIDC+mcLz+zJgNkK+eXZR1sEueM5LcK7iDzWPG8pPOfrKJMH
# v67m3XG1PYy54Qn3AHGIZzXPF+HgIatkEFE931TUTjUFhuTuiEtKft+gsZgEyCXG
# Km2e5fYiaBRUAtvQKPpDrocSazIP92x+blTaIKM1Z+Ysx/2YTwkpyMclviK7OisV
# JHzbKmxgLxhatMwCPtLbFuAUffDxG8igXstCbQ3Qoa4qj2HldQy4HVCYDfdA3PcV
# 9LGXPpCKGeSFCGekSdZW2f61xATc0mCpfECVTUQBJL4taNCeR219IfX20ETo+zH6
# epfSds5WtOnY/9uzFR6jLNtAx3IVrQwxggJIMIICRAIBATCBlTCBgDELMAkGA1UE
# BhMCUEwxIjAgBgNVBAoMGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNV
# BAsMHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAwwbQ2Vy
# dHVtIENvZGUgU2lnbmluZyBDQSBTSEEyAhAsVg2B6ID0xcVC3WGGbvm9MA0GCWCG
# SAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZIhvcN
# AQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUw
# LwYJKoZIhvcNAQkEMSIEIIDIPHwlLKEOOPkb/AGKE1cMCfL+6dzte3TNHSAeYCI0
# MA0GCSqGSIb3DQEBAQUABIIBAFe1gIXf6rm/DA7HeuWbAYEVhsq1AI+PLCpXxBCA
# Bg6t8KBygjRclBnmhZ5B7mNN3RgiEUeSR95eKC4NsVWrZOVdP2W3YWTwjGPeNWYG
# SAeDtqvf98/ZEuTXzUbTmhbqe1A73nyTGjRKMK6yGNNWembYH1Vv8KXobAdwfmdy
# x7dQhspUqXw6F0vosO/7Hv5RGoX4fiHP8BVW0VS8ky9pen9Na7wjkxw4z6BVYF/q
# +55sDvlZo3kF3FccTk2EXMPQH0IqvhpstrXR5D8C+va/TJ65KR62ohM8GwdQ3t5E
# iIFxidMRRMlEuE6F3uKxKmig9tylhV8w7+YfKvEAmKyfDPo=
# SIG # End signature block