[PowerShell] Name Parameters When Using ForEach-Object…or else!

…or else you script may not work properly. That’s all, I’m not that scary.

Words: 853
Time to read: ~ 4.5 minutes

ForEach-Object

I have a quick function just to check the total size of full, differential, and log backups in a folder. It’s handy because we group the database backups into a folder per database and we label all backups files based on what type of backup we’re taking e.g. full = “.bak”, diff = “.diff”, and log = “.trn”.

If you’re interested, the function code is here…


Function Get-BackupFilesSizes {
$Bak = $Diff = $Trn = 0;
$Bak = (Get-ChildItem File |
Where-Object { $_.Extension -eq '.bak' } |
Measure-Object Property Length Sum).Sum;
$Diff = (Get-ChildItem File |
Where-Object { $_.Extension -eq '.diff' } |
Measure-Object Property Length Sum).Sum;
$Trn = (Get-ChildItem File |
Where-Object { $_.Extension -eq '.trn' } |
Measure-Object Property Length Sum).Sum;
[PSCustomObject]@{
'Bak Files (GB)' = $Bak / 1GB
'Diff Files (GB)' = $Diff / 1GB
'Trn Files (GB)' = $Trn / 1GB
};
};

-Begin

Now this is not an advanced function.

There’s no mention of [cmdletbinding()] and no mention of param() in there.

While it uses Get-ChildItem, there’s no mention of the -Path parameter so it only works on the current directory you’re on.

All that being said, it does exactly why I ask for when I run it in the correct location.

WorksWithOne
I named it blog because I stripped out the database name in this version…

However, on this server, there are 20 different database folders and I want a list of all of them. So how do I do that?

Again, I don’t have a $path parameter (I swear I’ll add it in eventually) so I have to be physically in the directory before I can run my function.

Simple enough, we just add a Get-ChildItem, do a ForEach-Object, and then run my function.

However, with quick scripts, I don’t come from a Linux background so I don’t normally use ls or a Windows command line background so I don’t normally use dir either. I come from a Windows GUI background so the closest I get to shortcuts are not naming the parameters.


Get-ChildItem |
ForEach-Object {Set-Location $_.FullName; Get-BackupFilesSize };

AllOfthemWrong
Yes, I know it’s still wrong. SPOILER ALERT: I’m not going to change it.

What’s this? Ah damn, after everything is done we are still in one of the database directories instead of the parent folder. I know it’s a small thing but it annoyed me so I aimed to fix the above line to account for it.

It is here that we meet our foe!

-Process

I’ve read enough about PowerShell to know that ForEach-Object has an -End parameter that gets run after everything in the -Process parameter has run. Therefore I can just use that to go back to the parent directory after I’m done running my function on all the database folders!

ForEachEnd
Not required? We’ll see about that!

Specifies a script block that runs after this cmdlet processes all input objects

Now I happen to know that logically -End is at the end so it should come after the -Process scriptblock. So I modify my ad-hoc script above and tack on the -End parameter’s scriptblock to go back to the parent drectory.


Get-ChildItem |
ForEach-Object {Set-Location $_.FullName; Get-BackupFilesSize } {Set-Location ..}

ForeachErrors
“I:\” am wrong somewhere…

Eh…what?

Cannot process argument because the value of argument “path” is null.

The “path” argument is NULL? And I’m back to the very root of “I:\”?

Thankfully, at this stage, I know to go back and read the help file again before I let slip any curses.

ForEachEnd
Positional = No.

Hmm…the only thing I can see here is that it’s not a positional parameter. I know -Process is though, right? I mean if it isn’t then nearly every script I’ve seen should fail…

ForEachProcess
Yeah, the -Process scriptblock defaults to 0…

There’s also a -Begin parameter. This scriptblock runs only once before the -Process scriptblock. It, like the -End scriptblock, isn’t required either.

Specifies a script block that runs before this cmdlet processes any input objects.

ForEachBegin
Can I -Confirm that it’s not required?

But again, that position is named so that shouldn’t cause any problems, I assume

Luckily, I’m currently reading “Windows PowerShell In Action” by Bruce Payette ( twitter ) and this starts to ring a bell. So I go back and check out the section in the book on ForEach-Object and a little gem starts to shine out:

If -Begin is empty and -Process has more than two scriptblocks in the collection, then the first one is treated as the -Begin scriptblock and the second one is treated as the -Process scriptblock

Ah so it defaults that way!

That explains why I got the error message only once, and why I got all the way back to the “I:\” drive!
My first scriptblock, the one with my function ran once at the start, and then my second scriptblock, the one saying go back up a folder, ran around 20 times!

So if that’s the case, all we need to do is specify that the -End scriptblock is actually the -End scriptblock!


Get-ChildItem |
ForEach-Object { Set-Location $_.FullName; Get-BackupFilesSize } -End { Set-Location .. };

FinallyWorks
Foreach-Object … finally

-End

I don’t know if any other PowerShell cmdlet has this little quirk, especially since I haven’t finished the book yet. It would be interesting to see what does though.

I do know that it would have taken one metric tonne of testing to figure this little peculiarity out for myself. Thankfully, there are loads of resources out there to help with learning, and I have all the time in the world to keep learning.

Nice to know it’s already starting to pay off though 🙂

Author: Shane O'Neill

DBA, T-SQL and PowerShell admirer, Food, Coffee, Whiskey (not necessarily in that order)...

One thought on “[PowerShell] Name Parameters When Using ForEach-Object…or else!”

Leave a Reply

%d bloggers like this: