markdown_exec 0.2.3 → 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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: []