Casting Object to String Array Powershell - powershell

I want to create an array of strings instead of a variable object so that I can use the "contains" keyword on each index of the array.
$myArray = Get-ADDomain
The above creates an object, which is not what I want. I also tried
[string[]] $myArray = Get-ADDomain
But after that, $myArray only contains one string and it is the first non-empty property of Get-ADDomain, in my case "ComputersContainer". What should I do to receive an array of strings where each string is a different property, such as
$myArray[0] = "AllowedDNSSuffixes = {}"

PowerShell will always return objects by design of course, and specifying that [string[]], does not really change that.
For what you are trying to use, you have to force the array creation. The below is just one way, but I am sure others will have more elegant ways of doing this as well. Though I am curious why one would want to do this, this way. But, hey, that's just me.
# Create an empty array
$DomainData = #()
# Get all the data points for the utilized cmdlet, split on a common delimiter for the array
[string[]]$DomainData = (Get-ADDomain | Select *) -split ';'
# Display the array count
$DomainData.Count
34
# validate getting a value from the array by using an index number
$Item = $DomainData[17]
NetBIOSName=CONTOSO
[array]::IndexOf($DomainData, $Item)
17
# Use that element number to validate the use of the contains comparison operator
0..($DomainData.Count - 1) | %{ If($DomainData[$_] -contains $item){"Index key is $_ contains a value of $Item"} }
Index key is 17 contains a value of NetBIOSName=CONTOSO
# Use the previous with a partial string for a comparison, -contains cannot be used, like or match has to be used
# From the documentation:
# -Contains
# Description: Containment operator. Tells whether a collection of reference values includes a single test value.
$Item = '*domain*'
0..($DomainData.Count - 1) | %{ If($DomainData[$_] -like $item){"Index key is $_ like a value of $Item"} }
Index key is 1 like a value of *domain*
Index key is 6 like a value of *domain*
Index key is 7 like a value of *domain*
Index key is 8 like a value of *domain*
Index key is 18 like a value of *domain*
Index key is 20 like a value of *domain*

You cannot cast a PSObject directly to a string array like that.
However, this can be accomplished rather easily.
To get an array of string from the object
$myArray = Get-ADDomain
# You can use a standard array #() but these tends to be slower for bigger amount of data
$outArray = New-Object -TypeName System.Collections.Generic.List[String]
#To add just the value
$myArray.psobject.properties | Foreach { $outArray.Add($_.Value) }
# To add Name = {Value} instead
$myArray.psobject.properties | Foreach { $outArray.Add("$($_.Name) = {$($_.Value)}") }
Using an hasthable instead:
$myArray = Get-ADDomain
$hashtable = #{}
$myArray.psobject.properties | Foreach { $hashtable[$_.Name] = $_.Value }
# If you need to do something with the key
Foreach ($key in $hashtable.Keys) {
$Value = $hashtable[$key]
if ($value -like '*prod*') {
Write-Host $key
}
}

Related

Replace PSObj property value based on a list

I've the following PSObj with some properties stored in an $array :
ComputerName : MyComputer
Time : 08/11/2022 13:57:53
DetectionFile : MyBadFile.exe
ThreatName : WS.Reputation.1
Action : 12
I'm trying to replace the action ID number by it's corresponding description. I've a hashtable with the possibles reasons behind the Action ID
$ActionId = #{
0 = 'Unknown'
1 = 'Blocked'
2 = 'Allowed'
3 = 'No Action'
4 = 'Logged'
5 = 'Command Script Run'
6 = 'Corrected'
7 = 'Partially Corrected'
8 = 'Uncorrected'
10 = 'Delayed Requires reboot to finish the operation.'
11 = 'Deleted'
12 = 'Quarantined'
13 = 'Restored'
14 = 'Detected'
15 = 'Exonerated No longer suspicious (re-scored).'
16 = 'Tagged Marked with extended attributes.'
}
I'm trying to parse each item of this array, and each value of the reason ID to replace the ID by the reason string
# parse array
foreach ($Item in $array) {
# parse possible values
foreach ($value in $ActionId) {
if ($value -eq $item.Action) {
$Item.Action = $ActionId[$value]
$Item.Action
}
}
From my understanding, I'm missing the correct syntax here
$Item.Action = $ActionId[$value]
I do not get any errors, but from the debugger, I'm replacing the action property by $null with the above...
The immediate fix is to loop over the keys (.Keys) of your $ActionId hashtable:
foreach ($Item in $array) {
# parse possible values
foreach ($value in $ActionId.Keys) {
if ($value -eq $item.Action) {
$Item.Action = $ActionId[$value]
$Item.Action # diagnostic output
}
}
}
Note:
To avoid confusion, consider renaming $value to $key.
Generally, note that hashtables are not enumerated in the pipeline / in looping constructs in PowerShell.
That is, foreach ($value in $ActionId) ... doesn't actually loop over the hashtable's entries, and is the same as $value = $ActionID)
If you want to enumerate a hashtable's entries - as key-value pairs of type System.RuntimeType - you would need to use the .GetEnumerator() method; in your case, however, enumerating the keys is sufficient.
However, the simpler and more efficient solution is to test whether the $Item.Action value exists as a key in your hashtable, using the latter's .Contains() method:[1]
foreach ($Item in $array) {
if ($ActionId.Contains($Item.Action)) {
$Item.Action = $ActionId[$Item.Action]
$Item.Action # diagnostic output
}
}
You can further streamline this as follows, though it is conceptually a bit obscure:
foreach ($Item in $array) {
if ($null -ne ($value = $ActionId[$Item.Action])) {
$Item.Action = $value
$Item.Action # diagnostic output
}
}
= is only ever PowerShell's assignment operator; for equality / non-equality comparison, -eq / -ne is required.
Here, an assignment to $value is indeed being performed and the assigned value then acts as the RHS of the -ne operation; in other words: you can use assignment as expressions in PowerShell.
If hashtable $ActionId has no key with value $Item.Action, $ActionId[$Item.Action] quietly returns $null.
Finally - in PowerShell (Core) 7+ only - an even more concise (though not necessarily faster) solution is possible, using ??, the null-coalescing operator:
foreach ($Item in $array) {
$Item.Action = $ActionId[$Item.Action] ?? $Item.Action
$Item.Action # diagnostic output
}
That is, the value of $ActionId[$Item.Action] is only used if it isn't $null; otherwise, $Item.Action, i.e. the current value, is used (which is effectively a no-op).
[1] .ContainsKey() works too, and while this name is conceptually clearer than .Contains(), it is unfortunately not supported by PowerShell's [ordered] hashtables (System.Collections.Specialized.OrderedDictionary) and, generally speaking, not supported by other dictionary (hashtable-like types), given that the System.Collections.IDictionary interface only has .Contains()
In addition mklement0's helpful answer, I was just thinking outside the box (aka question):
This is typical situation where I would consider to use an enum except for the fact that that the keys do not (easily) accept spaces (as in your question).
Enum ActionTypes {
Unknown
Blocked
Allowed
NoAction
Logged
CommandScriptRun
Corrected
PartiallyCorrected
Uncorrected
Delayed
Deleted
Quarantined
Restored
Detected
Exonerated
Tagged
}
$PSObj = [PSCustomObject]#{
ComputerName = 'MyComputer'
Time = [DateTime]'08/11/2022 13:57:53'
DetectionFile = 'MyBadFile.exe'
ThreatName = 'WS.Reputation.1'
Action = 12
}
$PSObj.Action = [ActionTypes]$PSObj.Action
$PSObj
ComputerName : MyComputer
Time : 8/11/2022 1:57:53 PM
DetectionFile : MyBadFile.exe
ThreatName : WS.Reputation.1
Action : Restored
The advantage is that you won't lose the actual action id, meaning if you e.g. insert the object back into a database, it will automatically type cast to the original integer type:
$PSObj.Action
Restored
[int]$PSObj.Action
12

Append dictionary to a dictionary in powershell (hashtable)

I have two dictionaries like this:
$first = #{}
$first.Add('John', 'Doe')
$first.Add('Johnny', 'Doe')
$second = #{}
$second.Add('Jack', 'Test')
$second.Add('Jacky', 'Test')
And I have a general $all = #{} dictionary, that stands for all dictionaries combined.
Ex. when I want to see all keys that $all contains:
foreach($key in $all){
Write-Host $key
}
It will show this:
John
Johnny
Jack
Jacky
p.s. I have this one:
$all = #{}
$all_dict = #{}
$all_dict += $first
$all_dict += $second
foreach($dict in $all_dict){
foreach($key in $dict.Key){
$all.Add($key, $dict[$key])
}
}
But I was wondering if there is another way to do it without the need to add all dictionaries to an array and then iterate through them
I wouldn't do the += addition to hashtables, but instead use a ForEach-Object on the hashes .Keys. That way, the code can be shortened, but also it will leave you an easier choice whether you would want the possible duplicates from Hashtable 1 ($first) to be overwritten by the values from the second Hashtable ($second).
Something like this:
$first = #{}
$first.Add('John', 'Doe')
$first.Add('Johnny', 'Doe')
$second = #{}
$second.Add('Jack', 'Test')
$second.Add('Jacky', 'Test')
$second.Add('Johnny', 'Depp') # Duplicate key: same first name, different lastname
$all = #{}
# copy all keys and values from the $first Hashtable into $all
$first.Keys | ForEach-Object { $all[$_] = $first[$_] }
For the next part, you'll have to decide what to do with duplicate keys:
Method 1
# add the stuff from Hashtable $second to it:
# this will overwrite the value if the key already exists (i.e. $second value 'wins')
$second.Keys | ForEach-Object { $all[$_] = $second[$_] }
OR use Method 2
# make sure the value of the $first hashtable is NOT overwritten (i.e. $first value 'wins')
$second.Keys | ForEach-Object { if (!($all.ContainsKey($_))) { $all[$_] = $second[$_] }}
In case you choose to overwrite (method 1), the $all hash will contain
Name Value
---- -----
John Doe
Jacky Test
Johnny Depp
Jack Test
If you choose NOT to overwrite (method 2), $all will be
Name Value
---- -----
John Doe
Jacky Test
Johnny Doe
Jack Test
Edit
There is another approach where you rely on the fact that an exception is thrown if you try to add an entry that already exists. In that case, use the .Add(key, value) method and wrap it inside a try{..} catch{..} block.
Without that catch, the error prevents the $all Hashtable to be filled, as it stops at the first duplicate key you try to add.
$second.Keys | ForEach-Object {
try {
$all.Add($_, $second[$_])
}
catch {
# catch the exception in order to carry on adding items
# the effect will be that the values from $first will not be overwritten
# just like with method 2
Write-Warning $_.Exception.Message
}
}
I think your $all_dict already contains what you want (i.e. a hashtable with all 4 entries), but your foreach( $dict in $all_dict ) isn't enumerating the hashtable entries like you expect it to.
The quick answer is to iterate over the Keys collection instead:
foreach( $key in $all.Keys )
{
write-host $key
}
The longer answer is that in your example PowerShell is doing some "helper" things for you with enumeration - foreach($key in $all) is only enumerating over a single object ($all), but write-host $all is evaluating an array of all of the entries in $all and serializing them into a single string:
Compare the behaviour of these two lines and you can see the difference:
PS> foreach($item in #{ "aaa"="bbb"; "ccc"="ddd" }) { write-host $item }
System.Collections.DictionaryEntry System.Collections.DictionaryEntry
PS> foreach($item in #{ "aaa"="bbb"; "ccc"="ddd" }.Keys) { write-host $item }
ccc
aaa
By the way, watch out for if your keys collide - if you try #{ "aaa"="bbb"; "ccc"="ddd" } + #{ "aaa"="eee" } for example, you'll get an error Item has already been added. Key in dictionary: 'aaa' Key being added: 'aaa'. so you might want to find a better way to merge your hashtables rather than just using +.
Am I not understanding the question? You can add them.
$first = #{John = 'Doe'; Johnny = 'Doe'} # hashtables
$second = #{Jacky = 'Test'; Jack = 'Test'}
$all = $first + $second # merge two hashtables
foreach ($i in $all.getenumerator()) { $i } # loops 4 times

Unable to remove item from hash table

In Powershell, I have a hash table that contains data similar to this -
Name Value
---- -----
1-update.bat 1
2-update.bat 2
3-update.bat 3
3.1-update.bat 3.1
4-update.bat 4
I also have an variable that contians a number, for example 3
What I would like to do is loop through the array and remove any entry where the value is less than or equal to 3
I'm thinking that this will be easy, especially as the docs say that has tables contain a .remove method. However, the code I have below fails, yeilding this error -
Exception calling "Remove" with "1" argument(s): "Collection was of a
fixed size."
Here is the code that I used -
$versions = #{}
$updateFiles | ForEach-Object {
$versions.Add($_.name, [decimal]($_.name -split '-')[0])
}
[decimal]$lastUpdate = Get-Content $directory\$updatesFile
$versions | ForEach-Object {
if ( $_.Value -le $lastUpdate ) {
$versions.Remove($version.Name)
}
}
I first tried to loop $versions in a different manner, trying both the foreach and for approaches, but both failed in the same manner.
I also tried to create a temporary array to hold the name of the versions to remove, and then looping that array to remove them, but that also failed.
Next I hit Google, and while I can find several similar questions, none that answer my specific question. Mostly they suggest using a list (New-Object System.Collections.Generic.List[System.Object]), whcih from what I can tell is of no help to me here.
Is anyone able to suggest a fix?
Here you go, you can use .Remove(), you just need a clone of the hashtable so that it will let you remove items as you enumerate.
[hashtable]$ht = #{ '1-update.bat'=1;'2-update.bat'=2;'3-update.bat'=3;'3.1-update.bat'=3.1; '4-update.bat'=4 }
#Clone so that we can remove items as we're enumerating
$ht2 = $ht.Clone()
foreach($k in $ht.GetEnumerator()){
if([decimal]$k.Value -le 3){
#notice, deleting from clone, then return clone at the end
$ht2.Remove($k.Key)
}
}
$ht2
Notice I've cast the original variable too so that it's explicitly a hash table, may not be required, but I like to do it to at least keep things clear.
It looks like you just confused ForEach-Object with foreach but only halfway (maybe it was foreach before and you converted it).
You can't send a [hashtable] directly to ForEach-Object; the $_ in that case will just refer to the single [hashtable] you sent in. You can do:
foreach ($version in $versions.GetEnumerator()) {
$version.Name
$version.Value
}
or you can do something like this:
$versions.Keys | ForEach-Object {
$_ # the name/key
$versions[$_] # the value
$versions.$_ # also the value
}
$ht.Keys # list all keys
$ht[$_] # take an element of hastable
$ht.Remove($_) # remove an element of hastable by his key
what you want:
$ht.Keys | ? { $ht[$_] -le 3 } | %{$ht.Remove($_) }
You need to create a temporary array to hold the name/key of the versions to remove, and then looping that array to remove them from hash table:
$versionKeysToRemove = $versions.Keys | Where-Object { $versions[$_] -le $lastUpdate }
$versionKeysToRemove | ForEach-Object { $versions.Remove($_) }
Or shorter:
($versions.Keys | ? { $versions[$_] -le $lastUpdate }) | % { $versions.Remove($_) }
Please note the parentheses.

How do I retrieve a HashTable key from a value?

I have a PowerShell HashTable that contains a set of key-value pairs (naturally). All of the HashTable values are unique.
I would like to retrieve a HashTable key, based on a value that I specify, using PowerShell.
Another options:
to iterate over the HashTable keys and find a key that contains the
value:
$HashTable.Keys |? { $HashTable[$_] -eq $Val }
to iterate using GetEnumerator() function:
$HashTable.GetEnumerator() | ?{ $_.Value -eq $Val } | %{ $_.Key }
You can use PowerShell 4.0's Where method syntax to achieve this. The Where method accepts a PowerShell ScriptBlock to find objects matching the specified criteria. We can iterate over the HashTable keys and find a key that contains the desired value.
In case you do have a scenario where you have duplicate HashTable values, you can optionally specify a second parameter, of type WhereOperatorSelectionMode, that specifies which objects should be returned by the call to the Where method. By specifying First for the second method parameter, we can ensure that only a single HashTable key is ever returned.
All of the supported values for the second parameter are as follows:
Default
First
Last
SkipUntil
Until
Split
$HashTable = #{
1 = 10;
2 = 20;
3 = 30;
}
$Val = 30;
$HashTable.Keys.Where({ $HashTable[$PSItem] -eq $Val; }, [System.Management.Automation.WhereOperatorSelectionMode]::First);

What's the proper way to iteratively grab a value from an array when the key is a string?

I'm trying to export some lists from Sharepoint into a CSV file. My goal is to make a single function that is flexible enough to take a List name, identifier for the CSV file, and a list of FieldValues to export, and then produce a CSV file. Here's what I have so far:
function getTableData($_ctx, [string]$_colName)
{
$list = $_ctx.Web.Lists.GetByTitle("$_colName")
$camlQuery = [Microsoft.SharePoint.Client.CamlQuery]::CreateAllItemsQuery(100)
$colItems = $list.GetItems($camlQuery)
$_ctx.Load($colItems)
$_ctx.ExecuteQuery();
return $colItems
}
# More will go into this array, but for now a single entry is sufficient for testing purposes
$mstLists = #("GroupMst", "Groups", #("Title", "GroupCode"))
$cols = #()
foreach($col in $mstLists[0][2])
{
$cols += #{Name=$col;expression={$_[$col];}}
}
$cols
# Grab all items from a list
getListData $ctx $mstLists[0][0] |
%{ select-object -input $_ -prop $cols } |
Export-Csv -Path ($export_path + '\' + $current_date + '_' + $mstLists[0][1] + '.csv') -Encoding UTF8 -NoTypeInformation
The problem I'm having is in the loop that populates $cols. Basically, each item needs to look like #{Name="Title";expression={$_["Title"];}} in order for select-object in the ForEach to grab the proper fields from the List. Unfortunately $cols ends up being looking like this:
Name Value
---- -----
expression $_[$col];
Name Title
expression $_[$col];
Name GroupCode
Which (somehow) produces a CSV file that looks like this:
"Title","GroupCode"
"LA","LA"
"NY","NY"
"TK","TK"
When the output needs to look like this:
"Title","GroupCode"
"Los Angeles","LA"
"New York","NY"
"Tokyo","TK"
I know the field names are correct - if I hardcode them in like so...
# Grab all items from a list
getListData $ctx $mstLists[0][0] |
%{ select-object -input $_ -prop `
#{Name="Title";expression={$_["Title"];}}, `
#{Name='GroupCode';expression={$_["GroupCode"];}}; } |
Export-Csv -Path ($export_path + '\' + $current_date + '_' + $mstLists[0][1] + '.csv') -Encoding UTF8 -NoTypeInformation
...then I get the desired CSV output. I just can't figure out how to get $_[$col] to instead return $_["Title"]
Not sure what the $mstLists[0][2] meant to refer to, but the following code seems to give what you are after...
$mstLists = #("GroupMst", "Groups", #("Title", "GroupCode"))
$cols = #()
foreach($col in $mstLists[2])
{
$cols += #{Name=$col; Expression = [scriptblock]::Create('$_["{0}"]' -f $col)}
}
$cols
which gives...
Name Value
---- -----
Name Title
Expression $_["Title"]
Name GroupCode
Expression $_["GroupCode"]
In your response to andyb in the comments, you say that each item of the array will follow the format
#("ListName", "CSVFileID", #("Field1", "Field2", "Etc..."))
and that $mstLists[0][2] "refers to the list of fields in the first item in the array."
The problem is that it doesn't refer to the list of fields in the first item of the array, because the first item of the array isn't a list of anything, it's the string GroupMst. When you index into a string, you get the character indicated by the index. Since $mstLists[0] is a string, $mstLists[0][2] returns the third character of that string, which is o.
I suppose you were expecting that the # operator would make the array in the parentheses a single item, which becomes the first element of $mstLists? It doesn't. All the # does is ensure that the expression in the parentheses is evaluated as an array rather than a scalar. So, with $a = ('string'), $a is a string, whereas with $a = #('string'), $a is an array with a single string element.
However, since ("GroupMst", "Groups", #("Title", "GroupCode")) evaluates to an array anyway, putting an # in front of it is redundant. Either way you're still assigning a literal array to the variable. $mstLists is an array of three elements:
Element 0 is the string GroupMst
Element 1 is the string Groups
Element 2 is an array of the strings Title and GroupCode
What you want to do is use , as a unary operator:
$mstLists = , ("GroupMst", "Groups", #("Title", "GroupCode"))
Now $mstLists is an array of a single item whose value is the array described in the bulleted list above, and $mstLists[0][2] evaluates to an array of the strings Title and GroupCode, as you were expecting.
Note that , works as a unary or binary operator that returns an array of the operands. To return a single-element array, you use it as a unary operator in front of that element. If you have multiple literal arrays that you want to assign to mstLists, you only need commas between them, not the one in front:
$mstLists = ("ListName", "CSVFileID", #("Field1", "Field2", "Etc...")), ("ListName2", "CSVFileID2", #("Field1", "Field2", "Etc..."))
That addresses the main problem. That still won't quite give you what you want, because $col won't get interpolated in the Expression scriptblock, so Expression will always be literally $_[$col]. However, in order to figure out how to do what you actually want to do, it would be helpful to see a sample of the contents of $ctx.