http://blogs.clariusconsulting.net/kzu

Daniel Cazzulino's Blog

Go Back to
kzu′s Latest post

How to perform regular expression based replacements on files with MSBuild

And without a custom DLL with a task, too Smile.

The example at the bottom of the MSDN page on MSBuild Inline Tasks already provides pretty much all you need for that with a TokenReplace task that receives a file path, a token and a replacement and uses string.Replace with that. Similar in spirit but way more useful in its implementation is the RegexTransform in NuGet’s Build.tasks. It’s much better not only because it supports full regular expressions, but also because it receives items, which makes it very amenable to batching (applying the transforms to multiple items). You can read about how to use it for updating assemblies with a version number, for example.

I recently had a need to also supply RegexOptions to the task so I extended the metadata and a little bit of the inline task so that it can parse the optional flags. So when using the task, I can pass the flags as item metadata as follows:

<Target Name="ReplaceReleaseNotes">
    <ItemGroup>
        <RegexTransform Include="$(BuildRoot)**\*.nuspec"
                        Condition="'$(ReleaseNotes)' != ''">
            <Find><![CDATA[<releaseNotes />|<releaseNotes/>|<releaseNotes>.*</releaseNotes>]]></Find>
            <ReplaceWith><![CDATA[<releaseNotes>$(ReleaseNotes)</releaseNotes>]]></ReplaceWith>
            <Options>Singleline</Options>
        </RegexTransform>
    </ItemGroup>

    <RegexTransform Items="@(RegexTransform)" />
</Target>

It also supports specifying multiple options, like “Singleline | IgnorePatternWhitespace”  and the like.

The code changes to the task are minimal:

<!--
============================================================
            RegexTransform

Transforms the input Items parameter by evaluating the
regular expression in their Find metadata and
replacing with their ReplaceWith metadata. Optional, the
options for the regular expression evaluation can be specified.

Example input item:
        <RegexTransform Include="$(BuildRoot)Src\GlobalAssemblyInfo.cs">
            <Find>AssemblyFileVersion\(".*?"\)</Find>
            <ReplaceWith>AssemblyFileVersion("$(FileVersion)")</ReplaceWith>
            <Options>Multiline | IgnorePatternWhitespace</Options>
        </RegexTransform>

Invoking the target:
    <RegexTransform Items="@(RegexTransform)" />
============================================================
-->
<UsingTask TaskName="RegexTransform"
           TaskFactory="CodeTaskFactory"
           AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
    <ParameterGroup>
        <Items ParameterType="Microsoft.Build.Framework.ITaskItem[]" />
    </ParameterGroup>
    <Task>
        <Using Namespace="System.IO" />
        <Using Namespace="System.Text.RegularExpressions" />
        <Using Namespace="Microsoft.Build.Framework" />
        <Code Type="Fragment"
              Language="cs">
            <![CDATA[
       foreach(var item in Items)
       {
         string fileName = item.GetMetadata("FullPath");
         string find = item.GetMetadata("Find");
         string replaceWith = item.GetMetadata("ReplaceWith");
         string optionsValue = item.GetMetadata("Options") ?? "";

         var options = string.IsNullOrWhiteSpace(optionsValue) ?
             RegexOptions.None : (RegexOptions)Enum.Parse(typeof(RegexOptions), optionsValue.Replace('|', ','));

         if(!File.Exists(fileName))
         {
           Log.LogError("Could not find file: {0}", fileName);
           return false;
         }
         string content = File.ReadAllText(fileName);
         File.WriteAllText(
           fileName,
           Regex.Replace(
             content,
             find,
             replaceWith,
             options
           )
         );
       }
     ]]>
        </Code>
    </Task>
</UsingTask>

 

Update: As noted by Emperor, I simplified the code for parsing the enum from using Aggregate to a simple string replace of | with a comma, which is readily supported by Enum.Parse Smile

Comments

4 Comments

  1. Aggregate is nice, but is there any prohibition against commas in MSBuild? Enum.Parse will work with comma-separated input (Multiline, IgnorePatternWhitespace), or you could do optionsValue.Replace( '|', ',' ) to support the code style :)

  2. Thanks for this, very helpful! Thanks to Emperor XLII too for teaching me something about Enum.Parse!

  3. Great post Daniel, I was looking for something like this to turn my literal root namespace into $RootNamespace$ just before packaging, so the package can contain source code.

    I am curious about your $(ReleaseNotes) variable though. Where do you get if from? Do you read them from a plain text file or something?