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.
- data/lib/cli_tool/option_parser.rb +333 -59
- data/lib/cli_tool/remote.rb +310 -85
- data/lib/cli_tool/stdin_out.rb +61 -39
- data/lib/cli_tool/version.rb +1 -1
- metadata +19 -11
- checksums.yaml +0 -7
@@ -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
|
-
:
|
16
|
-
:
|
17
|
-
:
|
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
|
-
|
28
|
-
default_options(opts)
|
44
|
+
_create_preprocess_(opts)
|
29
45
|
@@options = @@options.concat(_create_gola_(opts)).uniq
|
30
46
|
end
|
31
47
|
|
32
|
-
|
33
|
-
|
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
|
-
|
36
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
83
|
+
def optionify_all(option, retval = false)
|
84
|
+
[option].compact.flatten.map { |x| optionify(x, retval) }
|
47
85
|
end
|
48
86
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
#
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
77
|
-
|
159
|
+
# Do the actual set for us
|
160
|
+
object.__send__(optionify(option, :set), value)
|
78
161
|
end
|
79
162
|
|
80
|
-
#
|
81
|
-
puts "
|
82
|
-
|
83
|
-
|
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
|
-
|
235
|
+
object.__send__(entrypoint, *args, &block)
|
90
236
|
else
|
91
|
-
|
237
|
+
object
|
92
238
|
end
|
93
239
|
end
|
94
240
|
|
95
|
-
|
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
|
-
|
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
|
-
|
100
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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[:
|
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.
|
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
|
data/lib/cli_tool/remote.rb
CHANGED
@@ -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
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
35
|
-
|
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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
47
|
-
|
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
|
-
|
50
|
-
|
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
|
-
|
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
|
-
|
240
|
+
build_script
|
241
|
+
else
|
242
|
+
false # Don't run anything
|
243
|
+
end
|
244
|
+
end
|
60
245
|
|
61
|
-
|
62
|
-
raise Failure, "Error running \"#{command}\" on #{@host} exited with code #{$?.to_i}."
|
246
|
+
queue(script)
|
63
247
|
end
|
64
|
-
end
|
65
248
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
end
|
249
|
+
def shutdown!
|
250
|
+
queue(Script.new.exec('shutdown -h now', :sudo))
|
251
|
+
end
|
70
252
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
end
|
253
|
+
def restart!
|
254
|
+
queue(Script.new.exec('shutdown -r now', :sudo))
|
255
|
+
end
|
75
256
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
script_exec
|
257
|
+
def run_suite!(*a, &b)
|
258
|
+
run(:run_suite!, *a, &b)
|
259
|
+
end
|
80
260
|
|
81
|
-
|
82
|
-
|
83
|
-
|
261
|
+
def custom!(*a, &b)
|
262
|
+
Proc.new { |obj| obj.instance_exec(*a, &b) }
|
263
|
+
false
|
264
|
+
end
|
84
265
|
|
85
|
-
|
86
|
-
|
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
|
90
|
-
|
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
|
-
|
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
|
-
#
|
105
|
-
|
106
|
-
|
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
|
-
|
109
|
-
|
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
|
-
|
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
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
126
|
-
|
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
|
data/lib/cli_tool/stdin_out.rb
CHANGED
@@ -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
|
-
:
|
9
|
-
:
|
10
|
-
:
|
11
|
-
:
|
12
|
-
:
|
13
|
-
:
|
14
|
-
:
|
15
|
-
:
|
16
|
-
:
|
17
|
-
:
|
18
|
-
:
|
19
|
-
:
|
20
|
-
:
|
21
|
-
:
|
22
|
-
:
|
23
|
-
:
|
24
|
-
:
|
25
|
-
:
|
26
|
-
:
|
27
|
-
:
|
28
|
-
:
|
29
|
-
:
|
30
|
-
:
|
31
|
-
:
|
32
|
-
:
|
33
|
-
:
|
34
|
-
:
|
35
|
-
:
|
36
|
-
:
|
37
|
-
:
|
38
|
-
:
|
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(
|
53
|
+
super(colorize(text, color))
|
53
54
|
|
54
55
|
# Sleep after displaying the message
|
55
56
|
if timer
|
56
|
-
puts(
|
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
|
-
|
77
|
+
print("#{message} ", color)
|
65
78
|
|
66
79
|
# Get the input from the CLI
|
67
|
-
|
68
|
-
|
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
|
-
|
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?
|
data/lib/cli_tool/version.rb
CHANGED
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
|
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:
|
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:
|
106
|
+
rubygems_version: 1.8.23
|
99
107
|
signing_key:
|
100
|
-
specification_version:
|
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
|