The official source of product insight from the Visual Studio Engineering Team
Some more information about this 4.0 feature. (I've also updated the first post with this, so everything's in one place for your reference.)
The full list of built-in [MSBuild] functions, like the one above, are in the MSDN topic here. They include arithmetic (useful, for example, for modifying version numbers), functions to convert to and from the MSBuild escaping format (on rare occasions, that is useful). Here's another example
And here's one other property function that will be useful to some people:
Looks in the designated directory, then progressively in the parent directories until it finds the file provided or hits the root. Then it returns the path to that root. What would you need such an odd function for? It's very useful if you have a tree of projects in source control, and want them all to share a single imported file. You can check it in at the root, but how do they find it to import it? They could all specify the relative path, but that's cumbersome as it's different depending on where they are. Or, you could set an environment variable pointing to the root, but you might not want to use environment variables. That's where this function comes in handy – you can write something like this, and all projects will be able to find and import it:
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), EnlistmentInfo.props))\EnlistmentInfo.props" Condition=" '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), EnlistmentInfo.props))' != '' " />
The functions parser is pretty robust but not necessarily that helpful when it doesn't wokr. Errors you can get include
(1) It doesn't evaluate but just comes out as a string. Your syntax isn't recognized as an attempt at a function, most likely you've missed a closing parenthesis somewhere. That's easy to do when there's lots of nesting.
(2) error MSB4184: The expression "…" cannot be evaluated. It treated it as a function, but probably it couldn't parse it.
(3) error MSB4184: The expression "…" cannot be evaluated. Method '…' not found. It could parse it, but not find a member it could coerce to, or it was considered ambiguous by the binder. Verify you weren't calling a static member using instance member syntax. Try to make the call less ambiguous between overloads, either by picking another overload (that perhaps has a unique number of parameters) or using the Convert class to force one of the parameters explicitly to the type the method wants. One common case where this happens is where one overload takes an integer, and the other an enumeration.
(4) error MSB4184: The expression "[System.Text.RegularExpressions.Regex]::Replace(d:\bar\libs;;c:\Foo\libs;, \lib\x86, '')" cannot be evaluated. parsing "\lib\x86" - Unrecognized escape sequence \l. Here's an example where it bound the method, but the method threw an exception ("unrecognized escape sequence") because the parameter values weren't valid.
(5) error MSB4186: Invalid static method invocation syntax: "....". Method 'System.Text.RegularExpressions.Regex.Replace' not found. Static method invocation should be of the form: $([FullTypeName]::Method()), e.g. $([System.IO.Path]::Combine(`a`, `b`)).. Hopefully this is self explanatory, but more often than a syntax mistake, you called an instance member using static member syntax.
Arrays are tricky as the C# style syntax "new Foo" does not work, and Array.CreateInstance needs a Type object. To get an array, you either need a method or property that returns one, or you use a special case where we can force a string into an array. Here's an example of the latter case:
In this case, the string.Split overload wants a string array, and we're converting the string into an array with one element.
Here I'm replacing a string in the property "LibraryPath", case insensitively.
<LibraryPath>$([System.Text.RegularExpressions.Regex]::Replace($(LibraryPath), `$(DXSDK_DIR)\\lib\\x86`, ``, System.Text.RegularExpressions.RegexOptions.IgnoreCase))</LibraryPath>
Here's how to do the same with string manipulation, less pretty.
<LibraryPath>$(LibraryPath.Remove($(LibraryPath.IndexOf(`$(DXSDK_DIR)\lib\x86`, 0, $(IncludePath.Length), System.StringComparison.OrdinalIgnoreCase)), $([MSBuild]::Add($(DXSDK_DIR.Length), 8))))</LibraryPath>
So far in my own work I've found this feature really useful, and far, far, better than creating a task. It can make some simple tasks that were impossible possible, and often, easy. But as you can see from the examples above, it often has rough edges and sometimes it can be horrible to read and write. Here's some ways we can make it better in future:
What do you think?