Search for a xml file and replace a string inside it

11,184

Solution 1

First, I'd recommend doing it all in PowerShell, which ultimately simplifies matters:
The complexity of dealing with 2 very different environments aside, invoking PowerShell commands with powershell -command and a command string containing the command to execute introduces quoting complexities that can be tricky to resolve.

Second, your issue is that your pipeline tries to do 2 disparate things at once:

  • pass a file's path through
  • while also transforming that file's content.

Your attempt muddles the two task: it passes in file objects (that resolve to their paths in a string context), then mistakenly treats them as if they were their contents, then magically expects the modified contents to turn back into file objects (paths) telling Set-Content what files to write back to.

While it is possible to do this with a single pipeline, it would be virtually unreadable, so I recommend performing the two tasks in separate steps:

Note: I know that your Get-ChildItem command will in practice only yield 1 file, but potentially it will yield multiple ones, so I'll try to solve that case.
Furthermore, I've avoided the use of cmd.exe variables (%AppDate%) and instead use only PowerShell's ($env:AppData).
Also, note the use of a back-reference (\1) in the regular expression below, which avoids the need to repeat AdvertPlaceholder, as well as the use of $1 om the substitution expression to refer to the first capture group in the regex (AdvertPlacholder)

If we focus on just PowerShell for now, we get:

$files = Get-ChildItem $env:AppData\Skype -Recurse config.xml
foreach($file in $files) { 
  (Get-Content $file.fullname) -replace '<(AdvertPlaceholder)>1</\1>', '<$1>0</$1>' |
    Set-Content $file.fullname
}

Note:

  • In terms of character encoding, Set-Content defaults to system's default code page. When in doubt, use an explicit encoding, such as -Encode UTF8.

  • The above assumes that the XML element in question is on a single line. If it isn't, things get more complicated: while you can use Get-Content -Raw (PowerShell v3+) to read the entire input file at once, Set-Content will add an extra newline to the output; also, you'd have to adapt the regex passed to -replace above to account for line-spanning whitespace.

    • Matt's helpful answer shows a more robust way of handling the value substitution, using proper XML parsing via the [xml] type (fully qualified name: System.Xml.XmlDocument).

If you still want to call the above PowerShell command from a batch file, here's how to do it:

Note the need to use a single-line literal to pass to powershell.exe -command, which makes the whole thing unreadable; also, adding -noprofile is generally a good idea so as to suppress loading of the profile file, which is intended only for interactive use.

 powershell -noprofile -command "$files = Get-ChildItem $env:AppData\Skype -Recurse config.xml; foreach($file in $files) { (Get-Content $file.fullname) -replace '<(AdvertPlaceholder)>1</\1>', '<$1>0</$1>' | Set-Content $file.fullname }"

Solution 2

You have a couple of issues and I think you are straying down an incorrect path. Nathan Tuggy had one part right, Set-Content takes pipeline input and sends it to file. The path of which, under most circumstances, needs to be specified with -Path (or positionally). Another issue that precedes that is you are not actually reading the file you located. Your current code is attempting to manipulate the file name itself, which is not what you want.

Something closer to what you are looking for would be:

powershell -Command "$file = Get-ChildItem %APPDATA%\Skype -Filter 'config.xml' -Recurse | Select -First 1 -ExpandProperty FullName; (Get-Content $file) | ForEach-Object { $_.replace('<AdvertPlaceholder>1</AdvertPlaceholder>', '<AdvertPlaceholder>0</AdvertPlaceholder>')} | Set-Content $file"

I haven't tested that in batch perfectly mostly because I just wanted to try and show you where you were going wrong.

That gets the file name assuming there is only the one config.xml file in that directory structure. Use -First 1 to ensure only one file name is returned. Then take that file name to feed into Get-Content to get the file. Then process the file in the loop to replace the text you want. Also used .Replace() instead of -replace as the latter supports regex and you are not.

I think, pending testing, that it would solve your issue.


However you are not editing an XML file it the most efficient and reliable way. You could also just have all this logic in a single PowerShell script. Lets look at that shall we:

# Append some loopback entries into the host file. Make sure you are running as admin else this would fail. 
"apps.skype.com","g.msn.com" | Foreach-Object{"127.0.0.1`t$_"} | Add-Content "$env:windir\System32\Drivers\Etc\Hosts"
# Beware that multiple executions of this would keep adding more entries as we do not check the existing ones. 

# Locate and read the config.xml document. Use object notation to find the node we need to change to 0 then save the file. 
$configPath = Get-Item "$($env:appdata)\Skype\*\config.xml" | Select-Object -ExpandProperty FullName
$xml = [xml](Get-Content $configPath)
$xml.config.UI.General.AdvertPlaceholder = "0"
$xml.Save($configPath)

If you need help runnning the script here is a good post to get you started.


I figured only only config.xml was present. If you wanted to edit all config files then very little needs to change. This time though we need to be sure that the value you want to edit exits. Since config.xml is not a unique name.

# Locate and read the config.xml documents. Use object notation to find the node we need to change to 0 then save the file. 
$configPaths = Get-ChildItem "$($env:appdata)\Skype" -Filter "config.xml"  -Recurse | Select-Object -ExpandProperty FullName

# Cycle each file found
$configPaths | ForEach-Object{
    $xml = [xml](Get-Content $_)
    if($xml.config.UI.General.AdvertPlaceholder){
        # Check to be sure this value actually is present before we try to change it. 
        $xml.config.UI.General.AdvertPlaceholder = "0"
        # Only need to save if change was made. 
        $xml.Save($_)
    }
}
Share:
11,184
IDDQD
Author by

IDDQD

Updated on June 28, 2022

Comments

  • IDDQD
    IDDQD almost 2 years

    I am trying to create a batch file in Windows 7 to help me remove Skype Ads.

    Here's what I've come with so far:

    @echo off
    
    echo Editing '%WINDIR%\System32\Drivers\Etc\Hosts' file
    echo 127.0.0.1       apps.skype.com >> %WINDIR%\System32\Drivers\Etc\Hosts
    echo 127.0.0.1       g.msn.com >> %WINDIR%\System32\Drivers\Etc\Hosts
    
    echo Editing '%APPDATA%\Skype' config
    powershell -Command "(Get-ChildItem %APPDATA%\Skype -Filter "config.xml" -Recurse) | ForEach-Object { $_ -replace '<AdvertPlaceholder>1</AdvertPlaceholder>', '<AdvertPlaceholder>0</AdvertPlaceholder>' } | Set-Content $_"
    
    ipconfig /flushdns
    
    pause
    

    The powershell command throws me an error:

    Editing 'C:\Users\user\AppData\Roaming\Skype' config
    Set-Content : Cannot bind argument to parameter 'Path' because it is null.
    At line:1 char:218
    + (Get-ChildItem C:\Users\user\AppData\Roaming\Skype -Filter config.xml -Recurse) | ForEach-Object { $_ -replace '<AdvertPlaceholder>1</AdvertPlaceholder>
    ', '<AdvertPlaceholder>0</AdvertPlaceholder>' } | Set-Content <<<<  $_
        + CategoryInfo          : InvalidData: (:) [Set-Content], ParameterBinding
       ValidationException
        + FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.SetContentCommand
    

    What am I doing wrong? How do I Set-Content to a file found by Get-ChildItem? If you have another approach, I'd like to hear it too. Much appreciated.

    EDIT:

    Here's what I ended up using:

    @echo off
    
    echo Editing '%WINDIR%\System32\Drivers\Etc\Hosts' file
    echo 127.0.0.1       apps.skype.com >> %WINDIR%\System32\Drivers\Etc\Hosts
    echo 127.0.0.1       g.msn.com >> %WINDIR%\System32\Drivers\Etc\Hosts
    
    echo Editing '%APPDATA%\Skype' config
    powershell -noprofile -c "$files = Get-ChildItem $env:AppData\Skype -Recurse config.xml; foreach($file in $files) { (Get-Content $file.fullname) -replace '<(AdvertPlaceholder)>1</\1>', '<$1>0</$1>' | Set-Content $file.fullname }"
    
    ipconfig /flushdns
    
    echo Done. Please restart Skype for changes to take effect.
    
    pause 
    

    Confirmed working at the time of this post.