pwshCloudCommands.psm1

# This is a locally sourced Imports file for local development.
# It can be imported by the psm1 in local development to add script level variables.
# It will merged in the build process. This is for local development only.

# region script variables
# $script:resourcePath = "$PSScriptRoot\Resources"

function Get-DataLocation {
    $folderName = 'pwshCloudCommands'
    if ($PROFILE) {
        $script:dataPath = (Join-Path (Split-Path -Parent $PROFILE) $folderName)
    }
    else {
        $script:dataPath = "~\${$folderName}"
    }
}

$domain = 'cloudfront.net'
$target = 'd42gqkczylm43'
Get-DataLocation
$script:dataFolderZip = 'pwshcloudcommands.zip'
$script:dataFolder = 'pwshcloudcommandsXML'
$script:dlURI = '{0}.{1}' -f $target, $domain


<#
.SYNOPSIS
    Confirm data output location. Creates output dir if not present.
.DESCRIPTION
    Evaluates presence of data output location for xml dataset. If the directory is not found, it will be created.
.EXAMPLE
    Confirm-DataLocation
 
    Confirms presence of data output location. Creates if not found.
.OUTPUTS
    System.Boolean
.NOTES
    Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/
.COMPONENT
    pwshCloudCommands
#>

function Confirm-DataLocation {
    [CmdletBinding()]
    param (
    )
    $result = $true #assume the best
    Write-Verbose -Message 'Verifying data set output location...'
    try {
        $pathEval = Test-Path -Path $script:dataPath -ErrorAction Stop
    }
    catch {
        $result = $false
        Write-Error $_
        return $result
    }

    if (-not ($pathEval)) {
        Write-Verbose -Message 'Creating output directory...'
        try {
            $newItemSplat = @{
                ItemType    = 'Directory'
                Path        = $script:dataPath
                ErrorAction = 'Stop'
            }
            $null = New-Item @newItemSplat
            Write-Verbose -Message 'Created.'
        }
        catch {
            $result = $false
            Write-Error $_
            return $result
        }
    } #if_TestPath
    else {
        Write-Verbose 'Data path confirmed.'
    } #else_TestPath

    return $result
} #Confirm-DataLocation


<#
.SYNOPSIS
    Confirms the XML dataset file is available and not beyond the expiration time.
.DESCRIPTION
    Confirms the XML dataset file is present on the file system for use. Determines the age of the XML dataset file. Returns true if present and 9 days older or more.
.EXAMPLE
    Confirm-XMLDataSet
 
    Checks for XML dataset and determines if it is 9 days older or more.
.OUTPUTS
    System.Boolean
.NOTES
    Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/
.COMPONENT
    pwshCloudCommands
#>

function Confirm-XMLDataSet {
    [CmdletBinding()]
    param (
    )
    $result = $true #assume the best
    $dataFile = '{0}/{1}' -f $script:dataPath, $script:dataFolderZip
    $dataExtract = '{0}/{1}' -f $script:dataPath, $script:dataFolder

    Write-Verbose -Message 'Confirming valid and current data set...'

    try {
        $pathEval = Test-Path -Path $dataFile -ErrorAction Stop
    }
    catch {
        $result = $false
        Write-Error $_
        return $result
    }

    if (-not ($pathEval)) {
        $result = $false
    } #if_pathEval
    else {
        Write-Verbose 'Data file found. Checking date of file...'
        try {
            $fileData = Get-ChildItem -Path $dataFile -ErrorAction Stop
        }
        catch {
            $result = $false
            Write-Error $_
            return $result
        }
        if ($fileData) {
            $creationDate = $fileData.LastWriteTime
            $now = Get-Date
            if (($now - $creationDate).Days -ge 9) {
                Write-Verbose 'Data file requires refresh.'
                $result = $false
            }
            else {
                Write-Verbose 'Data file verified'
                Write-Verbose 'Verifying extracted data set...'
                $dataSet = $null
                $dataSet = Get-ChildItem -Path $dataExtract -ErrorAction SilentlyContinue
                if ($null -eq $dataSet) {
                    Write-Verbose 'Extracted data set is missing.'
                    $result = $false
                    return $result
                }
            }
        } #if_fileData
        else {
            Write-Warning 'Unable to retrieve file information for pwshCloudCommands data set.'
            $result = $false
            return $result
        } #else_fileData
    } #else_pathEval

    return $result
} #Confirm-XMLDataSet


<#
.SYNOPSIS
    Unzips the XML data set.
.DESCRIPTION
    Evaluates for previous version of XML data set and removes if required. Expands the XML data set for use.
.EXAMPLE
    Expand-XMLDataSet
 
    Unzips and expands the XML data set.
.OUTPUTS
    System.Boolean
.NOTES
    Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/
.COMPONENT
    pwshCloudCommands
#>

function Expand-XMLDataSet {
    [CmdletBinding()]
    param (
    )
    $result = $true #assume the best
    $dataFolder = '{0}/{1}' -f $script:dataPath, $script:dataFolder

    Write-Verbose -Message 'Testing if data set folder already exists...'
    try {
        $pathEval = Test-Path -Path $dataFolder -ErrorAction Stop
        Write-Verbose -Message "EVAL: $true"
    }
    catch {
        $result = $false
        Write-Error $_
        return $result
    }

    if ($pathEval) {
        Write-Verbose -Message 'Removing existing data set folder...'
        try {
            $removeItemSplat = @{
                Force       = $true
                Path        = $dataFolder
                Recurse     = $true
                ErrorAction = 'Stop'
            }
            Remove-Item @removeItemSplat
        } #try
        catch {
            $result = $false
            Write-Error $_
            return $result
        } #catch
    } #if_pathEval

    Write-Verbose -Message 'Expanding data set archive...'
    try {
        $expandArchiveSplat = @{
            DestinationPath = '{0}/{1}' -f $script:dataPath, $script:dataFolder
            Force           = $true
            ErrorAction     = 'Stop'
            Path            = '{0}/{1}' -f $script:dataPath, $script:dataFolderZip
        }
        $null = Expand-Archive @expandArchiveSplat
        Write-Verbose -Message 'Expand completed.'
    } #try
    catch {
        $result = $false
        Write-Error $_
    } #catch

    return $result
} #Expand-XMLDataSet


<#
.SYNOPSIS
    Processes discovered cloud commands and returns formatted results in a PSCustomObject format.
.DESCRIPTION
    This function is called to process discovered cloud commands found in files.
    The function is passed a PSCustomObject that contains the discovered cloud command.
    Findings are organized by module name, function, and file found in.
    The function returns a PSCustomObject that contains the formatted results.
.EXAMPLE
    Format-FileFinding -CloudCommandObj $cloudCommands -FileInfo $fileInfo
.PARAMETER CloudCommandObj
    Object that contains the discovered cloud command(s).
.PARAMETER FileInfo
    Object containing the file information that cloud command discovery was done on.
.OUTPUTS
    System.Management.Automation.PSCustomObject
.NOTES
    Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/
 
    ModuleName Name
    ---------- ----
    AWS.Tools.SimpleSystemsManagement Get-SSMDocumentList
    AWS.Tools.SimpleSystemsManagement Get-SSMDocumentList
    AWS.Tools.EC2 Start-EC2Instance
    AWS.Tools.S3 Get-S3Bucket
    Az.Accounts Connect-AzAccount
    Az.Accounts Set-AzContext
    Az.Resources Get-AzResourceGroup
.COMPONENT
    pwshCloudCommands
#>

function Format-FileFinding {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true,
            HelpMessage = 'Object that contains the discovered cloud commands')]
        [psobject]
        $CloudCommandObj,

        [Parameter(Mandatory = $true,
            HelpMessage = 'Object containing the file information that cloud command discovery was done on')]
        [psobject]
        $FileInfo
    )

    $results = [System.Collections.ArrayList]::new()

    $uniqueModuleNames = $CloudCommandObj | Select-Object -ExpandProperty ModuleName -Unique

    foreach ($module in $uniqueModuleNames) {
        $uniqueFunctions = $cloudCommands | Where-Object {
            $_.ModuleName -eq $module
        } | Select-Object -ExpandProperty Name -Unique
        $obj = [PSCustomObject]@{
            ModuleName = $module
            Functions  = $uniqueFunctions
            FileName   = $FileInfo.Name
            FilePath   = $FileInfo.FullName
        }
        [void]$results.Add($obj)
    } #foreach_module

    return $results
} #Format-FileFinding


<#
.SYNOPSIS
    Retrieves all PowerShell files from specified path
.DESCRIPTION
    Retrieves all Get-ChildItem information with a ps1 filter from the specified path.
.EXAMPLE
    Get-AllPowerShellFile -Path "$env:HOME\pathToEvaluate"
 
    Retrieves all PowerShell files from specified path
.PARAMETER Path
    Path to search for PowerShell files
.OUTPUTS
    System.IO.FileInfo
.NOTES
    Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/
.COMPONENT
    pwshCloudCommands
#>

function Get-AllPowerShellFile {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true,
            Position = 0,
            HelpMessage = 'Path to search for PowerShell files')]
        [string]$Path
    )

    Write-Verbose -Message ('Retrieving PowerShell files from {0} ' -f $Path)
    $getChildItemSplat = @{
        Path        = $Path
        Filter      = '*.ps1'
        Recurse     = $true
        Force       = $true
        ErrorAction = 'SilentlyContinue'
    }
    $psFiles = Get-ChildItem @getChildItemSplat

    return $psFiles

} #Get-AllPowerShellFile


<#
.SYNOPSIS
    Initiates FunctionQuery against XML dataset for each discovered PowerShell function token.
.DESCRIPTION
    Takes in a collection of discovered tokens from file.
    Parses tokens for PowerShell function names.
    Initiates FunctionQuery against XML dataset for each discovered PowerShell function token.
    Returns search results.
.EXAMPLE
    Get-CloudCommandFromToken -Tokens $tokens
 
    Initiates FunctionQuery search for each discovered PowerShell function token.
.PARAMETER Tokens
    A collection of discovered tokens from file.
.OUTPUTS
    Deserialized.System.Management.Automation.PSCustomObject
.NOTES
    Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/
.COMPONENT
    pwshCloudCommands
#>

function Get-CloudCommandFromToken {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true,
            HelpMessage = 'A collection of discovered tokens from file')]
        [psobject]
        $Tokens
    )

    $results = [System.Collections.ArrayList]::new()

    foreach ($token in $Tokens) {
        if ($token.Type -ne 'Command' -or $token.Content -notlike '*-*') {
            continue
        }
        # -----------------------
        # resets
        $search = $null
        # -----------------------
        $search = Search-XMLDataSet -FunctionQuery $token.Content
        if ($null -ne $search) {
            [void]$results.Add($search)
        }
    } #foreach_token

    return $results

} #Get-CloudCommandFromToken


<#
.SYNOPSIS
    Downloads XML Data set to device.
.DESCRIPTION
    Retrieves XML Data zip file from web and downloads to device.
.EXAMPLE
    Get-XMLDataSet
 
    Downloads XML data set to data path.
.OUTPUTS
    System.Boolean
.NOTES
    Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/
    Overwrites existing zip file.
.COMPONENT
    pwshCloudCommands
#>

function Get-XMLDataSet {
    [CmdletBinding()]
    param (
    )
    $result = $true #assume the best

    Write-Verbose -Message 'Downloading XML data set...'
    try {
        $invokeWebRequestSplat = @{
            OutFile     = '{0}/{1}' -f $script:dataPath, $script:dataFolderZip
            Uri         = 'https://{0}/{1}' -f $script:dlURI, $script:dataFolderZip
            ErrorAction = 'Stop'
        }
        $oldProgressPreference = $progressPreference
        $progressPreference = 'SilentlyContinue'
        if ($PSEdition -eq 'Desktop') {
            $null = Invoke-WebRequest @invokeWebRequestSplat -PassThru -UseBasicParsing
        }
        else {
            $null = Invoke-WebRequest @invokeWebRequestSplat -PassThru
        }
    } #try
    catch {
        $result = $false
        Write-Error $_
    } #catch
    finally {
        $progressPreference = $oldProgressPreference
    } #finally
    return $result
} #Get-XMLDataSet


<#
.SYNOPSIS
    Invokes all child functions required to process retrieving the XML data set file.
.DESCRIPTION
    Runs all required child functions to successfully retrieve and process the XML data set file.
.EXAMPLE
    Invoke-XMLDataCheck
 
    Downloads, expands, and verified the XML data set file.
.OUTPUTS
    System.Boolean
.NOTES
    Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/
    Confirm-XMLDataSet
    Get-XMLDataSet
    Expand-XMLDataSet
.COMPONENT
    pwshCloudCommands
#>

function Invoke-XMLDataCheck {
    [CmdletBinding(ConfirmImpact = 'Low',
        SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $false,
            HelpMessage = 'Skip confirmation')]
        [switch]$Force
    )
    Begin {

        if (-not $PSBoundParameters.ContainsKey('Verbose')) {
            $VerbosePreference = $PSCmdlet.SessionState.PSVariable.GetValue('VerbosePreference')
        }
        if (-not $PSBoundParameters.ContainsKey('Confirm')) {
            $ConfirmPreference = $PSCmdlet.SessionState.PSVariable.GetValue('ConfirmPreference')
        }
        if (-not $PSBoundParameters.ContainsKey('WhatIf')) {
            $WhatIfPreference = $PSCmdlet.SessionState.PSVariable.GetValue('WhatIfPreference')
        }

        Write-Verbose -Message ('[{0}] Confirm={1} ConfirmPreference={2} WhatIf={3} WhatIfPreference={4}' -f $MyInvocation.MyCommand, $Confirm, $ConfirmPreference, $WhatIf, $WhatIfPreference)

        $results = $true #assume the best
    } #begin
    Process {
        # -Confirm --> $ConfirmPreference = 'Low'
        # ShouldProcess intercepts WhatIf* --> no need to pass it on
        if ($Force -or $PSCmdlet.ShouldProcess("ShouldProcess?")) {
            Write-Verbose -Message ('[{0}] Reached command' -f $MyInvocation.MyCommand)
            $ConfirmPreference = 'None'

            $dataOutputDir = Confirm-DataLocation

            if ($dataOutputDir -eq $true) {

                $confirm = Confirm-XMLDataSet
                if (($Confirm -eq $false)) {

                    $retrieve = Get-XMLDataSet
                    if ($retrieve -eq $true) {

                        $expand = Expand-XMLDataSet
                        if ($expand -eq $false) {
                            $results = $false
                        }

                    }
                    else {
                        $results = $false
                    }

                } #if_Confirm

            } #if_data_output
            else {
                $results = $false
            } #else_data_output

        } #if_Should
    } #process
    End {
        return $results
    } #end
} #Invoke-XMLDataCheck


<#
.SYNOPSIS
    Takes in free form query and optimizes for use in a verb / term bases search.
.DESCRIPTION
    Takes in free form query and removes all stop words, special characters, and white space.
    Identifies verb and terms and forms an object to be used in a search.
.EXAMPLE
    Optimize-Input -SearchInput 'write an object to a s3 bucket'
 
    Takes in free form query and optimizes for use in a verb / term bases search.
.OUTPUTS
    System.Management.Automation.PSCustomObject
.NOTES
    Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/
.COMPONENT
    pwshCloudCommands
#>

function Optimize-Input {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true,
            Position = 0,
            HelpMessage = 'TBD')]
        [string[]]$SearchInput
    )

    # $SearchInput = 'write write *OBJECT* to bucket $&*@(*@'
    $splitInput = $SearchInput.Split(' ')
    $noSpecialCharInput = $splitInput -replace '\W', ''
    $lowerInput = $noSpecialCharInput.ToLower()
    $noDupInput = $lowerInput | Select-Object -Unique
    $noEmptyArray = $noDupInput.Split('', [System.StringSplitOptions]::RemoveEmptyEntries)
    if ([string]::IsNullOrWhiteSpace($noEmptyArray)) {
        return $null
    }
    $cleanInput = Remove-StopWord -SearchInput $noEmptyArray

    $verbs = [System.Collections.ArrayList]::new()
    $terms = [System.Collections.ArrayList]::new()
    $psVerbs = Get-Verb | Select-Object -ExpandProperty Verb

    $cleanInput | ForEach-Object {
        if ($psVerbs -contains $_) {
            [void]$verbs.Add($_)
        }
        else {
            [void]$terms.Add($_)
        }
    }
    $obj = [PSCustomObject]@{
        Verbs = $verbs
        Terms = $terms
    }
    return $obj
} #Optimize-Input


<#
.SYNOPSIS
    Parses the given file and returns a list of all the command and command arguments tokens.
.DESCRIPTION
    Gets contents of specified file and parses it into a list of tokens.
    Command and command arguments tokens are returned.
.EXAMPLE
    Read-TokenCommandsFromFile -FilePath $file.FullName
 
    Returns all the command and command arguments tokens in the specified file.
.PARAMETER FilePath
    Path to file
.OUTPUTS
    System.Management.Automation.PSToken
.NOTES
    Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/
.COMPONENT
    pwshCloudCommands
#>

function Read-TokenCommandsFromFile {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true,
            HelpMessage = 'Path to file')]
        [string]
        $FilePath
    )

    if (-not (Test-Path -Path $FilePath -PathType Leaf)) {
        Write-Warning -Message ('{0} is not a valid file path.' -f $FilePath)
        throw $_
    }

    $getContentSplat = @{
        Path        = $FilePath
        Raw         = $true
        ErrorAction = 'Stop'
    }
    try {
        $rawFileContent = Get-Content @getContentSplat
    }
    catch {
        Write-Warning -Message ('Contents of {0} could not be retrieved' -f $FilePath)
        throw $_
    }

    if ([string]::IsNullOrWhiteSpace($rawFileContent)) {
        Write-Warning -Message ('Contents of {0} is empty' -f $FilePath)
        return
    }

    $commandTokens = [System.Collections.ArrayList]::new()

    $tokens = [System.Management.Automation.PSParser]::Tokenize(($rawFileContent), [ref]$null)
    foreach ($token in $tokens) {

        if ($token.Type -ne 'Command' -and $token.Type -ne 'CommandArgument') {
            continue
        }

        [void]$commandTokens.Add($token)

    } #foreach_token

    return $commandTokens

} #Read-TokenCommandsFromFile


<#
.SYNOPSIS
    Removes stop words from provided input text.
.DESCRIPTION
    Parses provided input text and removes stop words.
.EXAMPLE
    Remove-StopWord -SearchInput $searchInput
 
    Returns the input text with stop words removed.
.OUTPUTS
    System.String
.NOTES
    Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/
.COMPONENT
    pwshCloudCommands
#>

function Remove-StopWord {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true,
            Position = 0,
            HelpMessage = 'TBD')]
        [string[]]$SearchInput
    )

    $stopWords = @(
        'a'
        'about'
        'above'
        'actually'
        'after'
        'again'
        'against'
        'all'
        'almost'
        'also'
        'although'
        'always'
        'am'
        'an'
        'and'
        'any'
        'are'
        'as'
        'at'
        'be'
        'became'
        'because'
        'become'
        'been'
        'before'
        'being'
        'below'
        'between'
        'both'
        'but'
        'by'
        'can'
        'cannot'
        'could'
        'did'
        'do'
        'does'
        'doing'
        'down'
        'during'
        'each'
        'either'
        'else'
        'few'
        'for'
        'from'
        'further'
        'had'
        'has'
        'have'
        'having'
        'he'
        'hence'
        'her'
        'here'
        'hers'
        'herself'
        'him'
        'himself'
        'his'
        'how'
        'i'
        'if'
        'in'
        'into'
        'is'
        'it'
        'its'
        'itself'
        'just'
        'may'
        'maybe'
        'me'
        'might'
        'mine'
        'more'
        'most'
        'must'
        'my'
        'myself'
        'neither'
        'no'
        'nor'
        'not'
        'of'
        'off'
        'oh'
        'ok'
        'on'
        'once'
        'only'
        'or'
        'other'
        'ought'
        'our'
        'ours'
        'ourselves'
        'out'
        'over'
        'own'
        'same'
        'she'
        'so'
        'some'
        'such'
        'than'
        'that'
        'the'
        'their'
        'theirs'
        'them'
        'themselves'
        'then'
        'there'
        'these'
        'they'
        'this'
        'those'
        'through'
        'to'
        'too'
        'under'
        'very'
        'was'
        'we'
        'were'
        'what'
        'whenever'
        'whereas'
        'wherever'
        'whether'
        'who'
        'whoever'
        'whom'
        'whose'
        'why'
        'will'
        'with'
        'within'
        'without'
        'would'
        'yes'
        'yet'
        'you'
        'your'
        'yours'
        'yourself'
        'yourselves'
    )

    $cleanInput = $SearchInput | Where-Object { $stopWords -notcontains $_ }

    return $cleanInput

} #Remove-StopWord


<#
.SYNOPSIS
    Queries XML data set to discover PowerShell cloud commands based on provided inputs.
.DESCRIPTION
    Primary private function that performs the appropriate query against the XML data set.
 
.EXAMPLE
    Search-XMLDataSet -Query $cleanQuery
 
    Performs a general search against the XML data set.
 
.EXAMPLE
    Search-XMLDataSet -FunctionQuery 'Write-S3Object'
 
    Performs a function query against the XML data set.
.EXAMPLE
    Search-XMLDataSet -WildCardQuery 'write-s3*'
 
    Performs a wildcard query against the XML data set.
.PARAMETER Query
    Clean input query in Verb Term Format.
.PARAMETER FunctionQuery
    PowerShell function name to query against the XML data set.
.PARAMETER WildCardQuery
    PowerShell function name with wildcard to query against the XML data set.
.PARAMETER AllInfo
    Return all info from the XML data set.
.PARAMETER Filter
    Cloud filter to apply to the query. This drastically improves the performance of the query.
.OUTPUTS
    Deserialized.System.Management.Automation.PSCustomObject
.NOTES
    Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/
 
    If using a generic query this function expects a clean query input
    that is currently being provided by Optimize-Input
.COMPONENT
    pwshCloudCommands
#>

function Search-XMLDataSet {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true,
            Position = 0,
            HelpMessage = 'Clean input query in Verb Term Format',
            ParameterSetName = 'Input')]
        [psobject]$Query,

        [Parameter(Mandatory = $true,
            Position = 1,
            HelpMessage = 'PowerShell function name to query against the XML data set',
            ParameterSetName = 'Function')]
        [string]$FunctionQuery,

        [Parameter(Mandatory = $true,
            Position = 2,
            HelpMessage = 'PowerShell function name with wildcard to query against the XML data set',
            ParameterSetName = 'WildCard')]
        [string]$WildCardQuery,

        [Parameter(Mandatory = $true,
            Position = 3,
            HelpMessage = 'Return all info from the XML data set',
            ParameterSetName = 'All')]
        [switch]$AllInfo,

        [Parameter(Mandatory = $false,
            Position = 4,
            HelpMessage = 'Cloud filter to apply to the query')]
        [ValidateSet('AWS', 'Azure', 'Oracle')]
        [string]
        $Filter
    )

    Write-Debug -Message ('Parameter set: {0}' -f $PSCmdlet.ParameterSetName)

    switch ($Filter) {
        'AWS' {
            $searchFilter = 'AWS.Tools.*'
        }
        'Azure' {
            $searchFilter = 'Az.*'
        }
        'Oracle' {
            $searchFilter = 'OCI.*'
        }
        Default {
            $searchFilter = '*'
        }
    } #switch_filter
    Write-Debug -Message ('Search filter: {0}' -f $searchFilter)

    Write-Verbose -Message 'Retrieving xml file info...'
    $xmlDataPath = '{0}\{1}' -f $script:dataPath, $script:dataFolder
    $getChildItemSplat = @{
        Path        = $xmlDataPath
        Filter      = $searchFilter
        ErrorAction = 'Stop'
    }
    try {
        $xmlDataFiles = Get-ChildItem @getChildItemSplat
    }
    catch {
        Write-Warning -Message 'An error was encountered getting xml file info.'
        Write-Error $_
        throw
    }

    # special case for Azure selection where we will also retrieve Graph Modules
    if ($Filter -eq 'Azure') {
        Write-Debug -Message 'Retrieving Azure Graph xml file info...'
        $getChildItemSplat = @{
            Path        = $xmlDataPath
            Filter      = 'Microsoft.Graph.*'
            ErrorAction = 'Stop'
        }
        try {
            $graphFiles = Get-ChildItem @getChildItemSplat
        }
        catch {
            Write-Warning -Message 'An error was encountered getting xml file info.'
            Write-Error $_
            throw
        }
        $xmlDataFiles += $graphFiles
    }

    Write-Verbose -Message 'Running query...'
    if ($PSCmdlet.ParameterSetName -eq 'Function') {
        #------------------------------
        $xmlCount = ($xmlDataFiles | Measure-Object).Count
        $i = 0
        #------------------------------
        foreach ($xml in $xmlDataFiles) {
            # ------------------------------
            # resets
            $rawData = $null
            $xmlData = $null
            $function = $null
            # ------------------------------
            $i++
            Write-Progress -Activity 'Searching...' -PercentComplete ($i / $xmlCount * 100)

            Write-Debug -Message ('Processing {0}' -f $xml.Name)
            try {
                $rawData = Get-Content $xml.FullName -Raw -ErrorAction 'Stop'
            }
            catch {
                Write-Warning -Message ('An error was encountered reading xml data file {0}...' -f $xml.Name)
                Write-Error $_
                throw
            }

            if ($rawData -match $FunctionQuery) {
                Write-Debug -Message ('Function query match found in {0}' -f $xml.Name)
                $xmlData = $rawData | ConvertFrom-Clixml
            }
            else {
                continue
            }

            if ($xmlData.ExportedCommands.Keys -contains $FunctionQuery) {
                $function = $xmlData.Functions | Where-Object {
                    $_.Name -eq $FunctionQuery -or $_.DisplayName -eq $FunctionQuery
                }
                if ($null -ne $function) {

                    if ($function.CommandType.Value -eq 'Alias') {
                        Write-Debug -Message ('Alias located in: {0}' -f $xml.Name)
                        Write-Warning -Message ('Aliases not supported - {0} is an alias for {1}' -f $function.DisplayName, $function.ResolvedCommand)
                        continue
                    }
                    else {
                        Write-Debug -Message ('Function command found: {0}' -f $function.Name)
                    }

                    Add-Member -InputObject $function -MemberType NoteProperty -Name 'ModuleName' -Value $xmlData.Name -Force
                    $function.PSObject.TypeNames.Insert(0, 'pFindCCFormat')
                    return $function
                }
            }

        } #foreach_xml

        return $null

    } #if_function_query
    elseif ($PSCmdlet.ParameterSetName -eq 'WildCard') {
        #------------------------------
        $xmlCount = ($xmlDataFiles | Measure-Object).Count
        $i = 0
        $matchResults = [System.Collections.ArrayList]::new()
        $words = $WildCardQuery.Split('*')
        $wordsArray = $words | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
        #------------------------------
        foreach ($xml in $xmlDataFiles) {
            # ------------------------------
            # resets
            $rawData = $null
            $xmlData = $null
            $function = $null
            $fileParse = $false
            # ------------------------------
            $i++
            Write-Progress -Activity 'Searching...' -PercentComplete ($i / $xmlCount * 100)

            Write-Debug -Message ('Processing {0}' -f $xml.Name)
            try {
                $rawData = Get-Content $xml.FullName -Raw -ErrorAction 'Stop'
            }
            catch {
                Write-Warning -Message ('An error was encountered reading cloud data file {0}...' -f $xml.Name)
                Write-Error $_
                throw
            }

            foreach ($word in $wordsArray) {
                if ($rawData -match $word) {
                    Write-Debug -Message ('Match found in {0}' -f $xml.Name)
                    $fileParse = $true
                }
            }

            if ($fileParse -eq $true) {
                $xmlData = $rawData | ConvertFrom-Clixml
            }
            else {
                continue
            }

            foreach ($function in $xmlData.Functions) {
                Write-Debug -Message ('....Processing {0}' -f $function.Name)
                if ($function.Name -like $WildCardQuery -or $function.DisplayName -like $WildCardQuery) {
                    Write-Debug -Message ('Function query matched: {0}' -f $function.Name)

                    if ($function.CommandType.Value -eq 'Alias') {
                        Write-Debug -Message ('Alias located in: {0}' -f $xml.Name)
                        Write-Warning -Message ('Aliases not supported - {0} is an alias for {1}' -f $function.DisplayName, $function.ResolvedCommand)
                        continue
                    }

                    Add-Member -InputObject $function -MemberType NoteProperty -Name 'ModuleName' -Value $xmlData.Name -Force
                    $function.PSObject.TypeNames.Insert(0, 'pFindCCFormat')
                    [void]$matchResults.Add($function)
                }
            } #foreach_function

        } #foreach_xml

        return $matchResults

    } #elseif_wildcard
    elseif ($AllInfo -eq $true) {
        #------------------------------
        $xmlCount = ($xmlDataFiles | Measure-Object).Count
        $i = 0
        $matchResults = [System.Collections.ArrayList]::new()
        #------------------------------
        foreach ($xml in $xmlDataFiles) {
            # ------------------------------
            # resets
            $rawData = $null
            $xmlData = $null
            $function = $null
            $fileParse = $false
            # ------------------------------
            $i++
            Write-Progress -Activity 'Searching...' -PercentComplete ($i / $xmlCount * 100)

            Write-Debug -Message ('Processing {0}' -f $xml.Name)
            try {
                $rawData = Get-Content $xml.FullName -Raw -ErrorAction 'Stop'
                $xmlData = $rawData | ConvertFrom-Clixml
            }
            catch {
                Write-Warning -Message ('An error was encountered reading cloud data file {0}...' -f $xml.Name)
                Write-Error $_
                throw
            }

            foreach ($function in $xmlData.Functions) {
                Write-Debug -Message ('....Processing {0}' -f $function.Name)
                if ($function.Name -like $WildCardQuery -or $function.DisplayName -like $WildCardQuery) {
                    Write-Debug -Message ('Function query matched: {0}' -f $function.Name)

                    if ($function.CommandType.Value -eq 'Alias') {
                        # skip aliases
                        continue
                    }

                    Add-Member -InputObject $function -MemberType NoteProperty -Name 'ModuleName' -Value $xmlData.Name -Force
                    $function.PSObject.TypeNames.Insert(0, 'pFindCCFormat')
                    [void]$matchResults.Add($function)
                }
            } #foreach_function

        } #foreach_xml

        return $matchResults

    } #elseif_all
    else {
        #------------------------------
        $xmlCount = ($xmlDataFiles | Measure-Object).Count
        $i = 0
        $matchResults = [System.Collections.ArrayList]::new()
        #------------------------------
        foreach ($xml in $xmlDataFiles) {
            # ---------------------------
            # resets
            $rawData = $null
            $xmlData = $null
            $fileParse = $false
            # ---------------------------
            $i++
            Write-Progress -Activity 'Searching...' -PercentComplete ($i / $xmlCount * 100)

            Write-Debug -Message ('Processing {0}' -f $xml.Name)
            try {
                $rawData = Get-Content $xml.FullName -Raw -ErrorAction 'Stop'
            }
            catch {
                Write-Warning -Message ('An error was encountered reading cloud data file {0}...' -f $xml.Name)
                Write-Error $_
                throw
            }

            #--------------------------------
            # raw string match checks
            foreach ($verb in $Query.Verbs) {
                if ($rawData -match $verb) {
                    $fileParse = $true
                }
            }
            foreach ($term in $Query.Terms) {
                if ($rawData -match $term) {
                    $fileParse = $true
                }
            }
            #--------------------------------

            if ($fileParse -eq $true) {
                $xmlData = $rawData | ConvertFrom-Clixml
            }
            else {
                continue
            }

            foreach ($function in $xmlData.Functions) {
                # ---------------------------
                # resets
                $verbMatch = $false
                [int]$matchScore = 0
                # ---------------------------

                foreach ($verb in $Query.Verbs) {
                    if ($function.Verb -match $verb) {
                        Write-Debug -Message ('{0} matched verb: {1}' -f $function.Name, $verb)
                        $matchScore += 20
                        $verbMatch = $true
                    }
                } #foreach_verb

                foreach ($term in $Query.Terms) {
                    if ($function.Noun -match $term) {
                        Write-Debug -Message ('{0} matched noun: {1}' -f $function.Name, $term)
                        $matchScore += 10
                    }
                    if ($function.Synopsis -match $term) {
                        Write-Debug -Message ('{0} Synopsis matched: {1}' -f $function.Name, $term)
                        $matchScore += ($function.Synopsis | Select-String $term -AllMatches).Matches.Value.count
                    }
                    if ($function.Description -match $term) {
                        Write-Debug -Message ('{0} Description matched: {1}' -f $function.Name, $term)
                        $matchScore += ($function.Description | Select-String $term -AllMatches).Matches.Value.count
                    }

                } #foreach_term

                if ($verbMatch -eq $true ) {
                    [int]$cutOff = 20
                }
                else {
                    [int]$cutOff = 0
                }
                Write-Debug -Message ('Match score: {0}' -f $matchScore)
                Write-Debug -Message ('Cutoff: {0}' -f $matchScore)

                if ($matchScore -gt $cutOff) {
                    Add-Member -InputObject $function -MemberType NoteProperty -Name 'MatchScore' -Value $matchScore -Force
                    Add-Member -InputObject $function -MemberType NoteProperty -Name 'ModuleName' -Value $xmlData.Name -Force
                    $function.PSObject.TypeNames.Insert(0, 'pFindCCFormat')
                    [void]$matchResults.Add($function)
                }

            } #foreach_function

        } #foreach_xmldatafiles

        return $matchResults | Where-Object { $_.MatchScore -gt 0 }

    } #else_input

} #Search-XMLDataSet


<#
.EXTERNALHELP pwshCloudCommands-help.xml
#>

function Find-CloudCommand {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true,
            Position = 0,
            HelpMessage = 'Search input for PowerShell cloud commands.')]
        [ValidateLength(3, 60)]
        [string]
        $Query,

        [Parameter(Mandatory = $false,
            Position = 1,
            HelpMessage = 'Filters the search to a specific cloud platform')]
        [ValidateSet('AWS', 'Azure', 'Oracle')]
        [string]
        $Filter,

        [Parameter(Mandatory = $false,
            Position = 2,
            HelpMessage = 'Retrieves all search results without limiting the number')]
        [switch]
        $AllResults
    )

    Write-Verbose -Message 'Verifying XML Data Set Availability...'
    $dataSet = Invoke-XMLDataCheck
    if ($dataSet -ne $true) {
        Write-Warning -Message 'pwshCloudCommands was unable to source the required data set files.'
        Write-Warning -Message 'Ensure you have an active internet connection'
        return
    }

    Write-Verbose 'Determining query type...'
    if ($Query -match '^(\w+-\w+)$') {
        Write-Verbose -Message 'Query is a valid function name.'
        $queryType = 'function'
        $searchXMLDataSetSplat = @{
            FunctionQuery = $Query
        }
    }
    elseif ($Query -match '^\w.*\*.*$' -or $Query -match '^\*.*\w.*$') {
        Write-Verbose -Message 'Query is a wildcard function search.'
        $queryType = 'wildcard'
        $searchXMLDataSetSplat = @{
            WildCardQuery = $Query
        }
    }
    else {
        Write-Verbose -Message 'Free form query. Optimizing query input'
        $queryType = 'freeform'
        $cleanInput = Optimize-Input -SearchInput $Query
        $searchXMLDataSetSplat = @{
            Query = $cleanInput
        }
    }
    if ($Filter) {
        $searchXMLDataSetSplat.Add('Filter', $Filter)
    }

    Write-Verbose ($searchXMLDataSetSplat | Out-String)

    $results = Search-XMLDataSet @searchXMLDataSetSplat
    if ($queryType -eq 'freeform' -and $AllResults -eq $false) {
        $results = $results | Sort-Object MatchScore -Descending | Select-Object -First 30
    }

    return $results | Sort-Object MatchScore -Descending

} #Find-CloudCommand



<#
.EXTERNALHELP pwshCloudCommands-help.xml
#>

function Get-AllCloudCommandInfo {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false,
            Position = 0,
            HelpMessage = 'Filters the search to a specific cloud platform')]
        [ValidateSet('AWS', 'Azure', 'Oracle')]
        [string]
        $Filter
    )

    Write-Verbose -Message 'Verifying XML Data Set Availability...'
    $dataSet = Invoke-XMLDataCheck
    if ($dataSet -ne $true) {
        Write-Warning -Message 'pwshCloudCommands was unable to source the required data set files.'
        Write-Warning -Message 'Ensure you have an active internet connection'
        return
    }

    $searchXMLDataSetSplat = @{
        AllInfo = $true
    }
    if ($Filter) {
        $searchXMLDataSetSplat.Add('Filter', $Filter)
    }

    Write-Verbose ($searchXMLDataSetSplat | Out-String)

    $results = Search-XMLDataSet @searchXMLDataSetSplat

    return $results

} #Get-AllCloudCommandInfo



<#
.EXTERNALHELP pwshCloudCommands-help.xml
#>

function Get-CloudCommandFromFile {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true,
            Position = 0,
            HelpMessage = 'File or directory path to be evaluated for cloud command usage')]
        [string]
        $Path
    )

    Write-Verbose -Message 'Verifying XML Data Set Availability...'
    $dataSet = Invoke-XMLDataCheck
    if ($dataSet -ne $true) {
        Write-Warning -Message 'pwshCloudCommands was unable to source the required data set files.'
        Write-Warning -Message 'Ensure you have an active internet connection'
        return
    }

    try {
        $target = Get-Item -Path $Path -ErrorAction 'Stop'
    }
    catch {
        Write-Error $_
        throw
    }

    if ($target.PSIsContainer -eq $true) {
        $pwshFileInfo = Get-AllPowerShellFile -Path $Path
        if (-not ($pwshFileInfo)) {
            Write-Warning -Message ('No PowerShell files found at {0}' -f $Path)
            return
        }
    }
    else {
        try {
            $pwshFileInfo = Get-ChildItem -Path $Path -ErrorAction 'Stop'
        }
        catch {
            Write-Error $_
            throw
        }
        if ($pwshFileInfo.Extension -ne '.ps1') {
            Write-Warning -Message ('{0} is not a PowerShell file.' -f $pwshFileInfo.Name)
            return
        }
    }

    $results = [System.Collections.ArrayList]::new()

    foreach ($file in $pwshFileInfo) {
        # ----------------------------
        # resets
        $tokens = $null
        $cloudCommands = $null
        $fileResults = $null
        # ----------------------------
        $tokens = Read-TokenCommandsFromFile -FilePath $file.FullName
        $cloudCommands = Get-CloudCommandFromToken -Tokens $tokens
        if ($cloudCommands) {
            $fileResults = Format-FileFinding -CloudCommandObj $cloudCommands -FileInfo $file

            $obj = New-Object PSObject -Property ([ordered]@{
                    FileName      = $file.Name
                    CloudCommands = $fileResults
                })#psobject
            [void]$results.Add($obj)
        }
    } #foreach_file

    return $results

} #Get-CloudCommandFromFile