bash-merge 2.0.5 → 7.1.1

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.
@@ -5,6 +5,10 @@ module Bash
5
5
  # Extracts and tracks comments with their line numbers from Bash source.
6
6
  # Bash comments use the # syntax, making freeze block detection straightforward.
7
7
  #
8
+ # Inherits shared lookup, query, region-building, and attachment API from
9
+ # +Ast::Merge::Comment::HashTrackerBase+. Only format-specific comment
10
+ # extraction, shebang detection, and owner resolution are overridden here.
11
+ #
8
12
  # @example Basic usage
9
13
  # tracker = CommentTracker.new(bash_source)
10
14
  # tracker.comments # => [{line: 1, indent: 0, text: "This is a comment"}]
@@ -13,91 +17,14 @@ module Bash
13
17
  # @example Comment types
14
18
  # # Full-line comment
15
19
  # command # Inline comment
16
- class CommentTracker
17
- # Regex to match full-line comments (line is only whitespace + comment)
18
- FULL_LINE_COMMENT_REGEX = /\A(\s*)#\s?(.*)\z/
19
-
20
- # Regex to match inline comments (comment after Bash content)
21
- # Note: This is simplified and doesn't handle all edge cases like comments in strings
22
- INLINE_COMMENT_REGEX = /\s+#\s?(.*)$/
23
-
24
- # @return [Array<Hash>] All extracted comments with metadata
25
- attr_reader :comments
26
-
27
- # @return [Array<String>] Source lines
28
- attr_reader :lines
29
-
20
+ class CommentTracker < Ast::Merge::Comment::HashTrackerBase
30
21
  # Initialize comment tracker by scanning the source
31
22
  #
32
23
  # @param source [String] Bash source code
33
24
  def initialize(source)
34
25
  @source = source
35
- @lines = source.lines.map(&:chomp)
36
- @comments = extract_comments
37
- @comments_by_line = @comments.group_by { |c| c[:line] }
38
- end
39
-
40
- # Get comment at a specific line
41
- #
42
- # @param line_num [Integer] 1-based line number
43
- # @return [Hash, nil] Comment info or nil
44
- def comment_at(line_num)
45
- @comments_by_line[line_num]&.first
46
- end
47
-
48
- # Get all comments in a line range
49
- #
50
- # @param range [Range] Range of 1-based line numbers
51
- # @return [Array<Hash>] Comments in the range
52
- def comments_in_range(range)
53
- @comments.select { |c| range.cover?(c[:line]) }
54
- end
55
-
56
- # Get leading comments before a line (consecutive comment lines immediately above)
57
- #
58
- # @param line_num [Integer] 1-based line number
59
- # @return [Array<Hash>] Leading comments
60
- def leading_comments_before(line_num)
61
- leading = []
62
- current = line_num - 1
63
-
64
- while current >= 1
65
- comment = comment_at(current)
66
- break unless comment && comment[:full_line]
67
-
68
- leading.unshift(comment)
69
- current -= 1
70
- end
71
-
72
- leading
73
- end
74
-
75
- # Get trailing comment on the same line (inline comment)
76
- #
77
- # @param line_num [Integer] 1-based line number
78
- # @return [Hash, nil] Inline comment or nil
79
- def inline_comment_at(line_num)
80
- comment = comment_at(line_num)
81
- comment if comment && !comment[:full_line]
82
- end
83
-
84
- # Check if a line is a full-line comment
85
- #
86
- # @param line_num [Integer] 1-based line number
87
- # @return [Boolean]
88
- def full_line_comment?(line_num)
89
- comment = comment_at(line_num)
90
- comment&.dig(:full_line) || false
91
- end
92
-
93
- # Check if a line is blank
94
- #
95
- # @param line_num [Integer] 1-based line number
96
- # @return [Boolean]
97
- def blank_line?(line_num)
98
- return false if line_num < 1 || line_num > @lines.length
99
-
100
- @lines[line_num - 1].strip.empty?
26
+ @line_parser = Ast::Merge::Comment::QuotedHashLineParser.new
27
+ super(source.lines.map(&:chomp))
101
28
  end
102
29
 
103
30
  # Check if a line is a shebang
@@ -107,7 +34,19 @@ module Bash
107
34
  def shebang?(line_num)
108
35
  return false if line_num < 1 || line_num > @lines.length
109
36
 
110
- @lines[line_num - 1].start_with?("#!")
37
+ @lines[line_num - 1].start_with?('#!')
38
+ end
39
+
40
+ def augment(owners: [], **options)
41
+ Ast::Merge::Comment::Augmenter.new(
42
+ lines: @lines,
43
+ comments: @comments,
44
+ owners: owners,
45
+ style: :hash_comment,
46
+ total_comment_count: @comments.size,
47
+ inline_comment_count: @comments.count { |comment| !comment[:full_line] },
48
+ **options
49
+ )
111
50
  end
112
51
 
113
52
  private
@@ -119,30 +58,27 @@ module Bash
119
58
  line_num = idx + 1
120
59
 
121
60
  # Skip shebang lines
122
- next if line.start_with?("#!")
61
+ next if line.start_with?('#!')
62
+
63
+ parsed = @line_parser.parse(line)
64
+ next unless parsed
123
65
 
124
- # Check for full-line comment
125
- if (match = line.match(FULL_LINE_COMMENT_REGEX))
66
+ if parsed.full_line?
126
67
  comments << {
127
68
  line: line_num,
128
- indent: match[1].length,
129
- text: match[2],
69
+ indent: parsed.indent,
70
+ text: parsed.text,
130
71
  full_line: true,
131
- raw: line,
72
+ raw: parsed.raw
73
+ }
74
+ elsif parsed.inline?
75
+ comments << {
76
+ line: line_num,
77
+ indent: 0,
78
+ text: parsed.text,
79
+ full_line: false,
80
+ raw: parsed.raw
132
81
  }
133
- # Check for inline comment (simplified - doesn't handle quotes)
134
- elsif line.include?(" #") && !line.strip.start_with?("#")
135
- # Try to extract inline comment, but be careful with strings
136
- # This is a simplified approach
137
- if (inline_match = line.match(INLINE_COMMENT_REGEX))
138
- comments << {
139
- line: line_num,
140
- indent: 0,
141
- text: inline_match[1],
142
- full_line: false,
143
- raw: "# #{inline_match[1]}",
144
- }
145
- end
146
82
  end
147
83
  end
148
84
 
@@ -15,25 +15,25 @@ module Bash
15
15
  extend Ast::Merge::DebugLogger
16
16
 
17
17
  # Bash-specific configuration
18
- self.env_var_name = "BASH_MERGE_DEBUG"
19
- self.log_prefix = "[Bash::Merge]"
18
+ self.env_var_name = 'BASH_MERGE_DEBUG'
19
+ self.log_prefix = '[Bash::Merge]'
20
20
 
21
21
  class << self
22
22
  # Override log_node to handle Bash-specific node types.
23
23
  #
24
24
  # @param node [Object] Node to log information about
25
25
  # @param label [String] Label for the node
26
- def log_node(node, label: "Node")
26
+ def log_node(node, label: 'Node')
27
27
  return unless enabled?
28
28
 
29
29
  info = case node
30
- when Bash::Merge::FreezeNode
31
- {type: "FreezeNode", lines: "#{node.start_line}..#{node.end_line}"}
32
- when Bash::Merge::NodeWrapper
33
- {type: node.type.to_s, lines: "#{node.start_line}..#{node.end_line}"}
34
- else
35
- extract_node_info(node)
36
- end
30
+ when Bash::Merge::FreezeNode
31
+ { type: 'FreezeNode', lines: "#{node.start_line}..#{node.end_line}" }
32
+ when Bash::Merge::NodeWrapper
33
+ { type: node.type.to_s, lines: "#{node.start_line}..#{node.end_line}" }
34
+ else
35
+ extract_node_info(node)
36
+ end
37
37
 
38
38
  debug(label, info)
39
39
  end
@@ -13,21 +13,27 @@ module Bash
13
13
  # emitter.emit_comment("This is a comment")
14
14
  # emitter.emit_line("echo 'hello'")
15
15
  class Emitter < Ast::Merge::EmitterBase
16
+ include Ast::Merge::EmitterLineMetadataSupport
17
+
16
18
  # Initialize subclass-specific state
17
- def initialize_subclass_state(**options)
18
- # Bash doesn't need separator tracking like JSON
19
+ def initialize_subclass_state(**_options)
20
+ initialize_line_metadata_state
19
21
  end
20
22
 
21
23
  # Clear subclass-specific state
22
24
  def clear_subclass_state
23
- # Nothing to clear for Bash
25
+ clear_line_metadata_state
26
+ end
27
+
28
+ def emit_blank_line
29
+ append_line('')
24
30
  end
25
31
 
26
32
  # Emit a tracked comment from CommentTracker
27
33
  # @param comment [Hash] Comment with :text, :indent
28
34
  def emit_tracked_comment(comment)
29
- indent = " " * (comment[:indent] || 0)
30
- @lines << "#{indent}# #{comment[:text]}"
35
+ indent = ' ' * (comment[:indent] || 0)
36
+ append_line("#{indent}# #{comment[:text]}")
31
37
  end
32
38
 
33
39
  # Emit a comment line
@@ -41,15 +47,15 @@ module Bash
41
47
 
42
48
  @lines[-1] = "#{@lines[-1]} # #{text}"
43
49
  else
44
- @lines << "#{current_indent}# #{text}"
50
+ append_line("#{current_indent}# #{text}")
45
51
  end
46
52
  end
47
53
 
48
54
  # Emit a shebang line
49
55
  #
50
56
  # @param interpreter [String] Interpreter path (e.g., "/bin/bash")
51
- def emit_shebang(interpreter = "/bin/bash")
52
- @lines << "#!#{interpreter}"
57
+ def emit_shebang(interpreter = '/bin/bash', metadata: nil)
58
+ append_line("#!#{interpreter}", metadata)
53
59
  end
54
60
 
55
61
  # Emit a variable assignment
@@ -58,113 +64,119 @@ module Bash
58
64
  # @param value [String] Variable value
59
65
  # @param export [Boolean] Whether to export the variable
60
66
  # @param inline_comment [String, nil] Optional inline comment
61
- def emit_variable_assignment(name, value, export: false, inline_comment: nil)
62
- prefix = export ? "export " : ""
67
+ def emit_variable_assignment(name, value, export: false, inline_comment: nil, metadata: nil)
68
+ prefix = export ? 'export ' : ''
63
69
  line = "#{current_indent}#{prefix}#{name}=#{value}"
64
70
  line += " # #{inline_comment}" if inline_comment
65
- @lines << line
71
+ append_line(line, metadata)
66
72
  end
67
73
 
68
74
  # Emit a function definition start
69
75
  #
70
76
  # @param name [String] Function name
71
- def emit_function_start(name)
72
- @lines << "#{current_indent}#{name}() {"
77
+ def emit_function_start(name, metadata: nil)
78
+ append_line("#{current_indent}#{name}() {", metadata)
73
79
  indent
74
80
  end
75
81
 
76
82
  # Emit a function definition end
77
83
  def emit_function_end
78
84
  dedent
79
- @lines << "#{current_indent}}"
85
+ append_line("#{current_indent}}")
80
86
  end
81
87
 
82
88
  # Emit an if statement start
83
89
  #
84
90
  # @param condition [String] Condition expression
85
- def emit_if_start(condition)
86
- @lines << "#{current_indent}if #{condition}; then"
91
+ def emit_if_start(condition, metadata: nil)
92
+ append_line("#{current_indent}if #{condition}; then", metadata)
87
93
  indent
88
94
  end
89
95
 
90
96
  # Emit an elif clause
91
97
  #
92
98
  # @param condition [String] Condition expression
93
- def emit_elif(condition)
99
+ def emit_elif(condition, metadata: nil)
94
100
  dedent
95
- @lines << "#{current_indent}elif #{condition}; then"
101
+ append_line("#{current_indent}elif #{condition}; then", metadata)
96
102
  indent
97
103
  end
98
104
 
99
105
  # Emit an else clause
100
106
  def emit_else
101
107
  dedent
102
- @lines << "#{current_indent}else"
108
+ append_line("#{current_indent}else")
103
109
  indent
104
110
  end
105
111
 
106
112
  # Emit an if statement end
107
113
  def emit_fi
108
114
  dedent
109
- @lines << "#{current_indent}fi"
115
+ append_line("#{current_indent}fi")
110
116
  end
111
117
 
112
118
  # Emit a for loop start
113
119
  #
114
120
  # @param var [String] Loop variable name
115
121
  # @param items [String] Items to iterate over
116
- def emit_for_start(var, items)
117
- @lines << "#{current_indent}for #{var} in #{items}; do"
122
+ def emit_for_start(var, items, metadata: nil)
123
+ append_line("#{current_indent}for #{var} in #{items}; do", metadata)
118
124
  indent
119
125
  end
120
126
 
121
127
  # Emit a for/while loop end
122
128
  def emit_done
123
129
  dedent
124
- @lines << "#{current_indent}done"
130
+ append_line("#{current_indent}done")
125
131
  end
126
132
 
127
133
  # Emit a while loop start
128
134
  #
129
135
  # @param condition [String] Condition expression
130
- def emit_while_start(condition)
131
- @lines << "#{current_indent}while #{condition}; do"
136
+ def emit_while_start(condition, metadata: nil)
137
+ append_line("#{current_indent}while #{condition}; do", metadata)
132
138
  indent
133
139
  end
134
140
 
135
141
  # Emit a case statement start
136
142
  #
137
143
  # @param expression [String] Expression to match
138
- def emit_case_start(expression)
139
- @lines << "#{current_indent}case #{expression} in"
144
+ def emit_case_start(expression, metadata: nil)
145
+ append_line("#{current_indent}case #{expression} in", metadata)
140
146
  indent
141
147
  end
142
148
 
143
149
  # Emit a case pattern
144
150
  #
145
151
  # @param pattern [String] Pattern to match
146
- def emit_case_pattern(pattern)
147
- @lines << "#{current_indent}#{pattern})"
152
+ def emit_case_pattern(pattern, metadata: nil)
153
+ append_line("#{current_indent}#{pattern})", metadata)
148
154
  indent
149
155
  end
150
156
 
151
157
  # Emit a case pattern terminator
152
158
  def emit_case_pattern_end
153
159
  dedent
154
- @lines << "#{current_indent};;"
160
+ append_line("#{current_indent};;")
155
161
  end
156
162
 
157
163
  # Emit a case statement end
158
164
  def emit_esac
159
165
  dedent
160
- @lines << "#{current_indent}esac"
166
+ append_line("#{current_indent}esac")
161
167
  end
162
168
 
163
169
  # Emit a raw line of code
164
170
  #
165
171
  # @param line [String] Line to emit
166
- def emit_line(line)
167
- @lines << "#{current_indent}#{line}"
172
+ def emit_line(line, metadata: nil)
173
+ append_line("#{current_indent}#{line}", metadata)
174
+ end
175
+
176
+ def emit_raw_lines(raw_lines, metadata: nil)
177
+ raw_lines.each_with_index do |line, idx|
178
+ append_line(line.chomp, expanded_line_metadata(metadata, idx))
179
+ end
168
180
  end
169
181
 
170
182
  # Get the output as a Bash string
@@ -14,7 +14,7 @@ module Bash
14
14
  include Ast::Merge::FileAnalyzable
15
15
 
16
16
  # Default freeze token for identifying freeze blocks
17
- DEFAULT_FREEZE_TOKEN = "bash-merge"
17
+ DEFAULT_FREEZE_TOKEN = 'bash-merge'
18
18
 
19
19
  # @return [CommentTracker] Comment tracker for this file
20
20
  attr_reader :comment_tracker
@@ -42,12 +42,12 @@ module Bash
42
42
  # @param signature_generator [Proc, nil] Custom signature generator
43
43
  # @param parser_path [String, nil] Path to tree-sitter-bash parser library
44
44
  # @param options [Hash] Additional options (forward compatibility - ignored by FileAnalysis)
45
- def initialize(source, freeze_token: DEFAULT_FREEZE_TOKEN, signature_generator: nil, parser_path: nil, **options)
45
+ def initialize(source, freeze_token: DEFAULT_FREEZE_TOKEN, signature_generator: nil, parser_path: nil, **_options)
46
46
  @source = source
47
47
  @lines = source.lines.map(&:chomp)
48
48
  @freeze_token = freeze_token
49
49
  @signature_generator = signature_generator
50
- @parser_path = parser_path || self.class.find_parser_path
50
+ @parser_path = parser_path
51
51
  @errors = []
52
52
  # **options captures any additional parameters (e.g., node_typing) for forward compatibility
53
53
 
@@ -55,18 +55,18 @@ module Bash
55
55
  @comment_tracker = CommentTracker.new(source)
56
56
 
57
57
  # Parse the Bash script
58
- DebugLogger.time("FileAnalysis#parse_bash") { parse_bash }
58
+ DebugLogger.time('FileAnalysis#parse_bash') { parse_bash }
59
59
 
60
60
  # Extract freeze blocks and integrate with nodes
61
61
  @freeze_blocks = extract_freeze_blocks
62
62
  @nodes = integrate_nodes_and_freeze_blocks
63
63
 
64
- DebugLogger.debug("FileAnalysis initialized", {
65
- signature_generator: signature_generator ? "custom" : "default",
66
- nodes_count: @nodes.size,
67
- freeze_blocks: @freeze_blocks.size,
68
- valid: valid?,
69
- })
64
+ DebugLogger.debug('FileAnalysis initialized', {
65
+ signature_generator: signature_generator ? 'custom' : 'default',
66
+ nodes_count: @nodes.size,
67
+ freeze_blocks: @freeze_blocks.size,
68
+ valid: valid?
69
+ })
70
70
  end
71
71
 
72
72
  # Check if parse was successful
@@ -75,14 +75,102 @@ module Bash
75
75
  @errors.empty? && !@ast.nil?
76
76
  end
77
77
 
78
+ # Get shared comment capability information for this analysis.
79
+ #
80
+ # @return [Ast::Merge::Comment::Capability]
81
+ def comment_capability
82
+ @comment_capability ||= comment_tracker.augment(owners: []).capability
83
+ end
84
+
85
+ # Describe how Bash merges currently own and emit comments.
86
+ #
87
+ # Bash comment handling is fully source-augmented and emitted through the
88
+ # synthetic merge layer.
89
+ #
90
+ # @return [Ast::Merge::Comment::SupportStyle]
91
+ def comment_support_style
92
+ @comment_support_style ||= shared_comment_support_style(
93
+ source: :bash_source,
94
+ style: :hash_comment,
95
+ read_strategy: :source_augmented_portable_write
96
+ )
97
+ end
98
+
99
+ # Get all tracked comments converted to shared Ast::Merge comment nodes.
100
+ #
101
+ # @return [Array<Ast::Merge::Comment::Line>]
102
+ def comment_nodes
103
+ comment_tracker.comment_nodes
104
+ end
105
+
106
+ # Get a shared Ast::Merge comment node at a specific line.
107
+ #
108
+ # @param line_num [Integer] 1-based line number
109
+ # @return [Ast::Merge::Comment::Line, nil]
110
+ def comment_node_at(line_num)
111
+ comment_tracker.comment_node_at(line_num)
112
+ end
113
+
114
+ # Get comments in a line range converted to a shared comment region.
115
+ #
116
+ # @param range [Range] Range of 1-based line numbers
117
+ # @param kind [Symbol] Region kind (:leading, :inline, :orphan, etc.)
118
+ # @param full_line_only [Boolean] Whether to keep only full-line comments
119
+ # @return [Ast::Merge::Comment::Region]
120
+ def comment_region_for_range(range, kind:, full_line_only: false)
121
+ comment_tracker.comment_region_for_range(
122
+ range,
123
+ kind: kind,
124
+ full_line_only: full_line_only
125
+ )
126
+ end
127
+
128
+ # Build a passive shared comment attachment for an owner.
129
+ #
130
+ # @param owner [Object] Structural owner for the attachment
131
+ # @param options [Hash] Additional metadata / lookup overrides
132
+ # @return [Ast::Merge::Comment::Attachment]
133
+ def comment_attachment_for(owner, **options)
134
+ shared_comment_attachment_for(
135
+ owner,
136
+ tracker_attachment: comment_tracker.comment_attachment_for(owner, **options),
137
+ **options
138
+ )
139
+ end
140
+
141
+ # @return [Symbol]
142
+ def comment_attachment_strategy
143
+ :augmenter_preferred_tracker_layout
144
+ end
145
+
146
+ def ruleset_owner_selector
147
+ :line_bound_statements
148
+ end
149
+
150
+ def ruleset_render_family
151
+ :bash_script_statements
152
+ end
153
+
154
+ # Build a passive shared comment augmenter for this analysis.
155
+ #
156
+ # @param owners [Array<#start_line,#end_line>, nil] Owners used for attachment inference
157
+ # @param options [Hash] Additional augmenter options
158
+ # @return [Ast::Merge::Comment::Augmenter]
159
+ def comment_augmenter(owners: nil, **options)
160
+ comment_tracker.augment(
161
+ owners: owners || comment_augmenter_default_owners,
162
+ **options
163
+ )
164
+ end
165
+
78
166
  # The base module uses 'statements' - provide both names for compatibility
79
167
  # @return [Array<NodeWrapper, FreezeNodeBase>]
80
168
  def statements
81
- @nodes ||= []
169
+ @nodes || []
82
170
  end
83
171
 
84
172
  # Alias for convenience - bash-merge prefers "nodes" terminology
85
- alias_method :nodes, :statements
173
+ alias nodes statements
86
174
 
87
175
  # Check if a line is within a freeze block.
88
176
  #
@@ -120,30 +208,42 @@ module Bash
120
208
  def top_level_statements
121
209
  return [] unless valid?
122
210
 
123
- root = @ast.root_node
124
- return [] unless root
125
-
126
- statements = []
127
- root.each do |child|
128
- next if child.type.to_s == "comment" # Comments handled separately
129
-
130
- statements << NodeWrapper.new(child, lines: @lines, source: @source)
211
+ @top_level_statements ||= begin
212
+ root = @ast.root_node
213
+ if root
214
+ statements = []
215
+ root.each do |child|
216
+ next if child.type.to_s == 'comment' # Comments handled separately
217
+
218
+ statements << NodeWrapper.new(child, lines: @lines, source: @source)
219
+ end
220
+ statements
221
+ else
222
+ []
223
+ end
131
224
  end
132
- statements
133
225
  end
134
226
 
135
227
  private
136
228
 
137
229
  def parse_bash
138
- # TreeHaver handles grammar discovery and backend selection
230
+ # TreeHaver handles backend selection against the grammars Bash::Merge
231
+ # has already registered during bootstrap.
139
232
  # Set TREE_HAVER_BACKEND=ffi for bash (MRI/Rust have compatibility issues)
140
- parser = TreeHaver.parser_for(:bash, library_path: @parser_path)
233
+ parser = if @parser_path
234
+ TreeHaver.with_language_registration(
235
+ :bash,
236
+ :tree_sitter,
237
+ path: @parser_path,
238
+ symbol: TreeHaver::GrammarFinder.new(:bash).symbol_name
239
+ ) { TreeHaver.parser_for(:bash, backend_type: :tree_sitter) }
240
+ else
241
+ TreeHaver.parser_for(:bash, backend_type: :tree_sitter)
242
+ end
141
243
  @ast = parser.parse(@source)
142
244
 
143
245
  # Check for parse errors in the tree
144
- if @ast&.root_node&.has_error?
145
- collect_parse_errors(@ast.root_node)
146
- end
246
+ collect_parse_errors(@ast.root_node) if @ast&.root_node&.has_error?
147
247
  rescue TreeHaver::Error => e
148
248
  # TreeHaver::Error inherits from Exception, not StandardError.
149
249
  # This also catches TreeHaver::NotAvailable (subclass of Error).
@@ -156,12 +256,12 @@ module Bash
156
256
 
157
257
  def collect_parse_errors(node)
158
258
  # Collect ERROR and MISSING nodes from the tree
159
- if node.type.to_s == "ERROR" || node.missing?
259
+ if node.type.to_s == 'ERROR' || node.missing?
160
260
  @errors << {
161
261
  type: node.type.to_s,
162
262
  start_point: node.start_point,
163
263
  end_point: node.end_point,
164
- text: node.to_s,
264
+ text: node.to_s
165
265
  }
166
266
  end
167
267
 
@@ -180,10 +280,10 @@ module Bash
180
280
  next unless (match = line.match(freeze_pattern))
181
281
 
182
282
  marker_type = match[1]&.downcase # 'freeze' or 'unfreeze'
183
- if marker_type == "freeze"
184
- freeze_starts << {line: line_num, marker: line}
185
- elsif marker_type == "unfreeze"
186
- freeze_ends << {line: line_num, marker: line}
283
+ if marker_type == 'freeze'
284
+ freeze_starts << { line: line_num, marker: line }
285
+ elsif marker_type == 'unfreeze'
286
+ freeze_ends << { line: line_num, marker: line }
187
287
  end
188
288
  end
189
289
 
@@ -202,7 +302,7 @@ module Bash
202
302
  end_line: matching_end[:line],
203
303
  lines: @lines,
204
304
  start_marker: start_info[:marker],
205
- end_marker: matching_end[:marker],
305
+ end_marker: matching_end[:marker]
206
306
  )
207
307
  end
208
308