config_files 0.1.6 → 0.2.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +94 -0
- data/.gitignore +2 -3
- data/.rubocop.yml +81 -0
- data/CHANGELOG.md +154 -0
- data/Gemfile +12 -1
- data/MULTI_RUBY_SETUP.md +158 -0
- data/README.md +246 -23
- data/Rakefile +26 -3
- data/TESTING.md +226 -0
- data/config_files.gemspec +12 -9
- data/docker/Dockerfile.test +32 -0
- data/lib/config_files/file_factory.rb +7 -3
- data/lib/config_files/loader_factory.rb +20 -11
- data/lib/config_files/loaders/base_parser.rb +133 -0
- data/lib/config_files/loaders/conf.rb +61 -0
- data/lib/config_files/loaders/default.rb +3 -1
- data/lib/config_files/loaders/ini.rb +48 -0
- data/lib/config_files/loaders/json.rb +3 -1
- data/lib/config_files/loaders/xml.rb +67 -0
- data/lib/config_files/loaders/yaml.rb +2 -0
- data/lib/config_files/loaders.rb +6 -1
- data/lib/config_files/version.rb +3 -1
- data/lib/config_files.rb +34 -18
- data/lib/meta.rb +3 -1
- data/scripts/install_rubies_asdf.sh +187 -0
- data/scripts/test_docker.sh +91 -0
- data/scripts/test_multiple_rubies.sh +290 -0
- data/test/comprehensive_multi_directory_test.rb +168 -0
- data/test/config/dummy.json +10 -0
- data/test/config/dummy.yml +6 -0
- data/test/config_files_test.rb +10 -8
- data/test/etc/dummy.conf +14 -2
- data/test/etc/dummy.ini +12 -0
- data/test/etc/dummy.xml +13 -0
- data/test/etc/dummy.yml +2 -2
- data/test/loader_factory_test.rb +10 -10
- data/test/loaders_test.rb +362 -0
- data/test/local/dummy.json +10 -0
- data/test/local/dummy.yml +6 -0
- data/test/mixed_format_test.rb +152 -0
- data/test/multi_directory_test.rb +126 -0
- data/test/test_helper.rb +3 -0
- metadata +49 -26
- data/Gemfile.lock +0 -34
@@ -0,0 +1,133 @@
|
|
1
|
+
module ConfigFiles
|
2
|
+
module Loaders
|
3
|
+
# Base class providing common parsing functionality for configuration file loaders
|
4
|
+
class BaseParser
|
5
|
+
class << self
|
6
|
+
def call(file_name)
|
7
|
+
content = File.read(file_name)
|
8
|
+
parse(content)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
# Template method to be implemented by subclasses
|
14
|
+
def parse(content)
|
15
|
+
raise NotImplementedError, "Subclasses must implement #parse"
|
16
|
+
end
|
17
|
+
|
18
|
+
# Common value parsing logic shared across parsers
|
19
|
+
def parse_value(value)
|
20
|
+
return nil if value.nil?
|
21
|
+
return value if value.empty?
|
22
|
+
|
23
|
+
# Try to parse as boolean
|
24
|
+
boolean_result = boolean_value?(value)
|
25
|
+
return boolean_result unless boolean_result.nil?
|
26
|
+
|
27
|
+
# Try to parse as number
|
28
|
+
number_value = parse_number(value)
|
29
|
+
return number_value unless number_value.nil?
|
30
|
+
|
31
|
+
# Return as string
|
32
|
+
value
|
33
|
+
end
|
34
|
+
|
35
|
+
# Parse boolean values with common representations
|
36
|
+
def boolean_value?(value)
|
37
|
+
case value.downcase
|
38
|
+
when 'true', 'yes', 'on', '1'
|
39
|
+
true
|
40
|
+
when 'false', 'no', 'off', '0'
|
41
|
+
false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Parse numeric values (integers and floats)
|
46
|
+
def parse_number(value)
|
47
|
+
return value.to_i if integer?(value)
|
48
|
+
return value.to_f if float?(value)
|
49
|
+
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
# Check if string represents an integer
|
54
|
+
def integer?(value)
|
55
|
+
value.match?(/^\d+$/)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Check if string represents a float
|
59
|
+
def float?(value)
|
60
|
+
value.match?(/^\d+\.\d+$/)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Remove surrounding quotes from a value
|
64
|
+
def unquote(value)
|
65
|
+
value.gsub(/^["']|["']$/, '')
|
66
|
+
end
|
67
|
+
|
68
|
+
# Check if line is a comment based on comment prefixes
|
69
|
+
def comment?(line, prefixes = ['#'])
|
70
|
+
prefixes.any? { |prefix| line.start_with?(prefix) }
|
71
|
+
end
|
72
|
+
|
73
|
+
# Check if line is empty or whitespace only
|
74
|
+
def empty_line?(line)
|
75
|
+
line.strip.empty?
|
76
|
+
end
|
77
|
+
|
78
|
+
# Skip processing for comments and empty lines
|
79
|
+
def skip_line?(line, comment_prefixes = ['#'])
|
80
|
+
empty_line?(line) || comment?(line, comment_prefixes)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Parse section header like [section_name]
|
84
|
+
def parse_section_header(line)
|
85
|
+
match = line.match(/^\[(.+)\]$/)
|
86
|
+
match ? match[1].strip : nil
|
87
|
+
end
|
88
|
+
|
89
|
+
# Check if line is a section header
|
90
|
+
def section_header?(line)
|
91
|
+
line.match?(/^\[.+\]$/)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Set nested value using dot notation (e.g., "server.host" -> {"server" => {"host" => value}})
|
95
|
+
def set_nested_value(hash, key_path, value, section = nil)
|
96
|
+
keys = key_path.split('.')
|
97
|
+
target = section ? (hash[section] ||= {}) : hash
|
98
|
+
|
99
|
+
# Navigate to the nested location
|
100
|
+
keys[0..-2].each do |key|
|
101
|
+
target = (target[key] ||= {})
|
102
|
+
end
|
103
|
+
|
104
|
+
# Set the final value
|
105
|
+
target[keys.last] = value
|
106
|
+
end
|
107
|
+
|
108
|
+
# Check if key contains dot notation for nesting
|
109
|
+
def nested_key?(key)
|
110
|
+
key.include?('.')
|
111
|
+
end
|
112
|
+
|
113
|
+
# Set value in hash, handling sections and nesting
|
114
|
+
def set_value(hash, key, value, current_section = nil)
|
115
|
+
parsed_value = parse_value(value)
|
116
|
+
|
117
|
+
if nested_key?(key)
|
118
|
+
set_nested_value(hash, key, parsed_value, current_section)
|
119
|
+
elsif current_section
|
120
|
+
hash[current_section][key] = parsed_value
|
121
|
+
else
|
122
|
+
hash[key] = parsed_value
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Initialize section in hash if it doesn't exist
|
127
|
+
def ensure_section(hash, section_name)
|
128
|
+
hash[section_name] ||= {}
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require_relative 'base_parser'
|
2
|
+
|
3
|
+
module ConfigFiles
|
4
|
+
module Loaders
|
5
|
+
class Conf < BaseParser
|
6
|
+
# Supported key-value separators in order of preference
|
7
|
+
SEPARATORS = ['=', ':', ' '].freeze
|
8
|
+
|
9
|
+
class << self
|
10
|
+
private
|
11
|
+
|
12
|
+
def parse(content)
|
13
|
+
result = {}
|
14
|
+
current_section = nil
|
15
|
+
|
16
|
+
content.each_line do |line|
|
17
|
+
line = line.strip
|
18
|
+
next if skip_line?(line)
|
19
|
+
|
20
|
+
if section_header?(line)
|
21
|
+
current_section = parse_section_header(line)
|
22
|
+
ensure_section(result, current_section)
|
23
|
+
next
|
24
|
+
end
|
25
|
+
|
26
|
+
key, value = parse_conf_line(line)
|
27
|
+
next unless key && value
|
28
|
+
|
29
|
+
set_value(result, key, value, current_section)
|
30
|
+
end
|
31
|
+
|
32
|
+
result
|
33
|
+
end
|
34
|
+
|
35
|
+
# Parse CONF format line supporting multiple syntaxes:
|
36
|
+
# key=value, key: value, key value (space-separated)
|
37
|
+
def parse_conf_line(line)
|
38
|
+
key, value = extract_key_value_pair(line)
|
39
|
+
return [nil, nil] unless key && value
|
40
|
+
|
41
|
+
key = key.strip
|
42
|
+
value = unquote(value.strip)
|
43
|
+
|
44
|
+
[key, value]
|
45
|
+
end
|
46
|
+
|
47
|
+
# Extract key-value pair from line using different separators
|
48
|
+
def extract_key_value_pair(line)
|
49
|
+
SEPARATORS.each do |separator|
|
50
|
+
if line.include?(separator)
|
51
|
+
parts = line.split(separator, 2)
|
52
|
+
return parts if parts.length == 2
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
[nil, nil]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative 'base_parser'
|
2
|
+
|
3
|
+
module ConfigFiles
|
4
|
+
module Loaders
|
5
|
+
class Ini < BaseParser
|
6
|
+
# INI files support both # and ; for comments
|
7
|
+
COMMENT_PREFIXES = ['#', ';'].freeze
|
8
|
+
|
9
|
+
class << self
|
10
|
+
private
|
11
|
+
|
12
|
+
def parse(content)
|
13
|
+
result = {}
|
14
|
+
current_section = nil
|
15
|
+
|
16
|
+
content.each_line do |line|
|
17
|
+
line = line.strip
|
18
|
+
next if skip_line?(line, COMMENT_PREFIXES)
|
19
|
+
|
20
|
+
if section_header?(line)
|
21
|
+
current_section = parse_section_header(line)
|
22
|
+
ensure_section(result, current_section)
|
23
|
+
next
|
24
|
+
end
|
25
|
+
|
26
|
+
key, value = parse_ini_line(line)
|
27
|
+
next unless key && value
|
28
|
+
|
29
|
+
set_value(result, key, value, current_section)
|
30
|
+
end
|
31
|
+
|
32
|
+
result
|
33
|
+
end
|
34
|
+
|
35
|
+
# Parse INI format line (key=value only)
|
36
|
+
def parse_ini_line(line)
|
37
|
+
return [nil, nil] unless line.include?('=')
|
38
|
+
|
39
|
+
key, value = line.split('=', 2)
|
40
|
+
key = key.strip
|
41
|
+
value = unquote(value.strip)
|
42
|
+
|
43
|
+
[key, value]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'json'
|
2
4
|
|
3
5
|
module ConfigFiles
|
@@ -5,7 +7,7 @@ module ConfigFiles
|
|
5
7
|
class Json
|
6
8
|
class << self
|
7
9
|
def call(file_name, object_class: ::Hash)
|
8
|
-
::JSON.
|
10
|
+
::JSON.parse(::File.read(file_name), { object_class: object_class, quirks_mode: true })
|
9
11
|
end
|
10
12
|
end
|
11
13
|
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
require_relative 'base_parser'
|
3
|
+
|
4
|
+
module ConfigFiles
|
5
|
+
module Loaders
|
6
|
+
class Xml < BaseParser
|
7
|
+
class << self
|
8
|
+
private
|
9
|
+
|
10
|
+
def parse(content)
|
11
|
+
doc = REXML::Document.new(content)
|
12
|
+
doc.root ? parse_element(doc.root) : {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse_element(element)
|
16
|
+
result = parse_attributes(element)
|
17
|
+
parse_child_elements(element, result)
|
18
|
+
|
19
|
+
# Return text content if element has no children or attributes
|
20
|
+
return parse_text_content(element) if result.empty? && element.has_text?
|
21
|
+
|
22
|
+
result
|
23
|
+
end
|
24
|
+
|
25
|
+
# Parse element attributes, prefixing with @
|
26
|
+
def parse_attributes(element)
|
27
|
+
result = {}
|
28
|
+
element.attributes.each do |name, value|
|
29
|
+
result["@#{name}"] = parse_value(value)
|
30
|
+
end
|
31
|
+
result
|
32
|
+
end
|
33
|
+
|
34
|
+
# Parse child elements, handling duplicates and nesting
|
35
|
+
def parse_child_elements(element, result)
|
36
|
+
element.elements.each do |child|
|
37
|
+
key = child.name
|
38
|
+
child_value = child.has_elements? ? parse_element(child) : parse_leaf_element(child)
|
39
|
+
|
40
|
+
if result.key?(key)
|
41
|
+
result[key] = convert_to_array(result[key])
|
42
|
+
result[key] << child_value
|
43
|
+
else
|
44
|
+
result[key] = child_value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Parse leaf element (no child elements)
|
50
|
+
def parse_leaf_element(element)
|
51
|
+
text = element.text
|
52
|
+
text ? parse_value(text.strip) : nil
|
53
|
+
end
|
54
|
+
|
55
|
+
# Parse text content of element
|
56
|
+
def parse_text_content(element)
|
57
|
+
parse_value(element.text.strip)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Convert single value to array for handling multiple elements with same name
|
61
|
+
def convert_to_array(value)
|
62
|
+
value.is_a?(Array) ? value : [value]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/config_files/loaders.rb
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'loaders/json'
|
2
4
|
require_relative 'loaders/yaml'
|
3
|
-
require_relative 'loaders/
|
5
|
+
require_relative 'loaders/conf'
|
6
|
+
require_relative 'loaders/ini'
|
7
|
+
require_relative 'loaders/xml'
|
8
|
+
require_relative 'loaders/default'
|
data/lib/config_files/version.rb
CHANGED
data/lib/config_files.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'config_files/file_factory'
|
2
4
|
require 'config_files/loader_factory'
|
3
5
|
require 'config_files/loaders'
|
4
6
|
require 'config_files/version'
|
7
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
5
8
|
require 'active_support/core_ext/hash/deep_merge'
|
6
9
|
require 'active_support/core_ext/object/blank'
|
7
10
|
|
@@ -9,8 +12,8 @@ require 'meta'
|
|
9
12
|
require 'yaml'
|
10
13
|
|
11
14
|
class NoDirectoryEntry < NoMethodError; end
|
12
|
-
module ConfigFiles
|
13
15
|
|
16
|
+
module ConfigFiles
|
14
17
|
class << self
|
15
18
|
def included(base)
|
16
19
|
base.class_eval do
|
@@ -25,7 +28,7 @@ module ConfigFiles
|
|
25
28
|
|
26
29
|
def self.extended(base)
|
27
30
|
base.instance_eval do
|
28
|
-
self.directories=default_directories
|
31
|
+
self.directories = default_directories
|
29
32
|
end
|
30
33
|
end
|
31
34
|
|
@@ -38,21 +41,23 @@ module ConfigFiles
|
|
38
41
|
end
|
39
42
|
|
40
43
|
def default_directories
|
41
|
-
{ :
|
44
|
+
{ etc: ['config', 'etc', '/etc'] }
|
42
45
|
end
|
43
46
|
|
44
47
|
def config_directories(*arr)
|
45
|
-
self.directories||=default_directories
|
48
|
+
self.directories ||= default_directories
|
46
49
|
arr.each do |directory_list|
|
47
50
|
directory_list.each do |key, value|
|
48
|
-
self.directories[key]=value.map { |dir| ::File.expand_path(dir) }
|
51
|
+
self.directories[key] = value.map { |dir| ::File.expand_path(dir) }
|
49
52
|
meta_def("#{key}_dir") { @directories[key] }
|
50
53
|
end
|
51
54
|
end
|
52
55
|
end
|
53
56
|
|
54
57
|
def merged_hash(file)
|
55
|
-
|
58
|
+
all_config_files(file).inject(::HashWithIndifferentAccess.new) do |master, file|
|
59
|
+
master.deep_merge(FileFactory.call(file))
|
60
|
+
end
|
56
61
|
end
|
57
62
|
|
58
63
|
def build_combined(file)
|
@@ -61,7 +66,7 @@ module ConfigFiles
|
|
61
66
|
|
62
67
|
def static_config_files(*arr)
|
63
68
|
arr.each do |file|
|
64
|
-
content=build_combined(file)
|
69
|
+
content = build_combined(file)
|
65
70
|
meta_def(file) { content }
|
66
71
|
end
|
67
72
|
end
|
@@ -72,27 +77,38 @@ module ConfigFiles
|
|
72
77
|
end
|
73
78
|
end
|
74
79
|
|
75
|
-
|
80
|
+
alias config_files dynamic_config_files
|
76
81
|
|
77
82
|
private
|
83
|
+
|
78
84
|
def directory_listing(directory, file)
|
79
|
-
::Dir.glob(::File.join(directory, "#{file}.*"))
|
85
|
+
::Dir.glob(::File.join(directory, "#{file}.*")).sort
|
80
86
|
end
|
81
87
|
|
82
|
-
def first_directory(file, key=config_key)
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
raise NoDirectoryEntry, "Unable to find #{key} in #{self.directories}"
|
87
|
-
end
|
88
|
+
def first_directory(file, key = config_key)
|
89
|
+
self.directories[key]&.detect { |directory| directory_listing(directory, file).presence } || ''
|
90
|
+
rescue NoMethodError
|
91
|
+
raise NoDirectoryEntry, "Unable to find #{key} in #{self.directories}"
|
88
92
|
end
|
89
93
|
|
90
|
-
def files(file, key=config_key)
|
94
|
+
def files(file, key = config_key)
|
91
95
|
directory_listing(first_directory(file, key), file)
|
92
96
|
end
|
93
97
|
|
94
|
-
def
|
95
|
-
|
98
|
+
def all_config_files(file, key = config_key)
|
99
|
+
return [] unless self.directories && self.directories[key]
|
100
|
+
|
101
|
+
# Collect files by directory, maintaining alphabetical order within each directory
|
102
|
+
files_by_directory = []
|
103
|
+
self.directories[key].each do |directory|
|
104
|
+
if ::File.directory?(directory)
|
105
|
+
directory_files = directory_listing(directory, file)
|
106
|
+
files_by_directory << directory_files if directory_files.any?
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Reverse directory order but keep file order within each directory
|
111
|
+
files_by_directory.reverse.flatten
|
96
112
|
end
|
97
113
|
end
|
98
114
|
end
|
data/lib/meta.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# @author Paul McKibbin based on meta definitions by _why_the_lucky_stiff_
|
2
4
|
module Meta
|
3
5
|
def meta_self
|
@@ -6,7 +8,7 @@ module Meta
|
|
6
8
|
end
|
7
9
|
end
|
8
10
|
|
9
|
-
def meta_def
|
11
|
+
def meta_def(name, &blk)
|
10
12
|
meta_self.instance_eval { define_method name, &blk }
|
11
13
|
end
|
12
14
|
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
# Script to install all Ruby versions needed for testing using asdf
|
4
|
+
# This is a specialized script for asdf users
|
5
|
+
|
6
|
+
set -e
|
7
|
+
|
8
|
+
# Colors for output
|
9
|
+
RED='\033[0;31m'
|
10
|
+
GREEN='\033[0;32m'
|
11
|
+
YELLOW='\033[1;33m'
|
12
|
+
BLUE='\033[0;34m'
|
13
|
+
NC='\033[0m' # No Color
|
14
|
+
|
15
|
+
# Ruby versions to install
|
16
|
+
RUBY_VERSIONS=("2.7.8" "3.0.6" "3.1.4" "3.2.2" "3.3.0" "3.4.1")
|
17
|
+
|
18
|
+
echo -e "${BLUE}ConfigFiles Ruby Installation Script (asdf)${NC}"
|
19
|
+
echo -e "${BLUE}===========================================${NC}"
|
20
|
+
|
21
|
+
# Function to check if asdf is installed and configured
|
22
|
+
check_asdf() {
|
23
|
+
if ! command -v asdf >/dev/null 2>&1; then
|
24
|
+
echo -e "${RED}asdf not found. Please install asdf first.${NC}"
|
25
|
+
echo -e "${BLUE}Installation instructions:${NC}"
|
26
|
+
echo -e " macOS: brew install asdf"
|
27
|
+
echo -e " Linux: git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.14.0"
|
28
|
+
echo -e " More info: https://asdf-vm.com/guide/getting-started.html"
|
29
|
+
exit 1
|
30
|
+
fi
|
31
|
+
|
32
|
+
echo -e "${GREEN}✓ asdf found${NC}"
|
33
|
+
|
34
|
+
# Check if ruby plugin is installed
|
35
|
+
if ! asdf plugin list | grep -q "^ruby$"; then
|
36
|
+
echo -e "${YELLOW}Installing Ruby plugin...${NC}"
|
37
|
+
if asdf plugin add ruby https://github.com/asdf-vm/asdf-ruby.git; then
|
38
|
+
echo -e "${GREEN}✓ Ruby plugin installed${NC}"
|
39
|
+
else
|
40
|
+
echo -e "${RED}✗ Failed to install Ruby plugin${NC}"
|
41
|
+
exit 1
|
42
|
+
fi
|
43
|
+
else
|
44
|
+
echo -e "${GREEN}✓ Ruby plugin already installed${NC}"
|
45
|
+
fi
|
46
|
+
}
|
47
|
+
|
48
|
+
# Function to check if Ruby version is installed
|
49
|
+
check_ruby_version() {
|
50
|
+
local version=$1
|
51
|
+
asdf list ruby 2>/dev/null | grep -q "^\s*${version}$"
|
52
|
+
}
|
53
|
+
|
54
|
+
# Function to install Ruby version
|
55
|
+
install_ruby_version() {
|
56
|
+
local version=$1
|
57
|
+
echo -e "${YELLOW}Installing Ruby ${version}...${NC}"
|
58
|
+
|
59
|
+
# Show progress
|
60
|
+
if asdf install ruby "$version"; then
|
61
|
+
echo -e "${GREEN}✓ Ruby ${version} installed successfully${NC}"
|
62
|
+
return 0
|
63
|
+
else
|
64
|
+
echo -e "${RED}✗ Failed to install Ruby ${version}${NC}"
|
65
|
+
return 1
|
66
|
+
fi
|
67
|
+
}
|
68
|
+
|
69
|
+
# Function to show system requirements
|
70
|
+
show_requirements() {
|
71
|
+
echo -e "\n${BLUE}System Requirements:${NC}"
|
72
|
+
echo -e "Before installing Ruby versions, make sure you have the required dependencies:"
|
73
|
+
echo
|
74
|
+
|
75
|
+
if [[ "$OSTYPE" == "darwin"* ]]; then
|
76
|
+
echo -e "${YELLOW}macOS:${NC}"
|
77
|
+
echo -e " brew install openssl readline sqlite3 xz zlib"
|
78
|
+
echo -e " xcode-select --install"
|
79
|
+
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
80
|
+
echo -e "${YELLOW}Ubuntu/Debian:${NC}"
|
81
|
+
echo -e " sudo apt-get install -y build-essential libssl-dev libreadline-dev zlib1g-dev"
|
82
|
+
echo -e " sudo apt-get install -y libsqlite3-dev libxml2-dev libxslt1-dev libcurl4-openssl-dev"
|
83
|
+
echo -e " sudo apt-get install -y libffi-dev libyaml-dev"
|
84
|
+
echo
|
85
|
+
echo -e "${YELLOW}CentOS/RHEL/Fedora:${NC}"
|
86
|
+
echo -e " sudo yum groupinstall -y 'Development Tools'"
|
87
|
+
echo -e " sudo yum install -y openssl-devel readline-devel zlib-devel sqlite-devel"
|
88
|
+
fi
|
89
|
+
|
90
|
+
echo
|
91
|
+
read -p "Have you installed the required dependencies? (y/N): " -n 1 -r
|
92
|
+
echo
|
93
|
+
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
94
|
+
echo -e "${YELLOW}Please install the dependencies first, then run this script again.${NC}"
|
95
|
+
exit 1
|
96
|
+
fi
|
97
|
+
}
|
98
|
+
|
99
|
+
# Main execution
|
100
|
+
echo -e "\n${YELLOW}This script will install the following Ruby versions using asdf:${NC}"
|
101
|
+
for version in "${RUBY_VERSIONS[@]}"; do
|
102
|
+
echo -e " - Ruby ${version}"
|
103
|
+
done
|
104
|
+
|
105
|
+
echo
|
106
|
+
read -p "Continue? (y/N): " -n 1 -r
|
107
|
+
echo
|
108
|
+
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
109
|
+
echo -e "${YELLOW}Installation cancelled.${NC}"
|
110
|
+
exit 0
|
111
|
+
fi
|
112
|
+
|
113
|
+
# Check asdf installation
|
114
|
+
check_asdf
|
115
|
+
|
116
|
+
# Show system requirements
|
117
|
+
show_requirements
|
118
|
+
|
119
|
+
# Check current installations
|
120
|
+
echo -e "\n${BLUE}Checking current Ruby installations...${NC}"
|
121
|
+
already_installed=()
|
122
|
+
to_install=()
|
123
|
+
|
124
|
+
for ruby_version in "${RUBY_VERSIONS[@]}"; do
|
125
|
+
if check_ruby_version "$ruby_version"; then
|
126
|
+
echo -e "${GREEN}✓ Ruby ${ruby_version} is already installed${NC}"
|
127
|
+
already_installed+=("$ruby_version")
|
128
|
+
else
|
129
|
+
echo -e "${YELLOW}○ Ruby ${ruby_version} needs to be installed${NC}"
|
130
|
+
to_install+=("$ruby_version")
|
131
|
+
fi
|
132
|
+
done
|
133
|
+
|
134
|
+
if [ ${#to_install[@]} -eq 0 ]; then
|
135
|
+
echo -e "\n${GREEN}All Ruby versions are already installed! 🎉${NC}"
|
136
|
+
exit 0
|
137
|
+
fi
|
138
|
+
|
139
|
+
# Install missing versions
|
140
|
+
echo -e "\n${BLUE}Installing ${#to_install[@]} Ruby versions...${NC}"
|
141
|
+
failed_installations=()
|
142
|
+
successful_installations=()
|
143
|
+
|
144
|
+
for ruby_version in "${to_install[@]}"; do
|
145
|
+
echo -e "\n${YELLOW}[$(date '+%H:%M:%S')] Installing Ruby ${ruby_version}...${NC}"
|
146
|
+
|
147
|
+
if install_ruby_version "$ruby_version"; then
|
148
|
+
successful_installations+=("$ruby_version")
|
149
|
+
else
|
150
|
+
failed_installations+=("$ruby_version")
|
151
|
+
echo -e "${RED}Installation of Ruby ${ruby_version} failed. Continuing with others...${NC}"
|
152
|
+
fi
|
153
|
+
done
|
154
|
+
|
155
|
+
# Summary
|
156
|
+
echo -e "\n${BLUE}=== Installation Summary ===${NC}"
|
157
|
+
echo -e "Already installed: ${#already_installed[@]}"
|
158
|
+
echo -e "${GREEN}Successfully installed: ${#successful_installations[@]}${NC}"
|
159
|
+
echo -e "${RED}Failed installations: ${#failed_installations[@]}${NC}"
|
160
|
+
|
161
|
+
if [ ${#successful_installations[@]} -gt 0 ]; then
|
162
|
+
echo -e "\n${GREEN}Successfully installed:${NC}"
|
163
|
+
for version in "${successful_installations[@]}"; do
|
164
|
+
echo -e "${GREEN} ✓ Ruby ${version}${NC}"
|
165
|
+
done
|
166
|
+
fi
|
167
|
+
|
168
|
+
if [ ${#failed_installations[@]} -gt 0 ]; then
|
169
|
+
echo -e "\n${RED}Failed installations:${NC}"
|
170
|
+
for version in "${failed_installations[@]}"; do
|
171
|
+
echo -e "${RED} ✗ Ruby ${version}${NC}"
|
172
|
+
done
|
173
|
+
echo -e "\n${YELLOW}Troubleshooting tips:${NC}"
|
174
|
+
echo -e " 1. Make sure you have all system dependencies installed"
|
175
|
+
echo -e " 2. Check asdf ruby plugin: asdf plugin update ruby"
|
176
|
+
echo -e " 3. Try installing manually: asdf install ruby <version>"
|
177
|
+
echo -e " 4. Check logs in ~/.asdf/installs/ruby/<version>/install.log"
|
178
|
+
fi
|
179
|
+
|
180
|
+
# Show next steps
|
181
|
+
echo -e "\n${BLUE}Next Steps:${NC}"
|
182
|
+
echo -e " 1. Run the test suite: ./scripts/test_multiple_rubies.sh"
|
183
|
+
echo -e " 2. Set a global Ruby version: asdf global ruby <version>"
|
184
|
+
echo -e " 3. Set a local Ruby version: asdf local ruby <version>"
|
185
|
+
|
186
|
+
total_available=$((${#already_installed[@]} + ${#successful_installations[@]}))
|
187
|
+
echo -e "\n${GREEN}You now have ${total_available} Ruby versions available for testing! 🚀${NC}"
|