jls-clamp 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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