rubikon 0.3.0 → 0.4.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.
@@ -0,0 +1,70 @@
1
+ # This code is free software; you can redistribute it and/or modify it under
2
+ # the terms of the new BSD License.
3
+ #
4
+ # Copyright (c) 2010, Sebastian Staudt
5
+
6
+ module Rubikon
7
+
8
+ module Application
9
+
10
+ # The application sandbox is a wrapper used to secure internal Rubikon
11
+ # logic from access by user generated application code.
12
+ #
13
+ # This is mostly to prevent accidental execution or change of Rubikon's
14
+ # internal code. But it also helps to prevent possible security problems
15
+ # depending on the code used inside the application logic.
16
+ #
17
+ # @see Application::InstanceMethods
18
+ # @since 0.4.0
19
+ class Sandbox
20
+
21
+ # Create a new application sandbox
22
+ #
23
+ # @param [Application::Base] app The application to be sandboxed
24
+ def initialize(app)
25
+ raise ArgumentError unless app.is_a? Application::Base
26
+ @__app__ = app
27
+ end
28
+
29
+ # Method calls on the sandbox wrapper will be relayed to the singleton
30
+ # instance. Methods defined in InstanceMethods are protected and will
31
+ # raise a NoMethodError.
32
+ #
33
+ # @param (see ClassMethods#method_missing)
34
+ # @raise [NoMethodError] if a method is called that is defined inside
35
+ # InstanceMethods and should therefore be protected
36
+ # @see InstanceMethods
37
+ def method_missing(name, *args, &block)
38
+ if InstanceMethods.method_defined?(name) ||
39
+ InstanceMethods.private_method_defined?(name)
40
+ raise NoMethodError.new("Method `#{name}' is protected by the application sandbox", name)
41
+ end
42
+ @__app__.send(name, *args, &block)
43
+ end
44
+
45
+ # Relay putc to the instance method
46
+ #
47
+ # This is used to hide <tt>Kernel#putc</tt> so that the application's
48
+ # output IO object is used for printing characters
49
+ #
50
+ # @param [String, Numeric] char The character to write into the output
51
+ # stream
52
+ def putc(text)
53
+ @__app__.send(:putc, text)
54
+ end
55
+
56
+ # Relay puts to the instance method
57
+ #
58
+ # This is used to hide <tt>Kernel#puts</tt> so that the application's
59
+ # output IO object is used for printing text
60
+ #
61
+ # @param [String] text The text to write into the output stream
62
+ def puts(text)
63
+ @__app__.send(:puts, text)
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -5,6 +5,7 @@
5
5
 
6
6
  require 'rubikon/application/base'
7
7
  require 'rubikon/exceptions'
8
+ require 'rubikon/has_arguments'
8
9
  require 'rubikon/parameter'
9
10
 
10
11
  module Rubikon
@@ -16,31 +17,33 @@ module Rubikon
16
17
  # @since 0.3.0
17
18
  class Command
18
19
 
19
- include Parameter
20
+ include HasArguments
20
21
 
22
+ # @return [String] The description of this command
21
23
  attr_accessor :description
22
- attr_reader :args, :params
23
- alias_method :arguments, :args
24
+
25
+ # @return [Array<Parameter>] The parameters of this command
26
+ attr_reader :params
24
27
  alias_method :parameters, :params
25
28
 
26
29
  # Create a new application command with the given name with a reference to
27
30
  # the app it belongs to
28
31
  #
29
- # @param [Application::Base] app A reference to the application
30
- # instance this command belongs to
31
- # @param [#to_sym] name The name of this command, used in application
32
+ # @param [Application::Base] app The application this command belongs to
33
+ # @param [Symbol, #to_sym] name The name of this command, used in application
32
34
  # arguments
35
+ # @param [Range, Array, Numeric] arg_count The number of arguments this
36
+ # command takes.
33
37
  # @param [Proc] block The code block which should be executed by this
34
38
  # command
35
39
  # @raise [ArgumentError] if the given application object isn't a Rubikon
36
40
  # application
37
41
  # @raise [BlockMissingError] if no command code block is given and a
38
42
  # command file does not exist
39
- def initialize(app, name, &block)
40
- raise ArgumentError unless app.is_a? Application::Base
41
- super(name, nil)
43
+ # @see HasArguments#arg_count=
44
+ def initialize(app, name, arg_count = nil, &block)
45
+ super
42
46
 
43
- @app = app
44
47
  @params = {}
45
48
 
46
49
  if block_given?
@@ -53,14 +56,14 @@ module Rubikon
53
56
  end
54
57
  end
55
58
 
56
- # Add a new Parameter for this command
59
+ # Add a new parameter for this command
57
60
  #
58
- # @param [Parameter, Hash] parameter The parameter to add to this
61
+ # @param [Parameter, Hash] parameter The parameter to add to this
59
62
  # command. This might also be a Hash where every key will be an
60
63
  # alias to the corresponding value, e.g. <tt>{ :alias => :parameter
61
64
  # }</tt>.
62
65
  # @see Parameter
63
- def <<(parameter)
66
+ def add_param(parameter)
64
67
  if parameter.is_a? Hash
65
68
  parameter.each do |alias_name, name|
66
69
  alias_name = alias_name.to_sym
@@ -78,12 +81,33 @@ module Rubikon
78
81
  @params.each do |name, param|
79
82
  if param == parameter.name
80
83
  parameter.aliases << name
84
+ @params[name] = parameter
81
85
  end
82
86
  end
83
87
  @params[parameter.name] = parameter
84
88
  end
85
89
  end
86
90
 
91
+ # If a parameter with the specified method name exists, a call to that
92
+ # method will return the value of the parameter.
93
+ #
94
+ # @param (see ClassMethods#method_missing)
95
+ # @see DSLMethods#params
96
+ #
97
+ # @example
98
+ # option :user, [:who]
99
+ # command :hello, [:mood] do
100
+ # puts "Hello #{user.who}"
101
+ # puts "I feel #{mood}"
102
+ # end
103
+ def method_missing(name, *args, &block)
104
+ if args.empty? && !block_given? && @params.key?(name)
105
+ @params[name]
106
+ else
107
+ super
108
+ end
109
+ end
110
+
87
111
  # Parses the arguments of this command and sets each Parameter as active
88
112
  # if it has been supplied by the user on the command-line. Additional
89
113
  # arguments are passed to the individual parameters.
@@ -96,7 +120,6 @@ module Rubikon
96
120
  # @see Option
97
121
  def parse_arguments(args)
98
122
  @args = []
99
- parameter = nil
100
123
  args.each do |arg|
101
124
  if arg.start_with?('-')
102
125
  parameter_name = arg.start_with?('--') ? arg[2..-1] : arg[1..-1]
@@ -104,21 +127,41 @@ module Rubikon
104
127
  raise UnknownParameterError.new(arg) if parameter.nil?
105
128
  end
106
129
 
107
- unless parameter.nil? || parameter.active?
108
- parameter.active!
130
+ unless parameter.nil?
131
+ @app.current_param.active! unless @app.current_param.nil?
132
+ @app.current_param = parameter
109
133
  next
110
134
  end
111
135
 
112
- if parameter.nil? || !parameter.more_args?
113
- @args << arg
136
+ if @app.current_param.nil? || !@app.current_param.more_args?
137
+ self << arg
114
138
  else
115
- parameter << arg
139
+ @app.current_param << arg
116
140
  end
117
141
  end
118
142
 
119
- @params.values.each do |param|
120
- param.check_args if param.is_a?(Option) && param.active?
121
- end
143
+ @app.current_param.active! unless @app.current_param.nil?
144
+ @app.current_param = nil
145
+ end
146
+
147
+ # Resets this command to its initial state
148
+ #
149
+ # @see HasArguments#reset
150
+ # @since 0.4.0
151
+ def reset
152
+ super
153
+ @params.values.uniq.each { |param| param.reset if param.is_a? Parameter }
154
+ end
155
+
156
+ # Checks whether a parameter with the given name exists for this command
157
+ #
158
+ # This is used to determine if a method call would successfully return the
159
+ # value of a parameter.
160
+ #
161
+ # @return +true+ if named parameter with the specified name exists
162
+ # @see #method_missing
163
+ def respond_to_missing?(name, include_private = false)
164
+ @params.key?(name) || super
122
165
  end
123
166
 
124
167
  # Run this command's code block
@@ -127,7 +170,8 @@ module Rubikon
127
170
  # command
128
171
  def run(*args)
129
172
  parse_arguments(args)
130
- @app.instance_eval(&@block)
173
+ check_args
174
+ @app.sandbox.instance_eval(&@block)
131
175
  end
132
176
 
133
177
  end
data/lib/rubikon/flag.rb CHANGED
@@ -18,14 +18,6 @@ module Rubikon
18
18
 
19
19
  include Parameter
20
20
 
21
- # Creates a new flag with the given name and an optional code block
22
- #
23
- # @param name (see Parameter#initialize)
24
- # @param block (see Parameter#initialize)
25
- def initialize(name, &block)
26
- super(name, 0, &block)
27
- end
28
-
29
21
  # Adds an argument to this flag
30
22
  #
31
23
  # @param arg (see Parameter#<<)
@@ -0,0 +1,182 @@
1
+ # This code is free software; you can redistribute it and/or modify it under
2
+ # the terms of the new BSD License.
3
+ #
4
+ # Copyright (c) 2010, Sebastian Staudt
5
+
6
+ require 'rubikon/parameter'
7
+
8
+ module Rubikon
9
+
10
+ # This module is included in all classes used for parsing command-line
11
+ # arguments
12
+ #
13
+ # @author Sebastian Staudt
14
+ # @see Application::InstanceMethods
15
+ # @see Command
16
+ # @see Option
17
+ # @since 0.4.0
18
+ module HasArguments
19
+
20
+ include Parameter
21
+
22
+ # @return [Array<String>] The arguments given to this parameter
23
+ attr_reader :args
24
+ alias_method :arguments, :args
25
+
26
+ # Creates a new parameter with arguments with the given name and an
27
+ # optional code block
28
+ #
29
+ # @param [Application::Base] app The application this parameter belongs to
30
+ # @param [Symbol, #to_sym] name The name of the option
31
+ # @param [Fixnum, Range, Array] arg_count A range or array allows any
32
+ # number of arguments inside the limits between the first and the
33
+ # last element of the range or array (-1 stands for an arbitrary
34
+ # number of arguments). A positive number indicates the exact amount
35
+ # of required arguments while a negative argument count indicates
36
+ # the amount of required arguments, but allows additional, optional
37
+ # arguments. A argument count of 0 means there are no required
38
+ # arguments, but it allows optional arguments.
39
+ # Finally an array of symbols enables named arguments where the
40
+ # argument count is the size of the array and each argument is named
41
+ # after the corresponding symbol.
42
+ # @param [Proc] block An optional code block to be executed if this
43
+ # option is used
44
+ def initialize(app, name, arg_count = 0, &block)
45
+ super(app, name, &block)
46
+
47
+ @args = []
48
+ @arg_names = nil
49
+ if arg_count.is_a? Fixnum
50
+ if arg_count > 0
51
+ @min_arg_count = arg_count
52
+ @max_arg_count = arg_count
53
+ elsif arg_count <= 0
54
+ @min_arg_count = -arg_count
55
+ @max_arg_count = -1
56
+ end
57
+ elsif arg_count.is_a?(Array) && arg_count.all? { |a| a.is_a? Symbol }
58
+ @max_arg_count = @min_arg_count = arg_count.size
59
+ @arg_names = arg_count
60
+ elsif arg_count.is_a?(Range) || arg_count.is_a?(Array)
61
+ @min_arg_count = arg_count.first
62
+ @max_arg_count = arg_count.last
63
+ else
64
+ @min_arg_count = 0
65
+ @max_arg_count = 0
66
+ end
67
+ end
68
+
69
+ # Access the arguments of this parameter using a numeric or symbolic index
70
+ #
71
+ # @param [Numeric, Symbol] The index of the argument to return. Numeric
72
+ # indices can be used always while symbolic arguments are only
73
+ # available for named arguments.
74
+ # @return The argument with the specified index
75
+ # @since 0.4.0
76
+ def [](arg)
77
+ arg = @arg_names.index(arg) if arg.is_a? Symbol
78
+ @args[arg]
79
+ end
80
+
81
+ # Adds an argument to this parameter. Arguments can be accessed inside the
82
+ # application code using the args method.
83
+ #
84
+ # @param [String] arg The argument to add to the supplied arguments of this
85
+ # parameter
86
+ # @raise [ExtraArgumentError] if the parameter has all required arguments
87
+ # supplied and does not take optional arguments
88
+ # @return [Array] The supplied arguments of this parameter
89
+ # @see #[]
90
+ # @see #args
91
+ # @since 0.3.0
92
+ def <<(arg)
93
+ if args_full? && @args.size == @max_arg_count
94
+ raise ExtraArgumentError.new(@name)
95
+ end
96
+ @args << arg
97
+ end
98
+
99
+ # Marks this parameter as active when it has been supplied by the user on
100
+ # the command-line. This also checks the arguments given to this parameter.
101
+ #
102
+ # @see #check_args
103
+ # @see Paramter#active!
104
+ def active!
105
+ check_args
106
+ super
107
+ end
108
+
109
+ # Return the allowed range of argument counts this parameter takes
110
+ #
111
+ # @return [Range] The allowed range of argument counts this parameter takes
112
+ def arg_count
113
+ @min_arg_count..@max_arg_count
114
+ end
115
+
116
+ # Checks whether this parameter has all required arguments supplied
117
+ #
118
+ # @return +true+ if all required parameter arguments have been supplied
119
+ # @since 0.3.0
120
+ def args_full?
121
+ @args.size >= @min_arg_count
122
+ end
123
+
124
+ # Checks the arguments for this parameter
125
+ #
126
+ # @raise [MissingArgumentError] if there are not enough arguments for
127
+ # this parameter
128
+ # @since 0.3.0
129
+ def check_args
130
+ raise MissingArgumentError.new(@name) unless args_full?
131
+ end
132
+
133
+ # If a named argument with the specified method name exists, a call to that
134
+ # method will return the value of the argument.
135
+ #
136
+ # @param (see ClassMethods#method_missing)
137
+ # @see #args
138
+ # @see #[]
139
+ #
140
+ # @example
141
+ # option :user, [:name] do
142
+ # @user = name
143
+ # end
144
+ def method_missing(name, *args, &block)
145
+ if args.empty? && !block_given? && !@arg_names.nil? && @arg_names.include?(name)
146
+ @args[@arg_names.index(name)]
147
+ else
148
+ super
149
+ end
150
+ end
151
+
152
+ # Checks whether this parameter can take more arguments
153
+ #
154
+ # @return +true+ if this parameter can take more arguments
155
+ # @since 0.3.0
156
+ def more_args?
157
+ @max_arg_count == -1 || @args.size < @max_arg_count
158
+ end
159
+
160
+ # Resets this parameter to its initial state
161
+ #
162
+ # @see Parameter#reset
163
+ # @since 0.4.0
164
+ def reset
165
+ super
166
+ @args.clear
167
+ end
168
+
169
+ # Checks whether an argument with the given name exists for this parameter
170
+ #
171
+ # This is used to determine if a method call would successfully return the
172
+ # value of an argument.
173
+ #
174
+ # @return +true+ if named argument with the specified name exists
175
+ # @see #method_missing
176
+ def respond_to_missing?(name, include_private = false)
177
+ !@arg_names.nil? && @arg_names.include?(name)
178
+ end
179
+
180
+ end
181
+
182
+ end
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # Copyright (c) 2010, Sebastian Staudt
5
5
 
6
- require 'rubikon/parameter'
6
+ require 'rubikon/has_arguments'
7
7
 
8
8
  module Rubikon
9
9
 
@@ -16,14 +16,7 @@ module Rubikon
16
16
  # @since 0.3.0
17
17
  class Option
18
18
 
19
- # @return [Numeric] The number of arguments this parameter takes
20
- attr_reader :arg_count
21
-
22
- # @return [Array<String>] The arguments given to this parameter
23
- attr_reader :args
24
- alias_method :arguments, :args
25
-
26
- include Parameter
19
+ include HasArguments
27
20
 
28
21
  end
29
22
 
@@ -7,9 +7,10 @@ module Rubikon
7
7
 
8
8
  # A parameter is any command-line argument given to the application that is
9
9
  # not prefixed with one or two dashes. Once a parameter is supplied by the
10
- # user, it is relayed to the Command it belongs to.
10
+ # user, it is relayed to the command it belongs to.
11
11
  #
12
12
  # @author Sebastian Staudt
13
+ # @see Command
13
14
  # @since 0.3.0
14
15
  module Parameter
15
16
 
@@ -21,38 +22,18 @@ module Rubikon
21
22
 
22
23
  # Creates a new parameter with the given name
23
24
  #
25
+ # @param [Application::Base] app The application this parameter belongs to
24
26
  # @param [Symbol, #to_sym] name The name of the parameter
25
- # @param [Numeric] arg_count The number of arguments this parameter takes
26
- # if any
27
27
  # @param [Proc] block An optional code block to be executed if this
28
28
  # parameter is used
29
- #
30
- # A positive argument count indicates the exact amount of required
31
- # arguments, while a negative argument count indicates the amount of
32
- # required arguments, but allows additional, optional arguments. A argument
33
- # count of 0 means there are no required arguments, but it allows optional
34
- # arguments. If you need a parameter that does not allow arguments at all
35
- # you should use a flag instead.
36
- def initialize(name, arg_count = 0, &block)
37
- @active = false
38
- @aliases = []
39
- @arg_count = arg_count
40
- @args = []
41
- @block = block
42
- @name = name.to_sym
43
- end
29
+ def initialize(app, name, &block)
30
+ raise ArgumentError unless app.is_a? Application::Base
44
31
 
45
- # Adds an argument to this parameter. Parameter arguments can be accessed
46
- # inside the Application code using the parameter's args method.
47
- #
48
- # @param [String] arg The argument to add to the supplied arguments of this
49
- # parameter
50
- # @raise [ExtraArgumentError] if the parameter has all required arguments
51
- # supplied and does not take optional arguments
52
- # @return [Array] The supplied arguments of this parameter
53
- def <<(arg)
54
- raise ExtraArgumentError.new(@name) if args_full? && @arg_count > 0
55
- @args << arg
32
+ @active = false
33
+ @aliases = []
34
+ @app = app
35
+ @block = block
36
+ @name = name.to_sym
56
37
  end
57
38
 
58
39
  # Marks this parameter as active when it has been supplied by the user on
@@ -60,7 +41,7 @@ module Rubikon
60
41
  # exists
61
42
  def active!
62
43
  @active = true
63
- @block.call unless @block.nil?
44
+ @app.sandbox.instance_eval(&@block) unless @block.nil?
64
45
  end
65
46
 
66
47
  # Returns whether this parameter has is active, i.e. it has been supplied
@@ -70,30 +51,13 @@ module Rubikon
70
51
  def active?
71
52
  @active
72
53
  end
54
+ alias_method :given?, :active?
73
55
 
74
- # Checks whether this parameter has all required arguments supplied
75
- #
76
- # @return +true+ if all required parameter arguments have been supplied
77
- def args_full?
78
- arg_count = @arg_count
79
- arg_count = -arg_count if arg_count < 0
80
-
81
- arg_count == 0 || @args.size >= arg_count
82
- end
83
-
84
- # Checks the arguments for this parameter
85
- #
86
- # @raise [MissingArgumentError] if there are not enough arguments for
87
- # this parameter
88
- def check_args
89
- raise MissingArgumentError.new(@name) unless args_full?
90
- end
91
-
92
- # Checks whether this parameter can take more arguments
56
+ # Resets this parameter to its initial state
93
57
  #
94
- # @return +true+ if this parameter can take more arguments
95
- def more_args?
96
- arg_count <= 0 || @args.size < arg_count
58
+ # @since 0.4.0
59
+ def reset
60
+ @active = false
97
61
  end
98
62
 
99
63
  end
@@ -72,7 +72,7 @@ module Rubikon
72
72
  if add_progress > 0
73
73
  @ostream << @progress_char * add_progress
74
74
  @ostream.flush
75
- @ostream.puts '' if @progress == @size
75
+ @ostream.putc 10 if @progress == @size
76
76
  end
77
77
 
78
78
  self
data/lib/rubikon.rb CHANGED
@@ -6,15 +6,24 @@
6
6
  libdir = File.dirname(__FILE__)
7
7
  $:.unshift(libdir) unless $:.include?(libdir)
8
8
 
9
+ require 'core_ext/object'
9
10
  require 'core_ext/string'
10
11
  require 'rubikon/application/base'
11
12
 
12
- # A namespace module for all Rubikon related code
13
+ # Rubikon is a simple to use, yet powerful Ruby framework for building
14
+ # console-based applications. Rubikon aims to provide an easy to write and easy
15
+ # to read domain-specific language (DSL) to speed up development of
16
+ # command-line applications. With Rubikon it's a breeze to implement
17
+ # applications with only few options as well as more complex programs like
18
+ # RubyGems, Homebrew or even Git.
19
+ #
20
+ # This is the namespace module for all Rubikon related code.
13
21
  #
14
22
  # @author Sebastian Staudt
15
23
  # @since 0.1.0
16
24
  module Rubikon
17
25
 
18
- VERSION = '0.3.0'
26
+ # This is the current version of the Rubikon gem
27
+ VERSION = '0.4.0'
19
28
 
20
29
  end
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # This code is free software; you can redistribute it and/or modify it under
4
+ # the terms of the new BSD License.
5
+ #
6
+ # Copyright (c) 2010, Sebastian Staudt
7
+
8
+ if ENV['RUBIKON_DEV']
9
+ require File.join(File.expand_path(File.dirname(__FILE__)), '..', '..', 'lib', 'rubikon')
10
+ else
11
+ require 'rubygems'
12
+ require 'rubikon'
13
+ end
14
+
15
+ # A relatively simple Hello World application using Rubikon
16
+ class HelloWorld < Rubikon::Application::Base
17
+
18
+ # Greet the whole world per default
19
+ flag :more
20
+ option :name, [:who]
21
+ option :names, -1
22
+ default 'Simple hello world' do
23
+ debug 'Starting to greet the world...'
24
+ if given? :name
25
+ greet parameters[:name].who
26
+ elsif given? :names
27
+ names.args.each do |name|
28
+ greet name
29
+ end
30
+ else
31
+ greet 'World'
32
+ end
33
+ puts 'Nice to see you.' if given? :more
34
+ end
35
+
36
+ # Interactive mode
37
+ #
38
+ # Ask the user for his name and greet him
39
+ command :interactive, 'Greet interactively' do
40
+ name = input 'Please enter your name'
41
+ greet name
42
+ end
43
+
44
+ # Show a progress bar while iterating through a loop
45
+ command :progress, 'Display a progress bar' do
46
+ put 'Watch my progress while I greet the world: '
47
+ x = 1000000
48
+ progress_bar(:char => '+', :maximum => x, :size => 30) do |progress|
49
+ x.times do
50
+ progress.+
51
+ end
52
+ end
53
+ end
54
+
55
+ # Sleep for 5 seconds while displaying a throbber
56
+ command :throbber, 'Display a throbber' do
57
+ put 'Greeting the whole world takes some time... '
58
+ throbber do
59
+ sleep 5
60
+ end
61
+ puts 'done.'
62
+ end
63
+
64
+ # A standard Ruby class method for greeting
65
+ def greet(someone)
66
+ puts "Hello #{someone}!"
67
+ end
68
+
69
+ end