Tuesday, June 28, 2016

ISE Color Themes can now be found in the PowerShell Gallery!

Just a quick update to let you know that you can now find the ISEColorTheme.Cmdlets in the PowerShell Gallery at:

https://www.powershellgallery.com/packages/ISEColorTheme.Cmdlets 

I hope you'll find them much more convenient to use as a full fledged module in a hosted repository. After you install the module be sure to add the ISE Theme Menu to the ISE by running "Add-ISEThemeMenu".

With the move to this repository came the need to conform a bit more to established standards and modify the script layout. Fortunately, Ryan Yates offered to help to tidy it up and did a fine job. You can catch his blog here: blog.kilasuit.org . Be sure to watch that space in the coming days as I hear there might be a mention of the ISE Themes Cmdlets. ;)

One last thing... Since the PowerShell gallery lets me assign my own icon to the module, I thought I'd play around a little. Ladies and Gentlemen, introducing the "Tie Dye PowerShell logo"!




What do you think? Too much? Let me know in the comments below.



Tuesday, March 25, 2014

Introducing the New ISE Color Theme Cmdlets!

Last week I posted about the ability to theme the Powershell ISE script pane and provided a link to a few themes to play with. In that article I lamented about the lack of command line support for themes. Allow me to elaborate a little. You can certainly change any theme element from the command line but you can not do the following:

  • Apply a theme to your ISE session from a file or from a previously saved ISE theme
  • Import an xml theme to the ISE
  • Export an ISE theme to an xml file

Worse yet, the color values stored in the theme xml files are ARGB based, not hex, which is what you need to apply a theme color to an ISE property from the command line. For this reason the scripts used to apply themes are completely different than the xml files used for the import/export process. How could Microsoft come so far, yet come up so short?! Frustrating to say the least!

Solution

Where there is a problem, there is opportunity! After reviewing the situation and coming to the conclusion that all the pieces I needed were there, I decided to create the ISE Theme Cmdlets script module. This script module allows you to do all of the above and more from the command line. 

Here are a few things you can do with these cmdlets:
  • Batch import a collection of xml theme files
  • Batch export all of the custom themes available in the ISE
  • Choose a different custom theme from the command line
  • Alter the overall color scheme of a theme without the need to tweak each parameter individually

This last bullet point or feature is a lot of fun! The Adjust-ISEColor cmdlet allows you to easily alter a theme by giving it the following commands:
  • Cooler
  • Warmer
  • Greener
  • Lighter
  • Darker
This cmdlet also uses a "Degree" parameter to set the degree of change to make. By playing with this cmdlet I was able to generate all of the new themes attached below in a fraction of the time it would take with the default interface. Give it a try and share your work!

ISEThemeCmdlets

Here is the list of cmdlets with a short description and example. 

Get-FileName()
Gets a file name using an OpenFileDialog.
Get-FileName | Set-ISETheme 


Get-SaveFile()
Gets a save file name using a SaveFileDialog.
$File = Get-SaveFile


Convert-HexToARGB()
Converts Hex to ARGB values
$ARGB = Convert-HexToARGB #FF000022


Convert-ARGBToHex()
Converts ARGB to Hex values
$Hex = Convert-ARGBToHex "255,0,0,233"


Get-CurrentISETheme()
Gets current ISE theme. Hex colors are converted to ARGB and added back to the returned objects
$CurrentISETheme = Get-CurrentISETheme


Get-ISETheme()
Gets an ISE them from either the ISE or xml file
$Theme = Get-FileName | Get-ISETheme


Get-ImportedISEThemes()
Returns imported theme objects from the ISE
$Themes = Get-ImportedISEThemes


Set-ISETheme()
Applies ISE theme to current session
Get-FileName | Set-ISETheme


Import-ISEThemeFile()
Imports an ISE theme xml file into the ISE and applies it to the current session if ApplyTheme is passed
Get-ChildItem d:\sandbox -Include *.ps1xml -recurse | Import-ISEThemeFile -ApplyTheme


Export-ISEThemeFile()
Exports an ISE theme to an xml file and saves it into the ISE if SaveToISE is passed
Export-ISEThemeFile -SaveToISE


Remove-ISETheme()
Deletes an ISE theme from the ISE
Remove-ISETheme "Monokai"


Adjust-ISEColor()
Adjusts ISE Theme colors according to switch
Set-ISEThemeWarmth -Cooler


Select-ISETheme()
Selects and applies an ISE theme using a selection form
Select-ISETheme


More Color Themes

Last week I shared a collection of themes I found in various searches online. This week I'm happy to say the themes I'm sharing are all original and created using the ISE Color Theme Cmdlets!

Here they are:



Brownie




Aquatic



Nightvale



PaperRose





To batch import these new themes, extract them to your file system and run the following command:

gci "C:\YourThemes" -Filter *.ps1xml | Select FullName | Import-ISEThemeFile



Here is a link to the new PowerShell Color Themes

Here is a link to the ISE Color Theme Cmdlets


Friday, March 14, 2014

PowerShell ISE Color Themes

So admittedly, I'm late for the party once again. I've only recently started running PowerShell 4.0 and having come from 2.0, I am quite impressed to say the least! I won't go on about all the new features here because I'm sure you've already read about it elsewhere by now. If not, this article will fill you in: What's New in the Windows PowerShell ISE. Instead of rewriting that article, I'll talk about the not so new by now ISE themeing capability.

With PowerShell 3.0/4.0 you can now change the color of every element in the script/command panes and save these changes to an xml file for use in other ISE sessions. You can also change these elements with a script file or commands but here's the strange thing. There is no current way to use the xml file with the scripted method. Instead you must manually set the individual properties. On top of that, their xml file is less than user friendly.

Here's an example script from the scripting guys in this article Customize Colors and Fonts in the Windows PowerShell ISE. Fair warning, the color selections are hideous!


Set-PsISEcolorsAndFonts.ps1

# fonts             
$psISE.Options.FontName = 'Kartika'             
$psISE.Options.FontSize = 16             
# output pane             
$psISE.Options.OutputPaneBackgroundColor = '#FFFFEFD5'             
$psISE.Options.OutputPaneTextBackgroundColor = '#FFFFEFD5'             
$psISE.Options.OutputPaneForegroundColor = '#FF000000'             
# command pane             
$psISE.Options.CommandPaneBackgroundColor = '#FFFAEBD7'             
# script pane             
$psISE.Options.ScriptPaneBackgroundColor = '#FFFAEBD7'             
# tokens             
$psISE.Options.TokenColors.item('Command') = '#FFA0522D'             
$psISE.Options.TokenColors.item('Operator') = '#FFA0522D'             
$psISE.Options.TokenColors.item('Unknown') = '#FFFFFFFF'             
$psISE.Options.TokenColors.item('Member') = '#FFFFFFFF'             
$psISE.Options.TokenColors.item('Position') = '#FFFFFFFF'             
$psISE.Options.TokenColors.item('GroupEnd') = '#FFFFFFFF'             
$psISE.Options.TokenColors.item('GroupStart') = '#FFFFFFFF'             
$psISE.Options.TokenColors.item('LineContinuation') = '#FFFFFFFF'             
$psISE.Options.TokenColors.item('NewLine') = '#FFFFFFFF'             
$psISE.Options.TokenColors.item('StatementSeparator') = '#FFFFFFFF'             
$psISE.Options.TokenColors.item('Comment') = '#FFAEAEAE'             
$psISE.Options.TokenColors.item('String') = '#A2BC13'             
$psISE.Options.TokenColors.item('Keyword') = '#FFFFDE00'             
$psISE.Options.TokenColors.item('Attribute') = '#FF84A7C1'             
$psISE.Options.TokenColors.item('Type') = '#FF84A7C1'             
$psISE.Options.TokenColors.item('Variable') = '#EE9A00'             
$psISE.Options.TokenColors.item('CommandParameter') = '#FFFFDE00'             
$psISE.Options.TokenColors.item('CommandArgument') = '#FFFFFFFF'             
$psISE.Options.TokenColors.item('Number') = '#FF4169E1'             
$psISE.Options.TokenColors.item('LoopLabel') = '#FF4169E1'


Importing xml themes

While you could create a bunch of scripts to change your themes on command, a much better way is to use xml theme files. By using xml theme files, you can take advantage of the color adjustment features of the Options panel. You can also export your changes for use elsewhere. 

When I read about themes and played around with the color options, I was excited to see all the various PowerShell theme collections people must have created by now. Unfortunately, I couldn't find any theme collections out there. I couldn't even find many themes let alone theme collections. So I took the few themes I did find and put them together and attached them here.  

To use them do the following:
  1. Download and extract the theme files.
  2. Open the ISE and select Tools > Options.
  3. Select Manage Themes and import the extracted theme files.


Potential to script the import method

Although Microsoft did a nice job incorporating the theme option, they came up a little short in that you cannot import a theme using a PowerShell command. Kind of funny considering the nature of the product being themed.

Fortunately it won't take much to work around this deficiency. With a simple look through the registry, I can see they are creating string values with the content of the xmls stored as the data. It won't take much to create a function to do that! From there, I can build it into a form with preview pictures. Perhaps I can find some time to write this but I digress for now...

UPDATE: This has been done! Now you can import, change themes, and modify themes from the command line. Check out my latest post and grab my new ISE Theme Cmdlets! 

Themes contained in zip file

Here are some screenshots of the themes contained in the zip file. The original theme sources are also contained in the zip file.

Blackboard:

CoSolarized:

IR_Black:

Monokai:


My favorite of these 4 is Monokai. If you have an ISE color theme you'd like to share, let me know and I'll add it to this collection and update this post.

Here's the theme zip: 

PowerShellColorThemes.zip

Sunday, March 2, 2014

Requesting WMI registry data on a 64-bit platform using a 32-bit PowerShell process

We've all been there before. You've learned "all there is to know" about the differences between x86 and x64 Windows. When working with the file system, look for "Program Files (x86)" or "SysWow64". When working with the registry, look for "Wow6432Node" to find your 32-bit application data. Life's good and you go on coding in your 64-bit ISE world, confident that you'll avoid cross-architecture pitfalls with the knowledge you've learned. Everything looks good, right?


The Problem

You find yourself needing to run your script with SCCM 2007 which uses a 32-bit client agent. Your script accesses both 32-bit and 64-bit registry locations using that trusty "Wow6432Node" key you learned about. Suddenly you realize that nothing is working as you expected when running on a 64-bit machine. This is because a 32-bit process knows nothing of the Wow6432Node that you are hard coding in your script.


The Solution

To get around this, I use the .Net StdRegProv class. This class allows you to set the architecture or "alternate registry view" as part of your registry query. To find out more about requesting WMI data on a 64-bit platform, click here.

The following example can be run in either x86 or x64 processes and is compatible with PowerShell 2.0 or later.
#-----------------------------
#--  Get-RegKey           
#-----------------------------            
Function Get-RegKey {            
    [cmdletbinding()]            
    Param(            
        [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()]            
        [int] $Arch, # registry view to use            
                    
        [Parameter(ValueFromPipeline=$True,ValueFromPipelinebyPropertyName=$True)][ValidateNotNullOrEmpty()]            
        [string] $Computer = ".", # computer to query            
            
        [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()]            
        [string] $KeyPath, # path to the key to find            
                    
        [Parameter()] [ValidateNotNullOrEmpty()]            
        [object] $WMIObject # WMI connection object passed by another registry cmdlet                          
    )            
            
 Begin {            
        # Parse the Keypath            
        $i = $keyPath.IndexOf(":")            
        $Hkey = $keyPath.Substring(0,$i)            
        $KeyPath = $KeyPath.Substring($i + 2)            
    }            
            
 Process {            
  try {                    
            # Create a WMI connection object if one is not passed to the function already            
            If ($WMIObject -eq $null) {            
                # Create WMI connection object            
                $objswbem = New-Object -ComObject "WbemScripting.SWbemNamedValueSet"            
                $objswbem.Add("__ProviderArchitecture", $Arch) | Out-null            
                $objswbem.Add("__RequiredArchitecture", $True) | Out-null            
                $ObjLocator = New-Object -ComObject "Wbemscripting.SWbemLocator"            
                $objServices = $objLocator.ConnectServer($Computer,"root\Default",$null,$null,$null,$null,$null,$objswbem)            
                $objReg = $objServices.Get("stdRegProv")            
            } Else {            
                # Use the passed WMI connection object            
                $objReg = $WMIObject                
            }            
                        
            # Load the Enumkey method into the inparams            
            $Inparams = $objreg.Methods_.Item("EnumKey").Inparameters            
                        
            # Set the Hkey value            
            switch ($Hkey) {            
                “HKCR” {$HkeyVal = "&h80000000"} #HKEY_CLASSES_ROOT             
                “HKCU” {$HkeyVal = "&h80000001"} #HKEY_CURRENT_USER            
                “HKLM” {$HkeyVal = "&h80000002"} #HKEY_LOCAL_MACHINE             
                "HKU" {$HkeyVal = "&h80000003"}  #HKEY_USERS                             
                "HKCC" {$HkeyVal = "&h80000005"} #HKEY_CURRENT_CONFIG            
                "HKDD" {$HkeyVal = "&h80000006"} #HKEY_DYN_DATA                            
            }            
            
            # Load the parameters and execute method            
            $inparams.properties_.item("Hdefkey").Value = $HkeyVal            
            $inparams.properties_.item("sSubKeyName").Value = $KeyPath            
            $Outparams = $objReg.ExecMethod_.Invoke("EnumKey", $Inparams,$null,$objswbem)            
                        
            # Output the results                           
            if (($Outparams.Properties_ | where {$_.name -eq "ReturnValue"}).Value -eq 0) {              
                $OutputObj = New-Object -Type PSObject            
                $OutputObj | Add-Member -MemberType NoteProperty -Name "Computer" –Value $Computer            
                $OutputObj | Add-Member -MemberType NoteProperty -Name "Architecture" –Value "$Arch-bit"
                $OutputObj | Add-Member -MemberType NoteProperty -Name "KeyPath" –Value "$HKey`:\$KeyPath"
                $OutputObj | Add-Member -MemberType NoteProperty -Name "Exist" –Value $True            
                $OutputObj             
              
            } else {                
                $OutputObj = New-Object -Type PSObject            
                $OutputObj | Add-Member -MemberType NoteProperty -Name "Computer" –Value $Computer            
                $OutputObj | Add-Member -MemberType NoteProperty -Name "Architecture" –Value "$Arch-bit"
                $OutputObj | Add-Member -MemberType NoteProperty -Name "KeyPath" –Value "$HKey`:\$KeyPath"
                $OutputObj | Add-Member -MemberType NoteProperty -Name "Exist" –Value $False            
                $OutputObj               
            }                              
        } catch {                    
        #throw “Failed to retrieve keys in: ‘$KeyPath’. The error was: ‘$_’.”             
            $OutputObj = New-Object -Type PSObject            
            $OutputObj | Add-Member -MemberType NoteProperty -Name "Computer" –Value $Computer            
            $OutputObj | Add-Member -MemberType NoteProperty -Name "Architecture" –Value $_            
            $OutputObj | Add-Member -MemberType NoteProperty -Name "KeyPath" –Value "$HKey`:\$KeyPath"
            $OutputObj | Add-Member -MemberType NoteProperty -Name "Exist" –Value ""            
            $OutputObj                         
  }            
        }            
            
    End {}            
                
    <#
  .SYNOPSIS
   Checks if registry key exists

  .DESCRIPTION
   Checks if registry key exists using the enumkey method of the StdRegProv

  .PARAMETER Arch
   Registry architecture or view to use for query

  .PARAMETER Computer
   Computer name to query registry from. If none provided searches local computer.
   
  .PARAMETER KeyPath
   Registry key path to find
  
  .EXAMPLE
   PS C:\> Get-RegKey 64 mycomputer "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\SystemRestore"
            
        .EXAMPLE
   PS C:\> Get-RegKey -Arch 64 "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\SystemRestore"

  .EXAMPLE
   PS C:\> Get-RegKey 64 my computer "HKLM:\Software\Alps\Apoint"

  .INPUTS
   System.String

  .OUTPUTS
   
  .NOTES
                        Part of the RegistryCmdlets.ps1 script by Jeff Pollock
                        http://lifeinpowershell.blogspot.com 
                        http://gallery.technet.microsoft.com/Powershell-Registry-19689888                                                                 
 #>                        
}

As you can see there is quite a bit of coding just to properly get a simple RegKey and there are many other registry operations yet to cover. Fortunately I've coded the most useful ones for you. You can get them in the link at the end of this post. They are well commented so I won't go into detail about them here other than to provide the following.

List of the cmdlets provided in the script:

  • Get-RegKey()
  • Get-RegSubKeys()
  • Set-RegKey()
  • Remove-RegKey()
  • Get-RegValue()
  • Get-RegSubValues()
  • Set-RegValue
  • Remove-RegValue()
  • SearchRegValue()

  • Cmdlet features:

  • Both x86 and x64 compatible
  • Can accept a WMI registry object as a parameter to avoid creating multiple WMI connections.
  • Fully pipeline enabled

  • Over the next few posts I will talk a little more about some of the routines used in these cmdlets. For now, give them a try and let me know how it goes.

    Here is a link to the script on the Technet Gallery:

    Sunday, February 16, 2014

    Identify the only drivers a machine needs for the OSD driver import process in Configuration Manager

    Last week I talked about using a regex expression to find a syntax error in a directory containing all of my scripts. This week, I'll use the same method to find all of the driver INF files, needed for a given machine. This will allow me to provide a specific list of driver INF files to use for importing into Configuration Manager.


    The Problem

    One of the more tedious aspects of preparing new hardware for OS deployment with Configuration Manager is the driver import process. The process involves identifying the driver, downloading or extracting it to a central location, and then importing it. Rinse and repeat until all of the exclamation marks disappear in the Device Manager. This can make for a long morning and the temptation to grab a full driver cab and import the whole thing and be done with it is great. But then you are left with a bloated driver package where most of the drivers aren't even required. 

    The Solution

    I can't help you avoid having to download a bunch of drivers but with the help of the script discussed here, we can identify precisely what your computer needs and only import those drivers. Thus creating a much smaller and manageable driver store. 

    In order to do this, the script needs to:
    1. Identify all of the hardware Names and DeciveIDs in Win32_PNPEntity
    2. Toggle the ability to only search for unknown devices
    3. Extract a string from the DeviceID to be used as our regex espression
    4. Recursively search a given directory for INF files that contain our regex expression
    5. Return a device object with an array of INF file locations for each found device INF
    Here's the output:
    PS C:\> & "D:\Sandbox\Driver_INF_File_Search\Driver_INF_File_Search.ps1" C:\Dell
    DeviceName : Intel(R) Mobile Express Chipset SATA RAID Controller SearchPattern : DEV_282A FileArray : {C:\Dell\E6530\win8\x64\storage\2PWDK_A01-00\Drivers\AHCI\x64\iaStorAC.inf, C:\Dell\E6530\win8\x86\storage\2PWDK_A01-00\Drivers\AHCI\x32\iaStorAC.inf}

    Preparation

    The one caveat to this process is that in order to be able to search for the driver, it must be present in the given directory or subdirectory. This means we'll still have to download the drivers manually before we use the script.
    Some vendors provide full driver cabs for their machines. If your vendor provides them, download and extract them to the file system, then use the script to identify only those that your machine needs. 

    Querying Win32_PNPEntity

    To get the deviceIDs we use the Win32_PNPEntity class in WMI. Using this class also allows us to find devices that are marked unknown through the use of the ConfigManagerErrorCode property. The WMI query isn't terribly complex so let's move on to the file search routine.

    File search routine

    Once we get the list of IDs to search for we need to extract a string from them to use for the search because there is no place in the INF file that contains the entire deviceID string. On top of that, there are a few different forms that the ID can take. This script manages to find IDs that contain "&DEV" and "DLL". I haven't worked out the other possibilities but these two values cover a lot of ground.

    The following routine utilizes regex to first find an eligible value to extract a string from and then uses another regex to identify the sting to extract. It then assigns that string to the $Pattern variable to be used in the select-string statement. All matched deviceIDs are then searched for, discarding those that were not modified by the regex statements.

    #--Assign the deviceid to the pattern for evaluation            
    $Pattern = $Device.DeviceID            
                    
    #--Evaluate $Pattern for occurrence of "&DEV" or "DLL"            
    switch -regex ($Pattern) {            
       "&DEV" {$Pattern -match 'DEV_\w+[^&]' | out-null;$PatternModified = $True}
       "DLL"  {$Pattern -match 'DLL\w+[^\\]' | out-null;$PatternModified = $True} 
    }           
    #--Search directory for INF files with the occurance of $Pattern            
    If ($PatternModified) {            
       #--Set new regex pattern            
       $Pattern = $matches[0]            
       #--Find files that contain the extracted pattern            
       $Result = Get-ChildItem $Directory -include *.inf -recurse | select-string -pattern $Pattern | Select-Object -Unique Path            
    }

    Returning unique file locations

    While the derived search term is very successful in being found, it occurs in the INF file many times. This means that every time it is found, it will be returned to the query resulting in many references to the same file for a single device. We only want to return unique file locations. The way we accomplish this is by piping the returned result through the Select-Object cmdlet like this:
    Select-Object -Unique Path
    In preparation for writing an automated import process for Configuration Manager, I return objects rather than writing the results to the host. "When in doubt, use PSobjects"  - Abe Lincoln

    That's it for this week. Hopefully some Configuration Manager admins will find this method useful in taming the driver beast.

    Here is a link to the script on Github:
    Driver_INF_File_Search.ps1 

    And here is a link to the script on the Technet gallery:

    http://gallery.technet.microsoft.com/Driver-Inf-File-Search-for-fe8a95cc


    The Script

    #========================================================================            
    # Date              : 2/16/2014 4:49 PM            
    # Author            : Jeff Pollock            
    # Website           : http://lifeinpowershell.blogspot.com/            
    #             
    # Description       : Assists in identifying driver INF files for import            
    #                     into Configuration Manager 2007 & 2012. It does this            
    #                     by searching a given computer for all PNP devices            
    #                     and then searching a given directory for the related            
    #                     INF files.             
    #========================================================================            
    Param (            
        [parameter(Mandatory=$true,ValueFromPipeline=$True)]            
        [string]$SearchDir,  #Directory to search            
                
        [parameter(ValueFromPipeline=$True)]            
        [string]$Computer = ".",  #Computer to search            
                
        [parameter(ValueFromPipeline=$True)]            
        [switch]$UnknownOnly  #Determines whether to return all devices or just unknown            
    )            
                
    #----------------------------------------------            
    #region Functions            
    #----------------------------------------------            
    Function Get-PNPDevices {            
        [cmdletbinding()]            
        Param (            
            [bool]$UnknownOnly  #-determines whether to return all devices or just unknown                  
        )            
                
        #--Query Win32_PNPEntity for devices            
        If (!$UnknownOnly) {            
            $Devices = Get-WmiObject -ComputerName $Computer Win32_PNPEntity |
            Select Name, DeviceID            
        } Else {            
            $Devices = Get-WmiObject -ComputerName $Computer Win32_PNPEntity |
            Where-Object{$_.ConfigManagerErrorCode -ne 0} | Select Name, DeviceID    
        }            
                
        #--Return device objects            
        ForEach ($Device in $Devices) {            
            $DeviceObj = New-Object -Type PSObject            
            $DeviceObj | Add-Member -MemberType NoteProperty -Force -Name DeviceName -Value $Device.Name
            $DeviceObj | Add-Member -MemberType NoteProperty -Force -Name DeviceID -Value $Device.DeviceID
            $DeviceObj                    
        }            
    }            
                
    Function Search-DeviceINF {            
        [cmdletbinding()]            
     Param(            
         [parameter(Mandatory=$true,ValueFromPipeline=$True)]            
         [string]$Directory,  #Directory to search
    
         [parameter(Mandatory=$true,ValueFromPipeline=$True)]            
         [object]$Device  #Device object            
     )            
                    
        #--Create array to hold returned file names            
        $FileArray = @()            
                    
        #--Assign the deviceid to the pattern for evaluation            
        $Pattern = $Device.DeviceID            
                    
        #--Evaluate $Pattern for occurance of "&DEV" or "DLL"            
        switch -regex ($Pattern) {            
            "&DEV" {$Pattern -match 'DEV_\w+[^&]' | out-null;$PatternModified = $True}            
            "DLL"  {$Pattern -match 'DLL\w+[^\\]' | out-null;$PatternModified = $True}            
        }            
                
        #--Search directory for INF files with the occurance of $Pattern            
        If ($PatternModified) {            
            #--Set new regex pattern            
            $Pattern = $matches[0]            
            #--Find files that contain the extracted pattern            
            $Result = Get-ChildItem $Directory -include *.inf -recurse |
            select-string -pattern $Pattern | Select-Object -Unique Path            
        }            
                
        #--Output results            
        If($Result) {            
            #--Add returned files to the FileArray            
            $Result | ForEach-Object {$FileArray += $_.Path}            
                
            #Create object and output            
            $DeviceObj = New-Object -Type PSObject            
            $DeviceObj | Add-Member -MemberType NoteProperty -Force -Name DeviceName -Value $Device.DeviceName
            $DeviceObj | Add-Member -MemberType NoteProperty -Force -Name SearchPattern -Value $Pattern
            $DeviceObj | Add-Member -MemberType NoteProperty -Force -Name FileArray -Value $FileArray            
            $DeviceObj | format-list            
        }             
    }            
    #endregion Application Functions            
                
    #----------------------------------------------            
    # region Script            
    #----------------------------------------------            
    #--Set Unknown switch            
    If ($UnknownOnly) {            
        [bool]$Unknown = $True            
    } Else {            
        [bool]$Unknown = $False            
    }            
                
    #--Perform query            
    Get-PNPDevices $Unknown | ForEach-Object {              
       Search-DeviceINF $SearchDir $_             
    }            
    #endregion Script

    Monday, February 10, 2014

    Running an older script in PowerShell 3 or 4 generates the following error id: InvalidVariableReferenceWithDrive

    Today's post deals with a problem in a few of my scripts that I encountered after upgrading to PowerShell 3 from version 2. We'll discuss both the problem and the simple regex pattern I used to identify where it may have existed in all of our other scripts. 

    The problem has to do with variables used inside a string, followed immediately by a colon. Say what? Read on…

    The below code is interpreted correctly in PowerShell 1 & 2 but in PowerShell 3 and 4 it is expecting a mount point or a “drive” immediately following the colon. 

    Example:
    log "Installing Beyond Compare $exeversion:"
    log "$exeversion: Already Installed."

    Error:
    +         log "Installing Beyond Compare $exeversion:"
    +                                        ~~~~~~~~~~~~
    Variable reference is not valid. ':' was not followed by a valid variable name character. Consider using ${} to delimit the name.
        + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
        + FullyQualifiedErrorId : InvalidVariableReferenceWithDrive

    To correct this, the line needs to be updated to encapsulate the variable so it is processed first and then returned to the string.

    log "Installing Beyond Compare $($exeversion):”
    log "$($exeversion): Already Installed."

    You probably already know whether you've coded a string using a variable this way or not but we weren't sure how prevalent it was in our organization. 

    Finding the errant code:
    So now that we were aware of the problem, we had to identify all of our scripts that contained the errant code conditions. In essence we had to do the following:

    1. Get all occurrences of our "*.ps1" script files with a recursive child item query.
    2. Search each script for the occurance of our regex pattern.
    3. Display the full file name, line number, and string

    Here is the command line we used to do just that and its resulting output.

    PS C:\windows\system32> Get-ChildItem \\server\share -include *.ps1 -recurse | select-string -pattern '\".*\$\w+\:[\s*\"]'


    \\server\share\Adobe\script\CommonFunctions.ps1:2864:            } Else {Write-Output "$FontFile: Already present"}    
    \\server\share\Scooter Software\3.3.5\setup.ps1:161:        log "Installing Beyond Compare $exeversion:"
    \\server\share\SunGard\Fonts\CommonFunctions.ps1:2864:            } Else {Write-Output "$FontFile: Already present"}

    In a repository with 100's of scripts, only 3 presented this unique condition. The output of this query contains the file location, line number, and code line that needs to be corrected. At this point it wouldn't take much effort to automate the correction of the code as well but hey, I wouldn't want to steal all the fun!

    What's truly great about this method is that you can search any text readable file for anything! In a later post, I'll show you how I used this search method to tame the driver store beast in SCCM.

    Breaking down the regex:
    As promised, here's the breakdown of the regular expression I used.

    '\".*\$\w+\:[\s*\"]'

    \"          - Expression must begin with a quote
    .*           - Any amount of characters
    \$           - The dollar sign or beginning of the broken variable we are looking for
    \w+        - A series of characters with no spaces
    \:             - the colon or end of the broken variable we are looking for
    [\s*\"]   - A space followed by anything and then a quote

    For an excellent PowerShell regex reference, check out this link: 
    PowerShell Cookbook: Regular Expression Reference

    Thanks for checking out this article. I hope you find it informative. Until next time...