ValueFromPipelineByPropertyName can I shift function next positional parameters? - powershell

Considering to functions designed to use values by property name, the second function have his first argument passed by pipeline. Can I use positional parameter for this second function?
Example:
function Get-Customer {
[CmdletBinding()]
Param(
[string]$CustomerName = "*"
)
Process {
# real process iterate on fodlers.
New-Object PSObject -Property #{
CustomerName = $CustomerName;
}
}
}
function Get-Device {
[CmdletBinding()]
Param(
[Parameter(ValueFromPipelineByPropertyName)]
[string]$CustomerName = "*",
[string]$DeviceName = "*"
)
Process {
# real process iterate on fodlers.
New-Object PSObject -Property #{
CustomerName=$CustomerName;
DeviceName=$DeviceName
}
}
}
You can use it like:
Get-Customer "John" | Get-Device
Get-Customer "John" | Get-Device -DeviceName "Device 1"
But can you do this (actually with provided code it doesn't work)?
Get-Customer "John" | Get-Device "Device 1"

You need to define $DeviceName as the first positional parameter for that to work:
function Get-Device {
[CmdletBinding()]
Param(
[Parameter(ValueFromPipelineByPropertyName)]
[string]$CustomerName = "*",
[Parameter(Position=0)]
[string]$DeviceName = "*"
)
Process {
# real process iterate on fodlers.
New-Object PSObject -Property #{
CustomerName = $CustomerName;
DeviceName = $DeviceName
}
}
}

Related

Convert default parameter value of string to type array

Suppose you have the following function:
Function Test-Function {
Param (
[String[]]$ComputerNames = #($env:COMPUTERNAME, 'PC2'),
[String]$PaperSize = 'A4'
)
}
Get-DefaultParameterValuesHC -Path 'Test-Function'
Now to get the default values in the function arguments one can use AST:
Function Get-DefaultParameterValuesHC {
[OutputType([hashtable])]
Param (
[Parameter(Mandatory)]$Path
)
$ast = (Get-Command $Path).ScriptBlock.Ast
$selectParams = #{
Property = #{
Name = 'Name';
Expression = { $_.Name.VariablePath.UserPath }
},
#{
Name = 'Value';
Expression = { $_.DefaultValue.Extent.Text -replace "`"|'" }
}
}
$result = #{ }
$defaultValueParameters = #($ast.FindAll( {
$args[0] -is [System.Management.Automation.Language.ParameterAst] }
, $true) |
Where-Object { $_.DefaultValue } |
Select-Object #selectParams)
foreach ($d in $defaultValueParameters) {
$result[$d.Name] = foreach ($value in $d.Value) {
$ExecutionContext.InvokeCommand.ExpandString($value)
}
}
$result
}
The issue here is that the argument for $ComputerNames is read as a string while it is actually an array of string.
Is there a way that PowerShell can covnert a string to an array? Or even better, read the value correctly in the first place?
You need to look deeper into the AST structure...
I recommend you to play around with this PowerShell: AST Explorer GUI:
For your specific example:
Function Test-Function {
Param (
[String[]]$ComputerNames = #($env:COMPUTERNAME, 'PC2'),
[String]$PaperSize = 'A4'
)
}
$FunctionDefinitionAst = (Get-Command 'Test-Function').ScriptBlock.Ast
$Body = $FunctionDefinitionAst.Body
$ParamBlock = $Body.ParamBlock
$CNParameter = $ParamBlock.Parameters | Where-Object { $_.Name.VariablePath.UserPath -eq 'ComputerNames' }
$DefaultValue = $CNParameter.DefaultValue
$DefaultValue.SubExpression.Statements.PipelineElements.Expression.Elements
VariablePath : env:COMPUTERNAME
Splatted : False
StaticType : System.Object
Extent : $env:COMPUTERNAME
Parent : $env:COMPUTERNAME, 'PC2'
StringConstantType : SingleQuoted
Value : PC2
StaticType : System.String
Extent : 'PC2'
Parent : $env:COMPUTERNAME, 'PC2'
It's a bit of a hackish solution but this is what I came up with to solve the issue of not returning an array of string:
Function Get-DefaultParameterValuesHC {
Param (
[Parameter(Mandatory)]$Path
)
$ast = (Get-Command $Path).ScriptBlock.Ast
$selectParams = #{
Property = #{
Name = 'Name';
Expression = { $_.Name.VariablePath.UserPath }
},
#{
Name = 'Value';
Expression = {
if ($_.DefaultValue.StaticType.BaseType.Name -eq 'Array') {
$_.DefaultValue.SubExpression.Extent.Text -split ',' |
ForEach-Object { $_.trim() -replace "`"|'" }
}
else {
$_.DefaultValue.Extent.Text -replace "`"|'"
}
}
}
}
$result = #{ }
$defaultValueParameters = #($ast.FindAll( {
$args[0] -is [System.Management.Automation.Language.ParameterAst] }
, $true) |
Where-Object { $_.DefaultValue } |
Select-Object #selectParams)
foreach ($d in $defaultValueParameters) {
$result[$d.Name] = foreach ($value in $d.Value) {
$ExecutionContext.InvokeCommand.ExpandString($value)
}
}
$result
}
ExpandPath will only expand variables inside strings. To get the actual values (and not just the definition) you could use Invoke-Expression:
function Get-DefaultParameterValuesHC {
[OutputType([hashtable])]
Param (
[Parameter(Mandatory)]$Path
)
$result = #{ }
(Get-Command $Path).ScriptBlock.Ast.Body.ParamBlock.Parameters | where {$_.DefaultValue} | foreach {
$result[$_.Name.VariablePath.UserPath] = Invoke-Expression $_.DefaultValue.Extent.Text
}
$result
}
NOTE: This will actually invoke the default declaration, so any logic inside that expression will be run, just as when running the function. For example, a default value of $Parameter = (Get-Date) will always invoke Get-Date.
It would be preferable to create a function, that only returns the default declarations, and let the user decide to invoke the expression or not:
function Get-DefaultParameterDeclarations {
Param (
[Parameter(Mandatory, Position = 0)]
[string]$CommandName
)
(Get-Command $CommandName).ScriptBlock.Ast.Body.ParamBlock.Parameters | where {$_.DefaultValue} |
foreach {
[PSCustomObject]#{
Name = $_.Name.VariablePath.UserPath
Expression = $_.DefaultValue.Extent.Text
}
}
}
# get the declarations and (optionally) invoke the expressions:
Get-DefaultParameterDeclarations 'Test-Function' |
select Name, #{n="DefaultValue"; e={Invoke-Expression $_.Expression}}

How to work with System.Manangement.Automation.Language... ast

So I'm trying to create a custom PsAnalyzer rule for the office, and it's my first time using $ast based commands/variables. As a result I'm at a bit of a loss.
I've been using a couple of sites to get my head around the [System.Management.Automation.Language] object class, namely this, this, and this.
For testing purposes I'm using the function below - dodgy looking params intended.
Function IE {
[cmdletbinding()]
Param(
$Url,
$IEWIDTH = 550,
$IeHeight = 450
)
$IE = new-object -comobject InternetExplorer.Application
$IE.top = 200 ; $IE.width = $IEWIDTH ; $IE.height = $IEHEIGHT
$IE.Left = 100 ; $IE.AddressBar = $FALSE ; $IE.visible = $TRUE
$IE.navigate( "file:///$Url" )
}
Then with the code below, I'd expect $IEWIDTH as the only param to fail.
Function Measure-PascalCase {
<#
.SYNOPSIS
The variables names should be in PascalCase.
.DESCRIPTION
Variable names should use a consistent capitalization style, i.e. : PascalCase.
.EXAMPLE
Measure-PascalCase -ScriptBlockAst $ScriptBlockAst
.INPUTS
[System.Management.Automation.Language.ScriptBlockAst]
.OUTPUTS
[Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]
.NOTES
https://msdn.microsoft.com/en-us/library/dd878270(v=vs.85).aspx
https://msdn.microsoft.com/en-us/library/ms229043(v=vs.110).aspx
https://mathieubuisson.github.io/create-custom-rule-psscriptanalyzer/
#>
[CmdletBinding()]
[OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])]
Param
(
[Parameter(Mandatory = $True)]
[ValidateNotNullOrEmpty()]
[System.Management.Automation.Language.ScriptBlockAst]
$ScriptBlockAst
)
Process {
$Results = #()
try {
#region Define predicates to find ASTs.
[ScriptBlock]$Predicate = {
Param ([System.Management.Automation.Language.Ast]$Ast)
[bool]$ReturnValue = $false
if ( $Ast -is [System.Management.Automation.Language.ParameterAst] ){
[System.Management.Automation.Language.ParameterAst]$VariableAst = $Ast
if ( $VariableAst.Left.VariablePath.UserPath -eq 'i' ){
$ReturnValue = $false
} elseif ( $VariableAst.Left.VariablePath.UserPath.Length -eq 3 ){
$ReturnValue = $false
} elseif ($VariableAst.Left.VariablePath.UserPath -cnotmatch '^([A-Z][a-z]+)+$') {
$ReturnValue = $True
}
}
return $ReturnValue
}
#endregion
#region Finds ASTs that match the predicates.
[System.Management.Automation.Language.Ast[]]$Violations = $ScriptBlockAst.FindAll($Predicate, $True)
If ($Violations.Count -ne 0) {
Foreach ($Violation in $Violations) {
$Result = New-Object `
-Typename "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord" `
-ArgumentList "$((Get-Help $MyInvocation.MyCommand.Name).Description.Text)",$Violation.Extent,$PSCmdlet.MyInvocation.InvocationName,Information,$Null
$Results += $Result
}
}
return $Results
#endregion
}
catch {
$PSCmdlet.ThrowTerminatingError($_)
}
}
}
Export-ModuleMember -Function Measure-*
Instead I get:
Line Extent Message
---- ------ -------
6 $Url Variable names should use a consistent capitalization style, i.e. : PascalCase.
7 $IEWIDTH = 550 Variable names should use a consistent capitalization style, i.e. : PascalCase.
8 $IeHeight = 450 Variable names should use a consistent capitalization style, i.e. : PascalCase.
6 $Url Variable names should use a consistent capitalization style, i.e. : PascalCase.
7 $IEWIDTH = 550 Variable names should use a consistent capitalization style, i.e. : PascalCase.
8 $IeHeight = 450 Variable names should use a consistent capitalization style, i.e. : PascalCase.
Any ideas on what I'm doing wrong? I found another site here, if I use that method to test individual variables at a time, I get the results I'd expect to see.
To do that, add the following line at the end of the function IE,
[System.Management.Automation.Language.Parser]::ParseInput($MyInvocation.MyCommand.ScriptContents, [ref]$null, [ref]$null)
Then this code below will give you length numbers.
$stuffast = .\FunctionIe.ps1
$left = $Stuffast.FindAll({$args[0] -is [System.Management.Automation.Language.AssignmentStatementAst]},$true)
$left[0].Left.Extent.Text.Length
$left[0].Left.VariablePath.UserPath.Length
Unlike an AssignmentStatementAst, which has a Left and Right(-hand) value property, the ParameterAst has a Name and DefaultValue property, so you'll want to use Name in your predicate block:
$predicate = {
# ...
if ( $Ast -is [System.Management.Automation.Language.ParameterAst] ) {
[System.Management.Automation.Language.ParameterAst]$VariableAst = $Ast
if ( $VariableAst.Name.VariablePath.UserPath -eq 'i' ) {
$ReturnValue = $false
}
elseif ( $VariableAst.Name.VariablePath.UserPath.Length -eq 3 ) {
$ReturnValue = $false
}
elseif ($VariableAst.Name.VariablePath.UserPath -cnotmatch '^([A-Z][a-z]+)+$') {
$ReturnValue = $True
}
}
# ...
}
Alternatively, flip your predicate-logic around to search for VariableExpressionAst's where the parent node is one of AssignmentStatementAst or ParameterAst:
$predicate = {
param($ast)
$ValidParents = #(
[System.Management.Automation.Language.ParameterAst]
[System.Management.Automation.Language.AssignmentStatementAst]
)
if($ast -is [VariableExpressionAst] -and $ValidParents.Where({$ast -is $_}, 'First')){
[System.Management.Automation.Language.VariableExpressionAst]$variableAst = $ast
# inspect $variableAst.VariablePath here
}
return $false
}

How to implement `DynamicParam` through dynamic parameters?

Assumption:
$a = 1,2,3
if $a =1,2
Intent:Display parameter b
PS >test -<tab>
a
PS >test -a 1 -<tab>
b
PS >test -a 3 -<tab>
PS >test -a 3
How to achieve the following intent
Function test {
[CmdletBinding()]
Param()
DynamicParam {
parameter a
Dynamic parameter b (a is 1 =$true)
Dynamic parameter c (b is 3)
???
}
}
You don't have to assume. Research it.
What Dynamics Params are and how to use them if a fully documented use case via the MS docs site and many blog posts.
Searching for 'powershell dynamics parameters', will give you a solid list. Be sure to read the details, so as to limit any confusion about their use case.
Examples:
How can I pass dynamic parameters to powershell script and iterate
over the list?
Cmdlet dynamic parameters
PowerShell Deep Dive: Discovering dynamic parameters
Dynamic Parameters in PowerShell
How To Implement Dynamic Parameters in Your PowerShell Functions
enter link description here
# Example from the above link.
function Get-ConfigurationFile
{
[OutputType([System.IO.FileInfo])]
[CmdletBinding()]
param
()
DynamicParam
{
$ParamAttrib = New-Object System.Management.Automation.ParameterAttribute
$ParamAttrib.Mandatory = $true
$ParamAttrib.ParameterSetName = '__AllParameterSets'
$AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$AttribColl.Add($ParamAttrib)
$configurationFileNames = Get-ChildItem -Path 'C:\ConfigurationFiles' | Select-Object -ExpandProperty Name
$AttribColl.Add((New-Object System.Management.Automation.ValidateSetAttribute($configurationFileNames)))
$RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter('FileName', [string], $AttribColl)
$RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$RuntimeParamDic.Add('FileName', $RuntimeParam)
return $RuntimeParamDic
}
process
{
$configFileFolder = 'C:\ConfigurationFiles'
Get-ChildItem -Path $configFileFolder -Filter "$($PSBoundParameters.FileName).txt"
}
}
As for this...
As you can see,tabs cannot return a parameter
..., what you are showing is not the way this use case works.
Based on what you seem to be after, this sampler should get you there. please read the whole article for a better understanding.
Dynamic Parameters in PowerShell
Function Get-Order {
[CmdletBinding()]
Param(
[Parameter(
Mandatory=$true,
Position=1,
HelpMessage="How many cups would you like to purchase?"
)]
[int]$cups,
[Parameter(
Mandatory=$false,
Position=2,
HelpMessage="What would you like to purchase?"
)]
[ValidateSet("Lemonade","Water","Tea","Coffee")]
[string]$product="Lemonade"
)
Process {
$order = #()
for ($cup = 1; $cup -le $cups; $cup++) {
$order += "$($cup): A cup of $($product)"
}
$order
}
}
Or this one...
Using Dynamic Parameters
function Test-Department
{
[CmdletBinding()]
param
(
[Parameter(Mandatory=$true)]
[ValidateSet('Microsoft','Amazon','Google','Facebook')]
$Company
)
dynamicparam
{
# this hash table defines the departments available in each company
$data = #{
Microsoft = 'CEO', 'Marketing', 'Delivery'
Google = 'Marketing', 'Delivery'
Amazon = 'CEO', 'IT', 'Carpool'
Facebook = 'CEO', 'Facility', 'Carpool'
}
# check to see whether the user already chose a company
if ($Company)
{
# yes, so create a new dynamic parameter
$paramDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary
$attributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute]
# define the parameter attribute
$attribute = New-Object System.Management.Automation.ParameterAttribute
$attribute.Mandatory = $false
$attributeCollection.Add($attribute)
# create the appropriate ValidateSet attribute, listing the legal values for
# this dynamic parameter
$attribute = New-Object System.Management.Automation.ValidateSetAttribute($data.$Company)
$attributeCollection.Add($attribute)
# compose the dynamic -Department parameter
$Name = 'Department'
$dynParam = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter($Name,
[string], $attributeCollection)
$paramDictionary.Add($Name, $dynParam)
# return the collection of dynamic parameters
$paramDictionary
}
}
end
{
# take the dynamic parameters from $PSBoundParameters
$Department = $PSBoundParameters.Department
"Chosen department for $Company : $Department"
}
}
As well as this step by step one with full explanations...
How To: Add Dynamic Parameters to Your PowerShell Functions
As noted in my initial response, there are plenty of examples of this use case, but it requires study to fully grasp the concept, and how to use it/them.

How do I get named but undeclared parameters in a function in Powershell?

Here is an example of what I am trying to do.
Function Get-Parameters { Echo $SomeMagicMethod.Get("Name"); }
Get-Parameters -Name "John Doe"
$SomeMagicMethod is an automatic variable or any other method to get named undeclared parameters.
Is that possible in Powershell?
You'll have to parse the unbounded arguments yourself and find the argument that's right after whatever parameter name you're looking for.
I'd abstract it away in a separate function (you could also pass $args, but this is cleaner):
function Get-InvocationParameter
{
param(
[Parameter(Mandatory = $true, Position = 0)]
[System.Management.Automation.InvocationInfo]
$Invocation,
[Parameter(Mandatory = $true, Position = 1)]
[string]
$ParameterName
)
$Arguments = $Invocation.UnboundArguments
$ParamIndex = $Arguments.IndexOf("-$ParameterName")
if($ParamIndex -eq -1){
return
}
return $Arguments[$ParamIndex + 1]
}
Then use it like this:
function Get-Parameter
{
Get-InvocationParameter -Invocation $MyInvocation -ParameterName "Name"
}
And you should be able to see the arguments right after -Name (or nothing):
PS C:\> Get-Parameter -Name "John Doe"
John Doe
PS C:\> Get-Parameter -Name "John Doe","Jane Doe"
John Doe
Jane Doe
PS C:\> Get-Parameter -ID 123
PS C:\>
You can define a special parameter to catch all unbound arguments:
Function Get-Parameters {
Param(
[Parameter(Mandatory=$true)]
$SomeParam,
[Parameter(Mandatory=$false)]
$OtherParam = 'something',
...
[Parameter(Mandatory=$false, ValueFromRemainingArguments=$true)]
$OtherArgs
)
...
}
However, that will give you an array with the remaining arguments. There won't be an association between -Name and "John Doe".
If your function doesn't define any other parameters you could use the automatic variable $args to the same end.
If you want some kind of hashtable with the unbound "named" arguments you need to build that yourself, e.g. like this:
$UnboundNamed = #{}
$UnboundUnnamed = #()
$OtherArgs | ForEach-Object {
if ($_ -like '-*') {
$script:named = $_ -replace '^-'
$UnboundNamed[$script:named] = $null
} elseif ($script:named) {
$UnboundNamed[$script:named] = $_
$script:name = $null
} else {
$UnboundUnnamed += $_
}
}
If you truly want a function with no parameters, $args is the way to go. (Why you would want such a thing is a different question.) Anyway, code like the following will parse the $args array into a hashtable of parameter/argument pairs which you can use in the rest of the function body.
function NoParams
{
$myParams = #{}
switch ($args)
{
-Foo {
if (!$switch.MoveNext()) {throw "Missing argument for Foo"}
$myParams.Foo = $switch.Current
}
-Bar {
if (!$switch.MoveNext()) {throw "Missing argument for Bar"}
$myParams.Bar = $switch.Current
}
-Baz {
if (!$switch.MoveNext()) {throw "Missing argument for Baz"}
$myParams.Baz = $switch.Current
}
default { throw "Invalid parameter '$_'" }
}
$myParams
}

Windows "net group /domain" output filter

I need to grab members in particular AD group and add them into array. Using net group I can easily get the members of AD group.
However, I am not familier with the filter on Windows. I just want to get the user name from output.
Group name test
Comment
Members
---------------------------------------------------------------------
mike tom jackie
rick jason nick
The command completed successfully.
I can't use Get-ADGroupMember command using PowerShell. If there is a way to get a data and filter using PowerShell, it is also OK.
Well, the good news is that there is rarely only one way to do things in PowerShell. Here's part of a larger script I have on hand for some group related things where I don't always have the AD module available (such as on servers that other teams own):
$Identity = 'test'
$LDAP = "dc="+$env:USERDNSDOMAIN.Replace('.',',dc=')
$Filter = "(&(sAMAccountName=$Identity)(objectClass=group))"
$Searcher = [adsisearcher]$Filter
$Searcher.SearchRoot = "LDAP://$LDAP"
'Member','Description','groupType' | %{$Searcher.PropertiesToLoad.Add($_)|Out-Null}
$Results=$Searcher.FindAll()
$GroupTypeDef = #{
1='System'
2='Global'
4='Domain Local'
8='Universal'
16='APP_BASIC'
32='APP_QUERY'
-2147483648='Security'
}
If($Results.Count -gt 0){
$Group = New-Object PSObject #{
'DistinguishedName'=[string]$Results.Properties.Item('adspath') -replace "LDAP\:\/\/"
'Scope'=$GroupTypeDef.Keys|?{$_ -band ($($Results.properties.item('GroupType')))}|%{$GroupTypeDef.get_item($_)}
'Description'=[string]$Results.Properties.Item('description')
'Members'=[string[]]$Results.Properties.Item('member')|% -Begin {$Searcher.PropertiesToLoad.Clear();$Searcher.PropertiesToLoad.Add('objectClass')|Out-Null} {$Searcher.Filter = "(distinguishedName=$_)";[PSCustomObject][ordered]#{'MemberType'=$Searcher.FindAll().Properties.Item('objectClass').ToUpper()[-1];'DistinguishedName'=$_}}
}
$Group|Select DistinguishedName,Scope,Description
$Group.Members|FT -AutoSize
}
Else{"Unable to find group '$Group' in '$env:USERDNSDOMAIN'.`nPlease check that you can access that domain from your current domain, and that the group exists."}
Here's one way to get the direct members of an AD group without using the AD cmdlets:
param(
[Parameter(Mandatory)]
$GroupName
)
$ADS_ESCAPEDMODE_ON = 2
$ADS_SETTYPE_DN = 4
$ADS_FORMAT_X500 = 5
function Invoke-Method {
param(
[__ComObject]
$object,
[String]
$method,
$parameters
)
$output = $object.GetType().InvokeMember($method,"InvokeMethod",$null,$object,$parameters)
if ( $output ) { $output }
}
function Set-Property {
param(
[__ComObject]
$object,
[String]
$property,
$parameters
)
[Void] $object.GetType().InvokeMember($property,"SetProperty",$null,$object,$parameters)
}
$Pathname = New-Object -ComObject "Pathname"
Set-Property $Pathname "EscapedMode" $ADS_ESCAPEDMODE_ON
$Searcher = [ADSISearcher] "(&(objectClass=group)(name=$GroupName))"
$Searcher.PropertiesToLoad.AddRange(#("distinguishedName"))
$SearchResult = $searcher.FindOne()
if ( $SearchResult ) {
$GroupDN = $searchResult.Properties["distinguishedname"][0]
Invoke-Method $Pathname "Set" #($GroupDN,$ADS_SETTYPE_DN)
$Path = Invoke-Method $Pathname "Retrieve" $ADS_FORMAT_X500
$Group = [ADSI] $path
foreach ( $MemberDN in $Group.member ) {
Invoke-Method $Pathname "Set" #($MemberDN,$ADS_SETTYPE_DN)
$Path = Invoke-Method $Pathname "Retrieve" $ADS_FORMAT_X500
$Member = [ADSI] $Path
"" | Select-Object `
#{
Name="group_name"
Expression={$Group.name[0]}
},
#{
Name="member_objectClass"
Expression={$member.ObjectClass[$Member.ObjectClass.Count - 1]}
},
#{
Name="member_sAMAccountName";
Expression={$Member.sAMAccountName[0]}
}
}
}
else {
throw "Group not found"
}
This version uses the Pathname COM object to handle name escaping and outputs the the object class and sAMAccountName for each member of the group.