Skip to content

Commit 68a7ce8

Browse files
woodruffwRX14
authored andcommittedJan 2, 2018
INI: Rewrite parser to avoid regular expressions (#5442)
`INI.parse` now parses an INI-formatted string into a `Hash` without using any regular expressions.
1 parent 1eeb5c0 commit 68a7ce8

File tree

2 files changed

+82
-11
lines changed

2 files changed

+82
-11
lines changed
 

‎spec/std/ini_spec.cr

+38-3
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,58 @@ require "ini"
33

44
describe "INI" do
55
describe "parse from string" do
6+
it "fails on malformed section" do
7+
expect_raises(INI::ParseException, "unterminated section") do
8+
INI.parse("[section")
9+
end
10+
end
11+
12+
it "fails on data after section" do
13+
expect_raises(INI::ParseException, "data after section") do
14+
INI.parse("[section] foo ")
15+
end
16+
end
17+
18+
it "fails on malformed declaration" do
19+
expect_raises(INI::ParseException, "expected declaration") do
20+
INI.parse("foobar")
21+
end
22+
23+
expect_raises(INI::ParseException, "expected declaration") do
24+
INI.parse("foo: bar")
25+
end
26+
end
27+
628
it "parses key = value" do
729
INI.parse("key = value").should eq({"" => {"key" => "value"}})
830
end
931

32+
it "parses empty values" do
33+
INI.parse("key = ").should eq({"" => {"key" => ""}})
34+
end
35+
1036
it "ignores whitespaces" do
1137
INI.parse(" key = value ").should eq({"" => {"key" => "value"}})
38+
INI.parse(" [foo]").should eq({} of String => Hash(String, String))
39+
end
40+
41+
it "ignores comments" do
42+
INI.parse("; foo\n# bar\nkey = value").should eq({"" => {"key" => "value"}})
1243
end
1344

1445
it "parses sections" do
1546
INI.parse("[section]\na = 1").should eq({"section" => {"a" => "1"}})
1647
end
1748

18-
it "empty section" do
19-
INI.parse("[section]").should eq({"section" => {} of String => String})
49+
it "parses a reopened section" do
50+
INI.parse("[foo]\na=1\n[foo]\nb=2").should eq({"foo" => {"a" => "1", "b" => "2"}})
51+
end
52+
53+
it "ignores an empty section" do
54+
INI.parse("[section]").should eq({} of String => Hash(String, String))
2055
end
2156

22-
it "parse file" do
57+
it "parses a file" do
2358
INI.parse(File.read "#{__DIR__}/data/test_file.ini").should eq({
2459
"general" => {
2560
"log_level" => "DEBUG",

‎src/ini.cr

+44-8
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,58 @@
11
class INI
2+
# Exception thrown on an INI parse error.
3+
class ParseException < Exception
4+
getter line_number : Int32
5+
getter column_number : Int32
6+
7+
def initialize(message, @line_number, @column_number)
8+
super "#{message} at #{@line_number}:#{@column_number}"
9+
end
10+
11+
def location
12+
{line_number, column_number}
13+
end
14+
end
15+
216
# Parses INI-style configuration from the given string.
17+
# Raises a `ParseException` on any errors.
318
#
419
# ```
520
# INI.parse("[foo]\na = 1") # => {"foo" => {"a" => "1"}}
621
# ```
722
def self.parse(str) : Hash(String, Hash(String, String))
8-
ini = {} of String => Hash(String, String)
23+
ini = Hash(String, Hash(String, String)).new
24+
current_section = ini[""] = Hash(String, String).new
25+
lineno = 0
926

10-
section = ""
1127
str.each_line do |line|
12-
if line =~ /\s*(.*[^\s])\s*=\s*(.*[^\s])/
13-
ini[section] ||= {} of String => String if section == ""
14-
ini[section][$1] = $2
15-
elsif line =~ /\[(.*)\]/
16-
section = $1
17-
ini[section] = {} of String => String
28+
lineno += 1
29+
next if line.empty?
30+
31+
offset = 0
32+
line.each_char do |char|
33+
break unless char.ascii_whitespace?
34+
offset += 1
35+
end
36+
37+
case line[offset]
38+
when '#', ';'
39+
next
40+
when '['
41+
end_idx = line.index(']', offset)
42+
raise ParseException.new("unterminated section", lineno, line.size) unless end_idx
43+
raise ParseException.new("data after section", lineno, end_idx + 1) unless end_idx == line.size - 1
44+
45+
current_section_name = line[offset + 1...end_idx]
46+
current_section = ini[current_section_name] ||= Hash(String, String).new
47+
else
48+
key, eq, value = line.partition('=')
49+
raise ParseException.new("expected declaration", lineno, key.size) if eq != "="
50+
51+
current_section[key.strip] = value.strip
1852
end
1953
end
54+
55+
ini.delete_if { |_, v| v.empty? }
2056
ini
2157
end
2258
end

0 commit comments

Comments
 (0)
Please sign in to comment.