Parsing PowerShell script with AST - powershell

I'm trying to parse through a Pester script and extract values from the -Tag parameter. Anyone know how to do this using [System.Management.Automation.PSParser]?. I'm was thinking I'd have to loop through the tokens returned from [System.Management.Automation.PSParser]::Tokenize() but that seems pretty kludgy and given that the values for -Tag could be given in many different formats, not very practical.
At the end of the day, I'm hoping to return a collection with the Describe block name, and the list of tags (if any) for that block.
Name Tags
---- ----
Section1 {tag1, tag2}
Section2 {foo, bar}
Section3 {asdf}
Section4 {}
Here are the sample Pester tests that I'm working with.
describe 'Section1' -Tag #('tag1', 'tag2') {
it 'blah1' {
$true | should be $true
}
}
describe 'Section2' -Tag 'foo', 'bar' {
it 'blah2' {
$true | should be $true
}
}
describe 'Section3' -Tag 'asdf'{
it 'blah3' {
$true | should be $true
}
}
describe 'Section4' {
it 'blah4' {
$true | should be $true
}
}
Anyone have any ideas on how to solve this? Is [System.Management.Automation.PSParser] the right way to go or is there a better way?
Cheers

Using PS3.0+ Language namespace AST parser:
$text = Get-Content 'pester-script.ps1' -Raw # text is a multiline string, not an array!
$tokens = $null
$errors = $null
[Management.Automation.Language.Parser]::ParseInput($text, [ref]$tokens, [ref]$errors).
FindAll([Func[Management.Automation.Language.Ast,bool]]{
param ($ast)
$ast.CommandElements -and
$ast.CommandElements[0].Value -eq 'describe'
}, $true) |
ForEach {
$CE = $_.CommandElements
$secondString = ($CE | Where { $_.StaticType.name -eq 'string' })[1]
$tagIdx = $CE.IndexOf(($CE | Where ParameterName -eq 'Tag')) + 1
$tags = if ($tagIdx -and $tagIdx -lt $CE.Count) {
$CE[$tagIdx].Extent
}
New-Object PSCustomObject -Property #{
Name = $secondString
Tags = $tags
}
}
Name Tags
---- ----
'Section1' #('tag1', 'tag2')
'Section2' 'foo', 'bar'
'Section3' 'asdf'
'Section4'
The code doesn't interpret the tags as a list of strings, but simply uses the original text extent.
Use the debugger in PowerShell ISE / Visual Studio / VSCode to inspect the various data type cases.

Related

Add Custom Argument Completer for Cmdlet?

How do I add dynamic argument tab completion to a PowerShell Cmdlet?
When I type this and hit tab, I'd like for it to do tab completion.
PM> Paket-Add -NuGet FSharp.Co
These are the values I'd like to use in this example:
PM> Paket-FindPackages -SearchText FSharp.Co
FSharp.Core
FSharp.Core.3
FSharp.Configuration
FSharp.Core.Fluent-3.1
FSharp.Core.Fluent-4.0
FSharp.Compiler.Tools
FSharp.Compatibility.Scala
FSharp.Compatibility.OCaml
FSharp.Compiler.CodeDom
FSharp.Compiler.Service
FSharp.Control.Reactive
FSharp.Compatibility.Haskell
FSharp.Compatibility.OCaml.Numerics
FSharp.Compatibility.OCaml.Format
FSharp.Compatibility.OCaml.System
FSharp.Collections.ParallelSeq
FSharp.Compatibility.StandardML
FSharp.Compatibility.OCaml.LexYacc
FSharp.Control.AsyncSeq
I found this answer that gave a couple of helpful links and said I should run Get-Content function:TabExpansion2:
It looks like CommandCompletion.CompleteInput needs to implemented. I thought I read somewhere that there is a Hashtable of commands to functions. If so, where is it and how do I install custom ones? I'm using Chocolatey to distribute Paket.PowerShell. Here is the Cmdlet code.
UPDATE 2015-06-20:
I ended up getting it to work with the code here:
https://github.com/fsprojects/Paket/blob/76de1c44853ce09029ba157855525f435d951b85/src/Paket.PowerShell/ArgumentTabCompletion.ps1
# https://github.com/mariuszwojcik/RabbitMQTools/blob/master/TabExpansions.ps1
function createCompletionResult([string]$text, [string]$value, [string]$tooltip) {
if ([string]::IsNullOrEmpty($value)) { return }
if ([string]::IsNullOrEmpty($text)) { $text = $value }
if ([string]::IsNullOrEmpty($tooltip)) { $tooltip = $value }
$completionText = #{$true="'$value'"; $false=$value }[$value -match "\W"]
$completionText = $completionText -replace '\[', '``[' -replace '\]', '``]'
New-Object System.Management.Automation.CompletionResult $completionText, $text, 'ParameterValue', $tooltip | write
}
$findPackages = {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
Paket-FindPackages -SearchText $wordToComplete -Max 100 | % {
createCompletionResult $_ $_ $_ | write
}
}
$findPackageVersions = {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
if (-not $fakeBoundParameter.NuGet){ return }
Paket-FindPackageVersions -Name $fakeBoundParameter.NuGet -Max 100 | % {
createCompletionResult $_ $_ $_ | write
}
}
# create and add $global:options to the list of completers
# http://www.powertheshell.com/dynamicargumentcompletion/
if (-not $global:options) { $global:options = #{CustomArgumentCompleters = #{};NativeArgumentCompleters = #{}}}
$global:options['CustomArgumentCompleters']['Paket-Add:NuGet'] = $findPackages
$global:options['CustomArgumentCompleters']['Paket-Add:Version'] = $findPackageVersions
$function:tabexpansion2 = $function:tabexpansion2 -replace 'End\r\n{','End { if ($null -ne $options) { $options += $global:options} else {$options = $global:options}'
The completer param names are important. Renaming them will make it not work.
You may want to look at the TabExpansion++ module, which was designed to make extending tab completion easier.
I just played with it for few minutes, and I think you want something like this based on the example:
Import-Module TabExpansion++
function PaketAddNugetCompletion
{
[ArgumentCompleter(Parameter = 'Nuget', Command = 'Paket-Add')]
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
Paket-FindPackages -SearchText $wordToComplete |
ForEach-Object {
# not quite sure what property to use off the result, but this might work.
New-CompletionResult -CompletionText $_
}
}
These are called Dynamic parameters and are described in about_Functions_Advanced_Parameters.
The following example shows a sample function with standard
parameters named Name and Path, and an optional dynamic parameter
named DP1.The DP1 parameter is in the PSet1 parameter set and has a
type of Int32. The DP1 parameter is available in the Sample function
only when the value of the Path parameter contains "HKLM:", indicating
that it is being used in the HKEY_LOCAL_MACHINE registry drive.
function Get-Sample {
[CmdletBinding()]
Param ([String]$Name, [String]$Path)
DynamicParam
{
if ($path -match ".*HKLM.*:")
{
$attributes = new-object System.Management.Automation.ParameterAttribute
$attributes.ParameterSetName = "__AllParameterSets"
$attributes.Mandatory = $false
$attributeCollection = new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
$attributeCollection.Add($attributes)
$dynParam1 = new-object -Type System.Management.Automation.RuntimeDefinedParameter("dp1", [Int32], $attributeCollection)
$paramDictionary = new-object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
$paramDictionary.Add("dp1", $dynParam1)
return $paramDictionary
}
}
}
Here's another example that does validation sets dynamically.
I've re-read your question, and it looks like maybe you just want a static, pre-defined list of tab-completed values for a specific parameter. If that's the case, then you can simply use the [ValidateSet()] attribute:
function Get-Something {
[CmdletBinding()]
param(
[ValidateSet('One','Two','Three')]
[String]
$MyParam
)
}
But if the values need to be determined at runtime, then see the above section on dynamic parameters instead.

how to let function display one section

teaching myself about functions - I have written the below and problem I have at the moment is this:
1.The output is displayed in a hash table like format - ie #{FileVersion=6.1.7601.18606; IsReadOnly?=False; Directory=C:\windows\system32}. Is there a way of not displaying it in this format?
2.For the IF statment how do I get the code to just look at the ProductVersion bit of the output and not the whole function...or is that the point of a function, should I just have a function that just gets the file version separatly?
clear
function fileversion {
param ([string]$fileToCheck)
$fileInput = Get-Item $fileToCheck
$versionCheck = $fileInput.VersionInfo.ProductVersion
$DirectoryOfFile = $fileInput.DirectoryName
$IsReadOnly = $fileInput.IsReadOnly
$obj = New-Object psobject
$obj | Add-Member NoteProperty FileVersion $versionCheck
$obj | Add-Member NoteProperty IsReadOnly? $IsReadOnly
$obj | Add-Member NoteProperty Directory $DirectoryOfFile
Write-host $obj
}
$firstFile = fileversion C:\windows\system32\lsasrv.dll
If ($versionCheck -eq '6.1.7601.18606')
{ Write-host "File for is $TRUE" }
Else
{ Write-Host $False }
Don't know why you need a function since in the end you are just using one property of the object returned. Still, if you need the function you can just simplify it.
function fileversion{
param ([string]$fileToCheck)
Get-Item $fileToCheck | Select-Object #{Label="FileVersion";Expression={$_.VersionInfo.ProductVersion}},
#{Label="IsReadOnly?";Expression={$_.IsReadOnly}},
#{Label="Directory";Expression={$_.DirectoryName}}
}
No need to create a new object when the current one already has what you need. We use calculated properties to create the property names you wanted.
By default all output from functions is returned to the output stream. By sending the $obj to Write-Host you were removing that data from the stream. The output you say was Write-host casting the entire object as a [string]
Simple If
You could have just done this as well depending on your needs. This way you dont need a custom function
$version = (Get-Item C:\windows\system32\lsasrv.dll).VersionInfo.ProductVersion
If($version -eq '6.1.7601.18606'){
write-host "Version match"
} Else {
write-host "$version does not match"
}
You could always change the file path to a variable. Again, this was just suggestion. Please do whatever makes your scripting life comfortable.
use Write-output instead of write-host and check the version using the right property name (FileVersion) on the returned object:
If ($firstFile.FileVersion -eq '6.1.7601.18606')
{ Write-host "File for is $TRUE" }
Else
{ Write-Host $False }
** Revised **
1.) 'IF' condition statement should be applied to the property before $obj is created.
2.) To make the function more flexible and reusable, you should expand on the parameter criteria to allow for values from pipeline and and array of values as input.
function fileversion {
param(
[Parameter(Mandatory=$True,
ValueFromPipeline=$True,
ValueFromPipelineByPropertyName=$True)]
[Alias('files')]
[string[]]$filesToCheck
)
$fileInput = Get-Item $filesToCheck
foreach($f in $fileInput) {
$properties = #{
'File'=Split-Path $f -Leaf;
'FileVersion'=$f.VersionInfo.ProductVersion;
'CorrectVersion'=IF($versionCheck -eq '6.1.7601.18606') {$TRUE } ELSE {$False };
'IsReadyOnly'=$f.IsReadOnly;
'DirectoryOfFile'=$f.DirectoryName
}
$obj = New-Object psobject -Property $properties
Write-Output $obj
}
}

How can you test if an object has a specific property?

How can you test if an object has a specific property?
Appreciate I can do ...
$members = Get-Member -InputObject $myobject
and then foreach through the $members, but is there a function to test if the object has a specific property?
Additional Info:
The issue is I'm importing two different sorts of CSV file, one with two columns, the other with three. I couldn't get the check to work with "Property", only with "NoteProperty" ... whatever the difference is
if ( ($member.MemberType -eq "NoteProperty" ) -and ($member.Name -eq $propertyName) )
Like this?
[bool]($myObject.PSobject.Properties.name -match "myPropertyNameToTest")
You can use Get-Member
if (Get-Member -inputobject $var -name "Property" -Membertype Properties) {
#Property exists
}
This is succinct and readable:
"MyProperty" -in $MyObject.PSobject.Properties.Name
We can put it in a function:
function HasProperty($object, $propertyName)
{
$propertyName -in $object.PSobject.Properties.Name
}
For me MyProperty" -in $MyObject.PSobject.Properties.Name didn't work, however
$MyObject.PSobject.Properties.Name.Contains("MyProperty")
works
There are a number of solutions to this question that work in strict mode, but some are better than others.
Solutions that do not appear to iterate through every property are the fastest solutions.
Bernie White's solution and
esskar's solution (modified)
Solutions that look as though they iterate through every property are slower.
sebke CCU's solution and
dan-gph's solution
The solution that appears to iterate through every property and uses a regular expression is a little slower than the previous two solutions (because compiling and executing the regular expression takes more time)
CB.'s solution
The solution that uses GetMethod appears to iterate through every property, but its use of GetMethod makes it significantly slower.
Paul's GetMethod solution
The following script was used to compare the previously mentioned solutions in strict mode:
# Tested in PowerShell core 7.2.0
Set-StrictMode -Version Latest
$propertyExistsMethods = New-Object System.Collections.Generic.Dictionary'[string,scriptblock]'
# Fastest
$propertyExistsMethods.Add(
"PSObject.Properties (Bernie White's solution)",
{
Param( [PSObject] $Object, [string] $Property )
[bool]$Object.PSObject.Properties[$Property]
})
$propertyExistsMethods.Add(
"PSObject.Properties.Item (esskar's solution (modified))",
{
Param( [PSObject] $Object, [string] $Property )
[bool]$Object.PSObject.Properties.Item($property)
})
# Not as fast
$propertyExistsMethods.Add(
"Contains (sebke CCU's solution)",
{
Param( [PSObject] $Object, [string] $Property )
$Object.PSobject.Properties.Name.Contains($Property)
})
$propertyExistsMethods.Add(
"-in (dan-gph's solution)",
{
Param( [PSObject] $Object, [string] $Property )
$Property -in $Object.PSobject.Properties.Name
})
# Slower than the previously mentioned solutions
$propertyExistsMethods.Add(
"-match (CB.'s solution)",
{
Param( [PSObject] $Object, [string] $Property )
[bool]($Object.PSobject.Properties.name -match $Property)
})
# Slowest
$propertyExistsMethods.Add(
"GetMember (Paul's solution)",
{
Param( [PSObject] $Object, [string] $Property )
Get-Member -inputobject $Object -name $Property -Membertype Properties
})
foreach ($method in $propertyExistsMethods.Keys) {
$propertyExists = $propertyExistsMethods[$method]
$o = #{}
foreach ($i in 1..100000) {
$o[$i] = "p$i"
}
Write-Host $method
$measure = Measure-Command {
foreach ($i in 1..100000) {
# Always check for a property that does NOT exist
& $propertyExists -Object $o -Property 'p'
}
}
Write-Host $measure | % { $_.Milliseconds }
Write-Host ''
}
The output is as follows:
PSObject.Properties (Bernie White's solution)
00:00:03.1437587
PSObject.Properties.Item (esskar's solution)
00:00:03.5833642
Contains (sebke CCU's solution)
00:00:04.4812702
-in (dan-gph's solution)
00:00:04.6507811
-match (CB.'s solution)
00:00:05.1107066
GetMember (Paul's solution)
00:00:14.5305115
Try this for a one liner that is strict safe.
[bool]$myobject.PSObject.Properties[$propertyName]
For example:
Set-StrictMode -Version latest;
$propertyName = 'Property1';
$myobject = [PSCustomObject]#{ Property0 = 'Value0' };
if ([bool]$myobject.PSObject.Properties[$propertyName]) {
$value = $myobject.$propertyName;
}
I've been using the following which returns the property value, as it would be accessed via $thing.$prop, if the "property" would be to exist and not throw a random exception. If the property "doesn't exist" (or has a null value) then $null is returned: this approach functions in/is useful for strict mode, because, well, Gonna Catch 'em All.
I find this approach useful because it allows PS Custom Objects, normal .NET objects, PS HashTables, and .NET collections like Dictionary to be treated as "duck-typed equivalent", which I find is a fairly good fit for PowerShell.
Of course, this does not meet the strict definition of "has a property".. which this question may be explicitly limited to. If accepting the larger definition of "property" assumed here, the method can be trivially modified to return a boolean.
Function Get-PropOrNull {
param($thing, [string]$prop)
Try {
$thing.$prop
} Catch {
}
}
Examples:
Get-PropOrNull (Get-Date) "Date" # => Monday, February 05, 2018 12:00:00 AM
Get-PropOrNull (Get-Date) "flub" # => $null
Get-PropOrNull (#{x="HashTable"}) "x" # => "HashTable"
Get-PropOrNull ([PSCustomObject]#{x="Custom"}) "x" # => "Custom"
$oldDict = New-Object "System.Collections.HashTable"
$oldDict["x"] = "OldDict"
Get-PropOrNull $d "x" # => "OldDict"
And, this behavior might not [always] be desired.. ie. it's not possible to distinguish between x.Count and x["Count"].
Just check against null.
($myObject.MyProperty -ne $null)
If you have not set PowerShell to StrictMode, this works even if the property does not exist:
$obj = New-Object PSObject;
Add-Member -InputObject $obj -MemberType NoteProperty -Name Foo -Value "Bar";
$obj.Foo; # Bar
($obj.MyProperty -ne $null); # False, no exception
If you are using StrictMode and the psobject might be empty, it will give you an error.
For all purposes this will do:
if (($json.PSobject.Properties | Foreach {$_.Name}) -contains $variable)
I find this method more strict and faster when checking multiple properties
$null -ne $myobject.PSObject.Properties.Item("myPropertyNameToTest")
Real similar to a javascript check:
foreach($member in $members)
{
if($member.PropertyName)
{
Write $member.PropertyName
}
else
{
Write "Nope!"
}
}
Just to clarify
given the following object
$Object
With the following properties
type : message
user : john.doe#company.com
text :
ts : 11/21/2016 8:59:30 PM
The following are true
$Object.text -eq $NULL
$Object.NotPresent -eq $NULL
-not $Object.text
-not $Object.NotPresent
So the earlier answers that explicitly check for the property by name is the most correct way to verify that that property is not present.
I ended up with the following function ...
function HasNoteProperty(
[object]$testObject,
[string]$propertyName
)
{
$members = Get-Member -InputObject $testObject
if ($members -ne $null -and $members.count -gt 0)
{
foreach($member in $members)
{
if ( ($member.MemberType -eq "NoteProperty" ) -and `
($member.Name -eq $propertyName) )
{
return $true
}
}
return $false
}
else
{
return $false;
}
}
I recently switch to set strict-mode -version 2.0 and my null tests failed.
I added a function:
#use in strict mode to validate property exists before using
function exists {
param($obj,$prop)
try {
if ($null -ne $obj[$prop]) {return $true}
return $false
} catch {
return $false
}
return $false
}
Now I code
if (exists $run main) { ...
rather than
if ($run.main -ne $null) { ...
and we are on our way. Seems to work on objects and hashtables
As an unintended benefit it is less typing.
for me this work
Set-StrictMode -Version Latest
$TMP = ...
$HAS_SERVERS=($TMP | Select-Object Servers)
if (-not $HAS_SERVERS.Servers){
echo "No servers. Abort."
} else {
...
}
I just started using PowerShell with PowerShell Core 6.0 (beta) and following simply works:
if ($members.NoteProperty) {
# NoteProperty exist
}
or
if (-not $members.NoteProperty) {
# NoteProperty does not exist
}
You could check with:
($Member.PropertyNames -contains "Name") this will check for the Named property
For identifying which of the objects in an array have a property
$HasProperty = $ArrayOfObjects | Where-Object {$_.MyProperty}

Getting all Named Parameters from Powershell including empty and set ones

I'm trying to find a way to get all parameter information from a powershell script. Ex script:
function test()
{
Param(
[string]$foo,
[string]$bar,
[string]$baz = "baz"
)
foreach ($key in $MyInvocation.BoundParameters.keys)
{
write-host "Parameter: $($key) -> $($MyInvocation.BoundParameters[$key])"
}
}
test -foo "foo!"
I'd like to get the values of $bar and $baz in a dynamic way without knowing the names of the parameters ahead of time.
I've looked through $MyInvocation properties and methods but I don't see anything besides parameters that are set/passed.
Update 1:
I'm close to getting it with:
function test()
{
Param(
[string]$foo,
[string]$bar,
[string]$baz = "baz"
)
foreach($var in (get-variable -scope private))
{
write-host "$($var.name) -> $($var.value)"
}
}
test -foo "foo!"
If i could filter out the script parameters vs the default parameters I would be good to go.
Update 2:
The final working solution looks like this:
function test {
param (
[string] $Bar = 'test'
, [string] $Baz
, [string] $Asdf
)
$ParameterList = (Get-Command -Name $MyInvocation.InvocationName).Parameters;
foreach ($key in $ParameterList.keys)
{
$var = Get-Variable -Name $key -ErrorAction SilentlyContinue;
if($var)
{
write-host "$($var.name) > $($var.value)"
}
}
}
test -asdf blah;
Check this solution out. This uses the CmdletBinding() attribute, which provides some additional metadata through the use of the $PSCmdlet built-in variable. You can:
Dynamically retrieve the command's name, using $PSCmdlet
Get a list of the parameter for the command, using Get-Command
Examine the value of each parameter, using the Get-Variable cmdlet
Code:
function test {
[CmdletBinding()]
param (
[string] $Bar = 'test'
, [string] $Baz
, [string] $Asdf
)
# Get the command name
$CommandName = $PSCmdlet.MyInvocation.InvocationName;
# Get the list of parameters for the command
$ParameterList = (Get-Command -Name $CommandName).Parameters;
# Grab each parameter value, using Get-Variable
foreach ($Parameter in $ParameterList) {
Get-Variable -Name $Parameter.Values.Name -ErrorAction SilentlyContinue;
#Get-Variable -Name $ParameterList;
}
}
test -asdf blah;
Output
The output from the command looks like this:
Name Value
---- -----
Bar test
Baz
Asdf blah
To read the value dynamically use the get-variable function / cmdlet
write-host (get-variable "foo")
To print out all of the parameters do the following
foreach ($key in $MyInvocation.BoundParameters.keys)
{
$value = (get-variable $key).Value
write-host "$key -> $value"
}
Hopefully, some may find this one-liner useful:
function test()
{
Param(
[string]$foo,
[string]$bar,
[string]$baz = "baz"
)
$MyInvocation.MyCommand.Parameters | Format-Table -AutoSize #{ Label = "Key"; Expression={$_.Key}; }, #{ Label = "Value"; Expression={(Get-Variable -Name $_.Key -EA SilentlyContinue).Value}; }
}
test -foo "foo!"
Result
Keys Value
---- -----
foo foo!
bar
baz baz
I found this most useful for PS4 (Windows 2012 R2) - it includes default values / optional parameters:
$cmdName = $MyInvocation.InvocationName
$paramList = (Get-Command -Name $cmdName).Parameters
foreach ( $key in $paramList.Keys ) {
$value = (Get-Variable $key -ErrorAction SilentlyContinue).Value
if ( $value -or $value -eq 0 ) {
Write-Host "$key -> $value"
}
}
For those of you who do not want to use cmdletbinding() here's a variation on the one liner I found above:
(Get-Command -Name $PSCommandPath).Parameters | Format-Table -AutoSize #{ Label = "Key"; Expression={$_.Key}; }, #{ Label = "Value"; Expression={(Get-Variable -Name $_.Key -EA SilentlyContinue).Value}; }
$PSCommandPath is always available
I played with the 2 solutions i liked in this thread, they both work.
however I needed to produce an error out on missing parameter for a build script
$cmdName = $MyInvocation.InvocationName
$paramList = (Get-Command -Name $cmdName).Parameters
foreach ( $key in $paramList.Keys ) {
$value = (Get-Variable $key -ErrorAction Stop)
#Write-Host $value.Value #remove comment for error checking
if ([string]::IsNullOrEmpty($value.Value)){
$(throw ("$key is a mandatory value please declare with -$key <Required value> " ))
}
}
What if I don't know what arguments or how many will be passed? For example, the function could be called with:
test -foo "value1" -bar "value2" -baz "value3"
and someone else might call it with:
test -variable1 "somevalue" -variable2 "somevalue2" -variable3 "somevalue3" -variable4 "value4"
I looked at $MyInvocation and the arguments come across as UnboundArguments, so theoretically I could "pair up" argument 0 with argument 1, 2 with 3, etc and error out if there's an odd number of UnboundArguments.
Stumbled upon this trying to do something similar and figured out my preferred option.
Function ParamTest {
Param (
$t1 = '1234',
$t2,
[switch]$3
)
$MyInvocation |Add-Member -Name:'Param' -MemberType:'NoteProperty' -Value:(
(Get-Variable -Scope:'Local' -Include:#($MyInvocation.MyCommand.Parameters.keys)|
ForEach-Object -begin:{$h=#{}} -process:{$h.add($_.Name,$_.Value)} -end:{$h}
))
$MyInvocation.Param
}
Result
Name Value
---- -----
t1 1234
3 False
t2
PSVersionTable
Name Value
---- -----
PSVersion 5.1.19041.1320
PSEdition Desktop
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 10.0.19041.1320
CLRVersion 4.0.30319.42000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
A streamlined solution that:
builds on your own approach now shown at the bottom of the question,
while also including an additional piece of information, namely whether each parameter was bound on invocation, i.e. whether an argument was passed or not, based on its presence in the automatic $PSBoundParameter variable, which is a dictionary containing the bound parameters and their values (arguments).
Note: $PSBoundParameters does not include parameters bound by a default value, only those parameters to which an argument was explicitly passed; for your use case, that is arguably desirable in order to distinguish between a default value and an explicit one, but in scenarios where arguments should be passed through it may be desirable to have default-value parameters included - see GitHub issue #3285.
function test {
param (
[string]$foo,
[string]$bar,
[string]$baz = "baz"
)
foreach ($paramName in $MyInvocation.MyCommand.Parameters.Keys) {
$bound = $PSBoundParameters.ContainsKey($paramName)
[pscustomobject] #{
ParameterName = $paramName
ParameterValue = if ($bound) { $PSBoundParameters[$paramName] }
else { Get-Variable -Scope Local -ErrorAction Ignore -ValueOnly $paramName }
Bound = $bound
}
}
}
test -foo "foo!"
The above yields the following:
ParameterName ParameterValue Bound
------------- -------------- -----
foo foo! True
bar False
baz baz False
Note: This solution also handles dynamic parameters correctly:
Such parameters are reflected in $MyInvocation.MyCommand.Parameters if they situationally apply, but - unlike regular static parameters - are never reflected in scope-local variables. If they apply and are also bound, they and their values are reflected in $PSBoundParameters.
Thus - given that an applicable dynamic parameter may not be bound - the Get-Variable call to look for scope-local variables representing static parameters:
Must explicitly be limited to the current scope with -Scope Local so as not to accidentally pick up unrelated variables of the same name from ancestral scopes for unbound dynamic parameters.
Must ignore errors from the potentially resulting failure to find a variable in the current scope, using -ErrorAction Ignore.
I wanted a compact string of parameter key/value pairs that I can write out when catching error. Use content of the other answers, I came up with this:
$parameters = (Get-Variable -Scope:'Local' -Include:#($MyInvocation.MyCommand.Parameters.keys) |
Select-Object Name, Value | ForEach-Object { "$($_.Name) : $($_.Value)" }) -join ' | '
Sample output:
ComputerName : SomePC | Directory : C:\Tools\LogExpert | UserName : Hans Wurst | YesOrNo : False
Thanks to all other posters with great & helpful answers above!
I'm cobbling together some of the above posts to try to meet my requirements.
I'd like to show Params in a Powershell-language-compatible format, so that one could easily see & display these params, and then also copy+pasta back into either a script as Param() Defaults (ParamsFormat1) or into a commandline/function call (CommandFormat2). I struggled with this for a while, so I hope this saves someone some time.
Collection of various Scripts from above:
# Note: there are some subtle differences between these versions below
# Specifically in the first line for *which* object you're checking for Parameters, and subsequently what type of Parameters you're looking at.
# PSCmdlet version REQUIRES [CmdletBinding()] on your function!!!
# $($(Get-Command -Name $($PSCmdlet.MyInvocation.InvocationName)).Parameters) | `
# %{ Get-Variable -Name $_.Values.Name -ErrorAction SilentlyContinue; }
# -Scope:'Local' FILTERS out any global params
# (Get-Variable -Scope:'Local' -Include:#($MyInvocation.MyCommand.Parameters.keys)) | ft
# PSCommandPath supposedly available/usable "in all situations" - so this may be the most useful of all of them, BUT it shows global params [Debug, ErrorAction, etc...]
# (Get-Command -Name $PSCommandPath).Parameters | `
# %{ Get-Variable -Name $_.Values.Name -ErrorAction SilentlyContinue; }
ParamsFormat1 / HYBRID VERSION: To output "Default Params" as you would see them in a function definition. Combines the -Scope:"Local" and "always available" versions to get BOTH param TYPES as well as Name, Value
Write-Host "Params("
# TBD Figure out how to expand #{} of System.Collections.Hashtable
# HYBRID VERSION: Combine the -Scope:"Local" and "always available" versions to get BOTH param TYPES as well as Name, Value
# If you remove LocalList Filter here, it will also show "Global" function properties like Debug, ErrorAction, etc...
$LocalList = $(Get-Variable -Scope:'Local' -Include:#($MyInvocation.MyCommand.Parameters.keys) | Select-Object -ExpandProperty Name) -join "|"
# VERSION: Wrapper script with DEFAULTS : normally DOES include Global vars [Debug, ErrorAction, etc]. but DOES preserve param order as-written.
((Get-Command -Name $PSCommandPath).Parameters | `
Select -ExpandProperty Values | `
Where-Object { $_.Name -Match $LocalList } | `
Format-Table -HideTableHeaders -AutoSize `
#{ Label="Type"; Expression={"[$($_.ParameterType )]"}; }, `
#{ Label="Name"; Expression={"`t`$$($_.Name)"}; }, `
#{ Label="Equals"; Expression={"="}; }, `
#{ Label="Value"; Expression={ If( $_.ParameterType -Match "String" ) { "`"$((Get-Variable -Name $_.Name -EA SilentlyContinue).Value)`"" } Else{ $((Get-Variable -Name $_.Name -EA SilentlyContinue).Value)}; }; }, `
#{ Label="RowEndComma"; Expression={ "," }; }
##{ Label="Value"; Expression={ $((Get-Variable -Name $_.Name -EA SilentlyContinue).Value) }; } # (OPTIONAL) Values only, no wrapping quotes
)
Write-Host ")";
CommandFormat2 / SIMPLER VERSION: Call with CommandLine Args (TYPES not needed). This filters out Global vars, does NOT preserve param ordering. (sorted alphabetically?)
# VERSION: Call with CommandLine Args (TYPES not available - TBD needs more work to display String, Array, and Hashmap/PsCustomObject ["", #() and {}] types better): filters out Global vars, does NOT preserve param ordering.
# If needed, cut+paste OPTIONAL lines down above Format-Table line.
# Where-Object { $_.Value } | ` # (Optional) remove any Null items.
# Sort-Object -Property Name | ` # (Optional) sort output
(Get-Variable -Scope:'Local' -Include:#($MyInvocation.MyCommand.Parameters.keys) | `
Format-Table -HideTableHeaders -AutoSize `
#{ Label="Name"; Expression={"`t-$($_.Name)"}; }, `
#{ Label="Equals"; Expression ={":"}; }, `
#{ Label="Value"; Expression={ If( $_.ParameterType -NotMatch "String" ) { $_.Value; } Else {"`"$($_.Value)`"";} }; Alignment="left"; }
)

PowerShell cmdlet parameter value tab completion

How do you implement the parameter tab completion for PowerShell functions or cmdlets like Get-Service and Get-Process in PowerShell 3.0?
I realise ValidateSet works for a known list, but I want to generate the list on demand.
Adam Driscoll hints that it is possible for cmdlets but unfortunately hasn't elaborated.
Trevor Sullivan shows a technique for functions, but as I understand it, his code only generates the list at the time the function is defined.
I puzzled over this for a while, because I wanted to do the same thing. I put together something that I'm really happy with.
You can add ValidateSet attributes from a DynamicParam. Here's an example of where I've generated my ValidateSet on-the-fly from an xml file. See the "ValidateSetAttribute" in the following code:
function Foo() {
[CmdletBinding()]
Param ()
DynamicParam {
#
# The "modules" param
#
$modulesAttributeCollection = new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
# [parameter(mandatory=...,
# ...
# )]
$modulesParameterAttribute = new-object System.Management.Automation.ParameterAttribute
$modulesParameterAttribute.Mandatory = $true
$modulesParameterAttribute.HelpMessage = "Enter one or more module names, separated by commas"
$modulesAttributeCollection.Add($modulesParameterAttribute)
# [ValidateSet[(...)]
$moduleNames = #()
foreach($moduleXmlInfo in Select-Xml -Path "C:\Path\to\my\xmlFile.xml" -XPath "//enlistment[#name=""wp""]/module") {
$moduleNames += $moduleXmlInfo.Node.Attributes["name"].Value
}
$modulesValidateSetAttribute = New-Object -type System.Management.Automation.ValidateSetAttribute($moduleNames)
$modulesAttributeCollection.Add($modulesValidateSetAttribute)
# Remaining boilerplate
$modulesRuntimeDefinedParam = new-object -Type System.Management.Automation.RuntimeDefinedParameter("modules", [String[]], $modulesAttributeCollection)
$paramDictionary = new-object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
$paramDictionary.Add("modules", $modulesRuntimeDefinedParam)
return $paramDictionary
}
process {
# Do stuff
}
}
With that, I can type
Foo -modules M<press tab>
and it will tab-complete "MarcusModule" if that module was in the XML file. Furthermore, I can edit the XML file and the tab-completion behavior will immediately change; you don't have to re-import the function.
Check the TabExpansionPlusPlus module on github, written by a former PowerShell team magician.
https://github.com/lzybkr/TabExpansionPlusPlus#readme
Classically, I used regex.
for example,
function TabExpansion {
param($line, $lastWord)
if ( $line -match '(-(\w+))\s+([^-]*$)' )
{
### Resolve Command name & parameter name
$_param = $matches[2] + '*'
$_opt = $Matches[3].Split(" ,")[-1] + '*'
$_base = $Matches[3].Substring(0,$Matches[3].Length-$Matches[3].Split(" ,")[-1].length)
$_cmdlet = [regex]::Split($line, '[|;=]')[-1]
if ($_cmdlet -match '\{([^\{\}]*)$')
{
$_cmdlet = $matches[1]
}
if ($_cmdlet -match '\(([^()]*)$')
{
$_cmdlet = $matches[1]
}
$_cmdlet = $_cmdlet.Trim().Split()[0]
$_cmdlet = #(Get-Command -type 'Cmdlet,Alias,Function,Filter,ExternalScript' $_cmdlet)[0]
while ($_cmdlet.CommandType -eq 'alias')
{
$_cmdlet = #(Get-Command -type 'Cmdlet,Alias,Function,Filter,ExternalScript' $_cmdlet.Definition)[0]
}
### Currently target is Get-Alias & "-Name" parameter
if ( "Get-Alias" -eq $_cmdlet.Name -and "Name" -like $_param )
{
Get-Alias -Name $_opt | % { $_.Name } | sort | % { $_base + ($_ -replace '\s','` ') }
break;
}
}
}
Reference
http://gallery.technet.microsoft.com/scriptcenter/005d8bc7-5163-4a25-ad0d-25cffa90faf5
Posh-git renames TabExpansion to TabExpansionBackup in GitTabExpansion.ps1.
And posh-git's redifined TabExpansion calls original TabExpansion(TabExpansionBackup) when completions don't match with git commands.
So all you have to do is redefine TabExpansionBackup.
(cat .\GitTabExpansion.ps1 | select -last 18)
============================== GitTabExpansion.ps1 ==============================
if (Test-Path Function:\TabExpansion) {
Rename-Item Function:\TabExpansion TabExpansionBackup
}
function TabExpansion($line, $lastWord) {
$lastBlock = [regex]::Split($line, '[|;]')[-1].TrimStart()
switch -regex ($lastBlock) {
# Execute git tab completion for all git-related commands
"^$(Get-AliasPattern git) (.*)" { GitTabExpansion $lastBlock }
"^$(Get-AliasPattern tgit) (.*)" { GitTabExpansion $lastBlock }
# Fall back on existing tab expansion
default { if (Test-Path Function:\TabExpansionBackup) { TabExpansionBackup $line $lastWord } }
}
}
===============================================================================
Redefine TabExpansionBackup(original TabExpansion)
function TabExpansionBackup {
...
### Resolve Command name & parameter name
...
### Currently target is Get-Alias & "-Name" parameter
...
}