Skip to content

Commit

Permalink
INI: Rewrite parser to avoid regular expressions
Browse files Browse the repository at this point in the history
`INI.parse` now parses an INI-formatted string
into a `Hash` without using any regular expressions.
  • Loading branch information
woodruffw committed Dec 23, 2017
1 parent 3a5fde0 commit 5d1b114
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 7 deletions.
34 changes: 34 additions & 0 deletions spec/std/ini_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,48 @@ require "ini"

describe "INI" do
describe "parse from string" do
it "fails on malformed section" do
expect_raises(INI::ParseException, "unterminated section") do
INI.parse("[section")
end
end

it "fails on data after section" do
expect_raises(INI::ParseException, "data after section") do
INI.parse("[section] foo ")
end

expect_raises(INI::ParseException, "data after section") do
INI.parse("[]foo")
end
end

it "fails on malformed declaration" do
expect_raises(INI::ParseException, "expected declaration") do
INI.parse("foobar")
end

expect_raises(INI::ParseException, "expected declaration") do
INI.parse("foo: bar")
end
end

it "parses key = value" do
INI.parse("key = value").should eq({"" => {"key" => "value"}})
end

it "parses empty values" do
INI.parse("key = ").should eq({"" => {"key" => ""}})
end

it "ignores whitespaces" do
INI.parse(" key = value ").should eq({"" => {"key" => "value"}})
end

it "ignores comments" do
INI.parse("; foo\n# bar\nkey = value").should eq({"" => {"key" => "value"}})
end

it "parses sections" do
INI.parse("[section]\na = 1").should eq({"section" => {"a" => "1"}})
end
Expand Down
51 changes: 44 additions & 7 deletions src/ini.cr
Original file line number Diff line number Diff line change
@@ -1,22 +1,59 @@
class INI
# Exception thrown on an INI parse error.
class ParseException < Exception
getter line_number : Int32
getter column_number : Int32

def initialize(message, @line_number, @column_number)
super "#{message} at #{@line_number}:#{@column_number}"
end

def location
{line_number, column_number}
end
end

# Parses INI-style configuration from the given string.
# Raises a `ParseException` on any errors.
#
# ```
# INI.parse("[foo]\na = 1") # => {"foo" => {"a" => "1"}}
# ```
def self.parse(str) : Hash(String, Hash(String, String))
ini = {} of String => Hash(String, String)
current_section_name = ""
lineno = 0

section = ""
str.each_line do |line|
if line =~ /\s*(.*[^\s])\s*=\s*(.*[^\s])/
ini[section] ||= {} of String => String if section == ""
ini[section][$1] = $2
elsif line =~ /\[(.*)\]/
section = $1
ini[section] = {} of String => String
lineno += 1
next if line.empty?

offset = 0
line.each_char do |char|
break unless char.ascii_whitespace?
offset += 1
end

case line[offset]
when '#', ';'
next
when '['
end_idx = line.index(']', offset)
raise ParseException.new("unterminated section", lineno, line.size) unless end_idx
raise ParseException.new("data after section", lineno, end_idx + 1) unless end_idx == line.size - 1

current_section_name = line[1...end_idx]
ini[current_section_name] = {} of String => String
else
key, eq, value = line.partition('=')
raise ParseException.new("expected declaration", lineno, key.size) if eq != "="

section = ini[current_section_name]? || Hash(String, String).new
section[key.strip] = value.strip
ini[current_section_name] = section
end
end

ini
end
end

0 comments on commit 5d1b114

Please sign in to comment.