// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Tools.DotNETCommon;
using UnrealBuildTool;
namespace UnrealBuildTool
{
///
/// Specifies the action to take for a config line, as denoted by its prefix.
///
public enum ConfigLineAction
{
///
/// Assign the value to the key
///
Set,
///
/// Add the value to the key (denoted with +X=Y in config files)
///
Add,
///
/// Remove the key without having to match value (denoted with !X in config files)
///
RemoveKey,
///
/// Remove the matching key and value (denoted with -X=Y in config files)
///
RemoveKeyValue
}
///
/// Contains a pre-parsed raw config line, consisting of action, key and value components.
///
public class ConfigLine
{
///
/// The action to take when merging this key/value pair with an existing value
///
public ConfigLineAction Action;
///
/// Name of the key to modify
///
public string Key;
///
/// Value to assign to the key
///
public string Value;
///
/// Constructor.
///
/// Action to take when merging this key/value pair with an existing value
/// Name of the key to modify
/// Value to assign
public ConfigLine(ConfigLineAction Action, string Key, string Value)
{
this.Action = Action;
this.Key = Key;
this.Value = Value;
}
///
/// Formats this object for the debugger
///
/// The original config line
public override string ToString()
{
string Prefix = (Action == ConfigLineAction.Add)? "+" : (Action == ConfigLineAction.RemoveKey)? "!" : (Action == ConfigLineAction.RemoveKeyValue) ? "-" : "";
return String.Format("{0}{1}={2}", Prefix, Key, Value);
}
}
///
/// Contains the lines which appeared in a config section, with all comments and whitespace removed
///
public class ConfigFileSection
{
///
/// Name of the section
///
public string Name;
///
/// Lines which appeared in the config section
///
public List Lines = new List();
///
/// Construct an empty config section with the given name
///
/// Name of the config section
public ConfigFileSection(string Name)
{
this.Name = Name;
}
///
/// try to get a line using it's name, if the line doesn't exist returns false
///
/// Name of the line you want to get
/// The result of the operation
/// return true if the line is retrieved return false and null OutLine if Name isn't found in this section
public bool TryGetLine(string Name, out ConfigLine OutLine)
{
foreach ( ConfigLine Line in Lines)
{
if (Line.Key.Equals(Name))
{
OutLine = Line;
return true;
}
}
OutLine = null;
return false;
}
}
///
/// Represents a single config file as stored on disk.
///
public class ConfigFile
{
///
/// Maps names to config sections
///
Dictionary Sections = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
///
/// Reads config data from the given file.
///
/// File to read from
/// The default action to take when encountering arrays without a '+' prefix
public ConfigFile(FileReference Location, ConfigLineAction DefaultAction = ConfigLineAction.Set)
{
using (StreamReader Reader = new StreamReader(Location.FullName))
{
ConfigFileSection CurrentSection = null;
while(!Reader.EndOfStream)
{
// Find the first non-whitespace character
string Line = Reader.ReadLine();
for(int StartIdx = 0; StartIdx < Line.Length; StartIdx++)
{
if (Line[StartIdx] != ' ' && Line[StartIdx] != '\t')
{
// Find the last non-whitespace character. If it's an escaped newline, merge the following line with it.
int EndIdx = Line.Length;
for(; EndIdx > StartIdx; EndIdx--)
{
if(Line[EndIdx - 1] == '\\')
{
string NextLine = Reader.ReadLine();
if(NextLine == null)
{
break;
}
Line += NextLine;
EndIdx = Line.Length;
}
if(Line[EndIdx - 1] != ' ' && Line[EndIdx - 1] != '\t')
{
break;
}
}
// Break out if we've got a comment
if(Line[StartIdx] == ';')
{
break;
}
if(Line[StartIdx] == '/' && StartIdx + 1 < Line.Length && Line[StartIdx + 1] == '/')
{
break;
}
// Check if it's the start of a new section
if(Line[StartIdx] == '[')
{
CurrentSection = null;
if(Line[EndIdx - 1] == ']')
{
string SectionName = Line.Substring(StartIdx + 1, EndIdx - StartIdx - 2);
if(!Sections.TryGetValue(SectionName, out CurrentSection))
{
CurrentSection = new ConfigFileSection(SectionName);
Sections.Add(SectionName, CurrentSection);
}
}
break;
}
// Otherwise add it to the current section or add a new one
if(CurrentSection != null)
{
if(!TryAddConfigLine(CurrentSection, Line, StartIdx, EndIdx, DefaultAction))
{
Log.TraceWarning("Couldn't parse '{0}' in {1} of {2}", Line, CurrentSection, Location.FullName);
}
break;
}
// Otherwise just ignore it
break;
}
}
}
}
}
///
/// Reads config data from the given string.
///
/// Single line string of config settings in the format [Section1]:Key1=Value1,[Section2]:Key2=Value2
/// The default action to take when encountering arrays without a '+' prefix
public ConfigFile(string IniText, ConfigLineAction DefaultAction = ConfigLineAction.Set)
{
// Break into individual settings of the form [Section]:Key=Value
string[] SettingLines = IniText.Split(new char[] { ',' });
foreach (string Setting in SettingLines)
{
// Locate and break off the section name
string SectionName = Setting.Remove(Setting.IndexOf(':')).Trim(new char[] { '[', ']' });
if (SectionName.Length > 0)
{
ConfigFileSection CurrentSection = null;
if (!Sections.TryGetValue(SectionName, out CurrentSection))
{
CurrentSection = new ConfigFileSection(SectionName);
Sections.Add(SectionName, CurrentSection);
}
if (CurrentSection != null)
{
string IniKeyValue = Setting.Substring(Setting.IndexOf(':') + 1);
if (!TryAddConfigLine(CurrentSection, IniKeyValue, 0, IniKeyValue.Length, DefaultAction))
{
Log.TraceWarning("Couldn't parse '{0}'", IniKeyValue);
}
}
}
}
}
///
/// Try to parse a key/value pair from the given line, and add it to a config section
///
/// The section to receive the parsed config line
/// Text to parse
/// Index of the first non-whitespace character in this line
/// Index of the last (exclusive) non-whitespace character in this line
/// The default action to take if '+' or '-' is not specified on a config line
/// True if the line was parsed correctly, false otherwise
static bool TryAddConfigLine(ConfigFileSection Section, string Line, int StartIdx, int EndIdx, ConfigLineAction DefaultAction)
{
// Find the '=' character separating key and value
int EqualsIdx = Line.IndexOf('=', StartIdx, EndIdx - StartIdx);
if(EqualsIdx == -1 && Line[StartIdx] != '!')
{
return false;
}
// Keep track of the start of the key name
int KeyStartIdx = StartIdx;
// Remove the +/-/! prefix, if present
ConfigLineAction Action = DefaultAction;
if(Line[KeyStartIdx] == '+' || Line[KeyStartIdx] == '-' || Line[KeyStartIdx] == '!')
{
Action = (Line[KeyStartIdx] == '+')? ConfigLineAction.Add : (Line[KeyStartIdx] == '!') ? ConfigLineAction.RemoveKey : ConfigLineAction.RemoveKeyValue;
KeyStartIdx++;
while(Line[KeyStartIdx] == ' ' || Line[KeyStartIdx] == '\t')
{
KeyStartIdx++;
}
}
// RemoveKey actions do not require a value
if (Action == ConfigLineAction.RemoveKey && EqualsIdx == -1)
{
Section.Lines.Add(new ConfigLine(Action, Line.Substring(KeyStartIdx).Trim(), ""));
return true;
}
// Remove trailing spaces after the name of the key
int KeyEndIdx = EqualsIdx;
for(; KeyEndIdx > KeyStartIdx; KeyEndIdx--)
{
if(Line[KeyEndIdx - 1] != ' ' && Line[KeyEndIdx - 1] != '\t')
{
break;
}
}
// Make sure there's a non-empty key name
if(KeyStartIdx == EqualsIdx)
{
return false;
}
// Skip whitespace between the '=' and the start of the value
int ValueStartIdx = EqualsIdx + 1;
for(; ValueStartIdx < EndIdx; ValueStartIdx++)
{
if(Line[ValueStartIdx] != ' ' && Line[ValueStartIdx] != '\t')
{
break;
}
}
// Strip quotes around the value if present
int ValueEndIdx = EndIdx;
if(ValueEndIdx >= ValueStartIdx + 2 && Line[ValueStartIdx] == '"' && Line[ValueEndIdx - 1] == '"')
{
ValueStartIdx++;
ValueEndIdx--;
}
// Add it to the config section
string Key = Line.Substring(KeyStartIdx, KeyEndIdx - KeyStartIdx);
string Value = Line.Substring(ValueStartIdx, ValueEndIdx - ValueStartIdx);
Section.Lines.Add(new ConfigLine(Action, Key, Value));
return true;
}
///
/// Names of sections in this file
///
public IEnumerable SectionNames
{
get { return Sections.Keys; }
}
///
/// Tries to get a config section by name
///
/// Name of the section to look for
/// On success, the config section that was found
/// True if the section was found, false otherwise
public bool TryGetSection(string SectionName, out ConfigFileSection RawSection)
{
return Sections.TryGetValue(SectionName, out RawSection);
}
///
/// Write the config file out to the given location. Useful for debugging.
///
/// The file to write
public void Write(FileReference Location)
{
using (StreamWriter Writer = new StreamWriter(Location.FullName))
{
foreach (ConfigFileSection Section in Sections.Values)
{
Writer.WriteLine("[{0}]", Section.Name);
foreach (ConfigLine Line in Section.Lines)
{
Writer.WriteLine("{0}", Line.ToString());
}
Writer.WriteLine();
}
}
}
}
}