sycl 1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/sycl +184 -0
- data/lib/sycl.rb +483 -0
- metadata +67 -0
data/bin/sycl
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# sycl - command line YAML manipulation tool using Sycl library
|
3
|
+
# Andrew Ho (ho@groupon.com)
|
4
|
+
#
|
5
|
+
# Copyright (c) 2012, Groupon, Inc.
|
6
|
+
# All rights reserved.
|
7
|
+
#
|
8
|
+
# Redistribution and use in source and binary forms, with or without
|
9
|
+
# modification, are permitted provided that the following conditions
|
10
|
+
# are met:
|
11
|
+
#
|
12
|
+
# Redistributions of source code must retain the above copyright notice,
|
13
|
+
# this list of conditions and the following disclaimer.
|
14
|
+
#
|
15
|
+
# Redistributions in binary form must reproduce the above copyright
|
16
|
+
# notice, this list of conditions and the following disclaimer in the
|
17
|
+
# documentation and/or other materials provided with the distribution.
|
18
|
+
#
|
19
|
+
# Neither the name of GROUPON nor the names of its contributors may be
|
20
|
+
# used to endorse or promote products derived from this software without
|
21
|
+
# specific prior written permission.
|
22
|
+
#
|
23
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
24
|
+
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
25
|
+
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
26
|
+
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
27
|
+
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
28
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
29
|
+
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
30
|
+
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
31
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
32
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
33
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
34
|
+
|
35
|
+
require 'optparse'
|
36
|
+
require 'sycl'
|
37
|
+
require 'pp'
|
38
|
+
|
39
|
+
$ME = File.basename $0
|
40
|
+
$USAGE = "usage: #$ME [-h] [-i] [-y] program [file1 [file2 ...]]"
|
41
|
+
$HELP = <<"EndHelp"
|
42
|
+
|
43
|
+
#$USAGE
|
44
|
+
|
45
|
+
#$ME is a command line utility to run Ruby code on one or more YAML files.
|
46
|
+
It is similar to awk, but instead of processing lines of a file, it reads
|
47
|
+
full files as YAML; and instead of using awk language, you write Ruby code
|
48
|
+
which works with the data parsed from YAML.
|
49
|
+
|
50
|
+
The required first argument is a literal Ruby program. When this code is
|
51
|
+
run, the following variables are available:
|
52
|
+
|
53
|
+
f filename of the file being parsed
|
54
|
+
y the YAML text that was parsed
|
55
|
+
d data parsed from interpreting YAML
|
56
|
+
|
57
|
+
The d variable is a Sycl object (https://github.com/groupon/sycl)
|
58
|
+
This lets you access hashes with dot notation (for example, the lookup
|
59
|
+
d['foo']['bar'] can be written as d.foo.bar), including safe variants
|
60
|
+
with get() and set() methods (d.get('foo.bar') returns nil instead of
|
61
|
+
dying if d['foo'] does not exist). YAML output is sorted.
|
62
|
+
|
63
|
+
Options:
|
64
|
+
-h, --help display this help text and exit
|
65
|
+
-i, --inplace overwrite YAML files in place if d is modified
|
66
|
+
-y, --yaml output return value of program as YAML
|
67
|
+
|
68
|
+
Examples:
|
69
|
+
sycl 'puts "\#{d.hostname} \#{d.interfaces.eth0.inet[0]}"' host*.yml
|
70
|
+
sycl -i 'd.params.ntp_servers = %w{ns1 ns2}' host2.yml
|
71
|
+
sycl -y d unsorted.yml > sorted.yml
|
72
|
+
|
73
|
+
EndHelp
|
74
|
+
|
75
|
+
def main(argv)
|
76
|
+
in_place = false
|
77
|
+
yaml_output = false
|
78
|
+
opts = OptionParser.new do |opts|
|
79
|
+
opts.on('-h', '--help') { puts $HELP; exit 0 }
|
80
|
+
opts.on('-i', '--inplace') { in_place = true }
|
81
|
+
opts.on('-y', '--yaml') { yaml_output = true }
|
82
|
+
begin
|
83
|
+
opts.parse! argv
|
84
|
+
rescue OptionParser::InvalidOption => e
|
85
|
+
abort "#$ME: #{e}\n#$USAGE"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
abort "#$ME: missing required program\n#$USAGE" if argv.empty?
|
89
|
+
code = argv.shift
|
90
|
+
options = {}
|
91
|
+
if argv.empty?
|
92
|
+
abort "#$ME: cannot modify stdin in place" if in_place
|
93
|
+
$stderr.puts "#$ME: waiting for input from stdin..." if $stdin.tty?
|
94
|
+
retval = sycl_process code, $stdin, 'stdin'
|
95
|
+
puts retval.to_yaml if retval && yaml_output
|
96
|
+
else
|
97
|
+
modified = []
|
98
|
+
argv.each do |filename|
|
99
|
+
begin
|
100
|
+
fh = File.open filename
|
101
|
+
retval, new_data = sycl_process code, fh, filename
|
102
|
+
fh.close
|
103
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
104
|
+
abort "#$ME: could not open file: #{e}"
|
105
|
+
end
|
106
|
+
if in_place && new_data
|
107
|
+
output = new_data.to_yaml
|
108
|
+
output.sub! /^\-\-\- \n/, ''
|
109
|
+
output.gsub! /: $/m, ':'
|
110
|
+
begin
|
111
|
+
tmpfile = "#{filename}.tmp.#$$"
|
112
|
+
tmp = File.open tmpfile, 'w'
|
113
|
+
tmp.puts output
|
114
|
+
tmp.close
|
115
|
+
File.rename tmpfile, filename
|
116
|
+
modified << filename
|
117
|
+
ensure
|
118
|
+
begin
|
119
|
+
File.unlink tmpfile
|
120
|
+
rescue
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
puts retval.to_yaml if retval && yaml_output
|
125
|
+
end
|
126
|
+
end
|
127
|
+
0
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
# sycl_process(code, fh, filename) loads YAML from opened filehandle fh
|
132
|
+
# into a local variable y, parses it as a Sycl object into d, and runs
|
133
|
+
# the code string on it; the optional filename is used for error
|
134
|
+
# reporting, and also exposed as f. It returns the return value from the
|
135
|
+
# code that was run, and the new data structure, if it was modified.
|
136
|
+
|
137
|
+
def sycl_process(code, fh, f = nil)
|
138
|
+
begin
|
139
|
+
y = fh.read
|
140
|
+
d_orig = Sycl::load y
|
141
|
+
d = Sycl::load y
|
142
|
+
rescue Exception => e
|
143
|
+
abort "#$ME: could not parse YAML from #{f || 'unknown file'}: #{e}"
|
144
|
+
end
|
145
|
+
begin
|
146
|
+
retval = eval code
|
147
|
+
rescue Exception => e
|
148
|
+
abort "#$ME: program error while processing #{f || 'unknown file'}: #{e}"
|
149
|
+
end
|
150
|
+
prune_nil_values! d_orig
|
151
|
+
prune_nil_values! d
|
152
|
+
if d_orig.to_yaml == d.to_yaml
|
153
|
+
return [ retval ]
|
154
|
+
else
|
155
|
+
return [ retval, d ]
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
# Walk hashes and arrays, and delete any nil values.
|
161
|
+
|
162
|
+
def prune_nil_values!(o)
|
163
|
+
if o.is_a? Hash
|
164
|
+
o.keys.each do |k|
|
165
|
+
if o[k].nil?
|
166
|
+
o.delete k
|
167
|
+
else
|
168
|
+
o[k] = prune_nil_values o[k]
|
169
|
+
o.delete k if o[k].nil?
|
170
|
+
end
|
171
|
+
end
|
172
|
+
o
|
173
|
+
elsif o.is_a? Array
|
174
|
+
o.reject { |e| e.nil? }
|
175
|
+
o.collect! { |e| prune_nil_values e }
|
176
|
+
else
|
177
|
+
o
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
# Run main loop and exit
|
183
|
+
|
184
|
+
exit main ARGV
|
data/lib/sycl.rb
ADDED
@@ -0,0 +1,483 @@
|
|
1
|
+
# sycl.rb - Simple YAML Configuration Library
|
2
|
+
# Andrew Ho (ho@groupon.com)
|
3
|
+
#
|
4
|
+
# Copyright (c) 2012, Groupon, Inc.
|
5
|
+
# All rights reserved.
|
6
|
+
#
|
7
|
+
# Redistribution and use in source and binary forms, with or without
|
8
|
+
# modification, are permitted provided that the following conditions
|
9
|
+
# are met:
|
10
|
+
#
|
11
|
+
# Redistributions of source code must retain the above copyright notice,
|
12
|
+
# this list of conditions and the following disclaimer.
|
13
|
+
#
|
14
|
+
# Redistributions in binary form must reproduce the above copyright
|
15
|
+
# notice, this list of conditions and the following disclaimer in the
|
16
|
+
# documentation and/or other materials provided with the distribution.
|
17
|
+
#
|
18
|
+
# Neither the name of GROUPON nor the names of its contributors may be
|
19
|
+
# used to endorse or promote products derived from this software without
|
20
|
+
# specific prior written permission.
|
21
|
+
#
|
22
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
23
|
+
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
24
|
+
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
25
|
+
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
26
|
+
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
27
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
28
|
+
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
29
|
+
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
30
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
31
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
32
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
33
|
+
|
34
|
+
require 'yaml'
|
35
|
+
|
36
|
+
module Sycl
|
37
|
+
|
38
|
+
# Sycl::load(yaml), Sycl::load_file(filename), and Sycl::dump(object)
|
39
|
+
# function just like their YAML counterparts, but return and act on
|
40
|
+
# Sycl-blessed variants of Hashes and Arrays.
|
41
|
+
|
42
|
+
def self.load(yaml)
|
43
|
+
from_object YAML::load(yaml)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.load_file(filename)
|
47
|
+
from_object YAML::load_file(filename)
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.dump(object)
|
51
|
+
if (object.is_a?(::Hash) && !object.is_a?(Sycl::Hash)) ||
|
52
|
+
(object.is_a?(::Array) && !object.is_a?(Sycl::Array))
|
53
|
+
sycl_version = from_object object
|
54
|
+
sycl_version.to_yaml
|
55
|
+
else
|
56
|
+
object.to_yaml
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def self.from_object(o)
|
63
|
+
if o.is_a?(::Hash)
|
64
|
+
Sycl::Hash.from_hash(o)
|
65
|
+
elsif o.is_a?(::Array)
|
66
|
+
Sycl::Array.from_array(o)
|
67
|
+
else
|
68
|
+
o
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
# A Sycl::Array is like an Array, but creating one from an array blesses
|
74
|
+
# any child Array or Hash objects into Sycl::Array or Sycl::Hash objects.
|
75
|
+
#
|
76
|
+
# Sycl::Arrays support YAML preprocessing and postprocessing, and having
|
77
|
+
# individual nodes marked as being rendered in inline style. YAML
|
78
|
+
# output is also always sorted.
|
79
|
+
|
80
|
+
class Array < ::Array
|
81
|
+
def initialize(*args)
|
82
|
+
@yaml_preprocessor = nil
|
83
|
+
@yaml_postprocessor = nil
|
84
|
+
@yaml_style = nil
|
85
|
+
super
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.[](*args)
|
89
|
+
Sycl::Array.from_array super
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.load_file(f)
|
93
|
+
Sycl::Array.from_array YAML::load_file f
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.from_array(a)
|
97
|
+
retval = Sycl::Array.new
|
98
|
+
a.each { |e| retval << Sycl::from_object(e) }
|
99
|
+
retval
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
# Make sure that if we write to this array, we promote any inputs
|
104
|
+
# to their Sycl equivalents. This lets dot notation, styled YAML,
|
105
|
+
# and other Sycl goodies continue.
|
106
|
+
|
107
|
+
def []=(*args)
|
108
|
+
raise ArgumentError => 'wrong number of arguments' unless args.size > 1
|
109
|
+
unless args[-1].is_a?(Sycl::Hash) || args[-1].is_a?(Sycl::Array)
|
110
|
+
args[-1] = Sycl::from_object(args[-1])
|
111
|
+
end
|
112
|
+
super
|
113
|
+
end
|
114
|
+
|
115
|
+
def <<(e)
|
116
|
+
unless e.is_a?(Sycl::Hash) || e.is_a?(Sycl::Array)
|
117
|
+
e = Sycl::from_object(e)
|
118
|
+
end
|
119
|
+
super
|
120
|
+
end
|
121
|
+
|
122
|
+
def collect!(&block)
|
123
|
+
super { |o| Sycl::from_object(block.call o) }
|
124
|
+
end
|
125
|
+
alias_method :map!, :collect!
|
126
|
+
|
127
|
+
def concat(a)
|
128
|
+
a = Sycl::Array.from_array(a) unless a.is_a?(Sycl::Array)
|
129
|
+
super
|
130
|
+
end
|
131
|
+
|
132
|
+
def fill(*args, &block)
|
133
|
+
raise ArgumentError => 'wrong number of arguments' if args.empty?
|
134
|
+
if block_given?
|
135
|
+
super { |idx| Sycl::from_object(block.call idx) }
|
136
|
+
else
|
137
|
+
unless args[0].is_a?(Sycl::Hash) || args[0].is_a?(Sycl::Array)
|
138
|
+
args[0] = Sycl::from_object(args[0])
|
139
|
+
end
|
140
|
+
super
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def insert(i, *args)
|
145
|
+
raise ArgumentError => 'wrong number of arguments' if args.empty?
|
146
|
+
args.collect! do |o|
|
147
|
+
unless o.is_a?(Sycl::Hash) || o.is_a?(Sycl::Array)
|
148
|
+
o = Sycl::from_object(o)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
super
|
152
|
+
end
|
153
|
+
|
154
|
+
def push(*args)
|
155
|
+
raise ArgumentError => 'wrong number of arguments' if args.empty?
|
156
|
+
args.collect! do |o|
|
157
|
+
unless o.is_a?(Sycl::Hash) || o.is_a?(Sycl::Array)
|
158
|
+
o = Sycl::from_object(o)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
super
|
162
|
+
end
|
163
|
+
|
164
|
+
def replace(a)
|
165
|
+
a = Sycl::Array.from_array(a) unless a.is_a?(Sycl::Array)
|
166
|
+
super
|
167
|
+
end
|
168
|
+
|
169
|
+
def unshift(*args)
|
170
|
+
raise ArgumentError => 'wrong number of arguments' if args.empty?
|
171
|
+
args.collect! do |o|
|
172
|
+
unless o.is_a?(Sycl::Hash) || o.is_a?(Sycl::Array)
|
173
|
+
o = Sycl::from_object(o)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
super
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
# Make this array, or its children, rendered in inline/flow style YAML.
|
181
|
+
# The default is to render arrays in block (multi-line) style.
|
182
|
+
|
183
|
+
def render_inline!
|
184
|
+
@yaml_style = :inline
|
185
|
+
end
|
186
|
+
|
187
|
+
def render_values_inline!
|
188
|
+
self.each do |e|
|
189
|
+
e.render_inline! if e.respond_to?(:render_inline!)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
# Hooks to run before and after YAML dumping
|
195
|
+
|
196
|
+
def yaml_preprocessor(&block)
|
197
|
+
@yaml_preprocessor = block if block_given?
|
198
|
+
end
|
199
|
+
|
200
|
+
def yaml_postprocessor(&block)
|
201
|
+
@yaml_postprocessor = block if block_given?
|
202
|
+
end
|
203
|
+
|
204
|
+
def yaml_preprocess!
|
205
|
+
@yaml_preprocessor.call(self) if @yaml_preprocessor
|
206
|
+
end
|
207
|
+
|
208
|
+
def yaml_postprocess(yaml)
|
209
|
+
@yaml_postprocessor ? @yaml_postprocessor.call(yaml) : yaml
|
210
|
+
end
|
211
|
+
|
212
|
+
|
213
|
+
# The Psych YAML engine has a bug that results in infinite recursion
|
214
|
+
# if to_yaml is over-ridden on a non-native type. So, we fake out
|
215
|
+
# Psych and pretend Sycl::Array is a native type.
|
216
|
+
|
217
|
+
class MockNativeType
|
218
|
+
def source_location
|
219
|
+
['psych/core_ext.rb']
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def method(sym)
|
224
|
+
sym == :to_yaml ? MockNativeType.new : super
|
225
|
+
end
|
226
|
+
|
227
|
+
|
228
|
+
# YAML rendering overrides: run preprocessing and postprocessing,
|
229
|
+
# set flow/inline style if this node is marked accordingly, sort
|
230
|
+
# elements, and suppress taguri on output. For Psych, set a long line
|
231
|
+
# width to more or less suppress line wrap.
|
232
|
+
|
233
|
+
if defined?(YAML::ENGINE) && YAML::ENGINE.yamler == 'psych'
|
234
|
+
def encode_with(coder)
|
235
|
+
coder.style = Psych::Nodes::Sequence::FLOW if @yaml_style == :inline
|
236
|
+
coder.represent_seq nil, sort
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def to_yaml(opts = {})
|
241
|
+
yaml_preprocess!
|
242
|
+
if defined?(YAML::ENGINE) && YAML::ENGINE.yamler == 'psych'
|
243
|
+
opts ||= {}
|
244
|
+
opts[:line_width] ||= 999999
|
245
|
+
yaml = super
|
246
|
+
else
|
247
|
+
yaml = YAML::quick_emit(self, opts) do |out|
|
248
|
+
out.seq(nil, @yaml_style || to_yaml_style) do |seq|
|
249
|
+
sort.each { |e| seq.add(e) }
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
yaml_postprocess yaml
|
254
|
+
end
|
255
|
+
|
256
|
+
end
|
257
|
+
|
258
|
+
|
259
|
+
# A Sycl::Hash is like a Hash, but creating one from an hash blesses
|
260
|
+
# any child Array or Hash objects into Sycl::Array or Sycl::Hash objects.
|
261
|
+
#
|
262
|
+
# Hash contents can be accessed via "dot notation" (h.foo.bar means
|
263
|
+
# the same as h['foo']['bar']). However, h.foo.bar dies if h['foo']
|
264
|
+
# does not exist, so get() and set() methods exist: h.get('foo.bar')
|
265
|
+
# will return nil instead of dying if h['foo'] does not exist.
|
266
|
+
# There is also a convenient deep_merge() that is like Hash#merge(),
|
267
|
+
# but also descends into and merges child nodes of the new hash.
|
268
|
+
#
|
269
|
+
# Sycl::Hashes support YAML preprocessing and postprocessing, and having
|
270
|
+
# individual nodes marked as being rendered in inline style. YAML
|
271
|
+
# output is also always sorted by key.
|
272
|
+
|
273
|
+
class Hash < ::Hash
|
274
|
+
|
275
|
+
def initialize(*args)
|
276
|
+
@yaml_preprocessor = nil
|
277
|
+
@yaml_postprocessor = nil
|
278
|
+
@yaml_style = nil
|
279
|
+
super
|
280
|
+
end
|
281
|
+
|
282
|
+
def self.[](*args)
|
283
|
+
Sycl::Hash.from_hash super
|
284
|
+
end
|
285
|
+
|
286
|
+
def self.load_file(f)
|
287
|
+
Sycl::Hash.from_hash YAML::load_file f
|
288
|
+
end
|
289
|
+
|
290
|
+
def self.from_hash(h)
|
291
|
+
retval = Sycl::Hash.new
|
292
|
+
h.each { |k, v| retval[k] = Sycl::from_object(v) }
|
293
|
+
retval
|
294
|
+
end
|
295
|
+
|
296
|
+
|
297
|
+
# Make sure that if we write to this hash, we promote any inputs
|
298
|
+
# to their Sycl equivalents. This lets dot notation, styled YAML,
|
299
|
+
# and other Sycl goodies continue.
|
300
|
+
|
301
|
+
def []=(k, v)
|
302
|
+
unless v.is_a?(Sycl::Hash) || v.is_a?(Sycl::Array)
|
303
|
+
v = Sycl::from_object(v)
|
304
|
+
end
|
305
|
+
super
|
306
|
+
end
|
307
|
+
alias_method :store, :[]=
|
308
|
+
|
309
|
+
def merge!(h)
|
310
|
+
h = Sycl::Hash.from_hash(h) unless h.is_a?(Sycl::Hash)
|
311
|
+
super
|
312
|
+
end
|
313
|
+
alias_method :update, :merge!
|
314
|
+
|
315
|
+
|
316
|
+
# Allow method call syntax: h.foo.bar.baz == h['foo']['bar']['baz'].
|
317
|
+
#
|
318
|
+
# Accessing hash keys whose names overlap with names of Ruby Object
|
319
|
+
# built-in methods (id, type, etc.) will still need to be passed in
|
320
|
+
# with bracket notation (h['type'] instead of h.type).
|
321
|
+
|
322
|
+
def method_missing(method_symbol, *args, &block)
|
323
|
+
key = method_symbol.to_s
|
324
|
+
set = key.chomp!('=')
|
325
|
+
if set
|
326
|
+
self[key] = args.first
|
327
|
+
elsif self.key?(key)
|
328
|
+
self[key]
|
329
|
+
else
|
330
|
+
nil
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
|
335
|
+
# Safe dotted notation reads: h.get('foo.bar') == h['foo']['bar'].
|
336
|
+
#
|
337
|
+
# This will return nil instead of dying if h['foo'] does not exist.
|
338
|
+
|
339
|
+
def get(path)
|
340
|
+
path = path.split(/\./) if path.is_a?(String)
|
341
|
+
candidate = self
|
342
|
+
while !path.empty?
|
343
|
+
key = path.shift
|
344
|
+
if candidate[key]
|
345
|
+
candidate = candidate[key]
|
346
|
+
else
|
347
|
+
candidate = nil
|
348
|
+
last
|
349
|
+
end
|
350
|
+
end
|
351
|
+
candidate
|
352
|
+
end
|
353
|
+
|
354
|
+
|
355
|
+
# Dotted writes: h.set('foo.bar' => 'baz') means h['foo']['bar'] = 'baz'.
|
356
|
+
#
|
357
|
+
# This will auto-vivify any missing intervening hash keys, and also
|
358
|
+
# promote Hash and Array objects in the input to Scyl variants.
|
359
|
+
|
360
|
+
def set(path, value)
|
361
|
+
path = path.split(/\./) if path.is_a?(String)
|
362
|
+
target = self
|
363
|
+
while path.size > 1
|
364
|
+
key = path.shift
|
365
|
+
if !(target.key?(key) && target[key].is_a?(::Hash))
|
366
|
+
target[key] = Sycl::Hash.new
|
367
|
+
else
|
368
|
+
target[key] = Sycl::Hash.from_hash(target[key])
|
369
|
+
end
|
370
|
+
target = target[key]
|
371
|
+
end
|
372
|
+
target[path.first] = value
|
373
|
+
end
|
374
|
+
|
375
|
+
|
376
|
+
# Deep merge two hashes (the new hash wins on conflicts). Hash or
|
377
|
+
# and Array objects in the new hash are promoted to Sycl variants.
|
378
|
+
|
379
|
+
def deep_merge(h)
|
380
|
+
self.merge(h) do |key, v1, v2|
|
381
|
+
if v1.is_a?(::Hash) && v2.is_a?(Sycl::Hash)
|
382
|
+
self[key].deep_merge(v2)
|
383
|
+
elsif v1.is_a?(::Hash) && v2.is_a?(::Hash)
|
384
|
+
self[key].deep_merge(Sycl::Hash.from_hash(v2))
|
385
|
+
else
|
386
|
+
self[key] = Sycl::from_object(v2)
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
|
392
|
+
# Make Sycl::Hashes sortable alongside Sycl::Hashes and Strings.
|
393
|
+
# This makes YAML output in sorted order work.
|
394
|
+
|
395
|
+
include Comparable
|
396
|
+
|
397
|
+
def <=>(another)
|
398
|
+
self.to_str <=> another.to_str
|
399
|
+
end
|
400
|
+
|
401
|
+
def to_str
|
402
|
+
self.keys.sort.first
|
403
|
+
end
|
404
|
+
|
405
|
+
|
406
|
+
# Make this hash, or its children, rendered in inline/flow style YAML.
|
407
|
+
# The default is to render hashes in block (multi-line) style.
|
408
|
+
|
409
|
+
def render_inline!
|
410
|
+
@yaml_style = :inline
|
411
|
+
end
|
412
|
+
|
413
|
+
def render_values_inline!
|
414
|
+
self.values.each do |v|
|
415
|
+
v.render_inline! if v.respond_to?(:render_inline!)
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
|
420
|
+
# Hooks to run before and after YAML dumping
|
421
|
+
|
422
|
+
def yaml_preprocessor(&block)
|
423
|
+
@yaml_preprocessor = block if block_given?
|
424
|
+
end
|
425
|
+
|
426
|
+
def yaml_postprocessor(&block)
|
427
|
+
@yaml_postprocessor = block if block_given?
|
428
|
+
end
|
429
|
+
|
430
|
+
def yaml_preprocess!
|
431
|
+
@yaml_preprocessor.call(self) if @yaml_preprocessor
|
432
|
+
end
|
433
|
+
|
434
|
+
def yaml_postprocess(yaml)
|
435
|
+
@yaml_postprocessor ? @yaml_postprocessor.call(yaml) : yaml
|
436
|
+
end
|
437
|
+
|
438
|
+
|
439
|
+
# The Psych YAML engine has a bug that results in infinite recursion
|
440
|
+
# if to_yaml is over-ridden on a non-native type. So, we fake out
|
441
|
+
# Psych and pretend Sycl::Hash is a native type.
|
442
|
+
|
443
|
+
class MockNativeType
|
444
|
+
def source_location
|
445
|
+
['psych/core_ext.rb']
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
def method(sym)
|
450
|
+
sym == :to_yaml ? MockNativeType.new : super
|
451
|
+
end
|
452
|
+
|
453
|
+
|
454
|
+
# YAML rendering overrides: run preprocessing and postprocessing,
|
455
|
+
# set flow/inline style if this node is marked accordingly, sort by
|
456
|
+
# key, and suppress taguri on output. For Psych, set a long line
|
457
|
+
# width to more or less suppress line wrap.
|
458
|
+
|
459
|
+
if defined?(YAML::ENGINE) && YAML::ENGINE.yamler == 'psych'
|
460
|
+
def encode_with(coder)
|
461
|
+
coder.style = Psych::Nodes::Mapping::FLOW if @yaml_style == :inline
|
462
|
+
coder.represent_map nil, sort
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
def to_yaml(opts = {})
|
467
|
+
yaml_preprocess!
|
468
|
+
if defined?(YAML::ENGINE) && YAML::ENGINE.yamler == 'psych'
|
469
|
+
opts ||= {}
|
470
|
+
opts[:line_width] ||= 999999
|
471
|
+
yaml = super
|
472
|
+
else
|
473
|
+
yaml = YAML::quick_emit(self, opts) do |out|
|
474
|
+
out.map(nil, @yaml_style || to_yaml_style) do |map|
|
475
|
+
sort.each { |k, v| map.add(k, v) }
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
yaml_postprocess yaml
|
480
|
+
end
|
481
|
+
|
482
|
+
end
|
483
|
+
end
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sycl
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 15
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: "1.0"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Andrew Ho
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2012-03-23 00:00:00 +00:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: This library makes using YAML for configuration files convenient and easy.
|
22
|
+
email: rubygems@andrew.zeuscat.com
|
23
|
+
executables:
|
24
|
+
- sycl
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files: []
|
28
|
+
|
29
|
+
files:
|
30
|
+
- lib/sycl.rb
|
31
|
+
- bin/sycl
|
32
|
+
has_rdoc: true
|
33
|
+
homepage: http://zeuscat.com/andrew/sycl
|
34
|
+
licenses: []
|
35
|
+
|
36
|
+
post_install_message:
|
37
|
+
rdoc_options: []
|
38
|
+
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
hash: 3
|
47
|
+
segments:
|
48
|
+
- 0
|
49
|
+
version: "0"
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
hash: 3
|
56
|
+
segments:
|
57
|
+
- 0
|
58
|
+
version: "0"
|
59
|
+
requirements: []
|
60
|
+
|
61
|
+
rubyforge_project:
|
62
|
+
rubygems_version: 1.5.3
|
63
|
+
signing_key:
|
64
|
+
specification_version: 3
|
65
|
+
summary: Simple YAML Config Library
|
66
|
+
test_files: []
|
67
|
+
|