shellopts 0.9.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,130 @@
1
+ require "ext/array.rb"
2
+
3
+ require 'shellopts/grammar/node.rb'
4
+ require 'shellopts/grammar/option.rb'
5
+ require 'shellopts/grammar/command.rb'
6
+ require 'shellopts/grammar/program.rb'
7
+
8
+ module ShellOpts
9
+ module Grammar
10
+ # Compiles an option definition string and returns a Grammar::Program
11
+ # object. program_name is the name of the program and source is the
12
+ # option definition string
13
+ def self.compile(program_name, source)
14
+ program_name.is_a?(String) or raise Compiler::Error, "Expected String argument, got #{program_name.class}"
15
+ source.is_a?(String) or raise Compiler::Error, "Expected String argument, got #{source.class}"
16
+ Compiler.new(program_name, source).call
17
+ end
18
+
19
+ # Service object for compiling an option definition string. Returns a
20
+ # Grammar::Program object
21
+ #
22
+ # Compiler implements a recursive descend algorithm to compile the option
23
+ # string. The algorithm uses state variables and is embedded in a
24
+ # Grammar::Compiler service object
25
+ class Compiler
26
+ class Error < RuntimeError; end
27
+
28
+ # Initialize a Compiler object. source is the option definition string
29
+ def initialize(program_name, source)
30
+ @program_name, @tokens = program_name, source.split(/\s+/)
31
+
32
+ # @commands_by_path is an hash from command-path to Command or Program
33
+ # object. The top level Program object has nil as its path.
34
+ # @commands_by_path is used to check for uniqueness of commands and to
35
+ # link sub-commands to their parents
36
+ @commands_by_path = {}
37
+ end
38
+
39
+ def call
40
+ compile_program
41
+ end
42
+
43
+ private
44
+ using XArray # For Array#find_dup
45
+
46
+ # Returns the current token
47
+ def curr_token() @tokens.first end
48
+
49
+ # Returns the current token and advance to the next token
50
+ def next_token() @tokens.shift end
51
+
52
+ def error(msg) # Just a shorthand. Unrelated to ShellOpts.error
53
+ raise Compiler::Error.new(msg)
54
+ end
55
+
56
+ def compile_program
57
+ program = @commands_by_path[nil] = Grammar::Program.new(@program_name, compile_options)
58
+ while curr_token && curr_token != "--"
59
+ compile_command
60
+ end
61
+ program.args.concat(@tokens[1..-1]) if curr_token
62
+ program
63
+ end
64
+
65
+ def compile_command
66
+ path = curr_token[0..-2]
67
+ ident_list = compile_ident_list(path, ".")
68
+ parent_path = ident_list.size > 1 ? ident_list[0..-2].join(".") : nil
69
+ name = ident_list[-1]
70
+
71
+ parent = @commands_by_path[parent_path] or
72
+ error "No such command: #{parent_path.inspect}"
73
+ !@commands_by_path.key?(path) or error "Duplicate command: #{path.inspect}"
74
+ next_token
75
+ @commands_by_path[path] = Grammar::Command.new(parent, name, compile_options)
76
+ end
77
+
78
+ def compile_options
79
+ option_list = []
80
+ while curr_token && curr_token != "--" && !curr_token.end_with?("!")
81
+ option_list << compile_option
82
+ end
83
+ dup = option_list.map(&:names).flatten.find_dup and
84
+ error "Duplicate option name: #{dup.inspect}"
85
+ option_list
86
+ end
87
+
88
+ def compile_option
89
+ # Match string and build flags
90
+ flags = []
91
+ curr_token =~ /^(\+)?(.+?)(?:(=)(\$|\#)?(.*?)(\?)?)?$/
92
+ flags << :repeated if $1 == "+"
93
+ names = $2
94
+ flags << :argument if $3 == "="
95
+ flags << :integer if $4 == "#"
96
+ flags << :float if $4 == "$"
97
+ label = $5 == "" ? nil : $5
98
+ flags << :optional if $6 == "?"
99
+
100
+ # Build names
101
+ short_names = []
102
+ long_names = []
103
+ ident_list = compile_ident_list(names, ",")
104
+ (dup = ident_list.find_dup).nil? or
105
+ error "Duplicate identifier #{dup.inspect} in #{curr_token.inspect}"
106
+ ident_list.each { |ident|
107
+ if ident.size == 1
108
+ short_names << "-#{ident}"
109
+ else
110
+ long_names << "--#{ident}"
111
+ end
112
+ }
113
+
114
+ next_token
115
+ Grammar::Option.new(short_names, long_names, flags, label)
116
+ end
117
+
118
+ # Compile list of option names or a command path
119
+ def compile_ident_list(ident_list_str, sep)
120
+ ident_list_str.split(sep, -1).map { |str|
121
+ !str.empty? or error "Empty identifier in #{curr_token.inspect}"
122
+ !str.start_with?("-") or error "Identifier can't start with '-' in #{curr_token.inspect}"
123
+ str !~ /([^\w\d#{sep}-])/ or
124
+ error "Illegal character #{$1.inspect} in #{curr_token.inspect}"
125
+ str
126
+ }
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,64 @@
1
+ module ShellOpts
2
+ module Grammar
3
+ # A command. Commands are organized hierarchically with a Program object as
4
+ # the root node
5
+ #
6
+ # Sets Node#key to the name of the command incl. the exclamation point
7
+ class Command < Node
8
+ # Parent command. Nil if this is the top level command (the program)
9
+ attr_reader :parent
10
+
11
+ # Name of command (String). Name doesn't include the exclamation point ('!')
12
+ attr_reader :name
13
+
14
+ # Hash from option names (both short and long names) to option. This
15
+ # means an option can occur more than once as the hash value
16
+ attr_reader :options
17
+
18
+ # Sub-commands of this command. Is a hash from sub-command name to command object
19
+ attr_reader :commands
20
+
21
+ # List of options in declaration order
22
+ # order
23
+ attr_reader :option_list
24
+
25
+ # List of commands in declaration order
26
+ attr_reader :command_list
27
+
28
+ # Initialize a Command object. parent is the parent Command object or nil
29
+ # if this is the root object. name is the name of the command (without
30
+ # the exclamation mark), and option_list a list of Option objects
31
+ def initialize(parent, name, option_list)
32
+ super("#{name}!".to_sym)
33
+ @name = name
34
+ parent.attach(self) if parent
35
+ @option_list = option_list
36
+ @options = @option_list.flat_map { |opt| opt.names.map { |name| [name, opt] } }.to_h
37
+ @commands = {}
38
+ @command_list = []
39
+ end
40
+
41
+ # :nocov:
42
+ def dump(&block)
43
+ puts "#{key.inspect}"
44
+ indent {
45
+ puts "parent: #{parent&.key.inspect}"
46
+ puts "name: #{name.inspect}"
47
+ yield if block_given?
48
+ puts "options:"
49
+ indent { option_list.each { |opt| opt.dump } }
50
+ puts "commands: "
51
+ indent { command_list.each { |cmd| cmd.dump } }
52
+ }
53
+ end
54
+ # :nocov:
55
+
56
+ protected
57
+ def attach(command)
58
+ command.instance_variable_set(:@parent, self)
59
+ @commands[command.name] = command
60
+ @command_list << command
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,25 @@
1
+ module ShellOpts
2
+ module Grammar
3
+ # Root class for Grammar objects
4
+ #
5
+ # Node objects are created by ShellOpts::Grammar.compile that returns a
6
+ # Program object that in turn contains other node objects in a hierarchical
7
+ # structure that reflects the grammar of the program. Only
8
+ # ShellOpts::Grammar.compile should create node objects
9
+ class Node
10
+ # Key (Symbol) of node. Unique within the enclosing command
11
+ attr_reader :key
12
+
13
+ def initialize(key)
14
+ @key = key
15
+ end
16
+
17
+ # :nocov:
18
+ def dump(&block)
19
+ puts key.inspect
20
+ indent { yield } if block_given?
21
+ end
22
+ # :nocov:
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,55 @@
1
+ module ShellOpts
2
+ module Grammar
3
+ # Models an Option
4
+ #
5
+ # Sets Node#key to the first long option name if present or else the first short option
6
+ class Option < Node
7
+ # List of short names (incl. '-')
8
+ attr_reader :short_names
9
+
10
+ # List of long names (incl. '--')
11
+ attr_reader :long_names
12
+
13
+ # List of flags (Symbol)
14
+ def flags() @flags.keys end
15
+
16
+ # Informal name of argument (eg. 'FILE'). nil if not present
17
+ attr_reader :label
18
+
19
+ # Initialize an option. Short and long names are arrays of the short/long
20
+ # option names (incl. the '-'/'--' prefix). It is assumed that at least
21
+ # one name is given. Flags is a list of symbolic flags. Allowed flags are
22
+ # :repeated, :argument, :optional, :integer, and :float. Note that
23
+ # there's no :string flag, it's status is inferred. label is the optional
24
+ # informal name of the option argument (eg. 'FILE') or nil if not present
25
+ def initialize(short_names, long_names, flags, label = nil)
26
+ super((long_names.first || short_names.first).sub(/^-+/, "").to_sym)
27
+ @short_names, @long_names = short_names, long_names
28
+ @flags = flags.map { |flag| [flag, true] }.to_h
29
+ @label = label
30
+ end
31
+
32
+ # Array of option names with short names first and then the long names
33
+ def names() @short_names + @long_names end
34
+
35
+ # Flag query methods. Returns true if the flag is present and otherwise nil
36
+ def repeated?() @flags[:repeated] || false end
37
+ def argument?() @flags[:argument] || false end
38
+ def optional?() argument? && @flags[:optional] || false end
39
+ def string?() argument? && !integer? && !float? end
40
+ def integer?() argument? && @flags[:integer] || false end
41
+ def float?() argument? && @flags[:float] || false end
42
+
43
+ # :nocov:
44
+ def dump
45
+ super {
46
+ puts "short_names: #{short_names.inspect}"
47
+ puts "long_names: #{long_names.inspect}"
48
+ puts "flags: #{flags.inspect}"
49
+ puts "label: #{label.inspect}"
50
+ }
51
+ end
52
+ # :nocov:
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,65 @@
1
+ module ShellOpts
2
+ module Grammar
3
+ # Program is the root object of the grammar
4
+ class Program < Command
5
+ # Array of non-option litteral arguments (ie. what comes after the double dash ('+--+') in
6
+ # the usage definition). Initially empty but filled out during compilation
7
+ attr_reader :args
8
+
9
+ # Initialize a top-level Program object
10
+ def initialize(name, option_list)
11
+ super(nil, name, option_list)
12
+ @args = []
13
+ end
14
+
15
+ # Usage string to be used in error messages. The string is kept short by
16
+ # only listing the shortest option (if there is more than one)
17
+ def usage
18
+ (
19
+ render_options(option_list) +
20
+ commands.values.map { |cmd| render_command(cmd) } +
21
+ args
22
+ ).flatten.join(" ")
23
+ end
24
+
25
+ # :nocov:
26
+ def dump(&block)
27
+ super {
28
+ puts "args: #{args.inspect}"
29
+ puts "usage: #{usage.inspect}"
30
+ }
31
+ end
32
+ # :nocov:
33
+
34
+ private
35
+ def render_command(command)
36
+ [command.name] + render_options(command.option_list) +
37
+ command.commands.values.map { |cmd| render_command(cmd) }.flatten
38
+ end
39
+
40
+ def render_options(options)
41
+ options.map { |opt|
42
+ s = opt.names.first
43
+ if opt.argument?
44
+ arg_string =
45
+ if opt.label
46
+ opt.label
47
+ elsif opt.integer?
48
+ "INT"
49
+ elsif opt.float?
50
+ "FLOAT"
51
+ else
52
+ "ARG"
53
+ end
54
+ if opt.optional?
55
+ s += "[=#{arg_string}]"
56
+ else
57
+ s += "=#{arg_string}"
58
+ end
59
+ end
60
+ s
61
+ }
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,106 @@
1
+
2
+ require 'shellopts/ast/node.rb'
3
+ require 'shellopts/ast/option.rb'
4
+ require 'shellopts/ast/command.rb'
5
+ require 'shellopts/ast/program.rb'
6
+
7
+ module ShellOpts
8
+ module Ast
9
+ # Parse ARGV according to grammar. Returns a Ast::Program object
10
+ def self.parse(grammar, argv)
11
+ grammar.is_a?(Grammar::Program) or
12
+ raise InternalError, "Expected Grammar::Program object, got #{grammar.class}"
13
+ argv.is_a?(Array) or
14
+ raise InternalError, "Expected Array object, got #{argv.class}"
15
+ Parser.new(grammar, argv).call
16
+ end
17
+
18
+ private
19
+ # Parse a command line
20
+ class Parser
21
+ class Error < RuntimeError; end
22
+
23
+ def initialize(grammar, argv)
24
+ @grammar, @argv = grammar, argv.dup
25
+ @seen_options = {} # Used to keep track of repeated options
26
+ end
27
+
28
+ def call
29
+ program = Ast::Program.new(@grammar)
30
+ parse_command(program)
31
+ program.arguments = @argv
32
+ program
33
+ end
34
+
35
+ private
36
+ def parse_command(command)
37
+ @seen_options = {} # Every new command resets the seen options
38
+ while arg = @argv.first
39
+ if arg == "--"
40
+ @argv.shift
41
+ break
42
+ elsif arg.start_with?("-")
43
+ parse_option(command)
44
+ elsif cmd = command.grammar.commands[arg]
45
+ @argv.shift
46
+ command.command = Ast::Command.new(cmd, arg)
47
+ parse_command(command.command)
48
+ break
49
+ else
50
+ break
51
+ end
52
+ end
53
+ end
54
+
55
+ def parse_option(command)
56
+ # Split into name and argument
57
+ case @argv.first
58
+ when /^(--.+?)(?:=(.*))?$/
59
+ name, arg, short = $1, $2, false
60
+ when /^(-.)(.+)?$/
61
+ name, arg, short = $1, $2, true
62
+ end
63
+ @argv.shift
64
+
65
+ option = command.grammar.options[name] or raise Error, "Unknown option '#{name}'"
66
+ !@seen_options.key?(option.key) || option.repeated? or raise Error, "Duplicate option '#{name}'"
67
+ @seen_options[option.key] = true
68
+
69
+ # Parse (optional) argument
70
+ if option.argument?
71
+ if arg.nil? && !option.optional?
72
+ if !@argv.empty?
73
+ arg = @argv.shift
74
+ else
75
+ raise Error, "Missing argument for option '#{name}'"
76
+ end
77
+ end
78
+ arg &&= parse_arg(option, name, arg)
79
+ elsif arg && short
80
+ @argv.unshift("-#{arg}")
81
+ arg = nil
82
+ elsif !arg.nil?
83
+ raise Error, "No argument allowed for option '#{name}'"
84
+ end
85
+
86
+ command.options << Ast::Option.new(option, name, arg)
87
+ end
88
+
89
+ def parse_arg(option, name, arg)
90
+ if option.string?
91
+ arg
92
+ elsif arg == ""
93
+ nil
94
+ elsif option.integer?
95
+ arg =~ /^-?\d+$/ or raise Error, "Illegal integer in '#{name}' argument: '#{arg}'"
96
+ arg.to_i
97
+ else # option.float?
98
+ # https://stackoverflow.com/a/21891705/2130986
99
+ arg =~ /^[+-]?(?:0|[1-9]\d*)(?:\.(?:\d*[1-9]|0))?$/ or
100
+ raise Error, "Illegal float in '#{name}' argument: '#{arg}'"
101
+ arg.to_f
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,3 @@
1
+ module Shellopts
2
+ VERSION = "0.9.1"
3
+ end
data/lib/shellopts.rb ADDED
@@ -0,0 +1,195 @@
1
+ require "shellopts/version"
2
+
3
+ require 'shellopts/compiler.rb'
4
+ require 'shellopts/parser.rb'
5
+
6
+ # ShellOpts is a library for parsing command line options and sub-commands. The
7
+ # library API consists of the methods {ShellOpts.process}, {ShellOpts.error},
8
+ # and {ShellOpts.fail} and the result class {ShellOpts::ShellOpts}
9
+ #
10
+ module ShellOpts
11
+ # Process command line options and arguments. #process takes a usage string
12
+ # defining the options and the array of command line arguments to be parsed
13
+ # as arguments
14
+ #
15
+ # If called with a block, the block is called with name and value of each
16
+ # option or command and #process returns a list of remaining command line
17
+ # arguments. If called without a block a ShellOpts::ShellOpts object is
18
+ # returned
19
+ #
20
+ # The value of an option is its argument, the value of a command is an array
21
+ # of name/value pairs of options and subcommands. Option values are converted
22
+ # to the target type (String, Integer, Float) if specified
23
+ #
24
+ # Example
25
+ #
26
+ # # Define options
27
+ # USAGE = 'a,all g,global +v,verbose h,help save! snapshot f,file=FILE h,help'
28
+ #
29
+ # # Define defaults
30
+ # all = false
31
+ # global = false
32
+ # verbose = 0
33
+ # save = false
34
+ # snapshot = false
35
+ # file = nil
36
+ #
37
+ # # Process options
38
+ # argv = ShellOpts.process(USAGE, ARGV) do |name, value|
39
+ # case name
40
+ # when '-a', '--all'; all = true
41
+ # when '-g', '--global'; global = value
42
+ # when '-v', '--verbose'; verbose += 1
43
+ # when '-h', '--help'; print_help(); exit(0)
44
+ # when 'save'
45
+ # save = true
46
+ # value.each do |name, value|
47
+ # case name
48
+ # when '--snapshot'; snapshot = true
49
+ # when '-f', '--file'; file = value
50
+ # when '-h', '--help'; print_save_help(); exit(0)
51
+ # end
52
+ # end
53
+ # else
54
+ # raise "Not a user error. The developer forgot or misspelled an option"
55
+ # end
56
+ # end
57
+ #
58
+ # # Process remaining arguments
59
+ # argv.each { |arg| ... }
60
+ #
61
+ # If an error is encountered while compiling the usage string, a
62
+ # +ShellOpts::Compiler+ exception is raised. If the error happens while
63
+ # parsing the command line arguments, the program prints an error message and
64
+ # exits with status 1. Failed assertions raise a +ShellOpts::InternalError+
65
+ # exception
66
+ #
67
+ # Note that you can't process more than one command line at a time because
68
+ # #process saves a hidden {ShellOpts::ShellOpts} class variable used by the
69
+ # class methods #error and #fail. Call #reset to clear the global object if
70
+ # you really need to parse more than one command line. Alternatively you can
71
+ # create +ShellOpts::ShellOpts+ objects yourself and use the object methods
72
+ # #error and #fail instead:
73
+ #
74
+ # shellopts = ShellOpts::ShellOpts.new(USAGE, ARGS)
75
+ # shellopts.each { |name, value| ... }
76
+ # shellopts.args.each { |arg| ... }
77
+ # shellopts.error("Something went wrong")
78
+ #
79
+ def self.process(usage, argv, program_name: File.basename($0), &block)
80
+ if !block_given?
81
+ ShellOpts.new(usage, argv, program_name: program_name)
82
+ else
83
+ @shellopts.nil? or raise InternalError, "ShellOpts class variable already initialized"
84
+ @shellopts = ShellOpts.new(usage, argv, program_name: program_name)
85
+ @shellopts.each(&block)
86
+ @shellopts.args
87
+ end
88
+ end
89
+
90
+ # Reset the hidden +ShellOpts::ShellOpts+ class variable so that you can process
91
+ # another command line
92
+ def self.reset()
93
+ @shellopts = nil
94
+ end
95
+
96
+ # Print error message and usage string and exit with status 1. Can only be
97
+ # called after #process. Forwards to {::ShellOpts#error}
98
+ def self.error(*msgs)
99
+ @shellopts&.error(*msgs) or raise InternalError, "ShellOpts class variable not initialized"
100
+ end
101
+
102
+ # Print error message and exit with status 1. Forwards to {::ShellOpts#fail}
103
+ def self.fail(*msgs)
104
+ @shellopts&.fail(*msgs) or raise InternalError, "ShellOpts class variable not initialized"
105
+ end
106
+
107
+ # The compilation object
108
+ class ShellOpts
109
+ # Name of program
110
+ attr_reader :program_name
111
+
112
+ # Usage string. Shorthand for +grammar.usage+
113
+ def usage() @grammar.usage end
114
+
115
+ # The grammar compiled from the usage string. If #ast is defined, it's
116
+ # equal to ast.grammar
117
+ attr_reader :grammar
118
+
119
+ # The AST resulting from parsing the command line arguments
120
+ attr_reader :ast
121
+
122
+ # List of remaining non-option command line arguments. Shorthand for ast.arguments
123
+ def args() @ast.arguments end
124
+
125
+ # Compile a usage string into a grammar and use that to parse command line
126
+ # arguments
127
+ #
128
+ # +usage+ is the usage string, and +argv+ the command line (typically the
129
+ # global ARGV array). +program_name+ is the name of the program and is
130
+ # used in error messages. It defaults to the basename of the program
131
+ #
132
+ # Errors in the usage string raise a CompilerError exception. Errors in the
133
+ # argv arguments terminates the program with an error message
134
+ def initialize(usage, argv, program_name: File.basename($0))
135
+ @program_name = program_name
136
+ begin
137
+ @grammar = Grammar.compile(program_name, usage)
138
+ @ast = Ast.parse(@grammar, argv)
139
+ rescue Grammar::Compiler::Error => ex
140
+ raise CompilerError.new(5, ex.message)
141
+ rescue Ast::Parser::Error => ex
142
+ error(ex.message)
143
+ end
144
+ end
145
+
146
+ # Unroll the AST into a nested array
147
+ def to_a
148
+ @ast.values
149
+ end
150
+
151
+ # Iterate the result as name/value pairs. See {ShellOpts.process} for a
152
+ # detailed description
153
+ def each(&block)
154
+ if block_given?
155
+ to_a.each { |*args| yield(*args) }
156
+ else
157
+ to_a # FIXME: Iterator
158
+ end
159
+ end
160
+
161
+ # Print error message and usage string and exit with status 1. This method
162
+ # should be called in response to user-errors (eg. specifying an illegal
163
+ # option)
164
+ def error(*msgs)
165
+ $stderr.puts "#{program_name}: #{msgs.join}"
166
+ $stderr.puts "Usage: #{program_name} #{usage}"
167
+ exit 1
168
+ end
169
+
170
+ # Print error message and exit with status 1. This method should not be
171
+ # called in response to user-errors but system errors (like disk full)
172
+ def fail(*msgs)
173
+ $stderr.puts "#{program_name}: #{msgs.join}"
174
+ exit 1
175
+ end
176
+ end
177
+
178
+ # Base class for ShellOpts exceptions
179
+ class Error < RuntimeError; end
180
+
181
+ # Raised when an error is detected in the usage string
182
+ class CompilerError < Error
183
+ def initialize(start, message)
184
+ super(message)
185
+ set_backtrace(caller(start))
186
+ end
187
+ end
188
+
189
+ # Raised when an internal error is detected
190
+ class InternalError < Error; end
191
+
192
+ private
193
+ @shellopts = nil
194
+ end
195
+
data/shellopts.gemspec ADDED
@@ -0,0 +1,40 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "shellopts/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "shellopts"
8
+ spec.version = Shellopts::VERSION
9
+ spec.authors = ["Claus Rasmussen"]
10
+ spec.email = ["claus.l.rasmussen@gmail.com"]
11
+
12
+ spec.summary = %q{Parse command line options and arguments}
13
+ spec.description = %q{ShellOpts is a simple command line parsing libray
14
+ that covers most modern use cases incl. sub-commands.
15
+ Options and commands are specified using a
16
+ getopt(1)-like string that is interpreted by the
17
+ library to process the command line}
18
+ spec.homepage = "http://github.com/clrgit/shellopts"
19
+
20
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
21
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
22
+ if spec.respond_to?(:metadata)
23
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
24
+ else
25
+ raise "RubyGems 2.0+ is required to protect against public gem pushes"
26
+ end
27
+
28
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
29
+ f.match(%r{^(test|spec|features)/})
30
+ end
31
+ spec.bindir = "exe"
32
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
33
+ spec.require_paths = ["lib"]
34
+
35
+ spec.add_development_dependency "bundler", "~> 1.16"
36
+ spec.add_development_dependency "rake", "~> 10.0"
37
+ spec.add_development_dependency "rspec", "~> 3.0"
38
+ spec.add_development_dependency "indented_io"
39
+ spec.add_development_dependency "simplecov"
40
+ end