rink 1.0.0

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