PowerShell Functions and Return Types
- Posted in:
- Development
- PowerShell
As a result of some struggles trying to automate a process, I've learned some things about PowerShell. After getting to the bottom of a time-consuming problem, I thought it worth a blog post that might save someone else some time and heartache.
Let's begin with this simple function named Get-RandomDate. It generates and returns a random date that is between today and X days ago. It has an input parameter $DaysAgo, which is of type [System.Int32]--it is a mandatory parameter.
function Get-RandomDate()
{
Param (
[parameter(Mandatory=$true)]
[System.Int32]
$DaysAgo
)
[System.DateTime]$ret = [System.DateTime]::Today
$randomDays = Get-Random -Minimum 0 -Maximum $DaysAgo
$ret = [System.DateTime]::Today.AddDays($randomDays * -1)
return $ret
}
$randomDate1 = Get-RandomDate(365)
Write-Host $randomDate1.ToString() -ForegroundColor Yellow
Here is our output when we run the script:
PS C:\My Scripts\PowerShell\SQL Server> C:\My Scripts\PowerShell\Function Test.01.ps1
12/8/2020 12:00:00 AM
The $RandomDate variable that is assigned the return value of the function is not typed. But we can make it type-safe, if desired. This produces the same result as above:
[System.DateTime]$randomDate1 = Get-RandomDate(365)
Now we'll make things go sideways. Add a line for the "Hello world!" string and another for the integer 42, which despite what you've heard, is not an answer for anything in this exercise:
function Get-RandomDate()
{
Param (
[parameter(Mandatory=$true)]
[System.Int32]
$DaysAgo
)
[System.DateTime]$ret = [System.DateTime]::Today
$randomDays = Get-Random -Minimum 0 -Maximum $DaysAgo
$ret = [System.DateTime]::Today.AddDays($randomDays * -1)
"Hello world!"
42
return $ret
}
$randomDate3 = Get-RandomDate(365)
Write-Host $randomDate3.ToString() -ForegroundColor Yellow
PS C:\My Scripts\PowerShell\SQL Server> C:\My Scripts\PowerShell\Function Test.03.ps1
System.Object[]
What happened? When we tried to write out the value of our date variable, we got System.Object[]. Is our function no longer returning a System.DateTime type? Maybe we need to specify the output type of the function? Let's try that...
function Get-RandomDate()
{
[OutputType([System.DateTime])]
Param (
[parameter(Mandatory=$true)]
[System.Int32]
$DaysAgo
)
[System.DateTime]$ret = [System.DateTime]::Today
$randomDays = Get-Random -Minimum 0 -Maximum $DaysAgo
$ret = [System.DateTime]::Today.AddDays($randomDays * -1)
"Hello world!"
42
return $ret
}
$randomDate3 = Get-RandomDate(365)
Write-Host $randomDate3.ToString() -ForegroundColor Yellow
PS C:\My Scripts\PowerShell\SQL Server> C:\My Scripts\PowerShell\Function Test.03.ps1
System.Object[]
Well that didn't work. What if we make the variable type safe? Does that get us back on track?
function Get-RandomDate()
{
[OutputType([System.DateTime])]
Param (
[parameter(Mandatory=$true)]
[System.Int32]
$DaysAgo
)
[System.DateTime]$ret = [System.DateTime]::Today
$randomDays = Get-Random -Minimum 0 -Maximum $DaysAgo
$ret = [System.DateTime]::Today.AddDays($randomDays * -1)
"Hello world!"
42
return $ret
}
[System.DateTime]$randomDate4 = Get-RandomDate(365)
PS C:\My Scripts\PowerShell\SQL Server> C:\My Scripts\PowerShell\Function Test.04.ps1
Cannot convert the "System.Object[]" value of type "System.Object[]" to type "System.DateTime".
At C:\My Scripts\PowerShell\Function Test.04.ps1:19 char:1
+ [System.DateTime]$randomDate4 = Get-RandomDate(365)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : MetadataError: (:) [], ArgumentTransformationMetadataException
+ FullyQualifiedErrorId : RuntimeException
Nope, that didn't work either. Let's go back to the example that seemingly produced an output of System.Object[]. Here it is again, for reference (with two extra lines of code):
function Get-RandomDate()
{
[OutputType([System.DateTime])]
Param (
[parameter(Mandatory=$true)]
[System.Int32]
$DaysAgo
)
[System.DateTime]$ret = [System.DateTime]::Today
$randomDays = Get-Random -Minimum 0 -Maximum $DaysAgo
$ret = [System.DateTime]::Today.AddDays($randomDays * -1)
"Hello world!"
42
return $ret
}
$randomDate3 = Get-RandomDate(365)
Write-Host $randomDate3.ToString() -ForegroundColor Yellow
$randomDate3.GetType()
$randomDate3.Count
PS C:\My Scripts\PowerShell\SQL Server> C:\My Scripts\PowerShell\Function Test.ps1
System.Object[]
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
3
$randomDate3.GetType() tells us $randomDate3 is indeed an array of objects. $randomDate3.Count also tells us there are three items in the array. Can you guess what's in there? Let's find out with this bit of code:
function Get-RandomDate()
{
[OutputType([System.DateTime])]
Param (
[parameter(Mandatory=$true)]
[System.Int32]
$DaysAgo
)
[System.DateTime]$ret = [System.DateTime]::Today
$randomDays = Get-Random -Minimum 0 -Maximum $DaysAgo
$ret = [System.DateTime]::Today.AddDays($randomDays * -1)
"Hello world!"
42
return $ret
}
$randomDate5 = Get-RandomDate(365)
Write-Host (“return value index 0: Type = {0}, Value = {1}” –f $randomDate5[0].GetType(), $randomDate5[0]) -ForegroundColor Yellow
Write-Host (“return value index 1: Type = {0}, Value = {1}” –f $randomDate5[1].GetType(), $randomDate5[1]) -ForegroundColor Yellow
Write-Host (“return value index 2: Type = {0}, Value = {1}” –f $randomDate5[2].GetType(), $randomDate5[2]) -ForegroundColor Yellow
PS C:\My Scripts\PowerShell\SQL Server> C:\My Scripts\PowerShell\Function Test.ps1
return value index 0: Type = System.String, Value = Hello world!
return value index 1: Type = System.Int32, Value = 42
return value index 2: Type = System.DateTime, Value = 11/4/2021 12:00:00 AM
Well lo and behold, the first two items in the array are the "Hello world!" string and integer value 42 respectively. And the last item is the DateTime "return" value. Everything from the output stream of the function gets piled onto an array of objects and sent back to whatever called it.
Hello world?
Now the obvious question: why would you add "Hello world!" (or 42) as a line of code? Well, you wouldn't. At least I wouldn't. But it demonstrates the problem nicely: any output not captured needs to be accounted for.
It's not just senseless string literals or vagabound integers you need to worry about. Consider some .NET Framework methods that have a return type, but are called as if the return type was void. System.Text.StringBuilder.Append() and System.Data.SqlClient.SqlParameter.AddWithValue() are a couple of good examples. Let's take a look at the StringBuilder:
function Get-RandomDate()
{
[OutputType([System.DateTime])]
Param (
[parameter(Mandatory=$true)]
[System.Int32]
$DaysAgo
)
[System.DateTime]$ret = [System.DateTime]::Today
$randomDays = Get-Random -Minimum 0 -Maximum $DaysAgo
$ret = [System.DateTime]::Today.AddDays($randomDays * -1)
[System.Text.StringBuilder] $sb = New-Object System.Text.Stringbuilder
$sb.Append("Hello ")
$sb.Append("world!")
return $ret
}
$randomDate5 = Get-RandomDate(365)
Write-Host $randomDate5.ToString() -ForegroundColor Yellow
Write-Host (“return value index 0: Type = {0}, Value = {1}” –f $randomDate5[0].GetType(), $randomDate5[0]) -ForegroundColor Yellow
Write-Host (“return value index 1: Type = {0}, Value = {1}” –f $randomDate5[1].GetType(), $randomDate5[1]) -ForegroundColor Yellow
Write-Host (“return value index 2: Type = {0}, Value = {1}” –f $randomDate5[2].GetType(), $randomDate5[2]) -ForegroundColor Yellow
PS C:\My Scripts\PowerShell\SQL Server> C:\My Scripts\PowerShell\Function Test.ps1
System.Object[]
return value index 0: Type = System.Text.StringBuilder, Value = Hello world!
return value index 1: Type = System.Text.StringBuilder, Value = Hello world!
return value index 2: Type = System.DateTime, Value = 8/8/2021 12:00:00 AM
Our much more realistic scenario still has the same problem as the contrived examples. How do we address this issue? One option is to cast the expression as [void]:
[System.Text.StringBuilder] $sb = New-Object System.Text.Stringbuilder
[void] $sb.Append("Hello ")
[void] $sb.Append("world!")
This approach is not only counterintuitive to me, it feels profoundly wrong. It's a bad attempt at a jedi mind trick.
.NET Framework: Oh, I see you're using the Append() method with a StringBuilder. Nice choice! Its return value is a StringBuilder.
PowerShell: No, it isn't.
.NET Framework: Maybe it isn't. I'm gonna beat up the fool that told me those lies!
Another option is to hide the output via Out-Null, instead of sending it down the pipeline or displaying it:
[System.Text.StringBuilder] $sb = New-Object System.Text.Stringbuilder
$sb.Append("Hello ") | Out-Null
$sb.Append("world!") | Out-Null
This is considerably more intuitive and palatable. But you'd need to know the lines where it's necessary. I don't see any indication from the PowerShell ISE or Visual Studio Code where output isn't being captured. What are we supposed to do? Append | Out-Null to every line of code in the function?
As you can probably tell, I find all of this frustrating. Defining a function's type is pointless if functions aren't type-safe. PowerShell's exceptions are red herrings. And "return" doesn't mean what I think it should mean.
At least I've gained a better understanding, which may make my future PowerShell development less unproductive. Hopefully some of this helps you too.
Dave | Out-Null
Comments
Thanks for the explanation. This was helpful for a similar situation I'm going through currently.
Richard MartinezThank you! I was debugging my script with VSCODE and could not understand why I kept getting return as Object[] even though I was trying to Type cast it. I just need to sprinkle "| Out-Null" throughout my script now.
ArpitThanks man ! this helped
BenlyThis is definitely top 3 of the most idiotic things the Powershell devs did. c# doesn't try and override your return values, whoever designed this should be off of the Powershell project.
BradHere is a work around stackoverflow.com/.../function-return-value-in-powershell& Get-RandomDate() { [OutputType([System.DateTime])] Param ( [parameter(Mandatory=$true)] [System.Int32] $DaysAgo ) { [System.DateTime]$ret = [System.DateTime]::Today $randomDays = Get-Random -Minimum 0 -Maximum $DaysAgo $ret = [System.DateTime]::Today.AddDays($randomDays * -1) [System.Text.StringBuilder] $sb = New-Object System.Text.Stringbuilder $sb.Append("Hello ") $sb.Append("world!") } | Out-Null; return $ret }
BradThanks, also ran into that - what a gotcha.
ErwinThanks so much for this... it confirmed what I was seeing in a script today. I thought I was going insane.
GregYou're a genius. I thought I was going mad till I found this post. Thanks a bunch.
Alejandro MihanovichThis was some next level stange sh**. Why does PowerShell torture us like that? I don't think pyton ever did this to me.
NielsThis was really useful. I couldn't understand why I got an array in return. The Out-Null option did the trick. Thanks for taking the time to post.
Papageno