#region CPL License /* Nuclex Framework Copyright (C) 2002-2014 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library 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 IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #endregion using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using Nuclex.Support.Parsing; namespace Nuclex.Support.Settings { partial class ConfigurationFileStore { #region class ParserState /// Remembers the target store and current category of the parser private class ParserState { /// Store to which the parsed categories and options will be added public ConfigurationFileStore Store; /// Current category options belong to public Category Category; } #endregion // class ParserState /// Parses a configuration file from the specified text reader /// Reader the configuration file will be parsed from /// The configuration file parsed from the specified reader public static ConfigurationFileStore Parse(TextReader reader) { var store = new ConfigurationFileStore(); var state = new ParserState() { Store = store, Category = store.rootCategory }; for(; ; ) { string line = reader.ReadLine(); if(line == null) { break; } parseLine(state, line); } return store; } /// Incrementally parses a line read from a configuration file /// Current parser state /// Line that has been read private static void parseLine(ParserState state, string line) { // If the line is empty, ignore it int length = line.Length; if(length == 0) { return; } // Skip all spaces at the beginning of the line int firstCharacterIndex = 0; ParserHelper.SkipSpaces(line, ref firstCharacterIndex); // If the line contained nothing but spaces, ignore it if(firstCharacterIndex == length) { return; } // If the line is a comment, ignore it if((line[firstCharacterIndex] == '#') || (line[firstCharacterIndex] == ';')) { return; } // Now the line is either a category definition or some attempt to set an option if(line[firstCharacterIndex] == '[') { parseCategory(state, line, firstCharacterIndex); } else { parseOption(state, line, firstCharacterIndex); } state.Category.Lines.Add(line); } /// Parses a category definition encountered on a line /// Current parser state /// Line containing the category definition /// Index of the definition's first character private static void parseCategory( ParserState state, string line, int firstCharacterIndex ) { Debug.Assert(line[firstCharacterIndex] == '['); int nameStartIndex = firstCharacterIndex + 1; ParserHelper.SkipSpaces(line, ref nameStartIndex); int lastCharacterIndex = line.Length - 1; if(nameStartIndex >= lastCharacterIndex) { return; // No space left for closing brace } int nameEndIndex = line.IndexOf(']', nameStartIndex); if(nameEndIndex == -1) { return; // No closing brace in line } // Skip any whitespaces between the last character and the closing brace do { --nameEndIndex; } while(char.IsWhiteSpace(line, nameEndIndex)); // Now we know that the line holds a category definition and where exactly in // the line the category name is located. Create the category. state.Category = new Category() { CategoryName = new StringSegment( line, nameStartIndex, nameEndIndex - nameStartIndex + 1 ), OptionLookup = new Dictionary(), Lines = new List() }; state.Store.categoryLookup.Add(state.Category.CategoryName.ToString(), state.Category); } /// Parses an option definition encountered on a line /// Current parser state /// Line containing the option definition /// Index of the definition's first character private static void parseOption( ParserState state, string line, int firstCharacterIndex ) { int assignmentIndex = line.IndexOf('=', firstCharacterIndex + 1); if(assignmentIndex == -1) { return; // No assignment took place } // Cut off any whitespaces between the option name and the assignment int nameEndIndex = assignmentIndex; do { --nameEndIndex; } while(char.IsWhiteSpace(line, nameEndIndex)); // We have enough information to know that this is an assignment of some kind Option option = new Option() { LineIndex = state.Category.Lines.Count, OptionName = new StringSegment( line, firstCharacterIndex, nameEndIndex - firstCharacterIndex + 1 ) }; parseOptionValue(option, line, assignmentIndex + 1); // We've got the option assignment, either with an empty or proper value state.Store.options.Add(option); state.Category.OptionLookup.Add(option.OptionName.ToString(), option); } /// Parses the value assigned to an option /// Option to which a value is being assigned /// Line containing the option assignment /// Index one after the assignment character private static void parseOptionValue(Option option, string line, int assignmentEndIndex) { int firstCharacterIndex = assignmentEndIndex; ParserHelper.SkipSpaces(line, ref firstCharacterIndex); // Just for beauty, when the option value is empty but padded with spaces, // leave one space between the equals sign and the value. if(firstCharacterIndex > assignmentEndIndex) { ++assignmentEndIndex; } // If the line consists of only whitespace, create an empty value if(firstCharacterIndex == line.Length) { option.OptionValue = new StringSegment(line, assignmentEndIndex, 0); return; } char firstCharacter = line[firstCharacterIndex]; // Values can be quoted to allow for comments characters appearing in them int lastCharacterIndex; if(firstCharacter == '"') { lastCharacterIndex = line.LastIndexOf('"'); } else { lastCharacterIndex = firstCharacterIndex; } int commentStartIndex = line.IndexOf(';', lastCharacterIndex); if(commentStartIndex == -1) { commentStartIndex = line.IndexOf('#', lastCharacterIndex); } if(commentStartIndex == -1) { lastCharacterIndex = line.Length - 1; } else { lastCharacterIndex = commentStartIndex - 1; } while(lastCharacterIndex > firstCharacterIndex) { if(char.IsWhiteSpace(line, lastCharacterIndex)) { --lastCharacterIndex; } else { break; } } option.OptionValue = new StringSegment( line, firstCharacterIndex, lastCharacterIndex - firstCharacterIndex + 1 ); } /// Determines the best matching type for an option value /// Value for which the best matching type will be found /// The best matching type for the specified option value private static Type getBestMatchingType(ref StringSegment value) { if(value.Count == 0) { return typeof(string); } // If there are at least two characters, it may be an integer with // a sign in front of it if(value.Count >= 2) { int index = value.Offset; if(ParserHelper.SkipInteger(value.Text, ref index)) { if(index >= value.Offset + value.Count) { return typeof(int); } if(value.Text[index] == '.') { return typeof(float); } } } else { // If it's just a single character, it may be a number if(char.IsNumber(value.Text, value.Offset)) { return typeof(int); } } // If it parses as a boolean literal, then it must be a boolean if(parseBooleanLiteral(ref value) != null) { return typeof(bool); } return typeof(string); } /// Tries to parse a boolean literal /// Value that will be parsed as a boolean literal /// /// True or false if the value was a boolean literal, null if it wasn't /// private static bool? parseBooleanLiteral(ref StringSegment value) { switch(value.Count) { // If the string spells 'no', it is considered a boolean case 2: { bool isSpellingNo = ((value.Text[value.Offset + 0] == 'n') || (value.Text[value.Offset + 0] == 'N')) && ((value.Text[value.Offset + 1] == 'o') || (value.Text[value.Offset + 1] == 'O')); return isSpellingNo ? new Nullable(false) : null; } // If the string spells 'yes', it is considered a boolean case 3: { bool isSpellingYes = ((value.Text[value.Offset + 0] == 'y') || (value.Text[value.Offset + 0] == 'Y')) && ((value.Text[value.Offset + 1] == 'e') || (value.Text[value.Offset + 1] == 'E')) && ((value.Text[value.Offset + 2] == 's') || (value.Text[value.Offset + 2] == 'S')); return isSpellingYes ? new Nullable(true) : null; } // If the string spells 'true', it is considered a boolean case 4: { bool isSpellingTrue = ((value.Text[value.Offset + 0] == 't') || (value.Text[value.Offset + 0] == 'T')) && ((value.Text[value.Offset + 1] == 'r') || (value.Text[value.Offset + 1] == 'R')) && ((value.Text[value.Offset + 2] == 'u') || (value.Text[value.Offset + 2] == 'U')) && ((value.Text[value.Offset + 3] == 'e') || (value.Text[value.Offset + 3] == 'E')); return isSpellingTrue ? new Nullable(true) : null; } // If the string spells 'false', it is considered a boolean case 5: { bool isSpellingFalse = ((value.Text[value.Offset + 0] == 'f') || (value.Text[value.Offset + 0] == 'F')) && ((value.Text[value.Offset + 1] == 'a') || (value.Text[value.Offset + 1] == 'A')) && ((value.Text[value.Offset + 2] == 'l') || (value.Text[value.Offset + 2] == 'L')) && ((value.Text[value.Offset + 3] == 's') || (value.Text[value.Offset + 3] == 'S')) && ((value.Text[value.Offset + 4] == 'e') || (value.Text[value.Offset + 4] == 'E')); return isSpellingFalse ? new Nullable(false) : null; } // Anything else is not considered a boolean default: { return null; } } } } } // namespace Nuclex.Support.Configuration