|
1 | 1 | 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 | + |
2 | 16 | # Parses INI-style configuration from the given string.
|
| 17 | + # Raises a `ParseException` on any errors. |
3 | 18 | #
|
4 | 19 | # ```
|
5 | 20 | # INI.parse("[foo]\na = 1") # => {"foo" => {"a" => "1"}}
|
6 | 21 | # ```
|
7 | 22 | 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 |
9 | 26 |
|
10 |
| - section = "" |
11 | 27 | 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 |
18 | 52 | end
|
19 | 53 | end
|
| 54 | + |
| 55 | + ini.delete_if { |_, v| v.empty? } |
20 | 56 | ini
|
21 | 57 | end
|
22 | 58 | end
|
0 commit comments