Embrace the pipeline

I’ve been doing some reflection on my PowerShell scripting skills, let me explain.  When I started scripting KIXs was the norm, VBS followed quickly. One of the things I struggled with while learning PowerShell was to think in Objects. I got that one down, the pipeline… well that’s another story…

Jeffery Hicks along with other MVPs did a PowerShell Blogging Week which was great! One takeaway from that week was “Don’t Learn PowerShell, Use it!” The scripts that the MVPs made all looked and felt like cmdlets and took advantage of the pipeline!

So I reviewed my most frequently used scripts and sure enough no pipeline support! Sure the scripts do what they’re suppose to do, and in some cases they’re a bit overengineerd… Ok a lot!!!

I’m about to shame myself to make a point… Here goes…

Have a look at this:

<# 
.SYNOPSIS 
    Enumerate Groups in CSV file
.DESCRIPTION 
    
.NOTES 
    Author: 
.LINK 
    
#>
[CmdletBinding()]
param(
    [string]$csvFile="Users.csv", 
    
    [string]$logFile="udm-dsa.log",
    
    [ValidateSet(",", ";", "`t")]
    [string]$delimiter = "`t",
     
    [switch]$Export
)

#region: CMTraceLog Function formats logging in CMTrace style
function CMTraceLog {
    Param (
        [String]$Message,

        [String]$Component,
        
        [String]$ErrorMessage,

        [ValidateRange(1,3)]
        [Int]$Type,

        [Parameter(Mandatory=$true)]
        $LogFile
    )

    $Time = Get-Date -Format "HH:mm:ss.ffffff"
    $Date = Get-Date -Format "MM-dd-yyyy"

    if ($ErrorMessage -ne "") {$Type = 3}
    if ($Component -eq $null) {$Component = " "}
    if ($Type -eq $null) {$Type = 1}

    $LogMessage = "<![LOG[$Message $ErrorMessage" + "]LOG]!><time=`"$Time`" date=`"$Date`" component=`"$Component`" context=`"`" type=`"$Type`" thread=`"`" file=`"`">"
    $LogMessage | Out-File -Append -Encoding UTF8 -FilePath $LogFile
}
#endregion

#region: ADSI function Get-ADObjectDN
function Get-ADObjectDN {
    param (
        [Parameter(Mandatory=$true)]
        [String]$type,
        [String]$DNSDomainName="",
        [String]$CNObject
    )

    if ("group","user","printqueue","computer" -NotContains $type) {
        Throw "$($type) is not valid! Please use 'group','user','printqueue','computer'"
    }

    $root = [ADSI]"LDAP://$DNSDomainName"
    $searcher = new-object System.DirectoryServices.DirectorySearcher($root)

    if ($type -eq "printqueue") {
        $searcher.filter = "(&(objectCategory=$type)(printerName=$CNObject))"
    }
    Elseif ($type -eq "computer") {
        $searcher.filter = "(&(objectCategory=$type)(sAMAccountName=$CNObject$))"
    }
    Else {
        $searcher.filter = "(&(objectCategory=$type)(sAMAccountName=$CNObject))"
    }

    $ADObject = $searcher.findall()

    if ($ADObject.count -gt 1) {     
        $count = 0

        foreach($objFound in $ADObject) {
            write-host $count ": " $objFound.path 
            $count = $count + 1
        }

        $selection = Read-Host "Please select item: "
        return $ADObject[$selection].path
    }
    else {
        return $ADObject[0].path
    }
}
#endregion

#region: verify thata the logFile exists
if(!(test-path "$pwd\log\$logFile")) {
    New-Item "$pwd\log\$logFile" -ItemType File
}
#endregion

#region: Create hash CMTrace for splatting
$hshCMTrace = @{
    Message = ""
    Component = $(($MyInvocation.MyCommand.Name).TrimEnd('.ps1'))
    ErrorMessage = ""
    Type = 1 
    LogFile = "$pwd\log\$logFile"
}
#endregion

#region: Reading CSV file. Stop if file isn't found
Write-Verbose "Script started : $(Get-Date)`n"
Write-Verbose "Reading CSV File $csvFile"

if (test-path "$pwd\source\csv\$csvFile") {
    Write-Verbose "Importing csv file: $pwd\source\csv\$csvFile`n"
    
    $hshCMTrace.Message = "Importing csv file $csvFile"
    $hshCMTrace.Type = 1
    CMTraceLog @hshCMTrace
    
    $csvUsers = Import-CSV "$pwd\source\csv\$csvFile" -delimiter $delimiter -Encoding UTF8
    $LogDate = get-date -uformat "%d-%m-%Y"
    $exportFile = "$($LogDate)_$(($csvFile).TrimEnd('.csv'))_$(($MyInvocation.MyCommand.Name).TrimEnd('.ps1')).csv"
    $arrExportUsers = @()
    $usersCount = $(@($csvUsers).count)
    $indexUsers = 0
    $indexMissing = 0
    $indexFound = 0
} 
else {
    Write-Error -Message "File $csvFile not found...`n" -RecommendedAction "Please verify and try again...`n"

    $hshCMTrace.Message = "File $csvFile not found"
    $hshCMTrace.Type = 2 
    CMTraceLog @hshCMTrace

    exit
}

$hshCMTrace.Message = "Users count in $csvFile : $usersCount"
$hshCMTrace.Type = 1 
CMTraceLog @hshCMTrace

#endregion

#region: hashtable Progressbar
$progFindUsers = @{
    Activity = "Processing users in $($csvFile)"
    Status="Searching"
    CurrentOperation = ""
    PercentComplete = 0
}
#endregion

#region: hashtable UserAcccountControl
#Have a look @site http://maxvit.net/userAccountControl
$hshAccountControl =@{
    66048 = "NORMAL_ACCOUNT - ACCOUNT_ENABLED - DONT_EXPIRE_PASSWORD"
    66080 = "NORMAL_ACCOUNT - ACCOUNT_ENABLED - DONT_EXPIRE_PASSWORD - PASSWD_NOTREQD"
    512 = "NORMAL_ACCOUNT"
    514 = "NORMAL_ACCOUNT - ACCOUNTDISABLE"
    544 = "NORMAL_ACCOUNT - ACCOUNT_ENABLED - PASSWD_NOTREQD"
    546 = "NORMAL_ACCOUNT - ACCOUNTDISABLE - PASSWD_NOTREQD"
}
#endregion

Write-Verbose -Message "Script Started on $(get-date)"

$ADDomainInfo = Get-ADDomain
Write-Verbose -Message "Forest Distinguished Name: $($ADDomainInfo.DistinguishedName)"


foreach ($user in $csvUsers) {
    $progFindUsers.Status = "Processing user $($user.SamAccountName)"
    $progFindUsers.PercentComplete = ($indexUsers/$usersCount) * 100

    Write-Progress @progFindUsers
    
    #Get User DistinguishedName
    [ADSI]$ldapUser = Get-ADObjectDN -type "user" -DNSDomainName $ADDomainInfo.DistinguishedName -CNObject $user.SamAccountName

    if ($ldapUser -ne $null){

        $value = $ldapUser.userAccountControl
        
        $ouIndex = $($ldapUser.DistinguishedName).IndexOf("OU=")
        $OU = ($ldapUser.DistinguishedName).Substring($ouIndex)

        $objUser = new-object psobject -Property @{
            Name = $($ldapUser.Name)
            DistinguishedName =$($ldapUser.DistinguishedName)
            WhenCreated = $($ldapUser.WhenCreated)
            sAMAccountName =$($ldapUser.samACCountName)
            AccountControl = $hshAccountControl.Item($($value))
            HomeDir =$($ldapUser.HomeDirectory)
            OU= $($OU)
        }
        
        $indexFound++
    } 
    else {
        $objUser = new-object psobject -Property @{
            Name = $null
            DistinguishedName =$null
            WhenCreated = $null
            sAMAccountName =$($user.samACCountName)
            AccountControl = $null
            HomeDir =$null
            OU= $null
        }

        $indexMissing++

        $hshCMTrace.Message = "Missing: $($user.SamAccountName)"
        $hshCMTrace.Type = 2 
        CMTraceLog @hshCMTrace
    }

    $arrExportUsers += $objUser
    $indexUsers++
}

$hshCMTrace.Message = "Users found in $csvFile : $indexFound"
$hshCMTrace.Type = 1 
CMTraceLog @hshCMTrace

$hshCMTrace.Message = "Users mssing in $csvFile : $indexMissing"
$hshCMTrace.Type = 1 
CMTraceLog @hshCMTrace

if ($export) {
    "`r"
    Write-Verbose "Exporting results to $pwd\export\dsa\$exportFile"
    $arrExportUsers| select Name,SamAccountName,DistinguishedName,WhenCreated,AccountControl,HomeDir,OU |  Export-CSV -NoTypeInformation "$pwd\export\dsa\$exportFile" -delimiter $delimiter -Encoding UTF8
}
else {
    if (!($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent)) {
        $arrExportUsers | select Name,SamAccountName,DistinguishedName,WhenCreated,AccountControl,HomeDir,OU  | Out-GridView -Title "Found Users $($csvFile) - $(Get-Date)"
    }
}

"`n"
Write-Verbose "End script : $(Get-date)"	

I’m all over the place! There’s a bit of everything!!! Trust me it works… Looking at it now, gotta ask myself, what was I thinking??? This was a script way back in the days when cmdlet performance was questionable… Rule of thumb if there’s a cmdlet use it! No need to reinvent the wheel…

So here’s a better (readable) version

$Prop =  @('canonicalname','homedirectory','mail','homedrive','scriptpath','initials','profilepath','userprincipalname')
import-csv -Path .\source\csv\moas-users.csv -Delimiter "`t" -Encoding UTF8 |
ForEach-Object {
    get-aduser -Filter "SamAccountName -eq '$($_.SamAccountName)'" -properties $Prop  |
    Select-Object Name,GivenName,SurName,Initials,mail,SamAccountName,Enabled,DistinguishedName,canonicalname,
        @{name='OU';expression={($_.DistinguishedName).SubString($_.DistinguishedName.indexof('OU='))}},
        homedirectory,homedrive,scriptpath,profilepath,userprincipalname
} |
Out-GridView

This will get me the same information with waaay much lesser code… No need  for complex verification,progress etc etc… Clean and simple… Processing each object as they go through the pipeline…

It is important to know beforehand what your objective is, is it a script or a tool? Don jones sums it up nicely stating:

“A scripts is something you usually make for yourself. Scripts are often quick and dirty, and although they might be long and complicated, they’re just a way for you to automate something that only you will ever do…”

Just because a script seems complicated doesn’t make it a tool, case in point. The former script is over engineered plain and simple. I got a bit carried away with all the possibilities in PowerShell.

If I had to start learning PowerShell today I’d advice myself to better understand and use the pipeline…

Simplicity

Hope it’s worth something to you!

Ttyl,

Urv

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s