| File: src\msbuild\src\Shared\PropertyParser.cs | Web Access |
| Project: src\msbuild\src\Build\Microsoft.Build.csproj (Microsoft.Build) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.NET.StringTools; #nullable disable #if BUILD_ENGINE namespace Microsoft.Build.BackEnd #else using Microsoft.Build.Utilities; namespace Microsoft.Build.Tasks #endif { internal static class PropertyParser { /// <summary> /// Given a string of semi-colon delimited name=value pairs, this method parses it and creates /// a hash table containing the property names as keys and the property values as values. /// </summary> /// <returns>true on success, false on failure.</returns> internal static bool GetTable(TaskLoggingHelper log, string parameterName, string[] propertyList, out Dictionary<string, string> propertiesTable) { propertiesTable = null; if (propertyList != null) { propertiesTable = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); // Loop through the array. Each string in the array should be of the form: // MyPropName=MyPropValue foreach (string propertyNameValuePair in propertyList) { string propertyName = String.Empty; string propertyValue = String.Empty; // Find the first '=' sign in the string. int indexOfEqualsSign = propertyNameValuePair.IndexOf('='); // If we found one, then grab the stuff before it and put it into "propertyName", // and grab the stuff after it and put it into "propertyValue". But trim the // whitespace from beginning and end of both name and value. (When authoring a // project/targets file, people like to use whitespace and newlines to pretty up // the file format.) if (indexOfEqualsSign >= 0) { propertyName = propertyNameValuePair.AsSpan(0, indexOfEqualsSign).Trim().ToString(); propertyValue = propertyNameValuePair.AsSpan(indexOfEqualsSign + 1).Trim().ToString(); } // Make sure we have a property name and property value (though the value is allowed to be blank). if (propertyName.Length == 0) { // No equals sign? No property name? That's no good to us. log?.LogErrorWithCodeFromResources("General.InvalidPropertyError", parameterName, propertyNameValuePair); return false; } // Bag the property and its value. Trim whitespace from beginning and end of // both name and value. (When authoring a project/targets file, people like to // use whitespace and newlines to pretty up the file format.) propertiesTable[propertyName] = propertyValue; } } return true; } /// <summary> /// Given a string of semi-colon delimited name=value pairs, this method parses it and creates /// a hash table containing the property names as keys and the property values as values. /// This method escapes any special characters found in the property values, in case they /// are going to be passed to a method (such as that expects the appropriate escaping to have happened /// already. /// </summary> /// <returns>true on success, false on failure.</returns> internal static bool GetTableWithEscaping(TaskLoggingHelper log, string parameterName, string syntaxName, string[] propertyNameValueStrings, out Dictionary<string, string> finalPropertiesTable) { finalPropertiesTable = null; if (propertyNameValueStrings != null) { finalPropertiesTable = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var finalPropertiesList = new List<PropertyNameValuePair>(); // Loop through the array. Each string in the array should be of the form: // MyPropName=MyPropValue foreach (string propertyNameValueString in propertyNameValueStrings) { // Find the first '=' sign in the string. int indexOfEqualsSign = propertyNameValueString.IndexOf('='); if (indexOfEqualsSign != -1) { // If we found one, then grab the stuff before it and put it into "propertyName", // and grab the stuff after it and put it into "propertyValue". But trim the // whitespace from beginning and end of both name and value. (When authoring a // project/targets file, people like to use whitespace and newlines to pretty up // the file format.) string propertyName = propertyNameValueString.AsSpan(0, indexOfEqualsSign).Trim().ToString(); string propertyValue = EscapingUtilities.Escape(propertyNameValueString.AsSpan(indexOfEqualsSign + 1).Trim().ToString()); // Make sure we have a property name and property value (though the value is allowed to be blank). if (propertyName.Length == 0) { // No property name? That's no good to us. log?.LogErrorWithCodeFromResources("General.InvalidPropertyError", syntaxName, propertyNameValueString); return false; } // Store the property in our list. finalPropertiesList.Add(new PropertyNameValuePair(propertyName, propertyValue)); } else { // There's no '=' sign in the string. When this happens, we treat this string as basically // an appendage on the value of the previous property. For example, if the project file contains // // <PropertyGroup> // <WarningsAsErrors>1234;5678;9999</WarningsAsErrors> // </PropertyGroup> // <Target Name="Build"> // <MSBuild Projects="ConsoleApplication1.csproj" // Properties="WarningsAsErrors=$(WarningsAsErrors)"/> // </Target> // // , then this method (GetTableWithEscaping) will see this: // // propertyNameValueStrings[0] = "WarningsAsErrors=1234" // propertyNameValueStrings[1] = "5678" // propertyNameValueStrings[2] = "9999" // // And what we actually want to end up with in our final hashtable is this: // // NAME VALUE // =================== ================================ // WarningsAsErrors 1234;5678;9999 // if (finalPropertiesList.Count > 0) { // There was a property definition previous to this one. Append the current string // to that previous value, using semicolon as a separator. string propertyValue = EscapingUtilities.Escape(propertyNameValueString.Trim()); finalPropertiesList[finalPropertiesList.Count - 1].Value.Add(propertyValue); } else { // No equals sign in the very first property? That's a problem. log?.LogErrorWithCodeFromResources("General.InvalidPropertyError", syntaxName, propertyNameValueString); return false; } } } // Convert the data in the List to a Hashtable, because that's what the MSBuild task eventually // needs to pass onto the engine. log?.LogMessageFromText(parameterName, MessageImportance.Low); using SpanBasedStringBuilder stringBuilder = Strings.GetSpanBasedStringBuilder(); foreach (PropertyNameValuePair propertyNameValuePair in finalPropertiesList) { stringBuilder.Clear(); bool needsSemicolon = false; foreach (string valueFragment in propertyNameValuePair.Value) { if (needsSemicolon) { stringBuilder.Append(";"); } needsSemicolon = true; stringBuilder.Append(valueFragment); } string propertyValue = stringBuilder.ToString(); finalPropertiesTable[propertyNameValuePair.Name] = propertyValue; log?.LogMessageFromText( $" {propertyNameValuePair.Name}={propertyValue}", MessageImportance.Low); } } return true; } /// <summary> /// A very simple class that holds two strings, a property name and property value. /// </summary> private class PropertyNameValuePair { /// <summary> /// Property name /// </summary> internal string Name { get; } /// <summary> /// Property value fragments. Join with semicolon to get the final value. /// </summary> internal List<string> Value { get; } internal PropertyNameValuePair(string propertyName, string propertyValue) { Name = propertyName; Value = new List<string> { propertyValue }; } } } }