You'll recall from that PowerShell's pipeline allows object data to be passed between cmdlets.   This section discusses the key built-in object manipulation cmdlets that allow you to "slice and dice" object data before passing it along the pipeline, where it may be subsequently used for further cmdlet input or used to generate some kind of output, for example, a text file, CSV file, XML file, console output, HTTPS stream, Etc.

We'll begin by discussing the three most fundamental, and therefore most commonly used, object manipulation cmdlets (, and ).

P I P E L I N E Input Object(s) Where-Object Rows/Objects Filtered Select-Object Columns/Properties Filtered Sort-Object Rows/Objects Sorted

We need some form of input data in order to use (and experiment with) the Powershell pipeline.  For the purposes of this primer, we'll be using a small selection of Powershell's Get cmdlets, for example, Get-Process, Get-Service and Get-WmiObject.   PowerShell provides many cmdlets for retrieving system or application data, with the actual number of cmdlets being dependent on the version of Powershell being used and the currently loaded and snap-ins.

The first of the "slice and dice" operations is achieved using Where-Object.  This cmdlet achieves the same result as SQL's where clause, filtering the required object instances (think rows) from the pipeline.  Only objects that match the criteria provided will continue to be passed along the pipeline.

The example below retrieves all Windows Services whose name begins with "W" that are in a running state:

PS C:\Users\JohnDoe> Get-Service | Where-Object { ($_.ServiceName -like 'w*') -and ($_.Status -eq 'Running') } Status Name DisplayName ------ ---- ----------- Running Wcmsvc Windows Connection Manager Running wcncsvc Windows Connect Now - Config Registrar Running WdiServiceHost Diagnostic Service Host Running WdiSystemHost Diagnostic System Host Running WdNisSvc Windows Defender Network Inspection... Running WinDefend Windows Defender Service Running WinHttpAutoProx... WinHTTP Web Proxy Auto-Discovery Se... Running Winmgmt Windows Management Instrumentation Running WlanSvc WLAN AutoConfig Running WpnService Windows Push Notifications System S... Running wscsvc Security Center Running WSearch Windows Search Running wuauserv Windows Update Running wudfsvc Windows Driver Foundation - User-mo... PS C:\Users\JohnDoe>

In order to be able to apply a Where-Object operation, we must first know which properties are available for the object class that we are dealing with.  Using the above example, how did we know that the ServiceName and Status properties were available for the objects returned by Get-Service and how did we know that Running was a valid status value?   The answer was first discussed in section , where we used a combination of Get-Member and the Out-GridView cmdlets to explore object classes.  To recap, the Get-Member cmdlet can be used to view available methods and properties for a given class, for example:

PS C:\Users\JohnDoe> Get-Service | Get-Member -MemberType Property; TypeName: System.ServiceProcess.ServiceController Name MemberType Definition ---- ---------- ---------- CanPauseAndContinue Property bool CanPauseAndContinue {get;} CanShutdown Property bool CanShutdown {get;} CanStop Property bool CanStop {get;} Container Property System.ComponentModel.IContainer Container {get;} DependentServices Property System.ServiceProcess.ServiceController[] DependentServices {get;} DisplayName Property string DisplayName {get;set;} MachineName Property string MachineName {get;set;} ServiceHandle Property System.Runtime.InteropServices.SafeHandle ServiceHandle {get;} ServiceName Property string ServiceName {get;set;} ServicesDependedOn Property System.ServiceProcess.ServiceController[] ServicesDependedOn {get;} ServiceType Property System.ServiceProcess.ServiceType ServiceType {get;} Site Property System.ComponentModel.ISite Site {get;set;} StartType Property System.ServiceProcess.ServiceStartMode StartType {get;} Status Property System.ServiceProcess.ServiceControllerStatus Status {get;} PS C:\Users\JohnDoe>

And the Out-GridView cmdlet provides a useful method of viewing an object's property values, for example:

PS C:\Users\JohnDoe> Get-Service | Select-Object -Property * | Out-GridView; PS C:\Users\JohnDoe>

Using the above information, we can see that the Get-Service cmdlet returns numerous object properties.   This information allows us to build suitable criteria for our Where-Object cmdlet.

The Where-Object cmdlet has a built-in alias of ?, which can be used for brevity.  However, I strongly recommend not using aliases in production scripts or online examples (see for further information) and instead recommend limiting their use to command-line interaction, for example:

PS C:\Users\JohnDoe> gsv | ? { $_.Status -eq 'Running' } Status Name DisplayName ------ ---- ----------- Running AdobeUpdateService AdobeUpdateService Running AGSService Adobe Genuine Software Integrity Se... Running Appinfo Application Information ... ... ... Running WSearch Windows Search Running WTabletServicePro Wacom Professional Service Running wudfsvc Windows Driver Foundation - User-mo... PS C:\Users\JohnDoe>

More importantly, PowerShell V3.0 introduced a shorthand variation of the Where-Object cmdlet, that is more akin to natural language and reads like SQL's where clause.  The syntax of this variation is very prescriptive and requires that:

The example below demonstrates this shorthand form and uses the where alias in place of Where-Object:

PS C:\Users\JohnDoe> Get-Process | Where WorkingSet -gt 150MB; Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName ------- ------ ----- ----- ------ -- -- ----------- 1380 275 373348 393832 725.67 3892 10 iexplore 746 66 162656 157664 1,640.55 2400 0 MsMpEng 656 42 231580 249272 11.41 11388 10 powershell PS C:\Users\JohnDoe>

When dealing with data it is always a good idea to dispense with unwanted data as early as possible in order to reduce the amount of in-memory data operations (moves, copies, Etc.) as possible.  This holds true with PowerShell's pipeline.   Wherever possible, a filter operation should be applied as early as possible.  Thankfully, most of PowerShell's Get cmdlets support one or more filtering parameters.  The table below lists just a few examples:

Cmdlet Filtering Parameter Example
Get-ADUser -Filter Get-ADUser -Filter {(ObjectClass -eq 'user') -and (mail -like '*.powershellprimer.com')};
Get-ADUser -LDAPFilter Get-ADUser -LDAPFilter (&(objectCategory=person)(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=2));
Get-ChildItem -Filter Get-ChildItem -LiteralPath C:\ -Recurse -Filter *.xml;
Get-NetTCPConnection -State Get-NetTCPConnection -State Listen;
Get-Service -Include Get-Service -Include @('a*', 'w*');
Get-Service -DependentServices Get-Service -DependentServices 'rpcss';
Get-WmiObject -Filter Get-WmiObject -Class Win32_Volume -Filter 'DriveType = 3';

The performance characteristics of various coding techniques is often discussed (or argued) at great length.  However, the most pragmatic way to determine the relative efficiencies of different coding techniques is to actually take measurements.   It's for this reason that PowerShell V3.0 (and above) includes the cmdlet, which can be used to generate empirical performance metrics.  For example, the code examples below demonstrate the time taken to enumerate all .XML files in %SystemRoot% on a given reference system.  The first example performs the file-type filter as a post-enumeration operation, whereas the second example includes the filtering as part of the file enumeration operation:

PS C:\Users\JohnDoe> Measure-Command -Expression { $objXMLFiles = Get-ChildItem -LiteralPath $Env:SystemRoot -Recurse -ErrorAction silentlyContinue | Where-Object { $_.Extension -eq '.xml' } } Days : 0 Hours : 0 Minutes : 0 Seconds : 41 Milliseconds : 930 Ticks : 419304458 TotalDays : 0.000485306085648148 TotalHours : 0.0116473460555556 TotalMinutes : 0.698840763333333 TotalSeconds : 41.9304458 TotalMilliseconds : 41930.4458 PS C:\Users\JohnDoe>
PS C:\Users\JohnDoe> Measure-Command -Expression { $objXMLFiles = Get-ChildItem -LiteralPath $Env:SystemRoot -Filter *.xml -Recurse -ErrorAction silentlyContinue; } Days : 0 Hours : 0 Minutes : 0 Seconds : 9 Milliseconds : 479 Ticks : 94798965 TotalDays : 0.000109721024305556 TotalHours : 0.00263330458333333 TotalMinutes : 0.157998275 TotalSeconds : 9.4798965 TotalMilliseconds : 9479.8965 PS C:\Users\JohnDoe>

As you can see, the second technique is nearly 4½ times faster than the first.  Similar timings can be observed with other data operations and it is therefore very important for the developer to consider what data is being manipulated and what filtering options are available.  And, of course, it's also important to actually carry out performance testing rather than make assumptions.

The second of the "slice and dice" operations is achieved using Select-Object.  Whereas Where-Object filtered just rows, the Select-Object cmdlet can be used to filter rows and object properties (think columns).   Once again, to make good use of this cmdlet, we must first understand which columns are available.  And again, we can use the techniques described above to achieve this.

The row filtering capabilities of Select-Object are as follows:

Filtering Parameter Description Example
-First Returns the first n rows/objects passed along the pipeline. Get-EventLog -LogName System -EntryType Error | Select-Object -First 10;
-Index Returns specific rows/objects, based on their position in the data being passed along the pipeline. Get-WmiObject -Class Win32_SystemSlot | Select-Object -Index 0, 1, 2;
-Last Returns the last n rows/objects passed along the pipeline. (Invoke-WebRequest -Uri 'https://www.google.co.uk/#q=Hello%2C+World!').Links | Select-Object -Last 5;
-Skip Skips the first n rows/objects passed along the pipeline. Get-ChildItem -File -Filter *.queued | Sort-Object -Property LastWriteTime -Descending | Select-Object -Skip 1;
-Unique Returns a list of unique row/object values based on the given property. Get-Process | Select-Object -Unique ProcessName;

The -First, -Last and -Skip parameters can be combined, for example:

PS C:\Users\JohnDoe> Get-Content -LiteralPath "$Env:SystemRoot:\Windows\Logs\CBS\CBS.log" | Select-Object -First 5 -Last 5; 2015-10-24 20:31:01, Info CBS TI: --- Initializing Trusted Installer --- 2015-10-24 20:31:45, Info CBS TI: Last boot time: 2015-10-23 21:26:28.126 2015-10-24 20:31:45, Info CBS Starting TrustedInstaller initialization. 2015-10-24 20:31:45, Info CBS Ending TrustedInstaller initialization. 2015-10-24 20:31:45, Info CBS Starting the TrustedInstaller main loop. 2015-10-25 19:21:07, Info CBS Starting TrustedInstaller finalization. 2015-10-25 19:21:07, Info CBS Winlogon: Simplifying Winlogon CreateSession notifications 2015-10-25 19:21:07, Info CBS Winlogon: Stopping notify server 2015-10-25 19:21:07, Info CBS Winlogon: Unloading SysNotify DLL 2015-10-25 19:21:07, Info CBS Ending TrustedInstaller finalization. PS C:\Users\JohnDoe>

The Select-Object cmdlet's primary purpose is filtering object properties (again, think columns), much like we do with an SQL select statement, for example:

PS C:\Users\JohnDoe> Get-Process -name svchost | Select-Object -Property ProcessName, Id, PrivateMemorySize; ProcessName Id PrivateMemorySize ----------- -- ----------------- svchost 336 14659584 svchost 592 15708160 svchost 828 10010624 svchost 892 8634368 svchost 1000 63254528 svchost 1016 14921728 svchost 1196 9568256 svchost 1204 23302144 svchost 1748 8384512 svchost 1852 2998272 svchost 2024 2195456 svchost 2316 15265792 svchost 2452 8368128 svchost 3008 5111808 PS C:\Users\JohnDoe>

Here, we've returned just the ProcessName, process Id (PID) and PrivateMemorySize (in bytes) values for all SVCHOST.EXE processes.

Probably the most powerful aspect of the Select-Object cmdlet is its ability to add calculated properties to objects in the pipeline.  The format for adding a calculated property is as follows:

@{ Label='<PROPERTY_LABEL>'; Expression={ <SCRIPT_BLOCK> } }

...where:

A simple example of this capability involves using a calculated property to change the way an existing property is presented.   For example, we can easily use Get-Process to return the process name, process ID and private memory size of each svchost process:

PS C:\Users\JohnDoe> Get-Process -Name svchost | Select-Object -Property ProcessName, Id, PrivateMemorySize; ProcessName Id PrivateMemorySize ----------- -- ----------------- svchost 408 18604032 svchost 788 7372800 svchost 828 10321920 svchost 892 10825728 svchost 1012 55934976 svchost 1052 12857344 svchost 1200 18685952 svchost 1208 7892992 svchost 1772 8859648 svchost 1816 2596864 svchost 1972 2273280 svchost 2324 11771904 svchost 2412 10633216 svchost 3184 4833280 svchost 4736 14254080 svchost 9636 3805184 PS C:\Users\JohnDoe>

But, what if we wanted the private memory size to be reported in megabytes (MB) instead?  This is where a calculated property can be used.  In this case, we'll create a new property which divides the existing private memory size by 1MB (see ).   This can be seen below:

PS C:\Users\JohnDoe> Get-Process -Name svchost | Select-Object -Property ProcessName, Id, PrivateMemorySize, @{ Label='PrivateMemorySize(MB)'; Expression={ [Math]::Round($_.PrivateMemorySize / 1MB, 2); } } ProcessName Id PrivateMemorySize PrivateMemorySize(MB) ----------- -- ----------------- --------------------- svchost 408 17252352 16.45 svchost 788 7188480 6.86 svchost 828 8744960 8.34 svchost 892 6275072 5.98 svchost 1012 31977472 30.5 svchost 1052 11730944 11.19 svchost 1200 19177472 18.29 svchost 1208 7647232 7.29 svchost 1772 8687616 8.29 svchost 1816 2588672 2.47 svchost 1972 2134016 2.04 svchost 2324 12095488 11.54 svchost 2412 6557696 6.25 svchost 3184 3780608 3.61 PS C:\Users\JohnDoe>

We can then dispense with the original PrivateMemorySize property (column):

PS C:\Users\JohnDoe> Get-Process -Name svchost | Select-Object -Property ProcessName, Id, @{ Label='PrivateMemorySize(MB)'; Expression={ [Math]::Round($_.PrivateMemorySize / 1MB, 2); } } ProcessName Id PrivateMemorySize(MB) ----------- -- --------------------- svchost 408 16.45 svchost 788 6.86 svchost 828 8.34 svchost 892 5.98 svchost 1012 30.5 svchost 1052 11.19 svchost 1200 18.29 svchost 1208 7.29 svchost 1772 8.29 svchost 1816 2.47 svchost 1972 2.04 svchost 2324 11.54 svchost 2412 6.25 svchost 3184 3.61 PS C:\Users\JohnDoe>
# # List all running services with their underlying process' ID and name. # Select-Object -Property DisplayName, ProcessId, @{ Label='Process'; Expression={ (Get-WmiObject -Class Win32_Process -Filter ('ProcessId = {0}' -f $_.ProcessId)).Name } }
# # List the top 10 busiest processes and their CPU utilisation (%). In this example, CPU # utilisation isn't reported internally as a percentage, so we have to find the total # usage and then calculate the respective percentage usage for each process. # $objProcesses = Get-WmiObject Win32_PerfRawData_PerfProc_Process; ` $intTotalUtil = $objProcesses[-1].PercentProcessorTime; $objProcesses | ` Where-Object{ $_.Name -ne '_Total' } | ` Select-Object -Property Name, @{ Label='CPU%'; Expression={ [Math]::Round($_.PercentProcessorTime / $intTotalUtil * 100, 2)}} | ` Sort-Object -Property CPU% -Descending | ` Select-Object -First 10;
# # List all listening ports and underlying process. # Get-NetTCPConnection -State 'Listen' | ` Select-Object LocalAddress, LocalPort, OwningProcess, @{ Label = 'OwningProcessName'; Expression = { (Get-WmiObject -Class Win32_Process -Filter ('ProcessId = {0}' -f $_.OwningProcess)).Name } } | ` Sort-Object -Property LocalPort;

The Sort-Object cmdlet simply performs a sort operation against the object data being passed along the pipeline.  The sort evaluation is performed against the property (or properties) specified by the -Property parameter.  By default, the sort operation is case-insensitive and returns results in ascending order; use the -CaseSensitive parameter to enforce case-sensitivity and the -Descending parameter to reverse the sort order.  Finally, use the -Unique parameter to remove duplicate values prior to sorting.

The example below returns all .EXE files in %SystemRoot%\System32 and orders the result by file size (largest first):

PS C:\Users\JohnDoe> Get-ChildItem -LiteralPath "$Env:SystemRoot\System32" -Filter *.exe | Sort-Object -Property Length -Descending; Directory: C:\WINDOWS\System32 Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 16/10/2016 09:37 143495576 MRT.exe -a---- 15/10/2016 05:48 7817568 ntoskrnl.exe -a---- 05/10/2016 10:16 6664192 mspaint.exe ... ... ... -a---- 16/07/2016 12:42 10240 plasrv.exe -a---- 16/07/2016 12:43 10240 WebCache.exe -a---- 16/07/2016 12:42 9728 dpnsvr.exe PS C:\Users\JohnDoe>

Up until now, we have been manipulating the object data being passed along the pipeline.  We've used Where-Object and Select-Object to filter the object data and Sort-Object to re-order (sort) the object data.  As already discussed in the section, if we do nothing more at this stage, PowerShell will "burst" the object data and render the results to the console as a textual representation.

However, if we wish to do something with the object data, we can use the Foreach-Object cmdlet.  The example below uses Get-ChildItem to enumerate all .XML files in a given directory.  Then, using Foreach-Object, it carries out an operation against each file that is found.  In this particular case, the operation begins by determining if the current file is locked (we use Rename-Item to ascertain this).  If the file is locked, a warning message is displayed, otherwise, processing continues for the given file.

[String] $Local:strFolderToProcess = 'd:\data\inbox'; Get-ChildItem -File -LiteralPath $strFolderToProcess -Filter *.xml | ForEach-Object -Process { # # Use Rename-Item to detect if the file is locked (we attempt to rename the file # to its current name, i.e.: no change). # Rename-Item -LiteralPath $_.FullName -NewName $_.Name -ErrorAction silentlyContinue; if ( $? ) { Write-Host -Object ( 'Processing file "{0}"...' -f $_.Name ); # # Do something with the file... # } else { Write-Warning -Message ( 'File "{0}" is in use.' -f $_.Name ); } #else-if } #Foreach-Object

As seen above, the current pipeline object being processed can be referenced using the $_ or $PSItem .

Sometimes it might be useful to carry out some kind of pre-processing and/or post-processing when calling Foreach-Object, this can be achieved using the -begin and -end parameters, e.g:

PS C:\Users\JohnDoe> Get-ChildItem -LiteralPath . -Filter *.exe -Recurse | ForEach-Object -Begin { Get-Date } -Process { $_.Name } -End { Get-Date }

The PowerShell code in the -Begin will execute once, before the pipeline objects are enumerated.  The PowerShell code in the -End will execute once, when all of the pipeline objects have been processed.

The Foreach-Object cmdlet also has a built-in alias of %, which, again, can be used for brevity, for example:

PS C:\Users\JohnDoe> gci .\* -File -Include *.jpg, *.jpeg | % { & mspaint $_ }

The above example also uses the gci for Get-ChildItem and launches Microsoft Paint for all JPEG files in the current directory.   Once again, aliases should be reserved for command-line interaction only.

The Tee-Object cmdlet is very similar to the UNIX tee command, in that it concurrently outputs pipeline data to the specified file (or variable) whilst continuing to pass object data along the pipeline.  Although this may be useful in a PowerShell script, it can be particularly useful at the command line, for example:

PS C:\Users\JohnDoe> Get-ChildItem -LiteralPath c:\temp -File -Filter *.txt | Tee-Object -FilePath d:\data\logs\removedfiles.log | Remove-Item; PS C:\Users\JohnDoe>

In this example, we use Get-ChildItem to enumerate all .TXT files in C:\TEMP.  We then use Tee-Object to write the file details to a log file whilst simultaneously passing the file objects along the pipeline to Remove-Item, which removes (deletes) the file(s).

As we've seen, the PowerShell pipeline is a powerful tool.  However, as the adage goes, "With great power comes great responsibility".   It is all to easy to write a "deep" pipeline operation that performs a potentially crippling commit of some sorts in its final stages.   The default action for most cmdlets is to continue upon failure; this means it's possible for a pipeline operation to complete having failed processing one or more objects along the way.

Couple this behaviour with the fact that most of the built-in cmdlets are able to deal with multiple input objects (i.e.: arrays of objects) means that system-altering pipeline operations need to be approached with care.

The simplified example below attempts to demonstrate this.  In this example we create a set of text files, open one of them for writing (therefore creating an exclusive lock) and then attempt to delete them all:

PS C:\Users\JohnDoe> # Generate nine .TXT files, writing the current date/time to each. PS C:\Users\JohnDoe> 1..9 | ForEach-Object { Get-Date | Out-File -LiteralPath ('c:\temp\file{0}.txt' -f $_) } PS C:\Users\JohnDoe> # List the files. PS C:\Users\JohnDoe> Get-ChildItem -LiteralPath c:\temp -File -Filter file*.txt; Directory: C:\temp Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 14/01/2015 20:22 68 file1.txt -a---- 14/01/2015 20:22 68 file2.txt -a---- 14/01/2015 20:22 68 file3.txt -a---- 14/01/2015 20:22 68 file4.txt -a---- 14/01/2015 20:22 68 file5.txt -a---- 14/01/2015 20:22 68 file6.txt -a---- 14/01/2015 20:22 68 file7.txt -a---- 14/01/2015 20:22 68 file8.txt -a---- 14/01/2015 20:22 68 file9.txt PS C:\Users\JohnDoe> # Artificially lock one of the files. PS C:\Users\JohnDoe> $objFile = [System.IO.File]::Open( 'c:\temp\file5.txt', 'Open', 'ReadWrite', 'None' ); PS C:\Users\JohnDoe> # Attempt to remove all of the .TXT files.  Note that Remove-Item will process multiple input objects. PS C:\Users\JohnDoe> Get-ChildItem -LiteralPath c:\temp -File -Filter file*.txt | Remove-Item; Remove-Item : Cannot remove item C:\temp\file5.txt: The process cannot access the file 'C:\temp\file5.txt' because it is being used by another process. At line:1 char:56 + Get-ChildItem -LiteralPath c:\temp -File -Filter file*.txt | Remove-Item; + ~~~~~~~~~~~ + CategoryInfo : WriteError: (C:\temp\file5.txt:FileInfo) [Remove-Item], IOException + FullyQualifiedErrorId : RemoveFileSystemItemIOError,Microsoft.PowerShell.Commands.RemoveItemCommand PS C:\Users\JohnDoe> # Close the file handle. PS C:\Users\JohnDoe> $objFile.Close(); $objFile.Dispose(); PS C:\Users\JohnDoe> # List any remaining .TXT files. PS C:\Users\JohnDoe> Get-ChildItem -LiteralPath c:\temp -File -Filter file*.txt; Directory: C:\temp Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 14/01/2015 20:22 68 file5.txt PS C:\Users\JohnDoe>

As you can see, the locked file could not be deleted, and the Remove-Item call generated an .  However the pipeline operation continued.   In order to prevent the pipeline continuing when an error occurs, we must specify -ErrorAction Inquire or -ErrorAction Stop when calling any cmdlets that may fail.  However, this approach doesn't scale well and isn't appropriate for enterprise-class operations, such as manipulating Microsoft Active Directory user objects, Microsoft Exchange mailboxes or Hyper-V virtual machines en masse.

An arguably more robust method of dealing with large scale enterprise pipeline operations is discussed in section , below.

Most of PowerShell's built-in cmdlets that are capable of changing the system state (for example, those beginning with Add, Clear, Remove, Start, Stop, Etc.) support the -Confirm and -WhatIf parameters.   The -Confirm parameter can be used to explicitly state that the cmdlet must prompt the user for confirmation before executing (specify -Confirm:$true).   The -WhatIf parameter causes PowerShell to show the user what the cmdlet would do, if run without -WhatIf, and is particularly useful for sanity-checking potentially risky command-line operations.

The example below demonstrates the -WhatIf parameter being used to test a directory removal operation and gives the user an opportunity to spot that they may be in the wrong directory.

PS C:\WINDOWS> Get-ChildItem -Name 'E*' -Directory | Remove-Item -Recurse -WhatIf; What if: Performing the operation "Remove Directory" on target "C:\WINDOWS\en-US". PS C:\WINDOWS>

As discussed above, it is often necessary to break out of a PowerShell pipeline before system-affecting operations are carried out.  In this mode of operation, the pipeline is still used to obtain, filter and sort input objects.   However, once we have our candidate objects (our work "queue"), we use a ForEach call (not to be confused with ) to process each item of work in a more programmatic, controlled fashion.  This gives us an opportunity to detect and handle errors in a more robust way.

Continuing with the file deletion example in section , we can type the following to ascertain the object class returned by Get-ChildItem, when dealing with file objects:

PS C:\Users\JohnDoe> (Get-ChildItem -LiteralPath c:\temp -File)[0].GetType().FullName; System.IO.FileInfo PS C:\Users\JohnDoe>

We can therefore create an (covered, later, in section ) to hold instances of System.IO.FileInfo objects, plus a single instance of the object to be used as an array enumerator:

[System.IO.FileInfo[]] $Local:arrFileInfo = @(); # Array. [System.IO.FileInfo] $Local:objFileInfo = $null; # Array enumerator.

We can then populate the array using the Get-ChildItem output:

$arrFileInfo = Get-ChildItem -LiteralPath c:\temp -Filter file*.txt;

Finally, we can establish a variable to hold a return code and use the ForEach call to step through (enumerate) each member of the array (i.e.: each file):

[Int32] $Local:intRc = 0; # Zero for no-error. # # Process each file... # foreach ($objFileInfo in $arrFileInfo) { try { $objFileInfo.Delete(); # Attempt to delete the current file. } #try catch [System.Exception] { Write-Host -Object ( 'Failed to delete "{0}"; the error was "{1}".' -f $objFileInfo.Name, $_.Exception.Message ); $intRc = -1; # Set an error state. break; # Exit the ForEach loop. } #catch } #ForEach

As you can see, this lets us properly detect and handle errors by means of an , which we'll cover in section .   This technique gives us far more control over how objects (e.g.: files, mailboxes, virtual machines, Etc.) are processed when making changes to system state; more specifically, we can:

Please refer to section for further information regarding the foreach loop construct.