Elizium.Loopz.psm1

Set-StrictMode -Version 1.0

$global:LoopzHelpers = @{
  # Helper Script Blocks
  #
  WhItemDecoratorBlock = [scriptblock] {
    param(
      [Parameter(Mandatory)]
      $_underscore,

      [Parameter(Mandatory)]
      [int]$_index,

      [Parameter(Mandatory)]
      [System.Collections.Hashtable]$_passThru,

      [Parameter(Mandatory)]
      [boolean]$_trigger
    )

    return Write-HostFeItemDecorator -Underscore $_underscore `
      -Index $_index `
      -PassThru $_passThru `
      -Trigger $_trigger
  }

  SimpleSummaryBlock   = [scriptblock] {
    param(
      [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
      [int]$Count,
      [int]$Skipped,
      [boolean]$Triggered,
      [System.Collections.Hashtable]$PassThru = @{}
    )
  
    [System.Collections.Hashtable]$krayolaTheme = $PassThru.ContainsKey(
      'LOOPZ.WH-FOREACH-DECORATOR.KRAYOLA-THEME') `
      ? $PassThru['LOOPZ.WH-FOREACH-DECORATOR.KRAYOLA-THEME'] : $(Get-KrayolaTheme);

    $metaColours = $krayolaTheme['META-COLOURS'];

    $line = $colouredLine = $null;
    if ($PassThru.ContainsKey('LOOPZ.SUMMARY-BLOCK.LINE')) {
      $line = $PassThru['LOOPZ.SUMMARY-BLOCK.LINE'];
      $colouredLine = @($line) + $metaColours;

      Write-InColour -TextSnippets @(, $colouredLine);
    }

    [string[][]]$properties = @(
      @('Count', $Count),
      @('Skipped', $Skipped),
      @('Triggered', $Triggered)
    )

    [string]$message = $PassThru.ContainsKey('LOOPZ.SUMMARY-BLOCK.MESSAGE') `
      ? $PassThru['LOOPZ.SUMMARY-BLOCK.MESSAGE'] : 'Summary';

    Write-ThemedPairsInColour -Pairs $properties -Theme $krayolaTheme -Message $message;

    if ($colouredLine) {
      Write-InColour -TextSnippets @(, $colouredLine);
    }
  }
}

# Session UI state
#
[int]$global:_LineLength = 121;
[int]$global:_SmallLineLength = 81;
#
$global:LoopzUI = [ordered]@{
  # Line definitions:
  #
  UnderscoreLine      = (New-Object String("_", $_LineLength));
  EqualsLine          = (New-Object String("=", $_LineLength));
  DotsLine            = (New-Object String(".", $_LineLength));
  LightDotsLine       = ((New-Object String(".", (($_LineLength - 1) / 2))).Replace(".", ". ") + ".");
  TildeLine           = (New-Object String("~", $_LineLength));

  SmallUnderscoreLine = (New-Object String("_", $_SmallLineLength));
  SmallEqualsLine     = (New-Object String("=", $_SmallLineLength));
  SmallDotsLine       = (New-Object String(".", $_SmallLineLength));
  SmallLightDotsLine  = ((New-Object String(".", (($_SmallLineLength - 1) / 2))).Replace(".", ". ") + ".");
  SmallTildeLine      = (New-Object String("~", $_SmallLineLength));
}

function Invoke-ForeachFsItem {
  <#
  .NAME
    Invoke-ForeachFsItem
 
  .SYNOPSIS
    Allows a custom defined scriptblock or function to be invoked for all file system
  objects delivered through the pipeline.
 
  .DESCRIPTION
    2 parameters sets are defined, one for invoking a named function (InvokeFunction) and
  the other (InvokeScriptBlock, the default) for invoking a script-block. An optional
  Summary script block can be specified which will be invoked at the end of the pipeline
  batch. The user should assemble the candidate items from the file system, be they files or
  directories typically using Get-ChildItem, or can be any other function that delivers
  file systems items via the PowerShell pipeline. For each item in the pipeline,
  Invoke-ForeachFsItem will invoke the script-block/function specified. Invoke-ForeachFsItem
  will deliver what ever is returned from the script-block/function, so the result of
  Invoke-ForeachFsItem can be piped to another command.
 
  .PARAMETER pipelineItem
    This is the pipeline object, so should not be specified explicitly and can represent
  a file object (System.IO.FileInfo) or a directory object (System.IO.DirectoryInfo).
 
  .PARAMETER Condition
    This is a predicate scriptblock, which is invoked with either a DirectoryInfo or
  FileInfo object presented as a result of invoking Get-ChildItem. It provides a filtering
  mechanism that is defined by the user to define which file system objects are selected
  for function/scriptblock invocation.
 
  .PARAMETER Block
    The script block to be invoked. The script block is invoked for each item in the
  pipeline that satisfy the Condition with the following positional parameters:
    * pipelineItem: the item from the pipeline
    * index: the 0 based index representing current pipeline item
    * PassThru: a hash table containing miscellaneous information gathered internally
  throughout the pipeline batch. This can be of use to the user, because it is the way
  the user can perform bi-directional communication between the invoked custom script block
  and client side logic.
    * trigger: a boolean value, useful for state changing idempotent operations. At the end
  of the batch, the state of the trigger indicates whether any of the items were actioned.
  When the script block is invoked, the trigger should indicate if the trigger was pulled for
  any of the items so far processed in the pipeline. This is the responsibility of the
  client's block implementation. The trigger is only of use for state changing operations
  and can be ignored otherwise.
   
  In addition to these fixed positional parameters, if the invoked scriptblock is defined
  with additional parameters, then these will also be passed in. In order to achieve this,
  the client has to provide excess parameters in BlockParam and these parameters must be
  defined as the same type and in the same order as the additional parameters in the
  scriptblock.
 
  .PARAMETER BlockParams
    Optional array containing the excess parameters to pass into the script block.
 
  .PARAMETER Functee
    String defining the function to be invoked. Works in a similar way to the Block parameter
  for script-blocks. The Function's base signature is as follows:
    "Underscore": (See pipelineItem described above)
    "Index": (See index described above)
    "PassThru": (See PathThru described above)
    "Trigger": (See trigger described above)
 
  .PARAMETER FuncteeParams
    Optional hash-table containing the named parameters which are splatted into the Functee
  function invoke. As it's a hash table, order is not significant.
 
  .PARAMETER PassThru
    A hash table containing miscellaneous information gathered internally throughout the
  pipeline batch. This can be of use to the user, because it is the way the user can perform
  bi-directional communication between the invoked custom script block and client side logic.
 
  .PARAMETER Summary
    A script-block that is invoked at the end of the pipeline batch. The script-block is
  invoked with the following positional parameters:
    * count: the number of items processed in the pipeline batch.
    * skipped: the number of items skipped in the pipeline batch. An item is skipped if
    it fails the defined condition or is not of the correct type (eg if its a directory
    but we have specified the -File flag). Also note that, if the script-block/function
    sets the Break flag causing further iteration to stop, then those subsequent items
    in the pipeline which have not been processed are not reflected in the skip count.
    * trigger: Flag set by the script-block/function, but should typically be used to
    indicate whether any of the items processed were actively updated/written in this batch.
    This helps in written idempotent operations that can be re-run without adverse
    consequences.
    * PassThru: (see PassThru previously described)
 
  .PARAMETER File
    Switch to indicate that the invoked function/script-block (invokee) is to handle FileInfo
  objects. Is mutually exclusive with the Directory switch. If neither switch is specified, then
  the invokee must be able to handle both therefore the Underscore parameter it defines must
  be declared as FileSystemInfo.
 
  .PARAMETER Directory
    Switch to indicate that the invoked function/script-block (invokee) is to handle Directory
  objects.
 
  .PARAMETER StartIndex
    Some calling functions interact with Invoke-ForeachFsItem in a way that may require that
  there is external control of the starting index. For example, Invoke-TraverseDirectory
  (which invokes Invoke-ForeachFsItem) handles the root Directory separately from its descendants
  and to ensure that the allocated indices are correct, the starting index should be set to 1,
  because the root Directory has already been allocated index 0, outside of the ForeachFsItem
  batch.
    Normal use of ForeachFsItem does not require StartIndex to be specified.
 
  .EXAMPLE 1
  Invoke a script-block to handle .txt file objects from the same directory (without -Recurse):
  (NB: first parameter is of type FileInfo, -File specified on Get-ChildItem and
  Invoke-ForeachFsItem. If Get-ChildItem is missing -File, then any Directory objects passed in
  are filtered out by Invoke-ForeachFsItem. If -File is missing from Invoke-ForeachFsItem, then
  the script-block's first parameter, must be a FileSystemInfo to handle both types)
 
    [scriptblock]$block = {
      param(
        [System.IO.FileInfo]$FileInfo,
        [int]$Index,
        [System.Collections.Hashtable]$PassThru,
        [boolean]$Trigger
      )
      ...
    }
 
    Get-ChildItem './Tests/Data/fefsi' -Recurse -Filter '*.txt' -File | `
      Invoke-ForeachFsItem -File -Block $block;
 
  .EXAMPLE 2
  Invoke a function with additional parameters to handle directory objects from multiple directories
  (with -Recurse):
 
  function invoke-Target {
    param(
      [System.IO.DirectoryInfo]$Underscore,
      [int]$Index,
      [System.Collections.Hashtable]$PassThru,
      [boolean]$Trigger,
      [string]$Format
    )
    ...
  }
 
  [System.Collections.Hashtable]$parameters = @{
    'Format'
  }
  Get-ChildItem './Tests/Data/fefsi' -Recurse -Directory | `
    Invoke-ForeachFsItem -Directory -Functee 'invoke-Target' -FuncteeParams $parameters
 
  .EXAMPLE 3
  Invoke a script-block to handle empty .txt file objects from the same directory (without -Recurse):
    [scriptblock]$block = {
      param(
        [System.IO.FileInfo]$FileInfo,
        [int]$Index,
        [System.Collections.Hashtable]$PassThru,
        [boolean]$Trigger
      )
      ...
    }
 
    [scriptblock]$fileIsEmpty = {
      param(
        [System.IO.FileInfo]$FileInfo
      )
      return (0 -eq $FileInfo.Length)
    }
 
    Get-ChildItem './Tests/Data/fefsi' -Recurse -Filter '*.txt' -File | Invoke-ForeachFsItem `
      -Block $block -File -condition $fileIsEmpty;
 
  .EXAMPLE 4
  Invoke a script-block only for directories whose name starts with "A" from the same
  directory (without -Recurse); Note the use of the LOOPZ function "Select-FsItem" in the
  directory include filter:
 
    [scriptblock]$block = {
      param(
        [System.IO.FileInfo]$FileInfo,
        [int]$Index,
        [System.Collections.Hashtable]$PassThru,
        [boolean]$Trigger
      )
      ...
    }
 
  [scriptblock]$filterDirectories = {
    [OutputType([boolean])]
    param(
      [System.IO.DirectoryInfo]$directoryInfo
    )
    Select-FsItem -Name $directoryInfo.Name -Includes 'A*';
  }
 
    Get-ChildItem './Tests/Data/fefsi' -Directory | Invoke-ForeachFsItem `
      -Block $block -Directory -DirectoryIncludes $filterDirectories;
  #>

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
  [CmdletBinding(DefaultParameterSetName = 'InvokeScriptBlock')]
  [Alias('ife', 'Foreach-FsItem')]
  param(
    [Parameter(ParameterSetName = 'InvokeScriptBlock', Mandatory, ValueFromPipeline = $true)]
    [Parameter(ParameterSetName = 'InvokeFunction', Mandatory, ValueFromPipeline = $true)]
    [System.IO.FileSystemInfo]$pipelineItem,

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(ParameterSetName = 'InvokeFunction')]
    [scriptblock]$Condition = ( { return $true; }),

    [Parameter(ParameterSetName = 'InvokeScriptBlock', Mandatory)]
    [scriptblock]$Block,

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [ValidateScript( { $_ -is [Array] })]
    $BlockParams = @(),

    [Parameter(ParameterSetName = 'InvokeFunction', Mandatory)]
    [ValidateScript( { -not([string]::IsNullOrEmpty($_)); })]
    [string]$Functee,

    [Parameter(ParameterSetName = 'InvokeFunction')]
    [System.Collections.Hashtable]$FuncteeParams = @{},

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(ParameterSetName = 'InvokeFunction')]
    [System.Collections.Hashtable]$PassThru = @{},

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(ParameterSetName = 'InvokeFunction')]
    [scriptblock]$Summary = ( {
        param(
          [int]$count,
          [int]$skipped,
          [boolean]$trigger,
          [System.Collections.Hashtable]$passThru
        )
      }),

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(ParameterSetName = 'InvokeFunction')]
    [ValidateScript( { -not($PSBoundParameters.ContainsKey('Directory')) })]
    [switch]$File,

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(ParameterSetName = 'InvokeFunction')]
    [ValidateScript( { -not($PSBoundParameters.ContainsKey('File')) })]
    [switch]$Directory,

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(ParameterSetName = 'InvokeFunction')]
    [int]$StartIndex = 0
  ) # param

  begin {
    [boolean]$manageIndex = -not($PassThru.ContainsKey('LOOPZ.FOREACH.INDEX'));
    [int]$index = $manageIndex ? $StartIndex : $PassThru['LOOPZ.FOREACH.INDEX'];
    [int]$skipped = 0;
    [boolean]$broken = $false;
    [boolean]$trigger = $PassThru.ContainsKey('LOOPZ.FOREACH.TRIGGER');
  }

  process {
    [boolean]$itemIsDirectory = ($pipelineItem.Attributes -band
      [System.IO.FileAttributes]::Directory) -eq [System.IO.FileAttributes]::Directory;

    [boolean]$acceptAll = -not($File.ToBool()) -and -not($Directory.ToBool());

    if (-not($broken)) {
      if ( $acceptAll -or ($Directory.ToBool() -and $itemIsDirectory) -or
        ($File.ToBool() -and -not($itemIsDirectory)) ) {
        if ($Condition.Invoke($pipelineItem)) {
          $result = $null;

          try {
            if ('InvokeScriptBlock' -eq $PSCmdlet.ParameterSetName) {
              $positional = @($pipelineItem, $index, $PassThru, $trigger);

              if ($BlockParams.Length -gt 0) {
                $BlockParams | ForEach-Object {
                  $positional += $_;
                }
              }

              $result = Invoke-Command -ScriptBlock $Block -ArgumentList $positional;
            }
            elseif ('InvokeFunction' -eq $PSCmdlet.ParameterSetName) {
              [System.Collections.Hashtable]$parameters = $FuncteeParams.Clone();

              $parameters['Underscore'] = $pipelineItem;
              $parameters['Index'] = $index;
              $parameters['PassThru'] = $PassThru;
              $parameters['Trigger'] = $trigger;

              $result = & $Functee @parameters;
            }
          }
          catch {
            Write-Error "Foreach Error: ($_), for item: '$($pipelineItem.Name)'";
          }
          finally {
            if ($manageIndex) {
              $index++;
            }
            else {
              $index = $PassThru['LOOPZ.FOREACH.INDEX'];
            }

            if ($result) {
              if ($result.psobject.properties.match('Trigger') -and $result.Trigger) {
                $PassThru['LOOPZ.FOREACH.TRIGGER'] = $true;
                $trigger = $true;
              }

              if ($result.psobject.properties.match('Break') -and $result.Break) {
                $broken = $true;
              }

              if ($result.psobject.properties.match('Product') -and $result.Product) {
                $result.Product;
              }
            }
          }
        }
        else {
          # IDEA! We could allow the user to provide an extra script block which we
          # invoke for skipped items and set a string containing the reason why it was
          # skipped.
          $null = $skipped++;
        }
      }
      else {
        $null = $skipped++;
      }
    }
    else {
      $null = $skipped++;
    }
  }

  end {
    $PassThru['LOOPZ.FOREACH.TRIGGER'] = $trigger;
    if ($manageIndex) {
      $Summary.Invoke($index, $skipped, $trigger, $PassThru);
    }
  }
} # Invoke-ForeachFsItem
function Invoke-MirrorDirectoryTree {
  <#
  .NAME
    Invoke-MirrorDirectoryTree
 
  .SYNOPSIS
    Mirrors a directory tree to a new location, invoking a custom defined scriptblock
  or function as it goes.
 
  .DESCRIPTION
    Copies a source directory tree to a new location applying custom functionality for each
  directory. 2 parameters set are defined, one for invoking a named function (InvokeFunction) and
  the other (InvokeScriptBlock, the default) for invoking a scriptblock. An optional
  Summary script block can be specified which will be invoked at the end of the mirroring
  batch.
 
  .PARAMETER Path
    The source Path denoting the root of the directory tree to be mirrored.
 
  .PARAMETER DestinationPath
    The destination Path denoting the root of the directory tree where the source tree
  will be mirrored to.
 
  .PARAMETER DirectoryIncludes
    An array containing a list of filters, each must contain a wild-card ('*'). If a
  particular filter does not contain a wild-card, then it will be ignored. If the directory
  matches any of the filters in the list, it will be mirrored in the destination tree.
  If DirectoryIncludes contains just a single element which is the empty string, this means
  that nothing is included (rather than everything being included).
 
  .PARAMETER DirectoryExcludes
    An array containing a list of filters, each must contain a wild-card ('*'). If a
  particular filter does not contain a wild-card, then it will be ignored. If the directory
  matches any of the filters in the list, it will NOT be mirrored in the destination tree.
  Any match in the DirectoryExcludes overrides a match in DirectoryIncludes, so a directory
  that is matched in Include, can be excluded by the Exclude.
 
  .PARAMETER FileIncludes
    An array containing a list of filters, each may contain a wild-card ('*'). If a
  particular filter does not contain a wild-card, then it will be treated as a file suffix.
  If the file in the source tree matches any of the filters in the list, it will be mirrored
  in the destination tree. If FileIncludes contains just a single element which is the empty
  string, this means that nothing is included (rather than everything being included).
 
  .PARAMETER FileExcludes
    An array containing a list of filters, each may contain a wild-card ('*'). If a
  particular filter does not contain a wild-card, then it will be treated as a file suffix.
  If the file in the source tree matches any of the filters in the list, it will NOT be
  mirrored in the destination tree. Any match in the FileExcludes overrides a match in
  FileIncludes, so a file that is matched in Include, can be excluded by the Exclude.
 
  .PARAMETER PassThru
    A hash table containing miscellaneous information gathered internally
  throughout the pipeline batch. This can be of use to the user, because it is the way
  the user can perform bi-directional communication between the invoked custom script block
  and client side logic.
 
  .PARAMETER Block
    The script block to be invoked. The script block is invoked for each directory in the
  source directory tree that satisfy the specified Directory Include/Exclude filters with
  the following positional parameters:
    * underscore: the DirectoryInfo object representing the directory in the source tree
    * index: the 0 based index representing current directory in the source tree
    * PassThru object: a hash table containing miscellaneous information gathered internally
    throughout the mirroring batch. This can be of use to the user, because it is the way
    the user can perform bi-directional communication between the invoked custom script block
    and client side logic.
    * trigger: a boolean value, useful for state changing idempotent operations. At the end
    of the batch, the state of the trigger indicates whether any of the items were actioned.
    When the script block is invoked, the trigger should indicate if the trigger was pulled for
    any of the items so far processed in the batch. This is the responsibility of the
    client's script-block/function implementation.
   
  In addition to these fixed positional parameters, if the invoked scriptblock is defined
  with additional parameters, then these will also be passed in. In order to achieve this,
  the client has to provide excess parameters in BlockParams and these parameters must be
  defined as the same type and in the same order as the additional parameters in the
  script-block.
 
  The destination DirectoryInfo object can be accessed via the PassThru denoted by
  the 'LOOPZ.MIRROR.DESTINATION' entry.
 
  .PARAMETER BlockParams
    Optional array containing the excess parameters to pass into the script-block/function.
 
  .PARAMETER Functee
    String defining the function to be invoked. Works in a similar way to the Block parameter
  for script-blocks. The Function's base signature is as follows:
    "Underscore": (See underscore described above)
    "Index": (See index described above)
    "PassThru": (See PathThru described above)
    "Trigger": (See trigger described above)
 
  The destination DirectoryInfo object can be accessed via the PassThru denoted by
  the 'LOOPZ.MIRROR.DESTINATION' entry.
 
  .PARAMETER FuncteeParams
    Optional hash-table containing the named parameters which are splatted into the Functee
  function invoke. As it's a hash table, order is not significant.
 
  .PARAMETER CreateDirs
    Switch parameter indicates that directories should be created in the destination tree. If
  not set, then Invoke-MirrorDirectoryTree turns into a function that traverses the source
  directory invoking the function/script-block for matching directories.
 
  .PARAMETER CopyFiles
    Switch parameter that indicates that files matching the specified filters should be copied
 
  .PARAMETER Hoist
    Switch parameter. Without Hoist being specified, the filters can prove to be too restrictive
  on matching against directories. If a directory does not match the filters then none of its
  descendants will be considered to be mirrored in the destination tree. When Hoist is specified
  then a descendant directory that does match the filters will be mirrored even though any of
  its ancestors may not match the filters.
 
  .PARAMETER Summary
    A script-block that is invoked at the end of the mirroring batch. The script-block is
  invoked with the following positional parameters:
    * count: the number of items processed in the mirroring batch.
    * skipped: the number of items skipped in the mirroring batch. An item is skipped if
    it fails the defined condition or is not of the correct type (eg if its a directory
    but we have specified the -File flag).
    * trigger: Flag set by the script-block/function, but should typically be used to
    indicate whether any of the items processed were actively updated/written in this batch.
    This helps in written idempotent operations that can be re-run without adverse
    consequences.
    * PassThru: (see PassThru previously described)
 
  .EXAMPLE 1
    Invoke a named function for every directory in the source tree and mirror every
  directory in the destination tree. The invoked function has an extra parameter in it's
  signature, so the extra parameters must be passed in via FuncteeParams (the standard
  signature being the first 4 parameters shown.)
   
  function Test-Mirror {
    param(
      [System.IO.DirectoryInfo]$Underscore,
      [int]$Index,
      [System.Collections.Hashtable]$PassThru,
      [boolean]$Trigger,
      [string]$Format
    )
    ...
  }
 
  [System.Collections.Hashtable]$parameters = @{
    'Format' = '---- {0} ----';
  }
  Invoke-MirrorDirectoryTree -Path './Tests/Data/fefsi' `
    -DestinationPath './Tests/Data/mirror' -CreateDirs `
    -Functee 'Test-Mirror' -FuncteeParams $parameters;
 
  .EXAMPLE 2
  Invoke a script-block for every directory in the source tree and copy all files
 
  Invoke-MirrorDirectoryTree -Path './Tests/Data/fefsi' `
    -DestinationPath './Tests/Data/mirror' -CreateDirs -CopyFiles -block {
      param(
        [System.IO.DirectoryInfo]$Underscore,
        [int]$Index,
        [System.Collections.Hashtable]$PassThru,
        [boolean]$Trigger
      )
      ...
    };
 
  .EXAMPLE 3
  Mirror a directory tree, including only directories beginning with A (filter A*)
 
  Invoke-MirrorDirectoryTree -Path './Tests/Data/fefsi' -DestinationPath './Tests/Data/mirror' `
    -DirectoryIncludes @('A*')
 
  Note the possible issue with this example is that any descendants named A... which are located
  under an ancestor which is not named A..., will not be mirrored;
 
  eg './Tests/Data/fefsi/Audio/mp3/A/Amorphous Androgynous', even though "Audio", "A" and
  "Amorphous Androgynous" clearly match the A* filter, they will not be mirrored because
  the "mp3" directory, would be filtered out.
  See the following example for a resolution.
 
  .EXAMPLE 4
  Mirror a directory tree, including only directories beginning with A (filter A*) regardless of
  the matching of intermediate ancestors (specifying -Hoist flag resolves the possible
  issue in the previous example)
 
  Invoke-MirrorDirectoryTree -Path './Tests/Data/fefsi' -DestinationPath './Tests/Data/mirror' `
    -DirectoryIncludes @('A*') -CreateDirs -CopyFiles -Hoist
 
  Note that the directory filter must include a wild-card, otherwise it will be ignored. So a
  directory include of @('A'), is problematic, because A is not a valid directory filter so its
  ignored and there are no remaining filters that are able to include any directory, so no
  directory passes the filter.
 
  .EXAMPLE 5
  Mirror a directory tree, including files with either .flac or .wav suffix
 
  Invoke-MirrorDirectoryTree -Path './Tests/Data/fefsi' -DestinationPath './Tests/Data/mirror' `
    -FileIncludes @('flac', '*.wav') -CreateDirs -CopyFiles -Hoist
 
  Note that for files, a filter may or may not contain a wild-card. If the wild-card is missing
  then it is automatically treated as a file suffix; so 'flac' means '*.flac'.
 
  .EXAMPLE 6
  Mirror a directory tree copying over just flac files
 
  [scriptblock]$summary = {
    param(
      [int]$_count,
      [int]$_skipped,
      [boolean]$_triggered,
      [System.Collections.Hashtable]$_passThru
    )
    ...
  }
 
  Invoke-MirrorDirectoryTree -Path './Tests/Data/fefsi' -DestinationPath './Tests/Data/mirror' `
    -FileIncludes @('flac') -CopyFiles -Hoist -Summary $summary
 
  Note that -CreateDirs is missing which means directories will not be mirrored by default. They
  are only mirrored as part of the process of copying over flac files, so in the end the
  resultant mirror directory tree will contain directories that include flac files.
  #>


  [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'InvokeScriptBlock')]
  [Alias('imdt', 'Mirror-Directory')]
  param
  (
    [Parameter(Mandatory, ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(Mandatory, ParameterSetName = 'InvokeFunction')]
    [ValidateScript( { Test-path -Path $_; })]
    [String]$Path,

    [Parameter(Mandatory, ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(Mandatory, ParameterSetName = 'InvokeFunction')]
    [String]$DestinationPath,

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(ParameterSetName = 'InvokeFunction')]
    [String[]]$DirectoryIncludes = @('*'),

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(ParameterSetName = 'InvokeFunction')]
    [String[]]$DirectoryExcludes = @(),

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(ParameterSetName = 'InvokeFunction')]
    [String[]]$FileIncludes = @('*'),

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(ParameterSetName = 'InvokeFunction')]
    [String[]]$FileExcludes = @(),

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(ParameterSetName = 'InvokeFunction')]
    [System.Collections.Hashtable]$PassThru = @{},

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [scriptblock]$Block = ( {
        [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
        param(
          [System.IO.DirectoryInfo]$underscore,
          [int]$index,
          [System.Collections.Hashtable]$passThru,
          [boolean]$trigger
        )
      } ),

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [ValidateScript( { $_ -is [Array] })]
    $BlockParams = @(),

    [Parameter(ParameterSetName = 'InvokeFunction', Mandatory)]
    [ValidateScript( { -not([string]::IsNullOrEmpty($_)); })]
    [string]$Functee,

    [Parameter(ParameterSetName = 'InvokeFunction')]
    [ValidateScript( { $_.Count -gt 0; })]
    [System.Collections.Hashtable]$FuncteeParams = @{},

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(ParameterSetName = 'InvokeFunction')]
    [switch]$CreateDirs,

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(ParameterSetName = 'InvokeFunction')]
    [switch]$CopyFiles,

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(ParameterSetName = 'InvokeFunction')]
    [switch]$Hoist,

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(ParameterSetName = 'InvokeFunction')]
    [scriptblock]$Summary = ( {
        [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
        param(
          [int]$index,
          [int]$skipped,
          [boolean]$trigger,
          [System.Collections.Hashtable]$_passThru
        )
      })
  ) # param

  # ================================================================== [doMirrorBlock] ===
  #
  [scriptblock]$doMirrorBlock = {
    param(
      [Parameter(Mandatory)]
      [System.IO.DirectoryInfo]$_underscore,

      [Parameter(Mandatory)]
      [int]$_index,

      [Parameter(Mandatory)]
      [System.Collections.Hashtable]$_passThru,

      [Parameter(Mandatory)]
      [boolean]$_trigger
    )

    # Write-Host "[+] >>> doMirrorBlock: $($_underscore.Name)";

    [string]$rootSource = $_passThru['LOOPZ.MIRROR.ROOT-SOURCE'];
    [string]$rootDestination = $_passThru['LOOPZ.MIRROR.ROOT-DESTINATION'];

    $sourceDirectoryFullName = $_underscore.FullName;

    # sourceDirectoryFullName must end with directory separator
    #
    if (-not($sourceDirectoryFullName.EndsWith([System.IO.Path]::DirectorySeparatorChar))) {
      $sourceDirectoryFullName += [System.IO.Path]::DirectorySeparatorChar;
    }

    $destinationBranch = edit-RemoveSingleSubString -Target $sourceDirectoryFullName -Subtract $rootSource;
    $destinationDirectory = Join-Path -Path $rootDestination -ChildPath $destinationBranch;

    [boolean]$whatIf = $_passThru.ContainsKey('LOOPZ.MIRROR.WHAT-IF') -and ($_passThru['LOOPZ.MIRROR.WHAT-IF']);
    Write-Debug "[+] >>> doMirrorBlock: destinationDirectory: '$destinationDirectory'";

    if ($CreateDirs.ToBool()) {
      Write-Debug " [-] Creating destination branch directory: '$destinationBranch'";

      $destinationInfo = (Test-Path -Path $destinationDirectory) `
        ? (Get-Item -Path $destinationDirectory) `
        : (New-Item -ItemType 'Directory' -Path $destinationDirectory -WhatIf:$whatIf);
    }
    else {
      Write-Debug " [-] Creating destination branch directory INFO obj: '$destinationBranch'";
      $destinationInfo = New-Object -TypeName System.IO.DirectoryInfo ($destinationDirectory);
    }

    if ($CopyFiles.ToBool()) {
      Write-Debug " [-] Creating files for branch directory: '$destinationBranch'";

      # To use the include/exclude parameters on Copy-Item, the Path specified
      # must end in /*. We only need to add the star though because we added the /
      # previously.
      #
      [string]$sourceDirectoryWithWildCard = $sourceDirectoryFullName + '*';

      [string[]]$adjustedFileIncludes = $FileIncludes | ForEach-Object {
        $_.Contains('*') ? $_ : "*.$_".Replace('..', '.');
      }

      [string[]]$adjustedFileExcludes = $FileExcludes | ForEach-Object {
        $_.Contains('*') ? $_ : "*.$_".Replace('..', '.');
      }

      # Ensure that the destination directory exists, but only if there are
      # files to copy over which pass the include/exclude filters. This is
      # required in the case where CreateDirs has not been specified.
      #
      if (Get-ChildItem $sourceDirectoryWithWildCard `
          -Include $adjustedFileIncludes -Exclude $adjustedFileExcludes) {
        if (-not(Test-Path -Path $destinationDirectory)) {
          New-Item -ItemType 'Directory' -Path $destinationDirectory -WhatIf:$whatIf
        }
      }

      Copy-Item -Path $sourceDirectoryWithWildCard `
        -Include $adjustedFileIncludes -Exclude $adjustedFileExcludes `
        -Destination $destinationDirectory -WhatIf:$whatIf;
    }

    # To be consistent with Invoke-ForeachFsItem, the user function/block is invoked
    # with the source directory info. The destination for this mirror operation is
    # returned via 'LOOPZ.MIRROR.DESTINATION' within the PassThru.
    #
    $_passThru['LOOPZ.MIRROR.DESTINATION'] = $destinationInfo;

    $invokee = $_passThru['LOOPZ.MIRROR.INVOKEE'];

    try {
      if ($invokee -is [scriptblock]) {
        $positional = @($_underscore, $_index, $_passThru, $_trigger);

        if ($_passThru.ContainsKey('LOOPZ.MIRROR.INVOKEE.PARAMS')) {
          $_passThru['LOOPZ.MIRROR.INVOKEE.PARAMS'] | ForEach-Object {
            $positional += $_;
          }
        }

        $invokee.Invoke($positional);
      }
      elseif ($invokee -is [string]) {
        [System.Collections.Hashtable]$parameters = $_passThru.ContainsKey('LOOPZ.MIRROR.INVOKEE.PARAMS') `
          ? $_passThru['LOOPZ.MIRROR.INVOKEE.PARAMS'] : @{};
        $parameters['Underscore'] = $_underscore;
        $parameters['Index'] = $_index;
        $parameters['PassThru'] = $_passThru;
        $parameters['Trigger'] = $_trigger;

        & $invokee @parameters;
      }
      else {
        Write-Warning "User defined function/block not valid, not invoking.";
      }
    }
    catch {
      Write-Error "function invoke error doMirrorBlock: error ($_) occurred for '$destinationBranch'";
    }

    @{ Product = $destinationInfo }
  } #doMirrorBlock

  # ===================================================== [Invoke-MirrorDirectoryTree] ===

  [string]$resolvedSourcePath = Convert-Path $Path;
  [string]$resolvedDestinationPath = Convert-Path $DestinationPath;

  $PassThru['LOOPZ.MIRROR.ROOT-SOURCE'] = $resolvedSourcePath;
  $PassThru['LOOPZ.MIRROR.ROOT-DESTINATION'] = $resolvedDestinationPath;

  if ('InvokeScriptBlock' -eq $PSCmdlet.ParameterSetName) {
    $PassThru['LOOPZ.MIRROR.INVOKEE'] = $Block;

    if ($BlockParams.Count -gt 0) {
      $PassThru['LOOPZ.MIRROR.INVOKEE.PARAMS'] = $BlockParams;
    }
  }
  else {
    $PassThru['LOOPZ.MIRROR.INVOKEE'] = $Functee;

    if ($FuncteeParams.Count -gt 0) {
      $PassThru['LOOPZ.MIRROR.INVOKEE.PARAMS'] = $FuncteeParams.Clone();
    }
  }

  if ($PSBoundParameters.ContainsKey('WhatIf') -and ($true -eq $PSBoundParameters['WhatIf'])) {
    $PassThru['LOOPZ.MIRROR.WHAT-IF'] = $true;
  }

  [scriptblock]$filterDirectories = {
    [OutputType([boolean])]
    param(
      [System.IO.DirectoryInfo]$directoryInfo
    )
    Select-FsItem -Name $directoryInfo.Name `
      -Includes $DirectoryIncludes -Excludes $DirectoryExcludes;
  }

  Invoke-TraverseDirectory -Path $resolvedSourcePath `
    -Block $doMirrorBlock -PassThru $PassThru -Summary $Summary `
    -Condition $filterDirectories -Hoist:$Hoist;
} # Invoke-MirrorDirectoryTree

function Invoke-TraverseDirectory {
  <#
  .NAME
    Invoke-TraverseDirectory
 
  .SYNOPSIS
    Traverses a directory tree invoking a custom defined script-block or named function
  as it goes.
 
  .DESCRIPTION
    Navigates a directory tree applying custom functionality for each directory. A Condition
  script-block can be applied for conditional functionality. 2 parameters set are defined, one
  for invoking a named function (InvokeFunction) and the other (InvokeScriptBlock, the default)
  for invoking a scriptblock. An optional Summary script block can be specified which will be
  invoked at the end of the traversal batch.
 
  .PARAMETER Path
    The source Path denoting the root of the directory tree to be traversed.
 
  .PARAMETER Condition
    This is a predicate scriptblock, which is invoked with a DirectoryInfo object presented
  as a result of invoking Get-ChildItem. It provides a filtering mechanism that is defined
  by the user to define which directories are selected for function/scriptblock invocation.
 
  .PARAMETER PassThru
    A hash table containing miscellaneous information gathered internally throughout the
  traversal batch. This can be of use to the user, because it is the way the user can perform
  bi-directional communication between the invoked custom script block and client side logic.
 
  .PARAMETER Block
    The script block to be invoked. The script block is invoked for each directory in the
  source directory tree that satisfy the specified Condition predicate with
  the following positional parameters:
    * underscore: the DirectoryInfo object representing the directory in the source tree
    * index: the 0 based index representing current directory in the source tree
    * PassThru object: a hash table containing miscellaneous information gathered internally
    throughout the mirroring batch. This can be of use to the user, because it is the way
    the user can perform bi-directional communication between the invoked custom script block
    and client side logic.
    * trigger: a boolean value, useful for state changing idempotent operations. At the end
    of the batch, the state of the trigger indicates whether any of the items were actioned.
    When the script block is invoked, the trigger should indicate if the trigger was pulled for
    any of the items so far processed in the batch. This is the responsibility of the
    client's script-block/function implementation.
   
  In addition to these fixed positional parameters, if the invoked scriptblock is defined
  with additional parameters, then these will also be passed in. In order to achieve this,
  the client has to provide excess parameters in BlockParams and these parameters must be
  defined as the same type and in the same order as the additional parameters in the
  script-block.
 
  .PARAMETER BlockParams
    Optional array containing the excess parameters to pass into the script-block.
 
  .PARAMETER Functee
    String defining the function to be invoked. Works in a similar way to the Block parameter
  for script-blocks. The Function's base signature is as follows:
    "Underscore": (See underscore described above)
    "Index": (See index described above)
    "PassThru": (See PathThru described above)
    "Trigger": (See trigger described above)
 
  The destination DirectoryInfo object can be accessed via the PassThru denoted by
  the 'LOOPZ.MIRROR.DESTINATION' entry.
 
  .PARAMETER FuncteeParams
    Optional hash-table containing the named parameters which are splatted into the Functee
  function invoke. As it's a hash table, order is not significant.
 
  .PARAMETER Summary
    A script-block that is invoked at the end of the traversal batch. The script-block is
  invoked with the following positional parameters:
    * count: the number of items processed in the mirroring batch.
    * skipped: the number of items skipped in the mirroring batch. An item is skipped if
    it fails the defined condition or is not of the correct type (eg if its a directory
    but we have specified the -File flag).
    * trigger: Flag set by the script-block/function, but should typically be used to
    indicate whether any of the items processed were actively updated/written in this batch.
    This helps in written idempotent operations that can be re-run without adverse
    consequences.
    * PassThru: (see PassThru previously described)
 
  .PARAMETER Hoist
    Switch parameter. Without Hoist being specified, the Condition can prove to be too restrictive
  on matching against directories. If a directory does not match the Condition then none of its
  descendants will be considered to be traversed. When Hoist is specified then a descendant directory
  that does match the Condition will be traversed even though any of its ancestors may not match the
  same Condition.
 
  .EXAMPLE 1
    Invoke a script-block for every directory in the source tree.
 
    [scriptblock]$block = {
      param(
        $underscore,
        [int]$index,
        [System.Collections.Hashtable]$passThru,
        [boolean]$trigger
      )
      ...
    }
 
  Invoke-TraverseDirectory -Path './Tests/Data/fefsi' -Block $block
 
  .EXAMPLE 2
    Invoke a named function with extra parameters for every directory in the source tree.
 
  function Test-Traverse {
    param(
      [System.IO.DirectoryInfo]$Underscore,
      [int]$Index,
      [System.Collections.Hashtable]$PassThru,
      [boolean]$Trigger,
      [string]$Format
    )
    ...
  }
  [System.Collections.Hashtable]$parameters = @{
    'Format' = "=== {0} ===";
  }
 
  Invoke-TraverseDirectory -Path './Tests/Data/fefsi' `
    -Functee 'Test-Traverse' -FuncteeParams $parameters;
 
  .EXAMPLE 3
  Invoke a named function, including only directories beginning with A (filter A*)
 
  function Test-Traverse {
    param(
      [System.IO.DirectoryInfo]$Underscore,
      [int]$Index,
      [System.Collections.Hashtable]$PassThru,
      [boolean]$Trigger
    )
    ...
  }
 
  [scriptblock]$filterDirectories = {
    [OutputType([boolean])]
    param(
      [System.IO.DirectoryInfo]$directoryInfo
    )
 
    Select-FsItem -Name $directoryInfo.Name -Includes @('A*');
  }
 
  Invoke-TraverseDirectory -Path './Tests/Data/fefsi' -Functee 'Test-Traverse' `
    -Condition $filterDirectories;
 
  Note the possible issue with this example is that any descendants named A... which are located
  under an ancestor which is not named A..., will not be processed by the provided function
 
  .EXAMPLE 4
  Mirror a directory tree, including only directories beginning with A (filter A*) regardless of
  the matching of intermediate ancestors (specifying -Hoist flag resolves the possible
  issue in the previous example)
 
  function Test-Traverse {
    param(
      [System.IO.DirectoryInfo]$Underscore,
      [int]$Index,
      [System.Collections.Hashtable]$PassThru,
      [boolean]$Trigger
    )
    ...
  }
 
  [scriptblock]$filterDirectories = {
    [OutputType([boolean])]
    param(
      [System.IO.DirectoryInfo]$directoryInfo
    )
 
    Select-FsItem -Name $directoryInfo.Name -Includes @('A*');
  }
 
  Invoke-TraverseDirectory -Path './Tests/Data/fefsi' -Functee 'Test-Traverse' `
    -Condition $filterDirectories -Hoist;
 
  Note that the directory filter must include a wild-card, otherwise it will be ignored. So a
  directory include of @('A'), is problematic, because A is not a valid directory filter so its
  ignored and there are no remaining filters that are able to include any directory, so no
  directory passes the filter.
 
  #>

  [CmdletBinding(DefaultParameterSetName = 'InvokeScriptBlock')]
  [Alias('itd', 'Traverse-Directory')]
  param
  (
    [Parameter(ParameterSetName = 'InvokeScriptBlock', Mandatory)]
    [Parameter(ParameterSetName = 'InvokeFunction', Mandatory)]
    [ValidateScript( { Test-path -Path $_ })]
    [String]$Path,

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(ParameterSetName = 'InvokeFunction')]
    [ValidateScript( { -not($_ -eq $null) })]
    [scriptblock]$Condition = (
      {
        [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
        param([System.IO.DirectoryInfo]$directoryInfo)
        return $true;
      }
    ),

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(ParameterSetName = 'InvokeFunction')]
    [System.Collections.Hashtable]$PassThru = @{},

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [ValidateScript( { -not($_ -eq $null) })]
    [scriptblock]$Block,

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [ValidateScript( { $_ -is [Array] })]
    $BlockParams = @(),

    [Parameter(ParameterSetName = 'InvokeFunction', Mandatory)]
    [ValidateScript( { -not([string]::IsNullOrEmpty($_)); })]
    [string]$Functee,

    [Parameter(ParameterSetName = 'InvokeFunction')]
    [ValidateScript( { $_.Length -gt 0; })]
    [System.Collections.Hashtable]$FuncteeParams = @{},

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(ParameterSetName = 'InvokeFunction')]
    [scriptblock]$Summary = ( {
        [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
        param(
          [int]$Count,
          [int]$Skipped,
          [boolean]$Triggered,
          [System.Collections.Hashtable]$PassThru
        )
      }),

    [Parameter(ParameterSetName = 'InvokeScriptBlock')]
    [Parameter(ParameterSetName = 'InvokeFunction')]
    [switch]$Hoist
  ) # param

  # ======================================================= [recurseTraverseDirectory] ===
  #
  [scriptblock]$recurseTraverseDirectory = { # Invoked by adapter
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    param(
      [Parameter(Position = 0, Mandatory)]
      [System.IO.DirectoryInfo]$directoryInfo,

      [Parameter(Position = 1)]
      [ValidateScript( { -not($_ -eq $null) })]
      [scriptblock]$condition,

      [Parameter(Position = 2, Mandatory)]
      [ValidateScript( { -not($_ -eq $null) })]
      [System.Collections.Hashtable]$passThru,

      [Parameter(Position = 3)]
      [ValidateScript( { ($_ -is [scriptblock]) -or ($_ -is [string]) })]
      $invokee, # (scriptblock or function name; hence un-typed parameter)

      [Parameter(Position = 4)]
      [boolean]$trigger
    )

    $result = $null;
    $index = $passThru['LOOPZ.FOREACH.INDEX'];

    try {
      # This is the invoke, for the current directory
      #
      if ($invokee -is [scriptblock]) {
        $positional = @($directoryInfo, $index, $passThru, $trigger);

        if ($passThru.ContainsKey('LOOPZ.TRAVERSE.INVOKEE.PARAMS') -and
          ($passThru['LOOPZ.TRAVERSE.INVOKEE.PARAMS'] -gt 0)) {
          $passThru['LOOPZ.TRAVERSE.INVOKEE.PARAMS'] | ForEach-Object {
            $positional += $_;
          }
        }
        $result = $invokee.Invoke($positional);
      }
      else {
        [System.Collections.Hashtable]$parameters = $passThru.ContainsKey('LOOPZ.TRAVERSE.INVOKEE.PARAMS') `
          ? $passThru['LOOPZ.TRAVERSE.INVOKEE.PARAMS'] : @{};

        # These are directory specific overwrites. The custom parameters
        # will still be present
        #
        $parameters['Underscore'] = $directoryInfo;
        $parameters['Index'] = $index;
        $parameters['PassThru'] = $passThru;
        $parameters['Trigger'] = $trigger;

        $result = & $invokee @parameters;
      }
    }
    catch {
      Write-Error "recurseTraverseDirectory Error: ($_), for item: '$($directoryInfo.Name)'";
    }
    finally {
      $passThru['LOOPZ.FOREACH.INDEX']++;
    }

    [string]$fullName = $directoryInfo.FullName;
    [System.IO.DirectoryInfo[]]$directoryInfos = Get-ChildItem -Path $fullName `
      -Directory | Where-Object { $condition.Invoke($_) };

    [scriptblock]$adapter = $PassThru['LOOPZ.TRAVERSE.ADAPTOR'];

    if ($directoryInfos) {
      # adapter is always a script block, this has nothing to do with the invokee,
      # which may be a script block or a named function(functee)
      #
      $directoryInfos | Invoke-ForeachFsItem -Directory -Block $adapter `
        -PassThru $PassThru -Condition $condition -Summary $Summary;
    }

    return $result;
  } # recurseTraverseDirectory

  # ======================================================================== [adapter] ===
  #
  [scriptblock]$adapter = {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    param(
      [Parameter(Mandatory)]
      [System.IO.DirectoryInfo]$_underscore,

      [Parameter(Mandatory)]
      [int]$_index,

      [Parameter(Mandatory)]
      [System.Collections.Hashtable]$_passThru,

      [Parameter(Mandatory)]
      [boolean]$_trigger
    )

    [scriptblock]$adapted = $_passThru['LOOPZ.TRAVERSE.ADAPTED'];

    $adapted.Invoke(
      $_underscore,
      $_passThru['LOOPZ.TRAVERSE.CONDITION'],
      $_passThru,
      $PassThru['LOOPZ.TRAVERSE.INVOKEE'],
      $_trigger
    );
  } # adapter

  # ======================================================= [Invoke-TraverseDirectory] ===

  # Handle top level directory, before recursing through child directories
  #

  [System.IO.DirectoryInfo]$directory = Get-Item -Path $Path;

  [boolean]$itemIsDirectory = ($directory.Attributes -band
    [System.IO.FileAttributes]::Directory) -eq [System.IO.FileAttributes]::Directory;

  if ($itemIsDirectory) {
    [boolean]$trigger = $PassThru.ContainsKey('LOOPZ.FOREACH.TRIGGER');
    [boolean]$broken = $false;

    # The index of the top level directory is always 0
    #
    [int]$index = 0;

    if ('InvokeFunction' -eq $PSCmdlet.ParameterSetName) {
      # set-up custom parameters
      #
      [System.Collections.Hashtable]$parameters = $FuncteeParams.Clone();
      $parameters['Underscore'] = $directory;
      $parameters['Index'] = $index;
      $parameters['PassThru'] = $PassThru;
      $parameters['Trigger'] = $trigger;
      $PassThru['LOOPZ.TRAVERSE.INVOKEE.PARAMS'] = $parameters;
    }
    elseif ('InvokeScriptBlock' -eq $PSCmdlet.ParameterSetName) {
      $positional = @($directory, $index, $PassThru, $trigger);

      if ($BlockParams.Count -gt 0) {
        $BlockParams | Foreach-Object {
          $positional += $_;
        }
      }

      # Note, for the positional parameters, we can only pass in the additional
      # custom parameters provided by the client here via the PassThru otherwise
      # we could accidentally build up the array of positional parameters with
      # duplicated entries. This is in contrast to splatted arguments for function
      # invokes where parameter names are paired with parameter values in a
      # hashtable and naturally prevent duplicated entries. This is why we set
      # 'LOOPZ.TRAVERSE.INVOKEE.PARAMS' to $BlockParams and not $positional.
      #
      $PassThru['LOOPZ.TRAVERSE.INVOKEE.PARAMS'] = $BlockParams;
    }

    if (-not($Hoist.ToBool())) {
      # We only want to manage the index via $PassThru when we are recursing
      #
      $PassThru['LOOPZ.FOREACH.INDEX'] = $index;
    }
    $result = $null;

    # This is the top level invoke
    #
    if ($Condition.Invoke($directory)) {
      try {
        if ('InvokeScriptBlock' -eq $PSCmdlet.ParameterSetName) {
          $result = $Block.Invoke($positional);
        }
        elseif ('InvokeFunction' -eq $PSCmdlet.ParameterSetName) {
          $result = & $Functee @parameters;
        }
      }
      catch {
        Write-Error "Invoke-TraverseDirectory(top-level) Error: ($_), for item: '$($directory.Name)'";
      }
      finally {
        if ($Hoist.ToBool()) {
          $index++;
        }
        else {
          $PassThru['LOOPZ.FOREACH.INDEX']++;
          $index = $PassThru['LOOPZ.FOREACH.INDEX'];
        }
      }

      if ($result.psobject.properties.match('Trigger') -and $result.Trigger) {
        $PassThru['LOOPZ.FOREACH.TRIGGER'] = $true;
        $trigger = $true;
      }

      if ($result.psobject.properties.match('Break') -and $result.Break) {
        $broken = $true;
      }
    }

    # --- end of top level invoke ----------------------------------------------------------

    if ($Hoist.ToBool()) {
      # Perform non-recursive retrieval of descendant directories
      #
      [System.IO.DirectoryInfo[]]$directoryInfos = Get-ChildItem -Path $Path `
        -Directory -Recurse | Where-Object { $Condition.Invoke($_) }

      if ($directoryInfos) {
        # No need to manage the index, let Invoke-ForeachFsItem do this for us,
        # except we do need to inform Invoke-ForeachFsItem to start the index at
        # +1, because 0 is for the top level directory which has already been
        # handled.
        #
        [System.Collections.Hashtable]$parametersFeFsItem = @{
          'Directory'  = $true;
          'PassThru'   = $PassThru;
          'StartIndex' = $index;
          'Summary'    = $Summary;
        }

        if ('InvokeScriptBlock' -eq $PSCmdlet.ParameterSetName) {
          $parametersFeFsItem['Block'] = $Block;
          $parametersFeFsItem['BlockParams'] = $BlockParams;
        }
        else {
          $parametersFeFsItem['Functee'] = $Functee;
          $parametersFeFsItem['FuncteeParams'] = $FuncteeParams;
        }

        $directoryInfos | & 'Invoke-ForeachFsItem' @parametersFeFsItem;
      }
    }
    else {
      # Set up the adapter. (NB, can't use splatting because we're invoking a script block
      # as opposed to a named function.)
      #
      $PassThru['LOOPZ.TRAVERSE.CONDITION'] = $Condition;
      $PassThru['LOOPZ.TRAVERSE.ADAPTED'] = $recurseTraverseDirectory;
      $PassThru['LOOPZ.TRAVERSE.ADAPTOR'] = $adapter;

      if ('InvokeScriptBlock' -eq $PSCmdlet.ParameterSetName) {
        $PassThru['LOOPZ.TRAVERSE.INVOKEE'] = $Block;
      }
      elseif ('InvokeFunction' -eq $PSCmdlet.ParameterSetName) {
        $PassThru['LOOPZ.TRAVERSE.INVOKEE'] = $Functee;
      }

      # Now perform start of recursive traversal
      #
      [System.IO.DirectoryInfo[]]$directoryInfos = Get-ChildItem -Path $Path `
        -Directory | Where-Object { $Condition.Invoke($_) }

      if ($directoryInfos) {
        $directoryInfos | Invoke-ForeachFsItem -Directory -Block $adapter `
          -StartIndex $index -PassThru $PassThru -Condition $Condition -Summary $Summary;
      }

      [int]$skipped = 0;
      $index = $PassThru['LOOPZ.FOREACH.INDEX'];
      $trigger = $PassThru['LOOPZ.FOREACH.TRIGGER'];
      $Summary.Invoke($index, $skipped, $trigger, $PassThru);
    }
  }
  else {
    Write-Error "Path specified '$($Path)' is not a directory";
  }
} # Invoke-TraverseDirectory

function Select-FsItem {
  <#
  .NAME
    Select-FsItem
 
  .SYNOPSIS
    A predicate function that indicates whether an item identified by the Name matches
  the include/exclude filters specified.
 
  .DESCRIPTION
    Use this utility function to help specify a Condition for Invoke-TraverseDirectory.
  This function is partly required because the Include/Exclude parameters on functions
  such as Get-ChildItems/Copy-Item/Get-Item etc only work on files not directories.
 
  .PARAMETER Name
    A string to be matched against the filters.
 
  .PARAMETER Includes
      An array containing a list of filters, each must contain a wild-card ('*'). If a
  particular filter does not contain a wild-card, then it will be ignored. If Name matches
  any of the filters in Includes, and are not Excluded, the result will be true.
 
  .PARAMETER Excludes
    An array containing a list of filters, each must contain a wild-card ('*'). If a
  particular filter does not contain a wild-card, then it will be ignored. If the Name
  matches any of the filters in the list, will cause the end result to be false.
  Any match in the Excludes overrides a match in Includes, so an item
  that is matched in Include, can be excluded by the Exclude.
 
  .PARAMETER Case
    Switch parameter which controls case sensitivity of inclusion/exclusion. By default
  filtering is case insensitive. When The Case switch is specified, filtering is case
  sensitive.
 
  .EXAMPLE 1
    Define a Condition that allows only directories beginning with A, but also excludes
    any directory containing '_' or '-'.
 
    [scriptblock]$filterDirectories = {
      [OutputType([boolean])]
      param(
        [System.IO.DirectoryInfo]$directoryInfo
      )
      [string[]]$directoryIncludes = @('A*');
      [string[]]$directoryExcludes = @('*_*', '*-*');
 
      Select-FsItem -Name $directoryInfo.Name `
        -Includes $directoryIncludes -Excludes $directoryExcludes;
 
      Invoke-TraverseDirectory -Path <path> -Block <block> -Condition $filterDirectories;
    }
  #>


  [OutputType([boolean])]
  param(
    [Parameter(Mandatory)]
    [string]$Name,

    [Parameter()]
    [string[]]$Includes = @(),

    [Parameter()]
    [string[]]$Excludes = @(),

    [Parameter()]
    [switch]$Case
  )

  # Note we wrap the result inside @() array designator just in-case the where-object
  # returns just a single item in which case the array would be flattened out into
  # an individual scalar value which is what we don't want, damn you powershell for
  # doing this and making life just so much more difficult. Actually, on further
  # investigation, we don't need to wrap inside @(), because we've explicitly defined
  # the type of the includes variables to be arrays, which would preserve the type
  # even in the face of powershell annoyingly flattening the single item array. @()
  # being left in for clarity and show of intent.
  #
  [string[]]$validIncludes = @($Includes | Where-Object { $_.Contains('*') })
  [string[]]$validExcludes = @($Excludes | Where-Object { $_.Contains('*') })

  [boolean]$resolvedInclude = $validIncludes `
    ? (select-ResolvedFsItem -FsItem $Name -Filter $Includes -Case:$Case) `
    : $false;

  [boolean]$resolvedExclude = $validExcludes `
    ? (select-ResolvedFsItem -FsItem $Name -Filter $Excludes -Case:$Case) `
    : $false;

  ($resolvedInclude) -and -not($resolvedExclude)
} # Select-FsItem
function Write-HostFeItemDecorator {
  <#
  .NAME
    Write-HostFeItemDecorator
 
  .SYNOPSIS
    Wraps a function or scriptblock as a decorator writing appropriate user interface
    info to the host for each entry in the pipeline.
 
  .DESCRIPTION
      The script-block/function (invokee) being decorated may or may not Support ShouldProcess. If it does,
    then the client should add 'WHAT-IF' to the pass through, set to the current
    value of WhatIf; or more accurately the existence of 'WhatIf' in PSBoundParameters. Or another
    way of putting it is, the presence of WHAT-IF indicates SupportsShouldProcess, and the value of
    'WHAT-IF' dictates the value of WhatIf. This way, we only need a single
    value in the PassThru, rather than having to represent SupportShouldProcess explicitly with
    another value.
      The PastThru must contain either a 'LOOPZ.WH-FOREACH-DECORATOR.FUNCTION-NAME' entry meaning
    a named function is being decorated or 'LOOPZ.WH-FOREACH-DECORATOR.BLOCK' meaning a script
    block is being decorated, but not both.
      By default, Write-HostFeItemDecorator will display an item no for each object in the pipeline
    and a property representing the Product. The Product is a property that the invokee can set on the
    PSCustomObject it returns. However, additional properties can be displayed. This can be achieved by
    the invokee populating another property Pairs, which is an array of string based key/value pairs. All
    properties found in Pairs will be written out by Write-HostFeItemDecorator.
      By default, to render the value displayed (ie the 'Product' property item on the PSCustomObject
    returned by the invokee), ToString() is called. However, the 'Product' property may not have a
    ToString() method, in this case (you will see an error indicating ToString method not being
    available), the user should provide a custom script-block to determine how the value is
    constructed. This can be done by assigning a custom script-block to the
    'LOOPZ.WH-FOREACH-DECORATOR.GET-RESULT' entry in PassThru. eg:
 
      [scriptblock]$customGetResult = {
        param($result)
        $result.SomeCustomPropertyOfRelevanceThatIsAString;
      }
      $PassThru['LOOPZ.WH-FOREACH-DECORATOR.GET-RESULT'] = $customGetResult;
      ...
 
      Note also, the user can provide a custom 'GET-RESULT' in order to control what is displayed
    by Write-HostFeItemDecorator.
 
      This function is designed to be used with Invoke-ForeachFsItem and as such, it's signature
    needs to match that required by Invoke-ForeachFsItem. Any additional parameters can be
    passed in via the PassThru.
      The rationale behind Write-HostFeItemDecorator is to maintain separation of concerns
    that allows development of functions that could be used with Invoke-ForeachFsItem which do
    not contain any UI related code. This strategy also helps for the development of different
    commands that produce output to the terminal in a consistent manner.
 
  .PARAMETER $Underscore
    The current pipeline object.
 
  .PARAMETER $Index
    The 0 based index representing current item in the pipeline.
 
  .PARAMETER $PassThru
    A hash table containing miscellaneous information gathered internally
    throughout the iteration batch. This can be of use to the user, because it is the way
    the user can perform bi-directional communication between the invoked custom script block
    and client side logic.
 
  .PARAMETER $Trigger
      A boolean value, useful for state changing idempotent operations. At the end
    of the batch, the state of the trigger indicates whether any of the items were actioned.
    When the script block is invoked, the trigger should indicate if the trigger was pulled for
    any of the items so far processed in the batch. This is the responsibility of the
    client's block implementation.
 
  .RETURNS
    The result of invoking the decorated script-block.
 
  .EXAMPLE 1
 
  function Test-FN {
    param(
      [System.IO.DirectoryInfo]$Underscore,
      [int]$Index,
      [System.Collections.Hashtable]$PassThru,
      [boolean]$Trigger,
    )
 
    $format = $PassThru['CLIENT.FORMAT'];
    @{ Product = $format -f $Underscore.Name, $Underscore.Exists }
    ...
  }
 
  [Systems.Collection.Hashtable]$passThru = @{
    'LOOPZ.WH-FOREACH-DECORATOR.FUNCTION-NAME' = 'Test-FN';
    'CLIENT.FORMAT' = '=== [{0}] -- [{1}] ==='
  }
 
  Get-ChildItem ... | Invoke-ForeachFsItem -Path <path> -PassThru $passThru
    -Functee 'Write-HostFeItemDecorator'
 
    So, Test-FN is not concerned about writing any output to the console, it simply does
  what it does silently and Write-HostFeItemDecorator handles generation of output. It
  invokes the function defined in 'LOOPZ.WH-FOREACH-DECORATOR.FUNCTION-NAME' and generates
  corresponding output. It happens to use the console colouring facility provided by a
  a dependency Elizium.Krayola to create colourful output in a predefined format via the
  Krayola Theme.
 
  Note, Write-HostFeItemDecorator does not forward additional parameters to the decorated
  function (Test-FN), but this can be circumvented via the PassThru as illustrated by
  the 'CLIENT.FORMAT' parameter in this example.
 
  #>


  [OutputType([PSCustomObject])]
  [CmdletBinding()]
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
  [Alias('wife', 'Decorate-Foreach')]
  param (
    [Parameter(
      Mandatory = $true
    )]
    $Underscore,

    [Parameter(
      Mandatory = $true
    )]
    [int]$Index,

    [Parameter(
      Mandatory = $true
    )]
    [ValidateScript( {
        return ($_.ContainsKey('LOOPZ.WH-FOREACH-DECORATOR.FUNCTION-NAME') -xor
          $_.ContainsKey('LOOPZ.WH-FOREACH-DECORATOR.BLOCK'))
      })]
    [System.Collections.Hashtable]
    $PassThru,

    [Parameter()]
    [boolean]$Trigger
  )

  [scriptblock]$defaultGetResult = {
    param($result)
    try {
      $result.ToString();
    }
    catch {
      Write-Error "Default get-result function failed, consider defining custom function as 'LOOPZ.WH-FOREACH-DECORATOR.GET-RESULT' in PassThru";
    }
  }

  [scriptblock]$decorator = {
    param ($_underscore, $_index, $_passthru, $_trigger)

    if ($_passthru.Contains('LOOPZ.WH-FOREACH-DECORATOR.FUNCTION-NAME')) {
      [string]$functee = $_passthru['LOOPZ.WH-FOREACH-DECORATOR.FUNCTION-NAME'];

      [System.Collections.Hashtable]$parameters = @{
        'Underscore' = $_underscore;
        'Index'      = $_index;
        'PassThru'   = $_passthru;
        'Trigger'    = $_trigger;
      }
      if ($_passthru.Contains('WHAT-IF')) {
        $parameters['WhatIf'] = $_passthru['WHAT-IF'];
      }

      return & $functee @parameters;
    }
    elseif ($_passthru.Contains('LOOPZ.WH-FOREACH-DECORATOR.BLOCK')) {
      [scriptblock]$block = $_passthru['LOOPZ.WH-FOREACH-DECORATOR.BLOCK'];

      return $block.Invoke($_underscore, $_index, $_passthru, $_trigger);
    }
  }

  $invokeResult = $decorator.Invoke($Underscore, $Index, $PassThru, $Trigger);

  [string]$message = $PassThru['LOOPZ.WH-FOREACH-DECORATOR.MESSAGE'];
  [string]$productValue = [string]::Empty;
  [boolean]$ifTriggered = $PassThru.ContainsKey('LOOPZ.WH-FOREACH-DECORATOR.IF-TRIGGERED');
  [boolean]$resultIsTriggered = $invokeResult.psobject.properties.match('Trigger') -and $invokeResult.Trigger;

  # Suppress the write if client has set IF-TRIGGERED and the result is not triggered.
  # This makes re-runs of a state changing operation less verbose if that's required.
  #
  if (-not($ifTriggered) -or ($resultIsTriggered)) {
    $getResult = $PassThru.Contains('LOOPZ.WH-FOREACH-DECORATOR.GET-RESULT') `
      ? $PassThru['LOOPZ.WH-FOREACH-DECORATOR.GET-RESULT'] : $defaultGetResult;

    [string[][]]$themedPairs = @(, @('No', $("{0,3}" -f ($Index + 1))));

    # Get Product if it exists
    #
    [string]$productLabel = [string]::Empty;
    if ($invokeResult -and $invokeResult.psobject.properties.match('Product') -and $invokeResult.Product) {
      $productValue = $getResult.Invoke($invokeResult.Product);
      $productLabel = $PassThru.ContainsKey('LOOPZ.WH-FOREACH-DECORATOR.PRODUCT-LABEL') `
        ? $PassThru['LOOPZ.WH-FOREACH-DECORATOR.PRODUCT-LABEL'] : 'Product';

      if (-not([string]::IsNullOrWhiteSpace($productLabel))) {
        $themedPairs += , @($productLabel, $productValue);
      }
    }

    # Get Key/Value Pairs
    #
    if ($invokeResult -and $invokeResult.psobject.properties.match('Pairs') -and
      $invokeResult.Pairs -and ($invokeResult.Pairs -is [Array]) -and ($invokeResult.Pairs.Count -gt 0)) {
      $themedPairs += $invokeResult.Pairs;
    }

    # Write with a Krayola Theme
    #
    [System.Collections.Hashtable]$krayolaTheme = `
      $PassThru.ContainsKey('LOOPZ.WH-FOREACH-DECORATOR.KRAYOLA-THEME') `
      ? $PassThru['LOOPZ.WH-FOREACH-DECORATOR.KRAYOLA-THEME'] : (Get-KrayolaTheme);

    [System.Collections.Hashtable]$parameters = @{}
    $parameters['Pairs'] = $themedPairs;
    $parameters['Theme'] = $krayolaTheme;

    [string]$writerFn = 'Write-ThemedPairsInColour';

    if (-not([string]::IsNullOrWhiteSpace($message))) {
      $parameters['Message'] = $message;
    }

    if (-not([string]::IsNullOrWhiteSpace($writerFn))) {
      & $writerFn @parameters;
    }
  }

  return $invokeResult;
} # Write-HostFeItemDecorator

function edit-RemoveSingleSubString {
  <#
.NAME
  edit-RemoveSingleSubString
 
.SYNOPSIS
  Removes a sub-string from the target string provided.
 
.DESCRIPTION
  Either the first or the last occurrence of a single can be removed depending on
  whether the Last flag has been set.
 
.PARAMETER Target
  The string from which the subtraction is to occur.
 
.PARAMETER Subtract
  The sub string to subtract from the Target.
 
.PARAMETER Insensitive
  Flag to indicate if the search is case sensitive or not. By default, search is case
  sensitive.
 
.PARAMETER Last
  Flag to indicate whether the last occurrence of a sub string is to be removed from the
  Target.
#>

  [CmdletBinding(DefaultParameterSetName = 'Single')]
  [OutputType([string])]
  param
  (
    [Parameter(ParameterSetName = 'Single')]
    [String]$Target,

    [Parameter(ParameterSetName = 'Single')]
    [String]$Subtract,

    [Parameter(ParameterSetName = 'Single')]
    [switch]$Insensitive,

    [Parameter(ParameterSetName = 'Single')]
    [Parameter(ParameterSetName = 'LastOnly')]
    [switch]$Last
  )

  [StringComparison]$comparison = $Insensitive.ToBool() ? `
    [StringComparison]::OrdinalIgnoreCase : [StringComparison]::Ordinal;

  $result = $Target;

  # https://docs.microsoft.com/en-us/dotnet/standard/base-types/best-practices-strings
  #
  if (($Subtract.Length -gt 0) -and ($Target.Contains($Subtract, $comparison))) {
    $slen = $Subtract.Length;

    $foundAt = $Last.ToBool() ? $Target.LastIndexOf($Subtract, $comparison) : `
      $Target.IndexOf($Subtract, $comparison);

    if ($foundAt -eq 0) {
      $result = $Target.Substring($slen);
    }
    elseif ($foundAt -gt 0) {
      $result = $Target.Substring(0, $foundAt);
      $result += $Target.Substring($foundAt + $slen);
    }
  }

  $result;
}
  function select-ResolvedFsItem {
    [OutputType([boolean])]
    param(
      [Parameter(Mandatory)]
      [string]$FsItem,

      [Parameter(Mandatory)]
      [AllowEmptyCollection()]
      [string[]]$Filter,

      [Parameter()]
      [switch]$Case
    )

    [boolean]$liked = $false;
    [int]$counter = 0;

    do {
      $liked = $Case.ToBool() `
        ? $FsItem -CLike $Filter[$counter] `
        : $FsItem -Like $Filter[$counter];
      $counter++;
    } while (-not($liked) -and ($counter -lt $Filter.Count));

    $liked;
  }
function get-AnswerAdvancedFn {

  # This function is only required because the tests using the invoke operator
  # on a string can not correctly pick up the local function name (ie defined as part
  # of the test fixture) and see its definition to be invoked.
  #
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
  [CmdletBinding(SupportsShouldProcess)]
  param(
    [Parameter(Mandatory)]
    $Underscore,

    [Parameter(Mandatory)]
    [int]$Index,

    [Parameter(Mandatory)]
    [System.Collections.Hashtable]$PassThru,

    [Parameter(Mandatory)]
    [boolean]$Trigger
  )

  [PSCustomObject]@{ Product = "{0}: {1}" -f $Underscore, $PassThru['ANSWER'] }
}
function get-AnswerAdvancedFnWithTrigger {

  # This function is only required because the tests using the invoke operator
  # on a string can not correctly pick up the local function name (ie defined as part
  # of the test fixture) and see its definition to be invoked.
  #
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
  [CmdletBinding(SupportsShouldProcess)]
  param(
    [Parameter(Mandatory)]
    $Underscore,

    [Parameter(Mandatory)]
    [int]$Index,

    [Parameter(Mandatory)]
    [System.Collections.Hashtable]$PassThru,

    [Parameter(Mandatory)]
    [boolean]$Trigger
  )

  [PSCustomObject]@{ Product = ("{0}: {1}" -f $Underscore, $PassThru['ANSWER']);
    Trigger = $true }
}

# WTF, this should be a helper file in Tests/Helpers, but putting this function there and trying to
# dynamically invoke the function with the & operator from invoke-ForachFile doesnt find the
# function definition, regardless of wether its sourced inside BeforeEach, or defined inline or
# any other work-around. The only temporary shit solution, is to include the test helper function
# inside the module implementation, which fucks me off to high fucking heaven.
#
function invoke-Dummy {
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')]
  param(
    [Alias('Underscore')]
    [System.IO.FileInfo]$FileInfo,
    [int]$Index,
    [System.Collections.Hashtable]$PassThru,
    [boolean]$Trigger
  )
  Write-Warning "These aren't the droids you're looking for, ..., move along, move along!";

  [PSCustomObject]@{ Product = $FileInfo; }
}

function Test-FileResult {
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
  param(
    [Parameter(Mandatory)]
    [System.IO.FileInfo]$Underscore,

    [Parameter(Mandatory)]
    [int]$Index,

    [Parameter(Mandatory)]
    [System.Collections.Hashtable]$PassThru,

    [Parameter(Mandatory)]
    [boolean]$Trigger,

    [Parameter(Mandatory)]
    [string]$Format
  )

  [string]$result = $Format -f ($Underscore.Name);
  Write-Debug "Custom function; Test-FileResult: '$result'";
  @{ Product = $Underscore }
}

function Test-FireEXBreak {
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
  param(
    [Parameter(Mandatory)]
    [System.IO.DirectoryInfo]$Underscore,

    [Parameter(Mandatory)]
    [int]$Index,

    [Parameter(Mandatory)]
    [System.Collections.Hashtable]$PassThru,

    [Parameter(Mandatory)]
    [boolean]$Trigger
  )
  $break = ('EX' -eq $Underscore.Name);
  Write-Host " [-] Test-FireEXBreak(index: $Index): directory: $($Underscore.Name)";
  @{ Product = $Underscore; Break = $break }
}

function Test-FireEXTrigger {
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
  param(
    [Parameter(Mandatory)]
    [System.IO.DirectoryInfo]$Underscore,

    [Parameter(Mandatory)]
    [int]$Index,

    [Parameter(Mandatory)]
    [System.Collections.Hashtable]$PassThru,

    [Parameter(Mandatory)]
    [boolean]$Trigger
  )
  $localTrigger = ('EX' -eq $Underscore.Name);
  Write-Host " [-] Test-FireEXTrigger(index: $Index, local trigger: $localTrigger, Trigger: $Trigger): directory: $($Underscore.Name)";
  @{ Product = $Underscore; Trigger = $localTrigger }
}

function Test-HoistResult {
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
  param(
    [Parameter(Mandatory)]
    [System.IO.DirectoryInfo]$Underscore,

    [Parameter(Mandatory)]
    [int]$Index,

    [Parameter(Mandatory)]
    [System.Collections.Hashtable]$PassThru,

    [Parameter(Mandatory)]
    [boolean]$Trigger,

    [Parameter(Mandatory = $false)]
    [string]$Format = "These aren't the droids you're looking for, ..., move along, move along!:___{0}___"
  )

  [string]$result = $Format -f ($Underscore.Name);
  Write-Debug "Custom function; Test-HoistResult: '$result'";
  @{ Product = $Underscore }
}

function Test-ShowMirror {
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
  param(
    [Parameter(Mandatory)]
    [System.IO.DirectoryInfo]$Underscore,

    [Parameter(Mandatory)]
    [int]$Index,

    [Parameter(Mandatory)]
    [System.Collections.Hashtable]$PassThru,

    [Parameter(Mandatory)]
    [boolean]$Trigger,

    [Parameter(Mandatory)]
    [string]$Format
  )

  [string]$result = $Format -f ($Underscore.Name);
  Write-Debug "Custom function; Show-Mirror: '$result'";
  @{ Product = $Underscore }
}
Export-ModuleMember -Variable LoopzHelpers, LoopzUI

Export-ModuleMember -Function Invoke-ForeachFsItem, Invoke-MirrorDirectoryTree, Invoke-TraverseDirectory, Select-FsItem, Write-HostFeItemDecorator