Home > Software engineering >  How to achieve @args splatting in an advanced function in Powershell?
How to achieve @args splatting in an advanced function in Powershell?

Time:02-11

Consider the following simple function:

function Write-HostIfNotVerbose()
{
    if ($VerbosePreference -eq 'SilentlyContinue')
    {
        Write-Host @args
    }
}

And it works fine:

enter image description here

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:

enter image description here

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="&lt;CommandParameterName&gt;">Argument1</S>
    </MS>
  </Obj>
  <S>Hello</S>
  <Obj RefId="2">
    <S>-Argument2</S>
    <MS>
      <S N="&lt;CommandParameterName&gt;">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.

  • Related