markdown_exec 0.1.2 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d9bea3a54afa348fb845a58f5878a0ffaa7c1054c3ddcb3444d67a8ab82f3f33
4
- data.tar.gz: 3c305eb76fc06e013f38978144d668d864a67903d2be9a9de8eaa1e0f221c448
3
+ metadata.gz: 7a2b550aaccca7d0752e57c39808f5d8d3b889b9d74cf3330dee6832b2f6c535
4
+ data.tar.gz: 1296490dd79071f78b5009cb843c911fe822257ed148b4c1a516366eb02407ca
5
5
  SHA512:
6
- metadata.gz: 3ba10e7a6a74ad1d13c08c3f19ec7ccde273df98a15194022fd803088bfa5c11a880a1f2b094b9b9e9f8979a998b480afd133252bf7b5e960bd2a7f856f26c2f
7
- data.tar.gz: 3e031e16cf98a6301677d05caf09da89c227a32139c62f2844567e73acb5404171aa204a0bbf823844afab4e29a54894161f12a585d57e5223e304e587f96e2b
6
+ metadata.gz: 43f659be4397e4bb24811a8219de7b58e9665aa8751063281e2cfbbf94263ceda15f411a34fe31293869f0bbdba761c2afce72ea7f800b54edba75424313bd19
7
+ data.tar.gz: bd48ac8a5ebedbc8b92af6345743d1bd6cf50705d05769e7144ccad998ddf0f00de72366779cb654a4272c3cc2f044c4c7e46ae9df935dcb4fc486a67e54453e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.1] - 2022-03-12
4
+
5
+ - Accept file or folder as first optional positional argument.
6
+ - Hide blocks with parentheses in the name, like "(name)". This is useful for blocks that are to be required and not selected to execute alone.
7
+
8
+ ## [0.2.0] - 2022-03-12
9
+
10
+ - Improve processing of file and path sepcs.
11
+
3
12
  ## [0.1.0] - 2022-03-06
4
13
 
5
14
  - Initial release
data/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # MarkdownExec
2
2
 
3
- This gem allows you to run code blocks in markdown files.
3
+ This gem allows you to interactively select and run code blocks in markdown files.
4
4
 
5
- * Code blocks can be named.
5
+ * Code blocks may be named.
6
6
 
7
7
  * Named blocks can be required by other blocks.
8
8
 
9
- * The selected code block, and all required blocks, are collected in the order they appear in the markdown file.
9
+ * The user-selected code block, and all required blocks, are arranged in the order they appear in the markdown file.
10
+
11
+ * The code is presented for approval prior to execution.
10
12
 
11
13
  ## Installation
12
14
 
@@ -20,24 +22,79 @@ Install:
20
22
  Displays help information.
21
23
 
22
24
  `mde`
23
- Process `README.md` file in the current directory. Displays all the blocks in the file and allows you to select using [up], [down], and [return]. Press [ctrl]-c to abort selection.
25
+ Process `README.md` file in the current folder. Displays all the blocks in the file and allows you to select using [up], [down], and [return]. Press [ctrl]-c to abort selection.
24
26
 
25
27
  `mde -f my.md`
26
- Process `my.md` file in the current directory.
28
+ `mde my.md`
29
+ Select a block to execute from `my.md`.
27
30
 
28
- `mde -p child`
29
- Process markdown files in the `child` directory.
31
+ `mde -p .`
32
+ `mde .`
33
+ Select a markdown file in the current folder. Select a block to execute from that file.
30
34
 
31
35
  `mde --list-blocks`
32
- List all blocks in the selected files.
36
+ List all blocks in the all the markdown documents in the current folder.
33
37
 
34
38
  `mde --list-docs`
35
- List all markdown documents in the selected folder.
39
+ List all markdown documents in the current folder.
40
+
41
+ ## Behavior
42
+ * If no file and no folder are specified, blocks within `./README.md` are presented.
43
+ * If a file is specified, its blocks are presented.
44
+ * If a folder is specified, its files are presented. When a file is selected, its blocks are presented.
45
+
46
+ ## Configuration
47
+
48
+ While starting up, reads the YAML configuration file `.mde.yml` in the current folder if it exists.
49
+
50
+ e.g. Use to set the default file for the current folder.
51
+
52
+ * `filename: CHANGELOG.md` sets the file to open.
53
+ * `folder: documents` sets the folder to search for default or specified files.
54
+
55
+ # Example blocks
56
+
57
+ When prompted, select either the `awake` or `asleep` block.
58
+
59
+ ``` :(day)
60
+ export TIME=early
61
+ ```
62
+
63
+ ``` :(night)
64
+ export TIME=late
65
+ ```
66
+
67
+ ``` :awake +(day) +(report)
68
+ export ACTIVITY=awake
69
+ ```
70
+
71
+ ``` :asleep +(night) +(report)
72
+ export ACTIVITY=asleep
73
+ ```
74
+
75
+ ``` :(report)
76
+ echo "$TIME -> $ACTIVITY"
77
+ ```
78
+
79
+ ## Example blocks
80
+ ![Sample blocks](/assets/blocks.png)
81
+
82
+ ## Selecting a file
83
+ ![Selecting a file](/assets/select_file.png)
84
+
85
+ ## Selecting a block
86
+ ![Selecting a block](/assets/select.png)
87
+
88
+ ## Approving code
89
+ ![Approving code](/assets/approve.png)
90
+
91
+ ## Output
92
+ ![Output of execution](/assets/executed.png)
36
93
 
37
- ## License
94
+ # License
38
95
 
39
96
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
40
97
 
41
- ## Code of Conduct
98
+ # Code of Conduct
42
99
 
43
100
  Everyone interacting in the MarkdownExec project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/markdown_exec/blob/master/CODE_OF_CONDUCT.md).
Binary file
data/assets/blocks.png ADDED
Binary file
Binary file
data/assets/select.png ADDED
Binary file
Binary file
@@ -4,5 +4,5 @@ module MarkdownExec
4
4
  APP_NAME = 'MDE'
5
5
  APP_DESC = 'Markdown block executor'
6
6
  GEM_NAME = 'markdown_exec'
7
- VERSION = '0.1.2'
7
+ VERSION = '0.2.1'
8
8
  end
data/lib/markdown_exec.rb CHANGED
@@ -3,19 +3,34 @@
3
3
 
4
4
  # encoding=utf-8
5
5
 
6
- # rubocop:disable Style/GlobalVars
7
6
  $pdebug = !(ENV['MARKDOWN_EXEC_DEBUG'] || '').empty?
8
7
 
9
8
  require 'open3'
10
9
  require 'optparse'
11
- # require 'pathname'
12
10
  require 'tty-prompt'
13
11
  require 'yaml'
12
+
14
13
  require_relative 'markdown_exec/version'
15
14
 
15
+ $stderr.sync = true
16
+ $stdout.sync = true
17
+
16
18
  BLOCK_SIZE = 1024
17
19
  SELECT_PAGE_HEIGHT = 12
18
20
 
21
+ class Object # rubocop:disable Style/Documentation
22
+ def present?
23
+ self && !blank?
24
+ end
25
+ end
26
+
27
+ class String # rubocop:disable Style/Documentation
28
+ BLANK_RE = /\A[[:space:]]*\z/.freeze
29
+ def blank?
30
+ empty? || BLANK_RE.match?(self)
31
+ end
32
+ end
33
+
19
34
  module MarkdownExec
20
35
  class Error < StandardError; end
21
36
 
@@ -28,66 +43,50 @@ module MarkdownExec
28
43
  @options = options
29
44
  end
30
45
 
31
- def count_blocks
46
+ # Returns true if all files are EOF
47
+ #
48
+ def all_at_eof(files)
49
+ files.find { |f| !f.eof }.nil?
50
+ end
51
+
52
+ def count_blocks_in_filename
32
53
  cnt = 0
33
- File.readlines(options[:mdfilename]).each do |line|
54
+ File.readlines(options[:filename]).each do |line|
34
55
  cnt += 1 if line.match(/^```/)
35
56
  end
36
57
  cnt / 2
37
58
  end
38
59
 
39
- def find_files
40
- puts "pwd: #{`pwd`}" if $pdebug
41
- # `ls -1 *.md`.split("\n").tap { |ret| puts "find_files() ret: #{ret.inspect}" if $pdebug }
42
- `ls -1 #{File.join options[:mdfolder], '*.md'}`.split("\n").tap do |ret|
43
- puts "find_files() ret: #{ret.inspect}" if $pdebug
44
- end
45
- end
46
-
47
60
  def fout(str)
48
61
  puts str # to stdout
49
62
  end
50
63
 
51
- def copts(call_options = {}, options_block = nil)
52
- class_call_options = options.merge(call_options || {})
53
- if options_block
54
- options_block.call class_call_options
55
- else
56
- class_call_options
57
- end.tap { |ret| puts "copts() ret: #{ret.inspect}" if $pdebug }
58
- end
59
-
60
- def bsr(headings, title)
61
- # puts "bsr() headings: #{headings.inspect}"
62
- { headings: headings, name: title, title: title }
64
+ def get_block_by_name(table, name, default = {})
65
+ table.select { |block| block[:name] == name }.fetch(0, default)
63
66
  end
64
67
 
65
- def block_summary(opts, headings, block_title, current)
66
- puts "block_summary() block_title: #{block_title.inspect}" if $pdebug
68
+ def get_block_summary(opts, headings, block_title, current)
67
69
  return [current] unless opts[:struct]
68
70
 
69
- # return [{ body: current, name: block_title, title: block_title }] unless opts[:bash]
70
- return [bsr(headings, block_title).merge({ body: current })] unless opts[:bash]
71
+ return [summarize_block(headings, block_title).merge({ body: current })] unless opts[:bash]
71
72
 
72
73
  bm = block_title.match(/:(\S+)( |$)/)
73
74
  reqs = block_title.scan(/\+\S+/).map { |s| s[1..] }
74
75
 
75
- if $pdebug
76
- puts ["block_summary() bm: #{bm.inspect}",
77
- "block_summary() reqs: #{reqs.inspect}"]
78
- end
79
-
80
76
  if bm && bm[1]
81
- # [{ body: current, name: bm[1], reqs: reqs, title: bm[1] }]
82
- [bsr(headings, bm[1]).merge({ body: current, reqs: reqs })]
77
+ [summarize_block(headings, bm[1]).merge({ body: current, reqs: reqs })]
83
78
  else
84
- # [{ body: current, name: block_title, reqs: reqs, title: block_title }]
85
- [bsr(headings, block_title).merge({ body: current, reqs: reqs })]
79
+ [summarize_block(headings, block_title).merge({ body: current, reqs: reqs })]
86
80
  end
87
81
  end
88
82
 
89
- def get_blocks(call_options = {}, &options_block)
90
- opts = copts call_options, options_block
83
+ def list_blocks_in_file(call_options = {}, &options_block)
84
+ opts = optsmerge call_options, options_block
85
+
86
+ unless opts[:filename]&.present?
87
+ fout 'No blocks found.'
88
+ exit 1
89
+ end
91
90
 
92
91
  blocks = []
93
92
  current = nil
@@ -95,8 +94,7 @@ module MarkdownExec
95
94
  block_title = ''
96
95
 
97
96
  headings = []
98
- File.readlines(opts[:mdfilename]).each do |line|
99
- puts "get_blocks() line: #{line.inspect}" if $pdebug
97
+ File.readlines(opts[:filename]).each do |line|
100
98
  continue unless line
101
99
 
102
100
  if opts[:mdheadings]
@@ -107,18 +105,15 @@ module MarkdownExec
107
105
  elsif (lm = line.match(/^# *([^#]*?) *$/))
108
106
  headings = [lm[1]]
109
107
  end
110
- puts "get_blocks() headings: #{headings.inspect}" if $pdebug
111
108
  end
112
109
 
113
110
  if line.match(/^`{3,}/)
114
111
  if in_block
115
- puts 'get_blocks() in_block' if $pdebug
116
112
  if current
117
113
 
118
- # block_title ||= current.join(' ').gsub(/ +/, ' ')[0..64]
119
114
  block_title = current.join(' ').gsub(/ +/, ' ')[0..64] if block_title.nil? || block_title.empty?
120
115
 
121
- blocks += block_summary opts, headings, block_title, current
116
+ blocks += get_block_summary opts, headings, block_title, current
122
117
  current = nil
123
118
  end
124
119
  in_block = false
@@ -127,246 +122,141 @@ module MarkdownExec
127
122
  ## new block
128
123
  #
129
124
 
130
- # lm = line.match(/^`{3,}([^`\s]+)( .+)?$/)
131
125
  lm = line.match(/^`{3,}([^`\s]*) *(.*)$/)
132
-
133
126
  do1 = false
134
127
  if opts[:bash_only]
135
128
  do1 = true if lm && (lm[1] == 'bash')
136
- elsif opts[:exclude_expect_blocks]
137
- do1 = true unless lm && (lm[1] == 'expect')
138
129
  else
139
130
  do1 = true
140
- end
141
- if $pdebug
142
- puts ["get_blocks() lm: #{lm.inspect}",
143
- "get_blocks() opts: #{opts.inspect}",
144
- "get_blocks() do1: #{do1}"]
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
145
138
  end
146
139
 
140
+ in_block = true
147
141
  if do1 && (!opts[:title_match] || (lm && lm[2] && lm[2].match(opts[:title_match])))
148
142
  current = []
149
- in_block = true
150
143
  block_title = (lm && lm[2])
151
144
  end
152
-
153
145
  end
154
146
  elsif current
155
147
  current += [line.chomp]
156
148
  end
157
149
  end
158
- blocks.tap { |ret| puts "get_blocks() ret: #{ret.inspect}" if $pdebug }
159
- end # get_blocks
160
-
161
- def make_block_label(block, call_options = {})
162
- opts = options.merge(call_options)
163
- puts "make_block_label() opts: #{opts.inspect}" if $pdebug
164
- puts "make_block_label() block: #{block.inspect}" if $pdebug
165
- if opts[:mdheadings]
166
- heads = block.fetch(:headings, []).compact.join(' # ')
167
- "#{block[:title]} [#{heads}] (#{opts[:mdfilename]})"
168
- else
169
- "#{block[:title]} (#{opts[:mdfilename]})"
170
- 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 }
171
155
  end
172
156
 
173
- def make_block_labels(call_options = {})
174
- opts = options.merge(call_options)
175
- get_blocks(opts).map do |block|
176
- make_block_label block, opts
157
+ def list_files_per_options(options)
158
+ default_filename = 'README.md'
159
+ default_folder = '.'
160
+ if options[:filename]&.present?
161
+ list_files_specified(options[:filename], options[:folder], default_filename, default_folder)
162
+ else
163
+ list_files_specified(nil, options[:folder], default_filename, default_folder)
177
164
  end
178
165
  end
179
166
 
180
- def select_block(call_options = {}, &options_block)
181
- opts = copts call_options, options_block
182
-
183
- blocks = get_blocks(opts.merge(struct: true))
184
- puts "select_block() blocks: #{blocks.to_yaml}" if $pdebug
185
-
186
- prompt = TTY::Prompt.new(interrupt: :exit)
187
- pt = "#{opts.fetch(:prompt, nil) || 'Pick one'}:"
188
- puts "select_block() pt: #{pt.inspect}" if $pdebug
189
-
190
- blocks.each { |block| block.merge! label: make_block_label(block, opts) }
191
- block_labels = blocks.map { |block| block[:label] }
192
- puts "select_block() block_labels: #{block_labels.inspect}" if $pdebug
193
-
194
- if opts[:preview_options]
195
- select_per_page = 3
196
- block_labels.each do |bn|
197
- fout " - #{bn}"
198
- end
167
+ 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
183
+ if filetree
184
+ filetree.select { |filename| filename == fn || filename.match(/^#{fn}$/) || filename.match(%r{^#{fn}/.+$}) }
199
185
  else
200
- select_per_page = SELECT_PAGE_HEIGHT
201
- end
202
-
203
- return nil if block_labels.count.zero?
204
-
205
- sel = prompt.select(pt, block_labels, per_page: select_per_page)
206
- puts "select_block() sel: #{sel.inspect}" if $pdebug
207
- # catch
208
- # # catch TTY::Reader::InputInterrupt
209
- # puts "InputInterrupt"
210
- # end
211
-
212
- label_block = blocks.select { |block| block[:label] == sel }.fetch(0, nil)
213
- puts "select_block() label_block: #{label_block.inspect}" if $pdebug
214
- sel = label_block[:name]
215
- puts "select_block() sel: #{sel.inspect}" if $pdebug
216
-
217
- cbs = code_blocks(blocks, sel)
218
- puts "select_block() cbs: #{cbs.inspect}" if $pdebug
219
-
220
- ## display code blocks for approval
221
- #
222
- cbs.each { |cb| fout cb } if opts[:display] || opts[:approve]
223
-
224
- allow = true
225
- allow = prompt.yes? 'Process?' if opts[:approve]
226
-
227
- selected = block_by_name blocks, sel
228
- puts "select_block() selected: #{selected.inspect}" if $pdebug
229
- if allow && opts[:execute]
230
-
231
- ## process in script, to handle line continuations
232
- #
233
- cmd2 = cbs.flatten.join("\n")
234
- fout "$ #{cmd2.to_yaml}"
235
-
236
- # Open3.popen3(cmd2) do |stdin, stdout, stderr, wait_thr|
237
- # cnt += 1
238
- # # stdin.puts "This is sent to the command"
239
- # # stdin.close # we're done
240
- # stdout_str = stdout.read # read stdout to string. note that this will block until the command is done!
241
- # stderr_str = stderr.read # read stderr to string
242
- # status = wait_thr.value # will block until the command finishes; returns status that responds to .success?
243
- # fout "#{stdout_str}"
244
- # fout "#{cnt}: err: #{stderr_str}" if stderr_str != ''
245
- # # fout "#{cnt}: stt: #{status}"
246
- # end
247
-
248
- Open3.popen3(cmd2) do |stdin, stdout, stderr|
249
- stdin.close_write
250
- begin
251
- files = [stdout, stderr]
252
-
253
- until all_eof(files)
254
- ready = IO.select(files)
255
-
256
- next unless ready
257
-
258
- readable = ready[0]
259
- # writable = ready[1]
260
- # exceptions = ready[2]
261
-
262
- readable.each do |f|
263
- # fileno = f.fileno
264
-
265
- data = f.read_nonblock(BLOCK_SIZE)
266
- # fout "- fileno: #{fileno}\n#{data}"
267
- fout data
268
- rescue EOFError #=> e
269
- # fout "fileno: #{fileno} EOF"
270
- end
271
- end
272
- rescue IOError => e
273
- fout "IOError: #{e}"
274
- end
275
- end
276
- end
277
-
278
- selected[:name]
279
- end # select_block
280
-
281
- def select_md_file
282
- opts = options
283
- files = find_files
284
- if files.count == 1
285
- sel = files[0]
286
- elsif files.count >= 2
287
-
288
- if opts[:preview_options]
289
- select_per_page = 3
290
- files.each do |file|
291
- fout " - #{file}"
292
- end
293
- else
294
- select_per_page = SELECT_PAGE_HEIGHT
295
- end
296
-
297
- prompt = TTY::Prompt.new
298
- sel = prompt.select("#{opts.fetch(:prompt, 'Pick one')}:", files, per_page: select_per_page)
299
- end
186
+ Dir.glob(fn)
187
+ end.tap { |ret| puts "list_files_specified() ret: #{ret.inspect}" if $pdebug }
188
+ end
300
189
 
301
- sel
190
+ def list_markdown_files_in_folder
191
+ Dir.glob(File.join(options[:folder], '*.md'))
302
192
  end
303
193
 
304
- # Returns true if all files are EOF
305
- #
306
- def all_eof(files)
307
- files.find { |f| !f.eof }.nil?
194
+ def list_named_blocks_in_file(call_options = {}, &options_block)
195
+ opts = optsmerge call_options, options_block
196
+ list_blocks_in_file(opts).map do |block|
197
+ next if opts[:exclude_matching_block_names] && block[:name].match(/^\(.+\)$/)
198
+
199
+ block
200
+ end.compact.tap { |ret| puts "list_named_blocks_in_file() ret: #{ret.inspect}" if $pdebug }
308
201
  end
309
202
 
310
203
  def code(table, block)
311
- puts "code() table: #{table.inspect}" if $pdebug
312
- puts "code() block: #{block.inspect}" if $pdebug
313
- all = [block[:name]] + unroll(table, block[:reqs])
314
- puts "code() all: #{all.inspect}" if $pdebug
204
+ all = [block[:name]] + recursively_required(table, block[:reqs])
315
205
  all.reverse.map do |req|
316
- puts "code() req: #{req.inspect}" if $pdebug
317
- block_by_name(table, req).fetch(:body, '')
206
+ get_block_by_name(table, req).fetch(:body, '')
318
207
  end
319
208
  .flatten(1)
320
209
  .tap { |ret| puts "code() ret: #{ret.inspect}" if $pdebug }
321
210
  end
322
211
 
323
- def block_by_name(table, name, default = {})
324
- table.select { |block| block[:name] == name }.fetch(0, default)
325
- end
326
-
327
- def code_blocks(table, name)
328
- puts "code_blocks() table: #{table.inspect}" if $pdebug
329
- puts "code_blocks() name: #{name.inspect}" if $pdebug
330
- name_block = block_by_name(table, name)
331
- puts "code_blocks() name_block: #{name_block.inspect}" if $pdebug
332
- all = [name_block[:name]] + unroll(table, name_block[:reqs])
333
- puts "code_blocks() all: #{all.inspect}" if $pdebug
212
+ def list_recursively_required_blocks(table, name)
213
+ name_block = get_block_by_name(table, name)
214
+ all = [name_block[:name]] + recursively_required(table, name_block[:reqs])
334
215
 
335
216
  # in order of appearance in document
336
217
  table.select { |block| all.include? block[:name] }
337
218
  .map { |block| block.fetch(:body, '') }
338
219
  .flatten(1)
339
- .tap { |ret| puts "code_blocks() ret: #{ret.inspect}" if $pdebug }
220
+ .tap { |ret| puts "list_recursively_required_blocks() ret: #{ret.inspect}" if $pdebug }
340
221
  end
341
222
 
342
- def unroll(table, reqs)
343
- puts "unroll() table: #{table.inspect}" if $pdebug
344
- puts "unroll() reqs: #{reqs.inspect}" if $pdebug
345
- all = []
346
- rem = reqs
347
- while rem.count.positive?
348
- puts "unrol() rem: #{rem.inspect}" if $pdebug
349
- rem = rem.map do |req|
350
- puts "unrol() req: #{req.inspect}" if $pdebug
351
- next if all.include? req
223
+ def make_block_label(block, call_options = {})
224
+ opts = options.merge(call_options)
225
+ if opts[:mdheadings]
226
+ heads = block.fetch(:headings, []).compact.join(' # ')
227
+ "#{block[:title]} [#{heads}] (#{opts[:filename]})"
228
+ else
229
+ "#{block[:title]} (#{opts[:filename]})"
230
+ end
231
+ end
352
232
 
353
- all += [req]
354
- puts "unrol() all: #{all.inspect}" if $pdebug
355
- block_by_name(table, req).fetch(:reqs, [])
356
- end
357
- .compact
358
- .flatten(1)
359
- .tap { |_ret| puts "unroll() rem: #{rem.inspect}" if $pdebug }
233
+ def make_block_labels(call_options = {})
234
+ opts = options.merge(call_options)
235
+ list_blocks_in_file(opts).map do |block|
236
+ # next if opts[:exclude_matching_block_names] && block[:name].match(%r{^:\(.+\)$})
237
+
238
+ make_block_label block, opts
239
+ end.compact.tap { |ret| puts "make_block_labels() ret: #{ret.inspect}" if $pdebug }
240
+ end
241
+
242
+ def option_exclude_blocks(opts, blocks)
243
+ if opts[:exclude_matching_block_names]
244
+ blocks.reject { |block| block[:name].match(/^\(.+\)$/) }
245
+ else
246
+ blocks
360
247
  end
361
- all.tap { |ret| puts "unroll() ret: #{ret.inspect}" if $pdebug }
362
248
  end
363
249
 
364
- # $stderr.sync = true
365
- # $stdout.sync = true
250
+ def optsmerge(call_options = {}, options_block = nil)
251
+ class_call_options = options.merge(call_options || {})
252
+ if options_block
253
+ options_block.call class_call_options
254
+ else
255
+ class_call_options
256
+ end.tap { |ret| puts "optsmerge() ret: #{ret.inspect}" if $pdebug }
257
+ end
366
258
 
367
- ## configuration file
368
- #
369
- def read_configuration!(options, configuration_path)
259
+ def read_configuration_file!(options, configuration_path)
370
260
  if File.exist?(configuration_path)
371
261
  # rubocop:disable Security/YAMLLoad
372
262
  options.merge!((YAML.load(File.open(configuration_path)) || {})
@@ -376,52 +266,61 @@ module MarkdownExec
376
266
  options
377
267
  end
378
268
 
269
+ def recursively_required(table, reqs)
270
+ all = []
271
+ rem = reqs
272
+ while rem.count.positive?
273
+ rem = rem.map do |req|
274
+ next if all.include? req
275
+
276
+ all += [req]
277
+ get_block_by_name(table, req).fetch(:reqs, [])
278
+ end
279
+ .compact
280
+ .flatten(1)
281
+ .tap { |_ret| puts "recursively_required() rem: #{rem.inspect}" if $pdebug }
282
+ end
283
+ all.tap { |ret| puts "recursively_required() ret: #{ret.inspect}" if $pdebug }
284
+ end
285
+
379
286
  def run
380
287
  ## default configuration
381
288
  #
382
289
  options = {
383
290
  mdheadings: true,
384
291
  list_blocks: false,
385
- list_docs: false,
386
- mdfilename: 'README.md',
387
- mdfolder: '.'
292
+ list_docs: false
388
293
  }
389
294
 
390
- def options_finalize!(options); end
391
-
392
- # puts "MDE run() ARGV: #{ARGV.inspect}"
393
-
394
- # read local configuration file
295
+ ## post-parse options configuration
395
296
  #
396
- read_configuration! options, ".#{MarkdownExec::APP_NAME.downcase}.yml"
297
+ options_finalize = ->(_options) {}
397
298
 
398
- ## read current details for aws resources from app_data_file
299
+ # read local configuration file
399
300
  #
400
- # load_resources! options
401
- # puts "q31 options: #{options.to_yaml}" if $pdebug
301
+ read_configuration_file! options, ".#{MarkdownExec::APP_NAME.downcase}.yml"
402
302
 
403
- # rubocop:disable Metrics/BlockLength
404
303
  option_parser = OptionParser.new do |opts|
405
304
  executable_name = File.basename($PROGRAM_NAME)
406
305
  opts.banner = [
407
306
  "#{MarkdownExec::APP_NAME} - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})",
408
- "Usage: #{executable_name} [options]"
307
+ "Usage: #{executable_name} [filename or path] [options]"
409
308
  ].join("\n")
410
309
 
411
- ## menu top: on_head appear in reverse order added
310
+ ## menu top: items appear in reverse order added
412
311
  #
413
312
  opts.on('--config PATH', 'Read configuration file') do |value|
414
- read_configuration! options, value
313
+ read_configuration_file! options, value
415
314
  end
416
315
 
417
316
  ## menu body: items appear in order added
418
317
  #
419
- opts.on('-f RELATIVE', '--mdfilename', 'Name of document') do |value|
420
- options[:mdfilename] = value
318
+ opts.on('-f RELATIVE', '--filename', 'Name of document') do |value|
319
+ options[:filename] = value
421
320
  end
422
321
 
423
- opts.on('-p PATH', '--mdfolder', 'Path to documents') do |value|
424
- options[:mdfolder] = value
322
+ opts.on('-p PATH', '--path', 'Path to documents') do |value|
323
+ options[:folder] = value
425
324
  end
426
325
 
427
326
  opts.on('--list-blocks', 'List blocks') do |_value|
@@ -435,12 +334,12 @@ module MarkdownExec
435
334
  ## menu bottom: items appear in order added
436
335
  #
437
336
  opts.on_tail('-h', '--help', 'App help') do |_value|
438
- puts option_parser.help
337
+ fout option_parser.help
439
338
  exit
440
339
  end
441
340
 
442
341
  opts.on_tail('-v', '--version', 'App version') do |_value|
443
- puts MarkdownExec::VERSION
342
+ fout MarkdownExec::VERSION
444
343
  exit
445
344
  end
446
345
 
@@ -449,71 +348,161 @@ module MarkdownExec
449
348
  end
450
349
 
451
350
  opts.on_tail('-0', 'Show configuration') do |_v|
452
- options_finalize! options
453
- puts options.to_yaml
351
+ options_finalize.call options
352
+ fout options.to_yaml
454
353
  end
455
- end # OptionParser
456
- # rubocop:enable Metrics/BlockLength
457
-
354
+ end
458
355
  option_parser.load # filename defaults to basename of the program without suffix in a directory ~/.options
459
356
  option_parser.environment # env defaults to the basename of the program.
460
- option_parser.parse! # (into: options)
461
- options_finalize! options
357
+ rest = option_parser.parse! # (into: options)
358
+ options_finalize.call options
359
+
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
462
367
 
463
368
  ## process
464
369
  #
465
- # rubocop:disable Metrics/BlockLength
466
- loop do # once
467
- mp = MarkParse.new options
468
- options.merge!(
469
- {
470
- approve: true,
471
- bash: true,
472
- display: true,
473
- exclude_expect_blocks: true,
474
- execute: true,
475
- prompt: 'Execute',
476
- struct: true
477
- }
478
- )
479
-
480
- ## show
481
- #
482
- if options[:list_docs]
483
- fout mp.find_files
484
- break
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
383
+
384
+ ## show
385
+ #
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}"
485
426
  end
427
+ else
428
+ select_per_page = SELECT_PAGE_HEIGHT
429
+ end
486
430
 
487
- if options[:list_blocks]
488
- fout (mp.find_files.map do |file|
489
- mp.make_block_labels(mdfilename: file, struct: true)
490
- end).flatten(1).to_yaml
491
- break
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
441
+ #
442
+ cbs.each { |cb| fout cb } if opts[:display] || opts[:approve]
443
+
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]
456
+
457
+ until all_at_eof(files)
458
+ ready = IO.select(files)
459
+
460
+ next unless ready
461
+
462
+ readable = ready[0]
463
+ # writable = ready[1]
464
+ # exceptions = ready[2]
465
+
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
492
475
  end
476
+ end
493
477
 
494
- ## process
495
- #
496
- mp.select_block(bash: true, struct: true) if options[:mdfilename]
497
-
498
- # rubocop:disable Style/BlockComments
499
- =begin
500
- # rescue ArgumentError => e
501
- # puts "User abort: #{e}"
502
-
503
- # rescue StandardError => e
504
- # puts "ERROR: #{e}"
505
- # raise StandardError, e
506
-
507
- # ensure
508
- # exit
509
- =end
510
- # rubocop:enable Style/BlockComments
511
-
512
- break unless false # rubocop:disable Lint/LiteralAsCondition
513
- end # loop
514
- end # run
515
- end # class MarkParse
516
-
517
- # rubocop:enable Metrics/BlockLength
518
- # rubocop:enable Style/GlobalVars
519
- end # module MarkdownExec
478
+ selected[:name]
479
+ end
480
+
481
+ def select_md_file(files_ = nil)
482
+ opts = options
483
+ files = files_ || list_markdown_files_in_folder
484
+ if files.count == 1
485
+ sel = files[0]
486
+ 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)
499
+ end
500
+
501
+ sel
502
+ end
503
+
504
+ def summarize_block(headings, title)
505
+ { headings: headings, name: title, title: title }
506
+ end
507
+ end
508
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: markdown_exec
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fareed Stevenson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-03-12 00:00:00.000000000 Z
11
+ date: 2022-03-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: open3
@@ -66,8 +66,8 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: 0.2.0
69
- description: Execute shell blocks in markdown files. Name blocks and require named
70
- blocks.
69
+ description: Interactively select and execute shell blocks in markdown files. Build
70
+ complex scripts by naming blocks and requiring named blocks.
71
71
  email:
72
72
  - fareed@phomento.com
73
73
  executables:
@@ -83,6 +83,11 @@ files:
83
83
  - LICENSE.txt
84
84
  - README.md
85
85
  - Rakefile
86
+ - assets/approve.png
87
+ - assets/blocks.png
88
+ - assets/executed.png
89
+ - assets/select.png
90
+ - assets/select_file.png
86
91
  - bin/console
87
92
  - bin/mde
88
93
  - bin/setup
@@ -93,6 +98,8 @@ licenses:
93
98
  - MIT
94
99
  metadata:
95
100
  homepage_uri: https://rubygems.org/gems/markdown_exec
101
+ source_code_uri: https://github.com/fareedst/markdown_exec
102
+ changelog_uri: https://github.com/fareedst/markdown_exec/blob/main/CHANGELOG.md
96
103
  post_install_message:
97
104
  rdoc_options: []
98
105
  require_paths: