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/topic.rb
CHANGED
|
@@ -7,22 +7,25 @@ module Howzit
|
|
|
7
7
|
|
|
8
8
|
attr_accessor :content
|
|
9
9
|
|
|
10
|
-
attr_reader :title, :tasks, :prereqs, :postreqs, :results, :named_args
|
|
10
|
+
attr_reader :title, :tasks, :prereqs, :postreqs, :results, :named_args, :directives
|
|
11
11
|
|
|
12
12
|
##
|
|
13
13
|
## Initialize a topic object
|
|
14
14
|
##
|
|
15
15
|
## @param title [String] The topic title
|
|
16
16
|
## @param content [String] The raw topic content
|
|
17
|
+
## @param metadata [Hash] Optional metadata hash
|
|
17
18
|
##
|
|
18
|
-
def initialize(title, content)
|
|
19
|
+
def initialize(title, content, metadata = nil)
|
|
19
20
|
@title = title
|
|
20
21
|
@content = content
|
|
21
22
|
@parent = nil
|
|
22
23
|
@nest_level = 0
|
|
23
24
|
@named_args = {}
|
|
25
|
+
@metadata = metadata
|
|
24
26
|
arguments
|
|
25
27
|
|
|
28
|
+
@directives = parse_directives_with_conditionals
|
|
26
29
|
@tasks = gather_tasks
|
|
27
30
|
@results = { total: 0, success: 0, errors: 0, message: ''.c }
|
|
28
31
|
end
|
|
@@ -79,9 +82,22 @@ module Howzit
|
|
|
79
82
|
|
|
80
83
|
cols = check_cols
|
|
81
84
|
|
|
85
|
+
# Use sequential processing if we have directives with conditionals
|
|
86
|
+
if @directives && @directives.any?(&:conditional?)
|
|
87
|
+
return run_sequential(nested: nested, output: output, cols: cols)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Fall back to old behavior for backward compatibility
|
|
91
|
+
# Note: @set_var directives are already processed in gather_tasks for non-sequential path
|
|
92
|
+
# This section is kept for backward compatibility but shouldn't be needed
|
|
93
|
+
|
|
82
94
|
if @tasks.count.positive?
|
|
83
95
|
unless @prereqs.empty?
|
|
84
|
-
|
|
96
|
+
begin
|
|
97
|
+
puts TTY::Box.frame("{by}#{@prereqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols)
|
|
98
|
+
rescue Errno::EPIPE
|
|
99
|
+
# Pipe closed, ignore
|
|
100
|
+
end
|
|
85
101
|
res = Prompt.yn('Have the above prerequisites been met?', default: true)
|
|
86
102
|
Process.exit 1 unless res
|
|
87
103
|
|
|
@@ -123,7 +139,13 @@ module Howzit
|
|
|
123
139
|
|
|
124
140
|
output.push(@results[:message]) if Howzit.options[:log_level] < 2 && !nested && !Howzit.options[:run]
|
|
125
141
|
|
|
126
|
-
|
|
142
|
+
unless @postreqs.empty?
|
|
143
|
+
begin
|
|
144
|
+
puts TTY::Box.frame("{bw}#{@postreqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols)
|
|
145
|
+
rescue Errno::EPIPE
|
|
146
|
+
# Pipe closed, ignore
|
|
147
|
+
end
|
|
148
|
+
end
|
|
127
149
|
|
|
128
150
|
output
|
|
129
151
|
end
|
|
@@ -166,7 +188,8 @@ module Howzit
|
|
|
166
188
|
output = []
|
|
167
189
|
|
|
168
190
|
if keys[:action] =~ / *\[(.*?)\] *$/
|
|
169
|
-
Howzit.named_arguments
|
|
191
|
+
Howzit.named_arguments ||= {}
|
|
192
|
+
Howzit.named_arguments.merge!(@named_args) if @named_args
|
|
170
193
|
Howzit.arguments = Regexp.last_match(1).split(/ *, */).map!(&:render_arguments)
|
|
171
194
|
end
|
|
172
195
|
|
|
@@ -254,14 +277,16 @@ module Howzit
|
|
|
254
277
|
output.push(@title.format_header)
|
|
255
278
|
output.push('')
|
|
256
279
|
end
|
|
257
|
-
|
|
280
|
+
# Process conditional blocks first
|
|
281
|
+
metadata = @metadata || Howzit.buildnote&.metadata
|
|
282
|
+
topic = ConditionalContent.process(@content.dup, { metadata: metadata })
|
|
258
283
|
unless Howzit.options[:show_all_code]
|
|
259
284
|
topic.gsub!(/(?mix)^(`{3,})run([?!]*)\s*
|
|
260
285
|
([^\n]*)[\s\S]*?\n\1\s*$/, '@@@run\2 \3')
|
|
261
286
|
end
|
|
262
287
|
topic.split(/\n/).each do |l|
|
|
263
288
|
case l
|
|
264
|
-
when /@(before|after|prereq|end)/
|
|
289
|
+
when /@(before|after|prereq|end|if|unless)/
|
|
265
290
|
next
|
|
266
291
|
when /@include(?<optional>[!?]{1,2})?\((?<action>[^)]+)\)/
|
|
267
292
|
output.concat(process_include(Regexp.last_match.named_captures.symbolize_keys, opt))
|
|
@@ -300,11 +325,13 @@ module Howzit
|
|
|
300
325
|
# Store the actual title (not overridden by show_all_code - that's only for display)
|
|
301
326
|
task_args = { type: :include,
|
|
302
327
|
arguments: nil,
|
|
303
|
-
title: title.dup,
|
|
328
|
+
title: title.dup, # Make a copy to avoid reference issues
|
|
304
329
|
action: obj,
|
|
305
330
|
parent: self }
|
|
306
331
|
# Set named_arguments before processing titles for variable substitution
|
|
307
|
-
|
|
332
|
+
# Merge with existing named_arguments to preserve @set_var variables
|
|
333
|
+
Howzit.named_arguments ||= {}
|
|
334
|
+
Howzit.named_arguments.merge!(@named_args) if @named_args
|
|
308
335
|
case cmd
|
|
309
336
|
when /include/i
|
|
310
337
|
if title =~ /\[(.*?)\] *$/
|
|
@@ -321,6 +348,14 @@ module Howzit
|
|
|
321
348
|
when /run/i
|
|
322
349
|
task_args[:type] = :run
|
|
323
350
|
task_args[:title] = title.render_arguments
|
|
351
|
+
# Parse log_level from action if present (format: script, log_level=level)
|
|
352
|
+
if obj =~ /,\s*log_level\s*=\s*(\w+)/i
|
|
353
|
+
log_level = Regexp.last_match(1).downcase
|
|
354
|
+
task_args[:log_level] = log_level
|
|
355
|
+
# Remove log_level parameter from action
|
|
356
|
+
obj = obj.sub(/,\s*log_level\s*=\s*\w+/i, '').strip
|
|
357
|
+
end
|
|
358
|
+
task_args[:action] = obj
|
|
324
359
|
when /copy/i
|
|
325
360
|
task_args[:type] = :copy
|
|
326
361
|
task_args[:action] = Shellwords.escape(obj)
|
|
@@ -363,18 +398,57 @@ module Howzit
|
|
|
363
398
|
|
|
364
399
|
def gather_tasks
|
|
365
400
|
runnable = []
|
|
366
|
-
|
|
367
|
-
|
|
401
|
+
# Process conditional blocks first
|
|
402
|
+
# Set named_arguments before processing so conditions can access them
|
|
403
|
+
Howzit.named_arguments ||= {}
|
|
404
|
+
Howzit.named_arguments.merge!(@named_args) if @named_args
|
|
405
|
+
|
|
406
|
+
# Process @set_var directives before gathering tasks (for non-sequential path)
|
|
407
|
+
# This ensures variables are available when task actions are rendered
|
|
408
|
+
if @directives && !@directives.any?(&:conditional?)
|
|
409
|
+
@directives.each do |dir|
|
|
410
|
+
next unless dir.set_var?
|
|
411
|
+
|
|
412
|
+
value = dir.var_value
|
|
413
|
+
if value
|
|
414
|
+
# Check for command substitution: backticks or $()
|
|
415
|
+
if value =~ /^`(.+)`$/ || value =~ /^\$\((.+)\)$/
|
|
416
|
+
command = Regexp.last_match(1).strip
|
|
417
|
+
# Apply variable substitution to command before execution
|
|
418
|
+
command = command.render_arguments
|
|
419
|
+
# Execute command and capture output
|
|
420
|
+
begin
|
|
421
|
+
value = `#{command}`.strip
|
|
422
|
+
rescue StandardError => e
|
|
423
|
+
Howzit.console.warn("Error executing command in @set_var: #{e.message}")
|
|
424
|
+
value = ''
|
|
425
|
+
end
|
|
426
|
+
else
|
|
427
|
+
# Apply variable substitution to the value (in case it references other variables)
|
|
428
|
+
value = value.render_arguments
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
Howzit.named_arguments[dir.var_name] = value
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
metadata = @metadata || Howzit.buildnote&.metadata
|
|
437
|
+
processed_content = ConditionalContent.process(@content, { metadata: metadata })
|
|
438
|
+
|
|
439
|
+
@prereqs = processed_content.scan(/(?<=@before\n).*?(?=\n@end)/im).map(&:strip)
|
|
440
|
+
@postreqs = processed_content.scan(/(?<=@after\n).*?(?=\n@end)/im).map(&:strip)
|
|
368
441
|
|
|
369
442
|
rx = /(?mix)(?:
|
|
370
443
|
@(?<cmd>include|run|copy|open|url)(?<optional>[!?]{1,2})?\((?<action>[^)]*?)\)(?<title>[^\n]+)?
|
|
371
444
|
|(?<fence>`{3,})run(?<optional2>[!?]{1,2})?(?<title2>[^\n]+)?(?<block>.*?)\k<fence>
|
|
372
445
|
)/
|
|
373
446
|
matches = []
|
|
374
|
-
|
|
447
|
+
processed_content.scan(rx) { matches << Regexp.last_match }
|
|
375
448
|
matches.each do |m|
|
|
376
449
|
c = m.named_captures.symbolize_keys
|
|
377
|
-
Howzit.named_arguments
|
|
450
|
+
Howzit.named_arguments ||= {}
|
|
451
|
+
Howzit.named_arguments.merge!(@named_args) if @named_args
|
|
378
452
|
|
|
379
453
|
if c[:cmd].nil?
|
|
380
454
|
optional, default = define_optional(c[:optional2])
|
|
@@ -398,5 +472,494 @@ module Howzit
|
|
|
398
472
|
|
|
399
473
|
runnable
|
|
400
474
|
end
|
|
475
|
+
|
|
476
|
+
##
|
|
477
|
+
## Parse directives with conditional context for sequential evaluation
|
|
478
|
+
##
|
|
479
|
+
## @return [Array] Array of Directive objects
|
|
480
|
+
##
|
|
481
|
+
def parse_directives_with_conditionals
|
|
482
|
+
directives = []
|
|
483
|
+
lines = @content.split(/\n/)
|
|
484
|
+
conditional_stack = [] # Array of directive indices for @if/@unless directives
|
|
485
|
+
current_branch_index = nil # Track current @if/@elsif/@else branch index
|
|
486
|
+
line_num = 0
|
|
487
|
+
in_code_block = false
|
|
488
|
+
code_block_lines = []
|
|
489
|
+
code_block_fence = nil
|
|
490
|
+
code_block_title = nil
|
|
491
|
+
code_block_optional = nil
|
|
492
|
+
|
|
493
|
+
# Extract prereqs and postreqs from raw content
|
|
494
|
+
@prereqs = @content.scan(/(?<=@before\n).*?(?=\n@end)/im).map(&:strip)
|
|
495
|
+
@postreqs = @content.scan(/(?<=@after\n).*?(?=\n@end)/im).map(&:strip)
|
|
496
|
+
|
|
497
|
+
lines.each do |line|
|
|
498
|
+
line_num += 1
|
|
499
|
+
|
|
500
|
+
# Handle code blocks (fenced code)
|
|
501
|
+
if line =~ /^(`{3,})run([?!]*)\s*(.*?)$/i && !in_code_block
|
|
502
|
+
in_code_block = true
|
|
503
|
+
code_block_fence = Regexp.last_match(1)
|
|
504
|
+
code_block_optional = Regexp.last_match(2)
|
|
505
|
+
code_block_title = Regexp.last_match(3).strip
|
|
506
|
+
code_block_lines = []
|
|
507
|
+
next
|
|
508
|
+
elsif in_code_block
|
|
509
|
+
if line =~ /^#{Regexp.escape(code_block_fence)}\s*$/
|
|
510
|
+
# End of code block
|
|
511
|
+
block_content = code_block_lines.join("\n")
|
|
512
|
+
optional, default = define_optional(code_block_optional)
|
|
513
|
+
conditional_path = conditional_stack.dup
|
|
514
|
+
# If we're inside an @elsif/@else branch, include it in the path
|
|
515
|
+
conditional_path << current_branch_index if current_branch_index
|
|
516
|
+
directives << Howzit::Directive.new(
|
|
517
|
+
type: :task,
|
|
518
|
+
content: {
|
|
519
|
+
type: :block,
|
|
520
|
+
title: code_block_title,
|
|
521
|
+
action: block_content,
|
|
522
|
+
arguments: nil
|
|
523
|
+
},
|
|
524
|
+
optional: optional,
|
|
525
|
+
default: default,
|
|
526
|
+
line_number: line_num,
|
|
527
|
+
conditional_path: conditional_path
|
|
528
|
+
)
|
|
529
|
+
in_code_block = false
|
|
530
|
+
code_block_lines = []
|
|
531
|
+
code_block_fence = nil
|
|
532
|
+
else
|
|
533
|
+
code_block_lines << line
|
|
534
|
+
end
|
|
535
|
+
next
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# Handle conditional directives
|
|
539
|
+
if line =~ /^@(if|unless)\s+(.+)$/i
|
|
540
|
+
directive_type = Regexp.last_match(1).downcase
|
|
541
|
+
condition = Regexp.last_match(2).strip
|
|
542
|
+
directive_index = directives.length
|
|
543
|
+
conditional_stack << directive_index
|
|
544
|
+
current_branch_index = directive_index
|
|
545
|
+
directives << Howzit::Directive.new(
|
|
546
|
+
type: directive_type.to_sym,
|
|
547
|
+
condition: condition,
|
|
548
|
+
directive_type: directive_type,
|
|
549
|
+
line_number: line_num,
|
|
550
|
+
conditional_path: conditional_stack[0..-2].dup
|
|
551
|
+
)
|
|
552
|
+
next
|
|
553
|
+
elsif line =~ /^@elsif\s+(.+)$/i
|
|
554
|
+
condition = Regexp.last_match(1).strip
|
|
555
|
+
directive_index = directives.length
|
|
556
|
+
current_branch_index = directive_index
|
|
557
|
+
directives << Howzit::Directive.new(
|
|
558
|
+
type: :elsif,
|
|
559
|
+
condition: condition,
|
|
560
|
+
directive_type: 'elsif',
|
|
561
|
+
line_number: line_num,
|
|
562
|
+
conditional_path: conditional_stack[0..-2].dup
|
|
563
|
+
)
|
|
564
|
+
next
|
|
565
|
+
elsif line =~ /^@else\s*$/i
|
|
566
|
+
directive_index = directives.length
|
|
567
|
+
current_branch_index = directive_index
|
|
568
|
+
directives << Howzit::Directive.new(
|
|
569
|
+
type: :else,
|
|
570
|
+
directive_type: 'else',
|
|
571
|
+
line_number: line_num,
|
|
572
|
+
conditional_path: conditional_stack[0..-2].dup
|
|
573
|
+
)
|
|
574
|
+
next
|
|
575
|
+
elsif line =~ /^@end\s*$/i && !conditional_stack.empty?
|
|
576
|
+
# Closing a conditional block
|
|
577
|
+
conditional_stack.pop
|
|
578
|
+
current_branch_index = nil
|
|
579
|
+
directives << Howzit::Directive.new(
|
|
580
|
+
type: :end,
|
|
581
|
+
directive_type: 'end',
|
|
582
|
+
line_number: line_num,
|
|
583
|
+
conditional_path: conditional_stack.dup
|
|
584
|
+
)
|
|
585
|
+
next
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
# Handle @log_level directive
|
|
589
|
+
if line =~ /^@log_level\s*\(([^)]+)\)\s*$/i
|
|
590
|
+
log_level = Regexp.last_match(1).strip
|
|
591
|
+
conditional_path = conditional_stack.dup
|
|
592
|
+
directives << Howzit::Directive.new(
|
|
593
|
+
type: :log_level,
|
|
594
|
+
log_level_value: log_level,
|
|
595
|
+
line_number: line_num,
|
|
596
|
+
conditional_path: conditional_path
|
|
597
|
+
)
|
|
598
|
+
next
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# Handle @set_var directive
|
|
602
|
+
if line =~ /^@set_var\s*\(/i
|
|
603
|
+
# Extract content between parentheses, handling nested parentheses
|
|
604
|
+
paren_content = line.sub(/^@set_var\s*\(/i, '').sub(/\)\s*$/, '')
|
|
605
|
+
# Split by first comma only - everything after first comma is the value
|
|
606
|
+
if paren_content =~ /^([^,]+),\s*(.+)$/
|
|
607
|
+
var_name = Regexp.last_match(1).strip
|
|
608
|
+
var_value = Regexp.last_match(2).strip
|
|
609
|
+
# Validate variable name: alphanumeric, dashes, underscores only
|
|
610
|
+
if var_name =~ /^[A-Za-z0-9_-]+$/
|
|
611
|
+
# Remove quotes from value if present (handles both single and double quotes)
|
|
612
|
+
var_value = Regexp.last_match(1) if var_value =~ /^["'](.+)["']$/
|
|
613
|
+
conditional_path = conditional_stack.dup
|
|
614
|
+
directives << Howzit::Directive.new(
|
|
615
|
+
type: :set_var,
|
|
616
|
+
var_name: var_name,
|
|
617
|
+
var_value: var_value,
|
|
618
|
+
line_number: line_num,
|
|
619
|
+
conditional_path: conditional_path
|
|
620
|
+
)
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
next
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# Handle task directives (@run, @include, etc.)
|
|
627
|
+
unless line =~ /^@(?<cmd>include|run|copy|open|url)(?<optional>[!?]{1,2})?\((?<action>[^)]*?)\)(?<title>.*?)$/
|
|
628
|
+
next
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
cmd = Regexp.last_match(:cmd)
|
|
632
|
+
optional_str = Regexp.last_match(:optional) || ''
|
|
633
|
+
action = Regexp.last_match(:action)
|
|
634
|
+
title = Regexp.last_match(:title).strip
|
|
635
|
+
|
|
636
|
+
optional, default = define_optional(optional_str)
|
|
637
|
+
conditional_path = conditional_stack.dup
|
|
638
|
+
# If we're inside an @elsif/@else branch, include it in the path
|
|
639
|
+
conditional_path << current_branch_index if current_branch_index
|
|
640
|
+
directives << Howzit::Directive.new(
|
|
641
|
+
type: :task,
|
|
642
|
+
content: {
|
|
643
|
+
type: cmd.downcase.to_sym,
|
|
644
|
+
action: action,
|
|
645
|
+
title: title,
|
|
646
|
+
arguments: nil
|
|
647
|
+
},
|
|
648
|
+
optional: optional,
|
|
649
|
+
default: default,
|
|
650
|
+
line_number: line_num,
|
|
651
|
+
conditional_path: conditional_path
|
|
652
|
+
)
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
directives
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
##
|
|
659
|
+
## Run directives sequentially with conditional re-evaluation
|
|
660
|
+
##
|
|
661
|
+
def run_sequential(nested: false, output: [], cols: 80)
|
|
662
|
+
# Initialize conditional state
|
|
663
|
+
conditional_state = {} # { index => { evaluated: bool, result: bool, matched_chain: bool } }
|
|
664
|
+
directive_index = 0
|
|
665
|
+
current_log_level = nil # Track current log level set by @log_level directives
|
|
666
|
+
|
|
667
|
+
# Initialize named_arguments with topic's named args (don't overwrite on each iteration)
|
|
668
|
+
Howzit.named_arguments ||= {}
|
|
669
|
+
Howzit.named_arguments.merge!(@named_args) if @named_args
|
|
670
|
+
|
|
671
|
+
unless @prereqs.empty?
|
|
672
|
+
begin
|
|
673
|
+
puts TTY::Box.frame("{by}#{@prereqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols)
|
|
674
|
+
rescue Errno::EPIPE
|
|
675
|
+
# Pipe closed, ignore
|
|
676
|
+
end
|
|
677
|
+
res = Prompt.yn('Have the above prerequisites been met?', default: true)
|
|
678
|
+
Process.exit 1 unless res
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
# Process directives sequentially
|
|
682
|
+
while directive_index < @directives.length
|
|
683
|
+
directive = @directives[directive_index]
|
|
684
|
+
directive_index += 1
|
|
685
|
+
|
|
686
|
+
# Update context for condition evaluation
|
|
687
|
+
metadata = @metadata || Howzit.buildnote&.metadata
|
|
688
|
+
context = { metadata: metadata }
|
|
689
|
+
|
|
690
|
+
# Handle conditional directives
|
|
691
|
+
if directive.conditional?
|
|
692
|
+
case directive.type
|
|
693
|
+
when :if, :unless
|
|
694
|
+
# Evaluate condition
|
|
695
|
+
result = ConditionEvaluator.evaluate(directive.condition, context)
|
|
696
|
+
result = !result if directive.directive_type == 'unless'
|
|
697
|
+
|
|
698
|
+
conditional_state[directive_index - 1] = {
|
|
699
|
+
evaluated: true,
|
|
700
|
+
result: result,
|
|
701
|
+
matched_chain: result,
|
|
702
|
+
condition: directive.condition,
|
|
703
|
+
directive_type: directive.directive_type
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
when :elsif
|
|
707
|
+
# Find the matching @if/@unless
|
|
708
|
+
matching_if_index = find_matching_if_index(directive_index - 1)
|
|
709
|
+
if matching_if_index && conditional_state[matching_if_index]
|
|
710
|
+
# If previous branch matched, this is false
|
|
711
|
+
if conditional_state[matching_if_index][:matched_chain]
|
|
712
|
+
conditional_state[directive_index - 1] = {
|
|
713
|
+
evaluated: true,
|
|
714
|
+
result: false,
|
|
715
|
+
matched_chain: false,
|
|
716
|
+
condition: directive.condition,
|
|
717
|
+
directive_type: 'elsif',
|
|
718
|
+
parent_index: matching_if_index
|
|
719
|
+
}
|
|
720
|
+
else
|
|
721
|
+
# Evaluate condition
|
|
722
|
+
result = ConditionEvaluator.evaluate(directive.condition, context)
|
|
723
|
+
conditional_state[directive_index - 1] = {
|
|
724
|
+
evaluated: true,
|
|
725
|
+
result: result,
|
|
726
|
+
matched_chain: result,
|
|
727
|
+
condition: directive.condition,
|
|
728
|
+
directive_type: 'elsif',
|
|
729
|
+
parent_index: matching_if_index
|
|
730
|
+
}
|
|
731
|
+
conditional_state[matching_if_index][:matched_chain] = true if result
|
|
732
|
+
end
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
when :else
|
|
736
|
+
# Find the matching @if/@unless
|
|
737
|
+
matching_if_index = find_matching_if_index(directive_index - 1)
|
|
738
|
+
if matching_if_index && conditional_state[matching_if_index]
|
|
739
|
+
# If any previous branch matched, else is false
|
|
740
|
+
if conditional_state[matching_if_index][:matched_chain]
|
|
741
|
+
conditional_state[directive_index - 1] = {
|
|
742
|
+
evaluated: true,
|
|
743
|
+
result: false,
|
|
744
|
+
matched_chain: false,
|
|
745
|
+
directive_type: 'else',
|
|
746
|
+
parent_index: matching_if_index
|
|
747
|
+
}
|
|
748
|
+
else
|
|
749
|
+
conditional_state[directive_index - 1] = {
|
|
750
|
+
evaluated: true,
|
|
751
|
+
result: true,
|
|
752
|
+
matched_chain: true,
|
|
753
|
+
directive_type: 'else',
|
|
754
|
+
parent_index: matching_if_index
|
|
755
|
+
}
|
|
756
|
+
conditional_state[matching_if_index][:matched_chain] = true
|
|
757
|
+
end
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
when :end
|
|
761
|
+
# End of conditional block - no action needed, state is managed by stack
|
|
762
|
+
end
|
|
763
|
+
next
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
# Handle @log_level directive (before task check)
|
|
767
|
+
if directive.log_level?
|
|
768
|
+
current_log_level = directive.log_level_value
|
|
769
|
+
next
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
# Handle @set_var directive (before task check)
|
|
773
|
+
if directive.set_var?
|
|
774
|
+
# Set the variable in named_arguments
|
|
775
|
+
Howzit.named_arguments ||= {}
|
|
776
|
+
value = directive.var_value
|
|
777
|
+
|
|
778
|
+
if value
|
|
779
|
+
# Check for command substitution: backticks or $()
|
|
780
|
+
if value =~ /^`(.+)`$/ || value =~ /^\$\((.+)\)$/
|
|
781
|
+
command = Regexp.last_match(1).strip
|
|
782
|
+
# Apply variable substitution to command before execution
|
|
783
|
+
command = command.render_arguments
|
|
784
|
+
# Execute command and capture output
|
|
785
|
+
begin
|
|
786
|
+
value = `#{command}`.strip
|
|
787
|
+
rescue StandardError => e
|
|
788
|
+
Howzit.console.warn("Error executing command in @set_var: #{e.message}")
|
|
789
|
+
value = ''
|
|
790
|
+
end
|
|
791
|
+
else
|
|
792
|
+
# Apply variable substitution to the value (in case it references other variables)
|
|
793
|
+
value = value.render_arguments
|
|
794
|
+
end
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
Howzit.named_arguments[directive.var_name] = value
|
|
798
|
+
# Re-evaluate conditionals after setting variable
|
|
799
|
+
re_evaluate_conditionals(conditional_state, directive_index - 1, context)
|
|
800
|
+
next
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
# Handle task directives
|
|
804
|
+
next unless directive.task?
|
|
805
|
+
|
|
806
|
+
# Check if all parent conditionals are true
|
|
807
|
+
should_execute = true
|
|
808
|
+
|
|
809
|
+
# If path ends with an @elsif/@else, skip the parent @if index
|
|
810
|
+
# (the index right before the elsif/else in the path)
|
|
811
|
+
path_to_check = directive.conditional_path.dup
|
|
812
|
+
if path_to_check.length >= 2
|
|
813
|
+
last_idx = path_to_check.last
|
|
814
|
+
last_state = conditional_state[last_idx]
|
|
815
|
+
if last_state && %w[elsif else].include?(last_state[:directive_type])
|
|
816
|
+
# Skip the parent @if index (the one before the elsif/else)
|
|
817
|
+
parent_if_idx = path_to_check[path_to_check.length - 2]
|
|
818
|
+
parent_if_state = conditional_state[parent_if_idx]
|
|
819
|
+
if parent_if_state && %w[if unless].include?(parent_if_state[:directive_type])
|
|
820
|
+
path_to_check.delete(parent_if_idx)
|
|
821
|
+
end
|
|
822
|
+
end
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
path_to_check.each do |cond_idx|
|
|
826
|
+
cond_state = conditional_state[cond_idx]
|
|
827
|
+
if cond_state.nil? || !cond_state[:evaluated] || !cond_state[:result]
|
|
828
|
+
should_execute = false
|
|
829
|
+
break
|
|
830
|
+
end
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
next unless should_execute
|
|
834
|
+
|
|
835
|
+
# Convert directive to task
|
|
836
|
+
task = directive.to_task(self, current_log_level: current_log_level)
|
|
837
|
+
next unless task
|
|
838
|
+
|
|
839
|
+
next if (task.optional || Howzit.options[:ask]) && !ask_task(task)
|
|
840
|
+
|
|
841
|
+
run_output, total, success = task.run
|
|
842
|
+
|
|
843
|
+
output.concat(run_output)
|
|
844
|
+
@results[:total] += total
|
|
845
|
+
|
|
846
|
+
if success
|
|
847
|
+
@results[:success] += total
|
|
848
|
+
else
|
|
849
|
+
Howzit.console.warn %({bw}\u{2297} {br}Error running task {bw}"#{task.title}"{x}).c
|
|
850
|
+
|
|
851
|
+
@results[:errors] += total
|
|
852
|
+
|
|
853
|
+
break unless Howzit.options[:force]
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
log_task_result(task, success)
|
|
857
|
+
|
|
858
|
+
# Re-evaluate all open conditionals after task execution
|
|
859
|
+
re_evaluate_conditionals(conditional_state, directive_index - 1, context)
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
total = "{bw}#{@results[:total]}{by} #{@results[:total] == 1 ? 'task' : 'tasks'}".c
|
|
863
|
+
errors = "{bw}#{@results[:errors]}{by} #{@results[:errors] == 1 ? 'error' : 'errors'}".c
|
|
864
|
+
@results[:message] += if @results[:errors].zero?
|
|
865
|
+
"{bg}\u{2713} {by}Ran #{total}{x}".c
|
|
866
|
+
elsif Howzit.options[:force]
|
|
867
|
+
"{br}\u{2715} {by}Completed #{total} with #{errors}{x}".c
|
|
868
|
+
else
|
|
869
|
+
"{br}\u{2715} {by}Ran #{total}, terminated due to error{x}".c
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
output.push(@results[:message]) if Howzit.options[:log_level] < 2 && !nested && !Howzit.options[:run]
|
|
873
|
+
|
|
874
|
+
unless @postreqs.empty?
|
|
875
|
+
begin
|
|
876
|
+
puts TTY::Box.frame("{bw}#{@postreqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols)
|
|
877
|
+
rescue Errno::EPIPE
|
|
878
|
+
# Pipe closed, ignore
|
|
879
|
+
end
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
output
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
##
|
|
886
|
+
## Find the index of the matching @if/@unless for an @elsif/@else/@end
|
|
887
|
+
##
|
|
888
|
+
def find_matching_if_index(current_index)
|
|
889
|
+
stack_depth = 0
|
|
890
|
+
(current_index - 1).downto(0) do |i|
|
|
891
|
+
dir = @directives[i]
|
|
892
|
+
next unless dir.conditional?
|
|
893
|
+
|
|
894
|
+
case dir.type
|
|
895
|
+
when :end
|
|
896
|
+
stack_depth += 1
|
|
897
|
+
when :if, :unless
|
|
898
|
+
return i if stack_depth.zero?
|
|
899
|
+
|
|
900
|
+
stack_depth -= 1
|
|
901
|
+
|
|
902
|
+
when :elsif, :else
|
|
903
|
+
stack_depth -= 1 if stack_depth.positive?
|
|
904
|
+
end
|
|
905
|
+
end
|
|
906
|
+
nil
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
##
|
|
910
|
+
## Re-evaluate conditionals after a task runs (variables may have changed)
|
|
911
|
+
##
|
|
912
|
+
def re_evaluate_conditionals(conditional_state, current_index, context)
|
|
913
|
+
# Re-evaluate all conditionals that come after the current task
|
|
914
|
+
# and before the next task
|
|
915
|
+
(current_index + 1).upto(@directives.length - 1) do |i|
|
|
916
|
+
dir = @directives[i]
|
|
917
|
+
break if dir.task? # Stop at next task
|
|
918
|
+
|
|
919
|
+
next unless dir.conditional?
|
|
920
|
+
|
|
921
|
+
case dir.type
|
|
922
|
+
when :if, :unless
|
|
923
|
+
if conditional_state[i]
|
|
924
|
+
# Re-evaluate
|
|
925
|
+
result = ConditionEvaluator.evaluate(dir.condition, context)
|
|
926
|
+
result = !result if dir.directive_type == 'unless'
|
|
927
|
+
conditional_state[i][:result] = result
|
|
928
|
+
conditional_state[i][:matched_chain] = result
|
|
929
|
+
end
|
|
930
|
+
when :elsif
|
|
931
|
+
matching_if_index = find_matching_if_index(i)
|
|
932
|
+
if matching_if_index && conditional_state[matching_if_index]
|
|
933
|
+
parent_state = conditional_state[matching_if_index]
|
|
934
|
+
if conditional_state[i]
|
|
935
|
+
if parent_state[:matched_chain] && !conditional_state[i][:matched_chain]
|
|
936
|
+
conditional_state[i][:result] = false
|
|
937
|
+
else
|
|
938
|
+
result = ConditionEvaluator.evaluate(dir.condition, context)
|
|
939
|
+
conditional_state[i][:result] = result
|
|
940
|
+
conditional_state[i][:matched_chain] = result
|
|
941
|
+
parent_state[:matched_chain] = true if result
|
|
942
|
+
end
|
|
943
|
+
end
|
|
944
|
+
end
|
|
945
|
+
when :else
|
|
946
|
+
matching_if_index = find_matching_if_index(i)
|
|
947
|
+
if matching_if_index && conditional_state[matching_if_index]
|
|
948
|
+
parent_state = conditional_state[matching_if_index]
|
|
949
|
+
if conditional_state[i]
|
|
950
|
+
if parent_state[:matched_chain]
|
|
951
|
+
conditional_state[i][:result] = false
|
|
952
|
+
else
|
|
953
|
+
conditional_state[i][:result] = true
|
|
954
|
+
conditional_state[i][:matched_chain] = true
|
|
955
|
+
parent_state[:matched_chain] = true
|
|
956
|
+
end
|
|
957
|
+
end
|
|
958
|
+
end
|
|
959
|
+
when :end
|
|
960
|
+
# No re-evaluation needed
|
|
961
|
+
end
|
|
962
|
+
end
|
|
963
|
+
end
|
|
401
964
|
end
|
|
402
965
|
end
|