cowtech-lib 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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