├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── rhcl.rb └── rhcl │ ├── dump.rb │ ├── parse.tab.rb │ ├── parse.y │ └── version.rb ├── rhcl.gemspec └── spec ├── rhcl_dump_spec.rb ├── rhcl_parse_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | test.rb 19 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0.0 4 | script: 5 | - bundle install 6 | - bundle exec rake 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in rhcl.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Genki Sugawara 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rhcl 2 | 3 | Pure Ruby [HCL](https://github.com/hashicorp/hcl) parser 4 | 5 | [![Gem Version](https://badge.fury.io/rb/rhcl.png)](http://badge.fury.io/rb/rhcl) 6 | [![Build Status](https://travis-ci.org/winebarrel/rhcl.svg?branch=master)](https://travis-ci.org/winebarrel/rhcl) 7 | 8 | ## Installation 9 | 10 | Add this line to your application's Gemfile: 11 | 12 | gem 'rhcl' 13 | 14 | And then execute: 15 | 16 | $ bundle 17 | 18 | Or install it yourself as: 19 | 20 | $ gem install rhcl 21 | 22 | ## Usage 23 | 24 | ### Parse 25 | 26 | ```ruby 27 | Rhcl.parse(<<-EOS) 28 | variable "foo" { 29 | default = "bar" 30 | description = "bar" 31 | } 32 | 33 | variable "amis" { 34 | default = { 35 | east = "foo" 36 | } 37 | } 38 | EOS 39 | ``` 40 | 41 | ### Dump 42 | 43 | ```ruby 44 | Rhcl.dump( 45 | {"variable"=> 46 | {"foo"=>{"default"=>"bar", "description"=>"bar"}, 47 | "amis"=>{"default"=>{"east"=>"foo"}}}} 48 | ) 49 | ``` 50 | 51 | ## Contributing 52 | 53 | 1. Fork it ( http://github.com/winebarrel/rhcl/fork ) 54 | 2. Create your feature branch (`git checkout -b my-new-feature`) 55 | 3. Commit your changes (`git commit -am 'Add some feature'`) 56 | 4. Push to the branch (`git push origin my-new-feature`) 57 | 5. Create new Pull Request 58 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new('spec') 5 | task :default => :spec 6 | -------------------------------------------------------------------------------- /lib/rhcl.rb: -------------------------------------------------------------------------------- 1 | module Rhcl 2 | def parse(obj) 3 | Rhcl::Parse.parse(obj) 4 | end 5 | module_function :parse 6 | 7 | def dump(obj) 8 | Rhcl::Dump.dump(obj) 9 | end 10 | module_function :dump 11 | end 12 | 13 | require 'deep_merge' 14 | require 'rhcl/dump' 15 | require 'rhcl/parse.tab' 16 | require 'rhcl/version' 17 | -------------------------------------------------------------------------------- /lib/rhcl/dump.rb: -------------------------------------------------------------------------------- 1 | class Rhcl::Dump 2 | class << self 3 | def dump(obj) 4 | unless obj.kind_of?(Hash) 5 | raise TypeError, "wrong argument type #{obj.class} (expected Hash)" 6 | end 7 | 8 | dump0(obj).sub(/\A\s*\{/, '').sub(/\}\s*\z/, '').strip.gsub(/^\s+$/m, '') 9 | end 10 | 11 | private 12 | def dump0(obj, depth = 0) 13 | prefix = ' ' * depth 14 | prefix0 = ' ' * (depth.zero? ? 0 : depth - 1) 15 | 16 | case obj 17 | when Array 18 | '[' + 19 | obj.map {|i| dump0(i, depth + 1) }.join(', ') + 20 | "]\n" 21 | when Hash 22 | "#{prefix}{\n#{prefix}" + 23 | obj.map {|k, v| 24 | k = k.to_s.strip 25 | k = k.inspect unless k =~ /\A\w+\z/ 26 | k + (v.kind_of?(Hash) ? ' ' : " = ") + dump0(v, depth + 1).strip 27 | }.join("\n#{prefix}") + 28 | "\n#{prefix0}}\n" 29 | when Numeric, TrueClass, FalseClass 30 | obj.inspect 31 | else 32 | obj.to_s.inspect 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/rhcl/parse.tab.rb: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT MODIFY!!!! 3 | # This file is automatically generated by Racc 1.4.12 4 | # from Racc grammer file "". 5 | # 6 | 7 | require 'racc/parser.rb' 8 | 9 | 10 | require 'strscan' 11 | 12 | module Rhcl 13 | class Parse < Racc::Parser 14 | 15 | module_eval(<<'...end parse.y/module_eval...', 'parse.y', 107) 16 | 17 | TRUE_VALUES = %w(true on yes) 18 | FALSE_VALUES = %w(false off no ) 19 | BOOLEAN_VALUES = TRUE_VALUES + FALSE_VALUES 20 | 21 | def initialize(obj) 22 | src = obj.is_a?(IO) ? obj.read : obj.to_s 23 | @ss = StringScanner.new(src) 24 | end 25 | 26 | def scan 27 | tok = nil 28 | @backup = [] 29 | 30 | until @ss.eos? 31 | if (tok = backup { @ss.scan /\s+/ }) 32 | # nothing to do 33 | elsif (tok = backup { @ss.scan /#/ }) 34 | backup { @ss.scan_until /\n/ } 35 | elsif (tok = backup { @ss.scan %r|/| }) 36 | case (tok = backup { @ss.getch }) 37 | when '/' 38 | backup { @ss.scan_until /(\n|\z)/ } 39 | when '*' 40 | nested = 1 41 | 42 | until nested.zero? 43 | case (tok = backup { @ss.scan_until %r{(/\*|\*/|\z)} }) 44 | when %r|/\*\z| 45 | nested += 1 46 | when %r|\*/\z| 47 | nested -= 1 48 | else 49 | break 50 | end 51 | end 52 | else 53 | raise "comment expected, got #{tok.inspect}" 54 | end 55 | elsif (tok = backup { @ss.scan /-?\d+\.\d+/ }) 56 | yield [:FLOAT, tok.to_f] 57 | elsif (tok = backup { @ss.scan /-?\d+/ }) 58 | yield [:INTEGER, tok.to_i] 59 | elsif (tok = backup { @ss.scan /,/ }) 60 | yield [:COMMA, tok] 61 | elsif (tok = backup { @ss.scan /\=/ }) 62 | yield [:EQUAL, tok] 63 | elsif (tok = backup { @ss.scan /\[/ }) 64 | yield [:LEFTBRACKET, tok] 65 | elsif (tok = backup { @ss.scan /\]/ }) 66 | yield [:RIGHTBRACKET, tok] 67 | elsif (tok = backup { @ss.scan /\{/ }) 68 | yield [:LEFTBRACE, tok] 69 | elsif (tok = backup { @ss.scan /\}/ }) 70 | yield [:RIGHTBRACE, tok] 71 | elsif (tok = backup { @ss.scan /"/ }) 72 | yield [:STRING, (backup { @ss.scan_until /("|\z)/ } || '').sub(/"\z/, '')] 73 | else 74 | identifier = (backup { @ss.scan_until /(\s|\z)/ } || '').sub(/\s\z/, '') 75 | token_type = :IDENTIFIER 76 | 77 | if BOOLEAN_VALUES.include?(identifier) 78 | identifier = TRUE_VALUES.include?(identifier) 79 | token_type = :BOOL 80 | end 81 | 82 | yield [token_type, identifier] 83 | end 84 | end 85 | 86 | yield [false, '$end'] 87 | end 88 | private :scan 89 | 90 | def backup 91 | tok = yield 92 | @backup << tok if tok 93 | return tok 94 | end 95 | 96 | def parse 97 | yyparse self, :scan 98 | end 99 | 100 | def on_error(error_token_id, error_value, value_stack) 101 | raise_error(error_value) 102 | end 103 | 104 | def raise_error(error_value) 105 | header = "parse error on value: #{error_value}\n" 106 | error_value = @backup.pop 107 | 108 | if error_value =~ /\n\z/ 109 | error_value = '__' + error_value.chomp + "__\n" 110 | else 111 | error_value = '__' + error_value + '__' 112 | end 113 | 114 | prev = (@backup || []) 115 | prev = prev.empty? ? '' : prev.join + ' ' 116 | errmsg = prev + error_value 117 | 118 | if @ss and @ss.rest? 119 | errmsg << ' ' + @ss.rest 120 | end 121 | 122 | lines = errmsg.lines 123 | err_num = prev.count("\n") 124 | from_num = err_num - 3 125 | from_num = 0 if from_num < 0 126 | to_num = err_num + 3 127 | digit_num = lines.count.to_s.length 128 | 129 | errmsg = lines.each_with_index.map {|line, i| 130 | mark = (i == err_num) ? '*' : ' ' 131 | '%s %*d: %s' % [mark, digit_num, i + 1, line] 132 | }.slice(from_num..to_num).join 133 | 134 | raise Racc::ParseError, header + errmsg 135 | end 136 | private :raise_error 137 | 138 | def self.parse(obj) 139 | self.new(obj).parse 140 | end 141 | ...end parse.y/module_eval... 142 | ##### State transition tables begin ### 143 | 144 | racc_action_table = [ 145 | 10, 31, 32, 9, 16, 17, 20, 7, 14, 21, 146 | 22, 3, nil, 29, 6, 26, nil, 21, 22, 10, 147 | 3, 13, 29, 6, 6, nil, 21, 22, 30, 3, 148 | 24, 3, 6, nil, 6 ] 149 | 150 | racc_action_check = [ 151 | 9, 25, 25, 3, 9, 9, 9, 1, 7, 9, 152 | 9, 1, nil, 20, 1, 20, nil, 20, 20, 5, 153 | 0, 5, 32, 0, 5, nil, 32, 32, 23, 23, 154 | 10, 10, 23, nil, 10 ] 155 | 156 | racc_action_pointer = [ 157 | 16, 7, nil, -2, nil, 17, nil, 8, nil, -2, 158 | 27, nil, nil, nil, nil, nil, nil, nil, nil, nil, 159 | 6, nil, nil, 25, nil, -8, nil, nil, nil, nil, 160 | nil, nil, 15, nil ] 161 | 162 | racc_action_default = [ 163 | -23, -23, -1, -13, -10, -23, -14, -23, -2, -23, 164 | -23, -11, -12, -13, 34, -5, -6, -7, -8, -9, 165 | -23, -21, -22, -23, -4, -23, -16, -17, -19, -20, 166 | -3, -15, -23, -18 ] 167 | 168 | racc_goto_table = [ 169 | 8, 27, 1, 11, 12, 15, 25, 18, 19, nil, 170 | nil, nil, 23, 33, nil, nil, nil, nil, nil, nil, 171 | nil, nil, 8 ] 172 | 173 | racc_goto_check = [ 174 | 2, 9, 1, 3, 6, 4, 8, 3, 5, nil, 175 | nil, nil, 1, 9, nil, nil, nil, nil, nil, nil, 176 | nil, nil, 2 ] 177 | 178 | racc_goto_pointer = [ 179 | nil, 2, -1, -2, -4, -1, -1, nil, -14, -19 ] 180 | 181 | racc_goto_default = [ 182 | nil, nil, 2, nil, 28, nil, 4, 5, nil, nil ] 183 | 184 | racc_reduce_table = [ 185 | 0, 0, :racc_error, 186 | 1, 14, :_reduce_1, 187 | 2, 14, :_reduce_2, 188 | 3, 16, :_reduce_3, 189 | 2, 16, :_reduce_4, 190 | 3, 15, :_reduce_5, 191 | 3, 15, :_reduce_6, 192 | 3, 15, :_reduce_7, 193 | 3, 15, :_reduce_8, 194 | 3, 15, :_reduce_9, 195 | 1, 15, :_reduce_10, 196 | 2, 19, :_reduce_11, 197 | 2, 19, :_reduce_12, 198 | 1, 20, :_reduce_13, 199 | 1, 20, :_reduce_14, 200 | 3, 18, :_reduce_15, 201 | 2, 18, :_reduce_16, 202 | 1, 21, :_reduce_17, 203 | 3, 21, :_reduce_18, 204 | 1, 22, :_reduce_19, 205 | 1, 22, :_reduce_20, 206 | 1, 17, :_reduce_21, 207 | 1, 17, :_reduce_22 ] 208 | 209 | racc_reduce_n = 23 210 | 211 | racc_shift_n = 34 212 | 213 | racc_token_table = { 214 | false => 0, 215 | :error => 1, 216 | :LEFTBRACE => 2, 217 | :RIGHTBRACE => 3, 218 | :IDENTIFIER => 4, 219 | :EQUAL => 5, 220 | :BOOL => 6, 221 | :STRING => 7, 222 | :LEFTBRACKET => 8, 223 | :RIGHTBRACKET => 9, 224 | :COMMA => 10, 225 | :INTEGER => 11, 226 | :FLOAT => 12 } 227 | 228 | racc_nt_base = 13 229 | 230 | racc_use_result_var = false 231 | 232 | Racc_arg = [ 233 | racc_action_table, 234 | racc_action_check, 235 | racc_action_default, 236 | racc_action_pointer, 237 | racc_goto_table, 238 | racc_goto_check, 239 | racc_goto_default, 240 | racc_goto_pointer, 241 | racc_nt_base, 242 | racc_reduce_table, 243 | racc_token_table, 244 | racc_shift_n, 245 | racc_reduce_n, 246 | racc_use_result_var ] 247 | 248 | Racc_token_to_s_table = [ 249 | "$end", 250 | "error", 251 | "LEFTBRACE", 252 | "RIGHTBRACE", 253 | "IDENTIFIER", 254 | "EQUAL", 255 | "BOOL", 256 | "STRING", 257 | "LEFTBRACKET", 258 | "RIGHTBRACKET", 259 | "COMMA", 260 | "INTEGER", 261 | "FLOAT", 262 | "$start", 263 | "objectlist", 264 | "objectitem", 265 | "object", 266 | "number", 267 | "list", 268 | "block", 269 | "blockId", 270 | "listitems", 271 | "listitem" ] 272 | 273 | Racc_debug_parser = false 274 | 275 | ##### State transition tables end ##### 276 | 277 | # reduce 0 omitted 278 | 279 | module_eval(<<'.,.,', 'parse.y', 5) 280 | def _reduce_1(val, _values) 281 | val[0] 282 | 283 | end 284 | .,., 285 | 286 | module_eval(<<'.,.,', 'parse.y', 9) 287 | def _reduce_2(val, _values) 288 | val[0].deep_merge(val[1]) 289 | 290 | end 291 | .,., 292 | 293 | module_eval(<<'.,.,', 'parse.y', 14) 294 | def _reduce_3(val, _values) 295 | val[1] 296 | 297 | end 298 | .,., 299 | 300 | module_eval(<<'.,.,', 'parse.y', 18) 301 | def _reduce_4(val, _values) 302 | {} 303 | 304 | end 305 | .,., 306 | 307 | module_eval(<<'.,.,', 'parse.y', 23) 308 | def _reduce_5(val, _values) 309 | {val[0] => val[2]} 310 | 311 | end 312 | .,., 313 | 314 | module_eval(<<'.,.,', 'parse.y', 27) 315 | def _reduce_6(val, _values) 316 | {val[0] => val[2]} 317 | 318 | end 319 | .,., 320 | 321 | module_eval(<<'.,.,', 'parse.y', 31) 322 | def _reduce_7(val, _values) 323 | {val[0] => val[2]} 324 | 325 | end 326 | .,., 327 | 328 | module_eval(<<'.,.,', 'parse.y', 35) 329 | def _reduce_8(val, _values) 330 | {val[0] => val[2]} 331 | 332 | end 333 | .,., 334 | 335 | module_eval(<<'.,.,', 'parse.y', 39) 336 | def _reduce_9(val, _values) 337 | {val[0] => val[2]} 338 | 339 | end 340 | .,., 341 | 342 | module_eval(<<'.,.,', 'parse.y', 43) 343 | def _reduce_10(val, _values) 344 | val[0] 345 | 346 | end 347 | .,., 348 | 349 | module_eval(<<'.,.,', 'parse.y', 48) 350 | def _reduce_11(val, _values) 351 | {val[0] => val[1]} 352 | 353 | end 354 | .,., 355 | 356 | module_eval(<<'.,.,', 'parse.y', 52) 357 | def _reduce_12(val, _values) 358 | {val[0] => val[1]} 359 | 360 | end 361 | .,., 362 | 363 | module_eval(<<'.,.,', 'parse.y', 58) 364 | def _reduce_13(val, _values) 365 | val[0] 366 | 367 | end 368 | .,., 369 | 370 | module_eval(<<'.,.,', 'parse.y', 62) 371 | def _reduce_14(val, _values) 372 | val[0] 373 | 374 | end 375 | .,., 376 | 377 | module_eval(<<'.,.,', 'parse.y', 67) 378 | def _reduce_15(val, _values) 379 | val[1] 380 | 381 | end 382 | .,., 383 | 384 | module_eval(<<'.,.,', 'parse.y', 71) 385 | def _reduce_16(val, _values) 386 | [] 387 | 388 | end 389 | .,., 390 | 391 | module_eval(<<'.,.,', 'parse.y', 76) 392 | def _reduce_17(val, _values) 393 | [val[0]] 394 | 395 | end 396 | .,., 397 | 398 | module_eval(<<'.,.,', 'parse.y', 80) 399 | def _reduce_18(val, _values) 400 | val[0] + [val[2]] 401 | 402 | end 403 | .,., 404 | 405 | module_eval(<<'.,.,', 'parse.y', 85) 406 | def _reduce_19(val, _values) 407 | val[0] 408 | 409 | end 410 | .,., 411 | 412 | module_eval(<<'.,.,', 'parse.y', 89) 413 | def _reduce_20(val, _values) 414 | val[0] 415 | 416 | end 417 | .,., 418 | 419 | module_eval(<<'.,.,', 'parse.y', 94) 420 | def _reduce_21(val, _values) 421 | val[0] 422 | 423 | end 424 | .,., 425 | 426 | module_eval(<<'.,.,', 'parse.y', 98) 427 | def _reduce_22(val, _values) 428 | val[0] 429 | 430 | end 431 | .,., 432 | 433 | def _reduce_none(val, _values) 434 | val[0] 435 | end 436 | 437 | end # class Parse 438 | end # module Rhcl 439 | -------------------------------------------------------------------------------- /lib/rhcl/parse.y: -------------------------------------------------------------------------------- 1 | class Rhcl::Parse 2 | options no_result_var 3 | rule 4 | objectlist : objectitem 5 | { 6 | val[0] 7 | } 8 | | objectlist objectitem 9 | { 10 | val[0].deep_merge(val[1]) 11 | } 12 | 13 | object: LEFTBRACE objectlist RIGHTBRACE 14 | { 15 | val[1] 16 | } 17 | | LEFTBRACE RIGHTBRACE 18 | { 19 | {} 20 | } 21 | 22 | objectitem: IDENTIFIER EQUAL number 23 | { 24 | {val[0] => val[2]} 25 | } 26 | | IDENTIFIER EQUAL BOOL 27 | { 28 | {val[0] => val[2]} 29 | } 30 | | IDENTIFIER EQUAL STRING 31 | { 32 | {val[0] => val[2]} 33 | } 34 | | IDENTIFIER EQUAL object 35 | { 36 | {val[0] => val[2]} 37 | } 38 | | IDENTIFIER EQUAL list 39 | { 40 | {val[0] => val[2]} 41 | } 42 | | block 43 | { 44 | val[0] 45 | } 46 | 47 | block: blockId object 48 | { 49 | {val[0] => val[1]} 50 | } 51 | | blockId block 52 | { 53 | {val[0] => val[1]} 54 | } 55 | 56 | 57 | blockId: IDENTIFIER 58 | { 59 | val[0] 60 | } 61 | | STRING 62 | { 63 | val[0] 64 | } 65 | 66 | list: LEFTBRACKET listitems RIGHTBRACKET 67 | { 68 | val[1] 69 | } 70 | | LEFTBRACKET RIGHTBRACKET 71 | { 72 | [] 73 | } 74 | 75 | listitems: listitem 76 | { 77 | [val[0]] 78 | } 79 | | listitems COMMA listitem 80 | { 81 | val[0] + [val[2]] 82 | } 83 | 84 | listitem: number 85 | { 86 | val[0] 87 | } 88 | | STRING 89 | { 90 | val[0] 91 | } 92 | 93 | number: INTEGER 94 | { 95 | val[0] 96 | } 97 | | FLOAT 98 | { 99 | val[0] 100 | } 101 | 102 | ---- header 103 | 104 | require 'strscan' 105 | 106 | ---- inner 107 | 108 | TRUE_VALUES = %w(true on yes) 109 | FALSE_VALUES = %w(false off no ) 110 | BOOLEAN_VALUES = TRUE_VALUES + FALSE_VALUES 111 | 112 | def initialize(obj) 113 | src = obj.is_a?(IO) ? obj.read : obj.to_s 114 | @ss = StringScanner.new(src) 115 | end 116 | 117 | def scan 118 | tok = nil 119 | @backup = [] 120 | 121 | until @ss.eos? 122 | if (tok = backup { @ss.scan /\s+/ }) 123 | # nothing to do 124 | elsif (tok = backup { @ss.scan /#/ }) 125 | backup { @ss.scan_until /\n/ } 126 | elsif (tok = backup { @ss.scan %r|/| }) 127 | case (tok = backup { @ss.getch }) 128 | when '/' 129 | backup { @ss.scan_until /(\n|\z)/ } 130 | when '*' 131 | nested = 1 132 | 133 | until nested.zero? 134 | case (tok = backup { @ss.scan_until %r{(/\*|\*/|\z)} }) 135 | when %r|/\*\z| 136 | nested += 1 137 | when %r|\*/\z| 138 | nested -= 1 139 | else 140 | break 141 | end 142 | end 143 | else 144 | raise "comment expected, got #{tok.inspect}" 145 | end 146 | elsif (tok = backup { @ss.scan /-?\d+\.\d+/ }) 147 | yield [:FLOAT, tok.to_f] 148 | elsif (tok = backup { @ss.scan /-?\d+/ }) 149 | yield [:INTEGER, tok.to_i] 150 | elsif (tok = backup { @ss.scan /,/ }) 151 | yield [:COMMA, tok] 152 | elsif (tok = backup { @ss.scan /\=/ }) 153 | yield [:EQUAL, tok] 154 | elsif (tok = backup { @ss.scan /\[/ }) 155 | yield [:LEFTBRACKET, tok] 156 | elsif (tok = backup { @ss.scan /\]/ }) 157 | yield [:RIGHTBRACKET, tok] 158 | elsif (tok = backup { @ss.scan /\{/ }) 159 | yield [:LEFTBRACE, tok] 160 | elsif (tok = backup { @ss.scan /\}/ }) 161 | yield [:RIGHTBRACE, tok] 162 | elsif (tok = backup { @ss.scan /"/ }) 163 | yield [:STRING, (backup { @ss.scan_until /("|\z)/ } || '').sub(/"\z/, '')] 164 | else 165 | identifier = (backup { @ss.scan_until /(\s|\z)/ } || '').sub(/\s\z/, '') 166 | token_type = :IDENTIFIER 167 | 168 | if BOOLEAN_VALUES.include?(identifier) 169 | identifier = TRUE_VALUES.include?(identifier) 170 | token_type = :BOOL 171 | end 172 | 173 | yield [token_type, identifier] 174 | end 175 | end 176 | 177 | yield [false, '$end'] 178 | end 179 | private :scan 180 | 181 | def backup 182 | tok = yield 183 | @backup << tok if tok 184 | return tok 185 | end 186 | 187 | def parse 188 | yyparse self, :scan 189 | end 190 | 191 | def on_error(error_token_id, error_value, value_stack) 192 | raise_error(error_value) 193 | end 194 | 195 | def raise_error(error_value) 196 | header = "parse error on value: #{error_value}\n" 197 | error_value = @backup.pop 198 | 199 | if error_value =~ /\n\z/ 200 | error_value = '__' + error_value.chomp + "__\n" 201 | else 202 | error_value = '__' + error_value + '__' 203 | end 204 | 205 | prev = (@backup || []) 206 | prev = prev.empty? ? '' : prev.join + ' ' 207 | errmsg = prev + error_value 208 | 209 | if @ss and @ss.rest? 210 | errmsg << ' ' + @ss.rest 211 | end 212 | 213 | lines = errmsg.lines 214 | err_num = prev.count("\n") 215 | from_num = err_num - 3 216 | from_num = 0 if from_num < 0 217 | to_num = err_num + 3 218 | digit_num = lines.count.to_s.length 219 | 220 | errmsg = lines.each_with_index.map {|line, i| 221 | mark = (i == err_num) ? '*' : ' ' 222 | '%s %*d: %s' % [mark, digit_num, i + 1, line] 223 | }.slice(from_num..to_num).join 224 | 225 | raise Racc::ParseError, header + errmsg 226 | end 227 | private :raise_error 228 | 229 | def self.parse(obj) 230 | self.new(obj).parse 231 | end 232 | -------------------------------------------------------------------------------- /lib/rhcl/version.rb: -------------------------------------------------------------------------------- 1 | module Rhcl 2 | VERSION = '0.1.0' 3 | end 4 | -------------------------------------------------------------------------------- /rhcl.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'rhcl/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'rhcl' 8 | spec.version = Rhcl::VERSION 9 | spec.authors = ['Genki Sugawara'] 10 | spec.email = ['sgwr_dts@yahoo.co.jp'] 11 | spec.summary = %q{Pure Ruby HCL parser} 12 | spec.description = %q{Pure Ruby HCL parser} 13 | spec.homepage = 'https://github.com/winebarrel/rhcl' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_dependency 'deep_merge' 22 | spec.add_development_dependency 'bundler', '~> 1.5' 23 | spec.add_development_dependency 'rake' 24 | spec.add_development_dependency 'racc' 25 | spec.add_development_dependency 'rspec', '>= 3.0.0' 26 | end 27 | -------------------------------------------------------------------------------- /spec/rhcl_dump_spec.rb: -------------------------------------------------------------------------------- 1 | describe Rhcl do 2 | it 'basic' do 3 | dumped = Rhcl.dump( 4 | {"foo" => "bar"} 5 | ) 6 | 7 | expect(dumped).to eq 'foo = "bar"' 8 | end 9 | 10 | it 'decode_policy' do 11 | dumped = Rhcl.dump( 12 | {"key"=> 13 | {""=>{"policy"=>"read"}, 14 | "foo/"=>{"policy"=>"write"}, 15 | "foo/bar/"=>{"policy"=>"read"}, 16 | "foo/bar/baz"=>{"policy"=>"deny"}}} 17 | ) 18 | 19 | expect(dumped).to eq <<-EOS.strip 20 | key { 21 | "" { 22 | policy = "read" 23 | } 24 | "foo/" { 25 | policy = "write" 26 | } 27 | "foo/bar/" { 28 | policy = "read" 29 | } 30 | "foo/bar/baz" { 31 | policy = "deny" 32 | } 33 | } 34 | EOS 35 | end 36 | 37 | it 'decode_tf_variable' do 38 | dumped = Rhcl.dump( 39 | {"variable"=> 40 | {"foo"=>{"default"=>"bar", "description"=>"bar"}, 41 | "amis"=>{"default"=>{"east"=>"foo"}}}} 42 | ) 43 | 44 | expect(dumped).to eq <<-EOS.strip 45 | variable { 46 | foo { 47 | default = "bar" 48 | description = "bar" 49 | } 50 | amis { 51 | default { 52 | east = "foo" 53 | } 54 | } 55 | } 56 | EOS 57 | end 58 | 59 | it 'empty' do 60 | dumped = Rhcl.dump( 61 | {"resource"=>{"aws_instance"=>{"db"=>{}}}} 62 | ) 63 | 64 | expect(dumped).to eq <<-EOS.strip 65 | resource { 66 | aws_instance { 67 | db { 68 | 69 | } 70 | } 71 | } 72 | EOS 73 | end 74 | 75 | it 'flat' do 76 | dumped = Rhcl.dump( 77 | {"foo"=>"bar", "Key"=>7} 78 | ) 79 | 80 | expect(dumped).to eq <<-EOS.strip 81 | foo = "bar" 82 | Key = 7 83 | EOS 84 | end 85 | 86 | it 'structure' do 87 | dumped = Rhcl.dump( 88 | {"foo"=>{"baz"=>{"key"=>7, "foo"=>"bar"}}} 89 | ) 90 | 91 | expect(dumped).to eq <<-EOS.strip 92 | foo { 93 | baz { 94 | key = 7 95 | foo = "bar" 96 | } 97 | } 98 | EOS 99 | end 100 | 101 | it 'structure2' do 102 | dumped = Rhcl.dump( 103 | {"foo"=>{"baz"=>{"key"=>7, "foo"=>"bar"}, "key"=>7}} 104 | ) 105 | 106 | expect(dumped).to eq <<-EOS.strip 107 | foo { 108 | baz { 109 | key = 7 110 | foo = "bar" 111 | } 112 | key = 7 113 | } 114 | EOS 115 | end 116 | 117 | it 'structure_flat' do 118 | dumped = Rhcl.dump( 119 | {"foo"=>{"baz"=>{"key"=>7, "foo"=>"bar"}}} 120 | ) 121 | 122 | expect(dumped).to eq <<-EOS.strip 123 | foo { 124 | baz { 125 | key = 7 126 | foo = "bar" 127 | } 128 | } 129 | EOS 130 | end 131 | 132 | it 'structure_flatmap' do 133 | dumped = Rhcl.dump( 134 | {"foo"=>{"key"=>7, "foo"=>"bar"}} 135 | ) 136 | 137 | expect(dumped).to eq <<-EOS.strip 138 | foo { 139 | key = 7 140 | foo = "bar" 141 | } 142 | EOS 143 | end 144 | 145 | it 'structure_multi' do 146 | dumped = Rhcl.dump( 147 | {"foo"=>{"baz"=>{"key"=>7}, "bar"=>{"key"=>12}}} 148 | ) 149 | 150 | expect(dumped).to eq <<-EOS.strip 151 | foo { 152 | baz { 153 | key = 7 154 | } 155 | bar { 156 | key = 12 157 | } 158 | } 159 | EOS 160 | end 161 | 162 | it 'array' do 163 | dumped = Rhcl.dump( 164 | {"foo"=>[1, 2, "baz"], "bar"=>"baz"} 165 | ) 166 | 167 | expect(dumped).to eq <<-EOS.strip 168 | foo = [1, 2, "baz"] 169 | bar = "baz" 170 | EOS 171 | end 172 | 173 | it 'object' do 174 | dumped = Rhcl.dump( 175 | {"foo"=>{"bar"=>[1, 2, "baz"]}} 176 | ) 177 | 178 | expect(dumped).to eq <<-EOS.strip 179 | foo { 180 | bar = [1, 2, "baz"] 181 | } 182 | EOS 183 | end 184 | 185 | it 'types' do 186 | dumped = Rhcl.dump( 187 | {"foo"=>"bar", 188 | "bar"=>7, 189 | "baz"=>[1, 2, 3], 190 | "foo2"=>-12, 191 | "bar2"=>3.14159, 192 | "bar3"=>-3.14159, 193 | "hoge"=>true, 194 | "fuga"=>false} 195 | ) 196 | 197 | expect(dumped).to eq <<-EOS.strip 198 | foo = "bar" 199 | bar = 7 200 | baz = [1, 2, 3] 201 | foo2 = -12 202 | bar2 = 3.14159 203 | bar3 = -3.14159 204 | hoge = true 205 | fuga = false 206 | EOS 207 | end 208 | 209 | it 'complex' do 210 | dumped = Rhcl.dump( 211 | {"variable"=>{"foo"=>{"default"=>"bar", "description"=>"bar"}}, 212 | "provider"=> 213 | {"aws"=>{"access_key"=>"foo", "secret_key"=>"bar"}, 214 | "do"=>{"api_key"=>"${var.foo}"}}, 215 | "resource"=> 216 | {"aws_security_group"=>{"firewall"=>{"count"=>5}}, 217 | "aws_instance"=> 218 | {"web"=> 219 | {"ami"=>"${var.foo}", 220 | "security_groups"=>["foo", "${aws_security_group.firewall.foo}"], 221 | "network_interface"=> 222 | {"device_index"=>0, "description"=>"Main network interface"}}, 223 | "db"=> 224 | {"security_groups"=>"${aws_security_group.firewall.*.id}", 225 | "VPC"=>"foo", 226 | "depends_on"=>["aws_instance.web"]}}}, 227 | "output"=>{"web_ip"=>{"value"=>"${aws_instance.web.private_ip}"}}} 228 | ) 229 | 230 | expect(dumped).to eq <<-EOS.strip 231 | variable { 232 | foo { 233 | default = "bar" 234 | description = "bar" 235 | } 236 | } 237 | provider { 238 | aws { 239 | access_key = "foo" 240 | secret_key = "bar" 241 | } 242 | do { 243 | api_key = "${var.foo}" 244 | } 245 | } 246 | resource { 247 | aws_security_group { 248 | firewall { 249 | count = 5 250 | } 251 | } 252 | aws_instance { 253 | web { 254 | ami = "${var.foo}" 255 | security_groups = ["foo", "${aws_security_group.firewall.foo}"] 256 | network_interface { 257 | device_index = 0 258 | description = "Main network interface" 259 | } 260 | } 261 | db { 262 | security_groups = "${aws_security_group.firewall.*.id}" 263 | VPC = "foo" 264 | depends_on = ["aws_instance.web"] 265 | } 266 | } 267 | } 268 | output { 269 | web_ip { 270 | value = "${aws_instance.web.private_ip}" 271 | } 272 | } 273 | EOS 274 | end 275 | end 276 | -------------------------------------------------------------------------------- /spec/rhcl_parse_spec.rb: -------------------------------------------------------------------------------- 1 | describe Rhcl do 2 | it 'basic' do 3 | parsed = Rhcl.parse(<<-EOS) 4 | foo = "bar" 5 | EOS 6 | 7 | expect(parsed).to eq( 8 | {"foo" => "bar"} 9 | ) 10 | end 11 | 12 | it 'decode_policy' do 13 | parsed = Rhcl.parse(<<-EOS) 14 | key "" { 15 | policy = "read" 16 | } 17 | 18 | key "foo/" { 19 | policy = "write" 20 | } 21 | 22 | key "foo/bar/" { 23 | policy = "read" 24 | } 25 | 26 | key "foo/bar/baz" { 27 | policy = "deny" 28 | } 29 | EOS 30 | 31 | expect(parsed).to eq( 32 | {"key"=> 33 | {""=>{"policy"=>"read"}, 34 | "foo/"=>{"policy"=>"write"}, 35 | "foo/bar/"=>{"policy"=>"read"}, 36 | "foo/bar/baz"=>{"policy"=>"deny"}}} 37 | ) 38 | end 39 | 40 | it 'decode_tf_variable' do 41 | parsed = Rhcl.parse(<<-EOS) 42 | variable "foo" { 43 | default = "bar" 44 | description = "bar" 45 | } 46 | 47 | variable "amis" { 48 | default = { 49 | east = "foo" 50 | } 51 | } 52 | EOS 53 | 54 | expect(parsed).to eq( 55 | {"variable"=> 56 | {"foo"=>{"default"=>"bar", "description"=>"bar"}, 57 | "amis"=>{"default"=>{"east"=>"foo"}}}} 58 | ) 59 | end 60 | 61 | it 'empty' do 62 | parsed = Rhcl.parse(<<-EOS) 63 | resource "aws_instance" "db" {} 64 | EOS 65 | 66 | expect(parsed).to eq( 67 | {"resource"=>{"aws_instance"=>{"db"=>{}}}} 68 | ) 69 | end 70 | 71 | it 'flat' do 72 | parsed = Rhcl.parse(<<-EOS) 73 | foo = "bar" 74 | Key = 7 75 | EOS 76 | 77 | expect(parsed).to eq( 78 | {"foo"=>"bar", "Key"=>7} 79 | ) 80 | end 81 | 82 | it 'structure' do 83 | parsed = Rhcl.parse(<<-EOS) 84 | // This is a test structure for the lexer 85 | foo "baz" { 86 | key = 7 87 | foo = "bar" 88 | } 89 | EOS 90 | 91 | expect(parsed).to eq( 92 | {"foo"=>{"baz"=>{"key"=>7, "foo"=>"bar"}}} 93 | ) 94 | end 95 | 96 | it 'structure2' do 97 | parsed = Rhcl.parse(<<-EOS) 98 | // This is a test structure for the lexer 99 | foo "baz" { 100 | key = 7 101 | foo = "bar" 102 | } 103 | 104 | foo { 105 | key = 7 106 | } 107 | EOS 108 | 109 | expect(parsed).to eq( 110 | {"foo"=>{"baz"=>{"key"=>7, "foo"=>"bar"}, "key"=>7}} 111 | ) 112 | end 113 | 114 | it 'structure_flat' do 115 | parsed = Rhcl.parse(<<-EOS) 116 | # This is a test structure for the lexer 117 | foo "baz" { 118 | key = 7 119 | foo = "bar" 120 | } 121 | EOS 122 | 123 | expect(parsed).to eq( 124 | {"foo"=>{"baz"=>{"key"=>7, "foo"=>"bar"}}} 125 | ) 126 | end 127 | 128 | it 'structure_flatmap' do 129 | parsed = Rhcl.parse(<<-EOS) 130 | /* 131 | comment 132 | */ 133 | foo { 134 | key = 7 135 | } 136 | 137 | foo { 138 | foo = "bar" 139 | } 140 | EOS 141 | 142 | expect(parsed).to eq( 143 | {"foo"=>{"key"=>7, "foo"=>"bar"}} 144 | ) 145 | end 146 | 147 | it 'structure_multi' do 148 | parsed = Rhcl.parse(<<-EOS) 149 | foo "baz" { 150 | key = 7 151 | } 152 | 153 | foo "bar" { 154 | key = 12 155 | } 156 | EOS 157 | 158 | expect(parsed).to eq( 159 | {"foo"=>{"baz"=>{"key"=>7}, "bar"=>{"key"=>12}}} 160 | ) 161 | end 162 | 163 | it 'array' do 164 | parsed = Rhcl.parse(<<-EOS) 165 | foo = [1, 2, "baz"] 166 | bar = "baz" 167 | EOS 168 | 169 | expect(parsed).to eq( 170 | {"foo"=>[1, 2, "baz"], "bar"=>"baz"} 171 | ) 172 | end 173 | 174 | it 'object' do 175 | parsed = Rhcl.parse(<<-EOS) 176 | foo = { 177 | bar = [1, 2, "baz"] 178 | } 179 | EOS 180 | 181 | expect(parsed).to eq( 182 | {"foo"=>{"bar"=>[1, 2, "baz"]}} 183 | ) 184 | end 185 | 186 | it 'types' do 187 | parsed = Rhcl.parse(<<-EOS) 188 | foo = "bar" 189 | bar = 7 190 | baz = [1,2,3] 191 | foo2 = -12 192 | bar2 = 3.14159 193 | bar3 = -3.14159 194 | hoge = true 195 | fuga = false 196 | EOS 197 | 198 | expect(parsed).to eq( 199 | {"foo"=>"bar", 200 | "bar"=>7, 201 | "baz"=>[1, 2, 3], 202 | "foo2"=>-12, 203 | "bar2"=>3.14159, 204 | "bar3"=>-3.14159, 205 | "hoge"=>true, 206 | "fuga"=>false} 207 | ) 208 | end 209 | 210 | it 'assign_colon' do 211 | expect { 212 | Rhcl.parse(<<-EOS) 213 | resource = [{ 214 | "foo": { 215 | "bar": {}, 216 | "baz": [1, 2, "foo"], 217 | } 218 | }] 219 | EOS 220 | }.to raise_error 221 | end 222 | 223 | it 'assign_deep' do 224 | expect { 225 | Rhcl.parse(<<-EOS) 226 | resource = [{ 227 | foo = [{ 228 | bar = {} 229 | }] 230 | }] 231 | EOS 232 | }.to raise_error 233 | end 234 | 235 | it 'comment' do 236 | parsed = Rhcl.parse(<<-EOS) 237 | // Foo 238 | 239 | /* Bar */ 240 | 241 | /* 242 | /* 243 | Baz 244 | */ 245 | */ 246 | 247 | # Another 248 | 249 | # Multiple 250 | # Lines 251 | 252 | foo = "bar" 253 | EOS 254 | 255 | expect(parsed).to eq( 256 | {"foo"=>"bar"} 257 | ) 258 | end 259 | 260 | it 'complex' do 261 | parsed = Rhcl.parse(<<-EOS) 262 | // This comes from Terraform, as a test 263 | variable "foo" { 264 | default = "bar" 265 | description = "bar" 266 | } 267 | 268 | provider "aws" { 269 | access_key = "foo" 270 | secret_key = "bar" 271 | } 272 | 273 | provider "do" { 274 | api_key = "${var.foo}" 275 | } 276 | 277 | resource "aws_security_group" "firewall" { 278 | count = 5 279 | } 280 | 281 | resource aws_instance "web" { 282 | ami = "${var.foo}" 283 | security_groups = [ 284 | "foo", 285 | "${aws_security_group.firewall.foo}" 286 | ] 287 | 288 | network_interface { 289 | device_index = 0 290 | description = "Main network interface" 291 | } 292 | } 293 | 294 | resource "aws_instance" "db" { 295 | security_groups = "${aws_security_group.firewall.*.id}" 296 | VPC = "foo" 297 | 298 | depends_on = ["aws_instance.web"] 299 | } 300 | 301 | output "web_ip" { 302 | value = "${aws_instance.web.private_ip}" 303 | } 304 | EOS 305 | 306 | expect(parsed).to eq( 307 | {"variable"=>{"foo"=>{"default"=>"bar", "description"=>"bar"}}, 308 | "provider"=> 309 | {"aws"=>{"access_key"=>"foo", "secret_key"=>"bar"}, 310 | "do"=>{"api_key"=>"${var.foo}"}}, 311 | "resource"=> 312 | {"aws_security_group"=>{"firewall"=>{"count"=>5}}, 313 | "aws_instance"=> 314 | {"web"=> 315 | {"ami"=>"${var.foo}", 316 | "security_groups"=>["foo", "${aws_security_group.firewall.foo}"], 317 | "network_interface"=> 318 | {"device_index"=>0, "description"=>"Main network interface"}}, 319 | "db"=> 320 | {"security_groups"=>"${aws_security_group.firewall.*.id}", 321 | "VPC"=>"foo", 322 | "depends_on"=>["aws_instance.web"]}}}, 323 | "output"=>{"web_ip"=>{"value"=>"${aws_instance.web.private_ip}"}}} 324 | ) 325 | end 326 | 327 | it 'bool types' do 328 | parsed = Rhcl.parse(<<-EOS) 329 | foo = true 330 | bar = false 331 | zoo = on 332 | foo2 = off 333 | bar2 = yes 334 | zoo2 = no 335 | EOS 336 | 337 | expect(parsed).to eq( 338 | {"foo"=>true, 339 | "bar"=>false, 340 | "zoo"=>true, 341 | "foo2"=>false, 342 | "bar2"=>true, 343 | "zoo2"=>false} 344 | ) 345 | end 346 | end 347 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rhcl' 2 | require 'json' 3 | --------------------------------------------------------------------------------