Skip to content

Instantly share code, notes, and snippets.

@sneakin
Last active December 10, 2015 23:59
Show Gist options
  • Save sneakin/4513749 to your computer and use it in GitHub Desktop.
Save sneakin/4513749 to your computer and use it in GitHub Desktop.
A very basic Unix shell using #method_missing.
*~
TODO.html
#!/usr/bin/env ruby
require 'pathname'
$: << Pathname.new(__FILE__).parent.parent.join("lib").to_s
require 'rash'
Rash.new do
$stderr.write("\r$ ")
begin
$stderr.puts("# => " + instance_eval($stdin.readline).to_s)
$stderr.write("\r$ ")
end until $stdin.eof?
end
# Create an instance to call shell commands as though they were instance methods.
class Rash
def initialize(&block)
instance_eval(&block) if block_given?
end
def exit(code = 0)
Kernel.exit(code)
end
# Executes the shell command sharing the missing method's name. Passes any arguments
# as strings and yields a read/write pipe if a block is given.
def method_missing(command, *arguments, &piper)
cmdline = ([command] + arguments).map(&:to_s)
if block_given?
IO.popen(cmdline, 'r+', &piper)
else
system(*cmdline)
end
$? && $?.exitstatus
end
end
require 'pathname'
require 'open3'
describe 'bin/rash' do
context 'with no arguments' do
let(:cmd) { Pathname.new(__FILE__).parent.parent.parent.join("bin", "rash").to_s }
before do
@input, @output, @err, @wait_thr = Open3.popen3(cmd)
end
after do
@input.close unless @input.closed?
@output.close unless @output.closed?
@err.close unless @err.closed?
end
it "prints a prompt to stderr" do
@err.read(3).should == "\r$ "
end
it "reads and evaluates by line" do
@input.puts("echo 'hello'")
@output.readline.should == "hello\n"
@input.puts("echo 'world'")
@output.readline.should == "world\n"
end
it "prints the evaluated return value before the next prompt" do
@input.puts("ls '/boowho'")
@err.readline.should include('ls')
@err.readline.should == "# => 2\n"
@err.read(3).should == "\r\$ "
end
it "exits when the input stream closes" do
@wait_thr.should be_alive
@input.close
sleep(0.125)
@wait_thr.should_not be_alive
end
it "evaluates the input as Ruby" do
@input.puts("2 + 2")
@err.readline.should include("# => 4")
@input.puts("false")
@err.readline.should include("# => false")
@input.puts("self.false")
@err.readline.should include("# => 1")
@input.puts("x = 3; x + 2")
@err.readline.should include("# => 5")
end
end
end
describe Rash do
context 'initialization without arguments' do
it "does nothing" do
lambda { described_class.new }.should_not raise_error
end
end
context 'initialization with a block' do
it "evaluates the block" do
expect { |b| described_class.new(&b) }.to yield_control
end
end
describe '#method_missing' do
subject { described_class.new }
context 'without a block' do
it "executes the named shell command whose name matches the method's name" do
subject.should_receive(:system).with("ls")
subject.ls
end
it "passes the method's arguments to the command as strings" do
subject.should_receive(:system).with("echo", "hello", "123")
subject.echo(:hello, 123)
end
it "returns the shell command's exit status" do
subject.false.should == 1
subject.true.should == 0
end
end
context 'with a block' do
it "executes the named shell command whose name matches the method's name" do
subject.ls("/") do |io|
line = io.readline
%W(bin usr proc).should include(line.strip)
end
end
it "passes the method's arguments to the command as strings" do
subject.echo(:hello, 123 * 2) do |io|
io.readline.should == "hello 246\n"
end
end
it "yields a pipe for the command's stdin/out to the block" do
subject.sed('-e', 's/hello/goodbye/gi') do |io|
io.should be_kind_of(IO)
io.puts("Hello world")
io.close_write
io.readline.should == "goodbye world\n"
end
end
it "returns the shell command's exit status" do
subject.cat { |io| io.close_write }.should == 0
subject.ls("wtf") { |io| io.close_write }.should_not == 0
subject.false.should == 1
subject.true.should == 0
end
end
end
describe '#exit' do
it "exits Rash with the supplied exit code" do
Kernel.should_receive(:exit).with(123)
subject.exit(123)
end
end
end
TODO
+ [ ] pipes:
#+BEGIN_SRC ruby
rash.echo("hello world") | rash.sed("-e", "s/hello/HELLO/") | rash.cat { |io| io.readline } # => "HELLO world\n"
#+END_SRC
+ [ ] redirection: ~< > >&~
+ [ ] job control
+ [ ] exception handling
+ [ ] signal trapping
+ [ ] environent variables
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment