Powershell : Update properties file with key = value - powershell

My requirement is, I have a properties file say C:\google\configuration\backup\configuration.properties
with content shown below
backup.path = C:\\ProgramData\\google\\backup
backup.volume.guid = \\\\?\\Volume{49e5d325-8065-49f4-bf0d-r4be94cc1feb}\\
backup.max.count = 10
I have a method that takes key and value as input.
function Script:change_or_replace_value([string]$key, [string]$value) {
$origional_file_content = Get-Content $CONF_FILE_LOCATION
$key_value_map = ConvertFrom-StringData($origional_file_content -join [Environment]::NewLine)
$old_value = $key_value_map.$key
$Old_file_pattern = "$key = $old_value"
$new_file_pattern = "$key = $value"
$origional_file_content | ForEach-Object {$_ -Replace $Old_file_pattern, $new_file_pattern} | Set-Content $NEW_FILE_LOCATION
}
If key is "backup.volume.guid" and value is "\\?\Volume{111111-222-222-444-r4be94cc1feb}\"
method should replace the text
backup.path = C:\\ProgramData\\google\\backup
backup.volume.guid = \\\\?\\Volume{111111-222-222-444-r4be94cc1feb}\\
backup.max.count = 10
If key is "backup.volume.guid" and value is "" method should remove the line
backup.path = C:\\ProgramData\\google\\backup
backup.max.count = 10
If the value is empty delete the line else replace the text for the given key.
It contains special character like \ or other characters
How to delete the content if the key exists and value is an empty string

Your current approach has two problems, based on your attempt to update the properties by string manipulation via the file content as a single string:
In the ForEach-Object script block you'd need a different command to eliminate a line, because the -replace operator always returns something: if the regex pattern doesn't match the input, the input string is passed through.
You're missing an additional string-replacement step: ConvertFrom-StringData considers \ an escape character, so any pair of \\ in the input file turns into a single \ in the resulting hashtable. Therefore, you'll also have to double the \\ in $oldvalue and $value in order for the string replacement on the original file content to work.
Also, -replace, because it expects regex (regular expression) as the search operand, requires metacharachters such as \ to be escaped by \-escaping them; you could do that with [regex]::Escape($Old_file_pattern).
I suggest a different approach that avoids these problems, namely:
Directly modify the hashtable that ConvertFrom-StringData returns.
Then serialize the updated hashtable to the output file, using string formatting.
As part of the string formatting, ouble the \ in the values again by using the [string] type's .Replace() method, which operates on literal strings and is simpler (and faster) in this case; however, you could also use the somewhat counter-intuitive -replace '\\', '\\'
# Assign your real path here.
$OCUM_CONF_FILE_LOCATION = 'in.properties'
# Only for demonstration here: create a sample input file.
#'
backup.path = C:\\ProgramData\\google\\backup
backup.volume.guid = \\\\?\\Volume{49e5d325-8065-49f4-bf0d-r4be94cc1feb}\\
backup.max.count = 10
'# > $OCUM_CONF_FILE_LOCATION
# Function which either updates, adds, or removes an entry.
# NOTE:
# * This function updates input file $OCUM_CONF_FILE_LOCATION *in place*.
# To be safe, be sure to have a backup copy before you try this.
# * Set-Content's default character encoding is used to save the updated file.
# Use the -Encoding parameter as needed.
function Update-PropertiesFile ([string]$key, [string]$value) {
$ht = ConvertFrom-StringData (Get-Content -Raw $OCUM_CONF_FILE_LOCATION)
if ($ht.Contains($key)) { # update or delete existing entry
if ('' -eq $value) { $ht.Remove($key) }
else { $ht[$key] = $value }
} elseif ('' -eq $value) { # entry to remove not found
Write-Warning "No entry with key '$key' found; nothing to remove."
return
} else { # new entry
$ht[$key] = $value
}
# Serialize the updated hashtable back to the input file.
Set-Content $OCUM_CONF_FILE_LOCATION -Value $(
foreach ($key in $ht.Keys) {
'{0} = {1}' -f $key, $ht[$key].Replace('\', '\\')
}
)
}

Related

How to replace N number of characters in a file after a specific keyword in Powershell script?

I have a file prototype as follows:
// <some stuff>
#define KEYWORD release01-11
// <more stuff>
How can I delete the last two characters in the same line as KEYWORD and replace them with two different characters (12 in this case), in order to end up with:
// <some stuff>
#define KEYWORD release01-12
// <more stuff>
I'm trying to use Clear-Content and Add-Content but I cannot get it to do what I need. The rest of the file needs to remain unchanged after these symbols have been replaced. Is there a better alternative?
Use the -replace regex operator to identify the relevant statements and replace/remove the trailing numbers:
# read file into a variable
$code = Get-Content myfile.c
# replace the trailing -XX with 12 in all lines starting with `#define KEYWORD`, with
$code = $code -replace '(?<=#define KEYWORD .+-)\d{2}\s*$','12'
# write the contents back to the file
$code |Set-Content myfile.c
The regex construct (?<=...) is a positive lookbehind - it ensures that the following expression will only match at a position where text right behind it is #define KEYWORD followed by some characters and a -.
If you want to always increment the current value (as opposed to just replacing it with 12), we'll need some way to inspect and evaluate the current value before doing the substitution.
The [Regex]::Replace() method allows for just that:
# read file into a variable
$code = Get-Content myfile.c
$code = $code |ForEach-Object {
# Same as before, but now we can hook into the regex engine's substitution routine
[regex]::Replace($_, '(?<=#define KEYWORD .+-)\d{2}\s*$',{
param($m)
# extract the trailing numbers, convert to a numerical type
$value = $m.Value -as [int]
# increment the value
$value++
# return the new value
return $value
})
}
# write the contents back to the file
$code |Set-Content myfile.c
In PowerShell 6.1 and up, the -replace operator natively supports scriptblock substitutions:
$code = $code |ForEach-Object {
# Same as before, but now we can hook into the regex engine's substitution routine
$_ -replace '(?<=#define KEYWORD .+-)\d{2}\s*$',{
# extract the trailing numbers, convert to a numerical type
$value = $_.Value -as [int]
# increment the value
$value++
# return the new value
return $value
}
}

Powershell- match split and replace based on index

I have a file
AB*00*Name1First*Name1Last*test
BC*JCB*P1*Church St*Texas
CD*02*83*XY*Fax*LM*KY
EF*12*Code1*TX*1234*RJ
I need to replace the 5th element in the CD segment alone from LM to ET in each of the file in the folder. Element delimiter is * as mentioned in the above sample file content. I am new to PowerShell and tried a code as below but unfortunately it is not giving desired results. Can any of you please provide some help?
foreach($xfile in $inputfolder)
{
If ($_ match "^CD\*")
{
[System.IO.File]::ReadAllText($xfile).replace(($_.split("*")[5],"ET") | Set-Content $xfile
}
[System.IO.File]::WriteAllText($xfile),((Get-Content $xfile -join("~")))
}
here's a slightly different way to get there ... [grin] what it does ...
fakes reading in a test file
when ready to do this for real, remove the entire #region/#endregion block and use Get-Content.
sets the constants
iterates thru the imported text file lines
checks for a line that starts with the target pattern
if found ...
== escapes the old value with [regex]::Escape() to deal with the asterisks
== replaces the escaped old value with the new value
== outputs the new version of that line
if NOT found, outputs the line as-is
stores all the lines into the $OutStuff var
displays that on screen
the code ...
#region >>> fake reading in a plain text file
# in real life, use Get-Content
$InStuff = #'
AB*00*Name1First*Name1Last*test
BC*JCB*P1*Church St*Texas
CD*02*83*XY*Fax*LM*KY
EF*12*Code1*TX*1234*RJ
'# -split [System.Environment]::NewLine
#endregion >>> fake reading in a plain text file
$TargetLineStart = 'CD*'
$OldValue = '*LM*'
$NewValue = '*ET*'
$OutStuff = foreach ($IS_Item in $InStuff)
{
if ($IS_Item.StartsWith($TargetLineStart))
{
$IS_Item -replace [regex]::Escape($OldValue), $NewValue
}
else
{
$IS_Item
}
}
$OutStuff
output ...
AB*00*Name1First*Name1Last*test
BC*JCB*P1*Church St*Texas
CD*02*83*XY*Fax*ET*KY
EF*12*Code1*TX*1234*RJ
i will leave saving that to a new file [or overwriting the old one] to the user. [grin]
You could capture all that comes before the match in group 1, and match LM.
In the replacement use $1ET
^(CD*(?:[^*\r\n]+\*){5})LM\b
Regex demo
If you don't want to match LM literally, you could also match any other char than * or a newline.
^(CD*(?:[^*\r\n]+\*){5})[^*\r\n]+\b
Replace example
$allText = Get-Content -Raw file.txt
$allText -replace '(?m)^(CD*(?:[^*\r\n]+\*){5})LM\b','$1ET'
Output
AB*00*Name1First*Name1Last*test
BC*JCB*P1*Church St*Texas
CD*02*83*XY*Fax*ET*KY
EF*12*Code1*TX*1234*RJ

PowerShell -Replace appending (oddly) instead of replacing

EDIT: PowerShell version is 5.1
I am writing some code that will take the value of a variable from a file, and if that string is made up of other variables within the file, it will locate them and replace it to reconstruct the run-time value. It assumes the variable contains a string, and that the string describes a directory.
For example, the file contains:
$var0 = "C:\Users\v-anad\Documents"
$var1 = "$var0\TestFolder"
Then when the code looks for $var1, it should return something like: "C:\Users\v-anad\Documents\TestFolder\"
However, the actual output I see is:
\TestFolder"-anad\Documents"
When it replaces, it deletes the correct substring ($var0), but when it inserts the value of $var0, it skips over the characters that existed there before, and appends the remaining characters to the end of the string. I have no clue what/where I've gone wrong.
Here is the code in question:
function Get-Var-Value-In-File([string]$varName, [string]$file) {
$regex = "(?<=\$varName = )[^`n]*"
$content = Get-Content -Raw $file
return [regex]::Match($content, $regex).Value
}
$file = 'C:\Users\v-anad\Documents\TestFolder\TestVars.ps1'
$var = '$var1'
$value = Get-Var-Value-In-File $var $file
$regex = "\$[^\\]*"
$nextVar = [regex]::Match($value, $regex).Value
$nextValue = Get-Var-Value-In-File $nextVar $file
Write-Output "$var = $value"
Write-Output "$nextVar = $nextValue"
Write-Output $nextVar.Replace($nextVar, $nextValue)
Write-Output ($value -replace [regex]::Escape($nextVar),$nextValue)
Output:
$var1 = "$var0\TestFolder"
$var0 = "C:\Users\v-anad\Documents"
"C:\Users\v-anad\Documents"
\TestFolder"-anad\Documents"
Note how the code above does not account for the extra quotes that would be inserted into the final value, so should this curious behavior be fixed, the output will be: ""C:\Users\v-anad\Documents"\TestFolder\"
The problem was there was a carriage return (\r, or `r in PowerShell) that my regex included in the match, causing the behavior when the string replacement occurred. Thanks to PerSerAl for being a second pair of eyes to catch it.

I cannot update ini file. Treating ini file as hastable in Powershell

I need to update ini configuration file. I managed to convert the file to hastable and updating values. But when I check if the changes are correct in the file, it hasn't changed. Add-Content doesn't work. do I need to convert to String to use Add-Content function?
Configuration file is filled with plain text also.
"ini" Configuration file:
[sqlScript1Deployment]
sqlServerName = '??????????'
olapServerName = '??????????'
(...)
My ps1 code:
[hashtable]$ht = Get-Configuration($iniFilepath)
$ht["sqlScript1Deployment"]["sqlServerName"] = 'Master'
$ht | Add-Content $iniFilepath
Expected code in "ini" file:
[sqlScript1Deployment]
sqlServerName = 'Master'
Actual result in "ini" file:
[sqlScript1Deployment]
sqlServerName = '??????????'
I have no idea where you got the Get-Configuration function from, but if it creates a hashtable where each Key is a Section for the INI and every Value is a name/value pair like this:
$ht = #{
'sqlScript1Deployment' = #{
'sqlServerName' = '??????????'
'olapServerName' = '??????????'
}
}
The following code may help:
# set the new value for sqlServerName
$ht['sqlScript1Deployment']['sqlServerName'] = 'Master'
# write the Hashtable back to disk as .INI file
$sb = New-Object -TypeName System.Text.StringBuilder
# the Keys are the Sections in the Ini file
# the properties are name/value pairs within these keys
foreach ($section in $ht.Keys) {
[void]$sb.AppendLine("[$section]")
foreach ($name in $ht[$section].Keys) {
$value = $ht[$section][$name]
# the value needs to be quoted when:
# - it begins or ends with whitespace characters
# - it contains single or double quote characters
# - it contains possible comment characters ('#' or ';')
if ($value -match '^\s+|[#;"'']|\s+$') {
# escape quotes inside the value and surround the value with double quote marks
$value = '"' + ($value -replace '(["''])', '\$1') + '"'
}
[void]$sb.AppendLine("$name = $value")
}
}
$sb.ToString() | Out-File $iniFilepath
[void]$sb.Clear()
The resulting file will look like this:
[sqlScript1Deployment]
sqlServerName = Master
olapServerName = ??????????

Reading strings from text files using switch -regex returns null element

Question:
The intention of my script is to filter out the name and phone number from both text files and add them into a hash table with the name being the key and the phone number being the value.
The problem I am facing is
$name = $_.Current is returning $null, as a result of which my hash is not getting populated.
Can someone tell me what the issue is?
Contents of File1.txt:
Lori
234 east 2nd street
Raleigh nc 12345
9199617621
lori#hotmail.com
=================
Contents of File2.txt:
Robert
2531 10th Avenue
Seattle WA 93413
2068869421
robert#hotmail.com
Sample Code:
$hash = #{}
Switch -regex (Get-content -Path C:\Users\svats\Desktop\Fil*.txt)
{
'^[a-z]+$' { $name = $_.current}
'^\d{10}' {
$phone = $_.current
$hash.Add($name,$phone)
$name=$phone=$null
}
default
{
write-host "Nothing matched"
}
}
$hash
Remove the current property reference from $_:
$hash = #{}
Switch -regex (Get-content -Path C:\Users\svats\Desktop\Fil*.txt)
{
'^[a-z]+$' {
$name = $_
}
'^\d{10}' {
$phone = $_
$hash.Add($name, $phone)
$name = $phone = $null
}
default {
Write-Host "Nothing matched"
}
}
$hash
Mathias R. Jessen's helpful answer explains your problem and offers an effective solution:
it is automatic variable $_ / $PSItem itself that contains the current input object (whatever its type is - what properties $_ / $PSItem has therefore depends on the input object's specific type).
Aside from that, there's potential for making the code both less verbose and more efficient:
# Initialize the output hashtable.
$hash = #{}
# Create the regex that will be used on each input file's content.
# (?...) sets options: i ... case-insensitive; m ... ^ and $ match
# the beginning and end of every *line*.
$re = [regex] '(?im)^([a-z]+|\d{10})$'
# Loop over each input file's content (as a whole, thanks to -Raw).
Get-Content -Raw File*.txt | foreach {
# Look for name and phone number.
$matchColl = $re.Matches($_)
if ($matchColl.Count -eq 2) { # Both found, add hashtable entry.
$hash.Add($matchColl.Value[0], $matchColl.Value[1])
} else {
Write-Host "Nothing matched."
}
}
# Output the resulting hashtable.
$hash
A note on the construction of the .NET [System.Text.RegularExpressions.Regex] object (or [regex] for short), [regex] '(?im)^([a-z]+|\d{10})$':
Embedding matching options IgnoreCase and Multiline as inline options i and m directly in the regex string ((?im) is convenient, in that it allows using simple cast syntax ([regex] ...) to construct the regular-expression .NET object.
However, this syntax may be obscure and, furthermore, not all matching options are available in inline form, so here's the more verbose, but easier-to-read equivalent:
$re = New-Object regex -ArgumentList '^([a-z]+|\d{10})$', 'IgnoreCase, Multiline'
Note that the two options must be specified comma-separated, as a single string, which PowerShell translates into the bit-OR-ed values of the corresponding enumeration values.
other solution, use convertfrom-string
$template=#'
{name*:Lori}
{street:234 east 2nd street}
{city:Raleigh nc 12345}
{phone:9199617621}
{mail:lori#hotmail.com}
{name*:Robert}
{street:2531 10th Avenue}
{city:Seattle WA 93413}
{phone:2068869421}
{mail:robert#hotmail.com}
{name*:Robert}
{street:2531 Avenue}
{city:Seattle WA 93413}
{phone:2068869421}
{mail:robert#hotmail.com}
'#
Get-Content -Path "c:\temp\file*.txt" | ConvertFrom-String -TemplateContent $template | select name, phone