MS Build refactoring of repetitive commands - msbuild

I have an msbuild file, one section does a bunch of text replacement in a file (to generate a fill out a template nuspec file)
<ReplaceFileText InputFilename="$(SolutionDir)$(SolutionName).nuspec"
MatchExpression="\$Version\$" ReplacementText="$(Version)" />
<ReplaceFileText InputFilename="$(SolutionDir)$(SolutionName).nuspec"
MatchExpression="\$SolutionName\$" ReplacementText="$(SolutionName)" />
<ReplaceFileText InputFilename="$(SolutionDir)$(SolutionName).nuspec"
MatchExpression="\$Authors\$" ReplacementText="$(Authors)" />
<ReplaceFileText InputFilename="$(SolutionDir)$(SolutionName).nuspec"
MatchExpression="\$Owners\$" ReplacementText="$(Owners)" />
<ReplaceFileText InputFilename="$(SolutionDir)$(SolutionName).nuspec"
MatchExpression="\$Summary\$" ReplacementText="$(Summary)" />
<ReplaceFileText InputFilename="$(SolutionDir)$(SolutionName).nuspec"
MatchExpression="\$Description\$" ReplacementText="$(Description)" />
<ReplaceFileText InputFilename="$(SolutionDir)$(SolutionName).nuspec"
MatchExpression="\$Tags\$" ReplacementText="$(Tags)" />
<ReplaceFileText InputFilename="$(SolutionDir)$(SolutionName).nuspec"
MatchExpression="\$ProjectUrl\$" ReplacementText="$(ProjectUrl)" />
<ReplaceFileText InputFilename="$(SolutionDir)$(SolutionName).nuspec"
MatchExpression="\$IconUrl\$" ReplacementText="$(IconUrl)" />
<ReplaceFileText InputFilename="$(SolutionDir)$(SolutionName).nuspec"
MatchExpression="\$BuildDir\$" ReplacementText="$(BuildDir)" />
As you can see, this is a ton of duplication, aside from the actual text being replaced, and the property name (which themselves are the same).
Is there a way to rewrite this? I'm not having much luck finding the right command.
ps. ReplaceFileText is a custom Task.

We could complicate the mix with "What if I wanted to make those changes to more than just that .nuspec file?", but you're trying to refactor that to make it readable.
Lets create a sample project we call "batching.proj" that we can call from the command line using "MsBuild.exe Batching.proj" and view the output.
<Project ToolsVersion="4.0" DefaultTargets="ReplaceFileText" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Here are some sample properties that will appear throughout the script... -->
<PropertyGroup>
<SolutionDir>c:\MsBuild\</SolutionDir>
<SolutionName>Batching</SolutionName>
<Version>1.0.0.1</Version>
<Authors>Authors</Authors>
<Owners>Owner list</Owners>
<Summary>Lorem ipsum dolor sit amet.</Summary>
<Description>Consectetur adipiscing elit.</Description>
<Tags>lists, tags, collections</Tags>
<ProjectUrl>ProjectUrl</ProjectUrl>
<IconUrl>IconUrl</IconUrl>
<BuildDir>c:\MsBuild\Build\</BuildDir>
</PropertyGroup>
<ItemGroup>
<!-- In the custom item group, lets say we want to perform that replacement command on multiple input files. -->
<ReplaceFiles Include="$(SolutionDir)$(SolutionName).nuspec;$(SolutionDir)$(SolutionName).oldspec;$(SolutionDir)$(SolutionName).brrwdspec;$(SolutionDir)$(SolutionName).bluspec" />
<!-- Add our Key-Value pairs in their own itemgroup -->
<MatchExpression Include="\$Version\$">
<Text>$(Version)</Text>
</MatchExpression>
<MatchExpression Include="\$SolutionName\$">
<Text>$(SolutionName)</Text>
</MatchExpression>
<MatchExpression Include="\$Authors\$">
<Text>$(Authors)</Text>
</MatchExpression>
<MatchExpression Include="\$Owners\$">
<Text>$(Owners)</Text>
</MatchExpression>
<MatchExpression Include="\$Summary\$">
<Text>$(Summary)</Text>
</MatchExpression>
<MatchExpression Include="\$Description\$">
<Text>$(Description)</Text>
</MatchExpression>
<MatchExpression Include="\$Tags\$">
<Text>$(Tags)</Text>
</MatchExpression>
<MatchExpression Include="\$ProjectUrl\$">
<Text>$(ProjectUrl)</Text>
</MatchExpression>
<MatchExpression Include="\$IconUrl\$">
<Text>$(IconUrl)</Text>
</MatchExpression>
<MatchExpression Include="\$BuildDir\$">
<Text>$(BuildDir)</Text>
</MatchExpression>
</ItemGroup>
<!-- Our custom target batches over each input file in #(ReplaceFiles).
The Message task will
iterate over each #(MatchExpression) ItemGroup member invoking the message
task each time. This is where you'd call your custom replacement task. -->
<Target Name="ReplaceFileText" Inputs="#(ReplaceFiles)" Outputs="%(ReplaceFiles->'%(Identity).xml')">
<!-- Add your ReplaceFileText call here. -->
<Message Text="In file #(ReplaceFiles) replacing "%(MatchExpression.Identity)" with "%(MatchExpression.Text)" " Importance="High" />
</Target>
</Project>
Outputs:
ReplaceFileText:
In file c:\MsBuild\Batching.nuspec replacing "\$Version\$" with "1.0.0.1"
In file c:\MsBuild\Batching.nuspec replacing "\$SolutionName\$" with "Batching"
In file c:\MsBuild\Batching.nuspec replacing "\$Authors\$" with "Authors"
In file c:\MsBuild\Batching.nuspec replacing "\$Owners\$" with "Owner list"
In file c:\MsBuild\Batching.nuspec replacing "\$Summary\$" with "Lorem ipsum dolor sit amet."
In file c:\MsBuild\Batching.nuspec replacing "\$Description\$" with "Consectetur adipiscing elit."
In file c:\MsBuild\Batching.nuspec replacing "\$Tags\$" with "lists, tags, collections"
In file c:\MsBuild\Batching.nuspec replacing "\$ProjectUrl\$" with "ProjectUrl"
In file c:\MsBuild\Batching.nuspec replacing "\$IconUrl\$" with "IconUrl"
In file c:\MsBuild\Batching.nuspec replacing "\$BuildDir\$" with "c:\MsBuild\Build\"
ReplaceFileText:
In file c:\MsBuild\Batching.oldspec replacing "\$Version\$" with "1.0.0.1"
In file c:\MsBuild\Batching.oldspec replacing "\$SolutionName\$" with "Batching"
In file c:\MsBuild\Batching.oldspec replacing "\$Authors\$" with "Authors"
In file c:\MsBuild\Batching.oldspec replacing "\$Owners\$" with "Owner list"
In file c:\MsBuild\Batching.oldspec replacing "\$Summary\$" with "Lorem ipsum dolor sit amet."
In file c:\MsBuild\Batching.oldspec replacing "\$Description\$" with "Consectetur adipiscing elit."
In file c:\MsBuild\Batching.oldspec replacing "\$Tags\$" with "lists, tags, collections"
In file c:\MsBuild\Batching.oldspec replacing "\$ProjectUrl\$" with "ProjectUrl"
In file c:\MsBuild\Batching.oldspec replacing "\$IconUrl\$" with "IconUrl"
In file c:\MsBuild\Batching.oldspec replacing "\$BuildDir\$" with "c:\MsBuild\Build\"
ReplaceFileText:
In file c:\MsBuild\Batching.brrwdspec replacing "\$Version\$" with "1.0.0.1"
In file c:\MsBuild\Batching.brrwdspec replacing "\$SolutionName\$" with "Batching"
In file c:\MsBuild\Batching.brrwdspec replacing "\$Authors\$" with "Authors"
In file c:\MsBuild\Batching.brrwdspec replacing "\$Owners\$" with "Owner list"
In file c:\MsBuild\Batching.brrwdspec replacing "\$Summary\$" with "Lorem ipsum dolor sit amet."
In file c:\MsBuild\Batching.brrwdspec replacing "\$Description\$" with "Consectetur adipiscing elit."
In file c:\MsBuild\Batching.brrwdspec replacing "\$Tags\$" with "lists, tags, collections"
In file c:\MsBuild\Batching.brrwdspec replacing "\$ProjectUrl\$" with "ProjectUrl"
In file c:\MsBuild\Batching.brrwdspec replacing "\$IconUrl\$" with "IconUrl"
In file c:\MsBuild\Batching.brrwdspec replacing "\$BuildDir\$" with "c:\MsBuild\Build\"
ReplaceFileText:
In file c:\MsBuild\Batching.bluspec replacing "\$Version\$" with "1.0.0.1"
In file c:\MsBuild\Batching.bluspec replacing "\$SolutionName\$" with "Batching"
In file c:\MsBuild\Batching.bluspec replacing "\$Authors\$" with "Authors"
In file c:\MsBuild\Batching.bluspec replacing "\$Owners\$" with "Owner list"
In file c:\MsBuild\Batching.bluspec replacing "\$Summary\$" with "Lorem ipsum dolor sit amet."
In file c:\MsBuild\Batching.bluspec replacing "\$Description\$" with "Consectetur adipiscing elit."
In file c:\MsBuild\Batching.bluspec replacing "\$Tags\$" with "lists, tags, collections"
In file c:\MsBuild\Batching.bluspec replacing "\$ProjectUrl\$" with "ProjectUrl"
In file c:\MsBuild\Batching.bluspec replacing "\$IconUrl\$" with "IconUrl"
In file c:\MsBuild\Batching.bluspec replacing "\$BuildDir\$" with "c:\MsBuild\Build\"

Related

delete lines before and after a string is found using sed

below is my xml file content -
<?xml version="1.0" encoding="UTF-8"?>
<artifactListing>
<folder id="REPORTMART" path="/Repository Objects" pathAlias="/00"
modifiedBy="Maria" lastUpdated="1480426973000" description="Hyperion Root Folder"/>
<folder id="DATASOURCESFOLD"
path="/Repository Objects/HRInternalFolder/DataSources"
pathAlias="/00/HRInternal/DataSources" modifiedBy="Maria" lastUpdated="1492814854000"/>
<folder id="HRINTERNALFOLD"
path="/Repository Objects/HRInternalFolder"
pathAlias="/00/HRInternal" modifiedBy="Maria" lastUpdated="1492814854000"/>
<folder id="00000158e031595b-0000-0782-0ae57730"
path="/Repository Objects/TRCS" pathAlias="/00/0"
modifiedBy="demoadmin" lastUpdated="1492814854000" description="TRCS"/>
<resource id="JavaScriptUpdateResizeOn_dds_js"
path="/Repository Objects/Administration/Impact Manager/Script Repository"
pathAlias="/00/Administration/0/Script_Repository"
modifiedBy="Maria" lastUpdated="1492814880000"
description="JavaScript Update DDS configuration with Layout Manager"
name="JavaScriptUpdateResizeOn_dds.js" type="text/im-javascript" size="-1"/>
<resource id="449cb46e6b4492f3afb8ef693dffb43a90cdd992"
path="/Security" pathAlias="/02"
description="Shared Services Administrator"
name="epm_default_cloud_admin" type="UserPreferences" size="-1"/>
<resource id="0f62187cf5a8f5aecec7a9879c9e40497d6d8649"
path="/Security" pathAlias="/02" description="" name="Jacob"
type="UserPreferences" size="-1"/>
<resource id="0df02da8548eeef2174c97c2ade67b4c5adc3160"
path="/Security" pathAlias="/02" description="" name="Henry"
type="UserPreferences" size="-1"/>
<resource id="33dca1c0c1c5ae78f67580a76d9c6aba6a172e20"
path="/Security" pathAlias="/02" description="" name="Susan"
type="UserPreferences" size="-1"/>
<resource id="3e182b1ea9376483a38614d916a0b666ef531b6d"
path="/Security" pathAlias="/02" description="" name="Maria"
type="UserPreferences" size="-1"/>
<resource id="0f62187cf5a8f5aecec7a9879c9e40497d6d8649"
path="/Security" pathAlias="/02" description="" name="Jacques"
type="UserPreferences" size="-1"/>
<resource id="0df02da8548eeef2174c97c2ade67b4c5adc3160"
path="/Security" pathAlias="/02" description="" name="Frank"
type="UserPreferences" size="-1"/>
<resource id="PP_3e182b1ea9376483a38614d916a0b666ef531b6d_0"
path="/Product Preferences" pathAlias="/05"
description="This is your default Personal Page."
name="My Personal Page" type="PersonalPageContent" size="-1"/>
</artifactListing>
now using sed I would like to delete entire resource tag if "Susan" string is found within it, other non-Susan resource tag's should not be considered.
In this scenario, it's only 1 line before and after string, I've other cases where there are more line within the resource tag.
Using XML parser is the right way for manipulating XML documents.
xmlstarlet solution:
xmlstarlet ed -d '//resource[#name="Susan"]' yourxmlfile
ed - edit mode
-d - delete action
//resource[#name="Susan"] - xpath expression
For your scenario, try this:
$ sed -ibak -r '/^\s*<resource/!bend;:loop;N;/\/>.*$/{/Susan/d;bend};bloop;:end' filename
Explains:
/^\s*<resource/!bend: if pattern space does NOT start with ^\s*<resource, jump to the label named end to start a new loop.
:loop: set a label named loop to deal with the whole resource tag.
N: use N command to append a \n and next line into pattern space.
/\/>.*$/{/Susan/d}: if current pattern space ends with />$, which means we have got a complete resource tag in pattern space, then we can deal with it; if this complete resource tag contains Susan, which is your pattern, use d command to delete all contents in pattern space and then jump to label named end to start a new loop.
bloop: use a loop to append the remaining lines of current resource tag into pattern space.
use -ibak to backup the origin file.
P.S: It will ALSO work for other cases where there are more lines within the resource tag.

MSBuild UsingTask Resolve References

I feel like I've fixed this before, but I can't remember how.
I have a tasks file that looks like this (CustomTasks.tasks):
<UsingTask AssemblyFile="CustomTasks.dll" TaskName="MyCustomTask"/>
it references an assembly (namely Ionic.Zip.dll). Ionic.Zip.dll is not in the GAC (and I don't want it to be). It sits right next to my CustomTasks.dll.
I have a directory called MSBuild one level up from my sln file which has CustomTasks.tasks, CustomTasks.dll and Ionic.Zip.dll in it.
I have a csproj that references the tasks file and calls the custom task:
<Import Project="$(ProjectDir)\..\MSBuild\CustomTasks.tasks" />
<MyCustomTask ..... />
at build time, this yields:
The "MyCustomTask" task could not be loaded from the assembly ....MyCustomTasks.dll. Could not load file or assembly 'Ionic.Zip,......' or one of its dependencies.
Got tired and frustrated and took a direct approach...I don't think this is the same way I solved the problem previously...but maybe this will help someone else. Other, more elegant solutions are more than welcome.
<Target Name="BeforeBeforeBuild" BeforeTargets="BeforeBuild">
<HandleAssemblyResolve SearchPath="$(ProjectDir)\..\MSBuild\" />
</Target>
<UsingTask TaskName="HandleAssemblyResolve" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<SearchPath ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Using Namespace="System" />
<Using Namespace="System.IO" />
<Using Namespace="System.Reflection" />
<Code Type="Fragment" Language="cs">
<![CDATA[
AppDomain.CurrentDomain.AssemblyResolve += (sender, e) =>
{
var assemblySearchPath = Path.Combine(SearchPath, e.Name.Split(',')[0]);
if (File.Exists(assemblySearchPath)) return Assembly.LoadFrom(assemblySearchPath);
return null;
};
]]>
</Code>
</Task>
</UsingTask>
This is actually easy to fix. Put your custom build tasks and dependencies in a different folder. Then dependencies are loaded correctly.
For example like so:
<UsingTask AssemblyFile="..\BuildTools\CustomTasks.dll" TaskName="MyCustomTask"/>

Obtain file sizes in MSBuild script

Can I find out the sizes of a set of files from an MSBuild script without writing my own code?
MSBuild itself some metadata on individual items (for example, the last modified time at %(ModifiedTime)
), but no sizes. I can't see anything at http://msbuildextensionpack.com/.
Edit: based on Seva's answer, here's an inline task that returns the total size of an array of items:
<UsingTask TaskName="GetFileSize" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<Files ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
<TotalSize ParameterType="System.Int64" Output="true"/>
</ParameterGroup>
<Task>
<Using Namespace="System.IO"/>
<Code Type="Fragment" Language="cs"><![CDATA[
long l = 0;
foreach (var item in Files) {
var fi = new FileInfo(item.ItemSpec);
l += fi.Length;
}
TotalSize = l;
]]></Code>
</Task>
</UsingTask>
You can actually use a sub-set of .Net API within msbuild projects using inline tasks.
E.g. the following prints out a file size of a single file:
<UsingTask TaskName="GetFileSize" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<FileName Required="true" />
<FileSize ParameterType="System.Int64" Output="true"/>
</ParameterGroup>
<Task>
<Using Namespace="System.IO"/>
<Code Type="Fragment" Language="cs"><![CDATA[
FileInfo fi = new FileInfo(FileName);
FileSize = fi.Length;
]]></Code>
</Task>
</UsingTask>
<Target Name="PrintFileSize" >
<GetFileSize FileName="$(MyFileName)">
<Output TaskParameter="FileSize" PropertyName="MyFileSize" />
</GetFileSize>
<Message Text="file size of $(MyFileName) is $(MyFileSize)" />
</Target>

Why does the MsbuildExtension Detokenise class reload the project?

This is problematic because any Properties being passed in are lost.
Further Explanation: I pass in a property to the project file. This property is a path to a .props file. It contains tokens and replacement values for the detokenise class. The task apparently reloads the project and the path is not maintained. This doesn't seem to be the case for other task, for example the guid tasks.
In the example I am using a example proj entitled guids.proj
Invoked Using :
<MSBuild.ExtensionPack.FileSystem.Detokenise TaskAction="Detokenise" TargetFiles="#(FileCollectionToBeDetokenized )"/>
Some command line out put follows :
Task "MSBuild.ExtensionPack.FileSystem.Detokenise" (TaskId:11)
Detokenise Task Execution Started [13:04:35] (TaskId:11)
Loading Project: C:\Users\bstrausser\Desktop\guids.proj (TaskId:11)
Detokenising Collection: 1 files (TaskId:11)
C:\Users\*****\Desktop\guids.proj(37,9): error : Property not found: Asset
Directory
Full project file :
Project ToolsVersion="4.0" DefaultTargets="Default" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(ParentMSBuildProjectDirectory)\Bin\MSBuild\ExtensionPack\MSBuild.ExtensionPack.tasks" Condition="Exists('$(ParentMSBuildProjectDirectory)\Bin\MSBuild\ExtensionPack\MSBuild.ExtensionPack.tasks')"/>
<Import Project="C:\Program Files (x86)\MSBuild\ExtensionPack\4.0\MSBuild.ExtensionPack.tasks" Condition="!Exists('$(ParentMSBuildProjectDirectory)\Bin\MSBuild\ExtensionPack\MSBuild.ExtensionPack.tasks') AND Exists('C:\Program Files (x86)\MSBuild\ExtensionPack\4.0\MSBuild.ExtensionPack.tasks')"/>
<PropertyGroup>
<TPath>$(MSBuildProjectDirectory)\..\MSBuild.ExtensionPack.tasks</TPath>
<TPath Condition="Exists('$(MSBuildProjectDirectory)\..\..\Common\MSBuild.ExtensionPack.tasks')">$(MSBuildProjectDirectory)\..\..\Common\MSBuild.ExtensionPack.tasks</TPath>
<PROPS>$(DACP)</PROPS>
</PropertyGroup>
<Import Project="$(PROPS)" Condition="'$(DACP)' != ''" />
<Target Name="Default">
<Message text = "$(DACP)" />
<!-- Create a new Guid and get the formatted and unformatted values -->
<MSBuild.ExtensionPack.Framework.Guid TaskAction="Create">
<Output TaskParameter="FormattedGuidString" PropertyName="FormattedGuidString1" />
<Output TaskParameter="GuidString" PropertyName="GuidStringItem" />
</MSBuild.ExtensionPack.Framework.Guid>
<Message Text="GuidStringItem: $(GuidStringItem)"/>
<Message Text="FormattedGuidString: $(FormattedGuidString1)"/>
<!-- Create a new cryptographically strong Guid and get the formatted and unformatted values -->
<MSBuild.ExtensionPack.Framework.Guid TaskAction="CreateCrypto">
<Output TaskParameter="FormattedGuidString" PropertyName="FormattedGuidString1" />
<Output TaskParameter="GuidString" PropertyName="GuidStringItem" />
</MSBuild.ExtensionPack.Framework.Guid>
<Message Text="GuidStringItem Crypto: $(GuidStringItem)"/>
<Message Text="FormattedGuidString Crypto: $(FormattedGuidString1)"/>
<ItemGroup>
<FileCollectionToBeDetokenized Include="C:\Code\MSBuildGit\Configuration\TaskExecutorConfigTransforms\App.GREEN.SCRATCH.config"/>
</ItemGroup>
<Message text = "BaseUrl : $(BaseUrl)" />
<Message text = "DetokenizedTransformFile : #(FileCollectionToBeDetokenized)" />
<MSBuild.ExtensionPack.FileSystem.Detokenise TaskAction="Detokenise" TargetFiles="#(FileCollectionToBeDetokenized )"/>
</Target>

Copy Item's Metadata for batching

This is an extension to this question.
Suppose I'm creating items from other item's metadata:
<ItemGroup>
<A>
<files>f1;f2;f3;...</files>
<x>...</x>
<y>...</y>
<z>...</z>
...
</A>
<B Include="%(A.files)">
<x>%(A.x)</x>
<y>%(A.y)</y>
<z>%(A.z)</z>
...
</B>
</ItemGroup>
%(A.files) is a list of files separated by ;, such that for each A item I'm creating many B items (one for each file).
But frequently when I process B item I need the original's A item metadata. In this example I copied each metadata manually from A to B.
Is there a way to copy all of A's metadata to B without explicitly specifying each one of them?
Little late, but I like this solution better:
<B Include="#(A->Metadata('files'))" />
Full Example:
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<A Include="A1">
<files>a1_file1.htm;a1_file2.htm</files>
<x>a1</x>
<y>b1</y>
<z>c1</z>
</A>
<A Include="A2">
<files>a2_file.proj</files>
<x>a2</x>
<y>b2</y>
<z>c2</z>
</A>
<B Include="#(A->Metadata('files'))" />
</ItemGroup>
<Target Name="Build">
<Message Text="A: #(A->'%(Identity) x:%(x) y:%(y) z:%(z) files:%(files)', '
')" />
<Message Text="B: #(B->'%(Identity) x:%(x) y:%(y) z:%(z) files:%(files)', '
')" />
</Target>
</Project>
Output:
Build:
A: A1 x:a1 y:b1 z:c1 files:a1_file1.htm;a1_file2.htm
A2 x:a2 y:b2 z:c2 files:a2_file.proj
B: a1_file1.htm x:a1 y:b1 z:c1 files:a1_file1.htm;a1_file2.htm
a1_file2.htm x:a1 y:b1 z:c1 files:a1_file1.htm;a1_file2.htm
a2_file.proj x:a2 y:b2 z:c2 files:a2_file.proj
As far as I can tell, MSBuild only copies metadata when there is a one-to-one mapping between the input and output item lists. In your case, you're starting with one item and expanding to many items. To get around this, I suggest using item batching:
<?xml version="1.0" encoding="iso-8859-1"?>
<Project
xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
ToolsVersion="4.0"
DefaultTargets="Print">
<ItemGroup>
<A Include="A1">
<files>test.htm;test_sol1.htm</files>
<x>a1</x>
<y>b1</y>
<z>c1</z>
</A>
<A Include="A2">
<files>test.proj</files>
<x>a2</x>
<y>b2</y>
<z>c2</z>
</A>
</ItemGroup>
<Target Name="ExpandA">
<ItemGroup>
<ExpandedA Include="%(A.files)">
<Original>%(Identity)</Original>
</ExpandedA>
</ItemGroup>
</Target>
<Target
Name="CopyMetadata"
Outputs="%(ExpandedA.Identity)"
DependsOnTargets="ExpandA">
<PropertyGroup>
<ExpandedAIdentity>%(ExpandedA.Identity)</ExpandedAIdentity>
<ExpandedAOriginal>%(ExpandedA.Original)</ExpandedAOriginal>
</PropertyGroup>
<ItemGroup>
<ExpandedAMetadata Include="#(A)" Condition=" '%(Identity)' == '$(ExpandedAOriginal)' ">
<Expanded>$(ExpandedAIdentity)</Expanded>
</ExpandedAMetadata>
</ItemGroup>
</Target>
<Target Name="Print" DependsOnTargets="CopyMetadata">
<ItemGroup>
<B Include="#(ExpandedAMetadata->'%(Expanded)')" />
</ItemGroup>
<!--Use commas to illustrate that "files" has been expanded-->
<Message Text="A: %(A.files)" />
<Message Text="ExpandedA: #(ExpandedA, ',')" />
<Message Text="ExpandedAMetadata: #(ExpandedAMetadata, ',')" />
<Message Text="B: #(B->'%(Identity) x:%(x) y:%(y) z:%(z)', ',')" />
</Target>
</Project>
and the output of the "Print" target:
Print:
A: test.htm;test_sol1.htm
A: test.proj
ExpandedA: test.htm,test_sol1.htm,test.proj
ExpandedAMetadata: A1,A1,A2
B: test.htm x:a1 y:b1 z:c1,test_sol1.htm x:a1 y:b1 z:c1,test.proj x:a2 y:b2 z:c2
ExpandedA is similar to B in your original question; it is the expanded version of A but without any metadata. Then I run the CopyMetadata target once for each item in ExpandedA (thanks to item batching). Each run, the original A item is copied to the ExpandedAMetadata item group along with all of its metadata. The Original metadata is used to ensure that the correct A item is associated with each file. Finally, in the Print target, B is constructed using an item transformation, so that all the metadata from ExpandedAMetadata is copied over as well.
I'm not sure if I get what you need, but you can copy all A metedata to B as in the answer for related question:
<B Include="#(A)">
It should copy all metedata from A to B.
Here is a direct answer to the original question:
<!-- Copy all metadata from an input item to an output item -->
<UsingTask
TaskName="CopyMetadataTask"
TaskFactory="CodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll" >
<ParameterGroup>
<InputItem ParameterType="Microsoft.Build.Framework.ITaskItem" Required="true" />
<OutputItem ParameterType="Microsoft.Build.Framework.ITaskItem" Required="true" />
</ParameterGroup>
<Task>
<Using Namespace="System"/>
<Code Type="Fragment" Language="cs">
<![CDATA[
InputItem.CopyMetadataTo(OutputItem);
]]>
</Code>
</Task>
</UsingTask>
You can use this task to copy metadata between items the way it was originally intended, and it can easily be adapted to multiple items on input/output, though doing so does induce issues whereby it is necessary to match the inputs and outputs 1/1, or make other compromises, though it can easily be adapted to the exact situation since you can modify the C# code however is necessary. E.g.:
<UsingTask
TaskName="CopyMetadataTask"
TaskFactory="CodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll" >
<ParameterGroup>
<InputItem ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
<OutputItem ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
</ParameterGroup>
<Task>
<Using Namespace="System"/>
<Code Type="Fragment" Language="cs">
<![CDATA[
for (int i = 0; i < InputItem.Length; ++i)
{
InputItem[i].CopyMetadataTo(OutputItem[i]);
}
]]>
</Code>
</Task>
</UsingTask>
The task can then be called to copy metadata directly, for example (using the second version):
<ItemGroup>
<ItemListA Include="a;b;c" Metadata="MetadataValue" />
<ItemListB Include="d;e;f" OtherMetadata="OtherValue" />
</ItemGroup>
<CopyMetadataTask InputItem="#(ItemListA)" OutputItem="#(ItemListB)" />