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 +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +74 -37
- 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 +261 -311
- 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: 0d9976af006093768c7673d5ef0abeff6766539ff86029179d45db0530a4e203
|
4
|
+
data.tar.gz: c2814cf1b85ba80352819c2866862e40a9185874469edc65e0673485e0ca4f7e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9720f8af38d17eba836b73407dedf71848c19663fa017277d3a62e003e19da36a3cad881796ce58e7b84d3d03a94a3a130d6876ac41c908d4694dadc80b53c6b
|
7
|
+
data.tar.gz: 707486f6a6e424787ee7ef05336d5a7290375844f477f369aa7dce2dc72d7f0343d425d83ac778d6b12859b4e155c6fea0ed0acb0cc3719b2d42b5fbf99f2c7b
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,66 +1,103 @@
|
|
1
1
|
# MarkdownExec
|
2
2
|
|
3
|
-
|
3
|
+
This gem allows you to interactively select and run code blocks in markdown files.
|
4
4
|
|
5
|
-
|
5
|
+
* Code blocks may be named.
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
Add this line to your application's Gemfile:
|
7
|
+
* Named blocks can be required by other blocks.
|
10
8
|
|
11
|
-
|
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
|
-
|
11
|
+
* The code is presented for approval prior to execution.
|
16
12
|
|
17
|
-
|
13
|
+
## Installation
|
18
14
|
|
19
|
-
|
15
|
+
Install:
|
20
16
|
|
21
17
|
$ gem install markdown_exec
|
22
18
|
|
23
19
|
## Usage
|
24
20
|
|
25
|
-
|
21
|
+
`mde --help`
|
22
|
+
Displays help information.
|
26
23
|
|
27
|
-
|
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
|
-
|
27
|
+
`mde -f my.md`
|
28
|
+
Select a block to execute from `my.md`.
|
30
29
|
|
31
|
-
|
30
|
+
`mde -p .`
|
31
|
+
Select a markdown file in the current folder. Select a block to execute from that file.
|
32
32
|
|
33
|
-
|
33
|
+
`mde --list-blocks`
|
34
|
+
List all blocks in the all the markdown documents in the current folder.
|
34
35
|
|
35
|
-
|
36
|
+
`mde --list-docs`
|
37
|
+
List all markdown documents in the current folder.
|
36
38
|
|
37
|
-
##
|
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
|
-
|
44
|
+
## Configuration
|
40
45
|
|
41
|
-
|
46
|
+
While starting up, reads the YAML configuration file `.mde.yml` in the current folder if it exists.
|
42
47
|
|
43
|
-
|
48
|
+
e.g. Use to set the default file for the current folder.
|
44
49
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
82
|
+
## Example blocks
|
83
|
+

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

|
87
|
+
|
88
|
+
## Selecting a block
|
89
|
+

|
90
|
+
|
91
|
+
## Approving code
|
92
|
+

|
93
|
+
|
94
|
+
## Output
|
95
|
+

|
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).
|
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,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 "
|
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
|
174
|
-
|
175
|
-
|
176
|
-
|
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
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
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 "
|
204
|
+
.tap { |ret| puts "list_recursively_required_blocks() ret: #{ret.inspect}" if $pdebug }
|
340
205
|
end
|
341
206
|
|
342
|
-
def
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
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
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
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
|
-
|
365
|
-
|
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
|
-
|
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
|
-
|
391
|
-
|
392
|
-
# puts "MDE run() ARGV: #{ARGV.inspect}"
|
393
|
-
|
394
|
-
# read local configuration file
|
269
|
+
## post-parse options configuration
|
395
270
|
#
|
396
|
-
|
271
|
+
options_finalize = ->(_options) {}
|
397
272
|
|
398
|
-
|
273
|
+
# read local configuration file
|
399
274
|
#
|
400
|
-
|
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:
|
284
|
+
## menu top: items appear in reverse order added
|
412
285
|
#
|
413
286
|
opts.on('--config PATH', 'Read configuration file') do |value|
|
414
|
-
|
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', '--
|
420
|
-
options[:
|
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', '--
|
424
|
-
options[:
|
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
|
-
|
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
|
-
|
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
|
453
|
-
|
325
|
+
options_finalize.call options
|
326
|
+
fout options.to_yaml
|
454
327
|
end
|
455
|
-
end
|
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
|
332
|
+
options_finalize.call options
|
462
333
|
|
463
334
|
## process
|
464
335
|
#
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
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
|
-
|
483
|
-
|
484
|
-
|
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
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
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
|
-
|
495
|
-
#
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
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.
|
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-
|
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:
|