markdown_exec 2.3.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +11 -2
  3. data/CHANGELOG.md +19 -0
  4. data/Gemfile.lock +1 -1
  5. data/Rakefile +32 -8
  6. data/bats/bats.bats +33 -0
  7. data/bats/block-types.bats +56 -0
  8. data/bats/cli.bats +74 -0
  9. data/bats/fail.bats +11 -0
  10. data/bats/history.bats +34 -0
  11. data/bats/markup.bats +66 -0
  12. data/bats/mde.bats +29 -0
  13. data/bats/options.bats +92 -0
  14. data/bats/test_helper.bash +152 -0
  15. data/bin/tab_completion.sh +44 -20
  16. data/docs/dev/block-type-opts.md +10 -0
  17. data/docs/dev/block-type-port.md +24 -0
  18. data/docs/dev/block-type-vars.md +7 -0
  19. data/docs/dev/pass-through-arguments.md +8 -0
  20. data/docs/dev/specs-import.md +9 -0
  21. data/docs/dev/specs.md +83 -0
  22. data/docs/dev/text-decoration.md +7 -0
  23. data/examples/bash-blocks.md +4 -4
  24. data/examples/block-names.md +2 -2
  25. data/examples/import0.md +23 -0
  26. data/examples/import1.md +13 -0
  27. data/examples/link-blocks-vars.md +3 -3
  28. data/examples/opts-blocks-require.md +6 -6
  29. data/examples/table-markup.md +31 -0
  30. data/examples/text-markup.md +58 -0
  31. data/examples/vars-blocks.md +2 -2
  32. data/examples/wrap.md +87 -9
  33. data/lib/ansi_formatter.rb +12 -6
  34. data/lib/ansi_string.rb +153 -0
  35. data/lib/argument_processor.rb +160 -0
  36. data/lib/cached_nested_file_reader.rb +4 -2
  37. data/lib/ce_get_cost_and_usage.rb +4 -3
  38. data/lib/cli.rb +1 -1
  39. data/lib/colorize.rb +39 -11
  40. data/lib/constants.rb +17 -0
  41. data/lib/directory_searcher.rb +4 -2
  42. data/lib/doh.rb +190 -0
  43. data/lib/env.rb +1 -1
  44. data/lib/exceptions.rb +9 -6
  45. data/lib/fcb.rb +0 -199
  46. data/lib/filter.rb +18 -5
  47. data/lib/find_files.rb +8 -3
  48. data/lib/format_table.rb +406 -0
  49. data/lib/hash_delegator.rb +888 -603
  50. data/lib/hierarchy_string.rb +113 -25
  51. data/lib/input_sequencer.rb +16 -10
  52. data/lib/instance_method_wrapper.rb +2 -1
  53. data/lib/layered_hash.rb +143 -0
  54. data/lib/link_history.rb +22 -8
  55. data/lib/markdown_exec/version.rb +1 -1
  56. data/lib/markdown_exec.rb +413 -165
  57. data/lib/mdoc.rb +27 -34
  58. data/lib/menu.src.yml +825 -710
  59. data/lib/menu.yml +799 -703
  60. data/lib/namer.rb +6 -12
  61. data/lib/object_present.rb +1 -1
  62. data/lib/option_value.rb +7 -3
  63. data/lib/poly.rb +33 -14
  64. data/lib/resize_terminal.rb +60 -52
  65. data/lib/saved_assets.rb +45 -34
  66. data/lib/saved_files_matcher.rb +6 -3
  67. data/lib/streams_out.rb +7 -1
  68. data/lib/table_extractor.rb +166 -0
  69. data/lib/tap.rb +5 -6
  70. data/lib/text_analyzer.rb +144 -8
  71. metadata +26 -3
  72. data/lib/std_out_err_logger.rb +0 -119
data/lib/markdown_exec.rb CHANGED
@@ -115,12 +115,14 @@ module MarkdownExec
115
115
 
116
116
  # A class that generates a histogram bar in terminal using xterm-256 color codes.
117
117
  class Histogram
118
- # Generates and prints a histogram bar for a given value within a specified range and width, with an option for inverse display.
118
+ # Generates and prints a histogram bar for a given value within a
119
+ # specified range and width, with an option for inverse display.
119
120
  # @param integer_value [Integer] the value to represent in the histogram
120
121
  # @param min [Integer] the minimum value of the range
121
122
  # @param max [Integer] the maximum value of the range
122
123
  # @param width [Integer] the total width of the histogram in characters
123
- # @param inverse [Boolean] whether the histogram is displayed in inverse order (right to left)
124
+ # @param inverse [Boolean] whether the histogram is
125
+ # displayed in inverse order (right to left)
124
126
  def self.display(integer_value, min, max, width, inverse: false)
125
127
  return if max <= min # Ensure the range is valid
126
128
 
@@ -135,7 +137,7 @@ module MarkdownExec
135
137
 
136
138
  # # Generate the histogram bar using xterm-256 colors (color code 42 is green)
137
139
  # filled_bar = "\e[48;5;42m" + ' ' * filled_length + "\e[0m"
138
- filled_bar = ('¤' * filled_length).fg_rgbh_AF_AF_00
140
+ filled_bar = AnsiString.new('¤' * filled_length).fg_rgbh_AF_AF_00
139
141
  empty_bar = ' ' * (width - filled_length)
140
142
 
141
143
  # Determine the order of filled and empty parts based on the inverse flag
@@ -150,26 +152,34 @@ module MarkdownExec
150
152
  end
151
153
 
152
154
  def build_menu(file_names, directory_names, found_in_block_names,
153
- files_in_directories, vbn)
155
+ file_name_choices, choices_from_block_names)
154
156
  choices = []
155
157
 
156
158
  # Adding section title and data for file names
157
- choices << { disabled: '',
158
- name: "in #{file_names[:section_title]}".send(@chrome_color) }
159
+ choices << {
160
+ disabled: '',
161
+ name: AnsiString.new("in #{file_names[:section_title]}")
162
+ .send(@chrome_color)
163
+ }
159
164
  choices += file_names[:data].map { |str| FileInMenu.for_menu(str) }
160
165
 
161
166
  # Conditionally add directory names if data is present
162
- unless directory_names[:data].count.zero?
163
- choices << { disabled: '',
164
- name: "in #{directory_names[:section_title]}".send(@chrome_color) }
165
- choices += files_in_directories
167
+ if directory_names[:data].any?
168
+ choices << {
169
+ disabled: '',
170
+ name: AnsiString.new("in #{directory_names[:section_title]}")
171
+ .send(@chrome_color)
172
+ }
173
+ choices += file_name_choices
166
174
  end
167
175
 
168
176
  # Adding found in block names
169
- choices << { disabled: '',
170
- name: "in #{found_in_block_names[:section_title]}".send(@chrome_color) }
171
-
172
- choices += vbn
177
+ choices << {
178
+ disabled: '',
179
+ name: AnsiString.new("in #{found_in_block_names[:section_title]}")
180
+ .send(@chrome_color)
181
+ }
182
+ choices += choices_from_block_names
173
183
 
174
184
  choices
175
185
  end
@@ -181,18 +191,22 @@ module MarkdownExec
181
191
  {
182
192
  section_title: 'directory names',
183
193
  data: matched_directories,
184
- formatted_text: [{ content: AnsiFormatter.new(search_options).format_and_highlight_array(
185
- matched_directories, highlight: [highlight_value]
186
- ) }]
194
+ formatted_text: [{ content:
195
+ AnsiFormatter.new(search_options).format_and_highlight_array(
196
+ matched_directories, highlight: [highlight_value]
197
+ ) }]
187
198
  }
188
199
  end
189
200
 
190
201
  def found_in_block_names(search_options, highlight_value,
191
202
  formspec: '=%<index>4.d: %<line>s')
192
- matched_contents = (find_file_contents do |line|
193
- read_block_name(line, search_options[:fenced_start_and_end_regex],
194
- search_options[:block_name_match], search_options[:block_name_nick_match])
195
- end).map.with_index do |(file, contents), index|
203
+ matched_contents = (
204
+ find_file_contents do |line|
205
+ read_block_name(line,
206
+ search_options[:fenced_start_and_end_regex],
207
+ search_options[:block_name_match],
208
+ search_options[:block_name_nick_match])
209
+ end).map.with_index do |(file, contents), index|
196
210
  # [file, contents.map { |detail| format(formspec, detail.index, detail.line) }, index]
197
211
  [file, contents.map do |detail|
198
212
  format(formspec, { index: detail.index, line: detail.line })
@@ -201,13 +215,14 @@ module MarkdownExec
201
215
  {
202
216
  section_title: 'block names',
203
217
  data: matched_contents.map(&:first),
204
- formatted_text: matched_contents.map do |(file, details, index)|
205
- { header: format('- %3.d: %s', index + 1, file),
206
- content: AnsiFormatter.new(search_options).format_and_highlight_array(
207
- details,
208
- highlight: [highlight_value]
209
- ) }
210
- end,
218
+ formatted_text:
219
+ matched_contents.map do |(file, details, index)|
220
+ { header: format('- %3.d: %s', index + 1, file),
221
+ content: AnsiFormatter.new(search_options).format_and_highlight_array(
222
+ details,
223
+ highlight: [highlight_value]
224
+ ) }
225
+ end,
211
226
  matched_contents: matched_contents
212
227
  }
213
228
  end
@@ -217,9 +232,12 @@ module MarkdownExec
217
232
  {
218
233
  section_title: 'file names',
219
234
  data: matched_files,
220
- formatted_text: [{ content: AnsiFormatter.new(search_options).format_and_highlight_array(
221
- matched_files, highlight: [highlight_value]
222
- ).join("\n") }]
235
+ formatted_text:
236
+ [{ content:
237
+ AnsiFormatter.new(search_options).format_and_highlight_array(
238
+ matched_files,
239
+ highlight: [highlight_value]
240
+ ).join("\n") }]
223
241
  }
224
242
  end
225
243
 
@@ -268,20 +286,6 @@ module MarkdownExec
268
286
  )
269
287
  end
270
288
 
271
- # :reek:UtilityFunction
272
- def list_recent_output(saved_stdout_folder, saved_stdout_glob,
273
- list_count)
274
- SavedFilesMatcher.most_recent_list(saved_stdout_folder,
275
- saved_stdout_glob, list_count)
276
- end
277
-
278
- # :reek:UtilityFunction
279
- def list_recent_scripts(saved_script_folder, saved_script_glob,
280
- list_count)
281
- SavedFilesMatcher.most_recent_list(saved_script_folder,
282
- saved_script_glob, list_count)
283
- end
284
-
285
289
  def warn_format(name, message, opts = {})
286
290
  Exceptions.warn_format(
287
291
  "CachedNestedFileReader.#{name} -- #{message}",
@@ -329,22 +333,52 @@ module MarkdownExec
329
333
  }
330
334
  end
331
335
 
336
+ def choices_from_block_names(value, found_in_block_names)
337
+ found_in_block_names[:matched_contents].map do |matched_contents|
338
+ filename, details, = matched_contents
339
+ nexo = AnsiFormatter.new(@options).format_and_highlight_array(
340
+ details,
341
+ highlight: [value]
342
+ )
343
+ [FileInMenu.for_menu(filename)] +
344
+ nexo.map do |str|
345
+ { disabled: '', name: (' ' * 20) + str }
346
+ end
347
+ end.flatten
348
+ end
349
+
350
+ def choices_from_file_names(directory_names)
351
+ directory_names[:data].map do |dn|
352
+ find_files('*', [dn], exclude_dirs: true)
353
+ end.flatten(1).map { |str| FileInMenu.for_menu(str) }
354
+ end
355
+
332
356
  public
333
357
 
334
358
  ## Determines the correct filename to use for searching files
335
359
  #
336
- def determine_filename(specified_filename: nil, specified_folder: nil, default_filename: nil,
337
- default_folder: nil, filetree: nil)
338
- if specified_filename&.present?
339
- return specified_filename if specified_filename.start_with?('/')
340
-
341
- File.join(specified_folder || default_folder, specified_filename)
342
- elsif specified_folder&.present?
343
- File.join(specified_folder,
344
- filetree ? @options[:md_filename_match] : @options[:md_filename_glob])
345
- else
346
- File.join(default_folder, default_filename)
347
- end
360
+ def determine_filename(
361
+ specified_filename: nil, specified_folder: nil,
362
+ default_filename: nil, default_folder: nil, filetree: nil
363
+ )
364
+ File.join(
365
+ *(if specified_filename&.present?
366
+ if specified_filename.start_with?('/')
367
+ [specified_filename]
368
+ else
369
+ [specified_folder || default_folder, specified_filename]
370
+ end
371
+ elsif specified_folder&.present?
372
+ [specified_folder,
373
+ if filetree
374
+ @options[:md_filename_match]
375
+ else
376
+ @options[:md_filename_glob]
377
+ end]
378
+ else
379
+ [default_folder, default_filename]
380
+ end)
381
+ )
348
382
  end
349
383
 
350
384
  private
@@ -356,82 +390,57 @@ module MarkdownExec
356
390
  # raise ArgumentError, error
357
391
  # end
358
392
 
359
- # Reports and executes block logic
360
- def execute_block_logic(files)
361
- @options[:filename] = select_document_if_multiple(files)
362
- @options.document_inpseq
363
- rescue StandardError
364
- error_handler('execute_block_logic')
365
- # rubocop:disable Style/RescueStandardError
366
- rescue
367
- pp $!, $@
368
- exit 1
369
- # rubocop:enable Style/RescueStandardError
370
- end
371
-
372
393
  ## Executes the block specified in the options
373
394
  #
374
395
  def execute_block_with_error_handling
375
396
  finalize_cli_argument_processing
376
- execute_code_block_based_on_options(@options)
397
+ execute_code_block_based_on_options(@options, @options.run_state)
377
398
  rescue FileMissingError
378
399
  warn "File missing: #{$!}"
379
- rescue StandardError
380
- error_handler('execute_block_with_error_handling')
381
400
  end
382
401
 
383
402
  # Main method to execute a block based on options and block_name
384
- def execute_code_block_based_on_options(options)
403
+ def execute_code_block_based_on_options(options, run_state)
385
404
  options = calculated_options.merge(options)
386
405
  update_options(options, over: false)
406
+ # recognize commands with an opt_name, no procname
407
+ return if execute_simple_commands(options)
387
408
 
388
- simple_commands = {
389
- doc_glob: -> { @fout.fout options[:md_filename_glob] },
390
- # list_blocks: -> { list_blocks },
391
- list_default_env: -> { @fout.fout_list list_default_env },
392
- list_default_yaml: -> { @fout.fout_list list_default_yaml },
393
- list_docs: -> { @fout.fout_list files },
394
- list_recent_output: -> {
395
- @fout.fout_list list_recent_output(
396
- @options[:saved_stdout_folder],
397
- @options[:saved_stdout_glob], @options[:list_count]
398
- )
399
- },
400
- list_recent_scripts: -> {
401
- @fout.fout_list list_recent_scripts(
402
- options[:saved_script_folder],
403
- options[:saved_script_glob], options[:list_count]
404
- )
405
- },
406
- pwd: -> { @fout.fout File.expand_path('..', __dir__) },
407
- run_last_script: -> { run_last_script },
408
- tab_completions: -> { @fout.fout tab_completions },
409
- menu_export: -> { @fout.fout menu_export }
410
- }
411
-
412
- return if execute_simple_commands(simple_commands)
413
-
414
- files = opts_prepare_file_list(options)
415
- execute_block_logic(files)
409
+ mde_vux_main_loop(opts_prepare_file_list(options))
416
410
  return unless @options[:output_saved_script_filename]
417
411
 
418
- @fout.fout "script_block_name: #{@options.run_state.script_block_name}"
419
- @fout.fout "s_save_filespec: #{@options.run_state.saved_filespec}"
420
- rescue StandardError
421
- error_handler('execute_code_block_based_on_options')
412
+ @fout.fout "script_block_name: #{run_state.script_block_name}"
413
+ @fout.fout "s_save_filespec: #{run_state.saved_filespec}"
422
414
  end
423
415
 
424
416
  # Executes command based on the provided option keys
425
- def execute_simple_commands(simple_commands)
426
- simple_commands.each_key do |key|
427
- if @options[key]
428
- simple_commands[key].call
417
+ def execute_simple_commands(options)
418
+ simple_commands(options).each do |key, proc|
419
+ if @options[key].is_a?(TrueClass) || @options[key].present?
420
+ proc.call
429
421
  return true
430
422
  end
431
423
  end
432
424
  false
433
425
  end
434
426
 
427
+ # Extracts all lines matching the given regular expression from a file.
428
+ #
429
+ # @param file_path [String] The path to the file to be searched.
430
+ # @param pattern [Regexp] The regular expression pattern to match.
431
+ # @return [Array<String>] An array of lines from the file that match the pattern.
432
+ def extract_lines_matching(file_path, pattern)
433
+ matching_lines = []
434
+
435
+ File.open(file_path, 'r') do |file|
436
+ file.each_line do |line|
437
+ matching_lines << line if line =~ pattern
438
+ end
439
+ end
440
+
441
+ matching_lines
442
+ end
443
+
435
444
  ## post-parse options configuration
436
445
  #
437
446
  def finalize_cli_argument_processing(rest = @rest)
@@ -449,9 +458,6 @@ module MarkdownExec
449
458
  end
450
459
  end
451
460
 
452
- ## position 1: block name (optional)
453
- #
454
- @options[:block_name] = nil
455
461
  @options[:input_cli_rest] = @rest
456
462
  rescue FileMissingError
457
463
  warn_format('finalize_cli_argument_processing',
@@ -474,6 +480,31 @@ module MarkdownExec
474
480
  directory_names = searcher.directory_names(options, value)
475
481
 
476
482
  ### search in file contents (block names, chrome, or text)
483
+ found_report(found_in_block_names, directory_names, file_names)
484
+
485
+ return { exit: true } unless execute_chosen_found
486
+
487
+ file_name_choices = choices_from_file_names(directory_names)
488
+
489
+ unless file_names[:data]&.count.positive? ||
490
+ file_name_choices&.count.positive? ||
491
+ found_in_block_names[:data]&.count.positive?
492
+ return :exit
493
+ end
494
+
495
+ ## pick a document to open
496
+ #
497
+ found_files_build_menu_user_select(
498
+ file_names, directory_names, found_in_block_names,
499
+ file_name_choices,
500
+ choices_from_block_names(value, found_in_block_names)
501
+ )
502
+ { exit: false }
503
+ end
504
+
505
+ def found_report(found_in_block_names,
506
+ directory_names,
507
+ file_names)
477
508
  [found_in_block_names,
478
509
  directory_names,
479
510
  file_names].each do |data|
@@ -486,70 +517,155 @@ module MarkdownExec
486
517
  @fout.fout fi[:content] if fi[:content]
487
518
  end
488
519
  end
489
- return { exit: true } unless execute_chosen_found
490
-
491
- ## pick a document to open
492
- #
493
- files_in_directories = directory_names[:data].map do |dn|
494
- find_files('*', [dn], exclude_dirs: true)
495
- end.flatten(1).map { |str| FileInMenu.for_menu(str) }
496
-
497
- unless file_names[:data]&.count.positive? || files_in_directories&.count.positive? || found_in_block_names[:data]&.count.positive?
498
- return { exit: true }
499
- end
500
-
501
- vbn = found_in_block_names[:matched_contents].map do |matched_contents|
502
- filename, details, = matched_contents
503
- nexo = AnsiFormatter.new(@options).format_and_highlight_array(
504
- details,
505
- highlight: [value]
506
- )
507
- [FileInMenu.for_menu(filename)] +
508
- nexo.map do |str|
509
- { disabled: '', name: (' ' * 20) + str }
510
- end
511
- end.flatten
520
+ end
512
521
 
513
- choices = MenuBuilder.new.build_menu(file_names, directory_names, found_in_block_names,
514
- files_in_directories, vbn)
522
+ def found_files_build_menu_user_select(
523
+ file_names, directory_names, found_in_block_names,
524
+ file_name_choices, choices_from_block_names
525
+ )
526
+ choices = MenuBuilder.new.build_menu(
527
+ file_names, directory_names, found_in_block_names,
528
+ file_name_choices, choices_from_block_names
529
+ )
515
530
 
516
531
  @options[:filename] = FileInMenu.from_menu(
517
532
  select_document_if_multiple(
518
533
  choices,
519
- prompt: options[:prompt_select_md].to_s + ' ¤ Age in months'.fg_rgbh_AF_AF_00
534
+ prompt: options[:prompt_select_md].to_s +
535
+ AnsiString.new(' ¤ Age in months').fg_rgbh_AF_AF_00
520
536
  )
521
537
  )
522
- { exit: false }
538
+ end
539
+
540
+ def fout_list(list)
541
+ if @options[:list_output_format] == :yaml
542
+ @fout.fout list.to_yaml.sub(/^---\n/, '')
543
+ elsif @options[:list_output_format] == :json
544
+ @fout.fout list.to_json
545
+ else # :text
546
+ list.each do |item|
547
+ @fout.fout item
548
+ end
549
+ end
550
+ end
551
+
552
+ def history(probe: true)
553
+ if probe && @options[:probe].present?
554
+ probe_regexp = Regexp.new(@options[:probe], Regexp::IGNORECASE)
555
+ end
556
+
557
+ if @options[:sift].present?
558
+ sift_regexp = Regexp.new(@options[:sift], Regexp::IGNORECASE)
559
+ end
560
+
561
+ files_table_rows = @options.read_saved_assets_for_history_table
562
+
563
+ if sift_regexp
564
+ # Filter history to file names matching a pattern
565
+ files_table_rows.select! { |item| sift_regexp.match(item[:file]) }
566
+ end
567
+
568
+ if probe_regexp
569
+ # Filter history to files with lines matching a pattern
570
+ files_table_rows.each do |item|
571
+ item[:probe] = extract_lines_matching(item[:file], probe_regexp)
572
+ end
573
+ files_table_rows.select! { |item| item[:probe].count.positive? }
574
+ end
575
+
576
+ if files_table_rows.count.zero?
577
+ warn 'Nothing revealed.'
578
+ elsif probe || probe_regexp
579
+ if @options[:dig]
580
+ # Present menu of history
581
+ @options.register_console_attributes(@options)
582
+ @options.execute_history_select(
583
+ files_table_rows,
584
+ exit_prompt: @options[:prompt_exit],
585
+ pause_refresh: true,
586
+ stream: $stderr
587
+ )
588
+ elsif @options[:mine]
589
+ # List lines matched by probe
590
+ files_table_rows.each do |item|
591
+ @fout.fout(item[:file])
592
+ fout_list(item[:probe])
593
+ end
594
+ else
595
+ fout_list(files_table_rows.map(&:file))
596
+ end
597
+ else
598
+ @options.register_console_attributes(@options)
599
+ @options.execute_history_select(
600
+ files_table_rows,
601
+ exit_prompt: @options[:prompt_exit],
602
+ pause_refresh: true,
603
+ stream: $stderr
604
+ )
605
+ end
523
606
  end
524
607
 
525
608
  ## Sets up the options and returns the parsed arguments
526
609
  #
527
- def initialize_and_parse_cli_options
528
- # @options = base_options
610
+ def initialize_parse_execute_cli
529
611
  @options = HashDelegator.new(base_options)
530
-
612
+ @options.p_all_arguments = ARGV.dup # Duplicate ARGV to track original order
613
+ ### should not include after '--'
531
614
  read_configuration_file!(@options,
532
615
  ".#{MarkdownExec::APP_NAME.downcase}.yml")
533
616
 
617
+ options_parsed = []
534
618
  @option_parser = OptionParser.new do |opts|
535
619
  executable_name = File.basename($PROGRAM_NAME)
536
620
  opts.banner = [
537
621
  "#{MarkdownExec::APP_NAME}" \
538
622
  " - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})",
539
- "Usage: #{executable_name} [(directory | file [block_name] | search_keyword)] [options]"
623
+ "Usage: #{executable_name}" \
624
+ ' [(directory | file [block_name] | search_keyword)] [options]'
540
625
  ].join("\n")
541
626
 
542
627
  menu_iter do |item|
543
- opts_menu_option_append opts, @options, item
628
+ opts_menu_option_append(opts, @options, item, options_parsed)
544
629
  end
545
630
  end
546
631
  @option_parser.load
547
632
  @option_parser.environment
548
- @rest = rest = @option_parser.parse!(arguments_for_mde)
549
- @options.pass_args = ARGV[rest.count + 1..]
633
+ @options.p_params = {}
634
+ @rest = @option_parser.parse!(arguments_for_mde, into: @options.p_params)
635
+ @options.p_options_parsed = options_parsed
636
+ @options.p_rest = @rest.dup
637
+
638
+ # Arguments for script follow ARGV, excluding arguments reserved for MDE, and separator.
639
+ # Parsed options have already been removed from ARGV.
640
+ @options.pass_args = ARGV[@rest.count + 1..]
641
+
550
642
  @options.merge(@options.run_state.to_h)
643
+ end
551
644
 
552
- rest
645
+ def iter_source_blocks(source, &block)
646
+ case source
647
+ when 1
648
+ HashDelegator.new(@options).blocks_from_nested_files.each(&block)
649
+ when 2
650
+ blocks_in_file, menu_blocks, mdoc =
651
+ HashDelegator.new(@options)
652
+ .mdoc_menu_and_blocks_from_nested_files(LinkState.new)
653
+ blocks_in_file.each(&block)
654
+ when 3
655
+ blocks_in_file, menu_blocks, mdoc =
656
+ HashDelegator.new(@options)
657
+ .mdoc_menu_and_blocks_from_nested_files(LinkState.new)
658
+ menu_blocks.each(&block)
659
+ else
660
+ @options.iter_blocks_from_nested_files do |btype, fcb|
661
+ case btype
662
+ when :blocks
663
+ yield fcb
664
+ when :filter
665
+ %i[blocks]
666
+ end
667
+ end
668
+ end
553
669
  end
554
670
 
555
671
  ##
@@ -565,8 +681,9 @@ module MarkdownExec
565
681
  ->(_) { exit }
566
682
  when 'find', 'open'
567
683
  ->(value) {
568
- exit if find_value(value, execute_chosen_found: procname == 'open').fetch(:exit,
569
- false)
684
+ exit if find_value(
685
+ value, execute_chosen_found: procname == 'open'
686
+ ).fetch(:exit, false)
570
687
  }
571
688
  when 'help'
572
689
  ->(_) {
@@ -595,6 +712,8 @@ module MarkdownExec
595
712
  lambda(&:to_i)
596
713
  when 'val_as_str'
597
714
  lambda(&:to_s)
715
+ when 'val_as_sym'
716
+ lambda(&:to_sym)
598
717
  when 'version'
599
718
  lambda { |_|
600
719
  @fout.fout MarkdownExec::VERSION
@@ -605,7 +724,17 @@ module MarkdownExec
605
724
  end
606
725
  end
607
726
 
608
- # def list_blocks; end
727
+ def list_blocks
728
+ message = @options[:list_blocks_message]
729
+ block_eval = @options[:list_blocks_eval]
730
+
731
+ list = []
732
+ iter_source_blocks(@options[:list_blocks_type]) do |block|
733
+ list << (block_eval.present? ? eval(block_eval) : block.send(message))
734
+ end
735
+
736
+ fout_list(list)
737
+ end
609
738
 
610
739
  def list_default_env
611
740
  menu_iter do |item|
@@ -646,6 +775,35 @@ module MarkdownExec
646
775
  @options[:md_filename_glob]))
647
776
  end
648
777
 
778
+ # :reek:UtilityFunction
779
+ def list_recent_output(saved_stdout_folder, saved_stdout_glob,
780
+ list_count)
781
+ SavedFilesMatcher.most_recent_list(saved_stdout_folder,
782
+ saved_stdout_glob, list_count)
783
+ end
784
+
785
+ # :reek:UtilityFunction
786
+ def list_recent_scripts(saved_script_folder, saved_script_glob,
787
+ list_count)
788
+ SavedFilesMatcher.most_recent_list(saved_script_folder,
789
+ saved_script_glob, list_count)
790
+ end
791
+
792
+ # Reports and executes block logic
793
+ def mde_vux_main_loop(files)
794
+ @options[:filename] = select_document_if_multiple(files)
795
+ @options.vux_main_loop do |type, data|
796
+ case type
797
+ when :command_names
798
+ simple_commands(data).keys
799
+ when :call_proc
800
+ simple_commands(data[0])[data[1]].call
801
+ else
802
+ raise
803
+ end
804
+ end
805
+ end
806
+
649
807
  private
650
808
 
651
809
  ##
@@ -675,37 +833,88 @@ module MarkdownExec
675
833
  end.to_yaml
676
834
  end
677
835
 
678
- def opts_menu_option_append(opts, options, item)
836
+ def opts_menu_option_append(opts, options, item, options_parsed)
679
837
  return unless item[:long_name].present? || item[:short_name].present?
680
838
 
681
- opts.on(*[
839
+ optname = "-#{item[:short_name]}"
840
+ switches = [
841
+ # - argument style = :NONE, :REQUIRED, :OPTIONAL
842
+ case item[:procname]&.to_s
843
+ when nil
844
+ :NONE
845
+ when *%w[val_as_bool val_as_int val_as_str val_as_sym]
846
+ :REQUIRED
847
+ else # debug, exit, find, help, how, open, path, show_config, version
848
+ nil
849
+ end,
850
+
682
851
  # - long name
683
852
  if item[:long_name].present?
684
- "--#{item[:long_name]}#{item[:arg_name].present? ? " #{item[:arg_name]}" : ''}"
853
+ optname = "--#{item[:long_name]}"
854
+ "--#{item[:long_name]}" \
855
+ "#{item[:arg_name].present? ? " #{item[:arg_name]}" : ''}"
685
856
  end,
686
857
 
687
858
  # - short name
688
- item[:short_name].present? ? "-#{item[:short_name]}" : nil,
859
+ if item[:short_name].present?
860
+ "-#{item[:short_name]}" \
861
+ "#{item[:arg_name].present? ? " #{item[:arg_name]}" : ''}"
862
+ end,
689
863
 
690
864
  # - description and default
691
- [item[:description],
692
- ("[#{value_for_cli item[:default]}]" if item[:default].present?)].compact.join(' '),
865
+ [
866
+ item[:description],
867
+ ("[#{value_for_cli item[:default]}]" if item[:default].present?)
868
+ ].compact.join(' '),
869
+
870
+ # - type coercion
871
+ case item[:procname]&.to_s
872
+ when nil
873
+ nil
874
+ when 'val_as_bool'
875
+ item[:default] ? FalseClass : TrueClass # use sets to desired value
876
+ when 'val_as_int'
877
+ Integer
878
+ else # str, sym
879
+ # String
880
+ nil
881
+ end,
882
+ # Date – Anything accepted by Date.parse
883
+ # DateTime – Anything accepted by DateTime.parse
884
+ # Time – Anything accepted by Time.httpdate or Time.parse
885
+ # URI – Anything accepted by URI.parse
886
+ # Shellwords – Anything accepted by Shellwords.shellwords
887
+ # String – Any non-empty string
888
+ # Integer – Any integer. Will convert octal. (e.g. 124, -3, 040)
889
+ # Float – Any float. (e.g. 10, 3.14, -100E+13)
890
+ # Numeric – Any integer, float, or rational (1, 3.4, 1/3)
891
+ # DecimalInteger – Like Integer, but no octal format.
892
+ # OctalInteger – Like Integer, but no decimal format.
893
+ # DecimalNumeric – Decimal integer or float.
894
+ # TrueClass – Accepts ‘+, yes, true, -, no, false’ and defaults as true
895
+ # FalseClass – Same as TrueClass, but defaults to false
896
+ # Array – Strings separated by ‘,’ (e.g. 1,2,3)
897
+ # Regexp – Regular expressions. Also includes options.
693
898
 
694
899
  # apply proccode, if present, to value
695
900
  # save value to options hash if option is named
696
901
  #
697
902
  lambda { |value|
903
+ name = item[:long_name]&.present? ? '--' + item[:long_name].to_s : '-' + item[:short_name].to_s
904
+ options_parsed << item.merge(name: name, value: value)
698
905
  (item[:proccode] ? item[:proccode].call(value) : value).tap do |converted|
699
906
  options[item[:opt_name]] = converted if item[:opt_name]
700
907
  end
701
908
  }
702
- ].compact)
909
+ ].compact
910
+ opts.on(*switches)
703
911
  end
704
912
 
705
913
  def opts_prepare_file_list(options)
706
914
  list_files_specified(
707
915
  determine_filename(
708
- specified_filename: options[:filename]&.present? ? options[:filename] : nil,
916
+ specified_filename:
917
+ options[:filename]&.present? ? options[:filename] : nil,
709
918
  specified_folder: options[:path],
710
919
  default_filename: 'README.md',
711
920
  default_folder: '.'
@@ -724,12 +933,21 @@ module MarkdownExec
724
933
  public
725
934
 
726
935
  def run
727
- initialize_and_parse_cli_options
936
+ initialize_parse_execute_cli
728
937
  execute_block_with_error_handling
938
+ rescue BlockMissing
939
+ warn 'Block missing'
940
+ exit 1
941
+ rescue AppInterrupt, TTY::Reader::InputInterrupt, BlockMissing
942
+ warn 'Exiting...' if $DEBUG
943
+ exit 1
729
944
  rescue StandardError
730
945
  error_handler('run')
731
- ensure
732
- yield if block_given?
946
+ # rubocop:disable Style/RescueStandardError
947
+ rescue
948
+ warn 'Exiting...' if $DEBUG
949
+ exit 1
950
+ # rubocop:enable Style/RescueStandardError
733
951
  end
734
952
 
735
953
  private
@@ -741,7 +959,7 @@ module MarkdownExec
741
959
 
742
960
  saved_name_split filename
743
961
  @options[:save_executed_script] = false
744
- @options.document_inpseq
962
+ @options.vux_main_loop
745
963
  rescue StandardError
746
964
  error_handler('run_last_script')
747
965
  end
@@ -763,8 +981,11 @@ module MarkdownExec
763
981
 
764
982
  opts = options.dup
765
983
  select_option_or_exit(
766
- HashDelegator.new(@options).string_send_color(prompt,
767
- :prompt_color_after_script_execution),
984
+ HashDelegator.new(@options)
985
+ .string_send_color(
986
+ prompt,
987
+ :prompt_color_after_script_execution
988
+ ),
768
989
  files,
769
990
  opts.merge(per_page: opts[:select_page_height])
770
991
  )
@@ -777,6 +998,33 @@ module MarkdownExec
777
998
  )&.fetch(:selected)
778
999
  end
779
1000
 
1001
+ def simple_commands(options)
1002
+ {
1003
+ doc_glob: -> { @fout.fout options[:md_filename_glob] },
1004
+ history: -> { history },
1005
+ list_blocks: -> { list_blocks },
1006
+ list_default_env: -> { @fout.fout_list list_default_env },
1007
+ list_default_yaml: -> { @fout.fout_list list_default_yaml },
1008
+ list_docs: -> { @fout.fout_list opts_prepare_file_list(options) },
1009
+ list_recent_output: -> {
1010
+ @fout.fout_list list_recent_output(
1011
+ @options[:saved_stdout_folder],
1012
+ @options[:saved_stdout_glob], @options[:list_count]
1013
+ )
1014
+ },
1015
+ list_recent_scripts: -> {
1016
+ @fout.fout_list list_recent_scripts(
1017
+ options[:saved_script_folder],
1018
+ options[:saved_script_glob], options[:list_count]
1019
+ )
1020
+ },
1021
+ menu_export: -> { @fout.fout menu_export },
1022
+ pwd: -> { @fout.fout File.expand_path('..', __dir__) },
1023
+ run_last_script: -> { run_last_script },
1024
+ tab_completions: -> { @fout.fout tab_completions }
1025
+ }
1026
+ end
1027
+
780
1028
  public
781
1029
 
782
1030
  def tab_completions(data = menu_for_optparse)