markdown_exec 0.1.1 → 0.2.0

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: 57da333a75458ce1ba2d67c92ee1be458418df6a008f41a89c3545dc0afb94ed
4
- data.tar.gz: 4d0c80082485dc50bdb1460adcf101b8ddba65da7d6406d93c1824fd21f9fe95
3
+ metadata.gz: 0d9976af006093768c7673d5ef0abeff6766539ff86029179d45db0530a4e203
4
+ data.tar.gz: c2814cf1b85ba80352819c2866862e40a9185874469edc65e0673485e0ca4f7e
5
5
  SHA512:
6
- metadata.gz: 2f425417848f457325a54d6b8b72f4ecc6f9a5a4ab5ce334b7e1ac93f8760657f9e52c6c2713dcb645d5c9b6a2b1a8cfa99e6a6ef9640cc87e3401a84fff15b2
7
- data.tar.gz: 89ad4e3849de542cee33ad65f7bc6185a8f2cda880fec6d94d5f2c1657e957ad33a1c364cf89c56f5fa4999ffea5d5bba7287ebbf747c845307bc80540f0ff21
6
+ metadata.gz: 9720f8af38d17eba836b73407dedf71848c19663fa017277d3a62e003e19da36a3cad881796ce58e7b84d3d03a94a3a130d6876ac41c908d4694dadc80b53c6b
7
+ data.tar.gz: 707486f6a6e424787ee7ef05336d5a7290375844f477f369aa7dce2dc72d7f0343d425d83ac778d6b12859b4e155c6fea0ed0acb0cc3719b2d42b5fbf99f2c7b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2022-03-12
4
+
5
+ - Improve processing of file and path sepcs.
6
+
3
7
  ## [0.1.0] - 2022-03-06
4
8
 
5
9
  - Initial release
data/README.md CHANGED
@@ -1,66 +1,103 @@
1
1
  # MarkdownExec
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/markdown_exec`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ This gem allows you to interactively select and run code blocks in markdown files.
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ * Code blocks may be named.
6
6
 
7
- ## Installation
8
-
9
- Add this line to your application's Gemfile:
7
+ * Named blocks can be required by other blocks.
10
8
 
11
- ```ruby
12
- gem 'markdown_exec'
13
- ```
9
+ * The user-selected code block, and all required blocks, are arranged in the order they appear in the markdown file.
14
10
 
15
- And then execute:
11
+ * The code is presented for approval prior to execution.
16
12
 
17
- $ bundle install
13
+ ## Installation
18
14
 
19
- Or install it yourself as:
15
+ Install:
20
16
 
21
17
  $ gem install markdown_exec
22
18
 
23
19
  ## Usage
24
20
 
25
- TODO: Write usage instructions here
21
+ `mde --help`
22
+ Displays help information.
26
23
 
27
- ## Development
24
+ `mde`
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.
28
26
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
27
+ `mde -f my.md`
28
+ Select a block to execute from `my.md`.
30
29
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
30
+ `mde -p .`
31
+ Select a markdown file in the current folder. Select a block to execute from that file.
32
32
 
33
- ## Contributing
33
+ `mde --list-blocks`
34
+ List all blocks in the all the markdown documents in the current folder.
34
35
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/markdown_exec. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/markdown_exec/blob/master/CODE_OF_CONDUCT.md).
36
+ `mde --list-docs`
37
+ List all markdown documents in the current folder.
36
38
 
37
- ## License
39
+ ## Behavior
40
+ * If no file and no folder are specified, blocks within `./README.md` are presented.
41
+ * If a file is specified, its blocks are presented.
42
+ * If a folder is specified, its files are presented. When a file is selected, its blocks are presented.
38
43
 
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
44
+ ## Configuration
40
45
 
41
- ## Code of Conduct
46
+ While starting up, reads the YAML configuration file `.mde.yml` in the current folder if it exists.
42
47
 
43
- 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).
48
+ e.g. Use to set the default file for the current folder.
44
49
 
45
- # 2022-03-06
46
- ```
47
- gem build markdown_exec.gemspec
48
- ```
49
- ```expect
50
- Successfully built RubyGem
51
- Name: markdown_exec
52
- Version: 0.0.1
53
- File: markdown_exec-0.0.1.gem
54
- ```
55
- ```
56
- gem install ./markdown_exec-0.0.1.gem
50
+ * `filename: CHANGELOG.md` sets the file to open.
51
+ * `folder: documents` sets the folder to search for default or specified files.
52
+
53
+ # Example blocks
54
+
55
+ When prompted, select either the `awake` or `asleep` block. The standard output confirms which required were blocks were included. Naming "hidden" blocks with parentheses "(" and ")" is a convention used here to re-inforce the purpose of the named blocks.
56
+
57
+ ``` :(day)
58
+ # block named "(day)", required by other blocks
59
+ export MYTIME=early
57
60
  ```
61
+
62
+ ``` :(night)
63
+ # block named "(night)", required by other blocks
64
+ export MYTIME=late
58
65
  ```
59
- gem list | grep exec
66
+
67
+ ``` :awake +(day) +(report)
68
+ # block named "awake", select to see result
69
+ export ACTIVITY=awake
60
70
  ```
71
+
72
+ ``` :asleep +(night) +(report)
73
+ # block named "asleep", select to see result
74
+ export ACTIVITY=asleep
61
75
  ```
62
- gem uninstall markdown_exec
76
+
77
+ ``` :(report)
78
+ # block named "(report)", required by other blocks
79
+ echo "time: $MYTIME, activity: $ACTIVITY"
63
80
  ```
64
- pusher api key: rubygems_8c4b38ef685484719858c53c3161a124c15d7a9bec6cb65a
65
81
 
66
- gem push markdown_exec-0.0.1.gem
82
+ ## Example blocks
83
+ ![Sample blocks](/assets/blocks.png)
84
+
85
+ ## Selecting a file
86
+ ![Selecting a file](/assets/select_file.png)
87
+
88
+ ## Selecting a block
89
+ ![Selecting a block](/assets/select.png)
90
+
91
+ ## Approving code
92
+ ![Approving code](/assets/approve.png)
93
+
94
+ ## Output
95
+ ![Output of execution](/assets/executed.png)
96
+
97
+ # License
98
+
99
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
100
+
101
+ # Code of Conduct
102
+
103
+ 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.1'
7
+ VERSION = '0.2.0'
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,7 +122,6 @@ module MarkdownExec
127
122
  ## new block
128
123
  #
129
124
 
130
- # lm = line.match(/^`{3,}([^`\s]+)( .+)?$/)
131
125
  lm = line.match(/^`{3,}([^`\s]*) *(.*)$/)
132
126
 
133
127
  do1 = false
@@ -138,11 +132,6 @@ module MarkdownExec
138
132
  else
139
133
  do1 = true
140
134
  end
141
- if $pdebug
142
- puts ["get_blocks() lm: #{lm.inspect}",
143
- "get_blocks() opts: #{opts.inspect}",
144
- "get_blocks() do1: #{do1}"]
145
- end
146
135
 
147
136
  if do1 && (!opts[:title_match] || (lm && lm[2] && lm[2].match(opts[:title_match])))
148
137
  current = []
@@ -155,218 +144,93 @@ module MarkdownExec
155
144
  current += [line.chomp]
156
145
  end
157
146
  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
147
+ blocks.tap { |ret| puts "list_blocks_in_file() ret: #{ret.inspect}" if $pdebug }
171
148
  end
172
149
 
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
150
+ def list_files_per_options(options)
151
+ default_filename = 'README.md'
152
+ default_folder = '.'
153
+ if options[:filename]&.present?
154
+ list_files_specified(options[:filename], options[:folder], default_filename, default_folder)
155
+ else
156
+ list_files_specified(nil, options[:folder], default_filename, default_folder)
177
157
  end
178
158
  end
179
159
 
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
160
+ def list_files_specified(specified_filename, specified_folder, default_filename, default_folder, filetree = nil)
161
+ fn = if specified_filename&.present?
162
+ if specified_folder&.present?
163
+ "#{specified_folder}/#{specified_filename}"
164
+ else
165
+ "#{default_folder}/#{specified_filename}"
166
+ end
167
+ elsif specified_folder&.present?
168
+ if filetree
169
+ "#{specified_folder}/.+\\.md"
170
+ else
171
+ "#{specified_folder}/*.[Mm][Dd]"
172
+ end
173
+ else
174
+ "#{default_folder}/#{default_filename}"
175
+ end
176
+ if filetree
177
+ filetree.select { |filename| filename == fn || filename.match(/^#{fn}$/) || filename.match(%r{^#{fn}/.+$}) }
199
178
  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
300
-
301
- sel
179
+ Dir.glob(fn)
180
+ end.tap { |ret| puts "list_files_specified() ret: #{ret.inspect}" if $pdebug }
302
181
  end
303
182
 
304
- # Returns true if all files are EOF
305
- #
306
- def all_eof(files)
307
- files.find { |f| !f.eof }.nil?
183
+ def list_markdown_files_in_folder
184
+ Dir.glob(File.join(options[:folder], '*.md'))
308
185
  end
309
186
 
310
187
  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
188
+ all = [block[:name]] + recursively_required(table, block[:reqs])
315
189
  all.reverse.map do |req|
316
- puts "code() req: #{req.inspect}" if $pdebug
317
- block_by_name(table, req).fetch(:body, '')
190
+ get_block_by_name(table, req).fetch(:body, '')
318
191
  end
319
192
  .flatten(1)
320
193
  .tap { |ret| puts "code() ret: #{ret.inspect}" if $pdebug }
321
194
  end
322
195
 
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
196
+ def list_recursively_required_blocks(table, name)
197
+ name_block = get_block_by_name(table, name)
198
+ all = [name_block[:name]] + recursively_required(table, name_block[:reqs])
334
199
 
335
200
  # in order of appearance in document
336
201
  table.select { |block| all.include? block[:name] }
337
202
  .map { |block| block.fetch(:body, '') }
338
203
  .flatten(1)
339
- .tap { |ret| puts "code_blocks() ret: #{ret.inspect}" if $pdebug }
204
+ .tap { |ret| puts "list_recursively_required_blocks() ret: #{ret.inspect}" if $pdebug }
340
205
  end
341
206
 
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
207
+ def make_block_label(block, call_options = {})
208
+ opts = options.merge(call_options)
209
+ if opts[:mdheadings]
210
+ heads = block.fetch(:headings, []).compact.join(' # ')
211
+ "#{block[:title]} [#{heads}] (#{opts[:filename]})"
212
+ else
213
+ "#{block[:title]} (#{opts[:filename]})"
214
+ end
215
+ end
352
216
 
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 }
217
+ def make_block_labels(call_options = {})
218
+ opts = options.merge(call_options)
219
+ list_blocks_in_file(opts).map do |block|
220
+ make_block_label block, opts
360
221
  end
361
- all.tap { |ret| puts "unroll() ret: #{ret.inspect}" if $pdebug }
362
222
  end
363
223
 
364
- # $stderr.sync = true
365
- # $stdout.sync = true
224
+ def optsmerge(call_options = {}, options_block = nil)
225
+ class_call_options = options.merge(call_options || {})
226
+ if options_block
227
+ options_block.call class_call_options
228
+ else
229
+ class_call_options
230
+ end.tap { |ret| puts "optsmerge() ret: #{ret.inspect}" if $pdebug }
231
+ end
366
232
 
367
- ## configuration file
368
- #
369
- def read_configuration!(options, configuration_path)
233
+ def read_configuration_file!(options, configuration_path)
370
234
  if File.exist?(configuration_path)
371
235
  # rubocop:disable Security/YAMLLoad
372
236
  options.merge!((YAML.load(File.open(configuration_path)) || {})
@@ -376,31 +240,40 @@ module MarkdownExec
376
240
  options
377
241
  end
378
242
 
243
+ def recursively_required(table, reqs)
244
+ all = []
245
+ rem = reqs
246
+ while rem.count.positive?
247
+ rem = rem.map do |req|
248
+ next if all.include? req
249
+
250
+ all += [req]
251
+ get_block_by_name(table, req).fetch(:reqs, [])
252
+ end
253
+ .compact
254
+ .flatten(1)
255
+ .tap { |_ret| puts "recursively_required() rem: #{rem.inspect}" if $pdebug }
256
+ end
257
+ all.tap { |ret| puts "recursively_required() ret: #{ret.inspect}" if $pdebug }
258
+ end
259
+
379
260
  def run
380
261
  ## default configuration
381
262
  #
382
263
  options = {
383
264
  mdheadings: true,
384
265
  list_blocks: false,
385
- list_docs: false,
386
- mdfilename: 'README.md',
387
- mdfolder: '.'
266
+ list_docs: false
388
267
  }
389
268
 
390
- def options_finalize!(options); end
391
-
392
- # puts "MDE run() ARGV: #{ARGV.inspect}"
393
-
394
- # read local configuration file
269
+ ## post-parse options configuration
395
270
  #
396
- read_configuration! options, ".#{MarkdownExec::APP_NAME.downcase}.yml"
271
+ options_finalize = ->(_options) {}
397
272
 
398
- ## read current details for aws resources from app_data_file
273
+ # read local configuration file
399
274
  #
400
- # load_resources! options
401
- # puts "q31 options: #{options.to_yaml}" if $pdebug
275
+ read_configuration_file! options, ".#{MarkdownExec::APP_NAME.downcase}.yml"
402
276
 
403
- # rubocop:disable Metrics/BlockLength
404
277
  option_parser = OptionParser.new do |opts|
405
278
  executable_name = File.basename($PROGRAM_NAME)
406
279
  opts.banner = [
@@ -408,20 +281,20 @@ module MarkdownExec
408
281
  "Usage: #{executable_name} [options]"
409
282
  ].join("\n")
410
283
 
411
- ## menu top: on_head appear in reverse order added
284
+ ## menu top: items appear in reverse order added
412
285
  #
413
286
  opts.on('--config PATH', 'Read configuration file') do |value|
414
- read_configuration! options, value
287
+ read_configuration_file! options, value
415
288
  end
416
289
 
417
290
  ## menu body: items appear in order added
418
291
  #
419
- opts.on('-f RELATIVE', '--mdfilename', 'Name of document') do |value|
420
- options[:mdfilename] = value
292
+ opts.on('-f RELATIVE', '--filename', 'Name of document') do |value|
293
+ options[:filename] = value
421
294
  end
422
295
 
423
- opts.on('-p PATH', '--mdfolder', 'Path to documents') do |value|
424
- options[:mdfolder] = value
296
+ opts.on('-p PATH', '--folder', 'Path to documents') do |value|
297
+ options[:folder] = value
425
298
  end
426
299
 
427
300
  opts.on('--list-blocks', 'List blocks') do |_value|
@@ -435,12 +308,12 @@ module MarkdownExec
435
308
  ## menu bottom: items appear in order added
436
309
  #
437
310
  opts.on_tail('-h', '--help', 'App help') do |_value|
438
- puts option_parser.help
311
+ fout option_parser.help
439
312
  exit
440
313
  end
441
314
 
442
315
  opts.on_tail('-v', '--version', 'App version') do |_value|
443
- puts MarkdownExec::VERSION
316
+ fout MarkdownExec::VERSION
444
317
  exit
445
318
  end
446
319
 
@@ -449,71 +322,148 @@ module MarkdownExec
449
322
  end
450
323
 
451
324
  opts.on_tail('-0', 'Show configuration') do |_v|
452
- options_finalize! options
453
- puts options.to_yaml
325
+ options_finalize.call options
326
+ fout options.to_yaml
454
327
  end
455
- end # OptionParser
456
- # rubocop:enable Metrics/BlockLength
457
-
328
+ end
458
329
  option_parser.load # filename defaults to basename of the program without suffix in a directory ~/.options
459
330
  option_parser.environment # env defaults to the basename of the program.
460
331
  option_parser.parse! # (into: options)
461
- options_finalize! options
332
+ options_finalize.call options
462
333
 
463
334
  ## process
464
335
  #
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
336
+ options.merge!(
337
+ {
338
+ approve: true,
339
+ bash: true,
340
+ display: true,
341
+ exclude_expect_blocks: true,
342
+ execute: true,
343
+ prompt: 'Execute',
344
+ struct: true
345
+ }
346
+ )
347
+ mp = MarkParse.new options
348
+
349
+ ## show
350
+ #
351
+ if options[:list_docs]
352
+ fout mp.list_files_per_options options
353
+ return
354
+ end
355
+
356
+ if options[:list_blocks]
357
+ fout (mp.list_files_per_options(options).map do |file|
358
+ mp.make_block_labels(filename: file, struct: true)
359
+ end).flatten(1)
360
+ return
361
+ end
362
+
363
+ mp.select_block(
364
+ bash: true,
365
+ filename: select_md_file(list_files_per_options(options)),
366
+ struct: true
367
+ )
368
+ end
369
+
370
+ def select_block(call_options = {}, &options_block)
371
+ opts = optsmerge call_options, options_block
372
+
373
+ blocks = list_blocks_in_file(opts.merge(struct: true))
374
+
375
+ prompt = TTY::Prompt.new(interrupt: :exit)
376
+ pt = "#{opts.fetch(:prompt, nil) || 'Pick one'}:"
377
+
378
+ blocks.each { |block| block.merge! label: make_block_label(block, opts) }
379
+ block_labels = blocks.map { |block| block[:label] }
380
+
381
+ if opts[:preview_options]
382
+ select_per_page = 3
383
+ block_labels.each do |bn|
384
+ fout " - #{bn}"
385
+ end
386
+ else
387
+ select_per_page = SELECT_PAGE_HEIGHT
388
+ end
389
+
390
+ return nil if block_labels.count.zero?
391
+
392
+ sel = prompt.select(pt, block_labels, per_page: select_per_page)
393
+
394
+ label_block = blocks.select { |block| block[:label] == sel }.fetch(0, nil)
395
+ sel = label_block[:name]
396
+
397
+ cbs = list_recursively_required_blocks(blocks, sel)
398
+
399
+ ## display code blocks for approval
400
+ #
401
+ cbs.each { |cb| fout cb } if opts[:display] || opts[:approve]
402
+
403
+ allow = true
404
+ allow = prompt.yes? 'Process?' if opts[:approve]
405
+
406
+ selected = get_block_by_name blocks, sel
407
+ if allow && opts[:execute]
408
+
409
+ ## process in script, to handle line continuations
481
410
  #
482
- if options[:list_docs]
483
- fout mp.find_files
484
- break
411
+ cmd2 = cbs.flatten.join("\n")
412
+
413
+ Open3.popen3(cmd2) do |stdin, stdout, stderr|
414
+ stdin.close_write
415
+ begin
416
+ files = [stdout, stderr]
417
+
418
+ until all_at_eof(files)
419
+ ready = IO.select(files)
420
+
421
+ next unless ready
422
+
423
+ readable = ready[0]
424
+ # writable = ready[1]
425
+ # exceptions = ready[2]
426
+
427
+ readable.each do |f|
428
+ print f.read_nonblock(BLOCK_SIZE)
429
+ rescue EOFError #=> e
430
+ # do nothing at EOF
431
+ end
432
+ end
433
+ rescue IOError => e
434
+ fout "IOError: #{e}"
435
+ end
485
436
  end
437
+ end
438
+
439
+ selected[:name]
440
+ end
441
+
442
+ def select_md_file(files_ = nil)
443
+ opts = options
444
+ files = files_ || list_markdown_files_in_folder
445
+ if files.count == 1
446
+ sel = files[0]
447
+ elsif files.count >= 2
486
448
 
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
449
+ if opts[:preview_options]
450
+ select_per_page = 3
451
+ files.each do |file|
452
+ fout " - #{file}"
453
+ end
454
+ else
455
+ select_per_page = SELECT_PAGE_HEIGHT
492
456
  end
493
457
 
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
458
+ prompt = TTY::Prompt.new
459
+ sel = prompt.select("#{opts.fetch(:prompt, 'Pick one')}:", files, per_page: select_per_page)
460
+ end
461
+
462
+ sel
463
+ end
464
+
465
+ def summarize_block(headings, title)
466
+ { headings: headings, name: title, title: title }
467
+ end
468
+ end
469
+ 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.1
4
+ version: 0.2.0
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: