Advertisement

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 ThreadErrors 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.

References

Discussion

If you found a mistake in this article or would like to contribute some content to this article, please file an issue in this Git Repository

Related Articles