The latest release of RubyShell is so cool, it’s skipping right over 0.4 and straight to 0.5. If you want to skip the explanation, you can go ahead and download it now.

It bears mentioning also that this release marks the first release of RubyShell on Rubyforge. Thanks to Rubyforge for being awesome :)

RubyShell 0.5 adds some tasty flavors to the table with four new treats. First up are shell objects.

Shell Objects

Shell objects are not my idea :) viraptor waited until the moment that RubyShell was on Rubyforge to unleash this cool little concept. Basically, these are objects that represent handy Ruby interfaces to useful concepts. Along with an initial implementation, viraptor provided two such object classes: ps and if_config. ps is used to get to information about processes, while if_config gives information about network interfaces (just like their command counterparts). Here are some examples of using ps (with some parts abbreviated):


RubyShell> procs.collect { |proc| proc.name } .inspect
=>["init", "migration/0", "ksoftirqd/0", "migration/1", "ksoftirqd/1",
"events/0", "events/1", "khelper", "kthread", "kblockd/0", "kblockd/1", 
"kacpid", "ksuspend_usbd", ...,"artsd"]
RubyShell> ps.named('nspluginviewer').inspect
=>[#<RubyShell::ShellObjects::Ps::Process:0xb78a3b8c @name="nspluginviewer",
@cmdline=["/usr/kde/3.5/bin/nspluginviewer", "-dcopid", "nspluginviewer-27450"], @pid=27521>]
RubyShell> ps.named(/ns/).inspect
=>[#<RubyShell::ShellObjects::Ps::Process:0xb7878edc @name="mdnsd", @cmdline=["/usr/sbin/mdnsd"],
 @pid=6809>, #<RubyShell::ShellObjects::Ps::Process:0xb785ac20 @name="nspluginviewer",
@cmdline=["/usr/kde/3.5/bin/nspluginviewer", "-dcopid", "nspluginviewer-27450"], @pid=27521>]
RubyShell> ps.pids.inspect
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 88, 89, 90, 223, 226, 228, 240, 261, 262, 263, 861, 1019,
1066, 1068, 1069, 5006, 5090, 5173, 5174, 5180, 5181, 5182, 5183, 5186, 5199,
5201, 5337, 5381, 5443, 5532, 5949, 6060, 6202, 6224, 6225, 6226, 6316, 6633, ..., 29380, 31581]
RubyShell> ps.from_pid(1).inspect
=>#<RubyShell::ShellObjects::Ps::Process:0xb783a948 @cmdline=["init [3]"], @pid=1>
RubyShell> ps.from_name('init').inspect
=>#<RubyShell::ShellObjects::Ps::Process:0xb7834ae8 @name="init", @cmdline=["init [3]"], @pid=1>

You can also kill a process (ps.from_pid(1).kill ‘TERM’, for example) and do a few other things like check if it’s alive or if it’s a kernel process and get the parent process.

A couple of bugs made their way into the 0.5 release, including output that doesn’t call inspect automatically on its results and a ruby indicator that isn’t followed by a space. There’s also a bug in the Process class used in ps that means if it’s printed without an inspect, an error arises. These will all be fixed in RubyShell 0.6 (and, in fact, already are in trunk).

Here is some of what you can do with if_config:


RubyShell> if_config.interfaces.inspect
=>[#<RubyShell::ShellObjects::IfConfig::Base::Interface:0xb780ace8 @name="lo">,
#<RubyShell::ShellObjects::IfConfig::Base::Interface:0xb780ac48 @name="eth0">]
RubyShell> if_config.names.inspect
=>["lo", "eth0"]
RubyShell> if_config['eth0']
=>#<RubyShell::ShellObjects::IfConfig::Base::Interface:0xb77f4c2c @name="eth0">
RubyShell> if_config.eth0.inspect
=>#<RubyShell::ShellObjects::IfConfig::Base::Interface:0xb77f4c2c @name="eth0">
RubyShell> if_config['eth0'].ip
=>192.168.2.39
RubyShell> if_config['eth0'].mask
=>255.255.255.0

There’s the potential for much much more, but for the time being this is about it for if_config.

So these are shell objects. They’re a very promising idea, and I hope some people will contribute more objects. Note, though, that the way shell objects are implemented in 0.5 and currently in trunk is likely to change, I just haven’t thought it through 100% yet.

File Handling

Until 0.5, RubyShell only operated interactively—i.e., you couldn’t just hand it a file and have it read and run that file. That is no longer the case. Now, you can pass files on the command line to the ruby_shell.rb script and it will execute all of them. Currently, it executes them all in the same context; I’m still deciding whether or not I like this better than executing them in separate contexts.

Hand in hand with this new addition is the addition of an initialization file. RubyShell will now try to read a ~/.rbshrc and run whatever initialization code is there before running, either for files or interactively. This file can contain anything that’s valid RubyShell code. Here’re the contents of mine as it stands:


require 'colored'
hostname | host = $pipe.strip
shell.prompt = Proc.new do
  "\#{ENV['USER']}@\#{host}".green + " \#{Dir::pwd.gsub(File.expand_path('~'), '~')} # ".blue
end

alias 'ls', 'ls --color'

Whammo! And therein lie hints as to the two remaining treats that await within…

Interpolation

RubyShell now supports interpolation of RubyShell code. That is to say, it supports executing code and replacing that section of the line with the code’s results. This is what’s usually done in ``s in other shells like bash. As it turns out, RubyShell 0.5 uses #{} for this, just like string interpolation for Ruby.

Sadly, this interferes with the actual string interpolation. As you can see above, you can escape such an interpolation using \#. If that isn’t done in the above section, you’d get an interpolation that would happen once when the RubyShell parser hit it, and never again—thus resulting in a prompt that never updated its values. RubyShell 0.6 will, instead, use the traditional backticks (``) to just dodge this problem entirely.

Command Aliases

Finally, RubyShell 0.5 supports aliasing commands. This is done through a normal Ruby method, alias_cmd. It just takes a command and what it should become. However, the command can also be a regular expression, or anything that will behave nicely under #===. For example:


RubyShell> alias_cmd 'ls', 'ls --color'
RubyShell> alias_cmd /ram/, 'ls'
RubyShell> arama
*results of alsa*

The value of the regular expression thing is one I’m still not 100% sure of, but it seems like it could be used for interesting and cool things. Also, through some hackery in the RubyEvaluator, instead of alias_cmd, you may use alias. The only caveat is that alias has to be the very very first thing in the statement. Otherwise, the replacement won’t take place (so that regular aliasing can be used). Thus:


RubyShell> alias 'ls', 'ls --color'

What’s Next

On the horizon is an implementation of output redirection. Currently, the plan is to make the syntax `word>’, where `word’ corresponds to an actual method somewhere (haven’t decided where yet) that will take the filename as a parameter and could potentially apply transformations to the output as it goes.

12 Responses to “RubyShell 0.5: Objects, Files, Interpolation, and Aliasing”

  1. she Says:

    cool!

    now its time to make it known :)

    I found this due to someone else posting the link onto IRC

  2. Shadowfiend Says:

    Indeed it is! This is going to be a word-of-mouth thing, though :) I do my best by posting the release announcements on DZone, which usually gets them on the Ruby Inside front page. Plus we’re on Rubyforge now, so we’re somewhat easier to find. Beyond that, though, it’s going to be pure word of mouth.

    The first thing that my bash shell does at this point in its .bashrc is start up RubyShell :) Though there are still some bugs. Particularly with parsing of ../-style directories. I need to investigate those, as I’m not managing to reproduce them well.

  3. nec Says:
    when are we going to be able to do: RubyShell>cd codingmp3s RubyShell>Dir.glob("*.mp3").first.play
  4. Shadowfiend Says:

    Considering how computer-specific that is (what do you use? mpg123? amarok via DCOP? Something else via DBUS?), I’d say never within RubyShell proper. But it should be relatively straightforward to implement in .rbshrc by extending the File or the String class (since Dir.glob returns an array of Strings). I suppose some samples can be included of how to implement something like that.

    Alternatively, it could be implemented as a shell object, maybe called mp3s. So you could do, say:

    mp3s.play
    mp3s.list
    mp3s.select { |mp3| mp3.filename =~ /^Bob Dylan/ }.play

    With some more work (and some auxiliary libraries), you could even do:

    mp3s.for_artist('Bob Dylan').play
    mp3s.for_artist('50 Cent').delete!

    Or something along those lines.

  5. yard Says:

    Hm, there seems to be a problem with files starting with .>

    ~/test # ls -a . .. .test.txt test.txt ~/test # cat test.txt test ~/test # cat .test.txt Ruby Error: undefined local variable or method `cat’ for #<rubyshell::evaluation::rubyevaluator:0xb79c2fe0>

  6. Shadowfiend Says:

    While we’re talking about this, you could really go to town on things:

    music.for_type_and_artist('mp3', 'Bob Dylan').play
    music.for_artist('50 Cent').delete!
    music.for_type('...')
  7. Shadowfiend Says:

    There are more problems than just that one you mentioned, yard. These all seem to be situations where the Ruby validator determines that something is valid Ruby when it’s really a command. I’m thinking about how I can fix that.

    The strangest thing is that even if it’s declared as valid Ruby, the fact that cat is undefined should automatically mark it as invalid so that it can be handled as a command. This might be a mistake on my part in how I’m handling the parse tree returned by ParseTree.

  8. 7rans Says:

    Is is possible to use RubyShell in one’s Ruby scripts as a library?

  9. Shadowfiend Says:

    It depends on what you mean. You can start a RubyShell event loop with relative ease—this is basically the task that ruby_shell.rb does, and if you look in there you’ll see it’s relatively straightforward.

    If you mean passing your own commands to RubyShell outside of the user, it’s just a little bit more difficult, in the sense that you have to set up everything like you would for the event loop, but you also have to manually call the parser’s parsing method instead of using the monitors to do that for you. Still, this is as simple as invoking parser.parse_input with the appropriate value.

    Keep in mind also that you might have to write a new Outputter class to handle RubyShell’s output yourself, unless you want the results delivered to STDOUT as usual.

  10. Daniel Draper Says:

    I’d love to do this:

    Have a dir called foo/bar/ that has say 100 directories in it.

    Simply do

    ls foo/bar.first to see whats in the first one of those

    or maybe

    ls foo/bar.children.first

    Then if you wanted to move all dirs up a level you could do

    foo/bar.children.each_with_parent { |c,parent| mv c, parent.parent }

    and so on.

  11. Shadowfiend Says:

    Hmm… That’s interesting. Actually, that’s probably relatively easily doable with a little method_missing magic at the basic level. The problem being, of course, that any existing methods won’t work right as directories.

    There’s something nagging at me about directories… I should really come up with something that’ll catch all directory references and automagically figure out that they’re directory references. The problem is just about all directory references are also valid Ruby. That’s what’s complicating a lot of things… Also the fact that I don’t have a lot of time to work on RubyShell right now, but that’s secondary :-P

  12. Shadowfiend Says:

    Ok, so I’m definitely playing with this idea. I think I’ll make it so that all of the above examples will work, with one caveat: `foo/bar.children…’ will only work if `foo’ is not a local variable or method, since otherwise this could easily be some other Ruby code. Instead, there’ll be a Ruby method `dir’ or `path’ that will convert a string to a path object:

    
    RubyShell> foo/bar.children
    [#<RubyShell::FilePath: 'foo/bar/.'>, #<RubyShell::FilePath: 'foo/bar/..'>, #<RubyShell::FilePath: 'foo/bar/test'>]
    RubyShell> foo = 5
    RubyShell> foo/bar.children
    Ruby Error: undefined local variable or method `bar' for #<RubyShell::Evaluation::RubyEvaluator:0xb7841388>
    RubyShell> path('foo/bar').children
    [#<RubyShell::FilePath: 'foo/bar/.'>, #<RubyShell::FilePath: 'foo/bar/..'>, #<RubyShell::FilePath: 'foo/bar/test'>]
    RubyShell> ls foo/bar.children.first
    .  ..  test/
    

    I’m currently thinking of some useful things like providing a glob method on FilePath objects that’ll return FilePath objects, but operate like Dir.glob, and other such things. I’m going to add this to the feature tracker on Rubyforge now that I’ve got a more solid idea of where things are going.

Sorry, comments are closed for this article.