jls-clamp 0.3.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.
@@ -0,0 +1,142 @@
1
+ require 'clamp/errors'
2
+ require 'clamp/help'
3
+ require 'clamp/option/declaration'
4
+ require 'clamp/option/parsing'
5
+ require 'clamp/parameter/declaration'
6
+ require 'clamp/parameter/parsing'
7
+ require 'clamp/subcommand/declaration'
8
+ require 'clamp/subcommand/parsing'
9
+
10
+ module Clamp
11
+
12
+ # {Command} models a shell command. Each command invocation is a new object.
13
+ # Command options and parameters are represented as attributes
14
+ # (see {Command::Declaration}).
15
+ #
16
+ # The main entry-point is {#run}, which uses {#parse} to populate attributes based
17
+ # on an array of command-line arguments, then calls {#execute} (which you provide)
18
+ # to make it go.
19
+ #
20
+ class Command
21
+
22
+ # Create a command execution.
23
+ #
24
+ # @param [String] invocation_path the path used to invoke the command
25
+ # @param [Hash] context additional data the command may need
26
+ #
27
+ def initialize(invocation_path, context = {})
28
+ @invocation_path = invocation_path
29
+ @context = context
30
+ end
31
+
32
+ # @return [String] the path used to invoke this command
33
+ #
34
+ attr_reader :invocation_path
35
+
36
+ # @return [Array<String>] unconsumed command-line arguments
37
+ #
38
+ def remaining_arguments
39
+ @remaining_arguments
40
+ end
41
+
42
+ # Parse command-line arguments.
43
+ #
44
+ # @param [Array<String>] arguments command-line arguments
45
+ # @return [Array<String>] unconsumed arguments
46
+ #
47
+ def parse(arguments)
48
+ @remaining_arguments = arguments.dup
49
+ parse_environment
50
+ parse_options
51
+ parse_parameters
52
+ parse_subcommand
53
+ handle_remaining_arguments
54
+ end
55
+
56
+ # Run the command, with the specified arguments.
57
+ #
58
+ # This calls {#parse} to process the command-line arguments,
59
+ # then delegates to {#execute}.
60
+ #
61
+ # @param [Array<String>] arguments command-line arguments
62
+ #
63
+ def run(arguments)
64
+ parse(arguments)
65
+ execute
66
+ end
67
+
68
+ # Execute the command (assuming that all options/parameters have been set).
69
+ #
70
+ # This method is designed to be overridden in sub-classes.
71
+ #
72
+ def execute
73
+ if @subcommand
74
+ @subcommand.execute
75
+ else
76
+ raise "you need to define #execute"
77
+ end
78
+ end
79
+
80
+ # @return [String] usage documentation for this command
81
+ #
82
+ def help
83
+ self.class.help(invocation_path)
84
+ end
85
+
86
+ include Clamp::Option::Parsing
87
+ include Clamp::Parameter::Parsing
88
+ include Clamp::Subcommand::Parsing
89
+
90
+ protected
91
+
92
+ attr_accessor :context
93
+
94
+ def handle_remaining_arguments
95
+ unless remaining_arguments.empty?
96
+ signal_usage_error "too many arguments"
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def signal_usage_error(message)
103
+ e = UsageError.new(message, self)
104
+ e.set_backtrace(caller)
105
+ raise e
106
+ end
107
+
108
+ def request_help
109
+ raise HelpWanted, self
110
+ end
111
+
112
+ class << self
113
+
114
+ include Clamp::Option::Declaration
115
+ include Clamp::Parameter::Declaration
116
+ include Clamp::Subcommand::Declaration
117
+ include Help
118
+
119
+ # Create an instance of this command class, and run it.
120
+ #
121
+ # @param [String] invocation_path the path used to invoke the command
122
+ # @param [Array<String>] arguments command-line arguments
123
+ # @param [Hash] context additional data the command may need
124
+ #
125
+ def run(invocation_path = File.basename($0), arguments = ARGV, context = {})
126
+ begin
127
+ new(invocation_path, context).run(arguments)
128
+ rescue Clamp::UsageError => e
129
+ $stderr.puts "ERROR: #{e.message}"
130
+ $stderr.puts ""
131
+ $stderr.puts "See: '#{e.command.invocation_path} --help'"
132
+ exit(1)
133
+ rescue Clamp::HelpWanted => e
134
+ puts e.command.help
135
+ end
136
+ end
137
+
138
+ end
139
+
140
+ end
141
+
142
+ end
@@ -0,0 +1,26 @@
1
+ module Clamp
2
+
3
+ class Error < StandardError
4
+
5
+ def initialize(message, command)
6
+ super(message)
7
+ @command = command
8
+ end
9
+
10
+ attr_reader :command
11
+
12
+ end
13
+
14
+ # raise to signal incorrect command usage
15
+ class UsageError < Error; end
16
+
17
+ # raise to request usage help
18
+ class HelpWanted < Error
19
+
20
+ def initialize(command)
21
+ super("I need help", command)
22
+ end
23
+
24
+ end
25
+
26
+ end
data/lib/clamp/help.rb ADDED
@@ -0,0 +1,100 @@
1
+ require 'stringio'
2
+
3
+ module Clamp
4
+
5
+ module Help
6
+
7
+ def usage(usage)
8
+ @declared_usage_descriptions ||= []
9
+ @declared_usage_descriptions << usage
10
+ end
11
+
12
+ attr_reader :declared_usage_descriptions
13
+
14
+ def description=(description)
15
+ @description = description.dup
16
+ if @description =~ /^\A\n*( +)/
17
+ indent = $1
18
+ @description.gsub!(/^#{indent}/, '')
19
+ end
20
+ @description.strip!
21
+ end
22
+
23
+ attr_reader :description
24
+
25
+ def derived_usage_description
26
+ parts = ["[OPTIONS]"]
27
+ parts += parameters.map { |a| a.name }
28
+ if has_subcommands?
29
+ parts << "SUBCOMMAND"
30
+ parts << "[ARGS] ..."
31
+ end
32
+ parts.join(" ")
33
+ end
34
+
35
+ def usage_descriptions
36
+ declared_usage_descriptions || [derived_usage_description]
37
+ end
38
+
39
+ def help(invocation_path)
40
+ help = Builder.new
41
+ help.add_usage(invocation_path, usage_descriptions)
42
+ help.add_description(description)
43
+ if has_parameters?
44
+ help.add_list("Parameters", parameters)
45
+ end
46
+ if has_subcommands?
47
+ help.add_list("Subcommands", recognised_subcommands)
48
+ end
49
+ help.add_list("Options", recognised_options)
50
+ help.string
51
+ end
52
+
53
+ class Builder
54
+
55
+ def initialize
56
+ @out = StringIO.new
57
+ end
58
+
59
+ def string
60
+ @out.string
61
+ end
62
+
63
+ def add_usage(invocation_path, usage_descriptions)
64
+ puts "Usage:"
65
+ usage_descriptions.each do |usage|
66
+ puts " #{invocation_path} #{usage}".rstrip
67
+ end
68
+ end
69
+
70
+ def add_description(description)
71
+ if description
72
+ puts ""
73
+ puts description.gsub(/^/, " ")
74
+ end
75
+ end
76
+
77
+ DETAIL_FORMAT = " %-29s %s"
78
+
79
+ def add_list(heading, items)
80
+ puts "\n#{heading}:"
81
+ items.each do |item|
82
+ label, description = item.help
83
+ description.each_line do |line|
84
+ puts DETAIL_FORMAT % [label, line]
85
+ label = ''
86
+ end
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ def puts(*args)
93
+ @out.puts(*args)
94
+ end
95
+
96
+ end
97
+
98
+ end
99
+
100
+ end
@@ -0,0 +1,57 @@
1
+ require 'clamp/attribute_declaration'
2
+ require 'clamp/option'
3
+
4
+ module Clamp
5
+ class Option
6
+
7
+ module Declaration
8
+
9
+ include Clamp::AttributeDeclaration
10
+
11
+ def option(switches, type, description, opts = {}, &block)
12
+ option = Clamp::Option.new(switches, type, description, opts)
13
+ declared_options << option
14
+ define_accessors_for(option, &block)
15
+ end
16
+
17
+ def find_option(switch)
18
+ recognised_options.find { |o| o.handles?(switch) }
19
+ end
20
+
21
+ def declared_options
22
+ @declared_options ||= []
23
+ end
24
+
25
+ def recognised_options
26
+ declare_implicit_options
27
+ effective_options
28
+ end
29
+
30
+ private
31
+
32
+ def declare_implicit_options
33
+ return nil if defined?(@implicit_options_declared)
34
+ unless effective_options.find { |o| o.handles?("--help") }
35
+ help_switches = ["--help"]
36
+ help_switches.unshift("-h") unless effective_options.find { |o| o.handles?("-h") }
37
+ option help_switches, :flag, "print help" do
38
+ request_help
39
+ end
40
+ end
41
+ @implicit_options_declared = true
42
+ end
43
+
44
+ def effective_options
45
+ ancestors.inject([]) do |options, ancestor|
46
+ if ancestor.kind_of?(Clamp::Option::Declaration)
47
+ options + ancestor.declared_options
48
+ else
49
+ options
50
+ end
51
+ end
52
+ end
53
+
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,59 @@
1
+ module Clamp
2
+ class Option
3
+
4
+ module Parsing
5
+
6
+ protected
7
+
8
+ def parse_options
9
+ while remaining_arguments.first =~ /^-/
10
+
11
+ switch = remaining_arguments.shift
12
+ break if switch == "--"
13
+
14
+ case switch
15
+ when /^(-\w)(.+)$/ # combined short options
16
+ switch = $1
17
+ remaining_arguments.unshift("-#{$2}")
18
+ when /^(--[^=]+)=(.*)/
19
+ switch = $1
20
+ remaining_arguments.unshift($2)
21
+ end
22
+
23
+ option = find_option(switch)
24
+ value = option.extract_value(switch, remaining_arguments)
25
+
26
+ begin
27
+ send("#{option.attribute_name}=", value)
28
+ rescue ArgumentError => e
29
+ signal_usage_error "option '#{switch}': #{e.message}"
30
+ end
31
+
32
+ end
33
+ end
34
+
35
+ def parse_environment
36
+ self.class.recognised_options.each do |option|
37
+ next if option.env_var.nil?
38
+ next unless ENV.has_key?(option.env_var)
39
+ value = ENV[option.env_var]
40
+ if option.flag?
41
+ # Set true if the env var is "1" false otherwise.
42
+ send("#{option.attribute_name}=", value == "1")
43
+ else
44
+ send("#{option.attribute_name}=", value)
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def find_option(switch)
52
+ self.class.find_option(switch) ||
53
+ signal_usage_error("Unrecognised option '#{switch}'")
54
+ end
55
+
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,80 @@
1
+ require 'clamp/attribute'
2
+
3
+ module Clamp
4
+
5
+ class Option < Attribute
6
+
7
+ def initialize(switches, type, description, options = {})
8
+ @switches = Array(switches)
9
+ @type = type
10
+ @description = description
11
+ if options.has_key?(:attribute_name)
12
+ @attribute_name = options[:attribute_name].to_s
13
+ end
14
+ if options.has_key?(:default)
15
+ @default_value = options[:default]
16
+ end
17
+ if options.has_key?(:env)
18
+ @env_var = options[:env]
19
+ end
20
+ end
21
+
22
+ attr_reader :switches, :type
23
+
24
+ def attribute_name
25
+ @attribute_name ||= long_switch.sub(/^--(\[no-\])?/, '').tr('-', '_')
26
+ end
27
+
28
+ def long_switch
29
+ switches.find { |switch| switch =~ /^--/ }
30
+ end
31
+
32
+ def handles?(switch)
33
+ recognised_switches.member?(switch)
34
+ end
35
+
36
+ def flag?
37
+ @type == :flag
38
+ end
39
+
40
+ def flag_value(switch)
41
+ !(switch =~ /^--no-(.*)/ && switches.member?("--\[no-\]#{$1}"))
42
+ end
43
+
44
+ def read_method
45
+ if flag?
46
+ super + "?"
47
+ else
48
+ super
49
+ end
50
+ end
51
+
52
+ def extract_value(switch, arguments)
53
+ if flag?
54
+ flag_value(switch)
55
+ else
56
+ arguments.shift
57
+ end
58
+ end
59
+
60
+ def help_lhs
61
+ lhs = switches.join(", ")
62
+ lhs += " " + type unless flag?
63
+ lhs
64
+ end
65
+
66
+ private
67
+
68
+ def recognised_switches
69
+ switches.map do |switch|
70
+ if switch =~ /^--\[no-\](.*)/
71
+ ["--#{$1}", "--no-#{$1}"]
72
+ else
73
+ switch
74
+ end
75
+ end.flatten
76
+ end
77
+
78
+ end
79
+
80
+ end
@@ -0,0 +1,28 @@
1
+ require 'clamp/attribute_declaration'
2
+ require 'clamp/parameter'
3
+
4
+ module Clamp
5
+ class Parameter
6
+
7
+ module Declaration
8
+
9
+ include Clamp::AttributeDeclaration
10
+
11
+ def parameters
12
+ @parameters ||= []
13
+ end
14
+
15
+ def has_parameters?
16
+ !parameters.empty?
17
+ end
18
+
19
+ def parameter(name, description, options = {}, &block)
20
+ parameter = Parameter.new(name, description, options)
21
+ parameters << parameter
22
+ define_accessors_for(parameter, &block)
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ module Clamp
2
+ class Parameter
3
+
4
+ module Parsing
5
+
6
+ protected
7
+
8
+ def parse_parameters
9
+
10
+ self.class.parameters.each do |parameter|
11
+ begin
12
+ value = parameter.consume(remaining_arguments)
13
+ send("#{parameter.attribute_name}=", value) unless value.nil?
14
+ rescue ArgumentError => e
15
+ signal_usage_error "parameter '#{parameter.name}': #{e.message}"
16
+ end
17
+ end
18
+
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,80 @@
1
+ require 'clamp/attribute'
2
+
3
+ module Clamp
4
+
5
+ class Parameter < Attribute
6
+
7
+ def initialize(name, description, options = {})
8
+ @name = name
9
+ @description = description
10
+ infer_attribute_name_and_multiplicity
11
+ if options.has_key?(:attribute_name)
12
+ @attribute_name = options[:attribute_name].to_s
13
+ end
14
+ if options.has_key?(:default)
15
+ @default_value = options[:default]
16
+ end
17
+ end
18
+
19
+ attr_reader :name, :attribute_name
20
+
21
+ def help_lhs
22
+ name
23
+ end
24
+
25
+ def consume(arguments)
26
+ if required? && arguments.empty?
27
+ raise ArgumentError, "no value provided"
28
+ end
29
+ if multivalued?
30
+ if arguments.length > 0
31
+ arguments.shift(arguments.length)
32
+ end
33
+ else
34
+ arguments.shift
35
+ end
36
+ end
37
+
38
+ def default_value
39
+ if defined?(@default_value)
40
+ @default_value
41
+ elsif multivalued?
42
+ []
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ NAME_PATTERN = "([A-Za-z0-9_-]+)"
49
+
50
+ def infer_attribute_name_and_multiplicity
51
+ case @name
52
+ when /^\[#{NAME_PATTERN}\]$/
53
+ @attribute_name = $1
54
+ when /^\[#{NAME_PATTERN}\] ...$/
55
+ @attribute_name = "#{$1}_list"
56
+ @multivalued = true
57
+ when /^#{NAME_PATTERN} ...$/
58
+ @attribute_name = "#{$1}_list"
59
+ @multivalued = true
60
+ @required = true
61
+ when /^#{NAME_PATTERN}$/
62
+ @attribute_name = @name
63
+ @required = true
64
+ else
65
+ raise "invalid parameter name: '#{name}'"
66
+ end
67
+ @attribute_name = @attribute_name.downcase.tr('-', '_')
68
+ end
69
+
70
+ def multivalued?
71
+ @multivalued
72
+ end
73
+
74
+ def required?
75
+ @required
76
+ end
77
+
78
+ end
79
+
80
+ end
@@ -0,0 +1,44 @@
1
+ require 'clamp/subcommand'
2
+
3
+ module Clamp
4
+ class Subcommand
5
+
6
+ module Declaration
7
+
8
+ def recognised_subcommands
9
+ @recognised_subcommands ||= []
10
+ end
11
+
12
+ def subcommand(name, description, subcommand_class = self, &block)
13
+ if block
14
+ # generate a anonymous sub-class
15
+ subcommand_class = Class.new(subcommand_class, &block)
16
+ end
17
+ recognised_subcommands << Subcommand.new(name, description, subcommand_class)
18
+ end
19
+
20
+ def has_subcommands?
21
+ !recognised_subcommands.empty?
22
+ end
23
+
24
+ def find_subcommand(name)
25
+ recognised_subcommands.find { |sc| sc.is_called?(name) }
26
+ end
27
+
28
+ attr_writer :default_subcommand
29
+
30
+ def default_subcommand(*args, &block)
31
+ if args.empty?
32
+ @default_subcommand ||= nil
33
+ else
34
+ $stderr.puts "WARNING: Clamp default_subcommand syntax has changed; check the README."
35
+ $stderr.puts " (from #{caller.first})"
36
+ subcommand(*args, &block)
37
+ self.default_subcommand = args.first
38
+ end
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,41 @@
1
+ module Clamp
2
+ class Subcommand
3
+
4
+ module Parsing
5
+
6
+ protected
7
+
8
+ def parse_subcommand
9
+ return false unless self.class.has_subcommands?
10
+ subcommand_name = parse_subcommand_name
11
+ @subcommand = instatiate_subcommand(subcommand_name)
12
+ @subcommand.parse(remaining_arguments)
13
+ remaining_arguments.clear
14
+ end
15
+
16
+ private
17
+
18
+ def parse_subcommand_name
19
+ remaining_arguments.shift || self.class.default_subcommand || request_help
20
+ end
21
+
22
+ def find_subcommand(name)
23
+ self.class.find_subcommand(name) ||
24
+ signal_usage_error("No such sub-command '#{name}'")
25
+ end
26
+
27
+ def instatiate_subcommand(name)
28
+ subcommand_class = find_subcommand(name).subcommand_class
29
+ subcommand = subcommand_class.new("#{invocation_path} #{name}", context)
30
+ self.class.recognised_options.each do |option|
31
+ if instance_variable_defined?(option.ivar_name)
32
+ subcommand.instance_variable_set(option.ivar_name, instance_variable_get(option.ivar_name))
33
+ end
34
+ end
35
+ subcommand
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,23 @@
1
+ module Clamp
2
+
3
+ class Subcommand < Struct.new(:name, :description, :subcommand_class)
4
+
5
+ def initialize(names, description, subcommand_class)
6
+ @names = Array(names)
7
+ @description = description
8
+ @subcommand_class = subcommand_class
9
+ end
10
+
11
+ attr_reader :names, :description, :subcommand_class
12
+
13
+ def is_called?(name)
14
+ names.member?(name)
15
+ end
16
+
17
+ def help
18
+ [names.join(", "), description]
19
+ end
20
+
21
+ end
22
+
23
+ end