markdown_exec 0.2.0 → 0.2.4
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -0
- data/CHANGELOG.md +103 -1
- data/Gemfile +2 -0
- data/Gemfile.lock +7 -1
- data/README.md +29 -34
- data/Rakefile +18 -6
- data/assets/approve_code.png +0 -0
- data/assets/example_blocks.png +0 -0
- data/assets/output_of_execution.png +0 -0
- data/assets/select_a_block.png +0 -0
- data/assets/select_a_file.png +0 -0
- data/fixtures/bash1.md +12 -0
- data/fixtures/bash2.md +15 -0
- data/fixtures/exclude1.md +6 -0
- data/fixtures/exclude2.md +9 -0
- data/fixtures/exec1.md +8 -0
- data/fixtures/heading1.md +19 -0
- data/fixtures/sample1.md +9 -0
- data/fixtures/title1.md +6 -0
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +511 -230
- metadata +19 -10
- data/assets/approve.png +0 -0
- data/assets/blocks.png +0 -0
- data/assets/executed.png +0 -0
- data/assets/select.png +0 -0
- data/assets/select_file.png +0 -0
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,65 @@ 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
|
+
list_recent_scripts: false, # command
|
102
|
+
run_last_script: false, # command
|
103
|
+
select_recent_script: false, # command
|
104
|
+
|
105
|
+
# command options
|
106
|
+
filename: env_str('MDE_FILENAME', default: nil), # option Filename to open
|
107
|
+
list_count: 16,
|
108
|
+
logged_stdout_filename_prefix: 'mde',
|
109
|
+
output_execution_summary: env_bool('MDE_OUTPUT_EXECUTION_SUMMARY', default: false), # option
|
110
|
+
output_script: env_bool('MDE_OUTPUT_SCRIPT', default: false), # option
|
111
|
+
output_stdout: env_bool('MDE_OUTPUT_STDOUT', default: true), # option
|
112
|
+
path: env_str('MDE_PATH', default: nil), # option Folder to search for files
|
113
|
+
save_executed_script: env_bool('MDE_SAVE_EXECUTED_SCRIPT', default: false), # option
|
114
|
+
save_execution_output: env_bool('MDE_SAVE_EXECUTION_OUTPUT', default: false), # option
|
115
|
+
saved_script_filename_prefix: 'mde',
|
116
|
+
saved_script_folder: env_str('MDE_SAVED_SCRIPT_FOLDER', default: 'logs'), # option
|
117
|
+
saved_script_glob: 'mde_*.sh',
|
118
|
+
saved_stdout_folder: env_str('MDE_SAVED_STDOUT_FOLDER', default: 'logs'), # option
|
119
|
+
user_must_approve: env_bool('MDE_USER_MUST_APPROVE', default: true), # option Pause for user to approve script
|
120
|
+
|
121
|
+
# configuration options
|
122
|
+
block_name_excluded_match: env_str('MDE_BLOCK_NAME_EXCLUDED_MATCH', default: '^\(.+\)$'),
|
123
|
+
block_name_match: env_str('MDE_BLOCK_NAME_MATCH', default: ':(?<title>\S+)( |$)'),
|
124
|
+
block_required_scan: env_str('MDE_BLOCK_REQUIRED_SCAN', default: '\+\S+'),
|
125
|
+
fenced_start_and_end_match: env_str('MDE_FENCED_START_AND_END_MATCH', default: '^`{3,}'),
|
126
|
+
fenced_start_ex_match: env_str('MDE_FENCED_START_EX_MATCH', default: '^`{3,}(?<shell>[^`\s]*) *(?<name>.*)$'),
|
127
|
+
heading1_match: env_str('MDE_HEADING1_MATCH', default: '^# *(?<name>[^#]*?) *$'),
|
128
|
+
heading2_match: env_str('MDE_HEADING2_MATCH', default: '^## *(?<name>[^#]*?) *$'),
|
129
|
+
heading3_match: env_str('MDE_HEADING3_MATCH', default: '^### *(?<name>.+?) *$'),
|
130
|
+
md_filename_glob: env_str('MDE_MD_FILENAME_GLOB', default: '*.[Mm][Dd]'),
|
131
|
+
md_filename_match: env_str('MDE_MD_FILENAME_MATCH', default: '.+\\.md'),
|
132
|
+
mdheadings: true, # use headings (levels 1,2,3) in block lable
|
133
|
+
select_page_height: env_int('MDE_SELECT_PAGE_HEIGHT', default: 12)
|
134
|
+
}
|
135
|
+
end
|
136
|
+
|
137
|
+
def default_options
|
138
|
+
{
|
139
|
+
bash: true, # bash block parsing in get_block_summary()
|
140
|
+
exclude_expect_blocks: true,
|
141
|
+
hide_blocks_by_name: true,
|
142
|
+
output_saved_script_filename: false,
|
143
|
+
prompt_approve_block: 'Process?',
|
144
|
+
prompt_select_block: 'Choose a block:',
|
145
|
+
prompt_select_md: 'Choose a file:',
|
146
|
+
saved_script_filename: nil, # calculated
|
147
|
+
struct: true # allow get_block_summary()
|
148
|
+
}
|
44
149
|
end
|
45
150
|
|
46
151
|
# Returns true if all files are EOF
|
@@ -49,16 +154,145 @@ module MarkdownExec
|
|
49
154
|
files.find { |f| !f.eof }.nil?
|
50
155
|
end
|
51
156
|
|
157
|
+
def approve_block(opts, blocks_in_file)
|
158
|
+
required_blocks = list_recursively_required_blocks(blocks_in_file, opts[:block_name])
|
159
|
+
display_command(opts, required_blocks) if opts[:output_script] || opts[:user_must_approve]
|
160
|
+
|
161
|
+
allow = true
|
162
|
+
allow = @prompt.yes? opts[:prompt_approve_block] if opts[:user_must_approve]
|
163
|
+
opts[:ir_approve] = allow
|
164
|
+
selected = get_block_by_name blocks_in_file, opts[:block_name]
|
165
|
+
|
166
|
+
if opts[:ir_approve]
|
167
|
+
write_command_file(opts, required_blocks) if opts[:save_executed_script]
|
168
|
+
command_execute opts, required_blocks.flatten.join("\n")
|
169
|
+
end
|
170
|
+
|
171
|
+
selected[:name]
|
172
|
+
end
|
173
|
+
|
174
|
+
def code(table, block)
|
175
|
+
all = [block[:name]] + recursively_required(table, block[:reqs])
|
176
|
+
all.reverse.map do |req|
|
177
|
+
get_block_by_name(table, req).fetch(:body, '')
|
178
|
+
end
|
179
|
+
.flatten(1)
|
180
|
+
.tap_inspect
|
181
|
+
end
|
182
|
+
|
183
|
+
def command_execute(opts, cmd2)
|
184
|
+
@execute_files = Hash.new([])
|
185
|
+
@execute_options = opts
|
186
|
+
@execute_started_at = Time.now.utc
|
187
|
+
Open3.popen3(cmd2) do |stdin, stdout, stderr|
|
188
|
+
stdin.close_write
|
189
|
+
begin
|
190
|
+
files = [stdout, stderr]
|
191
|
+
|
192
|
+
until all_at_eof(files)
|
193
|
+
ready = IO.select(files)
|
194
|
+
|
195
|
+
next unless ready
|
196
|
+
|
197
|
+
# readable = ready[0]
|
198
|
+
# # writable = ready[1]
|
199
|
+
# # exceptions = ready[2]
|
200
|
+
ready.each.with_index do |readable, ind|
|
201
|
+
readable.each do |f|
|
202
|
+
block = f.read_nonblock(BLOCK_SIZE)
|
203
|
+
@execute_files[ind] = @execute_files[ind] + [block]
|
204
|
+
print block if opts[:output_stdout]
|
205
|
+
rescue EOFError #=> e
|
206
|
+
# do nothing at EOF
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
rescue IOError => e
|
211
|
+
fout "IOError: #{e}"
|
212
|
+
end
|
213
|
+
@execute_completed_at = Time.now.utc
|
214
|
+
end
|
215
|
+
rescue Errno::ENOENT => e
|
216
|
+
# error triggered by missing command in script
|
217
|
+
@execute_aborted_at = Time.now.utc
|
218
|
+
@execute_error_message = e.message
|
219
|
+
@execute_error = e
|
220
|
+
@execute_files[1] = e.message
|
221
|
+
fout "Error ENOENT: #{e.inspect}"
|
222
|
+
end
|
223
|
+
|
52
224
|
def count_blocks_in_filename
|
225
|
+
fenced_start_and_end_match = Regexp.new @options[:fenced_start_and_end_match]
|
53
226
|
cnt = 0
|
54
|
-
File.readlines(options[:filename]).each do |line|
|
55
|
-
cnt += 1 if line.match(
|
227
|
+
File.readlines(@options[:filename]).each do |line|
|
228
|
+
cnt += 1 if line.match(fenced_start_and_end_match)
|
56
229
|
end
|
57
230
|
cnt / 2
|
58
231
|
end
|
59
232
|
|
233
|
+
def display_command(_opts, required_blocks)
|
234
|
+
required_blocks.each { |cb| fout cb }
|
235
|
+
end
|
236
|
+
|
237
|
+
def exec_block(options, _block_name = '')
|
238
|
+
options = default_options.merge options
|
239
|
+
update_options options, over: false
|
240
|
+
|
241
|
+
# document and block reports
|
242
|
+
#
|
243
|
+
files = list_files_per_options(options)
|
244
|
+
if @options[:list_blocks]
|
245
|
+
fout_list (files.map do |file|
|
246
|
+
make_block_labels(filename: file, struct: true)
|
247
|
+
end).flatten(1)
|
248
|
+
return
|
249
|
+
end
|
250
|
+
|
251
|
+
if @options[:list_docs]
|
252
|
+
fout_list files
|
253
|
+
return
|
254
|
+
end
|
255
|
+
|
256
|
+
if @options[:list_recent_scripts]
|
257
|
+
fout_list list_recent_scripts
|
258
|
+
return
|
259
|
+
end
|
260
|
+
|
261
|
+
if @options[:run_last_script]
|
262
|
+
run_last_script
|
263
|
+
return
|
264
|
+
end
|
265
|
+
|
266
|
+
if @options[:select_recent_script]
|
267
|
+
select_recent_script
|
268
|
+
return
|
269
|
+
end
|
270
|
+
|
271
|
+
# process
|
272
|
+
#
|
273
|
+
@options[:filename] = select_md_file(files)
|
274
|
+
select_and_approve_block(
|
275
|
+
bash: true,
|
276
|
+
struct: true
|
277
|
+
)
|
278
|
+
fout "saved_filespec: #{@execute_script_filespec}" if @options[:output_saved_script_filename]
|
279
|
+
save_execution_output
|
280
|
+
output_execution_summary
|
281
|
+
end
|
282
|
+
|
283
|
+
# standard output; not for debug
|
284
|
+
#
|
60
285
|
def fout(str)
|
61
|
-
puts str
|
286
|
+
puts str
|
287
|
+
end
|
288
|
+
|
289
|
+
def fout_list(str)
|
290
|
+
puts str
|
291
|
+
end
|
292
|
+
|
293
|
+
def fout_section(name, data)
|
294
|
+
puts "# #{name}"
|
295
|
+
puts data.to_yaml
|
62
296
|
end
|
63
297
|
|
64
298
|
def get_block_by_name(table, name, default = {})
|
@@ -70,11 +304,11 @@ module MarkdownExec
|
|
70
304
|
|
71
305
|
return [summarize_block(headings, block_title).merge({ body: current })] unless opts[:bash]
|
72
306
|
|
73
|
-
bm = block_title.match(
|
74
|
-
reqs = block_title.scan(
|
307
|
+
bm = block_title.match(Regexp.new(opts[:block_name_match]))
|
308
|
+
reqs = block_title.scan(Regexp.new(opts[:block_required_scan])).map { |s| s[1..] }
|
75
309
|
|
76
310
|
if bm && bm[1]
|
77
|
-
[summarize_block(headings, bm[
|
311
|
+
[summarize_block(headings, bm[:title]).merge({ body: current, reqs: reqs })]
|
78
312
|
else
|
79
313
|
[summarize_block(headings, block_title).merge({ body: current, reqs: reqs })]
|
80
314
|
end
|
@@ -88,120 +322,125 @@ module MarkdownExec
|
|
88
322
|
exit 1
|
89
323
|
end
|
90
324
|
|
325
|
+
fenced_start_and_end_match = Regexp.new opts[:fenced_start_and_end_match]
|
326
|
+
fenced_start_ex = Regexp.new opts[:fenced_start_ex_match]
|
327
|
+
block_title = ''
|
91
328
|
blocks = []
|
92
329
|
current = nil
|
93
|
-
in_block = false
|
94
|
-
block_title = ''
|
95
|
-
|
96
330
|
headings = []
|
331
|
+
in_block = false
|
97
332
|
File.readlines(opts[:filename]).each do |line|
|
98
333
|
continue unless line
|
99
334
|
|
100
335
|
if opts[:mdheadings]
|
101
|
-
if (lm = line.match(
|
102
|
-
headings = [headings[0], headings[1], lm[
|
103
|
-
elsif (lm = line.match(
|
104
|
-
headings = [headings[0], lm[
|
105
|
-
elsif (lm = line.match(
|
106
|
-
headings = [lm[
|
336
|
+
if (lm = line.match(Regexp.new(opts[:heading3_match])))
|
337
|
+
headings = [headings[0], headings[1], lm[:name]]
|
338
|
+
elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
|
339
|
+
headings = [headings[0], lm[:name]]
|
340
|
+
elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
|
341
|
+
headings = [lm[:name]]
|
107
342
|
end
|
108
343
|
end
|
109
344
|
|
110
|
-
if line.match(
|
345
|
+
if line.match(fenced_start_and_end_match)
|
111
346
|
if in_block
|
112
347
|
if current
|
113
|
-
|
114
348
|
block_title = current.join(' ').gsub(/ +/, ' ')[0..64] if block_title.nil? || block_title.empty?
|
115
|
-
|
116
349
|
blocks += get_block_summary opts, headings, block_title, current
|
117
350
|
current = nil
|
118
351
|
end
|
119
352
|
in_block = false
|
120
353
|
block_title = ''
|
121
354
|
else
|
122
|
-
|
355
|
+
# new block
|
123
356
|
#
|
124
|
-
|
125
|
-
lm = line.match(/^`{3,}([^`\s]*) *(.*)$/)
|
126
|
-
|
357
|
+
lm = line.match(fenced_start_ex)
|
127
358
|
do1 = false
|
128
359
|
if opts[:bash_only]
|
129
|
-
do1 = true if lm && (lm[
|
130
|
-
elsif opts[:exclude_expect_blocks]
|
131
|
-
do1 = true unless lm && (lm[1] == 'expect')
|
360
|
+
do1 = true if lm && (lm[:shell] == 'bash')
|
132
361
|
else
|
133
362
|
do1 = true
|
363
|
+
do1 = !(lm && (lm[:shell] == 'expect')) if opts[:exclude_expect_blocks]
|
134
364
|
end
|
135
365
|
|
136
|
-
|
366
|
+
in_block = true
|
367
|
+
if do1 && (!opts[:title_match] || (lm && lm[:name] && lm[:name].match(opts[:title_match])))
|
137
368
|
current = []
|
138
|
-
|
139
|
-
block_title = (lm && lm[2])
|
369
|
+
block_title = (lm && lm[:name])
|
140
370
|
end
|
141
|
-
|
142
371
|
end
|
143
372
|
elsif current
|
144
373
|
current += [line.chomp]
|
145
374
|
end
|
146
375
|
end
|
147
|
-
blocks.
|
376
|
+
blocks.tap_inspect
|
148
377
|
end
|
149
378
|
|
150
379
|
def list_files_per_options(options)
|
151
380
|
default_filename = 'README.md'
|
152
381
|
default_folder = '.'
|
153
382
|
if options[:filename]&.present?
|
154
|
-
list_files_specified(options[:filename], options[:
|
383
|
+
list_files_specified(options[:filename], options[:path], default_filename, default_folder)
|
155
384
|
else
|
156
|
-
list_files_specified(nil, options[:
|
157
|
-
end
|
385
|
+
list_files_specified(nil, options[:path], default_filename, default_folder)
|
386
|
+
end.tap_inspect
|
158
387
|
end
|
159
388
|
|
160
389
|
def list_files_specified(specified_filename, specified_folder, default_filename, default_folder, filetree = nil)
|
161
|
-
fn = if specified_filename&.present?
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
390
|
+
fn = File.join(if specified_filename&.present?
|
391
|
+
if specified_folder&.present?
|
392
|
+
[specified_folder, specified_filename]
|
393
|
+
elsif specified_filename.start_with? '/'
|
394
|
+
[specified_filename]
|
395
|
+
else
|
396
|
+
[default_folder, specified_filename]
|
397
|
+
end
|
398
|
+
elsif specified_folder&.present?
|
399
|
+
if filetree
|
400
|
+
[specified_folder, @options[:md_filename_match]]
|
401
|
+
else
|
402
|
+
[specified_folder, @options[:md_filename_glob]]
|
403
|
+
end
|
404
|
+
else
|
405
|
+
[default_folder, default_filename]
|
406
|
+
end)
|
176
407
|
if filetree
|
177
408
|
filetree.select { |filename| filename == fn || filename.match(/^#{fn}$/) || filename.match(%r{^#{fn}/.+$}) }
|
178
409
|
else
|
179
410
|
Dir.glob(fn)
|
180
|
-
end.
|
411
|
+
end.tap_inspect
|
181
412
|
end
|
182
413
|
|
183
|
-
def
|
184
|
-
Dir.glob(File.join(options[:
|
414
|
+
def list_markdown_files_in_path
|
415
|
+
Dir.glob(File.join(@options[:path], @options[:md_filename_glob])).tap_inspect
|
185
416
|
end
|
186
417
|
|
187
|
-
def
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
418
|
+
def list_named_blocks_in_file(call_options = {}, &options_block)
|
419
|
+
opts = optsmerge call_options, options_block
|
420
|
+
block_name_excluded_match = Regexp.new opts[:block_name_excluded_match]
|
421
|
+
list_blocks_in_file(opts).map do |block|
|
422
|
+
next if opts[:hide_blocks_by_name] && block[:name].match(block_name_excluded_match)
|
423
|
+
|
424
|
+
block
|
425
|
+
end.compact.tap_inspect
|
194
426
|
end
|
195
427
|
|
196
428
|
def list_recursively_required_blocks(table, name)
|
197
429
|
name_block = get_block_by_name(table, name)
|
430
|
+
raise "Named code block `#{name}` not found." if name_block.nil? || name_block.keys.empty?
|
431
|
+
|
198
432
|
all = [name_block[:name]] + recursively_required(table, name_block[:reqs])
|
199
433
|
|
200
434
|
# in order of appearance in document
|
201
435
|
table.select { |block| all.include? block[:name] }
|
202
436
|
.map { |block| block.fetch(:body, '') }
|
203
437
|
.flatten(1)
|
204
|
-
.
|
438
|
+
.tap_inspect
|
439
|
+
end
|
440
|
+
|
441
|
+
def list_recent_scripts
|
442
|
+
Dir.glob(File.join(@options[:saved_script_folder],
|
443
|
+
@options[:saved_script_glob])).sort[0..(options[:list_count] - 1)].reverse.tap_inspect
|
205
444
|
end
|
206
445
|
|
207
446
|
def make_block_label(block, call_options = {})
|
@@ -217,27 +456,52 @@ module MarkdownExec
|
|
217
456
|
def make_block_labels(call_options = {})
|
218
457
|
opts = options.merge(call_options)
|
219
458
|
list_blocks_in_file(opts).map do |block|
|
459
|
+
# next if opts[:hide_blocks_by_name] && block[:name].match(%r{^:\(.+\)$})
|
460
|
+
|
220
461
|
make_block_label block, opts
|
462
|
+
end.compact.tap_inspect
|
463
|
+
end
|
464
|
+
|
465
|
+
def option_exclude_blocks(opts, blocks)
|
466
|
+
block_name_excluded_match = Regexp.new opts[:block_name_excluded_match]
|
467
|
+
if opts[:hide_blocks_by_name]
|
468
|
+
blocks.reject { |block| block[:name].match(block_name_excluded_match) }
|
469
|
+
else
|
470
|
+
blocks
|
221
471
|
end
|
222
472
|
end
|
223
473
|
|
224
474
|
def optsmerge(call_options = {}, options_block = nil)
|
225
|
-
class_call_options = options.merge(call_options || {})
|
475
|
+
class_call_options = @options.merge(call_options || {})
|
226
476
|
if options_block
|
227
477
|
options_block.call class_call_options
|
228
478
|
else
|
229
479
|
class_call_options
|
230
|
-
end.
|
480
|
+
end.tap_inspect
|
481
|
+
end
|
482
|
+
|
483
|
+
def output_execution_summary
|
484
|
+
return unless @options[:output_execution_summary]
|
485
|
+
|
486
|
+
fout_section 'summary', {
|
487
|
+
execute_aborted_at: @execute_aborted_at,
|
488
|
+
execute_completed_at: @execute_completed_at,
|
489
|
+
execute_error: @execute_error,
|
490
|
+
execute_error_message: @execute_error_message,
|
491
|
+
execute_files: @execute_files,
|
492
|
+
execute_options: @execute_options,
|
493
|
+
execute_started_at: @execute_started_at,
|
494
|
+
execute_script_filespec: @execute_script_filespec
|
495
|
+
}
|
231
496
|
end
|
232
497
|
|
233
498
|
def read_configuration_file!(options, configuration_path)
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
options
|
499
|
+
return unless File.exist?(configuration_path)
|
500
|
+
|
501
|
+
# rubocop:disable Security/YAMLLoad
|
502
|
+
options.merge!((YAML.load(File.open(configuration_path)) || {})
|
503
|
+
.transform_keys(&:to_sym))
|
504
|
+
# rubocop:enable Security/YAMLLoad
|
241
505
|
end
|
242
506
|
|
243
507
|
def recursively_required(table, reqs)
|
@@ -252,218 +516,235 @@ module MarkdownExec
|
|
252
516
|
end
|
253
517
|
.compact
|
254
518
|
.flatten(1)
|
255
|
-
.
|
519
|
+
.tap_inspect(name: 'rem')
|
256
520
|
end
|
257
|
-
all.
|
521
|
+
all.tap_inspect
|
258
522
|
end
|
259
523
|
|
260
524
|
def run
|
261
525
|
## default configuration
|
262
526
|
#
|
263
|
-
options =
|
264
|
-
mdheadings: true,
|
265
|
-
list_blocks: false,
|
266
|
-
list_docs: false
|
267
|
-
}
|
527
|
+
@options = base_options
|
268
528
|
|
269
529
|
## post-parse options configuration
|
270
530
|
#
|
271
531
|
options_finalize = ->(_options) {}
|
272
532
|
|
533
|
+
proc_self = ->(value) { value }
|
534
|
+
proc_to_i = ->(value) { value.to_i != 0 }
|
535
|
+
proc_true = ->(_) { true }
|
536
|
+
|
273
537
|
# read local configuration file
|
274
538
|
#
|
275
|
-
read_configuration_file! options, ".#{MarkdownExec::APP_NAME.downcase}.yml"
|
539
|
+
read_configuration_file! @options, ".#{MarkdownExec::APP_NAME.downcase}.yml"
|
276
540
|
|
277
541
|
option_parser = OptionParser.new do |opts|
|
278
542
|
executable_name = File.basename($PROGRAM_NAME)
|
279
543
|
opts.banner = [
|
280
|
-
"#{MarkdownExec::APP_NAME}
|
281
|
-
"
|
544
|
+
"#{MarkdownExec::APP_NAME}" \
|
545
|
+
" - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})",
|
546
|
+
"Usage: #{executable_name} [path] [filename] [options]"
|
282
547
|
].join("\n")
|
283
548
|
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
549
|
+
summary_head = [
|
550
|
+
['config', nil, nil, 'PATH', 'Read configuration file',
|
551
|
+
nil, ->(value) { read_configuration_file! options, value }],
|
552
|
+
['debug', 'd', 'MDE_DEBUG', 'BOOL', 'Debug output',
|
553
|
+
nil, ->(value) { $pdebug = value.to_i != 0 }]
|
554
|
+
]
|
555
|
+
|
556
|
+
summary_body = [
|
557
|
+
['filename', 'f', 'MDE_FILENAME', 'RELATIVE', 'Name of document',
|
558
|
+
:filename, proc_self],
|
559
|
+
['list-blocks', nil, nil, nil, 'List blocks',
|
560
|
+
:list_blocks, proc_true],
|
561
|
+
['list-docs', nil, nil, nil, 'List docs in current folder',
|
562
|
+
:list_docs, proc_true],
|
563
|
+
['list-recent-scripts', nil, nil, nil, 'List recent saved scripts',
|
564
|
+
:list_recent_scripts, proc_true],
|
565
|
+
['output-execution-summary', nil, 'MDE_OUTPUT_EXECUTION_SUMMARY', 'BOOL', 'Display summary for execution',
|
566
|
+
:output_execution_summary, proc_to_i],
|
567
|
+
['output-script', nil, 'MDE_OUTPUT_SCRIPT', 'BOOL', 'Display script',
|
568
|
+
:output_script, proc_to_i],
|
569
|
+
['output-stdout', nil, 'MDE_OUTPUT_STDOUT', 'BOOL', 'Display standard output from execution',
|
570
|
+
:output_stdout, proc_to_i],
|
571
|
+
['path', 'p', 'MDE_PATH', 'PATH', 'Path to documents',
|
572
|
+
:path, proc_self],
|
573
|
+
['run-last-script', nil, nil, nil, 'Run most recently saved script',
|
574
|
+
:run_last_script, proc_true],
|
575
|
+
['select-recent-script', nil, nil, nil, 'Select and execute a recently saved script',
|
576
|
+
:select_recent_script, proc_true],
|
577
|
+
['save-execution-output', nil, 'MDE_SAVE_EXECUTION_OUTPUT', 'BOOL', 'Save execution output',
|
578
|
+
:save_execution_output, proc_to_i],
|
579
|
+
['save-executed-script', nil, 'MDE_SAVE_EXECUTED_SCRIPT', 'BOOL', 'Save executed script',
|
580
|
+
:save_executed_script, proc_to_i],
|
581
|
+
['saved-script-folder', nil, 'MDE_SAVED_SCRIPT_FOLDER', 'SPEC', 'Saved script folder',
|
582
|
+
:saved_script_folder, proc_self],
|
583
|
+
['saved-stdout-folder', nil, 'MDE_SAVED_STDOUT_FOLDER', 'SPEC', 'Saved stdout folder',
|
584
|
+
:saved_stdout_folder, proc_self],
|
585
|
+
['user-must-approve', nil, 'MDE_USER_MUST_APPROVE', 'BOOL', 'Pause to approve execution',
|
586
|
+
:user_must_approve, proc_to_i]
|
587
|
+
]
|
588
|
+
|
589
|
+
# rubocop:disable Style/Semicolon
|
590
|
+
summary_tail = [
|
591
|
+
[nil, '0', nil, nil, 'Show configuration',
|
592
|
+
nil, ->(_) { options_finalize.call options; fout sorted_keys(options).to_yaml }],
|
593
|
+
['help', 'h', nil, nil, 'App help',
|
594
|
+
nil, ->(_) { fout option_parser.help; exit }],
|
595
|
+
['version', 'v', nil, nil, 'App version',
|
596
|
+
nil, ->(_) { fout MarkdownExec::VERSION; exit }],
|
597
|
+
['exit', 'x', nil, nil, 'Exit app',
|
598
|
+
nil, ->(_) { exit }]
|
599
|
+
]
|
600
|
+
# rubocop:enable Style/Semicolon
|
601
|
+
|
602
|
+
(summary_head + summary_body + summary_tail)
|
603
|
+
.map do |long_name, short_name, env_var, arg_name, description, opt_name, proc1| # rubocop:disable Metrics/ParameterLists
|
604
|
+
opts.on(*[if long_name.present?
|
605
|
+
"--#{long_name}#{arg_name.present? ? " #{arg_name}" : ''}"
|
606
|
+
end,
|
607
|
+
short_name.present? ? "-#{short_name}" : nil,
|
608
|
+
[description,
|
609
|
+
env_var.present? ? "env: #{env_var}" : nil].compact.join(' - '),
|
610
|
+
lambda { |value|
|
611
|
+
ret = proc1.call(value)
|
612
|
+
options[opt_name] = ret if opt_name
|
613
|
+
ret
|
614
|
+
}].compact)
|
327
615
|
end
|
328
616
|
end
|
329
617
|
option_parser.load # filename defaults to basename of the program without suffix in a directory ~/.options
|
330
618
|
option_parser.environment # env defaults to the basename of the program.
|
331
|
-
option_parser.parse! # (into: options)
|
332
|
-
options_finalize.call options
|
619
|
+
rest = option_parser.parse! # (into: options)
|
333
620
|
|
334
|
-
##
|
621
|
+
## finalize configuration
|
335
622
|
#
|
336
|
-
options
|
337
|
-
{
|
338
|
-
approve: true,
|
339
|
-
bash: true,
|
340
|
-
display: true,
|
341
|
-
exclude_expect_blocks: true,
|
342
|
-
execute: true,
|
343
|
-
prompt: 'Execute',
|
344
|
-
struct: true
|
345
|
-
}
|
346
|
-
)
|
347
|
-
mp = MarkParse.new options
|
623
|
+
options_finalize.call options
|
348
624
|
|
349
|
-
##
|
625
|
+
## position 0: file or folder (optional)
|
350
626
|
#
|
351
|
-
if
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
mp.make_block_labels(filename: file, struct: true)
|
359
|
-
end).flatten(1)
|
360
|
-
return
|
361
|
-
end
|
362
|
-
|
363
|
-
mp.select_block(
|
364
|
-
bash: true,
|
365
|
-
filename: select_md_file(list_files_per_options(options)),
|
366
|
-
struct: true
|
367
|
-
)
|
368
|
-
end
|
369
|
-
|
370
|
-
def select_block(call_options = {}, &options_block)
|
371
|
-
opts = optsmerge call_options, options_block
|
372
|
-
|
373
|
-
blocks = list_blocks_in_file(opts.merge(struct: true))
|
374
|
-
|
375
|
-
prompt = TTY::Prompt.new(interrupt: :exit)
|
376
|
-
pt = "#{opts.fetch(:prompt, nil) || 'Pick one'}:"
|
377
|
-
|
378
|
-
blocks.each { |block| block.merge! label: make_block_label(block, opts) }
|
379
|
-
block_labels = blocks.map { |block| block[:label] }
|
380
|
-
|
381
|
-
if opts[:preview_options]
|
382
|
-
select_per_page = 3
|
383
|
-
block_labels.each do |bn|
|
384
|
-
fout " - #{bn}"
|
627
|
+
if (pos = rest.fetch(0, nil))&.present?
|
628
|
+
if Dir.exist?(pos)
|
629
|
+
options[:path] = pos
|
630
|
+
elsif File.exist?(pos)
|
631
|
+
options[:filename] = pos
|
632
|
+
else
|
633
|
+
raise "Invalid parameter: #{pos}"
|
385
634
|
end
|
386
|
-
else
|
387
|
-
select_per_page = SELECT_PAGE_HEIGHT
|
388
635
|
end
|
389
636
|
|
390
|
-
|
391
|
-
|
392
|
-
sel = prompt.select(pt, block_labels, per_page: select_per_page)
|
393
|
-
|
394
|
-
label_block = blocks.select { |block| block[:label] == sel }.fetch(0, nil)
|
395
|
-
sel = label_block[:name]
|
396
|
-
|
397
|
-
cbs = list_recursively_required_blocks(blocks, sel)
|
398
|
-
|
399
|
-
## display code blocks for approval
|
637
|
+
## position 1: block name (optional)
|
400
638
|
#
|
401
|
-
|
402
|
-
|
403
|
-
allow = true
|
404
|
-
allow = prompt.yes? 'Process?' if opts[:approve]
|
639
|
+
block_name = rest.fetch(1, nil)
|
405
640
|
|
406
|
-
|
407
|
-
|
641
|
+
exec_block options, block_name
|
642
|
+
end
|
408
643
|
|
409
|
-
|
410
|
-
|
411
|
-
|
644
|
+
def run_last_script
|
645
|
+
filename = Dir.glob(File.join(@options[:saved_script_folder],
|
646
|
+
@options[:saved_script_glob])).sort[0..(options[:list_count] - 1)].last
|
647
|
+
filename.tap_inspect name: filename
|
648
|
+
mf = filename.match(/#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_(?<block>.+)\.sh/)
|
649
|
+
|
650
|
+
@options[:block_name] = mf[:block]
|
651
|
+
@options[:filename] = "#{mf[:file]}.md" ### other extensions
|
652
|
+
@options[:save_executed_script] = false
|
653
|
+
select_and_approve_block
|
654
|
+
save_execution_output
|
655
|
+
output_execution_summary
|
656
|
+
end
|
412
657
|
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
658
|
+
def save_execution_output
|
659
|
+
return unless @options[:save_execution_output]
|
660
|
+
|
661
|
+
fne = File.basename(@options[:filename], '.*')
|
662
|
+
@options[:logged_stdout_filename] =
|
663
|
+
"#{[@options[:logged_stdout_filename_prefix], Time.now.utc.strftime('%F-%H-%M-%S'), fne,
|
664
|
+
@options[:block_name]].join('_')}.out.txt"
|
665
|
+
@options[:logged_stdout_filespec] = File.join @options[:saved_stdout_folder], @options[:logged_stdout_filename]
|
666
|
+
@logged_stdout_filespec = @options[:logged_stdout_filespec]
|
667
|
+
dirname = File.dirname(@options[:logged_stdout_filespec])
|
668
|
+
Dir.mkdir dirname unless File.exist?(dirname)
|
669
|
+
File.write(@options[:logged_stdout_filespec], @execute_files&.fetch(0, ''))
|
670
|
+
end
|
417
671
|
|
418
|
-
|
419
|
-
|
672
|
+
def select_and_approve_block(call_options = {}, &options_block)
|
673
|
+
opts = optsmerge call_options, options_block
|
674
|
+
blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
|
420
675
|
|
421
|
-
|
676
|
+
unless opts[:block_name].present?
|
677
|
+
pt = (opts[:prompt_select_block]).to_s
|
678
|
+
blocks_in_file.each { |block| block.merge! label: make_block_label(block, opts) }
|
679
|
+
block_labels = option_exclude_blocks(opts, blocks_in_file).map { |block| block[:label] }
|
422
680
|
|
423
|
-
|
424
|
-
# writable = ready[1]
|
425
|
-
# exceptions = ready[2]
|
681
|
+
return nil if block_labels.count.zero?
|
426
682
|
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
# do nothing at EOF
|
431
|
-
end
|
432
|
-
end
|
433
|
-
rescue IOError => e
|
434
|
-
fout "IOError: #{e}"
|
435
|
-
end
|
436
|
-
end
|
683
|
+
sel = @prompt.select pt, block_labels, per_page: opts[:select_page_height]
|
684
|
+
label_block = blocks_in_file.select { |block| block[:label] == sel }.fetch(0, nil)
|
685
|
+
opts[:block_name] = @options[:block_name] = label_block[:name]
|
437
686
|
end
|
438
687
|
|
439
|
-
|
688
|
+
approve_block opts, blocks_in_file
|
440
689
|
end
|
441
690
|
|
442
691
|
def select_md_file(files_ = nil)
|
443
692
|
opts = options
|
444
|
-
files = files_ ||
|
693
|
+
files = files_ || list_markdown_files_in_path
|
445
694
|
if files.count == 1
|
446
|
-
|
695
|
+
files[0]
|
447
696
|
elsif files.count >= 2
|
697
|
+
@prompt.select opts[:prompt_select_md].to_s, files, per_page: opts[:select_page_height]
|
698
|
+
end
|
699
|
+
end
|
448
700
|
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
end
|
454
|
-
else
|
455
|
-
select_per_page = SELECT_PAGE_HEIGHT
|
456
|
-
end
|
701
|
+
def select_recent_script
|
702
|
+
filename = @prompt.select @options[:prompt_select_md].to_s, list_recent_scripts,
|
703
|
+
per_page: @options[:select_page_height]
|
704
|
+
mf = filename.match(/#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_(?<block>.+)\.sh/)
|
457
705
|
|
458
|
-
|
459
|
-
|
460
|
-
|
706
|
+
@options[:block_name] = mf[:block]
|
707
|
+
@options[:filename] = "#{mf[:file]}.md" ### other extensions
|
708
|
+
select_and_approve_block(
|
709
|
+
bash: true,
|
710
|
+
save_executed_script: false,
|
711
|
+
struct: true
|
712
|
+
)
|
713
|
+
save_execution_output
|
714
|
+
output_execution_summary
|
715
|
+
end
|
461
716
|
|
462
|
-
|
717
|
+
def sorted_keys(hash1)
|
718
|
+
hash1.keys.sort.to_h { |k| [k, hash1[k]] }
|
463
719
|
end
|
464
720
|
|
465
721
|
def summarize_block(headings, title)
|
466
722
|
{ headings: headings, name: title, title: title }
|
467
723
|
end
|
724
|
+
|
725
|
+
def update_options(opts = {}, over: true)
|
726
|
+
if over
|
727
|
+
@options = @options.merge opts
|
728
|
+
else
|
729
|
+
@options.merge! opts
|
730
|
+
end
|
731
|
+
@options
|
732
|
+
end
|
733
|
+
|
734
|
+
def write_command_file(opts, required_blocks)
|
735
|
+
fne = File.basename(opts[:filename], '.*')
|
736
|
+
opts[:saved_script_filename] =
|
737
|
+
"#{[opts[:saved_script_filename_prefix], Time.now.utc.strftime('%F-%H-%M-%S'), fne,
|
738
|
+
opts[:block_name]].join('_')}.sh"
|
739
|
+
@options[:saved_filespec] = File.join opts[:saved_script_folder], opts[:saved_script_filename]
|
740
|
+
@execute_script_filespec = @options[:saved_filespec]
|
741
|
+
dirname = File.dirname(@options[:saved_filespec])
|
742
|
+
Dir.mkdir dirname unless File.exist?(dirname)
|
743
|
+
File.write(@options[:saved_filespec], "#!/usr/bin/env bash\n" \
|
744
|
+
"# file_name: #{opts[:filename]}\n" \
|
745
|
+
"# block_name: #{opts[:block_name]}\n" \
|
746
|
+
"# time: #{Time.now.utc}\n" \
|
747
|
+
"#{required_blocks.flatten.join("\n")}\n")
|
748
|
+
end
|
468
749
|
end
|
469
750
|
end
|