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://docs.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
CodePudding user response:
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>
CodePudding user response:
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.