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