markdown_exec 0.1.2 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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: