I was fiddling around with PowerShell the other day. I spend my day in front of a PowerShell prompt and am always looking for ways to solve problems or answer questions without taking my hands off the keyboard. For some reason, I started thinking about metric conversions. How many feet are 50 meters? How many meters is 21.5 feet? For this sort of thing, I have a rough idea. But not something like converting grams to ounces. But I can look up a formula and use PowerShell.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
$v = 123.456
$v/28.35
This snippet will convert the value of grams ($v) to ounces. Seems pretty simple. I can even build a function around it.
Function Convert-Gram {
[cmdletbinding()]
[alias("gr2oz")]
[outputType([System.Double])]
Param(
[Parameter(Position = 0, HelpMessage = "Specify a gram value.")]
[double]$Value,
[Parameter(HelpMessage = "Round the result to this many decimal places")]
[int]$Round = 2
)
$from = "grams"
$to = "ounces"
Write-Verbose "Converting $value $from to $to"
[math]::Round($value / 28.35, $round)
{
In fact, I took the time to manually create all the functions I would need to convert from one unit to another.
Function Convert-Millimeter {
[cmdletbinding()]
[alias("mm2in")]
[OutputType([System.Double])]
Param(
[Parameter(Position = 0, HelpMessage = "Specify a millimeter value.")]
[double]$Value,
[Parameter(HelpMessage = "Round the result to this many decimal places")]
[int]$Round = 2
)
$from = "millimeters"
$to = "inches"
Write-Verbose "Converting $value $from to $to"
[math]::Round($value/25.4, $round)
}
When I finished, I realized I had spent a lot of time manually scripting. What I could have done is to let PowerShell write the functions for me!
Meta-Scripting
I decided to see if I could write PowerShell code to write PowerShell code. To begin, I need some guidelines. First, all I wanted was the function to exist in my PowerShell session. I didn't require a file. I wanted to follow a standard naming convention using the Convert verb. I wanted each command to have a short alias to make it easier to use from the console.
My meta function would need to accept parameters for the from unit, e.g. Gram, and the to unit, e.g. Ounce. This should generate a function called Convert-GramToOunce with an alias of gr2oz. The new function name is simple enough to construct.
$name = "global:Convert-$($from)To$($To)"
You'll notice I'm using the global: prefix. This is because, ultimately, I'm going to create the function in the Function: PSDrive.
New-Item -Path function: -Name $name -Value $value -Force
But because of scope, when this runs in the meta function, it doesn't create the function in the global scope that I am expecting. The New-Item cmdlet doesn't have a -Scope parameter like some cmdlets do, i.e., New-PSDrive. Instead, I have to rely on the global: prefix to give PowerShell a hint.
The value of the function will be a scriptblock built from a here-string.
$value = [scriptblock]::Create($body)
The here-string started as a copy of my original function body.
$body = @"
$help
[Cmdletbinding()]
[Alias("$alias")]
[OutputType([System.Double])]
Param(
[Parameter(Position = 0, Mandatory,ValueFromPipeline,HelpMessage = "Specify a $($from.tolower()) value.")]
[ValidateNotNullOrEmpty()]
$(If ($Validation) {
$v = "[$validation]"
$v
})
[double]`$Value,
[Parameter(HelpMessage = "Round the result to this many decimal places. Specify a value between 0 and 10.")]
[ValidateRange(0,10)]
[int]`$Round = 2,
[Parameter(HelpMessage = "Get a rich object result.")]
[switch]`$Detailed
)
Process {
Write-Verbose "Converting `$value from $($from.tolower()) to $($to.ToLower()) rounded to `$round decimal places."
`$r = [math]::Round(($code),`$round)
if (`$Detailed) {
[PSCustomObject]@{
$From = `$Value
$To = `$r
}
}
else {
`$r
}
}
"@
Variables like $code and $from will be expanded from parameter values. But I have to be careful to escape variables like Detailed. I want the final code to use $Detailed from the function. Not replace it with a value from my meta function.
I should point out that even though the function includes an Alias definition, creating the function item doesn't process this directive. I need to manually define the alias in my meta function.
Set-Alias -Name $alias -Value $name -Scope Global
Notice the use of the Scope parameter. I build the alias from a hashtable of names.
$abbreviations = @{
gram = "gr"
ounce = "oz"
meter = "m"
feet = "ft"
millimeter = "mm"
inch = "in"
mile = "mi"
kilometer = "km"
kilogram = "kg"
pound = "lb"
fahrenheit = "f"
celsius = "c"
yard = "yd"
liter = "l"
quart = "qt"
milliliter = "ml"
kelvin = "k"
}
$alias = "{0}2{1}" -f $abbreviations[$from], $abbreviations[$to]
This is how I create the gr2oz alias.
My meta function takes a string for the conversion code. This is the algorithm I was using in my original metric conversion function.
Adding Help
I also decided to autogenerate comment-based help. I am dynamically creating functions based on a template. Generating help should be no different. I can create a here-string and drop in parameter values. I wrote a separate function for this task.
Function New-MetricFunctionHelp {
[cmdletbinding()]
Param (
[Parameter(ValueFromPipelineByPropertyName)]
[string]$From,
[Parameter(ValueFromPipelineByPropertyName)]
[string]$To,
[string]$Alias,
[double]$ExampleValue
)
$cmd = "Convert-$($From)To$($To)"
#create the example value object
$obj = [pscustomobject] @{
$From = 1
$To = $ExampleValue
} | Out-String
$h = @"
<#
.Synopsis
Convert a value from $From to $To.
.Description
Use this function to convert values between $from and $to. The default output
is the converted value. Or you can use -Detailed to get a rich object. See examples.
.Parameter Value
Specify a value for the $($from.tolower()) unit.
.Parameter Round
Round the result to this many decimal places. Specify a value between 0 and 10.
.Parameter Detailed
Get a rich object result.
.Example
PS C:\> $cmd 1
$ExampleValue
Get a value result.
.Example
PS C:\> $cmd 1 -detailed
$($obj.trim())
Get an object result.
.Example
PS C:\> $alias 1
$ExampleValue
Using the function alias.
.Link
Convert-$($To)To$($From)
.Notes
This command has an alias of $alias. This is an auto-generated function.
.Inputs
System.Double
.Outputs
System.Double
#>
"@
$h
}
This function is called within New-MetricFunction. Because I need an actual value for the sample, I generate one and pass it to the help function.
$b = [scriptblock]::create("param (`$value) [math]::Round($code,2)")
$v1 = Invoke-Command $b -ArgumentList 1
$help = New-MetricFunctionHelp -From $From -to $To -Alias $alias -ExampleValue $v1
Re-running my command to create Convert-GramToOunce generates this code.
<#
.Synopsis
Convert a value from Gram to Ounce.
.Description
Use this function to convert values between Gram and Ounce. The default output
is the converted value. Or you can use -Detailed to get a rich object. See examples.
.Parameter Value
Specify a value for the gram unit.
.Parameter Round
Round the result to this many decimal places. Specify a value between 0 and 10.
.Parameter Detailed
Get a rich object result.
.Example
PS C:\> Convert-GramToOunce 1
0.04
Get a value result.
.Example
PS C:\> Convert-GramToOunce 1 -detailed
Gram Ounce
---- -----
1 0.04
Get an object result.
.Example
PS C:\> gr2oz 1
0.04
Using the function alias.
.Link
Convert-OunceToGram
.Notes
This command has an alias of gr2oz. This is an auto-generated function.
.Inputs
System.Double
.Outputs
System.Double
#>
[Cmdletbinding()]
[Alias("gr2oz")]
[OutputType([System.Double])]
Param(
[Parameter(Position = 0, Mandatory,ValueFromPipeline,HelpMessage = "Specify a gram value.")]
[ValidateNotNullOrEmpty()]
[double]$Value,
[Parameter(HelpMessage = "Round the result to this many decimal places. Specify a value between 0 and 10.")]
[ValidateRange(0,10)]
[int]$Round = 2,
[Parameter(HelpMessage = "Get a rich object result.")]
[switch]$Detailed
)
Process {
Write-Verbose "Converting $value from gram to ounce rounded to $round decimal places."
$r = [math]::Round(($value/28.35),$round)
if ($Detailed) {
[PSCustomObject]@{
Gram = $Value
Ounce = $r
}
}
else {
$r
}
}
Generating from Data
Still with me? Here's the complete meta scripting function.
Function New-MetricFunction {
[cmdletbinding(SupportsShouldProcess)]
Param(
[Parameter(Mandatory,ValueFromPipelineByPropertyName)]
[string]$From,
[Parameter(Mandatory,ValueFromPipelineByPropertyName)]
[string]$To,
[Parameter(Mandatory,ValueFromPipelineByPropertyName)]
[string]$Code,
[Parameter(ValueFromPipelineByPropertyName)]
[string]$Validation
)
Begin {
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)"
#hash table of unit abbreviations used to construct aliases
$abbreviations = @{
gram = "gr"
ounce = "oz"
meter = "m"
feet = "ft"
millimeter = "mm"
inch = "in"
mile = "mi"
kilometer = "km"
kilogram = "kg"
pound = "lb"
fahrenheit = "f"
celsius = "c"
yard = "yd"
liter = "l"
quart = "qt"
milliliter = "ml"
kelvin = "k"
}
} #begin
Process {
#make sure From and To are in proper case
$from = (Get-Culture).TextInfo.ToTitleCase($from.toLower())
$to = (Get-Culture).TextInfo.ToTitleCase($to.toLower())
#define a function name that will be found in the global scope
$name = "global:Convert-$($from)To$($To)"
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Creating function Convert-$($from)To$($To)"
#construct an alias from the data
$alias = "{0}2{1}" -f $abbreviations[$from], $abbreviations[$to]
#build comment-based help
$b = [scriptblock]::create("param (`$value) [math]::Round($code,2)")
$v1 = Invoke-Command $b -ArgumentList 1
$help = New-MetricFunctionHelp -From $From -to $To -Alias $alias -ExampleValue $v1
# this here string will be turned into the function's scriptblock. I need to be careful
# to escape $ where I want it to remain a variable in the output.
$body = @"
$help
[Cmdletbinding()]
[Alias("$alias")]
[OutputType([System.Double])]
Param(
[Parameter(Position = 0, Mandatory,ValueFromPipeline,HelpMessage = "Specify a $($from.tolower()) value.")]
[ValidateNotNullOrEmpty()]
$(If ($Validation) {
$v = "[$validation]"
$v
})
[double]`$Value,
[Parameter(HelpMessage = "Round the result to this many decimal places. Specify a value between 0 and 10.")]
[ValidateRange(0,10)]
[int]`$Round = 2,
[Parameter(HelpMessage = "Get a rich object result.")]
[switch]`$Detailed
)
Process {
Write-Verbose "Converting `$value from $($from.tolower()) to $($to.ToLower()) rounded to `$round decimal places."
`$r = [math]::Round(($code),`$round)
if (`$Detailed) {
[PSCustomObject]@{
$From = `$Value
$To = `$r
}
}
else {
`$r
}
}
"@
$value = [scriptblock]::Create($body)
if ($PSCmdlet.ShouldProcess($name)) {
New-Item -Path function: -Name $name -Value $value -Force
}
if ($PSCmdlet.ShouldProcess($name, "Create alias $alias")) {
#need to manually create the alias even though it is defined in the function
Set-Alias -Name $alias -Value $name -Scope Global
}
} #process
End {
Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)"
} #end
}
The function also allows me to specify a parameter validation string for the value. Also, note that I configured parameters to accept pipeline input by property name.
I can use an external data source such as a CSV file to generate all of the functions. In the script file with the previously defined functions, I'm defining the CSV data like this.
#use single quote for the here-string so that $value doesn't get
#treated as a variable upon conversion
$data = @'
From,To,Code,Validation
Gram,Ounce,$value/28.35,ValidateScript({$_ -gt 0})
Ounce,Gram,$value*28.35,ValidateScript({$_ -gt 0})
Millimeter,Inch,$value/25.4,ValidateScript({$_ -gt 0})
Inch,Millimeter,$value*25.4,ValidateScript({$_ -gt 0})
Meter,Feet,$value*3.281,ValidateScript({$_ -gt 0})
Feet,Meter,$value/3.281,ValidateScript({$_ -gt 0})
Kilometer,Mile,$value/1.609,ValidateScript({$_ -gt 0})
Mile,Kilometer,$value*1.609,ValidateScript({$_ -gt 0})
Kilogram,Pound,$value*2.205,ValidateScript({$_ -gt 0})
Pound,Kilogram,$value/2.205,ValidateScript({$_ -gt 0})
Yard,Meter,$value/1.094,ValidateScript({$_ -gt 0})
Meter,Yard,$value*1.094,ValidateScript({$_ -gt 0})
Liter,Quart,$value*1.057,ValidateScript({$_ -gt 0})
Quart,Liter,$value/1.057,ValidateScript({$_ -gt 0})
Milliliter,Ounce,$value/29.574,ValidateScript({$_ -gt 0})
Ounce,Milliliter,$value*29.574,ValidateScript({$_ -gt 0})
Celsius,Fahrenheit,($value*(9/5)) + 32
Fahrenheit,Celsius,($value - 32)*(5/9)
Kelvin,Fahrenheit,($value-273.15)*(9/5)+32
Fahrenheit,Kelvin,($value-32)*(5/9)+273.15
Kelvin,Celsius,($value - 273.15)
Celsius,Kelvin,($value + 273.15)
'@
Importing this data and passing it to the meta scripting functions is a one-line command.
$new = $data | ConvertFrom-Csv | New-MetricFunction
Finally, to make it easier for me to remember all of the new commands, I'll create a "cheat sheet."
$metric = $new | Select-Object -Property Name, @{Name = "Alias"; Expression = { Get-Alias -Definition "global:$($_.name)" } }
$metric
Assuming I dot-source the PowerShell script file, I'll have a reference variable.
With one command, I created all of these functions and loaded them into my PowerShell session.
If I decide to change something, like the verbose message, I can modify New-MetricFunction and regenerate the functions. I don't have to make changes in 22 different files.
Summary
While I value the results and will use these metric conversion functions, my real purpose is to share the meta scripting content and techniques. I don't have another project in mind to use this, but I'll be better prepared when I do. If you find a way to use these techniques, I hope you'll share.
2 thoughts on “Metric Meta PowerShell Scripting”
Comments are closed.