// 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);
}
}
}
}