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.
@@ -0,0 +1,342 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bash
4
+ module Merge
5
+ # Wraps TreeHaver nodes with comment associations, line information, and signatures.
6
+ # This provides a unified interface for working with Bash AST nodes during merging.
7
+ #
8
+ # @example Basic usage
9
+ # parser = TreeHaver::Parser.new
10
+ # parser.language = TreeHaver::Language.bash
11
+ # tree = parser.parse(source)
12
+ # wrapper = NodeWrapper.new(tree.root_node, lines: source.lines, source: source)
13
+ # wrapper.signature # => [:program, ...]
14
+ class NodeWrapper
15
+ # @return [TreeHaver::Node] The wrapped tree-haver node
16
+ attr_reader :node
17
+
18
+ # @return [Array<Hash>] Leading comments associated with this node
19
+ attr_reader :leading_comments
20
+
21
+ # @return [Hash, nil] Inline/trailing comment on the same line
22
+ attr_reader :inline_comment
23
+
24
+ # @return [Integer] Start line (1-based)
25
+ attr_reader :start_line
26
+
27
+ # @return [Integer] End line (1-based)
28
+ attr_reader :end_line
29
+
30
+ # @return [Array<String>] Source lines
31
+ attr_reader :lines
32
+
33
+ # @return [String] The original source string
34
+ attr_reader :source
35
+
36
+ # @param node [TreeHaver::Node] tree-haver node to wrap
37
+ # @param lines [Array<String>] Source lines for content extraction
38
+ # @param source [String] Original source string for byte-based text extraction
39
+ # @param leading_comments [Array<Hash>] Comments before this node
40
+ # @param inline_comment [Hash, nil] Inline comment on the node's line
41
+ def initialize(node, lines:, source: nil, leading_comments: [], inline_comment: nil)
42
+ @node = node
43
+ @lines = lines
44
+ @source = source || lines.join("\n")
45
+ @leading_comments = leading_comments
46
+ @inline_comment = inline_comment
47
+
48
+ # Extract line information from the tree-haver node (0-indexed to 1-indexed)
49
+ if node.respond_to?(:start_point)
50
+ point = node.start_point
51
+ @start_line = (point.respond_to?(:row) ? point.row : point[:row]) + 1
52
+ end
53
+ if node.respond_to?(:end_point)
54
+ point = node.end_point
55
+ @end_line = (point.respond_to?(:row) ? point.row : point[:row]) + 1
56
+ end
57
+
58
+ # Handle edge case where end_line might be before start_line
59
+ @end_line = @start_line if @start_line && @end_line && @end_line < @start_line
60
+ end
61
+
62
+ # Generate a signature for this node for matching purposes.
63
+ # Signatures are used to identify corresponding nodes between template and destination.
64
+ #
65
+ # @return [Array, nil] Signature array or nil if not signaturable
66
+ def signature
67
+ compute_signature(@node)
68
+ end
69
+
70
+ # Check if this is a freeze node
71
+ # @return [Boolean]
72
+ def freeze_node?
73
+ false
74
+ end
75
+
76
+ # Get the node type as a symbol
77
+ # @return [Symbol]
78
+ def type
79
+ @node.type.to_sym
80
+ end
81
+
82
+ # Check if this node has a specific type
83
+ # @param type_name [Symbol, String] Type to check
84
+ # @return [Boolean]
85
+ def type?(type_name)
86
+ @node.type.to_s == type_name.to_s
87
+ end
88
+
89
+ # Check if this is a function definition
90
+ # @return [Boolean]
91
+ def function_definition?
92
+ @node.type.to_s == "function_definition"
93
+ end
94
+
95
+ # Check if this is a variable assignment
96
+ # @return [Boolean]
97
+ def variable_assignment?
98
+ @node.type.to_s == "variable_assignment"
99
+ end
100
+
101
+ # Check if this is an if statement
102
+ # @return [Boolean]
103
+ def if_statement?
104
+ @node.type.to_s == "if_statement"
105
+ end
106
+
107
+ # Check if this is a for loop
108
+ # @return [Boolean]
109
+ def for_statement?
110
+ %w[for_statement c_style_for_statement].include?(@node.type.to_s)
111
+ end
112
+
113
+ # Check if this is a while loop
114
+ # @return [Boolean]
115
+ def while_statement?
116
+ @node.type.to_s == "while_statement"
117
+ end
118
+
119
+ # Check if this is a case statement
120
+ # @return [Boolean]
121
+ def case_statement?
122
+ @node.type.to_s == "case_statement"
123
+ end
124
+
125
+ # Check if this is a command
126
+ # @return [Boolean]
127
+ def command?
128
+ @node.type.to_s == "command"
129
+ end
130
+
131
+ # Check if this is a pipeline
132
+ # @return [Boolean]
133
+ def pipeline?
134
+ @node.type.to_s == "pipeline"
135
+ end
136
+
137
+ # Check if this is a comment
138
+ # @return [Boolean]
139
+ def comment?
140
+ @node.type.to_s == "comment"
141
+ end
142
+
143
+ # Get the function name if this is a function definition
144
+ # @return [String, nil]
145
+ def function_name
146
+ return unless function_definition?
147
+
148
+ # In bash tree-sitter, function name is in a 'name' or 'word' child
149
+ name_node = find_child_by_type("word") || find_child_by_field("name")
150
+ node_text(name_node) if name_node
151
+ end
152
+
153
+ # Get the variable name if this is a variable assignment
154
+ # @return [String, nil]
155
+ def variable_name
156
+ return unless variable_assignment?
157
+
158
+ # In bash tree-sitter, variable name is a child of type 'variable_name'
159
+ name_node = find_child_by_type("variable_name")
160
+ node_text(name_node) if name_node
161
+ end
162
+
163
+ # Get the command name if this is a command
164
+ # @return [String, nil]
165
+ def command_name
166
+ return unless command?
167
+
168
+ # First child that is a word or simple_expansion
169
+ @node.each do |child|
170
+ next if %w[comment file_redirect heredoc_redirect].include?(child.type.to_s)
171
+
172
+ return node_text(child) if %w[word command_name].include?(child.type.to_s)
173
+ end
174
+ nil
175
+ end
176
+
177
+ # Get children wrapped as NodeWrappers
178
+ # @return [Array<NodeWrapper>]
179
+ def children
180
+ return [] unless @node.respond_to?(:each)
181
+
182
+ result = []
183
+ @node.each do |child|
184
+ result << NodeWrapper.new(child, lines: @lines, source: @source)
185
+ end
186
+ result
187
+ end
188
+
189
+ # Find a child by field name
190
+ # @param field_name [String] Field name to look for
191
+ # @return [TreeSitter::Node, nil]
192
+ def find_child_by_field(field_name)
193
+ return unless @node.respond_to?(:child_by_field_name)
194
+
195
+ @node.child_by_field_name(field_name)
196
+ end
197
+
198
+ # Find a child by type
199
+ # @param type_name [String] Type name to look for
200
+ # @return [TreeSitter::Node, nil]
201
+ def find_child_by_type(type_name)
202
+ return unless @node.respond_to?(:each)
203
+
204
+ @node.each do |child|
205
+ return child if child.type.to_s == type_name
206
+ end
207
+ nil
208
+ end
209
+
210
+ # Get the text content for this node by extracting from source using byte positions
211
+ # @return [String]
212
+ def text
213
+ node_text(@node)
214
+ end
215
+
216
+ # Extract text from a tree-sitter node using byte positions
217
+ # @param ts_node [TreeSitter::Node] The tree-sitter node
218
+ # @return [String]
219
+ def node_text(ts_node)
220
+ return "" unless ts_node.respond_to?(:start_byte) && ts_node.respond_to?(:end_byte)
221
+
222
+ @source[ts_node.start_byte...ts_node.end_byte] || ""
223
+ end
224
+
225
+ # Get the content for this node from source lines
226
+ # @return [String]
227
+ def content
228
+ return "" unless @start_line && @end_line
229
+
230
+ (@start_line..@end_line).map { |ln| @lines[ln - 1] }.compact.join("\n")
231
+ end
232
+
233
+ # String representation for debugging
234
+ # @return [String]
235
+ def inspect
236
+ "#<#{self.class.name} type=#{@node.type} lines=#{@start_line}..#{@end_line}>"
237
+ end
238
+
239
+ private
240
+
241
+ def compute_signature(node)
242
+ node_type = node.type.to_s
243
+
244
+ case node_type
245
+ when "program"
246
+ # Root node - signature based on direct children structure
247
+ child_types = []
248
+ node.each { |child| child_types << child.type.to_s unless child.type.to_s == "comment" }
249
+ [:program, child_types.length]
250
+ when "function_definition"
251
+ # Functions are identified by their name
252
+ name = function_name
253
+ [:function_definition, name]
254
+ when "variable_assignment"
255
+ # Variable assignments are identified by variable name
256
+ name = variable_name
257
+ [:variable_assignment, name]
258
+ when "command"
259
+ # Commands identified by their command name
260
+ name = command_name
261
+ [:command, name, extract_command_signature_context(node)]
262
+ when "if_statement"
263
+ # If statements identified by their condition pattern
264
+ condition = extract_condition_pattern(node)
265
+ [:if_statement, condition]
266
+ when "for_statement", "c_style_for_statement"
267
+ # For loops identified by their loop variable
268
+ var = extract_loop_variable(node)
269
+ [:for_statement, var]
270
+ when "while_statement"
271
+ # While loops identified by condition
272
+ condition = extract_condition_pattern(node)
273
+ [:while_statement, condition]
274
+ when "case_statement"
275
+ # Case statements identified by the expression being matched
276
+ expr = extract_case_expression(node)
277
+ [:case_statement, expr]
278
+ when "pipeline"
279
+ # Pipelines identified by command names in order
280
+ commands = extract_pipeline_commands(node)
281
+ [:pipeline, commands]
282
+ when "comment"
283
+ # Comments identified by their content
284
+ [:comment, node_text(node).strip]
285
+ else
286
+ # Generic fallback - type and first few chars of content
287
+ content_preview = node_text(node).slice(0, 50).strip
288
+ [node_type.to_sym, content_preview]
289
+ end
290
+ end
291
+
292
+ def extract_command_signature_context(node)
293
+ # Extract additional context like redirections
294
+ redirections = []
295
+ node.each do |child|
296
+ if child.type.to_s.include?("redirect")
297
+ redirections << child.type.to_s
298
+ end
299
+ end
300
+ redirections.empty? ? nil : redirections.sort
301
+ end
302
+
303
+ def extract_condition_pattern(node)
304
+ # Try to extract the test/condition from if/while statements
305
+ # Look for test_command, compound_statement, etc.
306
+ node.each do |child|
307
+ if %w[test_command bracket_command].include?(child.type.to_s)
308
+ return node_text(child).slice(0, 100).strip
309
+ end
310
+ end
311
+ nil
312
+ end
313
+
314
+ def extract_loop_variable(node)
315
+ # Extract the loop variable from for statements
316
+ var_node = node.each.find { |child| child.type.to_s == "variable_name" }
317
+ node_text(var_node) if var_node
318
+ end
319
+
320
+ def extract_case_expression(node)
321
+ # Extract the expression being matched in a case statement
322
+ node.each do |child|
323
+ return node_text(child).slice(0, 50).strip if child.type.to_s == "word" || child.type.to_s == "variable_name"
324
+ end
325
+ nil
326
+ end
327
+
328
+ def extract_pipeline_commands(node)
329
+ # Extract command names from a pipeline
330
+ commands = []
331
+ node.each do |child|
332
+ if child.type.to_s == "command"
333
+ wrapper = NodeWrapper.new(child, lines: @lines, source: @source)
334
+ cmd_name = wrapper.command_name
335
+ commands << cmd_name if cmd_name
336
+ end
337
+ end
338
+ commands
339
+ end
340
+ end
341
+ end
342
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bash
4
+ module Merge
5
+ # Main entry point for intelligent Bash script merging.
6
+ # SmartMerger orchestrates the merge process using FileAnalysis,
7
+ # ConflictResolver, and MergeResult to merge two Bash scripts intelligently.
8
+ #
9
+ # @example Basic merge (destination customizations preserved)
10
+ # merger = SmartMerger.new(template_bash, dest_bash)
11
+ # result = merger.merge
12
+ # File.write("output.sh", result)
13
+ #
14
+ # @example Template updates win
15
+ # merger = SmartMerger.new(
16
+ # template_bash,
17
+ # dest_bash,
18
+ # preference: :template,
19
+ # add_template_only_nodes: true
20
+ # )
21
+ # result = merger.merge
22
+ #
23
+ # @example With custom signature generator
24
+ # sig_gen = ->(node) {
25
+ # if node.is_a?(NodeWrapper) && node.function_definition? && node.function_name == "main"
26
+ # [:special_main]
27
+ # else
28
+ # node # Fall through to default
29
+ # end
30
+ # }
31
+ # merger = SmartMerger.new(template, dest, signature_generator: sig_gen)
32
+ #
33
+ # @example With node_typing for per-node-type preferences
34
+ # merger = SmartMerger.new(template, dest,
35
+ # node_typing: { "function_definition" => ->(n) { NodeTyping.with_merge_type(n, :func) } },
36
+ # preference: { default: :destination, func: :template })
37
+ class SmartMerger < ::Ast::Merge::SmartMergerBase
38
+ # Creates a new SmartMerger for intelligent Bash script merging.
39
+ #
40
+ # @param template_content [String] Template Bash source code
41
+ # @param dest_content [String] Destination Bash source code
42
+ # @param signature_generator [Proc, nil] Custom signature generator
43
+ # @param preference [Symbol, Hash] :destination, :template, or per-type Hash
44
+ # @param add_template_only_nodes [Boolean] Whether to add nodes only in template
45
+ # @param freeze_token [String] Token for freeze block markers
46
+ # @param match_refiner [#call, nil] Match refiner for fuzzy matching
47
+ # @param regions [Array<Hash>, nil] Region configurations for nested merging
48
+ # @param region_placeholder [String, nil] Custom placeholder for regions
49
+ # @param node_typing [Hash{Symbol,String => #call}, nil] Node typing configuration
50
+ #
51
+ # @note To specify a custom parser path, use the TREE_SITTER_BASH_PATH environment
52
+ # variable. This is handled by tree_haver's GrammarFinder.
53
+ # @param options [Hash] Additional options for forward compatibility
54
+ #
55
+ # @raise [TemplateParseError] If template has syntax errors
56
+ # @raise [DestinationParseError] If destination has syntax errors
57
+ def initialize(
58
+ template_content,
59
+ dest_content,
60
+ signature_generator: nil,
61
+ preference: :destination,
62
+ add_template_only_nodes: false,
63
+ freeze_token: nil,
64
+ match_refiner: nil,
65
+ regions: nil,
66
+ region_placeholder: nil,
67
+ node_typing: nil,
68
+ **options
69
+ )
70
+ super(
71
+ template_content,
72
+ dest_content,
73
+ signature_generator: signature_generator,
74
+ preference: preference,
75
+ add_template_only_nodes: add_template_only_nodes,
76
+ freeze_token: freeze_token,
77
+ match_refiner: match_refiner,
78
+ regions: regions,
79
+ region_placeholder: region_placeholder,
80
+ node_typing: node_typing,
81
+ **options
82
+ )
83
+ end
84
+
85
+ # Perform the merge and return the result as a Bash string.
86
+ #
87
+ # @return [String] Merged Bash content
88
+ def merge
89
+ merge_result.to_bash
90
+ end
91
+
92
+ # Perform the merge and return detailed results including debug info.
93
+ #
94
+ # @return [Hash] Hash containing :content, :statistics, :decisions
95
+ def merge_with_debug
96
+ content = merge
97
+
98
+ {
99
+ content: content,
100
+ statistics: @result.statistics,
101
+ decisions: @result.decision_summary,
102
+ template_analysis: {
103
+ valid: @template_analysis.valid?,
104
+ nodes: @template_analysis.nodes.size,
105
+ freeze_blocks: @template_analysis.freeze_blocks.size,
106
+ },
107
+ dest_analysis: {
108
+ valid: @dest_analysis.valid?,
109
+ nodes: @dest_analysis.nodes.size,
110
+ freeze_blocks: @dest_analysis.freeze_blocks.size,
111
+ },
112
+ }
113
+ end
114
+
115
+ # Check if both files were parsed successfully.
116
+ #
117
+ # @return [Boolean]
118
+ def valid?
119
+ @template_analysis.valid? && @dest_analysis.valid?
120
+ end
121
+
122
+ # Get any parse errors from template or destination.
123
+ #
124
+ # @return [Array] Array of errors
125
+ def errors
126
+ errors = []
127
+ errors.concat(@template_analysis.errors.map { |e| {source: :template, error: e} })
128
+ errors.concat(@dest_analysis.errors.map { |e| {source: :destination, error: e} })
129
+ errors
130
+ end
131
+
132
+ protected
133
+
134
+ # @return [Class] The analysis class for Bash files
135
+ def analysis_class
136
+ FileAnalysis
137
+ end
138
+
139
+ # @return [String] The default freeze token
140
+ def default_freeze_token
141
+ "bash-merge"
142
+ end
143
+
144
+ # @return [Class] The resolver class for Bash files
145
+ def resolver_class
146
+ ConflictResolver
147
+ end
148
+
149
+ # @return [Class] The result class for Bash files
150
+ def result_class
151
+ MergeResult
152
+ end
153
+
154
+ # @return [Class] The template parse error class for Bash
155
+ def template_parse_error_class
156
+ TemplateParseError
157
+ end
158
+
159
+ # @return [Class] The destination parse error class for Bash
160
+ def destination_parse_error_class
161
+ DestinationParseError
162
+ end
163
+
164
+ # Perform the Bash-specific merge
165
+ #
166
+ # @return [MergeResult] The merge result
167
+ def perform_merge
168
+ @resolver.resolve(@result)
169
+ @result
170
+ end
171
+
172
+ # Build the resolver with Bash-specific options
173
+ def build_resolver
174
+ ConflictResolver.new(
175
+ @template_analysis,
176
+ @dest_analysis,
177
+ preference: @preference,
178
+ add_template_only_nodes: @add_template_only_nodes,
179
+ )
180
+ end
181
+
182
+ # Build the result (no-arg constructor for Bash)
183
+ def build_result
184
+ MergeResult.new
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bash
4
+ module Merge
5
+ # Version information for Bash::Merge
6
+ module Version
7
+ # Current version of the bash-merge gem
8
+ VERSION = "1.0.0"
9
+ end
10
+ VERSION = Version::VERSION # traditional location
11
+ end
12
+ end
data/lib/bash/merge.rb ADDED
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ # External gems
4
+ # TreeHaver provides a unified cross-Ruby interface to tree-sitter
5
+ require "tree_haver"
6
+
7
+ # BACKEND COMPATIBILITY for Bash:
8
+ # - FFI: Most portable and reliable with bash grammar (recommended)
9
+ # - MRI: Has ABI incompatibility with bash grammar
10
+ # - Rust: Has version mismatch with bash grammar
11
+ #
12
+ # Set TREE_HAVER_BACKEND=ffi (or mri/rust) to control backend selection.
13
+ # When MRI loads a grammar first, FFI gets incompatible pointers (symbol conflict).
14
+ # MRI statically links tree-sitter, FFI dynamically links libtree-sitter.so.
15
+
16
+ # Register tree-sitter bash grammar
17
+ bash_finder = TreeHaver::GrammarFinder.new(:bash)
18
+ bash_available = bash_finder.available?
19
+ bash_finder.register! if bash_available
20
+
21
+ # Only warn if the grammar file is actually missing (not just runtime unavailable)
22
+ # When the runtime isn't available, tree-sitter backends just won't be used,
23
+ # which is expected behavior - no need to warn the user.
24
+ unless bash_available
25
+ grammar_path = bash_finder.find_library_path
26
+ unless grammar_path
27
+ warn "WARNING: Bash grammar not available. #{bash_finder.not_found_message}"
28
+ end
29
+ end
30
+
31
+ require "version_gem"
32
+ require "set"
33
+
34
+ # Shared merge infrastructure
35
+ require "ast/merge"
36
+
37
+ # This gem
38
+ require_relative "merge/version"
39
+
40
+ # Bash::Merge provides a generic Bash script smart merge system using tree-sitter AST analysis.
41
+ # It intelligently merges template and destination Bash scripts by identifying matching
42
+ # statements and resolving differences using structural signatures.
43
+ #
44
+ # @example Basic usage
45
+ # template = File.read("template.sh")
46
+ # destination = File.read("destination.sh")
47
+ # merger = Bash::Merge::SmartMerger.new(template, destination)
48
+ # result = merger.merge
49
+ #
50
+ # @example With debug information
51
+ # merger = Bash::Merge::SmartMerger.new(template, destination)
52
+ # debug_result = merger.merge_with_debug
53
+ # puts debug_result[:content]
54
+ # puts debug_result[:statistics]
55
+ module Bash
56
+ # Smart merge system for Bash scripts using tree-sitter AST analysis.
57
+ # Provides intelligent merging by understanding Bash structure
58
+ # rather than treating files as plain text.
59
+ #
60
+ # @see SmartMerger Main entry point for merge operations
61
+ # @see FileAnalysis Analyzes Bash structure
62
+ # @see ConflictResolver Resolves content conflicts
63
+ module Merge
64
+ # Base error class for Bash::Merge
65
+ # Inherits from Ast::Merge::Error for consistency across merge gems.
66
+ class Error < Ast::Merge::Error; end
67
+
68
+ # Raised when a Bash script has parsing errors.
69
+ # Inherits from Ast::Merge::ParseError for consistency across merge gems.
70
+ #
71
+ # @example Handling parse errors
72
+ # begin
73
+ # analysis = FileAnalysis.new(bash_content)
74
+ # rescue ParseError => e
75
+ # puts "Bash syntax error: #{e.message}"
76
+ # e.errors.each { |error| puts " #{error}" }
77
+ # end
78
+ class ParseError < Ast::Merge::ParseError
79
+ # @param message [String, nil] Error message (auto-generated if nil)
80
+ # @param content [String, nil] The Bash source that failed to parse
81
+ # @param errors [Array] Parse errors from tree-sitter
82
+ def initialize(message = nil, content: nil, errors: [])
83
+ super(message, errors: errors, content: content)
84
+ end
85
+ end
86
+
87
+ # Raised when the template file has syntax errors.
88
+ #
89
+ # @example Handling template parse errors
90
+ # begin
91
+ # merger = SmartMerger.new(template, destination)
92
+ # result = merger.merge
93
+ # rescue TemplateParseError => e
94
+ # puts "Template syntax error: #{e.message}"
95
+ # e.errors.each do |error|
96
+ # puts " #{error.message}"
97
+ # end
98
+ # end
99
+ class TemplateParseError < ParseError; end
100
+
101
+ # Raised when the destination file has syntax errors.
102
+ #
103
+ # @example Handling destination parse errors
104
+ # begin
105
+ # merger = SmartMerger.new(template, destination)
106
+ # result = merger.merge
107
+ # rescue DestinationParseError => e
108
+ # puts "Destination syntax error: #{e.message}"
109
+ # e.errors.each do |error|
110
+ # puts " #{error.message}"
111
+ # end
112
+ # end
113
+ class DestinationParseError < ParseError; end
114
+
115
+ autoload :CommentTracker, "bash/merge/comment_tracker"
116
+ autoload :DebugLogger, "bash/merge/debug_logger"
117
+ autoload :Emitter, "bash/merge/emitter"
118
+ autoload :FreezeNode, "bash/merge/freeze_node"
119
+ autoload :FileAnalysis, "bash/merge/file_analysis"
120
+ autoload :MergeResult, "bash/merge/merge_result"
121
+ autoload :NodeWrapper, "bash/merge/node_wrapper"
122
+ autoload :ConflictResolver, "bash/merge/conflict_resolver"
123
+ autoload :SmartMerger, "bash/merge/smart_merger"
124
+ end
125
+ end
126
+
127
+ Bash::Merge::Version.class_eval do
128
+ extend VersionGem::Basic
129
+ end
data/lib/bash-merge.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # For technical reasons, if we move to Zeitwerk, this cannot be require_relative.
4
+ # See: https://github.com/fxn/zeitwerk#for_gem_extension
5
+ # Hook for other libraries to load this library (e.g. via bundler)
6
+ require "bash/merge"