markdown_exec 3.5.1 → 3.5.2
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/.ai-agent-instructions +54 -0
- data/.cursorrules +198 -0
- data/.rubocop.wide.yml +5 -0
- data/.rubocop.yml +7 -2
- data/CHANGELOG.md +12 -1
- data/Gemfile.lock +1 -1
- data/Rakefile +2 -0
- data/ai-principles.md +516 -0
- data/architecture-decisions.md +190 -0
- data/bats/block-hide.bats +1 -1
- data/bats/block-type-bash.bats +5 -5
- data/bats/block-type-link.bats +1 -1
- data/bats/block-type-opts.bats +3 -3
- data/bats/block-type-port.bats +2 -2
- data/bats/block-type-shell-require-ux.bats +2 -2
- data/bats/block-type-ux-allowed.bats +4 -4
- data/bats/block-type-ux-auto.bats +1 -1
- data/bats/block-type-ux-chained.bats +1 -1
- data/bats/block-type-ux-default.bats +1 -1
- data/bats/block-type-ux-echo-hash-transform.bats +1 -1
- data/bats/block-type-ux-echo-hash.bats +2 -2
- data/bats/block-type-ux-echo.bats +3 -3
- data/bats/block-type-ux-exec-hash-transform.bats +1 -1
- data/bats/block-type-ux-exec-hash.bats +2 -2
- data/bats/block-type-ux-exec.bats +1 -1
- data/bats/block-type-ux-force.bats +1 -1
- data/bats/block-type-ux-formats.bats +1 -1
- data/bats/block-type-ux-hidden.bats +1 -1
- data/bats/block-type-ux-invalid.bats +1 -1
- data/bats/block-type-ux-readonly.bats +1 -1
- data/bats/block-type-ux-require-chained.bats +2 -2
- data/bats/block-type-ux-require-context.bats +2 -2
- data/bats/block-type-ux-require.bats +2 -2
- data/bats/block-type-ux-required-variables.bats +1 -1
- data/bats/block-type-ux-row-format.bats +1 -1
- data/bats/block-type-ux-sources.bats +4 -4
- data/bats/block-type-ux-transform.bats +1 -1
- data/bats/block-type-vars.bats +3 -3
- data/bats/border.bats +1 -1
- data/bats/cli.bats +11 -11
- data/bats/command-substitution-options.bats +2 -2
- data/bats/command-substitution.bats +1 -1
- data/bats/document-shell.bats +1 -1
- data/bats/history.bats +5 -5
- data/bats/import-conflict.bats +1 -1
- data/bats/import-directive-line-continuation.bats +1 -1
- data/bats/import-directive-parameter-symbols.bats +1 -1
- data/bats/import-duplicates.bats +6 -6
- data/bats/import-parameter-symbols.bats +1 -1
- data/bats/import-with-text-substitution.bats +1 -1
- data/bats/import.bats +3 -3
- data/bats/indented-block-type-vars.bats +1 -1
- data/bats/indented-multi-line-output.bats +1 -1
- data/bats/line-decor-dynamic.bats +1 -1
- data/bats/line-wrapping.bats +1 -1
- data/bats/load-vars-state-demo.bats +4 -4
- data/bats/markup.bats +4 -4
- data/bats/mde.bats +4 -4
- data/bats/option-expansion.bats +1 -1
- data/bats/options-collapse.bats +4 -4
- data/bats/options.bats +47 -17
- data/bats/plain.bats +1 -1
- data/bats/publish.bats +2 -2
- data/bats/table-column-truncate.bats +1 -1
- data/bats/table.bats +2 -2
- data/bats/variable-expansion-multiline.bats +1 -1
- data/bats/variable-expansion.bats +6 -6
- data/conversation-template.md +611 -0
- data/docs/block-execution-modes.md +177 -0
- data/docs/block-filtering.md +252 -0
- data/docs/block-naming-patterns.md +210 -0
- data/docs/block-scanning-patterns.md +248 -0
- data/docs/cli-reference.md +370 -0
- data/docs/dev/block-hide.md +1 -1
- data/docs/dev/block-type-ux-transform.md +5 -4
- data/docs/dev/print_bytes.md +3 -0
- data/docs/dev/shebang.md +6 -0
- data/docs/docker-testing.md +5 -0
- data/docs/execution-control.md +384 -0
- data/docs/getting-started.md +209 -0
- data/docs/import-options.md +391 -0
- data/docs/tab-completion.md +7 -0
- data/docs/ux-blocks.md +376 -0
- data/examples/linked1.md +8 -1
- data/implementation-decisions.md +212 -0
- data/lib/cached_nested_file_reader.rb +138 -1
- data/lib/command_result.rb +27 -6
- data/lib/executed_shell_command.rb +512 -0
- data/lib/filter.rb +7 -7
- data/lib/hash_delegator.rb +403 -350
- data/lib/link_history.rb +22 -11
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/mdoc.rb +103 -44
- data/lib/menu.src.yml +110 -83
- data/lib/menu.yml +149 -83
- data/lib/transformed_shell_command.rb +449 -0
- data/lib/wl.rb +15 -0
- data/lib/ww.rb +16 -5
- data/requirements.md +111 -0
- data/semantic-tokens.md +132 -0
- data/tasks.md +69 -0
- metadata +26 -4
- data/docs/ux-blocks-examples.md +0 -120
- data/docs/ux-blocks-init-act.md +0 -100
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
#!/usr/bin/env -S bundle exec ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# encoding=utf-8
|
|
5
|
+
|
|
6
|
+
require_relative 'executed_shell_command'
|
|
7
|
+
|
|
8
|
+
##
|
|
9
|
+
# TransformedShellCommand executes a shell command and provides access to
|
|
10
|
+
# a transformed version of the output.
|
|
11
|
+
#
|
|
12
|
+
# The class accepts:
|
|
13
|
+
# * The same arguments as ExecutedShellCommand (command, chdir, env)
|
|
14
|
+
# * A regex pattern with named capture groups (supports multi-line output)
|
|
15
|
+
# * A format that can be either:
|
|
16
|
+
# - A Symbol: calls that method on the output (e.g., :strip, :upcase)
|
|
17
|
+
# - A String: format string with named placeholders like '%{group_name}'
|
|
18
|
+
#
|
|
19
|
+
# The command is executed automatically during initialization, and the
|
|
20
|
+
# entire output (including multi-line output) is transformed in a single
|
|
21
|
+
# transformation operation using the format.
|
|
22
|
+
#
|
|
23
|
+
# The regex pattern can match across multiple lines. Use the multiline
|
|
24
|
+
# flag (m) or construct patterns that handle newlines appropriately.
|
|
25
|
+
#
|
|
26
|
+
# Basic usage with format string:
|
|
27
|
+
#
|
|
28
|
+
# regex = /(?<name>\w+):(?<value>\d+)/
|
|
29
|
+
# format_str = "Name: %{name}, Value: %{value}"
|
|
30
|
+
# cmd = TransformedShellCommand.new("echo 'user:123'", regex: regex, format: format_str)
|
|
31
|
+
# cmd.transformed_output # => "Name: user, Value: 123"
|
|
32
|
+
# cmd.result # => original ExecutedShellCommand::Result
|
|
33
|
+
#
|
|
34
|
+
# Basic usage with Symbol:
|
|
35
|
+
#
|
|
36
|
+
# cmd = TransformedShellCommand.new("echo ' hello '", regex: /.*/, format: :strip)
|
|
37
|
+
# cmd.transformed_output # => "hello"
|
|
38
|
+
#
|
|
39
|
+
# Multi-line output example:
|
|
40
|
+
#
|
|
41
|
+
# regex = /(?<first>\w+)\n(?<second>\w+)\n(?<third>\w+)/m
|
|
42
|
+
# format_str = "%{first}-%{second}-%{third}"
|
|
43
|
+
# cmd = TransformedShellCommand.new("echo -e 'one\ntwo\nthree'", regex: regex, format: format_str)
|
|
44
|
+
# cmd.transformed_output # => "one-two-three"
|
|
45
|
+
#
|
|
46
|
+
class TransformedShellCommand
|
|
47
|
+
attr_reader :command, :env, :chdir, :regex, :format
|
|
48
|
+
|
|
49
|
+
def initialize(command, regex:, format:, chdir: nil, env: {})
|
|
50
|
+
@command = command
|
|
51
|
+
@chdir = chdir
|
|
52
|
+
@env = env
|
|
53
|
+
@regex = regex && (regex.is_a?(Regexp) ? regex : Regexp.new(regex))
|
|
54
|
+
@format = format
|
|
55
|
+
@result = nil
|
|
56
|
+
@transformed_output = nil
|
|
57
|
+
execute_and_transform
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
##
|
|
61
|
+
# Returns the transformed output string.
|
|
62
|
+
# The transformation is performed once during initialization and memoized.
|
|
63
|
+
#
|
|
64
|
+
attr_reader :transformed_output
|
|
65
|
+
|
|
66
|
+
##
|
|
67
|
+
# Returns the original ExecutedShellCommand result.
|
|
68
|
+
#
|
|
69
|
+
def result
|
|
70
|
+
@executed_command.result
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Convenience delegators to the original result:
|
|
74
|
+
|
|
75
|
+
def stdout
|
|
76
|
+
result.stdout
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def stderr
|
|
80
|
+
result.stderr
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def exit_code
|
|
84
|
+
result.exit_code
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def success?
|
|
88
|
+
result.success?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def failure?
|
|
92
|
+
!result.success?
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def duration
|
|
96
|
+
result.duration
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def started_at
|
|
100
|
+
result.started_at
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def finished_at
|
|
104
|
+
result.finished_at
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def pid
|
|
108
|
+
result.pid
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
##
|
|
114
|
+
# Execute the command and transform the output.
|
|
115
|
+
#
|
|
116
|
+
def execute_and_transform
|
|
117
|
+
@executed_command = ExecutedShellCommand.new(@command, chdir: @chdir,
|
|
118
|
+
env: @env)
|
|
119
|
+
@result = @executed_command.result
|
|
120
|
+
@transformed_output = transform_output(@result.stdout)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
##
|
|
124
|
+
# Transform the output using the regex and format.
|
|
125
|
+
#
|
|
126
|
+
# If format is a Symbol, calls that method on the value.
|
|
127
|
+
# If format is a String, extracts named groups and applies the format string.
|
|
128
|
+
# If the regex doesn't match or format is nil, returns the original value.
|
|
129
|
+
#
|
|
130
|
+
def transform_output(value)
|
|
131
|
+
return value if value.nil? || value.empty?
|
|
132
|
+
return value unless @format
|
|
133
|
+
|
|
134
|
+
# If format is a Symbol, call that method on the value
|
|
135
|
+
if @format.is_a?(Symbol)
|
|
136
|
+
return value.send(@format)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Extract named groups from the value using the regex
|
|
140
|
+
named_groups = @regex && extract_named_groups(value, @regex)
|
|
141
|
+
return value unless named_groups
|
|
142
|
+
|
|
143
|
+
# Apply format string with named placeholders
|
|
144
|
+
apply_format_string(@format, named_groups)
|
|
145
|
+
rescue StandardError
|
|
146
|
+
# On error, return original value
|
|
147
|
+
value
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
##
|
|
151
|
+
# Extract named groups from a string using a regex pattern.
|
|
152
|
+
#
|
|
153
|
+
# Supports multi-line strings. The regex pattern should be constructed
|
|
154
|
+
# to handle newlines (e.g., using the multiline flag 'm' or patterns
|
|
155
|
+
# that explicitly match newlines).
|
|
156
|
+
#
|
|
157
|
+
# @param str [String] the string to match (can be multi-line)
|
|
158
|
+
# @param pattern [Regexp] the regex pattern with named groups
|
|
159
|
+
# @return [Hash<Symbol, String>, nil] hash of named groups, or nil if no match
|
|
160
|
+
#
|
|
161
|
+
def extract_named_groups(str, pattern)
|
|
162
|
+
# Match against the entire string (including newlines)
|
|
163
|
+
match = str.match(pattern)
|
|
164
|
+
return nil unless match
|
|
165
|
+
|
|
166
|
+
match.named_captures&.transform_keys(&:to_sym)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
##
|
|
170
|
+
# Apply format string with named placeholders like '%{name}'.
|
|
171
|
+
#
|
|
172
|
+
# Replaces '%{group_name}' with the corresponding value from the named_groups hash.
|
|
173
|
+
#
|
|
174
|
+
# @param format_str [String] format string with '%{name}' placeholders
|
|
175
|
+
# @param named_groups [Hash<Symbol, String>] hash of named groups
|
|
176
|
+
# @return [String] formatted string
|
|
177
|
+
#
|
|
178
|
+
def apply_format_string(format_str, named_groups)
|
|
179
|
+
result = format_str.dup
|
|
180
|
+
|
|
181
|
+
# Replace each '%{name}' placeholder with the corresponding value
|
|
182
|
+
named_groups.each do |key, value|
|
|
183
|
+
placeholder = "%{#{key}}"
|
|
184
|
+
result.gsub!(placeholder, value.to_s)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
result
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Test suite when running as a script
|
|
192
|
+
return if $PROGRAM_NAME != __FILE__
|
|
193
|
+
|
|
194
|
+
require 'bundler/setup'
|
|
195
|
+
Bundler.require(:default)
|
|
196
|
+
|
|
197
|
+
require 'minitest/autorun'
|
|
198
|
+
|
|
199
|
+
class TransformedShellCommandTest < Minitest::Test
|
|
200
|
+
def test_basic_transformation
|
|
201
|
+
regex = /(?<name>\w+):(?<value>\d+)/
|
|
202
|
+
format_str = 'Name: %{name}, Value: %{value}'
|
|
203
|
+
cmd = TransformedShellCommand.new(
|
|
204
|
+
"echo 'user:123'",
|
|
205
|
+
regex: regex,
|
|
206
|
+
format: format_str
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
assert cmd.success?
|
|
210
|
+
assert_equal "user:123\n", cmd.stdout
|
|
211
|
+
assert_equal 'Name: user, Value: 123', cmd.transformed_output
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def test_symbol_transform
|
|
215
|
+
regex = /.*/
|
|
216
|
+
cmd = TransformedShellCommand.new(
|
|
217
|
+
"echo ' HELLO WORLD '",
|
|
218
|
+
regex: regex,
|
|
219
|
+
format: :strip
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
assert cmd.success?
|
|
223
|
+
assert_equal " HELLO WORLD \n", cmd.stdout
|
|
224
|
+
assert_equal 'HELLO WORLD', cmd.transformed_output
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def test_symbol_transform_upcase
|
|
228
|
+
regex = /.*/
|
|
229
|
+
cmd = TransformedShellCommand.new(
|
|
230
|
+
"echo 'hello'",
|
|
231
|
+
regex: regex,
|
|
232
|
+
format: :upcase
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
assert cmd.success?
|
|
236
|
+
assert_equal "hello\n", cmd.stdout
|
|
237
|
+
assert_equal "HELLO\n", cmd.transformed_output
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def test_no_match_returns_original
|
|
241
|
+
regex = /(?<name>\w+):(?<value>\d+)/
|
|
242
|
+
format_str = 'Name: %{name}, Value: %{value}'
|
|
243
|
+
cmd = TransformedShellCommand.new(
|
|
244
|
+
"echo 'no match here'",
|
|
245
|
+
regex: regex,
|
|
246
|
+
format: format_str
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
assert cmd.success?
|
|
250
|
+
assert_equal "no match here\n", cmd.stdout
|
|
251
|
+
assert_equal "no match here\n", cmd.transformed_output
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def test_multiple_named_groups
|
|
255
|
+
regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
|
|
256
|
+
format_str = 'Date: %{month}/%{day}/%{year}'
|
|
257
|
+
cmd = TransformedShellCommand.new(
|
|
258
|
+
"echo '2024-12-25'",
|
|
259
|
+
regex: regex,
|
|
260
|
+
format: format_str
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
assert cmd.success?
|
|
264
|
+
assert_equal "2024-12-25\n", cmd.stdout
|
|
265
|
+
assert_equal 'Date: 12/25/2024', cmd.transformed_output
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def test_delegates_to_result
|
|
269
|
+
regex = /(?<name>\w+)/
|
|
270
|
+
format_str = '%{name}'
|
|
271
|
+
cmd = TransformedShellCommand.new(
|
|
272
|
+
"echo 'test'",
|
|
273
|
+
regex: regex,
|
|
274
|
+
format: format_str
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
assert_kind_of ExecutedShellCommand::Result, cmd.result
|
|
278
|
+
assert_equal "test\n", cmd.stdout
|
|
279
|
+
assert_equal 0, cmd.exit_code
|
|
280
|
+
assert cmd.success?
|
|
281
|
+
assert_kind_of Numeric, cmd.duration
|
|
282
|
+
assert_kind_of Time, cmd.started_at
|
|
283
|
+
assert_kind_of Time, cmd.finished_at
|
|
284
|
+
assert_kind_of Integer, cmd.pid
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def test_with_chdir
|
|
288
|
+
regex = /(?<content>.*)/
|
|
289
|
+
format_str = 'Content: %{content}'
|
|
290
|
+
Dir.mktmpdir do |tmpdir|
|
|
291
|
+
test_file = File.join(tmpdir, 'test.txt')
|
|
292
|
+
File.write(test_file, 'hello world')
|
|
293
|
+
|
|
294
|
+
cmd = TransformedShellCommand.new(
|
|
295
|
+
'cat test.txt',
|
|
296
|
+
regex: regex,
|
|
297
|
+
format: format_str,
|
|
298
|
+
chdir: tmpdir
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
assert cmd.success?
|
|
302
|
+
assert_equal 'hello world', cmd.stdout
|
|
303
|
+
assert_equal 'Content: hello world', cmd.transformed_output
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def test_with_env
|
|
308
|
+
regex = /(?<var>\w+)=(?<val>\w+)/
|
|
309
|
+
format_str = '%{var} is %{val}'
|
|
310
|
+
cmd = TransformedShellCommand.new(
|
|
311
|
+
"echo 'TEST_VAR=test_value'",
|
|
312
|
+
regex: regex,
|
|
313
|
+
format: format_str,
|
|
314
|
+
env: { 'CUSTOM_VAR' => 'custom_value' }
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
assert cmd.success?
|
|
318
|
+
assert_equal "TEST_VAR=test_value\n", cmd.stdout
|
|
319
|
+
assert_equal 'TEST_VAR is test_value', cmd.transformed_output
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def test_regex_as_string
|
|
323
|
+
regex_str = '(?<name>\\w+):(?<value>\\d+)'
|
|
324
|
+
format_str = 'Name: %{name}, Value: %{value}'
|
|
325
|
+
cmd = TransformedShellCommand.new(
|
|
326
|
+
"echo 'user:123'",
|
|
327
|
+
regex: regex_str,
|
|
328
|
+
format: format_str
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
assert cmd.success?
|
|
332
|
+
assert_equal 'Name: user, Value: 123', cmd.transformed_output
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def test_format_string_with_literal_text
|
|
336
|
+
regex = /(?<num>\d+)/
|
|
337
|
+
format_str = 'The number is %{num}!'
|
|
338
|
+
cmd = TransformedShellCommand.new(
|
|
339
|
+
"echo '42'",
|
|
340
|
+
regex: regex,
|
|
341
|
+
format: format_str
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
assert cmd.success?
|
|
345
|
+
assert_equal "42\n", cmd.stdout
|
|
346
|
+
assert_equal 'The number is 42!', cmd.transformed_output
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def test_empty_output
|
|
350
|
+
regex = /(?<name>\w+)/
|
|
351
|
+
format_str = '%{name}'
|
|
352
|
+
cmd = TransformedShellCommand.new(
|
|
353
|
+
'true',
|
|
354
|
+
regex: regex,
|
|
355
|
+
format: format_str
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
assert cmd.success?
|
|
359
|
+
assert_equal '', cmd.stdout
|
|
360
|
+
assert_equal '', cmd.transformed_output
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def test_multiple_placeholders_same_group
|
|
364
|
+
regex = /(?<word>\w+)/
|
|
365
|
+
format_str = '%{word} %{word} %{word}'
|
|
366
|
+
cmd = TransformedShellCommand.new(
|
|
367
|
+
"echo 'hello'",
|
|
368
|
+
regex: regex,
|
|
369
|
+
format: format_str
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
assert cmd.success?
|
|
373
|
+
assert_equal "hello\n", cmd.stdout
|
|
374
|
+
assert_equal 'hello hello hello', cmd.transformed_output
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def test_multiline_output_single_transformation
|
|
378
|
+
regex = /(?<first>\w+)\n(?<second>\w+)\n(?<third>\w+)/m
|
|
379
|
+
format_str = '%{first}-%{second}-%{third}'
|
|
380
|
+
cmd = TransformedShellCommand.new(
|
|
381
|
+
"printf 'one\ntwo\nthree\n'",
|
|
382
|
+
regex: regex,
|
|
383
|
+
format: format_str
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
assert cmd.success?
|
|
387
|
+
assert_equal "one\ntwo\nthree\n", cmd.stdout
|
|
388
|
+
assert_equal 'one-two-three', cmd.transformed_output
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def test_multiline_output_with_multiline_regex
|
|
392
|
+
regex = /Name: (?<name>[\w\s]+)\nAge: (?<age>\d+)\nCity: (?<city>[\w\s]+)/m
|
|
393
|
+
format_str = '%{name} (%{age}) from %{city}'
|
|
394
|
+
cmd = TransformedShellCommand.new(
|
|
395
|
+
"printf 'Name: John Doe\nAge: 30\nCity: New York\n'",
|
|
396
|
+
regex: regex,
|
|
397
|
+
format: format_str
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
assert cmd.success?
|
|
401
|
+
assert_includes cmd.stdout, 'John Doe'
|
|
402
|
+
assert_includes cmd.stdout, '30'
|
|
403
|
+
assert_includes cmd.stdout, 'New York'
|
|
404
|
+
# Remove trailing newline for comparison
|
|
405
|
+
assert_equal 'John Doe (30) from New York', cmd.transformed_output.chomp
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def test_multiline_output_symbol_transform
|
|
409
|
+
regex = /.*/m
|
|
410
|
+
cmd = TransformedShellCommand.new(
|
|
411
|
+
"printf ' line1\n line2\n line3 \n'",
|
|
412
|
+
regex: regex,
|
|
413
|
+
format: :strip
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
assert cmd.success?
|
|
417
|
+
assert_equal " line1\n line2\n line3 \n", cmd.stdout
|
|
418
|
+
# strip removes leading/trailing whitespace from entire string
|
|
419
|
+
# Leading spaces from first line and trailing spaces/newline from last line are removed
|
|
420
|
+
assert_equal "line1\n line2\n line3", cmd.transformed_output
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def test_multiline_output_captures_spanning_lines
|
|
424
|
+
regex = /Start: (?<start>.*?)End: (?<end>.*?)$/m
|
|
425
|
+
format_str = 'From %{start} to %{end}'
|
|
426
|
+
cmd = TransformedShellCommand.new(
|
|
427
|
+
"printf 'Start: alpha\nbeta\ngamma\nEnd: delta\n'",
|
|
428
|
+
regex: regex,
|
|
429
|
+
format: format_str
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
assert cmd.success?
|
|
433
|
+
assert_equal "From alpha\nbeta\ngamma\n to delta", cmd.transformed_output
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def test_multiline_output_no_match_returns_original
|
|
437
|
+
regex = /(?<name>\w+):(?<value>\d+)/
|
|
438
|
+
format_str = 'Name: %{name}, Value: %{value}'
|
|
439
|
+
cmd = TransformedShellCommand.new(
|
|
440
|
+
"printf 'line1\nline2\nline3\n'",
|
|
441
|
+
regex: regex,
|
|
442
|
+
format: format_str
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
assert cmd.success?
|
|
446
|
+
assert_equal "line1\nline2\nline3\n", cmd.stdout
|
|
447
|
+
assert_equal "line1\nline2\nline3\n", cmd.transformed_output
|
|
448
|
+
end
|
|
449
|
+
end
|
data/lib/wl.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require "binding_of_caller"
|
|
2
|
+
|
|
3
|
+
def show(name)
|
|
4
|
+
# look one frame up and fetch the caller's local variable
|
|
5
|
+
value = binding.of_caller(1).local_variable_get(name)
|
|
6
|
+
puts name.to_s + ': ' + value.to_s
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def main
|
|
10
|
+
x = 1
|
|
11
|
+
show(:x)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
main()
|
|
15
|
+
|
data/lib/ww.rb
CHANGED
|
@@ -392,7 +392,10 @@ class Array
|
|
|
392
392
|
raise ArgumentError,
|
|
393
393
|
'Method must be a Symbol or String' unless method.is_a?(Symbol) || method.is_a?(String)
|
|
394
394
|
|
|
395
|
-
partition
|
|
395
|
+
partition do |item|
|
|
396
|
+
item.respond_to?(method) &&
|
|
397
|
+
item.send(method) == value
|
|
398
|
+
end
|
|
396
399
|
rescue NoMethodError => err
|
|
397
400
|
warn "Method #{method} not available on some items: #{err.message}"
|
|
398
401
|
[[], self]
|
|
@@ -412,7 +415,10 @@ class Array
|
|
|
412
415
|
raise ArgumentError,
|
|
413
416
|
'Method must be a Symbol or String' unless method.is_a?(Symbol) || method.is_a?(String)
|
|
414
417
|
|
|
415
|
-
reject
|
|
418
|
+
reject do |item|
|
|
419
|
+
item.respond_to?(method) &&
|
|
420
|
+
item.send(method) == value
|
|
421
|
+
end
|
|
416
422
|
end
|
|
417
423
|
rescue NoMethodError => err
|
|
418
424
|
warn "Method #{method} not available on some items: #{err.message}"
|
|
@@ -433,7 +439,10 @@ class Array
|
|
|
433
439
|
raise ArgumentError,
|
|
434
440
|
'Method must be a Symbol or String' unless method.is_a?(Symbol) || method.is_a?(String)
|
|
435
441
|
|
|
436
|
-
select
|
|
442
|
+
select do |item|
|
|
443
|
+
item.respond_to?(method) &&
|
|
444
|
+
item.send(method) == value
|
|
445
|
+
end
|
|
437
446
|
end
|
|
438
447
|
rescue NoMethodError => err
|
|
439
448
|
warn "Method #{method} not available on some items: #{err.message}"
|
|
@@ -704,8 +713,10 @@ class TestWwFunction < Minitest::Test
|
|
|
704
713
|
$debug = true
|
|
705
714
|
|
|
706
715
|
# Test that wwa exits (we can't easily test exit behavior in minitest)
|
|
707
|
-
# So we'll just verify it would call ww0 properly by testing the
|
|
708
|
-
#
|
|
716
|
+
# So we'll just verify it would call ww0 properly by testing the
|
|
717
|
+
# structure
|
|
718
|
+
# Note: wwa calls exit, so we can't test it directly without special
|
|
719
|
+
# handling
|
|
709
720
|
skip 'wwa exits the program, cannot test directly in minitest'
|
|
710
721
|
end
|
|
711
722
|
|
data/requirements.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Requirements
|
|
2
|
+
|
|
3
|
+
**STDD Methodology Version**: 1.0.0
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
This document defines the functional and non-functional requirements for your project. Each requirement should have a unique semantic token `[REQ:IDENTIFIER]` for traceability.
|
|
7
|
+
|
|
8
|
+
### Requirement Structure
|
|
9
|
+
|
|
10
|
+
Each requirement includes:
|
|
11
|
+
- **Description**: What the requirement specifies
|
|
12
|
+
- **Rationale**: Why the requirement exists
|
|
13
|
+
- **Satisfaction Criteria**: How we know the requirement is satisfied (acceptance criteria, success conditions)
|
|
14
|
+
- **Validation Criteria**: How we verify/validate the requirement is met (testing approach, verification methods, success metrics)
|
|
15
|
+
|
|
16
|
+
**Note**: Validation criteria defined here inform the testing strategy documented in `architecture-decisions.md` and the specific test implementations in `implementation-decisions.md`.
|
|
17
|
+
|
|
18
|
+
## Core Functionality
|
|
19
|
+
|
|
20
|
+
### 1. [REQ:SHEBANG_HIDING] CLI Option to Hide Shebang Lines in Document Output
|
|
21
|
+
|
|
22
|
+
**Priority: P1 (Important)**
|
|
23
|
+
|
|
24
|
+
- **Description**: MDE shall provide a CLI option that, when enabled (default), causes the shebang line (lines starting with `#!`) to be extracted from input file(s) and not displayed as part of the document output. The initial implementation will extract shebang lines during the cached nested read process.
|
|
25
|
+
- **Rationale**: Shebang lines are execution directives for scripts and are not typically part of the document content when displayed. Users may want to include shebang lines in their markdown source files for direct execution (e.g., `#!/usr/bin/env mde`), but these should not appear in the rendered document output by default. This improves document readability and follows common markdown processing conventions.
|
|
26
|
+
- **Satisfaction Criteria** (How we know the requirement is satisfied):
|
|
27
|
+
- A CLI option exists (e.g., `--hide-shebang` or `--no-show-shebang`) that controls shebang line visibility
|
|
28
|
+
- The option defaults to enabled (shebang lines are hidden by default)
|
|
29
|
+
- When enabled, shebang lines are extracted from input files during cached nested read
|
|
30
|
+
- Shebang lines are not included in the processed document output when the option is enabled
|
|
31
|
+
- When disabled, shebang lines are included in the document output as normal lines
|
|
32
|
+
- The option works with nested imports (imported files also have their shebang lines handled according to the option)
|
|
33
|
+
- **Validation Criteria** (How we verify/validate the requirement is met):
|
|
34
|
+
- Unit tests verify shebang line detection and extraction logic
|
|
35
|
+
- Integration tests verify shebang lines are excluded from document output when option is enabled
|
|
36
|
+
- Integration tests verify shebang lines are included when option is disabled
|
|
37
|
+
- Tests verify behavior with nested imports
|
|
38
|
+
- Manual verification with sample markdown files containing shebang lines
|
|
39
|
+
- CLI help/documentation shows the option and its default value
|
|
40
|
+
|
|
41
|
+
**Status**: ✅ Implemented
|
|
42
|
+
|
|
43
|
+
### 2. [REQ:EXAMPLE_FEATURE] Example Feature Name
|
|
44
|
+
|
|
45
|
+
**Priority: P0 (Critical) | P1 (Important) | P2 (Nice-to-Have) | P3 (Future)**
|
|
46
|
+
|
|
47
|
+
- **Description**: Brief description of what this feature does
|
|
48
|
+
- **Rationale**: Why this feature is needed
|
|
49
|
+
- **Satisfaction Criteria** (How we know the requirement is satisfied):
|
|
50
|
+
- Criterion 1
|
|
51
|
+
- Criterion 2
|
|
52
|
+
- Criterion 3
|
|
53
|
+
- **Validation Criteria** (How we verify/validate the requirement is met):
|
|
54
|
+
- Validation method 1 (e.g., unit tests, integration tests, manual verification)
|
|
55
|
+
- Validation method 2
|
|
56
|
+
- Success metrics or thresholds
|
|
57
|
+
|
|
58
|
+
**Status**: ⏳ Planned | ✅ Implemented
|
|
59
|
+
|
|
60
|
+
### 2. [REQ:ANOTHER_FEATURE] Another Feature Name
|
|
61
|
+
|
|
62
|
+
**Priority: P0 (Critical)**
|
|
63
|
+
|
|
64
|
+
- **Description**: Description of the feature
|
|
65
|
+
- **Rationale**: Why it's needed
|
|
66
|
+
- **Satisfaction Criteria** (How we know the requirement is satisfied):
|
|
67
|
+
- Criterion 1
|
|
68
|
+
- Criterion 2
|
|
69
|
+
- **Validation Criteria** (How we verify/validate the requirement is met):
|
|
70
|
+
- Validation method 1
|
|
71
|
+
- Validation method 2
|
|
72
|
+
|
|
73
|
+
**Status**: ⏳ Planned
|
|
74
|
+
|
|
75
|
+
## Non-Functional Requirements
|
|
76
|
+
|
|
77
|
+
### 1. Performance [REQ:PERFORMANCE]
|
|
78
|
+
- Requirement description
|
|
79
|
+
- Metrics or targets
|
|
80
|
+
|
|
81
|
+
### 2. Reliability [REQ:RELIABILITY]
|
|
82
|
+
- Requirement description
|
|
83
|
+
- Availability targets
|
|
84
|
+
|
|
85
|
+
### 3. Maintainability [REQ:MAINTAINABILITY]
|
|
86
|
+
- Requirement description
|
|
87
|
+
- Code quality standards
|
|
88
|
+
|
|
89
|
+
### 4. Usability [REQ:USABILITY]
|
|
90
|
+
- Requirement description
|
|
91
|
+
- User experience goals
|
|
92
|
+
|
|
93
|
+
## Edge Cases to Handle
|
|
94
|
+
|
|
95
|
+
1. **Edge Case 1**
|
|
96
|
+
- Description
|
|
97
|
+
- Expected behavior
|
|
98
|
+
|
|
99
|
+
2. **Edge Case 2**
|
|
100
|
+
- Description
|
|
101
|
+
- Expected behavior
|
|
102
|
+
|
|
103
|
+
## Future Enhancements (Out of Scope)
|
|
104
|
+
|
|
105
|
+
The following features are documented but marked as future enhancements:
|
|
106
|
+
- Feature 1
|
|
107
|
+
- Feature 2
|
|
108
|
+
- Feature 3
|
|
109
|
+
|
|
110
|
+
These may be considered for future iterations but are not required for the initial implementation.
|
|
111
|
+
|