Object Based
Most shells operate in a text-based environment, which means you typically have to manipulate the output for automation purposes. For example, if you need to pipe data from one command to the next, the output from the first command usually must be reformatted to meet the second command's requirements. Although this method has worked for years, dealing with text-based data can be difficult and frustrating.
Often, a lot of work is necessary to transform text data into a usable format. Microsoft has set out to change the standard with PowerShell, however. Instead of transporting data as plain text, PowerShell retrieves data in the form of .NET Framework objects, which makes it possible for commands (cmdlets) to access object properties and methods directly. This change has simplified shell use. Instead of modifying text data, you can just refer to the required data by name. Similarly, instead of writing code to transform data into a usable format, you can simply refer to objects and manipulate them as needed.
Understanding the Pipeline
The use of objects gives you a more robust method for dealing with data. In the past, data was transferred from one command to the next by using the pipeline, which makes it possible to string a series of commands together to gather information from a system. However, as mentioned previously, most shells have a major disadvantage: The information gathered from commands is text based. Raw text needs to be parsed (transformed) into a format the next command can understand before being piped. To see how parsing works, take a look at the following Bash example:
$ ps -ef | grep "bash" | cut -f2 |
The goal is to get the process ID (PID) for the bash process. A list of currently running processes is gathered with the ps command and then piped to the grep command and filtered on the string "bash". Next, the remaining information is piped to the cut command, which returns the second field containing the PID based on a tab delimiter.
Based on the man information for the grep and cut commands, it seems as though the ps command should work. However, the PID isn't returned or displayed in the correct format.
The command doesn't work because the Bash shell requires you to manipulate text data to display the PID. The output of the ps command is text based, so transforming the text into a more usable format requires a series of other commands, such as grep and cut. Manipulating text data makes this task more complicated. For example, to retrieve the PID from the data piped from the grep command, you need to provide the field location and the delimiter for separating text information to the cut command. To find this information, run the first part of the ps command:
$ ps -ef | grep "bash" bob 3628 1 con 16:52:46 /usr/bin/bash |
The field you need is the second one (3628). Notice that the ps command doesn't use a tab delimiter to separate columns in the output; instead, it uses a variable number of spaces or a whitespace delimiter, between fields.
The cut command has no way to tell that spaces should be used as a field separator, which is why the command doesn't work. To get the PID, you need to use the awk scripting language. The command and output in that language would look like this:
$ ps -ef | grep "bash" | awk '{print $2}' 3628 |
The point is that although most UNIX and Linux shell commands are powerful, using them can be complicated and frustrating. Because these shells are text-based, often commands lack functionality or require using additional commands or tools to perform tasks. To address the differences in text output from shell commands, many utilities and scripting languages have been developed to parse text.
The result of all this parsing is a tree of commands and tools that make working with shells unwieldy and time consuming, which is one reason for the proliferation of management interfaces that rely on GUIs. This trend can be seen among tools Windows administrators use, too; as Microsoft has focused on enhancing the management GUI at the expense of the CLI.
Windows administrators now have access to the same automation capabilities as their UNIX and Linux counterparts. However, PowerShell and its use of objects fill the automation need Windows administrators have had since the days of batch scripting and WSH in a more usable and less parsing intense manner. To see how the PowerShell pipeline works, take a look at the following PowerShell example:
PS C:\> get-process bash | format-table id -autosize Id -- 3628 PS C:\> |
Like the Bash example, the goal of this PowerShell example is to display the PID for the bash process. First, information about the bash process is gathered by using the Get-Process cmdlet. Second, the information is piped to the Format-Table cmdlet, which returns a table containing only the PID for the bash process.
The Bash example requires complex shell scripting, but the PowerShell example simply requires formatting a table. As you can see, the structure of PowerShell cmdlets is much easier to understand and use.
Now that you have the PID for the bash process, take a look at the following example, which shows how to kill (stop) that process:
PS C:\> get-process bash | stop-process PS C:\> |
.NET Framework Tips
Before continuing, you need to know a few points about how PowerShell interacts with the .NET Framework. This information is critical to understanding the scripts you review in later chapters.
New-Object cmdlet
You use the New-Object cmdlet to create an instance of a .NET object. To do this, you simply provide the fully qualified name of the .NET class you want to use, as shown:
PS C:\> $Ping = new-object Net.NetworkInformation.Ping PS C:\> |
By using the New-Object cmdlet, you now have an instance of the Ping class that enables you to detect whether a remote computer can be reached via Internet Control Message Protocol (ICMP). Therefore, you have an object-based version of the Ping.exe command-line tool.
If you're wondering what the replacement is for the VBScript CreateObject method, it's the New-Object cmdlet. You can also use the comObject switch with this cmdlet to create a COM object, simply by specifying the object's programmatic identifier (ProgID), as shown here:
PS C:\> $IE = new-object -comObject InternetExplorer.Application PS C:\> $IE.Visible=$True PS C:\> $IE.Navigate("www.cnn.com") PS C:\> |
Square Brackets
Throughout this book, you'll notice the use of square brackets ([ and ]), which indicate that the enclosed term is a .NET Framework reference. These references can be one of the following:
- A fully qualified class name—[System.DirectoryServices.ActiveDirectory.Forest], for example
- A class in the System namespace—[string], [int], [boolean], and so forth
- A type accelerator—[ADSI], [WMI], [Regex], and so on
Defining a variable is a good example of when to use a .NET Framework reference. In this case, the variable is assigned an enumeration value by using an explicit cast of a .NET class, as shown in this example:
PS C:\> $SomeNumber = [int]1 PS C:\> $Identity = [System.Security.Principal.NTAccount]"Administrator" PS C:\> |
If an enumeration can consist of only a fixed set of constants, and you don't know these constants, you can use the System.Enum class's GetNames method to find this information:
PS C:\> [enum]::GetNames([System.Security.AccessControl.FileSystemRights]) ListDirectory ReadData WriteData CreateFiles CreateDirectories AppendData ReadExtendedAttributes WriteExtendedAttributes Traverse ExecuteFile DeleteSubdirectoriesAndFiles ReadAttributes WriteAttributes Write Delete ReadPermissions Read ReadAndExecute Modify ChangePermissions TakeOwnership Synchronize FullControl PS C:\> |
Static Classes and Methods
Square brackets are used not only for defining variables, but also for using or calling static members of a .NET class. To do this, just use a double colon (::) between the class name and the static method or property, as shown in this example:
PS C:\> [System.DirectoryServices.ActiveDirectory.Forest]:: GetCurrentForest() Name : taosage.internal Sites : {HOME} Domains : {taosage.internal} GlobalCatalogs : {sol.taosage.internal} ApplicationPartitions : {DC=DomainDnsZones,DC=taosage,DC=internal, DC=ForestDns Zones,DC=taosage,DC=internal} ForestMode : Windows2003Forest RootDomain : taosage.internal Schema : CN=Schema,CN=Configuration,DC=taosage,DC=internal SchemaRoleOwner : sol.taosage.internal NamingRoleOwner : sol.taosage.internal PS C:\> |
Reflection
Reflection is a feature in the .NET Framework that enables developers to examine objects and retrieve their supported methods, properties, fields, and so on. Because PowerShell is built on the .NET Framework, it provides this feature, too, with the Get-Member cmdlet. This cmdlet analyzes an object or collection of objects you pass to it via the pipeline. For example, the following command analyzes the objects returned from the Get-Process cmdlet and displays their associated properties and methods:
PS C:\> get-process | get-member |
Developers often refer to this process as "interrogating" an object. It's a faster way to get information about objects than using the Get-Help cmdlet (which at the time of this writing provides limited information), reading the MSDN documentation, or searching the Internet.
PS C:\> get-process | get-member TypeName: System.Diagnostics.Process Name MemberType Definition ---- ---------- ---------- Handles AliasProperty Handles = Handlecount Name AliasProperty Name = ProcessName NPM AliasProperty NPM = NonpagedSystemMemorySize PM AliasProperty PM = PagedMemorySize VM AliasProperty VM = VirtualMemorySize WS AliasProperty WS = WorkingSet add_Disposed Method System.Void add_Disposed(Event... add_ErrorDataReceived Method System.Void add_ErrorDataRecei... add_Exited Method System.Void add_Exited(EventHa... add_OutputDataReceived Method System.Void add_OutputDataRece... BeginErrorReadLine Method System.Void BeginErrorReadLine() BeginOutputReadLine Method System.Void BeginOutputReadLine() CancelErrorRead Method System.Void CancelErrorRead() CancelOutputRead Method System.Void CancelOutputRead() Close Method System.Void Close() CloseMainWindow Method System.Boolean CloseMainWindow() CreateObjRef Method System.Runtime.Remoting.ObjRef... Dispose Method System.Void Dispose() Equals Method System.Boolean Equals(Object obj) get_BasePriority Method System.Int32 get_BasePriority() get_Container Method System.ComponentModel.IContain... get_EnableRaisingEvents Method System.Boolean get_EnableRaisi... ... __NounName NoteProperty System.String __NounName=Process BasePriority Property System.Int32 BasePriority {get;} Container Property System.ComponentModel.IContain... EnableRaisingEvents Property System.Boolean EnableRaisingEv... ExitCode Property System.Int32 ExitCode {get;} ExitTime Property System.DateTime ExitTime {get;} Handle Property System.IntPtr Handle {get;} HandleCount Property System.Int32 HandleCount {get;} HasExited Property System.Boolean HasExited {get;} Id Property System.Int32 Id {get;} MachineName Property System.String MachineName {get;} MainModule Property System.Diagnostics.ProcessModu... MainWindowHandle Property System.IntPtr MainWindowHandle... MainWindowTitle Property System.String MainWindowTitle ... MaxWorkingSet Property System.IntPtr MaxWorkingSet {g... MinWorkingSet Property System.IntPtr MinWorkingSet {g... ... Company ScriptProperty System.Object Company {get=$th... CPU ScriptProperty System.Object CPU {get=$this.T... Description ScriptProperty System.Object Description {get... FileVersion ScriptProperty System.Object FileVersion {get... Path ScriptProperty System.Object Path {get=$this.... Product ScriptProperty System.Object Product {get=$th... ProductVersion ScriptProperty System.Object ProductVersion {... PS C:\> |
This example shows that objects returned from the Get-Process cmdlet have additional property information that you didn't know. The following example uses this information to produce a report about Microsoft-owned processes and their folder locations. An example of such a report would be as follows:
PS C:\> get-process | where-object {$_.Company -match ".*Microsoft*"} | format-table Name, ID, Path -Autosize Name Id Path ---- -- ---- ctfmon 4052 C:\WINDOWS\system32\ctfmon.exe explorer 3024 C:\WINDOWS\Explorer.EXE iexplore 2468 C:\Program Files\Internet Explorer\iexplore.exe iexplore 3936 C:\Program Files\Internet Explorer\iexplore.exe mobsync 280 C:\WINDOWS\system32\mobsync.exe notepad 1600 C:\WINDOWS\system32\notepad.exe notepad 2308 C:\WINDOWS\system32\notepad.exe notepad 2476 C:\WINDOWS\system32\NOTEPAD.EXE notepad 2584 C:\WINDOWS\system32\notepad.exe OUTLOOK 3600 C:\Program Files\Microsoft Office\OFFICE11\OUTLOOK.EXE powershell 3804 C:\Program Files\Windows PowerShell\v1.0\powershell.exe WINWORD 2924 C:\Program Files\Microsoft Office\OFFICE11\WINWORD.EXE PS C:\> |
You wouldn't get nearly this much process information by using WSH with only a single line of code.
The Get-Member cmdlet isn't just for objects generated from PowerShell cmdlets. You can also use it on objects initialized from .NET classes, as shown in this example:
PS C:\> new-object System.DirectoryServices.DirectorySearcher |
The goal of using the DirectorySearcher class is to retrieve user information from Active Directory, but you don't know what methods the returned objects support. To retrieve this information, run the Get-Member cmdlet against a variable containing the mystery objects, as shown in this example.
PS C:\> $Searcher = new-object System.DirectoryServices.DirectorySearcher PS C:\> $Searcher | get-member TypeName: System.DirectoryServices.DirectorySearcher Name MemberType Definition ---- ---------- ---------- add_Disposed Method System.Void add_Disposed(EventHandle... CreateObjRef Method System.Runtime.Remoting.ObjRef Creat... Dispose Method System.Void Dispose() Equals Method System.Boolean Equals(Object obj) FindAll Method System.DirectoryServices.SearchResul... FindOne Method System.DirectoryServices.SearchResul... ... Asynchronous Property System.Boolean Asynchronous {get;set;} AttributeScopeQuery Property System.String AttributeScopeQuery {g... CacheResults Property System.Boolean CacheResults {get;set;} ClientTimeout Property System.TimeSpan ClientTimeout {get;s... Container Property System.ComponentModel.IContainer Con... DerefAlias Property System.DirectoryServices.Dereference... DirectorySynchronization Property System.DirectoryServices.DirectorySy... ExtendedDN Property System.DirectoryServices.ExtendedDN ... Filter Property System.String Filter {get;set;} PageSize Property System.Int32 PageSize {get;set;} PropertiesToLoad Property System.Collections.Specialized.Strin... PropertyNamesOnly Property System.Boolean PropertyNamesOnly {ge... ReferralChasing Property System.DirectoryServices.ReferralCha... SearchRoot Property System.DirectoryServices.DirectoryEn... SearchScope Property System.DirectoryServices.SearchScope... SecurityMasks Property System.DirectoryServices.SecurityMas... ServerPageTimeLimit Property System.TimeSpan ServerPageTimeLimit ... ServerTimeLimit Property System.TimeSpan ServerTimeLimit {get... Site Property System.ComponentModel.ISite Site {ge... SizeLimit Property System.Int32 SizeLimit {get;set;} Sort Property System.DirectoryServices.SortOption ... Tombstone Property System.Boolean Tombstone {get;set;} VirtualListView Property System.DirectoryServices. DirectoryVi... PS C:\> |
Notice the FindAll method and the Filter property. These are object attributes that can be used to search for information about users in an Active Directory domain. To use these attributes the first step is to filter the information returned from DirectorySearcher by using the Filter property, which takes a filter statement similar to what you'd find in a Lightweight Directory Access Protocol (LDAP) statement:
PS C:\> $Searcher.Filter = ("(objectCategory=user)") |
Next, you retrieve all users from the Active Directory domain with the FindAll method:
PS C:\> $Users = $Searcher.FindAll() |
At this point, the $Users variable contains a collection of objects holding the distinguished names for all users in the Active Directory domain:
PS C:\> $Users Path Properties ---- ---------- LDAP://CN=Administrator,CN=Users,DC=... {homemdb, samaccounttype, countrycod... LDAP://CN=Guest,CN=Users,DC=taosage,... {samaccounttype, objectsid, whencrea... LDAP://CN=krbtgt,CN=Users,DC=taosage... {samaccounttype, objectsid, whencrea... LDAP://CN=admintyson,OU=Admin Accoun... {countrycode, cn, lastlogoff, usncre... LDAP://CN=servmom,OU=Service Account... {samaccounttype, lastlogontimestamp,... LDAP://CN=SUPPORT_388945a0,CN=Users,... {samaccounttype, objectsid, whencrea... LDAP://CN=Tyson,OU=Acc... {msmqsigncertificates, distinguished... LDAP://CN=Maiko,OU=Acc... {homemdb, msexchhomeservername, coun... LDAP://CN=servftp,OU=Service Account... {samaccounttype, lastlogontimestamp,... LDAP://CN=Erica,OU=Accounts,OU... {samaccounttype, lastlogontimestamp,... LDAP://CN=Garett,OU=Accou... {samaccounttype, lastlogontimestamp,... LDAP://CN=Fujio,OU=Accounts,O... {samaccounttype, givenname, sn, when... LDAP://CN=Kiyomi,OU=Accounts,... {samaccounttype, givenname, sn, when... LDAP://CN=servsql,OU=Service Account... {samaccounttype, lastlogon, lastlogo... LDAP://CN=servdhcp,OU=Service Accoun... {samaccounttype, lastlogon, lastlogo... LDAP://CN=servrms,OU=Service Account... {lastlogon, lastlogontimestamp, msmq... PS C:\> |
Now that you have an object for each user, you can use the Get-Member cmdlet to learn what you can do with these objects:
PS C:\> $Users | get-member TypeName: System.DirectoryServices.SearchResult Name MemberType Definition ---- ---------- ---------- Equals Method System.Boolean Equals(Object obj) get_Path Method System.String get_Path() get_Properties Method System.DirectoryServices.ResultPropertyCollecti... GetDirectoryEntry Method System.DirectoryServices.DirectoryEntry GetDire... GetHashCode Method System.Int32 GetHashCode() GetType Method System.Type GetType() ToString Method System.String ToString() Path Property System.String Path {get;} Properties Property System.DirectoryServices.ResultPropertyCollecti... PS C:\> |
To collect information from these user objects, it seems as though you need to step through each object with the GetDirectoryEntry method. To determine what data you can retrieve from these objects, you use the Get-Member cmdlet again, as shown here:
PS C:\> $Users[0].GetDirectoryEntry() | get-member -MemberType Property TypeName: System.DirectoryServices.DirectoryEntry Name MemberType Definition ---- ---------- ---------- accountExpires Property System.DirectoryServices.Property... adminCount Property System.DirectoryServices.Property... badPasswordTime Property System.DirectoryServices.Property... badPwdCount Property System.DirectoryServices.Property... cn Property System.DirectoryServices.Property... codePage Property System.DirectoryServices.Property... countryCode Property System.DirectoryServices.Property... description Property System.DirectoryServices.Property... displayName Property System.DirectoryServices.Property... distinguishedName Property System.DirectoryServices.Property... homeMDB Property System.DirectoryServices.Property... homeMTA Property System.DirectoryServices.Property... instanceType Property System.DirectoryServices.Property... isCriticalSystemObject Property System.DirectoryServices.Property... lastLogon Property System.DirectoryServices.Property... lastLogonTimestamp Property System.DirectoryServices.Property... legacyExchangeDN Property System.DirectoryServices.Property... logonCount Property System.DirectoryServices.Property... mail Property System.DirectoryServices.Property... mailNickname Property System.DirectoryServices.Property... mDBUseDefaults Property System.DirectoryServices.Property... memberOf Property System.DirectoryServices.Property... msExchALObjectVersion Property System.DirectoryServices.Property... msExchHomeServerName Property System.DirectoryServices.Property... msExchMailboxGuid Property System.DirectoryServices.Property... msExchMailboxSecurityDescriptor Property System.DirectoryServices.Property... msExchPoliciesIncluded Property System.DirectoryServices.Property... msExchUserAccountControl Property System.DirectoryServices.Property... mSMQDigests Property System.DirectoryServices.Property... mSMQSignCertificates Property System.DirectoryServices.Property... name Property System.DirectoryServices.Property... nTSecurityDescriptor Property System.DirectoryServices.Property... objectCategory Property System.DirectoryServices.Property... objectClass Property System.DirectoryServices.Property... objectGUID Property System.DirectoryServices.Property... objectSid Property System.DirectoryServices.Property... primaryGroupID Property System.DirectoryServices.Property... proxyAddresses Property System.DirectoryServices.Property... pwdLastSet Property System.DirectoryServices.Property... sAMAccountName Property System.DirectoryServices.Property... sAMAccountType Property System.DirectoryServices.Property... showInAddressBook Property System.DirectoryServices.Property... textEncodedORAddress Property System.DirectoryServices.Property... userAccountControl Property System.DirectoryServices.Property... uSNChanged Property System.DirectoryServices.Property... uSNCreated Property System.DirectoryServices.Property... whenChanged Property System.DirectoryServices.Property... whenCreated Property System.DirectoryServices.Property... PS C:\> |
To use PowerShell effectively, you should make sure you're familiar with the Get-Member cmdlet. If you don't understand how it works, figuring out what an object can and can't do may be at times difficult.
Now that you understand how to pull information from Active Directory, it's time to put together all the commands used so far:
PS C:\> $Searcher = new-object System.DirectoryServices.DirectorySearcher PS C:\> $Searcher.Filter = ("(objectCategory=user)") PS C:\> $Users = $Searcher.FindAll() PS C:\> foreach ($User in $Users){$User.GetDirectoryEntry().sAMAccountName} Administrator Guest krbtgt admintyson servmom SUPPORT_388945a0 Tyson Maiko servftp Erica Garett Fujio Kiyomi servsql servdhcp servrms PS C:\> |
Although the list of users in this domain isn't long, it shows that you can interrogate a set of objects to understand their capabilities.
The same is true for static classes, however, when attempting to use the Get-Member cmdlet in the same manner as before creates the following error:
PS C:\> new-object System.Net.Dns New-Object : Constructor not found. Cannot find an appropriate constructor for type System.Net.Dns. At line:1 char:11 + New-Object <<<< System.Net.Dns PS C:\> |
As you can see, the System.Net.Dns class doesn't have a constructor, which poses a challenge when you're trying to find out what this class does. However, the Get-Member cmdlet can handle this challenge. With the Static parameter, you can gather information from static classes, as shown in this example:
PS C:\> [System.Net.Dns] | get-member -Static TypeName: System.Net.Dns Name MemberType Definition ---- ---------- ---------- BeginGetHostAddresses Method static System.IAsyncResult BeginGetHostAddr... BeginGetHostByName Method static System.IAsyncResult BeginGetHostByNa... BeginGetHostEntry Method static System.IAsyncResult BeginGetHostEntr... BeginResolve Method static System.IAsyncResult BeginResolve(Str... EndGetHostAddresses Method static System.Net.IPAddress[] EndGetHostAdd... EndGetHostByName Method static System.Net.IPHostEntry EndGetHostByN... EndGetHostEntry Method static System.Net.IPHostEntry EndGetHostEnt... EndResolve Method static System.Net.IPHostEntry EndResolve(IA... Equals Method static System.Boolean Equals(Object objA, O... GetHostAddresses Method static System.Net.IPAddress[] GetHostAddres... GetHostByAddress Method static System.Net.IPHostEntry GetHostByAddr... GetHostByName Method static System.Net.IPHostEntry GetHostByName... GetHostEntry Method static System.Net.IPHostEntry GetHostEntry(... GetHostName Method static System.String GetHostName() ReferenceEquals Method static System.Boolean ReferenceEquals(Objec... Resolve Method static System.Net.IPHostEntry Resolve(Strin... PS C:\> |
Now that you have information about the System.Net.Dns class, you can put it to work. As an example, use the GetHostAddress method to resolve the IP address for the Web site www.digg.com:
PS C:\> [System.Net.Dns]::GetHostAddresses("www.digg.com") IPAddressToString : 64.191.203.30 Address : 516669248 AddressFamily : InterNetwork ScopeId : IsIPv6Multicast : False IsIPv6LinkLocal : False IsIPv6SiteLocal : False PS C:\> |
Extended Type System (ETS)
You might think that scripting in PowerShell is typeless because you rarely need to specify the type for a variable. PowerShell is actually type driven, however, because it interfaces with different types of objects from the less than perfect .NET to Windows Management Instrumentation (WMI), Component Object Model (COM), ActiveX Data Objects (ADO), Active Directory Service Interfaces (ADSI), Extensible Markup Language (XML), and even custom objects. However, you typically don't need to be concerned about object types because PowerShell adapts to different object types and displays its interpretation of an object for you.
In a sense, PowerShell tries to provide a common abstraction layer that makes all object interaction consistent, despite the type. This abstraction layer is called the PSObject, a common object used for all object access in PowerShell. It can encapsulate any base object (.NET, custom, and so on), any instance members, and implicit or explicit access to adapted and type-based extended members, depending on the type of base object. Furthermore, it can state its type and add members dynamically. To do this, PowerShell uses the Extended Type System (ETS), which provides an interface that allows PowerShell cmdlet and script developers to manipulate and change objects as needed.
Therefore, with ETS, you can change objects by adapting their structure to your requirements or create new ones. One way to manipulate objects is to adapt (extend) existing object types or create new object types. To do this, you define custom types in a custom types file, based on the structure of the default types file, Types.ps1xml.
In the Types.ps1xml file, all types are contained in a <Type></Type> node, and each type can contain standard members, data members, and object methods. Using this structure as a basis, you can create your own custom types file and load it into a PowerShell session by using the Update-TypeData cmdlet, as shown here:
PS C:\> Update-TypeData D:\PS\My.Types.Ps1xml |
You can run this command manually during each PowerShell session or add it to your profile.ps1 file.
The second way to manipulate an object's structure is to use the Add-Member cmdlet to add a user-defined member to an existing object instance, as shown in this example:
PS C:\> $Procs = get-process PS C:\> $Procs | add-member -Type scriptProperty "TotalDays" { >> $Date = get-date >> $Date.Subtract($This.StartTime).TotalDays} >> PS C:\> |
This code creates a scriptProperty member called TotalDays for the collection of objects in the $Procs variable. The scriptProperty member can then be called like any other member for those objects, as shown in the next example:
PS C:\> $Procs | where {$_.name -Match "WINWORD"} | ft Name, TotalDays -AutoSize Name TotalDays ---- --------- WINWORD 5.1238899696898148 PS C:\> |
Although the new scriptProperty member isn't particularly useful, it does demonstrate how to extend an object. Being able to extend objects from both a scripting and cmdlet development context is extremely useful.