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.