Parameters

Using a list as a default parameter value

I got an interesting question in a comment today on my old post on Getting the Free Disk Space of Remote Computers. While I answered the comment in the original post, it brought up something that's not well understood, so I thought I'd take a moment to give a fuller answer here, where it will be more discoverable.

 

The reader complained that when he ran the script without parameters, he was getting an error. When I read the error, it was clear that he had modified the script to set a default value for the ComputerName parameter that was a list of computers, rather than a single computer. A reasonable change, since the script is set to handle a list or array of strings with [string[]]. And if you use the parameter with a list of -ComputerName 'server1','server2','server3', etc., from the command line, it handles that list as you would expect. But if you try to set a default value for the ComputerName parameter with that same list:

[CmdletBinding()]
Param ([Parameter(Mandatory=$False,Position=0)]
       [String[]]$ComputerName = 'server1','server2','server3')

You'll get an error:

Missing expression after ','

So, does this mean you can't set a default value that is a list? Of course not! But it does mean you need to let PowerShell know explicitly what you're doing. When you pass in an adhoc list like that from the command line, it just works because PowerShell does a fair amount of magic for things it sees on the command line. But in a script, we need to be more explicit. So, instead, we need to declare our list as... A List! We do that with:

[CmdletBinding()]
Param ([Parameter(Mandatory=$False,Position=0)]
       [String[]]$ComputerName = @('server1','server2','server3'))

Now, our script works as we'd expect. It accepts my default list of server1, server2, server3, but allows me to override that list with my own list at the command line. (And remember, a list can be a list of length one, having only a single member. )

Param() Tricks

One of the new features of PowerShell v5 is support for creating hard links, symbolic links and junctions. This is long overdue, and much appreciated. Before, I'd been forced to the workaround of using "cmd /c mklink" to create links, and I'm always glad to find a way to get rid of any vestige of cmd. Plus, having it as a part of PowerShell gives me way more flexibility in creating scripts.

 

As I was looking at some of my existing scripts, it occurred to me that I should be taking advantage of hard links in more scripts. I already use hard links for my various RDP connects, using a long switch statement. (I'll write that up one of these days, it's actually pretty cool.) But what caught my eye today was the script I wrote to create virtual machines for my labs -- New-myVM.ps1. I have a -Client parameter to the script that is a Boolean, defaulted to $False:

[CmdletBinding()]
Param([Parameter(Mandatory = $True,Position = 0)]
      [alias("Name")]
      [string]
      $VmName,
      [Parameter(Mandatory=$False)]
      [Boolean]
      $Client=$False
      )

Which is OK, but it occurred to me that I could do better. So, first, I created a new, hard-linked file with:

New-Item -Type HardLink -Name New-myClientVM.ps1 -Path .\New-myVM.ps1

Now I have one file with two names. Cool. So, let's take that a step further. I can tell which version of it I called from the command line by taking advantage of the automatic PowerShell variable $myInvocation:

$myInvocation.mycommand.name

This returns the filename (".name") of the command (".mycommand") that was executed ($myInvocation). So now, I can use:

$client =  ($myInvocation.mycommand.name -match "client")

I put that near the top of the script, and now I could branch depending on whether I was creating a server VM or a client VM. Which was definitely better, but still left me thinking it could be improved.

 

So, how about making the whole thing a lot cleaner by getting rid of that extra line? After all, I'm creating a variable and defaulting its value to $false, but why not default its value more intelligently, controlled by which file I executed to create the VM? I can still override it with the parameter (so no scripts that call this script will break), but now, I can set it automatically without using a parameter at all.

[CmdletBinding()]
Param([Parameter(Mandatory = $True,Position = 0)]
      [alias("Name")]
      [string]
      $VmName,
      [Parameter(Mandatory=$False)]
      [Boolean]
      $Client=($myInvocation.myCommand.Name -match "Client")
      )

Now that pleases me. It feels "cleaner", it's clear what I'm doing, and it doesn't take any longer to evaluate than it would as a standalone line.

Active Directory — Unlocking a User Account with PowerShell

As any SysAdmin knows, users periodically lock themselves out of their accounts, usually because they forgot a password or somehow mistyped it too many times. And after all, if it failed once, why not keep trying it? Unlocking that account is NOT something you do with Set-ADUser, unfortunately, because the PowerShell ActiveDirectory module has a special, single-purpose cmdlet - Unlock-ADAccount. Now, it doesn't take a whole script to run a single cmdlet, but sometimes there are good reasons to wrap a command in a more complicated script, and this is one I've had to. Primarily because I work on at least three different domains that are unrelated - my home domain, my "work" domain, and my testlab domain. With this script, I can also take advantage of an encrypted password for one of those domains, stored securely on my main desktop's hard disk. Plus, I've added in a bit of code to go grab the current holder of the FSMO PDCEmulator role, since I'd prefer to unlock the account directly against that PDC.

Also, to make life easier, and allow us to stick this in the middle of a pipeline when we need to, we'll add the ability to handle parameters from the pipeline. We do that in the Param section:

[CmdletBinding()]
Param(
     [Parameter(Mandatory=$True,ValueFromPipeline=$true,Position=0)]
     [alias("user","account")]
     [string[]]
     $Identity,
     [Parameter(Mandatory=$False,ValueFromPipeline=$True,Position=1)]
     [string]
     $Domain = "DOMAIN",
     [Parameter(Mandatory=$False,ValueFromPipeline=$True)]
     [PSCredential]
     $Credential = $NULL
     )

You'll notice here that we've allowed for each of our parameters to accept pipeline input. And they'll accept that "ByValue", meaning that the piped objects must have the same .NET type, or must be able to be converted to that type. So only an actual PSCredential object can be accepted for the -Credential parameter.

 

Next, we want to be able to handle credentials in any of several ways. So we use:

if ( $Credential ) {
   $myCred = $Credential
} else {
   # Test if there is a stored, encrypted, password we can use
   $pwPath = "$home\Documents\WindowsPowerShell\$domainPW.txt"
   if (Test-Path $pwPath ) {
      $mypw = Get-Content $pwPath | ConvertTo-SecureString
      $myCred = New-Object -TypeName System.Management.Automation.PSCredential `
                           -ArgumentList "$DOMAIN\Charlie",$mypw
   } else {
      # Prompt for a credential, since we don't seem to have one here. 
      $myCred = Get-Credential 
   }
}

Let's look at this for a moment. First, we know from the Param section that the default value of $Credential is $NULL, so our first test is whether that's been overridden at the command line with either a pipelined parameter, or a specifically entered credential object. If so, we're good, and we use that. But failing that, we'll check if there's one stored securely on disk. (More on storing credentials on disks in another post soon.) If we've stored one on disk that matches the domain we're going against, we'll use that. If not, and we still don't have a PSCredential object we can use, we'll simply prompt for one.

 

Next up, we want to run this against the domain controller that hosts the PDCEmulator role. Not a big deal in simplified domain environment, but really a good idea in a complicated, multi-site environment where site-to-site propagation is a bit slow.

$PDC = (Get-ADDomain -Identity $DOMAIN -Credential $myCred).PDCEmulator

We'll plug the value of $PDC into the -Server parameter of our AD commands.

Finally, the meat of the whole thing. I'm not assuming you're only unlocking a single account, so I've designed this to take a list of strings ([string[]]). (A list can, of course, be a list of length one.)

foreach ($usr in $Identity) {
   Unlock-ADAccount -Identity $usr `
                    -Credential $myCred `
                    -Server $PDC `
                    -PassThru `
       | Get-ADUser -Properties LockedOut

 

That last line ensures that we report back in a way that shows the account no longer locked out. But for Get-ADUser to actually do anything, you need to include the -PassThru parameter in the Unlock-ADAccount command. Otherwise, nothing at all gets passed to the Get-ADUser command.

So, here's the whole thing:

<#
.Synopsis
Unlocks a  domain account
.Description
Unlock-myUser accepts an array of DOMAIN account names and unlocks the accounts 

This script accepts pipeline input for the credential. If no credential is supplied, 
it will attempt to use one you have stored on disk. Failing that, it will prompt 
you for credentials. 
.Example
Unlock-myUser -Identity Charlie
Unlocks the account of Charlie 
.Example
Unlock-myUser -Identity Charlie, Sharon
Unlocks the accounts of Charlie and Sharon 
.Example
Unlock-myUser -Identity Charlie -Domain TREYRESEARCH `
              -Credential (Get-Credential `
                               -username "TREYRESEARCH\Alfie"  `
                               -Message "Enter your Domain PW")

Unlocks the account of TreyResearch\Charlie, prompting Alfie for credentials. 
.Parameter Identity
The AD identity of the user or users whose acounts are to be reset.
.Parameter Domain
The Active Directory domain of the user or users whose accounts are to be reset
.Parameter Credential
The user credentials to run the script under. 
.Inputs
[string[]]
[string]
[PSCredential]
.Notes
    Author: Charlie Russel
 Copyright: 2016 by Charlie Russel
          : Permission to use is granted but attribution is appreciated
   Initial: 07 Sept, 2016 (cpr)
   ModHist: 09 Sept, 2016 (cpr) - added PDC test and Domain parameter 
          :
#>
[CmdletBinding()]
Param(
     [Parameter(Mandatory=$True,ValueFromPipeline=$true,Position=0)]
     [alias("user","account")]
     [string[]]
     $Identity,
     [Parameter(Mandatory=$False,ValueFromPipeline=$True,Position=1)]
     [string]
     $Domain = "DOMAIN",
     [Parameter(Mandatory=$False,ValueFromPipeline=$True)]
     [PSCredential]
     $Credential = $NULL
     )

if ( $Credential ) {
   $myCred = $Credential
} else {
   # Test if there is a stored, encrypted, password we can use
   $pwPath = "$home\Documents\WindowsPowerShell\$domainPW.txt"
   if (Test-Path $pwPath ) {
      $mypw = Get-Content $pwPath | ConvertTo-SecureString
      $myCred = New-Object -TypeName System.Management.Automation.PSCredential `
                           -ArgumentList "DOMAIN\Charlie",$mypw
   } else {
      # Prompt for a credential, since we don't seem to have one here. 
      $myCred = Get-Credential 
   }
}

# Find out which server holds the PDC role. 
# Useful in complicated, multi-site environments where 
# Domain changes might not propagate quickly. 

$PDC = (Get-ADDomain -Identity $DOMAIN -Credential $myCred).PDCEmulator

foreach ($usr in $Identity) {
   Unlock-ADAccount -Identity $usr `
                    -Credential $myCred `
                    -Server $PDC `
                    -PassThru `
       | Get-ADUser -Properties LockedOut
}

As always, feel free to use this script or any portion of it that you find useful. However, if you do, I'd appreciate attribution and a pointer back to my blog.