markdown_exec 0.1.1 → 0.2.0

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: 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: