Tag Archives: parameter

Rename Hashtable Key Revised

Last week I posted an advanced PowerShell function to rename a hashtable key. As usual, the more I worked with it the more I realized it was missing something – namely the ability the take a pipelined object. My original version assumed you had saved the hashtable to a variable. But as I was working with ConvertTo-Hashtable I realized the shortcoming. The solution was to modify Rename-Hashtable so that it could accept a hashtable as a piped value.

I won’t go through the function again. You can read the original post to learn more about how it works. Let’s look at what changed. Because I wanted to retain the option to also specify a variable name, I created two parameters sets. One for the piped object and one for the variable name.


[cmdletbinding(SupportsShouldProcess=$True,DefaultParameterSetName="Pipeline")]

Param(
[parameter(Position=0,Mandatory=$True,
HelpMessage="Enter the name of your hash table variable without the `$",
ParameterSetName="Name")]
[ValidateNotNullorEmpty()]
[string]$Name,
[parameter(Position=0,Mandatory=$True,
ValueFromPipeline=$True,ParameterSetName="Pipeline")]
[ValidateNotNullorEmpty()]
[object]$InputObject,
[parameter(position=1,Mandatory=$True,
HelpMessage="Enter the existing key name you want to rename")]
[ValidateNotNullorEmpty()]
[string]$Key,
[parameter(position=2,Mandatory=$True,
HelpMessage="Enter the NEW key name")]
[ValidateNotNullorEmpty()]
[string]$NewKey,
[switch]$Passthru,
[ValidateSet("Global","Local","Script","Private",0,1,2,3)]
[ValidateNotNullOrEmpty()]
[string]$Scope="Global"
)

I defined parameter sets called Pipeline and Name and made the former the default in the cmdletbinding attribute. Because the remaining parameters would be in both parameter sets I didn’t specify one. When looking at the function’s help you can see the result.

rename-hashtable-paramsets

Because I’m taking input from the pipeline, I needed to add a Process scriptblock. Within the scriptblock, if an object has been piped in, I turn on the passthru variable and create a temporary copy of the piped in hashtable.


Process {
#validate Key and NewKey are not the same
if ($key -eq $NewKey) {
Write-Warning "The values you specified for -Key and -NewKey appear to be the same. Names are NOT case-sensitive"
Return
}

Try {
#validate variable is a hash table
if ($InputObject) {
$name="tmpInputHash"
Set-Variable -Name $name -Scope $scope -value $InputObject
$Passthru=$True
}
...

The rest of the code worked just fine and there was no reason to change it. All I needed to do was transform the -Inputobject value into the -Name value since I already had code that used $Name. Sometimes you need separate code blocks but in this case I didn’t. Once the transformation is complete, the rest of the function runs as originally designed. With this version I can now run commands like this:


PS C:\> $h = get-service spooler -computer Serenity | convertto-hashtable -NoEmpty -Exclude CanStop,CanPauseAndcontinue | rename-hashtable -key machinename -new computername
PS C:\> $h

Name Value
---- -----
computername Serenity
Name spooler
ServiceName spooler
RequiredServices {RPCSS, http}
DependentServices {Fax}
ServiceType Win32OwnProcess, InteractiveProcess
Status Running
ServicesDependedOn {RPCSS, http}
ServiceHandle SafeServiceHandle
DisplayName Print Spooler

Download Rename-Hashtable2 and give it a go.

PowerShell Scripting with [ValidateCount]

Here’s another parameter validation attribute you might want to use in your PowerShell scripting and functions. If your parameter can take an array of values, you might want to limit that array to a certain size. For example, your parameter can take an array of computer names but you don’t want to process more than 5 for some reason. This is where [ValidateCount()] comes in to play.

This attribute takes two values, the minimum number of accepted parameter values and the maximum.


[ValidateCount(1,10)]
[string[]]$Computername

If used, this would mean I would need at least one computername but no more than 10. You could also set both values the same if you wanted an exact number:


[ValidateCount(2,2)]
[int[]]$Numbers

Now, I’d have to pass exactly 2 numbers as parameter values. Let’s look at a more complete example.


#requires -version 2.0

Param (
[Parameter(Position=0,Mandatory=$True)]
[ValidateCount(1,5)]
[string[]]$Name
)

Foreach ($item in $name) {

#display the name in a random color
Write-Host $item -ForegroundColor ([system.consoleColor]::GetValues("system.consolecolor") | get-random)

}

This simple script writes each name in a random color, assuming I pass no more than 5 names.

If I exceed that count, PowerShell will throw a tantrum (I mean exception).

When you use this validation test, be sure your parameter is set to accept an array of values, e.g. [string[]]. If you’d like to try out my sample code feel free to download Demo-ValidateCount.

PowerShell Scripting with [ValidateLength]

In continuing the exploration of parameter validation attributes, today we’ll look at [ValidateLength()]. You can use this attribute in your PowerShell scripting to validate that a parameter value is at least a certain length and no more and a certain length. In other words, it has to be just right. Here’s what it looks like:


[ValidateLength(2,10)]
[string]$Fooby

In this example any value for -Fooby must be at least 2 characters long and no more than 10. Anything outside of that range and PowerShell will raise an exception. You have to supply both values in the validation attribute. An alternative would be to use [ValidateScript()].


[ValidateScript({ $_.Length -ge 5})]
[string]$Name

Now, any value for -Name must be at least 5 characters and there is no upper limit. Here’s a more complete example.


#requires -version 2.0

[cmdletbinding(SupportsShouldProcess=$True)]

Param (
[Parameter(Position=0,Mandatory=$True,HelpMessage="Enter name between 5 and 15 characters")]
[ValidateLength(5,15)]
[string]$Name,
[Parameter(Position=1,Mandatory=$True,HelpMessage="Enter password between 7 and 64 characters")]
[ValidateLength(7,64)]
[ValidatePattern({^\S+$})]
[string]$Password,
[string]$Computername=$env:computername,
[switch]$Passthru
)

Write-Host "Creating $name with password of $Password on $computername" -ForegroundColor Green
[ADSI]$Server="WinNT://$computername"
$User=$server.Create("User",$Name)

if ($pscmdlet.ShouldProcess($User.Path)) {
Write-Host "Committing new account changes" -ForegroundColor Green

<#
#uncomment the next lines if you really, really want to do this
$User.SetInfo()
Write-Host "Setting password" -ForegroundColor Green
$User.SetPassword($Password)
If ($passthru) {
Write-Output $User
}
#>
}

This script will create a local user account. I’m asking that the user name be between 5 and 15 characters and that the password be between 7 and 64 characters. I’ve also added a second validation check on the password using [ValidatePattern()] to verify it doesn’t contain any spaces.

The exception message you see with [ValidateLength()] depends on where you fall short.


PS S:\> .\Demo-ValidateLength.ps1 Jeff Password123
C:\scripts\Demo-ValidateLength.ps1 : Cannot validate argument on parameter 'Nam
e'. The number of characters (4) in the argument is too small. Specify an argum
ent whose length is greater than or equal to "5" and then try the command again
.
At line:1 char:26
+ .\Demo-ValidateLength.ps1 <<<< Jeff Password123
+ CategoryInfo : InvalidData: (:) [Demo-ValidateLength.ps1], Para
meterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Demo-ValidateLe
ngth.ps1

PS S:\> .\Demo-ValidateLength.ps1 JeffJeffJeffJeff Password123
C:\scripts\Demo-ValidateLength.ps1 : Cannot validate argument on parameter 'Nam
e'. The argument length of 16 is too long. Shorten the length of the argument t
o less than or equal to "15" and then try the command again.
At line:1 char:26
+ .\Demo-ValidateLength.ps1 <<<< JeffJeffJeffJeff Password123
+ CategoryInfo : InvalidData: (:) [Demo-ValidateLength.ps1], Para
meterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Demo-ValidateLe
ngth.ps1

If you'd like, you can download Demo-ValidateLength and try it out for yourself.

PowerShell Scripting with [ValidateSet]

Today we’ll continue our exploration of the parameter validation attributes you can use in you PowerShell scripting. We’ve already looked at [ValidateRange] and [ValidateScript]. Another attribute you are likely to use is [ValidateSet()]. You can use this to verify that the parameter value belongs to a pre-defined set.

To use, specify a comma separated list of possible values.

[ValidateRange("Apple","Banana","Cherry")]
[string]$Fruit

If the person running the script specifies something other than “Apple”, “Banana”, or “Cherry” as a value for -Fruit, PowerShell will throw an exception and the script will fail to run. And in case you were wondering, this is not case-sensitive.

If you are going to use this attribute, I recommend providing documentation either in comment based help and/or as part of a help message so the user knows what values to expect. The PowerShell help system doesn’t automatically detect this attribute and use it in syntax display as you might have seen with other cmdlets.

Here’s a more practical example.


#requires -version 2.0

Param (
[Parameter(Position=0)]
[ValidateSet("System","Application","Security","Directory Service","DNS Server")]
[string]$Log="System",
[ValidateRange(10,1000)]
[int]$Count=100,
[Parameter(Position=1,Mandatory=$True,
HelpMessage="What type of export file do you want to create? Valid choices are CSV, XML, CLIXML.")]
[ValidateSet("csv","xml","clixml")]
[string]$Export,
[ValidateNotNullorEmpty()]
[string]$Path="C:\Work",
[string]$Computername=$env:Computername
)

Write-Verbose "Getting last $Count events from $log event log on $computername"
#base logname
$base="{0:yyyyMMdd}_{1}_{2}" -f (Get-date),$Computername,$Log

Try {
$data=Get-EventLog -LogName $log -ComputerName $Computername -Newest $Count -errorAction Stop
}
Catch {
Write-Warning "Failed to retrieve $log event log entries from $computername. $($_.Exception.Message)"
}

If ($data) {
Write-Verbose "Exporting results to $($export.ToUpper())"
Switch ($Export) {
"csv" { $File=Join-path -Path $Path -ChildPath "$base.csv"
$data | Export-Csv -Path $File
}
"xml" { $File=Join-path -Path $Path -ChildPath "$base.xml"
($data | ConvertTo-XML).Save($File)
}
"clixml" {$File= Join-path -Path $Path -ChildPath "$base.xml"
$data | Export-Clixml -Path $File
}
} #switch

Write-Verbose "Results exported to $File"

} #if $data

The script will get recent events from a specified log on a specified computer and export the results. My script will only export from a short list of logs which I’m validating.


[ValidateSet("System","Application","Security","Directory Service","DNS Server")]
[string]$Log="System",

If an invalid value is detected PowerShell will complain.


PS S:\> .\Demo-ValidateSet.ps1 -comp jdhit-dc01 -log dns -verbose -Export clixml

C:\scripts\Demo-ValidateSet.ps1 : Cannot validate argument on parameter 'Log'.
The argument "dns" does not belong to the set "System,Application,Security,Dire
ctory Service,DNS Server" specified by the ValidateSet attribute. Supply an arg
ument that is in the set and then try the command again.
At line:1 char:45
+ .\Demo-ValidateSet.ps1 -comp jdhit-dc01 -log <<<< dns -verbose -Export clixm
l
+ CategoryInfo : InvalidData: (:) [Demo-ValidateSet.ps1], Paramet
erBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Demo-ValidateSe
t.ps1

The error message displays the expected values, but the better approach might be to include them in a help message, especially if the parameter is mandatory. For example, this script will export results based on -Export.


[Parameter(Position=1,Mandatory=$True,
HelpMessage="What type of export file do you want to create? Valid choices are CSV, XML, CLIXML.")]
[ValidateSet("csv","xml","clixml")]
[string]$Export,

If you'd like to try out my demo script, you can download Demo-ValidateSet.

PowerShell Scripting with [ValidateScript]

The last few days we’ve been looking at parameter validation attributes you might use in a script of function. Yesterday I wrote about [ValidateRange] and demonstrated how you might use it. That attribute works fine for any values that can be evaluated as numbers. But dates are a different story. I got a comment with a suggestion for validating dates and at first glance it appears to work. For a very small range it might, but here’s the code I was testing with.


Param (
[Parameter(Position=0,Mandatory=$True,HelpMessage="Enter a date",ValueFromPipeline=$True)]
[ValidateRange("1/1/2012","4/1/2012")]
$Date
)

Process {
write-host $date -ForegroundColor Green
}

I’m expecting date between 1/1/2012 and 4/1/2012. Anything else should throw an exception. Now watch what happens as I pipe some values to this:


PS S:\> "2/12/2012","5/1/2012","12/1/2011","13/2/2012" | .\Demo-ValidateRange-Da
te.ps1
2/12/2012
C:\scripts\Demo-ValidateRange-Date.ps1 : Cannot validate argument on parameter
'Date'. The 5/1/2012 argument is greater than the maximum allowed range of 4/1/
2012. Supply an argument that is less than 4/1/2012 and then try the command ag
ain.
At line:1 char:79
+ "2/12/2012","5/1/2012","12/1/2011","13/2/2012" | .\Demo-ValidateRange-Date.ps
1 <<<<
+ CategoryInfo : InvalidData: (5/1/2012:String) [Demo-ValidateRan
ge-Date.ps1], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Demo-ValidateRa
nge-Date.ps1

12/1/2011
13/2/2012

Values that are outside the range and even those that are bogus still are accepted. If I cast $date to [DateTime] which is what I would normally do, then nothing works at all. But that's fine because there is another attribute that is better suited to this task: [ValidateScript()].

To use this attribute we'll insert a scriptblock inside the parentheses. The scriptblock can be as complicated as you need it to be, but it must evaluate to either True or False. Use $_ to indicate the parameter value. So using my datetime example, I might use a validation script like this:


Param (
[Parameter(Position=0,Mandatory=$True,HelpMessage="Enter a date",ValueFromPipeline=$True)]
[ValidateScript( {
[datetime]$start="1/1/2012"
$end=Get-Date
($_ -ge $start) -AND ($_ -le $end)
}
)]
[datetime]$Date
)

Process {
write-host $date -ForegroundColor Green
}

With a validation script I have much more flexibility. Now look at the results:


PS S:\> "2/12/2012","5/1/2012","3/15/2012","12/1/2011","13/2/2012" | .\Demo-Vali
dateScript-Date.ps1 | clip
2/12/2012 12:00:00 AM
C:\scripts\Demo-ValidateScript-Date.ps1 : Cannot validate argument on parameter
'Date'. The "
[datetime]$start="1/1/2012"
$end=Get-Date
($_ -ge $start) -AND ($_ -le $end)
" validation script for the argument with value "5/1/2012 12:00:00 AM" did not
return true. Determine why the validation script failed and then try the comma
nd again.
At line:1 char:92
+ "2/12/2012","5/1/2012","3/15/2012","12/1/2011","13/2/2012" | .\Demo-ValidateS
cript-Date.ps1 <<<< | clip
+ CategoryInfo : InvalidData: (5/1/2012:String) [Demo-ValidateScr
ipt-Date.ps1], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Demo-ValidateSc
ript-Date.ps1

3/15/2012 12:00:00 AM
C:\scripts\Demo-ValidateScript-Date.ps1 : Cannot validate argument on parameter
'Date'. The "
[datetime]$start="1/1/2012"
$end=Get-Date
($_ -ge $start) -AND ($_ -le $end)
" validation script for the argument with value "12/1/2011 12:00:00 AM" did no
t return true. Determine why the validation script failed and then try the comm
and again.
At line:1 char:92
+ "2/12/2012","5/1/2012","3/15/2012","12/1/2011","13/2/2012" | .\Demo-ValidateS
cript-Date.ps1 <<<< | clip
+ CategoryInfo : InvalidData: (12/1/2011:String) [Demo-ValidateSc
ript-Date.ps1], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Demo-ValidateSc
ript-Date.ps1

C:\scripts\Demo-ValidateScript-Date.ps1 : The input object cannot be bound to a
ny parameters for the command either because the command does not take pipeline
input or the input and its properties do not match any of the parameters that
take pipeline input.
At line:1 char:92
+ "2/12/2012","5/1/2012","3/15/2012","12/1/2011","13/2/2012" | .\Demo-ValidateS
cript-Date.ps1 <<<<
+ CategoryInfo : InvalidArgument: (13/2/2012:String) [Demo-Valida
teScript-Date.ps1], ParameterBindingException
+ FullyQualifiedErrorId : InputObjectNotBound,Demo-ValidateScript-Date.ps1

The valid dates pass, dates outside the range fail the validation test and the last value which isn't a legal date also fails but with a slightly different error message. As with all of the validation attributes I could have inserted this code into the body of my script and thrown my own errors. That choice is up to you. [ValidateScript()] isn't difficult to use. Just remember to insert your commands into a scriptblock, use $_ for the parameter value, and make sure the scriptblock writes either $True or $False.

PowerShell Scripting with [ValidateRange]

After my post yesterday on using the ValidateScript attribute with PSCredentials, I thought you might find it helpful to have a brief discussion on some other parameter validation attributes such as [ValidateRange()]. You can use this attribute if you want to verify that a given parameter value falls between some range. Typically this is used for numeric values. This attribute is quite easy to use. Here’s a sample script.


Param (
[Parameter(Position=0)]
[string]$Property="WorkingSet",
[Parameter(Position=1,Mandatory=$True,HelpMessage="How many top processes do you want? The maximum is 20.")]
[ValidateRange(1,20)]
[string]$Count,
[ValidateNotNullOrEmpty()]
[string]$Computername=$env:computername
)

$msg="Getting top {0} processes from {1} sorted by {2}" -f $Count,$Computername,$Property
Write-Host $msg -ForegroundColor Green

Get-Process -ComputerName $computername | Sort -Property $property -Descending | Select -first $Count

This script gets the top X number of processes from a computer based on a user-specified property. The default property is WorkingSet. The Count property has [ValidateRange()] attribute that dictates that any value must be between 1 and 20. If you enter a value outside of that range, PowerShell will throw an exception and the script will not run.


PS S:\> .\Demo-ValidateRange.ps1 -Count 25
C:\scripts\Demo-ValidateRange.ps1 : Cannot validate argument on parameter 'Coun
t'. The 25 argument is greater than the maximum allowed range of 20. Supply an
argument that is less than 20 and then try the command again.
At line:1 char:32
+ .\Demo-ValidateRange.ps1 -Count <<<< 25
+ CategoryInfo : InvalidData: (:) [Demo-ValidateRange.ps1], Param
eterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Demo-ValidateRa
nge.ps1

PS S:\>.\Demo-ValidateRange.ps1 -Count 3
Getting top 3 processes from SERENITY sorted by WorkingSet

Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName
------- ------ ----- ----- ----- ------ -- -----------
2288 121 201804 287396 622 1,748.24 7088 chrome
775 111 254700 263620 501 102.31 8104 thunderbird
619 39 254124 261376 472 2,155.51 1152 svchost

Of course, you can skip this and add your own validation test within your script if you prefer to handle errors on your own and perhaps a bit more gracefully.

Using [ValidateRange()] really only works with numeric values. If you wanted to validate if a datetime value fell within a range, you’ll have to turn to something else. At least I have yet to find a way to use [ValidateRange()] with anything other than numbers. But we have options and I’ll be back to show you some of them.

ByValue, I Think He’s Got It

Recently I responded to an email from a student seeking clarification about the difference between ByValue and ByProperty when it comes to parameter binding. This is what makes pipelined expressions work in Windows PowerShell. When you look at cmdlet help, you’ll see that some parameters accept pipeline binding, which is what you are looking for. Often this means you don’t need to resort to a ForEach-Object construct. Here’s an example.

ByValue means if I see something I’ll use it. ByProperty means I’m looking for a specific object property. For example, Get-Service uses the Name property to find a service.

[cc lang="PowerShell"]
PS S:\> help get-service -Parameter Name

-Name
Specifies the service names of services to be retrieved. Wildcards are permitted. By default, Get-Service gets all
of the services on the computer.

Required? false
Position? 1
Default value
Accept pipeline input? true (ByValue, ByPropertyName)
Accept wildcard characters? true
[/cc]

This parameter accepts both types of binding. This means you can do this:

[cc lang="PowerShell"]
PS S:\> “wuauserv” | get-service

Status Name DisplayName
—— —- ———–
Running wuauserv Windows Update
[/cc]

This is an example of byValue. Get-Service sees something in the pipeline and assumes it is a service name. However, for Get-Service this would also work.

[cc lang="PowerShell"]
PS S:\> $all=get-service
PS S:\> $all[0] | get-service

Status Name DisplayName
—— —- ———–
Stopped AeLookupSvc Application Experience
[/cc]

$all[0] is a service object with name property which when piped to Get-Service, finds the property and binds to it. Here’s another example that shows binding by property name can be from any object, not just a service object.

[cc lang="PowerShell"]
PS S:\> new-object psobject -Property @{Name=”wuauserv”;Date=Get-date;computername=$env:computername} | get-service

Status Name DisplayName
—— —- ———–
Running wuauserv Windows Update
[/cc]

This is a custom object with a name property that happens to be valid service name. If you try it with a non-valid name, you’ll get an error which proves it is binding on the property name. The other properties in my New-Object example are just to have something. Let me wrap this discussion with one more example that leverages parameter binding:

[cc lang="PowerShell"]
PS C:\work> get-content servers.txt | where {$_ -AND (Test-Connection $_ -quiet -count 2) } | Select @{Name=”Computername”;Expression={$_.Trim()}} | get-service wuauserv | select Name,status,Machinename

Name Status MachineName
—- —— ———–
wuauserv Running CHI-DC01
wuauserv Running CHI-DC02
wuauserv Stopped CHI-FP01
wuauserv Running CHI-DB01
wuauserv Running CHI-EX01
[/cc]

This is a one line expression that leverages the pipeline and uses some helpful (I hope) techniques. The first part using Get-Content retrieves the list of computer names from the text file. Because the text file might have blank lines and some computers might be offline, each name is piped to Where-Object which will only pass on names that exist (skipping blanks) and that can be pinged. To speed things up I’m only sending 2 pings. Now the fun part. I could use ForEach-Object and pass $_ as the value for -Computername. But according to help, this parameter accepts binding by property name.

[cc lang="PowerShell"]
PS S:\> help get-service -Parameter Computername

-ComputerName
Gets the services running on the specified computers. The default is the local computer.

Type the NetBIOS name, an IP address, or a fully qualified domain name of a remote computer. To specify the local c
omputer, type the computer name, a dot (.), or “localhost”.

This parameter does not rely on Windows PowerShell remoting. You can use the ComputerName parameter of Get-Service
even if your computer is not configured to run remote commands.

Required? false
Position? named
Default value Localhost
Accept pipeline input? true (ByPropertyName)
Accept wildcard characters? false
[/cc]

So I’ll take the computername value coming from Where-Object and use a hash table with Select-Object to define a new “property” name, called Computername. I also take the liberty of trimming off any leading or trailing spaces, just in case. Now I have an object with a property called Computername that is piped to Get-Service which binds on the computername property. The rest is merely formatting.

Look for opportunities to bind by parameter, which means reading cmdlet help which is a good habit to have regardless.