Understanding Attributes

Attributes can add powerful functionality to your PowerShell code and shorten your code considerably. Let's discover un(der)documented PowerShell attributes and use them to control PowerShell.

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 on TAB, 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 for attribute 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!