PowerShell Pipeline to Rename Files

Posted

For me, Microsoft PowerShell is hard! I can’t wrap my mind around data pipelines (|). Instead, I keep reverting to if/then, loops and other traditional programming paradigms, similar to what I’d do in UNIX shell!

I’m a total beginner with PowerShell - no idea if what I present here is “best practice” but it works for me... and perhaps, only me!

Loop vs Pipeline

To illustrate - I want to rename some files if the filename matches a particular regular expression (so Get-ChildItem -Filter is not good enough).

For example (admittedly not a good example), to change “2019” to “2020” in file names a-2019.txt, b_2019.txt, c 2019.txt, etc., my first pass coding this would be:

$from = "2019"
$to = "2020"
Get-ChildItem . | ForEach-Object {
    if ($_.BaseName -match $from) {
        $NewName = $_.BaseName -replace $from, $to
        "Rename $_ to $NewName$($_.Extension)"
        $_ | Rename-Item -NewName { $NewName + $_.Extension }
    }
}

But in PowerShell the loop can be coded a single line:

$from = "2019"
$to = "2020"
Get-ChildItem . | Where-Object { $_.BaseName -match $from } | Rename-Item -NewName { $($_.BaseName -replace $from, $to) +  $_.Extension }

Modifying Pipeline Objects

Ah but now I want to list the files that were renamed and prompt for the user’s confirmation before proceeding to rename the files. Adding Rename-Item -PassThru can be used show the new name, but not the original file name. My initial thinking falls back to using ForEach-Object loops...

$from = "2019"
$to = "2020"
$c = Get-ChildItem . | Where-Object { $_.BaseName -match $from }
$c | ForEach-Object {
    $NewName = $_.BaseName -replace $from, $to
    "Rename $_ to $NewName$($_.Extension)"
}
if ((Read-Host "Rename? [y/n]") -match "[Yy]") {
    $c | ForEach-Object {
        $NewName = $_.BaseName -replace $from, $to
        $_ | Rename-Item -NewName { $NewName + $_.Extension }
    }
    "All done!"
}

But no, I resisted! My solution is to “add” a NewName “column” to the array of PowerShell FileInfo objects returned by Get-ChildItem, and then output the resulting table. No loops, just data pipelines... Weird, huh?

$from = "2019"
$to = "2020"
$c = Get-ChildItem . | Where-Object { $_.BaseName -match $from } | Select-Object *, @{ n = "NewName"; e = { ($_.BaseName -replace $from, $to) + $_.Extension } }
$c | Format-Table DirectoryName, Name, NewName
if ((Read-Host "Rename? [y/n]") -match "[Yy]") {
    $c | Rename-Item -NewName { $_.NewName }
    "All done!"
}

I don’t know which is more optimal, but one is readable the other is kind elegant, no?

Aside 1

If you want to read a single key press instead of having to press y followed by Enter.

"Ready [yn]? "
do { $key = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") } 
until ('Y','N' -contains $key.Character)
if ($key.Character -eq "y") { 
    #do something
}

Code adapted from Jaap Brasser's Blog.

Aside 2

If you encounter Read-Host happening (seemingly) before any previous output, it’s because of the way pipelining has delayed the console. You’ll need to add | Out-String or some formatting like I used $c | Format-Table above instead of just $c | Select-Object.

Aside 3

If you want more readable code, use a back tick ` to break up the pipeline, e.g.

$c = Get-ChildItem . `
    | Where-Object { $_.BaseName -match $from } 

Aside 4

Did you know that PowerShell Core can be installed on macOS and Linux? I didn’t!

In the spirit of overusing pipes - to download and decompress the current macOS version 6.2.3 released 12 September 2019:

curl -L https://github.com/PowerShell/PowerShell/releases/download/v6.2.3/powershell-6.2.3-osx-x64.tar.gz | tar xvf -

That’s it - you can run ./pwsh to start PowerShell. A version of the code in this example above was initially developed on Windows but really tested for this blog on macOS.