clive 0.8.1 → 1.0.0

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.
Files changed (52) hide show
  1. data/LICENSE +1 -1
  2. data/README.md +328 -227
  3. data/lib/clive.rb +130 -50
  4. data/lib/clive/argument.rb +170 -0
  5. data/lib/clive/arguments.rb +139 -0
  6. data/lib/clive/arguments/parser.rb +210 -0
  7. data/lib/clive/base.rb +189 -0
  8. data/lib/clive/command.rb +342 -444
  9. data/lib/clive/error.rb +66 -0
  10. data/lib/clive/formatter.rb +57 -141
  11. data/lib/clive/formatter/colour.rb +37 -0
  12. data/lib/clive/formatter/plain.rb +172 -0
  13. data/lib/clive/option.rb +185 -75
  14. data/lib/clive/option/runner.rb +163 -0
  15. data/lib/clive/output.rb +141 -16
  16. data/lib/clive/parser.rb +180 -87
  17. data/lib/clive/struct_hash.rb +109 -0
  18. data/lib/clive/type.rb +117 -0
  19. data/lib/clive/type/definitions.rb +170 -0
  20. data/lib/clive/type/lookup.rb +23 -0
  21. data/lib/clive/version.rb +3 -3
  22. data/spec/clive/a_cli_spec.rb +245 -0
  23. data/spec/clive/argument_spec.rb +148 -0
  24. data/spec/clive/arguments/parser_spec.rb +35 -0
  25. data/spec/clive/arguments_spec.rb +191 -0
  26. data/spec/clive/command_spec.rb +276 -209
  27. data/spec/clive/formatter/colour_spec.rb +129 -0
  28. data/spec/clive/formatter/plain_spec.rb +129 -0
  29. data/spec/clive/option/runner_spec.rb +92 -0
  30. data/spec/clive/option_spec.rb +149 -23
  31. data/spec/clive/output_spec.rb +86 -2
  32. data/spec/clive/parser_spec.rb +201 -81
  33. data/spec/clive/struct_hash_spec.rb +82 -0
  34. data/spec/clive/type/definitions_spec.rb +312 -0
  35. data/spec/clive/type_spec.rb +107 -0
  36. data/spec/clive_spec.rb +60 -0
  37. data/spec/extras/expectations.rb +86 -0
  38. data/spec/extras/focus.rb +22 -0
  39. data/spec/helper.rb +35 -0
  40. metadata +56 -36
  41. data/lib/clive/bool.rb +0 -67
  42. data/lib/clive/exceptions.rb +0 -54
  43. data/lib/clive/flag.rb +0 -199
  44. data/lib/clive/switch.rb +0 -31
  45. data/lib/clive/tokens.rb +0 -141
  46. data/spec/clive/bool_spec.rb +0 -54
  47. data/spec/clive/flag_spec.rb +0 -117
  48. data/spec/clive/formatter_spec.rb +0 -108
  49. data/spec/clive/switch_spec.rb +0 -14
  50. data/spec/clive/tokens_spec.rb +0 -38
  51. data/spec/shared_specs.rb +0 -16
  52. data/spec/spec_helper.rb +0 -12
@@ -1,64 +1,144 @@
1
- require 'ast_ast'
2
- require 'attr_plus'
1
+ $: << File.dirname(__FILE__)
3
2
 
4
- require 'clive/parser'
5
- require 'clive/exceptions'
6
- require 'clive/tokens'
3
+ require 'clive/error'
4
+ require 'clive/output'
5
+ require 'clive/version'
6
+ require 'clive/struct_hash'
7
+ require 'clive/formatter'
8
+ require 'clive/formatter/plain'
9
+ require 'clive/formatter/colour'
7
10
 
11
+ require 'clive/type'
12
+ require 'clive/argument'
13
+ require 'clive/arguments'
14
+ require 'clive/arguments/parser'
15
+ require 'clive/option/runner'
8
16
  require 'clive/option'
9
17
  require 'clive/command'
10
- require 'clive/switch'
11
- require 'clive/flag'
12
- require 'clive/bool'
18
+ require 'clive/parser'
19
+ require 'clive/base'
13
20
 
14
- require 'clive/output'
15
- require 'clive/formatter'
16
21
 
17
- # Clive is a simple dsl for creating command line interfaces
22
+ # Clive is a DSL for creating command line interfaces. Generally to use it you
23
+ # will inherit from it with your own class.
24
+ #
25
+ # class CLI < Clive
26
+ # opt :working, 'Test if it is working' do
27
+ # puts "YEP!".green
28
+ # end
29
+ # end
18
30
  #
19
- # @example Simple Example
31
+ # CLI.run ARGV
20
32
  #
21
- # require 'clive'
33
+ # # app.rb --working
34
+ # #=> "YEP!"
22
35
  #
23
- # class CLI
24
- # include Clive::Parser
25
- #
26
- # desc 'A switch'
27
- # switch :s, :switch do
28
- # puts "You used a switch"
29
- # end
30
- #
31
- # desc 'A flag'
32
- # flag :hello, :args => "NAME" do |name|
33
- # puts "Hello, #{name}"
34
- # end
35
- #
36
- # desc 'True or false'
37
- # bool :which do |which|
38
- # case which
39
- # when true
40
- # puts "true, yay"
41
- # when false
42
- # puts "false, not yay"
43
- # end
44
- # end
45
- #
46
- # option_list :purchases
47
- #
48
- # command :new, :buy do
49
- # switch :toaster do
50
- # purchases << :toaster
51
- # end
52
- #
53
- # switch :tv do
54
- # purchases << :tv
55
- # end
36
+ # But it is possible to create a new instance of Clive instead in almost the
37
+ # same way.
38
+ #
39
+ # cli = Clive.new do
40
+ # opt :working, 'Test if it is working' do
41
+ # puts "YEP!".green
56
42
  # end
57
- #
58
43
  # end
59
44
  #
60
- # CLI.parse(ARGV)
45
+ # cli.run ARGV
46
+ #
47
+ # For very small tasks where you _just_ need to collect options passed and query
48
+ # about them you can use the {Kernel#Clive} method.
49
+ #
50
+ # r = Clive(:quiet, :verbose).run(ARGV)
51
+ #
52
+ # $log = Logger.new(STDOUT)
53
+ # $log.level = Logger::FATAL if r.quiet
54
+ # $log.level = Logger::DEBUG if r.verbose
61
55
  #
62
- module Clive
63
-
56
+ # # do some stuff
57
+ #
58
+ class Clive
59
+
60
+ extend Type::Lookup
61
+
62
+ class << self
63
+ attr_accessor :instance
64
+
65
+ # Sets up proxy methods for each relevent method in {Base} to an instance of {Base}.
66
+ def inherited(klass)
67
+ klass.instance = Base.new
68
+
69
+ str = (Base.instance_methods(false) | Command.instance_methods(false)).map do |sym|
70
+ <<-EOS
71
+ def self.#{sym}(*args, &block)
72
+ instance.send(:#{sym}, *args, &block)
73
+ end
74
+ EOS
75
+ end.join("\n")
76
+ klass.instance_eval str
77
+ end
78
+
79
+ def method_missing(sym, *args, &block)
80
+ instance.send(sym, *args, &block)
81
+ end
82
+
83
+ def respond_to_missing?(sym, include_private=false)
84
+ instance.respond_to?(sym, include_private)
85
+ end
86
+
87
+ unless Kernel.respond_to?(:respond_to_missing?)
88
+ def respond_to?(sym, include_private=false)
89
+ respond_to_missing?(sym, include_private)
90
+ end
91
+ end
92
+
93
+ end
94
+
95
+ # This allows you to use Clive without defining a class, but while keeping all
96
+ # of the control.
97
+ #
98
+ # There is one caveat though when using this style: types can not be
99
+ # referenced with just the type name. Instead the full class path/name must be
100
+ # given. So instead of using +opt :num, as: Integer+ you need to use +opt
101
+ # :num, as: Clive::Type::Integer+, and similarly for all types.
102
+ #
103
+ # @param opts [Hash] Options to create with, see {Base#initialize}
104
+ # @example
105
+ #
106
+ # c = Clive.new { opt :v, :verbose }
107
+ # r = c.run ARGV
108
+ #
109
+ def self.new(opts={}, &block)
110
+ Base.new(opts, &block)
111
+ end
112
+
113
+ end
114
+
115
+ module Kernel
116
+
117
+ # The quickest way to grab a few options. This form does not allow arguments or
118
+ # commands! It is meant to be quick and simple.
119
+ #
120
+ # @example
121
+ #
122
+ # r = Clive(:verbose, [:b, :bare]).run(%w(--verbose))
123
+ # r.bare #=> false
124
+ # r.verbose #=> true
125
+ #
126
+ #
127
+ # # The above example is equivalent to
128
+ # r = Clive.new {
129
+ # opt :verbose
130
+ # opt :b, :bare
131
+ # }.run(%w(--verbose))
132
+ #
133
+ # @param names [#to_sym, Array<#to_sym>] List of names to create options for
134
+ # @return [Clive] A clive instance setup with the correct options
135
+ #
136
+ def Clive(*names)
137
+ c = Clive::Base.new
138
+ names.each do |o|
139
+ c.option *Array(o).map(&:to_sym)
140
+ end
141
+ c
142
+ end
143
+
64
144
  end
@@ -0,0 +1,170 @@
1
+ class Clive
2
+
3
+ # An Argument represents an argument for an Option or Command, it can be optional
4
+ # and can also be constricted by various other values, see {#initialize}.
5
+ class Argument
6
+
7
+ # Creates an object which will respond with true to all call to the method(s)
8
+ # given.
9
+ #
10
+ # @example
11
+ # eg = AlwaysTrue.for(:a, :b, :c)
12
+ # eg.a #=> true
13
+ # eg.b(1,2,3) #=> true
14
+ # eg.c { 1 } #=> true
15
+ # eg.d #=> NoMethodError
16
+ #
17
+ class AlwaysTrue
18
+
19
+ # @param syms [Symbol] Methods which should return true
20
+ def self.for(*syms)
21
+ c = Class.new
22
+ syms.each do |sym|
23
+ c.send(:define_method, sym) {|*a| true }
24
+ end
25
+ c.send(:define_method, :inspect) { "#<AlwaysTrue #{syms.map {|i| ":#{i}" }.join(', ') }>" }
26
+ c.new
27
+ end
28
+ end
29
+
30
+ # An Argument will have these traits by default.
31
+ DEFAULTS = {
32
+ :optional => false,
33
+ :type => Type::Object,
34
+ :match => AlwaysTrue.for(:match),
35
+ :within => AlwaysTrue.for(:include?),
36
+ :default => nil,
37
+ :constraint => AlwaysTrue.for(:call)
38
+ }
39
+
40
+ attr_reader :name, :default, :type
41
+
42
+ # A new instance of Argument.
43
+ #
44
+ # @param opts [Hash]
45
+ #
46
+ # @option opts [#to_sym] :name
47
+ # Name of the argument.
48
+ #
49
+ # @option opts [Boolean] :optional
50
+ # Whether this argument is optional. An optional argument does not have
51
+ # to be given and will pass +:default+ to the block instead.
52
+ #
53
+ # @option opts [Type] :type
54
+ # Type that the matching argument should be cast to. See {Type} and the
55
+ # various subclasses for details. Each {Type} defines something that the
56
+ # argument must match in addition to the +:match+ argument given.
57
+ #
58
+ # @option opts [#match] :match
59
+ # Regular expression the argument must match.
60
+ #
61
+ # @option opts [#include?] :within
62
+ # Collection that the argument should be in. This will be checked
63
+ # against the string argument and the cast object (see +:type+). So for
64
+ # instance if +:type+ is set to +Integer+ you can set +:within+ to be an array
65
+ # of integers, [1,2,3], or an array of strings, %w(1 2 3), and get the
66
+ # same result.
67
+ #
68
+ # @option opts :default
69
+ # Default value the argument takes. This is only set or used if the Option or
70
+ # Command is actually called.
71
+ #
72
+ # @option opts [#call, #to_proc] :constraint
73
+ # Proc which is passed the found argument and should return +true+ if the
74
+ # value is ok and false if not.
75
+ # If the object responds to #to_proc this will be called and the resulting
76
+ # Proc object saved for later use. This allows you to pass method symbols.
77
+ #
78
+ # @example
79
+ #
80
+ # Argument.new(:arg, :optional => true, :type => Integer, :constraint => :odd?)
81
+ #
82
+ def initialize(name, opts={})
83
+ @name = name.to_sym
84
+
85
+ opts[:constraint] = opts[:constraint].to_proc if opts[:constraint].respond_to?(:to_proc)
86
+ opts = DEFAULTS.merge(opts)
87
+
88
+ @optional = opts[:optional]
89
+ @type = Type.find_class(opts[:type].to_s) rescue opts[:type]
90
+ @match = opts[:match]
91
+ @within = opts[:within]
92
+ @default = opts[:default]
93
+ @constraint = opts[:constraint]
94
+ end
95
+
96
+ # @return Whether the argument is optional.
97
+ def optional?
98
+ @optional
99
+ end
100
+
101
+ # @return [String] String representation for the argument.
102
+ def to_s
103
+ optional? ? "[<#@name>]" : "<#@name>"
104
+ end
105
+
106
+ # @return [String]
107
+ # Choices or range of choices that can be made, for the help string.
108
+ def choice_str
109
+ if @within
110
+ case @within
111
+ when Array
112
+ '(' + @within.join(', ') + ')'
113
+ when Range
114
+ '(' + @within.to_s + ')'
115
+ else
116
+ ''
117
+ end
118
+ else
119
+ ''
120
+ end
121
+ end
122
+
123
+ def inspect
124
+ "#<#{self.class} #{to_s}>"
125
+ end
126
+
127
+ # Determines whether the object given can be this argument. Checks whether
128
+ # it is valid based on the options passed to {#initialize}.
129
+ #
130
+ # @param obj [String, Object]
131
+ # This method will be called at least twice for each argument, the first
132
+ # time when testing for {Arguments#possible?} and then for {Arguments#valid?}.
133
+ # When called in {Arguments#possible?} +obj+ will be passed as a string,
134
+ # for {Arguments#valid?} though +obj+ will have been cast using {#coerce}
135
+ # to the correct type meaning this method must deal with both cases.
136
+ #
137
+ # @return Whether +obj+ could be this argument.
138
+ #
139
+ def possible?(obj)
140
+ return false if obj.is_a?(String) && !@type.valid?(obj)
141
+ return false unless @match.match(obj.to_s)
142
+
143
+ coerced = coerce(obj)
144
+
145
+ unless @within.include?(obj.to_s) || @within.include?(coerced)
146
+ return false
147
+ end
148
+
149
+ begin
150
+ return false unless @constraint.call(obj.to_s)
151
+ rescue
152
+ begin
153
+ return false unless @constraint.call(coerced)
154
+ rescue
155
+ return false
156
+ end
157
+ end
158
+
159
+ true
160
+ end
161
+
162
+ # Converts the given String argument to the correct type determined by the
163
+ # +:type+ passed to {#initialize}.
164
+ def coerce(str)
165
+ return str unless str.is_a?(String)
166
+ @type.typecast(str)
167
+ end
168
+ end
169
+
170
+ end
@@ -0,0 +1,139 @@
1
+ class Clive
2
+ # An Array of {Clive::Argument} instances.
3
+ class Arguments < Array
4
+
5
+ # @example
6
+ #
7
+ # ArgumentList.create :args => '<a> [<b>]', :as => [Integer, String]
8
+ # #=> #<ArgumentList ...>
9
+ #
10
+ def self.create(opts)
11
+ new Parser.new(opts).to_args
12
+ end
13
+
14
+ # Zips a list of found arguments to this ArgumentList, but it also takes
15
+ # account of whether the found argument is possible and makes sure that
16
+ # optional Arguments are correctly handled.
17
+ #
18
+ # @example
19
+ #
20
+ # a = Argument.new(:a, type: Integer, constraint: :even?, optional: true)
21
+ # b = Argument.new(:b, type: Integer, constraint: :odd?)
22
+ # c = Argument.new(:c, type: Integer, constraint: :even?)
23
+ # d = Argument.new(:d, type: Integer, constraint: :odd?, optional: true)
24
+ #
25
+ # list = ArgumentList.new([a, b, c, d])
26
+ # found_args = %w(1 2 3)
27
+ #
28
+ # list.zip(found_args).map {|i| [i[0].to_s, i[1]] }
29
+ # #=> [['[<a>]', nil], ['<b>', 1], ['<c>', 2], ['[<d>]', 3]]
30
+ #
31
+ #
32
+ # @param other [Array<String>] Found list of arguments.
33
+ # @return [Array<Argument,String>]
34
+ #
35
+ def zip(other)
36
+ other = other.dup.compact
37
+ # Find the number of 'spares'
38
+ diff = other.size - find_all {|i| !i.optional? }.size
39
+ r = []
40
+
41
+ map do |arg|
42
+ if arg.possible?(other.first)
43
+ if arg.optional?
44
+ if diff > 0
45
+ [arg, other.shift]
46
+ else
47
+ [arg, nil]
48
+ end
49
+ else
50
+ [arg, other.shift]
51
+ end
52
+ else
53
+ [arg, nil]
54
+ end
55
+ end
56
+ end
57
+
58
+ # @return [String]
59
+ def to_s
60
+ map {|i| i.to_s }.join(' ').gsub('] [', ' ')
61
+ end
62
+
63
+ # @return [Integer] The minimum number of arguments that *must* be given.
64
+ def min
65
+ reject {|i| i.optional? }.size
66
+ end
67
+
68
+ # @return [Integer] The maximum number of arguments that *can* be given.
69
+ def max
70
+ size
71
+ end
72
+
73
+ # Whether the +list+ of found arguments could possibly be the arguments for
74
+ # this option. This does not need to check the minimum length as the list
75
+ # may not be completely built, this just checks it hasn't failed completely.
76
+ #
77
+ # @param list [Array<Object>]
78
+ def possible?(list)
79
+ return true if list.empty?
80
+ i = 0
81
+ optionals = []
82
+
83
+ list.each do |item|
84
+ break if i >= size
85
+
86
+ # Either, +item+ is self[i]
87
+ if self[i].possible?(item)
88
+ i += 1
89
+
90
+ # Or, the argument is optional and there is another argument to move to
91
+ # meaning it can be skipped
92
+ elsif self[i].optional? && (i < size - 1)
93
+ i += 1
94
+ optionals << item
95
+
96
+ # Or, an optional argument has been skipped and this could be it so bring
97
+ # it back from the dead and check, if it is remove it and move on
98
+ elsif optionals.size > 0 && self[i].possible?(optionals.first)
99
+ i += 1
100
+ optionals.shift
101
+
102
+ # Problem
103
+ else
104
+ return false
105
+ end
106
+ end
107
+
108
+ list.size <= max
109
+ end
110
+
111
+ # Whether the +list+ of found arguments is valid to be the arguments for this
112
+ # option. Here length is checked as we need to make sure enough arguments are
113
+ # present.
114
+ #
115
+ # It is important that when the arguments are put in the correct place
116
+ # that we check for missing arguments (which have been added as +nil+s)
117
+ # so compact the list _then_ check the size.
118
+ #
119
+ # @param list [Array<Object>]
120
+ def valid?(list)
121
+ zip(list).map do |a,i|
122
+ if a.optional?
123
+ nil
124
+ else
125
+ i
126
+ end
127
+ end.compact.size >= min && possible?(list)
128
+ end
129
+
130
+ # Will fill spaces in +list+ with default values, then coerces all arguments
131
+ # to the corect types.
132
+ #
133
+ # @return [Array]
134
+ def create_valid(list)
135
+ zip(list).map {|a,r| r ? a.coerce(r) : a.coerce(a.default) }
136
+ end
137
+
138
+ end
139
+ end