_Regular expressions based file system operations module with Bulk File/Directory renamer powered by :nazar_amulet: Elizium.Loopz and colouring provided by [:rainbow: Elizium.Krayola__](https://github.com/EliziumNet/Krayola)___
The module can be installed using the standard install-module command:
PS> install-module -Name Elizium.RexFs
The following dependencies will be installed automatically
:high_brightness: Rename-Many (remy) is a flexible regular expression based bulk rename command. In order to get the best out of this command, the user should become familiar and skilled in writing regular expressions.
:pushpin: Note, that it is recommended that this page is read in sequential order, in particular because the first usage scenario to be explained: Move Match, includes more screen snapshots and examples to help explain the concepts, which is not repeated to the same detail in subsequent usage scenarios. See also the Parameter Reference, for a more detailed explanation of each parameter and Safety Features to see how to Unlock the command so it becomes effective.
There are multiple modes of operation that Rename-Many runs in, which are Update/Move/Cut/Appendage. On top of this, a developer can extend its capabilities by providing a custom Transform and/or build Higher Order Commands.
Mode | DESCRIPTION |
---|---|
Move Match | Move a regular expression match to an anchor |
Update Match | Replace a regular expression match |
Cut Match | Remove a regular expression match |
Add Appendage | Add a fixed token to Start or End of item |
Transform | Apply a custom transform to perform rename |
Rename-Many works by receiving it's input from the pipeline and renaming these file system items according to parameters specified. The benefit of using this command comes when there is a need to rename multiple items according to the same criteria. This criteria amounts to specifying regular expressions. The 2 most commonly used scenarios are:
Moves a regex match to another point, known as an anchor.
:one: Let's say we have a directory containing a collection of log files, some of which are not named according to a fixed form eg they contain a date which is not at the end of the file name. We can bulk rename the rogue files with a command like:
gci ./*.log | Rename-Many -Pattern '(?\
\d{2})-(?\ \d{2})-(?\ \d{4})' -End -WhatIf
Note the following:
So an directory named:
'27-03-1997 Orbital - The Middle Of Nowhere Tour Pics'
would be renamed to
'Orbital - The Middle Of Nowhere Tour Pics27-03-1997'
:two: Extending this example a little further, let's say we also want to reformat the date so it's in ISO format (yyyy-mm-dd). We can achieve this by specifying the formatter parameter -With (With is used for move operations, whereas Paste is used for update operations) as illustrated below:
... | remy -Pattern '(?\
\d{2})-(?\ \d{2})-(?\ \d{4})' -End -With '\${y}-\${m}-\${d}' -WhatIf
So that same directory referenced in example :one: would now be renamed to:
'Orbital - The Middle Of Nowhere Tour Pics 1997-03-27'
You should note that now we are using a formatter, we can repair the fact that the new date placement now has a space separator after 'Pics' (the previous end of the item's name) as opposed to the match being inserted verbatim as in example :one:.
In practice, there are a few more subtleties that need to be accounted for when performing bulk renames and the reader will discover this when reading the rest of this documentation. One such nuance is that moving the date like this may require the insertion of additional content to get the graceful result we require, eg wrap the date in brackets, or insert a dash before the date, as shown here, ... etc.
Replace a regex match in it's present location with alternative content.
Continuing with our log files theme, let's say we just want to adjust the date format. So any date that appears as 'dd-mm-yyyy' we would like to adjust it to ISO format 'yyyy-mm-dd' instead. We can do this using the update mode of Rename-Many:
... | remy -Pattern '(?\
\d{2})-(?\ \d{2})-(?\ \d{4})' -Paste '\${y}-\${m}-\${d}'
Hopefully, the above examples have given a quick insight in the operation of the command, enough to get started with. Also note, to begin with, you can initially stick to using static patterns instead of more complex regex sequences for the more simple rename operations, eg rename the presence of the word 'CCY' to 'Currency', by using a pattern of 'CCY' and -With/-Paste of 'Currency'.
:warning: An important point of note that the user must take heed of early on is, all formatter parameters (-Drop, -Paste and -With), MUST be specified with single quotes not double quotes (see Formatter parameters must use single quotes for more details).
Rename-Many is a powerful command and should be used with caution. Because of the potential for 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; ie ($env:REXFS_REMY_LOCKED = $false, either temporarily in the session, or permanently in the powershell $Profile). 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)
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.)
All regular expression parameters as listed below ...
Regex Parameter | Alias | DESCRIPTION |
---|---|---|
Anchor | a | Move a match to an anchor |
AnchorEnd | ae | Move a match to an anchor or to end if anchor fails match |
AnchorStart | as | Move a match to an anchor or to start if anchor fails match |
Copy | co | Make a copy of this match for formatter reference |
Cut | :heavy_multiplication_x: | Remove this match without a replacement |
Except | x | Filter out items that match |
Include | i | Filter in items that match |
Pattern | w | Replace or Move this match |
... are all declared as arrays. This allows the user to augment the regular expression with an additional value denoting which match occurrence is in effect. This value can be either numeric which denotes which match to select or 'f' for the first match or 'l' indicating the last match. The exception to the use of the Occurrence value is with -Include/-Except, which are filtering parameters (see Filtering). Since they are used for filtering, the Occurrence value is irrelevant, so should not be supplied.
So for example -Pattern can be specified as:
Rename-Many -Pattern 'foo', 2 ...
This means that the second occurrence of the match 'foo' should be taken as the token match for each item being renamed.
If a regex parameter needs to use a regular expression character as a literal, it must be escaped. There are multiple ways of doing this:
The above 2 approaches escape the entire string. The second approach is more concise and avoids the necessary use of extra brackets and $.
This third method is required when the whole pattern should not be subjected to escaping.
Generally, the user must indicate which items are to be renamed using the pipeline. Any command can be used to select file system items (directories or files), but typically Get-ChildItem would be used and the result piped to Rename-Many. Get-ChildItem contains a -Filter parameter but this can only filter as a blob using wildcards where appropriate, but can not filter by a regular expression. The user could use an extra pipeline stage using Where-Object eg:
Get-ChildItem -LiteralPath ./ -File -Filter '*.log' | Where-Object { $_.Name -match 'bar' } | Rename-Many ...
However, this command is starting to get quite long (even if we used the ? alias for Where-Object). So instead, Rename-Many contains regex filters via the -Include/-Except parameters:
Get-ChildItem -LiteralPath ./ -File -Filter '*.log' | Rename-Many -Include 'bar' ...
and similarly for -Except.
Any items filtered out 'inband' (by the filter parameters on Rename-Many as opposed to 'out of band' filtering applied to the previous stage of the pipeline, eg the filter parameter on Get-ChildItem), will be counted as a 'Skipped' item in the Summary displayed at the end of the rename batch.
The following parameters are known as formatters. This means that they are strings which contain the replacement text for the match. The formatter can also reference named (or numbered) group references defined in the non-filtering regex parameters:
Formatter Parameter | Alias | DESCRIPTION |
---|---|---|
With | w | used when performing Move |
Drop | dr | for Move operations where the Pattern match is replaced by drop content |
Paste | ps | used when performing in place Update |
So given the following as an example (not all parameters have been defined so do not take this as a literal example)
... | Rename-Many -Pattern '(?\<y>\d{4})-(?\<m>\d{2})-(?\<d>\d{2})' -With '\${d}-\${m}-${y}'
we can see that inside the -With formatter, there are references to named group captures ('d', 'm', 'y') that are defined inside the -Pattern regex.
The following parameters belong to all Rename-Many parameter sets:
(The parameters marked as Interactive are those which are meant to be used interactively. The other parameters are intended for use by developers needing to expand the renaming possibilities of the command.)
General Parameter | Alias | Interactive (non Developer) | DESCRIPTION |
---|---|---|---|
Condition | :heavy_multiplication_x: | :x: | A custom predicate script-block that filters pipeline items |
Context | :heavy_multiplication_x: | :x: | A PSCustomObject with options to customise Rename-Many |
Directory | d | :heavy_check_mark: | Filter for Directory items only |
Diagnose | dg | :heavy_check_mark: | WhatIf mode with added diagnostic info |
End | e | :heavy_check_mark: | Move Pattern |
Except | x | :heavy_check_mark: | Filter out items that match this regex |
File | f | :heavy_check_mark: | Filter for File items only |
Include | i | :heavy_check_mark: | Filter out items that do not match this regex |
Start | s | :heavy_check_mark: | Move matched Pattern to start of items name |
Top | t | :heavy_check_mark: | Process the first n items only |
Transform | :heavy_multiplication_x: | :x: | A script-block to perform custom rename operation |
Whole | :heavy_multiplication_x: | :heavy_check_mark: | Applies 'whole word' match to regex parameters |
During the renaming process, an item maybe renamed resulting in unfavourable characteristics in the resultant file/directory name. Eg, it is not wise to leave a file name with trailing or leading spaces. The following are the characteristics that are automatically fixed by a post processing operation:
For Trim and Spaces, the post processing simply involves removal of the unwanted spaces. For MissingCapture this process involves the removal of any string of the form:
${some-variable}
which occurs as a result of a named group reference defined in a formatter parameter (-With/-Paste) not being populated due to application of a regex parameter to the item and it not matching. An example of this occurring is with the use of a Hybrid Anchor (-AnchorStart/-AnchorEnd), where the user specifies a pattern that includes a named group capture, but for some items, this match may fail (so by design, the match would fall back to being moved to start or end), but the formatter parameter -With contains a reference to that named capture group. When this match fails, the reference '${some-variable}' still remains, which is removed by the post processing operation.
The Dashes rule ensures that we never allow a poor dash formation to occur, something like '- -' which can typically occur if a pattern is designed to work on item names that are denoted by fields split by dashes and an operation moves a field from 1 location to another leaving behind unsightly dash/space sequences. These are normalised away by replacing with a tidy single ' - '.
These post-processing operations are made explicit in the UI because sometimes they occur as a result of a mal-formed regular expression that can be automatically fixed, but doing so may lure the user into thinking they're specifying the regex pattern correctly when in fact a minor correctable mistake has been made. The command aims to be explicit about such issues rather than fix them silently.
The following shows an example of the MissingCapture operation being applied and how it shows up in the output:
As the saying goes, 'a picture is worth a thousand words'. This is particularly cogent when eye-balling a stream of repetitive content generated as a result of an iterative operation such as the result of Rename-Many. Viewing and trying to process a wall of mono-coloured un-structured content can be difficult. This was the rationale behind the use of emojis (generalised into the concept of a Signal in commands, see Loopz) and of another Elizium PowerShell module Krayola. The combination of coloured, consistently structured text with the use of emoji based signals is intended to aid human readability of repetitive content.
For those parameter sets that require the -Pattern parameter (which is most of them; NoReplacement which uses the -Cut parameter, does not), the content that is matched by the Pattern, is removed prior to applying other regex parameters.
It is useful for the user to think about this when composing regex patterns. So for example, given the following input source:
01-From-My-Mind-To-Yours
... a -Pattern defined as:
'\d{2}-'
results in first, the removal of the -Pattern match leaving this behind:
From-My-Mind-To-Yours
It is to this remainder that all other regex parameters are applied.
The undo facility enables a rename batch to be reversed. The location of the scripts is displayed in the rename summary. By default, scripts are saved to '.elizium' under the home ($Home) directory, but this can be overridden.
To change this path, the user should define either an absolute or relative path in the environment variable 'ELIZIUM_PATH'. Relative paths are relative to the $Home directory.
Move Parameter | Alias | DESCRIPTION |
---|---|---|
Anchor | a | Move a match to an anchor |
AnchorEnd | ae | Move a match to an anchor or to end if anchor fails match |
AnchorStart | as | Move a match to an anchor or to start if anchor fails match |
End | e | Move match to end |
Start | s | Move match to start |
With | w | Formatter used when performing Move |
Moves a match from it's current location in an item to a target location known as the Anchor. The anchor itself is a regular expression. All of the parameters in this section, with the exception of -With are mutually exclusive (to see confirmation of this, the user can use the Parameter Set Tools in particular the command Show-ParameterSetInfo (ships), which reveals that they are indeed the unique parameters in their respective parameter sets).
In the following walk-throughs, example invocations are preceded with a :heavy_minus_sign: to indicate a solution that has some scope for improvement. Subsequent to this will be further discussion on how to improve the command and those which are deemed satisfactory are marked with :heavy_plus_sign:. Sometimes, a command does not work in the desired way. These examples are highlighted by a :x:.
Move a regex match identified by -Pattern from the items' name from its current location to a location specified by the -Anchor regex pattern.
Let's say we have a bunch of audio files which are currently named in the form:
\'<DISC-NO>-\<TRACK-NO>_\<TRACK-NAME>.mp3'
(the underscore used in all examples are meant to represent a single space)
... and we wish to move the DISC-NO to after the TRACK-NO. In this case, the DISC-NO would be the subject of the -Pattern match and the TRACK-NO would be the -Anchor.
So an initial attempt of the command could be:
:heavy_minus_sign: Rename-Many -Pattern '\d{2}' -Anchor '\d{2}' -WhatIf
gci ... | Rename-Many -Pattern '\d{2}' -Anchor '\d{2}' -WhatIf
:pushpin: At the end of this section a final version of the command will be illustrated, but we will get there in small steps, so that little nuances can be explained fully.
Focusing on a single file item in the batch being: '02-09 Radio Stars.mp3':
:heavy_minus_sign: Rename-Many -Pattern '\d{2}' -Anchor '\d{2}' -WhatIf
-0902 Radio Stars.mp3
Although the order of the DISC-NO and TRACK-NO have been swapped around, this is almost certainly not what we would want. We need to maintain the dash in between them. We can't include the dash inside the -Pattern because that would just result in '0902-'. This is where the -With formatter parameter and -Relation comes into play. We can format the replacement text:
:heavy_minus_sign: Rename-Many -Pattern '\d{2}' -Anchor '\d{2}' -With '\${_a}-$0' -WhatIf
-09-02 Radio Stars.mp3
This is starting to get better, but there is still a problem. We now have a stray leading dash, but before discussing that issue, the contents of the -With parameter needs explaining. Formatter parameters can access whole regex captures defined by other parameters and/or named/numeric capture groups defined within them. So in this example '\$0' represents the whole -Pattern match which evaluates to '02' and ${_a} represents the whole -Anchor match which evaluates to '09'.
So back to the issue at hand, being the leading stray '-'. We could solve this 1 of 2 ways
:radio_button: -Pattern '(?\<disc>\d{2})-' -Anchor '\d{2}' -With '\${_a}-${disc}' -WhatIf
09-02 Radio Stars.mp3
:radio_button: -Pattern '\d{2}' -Anchor '-(?\<track>\d{2})' -With '${track}-\$0' -WhatIf
09-02 Radio Stars.mp3
Finally, we might decide that the \<TRACK-NO>-\<DISC-NO> sequence needs to be more clearly separated from the \<TRACK-NAME>, so an extra ' - ' is inserted into -With, let's say using technique :one: above (but equally applies to :two:):
:heavy_plus_sign: Rename-Many -Pattern '(?\
09-02 - Radio Stars.mp3
Now that we have our somewhat finalised version, lets see how this looks in a batch:
(actually, the screen shot below uses the Top parameter to reduce the number of items processed, for brevity)
It can be seen that for each item renamed, the new name is displayed in red, with the original name displayed in white. Next to the new name, supplementary info is displayed, including 'Post (Spaces)' (we'll come to this a little later) and a -WhatIf indicator.
At the beginning of the batch, a title is shown with the Locked status highlighted. At the end of the batch, the rename summary is shown, displaying the value of key parameter values and some stats.
The 'Post (Spaces)' previously mentioned, indicates we have made a slight formatting error that has been fixed automatically for us by Post Processing. In this case, we have created a -With formatter that results in consecutive spaces in the resultant rename.
So looking at our definition of -With again:
-With '\${_a}-${disc} - '
That space at the end is our issue. There is already a space preceding the \<TRACK-NAME> which was not captured by either -Pattern or -Anchor, so we don't need to insert another. So adjusting this to be
-With '\${_a}-${disc} -'
... without the trailing space, fixes the problem:
:heavy_plus_sign: Rename-Many -Pattern '(?\<disc>\d{2})-' -Anchor '\d{2}' -With '\${_a}-${disc} -' -Top 10 -WhatIf
The Post Processing is there to watch our back by automatically enforcing desirable rules and makes the command less pedantic in its operation.
In other more complicated rename batches, we might (and probably will) encounter a scenario where the named group captures defined are not doing what we expected. In this case, we can run the command with diagnostics enabled via the -Diagnose parameter.
For this example, when -Diagnose is specified, we can see the value of named capture groups:
Focusing on the first entry, item named '01-01 Autobahn.mp3', we can see the diagnostics entry:
"[🧪] Pattern" => "([☂️] \<disc>)='01', ([☂️] <0>)='01-'"
This tells us that -Pattern contains named group reference(s), in this case:
This is quite a convenient and tidy example, because all the input items are of identical form, but this is not always the case. Let's assume, the first entry in this list: '01-01 Autobahn.mp3' is not named that way, instead it is '01-0 Autobahn.mp3' so the track number is now just a single digit '0'.
In this case, the -Pattern will match because there is still a 2 digit sequence, but the -Anchor will no longer match. This results in this item not being renamed and this is indicated in the output:
:warning: The -With format parameter (and also -Paste & -Drop ) MUST be defined with single quotes. Using double quotes causes string interpolation to occur resulting in named group references to not be evaluated as expected. Let's re-run the last command, but using double quotes for the -With parameter:
:x: Rename-Many -Pattern '(?\<disc>\d{2})-' -Anchor '\d{2}' -With "\${_a}-${disc} -" -Top 10 -WhatIf
This shows that '\${_a}' and '${disc}' are both evaluated to an empty string, breaking the desired result.
The final point worthy of note is the 'Undo Rename' in the summary. By default, all executed commands are undo-able (assuming the undo feature has not been disabled). If we find that after running the command (assuming it has been unlocked and -WhatIf is not specified), the results are not as envisioned (shouldn't really happen, because the -WhatIf should always be used for new executions), the rename can be undone.
The summary contains a path to an undo script under the 'Undo Rename' signal. The user can review its contents first (recommended before running any scripts on a system) and then source that file. The undo script is purely a sequence of renames in reverse with the original name and new names swapped around, thereby reversing the whole batch.
Move a regex match identified by -Pattern from the items' name from its current location to the start of an item's name.
Continuing with the audio files as discussed in Move To Anchor, let's say we want to move the \<TRACK-NO> to the start of an item's name.
Focusing on a single file item in the batch, this time being: '02-06 Airwaves.mp3':
:heavy_minus_sign: :one: Rename-Many -Pattern '\d{2}', 2 -Start -WhatIf
0602- Airwaves.mp3
However, as we discovered in the previous section, we need to do more to obtain a satisfactory result. We can tidy this up, with the use of the -With parameter:
:heavy_plus_sign: Rename-Many -Pattern '-(?\<track>\d{2})' -Start -With '${track}-' -Drop ' -' -WhatIf
06-02 - Airwaves.mp3
Let's explore each of the points that gets us to this result:
Now that we have our finalised version, lets see how this looks in a batch:
(actually, the screen shot below uses the Top parameter to reduce the number of items processed, for brevity)
:warning: When using the -Start and -End anchors (this does not apply to Hybrid Anchors), the user should be aware that if the match is already at the target location, then it will be skipped. For example, if we had a series of directories that contained a date in its name, but the location of the date was inconsistent, we might decide we want to move the date for every directory to the end. However, some directories may already have the date at the end, so there is no point in processing these items. That is why some items may be skipped when using -Start and -End anchors.
Move a regex match identified by -Pattern from the item's name from its current location to the end of its name.
This time, we want to move the \<DISK-NO> to the end of the item's name. The reader might be thinking well isn't this just the opposite to using Start? and they would be right. But in the discussion of Move To End we'll address some slightly different issues/techniques that illustrate other ways the command can be used.
Focusing on a single file item in the batch, this time being: '02-04 Intermission.mp3':
Our initial naive attempt might be:
:heavy_minus_sign: :two: Rename-Many -Pattern '\d{2}' -End -WhatIf
resulting in:
-04 Intermission02.mp3
This works, but it's not very graceful. So again we can optimise this via the -With formatter.
:heavy_minus_sign: Rename-Many -Pattern '(?\<disc>\d{2})-' -End -With ' (disc-${disc})' -WhatIf
04 Intermission (disc-02).mp3
We have chosen to spice up the formatter with extra content, but as we did before, we'll examine all the points that gets us to this state:
However, this is not our chosen solution. We want to insert a dash in between the TRACK-NO and TRACK-NAME as we did before so that our example is renamed to: '04 - Intermission (disc-02).mp3'
Achieving this, requires more work than we completed in our initial attempt :two:
:heavy_plus_sign: Rename-Many -Pattern '(?\<disc>\d{2})-(?\<track>\d{2})' -End -With ' (disc-\${disc})' -Drop '${track} -' -WhatIf
which results in:
04 - Intermission (disc-02).mp3
Bingo! The extra points worthy of note are:
Let's see this in our batch:
(actually, the screen shot below uses the Top parameter to reduce the number of items processed, for brevity)
In the rename batch, some items may match the -Anchor pattern and others may not. Ordinarily, if the -Anchor does not match, then the rename will not occur for this item. This would then require the user to re-run the command with a redefined anchor or run with an entirely different parameter set. However, with a Hybrid Anchor, what we're saying is:
If the specified anchor fails to match, then move the match to the Start or End
So using a hybrid anchor allows the user to perform an anchor oriented operation with a backup if the anchor doesn't match, all in the same batch.
Consider the following directory list in our somewhat contrived example:
We've decided that we want to move the date (if it exists) to precede the fragment '- at'. So in this case the date is the target of the -Pattern match and '- at' is our -Anchor. The directories are not consistently named, so we'll encounter different results from each item.
Let's tailor our requirement a little and say, move the date to the -Anchor if it exists and if it doesn't, then move it to the end. This is where we need a Hybrid Anchor. The hybrid parameters are -AnchorStart and -AnchorEnd.
Before we start using a Hybrid Anchor let's see what happens when we use a regular one:
:heavy_minus_sign: Rename-Many -Pattern '(?\d{2}-\d{2}-\d{4})?' -Anchor '- at' -Relation 'before' -WhatIf
Results in (please excuse the wraparounds):
Things to note:
So in this particular batch run, 2 items are not renamed because the -Anchor match failed. Now let's use a Hybrid Anchor (we simply replace -Anchor with -AnchorEnd):
:heavy_minus_sign: :three: Rename-Many -Pattern '\(?\d{2}-\d{2}-\d{4}\)?' -AnchorEnd '- at' -Relation 'before' -WhatIf
results in:
Now, all the un-skipped items in the batch are renamed. When the Anchor does not match, the date is moved to the end.
But again, the date inserted could do with some alteration. Let's say we wanted to change the date format so that they are in the ISO form '\<YEAR>-\<MONTH>-\<DAY>'. To do this, our -Pattern has to make use of named capture groups again. Also, other literal text can be defined, which we can achieve by using the -With parameter.
But before we move on, let's take another look at our command in :three:. Did you notice anything different about this example? Well, since the directories contain characters that are special to regular expressions and we access them in our -Pattern, they need to be escaped. In this case, the open '(' and close ')' brackets need escaping hence the '\(' and '\)' in the -Pattern.
So, improving our command line, we get to:
:heavy_minus_sign: Rename-Many -Pattern '\(?(?\<d>\d{2})-(?\<m>\d{2})-(?\<y>\d{4})\)?' -AnchorEnd '- at' -With ' - on \${y}-\${m}-\${d} \${_a}' -WhatIf
Resulting in:
Points of note:
On closer inspection, it appears we have an issue. Item 'Underworld - (01-06-1999) - at Brixton Academy' is renamed to: 'Underworld - - on 1999-06-01 - at Brixton Academy'. The '- -' looks ugly and is an un-intended result (caused by the leading ' -' previously mentioned), which occurs because of our -With replacement, not quite meeting the needs of this item. This kind of thing happens regularly and in this situation we can exclude it so that it can be processed in a separate batch. Since this is the only item to be excluded, we can use a very specific discriminating value for the -Except parameter, ie: 'Underworld':
:heavy_plus_sign: Rename-Many -Except 'Underworld' -Pattern '\(?(?\<d>\d{2})-(?\<m>\d{2})-(?\<y>\d{4})\)?' -AnchorEnd '- at' -With ' - on \${y}-\${m}-\${d} ${_a}' -WhatIf
and we finally arrive at:
Typically, -Except would be just generic enough to single out items to be skipped, but can't be so general as to exclude items that ought not to be. We only need to exclude a single item in this case, so our regular expression can be as specific as it needs to be ('Underworld').
What we've learnt about -AnchorEnd hybrid applies identically to -AnchorStart, except that the -Pattern match is moved to the start.
The Drop facility has a beneficial side effect. It can also be used to simply swap 2 pieces of content over.
In this example we will use a new directory list and this time we have a list which is already normalised but we wish to swap over the positions of two fields. Consider the directory list:
We can see that the directories are indeed structured uniformly, but we wish to swap over the date with the location. With the Drop facility, this can be achieved as follows:
:heavy_plus_sign: Rename-Many -Pattern '(?\
and we arrive at:
Points of note:
So in summary, the -Pattern and the -Anchor matches, comprise the two ends of the swap and we use -With and -Drop parameters respectively as the formatters whose contents reference the appropriate named group captures.
Update Parameter | Alias | DESCRIPTION |
---|---|---|
Paste | ps | Formatter used when performing in place Update |
Update-Match simply involves modifying a match in its present location. Since we don't have an -Anchor to deal with, it is much simpler to use than Move Match scenarios.
As well as -Anchor and related parameters, instead of using the -With parameter, we use the -Paste format parameter and it serves a similar purpose. The peculiarities of PowerShell parameter sets means that it is much easier to use a separate parameter, rather than to try and re-use -With in a different context (it is the same reason why new parameters were defined for the Hybrid Anchors, instead of re-purposing -Anchor/-Start/-End).
The file list which was the subject of Move To Hybrid will be used in the following discussion.
This time, we want to update the dates in place, changing the format to be in US date format (mm-dd-yyyy)
:heavy_plus_sign: Rename-Many -Pattern '\(?(?\<d>\d{2})-(?\<m>\d{2})-(?\<y>\d{4})\)?' -Paste '[\${m}-\${d}-\${y}]' -WhatIf
Regex Parameter | Alias | DESCRIPTION |
---|---|---|
Cut | :heavy_multiplication_x: | Remove this match without a replacement |
Simply removes the content matched by -Cut
Let's remove the date element from the directory list featured in the previous section
:heavy_plus_sign: Rename-Many -Cut '\(?(?\<d>\d{2})-(?\<m>\d{2})-(?\<y>\d{4})\)?' -WhatIf
Prefix Parameter | Alias | DESCRIPTION |
---|---|---|
Prepend | pr | Prefix items' name with this literal string |
Appends literal content to start of an item's name.
Using our audio file list, example from Move To Anchor, we can prefix each items name with some literal content:
:heavy_plus_sign: Rename-Many -Prepend 'Kraftwerk - ' -Top 10 -WhatIf
Suffix Parameter | Alias | DESCRIPTION |
---|---|---|
Append | ap | Append this literal string to items' name |
Appends literal content to end of an item's name.
Eg:
:heavy_plus_sign: Rename-Many -Append ' - Kraftwerk' -Top 10 -WhatIf
Type: [array](regular expression, string)
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.
Type: [array](regular expression, string)
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.
Type: [array](regular expression, string)
Similar to -Anchor except that if the pattern specified by -AnchorStart does not match, then the -Pattern match will be moved to the start. This is known as a Hybrid Anchor.
Type: [string]
Appends a literal string to end of item's name.
Type: [ScriptBlock] (predicate)
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.
Type: [PSCustomObject]
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:
Type: [array](regular expression, string)
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 format parameters -Paste and -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 literal 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 formatter parameters 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.
Type: [array](regular expression, string)
Is a replacement for the -Pattern parameter, when a Cut operation is required. The pattern match will be removed from the item's name and no other replacement occurs.
Type: [switch]
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.
Type: [switch]
Indicates 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.
Type: [string]
Only applicable to move operations. Defines what text is used to replace the -Pattern match with. 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 content in the location -Pattern was removed from.
See also Swap Content
Type: [switch]
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.
Type: [string](regular expression)
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.
Type: [switch]
Indicates 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.
Type: [string](regular expression)
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 the anchor parameters) or replaced (with the formatter parameters).
Type: [string]
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.
Type: [array](regular expression, string)
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.
Type: [string]
Prefixes a literal string to start of item's name.
Type: [string]("before" | "after")
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.
Type: [switch]
Another type of anchor used instead of -Anchor and specifies that the -Pattern match should be moved to the start of the new name.
Type: [switch]
Indicates if this is being invoked from a test case, so that the output can be suppressed if appropriate. By default, the test cases should be quiet. During development and test stage, the user might want to see actual output in the console. The presence of variable 'EliziumTest' in the environment will enable verbose tests. When invoked by an interactive user in production environment, the Test flag should not be set. Doing so will suppress the output depending on the presence of 'EliziumTest'. ALL test cases should specify this Test flag.
Type: [int]
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.
Type: [ScriptBlock]
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:
and should return the new name. If the transform does not change the name, it should return an empty string.
Type: [FileSystemInfo]
The pipeline item which should either be an instance of FileInfo or DirectoryInfo.
Type: [char]
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:
Type: [string]
Formatter which 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:
When -Pattern contains named capture groups, these variables can also be referenced. Eg if the -Pattern is defined as '(?\
Typically, -With is literal text which is used to replace the -Pattern match and is inserted according to the anchor parameters and -Relation. When using -With, whatever is defined in the anchor match IS removed from the pipeline's name and requires the user to re-insert it with '${_a}' inside -With if so required. The reason for this is that when -With is not present, the -Pattern match content is inserted verbatim next to the -Anchor either before or after. But if we use a -With, then the user has full control over whereabouts the -Anchor is inserted inside -With, so -Relation is redundant.
Most issues that occur with using Rename-Many are the result of not defining a regex pattern correctly. These clearly can only be fixed, by reviewing the pattern and adjusting accordingly.
It is advised that users always run with -WhatIf enabled for new invocations of Rename-Many, so that the results can be confirmed before being actioned for real. When unexpected results occur, the user can specify the -Diagnose parameter to ensure that named capture groups are working as expected. Readers are also directed to use other 3rd party resources to debug regex patterns such as regex101.
Common issues include:
Provides the facility to reuse the base Rename-Many functionality to build higher level functionality. Since regular expressions are not particularly easy to specify without prior skill and knowledge, it might be advantageous to build a high level command that wraps the base functionality and has embedded within it a commonly used combination of parameters and regular expression definitions. This way the high level version can hide-away difficult to remember regular expressions that are used on a regular basis.
To build a high level Rename-Many command, a developer should perform the following tasks:
Let's say we want to create a command to re-arrange dates in UK format (dd-mm-yyyy) to ISO format 'yyyy-mm-dd', called Convert-Date
:pencil: 1) Define a context
[PSCustomObject]$Context = [PSCustomObject]@{
Title = 'Reformat UK dates to ISO Format';
ItemMessage = 'To ISO format';
SummaryMessage = 'UK Dates Converted to ISO';
Locked = 'CONVERT_UK_DATES_LOCKED';
UndoDisabledEnVar = 'CONVERT_UK_DATES_UNDO_DISABLED';
OperantShortCode = 'convuk';
}
Taking one of the previously displayed screen-shots ...:
... we can see, where some of those context items appear in the output. From this screen-shot:
The other non UI elements:
:pencil: 2) Define the arguments passed into 'Rename-Many'
These arguments will be hardcoded into Convert-Date, so that the end user doesn't have to specify them.
:pencil: 3) Define new user facing parameters
We can either accept file system objects from the pipeline (but they would need to be collected up and passed into a new pipeline involving Rename-Many) or define a new -Path like parameter. We'll do the latter in this case and call it -LiteralPath.
We probably need to replicate -WhatIf and -Diagnose, which would be forwarded onto Rename-Many. In the case of -WhatIf, we don't define that explicitly, instead we decorate our function with SupportsShouldProcess. But it should be noted that generally, the value of -WhatIf flows from the user invoked command to other standard PowerShell functions (eg Move-Item), but it doesn't flow from one 3rd party command to another unless they are in the same module. That is to say, -WhatIf does not cross module boundaries (except the standard PowerShell functions). This means we need to forward the value of -WhatIf explicitly, by doing something like:
-WhatIf:$($PSBoundParameters.ContainsKey('WhatIf'))
Since $WhatIf doesn't exist in its own right, we need to use $PSBoundParameters to see if it is present in the bound parameters.
Now we end up with a command signature as follows:
function Convert-Date {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
[CmdletBinding(SupportsShouldProcess)]
[Alias('convuk')]
param(
[Parameter()]
[string]$LiteralPath,
[Parameter()]
[switch]$Diagnose
)
...
}
:pencil: 4) Define the implementation
Typically, the parameters to Rename-Many would be splatted, via a hashtable.
function Convert-Date {
# signature not repeated, see previous code snippet instead
[PSCustomObject]$context = @{
Title = 'Reformat UK dates to ISO Format';
ItemMessage = 'To ISO format';
SummaryMessage = 'UK Dates Converted to ISO';
Locked = 'CONVERT_UK_DATES_LOCKED';
UndoDisabledEnVar = 'CONVERT_UK_DATES_UNDO_DISABLED';
OperantShortCode = 'convuk';
}
[hashtable]$parameters = @{
'Pattern' = '(?<d>\d{2})-(?<m>\d{2})-(?<y>\d{4})';
'Paste' = '(${y}-${m}-${d})';
'Context' = $context;
'Diagnose' = $Diagnose.IsPresent;
'WhatIf' = $PSBoundParameters.ContainsKey('WhatIf');
}
Get-ChildItem -LiteralPath $LiteralPath | Rename-Many @parameters
}
The user can use the higher order command Convert-Date with a simpler more specialised interface instead of Rename-Many, eg:
:heavy_plus_sign: Convert-Date -LiteralPath ~/logs -WhatIf
Note, there is another way of complementing the functionality of existing commands that would apply in this scenario and that is with a Proxy Command. This is a slightly more involved process but contains some of the same techniques just discussed. It's out of the scope of this documentation, but here is a blog post that describes how to apply this technique.
Another way to obtain custom functionality from Rename-Many is to provide a custom script-block for the Transform parameter. This will be illustrated with an example that replaces all '[' with '(' and all ')' with ']'. This might be useful to skirt round a widely encountered problem using the -Path parameter of mutating commands like Rename-Item which ascribes custom semantics to '[' and ']' in file system paths, causing un-expected results. (This can be averted using -LiteralPath, but this is just an example to discuss using the Transform parameter.)
The signature of the Transform script-block is as follows
param(
[Parameter()]
[string]$Original,
[Parameter()]
[string]$Renamed,
[Parameter()]
[string]$PatternCapture,
[Parameter()]
[hashtable]$Exchange
)
where:
This could be implemented as follows
[ScriptBlock]$transformer = [ScriptBlock] {
param($Original, $Renamed, $PatternCapture, $Exchange)
return $Original.Replace('[', '(').Replace(']', ')');
}
... and theoretically, could be invoked as:
:heavy_multiplication_x: Rename-Many -Pattern '\[[\w]+\]' -Transform $transformer -WhatIf
But, the Transform parameter is not meant to be used interactively (although it could be, but would be cumbersome). Rather, the intention is that the user would create a Higher Order Command so invoking this function interactively would become more convenient on the command line.
:pushpin: In this example, the -Pattern becomes a filter, such that only items containing any word characters (\w) inside square brackets are processed.
This section will contain some examples that solve common renaming requirements. They are intended to give the reader a jump start in defining their own regular expressions and formatters to use with Rename-Many.
This module has the following developer dependencies:
After cloning the repo, change to the Elizium.RexFs directory from the root. You can look at the build script Elizium.RexFs.build.ps1, it will contain various tasks, the most important of which are explained below
To build the module and run the unit tests:
invoke-build
To build the module only:
invoke-build build
To Run the unit tests only (assuming already built)
invoke-build tests
To build external help:
invoke-build buildHelp
:warning: Elizium.RexFs makes use of PowerShell classes. Because of the nature of classes in PowerShell, re-building edited code can cause errors. This is not a fault of the Elizium.RexFs code, it's just the way PowerShell classes have been designed.
What you will find is, if a class has been modified then rebuilt in the same session, you may find multiple class errors like so:
[-] EndAdapter.given: EndAdapter.should: get name 31ms (30ms|1ms)
PSInvalidCastException: Cannot convert the "EndAdapter" value of type "EndAdapter" to type "EndAdapter".
ArgumentTransformationMetadataException: Cannot convert the "EndAdapter" value of type "EndAdapter" to type "EndAdapter".
at <ScriptBlock>, ..\github\PoSh\RexFs\Elizium.RexFs\Tests\Rename-Many.tests.ps1:21
Fear not, this is just reporting that the class definition has changed and because of this difference, one can't be substituted for another in the same PowerShell session (this is in contrast to the way functions work, where you can simply re-define a function in the same session and it will replace the previous definition. This luxury has not been afforded to classes unfortunately). All that's required is to restart a new session. The rebuild in the new session should progress without these errors.
It is a bit onerous having to restart a session for every build, but below is a function that can be defined in the users powershell profile that when invoked, begins a restart loop. Now, when an exit is issued, the session is automatically restarted:
Insert this into your PowerShell session file.
function Get-TagPath {
return Join-Path $env:temp -ChildPath 'restart-session.tag.txt';
}
function Restart-Session {
[Alias('ress')]
param()
[string]$tagPath = Get-TagPath;
if (-not([string]::IsNullOrEmpty($env:tag))) {
Set-Content -Path $tagPath -Value $env:tag;
}
elseif (Test-Path -Path $tagPath) {
Remove-Item -Path $tagPath;
}
[System.Management.Automation.PathInfo]$pathInfo = Get-Location;
while ($true) {
pwsh -Command {
[string]$tagPath = Get-TagPath;
[string]$greeting = "🍺 Restarted!";
if (Test-Path -Path $tagPath) {
$tag = Get-Content -Path $tagPath;
if (($tag -is [string]) -or ($tag -is [string[]])) {
$env:tag = $tag;
$greeting = "🍺 Restarted! (Pester Tag: '$env:tag' ✔️)";
}
}
Write-Host -ForegroundColor 'Cyan' $greeting;
} -NoExit -WorkingDirectory $($pathInfo.Path)
if ($LASTEXITCODE) {
break
}
}
}
Another feature this function possesses is the restoration of the Tag environment variable. The Tag is used to control which testcases Pester runs. Pester contains a Tag in its configuration and when set, it will only run those test cases decorated with this tag value.
So, when a restart occurs, the Tag if set is restored and you will see which tag is in play as part of the restart. If no tag is found then no tag is restored. This function just helps the tedium of having to keep redefining the Tag in-between restarts, as now this is automatically restored.
The sequence goes:
After restart, tag is restored and the restart message will indicate as such
... and repeat.
The user can set this flag in the environment (just set it to any non $null value).
By default, $env:EliziumTest, will not be present, this means, that the unit tests in Elizium.RexFs will run in silent mode. However, there are some tests which are less valuable in silent mode, doing so would invalidate them to some degree. There are only a few of the tests in this category (tagged as 'Host') and it's because they require Write-Host to be invoked. Theoretically, one could mock out the Write-Host call, but some errors can be much easier to spot visually. This generally is not the best technique in unit-testing, but these test cases have been backed up by non noisy equivalents to make sure all bases are covered.
During development, it is very useful to get a visual on how ui commands are behaving. This was the rationale behind the introduction of this flag. So when EliziumTest is defined, the user will see more output that reflects the execution of the Scribbler and Krayon.