Search string in text file and display until the next delimiter - powershell

I have got a file "servers.txt" :
[Server1]
Value_A
Value_B
Value_C
[Server2]
Value_A
[Server3]
Value_A
Value_B
Value_C
Value_D
===
I need to search into this file and display the server line + all his values.
Something like :
$search = "server3"
gc servers.txt | Select-String -Pattern $search and diplay until the next "["
(I can't tell for example, display the line+1, because the values are different, sometimes there are only 3, sometimes 1, etc.)
Thanks a lot!

How about:
$search = "server3"
(gc servers.txt -Delimiter '[') -like "$search]*" -replace '^','[' -replace '\s*\[$'
Cleaner solution (I think):
(gc servers.txt -raw) -split '\r\n(?=\[)' -like "?$search]*"

Looks like your delimiter is a blank line. How about reading the file and processing it so the first line is server name, all the following lines until a blank are an array of data, and then on blank lines it outputs a custom object with the server name and array of data as properties, and creating an array of those object?
Hm, that's confusing, and I wrote it. Let me post code, and then explain it.
$Server = ""
$Data = #()
$Collection = #()
Switch(GC C:\temp\test.txt){
{[String]::IsNullOrEmpty($Server) -and !([String]::IsNullOrWhiteSpace($_))}{$Server = $_;Continue}
{!([String]::IsNullOrEmpty($Server)) -and !([String]::IsNullOrEmpty($_))}{$Data+=$_;Continue}
{[String]::IsNullOrEmpty($_)}{$Collection+=[PSCustomObject]#{Server=$Server;Data=$Data};Remove-Variable Server; $Data=#()}
}
If(!([String]::IsNullOrEmpty($Server))){$Collection+=[PSCustomObject]#{Server=$Server;Data=$Data};Remove-Variable Server; $Data=#()}
Ok, it starts out by defining variables as either empty strings or arrays.
Then it processes each line, and performs one of three actions depending on the situation. The first line of the switch reads the text file, and processes it line by line. The first option in the Switch basically reads:
If there is nothing stored in the $Server variable, and the current line is not blank, then $Server = Current Line. Continue to the next line.
The second option is:
If $Server is not blank, and the current line is not blank, add this line to the array $Data. Continue to the next line.
The last option for the Switch is:
If the current line is blank, then this is the end of the current record. Create a custom object with two properties. The first property is named Server, and the value is whatever is in $Server. The second property is named Data, and the value is whatever is in $Data. Then remove the $server variable, and reset $Data to an empty array.
After the switch it checks to see if $Server still has data, and outputs one last object if it does. I do this in case there is no blank line at the end of the last record, just as cleanup.
What you are left with is $Collection being an array of objects that looks something like this:
Server Data
------ ----
[Server1] {Value_A , Value_B , Value_C}
[Server2] {Value_A}
[Server3] {Value_A , Value_B , Value_C , Value_D}

Related

powershell -match 2 different parts of one URL and get them

I want to get to different parts of a URL that I retrieve with PowerShell (https://repo.anaconda.com/archive/Anaconda3-2022.05-Windows-x86_64.exe) the -match returns true, the first match is a date that I can get correct and the second part is the name of the file (Anaconda3-2022.05-Windows-x86_64.exe) that I can not get it,
can someone help me with it, thanks in advance
here is my script
$Uri = 'https://www.anaconda.com/products/distribution#Downloads'
( $web = Invoke-WebRequest -Uri $uri)
( $downloadurl=$web.Links |Where-Object href -Like "*_64.exe" |Select-Object -First 1 | Select-Object -expand href )
( $downloadurl -match "(\d\d\d\d.\d{1,2}).*(Anaconda*.exe)" )
($latestversion = "$($Matches[1])")
($FileName = "$($Matches[2])")
There's an easier way to parse the filename than building a regex.
First, split on slash / to get an array of strings. Since the filename is the last one, use [-1] indexer to get it. Like so,
$downloadurl.split('/')[-1]
Anaconda3-2022.05-Windows-x86_64.exe
Now that you have a string with filenamme only, split on dash -:
$downloadurl.split('/')[-1].split('-')
Anaconda3
2022.05
Windows
x86_64.exe
Finding the version number is then simple indexing to 2nd element of the resulting array, and the filename is already availale from the previous split operation.

Get-GPOReport and Search For Matched Name Value

I'm trying to use the PowerShell command 'Get-GPOReport' to get GPO information in XML string format so I can search it for sub-Element values with unknown and different Element tag names (I don't think XML Object format will work for me, so I didn't perform a cast with "[xml]"), but I haven't been able to parse the XML output so that I can grab the line or two after a desired "Name" Element line that matches the text I'm searching for.
After, I have been trying to use 'Select-String' or 'Select-XML' with XPath (formatting is unclear and I don't know if I can use a format for various policy record locations) to match text and grab a value, but I haven't had any luck.
Also, if anyone know how to search for GPMC GUI names (i.e. "Enforce password history") instead of needing to first locate back-end equivalent names to search for (i.e. "PasswordHistorySize"), that would also be more helpful.
The following initial code is the part that works:
$String = "PasswordHistorySize" # This is an example string, as I will search for various strings eventually from a file, but I'm not sure if I could search for equivalent Group Policy GUI text "Enforce password history", if anyone knows how to do that.
$CurrentGPOReport = Get-GPOReport -Guid $GPO.Id -ReportType Xml -Domain $Domain -Server $NearestDC
If ($CurrentGPOReport -match $String)
{
Write-Host "Policy Found: ""$($String)""" -Foregroundcolor Green
#
#
# The following code is what I've tried to use to get value data, without any luck:
#
$ValueLine1 = $($CurrentGPOReport | Select-String -Pattern $String -Context 0,2)
$Value = $($Pattern = ">(.*?)</" ; [regex]::match($ValueLine1, $Pattern).Groups[1].Value)
}
I've been looking at this since yesterday and didn't understand why Select-String wasn't working, and I figured it out today... The report is stored as a multi-line string, rather than an array of strings. You could do a -match against it for the value, but Select-String doesn't like the multi-line formatting it seems. If you -split '[\r\n]+' on it you can get Select-String to find your string.
If you want to use RegEx to just snipe the setting value you can do it with a multi-line regex search like this:
$String = "PasswordHistorySize" # This is an example string, as I will search for various strings eventually from a file, but I'm not sure if I could search for equivalent Group Policy GUI text "Enforce password history", if anyone knows how to do that.
$CurrentGPOReport = Get-GPOReport -Guid $GPO.Id -ReportType Xml -Domain $Domain -Server $NearestDC
$RegEx = '(?s)' + [RegEx]::Escape($String) + '.+?Setting.*?>(.*?)<'
If($CurrentGPOReport -match $RegEx)
{
Write-Host "Policy Found: ""$String""" -Foregroundcolor Green
$Value = $Matches[1]
}
I'm not sure how to match the GPMC name, sorry about that, but this should get you closer to your goals.
Edit: To try and get every setting separated out into it's own chunk of text and not just work on that one policy I had to alter my RegEx a bit. This one's a little more messy with the output, but can be cleaned up simply enough I think. This will split a GPO into individual settings:
$Policies = $CurrentGPOReport -split '(\<(q\d+:.+?>).+?\<(?:\/\2))' | Where { $_ -match ':Name' }
That will get you a collection of things that look like this:
<q1:Account>
<q1:Name>PasswordHistorySize</q1:Name>
<q1:SettingNumber>21</q1:SettingNumber>
<q1:Type>Password</q1:Type>
</q1:Account>
From there you just have to filter for whatever setting you're looking for.
I have tried this with XPath, as you'll have more control navigating in the XML nodes:
[string]$SearchQuery = "user"
[xml]$Xml = Get-GPOReport -Name "Default Domain Policy" -ReportType xml
[array]$Nodes = Select-Xml -Xml $Xml -Namespace #{gpo="http://www.microsoft.com/GroupPolicy/Settings"} -XPath "//*"
$Nodes | Where-Object -FilterScript {$_.Node.'#text' -match $SearchQuery} | ForEach-Object -Process {
$_.Name #Name of the found node
$_.Node.'#text' #text in between the tags
$_.Node.ParentNode.ChildNodes.LocalName #other nodes on the same level
}
After testing we found that in the XML output of the Get-GPOReport cmdlet, the setting names does not always match that of the HTML output. For example: "Log on as a service" is found as "SeServiceLogonRight" in the XML output.

Remove blank lines after specific text (without using -notmatch)

We have a script that uses a function to go through a text file and replace certain words with either other words or with nothing. The spots that get replaced with nothing leave behind a blank line, which we need to remove in some cases (but not all). I've seen several places where people mention using things like -notmatch to copy over everything to a new file except what you want left behind, but there are a lot of blank lines we want left in place.
For example:
StrangerThings: A Netflix show
'blank line to be removed'
'blank line to be removed'
Cast: Some actors
Crew: hard-working people
'blank line left in place'
KeyGrip
'blank line to be removed'
Gaffer
'blank line left in place'
So that it comes out like this:
StrangerThings: A Netflix show
Cast: Some actors
Crew: hard-working people
KeyGrip
Gaffer
We've tried doing a -replace, but that doesn't get rid of the blank line. Additionally, we have to key off of the text to the left of the ":" in each line. The data to the right in most cases is dynamic, so we can't hard-code anything in for that.
function FormatData {
#FUNCTION FORMATS DATA BASED ON SECTIONS
#This is where we're replacing some words in the different sections
#Some of these we replace leave the blank lines behind
$data[$section[0]..$section[1]] -replace $oldword,$newword
$output | Set-Content $outputFile
}
$oldword = "oldword"
$newword = "newword"
FormatData
$oldword = "oldword1"
$newword = "" #leaves a blank line
FormatData
$oldword = "Some phrase: "
$newword = "" #leaves a blank line
FormatData
We just need a pointer in the right direction on how to delete/remove a blank line (or several lines) after specific text, please.
Since it looks like you are reading in an array and doing replacements, the array index will not go away. You can change the value to blank or white space, and it will still appear as a blank line when it is output to a file or console. Using the -replace operator with no replacement string, replaces the regex match with an empty string.
One approach could be to read the data in raw like Get-Content -Raw and then the text is read into memory as is, but you lose array indexing. At that point, you have full control over replacing newline characters if you choose to do so. A second approach would be to mark the blank lines you want to keep initially (<#####> in this example), do the replacements, remove the blank spaces, and then clean up the markings.
# Do this before any new word replacements happen. Pass this object into any functions.
$data = $data -replace "^\s*$","<#####>"
$data[$section[0]..$section[1]] -replace $oldword,$newword
($output | Where-Object {$_}) -replace "<#####>" | Set-Content $outputFile
Explanation:
Any value that is white space, blank, or null will evaluate to false in a PowerShell boolean conditional statement. Since the Where-Object script block performs a boolean conditional evaluation, you can simply just check the pipeline object ($_). Any value (in this case a line) that is not white space, null, or empty, will be true.
Below is a trivial example of the behavior:
$arr = "one","two","three"
$arr
one
two
three
$arr -replace "two"
one
three
$arr[1] = "two"
$arr
one
two
three
$arr -replace "two" | Where-Object {$_}
one
three
You can set a particular array value to $null and have it appear to go away. When writing to a file, it will appear as if the line has been removed. However, the array will still have that $null value. So you have to be careful.
$arr[1] = $null
$arr
one
three
$arr.count
3
If you use another collection type that supports resizing, you have the Remove method available. At that point though, you are adding extra logic to handle index removals and can't be enumerating the collection while you are changing its size.
If all you are doing is parsing a text file:
function FormatData {
$Input -replace $oldword,$newword
}
$FileContent = Get-Content "C:\TextFile.txt"
$OutputFile = "C:\TextOutput.txt"
$oldword = "oldword"
$newword = "newword"
$FileContent = $FileContent | FormatData
$oldword = '^(Crew: hard-working people)([`r`n]+).*oldword1.*[`r`n]+'
$newword = '$1$2$2' # Leaves a blank Line after Crew: hard-working people
$FileContent = $FileContent | FormatData
$oldword = '^.*oldword1.*[`r`n]+'
$newword = '' # Does not leave a blank Line
$FileContent = $FileContent | FormatData
$FileContent | Set-Content $outputFile

How to check column count in a file to satisfy a condition

I am trying to write a PowerShell script to check the column count and see if it satisfies the condition or else throw error or email.
something I have tried:
$columns=(Get-Content "C:\Users\xs15169\Desktop\temp\OEC2_CFLOW.txt" | select -First 1).Split(",")
$Count=columns.count
if ($count -eq 280)
echo "column count is:$count"
else
email
I'm going to assume your text file is in CSV format, I can't imagine what format you're working with if it's a text-file table and not formatted as CSV.
If your CSV has headers
Process the CSV file, and count the number of properties on the resulting Powershell object.
$columnCount = #( ( Import-Csv '\path\to\file.txt' ).PSObject.Properties ).Count
We need to force the Properties object to an array (which is the #() syntax) to accurately get the count. The PSObject property is a hidden property for metadata about an object in Powershell, which is where we look for the Properties (column names) and get the count of how many there are.
CSV without headers
If your CSV doesn't have headers, Import-Csv requires you to manually specify the headers. There are tricks you can do to build out unique column names on-the-fly, but they are overly complex for simply getting a column count.
To take what you've already tried above, we can get the data in the first line and process the number of columns, though you were doing it incorrectly in the question. Here's how to properly do it:
$columnCount = ( ( Get-Content "\path\to\file.txt" | Select-Object -First 1 ) -Split ',' ).Count
What was wrong with the original
Both above solutions consolidate getting the column count down to one line of code. But in your original sample, you made a couple small mistakes:
$columns=( Get-Content "\path\to\file.txt" | select -First 1 ).Split(",")
# You forgot to prepend "columns" with a $. Should look like the below line
$Count=$columns.count
And you forgot to use curly braces with your if block:
if ($count -eq 280) {
echo "column count is:$count"
} else {
email
}
As for using the -Split operator vs. the .Split() method - this is purely stylistic preference on my part, and using Split() is perfectly valid.

Read a CSV in powershell with a variable number of columns

I have a CSV that contains a username, and then one or more values for the rest of the record. There are no headers in the file.
joe.user,Accounting-SG,CustomerService-SG,MidwestRegion-SG
frank.user,Accounting-SG,EastRegion-SG
I would like to read the file into a powershell object where the Username property is set to the first column, and the Membership property is set to either the remainder of the row (including the commas) or ideally, an array of strings with each element containing a single membership value.
Unfortunately, the following line only grabs the first membership and ignores the rest of the line.
$memberships = Import-Csv -Path C:\temp\values.csv -Header "username", "membership"
#{username=joe.user; membership=Accounting-SG}
#{username=frank.user; membership=Accounting-SG}
I'm looking for either of these outputs:
#{username=joe.user; membership=Accounting-SG,CustomerService-SG,MidwestRegion-SG}
#{username=frank.user; membership=Accounting-SG,EastRegion-SG}
or
#{username=joe.user; membership=string[]}
#{username=frank.user; membership=string[]}
I've been able to get the first result by enclosing the "rest" of the data in the csv file in quotes, but that doesn't really feel like the best answer:
joe.user,"Accounting-SG,CustomerService-SG,MidwestRegion-SG"
Well, the issue is that what you have isn't really a (proper) CSV. The CSV format doesn't support that notation.
You can "roll your own" and just process the file yourself, something like this:
$memberships = Get-Content -LiteralPath C:\temp\values.csv |
ForEach-Object -Process {
$user,$membership = $_.Split(',')
New-Object -TypeName PSObject -Property #{
username = $user
membership = $membership
}
}
You could do a half and half sort of thing. Using your modification, where the groups are all a single field in quotes, do this:
$memberships = Import-Csv -Path C:\temp\values.csv -Header "username", "membership" |
ForEach-Object -Process {
$_.membership = $_.membership.Split(',')
$_
}
The first example just reads the file line by line, splits on commas, then creates a new object with the properties you want.
The second example uses Import-Csv to create the object initially, then just resets the .membership property (it starts as a string, and we split the string so it's now an array).
The second way only makes sense if whatever is creating the "CSV" can create it that way in the first place. If you have to modify it yourself every time, just skip this and process it as it is.