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.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -0
- data/CHANGELOG.md +106 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +7 -1
- data/README.md +25 -27
- 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 +410 -258
- 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,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
|
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(
|
74
|
-
reqs = block_title.scan(
|
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[
|
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[
|
103
|
-
elsif (lm = line.match(
|
104
|
-
headings = [headings[0], lm[
|
105
|
-
elsif (lm = line.match(
|
106
|
-
headings = [lm[
|
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(
|
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
|
-
|
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[
|
335
|
+
do1 = true if lm && (lm[:shell] == 'bash')
|
129
336
|
else
|
130
337
|
do1 = true
|
131
|
-
do1 = !(lm && (lm[
|
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[
|
342
|
+
if do1 && (!opts[:title_match] || (lm && lm[:name] && lm[:name].match(opts[:title_match])))
|
142
343
|
current = []
|
143
|
-
block_title = (lm && lm[
|
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.
|
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[:
|
358
|
+
list_files_specified(options[:filename], options[:path], default_filename, default_folder)
|
162
359
|
else
|
163
|
-
list_files_specified(nil, options[:
|
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
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
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.
|
386
|
+
end.tap_inspect
|
188
387
|
end
|
189
388
|
|
190
|
-
def
|
191
|
-
Dir.glob(File.join(options[:
|
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.
|
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
|
-
.
|
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.
|
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.
|
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
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
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
|
-
.
|
487
|
+
.tap_inspect(name: 'rem')
|
282
488
|
end
|
283
|
-
all.
|
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}
|
307
|
-
"
|
512
|
+
"#{MarkdownExec::APP_NAME}" \
|
513
|
+
" - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})",
|
514
|
+
"Usage: #{executable_name} [path] [filename] [options]"
|
308
515
|
].join("\n")
|
309
516
|
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
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
|
-
|
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
|
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
|
-
##
|
581
|
+
## position 0: file or folder (optional)
|
385
582
|
#
|
386
|
-
if
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
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
|
-
|
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
|
-
|
595
|
+
block_name = rest.fetch(1, nil)
|
443
596
|
|
444
|
-
|
445
|
-
|
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
|
-
|
458
|
-
|
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
|
-
|
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
|
-
|
463
|
-
# writable = ready[1]
|
464
|
-
# exceptions = ready[2]
|
609
|
+
return nil if block_labels.count.zero?
|
465
610
|
|
466
|
-
|
467
|
-
|
468
|
-
|
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
|
-
|
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_ ||
|
621
|
+
files = files_ || list_markdown_files_in_path
|
484
622
|
if files.count == 1
|
485
|
-
|
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
|