I have used closures in Powershell to create classes with static and instance methods. Is there a better way to do this?
Create and object with a static "method".
function person_get_type {
return 'Person'
}
function make_person {
param
(
[parameter(mandatory=$true)][string]$name)
$properties = #{'name'=$name;'get_type'=$null}
$person = New-Object –TypeName PSObject –Prop $properties
$person.get_type = ${function:person_get_type}
return $person
}
$person = make_person -name 'James'
$type = $person.get_type.InvokeReturnAsIs()
Write-Host $type
Create an object with an instance "method".
function dog_get_type {
return 'Dog'
}
function dog_make_get_name{
param
(
[Parameter(Mandatory=$true)][System.Management.Automation.PSObject]$self
)
return {return $self.name}.GetNewClosure()
}
function dog_make_set_name{
param
(
[Parameter(Mandatory=$true)][System.Management.Automation.PSObject]$self
)
return {param([parameter(mandatory=$true)][string]$name) $self.name = $name}.GetNewClosure()
}
function make_dog {
param
(
[parameter(mandatory=$true)][string]$name
)
$properties = #{'name'=$name;'get_type'=$null;'get_name'=$null;'set_name'=$null}
$dog = New-Object –TypeName PSObject –Prop $properties
$dog.get_type = ${function:dog_get_type}
$dog.get_name = dog_make_get_name -self $dog
$dog.set_name = dog_make_set_name -self $dog
return $dog
}
$dog = make_dog -name 'Spot'
$name = $dog.get_name.InvokeReturnAsIs()
Write-Host $name
$dog.set_name.Invoke("Paws")
$name = $dog.get_name.InvokeReturnAsIs()
Write-Host $name
$stuff = #($person,$dog)
foreach($thing in $stuff) {
Write-Host $thing.get_type.InvokeReturnAsIs()
}
I've see where it's possible to use this approach:
$object = New-Module -AsCustomObject -ScriptBlock {...}
But I don't see that's possible to create instance methods using this approach.
Instance methods should be easy using New-Module. Your instance fields are the top level variables in the scriptblock e.g.:
$sb = {
param($theName,$theAge,$theBreed)
$Name = $theName
$Age = $theAge
$Breed = $theBreed
$global:NumDogs++
function Description {
"Dog named $Name, age $Age, breed $Breed"
}
function get_NumDogs {
"Total number of dogs is $NumDogs"
}
Export-ModuleMember -Variable Name,Age,Breed -Function Description,get_NumDogs
}
$dog1 = New-Module $sb -AsCustomObject -ArgumentList 'Mr. Bill',1,'Jack Russell'
$dog1.Name
$dog1.Age
$dog1.Description()
$dog1.get_NumDogs()
$dog2 = New-Module $sb -AsCustomObject -ArgumentList Fluffy,3,Poodle
$dog2.Name
$dog2.Age
$dog2.Description()
$dog2.get_NumDogs()
Related
Suppose you have the following function:
Function Test-Function {
Param (
[String[]]$ComputerNames = #($env:COMPUTERNAME, 'PC2'),
[String]$PaperSize = 'A4'
)
}
Get-DefaultParameterValuesHC -Path 'Test-Function'
Now to get the default values in the function arguments one can use AST:
Function Get-DefaultParameterValuesHC {
[OutputType([hashtable])]
Param (
[Parameter(Mandatory)]$Path
)
$ast = (Get-Command $Path).ScriptBlock.Ast
$selectParams = #{
Property = #{
Name = 'Name';
Expression = { $_.Name.VariablePath.UserPath }
},
#{
Name = 'Value';
Expression = { $_.DefaultValue.Extent.Text -replace "`"|'" }
}
}
$result = #{ }
$defaultValueParameters = #($ast.FindAll( {
$args[0] -is [System.Management.Automation.Language.ParameterAst] }
, $true) |
Where-Object { $_.DefaultValue } |
Select-Object #selectParams)
foreach ($d in $defaultValueParameters) {
$result[$d.Name] = foreach ($value in $d.Value) {
$ExecutionContext.InvokeCommand.ExpandString($value)
}
}
$result
}
The issue here is that the argument for $ComputerNames is read as a string while it is actually an array of string.
Is there a way that PowerShell can covnert a string to an array? Or even better, read the value correctly in the first place?
You need to look deeper into the AST structure...
I recommend you to play around with this PowerShell: AST Explorer GUI:
For your specific example:
Function Test-Function {
Param (
[String[]]$ComputerNames = #($env:COMPUTERNAME, 'PC2'),
[String]$PaperSize = 'A4'
)
}
$FunctionDefinitionAst = (Get-Command 'Test-Function').ScriptBlock.Ast
$Body = $FunctionDefinitionAst.Body
$ParamBlock = $Body.ParamBlock
$CNParameter = $ParamBlock.Parameters | Where-Object { $_.Name.VariablePath.UserPath -eq 'ComputerNames' }
$DefaultValue = $CNParameter.DefaultValue
$DefaultValue.SubExpression.Statements.PipelineElements.Expression.Elements
VariablePath : env:COMPUTERNAME
Splatted : False
StaticType : System.Object
Extent : $env:COMPUTERNAME
Parent : $env:COMPUTERNAME, 'PC2'
StringConstantType : SingleQuoted
Value : PC2
StaticType : System.String
Extent : 'PC2'
Parent : $env:COMPUTERNAME, 'PC2'
It's a bit of a hackish solution but this is what I came up with to solve the issue of not returning an array of string:
Function Get-DefaultParameterValuesHC {
Param (
[Parameter(Mandatory)]$Path
)
$ast = (Get-Command $Path).ScriptBlock.Ast
$selectParams = #{
Property = #{
Name = 'Name';
Expression = { $_.Name.VariablePath.UserPath }
},
#{
Name = 'Value';
Expression = {
if ($_.DefaultValue.StaticType.BaseType.Name -eq 'Array') {
$_.DefaultValue.SubExpression.Extent.Text -split ',' |
ForEach-Object { $_.trim() -replace "`"|'" }
}
else {
$_.DefaultValue.Extent.Text -replace "`"|'"
}
}
}
}
$result = #{ }
$defaultValueParameters = #($ast.FindAll( {
$args[0] -is [System.Management.Automation.Language.ParameterAst] }
, $true) |
Where-Object { $_.DefaultValue } |
Select-Object #selectParams)
foreach ($d in $defaultValueParameters) {
$result[$d.Name] = foreach ($value in $d.Value) {
$ExecutionContext.InvokeCommand.ExpandString($value)
}
}
$result
}
ExpandPath will only expand variables inside strings. To get the actual values (and not just the definition) you could use Invoke-Expression:
function Get-DefaultParameterValuesHC {
[OutputType([hashtable])]
Param (
[Parameter(Mandatory)]$Path
)
$result = #{ }
(Get-Command $Path).ScriptBlock.Ast.Body.ParamBlock.Parameters | where {$_.DefaultValue} | foreach {
$result[$_.Name.VariablePath.UserPath] = Invoke-Expression $_.DefaultValue.Extent.Text
}
$result
}
NOTE: This will actually invoke the default declaration, so any logic inside that expression will be run, just as when running the function. For example, a default value of $Parameter = (Get-Date) will always invoke Get-Date.
It would be preferable to create a function, that only returns the default declarations, and let the user decide to invoke the expression or not:
function Get-DefaultParameterDeclarations {
Param (
[Parameter(Mandatory, Position = 0)]
[string]$CommandName
)
(Get-Command $CommandName).ScriptBlock.Ast.Body.ParamBlock.Parameters | where {$_.DefaultValue} |
foreach {
[PSCustomObject]#{
Name = $_.Name.VariablePath.UserPath
Expression = $_.DefaultValue.Extent.Text
}
}
}
# get the declarations and (optionally) invoke the expressions:
Get-DefaultParameterDeclarations 'Test-Function' |
select Name, #{n="DefaultValue"; e={Invoke-Expression $_.Expression}}
How do I add String data to the array of the class OnePerson in order to group the data?
$people = import-csv "./people.csv"
class OnePerson {
[String] $Info
people () { }
}
$myPerson = New-Object -TypeName OnePerson
$manyPeople = New-Object System.Object
$myArray = #()
ForEach ($person in $people) {
if ($person -match '[0-9]') {
Write-host $person
#add $person string to the array $Info of $myPerson
}
else {
write-host "new person"
write-host $person
$myArray += $myPerson
$myPerson = New-Object -TypeName OnePerson
}
}
write-host $myArray
output:
thufir#dur:~/flwor/people$
thufir#dur:~/flwor/people$ pwsh foo.ps1
new person
#{people=joe}
#{people=phone1}
#{people=phone2}
#{people=phone3}
new person
#{people=sue}
#{people=cell4}
#{people=home5}
new person
#{people=alice}
#{people=atrib6}
#{people=x7}
#{people=y9}
#{people=z10}
OnePerson OnePerson OnePerson
thufir#dur:~/flwor/people$
Example on how you can use OnePerson class and adding that element to your array is,
class OnePerson {
[String] $Info
OnePerson () { }
OnePerson ([String]$newinfo) { $this.Info = $newInfo }
}
$myArray = #()
$myArray += [OnePerson]::new("John")
$myArray += [OnePerson]::new("Smith")
Constructors you use in class have to have the same name as your class itself. Once the person has been created and added to myArray, it no longer exists on it's own, only available via reference from myArray
If you have a hashtable containing nested hashtables, how to convert that to a PsObject recursively?
#{
Foo = #{
Bar = #{
Key = 'Value'
Test = 1
}
}
}
The result should be
$_.Foo.Bar.Key = 'Value'
$_.Foo.Bar.Test = 1
One approach is to create a recursive function:
function ConvertTo-PsObject {
param (
[hashtable] $Value
)
foreach ( $key in $Value.Keys | Where-Object { $Value[$_].GetType() -eq #{}.GetType() } ) {
$Value[$key] = ConvertTo-PsObject $Value[$key]
}
New-Object PSObject -Property $Value | Write-Output
}
Another way of doing it which hasn't been mentioned so far, and is more appropriate in many cases is to use New-Object PSObject.
$a = #{
Foo = #{
Bar = #{
Key = 'Value'
Test = 1
}
}
}
$b = New-Object -Type PSObject -Property $a
Doing it this way makes it work correctly with Format-Table for instance, and probably other places too.
Cast it to a [PsCustomObject]:
$a = [PsCustomObject]#{
Foo = #{
Bar = #{
Key = 'Value'
Test = 1
}
}
}
$a.Foo.Bar.Key # --> "Value"
$a.Foo.Bar.Test # --> 1
I would like to find all cells in a range based on a property value using EPPlus. Let's say I need to find all cells with bold text in an existing spreadsheet. I need to create a function that will accept a configurable properties parameter but I'm having trouble using a property stored in a variable:
$cellobject = $ws.cells[1,1,10,10]
$properties = 'Style.Font.Bold'
$cellobject.$properties
$cellobject.{$properties}
$cellobject.($properties)
$cellobject."$properties"
None of these work and cause a call depth overflow.
If this way wont work, is there something in the library I can use?
Edited: To show the final solution I updated the function with concepts provided by HanShotFirst...
function Get-CellObject($ExcelSheet,[string]$PropertyString,[regex]$Value){
#First you have to get the last row with text,
#solution for that is not provided here...
$Row = Get-LastUsedRow -ExcelSheet $ExcelSheet -Dimension $true
while($Row -gt 0){
$range = $ExcelSheet.Cells[$Row, 1, $Row, $ExcelSheet.Dimension.End.Column]
foreach($cellObject in $range){
if($PropertyString -like '*.*'){
$PropertyArr = $PropertyString.Split('.')
$thisObject = $cellObject
foreach($Property in $PropertyArr){
$thisObject = $thisObject.$Property
if($thisObject -match $Value){
$cellObject
}
}
}
else{
if($cellObject.$PropertyString -match $Value){
$cellObject
}
}
}
$Row--
}
}
#The ExcelSheet parameter takes a worksheet object
Get-CellObject -ExcelSheet $ws -Property 'Style.Font.Bold' -Value 'True'
Dot walking into properties does not really work with a string. You need to separate the layers of properties. Here is an example for an object with three layers of properties.
# create object
$props = #{
first = #{
second = #{
third = 'test'
}
}
}
$obj = New-Object -TypeName psobject -Property $props
# outputs "test"
$obj.first.second.third
# does not work
$obj.'first.second.third'
# outputs "test"
$a = 'first'
$b = 'second'
$c = 'third'
$obj.$a.$b.$c
In your example this would be something like this:
$cellobject = $ws.cells[1,1,10,10]
$p1 = 'Style'
$p2 = 'Font'
$p3 = 'Bold'
$cellobject.$p1.$p2.$p3
Or you can do it a bit dynamic. This should produce the same result:
$cellobject = $ws.cells[1,1,10,10]
$props = 'Style.Font.Bold'.Split('.')
$result = $cellobject
foreach ($prop in $props) {
$result = $result.$prop
}
$result
And since its Friday, here is a function for it :)
function GetValue {
param (
[psobject]$InputObject,
[string]$PropertyString
)
if ($PropertyString -like '*.*') {
$props = $PropertyString.Split('.')
$result = $InputObject
foreach ($prop in $props) {
$result = $result.$prop
}
} else {
$result = $InputObject.$PropertyString
}
$result
}
# then call the function
GetValue -InputObject $cellobject -PropertyString 'Style.Font.Bold'
I have a collection of PSObjects over which I'd like to iterate, setting contituent member's properties. I set up a for loop and pass the current object by reference to a function, but do not know how to access the object properties. Example:
function create-object {
$foo = new-object -TypeName PSObject -Prop `
#{
"p1" = $null
"p2" = $null
}
$foo
}
$objCol = #()
foreach ($k in (1 .. 3)){$objCol += create-object}
for ($i=0;$i -le $objCol.Length;$i++) {
Write-Host "hi"
reftest ([ref]$objCol[$i])
}
function reftest([ref]$input)
{
$input.p1.value="property1"
}
$objCol
... returns Property 'p1' cannot be found on this object --how do I set $object.p1 from a function by reference?
I've reformatted your example, also incorporating the change of $input to a different name, $arg, as pointed out by Christian. The following works:
function create-object {
$foo = new-object PSObject -Property #{
"p1" = $null
"p2" = $null
}
$foo
}
function reftest($arg)
{
$arg.p1="property1"
}
$objCol = #()
(1..3) | % {$objCol += create-object}
for ($i=0;$i -lt $objCol.Length;$i++) {
reftest $objCol[$i]
}
$objCol