Skip to content

Instantly share code, notes, and snippets.

@hayeah
Created April 20, 2010 05:52
Show Gist options
  • Save hayeah/372105 to your computer and use it in GitHub Desktop.
Save hayeah/372105 to your computer and use it in GitHub Desktop.
require 'rubygems'
require 'rdiscount'
require 'nokogiri'
$blog = [:blog,
[[:timestamps, "2009-03-17T16:04:34-07:00"],
[:title, "Does Ruby Dream An Eclectic Shell?"],
[:categories, "hack"]],
[[:md,
[],
["# Does Ruby Dream An Eclectic Shell?\n\nWhenever I encounter something that sounds even slightly hard to do in Bash, I'd think: \"hmmm, how do I do this in Bash? Oh, I know, I'll use Ruby.\" So I never actually bothered to learn Bash. Better for my sanity.\n\nBut sometimes in the depth of night, contemplating my eventual existential end, I'd think to myself, hmmm, wouldn't it be nice to have a shell that's consistent, expressive, powerful, concise, and **FUN** to use?\n\nI was motivated by two thoughts:\n\n+ Ruby's syntax makes a **plausible** shell.\n+ Ruby itself makes a **compelling** shell.\n\nSo I wrote a Ruby shell called [Rubish](http://github.com/hayeah/rubish/tree/master). Rubish, like Ruby, **is object oriented** from the ground up, and it only uses Ruby syntax (**it has no metasyntax** of its own). And unlike Bash, Rubish is not rubbish. (Always fun to bash Bash).\n\nIn a series of articles, I'll write about Rubish itself, its design, and what potentials it could have (aside from being yet another attempt at programmatic shell that's merely _quaint_.).\n\nAlong the way, I'll have digressions about the internals of Rubish to demostrate metaprogramming techniques in Ruby. Feel free to skip these, but I hope they would stimulate your hacking muscles as they did mine. For example, what use is a class that has all its methods undefined (I wonder if Ruby classes have castration anxiety)?\n\n"]],
"\n\n",
[:ruby,
[],
["class Mu\n self.public_instance_methods.each do |m|\n self.send(:undef_method,m)\n end\nend"]],
"\n\n",
[:md,
[],
["For the rest of this article, I'd like to ask you to imagine how you'd design a Ruby shell yourself.\n\n#Now, Imagine!\n\nRuby's syntax is concise enough that in the degenerate case, it's like any other Unix shell.\n"]],
"\n\n",
[:ruby, [], ["> ls\n> ls :la # i.e. ls -la\n> ls \"-l *\""]],
"\n\n",
[:md,
[],
["Command evaluations are just method calls handled by the shell object. Of course, since we are `eval`ing, any Ruby expression is valid.\n"]],
"\n\n",
[:ruby, [], ["> 1+1\n2\n> \"abcd\" == \"dcba\"\nfalse"]],
"\n\n",
[:md,
[],
["Since it's Ruby, it follows that we can abstract unix commands as objects."]],
"\n\n",
[:ruby,
[],
["> @cmd = ls ; false\nfalse\n> @cmd.inspect\n\"<#Rubish::Executable @cmd=\"ls\">\""]],
"\n\n",
[:md,
[],
["Because commands are objects, it's easy to build extra functionalities for them with Ruby. All these extensions to unix commands are just be _ad hoc_ wrappers that munge lines from a pipe. So unlike [PowerShell(TM)](http://en.wikipedia.org/wiki/Powershell), Rubish assumes no specialized support from the underlying platform.\n\nYou could, for example, imagine a mixin for the `ls` command (I say \"imagine\", because this is not necessarily useful or even pretty. But it illustrates the possibilities).\n"]],
"\n\n",
[:ruby,
[],
["# we'll mix this into the an Executable instance\nmodule LsMixin\n def each_file(filter=nil)\n # Executable#each yields to a block each line\n # of the executable's output.\n self.each do |line|\n if filter.nil? or line =~ filter\n f = File.new(line)\n yield(f)\n end\n end\n end\nend"]],
"\n\n",
[:md,
[],
["Then you could extend `ls` so you yield to a block each line of its output as a Ruby File object,"]],
"\n\n",
[:ruby,
[],
["# use the filter to include only *.rb\n> ls.extend(LsMixin).each_file(/.rb$/) { |f|... }"]],
"\n\n",
[:md,
[],
["To summarize again,\n\n+ Ruby's concise syntax makes a **plausible** shell.\n+ Ruby itself makes a **compelling** shell.\n\nIn the next article, I'd like to give you a tour of Rubish. And before we go into Rubish itself, I'd love to know how *YOU* would design a Ruby shell given these ideas. So take a coffee break, and imagine :)\n\nCheck out these shells for inspirations:\n\n* [iPython](http://ipython.scipy.org/moin/) is in Python. It has fairly extensive metasyntax and magic to make it practical. Whereas for Rubish, I chose not to add any metasyntax, because Ruby's own is (arguably) good enough.\n\n* [rush](http://rush.heroku.com) is another ruby shell by [Adam Wiggins](http://adam.blog.heroku.com). I actually wanted to call my shell Rush. But he got there first. Rush is far more Rubyesque than Unixy. And it's more designed for handling multiple machines over network. Rubish tries to fuse better with Unix, but keeping with Ruby's spirit.\n\n* [scsh](http://www.scsh.net/) is a shell in scheme. Being a Lisp weenie, how can I not mention this? Scsh is the work of Olin Shivers, implemented on [Scheme48](http://groups.csail.mit.edu/mac/projects/s48/). Shivers' [acknowledgement](http://www.scsh.net/docu/html/man.html) is worth reading. I couldn't get scsh to work though.\n\n"]],
"\n\n\n"]]
class Curly
def initialize(parse)
@parse = parse
end
def generate
build_xml(@parse)
end
protected
def build_xml(element)
doc = Nokogiri::XML::Document.new
doc.add_child(build_node(doc,element))
doc
end
def build_node(doc,element)
head, attributes, children = element
node = create_element(doc,head,attributes)
children.each { |child|
insert(node,child)
}
node
end
def create_element(doc,head,attributes)
attributes = attributes.inject({}) { |h,(k,v)|
h[k] = v; h
}
name, id, classes = parse_tag_name(head)
node = doc.create_element(name,attributes)
if id
node["id"] = id
end
if !classes.empty?
node["class"] = classes.join(" ")
end
node
end
def insert(node,element)
doc = node.document
case element
when String
node << doc.create_text_node(element)
when Array
head, attributes, body = element
case head
when %s(!)
# do nothing
when :cdata
node << Nokogiri::XML::CDATA.new(doc, body.to_s)
else
child = build_node(node.document,element)
node << child
end
end
end
def parse_tag_name(head)
head = head.to_s
id = nil
head.gsub!(/#([^.]*)/) { |match|
id = $1
raise "empty id" if id.empty?
""
}
tokens = head.split(".")
head = tokens.shift
classes = tokens
raise "empty head" if head.empty?
return [head,id,classes]
end
end
def dom
Curly.new($blog).generate
end
def markdown(text)
RDiscount.new(text).to_html
end
def process
div = dom
##################################################
# markdown
(div / "md").each do |node|
md = markdown(node.content)
frag = Nokogiri::XML::DocumentFragment.parse(md)
node.replace(frag)
end
end
loop {
process
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment