Can I suppress parameter aliases? - powershell

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 '--'.

Related

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.

Use a variable in PowerShell to pass multiple arguments to an external program

I downloaded the npm package for merge junit reports - https://www.npmjs.com/package/junit-merge.
The problem is that I have multiple files to merge and I am trying to use string variable to hold file names to merge.
When I write the script myslef like:
junit-merge a.xml b.xml c.xml
This works, the merged file is being created, but when I do it like
$command = "a.xml b.xml c.xml"
junit-merge $command
This does not work. The error is
Error: File not found
Has anyone faced similar issues?
# WRONG
$command = "a.xml b.xml c.xml"; junit-merge $command
results in command line junit-merge "a.xml b.xml c.xml"[1], i.e. it passes a string with verbatim value a.xml b.xml c.xml as a single argument to junit-merge, which is not the intent.
PowerShell does not act like POSIX-like shells such as bash do in this regard: In bash, the value of variable $command - due to being referenced unquoted - would be subject to word splitting (one of the so-called shell expansions) and would indeed result in 3 distinct arguments (though even there an array-based invocation would be preferable).
PowerShell supports no bash-like shell expansions[2]; it has different, generally more flexible constructs, such as the splatting technique discussed below.
Instead, define your arguments as individual elements of an array, as justnotme advises:
# Define the *array* of *individual* arguments.
$command = "a.xml", "b.xml", "c.xml"
# Pass the array to junit-merge, which causes PowerShell
# to pass its elements as *individual arguments*; it is the equivalent of:
# junit-merge a.xml b.xml c.xml
junit-merge $command
This is an application of a PowerShell technique called splatting, where you specify arguments to pass to a command via a variable:
Either (typically only used for external programs, as in your case):
As an array of arguments to pass individually as positional arguments, as shown above.
Or (more typically when calling PowerShell commands):
As a hashtable to pass named parameter values, in which you must replace the $ sigil in the variable reference with #; e.g., in your case #command; e.g., the following is the equivalent of calling Get-ChildItem C:\ -Directory:
$paramVals = #{ LiteralPath = 'C:\'; Directory = $true }; Get-ChildItem #paramVals
Caveat re array-based splatting:
Due to a bug detailed in GitHub issue #6280, PowerShell doesn't pass empty arguments through to external programs (applies to all Windows PowerShell versions / and as of PowerShell (Core) 7.2.x; a fix may be coming in 7.3, via the $PSNativeCommandArgumentPassing preference variable, which in 7.2.x relies on an explicitly activated experimental feature).
E.g., foo.exe "" unexpectedly results in just foo.exe being called.
This problem equally affects array-based splatting, so that
$cmdArgs = "", "other"; foo.exe $cmdArgs results in foo.exe other rather than the expected foo.exe "" other.
Optional use of # with array-based splatting:
You can use the # sigil also with arrays, so this would work too:
junit-merge #command
There is a subtle distinction, however.
While it will rarely matter in practice,
the safer choice is to use $, because it guards against (the however hypothetical) accidental misinterpretation of a --% array element you intend to be a literal.
Only the # syntax recognizes an array element --% as the special stop-parsing symbol, --%
Said symbol tells PowerShell not to parse the remaining arguments as it normally would and instead pass them through as-is - unexpanded, except for expanding cmd.exe-style variable references such as %USERNAME%.
This is normally only useful when not using splatting, typically in the context of being able to use command lines that were written for cmd.exe from PowerShell as-is, without having to account for PowerShell's syntactical differences.
In the context of splatting, however, the behavior resulting from --% is non-obvious and best avoided:
As in direct argument passing, the --% is removed from the resulting command line.
Argument boundaries are lost, so that a single array element foo bar, which normally gets placed as "foo bar" on the command line, is placed as foo bar, i.e. effectively as 2 arguments.
[1] Your call implies the intent to pass the value of variable $command as a single argument, so when PowerShell builds the command line behind the scenes, it double-quotes the verbatim a.xml b.xml c.xml string contained in $command to ensure that. Note that these double quotes are unrelated to how you originally assigned a value to $command.
Unfortunately, this automatic quoting is broken for values with embedded " chars. - see this answer, for instance.
[2] As a nod to POSIX-like shells, PowerShell does perform one kind of shell expansion, but (a) only on Unix-like platforms (macOS, Linux) and (b) only when calling external programs: Unquoted wildcard patterns such as *.txt are indeed expanded to their matching filenames when you call an external program (e.g., /bin/echo *.txt), which is feature that PowerShell calls native globbing.
I had a similar problem. This technique from powershell worked for me:
Invoke-Expression "junit-merge $command"
I also tried the following (from a powershell script) and it works:
cmd / c "junit-merge $command"

Can you splat positional arguments in PowerShell?

Does PowerShell support splatting of positional arguments as opposed to named parameters?
PowerShell's argument splatting (see Get-Help about_Splatting) offers two fundamental choices:
splatting via a hashtable: works for named arguments only (e.g., -Path C:\Windows
splatting via an array-like value: works for positional arguments only (e.g., C:\Windows) - except in non-advanced functions that pass all [unbound] arguments through to another command via #args (i.e., by splatting the automatic $args array variable containing all unbound arguments), in which case named arguments are also supported, owing to magic built into #args only.
Note: This dichotomy applies when passing arguments to PowerShell cmdlets / functions (with declared parameters), whereas external programs perform their own argument parsing, which may or may not interpret the set of arguments passed as named.[1]
That said, you can combine either form with regular, individual argument passing - using any combination of individual positional arguments, individual named arguments, hashtable-splatting, and array-splatting.
In both cases, the source data structure must be:
stored in a variable beforehand.
referenced with sigil # instead of $.
Note: A future enhancement, detailed in this RFC, may bring the ability to splat expressions directly, without the need for an intermediate variable, though as of PowerShell Core 7 it is unclear when this will be implemented.
Examples:
# Positional binding via *array*.
# Note that a function's / script block's parameters are by default positional.
PS> $posArgs = 'one', 'two'; & { param($foo, $bar) "`$foo: $foo"; "`$bar: $bar" } #posArgs
$foo: one
$bar: two
# Named binding via *hashtable*
PS> $namedArgs=#{bar='two';foo='one'}; & { param($foo, $bar) "`$foo: $foo"; "`$bar: $bar" } #namedArgs
$foo: one
$bar: two
# *Combining* hashtable splatting with a regular, positional argument
PS> $namedArgs=#{bar='two'}; & { param($foo, $bar) "`$foo: $foo"; "`$bar: $bar" } #namedArgs one
$foo: one
$bar: two
[1] Splatting with external programs:
Generally, you do not need splatting when you call external programs, because:
You can pass arrays as-is (with the usual $ sigil)
The only exception is if you wanted to include %--, the stop-parsing symbol (see Get-Help about_Parsing, in the array of arguments; you do need to use the # sigil in that event.
Use the individual array elements to satisfy the external program's syntax requirements, including its named arguments, if any
(e.g., $args = '/c', 'ver'; cmd $args to execute cmd /c ver).
The way hashtable splats are translated into command-line tokens may or may not be recognized by external programs:
Specifically, a hashtable entry with key <paramName> and value <value> is translated into a single argument formatted as -<paramName>:<value> - a format that few external command-line utilities recognize.

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]

Make all of the function's arguments mandatory by default

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.