clamp 1.5.0 → 1.5.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f2a9c7d404e87023cc8694e18498df5719974cd11e58663df640c08a0f87c3e
4
- data.tar.gz: da2a65496e24eb9abc13eaddc370f31d3e5764a832c3d0a18015dd636e43dbfa
3
+ metadata.gz: ceef639e7f7761b30abc7be04ab5384ee2dc238ba472a12c808e2cdffc14e540
4
+ data.tar.gz: 7f97fc3d84a9fad89123fad6ed35dffeb37efc9e82815a44a6ca76ad134905ec
5
5
  SHA512:
6
- metadata.gz: 7318c3498b2e425ecd18395e4ad3d24109e130bac38f9f9406fba97b02b8b78d859374f5e1439a064dff1e4037b2cab2df8210a13a9ea54cac030df8b35e86c8
7
- data.tar.gz: 5f46c6724d06d2fa30425369a1867f0e2199e05997d796142af125dad5369a08c444f5a06fb15c2ad206b8e62afcb582fe1442c36fbe7ce994fa7d8400f71993
6
+ metadata.gz: 886c0eb9476e36c0c46e07812a412108f8575799f2a1b5588d0e6ed6feac5d7d8fe25ea27a1b8c4e244e691a62a9c502f0ecdb848eb56022cf48004f353d11ed
7
+ data.tar.gz: 18cc7167ae022ea13afd50b2367e47f5a9832e67d3754c683077125d4eaa99e75efba69b736fd327173ce9b2662f16be43bdbf53eea33f17b409703579f11b03
data/CHANGES.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.1 (2026-03-11)
4
+
5
+ * Fix shell completion scripts: required parameters, subcommand aliases, and option value handling.
6
+
3
7
  ## 1.5.0 (2026-03-03)
4
8
 
5
9
  * Add `--shell-completions` support.
@@ -19,22 +19,25 @@ module Clamp
19
19
  "",
20
20
  takes_value_function,
21
21
  "",
22
- completion_function,
22
+ param_count_function,
23
23
  "",
24
- "complete -F _#{function_name} #{@executable_name}"
24
+ canonical_function,
25
+ "",
26
+ main_function,
27
+ "",
28
+ "complete -F #{completion_function} #{@executable_name}"
25
29
  ].push("").join("\n")
26
30
  end
27
31
 
28
32
  private
29
33
 
30
- def function_name
31
- @executable_name.gsub(/[^a-zA-Z0-9_]/, "_")
34
+ def completion_function
35
+ "_clamp_complete_#{Completion.encode_name(@executable_name)}"
32
36
  end
33
37
 
34
- def completion_function
35
- fn = function_name
38
+ def main_function
36
39
  [
37
- "_#{fn}() {",
40
+ "#{completion_function}() {",
38
41
  " local cur prev",
39
42
  " if type _init_completion &>/dev/null; then",
40
43
  " _init_completion",
@@ -43,13 +46,21 @@ module Clamp
43
46
  ' prev="${COMP_WORDS[COMP_CWORD-1]}"',
44
47
  " fi",
45
48
  "",
46
- " local subcmd",
47
- " subcmd=$(__#{fn}_find_subcmd)",
48
- " if __#{fn}_takes_value \"$prev\" \"$subcmd\"; then",
49
+ " local subcmd_info subcmd params_remaining",
50
+ " subcmd_info=$(#{completion_function}_find_subcmd)",
51
+ ' subcmd="${subcmd_info%% *}"',
52
+ ' params_remaining="${subcmd_info##* }"',
53
+ " if #{completion_function}_takes_value \"$prev\" \"$subcmd\"; then",
49
54
  " return",
50
55
  " fi",
51
56
  "",
52
- completions_case("$subcmd"),
57
+ ' if [[ "$cur" == -* ]]; then',
58
+ options_case("$subcmd"),
59
+ " fi",
60
+ "",
61
+ ' if [ "$params_remaining" -eq 0 ]; then',
62
+ subcommands_case("$subcmd"),
63
+ " fi",
53
64
  "}",
54
65
  "",
55
66
  find_subcmd_function
@@ -57,54 +68,111 @@ module Clamp
57
68
  end
58
69
 
59
70
  def find_subcmd_function
60
- fn = function_name
61
- subcmds = Completion.collect_subcommand_names(@command_class).join("|")
62
71
  [
63
- "__#{fn}_find_subcmd() {",
64
- " local i=1 word subcmd",
72
+ "#{completion_function}_find_subcmd() {",
73
+ " local i=1 word subcmd skip",
74
+ " skip=$(#{completion_function}_param_count \"\")",
65
75
  ' while [ "$i" -lt "$COMP_CWORD" ]; do',
66
76
  ' word="${COMP_WORDS[$i]}"',
67
77
  ' case "$word" in',
68
78
  " -*)",
69
- " if __#{fn}_takes_value \"$word\" \"$subcmd\"; then",
79
+ " if #{completion_function}_takes_value \"$word\" \"$subcmd\"; then",
70
80
  " ((i++))",
71
81
  " fi",
72
82
  " ;;",
73
83
  " *)",
74
- ' case "$word" in',
75
- " #{subcmds})",
76
- ' if [ -z "$subcmd" ]; then',
77
- ' subcmd="$word"',
78
- " else",
79
- ' subcmd="${subcmd}::${word}"',
80
- " fi",
81
- " ;;",
82
- " esac",
84
+ find_subcmd_match_word,
83
85
  " ;;",
84
86
  " esac",
85
87
  " ((i++))",
86
88
  " done",
87
- ' echo "$subcmd"',
89
+ ' echo "$subcmd $skip"',
88
90
  "}"
89
91
  ].join("\n")
90
92
  end
91
93
 
92
- def completions_case(var)
94
+ def find_subcmd_match_word
95
+ subcmds = Completion.collect_subcommand_names(@command_class).join("|")
96
+ [
97
+ ' if [ "$skip" -gt 0 ]; then',
98
+ " ((skip--))",
99
+ " else",
100
+ ' case "$word" in',
101
+ " #{subcmds})",
102
+ " local canonical=$(#{completion_function}_canonical \"$word\")",
103
+ ' if [ -z "$subcmd" ]; then',
104
+ ' subcmd="$canonical"',
105
+ " else",
106
+ ' subcmd="${subcmd}::${canonical}"',
107
+ " fi",
108
+ " skip=$(#{completion_function}_param_count \"$subcmd\")",
109
+ " ;;",
110
+ " esac",
111
+ " fi"
112
+ ].join("\n")
113
+ end
114
+
115
+ def options_case(var)
116
+ build_case(var, " ", "COMPREPLY=") do |cmd, _has_children|
117
+ Completion.visible_options(cmd).flat_map { |o| Completion.expanded_switches(o) }
118
+ end
119
+ end
120
+
121
+ def subcommands_case(var)
122
+ build_case(var, " ", "COMPREPLY+=") do |cmd, has_children|
123
+ cmd.recognised_subcommands.flat_map(&:names) if has_children
124
+ end
125
+ end
126
+
127
+ def build_case(var, indent, assign)
93
128
  entries = {}
94
129
  Completion.walk_command_tree(@command_class) do |cmd, path, has_children|
130
+ words = yield(cmd, has_children)
131
+ next if words.nil? || words.empty?
132
+
95
133
  path_str = path.map { |sub| sub.names.first }.join("::")
96
- words = Completion.visible_options(cmd).flat_map { |o| Completion.expanded_switches(o) }
97
- cmd.recognised_subcommands.each { |sub| words.concat(sub.names) } if has_children
98
134
  entries[path_str] = words.join(" ")
99
135
  end
100
- lines = [" case \"#{var}\" in"]
136
+ lines = ["#{indent}case \"#{var}\" in"]
101
137
  entries.each do |path, words|
102
138
  pattern = path.empty? ? '""' : "\"#{path}\""
103
- lines << " #{pattern})"
104
- lines << " COMPREPLY=($(compgen -W \"#{words}\" -- \"$cur\"))"
105
- lines << " ;;"
139
+ lines << "#{indent} #{pattern})"
140
+ lines << "#{indent} #{assign}($(compgen -W \"#{words}\" -- \"$cur\"))"
141
+ lines << "#{indent} ;;"
142
+ end
143
+ lines << "#{indent}esac"
144
+ lines.join("\n")
145
+ end
146
+
147
+ def param_count_function
148
+ entries = {}
149
+ Completion.walk_command_tree(@command_class) do |cmd, path, has_children|
150
+ next unless has_children
151
+
152
+ entries[path.map { |s| s.names.first }.join("::")] = Completion.required_parameter_count(cmd)
153
+ end
154
+ build_lookup_function("param_count", entries, "echo", default: "echo 0")
155
+ end
156
+
157
+ def canonical_function
158
+ aliases = {}
159
+ Completion.walk_command_tree(@command_class) do |cmd, _path, has_children|
160
+ next unless has_children
161
+
162
+ cmd.recognised_subcommands.each do |sub|
163
+ sub.names.drop(1).each { |name| aliases[name] = sub.names.first }
164
+ end
165
+ end
166
+ build_lookup_function("canonical", aliases, "echo", default: 'echo "$1"')
167
+ end
168
+
169
+ def build_lookup_function(suffix, entries, verb, default:)
170
+ lines = ["#{completion_function}_#{suffix}() {", ' case "$1" in']
171
+ entries.each do |key, value|
172
+ pattern = key.empty? ? '""' : key
173
+ lines << " #{pattern}) #{verb} #{value.is_a?(String) ? "\"#{value}\"" : value} ;;"
106
174
  end
107
- lines << " esac"
175
+ lines.push(" *) #{default} ;;", " esac", "}")
108
176
  lines.join("\n")
109
177
  end
110
178
 
@@ -116,7 +184,7 @@ module Clamp
116
184
  .flat_map { |o| Completion.expanded_switches(o) }
117
185
  end
118
186
  lines = [
119
- "__#{function_name}_takes_value() {",
187
+ "#{completion_function}_takes_value() {",
120
188
  ' local option="$1"',
121
189
  ' local subcmd="$2"',
122
190
  ' case "$subcmd" in'
@@ -16,40 +16,119 @@ module Clamp
16
16
  lines << "# Fish completions for #{@executable_name}"
17
17
  lines << "# Generated by Clamp"
18
18
  lines << ""
19
+ helpers = [subcmd_args_function]
20
+
21
+ # Track visible option switches at each depth, so we can identify
22
+ # which options are new at each level. Depth-first walk order means
23
+ # the entry at depth N-1 is always the current node's parent.
24
+ switches_at_depth = []
25
+
19
26
  Completion.walk_command_tree(@command_class) do |cmd, path, has_children|
20
- child_names = has_children ? cmd.recognised_subcommands.flat_map(&:names) : []
21
- condition = condition_for(path, child_names)
22
- Completion.visible_options(cmd).each do |option|
23
- lines << option_completion(option, condition)
24
- end
27
+ generate_option_completions(lines, cmd, path, switches_at_depth)
28
+
25
29
  next unless has_children
26
30
 
27
- cmd.recognised_subcommands.each do |sub|
28
- sub.names.each do |name|
29
- lines << "complete -c #{@executable_name} -f -n '#{condition}' -a #{name} " \
30
- "-d '#{escape(sub.description)}'"
31
- end
32
- end
31
+ # Subcommand names need an exclusive condition (only at this level).
32
+ child_names = cmd.recognised_subcommands.flat_map(&:names)
33
+ exclusive_cond = condition_for(path, child_names)
34
+ subcmd_cond = subcommand_condition(cmd, path, exclusive_cond, helpers)
35
+ generate_subcommand_completions(lines, cmd, subcmd_cond)
33
36
  lines << ""
34
37
  end
35
- "#{lines.join("\n")}\n"
38
+ "#{helpers.join("\n\n")}\n\n#{lines.join("\n")}\n"
36
39
  end
37
40
 
38
41
  private
39
42
 
43
+ def subcommand_condition(cmd, path, condition, helpers)
44
+ param_count = Completion.required_parameter_count(cmd)
45
+ return condition unless param_count.positive?
46
+
47
+ helper = ParamsSatisfiedHelper.new(@executable_name, path, param_count)
48
+ helpers << helper.to_s
49
+ "#{condition}; and #{helper.function_name}"
50
+ end
51
+
52
+ def generate_subcommand_completions(lines, cmd, condition)
53
+ cmd.recognised_subcommands.each do |sub|
54
+ sub.names.each do |name|
55
+ lines << "complete -c #{@executable_name} -f -n '#{condition}' -a #{name} " \
56
+ "-d '#{escape(sub.description)}'"
57
+ end
58
+ end
59
+ end
60
+
61
+ # Emit only options new at this level (not inherited from parent).
62
+ # Use an inclusive condition so they also apply to child subcommands.
63
+ def generate_option_completions(lines, cmd, path, switches_at_depth)
64
+ depth = path.length
65
+ visible = Completion.visible_options(cmd)
66
+ parent_switches = depth.positive? ? switches_at_depth[depth - 1] : nil
67
+ new_options = if parent_switches
68
+ visible.reject { |o| parent_switches.include?(o.switches) }
69
+ else
70
+ visible
71
+ end
72
+ option_cond = depth.positive? ? inclusive_condition_for(path) : nil
73
+ new_options.each do |option|
74
+ lines << option_completion(option, option_cond)
75
+ end
76
+ switches_at_depth[depth] = Set.new(visible.map(&:switches))
77
+ end
78
+
79
+ # Condition that matches this level AND all child subcommands.
80
+ def inclusive_condition_for(path)
81
+ path.map { |sub| "#{completion_function}_seen_subcommand_from #{sub.names.join(' ')}" }.join("; and ")
82
+ end
83
+
84
+ # Condition that matches this level only (excludes child subcommands).
40
85
  def condition_for(path, child_names)
41
86
  if path.empty?
42
- "__fish_use_subcommand"
87
+ "not #{completion_function}_subcmd_args >/dev/null"
43
88
  else
44
- parts = path.map { |sub| "__fish_seen_subcommand_from #{sub.names.join(' ')}" }
45
- parts << "not __fish_seen_subcommand_from #{child_names.join(' ')}" if child_names.any?
89
+ parts = path.map { |sub| "#{completion_function}_seen_subcommand_from #{sub.names.join(' ')}" }
90
+ parts << "not #{completion_function}_seen_subcommand_from #{child_names.join(' ')}" if child_names.any?
46
91
  parts.join("; and ")
47
92
  end
48
93
  end
49
94
 
50
- def option_completion(option, condition)
95
+ def completion_function
96
+ "_clamp_complete_#{@executable_name}"
97
+ end
98
+
99
+ def subcmd_args_function
100
+ optspecs = argparse_optspecs(@command_class)
101
+ lines = [
102
+ "function #{completion_function}_subcmd_args",
103
+ " set -l tokens (commandline -opc)",
104
+ " set -e tokens[1]",
105
+ " argparse -si #{optspecs.map { |s| "'#{s}'" }.join(' ')} -- $tokens 2>/dev/null",
106
+ " or return 1",
107
+ " test (count $argv) -gt 0",
108
+ " and printf '%s\\n' $argv",
109
+ "end",
110
+ "",
111
+ "function #{completion_function}_seen_subcommand_from",
112
+ " for p in (#{completion_function}_subcmd_args)",
113
+ " if contains -- $p $argv",
114
+ " return 0",
115
+ " end",
116
+ " end",
117
+ " return 1",
118
+ "end"
119
+ ]
120
+ lines.join("\n")
121
+ end
122
+
123
+ def argparse_optspecs(command_class)
124
+ command_class.recognised_options.flat_map do |option|
125
+ Completion.argparse_specs_for(option)
126
+ end
127
+ end
128
+
129
+ def option_completion(option, condition = nil)
51
130
  parts = ["complete -c #{@executable_name} -f"]
52
- parts << "-n '#{condition}'"
131
+ parts << "-n '#{condition}'" if condition
53
132
 
54
133
  Completion.expanded_switches(option).each do |switch|
55
134
  parts << if switch.start_with?("--")
@@ -71,5 +150,57 @@ module Clamp
71
150
 
72
151
  end
73
152
 
153
+ # Generates a fish function that checks whether required parameters
154
+ # have been provided before offering subcommand completions.
155
+ class ParamsSatisfiedHelper
156
+
157
+ def initialize(executable_name, path, required_count)
158
+ @executable_name = executable_name
159
+ @path = path
160
+ @required_count = required_count
161
+ end
162
+
163
+ def function_name
164
+ parts = [@executable_name]
165
+ @path.each { |sub| parts << sub.names.first }
166
+ "_clamp_complete_#{parts.join('_')}_params_satisfied"
167
+ end
168
+
169
+ def to_s
170
+ lines = ["function #{function_name}", " set -l tokens (commandline -opc)", " set -l positional 0"]
171
+ if @path.empty?
172
+ count_positional_root(lines)
173
+ else
174
+ count_positional_nested(lines)
175
+ end
176
+ lines.push(" test $positional -ge #{@required_count}", "end")
177
+ lines.join("\n")
178
+ end
179
+
180
+ private
181
+
182
+ def count_positional_root(lines)
183
+ lines.push(" for token in $tokens[2..]",
184
+ " if not string match -q -- '-*' $token",
185
+ " set positional (math $positional + 1)",
186
+ " end", " end")
187
+ end
188
+
189
+ def count_positional_nested(lines)
190
+ lines.push(" set -l depth 0", " for token in $tokens[2..]", " switch $depth")
191
+ @path.each_with_index do |sub, i|
192
+ all_names = sub.names.join(" ")
193
+ lines.push(" case #{i}",
194
+ " if contains -- $token #{all_names}",
195
+ " set depth #{i + 1}", " end")
196
+ end
197
+ lines.push(" case #{@path.length}",
198
+ " if not string match -q -- '-*' $token",
199
+ " set positional (math $positional + 1)",
200
+ " end", " end", " end")
201
+ end
202
+
203
+ end
204
+
74
205
  end
75
206
  end
@@ -14,27 +14,27 @@ module Clamp
14
14
 
15
15
  def generate
16
16
  lines = ["#compdef #{@executable_name}", ""]
17
- generate_functions(lines, @command_class, [function_name], Set.new)
18
- lines << "_#{function_name}"
17
+ generate_functions(lines, @command_class, [completion_function], Set.new)
18
+ lines << completion_function
19
19
  lines.push("").join("\n")
20
20
  end
21
21
 
22
22
  private
23
23
 
24
- def function_name
25
- @executable_name.gsub(/[^a-zA-Z0-9_]/, "_")
24
+ def completion_function
25
+ "_clamp_complete_#{Completion.encode_name(@executable_name)}"
26
26
  end
27
27
 
28
28
  def generate_functions(lines, command_class, path, visited)
29
29
  has_children = command_class.has_subcommands? && !visited.include?(command_class)
30
30
  visited |= [command_class]
31
- func_name = "_#{path.join('_')}"
31
+ func_name = path.join("_")
32
32
 
33
33
  if has_children
34
34
  generate_subcommand_node(lines, command_class, path, func_name, visited)
35
35
  else
36
36
  lines << "#{func_name}() {"
37
- specs = Completion.visible_options(command_class).map { |o| option_spec(o) }
37
+ specs = Completion.visible_options(command_class).flat_map { |o| option_specs(o) }
38
38
  generate_arguments_call(lines, specs) if specs.any?
39
39
  lines << "}"
40
40
  lines << ""
@@ -46,7 +46,7 @@ module Clamp
46
46
  lines << " local context state state_descr line"
47
47
  lines << " typeset -A opt_args"
48
48
  lines << ""
49
- specs = Completion.visible_options(command_class).map { |o| option_spec(o) }
49
+ specs = Completion.visible_options(command_class).flat_map { |o| option_specs(o) }
50
50
  specs << "'1:command:->commands'"
51
51
  specs << "'*::args:->args'"
52
52
  lines << " _arguments -C \\"
@@ -56,7 +56,7 @@ module Clamp
56
56
  lines << "}"
57
57
  lines << ""
58
58
  command_class.recognised_subcommands.each do |sub|
59
- generate_functions(lines, sub.subcommand_class, path + [sanitize(sub.names.first)], visited)
59
+ generate_functions(lines, sub.subcommand_class, path + [Completion.encode_name(sub.names.first)], visited)
60
60
  end
61
61
  end
62
62
 
@@ -88,7 +88,7 @@ module Clamp
88
88
  lines << " args)"
89
89
  lines << " case $line[1] in"
90
90
  command_class.recognised_subcommands.each do |sub|
91
- sub_fn = "_#{(path + [sanitize(sub.names.first)]).join('_')}"
91
+ sub_fn = (path + [Completion.encode_name(sub.names.first)]).join("_")
92
92
  pattern = sub.names.join("|")
93
93
  lines << " #{pattern}) #{sub_fn} ;;"
94
94
  end
@@ -97,23 +97,20 @@ module Clamp
97
97
  lines << " esac"
98
98
  end
99
99
 
100
- def option_spec(option)
100
+ def option_specs(option)
101
101
  expanded = Completion.expanded_switches(option)
102
- desc = "[#{escape(option.description)}]"
103
- arg_spec = option.flag? ? "" : ":#{option.type.to_s.downcase}:"
104
- exclusion = expanded.length > 1 ? "(#{expanded.join(' ')})" : ""
105
-
106
- "'#{exclusion}#{switch_pattern(option.switches)}#{desc}#{arg_spec}'"
107
- end
108
-
109
- def switch_pattern(switches)
110
- short = switches.find { |s| s =~ /^-[^-]/ }
111
- long = switches.find { |s| s =~ /^--/ }
112
- short && long ? "{#{short},#{long}}" : (long || short).to_s
113
- end
114
-
115
- def sanitize(name)
116
- name.gsub(/[^a-zA-Z0-9_]/, "_")
102
+ suffix = "[#{escape(option.description)}]"
103
+ suffix += ":#{option.type.to_s.downcase}:" unless option.flag?
104
+ exclusion = expanded.length > 1 ? "'(#{expanded.join(' ')})'" : ""
105
+ short = expanded.find { |s| s.match?(/^-[^-]/) }
106
+ longs = expanded.grep(/^--/)
107
+
108
+ if short && longs.length == 1
109
+ # Braces outside quotes for zsh brace expansion
110
+ ["#{exclusion}{#{short},#{longs.first}}'#{suffix}'"]
111
+ else
112
+ expanded.map { |sw| "#{exclusion}'#{sw}#{suffix}'" }
113
+ end
117
114
  end
118
115
 
119
116
  def escape(str)
@@ -33,6 +33,12 @@ module Clamp
33
33
 
34
34
  module_function
35
35
 
36
+ # Encode a name for use as a shell function identifier.
37
+ # Special characters are replaced with _XX hex codes.
38
+ def encode_name(name)
39
+ name.gsub(/[^a-zA-Z0-9_]/) { |c| format("_%02x", c.ord) }
40
+ end
41
+
36
42
  def generate(command_class, shell, executable_name)
37
43
  generator_class = GENERATORS.fetch(shell) do
38
44
  raise ArgumentError, "unsupported shell: #{shell.inspect}"
@@ -71,6 +77,24 @@ module Clamp
71
77
  end
72
78
  end
73
79
 
80
+ # Count required, non-multivalued parameters for a command.
81
+ def required_parameter_count(command_class)
82
+ command_class.parameters.count { |p| p.required? && !p.multivalued? }
83
+ end
84
+
85
+ # Return fish argparse optspecs for an option.
86
+ def argparse_specs_for(option)
87
+ switches = expanded_switches(option)
88
+ suffix = option.flag? ? "" : "="
89
+ short = switches.find { |s| s.match?(/^-[^-]$/) }
90
+ longs = switches.select { |s| s.start_with?("--") }
91
+ if short && longs.length == 1
92
+ ["#{short.delete_prefix('-')}/#{longs.first.delete_prefix('--')}#{suffix}"]
93
+ else
94
+ longs.map { |l| "#{l.delete_prefix('--')}#{suffix}" }
95
+ end
96
+ end
97
+
74
98
  # Collect all subcommand names across the command tree.
75
99
  def collect_subcommand_names(command_class)
76
100
  names = []
data/lib/clamp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clamp
4
- VERSION = "1.5.0"
4
+ VERSION = "1.5.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: clamp
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Williams