cli_tool 0.0.3 → 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.
@@ -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