Recently I added a Read-Eval-Print-Loop-Console
(REPL)-console to the
aruba
-project as playground for our users similar to the one you find in rails
.
This article first shows you aruba
’s console a bit and then describes how you
can build your own console in Ruby with some help of IRB
.
Playing around with “aruba”’s console
aruba
’s console is a based on Ruby’s very own
IRB
with some of IRB
’s extensions activated
– e.g. history and command-completion. You need to install aruba
with
the following command.
gem install aruba -v 0.9.0
After that you can start the console. But be aware that this will setup
tmp/aruba
. If a directory with this name already exists in your current
working directory, it will be deleted and re-created.
aruba console
As said before, the console supports a history and has command-completion enabled for
all aruba
-methods. So you might want to enter “create_dire” and press
<TAB>
.
aruba:001:0> create_directory
Then pass the name of the directory you want to create to the method and press
<ENTER>
.
aruba:001:0> create_directory 'dir.d'
If you now have a look at the filesystem you will see, that a directory named
tmp/aruba/dir.d
exists – please don’t close aruba
’s console to check
this but open a new terminal.
ls -al tmp/aruba/
# => drwxr-xr-x 2 d d 4096 Aug 25 09:13 tmp/aruba/dir.d
You can even cd
to the directory, run a command within it and output STDOUT
of the command.
# Run "create_directory" again if you closed the console before
aruba:001:0> create_directory 'dir.d'
aruba:001:0> cd('dir.d')
aruba:001:0> run('pwd')
aruba:001:0> puts last_command_start.stdout
/home/<user>/test/tmp/aruba/dir.d
# => nil
You can also use plain Ruby in this console – only the output is
slightly different from plain IRB
since it uses a modified prompt.
aruba:001:0> result = 1 + 1
# => 2
aruba:001:0> puts result
2
# => nil
Build your own console based on “IRB”
Without Context-Object
If you have the need to build your own console, we’re now coming to the fun part. I will go through some iterations of my code before I present the latest version of it. In the first iteration I tried the following.
require 'irb'
IRB.start
With Context-Object
Unfortunately it was not possible to pass an Object
to IRB.start
to use it
as context for command-completion. So I looked for another solution. Then I
found [1] and used this article as starting point to implement the
next version of “aruba”’s console.
require 'irb'
IRB.setup nil
IRB.conf[:MAIN_CONTEXT] = IRB::Irb.new.context
require 'irb/ext/multi-irb'
IRB.irb nil, self
With this approach you can use another Object
as context – just
replace self
in IRB.irb nil, self
.
With Context-Object and without ThreadErrors
Unfortunately I sometimes got ThreadError
s with the code found above. That’s
why I decided to try something different after reading IRB
’s source code.
This is the result. Please don’t wonder, I removed all code referencing aruba
to make the example more readable. I will explain the details down below.
require 'irb'
module MyProject
class Console
def start
ARGV.clear
IRB.setup nil
context = Class.new do
def hello_world
'Hello World'
end
end
irb = IRB::Irb.new(IRB::WorkSpace.new(context.new))
IRB.conf[:MAIN_CONTEXT] = irb.context
trap("SIGINT") do
irb.signal_handle
end
begin
catch(:IRB_EXIT) do
irb.eval_input
end
ensure
IRB.irb_at_exit
end
end
end
end
MyProject::Console.new.start
The following lines set everything up. You need to clear ARGV
before
running IRB.setup
. Otherwise IRB
will try to use the content of ARGV
and
fail with an error.
ARGV.clear
IRB.setup nil
The context for the console is an anonymous class. You can use .include
to include your
own API-module. This is what we do in aruba
’s
console-code.
context = Class.new do
# include YourApiModule
def hello_world
'Hello World'
end
end
Next, we create an IRB::WorkSpace
-object which gets an instance of our
anonymous class as context with context.new
. After that we pass the workspace
to IRB::Irb.new
to create our console.
irb = IRB::Irb.new(IRB::WorkSpace.new(context.new))
IRB.conf[:MAIN_CONTEXT] = irb.context
The following code evaluates the user input.
catch(:IRB_EXIT) do
irb.eval_input
end
And this handles SIGINT
if the user presses CTRL+C
.
trap("SIGINT") do
irb.signal_handle
end
And you should definitively run the following code, otherwise some
IRB
-extensions won’t work – e.g. irb/ext/save-history
because it run
all registered at_exit
-hooks.
IRB.irb_at_exit
The last line starts the console.
MyProject::Console.new.start
To run the console, please copy & paste the code to a file named console.rb
and
run it with ruby
.
ruby console.rb
Please enter hello_world
and followed by <ENTER>
.
irb(#<#<Class:0x00000001bb9030>:0x00000001bb8f90>):001:0> hello_world<ENTER>
# => "Hello World"
With Context-Object, with History and Command-Completion
To make the life of your users a little bit easier, we’re now adding some more
code to modify the prompt, activate IRB
’s history and command-completion.
Please see the full code below plus some more information about the added
code sections.
require 'irb'
module MyProject
class Console
def start
ARGV.clear
IRB.setup nil
IRB.conf[:PROMPT] = {}
IRB.conf[:IRB_NAME] = 'myproject'
IRB.conf[:PROMPT][:MY_PROJECT] = {
:PROMPT_I => '%N:%03n:%i> ',
:PROMPT_N => '%N:%03n:%i> ',
:PROMPT_S => '%N:%03n:%i%l ',
:PROMPT_C => '%N:%03n:%i* ',
:RETURN => "# => %s\n"
}
IRB.conf[:PROMPT_MODE] = :MY_PROJECT
IRB.conf[:RC] = false
require 'irb/completion'
require 'irb/ext/save-history'
IRB.conf[:READLINE] = true
IRB.conf[:SAVE_HISTORY] = 1000
IRB.conf[:HISTORY_FILE] = '~/.my_project_history'
context = Class.new do
def hello_world
'Hello World'
end
end
irb = IRB::Irb.new(IRB::WorkSpace.new(context.new))
IRB.conf[:MAIN_CONTEXT] = irb.context
trap("SIGINT") do
IRB.irb.signal_handle
end
begin
catch(:IRB_EXIT) do
irb.eval_input
end
ensure
IRB.irb_at_exit
end
end
end
end
MyProject::Console.new.start
To change the name of the “prompt” use the following code.
IRB.conf[:IRB_NAME] = 'myproject'
Using this code, you can change the prompt to your needs
– IRB
-documentation. First you should pass an empty Hash
to
IRB.conf[:PROMPT]
. Then you can define your own prompt mode using an
arbitrary Symbol
.
IRB.conf[:PROMPT] = {}
IRB.conf[:PROMPT][:MY_PROJECT] = {
:PROMPT_I => '%N:%03n:%i> ', # myproject:001:0>
:PROMPT_S => '%N:%03n:%i%l ', # myproject:002:0"
:PROMPT_C => '%N:%03n:%i* ', # myproject:002:0*
:RETURN => "# => %s\n"
}
IRB.conf[:PROMPT_MODE] = :MY_PROJECT
As we don’t want IRB
not to read any .irbrc
we set RC
to false
.
IRB.conf[:RC] = false
To record a history for your users, use the following code and adjust it to
your needs. It records 1.000 commands to ~/.my_project_history
. That’s it.
You now have got a working REPL for your projects.
require 'irb/completion'
require 'irb/ext/save-history'
IRB.conf[:READLINE] = true
IRB.conf[:SAVE_HISTORY] = 1000
IRB.conf[:HISTORY_FILE] = '~/.my_project_history'
And again: To run the console, please copy & paste the code to a file named
console.rb
and run it with ruby
.
ruby console.rb
Please enter hello
and press <TAB>
. You should now see hello_world
. After
that, please press <ENTER>
.
myproject:001:0> hello<TAB>
myproject:001:0> hello_world<ENTER>
# => "Hello World"
Using Alternatives like “Pry”
You might ask yourself, why I decided to use IRB
instead Pry
or something
else. Personally I use pry
a lot and like it very much, but there’s at least
one method-name-conflict: Both projects – aruba
and pry
– use
cd
, but with a different implementation.
If this not a problem for you, you might want to give Pry
a try to build your
REPL-console. You can pass Pry.start
an Object. It will use this object as
context. Just use this piece of code in your project.
require 'pry'
class MyClass
# include MyApi
def hello_world
'Hello World'
end
end
Pry.start MyClass.new
Then save it to console.rb
and run it with ruby
.
ruby console.rb
You should then see the following output
[1] pry(#<MyClass>)>hello_world
# => "Hello World"
That’s it. Thanks for reading.