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.
- 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
|