Elizium.RexFs.psm1

using module Elizium.Krayola;
using module Elizium.Loopz;
Set-StrictMode -Version 1.0

$global:RexFs = [PSCustomObject]@{
  Defaults = [PSCustomObject]@{
    Remy = [PSCustomObject]@{
      Marker  = [char]0x2BC1;

      Context = [PSCustomObject]@{
        Title             = 'Rename';
        ItemMessage       = 'Rename Item';
        SummaryMessage    = 'Rename Summary';
        Locked            = 'REXFS_REMY_LOCKED';
        UndoDisabledEnVar = 'REXFS_REMY_UNDO_DISABLED';
        OperantShortCode  = 'remy';
      }
    }
  }

  Rules    = [PSCustomObject]@{
    Remy = @(
      @{
        ID             = 'MissingCapture';
        'IsApplicable' = [scriptblock] {
          param([string]$_Input)
          $_Input -match '\$\{\w+\}';
        };

        'Transform'    = [scriptblock] {
          param([string]$_Input)
          $_Input -replace "\$\{\w+\}", ''
        };
        'Signal'       = 'MISSING-CAPTURE'
      },

      @{
        ID             = 'Trim';
        'IsApplicable' = [scriptblock] {
          param([string]$_Input)
          $($_Input.StartsWith(' ') -or $_Input.EndsWith(' '));
        };

        'Transform'    = [scriptblock] {
          param([string]$_Input)
          $_Input.Trim();
        };
        'Signal'       = 'TRIM'
      },

      @{
        ID             = 'Dashes';
        'IsApplicable' = [scriptblock] {
          param([string]$_Input)
          $_Input -match '(?:\s{,2})?(?:-\s+-)|(?:--)(?:\s{,2})?';
        };

        'Transform'    = [scriptblock] {
          param([string]$_Input)

          [regex]$regex = [regex]::new('(?:\s{,2})?(?:-\s+-)|(?:--)(?:\s{,2})?');
          [string]$result = $_Input;

          while ($regex.IsMatch($result)) {
            $result = $regex.Replace($result, ' - ');
          }
          $result;
        };
        'Signal'       = 'REMY.DASHES'
      },

      @{
        ID             = 'Spaces';
        'IsApplicable' = [scriptblock] {
          param([string]$_Input)
          $_Input -match "\s{2,}";
        };

        'Transform'    = [scriptblock] {
          param([string]$_Input)
          $_Input -replace "\s{2,}", ' '
        };
        'Signal'       = 'MULTI-SPACES'
      }
    );
  }
}

function Rename-Many {
  <#
  .NAME
    Rename-Many
 
  .SYNOPSIS
    Performs a bulk rename for all file system objects delivered through the pipeline,
  via regular expression replacement.
 
  .DESCRIPTION
    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,
  Rename-Many will perform a rename.
    Rename-Many is a powerful command and should be used with caution. Because of the
  potential of accidental misuse, a number of protections have been put in place:
 
  * By default, the command is locked. This means that the command will not actually
  perform any renames until it has been unlocked by the user. When locked, the command
  runs as though -WhatIf has been specified. There are indications in the output to show
  that the command is in a locked state (there is an indicator in the batch header and
  a 'Novice' indicator in the summary). To activate the command, the user needs to
  set the environment variable 'REXFS_REMY_LOCKED' to $false. The user should not
  unlock the command until they are comfortable with how to use this command properly
  and knows how to write regular expressions correctly. (See regex101.com)
 
  * An undo script is generated by default. If the user has invoked a rename operation
  by accident without specifying $WhatIf (or any other WhatIf equivalent like $Diagnose)
  then the user can execute the undo script to reverse the rename operation. The user
  should clearly do this immediately on recognising the error of their ways. In a panic,
  the user may terminate the command via ctrl-c. In this case, a partial undo script is
  still generated and should contain the undo operations for the renames that were
  performed up to the point of the termination request.
    The name of the undo script is based upon the current date and time and is displayed
  in the summary. (The user can, if they wish disable the undo feature if they don't want
  to have to manage the accumulation of undo scripts, by setting the environment variable
  REXFS_REMY_UNDO_DISABLED to $true)
 
  Another important point of note is that there are currently 3 modes of operation:
  'move', 'update' or 'cut':
  * 'move': requires an anchor, which may be an $Anchor pattern or
    either $Start or $End switches.
  * 'update': requires $With or $Paste without an anchor.
  * 'cut': no anchor or $With/$Paste specified, the $Pattern match is simply removed
    from the name.
 
  The following regular expression parameters:
  * $Pattern
  * $Anchor
  * $Copy
  can optionally have an occurrence value specified that can be used to select which
  match is active. In the case where a provided expression has multiple matches, the
  occurrence value can be used to single out which one. When no occurrence is specified,
  the default is the first match only. The occurrence for a parameter can be:
 
  * f: first occurrence
  * l: last occurrence
  * <number>: the nth occurrence
  The occurrence is specified after the regular expression eg:
  -Pattern '\w\d{2,3}', l
    which means match the Last occurrence of the expression.
  (Actually, an occurrence may be specified for $Include and $Except but there is no
  point in doing so because these patterns only provide a filtering function and play
  no part in the actual renaming process).
 
    A note about escaping. If a pattern needs to use an regular expression character as
  a literal, it must be escaped. There are multiple ways of doing this:
  * use the 'esc' function; eg: -Pattern $($esc('(\d{2})'))
  * use a leading ~; -Pattern '~(123)'
 
  The above 2 approaches escape the entire string. The second approach is more concise
  and avoids the necessary use of extra brackets and $.
  * use 'esc' alongside other string concatenation:
    eg: -Pattern $($esc('(123)') + '-(?<ccy>GBP|SEK)').
  This third method is required when the whole pattern should not be subjected to
  escaping.
 
  .LINK
    https://eliziumnet.github.io/RexFs/
 
  .PARAMETER Anchor
    Indicates that the rename operation will be a move of the token from its original point
  to the point indicated by Anchor. Anchor is a regular expression string applied to the
  pipeline item's name (after the $Pattern match has been removed). The $Pattern match that
  is removed is inserted at the position indicated by the anchor match in collaboration with
  the $Relation parameter.
 
  .PARAMETER AnchorEnd
    Similar to Anchor except that if the pattern specified by AnchorEnd does not match, then
  the Pattern match will be moved to the End. This is known as a Hybrid Anchor.
 
  .PARAMETER AnchorStart
    Similar to Anchor except that if the pattern specified by AnchorEnd does not match, then
  the Pattern match will be moved to the Start. This is known as a Hybrid Anchor.
 
  .PARAMETER Append
    Appends a literal string to end of items name
 
  .PARAMETER Condition
    Provides another way of filtering pipeline items. This is not typically specified on the
  command line, rather it is meant for those wanting to build functionality on top of Rename-Many.
 
  .PARAMETER Context
    Provides another way of customising Rename-Many. This is not typically specified on the
  command line, rather it is meant for those wanting to build functionality on top of Rename-Many.
  $Context should be a PSCustomObject with the following note properties:
  * Title (default: 'Rename') the name used in the batch header.
  * ItemMessage (default: 'Rename Item') the operation name used for each renamed item.
  * SummaryMessage (default: 'Rename Summary') the name used in the batch summary.
  * Locked (default: 'REXFS_REMY_LOCKED) the name of the environment variable which controls
    the locking of the command.
  * DisabledEnVar (default: 'REXFS_REMY_UNDO_DISABLED') the name of the environment variable
    which controls if the undo script feature is disabled.
  * UndoDisabledEnVar (default: 'REXFS_REMY_UNDO_DISABLED') the name of the environment
    variable which determines if the Undo feature is disabled. This allows any other function
    built on top of Rename-Many to control the undo feature for itself independently of
    Rename-Many.
 
  .PARAMETER Copy
    Regular expression string applied to the pipeline item's name (after the $Pattern match
  has been removed), indicating a portion which should be copied and re-inserted (via the
  $Paste parameter; see $Paste or $With). Since this is a regular expression to be used in
  $Paste/$With, there is no value in the user specifying a static pattern, because that
  static string can just be defined in $Paste/$With. The value in the $Copy parameter comes
  when a generic pattern is defined eg \d{3} (is non Literal), specifies any 3 digits as
  opposed to say '123', which could be used directly in the $Paste/$With parameter without
  the need for $Copy. The match defined by $Copy is stored in special variable ${_c} and
  can be referenced as such from $Paste and $With.
 
  .PARAMETER Cut
    Is a replacement for the Pattern parameter, when a Cut operation is required. The matched
  items will be removed from the item's name, and no other replacement occurs.
 
  .PARAMETER Diagnose
    switch parameter that indicates the command should be run in WhatIf mode. When enabled
  it presents additional information that assists the user in correcting the un-expected
  results caused by an incorrect/un-intended regular expression. The current diagnosis
  will show the contents of named capture groups that they may have specified. When an item
  is not renamed (usually because of an incorrect regular expression), the user can use the
  diagnostics along side the 'Not Renamed' reason to track down errors. When $Diagnose has
  been specified, $WhatIf does not need to be specified.
 
  .PARAMETER Directory
    switch to indicate only Directory items in the pipeline will be processed. If neither
  this switch or the File switch are specified, then both File and Directory items
  are processed.
 
  .PARAMETER Drop
    A string parameter (only applicable to move operations, ie Anchor/Star/End/hybrid) that
  defines what text is used to replace the Pattern match. So in this use-case, the user wants
  to move a particular token/pattern to another part of the name and at the same time drop a
  static string in the place where the $Pattern was removed from.
 
  .PARAMETER End
    Is another type of anchor used instead of $Anchor and specifies that the $Pattern match
  should be moved to the end of the new name.
 
  .PARAMETER Except
    Regular expression string applied to the original pipeline item's name (before the $Pattern
  match has been removed). Allows the user to exclude some items that have been fed in via the
  pipeline. Those items that match the exclusion are skipped during the rename batch.
 
  .PARAMETER File
    switch to indicate only File items in the pipeline will be processed. If neither
  this switch or the Directory switch are specified, then both File and Directory items
  are processed.
 
  .PARAMETER Include
    Regular expression string applied to the original pipeline item's name (before the $Pattern
  match has been removed). Allows the user to include some items that have been fed in via the
  pipeline. Only those items that match $Include pattern are included during the rename batch,
  the others are skipped. The value of the Include parameter comes when you want to define
  a pattern which pipeline items match, without it be removed from the original name, which is
  what happens with $Pattern. Eg, the user may want to specify the only items that should be
  considered a candidate to be renamed are those that match a particular pattern but doing so
  in $Pattern would simply remove that pattern. That may be ok, but if it's not, the user should
  specify a pattern in the $Include and use $Pattern for the match you do want to be moved
  (with Anchor/Start/End) or replaced (with $With/$Paste).
 
  .PARAMETER Paste
    Formatter parameter for Update operations. Can contain named/numbered group references
  defined inside regular expression parameters, or use special named references $0 for the whole
  Pattern match and ${_c} for the whole Copy match.
 
  .PARAMETER Pattern
    Regular expression string that indicates which part of the pipeline items' name that
  either needs to be moved or replaced as part of bulk rename operation. Those characters
  in the name which match are removed from the name.
    The pattern can be followed by an occurrence indicator. As the $Pattern parameter is
  strictly speaking an array, the user can specify the occurrence after the regular
  expression eg:
    $Pattern '(?<code>\w\d{2})', l
 
    => This indicates that the last match should be captured into named group 'code'.
 
  .PARAMETER Prepend
    Prefixes a literal string to start of items name
 
  .PARAMETER Relation
    Used in conjunction with the $Anchor parameter and can be set to either 'before' or
  'after' (the default). Defines the relationship of the $pattern match with the $Anchor
  match in the new name for the pipeline item.
 
  .PARAMETER Start
    Is another type of anchor used instead of $Anchor and specifies that the $Pattern match
  should be moved to the start of the new name.
 
  .PARAMETER Top
    A number indicating how many items to process. If it is known that the number of items
  that will be candidates to be renamed is large, the user can limit this to the first $Top
  number of items. This is typically used as an exploratory tool, to determine the effects
  of the rename operation.
 
  .PARAMETER Transform
    A script block which is given the chance to perform a modification to the finally named
  item. The transform is invoked prior to post-processing, so that the post-processing rules
  are not breached and the transform does not have to worry about breaking them. The transform
  function's signature is as follows:
 
  * Exchange
  * Value: original item's name
 
  and should return a PSCustomObject, with a Payload member set to the new name and a Success
  boolean member. If failed for whatever reason, there should be FailedReason string member
  instead of the Payload.
 
  .PARAMETER Whole
    Provides an alternative way to indicate that the regular expression parameters
  should be treated as a whole word (it just wraps the expression inside \b tokens).
  If set to '*', then it applies to all expression parameters otherwise a single letter
  can specify which of the parameters 'Whole' should be applied to. Valid values are:
 
  * 'p': $Pattern
  * 'a': $Anchor/AnchorEnd/AnchorStart
  * 'c': $Copy
  * 'i': $Include
  * 'x': $Except
  * '*': All the above
  (NB: Currently, can't be set to more than 1 of the above items at a time)
 
  .PARAMETER With
    This is a NON regular expression string. It would be more accurately described as a formatter,
  similar to the $Paste parameter. Defines what text is used as the replacement for the $Pattern
  match. Works in concert with $Relation (whereas $Paste does not). $With can reference special
  variables:
 
  * $0: the pattern match
  * ${_a}: the anchor match
  * ${_c}: the copy match
 
  When $Pattern contains named capture groups, these variables can also be referenced. Eg if the
  $Pattern is defined as '(?<day>\d{1,2})-(?<mon>\d{1,2})-(?<year>\d{4})', then the variables
  ${day}, ${mon} and ${year} also become available for use in $With or $Paste.
  Typically, $With is literal text which is used to replace the $Pattern match and is inserted
  according to the Anchor match, (or indeed $Start or $End) and $Relation. When using $With,
  whatever is defined in the $Anchor match is removed from the pipeline's name.
 
  .PARAMETER underscore
    The pipeline item which should either be an instance of FileInfo or DirectoryInfo.
 
  .INPUTS
    FileSystemInfo (FileInfo or DirectoryInfo) bound to $underscore
 
  * MOVE EXAMPLES (anchored)
 
  .EXAMPLE 1
  Move a static string before anchor (consider file items only):
 
  gci ... | Rename-Many -File -Pattern 'data' -Anchor 'loopz' -Relation 'before'
 
  .EXAMPLE 2
  Move last occurrence of whole-word static string before anchor:
 
  gci ... | Rename-Many -Pattern 'data',l -Anchor 'loopz' -Relation 'before' -Whole p
 
  .EXAMPLE 3
  Move a static string before anchor and drop (consider Directory items only):
 
  gci ... | Rename-Many -Directory -Pattern 'data' -Anchor 'loopz' -Relation 'before' -Drop '-'
 
  .EXAMPLE 4
  Move a static string before anchor and drop (consider Directory items only), if anchor
  does not match, move the pattern match to end:
 
  gci ... | Rename-Many -Directory -Pattern 'data' -AnchorEnd 'loopz' -Relation 'before' -Drop '-'
 
  .EXAMPLE 5
  Move a static string to start and drop (consider Directory items only):
 
  gci ... | Rename-Many -Directory -Pattern 'data' -Start -Drop '-'
 
  .EXAMPLE 6
  Move a match before anchor:
 
  gci ... | Rename-Many -Pattern '\d{2}-data' -Anchor 'loopz' -Relation 'before'
 
  .EXAMPLE 7
  Move last occurrence of whole-word static string before anchor:
 
  gci ... | Rename-Many -Pattern '\d{2}-data',l -Anchor 'loopz' -Relation 'before' -Whole p
 
  .EXAMPLE 8
  Move a match before anchor and drop:
 
  gci ... | Rename-Many -Pattern '\d{2}-data' -Anchor 'loopz' -Relation 'before' -Drop '-'
 
  * UPDATE EXAMPLES (Paste)
 
  .EXAMPLE 9
  Update last occurrence of whole-word static string using $Paste:
 
  gci ... | Rename-Many -Pattern 'data',l -Whole p -Paste '_info_'
 
  .EXAMPLE 10
  Update a static string using $Paste:
 
  gci ... | Rename-Many -Pattern 'data' -Paste '_info_'
 
  .EXAMPLE 11
  Update 2nd occurrence of whole-word match using $Paste and preserve anchor:
 
  gci ... | Rename-Many -Pattern '\d{2}-data', l -Paste '${_a}_info_'
 
  .EXAMPLE 12
  Update match contain named capture group using $Paste and preserve the anchor:
 
  gci ... | Rename-Many -Pattern (?<day>\d{2})-(?<mon>\d{2})-(?<year>\d{2})
    -Paste '(${year})-(${mon})-(${day}) ${_a}'
 
  .EXAMPLE 13
  Update match contain named capture group using $Paste and preserve the anchor and copy
  whole last occurrence:
 
  gci ... | Rename-Many -Pattern (?<day>\d{2})-(?<mon>\d{2})-(?<year>\d{2})
    -Copy '[A-Z]{3}',l -Whole c -Paste 'CCY_${_c} (${year})-(${mon})-(${day}) ${_a}'
 
  * CUT EXAMPLES (Cut)
 
  .EXAMPLE 14
  Cut a literal token:
 
  gci ... | Rename-Many -Cut 'data'
 
  .EXAMPLE 15
  Cut last occurrence of literal token:
 
  gci ... | Rename-Many -Cut, l 'data'
 
  .EXAMPLE 16
  Cut the second 2 digit sequence:
 
  gci ... | Rename-Many -Cut, 2 '\d{2}'
 
  * APPENDAGE EXAMPLES
 
  .EXAMPLE 17
  Prefix items with fixed token:
 
  gci ... | Rename-Many -Prepend 'begin_'
 
  .EXAMPLE 18
  Append fixed token to items:
 
  gci ... | Rename-Many -Append '_end'
 
  #>

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '',
    Justification = 'WhatIf IS accessed and passed into Exchange')]
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '')]
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')]
  [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'UpdateInPlace')]
  [Alias('remy')]
  param
  (
    [Parameter(Mandatory, ValueFromPipeline = $true)]
    [System.IO.FileSystemInfo]$underscore,

    [Parameter(ParameterSetName = 'HybridStart', Mandatory, Position = 1)]
    [Parameter(ParameterSetName = 'HybridEnd', Mandatory, Position = 1)]
    [Parameter(ParameterSetName = 'MoveToAnchor', Mandatory, Position = 1)]
    [Parameter(ParameterSetName = 'UpdateInPlace', Mandatory, Position = 1)]
    [Parameter(ParameterSetName = 'MoveToStart', Mandatory, Position = 1)]
    [Parameter(ParameterSetName = 'MoveToEnd', Mandatory, Position = 1)]
    [ValidateScript( { { $(test-ValidPatternArrayParam -Arg $_ -AllowWildCard ) } })]
    [Alias('p')]
    [array]$Pattern,

    [Parameter(ParameterSetName = 'MoveToAnchor', Mandatory, Position = 2)]
    [ValidateScript( { $(test-ValidPatternArrayParam -Arg $_) })]
    [Alias('a')]
    [array]$Anchor,

    [Parameter(ParameterSetName = 'HybridStart', Mandatory, Position = 2)]
    [ValidateScript( { $(test-ValidPatternArrayParam -Arg $_) })]
    [Alias('as')]
    [array]$AnchorStart,

    [Parameter(ParameterSetName = 'HybridEnd', Mandatory, Position = 2)]
    [ValidateScript( { $(test-ValidPatternArrayParam -Arg $_) })]
    [Alias('ae')]
    [array]$AnchorEnd,

    [Parameter(ParameterSetName = 'HybridStart')]
    [Parameter(ParameterSetName = 'HybridEnd')]
    [Parameter(ParameterSetName = 'MoveToAnchor')]
    [ValidateSet('before', 'after')]
    [Alias('r')]
    [string]$Relation = 'after',

    [Parameter(ParameterSetName = 'HybridStart')]
    [Parameter(ParameterSetName = 'HybridEnd')]
    [Parameter(ParameterSetName = 'MoveToAnchor')]
    [Parameter(ParameterSetName = 'UpdateInPlace')]
    [Parameter(ParameterSetName = 'Prefix')]
    [Parameter(ParameterSetName = 'Affix')]
    [Parameter(ParameterSetName = 'MoveToStart')]
    [Parameter(ParameterSetName = 'MoveToEnd')]
    [ValidateScript( { { $(test-ValidPatternArrayParam -Arg $_) } })]
    [Alias('co')]
    [array]$Copy,

    [Parameter(ParameterSetName = 'HybridStart')]
    [Parameter(ParameterSetName = 'HybridEnd')]
    [Parameter(ParameterSetName = 'MoveToAnchor', Position = 3)]
    [Parameter(ParameterSetName = 'MoveToStart', Position = 2)]
    [Parameter(ParameterSetName = 'MoveToEnd', Position = 2)]
    [Parameter(ParameterSetName = 'UpdateInPlace', Position = 2)]
    [Alias('w')]
    [string]$With,

    [Parameter(ParameterSetName = 'MoveToStart', Mandatory)]
    [Alias('s')]
    [switch]$Start,

    [Parameter(ParameterSetName = 'MoveToEnd', Mandatory)]
    [Alias('e')]
    [switch]$End,

    [Parameter(ParameterSetName = 'UpdateInPlace', Mandatory)]
    [Alias('ps')]
    [string]$Paste,

    [Parameter(ParameterSetName = 'HybridStart')]
    [Parameter(ParameterSetName = 'HybridEnd')]
    [Parameter(ParameterSetName = 'MoveToAnchor')]
    [Parameter(ParameterSetName = 'MoveToStart')]
    [Parameter(ParameterSetName = 'MoveToEnd')]
    [Alias('dr')]
    [string]$Drop,

    [Parameter(ParameterSetName = 'NoReplacement', Mandatory)]
    [array]$Cut,

    [Parameter(ParameterSetName = 'Prefix', Mandatory)]
    [Alias('pr')]
    [string]$Prepend,

    [Parameter(ParameterSetName = 'Affix', Mandatory)]
    [Alias('ap')]
    [string]$Append,

    # Defining parameter sets for File and Directory, just to ensure both of these switches
    # are mutually exclusive makes the whole parameter set definition exponentially more
    # complex. It's easier just to enforce this with a ValidateScript.
    #
    [Parameter()]
    [Alias('f')]
    [ValidateScript( { -not($PSBoundParameters.ContainsKey('Directory')); })]
    [switch]$File,

    [Parameter()]
    [Alias('d')]
    [ValidateScript( { -not($PSBoundParameters.ContainsKey('File')); })]
    [switch]$Directory,

    [Parameter()]
    [Alias('x')]
    [string]$Except = [string]::Empty,

    [Parameter()]
    [Alias('i')]
    [string]$Include,

    [Parameter()]
    [ValidateSet('p', 'a', 'c', 'i', 'x', 'u', '*')]
    [Alias('wh')]
    [string]$Whole,

    [Parameter()]
    [scriptblock]$Condition = ( { return $true; }),

    [Parameter()]
    [Alias('t')]
    [ValidateScript( { $_ -gt 0 } )]
    [int]$Top,

    [Parameter(ParameterSetName = 'Transformer', Mandatory)]
    [scriptblock]$Transform,

    [Parameter()]
    [PSCustomObject]$Context = $RexFs.Defaults.Remy.Context,

    [Parameter()]
    [Alias('dg')]
    [switch]$Diagnose,

    [Parameter()]
    [switch]$Test
  )

  begin {
    Write-Debug ">>> Rename-Many [ParameterSet: '$($PSCmdlet.ParameterSetName)]' >>>";

    function get-fixedIndent {
      [OutputType([int])]
      param(
        [Parameter()]
        [hashtable]$Theme,

        [Parameter()]
        [string]$Message = [string]::Empty
      )
      [int]$indent = $Message.Length;

      # 1 2 3 4
      # 1234567890123456789012345678901234567890
      # [🏷️] Rename Item // ["No" => " 1",
      # |<-- fixed bit -->|
      #
      $indent += $Theme['MESSAGE-SUFFIX'].Length;
      $indent += $Theme['OPEN'].Length;
      $indent += $Theme['FORMAT'].Replace($Theme['KEY-PLACE-HOLDER'], "No").Replace(
        $Theme['VALUE-PLACE-HOLDER'], '999').Length;
      $indent += $Theme['SEPARATOR'].Length;
      return $indent;
    }

    function use-actionParams {
      [OutputType([hashtable])]
      param(
        [Parameter(Mandatory)]
        [hashtable]$exchange,

        [Parameter(Mandatory)]
        $endAdapter
      )
      [string]$action = $exchange["$($Remy_EXS).ACTION"];
      [boolean]$diagnose = ($exchange.ContainsKey('LOOPZ.DIAGNOSE') -and
        $exchange['LOOPZ.DIAGNOSE']);
      [string]$adjustedName = $endAdapter.GetAdjustedName();

      # To do, make the action applicable in all modes
      # We need extra actions:
      # + Cut (handled by move-match)
      # + Add-Appendage append/prepend
      #
      [hashtable]$actionParameters = if ($action -eq 'Add-Appendage') {
        # Bind append/prepend action parameters
        #
        [hashtable]$_params = @{
          'Value'     = $adjustedName;
          'Appendage' = $exchange["$($Remy_EXS).APPENDAGE"];
          'Type'      = $exchange["$($Remy_EXS).APPENDAGE.TYPE"];
        }

        $_params;
      }
      else {
        # Bind move/update/(cut/transform) action parameters
        #
        [hashtable]$_params = @{
          'Value' = $adjustedName;
        }

        if ($exchange.ContainsKey("$($Remy_EXS).PATTERN-REGEX")) {
          $_params['Pattern'] = $exchange["$($Remy_EXS).PATTERN-REGEX"];

          $_params['PatternOccurrence'] = $exchange.ContainsKey("$($Remy_EXS).PATTERN-OCC") `
            ? $exchange["$($Remy_EXS).PATTERN-OCC"] : 'f';
        }
        elseif ($exchange.ContainsKey("$($Remy_EXS).CUT-REGEX")) {
          $_params['Cut'] = $exchange["$($Remy_EXS).CUT-REGEX"];

          $_params['CutOccurrence'] = $exchange.ContainsKey("$($Remy_EXS).CUT-OCC") `
            ? $exchange["$($Remy_EXS).CUT-OCC"] : 'f';
        }
        elseif ($exchange.ContainsKey("$($Remy_EXS).TRANSFORM")) {
          $_params['Exchange'] = $exchange;
        }

        if ($action -eq 'Move-Match') {
          if ($exchange.ContainsKey("$($Remy_EXS).ANCHOR.REGEX")) {
            $_params['Anchor'] = $exchange["$($Remy_EXS).ANCHOR.REGEX"];
          }
          if ($exchange.ContainsKey("$($Remy_EXS).ANCHOR-OCC")) {
            $_params['AnchorOccurrence'] = $exchange["$($Remy_EXS).ANCHOR-OCC"];
          }

          if ($exchange.ContainsKey("$($Remy_EXS).DROP")) {
            $_params['Drop'] = $exchange["$($Remy_EXS).DROP"];
            $_params['Marker'] = $exchange["$($Remy_EXS).MARKER"];
          }

          switch ($exchange["$($Remy_EXS).ANCHOR-TYPE"]) {
            'MATCHED-ITEM' {
              if ($exchange.ContainsKey("$($Remy_EXS).RELATION")) {
                $_params['Relation'] = $exchange["$($Remy_EXS).RELATION"];
              }
              break;
            }
            'HYBRID-START' {
              if ($exchange.ContainsKey("$($Remy_EXS).RELATION")) {
                $_params['Relation'] = $exchange["$($Remy_EXS).RELATION"];
              }
              $_params['Start'] = $true;
              break;
            }
            'HYBRID-END' {
              if ($exchange.ContainsKey("$($Remy_EXS).RELATION")) {
                $_params['Relation'] = $exchange["$($Remy_EXS).RELATION"];
              }
              $_params['End'] = $true;
              break;
            }
            'START' {
              $_params['Start'] = $true;
              break;
            }
            'END' {
              $_params['End'] = $true;
              break;
            }
            'CUT' {
              # no op
              break;
            }
            default {
              throw "doRenameFsItems: encountered Invalid '$($Remy_EXS).ANCHOR-TYPE': '$AnchorType'";
            }
          }
        } # $action

        $_params;
      }

      # Bind generic action parameters
      #
      if ($diagnose) {
        $actionParameters['Diagnose'] = $exchange['LOOPZ.DIAGNOSE'];
      }

      if ($exchange.ContainsKey("$($Remy_EXS).COPY.REGEX")) {
        $actionParameters['Copy'] = $exchange["$($Remy_EXS).COPY.REGEX"];

        if ($exchange.ContainsKey("$($Remy_EXS).COPY-OCC")) {
          $actionParameters['CopyOccurrence'] = $exchange["$($Remy_EXS).COPY-OCC"];
        }
      }

      if ($exchange.ContainsKey("$($Remy_EXS).WITH")) {
        $actionParameters['With'] = $exchange["$($Remy_EXS).WITH"];
      }

      if ($exchange.ContainsKey("$($Remy_EXS).PASTE")) {
        $actionParameters['Paste'] = $exchange["$($Remy_EXS).PASTE"];
      }

      return $actionParameters;
    } # use-actionParams

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

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

        [Parameter(Mandatory)]
        [hashtable]$_exchange,

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

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

      $endAdapter = New-EndAdapter($_underscore);

      [string]$action = $_exchange["$($Remy_EXS).ACTION"];
      [boolean]$performDiagnosis = ($_exchange.ContainsKey('LOOPZ.DIAGNOSE') -and
        $_exchange['LOOPZ.DIAGNOSE']);

      # ------------------------------------------ [ Bind action parameters ] ---
      #
      [hashtable]$actionParameters = use-actionParams -exchange $_exchange -endAdapter $endAdapter;

      # -------------------------------------------------- [ Execute action ] ---
      #
      [line]$properties = [line]::new();
      [line[]]$lines = @();
      [hashtable]$signals = $_exchange['LOOPZ.SIGNALS'];
      [string]$errorReason = [string]::Empty;

      try {
        [PSCustomObject]$actionResult = convert-ActionResult -Result $(& $action @actionParameters);
        [string]$newItemName = $actionResult.Success ? $actionResult.Payload : $_underscore.Name;

        if ($actionResult.Success) {
          [string]$newItemName = $actionResult.Payload;
        }
        else {
          [string]$newItemName = $_underscore.Name;
          $errorReason = $actionResult.FailedReason;
        }
      }
      catch {
        [string]$newItemName = $_underscore.Name;
        $errorReason = invoke-HandleError -message $_.Exception.Message -prefix 'Action';

        [PSCustomObject]$actionResult = [PSCustomObject]@{
          FailedReason = $errorReason;
          Success      = $false;
        }
      }

      $postResult = invoke-PostProcessing -InputSource $newItemName -Rules $RexFs.Rules.Remy `
        -Signals $signals;

      if ($postResult.Modified) {
        [couplet]$postSignal = Get-FormattedSignal -Name 'REMY.POST' `
          -Signals $signals -Value $postResult.Indication -CustomLabel $postResult.Label;
        $properties.append($postSignal);
        $newItemName = $postResult.TransformResult;
      }

      $newItemName = $endAdapter.GetNameWithExtension($newItemName);
      Write-Debug "Rename-Many; New Item Name: '$newItemName'";

      # -------------------------------------------------- [ Perform Rename ] ---
      #
      [string]$parent = $itemIsDirectory ? $_underscore.Parent.FullName : $_underscore.Directory.FullName;
      [boolean]$nameHasChanged = -not($_underscore.Name -ceq $newItemName);
      [string]$newItemFullPath = Join-Path -Path $parent -ChildPath $newItemName;
      [boolean]$clash = (Test-Path -LiteralPath $newItemFullPath) -and $nameHasChanged;
      [boolean]$trigger = $false;
      [boolean]$whatIf = $_exchange.ContainsKey('WHAT-IF') -and ($_exchange['WHAT-IF']);

      if ($nameHasChanged -and -not($clash) -and [string]::IsNullOrEmpty($errorReason)) {
        try {
          $product = rename-FsItem -From $_underscore -To $newItemName -WhatIf:$whatIf -UndoOperant $operant;

          if ($null -ne $product) {
            # [UndoRename]
            [object]$operant = $_exchange.ContainsKey("$($Remy_EXS).UNDO") `
              ? $_exchange["$($Remy_EXS).UNDO"] : $null;
            $trigger = $true;
          }
          else {
            $product = $newItemName;
            $errorReason = 'Failed (Possible Access Denied)';
          }
        }
        catch {
          $product = $newItemName;
          $errorReason = invoke-HandleError -message $_.Exception.Message -prefix 'Rename';
        }
      }
      else {
        $product = $_underscore;
      }

      # -------------------------------------------- [ Prepare Display Info ] ---
      #
      [string]$fileSystemItemType = $itemIsDirectory ? 'Directory' : 'File';

      [PSCustomObject]$context = $_exchange["$($Remy_EXS).CONTEXT"];
      [int]$maxItemMessageSize = $_exchange["$($Remy_EXS).MAX-ITEM-MESSAGE-SIZE"];
      [string]$normalisedItemMessage = $Context.ItemMessage.replace(
        $Loopz.FsItemTypePlaceholder, $fileSystemItemType);

      [string]$messageLabel = if ($context.psobject.properties.match('ItemMessage') -and `
          -not([string]::IsNullOrEmpty($Context.ItemMessage))) {

        Get-PaddedLabel -Label $($Context.ItemMessage.replace(
            $Loopz.FsItemTypePlaceholder, $fileSystemItemType)) -Width $maxItemMessageSize;
      }
      else {
        $normalisedItemMessage;
      }

      [string]$signalName = $itemIsDirectory ? 'DIRECTORY-A' : 'FILE-A';
      [string]$message = Get-FormattedSignal -Name $signalName `
        -Signals $signals -CustomLabel $messageLabel -Format ' [{1}] {0}';

      [int]$magic = 5;
      [int]$indent = $_exchange["$($Remy_EXS).FIXED-INDENT"] + $message.Length - $magic;
      $_exchange['LOOPZ.WH-FOREACH-DECORATOR.INDENT'] = $indent;
      $_exchange['LOOPZ.WH-FOREACH-DECORATOR.MESSAGE'] = $message;
      $_exchange['LOOPZ.WH-FOREACH-DECORATOR.PRODUCT-LABEL'] = $(Get-PaddedLabel -Label $(
          $fileSystemItemType) -Width 9);

      if (-not([string]::IsNullOrEmpty($errorReason))) {
        $null = $lines += (New-Line(
            New-Pair(@($_exchange["$($Remy_EXS).FROM-LABEL"], $_underscore.Name))
          ));

        [couplet]$errorSignal = Get-FormattedSignal -Name 'BAD-A' `
          -Signals $signals -CustomLabel 'Error' -Value $errorReason;

        $errorSignal.Affirm = $true;
        $null = $lines += (New-Line(
            $errorSignal
          ));
      }
      elseif ($trigger) {
        $null = $lines += (New-Line(
            New-Pair(@($_exchange["$($Remy_EXS).FROM-LABEL"], $_underscore.Name))
          ));
      }
      else {
        if ($clash) {
          Write-Debug "!!! doRenameFsItems; path: '$newItemFullPath' already exists, rename skipped";
          [couplet]$clashSignal = Get-FormattedSignal -Name 'CLASH' `
            -Signals $signals -EmojiAsValue -EmojiOnlyFormat '{0}';
          $properties.append($clashSignal);
        }
        else {
          [couplet]$notActionedSignal = Get-FormattedSignal -Name 'NOT-ACTIONED' `
            -Signals $signals -EmojiAsValue -CustomLabel 'Not Renamed' -EmojiOnlyFormat '{0}';
          $properties.append($notActionedSignal);

          [string]$reason = Get-PsObjectField -Object $actionResult -Field 'FailedReason'
          if (-not([string]::IsNullOrEmpty($reason))) {
            [couplet]$becauseSignal = Get-FormattedSignal -Name 'BECAUSE' `
              -Signals $signals -Value $reason;
            $properties.append($becauseSignal);
          }
          elseif (-not($nameHasChanged)) {
            [couplet]$becauseSignal = Get-FormattedSignal -Name 'BECAUSE' `
              -Signals $signals -Value 'Unchanged';
            $properties.append($becauseSignal);
          }
        }
      }

      if ($whatIf) {
        [couplet]$whatIfSignal = Get-FormattedSignal -Name 'WHAT-IF' `
          -Signals $signals -EmojiAsValue -EmojiOnlyFormat '{0}';
        $properties.append($whatIfSignal);
      }

      # -------------------------------------------------- [ Do diagnostics ] ---
      #
      if ($performDiagnosis -and $actionResult.Diagnostics.Named -and
        ($actionResult.Diagnostics.Named.PSBase.Count -gt 0)) {

        [string]$diagnosticEmoji = Get-FormattedSignal -Name 'DIAGNOSTICS' -Signals $signals `
          -EmojiOnly;

        [string]$captureEmoji = Get-FormattedSignal -Name 'CAPTURE' -Signals $signals `
          -EmojiOnly -EmojiOnlyFormat '[{0}]';

        foreach ($namedItem in $actionResult.Diagnostics.Named) {
          foreach ($namedKey in $namedItem.Keys) {
            [hashtable]$groups = $actionResult.Diagnostics.Named[$namedKey];
            [string[]]$diagnosticLines = @();

            foreach ($groupName in $groups.Keys) {
              [string]$captured = $groups[$groupName];
              [string]$compoundValue = "({0} <{1}>)='{2}'" -f $captureEmoji, $groupName, $captured;
              [string]$namedLabel = Get-PaddedLabel -Label ($diagnosticEmoji + $namedKey);

              $diagnosticLines += $compoundValue;
            }
            $null = $lines += (New-Line(
                New-Pair(@($namedLabel, $($diagnosticLines -join ', ')))
              ));
          }
        }
      }

      # -------------------------------------------------- [ Compose Result ] ---
      #
      [PSCustomObject]$result = [PSCustomObject]@{
        Product = $product;
      }

      $result | Add-Member -MemberType NoteProperty -Name 'Pairs' -Value $properties;

      if ($lines.Length -gt 0) {
        $result | Add-Member -MemberType NoteProperty -Name 'Lines' -Value $lines;
      }

      if ($trigger) {
        $result | Add-Member -MemberType NoteProperty -Name 'Trigger' -Value $true;
      }

      [boolean]$differsByCaseOnly = $newItemName.ToLower() -eq $_underscore.Name.ToLower();
      [boolean]$affirm = $trigger -and ($product) -and -not($differsByCaseOnly);
      if ($affirm) {
        $result | Add-Member -MemberType NoteProperty -Name 'Affirm' -Value $true;
      }

      if (-not([string]::IsNullOrEmpty($errorReason))) {
        $result | Add-Member -MemberType NoteProperty -Name 'ErrorReason' -Value $errorReason;
      }

      return $result;
    } # doRenameFsItems

    [scriptblock]$getResult = {
      param($result)

      $result.GetType() -in @([System.IO.FileInfo], [System.IO.DirectoryInfo]) ? $result.Name : $result;
    }

    [System.IO.FileSystemInfo[]]$collection = @();

    [Krayon]$_krayon = Get-Krayon
    [Scribbler]$_scribbler = New-Scribbler -Krayon $_krayon -Test:$Test.IsPresent;
  } # begin

  process {
    Write-Debug "=== Rename-Many [$($underscore.Name)] ===";

    $collection += $underscore;
  }

  end {
    Write-Debug '<<< Rename-Many <<<';

    # ------------------------------------------------------ [ Init Phase ] ---
    #
    [hashtable]$signals = $(Get-Signals);
    [hashtable]$theme = $_scribbler.Krayon.Theme;
    [boolean]$locked = Get-IsLocked -Variable $(
      [string]::IsNullOrEmpty($Context.Locked) ? 'REXFS_REMY_LOCKED' : $Context.Locked
    );

    [string]$title = $Context.psobject.properties.match('Title') -and `
      -not([string]::IsNullOrEmpty($Context.Title)) `
      ? $Context.Title : 'Rename';

    if ($locked) {
      $title = Get-FormattedSignal -Name 'LOCKED' -Signals $signals `
        -Format '{1} {0} {1}' -CustomLabel $('Locked: ' + $title);
    }

    [boolean]$whatIf = $PSBoundParameters.ContainsKey('WhatIf') -or $locked;
    [PSCustomObject]$containers = [PSCustomObject]@{
      Wide  = [line]::new();
      Props = [line]::new();
    }

    [int]$maxItemMessageSize = $Context.ItemMessage.replace(
      $Loopz.FsItemTypePlaceholder, 'Directory').Length;

    [string]$summaryMessage = $Context.psobject.properties.match('SummaryMessage') -and `
      -not([string]::IsNullOrEmpty($Context.SummaryMessage)) `
      ? $Context.SummaryMessage : 'Rename Summary';

    $summaryMessage = Get-FormattedSignal -Name 'SUMMARY-A' -Signals $signals -CustomLabel $summaryMessage;

    [hashtable]$rendezvous = @{
      'LOOPZ.SCRIBBLER'                       = $_scribbler;
      'LOOPZ.SIGNALS'                         = $signals;

      'LOOPZ.WH-FOREACH-DECORATOR.BLOCK'      = $doRenameFsItems;
      'LOOPZ.WH-FOREACH-DECORATOR.GET-RESULT' = $getResult;

      'LOOPZ.HEADER-BLOCK.CRUMB-SIGNAL'       = 'CRUMB-A';
      'LOOPZ.HEADER-BLOCK.LINE'               = $LoopzUI.DashLine;
      'LOOPZ.HEADER-BLOCK.MESSAGE'            = $title;

      'LOOPZ.SUMMARY-BLOCK.LINE'              = $LoopzUI.EqualsLine;
      'LOOPZ.SUMMARY-BLOCK.MESSAGE'           = $summaryMessage;

      "$($Remy_EXS).CONTEXT"                  = $Context;
      "$($Remy_EXS).MAX-ITEM-MESSAGE-SIZE"    = $maxItemMessageSize;
      "$($Remy_EXS).FIXED-INDENT"             = get-fixedIndent -Theme $theme;
      "$($Remy_EXS).FROM-LABEL"               = Get-PaddedLabel -Label 'From' -Width 9;

      "$($Remy_EXS).USER-PARAMS"              = $PSBoundParameters;
    }

    [string]$adjustedWhole = if ($PSBoundParameters.ContainsKey('Whole')) {
      $Whole.ToLower();
    }
    else {
      [string]::Empty;
    }

    [PSCustomObject]$bootStrapOptions = [PSCustomObject]@{};
    if (-not([string]::IsNullOrEmpty($adjustedWhole))) {
      $bootStrapOptions | Add-Member -MemberType NoteProperty -Name 'Whole' -Value $adjustedWhole;
    }

    [BootStrap]$bootStrap = New-BootStrap `
      -Exchange $rendezvous `
      -Containers $containers `
      -Options $bootStrapOptions;

    # ------------------------------------------------ [ Primary Entities ] ---
    # (Note: Keep Signal Registry up to date)
    #

    # [Pattern]
    #
    [PSCustomObject]$patternSpec = [PSCustomObject]@{
      Activate       = $PSBoundParameters.ContainsKey('Pattern') -and `
        -not([string]::IsNullOrEmpty($Pattern));
      SpecType       = 'regex';
      Name           = 'Pattern';
      Value          = $Pattern;
      Signal         = 'PATTERN';
      WholeSpecifier = 'p';
      RegExKey       = "$($Remy_EXS).PATTERN-REGEX";
      OccurrenceKey  = "$($Remy_EXS).PATTERN-OCC";
    }
    $bootStrap.Register($patternSpec);

    if ($PSBoundParameters.ContainsKey('Pattern') -and -not([string]::IsNullOrEmpty($Pattern))) {
      [string]$patternExpression, [string]$patternOccurrence = Resolve-PatternOccurrence $Pattern
    }

    # [Anchor]
    #
    [PSCustomObject]$anchorSpec = [PSCustomObject]@{
      Activate       = $PSBoundParameters.ContainsKey('Anchor') -and `
        -not([string]::IsNullOrEmpty($Anchor));
      SpecType       = 'regex';
      Name           = 'Anchor';
      Value          = $Anchor;
      Signal         = 'REMY.ANCHOR';
      WholeSpecifier = 'a';
      RegExKey       = "$($Remy_EXS).ANCHOR.REGEX";
      OccurrenceKey  = "$($Remy_EXS).ANCHOR-OCC";
      Keys           = @{
        "$($Remy_EXS).ACTION"      = 'Move-Match';
        "$($Remy_EXS).ANCHOR-TYPE" = 'MATCHED-ITEM';
      }
    }
    $bootStrap.Register($anchorSpec);

    # [AnchorStart]
    #
    [PSCustomObject]$anchorStartSpec = [PSCustomObject]@{
      Activate       = $PSBoundParameters.ContainsKey('AnchorStart') -and `
        -not([string]::IsNullOrEmpty($AnchorStart));
      SpecType       = 'regex';
      Name           = 'AnchorStart';
      Value          = $AnchorStart;
      Signal         = 'REMY.ANCHOR';
      WholeSpecifier = 'a';
      RegExKey       = "$($Remy_EXS).ANCHOR.REGEX";
      OccurrenceKey  = "$($Remy_EXS).ANCHOR-OCC";
      Keys           = @{
        "$($Remy_EXS).ACTION"      = 'Move-Match';
        "$($Remy_EXS).ANCHOR-TYPE" = 'HYBRID-START';
      }
    }
    $bootStrap.Register($anchorStartSpec);

    # [AnchorEnd]
    #
    [PSCustomObject]$anchorEndSpec = [PSCustomObject]@{
      Activate       = $PSBoundParameters.ContainsKey('AnchorEnd') -and `
        -not([string]::IsNullOrEmpty($AnchorEnd));
      SpecType       = 'regex';
      Name           = 'AnchorEnd';
      Value          = $AnchorEnd;
      Signal         = 'REMY.ANCHOR';
      WholeSpecifier = 'a';
      RegExKey       = "$($Remy_EXS).ANCHOR.REGEX";
      OccurrenceKey  = "$($Remy_EXS).ANCHOR-OCC";
      Keys           = @{
        "$($Remy_EXS).ACTION"      = 'Move-Match';
        "$($Remy_EXS).ANCHOR-TYPE" = 'HYBRID-END';
      }
    }
    $bootStrap.Register($anchorEndSpec);

    # [Copy]
    #
    [PSCustomObject]$copySpec = [PSCustomObject]@{
      Activate       = $PSBoundParameters.ContainsKey('Copy') -and `
        -not([string]::IsNullOrEmpty($Copy));
      SpecType       = 'regex';
      Name           = 'Copy';
      Value          = $Copy;
      Signal         = 'COPY-A';
      WholeSpecifier = 'c';
      RegExKey       = "$($Remy_EXS).COPY.REGEX";
      OccurrenceKey  = "$($Remy_EXS).COPY-OCC";
    }
    $bootStrap.Register($copySpec);

    # [With]
    #
    [PSCustomObject]$withSpec = [PSCustomObject]@{
      Activate    = $PSBoundParameters.ContainsKey('With') -and `
        -not([string]::IsNullOrEmpty($With));
      SpecType    = 'formatter';
      Name        = 'With';
      Value       = $With;
      Signal      = 'WITH';
      SignalValue = $With;
      Keys        = @{
        "$($Remy_EXS).WITH" = $With;
      }
    }
    $bootStrap.Register($withSpec);

    # [Include]
    #
    [PSCustomObject]$includeSpec = [PSCustomObject]@{
      Activate      = $PSBoundParameters.ContainsKey('Include') -and `
        -not([string]::IsNullOrEmpty($Include));
      SpecType      = 'regex';
      Name          = 'Include';
      Value         = $Include;
      Signal        = 'INCLUDE';
      RegExKey      = "$($Remy_EXS).INCLUDE.REGEX";
      OccurrenceKey = "$($Remy_EXS).INCLUDE-OCC";
    }
    $bootStrap.Register($includeSpec);

    # [Except]
    #
    [PSCustomObject]$exceptSpec = [PSCustomObject]@{
      Activate      = $PSBoundParameters.ContainsKey('Except') -and `
        -not([string]::IsNullOrEmpty($Except));
      SpecType      = 'regex';
      Name          = 'Except';
      Value         = $Except;
      Signal        = 'EXCLUDE';
      RegExKey      = "$($Remy_EXS).EXCLUDE.REGEX";
      OccurrenceKey = "$($Remy_EXS).EXCLUDE-OCC";
    }
    $bootStrap.Register($exceptSpec);

    # [Diagnose]
    #
    [PSCustomObject]$diagnoseSpec = [PSCustomObject]@{
      Activate    = $Diagnose.ToBool();
      SpecType    = 'signal';
      Name        = 'Diagnose';
      Value       = $true;
      Signal      = 'DIAGNOSTICS';
      SignalValue = $('[{0}]' -f $signals['SWITCH-ON'].Value);
      Force       = 'Props';
    }
    $bootStrap.Register($diagnoseSpec);

    # [Paste]
    #
    [PSCustomObject]$pasteSpec = [PSCustomObject]@{
      Activate    = $PSBoundParameters.ContainsKey('Paste') -and `
        -not([string]::IsNullOrEmpty($Paste));
      Name        = 'Paste';
      Value       = $Paste;
      SpecType    = 'formatter';
      Signal      = 'PASTE-A';
      SignalValue = $Paste;
      Keys        = @{
        "$($Remy_EXS).PASTE" = $Paste;
      }
    }
    $bootStrap.Register($pasteSpec);

    # [Append]
    #
    [PSCustomObject]$appendSpec = [PSCustomObject]@{
      Activate    = $PSBoundParameters.ContainsKey('Append') -and `
        -not([string]::IsNullOrEmpty($Append));
      Name        = 'Append';
      Value       = $Append;
      SpecType    = 'formatter';
      Signal      = 'APPEND';
      SignalValue = $Append;
      Keys        = @{
        "$($Remy_EXS).APPENDAGE"      = $Append;
        "$($Remy_EXS).ACTION"         = 'Add-Appendage';
        "$($Remy_EXS).APPENDAGE.TYPE" = 'Append';
      }
    }
    $bootStrap.Register($appendSpec);

    # [Prepend]
    #
    [PSCustomObject]$prependSpec = [PSCustomObject]@{
      Activate    = $PSBoundParameters.ContainsKey('Prepend') -and `
        -not([string]::IsNullOrEmpty($Prepend));
      Name        = 'Prepend';
      Value       = $Prepend;
      SpecType    = 'formatter';
      Signal      = 'PREPEND';
      SignalValue = $Prepend;
      Keys        = @{
        "$($Remy_EXS).APPENDAGE"      = $Prepend;
        "$($Remy_EXS).ACTION"         = 'Add-Appendage';
        "$($Remy_EXS).APPENDAGE.TYPE" = 'Prepend';
      }
    }
    $bootStrap.Register($prependSpec);

    # [Start]
    #
    [PSCustomObject]$startSpec = [PSCustomObject]@{
      Activate    = $PSBoundParameters.ContainsKey('Start') -and $Start;
      Name        = 'Start';
      SpecType    = 'signal';
      Value       = $true;
      Signal      = 'REMY.ANCHOR';
      CustomLabel = 'Start';
      Force       = 'Props';
      SignalValue = $signals['SWITCH-ON'].Value;
      Keys        = @{
        "$($Remy_EXS).ACTION"      = 'Move-Match';
        "$($Remy_EXS).ANCHOR-TYPE" = 'START';
      };
    }
    $bootStrap.Register($startSpec);

    # [End]
    #
    [PSCustomObject]$endSpec = [PSCustomObject]@{
      Activate    = $PSBoundParameters.ContainsKey('End') -and $End;
      Name        = 'End';
      SpecType    = 'signal';
      Value       = $true;
      Signal      = 'REMY.ANCHOR';
      CustomLabel = 'End';
      Force       = 'Props';
      SignalValue = $signals['SWITCH-ON'].Value;
      Keys        = @{
        "$($Remy_EXS).ACTION"      = 'Move-Match';
        "$($Remy_EXS).ANCHOR-TYPE" = 'END';
      };
    }
    $bootStrap.Register($endSpec);

    # [Drop]
    #
    [PSCustomObject]$dropSpec = [PSCustomObject]@{
      Activate    = $PSBoundParameters.ContainsKey('Drop') -and `
        -not([string]::IsNullOrEmpty($Drop));
      Name        = 'Drop';
      Value       = $Drop;
      SpecType    = 'formatter';
      Signal      = 'REMY.DROP';
      SignalValue = $Drop;
      Force       = 'Wide';
      Keys        = @{
        "$($Remy_EXS).DROP"   = $Drop;
        "$($Remy_EXS).MARKER" = $RexFs.Defaults.Remy.Marker;
      }
    }
    $bootStrap.Register($dropSpec);

    # [Novice]
    #
    [PSCustomObject]$noviceSpec = [PSCustomObject]@{
      Activate    = $locked;
      Name        = 'Novice';
      SpecType    = 'signal';
      Signal      = 'NOVICE';
      SignalValue = $signals['SWITCH-ON'].Value;
      Force       = 'Wide';
    }
    $bootStrap.Register($noviceSpec);

    # [Transform]
    #
    [PSCustomObject]$transformSpec = [PSCustomObject]@{
      Activate    = $PSBoundParameters.ContainsKey('Transform') -and $Transform;
      SpecType    = 'signal';
      Name        = 'Transform';
      Signal      = 'TRANSFORM';
      SignalValue = $signals['SWITCH-ON'].Value;
      Force       = 'Wide';
      Keys        = @{
        "$($Remy_EXS).TRANSFORM" = $Transform;
        "$($Remy_EXS).ACTION"    = 'Invoke-Transform';
      }
    }
    $bootStrap.Register($transformSpec);

    # [Undo]
    #
    [PSCustomObject]$operantOptions = [PSCustomObject]@{
      ShortCode     = $Context.OperantShortCode;
      OperantName   = 'UndoRename';
      Shell         = 'PoShShell';
      BaseFilename  = 'undo-rename';
      DisabledEnVar = $Context.UndoDisabledEnVar;
    }
    # ref: https://stackoverflow.com/questions/36804102/powershell-5-and-classes-cannot-convert-the-x-value-of-type-x-to-type-x#36812564
    # Argh, for some reason strong typing is breaking here:
    # [UndoRename]
    [object]$operant = Initialize-ShellOperant -Options $operantOptions;

    [PSCustomObject]$undoSpec = [PSCustomObject]@{
      Activate    = $true;
      SpecType    = 'signal';
      Name        = 'Undo';
      Signal      = 'REMY.UNDO';
      SignalValue = $($operant ? $operant.Shell.FullPath : $signals['SWITCH-OFF'].Value);
      Force       = 'Wide';
      Keys        = @{
        "$($Remy_EXS).UNDO" = $operant;
      }
    }
    $bootStrap.Register($undoSpec);

    # [Relation]
    #
    [PSCustomObject]$relationSpec = [PSCustomObject]@{
      Activate = $PSBoundParameters.ContainsKey('Relation') -and `
        -not([string]::IsNullOrEmpty($Relation));
      Name     = 'Relation';
      SpecType = 'simple';
      Value    = $Relation;
      Keys     = @{
        "$($Remy_EXS).RELATION" = $Relation;
      }
    }
    $bootStrap.Register($relationSpec);

    # [Cut]
    #
    [PSCustomObject]$cutSpec = [PSCustomObject]@{
      Activate       = $PSBoundParameters.ContainsKey('Cut') -and $Cut;
      SpecType       = 'regex';
      Name           = 'Cut';
      Value          = $Cut;
      Signal         = 'CUT-A';
      WholeSpecifier = 'u';
      RegExKey       = "$($Remy_EXS).CUT-REGEX";
      OccurrenceKey  = "$($Remy_EXS).CUT-OCC";
      Keys           = @{
        "$($Remy_EXS).ACTION"      = 'Move-Match';
        "$($Remy_EXS).ANCHOR-TYPE" = 'CUT';
      }
    }
    $bootStrap.Register($cutSpec);

    # ------------------------------------------------------- [ Relations ] ---
    #

    # [IsMove] (Doesn't need to define the action, its simply a flag. To determine
    # if the operation is a move, can use the presence of this entity as an indicator)
    #
    [PSCustomObject]$isMoveSpec = [PSCustomObject]@{
      Activator = [scriptblock] {
        [OutputType([boolean])]
        param(
          [hashtable]$Entities,
          [hashtable]$Relations
        )

        [boolean]$result = $Entities.ContainsKey('Anchor') -or `
          $Entities.ContainsKey('Start') -or $Entities.ContainsKey('End') -or `
          $Entities.ContainsKey('AnchorStart') -or $Entities.ContainsKey('AnchorEnd');

        return $result;
      }
      Name      = 'IsMove';
      SpecType  = 'simple';
    }

    # [IsUpdate]
    #
    [PSCustomObject]$isUpdateSpec = [PSCustomObject]@{
      Activator = [scriptblock] {
        [OutputType([boolean])]
        param(
          [hashtable]$Entities,
          [hashtable]$Relations
        )
        [boolean]$result = $(-not($Relations.Contains('IsMove')) -and `
            -not($Entities.Contains('Append')) -and `
            -not($Entities.Contains('Prepend')) -and `
            -not($Entities.Contains('Cut')) -and `
            -not($Entities.Contains('Transform'))
        );
        return $result;
      }
      Name      = 'IsUpdate';
      SpecType  = 'simple';
      Keys      = @{
        "$($Remy_EXS).ACTION" = 'Update-Match';
      }
    }

    # Bootstrap ***
    #
    $null = $bootStrap.Build(@($isMoveSpec, $isUpdateSpec));

    # --------------------------------------- [ Bootstrap dependent setup ] ---
    #

    [RegexEntity]$patternEntity = $bootStrap.Get('Pattern');
    if ($bootStrap.Contains('IsMove')) {
      # !!! This is now redundant; replace-all functionality can no longer be invoked.
      # This will be implemented as a separate derivative command that uses Transform.
      #
      if ($patternEntity -and $patternEntity.Occurrence -eq '*') {
        [string]$errorMessage = "'Pattern' wildcard prohibited for move operation (Anchor/Start/End).`r`n";
        $errorMessage += "Please use a digit, 'f' (first) or 'l' (last) for Pattern Occurrence";
        Write-Error $errorMessage -ErrorAction Stop;
      }
    }

    [RegExEntity]$ie = $bootStrap.Get('Include');
    [regex]$includedRegEx = ${ie}?.get_RegEx();

    [RegExEntity]$ee = $bootStrap.Get('Except');
    [regex]$exceptRegEx = ${ee}?.get_RegEx();

    [regex]$patternRegEx = ${patternEntity}?.get_RegEx();

    [scriptblock]$clientCondition = $Condition;
    [scriptblock]$compoundCondition = {
      param(
        [System.IO.FileSystemInfo]$pipelineItem
      )

      [boolean]$clientResult = $clientCondition.InvokeReturnAsIs($pipelineItem);
      [boolean]$isStart = $bootStrap.Contains('Start');
      [boolean]$isEnd = $bootStrap.Contains('End');

      [boolean]$isAlreadyAnchoredAt = if ($isStart -or $isEnd) {
        [hashtable]$anchoredParameters = @{
          'Source'     = $pipelineItem.Name;
          'Expression' = $patternRegEx;
          'Occurrence' = $patternEntity.Occurrence;
        }

        if ($isStart) {
          $anchoredParameters['Start'] = $true;
        }
        else {
          $anchoredParameters['End'] = $true;
        }

        Test-IsAlreadyAnchoredAt @anchoredParameters;
      }
      else {
        $false;
      }

      return $($clientResult -and -not($isAlreadyAnchoredAt));
    } # compoundCondition

    [scriptblock]$invokeCondition = {
      param(
        [System.IO.FileSystemInfo]$pipelineItem
      )
      # Inside the scope of this script block, $Condition is assigned to Invoke-ForeachFsItem's
      # version of the Condition parameter which is this scriptblock and thus results in a stack
      # overflow due to infinite recursion. We need to use a temporary variable so that
      # the client's Condition (Rename-Many) is not accidentally hidden.
      #
      [boolean]$isIncluded = ($null -ne $includedRegEx) ? $includedRegEx.IsMatch($pipelineItem.Name) : $true;
      [boolean]$patternIsMatch = ($null -ne $patternRegEx) ? ($patternRegEx.IsMatch($pipelineItem.Name)) : $true;
      [boolean]$notExcluded = ($null -ne $exceptRegEx) ? -not($exceptRegEx.IsMatch($pipelineItem.Name)) : $true;

      return $patternIsMatch -and $isIncluded -and `
        $notExcluded -and $compoundCondition.InvokeReturnAsIs($pipelineItem);
    }

    # ------------------------------------------------- [ Execution Phase ] ---
    #

    [hashtable]$feParameters = @{
      'Condition' = $invokeCondition;
      'Exchange'  = $rendezvous;
      'Header'    = $LoopzHelpers.HeaderBlock;
      'Summary'   = $LoopzHelpers.SummaryBlock;
      'Block'     = $LoopzHelpers.WhItemDecoratorBlock;
    }

    if ($PSBoundParameters.ContainsKey('File')) {
      $feParameters['File'] = $true;
    }
    elseif ($PSBoundParameters.ContainsKey('Directory')) {
      $feParameters['Directory'] = $true;
    }

    if ($PSBoundParameters.ContainsKey('Top')) {
      $feParameters['Top'] = $Top;
    }

    if ($whatIf -or $Diagnose.ToBool()) {
      $rendezvous['WHAT-IF'] = $true;
    }

    if ($Diagnose.ToBool()) {
      $rendezvous['LOOPZ.DIAGNOSE'] = $true;
    }

    [System.Exception]$deferredException = $null;
    try {
      $null = $collection | Invoke-ForeachFsItem @feParameters;
    }
    catch {
      # ctrl-c doesn't invoke an exception, it just abandons processing,
      # ending up in the finally block.
      #
      $deferredException = $_.Exception;
      Write-Host $_.Exception.StackTrace;
    }
    finally {
      # catch ctrl-c
      if ($operant -and -not($whatIf)) {
        $operant.finalise();
      }
    }

    if ($deferredException) {
      throw $deferredException;
    }
  } # end
} # Rename-Many

function Add-Appendage {
  <#
  .NAME
    Add-Appendage
 
  .SYNOPSIS
    The core appendage action function principally used by Rename-Many. Adds either
  a prefix or suffix to the Value.
 
  .DESCRIPTION
    Returns a new string that reflects the addition of an appendage, which can be Prepend
  or Append. The appendage itself can be static text, or can act like a formatter supporting
  Copy named group reference s, if present. The user can decide to reference the whole Copy
  match with ${_c}, or if it contains named captures, these can be referenced inside the
  appendage as ${<group-name-ref>}
 
  .LINK
    https://eliziumnet.github.io/RexFs/
 
  .PARAMETER Value
    The source value against which regular expressions are applied.
 
  .PARAMETER Appendage
    String to either prepend or append to Value. Supports named captures inside Copy regex
  parameter.
 
  .PARAMETER Type
    Denotes the appendage type, can be 'Prepend' or 'Append'.
 
  .PARAMETER Copy
    Regular expression string applied to $Value, indicating a portion which should be copied and
  inserted into the Appendage. The match defined by $Copy is stored in special variable ${_c} and
  can be referenced as such from $Appendage.
 
  .PARAMETER CopyOccurrence
    Can be a number or the letters f, l
  * f: first occurrence
  * l: last occurrence
  * <number>: the nth occurrence
 
  .PARAMETER Diagnose
    switch parameter that indicates the command should be run in WhatIf mode. When enabled
  it presents additional information that assists the user in correcting the un-expected
  results caused by an incorrect/un-intended regular expression. The current diagnosis
  will show the contents of named capture groups that they may have specified. When an item
  is not renamed (usually because of an incorrect regular expression), the user can use the
  diagnostics along side the 'Not Renamed' reason to track down errors.
 
  #>

  [OutputType([PSCustomObject])]
  param(
    [Parameter(Mandatory)]
    [string]$Value,

    [Parameter(Mandatory)]
    [string]$Appendage,

    [Parameter(Mandatory)]
    [ValidateSet('Prepend', 'Append')]
    [string]$Type,

    [Parameter()]
    [System.Text.RegularExpressions.RegEx]$Copy,

    [Parameter()]
    [ValidateScript( { ($_ -ne '*') -and ($_ -ne '0') })]
    [string]$CopyOccurrence = 'f',

    [Parameter()]
    [switch]$Diagnose
  )

  [string]$failedReason = [string]::Empty;
  [string]$result = [string]::Empty;
  [PSCustomObject]$groups = [PSCustomObject]@{
    Named = @{}
  }

  if ($PSBoundParameters.ContainsKey('Copy')) {
    if ($Value -match $Copy) {
      [string]$appendageContent = $Appendage;
      [hashtable]$parameters = @{
        'Source'       = $Value
        'PatternRegEx' = $Copy
        'Occurrence'   = ($PSBoundParameters.ContainsKey('CopyOccurrence') ? $CopyOccurrence : 'f')
      }

      # With this implementation, it is up to the user to supply a regex proof
      # pattern, so if the Copy contains regex chars which must be treated literally, they
      # must pass in the string pre-escaped: -Copy $(esc('some-pattern') + 'other stuff').
      #
      [string]$capturedCopy, $null, `
        [System.Text.RegularExpressions.Match]$copyMatch = Split-Match @parameters;

      [Hashtable]$copyCaptures = get-Captures -MatchObject $copyMatch;

      if ($Diagnose.ToBool()) {
        $groups.Named['Copy'] = $copyCaptures;
      }
      $appendageContent = $appendageContent.Replace('${_c}', $capturedCopy);

      # Now cross reference the Copy group references
      #
      $appendageContent = Update-GroupRefs -Source $appendageContent -Captures $copyCaptures;

      $result = if ($Type -eq 'Prepend') {
        $($appendageContent + $Value);
      }
      elseif ($Type -eq 'Append') {
        $($Value + $appendageContent);
      }
    }
    else {
      # Copy doesn't match so abort and return unmodified source
      #
      $failedReason = 'Copy Match';
    }
  }
  else {
    $result = if ($Type -eq 'Prepend') {
      $($Appendage + $Value);
    }
    elseif ($Type -eq 'Append') {
      $($Value + $Appendage);
    }
    else {
      throw [System.Management.Automation.MethodInvocationException]::new(
        "Add-Appendage: Invalid Appendage Type: '$Type', Appendage: '$Appendage'");
    }
  }

  [boolean]$success = $([string]::IsNullOrEmpty($failedReason));
  if (-not($success)) {
    $result = $Value;
  }

  [PSCustomObject]$appendageResult = [PSCustomObject]@{
    Payload = $result;
    Success = $success;
  }

  if (-not([string]::IsNullOrEmpty($failedReason))) {
    $appendageResult | Add-Member -MemberType NoteProperty -Name 'FailedReason' -Value $failedReason;
  }

  if ($Diagnose.ToBool() -and ($groups.Named.Count -gt 0)) {
    $appendageResult | Add-Member -MemberType NoteProperty -Name 'Diagnostics' -Value $groups;
  }

  return $appendageResult;
}

function Format-Escape {
  <#
  .NAME
    Format-Escape
 
  .SYNOPSIS
    Escapes the regular expression specified. This is just a wrapper around the
  .net regex::escape method, but gives the user a much easier way to
  invoke it from the command line.
 
  .DESCRIPTION
    Various functions in Loopz have parameters that accept a regular expression. This
  function gives the user an easy way to escape the regex, without them having to do
  this manually themselves which could be tricky to get right depending on their
  requirements. NB: an alternative to using the 'esc' function is to add a ~ to the start
  of the pattern. The tilde is not taken as part of the pattern and is stripped off.
 
  .LINK
    https://eliziumnet.github.io/RexFs/
 
  .PARAMETER Source
    The source string to escape.
 
  .EXAMPLE 1
  Rename-Many -Pattern $(esc('(123)'))
 
  Use the 'esc' alias with the Rename-Many command, escaping the regex characters in the Pattern definition
 
  .EXAMPLE 2
  Rename-Many -Pattern '~(123)'
 
  Use a leading '~' in the pattern definition, to escape the whole value.
 
  .EXAMPLE 3
  Rename-Many -Pattern $('esc(123)' + '_(?<n>\d{3})')
 
  Split the pattern into the parts that need escaping and those that don't. This will
  match
  #>

  [Alias('esc')]
  [OutputType([string])]
  param(
    [Parameter(Position = 0, Mandatory)]$Source
  )
  [regex]::Escape($Source);
}

function Get-RemyExchangeSpace {
  # Really only required for unit tests that interact with the exchange
  # (exchange space is a namespace for the exchange)
  #
  return $Remy_EXS;
}

function Move-Match {
  <#
  .NAME
    Move-Match
 
  .SYNOPSIS
    The core move match action function principally used by Rename-Many. Moves a
  match according to the specified anchor(s).
 
  .DESCRIPTION
    Returns a new string that reflects moving the specified $Pattern match to either
  the location designated by $Anchor/$AnchorOccurrence/$Relation or to the Start or
  End of the value indicated by the presence of the $Start/$End switch parameters.
    First Move-Match, removes the Pattern match from the source. This makes the With and
  Anchor match against the remainder ($patternRemoved) of the source. This way, there is
  no overlap between the Pattern match and With/Anchor and it also makes the functionality more
  understandable for the user. NB: $Pattern only tells you what to remove, but it's the
  $With, $Copy and $Paste that defines what to insert, with the $Anchor/$Start/$End
  defining where the replacement text should go. The user should not be using named capture
  groups in $Copy, or $Anchor, rather, they should be defined inside $Paste and referenced
  inside $Paste/$With.
 
  .LINK
    https://eliziumnet.github.io/RexFs/
 
  .PARAMETER Anchor
    Anchor is a regular expression string applied to $Value (after the $Pattern match has
  been removed). The $Pattern match that is removed is inserted at the position indicated
  by the anchor match in collaboration with the $Relation parameter.
 
  .PARAMETER AnchorOccurrence
    Can be a number or the letters f, l
  * f: first occurrence
  * l: last occurrence
  * <number>: the nth occurrence
 
  .PARAMETER Copy
    Regular expression object applied to $Value (after the $Pattern match has been removed),
  indicating a portion which should be copied and re-inserted (via the $Paste parameter;
  see $Paste or $With). Since this is a regular expression to be used in $Paste/$With, there
  is no value in the user specifying a static pattern, because that static string can just be
  defined in $Paste/$With. The value in the $Copy parameter comes when a generic pattern is
  defined eg \d{3} (is non static), specifies any 3 digits as opposed to say '123', which
  could be used directly in the $Paste/$With parameter without the need for $Copy. The match
  defined by $Copy is stored in special variable ${_c} and can be referenced as such from
  $Paste and $With.
 
  .PARAMETER CopyOccurrence
    Can be a number or the letters f, l
  * f: first occurrence
  * l: last occurrence
  * <number>: the nth occurrence
 
  .PARAMETER Cut
    Regular expression object that indicates which part of the $Value that
  either needs removed as part of overall rename operation. Those characters
  in $Value which match $Cut, are removed.
 
  .PARAMETER CutOccurrence
    Can be a number or the letters f, l
  * f: first occurrence
  * l: last occurrence
  * <number>: the nth occurrence
 
  .PARAMETER Diagnose
    switch parameter that indicates the command should be run in WhatIf mode. When enabled
  it presents additional information that assists the user in correcting the un-expected
  results caused by an incorrect/un-intended regular expression. The current diagnosis
  will show the contents of named capture groups that they may have specified. When an item
  is not renamed (usually because of an incorrect regular expression), the user can use the
  diagnostics along side the 'Not Renamed' reason to track down errors. When $Diagnose has
  been specified, $WhatIf does not need to be specified.
 
  .PARAMETER Drop
    A string parameter (only applicable to move operations, ie any of these Anchor/Star/End
  are present) that defines what text is used to replace the $Pattern match. So in this
  use-case, the user wants to move a particular token/pattern to another part of the name
  and at the same time drop a static string in the place where the $Pattern was removed from.
  The user can also reference named group captures defined inside Pattern or Copy. (Note that
  the whole Copy capture can be referenced with ${_c}.)
 
  .PARAMETER End
    Is another type of anchor used instead of $Anchor and specifies that the $Pattern match
  should be moved to the end of the new name.
 
  .PARAMETER Marker
    A character used to mark the place where the $Pattern was removed from. It should be a
  special character that is not easily typed on the keyboard by the user so as to not
  interfere wth $Anchor/$Copy matches which occur after $Pattern match is removed.
 
  .PARAMETER Pattern
    Regular expression object that indicates which part of the $Value that
  either needs to be moved or replaced as part of overall rename operation. Those characters
  in $Value which match $Pattern, are removed.
 
  .PARAMETER PatternOccurrence
    Can be a number or the letters f, l
  * f: first occurrence
  * l: last occurrence
  * <number>: the nth occurrence
 
  .PARAMETER Relation
    Used in conjunction with the $Anchor parameter and can be set to either 'before' or
  'after' (the default). Defines the relationship of the $Pattern match with the $Anchor
  match in the new name for $Value.
 
  .PARAMETER Start
    Is another type of anchor used instead of $Anchor and specifies that the $Pattern match
  should be moved to the start of the new name.
 
  .PARAMETER Value
    The source value against which regular expressions are applied.
 
  .PARAMETER With
    This is a NON regular expression string. It would be more accurately described as a formatter.
  Defines what text is used as the replacement for the $Pattern
  match. $With can reference special variables:
  * $0: the pattern match
  * ${_a}: the anchor match
  * ${_c}: the copy match
  When $Pattern contains named capture groups, these variables can also be referenced. Eg if the
  $Pattern is defined as '(?<day>\d{1,2})-(?<mon>\d{1,2})-(?<year>\d{4})', then the variables
  ${day}, ${mon} and ${year} also become available for use in $With.
  Typically, $With is static text which is used to replace the $Pattern match and is inserted
  according to the Anchor match, (or indeed $Start or $End).
 
  .EXAMPLE 1 (Anchor)
  Move-Match 'VAL 1999-02-21 + RH - CLOSE' '(?<dt>\d{4}-\d{2}-\d{2})' -Anchor ' -' -Relation 'before'
 
  Move a match before an anchor
 
  .EXAMPLE 2 (Anchor)
  Move-Match 'VAL 1999-02-21 + RH - CLOSE' '(?<dt>\d{4}-\d{2}-\d{2})' -Anchor ' -' -Relation 'before' -Drop '-'
 
  Move a match before an anchor and drop a literal in place of Pattern
 
  .EXAMPLE 3 (Hybrid Anchor)
  Move-Match 'VAL 1999-02-21 + RH - CLOSE' '(?<dt>\d{4}-\d{2}-\d{2})' -Anchor ' -' -End -Relation 'before' -Drop '-'
 
  Move a match before an anchor, if anchor match fails, then move to end, then drop a literal in place of Pattern.
 
  .EXAMPLE 4 (Anchor with Occurrence)
  Move-Match 'VAL 1999-02-21 + RH - CLOSE' '(?<dt>\d{4}-\d{2}-\d{2})' -Anchor ' -' -Relation 'before' -AnchorOccurrence 'l'
 
  .EXAMPLE 5 (Result formatted by With, named group reference)
  Move-Match 'VAL 1999-02-21 + RH - CLOSE' '(?<dt>\d{4}-\d{2}-\d{2})' -Anchor ' -' -With '--(${dt})--'
 
  Move a match to the anchor, and format the output including group references, no anchor
 
  .EXAMPLE 6 (Result formatted by With, named group reference and insert anchor)
  Move-Match 'VAL 1999-02-21 + RH - CLOSE' '(?<dt>\d{4}-\d{2}-\d{2})' -Anchor ' -' -With '${_a}, --(${dt})--'
 
  Move a match to the anchor, and format the output including group references, insert anchor
  #>


  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSPossibleIncorrectUsageOfAssignmentOperator', '')]
  [Alias('moma')]
  [OutputType([string])]
  param (
    [Parameter(Mandatory, Position = 0)]
    [string]$Value,

    [Parameter(ParameterSetName = 'HybridStart', Mandatory, Position = 1)]
    [Parameter(ParameterSetName = 'HybridEnd', Mandatory, Position = 1)]
    [Parameter(ParameterSetName = 'MoveToAnchor', Mandatory, Position = 1)]
    [Parameter(ParameterSetName = 'MoveToStart', Mandatory, Position = 1)]
    [Parameter(ParameterSetName = 'MoveToEnd', Mandatory, Position = 1)]
    [System.Text.RegularExpressions.RegEx]$Pattern,

    [Parameter(ParameterSetName = 'HybridStart')]
    [Parameter(ParameterSetName = 'HybridEnd')]
    [Parameter(ParameterSetName = 'MoveToAnchor')]
    [Parameter(ParameterSetName = 'MoveToStart')]
    [Parameter(ParameterSetName = 'MoveToEnd')]
    [string]$PatternOccurrence = 'f',

    [Parameter(ParameterSetName = 'NoReplacement', Mandatory)]
    [System.Text.RegularExpressions.RegEx]$Cut,

    [Parameter(ParameterSetName = 'NoReplacement')]
    [string]$CutOccurrence = 'f',

    [Parameter(ParameterSetName = 'HybridStart', Mandatory)]
    [Parameter(ParameterSetName = 'HybridEnd', Mandatory)]
    [Parameter(ParameterSetName = 'MoveToAnchor', Mandatory)]
    [System.Text.RegularExpressions.RegEx]$Anchor,

    [Parameter(ParameterSetName = 'HybridStart')]
    [Parameter(ParameterSetName = 'HybridEnd')]
    [Parameter(ParameterSetName = 'MoveToAnchor')]
    [string]$AnchorOccurrence = 'f',

    [Parameter(ParameterSetName = 'HybridStart')]
    [Parameter(ParameterSetName = 'HybridEnd')]
    [Parameter(ParameterSetName = 'MoveToAnchor')]
    [ValidateSet('before', 'after')]
    [string]$Relation = 'after',

    [Parameter(ParameterSetName = 'HybridStart')]
    [Parameter(ParameterSetName = 'HybridEnd')]
    [Parameter(ParameterSetName = 'MoveToAnchor')]
    [Parameter(ParameterSetName = 'MoveToStart')]
    [Parameter(ParameterSetName = 'MoveToEnd')]
    [System.Text.RegularExpressions.RegEx]$Copy,

    [Parameter(ParameterSetName = 'HybridStart')]
    [Parameter(ParameterSetName = 'HybridEnd')]
    [Parameter(ParameterSetName = 'MoveToAnchor')]
    [Parameter(ParameterSetName = 'MoveToStart')]
    [Parameter(ParameterSetName = 'MoveToEnd')]
    [string]$CopyOccurrence = 'f',

    [Parameter(ParameterSetName = 'HybridStart')]
    [Parameter(ParameterSetName = 'HybridEnd')]
    [Parameter(ParameterSetName = 'MoveToAnchor')]
    [Parameter(ParameterSetName = 'MoveToStart')]
    [Parameter(ParameterSetName = 'MoveToEnd')]
    [string]$With,

    [Parameter(ParameterSetName = 'HybridStart', Mandatory)]
    [Parameter(ParameterSetName = 'MoveToStart', Mandatory)]
    [switch]$Start,

    [Parameter(ParameterSetName = 'HybridEnd', Mandatory)]
    [Parameter(ParameterSetName = 'MoveToEnd', Mandatory)]
    [switch]$End,

    [Parameter(ParameterSetName = 'HybridStart')]
    [Parameter(ParameterSetName = 'HybridEnd')]
    [Parameter(ParameterSetName = 'MoveToAnchor')]
    [Parameter(ParameterSetName = 'MoveToStart')]
    [Parameter(ParameterSetName = 'MoveToEnd')]
    [string]$Drop,

    [Parameter()]
    [switch]$Diagnose,

    [Parameter()]
    [char]$Marker = 0x20DE
  )

  [string]$result = [string]::Empty;
  [string]$failedReason = [string]::Empty;
  [hashtable]$notes = [hashtable]@{}
  [PSCustomObject]$diagnostics = [PSCustomObject]@{
    Named = @{}
  }

  [boolean]$isAnchored = $PSBoundParameters.ContainsKey('Anchor') -and -not([string]::IsNullOrEmpty($Anchor));
  [boolean]$isFormattedWith = $PSBoundParameters.ContainsKey('With') -and -not([string]::IsNullOrEmpty($With));
  [boolean]$dropped = $PSBoundParameters.ContainsKey('Drop') -and -not([string]::IsNullOrEmpty($Drop));
  [boolean]$isPattern = $PSBoundParameters.ContainsKey('Pattern') -and -not([string]::IsNullOrEmpty($Pattern));
  [boolean]$isCut = $PSCmdlet.ParameterSetName -eq 'NoReplacement';
  [boolean]$isVanillaMove = -not($PSBoundParameters.ContainsKey('With')) -and -not($isCut);
  [boolean]$isCopy = $PSBoundParameters.ContainsKey('Copy');

  [Hashtable]$patternCaptures = @{}
  [Hashtable]$copyCaptures = @{}
  [Hashtable]$anchorCaptures = @{}

  # Determine what to remove
  #
  [string]$removalText, $remainingText = if ($isPattern) {
    [hashtable]$parameters = @{
      'Source'       = $Value;
      'PatternRegEx' = $Pattern;
      'Occurrence'   = $($PSBoundParameters.ContainsKey('PatternOccurrence') ? $PatternOccurrence : 'f');
    }

    if ($dropped) {
      $parameters['Marker'] = $Marker;
    }

    [string]$capturedPattern, [string]$patternRemoved, `
      [System.Text.RegularExpressions.Match]$patternMatch = Split-Match @parameters;

    if (-not([string]::IsNullOrEmpty($capturedPattern))) {
      [hashtable]$patternCaptures = get-Captures -MatchObject $patternMatch;
      if ($Diagnose.ToBool()) {
        $diagnostics.Named['Pattern'] = $patternCaptures;
      }
    }
    else {
      # Source doesn't match Pattern
      #
      $failedReason = 'Pattern Match';
    }

    $capturedPattern, $patternRemoved;
  }
  elseif ($isCut) {
    [hashtable]$parameters = @{
      'Source'       = $Value;
      'PatternRegEx' = $Cut;
      'Occurrence'   = $($PSBoundParameters.ContainsKey('CutOccurrence') ? $CutOccurrence : 'f');
    }

    [string]$capturedCut, [string]$cutRemoved, `
      [System.Text.RegularExpressions.Match]$cutMatch = Split-Match @parameters;

    if (-not([string]::IsNullOrEmpty($capturedCut))) {
      $result = $cutRemoved;
    }
    else {
      # Source doesn't match Cut
      #
      $failedReason = 'Cut Match';
    }

    $capturedCut, $cutRemoved;
  }

  # Determine the replacement text (Copy/With)
  #
  if ([string]::IsNullOrEmpty($failedReason)) {
    if ([string]::IsNullOrEmpty($result)) {
      # The replaceWith here takes into account pattern ($0) and copy (${_c}) references inside the With
      # and also any of the user defined named group/capture no references in With, regardless of $isPattern,
      # $isCut, $Anchor, $Start or $End; ie it's universal.
      #
      [string]$replaceWith = if ($isVanillaMove) {
        # Insert the original pattern match, because there is no Copy/With.
        # If there is a Copy, without a With, the Copy is ignored, its only processed if
        # doing an exotic move (the opposite from $isVanilla)
        #
        $capturedPattern;
      }
      else {
        # Determine what to Copy
        #
        [string]$copyText = if ($isCopy) {
          [hashtable]$parameters = @{
            'Source'       = $remainingText
            'PatternRegEx' = $Copy
            'Occurrence'   = $($PSBoundParameters.ContainsKey('CopyOccurrence') ? $CopyOccurrence : 'f')
          }

          # With this implementation, it is up to the user to supply a regex proof
          # pattern, so if the Copy contains regex chars which must be treated literally, they
          # must pass in the string pre-escaped: -Copy $(esc('some-pattern') + 'other stuff').
          #
          [string]$capturedCopy, $null, `
            [System.Text.RegularExpressions.Match]$copyMatch = Split-Match @parameters;

          if (-not([string]::IsNullOrEmpty($capturedCopy))) {
            [hashtable]$copyCaptures = get-Captures -MatchObject $copyMatch;
            if ($Diagnose.ToBool()) {
              $diagnostics.Named['Copy'] = $copyCaptures;
            }
            $capturedCopy;
          }
          else {
            # Copy doesn't match so abort
            #
            $failedReason = 'Copy Match';
            [string]::Empty;
          }
        } # $copyText =
        else {
          [System.Text.RegularExpressions.Match]$copyMatch = $null;
          [string]::Empty;
        }

        # Determine With
        #
        if ([string]::IsNullOrEmpty($failedReason)) {
          # With can be something like '___ ${_a}, (${x}, ${y}, [$0], ${_c} ___', where $0
          # represents the pattern capture, the special variable _c represents $Copy,
          # _a represents the anchor and ${x} and ${y} represents user defined capture groups.
          # The With replaces the anchor, so to re-insert the anchor _a, it must be referenced
          # in the With format. Numeric captures may also be referenced.
          #

          # Handle whole Copy/Pattern reference inside the replacement text (With)
          #
          [string]$tempWith = $With.Replace('${_c}', $copyText).Replace('$0', $capturedPattern);

          # Handle Pattern group references
          #
          $tempWith = Update-GroupRefs -Source $tempWith -Captures $patternCaptures;

          # Handle Copy group references
          #
          $tempWith = Update-GroupRefs -Source $tempWith -Captures $copyCaptures;

          $tempWith;
        }
        else {
          [string]::Empty;
        }
      } # = replaceWith
    } # $result
  } # $failedReason
  else {
    [string]$replaceWith = [string]::Empty;
  }

  # Where to move match to
  #
  [string]$capturedAnchor = [string]::Empty;

  if ([string]::IsNullOrEmpty($failedReason)) {
    if ([string]::IsNullOrEmpty($result)) {
      # The anchor is the final replacement. The anchor match can contain group
      # references. The anchor is replaced by With (replacement text) and it's the With
      # that can contain Copy/Pattern/Anchor group references. If there is no Start/End/Anchor
      # then the move operation is a Cut which is handled separately.
      #
      # Here we need to apply exotic move to Start/End as well as Anchor => move-Exotic
      # The only difference between these 3 scenarios is the destination
      #
      if ($isAnchored) {
        [hashtable]$parameters = @{
          'Source'       = $remainingText;
          'PatternRegEx' = $Anchor;
          'Occurrence'   = $($PSBoundParameters.ContainsKey('AnchorOccurrence') ? $AnchorOccurrence : 'f');
        }

        # As with the Copy parameter, if the user wants to specify an anchor by a pattern
        # which contains regex chars, then can use -Anchor $(esc('anchor-pattern')). If
        # there are no regex chars, then they can use -Anchor 'pattern'. However, if the
        # user needs to do partial escapes, then they will have to do the escaping
        # themselves: -Anchor $(esc('partial-pattern') + 'remaining-pattern').
        #
        [string]$capturedAnchor, $null, `
          [System.Text.RegularExpressions.Match]$anchorMatch = Split-Match @parameters;

        if (-not([string]::IsNullOrEmpty($capturedAnchor))) {
          [string]$format = if ($isFormattedWith) {
            $replaceWith;
          }
          else {
            ($Relation -eq 'before') ? $replaceWith + $capturedAnchor : $capturedAnchor + $replaceWith;
          }

          # Resolve anchor reference inside With
          #
          $format = $format.Replace('${_a}', $capturedAnchor);

          # This resolves the Anchor named group references ...
          #
          $result = $Anchor.Replace($remainingText, $format, 1, $anchorMatch.Index);

          # ... but we still need to capture the named groups for diagnostics
          #
          $anchorCaptures = get-Captures -MatchObject $anchorMatch;
          if ($Diagnose.ToBool()) {
            $diagnostics.Named['Anchor'] = $anchorCaptures;
          }
        }
        else {
          # Anchor doesn't match Pattern
          #
          if ($Start.ToBool()) {
            $result = $replaceWith + $remainingText;
            $notes['hybrid'] = 'Start (Anchor failed)';
          }
          elseif ($End.ToBool()) {
            $result = $remainingText + $replaceWith;
          }
          else {
            $failedReason = 'Anchor Match';
            $notes['hybrid'] = 'End (Anchor failed)';
          }
        }
      }
      elseif ($Start.ToBool()) {
        $result = $replaceWith + $remainingText;
      }
      elseif ($End.ToBool()) {
        $result = $remainingText + $replaceWith;
      }
      elseif ($isCut) {
        $result = $remainingText;
      }
    } # $result
  }  # $failedReason

  # Perform Drop
  #
  if ([string]::IsNullOrEmpty($failedReason)) {
    if ($isPattern) {
      [string]$dropText = $Drop;

      # Now cross reference the Pattern group references
      #
      if ($patternCaptures.PSBase.Count -gt 0) {
        $dropText = Update-GroupRefs -Source $dropText -Captures $patternCaptures;
      }

      # Now cross reference the Copy group references
      #
      if ($isCopy -and ($copyCaptures.PSBase.Count -gt 0)) {
        $dropText = $dropText.Replace('${_c}', $copyCaptures['0']);

        $dropText = Update-GroupRefs -Source $dropText -Captures $copyCaptures;
      }

      # Now cross reference the Anchor group references
      #
      if (-not([string]::IsNullOrEmpty($capturedAnchor))) {
        $dropText = $dropText.Replace('${_a}', $capturedAnchor);
      }

      if ($anchorCaptures.PSBase.Count -gt 0) {
        $dropText = Update-GroupRefs -Source $dropText -Captures $anchorCaptures;
      }

      $result = $result.Replace([string]$Marker, $dropText);
    }
  } # $failedReason

  [boolean]$success = [string]::IsNullOrEmpty($failedReason);
  [PSCustomObject]$moveResult = [PSCustomObject]@{
    Payload         = $success ? $result : $Value;
    Success         = $success;
    CapturedPattern = $isPattern ? $capturedPattern : $($isCut ? $capturedCut : [string]::Empty);
  }

  if (-not([string]::IsNullOrEmpty($failedReason))) {
    $moveResult | Add-Member -MemberType NoteProperty -Name 'FailedReason' -Value $failedReason;
  }

  if ($Diagnose.ToBool() -and ($diagnostics.Named.Count -gt 0)) {
    $moveResult | Add-Member -MemberType NoteProperty -Name 'Diagnostics' -Value $diagnostics;
  }

  if ($notes.PSBase.Count -gt 0) {
    $moveResult | Add-Member -MemberType NoteProperty -Name 'Notes' -Value $notes;
  }

  return $moveResult;
}

function Test-IsAlreadyAnchoredAt {
  <#
  .NAME
    Test-IsAlreadyAnchoredAt
 
  .SYNOPSIS
    Checks to see if a given pattern is matched at the start or end of an input string.
 
  .DESCRIPTION
    When Rename-Many uses the Start or End switches to move a match to the corresponding location,
  it needs to filter out those entries where the specified occurrence of the Pattern is already
  at the desire location. We can't do this using a synthetic anchored regex using ^ and $, rather
  we must use the origin regex, perform the match and then see where that match resides, by consulting
  the index and length of that match instance.
 
  .LINK
    https://eliziumnet.github.io/RexFs/
 
  .PARAMETER Source
    The input source
 
  .PARAMETER Expression
    A regex instance to match against
 
  .PARAMETER Occurrence
    Which match occurrence in Expression do we want to check
 
  .PARAMETER Start
    Check match is at the start of the input source
 
  .PARAMETER End
    Check match is at the end of the input source
  #>

  [OutputType([boolean])]
  param(
    [Parameter()]
    [string]$Source,

    [Parameter()]
    [regex]$Expression,

    [Parameter()]
    [string]$Occurrence,

    [Parameter()]
    [switch]$Start,

    [Parameter()]
    [switch]$End
  )

  [hashtable]$parameters = @{
    'Source'       = $Source;
    'PatternRegEx' = $Expression;
    'Occurrence'   = $Occurrence;
  }

  [string]$capturedExpression, $null, `
    [System.Text.RegularExpressions.Match]$expressionMatch = Split-Match @parameters;

  [boolean]$result = if (-not([string]::IsNullOrEmpty($capturedExpression))) {
    if ($Start.IsPresent) {
      # For the Start, its easy to see if the match is already at the start,
      # we just check the match's index being 0.
      #
      $expressionMatch.Index -eq 0;
    }
    elseif ($End.IsPresent) {
      # 012345
      # source = ABCDEA
      # PATTERN = 'A'
      # OCC = 1
      #
      # In the above example, if we wanted to move the first A to the end, we need
      # to see if that occurrence is at the end, NOT does that pattern appear at the
      # end. The old logic, using a synthesized Anchored regex, performed the latter
      # logic and that's why it failed. What we want, is to check that our specific
      # Occurrence is not already at the end, which of course it isn't. We do this,
      # by checking the location of our match.
      #
      $($expressionMatch.Index + $expressionMatch.Length) -eq $Source.Length;
    }
    else {
      $false;
    }
  }
  else {
    $false;
  }

  return $result;
}

function Update-Match {

  <#
  .NAME
    Update-Match
 
  .SYNOPSIS
    The core update match action function principally used by Rename-Many. Updates
  $Pattern match in it's current location.
 
  .DESCRIPTION
    Returns a new string that reflects updating the specified $Pattern match.
    Firstly, Update-Match removes the Pattern match from $Value. This makes the Paste and
  Copy match against the remainder ($patternRemoved) of $Value. This way, there is
  no overlap between the Pattern match and $Paste and it also makes the functionality more
  understandable for the user. NB: Pattern only tells you what to remove, but it's the
  Copy and Paste that defines what to insert.
 
  .LINK
    https://eliziumnet.github.io/RexFs/
 
  .PARAMETER Copy
    Regular expression string applied to $Value (after the $Pattern match has been removed),
  indicating a portion which should be copied and re-inserted (via the $Paste parameter;
  see $Paste). Since this is a regular expression to be used in $Paste, there
  is no value in the user specifying a static pattern, because that static string can just be
  defined in $Paste. The value in the $Copy parameter comes when a non literal pattern is
  defined eg \d{3} (is non literal), specifies any 3 digits as opposed to say '123', which
  could be used directly in the $Paste parameter without the need for $Copy. The match
  defined by $Copy is stored in special variable ${_c} and can be referenced as such from
  $Paste.
 
  .PARAMETER CopyOccurrence
    Can be a number or the letters f, l
  * f: first occurrence
  * l: last occurrence
  * <number>: the nth occurrence
 
  .PARAMETER Diagnose
    switch parameter that indicates the command should be run in WhatIf mode. When enabled
  it presents additional information that assists the user in correcting the un-expected
  results caused by an incorrect/un-intended regular expression. The current diagnosis
  will show the contents of named capture groups that they may have specified. When an item
  is not renamed (usually because of an incorrect regular expression), the user can use the
  diagnostics along side the 'Not Renamed' reason to track down errors. When $Diagnose has
  been specified, $WhatIf does not need to be specified.
 
  .PARAMETER Paste
    Formatter parameter for Update operations. Can contain named/numbered group references
  defined inside regular expression parameters, or use special named references $0 for the whole
  Pattern match and ${_c} for the whole Copy match. The Paste can also contain named/numbered
  group references defined in $Pattern.
 
  .PARAMETER Pattern
    Regular expression string that indicates which part of the $Value that either needs
  to be moved or replaced as part of overall rename operation. Those characters in $Value
  which match $Pattern, are removed.
 
  .PARAMETER PatternOccurrence
    Can be a number or the letters f, l
  * f: first occurrence
  * l: last occurrence
  * <number>: the nth occurrence
 
  .PARAMETER Value
    The source value against which regular expressions are applied.
 
  .EXAMPLE 1 (Update with literal content)
  Update-Match 'VAL 1999-02-21 + RH - CLOSE' '(?<dt>\d{4}-\d{2}-\d{2})' -Paste '----X--X--'
 
  .EXAMPLE 2 (Update with variable content)
  [string]$today = Get-Date -Format 'yyyy-MM-dd'
  Update-Match 'VAL 1999-02-21 + RH - CLOSE' '(?<dt>\d{4}-\d{2}-\d{2})' -Paste $('_(' + $today + ')_')
 
  .EXAMPLE 3 (Update with whole copy reference)
  Update-Match 'VAL 1999-02-21 + RH - CLOSE' '(?<dt>\d{4}-\d{2}-\d{2})' -Paste '${_c},----X--X--' -Copy '[^\s]+'
 
  .EXAMPLE 4 (Update with group references)
  Update-Match 'VAL 1999-02-21 + RH - CLOSE' '(?<dt>\d{4}-\d{2}-\d{2})' -Paste '${first},----X--X--' -Copy '(?<first>[^\s]+)'
 
  .EXAMPLE 5 (Update with 2nd copy occurrence)
  Update-Match 'VAL 1999-02-21 + RH - CLOSE' '(?<dt>\d{4}-\d{2}-\d{2})' -Paste '${_c},----X--X--' -Copy '[^\s]+' -CopyOccurrence 2
  #>

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
  [OutputType([string])]
  param(
    [Parameter(Mandatory, Position = 0)]
    [string]$Value,

    [Parameter(Mandatory, Position = 1)]
    [System.Text.RegularExpressions.RegEx]$Pattern,

    [Parameter()]
    [ValidateScript( { $_ -ne '0' })]
    [string]$PatternOccurrence = 'f',

    [Parameter()]
    [System.Text.RegularExpressions.RegEx]$Copy,

    [Parameter()]
    [ValidateScript( { $_ -ne '0' })]
    [string]$CopyOccurrence = 'f',

    [Parameter()]
    [string]$Paste,

    [Parameter()]
    [switch]$Diagnose
  )

  [string]$failedReason = [string]::Empty;
  [PSCustomObject]$diagnostics = [PSCustomObject]@{
    Named = @{}
  }

  [string]$pOccurrence = $PSBoundParameters.ContainsKey('PatternOccurrence') `
    ? $PatternOccurrence : 'f';

  [string]$capturedPattern, $patternRemoved, [System.Text.RegularExpressions.Match]$patternMatch = `
    Split-Match -Source $Value -PatternRegEx $Pattern `
    -Occurrence $pOccurrence;

  if (-not([string]::IsNullOrEmpty($capturedPattern))) {
    [Hashtable]$patternCaptures = get-Captures -MatchObject $patternMatch;
    if ($Diagnose.ToBool()) {
      $diagnostics.Named['Pattern'] = $patternCaptures;
    }
    [Hashtable]$copyCaptures = @{}

    [string]$copyText = if ($PSBoundParameters.ContainsKey('Copy')) {
      [string]$capturedCopy, $null, [System.Text.RegularExpressions.Match]$copyMatch = `
        Split-Match -Source $patternRemoved -PatternRegEx $Copy `
        -Occurrence ($PSBoundParameters.ContainsKey('CopyOccurrence') ? $CopyOccurrence : 'f');

      if (-not([string]::IsNullOrEmpty($capturedCopy))) {
        $copyCaptures = get-Captures -MatchObject $copyMatch;
        if ($Diagnose.ToBool()) {
          $diagnostics.Named['Copy'] = $copyCaptures;
        }
      }
      else {
        $failedReason = 'Copy Match';
      }
      $capturedCopy;
    }

    if ([string]::IsNullOrEmpty($failedReason)) {
      [string]$format = $PSBoundParameters.ContainsKey('Paste') `
        ? $Paste.Replace('${_c}', $copyText).Replace('$0', $capturedPattern) `
        : [string]::Empty;

      # Resolve all named/numbered group references
      #
      $format = Update-GroupRefs -Source $format -Captures $patternCaptures;
      $format = Update-GroupRefs -Source $format -Captures $copyCaptures;

      [string]$result = $Pattern.Replace($Value, $format, 1, $patternMatch.Index);
    }
  }
  else {
    $failedReason = 'Pattern Match';
  }

  [boolean]$success = $([string]::IsNullOrEmpty($failedReason));
  if (-not($success)) {
    $result = $Value;
  }

  [PSCustomObject]$updateResult = [PSCustomObject]@{
    Payload         = $result;
    Success         = $success;
    CapturedPattern = $capturedPattern;
  }

  if (-not([string]::IsNullOrEmpty($failedReason))) {
    $updateResult | Add-Member -MemberType NoteProperty -Name 'FailedReason' -Value $failedReason;
  }

  if ($Diagnose.ToBool() -and ($diagnostics.Named.Count -gt 0)) {
    $updateResult | Add-Member -MemberType NoteProperty -Name 'Diagnostics' -Value $diagnostics;
  }

  return $updateResult;
} # Update-Match


function convert-ActionResult {
  param(
    [Parameter()]
    [object]$Result
  )

  [PSCustomObject]$actionResult = if ($Result -is [PSCustomObject]) {
    if (($null -ne ${Result}.Success) -and $Result.Success) {
      if (($null -ne ${Result}.Payload)) {
        [string]::IsNullOrEmpty($Result.Payload) ? $(
          $EmptyActionResult;
        ) : $($Result);
      }
      else {
        $EmptyActionResult;
      }
    }
    else {
      if (($null -ne ${Result}.Payload)) {
        [string]::IsNullOrEmpty($Result.Payload) ? $(
          $EmptyActionResult;
        ) : $(
          [PSCustomObject]@{
            PayLoad = $Result.Payload;
            Success = $true;
          }
        );
      }
      else {
        if (($null -ne ${Result}.FailedReason)) {
          [string]::IsNullOrEmpty($Result.FailedReason) ? $(
            $EmptyActionResult;
          ) : $(
            [PSCustomObject]@{
              FailedReason = $Result.FailedReason;
              Success = $false
            }
          ); #
        }
        else {
          $EmptyActionResult;
        }
      }
    }
  }
  elseif ($Result -is [string]) {
    [string]::IsNullOrEmpty($Result) ? $(
      $EmptyActionResult;
    ) : $(
      [PSCustomObject]@{
        Payload = $Result;
        Success = $true;
      }
    );
  }
  else {
    [PSCustomObject]@{
      FailedReason = "Unsupported action result type (type: '$($Result.GetType())')";
      Success      = $false;
    }
  }

  return $actionResult;
}

function invoke-HandleError {
  param(
    [Parameter()]
    [string]$message,

    [Parameter()]
    [string]$prefix,

    [Parameter()]
    [string]$reThrowIfMatch = 'Expected strings to be the same, but they were different'
  )

  [string]$errorReason = $(
    "$prefix`: " +
    ($message -split '\n')[0]
  );
  # We need Pester to throw pester specific errors. In the lack of, we have to
  # guess that its a Pester assertion failure and let the exception through so
  # the test fails.
  #
  if ($errorReason -match $reThrowIfMatch) {
    throw $_;
  }

  return $errorReason;
}

function invoke-PostProcessing {
  param(
    [Parameter()]
    [string]$InputSource,

    [Parameter()]
    [PSCustomObject[]]$Rules,

    [Parameter()]
    [hashtable]$signals
  )
  [string]$transformResult = $InputSource;

  [string[]]$appliedSignals = foreach ($rule in $Rules) {
    if ($rule['IsApplicable'].InvokeReturnAsIs($transformResult)) {
      $transformResult = $rule['Transform'].InvokeReturnAsIs($transformResult);
      $rule['Signal'];
    }
  }

  [PSCustomObject]$result = if ($appliedSignals.Count -gt 0) {
    [System.Collections.Generic.List[string]]$labels = [System.Collections.Generic.List[string]]::new()

    [string]$indication = -join $(foreach ($name in $appliedSignals) {
      $labels.Add($signals[$name].Key);
      $signals[$name].Value;
    })
    $indication = "[{0}]" -f $indication;

    [PSCustomObject]@{
      TransformResult = $transformResult;
      Indication      = $indication;
      Signals         = $appliedSignals;
      Label           = 'Post ({0})' -f $($labels -join ', ');
      Modified        = $true;
    }
  }
  else {
    [PSCustomObject]@{
      TransformResult = $InputSource;
      Modified        = $false;
    }
  }

  $result;
}

function invoke-Transform {

  param(
    [Parameter()]
    [hashtable]$Exchange,

    [Parameter()]
    [string]$Value
  )

  [PSCustomObject]$actionResult = try {
    if ($Exchange.ContainsKey("$($Remy_EXS).TRANSFORM")) {
      [scriptblock]$transform = $Exchange["$($Remy_EXS).TRANSFORM"];

      if ($transform) {
        [string]$transformed = $transform.InvokeReturnAsIs(
          $Value,
          $Exchange
        );

        if (-not([string]::IsNullOrEmpty($transformed))) {
          [PSCustomObject]@{
            Payload = $transformed;
            Success      = $true;
          } 
        }
        else {
          [PSCustomObject]@{
            FailedReason = 'Transform returned empty';
            Success      = $false;
          }
        }
      }
      else {
        [PSCustomObject]@{
          FailedReason = 'Internal error, transform missing';
          Success      = $false;
        }
      }
    }
  }
  catch {
    $errorReason = invoke-HandleError -message $_.Exception.Message -prefix 'Transform';

    [PSCustomObject]@{
      FailedReason = $errorReason;
      Success      = $false;
    }
  }

  return $actionResult;
}

function rename-FsItem {
  [CmdletBinding(SupportsShouldProcess)]
  param(
    [Parameter()]
    [System.IO.FileSystemInfo]$From,

    [Parameter()]
    [string]$To,

    # [UndoRename]
    [Parameter()]
    [AllowNull()]
    [object]$UndoOperant
  )
  [boolean]$itemIsDirectory = ($From.Attributes -band
    [System.IO.FileAttributes]::Directory) -eq [System.IO.FileAttributes]::Directory;

  [string]$parentPath = $itemIsDirectory ? $From.Parent.FullName : $From.Directory.FullName;
  [string]$destinationPath = Join-Path -Path $parentPath -ChildPath $To;

  if (-not($PSBoundParameters.ContainsKey('WhatIf') -and $PSBoundParameters['WhatIf'])) {
    [boolean]$differByCaseOnly = $From.Name.ToLower() -eq $To.ToLower();

    if ($differByCaseOnly) {
      # Just doing a double rename to get around the problem of not being able to rename
      # an item unless the case is different
      #
      [string]$tempName = $From.Name + "_";

      Rename-Item -LiteralPath $From.FullName -NewName $tempName -PassThru | `
        Rename-Item -NewName $To;
    }
    else {
      Rename-Item -LiteralPath $From.FullName -NewName $To;
    }

    if ($UndoOperant) {
      [PSCustomObject]$operation = [PSCustomObject]@{
        Directory = $parentPath;
        From      = $From.Name;
        To        = $To;
      }
      Write-Debug "rename-FsItem (Undo Rename) => alert: From: '$($operation.From.Name)', To: '$($operation.To)'";
      $UndoOperant.alert($operation);
    }

    $result = Get-Item -LiteralPath $destinationPath;
  }
  else {
    $result = $To;
  }

  return $result;
} # rename-FsItem

class EndAdapter {
  [System.IO.FileSystemInfo]$_fsInfo;
  [boolean]$_isDirectory;
  [string]$_adjustedName;

  EndAdapter([System.IO.FileSystemInfo]$fsInfo) {
    $this._fsInfo = $fsInfo;
    $this._isDirectory = ($fsInfo.Attributes -band
      [System.IO.FileAttributes]::Directory) -eq [System.IO.FileAttributes]::Directory;

    $this._adjustedName = $this._isDirectory ? $fsInfo.Name `
      : [System.IO.Path]::GetFileNameWithoutExtension($this._fsInfo.Name);
  }

  [string] GetAdjustedName() {
    return $this._adjustedName;
  }

  [string] GetNameWithExtension([string]$newName) {
    [string]$result = ($this._isDirectory) ? $newName `
      : ($newName + [System.IO.Path]::GetExtension($this._fsInfo.Name));

    return $result;
  }

  [string] GetNameWithExtension([string]$newName, [string]$extension) {
    [string]$result = ($this._isDirectory) ? $newName `
      : ($newName + $extension);

    return $result;
  }
}

function New-EndAdapter {
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions',
    '', Justification = 'Not a state changing function, its a factory')]
  param(
    [System.IO.FileSystemInfo]$fsInfo
  )
  return [EndAdapter]::new($fsInfo);
}

# These classes in the same file because of the issues of class reference in modules.
# We can't add a using module statement in files that reference these classes. Class
# references via import module only works with classes defined in other modules. Any
# class defined inside Shelly can't be accessed across different files.
#
class Shell {
  [string]$FullPath;
  [System.Text.StringBuilder] $_builder = [System.Text.StringBuilder]::new()

  Shell([string]$path) {
    $this.FullPath = $path;
  }

  [void] persist() {
    throw [System.Management.Automation.MethodInvocationException]::new(
      'Abstract method not implemented (Shell.persist)');
  }
}

class PoShShell : Shell {
  PoShShell([string]$path): base($path) {
  }

  [void] rename ([string]$from, [string]$to) {
    [string]$toFilename = [System.IO.Path]::GetFileName($to)
    [void]$this._builder.AppendLine($("Rename-Item -LiteralPath '$from' -NewName '$toFilename'"));
  }

  [void] persist([string]$content) {
    Set-Content -LiteralPath $this.FullPath -Value $content;
  }
}

class Operant {

}

class Undo : Operant {
  [Shell]$Shell;
  [System.Collections.ArrayList]$Operations = [System.Collections.ArrayList]::new();

  Undo([Shell]$shell) {
    $this.Shell = $shell;
  }

  [void] alert([PSCustomObject]$operation) {
    # User should pass in a PSCustomObject with Directory, From and To fields
    #
    [void]$this.Operations.Add($operation);
  }

  [void] persist() {
    throw [System.Management.Automation.MethodInvocationException]::new(
      'Abstract method not implemented (Undo.persist)');
  }
}

class UndoRename : Undo {
  UndoRename([Shell]$shell) : base($shell) {

  }

  [string] generate() {
    [string]$result = if ($this.Operations.count -gt 0) {
      $($this.Operations.count - 1)..0 | ForEach-Object {
        [PSCustomObject]$operation = $this.Operations[$_];

        [string]$toPath = Join-Path -Path $operation.Directory -ChildPath $operation.To;
        $this.Shell.rename($toPath, $operation.From);
      }

      $this.Shell._builder.ToString();
    }
    else {
      [string]::Empty;
    }

    return $result;
  }

  [void] finalise() {
    $this.Shell.persist($this.generate());
  }
}
Export-ModuleMember -Variable RexFs

Export-ModuleMember -Alias remy, esc, moma

Export-ModuleMember -Function Rename-Many, Add-Appendage, Format-Escape, Get-RemyExchangeSpace, Move-Match, Test-IsAlreadyAnchoredAt, Update-Match

# Custom Module Initialisation
#

[array]$remySignals = @(
  'ABORTED-A', 'APPEND', 'BECAUSE', 'CAPTURE', 'CLASH', 'COPY-A', 'CUT-A', 'DIAGNOSTICS',
  'DIRECTORY-A', 'EXCLUDE', 'FILE-A', 'INCLUDE', 'LOCKED', 'MULTI-SPACES', 'NOT-ACTIONED',
  'NOVICE', 'PASTE-A', 'PATTERN', 'PREPEND', 'REMY.ANCHOR', 'REMY.ANCHOR', 'REMY.DROP',
  'REMY.POST', 'REMY.UNDO', 'TRANSFORM', 'TRIM', 'WHAT-IF', 'WITH'
);

Register-CommandSignals -Alias 'remy' -UsedSet $remySignals -Silent;

New-Variable -Name Remy_EXS -Value 'REXFS.REMY' -Scope Script -Option ReadOnly -Force;

New-Variable -Name EmptyActionResult -Scope Script -Option ReadOnly -Force -Value $([PSCustomObject]@{
    FailedReason = 'Empty action result';
    Success      = $false;
  });