Select Git revision
-
Martin Mareš authoredMartin Mareš authored
ConfigParser.cs 10.75 KiB
#nullable enable
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Linq;
using Color = System.Drawing.Color;
namespace Config;
abstract class ConfigException: Exception
{
public override string ToString()
{
return $"Mistake in config: {ErrorString()}";
}
abstract public string ErrorString();
}
abstract class ConfigDuplicitException: ConfigException
{
public string Name;
public int LineA, LineB;
public ConfigDuplicitException(string name, int lineA, int lineB)
{
Name = name;
LineA = lineA;
LineB = lineB;
}
}
class ConfigDuplicitSectionException: ConfigDuplicitException
{
public ConfigDuplicitSectionException(string name, int lineA, int lineB)
:base(name, lineA, lineB){}
public override string ErrorString()
{
return $"Duplicit section name {Name} on lines {LineA}, {LineB}";
}
}
class ConfigDuplicitKeyException: ConfigDuplicitException
{
public ConfigDuplicitKeyException(string name, int lineA, int lineB)
:base(name, lineA, lineB){}
public override string ErrorString()
{
return $"Duplicit key name {Name} on lines {LineA}, {LineB}";
}
}
class ConfigNoAmenException: ConfigException
{
public ConfigValue Value;
public ConfigNoAmenException(ConfigValue value)
{
Value = value;
}
public override string ErrorString()
{
return $"End string \"{Value.amen}\" for block {Value.Key} starting at line {Value.Line} not found";
}
}
class ConfigMixedTabException: ConfigException
{
public ConfigValue Value;
public int Line;
public ConfigMixedTabException(ConfigValue value, int line)
{
Value = value;
Line = line;
}
public override string ErrorString()
{
return $"{Line}: {Value.FullName}: Combination of tabs and spaces is not supported.";
}
}
class ConfigWrongIndentException: ConfigException
{
public ConfigValue? Value;
public int Line;
public ConfigWrongIndentException(ConfigValue? value, int line)
{
Value = value;
Line = line;
}
public override string ErrorString()
{
if(Value == null)
return $"{Line}: Wrong indent.";
return $"{Line}: {Value.FullName}: Wrong indent.";
}
}
class ConfigSectionNotDefinedException: ConfigException
{
public string Name;
public ConfigSectionNotDefinedException(string name)
{
Name = name;
}
public override string ErrorString()
{
return $"Section {Name} is not defined.";
}
}
class ConfigNotDefinedException: ConfigException
{
public ConfigSection Section;
public string Name;
public ConfigNotDefinedException(ConfigSection section, string name)
{
Section = section;
Name = name;
}
public override string ErrorString()
{
return $"{Section.SectionName ?? "[global]"}: Attribute {Name} is not defined.";
}
}
class ConfigNoValueException: ConfigException
{
public ConfigValue Value;
public string? Reason;
public ConfigNoValueException(ConfigValue value, string? reason=null)
{
Value = value;
Reason = reason;
}
public override string ErrorString()
{
return $"{Value.Line}: Attribute {Value.FullName} need value." ;
}
}
class ConfigParseAttributeException: ConfigException
{
public ConfigValue Value;
public string? Reason;
public ConfigParseAttributeException(ConfigValue value, string? reason=null)
{
Value = value;
Reason = reason;
}
public override string ErrorString()
{
string name = Value.Section.SectionName != null ? $"{Value.Section.SectionName}: {Value.Key}" : Value.Key;
return $"{Value.Line}: Parsing attribute {Value.FullName} fail" +
(Reason==null?".":$": {Reason}.");
}
}
class ConfigMistake: ConfigException
{
public ConfigValue Value;
public string Text;
public ConfigMistake(ConfigValue value, string text)
{
Value = value;
Text = text;
}
public override string ErrorString()
{
string name = Value.Section.SectionName != null ? $"{Value.Section.SectionName}: {Value.Key}" : Value.Key;
return $"{Value.Line}: {Value.FullName}: {Text}";
}
}
class ConfigValue
{
public ConfigSection Section {get; init;}
internal string? amen;
internal string? tabs;
public string Key {get; init;}
internal string? Value;
public bool HasText() => Value != null;
public int Line;
public string FullName
{
get => Section.SectionName != null ? $"{Section.SectionName}: {Key}" : Key;
}
public string AsString()
{
if(Value == null) throw new ConfigNoValueException(this);
return Value;
}
public string AsPath()
{
string s = AsString();
StringBuilder output = new();
for(int i=0;i<s.Length;i++)
{
if(i == 0 && s[i]=='~')
{
output.Append(Environment.GetEnvironmentVariable("HOME") ?? "");
}
else if(s[i]=='$')
{
int start, end;
if(s[i+1]=='{')
{
start = i+2;
while(i<s.Length && s[i]!='}') i++;
end = i;
}
else
{
start = i+1;
while(i+1<s.Length && "qwertyuiopasdfghjklzxcvbnm1234567890QWERTYUIOPASDFGHJKLZXCVBNM_".Contains(s[i+1])) i++;
end = i+1;
}
output.Append(Environment.GetEnvironmentVariable(s[start..end]) ?? "");
}
else
output.Append(s[i]);
}
return output.ToString();
}
public Color AsColor()
{
try
{
return System.Drawing.ColorTranslator.FromHtml(AsString());
} catch (ArgumentException)
{
throw new ConfigParseAttributeException(this, "String is not color");
}
}
public double AsDouble()
{
try
{
return double.Parse(AsString());
} catch (ArgumentException)
{
throw new ConfigParseAttributeException(this, "String is not color");
}
}
public ConfigParser AsConfig()
{
return new ConfigParser(AsString(), Line);
}
public static implicit operator string(ConfigValue v) => v.AsString();
internal ConfigValue(ConfigSection _Section, string _Key, int _line)
{
Section = _Section;
Key = _Key;
Line = _line;
}
}
class ConfigSection: IReadOnlyList<ConfigValue>
{
public ConfigParser Root {get; init;}
public string? SectionName {get; init;}
public int Line;
List<ConfigValue> values = new();
Dictionary<string, ConfigValue> valueByName = new();
internal ConfigSection(ConfigParser _Root, string? _SectionName, int _line)
{
Root = _Root;
SectionName = _SectionName;
Line = _line;
}
internal void Add(ConfigValue v)
{
if(valueByName.ContainsKey(v.Key))
throw new ConfigDuplicitKeyException(v.Key, v.Line, valueByName[v.Key].Line);
valueByName[v.Key] = v;
values.Add(v);
}
public IEnumerator<ConfigValue> GetEnumerator()
{
foreach(var s in values)
yield return s;
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count{ get{return values.Count;}}
public ConfigValue this[int i]
{
get { return values[i]; }
}
public ConfigValue? this[string key]
{
get { return Optional(key); }
}
public ConfigValue? Optional(string key)
{
if(!valueByName.TryGetValue(key, out var r))
return null;
return r;
}
public ConfigValue Mandatory(string key)
{
if(!valueByName.TryGetValue(key, out var r))
throw new ConfigNotDefinedException(this, key);
return r;
}
public void Print(TextWriter w)
{
w.WriteLine($"### {SectionName}");
foreach(var x in values)
{
w.WriteLine($"'{x.Key}' = '{(x.HasText() ? x.AsString() : "null")}'");
}
}
}
class ConfigParser: IReadOnlyList<ConfigSection>
{
public ConfigSection MainSection;
List<ConfigSection> sections = new();
Dictionary<string, ConfigSection> sectionByName = new();
public ConfigParser(string s, int firstLineIndex = 1)
{
string[] lines = s.Split("\n");
var currentSection = MainSection = new(this, null, 0);
ConfigValue? lastVal = null;
for(int i=0;i<lines.Count();i++)
{
string l = lines[i];
string tl = l.TrimEnd();
//Console.WriteLine($"l: '{l}' sl:'{tl}'");
if(l.Trim() == "") continue;
if(l.Trim()[0] == '#') continue;
if(tl[0] == '[' && tl[^1] == ']' && tl.Length >= 3)
{
#pragma warning disable 8604
lastVal = null;
currentSection = new ConfigSection(this, tl[1..^1], firstLineIndex + i);
if(sectionByName.ContainsKey(currentSection.SectionName))
{
throw new ConfigDuplicitSectionException(currentSection.SectionName, sectionByName[currentSection.SectionName].Line, firstLineIndex + i);
}
sections.Add(currentSection);
sectionByName[currentSection.SectionName] = currentSection;
continue;
#pragma warning restore 8604
}
if(tl[0]==' ' || tl[0]=='\t')
{
if(lastVal == null)
throw new ConfigWrongIndentException(null, firstLineIndex + i);
if(lastVal.tabs == null)
{
int spacesLen=0;
for(;tl[spacesLen]==tl[0];spacesLen++);
if(tl[spacesLen]==' ' || tl[spacesLen]=='\t')
throw new ConfigMixedTabException(lastVal, firstLineIndex + i);
lastVal.tabs = tl[0..spacesLen];
if(lastVal.Value == null || lastVal.Value == "")
{
lastVal.Line = firstLineIndex + i;
lastVal.Value = tl[lastVal.tabs.Length..];
}
else
lastVal.Value += "\n" + tl[lastVal.tabs.Length..];
}
else
{
if(!tl.StartsWith(lastVal.tabs))
throw new ConfigWrongIndentException(lastVal, firstLineIndex + i);
lastVal.Value += "\n" + tl[lastVal.tabs.Length..];
}
continue;
}
int indexIs = l.IndexOf("=");
int indexArrow = l.IndexOf("<<");
if(indexIs == -1 && indexArrow == -1)
{
currentSection.Add(lastVal = new ConfigValue(currentSection, tl, firstLineIndex + i));
continue;
}
int index = indexIs == -1 ? indexArrow : indexArrow == -1 ? indexIs : Math.Min(indexIs, indexArrow);
string key = l[0..index].Trim();
currentSection.Add(lastVal = new ConfigValue(currentSection, key, firstLineIndex + i));
if(indexIs != -1 && (indexArrow == -1 || indexIs < indexArrow))
{
string val = l[(indexIs+1)..].Trim();
lastVal.Value = val;
}
else
{
lastVal.amen = l[(indexArrow+2)..].Trim();
StringBuilder val = new();
i++;
bool first = true;
while(true)
{
if(i>=lines.Count())
throw new ConfigNoAmenException(lastVal);
if(lines[i].Trim() == lastVal.amen) break;
if(!first) val.Append("\n");
val.Append(lines[i]);
i++;
first = false;
}
lastVal.Value = val.ToString();
}
}
}
public void Print(TextWriter w)
{
MainSection.Print(w);
foreach(var x in sections) x.Print(w);
}
public IEnumerator<ConfigSection> GetEnumerator()
{
foreach(var s in sections)
yield return s;
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count{ get{return sections.Count;}}
public ConfigSection this[int i]
{
get { return sections[i]; }
}
public ConfigSection? this[string? key]
{
get { return Optional(key); }
}
public ConfigSection? Optional(string? key)
{
if(key == null) return MainSection;
if(!sectionByName.TryGetValue(key, out var r))
return null;
return r;
}
public ConfigSection Mandatory(string? key)
{
if(key == null) return MainSection;
if(!sectionByName.TryGetValue(key, out var r))
throw new ConfigSectionNotDefinedException(key);
return r;
}
}