Given the following MSBuild project file:
<Project ToolsVersion="3.5" DefaultTargets="DoA" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<A Include="1.txt">
<Define>B=2;C=3</Define>
</A>
<A Include="2.txt" />
</ItemGroup>
<Target Name="DoA" Inputs="#(A)" Outputs="out\%(A.Filename).csv">
<Message Text="perl myscript.pl #(A) ???" />
</Target>
</Project>
What do I need to substitute for the ??? to have the text output be:
perl myscript.pl 1.txt --define B=2 --define C=3
perl myscript.pl 2.txt
?
You can use %(A.Define) but you'd have to change your Define property to:
<Define>--define B=2 --define C=3</Define>
I don't believe it's possible to treat item metadata as an item itself, though it does seem useful in this case.
Related
Say I have a property like:
<MyProp>Foo=Bar;Hello=World</MyProp>
This seems like a reasonably common property pattern in MSBuild. How would I go about fetching the value "World"? In an ideal world this might look something like:
$(MyProp).(Hello)
Edit: To be clear, the property is not of my own creation, it is the output from another target that is out of my control, so I cannot change the way the property is declared.
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MyProp>Foo=Bar;Hello=World</MyProp>
</PropertyGroup>
<Target Name="Foo">
<CreateItem Include="MyProp" AdditionalMetadata="$(MyProp)">
<Output TaskParameter="Include" ItemName="MyProp" />
</CreateItem>
<Message Text="Foo %(MyProp.Foo)" />
<Message Text="Hello %(MyProp.Hello)" />
</Target>
</Project>
You have two routes to follow as far as I am concerned
Declare a Property Group just like the following:
<PropertyGroup>
<Foo>Bar</Foo>
<Hello>World</Hello>
</PropertyGroup>
and then use the following method to access your properties
<Target Name="DoSomething">
<Message Text="Print this : $(Foo)" />
</Target>
or you might want to take ItemGroup Element approach like the following
<ItemGroup>
<MySolutionFiles Include="..\mySolution.sln" />
</ItemGroup>
<Target Name="PrintItems">
<Message Text="My Files: #(MySolutionFiles)" />
</Target>
You can have the following as well
<ItemGroup>
<MyProp
Include="Foo;Hello" />
</ItemGroup>
<Target Name="PrintMyItems">
<Message Text="MyProp: #(MyProp)" />
</Target>
If there is no choice over the input then one possible solution is parsing the input into an array and then taking it from there like the following:
<PropertyGroup>
<MyProp>Foo=Bar;Hello=World</MyProp>
<Split>$(MyProp.Split(';'))</Split>
</PropertyGroup>
and then play with the array items like the following:
<Target Name="DoPrint">
<Message text="$(Split[0])" />
</Target>
Split[0] item contains your Foo=Bar which can be split into two more strings just like above. This should keep you going for now.
You don't need a property group, it's just nice to have default values in case the user doesn't pass them.
For each property you pass, the syntax to access that property is $(PropertyName).
So if you pass:
msbuild.exe /p:P1=V1 /p:P2=V2;P3=V3
You would use the property name $(P1), $(P2), $(P3).
More on MsBuild properties here.
I am stuck at the following issue I have in MSBuild.
I have a text file (buildsolutions1.txt) containing the list (line by line) with all the solutions I need to build and the related developers emails separated by comma :
Common\Common.sln,am#email,com
ExcGw/ExcDataService.sln,pm#email.com;am#email,com;jk#email.com;mk#email.com;Ppp#email.com
MessB/MessB/Message.sln,am#email,com
RiskS/RiskS2.sln,jp#email.com;mz#email.com;mk#email.com;jk#email.com;ps#email.com
I need to read this file line by line,compile each solution and in case it fails –send email to related developer(s)
My idea is to create an item group Lines where every item is a line from this file and it has 2 metadata values:
Solution –first part of the line until comma
Emails –second part of the line from comma to the end of line
So I created a Property Grroup and a target ReadSolutions like below.
I read the file line by line but I do not know how to set the metadata for each line item:
FirstPartOfTheCurrentLine(%(LinesFromFile.Identity))
SecondPartOfTheCurrentLine(%(LinesFromFile.Identity))
This syntax does not work:
%(LinesFromFile.Identity.Split(',')[0])
%(LinesFromFile.Identity.Split(',')[1])
Maybe someone would know how to set the metadata correctly or maybe has another approach to this task.
Thanks
Here is the code:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
ToolsVersion="4.0" DefaultTargets="CoreBuild">
<PropertyGroup>
<TPath>$(MSBuildProjectDirectory)\..\tools\MSBuild Extension Pack Binaries\MSBuild.ExtensionPack.tasks</TPath>
</PropertyGroup>
<Import Project="$(TPath)"/>
<PropertyGroup>
<!-- Default working folder -->
<RootFolder Condition=" '$(RootFolder)' == '' " >c:\ff\</RootFolder>
<BuildSolutionsFile >buildsolutions1.txt</BuildSolutionsFile>
<BuildSolutionsFileFullPath >$(RootFolder)$(BuildSolutionsFile)</BuildSolutionsFileFullPath>
</PropertyGroup>
<Target Name="ReadSolutions">
<Message Text=" Build solutions text file is : $(BuildSolutionsFileFullPath)" />
<!—Read the file and store each line as an item into LinesFromFile item group-->
<ReadLinesFromFile
File="$(BuildSolutionsFileFullPath)" >
<Output
TaskParameter="Lines"
ItemName="LinesFromFile"/>
</ReadLinesFromFile>
<Message Text="Current line : %(LinesFromFile.Identity)" />
<Message Text="===================================" />
<!—Create the other item group where each item is a line and has the metadata Solution and Emails -->
<ItemGroup>
<Lines Include="%(LinesFromFile.Identity)" >
<Solution>FirstPartOfTheCurrentLine(%(LinesFromFile.Identity))</Solution>
<Emails>SecondPartOfTheCurrentLine(%(LinesFromFile.Identity)) </Emails>
</Lines>
</ItemGroup>
<Message Text="All the Lines :%0A#(Lines,'%0A')" />
</Target>
Here is the data I was working with:
Common\Common.sln,am#email,com
ExcGw/ExcDataService.sln,pm#email.com;am#email,com;jk#email.com;mk#email.com;Ppp#email.com
MessB/MessB/Message.sln,am#email,com
RiskS/RiskS2.sln,jp#email.com;mz#email.com;mk#email.com;jk#email.com;ps#email.com
Slightly modified your sample to include a line break for the fourth solution.
Here is the modified code:
<ItemGroup>
<Lines Include="#(LinesFromFile)" >
<Solution>$([System.String]::Copy('%(LinesFromFile.Identity)').Split(',')[0])</Solution>
<Emails>$([System.String]::Copy('%(LinesFromFile.Identity)').Split(',')[1])</Emails>
</Lines>
</ItemGroup>
<Message Text="Solutions to Emails-> %(Lines.Solution) -> %(Lines.Emails)" />
We're copying the value to a property so we can use a property function to split the value and get the part we need.
Here is the output:
Solutions to Emails-> Common\Common.sln -> am#email
Solutions to Emails-> ExcGw/ExcDataService.sln -> pm#email.com;am#email
Solutions to Emails-> MessB/MessB/Message.sln -> am#email
Solutions to Emails-> RiskS/RiskS2.sln -> jp#email.com;mz#email.com;mk#email.com;jk#email.com;ps#email.com
I know I can do the following
<PropertyGroup>
<Foo>Bar</Foo>
<Foo1>Bar1</Foo1>
<Foos>$(Foo) $(foo1)</Foos>
</PropertyGroup>
<Target Name="Def">
<Message Text="$(Foos)"/>
</Target>
and get Bar Bar1
But this doesn't scale very well if you have many properties in the PropertyGroup.
Is there any way to reference a PropertyGroup or some other node and have MSBuild do the hard work for you?
I know the PropertyGroup element doesn't support it but imagine being able to do
<PropertyGroup Name="Bob">
<Foo>Bar</Foo>
<Foo1>Bar 1</Foo1>
</PropertyGroup>
<Target Name="Def">
<Message Text="$(Bob)"/>
</Target>
and get Bar Bar 1
This can be achieved using an ItemGroup and #() notation.
Example
<ItemGroup>
<Foo Include="Bar"/>
<Foo Include="Bar1"/>
</ItemGroup>
<Message Text="#(Foo)"/>
prints Bar;Bar1
notice the # symbol joins the items wt a semicolon by default. We can change this with a second paramter to #(..).
<ItemGroup>
<Foo Include="Bar"/>
<Foo Include="Bar1"/>
</ItemGroup>
<Message Text="#(Foo, ' ')"/>
prints Bar Bar1
Not sure if that the right title for the question, but what I'm trying to do is this:
<ItemGroup>
<item1 Include="a;b;c;"/>
<item2 Include="x;y;z;"/>
<itemNames Include="item1;item2"/>
</ItemGroup>
<Target Name="DefaultName">
<Message Text="%(%(itemNames.Identity))"/>
</Target>
I'm expecting output to be:
a;b;c;
x;y;z;
Instead, the output is:
%(item1)
%(item2)
So my guess is that the Text property is parsed only once and the resulting string is not. Any workarounds arround this?
The following will produce the output you are looking for:
<ItemGroup>
<item1 Include="a;b;c;"/>
<item2 Include="x;y;z;"/>
<itemNames Include="item1;item2"/>
</ItemGroup>
<Target Name="DefaultName"
Outputs="%(itemNames.Identity)">
<PropertyGroup>
<ThisItem>%(itemNames.Identity)</ThisItem>
</PropertyGroup>
<ItemGroup>
<ThisItem Include="#($(ThisItem))" />
</ItemGroup>
<Message Text="#(ThisItem)" />
</Target>
...shows the following output...
DefaultName:
a;b;c
DefaultName:
x;y;z
Excerpted from MSBuild Trickery tricks #68 and 69
I'm trying to copy a bunch of files whose names begin with the prefix DR__, but the copies must have that prefix removed. That is, DR__foo must be copied as foo. I'm trying this, which is based in the example provided in the documentation (the .chm):
<Target Name="CopyAuxiliaryFiles">
<MakeDir Directories="$(TargetDir)Parameters" Condition="!Exists('$(TargetDir)Parameters')" />
<ItemGroup>
<ContextVisionParameterFiles Include="$(SolutionDir)CVParameters\DR__*" />
</ItemGroup>
<Message Text="Files to copy and rename: #(ContextVisionParameterFiles)"/>
<RegexReplace Input="#(ContextVisionParametersFiles)" Expression="DR__" Replacement="">
<Output ItemName ="DestinationFullPath" TaskParameter="Output" />
</RegexReplace>
<Message Text="Renamed Files: #(DestinationFullPath)"/>
<Copy SourceFiles="#(ContextVisionParameterFiles)" DestinationFiles="#(DestinationFullPath)" />
</Target>
DestinationFullPath comes out empty (or that's what I see when I display it with Message). Thus, Copy fails because no DestinationFiles are specified. What's wrong here?
Edit: ContextVisionParameterFiles is not empty, it contains this:
D:\SVN.DRA.WorkingCopy\CVParameters\DR__big_bone.alut;D:\SVN.DRA.WorkingCopy\CVParameters\DR__big_medium.gop
They're actually 40 files, but I trimmed it for the sake of clarity
Got it! It seems to have been the combination of a stupid error and a seemingly compulsory parameter. As for the first one, there were two Targets called CopyAuxiliaryFiles. As for the second one, it seems the Count parameter is needed.
The final, working version:
<Target Name="CopyCvParameters">
<ItemGroup>
<CvParamFiles Include="$(SolutionDir)CVParameters\DR__*" />
</ItemGroup>
<Message Text="Input:
#(CvParamFiles, '
')"/>
<!-- Replaces first occurance of "foo." with empty string-->
<RegexReplace Input="#(CvParamFiles)" Expression="^.*DR__" Replacement="$(TargetDir)Parameters\" Count="1">
<Output ItemName ="RenamedCvParamFiles" TaskParameter="Output" />
</RegexReplace>
<Message Text="
Output RenamedCvParamFiles:
#(RenamedCvParamFiles, '
')" />
<Copy SourceFiles="#(CvParamFiles)" DestinationFiles="#(RenamedCvParamFiles)" SkipUnchangedFiles="True" />
</Target>
Notice that:
I renamed the Target to solve the name collision (Why doesn't Visual Studio detect this as an error?)
I pretty-printed the ItemGroups with the #(CvParamFiles, '
') syntax, which seems to replace ; with line breaks
My regex replaces the absolute path and the prefix
Count="1" is now passed to RegexReplace