© Adam Bertram 2020
A. BertramBuilding Better PowerShell Codehttps://doi.org/10.1007/978-1-4842-6388-4_15

15. Return Standardized, Informational Output

Adam Bertram1  
(1)
Evansville, IN, USA
 

Have you ever run a script or function you received from someone else and wondered if it worked? It ran without showing an error, but then again, it returned nothing at all! You don’t have a clue what it did nor could see its progress as it was executing.

Because it didn’t return any object to the pipeline, you also can’t use its outcome in other commands. You are forced to write more code to check whether it did its job which results in wasted time and added complexity.

In this chapter, you’re going to learn some tips on what to return to your user, how often, and how to return output in many different ways.

Use Progress Bars Wisely

PowerShell has a handy cmdlet called Write-Progress . This cmdlet displays a progress bar in the PowerShell console. It’s never a good idea to leave the user of your script staring at a blinking cursor. The user has no idea if the task should take 10 seconds or 10 minutes. They also might think the script has halted or crashed in some manner. It’s important you provide some visual cues as to what’s going on.

Use Write-Progress for any task that takes more than 10 seconds or so. The importance increases with time. For more complicated scripts with many transactions, be sure to use Write-Progress at a higher level. Use the progress bar as the main indicator of progress and leave the smaller steps to a verbose message, for example.

Perhaps you have a script that connects to a list of servers and parses through a bunch of files on each server. Each server has a few thousand files that take 10–20 seconds each to read and parse through. Take a look here for an example of what this script might look like:
## Pull server computer names from AD
$serverNames = Get-AdComputer -Filter '*' -SearchBase 'OU=Servers,DC=company,DC=local' | Select-Object -ExpandProperty Name
foreach ($name in $serverNames) {
    foreach ($file in Get-ChildItem -Path "\$namec$SomeFolderWithABunchOfFiles" -File) {
        ## Read and do stuff to each one of these files
    }
}
In the preceding example, you essentially have two “progress layers” represented with a foreach loop – processing a server at a time and also processing a file at a time. Try to add a progress bar at the “higher-level layer” or processing the server. You wouldn’t want tens of thousands of file paths flying by on the progress bar. Instead, a messaging saying “Processing files on ABC server” that continually updates would be better.
## Pull server computer names from AD
$serverNames = Get-AdComputer -Filter '*' -SearchBase 'OU=Servers,DC=company,DC=local' | Select-Object -ExpandProperty Name
## Process each server name
for ($i=0;$i -lt $serverNames.Count;$i++) {
    Write-Progress -Activity 'Processing files' -Status "Server [$($serverNames[$i])]" -PercentComplete (($i / $serverNames.Count) * 100)
    ## Process each file on the server
        foreach ($file in Get-ChildItem -Path "\$($serverNames[$i])c$SomeFolderWithABunchOfFiles" -File) {
        ## Read and do stuff to each one of these files
    }
}

Now if you run the example script, you’d see a progress bar in Figure 15-1 that just displays each server that’s being processed.

Tip Source: https://twitter.com/brentblawat and https://twitter.com/danielclasson
../images/501963_1_En_15_Chapter/501963_1_En_15_Fig1_HTML.jpg
Figure 15-1

Using Write-Progress

Further Learning

Leave the Format Cmdlets to the Console

For any script that returns some kind of output, a well-developed script contains two “layers” processing and presentation. The processing “layer” contains all of the code necessary to perform whatever task is at hand. The “presentation” layer displays what the scripts outputs to the console. Never combine the two.

If you need to change up what the output looks like in the console, do it outside the script. For example, never use a Format-* cmdlet inside of a script. You should treat scripts like reusable tools. Unless you are 100% certain that script will never need to send output to another script or function, don’t attempt to format the output in the script. Instead, pipe objects from the script into a formatting command at the console or perhaps another “formatting” script.

Perhaps you have a script that reads some files. To make the output easier to look at, you decide to pipe the contents of Get-ChildItem to Format-Table.
## readsomefiles.ps1
Get-ChildItem -Path 'somepathhere' | Format-Table
The output looks fine but you then need to perform some action on each of those files; maybe it’s removing them. Knowing that you can pipe the contents of Get-ChildItem to Remove-Item, you add Remove-Item onto the end.
## readsomefiles.ps1
Get-ChildItem -Path 'somepathhere' | Format-Table | Remove-Item
You’ll soon find out that PowerShell doesn’t like that at all. Why? Because Format-Table, just like any of the Format-* cmdlets, does not return objects to the pipeline. Instead of using a formatting cmdlet inside of the script, remove the Format-Table reference from the script and instead use the cmdlet in the console.
PS> . eadsomefiles.ps1 | Format-Table

Further Learning

Use Write-Verbose

Verbose messaging comes in handy in many different scenarios from troubleshooting, script progress indication, and debugging. Use the Write-Verbose cmdlet as much as possible to return granular information about what’s happening in a script. Use verbose messages to display variable values at runtime, indicate what path code takes in a condition statement like if/then, or indicate when a function starts and stops.

There are no defined rules to indicate when to return a verbose message. Since verbose messaging is off by default, you don’t need to worry about spewing text to the console. It’s better to have more information than less when it comes to verbose messaging.

Perhaps you have a script that gathers ACLs for DNS records stored in Active Directory (AD).
#requires -Module ActiveDirectory
[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [string[]]$DnsHostname,
    [Parameter()]
    [string]$DomainName = (Get-ADDomain).Forest
)
$Path = "AD:DC=$DomainName,CN=MicrosoftDNS,DC=ForestDnsZones,DC=$($DomainName.Split('.') -join ',DC=')"
foreach ($Record in (Get-ChildItem -Path $Path)) {
    if ($DnsHostname -contains $Record.Name) {
        Get-Acl -Path "ActiveDirectory:://RootDSE/$($Record.DistinguishedName)"
    }
}

When you run this script, it doesn’t return any messaging letting you know what’s going on. It only returns ACLs matching the DnsHostName parameter. You could add some messaging to this function to make it better using some verbose messages.

You can see in the following some verbose messaging was added to the script. To see this verbose messaging when you run this script, you would use the Verbose parameter like .Get-DnsAdAcl.ps1 -Verbose.
#requires -Module ActiveDirectory
[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [string[]]$DnsHostname,
    [Parameter()]
    [string]$DomainName = (Get-ADDomain).Forest
)
Write-Verbose -Message 'Starting script...'
$Path = "AD:DC=$DomainName,CN=MicrosoftDNS,DC=ForestDnsZones,DC=$($DomainName.Split('.') -join ',DC=')"
foreach ($Record in (Get-ChildItem -Path $Path)) {
    if ($DnsHostname -contains $Record.Name) {
        Write-Verbose -Message "Getting ACL for [$($Record.Name)]..."
        Get-Acl -Path "ActiveDirectory:://RootDSE/$($Record.DistinguishedName)"
        Write-Verbose -Message "Finished getting ACL for [$($Record.Name)]."
    }
}
Write-Verbose -Message 'Script ending...'

Tip Source: https://twitter.com/UTBlizzard

Further Learning

Use Write-Information

PowerShell has six streams. You can think about three of those streams in terms of verbosity Debug, Verbose, and Information. The debug stream is at the bottom and should return lots of granular information about code activity while the information stream should contain high-level messages.

Use the Write-Information cmdlet to display top-level information similar to what would be down in a progress bar. The user doesn’t need to see variables, if/then logic, or any of that. Use Write-Information to display basic, high-level status messages about script activity.

Using the example from the “Use Write-Verbose” tip, you could improve that a bit by using Write-Information instead of Write-Verbose when the script starts and stops.
#requires -Module ActiveDirectory
[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [string[]]$DnsHostname,
    [Parameter()]
    [string]$DomainName = (Get-ADDomain).Forest
)
Write-Information -MessageData 'Starting script...'
$Path = "AD:DC=$DomainName,CN=MicrosoftDNS,DC=ForestDnsZones,DC=$($DomainName.Split('.') -join ',DC=')"
foreach ($Record in (Get-ChildItem -Path $Path)) {
    if ($DnsHostname -contains $Record.Name) {
        Write-Verbose -Message "Getting ACL for [$($Record.Name)]..."
        Get-Acl -Path "ActiveDirectory:://RootDSE/$($Record.DistinguishedName)"
        Write-Verbose -Message "Finished getting ACL for [$($Record.Name)]."
    }
}
Write-Information -MessageData 'Script ending...'

When and where to use Write-Information vs. Write-Verbose is completely up to you. The use cases vary wildly. Just remember to use Write-Information to display higher-level activity and use Write-Verbose to display more granular activity.

Further Learning

Ensure a Command Returns One Type of Object

Nothing will confuse a PowerShell developer more than when a script or function returns different types of objects. Keep it simple and ensure regardless of the circumstances, the command only returns one type.

When a script or function returns different types of objects based on various scenarios, it becomes hard to write code that takes input from that command.

Perhaps you’re writing a server inventory script. You have a script that queries a remote server and returns information like service status and user profiles on that server.
$servers = ('SRV1','SRV2','SRV3','SRV4')
foreach ($server in $servers) {
    Get-Service -ComputerName $server
    Get-ChildItem -Path "\$serverc$Users" -Directory
}

The preceding script returns two different types of objects – a System.Service.ServiceController object via Get-Service and a System.IO.DirectoryInfo object via Get-ChildItem.

Even though you want to see the output of each cmdlet, do not leave the script as is. Instead, consolidate these two types of output into your own object type, preferably a PSCustomObject. You can see an example in the following of returning one PSCustomObject object type for each server:
$servers = ('SRV1','SRV2','SRV3','SRV4')
foreach ($server in $servers) {
    $services = Get-Service -ComputerName $server
    $userProfiles = Get-ChildItem -Path "\$serverc$Users" -Directory
    [pscustomobject]@{
        Services = $services
        UserProfiles = $userProfiles
    }
}

Allowing your scripts and functions to only return one object type forces standardization. It also makes it easier to chain functions together. If you can expect a certain object type to be returned for each function in a module, for example, you can write other functions that accept that input much easier than if you had to code around a lot of different types.

Further Learning

Only Return Necessary Information to the Pipeline

If a command returns information you have no use for, don’t allow it to return objects to the pipeline. Instead, assign the output to $null or pipe the output to the Out-Null cmdlet.

If the command should return the output sometimes but not all the time, create a PassThru parameter. By creating a PassThru parameter, you give the user the power to decide to return information or not.

Maybe you have a script that, as a part of it, creates a directory using New-Item. Some cmdlets that do not have the Get verb return unnecessary information; the New-Item cmdlet is one of them.
New-Item -Path 'C:path ofolder' -ItemType Directory
You can see in Figure 15-2 when you run New-Item, it returns an object. If you just need to create a directory, you probably don’t need that object. If you’d leave this command in your script, the script would return this object. Ensure that doesn’t happen.
../images/501963_1_En_15_Chapter/501963_1_En_15_Fig2_HTML.jpg
Figure 15-2

New-Item returning an unnecessary object

Instead of allowing cmdlets and other functions to place unnecessary objects on the pipeline, send the output to $null. Assigning output to $null essentially removes it entirely and prevents anything from going to the pipeline.
$null = New-Item -Path 'C:path ofolder' -ItemType Directory

You can also pipe output to Out-Null but I prefer assigning output to the $null variable . Why? Because when working with large collections, you should not use the pipeline at all, if possible for performance reasons.

Tip Source: https://twitter.com/JimMoyle

Further Learning

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.112.220