topt 0.0.1

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