Msbuild: Globbing should be extended to support basic pattern matching.

Created on 21 Jul 2017  路  14Comments  路  Source: dotnet/msbuild

It is often the case that you want to nest certain files matching a given pattern under another file that matches a corresponding pattern.

One such case is nesting *.Designer.cs under the corresponding *.resx.

It would be useful if globbing support could be extended to include basic pattern matching such that the following is possible:

<Compile Update"*.Designer.cs" DependentUpon="*.resx" />

I imagine that enabling this functionality could be done via an explicit property (<Compile Update="" DependentUpon="" UsePatternMatching="true" />) or via some specialized syntax (regex supports this via one of the 'capturing' grouping constructs: https://docs.microsoft.com/en-us/dotnet/standard/base-types/grouping-constructs-in-regular-expressions#named_matched_subexpression).

Most helpful comment

@Denis535 If a relative instead of absolute path is specified, the project directory is the basis of the path. So your Update="**" means "everything under this folder", not "absolutely everything".

All 14 comments

FYI. @srivatsn

I don't understand what the expected output is from your proposed syntax. Can you give an example?

@rainersigwald, if I declared <Compile Update"*.Designer.cs" DependentUpon="*.resx" /> (or w/e syntax was decided upon) and I had a Resources.resx/Resources.Designer.cs and a InternalResources.resx/InternalResources.Designer.cs file, I would expect it to have the same behavior as manually declaring:

<Compile Update="Resources.Designer.cs" DependentUpon="Resources.resx" />
<Compile Update="InternalResources.Designer.cs" DependentUpon="InternalResources.resx" />

It allows users to apply metadata (or other properties) to items based on matching patterns.

This also applies to some other patterns such as *.xaml and *.designer.cs, *.css and *.min.css, *.js and *.min.js, Strings.xlf and Strings.*.xlf, etc....

Ah, so you want regex backreferences? Or I guess this is more directly capture groups.

Yes, pretty much 馃槃

This works:

<Compile Update="**\*.Designer.cs" DependentUpon="$([System.IO.Path]::ChangeExtension($([System.IO.Path]::GetFileNameWithoutExtension(%(Identity))), '.resx'))" />

It would still be useful if we had some dedicated syntax for this (IMO). But I am glad to know there is an easy workaround.

@rainersigwald, is there existing syntax for removing metadata from an item (I don't think there is today)? I think that would be the other thing that would block the SDK or Project System from providing defaults for some of these (although it is nothing a conditioned ItemGroup can't workaround for now 馃槃).

@tannergooding MSBuild doesn't distinguish between "set to empty" and "absent" for metadata so you can just set it to empty.

<Project>
 <ItemGroup>
  <Compile Include="foo" />
  <Compile Include="bar" />

  <Compile Update="@(Compile)" Metadatum="value" />
  <Compile Update="foo" Metadatum="" />
 </ItemGroup>

 <Target Name="Build">
  <Warning Text="Compile: @(Compile->'%(Identity) m: %(Metadatum)')" />
 </Target>
</Project>

```
C:\Users\raines\Source\resx>msbuild /v:q
Microsoft (R) Build Engine version 15.3.407.29267 for .NET Framework
Copyright (C) Microsoft Corporation. All rights reserved.

C:\Users\raines\Source\resx\test.proj(11,3): warning : Compile: foo m: ;bar m: value

I tried to apply the workaround above to Roslyn, and ran into a problem. Here's a short snippet of what I want to add to our equivalent of Directory.build.targets:

<ItemGroup>
  <!-- Associate [name].Designer.cs with [name].resx -->
  <Compile Update="**\*.Designer.cs"
           Condition="'%(DependentUpon)' == '' AND Exists('$(RecursiveDir)$([System.IO.Path]::ChangeExtension($([System.IO.Path]::GetFileNameWithoutExtension(%(Identity))), `.resx`))')"
           DependentUpon="$([System.IO.Path]::ChangeExtension($([System.IO.Path]::GetFileNameWithoutExtension(%(Identity))), '.resx'))">
    <AutoGen>True</AutoGen>
    <DesignTime>True</DesignTime>
  </Compile>
</ItemGroup>

The attempt ended up failing with the following error:

The reference to custom metadata "DependentUpon" at position 1 is not allowed in this condition "'%(DependentUpon)' == '' AND Exists('$(RecursiveDir)$([System.IO.Path]::ChangeExtension($([System.IO.Path]::GetFileNameWithoutExtension(%(Identity))), `.resx`))')".

When I removed the first condition (DependentUpon not already set), the error message changed to the following:

The reference to the built-in metadata "Identity" at position 107 is not allowed in this condition "Exists('$(RecursiveDir)$([System.IO.Path]::ChangeExtension($([System.IO.Path]::GetFileNameWithoutExtension(%(Identity))), `.resx`))')".

The latter error will make it difficult to deploy the known workaround because several different input files produce a generated file matching **\*.Designer.cs:

  • .resx
  • .settings
  • .myapp

Doesn't this work?

<Compile Update="**\*.Designer.cs" DependentUpon="%(RootDir)%(Directory)%(FileName).resx" />

@cdmihai no, because %(FileName) would be Resources.Designer instead of just Resources

@sharwell I'm not sure why MSBuild doesn't let you access metadata in item conditions, but you can work around it by moving the condition to the metadata:

<ItemGroup>
  <!-- Associate [name].Designer.cs with [name].resx -->
  <Compile Update="**\*.Designer.cs">
    <DependentUpon Condition="'%(DependentUpon)' == '' AND Exists('$(RecursiveDir)$([System.IO.Path]::ChangeExtension($([System.IO.Path]::GetFileNameWithoutExtension(%(Identity))), `.resx`))')"
          >$([System.IO.Path]::ChangeExtension($([System.IO.Path]::GetFileNameWithoutExtension(%(Identity))), '.resx'))</DependentUpon>
  </Compile>
</ItemGroup>

If you need to also set the AutoGen and DesignTime metadata (which based on the discussion today, it seems like you might not), then you would need to duplicate the condition, or maybe set AutoGen to true based on the condition and then set the other metadata based on whether AutoGen is true.

I'm facing a strange problem.
For example I have the next items:

<ItemGroup>
    <Item1 Include="C:\Users\Den\.nuget\packages\newtonsoft.json\12.0.3\lib\netstandard2.0\Newtonsoft.Json.dll" />
    <Item1 Include="C:/Folder1/Folder2/Lib1.dll" />
    <Item1 Include="C:/Folder1/Folder2/Lib2.dll" />
    <Item1 Include="C:/Folder1/Folder2/Lib3.dll" />
</ItemGroup>

<ItemGroup>
    <Item1 Update="**" MyMetadata="true" /> // It doesn't work
    <Item1 Update="C:\**\Newtonsoft.Json.dll" MyMetadata="true" />
    <Item1 Update="C:\**" MyMetadata="true" />
</ItemGroup>

"Update" works only when the root directory is specified. Is it by design or not?

@Denis535 If a relative instead of absolute path is specified, the project directory is the basis of the path. So your Update="**" means "everything under this folder", not "absolutely everything".

Was this page helpful?
0 / 5 - 0 ratings