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.
Related
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>
What is a good practice to handle parameters, when do I choose what option?
Example: usually I write functions like this:
function Do-Something ($variable1, $variable2, $variable3....)
{ Do Stuff }
Now apparently this is also an option:
Param(
[Parameter(Position=0,
Mandatory=$True,
ValueFromPipeline=$True)]
[string]$userName,
...
)
I can, however, not find out why to use the second, or what the advantages really are to use this.
the second Param() block allows you to do a lot of advanced param validation.
if I need to write a short function with minimal input validation i'll use something like this:
Function Test-Port ([string]$Server,[uint16]$Port=80,[uint16]$Timeout = 3000) {
#code here
}
but if I need to write something with advanced checking like this:
function Get-SiteCert{
[CmdletBinding(DefaultParameterSetName = "Return")]
Param(
[Parameter(Mandatory=$true,Position=0)]
[string]$Server,
[Parameter(Position=1)]
[uint16]$Port=443,
[Parameter(Mandatory=$false, ParameterSetName="RawObj")]
[switch]$Raw,
[Parameter(Mandatory=$false, ParameterSetName="Export")]
[switch]$Export,
[Parameter(Mandatory=$true, ParameterSetName="Export")]
[string]$Path,
[uint16]$Timeout=3000
)
#code here
}
I'm definitely not fitting it all into the top bar, even though they're similar scripts, the second one 'does' a lot more. it's really just a case by case basis.
you can check out this link for examples of what you can do with the expansive parameters, but if you don't need these feel free to keep using whichever you prefer.
As #ConnorLSW wrote in the above answer, validation is one of the biggest benefits. With the Param block you're able to use Validate attributes, like:
Function Foo
{
Param(
[Parameter(Mandatory=$true,Position=0)]
[ValidateSet("Tom","Dick","Jane")]
[String]
$Name
,
[ValidateRange(21,65)]
[Int]
$Age
,
[ValidateScript({Test-Path $_ -PathType 'Container'})]
[string]
$Path
)
Process
{
write-host "New-Foo"
}
}
You are also able to define different parameter sets, if your function should support different parameter combination. Additionally you'll also get "out-of-box" documentation via Get-Help if you are the Mandatory and Positional properties of the Parameter attribute. E.g.:
get-help Foo -Detailed
NAME
Foo
SYNTAX
Foo [-Name] {Tom | Dick | Jane} [[-Age] <int>] [-Path <string>] [<CommonParameters>]
PARAMETERS
-Age <int>
-Name <string>
-Path <string>
<CommonParameters>
This cmdlet supports the common parameters: Verbose, Debug,
ErrorAction, ErrorVariable, WarningAction, WarningVariable,
OutBuffer, PipelineVariable, and OutVariable. For more information, see
about_CommonParameters (http://go.microsoft.com/fwlink/?LinkID=113216).
ALIASES
None
REMARKS
None
Based on the out brackets of the Age parameter you'll that it is an optional parameter. So its all about description, validation and documentation.
Hope that helps.
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.
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.
Here is my code:
function Get-OSInfo {
param([string]$Computer)
$OS = gwmi -class Win32_OperatingSystem -computer $Computer
$OS | Add-Member –MemberType NoteProperty –Name OSType –Value ""
$OS.OSType = Get-OSType -Input $OS
write $OS
}
function Get-OSType {
param([?]$Input)
if ($Input.ProductType -eq 1) {
write "Client OS"
}
}
$blah = Get-OSInfo -Computer mypc
$blah | fl *
I realize that this could be done with a single function (or in the body of the script itself), but I have simplified the functions to highlight the trouble I'm having. What I want to do is pass the gwmi dataset from the Get-OSInfo function as a parameter variable in the Get-OSType so I can reference all of its properties in the second function without passing them individually from the first. Clear as mud?
I have tried multiple parameter accelerator types, [ref], [array], [object[]], etc., but I haven't found anything that works as a parameter. The only thing that has proven to work is to change the second function to use args[0] for accepting input, but that is not as clean as using parameters, and since it works, I can't help but think there is a parameter that should work as well.
Avoid using $input as that has special meaning in functions (representing pipeline input). Just rename the parameter to something like $OS.
The docs (man about_automatic_variables) on $input say:
Contains an enumerator that enumerates all input that is
passed to a function. The $input variable is available only to
functions and script blocks (which are unnamed functions). In the
Process block of a function, the $input variable enumerates the
object that is currently in the pipeline. When the Process block
completes, there are no objects left in the pipeline, so the $input
variable enumerates an empty collection. If the function does not
have a Process block, then in the End block, the $input variable
enumerates the collection of all input to the function.