File: MSBuildPropertyParser.cs
Web Access
Project: ..\..\..\src\Cli\Microsoft.DotNet.Cli.Utils\Microsoft.DotNet.Cli.Utils.csproj (Microsoft.DotNet.Cli.Utils)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
namespace Microsoft.DotNet.Cli.Utils;
 
/// <summary>
/// Parses property key value pairs that have already been forwarded through the PropertiesOption class.
/// Does not parse -p and etc. formats, (this is done by PropertiesOption) but does parse property values separated by =, ;, and using quotes.
/// </summary>
public static class MSBuildPropertyParser
{
    public static IEnumerable<(string key, string value)> ParseProperties(string input)
    {
        var currentPos = 0;
        StringBuilder currentKey = new();
        StringBuilder currentValue = new();
 
        (string key, string value) EmitAndReset()
        {
            var key = currentKey.ToString();
            var value = currentValue.ToString();
            currentKey.Clear();
            currentValue.Clear();
            return (key, value);
        }
 
        char? Peek() => currentPos < input.Length ? input[currentPos] : null;
 
        bool TryConsume(out char? consumed)
        {
            if (input.Length > currentPos)
            {
                consumed = input[currentPos];
                currentPos++;
                return true;
            }
            else
            {
                consumed = null;
                return false;
            }
        }
 
        void ParseKey()
        {
            while (TryConsume(out var c) && c != '=')
            {
                currentKey.Append(c);
            }
        }
 
        void ParseQuotedValue()
        {
            TryConsume(out var leadingQuote); // consume the leading quote, which we know is there
            currentValue.Append(leadingQuote);
            while (TryConsume(out char? c))
            {
                currentValue.Append(c);
                if (c == '"')
                {
                    // we're done
                    return;
                }
                if (c == '\\' && Peek() == '"')
                {
                    // consume the escaped quote
                    TryConsume(out var c2);
                    currentValue.Append(c2);
                }
            }
        }
 
        void ParseUnquotedValue()
        {
            while (TryConsume(out char? c) && c != ';')
            {
                currentValue.Append(c);
            }
            // we're either at the end or 
            if (AtEnd()) return;
            // we're just past a semicolon
            // if semicolon, we need to check if there are any other = in the string (signifying property pairs)
            if (input.IndexOf('=', currentPos) != -1)
            {
                // there are more = in the string, so eject and let a new key/value pair be parsed
                return;
            }
            else
            {
                currentValue.Append(';');
                // there are no more = in the string, so consume the remainder of the string
                while (TryConsume(out char? c))
                {
                    currentValue.Append(c);
                }
            }
        }
 
        void ParseValue()
        {
            if (Peek() == '"')
            {
                ParseQuotedValue();
            }
            else
            {
                ParseUnquotedValue();
            }
        }
 
        (string key, string value) ParseKeyValue()
        {
            ParseKey();
            ParseValue();
            return EmitAndReset();
        }
 
        bool AtEnd() => currentPos == input.Length;
 
        while (!(AtEnd()))
        {
            yield return ParseKeyValue();
            if (Peek() is char c && (c == ';' || c == ','))
            {
                TryConsume(out _); // swallow the next semicolon or comma delimiter
            }
        }
    }
}