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

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.

Related

Powershell - How to Transpose an array

I have an array with data separated by a comma. I need to transpose it so the first part before the comma for each line is joined together by a delimiter as one line and the same for a second part. Example:
AC-2.22,CCI-000012
AC-5.1,CCI-000036
AC-5.3,CCI-001380
I want to have 2 separate variables like so:
variable 1 = AC-2.22; AC-5.1; AC-5.3
Variable 2 = CCI-000012; CCI-000036; CCI-001380
I know this should be simple but I've been staring at code all day and I just want to go eat dinner and goto sleep.
Thanks in advance
Based on the $array of the helpful answer from Santiago Squarzon, you might also use the ConvertFrom-Csv cmdlet to transpose the data using member-access enumeration:
$data = ConvertFrom-Csv $Array -Header var1, var2
$data.var1 -Join ';'
AC-2.22;AC-5.1;AC-5.3
$data.var2 -Join ';'
CCI-000012;CCI-000036;CCI-001380
This is not too hard using .Where method with Split mode:
$array = #(
'AC-2.22,CCI-000012'
'AC-5.1,CCI-000036'
'AC-5.3,CCI-001380'
)
$i = $false
$var1, $var2 = $array.Split(',').Where({ ($i = -not $i) }, 'Split')
$var1 -join '; '
$var2 -join '; '
.Split method works in this case thanks to Member-Access Enumeration, because arrays don't have an .Split method the same is called over each element of the array:
When you use the member-access operator on any object and the specified member exists on that object, the member is invoked. For property members, the operator returns the value of that property. For method members, the operator calls that method on the object.
When you use the member-access operator on a list collection object that doesn't have the specified member, PowerShell automatically enumerates the items in that collection and uses the member-access operator on each enumerated item.

Question regarding incrementing a string value in a text file using Powershell

Just beginning with Powershell. I have a text file that contains the string "CloseYear/2019" and looking for a way to increment the "2019" to "2020". Any advice would be appreciated. Thank you.
If the question is how to update text within a file, you can do the following, which will replace specified text with more specified text. The file (t.txt) is read with Get-Content, the targeted text is updated with the String class Replace method, and the file is rewritten using Set-Content.
(Get-Content t.txt).Replace('CloseYear/2019','CloseYear/2020') | Set-Content t.txt
Additional Considerations:
General incrementing would require a object type that supports incrementing. You can isolate the numeric data using -split, increment it, and create a new, joined string. This solution assumes working with 32-bit integers but can be updated to other numeric types.
$str = 'CloseYear/2019'
-join ($str -split "(\d+)" | Foreach-Object {
if ($_ -as [int]) {
[int]$_ + 1
}
else {
$_
}
})
Putting it all together, the following would result in incrementing all complete numbers (123 as opposed to 1 and 2 and 3 individually) in a text file. Again, this can be tailored to target more specific numbers.
$contents = Get-Content t.txt -Raw # Raw to prevent an array output
-join ($contents -split "(\d+)" | Foreach-Object {
if ($_ -as [int]) {
[int]$_ + 1
}
else {
$_
}
}) | Set-Content t.txt
Explanation:
-split uses regex matching to split on the matched result resulting in an array. By default, -split removes the matched text. Creating a capture group using (), ensures the matched text displays as is and is not removed. \d+ is a regex mechanism matching a digit (\d) one or more (+) successive times.
Using the -as operator, we can test that each item in the split array can be cast to [int]. If successful, the if statement will evaluate to true, the text will be cast to [int], and the integer will be incremented by 1. If the -as operator is not successful, the pipeline object will remain as a string and just be output.
The -join operator just joins the resulting array (from the Foreach-Object) into a single string.
AdminOfThings' answer is very detailed and the correct answer.
I wanted to provide another answer for options.
Depending on what your end goal is, you might need to convert the date to a datetime object for future use.
Example:
$yearString = 'CloseYear/2019'
#convert to datetime
[datetime]$dateConvert = [datetime]::new((($yearString -split "/")[-1]),1,1)
#add year
$yearAdded = $dateConvert.AddYears(1)
#if you want to display "CloseYear" with the new date and write-host
$out = "CloseYear/{0}" -f $yearAdded.Year
Write-Host $out
This approach would allow you to use $dateConvert and $yearAdded as a datetime allowing you to accurately manipulate dates and cultures, for example.

Exclude from file if line contains value from variable A OR B

I am writing to a file using streamwriter and I want to exclude any rows that match the values containing in two parameters. I have tried the below code but it does not output any values when I include the second condition ($file_stream -notmatch $exclude_permission_type).
$exclude_user_accounts = 'account1', 'account2', 'account3'
$exclude_permission_type = 'WRITE'
while ($file_stream = $report_input.ReadLine()) {
if ($file_stream -notmatch $exclude_user_accounts -and $file_stream -notmatch $exclude_permission_type) {
$_report_output.WriteLine($file_stream)
}
}
It's clearly not possible that your code has ever worked the way you intended, even with just the first condition, because a string can never match an array of strings . <string> -notmatch <array> will always evaluate to true even if the array contained an exact match. You cannot do partial matches like that at all.
Build one regular expression from all your filter strings:
$excludes = 'account1', 'account2', 'account3', 'WRITE'
$re = ($excludes | ForEach-Object {[regex]::Escape($_)}) -join '|'
then filter your strings using that regular expression:
if ($file_stream -notmatch $re) {
$_report_output.WriteLine($file_stream)
}

Casting Object to String Array 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
}
}

Powershell trim text using a special character

I have a list of hostnames which have the domain and subdomian in the FQDN. I need to get only the parent (root) domain name from it.
I have tried with substring but have not succeeded.
abc.xyz.me.com
def.me.com
ghi.sub.new.com
jkl.sup.old.com
these are some examples and from the list and I would like to get the root domains (me,new,old).
The simple way would be to use split and get the index of -2
$FQDN = ('abc.xyz.me.com', 'def.me.com', 'ghi.sub.new.com', 'jkl.sup.old.com')
ForEach ($Domain in $FQDN)
{
write-host ($Domain.split(".")[-2])
}
You probably want to do more than just write to host, but that should give you the idea.
Split the string, reverse the array and then pull the second member
$list = #(
"abc.xyz.me.com",
"def.me.com",
"ghi.sub.new.com",
"jkl.sup.old.com"
)
$List | ForEach-Object {
[Array]::Reverse(($Arr = $_.Split(".")))
$TLD, $SLD, $Null = $Arr
$TLD # You're top level domain
$SLD # The second level domain
# The rest of the values after those two get sent to null land
}
Here's a solution that gets a unique list of the full parent domains into a variable named $ParentDomains:
$Domains = 'abc.xyz.me.com', 'def.me.com', 'ghi.sub.new.com', 'jkl.sup.old.com'
$ParentDomains = $Domains | ForEach-Object {
$Domain = $_.Split('.')
$Domain[-2]+'.'+$Domain[-1]
} | Get-Unique
$ParentDomains
Explanation:
Iterates through the list of domains via ForEach-Object. Each domain in the loop is represented by $_.
Splits the string on the '.' character
Uses the index indicator [] to get the second to last [-2] and last [-1] items in each array and outputs them as a new string separated by a '.'.
Pipes the result to Get-Unique to remove duplicates.