cli_tool 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,6 @@
1
1
  require 'getoptlong'
2
+ require 'pry'
3
+ require 'awesome_print'
2
4
 
3
5
  module CliTool
4
6
  module OptionParser
@@ -6,15 +8,30 @@ module CliTool
6
8
  # Use to add the methods below to any class
7
9
  def self.included(base)
8
10
  base.extend(ClassMethods)
11
+ base.options({
12
+ debug: {
13
+ argument: :none,
14
+ documentation: [
15
+ "This is used to trigger debug mode in your app. It will be set to #{base}.debug.",
16
+ "In debug mode we do not respect the secure option and your secure fields will be displayed in clear text!",
17
+ [:black, :white_bg]
18
+ ]
19
+ },
20
+ help: {
21
+ argument: :none,
22
+ short: :'?',
23
+ documentation: "Shows this help record."
24
+ }
25
+ })
9
26
  end
10
27
 
11
28
  module ClassMethods
12
29
 
13
30
  # Map for symbol types
14
31
  GOL_MAP = {
15
- :none => GetoptLong::NO_ARGUMENT,
16
- :optional => GetoptLong::OPTIONAL_ARGUMENT,
17
- :required => GetoptLong::REQUIRED_ARGUMENT
32
+ none: GetoptLong::NO_ARGUMENT,
33
+ optional: GetoptLong::OPTIONAL_ARGUMENT,
34
+ required: GetoptLong::REQUIRED_ARGUMENT
18
35
  }
19
36
 
20
37
  # Create the options array
@@ -24,93 +41,319 @@ module CliTool
24
41
  # If no options were passed then return
25
42
  return @@options.uniq unless opts
26
43
 
27
- _create_attrs_(opts)
28
- default_options(opts)
44
+ _create_preprocess_(opts)
29
45
  @@options = @@options.concat(_create_gola_(opts)).uniq
30
46
  end
31
47
 
32
- def default_options(opts = {})
33
- @@defaults ||= []
48
+ # Ensure the right format of the options (primarily dashes and casing)
49
+ def optionify(option, retval = false)
50
+ if option.is_a?(Array)
51
+ optionify_all(option)
52
+ else
53
+ option = "#{option}".gsub(/^[\-]+/, '').gsub(/(-| )/, '_').to_sym
34
54
 
35
- # If no options were passed then return
36
- return @@defaults.uniq.inject({}) { |o,(k,v)| o[k] = v; o } unless opts
55
+ # Help us get the primary option over the alias
56
+ option =
57
+ case retval
58
+ when :set, :setter
59
+ "#{option}="
60
+ when :primary
61
+ all_opts = __get_options(:all_options)
37
62
 
38
- # Set the default options
39
- opts.each do |opt, details|
40
- next unless details.is_a?(Hash)
41
- next unless details[:default]
42
- opt = opt.first if opt.is_a?(Array)
43
- @@defaults << [optionify(opt), details[:default]]
63
+ if all_opts.has_key?(option)
64
+ option
65
+ else
66
+ all_opts.map { |opt, option_args|
67
+ aliases = option_args[:aliases]
68
+ if aliases && aliases.include?(option)
69
+ opt
70
+ else
71
+ nil
72
+ end
73
+ }.compact.flatten.first
74
+ end
75
+ else
76
+ option
77
+ end
78
+
79
+ option.to_sym
44
80
  end
81
+ end
45
82
 
46
- @@defaults = @@defaults.uniq
83
+ def optionify_all(option, retval = false)
84
+ [option].compact.flatten.map { |x| optionify(x, retval) }
47
85
  end
48
86
 
49
- # Parse to correct option set
50
- def optionify(option, setter = false)
51
- option = "#{option}".gsub(/^[\-]+/, '').gsub(/(-| )/, '_')
52
- (setter ? option + '=' : option).to_sym
87
+ def trap!
88
+ Signal.trap("INT") do |signo|
89
+ if Signal.respond_to?(:signame)
90
+ signal = Signal.signame(signo)
91
+ puts "Received: #{signal}..."
92
+ puts "Exiting..."
93
+ else
94
+ puts "#{signo} Exiting..."
95
+ end
96
+ exit 1
97
+ end
53
98
  end
54
99
 
55
100
  # Handle running options
56
- def run(entrypoint = false, *args)
57
- if args.last.instance_of?(self)
58
- instance = args.pop
59
- else
60
- instance = new
61
- end
101
+ def run(entrypoint = false, *args, &block)
62
102
 
63
- # Option Setter Proc
103
+ # Get the object to work with
104
+ object =
105
+ if args.last.class <= self
106
+ args.pop
107
+ elsif self < Singleton
108
+ instance
109
+ else
110
+ new
111
+ end
112
+
113
+ # Get class variable hash
114
+ class_vars = __get_options
115
+
116
+ # Cache variables
117
+ exit_code = 0
118
+ max_puts_length = 0
119
+ processed_options = []
120
+ missing_arguments = []
121
+
122
+ # Option setter proc
64
123
  option_setter = Proc.new do |option, value|
65
- value = case value
66
- when ''
67
- true
68
- when 'true'
69
- true
70
- when 'false'
71
- false
124
+ option = optionify(option, :primary)
125
+ processed_options << option
126
+
127
+ # Help process values
128
+ value =
129
+ case value
130
+ when ''
131
+ true
132
+ when 'true'
133
+ true
134
+ when 'false'
135
+ false
136
+ when 'nil', 'null'
137
+ nil
138
+ else
139
+ value
140
+ end
141
+
142
+ # Run preprocessor on the data (if applicable)
143
+ preprocessor = class_vars[:preprocessors][option]
144
+ if preprocessor
145
+ value = object.__send__(:instance_exec, value, &preprocessor)
146
+ end
147
+
148
+ # Show notice of the setting being set on the instance
149
+ if class_vars[:private_options].include?(option) && ! instance.debug
150
+ m = "Setting @#{option} = #{'*' * value.length} :: Value hidden for privacy"
151
+ max_puts_length = m.length if m.length > max_puts_length
152
+ puts m, :blue
72
153
  else
73
- value
154
+ m = "Setting @#{option} = #{value}"
155
+ max_puts_length = m.length if m.length > max_puts_length
156
+ puts m, :green
74
157
  end
75
158
 
76
- puts "Setting @#{optionify(option)} = #{value}"
77
- instance.__send__(optionify(option, :set), value)
159
+ # Do the actual set for us
160
+ object.__send__(optionify(option, :set), value)
78
161
  end
79
162
 
80
- # Set options
81
- puts "CliTool... Loading Options..."
82
- default_options.each(&option_setter)
83
- GetoptLong.new(*options).each(&option_setter)
163
+ # Actually grab the options from GetoptLong and process them
164
+ puts "\nCliTool... Loading Options...\n", :blue
165
+ puts '#' * 29, [:red_bg, :red]
166
+ class_vars[:default_options].to_a.each(&option_setter)
167
+ begin
168
+ GetoptLong.new(*options).each(&option_setter)
169
+ rescue GetoptLong::MissingArgument => e
170
+ missing_arguments << e.message
171
+ end
172
+ puts '#' * 29, [:red_bg, :red]
84
173
  puts ''
85
174
 
175
+ # If we wanted help in the first place then don't do any option dependency validations
176
+ unless object.help
177
+
178
+ # Handle any missing arguments that are required!
179
+ unless missing_arguments.empty?
180
+ missing_arguments.each { |m| puts "The required #{m}", :red }
181
+ puts ''
182
+
183
+ object.help = true
184
+ exit_code = 1
185
+ end
186
+
187
+ # Get the missing options that were expected
188
+ missing_options = class_vars[:required_options].keys - processed_options
189
+
190
+ # Handle missing options and their potential alternatives
191
+ __slice_hash(class_vars[:required_options], *missing_options).each do |option, option_args|
192
+ if (option_args[:alternatives] & processed_options).empty?
193
+ object.help = true
194
+ exit_code = 1
195
+ else
196
+ missing_options.delete(option)
197
+ end
198
+ end
199
+
200
+ # Ensure that the dependencies for options are met (if applicable)
201
+ __slice_hash(class_vars[:required_options], *processed_options).each do |option, option_args|
202
+ missing_dependencies = option_args[:dependencies] - processed_options
203
+ missing_dependencies.each do |dep_opt|
204
+ puts "The option `--#{option}' expected a value for `--#{dep_opt}', but not was received", :red
205
+ missing_options << dep_opt
206
+
207
+ object.help = true
208
+ exit_code = 1
209
+ end
210
+
211
+ puts '' unless missing_dependencies.empty?
212
+ end
213
+
214
+ # Raise an error when required options were not provided.
215
+ # Change the exit code and enable help output (which will exit 1; on missing opts)
216
+ unless missing_options.empty?
217
+ missing_options.uniq.each do |option|
218
+ puts "The required option `--#{option}' was not provided.", :red
219
+ end
220
+
221
+ object.help = true
222
+ exit_code = 1
223
+ end
224
+ end
225
+
226
+ # Show the help text
227
+ if object.help || exit_code == 1
228
+ puts help(nil, missing_options || [])
229
+ exit(exit_code || 0)
230
+ end
231
+
86
232
  # Handle the entrypoint
87
233
  if entrypoint
88
234
  entrypoint = optionify(entrypoint)
89
- instance.__send__(entrypoint, *args)
235
+ object.__send__(entrypoint, *args, &block)
90
236
  else
91
- instance
237
+ object
92
238
  end
93
239
  end
94
240
 
95
- private
241
+ def help(message = nil, missing_options = [])
242
+ if message.nil?
243
+ help_text = __get_options(:all_options).map do |option, option_args|
244
+
245
+ # Show the argument with the default value (if applicable)
246
+ case option_args[:argument]
247
+ when :required
248
+ long_dep = "=<#{option_args[:default] || 'value'}>"
249
+ short_dep = " <#{option_args[:default] || 'value'}>"
250
+ when :optional
251
+ long_dep = "=[#{option_args[:default] || 'value'}]"
252
+ short_dep = " [#{option_args[:default] || 'value'}]"
253
+ when :none
254
+ long_dep = ''
255
+ short_dep = ''
256
+ end
96
257
 
97
- def _create_attrs_(opts)
258
+ # Set up the options list
259
+ message = "\t" + (option_args[:aliases] << option).map{ |x| "--#{x}#{long_dep}"}.join(', ')
260
+ message << ", -#{option_args[:short]}#{short_dep}" if option_args[:short]
261
+ message << %{ :: Default: "#{option_args[:default]}"} if option_args[:default]
98
262
 
99
- # Get the option keys for attr creation
100
- keys = opts.keys
263
+ # Highlight missing options
264
+ unless missing_options.empty?
265
+ missing_required_option = ! (missing_options & option_args[:aliases]).empty?
266
+ message = colorize(message, missing_required_option ? :red : :default)
267
+ end
101
268
 
102
- # "symlink" the additional names to the original method
103
- keys.each do |key|
104
- default = nil
105
- key = [key] unless key.is_a?(Array)
106
- key.each_with_index do |k, i|
107
- if i < 1
108
- default = optionify(k)
109
- attr_accessor(default)
110
- else
111
- define_method(optionify(k, :set), Proc.new { |x| send("#{default}=", x) })
269
+ # Prepare the option documentation
270
+ if option_args[:documentation]
271
+ doc = option_args[:documentation]
272
+ if doc.is_a?(Array) \
273
+ && (doc.last.is_a?(Symbol) \
274
+ || (doc.last.is_a?(Array) \
275
+ && doc.last.reduce(true) { |o, d| o && d.is_a?(Symbol) }
276
+ ))
277
+
278
+ colors = doc.pop
279
+ len = doc.reduce(0) { |o, s| s.length > o ? s.length : o }
280
+ doc = doc.map{ |s| colorize(s.ljust(len, ' '), colors) }.join("\n\t\t")
281
+ elsif doc.is_a?(Array)
282
+ doc = doc.join("\n\t\t")
283
+ end
284
+
285
+ message << %{\n\t\t#{doc}}
286
+ message << "\n"
112
287
  end
113
288
  end
289
+
290
+ # Print and format the message
291
+ %{\nHelp: #{$0}\n\n### Options ###\n\n#{help_text.join("\n")}\n### Additional Details ###\n\n#{@@help_message || "No additional documentation"}\n}
292
+ else
293
+ @@help_message = message.split(/\n/).map{ |x| "\t#{x.strip}" }.join("\n")
294
+ end
295
+ end
296
+
297
+ private
298
+
299
+ def _create_preprocess_(opts)
300
+
301
+ # Set the preprocessor for options (used primarily for formatting or changing types)
302
+ __generic_option_reducer(:preprocessors, {}, opts, only_with: :preprocess) do |pre_proc, (options, option_args)|
303
+ pre_proc.merge(options.shift => option_args[:preprocess])
304
+ end
305
+
306
+ # Set the required options and dependencies for the parser
307
+ __generic_option_reducer(:required_options, {}, opts, only_with: :required) do |req_opts, (options, option_args)|
308
+ primary_name = options.shift
309
+
310
+ # Set the aliases for the required option
311
+ hash = (option_args[:required].is_a?(Hash) ? option_args[:required] : {}).merge(aliases: options)
312
+
313
+ # Ensure that the option names are properly formatted in the alternatives and dependencies
314
+ hash[:dependencies] = optionify_all(hash[:dependencies])
315
+ hash[:alternatives] = optionify_all(hash[:alternatives])
316
+
317
+ # Merge together the options as required
318
+ if option_args[:required].is_a?(Hash)
319
+ req_opts.merge(primary_name => hash)
320
+ else
321
+ req_opts.merge(primary_name => hash.merge(force: !! option_args[:required]))
322
+ end
323
+ end
324
+
325
+ # Create a cache of all the options available
326
+ __generic_option_reducer(:all_options, {}, opts) do |opt_cache, (options, option_args)|
327
+ primary_name = options.shift
328
+
329
+ if option_args.is_a?(Hash)
330
+ opt_cache.merge(primary_name => option_args.merge(aliases: options))
331
+ else
332
+ opt_cache.merge(primary_name => {aliases: options, argument: option_args})
333
+ end
334
+ end
335
+
336
+ # Create a list of the "secure options" (hide value from client; as much as possible)
337
+ __generic_option_reducer(:private_options, [], opts, only_with: :private) do |priv_opts, (options, option_args)|
338
+ primary_name = options.shift
339
+
340
+ if option_args[:private]
341
+ priv_opts << primary_name
342
+ else
343
+ priv_opts
344
+ end
345
+ end
346
+
347
+ # Set the default options to be set when no values are passed
348
+ __generic_option_reducer(:default_options, {}, opts, only_with: :default) do |default_opts, (options, option_args)|
349
+ default_opts.merge(options.shift => option_args[:default])
350
+ end
351
+
352
+ # Create the attribute accessors
353
+ opts.keys.each do |options|
354
+ options = optionify_all(options)
355
+ primary_name = options.shift
356
+ attr_accessor(primary_name)
114
357
  end
115
358
  end
116
359
 
@@ -127,7 +370,7 @@ module CliTool
127
370
  # If we have extended details determine them
128
371
  if details.is_a?(Hash)
129
372
  short = details[:short]
130
- dependency = details[:dependency]
373
+ dependency = details[:argument]
131
374
  else
132
375
  dependency = details
133
376
  end
@@ -140,8 +383,9 @@ module CliTool
140
383
  # If the option has aliases then create
141
384
  # additional GetoptLong Array options
142
385
  if opt.is_a?(Array)
143
- opt.each do |key|
386
+ opt.each_with_index do |key, i|
144
387
  golaot = golao.dup
388
+ golaot.shift if i > 0 # Remove Short Code
145
389
  golaot.unshift("--#{key}")
146
390
  gola << golaot
147
391
  end
@@ -153,6 +397,36 @@ module CliTool
153
397
 
154
398
  gola
155
399
  end
400
+
401
+ private
402
+
403
+ def __generic_option_reducer(instance_var, default = [], opts = {}, args = {}, &block)
404
+ opts = opts.reduce({}) { |o, (k, v)| o.merge(optionify_all(k) => v) }
405
+
406
+ # Require certain keys be in the option cache. This is done to save time of the processing and make it easier to write
407
+ # the code to parse the options and set the requirements and dependencies of each option.
408
+ if args[:only_with]
409
+ opts = opts.select { |k, v| v.is_a?(Hash) && [args[:only_with]].flatten.reduce(true) { |o, key| o && v.has_key?(key) } }
410
+ end
411
+
412
+ # Run the reducer and set the class variables accordingly
413
+ class_variable_set("@@__#{instance_var}", default) unless class_variable_defined?("@@__#{instance_var}")
414
+ class_variable_set("@@__#{instance_var}", opts.reduce(class_variable_get("@@__#{instance_var}") || default, &block))
415
+ end
416
+
417
+ def __get_options(instance_var = nil)
418
+ instance_var ? class_variable_get("@@__#{instance_var}") : Proc.new {
419
+ self.class_variables.reduce({}) do |o, x|
420
+ o.merge(x[4..-1].to_sym => class_variable_get(x))
421
+ end
422
+ }.call
423
+ end
424
+
425
+ def __slice_hash(hash, *keys)
426
+ keys.reduce({}) do |out, key|
427
+ hash.has_key?(key) ? out.merge(key => hash[key]) : out
428
+ end
429
+ end
156
430
  end
157
431
  end
158
432
  end
@@ -1,130 +1,355 @@
1
+ require 'singleton'
2
+ require 'pry'
3
+
1
4
  module CliTool
2
5
  module Remote
3
6
  class WaitingForSSH < StandardError; end;
4
7
  class Failure < StandardError; end;
5
8
 
6
- class Script < String
7
- def <<(script)
8
- script = script.strip + ';' unless script.match(/;$/)
9
- script = Script.new(script)
10
- super("#{script}\n")
9
+ class Script
10
+ include CliTool::StdinOut
11
+ attr_accessor :commands
12
+ attr_accessor :environment
13
+ attr_accessor :indent
14
+
15
+ def initialize
16
+ @environment = {}
17
+ @commands = []
18
+ @apt = {}
19
+ @remote_installed = 0
20
+ @indent = 0
21
+ end
22
+ alias reset initialize
23
+
24
+ ################
25
+ #= User Tools =#
26
+ ################
27
+
28
+ # def adduser(user, system = false)
29
+ # script "adduser --disabled-password --quiet --gecos '' #{user}".squeeze(' '), :sudo
30
+ # end
31
+
32
+ ###############
33
+ #= Apt Tools =#
34
+ ###############
35
+
36
+ def install(*packages); apt(:install, *packages) end
37
+
38
+ def purge(*packages); apt(:purge, *packages) end
39
+
40
+ def remove(*packages); apt(:remove, *packages) end
41
+
42
+ def update; apt(:update) end
43
+
44
+ def upgrade; apt(:upgrade) end
45
+
46
+ def upgrade!; apt(:'dist-upgrade') end
47
+
48
+ def aptkey(*keys)
49
+ @environment['DEBIAN_FRONTEND'] = %{noninteractive}
50
+ keyserver = yield if block_given?
51
+ keyserver ||= 'keyserver.ubuntu.com'
52
+ exec("apt-key adv --keyserver #{keyserver} --recv-keys #{keys.join(' ')}", :sudo)
53
+ self
54
+ end
55
+
56
+ ##########
57
+ #= DPKG =#
58
+ ##########
59
+
60
+ def if_installed?(*packages); check_installed(true, *packages) end
61
+
62
+ def unless_installed?(*packages); check_installed(false, *packages) end
63
+
64
+ def dpkg_install(*packages)
65
+ packages.each do |package|
66
+ exec("dpkg -i #{package}", :sudo)
67
+ end
68
+ self
69
+ end
70
+
71
+ def remote_install(*packages)
72
+ @remote_installed = 0
73
+ packages.each do |package|
74
+ @remote_installed += 1
75
+ num = "#{@remote_installed}".rjust(3,'0')
76
+ tmp = "/tmp/package#{num}.deb"
77
+ curl(package, tmp, :sudo)
78
+ dpkg_install(tmp)
79
+ exec("rm -f #{tmp}", :sudo)
80
+ end
81
+ self
82
+ end
83
+
84
+ ###########
85
+ #= Tools =#
86
+ ###########
87
+
88
+ def wget(from, to, sudo = false, sudouser = :root)
89
+ install(:wget)
90
+ exec("wget -O #{to} #{from}", sudo, sudouser)
91
+ self
92
+ end
93
+
94
+ def curl(from, to, sudo = false, sudouser = :root)
95
+ install(:curl)
96
+ exec("curl -# -o #{to} #{from}", sudo, sudouser)
97
+ self
98
+ end
99
+
100
+ def service(name, action)
101
+ exec("service #{name} #{action}", :sudo)
102
+ end
103
+
104
+ def file_exist?(file, exist = true, &block)
105
+ if?((exist ? '' : '! ') + %{-f "#{file}"}, &block)
106
+ end
107
+
108
+ def directory_exist(directory, exist = true, &block)
109
+ if?((exist ? '' : '! ') + %{-d "#{file}"}, &block)
110
+ end
111
+
112
+ ##########
113
+ #= Exec =#
114
+ ##########
115
+
116
+ def exec(script, sudo = false, sudouser = :root)
117
+ if File.exist?(script)
118
+ script = File.read(script)
119
+ end
120
+
121
+ # Wrap the script in a sudoers block
122
+ if sudo || sudo == :sudo
123
+ sudo_script = %{sudo su -c "/bin/bash" #{sudouser || :root}}
124
+ sudo_script << %{ <<-EOF\n#{get_environment_exports}#{script.rstrip}\nEOF}
125
+ script = sudo_script
126
+ end
127
+
128
+ @commands << script.rstrip
129
+ self
130
+ end
131
+
132
+ def to_s(indent = 0)
133
+ @commands.reduce([get_environment_exports(@indent)]){ |out, x| out << ((' ' * @indent) + x) }.join("\n")
11
134
  end
12
135
 
13
- def prepend(script)
14
- script = script.strip + ';' unless script.match(/;$/)
15
- script = Script.new(script)
16
- super("#{script}\n")
136
+ private
137
+
138
+ def apt(command, *p, &b)
139
+ @environment['DEBIAN_FRONTEND'] = %{noninteractive}
140
+ return aptkey(*p, &b) if command == :key
141
+ #return if ((@apt[command] ||= []) - p).empty? && [:install, :purge, :remove].include?(command)
142
+ #((@apt[command] ||= []) << p).flatten!
143
+ exec(("apt-get -o Dpkg::Options::='--force-confnew' -q -y --force-yes #{command} " + p.map(&:to_s).join(' ')), :sudo)
144
+ self
145
+ end
146
+
147
+ def check_installed(installed, *p, &block)
148
+ installed = installed ? '1' : '0'
149
+
150
+ condition = packages.reduce([]) { |command, package|
151
+ command << %{[ "$(dpkg -s #{package} > /dev/null 2>1 && echo '1' || echo '0')" == '#{installed}' ]}
152
+ }.join(' && ')
153
+
154
+ if?(condition, &block)
155
+ self
156
+ end
157
+
158
+ def if?(condition, &block)
159
+ exec(%{if [ #{condition} ]; then\n"} << Script.new.__send__(:instance_exec, &block).to_s(@indent + 2) << "\nfi")
160
+ self
161
+ end
162
+
163
+ def get_environment_exports(indent = 0)
164
+ @environment.reduce([]) { |out, (key, val)|
165
+ out << %{export #{(' ' * indent)}#{key.upcase}=#{val}}
166
+ }.join("\n") << "\n"
17
167
  end
18
168
  end
19
169
 
20
170
  def self.included(base)
171
+ base.__send__(:include, ::Singleton)
21
172
  base.__send__(:include, ::CliTool::StdinOut)
22
173
  base.__send__(:include, ::CliTool::OptionParser)
23
174
  base.__send__(:include, ::CliTool::SoftwareDependencies)
24
175
  base.extend(ClassMethods)
25
176
  base.software(:ssh, :nc)
26
- base.options host: :required,
27
- identity: :required,
28
- debug: :none,
29
- user: {
30
- dependency: :required,
31
- default: %x{whoami}.strip
177
+ base.options({
178
+ [:username, :user] => {
179
+ default: %x{whoami}.strip,
180
+ argument: :required,
181
+ short: :u,
182
+ documentation: 'SSH username'
183
+ },
184
+ [:password, :pass] => {
185
+ argument: :required,
186
+ short: :p,
187
+ documentation: 'SSH password (not implemented)',
188
+ secure: true
189
+ },
190
+ identity: {
191
+ argument: :required,
192
+ short: :i,
193
+ documentation: 'SSH key to use'
194
+ },
195
+ host: {
196
+ argument: :required,
197
+ short: :h,
198
+ documentation: 'SSH host to connect to',
199
+ required: true
32
200
  },
33
201
  port: {
34
- dependency: :required,
35
- default: '22'
202
+ default: '22',
203
+ argument: :required,
204
+ documentation: 'SSH port to connect on'
205
+ },
206
+ [:tags, :tag] => {
207
+ argument: :required,
208
+ short: :t,
209
+ documentation: 'Run tags (limit scripts to run)',
210
+ preprocess: ->(tags) { tags.split(',').map{ |x| x.strip.to_sym } }
36
211
  }
212
+ })
37
213
  end
38
214
 
39
- def script(script = nil, sudo = false, sudouser = nil)
40
- @_script ||= Script.new
41
- return Script.new(@_script.strip) if script.nil?
42
- return @_script = Script.new if script == :reset
43
- @_script << (sudo == :sudo ? %{sudo su -l -c "#{script.strip.gsub('"','\"')}" #{sudouser}} : script).strip
44
- end
215
+ module ClassMethods
216
+ def script(options = {}, &block)
217
+ script = Proc.new do
218
+ run = true
45
219
 
46
- def script_exec
47
- wait4ssh
220
+ # Allow restricting the runs based on the tags provided
221
+ if self.tags
222
+ script_tags = (options[:tags] || []).concat([options[:tag]]).compact.flatten
223
+ run = false if (self.tags & script_tags).empty?
224
+ end
48
225
 
49
- command =[ "ssh -t -t" ]
50
- command << "-I #{@identity}" if @identity
51
- command << "-p #{@port}" if @port
52
- command << "#{@user}@#{@host}"
53
- command << "<<-SCRIPT\n#{script}\nexit;\nSCRIPT"
54
- command = command.join(' ')
55
- script :reset
226
+ # Run only when the tag is provided if tag_only option is provided
227
+ run = false if run && options[:tag_only] == true && self.tags.empty?
56
228
 
57
- puts("Running Remotely:\n#{command}\n", [:blue, :white_bg])
229
+ if run # Do we want to run this script?
230
+ build_script = Script.new
231
+ build_script.__send__(:instance_exec, self, &block)
232
+ if options[:reboot] == true
233
+ build_script.exec('shutdown -r now', :sudo)
234
+ puts "\nServer will reboot upon completion of this block!\n", [:blue, :italic]
235
+ elsif options[:shutdown] == true
236
+ build_script.exec('shutdown -h now', :sudo)
237
+ puts "\nServer will shutdown upon completion of this block!\n", [:blue, :italic]
238
+ end
58
239
 
59
- system(command)
240
+ build_script
241
+ else
242
+ false # Don't run anything
243
+ end
244
+ end
60
245
 
61
- unless $?.success?
62
- raise Failure, "Error running \"#{command}\" on #{@host} exited with code #{$?.to_i}."
246
+ queue(script)
63
247
  end
64
- end
65
248
 
66
- def aptget(action = :update, *software)
67
- software = software.map(&:to_s).join(' ')
68
- script "apt-get #{action} -q -y --force-yes #{software}", :sudo
69
- end
249
+ def shutdown!
250
+ queue(Script.new.exec('shutdown -h now', :sudo))
251
+ end
70
252
 
71
- def aptkeyadd(*keys)
72
- keys = keys.map(&:to_s).join(' ')
73
- script "apt-key adv --keyserver keyserver.ubuntu.com --recv-keys #{keys}", :sudo
74
- end
253
+ def restart!
254
+ queue(Script.new.exec('shutdown -r now', :sudo))
255
+ end
75
256
 
76
- def restart
77
- script :reset
78
- script "shutdown -r now &", :sudo
79
- script_exec
257
+ def run_suite!(*a, &b)
258
+ run(:run_suite!, *a, &b)
259
+ end
80
260
 
81
- # Let the server shutdown
82
- sleep 5
83
- end
261
+ def custom!(*a, &b)
262
+ Proc.new { |obj| obj.instance_exec(*a, &b) }
263
+ false
264
+ end
84
265
 
85
- def adduser(user, system = false)
86
- script "adduser --disabled-password --quiet --gecos '' #{user}".squeeze(' '), :sudo
266
+ private
267
+
268
+ def queue(*items)
269
+ return @@queue || [] if items.empty?
270
+ items.map!{ |x| x.is_a?(Script) ? x.to_s : x }.flatten!
271
+ ((@@queue ||= []) << items).flatten!
272
+ self
273
+ end
87
274
  end
88
275
 
89
- def wait4ssh
90
- _retry ||= false
91
- %x{nc -z #{@host} #{@port}}
92
- raise WaitingForSSH unless $?.success?
93
- puts("\nSSH is now available!", :green) if _retry
94
- rescue WaitingForSSH
95
- print "Waiting for ssh..." unless _retry
96
- _retry = true
97
- print '.'
98
- sleep 2
99
- retry
276
+ def export(dir = nil)
277
+ self.class.queue.select{ |x| x.is_a?(String) }
100
278
  end
101
279
 
102
- module ClassMethods
280
+ def remote_exec!(script)
281
+ ssh_cmd =[ 'ssh -t -t' ]
282
+ ssh_cmd << "-I #{@identity}" if @identity
283
+ ssh_cmd << "-p #{@port}" if @port
284
+ ssh_cmd << "#{@username}@#{@host}"
285
+ ssh_cmd << "/bin/bash -s"
103
286
 
104
- # Create the options array
105
- def software(*soft)
106
- @@software ||= []
287
+ # Show debug script
288
+ if self.debug
289
+ pretty_cmd = ssh_cmd.concat(["<<-SCRIPT\n#{script}\nexit;\nSCRIPT"]).join(' ')
290
+ message = "About to run remote process over ssh on #{@username}@#{@host}:#{@port}"
291
+ puts message, :blue
292
+ puts '#' * message.length, :blue
293
+ puts pretty_cmd, :green
294
+ puts '#' * message.length, :blue
295
+ confirm "Should we continue?", :orange
296
+ else
297
+ sleep 2
298
+ end
107
299
 
108
- # If no software dependencies were passed then return
109
- return @@software.uniq unless soft
300
+ #ssh_cmd << %{"#{script.gsub(/"/, '\\\1')}"}
301
+ ssh_cmd << "<<-SCRIPT\n#{script}\nexit;\nSCRIPT"
302
+ ssh_cmd << %{| grep -v 'stdin: is not a tty'}
110
303
 
111
- # Find missing software
112
- missing = []
113
- soft.each do |app|
114
- %x{which #{app}}
115
- missing << app unless $?.success?
116
- end
304
+ puts "Running Script", [:blue, :italic]
117
305
 
118
- # Raise if there were any missing software's
119
- unless missing.empty?
120
- missing = missing.join(', ')
121
- raise CliTool::MissingDependencies,
122
- %{The required software packages "#{missing}" could not be found in your $PATH.}
123
- end
306
+ # Run command if a connection is available
307
+ return false unless ssh_connection?
308
+ puts "" # Empty Line
309
+ system(ssh_cmd.join(' '))
310
+ exec_success = $?.success?
311
+ puts "" # Empty Line
124
312
 
125
- # Append to the software list
126
- @@software = @@software.concat(soft).uniq
313
+ # Print message with status
314
+ if exec_success
315
+ puts "Script finished successfully.", [:green, :italic]
316
+ else
317
+ puts "There was an error running remote execution!", [:red, :italic]
127
318
  end
319
+
320
+ # Return status
321
+ exec_success
322
+ end
323
+
324
+ def run_suite!
325
+ self.class.__send__(:queue).each do |item|
326
+ item = instance_exec(self, &item) if item.is_a?(Proc)
327
+ remote_exec!(item.to_s) if item.is_a?(String) || item.is_a?(Script)
328
+ end
329
+ end
330
+
331
+ def tag?(*tgs)
332
+ self.tags && ! (self.tags & tgs).empty?
333
+ end
334
+
335
+ private
336
+
337
+ def ssh_connection?
338
+ port_available = false
339
+ tries = 0
340
+
341
+ while ! port_available && tries < 6
342
+ %x{nc -z #{@host} #{@port}}
343
+ port_available = $?.success?
344
+ break if port_available
345
+ print 'Waiting for ssh...', [:blue, :italic] if tries < 1
346
+ print '.'
347
+ tries += 1
348
+ sleep 4
349
+ end
350
+
351
+ puts ''
352
+ port_available
128
353
  end
129
354
  end
130
355
  end
@@ -1,3 +1,4 @@
1
+ require 'io/console'
1
2
  require 'timeout'
2
3
 
3
4
  module CliTool
@@ -5,37 +6,37 @@ module CliTool
5
6
  class MissingInput < StandardError; end;
6
7
 
7
8
  ANSI_COLORS = {
8
- :reset => 0,
9
- :bold => 1,
10
- :italic => 3,
11
- :underline => 4,
12
- :inverse => 7,
13
- :strike => 9,
14
- :bold_off => 22,
15
- :italic_off => 23,
16
- :underline_off => 24,
17
- :inverse_off => 27,
18
- :strike_off => 29,
19
- :black => 30,
20
- :red => 31,
21
- :green => 32,
22
- :yellow => 33,
23
- :blue => 34,
24
- :magenta => 35,
25
- :purple => 35,
26
- :cyan => 36,
27
- :white => 37,
28
- :default => 39,
29
- :black_bg => 40,
30
- :red_bg => 41,
31
- :green_bg => 42,
32
- :yellow_bg => 43,
33
- :blue_bg => 44,
34
- :magenta_bg => 45,
35
- :purple_bg => 45,
36
- :cyan_bg => 46,
37
- :white_bg => 47,
38
- :default_bg => 49
9
+ reset: 0,
10
+ bold: 1,
11
+ italic: 3,
12
+ underline: 4,
13
+ inverse: 7,
14
+ strike: 9,
15
+ bold_off: 22,
16
+ italic_off: 23,
17
+ underline_off: 24,
18
+ inverse_off: 27,
19
+ strike_off: 29,
20
+ black: 30,
21
+ red: 31,
22
+ green: 32,
23
+ yellow: 33,
24
+ blue: 34,
25
+ magenta: 35,
26
+ purple: 35,
27
+ cyan: 36,
28
+ white: 37,
29
+ default: 39,
30
+ black_bg: 40,
31
+ red_bg: 41,
32
+ green_bg: 42,
33
+ yellow_bg: 43,
34
+ blue_bg: 44,
35
+ magenta_bg: 45,
36
+ purple_bg: 45,
37
+ cyan_bg: 46,
38
+ white_bg: 47,
39
+ default_bg: 49
39
40
  }
40
41
 
41
42
  def self.included(base)
@@ -49,11 +50,23 @@ module CliTool
49
50
  def puts(text, color = :reset, timer = nil)
50
51
 
51
52
  # Process information for ANSI color codes
52
- super(_colorize_(text, color))
53
+ super(colorize(text, color))
53
54
 
54
55
  # Sleep after displaying the message
55
56
  if timer
56
- puts(_colorize_("Sleeping for #{timer} seconds...", color))
57
+ puts(colorize("Sleeping for #{timer} seconds...", color))
58
+ sleep(timer)
59
+ end
60
+ end
61
+
62
+ def print(text, color = :reset, timer = nil)
63
+
64
+ # Process information for ANSI color codes
65
+ super(colorize(text, color))
66
+
67
+ # Sleep after displaying the message
68
+ if timer
69
+ puts(colorize("Sleeping for #{timer} seconds...", color))
57
70
  sleep(timer)
58
71
  end
59
72
  end
@@ -61,11 +74,18 @@ module CliTool
61
74
  def input(message = '', color = :reset, timer = nil, default = nil)
62
75
 
63
76
  # Prompt for input
64
- puts(message, color)
77
+ print("#{message} ", color)
65
78
 
66
79
  # Get the input from the CLI
67
- gets = Proc.new do
68
- STDIN.gets.strip
80
+ if block_given? && yield == :noecho
81
+ gets = Proc.new do
82
+ STDIN.noecho(&:gets).strip
83
+ print "\n"
84
+ end
85
+ else
86
+ gets = Proc.new do
87
+ STDIN.gets.strip
88
+ end
69
89
  end
70
90
 
71
91
  # Handle timing out
@@ -82,6 +102,10 @@ module CliTool
82
102
  result
83
103
  end
84
104
 
105
+ def password(*a)
106
+ input(*a) { :noecho }
107
+ end
108
+
85
109
  def confirm(message, color = :reset, default = :n, timer = nil)
86
110
 
87
111
  # Handle the default value
@@ -107,9 +131,7 @@ module CliTool
107
131
  result
108
132
  end
109
133
 
110
- private
111
-
112
- def _colorize_(text, *color)
134
+ def colorize(text, *color)
113
135
 
114
136
  # Determine what to colors we should use
115
137
  color = [:reset] if color.empty?
@@ -1,3 +1,3 @@
1
1
  module CliTool
2
- VERSION = "0.0.3"
2
+ VERSION = "0.1.0"
3
3
  end
metadata CHANGED
@@ -1,18 +1,20 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cli_tool
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.0
5
+ prerelease:
5
6
  platform: ruby
6
7
  authors:
7
8
  - Kelly Becker
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2013-10-26 00:00:00.000000000 Z
12
+ date: 2014-02-25 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: bundler
15
16
  requirement: !ruby/object:Gem::Requirement
17
+ none: false
16
18
  requirements:
17
19
  - - ~>
18
20
  - !ruby/object:Gem::Version
@@ -20,6 +22,7 @@ dependencies:
20
22
  type: :development
21
23
  prerelease: false
22
24
  version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
23
26
  requirements:
24
27
  - - ~>
25
28
  - !ruby/object:Gem::Version
@@ -27,29 +30,33 @@ dependencies:
27
30
  - !ruby/object:Gem::Dependency
28
31
  name: rake
29
32
  requirement: !ruby/object:Gem::Requirement
33
+ none: false
30
34
  requirements:
31
- - - '>='
35
+ - - ! '>='
32
36
  - !ruby/object:Gem::Version
33
37
  version: '0'
34
38
  type: :development
35
39
  prerelease: false
36
40
  version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
37
42
  requirements:
38
- - - '>='
43
+ - - ! '>='
39
44
  - !ruby/object:Gem::Version
40
45
  version: '0'
41
46
  - !ruby/object:Gem::Dependency
42
47
  name: pry
43
48
  requirement: !ruby/object:Gem::Requirement
49
+ none: false
44
50
  requirements:
45
- - - '>='
51
+ - - ! '>='
46
52
  - !ruby/object:Gem::Version
47
53
  version: '0'
48
54
  type: :development
49
55
  prerelease: false
50
56
  version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
51
58
  requirements:
52
- - - '>='
59
+ - - ! '>='
53
60
  - !ruby/object:Gem::Version
54
61
  version: '0'
55
62
  description: Have trouble with advanced command line options, software dependency
@@ -78,26 +85,27 @@ files:
78
85
  homepage: http://kellybecker.me
79
86
  licenses:
80
87
  - MIT
81
- metadata: {}
82
88
  post_install_message:
83
89
  rdoc_options: []
84
90
  require_paths:
85
91
  - lib
86
92
  required_ruby_version: !ruby/object:Gem::Requirement
93
+ none: false
87
94
  requirements:
88
- - - '>='
95
+ - - ! '>='
89
96
  - !ruby/object:Gem::Version
90
97
  version: '0'
91
98
  required_rubygems_version: !ruby/object:Gem::Requirement
99
+ none: false
92
100
  requirements:
93
- - - '>='
101
+ - - ! '>='
94
102
  - !ruby/object:Gem::Version
95
103
  version: '0'
96
104
  requirements: []
97
105
  rubyforge_project:
98
- rubygems_version: 2.0.3
106
+ rubygems_version: 1.8.23
99
107
  signing_key:
100
- specification_version: 4
108
+ specification_version: 3
101
109
  summary: Tools to help with writing command line applications
102
110
  test_files: []
103
111
  has_rdoc:
checksums.yaml DELETED
@@ -1,7 +0,0 @@
1
- ---
2
- SHA1:
3
- metadata.gz: 5d6bebdc51c5d3fa303521a576030ef09a2a2f1e
4
- data.tar.gz: 44314ab4226493adced0d5300540c55de8194801
5
- SHA512:
6
- metadata.gz: c4f62e534448942a0f7ef307b7c3be7b37807ca658692f89c7df97a11db30de081c6ec5eb82fdc3a6f0408df5e922e92edcc1e2351d636d64124e8bfffa5b57d
7
- data.tar.gz: 0f5cc7400c20e4c3732aba67f733873d89843eff4f17901a362617e021518d72dfc22a65dd8d1217b12abdda98eb4c838b35553f26bc48854408397de2f12ec0