C# INI file parsing library.
#region - The MIT License (MIT) -
//
// Copyright (c) 2013 Atif Aziz. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
#endregion
//
// # INI File Format Parser
//
// Source: https://github.com/atifaziz/Gini
//
// ## Usage
//
// The most basic method in Gini is called Parse that parses the INI file format
// and yields groups of key-value pairs. So sections in an INI file format
// become groups where the group key is the section name and the entries of the
// section become key-value pairs of the group.
//
// The following code C# example:
//
// const string ini = @"
// ; last modified 1 April 2001 by John Doe
// [owner]
// name=John Doe
// organization=Acme Widgets Inc.
//
// [database]
// ; use IP address in case network name resolution is not working
// server=192.0.2.62
// port=143
// file=payroll.dat";
//
// foreach (var g in Ini.Parse(ini))
// {
// Console.WriteLine("[{0}]", g.Key);
// foreach (var e in g)
// Console.WriteLine("{0}={1}", e.Key, e.Value);
// }
//
// produces the output:
//
// [owner]
// name=John Doe
// organization=Acme Widgets Inc.
// [database]
// server=192.0.2.62
// port=143
// file=payroll.dat
//
// The ParseHash method parses the INI file format and return a dictionary of
// sections where each section is itself a dictionary of entries:
//
// const string ini = @"
// ; last modified 1 April 2001 by John Doe
// [owner]
// name=John Doe
// organization=Acme Widgets Inc.
//
// [database]
// ; use IP address in case network name resolution is not working
// server=192.0.2.62
// port=143
// file=payroll.dat";
//
// var config = Ini.ParseHash(ini);
// var owner = config["owner"];
// Console.WriteLine("Owner Name = {0}", owner["name"]);
// Console.WriteLine("Owner Organization = {0}", owner["organization"]);
// var database = config["database"];
// Console.WriteLine("Database Server = {0}", database["server"]);
// Console.WriteLine("Database Port = {0}", database["port"]);
// Console.WriteLine("Database File = {0}", database["file"]);
//
// The output produced by the preceding code is:
//
// Owner Name = John Doe
// Owner Organization = Acme Widgets Inc.
// Database Server = 192.0.2.62
// Database Port = 143
// Database File = payroll.dat
//
// The ParseHashFlat method is like ParseFlat except it returns a single
// dictionary of entries. The section names are merged with the key names via a
// mapper function to generate unique entries:
//
// const string ini = @"
// ; last modified 1 April 2001 by John Doe
// [owner]
// name=John Doe
// organization=Acme Widgets Inc.
//
// [database]
// ; use IP address in case network name resolution is not working
// server=192.0.2.62
// port=143
// file=payroll.dat";
//
// foreach (var e in Ini.ParseFlatHash(ini, (s, k) => s + "." + k))
// Console.WriteLine("{0} = {1}", e.Key, e.Value);
//
// The output is as follows:
//
// owner.name = John Doe
// owner.organization = Acme Widgets Inc.
// database.server = 192.0.2.62
// database.port = 143
// database.file = payroll.dat
//
// Gini can also parse the INI file format into a dynamic object via the
// ParseObject:
//
// const string ini = @"
// ; last modified 1 April 2001 by John Doe
// [owner]
// name=John Doe
// organization=Acme Widgets Inc.
//
// [database]
// ; use IP address in case network name resolution is not working
// server=192.0.2.62
// port=143
// file=payroll.dat";
//
// var config = Ini.ParseObject(ini);
// var owner = config.Owner;
// Console.WriteLine("Owner Name = {0}", owner.Name);
// Console.WriteLine("Owner Organization = {0}", owner.Organization);
// var database = config.Database;
// Console.WriteLine("Database Server = {0}", database.Server);
// Console.WriteLine("Database Port = {0}", database.Port);
// Console.WriteLine("Database File = {0}", database.File);
//
// The output is:
//
// Owner Name = John Doe
// Owner Organization = Acme Widgets Inc.
// Database Server = 192.0.2.62
// Database Port = 143
// Database File = payroll.dat
// Note that the lookup of properties on the dynamic object is case-insensitive.
//
// Like there is ParseFlatHash for ParseFlat, there is ParseFlatObject for
// ParseObject that returns a single object of entries with a mapper function
// determining how to merge section and key names:
//
// const string ini = @"
// ; last modified 1 April 2001 by John Doe
// [owner]
// name=John Doe
// organization=Acme Widgets Inc.
//
// [database]
// ; use IP address in case network name resolution is not working
// server=192.0.2.62
// port=143
// file=payroll.dat";
//
// var config = Ini.ParseFlatObject(ini, (s, k) => s + k);
// Console.WriteLine("Owner Name = {0}", config.OwnerName);
// Console.WriteLine("Owner Organization = {0}", config.OwnerOrganization);
// Console.WriteLine("Database Server = {0}", config.DatabaseServer);
// Console.WriteLine("Database Port = {0}", config.DatabasePort);
// Console.WriteLine("Database File = {0}", config.DatabaseFile);
//
// The output is again:
//
// Owner Name = John Doe
// Owner Organization = Acme Widgets Inc.
// Database Server = 192.0.2.62
// Database Port = 143
// Database File = payroll.dat
//
// Syntax errors in the INI file format are silently ignored. Only bits that can
// be successfully parsed are returned or processed.
//
namespace IniParser
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Dynamic;
using System.Globalization;
public partial class Ini
{
public static dynamic ParseObject(string ini) =>
ParseObject(ini, null);
public static dynamic ParseObject(string ini, IEqualityComparer<string> comparer)
{
comparer = comparer ?? StringComparer.OrdinalIgnoreCase;
var config = new Dictionary<string, Config<string>>(comparer);
foreach (var section in from g in Parse(ini).GroupBy(e => e.Key ?? string.Empty, comparer)
select KeyValuePair.Create(g.Key, g.SelectMany(e => e)))
{
var settings = new Dictionary<string, string>(comparer);
foreach (var setting in section.Value)
settings[setting.Key] = setting.Value;
config[section.Key] = new Config<string>(settings);
}
return new Config<Config<string>>(config);
}
public static dynamic ParseFlatObject(string ini, Func<string, string, string> keyMerger) =>
ParseFlatObject(ini, keyMerger, null);
public static dynamic ParseFlatObject(string ini, Func<string, string, string> keyMerger, IEqualityComparer<string> comparer)
{
if (keyMerger == null) throw new ArgumentNullException(nameof(keyMerger));
return new Config<string>(ParseFlatHash(ini, keyMerger, comparer));
}
private sealed class Config<T> : DynamicObject
{
private readonly IDictionary<string, T> _entries;
public Config(IDictionary<string, T> entries)
{
Debug.Assert(entries != null);
_entries = entries;
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
if (binder == null) throw new ArgumentNullException(nameof(binder));
result = Find(binder.Name);
return true;
}
public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
{
if (indexes == null) throw new ArgumentNullException(nameof(indexes));
if (indexes.Length != 1) throw new ArgumentException("Too many indexes supplied.");
var index = indexes[0];
result = Find(index == null ? null : Convert.ToString(index, CultureInfo.InvariantCulture));
return true;
}
private object Find(string name) =>
_entries.TryGetValue(name, out var value) ? value : default;
}
}
public static partial class Ini
{
private static class Parser
{
private static readonly Regex Regex;
private static readonly int SectionNumber;
private static readonly int KeyNumber;
private static readonly int ValueNumber;
static Parser()
{
var re = Regex =
new Regex(@"^ *(\[(?<s>[a-z0-9-._][a-z0-9-._ ]*)\]|(?<k>[a-z0-9-._][a-z0-9-._ ]*)= *(?<v>[^\r\n]*))\s*$",
RegexOptions.Multiline
| RegexOptions.IgnoreCase
| RegexOptions.CultureInvariant);
SectionNumber = re.GroupNumberFromName("s");
KeyNumber = re.GroupNumberFromName("k");
ValueNumber = re.GroupNumberFromName("v");
}
// ReSharper disable once MemberHidesStaticFromOuterClass
public static IEnumerable<T> Parse<T>(string ini, Func<string, string, string, T> selector)
{
return from Match m in Regex.Matches(ini ?? string.Empty)
select m.Groups
into g
select selector(g[SectionNumber].Value.TrimEnd(),
g[KeyNumber].Value.TrimEnd(),
g[ValueNumber].Value.TrimEnd());
}
}
public static IEnumerable<IGrouping<string, KeyValuePair<string, string>>> Parse(string ini) =>
Parse(ini, KeyValuePair.Create);
public static IEnumerable<IGrouping<string, T>> Parse<T>(string ini, Func<string, string, T> settingSelector) =>
Parse(ini, (_, k, v) => settingSelector(k, v));
public static IEnumerable<IGrouping<string, T>> Parse<T>(string ini, Func<string, string, string, T> settingSelector)
{
if (settingSelector == null) throw new ArgumentNullException(nameof(settingSelector));
ini = ini.Trim();
if (string.IsNullOrEmpty(ini))
return Enumerable.Empty<IGrouping<string, T>>();
var entries =
from ms in new[]
{
Parser.Parse(ini, (s, k, v) => new
{
Section = s,
Setting = KeyValuePair.Create(k, v)
})
}
from p in Enumerable.Repeat(new
{
Section = (string) null,
Setting = KeyValuePair.Create(string.Empty, string.Empty)
}, 1)
.Concat(ms)
.GroupAdjacent(s => s.Section == null || s.Section.Length > 0)
.Pairwise((prev, curr) => new {Prev = prev, Curr = curr})
where p.Prev.Key
select KeyValuePair.Create(p.Prev.Last().Section, p.Curr)
into e
from s in e.Value
select KeyValuePair.Create(e.Key, settingSelector(e.Key, s.Setting.Key, s.Setting.Value));
return entries.GroupAdjacent(e => e.Key, e => e.Value);
}
public static IDictionary<string, IDictionary<string, string>> ParseHash(string ini) =>
ParseHash(ini, null);
public static IDictionary<string, IDictionary<string, string>> ParseHash(string ini, IEqualityComparer<string> comparer)
{
comparer = comparer ?? StringComparer.OrdinalIgnoreCase;
var sections = Parse(ini);
return sections.GroupBy(g => g.Key ?? string.Empty, comparer)
.ToDictionary(g => g.Key,
g => (IDictionary<string, string>) g.SelectMany(e => e)
.GroupBy(e => e.Key, comparer)
.ToDictionary(e => e.Key, e => e.Last().Value, comparer),
comparer);
}
public static IDictionary<string, string> ParseFlatHash(string ini, Func<string, string, string> keyMerger) =>
ParseFlatHash(ini, keyMerger, null);
public static IDictionary<string, string> ParseFlatHash(string ini, Func<string, string, string> keyMerger, IEqualityComparer<string> comparer)
{
if (keyMerger == null) throw new ArgumentNullException(nameof(keyMerger));
var settings = new Dictionary<string, string>(comparer ?? StringComparer.OrdinalIgnoreCase);
foreach (var setting in from section in Parse(ini)
from setting in section
select KeyValuePair.Create(keyMerger(section.Key, setting.Key), setting.Value))
{
settings[setting.Key] = setting.Value;
}
return settings;
}
private static class KeyValuePair
{
public static KeyValuePair<TKey, TValue> Create<TKey, TValue>(TKey key, TValue value) =>
new KeyValuePair<TKey, TValue>(key, value);
}
#region MoreLINQ
// MoreLINQ - Extensions to LINQ to Objects
// Copyright (c) 2012 Atif Aziz. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
public static IEnumerable<TResult> Pairwise<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TSource, TResult> resultSelector)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (resultSelector == null) throw new ArgumentNullException(nameof(resultSelector));
return _();
IEnumerable<TResult> _()
{
using (var e = source.GetEnumerator())
{
if (!e.MoveNext())
yield break;
var previous = e.Current;
while (e.MoveNext())
{
yield return resultSelector(previous, e.Current);
previous = e.Current;
}
}
}
}
private static IEnumerable<IGrouping<TKey, TSource>> GroupAdjacent<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector)
{
return GroupAdjacent(source, keySelector, null);
}
private static IEnumerable<IGrouping<TKey, TSource>> GroupAdjacent<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
IEqualityComparer<TKey> comparer)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
return GroupAdjacent(source, keySelector, e => e, comparer);
}
private static IEnumerable<IGrouping<TKey, TElement>> GroupAdjacent<TSource, TKey, TElement>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
Func<TSource, TElement> elementSelector)
{
return GroupAdjacent(source, keySelector, elementSelector, null);
}
private static IEnumerable<IGrouping<TKey, TElement>> GroupAdjacent<TSource, TKey, TElement>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
Func<TSource, TElement> elementSelector,
IEqualityComparer<TKey> comparer)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
if (elementSelector == null) throw new ArgumentNullException(nameof(elementSelector));
return GroupAdjacentImpl(source, keySelector, elementSelector,
comparer ?? EqualityComparer<TKey>.Default);
}
private static IEnumerable<IGrouping<TKey, TElement>> GroupAdjacentImpl<TSource, TKey, TElement>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
Func<TSource, TElement> elementSelector,
IEqualityComparer<TKey> comparer)
{
Debug.Assert(source != null);
Debug.Assert(keySelector != null);
Debug.Assert(elementSelector != null);
Debug.Assert(comparer != null);
using (var iterator = source.GetEnumerator())
{
var group = default(TKey);
var members = (List<TElement>) null;
while (iterator.MoveNext())
{
var key = keySelector(iterator.Current);
var element = elementSelector(iterator.Current);
if (members != null && comparer.Equals(group, key))
{
members.Add(element);
}
else
{
if (members != null)
yield return CreateGroupAdjacentGrouping(group, members);
group = key;
members = new List<TElement> {element};
}
}
if (members != null)
yield return CreateGroupAdjacentGrouping(group, members);
}
}
private static Grouping<TKey, TElement> CreateGroupAdjacentGrouping<TKey, TElement>(TKey key, IList<TElement> members)
{
Debug.Assert(members != null);
return new Grouping<TKey, TElement>(key, members.IsReadOnly ? members : new ReadOnlyCollection<TElement>(members));
}
private sealed class Grouping<TKey, TElement> : IGrouping<TKey, TElement>
{
private readonly IEnumerable<TElement> _members;
public Grouping(TKey key, IEnumerable<TElement> members)
{
Debug.Assert(members != null);
Key = key;
_members = members;
}
public TKey Key { get; }
public IEnumerator<TElement> GetEnumerator()
{
return _members.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
#endregion
}
}