How to define named parameter as [ref] in PowerShell - powershell

I'm trying to use [ref] named parameters. However, I am getting an error:
workflow Test
{
Param([Parameter(Mandatory=$true)][String][ref]$someString)
write-verbose $someString -Verbose
$someString = "this is the new string"
}
cls
$someString = "hi"
Test -someString [ref]$someString
write-host $someString
#Error: Cannot process argument transformation on parameter 'someString'. Reference type is expected in argument.
How can I fix this problem?

I noticed that you are using a "workflow" in your example of a [ref] parameter.
For simplicity, let's call it a "function" and get back to "workflow" later.
There are three things you need to change in your code:
When passing a [ref] parameter to function, you need to enclose the parameter in parenthesis ().
When using a [ref] parameter within a function refer to $variable.value
Remove [string] type from your parameter definition. It can be a [string], or [ref], but not both.
Here is code that works:
function Test
{
Param([Parameter(Mandatory=$true)][ref]$someString)
write-verbose $someString.value -Verbose
$someString.value = "this is the new string"
}
cls
$someString = "hi"
Test -someString ([ref]$someString)
write-host $someString
As for "workflows". They are very restricted, read PowerShell Workflows: Restrictions. In particular you can't invoke a method on an object within workflow. This will break the line:
$someString.value = "this is the new string"
I don't think that using [ref] parameters in a workflow is practical, because of workflow restrictions.

I felt I needed to write this complementary very simplistic answer since this was the first google hit when searching for info on using reference parameters in Powershell functions. Although your question was not about functions but workflows:
Example using reference parameters in functions (doesn't work with workflow):
Function myFunction ([ref]$aString) {
$aString.Value = "newValue";
}
$localVariable = "oldValue"
Write-Host $localVariable # Outputs: oldValue
myFunction ([ref]$localVariable);
Write-Host $localVariable # Outputs: newValue
With functions you can define parameter to be both a reference and another type, like this (but not with workflows):
Function myFunction ([ref][string]$aString) {
$aString.Value = "newValue";
}
$localVariable = "oldValue"
Write-Host $localVariable # Outputs: oldValue
myFunction ([ref]$localVariable);
Write-Host $localVariable # Outputs: newValue
I agree with Jan, you should not be trying to use reference parameters in workflows because of workflow restrictions (Method invocation on objects): https://blogs.technet.microsoft.com/heyscriptingguy/2013/01/02/powershell-workflows-restrictions/

Related

Perform functions default action, unless pipeline input was given?

Lets say I have a function like this:
Function Hello(){
    Write-Host "Hello, World"}
when used on its own it works perfect but I would also like for it to receive pipeline input:
$MyString = "Something Else please"
$MyString | Hello
In the second example, Something Else please should be printed instead of the default value of Hello, World.
I have searched and searched and have turned up empty handed. everything out there keeps mentioning default parameters. I am not looking to use parameters.
Someone please point me in the right direction.
Declare a parameter, and:
assign it a default value.
make it pipeline-binding, and be sure to process each pipeline input object in the function body, namely via a process block
Note: Declaring a parameter pipeline-binding (with [Parameter(ValueFromPipeline)] below) implicitly makes a function an advanced one, which has (generally beneficial) behavioral implications - see this answer.
function Hello {
param(
[Parameter(ValueFromPipeline)]
$InputObject = 'Hello, World' # default value
)
process {
# Called for each pipeline input object, or once with the default value.
Write-Host $InputObject
}
}
For robustness, it's generally preferable to declare parameters explicitly, as shown above.
The - less desirable - parameter-less, simple-function alternative is the following, which collects all pipeline input up front, as it too implicitly runs in an end block, and uses the automatic $input variable and $MyInvocation.ExpectingInput to detect if pipeline input was provided, as suggested by Santiago Squarzon:
function Hello {
if ($MyInvocation.ExpectingInput) { # input from the pipeline
$input | Write-Host # pass each pipeline input object to Write-Host
} else { # no pipeline input -> use default value
'Hello, World' | Write-Host
}
}
As for what you tried in your answer:
By not using a process block, in effect only the last input object from the pipeline is bound to parameter $InputObject, because a function body without (any one of) begin, process and end blocks implicitly runs in an end block, i.e. after all pipeline input has been received.
Generally, there's no good reason to type a parameter [PSObject] or [PSObject[]], given that [psobject] is a usually invisible helper type used behind the scenes.
Not typing a parameter is the same as typing it [object], which is what should be used to accept arguments of any type.
Typing it [array] is the same as typing it [object[]], but note that if you type a pipeline-binding parameter as an array, each individual input object is automatically converted to an array - which slows down processing.
Only if you need to accept multiple values as a single command-line argument (as opposed to via the pipeline) is declaring a parameter as an array warranted - and when you do, you then need to enumerate each bound parameter value in your process block, as it may itself be a true array.
As an aside: That pipeline-binding parameters declared with scalar types only accept multiple values via the pipeline, but not also implicitly as an argument is the subject of GitHub issue #4242
By using this syntax for you function you can create what I think you're after.
Clear-Host
Function Hello {
Param (
[Parameter(Mandatory=$False,ValueFromPipeline=$True)]
[String] $MyText = "Hello"
)
Write-Host "$MyText, World"
}
Hello
"It's A Wonderful" | Hello
Output:
Hello, World
It's A Wonderful, World
PS>
Here is what I arrived at after #Santiago Squarzon pointed me to the right direction
Function Hello {
Param (
[Parameter(Mandatory=$False,ValueFromPipeline=$True)]
[PSObject[]] $InputObject,
)
if ($PSCmdlet.MyInvocation.ExpectingInput) {
"Data received from pipeline input: '$($InputObject)'"
}
else {
Write-host "Hello World"
}
}

Why does my mandatory hashtable parameter not take my value

I have a function with parameters like this:
function foo {
Param(
[Parameter(Mandatory)]
[string]$foo,
[Parameter(Mandatory)]
[hashtable]$bar
)
}
if i call it like this everything works fine:
foo -foo "abc" -bar #{"a"="b"}
but if I only call foo and PowerShell asks me to specify values for my mandatory parameters, it won't accept my hashtable.
PS C:\Users\abc> foo
cmdlet foo at command pipeline position 1
Supply values for the following parameters:
foo: "abc"
bar: #{"a"="b"}
Cannot convert the "#{"a"="b"}" value of type "System.String" to type "System.Collections.Hashtable".
why does PowerShell not take my specified value?
I also tried to write it different ways like #{a=b}, "a"="b", #{a=b;}, #{"a"="b";} etc.
When you mark a parameter as Mandatory and don't specify a value (argument) for it on invocation, PowerShell automatically prompts you for a value.
Unfortunately, this automatic prompting mechanism for missing mandatory parameter values is broken (it has never worked, and still doesn't as of PowerShell v7):
You currently cannot interactively supply a value for any of the following types:
[hashtable],[scriptblock], [bool], [switch] (the latter two being uncommon as mandatory parameters) - their literal forms #{ ... }, { ... } and $true / $false are simply not recognized.
There's also no way to interactively specify $null.
Additionally, the automatic prompts, when they do work, are inconvenient (no tab completion, no re-prompting on invalid input), and therefore currently of little use.
These problems are being discussed in this GitHub issue.
Given the above, it's better to bypass the automatic prompts altogether and instead throw an error when no value was provided:
function foo {
Param(
# Use a default value that throws an error.
[string] $foo = $(Throw "Please pass a value for -foo."),
# Use a default value that throws an error.
[hashtable] $bar = $(Throw "Please pass a value for -bar.")
)
}
Note that [Parameter(Mandatory)] then mustn't be used, as that would still trigger an automatic prompt, despite the presence of a default value.
Read-Host (which is invoked automatically when this happens) always returns strings. If you want to take hashtable input like that, you will need to process the input. I would do something like:
if ($foo -is [system.Collections.hashtable]) {
//proceed
} else if ($foo -is [system.array]) {
Foreach ($fooItem in $foo) {
$myhash.add($key, $fooItem)
}
} else if ($foo -is [system.string]) {
$fooItem = $foo.split(,)
...
} else {
//error
}

How to change array of PSCustomObject in Powershell

I can modify PSCustomObject property which is Array like:
$a.b.c += "new item"
Is it possible to do it with function? I mean to pass $a.b.c to function and to modify it. Seems that it's not trivial (if possible even): I tried [ref] but without success. Only one way which works for me is to return new value and to assign it, but this involve $a.b.c expression on both sides of function call which makes line long. I tried also:
function AddItem {
Param(
$Obj,
$Value,
[switch]$Uniq
)
$MutObj = [System.Collections.ArrayList]$Obj
$MutObj.Add($Value) > $null
}
but this also does not work, seems that $a.b.c += x is actually change property "c" of $a.b object and I have not $a.b in function scope. Is it achievable even with function and with modification in place in its body?
I think what you are asking is why the object doesn't change in the function.
Powershell handles Function variables as Values which means it creates another variable exactly like the one passed in and changes it in scope of the function. What you want to do is make this by Reference meaning it will mess with the same object passed into the function. This is done by the type [Ref] added to the value passed into the parameter. In this example the magic is handled here Additem -Object ([ref]$JSON) -Value "TEST"
Full script
function AddItem {
Param(
$Object,
$Value,
[switch]$Uniq
)
$Object.Value += $Value
}
$JSON = '[["aaa", "bbb"], ["ccc", "ddd"]]' | ConvertFrom-Json
Additem -Object ([ref]$JSON) -Value "TEST"
"0 : $($JSON[0])"
"0 : $($JSON[1])"
"0 : $($JSON[2])"

Default value of mandatory parameter in PowerShell

Is it possible to set a default value on a mandatory parameter in a function?
It works without having it set as an mandatory ...
Ie.
Function Get-Hello {
[CmdletBinding()]
Param([Parameter(Mandatory=$true)]
[String]$Text = $Script:Text
)
BEGIN {
}
PROCESS {
Write-Host "$Script:Text"
Write-Host "$Text"
}
END {
}
}
$Text = "hello!"
Get-Hello
Reason for asking this is because i have a function that has some required parameters and the function works perfect with calling it with the required parameters but i also want to make it possible for these variables to be defined in the scripts that use this function in a "better presentable & editable" way along with the function to be able to be run with defining the required parameters.
Hence if defined in the script scope it should take that as default else it should prompty for the value.
Thanks in Advance,
If you targeting to PowerShell V3+, then you can use $PSDefaultParameterValues preferences variable:
$PSDefaultParameterValues['Get-Hello:Text']={
if(Test-Path Variable::Script:Text){
# Checking that variable exists, so we does not return $null, or produce error in strict mode.
$Script:Text
}
}

How to pass a named function as a parameter (scriptblock)

Let's take the classic first-order functions example:
function Get-MyName { "George" }
function Say-Hi([scriptblock]$to) {
Write-Host ("Hi "+(& $to))
}
This works just fine:
Say-Hi { "Fred Flintstone" }
this does not:
Say-Hi Get-MyName
because Get-MyName is evaluated, not passed as a value itself. How do I pass Get-MyName as a value?
You have to pass Get-Myname as a scriptblock, because that's how you've defined the variable type.
Say-Hi ${function:Get-MyName}
If you are ready to sacrifice the [scriptblock] parameter type declaration
then there is one more way, arguably the simplest to use and effective. Just
remove [scriptblock] from the parameter (or replace it with [object]):
function Get-MyName { "George" }
function Say-Hi($to) {
Write-Host ("Hi "+(& $to))
}
Say-Hi Get-MyName
Say-Hi { "George" }
So now $to can be a script block or a command name (not just a function but also alias, cmdlet, and script).
The only disadvantage is that the declaration of Say-Hi is not so self describing.
And, of course, if you do not own the code and cannot change it then this is
not applicable at all.
I wish PowerShell has a special type for this, see this suggestion.
In that case function Say-Hi([command]$to) would be ideal.
This might be a better example to illustrate the question, and details of execution scope. #mjolinor's answer appears to work nicely for this use case:
function Get-MyName($name) { $name; throw "Meh" }
function Say-Hi([scriptblock]$to) {
try {
Write-Host ("Hi "+(& $to $args)) # pass all other args to scriptblock
} catch {
Write-Host "Well hello, $_ exception!"
}
}
The command and its output:
PS C:\> Say-Hi ${function:Get-MyName} 'George'
Well hello, Meh exception
In particular, I'm using this pattern for wrapping functions that work with a flaky remote SQL Server database connection, which sleep, then retry several times before finally succeeding or throwing a higher exception.
Strictly based on your code, the correct answer is this:
Say-Hi {(Get-MyName)}
This will produce "Hi George"