cli-topic 0.9.1

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.
@@ -0,0 +1,569 @@
1
+ # encoding: utf-8
2
+
3
+
4
+ #"The MIT License (MIT)
5
+
6
+ # Copyright © Heroku 2008 - 2014
7
+
8
+ # Permission is hereby granted, free of charge, to any person obtaining
9
+ # a copy of this software and associated documentation files (the
10
+ # "Software"), to deal in the Software without restriction, including
11
+ # without limitation the rights to use, copy, modify, merge, publish,
12
+ # distribute, sublicense, and/or sell copies of the Software, and to
13
+ # permit persons to whom the Software is furnished to do so, subject to
14
+ # the following conditions:
15
+
16
+ # The above copyright notice and this permission notice shall be
17
+ # included in all copies or substantial portions of the Software.
18
+
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
22
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
23
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
24
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
25
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
26
+ require 'fileutils'
27
+ module Clitopic
28
+ module Helpers
29
+
30
+ extend self
31
+
32
+ def home_directory
33
+ Dir.home
34
+ end
35
+
36
+ def display(msg="", new_line=true)
37
+ if new_line
38
+ puts(msg)
39
+ else
40
+ print(msg)
41
+ end
42
+ $stdout.flush
43
+ end
44
+
45
+ def redisplay(line, line_break = false)
46
+ display("\r\e[0K#{line}", line_break)
47
+ end
48
+
49
+ def deprecate(message)
50
+ display "WARNING: #{message}"
51
+ end
52
+
53
+ def debug(*args)
54
+ $stderr.puts(*args) if debugging?
55
+ end
56
+
57
+ def stderr_puts(*args)
58
+ $stderr.puts(*args)
59
+ end
60
+
61
+ def stderr_print(*args)
62
+ $stderr.print(*args)
63
+ end
64
+
65
+ def debugging?
66
+ ENV['CLITOPIC_DEBUG']
67
+ end
68
+
69
+ def confirm(message="Are you sure you wish to continue? (y/n)")
70
+ display("#{message} ", false)
71
+ ['y', 'yes'].include?(ask.downcase)
72
+ end
73
+
74
+ def confirm_command(app_to_confirm = app, message=nil)
75
+ if confirmed_app = Clitopic::Commands.current_options[:confirm]
76
+ unless confirmed_app == app_to_confirm
77
+ raise(Clitopic::Command::CommandFailed, "Confirmed app #{confirmed_app} did not match the selected app #{app_to_confirm}.")
78
+ end
79
+ return true
80
+ else
81
+ display
82
+ message ||= "WARNING: Destructive Action\nThis command will affect the app: #{app_to_confirm}"
83
+ message << "\nTo proceed, type \"#{app_to_confirm}\" or re-run this command with --confirm #{app_to_confirm}"
84
+ output_with_bang(message)
85
+ display
86
+ display "> ", false
87
+ if ask.downcase != app_to_confirm
88
+ error("Confirmation did not match #{app_to_confirm}. Aborted.")
89
+ else
90
+ true
91
+ end
92
+ end
93
+ end
94
+
95
+ def format_date(date)
96
+ date = Time.parse(date).utc if date.is_a?(String)
97
+ date.strftime("%Y-%m-%d %H:%M %Z").gsub('GMT', 'UTC')
98
+ end
99
+
100
+ def ask
101
+ $stdin.gets.to_s.strip
102
+ end
103
+
104
+ def shell(cmd)
105
+ FileUtils.cd(Dir.pwd) {|d| return `#{cmd}`}
106
+ end
107
+
108
+ def run_command(command, args=[])
109
+ Clitopic::Command.run(command, args)
110
+ end
111
+
112
+ def retry_on_exception(*exceptions)
113
+ retry_count = 0
114
+ begin
115
+ yield
116
+ rescue *exceptions => ex
117
+ raise ex if retry_count >= 3
118
+ sleep 3
119
+ retry_count += 1
120
+ retry
121
+ end
122
+ end
123
+
124
+ def has_git?
125
+ %x{ git --version }
126
+ $?.success?
127
+ end
128
+
129
+ def git(args)
130
+ return "" unless has_git?
131
+ flattened_args = [args].flatten.compact.join(" ")
132
+ %x{ git #{flattened_args} 2>&1 }.strip
133
+ end
134
+
135
+ def time_ago(since)
136
+ if since.is_a?(String)
137
+ since = Time.parse(since)
138
+ end
139
+
140
+ elapsed = Time.now - since
141
+
142
+ message = since.strftime("%Y/%m/%d %H:%M:%S")
143
+ if elapsed <= 60
144
+ message << " (~ #{elapsed.floor}s ago)"
145
+ elsif elapsed <= (60 * 60)
146
+ message << " (~ #{(elapsed / 60).floor}m ago)"
147
+ elsif elapsed <= (60 * 60 * 25)
148
+ message << " (~ #{(elapsed / 60 / 60).floor}h ago)"
149
+ end
150
+ message
151
+ end
152
+
153
+ def time_remaining(from, to)
154
+ secs = (to - from).to_i
155
+ mins = secs / 60
156
+ hours = mins / 60
157
+ return "#{hours}h #{mins % 60}m" if hours > 0
158
+ return "#{mins}m #{secs % 60}s" if mins > 0
159
+ return "#{secs}s" if secs >= 0
160
+ end
161
+
162
+ def truncate(text, length)
163
+ return "" if text.nil?
164
+ if text.size > length
165
+ text[0, length - 2] + '..'
166
+ else
167
+ text
168
+ end
169
+ end
170
+
171
+ @@kb = 1024
172
+ @@mb = 1024 * @@kb
173
+ @@gb = 1024 * @@mb
174
+ def format_bytes(amount)
175
+ amount = amount.to_i
176
+ return '(empty)' if amount == 0
177
+ return amount if amount < @@kb
178
+ return "#{(amount / @@kb).round}k" if amount < @@mb
179
+ return "#{(amount / @@mb).round}M" if amount < @@gb
180
+ return "#{(amount / @@gb).round}G"
181
+ end
182
+
183
+ def quantify(string, num)
184
+ "%d %s" % [ num, num.to_i == 1 ? string : "#{string}s" ]
185
+ end
186
+
187
+ def has_git_remote?(remote)
188
+ git('remote').split("\n").include?(remote) && $?.success?
189
+ end
190
+
191
+ def create_git_remote(remote, url)
192
+ return if has_git_remote? remote
193
+ git "remote add #{remote} #{url}"
194
+ display "Git remote #{remote} added" if $?.success?
195
+ end
196
+
197
+ def longest(items)
198
+ items.map { |i| i.to_s.length }.sort.last
199
+ end
200
+
201
+ def display_table(objects, columns, headers)
202
+ lengths = []
203
+ columns.each_with_index do |column, index|
204
+ header = headers[index]
205
+ lengths << longest([header].concat(objects.map { |o| o[column].to_s }))
206
+ end
207
+ lines = lengths.map {|length| "-" * length}
208
+ lengths[-1] = 0 # remove padding from last column
209
+ display_row headers, lengths
210
+ display_row lines, lengths
211
+ objects.each do |row|
212
+ display_row columns.map { |column| row[column] }, lengths
213
+ end
214
+ end
215
+
216
+ def display_row(row, lengths)
217
+ row_data = []
218
+ row.zip(lengths).each do |column, length|
219
+ format = column.is_a?(Fixnum) ? "%#{length}s" : "%-#{length}s"
220
+ row_data << format % column
221
+ end
222
+ display(row_data.join(" "))
223
+ end
224
+
225
+ def json_encode(object)
226
+ JSON.generate(object)
227
+ end
228
+
229
+ def json_decode(json)
230
+ JSON.parse(json)
231
+ rescue JSON::ParserError
232
+ nil
233
+ end
234
+
235
+ def set_buffer(enable)
236
+ with_tty do
237
+ if enable
238
+ `stty icanon echo`
239
+ else
240
+ `stty -icanon -echo`
241
+ end
242
+ end
243
+ end
244
+
245
+ def with_tty(&block)
246
+ return unless $stdin.isatty
247
+ begin
248
+ yield
249
+ rescue
250
+ # fails on windows
251
+ end
252
+ end
253
+
254
+ def get_terminal_environment
255
+ { "TERM" => ENV["TERM"], "COLUMNS" => `tput cols`.strip, "LINES" => `tput lines`.strip }
256
+ rescue
257
+ { "TERM" => ENV["TERM"] }
258
+ end
259
+
260
+ def fail(message)
261
+ raise Clitopic::Command::CommandFailed, message
262
+ end
263
+
264
+ ## DISPLAY HELPERS
265
+
266
+ def action(message, options={})
267
+ display("#{in_message(message, options)}... ", false)
268
+ Clitopic::Helpers.error_with_failure = true
269
+ ret = yield
270
+ Clitopic::Helpers.error_with_failure = false
271
+ display((options[:success] || "done"), false)
272
+ if @status
273
+ display(", #{@status}", false)
274
+ @status = nil
275
+ end
276
+ display
277
+ ret
278
+ end
279
+
280
+ def in_message(message, options={})
281
+ message = "#{message} in space #{options[:space]}" if options[:space]
282
+ message = "#{message} in organization #{org}" if options[:org]
283
+ message
284
+ end
285
+
286
+ def status(message)
287
+ @status = message
288
+ end
289
+
290
+ def format_with_bang(message)
291
+ return '' if message.to_s.strip == ""
292
+ " ! " + message.split("\n").join("\n ! ")
293
+ end
294
+
295
+ def output_with_bang(message="", new_line=true)
296
+ return if message.to_s.strip == ""
297
+ display(format_with_bang(message), new_line)
298
+ end
299
+
300
+ def error(message, report=false)
301
+ if Clitopic::Helpers.error_with_failure
302
+ display("failed")
303
+ Clitopic::Helpers.error_with_failure = false
304
+ end
305
+ $stderr.puts(format_with_bang(message))
306
+ exit(1)
307
+ end
308
+
309
+ def self.error_with_failure
310
+ @@error_with_failure ||= false
311
+ end
312
+
313
+ def self.error_with_failure=(new_error_with_failure)
314
+ @@error_with_failure = new_error_with_failure
315
+ end
316
+
317
+ def self.included_into
318
+ @@included_into ||= []
319
+ end
320
+
321
+ def self.extended_into
322
+ @@extended_into ||= []
323
+ end
324
+
325
+ def self.included(base)
326
+ included_into << base
327
+ end
328
+
329
+ def self.extended(base)
330
+ extended_into << base
331
+ end
332
+
333
+ def display_header(message="", new_line=true)
334
+ return if message.to_s.strip == ""
335
+ display("=== " + message.to_s.split("\n").join("\n=== "), new_line)
336
+ end
337
+
338
+ def display_object(object)
339
+ case object
340
+ when Array
341
+ # list of objects
342
+ object.each do |item|
343
+ display_object(item)
344
+ end
345
+ when Hash
346
+ # if all values are arrays, it is a list with headers
347
+ # otherwise it is a single header with pairs of data
348
+ if object.values.all? {|value| value.is_a?(Array)}
349
+ object.keys.sort_by {|key| key.to_s}.each do |key|
350
+ display_header(key)
351
+ display_object(object[key])
352
+ hputs
353
+ end
354
+ end
355
+ else
356
+ hputs(object.to_s)
357
+ end
358
+ end
359
+
360
+ def hputs(string='')
361
+ Kernel.puts(string)
362
+ end
363
+
364
+ def hprint(string='')
365
+ Kernel.print(string)
366
+ $stdout.flush
367
+ end
368
+
369
+ def spinner(ticks)
370
+ %w(/ - \\ |)[ticks % 4]
371
+ end
372
+
373
+ def launchy(message, url)
374
+ action(message) do
375
+ require("launchy")
376
+ launchy = Launchy.open(url)
377
+ if launchy.respond_to?(:join)
378
+ launchy.join
379
+ end
380
+ end
381
+ end
382
+
383
+ # produces a printf formatter line for an array of items
384
+ # if an individual line item is an array, it will create columns
385
+ # that are lined-up
386
+ #
387
+ # line_formatter(["foo", "barbaz"]) # => "%-6s"
388
+ # line_formatter(["foo", "barbaz"], ["bar", "qux"]) # => "%-3s %-6s"
389
+ #
390
+ def line_formatter(array)
391
+ if array.any? {|item| item.is_a?(Array)}
392
+ cols = []
393
+ array.each do |item|
394
+ if item.is_a?(Array)
395
+ item.each_with_index { |val,idx| cols[idx] = [cols[idx]||0, (val || '').length].max }
396
+ end
397
+ end
398
+ cols.map { |col| "%-#{col}s" }.join(" ")
399
+ else
400
+ "%s"
401
+ end
402
+ end
403
+
404
+ def styled_array(array, options={})
405
+ fmt = line_formatter(array)
406
+ array = array.sort unless options[:sort] == false
407
+ array.each do |element|
408
+ display((fmt % element).rstrip)
409
+ end
410
+ display
411
+ end
412
+
413
+ def format_error(error, message='Clitopic client internal error.')
414
+ formatted_error = []
415
+ formatted_error << " ! #{message}"
416
+ formatted_error << ' ! Search for help at: https://help.clitopic.com"'
417
+ formatted_error << ' ! Or report a bug at: https://github.com/ant31/clitopic/issues/new'
418
+ formatted_error << ''
419
+
420
+ command = ARGV.map do |arg|
421
+ if arg.include?(' ')
422
+ arg = %{"#{arg}"}
423
+ else
424
+ arg
425
+ end
426
+ end.join(' ')
427
+ formatted_error << " Command: #{command}"
428
+
429
+ if http_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY']
430
+ formatted_error << " HTTP Proxy: #{http_proxy}"
431
+ end
432
+ if https_proxy = ENV['https_proxy'] || ENV['HTTPS_PROXY']
433
+ formatted_error << " HTTPS Proxy: #{https_proxy}"
434
+ end
435
+ formatted_error << " Version: #{Clitopic.version}"
436
+ formatted_error << " Error: #{error.message} (#{error.class})"
437
+ formatted_error << " Backtrace:\n" + "#{error.backtrace.join("\n")}".indent(17) unless error.backtrace.nil?
438
+ formatted_error << "\n"
439
+ formatted_error << " More information in #{error_log_path}"
440
+ formatted_error << "\n"
441
+ formatted_error.join("\n")
442
+ end
443
+
444
+ def styled_error(error, message='Clitopic client internal error.')
445
+ if Clitopic::Helpers.error_with_failure
446
+ display("failed")
447
+ Clitopic::Helpers.error_with_failure = false
448
+ end
449
+ error_log(message, error.message, error.backtrace.join("\n"))
450
+ $stderr.puts(format_error(error, message))
451
+ rescue => e
452
+ $stderr.puts e, e.backtrace, error, error.backtrace
453
+ end
454
+
455
+ def error_log(*obj)
456
+ FileUtils.mkdir_p(File.dirname(error_log_path))
457
+ File.open(error_log_path, 'a') do |file|
458
+ file.write(obj.join("\n") + "\n")
459
+ end
460
+ end
461
+
462
+ def error_log_path
463
+ File.join(home_directory, '.clitopic', 'error.log')
464
+ end
465
+
466
+ def styled_header(header)
467
+ display("=== #{header}")
468
+ end
469
+
470
+ def styled_hash(hash, keys=nil)
471
+ max_key_length = hash.keys.map {|key| key.to_s.length}.max + 2
472
+ keys ||= hash.keys.sort {|x,y| x.to_s <=> y.to_s}
473
+ keys.each do |key|
474
+ case value = hash[key]
475
+ when Array
476
+ if value.empty?
477
+ next
478
+ else
479
+ elements = value.sort {|x,y| x.to_s <=> y.to_s}
480
+ display("#{key}: ".ljust(max_key_length), false)
481
+ display(elements[0])
482
+ elements[1..-1].each do |element|
483
+ display("#{' ' * max_key_length}#{element}")
484
+ end
485
+ if elements.length > 1
486
+ display
487
+ end
488
+ end
489
+ when nil
490
+ next
491
+ else
492
+ display("#{key}: ".ljust(max_key_length), false)
493
+ display(value)
494
+ end
495
+ end
496
+ end
497
+
498
+ def string_distance(first, last)
499
+ distances = [] # 0x0s
500
+ 0.upto(first.length) do |index|
501
+ distances << [index] + [0] * last.length
502
+ end
503
+ distances[0] = 0.upto(last.length).to_a
504
+ 1.upto(last.length) do |last_index|
505
+ 1.upto(first.length) do |first_index|
506
+ first_char = first[first_index - 1, 1]
507
+ last_char = last[last_index - 1, 1]
508
+ if first_char == last_char
509
+ distances[first_index][last_index] = distances[first_index - 1][last_index - 1] # noop
510
+ else
511
+ distances[first_index][last_index] = [
512
+ distances[first_index - 1][last_index], # deletion
513
+ distances[first_index][last_index - 1], # insertion
514
+ distances[first_index - 1][last_index - 1] # substitution
515
+ ].min + 1 # cost
516
+ if first_index > 1 && last_index > 1
517
+ first_previous_char = first[first_index - 2, 1]
518
+ last_previous_char = last[last_index - 2, 1]
519
+ if first_char == last_previous_char && first_previous_char == last_char
520
+ distances[first_index][last_index] = [
521
+ distances[first_index][last_index],
522
+ distances[first_index - 2][last_index - 2] + 1 # transposition
523
+ ].min
524
+ end
525
+ end
526
+ end
527
+ end
528
+ end
529
+ distances[first.length][last.length]
530
+ end
531
+
532
+ def display_suggestion(actual, possibilities, allowed_distance=4)
533
+ suggestions = suggestion(actual, possibilities, allowed_distance)
534
+ if suggestions.length == 1
535
+ "Perhaps you meant:\n" + suggestions.first.indent(20)
536
+ elsif suggestions.length > 1
537
+ "Perhaps you meant:\n" + "#{suggestions.map {|suggestion| "- #{suggestion}"}.join("\n").indent(2)}."
538
+ else
539
+ nil
540
+ end
541
+ end
542
+
543
+ def suggestion(actual, possibilities, allowed_distance=4)
544
+ distances = Hash.new {|hash,key| hash[key] = []}
545
+ begin_with = []
546
+ possibilities.each do |suggestion|
547
+ if suggestion.start_with?(actual)
548
+ distances[0] << suggestion
549
+ else
550
+ distances[string_distance(actual, suggestion)] << suggestion
551
+ end
552
+ end
553
+
554
+ minimum_distance = distances.keys.min
555
+
556
+ if minimum_distance < allowed_distance
557
+ suggestions = distances[minimum_distance].sort
558
+ return suggestions
559
+ else
560
+ []
561
+ end
562
+ end
563
+
564
+ # cheap deep clone
565
+ def deep_clone(obj)
566
+ json_decode(json_encode(obj))
567
+ end
568
+ end
569
+ end
@@ -0,0 +1,15 @@
1
+ module Clitopic
2
+ module Parser
3
+ module Dummy
4
+
5
+ def help
6
+ puts parse
7
+ end
8
+
9
+ def parse(args)
10
+ puts "i'm parsing #{args}"
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,70 @@
1
+ require 'optparse'
2
+ require 'clitopic/commands'
3
+
4
+ module Clitopic
5
+ module Parser
6
+ module OptParser
7
+ def process_options(parser, opts)
8
+ opts.each do |option|
9
+ parser.on(*option[:args]) do |value|
10
+ if option[:proc]
11
+ option[:proc].call(value)
12
+ end
13
+ name = option[:name]
14
+ if options.has_key?(name) && options[name].is_a?(Array)
15
+ options[name] += value
16
+ else
17
+ puts "Warning: already defined option: --#{option[:name]} #{options[name]}" if options.has_key?(name) && option[:default] == nil
18
+ options[name] = value
19
+ end
20
+ end
21
+ end
22
+ options
23
+ end
24
+
25
+ def parser
26
+ @opt_parser = OptionParser.new do |parser|
27
+ # remove OptionParsers Officious['version'] to avoid conflicts
28
+ # see: https://github.com/ruby/ruby/blob/trunk/lib/optparse.rb#L814
29
+ parser.banner = self.banner unless self.banner.nil?
30
+ parser.base.long.delete('version')
31
+ process_options(parser, self.cmd_options)
32
+
33
+ if !self.topic.nil? && self.topic.topic_options.size > 0
34
+ parser.separator ""
35
+ parser.separator "Topic options:"
36
+ process_options(parser, self.topic.topic_options)
37
+ end
38
+
39
+ parser.separator ""
40
+ parser.separator "Common options:"
41
+ process_options(parser, Clitopic::Commands.global_options)
42
+
43
+ # No argument, shows at tail. This will print an options summary.
44
+ # Try it and see!
45
+ parser.on_tail("-h", "--help", "Show this message") do
46
+ puts parser
47
+ exit 0
48
+ end
49
+ end
50
+ return @opt_parser
51
+ end
52
+
53
+ def help
54
+ parser.to_s
55
+ end
56
+
57
+ def parse(args)
58
+ @invalid_options ||= []
59
+ parser.order!(args)
60
+ @arguments = args
61
+ Clitopic::Commands.validate_arguments!(@invalid_options)
62
+ return @options, @arguments
63
+ rescue OptionParser::InvalidOption => ex
64
+ @invalid_options << ex.args.first
65
+ retry
66
+ end
67
+
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,3 @@
1
+ Dir[File.join(File.dirname(__FILE__), "parser", "*.rb")].each do |file|
2
+ require file
3
+ end