Support for attributes was added in PowerShell 2 so attributes are available pretty much in every PowerShell version that is around today. Surprisingly enough, the majority of PowerShell users isn’t very familiar with attributes, and a lot of highly useful functionality is wasted.
So let’s change that, it won’t take long. I’ll first introduce the concept of attributes and then provide you with the code snippets necessary to uncover secret PowerShell attributes and their supported values.
At the end, we’ll take a look at some useful examples, and I provide you with all the directions you need to go exploring on your own.
Attributes: PowerShell “Wish Lists”
Attributes pretty much work like “wish lists” where you can list all kinds of wishes. Next, attach these attributes to PowerShell code to adjust it to your needs. Voilá: PowerShell magically reads your wishes and acts accordingly, and you unleash features that you might not even knew existed.
Attributes and Types
Before we start, it’s important to understand how attributes differ from types.
Attributes look almost like types and consist of a name enclosed in square brackets. In addition, attributes have a list of comma-separated values (your “wishes”, or the “metadata” that you want to attach to some PowerShell code) enclosed in parenthesis.
# a type
[int]
# an attribute
[ValidateSet()]
Should you try and run the code above you’ll quickly notice that you can run types, but you cannot run attributes. Attributes need to be attached to something and do not work stand-alone.
Types Define Content
A type defines some general type of content. [int]
for example can be used define the content of variables. In the example below, the type [int]
makes sure the variable $number can only store integers (whole numbers), and when you assign a string or double instead, PowerShell automatically converts the value to an integer.
If the assigned value can’t be converted, an exception is thrown:
# let's take a look at types first
# $number can store integers only:
[int]$number = 10
# non-integers like floating point numbers are rounded and
# converted to integers
$number = 9.6
$number
# if the value can't be represented by integers, an exception
# is thrown
$number = 'I am no number!'
Attributes Define Metadata
As you have seen, attributes can’t work stand-alone. They are a vehicle to transport information to PowerShell code, so attributes need to be attached to the PowerShell code that you want the information to receive.
Each attribute has (a) a special purpose, (b) a distinct recipient it can be attached to, and (c) a list of supported values.
The attribute [ValidateSet()]
, for example, (a) limits variables to a set of string values, (b) can be attached to variables, and (c) supports an array of strings.
So while the type [string]
defines a rather general content type, the attribute [ValidateSet()]
can specifically qualify the variable and restrict string values to a given set of strings.
# variable $name can store any string:
[string]$name = 'Tobias'
# variable $customer can store only the strings defined by the attribute
[string][ValidateSet('Microsoft','Amazon','Google')]$customer = 'Microsoft'
$customer = 'Amazon'
$customer = 'Google'
# when you assign a string that is not defined by the attribute,
# an exception is raised:
$customer = 'Tesla'
Attributes are your way of assigning additional information (“metadata”) to PowerShell code. What exactly the attribute does depends entirely on who actually cares about the attribute and its values.
Whenever you assign a new value to a variable, for example, PowerShell automatically searches for a number of attributes (like
[ValidateSet()]
) and uses the extra information provided by the attributes to validate the assignment and make sure it meets your criteria. Attributes like[ValidateSet()]
can be a clever way of shortening your code because you can delegate the effort to check certain requirements to PowerShell and won’t need to write the validation code yourself.If you use a variable inside a param() block to define parameters, PowerShell takes the string values defined by the
[ValidateSet()]
attribute to also provide argument completion onTAB
, and editors like ISE or VSCode even provide rich Intellisense menus, listing the string values defined by the attribute.
Finding Attributes
To start playing with attributes, the hardest part often is to find out their names. All attributes are defined by special types that derive from [System.Attribute]
, and their type name ends with Attribute.
This code snippet dumps all attributes defined by PowerShell:
filter Test-Attribute([Type]$DerivedFrom)
{
# get the parent type of this type
$baseType = $_.BaseType
do
{
# if the parent is derived from the desired type,
# return it
if ($baseType -eq $DerivedFrom)
{
$_
}
# else walk up the inheritance chain
# by looking at the parent of the
# current parent until no more
# parent exists
$baseType = $baseType.BaseType
} while ($baseType)
}
# dump all PowerShell types by taking (any) powershell type,
# identify its assembly, and dump all public types
[PSObject].Assembly.GetTypes() |
Where-Object IsPublic |
# take only types that derive from "System.Attribute"
Test-Attribute -DerivedFrom ([System.Attribute]) |
# remove "Attribute" suffix
ForEach-Object { $_ -replace 'Attribute$' } |
# remove namespace prefix
Foreach-Object { $_.Split('.')[-1] } |
# output in attribute syntax
ForEach-Object { "[$_()]"} |
Sort-Object
The output looks similar to this:
[Alias()]
[AllowEmptyCollection()]
[AllowEmptyString()]
[AllowNull()]
[ArgumentCompleter()]
[ArgumentToConfigurationDataTransformation()]
[ArgumentTransformation()]
[Cmdlet()]
[CmdletBinding()]
[CmdletCommonMetadata()]
[CmdletMetadata()]
[CmdletProvider()]
[Credential()]
[DscLocalConfigurationManager()]
[DscProperty()]
[DscResource()]
[DynamicClassImplementationAssembly()]
[EtwEvent()]
[Hidden()]
[OutputType()]
[Parameter()]
[ParsingBase()]
[PSDefaultValue()]
[PSTypeName()]
[RunspaceConfigurationType()]
[SupportsWildcards()]
[ValidateArguments()]
[ValidateCount()]
[ValidateDrive()]
[ValidateEnumeratedArguments()]
[ValidateLength()]
[ValidateNotNull()]
[ValidateNotNullOrEmpty()]
[ValidatePattern()]
[ValidateRange()]
[ValidateScript()]
[ValidateSet()]
[ValidateTrustedData()]
[ValidateUserDrive()]
This lists the attributes exclusively defined by PowerShell only.
In addition, PowerShell reads and understands a number of attributes defined elsewhere, for example the attributes
[DebuggerHidden()]
and[DebuggerNotUserCode()]
which can be used to hide PowerShell source code from the debugger.
Exploring Little-Known Attributes
Above list contains a lot of lesser-known attributes such as [SupportsWildcards()] and [PSDefaultValue()].
These two attributes were introduced in PowerShell 3 to give function authors more control over what is shown in PowerShell help. Here is an example:
function test
{
[CmdletBinding()]
param
(
[string]
$Path,
[SupportsWildcards()]
[PSDefaultValue(Help = "Wildcard")]
[string]
$Filter = '*'
)
}
Theoretically, these two attributes would fill in the marked positions in the parameter help that shows when you run help test -Parameters *
:
-Filter <string>
Required? false
Position? 1
Default value >[PSDefaultValue()]<
Accept pipeline input? false
Accept wildcard characters? >[SupportsWildcards()]<
Unfortunately, their functionality seems to have been lost in newer versions of PowerShell. At least I wasn’t able to make them add information to the help. If you can, please leave a comment at the bottom.
Attribute Inheritance
Not all attributes are actually used. Some of the types are abstract types and used for inheritance only. The graph below visualizes the inheritance. Abstract types are colored in a darker color:
gantt
dateFormat YYYY-MM-DD
axisFormat
section .
Attribute :done, , 2020-01-01,2d
ArchitectureSensitive :active, , 2020-01-02,5d
ArgumentCompleter :active, , 2020-01-02,5d
CmdletMetadata : , 2020-01-02,5d
AllowEmptyCollection :active, , 2020-01-04,5d
AllowEmptyString :active, , 2020-01-04,5d
AllowNull :active, , 2020-01-04,5d
ArgumentTransformation : , 2020-01-04,5d
ArgumentToConfigurationDataTransformation :active, , 2020-01-06,5d
ArgumentToModuleTransformation :active, , 2020-01-06,5d
ArgumentToVersionTransformation :active, , 2020-01-06,5d
ArgumentTypeConverter :active, , 2020-01-06,5d
Credential :active, , 2020-01-06,5d
CmdletCommonMetadata : , 2020-01-04,5d
Cmdlet :active, , 2020-01-06,5d
CmdletBinding :active, , 2020-01-06,5d
DscLocalConfigurationManager :active, , 2020-01-04,5d
DscProperty :active, , 2020-01-04,5d
DscResource :active, , 2020-01-04,5d
OutputType :active, , 2020-01-04,5d
ParsingBase : , 2020-01-04,5d
Alias :active, , 2020-01-06,5d
Hidden :active, , 2020-01-06,5d
Parameter :active, , 2020-01-06,5d
PSDefaultValue :active, , 2020-01-06,5d
SupportsWildcards :active, , 2020-01-06,5d
ValidateArguments : , 2020-01-04,5d
ValidateCount :active, , 2020-01-06,5d
ValidateDrive :active, , 2020-01-06,5d
ValidateUserDrive :active, , 2020-01-08,5d
ValidateEnumeratedArguments : , 2020-01-06,5d
ValidateLength :active, , 2020-01-08,5d
ValidatePattern :active, , 2020-01-08,5d
ValidateRange :active, , 2020-01-08,5d
ValidateScript :active, , 2020-01-08,5d
ValidateSet :active, , 2020-01-08,5d
ValidateNotNull :active, , 2020-01-06,5d
ValidateNotNullOrEmpty :active, , 2020-01-06,5d
ValidateTrustedData :active, , 2020-01-06,5d
ValidateVersion :active, , 2020-01-06,5d
CmdletProvider :active, , 2020-01-02,5d
DynamicClassImplementationAssembly :active, , 2020-01-02,5d
PSTypeName :active, , 2020-01-02,5d
RunspaceConfigurationType :active, , 2020-01-02,5d
TraceSource :active, , 2020-01-02,5d
Finding Attribute Values
Once you know the name of an attribute, you also need to know the type of values you can assign to it (the nature of its “metadata”).
Each attribute is defined by a type, and the “metadata” values that you can assign are really properties of this type. So you can find out the names and data types of the metadata by examining the properties of the type that defines the attribute.
Or just use Get-AttributeInfo
below:
function Get-AttributeInfo
{
filter Test-Attribute([Type]$DerivedFrom = [System.Attribute])
{
# get the parent type of this type
$baseType = $_.BaseType
do
{
# if the parent is derived from the desired type,
# return it
if ($baseType -eq $DerivedFrom)
{
$_
}
# else walk up the inheritance chain
# by looking at the parent of the
# current parent until no more
# parent exists
$baseType = $baseType.BaseType
} while ($baseType)
}
# dump all PowerShell types by taking (any) powershell type,
# identify its assembly, and dump all public types
$attributes = [PSObject].Assembly.GetTypes() |
Where-Object IsPublic |
# take only types that derive from "System.Attribute"
Test-Attribute -DerivedFrom ([System.Attribute]) |
Foreach-Object -begin {
# create a new empty ordered hashtable
$hash = [Ordered]@{}
} -process {
# for each attribute, get name:
$name = $_.Name.Split('.')[-1] -replace 'Attribute$'
# add hashtable entry and assign writeable properties
# to it:
$hash[$name] = $_.GetProperties() |
ForEach-Object {
# ignore property "TypeId"
if ($_.Name -ne 'TypeId')
{
# if the property is writeable, it is
# a named value
$ValueName = $_.Name
# else it is the default value and has
# no key (we mark the name with brackets)
if (!$_.CanWrite)
{
$ValueName = "[$ValueName]"
}
[PSCustomObject]@{
Name = $ValueName
Type = $_.PropertyType.FullName
}
}
}
} -end {
# return filled hashtable
$hash
}
filter Test-Attribute([Type]$DerivedFrom)
{
# get the parent type of this type
$baseType = $_.BaseType
do
{
# if the parent is derived from the desired type,
# return it
if ($baseType -eq $DerivedFrom)
{
$_
}
# else walk up the inheritance chain
# by looking at the parent of the
# current parent until no more
# parent exists
$baseType = $baseType.BaseType
} while ($baseType)
}
# dump all PowerShell types by taking (any) powershell type,
# identify its assembly, and dump all public types
[PSObject].Assembly.GetTypes() |
Where-Object IsPublic |
# take only types that derive from "System.Attribute"
Test-Attribute -DerivedFrom ([System.Attribute]) |
Foreach-Object -begin {
# create a new empty ordered hashtable
$hash = [Ordered]@{}
} -process {
# for each attribute, get name:
$name = $_.Name.Split('.')[-1] -replace 'Attribute$'
# keep track of positional parameters so we can later
# exclude named parameters of same name:
$positional = @{}
$hash[$name] = & {
# all constructor arguments are positional parameters
$_.GetConstructors() | ForEach-Object {
$_.GetParameters() | ForEach-Object {
$positional[$_.Name] = $true
[PSCustomObject]@{
Name = $_.Name
Kind = 'Positional'
Type = $_.ParameterType
}
}
}
$_.GetProperties() |
ForEach-Object {
# ignore property "TypeId" and include only writeable properties that
# haven't been added before
if ($_.Name -ne 'TypeId' -and $_.CanWrite -and !$positional.ContainsKey($_.Name))
{
[PSCustomObject]@{
Name = $_.Name
Kind = 'Named'
Type = $_.PropertyType.FullName
}
}
}
}
} -end {
# return filled hashtable
$hash
}
}
$attributes = Get-AttributeInfo
Like the example above, the code searches for all types derived from [System.Attribute]
and then lists all property names and their type. The code returns a hashtable in $attributes:
# dump all attribute names:
$attributes.Keys | Sort-Object
Alias
AllowEmptyCollection
AllowEmptyString
AllowNull
ArgumentCompleter
ArgumentToConfigurationDataTransformation
ArgumentTransformation
Cmdlet
CmdletBinding
CmdletCommonMetadata
CmdletMetadata
CmdletProvider
Credential
DscLocalConfigurationManager
DscProperty
DscResource
DynamicClassImplementationAssembly
EtwEvent
Hidden
OutputType
Parameter
ParsingBase
PSDefaultValue
PSTypeName
RunspaceConfigurationType
SupportsWildcards
ValidateArguments
ValidateCount
ValidateDrive
ValidateEnumeratedArguments
ValidateLength
ValidateNotNull
ValidateNotNullOrEmpty
ValidatePattern
ValidateRange
ValidateScript
ValidateSet
ValidateTrustedData
ValidateUserDrive
# dump keys for attribute [ValidateSet()]
$attributes.ValidateSet
Name Kind Type
---- ---- ----
validValues Positional System.String[]
IgnoreCase Named System.Boolean
# dump keys for attribute [Parameter()]
$attributes.Parameter
Name Kind Type
---- ---- ----
Position Named System.Int32
ParameterSetName Named System.String
Mandatory Named System.Boolean
ValueFromPipeline Named System.Boolean
ValueFromPipelineByPropertyName Named System.Boolean
ValueFromRemainingArguments Named System.Boolean
HelpMessage Named System.String
HelpMessageBaseName Named System.String
HelpMessageResourceId Named System.String
DontShow Named System.Boolean
When you look at the attribute [ValidateSet()]
, secrets start to unfold:
$attributes.ValidateSet
Name Kind Type
---- ---- ----
validValues Positional System.String[]
IgnoreCase Named System.Boolean
As you discover, [ValidateSet()]
supports two arguments: the named argument IgnoreCase, and a positional argument ValidValues. Now take a look at how we used this attribute before:
[ValidateSet('Microsoft','Amazon','Google')]$customer = 'Microsoft'
Inside the parenthesis, you see the positional argument: it is not using a named key name. We assigned a list of strings which coincidentally is exactly the type required by ValidValues as per definition above.
When you play with the variable $customer, you’ll discover that the attribute does not care about case by default. As you now know, there is an additional named value IgnoreCase of type Boolean, so let’s add it:
[ValidateSet('Microsoft','Amazon','Google',IgnoreCase=$false)]$customer = 'Microsoft'
When you run this line, now the attribute is case sensitive, and you now must use the exact casing.
Go Exploring!
By now you know what attributes are and how you can identify them, what their names are, and what type of values you can assign. That’s already enough to go exploring for a day or two:
- Take a look at your PowerShell code, or any PowerShell code you find anywhere, and start by searching for attributes. They always have this format: “[AttributeName(Value1, Value2, …)]”.
- Look up the attribute in $attributes, and take a look at the supported values. Try some new values, and investigate what they might do.
- Google for any of the attributes you identified above. For example, to find inspiration and examples for the attribute
[Parameter()]
, google forattribute powershell parameter
.
For example, you may have come across code like this:
function New-Function
{
[CmdletBinding()]
param
(
[String]
[Parameter(Mandatory)]
$Path,
[Switch]
$Force
)
# some code
}
You now know that this code uses two attributes, simply by looking at the syntax: [CmdletBinding()]
and [Parameter()]
. The first attribute is attached to a param() block, and the second to a variable.
With your $attributes cheat sheet, you can dump the supported values for both attributes:
$attributes.CmdletBinding
Name Kind Type
---- ---- ----
PositionalBinding Named System.Boolean
DefaultParameterSetName Named System.String
SupportsShouldProcess Named System.Boolean
SupportsPaging Named System.Boolean
SupportsTransactions Named System.Boolean
ConfirmImpact Named System.Management.Automation.ConfirmImpact
HelpUri Named System.String
RemotingCapability Named System.Management.Automation.RemotingCapability
$attributes.Parameter
Name Kind Type
---- ---- ----
Position Named System.Int32
ParameterSetName Named System.String
Mandatory Named System.Boolean
ValueFromPipeline Named System.Boolean
ValueFromPipelineByPropertyName Named System.Boolean
ValueFromRemainingArguments Named System.Boolean
HelpMessage Named System.String
HelpMessageBaseName Named System.String
HelpMessageResourceId Named System.String
DontShow Named System.Boolean
Boom, there you go! Either trust your intuition, and play with some of the values, or use the key names to google, for example powershell parameter position attribute
or powershell cmdletbinding supportsshouldprocess
.
In no time, you’ll learn plenty of new secrets. For example, to add the common parameters -WhatIf
and -Confirm
to your functions, add the key SupportsShouldProcess
:
function New-Function
{
[CmdletBinding(SupportsShouldProcess)]
param
(
[String]
[Parameter(Mandatory)]
$Path,
[Switch]
$Force
)
# some code
}
When you run the code, your function New-Function
now has the additional parameters -WhatIf
and -Confirm
, and both parameters are already hard-wired and on request turn on the simulation mode for all cmdlets used inside your function.
Or, you can now hide parameters from Intellisense. If for some reason, you want to hide the parameter -Force
, use the key DontShow
provided by the attribute [Parameter()]
:
function New-Function
{
[CmdletBinding()]
param
(
[String]
[Parameter(Mandatory)]
$Path,
[Switch]
[Parameter(DontShow)]
$Force
)
if ($Force)
{
"You have the power, Dude!"
}
else
{
"You are in vanilla mode."
}
}
When you run this code, your function New-Function
now hides all parameters from tab completion and intellisense that are decorated with the key DontShow. They still work, though:
New-Function -Path test -Force
You have the power, Dude!
As a side effect, whenever you use DontShow on any parameter, all common parameters are hidden. So this is also a great way if you want to use Advanced Functions but hate that a plethora of common parameters pollute the intellisense menu.
What’s Next
In our next part I’ll start showcasing some of the attributes in action. So stay tuned if you are hungry for more! And make sure you have PowerShell Conference EU 2020 on your radar. That’s the place to be in June if you enjoy stuff like this. See you there!