clamp 1.2.0.beta1 → 1.3.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.
Files changed (51) hide show
  1. checksums.yaml +5 -5
  2. data/.editorconfig +9 -0
  3. data/.gitignore +1 -1
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +26 -19
  6. data/.travis.yml +3 -6
  7. data/CHANGES.md +17 -1
  8. data/Gemfile +8 -6
  9. data/Guardfile +3 -1
  10. data/README.md +36 -43
  11. data/Rakefile +8 -0
  12. data/clamp.gemspec +8 -6
  13. data/examples/admin +3 -2
  14. data/examples/defaulted +4 -3
  15. data/examples/flipflop +1 -0
  16. data/examples/fubar +1 -0
  17. data/examples/gitdown +2 -1
  18. data/examples/scoop +3 -2
  19. data/examples/speak +3 -2
  20. data/examples/subcommand_missing +1 -0
  21. data/examples/word +1 -0
  22. data/lib/clamp.rb +3 -1
  23. data/lib/clamp/attribute/declaration.rb +5 -0
  24. data/lib/clamp/attribute/definition.rb +25 -11
  25. data/lib/clamp/attribute/instance.rb +25 -3
  26. data/lib/clamp/command.rb +9 -1
  27. data/lib/clamp/errors.rb +7 -3
  28. data/lib/clamp/help.rb +38 -17
  29. data/lib/clamp/messages.rb +25 -15
  30. data/lib/clamp/option/declaration.rb +5 -1
  31. data/lib/clamp/option/definition.rb +9 -3
  32. data/lib/clamp/option/parsing.rb +38 -43
  33. data/lib/clamp/parameter/declaration.rb +4 -0
  34. data/lib/clamp/parameter/definition.rb +9 -3
  35. data/lib/clamp/parameter/parsing.rb +5 -1
  36. data/lib/clamp/subcommand/declaration.rb +17 -15
  37. data/lib/clamp/subcommand/definition.rb +5 -6
  38. data/lib/clamp/subcommand/execution.rb +12 -1
  39. data/lib/clamp/subcommand/parsing.rb +4 -0
  40. data/lib/clamp/truthy.rb +4 -2
  41. data/lib/clamp/version.rb +3 -1
  42. data/spec/clamp/command_group_spec.rb +29 -11
  43. data/spec/clamp/command_spec.rb +130 -48
  44. data/spec/clamp/help_spec.rb +63 -0
  45. data/spec/clamp/messages_spec.rb +5 -4
  46. data/spec/clamp/option/definition_spec.rb +13 -11
  47. data/spec/clamp/option_module_spec.rb +3 -1
  48. data/spec/clamp/option_reordering_spec.rb +6 -4
  49. data/spec/clamp/parameter/definition_spec.rb +14 -12
  50. data/spec/spec_helper.rb +3 -3
  51. metadata +9 -7
@@ -1,4 +1,5 @@
1
1
  #! /usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  # An example of options and parameters
4
5
 
@@ -6,12 +7,12 @@ require "clamp"
6
7
 
7
8
  Clamp do
8
9
 
9
- option "--timeout", "SECONDS", "connection timeout", :default => 5, :environment_variable => "MYAPP_TIMEOUT" do |x|
10
+ option "--timeout", "SECONDS", "connection timeout", default: 5, environment_variable: "MYAPP_TIMEOUT" do |x|
10
11
  Integer(x)
11
12
  end
12
13
 
13
14
  parameter "HOST", "server address"
14
- parameter "[PORT]", "server port", :default => 80, :environment_variable => "MYAPP_PORT"
15
+ parameter "[PORT]", "server port", default: 80, environment_variable: "MYAPP_PORT"
15
16
 
16
17
  def execute
17
18
  puts "trying to connect to #{host} on port #{port} (waiting up to #{timeout} seconds)"
@@ -1,4 +1,5 @@
1
1
  #! /usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  # An example of default values and methods
4
5
 
@@ -8,10 +9,10 @@ require "highline"
8
9
  Clamp do
9
10
 
10
11
  option ["-U", "--user"], "USER", "user name",
11
- :environment_variable => "THE_USER",
12
- :default => "bob"
12
+ environment_variable: "THE_USER",
13
+ default: "bob"
13
14
  option ["-P", "--password"], "PASSWORD", "password",
14
- :environment_variable => "THE_PASSWORD"
15
+ environment_variable: "THE_PASSWORD"
15
16
 
16
17
  def execute
17
18
  puts "User: #{user}, Password: #{password}"
@@ -1,4 +1,5 @@
1
1
  #! /usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  # An example of subcommands (including a default subcommand)
4
5
 
@@ -1,4 +1,5 @@
1
1
  #! /usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  # An example of nested subcommands
4
5
 
@@ -1,4 +1,5 @@
1
1
  #! /usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  # Demonstrate how subcommands can be declared as classes
4
5
 
@@ -25,7 +26,7 @@ module GitDown
25
26
  class CloneCommand < AbstractCommand
26
27
 
27
28
  parameter "REPOSITORY", "repository to clone"
28
- parameter "[DIR]", "working directory", :default => "."
29
+ parameter "[DIR]", "working directory", default: "."
29
30
 
30
31
  def execute
31
32
  say "cloning to #{dir}"
@@ -1,4 +1,5 @@
1
1
  #! /usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  # An example of multi-valued options
4
5
 
@@ -7,8 +8,8 @@ require "clamp"
7
8
  Clamp do
8
9
 
9
10
  option ["-f", "--flavour"], "FLAVOUR", "flavour",
10
- :multivalued => true, :default => ["chocolate"],
11
- :attribute_name => :flavours
11
+ multivalued: true, default: ["chocolate"],
12
+ attribute_name: :flavours
12
13
 
13
14
  def execute
14
15
  puts "one #{flavours.join(' and ')} ice-cream"
@@ -1,4 +1,5 @@
1
1
  #! /usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  # A simple Clamp command, with options and parameters
4
5
 
@@ -11,11 +12,11 @@ Clamp do
11
12
  )
12
13
 
13
14
  option "--loud", :flag, "say it loud"
14
- option ["-n", "--iterations"], "N", "say it N times", :default => 1 do |s|
15
+ option ["-n", "--iterations"], "N", "say it N times", default: 1 do |s|
15
16
  Integer(s)
16
17
  end
17
18
 
18
- parameter "WORDS ...", "the thing to say", :attribute_name => :words
19
+ parameter "WORDS ...", "the thing to say", attribute_name: :words
19
20
 
20
21
  def execute
21
22
  the_truth = words.join(" ")
@@ -1,4 +1,5 @@
1
1
  #! /usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  # Demonstrate subcommand_missing
4
5
 
@@ -1,4 +1,5 @@
1
1
  #! /usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  # Demonstrate "restful" style; parameters precede subcommands
4
5
 
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "clamp/version"
2
4
 
3
5
  require "clamp/command"
4
6
 
5
- def Clamp(&block)
7
+ def Clamp(&block) # rubocop:disable Naming/MethodName
6
8
  Class.new(Clamp::Command, &block).run
7
9
  end
@@ -1,6 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Clamp
2
4
  module Attribute
3
5
 
6
+ # Methods to generate attribute accessors.
7
+ #
4
8
  module Declaration
5
9
 
6
10
  protected
@@ -25,6 +29,7 @@ module Clamp
25
29
  end
26
30
 
27
31
  def define_default_for(attribute)
32
+ return false if attribute.default_value.nil?
28
33
  define_method(attribute.default_method) do
29
34
  attribute.default_value
30
35
  end
@@ -1,25 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "clamp/attribute/instance"
2
4
 
3
5
  module Clamp
4
6
  module Attribute
5
7
 
8
+ # Represents an attribute of a Clamp::Command class.
9
+ #
6
10
  class Definition
7
11
 
8
12
  def initialize(options)
9
- if options.key?(:attribute_name)
10
- @attribute_name = options[:attribute_name].to_s
11
- end
13
+ @attribute_name = options[:attribute_name].to_s if options.key?(:attribute_name)
12
14
  @default_value = options[:default] if options.key?(:default)
13
- if options.key?(:environment_variable)
14
- @environment_variable = options[:environment_variable]
15
- end
15
+ @environment_variable = options[:environment_variable] if options.key?(:environment_variable)
16
16
  @hidden = options[:hidden] if options.key?(:hidden)
17
17
  end
18
18
 
19
19
  attr_reader :description, :environment_variable
20
20
 
21
21
  def help_rhs
22
- description + default_description
22
+ rhs = description
23
+ comments = required_indicator || default_description
24
+ rhs += " (#{comments})" if comments
25
+ rhs
23
26
  end
24
27
 
25
28
  def help
@@ -51,11 +54,11 @@ module Clamp
51
54
  end
52
55
 
53
56
  def required?
54
- @required
57
+ @required ||= false
55
58
  end
56
59
 
57
60
  def hidden?
58
- @hidden
61
+ @hidden ||= false
59
62
  end
60
63
 
61
64
  def attribute_name
@@ -74,6 +77,17 @@ module Clamp
74
77
  Attribute::Instance.new(self, command)
75
78
  end
76
79
 
80
+ def option_missing_message
81
+ if environment_variable
82
+ Clamp.message(:option_or_env_required,
83
+ option: switches.first,
84
+ env: environment_variable)
85
+ else
86
+ Clamp.message(:option_required,
87
+ option: switches.first)
88
+ end
89
+ end
90
+
77
91
  private
78
92
 
79
93
  def default_description
@@ -81,8 +95,8 @@ module Clamp
81
95
  ("$#{@environment_variable}" if defined?(@environment_variable)),
82
96
  (@default_value.inspect if defined?(@default_value))
83
97
  ].compact
84
- return "" if default_sources.empty?
85
- " (default: " + default_sources.join(", or ") + ")"
98
+ return nil if default_sources.empty?
99
+ "#{Clamp.message(:default)}: " + default_sources.join(", #{Clamp.message(:or)} ")
86
100
  end
87
101
 
88
102
  end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Clamp
2
4
  module Attribute
3
5
 
4
- # Represents an option/parameter of a Clamp::Command instance.
6
+ # Represents an attribute of a Clamp::Command instance.
5
7
  #
6
8
  class Instance
7
9
 
@@ -27,7 +29,7 @@ module Clamp
27
29
  end
28
30
 
29
31
  def default
30
- command.send(attribute.default_method)
32
+ command.send(attribute.default_method) if command.respond_to?(attribute.default_method, true)
31
33
  end
32
34
 
33
35
  # default implementation of read_method
@@ -60,6 +62,10 @@ module Clamp
60
62
  end
61
63
  end
62
64
 
65
+ def signal_usage_error(*args)
66
+ command.send(:signal_usage_error, *args)
67
+ end
68
+
63
69
  def default_from_environment
64
70
  return if self.defined?
65
71
  return if attribute.environment_variable.nil?
@@ -69,10 +75,26 @@ module Clamp
69
75
  begin
70
76
  take(value)
71
77
  rescue ArgumentError => e
72
- command.send(:signal_usage_error, Clamp.message(:env_argument_error, :env => attribute.environment_variable, :message => e.message))
78
+ signal_usage_error Clamp.message(:env_argument_error, env: attribute.environment_variable, message: e.message)
79
+ end
80
+ end
81
+
82
+ def unset?
83
+ if attribute.multivalued?
84
+ read.empty?
85
+ else
86
+ read.nil?
73
87
  end
74
88
  end
75
89
 
90
+ def missing?
91
+ attribute.required? && unset?
92
+ end
93
+
94
+ def verify_not_missing
95
+ signal_usage_error attribute.option_missing_message if missing?
96
+ end
97
+
76
98
  end
77
99
 
78
100
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "clamp/messages"
2
4
  require "clamp/errors"
3
5
  require "clamp/help"
@@ -48,6 +50,7 @@ module Clamp
48
50
  parse_options
49
51
  parse_parameters
50
52
  parse_subcommand
53
+ verify_required_options_are_set
51
54
  handle_remaining_arguments
52
55
  end
53
56
 
@@ -81,7 +84,7 @@ module Clamp
81
84
  #
82
85
  # @ param [String] name subcommand_name
83
86
  def subcommand_missing(name)
84
- signal_usage_error(Clamp.message(:no_such_subcommand, :name => name))
87
+ signal_usage_error(Clamp.message(:no_such_subcommand, name: name))
85
88
  end
86
89
 
87
90
  include Clamp::Option::Parsing
@@ -122,6 +125,11 @@ module Clamp
122
125
  include Clamp::Subcommand::Declaration
123
126
  include Help
124
127
 
128
+ # An alternative to "def execute"
129
+ def execute(&block)
130
+ define_method(:execute, &block)
131
+ end
132
+
125
133
  # Create an instance of this command class, and run it.
126
134
  #
127
135
  # @param [String] invocation_path the path used to invoke the command
@@ -1,8 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Clamp
2
4
 
5
+ # raised to indicate invalid option/parameter declaration
3
6
  class DeclarationError < StandardError
4
7
  end
5
8
 
9
+ # abstract command runtime error
6
10
  class RuntimeError < StandardError
7
11
 
8
12
  def initialize(message, command)
@@ -14,10 +18,10 @@ module Clamp
14
18
 
15
19
  end
16
20
 
17
- # raise to signal incorrect command usage
21
+ # raised to signal incorrect command usage
18
22
  class UsageError < RuntimeError; end
19
23
 
20
- # raise to request usage help
24
+ # raised to request usage help
21
25
  class HelpWanted < RuntimeError
22
26
 
23
27
  def initialize(command)
@@ -26,7 +30,7 @@ module Clamp
26
30
 
27
31
  end
28
32
 
29
- # raise to signal error during execution
33
+ # raised to signal error during execution
30
34
  class ExecutionError < RuntimeError
31
35
 
32
36
  def initialize(message, command, status = 1)
@@ -1,8 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "stringio"
2
4
  require "clamp/messages"
3
5
 
4
6
  module Clamp
5
7
 
8
+ # Command help generation.
9
+ #
6
10
  module Help
7
11
 
8
12
  def usage(usage)
@@ -41,47 +45,66 @@ module Clamp
41
45
  help = builder
42
46
  help.add_usage(invocation_path, usage_descriptions)
43
47
  help.add_description(description)
44
- if has_parameters?
45
- help.add_list(Clamp.message(:parameters_heading), parameters)
46
- end
47
- if has_subcommands?
48
- help.add_list(Clamp.message(:subcommands_heading), recognised_subcommands)
49
- end
48
+ help.add_list(Clamp.message(:parameters_heading), parameters) if has_parameters?
49
+ help.add_list(Clamp.message(:subcommands_heading), recognised_subcommands) if has_subcommands?
50
50
  help.add_list(Clamp.message(:options_heading), recognised_options)
51
51
  help.string
52
52
  end
53
53
 
54
+ # A builder for auto-generated help.
55
+ #
54
56
  class Builder
55
57
 
56
58
  def initialize
57
- @out = StringIO.new
59
+ @lines = []
58
60
  end
59
61
 
60
62
  def string
61
- @out.string
63
+ left_column_width = lines.grep(Array).map(&:first).map(&:size).max
64
+ StringIO.new.tap do |out|
65
+ lines.each do |line|
66
+ case line
67
+ when Array
68
+ line[0] = line[0].ljust(left_column_width)
69
+ line.unshift("")
70
+ out.puts(line.join(" "))
71
+ else
72
+ out.puts(line)
73
+ end
74
+ end
75
+ end.string
76
+ end
77
+
78
+ def line(text = "")
79
+ @lines << text
80
+ end
81
+
82
+ def row(lhs, rhs)
83
+ @lines << [lhs, rhs]
62
84
  end
63
85
 
64
86
  def add_usage(invocation_path, usage_descriptions)
65
- puts Clamp.message(:usage_heading) + ":"
87
+ line Clamp.message(:usage_heading) + ":"
66
88
  usage_descriptions.each do |usage|
67
- puts " #{invocation_path} #{usage}".rstrip
89
+ line " #{invocation_path} #{usage}".rstrip
68
90
  end
69
91
  end
70
92
 
71
93
  def add_description(description)
72
94
  return unless description
73
- puts ""
74
- puts description.gsub(/^/, " ")
95
+ line
96
+ line description.gsub(/^/, " ")
75
97
  end
76
98
 
77
99
  DETAIL_FORMAT = " %-29s %s".freeze
78
100
 
79
101
  def add_list(heading, items)
80
- puts "\n#{heading}:"
102
+ line
103
+ line "#{heading}:"
81
104
  items.reject { |i| i.respond_to?(:hidden?) && i.hidden? }.each do |item|
82
105
  label, description = item.help
83
106
  description.each_line do |line|
84
- puts format(DETAIL_FORMAT, label, line)
107
+ row(label, line)
85
108
  label = ""
86
109
  end
87
110
  end
@@ -89,9 +112,7 @@ module Clamp
89
112
 
90
113
  private
91
114
 
92
- def puts(*args)
93
- @out.puts(*args)
94
- end
115
+ attr_accessor :lines
95
116
 
96
117
  end
97
118