How to use a powershell function to return the expected value? - powershell

As we know, PowerShell has wacky return semantics.
Function return value in PowerShell shows there are two main ideas to wrap my head around:
All output is captured, and returned
The return keyword just indicates a logical exit point
Even things like reserving variables in outer scopes cause output, like [boolean]$isEnabled. Another good one is $someCollection.Add("toto") which spits the new collection count. Even Append() function causes output.
For example :
Function MyFunc {
$res1 = new-object System.Text.StringBuilder
$res1.Append("titi");
$res2 = "toto"
return $res2
}
$s = MyFunc
Write-Host $s
The output is : titi toto.
The expected output should be toto.
How to use a powershell function to return the expected value? (at least when viewed from a more traditional programming perspective)

Change
$res1.Append("titi");
to
$res1.Append("titi") | Out-Null
because the function returns every output which otherwise would be visible in the console.

if by using 'toto' you are trying to understand if your function succeeded, you could do
Function MyFunc {
$res1 = new-object System.Text.StringBuilder
$res1.Append("titi") | Out-Null
return $?
}
"$?" returns a boolean if the previous command succeeded (true) or failed (false). so externally it would look like
$s = MyFunc
if ($s) {
Write-Host "successful" -Foregroundcolor Green
}
else {
Write-Error "unsuccessful"
}

When PowerShell was being developed, the team wanted to make it simple to use. But, it was confusing to people who know return from other languages. The implementation in classes is an attempt to rectify that mistake.
The return keyword works very differently in methods in PowerShell classes. It works like the return statements in other languages.
In a class method, the return keyword:
Exits the current scope.
Returns the associated object (return ).
Returns only the associated object.
The object that Return returns must match the return type of the method.
It is consistent with the return keyword and analogous keywords in other languages.
class ClassMyFunc
{
[string] MyFunc
{
$res1 = new-object System.Text.StringBuilder
$res1.Append("titi")
$res2 = "toto"
return $res2
}
}
$cmf = New-Object -TypeName ClassMyFunc
$cmf.MyFunc()
The output is : toto, as expected.
Using classes solved my problem, without having to search all functions returning a value in the console and piping it to Out-Null (as suggested by #TobyU).

Related

Unable to Return [BindingList[RandomType]] from ScriptProperty

I have a complex class that dynamically adds members to itself based on a file loaded via Import-Clixml.
Boiling the class down to the problematic part leaves us with this (Take note of the commented line used to prove success up to that point):
class TestClass {
[hashtable]$_data = #{}
[void]DefineData([object]$data) {
$this._data['Data'] = $data
$this | Add-Member -MemberType ScriptProperty -Name 'ScriptProperty' -Value {
#$this._data['Data'].GetType() | Out-Host
return $this._data['Data']
}
}
}
In the following code, there are 4 statements for assigning a value to $OriginalValue. Leave 3 of these statements commented and uncomment the one you want to try. When executed, the code should result in $ReturnValue containing the same value as $OriginalValue, but in the case of assigning $OriginalValue an instance of [BindingList[RandomType]], $ReturnValue is $null.
$ClassVar = [TestClass]::new()
$OriginalValue = [System.ComponentModel.BindingList[string]]::new()
#$OriginalValue = #{}
#$OriginalValue = [PSCustomObject]#{ Name = 'Value' }
#$OriginalValue = "Test String"
$OriginalValue.GetType()
$ClassVar.DefineData($OriginalValue)
$ReturnValue = $ClassVar.ScriptProperty
$ReturnValue.GetType()
Yes, I can hack my way around the problem by storing instances of [BindingList[RandomType]] in a [hashtable], but could someone explain what is going on, or even better yet, how to fix the code for all data types?
As explained in comments, the problem is not the BindingList but the output from your Script Block being enumerated. Since your BindingList has no elements when you call .DefineData($OriginalValue) then enumerating a list with no elements via .ScriptProperty results in null value:
(& { [System.ComponentModel.BindingList[string]]::new() }).GetType()
# Errors with:
# InvalidOperation: You cannot call a method on a null-valued expression.
A simple workaround is to wrap the output in a single element array before outputting, for this you can use the comma operator ,.
(& { , [System.ComponentModel.BindingList[string]]::new() }).GetType()
# Output type is preserved and the wrapping array is lost due to enumeration
So, your class method could look as follows considering the hashtable property is not needed:
class TestClass {
[void] DefineData([object] $data) {
$this.PSObject.Properties.Add(
[psscriptproperty]::new(
'ScriptProperty',
{ , $data }.GetNewClosure()
)
)
}
}

PowerShell Pipeline Pollution

Is this question about "[System.Collections.Generic.List[string]] as return value" I got an interesting answer from #Ash about using $PSCmdlet.WriteObject() instead of Return to avoid pipeline pollution. And it seemed to work in that context. However, in trying to explain this to someone, I made this little example, using an ArrayList instead, since I know that pollutes the pipeline every time. And here it does NOT work. Without the ArrayList lines, both return a type of [System.String]. With those lines they BOTH return [System.Object]. And if I prefix the ArrayList line with [Void] to redirect the pollution, it's back to [System.String].
I have verified that in both failure cases the .Count of the returned object is 2. Which got me thinking, is this only a solution for the unroll problem? So I revised the test to use a System.Collections.Generic.List[string] for the return, and I find that without the ArrayList to pollute, WriteObject works, while return only works with the unroll fix, i.e. return ,$return. Add the pollution back in, and they both get polluted.
So, was my understanding wrong, and this solution is only applicable to the unroll problem? And if so, is there anything specific to recommend one approach over the other?
And, IS THERE a real solution to the pipeline pollution problem with functions? Or is this just how functions work, and the answer is to use classes where you have complete control over return types, and PowerShell doesn't pour it's effluent into your variables willy nilly?
function ReturnObject () {
[String]$return = 'Return a string'
(New-Object System.Collections.ArrayList).Add('Pollution')
return $return
}
function WriteObject {
[CmdletBinding()]
param ()
[String]$writeObject = 'Writeobject a string'
(New-Object System.Collections.ArrayList).Add('Pollution')
$PSCmdlet.WriteObject($writeObject)
}
CLS
$Returned = ReturnObject
Write-Host "$Returned ($($Returned.GetType().FullName))"
$Written = WriteObject
Write-Host "$Written ($($Written.GetType().FullName))"
And the Generic.List variant
function ReturnObject () {
$return = New-Object System.Collections.Generic.List[string]
$return.Add('Return a List')
(New-Object System.Collections.ArrayList).Add('Pollution')
return ,$return
}
function WriteObject {
[CmdletBinding()]
param ()
$writeObject = New-Object System.Collections.Generic.List[string]
$writeObject.Add('Writeobject a List')
(New-Object System.Collections.ArrayList).Add('Pollution')
$PSCmdlet.WriteObject($writeObject)
}
CLS
$Returned = ReturnObject
Write-Host "($($Returned.GetType().FullName)) $($Returned.Count)"
$Written = WriteObject
Write-Host "($($Written.GetType().FullName)) $($Returned.Count)"

Specify function return type

How do I specify the return type in a powershell function?
function getSomeString {
return "hello world"
}
$xyz = getSomeString() # <--- IDE does not recognize $xyz as a string
Before getting to the answer, I should point out that PowerShell commands do NOT have static return types!
In terms of I/O, commands in PowerShell are like little black boxes - you send 0 or more input objects in one end, and 0 or more outputs object are emitted at the other end - the type(s) of which may vary.
That being said, you can add a type inference hint to your function by adding an [OutputType] attribute decorator to the function's param block:
function Get-SomeString {
[OutputType([string])]
param()
return "hello world"
}
$xyz = Get-SomeString # Tools can now infer type of `$xyz`
Again, this is a hint, not a guarantee - you can lie if you want to:
function Get-Integers
{
[OutputType([int[]])]
param()
if((1..10 |Get-Random) -gt 7){
return "lol, I'll do what I want"
}
return 1,2,3
}
$f = Get-Integers
In this case, Get-Integers will return a [string] 30% of the time, but the IDE will always act like both $f and Get-Integers resolves to an array of [int]'s - because that's what we told it to expect.

Why weird assignment from variable inside Powershell switch statement?

I'm a beginner at Powershell and am struggling to understand some syntax from some code I found on Github. I've read the docs on Powershell assignment, and on switch statements, and can't understand what is going on with the = $Yes and = $No in this code snippet:
Switch ($Prompt3) {
Yes {
Stop-EdgePDF
Write-Output "Edge will no longer take over as the default PDF viewer."; = $Yes
}
No {
= $No
}
}
I haven't been able to find any references to this kind of syntax, and it doesn't seem to do anything in the script. So why is it there?
UPDATE: This issue has been resolved.
Looks to me like the variable name that was getting the assignment was deleted in a change back in August.
$PublishSettings = $Yes
Was changed to:
= $Yes
And:
$PublishSettings = $No
Was changed to:
= $No
Looks like poor search and replace.
I've created an issue for the problem at GitHub.
There are many characters that are valid in a function (or variable) name; this includes the = symbol. What you're observing is a function or alias.
Examples:
# standard function
function =
{
return $args
}
# accessing the function: drive
${Function:=} = {
return $args
}
# defining a new alias
New-Alias -Name = -Value Get-Variable
# using the Alias attribute
function Test-Thing
{
[Alias('=')]
param()
return $args
}

Explicit Return in Powershell

I can write the following code in javascript:
function sum(num1, num2) {
return num1 + num2;
}
and then get a value
var someNum = sum(2,5);
I would like to do the same thing in Powershell, but I read the following guide:
PowerShell also knows the return keyword; however, it follows a
different logic. In general, the purpose of return is to end the
execution of a code section and to give the control back to the parent
block.
If you add a parameter to the return statement, the value will indeed
be returned to the calling subroutine. However, this also applies for
all other statements with an output. This means that any output
produced in the function will be stored in the variable together with
the return parameter.
I want to do this for the sake of having pure functions. However, it seems doing
var someNum = sum(2,5);
is entirely redundant, when I can just call the function above, define someNum inside of it, and it will be available in the global scope.
Am I missing something or is it possible to write pure functions in Powershell that don't return everything inside the function?
A bit tangential, but here is my actual code:
function GetPreviousKeyMD5Hashes() {
$query = "SELECT Name, MD5, executed FROM [AMagicDb].[dbo].cr_Scripts";
$command = New-Object System.Data.SQLClient.SQLCommand;
$command.Connection = $connection;
$command.CommandText = $query;
try {
$reader = $command.ExecuteReader();
while ($reader.Read()) {
$key = $reader.GetString(1)
$previousScripts.Add($key) | Out-Null
}
$reader.Close();
Write-Output "$(Get-Date) Finished querying previous scripts"
}
catch {
$exceptionMessage = $_.Exception.Message;
Write-Output "$(Get-Date) Error running SQL at with exception $exceptionMessage"
}
}
and then:
$previousScripts = New-Object Collections.Generic.HashSet[string];
GetPreviousKeyMD5Hashes;
This code isn't clear to me at all - running GetPreviousKeyMD5Hashes does set $previousScripts, but this is entirely unclear to whoever modifies this after me. My only other alternative (afaik) is to have all this in line, which also isn't readable.
is entirely redundant, when I can just call the function above, define someNum inside of it, and it will be available in the global scope.
No: functions execute in a child scope (unless you dot-source them with .), so variables created or assigned to inside a function are local to it.
Am I missing something or is it possible to write pure functions in Powershell that don't return everything inside the function?
Yes: The implicit output behavior only applies to statements whose output is neither captured - $var = ... - nor redirected - ... > foo.txt
If there are statements that happen to produce output that you'd like to discard, use $null = ... or ... > $null
Note: ... | Out-Null works in principle too, but will generally perform worse, especially in earlier PowerShell versions - thanks, TheIncorrigible1.
If there are status messages that you'd like to write without their becoming part of the output, use Write-Host or, preferably Write-Verbose or, in PSv5+, Write-Information, though note that the latter two require opt-in for their output to be visible in the console.
Do NOT use Write-Output to write status messages, as it writes to the success output stream, whose purpose is to output data ("return values").
See this answer of mine for more information about PowerShell's output streams.
The equivalent of your JavaScript code is therefore:
function sum($num1, $num2) {
Write-Host "Adding $num1 and $num2..." # print status message to host (console)
$num1 + $num2 # perform the addition and implicitly output result
}
PS> $someNum = sum 1 2 # NOTE: arguments are whitespace-separated, without (...)
Adding 1 and 2... # Write-Host output was passed through to console
PS> $someNum # $someNum captured the success output stream of sum()
3
Am I missing something or is it possible to write pure functions in Powershell that don't return everything inside the function?
You can't have your cake and eat it too...
If you have no out put in your function, then it is "pure" like you desire. If you have output, that also becomes part of the return.
You can use [ref] params. See below for example.
function DoStuff([ref]$refObj)
{
Write-Output "DoStuff: Enter"
$refObj.Value += $(1 + 2)
$refObj.Value += "more strings"
Write-Output "DoStuff: Exit"
}
$refRet = #()
$allRet = DoStuff([ref]$refRet)
"allRet"
$allRet
"refRet"
$refRet
"`n`nagain"
$allRet = DoStuff([ref]$refRet)
"allRet"
$allRet
"refRet"
$refRet
Note: Powershell doesn't need semicolons at the end of each statement; only for separating multiple statements on the same line.
Whenever possible, it's a good idea to avoid changing global state within a function. Pass input as parameters, and return the output, so you aren't tied to using the function in only one way. Your sample could look like this:
function sum
{
param($num1,$num2)
return $num1+$num2
}
$somenum=sum 2 5
Now, with Powershell, the return statement isn't needed. The result of every statement that isn't otherwise assigned, captured, redirected, or otherwise used, is just thrown in with the return value. So we could replace the return statement above with simply
$num1+$num2
You're already making use of this in your code with:
$previousScripts.Add($key) | Out-Null
where you are discarding the result of .Add(). Otherwise it would be included in the return value.
Personally, I find using return to explicitly mark the return value makes it easier to read. Powershell's way of putting all if the output in the return caused a lot of trouble for me as I was learning.
So, the only fixes to your code I would make are:
Move $previousScripts = New-Object Collections.Generic.HashSet[string] to inside the function, making it local.
Add return $previousScripts to the end of the function.