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 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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'ytools/path/cli'
5
+ YTools::Path::CLI.new(ARGV).execute!
data/bin/ytemplates ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'ytools/templates/cli'
5
+ YTools::Templates::CLI.new(ARGV).execute!
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,7 @@
1
+
2
+ module YTools
3
+ Error = Class.new(RuntimeError)
4
+
5
+ ConfigurationError = Class.new(YTools::Error)
6
+ PathError = Class.new(YTools::Error)
7
+ 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