How-to & Workflow

PowerShell Batch Rename Files (and Where Scripts Hit the Content Wall)

You've got a folder of PDFs named scan001.pdf through scan400.pdf, and you want them renamed in bulk. You know PowerShell. You've probably already run Rename-Item a hundred times, so you open a session, pipe Get-ChildItem into a rename, and thirty seconds later your job's done. That's the easy version. The hard version, the one that brought you here, is when the new name has to come from inside the file: the invoice number, the client name, the date printed on the page. That's the with content part, and it's where most guides go quiet. PowerShell is excellent at renaming files based on patterns in the existing name or metadata, and it hits a hard wall the moment the name has to come from a scanned page. This walks through both.

Batch Rename by Pattern With Rename-Item

The workhorse for renaming in PowerShell is Get-ChildItem piped into Rename-Item. Get-ChildItem lists the files, the pipeline hands each one to Rename-Item, and the -NewName parameter decides what the new name should be. The trick that makes it powerful is that -NewName accepts a script block, so you can compute a name per file rather than pass a single fixed string. Inside that block, $_ is the current file, which gives you $_.Name, $_.BaseName, $_.Extension, and the full path to work with.

The simplest job is adding a prefix. Something like Get-ChildItem *.pdf | Rename-Item -NewName { "Invoice_" + $_.Name } walks every PDF in the folder and prepends Invoice_ to each existing name. The block runs once per file, so every file keeps its own tail and just gains the prefix.

Zero-padded sequential numbering needs a counter you increment yourself. Declare $i = 1 first, then run Get-ChildItem *.pdf | Rename-Item -NewName { $name = 'Invoice_{0:D3}{1}' -f $script:i, $_.Extension; $script:i++; $name }. The {0:D3} format specifier pads the number to three digits, so you get Invoice_001.pdf, Invoice_002.pdf, and so on, and the $script:i++ bumps the counter after each file. Sorting the Get-ChildItem output first (pipe it through Sort-Object Name) keeps the numbering predictable.

Reformatting text that's already in the name is a job for -replace, which takes a regular expression. To turn a date written as 20250314 into 2025-03-14 inside each filename you can run Get-ChildItem *.pdf | Rename-Item -NewName { $_.Name -replace '(\d{4})(\d{2})(\d{2})', '$1-$2-$3' }. The capture groups grab the year, month, and day, and the replacement string stitches them back with hyphens. Because -replace is regex, you can reorder, strip, or normalize almost any pattern that already exists in the old name.

Changing extensions is the same shape: Get-ChildItem *.jpeg | Rename-Item -NewName { $_.Name -replace '\.jpeg$', '.jpg' } rewrites the tail on every match. And you never have to fly blind. Add -WhatIf to any Rename-Item and PowerShell prints exactly what it would do without touching a single file, which is the single most important habit when you're about to rename hundreds of files at once. Add -Verbose to see each rename as it happens on the real run.

Two more flags matter for bulk work. -Recurse on Get-ChildItem walks subfolders so you can rename an entire tree in one pass, and -Filter (or a trailing Where-Object clause) narrows the set before anything gets renamed. For example Get-ChildItem -Recurse -Filter *.pdf | Where-Object { $_.Length -gt 0 } skips zero-byte files across every subfolder. Between the script block, -replace, a counter, and -WhatIf, PowerShell handles essentially any rename where the new name can be built from the old name, the extension, or file metadata.

CSV-Driven Rename (One-to-One Mapping)

Pattern rename gives every file a name derived from the same rule. But sometimes each file needs its own distinct name that no pattern can generate, and you already know what those names should be. That's the case for a CSV-driven rename, where you supply the mapping yourself and PowerShell just executes it.

Build a two-column CSV with a header row of OldName,NewName, then one row per file: scan001.pdf,Acme_Invoice_2025-03-14.pdf on the next line, scan002.pdf,Globex_Invoice_2025-03-15.pdf below that, and so on. Save it as rename-map.csv in the folder. Then drive it with Import-Csv piped into a ForEach-Object that calls Rename-Item once per row: Import-Csv rename-map.csv | ForEach-Object { Rename-Item -Path $_.OldName -NewName $_.NewName -WhatIf }. Import-Csv turns each row into an object whose properties are the column headers, so $_.OldName and $_.NewName pull the two values straight out.

Keep the -WhatIf on the first pass to preview the whole mapping, confirm the pairs line up, then remove it to commit. PowerShell is honest about the edge cases here rather than silent. If a row's OldName points at a file that isn't in the folder, Rename-Item raises an error for that row and moves on to the next, so one bad line doesn't halt the batch (add -ErrorAction SilentlyContinue if you want the errors suppressed). And if a NewName already exists on disk, Rename-Item refuses the operation and reports it rather than overwriting the existing file, which protects you from silently clobbering data.

This is how PowerShell gives every file a different, meaningful name without ever reading a document: you supply the names, the script applies them at scale. The catch is exactly that phrase. You have to build the mapping, which means you already had to know that scan001.pdf is the Acme invoice. When you don't, because the only place that fact lives is inside the file, the CSV approach can't help you author the mapping. Which is the wall.

Where PowerShell Hits the Content Wall

Everything so far reads two things: the filename and file metadata. Rename-Item never opens the document. You can push one step further with a module. Load a .NET PDF library into your session (a PdfPig or iTextSharp style assembly) or shell out to an external tool like pdftotext, and PowerShell can pull the text LAYER out of a born-digital PDF. Once you have the page text as a string, you're back on familiar ground: run a regex over it to grab the invoice number or the date, and feed that into Rename-Item -NewName. For born-digital PDFs, that genuinely works, and it's a reasonable amount of code.

The wall is scans. A scanned PDF is not text wearing a PDF wrapper, it's an image of a page. There is no text layer to extract, so pdftotext or a PDF library returns an empty string no matter how clean the scan looks to your eye. The invoice number is right there on screen, and every extraction call comes back with nothing, because to the parser the page is a picture.

The only way through is optical character recognition. To read a scan, your script has to call an OCR engine, either a local one like Tesseract or a cloud OCR API, hand it the page image, get text back, and only then run your regex. That's a legitimate technique, but notice what just happened to the scope. You're no longer writing a rename one-liner. You're building and maintaining an OCR pipeline: installing and configuring the engine, converting PDF pages to images at the right resolution, cleaning up OCR noise, and writing per-vendor regex because every invoice layout puts the number somewhere different. It works, but it is real, ongoing engineering, and it comes with none of the safety rails you'd want. There is no preview and no undo unless you build them yourself, so a regex that grabs the wrong field renames the whole batch wrong in a single pass. The deeper scanned-document workflow, and why scans are their own category of problem, lives on the hub page on how to rename scanned PDF files.

PowerShell Rename: What Works and Where It Stops

TaskPowerShell approachWorks on scanned PDFs?
Pattern, prefix, or sequential number renameGet-ChildItem piped to Rename-Item -NewName (script block plus counter)No content needed
Regex reformat of text already in the nameRename-Item -NewName with a -replace regexN/A (works on the filename, not the file)
One-to-one mapping, a distinct name per fileImport-Csv piped to Rename-Item, you supply the namesYes, but only because you supply the names
Content-based name from a born-digital PDFText-extraction module or pdftotext, then regex into Rename-ItemYes for born-digital only (there is a text layer)
Content-based name from a scanned PDFMust call an OCR engine (Tesseract or a cloud OCR API) from the scriptOnly if you wire up OCR yourself

When a Script Stops Being Worth It

PowerShell scripts have a real, honest advantage: they're free, they run unattended, and they're perfect for a consistent pile of files. If your folder is four hundred born-digital PDFs with the invoice number in a predictable spot, a script that extracts the text and renames on a schedule is close to ideal. Write it once, run it forever, pay nothing.

The math changes when the pile is mixed. Scans from three different vendors, a few phone photos of receipts, PDFs where the layout moves every quarter, and now your one-liner is an OCR project with a maintenance burden. You're tuning image resolution, cleaning OCR output, and rewriting regex every time a vendor changes their template, and you still have no preview and no undo. Every hour spent maintaining that pipeline is an hour the rename was supposed to save you.

At that point a content-aware tool that already bundles OCR and naming (like renamer.ai, with a preview step so you confirm before anything is renamed) is often less total work than keeping a script alive. It's not that scripting is wrong, it's that the break-even moves once content extraction and scans enter the picture. The deeper scanned-document workflow lives on the hub page on how to rename scanned PDF files, which is worth reading before you commit to building the pipeline yourself.

The Short Version

Pattern rename and CSV rename in PowerShell are excellent and free. Get-ChildItem piped to Rename-Item with a script block, a counter, and -replace handles prefixes, numbering, and reformatting, and -WhatIf lets you preview it safely. A two-column CSV driven through Import-Csv gives every file its own distinct name whenever you already know the names.

Content-based rename is where it narrows. It works for born-digital PDFs if you add a text-extraction module and regex the value you need. Scanned PDFs have no text layer, so they need OCR wired into the script, and that turns a one-liner into a maintained pipeline with no preview or undo. If your folder is mostly scans, that's the moment to weigh a tool built for it against the cost of maintaining your own. The hub page on how to rename scanned PDF files covers that side in full.

Frequently Asked Questions

How do I batch rename files in PowerShell?

Pipe Get-ChildItem into Rename-Item and use a script block for -NewName so you can compute a name per file. For example, Get-ChildItem *.pdf | Rename-Item -NewName { 'Invoice_' + $_.Name } prefixes every PDF. Use a counter with a {0:D3} format specifier for zero-padded sequential numbers, and -replace for regex reformatting. Always run it with -WhatIf first to preview the result before committing.

How do I rename files from a CSV list in PowerShell?

Build a two-column CSV with a header row of OldName,NewName and one row per file, then run Import-Csv map.csv | ForEach-Object { Rename-Item -Path $_.OldName -NewName $_.NewName }. Import-Csv turns each row into an object whose properties match the column headers. Add -WhatIf to preview the whole mapping first. If an OldName is missing PowerShell errors and skips that row, and if a NewName already exists it refuses the rename rather than overwriting the file.

Can PowerShell rename files based on their content?

For born-digital PDFs, yes. Load a .NET PDF library or call an external tool like pdftotext to pull the text layer out of the document, run a regex over that text to grab the value you want (an invoice number, a date), and feed it into Rename-Item -NewName. This works because a born-digital PDF actually contains selectable text. It's more code than a pattern rename, but it's well within PowerShell's reach.

Can PowerShell rename scanned PDFs by content?

Not on its own. A scanned PDF is an image with no text layer, so extraction returns nothing and there's no content for a regex to read. To name a scan by its content, your script has to call an OCR engine such as Tesseract or a cloud OCR API, convert pages to images, clean up the OCR output, and write per-layout regex. It's doable, but at that point you're building and maintaining an OCR pipeline with no preview or undo, not writing a rename one-liner.