ytools 0.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/Rakefile +52 -0
- data/bin/ypath +5 -0
- data/bin/ytemplates +5 -0
- data/lib/VERSION +1 -0
- data/lib/ytools/basecli.rb +67 -0
- data/lib/ytools/errors.rb +7 -0
- data/lib/ytools/path/cli.rb +103 -0
- data/lib/ytools/path/examples.txt +64 -0
- data/lib/ytools/path/executor.rb +58 -0
- data/lib/ytools/path/lexer.rb +166 -0
- data/lib/ytools/path/parser.rb +135 -0
- data/lib/ytools/path/selectors.rb +132 -0
- data/lib/ytools/templates/cli.rb +108 -0
- data/lib/ytools/templates/examples.txt +52 -0
- data/lib/ytools/templates/executor.rb +28 -0
- data/lib/ytools/templates/yaml_object.rb +11 -0
- data/lib/ytools/version.rb +12 -0
- data/lib/ytools/yaml_object.rb +112 -0
- data/lib/ytools/yreader.rb +21 -0
- data/spec/helpers.rb +15 -0
- data/spec/path/executor_spec.rb +45 -0
- data/spec/path/lexer_spec.rb +127 -0
- data/spec/path/parser_spec.rb +104 -0
- data/spec/path/selectors_spec.rb +123 -0
- data/spec/path/yamls/1.yml +2 -0
- data/spec/path/yamls/2.yml +2 -0
- data/spec/path/yamls/3.yml +6 -0
- data/spec/path/yamls/4.yml +12 -0
- data/spec/path/yamls/5.yml +4 -0
- data/spec/yaml_object_spec.rb +61 -0
- metadata +118 -0
data/Rakefile
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift File.expand_path("../spec", __FILE__)
|
3
|
+
|
4
|
+
require 'fileutils'
|
5
|
+
require 'rake'
|
6
|
+
require 'rubygems'
|
7
|
+
require 'spec/rake/spectask'
|
8
|
+
require 'ytools/version'
|
9
|
+
|
10
|
+
task :default => :spec
|
11
|
+
|
12
|
+
desc "Run the RSpec tests"
|
13
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
14
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
15
|
+
t.spec_opts = ['-b', '-c', '-f', 'p']
|
16
|
+
t.fail_on_error = false
|
17
|
+
end
|
18
|
+
|
19
|
+
begin
|
20
|
+
require 'jeweler'
|
21
|
+
Jeweler::Tasks.new do |gem|
|
22
|
+
gem.name = 'ytools'
|
23
|
+
gem.version = YTools::Version.to_s
|
24
|
+
gem.executables = %W{ypath ytemplates}
|
25
|
+
gem.summary = 'For reading or writing configuration files using YAML.'
|
26
|
+
gem.description = "Installs the ypath tool for reading YAML files using an XPath-like syntax. Installs the ytemplates tool for writing ERB templates using YAML files as the template binding object."
|
27
|
+
gem.email = ['madeonamac@gmail.com']
|
28
|
+
gem.authors = ['Gabe McArthur']
|
29
|
+
gem.homepage = 'http://github.com/gabemc/ytools'
|
30
|
+
gem.files = FileList["[A-Z]*", "{bin,lib,spec}/**/*"]
|
31
|
+
|
32
|
+
gem.add_development_dependency 'rspec', '>=1.3.0'
|
33
|
+
end
|
34
|
+
rescue LoadError
|
35
|
+
puts "Jeweler or dependencies are not available. Install it with: sudo gem install jeweler"
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "Deploys the gem to rubygems.org"
|
39
|
+
task :gem => :release do
|
40
|
+
system("gem build ytools.gemspec")
|
41
|
+
system("gem push ytools-#{YTools::Version.to_s}.gem")
|
42
|
+
end
|
43
|
+
|
44
|
+
desc "Does the full release cycle."
|
45
|
+
task :deploy => [:gem, :clean] do
|
46
|
+
end
|
47
|
+
|
48
|
+
desc "Cleans the gem files up."
|
49
|
+
task :clean do
|
50
|
+
FileUtils.rm(Dir.glob('*.gemspec'))
|
51
|
+
FileUtils.rm(Dir.glob('*.gem'))
|
52
|
+
end
|
data/bin/ypath
ADDED
data/bin/ytemplates
ADDED
data/lib/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'ytools/errors'
|
3
|
+
require 'ytools/version'
|
4
|
+
|
5
|
+
module YTools
|
6
|
+
|
7
|
+
class BaseCLI
|
8
|
+
attr_reader :options, :args
|
9
|
+
|
10
|
+
def initialize(args)
|
11
|
+
@args = args
|
12
|
+
@options = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute!
|
16
|
+
begin
|
17
|
+
sargs = args.dup
|
18
|
+
parse(sargs)
|
19
|
+
validate(sargs)
|
20
|
+
execute(sargs)
|
21
|
+
rescue SystemExit => e
|
22
|
+
raise
|
23
|
+
rescue YTools::ConfigurationError => e
|
24
|
+
print_error(e.message)
|
25
|
+
rescue OptionParser::InvalidOption => e
|
26
|
+
print_error(e.message)
|
27
|
+
rescue Exception => e
|
28
|
+
STDERR.puts e.backtrace
|
29
|
+
print_error(e.message)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
def parse(args)
|
35
|
+
# To override
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate(args)
|
39
|
+
# To override
|
40
|
+
end
|
41
|
+
|
42
|
+
def execute(args)
|
43
|
+
# To override
|
44
|
+
end
|
45
|
+
|
46
|
+
def print_version
|
47
|
+
puts "#{File.basename($0)} version: #{YTools::Version.to_s}"
|
48
|
+
exit 0
|
49
|
+
end
|
50
|
+
|
51
|
+
def print_examples(basedir)
|
52
|
+
examples = File.join(basedir, 'examples.txt')
|
53
|
+
File.open(examples, 'r') do |f|
|
54
|
+
f.each_line do |line|
|
55
|
+
puts line
|
56
|
+
end
|
57
|
+
end
|
58
|
+
exit 0
|
59
|
+
end
|
60
|
+
|
61
|
+
def print_error(e)
|
62
|
+
STDERR.puts "ERROR: #{File.basename($0)}: #{e}"
|
63
|
+
STDERR.puts "ERROR: #{File.basename($0)}: Try '--help' for more information"
|
64
|
+
exit 1
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'ytools/basecli'
|
3
|
+
require 'ytools/errors'
|
4
|
+
require 'ytools/path/executor'
|
5
|
+
|
6
|
+
module YTools::Path
|
7
|
+
class CLI < YTools::BaseCLI
|
8
|
+
protected
|
9
|
+
def execute(sargs)
|
10
|
+
begin
|
11
|
+
executor = Executor.new(options[:path], sargs)
|
12
|
+
|
13
|
+
if options[:debug]
|
14
|
+
STDERR.puts executor.yaml_object
|
15
|
+
end
|
16
|
+
|
17
|
+
puts executor.process!
|
18
|
+
rescue YTools::Path::ParseError => e
|
19
|
+
print_path_error(e)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse(args)
|
24
|
+
OptionParser.new do |opts|
|
25
|
+
opts.banner = "Usage: #{File.basename($0)} [OPTIONS] YAML_FILES"
|
26
|
+
opts.separator <<EOF
|
27
|
+
Description:
|
28
|
+
This tool uses a kind of XPath syntax for locating and printing elements
|
29
|
+
from within YAML files. Check out the '--examples' flag for details
|
30
|
+
on the exact path syntax.
|
31
|
+
|
32
|
+
It accepts multiple yaml files, and will merge their contents in the
|
33
|
+
order in which they are given. Thus, files listed later, if their
|
34
|
+
keys conflict with ones listed earlier, override the earlier listed
|
35
|
+
values. If you pass in files that don't exist, no error will be
|
36
|
+
raised unless the '--strict' flag is passed.
|
37
|
+
|
38
|
+
Options:
|
39
|
+
EOF
|
40
|
+
|
41
|
+
opts.on('-p', '--path PATTERN',
|
42
|
+
"The pattern to use to access the",
|
43
|
+
"configuration.") do |p|
|
44
|
+
options[:path] = p
|
45
|
+
end
|
46
|
+
opts.on('-s', '--strict',
|
47
|
+
"Checks to make sure all of the YAML files",
|
48
|
+
"exist before proceeding.") do |s|
|
49
|
+
options[:strict] = true
|
50
|
+
end
|
51
|
+
opts.separator ""
|
52
|
+
|
53
|
+
opts.on('-e', '--examples',
|
54
|
+
"Show some examples on how to use the",
|
55
|
+
"path syntax.") do
|
56
|
+
print_examples(File.dirname(__FILE__))
|
57
|
+
end
|
58
|
+
opts.on('-v', '--version',
|
59
|
+
"Show the version information") do |v|
|
60
|
+
print_version
|
61
|
+
end
|
62
|
+
opts.on('-d', '--debug',
|
63
|
+
"Prints out the merged yaml as a",
|
64
|
+
"ruby object to STDERR.") do |d|
|
65
|
+
options[:debug] = true
|
66
|
+
end
|
67
|
+
opts.on('-h', '--help',
|
68
|
+
"Show this help message.") do
|
69
|
+
puts opts
|
70
|
+
exit 0
|
71
|
+
end
|
72
|
+
end.parse!(args)
|
73
|
+
end
|
74
|
+
|
75
|
+
def validate(args)
|
76
|
+
if options[:path].nil?
|
77
|
+
raise YTools::ConfigurationError.new("The path expression was empty.")
|
78
|
+
end
|
79
|
+
if args.length == 0
|
80
|
+
raise YTools::ConfigurationError.new("No YAML files given as arguments")
|
81
|
+
end
|
82
|
+
|
83
|
+
if options[:strict]
|
84
|
+
args.each do |arg|
|
85
|
+
if !File.exists?(arg)
|
86
|
+
raise YTools::ConfigurationError.new("Non-existant YAML file: #{arg}")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def print_path_error(e)
|
93
|
+
STDERR.puts "ERROR: Path error: #{e.token.path}"
|
94
|
+
spacer = "ERROR: "
|
95
|
+
e.token.offset.downto(1) do
|
96
|
+
spacer << " "
|
97
|
+
end
|
98
|
+
spacer << "^"
|
99
|
+
STDERR.puts spacer
|
100
|
+
print_error("Path expression parsing error - #{e.message}")
|
101
|
+
end
|
102
|
+
end # CLI
|
103
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
Examples
|
2
|
+
--------
|
3
|
+
|
4
|
+
Something like XPath, we use dotted expressions to pull out
|
5
|
+
specific configuration values.
|
6
|
+
|
7
|
+
The path expression have a particular syntax:
|
8
|
+
|
9
|
+
- YAML hashes are accessed using the '/' syntax, so
|
10
|
+
---
|
11
|
+
this:
|
12
|
+
goes: here
|
13
|
+
---
|
14
|
+
would access the value 'here' with the expression '/this/goes'.
|
15
|
+
|
16
|
+
If the key itself has '/' characters, wrap the key in '|'s, so
|
17
|
+
---
|
18
|
+
this/is/complex:
|
19
|
+
goes: here
|
20
|
+
---
|
21
|
+
would access the value 'here' with the expression
|
22
|
+
'/|this/is/complex|/goes'. The '|' can be escaped with '\|'
|
23
|
+
|
24
|
+
- Array values can be specified by index so
|
25
|
+
---
|
26
|
+
this:
|
27
|
+
- is a list
|
28
|
+
- of values
|
29
|
+
---
|
30
|
+
would access the 2nd array value 'of values' with the
|
31
|
+
expression '/this[1]'
|
32
|
+
|
33
|
+
- If you want all of the values in a list or in a collection
|
34
|
+
of descendants, each value on a separate line,
|
35
|
+
---
|
36
|
+
this:
|
37
|
+
- name: joe
|
38
|
+
age: 32
|
39
|
+
- name: jack
|
40
|
+
age: 10
|
41
|
+
---
|
42
|
+
would access all of ages could be accessed by '/this//age'
|
43
|
+
and would return:
|
44
|
+
|
45
|
+
32
|
46
|
+
10
|
47
|
+
|
48
|
+
- If you want all the keys in a hash, each key on a
|
49
|
+
separate line,
|
50
|
+
---
|
51
|
+
this:
|
52
|
+
host1:
|
53
|
+
value: blip
|
54
|
+
host2:
|
55
|
+
value: bloom
|
56
|
+
host3:
|
57
|
+
value: blam
|
58
|
+
---
|
59
|
+
you would access 'host[1-3]' with the expression '/this'.
|
60
|
+
|
61
|
+
Caveats
|
62
|
+
-------
|
63
|
+
Missing elements output nothing. Malformed path expressions
|
64
|
+
print an error and exit 1.
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'ytools/yaml_object'
|
2
|
+
require 'ytools/path/parser'
|
3
|
+
|
4
|
+
module YTools::Path
|
5
|
+
|
6
|
+
class Executor
|
7
|
+
attr_reader :path, :yaml_object
|
8
|
+
|
9
|
+
def initialize(path, files)
|
10
|
+
@path = path
|
11
|
+
@yaml_object = YTools::YamlObject.from_files(files)
|
12
|
+
end
|
13
|
+
|
14
|
+
def process!
|
15
|
+
parser = Parser.new(path)
|
16
|
+
selectors = parser.parse!
|
17
|
+
|
18
|
+
found = selectors.select(yaml_object)
|
19
|
+
if found.is_a?(YTools::YamlObject)
|
20
|
+
show_yaml_object(found)
|
21
|
+
elsif found.is_a?(Array)
|
22
|
+
show_array(found)
|
23
|
+
else
|
24
|
+
found.to_s
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def show_yaml_object(found)
|
30
|
+
output = ""
|
31
|
+
first = true
|
32
|
+
found.yhash.each_key do |key|
|
33
|
+
if first
|
34
|
+
first = false
|
35
|
+
else
|
36
|
+
output << "\n"
|
37
|
+
end
|
38
|
+
output << key.to_s
|
39
|
+
end
|
40
|
+
output
|
41
|
+
end
|
42
|
+
|
43
|
+
def show_array(found)
|
44
|
+
output = ""
|
45
|
+
first = true
|
46
|
+
found.each do |found|
|
47
|
+
if first
|
48
|
+
first = false
|
49
|
+
else
|
50
|
+
output << "\n"
|
51
|
+
end
|
52
|
+
output << found.to_s
|
53
|
+
end
|
54
|
+
output
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'ytools/errors'
|
2
|
+
|
3
|
+
module YTools::Path
|
4
|
+
|
5
|
+
Token = Struct.new(:path, :offset, :length, :type) do
|
6
|
+
def value
|
7
|
+
case type
|
8
|
+
when :path_separator then '/'
|
9
|
+
when :path_part then path # We cheat and put in the actual
|
10
|
+
# value of the path part here,
|
11
|
+
# because we don't care about the offset
|
12
|
+
when :lbrace then '['
|
13
|
+
when :rbrace then ']'
|
14
|
+
when :number then path[offset, length].to_i
|
15
|
+
else raise YPath::PathError("Unrecognized token type!!!!")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Lexer
|
21
|
+
attr_reader :offset, :path
|
22
|
+
|
23
|
+
def initialize(path)
|
24
|
+
@path = path
|
25
|
+
@offset = 0
|
26
|
+
@buffer = []
|
27
|
+
end
|
28
|
+
|
29
|
+
def [](index)
|
30
|
+
if offset + index >= path.length
|
31
|
+
nil
|
32
|
+
else
|
33
|
+
@path[offset + index]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def next
|
38
|
+
if @buffer.length > 0
|
39
|
+
@buffer.pop
|
40
|
+
else
|
41
|
+
token
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def peek(count=nil)
|
46
|
+
count ||= 0
|
47
|
+
|
48
|
+
if count >= @buffer.length
|
49
|
+
(@buffer.length - count).downto(0) do
|
50
|
+
@buffer.push(token)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
@buffer[count]
|
55
|
+
end
|
56
|
+
|
57
|
+
def has_next?
|
58
|
+
!peek.nil?
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
def token
|
63
|
+
return nil if offset >= path.length
|
64
|
+
|
65
|
+
case @path[offset]
|
66
|
+
when ?/ then path_separator
|
67
|
+
when ?[ then lbrace
|
68
|
+
when ?] then rbrace
|
69
|
+
when ?- then path_part
|
70
|
+
when ?0..?9 then number
|
71
|
+
else path_part
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def path_separator
|
76
|
+
tok = Token.new(path, offset, 1, :path_separator)
|
77
|
+
@offset += 1
|
78
|
+
tok
|
79
|
+
end
|
80
|
+
|
81
|
+
def lbrace
|
82
|
+
tok = Token.new(path, offset, 1, :lbrace)
|
83
|
+
@offset += 1
|
84
|
+
tok
|
85
|
+
end
|
86
|
+
|
87
|
+
def rbrace
|
88
|
+
tok = Token.new(path, offset, 1, :rbrace)
|
89
|
+
@offset += 1
|
90
|
+
tok
|
91
|
+
end
|
92
|
+
|
93
|
+
def number
|
94
|
+
starting_offset = offset
|
95
|
+
|
96
|
+
while offset < path.length
|
97
|
+
case path[offset]
|
98
|
+
when ?0..?9 then @offset += 1
|
99
|
+
else break
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
Token.new(path, starting_offset, offset - starting_offset, :number)
|
104
|
+
end
|
105
|
+
|
106
|
+
def path_part
|
107
|
+
starting_offset = offset
|
108
|
+
in_bar = false
|
109
|
+
str = ""
|
110
|
+
|
111
|
+
while offset < path.length
|
112
|
+
case path[offset]
|
113
|
+
when ?\\
|
114
|
+
if offset + 1 >= path.length
|
115
|
+
raise YTools::PathError.new("Last character in a path cannot be a '\\' character: '#{path}'")
|
116
|
+
end
|
117
|
+
|
118
|
+
lookahead = path[offset + 1]
|
119
|
+
case lookahead
|
120
|
+
when ?[ , ?] , ?| , ?\ , ?/
|
121
|
+
str << lookahead
|
122
|
+
@offset += 2
|
123
|
+
else
|
124
|
+
raise YTools::PathError.new("Unescaped backslash character at position #{offset} in '#{path}'")
|
125
|
+
end
|
126
|
+
when ?|
|
127
|
+
in_bar = !in_bar
|
128
|
+
@offset += 1
|
129
|
+
when ?/
|
130
|
+
if in_bar
|
131
|
+
str << ?/
|
132
|
+
@offset += 1
|
133
|
+
else
|
134
|
+
return path_or_number(str, starting_offset, offset - starting_offset)
|
135
|
+
end
|
136
|
+
when ?[ , ?] , ?@ then
|
137
|
+
return path_or_number(str, starting_offset, offset - starting_offset)
|
138
|
+
else
|
139
|
+
if !path[offset].nil?
|
140
|
+
str << path[offset]
|
141
|
+
@offset += 1
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
if in_bar
|
147
|
+
raise YTools::PathError.new("There was no closing bar '|' character in the path '#{path}'")
|
148
|
+
end
|
149
|
+
|
150
|
+
return path_or_number(str, starting_offset, offset - starting_offset)
|
151
|
+
end
|
152
|
+
|
153
|
+
# FIXME: there's a bug in here somewhere about the offset for numbers/paths.
|
154
|
+
|
155
|
+
def path_or_number(str, start, length)
|
156
|
+
i = str.to_i
|
157
|
+
if i == 0
|
158
|
+
return Token.new(str, start, length, :path_part)
|
159
|
+
elsif i.to_s == str
|
160
|
+
return Token.new(str, 0, length, :number)
|
161
|
+
else
|
162
|
+
return Token.new(str, start, length, :path_part)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end # PathLexer
|
166
|
+
end
|