thor 1.2.2 → 1.3.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.
@@ -30,11 +30,7 @@ class Thor
30
30
 
31
31
  arguments.each do |argument|
32
32
  if !argument.default.nil?
33
- begin
34
- @assigns[argument.human_name] = argument.default.dup
35
- rescue TypeError # Compatibility shim for un-dup-able Fixnum in Ruby < 2.4
36
- @assigns[argument.human_name] = argument.default
37
- end
33
+ @assigns[argument.human_name] = argument.default.dup
38
34
  elsif argument.required?
39
35
  @non_assigned_required << argument
40
36
  end
@@ -121,8 +117,18 @@ class Thor
121
117
  #
122
118
  def parse_array(name)
123
119
  return shift if peek.is_a?(Array)
120
+
124
121
  array = []
125
- array << shift while current_is_value?
122
+
123
+ while current_is_value?
124
+ value = shift
125
+
126
+ if !value.empty?
127
+ validate_enum_value!(name, value, "Expected all values of '%s' to be one of %s; got %s")
128
+ end
129
+
130
+ array << value
131
+ end
126
132
  array
127
133
  end
128
134
 
@@ -138,11 +144,9 @@ class Thor
138
144
  end
139
145
 
140
146
  value = $&.index(".") ? shift.to_f : shift.to_i
141
- if @switches.is_a?(Hash) && switch = @switches[name]
142
- if switch.enum && !switch.enum.include?(value)
143
- raise MalformattedArgumentError, "Expected '#{name}' to be one of #{switch.enum.join(', ')}; got #{value}"
144
- end
145
- end
147
+
148
+ validate_enum_value!(name, value, "Expected '%s' to be one of %s; got %s")
149
+
146
150
  value
147
151
  end
148
152
 
@@ -156,15 +160,27 @@ class Thor
156
160
  nil
157
161
  else
158
162
  value = shift
159
- if @switches.is_a?(Hash) && switch = @switches[name]
160
- if switch.enum && !switch.enum.include?(value)
161
- raise MalformattedArgumentError, "Expected '#{name}' to be one of #{switch.enum.join(', ')}; got #{value}"
162
- end
163
- end
163
+
164
+ validate_enum_value!(name, value, "Expected '%s' to be one of %s; got %s")
165
+
164
166
  value
165
167
  end
166
168
  end
167
169
 
170
+ # Raises an error if the switch is an enum and the values aren't included on it.
171
+ #
172
+ def validate_enum_value!(name, value, message)
173
+ return unless @switches.is_a?(Hash)
174
+
175
+ switch = @switches[name]
176
+
177
+ return unless switch
178
+
179
+ if switch.enum && !switch.enum.include?(value)
180
+ raise MalformattedArgumentError, message % [name, switch.enum_to_s, value]
181
+ end
182
+ end
183
+
168
184
  # Raises an error if @non_assigned_required array is not empty.
169
185
  #
170
186
  def check_requirement!
@@ -11,7 +11,7 @@ class Thor
11
11
  super
12
12
  @lazy_default = options[:lazy_default]
13
13
  @group = options[:group].to_s.capitalize if options[:group]
14
- @aliases = Array(options[:aliases])
14
+ @aliases = normalize_aliases(options[:aliases])
15
15
  @hide = options[:hide]
16
16
  end
17
17
 
@@ -69,7 +69,7 @@ class Thor
69
69
  value.class.name.downcase.to_sym
70
70
  end
71
71
 
72
- new(name.to_s, :required => required, :type => type, :default => default, :aliases => aliases)
72
+ new(name.to_s, required: required, type: type, default: default, aliases: aliases)
73
73
  end
74
74
 
75
75
  def switch_name
@@ -90,7 +90,7 @@ class Thor
90
90
  sample = "[#{sample}]".dup unless required?
91
91
 
92
92
  if boolean?
93
- sample << ", [#{dasherize('no-' + human_name)}]" unless (name == "force") || name.start_with?("no-")
93
+ sample << ", [#{dasherize('no-' + human_name)}]" unless (name == "force") || name.match(/\Ano[\-_]/)
94
94
  end
95
95
 
96
96
  aliases_for_usage.ljust(padding) + sample
@@ -104,6 +104,15 @@ class Thor
104
104
  end
105
105
  end
106
106
 
107
+ def show_default?
108
+ case default
109
+ when TrueClass, FalseClass
110
+ true
111
+ else
112
+ super
113
+ end
114
+ end
115
+
107
116
  VALID_TYPES.each do |type|
108
117
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
109
118
  def #{type}?
@@ -142,8 +151,8 @@ class Thor
142
151
  raise ArgumentError, err
143
152
  elsif @check_default_type == nil
144
153
  Thor.deprecation_warning "#{err}.\n" +
145
- 'This will be rejected in the future unless you explicitly pass the options `check_default_type: false`' +
146
- ' or call `allow_incompatible_default_type!` in your code'
154
+ "This will be rejected in the future unless you explicitly pass the options `check_default_type: false`" +
155
+ " or call `allow_incompatible_default_type!` in your code"
147
156
  end
148
157
  end
149
158
  end
@@ -159,5 +168,11 @@ class Thor
159
168
  def dasherize(str)
160
169
  (str.length > 1 ? "--" : "-") + str.tr("_", "-")
161
170
  end
171
+
172
+ private
173
+
174
+ def normalize_aliases(aliases)
175
+ Array(aliases).map { |short| short.to_s.sub(/^(?!\-)/, "-") }
176
+ end
162
177
  end
163
178
  end
@@ -29,8 +29,10 @@ class Thor
29
29
  #
30
30
  # If +stop_on_unknown+ is true, #parse will stop as soon as it encounters
31
31
  # an unknown option or a regular argument.
32
- def initialize(hash_options = {}, defaults = {}, stop_on_unknown = false, disable_required_check = false)
32
+ def initialize(hash_options = {}, defaults = {}, stop_on_unknown = false, disable_required_check = false, relations = {})
33
33
  @stop_on_unknown = stop_on_unknown
34
+ @exclusives = (relations[:exclusive_option_names] || []).select{|array| !array.empty?}
35
+ @at_least_ones = (relations[:at_least_one_option_names] || []).select{|array| !array.empty?}
34
36
  @disable_required_check = disable_required_check
35
37
  options = hash_options.values
36
38
  super(options)
@@ -50,8 +52,7 @@ class Thor
50
52
  options.each do |option|
51
53
  @switches[option.switch_name] = option
52
54
 
53
- option.aliases.each do |short|
54
- name = short.to_s.sub(/^(?!\-)/, "-")
55
+ option.aliases.each do |name|
55
56
  @shorts[name] ||= option.switch_name
56
57
  end
57
58
  end
@@ -101,7 +102,7 @@ class Thor
101
102
  unshift($1.split("").map { |f| "-#{f}" })
102
103
  next
103
104
  when EQ_RE
104
- unshift($2, :is_value => true)
105
+ unshift($2, is_value: true)
105
106
  switch = $1
106
107
  when SHORT_NUM
107
108
  unshift($2)
@@ -132,12 +133,38 @@ class Thor
132
133
  end
133
134
 
134
135
  check_requirement! unless @disable_required_check
136
+ check_exclusive!
137
+ check_at_least_one!
135
138
 
136
139
  assigns = Thor::CoreExt::HashWithIndifferentAccess.new(@assigns)
137
140
  assigns.freeze
138
141
  assigns
139
142
  end
140
143
 
144
+ def check_exclusive!
145
+ opts = @assigns.keys
146
+ # When option A and B are exclusive, if A and B are given at the same time,
147
+ # the diffrence of argument array size will decrease.
148
+ found = @exclusives.find{ |ex| (ex - opts).size < ex.size - 1 }
149
+ if found
150
+ names = names_to_switch_names(found & opts).map{|n| "'#{n}'"}
151
+ class_name = self.class.name.split("::").last.downcase
152
+ fail ExclusiveArgumentError, "Found exclusive #{class_name} #{names.join(", ")}"
153
+ end
154
+ end
155
+
156
+ def check_at_least_one!
157
+ opts = @assigns.keys
158
+ # When at least one is required of the options A and B,
159
+ # if the both options were not given, none? would be true.
160
+ found = @at_least_ones.find{ |one_reqs| one_reqs.none?{ |o| opts.include? o} }
161
+ if found
162
+ names = names_to_switch_names(found).map{|n| "'#{n}'"}
163
+ class_name = self.class.name.split("::").last.downcase
164
+ fail AtLeastOneRequiredArgumentError, "Not found at least one of required #{class_name} #{names.join(", ")}"
165
+ end
166
+ end
167
+
141
168
  def check_unknown!
142
169
  to_check = @stopped_parsing_after_extra_index ? @extra[0...@stopped_parsing_after_extra_index] : @extra
143
170
 
@@ -148,6 +175,17 @@ class Thor
148
175
 
149
176
  protected
150
177
 
178
+ # Option names changes to swith name or human name
179
+ def names_to_switch_names(names = [])
180
+ @switches.map do |_, o|
181
+ if names.include? o.name
182
+ o.respond_to?(:switch_name) ? o.switch_name : o.human_name
183
+ else
184
+ nil
185
+ end
186
+ end.compact
187
+ end
188
+
151
189
  def assign_result!(option, result)
152
190
  if option.repeatable && option.type == :hash
153
191
  (@assigns[option.human_name] ||= {}).merge!(result)
data/lib/thor/runner.rb CHANGED
@@ -23,7 +23,7 @@ class Thor::Runner < Thor #:nodoc:
23
23
  initialize_thorfiles(meth)
24
24
  klass, command = Thor::Util.find_class_and_command_by_namespace(meth)
25
25
  self.class.handle_no_command_error(command, false) if klass.nil?
26
- klass.start(["-h", command].compact, :shell => shell)
26
+ klass.start(["-h", command].compact, shell: shell)
27
27
  else
28
28
  super
29
29
  end
@@ -38,11 +38,11 @@ class Thor::Runner < Thor #:nodoc:
38
38
  klass, command = Thor::Util.find_class_and_command_by_namespace(meth)
39
39
  self.class.handle_no_command_error(command, false) if klass.nil?
40
40
  args.unshift(command) if command
41
- klass.start(args, :shell => shell)
41
+ klass.start(args, shell: shell)
42
42
  end
43
43
 
44
44
  desc "install NAME", "Install an optionally named Thor file into your system commands"
45
- method_options :as => :string, :relative => :boolean, :force => :boolean
45
+ method_options as: :string, relative: :boolean, force: :boolean
46
46
  def install(name) # rubocop:disable Metrics/MethodLength
47
47
  initialize_thorfiles
48
48
 
@@ -53,7 +53,7 @@ class Thor::Runner < Thor #:nodoc:
53
53
  package = :file
54
54
  require "open-uri"
55
55
  begin
56
- contents = URI.send(:open, name, &:read) # Using `send` for Ruby 2.4- support
56
+ contents = URI.open(name, &:read)
57
57
  rescue OpenURI::HTTPError
58
58
  raise Error, "Error opening URI '#{name}'"
59
59
  end
@@ -69,7 +69,7 @@ class Thor::Runner < Thor #:nodoc:
69
69
  base = name
70
70
  package = :file
71
71
  require "open-uri"
72
- contents = URI.send(:open, name, &:read) # for ruby 2.1-2.4
72
+ contents = URI.open(name, &:read)
73
73
  end
74
74
  rescue Errno::ENOENT
75
75
  raise Error, "Error opening file '#{name}'"
@@ -101,9 +101,9 @@ class Thor::Runner < Thor #:nodoc:
101
101
  end
102
102
 
103
103
  thor_yaml[as] = {
104
- :filename => Digest::SHA256.hexdigest(name + as),
105
- :location => location,
106
- :namespaces => Thor::Util.namespaces_in_content(contents, base)
104
+ filename: Digest::SHA256.hexdigest(name + as),
105
+ location: location,
106
+ namespaces: Thor::Util.namespaces_in_content(contents, base)
107
107
  }
108
108
 
109
109
  save_yaml(thor_yaml)
@@ -164,14 +164,14 @@ class Thor::Runner < Thor #:nodoc:
164
164
  end
165
165
 
166
166
  desc "installed", "List the installed Thor modules and commands"
167
- method_options :internal => :boolean
167
+ method_options internal: :boolean
168
168
  def installed
169
169
  initialize_thorfiles(nil, true)
170
170
  display_klasses(true, options["internal"])
171
171
  end
172
172
 
173
173
  desc "list [SEARCH]", "List the available thor commands (--substring means .*SEARCH)"
174
- method_options :substring => :boolean, :group => :string, :all => :boolean, :debug => :boolean
174
+ method_options substring: :boolean, group: :string, all: :boolean, debug: :boolean
175
175
  def list(search = "")
176
176
  initialize_thorfiles
177
177
 
@@ -313,7 +313,7 @@ private
313
313
  say shell.set_color(namespace, :blue, true)
314
314
  say "-" * namespace.size
315
315
 
316
- print_table(list, :truncate => true)
316
+ print_table(list, truncate: true)
317
317
  say
318
318
  end
319
319
  alias_method :display_tasks, :display_commands
@@ -1,8 +1,10 @@
1
+ require_relative "column_printer"
2
+ require_relative "table_printer"
3
+ require_relative "wrapped_printer"
4
+
1
5
  class Thor
2
6
  module Shell
3
7
  class Basic
4
- DEFAULT_TERMINAL_WIDTH = 80
5
-
6
8
  attr_accessor :base
7
9
  attr_reader :padding
8
10
 
@@ -145,14 +147,14 @@ class Thor
145
147
  # "yes".
146
148
  #
147
149
  def yes?(statement, color = nil)
148
- !!(ask(statement, color, :add_to_history => false) =~ is?(:yes))
150
+ !!(ask(statement, color, add_to_history: false) =~ is?(:yes))
149
151
  end
150
152
 
151
153
  # Make a question the to user and returns true if the user replies "n" or
152
154
  # "no".
153
155
  #
154
156
  def no?(statement, color = nil)
155
- !!(ask(statement, color, :add_to_history => false) =~ is?(:no))
157
+ !!(ask(statement, color, add_to_history: false) =~ is?(:no))
156
158
  end
157
159
 
158
160
  # Prints values in columns
@@ -161,16 +163,8 @@ class Thor
161
163
  # Array[String, String, ...]
162
164
  #
163
165
  def print_in_columns(array)
164
- return if array.empty?
165
- colwidth = (array.map { |el| el.to_s.size }.max || 0) + 2
166
- array.each_with_index do |value, index|
167
- # Don't output trailing spaces when printing the last column
168
- if ((((index + 1) % (terminal_width / colwidth))).zero? && !index.zero?) || index + 1 == array.length
169
- stdout.puts value
170
- else
171
- stdout.printf("%-#{colwidth}s", value)
172
- end
173
- end
166
+ printer = ColumnPrinter.new(stdout)
167
+ printer.print(array)
174
168
  end
175
169
 
176
170
  # Prints a table.
@@ -181,58 +175,11 @@ class Thor
181
175
  # ==== Options
182
176
  # indent<Integer>:: Indent the first column by indent value.
183
177
  # colwidth<Integer>:: Force the first column to colwidth spaces wide.
178
+ # borders<Boolean>:: Adds ascii borders.
184
179
  #
185
180
  def print_table(array, options = {}) # rubocop:disable Metrics/MethodLength
186
- return if array.empty?
187
-
188
- formats = []
189
- indent = options[:indent].to_i
190
- colwidth = options[:colwidth]
191
- options[:truncate] = terminal_width if options[:truncate] == true
192
-
193
- formats << "%-#{colwidth + 2}s".dup if colwidth
194
- start = colwidth ? 1 : 0
195
-
196
- colcount = array.max { |a, b| a.size <=> b.size }.size
197
-
198
- maximas = []
199
-
200
- start.upto(colcount - 1) do |index|
201
- maxima = array.map { |row| row[index] ? row[index].to_s.size : 0 }.max
202
- maximas << maxima
203
- formats << if index == colcount - 1
204
- # Don't output 2 trailing spaces when printing the last column
205
- "%-s".dup
206
- else
207
- "%-#{maxima + 2}s".dup
208
- end
209
- end
210
-
211
- formats[0] = formats[0].insert(0, " " * indent)
212
- formats << "%s"
213
-
214
- array.each do |row|
215
- sentence = "".dup
216
-
217
- row.each_with_index do |column, index|
218
- maxima = maximas[index]
219
-
220
- f = if column.is_a?(Numeric)
221
- if index == row.size - 1
222
- # Don't output 2 trailing spaces when printing the last column
223
- "%#{maxima}s"
224
- else
225
- "%#{maxima}s "
226
- end
227
- else
228
- formats[index]
229
- end
230
- sentence << f % column.to_s
231
- end
232
-
233
- sentence = truncate(sentence, options[:truncate]) if options[:truncate]
234
- stdout.puts sentence
235
- end
181
+ printer = TablePrinter.new(stdout, options)
182
+ printer.print(array)
236
183
  end
237
184
 
238
185
  # Prints a long string, word-wrapping the text to the current width of the
@@ -245,33 +192,8 @@ class Thor
245
192
  # indent<Integer>:: Indent each line of the printed paragraph by indent value.
246
193
  #
247
194
  def print_wrapped(message, options = {})
248
- indent = options[:indent] || 0
249
- width = terminal_width - indent
250
- paras = message.split("\n\n")
251
-
252
- paras.map! do |unwrapped|
253
- words = unwrapped.split(" ")
254
- counter = words.first.length
255
- words.inject do |memo, word|
256
- word = word.gsub(/\n\005/, "\n").gsub(/\005/, "\n")
257
- counter = 0 if word.include? "\n"
258
- if (counter + word.length + 1) < width
259
- memo = "#{memo} #{word}"
260
- counter += (word.length + 1)
261
- else
262
- memo = "#{memo}\n#{word}"
263
- counter = word.length
264
- end
265
- memo
266
- end
267
- end.compact!
268
-
269
- paras.each do |para|
270
- para.split("\n").each do |line|
271
- stdout.puts line.insert(0, " " * indent)
272
- end
273
- stdout.puts unless para == paras.last
274
- end
195
+ printer = WrappedPrinter.new(stdout, options)
196
+ printer.print(message)
275
197
  end
276
198
 
277
199
  # Deals with file collision and returns true if the file should be
@@ -289,7 +211,7 @@ class Thor
289
211
  loop do
290
212
  answer = ask(
291
213
  %[Overwrite #{destination}? (enter "h" for help) #{options}],
292
- :add_to_history => false
214
+ add_to_history: false
293
215
  )
294
216
 
295
217
  case answer
@@ -316,24 +238,11 @@ class Thor
316
238
 
317
239
  say "Please specify merge tool to `THOR_MERGE` env."
318
240
  else
319
- say file_collision_help
241
+ say file_collision_help(block_given?)
320
242
  end
321
243
  end
322
244
  end
323
245
 
324
- # This code was copied from Rake, available under MIT-LICENSE
325
- # Copyright (c) 2003, 2004 Jim Weirich
326
- def terminal_width
327
- result = if ENV["THOR_COLUMNS"]
328
- ENV["THOR_COLUMNS"].to_i
329
- else
330
- unix? ? dynamic_width : DEFAULT_TERMINAL_WIDTH
331
- end
332
- result < 10 ? DEFAULT_TERMINAL_WIDTH : result
333
- rescue
334
- DEFAULT_TERMINAL_WIDTH
335
- end
336
-
337
246
  # Called if something goes wrong during the execution. This is used by Thor
338
247
  # internally and should not be used inside your scripts. If something went
339
248
  # wrong, you can always raise an exception. If you raise a Thor::Error, it
@@ -384,16 +293,21 @@ class Thor
384
293
  end
385
294
  end
386
295
 
387
- def file_collision_help #:nodoc:
388
- <<-HELP
296
+ def file_collision_help(block_given) #:nodoc:
297
+ help = <<-HELP
389
298
  Y - yes, overwrite
390
299
  n - no, do not overwrite
391
300
  a - all, overwrite this and all others
392
301
  q - quit, abort
393
- d - diff, show the differences between the old and the new
394
302
  h - help, show this help
395
- m - merge, run merge tool
396
303
  HELP
304
+ if block_given
305
+ help << <<-HELP
306
+ d - diff, show the differences between the old and the new
307
+ m - merge, run merge tool
308
+ HELP
309
+ end
310
+ help
397
311
  end
398
312
 
399
313
  def show_diff(destination, content) #:nodoc:
@@ -411,46 +325,8 @@ class Thor
411
325
  mute? || (base && base.options[:quiet])
412
326
  end
413
327
 
414
- # Calculate the dynamic width of the terminal
415
- def dynamic_width
416
- @dynamic_width ||= (dynamic_width_stty.nonzero? || dynamic_width_tput)
417
- end
418
-
419
- def dynamic_width_stty
420
- `stty size 2>/dev/null`.split[1].to_i
421
- end
422
-
423
- def dynamic_width_tput
424
- `tput cols 2>/dev/null`.to_i
425
- end
426
-
427
328
  def unix?
428
- RUBY_PLATFORM =~ /(aix|darwin|linux|(net|free|open)bsd|cygwin|solaris)/i
429
- end
430
-
431
- def truncate(string, width)
432
- as_unicode do
433
- chars = string.chars.to_a
434
- if chars.length <= width
435
- chars.join
436
- else
437
- chars[0, width - 3].join + "..."
438
- end
439
- end
440
- end
441
-
442
- if "".respond_to?(:encode)
443
- def as_unicode
444
- yield
445
- end
446
- else
447
- def as_unicode
448
- old = $KCODE
449
- $KCODE = "U"
450
- yield
451
- ensure
452
- $KCODE = old
453
- end
329
+ Terminal.unix?
454
330
  end
455
331
 
456
332
  def ask_simply(statement, color, options)
@@ -1,4 +1,5 @@
1
1
  require_relative "basic"
2
+ require_relative "lcs_diff"
2
3
 
3
4
  class Thor
4
5
  module Shell
@@ -6,6 +7,8 @@ class Thor
6
7
  # Thor::Shell::Basic to see all available methods.
7
8
  #
8
9
  class Color < Basic
10
+ include LCSDiff
11
+
9
12
  # Embed in a String to clear all previous ANSI sequences.
10
13
  CLEAR = "\e[0m"
11
14
  # The start of an ANSI bold sequence.
@@ -105,52 +108,7 @@ class Thor
105
108
  end
106
109
 
107
110
  def are_colors_disabled?
108
- !ENV['NO_COLOR'].nil? && !ENV['NO_COLOR'].empty?
109
- end
110
-
111
- # Overwrite show_diff to show diff with colors if Diff::LCS is
112
- # available.
113
- #
114
- def show_diff(destination, content) #:nodoc:
115
- if diff_lcs_loaded? && ENV["THOR_DIFF"].nil? && ENV["RAILS_DIFF"].nil?
116
- actual = File.binread(destination).to_s.split("\n")
117
- content = content.to_s.split("\n")
118
-
119
- Diff::LCS.sdiff(actual, content).each do |diff|
120
- output_diff_line(diff)
121
- end
122
- else
123
- super
124
- end
125
- end
126
-
127
- def output_diff_line(diff) #:nodoc:
128
- case diff.action
129
- when "-"
130
- say "- #{diff.old_element.chomp}", :red, true
131
- when "+"
132
- say "+ #{diff.new_element.chomp}", :green, true
133
- when "!"
134
- say "- #{diff.old_element.chomp}", :red, true
135
- say "+ #{diff.new_element.chomp}", :green, true
136
- else
137
- say " #{diff.old_element.chomp}", nil, true
138
- end
139
- end
140
-
141
- # Check if Diff::LCS is loaded. If it is, use it to create pretty output
142
- # for diff.
143
- #
144
- def diff_lcs_loaded? #:nodoc:
145
- return true if defined?(Diff::LCS)
146
- return @diff_lcs_loaded unless @diff_lcs_loaded.nil?
147
-
148
- @diff_lcs_loaded = begin
149
- require "diff/lcs"
150
- true
151
- rescue LoadError
152
- false
153
- end
111
+ !ENV["NO_COLOR"].nil? && !ENV["NO_COLOR"].empty?
154
112
  end
155
113
  end
156
114
  end
@@ -0,0 +1,29 @@
1
+ require_relative "terminal"
2
+
3
+ class Thor
4
+ module Shell
5
+ class ColumnPrinter
6
+ attr_reader :stdout, :options
7
+
8
+ def initialize(stdout, options = {})
9
+ @stdout = stdout
10
+ @options = options
11
+ @indent = options[:indent].to_i
12
+ end
13
+
14
+ def print(array)
15
+ return if array.empty?
16
+ colwidth = (array.map { |el| el.to_s.size }.max || 0) + 2
17
+ array.each_with_index do |value, index|
18
+ # Don't output trailing spaces when printing the last column
19
+ if ((((index + 1) % (Terminal.terminal_width / colwidth))).zero? && !index.zero?) || index + 1 == array.length
20
+ stdout.puts value
21
+ else
22
+ stdout.printf("%-#{colwidth}s", value)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+