Last active
August 19, 2023 11:49
-
-
Save bozidarsk/d372a1f689b0499d43e8df7eeab2b13a to your computer and use it in GitHub Desktop.
Easy way of adding configurations to c# projects.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// #define NO_FILES | |
// #define NO_CSS | |
// or as command line option when compiling '-define:NO_FILES,NO_CSS' | |
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using System.Reflection; | |
using System.Text.RegularExpressions; | |
/* | |
the order you call Config.Initialize(ref string[] args, string pathConfigDir): | |
- check args[] if help message needs to be printed | |
- call Initialize (passing args with ref will remove any options that were processed from the array), if NO_FILES is defined pathConfigDir is irrelevant | |
- do anything else | |
in another file: (example 'ProjectConfig.cs') | |
'public static partial class Config' | |
- 'private static readonly Option[] OptionsDefinition' (describes all command line options and their behaviour) | |
if NO_FILES is not defined it must contain the following fields: | |
- 'private static readonly string DefaultConfig' | |
- 'private static readonly string DefaultCss' (only if NO_CSS is not defined) | |
- 'private static readonly string DefaultConfigDir' | |
if NO_FILES is defined the following is irrelevant | |
the config file: | |
- format is 'NAME=VALUE' | |
- each space is treated as part of NAME or VALUE depending on its position | |
- lines that start with '#' are comments and are skipped | |
- if a line starts with '!' environment variable expansion will not be used | |
- each environment variable (starting with '$') will be expanded to its corresponding value | |
properties defined in config file must be defined in 'ProjectConfig.cs' as: | |
- 'public static TYPE NAME { private set; get; }' | |
- 'public static TYPE[] NAME { private set; get; }' | |
you can add a default value like 'public static TYPE NAME { private set; get; } = VALUE;' | |
arrays are defined in config file as: | |
'NAME[]=ITEM0,ITEM1,ITEM2' (supports ony 1d arrays; again be carefull of spaces) | |
*/ | |
public static partial class Config | |
{ | |
#if !NO_FILES | |
public static void CreateDefaults() | |
{ | |
if (!Directory.Exists(Config.DefaultConfigDir)) { Directory.CreateDirectory(Config.DefaultConfigDir); } | |
File.WriteAllText(Config.DefaultConfigDir + "/config", Config.DefaultConfig); | |
#if !NO_CSS | |
File.WriteAllText(Config.DefaultConfigDir + "/style.css", Config.DefaultCss); | |
#endif | |
} | |
#endif | |
private static object ParseContent(string content, Type type) => type.IsEnum ? Enum.Parse(type, content) : Convert.ChangeType(content, type); | |
#if !NO_FILES | |
public static void Initialize(ref string[] args, string pathConfigDir) | |
#else | |
public static void Initialize(ref string[] args) | |
#endif | |
{ | |
#if !NO_FILES | |
if (pathConfigDir == null && !Directory.Exists(Config.DefaultConfigDir)) { Config.CreateDefaults(); } | |
string configPath = (pathConfigDir ?? Config.DefaultConfigDir) + "/config"; | |
#if !NO_CSS | |
string cssPath = (pathConfigDir ?? Config.DefaultConfigDir) + "/style.css"; | |
Config.Css = File.Exists(cssPath) ? File.ReadAllText(cssPath) : Config.DefaultCss; | |
#endif | |
string[] config = (File.Exists(configPath) ? File.ReadAllText(configPath) : Config.DefaultConfig).Split('\n').Where(x => !string.IsNullOrEmpty(x) && x[0] != '#').ToArray(); | |
for (int i = 0; i < config.Length; i++) | |
{ | |
if (string.IsNullOrEmpty(config[i])) { continue; } | |
int index = config[i].IndexOf("="); | |
string name = config[i].Substring(0, index); | |
string content = config[i].Remove(0, index + 1); | |
bool isArray = name.EndsWith("[]"); | |
if (isArray) { name = name.Substring(0, name.Length - 2); } | |
if (name[0] != '!') | |
{ | |
for (Match match = Regex.Match(content, "\\$[a-zA-Z0-9_\\-]+"); match.Success; match = Regex.Match(content, "\\$[a-zA-Z0-9_\\-]+")) | |
{ match.Captures.ToList().ForEach(x => content = content.Replace(x.Value, Environment.GetEnvironmentVariable(x.Value.Remove(0, 1)))); } | |
} else { name = name.Remove(0, 1); } | |
PropertyInfo property = typeof(Config).GetProperty(name); | |
if (property == null) | |
{ | |
Console.WriteLine("Property '" + name + "' was not found."); | |
Environment.Exit(1); | |
} | |
try | |
{ | |
if (isArray) | |
{ | |
string[] items = content.Split(','); | |
Type itemType = Type.GetType(property.PropertyType.ToString().Replace("[]", "")); | |
Array array = Array.CreateInstance(itemType, items.Length); | |
for (int t = 0; t < array.Length; t++) { array.SetValue(ParseContent(items[t], itemType), t); } | |
property.SetValue(null, array); | |
} else { property.SetValue(null, ParseContent(content, property.PropertyType)); } | |
} | |
catch | |
{ | |
Console.WriteLine("Error parsing '" + content + "' for '" + name + "'."); | |
Environment.Exit(1); | |
} | |
} | |
#endif | |
List<string> list = args.ToList(); | |
for (int i = 0; i < list.Count; i++) | |
{ | |
bool hasArg = false; | |
if (list[i][0] == '-' && list[i][1] != '-') | |
{ | |
for (int t = 1; t < list[i].Length; t++) | |
{ | |
Option option = OptionsDefinition.Where(x => x.ShortName == list[i][t]).FirstOrDefault(); | |
if (option == null) { Console.WriteLine("Unknown option '-" + list[i][t] + "'."); Environment.Exit(1); } | |
if (option.HasArg && i >= list.Count) { Console.WriteLine("Invalid option."); Environment.Exit(1); } | |
option.ParseMethod.Invoke(option.HasArg ? list[i + 1] : null); | |
hasArg = option.HasArg; | |
} | |
} | |
else if (list[i][0] == '-' && list[i][1] == '-') | |
{ | |
Option option = OptionsDefinition.Where(x => x.Name == list[i]).FirstOrDefault(); | |
if (option == null) { Console.WriteLine("Unknown option '" + list[i] + "'."); Environment.Exit(1); } | |
if (option.HasArg && i >= list.Count) { Console.WriteLine("Invalid option."); Environment.Exit(1); } | |
option.ParseMethod.Invoke(option.HasArg ? list[i + 1] : null); | |
hasArg = option.HasArg; | |
} | |
else | |
{ | |
continue; | |
} | |
if (hasArg) { list.RemoveAt(i); } | |
list.RemoveAt(i--); | |
} | |
args = list.ToArray(); | |
} | |
private sealed class Option | |
{ | |
public string Name { private set; get; } | |
public char ShortName { private set; get; } | |
public bool HasArg { private set; get; } | |
public Action<string> ParseMethod { private set; get; } | |
private void DefaultParseMethod(string arg) | |
{ | |
string propertyName = ""; | |
for (int i = 1; i < this.Name.Length; i++) | |
{ | |
if (this.Name[i] == '-') | |
{ | |
propertyName += char.ToUpper(this.Name[++i]); | |
continue; | |
} | |
propertyName += this.Name[i]; | |
} | |
PropertyInfo property = typeof(Config).GetProperty(propertyName); | |
property.SetValue(null, this.HasArg ? ParseContent(arg, property.PropertyType) : true); | |
} | |
public Option(string name, char shortName, bool hasArg, Action<string> parseMethod) | |
{ | |
this.Name = name; | |
this.ShortName = shortName; | |
this.HasArg = hasArg; | |
this.ParseMethod = parseMethod ?? DefaultParseMethod; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment