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.

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 run this code:


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
$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 
  }

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

# dump keys for attribute [ValidateSet()]
$attributes.ValidateSet

# dump keys for attribute [Parameter()]
$attributes.Parameter

When you look at the attribute [ValidateSet()] that we used above, some exciting secrets unfold:

PS> $attributes.ValidateSet
Name           Type                            
----           ----                                                                    
IgnoreCase     System.Boolean                                 
[ValidValues]  System.Collections.Generic.IList`1[[System.String, ...]]

As you discover, [ValidateSet()]supports two values: the key IgnoreCase, and a default value without a key listed as [ValidValues]. The name of this value is placed in square brackets to indicate that this value really has no key and is a default value.

Now take a look at how we used this attribute before:

[ValidateSet('Microsoft','Amazon','Google')]$customer = 'Microsoft'

Inside the parenthesis, you see the default value in action: it is not using any 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 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:

PS> $attributes.CmdletBinding

Name                    Type                                           
----                    ----                                           
PositionalBinding       System.Boolean                                 
DefaultParameterSetName System.String                                  
SupportsShouldProcess   System.Boolean                                 
SupportsPaging          System.Boolean                                 
SupportsTransactions    System.Boolean                                 
ConfirmImpact           System.Management.Automation.ConfirmImpact     
HelpUri                 System.String                                  
RemotingCapability      System.Management.Automation.RemotingCapability

PS> $attributes.Parameter

Name                            Type          
----                            ----          
Position                        System.Int32  
ParameterSetName                System.String 
Mandatory                       System.Boolean
ValueFromPipeline               System.Boolean
ValueFromPipelineByPropertyName System.Boolean
ValueFromRemainingArguments     System.Boolean
HelpMessage                     System.String 
HelpMessageBaseName             System.String 
HelpMessageResourceId           System.String 
DontShow                        System.Boolean

Boom, and 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:

PS> 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

This is part 1 in a series. 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!