clive 0.8.1 → 1.0.0

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