ListReader.psm1

function Get-ListValue {
    <#
    .SYNOPSIS
        Access an item from the list of data prepared to iterate over.
     
    .DESCRIPTION
        Access an item from the list of data prepared to iterate over.
        Use "Start-ListReader" to prepare a list for iteration.
     
    .PARAMETER Index
        The index position of the item to retrieve.
     
    .PARAMETER Properties
        The sub-properties to access on the item selected.
        Can be multiple properties, in which case they will be considered nested sub-properties in the same order.
        E.g.: If you provide "LastWriteTime","Year", it will access the "LastWriteTime" property on the index item,
        then access the "Year" property on the result from that.
     
    .EXAMPLE
        PS C:\> Get-ListValue -Index 0
 
        Retrieve the first item of the list.
 
    .EXAMPLE
        PS C:\> Get-ListValue -Index 2 -Properties changed
 
        Retrieve the third item of the list and directly access the sub-property "changed"
    #>

    [Alias('L')]
    [CmdletBinding()]
    param (
        [Parameter(Position = 0)]
        [int[]]
        $Index = $script:__LR_Config.Index,
        
        [Parameter(Position = 1)]
        [AllowEmptyCollection()]
        [string[]]
        $Properties = $script:__LR_Config.TempProperties
    )
    if (-not $script:__LR_Config.List) { return }

    foreach ($indexItem in $Index) {
        if ($indexItem -ge @($script:__LR_Config.List).Count) {
            Write-Warning "Index specified exceeds total number of items: $indexItem specified, number of items: $(@($script:__LR_Config.List).Count)"
        }

        $item = $script:__LR_Config.List[$indexItem]
        if ($script:__LR_Config.Event) {
            try { & $script:__LR_Config.Event $item }
            catch { Write-Warning "Error processing list-item event: $_"}
        }

        if ($script:__LR_Config.Property) {
            foreach ($property in $script:__LR_Config.Property) {
                $item = $item.$property
            }
        }
        foreach ($property in $Properties) {
            $item = $item.$property
        }
        $item
    }

    $script:__LR_Config.Index = 0
    $script:__LR_Config.TempProperties = @()
}

function Register-ListHandler {
    <#
    .SYNOPSIS
        Registers the shortcut for list iteration.
     
    .DESCRIPTION
        Registers the shortcut for list iteration.
        This hijacks the "CommandNotFoundAction" event to resolve the following kinds of notations:
 
        + L0 --> Get-ListValue -Value 0
        + l-1 --> Get-ListValue -Value -1
        + L2.LastWriteTime --> Get-ListValue -Value 2 -Properties LastWriteTime
        + L2.LastWriteTime.Year --> Get-ListValue -Value 2 -Properties LastWriteTime, Year
 
        Runnning this command explicitly is optional, Start-ListReader already calls it for you.
     
    .EXAMPLE
        PS C:\> Register-ListHandler
         
        Registers the shortcut for list iteration.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidAssignmentToAutomaticVariable", "")]
    [CmdletBinding()]
    param ()

    $ExecutionContext.InvokeCommand.CommandNotFoundAction = {
        Param ($Name, $EventArgs)
        
        if ($Name.Trim('.\') -notmatch '^L(?<id>\-{0,1}\d+)([\.\w+]*)$') { return }

        $properties = @()
        if ($matches.1) {
            $properties = ($matches.1).Trim(".").Split(".")
        }
        Set-ListReaderIndex -Index $matches.id -Properties $properties
        $EventArgs.Command = Get-Command Get-ListValue
        $EventArgs.StopSearch = $true
    }
}

function Set-ListReaderIndex {
    <#
    .SYNOPSIS
        Applies the parameters chosen by the user during the list handler event.
     
    .DESCRIPTION
        Applies the parameters chosen by the user during the list handler event.
         
        When using the shortcut notation like "L0" or "L12" to access the list reader,
        this is enabled through the event handler, that injects itself into the event,
        that triggers when PowerShell cannot find a command.
 
        Problem here: This can only resolve the command, but not its parameters.
        This is resolved by using this command:
        The resolving event executes command to store the last picked index & properties,
        so that the command can use them.
     
    .PARAMETER Index
        The index of the item to look at.
     
    .PARAMETER Properties
        The sub-properties to select.
     
    .EXAMPLE
        PS C:\> Set-ListReaderIndex -Index 0 -Properties Year
 
        Sets the first item on the list to be selected, and requests the sub-property "Year"
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [int]
        $Index,

        [string[]]
        $Properties
    )
    process {
        $script:__LR_Config.Index = $Index
        $script:__LR_Config.TempProperties = $Properties
    }
}

function Start-ListReader {
    <#
    .SYNOPSIS
        Defines the list of values to read through.
     
    .DESCRIPTION
        Defines the list of values to read through.
 
        This starts the whole list reader system.
        Once this has been completed, you can for example use "L0" to access the first item, "L2" to access the third.
     
    .PARAMETER List
        The list of values to read
     
    .PARAMETER Property
        A property to retrieve, rather than just the base object.
        Supports a list of properties to look into nested sub-properties.
 
    .PARAMETER EventCode
        An event that is executed for each item iterated over.
        Allows arbitrary actions, but is intended for adding information to the user, e.g. via Write-Host
        Receives the full item (no pre-selected sub-properties).
     
    .EXAMPLE
        PS C:\> Start-ListReader -List $data
         
        Prepares the content of $data for manual iteration
 
    .EXAMPLE
        PS C:\> Start-ListReader -List $data -Property changed
         
        Prepares the content of $data for manual iteration, always directlyy accessing the content of the "changed" property.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        $List,

        [string[]]
        $Property,

        [scriptblock]
        $EventCode
    )

    Register-ListHandler

    $script:__LR_Config = @{
        List           = @($List)
        Property       = $Property
        Index          = 0
        TempProperties = @()
        Event          = $EventCode
    }
}

$script:__LR_Config = @{
    List     = $null
    Property = @()
    Index    = 0
    TempProperties = @()
}