markdown_exec 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +68 -11
- data/assets/approve.png +0 -0
- data/assets/blocks.png +0 -0
- data/assets/executed.png +0 -0
- data/assets/select.png +0 -0
- data/assets/select_file.png +0 -0
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +307 -318
- metadata +11 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7a2b550aaccca7d0752e57c39808f5d8d3b889b9d74cf3330dee6832b2f6c535
|
4
|
+
data.tar.gz: 1296490dd79071f78b5009cb843c911fe822257ed148b4c1a516366eb02407ca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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
|
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
|
-
|
28
|
+
`mde my.md`
|
29
|
+
Select a block to execute from `my.md`.
|
27
30
|
|
28
|
-
`mde -p
|
29
|
-
|
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
|
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
|
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
|
+

|
81
|
+
|
82
|
+
## Selecting a file
|
83
|
+

|
84
|
+
|
85
|
+
## Selecting a block
|
86
|
+

|
87
|
+
|
88
|
+
## Approving code
|
89
|
+

|
90
|
+
|
91
|
+
## Output
|
92
|
+

|
36
93
|
|
37
|
-
|
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
|
-
|
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).
|
data/assets/approve.png
ADDED
Binary file
|
data/assets/blocks.png
ADDED
Binary file
|
data/assets/executed.png
ADDED
Binary file
|
data/assets/select.png
ADDED
Binary file
|
Binary file
|
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
|
-
|
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[:
|
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
|
52
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
90
|
-
opts =
|
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[:
|
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 +=
|
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
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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 "
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
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
|
174
|
-
|
175
|
-
|
176
|
-
|
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
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
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
|
-
|
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
|
-
|
190
|
+
def list_markdown_files_in_folder
|
191
|
+
Dir.glob(File.join(options[:folder], '*.md'))
|
302
192
|
end
|
303
193
|
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
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
|
-
|
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
|
-
|
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
|
324
|
-
|
325
|
-
|
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 "
|
220
|
+
.tap { |ret| puts "list_recursively_required_blocks() ret: #{ret.inspect}" if $pdebug }
|
340
221
|
end
|
341
222
|
|
342
|
-
def
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
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
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
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
|
-
|
365
|
-
|
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
|
-
|
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
|
-
|
391
|
-
|
392
|
-
# puts "MDE run() ARGV: #{ARGV.inspect}"
|
393
|
-
|
394
|
-
# read local configuration file
|
295
|
+
## post-parse options configuration
|
395
296
|
#
|
396
|
-
|
297
|
+
options_finalize = ->(_options) {}
|
397
298
|
|
398
|
-
|
299
|
+
# read local configuration file
|
399
300
|
#
|
400
|
-
|
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:
|
310
|
+
## menu top: items appear in reverse order added
|
412
311
|
#
|
413
312
|
opts.on('--config PATH', 'Read configuration file') do |value|
|
414
|
-
|
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', '--
|
420
|
-
options[:
|
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', '--
|
424
|
-
options[:
|
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
|
-
|
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
|
-
|
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
|
453
|
-
|
351
|
+
options_finalize.call options
|
352
|
+
fout options.to_yaml
|
454
353
|
end
|
455
|
-
end
|
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
|
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
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
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
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
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
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
=
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
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
|
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-
|
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:
|
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:
|