Search code examples
powershellfilebatch-fileediting

Find and replace in config file powershell or batch file


Say I have a config file that looks like this:

{name1}
Settinga=1
settingb=2
settingc=3
{name2}
Settinga=1
settingb=2
settingc=3
{name3}
Settinga=1
settingb=2
settingc=3

I want to be able to change the line settingb=2 under {name3} to another value such as settingb=4

It would be a file store on a Windows OS so ideally it would be done under PowerShell or batch command.

Anyone have any ideas or if this is possible?

Thanks


Solution

  • What you could do read your config file using Get-Content, and store the section content under each name in a nested hash table, where the name lines are outer keys, and the settings lines are split into keys and values of an inner hash table. To maintain order of keys found from the original file, we can make use of System.Collections.Specialized.OrderedDictionary. To create one, simply add the [ordered] attribute to a hashtable @{}. You can find out more at about_Hash_Tables.

    We can also use System.String.Split to split the lines by =, which will use the length to determine if the line is a name or a setting. Length of 1 is a name and length of 2 is a setting.

    # Read lines from config file
    $config = Get-Content -Path .\config.txt
    
    # Use an ordered hashtable to remember order of keys inserted
    $sections = [ordered]@{}
    
    # Keep a key which indicates the current name being added
    $currentKey = $null
    
    # Go through each line in the config file
    foreach ($line in $config) {
    
        # Split each line by '='
        $items = $line.Split("=")
    
        # If splitted items is only one value, we found a new name
        # Set the new name and create an inner settings dictionary
        if ($items.Length -eq 1) {
            $currentKey = $line
            $sections[$currentKey] = [ordered]@{}
        }
    
        # Otherwise we found a normal line
        else {
    
            # Only add the setting if the current name is not null
            if ($null -ne $currentKey) {
                $sections[$currentKey][$items[0]] = $items[1]
            }
        }
    }
    

    Which will give a hash table $sections that looks like the following:

    Name                           Value
    ----                           -----
    {name1}                        {Settinga, settingb, settingc}
    {name2}                        {Settinga, settingb, settingc}
    {name3}                        {Settinga, settingb, settingc}
    

    Then you could set a value(or multiple values) like this:

    $sections["{name3}"].settingb = 4
    

    And write the updated hash table to a output file using Out-File. To iterate the outer and inner hash tables, we need to iterate their key value pairs with System.Collections.Hashtable.GetEnumerator.

    & {
        # Output each outer key first, where the names are stored
        foreach ($outerKvp in $sections.GetEnumerator()) {
            $outerKvp.Key
    
            # Then output each setting and value
            foreach ($innerKvp in $outerKvp.Value.GetEnumerator()) {
                "$($innerKvp.Key)=$($innerKvp.Value)"
            }
        }
    
    # Pipe output from script block to output file
    } | Out-File -FilePath .\output.txt
    

    The above wraps the foreach loops inside a Call Operator & to run the script block and pipe the output to Out-File. You can have a a look at about_Pipelines and about_Script_Blocks for more information.

    Since I mentioned pipelines and script blocks, we can also use of Foreach-Object to pass input down the pipeline. From some initial testing it seems this is slightly slower than the above solution(will need to investigate further with larger inputs). You can have a look at this Runtime of Foreach-Object vs Foreach loop question for the differences between both approaches.

    $sections.GetEnumerator() | ForEach-Object {
        $_.Key
        $_.Value.GetEnumerator() | ForEach-Object {
            "$($_.Key)=$($_.Value)"
        }
    } | Out-File -FilePath .\output.txt
    

    And finally the newly created output file below.

    output.txt

    {name1}
    Settinga=1
    settingb=2
    settingc=3
    {name2}
    Settinga=1
    settingb=2
    settingc=3
    {name3}
    Settinga=1
    settingb=4
    settingc=3
    

    Which shows settingb for {name3} was updated from 2 from 4.