48

In MSBuild v4 one can use functions (like string.replace) on Properties. But how can I use functions on Metadata?

I'd like to use the string.replace function as below:

<Target Name="Build">
  <Message Text="@(Files->'%(Filename).Replace(&quot;.config&quot;,&quot;&quot;)')" />
</Target>   

Unfortunately this outputs as (not quite what I was going for):

log4net.Replace(".config","");ajaxPro.Replace(".config","");appSettings.Replace(".config","");cachingConfiguration20.Replace(".config","");cmsSiteConfiguration.Replace(".config","");dataProductsGraphConfiguration.Replace(".config","");ajaxPro.Replace(".config","");appSettings.Replace(".config","");cachingConfiguration20.Replace(".config","");cmsSiteConfiguration

Any thoughts?

abatishchev
  • 98,240
  • 88
  • 296
  • 433
willem
  • 25,977
  • 22
  • 75
  • 115
  • What are you actually trying to accomplish? Is it desired end result to remove the extension from the items in a item group? You might want to approach this as create a new item group from the original item group modifying the entries. A transform or a custom task if more control is needed could do this. – Brian Walker Feb 24 '11 at 16:57
  • Hi. Did you solve this problem in the end? How? I have a similar issue: property definition does not work as Target has a file list as Input, not a single filename. – superjos Nov 08 '11 at 10:05

6 Answers6

77

You can do this with a little bit of trickery:

$([System.String]::Copy('%(Filename)').Replace('config',''))

Basically, we call the static method 'Copy' to create a new string (for some reason it doesn't like it if you just try $('%(Filename)'.Replace('.config',''))), then call the replace function on the string.

The full text should look like this:

<Target Name="Build">
        <Message Text="@(Files->'$([System.String]::Copy(&quot;%(Filename)&quot;).Replace(&quot;.config&quot;,&quot;&quot;))')" />
</Target>

Edit: MSBuild 12.0 seems to have broken the above method. As an alternative, we can add a new metadata entry to all existing Files items. We perform the replace while defining the metadata item, then we can access the modified value like any other metadata item.

e.g.

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

    <ItemGroup>
        <Files Include="Alice.jpg"/>
        <Files Include="Bob.not-config.gif"/>
        <Files Include="Charlie.config.txt"/>
    </ItemGroup>

    <Target Name="Build">
        <ItemGroup>
            <!-- 
            Modify all existing 'Files' items so that they contain an entry where we have done our replace.
            Note: This needs to be done WITHIN the '<Target>' (it's a requirment for modifying existing items like this
            -->
            <Files>
                <FilenameWithoutConfig>$([System.String]::Copy('%(Filename)').Replace('.config', ''))</FilenameWithoutConfig>
            </Files>
        </ItemGroup>

        <Message Text="@(Files->'%(FilenameWithoutConfig)')" Importance="high" />
    </Target>
</Project>

Result:

D:\temp>"c:\Program Files (x86)\MSBuild\12.0\Bin\MSBuild.exe" /nologo test.xml
Build started 2015/02/11 11:19:10 AM.
Project "D:\temp\test.xml" on node 1 (default targets).
Build:
  Alice;Bob.not-config;Charlie
Done Building Project "D:\temp\test.xml" (default targets).
Grant Peters
  • 7,691
  • 3
  • 45
  • 57
  • 2
    using msbuild 12.0 (and .net 4.0) I get "%(Filename);%(Filename);.." as result. what's wrong with my environment? – nilleb Jan 07 '15 at 09:57
  • @nilleb Can you post the relevant code somewhere (e.g. pastebin.com) and I'll see if I can spot what is wrong – Grant Peters Jan 08 '15 at 01:49
  • I've posted an alternative answer for MSBuild 12.0 – RobV8R Jan 09 '15 at 17:25
  • @nilleb It seems they have changed around the evaluation order in 2012, I'll post a workaround for 2012 shortly – Grant Peters Feb 11 '15 at 01:08
  • 5
    I'd suggest using `$([MSBuild]::ValueOrDefault('%(MetadataName)', '').Replace('foo', 'bar'))`instead of String.Copy, in order to avoid actually creating a copy of the string's data. – Daniel Plaisted May 26 '17 at 20:50
  • Do you know whether it is possible to make functon in MSBuild so I could have simple ReplaceString(%(FileName)) instead of this long line ? – NN_ Jul 10 '19 at 12:08
  • 2
    For information: there's a related open issue on MSBuild's GitHub repository: [Metadata should support instance methods](https://github.com/microsoft/msbuild/issues/1155). – 0xced Sep 20 '19 at 14:00
46

I needed to do something similar, the following worked for me.

<Target Name="Build">
  <Message Text="@(Files->'%(Filename)'->Replace('.config', ''))" />
</Target>
Mike Coumbe
  • 461
  • 4
  • 3
  • 2
    This looks be the most succinct answer. – Thomson Feb 11 '15 at 03:22
  • 3
    This looks like the best answer to me. Short and sweet. – Matt Varblow Aug 13 '15 at 01:00
  • 2
    This makes use of the MSBuild Item Functions, and I'd say it's the cleanest and most idiomatic. It also doesn't incur a string copying like the alternatives. See more at https://learn.microsoft.com/en-us/visualstudio/msbuild/item-functions – kzu Mar 27 '18 at 14:25
23

Those functions works in properties only (as I know). So create target which will perform operation throw batching:

<Target Name="Build"                                 
      DependsOnTargets="ProcessFile" />

<Target Name="ProcessFile"
       Outputs="%(Files.Identity)">
   <PropertyGroup>
       <OriginalFileName>%(Files.Filename)</OriginalFileName>
       <ModifiedFileName>$(OriginalFileName.Replace(".config",""))</ModifiedFileName>
   </PropertyGroup>
   <Message Text="$(ModifiedFileName)" Importance="High"/>
</Target>

Do you really need in your example such kind of task? I mean there exists MSBuild Well-known Item Metadata

EDIT: I should specify that this task processes all items in @(Files).

Community
  • 1
  • 1
Sergio Rykov
  • 4,176
  • 25
  • 23
2

i dont think you can use functions directly with itemgroups and metadata (that would be easy) However you can use batching:

Taking the ideas from this post: array-iteration

I was trying to trim an itemgroup to send to a commandline tool (i needed to lose .server off the filename)

<Target Name="ProcessFile"  DependsOnTargets="FullPaths">

    <ItemGroup>
        <Environments Include="$(TemplateFolder)\$(Branch)\*.server.xml"/>
    </ItemGroup>

    <MSBuild Projects=".\Configure.msbuild" 
             Properties="CurrentXmlFile=%(Environments.Filename)"
             Targets="Configure"/>

</Target>

<Target Name="Configure" DependsOnTargets="FullPaths">
    <PropertyGroup>
        <Trimmed>$(CurrentXmlFile.Replace('.server',''))</Trimmed>
    </PropertyGroup>

    <Message Text="Trimmed: $(Trimmed)"/>

    <Exec  Command="ConfigCmd $(Trimmed)"/>
</Target>
Community
  • 1
  • 1
James Woolfenden
  • 6,498
  • 33
  • 53
1

For MSBuild 12.0, here's an alternative.

<Target Name="Build">
    <Message Text="$([System.String]::Copy(&quot;%(Files.Filename)&quot;).Replace(&quot;.config&quot;,&quot;&quot;))" />
</Target>
RobV8R
  • 1,036
  • 8
  • 16
0

Got the same problem (except with MakeRelative), so I passed with another solution : Using good old CreateItem that take a string and transform to Item :)

        <ItemGroup>
            <_ToUploadFTP Include="$(PublishDir)**\*.*"></_ToUploadFTP>
        </ItemGroup>
        <CreateItem Include="$([MSBuild]::MakeRelative('c:\$(PublishDir)','c:\%(relativedir)%(filename)%(_ToUploadFTP.extension)'))">
            <Output ItemName="_ToUploadFTPRelative" TaskParameter="Include"/>
        </CreateItem>
        <FtpUpload Username="$(UserName)" 
                   Password="$(UserPassword)" 
                   RemoteUri="$(FtpHost)"
                   LocalFiles="@(_ToUploadFTP)"
                   RemoteFiles="@(_ToUploadFTPRelative->'$(FtpSitePath)/%(relativedir)%(filename)%(extension)')"
                   UsePassive="$(FtpPassiveMode)" ></FtpUpload>
Nicolas Dorier
  • 7,383
  • 11
  • 58
  • 71