Renaming Multiple Files in PowerShell

I’ve got a whole bunch of PDF files for books. Many of them were from the fantastic offers that Packt do such as this or which allow you to receive a free book each day (and download it, which isn’t possible any more), but some were purchases or downloads of sample chapters etc.

As you can imagine, they are named in a whole host of differing ways depending on authors, publishers, editions and titles, which is jarring to my need for consistency! If I could choose? My preference would be to have the names:

  • all in lowercase,
  • with no spaces, and
  • using abbreviations where I can.

As an example of that latter point, I like to use the string “ed“ rather than “edition“ which is a small thing perhaps, but it makes the filename smaller and easier to read.

To illustrate this better, here’s an example file:

1
Nice PDF book from Book Publisher, 3rd Edition.pdf

and I would rather it was named:

1
nice-pdf-book-from-book-publisher-3rd-ed.pdf

No big deal, right? But the crazy thing was that I started to do this manually. If I had a dozen books or so, it wouldn’t have been a problem but when you have many, many more, it’s a form of madness. As in the past, let’s turn to PowerShell to get the job done quickly and easily.

To solve this, we’re going to broadly follow these steps:

  1. Get a list of files, including sub-directories
  2. Make the filename lowercase
  3. Replace any abbreviations
  4. Take out any funky characters like exclamation marks and commas
  5. Remove all spaces and replace with hyphens
  6. Actually rename the file!

Getting a List of Files

We can achieve this using a commandlet called Get-ChildItem. Two options are going to matter here: 1. The file pattern we are looking for (which will be *.pdf) and 2. The fact that we want to recurse through any subdirectories, picking up any files there, too.

1
2
3
cd "c:\temp\IT Books"
$files = Get-ChildItem *.pdf -Recurse
$files

Wrapped around that above are a quick change into the directory we want to examine, and something that shows us what the commandlet found. Let’s take a look at the results of running that now:

1
2
3
4
5
6
7
8
9
10
11
12
    Directory: C:\temp\IT Books\SubFolder

Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 18/06/2020 11:53 1894845 Another Nice PDF book from Book Publisher, 3rd Edition.pdf

Directory: C:\temp\IT Books
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 18/06/2020 11:53 1894845 Nice PDF book from Book Publisher, 3rd Edition.pdf

PS C:\temp\IT Books>

Great! We’ve found two files:

  • SubFolder\Another Nice PDF book from Book Publisher, 3rd Edition.pdf and,
  • Nice PDF book from Book Publisher, 3rd Edition.pdf

Bet you haven’t got those PDFs in your library!

Next, we are going to need to iterate over all the files found, so for that we need a foreach loop. We can add that in now together with a way to display a couple of the possible fields.

1
2
3
4
5
6
Foreach ($file in $files)
{

$file.DirectoryName
$file.Name
}

Over to the output, we can now see:

1
2
3
4
C:\temp\IT Books\SubFolder
Another Nice PDF book from Book Publisher, 3rd Edition.pdf
C:\temp\IT Books
Nice PDF book from Book Publisher, 3rd Edition.pdf

Perfect! We’ll need that folder and filename separated soon, so keep that in mind. That’s our first step complete - get the files from the subdirectories. What about the lowercasing of the filename?

Making the Filename Lowercase

To start, let’s place the name of the file into a variable.

1
$new_name = $file.Name

But why stop there? We can chain the assignment to lowercase the string at the same time:

1
$new_name = $file.Name.ToLower()

If we add that to our script, removing some of the output statements, what does it look like?

1
2
3
4
5
6
7
8
cd "c:\temp\IT Books"
$files = Get-ChildItem *.pdf -Recurse

Foreach ($file in $files)
{
$new_name = $file.Name.ToLower()
$new_name
}

Running that gives us:

1
2
another nice pdf book from book publisher, 3rd edition.pdf
nice pdf book from book publisher, 3rd edition.pdf

We’re gaining momentum :-)

Removing Abbreviations

For this, we are going to be using more string manipulation, and leveraging the -replace parameter on strings. I’ll just show you and hopefully it will be obvious:

1
2
$new_name = $new_name -replace 'edition', 'ed'
$new_name

That will take any mention of the string edition and replace it with ed. As an example, here’s what both manipulations (lowercasing and replacing) would do to our file named: Nice PDF book from Book Publisher, 3rd Edition.pdf

1
nice pdf book from book publisher, 3rd ed.pdf

Removing Funky Characters

Time to tackle the funk! I think we should remove commas, brackets, exclamation marks and apostrophes because Windows and my tired eye-balls hate them.

1
2
3
4
5
6
7
8
9
10
11
12
# remove brackets
$new_name = $new_name -replace "\(", ''
$new_name = $new_name -replace "\)", ''

# remove apostrophes
$new_name = $new_name -replace "'", ''

# remove exclamation marks
$new_name = $new_name -replace "!", ''

# remove commas
$new_name = $new_name -replace ',', ''

From the above, the only thing unusual is the escaping that I’ve needed to do with the brackets; you can see I preface them with a backward-slash? I’ve also used double-quotes in places, but especially for where we have the single-quote removal so that PowerShell doesn’t get confused.

We’ve got quite a few new lines to add, so let’s put them all together to see what we have so far:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
cd "c:\temp\IT Books"
$files = Get-ChildItem *.pdf -Recurse

Foreach ($file in $files)
{
# begin with a lower case name
$new_name = $file.Name.ToLower()

# map edition to => ed
$new_name = $new_name -replace 'edition', 'ed'

# remove brackets
$new_name = $new_name -replace "\(", ''
$new_name = $new_name -replace "\)", ''

# remove apostrophes
$new_name = $new_name -replace "'", ''

# remove exclamation marks
$new_name = $new_name -replace "!", ''

# remove commas
$new_name = $new_name -replace ',', ''

$new_name
}

Almost done. We’ve just got to handle those spaces and do the actual renaming.

Removing Spaces and Replacing With Hyphens

Your first thought here would be just to do something like this:

1
$new_name = $new_name -replace ' ', '-'

and that would work. Kind of. What if you had a filename with two spaces in it like this: filename book.pdf? That would become: filename--book.pdf. Is that what you intended? Or how about this: filename - book.pdf => filename---book.pdf?

Personally, I would like to compress all multiple spaces/hyphens into one single hyphen. Also, I’d like to remove any underscores _ and make those hyphens too, for good measure.

Here’s how I did it. You can take a look and try to understand it, but I’ll explain my thinking straight afterwards anyway.

1
2
3
4
5
6
7
8
# handle spacing
$new_name = $new_name -replace ' ', '-'
$new_name = $new_name -replace '_', '-'
do
{
$last_new_name = $new_name
$new_name = $new_name -replace "--", "-"
} while ($last_new_name -ne $new_name)

The first line replaces spaces with hyphens. The second handles replacing the underscores. Fine. Then we come to the loop. That basically states that we should see what the current name is, and assign it to $last_new_name. Then try and replace any double-hyphens with singular ones, storing the result in $new_name. The loop’s condition checks if the two variables are not the same - meaning we managed to replace a double-hyphen with a singular, changing it - and we ring the boxing bell again, and go to the next round to have another go. I mean iteration.

At the end of the loop, both variables will be the same, meaning that no more alterations were made and, fingers crossed, we have no double-hyphens spoiling our “look and feel”.

Renaming the File

We are at the finishing post now so we just need to finally rename the file. This is where we’re going to make use of that field which gave us the folder name so that we can rebuild the new path. I’ll show you first, and then as before, explain it.

1
2
3
$new_path = ($file.DirectoryName + "\" + $new_name)
Write-Host "Renaming:" $file.FullName "to" $new_path
Rename-Item $file.FullName $new_path

$new_path is our new variable which consists of the directory path in which the file resided, coupled with a directory separator (the backslash) and our new filename which we have just generated after all our manipulations.

We then echo something to the screen with the Write-Host commandlet, which is another way for displaying text. That’s right - I like to ying and yang in my scripts.

Lastly, we use the Rename-Item commandlet to do the heavy lifting and perform the actual renaming.

Here’s the whole script as one:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
cd "c:\temp\IT Books"
$files = Get-ChildItem *.pdf -Recurse
$files

Foreach ($file in $files)
{
# begin with a lower case name
$new_name = $file.Name.ToLower()

# map edition to => ed
$new_name = $new_name -replace 'edition', 'ed'

# handle spacing
$new_name = $new_name -replace ' ', '-'
$new_name = $new_name -replace '_', '-'
do
{
$last_new_name = $new_name
$new_name = $new_name -replace "--", "-"
} while ($last_new_name -ne $new_name)

# remove brackets
$new_name = $new_name -replace "\(", ''
$new_name = $new_name -replace "\)", ''

# remove apostrophes
$new_name = $new_name -replace "'", ''

# remove exclamation marks
$new_name = $new_name -replace "!", ''

# remove commas
$new_name = $new_name -replace ',', ''

$new_path = ($file.DirectoryName + "\" + $new_name)
Write-Host "Renaming:" $file.FullName "to" $new_path
Rename-Item $file.FullName $new_path
}

That took me about 10 minutes with testing and with an eye to writing about it in a blog post. Had I done it by hand? I’d probably still be at it!


Hi! Did you find this useful or interesting? I have an email list coming soon, but in the meantime, if you ready anything you fancy chatting about, I would love to hear from you. You can contact me here or at stephen ‘at’ logicalmoon.com