Skip to content

Instantly share code, notes, and snippets.

@bozidarsk
Last active August 19, 2023 11:49
Show Gist options
  • Save bozidarsk/d372a1f689b0499d43e8df7eeab2b13a to your computer and use it in GitHub Desktop.
Save bozidarsk/d372a1f689b0499d43e8df7eeab2b13a to your computer and use it in GitHub Desktop.
Easy way of adding configurations to c# projects.
// #define NO_FILES
// #define NO_CSS
/* or as command line option when compiling '-define:NO_FILES,NO_CSS' */
/*
NO_FILES - no config and no style.css files
NO_CSS - a config file but not a style.css file
the order you call Config.Initialize(ref string[] args):
- 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)
- do anything else
in another file: (example 'ProjectConfig.cs')
'public static partial class Config':
- 'public static string ConfigDir { private set; get; } = "DEFAULT/CONFIG/DIR";' (only if NO_FILES is not defined)
- 'public static string Css { private set; get; } = "DEFAULTCSS";' (only if NO_CSS and NO_FILES are not defined)
- 'private static readonly Option[] OptionsDefinition' (describes all command line options and their behaviour)
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 '$', 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; } = DEFAULTVALUE;'
- 'public static TYPE[] NAME { private set; get; } = DEFAULTVALUE;'
arrays are defined in config file as:
'NAME[]=ITEM0,ITEM1,ITEM2' (supports ony 1d arrays; again be carefull of spaces)
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
public static partial class Config
{
// if a property is set from a command line option dont override it with the config file
private static List<string> propertiesFromOptions = new List<string>();
#if !NO_FILES
public static void CreateDefaults()
{
if (string.IsNullOrEmpty(Config.ConfigDir))
{
Console.WriteLine("'Config.ConfigDir' must not be null or empty.");
Environment.Exit(1);
}
if (!Directory.Exists(Config.ConfigDir)) { Directory.CreateDirectory(Config.ConfigDir); }
PropertyInfo[] properties = typeof(Config).GetProperties();
string config = "";
for (int i = 0; i < properties.Length; i++)
{
if (properties[i].Name == "ConfigDir" || properties[i].Name == "Css") { continue; }
if (properties[i].PropertyType.ToString().EndsWith("[]"))
{
config += $"{properties[i].Name}[]=";
Array array = (Array)properties[i].GetValue(null);
for (int t = 0; t < array.Length; t++) { config += $"{array.GetValue(t)}{(t < array.Length - 1) ? "," : "\n"}"; }
} else { config += $"{properties[i].Name}={properties[i].GetValue(null)}\n"; }
}
File.WriteAllText(Config.ConfigDir + "/config", config);
#if !NO_CSS
File.WriteAllText(Config.ConfigDir + "/style.css", Config.Css);
#endif
}
#endif
private static object ParseContent(string content, Type type) => type.IsEnum ? Enum.Parse(type, content) : Convert.ChangeType(content, type);
#if !NO_FILES
private static void InitializeFromFile()
{
if (Config.ConfigDir == null || !Directory.Exists(Config.ConfigDir)) { return; }
#if !NO_CSS
if (File.Exists(Config.ConfigDir + "/style.css") && !propertiesFromOptions.Contains("Css"))
{ Config.Css = File.ReadAllText(Config.ConfigDir + "/style.css"); }
#endif
if (!File.Exists(Config.ConfigDir + "/config")) { return; }
string[] config =
File.ReadAllText(Config.ConfigDir + "/config")
.Split('\n')
.Where(x => !string.IsNullOrEmpty(x) && x[0] != '#')
.ToArray()
;
for (int i = 0; i < config.Length; i++)
{
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] == '$')
{
name = name.Remove(0, 1);
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)))); }
}
// skip this property because it is already set from options (dont override command line options with config file)
if (propertiesFromOptions.Contains(name)) { continue; }
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
private static void InitializeFromOptions(ref string[] args)
{
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();
}
public static void Initialize(ref string[] args)
{
InitializeFromOptions(ref args);
#if !NO_FILES
InitializeFromFile();
#endif
}
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);
Config.propertiesFromOptions.Add(this.Name);
}
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