markdown_exec 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/markdown_exec.rb CHANGED
@@ -3,20 +3,42 @@
3
3
 
4
4
  # encoding=utf-8
5
5
 
6
- $pdebug = !(ENV['MARKDOWN_EXEC_DEBUG'] || '').empty?
7
-
8
6
  require 'open3'
9
7
  require 'optparse'
10
8
  require 'tty-prompt'
11
9
  require 'yaml'
12
10
 
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 (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 (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
+ ENV[name] || default
32
+ end
33
+
34
+ $pdebug = env_bool 'MDE_DEBUG'
35
+
13
36
  require_relative 'markdown_exec/version'
14
37
 
15
38
  $stderr.sync = true
16
39
  $stdout.sync = true
17
40
 
18
41
  BLOCK_SIZE = 1024
19
- SELECT_PAGE_HEIGHT = 12
20
42
 
21
43
  class Object # rubocop:disable Style/Documentation
22
44
  def present?
@@ -31,6 +53,30 @@ class String # rubocop:disable Style/Documentation
31
53
  end
32
54
  end
33
55
 
56
+ public
57
+
58
+ # debug output
59
+ #
60
+ def tap_inspect(format: nil, name: 'return')
61
+ return self unless $pdebug
62
+
63
+ fn = case format
64
+ when :json
65
+ :to_json
66
+ when :string
67
+ :to_s
68
+ when :yaml
69
+ :to_yaml
70
+ else
71
+ :inspect
72
+ end
73
+
74
+ puts "-> #{caller[0].scan(/in `?(\S+)'$/)[0][0]}()" \
75
+ " #{name}: #{method(fn).call}"
76
+
77
+ self
78
+ end
79
+
34
80
  module MarkdownExec
35
81
  class Error < StandardError; end
36
82
 
@@ -41,6 +87,55 @@ module MarkdownExec
41
87
 
42
88
  def initialize(options = {})
43
89
  @options = options
90
+ @prompt = TTY::Prompt.new(interrupt: :exit)
91
+ end
92
+
93
+ ##
94
+ # options necessary to start, parse input, defaults for cli options
95
+
96
+ def base_options
97
+ {
98
+ # commands
99
+ list_blocks: false, # command
100
+ list_docs: false, # command
101
+
102
+ # command options
103
+ filename: env_str('MDE_FILENAME', default: nil), # option Filename to open
104
+ output_execution_summary: env_bool('MDE_OUTPUT_EXECUTION_SUMMARY', default: false), # option
105
+ output_script: env_bool('MDE_OUTPUT_SCRIPT', default: false), # option
106
+ output_stdout: env_bool('MDE_OUTPUT_STDOUT', default: true), # option
107
+ path: env_str('MDE_PATH', default: nil), # option Folder to search for files
108
+ save_executed_script: env_bool('MDE_SAVE_EXECUTED_SCRIPT', default: false), # option
109
+ saved_script_folder: env_str('MDE_SAVED_SCRIPT_FOLDER', default: 'logs'), # option
110
+ user_must_approve: env_bool('MDE_USER_MUST_APPROVE', default: true), # option Pause for user to approve script
111
+
112
+ # configuration options
113
+ block_name_excluded_match: env_str('MDE_BLOCK_NAME_EXCLUDED_MATCH', default: '^\(.+\)$'),
114
+ block_name_match: env_str('MDE_BLOCK_NAME_MATCH', default: ':(?<title>\S+)( |$)'),
115
+ block_required_scan: env_str('MDE_BLOCK_REQUIRED_SCAN', default: '\+\S+'),
116
+ fenced_start_and_end_match: env_str('MDE_FENCED_START_AND_END_MATCH', default: '^`{3,}'),
117
+ fenced_start_ex_match: env_str('MDE_FENCED_START_EX_MATCH', default: '^`{3,}(?<shell>[^`\s]*) *(?<name>.*)$'),
118
+ heading1_match: env_str('MDE_HEADING1_MATCH', default: '^# *(?<name>[^#]*?) *$'),
119
+ heading2_match: env_str('MDE_HEADING2_MATCH', default: '^## *(?<name>[^#]*?) *$'),
120
+ heading3_match: env_str('MDE_HEADING3_MATCH', default: '^### *(?<name>.+?) *$'),
121
+ md_filename_glob: env_str('MDE_MD_FILENAME_GLOB', default: '*.[Mm][Dd]'),
122
+ md_filename_match: env_str('MDE_MD_FILENAME_MATCH', default: '.+\\.md'),
123
+ mdheadings: true, # use headings (levels 1,2,3) in block lable
124
+ select_page_height: env_int('MDE_SELECT_PAGE_HEIGHT', default: 12)
125
+ }
126
+ end
127
+
128
+ def default_options
129
+ {
130
+ bash: true, # bash block parsing in get_block_summary()
131
+ exclude_expect_blocks: true,
132
+ exclude_matching_block_names: true, # exclude hidden blocks
133
+ output_saved_script_filename: false,
134
+ prompt_select_block: 'Choose a block:', # in select_and_approve_block()
135
+ prompt_select_md: 'Choose a file:', # in select_md_file()
136
+ saved_script_filename: nil, # calculated
137
+ struct: true # allow get_block_summary()
138
+ }
44
139
  end
45
140
 
46
141
  # Returns true if all files are EOF
@@ -49,16 +144,130 @@ module MarkdownExec
49
144
  files.find { |f| !f.eof }.nil?
50
145
  end
51
146
 
147
+ def approve_block(opts, blocks_in_file)
148
+ required_blocks = list_recursively_required_blocks(blocks_in_file, opts[:block_name])
149
+ display_command(opts, required_blocks) if opts[:output_script] || opts[:user_must_approve]
150
+
151
+ allow = true
152
+ allow = @prompt.yes? 'Process?' if opts[:user_must_approve]
153
+ opts[:ir_approve] = allow
154
+ selected = get_block_by_name blocks_in_file, opts[:block_name]
155
+
156
+ if opts[:ir_approve]
157
+ write_command_file(opts, required_blocks) if opts[:save_executed_script]
158
+ command_execute opts, required_blocks.flatten.join("\n")
159
+ end
160
+
161
+ selected[:name]
162
+ end
163
+
164
+ def code(table, block)
165
+ all = [block[:name]] + recursively_required(table, block[:reqs])
166
+ all.reverse.map do |req|
167
+ get_block_by_name(table, req).fetch(:body, '')
168
+ end
169
+ .flatten(1)
170
+ .tap_inspect
171
+ end
172
+
173
+ def command_execute(opts, cmd2)
174
+ @execute_options = opts
175
+ @execute_started_at = Time.now.utc
176
+ Open3.popen3(cmd2) do |stdin, stdout, stderr|
177
+ stdin.close_write
178
+ begin
179
+ files = [stdout, stderr]
180
+
181
+ until all_at_eof(files)
182
+ ready = IO.select(files)
183
+
184
+ next unless ready
185
+
186
+ # readable = ready[0]
187
+ # # writable = ready[1]
188
+ # # exceptions = ready[2]
189
+ @execute_files = Hash.new([])
190
+ ready.each.with_index do |readable, ind|
191
+ readable.each do |f|
192
+ block = f.read_nonblock(BLOCK_SIZE)
193
+ @execute_files[ind] = @execute_files[ind] + [block]
194
+ print block if opts[:output_stdout]
195
+ rescue EOFError #=> e
196
+ # do nothing at EOF
197
+ end
198
+ end
199
+ end
200
+ rescue IOError => e
201
+ fout "IOError: #{e}"
202
+ end
203
+ @execute_completed_at = Time.now.utc
204
+ end
205
+ rescue Errno::ENOENT => e
206
+ @execute_aborted_at = Time.now.utc
207
+ @execute_error_message = e.message
208
+ @execute_error = e
209
+ fout "Error ENOENT: #{e.inspect}"
210
+ end
211
+
52
212
  def count_blocks_in_filename
213
+ fenced_start_and_end_match = Regexp.new @options[:fenced_start_and_end_match]
53
214
  cnt = 0
54
- File.readlines(options[:filename]).each do |line|
55
- cnt += 1 if line.match(/^```/)
215
+ File.readlines(@options[:filename]).each do |line|
216
+ cnt += 1 if line.match(fenced_start_and_end_match)
56
217
  end
57
218
  cnt / 2
58
219
  end
59
220
 
221
+ def display_command(_opts, required_blocks)
222
+ required_blocks.each { |cb| fout cb }
223
+ end
224
+
225
+ def exec_block(options, block_name = '')
226
+ options = default_options.merge options
227
+ update_options options, over: false
228
+
229
+ # document and block reports
230
+ #
231
+ files = list_files_per_options(options)
232
+ if @options[:list_docs]
233
+ fout_list files
234
+ return
235
+ end
236
+
237
+ if @options[:list_blocks]
238
+ fout_list (files.map do |file|
239
+ make_block_labels(filename: file, struct: true)
240
+ end).flatten(1)
241
+ return
242
+ end
243
+
244
+ # process
245
+ #
246
+ select_and_approve_block(
247
+ bash: true,
248
+ block_name: block_name,
249
+ filename: select_md_file(files),
250
+ struct: true
251
+ )
252
+
253
+ fout "saved_filespec: #{@execute_script_filespec}" if @options[:output_saved_script_filename]
254
+
255
+ output_execution_summary if @options[:output_execution_summary]
256
+ end
257
+
258
+ # standard output; not for debug
259
+ #
60
260
  def fout(str)
61
- puts str # to stdout
261
+ puts str
262
+ end
263
+
264
+ def fout_list(str)
265
+ puts str
266
+ end
267
+
268
+ def fout_section(name, data)
269
+ puts "# #{name}"
270
+ puts data.to_yaml
62
271
  end
63
272
 
64
273
  def get_block_by_name(table, name, default = {})
@@ -70,11 +279,11 @@ module MarkdownExec
70
279
 
71
280
  return [summarize_block(headings, block_title).merge({ body: current })] unless opts[:bash]
72
281
 
73
- bm = block_title.match(/:(\S+)( |$)/)
74
- reqs = block_title.scan(/\+\S+/).map { |s| s[1..] }
282
+ bm = block_title.match(Regexp.new(opts[:block_name_match]))
283
+ reqs = block_title.scan(Regexp.new(opts[:block_required_scan])).map { |s| s[1..] }
75
284
 
76
285
  if bm && bm[1]
77
- [summarize_block(headings, bm[1]).merge({ body: current, reqs: reqs })]
286
+ [summarize_block(headings, bm[:title]).merge({ body: current, reqs: reqs })]
78
287
  else
79
288
  [summarize_block(headings, block_title).merge({ body: current, reqs: reqs })]
80
289
  end
@@ -88,136 +297,120 @@ module MarkdownExec
88
297
  exit 1
89
298
  end
90
299
 
300
+ fenced_start_and_end_match = Regexp.new opts[:fenced_start_and_end_match]
301
+ fenced_start_ex = Regexp.new opts[:fenced_start_ex_match]
302
+ block_title = ''
91
303
  blocks = []
92
304
  current = nil
93
- in_block = false
94
- block_title = ''
95
-
96
305
  headings = []
306
+ in_block = false
97
307
  File.readlines(opts[:filename]).each do |line|
98
308
  continue unless line
99
309
 
100
310
  if opts[:mdheadings]
101
- if (lm = line.match(/^### *(.+?) *$/))
102
- headings = [headings[0], headings[1], lm[1]]
103
- elsif (lm = line.match(/^## *([^#]*?) *$/))
104
- headings = [headings[0], lm[1]]
105
- elsif (lm = line.match(/^# *([^#]*?) *$/))
106
- headings = [lm[1]]
311
+ if (lm = line.match(Regexp.new(opts[:heading3_match])))
312
+ headings = [headings[0], headings[1], lm[:name]]
313
+ elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
314
+ headings = [headings[0], lm[:name]]
315
+ elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
316
+ headings = [lm[:name]]
107
317
  end
108
318
  end
109
319
 
110
- if line.match(/^`{3,}/)
320
+ if line.match(fenced_start_and_end_match)
111
321
  if in_block
112
322
  if current
113
-
114
323
  block_title = current.join(' ').gsub(/ +/, ' ')[0..64] if block_title.nil? || block_title.empty?
115
-
116
324
  blocks += get_block_summary opts, headings, block_title, current
117
325
  current = nil
118
326
  end
119
327
  in_block = false
120
328
  block_title = ''
121
329
  else
122
- ## new block
330
+ # new block
123
331
  #
124
-
125
- lm = line.match(/^`{3,}([^`\s]*) *(.*)$/)
332
+ lm = line.match(fenced_start_ex)
126
333
  do1 = false
127
334
  if opts[:bash_only]
128
- do1 = true if lm && (lm[1] == 'bash')
335
+ do1 = true if lm && (lm[:shell] == 'bash')
129
336
  else
130
337
  do1 = true
131
- do1 = !(lm && (lm[1] == 'expect')) if opts[:exclude_expect_blocks]
132
-
133
- # if do1 && opts[:exclude_matching_block_names]
134
- # puts " MW a4"
135
- # puts " MW a4 #{(lm[2].match %r{^:\(.+\)$})}"
136
- # do1 = !(lm && (lm[2].match %r{^:\(.+\)$}))
137
- # end
338
+ do1 = !(lm && (lm[:shell] == 'expect')) if opts[:exclude_expect_blocks]
138
339
  end
139
340
 
140
341
  in_block = true
141
- if do1 && (!opts[:title_match] || (lm && lm[2] && lm[2].match(opts[:title_match])))
342
+ if do1 && (!opts[:title_match] || (lm && lm[:name] && lm[:name].match(opts[:title_match])))
142
343
  current = []
143
- block_title = (lm && lm[2])
344
+ block_title = (lm && lm[:name])
144
345
  end
145
346
  end
146
347
  elsif current
147
348
  current += [line.chomp]
148
349
  end
149
350
  end
150
- blocks.tap { |ret| puts "list_blocks_in_file() ret: #{ret.inspect}" if $pdebug }
151
- # blocks.map do |block|
152
- # next if opts[:exclude_matching_block_names] && block[:name].match(%r{^\(.+\)$})
153
- # block
154
- # end.compact.tap { |ret| puts "list_blocks_in_file() ret: #{ret.inspect}" if $pdebug }
351
+ blocks.tap_inspect
155
352
  end
156
353
 
157
354
  def list_files_per_options(options)
158
355
  default_filename = 'README.md'
159
356
  default_folder = '.'
160
357
  if options[:filename]&.present?
161
- list_files_specified(options[:filename], options[:folder], default_filename, default_folder)
358
+ list_files_specified(options[:filename], options[:path], default_filename, default_folder)
162
359
  else
163
- list_files_specified(nil, options[:folder], default_filename, default_folder)
164
- end
360
+ list_files_specified(nil, options[:path], default_filename, default_folder)
361
+ end.tap_inspect
165
362
  end
166
363
 
167
364
  def list_files_specified(specified_filename, specified_folder, default_filename, default_folder, filetree = nil)
168
- fn = if specified_filename&.present?
169
- if specified_folder&.present?
170
- "#{specified_folder}/#{specified_filename}"
171
- else
172
- "#{default_folder}/#{specified_filename}"
173
- end
174
- elsif specified_folder&.present?
175
- if filetree
176
- "#{specified_folder}/.+\\.md"
177
- else
178
- "#{specified_folder}/*.[Mm][Dd]"
179
- end
180
- else
181
- "#{default_folder}/#{default_filename}"
182
- end
365
+ fn = File.join(if specified_filename&.present?
366
+ if specified_folder&.present?
367
+ [specified_folder, specified_filename]
368
+ elsif specified_filename.start_with? '/'
369
+ [specified_filename]
370
+ else
371
+ [default_folder, specified_filename]
372
+ end
373
+ elsif specified_folder&.present?
374
+ if filetree
375
+ [specified_folder, @options[:md_filename_match]]
376
+ else
377
+ [specified_folder, @options[:md_filename_glob]]
378
+ end
379
+ else
380
+ [default_folder, default_filename]
381
+ end)
183
382
  if filetree
184
383
  filetree.select { |filename| filename == fn || filename.match(/^#{fn}$/) || filename.match(%r{^#{fn}/.+$}) }
185
384
  else
186
385
  Dir.glob(fn)
187
- end.tap { |ret| puts "list_files_specified() ret: #{ret.inspect}" if $pdebug }
386
+ end.tap_inspect
188
387
  end
189
388
 
190
- def list_markdown_files_in_folder
191
- Dir.glob(File.join(options[:folder], '*.md'))
389
+ def list_markdown_files_in_path
390
+ Dir.glob(File.join(@options[:path], @options[:md_filename_glob])).tap_inspect
192
391
  end
193
392
 
194
393
  def list_named_blocks_in_file(call_options = {}, &options_block)
195
394
  opts = optsmerge call_options, options_block
395
+ block_name_excluded_match = Regexp.new opts[:block_name_excluded_match]
196
396
  list_blocks_in_file(opts).map do |block|
197
- next if opts[:exclude_matching_block_names] && block[:name].match(/^\(.+\)$/)
397
+ next if opts[:exclude_matching_block_names] && block[:name].match(block_name_excluded_match)
198
398
 
199
399
  block
200
- end.compact.tap { |ret| puts "list_named_blocks_in_file() ret: #{ret.inspect}" if $pdebug }
201
- end
202
-
203
- def code(table, block)
204
- all = [block[:name]] + recursively_required(table, block[:reqs])
205
- all.reverse.map do |req|
206
- get_block_by_name(table, req).fetch(:body, '')
207
- end
208
- .flatten(1)
209
- .tap { |ret| puts "code() ret: #{ret.inspect}" if $pdebug }
400
+ end.compact.tap_inspect
210
401
  end
211
402
 
212
403
  def list_recursively_required_blocks(table, name)
213
404
  name_block = get_block_by_name(table, name)
405
+ raise "Named code block `#{name}` not found." if name_block.nil? || name_block.keys.empty?
406
+
214
407
  all = [name_block[:name]] + recursively_required(table, name_block[:reqs])
215
408
 
216
409
  # in order of appearance in document
217
410
  table.select { |block| all.include? block[:name] }
218
411
  .map { |block| block.fetch(:body, '') }
219
412
  .flatten(1)
220
- .tap { |ret| puts "list_recursively_required_blocks() ret: #{ret.inspect}" if $pdebug }
413
+ .tap_inspect
221
414
  end
222
415
 
223
416
  def make_block_label(block, call_options = {})
@@ -236,34 +429,47 @@ module MarkdownExec
236
429
  # next if opts[:exclude_matching_block_names] && block[:name].match(%r{^:\(.+\)$})
237
430
 
238
431
  make_block_label block, opts
239
- end.compact.tap { |ret| puts "make_block_labels() ret: #{ret.inspect}" if $pdebug }
432
+ end.compact.tap_inspect
240
433
  end
241
434
 
242
435
  def option_exclude_blocks(opts, blocks)
436
+ block_name_excluded_match = Regexp.new opts[:block_name_excluded_match]
243
437
  if opts[:exclude_matching_block_names]
244
- blocks.reject { |block| block[:name].match(/^\(.+\)$/) }
438
+ blocks.reject { |block| block[:name].match(block_name_excluded_match) }
245
439
  else
246
440
  blocks
247
441
  end
248
442
  end
249
443
 
250
444
  def optsmerge(call_options = {}, options_block = nil)
251
- class_call_options = options.merge(call_options || {})
445
+ class_call_options = @options.merge(call_options || {})
252
446
  if options_block
253
447
  options_block.call class_call_options
254
448
  else
255
449
  class_call_options
256
- end.tap { |ret| puts "optsmerge() ret: #{ret.inspect}" if $pdebug }
450
+ end.tap_inspect
451
+ end
452
+
453
+ def output_execution_summary
454
+ fout_section 'summary', {
455
+ execute_aborted_at: @execute_aborted_at,
456
+ execute_completed_at: @execute_completed_at,
457
+ execute_error: @execute_error,
458
+ execute_error_message: @execute_error_message,
459
+ execute_files: @execute_files,
460
+ execute_options: @execute_options,
461
+ execute_started_at: @execute_started_at,
462
+ execute_script_filespec: @execute_script_filespec
463
+ }
257
464
  end
258
465
 
259
466
  def read_configuration_file!(options, configuration_path)
260
- if File.exist?(configuration_path)
261
- # rubocop:disable Security/YAMLLoad
262
- options.merge!((YAML.load(File.open(configuration_path)) || {})
263
- .transform_keys(&:to_sym))
264
- # rubocop:enable Security/YAMLLoad
265
- end
266
- options
467
+ return unless File.exist?(configuration_path)
468
+
469
+ # rubocop:disable Security/YAMLLoad
470
+ options.merge!((YAML.load(File.open(configuration_path)) || {})
471
+ .transform_keys(&:to_sym))
472
+ # rubocop:enable Security/YAMLLoad
267
473
  end
268
474
 
269
475
  def recursively_required(table, reqs)
@@ -278,231 +484,177 @@ module MarkdownExec
278
484
  end
279
485
  .compact
280
486
  .flatten(1)
281
- .tap { |_ret| puts "recursively_required() rem: #{rem.inspect}" if $pdebug }
487
+ .tap_inspect(name: 'rem')
282
488
  end
283
- all.tap { |ret| puts "recursively_required() ret: #{ret.inspect}" if $pdebug }
489
+ all.tap_inspect
284
490
  end
285
491
 
286
492
  def run
287
493
  ## default configuration
288
494
  #
289
- options = {
290
- mdheadings: true,
291
- list_blocks: false,
292
- list_docs: false
293
- }
495
+ @options = base_options
294
496
 
295
497
  ## post-parse options configuration
296
498
  #
297
499
  options_finalize = ->(_options) {}
298
500
 
501
+ proc_self = ->(value) { value }
502
+ proc_to_i = ->(value) { value.to_i != 0 }
503
+ proc_true = ->(_) { true }
504
+
299
505
  # read local configuration file
300
506
  #
301
- read_configuration_file! options, ".#{MarkdownExec::APP_NAME.downcase}.yml"
507
+ read_configuration_file! @options, ".#{MarkdownExec::APP_NAME.downcase}.yml"
302
508
 
303
509
  option_parser = OptionParser.new do |opts|
304
510
  executable_name = File.basename($PROGRAM_NAME)
305
511
  opts.banner = [
306
- "#{MarkdownExec::APP_NAME} - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})",
307
- "Usage: #{executable_name} [filename or path] [options]"
512
+ "#{MarkdownExec::APP_NAME}" \
513
+ " - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})",
514
+ "Usage: #{executable_name} [path] [filename] [options]"
308
515
  ].join("\n")
309
516
 
310
- ## menu top: items appear in reverse order added
311
- #
312
- opts.on('--config PATH', 'Read configuration file') do |value|
313
- read_configuration_file! options, value
314
- end
315
-
316
- ## menu body: items appear in order added
317
- #
318
- opts.on('-f RELATIVE', '--filename', 'Name of document') do |value|
319
- options[:filename] = value
320
- end
321
-
322
- opts.on('-p PATH', '--path', 'Path to documents') do |value|
323
- options[:folder] = value
324
- end
325
-
326
- opts.on('--list-blocks', 'List blocks') do |_value|
327
- options[:list_blocks] = true
328
- end
329
-
330
- opts.on('--list-docs', 'List docs in current folder') do |_value|
331
- options[:list_docs] = true
332
- end
333
-
334
- ## menu bottom: items appear in order added
335
- #
336
- opts.on_tail('-h', '--help', 'App help') do |_value|
337
- fout option_parser.help
338
- exit
339
- end
340
-
341
- opts.on_tail('-v', '--version', 'App version') do |_value|
342
- fout MarkdownExec::VERSION
343
- exit
344
- end
345
-
346
- opts.on_tail('-x', '--exit', 'Exit app') do |_value|
347
- exit
348
- end
349
-
350
- opts.on_tail('-0', 'Show configuration') do |_v|
351
- options_finalize.call options
352
- fout options.to_yaml
517
+ summary_head = [
518
+ ['config', nil, nil, 'PATH', 'Read configuration file',
519
+ nil, ->(value) { read_configuration_file! options, value }],
520
+ ['debug', 'd', 'MDE_DEBUG', 'BOOL', 'Debug output',
521
+ nil, ->(value) { $pdebug = value.to_i != 0 }]
522
+ ]
523
+
524
+ summary_body = [
525
+ ['filename', 'f', 'MDE_FILENAME', 'RELATIVE', 'Name of document',
526
+ :filename, proc_self],
527
+ ['list-blocks', nil, nil, nil, 'List blocks',
528
+ :list_blocks, proc_true],
529
+ ['list-docs', nil, nil, nil, 'List docs in current folder',
530
+ :list_docs, proc_true],
531
+ ['output-execution-summary', nil, 'MDE_OUTPUT_EXECUTION_SUMMARY', 'BOOL', 'Display summary for execution',
532
+ :output_execution_summary, proc_to_i],
533
+ ['output-script', nil, 'MDE_OUTPUT_SCRIPT', 'BOOL', 'Display script',
534
+ :output_script, proc_to_i],
535
+ ['output-stdout', nil, 'MDE_OUTPUT_STDOUT', 'BOOL', 'Display standard output from execution',
536
+ :output_stdout, proc_to_i],
537
+ ['path', 'p', 'MDE_PATH', 'PATH', 'Path to documents',
538
+ :path, proc_self],
539
+ ['save-executed-script', nil, 'MDE_SAVE_EXECUTED_SCRIPT', 'BOOL', 'Save executed script',
540
+ :save_executed_script, proc_to_i],
541
+ ['saved-script-folder', nil, 'MDE_SAVED_SCRIPT_FOLDER', 'SPEC', 'Saved script folder',
542
+ :saved_script_folder, proc_self],
543
+ ['user-must-approve', nil, 'MDE_USER_MUST_APPROVE', 'BOOL', 'Pause to approve execution',
544
+ :user_must_approve, proc_to_i]
545
+ ]
546
+
547
+ # rubocop:disable Style/Semicolon
548
+ summary_tail = [
549
+ [nil, '0', nil, nil, 'Show configuration',
550
+ nil, ->(_) { options_finalize.call options; fout options.to_yaml }],
551
+ ['help', 'h', nil, nil, 'App help',
552
+ nil, ->(_) { fout option_parser.help; exit }],
553
+ ['version', 'v', nil, nil, 'App version',
554
+ nil, ->(_) { fout MarkdownExec::VERSION; exit }],
555
+ ['exit', 'x', nil, nil, 'Exit app',
556
+ nil, ->(_) { exit }]
557
+ ]
558
+ # rubocop:enable Style/Semicolon
559
+
560
+ (summary_head + summary_body + summary_tail)
561
+ .map do |long_name, short_name, env_var, arg_name, description, opt_name, proc1| # rubocop:disable Metrics/ParameterLists
562
+ opts.on(*[long_name.present? ? "--#{long_name}#{arg_name.present? ? (' ' + arg_name) : ''}" : nil,
563
+ short_name.present? ? "-#{short_name}" : nil,
564
+ [description,
565
+ env_var.present? ? "env: #{env_var}" : nil].compact.join(' - '),
566
+ lambda { |value|
567
+ ret = proc1.call(value)
568
+ options[opt_name] = ret if opt_name
569
+ ret
570
+ }].compact)
353
571
  end
354
572
  end
355
573
  option_parser.load # filename defaults to basename of the program without suffix in a directory ~/.options
356
574
  option_parser.environment # env defaults to the basename of the program.
357
575
  rest = option_parser.parse! # (into: options)
358
- options_finalize.call options
359
576
 
360
- if rest.fetch(0, nil)&.present?
361
- if Dir.exist?(rest[0])
362
- options[:folder] = rest[0]
363
- elsif File.exist?(rest[0])
364
- options[:filename] = rest[0]
365
- end
366
- end
367
-
368
- ## process
577
+ ## finalize configuration
369
578
  #
370
- options.merge!(
371
- {
372
- approve: true,
373
- bash: true,
374
- display: true,
375
- exclude_expect_blocks: true,
376
- exclude_matching_block_names: true,
377
- execute: true,
378
- prompt: 'Execute',
379
- struct: true
380
- }
381
- )
382
- mp = MarkParse.new options
579
+ options_finalize.call options
383
580
 
384
- ## show
581
+ ## position 0: file or folder (optional)
385
582
  #
386
- if options[:list_docs]
387
- fout mp.list_files_per_options options
388
- return
389
- end
390
-
391
- if options[:list_blocks]
392
- fout (mp.list_files_per_options(options).map do |file|
393
- mp.make_block_labels(filename: file, struct: true)
394
- end).flatten(1)
395
- return
396
- end
397
-
398
- mp.select_block(
399
- bash: true,
400
- filename: select_md_file(list_files_per_options(options)),
401
- struct: true
402
- )
403
- end
404
-
405
- def select_block(call_options = {}, &options_block)
406
- opts = optsmerge call_options, options_block
407
-
408
- blocks = list_blocks_in_file(opts.merge(struct: true))
409
-
410
- prompt = TTY::Prompt.new(interrupt: :exit)
411
- pt = "#{opts.fetch(:prompt, nil) || 'Pick one'}:"
412
-
413
- # blocks.map do |block|
414
- # next if opts[:exclude_matching_block_names] && block[:name].match(%r{^\(.+\)$})
415
- # block
416
- # end.compact.tap { |ret| puts "list_blocks_in_file() ret: #{ret.inspect}" if $pdebug }
417
-
418
- blocks.each { |block| block.merge! label: make_block_label(block, opts) }
419
- # block_labels = blocks.map { |block| block[:label] }
420
- block_labels = option_exclude_blocks(opts, blocks).map { |block| block[:label] }
421
-
422
- if opts[:preview_options]
423
- select_per_page = 3
424
- block_labels.each do |bn|
425
- fout " - #{bn}"
583
+ if (pos = rest.fetch(0, nil))&.present?
584
+ if Dir.exist?(pos)
585
+ options[:path] = pos
586
+ elsif File.exist?(pos)
587
+ options[:filename] = pos
588
+ else
589
+ raise "Invalid parameter: #{pos}"
426
590
  end
427
- else
428
- select_per_page = SELECT_PAGE_HEIGHT
429
591
  end
430
592
 
431
- return nil if block_labels.count.zero?
432
-
433
- sel = prompt.select(pt, block_labels, per_page: select_per_page)
434
-
435
- label_block = blocks.select { |block| block[:label] == sel }.fetch(0, nil)
436
- sel = label_block[:name]
437
-
438
- cbs = list_recursively_required_blocks(blocks, sel)
439
-
440
- ## display code blocks for approval
593
+ ## position 1: block name (optional)
441
594
  #
442
- cbs.each { |cb| fout cb } if opts[:display] || opts[:approve]
595
+ block_name = rest.fetch(1, nil)
443
596
 
444
- allow = true
445
- allow = prompt.yes? 'Process?' if opts[:approve]
446
-
447
- selected = get_block_by_name blocks, sel
448
- if allow && opts[:execute]
449
-
450
- cmd2 = cbs.flatten.join("\n")
451
-
452
- Open3.popen3(cmd2) do |stdin, stdout, stderr|
453
- stdin.close_write
454
- begin
455
- files = [stdout, stderr]
597
+ exec_block options, block_name
598
+ end
456
599
 
457
- until all_at_eof(files)
458
- ready = IO.select(files)
600
+ def select_and_approve_block(call_options = {}, &options_block)
601
+ opts = optsmerge call_options, options_block
602
+ blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
459
603
 
460
- next unless ready
604
+ unless opts[:block_name].present?
605
+ pt = (opts[:prompt_select_block]).to_s
606
+ blocks_in_file.each { |block| block.merge! label: make_block_label(block, opts) }
607
+ block_labels = option_exclude_blocks(opts, blocks_in_file).map { |block| block[:label] }
461
608
 
462
- readable = ready[0]
463
- # writable = ready[1]
464
- # exceptions = ready[2]
609
+ return nil if block_labels.count.zero?
465
610
 
466
- readable.each do |f|
467
- print f.read_nonblock(BLOCK_SIZE)
468
- rescue EOFError #=> e
469
- # do nothing at EOF
470
- end
471
- end
472
- rescue IOError => e
473
- fout "IOError: #{e}"
474
- end
475
- end
611
+ sel = @prompt.select(pt, block_labels, per_page: opts[:select_page_height])
612
+ label_block = blocks_in_file.select { |block| block[:label] == sel }.fetch(0, nil)
613
+ opts[:block_name] = label_block[:name]
476
614
  end
477
615
 
478
- selected[:name]
616
+ approve_block opts, blocks_in_file
479
617
  end
480
618
 
481
619
  def select_md_file(files_ = nil)
482
620
  opts = options
483
- files = files_ || list_markdown_files_in_folder
621
+ files = files_ || list_markdown_files_in_path
484
622
  if files.count == 1
485
- sel = files[0]
623
+ files[0]
486
624
  elsif files.count >= 2
487
-
488
- if opts[:preview_options]
489
- select_per_page = 3
490
- files.each do |file|
491
- fout " - #{file}"
492
- end
493
- else
494
- select_per_page = SELECT_PAGE_HEIGHT
495
- end
496
-
497
- prompt = TTY::Prompt.new
498
- sel = prompt.select("#{opts.fetch(:prompt, 'Pick one')}:", files, per_page: select_per_page)
625
+ @prompt.select(opts[:prompt_select_md].to_s, files, per_page: opts[:select_page_height])
499
626
  end
500
-
501
- sel
502
627
  end
503
628
 
504
629
  def summarize_block(headings, title)
505
630
  { headings: headings, name: title, title: title }
506
631
  end
632
+
633
+ def update_options(opts = {}, over: true)
634
+ if over
635
+ @options = @options.merge opts
636
+ else
637
+ @options.merge! opts
638
+ end
639
+ @options
640
+ end
641
+
642
+ def write_command_file(opts, required_blocks)
643
+ return unless opts[:saved_script_filename].present?
644
+
645
+ fne = File.basename(opts[:filename], '.*').gsub(/[^a-z0-9]/i, '-') # scan(/[a-z0-9]/i).join
646
+ bne = opts[:block_name].gsub(/[^a-z0-9]/i, '-') # scan(/[a-z0-9]/i).join
647
+ opts[:saved_script_filename] = "mde_#{Time.now.utc.strftime '%F-%H-%M-%S'}_#{fne}_#{bne}.sh"
648
+
649
+ @options[:saved_filespec] = File.join opts[:saved_script_folder], opts[:saved_script_filename]
650
+ @execute_script_filespec = @options[:saved_filespec]
651
+ dirname = File.dirname(@options[:saved_filespec])
652
+ Dir.mkdir dirname unless File.exist?(dirname)
653
+ File.write(@options[:saved_filespec], "#!/usr/bin/env bash\n" \
654
+ "# file_name: #{opts[:filename]}\n" \
655
+ "# block_name: #{opts[:block_name]}\n" \
656
+ "# time: #{Time.now.utc}\n" \
657
+ "#{required_blocks.flatten.join("\n")}\n")
658
+ end
507
659
  end
508
660
  end