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; }); |