bash-merge 1.0.0

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.
data/REEK ADDED
File without changes
data/RUBOCOP.md ADDED
@@ -0,0 +1,71 @@
1
+ # RuboCop Usage Guide
2
+
3
+ ## Overview
4
+
5
+ A tale of two RuboCop plugin gems.
6
+
7
+ ### RuboCop Gradual
8
+
9
+ This project uses `rubocop_gradual` instead of vanilla RuboCop for code style checking. The `rubocop_gradual` tool allows for gradual adoption of RuboCop rules by tracking violations in a lock file.
10
+
11
+ ### RuboCop LTS
12
+
13
+ This project uses `rubocop-lts` to ensure, on a best-effort basis, compatibility with Ruby >= 1.9.2.
14
+ RuboCop rules are meticulously configured by the `rubocop-lts` family of gems to ensure that a project is compatible with a specific version of Ruby. See: https://rubocop-lts.gitlab.io for more.
15
+
16
+ ## Checking RuboCop Violations
17
+
18
+ To check for RuboCop violations in this project, always use:
19
+
20
+ ```bash
21
+ bundle exec rake rubocop_gradual:check
22
+ ```
23
+
24
+ **Do not use** the standard RuboCop commands like:
25
+ - `bundle exec rubocop`
26
+ - `rubocop`
27
+
28
+ ## Understanding the Lock File
29
+
30
+ The `.rubocop_gradual.lock` file tracks all current RuboCop violations in the project. This allows the team to:
31
+
32
+ 1. Prevent new violations while gradually fixing existing ones
33
+ 2. Track progress on code style improvements
34
+ 3. Ensure CI builds don't fail due to pre-existing violations
35
+
36
+ ## Common Commands
37
+
38
+ - **Check violations**
39
+ - `bundle exec rake rubocop_gradual`
40
+ - `bundle exec rake rubocop_gradual:check`
41
+ - **(Safe) Autocorrect violations, and update lockfile if no new violations**
42
+ - `bundle exec rake rubocop_gradual:autocorrect`
43
+ - **Force update the lock file (w/o autocorrect) to match violations present in code**
44
+ - `bundle exec rake rubocop_gradual:force_update`
45
+
46
+ ## Workflow
47
+
48
+ 1. Before submitting a PR, run `bundle exec rake rubocop_gradual:autocorrect`
49
+ a. or just the default `bundle exec rake`, as autocorrection is a pre-requisite of the default task.
50
+ 2. If there are new violations, either:
51
+ - Fix them in your code
52
+ - Run `bundle exec rake rubocop_gradual:force_update` to update the lock file (only for violations you can't fix immediately)
53
+ 3. Commit the updated `.rubocop_gradual.lock` file along with your changes
54
+
55
+ ## Never add inline RuboCop disables
56
+
57
+ Do not add inline `rubocop:disable` / `rubocop:enable` comments anywhere in the codebase (including specs, except when following the few existing `rubocop:disable` patterns for a rule already being disabled elsewhere in the code). We handle exceptions in two supported ways:
58
+
59
+ - Permanent/structural exceptions: prefer adjusting the RuboCop configuration (e.g., in `.rubocop.yml`) to exclude a rule for a path or file pattern when it makes sense project-wide.
60
+ - Temporary exceptions while improving code: record the current violations in `.rubocop_gradual.lock` via the gradual workflow:
61
+ - `bundle exec rake rubocop_gradual:autocorrect` (preferred; will autocorrect what it can and update the lock only if no new violations were introduced)
62
+ - If needed, `bundle exec rake rubocop_gradual:force_update` (as a last resort when you cannot fix the newly reported violations immediately)
63
+
64
+ In general, treat the rules as guidance to follow; fix violations rather than ignore them. For example, RSpec conventions in this project expect `described_class` to be used in specs that target a specific class under test.
65
+
66
+ ## Benefits of rubocop_gradual
67
+
68
+ - Allows incremental adoption of code style rules
69
+ - Prevents CI failures due to pre-existing violations
70
+ - Provides a clear record of code style debt
71
+ - Enables focused efforts on improving code quality over time
data/SECURITY.md ADDED
@@ -0,0 +1,21 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ |----------|-----------|
7
+ | 1.latest | ✅ |
8
+
9
+ ## Security contact information
10
+
11
+ To report a security vulnerability, please use the
12
+ [Tidelift security contact](https://tidelift.com/security).
13
+ Tidelift will coordinate the fix and disclosure.
14
+
15
+ ## Additional Support
16
+
17
+ If you are interested in support for versions older than the latest release,
18
+ please consider sponsoring the project / maintainer @ https://liberapay.com/pboling/donate,
19
+ or find other sponsorship links in the [README].
20
+
21
+ [README]: README.md
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bash
4
+ module Merge
5
+ # Extracts and tracks comments with their line numbers from Bash source.
6
+ # Bash comments use the # syntax, making freeze block detection straightforward.
7
+ #
8
+ # @example Basic usage
9
+ # tracker = CommentTracker.new(bash_source)
10
+ # tracker.comments # => [{line: 1, indent: 0, text: "This is a comment"}]
11
+ # tracker.comment_at(1) # => {line: 1, indent: 0, text: "This is a comment"}
12
+ #
13
+ # @example Comment types
14
+ # # Full-line comment
15
+ # 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
+
30
+ # Initialize comment tracker by scanning the source
31
+ #
32
+ # @param source [String] Bash source code
33
+ def initialize(source)
34
+ @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?
101
+ end
102
+
103
+ # Check if a line is a shebang
104
+ #
105
+ # @param line_num [Integer] 1-based line number
106
+ # @return [Boolean]
107
+ def shebang?(line_num)
108
+ return false if line_num < 1 || line_num > @lines.length
109
+
110
+ @lines[line_num - 1].start_with?("#!")
111
+ end
112
+
113
+ private
114
+
115
+ def extract_comments
116
+ comments = []
117
+
118
+ @lines.each_with_index do |line, idx|
119
+ line_num = idx + 1
120
+
121
+ # Skip shebang lines
122
+ next if line.start_with?("#!")
123
+
124
+ # Check for full-line comment
125
+ if (match = line.match(FULL_LINE_COMMENT_REGEX))
126
+ comments << {
127
+ line: line_num,
128
+ indent: match[1].length,
129
+ text: match[2],
130
+ full_line: true,
131
+ raw: line,
132
+ }
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
+ end
147
+ end
148
+
149
+ comments
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bash
4
+ module Merge
5
+ # Resolves conflicts between template and destination Bash content
6
+ # using structural signatures and configurable preferences.
7
+ #
8
+ # @example Basic usage
9
+ # resolver = ConflictResolver.new(template_analysis, dest_analysis)
10
+ # resolver.resolve(result)
11
+ class ConflictResolver < ::Ast::Merge::ConflictResolverBase
12
+ # Alias for backward compatibility with existing API
13
+ alias_method :preference, :preference
14
+
15
+ # Creates a new ConflictResolver
16
+ #
17
+ # @param template_analysis [FileAnalysis] Analyzed template file
18
+ # @param dest_analysis [FileAnalysis] Analyzed destination file
19
+ # @param preference [Symbol] Which version to prefer when
20
+ # nodes have matching signatures:
21
+ # - :destination (default) - Keep destination version (customizations)
22
+ # - :template - Use template version (updates)
23
+ # @param add_template_only_nodes [Boolean] Whether to add nodes only in template
24
+ # @param match_refiner [#call, nil] Optional match refiner for fuzzy matching
25
+ # @param options [Hash] Additional options for forward compatibility
26
+ def initialize(template_analysis, dest_analysis, preference: :destination, add_template_only_nodes: false, match_refiner: nil, **options)
27
+ super(
28
+ strategy: :batch,
29
+ preference: preference,
30
+ template_analysis: template_analysis,
31
+ dest_analysis: dest_analysis,
32
+ add_template_only_nodes: add_template_only_nodes,
33
+ match_refiner: match_refiner,
34
+ **options
35
+ )
36
+ end
37
+
38
+ # Resolve conflicts and populate the result
39
+ #
40
+ # @param result [MergeResult] Result object to populate
41
+ def resolve(result)
42
+ DebugLogger.time("ConflictResolver#resolve") do
43
+ template_nodes = @template_analysis.nodes
44
+ dest_nodes = @dest_analysis.nodes
45
+
46
+ # Build signature maps
47
+ template_by_sig = build_signature_map(template_nodes, @template_analysis)
48
+ dest_by_sig = build_signature_map(dest_nodes, @dest_analysis)
49
+
50
+ # Track which nodes have been processed
51
+ processed_template_sigs = ::Set.new
52
+ processed_dest_sigs = ::Set.new
53
+
54
+ # Process nodes in order, preferring destination order when nodes match
55
+ merge_nodes(
56
+ template_nodes,
57
+ dest_nodes,
58
+ template_by_sig,
59
+ dest_by_sig,
60
+ processed_template_sigs,
61
+ processed_dest_sigs,
62
+ result,
63
+ )
64
+
65
+ DebugLogger.debug("Conflict resolution complete", {
66
+ template_nodes: template_nodes.size,
67
+ dest_nodes: dest_nodes.size,
68
+ result_lines: result.line_count,
69
+ })
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def merge_nodes(template_nodes, dest_nodes, template_by_sig, dest_by_sig, processed_template_sigs, processed_dest_sigs, result)
76
+ # Determine the output order based on preference
77
+ # We'll iterate through destination nodes first (to preserve dest order for matches)
78
+ # then add any template-only nodes if configured
79
+
80
+ # First pass: Process destination nodes and find matches
81
+ dest_nodes.each do |dest_node|
82
+ dest_sig = @dest_analysis.generate_signature(dest_node)
83
+
84
+ # Freeze blocks from destination are always preserved
85
+ if freeze_node?(dest_node)
86
+ add_node_to_result(dest_node, result, :destination, DECISION_FREEZE_BLOCK)
87
+ processed_dest_sigs << dest_sig if dest_sig
88
+ next
89
+ end
90
+
91
+ if dest_sig && template_by_sig[dest_sig]
92
+ # Found matching node in template
93
+ template_info = template_by_sig[dest_sig].first
94
+ template_node = template_info[:node]
95
+
96
+ # Decide which to keep based on preference
97
+ if @preference == :destination
98
+ add_node_to_result(dest_node, result, :destination, DECISION_KEPT_DEST)
99
+ else
100
+ add_node_to_result(template_node, result, :template, DECISION_KEPT_TEMPLATE)
101
+ end
102
+
103
+ processed_dest_sigs << dest_sig
104
+ processed_template_sigs << dest_sig
105
+ else
106
+ # Destination-only node - always keep
107
+ add_node_to_result(dest_node, result, :destination, DECISION_KEPT_DEST)
108
+ processed_dest_sigs << dest_sig if dest_sig
109
+ end
110
+ end
111
+
112
+ # Second pass: Add template-only nodes if configured
113
+ return unless @add_template_only_nodes
114
+
115
+ template_nodes.each do |template_node|
116
+ template_sig = @template_analysis.generate_signature(template_node)
117
+
118
+ # Skip if already processed (matched with dest)
119
+ next if template_sig && processed_template_sigs.include?(template_sig)
120
+
121
+ # Skip freeze blocks from template (they shouldn't exist, but just in case)
122
+ next if freeze_node?(template_node)
123
+
124
+ # Add template-only node
125
+ add_node_to_result(template_node, result, :template, DECISION_ADDED)
126
+ processed_template_sigs << template_sig if template_sig
127
+ end
128
+ end
129
+
130
+ def add_node_to_result(node, result, source, decision)
131
+ if freeze_node?(node)
132
+ result.add_freeze_block(node)
133
+ elsif node.is_a?(NodeWrapper)
134
+ add_wrapper_to_result(node, result, source, decision)
135
+ else
136
+ DebugLogger.debug("Unknown node type", {node_type: node.class.name})
137
+ end
138
+ end
139
+
140
+ def add_wrapper_to_result(wrapper, result, source, decision)
141
+ return unless wrapper.start_line && wrapper.end_line
142
+
143
+ analysis = (source == :template) ? @template_analysis : @dest_analysis
144
+
145
+ # Include leading comments
146
+ leading = analysis.comment_tracker.leading_comments_before(wrapper.start_line)
147
+ leading.each do |comment|
148
+ result.add_line(comment[:raw], decision: decision, source: source, original_line: comment[:line])
149
+ end
150
+
151
+ # Add the node content
152
+ (wrapper.start_line..wrapper.end_line).each do |line_num|
153
+ line = analysis.line_at(line_num)
154
+ next unless line
155
+
156
+ result.add_line(line.chomp, decision: decision, source: source, original_line: line_num)
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bash
4
+ module Merge
5
+ # Debug logging utility for Bash::Merge.
6
+ # Extends the base Ast::Merge::DebugLogger with Bash-specific configuration.
7
+ #
8
+ # @example Enable debug logging
9
+ # ENV['BASH_MERGE_DEBUG'] = '1'
10
+ # DebugLogger.debug("Processing node", {type: "function_definition", line: 5})
11
+ #
12
+ # @example Disable debug logging (default)
13
+ # DebugLogger.debug("This won't be printed", {})
14
+ module DebugLogger
15
+ extend Ast::Merge::DebugLogger
16
+
17
+ # Bash-specific configuration
18
+ self.env_var_name = "BASH_MERGE_DEBUG"
19
+ self.log_prefix = "[Bash::Merge]"
20
+
21
+ class << self
22
+ # Override log_node to handle Bash-specific node types.
23
+ #
24
+ # @param node [Object] Node to log information about
25
+ # @param label [String] Label for the node
26
+ def log_node(node, label: "Node")
27
+ return unless enabled?
28
+
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
37
+
38
+ debug(label, info)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bash
4
+ module Merge
5
+ # Custom Bash emitter that preserves comments and formatting.
6
+ # This class provides utilities for emitting Bash while maintaining
7
+ # the original structure, comments, and style choices.
8
+ #
9
+ # @example Basic usage
10
+ # emitter = Emitter.new
11
+ # emitter.emit_comment("This is a comment")
12
+ # emitter.emit_line("echo 'hello'")
13
+ class Emitter
14
+ # @return [Array<String>] Output lines
15
+ attr_reader :lines
16
+
17
+ # @return [Integer] Current indentation level
18
+ attr_reader :indent_level
19
+
20
+ # @return [Integer] Spaces per indent level
21
+ attr_reader :indent_size
22
+
23
+ # Initialize a new emitter
24
+ #
25
+ # @param indent_size [Integer] Number of spaces per indent level
26
+ def initialize(indent_size: 2)
27
+ @lines = []
28
+ @indent_level = 0
29
+ @indent_size = indent_size
30
+ end
31
+
32
+ # Emit a comment line
33
+ #
34
+ # @param text [String] Comment text (without #)
35
+ # @param inline [Boolean] Whether this is an inline comment
36
+ def emit_comment(text, inline: false)
37
+ if inline
38
+ # Inline comments are appended to the last line
39
+ return if @lines.empty?
40
+
41
+ @lines[-1] = "#{@lines[-1]} # #{text}"
42
+ else
43
+ @lines << "#{current_indent}# #{text}"
44
+ end
45
+ end
46
+
47
+ # Emit leading comments
48
+ #
49
+ # @param comments [Array<Hash>] Comment hashes from CommentTracker
50
+ def emit_leading_comments(comments)
51
+ comments.each do |comment|
52
+ # Preserve original indentation from comment
53
+ indent = " " * (comment[:indent] || 0)
54
+ @lines << "#{indent}# #{comment[:text]}"
55
+ end
56
+ end
57
+
58
+ # Emit a blank line
59
+ def emit_blank_line
60
+ @lines << ""
61
+ end
62
+
63
+ # Emit a shebang line
64
+ #
65
+ # @param interpreter [String] Interpreter path (e.g., "/bin/bash")
66
+ def emit_shebang(interpreter = "/bin/bash")
67
+ @lines << "#!#{interpreter}"
68
+ end
69
+
70
+ # Emit a variable assignment
71
+ #
72
+ # @param name [String] Variable name
73
+ # @param value [String] Variable value
74
+ # @param export [Boolean] Whether to export the variable
75
+ # @param inline_comment [String, nil] Optional inline comment
76
+ def emit_variable_assignment(name, value, export: false, inline_comment: nil)
77
+ prefix = export ? "export " : ""
78
+ line = "#{current_indent}#{prefix}#{name}=#{value}"
79
+ line += " # #{inline_comment}" if inline_comment
80
+ @lines << line
81
+ end
82
+
83
+ # Emit a function definition start
84
+ #
85
+ # @param name [String] Function name
86
+ def emit_function_start(name)
87
+ @lines << "#{current_indent}#{name}() {"
88
+ @indent_level += 1
89
+ end
90
+
91
+ # Emit a function definition end
92
+ def emit_function_end
93
+ @indent_level -= 1 if @indent_level > 0
94
+ @lines << "#{current_indent}}"
95
+ end
96
+
97
+ # Emit an if statement start
98
+ #
99
+ # @param condition [String] Condition expression
100
+ def emit_if_start(condition)
101
+ @lines << "#{current_indent}if #{condition}; then"
102
+ @indent_level += 1
103
+ end
104
+
105
+ # Emit an elif clause
106
+ #
107
+ # @param condition [String] Condition expression
108
+ def emit_elif(condition)
109
+ @indent_level -= 1 if @indent_level > 0
110
+ @lines << "#{current_indent}elif #{condition}; then"
111
+ @indent_level += 1
112
+ end
113
+
114
+ # Emit an else clause
115
+ def emit_else
116
+ @indent_level -= 1 if @indent_level > 0
117
+ @lines << "#{current_indent}else"
118
+ @indent_level += 1
119
+ end
120
+
121
+ # Emit an if statement end
122
+ def emit_fi
123
+ @indent_level -= 1 if @indent_level > 0
124
+ @lines << "#{current_indent}fi"
125
+ end
126
+
127
+ # Emit a for loop start
128
+ #
129
+ # @param var [String] Loop variable name
130
+ # @param items [String] Items to iterate over
131
+ def emit_for_start(var, items)
132
+ @lines << "#{current_indent}for #{var} in #{items}; do"
133
+ @indent_level += 1
134
+ end
135
+
136
+ # Emit a for loop end
137
+ def emit_done
138
+ @indent_level -= 1 if @indent_level > 0
139
+ @lines << "#{current_indent}done"
140
+ end
141
+
142
+ # Emit a while loop start
143
+ #
144
+ # @param condition [String] Condition expression
145
+ def emit_while_start(condition)
146
+ @lines << "#{current_indent}while #{condition}; do"
147
+ @indent_level += 1
148
+ end
149
+
150
+ # Emit a case statement start
151
+ #
152
+ # @param expression [String] Expression to match
153
+ def emit_case_start(expression)
154
+ @lines << "#{current_indent}case #{expression} in"
155
+ @indent_level += 1
156
+ end
157
+
158
+ # Emit a case pattern
159
+ #
160
+ # @param pattern [String] Pattern to match
161
+ def emit_case_pattern(pattern)
162
+ @lines << "#{current_indent}#{pattern})"
163
+ @indent_level += 1
164
+ end
165
+
166
+ # Emit a case pattern terminator
167
+ def emit_case_pattern_end
168
+ @indent_level -= 1 if @indent_level > 0
169
+ @lines << "#{current_indent};;"
170
+ end
171
+
172
+ # Emit a case statement end
173
+ def emit_esac
174
+ @indent_level -= 1 if @indent_level > 0
175
+ @lines << "#{current_indent}esac"
176
+ end
177
+
178
+ # Emit a raw line of code
179
+ #
180
+ # @param line [String] Line to emit
181
+ def emit_line(line)
182
+ @lines << "#{current_indent}#{line}"
183
+ end
184
+
185
+ # Emit raw lines (for preserving existing content)
186
+ #
187
+ # @param raw_lines [Array<String>] Lines to emit as-is
188
+ def emit_raw_lines(raw_lines)
189
+ raw_lines.each { |line| @lines << line.chomp }
190
+ end
191
+
192
+ # Get the output as a single string
193
+ #
194
+ # @return [String]
195
+ def to_bash
196
+ content = @lines.join("\n")
197
+ content += "\n" unless content.empty? || content.end_with?("\n")
198
+ content
199
+ end
200
+
201
+ # Alias for consistency with other merge gems
202
+ # @return [String]
203
+ alias_method :to_s, :to_bash
204
+
205
+ # Clear the output
206
+ def clear
207
+ @lines = []
208
+ @indent_level = 0
209
+ end
210
+
211
+ private
212
+
213
+ def current_indent
214
+ " " * (@indent_level * @indent_size)
215
+ end
216
+ end
217
+ end
218
+ end