I have a string of the following format (the number of lines in this string may vary):
$content = #"
key1
value1
key2
value2
key3
value3
"#
I want to put this data in a hashtable.
(In my case, the data in the $content variable is received in the body of the HTTP response from the Invoke-WebRequest cmdlet by Client/Server Protocol in the 'LiveJournal'. But I am interested in the answer to my question for the general case as well.)
I tried to use the cmdlet ConvertFrom-StringData, but it doesn't work for this case:
PS C:\> ConvertFrom-StringData -StringData $content -Delimiter "`n"
ConvertFrom-StringData: Data line 'key1' is not in 'name=value' format.
I wrote the following function:
function toHash($str) {
$arr = $str -split '\r?\n'
$hash = #{}
for ($i = 0; $i -le ($arr.Length - 1); $i += 2) {
$hash[$arr[$i]] = $arr[$i + 1]
}
return $hash
}
This function works well:
PS C:\> toHash($content)
Name Value
---- -----
key3 value3
key2 value2
key1 value1
My question is: is it possible to do the same thing, but shorter or more elegant? Preferably in one-liner (see the definition of this term in the book 'PowerShell 101'). Maybe there is a convenient regular expression for this case?
As commented by #Santiago Squarzon;
The code you already have looks elegant to me, shorter -ne more elegant
For the "Preferably in one-liner", what exactly is definition of a one line:
A single line, meaning a text string with no linefeeds?
Or a single statement meaning a text string with no linefeeds and no semicolons?
Knowing that there are several ways to cheat on this, like assigning a variable in a condition (which is hard to read).
Anyways, a few side notes:
The snippet you show might have a pitfall if you have an odd number of lines and Set-StrictMode -Version Lastest enabled:
Set-StrictMode -Version Latest
toHash "key1`nvalue1`nkey2"
OperationStopped:
Line |
5 | $hash[$arr[$i]] = $arr[$i + 1]
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Index was outside the bounds of the array.
Name Value
---- -----
key1 value1
The variable neme $content, suggests that you reading the data from a file, possibly with Get-Connent. If that is indeed the case you might consider to stream the input (which conserves memory):
$Content -split '\r?\n' | # Get-Content .\Data.txt
Foreach-Object -Begin {
$Hash = #{}
$Key = $Null
} -Process {
if (!$Key) {
$Key = $_
}
else {
$Hash[$Key] = $_
$Key = $Null
}
} -End {
$Hash
}
And if you create use an [ordered] dictionary, you might even put this is a single statement like:
$Content -split '\r?\n' |Foreach-Object { $h = [Ordered]#{} } { if (!$h.count -or $h[-1]) { $h[$_] = $Null } else { $h[$h.Count - 1] = $_ } } { $h }
(Note that -as with the propose in the question- I do not take into account that there might be empty lines in the input data)
See also PowerShell issue: #13817 Enhance hash table syntax
I have long text files, containing markup for another software. The form of the text is this:
*INIT=D:\ws\**randomnonesense*
ROW=THISISROW,D:\ws\morestuff
PALVELU=200,WSTIME70.DLL,N,A
PALVELU=201,WSLDIR70.DLL,N,A
PALVELU=202,WSLDIX70.DLL,N,A
PALVELU=204,WSEXCE32.DLL,N,A
PALVELU=205,WMON.DLL,N,A
PALVELU=206,WSWORD32.DLL,N,A
PALVELU=207,WSLEPT32.DLL,N,A
PALVELU=208,WSCONV70.DLL,N,A
PALVELU=209,WSFTPC70.DLL,N,A
KUVAUS=\\192.168.169.17\adwise$\applic\LIKSA_TURE.A70,D:\ws\%aspno%\%username%
MDBS-KANTA=LIKSAV,%aspno%LIK.DB,5,RTTL,ANSI,111.111.111.11MDBS-
KANTA=LANKA,%aspno%LAN.DB,5,RTTL,ANSI,1000.000.111.11
I am writing a script, that needs to replace all of those rows that start with the word PALVELU. The amount of other stuff before and after those rows can be any. Also number of those PALVELU-rows or their lenght is not the same in every file. Still I would need to replace all of those rows in every file with another set of rows. Is there a way to do this?
You can use the following method:
Replace(oldStr, newStr)
eg:
Write-Host "replacing a text in string"
$test=" old text to be replaced"
Write-Host "going to replace old"
$test.Replace("old","New")
Write-Host "Text is replaced"
Hope this will helpful.
You can use [regex]::split to split content of your file by lines, after you have all line you can check if each line contain the word PALVELU using -like operator.
I use \n in the regex pattern to split your content on every start of a newline
$rows = [regex]::split("*INIT=D:\ws\**randomnonesense*
ROW=THISISROW,D:\ws\morestuff
PALVELU=200,WSTIME70.DLL,N,A
PALVELU=201,WSLDIR70.DLL,N,A
PALVELU=202,WSLDIX70.DLL,N,A
PALVELU=204,WSEXCE32.DLL,N,A
PALVELU=205,WMON.DLL,N,A
PALVELU=206,WSWORD32.DLL,N,A
PALVELU=207,WSLEPT32.DLL,N,A
PALVELU=208,WSCONV70.DLL,N,A
PALVELU=209,WSFTPC70.DLL,N,A
KUVAUS=\\192.168.169.17\adwise$\applic\LIKSA_TURE.A70,D:\ws\%aspno%\%username%
MDBS-KANTA=LIKSAV,%aspno%LIK.DB,5,RTTL,ANSI,111.111.111.11MDBS-
KANTA=LANKA,%aspno%LAN.DB,5,RTTL,ANSI,1000.000.111.11", "(.*)\n")
$output = [System.Collections.ArrayList]#()
foreach($row in $rows) {
if($row -like '*PALVELU*') {
$output.Add($row.Replace("PALVELU", "TEST")) | Out-Null
} else {
$output.Add($row) | Out-Null
}
}
If the goal is to replace each PALVELU row with a new row, you may do the following:
# Assumes your file is a.txt
$ReplaceThis = '^PALVELU.*$'
$ReplaceWith = '<new row>'
$(switch -regex -file a.txt {
$ReplaceThis { $_ -replace $ReplaceThis,$ReplaceWith }
Default { $_ }
}) | Set-Content a.txt
The code above will result in X number of PALVELU rows being replaced with X number of <new row>.
If the goal is to replace consecutive PALVELU rows with a single <new row> string in files that only contain one set of consecutive PALVELU rows, you may do the following:
$ReplaceThis = '(?sm)^PALVELU.*(?-s)^PALVELU.*$'
$ReplaceWith = '<new row>'
(Get-Content a.txt -Raw) -replace $ReplaceThis,$ReplaceWith |
Set-Content a.txt
If the goal is to replace all PALVELU rows with a single <new row> string at the location of the first PALVELU row and the rows could be anywhere in the file, you may do the following:
$ReplaceThis = '^PALVELU.*$'
$ReplaceWith = '<new row>'
$firstMatch = $false
$(switch -regex -file a.txt {
$ReplaceThis {
if ($firstMatch) {
$_ -replace $ReplaceThis
}
else {
$_ -replace $ReplaceThis,$ReplaceWith
$firstMatch = $true
}
}
default { $_ }
}) -ne '' | Set-Content a.txt
I'm looking for a way to find a word with a value behind it in a piece of text and then update the value.
Example:
In the file the are multiple occurrences of 'schema="anon" maxFileSize="??????" maxBufferSize="123"'
I want to find all the lines containing maxFileSize and then update the unknown value ?????? to 123456.
So far, I came up with this:
cls
$Files = "C:\temp1\file.config","C:\temp2\file.config"
$newMaxFileSize = "123456"
ForEach ($File in $Files) {
If ((Test-Path $File -PathType Leaf) -eq $False) {
Write-Host "File $File doesn't exist"
} Else {
# Check content of file and select all lines where maxFileSize isn't equal to 123456 yet
$Result = Get-Content $File | Select-String -Pattern "maxFileSize" -AllMatches | Select-String -Pattern "123456" -NotMatch -AllMatches
Write-Host $Result
<#
ROUTINE TO UPDATE THE SIZE
#>
}
}
Yet, I have no clue how to find the word "maxFileSize", let alone how to update the value behind it...
Assuming the input file is actually XML, use the following XPath expression to locate all nodes that have a maxFileSize attribute (regardless of value):
# Parse input file as XML
$configXml = [xml](Get-Content $file)
# Use Select-Xml to find relevant nodes
$configXml |Select-Xml '//*[#maxFileSize]' |ForEach-Object {
# Update maxFileSize attribute value
$_.Node.SetAttribute('maxFileSize','123456')
}
# Overwrite original file with updated XML
$configXml.Save($file.FullName)
If the config file is some archaic format for which no readily available parser exists, use the -replace operator to update the value where appropriate:
$Results = #(Get-Content $File) -creplace '(?<=maxFileSize=")[^"]*(?=")','123456'
The pattern used above, (?<=maxFileSize=")[^"]*(?="), describes:
(?<= # Positive look-behind assertion, this pattern MUST precede the match
maxFileSize=" # literal string `maxFileSize="`
) # Close look-behind
[^"]* # Match 0 or more non-" characters
(?= # Positive look-ahead assertion, this pattern MUST succeed the match
" # literal string `"`
) # Close look-ahead
I have a 1st text file looks like this : 12AB34.US. The second text file is CD 34 EF.
I want to find my 2nd text file exist or not in the 1st text file.
I tried to cut 3 characters last in the first text file (.US). Then I split to each 2 characters (because the 2nd text file consist of 2 characters). Then, I tried this code, and it always return "Not Found".
$String = Get-Content "C:\Users\te2.txt"
$Data = Get-Content "C:\Users\Fixed.txt"
$Split = $Data -split '(..)'
$Cut = $String.Substring(0,6)
$String_Split = $Cut -split '(..)'
$String_Split
$Check= $String_Split | %{$_ -match $Split}
if ($Check-contains $true) {
Write-Host "0"
} else {
Write-Host "1"
}
There are a number of problems with your current approach.
The 2-char groups don't align:
# strings split into groups of two
'12' 'AB' '34' # first string
'CD' ' 3' '4 ' # second string
When you test multiple strings with -match, you need to
escape the input string to avoid matchings on meta characters (like .), and
place the collection on the left-hand side of the operator, the pattern on the right:
$Compare = $FBString_Split | % {$Data_Split -match [regex]::Escape($_)}
if ($Compare -contains $true) {
Write-Host "Found"
} else {
Write-Host "Not Found"
}
For a more general solution to find out if any substring of N chars of one string is also a substring of another, you could probably do something like this instead:
$a = '12AB34.US'
$b = 'CD 34 EF'
# we want to test all substrings of length 2
$n = 2
$possibleSubstrings = 0..($n - 1) | ForEach-Object {
# grab substrings of length $n at every offset from 0 to $n
$a.Substring($_) -split "($('.'*$n))" | Where-Object Length -eq $n |ForEach-Object {
# escape the substring for later use with `-match`
[regex]::Escape($_)
}
} |Sort-Object -Unique
# We can construct a single regex pattern for all possible substrings:
$pattern = $possibleSubstrings -join '|'
# And finally we test if it matches
if($b -match $pattern){
Write-Host "Found!"
}
else {
Write-Host "Not found!"
}
This approach will give you the correct answer, but it'll become extremely slow on large inputs, at which point you may want to look at non-regex based strategies like Boyer-Moore
I have a text file with a number of lines. I would like to search each line individually for a particular pattern and, if that pattern is found output a substring at a particular position relative to where the pattern was found.
i.e. if a line contains the pattern at position 20, I would like to output the substring that begins at position 25 on the same line and lasts for five characters.
The following code will output every line that contains the pattern:
select-string -path C:\Scripts\trimatrima\DEBUG.txt -pattern $PATTERN
Where do I go from here?
You can use the $Matches automatic variable:
Last match is stored in $Matches[0], but you can also use named capture groups, like this:
"test","fest","blah" |ForEach-Object {
if($_ -match "^[bf](?<groupName>es|la).$"){
$Matches["groupName"]
}
}
returns es (from "fest") and la (from "blah")
Couple of options.
Keeping Select-String, you'll want to use the .line property to get your substrings:
select-string -path C:\Scripts\trimatrima\DEBUG.txt -pattern $PATTERN |
foreach { $_.line.Substring(19,5) }
For large files, Get-Content with -ReadCount and -match may be faster:
Get-Content C:\Scripts\trimatrima\DEBUG.txt-ReadCount 1000 |
foreach {
$_ -match $pattern |
foreach { $_.substring(19,5) }
}