Earlier we mentioned that PowerShell variables are actually objects. In .NET, string variables are extremely robust, and have a number of methods and properties. Refer to Appendix B for a complete summary of the methods and properties. One object is Split, which is a method that takes a string and creates an array (or list) by breaking the list on some character such as comma or a space. Try this in PowerShell:
PS C\:> "1,2,3,4".Split(",")
This tells PowerShell to "take this string, and execute its Split method. Use a comma for the method's input argument." When PowerShell executes this commend, the method returns an array of four elements, each element containing a number. PowerShell displays the array it in a textual fashion, with one array element per line:
1 2 3 4
There are other ways to use this. For example, PowerShell has a cmdlet called Get-Member that displays the methods and variables associated with a given object instance. Taking a string like "Hello, World", which is a String object, and piping it to the Get-Member cmdlet, displays information about that String object:
PS C:\> "Hello, World" | get-member TypeName: System.String Name MemberType Definition ---- ---------- ---------- Clone Method System.Object Clone() CompareTo Method System.Int32 CompareTo(Object v Contains Method System.Boolean Contains(String CopyTo Method System.Void CopyTo(Int32 source EndsWith Method System.Boolean EndsWith(String Equals Method System.Boolean Equals(Object ob GetEnumerator Method System.CharEnumerator GetEnume GetHashCode Method System.Int32 GetHashCode() GetType Method System.Type GetType() GetTypeCode Method System.TypeCode GetTypeCode() get_Chars Method System.Char get_Chars(Int32 ind get_Length Method System.Int32 get_Length() IndexOf Method System.Int32 IndexOf(Char value IndexOfAny Method System.Int32 IndexOfAny(Char[] Insert Method System.String Insert(Int32 star IsNormalized Method System.Boolean IsNormalized(), LastIndexOf Method System.Int32 LastIndexOf(Char v LastIndexOfAny Method System.Int32 LastIndexOfAny Normalize Method System.String Normalize(), Syst PadLeft Method System.String PadLeft(Int32 tot PadRight Method System.String PadRight(Int32 to Remove Method System.String Remove(Int32 star Replace Method System.String Replace(Char oldC Split Method System.String[] Split(Params Ch StartsWith Method System.Boolean StartsWith(Strin Substring Method System.String Substring(Int32 s ToCharArray Method System.Char[] TSystem.Char[] To ToLower Method System.String ToLower(), Syste ToLowerInvariant Method System.String ToLowerInvariant( ToString Method System.String ToString(), Syste ToUpper Method System.String ToUpper(), System ToUpperInvariant Method System.String ToUpperInvariant( Trim Method System.String Trim(Params Char[ TrimEnd Method System.String TrimEnd(Params Ch TrimStart Method System.String TrimStart(Params Chars ParameterizedProperty System.Char Chars(Int32 inde Length Property System.Int32 Length {get;}
Even though this output is truncated a bit to fit in this book, you can see that it includes every method and property of the String. It also correctly identifies "Hello, World" as a "System.String" type, which is the unique type name that describes what we informally call a String object. You can pipe nearly anything to Get-Member to learn more about that particular object and its capabilities.
One of the most frequently used object variables is the $_ variable. We've used it repeatedly in our examples. $_ is an automatic variable that stands for the current object. Here's an example:
PS C:\> get-process | where {$_.workingset -gt 10000*1024} Handles NPM(K) PM(K) WS(K) VS(M) CPU(s) Id ProcessName ------- ------ ----- ----- ----- ------ -- ----------- 604 14 23856 37308 112 32.26 2236 explorer 114 5 6540 12380 52 3.12 2444 i_view32 617 10 56252 52784 172 7.02 632 PS 246 5 8672 12300 45 3.02 1512 MsMpEng 201 12 11528 19400 71 1.24 3452 Skype 198 6 26016 12816 1584 1.07 544 sqlservr 1416 49 17768 26312 131 5.25 1556 svchost 405 14 37140 58236 287 111.07 3900 WINWORD
This pipes the output of Get-Process to the Where cmdlet and only displays those processes where the workingset property of the current object ($_) is greater than 10240000. We use dotted notation to define the object and property: $_.workingset.
How Did We Know That?
The output of the Get-Process cmdlet is a System.Diagnostic.Process object. One of this object's properties is workingset. You can find the properties yourself by running:
PS C:\> get-process |get-member -membertype property
Let's go back and look at the array of service objects we created earlier in this chapter. We used the following expression to create the object:
PS C:\> $svc=get-service | where {$_.status -eq "running"}
Since we know the array contains service objects, it might help to know more about these objects. Let's look at the first element of the array:
PS C:\> $svc[0] Status Name DisplayName ------ ---- ----------- Running AudioSrv Windows Audio
We know the first running service in the array is Windows Audio. We're going to learn about this object using this service as an example even if we're not interested in that specific service. We'll pass the first element of the array to Get-Member and display the object's properties.
PS C:\> $svc[0] |get-member -membertype properties TypeName: System.ServiceProcess.ServiceController Name MemberType Definition ---- ---------- ---------- Name AliasProperty Name = ServiceName CanPauseAndContinue Property System.Boolean CanPauseAndCont CanShutdown Property System.Boolean CanShutdown {ge CanStop Property System.Boolean CanStop {get;} Container Property System.ComponentModel.IContain DependentServices Property System.ServiceProcess.ServiceC DisplayName Property System.String DisplayName {get MachineName Property System.String MachineName {get ServiceHandle Property System.Runtime.InteropServices ServiceName Property System.String ServiceName {get ServicesDependedOn Property System.ServiceProcess.ServiceC ServiceType Property System.ServiceProcess.ServiceT Site Property System.ComponentModel.ISite Si Status Property System.ServiceProcess.ServiceC
Every property is not necessarily populated, nor may they all be of interest. However, we want to show you where this information comes from and how it is connected to the object.
The easier way to learn property names is by sending the output of the array element to Format-List:
PS C:\> $svc[0] |format-list Name : AudioSrv DisplayName : Windows Audio Status : Running DependentServices : {} ServicesDependedOn : {RpcSs, PlugPlay} CanPauseAndContinue : False CanShutdown : False CanStop : True ServiceType : Win32ShareProcess
The fact that PowerShell is built on .NET gives it tremendous versatility that isn't always obvious. For example, in Chapter 1, we explained that any PowerShell variable can contain any type of data. This is true because all types of data such as strings, integers, and dates are .NET classes, which means they all inherit from the base class named Object. A PowerShell variable can contain anything that inherits from Object. However, as you saw in earlier examples with a string, PowerShell can tell the difference between different classes that inherit from Object.
You can force PowerShell to treat objects as a more specific type. When you do this, you are asking PowerShell to cast a variable to a specific type. For example, take a look at this sequence:
PS C:\> $one = 5 PS C:\> $two = "5" PS C:\> $one + $two 10 PS C:\> $one = 5 PS C:\> $two = "5" PS C:\> $one + $two 10 PS C:\> $two + $one 55
Here we give PowerShell two variables. One variable contains the number five, while the other variable contains the string character "5." This might look the same to you, but this is a big difference to a computer! Since we didn't specify what type of data these variables are, PowerShell assumes they are both the generic Object type. This caused PowerShell to decide it would figure out something more specific when the variables are used.
When we added $one and $two or 5 + "5," PowerShell said, "Aha, this must be addition. The first character is definitely not a string because it's not in double quotes. The second character is in double quotes but… Well, if I take the quotes away it looks like a number, so I'll add them." PowerShell logic correctly gave ten as the result.
However, when we add $two and $one, reversing the order, PowerShell has a different decision to make. In this case PowerShell said, "I see addition, but this first operand is clearly a string. The second one is a generic Object, so let's also treat it like a string, and concatenate the two." This PowerShell logic gave us the string "55," which is just the first five tacked onto the second.
But what about:
PS C:\> [int]$two + $one 10
This is the same order as the example that resulted in "55." However, this time we specifically told PowerShell to cast the generic object in $two as an Int or integer, which is a type PowerShell knows. So, it used the same logic as in the first example. It added the two to come up with ten.
You can force PowerShell to treat anything as a specific type. For example:
PS C:\> $int = [int]"5" PS C:\> $int | get-member TypeName: System.Int32 Name MemberType Definition ---- ---------- ---------- CompareTo Method System.Int32 CompareTo(Int32 value), System.Int Equals Method System.Boolean Equals(Object obj), System.Boole GetHashCode Method System.Int32 GetHashCode() GetType Method System.Type GetType() GetTypeCode Method System.TypeCode GetTypeCode() ToString Method System.String ToString(), System.String ToStrin
Here the value "5" would normally be either a String object or, at best, a generic Object. However, by specifying the type [int], we forced PowerShell to try and convert "5" into an integer before storing it in the variable $int. The conversion was successful because we can see where we piped $int to Get-Member, which revealed the object's type: System.Int32.
Note that once you apply a specific type to a variable, it stays that way until you specifically change it. For example:
[int]$int = 1
This creates a variable named $int as an integer, and assigns it the value 1. The $int variable will be treated as an integer from now on, even if you don't include the type:
$int = 2
It is still using $int as an integer because it was already cast into a specific type. Once set up to be an integer, you can't put other types of data into it. Here's an example of an error because we tried to put a string into a variable that was already specifically cast as an integer:
PS C:\> [int]$int = 1 PS C:\> $int = 2 PS C:\> $int = "hello" Cannot convert value "hello" to type "System.Int32". Error: "Input string was not in a correct format." At line:1 char:5 + $int <<<< = "hello" PS C:\>
However, you can recast a variable by reassigning a new, specific type:
[string]$int = "Hello"
That works fine, and $int will now be treated as a string by PowerShell.
PowerShell isn't a miracle worker. For example, PowerShell will complain if you try to force it to convert something that doesn't make sense:
PS C:\> $int = [int]"Hello" Cannot convert "Hello" to "System.Int32". Error: "Input string was not in a correct format." At line:1 char:13 + $int = [int]" <<<< Hello"
This occurred because "Hello" can't sensibly be made into a number. The next example is even more fun since it illustrates some of the advanced data types:
PS C:\> $xml = [xml]"<users><user name='joe' /></users>" PS C:\> $xml.users.user name ---- joe
Here we created a string, but told PowerShell it was of the type XML, which is another data type with which PowerShell is familiar. XML data works sort of like an object in that we define a parent object named Users, and a child object named User. The child object has an attribute called Name, with a value of Joe. So, when we ask PowerShell to display $xml.users.user, it displays all the attributes for that user. We can prove that PowerShell treated $xml as XML data by using Get-Member:
PS C:\> $xml | get-member TypeName: System.Xml.XmlDocument Name MemberType Definition ---- ---------- ---------- ToString CodeMethod static System.Stri add_NodeChanged Method System.Void add_No add_NodeChanging Method System.Void add_No add_NodeInserted Method System.Void add_No add_NodeInserting Method System.Void add_No add_NodeRemoved Method System.Void add_No add_NodeRemoving Method System.Void add_No AppendChild Method System.Xml.XmlNode Clone Method System.Xml.XmlNode CloneNode Method System.Xml.XmlNode CreateAttribute Method System.Xml.XmlAttr CreateCDataSection Method System.Xml.XmlCDat CreateComment Method System.Xml.XmlComm CreateDocumentFragment Method System.Xml.XmlDocu CreateDocumentType Method System.Xml.XmlDocu CreateElement Method System.Xml.XmlElem CreateEntityReference Method System.Xml.XmlEnti CreateNavigator Method System.Xml.XPath.X CreateNode Method System.Xml.XmlNode CreateProcessingInstruction Method System.Xml.XmlProc ...
This demonstrates not only that variables are objects, but also that PowerShell does understand different types of data, and provides different capabilities for them.
If you're curious about what object types are available, here's a quick list of more common types:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
There are more types than those listed above. In fact, we'll be popping in with details on the other types as appropriate throughout this book. Some of them aren't frequently used in administrative scripting, so we don't want to arbitrarily hit you with all of them at once. Instead, we'll cover them in a context where they're used for something useful. Just remember that, unlike other scripting languages with which you may be familiar, a PowerShell variable can contain more than a number or string.
One thing to be careful of is PowerShell's ability to change the type of a variable if you haven't explicitly selected a type. For example:
Write-host $a.ToUpper()
This works fine if $a contains a string, as shown here:
PS C:\> $a = "Hello" PS C:\> write-host $a.ToUpper() HELLO PS C:\>
However, you'll get an error if $a was already set to an integer value:
PS C:\> $a = 1 PS C:\> write-host $a.ToUpper() Method invocation failed because [System.Int32] doesn't contain a method named 'ToUpper'. At line:1 char:22 + write-host $a.ToUpper( <<<< ) PS C:\>
This occurs because, as an integer, $a doesn't have a ToUpper() method. Since this type of error can be tricky to troubleshoot, you need to watch out for this when you're writing scripts that take input from other sources such as a user or a file.
One way around it is to force PowerShell to treat the variable as the string you're expecting it to be:
PS C:\> $a = 1 PS C:\> $a = [string]$a PS C:\> write-host $a.ToUpper() 1 PS C:\>
You don't necessarily need to select a type up front for every variable you use, but you should be aware of situations that can make a variable contain a type of data other than what you originally expected.
You should also take precautions with variable naming. PowerShell is pretty forgiving and will let you use just about anything as a variable name:
PS C:\> $$="apple" PS C:\> $$ apple PS C:\> ${var}=100 PS C:\> ${var} 100 PS C:\> PS C:\> $7="PowerShell Scripting" PS C:\> $7 PowerShell Scripting PS C:\>
However, if you attempt to create a variable with anything other than a number or letter, PowerShell will complain:
PS C:\> $(j)="SAPIEN" Invalid assignment expression. The left hand side of an assignment operator nee ds to be something that can be assigned to like a variable or a property. At line:1 char:6 + $(j)=" <<<< SAPIEN" PS C:\>
Using these types of variables names isn't very practical or recommended. Instead, you should use variable names that are meaningful to you. So instead of:
$g=Get-Process
Use something like:
$Processes=Get-Process
It may require a bit more typing, but using meaningful variable names will definitely make it easier to troubleshoot and maintain your PowerShell scripts.