How do I package powershell advanced function args into a hashtable - function

I have a function
function Add-PromptSection() {
[CmdletBinding()]
Param(
[ConsoleColor]$Fore,
[ConsoleColor]$Back,
[Switch]$Newline,
[Scriptblock]$Condition,
[String]$String
)
$args = <something>
}
What I want to do is to collect the function arguments into a hashtable
#{ Fore=$Fore; Back=$Back; Newline=$Newline;
Condition=$Condition; String=$String }
However, if the user omitted a particular argument when calling the function, I want that key to be omitted from the hashtable. The idea is that after a bit of preprocessing, I will call Write-Host with the hashtable, as Write-Host #args. So I don't want something like $args["Back"] = $null in there.
Obviously I can do this by explicitly checking each argument (actually, can I? How do I tell if the user didn't supply $Fore? Checking for $null will work, I guess, but it's not quite the same behaviour as Write-Host) but is there a more compact approach?

It seems like what you're wanting (at least the hash table) is already being done for you by $PSBoundParameters.

if I understand your question, you are looking for default values for your arguments. Please correct me if I am wrong.
function Add-PromptSection() {
[CmdletBinding()]
Param(
[ConsoleColor]$Fore,
[ConsoleColor]$Back="Red",
[Switch]$Newline,
[Scriptblock]$Condition,
[String]$String
)
Write-Host $args
}
Now, the user can safely ignore providing $back in hashtable.

Related

How to achieve #args splatting in an advanced function in Powershell?

Consider the following simple function:
function Write-HostIfNotVerbose()
{
if ($VerbosePreference -eq 'SilentlyContinue')
{
Write-Host #args
}
}
And it works fine:
Now I want to make it an advanced function, because I want it to inherit the verbosity preference:
function Write-HostIfNotVerbose([Parameter(ValueFromRemainingArguments)]$MyArgs)
{
if ($VerbosePreference -eq 'SilentlyContinue')
{
Write-Host #MyArgs
}
}
But it does not work:
And what drives me nuts is that I am unable to identify how $args in the first example is different from $args in the second.
I know that the native #args splatting does not work for advanced functions by default - https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_splatting?view=powershell-7.2#notes
But I hoped it could be simulated, yet it does not work either. My question is - what is wrong with the way I am trying to simulate it and whether it is possible to fix my code without surfacing all the Write-Host parameters at Write-HostIfNotVerbose
Santiago Squarzon's helpful answer contains some excellent sleuthing that reveals the hidden magic behind #args, i.e. splatting using the automatic $args variable, which is available in simple (non-advanced) functions only.
The solution in Santiago's answer isn't just complex, it also isn't fully robust, as it wouldn't be able to distinguish -ForegroundColor (a parameter name) from '-ForegroundColor' a parameter value that happens to look like a parameter name, but is distinguished from it by quoting.
As an aside: even the built-in #args magic has a limitation: it doesn't correctly pass a [switch] parameter specified with an explicit value through, such as
-NoNewLine:$false[1]
A robust solution requires splatting via the automatic $PSBoundParameters variable, which in turn requires that the wrapping function itself also declare all potential pass-through parameters.
Such a wrapping function is called a proxy function, and the PowerShell SDK facilitates scaffolding such functions via the PowerShell SDK, as explained in this answer.
In your case, you'd have to define your function as follows:
function Write-HostIfNotVerbose {
[CmdletBinding()]
param(
[Parameter(Position = 0, ValueFromPipeline, ValueFromRemainingArguments)]
[Alias('Msg', 'Message')]
$Object,
[switch] $NoNewline,
$Separator,
[System.ConsoleColor] $ForegroundColor,
[System.ConsoleColor] $BackgroundColor
)
begin {
$scriptCmd =
if ($VerbosePreference -eq 'SilentlyContinue') { { Write-Host #PSBoundParameters } }
else { { Out-Null } }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
}
process {
$steppablePipeline.Process($_)
}
end {
$steppablePipeline.End()
}
}
[1] Such an argument is invariably passed through as two arguments, namely as parameter name -NoNewLine by itself, followed by a separate argument, $false. The problem is that at the time the original arguments are parsed into $args, it isn't yet known what formally declared parameters they will bind to. The NoteProperty tagging applied to $args for marking elements as parameter names doesn't preserve the information as to whether the subsequent argument was separated from the parameter name with :, which for a [switch] parameter is necessary to identify that argument as belonging to the switch. In the absence of this information, two separate arguments are always passed during splatting.
This is too obscure for me to explain, but for the sake of answering what PowerShell could be doing with $args you can test this:
function Write-HostIfNotVerbose {
param(
[parameter(ValueFromRemainingArguments)]
[object[]]$MagicArgs
)
$params = #{
NotePropertyName = '<CommandParameterName>'
PassThru = $true
InputObject = ''
}
$z = foreach($i in $MagicArgs) {
if($i.StartsWith('-')) {
$params.NotePropertyValue = $i
Add-Member #params
continue
}
$i
}
if ($VerbosePreference -eq 'SilentlyContinue') {
Write-Host #z
}
}
Write-HostIfNotVerbose -ForegroundColor Green Hello world! -BackgroundColor Yellow
A way of seeing what $args is doing automatically for us could be to serialize the variable:
function Test-Args {
[System.Management.Automation.PSSerializer]::Serialize($args)
}
Test-Args -Argument1 Hello -Argument2 World
Above would give us the serialized representation of $args where we would observe the following:
<LST>
<Obj RefId="1">
<S>-Argument1</S>
<MS>
<S N="<CommandParameterName>">Argument1</S>
</MS>
</Obj>
<S>Hello</S>
<Obj RefId="2">
<S>-Argument2</S>
<MS>
<S N="<CommandParameterName>">Argument2</S>
</MS>
</Obj>
<S>World</S>
</LST>

Cannot use variable in a Where-Object in a function

I am trying to have a function that can count jobs based on the LatestStatus value that I would pass a parameter. So far what I got:
Function JobCountStatus {
Write-Output (Get-VBRJob | ?{$_.Info.LatestStatus -eq $args} | Measure-Object).Count
}
The issue is that as I've read somewhere there will be a subshell(?) executing the where so the argument is not passed.
If I replace the $args with a specific string like "Failed" it will work.
Is there a way to overcome this? I do not want to write separate functions for all possible values.
I would appreciate any comments - Thanks
Well you can just name the value when you run the function as $args is an Automatic Variable
JobCountStatus "Failed"
You can use an advanced function with a parameter, named or not:
function JobCountStatus {
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true, Position = 0)]
[System.String]
$Status
)
Process {
(Get-VBRJob | Where-Object { $_.Info.LatestStatus -eq $Status } | Measure-Object).Count
}
}
And call it like so:
JobCountStatus -Status "Failed"
# OR
JobCountStatus "Failed"
The latter having the same end result as just using $args. The only possible advantage to specifying your own parameter here is you could define a ValidateSet of statuses or an Enum of Status values so you can tab through them. An example of the former would be like so:
function JobCountStatus {
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true, Position = 0)]
[ValidateSet("Failed", "Running", "Successful", "Unknown")]
[System.String]
$Status
)
Process {
(Get-VBRJob | Where-Object { $_.Info.LatestStatus -eq $Status } | Measure-Object).Count
}
}
$args is an array, not a single value. In any case, a ScriptBlock {} is an unnamed function and $args has it's own meaning within it, so you can't use it without some modification in something like Where-Object. You would have to store the elements of $args as another variable or multiple variables to reference within a child ScriptBlock. Not a huge change for this function, but for one where more parameters are expected this can result in a lot of unnecessary code which can be difficult to maintain.
I prefer to recommend defining named parameters in most cases, and this would be a simpler change than making the parent $args work in a child ScriptBlock:
Function JobCountStatus {
Param(
[string]$Status
)
( Get-VBRJob | Where-Object { $_.Info.LatestStatus -eq $Status } ).Count
}
I've also made a few more changes in this function, I'll explain below:
Use Param() to strongly define parameters by name. You could use the simpler syntax of function JobCountStatus([string]$Status) {} but for this case it's really a matter of preference for which technique to use. Using Param() is something I recommend as a matter of convention, but you'll need to use either technique to get a named parameter.
I replaced the $args reference with $Status.
Your use of Measure-Object is extraneous and so I've removed it. Where-Object returns a collection which already has the Count property.
You can use ? if you want but it's considered best practice to omit aliases and use full cmdlet names in scripts and modules, so I've replaced ? with Where-Object.
Note that you can invoke the function/cmdlet (the difference is minimal for PowerShell-defined cmdlets) with or without the parameter, as when you don't define a positional order, the order is automatically determined in the order of declaration:
# Either will work
JobCountStatus -Status Running
JobCountStatus Running
Here is some more documentation you may find useful:
About Functions
About Advanced Functions
#Ash's answer gives some more advanced examples of what you can do with param() which are mentioned in the the Advanced Functions link above. You cannot use advanced parameter attributes with the simple function syntax I mentioned in the first bullet point.

PowerShell adds int indexes of items when returning object

I've seen a lot of similar questions on here but none of the solutions are working for me.
I have a function that looks like this
function Get-ManagedDevices {
# ...(params and stuff here)...
$batchResponse = Invoke-RestMethod #restParams
$responses = #{}
foreach ($response in $batchResponse.responses) {
$responses.Add($response.id, $response.body) > $null
}
return , $responses
}
that gets called like this
$data = Get-Devices -AccessToken $AccessToken -DeviceIds $DeviceIds
When I'm debugging inside of the function call I am getting what I expect when I look at the $response variable which is something like
PS> $responses
Name Value
---- -----
d1290207-c693-4663-88bf-58512… #{#odata.context=https://graph.microsoft.com/v1.0/$metadat…
7ad71b70-b992-490f-8822-1561a… #{#odata.context=https://graph.microsoft.com/v1.0/$metadat…
PS> $responses.count
1
However, when I try to use the $data variable from after the function call I get this
PS> $data
0
1
Name Value
---- -----
d1290207-c693-4663-88bf-58512… #{#odata.context=https://graph.microsoft.com/v1.0/$metadat…
7ad71b70-b992-490f-8822-1561a… #{#odata.context=https://graph.microsoft.com/v1.0/$metadat…
PS> $data.count
3
I'm so confused. I piped the addition of the variable to the hashtable to $null with >$null.
I added the unary operator with return like return , $responses which is what's recommended in the 'about_return' PowerShell documentation .
I tried omitting the return keyword, using an ArrayList instead, using an array instead, etc...
What on earth is going on and why can't I return a hashtable from this function?! I feel like I'm going crazy.
The about_Return Documentation makes no mention of returning a hash table directly from a function. The common problem your referring to is the ability to return an array in whole rather than iterated down the pipeline.
Its common and straight forward to return a hash table. At a glance your code could be:
function Get-ManagedDevices {
# ...(params and stuff here)...
$batchResponse = Invoke-RestMethod #restParams
$responses = #{}
foreach ($response in $batchResponse.responses) {
$responses.Add($response.id, $response.body)
}
return $responses
}
It shouldn't be more complicated that that. Remove the comma it has no effect when returning a hash. >$null has no effect on the HashTable .Add(...) method.
Given your results and the example, there may be some artifact from your earlier attempts using an ArrayList. The .Add(...) method on an ArrayList will emit the index # that it's adding to. When we forget to [void] or | Out-Null or $null = ...Add(...) this usually results in unwanted output polluting the pipeline. As you're getting the 2 index numbers then your data...

Declaring parameters does not work if anything is above param

I have a script with parameters. In order to ease the debug of the script I create a small function I found on the net to list all my variables. In order to do so, I start by getting all existing variables at the top of the script, then I create a function which compares recorded variables before and after getting parameters
Problem is when I put the $AutomaticVariables and the function before param declaration, PowerShell gives me the following error for any parameter where I set a default value. Is there anyway to workaround this … bug? If it's not a bug, why the hell this behavior. I don't see the point.
The assignment expression is not valid. The input to an assignment operator must be an object that is able to accept assignments, such as a
variable or a property.
# Array and function to debug script variable content
$AutomaticVariables = Get-Variable
function check_variables {
Compare-Object (Get-Variable) $AutomaticVariables -Property Name -PassThru |
Where -Property Name -ne "AutomaticVariables"
}
param(
[String]$hostname,
[String]$jobdesc,
[String]$type = "standard",
[String]$repo,
[String]$ocred,
[String]$site,
[String]$cred = "SRC-$($site)-adm",
[String]$sitetype,
[String]$room,
[String]$chsite = "chub"
)
# TEST - Display variables
check_variables
As mentioned in the comments, you should gather the variables you want to exclude in the calling scope:
Define function (could as well be a script), notice the $DebugFunc parameter I've added at the end:
function Do-Stuff
{
param(
[String]$hostname,
[String]$jobdesc,
[String]$type = "standard",
[String]$repo,
[String]$ocred,
[String]$site,
[String]$cred = "SRC-$($site)-adm",
[String]$sitetype,
[String]$room,
[String]$chsite = "chub",
[scriptblock]$DebugFunc
)
if($PSBoundParameters.ContainsKey('DebugFunc')){
. $DebugFunc
}
}
Now, gather the variables and define your function, then inject it into Do-Stuff:
# Array and function to debug script variable content
$AutomaticVariables = Get-Variable
function check_variables {
Compare-Object (Get-Variable) $AutomaticVariables -Property Name -PassThru | Where -Property Name -ne "AutomaticVariables"
}
Do-Stuff -DebugFunc $Function:check_variables
It's not a bug. The param section defines the input parameter of your script thus has to be the first statement (same as with functions). There is no need to perform any action before the param block.
If you explain what you want to achieve with your check_variables (not what it does). We probably can show you how to do it right.

PowerShell Function: Hashes Called by Reference?

I have several hashes and some functions should work with them. I want to make a call by reference for these hashes (do I need that):
$H1 = #{...}
$H2 = #{...}
$H3 = #{...}
...
function manage{ param( [ref]$a, [ref]$b, [ref]$c )
...
}
manage $H1 $H2 $H3
But this seems not to work. How do I do this correctly?
Thanks in advance
Hashes are considered reference types in PowerShell and are always being passed by reference so no need to use [ref] in the params here, see about_Ref.
Try without the [ref], or using [hashtable] type specifically:
function manage {
param (
[hashtable] $a,
[hashtable] $b,
[hashtable] $c
)
...
}
The changes you make to the hashes using the $a, $b, $c references inside the function will be done on the underlying hash objects and will be visible outside the function via the original $H1, $H2, $H3 references.