Skip to main content

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
    }
}