145

I have several configuration files nested like such:

C:\Projects\Project_1\project1.config
  
C:\Projects\Project_2\project2.config

In my configuration I need to do a string replace like such:

<add key="Environment" value="Dev"/>

will become:

<add key="Environment" value="Demo"/>

I thought about using batch scripting, but there was no good way to do this, and I heard that with PowerShell scripting you can easily perform this. I have found examples of find/replace, but I was hoping for a way that would traverse all folders within my C:\Projects directory and find any files that end with the '.config' extension. When it finds one, I want it to replace my string values.

Any good resources to find out how to do this or any PowerShell gurus that can offer some insight?

Flavio Vilante
  • 5,131
  • 1
  • 11
  • 15
Brandon
  • 10,744
  • 18
  • 64
  • 97
  • 1
    Let us know how you got on or if there were some odd formatting issues with the files that needed to be addressed. One good thing about the problem is that it's test without affecting production code – Robben_Ford_Fan_boy May 15 '10 at 01:31
  • Okay, I came here looking for PowerShell too, but I _almost_ got suckered into taking up the "there was no good way to do this [in batch]" challenge. Luckily there are a few impressive cmd.exe/DOS style answers if you look far enough down [on this question](https://stackoverflow.com/q/60034/1028230). – ruffin Oct 14 '21 at 20:06

8 Answers8

216

Here a first attempt at the top of my head.

$configFiles = Get-ChildItem . *.config -rec
foreach ($file in $configFiles)
{
    (Get-Content $file.PSPath) |
    Foreach-Object { $_ -replace "Dev", "Demo" } |
    Set-Content $file.PSPath
}
Daniel Liuzzi
  • 16,807
  • 8
  • 52
  • 57
Robben_Ford_Fan_boy
  • 8,494
  • 11
  • 64
  • 85
  • Great stuff Brandon. I havn't fully embraced Powershell but when you consider how long this would take in VBScript!!!! – Robben_Ford_Fan_boy May 18 '10 at 19:18
  • 11
    For this to work in files in subdirectories, you need ".PSPath". Interestingly, when I tried to make this work without a () around get-content it failed at write-content because the file was locked. – Frank Schwieterman Jun 02 '10 at 04:00
  • 27
    Short version (common aliases used): `ls *.config -rec | %{ $f=$_; (gc $f.PSPath) | %{ $_ -replace "Dev", "Demo" } | sc $f.PSPath }` – Artyom Jul 15 '13 at 20:21
  • 1
    Using the get-content and set-content inside the same pipeline is giving me an error because the file locked (this seems like the obvious expected behaviour here). I've avoided the issue by storing the get-content result in a variable. Is there something I'm missing that would allow this to work otherwise? – eddie.sholl Oct 27 '14 at 23:48
  • You have to user bracket in (Get-Content $file.PSPath).. otherwise it remains opened. Same happened to me. – Josef Jetmar Mar 31 '15 at 14:21
  • 1
    The search and replace pattern should be longer to prevent false positive matches. The string "Dev" could occur in other words or locations in the file. – Paul Chernoch Apr 21 '15 at 14:36
  • You might also get an `UnauthorizedAccessException`, in which case you might need to [clear the ReadOnly flag](http://stackoverflow.com/a/895866/1366033) like this: `$configFiles | Set-ItemProperty -Name IsReadOnly -Value $false` – KyleMit Jul 08 '15 at 15:02
  • If you want to filter the files before the replace you can add the filter after the gci: | where-object {$_ | select-string "dev" -quiet} | – Domc Jul 27 '15 at 01:25
  • 5
    @Artyom don't forget the `.` after the `ls`. Got stung by that myself. – AncientSwordRage Sep 26 '15 at 11:32
  • 1
    There is no need for an extra foreach here. (Get-Content myFile ) -replace "cat", "dog" will replace all occurences of "cat" by "dog" – maxday Aug 24 '16 at 14:28
  • 7
    UnauthorizedAccessException may also cause due to folders if you will remove the *.config to run on all files. You can add -File filter to the Get-ChildItem... Took a while to figure it out – Amir Katz May 16 '17 at 12:00
  • Careful!! The `-replace` operator uses regex, so you'll get some unexpected results if you're trying to replace any symbols (like "*"). This burned me just now. – Gabriel Luci Jun 08 '18 at 18:34
  • 1
    This is a good, practical answer, but, unfortunately, it **adds a newline at the end of the file** if there wasn't one already ([possible solution here](https://stackoverflow.com/a/11644795/1028230)) and **removes the [BOM](https://stackoverflow.com/q/2223882/1028230)** if the file uses one. Looks like adding [`-Encoding utf8BOM`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/set-content?view=powershell-7.1#parameters) to `Set-Content` may fix that (if you're using UTF-8 with a BOM, natch). YMMV if these are things to worry about or not. – ruffin Oct 14 '21 at 21:15
  • 1
    This can unintentionally change line endings to Windows' style. For UNIX style line endings (LF), with the common alias form: `ls *.config -rec | %{ $f=$_; ((gc $f.PSPath) -join "\`n") + "\`n" | %{ $_ -replace "Dev", "Demo" } | sc $f.PSPath -NoNewLine }`  . Credit to mklement0 from [SO answer](https://stackoverflow.com/questions/19127741/replace-crlf-using-powershell). WARNING: There's a file size limit. Read the caveats from mklement0's answer. – Skurfur Apr 13 '22 at 13:57
35

PowerShell is a good choice ;) It is very easy to enumerate files in given directory, read them and process.

The script could look like this:

Get-ChildItem C:\Projects *.config -recurse |
    Foreach-Object {
        $c = ($_ | Get-Content) 
        $c = $c -replace '<add key="Environment" value="Dev"/>','<add key="Environment" value="Demo"/>'
        [IO.File]::WriteAllText($_.FullName, ($c -join "`r`n"))
    }

I split the code to more lines to be readable for you. Note that you could use Set-Content instead of [IO.File]::WriteAllText, but it adds new line at the end. With WriteAllText you can avoid it.

Otherwise the code could look like this: $c | Set-Content $_.FullName.

stej
  • 28,745
  • 11
  • 71
  • 104
18

This approach works well:

gci C:\Projects *.config -recurse | ForEach {
  (Get-Content $_ | ForEach {$_ -replace "old", "new"}) | Set-Content $_ 
}
  • Change "old" and "new" to their corresponding values (or use variables).
  • Don't forget the parenthesis -- without which you will receive an access error.
arcain
  • 14,920
  • 6
  • 55
  • 75
Tolga
  • 2,643
  • 1
  • 27
  • 18
  • 5
    So I went with this one for succinct expression - but I had to replace `Get-Content $_` with `Get-Content $_.FullName`, and the equivalent for `Set-Content` for it to handle files that weren't at the root. – Matt Whitfield Sep 28 '19 at 09:41
13

This powershell example looks for all instances of the string "\foo\" in a folder and its subfolders, replaces "\foo\" with "\bar\" AND DOES NOT REWRITE files that don't contain the string "\foo\" This way you don't destroy the file last update datetime stamps where the string was not found:

Get-ChildItem  -Path C:\YOUR_ROOT_PATH\*.* -recurse 
 | ForEach {If (Get-Content $_.FullName | Select-String -Pattern '\\foo\\') 
           {(Get-Content $_ | ForEach {$_ -replace '\\foo\\', '\bar\'}) | Set-Content $_ }
           }
KyleMit
  • 30,350
  • 66
  • 462
  • 664
Kaye
  • 131
  • 1
  • 2
12

I found @Artyom's comment useful but unfortunately they have not posted it as an answer.

This is the short version, in my opinion a better one, of the accepted answer;

ls *.config -rec | %{$f=$_; (gc $f.PSPath) | %{$_ -replace "Dev", "Demo"} | sc $f.PSPath}
M--
  • 25,431
  • 8
  • 61
  • 93
  • 3
    In case anyone else runs across this, as I did -- looking to execute this directly from a batch file -- It may help to use `foreach-object` instead of the `%` alias when executing a command like this. Otherwise, it may result in the error: `Expressions are only allowed as the first element of a pipeline` – Dustin Halstead Mar 08 '18 at 18:50
11

I would go with xml and xpath:

dir C:\Projects\project_*\project*.config -recurse | foreach-object{  
   $wc = [xml](Get-Content $_.fullname)
   $wc.SelectNodes("//add[@key='Environment'][@value='Dev']") | Foreach-Object {$_.value = 'Demo'}  
   $wc.Save($_.fullname)  
}
Leonardo
  • 2,439
  • 6
  • 26
  • 27
Shay Levy
  • 121,444
  • 32
  • 184
  • 206
9

I have written a little helper function to replace text in a file:

function Replace-TextInFile
{
    Param(
        [string]$FilePath,
        [string]$Pattern,
        [string]$Replacement
    )

    [System.IO.File]::WriteAllText(
        $FilePath,
        ([System.IO.File]::ReadAllText($FilePath) -replace $Pattern, $Replacement)
    )
}

Example:

Get-ChildItem . *.config -rec | ForEach-Object { 
    Replace-TextInFile -FilePath $_ -Pattern 'old' -Replacement 'new' 
}
Martin Brandl
  • 56,134
  • 13
  • 133
  • 172
5

When doing recursive replacement, the path and filename need to be included:

Get-ChildItem -Recurse | ForEach {  (Get-Content $_.PSPath | 
ForEach {$ -creplace "old", "new"}) | Set-Content $_.PSPath }

This wil replace all "old" with "new" case-sensitive in all the files of your folders of your current directory.

Geir Ivar Jerstad
  • 486
  • 1
  • 6
  • 9
  • 1
    The ".PSPath" part of your answer really helped me. But I had to change the inner "{$" to "$_". I'd edit your answer, but I'm not using your -creplace part--I'm using the accepted answer with .PSPath – aaaa bbbb Apr 23 '19 at 20:30