shellopts 0.9.1

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