markdown_exec 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/markdown_exec.rb CHANGED
@@ -3,38 +3,14 @@
3
3
 
4
4
  # encoding=utf-8
5
5
 
6
+ require 'English'
7
+ require 'clipboard'
6
8
  require 'open3'
7
9
  require 'optparse'
8
10
  require 'tty-prompt'
9
11
  require 'yaml'
10
12
 
11
- ##
12
- # default if nil
13
- # false if empty or '0'
14
- # else true
15
-
16
- def env_bool(name, default: false)
17
- return default if name.nil? || (val = ENV[name]).nil?
18
- return false if val.empty? || val == '0'
19
-
20
- true
21
- end
22
-
23
- def env_int(name, default: 0)
24
- return default if name.nil? || (val = ENV[name]).nil?
25
- return default if val.empty?
26
-
27
- val.to_i
28
- end
29
-
30
- def env_str(name, default: '')
31
- return default if name.nil? || (val = ENV[name]).nil?
32
-
33
- val || default
34
- end
35
-
36
- $pdebug = env_bool 'MDE_DEBUG'
37
-
13
+ require_relative 'shared'
38
14
  require_relative 'markdown_exec/version'
39
15
 
40
16
  $stderr.sync = true
@@ -62,24 +38,17 @@ end
62
38
 
63
39
  public
64
40
 
65
- # debug output
66
- #
67
- def tap_inspect(format: nil, name: 'return')
68
- return self unless $pdebug
69
-
70
- cvt = {
71
- json: :to_json,
72
- string: :to_s,
73
- yaml: :to_yaml,
74
- else: :inspect
75
- }
76
- fn = cvt.fetch(format, cvt[:else])
41
+ # display_level values
42
+ DISPLAY_LEVEL_BASE = 0 # required output
43
+ DISPLAY_LEVEL_ADMIN = 1
44
+ DISPLAY_LEVEL_DEBUG = 2
45
+ DISPLAY_LEVEL_DEFAULT = DISPLAY_LEVEL_ADMIN
46
+ DISPLAY_LEVEL_MAX = DISPLAY_LEVEL_DEBUG
77
47
 
78
- puts "-> #{caller[0].scan(/in `?(\S+)'$/)[0][0]}()" \
79
- " #{name}: #{method(fn).call}"
80
-
81
- self
82
- end
48
+ # @execute_files[ind] = @execute_files[ind] + [block]
49
+ EF_STDOUT = 0
50
+ EF_STDERR = 1
51
+ EF_STDIN = 2
83
52
 
84
53
  module MarkdownExec
85
54
  class Error < StandardError; end
@@ -98,15 +67,22 @@ module MarkdownExec
98
67
  # options necessary to start, parse input, defaults for cli options
99
68
 
100
69
  def base_options
101
- menu_data
102
- .map do |_long_name, _short_name, env_var, _arg_name, _description, opt_name, default, proc1| # rubocop:disable Metrics/ParameterLists
103
- next unless opt_name.present?
104
-
105
- value = env_str(env_var, default: value_for_hash(default))
106
- [opt_name, proc1 ? proc1.call(value) : value]
70
+ menu_iter do |item|
71
+ item.tap_inspect name: :item, format: :yaml
72
+ next unless item[:opt_name].present?
73
+
74
+ item_default = item[:default]
75
+ item_default.tap_inspect name: :item_default
76
+ value = if item_default.nil?
77
+ item_default
78
+ else
79
+ env_str(item[:env_var], default: value_for_hash(item_default))
80
+ end
81
+ [item[:opt_name], item[:proc1] ? item[:proc1].call(value) : value]
107
82
  end.compact.to_h.merge(
108
83
  {
109
84
  mdheadings: true, # use headings (levels 1,2,3) in block lable
85
+ menu_exit_at_top: true,
110
86
  menu_with_exit: true
111
87
  }
112
88
  ).tap_inspect format: :yaml
@@ -138,8 +114,35 @@ module MarkdownExec
138
114
  display_command(opts, required_blocks) if opts[:output_script] || opts[:user_must_approve]
139
115
 
140
116
  allow = true
141
- allow = @prompt.yes? opts[:prompt_approve_block] if opts[:user_must_approve]
142
- opts[:ir_approve] = allow
117
+ if opts[:user_must_approve]
118
+ loop do
119
+ # (sel = @prompt.select(opts[:prompt_approve_block], %w(Yes No Copy_script_to_clipboard Save_script), cycle: true)).tap_inspect name: :sel
120
+ (sel = @prompt.select(opts[:prompt_approve_block], filter: true) do |menu|
121
+ menu.default 1
122
+ # menu.enum '.'
123
+ # menu.filter true
124
+
125
+ menu.choice 'Yes', 1
126
+ menu.choice 'No', 2
127
+ menu.choice 'Copy script to clipboard', 3
128
+ menu.choice 'Save script', 4
129
+ end).tap_inspect name: :sel
130
+ allow = (sel == 1)
131
+ if sel == 3
132
+ text = required_blocks.flatten.join($INPUT_RECORD_SEPARATOR)
133
+ Clipboard.copy(text)
134
+ fout "Clipboard updated: #{required_blocks.count} blocks, #{required_blocks.flatten.count} lines, #{text.length} characters"
135
+ end
136
+ if sel == 4
137
+ # opts[:saved_script_filename] = saved_name_make(opts)
138
+ write_command_file(opts.merge(save_executed_script: true), required_blocks)
139
+ fout "File saved: #{@options[:saved_filespec]}"
140
+ end
141
+ break if [1, 2].include? sel
142
+ end
143
+ end
144
+ (opts[:ir_approve] = allow).tap_inspect name: :allow
145
+
143
146
  selected = get_block_by_name blocks_in_file, opts[:block_name]
144
147
 
145
148
  if opts[:ir_approve]
@@ -147,6 +150,7 @@ module MarkdownExec
147
150
  command_execute opts, required_blocks.flatten.join("\n")
148
151
  save_execution_output
149
152
  output_execution_summary
153
+ output_execution_result
150
154
  end
151
155
 
152
156
  selected[:name]
@@ -165,40 +169,45 @@ module MarkdownExec
165
169
  @execute_files = Hash.new([])
166
170
  @execute_options = opts
167
171
  @execute_started_at = Time.now.utc
168
- Open3.popen3(cmd2) do |stdin, stdout, stderr|
169
- stdin.close_write
170
- begin
171
- files = [stdout, stderr]
172
-
173
- until all_at_eof(files)
174
- ready = IO.select(files)
175
-
176
- next unless ready
177
-
178
- # readable = ready[0]
179
- # # writable = ready[1]
180
- # # exceptions = ready[2]
181
- ready.each.with_index do |readable, ind|
182
- readable.each do |f|
183
- block = f.read_nonblock(BLOCK_SIZE)
184
- @execute_files[ind] = @execute_files[ind] + [block]
185
- print block if opts[:output_stdout]
186
- rescue EOFError #=> e
187
- # do nothing at EOF
188
- end
189
- end
172
+
173
+ Open3.popen3(@options[:shell], '-c', cmd2) do |stdin, stdout, stderr, exec_thr|
174
+ # pid = exec_thr.pid # pid of the started process
175
+
176
+ t1 = Thread.new do
177
+ until (line = stdout.gets).nil?
178
+ @execute_files[EF_STDOUT] = @execute_files[EF_STDOUT] + [line]
179
+ print line if opts[:output_stdout]
180
+ yield nil, line, nil, exec_thr if block_given?
190
181
  end
191
- rescue IOError => e
192
- fout "IOError: #{e}"
193
182
  end
194
- @execute_completed_at = Time.now.utc
183
+
184
+ t2 = Thread.new do
185
+ until (line = stderr.gets).nil?
186
+ @execute_files[EF_STDERR] = @execute_files[EF_STDERR] + [line]
187
+ print line if opts[:output_stdout]
188
+ yield nil, nil, line, exec_thr if block_given?
189
+ end
190
+ end
191
+
192
+ in_thr = Thread.new do
193
+ while exec_thr.alive? # reading input until the child process ends
194
+ stdin.puts(line = $stdin.gets)
195
+ @execute_files[EF_STDIN] = @execute_files[EF_STDIN] + [line]
196
+ yield line, nil, nil, exec_thr if block_given?
197
+ end
198
+ end
199
+
200
+ exec_thr.join
201
+ in_thr.kill
202
+ # @return_code = exec_thr.value
195
203
  end
204
+ @execute_completed_at = Time.now.utc
196
205
  rescue Errno::ENOENT => e
197
206
  # error triggered by missing command in script
198
207
  @execute_aborted_at = Time.now.utc
199
208
  @execute_error_message = e.message
200
209
  @execute_error = e
201
- @execute_files[1] = e.message
210
+ @execute_files[EF_STDERR] += [e.message]
202
211
  fout "Error ENOENT: #{e.inspect}"
203
212
  end
204
213
 
@@ -212,7 +221,9 @@ module MarkdownExec
212
221
  end
213
222
 
214
223
  def display_command(_opts, required_blocks)
224
+ fout ' #=#=#'.yellow
215
225
  required_blocks.each { |cb| fout cb }
226
+ fout ' #=#=#'.yellow
216
227
  end
217
228
 
218
229
  def exec_block(options, _block_name = '')
@@ -239,7 +250,8 @@ module MarkdownExec
239
250
  run_last_script: -> { run_last_script },
240
251
  select_recent_output: -> { select_recent_output },
241
252
  select_recent_script: -> { select_recent_script },
242
- tab_completions: -> { fout tab_completions }
253
+ tab_completions: -> { fout tab_completions },
254
+ menu_export: -> { fout menu_export }
243
255
  }
244
256
  simple_commands.each_key do |key|
245
257
  if @options[key]
@@ -292,6 +304,19 @@ module MarkdownExec
292
304
  end
293
305
  end
294
306
 
307
+ def approved_fout?(level)
308
+ level <= @options[:display_level]
309
+ end
310
+
311
+ # display output at level or lower than filter (DISPLAY_LEVEL_DEFAULT)
312
+ #
313
+ def lout(str, level: DISPLAY_LEVEL_BASE)
314
+ return unless approved_fout? level
315
+
316
+ # fout level == DISPLAY_LEVEL_BASE ? str : DISPLAY_LEVEL_XBASE_PREFIX + str
317
+ fout level == DISPLAY_LEVEL_BASE ? str : @options[:display_level_xbase_prefix] + str
318
+ end
319
+
295
320
  def list_blocks_in_file(call_options = {}, &options_block)
296
321
  opts = optsmerge call_options, options_block
297
322
 
@@ -360,25 +385,23 @@ module MarkdownExec
360
385
  end
361
386
 
362
387
  def list_default_env
363
- menu_data
364
- .map do |_long_name, _short_name, env_var, _arg_name, description, _opt_name, default, _proc1| # rubocop:disable Metrics/ParameterLists
365
- next unless env_var.present?
388
+ menu_iter do |item|
389
+ next unless item[:env_var].present?
366
390
 
367
391
  [
368
- "#{env_var}=#{value_for_cli default}",
369
- description.present? ? description : nil
392
+ "#{item[:env_var]}=#{value_for_cli item[:default]}",
393
+ item[:description].present? ? item[:description] : nil
370
394
  ].compact.join(' # ')
371
395
  end.compact.sort
372
396
  end
373
397
 
374
398
  def list_default_yaml
375
- menu_data
376
- .map do |_long_name, _short_name, _env_var, _arg_name, description, opt_name, default, _proc1| # rubocop:disable Metrics/ParameterLists
377
- next unless opt_name.present? && default.present?
399
+ menu_iter do |item|
400
+ next unless item[:opt_name].present? && item[:default].present?
378
401
 
379
402
  [
380
- "#{opt_name}: #{value_for_yaml default}",
381
- description.present? ? description : nil
403
+ "#{item[:opt_name]}: #{value_for_yaml item[:default]}",
404
+ item[:description].present? ? item[:description] : nil
382
405
  ].compact.join(' # ')
383
406
  end.compact.sort
384
407
  end
@@ -487,83 +510,400 @@ module MarkdownExec
487
510
  end.compact.tap_inspect
488
511
  end
489
512
 
490
- def menu_data
513
+ def menu_data1
491
514
  val_as_bool = ->(value) { value.class.to_s == 'String' ? (value.chomp != '0') : value }
492
515
  val_as_int = ->(value) { value.to_i }
493
516
  val_as_str = ->(value) { value.to_s }
494
-
495
- summary_head = [
496
- ['config', nil, nil, 'PATH', 'Read configuration file', nil, '.', lambda { |value|
497
- read_configuration_file! options, value
498
- }],
499
- ['debug', 'd', 'MDE_DEBUG', 'BOOL', 'Debug output', nil, false, ->(value) { $pdebug = value.to_i != 0 }]
500
- ]
501
-
502
- # rubocop:disable Layout/LineLength
503
- summary_body = [
504
- ['block-name', 'f', 'MDE_BLOCK_NAME', 'RELATIVE', 'Name of block', :block_name, nil, val_as_str],
505
- ['filename', 'f', 'MDE_FILENAME', 'RELATIVE', 'Name of document', :filename, nil, val_as_str],
506
- ['list-blocks', nil, nil, nil, 'List blocks', :list_blocks, false, val_as_bool],
507
- ['list-count', nil, 'MDE_LIST_COUNT', 'NUM', 'Max. items to return in list', :list_count, 16, val_as_int],
508
- ['list-default-env', nil, nil, nil, 'List default configuration as environment variables', :list_default_env, false, val_as_bool],
509
- ['list-default-yaml', nil, nil, nil, 'List default configuration as YAML', :list_default_yaml, false, val_as_bool],
510
- ['list-docs', nil, nil, nil, 'List docs in current folder', :list_docs, false, val_as_bool],
511
- ['list-recent-output', nil, nil, nil, 'List recent saved output', :list_recent_output, false, val_as_bool],
512
- ['list-recent-scripts', nil, nil, nil, 'List recent saved scripts', :list_recent_scripts, false, val_as_bool],
513
- ['logged-stdout-filename-prefix', nil, 'MDE_LOGGED_STDOUT_FILENAME_PREFIX', 'NAME', 'Name prefix for stdout files', :logged_stdout_filename_prefix, 'mde', val_as_str],
514
- ['output-execution-summary', nil, 'MDE_OUTPUT_EXECUTION_SUMMARY', 'BOOL', 'Display summary for execution', :output_execution_summary, false, val_as_bool],
515
- ['output-script', nil, 'MDE_OUTPUT_SCRIPT', 'BOOL', 'Display script prior to execution', :output_script, false, val_as_bool],
516
- ['output-stdout', nil, 'MDE_OUTPUT_STDOUT', 'BOOL', 'Display standard output from execution', :output_stdout, true, val_as_bool],
517
- ['path', 'p', 'MDE_PATH', 'PATH', 'Path to documents', :path, nil, val_as_str],
518
- ['pwd', nil, nil, nil, 'Gem home folder', :pwd, false, val_as_bool],
519
- ['run-last-script', nil, nil, nil, 'Run most recently saved script', :run_last_script, false, val_as_bool],
520
- ['save-executed-script', nil, 'MDE_SAVE_EXECUTED_SCRIPT', 'BOOL', 'Save executed script', :save_executed_script, false, val_as_bool],
521
- ['save-execution-output', nil, 'MDE_SAVE_EXECUTION_OUTPUT', 'BOOL', 'Save standard output of the executed script', :save_execution_output, false, val_as_bool],
522
- ['saved-script-filename-prefix', nil, 'MDE_SAVED_SCRIPT_FILENAME_PREFIX', 'NAME', 'Name prefix for saved scripts', :saved_script_filename_prefix, 'mde', val_as_str],
523
- ['saved-script-folder', nil, 'MDE_SAVED_SCRIPT_FOLDER', 'SPEC', 'Saved script folder', :saved_script_folder, 'logs', val_as_str],
524
- ['saved-script-glob', nil, 'MDE_SAVED_SCRIPT_GLOB', 'SPEC', 'Glob matching saved scripts', :saved_script_glob, 'mde_*.sh', val_as_str],
525
- ['saved-stdout-folder', nil, 'MDE_SAVED_STDOUT_FOLDER', 'SPEC', 'Saved stdout folder', :saved_stdout_folder, 'logs', val_as_str],
526
- ['saved-stdout-glob', nil, 'MDE_SAVED_STDOUT_GLOB', 'SPEC', 'Glob matching saved outputs', :saved_stdout_glob, 'mde_*.out.txt', val_as_str],
527
- ['select-recent-output', nil, nil, nil, 'Select and execute a recently saved output', :select_recent_output, false, val_as_bool],
528
- ['select-recent-script', nil, nil, nil, 'Select and execute a recently saved script', :select_recent_script, false, val_as_bool],
529
- ['tab-completions', nil, nil, nil, 'List tab completions', :tab_completions, false, val_as_bool],
530
- ['user-must-approve', nil, 'MDE_USER_MUST_APPROVE', 'BOOL', 'Pause for user to approve script', :user_must_approve, true, val_as_bool]
531
- ]
532
- # rubocop:enable Layout/LineLength
533
-
534
- # rubocop:disable Style/Semicolon
535
- summary_tail = [
536
- [nil, '0', nil, nil, 'Show current configuration values',
537
- nil, nil, ->(_) { options_finalize options; fout sorted_keys(options).to_yaml }],
538
- ['help', 'h', nil, nil, 'App help',
539
- nil, nil, ->(_) { fout menu_help; exit }],
540
- ['version', 'v', nil, nil, "Print the gem's version",
541
- nil, nil, ->(_) { fout MarkdownExec::VERSION; exit }],
542
- ['exit', 'x', nil, nil, 'Exit app',
543
- nil, nil, ->(_) { exit }]
544
- ]
545
- # rubocop:enable Style/Semicolon
546
-
547
- env_vars = [
548
- [nil, nil, 'MDE_BLOCK_NAME_EXCLUDED_MATCH', nil, 'Pattern for blocks to hide from user-selection',
549
- :block_name_excluded_match, '^\(.*\)$', val_as_str],
550
- [nil, nil, 'MDE_BLOCK_NAME_MATCH', nil, '', :block_name_match, ':(?<title>\S+)( |$)', val_as_str],
551
- [nil, nil, 'MDE_BLOCK_REQUIRED_SCAN', nil, '', :block_required_scan, '\+\S+', val_as_str],
552
- [nil, nil, 'MDE_FENCED_START_AND_END_MATCH', nil, '', :fenced_start_and_end_match, '^`{3,}', val_as_str],
553
- [nil, nil, 'MDE_FENCED_START_EX_MATCH', nil, '', :fenced_start_ex_match,
554
- '^`{3,}(?<shell>[^`\s]*) *(?<name>.*)$', val_as_str],
555
- [nil, nil, 'MDE_HEADING1_MATCH', nil, '', :heading1_match, '^# *(?<name>[^#]*?) *$', val_as_str],
556
- [nil, nil, 'MDE_HEADING2_MATCH', nil, '', :heading2_match, '^## *(?<name>[^#]*?) *$', val_as_str],
557
- [nil, nil, 'MDE_HEADING3_MATCH', nil, '', :heading3_match, '^### *(?<name>.+?) *$', val_as_str],
558
- [nil, nil, 'MDE_MD_FILENAME_GLOB', nil, '', :md_filename_glob, '*.[Mm][Dd]', val_as_str],
559
- [nil, nil, 'MDE_MD_FILENAME_MATCH', nil, '', :md_filename_match, '.+\\.md', val_as_str],
560
- [nil, nil, 'MDE_OUTPUT_VIEWER_OPTIONS', nil, 'Options for viewing saved output file', :output_viewer_options,
561
- '', val_as_str],
562
- [nil, nil, 'MDE_SELECT_PAGE_HEIGHT', nil, '', :select_page_height, 12, val_as_int]
563
- # [nil, nil, 'MDE_', nil, '', nil, '', nil],
517
+ # val_true = ->(_value) { true } # for commands, sets option to true
518
+ set1 = [
519
+ {
520
+ arg_name: 'PATH',
521
+ default: '.',
522
+ description: 'Read configuration file',
523
+ long_name: 'config',
524
+ proc1: lambda { |value|
525
+ read_configuration_file! options, value
526
+ }
527
+ },
528
+ {
529
+ arg_name: 'BOOL',
530
+ default: false,
531
+ description: 'Debug output',
532
+ env_var: 'MDE_DEBUG',
533
+ long_name: 'debug',
534
+ short_name: 'd',
535
+ proc1: lambda { |value|
536
+ $pdebug = value.to_i != 0
537
+ }
538
+ },
539
+ {
540
+ arg_name: "INT.#{DISPLAY_LEVEL_BASE}-#{DISPLAY_LEVEL_MAX}",
541
+ default: DISPLAY_LEVEL_DEFAULT,
542
+ description: "Output display level (#{DISPLAY_LEVEL_BASE} to #{DISPLAY_LEVEL_MAX})",
543
+ env_var: 'MDE_DISPLAY_LEVEL',
544
+ long_name: 'display-level',
545
+ opt_name: :display_level,
546
+ proc1: val_as_int
547
+ },
548
+ {
549
+ arg_name: 'NAME',
550
+ compreply: false,
551
+ description: 'Name of block',
552
+ env_var: 'MDE_BLOCK_NAME',
553
+ long_name: 'block-name',
554
+ opt_name: :block_name,
555
+ short_name: 'f',
556
+ proc1: val_as_str
557
+ },
558
+ {
559
+ arg_name: 'RELATIVE_PATH',
560
+ compreply: '.',
561
+ description: 'Name of document',
562
+ env_var: 'MDE_FILENAME',
563
+ long_name: 'filename',
564
+ opt_name: :filename,
565
+ short_name: 'f',
566
+ proc1: val_as_str
567
+ },
568
+ {
569
+ description: 'List blocks',
570
+ long_name: 'list-blocks',
571
+ opt_name: :list_blocks,
572
+ proc1: val_as_bool
573
+ },
574
+ {
575
+ arg_name: 'INT.1-',
576
+ default: 32,
577
+ description: 'Max. items to return in list',
578
+ env_var: 'MDE_LIST_COUNT',
579
+ long_name: 'list-count',
580
+ opt_name: :list_count,
581
+ proc1: val_as_int
582
+ },
583
+ {
584
+ description: 'List default configuration as environment variables',
585
+ long_name: 'list-default-env',
586
+ opt_name: :list_default_env
587
+ },
588
+ {
589
+ description: 'List default configuration as YAML',
590
+ long_name: 'list-default-yaml',
591
+ opt_name: :list_default_yaml
592
+ },
593
+ {
594
+ description: 'List docs in current folder',
595
+ long_name: 'list-docs',
596
+ opt_name: :list_docs,
597
+ proc1: val_as_bool
598
+ },
599
+ {
600
+ description: 'List recent saved output',
601
+ long_name: 'list-recent-output',
602
+ opt_name: :list_recent_output,
603
+ proc1: val_as_bool
604
+ },
605
+ {
606
+ description: 'List recent saved scripts',
607
+ long_name: 'list-recent-scripts',
608
+ opt_name: :list_recent_scripts,
609
+ proc1: val_as_bool
610
+ },
611
+ {
612
+ arg_name: 'PREFIX',
613
+ default: MarkdownExec::BIN_NAME,
614
+ description: 'Name prefix for stdout files',
615
+ env_var: 'MDE_LOGGED_STDOUT_FILENAME_PREFIX',
616
+ long_name: 'logged-stdout-filename-prefix',
617
+ opt_name: :logged_stdout_filename_prefix,
618
+ proc1: val_as_str
619
+ },
620
+ {
621
+ arg_name: 'BOOL',
622
+ default: false,
623
+ description: 'Display summary for execution',
624
+ env_var: 'MDE_OUTPUT_EXECUTION_SUMMARY',
625
+ long_name: 'output-execution-summary',
626
+ opt_name: :output_execution_summary,
627
+ proc1: val_as_bool
628
+ },
629
+ {
630
+ arg_name: 'BOOL',
631
+ default: false,
632
+ description: 'Display script prior to execution',
633
+ env_var: 'MDE_OUTPUT_SCRIPT',
634
+ long_name: 'output-script',
635
+ opt_name: :output_script,
636
+ proc1: val_as_bool
637
+ },
638
+ {
639
+ arg_name: 'BOOL',
640
+ default: true,
641
+ description: 'Display standard output from execution',
642
+ env_var: 'MDE_OUTPUT_STDOUT',
643
+ long_name: 'output-stdout',
644
+ opt_name: :output_stdout,
645
+ proc1: val_as_bool
646
+ },
647
+ {
648
+ arg_name: 'RELATIVE_PATH',
649
+ default: '.',
650
+ description: 'Path to documents',
651
+ env_var: 'MDE_PATH',
652
+ long_name: 'path',
653
+ opt_name: :path,
654
+ short_name: 'p',
655
+ proc1: val_as_str
656
+ },
657
+ {
658
+ description: 'Gem home folder',
659
+ long_name: 'pwd',
660
+ opt_name: :pwd,
661
+ proc1: val_as_bool
662
+ },
663
+ {
664
+ description: 'Run most recently saved script',
665
+ long_name: 'run-last-script',
666
+ opt_name: :run_last_script,
667
+ proc1: val_as_bool
668
+ },
669
+ {
670
+ arg_name: 'BOOL',
671
+ default: false,
672
+ description: 'Save executed script',
673
+ env_var: 'MDE_SAVE_EXECUTED_SCRIPT',
674
+ long_name: 'save-executed-script',
675
+ opt_name: :save_executed_script,
676
+ proc1: val_as_bool
677
+ },
678
+ {
679
+ arg_name: 'BOOL',
680
+ default: false,
681
+ description: 'Save standard output of the executed script',
682
+ env_var: 'MDE_SAVE_EXECUTION_OUTPUT',
683
+ long_name: 'save-execution-output',
684
+ opt_name: :save_execution_output,
685
+ proc1: val_as_bool
686
+ },
687
+ {
688
+ arg_name: 'INT',
689
+ default: 0o755,
690
+ description: 'chmod for saved scripts',
691
+ env_var: 'MDE_SAVED_SCRIPT_CHMOD',
692
+ long_name: 'saved-script-chmod',
693
+ opt_name: :saved_script_chmod,
694
+ proc1: val_as_int
695
+ },
696
+ {
697
+ arg_name: 'PREFIX',
698
+ default: MarkdownExec::BIN_NAME,
699
+ description: 'Name prefix for saved scripts',
700
+ env_var: 'MDE_SAVED_SCRIPT_FILENAME_PREFIX',
701
+ long_name: 'saved-script-filename-prefix',
702
+ opt_name: :saved_script_filename_prefix,
703
+ proc1: val_as_str
704
+ },
705
+ {
706
+ arg_name: 'RELATIVE_PATH',
707
+ default: 'logs',
708
+ description: 'Saved script folder',
709
+ env_var: 'MDE_SAVED_SCRIPT_FOLDER',
710
+ long_name: 'saved-script-folder',
711
+ opt_name: :saved_script_folder,
712
+ proc1: val_as_str
713
+ },
714
+ {
715
+ arg_name: 'GLOB',
716
+ default: 'mde_*.sh',
717
+ description: 'Glob matching saved scripts',
718
+ env_var: 'MDE_SAVED_SCRIPT_GLOB',
719
+ long_name: 'saved-script-glob',
720
+ opt_name: :saved_script_glob,
721
+ proc1: val_as_str
722
+ },
723
+ {
724
+ arg_name: 'RELATIVE_PATH',
725
+ default: 'logs',
726
+ description: 'Saved stdout folder',
727
+ env_var: 'MDE_SAVED_STDOUT_FOLDER',
728
+ long_name: 'saved-stdout-folder',
729
+ opt_name: :saved_stdout_folder,
730
+ proc1: val_as_str
731
+ },
732
+ {
733
+ arg_name: 'GLOB',
734
+ default: 'mde_*.out.txt',
735
+ description: 'Glob matching saved outputs',
736
+ env_var: 'MDE_SAVED_STDOUT_GLOB',
737
+ long_name: 'saved-stdout-glob',
738
+ opt_name: :saved_stdout_glob,
739
+ proc1: val_as_str
740
+ },
741
+ {
742
+ description: 'Select and execute a recently saved output',
743
+ long_name: 'select-recent-output',
744
+ opt_name: :select_recent_output,
745
+ proc1: val_as_bool
746
+ },
747
+ {
748
+ description: 'Select and execute a recently saved script',
749
+ long_name: 'select-recent-script',
750
+ opt_name: :select_recent_script,
751
+ proc1: val_as_bool
752
+ },
753
+ {
754
+ description: 'YAML export of menu',
755
+ long_name: 'menu-export',
756
+ opt_name: :menu_export,
757
+ proc1: val_as_bool
758
+ },
759
+ {
760
+ description: 'List tab completions',
761
+ long_name: 'tab-completions',
762
+ opt_name: :tab_completions,
763
+ proc1: val_as_bool
764
+ },
765
+ {
766
+ arg_name: 'BOOL',
767
+ default: true,
768
+ description: 'Pause for user to approve script',
769
+ env_var: 'MDE_USER_MUST_APPROVE',
770
+ long_name: 'user-must-approve',
771
+ opt_name: :user_must_approve,
772
+ proc1: val_as_bool
773
+ },
774
+ {
775
+ description: 'Show current configuration values',
776
+ short_name: '0',
777
+ proc1: lambda { |_|
778
+ options_finalize options
779
+ fout sorted_keys(options).to_yaml
780
+ }
781
+ },
782
+ {
783
+ description: 'App help',
784
+ long_name: 'help',
785
+ short_name: 'h',
786
+ proc1: lambda { |_|
787
+ fout menu_help
788
+ exit
789
+ }
790
+ },
791
+ {
792
+ description: "Print the gem's version",
793
+ long_name: 'version',
794
+ short_name: 'v',
795
+ proc1: lambda { |_|
796
+ fout MarkdownExec::VERSION
797
+ exit
798
+ }
799
+ },
800
+ {
801
+ description: 'Exit app',
802
+ long_name: 'exit',
803
+ short_name: 'x',
804
+ proc1: ->(_) { exit }
805
+ },
806
+ {
807
+ default: '^\(.*\)$',
808
+ description: 'Pattern for blocks to hide from user-selection',
809
+ env_var: 'MDE_BLOCK_NAME_EXCLUDED_MATCH',
810
+ opt_name: :block_name_excluded_match,
811
+ proc1: val_as_str
812
+ },
813
+ {
814
+ default: ':(?<title>\S+)( |$)',
815
+ env_var: 'MDE_BLOCK_NAME_MATCH',
816
+ opt_name: :block_name_match,
817
+ proc1: val_as_str
818
+ },
819
+ {
820
+ default: '\+\S+',
821
+ env_var: 'MDE_BLOCK_REQUIRED_SCAN',
822
+ opt_name: :block_required_scan,
823
+ proc1: val_as_str
824
+ },
825
+ {
826
+ default: '> ',
827
+ env_var: 'MDE_DISPLAY_LEVEL_XBASE_PREFIX',
828
+ opt_name: :display_level_xbase_prefix,
829
+ proc1: val_as_str
830
+ },
831
+ {
832
+ default: '^`{3,}',
833
+ env_var: 'MDE_FENCED_START_AND_END_MATCH',
834
+ opt_name: :fenced_start_and_end_match,
835
+ proc1: val_as_str
836
+ },
837
+ {
838
+ default: '^`{3,}(?<shell>[^`\s]*) *(?<name>.*)$',
839
+ env_var: 'MDE_FENCED_START_EX_MATCH',
840
+ opt_name: :fenced_start_ex_match,
841
+ proc1: val_as_str
842
+ },
843
+ {
844
+ default: '^# *(?<name>[^#]*?) *$',
845
+ env_var: 'MDE_HEADING1_MATCH',
846
+ opt_name: :heading1_match,
847
+ proc1: val_as_str
848
+ },
849
+ {
850
+ default: '^## *(?<name>[^#]*?) *$',
851
+ env_var: 'MDE_HEADING2_MATCH',
852
+ opt_name: :heading2_match,
853
+ proc1: val_as_str
854
+ },
855
+ {
856
+ default: '^### *(?<name>.+?) *$',
857
+ env_var: 'MDE_HEADING3_MATCH',
858
+ opt_name: :heading3_match,
859
+ proc1: val_as_str
860
+ },
861
+ {
862
+ default: '*.[Mm][Dd]',
863
+ env_var: 'MDE_MD_FILENAME_GLOB',
864
+ opt_name: :md_filename_glob,
865
+ proc1: val_as_str
866
+ },
867
+ {
868
+ default: '.+\\.md',
869
+ env_var: 'MDE_MD_FILENAME_MATCH',
870
+ opt_name: :md_filename_match,
871
+ proc1: val_as_str
872
+ },
873
+ {
874
+ description: 'Options for viewing saved output file',
875
+ env_var: 'MDE_OUTPUT_VIEWER_OPTIONS',
876
+ opt_name: :output_viewer_options,
877
+ proc1: val_as_str
878
+ },
879
+ {
880
+ default: 24,
881
+ description: 'Maximum # of rows in select list',
882
+ env_var: 'MDE_SELECT_PAGE_HEIGHT',
883
+ opt_name: :select_page_height,
884
+ proc1: val_as_int
885
+ },
886
+ {
887
+ default: '#!/usr/bin/env',
888
+ description: 'Shebang for saved scripts',
889
+ env_var: 'MDE_SHEBANG',
890
+ opt_name: :shebang,
891
+ proc1: val_as_str
892
+ },
893
+ {
894
+ default: 'bash',
895
+ description: 'Shell for launched scripts',
896
+ env_var: 'MDE_SHELL',
897
+ opt_name: :shell,
898
+ proc1: val_as_str
899
+ }
564
900
  ]
901
+ # commands first, options second
902
+ (set1.reject { |v1| v1[:arg_name] }) + (set1.select { |v1| v1[:arg_name] })
903
+ end
565
904
 
566
- summary_head + summary_body + summary_tail + env_vars
905
+ def menu_iter(data = menu_data1, &block)
906
+ data.map(&block)
567
907
  end
568
908
 
569
909
  def menu_help
@@ -609,6 +949,24 @@ module MarkdownExec
609
949
  end.tap_inspect
610
950
  end
611
951
 
952
+ def output_execution_result
953
+ oq = [['Block', @options[:block_name], DISPLAY_LEVEL_ADMIN],
954
+ ['Command',
955
+ [MarkdownExec::BIN_NAME,
956
+ @options[:filename],
957
+ @options[:block_name]].join(' '),
958
+ DISPLAY_LEVEL_ADMIN]]
959
+
960
+ [['Script', :saved_filespec],
961
+ ['StdOut', :logged_stdout_filespec]].each do |label, name|
962
+ oq << [label, @options[name], DISPLAY_LEVEL_ADMIN] if @options[name]
963
+ end
964
+
965
+ oq.map do |label, value, level|
966
+ lout ["#{label}:".yellow, value.to_s].join(' '), level: level
967
+ end
968
+ end
969
+
612
970
  def output_execution_summary
613
971
  return unless @options[:output_execution_summary]
614
972
 
@@ -626,9 +984,12 @@ module MarkdownExec
626
984
 
627
985
  def prompt_with_quit(prompt_text, items, opts = {})
628
986
  exit_option = '* Exit'
629
- sel = @prompt.select prompt_text,
630
- items + (@options[:menu_with_exit] ? [exit_option] : []),
631
- opts
987
+ all_items = if @options[:menu_exit_at_top]
988
+ (@options[:menu_with_exit] ? [exit_option] : []) + items
989
+ else
990
+ items + (@options[:menu_with_exit] ? [exit_option] : [])
991
+ end
992
+ sel = @prompt.select(prompt_text, all_items, opts.merge(filter: true))
632
993
  sel == exit_option ? nil : sel
633
994
  end
634
995
 
@@ -675,19 +1036,19 @@ module MarkdownExec
675
1036
  "Usage: #{executable_name} [(path | filename [block_name])] [options]"
676
1037
  ].join("\n")
677
1038
 
678
- menu_data
679
- .map do |long_name, short_name, _env_var, arg_name, description, opt_name, default, proc1| # rubocop:disable Metrics/ParameterLists
680
- next unless long_name.present? || short_name.present?
1039
+ menu_iter do |item|
1040
+ next unless item[:long_name].present? || item[:short_name].present?
681
1041
 
682
- opts.on(*[if long_name.present?
683
- "--#{long_name}#{arg_name.present? ? " #{arg_name}" : ''}"
1042
+ opts.on(*[if item[:long_name].present?
1043
+ "--#{item[:long_name]}#{item[:arg_name].present? ? " #{item[:arg_name]}" : ''}"
684
1044
  end,
685
- short_name.present? ? "-#{short_name}" : nil,
686
- [description,
687
- default.present? ? "[#{value_for_cli default}]" : nil].compact.join(' '),
1045
+ item[:short_name].present? ? "-#{item[:short_name]}" : nil,
1046
+ [item[:description],
1047
+ item[:default].present? ? "[#{value_for_cli item[:default]}]" : nil].compact.join(' '),
688
1048
  lambda { |value|
689
- ret = proc1.call(value)
690
- options[opt_name] = ret if opt_name
1049
+ # ret = item[:proc1].call(value)
1050
+ ret = item[:proc1] ? item[:proc1].call(value) : value
1051
+ options[item[:opt_name]] = ret if item[:opt_name]
691
1052
  ret
692
1053
  }].compact)
693
1054
  end
@@ -702,7 +1063,7 @@ module MarkdownExec
702
1063
  end
703
1064
 
704
1065
  FNR11 = '/'
705
- FNR12 = ',;'
1066
+ FNR12 = ',~'
706
1067
 
707
1068
  def saved_name_make(opts)
708
1069
  fne = opts[:filename].gsub(FNR11, FNR12)
@@ -741,28 +1102,51 @@ module MarkdownExec
741
1102
  @logged_stdout_filespec = @options[:logged_stdout_filespec]
742
1103
  dirname = File.dirname(@options[:logged_stdout_filespec])
743
1104
  Dir.mkdir dirname unless File.exist?(dirname)
744
- File.write(@options[:logged_stdout_filespec], @execute_files&.fetch(0, ''))
1105
+
1106
+ # File.write(@options[:logged_stdout_filespec], @execute_files&.fetch(EF_STDOUT, ''))
1107
+ ol = ["-STDOUT-\n"]
1108
+ ol += @execute_files&.fetch(EF_STDOUT, [])
1109
+ ol += ["-STDERR-\n"].tap_inspect name: :ol3
1110
+ ol += @execute_files&.fetch(EF_STDERR, [])
1111
+ ol += ["-STDIN-\n"]
1112
+ ol += @execute_files&.fetch(EF_STDIN, [])
1113
+ File.write(@options[:logged_stdout_filespec], ol.join)
745
1114
  end
746
1115
 
747
1116
  def select_and_approve_block(call_options = {}, &options_block)
748
1117
  opts = optsmerge call_options, options_block
749
1118
  blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
750
1119
 
751
- unless opts[:block_name].present?
752
- pt = (opts[:prompt_select_block]).to_s
753
- blocks_in_file.each { |block| block.merge! label: make_block_label(block, opts) }
754
- block_labels = option_exclude_blocks(opts, blocks_in_file).map { |block| block[:label] }
1120
+ loop1 = true && !opts[:block_name].present?
755
1121
 
756
- return nil if block_labels.count.zero?
1122
+ loop do
1123
+ unless opts[:block_name].present?
1124
+ pt = (opts[:prompt_select_block]).to_s
1125
+ blocks_in_file.each { |block| block.merge! label: make_block_label(block, opts) }
1126
+ block_labels = option_exclude_blocks(opts, blocks_in_file).map { |block| block[:label] }
757
1127
 
758
- sel = prompt_with_quit pt, block_labels, per_page: opts[:select_page_height]
759
- return nil if sel.nil?
1128
+ return nil if block_labels.count.zero?
760
1129
 
761
- label_block = blocks_in_file.select { |block| block[:label] == sel }.fetch(0, nil)
762
- opts[:block_name] = @options[:block_name] = label_block[:name]
763
- end
1130
+ sel = prompt_with_quit pt, block_labels, per_page: opts[:select_page_height]
1131
+ return nil if sel.nil?
764
1132
 
765
- approve_block opts, blocks_in_file
1133
+ # if sel.nil?
1134
+ # loop1 = false
1135
+ # break
1136
+ # end
1137
+
1138
+ label_block = blocks_in_file.select { |block| block[:label] == sel }.fetch(0, nil)
1139
+ opts[:block_name] = @options[:block_name] = label_block[:name]
1140
+
1141
+ end
1142
+ # if loop1
1143
+ approve_block opts, blocks_in_file
1144
+ # end
1145
+
1146
+ break unless loop1
1147
+
1148
+ opts[:block_name] = ''
1149
+ end
766
1150
  end
767
1151
 
768
1152
  def select_md_file(files_ = nil)
@@ -804,9 +1188,16 @@ module MarkdownExec
804
1188
  { headings: headings, name: title, title: title }
805
1189
  end
806
1190
 
807
- def tab_completions(data = menu_data)
1191
+ def menu_export(data = menu_data1)
1192
+ data.map do |item|
1193
+ item.delete(:proc1)
1194
+ item
1195
+ end.to_yaml
1196
+ end
1197
+
1198
+ def tab_completions(data = menu_data1)
808
1199
  data.map do |item|
809
- "--#{item[0]}" if item[0]
1200
+ "--#{item[:long_name]}" if item[:long_name]
810
1201
  end.compact
811
1202
  end
812
1203
 
@@ -819,19 +1210,6 @@ module MarkdownExec
819
1210
  @options.tap_inspect format: :yaml
820
1211
  end
821
1212
 
822
- def value_for_cli(value)
823
- case value.class.to_s
824
- when 'String'
825
- "'#{value}'"
826
- when 'FalseClass', 'TrueClass'
827
- value ? '1' : '0'
828
- when 'Integer'
829
- value
830
- else
831
- value.to_s
832
- end
833
- end
834
-
835
1213
  def value_for_hash(value, default = nil)
836
1214
  return default if value.nil?
837
1215
 
@@ -872,11 +1250,24 @@ module MarkdownExec
872
1250
 
873
1251
  dirname = File.dirname(@options[:saved_filespec])
874
1252
  Dir.mkdir dirname unless File.exist?(dirname)
875
- File.write(@options[:saved_filespec], "#!/usr/bin/env bash\n" \
1253
+ (shebang = if @options[:shebang]&.present?
1254
+ "#{@options[:shebang]} #{@options[:shell]}\n"
1255
+ else
1256
+ ''
1257
+ end
1258
+ ).tap_inspect name: :shebang
1259
+ File.write(@options[:saved_filespec], shebang +
876
1260
  "# file_name: #{opts[:filename]}\n" \
877
1261
  "# block_name: #{opts[:block_name]}\n" \
878
1262
  "# time: #{Time.now.utc}\n" \
879
1263
  "#{required_blocks.flatten.join("\n")}\n")
1264
+
1265
+ @options[:saved_script_chmod].tap_inspect name: :@options_saved_script_chmod
1266
+ return if @options[:saved_script_chmod].zero?
1267
+
1268
+ @options[:saved_script_chmod].tap_inspect name: :@options_saved_script_chmod
1269
+ File.chmod @options[:saved_script_chmod], @options[:saved_filespec]
1270
+ @options[:saved_script_chmod].tap_inspect name: :@options_saved_script_chmod
880
1271
  end
881
1272
  end
882
1273
  end