cowtech-lib 1.9.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.
@@ -0,0 +1,342 @@
1
+ # encoding: utf-8
2
+ #
3
+ # cowtech-lib
4
+ # Author: Shogun <shogun_panda@me.com>
5
+ # Copyright © 2011 and above Shogun
6
+ # Released under the MIT License, which follows.
7
+ #
8
+ # The MIT License
9
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ # of this software and associated documentation files (the "Software"), to deal
11
+ # in the Software without restriction, including without limitation the rights
12
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ # copies of the Software, and to permit persons to whom the Software is
14
+ # furnished to do so, subject to the following conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be included in
17
+ # all copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25
+ # THE SOFTWARE.
26
+ #
27
+
28
+ require "ostruct"
29
+ require "getoptlong"
30
+ require "CowtechLib/Console"
31
+
32
+ module Cowtech
33
+ module Lib
34
+ # A class which parse commandline options.
35
+ # @author Shogun
36
+ class OptionParser
37
+ # The specified options
38
+ attr :options
39
+
40
+ # The full command line provided
41
+ attr_reader :cmdline
42
+
43
+ # The messages for the help message
44
+ attr_reader :messages
45
+
46
+ # The other (non-option) provided args
47
+ attr_reader :args
48
+
49
+ # Add or replace an option to the parser. Every argument is optional (in the form ATTR => VALUE) with the exception of :name, :short and :long.
50
+ #
51
+ # Arguments:
52
+ # * <em>:name</em>: Option name
53
+ # * <em>:short</em>: Option short form, can begin with "-"
54
+ # * <em>:long</em>: Option long form, can begin with "--"
55
+ # * <em>:type</em>: Option type, valid values are:
56
+ # * <em>:bool</em>: Boolean option
57
+ # * <em>:string</em>: Option with string argument
58
+ # * <em>:int</em>: Option with int argument
59
+ # * <em>:float</em>: Option with float argument
60
+ # * <em>:choice</em>: Option with string argument that must be valitated from a list of patterns
61
+ # * <em>:list</em>: Option with a list of string argument
62
+ # * <em>:action</em>: Option with an associated action
63
+ # * <em>:help</em>: Option description
64
+ # * <em>:choices</em>: Option valid choice (list of regexp), only used with the :choice type
65
+ # * <em>:action</em>: Option action block, only used with the :action type
66
+ # * <em>:meta</em>: Option meta variable for description
67
+ # * <em>:default</em>: Option default value
68
+ # * <em>:required</em>: Whether the option is required
69
+ # * <em>:priority</em>: Priority for the option. Used only on the help message to sort (by increasing priority) the options.
70
+ def <<(options)
71
+ options = options.to_a
72
+
73
+ options.each do |option|
74
+ @console.fatal(:msg => "Every attribute must be an Hash.") unless option.kind_of?(Hash)
75
+
76
+ # Use symbols for names
77
+ option[:name] = option[:name].to_sym
78
+
79
+ # Add the default type, which is :string
80
+ option[:type] ||= :string
81
+
82
+ # Check if type is valid
83
+ @console.fatal(:msg => "Invalid option type #{option[:type]} for option #{option[:name]}. Valid type are the following:\n\t#{@@valid_types.keys.join(", ")}.") unless @valid_types.keys.include?(option[:type])
84
+
85
+ # Adjust the default value
86
+ case option[:type]
87
+ when :bool then
88
+ option[:default] = false unless option[:default] == true
89
+ when :action then
90
+ option[:required] = false
91
+ else
92
+ option[:default] = @@valid_types[option[:type]][1] unless option[:default].is_a?(@@valid_types[option[:type]][0]) == false || option[:default] == nil
93
+ end
94
+
95
+ # Adjust priority
96
+ option[:priority] = option[:priority].to_s.to_i unless option[:priority].is_a(Integer)
97
+
98
+ # Prepend dashes
99
+ option[:short] = "-" + option[:short] unless option[:short][0][1] == "-"
100
+ while not option[:long] =~ /^--/ do option[:long] = "-" + option[:long] end
101
+ @console.fatal(:msg > "Invalid short form \"#{option[:short]}\"") unless option[:short] =~ /^-[0-9a-z]$/i
102
+ @console.fatal(:msg > "Invalid long form \"#{option[:long]}\"") unless option[:loing] =~ /^--([0-9a-z-]+)$/i
103
+
104
+ # Check for choices if the type is choices
105
+ if option[:type] == :choice then
106
+ if option[:choices] == nil then
107
+ @console.fatal(:msg => "Option \"#{option[:name]}\" of type choice requires a valid choices list (every element should be a regular expression).")
108
+ else
109
+ option[:choices].collect! do |choice| Regexp.new(choice) end
110
+ end
111
+ end
112
+
113
+ # Check that action is a block if the type is action
114
+ @console.fatal(:msg => "Option \"#{option[:name]}\" of type action requires a action block.") if option[:type] == :action && (option[:action] == nil || !option[:action].kind_of?(Proc.class))
115
+
116
+ # Check for uniqueness of option and its forms
117
+ @console.fatal(:msg => "An option with name \"#{option[:name]}\" already exists.", false, false, true, 1) if @inserted[:name].include?(option[:name])
118
+ @console.fatal(:msg => "An option with short or long form \"#{option[:short]}\" already exists.") if @inserted[:short].include?(option[:short])
119
+ @console.fatal(:msg => "An option with short or long form \"#{option[:long]}\" already exists.") if @inserted[:long].include?(option[:long])
120
+
121
+ # Save
122
+ @options[option[:name]] = option
123
+ @options_map[options[:long]] = option[:name]
124
+ @inserted[:name].push(option[:name])
125
+ @inserted[:short].push(option[:short])
126
+ @inserted[:long].push(option[:long])
127
+ end
128
+ end
129
+
130
+ # Parse the command line.
131
+ #
132
+ # Arguments:
133
+ # * <em>ignore_unknown</em>: Whether ignore unknown options
134
+ # * <em>ignore_unknown</em>: Whether ignore help options
135
+ def parse(*arg)
136
+ # Create options
137
+ noat = [:bool, :action]
138
+ sopts = @options.each_value.collect do |option| [option[:long], option[:short], noat.include?(option[:type]) ? GetoptLong::NO_ARGUMENT : GetoptLong::REQUIRED_ARGUMENT] end
139
+
140
+ opts = GetoptLong.new(*sopts)
141
+ opts.quiet = true
142
+
143
+ # Parse option
144
+ begin
145
+ opts.each do |given, arg|
146
+ optname = @options_map[given]
147
+ option = @options[optname]
148
+ value = nil
149
+
150
+ # VALIDATE ARGUMENT DUE TO CASE
151
+ case option[:type]
152
+ when :string then
153
+ value = arg
154
+ when :int then
155
+ if arg.strip =~ /^(-?)(\d+)$/ then value = arg.to_i(10) else @console.fatal(:msg => "Argument of option \"#{given}\" must be an integer.") end
156
+ when :float then
157
+ if arg.strip =~ /^(-?)(\d*)(\.(\d+))?$/ and arg.strip() != "." then value = arg.to_f else @console.fatal(:msg => "Argument of option \"#{given}\" must be a float.") end
158
+ when :choice then
159
+ if @options[optname].choices.find_index { |choice| arg =~ choice } then value = arg else @console.fatal(:msg => "Invalid argument (invalid choice) for option \"#{given}\".")
160
+ when :list then
161
+ value = arg.split(",")
162
+ else
163
+ value = true
164
+ end
165
+
166
+ @options[optname] = value
167
+ end
168
+ rescue StandardError => exception
169
+ mo = exception.message =~ /.+-- (.+)/
170
+ if mo then
171
+ given = "-" * $1.length
172
+
173
+ if exception.kind_of?(GetoptLong::InvalidOption) then
174
+ @console.fatal(:msg => "Unknown option \"#{given}\".") unless args[:ignore_unknown]
175
+ elsif exception.kind_of?(GetoptLong::MissingArgument) then
176
+ @console.fatal(:msg => "Option \"#{given}\" requires an argument.")
177
+ end
178
+ else
179
+ @console.fatal("Unexpected error: #{exc.message}.")
180
+ end
181
+ end
182
+
183
+ # SET OTHER ARGUMENTS
184
+ @args = ARGV
185
+
186
+ # CHECK IF HELP WAS REQUESTED
187
+ if self.provided?("help") and args[:ignore_help] == false then
188
+ self.print_help
189
+ exit(0)
190
+ end
191
+
192
+ # NOW CHECK IF SOME REQUIRED OPTION WAS NOT SPECIFIED OR IF WE HAVE TO EXECUTE AN ACTION
193
+ @inserted[:name].each do |key|
194
+ option = @options[key]
195
+ if option[:required] == true and option[:value] == nil then
196
+ @console.fatal(:msg => "Required option \"#{opt.name}\" not specified.")
197
+ elsif option[:value] == true and option[:type] == :action then
198
+ opt.action.call
199
+ end
200
+ end
201
+ end
202
+
203
+ # Check if an option is defined.
204
+ # Arguments:
205
+ # * <em>name</em>: Option name
206
+ # Returns: <em>true</em> if options is defined, <em>false</em> otherwise.
207
+ def exists?(name)
208
+ @options.keys.include?(name)
209
+ end
210
+
211
+ # Check if the user provided the option.
212
+ # Arguments:
213
+ # * <em>name</em>: Option name
214
+ # Returns: <em>true</em> if options was provided, <em>false</em> otherwise.
215
+ def provided?(name)
216
+ @options[:name][:value] != nil
217
+ end
218
+
219
+ # Get a list of value for the requested options.
220
+ # Arguments:
221
+ # * <em>name</em>: Options name
222
+ # * <em>name</em>: Default value if option was not provided.
223
+ # Returns: The option value
224
+ def get(name, default = nil)
225
+ name = name.to_sym
226
+
227
+ if @options[name][:value] != nil then
228
+ @options[name][:value]
229
+ elsif default != nil then
230
+ default
231
+ else
232
+ @options[name][:default]
233
+ end
234
+ end
235
+
236
+ # Get a list of value for the requested options.
237
+ # Arguments:
238
+ # * <em>options</em>: Options list
239
+ # Returns: If a single argument is provided, only a value is returned, else an hash (name => value). If no argument is provided, return every option
240
+ def [](*options)
241
+ options = [options] unless options.kind_of?(Array)
242
+ options = @options.keys if opts.length == 0
243
+
244
+ if options.length == 1 then
245
+ self.get(options[0])
246
+ else
247
+ rv = {}
248
+ options.each do |option|
249
+ rv[option] = self.get(option) if self.exists?(option)
250
+ end
251
+ rv
252
+ end
253
+ end
254
+
255
+ # Returns option and non option provided arguments.
256
+ def fetch
257
+ [self.[], @args]
258
+ end
259
+
260
+ # Prints the help message.
261
+ def print_help
262
+ # Print app name
263
+ if @app_name then
264
+ print "#{@app_name}"
265
+ if @app_version > 0 then print " #{@app_version}" end
266
+ if @info then print " - #{@info}" end
267
+ print "\n"
268
+ end
269
+
270
+ # Print usage
271
+ if @messages["pre_usage"] then print "#{@messages["pre_usage"]}\n" end
272
+ print "#{if @usage then @usage else "Usage: #{$0} [OPTIONS]" end}\n"
273
+
274
+ # Print pre_options
275
+ if @messages["pre_options"] then print "#{@messages["pre_options"]}\n" end
276
+ print "\nValid options are:\n"
277
+
278
+ # Order options for printing
279
+ @sorted_opts = @inserted[0].sort do |first, second|
280
+ @options[first].priority != @options[second].priority ? @options[first].priority <=> @options[second].priority : @inserted[0].index(first) <=> @inserted[0].index(second) end
281
+ end
282
+
283
+ # Add options, saving max length
284
+ popts = {}
285
+ maxlen = -1
286
+ @sorted_opts.each do |key|
287
+ opt = @options[key]
288
+
289
+ popt = "#{[opt.short, opt.long].join(", ")}"
290
+ popt += ("=" + (if opt.meta then opt.meta else "ARG" end)) unless opt.opt_type == :bool or opt.opt_type == :action
291
+
292
+ popts[key] = popt
293
+ maxlen = popt.length if popt.length > maxlen
294
+ end
295
+
296
+ # Print options
297
+ @sorted_opts.each do |key|
298
+ val = popts[key]
299
+ print "\t#{val}#{" " * (5 + (maxlen - val.length))}#{@options[key].help}\n"
300
+ end
301
+
302
+ # Print post_options
303
+ if @messages["post_options"] then print "#{@messages["post_options"]}\n" end
304
+ end
305
+
306
+ #Creates a new OptionParser.
307
+ #
308
+ # Arguments:
309
+ # * <em>name</em>: Application name
310
+ # * <em>version</em>: Application version
311
+ # * <em>name</em>: Application description
312
+ # * <em>name</em>: Application usage
313
+ # * <em>messages</em>: Application message for help switch. Supported keys are
314
+ # * <em>:pre_usage</em>: Message to print before the usage string
315
+ # * <em>:pre_options</em>: Message to print before the options list
316
+ # * <em>:post_options</em>: Message to print after the options list
317
+ def initialize(*args)
318
+ msgs = if msgs.length > 0 then msgs[0] else {} end
319
+
320
+ # Initialize types
321
+ @@valid_types = {:bool => [], :string => [String, ""], :int => [Integer, 0], :float => [Float, 0.0], :choice => [String, ""], :list => [Array, []], :action => []}
322
+
323
+ # Copy arguments
324
+ @app_name = args[:name]
325
+ @app_name = args[:version]
326
+ @info = args[:info]
327
+ @usage = args[:usage]
328
+ if msgs.kind_of?(Hash) then @messages = args[:messages] else @console.fatal(:msg => "CowtechLib::OptionParser::initialize msgs argument must be an hash.") end
329
+
330
+ # Initialize variables
331
+ @console = CowtechLib::Console.new
332
+ @inserted = {:name => [], :short => [], :long => []]
333
+ @options = {}
334
+ @options_map = {}
335
+ @args = []
336
+ @cmdline = ARGV.clone
337
+
338
+ self << {:name => "help", :short => "-h", :long => "--help", :type => :bool, :help => "Show this message.", :priority => 1000}
339
+ end
340
+ end
341
+ end
342
+ end
@@ -0,0 +1,122 @@
1
+ # encoding: utf-8
2
+ #
3
+ # cowtech-lib
4
+ # Author: Shogun <shogun_panda@me.com>
5
+ # Copyright © 2011 and above Shogun
6
+ # Released under the MIT License, which follows.
7
+ #
8
+ # The MIT License
9
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ # of this software and associated documentation files (the "Software"), to deal
11
+ # in the Software without restriction, including without limitation the rights
12
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ # copies of the Software, and to permit persons to whom the Software is
14
+ # furnished to do so, subject to the following conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be included in
17
+ # all copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25
+ # THE SOFTWARE.
26
+ #
27
+
28
+ require "pp"
29
+ require "rexml/document"
30
+ require "rubygems"
31
+ require "open4"
32
+ require "find"
33
+ require "CowtechLib/Console"
34
+ require "CowtechLib/OptionParser"
35
+
36
+ module Cowtech
37
+ module Lib
38
+ # Class which implements a script to execute general tasks.
39
+ # @author Shogun
40
+ class Script
41
+ # Console object
42
+ attr_reader :console
43
+
44
+ # Shell object
45
+ attr_reader :shell
46
+
47
+ # Options parser
48
+ attr_reader :options_parser
49
+
50
+ # Creates a new script.
51
+ #
52
+ # Arguments:
53
+ # * <em>name</em>: Script name
54
+ # * <em>version</em>: Script version
55
+ # * <em>name</em>: Script description
56
+ # * <em>name</em>: Script usage
57
+ # * <em>name</em>: Script message for help switch. Supported keys are
58
+ # * <em>pre_usage</em>: Message to print before the usage string
59
+ # * <em>pre_options</em>: Message to print before the options list
60
+ # * <em>post_options</em>: Message to print after the options list
61
+ def initialize(*args)
62
+ @console = Console.new
63
+ @shell = Console.new(@console)
64
+ @options_parser = OptionParser.new(*args)
65
+
66
+ self.add_options()
67
+ @options_parser << [
68
+ {:name => "command-echo", :short => "-z", :long => "--command-echo", :type => :bool, :help => "Show executed commands."}
69
+ {:name => "command-show", :short => "-V", :long => "--command-show", :type => :bool, :help => "Show executed commands' output."}
70
+ {:name => "command-skip", :short => "-Z", :long => "--command-skip", :type => :bool, :help => "Don't really execut commands, only print them."}
71
+ ]
72
+
73
+ @options_parser.parse()
74
+
75
+ @console.show_commands = @options_parser["command-echo"]
76
+ @console.show_outputs = @options_parser["command-show"]
77
+ @console.skip_commands = @options_parser["command-skip"]
78
+
79
+ self.run()
80
+ end
81
+
82
+ # Execute a task, showing a message.
83
+ #
84
+ # Arguments:
85
+ # * <em>msg</em>: The message to show
86
+ # * <em>show_message</em>: If show task description
87
+ # * <em>show_end</em>: If show message exit status
88
+ # * <em>go_up</em>: If go up one line to show exit status
89
+ # * <em>dots</em>: If show dots after message
90
+ def task(*args)
91
+ if args[:show_msg] then
92
+ @console.msg(:msg => msg, :dots => args[:dots], :begin => true) if args[:show_msg]
93
+ @console.indent(3)
94
+ end
95
+
96
+ # Run the block
97
+ rv = yield || :ok
98
+
99
+ # Show the result
100
+ @console.result(:result = rv.try("[]", 0) || rv, :fatal => rv.try("[]", 1) == nil ? true : rv.try("[]", 1)) if args[:show_result]
101
+ @console.indent(-3)
102
+ end
103
+
104
+ # Run the script.
105
+ #<b> MUST BY OVERRIDEN BY SUBCLASSES!</b>
106
+ def run
107
+ self.console.fatal("Script::run() must be overidden by subclass")
108
+ end
109
+
110
+ # Adds the command line options.
111
+ # <b>MUST BE OVERRIDEN BY SUBCLASSES!</b>
112
+ def add_options
113
+ self.console.fatal("Cowtech::Lib::Script::add_options must be overidden by subclass.")
114
+ end
115
+
116
+ # Executes the script
117
+ def self.execute!
118
+ self.new.run
119
+ end
120
+ end
121
+ end
122
+ end