|
1 | 1 | using System;
|
2 | 2 | using System.Linq;
|
3 | 3 | using System.Linq.Expressions;
|
4 |
| -using System.Reflection; |
5 | 4 | using System.Text.RegularExpressions;
|
6 | 5 |
|
7 | 6 | namespace GitVersion.Helpers
|
8 | 7 | {
|
9 | 8 | internal static class StringFormatWithExtension
|
10 | 9 | {
|
11 |
| - private static readonly Regex TokensRegex = new Regex(@"{(?<env>env:)??\w+(\s+(\?\?)??\s+\w+)??}", RegexOptions.Compiled); |
| 10 | + // This regex matches an expression to replace. |
| 11 | + // - env:ENV name OR a member name |
| 12 | + // - optional fallback value after " ?? " |
| 13 | + // - the fallback value should be a quoted string, but simple unquoted text is allowed for back compat |
| 14 | + private static readonly Regex TokensRegex = new Regex(@"{((env:(?<envvar>\w+))|(?<member>\w+))(\s+(\?\?)??\s+((?<fallback>\w+)|""(?<fallback>.*)""))??}", RegexOptions.Compiled); |
12 | 15 |
|
13 | 16 | /// <summary>
|
14 |
| - /// Formats a string template with the given source object. |
15 |
| - /// Expression like {Id} are replaced with the corresponding |
16 |
| - /// property value in the <paramref name="source" />. |
17 |
| - /// Supports property access expressions. |
18 |
| - /// </summary> |
19 |
| - /// <param name="template" this="true">The template to be replaced with values from the source object. The template can contain expressions wrapped in curly braces, that point to properties or fields on the source object to be used as a substitute, e.g '{Foo.Bar.CurrencySymbol} foo {Foo.Bar.Price}'.</param> |
20 |
| - /// <param name="source">The source object to apply to format</param> |
| 17 | + /// Formats the <paramref name="template"/>, replacing each expression wrapped in curly braces |
| 18 | + /// with the corresponding property from the <paramref name="source"/> or <paramref name="environment"/>. |
| 19 | + /// </summary> |
| 20 | + /// <param name="template" this="true">The source template, which may contain expressions to be replaced, e.g '{Foo.Bar.CurrencySymbol} foo {Foo.Bar.Price}'</param> |
| 21 | + /// <param name="source">The source object to apply to the <paramref name="template"/></param> |
21 | 22 | /// <param name="environment"></param>
|
| 23 | + /// <exception cref="ArgumentNullException">The <paramref name="template"/> is null.</exception> |
| 24 | + /// <exception cref="ArgumentException">An environment variable was null and no fallback was provided.</exception> |
| 25 | + /// <remarks> |
| 26 | + /// An expression containing "." is treated as a property or field access on the <paramref name="source"/>. |
| 27 | + /// An expression starting with "env:" is replaced with the value of the corresponding variable from the <paramref name="environment"/>. |
| 28 | + /// Each expression may specify a single hardcoded fallback value using the {Prop ?? "fallback"} syntax, which applies if the expression evaluates to null. |
| 29 | + /// </remarks> |
| 30 | + /// <example> |
| 31 | + /// // replace an expression with a property value |
| 32 | + /// "Hello {Name}".FormatWith(new { Name = "Fred" }, env); |
| 33 | + /// "Hello {Name ?? \"Fred\"}".FormatWith(new { Name = GetNameOrNull() }, env); |
| 34 | + /// // replace an expression with an environment variable |
| 35 | + /// "{env:BUILD_NUMBER}".FormatWith(new { }, env); |
| 36 | + /// "{env:BUILD_NUMBER ?? \"0\"}".FormatWith(new { }, env); |
| 37 | + /// </example> |
22 | 38 | public static string FormatWith<T>(this string template, T source, IEnvironment environment)
|
23 | 39 | {
|
24 | 40 | if (template == null)
|
25 | 41 | {
|
26 | 42 | throw new ArgumentNullException(nameof(template));
|
27 | 43 | }
|
28 | 44 |
|
29 |
| - // {MajorMinorPatch}+{Branch} |
30 |
| - var objType = source.GetType(); |
31 | 45 | foreach (Match match in TokensRegex.Matches(template))
|
32 | 46 | {
|
33 |
| - var memberAccessExpression = TrimBraces(match.Value); |
34 | 47 | string propertyValue;
|
| 48 | + string fallback = match.Groups["fallback"].Success ? match.Groups["fallback"].Value : null; |
35 | 49 |
|
36 |
| - // Support evaluation of environment variables in the format string |
37 |
| - // For example: {env:JENKINS_BUILD_NUMBER ?? fall-back-string} |
38 |
| - |
39 |
| - if (match.Groups["env"].Success) |
| 50 | + if (match.Groups["envvar"].Success) |
40 | 51 | {
|
41 |
| - memberAccessExpression = memberAccessExpression.Substring(memberAccessExpression.IndexOf(':') + 1); |
42 |
| - string envVar = memberAccessExpression, fallback = null; |
43 |
| - var components = (memberAccessExpression.Contains("??")) ? memberAccessExpression.Split(new[] { "??" }, StringSplitOptions.None) : null; |
44 |
| - if (components != null) |
45 |
| - { |
46 |
| - envVar = components[0].Trim(); |
47 |
| - fallback = components[1].Trim(); |
48 |
| - } |
49 |
| - |
50 |
| - propertyValue = environment.GetEnvironmentVariable(envVar); |
51 |
| - if (propertyValue == null) |
52 |
| - { |
53 |
| - if (fallback != null) |
54 |
| - propertyValue = fallback; |
55 |
| - else |
56 |
| - throw new ArgumentException($"Environment variable {envVar} not found and no fallback string provided"); |
57 |
| - } |
| 52 | + string envVar = match.Groups["envvar"].Value; |
| 53 | + propertyValue = environment.GetEnvironmentVariable(envVar) ?? fallback |
| 54 | + ?? throw new ArgumentException($"Environment variable {envVar} not found and no fallback string provided"); |
58 | 55 | }
|
59 | 56 | else
|
60 | 57 | {
|
| 58 | + var objType = source.GetType(); |
| 59 | + string memberAccessExpression = match.Groups["member"].Value; |
61 | 60 | var expression = CompileDataBinder(objType, memberAccessExpression);
|
62 |
| - propertyValue = expression(source); |
| 61 | + // It would be better to throw if the expression and fallback produce null, but provide an empty string for back compat. |
| 62 | + propertyValue = expression(source)?.ToString() ?? fallback ?? ""; |
63 | 63 | }
|
| 64 | + |
64 | 65 | template = template.Replace(match.Value, propertyValue);
|
65 | 66 | }
|
66 | 67 |
|
67 | 68 | return template;
|
68 | 69 | }
|
69 | 70 |
|
70 |
| - |
71 |
| - private static string TrimBraces(string originalExpression) |
72 |
| - { |
73 |
| - if (!string.IsNullOrWhiteSpace(originalExpression)) |
74 |
| - { |
75 |
| - return originalExpression.TrimStart('{').TrimEnd('}'); |
76 |
| - } |
77 |
| - return originalExpression; |
78 |
| - } |
79 |
| - |
80 |
| - private static Func<object, string> CompileDataBinder(Type type, string expr) |
| 71 | + private static Func<object, object> CompileDataBinder(Type type, string expr) |
81 | 72 | {
|
82 |
| - var param = Expression.Parameter(typeof(object)); |
| 73 | + ParameterExpression param = Expression.Parameter(typeof(object)); |
83 | 74 | Expression body = Expression.Convert(param, type);
|
84 |
| - var members = expr.Split('.'); |
85 |
| - body = members.Aggregate(body, Expression.PropertyOrField); |
86 |
| - |
87 |
| - var staticOrPublic = BindingFlags.Static | BindingFlags.Public; |
88 |
| - var method = GetMethodInfo("ToString", staticOrPublic, new[] { body.Type }); |
89 |
| - if (method == null) |
90 |
| - { |
91 |
| - method = GetMethodInfo("ToString", staticOrPublic, new[] { typeof(object) }); |
92 |
| - body = Expression.Call(method, Expression.Convert(body, typeof(object))); |
93 |
| - } |
94 |
| - else |
95 |
| - { |
96 |
| - body = Expression.Call(method, body); |
97 |
| - } |
98 |
| - |
99 |
| - return Expression.Lambda<Func<object, string>>(body, param).Compile(); |
100 |
| - } |
101 |
| - |
102 |
| - private static MethodInfo GetMethodInfo(string name, BindingFlags bindingFlags, Type[] types) |
103 |
| - { |
104 |
| - var methodInfo = typeof(Convert).GetMethod(name, bindingFlags, null, types, null); |
105 |
| - return methodInfo; |
| 75 | + body = expr.Split('.').Aggregate(body, Expression.PropertyOrField); |
| 76 | + body = Expression.Convert(body, typeof(object)); // Convert result in case the body produces a Nullable value type. |
| 77 | + return Expression.Lambda<Func<object, object>>(body, param).Compile(); |
106 | 78 | }
|
107 | 79 | }
|
108 | 80 | }
|
0 commit comments