We’ve already discussed the biggest new PowerShell language feature – workflows – in a previous post. In this post, I’m going to describe a number of small changes we’ve made to the language. Most of the changes described here don’t introduce any new syntax, but I think you’ll agree that most of these changes make various aspects of scripting in PowerShell simpler. To me, many of these features are what make PowerShell a joy to use.
Member Enumeration
To start, I’m going to describe a little feature that had no official name until I started this blog post. I’m also starting with this feature because it was a surprise hit (to me anyway) when we demonstrated to the PowerShell MVPs recently.
If you find yourself frequently piping a collection to ForEach-Object or Select-Object (or more likely the aliases % or select) to get a single property, then you’ll love this feature.
Say you want a list of full paths to some files. In PowerShell V2.0, you’d write:
dir | % { $_.FullName }
In PowerShell V3.0, you can write:
(dir).FullName
It doesn’t matter if the cmdlet is returning one result or multiple results. Under the covers, PowerShell is automatically handling the differences for you, as though you were using ForEach-Object in the first example, except it’s faster because the ForEach-Object cmdlet isn’t actually running.
This new capability doesn’t introduce any new syntax and it just works wherever you would get a property or invoke a method. But you can’t use this syntax to set properties.
We added this feature to make it easier to deal with commands that return one or many objects. Experienced PowerShell scripters knew they needed to use @() to handle that situation correctly, but we really thought that was less than perfect.
Member enumeration is one piece of handling commands that return one or many objects, but there are a few other details that we’ve taken care of as well.
You can now use Count or Length on any object, even if it didn’t have the property. If the object didn’t have a Count or Length property, it will will return 1 (or 0 for $null). Objects that have Count or Length properties will continue to work as they always have.
PS> $a = 42
PS> $a.Count
1
You can also index into any object even if it didn’t have an index operation. If the object has no indexer, index 0 simply returns the object, any other index returns nothing (or an error if strict-mode is enabled).
PS> $a[0]
42
PS> $a[-1]
42
Combining indexing and Count, you can now use for loops without worrying if the input is an array or single object:
$x = dir # returns 1 or more objects
for ($i = 0; $i –lt $x.Count; $i++)
{
$x[$i] ...
}
These language features could actually change the behavior some scripts. We’ve found a few real-world scripts that stopped working due to these changes. But we also found, after closer review of these scripts, that they were already broken. In one example the script was actually failing to process all of the data it was trying to verify. We are not aware of any scripts where this change broke a script, but did not expose an underlying problem in the script.
New Redirection Operators
There are new redirection operators so you can redirect verbose, warning, and debug streams. Merging and file redirections work in the same way as error redirection. Here are some examples:
Do-Something 3> warning.txt # Writes warning output to warning.txt
Do-Something 4>> verbose.txt # Appends verbose.txt with the verbose output
Do-Something 5>&1 # Writes debug output to the output stream
Do-Something *> out.txt # Redirects all streams (output, error, warning, verbose, and debug) to out.txt
We didn’t get a chance to add redirection support for host output (Write-Host, $host.UI.Write…).
Passing Local Variables to Remote Sessions
In PowerShell 2.0, if you want to use local variables when executing a script block remotely, you have to do something like:
PS> $localVar = 42
PS> Invoke-Command -cn srv123 { param($localVar) echo $localVar } –ArgumentList $localVar
42
In PowerShell 3.0, variables prefixed with $using: are automatically recognized as local variables and are sent to the remote machine:
PS> Invoke-Command –cn srv123 { echo $using:localVar }
42
PSItem
We sometimes receive feedback that $_ is cryptic and confusing to people who are new to PowerShell and scripting. Some people see $_ as a magic symbol. To help those people, we introduced an alias for $_. You can use $PSItem anywhere you would have used $_. For example, instead of writing:
dir | % { $_.FullName }
You can write:
dir | % { $PSItem.FullName }
More Flexible Script Formatting
We reviewed many places where newlines were previously required and relaxed those restrictions whenever it didn’t introduce any ambiguities or break existing scripts.
One of the more interesting places you might find this useful is in using a fluent API. You can now have whitespace/newlines after the ‘.’ or ‘::’ tokens in expression mode.
[PowerShell]::Create().
AddCommand("echo").
AddParameter("InputObject", 42).
Invoke()
In PowerShell 2.0, this needs to be all on one line, or broken into multiple statements. For example, the following probably doesn’t do what you think it should.
Write-Output ([int]::MaxValue). GetType()
Write-Output ([int]::MaxValue).
GetType()
The same expressions work when they don’t begin with a command:
([int]::MaxValue). GetType()
([int]::MaxValue).
GetType()
Attributes on Variables
Validation attributes and argument transformation attributes are familiar to anyone who writes advanced functions. In PowerShell V2.0, these attributes can be used only on parameters. In PowerShell V3.0, you can place an attribute on any variable.
PS> [ValidateRange(1,10)][int]$x = 1
PS> $x = 11
The variable cannot be validated because the value 11 is not a valid value for the x variable.
At line:1 char:1
+ $x = 11
+ ~~~~~~~
+ CategoryInfo : MetadataError: (:) [], ValidationMetadataException
+ FullyQualifiedErrorId : ValidateSetFailure
Attribute Arguments Don’t Need = $true
In PowerShell V2.0, you write:
param([Parameter(Mandatory=$true)]$p)
In PowerShell V3.0, you can write:
param([Parameter(Mandatory)]$p)
If you omit the argument, there is an implied argument value of $true.
Smarter Parsing of Command Names
In PowerShell V2.0, if you are running 7z or another command that starts with a number, you have to use the command invocation operator:
PS> & 7z
In PowerShell V3.0, you don’t need the ‘&’ anymore.
Easier Reuse of Command Lines From Cmd.exe
The web is full of command lines written for Cmd.exe. These commands lines work often enough in PowerShell, but when they include certain characters, e.g. a semicolon (;) a dollar sign ($), or curly braces, you have to make some changes, probably adding some quotes. This seemed to be the source of many minor headaches.
To help address this scenario, we added a new way to “escape” the parsing of command lines. If you use a magic parameter --%, we stop our normal parsing of your command line and switch to something much simpler. We don’t match quotes. We don’t stop at semicolon. We don’t expand PowerShell variables. We do expand environment variables if you use Cmd.exe syntax (e.g. %TEMP%). Other than that, the arguments up to the end of the line (or pipe, if you are piping) are passed as is. Here is an example:
PS> echoargs.exe --% %USERNAME%,this=$something{weird}
Arg 0 is <jason,this=$something{weird}>
Better Splatting of Arrays
Sometimes you want to write a quick wrapper for a script or cmdlet, but you don’t want to write a full-on proxy cmdlet. For example:
function mkdir($path)
{
New-Item -Path $path -ItemType Directory @args
}
Now if I pass -Force or whatever to mkdir, it will be passed on correctly, but I didn’t need to specify any of the parameters to New-Item.
New Operators
We added a few operators:
-shl | Shift bits left |
-shr | Shift bits right – preserves sign for signed values |
-in | Like –contains, but with the operands reversed |
-notin | Like –notcontains, but with the operands reversed |
-in/-notin were added to complement the “simplified foreach” feature. We couldn’t support the –contains/-notcontains operators in simplified foreach, but by reversing the operands, we could have ForEach-Object support and operator support.
New Conversions
Sometimes you want the keys of a hash literal to be ordered. You can now cast to [ordered] and we create an OrderedDictionary instead of HashTable. This only works with literals – if you try it on a variable, the ordering is no longer available.
[ordered]@{a=1; b=2}
In a similar manner, you can create custom objects with a simple cast:
[pscustomobject]@{x=1; y=2}
This does not create a HashTable; it creates a PSObject with note properties. If you are casting a literal, the note properties are added in the order they appear in the literal. If you are casting anything else, the ordering is determined by the IDictionary iterator.
If a type has a default constructor and settable properties, you can also use a hash table to case an object to that type:
[System.Drawing.Point]@{X=1; Y=2}
This will call the default constructor and set the properties named in the hash table.
In a similar manner, deserialized objects can be reconstructed if the properties are all settable.
ForEach statement does not iterate over $null
In PowerShell V2.0, people were often surprised by:
PS> foreach ($i in $null) { 'got here' }
got here
This situation often comes up when a cmdlet doesn’t return any objects. In PowerShell V3.0, you don’t need to add an if statement to avoid iterating over $null. We take care of that for you.
Comment Based Help Improvements
PowerShell V2.0 comment based help supports most of the features available in xml based help, but there were a couple of holes that we’ve addressed in V3, both related to parameter help. If you use Get-Help –full, you see a bunch of parameter descriptions that include text like the following:
Required? true
Position? named
Default value False
Accept pipeline input? false
Accept wildcard characters? false
PowerShell V2.0 didn’t provide any way to specify what help gets displayed for the highlighted fields above. In PowerShell V2.0, we’ve fixed that.
For default values, if you’ve specified a default value, we include that. If you want finer control over what gets displayed, say to provide some context about the default value, you can use the attribute PSDefaultValue.
If your parameter supports wildcards, you can add the SupportsWildCards attribute.
Note that these 2 attributes are only used for help. PowerShell doesn’t use these attributes for anything else.
Here is an example:
function Test-Something($path)
{
param([PSDefaultValue(Help = 'A random number.')]$p = $(Get-Random),
[SupportsWildcards()]$q)
}
Conclusion
We hope these new language features make scripting easier and more fun. In an upcoming post, we’ll cover other ways that we’ve made scripting easier.
Jason Shirk
Software Design Engineer – Windows PowerShell
Microsoft Corporation