howzit 2.1.28 â 2.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +121 -0
- data/README.md +5 -1
- data/Rakefile +11 -4
- data/bin/howzit +65 -65
- data/howzit.gemspec +1 -1
- data/lib/howzit/buildnote.rb +4 -8
- data/lib/howzit/colors.rb +50 -22
- data/lib/howzit/condition_evaluator.rb +307 -0
- data/lib/howzit/conditional_content.rb +96 -0
- data/lib/howzit/config.rb +28 -3
- data/lib/howzit/console_logger.rb +74 -2
- data/lib/howzit/directive.rb +137 -0
- data/lib/howzit/prompt.rb +20 -12
- data/lib/howzit/run_report.rb +1 -1
- data/lib/howzit/script_comm.rb +105 -0
- data/lib/howzit/script_support.rb +479 -0
- data/lib/howzit/stringutils.rb +4 -4
- data/lib/howzit/task.rb +68 -6
- data/lib/howzit/topic.rb +576 -13
- data/lib/howzit/util.rb +11 -3
- data/lib/howzit/version.rb +1 -1
- data/lib/howzit.rb +5 -0
- data/spec/condition_evaluator_spec.rb +261 -0
- data/spec/conditional_blocks_integration_spec.rb +159 -0
- data/spec/conditional_content_spec.rb +296 -0
- data/spec/log_level_spec.rb +247 -0
- data/spec/script_comm_spec.rb +303 -0
- data/spec/sequential_conditional_spec.rb +319 -0
- data/spec/set_var_spec.rb +603 -0
- data/spec/spec_helper.rb +3 -1
- data/spec/topic_spec.rb +8 -6
- data/src/_README.md +5 -1
- metadata +23 -4
data/lib/howzit/colors.rb
CHANGED
|
@@ -281,11 +281,40 @@ module Howzit
|
|
|
281
281
|
end
|
|
282
282
|
end
|
|
283
283
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
284
|
+
# Build colors hash from configured_colors, generating escape codes directly
|
|
285
|
+
color_map = configured_colors.to_h
|
|
286
|
+
colors = if coloring?
|
|
287
|
+
{
|
|
288
|
+
w: "\e[#{color_map[:white]}m",
|
|
289
|
+
k: "\e[#{color_map[:black]}m",
|
|
290
|
+
g: "\e[#{color_map[:green]}m",
|
|
291
|
+
l: "\e[#{color_map[:blue]}m",
|
|
292
|
+
y: "\e[#{color_map[:yellow]}m",
|
|
293
|
+
c: "\e[#{color_map[:cyan]}m",
|
|
294
|
+
m: "\e[#{color_map[:magenta]}m",
|
|
295
|
+
r: "\e[#{color_map[:red]}m",
|
|
296
|
+
W: "\e[#{color_map[:bgwhite]}m",
|
|
297
|
+
K: "\e[#{color_map[:bgblack]}m",
|
|
298
|
+
G: "\e[#{color_map[:bggreen]}m",
|
|
299
|
+
L: "\e[#{color_map[:bgblue]}m",
|
|
300
|
+
Y: "\e[#{color_map[:bgyellow]}m",
|
|
301
|
+
C: "\e[#{color_map[:bgcyan]}m",
|
|
302
|
+
M: "\e[#{color_map[:bgmagenta]}m",
|
|
303
|
+
R: "\e[#{color_map[:bgred]}m",
|
|
304
|
+
d: "\e[#{color_map[:dark]}m",
|
|
305
|
+
b: "\e[#{color_map[:bold]}m",
|
|
306
|
+
u: "\e[#{color_map[:underline]}m",
|
|
307
|
+
i: "\e[#{color_map[:italic]}m",
|
|
308
|
+
x: "\e[#{color_map[:reset]}m"
|
|
309
|
+
}
|
|
310
|
+
else
|
|
311
|
+
# When coloring is disabled, return empty strings
|
|
312
|
+
{
|
|
313
|
+
w: '', k: '', g: '', l: '', y: '', c: '', m: '', r: '',
|
|
314
|
+
W: '', K: '', G: '', L: '', Y: '', C: '', M: '', R: '',
|
|
315
|
+
d: '', b: '', u: '', i: '', x: ''
|
|
316
|
+
}
|
|
317
|
+
end
|
|
289
318
|
|
|
290
319
|
result = fmt.empty? ? input : format(fmt, colors)
|
|
291
320
|
# Unescape braces and dollar signs that were escaped to prevent color code interpretation
|
|
@@ -295,24 +324,24 @@ module Howzit
|
|
|
295
324
|
|
|
296
325
|
# Dynamically generate methods for each color name. Each
|
|
297
326
|
# resulting method can be called with a string or a block.
|
|
298
|
-
configured_colors.each do |c, v|
|
|
327
|
+
Color.configured_colors.each do |c, v|
|
|
299
328
|
new_method = <<-EOSCRIPT
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
329
|
+
# Color string as #{c}
|
|
330
|
+
def #{c}(string = nil)
|
|
331
|
+
result = ''
|
|
332
|
+
result << "\e[#{v}m" if Howzit::Color.coloring?
|
|
333
|
+
if block_given?
|
|
334
|
+
result << yield
|
|
335
|
+
elsif string.respond_to?(:to_str)
|
|
336
|
+
result << string.to_str
|
|
337
|
+
elsif respond_to?(:to_str)
|
|
338
|
+
result << to_str
|
|
339
|
+
else
|
|
340
|
+
return result #only switch on
|
|
341
|
+
end
|
|
342
|
+
result << "\e[0m" if Howzit::Color.coloring?
|
|
343
|
+
result
|
|
312
344
|
end
|
|
313
|
-
result << "\e[0m" if Howzit::Color.coloring?
|
|
314
|
-
result
|
|
315
|
-
end
|
|
316
345
|
EOSCRIPT
|
|
317
346
|
|
|
318
347
|
module_eval(new_method)
|
|
@@ -383,6 +412,5 @@ module Howzit
|
|
|
383
412
|
def attributes
|
|
384
413
|
ATTRIBUTE_NAMES
|
|
385
414
|
end
|
|
386
|
-
extend self
|
|
387
415
|
end
|
|
388
416
|
end
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'English'
|
|
4
|
+
|
|
5
|
+
module Howzit
|
|
6
|
+
# Condition Evaluator module
|
|
7
|
+
# Handles evaluation of @if/@unless conditions
|
|
8
|
+
module ConditionEvaluator
|
|
9
|
+
class << self
|
|
10
|
+
##
|
|
11
|
+
## Evaluate a condition expression
|
|
12
|
+
##
|
|
13
|
+
## @param condition [String] The condition to evaluate
|
|
14
|
+
## @param context [Hash] Context with metadata, arguments, etc.
|
|
15
|
+
##
|
|
16
|
+
## @return [Boolean] Result of condition evaluation
|
|
17
|
+
##
|
|
18
|
+
def evaluate(condition, context = {})
|
|
19
|
+
condition = condition.strip
|
|
20
|
+
|
|
21
|
+
# Handle negation with 'not' or '!'
|
|
22
|
+
negated = false
|
|
23
|
+
if condition =~ /^(not\s+|!)/
|
|
24
|
+
negated = true
|
|
25
|
+
condition = condition.sub(/^(not\s+|!)/, '').strip
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
result = evaluate_condition(condition, context)
|
|
29
|
+
negated ? !result : result
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
##
|
|
35
|
+
## Evaluate a single condition (without negation)
|
|
36
|
+
##
|
|
37
|
+
def evaluate_condition(condition, context)
|
|
38
|
+
# Handle special conditions FIRST to avoid false matches with comparison patterns
|
|
39
|
+
# Check file contents before other patterns since it has arguments and operators
|
|
40
|
+
if condition =~ /^file\s+contents\s+(.+?)\s+(\*\*=|\*=|\^=|\$=|==|!=|=~)\s*(.+)$/i
|
|
41
|
+
return evaluate_file_contents(condition, context)
|
|
42
|
+
# Check file/dir/topic exists before other patterns since they have arguments
|
|
43
|
+
elsif condition =~ /^(file\s+exists|dir\s+exists|topic\s+exists)\s+(.+)$/i
|
|
44
|
+
return evaluate_special(condition, context)
|
|
45
|
+
elsif condition =~ /^(git\s+dirty|git\s+clean)$/i
|
|
46
|
+
return evaluate_special(condition, context)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Handle =~ regex comparisons separately (before string == to avoid conflicts)
|
|
50
|
+
if (match = condition.match(%r{^(.+?)\s*=~\s*/(.+)/$}))
|
|
51
|
+
left = match[1].strip
|
|
52
|
+
pattern = match[2].strip
|
|
53
|
+
|
|
54
|
+
left_val = get_value(left, context)
|
|
55
|
+
return false if left_val.nil?
|
|
56
|
+
|
|
57
|
+
!!(left_val.to_s =~ /#{pattern}/)
|
|
58
|
+
# Handle comparisons with ==, !=, >, >=, <, <=
|
|
59
|
+
# Determine if numeric or string comparison based on values
|
|
60
|
+
elsif (match = condition.match(/^(.+?)\s*(==|!=|>=|<=|>|<)\s*(.+)$/))
|
|
61
|
+
left = match[1].strip
|
|
62
|
+
operator = match[2]
|
|
63
|
+
right = match[3].strip
|
|
64
|
+
|
|
65
|
+
left_val = get_value(left, context)
|
|
66
|
+
# If get_value returned nil, try using the original string as a literal
|
|
67
|
+
left_val = left if left_val.nil? && numeric_value(left)
|
|
68
|
+
right_val = get_value(right, context)
|
|
69
|
+
# If get_value returned nil, try using the original string as a literal
|
|
70
|
+
right_val = right if right_val.nil? && numeric_value(right)
|
|
71
|
+
|
|
72
|
+
# Try to convert to numbers
|
|
73
|
+
left_num = numeric_value(left_val)
|
|
74
|
+
right_num = numeric_value(right_val)
|
|
75
|
+
|
|
76
|
+
# If both are numeric, use numeric comparison
|
|
77
|
+
if left_num && right_num
|
|
78
|
+
case operator
|
|
79
|
+
when '=='
|
|
80
|
+
left_num == right_num
|
|
81
|
+
when '!='
|
|
82
|
+
left_num != right_num
|
|
83
|
+
when '>'
|
|
84
|
+
left_num > right_num
|
|
85
|
+
when '>='
|
|
86
|
+
left_num >= right_num
|
|
87
|
+
when '<'
|
|
88
|
+
left_num < right_num
|
|
89
|
+
when '<='
|
|
90
|
+
left_num <= right_num
|
|
91
|
+
else
|
|
92
|
+
false
|
|
93
|
+
end
|
|
94
|
+
# Otherwise use string comparison for == and !=, or return false for others
|
|
95
|
+
else
|
|
96
|
+
case operator
|
|
97
|
+
when '=='
|
|
98
|
+
# Handle nil comparisons
|
|
99
|
+
left_val.nil? == right_val.nil? && (left_val.nil? || left_val.to_s == right_val.to_s)
|
|
100
|
+
when '!='
|
|
101
|
+
left_val.nil? != right_val.nil? || (!left_val.nil? && !right_val.nil? && left_val.to_s != right_val.to_s)
|
|
102
|
+
else
|
|
103
|
+
# For >, >=, <, <=, return false if not numeric
|
|
104
|
+
false
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
# Handle string-only comparisons: **= (fuzzy match), *= (contains), ^= (starts with), $= (ends with)
|
|
108
|
+
# Note: **= must come before *= in the regex to avoid matching *= first
|
|
109
|
+
elsif (match = condition.match(/^(.+?)\s*(\*\*=|\*=|\^=|\$=)\s*(.+)$/))
|
|
110
|
+
left = match[1].strip
|
|
111
|
+
operator = match[2]
|
|
112
|
+
right = match[3].strip
|
|
113
|
+
|
|
114
|
+
left_val = get_value(left, context)
|
|
115
|
+
right_val = get_value(right, context)
|
|
116
|
+
# If right side is nil (variable not found), treat it as a literal string
|
|
117
|
+
right_val = right if right_val.nil?
|
|
118
|
+
|
|
119
|
+
return false if left_val.nil? || right_val.nil?
|
|
120
|
+
|
|
121
|
+
case operator
|
|
122
|
+
when '*='
|
|
123
|
+
left_val.to_s.include?(right_val.to_s)
|
|
124
|
+
when '^='
|
|
125
|
+
left_val.to_s.start_with?(right_val.to_s)
|
|
126
|
+
when '$='
|
|
127
|
+
left_val.to_s.end_with?(right_val.to_s)
|
|
128
|
+
when '**='
|
|
129
|
+
# Fuzzy match: split search string into chars and join with .*? for regex
|
|
130
|
+
pattern = "^.*?#{right_val.to_s.split('').map { |c| Regexp.escape(c) }.join('.*?')}.*?$"
|
|
131
|
+
!!(left_val.to_s =~ /#{pattern}/)
|
|
132
|
+
else
|
|
133
|
+
false
|
|
134
|
+
end
|
|
135
|
+
# Simple existence check (just a variable name)
|
|
136
|
+
else
|
|
137
|
+
val = get_value(condition, context)
|
|
138
|
+
!val.nil? && val.to_s != ''
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
##
|
|
143
|
+
## Get value from various sources
|
|
144
|
+
##
|
|
145
|
+
def get_value(expr, context)
|
|
146
|
+
expr = expr.strip
|
|
147
|
+
|
|
148
|
+
# Remove quotes if present
|
|
149
|
+
return Regexp.last_match(1) if expr =~ /^["'](.+)["']$/
|
|
150
|
+
|
|
151
|
+
# Remove ${} wrapper if present (for consistency with variable substitution syntax)
|
|
152
|
+
expr = Regexp.last_match(1) if expr =~ /^\$\{(.+)\}$/
|
|
153
|
+
|
|
154
|
+
# Check positional arguments
|
|
155
|
+
if expr =~ /^\$(\d+)$/
|
|
156
|
+
idx = Regexp.last_match(1).to_i - 1
|
|
157
|
+
return Howzit.arguments[idx] if Howzit.arguments && Howzit.arguments[idx]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Check named arguments
|
|
161
|
+
return Howzit.named_arguments[expr.to_sym] if Howzit.named_arguments&.key?(expr.to_sym)
|
|
162
|
+
return Howzit.named_arguments[expr] if Howzit.named_arguments&.key?(expr)
|
|
163
|
+
|
|
164
|
+
# Check metadata (from context only, to avoid circular dependencies)
|
|
165
|
+
metadata = context[:metadata]
|
|
166
|
+
return metadata[expr] if metadata&.key?(expr)
|
|
167
|
+
return metadata[expr.downcase] if metadata&.key?(expr.downcase)
|
|
168
|
+
|
|
169
|
+
# Check environment variables
|
|
170
|
+
return ENV[expr] if ENV.key?(expr)
|
|
171
|
+
return ENV[expr.upcase] if ENV.key?(expr.upcase)
|
|
172
|
+
|
|
173
|
+
# Check for special values: cwd, working directory
|
|
174
|
+
return Dir.pwd if expr =~ /^(cwd|working\s+directory)$/i
|
|
175
|
+
|
|
176
|
+
# Return nil if nothing matched (variable is undefined)
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
##
|
|
181
|
+
## Convert value to numeric if possible
|
|
182
|
+
##
|
|
183
|
+
def numeric_value(val)
|
|
184
|
+
return val if val.is_a?(Numeric)
|
|
185
|
+
|
|
186
|
+
str = val.to_s.strip
|
|
187
|
+
return nil if str.empty?
|
|
188
|
+
|
|
189
|
+
# Try integer first
|
|
190
|
+
return str.to_i if str =~ /^-?\d+$/
|
|
191
|
+
|
|
192
|
+
# Try float
|
|
193
|
+
return str.to_f if str =~ /^-?\d+\.\d+$/
|
|
194
|
+
|
|
195
|
+
nil
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
##
|
|
199
|
+
## Evaluate special conditions
|
|
200
|
+
##
|
|
201
|
+
def evaluate_special(condition, context)
|
|
202
|
+
condition = condition.downcase.strip
|
|
203
|
+
|
|
204
|
+
if condition =~ /^git\s+dirty$/i
|
|
205
|
+
git_dirty?
|
|
206
|
+
elsif condition =~ /^git\s+clean$/i
|
|
207
|
+
!git_dirty?
|
|
208
|
+
elsif (match = condition.match(/^file\s+exists\s+(.+)$/i))
|
|
209
|
+
file = match[1].strip
|
|
210
|
+
# get_value returns nil if not found, so use original path if nil
|
|
211
|
+
file_val = get_value(file, context)
|
|
212
|
+
file_path = file_val.nil? ? file : file_val.to_s
|
|
213
|
+
File.exist?(file_path) && !File.directory?(file_path)
|
|
214
|
+
elsif (match = condition.match(/^dir\s+exists\s+(.+)$/i))
|
|
215
|
+
dir = match[1].strip
|
|
216
|
+
dir_val = get_value(dir, context)
|
|
217
|
+
dir_path = dir_val.nil? ? dir : dir_val.to_s
|
|
218
|
+
File.directory?(dir_path)
|
|
219
|
+
elsif (match = condition.match(/^topic\s+exists\s+(.+)$/i))
|
|
220
|
+
topic_name = match[1].strip
|
|
221
|
+
topic_name = get_value(topic_name, context).to_s
|
|
222
|
+
find_topic(topic_name)
|
|
223
|
+
else
|
|
224
|
+
false
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
##
|
|
229
|
+
## Check if git repository is dirty
|
|
230
|
+
##
|
|
231
|
+
def git_dirty?
|
|
232
|
+
return false unless `which git`.strip != ''
|
|
233
|
+
|
|
234
|
+
Dir.chdir(Dir.pwd) do
|
|
235
|
+
`git diff --quiet 2>/dev/null`
|
|
236
|
+
$CHILD_STATUS.exitstatus != 0
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
##
|
|
241
|
+
## Check if topic exists in buildnote
|
|
242
|
+
##
|
|
243
|
+
def find_topic(topic_name)
|
|
244
|
+
return false unless Howzit.buildnote
|
|
245
|
+
|
|
246
|
+
matches = Howzit.buildnote.find_topic(topic_name)
|
|
247
|
+
!matches.empty?
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
##
|
|
251
|
+
## Evaluate file contents condition
|
|
252
|
+
## Reads file and performs string comparison
|
|
253
|
+
##
|
|
254
|
+
def evaluate_file_contents(condition, context)
|
|
255
|
+
match = condition.match(/^file\s+contents\s+(.+?)\s+(\*\*=|\*=|\^=|\$=|==|!=|=~)\s*(.+)$/i)
|
|
256
|
+
return false unless match
|
|
257
|
+
|
|
258
|
+
file_path = match[1].strip
|
|
259
|
+
operator = match[2]
|
|
260
|
+
search_value = match[3].strip
|
|
261
|
+
|
|
262
|
+
# Resolve file path (could be a variable)
|
|
263
|
+
file_path_val = get_value(file_path, context)
|
|
264
|
+
file_path = file_path_val.nil? ? file_path : file_path_val.to_s
|
|
265
|
+
|
|
266
|
+
# Resolve search value (could be a variable)
|
|
267
|
+
search_val = get_value(search_value, context)
|
|
268
|
+
search_val = search_val.nil? ? search_value : search_val.to_s
|
|
269
|
+
|
|
270
|
+
# Read file contents
|
|
271
|
+
return false unless File.exist?(file_path) && !File.directory?(file_path)
|
|
272
|
+
|
|
273
|
+
begin
|
|
274
|
+
file_contents = File.read(file_path).strip
|
|
275
|
+
rescue StandardError
|
|
276
|
+
return false
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Perform comparison based on operator
|
|
280
|
+
case operator
|
|
281
|
+
when '=='
|
|
282
|
+
file_contents == search_val.to_s
|
|
283
|
+
when '!='
|
|
284
|
+
file_contents != search_val.to_s
|
|
285
|
+
when '*='
|
|
286
|
+
file_contents.include?(search_val.to_s)
|
|
287
|
+
when '^='
|
|
288
|
+
file_contents.start_with?(search_val.to_s)
|
|
289
|
+
when '$='
|
|
290
|
+
file_contents.end_with?(search_val.to_s)
|
|
291
|
+
when '**='
|
|
292
|
+
# Fuzzy match: split search string into chars and join with .*? for regex
|
|
293
|
+
pattern = "^.*?#{search_val.to_s.split('').map { |c| Regexp.escape(c) }.join('.*?')}.*?$"
|
|
294
|
+
!!(file_contents =~ /#{pattern}/)
|
|
295
|
+
when '=~'
|
|
296
|
+
# Regex match - search_value should be a regex pattern
|
|
297
|
+
pattern = search_val.to_s
|
|
298
|
+
# Remove leading/trailing slashes if present
|
|
299
|
+
pattern = pattern[1..-2] if pattern.start_with?('/') && pattern.end_with?('/')
|
|
300
|
+
!!(file_contents =~ /#{pattern}/)
|
|
301
|
+
else
|
|
302
|
+
false
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Howzit
|
|
4
|
+
# Conditional Content processor
|
|
5
|
+
# Handles @if/@unless/@end blocks in topic content
|
|
6
|
+
module ConditionalContent
|
|
7
|
+
class << self
|
|
8
|
+
##
|
|
9
|
+
## Process conditional blocks in content
|
|
10
|
+
##
|
|
11
|
+
## @param content [String] The content to process
|
|
12
|
+
## @param context [Hash] Context for condition evaluation
|
|
13
|
+
##
|
|
14
|
+
## @return [String] Content with conditional blocks processed
|
|
15
|
+
##
|
|
16
|
+
def process(content, context = {})
|
|
17
|
+
lines = content.split(/\n/)
|
|
18
|
+
output = []
|
|
19
|
+
condition_stack = []
|
|
20
|
+
# Track if any condition in the current chain has been true
|
|
21
|
+
# This is used for @elsif and @else to know if a previous branch matched
|
|
22
|
+
chain_matched_stack = []
|
|
23
|
+
|
|
24
|
+
lines.each do |line|
|
|
25
|
+
# Check for @if or @unless
|
|
26
|
+
if line =~ /^@(if|unless)\s+(.+)$/i
|
|
27
|
+
directive = Regexp.last_match(1).downcase
|
|
28
|
+
condition = Regexp.last_match(2).strip
|
|
29
|
+
|
|
30
|
+
# Evaluate condition
|
|
31
|
+
result = ConditionEvaluator.evaluate(condition, context)
|
|
32
|
+
# For @unless, negate the result
|
|
33
|
+
result = !result if directive == 'unless'
|
|
34
|
+
|
|
35
|
+
condition_stack << result
|
|
36
|
+
chain_matched_stack << result
|
|
37
|
+
|
|
38
|
+
# Don't include the @if/@unless line itself
|
|
39
|
+
next
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Check for @elsif
|
|
43
|
+
if line =~ /^@elsif\s+(.+)$/i
|
|
44
|
+
condition = Regexp.last_match(1).strip
|
|
45
|
+
|
|
46
|
+
# If previous condition in chain was true, this branch is false
|
|
47
|
+
# Otherwise, evaluate the condition
|
|
48
|
+
if !condition_stack.empty? && chain_matched_stack.last
|
|
49
|
+
# Previous branch matched, so this one is false
|
|
50
|
+
condition_stack[-1] = false
|
|
51
|
+
else
|
|
52
|
+
# Previous branch didn't match, evaluate this condition
|
|
53
|
+
result = ConditionEvaluator.evaluate(condition, context)
|
|
54
|
+
condition_stack[-1] = result
|
|
55
|
+
chain_matched_stack[-1] = result if result
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Don't include the @elsif line itself
|
|
59
|
+
next
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Check for @else
|
|
63
|
+
if line =~ /^@else\s*$/i
|
|
64
|
+
# If any previous condition in chain was true, this branch is false
|
|
65
|
+
# Otherwise, this branch is true
|
|
66
|
+
if !condition_stack.empty? && chain_matched_stack.last
|
|
67
|
+
# Previous branch matched, so else is false
|
|
68
|
+
condition_stack[-1] = false
|
|
69
|
+
else
|
|
70
|
+
# No previous branch matched, so else is true
|
|
71
|
+
condition_stack[-1] = true
|
|
72
|
+
chain_matched_stack[-1] = true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Don't include the @else line itself
|
|
76
|
+
next
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Check for @end - only skip if it's closing an @if/@unless/@elsif/@else block
|
|
80
|
+
if (line =~ /^@end\s*$/) && !condition_stack.empty?
|
|
81
|
+
# This @end closes a conditional block, so skip it
|
|
82
|
+
condition_stack.pop
|
|
83
|
+
chain_matched_stack.pop
|
|
84
|
+
next
|
|
85
|
+
end
|
|
86
|
+
# Otherwise, this @end is for @before/@after, so include it
|
|
87
|
+
|
|
88
|
+
# Include the line only if all conditions in stack are true
|
|
89
|
+
output << line if condition_stack.all? { |cond| cond }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
output.join("\n")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
data/lib/howzit/config.rb
CHANGED
|
@@ -129,14 +129,26 @@ module Howzit
|
|
|
129
129
|
|
|
130
130
|
## Update editor config
|
|
131
131
|
def update_editor
|
|
132
|
-
|
|
132
|
+
begin
|
|
133
|
+
puts 'No $EDITOR defined, no value in config'
|
|
134
|
+
rescue Errno::EPIPE
|
|
135
|
+
# Pipe closed, ignore
|
|
136
|
+
end
|
|
133
137
|
editor = Prompt.read_editor
|
|
134
138
|
if editor.nil?
|
|
135
|
-
|
|
139
|
+
begin
|
|
140
|
+
puts 'Cancelled, no editor stored.'
|
|
141
|
+
rescue Errno::EPIPE
|
|
142
|
+
# Pipe closed, ignore
|
|
143
|
+
end
|
|
136
144
|
Process.exit 1
|
|
137
145
|
end
|
|
138
146
|
update_config_option({ config_editor: editor, editor: editor })
|
|
139
|
-
|
|
147
|
+
begin
|
|
148
|
+
puts "Default editor set to #{editor}, modify in config file"
|
|
149
|
+
rescue Errno::EPIPE
|
|
150
|
+
# Pipe closed, ignore
|
|
151
|
+
end
|
|
140
152
|
editor
|
|
141
153
|
end
|
|
142
154
|
|
|
@@ -182,6 +194,19 @@ module Howzit
|
|
|
182
194
|
config = load_config
|
|
183
195
|
load_theme
|
|
184
196
|
@options = flags.merge(config)
|
|
197
|
+
|
|
198
|
+
# Check for HOWZIT_LOG_LEVEL environment variable
|
|
199
|
+
return unless ENV['HOWZIT_LOG_LEVEL']
|
|
200
|
+
|
|
201
|
+
level_str = ENV['HOWZIT_LOG_LEVEL'].downcase
|
|
202
|
+
level_map = {
|
|
203
|
+
'debug' => 0,
|
|
204
|
+
'info' => 1,
|
|
205
|
+
'warn' => 2,
|
|
206
|
+
'warning' => 2,
|
|
207
|
+
'error' => 3
|
|
208
|
+
}
|
|
209
|
+
@options[:log_level] = level_map[level_str] || level_str.to_i
|
|
185
210
|
end
|
|
186
211
|
|
|
187
212
|
##
|
|
@@ -31,6 +31,50 @@ module Howzit
|
|
|
31
31
|
@log_level = Howzit.options[:log_level]
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
##
|
|
35
|
+
## Get emoji for log level
|
|
36
|
+
##
|
|
37
|
+
## @param level [Symbol] The level
|
|
38
|
+
##
|
|
39
|
+
## @return [String] Emoji for the level
|
|
40
|
+
##
|
|
41
|
+
def emoji_for_level(level)
|
|
42
|
+
case level
|
|
43
|
+
when :debug
|
|
44
|
+
'đ'
|
|
45
|
+
when :info
|
|
46
|
+
'âšī¸'
|
|
47
|
+
when :warn
|
|
48
|
+
'â ī¸'
|
|
49
|
+
when :error
|
|
50
|
+
'â'
|
|
51
|
+
else
|
|
52
|
+
''
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
## Get color prefix for log level
|
|
58
|
+
##
|
|
59
|
+
## @param level [Symbol] The level
|
|
60
|
+
##
|
|
61
|
+
## @return [String] Color template string
|
|
62
|
+
##
|
|
63
|
+
def color_for_level(level)
|
|
64
|
+
case level
|
|
65
|
+
when :debug
|
|
66
|
+
'{d}'
|
|
67
|
+
when :info
|
|
68
|
+
'{c}'
|
|
69
|
+
when :warn
|
|
70
|
+
'{y}'
|
|
71
|
+
when :error
|
|
72
|
+
'{r}'
|
|
73
|
+
else
|
|
74
|
+
''
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
34
78
|
##
|
|
35
79
|
## Write a message to the console based on the urgency
|
|
36
80
|
## level and user's log level setting
|
|
@@ -39,7 +83,21 @@ module Howzit
|
|
|
39
83
|
## @param level [Symbol] The level
|
|
40
84
|
##
|
|
41
85
|
def write(msg, level = :info)
|
|
42
|
-
|
|
86
|
+
return unless LOG_LEVELS[level] >= @log_level
|
|
87
|
+
|
|
88
|
+
emoji = emoji_for_level(level)
|
|
89
|
+
color = color_for_level(level)
|
|
90
|
+
formatted_msg = if emoji && color
|
|
91
|
+
"#{emoji} #{color}#{msg}{x}".c
|
|
92
|
+
else
|
|
93
|
+
msg
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
begin
|
|
97
|
+
$stderr.puts formatted_msg
|
|
98
|
+
rescue Errno::EPIPE
|
|
99
|
+
# Pipe closed, ignore
|
|
100
|
+
end
|
|
43
101
|
end
|
|
44
102
|
|
|
45
103
|
##
|
|
@@ -66,7 +124,21 @@ module Howzit
|
|
|
66
124
|
## @param msg The message
|
|
67
125
|
##
|
|
68
126
|
def warn(msg)
|
|
69
|
-
|
|
127
|
+
return unless LOG_LEVELS[:warn] >= @log_level
|
|
128
|
+
|
|
129
|
+
emoji = emoji_for_level(:warn)
|
|
130
|
+
color = color_for_level(:warn)
|
|
131
|
+
formatted_msg = if emoji && color
|
|
132
|
+
"#{emoji} #{color}#{msg}{x}".c
|
|
133
|
+
else
|
|
134
|
+
msg
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
begin
|
|
138
|
+
$stderr.puts formatted_msg
|
|
139
|
+
rescue Errno::EPIPE
|
|
140
|
+
# Pipe closed, ignore
|
|
141
|
+
end
|
|
70
142
|
end
|
|
71
143
|
|
|
72
144
|
##
|