markdown_exec 1.0.0 → 1.2.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,46 +3,38 @@
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 'colorize'
14
+ require_relative 'env'
15
+ require_relative 'shared'
16
+ require_relative 'tap'
38
17
  require_relative 'markdown_exec/version'
39
18
 
19
+ include Tap # rubocop:disable Style/MixinUsage
20
+
40
21
  $stderr.sync = true
41
22
  $stdout.sync = true
42
23
 
43
24
  BLOCK_SIZE = 1024
44
25
 
45
- class Object # rubocop:disable Style/Documentation
26
+ # hash with keys sorted by name
27
+ #
28
+ class Hash
29
+ def sort_by_key
30
+ keys.sort.to_h { |key| [key, self[key]] }
31
+ end
32
+ end
33
+
34
+ # is the value a non-empty string or a binary?
35
+ #
36
+ # :reek:ManualDispatch ### temp
37
+ class Object
46
38
  def present?
47
39
  case self.class.to_s
48
40
  when 'FalseClass', 'TrueClass'
@@ -53,7 +45,9 @@ class Object # rubocop:disable Style/Documentation
53
45
  end
54
46
  end
55
47
 
56
- class String # rubocop:disable Style/Documentation
48
+ # is value empty?
49
+ #
50
+ class String
57
51
  BLANK_RE = /\A[[:space:]]*\z/.freeze
58
52
  def blank?
59
53
  empty? || BLANK_RE.match?(self)
@@ -62,51 +56,249 @@ end
62
56
 
63
57
  public
64
58
 
65
- # debug output
59
+ # display_level values
60
+ DISPLAY_LEVEL_BASE = 0 # required output
61
+ DISPLAY_LEVEL_ADMIN = 1
62
+ DISPLAY_LEVEL_DEBUG = 2
63
+ DISPLAY_LEVEL_DEFAULT = DISPLAY_LEVEL_ADMIN
64
+ DISPLAY_LEVEL_MAX = DISPLAY_LEVEL_DEBUG
65
+
66
+ # @execute_files[ind] = @execute_files[ind] + [block]
67
+ EF_STDOUT = 0
68
+ EF_STDERR = 1
69
+ EF_STDIN = 2
70
+
71
+ # execute markdown documents
66
72
  #
67
- def tap_inspect(format: nil, name: 'return')
68
- return self unless $pdebug
73
+ module MarkdownExec
74
+ # :reek:IrresponsibleModule
75
+ class Error < StandardError; end
69
76
 
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])
77
+ ## an imported markdown document
78
+ #
79
+ class MDoc
80
+ def initialize(table)
81
+ @table = table
82
+ end
77
83
 
78
- puts "-> #{caller[0].scan(/in `?(\S+)'$/)[0][0]}()" \
79
- " #{name}: #{method(fn).call}"
84
+ def code(block)
85
+ all = [block[:name]] + recursively_required(block[:reqs])
86
+ all.reverse.map do |req|
87
+ get_block_by_name(req).fetch(:body, '')
88
+ end
89
+ .flatten(1)
90
+ .tap_inspect
91
+ end
80
92
 
81
- self
82
- end
93
+ def get_block_by_name(name, default = {})
94
+ @table.select { |block| block[:name] == name }.fetch(0, default)
95
+ end
83
96
 
84
- module MarkdownExec
85
- class Error < StandardError; end
97
+ def list_recursively_required_blocks(name)
98
+ name_block = get_block_by_name(name)
99
+ raise "Named code block `#{name}` not found." if name_block.nil? || name_block.keys.empty?
100
+
101
+ all = [name_block[:name]] + recursively_required(name_block[:reqs])
102
+
103
+ # in order of appearance in document
104
+ @table.select { |block| all.include? block[:name] }
105
+ .map { |block| block.fetch(:body, '') }
106
+ .flatten(1)
107
+ .tap_inspect
108
+ end
109
+
110
+ def option_exclude_blocks(opts)
111
+ block_name_excluded_match = Regexp.new opts[:block_name_excluded_match]
112
+ if opts[:hide_blocks_by_name]
113
+ @table.reject { |block| block[:name].match(block_name_excluded_match) }
114
+ else
115
+ @table
116
+ end
117
+ end
118
+
119
+ def recursively_required(reqs)
120
+ all = []
121
+ rem = reqs
122
+ while rem.count.positive?
123
+ rem = rem.map do |req|
124
+ next if all.include? req
125
+
126
+ all += [req]
127
+ get_block_by_name(req).fetch(:reqs, [])
128
+ end
129
+ .compact
130
+ .flatten(1)
131
+ .tap_inspect(name: 'rem')
132
+ end
133
+ all.tap_inspect
134
+ end
135
+ end
136
+
137
+ # format option defaults and values
138
+ #
139
+ # :reek:TooManyInstanceVariables
140
+ class BlockLabel
141
+ def initialize(filename:, headings:, menu_blocks_with_docname:, menu_blocks_with_headings:, title:)
142
+ @filename = filename
143
+ @headings = headings
144
+ @menu_blocks_with_docname = menu_blocks_with_docname
145
+ @menu_blocks_with_headings = menu_blocks_with_headings
146
+ @title = title
147
+ end
148
+
149
+ def make
150
+ ([@title] +
151
+ (if @menu_blocks_with_headings
152
+ [@headings.compact.join(' # ')]
153
+ else
154
+ []
155
+ end) +
156
+ (
157
+ if @menu_blocks_with_docname
158
+ [@filename]
159
+ else
160
+ []
161
+ end
162
+ )).join(' ')
163
+ end
164
+ end
165
+
166
+ FNR11 = '/'
167
+ FNR12 = ',~'
168
+
169
+ # format option defaults and values
170
+ #
171
+ class SavedAsset
172
+ def initialize(filename:, prefix:, time:, blockname:)
173
+ @filename = filename
174
+ @prefix = prefix
175
+ @time = time
176
+ @blockname = blockname
177
+ end
178
+
179
+ def script_name
180
+ fne = @filename.gsub(FNR11, FNR12)
181
+ "#{[@prefix, @time.strftime('%F-%H-%M-%S'), fne, ',', @blockname].join('_')}.sh".tap_inspect
182
+ end
183
+
184
+ def stdout_name
185
+ "#{[@prefix, @time.strftime('%F-%H-%M-%S'), @filename, @blockname].join('_')}.out.txt".tap_inspect
186
+ end
187
+ end
188
+
189
+ # format option defaults and values
190
+ #
191
+ class OptionValue
192
+ def initialize(value)
193
+ @value = value
194
+ end
195
+
196
+ # as default value in env_str()
197
+ #
198
+ def for_hash(default = nil)
199
+ return default if @value.nil?
200
+
201
+ case @value.class.to_s
202
+ when 'String', 'Integer'
203
+ @value
204
+ when 'FalseClass', 'TrueClass'
205
+ @value ? true : false
206
+ when @value.empty?
207
+ default
208
+ else
209
+ @value.to_s
210
+ end
211
+ end
212
+
213
+ # for output as default value in list_default_yaml()
214
+ #
215
+ def for_yaml(default = nil)
216
+ return default if @value.nil?
217
+
218
+ case @value.class.to_s
219
+ when 'String'
220
+ "'#{@value}'"
221
+ when 'Integer'
222
+ @value
223
+ when 'FalseClass', 'TrueClass'
224
+ @value ? true : false
225
+ when @value.empty?
226
+ default
227
+ else
228
+ @value.to_s
229
+ end
230
+ end
231
+ end
232
+
233
+ # a generated list of saved files
234
+ #
235
+ class Sfiles
236
+ def initialize(folder, glob)
237
+ @folder = folder
238
+ @glob = glob
239
+ end
240
+
241
+ def list_all
242
+ Dir.glob(File.join(@folder, @glob)).tap_inspect
243
+ end
244
+
245
+ def most_recent(arr = list_all)
246
+ return unless arr
247
+ return if arr.count < 1
248
+
249
+ arr.max.tap_inspect
250
+ end
251
+
252
+ def most_recent_list(arr = list_all)
253
+ return unless arr
254
+ return if (ac = arr.count) < 1
255
+
256
+ arr.sort[-[ac, options[:list_count]].min..].reverse.tap_inspect
257
+ end
258
+ end
86
259
 
87
260
  ##
88
261
  #
262
+ # :reek:DuplicateMethodCall { allow_calls: ['block', 'item', 'lm', 'opts', 'option', '@options', 'required_blocks'] }
263
+ # :reek:MissingSafeMethod { exclude: [ read_configuration_file! ] }
264
+ # :reek:TooManyInstanceVariables ### temp
265
+ # :reek:TooManyMethods ### temp
89
266
  class MarkParse
90
- attr_accessor :options
267
+ attr_reader :options
91
268
 
92
269
  def initialize(options = {})
93
270
  @options = options
94
271
  @prompt = TTY::Prompt.new(interrupt: :exit)
272
+ @execute_aborted_at = nil
273
+ @execute_completed_at = nil
274
+ @execute_error = nil
275
+ @execute_error_message = nil
276
+ @execute_files = nil
277
+ @execute_options = nil
278
+ @execute_script_filespec = nil
279
+ @execute_started_at = nil
280
+ @option_parser = nil
95
281
  end
96
282
 
97
283
  ##
98
284
  # options necessary to start, parse input, defaults for cli options
99
285
 
100
286
  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]
287
+ menu_iter do |item|
288
+ # noisy item.tap_inspect name: :item, format: :yaml
289
+ next unless item[:opt_name].present?
290
+
291
+ item_default = item[:default]
292
+ # noisy item_default.tap_inspect name: :item_default
293
+ value = if item_default.nil?
294
+ item_default
295
+ else
296
+ env_str(item[:env_var], default: OptionValue.new(item_default).for_hash)
297
+ end
298
+ [item[:opt_name], item[:proc1] ? item[:proc1].call(value) : value]
107
299
  end.compact.to_h.merge(
108
300
  {
109
- mdheadings: true, # use headings (levels 1,2,3) in block lable
301
+ menu_exit_at_top: true,
110
302
  menu_with_exit: true
111
303
  }
112
304
  ).tap_inspect format: :yaml
@@ -127,78 +319,110 @@ module MarkdownExec
127
319
  }
128
320
  end
129
321
 
130
- # Returns true if all files are EOF
131
- #
132
- def all_at_eof(files)
133
- files.find { |f| !f.eof }.nil?
134
- end
135
-
136
- def approve_block(opts, blocks_in_file)
137
- required_blocks = list_recursively_required_blocks(blocks_in_file, opts[:block_name])
322
+ def approve_block(opts, mdoc)
323
+ required_blocks = mdoc.list_recursively_required_blocks(opts[:block_name])
138
324
  display_command(opts, required_blocks) if opts[:output_script] || opts[:user_must_approve]
139
325
 
140
326
  allow = true
141
- allow = @prompt.yes? opts[:prompt_approve_block] if opts[:user_must_approve]
142
- opts[:ir_approve] = allow
143
- selected = get_block_by_name blocks_in_file, opts[:block_name]
327
+ if opts[:user_must_approve]
328
+ loop do
329
+ (sel = @prompt.select(opts[:prompt_approve_block], filter: true) do |menu|
330
+ menu.default 1
331
+ # menu.enum '.'
332
+ # menu.filter true
333
+
334
+ menu.choice 'Yes', 1
335
+ menu.choice 'No', 2
336
+ menu.choice 'Copy script to clipboard', 3
337
+ menu.choice 'Save script', 4
338
+ end).tap_inspect name: :sel
339
+ allow = (sel == 1)
340
+ if sel == 3
341
+ text = required_blocks.flatten.join($INPUT_RECORD_SEPARATOR)
342
+ Clipboard.copy(text)
343
+ fout "Clipboard updated: #{required_blocks.count} blocks," /
344
+ " #{required_blocks.flatten.count} lines," /
345
+ " #{text.length} characters"
346
+ end
347
+ if sel == 4
348
+ write_command_file(opts.merge(save_executed_script: true), required_blocks)
349
+ fout "File saved: #{@options[:saved_filespec]}"
350
+ end
351
+ break if [1, 2].include? sel
352
+ end
353
+ end
354
+ (opts[:ir_approve] = allow).tap_inspect name: :allow
355
+
356
+ selected = mdoc.get_block_by_name opts[:block_name]
144
357
 
145
358
  if opts[:ir_approve]
146
359
  write_command_file opts, required_blocks
147
360
  command_execute opts, required_blocks.flatten.join("\n")
148
361
  save_execution_output
149
362
  output_execution_summary
363
+ output_execution_result
150
364
  end
151
365
 
152
366
  selected[:name]
153
367
  end
154
368
 
155
- def code(table, block)
156
- all = [block[:name]] + recursively_required(table, block[:reqs])
157
- all.reverse.map do |req|
158
- get_block_by_name(table, req).fetch(:body, '')
159
- end
160
- .flatten(1)
161
- .tap_inspect
162
- end
163
-
164
- def command_execute(opts, cmd2)
369
+ # :reek:DuplicateMethodCall
370
+ # :reek:UncommunicativeVariableName { exclude: [ e ] }
371
+ # :reek:LongYieldList
372
+ def command_execute(opts, command)
165
373
  @execute_files = Hash.new([])
166
374
  @execute_options = opts
167
375
  @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
376
+
377
+ Open3.popen3(@options[:shell], '-c', command) do |stdin, stdout, stderr, exec_thr|
378
+ # pid = exec_thr.pid # pid of the started process
379
+
380
+ Thread.new do
381
+ until (line = stdout.gets).nil?
382
+ @execute_files[EF_STDOUT] = @execute_files[EF_STDOUT] + [line]
383
+ print line if opts[:output_stdout]
384
+ yield nil, line, nil, exec_thr if block_given?
385
+ end
386
+ rescue IOError
387
+ # thread killed, do nothing
388
+ end
389
+
390
+ Thread.new do
391
+ until (line = stderr.gets).nil?
392
+ @execute_files[EF_STDERR] = @execute_files[EF_STDERR] + [line]
393
+ print line if opts[:output_stdout]
394
+ yield nil, nil, line, exec_thr if block_given?
395
+ end
396
+ rescue IOError
397
+ # thread killed, do nothing
398
+ end
399
+
400
+ in_thr = Thread.new do
401
+ while exec_thr.alive? # reading input until the child process ends
402
+ stdin.puts(line = $stdin.gets)
403
+ @execute_files[EF_STDIN] = @execute_files[EF_STDIN] + [line]
404
+ yield line, nil, nil, exec_thr if block_given?
190
405
  end
191
- rescue IOError => e
192
- fout "IOError: #{e}"
193
406
  end
194
- @execute_completed_at = Time.now.utc
407
+
408
+ exec_thr.join
409
+ in_thr.kill
410
+ # @return_code = exec_thr.value
195
411
  end
412
+ @execute_completed_at = Time.now.utc
196
413
  rescue Errno::ENOENT => e
197
414
  # error triggered by missing command in script
198
415
  @execute_aborted_at = Time.now.utc
199
416
  @execute_error_message = e.message
200
417
  @execute_error = e
201
- @execute_files[1] = e.message
418
+ @execute_files[EF_STDERR] += [@execute_error_message]
419
+ fout "Error ENOENT: #{e.inspect}"
420
+ rescue SignalException => e
421
+ # SIGTERM triggered by user or system
422
+ @execute_aborted_at = Time.now.utc
423
+ @execute_error_message = 'SIGTERM'
424
+ @execute_error = e
425
+ @execute_files[EF_STDERR] += [@execute_error_message]
202
426
  fout "Error ENOENT: #{e.inspect}"
203
427
  end
204
428
 
@@ -211,10 +435,15 @@ module MarkdownExec
211
435
  cnt / 2
212
436
  end
213
437
 
438
+ # :reek:DuplicateMethodCall
214
439
  def display_command(_opts, required_blocks)
440
+ frame = ' #=#=#'.yellow
441
+ fout frame
215
442
  required_blocks.each { |cb| fout cb }
443
+ fout frame
216
444
  end
217
445
 
446
+ # :reek:DuplicateMethodCall
218
447
  def exec_block(options, _block_name = '')
219
448
  options = default_options.merge options
220
449
  update_options options, over: false
@@ -239,7 +468,8 @@ module MarkdownExec
239
468
  run_last_script: -> { run_last_script },
240
469
  select_recent_output: -> { select_recent_output },
241
470
  select_recent_script: -> { select_recent_script },
242
- tab_completions: -> { fout tab_completions }
471
+ tab_completions: -> { fout tab_completions },
472
+ menu_export: -> { fout menu_export }
243
473
  }
244
474
  simple_commands.each_key do |key|
245
475
  if @options[key]
@@ -273,17 +503,15 @@ module MarkdownExec
273
503
  puts data.to_yaml
274
504
  end
275
505
 
276
- def get_block_by_name(table, name, default = {})
277
- table.select { |block| block[:name] == name }.fetch(0, default)
278
- end
279
-
280
- def get_block_summary(opts, headings, block_title, current)
506
+ # :reek:LongParameterList
507
+ def get_block_summary(opts, headings:, block_title:, current:)
281
508
  return [current] unless opts[:struct]
282
509
 
283
510
  return [summarize_block(headings, block_title).merge({ body: current })] unless opts[:bash]
284
511
 
285
512
  bm = block_title.match(Regexp.new(opts[:block_name_match]))
286
- reqs = block_title.scan(Regexp.new(opts[:block_required_scan])).map { |s| s[1..] }
513
+ reqs = block_title.scan(Regexp.new(opts[:block_required_scan]))
514
+ .map { |scanned| scanned[1..] }
287
515
 
288
516
  if bm && bm[1]
289
517
  [summarize_block(headings, bm[:title]).merge({ body: current, reqs: reqs })]
@@ -292,6 +520,20 @@ module MarkdownExec
292
520
  end
293
521
  end
294
522
 
523
+ def approved_fout?(level)
524
+ level <= @options[:display_level]
525
+ end
526
+
527
+ # display output at level or lower than filter (DISPLAY_LEVEL_DEFAULT)
528
+ #
529
+ def lout(str, level: DISPLAY_LEVEL_BASE)
530
+ return unless approved_fout? level
531
+
532
+ # fout level == DISPLAY_LEVEL_BASE ? str : DISPLAY_LEVEL_XBASE_PREFIX + str
533
+ fout level == DISPLAY_LEVEL_BASE ? str : @options[:display_level_xbase_prefix] + str
534
+ end
535
+
536
+ # :reek:DuplicateMethodCall
295
537
  def list_blocks_in_file(call_options = {}, &options_block)
296
538
  opts = optsmerge call_options, options_block
297
539
 
@@ -315,7 +557,7 @@ module MarkdownExec
315
557
  File.readlines(opts[:filename]).each do |line|
316
558
  continue unless line
317
559
 
318
- if opts[:mdheadings]
560
+ if opts[:menu_blocks_with_headings]
319
561
  if (lm = line.match(Regexp.new(opts[:heading3_match])))
320
562
  headings = [headings[0], headings[1], lm[:name]]
321
563
  elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
@@ -329,7 +571,7 @@ module MarkdownExec
329
571
  if in_block
330
572
  if current
331
573
  block_title = current.join(' ').gsub(/ +/, ' ')[0..64] if block_title.nil? || block_title.empty?
332
- blocks += get_block_summary opts, headings, block_title, current
574
+ blocks += get_block_summary opts, headings: headings, block_title: block_title, current: current
333
575
  current = nil
334
576
  end
335
577
  in_block = false
@@ -338,16 +580,16 @@ module MarkdownExec
338
580
  # new block
339
581
  #
340
582
  lm = line.match(fenced_start_ex)
341
- do1 = false
583
+ block_allow = false
342
584
  if opts[:bash_only]
343
- do1 = true if lm && (lm[:shell] == 'bash')
585
+ block_allow = true if lm && (lm[:shell] == 'bash')
344
586
  else
345
- do1 = true
346
- do1 = !(lm && (lm[:shell] == 'expect')) if opts[:exclude_expect_blocks]
587
+ block_allow = true
588
+ block_allow = !(lm && (lm[:shell] == 'expect')) if opts[:exclude_expect_blocks]
347
589
  end
348
590
 
349
591
  in_block = true
350
- if do1 && (!opts[:title_match] || (lm && lm[:name] && lm[:name].match(opts[:title_match])))
592
+ if block_allow && (!opts[:title_match] || (lm && lm[:name] && lm[:name].match(opts[:title_match])))
351
593
  current = []
352
594
  block_title = (lm && lm[:name])
353
595
  end
@@ -360,39 +602,39 @@ module MarkdownExec
360
602
  end
361
603
 
362
604
  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?
605
+ menu_iter do |item|
606
+ next unless item[:env_var].present?
366
607
 
367
608
  [
368
- "#{env_var}=#{value_for_cli default}",
369
- description.present? ? description : nil
609
+ "#{item[:env_var]}=#{value_for_cli item[:default]}",
610
+ item[:description].present? ? item[:description] : nil
370
611
  ].compact.join(' # ')
371
612
  end.compact.sort
372
613
  end
373
614
 
374
615
  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?
616
+ menu_iter do |item|
617
+ next unless item[:opt_name].present? && item[:default].present?
378
618
 
379
619
  [
380
- "#{opt_name}: #{value_for_yaml default}",
381
- description.present? ? description : nil
620
+ "#{item[:opt_name]}: #{OptionValue.new(item[:default]).for_yaml}",
621
+ item[:description].present? ? item[:description] : nil
382
622
  ].compact.join(' # ')
383
623
  end.compact.sort
384
624
  end
385
625
 
386
626
  def list_files_per_options(options)
387
627
  list_files_specified(
388
- options[:filename]&.present? ? options[:filename] : nil,
389
- options[:path],
390
- 'README.md',
391
- '.'
628
+ specified_filename: options[:filename]&.present? ? options[:filename] : nil,
629
+ specified_folder: options[:path],
630
+ default_filename: 'README.md',
631
+ default_folder: '.'
392
632
  ).tap_inspect
393
633
  end
394
634
 
395
- def list_files_specified(specified_filename, specified_folder, default_filename, default_folder, filetree = nil)
635
+ # :reek:LongParameterList
636
+ def list_files_specified(specified_filename: nil, specified_folder: nil,
637
+ default_filename: nil, default_folder: nil, filetree: nil)
396
638
  fn = File.join(if specified_filename&.present?
397
639
  if specified_folder&.present?
398
640
  [specified_folder, specified_filename]
@@ -431,51 +673,14 @@ module MarkdownExec
431
673
  end.compact.tap_inspect
432
674
  end
433
675
 
434
- def list_recursively_required_blocks(table, name)
435
- name_block = get_block_by_name(table, name)
436
- raise "Named code block `#{name}` not found." if name_block.nil? || name_block.keys.empty?
437
-
438
- all = [name_block[:name]] + recursively_required(table, name_block[:reqs])
439
-
440
- # in order of appearance in document
441
- table.select { |block| all.include? block[:name] }
442
- .map { |block| block.fetch(:body, '') }
443
- .flatten(1)
444
- .tap_inspect
445
- end
446
-
447
- def most_recent(arr)
448
- return unless arr
449
- return if arr.count < 1
450
-
451
- arr.max.tap_inspect
452
- end
453
-
454
- def most_recent_list(arr)
455
- return unless arr
456
- return if (ac = arr.count) < 1
457
-
458
- arr.sort[-[ac, options[:list_count]].min..].reverse.tap_inspect
459
- end
460
-
461
676
  def list_recent_output
462
- most_recent_list(Dir.glob(File.join(@options[:saved_stdout_folder],
463
- @options[:saved_stdout_glob]))).tap_inspect
677
+ Sfiles.new(@options[:saved_stdout_folder],
678
+ @options[:saved_stdout_glob]).most_recent_list
464
679
  end
465
680
 
466
681
  def list_recent_scripts
467
- most_recent_list(Dir.glob(File.join(@options[:saved_script_folder],
468
- @options[:saved_script_glob]))).tap_inspect
469
- end
470
-
471
- def make_block_label(block, call_options = {})
472
- opts = options.merge(call_options)
473
- if opts[:mdheadings]
474
- heads = block.fetch(:headings, []).compact.join(' # ')
475
- "#{block[:title]} [#{heads}] (#{opts[:filename]})"
476
- else
477
- "#{block[:title]} (#{opts[:filename]})"
478
- end
682
+ Sfiles.new(@options[:saved_script_folder],
683
+ @options[:saved_script_glob]).most_recent_list
479
684
  end
480
685
 
481
686
  def make_block_labels(call_options = {})
@@ -483,102 +688,435 @@ module MarkdownExec
483
688
  list_blocks_in_file(opts).map do |block|
484
689
  # next if opts[:hide_blocks_by_name] && block[:name].match(%r{^:\(.+\)$})
485
690
 
486
- make_block_label block, opts
691
+ BlockLabel.new(filename: opts[:filename],
692
+ headings: block.fetch(:headings, []),
693
+ menu_blocks_with_docname: opts[:menu_blocks_with_docname],
694
+ menu_blocks_with_headings: opts[:menu_blocks_with_headings],
695
+ title: block[:title]).make
487
696
  end.compact.tap_inspect
488
697
  end
489
698
 
490
- def menu_data
699
+ # :reek:DuplicateMethodCall
700
+ # :reek:UncommunicativeMethodName ### temp
701
+ def menu_data1
491
702
  val_as_bool = ->(value) { value.class.to_s == 'String' ? (value.chomp != '0') : value }
492
703
  val_as_int = ->(value) { value.to_i }
493
704
  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],
705
+ # val_true = ->(_value) { true } # for commands, sets option to true
706
+ menu_options = [
707
+ {
708
+ arg_name: 'PATH',
709
+ default: '.',
710
+ description: 'Read configuration file',
711
+ long_name: 'config',
712
+ proc1: lambda { |value|
713
+ read_configuration_file! options, value
714
+ }
715
+ },
716
+ {
717
+ arg_name: 'BOOL',
718
+ default: false,
719
+ description: 'Debug output',
720
+ env_var: 'MDE_DEBUG',
721
+ long_name: 'debug',
722
+ short_name: 'd',
723
+ proc1: lambda { |value|
724
+ tap_config value.to_i != 0
725
+ }
726
+ },
727
+ {
728
+ arg_name: "INT.#{DISPLAY_LEVEL_BASE}-#{DISPLAY_LEVEL_MAX}",
729
+ default: DISPLAY_LEVEL_DEFAULT,
730
+ description: "Output display level (#{DISPLAY_LEVEL_BASE} to #{DISPLAY_LEVEL_MAX})",
731
+ env_var: 'MDE_DISPLAY_LEVEL',
732
+ long_name: 'display-level',
733
+ opt_name: :display_level,
734
+ proc1: val_as_int
735
+ },
736
+ {
737
+ arg_name: 'NAME',
738
+ compreply: false,
739
+ description: 'Name of block',
740
+ env_var: 'MDE_BLOCK_NAME',
741
+ long_name: 'block-name',
742
+ opt_name: :block_name,
743
+ short_name: 'f',
744
+ proc1: val_as_str
745
+ },
746
+ {
747
+ arg_name: 'RELATIVE_PATH',
748
+ compreply: '.',
749
+ description: 'Name of document',
750
+ env_var: 'MDE_FILENAME',
751
+ long_name: 'filename',
752
+ opt_name: :filename,
753
+ short_name: 'f',
754
+ proc1: val_as_str
755
+ },
756
+ {
757
+ description: 'List blocks',
758
+ long_name: 'list-blocks',
759
+ opt_name: :list_blocks,
760
+ proc1: val_as_bool
761
+ },
762
+ {
763
+ arg_name: 'INT.1-',
764
+ default: 32,
765
+ description: 'Max. items to return in list',
766
+ env_var: 'MDE_LIST_COUNT',
767
+ long_name: 'list-count',
768
+ opt_name: :list_count,
769
+ proc1: val_as_int
770
+ },
771
+ {
772
+ description: 'List default configuration as environment variables',
773
+ long_name: 'list-default-env',
774
+ opt_name: :list_default_env
775
+ },
776
+ {
777
+ description: 'List default configuration as YAML',
778
+ long_name: 'list-default-yaml',
779
+ opt_name: :list_default_yaml
780
+ },
781
+ {
782
+ description: 'List docs in current folder',
783
+ long_name: 'list-docs',
784
+ opt_name: :list_docs,
785
+ proc1: val_as_bool
786
+ },
787
+ {
788
+ description: 'List recent saved output',
789
+ long_name: 'list-recent-output',
790
+ opt_name: :list_recent_output,
791
+ proc1: val_as_bool
792
+ },
793
+ {
794
+ description: 'List recent saved scripts',
795
+ long_name: 'list-recent-scripts',
796
+ opt_name: :list_recent_scripts,
797
+ proc1: val_as_bool
798
+ },
799
+ {
800
+ arg_name: 'PREFIX',
801
+ default: MarkdownExec::BIN_NAME,
802
+ description: 'Name prefix for stdout files',
803
+ env_var: 'MDE_LOGGED_STDOUT_FILENAME_PREFIX',
804
+ long_name: 'logged-stdout-filename-prefix',
805
+ opt_name: :logged_stdout_filename_prefix,
806
+ proc1: val_as_str
807
+ },
808
+ {
809
+ arg_name: 'BOOL',
810
+ default: false,
811
+ description: 'Display document name in block selection menu',
812
+ env_var: 'MDE_MENU_BLOCKS_WITH_DOCNAME',
813
+ long_name: 'menu-blocks-with-docname',
814
+ opt_name: :menu_blocks_with_docname,
815
+ proc1: val_as_bool
816
+ },
817
+ {
818
+ arg_name: 'BOOL',
819
+ default: false,
820
+ description: 'Display headings (levels 1,2,3) in block selection menu',
821
+ env_var: 'MDE_MENU_BLOCKS_WITH_HEADINGS',
822
+ long_name: 'menu-blocks-with-headings',
823
+ opt_name: :menu_blocks_with_headings,
824
+ proc1: val_as_bool
825
+ },
826
+ {
827
+ arg_name: 'BOOL',
828
+ default: false,
829
+ description: 'Display summary for execution',
830
+ env_var: 'MDE_OUTPUT_EXECUTION_SUMMARY',
831
+ long_name: 'output-execution-summary',
832
+ opt_name: :output_execution_summary,
833
+ proc1: val_as_bool
834
+ },
835
+ {
836
+ arg_name: 'BOOL',
837
+ default: false,
838
+ description: 'Display script prior to execution',
839
+ env_var: 'MDE_OUTPUT_SCRIPT',
840
+ long_name: 'output-script',
841
+ opt_name: :output_script,
842
+ proc1: val_as_bool
843
+ },
844
+ {
845
+ arg_name: 'BOOL',
846
+ default: true,
847
+ description: 'Display standard output from execution',
848
+ env_var: 'MDE_OUTPUT_STDOUT',
849
+ long_name: 'output-stdout',
850
+ opt_name: :output_stdout,
851
+ proc1: val_as_bool
852
+ },
853
+ {
854
+ arg_name: 'RELATIVE_PATH',
855
+ default: '.',
856
+ description: 'Path to documents',
857
+ env_var: 'MDE_PATH',
858
+ long_name: 'path',
859
+ opt_name: :path,
860
+ short_name: 'p',
861
+ proc1: val_as_str
862
+ },
863
+ {
864
+ description: 'Gem home folder',
865
+ long_name: 'pwd',
866
+ opt_name: :pwd,
867
+ proc1: val_as_bool
868
+ },
869
+ {
870
+ description: 'Run most recently saved script',
871
+ long_name: 'run-last-script',
872
+ opt_name: :run_last_script,
873
+ proc1: val_as_bool
874
+ },
875
+ {
876
+ arg_name: 'BOOL',
877
+ default: false,
878
+ description: 'Save executed script',
879
+ env_var: 'MDE_SAVE_EXECUTED_SCRIPT',
880
+ long_name: 'save-executed-script',
881
+ opt_name: :save_executed_script,
882
+ proc1: val_as_bool
883
+ },
884
+ {
885
+ arg_name: 'BOOL',
886
+ default: false,
887
+ description: 'Save standard output of the executed script',
888
+ env_var: 'MDE_SAVE_EXECUTION_OUTPUT',
889
+ long_name: 'save-execution-output',
890
+ opt_name: :save_execution_output,
891
+ proc1: val_as_bool
892
+ },
893
+ {
894
+ arg_name: 'INT',
895
+ default: 0o755,
896
+ description: 'chmod for saved scripts',
897
+ env_var: 'MDE_SAVED_SCRIPT_CHMOD',
898
+ long_name: 'saved-script-chmod',
899
+ opt_name: :saved_script_chmod,
900
+ proc1: val_as_int
901
+ },
902
+ {
903
+ arg_name: 'PREFIX',
904
+ default: MarkdownExec::BIN_NAME,
905
+ description: 'Name prefix for saved scripts',
906
+ env_var: 'MDE_SAVED_SCRIPT_FILENAME_PREFIX',
907
+ long_name: 'saved-script-filename-prefix',
908
+ opt_name: :saved_script_filename_prefix,
909
+ proc1: val_as_str
910
+ },
911
+ {
912
+ arg_name: 'RELATIVE_PATH',
913
+ default: 'logs',
914
+ description: 'Saved script folder',
915
+ env_var: 'MDE_SAVED_SCRIPT_FOLDER',
916
+ long_name: 'saved-script-folder',
917
+ opt_name: :saved_script_folder,
918
+ proc1: val_as_str
919
+ },
920
+ {
921
+ arg_name: 'GLOB',
922
+ default: 'mde_*.sh',
923
+ description: 'Glob matching saved scripts',
924
+ env_var: 'MDE_SAVED_SCRIPT_GLOB',
925
+ long_name: 'saved-script-glob',
926
+ opt_name: :saved_script_glob,
927
+ proc1: val_as_str
928
+ },
929
+ {
930
+ arg_name: 'RELATIVE_PATH',
931
+ default: 'logs',
932
+ description: 'Saved stdout folder',
933
+ env_var: 'MDE_SAVED_STDOUT_FOLDER',
934
+ long_name: 'saved-stdout-folder',
935
+ opt_name: :saved_stdout_folder,
936
+ proc1: val_as_str
937
+ },
938
+ {
939
+ arg_name: 'GLOB',
940
+ default: 'mde_*.out.txt',
941
+ description: 'Glob matching saved outputs',
942
+ env_var: 'MDE_SAVED_STDOUT_GLOB',
943
+ long_name: 'saved-stdout-glob',
944
+ opt_name: :saved_stdout_glob,
945
+ proc1: val_as_str
946
+ },
947
+ {
948
+ description: 'Select and execute a recently saved output',
949
+ long_name: 'select-recent-output',
950
+ opt_name: :select_recent_output,
951
+ proc1: val_as_bool
952
+ },
953
+ {
954
+ description: 'Select and execute a recently saved script',
955
+ long_name: 'select-recent-script',
956
+ opt_name: :select_recent_script,
957
+ proc1: val_as_bool
958
+ },
959
+ {
960
+ description: 'YAML export of menu',
961
+ long_name: 'menu-export',
962
+ opt_name: :menu_export,
963
+ proc1: val_as_bool
964
+ },
965
+ {
966
+ description: 'List tab completions',
967
+ long_name: 'tab-completions',
968
+ opt_name: :tab_completions,
969
+ proc1: val_as_bool
970
+ },
971
+ {
972
+ arg_name: 'BOOL',
973
+ default: true,
974
+ description: 'Pause for user to approve script',
975
+ env_var: 'MDE_USER_MUST_APPROVE',
976
+ long_name: 'user-must-approve',
977
+ opt_name: :user_must_approve,
978
+ proc1: val_as_bool
979
+ },
980
+ {
981
+ description: 'Show current configuration values',
982
+ short_name: '0',
983
+ proc1: lambda { |_|
984
+ options_finalize options
985
+ fout options.sort_by_key.to_yaml
986
+ }
987
+ },
988
+ {
989
+ description: 'App help',
990
+ long_name: 'help',
991
+ short_name: 'h',
992
+ proc1: lambda { |_|
993
+ fout menu_help
994
+ exit
995
+ }
996
+ },
997
+ {
998
+ description: "Print the gem's version",
999
+ long_name: 'version',
1000
+ short_name: 'v',
1001
+ proc1: lambda { |_|
1002
+ fout MarkdownExec::VERSION
1003
+ exit
1004
+ }
1005
+ },
1006
+ {
1007
+ description: 'Exit app',
1008
+ long_name: 'exit',
1009
+ short_name: 'x',
1010
+ proc1: ->(_) { exit }
1011
+ },
1012
+ {
1013
+ default: '^\(.*\)$',
1014
+ description: 'Pattern for blocks to hide from user-selection',
1015
+ env_var: 'MDE_BLOCK_NAME_EXCLUDED_MATCH',
1016
+ opt_name: :block_name_excluded_match,
1017
+ proc1: val_as_str
1018
+ },
1019
+ {
1020
+ default: ':(?<title>\S+)( |$)',
1021
+ env_var: 'MDE_BLOCK_NAME_MATCH',
1022
+ opt_name: :block_name_match,
1023
+ proc1: val_as_str
1024
+ },
1025
+ {
1026
+ default: '\+\S+',
1027
+ env_var: 'MDE_BLOCK_REQUIRED_SCAN',
1028
+ opt_name: :block_required_scan,
1029
+ proc1: val_as_str
1030
+ },
1031
+ {
1032
+ default: '> ',
1033
+ env_var: 'MDE_DISPLAY_LEVEL_XBASE_PREFIX',
1034
+ opt_name: :display_level_xbase_prefix,
1035
+ proc1: val_as_str
1036
+ },
1037
+ {
1038
+ default: '^`{3,}',
1039
+ env_var: 'MDE_FENCED_START_AND_END_MATCH',
1040
+ opt_name: :fenced_start_and_end_match,
1041
+ proc1: val_as_str
1042
+ },
1043
+ {
1044
+ default: '^`{3,}(?<shell>[^`\s]*) *(?<name>.*)$',
1045
+ env_var: 'MDE_FENCED_START_EX_MATCH',
1046
+ opt_name: :fenced_start_ex_match,
1047
+ proc1: val_as_str
1048
+ },
1049
+ {
1050
+ default: '^# *(?<name>[^#]*?) *$',
1051
+ env_var: 'MDE_HEADING1_MATCH',
1052
+ opt_name: :heading1_match,
1053
+ proc1: val_as_str
1054
+ },
1055
+ {
1056
+ default: '^## *(?<name>[^#]*?) *$',
1057
+ env_var: 'MDE_HEADING2_MATCH',
1058
+ opt_name: :heading2_match,
1059
+ proc1: val_as_str
1060
+ },
1061
+ {
1062
+ default: '^### *(?<name>.+?) *$',
1063
+ env_var: 'MDE_HEADING3_MATCH',
1064
+ opt_name: :heading3_match,
1065
+ proc1: val_as_str
1066
+ },
1067
+ {
1068
+ default: '*.[Mm][Dd]',
1069
+ env_var: 'MDE_MD_FILENAME_GLOB',
1070
+ opt_name: :md_filename_glob,
1071
+ proc1: val_as_str
1072
+ },
1073
+ {
1074
+ default: '.+\\.md',
1075
+ env_var: 'MDE_MD_FILENAME_MATCH',
1076
+ opt_name: :md_filename_match,
1077
+ proc1: val_as_str
1078
+ },
1079
+ {
1080
+ description: 'Options for viewing saved output file',
1081
+ env_var: 'MDE_OUTPUT_VIEWER_OPTIONS',
1082
+ opt_name: :output_viewer_options,
1083
+ proc1: val_as_str
1084
+ },
1085
+ {
1086
+ default: 24,
1087
+ description: 'Maximum # of rows in select list',
1088
+ env_var: 'MDE_SELECT_PAGE_HEIGHT',
1089
+ opt_name: :select_page_height,
1090
+ proc1: val_as_int
1091
+ },
1092
+ {
1093
+ default: '#!/usr/bin/env',
1094
+ description: 'Shebang for saved scripts',
1095
+ env_var: 'MDE_SHEBANG',
1096
+ opt_name: :shebang,
1097
+ proc1: val_as_str
1098
+ },
1099
+ {
1100
+ default: 'bash',
1101
+ description: 'Shell for launched scripts',
1102
+ env_var: 'MDE_SHELL',
1103
+ opt_name: :shell,
1104
+ proc1: val_as_str
1105
+ }
564
1106
  ]
1107
+ # commands first, options second
1108
+ (menu_options.reject { |option| option[:arg_name] }) +
1109
+ (menu_options.select { |option| option[:arg_name] })
1110
+ end
565
1111
 
566
- summary_head + summary_body + summary_tail + env_vars
1112
+ def menu_iter(data = menu_data1, &block)
1113
+ data.map(&block)
567
1114
  end
568
1115
 
569
1116
  def menu_help
570
1117
  @option_parser.help
571
1118
  end
572
1119
 
573
- def option_exclude_blocks(opts, blocks)
574
- block_name_excluded_match = Regexp.new opts[:block_name_excluded_match]
575
- if opts[:hide_blocks_by_name]
576
- blocks.reject { |block| block[:name].match(block_name_excluded_match) }
577
- else
578
- blocks
579
- end
580
- end
581
-
582
1120
  ## post-parse options configuration
583
1121
  #
584
1122
  def options_finalize(rest)
@@ -600,6 +1138,7 @@ module MarkdownExec
600
1138
  @options[:block_name] = block_name if block_name.present?
601
1139
  end
602
1140
 
1141
+ # :reek:ControlParameter
603
1142
  def optsmerge(call_options = {}, options_block = nil)
604
1143
  class_call_options = @options.merge(call_options || {})
605
1144
  if options_block
@@ -609,6 +1148,24 @@ module MarkdownExec
609
1148
  end.tap_inspect
610
1149
  end
611
1150
 
1151
+ def output_execution_result
1152
+ oq = [['Block', @options[:block_name], DISPLAY_LEVEL_ADMIN],
1153
+ ['Command',
1154
+ [MarkdownExec::BIN_NAME,
1155
+ @options[:filename],
1156
+ @options[:block_name]].join(' '),
1157
+ DISPLAY_LEVEL_ADMIN]]
1158
+
1159
+ [['Script', :saved_filespec],
1160
+ ['StdOut', :logged_stdout_filespec]].each do |label, name|
1161
+ oq << [label, @options[name], DISPLAY_LEVEL_ADMIN] if @options[name]
1162
+ end
1163
+
1164
+ oq.map do |label, value, level|
1165
+ lout ["#{label}:".yellow, value.to_s].join(' '), level: level
1166
+ end
1167
+ end
1168
+
612
1169
  def output_execution_summary
613
1170
  return unless @options[:output_execution_summary]
614
1171
 
@@ -626,12 +1183,16 @@ module MarkdownExec
626
1183
 
627
1184
  def prompt_with_quit(prompt_text, items, opts = {})
628
1185
  exit_option = '* Exit'
629
- sel = @prompt.select prompt_text,
630
- items + (@options[:menu_with_exit] ? [exit_option] : []),
631
- opts
1186
+ all_items = if @options[:menu_exit_at_top]
1187
+ (@options[:menu_with_exit] ? [exit_option] : []) + items
1188
+ else
1189
+ items + (@options[:menu_with_exit] ? [exit_option] : [])
1190
+ end
1191
+ sel = @prompt.select(prompt_text, all_items, opts.merge(filter: true))
632
1192
  sel == exit_option ? nil : sel
633
1193
  end
634
1194
 
1195
+ # :reek:UtilityFunction ### temp
635
1196
  def read_configuration_file!(options, configuration_path)
636
1197
  return unless File.exist?(configuration_path)
637
1198
 
@@ -641,23 +1202,7 @@ module MarkdownExec
641
1202
  # rubocop:enable Security/YAMLLoad
642
1203
  end
643
1204
 
644
- def recursively_required(table, reqs)
645
- all = []
646
- rem = reqs
647
- while rem.count.positive?
648
- rem = rem.map do |req|
649
- next if all.include? req
650
-
651
- all += [req]
652
- get_block_by_name(table, req).fetch(:reqs, [])
653
- end
654
- .compact
655
- .flatten(1)
656
- .tap_inspect(name: 'rem')
657
- end
658
- all.tap_inspect
659
- end
660
-
1205
+ # :reek:NestedIterators
661
1206
  def run
662
1207
  ## default configuration
663
1208
  #
@@ -675,19 +1220,19 @@ module MarkdownExec
675
1220
  "Usage: #{executable_name} [(path | filename [block_name])] [options]"
676
1221
  ].join("\n")
677
1222
 
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?
1223
+ menu_iter do |item|
1224
+ next unless item[:long_name].present? || item[:short_name].present?
681
1225
 
682
- opts.on(*[if long_name.present?
683
- "--#{long_name}#{arg_name.present? ? " #{arg_name}" : ''}"
1226
+ opts.on(*[if item[:long_name].present?
1227
+ "--#{item[:long_name]}#{item[:arg_name].present? ? " #{item[:arg_name]}" : ''}"
684
1228
  end,
685
- short_name.present? ? "-#{short_name}" : nil,
686
- [description,
687
- default.present? ? "[#{value_for_cli default}]" : nil].compact.join(' '),
1229
+ item[:short_name].present? ? "-#{item[:short_name]}" : nil,
1230
+ [item[:description],
1231
+ item[:default].present? ? "[#{value_for_cli item[:default]}]" : nil].compact.join(' '),
688
1232
  lambda { |value|
689
- ret = proc1.call(value)
690
- options[opt_name] = ret if opt_name
1233
+ # ret = item[:proc1].call(value)
1234
+ ret = item[:proc1] ? item[:proc1].call(value) : value
1235
+ options[item[:opt_name]] = ret if item[:opt_name]
691
1236
  ret
692
1237
  }].compact)
693
1238
  end
@@ -701,15 +1246,6 @@ module MarkdownExec
701
1246
  exec_block options, options[:block_name]
702
1247
  end
703
1248
 
704
- FNR11 = '/'
705
- FNR12 = ',;'
706
-
707
- def saved_name_make(opts)
708
- fne = opts[:filename].gsub(FNR11, FNR12)
709
- "#{[opts[:saved_script_filename_prefix], Time.now.utc.strftime('%F-%H-%M-%S'), fne,
710
- ',', opts[:block_name]].join('_')}.sh"
711
- end
712
-
713
1249
  def saved_name_split(name)
714
1250
  mf = name.match(/#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_,_(?<block>.+)\.sh/)
715
1251
  return unless mf
@@ -719,58 +1255,91 @@ module MarkdownExec
719
1255
  end
720
1256
 
721
1257
  def run_last_script
722
- filename = most_recent Dir.glob(File.join(@options[:saved_script_folder],
723
- @options[:saved_script_glob]))
1258
+ filename = Sfiles.new(@options[:saved_script_folder],
1259
+ @options[:saved_script_glob]).most_recent
724
1260
  return unless filename
725
1261
 
726
- filename.tap_inspect name: filename
727
1262
  saved_name_split filename
728
1263
  @options[:save_executed_script] = false
729
1264
  select_and_approve_block
730
1265
  end
731
1266
 
732
1267
  def save_execution_output
1268
+ @options.tap_inspect name: :options
733
1269
  return unless @options[:save_execution_output]
734
1270
 
735
- fne = File.basename(@options[:filename], '.*')
736
-
737
1271
  @options[:logged_stdout_filename] =
738
- "#{[@options[:logged_stdout_filename_prefix], Time.now.utc.strftime('%F-%H-%M-%S'), fne,
739
- @options[:block_name]].join('_')}.out.txt"
1272
+ SavedAsset.new(blockname: @options[:block_name],
1273
+ filename: File.basename(@options[:filename], '.*'),
1274
+ prefix: @options[:logged_stdout_filename_prefix],
1275
+ time: Time.now.utc).stdout_name
1276
+
740
1277
  @options[:logged_stdout_filespec] = File.join @options[:saved_stdout_folder], @options[:logged_stdout_filename]
741
1278
  @logged_stdout_filespec = @options[:logged_stdout_filespec]
742
- dirname = File.dirname(@options[:logged_stdout_filespec])
1279
+ (dirname = File.dirname(@options[:logged_stdout_filespec])).tap_inspect name: :dirname
743
1280
  Dir.mkdir dirname unless File.exist?(dirname)
744
- File.write(@options[:logged_stdout_filespec], @execute_files&.fetch(0, ''))
1281
+
1282
+ ol = ["-STDOUT-\n"]
1283
+ ol += @execute_files&.fetch(EF_STDOUT, [])
1284
+ ol += ["\n-STDERR-\n"]
1285
+ ol += @execute_files&.fetch(EF_STDERR, [])
1286
+ ol += ["\n-STDIN-\n"]
1287
+ ol += @execute_files&.fetch(EF_STDIN, [])
1288
+ ol += ["\n"]
1289
+ File.write(@options[:logged_stdout_filespec], ol.join)
745
1290
  end
746
1291
 
747
1292
  def select_and_approve_block(call_options = {}, &options_block)
748
1293
  opts = optsmerge call_options, options_block
749
1294
  blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
1295
+ mdoc = MDoc.new(blocks_in_file)
750
1296
 
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] }
1297
+ repeat_menu = true && !opts[:block_name].present?
755
1298
 
756
- return nil if block_labels.count.zero?
1299
+ loop do
1300
+ unless opts[:block_name].present?
1301
+ pt = (opts[:prompt_select_block]).to_s
757
1302
 
758
- sel = prompt_with_quit pt, block_labels, per_page: opts[:select_page_height]
759
- return nil if sel.nil?
1303
+ blocks_in_file.each do |block|
1304
+ block.merge! label:
1305
+ BlockLabel.new(filename: opts[:filename],
1306
+ headings: block.fetch(:headings, []),
1307
+ menu_blocks_with_docname: opts[:menu_blocks_with_docname],
1308
+ menu_blocks_with_headings: opts[:menu_blocks_with_headings],
1309
+ title: block[:title]).make
1310
+ end
760
1311
 
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
1312
+ block_labels = mdoc.option_exclude_blocks(opts).map { |block| block[:label] }
1313
+
1314
+ return nil if block_labels.count.zero?
1315
+
1316
+ sel = prompt_with_quit pt, block_labels, per_page: opts[:select_page_height]
1317
+ return nil if sel.nil?
1318
+
1319
+ # if sel.nil?
1320
+ # repeat_menu = false
1321
+ # break
1322
+ # end
1323
+
1324
+ label_block = blocks_in_file.select { |block| block[:label] == sel }.fetch(0, nil)
1325
+ opts[:block_name] = @options[:block_name] = label_block[:name]
1326
+
1327
+ end
1328
+ # if repeat_menu
1329
+ approve_block opts, mdoc
1330
+ # end
1331
+
1332
+ break unless repeat_menu
764
1333
 
765
- approve_block opts, blocks_in_file
1334
+ opts[:block_name] = ''
1335
+ end
766
1336
  end
767
1337
 
768
- def select_md_file(files_ = nil)
1338
+ def select_md_file(files = list_markdown_files_in_path)
769
1339
  opts = options
770
- files = files_ || list_markdown_files_in_path
771
- if files.count == 1
1340
+ if (count = files.count) == 1
772
1341
  files[0]
773
- elsif files.count >= 2
1342
+ elsif count >= 2
774
1343
  prompt_with_quit opts[:prompt_select_md].to_s, files, per_page: opts[:select_page_height]
775
1344
  end
776
1345
  end
@@ -796,20 +1365,25 @@ module MarkdownExec
796
1365
  )
797
1366
  end
798
1367
 
799
- def sorted_keys(hash1)
800
- hash1.keys.sort.to_h { |k| [k, hash1[k]] }
801
- end
802
-
803
1368
  def summarize_block(headings, title)
804
1369
  { headings: headings, name: title, title: title }
805
1370
  end
806
1371
 
807
- def tab_completions(data = menu_data)
1372
+ def menu_export(data = menu_data1)
808
1373
  data.map do |item|
809
- "--#{item[0]}" if item[0]
1374
+ item.delete(:proc1)
1375
+ item
1376
+ end.to_yaml
1377
+ end
1378
+
1379
+ def tab_completions(data = menu_data1)
1380
+ data.map do |item|
1381
+ "--#{item[:long_name]}" if item[:long_name]
810
1382
  end.compact
811
1383
  end
812
1384
 
1385
+ # :reek:BooleanParameter
1386
+ # :reek:ControlParameter
813
1387
  def update_options(opts = {}, over: true)
814
1388
  if over
815
1389
  @options = @options.merge opts
@@ -819,64 +1393,37 @@ module MarkdownExec
819
1393
  @options.tap_inspect format: :yaml
820
1394
  end
821
1395
 
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
- def value_for_hash(value, default = nil)
836
- return default if value.nil?
837
-
838
- case value.class.to_s
839
- when 'String', 'Integer', 'FalseClass', 'TrueClass'
840
- value
841
- when value.empty?
842
- default
843
- else
844
- value.to_s
845
- end
846
- end
1396
+ def write_command_file(call_options, required_blocks)
1397
+ return unless call_options[:save_executed_script]
847
1398
 
848
- def value_for_yaml(value)
849
- return default if value.nil?
1399
+ time_now = Time.now.utc
1400
+ opts = optsmerge call_options
1401
+ opts[:saved_script_filename] =
1402
+ SavedAsset.new(blockname: opts[:block_name],
1403
+ filename: opts[:filename],
1404
+ prefix: opts[:saved_script_filename_prefix],
1405
+ time: time_now).script_name
850
1406
 
851
- case value.class.to_s
852
- when 'String'
853
- "'#{value}'"
854
- when 'Integer'
855
- value
856
- when 'FalseClass', 'TrueClass'
857
- value ? true : false
858
- when value.empty?
859
- default
860
- else
861
- value.to_s
862
- end
863
- end
864
-
865
- def write_command_file(opts, required_blocks)
866
- return unless opts[:save_executed_script]
867
-
868
- opts[:saved_script_filename] = saved_name_make(opts)
869
1407
  @execute_script_filespec =
870
1408
  @options[:saved_filespec] =
871
1409
  File.join opts[:saved_script_folder], opts[:saved_script_filename]
872
1410
 
873
1411
  dirname = File.dirname(@options[:saved_filespec])
874
1412
  Dir.mkdir dirname unless File.exist?(dirname)
875
- File.write(@options[:saved_filespec], "#!/usr/bin/env bash\n" \
1413
+ (shebang = if @options[:shebang]&.present?
1414
+ "#{@options[:shebang]} #{@options[:shell]}\n"
1415
+ else
1416
+ ''
1417
+ end
1418
+ ).tap_inspect name: :shebang
1419
+ File.write(@options[:saved_filespec], shebang +
876
1420
  "# file_name: #{opts[:filename]}\n" \
877
1421
  "# block_name: #{opts[:block_name]}\n" \
878
- "# time: #{Time.now.utc}\n" \
1422
+ "# time: #{time_now}\n" \
879
1423
  "#{required_blocks.flatten.join("\n")}\n")
1424
+ return if @options[:saved_script_chmod].zero?
1425
+
1426
+ File.chmod @options[:saved_script_chmod], @options[:saved_filespec]
880
1427
  end
881
1428
  end
882
1429
  end