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 +5 -0
- data/.gitignore +22 -0
- data/LICENSE +20 -0
- data/README.rdoc +135 -0
- data/Rakefile +47 -0
- data/VERSION +1 -0
- data/bin/rink +66 -0
- data/lib/core_ext/object.rb +15 -0
- data/lib/rink/console.rb +335 -0
- data/lib/rink/delegation.rb +139 -0
- data/lib/rink/input_method/base.rb +41 -0
- data/lib/rink/input_method/file.rb +24 -0
- data/lib/rink/input_method/io.rb +34 -0
- data/lib/rink/input_method/readline.rb +49 -0
- data/lib/rink/io_methods.rb +44 -0
- data/lib/rink/lexer.rb +58 -0
- data/lib/rink/line_processor/base.rb +23 -0
- data/lib/rink/line_processor/pure_ruby.rb +180 -0
- data/lib/rink/output_method/base.rb +31 -0
- data/lib/rink/output_method/io.rb +22 -0
- data/lib/rink.rb +12 -0
- data/rink.gemspec +83 -0
- data/spec/lib/core_ext/object_spec.rb +19 -0
- data/spec/lib/rink/console_spec.rb +129 -0
- data/spec/lib/rink/io_methods_spec.rb +39 -0
- data/spec/lib/rink/pure_ruby_line_processor_spec.rb +33 -0
- data/spec/lib/rink_spec.rb +5 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +12 -0
- metadata +110 -0
data/.document
ADDED
data/.gitignore
ADDED
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
|
data/lib/rink/console.rb
ADDED
@@ -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
|