C++용 YAML 파서를 C# 용으로 바꿔봤다. 자세한 내용은 C++ 버전을 참고.
고칠 점 투성이일 듯. 파싱할 때 그냥 문자열이 아니라, 정규식 사용하면 좀 더 깔끔할까?
//////////////////////////////////////////////////////////////////////////////// // File : YamlTree.cs // Author : 김성민 (http://serious-code.net) // Desc : 간단 YAML 파서 // Created : 2006-07-24 // Last Update : 2006-07-24 //////////////////////////////////////////////////////////////////////////////// namespace Yaml { /// <summary> /// 간단한 YAML 파서. XML 쪽의 DOM 파서를 생각하면 된다. 즉 문서를 통채로 /// 로드해서 메모리 상에 트리 형태로 구성하는 파서이다. /// /// XML 파일이 너무 지저분한 경향이 있어, 대안을 찾다가 YAML이라는 것을 /// 알게 되었다. 문법이 깔끔하기는 한데, C/C++ 파서가 현재 존재하지 않아서 /// 대충 쓰는 기능들만 모아서 한번 만들어봤다. 현재 지원하는 기능을 보자면 /// 다음과 같다. /// /// - 기본적인 scalar, sequence, mapping 지원 /// - Block scalar 및 folded scalar 지원 /// - Anchor & alias 지원 /// - 라인 주석 지원 /// /// 자식 노드는 맵 형태 또는 배열 형태로 액세스할 수 있다. 둘을 혼용할 수는 /// 없다. 기본적으로는 키 문자열을 이용해 맵 형태로 자식 노드들을 두게 된다. /// 이는 같은 이름(key)의 자식 노드가 2개 이상 있는 경우, 그 중에 하나만 /// 선택됨을 의미한다. Ruby에 있는 YAML 파서 같은 경우에는 제일 마지막에 /// 들어온 자식 노드를 선택한다. 하지만 여기서는 제일 첫번째로 들어온 자식 /// 노드를 선택하고, 그 다음 들어오는 노드는 삭제해 버리는 방식을 택했다. /// 배열 형태로 자식을 두기 위해서는 '-' 연산자를 이용해야 한다. 자세한 것은 /// 역시 YAML 문법을 참고. /// </summary> /// <remarks>Verbatim 처리가 필요하다.</remarks> public class YamlTree { // 스칼라의 종류 public enum ScalarType { NORMAL, BLOCK, FOLDED }; // 차일드 노드 저장 방식 public enum ChildType { MAPPED, SEQNENTIAL, UNKNOWN }; // typedefs public class Mapping : System.Collections.Generic.Dictionary<string, YamlTree> {}; public class Sequence : System.Collections.Generic.List<YamlTree> {}; public class Scalars : System.Collections.Generic.List<string> {}; // instance variables protected string m_Key = ""; protected string m_Value = ""; protected string m_Comment = ""; protected ScalarType m_ScalarType = ScalarType.NORMAL; protected Scalars m_Scalars = new Scalars(); protected YamlTree m_Alias = null; protected ChildType m_ChildType = ChildType.UNKNOWN; protected Mapping m_MappedChilds = new Mapping(); protected Sequence m_SequentialChilds = new Sequence(); protected int m_MaxChildKeyLength = 0; // properties public string Key { get { return m_Key; } } public string Value { get { return m_Value; } set { m_Value = value; } } public YamlTree Alias { get { return m_Alias; } set { m_Alias = value; } } public Mapping ChildAsMapping { get { return m_MappedChilds; } } public Sequence ChildAsSequence { get { return m_SequentialChilds; } } public int ChildCount { get { return m_SequentialChilds.Count; } } // constructor public YamlTree() { } // constructor public YamlTree(string key, object value) { m_Key = key; m_Value = value.ToString(); } // destructor ~YamlTree() { m_MappedChilds.Clear(); m_SequentialChilds.Clear(); } // get node value public string GetValueAsString() { return m_Value; } public int GetValueAsInt() { return int.Parse(m_Value); } public float GetValueAsFloat() { return float.Parse(m_Value); } public double GetValueAsDouble() { return double.Parse(m_Value); } public bool GetValueAsBoolean() { return bool.Parse(m_Value); } // set node value public void SetValue(string value) { m_Value = value; } public void SetValue(int value) { m_Value = value.ToString(); } public void SetValue(float value) { m_Value = value.ToString(); } public void SetValue(double value) { m_Value = value.ToString(); } public void SetValue(bool value) { m_Value = value.ToString(); } // comment public string GetComment() { return m_Alias == null ? m_Comment : m_Alias.GetComment(); } public void SetComment(string comment) { if (m_Alias == null) { m_Comment = comment; } else { m_Alias.SetComment(comment); } } // scalar public ScalarType GetScalarType() { return m_Alias == null ? m_ScalarType : m_Alias.GetScalarType(); } public void SetScalarType(ScalarType type) { if (m_Alias == null) { m_ScalarType = type; } else { m_Alias.SetScalarType(type); } } public void AddScalar(string text) { if (m_Alias == null) { m_Scalars.Add(text); } else { m_Alias.AddScalar(text); } } public Scalars GetScalars() { return m_Alias == null ? m_Scalars : m_Alias.GetScalars(); } // 자식 노드를 추가한다. public YamlTree AddChild(YamlTree child) { if (m_Alias != null) throw new System.Exception("cannot add child to aliased node"); if (m_Value.Length > 0 && m_Value[0] != '&' && m_Value[0] != '*') throw new System.Exception("cannot add child to scalar node"); if (child.Key == "") { if (m_ChildType == ChildType.UNKNOWN) m_ChildType = ChildType.SEQNENTIAL; if (m_ChildType != ChildType.SEQNENTIAL) throw new System.Exception("cannot add sequencial node"); string buf = string.Format("{0,5:N}", m_SequentialChilds.Count); m_SequentialChilds.Add(child); m_MappedChilds.Add(buf.ToString(), child); return child; } else { if (m_ChildType == ChildType.UNKNOWN) m_ChildType = ChildType.MAPPED; if (m_ChildType != ChildType.MAPPED) throw new System.Exception("cannot add mapped node"); if (!m_MappedChilds.ContainsKey(child.Key.ToLower())) { m_MaxChildKeyLength = System.Math.Max(m_MaxChildKeyLength, child.Key.Length); m_MappedChilds.Add(child.Key.ToLower(), child); m_SequentialChilds.Add(child); return child; } else { throw new System.Exception( string.Format("cannot add duplicated mapping - {0}", child.Key) ); } } } // 자식 노드의 존재 여부를 체크한다. public bool HasChild(string key) { if (m_Alias != null) return m_Alias.HasChild(key); if (m_ChildType == ChildType.MAPPED) { if (m_MappedChilds.ContainsKey(key.ToLower())) return true; } else if (m_ChildType == ChildType.SEQNENTIAL) { foreach (YamlTree child in m_SequentialChilds) if (child.Key == key) return true; } return false; } // 해당하는 자식 노드를 반환한다. public YamlTree GetChild(string key, bool strict) { if (m_Alias != null) return m_Alias.GetChild(key, strict); if (m_ChildType == ChildType.MAPPED) { if (m_MappedChilds.ContainsKey(key.ToLower())) return m_MappedChilds[key.ToLower()]; if (strict) throw new System.Exception( string.Format("cannot find specified child node {0} at {1}", key, m_Key) ); return null; } else if (m_ChildType == ChildType.SEQNENTIAL) { foreach (YamlTree child in m_SequentialChilds) if (child.Key == key) return child; if (strict) throw new System.Exception( string.Format("cannot find specified child node {0} at {1}", key, m_Key) ); return null; } if (strict) throw new System.Exception(string.Format("{0} node has invalid child", m_Key)); return null; } // 해당하는 자식 노드를 반환한다. public YamlTree GetChild(int i, bool strict) { if (m_Alias != null) return m_Alias.GetChild(i, strict); if (i < m_SequentialChilds.Count) return m_SequentialChilds[i]; if (strict) throw new System.Exception( string.Format("out of range in node {0} with {1}", m_Key, i) ); return null; } // 자식 노드의 값을 설정한다. public YamlTree AddAttr(string key, string value) { return AddChild(new YamlTree(key, value)); } public YamlTree AddAttr(string key, int value) { return AddChild(new YamlTree(key, value)); } public YamlTree AddAttr(string key, float value) { return AddChild(new YamlTree(key, value)); } public YamlTree AddAttr(string key, double value) { return AddChild(new YamlTree(key, value)); } public YamlTree AddAttr(string key, bool value) { return AddChild(new YamlTree(key, value)); } // 자식 노드의 값을 반환한다. public string AttrAsString(string key) { YamlTree child = GetChild(key, true); return child != null ? child.GetValueAsString() : ""; } public int AttrAsInt(string key) { YamlTree child = GetChild(key, true); return child != null ? child.GetValueAsInt() : 0; } public float AttrAsFloat(string key) { YamlTree child = GetChild(key, true); return child != null ? child.GetValueAsFloat() : 0.0f; } public double AttrAsDouble(string key) { YamlTree child = GetChild(key, true); return child != null ? child.GetValueAsFloat() : 0.0; } public bool AttrAsBool(string key) { YamlTree child = GetChild(key, true); return child != null ? child.GetValueAsBoolean() : false; } // 자식 노드의 값을 안전하게(-_-) 반환한다. public string AttrAsStringSafe(string key, string nullValue) { YamlTree child = GetChild(key, false); return child != null ? child.GetValueAsString() : nullValue; } public int AttrAsIntSafe(string key, int nullValue) { YamlTree child = GetChild(key, false); return child != null ? child.GetValueAsInt() : nullValue; } public float AttrAsFloatSafe(string key, float nullValue) { YamlTree child = GetChild(key, false); return child != null ? child.GetValueAsFloat() : nullValue; } public double AttrAsDoubleSafe(string key, double nullValue) { YamlTree child = GetChild(key, false); return child != null ? child.GetValueAsFloat() : nullValue; } public bool AttrAsBoolSafe(string key, bool nullValue) { YamlTree child = GetChild(key, false); return child != null ? child.GetValueAsBoolean() : nullValue; } // 문자열화한다. public override string ToString() { System.Text.StringBuilder msg = new System.Text.StringBuilder(); if (m_Comment.Length > 0) msg.AppendFormat("{0}\n", m_Comment); bool mapped = m_ChildType == ChildType.MAPPED; foreach (YamlTree child in m_SequentialChilds) { string result = child.ToString(0, mapped, "", m_MaxChildKeyLength); msg.AppendFormat("{0}", result); } return msg.ToString(); } // 문자열화한다. private string ToString(int indent, bool mapped, string header, int maxKeyLength) { System.Text.StringBuilder msg = new System.Text.StringBuilder(); string spaces = ""; for (int i=0; i<indent; ++i) spaces = spaces + " "; if (m_Comment.Length > 0) { string comment = m_Comment; comment = comment.Replace("\n", "\n" + spaces); string trimmed = comment.Trim(); if (trimmed.Length > 0 && trimmed[trimmed.Length-1] == '\n') msg.AppendFormat("{0}{1}", spaces, comment); else msg.AppendFormat("{0}{1}\n", spaces, comment); } if (mapped) { msg.AppendFormat("{0}{1}{2}:", spaces, header, m_Key); string value = m_Value; switch (m_ScalarType) { case ScalarType.NORMAL: msg.AppendFormat(" {0}\n", value); break; case ScalarType.BLOCK: msg.AppendFormat(" |\n"); foreach (string text in m_Scalars) msg.AppendFormat("{0} {1}\n", spaces, text); break; case ScalarType.FOLDED: msg.AppendFormat(" >\n"); foreach (string text in m_Scalars) msg.AppendFormat("{0} {1}\n", spaces, text); break; default: msg.AppendFormat("{0}{1}: {2}\n", spaces, m_Key, value); break; } if (m_ChildType == ChildType.MAPPED) { foreach (YamlTree child in m_SequentialChilds) { if (header.Length == 0) msg.Append(child.ToString(indent + 2, true, "", m_MaxChildKeyLength)); else msg.Append(child.ToString(indent + 3, true, "", m_MaxChildKeyLength)); } } else if (m_ChildType == ChildType.SEQNENTIAL) { foreach (YamlTree child in m_SequentialChilds) { msg.Append(child.ToString(indent + 1, false, "", m_MaxChildKeyLength)); } } } else { bool first = true; bool childMapped = m_ChildType == ChildType.MAPPED; foreach (YamlTree child in m_SequentialChilds) { if (first) { msg.Append(child.ToString(indent, childMapped, "- ", m_MaxChildKeyLength)); first = false; } else { msg.Append(child.ToString(indent + 1, childMapped, "", m_MaxChildKeyLength)); } } } return msg.ToString(); } // 모든 노드 삭제 public void Clear() { if (m_SequentialChilds.Count != m_MappedChilds.Count) throw new System.Exception( string.Format("child count mismatch at node {0}", m_Key) ); m_Key = ""; m_Value = ""; m_Comment = ""; m_ScalarType = ScalarType.NORMAL; m_Scalars.Clear(); m_Alias = null; m_ChildType = ChildType.UNKNOWN; m_MappedChilds.Clear(); m_SequentialChilds.Clear(); m_MaxChildKeyLength = 0; } } // 파싱 중에 내부적으로 사용하기 위한 구조체 internal struct STATE { public YamlTree node; public int indent; public STATE(YamlTree n, int i) { node = n; indent = i; } }; // 위에서 선언한 구조체를 이용한 스택 internal class StateStack : System.Collections.Generic.Stack<STATE> { }; /// <summary> /// 형식화된 파일 입출력을 위한 스트림 객체. 라인 단위로 파일을 읽어들이는 /// 기능 외에 마지막으로 읽어들인 문자열을 이용해 다음 라인을 어떻게 /// 처리하느냐를 판단하는 기능도 담당한다. /// </summary> internal class YamlStream { // 상수들 private const string YAML_WHITESPACES = " \t\r\n"; private const string YAML_LINEFEEDS = "\r\n"; private const string YAML_INDENT = " \t"; private const char YAML_COLON = ':'; private const char YAML_SHARP = '#'; private const char YAML_MINUS = '-'; private const char YAML_AMPERSAND = '&'; private const char YAML_ASTERISK = '*'; private const string YAML_PIPE = "|"; private const string YAML_RIGHT_BRACKET = ">"; private const string YAML_DOCUMENT_HEADER = "---"; private const string YAML_DOCUMENT_TERMINATOR = "..."; // 파싱 결과 public enum ParseResult { NORMAL, EMPTY }; // 현재 파싱 모드 public enum ParseMode { NORMAL, BLOCK, FOLDED }; // 현재 인덴트 값을 기준으로 스택에서 정확한 부모를 찾아, 주어진 노드를 // 자식 노드로 편입시킨다. public static YamlTree AdoptChild(StateStack stack, int indent, YamlTree child) { while (stack.Count > 0) { STATE e = stack.Peek(); if (e.indent == indent) return e.node.AddChild(child); else stack.Pop(); } return null; } // C++ STL string::find_first_not_of public static int IndexNotOf(string text, string delimiters, int startIndex) { int index = startIndex; while (index < text.Length) { if (delimiters.IndexOf(text[index]) == -1) return index; index++; } return -1; } // C++ STL string::find_last_not_of public static int LastIndexNotOf(string text, string delimiters, int startIndex) { int index = startIndex; int foundIdx = -1; while (index < text.Length) { if (delimiters.IndexOf(text[index]) == -1) foundIdx = index; index++; } return foundIdx; } // instance variables System.IO.StreamReader m_File = null; YamlDocument m_Document = null; int m_Line = 0; int m_Indent = 0; string m_Text = ""; string m_Comment = ""; ParseMode m_Mode = ParseMode.NORMAL; // properties public ParseMode Mode { get { return m_Mode; } } public int Line { get { return m_Line; } } public int Indent { get { return m_Indent; } } public string Text { get { return m_Text; } } // costructor public YamlStream(string fileName, YamlDocument document) { m_File = new System.IO.StreamReader(fileName, document.Encoding); m_Document = document; } // 한 라인을 읽어들여 파싱한다. public ParseResult ParseNextLine(bool processComment) { ++m_Line; m_Text = m_File.ReadLine(); if (m_Text.Length == 0) return ParseResult.EMPTY; // empty line int pos = IndexNotOf(m_Text, YAML_INDENT, 0); if (pos < 0) { m_Comment += "\n"; return ParseResult.EMPTY; // only whitespace } if (processComment && m_Text[pos] == YAML_SHARP) { m_Comment += m_Text.Substring(pos) + "\n"; return ParseResult.EMPTY; // comment line } m_Indent = pos; return ParseResult.NORMAL; } // 현재 라인의 텍스트를 이용해 트리 객체를 생성한다. public YamlTree CreateTree() { string text = m_Text; int pos = text.IndexOf(YAML_COLON); if (pos < 0) throw new System.Exception("cannot find colon character"); string key = text.Substring(0, pos).Trim(); string value = text.Substring(pos + 1).Trim(); if (key.Length == 0) throw new System.Exception("key text is empty"); YamlTree result = null; if (key[0] == YAML_MINUS) { m_Indent += IndexNotOf(key, YAML_INDENT, 1); key = key.Substring(1).Trim(); result = new YamlTree("", ""); result.AddChild(new YamlTree(key, value)); } else { result = new YamlTree(key, value); } if (m_Comment.Length > 0) { result.SetComment(m_Comment.Substring(0, m_Comment.Length - 1)); m_Comment = ""; } if (value.Length > 1) { if (value[0] == YAML_AMPERSAND) { m_Document.AddAnchor(value.Substring(1).Trim(), result); } else if (value[0] == YAML_ASTERISK) { string trimmed = value.Substring(1); YamlTree anchor = m_Document.GetAnchor(trimmed); if (anchor == null) throw new System.Exception("no such anchor " + trimmed); result.Alias = anchor; } } if (value == YAML_PIPE) { m_Mode = ParseMode.BLOCK; result.SetValue(""); } else if (value == YAML_RIGHT_BRACKET) { m_Mode = ParseMode.FOLDED; result.SetValue(""); } else { m_Mode = ParseMode.NORMAL; } return result; } // 스트림의 상태가 정상적인가의 여부 public bool IsGood() { return m_File.EndOfStream == false; } // 현재 라인이 빈 라인인지의 여부 public bool IsEmptyLine() { string trimmed = m_Text.Trim(); return trimmed.Length == 0 || trimmed[0] == YAML_SHARP; } }; /// <summary> /// YamlTree 최상위 노드. /// /// 실제로 파일 입출력을 다룰 때에는 이 클래스를 이용해야 한다. 이와 같은 /// 클래스가 필요한 이유는 ANCHOR 정보가 문서 전체를 통해 전역으로 존재하기 /// 때문이다. Load/Save 함수도 어차피 이와 같은 클래스를 따로 두고, 이 안에 /// 넣는 것이 깔끔하기는 하다. /// /// Anchor & alias 같은 경우, 파일을 읽어들일 때, 즉 이미 존재하는 /// anchor & alias 정보를 읽어들이는 것은 별 문제가 없다. 문제는 그 반대, /// 즉 메모리 상에서 트리 구조를 구성한 후, 파일에다 쓸 때는 인터페이스를 /// 어떻게 제공해야 할지를 잘 모르겠다. /// </summary> public class YamlDocument : YamlTree { // typedef class Anchors : System.Collections.Generic.Dictionary<string,YamlTree> {}; // instance variables System.Text.Encoding m_Encoding = null; Anchors m_Anchors = new Anchors(); string m_LastError = ""; // properties public System.Text.Encoding Encoding { get { return m_Encoding; } set { m_Encoding = value; } } public string LastError { get { return m_LastError; } } // anchors public void AddAnchor(string name, YamlTree tree) { m_Anchors.Add(name.ToLower(), tree); } public YamlTree GetAnchor(string name) { return m_Anchors.ContainsKey(name.ToLower()) ? m_Anchors[name.ToLower()] : null; } public void ClearAnchors() { m_Anchors.Clear(); } // 생성자 public YamlDocument() : base() { m_Key = "<ROOT>"; m_Encoding = System.Text.Encoding.Default; } // 생성자 public YamlDocument(System.Text.Encoding encoding) : base() { m_Key = "<ROOT>"; m_Encoding = encoding; } // 불러오기 public bool Load(string fileName) { Clear(); ClearAnchors(); YamlStream stream = new YamlStream(fileName, this); if (!stream.IsGood()) { m_LastError = "cannot open " + fileName; return false; } StateStack stack = new StateStack(); stack.Push(new STATE(this, -1)); bool result = true; YamlStream.ParseMode mode = YamlStream.ParseMode.NORMAL; YamlTree lastChild = null; try { while (stream.IsGood()) { if (stack.Count == 0) throw new System.Exception("indent error"); bool normal = mode == YamlStream.ParseMode.NORMAL; if (stream.ParseNextLine(normal) != YamlStream.ParseResult.NORMAL) continue; int indent = stream.Indent; STATE top = stack.Peek(); if (top.indent == -1) { stack.Pop(); stack.Push(new STATE(top.node, indent)); top = stack.Peek(); } if (mode == YamlStream.ParseMode.NORMAL) { if (top.indent == indent) { if (top.node.Alias != null) throw new System.Exception( "cannot add child node to aliased node " + top.node.Key ); YamlTree child = stream.CreateTree(); if (child.ChildCount == 0 || child.Alias != null) { lastChild = top.node.AddChild(child); } else { top.node.AddChild(child); stack.Push(new STATE(child, stream.Indent)); lastChild = child.GetChild(0, true); } } else if (top.indent < indent) { if (lastChild == null) throw new System.Exception("indent error"); if (lastChild.Alias != null) throw new System.Exception( "cannot add child node to aliased node " + lastChild.Key ); stack.Push(new STATE(lastChild, indent)); YamlTree child = stream.CreateTree(); if (child.ChildCount == 0 || child.Alias != null) { lastChild = lastChild.AddChild(child); } else { stack.Push(new STATE(child, stream.Indent)); lastChild.AddChild(child); lastChild = child.GetChild(0, true); } } else if (indent < top.indent) { YamlTree child = stream.CreateTree(); if (child.ChildCount == 0 || child.Alias != null) { lastChild = YamlStream.AdoptChild(stack, indent, child); } else { YamlStream.AdoptChild(stack, indent, child); stack.Push(new STATE(child, stream.Indent)); lastChild = child.GetChild(0, true); } } mode = stream.Mode; } else { if (top.indent == indent) { if (stream.IsEmptyLine()) continue; YamlTree child = stream.CreateTree(); lastChild = top.node.AddChild(child); mode = stream.Mode; } else if (top.indent < indent) { string trimmed = stream.Text.Trim(); if (mode == YamlStream.ParseMode.BLOCK) { lastChild.SetValue(lastChild.GetValueAsString() + "\n" + trimmed); lastChild.SetScalarType(YamlTree.ScalarType.BLOCK); lastChild.AddScalar(trimmed); } else if (mode == YamlStream.ParseMode.FOLDED) { lastChild.SetValue(lastChild.GetValueAsString() + " " + trimmed); lastChild.SetScalarType(YamlTree.ScalarType.FOLDED); lastChild.AddScalar(trimmed); } } else if (indent < top.indent) { if (stream.IsEmptyLine()) continue; lastChild = YamlStream.AdoptChild(stack, indent, stream.CreateTree()); mode = stream.Mode; } } } } catch (System.Exception e) { m_LastError = string.Format("{0}:{1} {2}", fileName, stream.Line, e.ToString()); result = false; } return result; } // 저장 public bool Save(string fileName) { System.IO.StreamWriter file = new System.IO.StreamWriter(fileName, false, m_Encoding); string msg = ToString(); file.WriteLine(msg); file.Close(); return true; } }; }