rink 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.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ .idea
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Colin MacKenzie IV
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,135 @@
1
+ = rink
2
+
3
+ Makes interactive consoles awesome. More specifically, it does this by automating as much as conceivably possible so
4
+ that you can get right down to what you really care about: your application.
5
+
6
+ After the second copy-and-paste of an interactive console that I'd written for a few of my various gems, I decided to
7
+ extract that code into a plug-and-play-friendly console gem. This library is the result.
8
+
9
+ == Examples
10
+
11
+ To create a new interactive console:
12
+
13
+ require 'rink'
14
+ Rink::Console.new
15
+
16
+ The above example creates a console, but it doesn't add much in the way of usefulness. To really get started on your
17
+ application-specific interactive console, extend the Rink::Console class:
18
+
19
+ class MyConsole < Rink::Console
20
+ # ...
21
+ end
22
+
23
+ #...
24
+ MyConsole.new
25
+
26
+ By default, Rink will execute code within the context of its #namespace (see the Rink::Console class documentation for
27
+ details). You can, however, add specific commands to Rink like so:
28
+
29
+ class MyConsole < Rink::Console
30
+ command :help do |args|
31
+ if args.length == 0
32
+ puts "What do you need help with?"
33
+ else
34
+ puts "Sorry, I don't know anything about #{args.inspect}."
35
+ end
36
+ end
37
+ end
38
+
39
+ MyConsole.new
40
+
41
+ # produces...
42
+
43
+ >> Interactive Console <<
44
+
45
+ MyConsole > help
46
+ What do you need help with?
47
+
48
+ MyConsole > help feed the poor
49
+ Sorry, I don't know anything about ["feed", "the", "poor"]
50
+
51
+ MyConsole >
52
+
53
+ In addition to commands, you can also easily add or override default options in your console:
54
+
55
+ class MyConsole < Rink::Console
56
+ option :allow_ruby => false, :welcome => "Hello there!"
57
+ command :greet_me do |args|
58
+ puts options[:welcome]
59
+ end
60
+ end
61
+
62
+ MyConsole.new
63
+
64
+ # produces...
65
+
66
+ >> Interactive Console <<
67
+
68
+ MyConsole > inspect
69
+ I don't know the word "inspect."
70
+
71
+ MyConsole > greet_me
72
+ Hello there!
73
+
74
+ == Running From The Command Line
75
+
76
+ You've seen numerous examples of how to start Rink from within Ruby. The same thing works from within IRB or a Rake
77
+ task. Additionally, Rink ships with a script so that you can run it directly from the command line:
78
+
79
+ $ rink
80
+
81
+ If you've extended Rink, you can give it the name of the class you extended it with. Rink will look in both the current
82
+ directory, and the "lib" directory (if present) beneath the current location:
83
+
84
+ $ rink My::Console
85
+
86
+ Loaded constant My::Console from /Users/colin/projects/gems/rink/my/console.rb
87
+
88
+ >> Interactive Console <<
89
+ My::Console >
90
+
91
+ If you need to load the console from a nonstandard directory, specify the path to that directory as a second argument.
92
+ Rink will check both that directory and any 'lib' directory beneath it for your console.
93
+
94
+ == Testing
95
+
96
+ Testing your console turns out to be really easy, since the Rink::Console initializer takes some optional arguments to
97
+ override the input and output streams. To can the set of input, for instance, just use:
98
+
99
+ input_string = "help"
100
+ MyConsole.new(:input => input_string)
101
+
102
+ You'll see the output of the above dumped to STDOUT. If you want to capture that output, the answer is pretty obvious:
103
+
104
+ output_string = ""
105
+ MyConsole.new(:input => input_string, :output => output_string)
106
+ #=> output_string now contains the contents of the result of running the commands found inside of input_string.
107
+
108
+ You can also pass IO objects in directly:
109
+
110
+ File.open("commands.txt", "r") do |cmd_file|
111
+ File.open("output.log", "w") do |log_file|
112
+ MyConsole.new(:input => cmd_file, :output => log_file)
113
+ end
114
+ end
115
+
116
+ Normally, if an error occurs, Rink will print a nicely-formatted message to its output stream. This is helpful if you're
117
+ using the console but not if you're trying to write tests for one. So, you can disable error catching within Rink by
118
+ passing :rescue_errors => false to the initializer:
119
+
120
+ MyConsole.new(:input => "raise", :rescue_errors => false)
121
+ #=> RuntimeError!
122
+
123
+ == Note on Patches/Pull Requests
124
+
125
+ * Fork the project.
126
+ * Make your feature addition or bug fix.
127
+ * Add tests for it. This is important so I don't break it in a
128
+ future version unintentionally.
129
+ * Commit, do not mess with rakefile, version, or history.
130
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
131
+ * Send me a pull request. Bonus points for topic branches.
132
+
133
+ == Copyright
134
+
135
+ Copyright (c) 2010 Colin MacKenzie IV. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,47 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "rink"
8
+ gem.summary = %Q{Makes interactive consoles awesome.}
9
+ gem.description = %Q{Makes interactive consoles awesome.}
10
+ gem.email = "sinisterchipmunk@gmail.com"
11
+ gem.homepage = "http://github.com/sinisterchipmunk/rink"
12
+ gem.authors = ["Colin MacKenzie IV"]
13
+
14
+ gem.add_development_dependency "rspec", ">= 1.3.0"
15
+
16
+ gem.test_files = FileList['spec/**/*']
17
+ end
18
+ Jeweler::GemcutterTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
21
+ end
22
+
23
+ require 'spec/rake/spectask'
24
+ Spec::Rake::SpecTask.new(:spec) do |spec|
25
+ spec.libs << 'lib' << 'spec'
26
+ spec.spec_files = FileList['spec/**/*_spec.rb']
27
+ end
28
+
29
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
30
+ spec.libs << 'lib' << 'spec'
31
+ spec.pattern = 'spec/**/*_spec.rb'
32
+ spec.rcov = true
33
+ end
34
+
35
+ task :spec => :check_dependencies
36
+
37
+ task :default => :spec
38
+
39
+ require 'rake/rdoctask'
40
+ Rake::RDocTask.new do |rdoc|
41
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
42
+
43
+ rdoc.rdoc_dir = 'rdoc'
44
+ rdoc.title = "rink #{version}"
45
+ rdoc.rdoc_files.include('README*')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
data/bin/rink ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env ruby
2
+ require File.expand_path(File.join(File.dirname(__FILE__), "../lib/rink"))
3
+
4
+ def find_class(name, path)
5
+ if const = eval(name)
6
+ return const
7
+ end rescue nil
8
+
9
+ paths = [path, File.join(path, "lib")]
10
+ filename = name.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
11
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').tr("-", "_").downcase
12
+
13
+ filename += ".rb" unless filename[/\.rb$/]
14
+
15
+ paths.each do |dir|
16
+ file = File.expand_path(File.join(dir, filename))
17
+ if File.exist?(file)
18
+ load file
19
+ if const = eval(name)
20
+ puts "Loaded constant #{const} from #{file}"
21
+ puts
22
+ return const
23
+ end rescue nil
24
+ end
25
+ end
26
+
27
+ raise "Console could not be found: #{name}\n (searching for '#{filename}' in #{paths.inspect})"
28
+ end
29
+
30
+ def banner
31
+ puts "Usage:"
32
+ puts " rink [My::Console] [path/to/file]"
33
+ puts
34
+ puts "By default, Rink::Console will be used. If path is omitted, the"
35
+ puts "current directory will be used."
36
+ puts
37
+ puts "Rink will check ./[file] and ./lib/[file] for the console to"
38
+ puts "load, where [file] is the underscored class name. For example,"
39
+ puts "App::Console would be found in either ./app/console.rb or "
40
+ puts "./lib/app/console.rb"
41
+ puts
42
+ exit
43
+ end
44
+
45
+ ARGV.each do |cmd|
46
+ if cmd == 'help' || cmd == '-h' || cmd == '/h' || cmd == '--help'
47
+ banner
48
+ end
49
+ end
50
+
51
+ puts
52
+ begin
53
+ if ARGV.length == 0
54
+ klass = Rink::Console
55
+ elsif ARGV.length == 1
56
+ klass = find_class(ARGV.first, ".")
57
+ elsif ARGV.length == 2
58
+ klass = find_class(ARGV.first, ARGV.last)
59
+ else
60
+ banner
61
+ end
62
+ klass.new
63
+ rescue
64
+ puts $!.message
65
+ puts
66
+ end
@@ -0,0 +1,15 @@
1
+ class Object
2
+ unless defined?(instance_exec)
3
+ def instance_exec(*args, &block)
4
+ mname = "__instance_exec_#{Thread.current.object_id.abs}"
5
+ eigen = class << self; self; end
6
+ eigen.class_eval { define_method(mname, &block) }
7
+ begin
8
+ ret = send(mname, *args)
9
+ ensure
10
+ eigen.class_eval { undef_method(mname) } rescue nil
11
+ end
12
+ ret
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,335 @@
1
+ module Rink
2
+ class Console
3
+ extend Rink::Delegation
4
+ attr_reader :line_processor
5
+ attr_writer :namespace, :silenced
6
+ attr_reader :input, :output
7
+ delegate :silenced?, :print, :write, :puts, :to => :output, :allow_nil => true
8
+ delegate :banner, :commands, :to => 'self.class'
9
+
10
+ # One caveat: if you override #initialize, make sure to do as much setup as possible before calling super -- or,
11
+ # call super with :defer => true -- because otherwise Rink will start the console before your init code executes.
12
+ def initialize(options = {})
13
+ options = default_options.merge(options)
14
+ apply_options(options)
15
+ run(options) unless options[:defer]
16
+ end
17
+
18
+ # The Ruby object within whose context the console will be run.
19
+ # For example:
20
+ # class CustomNamespace
21
+ # def save_the_world
22
+ # 'maybe later'
23
+ # end
24
+ # end
25
+ #
26
+ # Rink::Console.new(:namespace => CustomNamespace.new)
27
+ # # ...
28
+ # Rink::Console > save_the_world
29
+ # => "maybe later"
30
+ #
31
+ # This is most useful if you have an object with a lot of methods that you wish to treat
32
+ # as console commands. Also, it segregates the user from the Rink::Console instance, preventing
33
+ # them from making any changes to it.
34
+ #
35
+ # Note that you can set a console's namespace to itself if you _want_ the user to have access to it:
36
+ #
37
+ # Rink::Console.new(:namespace => :self)
38
+ #
39
+ def namespace
40
+ @namespace ||= begin
41
+ # We want namespace to be any object, and in order to do that, Rink will call namespace#binding.
42
+ # But by default, the namespace should be TOPLEVEL_BINDING. If we set @namespace to this,
43
+ # Rink will call TOPLEVEL_BINDING#binding, which is an error. So instead we'll create a singleton
44
+ # object and override #binding on that object to return TOPLEVEL_BINDING. Effectively, that
45
+ # singleton object becomes (more-or-less) a proxy into the toplevel object. (Is there a better way?)
46
+ klass = Class.new(Object)
47
+ klass.send(:define_method, :binding) { TOPLEVEL_BINDING }
48
+ klass.new
49
+ end
50
+ end
51
+
52
+ # Runs a series of commands in the context of this Console. Input can be either a string
53
+ # or an input stream. Other options include:
54
+ #
55
+ # :input => a string or an input stream
56
+ # :output => a string or an output stream.
57
+ # :banner => boolean: whether to print a welcome banner.
58
+ # :silent => boolean: whether to print any output at all.
59
+ # :namespace => any object (other than nil). Will be used as the default namespace.
60
+ #
61
+ # Note also that any value can be a proc. In this case, the proc will be called while
62
+ # applying the options and the return value of that proc will be used. This is useful
63
+ # for lazy loading a value or for setting options based on some condition.
64
+ #
65
+ def run(input = {}, options = {})
66
+ if input.kind_of?(Hash)
67
+ options = options.merge(input)
68
+ else
69
+ options.merge! :input => input
70
+ end
71
+
72
+ temporary_options(options) do
73
+ puts banner if options.key?(:banner) ? options[:banner] : default_options[:banner]
74
+ enter_input_loop
75
+ end
76
+ end
77
+
78
+ # runs a block of code with the specified options set, and then sets them back to their previous state.
79
+ # Options that are nil or not specified will be inherited from the previous state; and options in the
80
+ # previous state that are nil or not specified will not be reverted.
81
+ def temporary_options(options)
82
+ old_options = gather_options
83
+ apply_options(options)
84
+ yield
85
+ ensure
86
+ apply_options(old_options)
87
+ end
88
+
89
+ # Applies a new set of options. Options that are currently unset or nil will not be modified.
90
+ def apply_options(options)
91
+ return unless options
92
+
93
+ options.each do |key, value|
94
+ options[key] = value.call if value.kind_of?(Proc)
95
+ end
96
+ @_options ||= {}
97
+ @_options.merge! options
98
+ @input = setup_input_method(options[:input] || @input)
99
+ @output = setup_output_method(options[:output] || @output)
100
+ @output.silenced = options.key?(:silent) ? options[:silent] : !@output || @output.silenced?
101
+ @line_processor = options[:processor] || options[:line_processor] || @line_processor
102
+ @allow_ruby = options.key?(:allow_ruby) ? options[:allow_ruby] : @allow_ruby
103
+
104
+ if options[:namespace]
105
+ ns = options[:namespace] == :self ? self : options[:namespace]
106
+ if ns != @namespace
107
+ @namespace = ns
108
+ @namespace_binding = nil
109
+ end
110
+ end
111
+
112
+ if @input
113
+ @input.output = @output
114
+ @input.prompt = prompt
115
+ if @input.respond_to?(:completion_proc)
116
+ @input.completion_proc = proc { |line| autocomplete(line) }
117
+ end
118
+ end
119
+ end
120
+
121
+ # Returns the current set of options.
122
+ def gather_options
123
+ @_options
124
+ end
125
+ alias options gather_options
126
+
127
+ class << self
128
+ # Sets or returns the banner displayed when the console is started.
129
+ def banner(msg = nil)
130
+ if msg.nil?
131
+ @banner ||= ">> Interactive Console <<"
132
+ else
133
+ @banner = msg
134
+ end
135
+ end
136
+
137
+ # Sets or overrides a default option.
138
+ #
139
+ # Example:
140
+ #
141
+ # class MyConsole < Rink::Console
142
+ # option :allow_ruby => false, :greeting => "Hi there!"
143
+ # end
144
+ #
145
+ def option(options)
146
+ default_options.merge! options
147
+ end
148
+
149
+ # Sets or returns the prompt for this console.
150
+ def prompt(msg = nil)
151
+ if msg.nil?
152
+ @prompt ||= "#{name} > "
153
+ else
154
+ @prompt = msg
155
+ end
156
+ end
157
+
158
+ # Adds a custom command to the console. When the command is typed, a custom block of code
159
+ # will fire. The command may contain spaces. Any words following the command will be sent
160
+ # to the block as an array of arguments.
161
+ def command(name, case_sensitive = false, &block)
162
+ commands[name.to_s] = { :case_sensitive => case_sensitive, :block => block }
163
+ end
164
+
165
+ # Returns a hash containing all registered commands.
166
+ def commands
167
+ @commands ||= {}
168
+ end
169
+
170
+ # Default options are:
171
+ # :processor => Rink::LineProcessor::PureRuby.new(self),
172
+ # :output => STDOUT,
173
+ # :input => STDIN,
174
+ # :banner => true, # if false, Rink won't show a banner.
175
+ # :silent => false, # if true, Rink won't produce output.
176
+ # :rescue_errors => true # if false, Rink won't catch errors.
177
+ # :defer => false # if true, Rink won't automatically wait for input.
178
+ # :allow_ruby => true # if false, Rink won't execute unmatched commands as Ruby code.
179
+ def default_options
180
+ @default_options ||= {
181
+ :output => STDOUT,
182
+ :input => STDIN,
183
+ :banner => true,
184
+ :silent => false,
185
+ :processor => Rink::LineProcessor::PureRuby.new(self),
186
+ :rescue_errors => true,
187
+ :defer => false,
188
+ :allow_ruby => true,
189
+ }
190
+ end
191
+ end
192
+
193
+ command(:exit) { |args| instance_variable_set("@exiting", true) }
194
+
195
+ protected
196
+ # The default set of options which will be used wherever an option from #apply_options is unset or nil.
197
+ def default_options
198
+ @default_options ||= self.class.default_options
199
+ end
200
+
201
+ # The prompt that is displayed next to the cursor.
202
+ def prompt
203
+ self.class.prompt
204
+ end
205
+
206
+ # Executes the given command, which is a String, and returns a String to be
207
+ # printed to @output. If a command cannot be found, it is treated as Ruby code
208
+ # and is executed within the context of @namespace.
209
+ #
210
+ # You can override this method to produce custom results, or you can use the
211
+ # +:allow_ruby => false+ option in #run to prevent Ruby code from being executed.
212
+ def process_line(line)
213
+ args = line.split
214
+ cmd = args.shift
215
+
216
+ catch(:command_not_found) { return process_command(cmd, args) }
217
+
218
+ # no matching commands, try to process it as ruby code
219
+ if @allow_ruby
220
+ result = process_ruby_code(line)
221
+ puts " => #{result.inspect}"
222
+ return result
223
+ end
224
+
225
+ puts "I don't know the word \"#{cmd}.\""
226
+ end
227
+
228
+ def process_ruby_code(code)
229
+ prepare_scanner_for(code)
230
+ evaluate_scanner_statement
231
+ end
232
+
233
+ # Returns the instance of Rink::Lexer used to process Ruby code.
234
+ def scanner
235
+ return @scanner if @scanner
236
+ @scanner = Rink::Lexer.new
237
+ @scanner.exception_on_syntax_error = false
238
+ @scanner
239
+ end
240
+
241
+ # Searches for a command matching cmd and returns the result of running its block.
242
+ # If the command is not found, process_command throws :command_not_found.
243
+ def process_command(cmd, args)
244
+ commands.each do |command, options|
245
+ if (options[:case_sensitive] && cmd == command) ||
246
+ (!options[:case_sensitive] && cmd.downcase == command.downcase)
247
+ #return options[:block].call(args)
248
+ return instance_exec(args, &options[:block])
249
+ end
250
+ end
251
+ throw :command_not_found
252
+ end
253
+
254
+ private
255
+ include Rink::IOMethods
256
+
257
+ def evaluate_scanner_statement
258
+ _caller = eval("caller", namespace_binding)
259
+ scanner.each_top_level_statement do |code, line_no|
260
+ begin
261
+ return eval(code, namespace_binding, self.class.name, line_no)
262
+ rescue
263
+ # clean out the backtrace so that it starts with the console line instead of program invocation.
264
+ _caller.reverse.each { |line| $!.backtrace.pop if $!.backtrace.last == line }
265
+ raise
266
+ end
267
+ end
268
+ end
269
+
270
+ def namespace_binding
271
+ @namespace_binding ||= namespace.send(:binding)
272
+ end
273
+
274
+ def prepare_scanner_for(code)
275
+ # the scanner prompt should be empty at first because we've already received the first line. Nothing to prompt for.
276
+ scanner.set_prompt nil
277
+
278
+ # redirect scanner output to @output so that prompts go where they belong
279
+ scanner.output = @output
280
+
281
+ # the meat: scanner will yield to set_input whenever it needs another line of code (including the first line).
282
+ # the first yield must give the code we've already received; subsequent yields should get more data from @input.
283
+ first = true
284
+ scanner.set_input(@input) do
285
+ line = if !first
286
+ # For subsequent gets, we need a prompt.
287
+ scanner.set_prompt prompt
288
+ line = @input.gets
289
+ scanner.set_prompt nil
290
+ line
291
+ else
292
+ first = false
293
+ code + "\n"
294
+ end
295
+
296
+ line
297
+ end
298
+ end
299
+
300
+ def enter_input_loop
301
+ @exiting = false
302
+ while !@exiting && (cmd = @input.gets)
303
+ cmd.strip!
304
+ unless cmd.length == 0
305
+ begin
306
+ @last_value = process_line(cmd)
307
+ rescue SystemExit, SignalException
308
+ raise
309
+ rescue Exception
310
+ raise unless gather_options[:rescue_errors]
311
+ print $!.class.name, ": ", $!.message, "\n"
312
+ print "\t", $!.backtrace.join("\n\t"), "\n"
313
+ end
314
+ end
315
+ end
316
+ @last_value
317
+ end
318
+
319
+ # Runs the autocomplete method from the line processor, then reformats its result to be an array.
320
+ def autocomplete(line)
321
+ return [] unless @line_processor
322
+ result = @line_processor.autocomplete(line, namespace)
323
+ case result
324
+ when String
325
+ [result]
326
+ when nil
327
+ []
328
+ when Array
329
+ result
330
+ else
331
+ result.to_a
332
+ end
333
+ end
334
+ end
335
+ end