How to work with System.Manangement.Automation.Language... ast - powershell

So I'm trying to create a custom PsAnalyzer rule for the office, and it's my first time using $ast based commands/variables. As a result I'm at a bit of a loss.
I've been using a couple of sites to get my head around the [System.Management.Automation.Language] object class, namely this, this, and this.
For testing purposes I'm using the function below - dodgy looking params intended.
Function IE {
[cmdletbinding()]
Param(
$Url,
$IEWIDTH = 550,
$IeHeight = 450
)
$IE = new-object -comobject InternetExplorer.Application
$IE.top = 200 ; $IE.width = $IEWIDTH ; $IE.height = $IEHEIGHT
$IE.Left = 100 ; $IE.AddressBar = $FALSE ; $IE.visible = $TRUE
$IE.navigate( "file:///$Url" )
}
Then with the code below, I'd expect $IEWIDTH as the only param to fail.
Function Measure-PascalCase {
<#
.SYNOPSIS
The variables names should be in PascalCase.
.DESCRIPTION
Variable names should use a consistent capitalization style, i.e. : PascalCase.
.EXAMPLE
Measure-PascalCase -ScriptBlockAst $ScriptBlockAst
.INPUTS
[System.Management.Automation.Language.ScriptBlockAst]
.OUTPUTS
[Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]
.NOTES
https://msdn.microsoft.com/en-us/library/dd878270(v=vs.85).aspx
https://msdn.microsoft.com/en-us/library/ms229043(v=vs.110).aspx
https://mathieubuisson.github.io/create-custom-rule-psscriptanalyzer/
#>
[CmdletBinding()]
[OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])]
Param
(
[Parameter(Mandatory = $True)]
[ValidateNotNullOrEmpty()]
[System.Management.Automation.Language.ScriptBlockAst]
$ScriptBlockAst
)
Process {
$Results = #()
try {
#region Define predicates to find ASTs.
[ScriptBlock]$Predicate = {
Param ([System.Management.Automation.Language.Ast]$Ast)
[bool]$ReturnValue = $false
if ( $Ast -is [System.Management.Automation.Language.ParameterAst] ){
[System.Management.Automation.Language.ParameterAst]$VariableAst = $Ast
if ( $VariableAst.Left.VariablePath.UserPath -eq 'i' ){
$ReturnValue = $false
} elseif ( $VariableAst.Left.VariablePath.UserPath.Length -eq 3 ){
$ReturnValue = $false
} elseif ($VariableAst.Left.VariablePath.UserPath -cnotmatch '^([A-Z][a-z]+)+$') {
$ReturnValue = $True
}
}
return $ReturnValue
}
#endregion
#region Finds ASTs that match the predicates.
[System.Management.Automation.Language.Ast[]]$Violations = $ScriptBlockAst.FindAll($Predicate, $True)
If ($Violations.Count -ne 0) {
Foreach ($Violation in $Violations) {
$Result = New-Object `
-Typename "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord" `
-ArgumentList "$((Get-Help $MyInvocation.MyCommand.Name).Description.Text)",$Violation.Extent,$PSCmdlet.MyInvocation.InvocationName,Information,$Null
$Results += $Result
}
}
return $Results
#endregion
}
catch {
$PSCmdlet.ThrowTerminatingError($_)
}
}
}
Export-ModuleMember -Function Measure-*
Instead I get:
Line Extent Message
---- ------ -------
6 $Url Variable names should use a consistent capitalization style, i.e. : PascalCase.
7 $IEWIDTH = 550 Variable names should use a consistent capitalization style, i.e. : PascalCase.
8 $IeHeight = 450 Variable names should use a consistent capitalization style, i.e. : PascalCase.
6 $Url Variable names should use a consistent capitalization style, i.e. : PascalCase.
7 $IEWIDTH = 550 Variable names should use a consistent capitalization style, i.e. : PascalCase.
8 $IeHeight = 450 Variable names should use a consistent capitalization style, i.e. : PascalCase.
Any ideas on what I'm doing wrong? I found another site here, if I use that method to test individual variables at a time, I get the results I'd expect to see.
To do that, add the following line at the end of the function IE,
[System.Management.Automation.Language.Parser]::ParseInput($MyInvocation.MyCommand.ScriptContents, [ref]$null, [ref]$null)
Then this code below will give you length numbers.
$stuffast = .\FunctionIe.ps1
$left = $Stuffast.FindAll({$args[0] -is [System.Management.Automation.Language.AssignmentStatementAst]},$true)
$left[0].Left.Extent.Text.Length
$left[0].Left.VariablePath.UserPath.Length

Unlike an AssignmentStatementAst, which has a Left and Right(-hand) value property, the ParameterAst has a Name and DefaultValue property, so you'll want to use Name in your predicate block:
$predicate = {
# ...
if ( $Ast -is [System.Management.Automation.Language.ParameterAst] ) {
[System.Management.Automation.Language.ParameterAst]$VariableAst = $Ast
if ( $VariableAst.Name.VariablePath.UserPath -eq 'i' ) {
$ReturnValue = $false
}
elseif ( $VariableAst.Name.VariablePath.UserPath.Length -eq 3 ) {
$ReturnValue = $false
}
elseif ($VariableAst.Name.VariablePath.UserPath -cnotmatch '^([A-Z][a-z]+)+$') {
$ReturnValue = $True
}
}
# ...
}
Alternatively, flip your predicate-logic around to search for VariableExpressionAst's where the parent node is one of AssignmentStatementAst or ParameterAst:
$predicate = {
param($ast)
$ValidParents = #(
[System.Management.Automation.Language.ParameterAst]
[System.Management.Automation.Language.AssignmentStatementAst]
)
if($ast -is [VariableExpressionAst] -and $ValidParents.Where({$ast -is $_}, 'First')){
[System.Management.Automation.Language.VariableExpressionAst]$variableAst = $ast
# inspect $variableAst.VariablePath here
}
return $false
}

Related

Unable to evaluate against a specific value

I am having trouble thinking of the correct logic on how to go about this. I have a function that accepts two types of switches:
-Add
-Remove
Function Test-Bool {
Param (
# Input Parameters
[Parameter(Mandatory = $false,
HelpMessage='Enter. Workflow. Name.')]
[Alias('OMB','MailBox')]
[string]$Workflow,
[Parameter(Mandatory = $false)]
[Alias('EDIPI','DisplayName')]
[string[]]$UserName
)
DynamicParam {
if ($Workflow -ne $null -and $UserName -ne $null) {
$parameterAttribute = [System.Management.Automation.ParameterAttribute]#{
ParameterSetName = "AddingMembers"
Mandatory = $false
}
$attributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
$attributeCollection.Add($parameterAttribute)
$dynParam1 = [System.Management.Automation.RuntimeDefinedParameter]::new(
'Add', [switch], $attributeCollection
)
$paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
$paramDictionary.Add('Add', $dynParam1)
$parameterAttribute1 = [System.Management.Automation.ParameterAttribute]#{
ParameterSetName = "RemovingMembers"
Mandatory = $false
}
$attributeCollection1 = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
$attributeCollection1.Add($parameterAttribute1)
$dynParam11 = [System.Management.Automation.RuntimeDefinedParameter]::new(
'Remove', [switch], $attributeCollection1
)
$paramDictionary.Add('Remove', $dynParam11)
return $paramDictionary
}
}
Begin {
$ABool = {
'Add Block'
$Bool = Read-Host -Prompt 'Adding or Removing Member(s)'
if ($Bool.ToLower() -like 'a*') {
$true
}
else {
Break;
}
}
$RBool = {
'Remove Block'
$Bool = Read-Host -Prompt 'Adding or Removing Member(s)'
if ($Bool.ToLower() -like 'r*') {
$true
}
else {
Break;
}
}
if ($PSBoundParameters['Add'].IsPresent) { [bool]$Add = $true }
elseif ($PSBoundParameters['Remove'].IsPresent) { [bool]$Remove = $true }
elseif (-not$PSBoundParameters['Add'].IsPresent) { $Add = & $ABool }
elseif (-not$PSBoundParameters['Remove'].IsPresent) { $Remove = & $RBool }
}
Process {
if ($Add) {
"Add was selected"
}
if ($Remove) {
"Remove was selected"
}
}
}
I can run it several ways:
Test-Bool -Workflow spg -UserName a -Add - works
Test-Bool -Workflow spg -UserName a -Remove - works
Test-Bool -Workflow spg
Should ask to remove or add: Typing Add, works.
Typing Remove, does not work.
Which makes sense, because elseif (-not$PSBoundParameters['Add'].IsPresent) { $Add = & $ABool } is evaluated first and when asked, Remove is typed in which throws it into the else block making it Break/Exit.
QUESTION:
What can I change to make $Remove be evaluated to true and vise-versa when Add is typed (to make $Add True)?
I honestly confused myself so much that I just decided to post here instead of attempting to figure this out on my own.
Use Parameter Sets instead:
Function Test-Bool {
[CmdletBinding(DefaultParameterSetName = 'Undecided')]
Param (
# Input Parameters
[Parameter(Mandatory = $false,
HelpMessage='Enter. Workflow. Name.')]
[Alias('OMB','MailBox')]
[string]$Workflow,
[Parameter(Mandatory = $false)]
[Alias('EDIPI','DisplayName')]
[string[]]$UserName,
[Parameter(Mandatory = $true, ParameterSetName = 'Add')]
[switch]$Add,
[Parameter(Mandatory = $true, ParameterSetName = 'Remove')]
[switch]$Remove
)
begin {
$action = if($PSCmdlet.ParameterSetName -eq 'Undecided'){
$answer = Read-Host -Prompt 'Adding or Removing Member(s)?'
if($answer -like 'a*'){
'Add'
}
elseif($answer -like 'r*') {
'Remove'
}
else {
throw 'Invalid option provided'
}
}
else {
$PSCmdlet.ParameterSetName
}
# $action now contains either 'Add' or 'Remove'
$actionBlock = #{
'Add' = { <# code to add user to workflow #> }
'Remove' = { <# code to remove user from workflow #> }
}[$action]
}
process {
# if/else statements no longer needed, $actionBlock contains the correct scriptblock
. $actionBlock
}
}
If a user doesn't specify either switch, the parameter set name will be Undecided, and the user will be prompted in the begin block - otherwise we simply use the parameter set name associated with the specified switch.
Since the two switch parameters belong to separate parameter sets, the user can no longer pick both.

A more compact / elegant way to navigate complex / nested hashtables with Invoke-Expression? [duplicate]

This question already has answers here:
Access PSObject property indirectly with variable
(3 answers)
Editing/Accessing Powershell objects using variables with dot notation
(3 answers)
Closed 2 years ago.
I'm working on a function to acess/modify nested hashtables via string input of keys hierarchy like so:
putH() $hashtable "key.key.key...etc." "new value"
Given:
$c = #{
k1 = #{
k1_1 = #{
k1_1_1 = #{ key = "QQQQQ"}
}
}
}
so far i've come up with this function for modifying values:
function putH ($h,$hKEYs,$nVAL){
if ($hKEYs.count -eq 1) {
$bID = $hKEYs #match the last remaining obj in $hkeys
}
else {
$bID = $hKEYs[0] #match the first obj in $hekys
}
foreach ($tk in $h.keys){
if ($tk -eq $bID){
if ($hKEYs.count -eq 1){ #reached the last obj in $hkeys so modify
$h.$tk = $nVAL
break
}
else {
$trash,$hKEYs = $hKEYs #take out the first obj in $hkeys
$h.$tk = putH $h.$tk $hKEYs $nVAL #call the function again for the nested hashtale
break
}
}
}
return $h
}
and this function for getting values :
function getH ($h,$hKEYs){
if ($hKEYs.count -eq 1) {
$bID = $hKEYs
}
else {
$bID = $hKEYs[0]
}
foreach ($tk in $h.keys){
if ($tk -eq $bID){
if ($hKEYs.count -eq 1){
$h = $h.$tk
break
}
else {
$trash,$hKEYs = $hKEYs
$h = getH $h.$tk $hKEYs
break
}
}
}
return $h
}
that i use like so:
$s = "k1.k_1.k1_1_1" #custom future input
$s = $s.split(".")
putH $c ($s) "NEW_QQQQQ"
$getval = getH $c ($s)
My question:
is there a more elegant way to achieve the function's results...say with invoke-expression?
i've tried invoke-expression - but can't access the hassstables trough it (no matter the combinations, nested quotes)
$s = "k1.k_1.k1_1_1" #custom future input
iex "$c.$s"
returns
System.Collections.Hashtable.k1.k_1.k1_1_1
Don't use Invoke-Expression
I'll answer your question at the bottom, but I feel obliged to point out that calling Invoke-Expression here is both dangerous and, more importantly, unnecessary.
You can resolve the whole chain of nested member references by simply splitting the "path" into its individual parts ('A.B.C' -> #('A', 'B', 'C')) and then dereferencing them one-by-one (you don't even need recursion for this!):
function Resolve-MemberChain
{
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[psobject[]]$InputObject,
[Parameter(Mandatory = $true, Position = 0)]
[string[]]$MemberPath,
[Parameter(Mandatory = $false)]
[ValidateNotNullOrEmpty()]
[string]$Delimiter = '.'
)
begin {
$MemberPath = $MemberPath.Split([string[]]#($Delimiter))
}
process {
foreach($o in $InputObject){
foreach($m in $MemberPath){
$o = $o.$m
}
$o
}
}
}
Now you can solve your problem without iex:
$ht = #{
A = #{
B = #{
C = "Here's the value!"
}
}
}
$ht |Resolve-MemberChain 'A.B.C' -Delimiter '.'
You can use the same approach to update nested member values - simply stop at the last step and then assign to $parent.$lastMember:
function Set-NestedMemberValue
{
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[psobject[]]$InputObject,
[Parameter(Mandatory = $true, Position = 0)]
[string[]]$MemberPath,
[Parameter(Mandatory = $true, position = 1)]
$Value,
[Parameter(Mandatory = $false)]
[ValidateNotNullOrEmpty()]
[string]$Delimiter = '.'
)
begin {
$MemberPath = $MemberPath.Split([string[]]#($Delimiter))
$leaf = $MemberPath |Select -Last 1
$MemberPath = $MemberPath |select -SkipLast 1
}
process {
foreach($o in $InputObject){
foreach($m in $MemberPath){
$o = $o.$m
}
$o.$leaf = $Value
}
}
}
And in action:
PS ~> $ht.A.B.C
Here's the value!
PS ~> $ht |Set-NestedMemberValue 'A.B.C' 'New Value!'
PS ~> $ht.A.B.C
New Value!
Why isn't your current approach working?
The problem you're facing with your current implementation is that the $c in $c.$s gets expanded as soon as the string literal "$c.$s" is evaluated - to avoid that, simply escape the first $:
iex "`$c.$s"

Convert default parameter value of string to type array

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}}

Powershell only returns 2 values, using "return x,y"

I have a weird problem. I have a function that is supposed to return 1 or 2 values, a letter and a number.
For some reason, it only works when I specify the return as
return $x, $y
but it doesn't work like this:
return $x
return $y
The code:
$ModelsDesktop = #("Dimension","Optiplex")
$ModelsLaptop = #("Latitude","Venue")
<#
Returns L or D depending on the Computer Name. Sets U if the model is uncertain.
#>
Function Get-TypeByComputerName{
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
[ValidateNotNullOrEmpty()]
[string]$ComputerName
)
Process {
if ($ComputerName -like "*-L-*" -or $ComputerName -like "*-LT-*") {
$ModelType = "L"
}
elseif ($ComputerName -like "*-D-*" -or $ComputerName -like "*-WRK-*") {
$ModelType = "D"
}
else {
$ModelType = "U" #unsure
}
return $ModelType
}
}
<#
Returns L or D depending on the Computer Model. Sets U if the model is uncertain.
#>
Function Get-TypeByModel{
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
[ValidateNotNullOrEmpty()]
[string]$Model
)
Process {
if (($ModelsLaptop | %{($Model) -like("*$_*")}) -contains $true) {
$ModelType = "L"
}
elseif (($ModelsDesktop | %{($Model) -like("*$_*")}) -contains $true) {
$ModelType = "D"
}
else {
$ModelType = "U"
}
return $ModelType
}
}
<#
Returns L or D depending on the Computer Name and Model. Sets a flag if the model is uncertain.
#>
Function Get-Type{
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$ComputerName,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$Model
)
Process {
if ((($ComputerName | Get-TypeByComputerName) -eq ($Model | Get-TypeByModel)) -and (($ComputerName | Get-TypeByComputerName) -ne "U")) {
$ModelType = ($ComputerName | Get-TypeByComputerName)
}
elseif (($ComputerName | Get-TypeByComputerName) -ne "U") {
$ModelType = ($ComputerName | Get-TypeByComputerName)
$Flag = 1
}
elseif (($Model | Get-TypeByModel) -ne "U") {
$ModelType = ($Model | Get-TypeByModel)
$Flag = 1
}
else {
$ModelType = "D"
$Flag = 1
}
return $ModelType
return $Flag
}
}
The value:
$test = New-Object psobject -Property #{ComputerName="crd-l-02-00001";Model="opti 343"}
Output with 2 return statements (as in the previous code):
$test
ComputerName Model
------------ -----
crd-l-02-00001 opti 343
PS C:\Users\u0096902> (Get-Type -ComputerName $test.ComputerName -Model $test.Model)
L
Output with the corrected "return $ModelType, $Flag":
$test
ComputerName Model
------------ -----
crd-l-02-00001 opti 343
PS C:\Users\u0096902> (Get-Type -ComputerName $test.ComputerName -Model $test.Model)
L
1
What am I missing? Can't seem to figure it out. It only seems to return the first "return", but I don't know why.
This example code seems to work perfectly fine:
function get-multiplereturnvalues {
"Return value 1"
"Return value 2"
}
$return = get-multiplereturnvalues
$return[0] # Outputs "Return value 1"
$return[1] # Outputs "Return value 2"
"In computer programming, a return statement causes execution to leave the current subroutine and resume at the point in the code immediately after where the subroutine was called, known as its return address."
Once you call return you are saying that you are finished with that function.
From Get-Help about_Return:
LONG DESCRIPTION
The Return keyword exits a function, script, or script block. It can be
used to exit a scope at a specific point, to return a value, or to indicate
that the end of the scope has been reached.
Your first Return is forcing immediate exit from the function, so the second one never gets a chance to run.

Powershell: Property stored in a variable

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'