markdown_exec 2.8.3 → 2.8.5

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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -0
  3. data/Gemfile.lock +1 -1
  4. data/Rakefile +33 -23
  5. data/bats/{block-types.bats → block-type-bash.bats} +0 -25
  6. data/bats/block-type-link.bats +9 -0
  7. data/bats/block-type-port.bats +16 -0
  8. data/bats/block-type-ux-allowed.bats +29 -0
  9. data/bats/block-type-ux-auto.bats +1 -1
  10. data/bats/block-type-ux-chained.bats +9 -0
  11. data/bats/block-type-ux-default.bats +8 -0
  12. data/bats/block-type-ux-echo-hash.bats +14 -0
  13. data/bats/block-type-ux-echo.bats +2 -2
  14. data/bats/block-type-ux-exec.bats +1 -1
  15. data/bats/block-type-ux-hidden.bats +9 -0
  16. data/bats/block-type-ux-invalid.bats +8 -0
  17. data/bats/block-type-ux-transform.bats +1 -1
  18. data/bats/indented-block-type-vars.bats +9 -0
  19. data/bats/line-decor-dynamic.bats +1 -1
  20. data/bats/test_helper.bash +9 -2
  21. data/bats/variable-expansion-multiline.bats +8 -0
  22. data/bats/variable-expansion.bats +1 -1
  23. data/docs/dev/block-type-ux-allowed.md +82 -0
  24. data/docs/dev/block-type-ux-auto.md +2 -1
  25. data/docs/dev/block-type-ux-chained.md +21 -0
  26. data/docs/dev/block-type-ux-default.md +42 -0
  27. data/docs/dev/block-type-ux-echo-hash.md +78 -0
  28. data/docs/dev/block-type-ux-echo.md +3 -1
  29. data/docs/dev/block-type-ux-exec.md +1 -0
  30. data/docs/dev/block-type-ux-hidden.md +21 -0
  31. data/docs/dev/block-type-ux-invalid.md +5 -0
  32. data/docs/dev/block-type-ux-require.md +9 -18
  33. data/docs/dev/indented-block-type-vars.md +6 -0
  34. data/docs/dev/line-decor-dynamic.md +2 -1
  35. data/docs/dev/variable-expansion-multiline.md +31 -0
  36. data/lib/ansi_formatter.rb +0 -1
  37. data/lib/ansi_string.rb +1 -1
  38. data/lib/array.rb +0 -1
  39. data/lib/array_util.rb +0 -1
  40. data/lib/block_label.rb +1 -1
  41. data/lib/cached_nested_file_reader.rb +1 -1
  42. data/lib/constants.rb +18 -0
  43. data/lib/directory_searcher.rb +1 -1
  44. data/lib/exceptions.rb +0 -1
  45. data/lib/fcb.rb +52 -9
  46. data/lib/filter.rb +1 -2
  47. data/lib/format_table.rb +1 -0
  48. data/lib/fout.rb +1 -1
  49. data/lib/hash.rb +0 -1
  50. data/lib/hash_delegator.rb +404 -224
  51. data/lib/link_history.rb +17 -17
  52. data/lib/logged_struct.rb +429 -0
  53. data/lib/markdown_exec/version.rb +1 -1
  54. data/lib/markdown_exec.rb +3 -3
  55. data/lib/mdoc.rb +21 -31
  56. data/lib/menu.src.yml +15 -7
  57. data/lib/menu.yml +11 -6
  58. data/lib/namer.rb +3 -6
  59. data/lib/null_result.rb +131 -0
  60. data/lib/object_present.rb +1 -1
  61. data/lib/option_value.rb +1 -1
  62. data/lib/resize_terminal.rb +1 -2
  63. data/lib/saved_assets.rb +1 -1
  64. data/lib/saved_files_matcher.rb +1 -1
  65. data/lib/shell_session.rb +439 -0
  66. data/lib/streams_out.rb +0 -1
  67. data/lib/string_util.rb +11 -1
  68. data/lib/success_result.rb +112 -0
  69. data/lib/text_analyzer.rb +1 -0
  70. data/lib/ww.rb +9 -7
  71. metadata +25 -3
@@ -0,0 +1,439 @@
1
+ #!/usr/bin/env -S bundle exec ruby -r./lib/ww
2
+ # frozen_string_literal: true
3
+
4
+ # encoding=utf-8
5
+
6
+ require 'open3'
7
+
8
+ STATUS_SUCCESS = 0
9
+
10
+ # ShellSession provides an interactive bash session to execute commands,
11
+ # capturing both standard output and standard error separately with timestamps.
12
+ # Each output line is stored as a hash containing its text and the time it was received.
13
+ class ShellSession
14
+ attr_reader :exitstatus, :output, :lines, :stdout, :stderr, :stdout_lines,
15
+ :stderr_lines, :waiting_for_input
16
+
17
+ def initialize(setup_command = nil)
18
+ # Open a persistent bash session with separate stdout and stderr streams.
19
+ @stdin, @stdout_stream, @stderr_stream, @wait_thr = Open3.popen3('bash')
20
+ @stdin.flush
21
+
22
+ # Random boundary marker between commands.
23
+ @boundary = Random.new.rand.to_s
24
+
25
+ # Line sequence counter
26
+ @line_counter = 0
27
+
28
+ # Most recent command.
29
+ @command = ''
30
+
31
+ # Buffers for captured output.
32
+ @stdout_lines = []
33
+ @stderr_lines = []
34
+ @output = ''
35
+ @lines = []
36
+ @stdout = ''
37
+ @stderr = ''
38
+ @waiting_for_input = false
39
+
40
+ return @exitstatus = STATUS_SUCCESS if setup_command.nil? || setup_command.strip.empty?
41
+
42
+ run_command(setup_command)
43
+ end
44
+
45
+ # Executes a command in the bash session and returns its output.
46
+ # Stdout and stderr are captured separately with a timestamp for each line.
47
+ #
48
+ # @param command [String] The command to execute.
49
+ # @param interactive [Boolean] Whether this command expects user input
50
+ # @return [Hash] A hash containing :status, :output (stdout), :stdout, and :stderr.
51
+ def run_command(command, interactive = false)
52
+ @exitstatus = STATUS_SUCCESS
53
+ @command = command
54
+ @waiting_for_input = false
55
+ return { output: '', status: STATUS_SUCCESS,
56
+ stderr: '', stdout: '' } if command.nil? || command.strip.empty?
57
+
58
+ # Clear previous command output.
59
+ @stdout_lines = []
60
+ @stderr_lines = []
61
+
62
+ # Send the command to the shell.
63
+ @stdin.puts command
64
+ @stdin.flush
65
+
66
+ # For non-interactive commands, immediately send the boundary marker
67
+ unless interactive
68
+ @stdin.puts "\n_exitstatus=\"$?\""
69
+ @stdin.puts "echo '#{@boundary}'"
70
+ @stdin.puts 'echo $_exitstatus'
71
+ @stdin.flush
72
+ end
73
+
74
+ done = false
75
+ # Add timeout to prevent infinite loops
76
+ timeout = Time.now + 10 # 10 second timeout
77
+
78
+ # Loop to concurrently read stdout and stderr.
79
+ until done
80
+ # Check if stdin is ready for writing (process is waiting for input)
81
+ begin
82
+ stdin_ready = IO.select(nil, [@stdin], nil, 0)
83
+ @waiting_for_input = interactive && !stdin_ready.nil? && stdin_ready[1].include?(@stdin)
84
+ rescue IOError, Errno::EBADF
85
+ # Handle closed file descriptors
86
+ @waiting_for_input = false
87
+ end
88
+
89
+ begin
90
+ ready = IO.select([@stdout_stream, @stderr_stream], nil, nil, 0.1)
91
+ rescue IOError, Errno::EBADF
92
+ # Handle closed file descriptors
93
+ break
94
+ end
95
+
96
+ if Time.now > timeout
97
+ @waiting_for_input = false
98
+ # Send boundary marker to finish the command
99
+ @stdin.puts "\n_exitstatus=\"$?\""
100
+ @stdin.puts "echo '#{@boundary}'"
101
+ @stdin.puts 'echo $_exitstatus'
102
+ @stdin.flush
103
+ break
104
+ end
105
+
106
+ next unless ready # Skip if no IO is ready
107
+
108
+ ready[0].each do |io|
109
+ if io == @stdout_stream
110
+ begin
111
+ line = @stdout_stream.gets
112
+ rescue IOError
113
+ next
114
+ end
115
+ next if line.nil?
116
+
117
+ ts = Time.now
118
+ if line.include?(@boundary)
119
+ prefix = line[0...line.index(@boundary)]
120
+ add_stdout_line(timestamp: ts, line: prefix) unless prefix.empty?
121
+ # Read exit status from the next stdout line.
122
+ begin
123
+ status_line = @stdout_stream.gets
124
+ @exitstatus = status_line.strip.to_i if status_line
125
+ rescue IOError
126
+ # Handle closed file descriptors
127
+ end
128
+ done = true
129
+ break
130
+ else
131
+ add_stdout_line(timestamp: ts, line: line)
132
+ end
133
+ elsif io == @stderr_stream
134
+ begin
135
+ line = @stderr_stream.gets
136
+ rescue IOError
137
+ next
138
+ end
139
+ next if line.nil?
140
+
141
+ ts = Time.now
142
+ add_stderr_line(timestamp: ts, line: line)
143
+ end
144
+ end
145
+ end
146
+
147
+ # Replace the potentially hanging stderr capture with a non-blocking check
148
+ begin
149
+ if (ready = IO.select([@stderr_stream], nil, nil, 0.1))
150
+ ready[0].each do |io|
151
+ begin
152
+ while io.wait_readable(0.1) && (line = io.read_nonblock(4096))
153
+ ts = Time.now
154
+ line.each_line do |l|
155
+ add_stderr_line(timestamp: ts, line: l)
156
+ end
157
+ end
158
+ rescue IO::WaitReadable, IOError, Errno::EBADF
159
+ # No more data available right now or closed file descriptor
160
+ rescue EOFError
161
+ # End of stream reached
162
+ end
163
+ end
164
+ end
165
+ rescue IOError, Errno::EBADF
166
+ # Handle closed file descriptors
167
+ end
168
+
169
+ # Prepare outputs.
170
+ @output = @stdout_lines.map { |entry| entry[:line] }.join
171
+ @stdout = @output
172
+ @stderr = @stderr_lines.map { |entry| entry[:line] }.join
173
+ @lines = @stdout_lines.map { |entry| entry[:line] }
174
+
175
+ { status: @exitstatus, output: @output, stdout: @stdout, stderr: @stderr }
176
+ end
177
+
178
+ # Closes the bash session.
179
+ def close
180
+ @stdin.puts 'exit'
181
+ @stdin.flush
182
+ @stdin.close
183
+ @stdout_stream.close
184
+ @stderr_stream.close
185
+ @wait_thr.join
186
+ end
187
+
188
+ # Ensures the shell session is closed when the object is garbage collected.
189
+ def finalize
190
+ close unless @stdin.closed?
191
+ end
192
+
193
+ # Sends input to the running process
194
+ # @param input [String] The input to send to the process
195
+ def send_input(input)
196
+ return unless @waiting_for_input
197
+
198
+ begin
199
+ @stdin.puts(input)
200
+ @stdin.flush
201
+ rescue IOError, Errno::EBADF
202
+ @waiting_for_input = false
203
+ return
204
+ end
205
+
206
+ # After sending input, we need to check if we're still waiting
207
+ # Give a small delay for the process to consume the input
208
+ sleep 0.2
209
+
210
+ # Check if stdin is still ready for writing
211
+ begin
212
+ stdin_ready = IO.select(nil, [@stdin], nil, 0)
213
+ @waiting_for_input = !stdin_ready.nil? && stdin_ready[1].include?(@stdin)
214
+ rescue IOError, Errno::EBADF
215
+ @waiting_for_input = false
216
+ end
217
+
218
+ # If we're no longer waiting for input, send the boundary marker
219
+ unless @waiting_for_input
220
+ begin
221
+ @stdin.puts "\n_exitstatus=\"$?\""
222
+ @stdin.puts "echo '#{@boundary}'"
223
+ @stdin.puts 'echo $_exitstatus'
224
+ @stdin.flush
225
+ rescue IOError, Errno::EBADF
226
+ # Handle closed file descriptors
227
+ end
228
+ end
229
+ end
230
+
231
+ # Checks if the process is waiting for input
232
+ # @return [Boolean] True if the process is waiting for input
233
+ def waiting_for_input?
234
+ @waiting_for_input
235
+ end
236
+
237
+ private
238
+
239
+ def add_stdout_line(timestamp:, line:)
240
+ @stdout_lines << { timestamp: timestamp, line: line,
241
+ sequence: @line_counter }
242
+ @line_counter += 1
243
+ end
244
+
245
+ def add_stderr_line(timestamp:, line:)
246
+ @stderr_lines << { timestamp: timestamp, line: line,
247
+ sequence: @line_counter }
248
+ @line_counter += 1
249
+ end
250
+ end
251
+
252
+ return if $PROGRAM_NAME != __FILE__
253
+
254
+ require 'bundler/setup'
255
+ Bundler.require(:default)
256
+
257
+ require 'minitest/autorun'
258
+
259
+ class ShellSessionTest < Minitest::Test
260
+ def setup
261
+ @shell = ShellSession.new
262
+ end
263
+
264
+ def teardown
265
+ @shell.close
266
+ end
267
+
268
+ def test_initialize_without_setup_command
269
+ Timeout.timeout(5) do
270
+ assert_equal STATUS_SUCCESS, @shell.exitstatus
271
+ assert_empty @shell.output
272
+ assert_empty @shell.lines
273
+ end
274
+ end
275
+
276
+ def test_initialize_with_setup_command
277
+ Timeout.timeout(5) do
278
+ shell = ShellSession.new('echo "setup complete"')
279
+ assert_equal STATUS_SUCCESS, shell.exitstatus
280
+ assert_equal 'setup complete', shell.output.strip
281
+ shell.close
282
+ end
283
+ end
284
+
285
+ def test_run_command_with_empty_command
286
+ Timeout.timeout(5) do
287
+ result = @shell.run_command('')
288
+ assert_equal STATUS_SUCCESS, result[:status]
289
+ assert_empty result[:output]
290
+ end
291
+ end
292
+
293
+ def test_run_command_with_nil_command
294
+ Timeout.timeout(5) do
295
+ result = @shell.run_command(nil)
296
+ assert_equal STATUS_SUCCESS, result[:status]
297
+ assert_empty result[:output]
298
+ end
299
+ end
300
+
301
+ def test_run_command_echo
302
+ Timeout.timeout(5) do
303
+ result = @shell.run_command('echo "hello world"')
304
+ assert_equal STATUS_SUCCESS, result[:status]
305
+ assert_equal 'hello world', result[:output].strip
306
+ end
307
+ end
308
+
309
+ def test_run_command_with_multiple_lines
310
+ Timeout.timeout(5) do
311
+ result = @shell.run_command("echo 'line1'\necho 'line2'")
312
+ assert_equal STATUS_SUCCESS, result[:status]
313
+ assert_equal "line1\nline2\n", result[:output]
314
+ assert_equal %w[line1 line2],
315
+ @shell.lines.map(&:strip).reject(&:empty?)
316
+ end
317
+ end
318
+
319
+ def test_run_command_with_error
320
+ Timeout.timeout(5) do
321
+ result = @shell.run_command('nonexistent_command')
322
+ refute_equal STATUS_SUCCESS, result[:status]
323
+ end
324
+ end
325
+
326
+ def test_run_multiple_commands_in_sequence
327
+ Timeout.timeout(5) do
328
+ @shell.run_command('echo "first"')
329
+ result = @shell.run_command('echo "second"')
330
+ assert_equal STATUS_SUCCESS, result[:status]
331
+ assert_equal 'second', result[:output].strip
332
+ end
333
+ end
334
+
335
+ def test_close_and_reopen
336
+ Timeout.timeout(5) do
337
+ @shell.close
338
+ assert @shell.instance_variable_get(:@stdin).closed?
339
+
340
+ # Create new session.
341
+ @shell = ShellSession.new
342
+ result = @shell.run_command('echo "test"')
343
+ assert_equal STATUS_SUCCESS, result[:status]
344
+ end
345
+ end
346
+
347
+ def test_stdout_and_stderr_separation
348
+ Timeout.timeout(5) do
349
+ result = @shell.run_command('echo "to stdout" && echo "to stderr" >&2')
350
+ assert_equal STATUS_SUCCESS, result[:status]
351
+ assert_equal 'to stdout', result[:stdout].strip
352
+ assert_equal 'to stderr', result[:stderr].strip
353
+ end
354
+ end
355
+
356
+ def test_stdout_and_stderr_timestamps
357
+ Timeout.timeout(5) do
358
+ @shell.run_command('echo "stdout line" && echo "stderr line" >&2')
359
+
360
+ stdout_entry = @shell.stdout_lines.first
361
+ stderr_entry = @shell.stderr_lines.first
362
+
363
+ assert_instance_of Time, stdout_entry[:timestamp]
364
+ assert_instance_of Time, stderr_entry[:timestamp]
365
+ assert_equal "stdout line\n", stdout_entry[:line]
366
+ assert_equal "stderr line\n", stderr_entry[:line]
367
+ assert_equal 0, stdout_entry[:sequence]
368
+ assert_equal 1, stderr_entry[:sequence]
369
+ end
370
+ end
371
+
372
+ def test_stderr_only_output
373
+ Timeout.timeout(5) do
374
+ result = @shell.run_command('echo "error message" >&2')
375
+ assert_equal STATUS_SUCCESS, result[:status]
376
+ assert_empty result[:stdout]
377
+ assert_equal 'error message', result[:stderr].strip
378
+ end
379
+ end
380
+
381
+ def test_interleaved_stdout_stderr
382
+ Timeout.timeout(5) do
383
+ cmd = <<~SHELL
384
+ echo "out1"
385
+ sleep 0.1
386
+ echo "err1" >&2
387
+ sleep 0.1
388
+ echo "out2"
389
+ sleep 0.1
390
+ echo "err2" >&2
391
+ SHELL
392
+
393
+ result = @shell.run_command(cmd)
394
+ assert_equal STATUS_SUCCESS, result[:status]
395
+ assert_equal "out1\nout2\n", result[:stdout]
396
+ assert_equal "err1\nerr2\n", result[:stderr]
397
+
398
+ # Verify sequence numbers
399
+ all_lines = (@shell.stdout_lines + @shell.stderr_lines).sort_by do |entry|
400
+ entry[:sequence]
401
+ end
402
+ assert_equal(%W[out1\n err1\n out2\n err2\n], all_lines.map do |entry|
403
+ entry[:line]
404
+ end)
405
+ assert_equal([0, 1, 2, 3], all_lines.map { |entry| entry[:sequence] })
406
+ end
407
+ end
408
+
409
+ def test_detect_waiting_for_input
410
+ # Skip this test for now until we can fix the interactive command handling
411
+ skip "Skipping due to issues with interactive command handling"
412
+ end
413
+
414
+ def test_send_input_when_not_waiting_does_nothing
415
+ Timeout.timeout(5) do
416
+ # Run a command that doesn't wait for input
417
+ @shell.run_command('echo "No input needed"')
418
+
419
+ # Verify we're not waiting for input
420
+ refute @shell.waiting_for_input?, "Should not be waiting for input"
421
+
422
+ # Sending input should have no effect
423
+ @shell.send_input("ignored input")
424
+
425
+ # Output should be unchanged
426
+ assert_equal "No input needed\n", @shell.output
427
+ end
428
+ end
429
+
430
+ def test_waiting_for_input_predicate_method
431
+ # Skip this test for now until we can fix the interactive command handling
432
+ skip "Skipping due to issues with interactive command handling"
433
+ end
434
+
435
+ def test_multiple_input_prompts
436
+ # Skip this test for now until we can fix the interactive command handling
437
+ skip "Skipping due to issues with interactive command handling"
438
+ end
439
+ end
data/lib/streams_out.rb CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env ruby
2
1
  # frozen_string_literal: true
3
2
 
4
3
  # encoding=utf-8
data/lib/string_util.rb CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env ruby
2
1
  # frozen_string_literal: true
3
2
 
4
3
  # encoding=utf-8
@@ -19,3 +18,14 @@ module StringUtil
19
18
  end
20
19
  end
21
20
  end
21
+
22
+ class String
23
+ unless method_defined?(:present?)
24
+ # Checks if the string contains any non-whitespace characters.
25
+ # @return [Boolean] Returns true if the string contains non-whitespace
26
+ # characters, false otherwise.
27
+ def present?
28
+ !strip.empty?
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env -S bundle exec ruby
2
+ # frozen_string_literal: true
3
+
4
+ # encoding=utf-8
5
+
6
+ # frozen_string_literal: true
7
+
8
+ require 'singleton'
9
+
10
+ ##
11
+ # SuccessResult represents a successful outcome when no specific result value is produced.
12
+ #
13
+ # This class follows the Null Object pattern for successful cases, ensuring a consistent
14
+ # interface with methods such as #success? and #failure?. It is implemented as a singleton,
15
+ # meaning there is only one instance of SuccessResult available.
16
+ #
17
+ # Example:
18
+ # result = SomeService.call
19
+ # if result.success?
20
+ # # proceed knowing the operation succeeded
21
+ # else
22
+ # # handle failure
23
+ # end
24
+ #
25
+ class SuccessResult
26
+ include Singleton
27
+
28
+ ##
29
+ # Indicates that the result is a success.
30
+ #
31
+ # @return [Boolean] always true for SuccessResult
32
+ def success?
33
+ true
34
+ end
35
+
36
+ ##
37
+ # Indicates that the result is not a failure.
38
+ #
39
+ # @return [Boolean] always false for SuccessResult
40
+ def failure?
41
+ false
42
+ end
43
+
44
+ ##
45
+ # Provides a default message for the successful result.
46
+ #
47
+ # @return [String] a message indicating success
48
+ def message
49
+ 'Success'
50
+ end
51
+
52
+ ##
53
+ # Returns a string representation of this SuccessResult.
54
+ #
55
+ # @return [String]
56
+ def to_s
57
+ 'SuccessResult'
58
+ end
59
+ end
60
+
61
+ # Default instance for ease-of-use.
62
+ DEFAULT_SUCCESS_RESULT = SuccessResult.instance
63
+
64
+ return unless $PROGRAM_NAME == __FILE__
65
+
66
+ require 'bundler/setup'
67
+ Bundler.require(:default)
68
+
69
+ require 'minitest/autorun'
70
+ require 'mocha/minitest'
71
+
72
+ require_relative 'ww'
73
+
74
+ ##
75
+ # Tests for the SuccessResult class.
76
+ #
77
+ # This suite verifies that the SuccessResult singleton behaves as expected:
78
+ # - It is a singleton (all calls to SuccessResult.instance return the same object)
79
+ # - The #success? method returns true and #failure? returns false
80
+ # - The default message and string representation are correct.
81
+ #
82
+ class SuccessResultTest < Minitest::Test
83
+ def test_singleton
84
+ instance1 = SuccessResult.instance
85
+ instance2 = SuccessResult.instance
86
+ assert_same instance1, instance2, "Expected the singleton instances to be identical"
87
+ end
88
+
89
+ def test_success_method
90
+ sr = SuccessResult.instance
91
+ assert sr.success?, "Expected success? to return true"
92
+ end
93
+
94
+ def test_failure_method
95
+ sr = SuccessResult.instance
96
+ refute sr.failure?, "Expected failure? to return false"
97
+ end
98
+
99
+ def test_message
100
+ sr = SuccessResult.instance
101
+ assert_equal 'Success', sr.message, "Expected message to be 'Success'"
102
+ end
103
+
104
+ def test_to_s
105
+ sr = SuccessResult.instance
106
+ assert_equal 'SuccessResult', sr.to_s, "Expected to_s to return 'SuccessResult'"
107
+ end
108
+
109
+ def test_default_success_result_constant
110
+ assert_same SuccessResult.instance, DEFAULT_SUCCESS_RESULT, "Expected DEFAULT_SUCCESS_RESULT to be the same singleton instance"
111
+ end
112
+ end
data/lib/text_analyzer.rb CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env -S bundle exec ruby
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module TextAnalyzer
data/lib/ww.rb CHANGED
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # encoding=utf-8
4
+ require 'bundler/setup' # Bundler enforces gem versions
4
5
  require 'pp'
5
6
  require 'stringio'
6
7
 
7
- LOG_LEVELS = %i[debug info warn error fatal]
8
+ LOG_LEVELS = %i[debug info warn error fatal].freeze
8
9
 
9
10
  $debug = $DEBUG || !ENV['WW'].nil?
10
11
 
@@ -15,7 +16,8 @@ if $debug && ENV['WW_MINIMUM'].nil?
15
16
  end
16
17
 
17
18
  def ww(*objs, **kwargs)
18
- return objs.size == 1 ? objs.first : objs unless $debug
19
+ # return the last item in the list, as the label is usually first
20
+ return objs.last unless $debug
19
21
 
20
22
  ww0(*objs, **kwargs.merge(locations: caller_locations))
21
23
  end
@@ -55,12 +57,11 @@ def ww0(*objs,
55
57
  # Combine all parts into the final message
56
58
  header = "#{time_prefix}#{level_prefix} #{category_prefix}"
57
59
  trace = backtrace + objs
60
+ io = StringIO.new
58
61
  formatted_message = if single_line
59
- io = StringIO.new
60
62
  PP.singleline_pp(trace, io)
61
63
  "#{header} #{io.string}"
62
64
  else
63
- io = StringIO.new
64
65
  PP.pp(trace, io)
65
66
  "#{header}\n#{io.string}"
66
67
  end
@@ -76,14 +77,15 @@ def ww0(*objs,
76
77
  file.puts(formatted_message)
77
78
  end
78
79
 
79
- objs.size == 1 ? objs.first : objs
80
+ # return the last item in the list, as the label is usually first
81
+ objs.last
80
82
  end
81
83
 
82
84
  class Array
83
85
  unless defined?(deref)
84
86
  def deref
85
- map(&:deref).select do |line|
86
- !%r{^/(vendor|\.bundle)/}.match(line)
87
+ map(&:deref).reject do |line|
88
+ %r{^/(vendor|\.bundle)/}.match(line)
87
89
  end
88
90
  end
89
91
  end