Make all of the function's arguments mandatory by default - powershell

TIL that all of the function's arguments are optional by default.
function f([int] $x) {
if (!$x) {
echo "Why so null?"
}
}
f
Et voilĂ ! The forgotten $x argument just became a $null
> .\script.ps1
Why so null?
For $x to be mandatory, its declaration needs to be updated to [parameter(Mandatory=$true)][int] $x, which doesn't appear to be a sane solution if there's more than one or two parameters. It'd be nice to have this behavior as the default, because otherwise a huge codebase containing a lots of functions looks a little bit verbose and oversaturated with attributes.
At first glance, Set-StrictMode sounds like a magic word that should make all of the function's arguments mandatory by default, but unfortunately it doesn't behave this way.
What are the best practices for making all of the function's arguments in the scope mandatory?

The best practice is to mark all mandatory arguments as mandatory.
With PowerShell all mandatory arguments must be explicitly specified or else they will be considered optional, at this time no catch all exists.
You can find more on PowerShell argument properties here.

Related

In Powershell how to generate an error on invalid flags and switches that are not listed in the param statement?

Trying to get get param(...) to be have some basic error checking... one thing that puzzles me is how to detect invalid switch and flags that are not in the param list?
function abc {
param(
[switch]$one,
[switch]$two
)
}
When I use it:
PS> abc -One -Two
# ok... i like this
PS> abc -One -Two -NotAValidSwitch
# No Error here for -NotAValidSwitch? How to make it have an error for invalid switches?
As Santiago Squarzon, Abraham Zinala, and zett42 point out in comments, all you need to do is make your function (or script) an advanced (cmdlet-like) one:
explicitly, by decorating the param(...) block with a [CmdletBinding()] attribute.
and/or implicitly, by decorating at least one parameter variable with a [Parameter()] attribute.
function abc {
[CmdletBinding()] # Make function an advanced one.
param(
[switch]$one,
[switch]$two
)
}
An advanced function automatically ensures that only arguments that bind to explicitly declared parameters may be passed.
If unexpected arguments are passed, the invocation fails with a statement-terminating error.
Switching to an advanced script / function has side effects, but mostly beneficial ones:
You gain automatic support for common parameters, such as -OutVariable or -Verbose.
You lose the ability to receive unbound arguments, via the automatic $args variable variable (which is desired here); however, you can declare a catch-all parameter for any remaining positional arguments via [Parameter(ValueFromRemainingArguments)]
To accept pipeline input in an advanced function or script, a parameter must explicitly be declared as pipeline-binding, via [Parameter(ValueFromPipeline)] (objects as a whole) or [Parameter(ValueFromPipelineByPropertyName)] (value of the property of input objects that matches the parameter name) attributes.
For a juxtaposition of simple (non-advanced) and advanced functions, as well as binary cmdlets, see this answer.
If you do not want to make your function an advanced one:
Check if the automatic $args variable - reflecting any unbound arguments (a simpler alternative to $MyInvocation.UnboundArguments) - is empty (an empty array) and, if not, throw an error:
function abc {
param(
[switch]$one,
[switch]$two
)
if ($args.Count) { throw "Unexpected arguments passed: $args" }
}
Potential reasons for keeping a function a simple (non-advanced) one:
To "cut down on ceremony" in the parameter declarations, e.g. for pipeline-input processing via the automatic $input variable alone.
Generally, for simple helper functions, such as for module- or script-internal use that don't need support for common parameters.
When a function acts as a wrapper for an external program to which arguments are to be passed through and whose parameters (options) conflict with the names and aliases of PowerShell's common parameters, such as -verbose or -ov (-Out-Variable).
What isn't a good reason:
When your function is exported from a module and has an irregular name (not adhering to PowerShell's <Verb>-<Noun> naming convention based on approved verbs) and you want to avoid the warning that is emitted when you import that module.
First and foremost, this isn't an issue of simple vs. advanced functions, but relates solely to exporting a function from a module; that is, even an irregularly named simple function will trigger the warning. And the warning exists for a good reason: Functions exported from modules are typically "public", i.e. (also) for use by other users, who justifiable expect command names to follow PowerShell's naming conventions, which greatly facilitates command discovery. Similarly, users will expect cmdlet-like behavior from functions exported by a module, so it's best to only export advanced functions.
If you still want to use an irregular name while avoiding a warning, you have two options:
Disregard the naming conventions altogether (not advisable) and choose a name that contains no - character, e.g. doStuff - PowerShell will then not warn. A better option is to choose a regular name and define the irregular names as an alias for it (see below), but note that even aliases have a (less strictly adhered-to) naming convention, based on an official one or two-letter prefix defined for each approved verb, such as g for Get- and sa for Start- (see the approved-verbs doc link above).
If you do want to use the <Verb>-<Noun> convention but use an unapproved verb (token before -), define the function with a regular name (using an approved verb) and also define and export an alias for it that uses the irregular name (aliases aren't subject to the warning). E.g., if you want a command named Ensure-Foo, name the function Set-Foo, for instance, and define Set-Alias Ensure-Foo Set-Foo. Do note that both commands need to be exported and are therefore visible to the importer.
Finally, note that the warning can also be suppressed on import, namely via Import-Module -DisableNameChecking. The downside of this approach - aside from placing the burden of silencing the warning on the importer - is that custom classes exported by a module can't be imported this way, because importing such classes requires a using module statement, which has no silencing option (as of PowerShell 7.2.1; see GitHub issue #2449 for background information.

Powershell function call changing passed string into int

So I am using the kind of buggy Sapien powershell studio to make a powershell driven GUI application, and I am attempting to perform an ADSI query.
$nameOfDeviceInput is a System.Windows.Forms.TextBox
On one form, I have the following function:
$buttonPerformAction_Click={
if (FindInAD($nameOfDeviceInput.Text).Count -gt 0)
{
$buttonPerformAction.BackColor = 'Red'
$buttonPerformAction.Text = "System already exists in AD with that name. Try another name"
return
}
.....
}
On the "main" form, I have the function FindInAD
function FindInAd($nameOfSystem)
{
Write-Host "seeking system" $nameOfSystem
([adsisearcher]"(CN=$nameOfSystem)").FindAll()
}
FindInAd() is failing because for whatever reason, $nameOfSystem is set to 1, and if I don't explicitly cast it as a string, it gets implicitly cast to Int32 (obviously)
I have tried the following:
Fully qualifying the textbox input by notating the form it belongs to ( $adObjectModifier )
$buttonPerformAction_Click={
if (FindInAD($adObjectModifier.$nameOfDeviceInput.Text).Count -gt 0)
{
$buttonPerformAction.BackColor = 'Red'
$buttonPerformAction.Text = "System already exists in AD with that name. Try another name"
return
}
.....
}
Explicitly casting the $nameOfSystem parameter as a type of [string]
function FindInAd([string]$nameOfSystem)
{
Write-Host "seeking system" $nameOfSystem
([adsisearcher]"(CN=$nameOfSystem)").FindAll()
}
Passing a raw string into FindInAD from the AdObjectModifier form.
....
if (FindInAD("Test").Count -gt 0)
....
There is nothing else on the output pipeline at the time, (at least not from me) in between the method invocation. It is EventHandler > Function Call with String parameter
Why are the strings I'm passing getting changed to a digit???
EDIT: I think my passed parameter is being automatically replaced with the resulting boolean somehow, but this doesn't make any sense to me....
Your have a syntax problem:
FindInAD($nameOfDeviceInput.Text).Count # WRONG
Note: Wrong in this context means: the syntax is formally valid, but doesn't do what you expect - see the bottom section.
It should be:
(FindInAD $nameOfDeviceInput.Text).Count
PowerShell commands - functions, cmdlets, scripts and external programs - are invoked like shell commands - foo arg1 arg2 - and not like C# methods - foo('arg1', 'arg2').
That is:
Do not put (...) around the list of arguments.
However, you do need (...) around the call as a whole if you want a command call to participate in an expression, as shown above with the access to property .Count - see this answer for more information.
Separate arguments with spaces, both from each other and from the command name - do not use ,
, between arguments functions differently: It constructs an array that is passed as a single argument - see below.
You may pass simple strings (ones that contain neither spaces nor PowerShell metacharacters such as ; or &) as barewords; that is, quoting them is optional; e.g., instead of foo 'bar', you can call foo bar - see this answer for how PowerShell parses unquoted command arguments.
Also, if a target function or script has explicitly declared parameters (which binary cmdlets invariably do), such as -bar and -baz, you can pass your values as named arguments, i.e. by prepending them with the target parameter name; doing so is good practice in scripts: foo -bar arg1 -baz arg2
By contrast, calling methods of objects uses the syntax familiar from regular programming languages such as C# ($obj.foo('arg1', 'arg2'))
This difference relates two PowerShell's two fundamental parsing modes, explained in detail in this answer:
Commands are parsed in argument mode - as in shells.
Method calls and operator-based expressions are parsed in expression mode - as in regular programming languages.
These modes are required in order to allow PowerShell serve double duty: as a shell on the one hand, and as a scripting (programming) language on the other.
PowerShell can help you avoid this syntax problem:
Note that the problem isn't that using method syntax to call a command is invalid syntax, but that it doesn't work as intended, which can be difficult to diagnose.
In short: When you call command foo as foo('foo', 'bar'), ('foo', 'bar')is a 2-element array, which is then passed to foo as a single argument.
To prevent the problem to begin with, you can set Set-StrictMode to -Version 2 or higher, which makes PowerShell report an error if you accidentally use method syntax when calling a command:
# Turn on the check for accidental method syntax.
# Note: This also turns on ADDITIONAL checks - see below.
Set-StrictMode -Version 2
# This call now produces an ERROR, because the proper syntax would be:
# foo 'a' 'b'
foo('a', 'b')
Caveats:
Set-StrictMode -Version 2 comprises additional strictness checks that you must then also conform to, notably:
You must not reference non-existent variables.
You must not reference non-existent properties; see GitHub issue #2798 for an associated pitfall in connection with PowerShell's unified handling of scalars and collections.
An error is reported only for pseudo method calls with multiple arguments (e.g.,
foo('bar', 'baz')), not with only one; e.g., foo('bar') is accepted, because the single-argument case generally still (accidentally) works.
The errors reported for strictness violations are statement-terminating errors: that is, they only terminate the statement at hand, but by default the script continues; to ensure that overall execution aborts - on any type of error - you'd have to set
$ErrorActionPreference = 'Stop' at the start of your code. See this answer for more information.
As for what you tried:
FindInAD($nameOfDeviceInput.Text).Count
is the same as:
FindInAD ($nameOfDeviceInput.Text).Count
That is, the result of expression ($nameOfDeviceInput.Text).Count is passed as an argument to function FindInAD.

PowerShell script not assigning named parameter correctly [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 4 years ago.
Improve this question
I have a script that starts with the following block
Param(
[string] $c,
[Parameter(ValueFromRemainingArguments=$true)]
[string] $args
)
When I call this script and specify -n -cc -ou name -c 16.0., it seems like it just takes the whole command line and stuffs it into $c. If I do it with
[Parameter(Position=0, ValueFromRemainingArguments=$true)]
[string] $args
then it does not actually pick up -c 16.0. and put it into $c at all, but just stuffs everything into $args. Why?
So what I want is hopefully simple. -c can be specified, but doesn't have to. There can also be a bunch of other params that can be specified, but don't have to and for those I just want to collect them in one string. If -c is specified, I want it to be put into $c, but left out of $args. if -c is not specified, $c variable should be empty. How to achieve that?
This is the answer I tried using and it doesn't work for me:
How do I force declared parameters to require explicit naming?
While it is generally ill-advised to name a parameter $args, as Ansgar Wiechers points out, because $args is normally an automatic variable, your solution should still work in principle, at least in recent PowerShell versions.
However, as TessellatingHeckler points out, invocation with -ou causes an error, because - due to use of a parameter attribute ([Parameter(...)]) - your function is an advanced function that implicitly supports common parameters such as -OutVariable and -OutBuffer, which -ou tries to bind to based on name-prefix matching, but fails due to ambiguity.
The bigger issue, however, is that you therefore cannot pass (non-declared) parameter names that are prefixes of common parameter names in advanced functions as-is.
(Another side effect of using an advanced function is that the automatic $Args variable is not populated, because you can only pass arguments that bind to declared parameters to an advanced function.)
Suboptimal workaround:
Place all all pass-through arguments at the end of the command line and precede them --, the special parameter that indicates that all remaining tokens are to be interpreted as positional arguments, even if they look like parameter names.
# Everything after -- (which itself will be removed), is passed
# through as a *positional* argument (which a ValueFromRemainingArguments
# parameter would collect).
Foo -c 16.0 -- -n -cc -ou name
Note: You could alternatively quote individual tokens to prevent their interpretation as a parameter name (e.g., Foo -n -cc '-ou' name -c 16.0), but that would require you to remember the names of all common parameters and apply quoting as needed.
If the features of an advanced function aren't strictly needed, a better workaround is to use a simple function:
Use the automatic $Args variable for accessing all arguments not bound to declared parameters (do not declare it as a parameter).
However, this requires that you neither use any parameter attributes nor the [CmdletBinding()] attribute, to prevent the function from becoming an advanced one - see about_Functions_Advanced.
function Foo {
# Don't use [CmdletBinding()] or [Parameter(...)] attributes.
# Only then will the automatic $Args variable work.
Param(
[string] $c
)
"`$c: [$c]"
# Use the automatic $Args variable, which collects all arguments
# that did not bind to declared parameters in an array.
$argsAsString = [string] $Args
"`$Args: [$argsAsString]"
}
Foo -n -cc -ou name -c 16.0
The above yields:
$c: [16.0]
$args: [-n -cc -ou name]

Should I use colons for all arguments of a function/script call?

I recently started using PowerShell, and noticed that I could pass argument values using a space between the argument name and value, or using a colon, like this:
MyFunction -Parameter value
or
MyFunction -Parameter:value
I started using the colon because it differentiates the line a bit more (for readability reasons), but from what I've seen, most people do not use it.
I've read a bit also about the differences between these approaches when working with switch typed arguments, that normally do not need values. In that situation, one needs to use the colon, or else the command will not work. This is another reason why I'm leaning towards using the colon for every parameter, for consistency reasons.
Is there something else I should keep in mind?
Generally speaking, when I need to execute a function with a switch parameter set to false, I simply omit the switch. That's the design intent of a switch parameter. The only time I think I would ever use the colon in a parameter is when I need to programmatically determine the value of a switch.
For example, let's say I need to get a regular directory listing on even days, and a recursive directory listing on odd days:
Get-ChildItem -Path $Path -Recurse:$((Get-Date).Day % 2 -eq 1) | ForEach-Object {...}
Beyond that, I personally wouldn't bother with the colon unless it significantly added to the readability of a given statement. It's not a commonly used syntax, so people who read your code later are more likely to be confused by it.
In general I would leave the colon off. Only use it in the situation of setting switch a parameter (typically when you want to pass a variable to it, like -Verbose:$someVariable.
Basically, I think you should be consistent with the more accepted style, which is what I've described.
If you really want to set all parameters in an internally consistent way which allows for variables for switch parameters, and is an accepted (though less known) way of passing parameters, consider splatting like so:
$params = #{
'Param1' = $value1
'Param2' = 5
'WhatIf' = $true
}
Some-Cmdlet #params

Can I suppress parameter aliases?

Given this function and invocation:
function Foo($bar) {
Write-Host "bar=$bar, other args = $args"
}
Foo -b baz -quux quuux
Powershell will treat the -b argument as an alias for the $bar argument and print
bar=baz, other args = -quux quuux
I don't want it to do this! My function needs to work with both named arguments and arbitrary additional arguments; if one of those arbitrary arguments is -b, I don't want it assigned to $bar. That is, I want it to print:
bar=, other args = -b baz -quux quuux
Can I suppress the default parameter aliasing that powershell does?
I'm using powershell 2.0
AFAIK, you can't have it both ways. You can use a "basic" function that exhibits the behavior you see here, or you can use an advanced function with named parameters which will validate the parameter names strictly.
"Arbitrary parameters" get really, really ugly and IMHO should be avoided for a number of reasons (some of which involve code security - could you end up executing unknown arbitrary code?).
Make Foo an advanced function, and make your "arbitrary" arguments a collection of Objects which is optional.
function Foo {
[cmdletBinding()]
param (
[Parameter(Mandatory=$True)]
[string]$bar,
[Parameter(Mandatory=$False)]
[object[]]$args
)
Write-Output "bar=$bar";
# Test for $args here and process each item in it
}
You can stop the parser from interpreting the -b as a parameter alias by quoting it:
function Foo($bar) {
Write-Host "bar=$bar, other args = $args"
}
Foo '-b' baz -quux quuux
bar=-b, other args = baz -quux quuux
PowerShell has no way of disabling support for unambiguous prefix matching of parameters.
There are a few options though with various tradeoffs.
Option 1 - parse arguments yourself:
function foo {
param()
# $args has all of the parameters and arguments
}
This is more difficult for you to write, but solves the problem you hope to solve because you will completely own parameter/argument binding.
The other downside is no tab completion for parameters, at least not without some extra corresponding help in TabExpansion or TabExpansion2 (e.g. using a module like TabExpansion++ or PowerTab).
Note in this example you don't want cmdlet binding even if you have a parameter that is "value from remaining arguments" because cmdlet binding will add the common parameters and those parameters will be bound if an unambiguous prefix is used.
Option 2 - Require the caller to do something different
There are several cmdlets with a similar problem, e.g. Invoke-Command or Get-Command. Both accept an array of arguments. Users must quote parameters:
icm { param($a,$b) $b } -ArgumentList '-b','hello'
These cmdlets use -ArgumentList because they really must accept arbitrary arguments that might conflict with the cmdlet's arguments.
If your problem is more constrained, there might be another option, but it's requires some uncommonly used syntax and will still have problems with some parameters. An example:
function foo
{
param([Parameter(ValueFromRemainingArguments=$true)]$ArgumentList)
$ArgumentList
}
foo -- -A 1 -Ar 2 -Arg 3
The special token '--' tells PowerShell everything after is an argument. This way, arguments that look like parameters are treated as arguments.
This approach could be confusing if callers forget '--'.