Renaming files using PowerShell

From Svendsen Tech PowerShell Wiki
Jump to: navigation, search

In this article I extensively demonstrate how to rename files in PowerShell, using custom criteria with if statements, regexes, splitting, string methods like ToUpper() for upper-casing and ToLower() for lower-casing, inserting stuff into specific places in a file name, and removing certain parts of a file name.

As I wrote this article, I realized it's really as much about string manipulation as renaming files per se.

The code and examples in this article work with PowerShell version 3 and up (Windows Management Framework 3.0), unless otherwise stated. Some of the things will have to be done differently with PowerShell version 2 and earlier, but the Rename-Item and code in the -NewName parameter itself is compatible with v2.

I will be using Get-ChildItem and one of its aliases: "dir" (also "ls" and "gci"). They are the same (dir, ls and gci are aliases to Get-ChildItem).

Remember that you can always add the flag "-Recursive" to Get-ChildItem to replace recursively.

If you are using Get-ChildItem in PSv2, you can filter out only files using Where-Object and the boolean property "PSIsContainer". In PowerShell v3 and up you have the -File and -Directory parameters for Get-ChildItem for filtering this.

For files only in PSv2:

Get-ChildItem -Path C:\temp | Where { -not $_.PSIsContainer } | Rename-Item ...

For directories only in PSv2:

Get-ChildItem -Path c:\temp | Where { $_.PSIsContainer } | Rename-Item ...




The extremely basic example

Not using Get-ChildItem to retrieve target files, and not using a script block for -NewName, you can rename a single file or directory like this:

PS C:\temp\dir> "" > file.txt # creates an empty file called file.txt
PS C:\temp\dir> Rename-Item -Path file.txt -NewName NewFileName.txt
PS C:\temp\dir> (dir .\NewFileName.txt).Name
NewFileName.txt

I should mention the -WhatIf parameter right off the bat. If you're unsure, use test files, and if you trust the PowerShell team to always implement -WhatIf support in the Rename-Item cmdlet, you can run it against the real deal as well. Testing the actual -WhatIf first is something we paranoid people do on new systems.

PS C:\temp\dir> dir -Path test*.log | Rename-Item -NewName { $_ -replace '\.log$', '.txt' } -WhatIf
What if: Performing the operation "Rename File" on target "Item: C:\temp\dir\test.log Destination: C:\temp\dir\test.txt".

Upper- and lowercasing file names

Getting file names using Get-ChildItem and then piping to, and using a script block for Rename-Item’s –NewName parameter makes this very easy and flexible. Remember that you can simply add the switch flag "-Recursive" to Get-ChildItem to do this recursively.

Uppercasing the entire file name can be done, for instance like this:

PS C:\temp\dir> (Get-ChildItem -File).Name
random-01.txt
random-02.txt
random-03.txt
random-04.txt

PS C:\temp\dir> Get-ChildItem -File -Path *.txt | Rename-Item -NewName { $_.Name.ToUpper() }

PS C:\temp\dir> (Get-ChildItem -File).Name
RANDOM-01.TXT
RANDOM-02.TXT
RANDOM-03.TXT
RANDOM-04.TXT

To make the same file names all lowercase, we can use the same technique, except for replacing the System.String method ToUpper() (here used on the string "$_.Name") with ToLower().

PS C:\temp\dir> (dir -File).Name
RANDOM-01.TXT
RANDOM-02.TXT
RANDOM-03.TXT
RANDOM-04.TXT

PS C:\temp\dir> Get-ChildItem -File -Path *.txt | Rename-Item -NewName { $_.Name.ToLower() }

PS C:\temp\dir> (dir -File).Name
random-01.txt
random-02.txt
random-03.txt
random-04.txt

Changing file extensions

Let's say we want to rename all files with the .txt extension to instead have the extension .log. This can be done multiple ways, and I'll demonstrate both using the BaseName property of the FileInfo object from Get-ChildItem, as well as with using a regex.

PS C:\temp\dir> (dir -File).Name
pseudorandom-01.txt
pseudorandom-02.txt
pseudorandom-03.txt
random-06.txt
random-07.txt
random-08.txt
PS C:\temp\dir> dir -File | Rename-Item -NewName { $_.BaseName + '.log' }
PS C:\temp\dir> (dir -File).Name
pseudorandom-01.log
pseudorandom-02.log
pseudorandom-03.log
random-06.log
random-07.log
random-08.log

The above approach would generally be preferred. Using a regex, you could do something like the following, where I change the extensions back to .txt.

PS C:\temp\dir> dir -File | Rename-Item -NewName { $_.Name -replace '(\.[^.]+)$', '.txt' }
PS C:\temp\dir> (dir -File).Name
pseudorandom-01.txt
pseudorandom-02.txt
pseudorandom-03.txt
random-06.txt
random-07.txt
random-08.txt

Removing parts from, or adding parts to, a file name

We're beginning with a simple example where we want strings starting with "random-" to be renamed to "random-x-" and then the rest of the file name to follow it as-is.

Again, we're using the script block argument for -NewName, and this is a breeze. See demo code. The -replace operator, which you can read more about here, takes a regex (regular expression) as its first parameter, and a string as its second (if the second parameter is left out, the regex part will be removed, as if you had passed an empty string).

The regex meta-character "^" means "the beginning of the string". "$" means the end (or before a newline before the end - read more about PowerShell regular expressions here).

PS C:\temp\dir> Get-ChildItem -Path *.txt | Format-Table -AutoSize
    Directory: C:\temp\dir
Mode         LastWriteTime Length Name         
----         ------------- ------ ----         
-a--- 2016-08-15     21:41   1024 random-01.txt
-a--- 2016-08-15     21:41   1024 random-02.txt
-a--- 2016-08-15     21:41   1024 random-03.txt
-a--- 2016-08-15     21:41   1024 random-04.txt

PS C:\temp\dir> Get-ChildItem -Path *.txt | Rename-Item -NewName { $_.Name -replace '^random-', 'random-x-' }

PS C:\temp\dir> Get-ChildItem -Path *.txt | Format-Table -AutoSize
    Directory: C:\temp\dir
Mode         LastWriteTime Length Name           
----         ------------- ------ ----           
-a--- 2016-08-15     21:41   1024 random-x-01.txt
-a--- 2016-08-15     21:41   1024 random-x-02.txt
-a--- 2016-08-15     21:41   1024 random-x-03.txt
-a--- 2016-08-15     21:41   1024 random-x-04.txt

Another, equally complex, example (it gets a bit worse if you read on). Let's pretend we have a need to add the string "y" after every letter (string) "x" in a file name, but only if this letter x is surrounded by hyphens on either side.

Using the same files as in the example above, we'll do just this, using the -replace operator again.

PS C:\temp\dir> Get-ChildItem -Path *.txt | Rename-Item -NewName { $_.Name -replace '-x-', '-xy-' }

PS C:\temp\dir> Get-ChildItem -Path *.txt | Format-Table -AutoSize
    Directory: C:\temp\dir
Mode         LastWriteTime Length Name            
----         ------------- ------ ----            
-a--- 2016-08-15     21:41   1024 random-xy-01.txt
-a--- 2016-08-15     21:41   1024 random-xy-02.txt
-a--- 2016-08-15     21:41   1024 random-xy-03.txt
-a--- 2016-08-15     21:41   1024 random-xy-04.txt

Now we'll increase the complexity a bit and pretend our logical requirement is to insert a string before the first number that might occur in a file name (we'll do before the last number next). Let's go for the string "zzz", to be sufficiently boring.

To do this, I'll use Get-ChildItem, a pipe, and a script block for Rename-Item's -NewName parameter again, and the -replace operator with captures.

PS C:\temp\dir> Get-ChildItem -Path *.txt | Format-Table -AutoSize
    Directory: C:\temp\dir
Mode         LastWriteTime Length Name         
----         ------------- ------ ----         
-a--- 2016-08-17     21:58   1024 random-01.txt
-a--- 2016-08-17     21:58   1024 random-02.txt
-a--- 2016-08-17     21:58   1024 random-03.txt
-a--- 2016-08-17     21:58   1024 random-04.txt

PS C:\temp\dir> Get-ChildItem -Path *.txt | Rename-Item -NewName { $_.Name -replace '^(\D+)(\d.*)', '${1}zzz${2}' }

PS C:\temp\dir> Get-ChildItem -Path *.txt | Format-Table -AutoSize
    Directory: C:\temp\dir
Mode         LastWriteTime Length Name            
----         ------------- ------ ----            
-a--- 2016-08-17     21:58   1024 random-zzz01.txt
-a--- 2016-08-17     21:58   1024 random-zzz02.txt
-a--- 2016-08-17     21:58   1024 random-zzz03.txt
-a--- 2016-08-17     21:58   1024 random-zzz04.txt

As promised, here's how to insert the string "x" before the last number in the file name (string).

PS C:\temp\dir> Get-ChildItem -Path *.txt | Format-Table -AutoSize
    Directory: C:\temp\dir
Mode         LastWriteTime Length Name         
----         ------------- ------ ----         
-a--- 2016-08-17     22:21   1024 random-01.txt
-a--- 2016-08-17     22:21   1024 random-02.txt
-a--- 2016-08-17     22:21   1024 random-03.txt
-a--- 2016-08-17     22:21   1024 random-04.txt

PS C:\temp\dir> Get-ChildItem -Path *.txt | Rename-Item -NewName { $_.Name -replace '^(.*)(\d.*?)$', '${1}x${2}' }

PS C:\temp\dir> Get-ChildItem -Path *.txt | Format-Table -AutoSize
    Directory: C:\temp\dir
Mode         LastWriteTime Length Name          
----         ------------- ------ ----          
-a--- 2016-08-17     22:21   1024 random-0x1.txt
-a--- 2016-08-17     22:21   1024 random-0x2.txt
-a--- 2016-08-17     22:21   1024 random-0x3.txt
-a--- 2016-08-17     22:21   1024 random-0x4.txt

Removing a character from a given position in a file name

Let's now pretend we want to remove characters 4-6 in the file name. Again, we go to the -replace operator - it's much easier than some convoluted System.String.SubString() soup. SubString() will usually be faster. If performance matters for this scenario, you can see this article for more info on removing characters from a string ("first or last" is discussed there, but it can be adapted; experimentation recommended).

PS C:\temp\dir> Get-ChildItem -Path *.txt | Format-Table -AutoSize
    Directory: C:\temp\dir
Mode         LastWriteTime Length Name          
----         ------------- ------ ----          
-a--- 2016-08-17     22:21   1024 random-0x1.txt
-a--- 2016-08-17     22:21   1024 random-0x2.txt
-a--- 2016-08-17     22:21   1024 random-0x3.txt
-a--- 2016-08-17     22:21   1024 random-0x4.txt

PS C:\temp\dir> Get-ChildItem -Path *.txt | Rename-Item -NewName { $_.Name -replace '^(.{3}).{3}(.*)', '${1}${2}' }

PS C:\temp\dir> Get-ChildItem -Path *.txt | Format-Table -AutoSize
    Directory: C:\temp\dir
Mode         LastWriteTime Length Name       
----         ------------- ------ ----       
-a--- 2016-08-17     22:21   1024 ran-0x1.txt
-a--- 2016-08-17     22:21   1024 ran-0x2.txt
-a--- 2016-08-17     22:21   1024 ran-0x3.txt
-a--- 2016-08-17     22:21   1024 ran-0x4.txt

Match evaluator replace action

Complexity currently going up a notch. This time around, we pretend to have a need to increment a number by one in a file name. If the number is 1, it should change to 2, if it is 9, it should change to 10, and so on. If there is more than one number, we increase them all. Anchor on a fixed part of the file name or on the beginning or end to target only one number (using "^" for "beginning of string" or "$" for "end of string" - see more info in the regex article).

I'll use the [regex] class' Replace() method with a script block as the third argument. This is a "match evaluator", and again I refer to the regex article for further information.

PS C:\temp\dir> Get-ChildItem -Path *.txt | Format-Table -AutoSize
    Directory: C:\temp\dir
Mode         LastWriteTime Length Name       
----         ------------- ------ ----       
-a--- 2016-08-17     22:21   1024 ran-0x1.txt
-a--- 2016-08-17     22:21   1024 ran-0x2.txt
-a--- 2016-08-17     22:21   1024 ran-0x3.txt
-a--- 2016-08-17     22:21   1024 ran-0x4.txt

PS C:\temp\dir> Get-ChildItem -Path *.txt | Rename-Item -NewName { [regex]::Replace($_.Name, '(\d+)', { $TempNum = [int] $args[0].Groups[1].Value; $TempNum + 1 }) }

PS C:\temp\dir> dir -File
    Directory: C:\temp\dir
Mode                LastWriteTime     Length Name                                                                                                                                      
----                -------------     ------ ----                                                                                                                                      
-a---        2016-08-17     22:21       1024 ran-1x2.txt                                                                                                                               
-a---        2016-08-17     22:21       1024 ran-1x3.txt                                                                                                                               
-a---        2016-08-17     22:21       1024 ran-1x4.txt                                                                                                                               
-a---        2016-08-17     22:21       1024 ran-1x5.txt

Now, if you're going to increment numbers of log files that have names as shown here, and also want to preserve the zero-padding, but still perform numerical operations, you will soon realize that Rename-Item will clash with existing files, except for the last one, as demonstrated here:

PS C:\temp\dir> (dir -File).Name
random_file-01.log
random_file-02.log
random_file-03.log
random_file-04.log
random_file-05.log
random_file-06.log
random_file-07.log
random_file-08.log
random_file-09.log
random_file-10.log
random_file-11.log

PS C:\temp\dir> dir -File | Rename-Item -NewName { 
    [regex]::Replace($_.Name, '(\d+)', {
        $n = [int] $args[0].Groups[1].Value
        $n += 1
        "{0:D2}" -f $n
    })
}
Rename-Item : Cannot create a file when that file already exists.
At line:1 char:13
+ dir -File | Rename-Item -NewName {
    + CategoryInfo          : WriteError: (C:\temp\dir\random_file-01.log:String) [Rename-Item], IOException
    + FullyQualifiedErrorId : RenameItemIOError,Microsoft.PowerShell.Commands.RenameItemCommand
 
Rename-Item : Cannot create a file when that file already exists.
At line:1 char:13
+ dir -File | Rename-Item -NewName {
    + CategoryInfo          : WriteError: (C:\temp\dir\random_file-02.log:String) [Rename-Item], IOException
    + FullyQualifiedErrorId : RenameItemIOError,Microsoft.PowerShell.Commands.RenameItemCommand
....
# and 8 more of these ...

And we can see that only the last file was actually renamed:

PS C:\temp\dir> (dir -File).Name
random_file-01.log
random_file-02.log
random_file-03.log
random_file-04.log
random_file-05.log
random_file-06.log
random_file-07.log
random_file-08.log
random_file-09.log
random_file-10.log
random_file-12.log

Notice how only the last file name changed. It's a good thing Rename-Item didn't just overwrite. I'm guessing the -Force parameter will make it do that (you usually really don't want that).

One trick to pull out that will often work is simply sorting on the name descendingly, but beware that if the numbers aren't zero-padded (e.g. "001" rather than "1"), the sorting will be as text, and may give you inconsistent and bad results. You could pull out the tricks I mention in the article I wrote about sorting with numbers zero-padded in the sorting for those situations.

Here's the trick with sorting on names descendingly. I certainly recommend testing on test files first, not directly in the real environment. Also remember the -WhatIf parameter.

PS C:\temp\dir> (dir -File).Name
random_file-01.log
random_file-02.log
random_file-03.log
random_file-04.log
random_file-05.log
random_file-06.log
random_file-07.log
random_file-08.log
random_file-09.log
random_file-10.log
random_file-11.log

PS C:\temp\dir> dir -File | Sort -Descending Name | 
  Rename-Item -NewName { 
    [regex]::Replace($_.Name, '(\d+)', {
        $n = [int] $args[0].Groups[1].Value
        $n += 1
        "{0:D2}" -f $n
    })
}

PS C:\temp\dir> (dir -File).Name
random_file-02.log
random_file-03.log
random_file-04.log
random_file-05.log
random_file-06.log
random_file-07.log
random_file-08.log
random_file-09.log
random_file-10.log
random_file-11.log
random_file-12.log

PS C:\temp\dir> 

Match evaluator, custom date formats and date manipulation

Here I demonstrate some arbitrarily sophisticated date manipulation techniques (when adapted) in a self-contained code example with a few comments in the code.

See this StackOverflow article for some more information about the datetime culture stuff. I just pass $null. I've seen [cultureinfo]::InvariantCulture being passed often.

PS C:\temp\dir> 1..8 | %{ $t = $_*7; '' > "FileWithDate-$(
(Get-Date).AddDays($t).ToString('yyyy-MM-dd_HH-mm-ss')).log" }
# create files with dates

PS C:\temp\dir> (dir -File).Name 
FileWithDate-2016-11-30_02-52-12.log
FileWithDate-2016-12-07_02-52-12.log
FileWithDate-2016-12-14_02-52-12.log
FileWithDate-2016-12-21_02-52-12.log
FileWithDate-2016-12-28_02-52-12.log
FileWithDate-2017-01-04_02-52-12.log
FileWithDate-2017-01-11_02-52-12.log
FileWithDate-2017-01-18_02-52-12.log

PS C:\temp\dir> # now I want all the dates
                # shifted 30 days back in time ...

PS C:\temp\dir> Get-ChildItem -File | Rename-Item -NewName {
    [regex]::Replace($_.Name, "\d{4}-\d\d-\d\d_\d\d-\d\d-\d\d", {
        if ($DateTime = [datetime]::ParseExact($args[0].Value, "yyyy-MM-dd_HH-mm-ss", $null)) {
            $DateTime = $DateTime.AddDays(-30)
            $DateTime.ToString('yyyy-MM-dd_HH-mm-ss')
        }
    })
}

PS C:\temp\dir> (dir -File).Name 
FileWithDate-2016-10-31_02-52-12.log
FileWithDate-2016-11-07_02-52-12.log
FileWithDate-2016-11-14_02-52-12.log
FileWithDate-2016-11-21_02-52-12.log
FileWithDate-2016-11-28_02-52-12.log
FileWithDate-2016-12-05_02-52-12.log
FileWithDate-2016-12-12_02-52-12.log
FileWithDate-2016-12-19_02-52-12.log

Renaming files and using an if statement for conditions

I'm going back to the files used in the example earlier in the article where I change extensions, but now I want to change the files starting with "pseudo" to have the extension .pseudo, and the files beginning with "random" to have the (different) extension .rand.

PS C:\temp\dir> (dir -File).Name
pseudorandom-01.txt
pseudorandom-02.txt
pseudorandom-03.txt
random-06.txt
random-07.txt
random-08.txt
PS C:\temp\dir> Get-ChildItem -File | 
    Rename-Item -NewName {
        if ($_.Name.StartsWith('pseudo')) { $ext = '.pseudo' }
        elseif ($_.Name.StartsWith("random")) { $ext = '.rand' }
        else { $ext = $_.Extension }  # no match = keep current ext
    $_.BaseName + $ext
}
PS C:\temp\dir> (dir -File).Name
pseudorandom-01.pseudo
pseudorandom-02.pseudo
pseudorandom-03.pseudo
random-06.rand
random-07.rand
random-08.rand