Select Git revision
splay_operation.h
-
David Mareček authoredDavid Mareček authored
ConfigParser.cs 15.10 KiB
// i3csstatus - Alternative generator of i3 status bar written in c#
// (c) 2022 Jiri Kalvoda <jirikalvoda@kam.mff.cuni.cz>
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#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 ConfigParser Parser;
internal ConfigException(ConfigParser parser)
{
Parser = parser;
}
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(ConfigParser parser, string name, int lineA, int lineB): base(parser)
{
Name = name;
LineA = lineA;
LineB = lineB;
}
}
class ConfigDuplicitSectionException: ConfigDuplicitException
{
public ConfigDuplicitSectionException(ConfigParser parser, string name, int lineA, int lineB)
:base(parser, name, lineA, lineB){}
public override string ErrorString()
{
return $"Duplicit section name {Name} on lines {LineA}, {LineB}";
}
}
class ConfigDuplicitKeyException: ConfigDuplicitException
{
public ConfigDuplicitKeyException(ConfigSection section, string name, int lineA, int lineB)
:base(section.Root, name, lineA, lineB){}
public override string ErrorString()
{
return $"Duplicit key name {Name} on lines {LineA}, {LineB}";
}
}
class ConfigNoAmenException: ConfigException
{
public ConfigOption Value;
public ConfigNoAmenException(ConfigOption value):base(value.Section.Root)
{
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 ConfigOption Value;
public int Line;
public ConfigMixedTabException(ConfigOption value, int line):base(value.Section.Root)
{
Value = value;
Line = line;
}
public override string ErrorString()
{
return $"{Value.Place}: Combination of tabs and spaces is not supported.";
}
}
class ConfigWrongIndentException: ConfigException
{
public ConfigOption? Value;
public int Line;
public ConfigWrongIndentException(ConfigSection section, ConfigOption? value, int line):base(section.Root)
{
Value = value;
Line = line;
}
public override string ErrorString()
{
if(Value == null)
return $"{Line}: Wrong indent.";
return $"{Value.Place}: Wrong indent.";
}
}
class ConfigSectionNotDefinedException: ConfigException
{
public string Name;
public ConfigSectionNotDefinedException(ConfigParser parser, string name):base(parser)
{
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):base(section.Root)
{
Section = section;
Name = name;
}
public override string ErrorString()
{
return $"{Section.SectionName ?? "[global]"}: Option \"{Name}\" is not defined.";
}
}
class ConfigNoValueException: ConfigException
{
public ConfigValue Value;
public string? Reason;
public ConfigNoValueException(ConfigValue value, string? reason=null):base(value.Root)
{
Value = value;
Reason = reason;
}
public override string ErrorString()
{
return $"{Value.Place}: Value needed." ;
}
}
class ConfigUnusedOptionException: ConfigException
{
public ConfigOption Value;
public ConfigUnusedOptionException(ConfigOption value):base(value.Root)
{
Value = value;
}
public override string ErrorString()
{
return $"{Value.Place}: No such option." ;
}
}
class ConfigUnusedSectionException: ConfigException
{
public ConfigSection Section;
public ConfigUnusedSectionException(ConfigSection section):base(section.Root)
{
Section = section;
}
public override string ErrorString()
{
if(Section.SectionName == null)
return $"Global section not allowed." ;
else
return $"{Section.SectionName}: No such section." ;
}
}
class ConfigParseValueException: ConfigException
{
public ConfigValue Value;
public string? Reason;
public ConfigParseValueException(ConfigValue value, string? reason=null):base(value.Root)
{
Value = value;
Reason = reason;
}
public override string ErrorString()
{
return $"{Value.Place}: Parsing fail" +
(Reason==null?".":$": {Reason}.");
}
}
class ConfigMistake: ConfigException
{
public ConfigValue Value;
public string Text;
public ConfigMistake(ConfigValue value, string text):base(value.Root)
{
Value = value;
Text = text;
}
public override string ErrorString()
{
return $"{Value.Place}: {Text}";
}
}
abstract class ConfigValue
{
public ConfigParser Root;
public int Line;
internal string? Value;
public bool HasText() => Value != null;
public virtual string Place{get => $"{Line}: {FullName}";}
public abstract string FullName{get;} // For showing in error
public ConfigValue(ConfigParser _Root)
{
Root = _Root;
}
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 ConfigParseValueException(this, "String is not color");
}
}
public bool AsBool()
{
if(Value == null) return true;
string v = Value.ToLower();
if(v == "true" || v == "yes" || v == "use" || v == "1")
return true;
if(v == "false" || v == "no" || v == "unuse" || v == "0")
return false;
throw new ConfigParseValueException(this, "String is not bool");
}
public double AsDouble()
{
try
{
return double.Parse(AsString());
} catch (FormatException)
{
throw new ConfigParseValueException(this, "String is not number");
}
}
public int AsInt()
{
try
{
return int.Parse(AsString());
} catch (FormatException)
{
throw new ConfigParseValueException(this, "String is not int");
}
}
public int AsMs()
{
return (int)(1000 * AsDouble());
}
public ConfigParser AsConfig()
{
return new ConfigParser(AsString(), Root.FileName, Line);
}
public static implicit operator string(ConfigValue v) => v.AsString();
}
class ConfigOption: ConfigValue
{
public ConfigSection Section {get; init;}
internal string? amen;
internal string? tabs;
public string Key {get; init;}
public bool Used = false;
public ConfigOption Use()
{
Used = true;
return this;
}
public override string FullName
{
get => Section.SectionName != null ? $"{Section.SectionName}: {Key}" : Key;
}
internal ConfigOption(ConfigSection _Section, string _Key, int _line):base(_Section.Root)
{
Section = _Section;
Key = _Key;
Line = _line;
}
public ConfigKeyAsValue KeyAsValue() => new(this);
}
class ConfigKeyAsValue: ConfigValue
{
public ConfigSection Section {get; init;}
public override string FullName
{
get => (Section.SectionName != null ? $"{Section.SectionName}: " : "") + $"Key {Value}";
}
internal ConfigKeyAsValue(ConfigOption keyValue):base(keyValue.Root)
{
Line = keyValue.Line;
Value = keyValue.Key;
Section = keyValue.Section;
}
}
class ConfigSectionNameAsValue: ConfigValue
{
public ConfigSection Section {get; init;}
public override string FullName
{
get => "Section name " + (Section.SectionName != null ? $"\"{Section.SectionName}\"" : "of main section");
}
internal ConfigSectionNameAsValue(ConfigSection _Section):base(_Section.Root)
{
Section = _Section;
Value = Section.SectionName;
Line = Section.Line;
}
}
class ConfigSectionNameAsDefaultValue: ConfigSectionNameAsValue
{
protected string defaultFor;
internal ConfigSectionNameAsDefaultValue(ConfigSection _Section, string _defaultFor): base(_Section)
{
defaultFor = _defaultFor;
}
public override string FullName
{
get => base.FullName + $" as default for \"{defaultFor}\" option";
}
}
class ConfigSection: IReadOnlyList<ConfigOption>
{
public ConfigParser Root {get; init;}
public string? SectionName {get; init;}
public int Line;
List<ConfigOption> values = new();
Dictionary<string, ConfigOption> valueByName = new();
public bool Used = false;
public ConfigSection Use()
{
Used = true;
return this;
}
internal ConfigSection(ConfigParser _Root, string? _SectionName, int _line)
{
Root = _Root;
SectionName = _SectionName;
Line = _line;
}
internal void Add(ConfigOption v)
{
if(valueByName.ContainsKey(v.Key))
throw new ConfigDuplicitKeyException(this, v.Key, v.Line, valueByName[v.Key].Line);
valueByName[v.Key] = v;
values.Add(v);
}
public IEnumerator<ConfigOption> 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 ConfigOption this[int i]
{
get { return values[i]; }
}
public ConfigOption? this[string key]
{
get { return Optional(key); }
}
public ConfigOption? Optional(string key)
{
if(!valueByName.TryGetValue(key, out var r))
return null;
return r.Use();
}
public ConfigValue OptionalDefaultIsSectionName(string key)
{
return (ConfigValue?)Optional(key) ?? new ConfigSectionNameAsDefaultValue(this, key);
}
public ConfigOption Mandatory(string key)
{
if(!valueByName.TryGetValue(key, out var r))
throw new ConfigNotDefinedException(this, key);
return r.Use();
}
public bool Contains(string key)
{
return valueByName.ContainsKey(key);
}
public void Print(TextWriter w)
{
w.WriteLine($"### {SectionName}");
foreach(var x in values)
{
w.WriteLine($"'{x.Key}' = '{(x.HasText() ? x.AsString() : "null")}'");
}
}
public ConfigSectionNameAsValue SectionNameAsValue() => new(this);
public void CheckUnused()
{
foreach(ConfigOption it in this)
{
if(it.Used == false)
throw new ConfigUnusedOptionException(it);
}
}
}
class ConfigParser: IReadOnlyList<ConfigSection>
{
public string? FileName {get; init;}
public ConfigSection MainSection;
List<ConfigSection> sections = new();
Dictionary<string, ConfigSection> sectionByName = new();
public ConfigParser(string s, string? _FileName = null, int firstLineIndex = 1)
{
FileName = _FileName;
string[] lines = s.Split("\n");
var currentSection = MainSection = new(this, null, 0);
ConfigOption? 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(this, 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(currentSection, 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(currentSection, 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 ConfigOption(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 ConfigOption(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.Use();
}
public ConfigSection Mandatory(string? key)
{
if(key == null) return MainSection;
if(!sectionByName.TryGetValue(key, out var r))
throw new ConfigSectionNotDefinedException(this, key);
return r.Use();
}
public void CheckUnused()
{
foreach(ConfigSection it in this)
{
if(it.Used == false)
throw new ConfigUnusedSectionException(it);
}
}
}