topt 0.0.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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: dfc4bd34d4b41a577a80ec9e4b05bb518299d366
4
+ data.tar.gz: f8a0af259dc4659bddf91dabf79587e6c5383544
5
+ SHA512:
6
+ metadata.gz: 94c007f4b87d60f08688207a05b5aaa83ec4511adb2c47ab204212204c24a536461937b16a5f6ae82e746f711477587d59271cf05d6cc17b03bd6c13167601f2
7
+ data.tar.gz: 99e173354685f903170ee6a6112c4c8ca02775771f1c588f837d15b94482123eaf716619da3089db101d8db8a8b0635eb56320c1f94da8896c118af1fa234dea
@@ -0,0 +1,24 @@
1
+ Substantial portions of this code were extracted from Thor, which is
2
+ Copyright (c) 2008 Yehuda Katz, Eric Hodel, et al.
3
+
4
+ Portions of this code not extracted from Thor
5
+ Copyright (c) 2013 Martin Emde
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining
8
+ a copy of this software and associated documentation files (the
9
+ "Software"), to deal in the Software without restriction, including
10
+ without limitation the rights to use, copy, modify, merge, publish,
11
+ distribute, sublicense, and/or sell copies of the Software, and to
12
+ permit persons to whom the Software is furnished to do so, subject to
13
+ the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be
16
+ included in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,51 @@
1
+ # Topt
2
+
3
+ A Thor compatible replacement for Ruby's OptionParser (optparse).
4
+
5
+ Provides extended options parsing, compared to optparse, using a [Thor](https://github.com/wycats/thor) compatible option parser.
6
+
7
+ ## About
8
+
9
+ This is still a very fresh extraction. I haven't created any specs yet, so use with caution.
10
+
11
+ Open an issue if you want to use this but you're scared.
12
+
13
+ ## Example
14
+
15
+ See the examples directory from examples.
16
+
17
+ class Command
18
+ extend Topt
19
+
20
+ option :verbose, type: :boolean, aliases: %w[-v], :default => false, desc: "Be noisy"
21
+ argument :filename
22
+
23
+ def initialize(given_args=ARGV)
24
+ @arguments, @options, @extras = self.class.parse_options!(given_args)
25
+ rescue Topt::Error => e
26
+ puts e.message
27
+ puts "Usage: command [--verbose] filename"
28
+ exit 1
29
+ end
30
+
31
+ def call
32
+ filename = @arguments[:filename]
33
+ puts "Opening file #{filename}" if @options[:verbose]
34
+
35
+ File.open(filename) do |f|
36
+ f.each_line do |line|
37
+ puts "outputting line #{line}" if @options[:verbose]
38
+ puts line
39
+ end
40
+ end
41
+
42
+ puts "Done" if @options[:verbose]
43
+ end
44
+ end
45
+
46
+ Command.new(ARGV).call
47
+
48
+ ## Credit
49
+
50
+ A substantial portion of this code is extracted directly from [Thor](https://github.com/wycats/thor)
51
+ by Yehuda Katz, José Valim, and more.
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift(File.expand_path('../lib', File.dirname(__FILE__)))
4
+
5
+ require 'topt'
6
+
7
+ class Kat
8
+ extend Topt
9
+
10
+ option :blank, type: :boolean, aliases: %w[-b],
11
+ desc: "Number the non-blank output lines, starting at 1."
12
+
13
+ option :number, type: :boolean, aliases: %w[-n],
14
+ desc: "Number the output lines, starting at 1."
15
+
16
+ # If we only wanted to accept one file, we could use this and then access it
17
+ # with @arguments[:filename], but we'd rather accept many files, so we'll
18
+ # use extras to collect extra command line args
19
+ #
20
+ # argument :filename
21
+
22
+ def initialize(given_args=ARGV)
23
+ @options, @arguments, @extras = self.class.parse_options!(given_args)
24
+ end
25
+
26
+ def call
27
+ if @extras.empty?
28
+ output $stdin
29
+ else
30
+ @extras.each do |file|
31
+ File.open(file) do |f|
32
+ output f
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ def output(input)
39
+ num = 1
40
+ input.each_line do |line|
41
+ if (@options[:number] || @options[:blank]) && !(@options[:blank] && line.strip.empty?)
42
+ puts "#{num.to_s.rjust(6)}\t#{line}"
43
+ num += 1
44
+ else
45
+ puts line
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ Kat.new(ARGV).call
@@ -0,0 +1,53 @@
1
+ require 'topt/parser'
2
+
3
+ # Extend your class with this module to add Thor-like options methods.
4
+ #
5
+ # This currently does not do any magical method based things, so you'll
6
+ # need to do one command per class or instantiate the parser directly.
7
+ #
8
+ # Instantiating directly can really make your command easy to understand,
9
+ # since there is very little magic going on at that point.
10
+ module Topt
11
+ class Error < StandardError
12
+ end
13
+
14
+ class UnknownArgumentError < Error
15
+ end
16
+
17
+ class RequiredArgumentMissingError < Error
18
+ end
19
+
20
+ class MalformattedArgumentError < Error
21
+ end
22
+
23
+ def options_parser
24
+ @parser ||= Parser.new
25
+ end
26
+
27
+ def options
28
+ options_parser.options
29
+ end
30
+ alias_method :method_options, :options # Thor compatibility
31
+
32
+ def option(name, options)
33
+ options_parser.option(name, options)
34
+ end
35
+ alias_method :method_option, :option # Thor compatibility
36
+
37
+ def arguments
38
+ options_parser.arguments
39
+ end
40
+
41
+ def argument(name, options={})
42
+ attr_accessor name
43
+ options_parser.argument(name, options)
44
+ end
45
+
46
+ def remove_argument(*names)
47
+ options_parser.remove_argument(*nname)
48
+ end
49
+
50
+ def parse_options!(given_args=ARGV, defaults_hash = {})
51
+ options_parser.parse(given_args, defaults_hash)
52
+ end
53
+ end
@@ -0,0 +1,74 @@
1
+ module Topt
2
+ class Argument #:nodoc:
3
+ VALID_TYPES = [ :numeric, :hash, :array, :string ]
4
+
5
+ attr_reader :name, :description, :enum, :required, :type, :default, :banner
6
+ alias :human_name :name
7
+
8
+ def initialize(name, options={})
9
+ class_name = self.class.name.split("::").last
10
+
11
+ type = options[:type]
12
+
13
+ raise ArgumentError, "#{class_name} name can't be nil." if name.nil?
14
+ raise ArgumentError, "Type :#{type} is not valid for #{class_name.downcase}s." if type && !valid_type?(type)
15
+
16
+ @name = name.to_s
17
+ @description = options[:desc]
18
+ @required = options.key?(:required) ? options[:required] : true
19
+ @type = (type || :string).to_sym
20
+ @default = options[:default]
21
+ @banner = options[:banner] || default_banner
22
+ @enum = options[:enum]
23
+
24
+ validate! # Trigger specific validations
25
+ end
26
+
27
+ def usage
28
+ required? ? banner : "[#{banner}]"
29
+ end
30
+
31
+ def required?
32
+ required
33
+ end
34
+
35
+ def show_default?
36
+ case default
37
+ when Array, String, Hash
38
+ !default.empty?
39
+ else
40
+ default
41
+ end
42
+ end
43
+
44
+ protected
45
+
46
+ def validate!
47
+ if required? && !default.nil?
48
+ raise ArgumentError, "An argument cannot be required and have default value."
49
+ elsif @enum && !@enum.is_a?(Array)
50
+ raise ArgumentError, "An argument cannot have an enum other than an array."
51
+ end
52
+ end
53
+
54
+ def valid_type?(type)
55
+ self.class::VALID_TYPES.include?(type.to_sym)
56
+ end
57
+
58
+ def default_banner
59
+ case type
60
+ when :boolean
61
+ nil
62
+ when :string, :default
63
+ human_name.upcase
64
+ when :numeric
65
+ "N"
66
+ when :hash
67
+ "key:value"
68
+ when :array
69
+ "one two three"
70
+ end
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,174 @@
1
+ module Topt
2
+ class Arguments #:nodoc:
3
+ NUMERIC = /(\d*\.\d+|\d+)/
4
+
5
+ # Receives an array of args and returns two arrays, one with arguments
6
+ # and one with switches.
7
+ #
8
+ def self.split(args)
9
+ arguments = []
10
+
11
+ args.each do |item|
12
+ break if item =~ /^-/
13
+ arguments << item
14
+ end
15
+
16
+ return arguments, args[Range.new(arguments.size, -1)]
17
+ end
18
+
19
+ def self.parse(*args)
20
+ to_parse = args.pop
21
+ new(*args).parse(to_parse)
22
+ end
23
+
24
+ # Takes an array of Thor::Argument objects.
25
+ #
26
+ def initialize(arguments=[])
27
+ @assigns, @non_assigned_required = {}, []
28
+ @switches = arguments
29
+
30
+ arguments.each do |argument|
31
+ if argument.default != nil
32
+ @assigns[argument.human_name] = argument.default
33
+ elsif argument.required?
34
+ @non_assigned_required << argument
35
+ end
36
+ end
37
+ end
38
+
39
+ def parse(args)
40
+ @pile = args.dup
41
+
42
+ @switches.each do |argument|
43
+ break unless peek
44
+ @non_assigned_required.delete(argument)
45
+ @assigns[argument.human_name] = send(:"parse_#{argument.type}", argument.human_name)
46
+ end
47
+
48
+ check_requirement!
49
+
50
+ assigns = Topt::HashWithIndifferentAccess.new(@assigns)
51
+ assigns.freeze
52
+ assigns
53
+ end
54
+
55
+ def remaining
56
+ @pile
57
+ end
58
+
59
+ private
60
+
61
+ def no_or_skip?(arg)
62
+ arg =~ /^--(no|skip)-([-\w]+)$/
63
+ $2
64
+ end
65
+
66
+ def last?
67
+ @pile.empty?
68
+ end
69
+
70
+ def peek
71
+ @pile.first
72
+ end
73
+
74
+ def shift
75
+ @pile.shift
76
+ end
77
+
78
+ def unshift(arg)
79
+ unless arg.kind_of?(Array)
80
+ @pile.unshift(arg)
81
+ else
82
+ @pile = arg + @pile
83
+ end
84
+ end
85
+
86
+ def current_is_value?
87
+ peek && peek.to_s !~ /^-/
88
+ end
89
+
90
+ # Runs through the argument array getting strings that contains ":" and
91
+ # mark it as a hash:
92
+ #
93
+ # [ "name:string", "age:integer" ]
94
+ #
95
+ # Becomes:
96
+ #
97
+ # { "name" => "string", "age" => "integer" }
98
+ #
99
+ def parse_hash(name)
100
+ return shift if peek.is_a?(Hash)
101
+ hash = {}
102
+
103
+ while current_is_value? && peek.include?(?:)
104
+ key, value = shift.split(':',2)
105
+ hash[key] = value
106
+ end
107
+ hash
108
+ end
109
+
110
+ # Runs through the argument array getting all strings until no string is
111
+ # found or a switch is found.
112
+ #
113
+ # ["a", "b", "c"]
114
+ #
115
+ # And returns it as an array:
116
+ #
117
+ # ["a", "b", "c"]
118
+ #
119
+ def parse_array(name)
120
+ return shift if peek.is_a?(Array)
121
+ array = []
122
+
123
+ while current_is_value?
124
+ array << shift
125
+ end
126
+ array
127
+ end
128
+
129
+ # Check if the peek is numeric format and return a Float or Integer.
130
+ # Otherwise raises an error.
131
+ #
132
+ def parse_numeric(name)
133
+ return shift if peek.is_a?(Numeric)
134
+
135
+ unless peek =~ NUMERIC && $& == peek
136
+ raise MalformattedArgumentError, "Expected numeric value for '#{name}'; got #{peek.inspect}"
137
+ end
138
+
139
+ $&.index('.') ? shift.to_f : shift.to_i
140
+ end
141
+
142
+ # Parse string:
143
+ # for --string-arg, just return the current value in the pile
144
+ # for --no-string-arg, nil
145
+ #
146
+ def parse_string(name)
147
+ if no_or_skip?(name)
148
+ nil
149
+ else
150
+ value = shift
151
+ if @switches.is_a?(Hash) && switch = @switches[name]
152
+ if switch.enum && !switch.enum.include?(value)
153
+ raise MalformattedArgumentError, "Expected '#{name}' to be one of #{switch.enum.join(', ')}; got #{value}"
154
+ end
155
+ end
156
+ value
157
+ end
158
+ end
159
+
160
+ # Raises an error if @non_assigned_required array is not empty.
161
+ #
162
+ def check_requirement!
163
+ unless @non_assigned_required.empty?
164
+ names = @non_assigned_required.map do |o|
165
+ o.respond_to?(:switch_name) ? o.switch_name : o.human_name
166
+ end.join("', '")
167
+
168
+ class_name = self.class.name.split('::').last.downcase
169
+ raise RequiredArgumentMissingError, "No value provided for required #{class_name} '#{names}'"
170
+ end
171
+ end
172
+
173
+ end
174
+ end
@@ -0,0 +1,71 @@
1
+ module Topt
2
+ # A hash with indifferent access and magic predicates.
3
+ #
4
+ # hash = Thor::CoreExt::HashWithIndifferentAccess.new 'foo' => 'bar', 'baz' => 'bee', 'force' => true
5
+ #
6
+ # hash[:foo] #=> 'bar'
7
+ # hash['foo'] #=> 'bar'
8
+ # hash.foo? #=> true
9
+ #
10
+ class HashWithIndifferentAccess < ::Hash #:nodoc:
11
+
12
+ def initialize(hash={})
13
+ super()
14
+ hash.each do |key, value|
15
+ self[convert_key(key)] = value
16
+ end
17
+ end
18
+
19
+ def [](key)
20
+ super(convert_key(key))
21
+ end
22
+
23
+ def []=(key, value)
24
+ super(convert_key(key), value)
25
+ end
26
+
27
+ def delete(key)
28
+ super(convert_key(key))
29
+ end
30
+
31
+ def values_at(*indices)
32
+ indices.collect { |key| self[convert_key(key)] }
33
+ end
34
+
35
+ def merge(other)
36
+ dup.merge!(other)
37
+ end
38
+
39
+ def merge!(other)
40
+ other.each do |key, value|
41
+ self[convert_key(key)] = value
42
+ end
43
+ self
44
+ end
45
+
46
+ protected
47
+
48
+ def convert_key(key)
49
+ key.is_a?(Symbol) ? key.to_s : key
50
+ end
51
+
52
+ # Magic predicates. For instance:
53
+ #
54
+ # options.force? # => !!options['force']
55
+ # options.shebang # => "/usr/lib/local/ruby"
56
+ # options.test_framework?(:rspec) # => options[:test_framework] == :rspec
57
+ #
58
+ def method_missing(method, *args, &block)
59
+ method = method.to_s
60
+ if method =~ /^(\w+)\?$/
61
+ if args.empty?
62
+ !!self[$1]
63
+ else
64
+ self[$1] == args.first
65
+ end
66
+ else
67
+ self[method]
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,123 @@
1
+ require 'topt/argument'
2
+
3
+ module Topt
4
+ class Option < Argument #:nodoc:
5
+ attr_reader :aliases, :group, :lazy_default, :hide
6
+
7
+ VALID_TYPES = [:boolean, :numeric, :hash, :array, :string]
8
+
9
+ def initialize(name, options={})
10
+ options[:required] = false unless options.key?(:required)
11
+ super
12
+ @lazy_default = options[:lazy_default]
13
+ @group = options[:group].to_s.capitalize if options[:group]
14
+ @aliases = Array(options[:aliases])
15
+ @hide = options[:hide]
16
+ end
17
+
18
+ # This parse quick options given as method_options. It makes several
19
+ # assumptions, but you can be more specific using the option method.
20
+ #
21
+ # parse :foo => "bar"
22
+ # #=> Option foo with default value bar
23
+ #
24
+ # parse [:foo, :baz] => "bar"
25
+ # #=> Option foo with default value bar and alias :baz
26
+ #
27
+ # parse :foo => :required
28
+ # #=> Required option foo without default value
29
+ #
30
+ # parse :foo => 2
31
+ # #=> Option foo with default value 2 and type numeric
32
+ #
33
+ # parse :foo => :numeric
34
+ # #=> Option foo without default value and type numeric
35
+ #
36
+ # parse :foo => true
37
+ # #=> Option foo with default value true and type boolean
38
+ #
39
+ # The valid types are :boolean, :numeric, :hash, :array and :string. If none
40
+ # is given a default type is assumed. This default type accepts arguments as
41
+ # string (--foo=value) or booleans (just --foo).
42
+ #
43
+ # By default all options are optional, unless :required is given.
44
+ #
45
+ def self.parse(key, value)
46
+ if key.is_a?(Array)
47
+ name, *aliases = key
48
+ else
49
+ name, aliases = key, []
50
+ end
51
+
52
+ name = name.to_s
53
+ default = value
54
+
55
+ type = case value
56
+ when Symbol
57
+ default = nil
58
+ if VALID_TYPES.include?(value)
59
+ value
60
+ elsif required = (value == :required)
61
+ :string
62
+ end
63
+ when TrueClass, FalseClass
64
+ :boolean
65
+ when Numeric
66
+ :numeric
67
+ when Hash, Array, String
68
+ value.class.name.downcase.to_sym
69
+ end
70
+ self.new(name.to_s, :required => required, :type => type, :default => default, :aliases => aliases)
71
+ end
72
+
73
+ def switch_name
74
+ @switch_name ||= dasherized? ? name : dasherize(name)
75
+ end
76
+
77
+ def human_name
78
+ @human_name ||= dasherized? ? undasherize(name) : name
79
+ end
80
+
81
+ def usage(padding=0)
82
+ sample = if banner && !banner.to_s.empty?
83
+ "#{switch_name}=#{banner}"
84
+ else
85
+ switch_name
86
+ end
87
+
88
+ sample = "[#{sample}]" unless required?
89
+
90
+ if aliases.empty?
91
+ (" " * padding) << sample
92
+ else
93
+ "#{aliases.join(', ')}, #{sample}"
94
+ end
95
+ end
96
+
97
+ VALID_TYPES.each do |type|
98
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
99
+ def #{type}?
100
+ self.type == #{type.inspect}
101
+ end
102
+ RUBY
103
+ end
104
+
105
+ protected
106
+
107
+ def validate!
108
+ raise ArgumentError, "An option cannot be boolean and required." if boolean? && required?
109
+ end
110
+
111
+ def dasherized?
112
+ name.index('-') == 0
113
+ end
114
+
115
+ def undasherize(str)
116
+ str.sub(/^-{1,2}/, '')
117
+ end
118
+
119
+ def dasherize(str)
120
+ (str.length > 1 ? "--" : "-") + str.gsub('_', '-')
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,182 @@
1
+ require 'topt/arguments'
2
+ require 'topt/hash_with_indifferent_access'
3
+
4
+ module Topt
5
+ class Options < Arguments #:nodoc:
6
+ LONG_RE = /^(--\w+(?:-\w+)*)$/
7
+ SHORT_RE = /^(-[a-z])$/i
8
+ EQ_RE = /^(--\w+(?:-\w+)*|-[a-z])=(.*)$/i
9
+ SHORT_SQ_RE = /^-([a-z]{2,})$/i # Allow either -x -v or -xv style for single char args
10
+ SHORT_NUM = /^(-[a-z])#{NUMERIC}$/i
11
+
12
+ # Receives a hash and makes it switches.
13
+ def self.to_switches(options)
14
+ options.map do |key, value|
15
+ case value
16
+ when true
17
+ "--#{key}"
18
+ when Array
19
+ "--#{key} #{value.map{ |v| v.inspect }.join(' ')}"
20
+ when Hash
21
+ "--#{key} #{value.map{ |k,v| "#{k}:#{v}" }.join(' ')}"
22
+ when nil, false
23
+ ""
24
+ else
25
+ "--#{key} #{value.inspect}"
26
+ end
27
+ end.join(" ")
28
+ end
29
+
30
+ # Takes a hash of Thor::Option and a hash with defaults.
31
+ def initialize(hash_options={}, defaults={})
32
+ options = hash_options.values
33
+ super(options)
34
+
35
+ # Add defaults
36
+ defaults.each do |key, value|
37
+ @assigns[key.to_s] = value
38
+ @non_assigned_required.delete(hash_options[key])
39
+ end
40
+
41
+ @shorts, @switches, @extra = {}, {}, []
42
+
43
+ options.each do |option|
44
+ @switches[option.switch_name] = option
45
+
46
+ option.aliases.each do |short|
47
+ name = short.to_s.sub(/^(?!\-)/, '-')
48
+ @shorts[name] ||= option.switch_name
49
+ end
50
+ end
51
+ end
52
+
53
+ def remaining
54
+ @extra
55
+ end
56
+
57
+ def parse(args)
58
+ @pile = args.dup
59
+
60
+ while peek
61
+ match, is_switch = current_is_switch?
62
+ shifted = shift
63
+
64
+ if is_switch
65
+ case shifted
66
+ when SHORT_SQ_RE
67
+ unshift($1.split('').map { |f| "-#{f}" })
68
+ next
69
+ when EQ_RE, SHORT_NUM
70
+ unshift($2)
71
+ switch = $1
72
+ when LONG_RE, SHORT_RE
73
+ switch = $1
74
+ end
75
+
76
+ switch = normalize_switch(switch)
77
+ option = switch_option(switch)
78
+ @assigns[option.human_name] = parse_peek(switch, option)
79
+ elsif match
80
+ @extra << shifted
81
+ @extra << shift while peek && peek !~ /^-/
82
+ else
83
+ @extra << shifted
84
+ end
85
+ end
86
+
87
+ check_requirement!
88
+
89
+ assigns = Topt::HashWithIndifferentAccess.new(@assigns)
90
+ assigns.freeze
91
+ assigns
92
+ end
93
+
94
+ def check_unknown!
95
+ # an unknown option starts with - or -- and has no more --'s afterward.
96
+ unknown = @extra.select { |str| str =~ /^--?(?:(?!--).)*$/ }
97
+ raise UnknownArgumentError, "Unknown switches '#{unknown.join(', ')}'" unless unknown.empty?
98
+ end
99
+
100
+ protected
101
+
102
+ # Returns true if the current value in peek is a registered switch.
103
+ #
104
+ def current_is_switch?
105
+ case peek
106
+ when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM
107
+ [true, switch?($1)]
108
+ when SHORT_SQ_RE
109
+ [true, $1.split('').any? { |f| switch?("-#{f}") }]
110
+ else
111
+ [false, false]
112
+ end
113
+ end
114
+
115
+ def current_is_switch_formatted?
116
+ case peek
117
+ when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM, SHORT_SQ_RE
118
+ true
119
+ else
120
+ false
121
+ end
122
+ end
123
+
124
+ def switch?(arg)
125
+ switch_option(normalize_switch(arg))
126
+ end
127
+
128
+ def switch_option(arg)
129
+ if match = no_or_skip?(arg)
130
+ @switches[arg] || @switches["--#{match}"]
131
+ else
132
+ @switches[arg]
133
+ end
134
+ end
135
+
136
+ # Check if the given argument is actually a shortcut.
137
+ #
138
+ def normalize_switch(arg)
139
+ (@shorts[arg] || arg).tr('_', '-')
140
+ end
141
+
142
+ # Parse boolean values which can be given as --foo=true, --foo or --no-foo.
143
+ #
144
+ def parse_boolean(switch)
145
+ if current_is_value?
146
+ if ["true", "TRUE", "t", "T", true].include?(peek)
147
+ shift
148
+ true
149
+ elsif ["false", "FALSE", "f", "F", false].include?(peek)
150
+ shift
151
+ false
152
+ else
153
+ true
154
+ end
155
+ else
156
+ @switches.key?(switch) || !no_or_skip?(switch)
157
+ end
158
+ end
159
+
160
+ # Parse the value at the peek analyzing if it requires an input or not.
161
+ #
162
+ def parse_peek(switch, option)
163
+ if current_is_switch_formatted? || last?
164
+ if option.boolean?
165
+ # No problem for boolean types
166
+ elsif no_or_skip?(switch)
167
+ return nil # User set value to nil
168
+ elsif option.string? && !option.required?
169
+ # Return the default if there is one, else the human name
170
+ return option.lazy_default || option.default || option.human_name
171
+ elsif option.lazy_default
172
+ return option.lazy_default
173
+ else
174
+ raise MalformattedArgumentError, "No value provided for option '#{switch}'"
175
+ end
176
+ end
177
+
178
+ @non_assigned_required.delete(option)
179
+ send(:"parse_#{option.type}", switch)
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,153 @@
1
+ require 'topt/arguments'
2
+ require 'topt/argument'
3
+ require 'topt/options'
4
+ require 'topt/option'
5
+
6
+ module Topt
7
+ class Parser
8
+ attr_reader :options
9
+ attr_reader :arguments
10
+
11
+ # Create a parser for parsing ARGV input into options.
12
+ #
13
+ def initialize
14
+ @options = {}
15
+ @arguments = []
16
+ yield self if block_given?
17
+ end
18
+
19
+ # Adds an option to the set of method options. If :for is given as option,
20
+ # it allows you to change the options from a previous defined task.
21
+ #
22
+ # parser.option :foo => :bar
23
+ #
24
+ # ==== Parameters
25
+ # name<Symbol>:: The name of the argument.
26
+ # options<Hash>:: Described below.
27
+ #
28
+ # ==== Options
29
+ # :desc - Description for the argument.
30
+ # :required - If the argument is required or not.
31
+ # :default - Default value for this argument. It cannot be required and have default values.
32
+ # :aliases - Aliases for this option.
33
+ # :type - The type of the argument, can be :string, :hash, :array, :numeric or :boolean.
34
+ # :banner - String to show on usage notes.
35
+ # :hide - If you want to hide this option from the help.
36
+ #
37
+ def option(name, opts)
38
+ @options[name] = Topt::Option.new(name, opts)
39
+ end
40
+
41
+ # Adds an argument to the class and creates an attr_accessor for it.
42
+ #
43
+ # Arguments are different from options in several aspects. The first one
44
+ # is how they are parsed from the command line, arguments are retrieved
45
+ # from position:
46
+ #
47
+ # thor task NAME
48
+ #
49
+ # Instead of:
50
+ #
51
+ # thor task --name=NAME
52
+ #
53
+ # Besides, arguments are used inside your code as an accessor (self.argument),
54
+ # while options are all kept in a hash (self.options).
55
+ #
56
+ # Finally, arguments cannot have type :default or :boolean but can be
57
+ # optional (supplying :optional => :true or :required => false), although
58
+ # you cannot have a required argument after a non-required argument. If you
59
+ # try it, an error is raised.
60
+ #
61
+ # ==== Parameters
62
+ # name<Symbol>:: The name of the argument.
63
+ # options<Hash>:: Described below.
64
+ #
65
+ # ==== Options
66
+ # :desc - Description for the argument.
67
+ # :required - If the argument is required or not.
68
+ # :optional - If the argument is optional or not.
69
+ # :type - The type of the argument, can be :string, :hash, :array, :numeric.
70
+ # :default - Default value for this argument. It cannot be required and have default values.
71
+ # :banner - String to show on usage notes.
72
+ #
73
+ # ==== Errors
74
+ # ArgumentError:: Raised if you supply a required argument after a non required one.
75
+ #
76
+ def argument(name, opts={})
77
+ required = if opts.key?(:optional)
78
+ !opts[:optional]
79
+ elsif opts.key?(:required)
80
+ opts[:required]
81
+ else
82
+ opts[:default].nil?
83
+ end
84
+
85
+ remove_argument name
86
+
87
+ if required
88
+ @arguments.each do |argument|
89
+ next if argument.required?
90
+ raise ArgumentError,
91
+ "You cannot have #{name.to_s.inspect} as required argument after " \
92
+ "the non-required argument #{argument.human_name.inspect}."
93
+ end
94
+ end
95
+
96
+ opts[:required] = required
97
+
98
+ @arguments << Topt::Argument.new(name, opts)
99
+ end
100
+
101
+ # Removes a previous defined argument.
102
+ #
103
+ # ==== Parameters
104
+ # names<Array>:: Arguments to be removed
105
+ #
106
+ # ==== Examples
107
+ #
108
+ # parser.remove_argument :foo
109
+ # parser.remove_argument :foo, :bar, :baz
110
+ #
111
+ def remove_argument(*names)
112
+ names.each do |name|
113
+ @arguments.delete_if { |a| a.name == name.to_s }
114
+ end
115
+ end
116
+
117
+ # Parse the given argv-style arguments and return options, arguments,
118
+ # and any remaining unnamed arguments
119
+ def parse(given_args=ARGV, defaults_hash = {})
120
+ # split inbound arguments at the first argument
121
+ # that looks like an option (starts with - or --).
122
+ argv_args, argv_switches = Topt::Options.split(given_args.dup)
123
+
124
+ # Let Thor::Options parse the options first, so it can remove
125
+ # declared options from the array. This will leave us with
126
+ # a list of arguments that weren't declared.
127
+ parsed_options, remaining = parse_options(argv_switches, defaults_hash)
128
+
129
+ # Add the remaining arguments from the options parser to the
130
+ # arguments from argv_args. Then remove any positional
131
+ # arguments declared using #argument. This will leave us with
132
+ # the remaining positional arguments.
133
+ to_parse = argv_args + remaining
134
+ parsed_arguments, parsed_remaining = parse_arguments(to_parse)
135
+
136
+ [parsed_options, parsed_arguments, parsed_remaining]
137
+ end
138
+
139
+ # Parse option switches array into options and remaining non-option
140
+ # positional arguments.
141
+ def parse_options(argv_switches, defaults_hash)
142
+ options_parser = Topt::Options.new(@options, defaults_hash)
143
+ [options_parser.parse(argv_switches), options_parser.remaining]
144
+ end
145
+
146
+ # Parse declared arguments from the given argument array, returning
147
+ # a hash of argument name and values, and an array of remaining args.
148
+ def parse_arguments(to_parse)
149
+ args_parser = Topt::Arguments.new(@arguments)
150
+ [args_parser.parse(to_parse), args_parser.remaining]
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,3 @@
1
+ module Topt
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/topt/version', __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.add_development_dependency 'bundler', '~> 1.0'
6
+ s.add_development_dependency 'fakeweb', '~> 1.3'
7
+ s.add_development_dependency 'rake', '~> 0.9'
8
+ s.add_development_dependency 'rdoc', '~> 3.9'
9
+ s.add_development_dependency 'rspec', '~> 2.3'
10
+ s.add_development_dependency 'simplecov', '~> 0.4'
11
+ #s.add_development_dependency 'childlabor'
12
+ s.authors = ['Yehuda Katz', 'José Valim', 'Martin Emde']
13
+ s.description = %q{Thor compatible command line option parser. A replacement for Ruby's OptionParser (optparse).}
14
+ s.summary = <<-SUMMARY
15
+ A replacement for OptionParser ('optparse') and Thor that supports extended options parsing based on Thor's parsing rules.
16
+ The goal of topt is a drop in replacement for Thor command line parsing where backwards compatibility is important.
17
+ SUMMARY
18
+ s.email = 'me@martinemde.com'
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{|f| File.basename(f)}
20
+ s.extra_rdoc_files = ['LICENSE.md', 'README.md']
21
+ s.license = 'MIT'
22
+ s.files = `git ls-files`.split("\n")
23
+ s.homepage = 'https://github.com/martinemde/topt'
24
+ s.name = 'topt'
25
+ s.rdoc_options = ['--charset=UTF-8']
26
+ s.require_paths = ['lib']
27
+ s.required_rubygems_version = Gem::Requirement.new('>= 1.3.6')
28
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
29
+ s.version = Topt::VERSION
30
+ end
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: topt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Yehuda Katz
8
+ - José Valim
9
+ - Martin Emde
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2014-09-30 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: bundler
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - "~>"
20
+ - !ruby/object:Gem::Version
21
+ version: '1.0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '1.0'
29
+ - !ruby/object:Gem::Dependency
30
+ name: fakeweb
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - "~>"
34
+ - !ruby/object:Gem::Version
35
+ version: '1.3'
36
+ type: :development
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - "~>"
41
+ - !ruby/object:Gem::Version
42
+ version: '1.3'
43
+ - !ruby/object:Gem::Dependency
44
+ name: rake
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '0.9'
50
+ type: :development
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - "~>"
55
+ - !ruby/object:Gem::Version
56
+ version: '0.9'
57
+ - !ruby/object:Gem::Dependency
58
+ name: rdoc
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: '3.9'
64
+ type: :development
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - "~>"
69
+ - !ruby/object:Gem::Version
70
+ version: '3.9'
71
+ - !ruby/object:Gem::Dependency
72
+ name: rspec
73
+ requirement: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - "~>"
76
+ - !ruby/object:Gem::Version
77
+ version: '2.3'
78
+ type: :development
79
+ prerelease: false
80
+ version_requirements: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - "~>"
83
+ - !ruby/object:Gem::Version
84
+ version: '2.3'
85
+ - !ruby/object:Gem::Dependency
86
+ name: simplecov
87
+ requirement: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - "~>"
90
+ - !ruby/object:Gem::Version
91
+ version: '0.4'
92
+ type: :development
93
+ prerelease: false
94
+ version_requirements: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - "~>"
97
+ - !ruby/object:Gem::Version
98
+ version: '0.4'
99
+ description: Thor compatible command line option parser. A replacement for Ruby's
100
+ OptionParser (optparse).
101
+ email: me@martinemde.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files:
105
+ - LICENSE.md
106
+ - README.md
107
+ files:
108
+ - LICENSE.md
109
+ - README.md
110
+ - examples/kat
111
+ - lib/topt.rb
112
+ - lib/topt/argument.rb
113
+ - lib/topt/arguments.rb
114
+ - lib/topt/hash_with_indifferent_access.rb
115
+ - lib/topt/option.rb
116
+ - lib/topt/options.rb
117
+ - lib/topt/parser.rb
118
+ - lib/topt/version.rb
119
+ - topt.gemspec
120
+ homepage: https://github.com/martinemde/topt
121
+ licenses:
122
+ - MIT
123
+ metadata: {}
124
+ post_install_message:
125
+ rdoc_options:
126
+ - "--charset=UTF-8"
127
+ require_paths:
128
+ - lib
129
+ required_ruby_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: 1.3.6
139
+ requirements: []
140
+ rubyforge_project:
141
+ rubygems_version: 2.2.2
142
+ signing_key:
143
+ specification_version: 4
144
+ summary: A replacement for OptionParser ('optparse') and Thor that supports extended
145
+ options parsing based on Thor's parsing rules. The goal of topt is a drop in replacement
146
+ for Thor command line parsing where backwards compatibility is important.
147
+ test_files: []