Category Archives: Scripts

Scheduled tasks, PowerShell's -file parameter, and array values

I wrote a script that accepts a comma-separated list of values, and the script worked just fine from the command-line. However, when I tried to configure a scheduled task to run the script, it always failed.
Why? Well, I started a cmd.exe session and then launched the script in the same way that the scheduled task did, using PowerShell’s -file parameter. And when I did that, the error message that I emit from the script showed me that the list was being parsed as a single string argument.
To confirm and experiment, I wrote a short little test script:

process {
    foreach ($spell in $spells ) {
        "Casting $spell"

When run from within a PowerShell session, it works as expected:

PS C:\> .\Cast-WizardSpell.ps1 -SpellList 'Ray of Frost','Light','Detect Magic'
Casting Ray of Frost
Casting Light
Casting Detect Magic

When invoked using the PowerShell -file parameter, the comma-separated list is parsed as a single parameter (note: cmd.exe doesn’t like single quotes):

C:\>powershell -file .\Cast-WizardSpell.ps1 -SpellList "Ray of Frost","Light","Detect Magic"
Casting Ray of Frost,Light,Detect Magic
# Trying explicit array syntax, but no luck
C:\>powershell -file .\Cast-WizardSpell.ps1 -SpellList @("Ray of Frost","Light","Detect Magic")
Casting @(Ray of Frost,Light,Detect Magic)

What does work is to use the old-style -command syntax:

C:\>powershell -command "& .\Cast-WizardSpell.ps1 -SpellList 'Ray of Frost','Light','Detect Magic'"
Casting Ray of Frost
Casting Light
Casting Detect Magic

Alternatively, one can adjust the parameter syntax, adding the ValueFromRemainingArguments attribute. However, for this to work, you can’t specifiy the parameter name.

C:\>powershell -file .\Cast-WizardSpell.ps1  "Ray of Frost" "Light" "Detect Magic"
Casting Ray of Frost
Casting Light
Casting Detect Magic
C:\local\scripts>powershell -file .\Cast-WizardSpell.ps1 -SpellList "Ray of Frost" "Light" "Detect Magic"
C:\local\scripts\Cast-WizardSpell.ps1 : A positional parameter cannot be found that accepts argument 'Light'.
+ CategoryInfo          : InvalidArgument: (:) [Cast-WizardSpell.ps1], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : PositionalParameterNotFound,Cast-WizardSpell.ps1

I’m not thrilled with either of these options, because some person like me may come along and, in an effort to be helpful, may twiddle the command line, thinking we’re normalizing or updating the syntax, when we’re really breaking things. However, I think using the -Command invocation is the least surprising, most consistent implementation. I’ll just make notes in the script help and in the description of the scheduled task about the reason I’ve used that method.

PowerShell Script: New-RandomString.ps1

I need to automate the setting of passwords on some Active Directory accounts. Since resetting passwords is also a task that I’m asked to perform with some routine, I decided to make a more generic tool script that could be used in a variety of tasks ( I listened to Don Jones‘ advice on building Tools and Controllers).
I also got a head start from Bill Stewart’s useful Windows IT Pro article Generating Random Passwords in PowerShell. Among the changes I made are source character class handling, and a new SecureString output option. Please let me know if you find the script useful, or if you find any bugs.

Generates one or more randomized strings containing specified
character classes.
The length of the string to be generated.
.PARAMETER CharacterClasses
An array of Character Classes from which to generate the string. The string
will contain at least one character from each specificied class. You may also use the alias 'Classes' for the parameter name
Valid Character classes are:
    Upper    - A..Z
    Lower    - a..z
    Digits   - 0..9
    AlphaNum - shorthand for Upper,Lower,Digits
    Symbols  - !"#$%&'()*+,-./:;?@[\]^_`{|}~
    Safe     - #$%+-./:=\_~  (ODBC Safe, Shell Safe if quoted)
If no classes are specified, a string is generated with mixed-case letters,
digits, and symbol characters (i.e., ALL the classes).
.PARAMETER IncludeCharacters
A string of characters to include in the generated string:
.PARAMETER ExcludeCharacters
A string a characters to exclude in the generated string:
The number of strings to be generated.
.PARAMETER AsSecureString
Specifies that the new random string(s) will be returned as Secure String
objects, to make their use as passwords easier.
> New-RandomString.ps1 -CharacterClasses Lower,Digits -Length 14 -Count 5
Generated five strings, each fourteen characters long, comprised of lowercase
letters and digits.
> New-RandomString.ps1 -Classes AlphaNum,Symbols -length 21
> New-RandomString.ps1 -length 21
The previous two commands are equivalent, because the default character classes
used are upper and lowercase letters, digits, and symbol characters.
> New-RandomString.ps1 -Class 'AlphaNum' -Include '#$%^'
The generated string will contain characters from the UpperCase, LowerCase
and Digits classes, as well as at least one character from among the four
> New-RandomString.ps1 -Class 'AlphaNum' -Exclude 'O0l1'
The generated string will contain characters from the UpperCase, LowerCase
and Digits classes, but will not contain the "look-alike' characters.
Author     : Geoff Duke 
Last Edit  : 2014-11-07
Based on script "Get-RandomString.ps1" for Windows IT Pro:
#Requires -version 3
        $length = 21,
        $count = 1,
        $CharacterClasses = @('Upper','Lower','Digits','Symbols'),
        $IncludeCharacters = '',
        $ExcludeCharacters = '',
Set-StrictMode -version 'Latest'
# Additional parameter wrangling
# --------------------------------------------------------------------
[string[]] $Classes = $CharacterClasses.ToLower()
if ( $Classes.Contains('safe') -and $Classes.Contains('symbols') ) {
    write-warning 'You specified both "Symbols" and "Safe" character classes; this is the same as just specifying "Symbols".'
    $Classes = $Classes | where { $_ -ne 'safe' }
# Replace alphanum with the upper,lower, and digits classes
if ( $Classes.Contains('alphanum') ) {
    $Classes = $Classes | where { $_ -ne 'alphanum' }
    $Classes += 'upper','lower','digits'
# remove any duplicated classes
$Classes = $Classes | select -unique
# Setup source characters
# --------------------------------------------------------------------
# Character classes - functionally, a strongly-typed hash of string arrays
#       (addresses issue of singleton arrays turning into simple strings)
$chars = New-Object 'Collections.Generic.Dictionary[string,char[]]'
$chars['lower']    =  97..122 | foreach-object { [Char] $_ }
$chars['upper']    =  65..90  | foreach-object { [Char] $_ }
$chars['digits']   =  48..57  | foreach-object { [Char] $_ }
$chars['symbols']  = (33..47+58..64+91..96+123..126) | foreach-object { [Char] $_ }
$chars['safe']     = '#$%+-./:=\_~'.ToCharArray()
write-verbose $( 'String must include a character from each of ' +
              $( $Classes -join ',' ) +
              $( if ( $IncludeCharacters ) { " plus [$IncludeCharacters] " } ) +
              $( if ( $ExcludeCharacters ) {
                  "but must not include any of [$ExcludeCharacters]" } ) )
if ( $IncludeCharacters ) {
    $Classes += 'include'
    $chars['include'] = $IncludeCharacters.ToCharArray()
[char[]] $char_source  = $chars[ $Classes ] | % { $_ } | select -unique
if ( $ExcludeCharacters ) {
    $char_source = $char_source | Where { $_ -NotIn $ExcludeCharacters.ToCharArray() }
write-verbose "Source chars: $(-join $char_source)"
# Generating the random string(s)
# --------------------------------------------------------------------
$string_count = 0
:NewString while ( $string_count -lt $Count )  {
    $output = ''
    for ( $i=0; $i -lt $length; $i++) {
        $output += get-random @($char_source)
    write-debug "NewString: generated string is -> $output"
    # Ensure that the requested character classes are present
    :CharClass foreach ($class in $Classes) {
        foreach ( $char in $output.ToCharArray() ) {
            if ( $chars[$class] -Ccontains $char ) {
                write-debug "CharClass: '$char' is in $class"
                continue CharClass # check the next character class
        } # end foreach $char, didn't match the current character class
        write-debug "CharClass: No character from $class! Start again"
        continue NewString # Need to generate a new string
    } # end foreach #class
    # string matches required character classes"
    if ( $AsSecureString ) {
        ConvertTo-SecureString $output -AsPlainText -Force
    else {
} # end while

It was while I was writing this script that I ran into the Loop Label documentation error. In PowerShell, as in Perl, Loop Labels do not include the colon when used with a break or continue statement.

Get-PrintJobs.ps1 PowerShell script

After a recent upgrade of our print servers, I discovered that the Print Spooler service event logging had been enhanced, and changed enough that some PowerShell reporting scripts that worked just fine on Windows Server 2008 (32-bit) no longer worked on Server 2012 R2.
To get the reports working again, I had to enable the Microsoft-Windows-PrintService/Operational log. I also had to increase the log size from the default in order to retain more than one day’s events. The trickiest part was figuring out the XPath query syntax for retrieving events from a particular printer. The newer syntax makes more sense to me, but it took me a long time to arrive at it.
Following Don Jones‘ entreaty to build tools and controllers, I offer this tool script, which retrieves (simplified) print job events, and cares not a whit about formatting or saving.

Returns objects representing all the successful print jobs (events with id 307).
C:\> Get-PrintJobs.ps1 -PrinterName 'Accounting HP LaserJet'
Returns objects for all the jobs on the Accounting printer.
C:\> Get-PrintJobs.ps1 -PrinterName 'Accounting HP LaserJet' -StartTime (Get-Date).AddHours(-12)
Returns objects for all the jobs on the Accounting printer generated in the last twelve hours.
Script Name: Get-PrintJobs.ps1
Author : Geoff Duke 
Edit 2014-10-08: Generalizing from dept printer report script, fixing XPath
query syntax.
Edit 2012-11-29: Job is run as SYSTEM, and computer object has been granted
Modify rights to the destination directory.
[string] $PrinterName,
[datetime] $StartTime,
[datetime] $EndTime
Set-StrictMode -version latest
# Building XPath query to select the right events
$filter_start = @'

$filter_end = @'

$filter_match = '*[System[(EventID=307)' #need to add ']]' to close
if ( $StartTime -or $EndTime) {
    $filter_match += ' and TimeCreated[' #need to add ']' to close
    $time_conds = @()
    if ( $StartTime ) {
        $time_conds += ( '@SystemTime>=' +
            "'{0:yyyy-MM-ddTHH:mm:ss.000Z}'" -f $StartTime.ToUniversalTime()
    if ( $EndTime ) {
        $time_conds += ( '@SystemTime<=' +
            "'{0:yyyy-MM-ddTHH:mm:ss.000Z}'" -f $EndTime.ToUniversalTime()
    $filter_match += ( $time_conds -join ' and ' ) + ' ]' # Closing TimeCreated[
$filter_match += "]]`n" # Closing [System[
if ( $PrinterName ) {
    $filter_match += @"
write-debug "Using Filter:`n $filter_match"
# The $filter variable below is cast as XML, that's getting munged
# by WordPress or the SyntaxHighlighter as '1'
 $filter = ($filter_start + $filter_match + $filter_end)
get-winevent -filterXML $filter | foreach {
    $Properties = @{
        'Time' = $_.TimeCreated;
        'Printer' = $_.Properties[4].value;
        'ClientIP' = $[3].value.SubString(2);
        'User' = $[2].value;
        'Pages' = [int] $[7].value;
        'Size' = [int] $[6].value
    New-Object PsObject -Property $Properties

If you find this script useful, please let me know. If you find any bugs, definitely let me know!

Set default printer with PowerShell

Closely related to my previous post, this simple script uses a WScript.Network COM object to set the default printer. The comment block is longer than the script, but I think it’s a useful little tool.

Sets a Network Printer connection as the default printer.
Uses a COM object to sets the specified, installed printer as the default. If
an error is encountered, e.g., the specified printer isn't installed, the
exception is written to a file called Set-DefaultPrinter.err in the current
$env:temp directory, and then the script terminates, throwing the exception.
Based on my colleague's VBScript solution:
.PARAMETER PrinterShare
The UNC path to the shared printer.
e.g. \\\ETS-SAA-SamsungML-3560
Set-DefaultPrinter.ps1 -PrinterShare '\\\ETS-SAA-SamsungML-3560'
    Script Name: Set-DefaultPrinter.ps1
    Author     : Geoff Duke 
        HelpMessage="Enter the UNC path to the network printer")]
    [string] $PrinterShare
Set-PSDebug -Strict
$PSDefaultParameterValues = @{"out-file:Encoding"="ASCII"}
$ws_net = New-Object -COM WScript.Network
try {
catch {
    $error[0].exception | out-file (join-path $env:temp 'Set-DefaultPrinter.err')
    throw $error[0]
write-verbose "Default printer now $PrinterShare"

Add network printer with PowerShell

This is my PowerShwell translation of my colleague’s VBScript solution for mapping network printers with a script.

Add a Network Printer connection, optionally making it the default printer.
Uses a COM object to add a Network Printer, and optionally sets that printer
as the default. If an error is encountered, the exception is written to a
file called Add-NetworkPrinter.err in the current $env:temp directory, and then
the script terminates.
This is my PowerShell translation of my colleague's VBScript solution:
.PARAMETER PrinterShare
The UNC path to the shared printer.
e.g. \\\ETS-SAA-SamsungML-3560
Specifies that the printer will also be set as the default printer for the current user.
Add-NetworkPrinter.ps1 -PrinterShare '\\\ETS-SAA-SamsungML-3560' -Default
    Script Name: Add-NetworkPrinter.ps1
    Author     : Geoff Duke 
        HelpMessage="Enter the UNC path to the network printer")]
    [string] $PrinterShare,
    [switch] $Default
Set-PSDebug -Strict
$PSDefaultParameterValues = @{"out-file:Encoding"="ASCII"}
$ws_net = New-Object -COM WScript.Network
write-verbose "Adding connection to $PrinterShare"
try {
catch {
    $error[0].exception | out-file (join-path $env:temp 'Add-NetworkPrinter.err')
    throw $error[0]
write-verbose "Setting the printer as the default"
if ( $Default ) {
    try {
    catch {
        $error[0].exception | out-file (join-path $env:temp 'Add-NetworkPrinter.err')
        throw $error[0]
# the end

For use with Group Policy, it will probably be helpful to create a simple Set-DefaultPrinter.ps1 script. But that’s just the second stanza from the script above.

Printer drivers and architectures with PowerShell

We have a number of 32-bit Windows 2008 print servers that we want to migrate to Windows Server 2012, for the printer management PowerShell cmdlets, among other things. I found a helpful blog post about using the PRINTBRM utility to migrate print queues, which mentions that you need to have both 32-bit and 64-bit drivers versions of all the drivers in order to migrate from a 32-bit to a 64-bit OS instance.
I wrote a little script to quickly show me which print drivers need a 64-bit version installed. It can take a moment to run if you have many printers configured.

UPDATE: I made a couple changes, most notably that the count of printers using a driver is now optional (since it can take a while on a system with lots of printers).

 .\Get-PrinterDriverArchitecture.ps1 | format-table -auto
Name                                       x86   x64
----                                       ---   ---
Brother HL-5250DN                         True False
Epson LQ-570+ ESC/P 2                     True False
HP Business Inkjet 2230/2280              True False
HP Business Inkjet 2250 (PCL5C)           True  True
HP Business Inkjet 2800 PCL 5             True False
This command lists the installed printer drivers, and whether 32-bit (x86)
or 64-bit (x64) drivers are available.
PS C:\local\scripts> .\Get-PrinterDriverArchitecture.ps1 | where x64 -eq $false | ft -a
Name                                      x86   x64
----                                      ---   ---
Brother HL-5250DN                        True False
Epson LQ-570+ ESC/P 2                    True False
HP Business Inkjet 2230/2280             True False
HP Business Inkjet 2800 PCL 5            True False
This command uses the Where[-Object] cmdlet to filter out those drivers that
have a 64-bit driver installed.
PS C:\local\scripts> .\Get-PrinterDriverArchitecture.ps1 -Printers | ft -a
Enumerating printers:
    WID - Kyocera TASKalfa 3050ci KX
    WC - Kyocera TASKalfa 300ci KX
    UFS - Canon iR-ADV C5035
Name                                       x86   x64 Printers
----                                       ---   --- --------
Brother HL-5250DN                         True False        5
Epson LQ-570+ ESC/P 2                     True False        0
HP Business Inkjet 2230/2280              True False        1
HP Business Inkjet 2250 (PCL5C)           True  True        0
HP Business Inkjet 2800 PCL 5             True False        1
This command includes the -Printers switch parameter to add a count of
the printers using each driver. Enumerating the printers can take a while
if there are lots of them installed, so this behavior is optional.
 - Author    :
 - Mod. Date : May 28, 2013
param( [switch] $printers )
$wmi_drivers = get-wmiobject Win32_PrinterDriver -Property Name
$drivers = @{}
foreach ($driver in $wmi_drivers) {
    # Isolate the driver name and platform
    $name,$null,$platform = $driver.Name -split ','
    if ( -not $drivers[$name] ) {
        switch ( $platform ) {
            'Windows NT x86' { $drivers[$name] = [ordered]@{
                                 'Name'=$name; 'x86'=$true; 'x64'=$false }; break }
            'Windows x64'    { $drivers[$name] = [ordered]@{
                                 'Name'=$name; 'x86'=$false; 'x64'=$true }; break }
             default         { write-warning "Unexpect platform $platform on driver $name"}
    else {
        switch ( $platform ) {
            'Windows x64'    { $drivers[$name]['x64'] = $true; break }
            'Windows NT x86' { $drivers[$name]['x86'] = $true; break }
             default         { write-warning "Unexpect platform $platform on driver $name"}
if ( $printers ) {
    # Initialize all printer counts
    $drivers.keys | foreach { $drivers[$_]['Printers'] = 0 }
    # Add a count of the number of printers using each driver
    # With some progress info
    write-host 'Enumerating printers:'
    $count = 0
    get-wmiobject Win32_Printer -Property Name,DriverName | foreach {
        write-host "    $($_.Name)"  -foreground darkgray
    write-host "Retrieved $count printers"
# Output collection of objects
$drivers.keys | sort | foreach { New-Object PSObject -Property $drivers[$_] }

I hope this is useful to others.

Which Disk is that volume on?

I administer a server VM with a lot of disks, and many of them are the same size. When I need to make changes to the system’s storage, I’m always nervous that I’m going to poke the wrong disk. I could trust that the order of the disks listed in the vSphere client is the same as the order that the guest OS lists (starting at 1 and 0 respectively). But I want a little more assurance.
Using diskpart, you can list the details for individual disks, partitions and volumes, but I wanted a report showing all the disks, the partitions on those disks, and the volumes residing on those partitions. I have reported some of this info previously, using PowerShell’s Get-WMIObject cmdlet to query the Win32_DiskDrive, Win32_Partition, and Win32_Volume classes. I figured there must me a way to correlate instances of these classes.
I found these two blog posts:

They did most of the heavy lifting in building the WQL ASSOCIATOR OF queries. I put together a short script to give me a little more detail. Here’s some sample output:

PS C:\local\scripts> .\Get-DiskInfo.ps1
Disk 0 - SCSI 0:0:2:0 - 45.00 GB
    Partition 0  100.00 MB  Installable File System
    Partition 1  44.90 GB  Installable File System
        C: [NTFS] 44.90 GB ( 3.46 GB free )
Disk 5 - SCSI 0:0:2:5 - 39.99 GB
    Partition 0  40.00 GB  Installable File System
        B: [NTFS] 40.00 GB ( 34.54 GB free )

This will make it easier to be sure about the vSphere storage element that corresponds to a particular volume (or, more accurately, the Physical Disk on which the volume resides).
Here’s the actual script:

    Author: Geoff Duke 
    Based on and
Set-PSDebug -Strict
Function Main {
    $diskdrives = get-wmiobject Win32_DiskDrive | sort Index
    $colSize = @{Name='Size';Expression={Get-HRSize $_.Size}}
    foreach ( $disk in $diskdrives ) {
        $scsi_details = 'SCSI ' + $disk.SCSIBus         + ':' +
                                  $disk.SCSILogicalUnit + ':' +
                                  $disk.SCSIPort        + ':' +
        write $( 'Disk ' + $disk.Index + ' - ' + $scsi_details +
                 ' - ' + ( Get-HRSize $disk.size) )
        $part_query = 'ASSOCIATORS OF {Win32_DiskDrive.DeviceID="' +
                      $disk.DeviceID.replace('\','\\') +
                      '"} WHERE AssocClass=Win32_DiskDriveToDiskPartition'
        $partitions = @( get-wmiobject -query $part_query |
                         sort StartingOffset )
        foreach ($partition in $partitions) {
            $vol_query = 'ASSOCIATORS OF {Win32_DiskPartition.DeviceID="' +
                         $partition.DeviceID +
                         '"} WHERE AssocClass=Win32_LogicalDiskToPartition'
            $volumes   = @(get-wmiobject -query $vol_query)
            write $( '    Partition ' + $partition.Index + '  ' +
                     ( Get-HRSize $partition.Size) + '  ' +
            foreach ( $volume in $volumes) {
                write $( '        ' + $ +
                         ' [' + $volume.FileSystem + '] ' +
                         ( Get-HRSize $volume.Size ) + ' ( ' +
                         ( Get-HRSize $volume.FreeSpace ) + ' free )'
            } # end foreach vol
        } # end foreach part
        write ''
    } # end foreach disk
function Get-HRSize {
        [Parameter(Mandatory=$True, ValueFromPipeline=$True)]
        [INT64] $bytes
    process {
        if     ( $bytes -gt 1pb ) { "{0:N2} PB" -f ($bytes / 1pb) }
        elseif ( $bytes -gt 1tb ) { "{0:N2} TB" -f ($bytes / 1tb) }
        elseif ( $bytes -gt 1gb ) { "{0:N2} GB" -f ($bytes / 1gb) }
        elseif ( $bytes -gt 1mb ) { "{0:N2} MB" -f ($bytes / 1mb) }
        elseif ( $bytes -gt 1kb ) { "{0:N2} KB" -f ($bytes / 1kb) }
        else   { "{0:N} Bytes" -f $bytes }
} # End Function:Get-HRSize

Please let me know if you find this helpful.

Script: Shadow Copy Report

We use EMC NetWorker for our enterprise backup solution. Since we migrated our primary file server from a NetApp filer to a native Windows server, we’ve been having a recurring problem with all the Shadow Copies for a volume getting deleted. There are strong indications that the problem is related to the NetWorker backups.
As we have been working on this issue with EMC (since the first week in January!), I wrote a script to tell me two things each morning; how many snapshots exist for each volume, and what VSS errors were logged, if any.
I thought someone might find it useful, so I’ve posted it as a separate page (the script doesn’t fit nicely in the column on the blog).
PowerShell Script: chksnap.ps1