sycl 1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|