// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; namespace UnrealBuildTool { /// /// Exception when parsing ini files /// public class IniParsingException : Exception { public IniParsingException(string Message) : base(Message) {} public IniParsingException(string Format, params object[] Args) : base(String.Format(Format, Args)) {} } /// /// Equivalent of FConfigCacheIni. Parses ini files. /// public class ConfigCacheIni { /// /// List of values (or a single value) /// public class IniValues : List { public override string ToString() { return String.Join(",", ToArray()); } } /// /// Ini section (map of keys and values) /// public class IniSection : Dictionary { public IniSection() : base(StringComparer.InvariantCultureIgnoreCase) {} public override string ToString() { return "IniSection"; } } /// /// All sections parsed from ini file /// Dictionary Sections; /// /// Constructor. Parses ini hierarchy for the specified project. No Platform settings. /// /// Project path /// Target platform /// Ini name (Engine, Editor, etc) public ConfigCacheIni(string BaseIniName, string ProjectDirectory, string EngineDirectory = null) { Init(UnrealTargetPlatform.Unknown, BaseIniName, ProjectDirectory, EngineDirectory); } /// /// Constructor. Parses ini hierarchy for the specified platform and project. /// /// Project path /// Target platform /// Ini name (Engine, Editor, etc) public ConfigCacheIni(UnrealTargetPlatform Platform, string BaseIniName, string ProjectDirectory, string EngineDirectory = null) { Init(Platform, BaseIniName, ProjectDirectory, EngineDirectory); } private void Init(UnrealTargetPlatform Platform, string BaseIniName, string ProjectDirectory, string EngineDirectory) { Sections = new Dictionary(StringComparer.InvariantCultureIgnoreCase); if (String.IsNullOrEmpty(EngineDirectory)) { EngineDirectory = BuildConfiguration.RelativeEnginePath; } foreach (var IniFileName in EnumerateCrossPlatformIniFileNames(ProjectDirectory, EngineDirectory, Platform, BaseIniName)) { if (File.Exists(IniFileName)) { ParseIniFile(IniFileName); } } } /// /// Finds a section in INI /// /// /// Found section or null public IniSection FindSection(string SectionName) { IniSection Section; Sections.TryGetValue(SectionName, out Section); return Section; } /// /// Finds values associated with the specified key (does not copy the list) /// private bool GetList(string SectionName, string Key, out IniValues Value) { bool Result = false; var Section = FindSection(SectionName); Value = null; if (Section != null) { if (Section.TryGetValue(Key, out Value)) { Result = true; } } return Result; } /// /// Gets all values associated with the specified key /// /// Section where the key is located /// Key name /// Copy of the list containing all values associated with the specified key /// True if the key exists public bool GetArray(string SectionName, string Key, out List Value) { Value = null; IniValues ValueList; bool Result = GetList(SectionName, Key, out ValueList); if (Result) { Value = new List(ValueList); } return Result; } /// /// Gets a single string value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetString(string SectionName, string Key, out string Value) { Value = String.Empty; IniValues ValueList; bool Result = GetList(SectionName, Key, out ValueList); if (Result && ValueList != null && ValueList.Count > 0) { Value = ValueList[0]; Result = true; } else { Result = false; } return Result; } /// /// Gets a single bool value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetBool(string SectionName, string Key, out bool Value) { Value = false; string TextValue; bool Result = GetString(SectionName, Key, out TextValue); if (Result) { // C# Boolean type expects "False" or "True" but since we're not case sensitive, we need to suppor that manually if (String.Compare(TextValue, "true", true) == 0 || String.Compare(TextValue, "1") == 0) { Value = true; } else if (String.Compare(TextValue, "false", true) == 0 || String.Compare(TextValue, "0") == 0) { Value = false; } else { // Failed to parse Result = false; } } return Result; } /// /// Gets a single Int32 value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetInt32(string SectionName, string Key, out int Value) { Value = 0; string TextValue; bool Result = GetString(SectionName, Key, out TextValue); if (Result) { Result = Int32.TryParse(TextValue, out Value); } return Result; } /// /// Gets a single float value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetSingle(string SectionName, string Key, out float Value) { Value = 0.0f; string TextValue; bool Result = GetString(SectionName, Key, out TextValue); if (Result) { Result = Single.TryParse(TextValue, out Value); } return Result; } /// /// Gets a single double value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetDouble(string SectionName, string Key, out double Value) { Value = 0.0; string TextValue; bool Result = GetString(SectionName, Key, out TextValue); if (Result) { Result = Double.TryParse(TextValue, out Value); } return Result; } private static bool ExtractPath(string Source, out string Path) { int start = Source.IndexOf('"'); int end = Source.LastIndexOf('"'); if(start != 1 && end != -1 && start < end) { ++start; Path = Source.Substring(start, end - start); return true; } else { Path = ""; } return false; } public bool GetPath(string SectionName, string Key, out string Value) { string temp; if(GetString(SectionName, Key, out temp)) { return ExtractPath(temp, out Value); } else { Value = ""; } return false; } /// /// List of actions that can be performed on a single line from ini file /// enum ParseAction { None, New, Add, Remove } /// /// Checks what action should be performed on a single line from ini file /// private ParseAction GetActionForLine(ref string Line) { if (String.IsNullOrEmpty(Line) || Line.StartsWith(";") || Line.StartsWith("//")) { return ParseAction.None; } else if (Line.StartsWith("-")) { Line = Line.Substring(1).TrimStart(); return ParseAction.Remove; } else if (Line.StartsWith("+")) { Line = Line.Substring(1).TrimStart(); return ParseAction.Add; } else { return ParseAction.New; } } /// /// Loads and parses ini file. /// public void ParseIniFile(string Filename) { String[] IniLines = null; try { IniLines = File.ReadAllLines(Filename); } catch (Exception ex) { Console.WriteLine("Error reading ini file: " + Filename + " Exception: " + ex.Message); } if (IniLines != null) { IniSection CurrentSection = null; // Line Index for exceptions var LineIndex = 1; var bMultiLine = false; var SingleValue = ""; var Key = ""; var LastAction = ParseAction.None; // Parse each line foreach (var Line in IniLines) { var TrimmedLine = Line.Trim(); // Multiline value support bool bWasMultiLine = bMultiLine; bMultiLine = TrimmedLine.EndsWith("\\"); if (bMultiLine) { TrimmedLine = TrimmedLine.Substring(0, TrimmedLine.Length - 1).TrimEnd(); } if (!bWasMultiLine) { if (TrimmedLine.StartsWith("[")) { CurrentSection = FindOrAddSection(TrimmedLine, Filename, LineIndex); LastAction = ParseAction.None; } else { if (LastAction != ParseAction.None) { throw new IniParsingException("Parsing new key/value pair when the previous one has not yet been processed ({0}, {1}) in {2}, line {3}: {4}", Key, SingleValue, Filename, LineIndex, TrimmedLine); } // Check if the line is empty or a comment, also remove any +/- markers LastAction = GetActionForLine(ref TrimmedLine); if (LastAction != ParseAction.None) { if (CurrentSection == null) { throw new IniParsingException("Trying to parse key/value pair that doesn't belong to any section in {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine); } ParseKeyValuePair(TrimmedLine, Filename, LineIndex, out Key, out SingleValue); } } } if (bWasMultiLine) { SingleValue += TrimmedLine; } if (!bMultiLine && LastAction != ParseAction.None) { ProcessKeyValuePair(CurrentSection, Key, SingleValue, LastAction); LastAction = ParseAction.None; SingleValue = ""; Key = ""; } LineIndex++; } } } /// /// Splits a line into key and value /// private static void ParseKeyValuePair(string TrimmedLine, string Filename, int LineIndex, out string Key, out string Value) { var AssignIndex = TrimmedLine.IndexOf('='); if (AssignIndex < 0) { throw new IniParsingException("Failed to find value when parsing {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine); } Key = TrimmedLine.Substring(0, AssignIndex).Trim(); if (String.IsNullOrEmpty(Key)) { throw new IniParsingException("Empty key when parsing {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine); } Value = TrimmedLine.Substring(AssignIndex + 1).Trim(); if (Value.StartsWith("\"")) { // Remove quotes var QuoteEnd = Value.LastIndexOf('\"'); if (QuoteEnd == 0) { throw new IniParsingException("Mismatched quotes when parsing {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine); } Value = Value.Substring(1, Value.Length - 2); } } /// /// Processes parsed key/value pair /// private static void ProcessKeyValuePair(IniSection CurrentSection, string Key, string SingleValue, ParseAction Action) { switch (Action) { case ParseAction.New: { // New/replace IniValues Value; if (CurrentSection.TryGetValue(Key, out Value) == false) { Value = new IniValues(); CurrentSection.Add(Key, Value); } Value.Clear(); Value.Add(SingleValue); } break; case ParseAction.Add: { IniValues Value; if (CurrentSection.TryGetValue(Key, out Value) == false) { Value = new IniValues(); CurrentSection.Add(Key, Value); } Value.Add(SingleValue); } break; case ParseAction.Remove: { IniValues Value; if (CurrentSection.TryGetValue(Key, out Value)) { var ExistingIndex = Value.FindIndex(X => (String.Compare(Key, X, true) == 0)); if (ExistingIndex >= 0) { Value.RemoveAt(ExistingIndex); } } } break; } } /// /// Finds an existing section or adds a new one /// private IniSection FindOrAddSection(string TrimmedLine, string Filename, int LineIndex) { var SectionEndIndex = TrimmedLine.IndexOf(']'); if (SectionEndIndex != (TrimmedLine.Length - 1)) { throw new IniParsingException("Mismatched brackets when parsing section name in {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine); } var SectionName = TrimmedLine.Substring(1, TrimmedLine.Length - 2); if (String.IsNullOrEmpty(SectionName)) { throw new IniParsingException("Empty section name when parsing {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine); } IniSection CurrentSection; if (Sections.TryGetValue(SectionName, out CurrentSection) == false) { CurrentSection = new IniSection(); Sections.Add(SectionName, CurrentSection); } return CurrentSection; } /// /// Returns a list of INI filenames for the given project /// private static IEnumerable EnumerateCrossPlatformIniFileNames(string ProjectDirectory, string EngineDirectory, UnrealTargetPlatform Platform, string BaseIniName) { // Engine/Config/Base.ini (included in every ini type, required) yield return Path.Combine(EngineDirectory, "Config", "Base.ini"); // Engine/Config/Base* ini yield return Path.Combine(EngineDirectory, "Config", "Base" + BaseIniName + ".ini"); // Engine/Config/NotForLicensees/Base* ini yield return Path.Combine(EngineDirectory, "Config", "NotForLicensees", "Base" + BaseIniName + ".ini"); // NOTE: 4.7: See comment in GetSourceIniHierarchyFilenames() // Engine/Config/NoRedist/Base* ini // yield return Path.Combine(EngineDirectory, "Config", "NoRedist", "Base" + BaseIniName + ".ini"); if(!String.IsNullOrEmpty(ProjectDirectory)) { // Game/Config/Default* ini yield return Path.Combine(ProjectDirectory, "Config", "Default" + BaseIniName + ".ini"); // Game/Config/NotForLicensees/Default* ini yield return Path.Combine(ProjectDirectory, "Config", "NotForLicensees", "Default" + BaseIniName + ".ini"); // Game/Config/NoRedist/Default* ini yield return Path.Combine(ProjectDirectory, "Config", "NoRedist", "Default" + BaseIniName + ".ini"); } string PlatformName = GetIniPlatformName(Platform); if (Platform != UnrealTargetPlatform.Unknown) { // Engine/Config/Platform/Platform* ini yield return Path.Combine(EngineDirectory, "Config", PlatformName, PlatformName + BaseIniName + ".ini"); if(!String.IsNullOrEmpty(ProjectDirectory)) { // Game/Config/Platform/Platform* ini yield return Path.Combine(ProjectDirectory, "Config", PlatformName, PlatformName + BaseIniName + ".ini"); } } string UserSettingsFolder = Utils.GetUserSettingDirectory(); // Match FPlatformProcess::UserSettingsDir() string PersonalFolder = null; // Match FPlatformProcess::UserDir() if (BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Mac) { PersonalFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Documents"); } else if (Environment.OSVersion.Platform == PlatformID.Unix) { PersonalFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Documents"); } else { PersonalFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal); } // /UE4/EngineConfig/User* ini yield return Path.Combine(UserSettingsFolder, "Unreal Engine", "Engine", "Config", "User" + BaseIniName + ".ini"); // /UE4/EngineConfig/User* ini yield return Path.Combine(PersonalFolder, "Unreal Engine", "Engine", "Config", "User" + BaseIniName + ".ini"); // Game/Config/User* ini if (!String.IsNullOrEmpty(ProjectDirectory)) { yield return Path.Combine(ProjectDirectory, "Config", "User" + BaseIniName + ".ini"); } } /// /// Returns the platform name to use as part of platform-specific config files /// private static string GetIniPlatformName(UnrealTargetPlatform TargetPlatform) { if (TargetPlatform == UnrealTargetPlatform.Win32 || TargetPlatform == UnrealTargetPlatform.Win64) { return "Windows"; } else { return Enum.GetName(typeof(UnrealTargetPlatform), TargetPlatform); } } } }