markdown_exec 0.2.3 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ec074d1854ef0150cf60852fd39d7db7bc88461f7d9f5ae6547fe009142ffb4
4
- data.tar.gz: 8d6eb3c7759bf180b996f8a68ec8b52d0e52a230365b163f06528e9c11627e42
3
+ metadata.gz: c8baa6b01b9d965c3a8856d08a86225457c09106c366ecd174f7f0e547f43f4a
4
+ data.tar.gz: bd0348f31ddb1bb0cda0328e5c7dd3187e6692269e8b132849833ce1d44f5b0f
5
5
  SHA512:
6
- metadata.gz: '095584193734695c29729060ca4ccd424088ca59cdd8bf539dda5a6b914d7fd71d373304f071e46b49072bf01b09ae2a1f0cf86bf314f3d79ace9acd166a3531'
7
- data.tar.gz: c8e55fa064fe9bc8c170c9431dd1a5dc9fd3f20f1fe42925c00c4f2ed5113d597f457ecdba225cf4533563cd0c3f14f35ebc1ad096e8a89f8ec654a5cd87cd8c
6
+ metadata.gz: e6f8028d4fe4dea45bacb14e8cb12306d0ee975ac729c04d0f8db990a6ce92758b13400329358656d5902c85dd5411aef717210b0151de4ae2cec375f51b0b98
7
+ data.tar.gz: 390dd965fe49ab4ac1651996fc7500ff971fbe71ce580ff807d8652b62aff5b50768ce971920e9f93682d5adb692cd5bcf99834b8b022c41734dc24f61cf940c
data/CHANGELOG.md CHANGED
@@ -4,11 +4,9 @@
4
4
 
5
5
  - pipe stdin to script
6
6
  - yes/no/write/clipboard/record/edit/history
7
- - add confirm block to generated file
8
7
  - present timestamp, result of last exec for each command
9
8
  - user settings
10
9
  - hidden w , w/o () in names
11
- - exit in menus
12
10
  - fix regexp in pathnames
13
11
  - tab completion from md file
14
12
  - read file once to allow for tempdoc stream
@@ -20,18 +18,53 @@
20
18
 
21
19
  - include blocks from local md file
22
20
 
23
- - save outputs, errors
24
-
25
21
  - chmod a+x logged script
26
22
 
27
- - exec most recent logged scripts
23
+ - add shebang to saved script
24
+
25
+ ## [0.2.6] - 2022-04-07
26
+
27
+ ### Changed
28
+
29
+ - Fixed default values for command line options.
30
+
31
+ ## [0.2.5] - 2022-04-03
28
32
 
29
- - cmd to list last n
30
- - cmd to repeat last
33
+ ### Added
34
+
35
+ - Command `--list-default-env` to show default configuration as environment variables.
36
+ - Command `--list-default-yaml` to show default configuration as YAML.
37
+ - Option to exit program when selecting files or blocks.
38
+
39
+ ### Changed
31
40
 
32
- ## [Unreleased]
41
+ - Composition of menu to facilitate reports.
42
+ - List default values in menu help.
33
43
 
34
- ## [0.2.3] - 2022-03
44
+ ## [0.2.4] - 2022-04-01
45
+
46
+ ### Added
47
+
48
+ - Command `--list-recent-scripts` to list the last *N* saved scripts.
49
+ - Command `--run-last-script` to re-run the last saved script.
50
+ - Command `--select-recent-script` to select and execute a recently saved script.
51
+
52
+ | YAML Name | Environment Variable | Option Name | Default | Purpose |
53
+ | :--- | :--- | :--- | :--- | :--- |
54
+ | list_count | MDE_LIST_COUNT | `--list-count` | `16` | Max. items to return in list |
55
+ | logged_stdout_filename_prefix | MDE_LOGGED_STDOUT_FILENAME_PREFIX | | `mde` | Name prefix for stdout files |
56
+ | save_execution_output | MDE_SAVE_EXECUTION_OUTPUT | `--save-execution-output` | False | Save standard output of the executed script |
57
+ | saved_script_filename_prefix | MDE_SAVED_SCRIPT_FILENAME_PREFIX | | `mde` | Name prefix for saved scripts |
58
+ | saved_script_folder | MDE_SAVED_SCRIPT_FOLDER | `--saved-script-folder` | `logs` | Saved script folder |
59
+ | saved_script_glob | MDE_SAVED_SCRIPT_GLOB | | `mde_*.sh` | Glob matching saved scripts |
60
+ | saved_stdout_folder | MDE_SAVED_STDOUT_FOLDER | `--saved-stdout-folder` | `logs` | Saved stdout folder |
61
+
62
+ ### Changed
63
+
64
+ - Fix saving of executed script.
65
+ - Sort configuration keys output by `-0` (Show configuration.)
66
+
67
+ ## [0.2.3] - 2022-03-29
35
68
 
36
69
  ### Added
37
70
 
@@ -45,9 +78,7 @@
45
78
  ### Changed
46
79
 
47
80
  - Naming saved script files: The file name contains the time stamp, document name, and block name.
48
-
49
81
  - Renamed folder with fixtures.
50
-
51
82
  - Command options:
52
83
 
53
84
  | YAML Name | Environment Variable | Option Name | Default | Purpose |
@@ -70,8 +101,8 @@
70
101
  | block_name_excluded_match | MDE_BLOCK_NAME_EXCLUDED_MATCH | `^\(.+\)$` |
71
102
  | block_name_match | MDE_BLOCK_NAME_MATCH | `:(?<title>\S+)( \|$)` |
72
103
  | block_required_scan | MDE_BLOCK_REQUIRED_SCAN | `\+\S+` |
73
- | fenced_start_and_end_match | MDE_FENCED_START_AND_END_MATCH | `^\`{3,}` |
74
- | fenced_start_ex_match | MDE_FENCED_START_EX_MATCH | `^\`{3,}(?<shell>[^\`\s]*) *(?<name>.*)$` |
104
+ | fenced_start_and_end_match | MDE_FENCED_START_AND_END_MATCH | ``^`{3,}`` |
105
+ | fenced_start_ex_match | MDE_FENCED_START_EX_MATCH | ``^`{3,}(?<shell>[^`\s]*) *(?<name>.*)$`` |
75
106
  | heading1_match | MDE_HEADING1_MATCH | `^# *(?<name>[^#]*?) *$` |
76
107
  | heading2_match | MDE_HEADING2_MATCH | `^## *(?<name>[^#]*?) *$` |
77
108
  | heading3_match | MDE_HEADING3_MATCH | `^### *(?<name>.+?) *$` |
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- markdown_exec (0.2.3)
4
+ markdown_exec (0.2.5)
5
5
  open3 (~> 0.1.1)
6
6
  optparse (~> 0.1.1)
7
7
  tty-prompt (~> 0.23.1)
data/README.md CHANGED
@@ -1,30 +1,35 @@
1
1
  # MarkdownExec
2
2
 
3
- Interactively select and execute fenced code blocks in markdown files. Build complex scripts by naming and requiring blocks.
3
+ Interactively select and execute fenced code blocks in markdown files. Build complex scripts by naming and requiring blocks. Log resulting scripts and output. Re-run scripts.
4
4
 
5
- * Code blocks may be named.
5
+ * Code blocks may be named. Named blocks can be required by other blocks.
6
6
 
7
- * Named blocks can be required by other blocks.
7
+ * The user-selected code block, and all required blocks, are arranged into a script in the order they appear in the markdown file. The script can be presented for approval prior to execution.
8
8
 
9
- * The user-selected code block, and all required blocks, are arranged in the order they appear in the markdown file.
9
+ * Executed scripts can be saved. Saved scripts can be listed, selected, and executed.
10
10
 
11
- * The code is presented for approval prior to execution.
11
+ * Output from executed scripts can be saved.
12
12
 
13
13
  ## Screenshots
14
14
 
15
15
  ### Select a file
16
+
16
17
  ![Select a file](/assets/select_a_file.png)
17
18
 
18
19
  ### Select a block
20
+
19
21
  ![Select a block](/assets/select_a_block.png)
20
22
 
21
23
  ### Approve code
24
+
22
25
  ![Approve code](/assets/approve_code.png)
23
26
 
24
27
  ### Output
28
+
25
29
  ![Output of execution](/assets/output_of_execution.png)
26
30
 
27
31
  ### Example blocks
32
+
28
33
  ![Example blocks](/assets/example_blocks.png)
29
34
 
30
35
  ## Installation
@@ -34,39 +39,111 @@ Install:
34
39
 
35
40
  ## Usage
36
41
 
37
- ### `mde --help`
42
+ ### Help
43
+
44
+ #### `mde --help`
45
+
38
46
  Displays help information.
39
47
 
40
- ### `mde`
48
+ ### Basic
49
+
50
+ #### `mde`
51
+
41
52
  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.
42
53
 
43
- ### `mde my.md` or `mde -f my.md`
54
+ #### `mde my.md` or `mde -f my.md`
55
+
44
56
  Select a block to execute from `my.md`.
45
57
 
46
- ### `mde .` or `mde -p .`
58
+ #### `mde .` or `mde -p .`
47
59
 
48
60
  Select a markdown file in the current folder. Select a block to execute from that file.
49
61
 
50
- ### `mde --list-blocks`
62
+ ### Report documents and blocks
63
+
64
+ #### `mde --list-blocks`
65
+
51
66
  List all blocks in the all the markdown documents in the current folder.
52
67
 
53
- ### `mde --list-docs`
68
+ #### `mde --list-docs`
69
+
54
70
  List all markdown documents in the current folder.
55
71
 
72
+ ### Configuration
73
+
74
+ #### `mde --list-default-env` or `mde --list-default-yaml`
75
+
76
+ List default values that can be set in configuration file, environment, and command line.
77
+
78
+ #### `mde -0`
79
+
80
+ Show current configuation values that will be applied to the current run. Does not interrupt processing.
81
+
82
+ ### Save scripts
83
+
84
+ #### `mde --save-executed-script 1`
85
+
86
+ Save executed script in saved script folder.
87
+
88
+ #### `mde --list-recent-scripts`
89
+
90
+ List recent saved scripts in saved script folder.
91
+
92
+ #### `mde --select-recent-script`
93
+
94
+ Select and execute a recently saved script in saved script folder.
95
+
96
+ ### Save output
97
+
98
+ #### `mde --save-execution-output 1`
99
+
100
+ Save execution output in saved output folder.
101
+
56
102
  ## Behavior
103
+
57
104
  * If no file and no folder are specified, blocks within `./README.md` are presented.
58
105
  * If a file is specified, its blocks are presented.
59
106
  * If a folder is specified, its files are presented. When a file is selected, its blocks are presented.
60
107
 
61
108
  ## Configuration
62
- While starting up, reads the YAML configuration file `.mde.yml` in the current folder if it exists.
63
109
 
64
- e.g. Use to set the default file for the current folder.
110
+ ### Environment Variables
111
+
112
+ When executed, `mde` reads the current environment.
113
+ * Configuration in current and children shells, e.g. `export MDE_SAVE_EXECUTED_SCRIPT=1`.
114
+ * Configuration for the current command, e.g. `MDE_SAVE_EXECUTED_SCRIPT=1 mde`.
115
+
116
+ ### Configuration Files
117
+
118
+ * Configuration in all shells, e.g. environment variables set in your user's `~/.bashrc` or `~/.bash_profile` files.
119
+ * Configuration in the optional file `.mde.yml` in the current folder. .e.g. `save_executed_script: true`
120
+ * Configuration in a YAML file and read while parsing the inputs, e.g. `--config my_path/my_file.yml`
65
121
 
66
- * `filename: CHANGELOG.md` sets the file to open.
67
- * `folder: documents` sets the folder to search for default or specified files.
122
+ ### Program Arguments
123
+
124
+ * Configuration in command options, e.g. `mde --save-executed-script 1`
125
+
126
+ ## Representing boolean values
127
+
128
+ Boolean values expressed as strings are interpreted as:
129
+ | String | Boolean |
130
+ | :---: | :---: |
131
+ | *empty string* | False |
132
+ | `0` | False |
133
+ | `1` | True |
134
+ | *anything else* | True |
135
+
136
+ E.g. `opt1=1` will set option `opt1` to True.
137
+
138
+ Boolean options configured with environment variables:
139
+ - Set to `1` or non-empty value to save executed scripts; empty or `0` to disable saving.
140
+ e.g. `export MDE_SAVE_EXECUTED_SCRIPT=1`
141
+ e.g. `export MDE_SAVE_EXECUTED_SCRIPT=`
142
+ - Specify variable on command line.
143
+ e.g. `MDE_SAVE_EXECUTED_SCRIPT=1 mde`
68
144
 
69
145
  # Example blocks
146
+
70
147
  When prompted, select either the `awake` or `asleep` block.
71
148
 
72
149
  ``` :(day)
@@ -89,6 +166,17 @@ export ACTIVITY=asleep
89
166
  echo "$TIME -> $ACTIVITY"
90
167
  ```
91
168
 
169
+ ``` :missing_command
170
+ fail
171
+ ```
172
+
173
+ ``` :exit_value
174
+ echo "a"
175
+ echo "b"
176
+ echo "c" >>/dev/stderr
177
+ grep nx Gemfile
178
+ ```
179
+
92
180
  # License
93
181
 
94
182
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -2,7 +2,7 @@
2
2
 
3
3
  module MarkdownExec
4
4
  APP_NAME = 'MDE'
5
- APP_DESC = 'Markdown block executor'
5
+ APP_DESC = 'Markdown Executor'
6
6
  GEM_NAME = 'markdown_exec'
7
- VERSION = '0.2.3'
7
+ VERSION = '0.2.6'
8
8
  end
data/lib/markdown_exec.rb CHANGED
@@ -14,21 +14,23 @@ require 'yaml'
14
14
  # else true
15
15
 
16
16
  def env_bool(name, default: false)
17
- return default if (val = ENV[name]).nil?
17
+ return default if name.nil? || (val = ENV[name]).nil?
18
18
  return false if val.empty? || val == '0'
19
19
 
20
20
  true
21
21
  end
22
22
 
23
23
  def env_int(name, default: 0)
24
- return default if (val = ENV[name]).nil?
24
+ return default if name.nil? || (val = ENV[name]).nil?
25
25
  return default if val.empty?
26
26
 
27
27
  val.to_i
28
28
  end
29
29
 
30
30
  def env_str(name, default: '')
31
- ENV[name] || default
31
+ return default if name.nil? || (val = ENV[name]).nil?
32
+
33
+ val || default
32
34
  end
33
35
 
34
36
  $pdebug = env_bool 'MDE_DEBUG'
@@ -42,7 +44,12 @@ BLOCK_SIZE = 1024
42
44
 
43
45
  class Object # rubocop:disable Style/Documentation
44
46
  def present?
45
- self && !blank?
47
+ case self.class.to_s
48
+ when 'FalseClass', 'TrueClass'
49
+ true
50
+ else
51
+ self && (!respond_to?(:blank?) || !blank?)
52
+ end
46
53
  end
47
54
  end
48
55
 
@@ -94,45 +101,28 @@ module MarkdownExec
94
101
  # options necessary to start, parse input, defaults for cli options
95
102
 
96
103
  def base_options
97
- {
98
- # commands
99
- list_blocks: false, # command
100
- list_docs: false, # command
101
-
102
- # command options
103
- filename: env_str('MDE_FILENAME', default: nil), # option Filename to open
104
- output_execution_summary: env_bool('MDE_OUTPUT_EXECUTION_SUMMARY', default: false), # option
105
- output_script: env_bool('MDE_OUTPUT_SCRIPT', default: false), # option
106
- output_stdout: env_bool('MDE_OUTPUT_STDOUT', default: true), # option
107
- path: env_str('MDE_PATH', default: nil), # option Folder to search for files
108
- save_executed_script: env_bool('MDE_SAVE_EXECUTED_SCRIPT', default: false), # option
109
- saved_script_folder: env_str('MDE_SAVED_SCRIPT_FOLDER', default: 'logs'), # option
110
- user_must_approve: env_bool('MDE_USER_MUST_APPROVE', default: true), # option Pause for user to approve script
111
-
112
- # configuration options
113
- block_name_excluded_match: env_str('MDE_BLOCK_NAME_EXCLUDED_MATCH', default: '^\(.+\)$'),
114
- block_name_match: env_str('MDE_BLOCK_NAME_MATCH', default: ':(?<title>\S+)( |$)'),
115
- block_required_scan: env_str('MDE_BLOCK_REQUIRED_SCAN', default: '\+\S+'),
116
- fenced_start_and_end_match: env_str('MDE_FENCED_START_AND_END_MATCH', default: '^`{3,}'),
117
- fenced_start_ex_match: env_str('MDE_FENCED_START_EX_MATCH', default: '^`{3,}(?<shell>[^`\s]*) *(?<name>.*)$'),
118
- heading1_match: env_str('MDE_HEADING1_MATCH', default: '^# *(?<name>[^#]*?) *$'),
119
- heading2_match: env_str('MDE_HEADING2_MATCH', default: '^## *(?<name>[^#]*?) *$'),
120
- heading3_match: env_str('MDE_HEADING3_MATCH', default: '^### *(?<name>.+?) *$'),
121
- md_filename_glob: env_str('MDE_MD_FILENAME_GLOB', default: '*.[Mm][Dd]'),
122
- md_filename_match: env_str('MDE_MD_FILENAME_MATCH', default: '.+\\.md'),
123
- mdheadings: true, # use headings (levels 1,2,3) in block lable
124
- select_page_height: env_int('MDE_SELECT_PAGE_HEIGHT', default: 12)
125
- }
104
+ menu_data
105
+ .map do |_long_name, _short_name, env_var, _arg_name, _description, opt_name, default, _proc1| # rubocop:disable Metrics/ParameterLists
106
+ next unless opt_name.present?
107
+
108
+ [opt_name, env_bool(env_var, default: value_for_hash(default))]
109
+ end.compact.to_h.merge(
110
+ {
111
+ mdheadings: true, # use headings (levels 1,2,3) in block lable
112
+ menu_with_exit: true
113
+ }
114
+ ).tap_inspect format: :yaml
126
115
  end
127
116
 
128
117
  def default_options
129
118
  {
130
119
  bash: true, # bash block parsing in get_block_summary()
131
120
  exclude_expect_blocks: true,
132
- exclude_matching_block_names: true, # exclude hidden blocks
121
+ hide_blocks_by_name: true,
133
122
  output_saved_script_filename: false,
134
- prompt_select_block: 'Choose a block:', # in select_and_approve_block()
135
- prompt_select_md: 'Choose a file:', # in select_md_file()
123
+ prompt_approve_block: 'Process?',
124
+ prompt_select_block: 'Choose a block:',
125
+ prompt_select_md: 'Choose a file:',
136
126
  saved_script_filename: nil, # calculated
137
127
  struct: true # allow get_block_summary()
138
128
  }
@@ -149,7 +139,7 @@ module MarkdownExec
149
139
  display_command(opts, required_blocks) if opts[:output_script] || opts[:user_must_approve]
150
140
 
151
141
  allow = true
152
- allow = @prompt.yes? 'Process?' if opts[:user_must_approve]
142
+ allow = @prompt.yes? opts[:prompt_approve_block] if opts[:user_must_approve]
153
143
  opts[:ir_approve] = allow
154
144
  selected = get_block_by_name blocks_in_file, opts[:block_name]
155
145
 
@@ -171,6 +161,7 @@ module MarkdownExec
171
161
  end
172
162
 
173
163
  def command_execute(opts, cmd2)
164
+ @execute_files = Hash.new([])
174
165
  @execute_options = opts
175
166
  @execute_started_at = Time.now.utc
176
167
  Open3.popen3(cmd2) do |stdin, stdout, stderr|
@@ -186,7 +177,6 @@ module MarkdownExec
186
177
  # readable = ready[0]
187
178
  # # writable = ready[1]
188
179
  # # exceptions = ready[2]
189
- @execute_files = Hash.new([])
190
180
  ready.each.with_index do |readable, ind|
191
181
  readable.each do |f|
192
182
  block = f.read_nonblock(BLOCK_SIZE)
@@ -203,9 +193,11 @@ module MarkdownExec
203
193
  @execute_completed_at = Time.now.utc
204
194
  end
205
195
  rescue Errno::ENOENT => e
196
+ # error triggered by missing command in script
206
197
  @execute_aborted_at = Time.now.utc
207
198
  @execute_error_message = e.message
208
199
  @execute_error = e
200
+ @execute_files[1] = e.message
209
201
  fout "Error ENOENT: #{e.inspect}"
210
202
  end
211
203
 
@@ -222,37 +214,60 @@ module MarkdownExec
222
214
  required_blocks.each { |cb| fout cb }
223
215
  end
224
216
 
225
- def exec_block(options, block_name = '')
217
+ def exec_block(options, _block_name = '')
226
218
  options = default_options.merge options
227
219
  update_options options, over: false
228
220
 
229
221
  # document and block reports
230
222
  #
231
223
  files = list_files_per_options(options)
224
+ if @options[:list_blocks]
225
+ fout_list (files.map do |file|
226
+ make_block_labels(filename: file, struct: true)
227
+ end).flatten(1)
228
+ return
229
+ end
230
+
231
+ if @options[:list_default_yaml]
232
+ fout_list list_default_yaml
233
+ return
234
+ end
235
+
232
236
  if @options[:list_docs]
233
237
  fout_list files
234
238
  return
235
239
  end
236
240
 
237
- if @options[:list_blocks]
238
- fout_list (files.map do |file|
239
- make_block_labels(filename: file, struct: true)
240
- end).flatten(1)
241
+ if @options[:list_default_env]
242
+ fout_list list_default_env
243
+ return
244
+ end
245
+
246
+ if @options[:list_recent_scripts]
247
+ fout_list list_recent_scripts
248
+ return
249
+ end
250
+
251
+ if @options[:run_last_script]
252
+ run_last_script
253
+ return
254
+ end
255
+
256
+ if @options[:select_recent_script]
257
+ select_recent_script
241
258
  return
242
259
  end
243
260
 
244
261
  # process
245
262
  #
263
+ @options[:filename] = select_md_file(files)
246
264
  select_and_approve_block(
247
265
  bash: true,
248
- block_name: block_name,
249
- filename: select_md_file(files),
250
266
  struct: true
251
267
  )
252
-
253
268
  fout "saved_filespec: #{@execute_script_filespec}" if @options[:output_saved_script_filename]
254
-
255
- output_execution_summary if @options[:output_execution_summary]
269
+ save_execution_output
270
+ output_execution_summary
256
271
  end
257
272
 
258
273
  # standard output; not for debug
@@ -351,14 +366,37 @@ module MarkdownExec
351
366
  blocks.tap_inspect
352
367
  end
353
368
 
369
+ def list_default_env
370
+ menu_data
371
+ .map do |_long_name, _short_name, env_var, _arg_name, description, _opt_name, default, _proc1| # rubocop:disable Metrics/ParameterLists
372
+ next unless env_var.present?
373
+
374
+ [
375
+ "#{env_var}=#{value_for_cli default}",
376
+ description.present? ? description : nil
377
+ ].compact.join(' # ')
378
+ end.compact.sort
379
+ end
380
+
381
+ def list_default_yaml
382
+ menu_data
383
+ .map do |_long_name, _short_name, _env_var, _arg_name, description, opt_name, default, _proc1| # rubocop:disable Metrics/ParameterLists
384
+ next unless opt_name.present? && default.present?
385
+
386
+ [
387
+ "#{opt_name}: #{value_for_yaml default}",
388
+ description.present? ? description : nil
389
+ ].compact.join(' # ')
390
+ end.compact.sort
391
+ end
392
+
354
393
  def list_files_per_options(options)
355
- default_filename = 'README.md'
356
- default_folder = '.'
357
- if options[:filename]&.present?
358
- list_files_specified(options[:filename], options[:path], default_filename, default_folder)
359
- else
360
- list_files_specified(nil, options[:path], default_filename, default_folder)
361
- end.tap_inspect
394
+ list_files_specified(
395
+ options[:filename]&.present? ? options[:filename] : nil,
396
+ options[:path],
397
+ 'README.md',
398
+ '.'
399
+ ).tap_inspect
362
400
  end
363
401
 
364
402
  def list_files_specified(specified_filename, specified_folder, default_filename, default_folder, filetree = nil)
@@ -394,7 +432,7 @@ module MarkdownExec
394
432
  opts = optsmerge call_options, options_block
395
433
  block_name_excluded_match = Regexp.new opts[:block_name_excluded_match]
396
434
  list_blocks_in_file(opts).map do |block|
397
- next if opts[:exclude_matching_block_names] && block[:name].match(block_name_excluded_match)
435
+ next if opts[:hide_blocks_by_name] && block[:name].match(block_name_excluded_match)
398
436
 
399
437
  block
400
438
  end.compact.tap_inspect
@@ -413,6 +451,11 @@ module MarkdownExec
413
451
  .tap_inspect
414
452
  end
415
453
 
454
+ def list_recent_scripts
455
+ Dir.glob(File.join(@options[:saved_script_folder],
456
+ @options[:saved_script_glob])).sort[0..(options[:list_count] - 1)].reverse.tap_inspect
457
+ end
458
+
416
459
  def make_block_label(block, call_options = {})
417
460
  opts = options.merge(call_options)
418
461
  if opts[:mdheadings]
@@ -426,21 +469,117 @@ module MarkdownExec
426
469
  def make_block_labels(call_options = {})
427
470
  opts = options.merge(call_options)
428
471
  list_blocks_in_file(opts).map do |block|
429
- # next if opts[:exclude_matching_block_names] && block[:name].match(%r{^:\(.+\)$})
472
+ # next if opts[:hide_blocks_by_name] && block[:name].match(%r{^:\(.+\)$})
430
473
 
431
474
  make_block_label block, opts
432
475
  end.compact.tap_inspect
433
476
  end
434
477
 
478
+ def menu_data
479
+ val_as_bool = ->(value) { value.to_i != 0 }
480
+ val_as_int = ->(value) { value.to_i }
481
+ val_as_str = ->(value) { value.to_s }
482
+ val_true = ->(_) { true }
483
+
484
+ summary_head = [
485
+ ['config', nil, nil, 'PATH', 'Read configuration file', nil, '.', lambda { |value|
486
+ read_configuration_file! options, value
487
+ }],
488
+ ['debug', 'd', 'MDE_DEBUG', 'BOOL', 'Debug output', nil, false, ->(value) { $pdebug = value.to_i != 0 }]
489
+ ]
490
+
491
+ # rubocop:disable Layout/LineLength
492
+ summary_body = [
493
+ ['filename', 'f', 'MDE_FILENAME', 'RELATIVE', 'Name of document', :filename, nil, val_as_str],
494
+ ['list-blocks', nil, nil, nil, 'List blocks', :list_blocks, nil, val_true],
495
+ ['list-count', nil, 'MDE_LIST_COUNT', 'NUM', 'Max. items to return in list', :list_count, 16, val_as_int],
496
+ ['list-default-env', nil, nil, nil, 'List default configuration as environment variables', :list_default_env, nil, val_true],
497
+ ['list-default-yaml', nil, nil, nil, 'List default configuration as YAML', :list_default_yaml, nil, val_true],
498
+ ['list-docs', nil, nil, nil, 'List docs in current folder', :list_docs, nil, val_true],
499
+ ['list-recent-scripts', nil, nil, nil, 'List recent saved scripts', :list_recent_scripts, nil, val_true],
500
+ ['logged-stdout-filename-prefix', nil, 'MDE_LOGGED_STDOUT_FILENAME_PREFIX', 'NAME', 'Name prefix for stdout files', :logged_stdout_filename_prefix, 'mde', val_as_str],
501
+ ['output-execution-summary', nil, 'MDE_OUTPUT_EXECUTION_SUMMARY', 'BOOL', 'Display summary for execution', :output_execution_summary, false, val_as_bool],
502
+ ['output-script', nil, 'MDE_OUTPUT_SCRIPT', 'BOOL', 'Display script prior to execution', :output_script, false, val_as_bool],
503
+ ['output-stdout', nil, 'MDE_OUTPUT_STDOUT', 'BOOL', 'Display standard output from execution', :output_stdout, true, val_as_bool],
504
+ ['path', 'p', 'MDE_PATH', 'PATH', 'Path to documents', :path, nil, val_as_str],
505
+ ['run-last-script', nil, nil, nil, 'Run most recently saved script', :run_last_script, nil, val_true],
506
+ ['select-recent-script', nil, nil, nil, 'Select and execute a recently saved script', :select_recent_script, nil, val_true],
507
+ ['save-executed-script', nil, 'MDE_SAVE_EXECUTED_SCRIPT', 'BOOL', 'Save executed script', :save_executed_script, false, val_as_bool],
508
+ ['save-execution-output', nil, 'MDE_SAVE_EXECUTION_OUTPUT', 'BOOL', 'Save standard output of the executed script', :save_execution_output, false, val_as_bool],
509
+ ['saved-script-filename-prefix', nil, 'MDE_SAVED_SCRIPT_FILENAME_PREFIX', 'NAME', 'Name prefix for saved scripts', :saved_script_filename_prefix, 'mde', val_as_str],
510
+ ['saved-script-folder', nil, 'MDE_SAVED_SCRIPT_FOLDER', 'SPEC', 'Saved script folder', :saved_script_folder, 'logs', val_as_str],
511
+ ['saved-script-glob', nil, 'MDE_SAVED_SCRIPT_GLOB', 'SPEC', 'Glob matching saved scripts', :saved_script_glob, 'mde_*.sh', val_as_str],
512
+ ['saved-stdout-folder', nil, 'MDE_SAVED_STDOUT_FOLDER', 'SPEC', 'Saved stdout folder', :saved_stdout_folder, 'logs', val_as_str],
513
+ ['user-must-approve', nil, 'MDE_USER_MUST_APPROVE', 'BOOL', 'Pause for user to approve script', :user_must_approve, true, val_as_bool]
514
+ ]
515
+ # rubocop:enable Layout/LineLength
516
+
517
+ # rubocop:disable Style/Semicolon
518
+ summary_tail = [
519
+ [nil, '0', nil, nil, 'Show current configuration values',
520
+ nil, nil, ->(_) { options_finalize options; fout sorted_keys(options).to_yaml }],
521
+ ['help', 'h', nil, nil, 'App help',
522
+ nil, nil, ->(_) { fout menu_help; exit }],
523
+ ['version', 'v', nil, nil, 'App version',
524
+ nil, nil, ->(_) { fout MarkdownExec::VERSION; exit }],
525
+ ['exit', 'x', nil, nil, 'Exit app',
526
+ nil, nil, ->(_) { exit }]
527
+ ]
528
+ # rubocop:enable Style/Semicolon
529
+
530
+ env_vars = [
531
+ [nil, nil, 'MDE_BLOCK_NAME_EXCLUDED_MATCH', nil, 'Pattern for blocks to hide from user-selection',
532
+ :block_name_excluded_match, '^\(.+\)$', nil],
533
+ [nil, nil, 'MDE_BLOCK_NAME_MATCH', nil, '', :block_name_match, ':(?<title>\S+)( |$)', nil],
534
+ [nil, nil, 'MDE_BLOCK_REQUIRED_SCAN', nil, '', :block_required_scan, '\+\S+', nil],
535
+ [nil, nil, 'MDE_FENCED_START_AND_END_MATCH', nil, '', :fenced_start_and_end_match, '^`{3,}', nil],
536
+ [nil, nil, 'MDE_FENCED_START_EX_MATCH', nil, '', :fenced_start_ex_match,
537
+ '^`{3,}(?<shell>[^`\s]*) *(?<name>.*)$', nil],
538
+ [nil, nil, 'MDE_HEADING1_MATCH', nil, '', :heading1_match, '^# *(?<name>[^#]*?) *$', nil],
539
+ [nil, nil, 'MDE_HEADING2_MATCH', nil, '', :heading2_match, '^## *(?<name>[^#]*?) *$', nil],
540
+ [nil, nil, 'MDE_HEADING3_MATCH', nil, '', :heading3_match, '^### *(?<name>.+?) *$', nil],
541
+ [nil, nil, 'MDE_MD_FILENAME_GLOB', nil, '', :md_filename_glob, '*.[Mm][Dd]', nil],
542
+ [nil, nil, 'MDE_MD_FILENAME_MATCH', nil, '', :md_filename_match, '.+\\.md', nil],
543
+ [nil, nil, 'MDE_SELECT_PAGE_HEIGHT', nil, '', :select_page_height, 12, nil]
544
+ # [nil, nil, 'MDE_', nil, '', nil, '', nil],
545
+ ]
546
+
547
+ summary_head + summary_body + summary_tail + env_vars
548
+ end
549
+
550
+ def menu_help
551
+ @option_parser.help
552
+ end
553
+
435
554
  def option_exclude_blocks(opts, blocks)
436
555
  block_name_excluded_match = Regexp.new opts[:block_name_excluded_match]
437
- if opts[:exclude_matching_block_names]
556
+ if opts[:hide_blocks_by_name]
438
557
  blocks.reject { |block| block[:name].match(block_name_excluded_match) }
439
558
  else
440
559
  blocks
441
560
  end
442
561
  end
443
562
 
563
+ ## post-parse options configuration
564
+ #
565
+ def options_finalize(rest)
566
+ ## position 0: file or folder (optional)
567
+ #
568
+ if (pos = rest.fetch(0, nil))&.present?
569
+ if Dir.exist?(pos)
570
+ @options[:path] = pos
571
+ elsif File.exist?(pos)
572
+ @options[:filename] = pos
573
+ else
574
+ raise "Invalid parameter: #{pos}"
575
+ end
576
+ end
577
+
578
+ ## position 1: block name (optional)
579
+ #
580
+ @options[:block_name] = rest.fetch(1, nil)
581
+ end
582
+
444
583
  def optsmerge(call_options = {}, options_block = nil)
445
584
  class_call_options = @options.merge(call_options || {})
446
585
  if options_block
@@ -451,6 +590,8 @@ module MarkdownExec
451
590
  end
452
591
 
453
592
  def output_execution_summary
593
+ return unless @options[:output_execution_summary]
594
+
454
595
  fout_section 'summary', {
455
596
  execute_aborted_at: @execute_aborted_at,
456
597
  execute_completed_at: @execute_completed_at,
@@ -463,6 +604,14 @@ module MarkdownExec
463
604
  }
464
605
  end
465
606
 
607
+ def prompt_with_quit(prompt_text, items, opts = {})
608
+ exit_option = '* Exit'
609
+ sel = @prompt.select prompt_text,
610
+ items + (@options[:menu_with_exit] ? [exit_option] : []),
611
+ opts
612
+ sel == exit_option ? nil : sel
613
+ end
614
+
466
615
  def read_configuration_file!(options, configuration_path)
467
616
  return unless File.exist?(configuration_path)
468
617
 
@@ -494,19 +643,11 @@ module MarkdownExec
494
643
  #
495
644
  @options = base_options
496
645
 
497
- ## post-parse options configuration
498
- #
499
- options_finalize = ->(_options) {}
500
-
501
- proc_self = ->(value) { value }
502
- proc_to_i = ->(value) { value.to_i != 0 }
503
- proc_true = ->(_) { true }
504
-
505
- # read local configuration file
646
+ ## read local configuration file
506
647
  #
507
648
  read_configuration_file! @options, ".#{MarkdownExec::APP_NAME.downcase}.yml"
508
649
 
509
- option_parser = OptionParser.new do |opts|
650
+ @option_parser = option_parser = OptionParser.new do |opts|
510
651
  executable_name = File.basename($PROGRAM_NAME)
511
652
  opts.banner = [
512
653
  "#{MarkdownExec::APP_NAME}" \
@@ -514,55 +655,16 @@ module MarkdownExec
514
655
  "Usage: #{executable_name} [path] [filename] [options]"
515
656
  ].join("\n")
516
657
 
517
- summary_head = [
518
- ['config', nil, nil, 'PATH', 'Read configuration file',
519
- nil, ->(value) { read_configuration_file! options, value }],
520
- ['debug', 'd', 'MDE_DEBUG', 'BOOL', 'Debug output',
521
- nil, ->(value) { $pdebug = value.to_i != 0 }]
522
- ]
523
-
524
- summary_body = [
525
- ['filename', 'f', 'MDE_FILENAME', 'RELATIVE', 'Name of document',
526
- :filename, proc_self],
527
- ['list-blocks', nil, nil, nil, 'List blocks',
528
- :list_blocks, proc_true],
529
- ['list-docs', nil, nil, nil, 'List docs in current folder',
530
- :list_docs, proc_true],
531
- ['output-execution-summary', nil, 'MDE_OUTPUT_EXECUTION_SUMMARY', 'BOOL', 'Display summary for execution',
532
- :output_execution_summary, proc_to_i],
533
- ['output-script', nil, 'MDE_OUTPUT_SCRIPT', 'BOOL', 'Display script',
534
- :output_script, proc_to_i],
535
- ['output-stdout', nil, 'MDE_OUTPUT_STDOUT', 'BOOL', 'Display standard output from execution',
536
- :output_stdout, proc_to_i],
537
- ['path', 'p', 'MDE_PATH', 'PATH', 'Path to documents',
538
- :path, proc_self],
539
- ['save-executed-script', nil, 'MDE_SAVE_EXECUTED_SCRIPT', 'BOOL', 'Save executed script',
540
- :save_executed_script, proc_to_i],
541
- ['saved-script-folder', nil, 'MDE_SAVED_SCRIPT_FOLDER', 'SPEC', 'Saved script folder',
542
- :saved_script_folder, proc_self],
543
- ['user-must-approve', nil, 'MDE_USER_MUST_APPROVE', 'BOOL', 'Pause to approve execution',
544
- :user_must_approve, proc_to_i]
545
- ]
546
-
547
- # rubocop:disable Style/Semicolon
548
- summary_tail = [
549
- [nil, '0', nil, nil, 'Show configuration',
550
- nil, ->(_) { options_finalize.call options; fout options.to_yaml }],
551
- ['help', 'h', nil, nil, 'App help',
552
- nil, ->(_) { fout option_parser.help; exit }],
553
- ['version', 'v', nil, nil, 'App version',
554
- nil, ->(_) { fout MarkdownExec::VERSION; exit }],
555
- ['exit', 'x', nil, nil, 'Exit app',
556
- nil, ->(_) { exit }]
557
- ]
558
- # rubocop:enable Style/Semicolon
559
-
560
- (summary_head + summary_body + summary_tail)
561
- .map do |long_name, short_name, env_var, arg_name, description, opt_name, proc1| # rubocop:disable Metrics/ParameterLists
562
- opts.on(*[long_name.present? ? "--#{long_name}#{arg_name.present? ? (' ' + arg_name) : ''}" : nil,
658
+ menu_data
659
+ .map do |long_name, short_name, _env_var, arg_name, description, opt_name, default, proc1| # rubocop:disable Metrics/ParameterLists
660
+ next unless long_name.present? || short_name.present?
661
+
662
+ opts.on(*[if long_name.present?
663
+ "--#{long_name}#{arg_name.present? ? " #{arg_name}" : ''}"
664
+ end,
563
665
  short_name.present? ? "-#{short_name}" : nil,
564
666
  [description,
565
- env_var.present? ? "env: #{env_var}" : nil].compact.join(' - '),
667
+ default.present? ? "[#{value_for_cli default}]" : nil].compact.join(' '),
566
668
  lambda { |value|
567
669
  ret = proc1.call(value)
568
670
  options[opt_name] = ret if opt_name
@@ -574,27 +676,43 @@ module MarkdownExec
574
676
  option_parser.environment # env defaults to the basename of the program.
575
677
  rest = option_parser.parse! # (into: options)
576
678
 
577
- ## finalize configuration
578
- #
579
- options_finalize.call options
679
+ options_finalize rest
580
680
 
581
- ## position 0: file or folder (optional)
582
- #
583
- if (pos = rest.fetch(0, nil))&.present?
584
- if Dir.exist?(pos)
585
- options[:path] = pos
586
- elsif File.exist?(pos)
587
- options[:filename] = pos
588
- else
589
- raise "Invalid parameter: #{pos}"
590
- end
591
- end
681
+ exec_block options, options[:block_name]
682
+ end
592
683
 
593
- ## position 1: block name (optional)
594
- #
595
- block_name = rest.fetch(1, nil)
684
+ def run_last_script
685
+ filename = Dir.glob(File.join(@options[:saved_script_folder],
686
+ @options[:saved_script_glob])).sort[0..(options[:list_count] - 1)].last
687
+ filename.tap_inspect name: filename
688
+ mf = filename.match(/#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_(?<block>.+)\.sh/)
596
689
 
597
- exec_block options, block_name
690
+ @options[:block_name] = mf[:block]
691
+ @options[:filename] = "#{mf[:file]}.md" ### other extensions
692
+ @options[:save_executed_script] = false
693
+ select_and_approve_block
694
+ save_execution_output
695
+ output_execution_summary
696
+ end
697
+
698
+ def save_execution_output
699
+ return unless @options[:save_execution_output]
700
+
701
+ fne = File.basename(@options[:filename], '.*')
702
+ @options[:logged_stdout_filename] =
703
+ "#{[@options[:logged_stdout_filename_prefix], Time.now.utc.strftime('%F-%H-%M-%S'), fne,
704
+ @options[:block_name]].join('_')}.out.txt"
705
+ @options[:logged_stdout_filespec] = File.join @options[:saved_stdout_folder], @options[:logged_stdout_filename]
706
+ @logged_stdout_filespec = @options[:logged_stdout_filespec]
707
+ dirname = File.dirname(@options[:logged_stdout_filespec])
708
+ Dir.mkdir dirname unless File.exist?(dirname)
709
+ File.write(@options[:logged_stdout_filespec], @execute_files&.fetch(0, ''))
710
+ # @options[:logged_stderr_filename] =
711
+ # "#{[@options[:logged_stdout_filename_prefix], Time.now.utc.strftime('%F-%H-%M-%S'), fne,
712
+ # @options[:block_name]].join('_')}.err.txt"
713
+ # @options[:logged_stderr_filespec] = File.join @options[:saved_stdout_folder], @options[:logged_stderr_filename]
714
+ # @logged_stderr_filespec = @options[:logged_stderr_filespec]
715
+ # File.write(@options[:logged_stderr_filespec], @execute_files&.fetch(1, ''))
598
716
  end
599
717
 
600
718
  def select_and_approve_block(call_options = {}, &options_block)
@@ -608,9 +726,11 @@ module MarkdownExec
608
726
 
609
727
  return nil if block_labels.count.zero?
610
728
 
611
- sel = @prompt.select(pt, block_labels, per_page: opts[:select_page_height])
729
+ sel = prompt_with_quit pt, block_labels, per_page: opts[:select_page_height]
730
+ return nil if sel.nil?
731
+
612
732
  label_block = blocks_in_file.select { |block| block[:label] == sel }.fetch(0, nil)
613
- opts[:block_name] = label_block[:name]
733
+ opts[:block_name] = @options[:block_name] = label_block[:name]
614
734
  end
615
735
 
616
736
  approve_block opts, blocks_in_file
@@ -622,10 +742,32 @@ module MarkdownExec
622
742
  if files.count == 1
623
743
  files[0]
624
744
  elsif files.count >= 2
625
- @prompt.select(opts[:prompt_select_md].to_s, files, per_page: opts[:select_page_height])
745
+ prompt_with_quit opts[:prompt_select_md].to_s, files, per_page: opts[:select_page_height]
626
746
  end
627
747
  end
628
748
 
749
+ def select_recent_script
750
+ filename = prompt_with_quit @options[:prompt_select_md].to_s, list_recent_scripts,
751
+ per_page: @options[:select_page_height]
752
+ return if filename.nil?
753
+
754
+ mf = filename.match(/#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_(?<block>.+)\.sh/)
755
+
756
+ @options[:block_name] = mf[:block]
757
+ @options[:filename] = "#{mf[:file]}.md" ### other extensions
758
+ select_and_approve_block(
759
+ bash: true,
760
+ save_executed_script: false,
761
+ struct: true
762
+ )
763
+ save_execution_output
764
+ output_execution_summary
765
+ end
766
+
767
+ def sorted_keys(hash1)
768
+ hash1.keys.sort.to_h { |k| [k, hash1[k]] }
769
+ end
770
+
629
771
  def summarize_block(headings, title)
630
772
  { headings: headings, name: title, title: title }
631
773
  end
@@ -636,16 +778,57 @@ module MarkdownExec
636
778
  else
637
779
  @options.merge! opts
638
780
  end
639
- @options
781
+ @options.tap_inspect format: :yaml
640
782
  end
641
783
 
642
- def write_command_file(opts, required_blocks)
643
- return unless opts[:saved_script_filename].present?
784
+ def value_for_cli(value)
785
+ case value.class.to_s
786
+ when 'String'
787
+ "'#{value}'"
788
+ when 'FalseClass', 'TrueClass'
789
+ value ? '1' : '0'
790
+ when 'Integer'
791
+ value
792
+ else
793
+ value.to_s
794
+ end
795
+ end
796
+
797
+ def value_for_hash(value, default = nil)
798
+ return default if value.nil?
644
799
 
645
- fne = File.basename(opts[:filename], '.*').gsub(/[^a-z0-9]/i, '-') # scan(/[a-z0-9]/i).join
646
- bne = opts[:block_name].gsub(/[^a-z0-9]/i, '-') # scan(/[a-z0-9]/i).join
647
- opts[:saved_script_filename] = "mde_#{Time.now.utc.strftime '%F-%H-%M-%S'}_#{fne}_#{bne}.sh"
800
+ case value.class.to_s
801
+ when 'String', 'Integer', 'FalseClass', 'TrueClass'
802
+ value
803
+ when value.empty?
804
+ default
805
+ else
806
+ value.to_s
807
+ end
808
+ end
648
809
 
810
+ def value_for_yaml(value)
811
+ return default if value.nil?
812
+
813
+ case value.class.to_s
814
+ when 'String'
815
+ "'#{value}'"
816
+ when 'Integer'
817
+ value
818
+ when 'FalseClass', 'TrueClass'
819
+ value ? true : false
820
+ when value.empty?
821
+ default
822
+ else
823
+ value.to_s
824
+ end
825
+ end
826
+
827
+ def write_command_file(opts, required_blocks)
828
+ fne = File.basename(opts[:filename], '.*')
829
+ opts[:saved_script_filename] =
830
+ "#{[opts[:saved_script_filename_prefix], Time.now.utc.strftime('%F-%H-%M-%S'), fne,
831
+ opts[:block_name]].join('_')}.sh"
649
832
  @options[:saved_filespec] = File.join opts[:saved_script_folder], opts[:saved_script_filename]
650
833
  @execute_script_filespec = @options[:saved_filespec]
651
834
  dirname = File.dirname(@options[:saved_filespec])
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.2.3
4
+ version: 0.2.6
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-30 00:00:00.000000000 Z
11
+ date: 2022-04-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: open3
@@ -67,7 +67,8 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: 0.2.0
69
69
  description: Interactively select and execute fenced code blocks in markdown files.
70
- Build complex scripts by naming and requiring blocks.
70
+ Build complex scripts by naming and requiring blocks. Log resulting scripts and
71
+ output. Re-run scripts.
71
72
  email:
72
73
  - fareed@phomento.com
73
74
  executables:
@@ -127,5 +128,5 @@ requirements: []
127
128
  rubygems_version: 3.2.32
128
129
  signing_key:
129
130
  specification_version: 4
130
- summary: Execute shell blocks in markdown files.
131
+ summary: Interactively select and execute fenced code blocks in markdown files.
131
132
  test_files: []