thor 1.2.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+