From d2f1453101ba390dd6b8425bb1c3956f162524ce Mon Sep 17 00:00:00 2001 From: RockfordWei Date: Thu, 14 Dec 2017 16:06:26 -0500 Subject: [PATCH] Rewriting everything. --- Package.swift | 19 ++- README.md | 15 +- README.zh_CN.md | 15 +- Sources/INIParser.swift | 156 ------------------- Sources/INIParser/INIParser.swift | 174 ++++++++++++++++++++++ Tests/INIParserTests/INIParserTests.swift | 42 ++++-- 6 files changed, 226 insertions(+), 195 deletions(-) delete mode 100644 Sources/INIParser.swift create mode 100644 Sources/INIParser/INIParser.swift diff --git a/Package.swift b/Package.swift index 1df78f4..5e5e1bf 100644 --- a/Package.swift +++ b/Package.swift @@ -1,3 +1,5 @@ +// swift-tools-version:4.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. // // Package.swift // Perfect-INIParser @@ -20,5 +22,20 @@ import PackageDescription let package = Package( - name: "INIParser" + name: "INIParser", + products: [ + .library( + name: "INIParser", + targets: ["INIParser"]), + ], + dependencies: [ + ], + targets: [ + .target( + name: "INIParser", + dependencies: []), + .testTarget( + name: "INIParserTests", + dependencies: ["INIParser"]), + ] ) diff --git a/README.md b/README.md index 0fd0e3a..ac9a658 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@

Swift 4.0 - Swift 3.1 Platforms OS X | Linux @@ -42,22 +41,14 @@ This project provides an express parser for [INI](https://en.wikipedia.org/wiki/INI_file) files. -This package builds with Swift Package Manager of Swift 3.1 Tool Chain and is part of the [Perfect](https://github.com/PerfectlySoft/Perfect) project but can be used as an independent module. +This package builds with Swift Package Manager of Swift 4 Tool Chain and is part of the [Perfect](https://github.com/PerfectlySoft/Perfect) project but can be used as an independent module. ## Quick Start Configure Package.swift: -For Swift 3.1: - -``` swift -.Package(url: "https://github.com/PerfectlySoft/Perfect-INIParser.git", majorVersion: 1) -``` - -For Swift 4.0: - ``` swift -.package(url: "https://github.com/PerfectlySoft/Perfect-INIParser.git", from: "1.0.0") +.package(url: "https://github.com/PerfectlySoft/Perfect-INIParser.git", from: "3.0.0") ... @@ -89,7 +80,7 @@ For most regular lines under a certain section, use `sections` attribute of `INI myVariable = myValue ``` -Then `let v = ini.sections["[GroupA]"]?["myVariable"]` will get the value as `"myValue"`. +Then `let v = ini.sections["GroupA"]?["myVariable"]` will get the value as `"myValue"`. ### Variables without Section diff --git a/README.zh_CN.md b/README.zh_CN.md index 794a621..ca8654c 100644 --- a/README.zh_CN.md +++ b/README.zh_CN.md @@ -24,7 +24,6 @@

Swift 4.0 - Swift 3.1 Platforms OS X | Linux @@ -42,22 +41,14 @@ 本项目是一个简单的[INI文件](http://baike.baidu.com/item/ini文件)解析器。 -本项目采用Swift 3.1 工具链中的SPM软件包管理器编译,是[Perfect](https://github.com/PerfectlySoft/Perfect) 项目的一部分,但也可以作为独立模块使用。 +本项目采用Swift 4 工具链中的SPM软件包管理器编译,是[Perfect](https://github.com/PerfectlySoft/Perfect) 项目的一部分,但也可以作为独立模块使用。 ## 快速上手 配置 Package.swift 文件: -如果使用 Swift 3.1: - -``` swift -.Package(url: "https://github.com/PerfectlySoft/Perfect-INIParser.git", majorVersion: 1) -``` - -如果使用 Swift 4.0: - ``` swift -.package(url: "https://github.com/PerfectlySoft/Perfect-INIParser.git", from: "1.0.0") +.package(url: "https://github.com/PerfectlySoft/Perfect-INIParser.git", from: "3.0.0") ... @@ -89,7 +80,7 @@ let ini = try INIParser("/path/to/somefile.ini") myVariable = myValue ``` -此时使用语句 `let v = ini.sections["[GroupA]"]?["myVariable"]` 可以得到字符串值 `"myValue"`. +此时使用语句 `let v = ini.sections["GroupA"]?["myVariable"]` 可以得到字符串值 `"myValue"`. ### 无章节变量 diff --git a/Sources/INIParser.swift b/Sources/INIParser.swift deleted file mode 100644 index 6c10c01..0000000 --- a/Sources/INIParser.swift +++ /dev/null @@ -1,156 +0,0 @@ -// -// INIParser.swift -// Perfect-INIParser -// -// Created by Rockford Wei on 2017-04-25. -// Copyright © 2017 PerfectlySoft. All rights reserved. -// -//===----------------------------------------------------------------------===// -// -// This source file is part of the Perfect.org open source project -// -// Copyright (c) 2017 - 2018 PerfectlySoft Inc. and the Perfect project authors -// Licensed under Apache License v2.0 -// -// See http://perfect.org/licensing.html for license information -// -//===----------------------------------------------------------------------===// -// - -#if os(Linux) - import SwiftGlibc -#else - import Darwin -#endif - -/// INI Configuration File Reader -public class INIParser { - - internal var _sections: [String: [String: String]] = [:] - internal var _anonymousSection: [String: String] = [:] - - public var sections: [String:[String:String]] { get { return _sections } } - public var anonymousSection: [String: String] { get { return _anonymousSection } } - - public enum Exception: Error { - case InvalidFile, IncompleteReading, RegexFault - }//end enum - - private var reg_sec = regex_t() - - private func trim(_ word: String) -> String { - var buf = [UInt8]() - var trimming = true - for c in word.utf8 { - if trimming && c < 33 { continue } - trimming = false - buf.append(c) - }//end ltrim - while let last = buf.last, last < 33 { - buf.removeLast() - }//end rtrim - buf.append(0) - return String(cString: buf) - }//end trim - - internal func parseSection(_ line: String) -> String? { - var p = regmatch_t() - guard 0 == regexec(®_sec, line, 1, &p, 0), let s = strdup(line) else { - return nil - }//end guard - s.advanced(by: Int(p.rm_eo)).pointee = 0 - let section = trim(String(cString: s.advanced(by: Int(p.rm_so)))) - free(s) - return section - }//end parseSection - - internal func parseEquation(_ line: String) -> (key:String, value: String)? { - if let word = strdup(line) { - if let eq = strchr(word, 61) { - if eq == word { - free(word) - return nil - }//end if - eq.pointee = 0 - let key = trim(String(cString: word)) - let value = trim(String(cString: eq.advanced(by: 1))) - free(word) - if key.isEmpty || value.isEmpty { - return nil - }else{ - return (key: key, value: value) - }//end if - }//end if - free(word) - }//end if - return nil - }//end parseEquation - - internal func decommented(line: String) -> String { - - guard let str = strdup(line) else { return line } - - if let a = strchr(str, 59) { - a.pointee = 0 - }//end if - - if let b = strchr(str, 35) { - b.pointee = 0 - }//end if - - let ret = String(cString: str) - - free(str) - - return ret - }//end func - - deinit { - regfree(®_sec) - }//end deinit - - /// Constructor - /// - parameters: - /// - path: path of INI file to load - /// - throws: - /// Exception - public init(_ path: String) throws { - guard 0 == regcomp(®_sec, "\\[(.*)\\]", REG_EXTENDED) else { - throw Exception.RegexFault - } - var st = stat() - let r = lstat(path, &st) - let size = Int(st.st_size) - guard r == 0, size > 0, let f = fopen(path, "r") else { - throw Exception.InvalidFile - } - let buf = UnsafeMutablePointer.allocate(capacity: size + 1) - let count = fread(buf, 1, size, f) - guard count == size else { - buf.deallocate(capacity: size + 1) - throw Exception.IncompleteReading - }//end guard - buf.advanced(by: size).pointee = 0 - let content = String(cString: buf) - buf.deallocate(capacity: size + 1) - let lines:[String] = content.utf8.split(separator: 10) - .map { String($0) ?? "" } - .map { decommented(line: trim(String(describing: $0))) } - .filter { !$0.isEmpty } - var section = "" - for line in lines { - if let title = parseSection(line) { - section = title - continue - }else if let eq = parseEquation(line) { - if section.isEmpty { - _anonymousSection[eq.key] = eq.value - }else { - var sec = _sections[section] ?? [:] - sec[eq.key] = eq.value - _sections[section] = sec - }//end if - }//end if - }//next - }//end init -}//end INIReader diff --git a/Sources/INIParser/INIParser.swift b/Sources/INIParser/INIParser.swift new file mode 100644 index 0000000..d3ab0cc --- /dev/null +++ b/Sources/INIParser/INIParser.swift @@ -0,0 +1,174 @@ +// +// INIParser.swift +// Perfect-INIParser +// +// Created by Rockford Wei on 2017-04-25. +// Copyright © 2017 PerfectlySoft. All rights reserved. +// +//===----------------------------------------------------------------------===// +// +// This source file is part of the Perfect.org open source project +// +// Copyright (c) 2017 - 2018 PerfectlySoft Inc. and the Perfect project authors +// Licensed under Apache License v2.0 +// +// See http://perfect.org/licensing.html for license information +// +//===----------------------------------------------------------------------===// +// + +import Foundation + +public class Stack { + internal var array: [T] = [] + public func push(_ element: T) { + array.append(element) + } + public func pop () -> T? { + if array.isEmpty { return nil } + let element = array.removeLast() + return element + } + public func top () -> T? { + return array.last + } + public var isEmpty: Bool { return array.isEmpty } +} + +/// INI Configuration File Reader +public class INIParser { + + internal var _sections: [String: [String: String]] = [:] + internal var _anonymousSection: [String: String] = [:] + + public var sections: [String:[String:String]] { return _sections } + public var anonymousSection: [String: String] { return _anonymousSection } + + public enum Exception: Error { + case InvalidSyntax, InvalidFile + } + + enum State { + case Title, Variable, Value, SingleQuotation, DoubleQuotation + } + + enum ContentType { + case Section(String) + case Assignment(String, String) + } + + internal func parse(line: String) throws -> ContentType? { + var cache = "" + var state = State.Variable + let stack = Stack() + + var variable: String? = nil + for c in line { + switch c { + case " ", "\t": + if state == .SingleQuotation || state == .DoubleQuotation { + cache.append(c) + } + break + case "[": + if state == .Variable { + cache = "" + stack.push(state) + state = .Title + } + break + case "]": + if state == .Title { + guard let last = stack.pop() else { throw Exception.InvalidSyntax } + state = last + return ContentType.Section(cache) + } + break + case "=": + if state == .Variable { + variable = cache + cache = "" + state = .Value + } + break + case "#", ";": + if state == .Value { + if let v = variable { + return ContentType.Assignment(v, cache) + } else { + throw Exception.InvalidSyntax + } + } else { + return nil + } + case "\"": + if state == .DoubleQuotation { + guard let last = stack.pop() else { + throw Exception.InvalidSyntax + } + state = last + } else { + stack.push(state) + state = .DoubleQuotation + } + cache.append(c) + break + case "\'": + if state == .SingleQuotation { + guard let last = stack.pop() else { + throw Exception.InvalidSyntax + } + state = last + } else { + stack.push(state) + state = .SingleQuotation + } + cache.append(c) + break + default: + cache.append(c) + } + } + guard state == .Value, let v = variable else { + throw Exception.InvalidSyntax + } + return ContentType.Assignment(v, cache) + } + /// Constructor + /// - parameters: + /// - path: path of INI file to load + /// - throws: + /// Exception + public init(_ path: String) throws { + let data = try Data(contentsOf: URL(fileURLWithPath: path)) + guard let text = String(bytes: data, encoding: .utf8) else { + throw Exception.InvalidFile + } + let lines: [String] = text.split(separator: "\n").map { String($0) } + var title: String? = nil + for line in lines { + if let content = try parse(line: line) { + debugPrint(content) + switch content { + case .Section(let newTitle): + title = newTitle + break + case .Assignment(let variable, let value): + if let currentTitle = title { + if var sec = _sections[currentTitle] { + sec[variable] = value + _sections[currentTitle] = sec + } else { + var sec: [String: String] = [:] + sec[variable] = value + _sections[currentTitle] = sec + } + } else { + _anonymousSection[variable] = value + } + break + } + } + } + } +} diff --git a/Tests/INIParserTests/INIParserTests.swift b/Tests/INIParserTests/INIParserTests.swift index f2874db..93f0bb2 100644 --- a/Tests/INIParserTests/INIParserTests.swift +++ b/Tests/INIParserTests/INIParserTests.swift @@ -22,25 +22,39 @@ import XCTest class INIParserTests: XCTestCase { func testExample() { - let raw = "; last modified 1 April 2017 by Rockford Wei \t \n ## This is another comment \n freeVar1 = 1 \n freeVar2 = 2; \n [owner] \n " + - "name = Rocky \n organization = PerfectlySoft \n ; \n [database] \n " + - "\t\t server = 192.0.2.42 ; use IP address in case network name resolution is not working \n \n\n\n " + - " port = 143 \n file = \"中文.dat\" \n [汉化] \n 变量1 = 🇨🇳 ;使用utf8 \n 变量2 = 加拿大。 ~ \n [ bad sec \n even worse ] \n [ 乱死了 ] \n = 没变量 \n novalue = \n" + let raw = """ +; last modified 1 April 2017 by Rockford Wei +## This is another comment + freeVar1 = 1 + freeVar2 = 2; + [owner] + name = Rocky + organization = PerfectlySoft + ; + [database] + server = 192.0.2.42 ; use IP address in case network name resolution is not working + + port = 143 + file = \"中文.dat ' ' \" + [汉化] + 变量1 = 🇨🇳 ;使用utf8 + 变量2 = 加拿大。 + [ 乱死了 ] +""" + let path = "/tmp/a.ini" - let f = fopen(path, "w") - fwrite(raw, 1, raw.utf8.count, f) - fclose(f) do { + try raw.write(to: URL.init(fileURLWithPath: path), atomically: true, encoding: .utf8) let ini = try INIParser(path) XCTAssertEqual(ini.anonymousSection["freeVar1"] ?? "", "1") XCTAssertEqual(ini.anonymousSection["freeVar2"] ?? "", "2") - XCTAssertEqual(ini.sections["[owner]"]?["name"] ?? "", "Rocky") - XCTAssertEqual(ini.sections["[owner]"]?["organization"] ?? "", "PerfectlySoft") - XCTAssertEqual(ini.sections["[database]"]?["server"] ?? "", "192.0.2.42") - XCTAssertEqual(ini.sections["[database]"]?["port"] ?? "", "143") - XCTAssertEqual(ini.sections["[database]"]?["file"] ?? "", "\"中文.dat\"") - XCTAssertEqual(ini.sections["[汉化]"]?["变量1"] ?? "", "🇨🇳") - XCTAssertEqual(ini.sections["[汉化]"]?["变量2"] ?? "", "加拿大。 ~") + XCTAssertEqual(ini.sections["owner"]?["name"] ?? "", "Rocky") + XCTAssertEqual(ini.sections["owner"]?["organization"] ?? "", "PerfectlySoft") + XCTAssertEqual(ini.sections["database"]?["server"] ?? "", "192.0.2.42") + XCTAssertEqual(ini.sections["database"]?["port"] ?? "", "143") + XCTAssertEqual(ini.sections["database"]?["file"] ?? "", "\"中文.dat \' \' \"") + XCTAssertEqual(ini.sections["汉化"]?["变量1"] ?? "", "🇨🇳") + XCTAssertEqual(ini.sections["汉化"]?["变量2"] ?? "", "加拿大。") }catch (let err) { XCTFail(err.localizedDescription) }