chitin 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/ANNOUNCEMENT +67 -0
- data/README +414 -0
- data/TODO +32 -0
- data/bin/chitin +26 -0
- data/chitin.gemspec +31 -0
- data/chitinrc +86 -0
- data/lib/chitin/commands/builtins.rb +224 -0
- data/lib/chitin/commands/executable.rb +139 -0
- data/lib/chitin/commands/pipe.rb +112 -0
- data/lib/chitin/commands/ruby.rb +152 -0
- data/lib/chitin/commands/runnable.rb +84 -0
- data/lib/chitin/core_ext/coolline.rb +104 -0
- data/lib/chitin/core_ext/io.rb +7 -0
- data/lib/chitin/core_ext/object.rb +21 -0
- data/lib/chitin/core_ext/string.rb +35 -0
- data/lib/chitin/core_ext/symbol.rb +6 -0
- data/lib/chitin/file.rb +163 -0
- data/lib/chitin/sandbox.rb +18 -0
- data/lib/chitin/session.rb +167 -0
- data/lib/chitin/support.rb +51 -0
- data/lib/chitin/tools/csv.rb +0 -0
- data/lib/chitin/tools/replay.rb +45 -0
- data/lib/chitin/tools/string_methods.rb +10 -0
- data/lib/chitin/version.rb +4 -0
- data/lib/chitin.rb +26 -0
- data/sample.txt +187 -0
- metadata +126 -0
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'coolline'
|
2
|
+
|
3
|
+
class Coolline
|
4
|
+
def bind(key, &action)
|
5
|
+
@handlers.unshift Handler.new(key, &action)
|
6
|
+
end
|
7
|
+
|
8
|
+
# This is to deal wtih adding closing quotes on completions.
|
9
|
+
# It assumes that the completion proc will automatically place
|
10
|
+
# a leading quote on the line.
|
11
|
+
def complete
|
12
|
+
return if word_boundary? line[pos - 1]
|
13
|
+
|
14
|
+
completions = @completion_proc.call(self)
|
15
|
+
|
16
|
+
if completions.empty?
|
17
|
+
menu.string = ""
|
18
|
+
elsif completions.size == 1
|
19
|
+
menu.string = ""
|
20
|
+
word = completions.first
|
21
|
+
|
22
|
+
# don't close quotes on directories
|
23
|
+
if word[-1, 1] != '/'
|
24
|
+
word += word[0, 1] # because there's a quote from when we expand the options
|
25
|
+
end
|
26
|
+
|
27
|
+
self.completed_word = word
|
28
|
+
else
|
29
|
+
menu.list = completions
|
30
|
+
self.completed_word = common_beginning(completions)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
alias_method :old_word_beginning_before, :word_beginning_before
|
35
|
+
|
36
|
+
# This is to make word completion complete entire unquoted strings.
|
37
|
+
# Very useful since Ruby doesn't have space escapes like in Bash.
|
38
|
+
def word_beginning_before(pos)
|
39
|
+
# if the line is an uncompleted string, make it the whole string
|
40
|
+
# else, do the normal shit
|
41
|
+
if incomplete_string line[0..pos]
|
42
|
+
point = line[0..pos].reverse.index incomplete_string(line[0..pos])
|
43
|
+
line[0..pos].size - point - 1
|
44
|
+
else
|
45
|
+
old_word_beginning_before(pos)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# This is to comment out `reset_line`.
|
50
|
+
# Now why would we do that, you ask?
|
51
|
+
# Because otherwise it overwrites the entire line when printing the prompt.
|
52
|
+
# And what's wrong with that?
|
53
|
+
# This is undesirable behavior if the previous program run prints output
|
54
|
+
# that does NOT have a trailing newline.
|
55
|
+
# This was the source of a "bug" for a few days now.
|
56
|
+
#
|
57
|
+
# NB: This preserves any input on the line for ONLY the initial printing of
|
58
|
+
# the prompt. After that, the render call in the #getch loop will overwrite
|
59
|
+
# it. This needs some work, obviously.
|
60
|
+
def readline(prompt = ">> ")
|
61
|
+
@prompt = prompt
|
62
|
+
|
63
|
+
@history.delete_empty
|
64
|
+
|
65
|
+
@line = ""
|
66
|
+
@pos = 0
|
67
|
+
@accumulator = nil
|
68
|
+
|
69
|
+
@history_moved = false
|
70
|
+
|
71
|
+
@should_exit = false
|
72
|
+
|
73
|
+
#reset_line
|
74
|
+
print @prompt
|
75
|
+
|
76
|
+
@history.index = @history.size - 1
|
77
|
+
@history << @line
|
78
|
+
|
79
|
+
until (char = @input.getch) == "\r"
|
80
|
+
@menu.erase
|
81
|
+
|
82
|
+
handle(char)
|
83
|
+
return if @should_exit
|
84
|
+
|
85
|
+
if @history_moved
|
86
|
+
@history_moved = false
|
87
|
+
end
|
88
|
+
|
89
|
+
render
|
90
|
+
end
|
91
|
+
|
92
|
+
@menu.erase
|
93
|
+
|
94
|
+
print "\n"
|
95
|
+
|
96
|
+
@history[-1] = @line if @history.size != 0
|
97
|
+
@history.index = @history.size
|
98
|
+
|
99
|
+
@history.save_line
|
100
|
+
|
101
|
+
@line
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Object
|
2
|
+
|
3
|
+
# it does NOT return self, but rather the value of block.
|
4
|
+
# if no block is given, it returns nil
|
5
|
+
def bottle # since it's not tap
|
6
|
+
if block_given?
|
7
|
+
yield self
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# access private methods
|
12
|
+
def [](*args)
|
13
|
+
if method(args.first)
|
14
|
+
method(args.first).call *args[1..-1]
|
15
|
+
else
|
16
|
+
raise NoMethodError.new("undefined method" +
|
17
|
+
"`#{args.first}' for #{self}:#{self.class}")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'wirble'
|
2
|
+
|
3
|
+
class String
|
4
|
+
def -@
|
5
|
+
"-#{self}"
|
6
|
+
end
|
7
|
+
|
8
|
+
def nothing ; Wirble::Colorize.colorize_string self, :nothing ; end
|
9
|
+
def black ; Wirble::Colorize.colorize_string self, :black ; end
|
10
|
+
def red ; Wirble::Colorize.colorize_string self, :red ; end
|
11
|
+
def green ; Wirble::Colorize.colorize_string self, :green ; end
|
12
|
+
def brown ; Wirble::Colorize.colorize_string self, :brown ; end
|
13
|
+
def blue ; Wirble::Colorize.colorize_string self, :blue ; end
|
14
|
+
def cyan ; Wirble::Colorize.colorize_string self, :cyan ; end
|
15
|
+
def purple ; Wirble::Colorize.colorize_string self, :purple ; end
|
16
|
+
def light_gray ; Wirble::Colorize.colorize_string self, :light_gray ; end
|
17
|
+
def dark_gray ; Wirble::Colorize.colorize_string self, :dark_gray ; end
|
18
|
+
def light_red ; Wirble::Colorize.colorize_string self, :light_red ; end
|
19
|
+
def light_green ; Wirble::Colorize.colorize_string self, :light_green ; end
|
20
|
+
def yellow ; Wirble::Colorize.colorize_string self, :yellow ; end
|
21
|
+
def light_blue ; Wirble::Colorize.colorize_string self, :light_blue ; end
|
22
|
+
def light_cyan ; Wirble::Colorize.colorize_string self, :light_cyan ; end
|
23
|
+
def light_purple; Wirble::Colorize.colorize_string self, :light_purple; end
|
24
|
+
def white ; Wirble::Colorize.colorize_string self, :white ; end
|
25
|
+
|
26
|
+
def map_lines(&block)
|
27
|
+
split("\n").map(&block)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Make an executable out of File.join(self, other)
|
31
|
+
def /(other)
|
32
|
+
Chitin::Executable.new File.join(self, other.to_s)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
data/lib/chitin/file.rb
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
module Chitin
|
2
|
+
class FSObject
|
3
|
+
attr_accessor :path
|
4
|
+
|
5
|
+
# The choice to not have File.expand_path here is explicitly done.
|
6
|
+
# Relativity is up to the user.
|
7
|
+
# Also it got really annoying seeing the same prefix over and over
|
8
|
+
# and over again on lists of files.
|
9
|
+
def initialize(path, opts={})
|
10
|
+
@path = path
|
11
|
+
@unknown = opts[:unknown]
|
12
|
+
end
|
13
|
+
|
14
|
+
def unknown_type?
|
15
|
+
@unknown
|
16
|
+
end
|
17
|
+
|
18
|
+
def lstat
|
19
|
+
File.lstat @path
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
path
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class FileObject < FSObject
|
28
|
+
def readable?(force=false)
|
29
|
+
@readable = force ? File.readable(path) : @readable
|
30
|
+
end
|
31
|
+
|
32
|
+
def writeable?(force=false)
|
33
|
+
@writeable = force ? File.writeable?(path) : @writeable
|
34
|
+
end
|
35
|
+
|
36
|
+
def exists?
|
37
|
+
File.exists? path
|
38
|
+
end
|
39
|
+
|
40
|
+
def open(permissions, &block)
|
41
|
+
File.open path, permissions do |f|
|
42
|
+
block.call f
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def delete
|
47
|
+
File.safe_unlink @path
|
48
|
+
end
|
49
|
+
|
50
|
+
def executable?(force=false)
|
51
|
+
@executable = force ? File.executable?(path) : @executable
|
52
|
+
end
|
53
|
+
|
54
|
+
def inspect
|
55
|
+
"#<Chitin::FileObject #{path.inspect}>"
|
56
|
+
end
|
57
|
+
|
58
|
+
def symlink?
|
59
|
+
false
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class Symlink < FileObject
|
64
|
+
def symlink?
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
68
|
+
def inspect
|
69
|
+
"#<Chitin::Symlink #{path}>"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class Directory < FSObject
|
74
|
+
include Enumerable
|
75
|
+
|
76
|
+
def files
|
77
|
+
return @files if @files
|
78
|
+
files = Dir[File.join(path, '*')]
|
79
|
+
@files = files.inject({}) do |h, f|
|
80
|
+
f = f[2..-1] if f.start_with? './'
|
81
|
+
h[f] = if File.directory? f
|
82
|
+
D f
|
83
|
+
elsif File.file? f
|
84
|
+
F f
|
85
|
+
elsif File.symlink? f
|
86
|
+
S f
|
87
|
+
else
|
88
|
+
FSObject.new f, :unknown => true
|
89
|
+
end
|
90
|
+
|
91
|
+
h
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def down
|
96
|
+
self.path = File.join path, '**'
|
97
|
+
self
|
98
|
+
end
|
99
|
+
|
100
|
+
# Get a specific level down
|
101
|
+
def level(n=1)
|
102
|
+
n.times { self.path = File.join path, '*' }
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
def each(&block)
|
107
|
+
files.values.each &block
|
108
|
+
end
|
109
|
+
|
110
|
+
def to_a
|
111
|
+
files.values
|
112
|
+
end
|
113
|
+
|
114
|
+
def delete
|
115
|
+
# this is probably unwise...
|
116
|
+
File.rm_rf @path
|
117
|
+
end
|
118
|
+
|
119
|
+
def inspect
|
120
|
+
"#<Chitin::Directory #{path.inspect}>"
|
121
|
+
end
|
122
|
+
|
123
|
+
def symlink?
|
124
|
+
false
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Consumes all input.
|
129
|
+
# Gives no output.
|
130
|
+
# Takes no prisoners.
|
131
|
+
# Used for NULLIN, NULLOUT, and NULLERR.
|
132
|
+
class Abyss
|
133
|
+
def eof?
|
134
|
+
true
|
135
|
+
end
|
136
|
+
|
137
|
+
def read
|
138
|
+
""
|
139
|
+
end
|
140
|
+
alias_method :readline, :read
|
141
|
+
|
142
|
+
def <<(*args)
|
143
|
+
end
|
144
|
+
alias_method :write, :<<
|
145
|
+
alias_method :puts, :<<
|
146
|
+
|
147
|
+
def >(executable)
|
148
|
+
executable[:in=, self]
|
149
|
+
executable
|
150
|
+
end
|
151
|
+
|
152
|
+
def close
|
153
|
+
true
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def D(path); Chitin::Directory.new path; end
|
159
|
+
def F(path); Chitin::FileObject.new path; end
|
160
|
+
def S(path); Chitin::Symlink.new path; end
|
161
|
+
|
162
|
+
NULLIN = NULLOUT = NULLERR = Chitin::Abyss.new
|
163
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Chitin
|
2
|
+
class Sandbox
|
3
|
+
def initialize
|
4
|
+
@binding = binding
|
5
|
+
@previous = nil
|
6
|
+
end
|
7
|
+
|
8
|
+
def inspect
|
9
|
+
"(sandbox)"
|
10
|
+
end
|
11
|
+
|
12
|
+
def evaluate(code)
|
13
|
+
@previous = eval code, @binding
|
14
|
+
eval "_ = @previous", @binding
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'coderay'
|
2
|
+
|
3
|
+
module Chitin
|
4
|
+
class Session
|
5
|
+
attr_accessor :sandbox
|
6
|
+
attr_accessor :out
|
7
|
+
|
8
|
+
attr_reader :config
|
9
|
+
attr_reader :editor
|
10
|
+
|
11
|
+
def initialize(config=nil)
|
12
|
+
@config = config
|
13
|
+
@out = STDOUT
|
14
|
+
@sandbox = Sandbox.new # give it its own private space to work
|
15
|
+
(class << @sandbox; self; end).send :include, @config # include the config and builtins
|
16
|
+
|
17
|
+
@editor = Coolline.new do |c|
|
18
|
+
# Remove the default of '-' and add support for strings
|
19
|
+
# starting after parentheses.
|
20
|
+
c.word_boundaries = [' ', "\t", "(", ")"]
|
21
|
+
c.history_file = File.join ENV['HOME'], '.chitin_history'
|
22
|
+
|
23
|
+
# Make sure we don't kill the shell accidentally when we're trying to
|
24
|
+
# kill a file. That's what the default does, so we're overriding that
|
25
|
+
# here.
|
26
|
+
c.bind(?\C-c) {}
|
27
|
+
|
28
|
+
c.transform_proc = proc do
|
29
|
+
CodeRay.scan(c.line, :ruby).term
|
30
|
+
end
|
31
|
+
|
32
|
+
c.completion_proc = Proc.new do
|
33
|
+
line = c.completed_word
|
34
|
+
|
35
|
+
# expand ~ to homedir
|
36
|
+
if line.start_with? '~'
|
37
|
+
line = ENV['HOME'] + line[1..-1]
|
38
|
+
end
|
39
|
+
|
40
|
+
# if there's a quote, remove it. we'll add it back in later, but it ruins
|
41
|
+
# searching so we need it removed for now.
|
42
|
+
unquoted_line = ['"', '\''].include?(line[0, 1]) ? line[1..-1] : line
|
43
|
+
|
44
|
+
Dir[unquoted_line + '*'].map do |w|
|
45
|
+
slash_it = File.directory?(w) and line[-1] != '/' and w[-1] != '/'
|
46
|
+
|
47
|
+
"\"#{w}#{slash_it ? '/' : ''}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
if @sandbox.completion_proc
|
53
|
+
@editor.completion_proc = @sandbox.completion_proc
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Read
|
58
|
+
# Evaluate
|
59
|
+
# Print (display)
|
60
|
+
# Loop
|
61
|
+
def start
|
62
|
+
while (val = read)
|
63
|
+
next if val.empty?
|
64
|
+
|
65
|
+
begin
|
66
|
+
res = evaluate val
|
67
|
+
display res unless val.lstrip[0, 1] == '#'
|
68
|
+
rescue StandardError, ScriptError, Interrupt => e
|
69
|
+
@config.post_processing[e.class].call e, val
|
70
|
+
|
71
|
+
print e.backtrace.first, ': '
|
72
|
+
puts "#{e.message} (#{e.class})"
|
73
|
+
e.backtrace[1..-1].each {|l| puts "\t#{l}" }
|
74
|
+
nil
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# THIS METHOD WILL POSSIBLY RETURN NIL!!!!!
|
81
|
+
# So it can return a string or nil. Remember that, folks.
|
82
|
+
def read
|
83
|
+
inp = @editor.readline @sandbox.prompt
|
84
|
+
|
85
|
+
|
86
|
+
inp ? inp.chomp : nil # return nil so that the while loop in #start can end
|
87
|
+
end
|
88
|
+
|
89
|
+
# we need to save the frame or something i think. could use fibers
|
90
|
+
# to make the whole thing a generator so that original frame would be saved.
|
91
|
+
# why did i write those lines. that makes no sense.
|
92
|
+
# AH now i remember. we need to save a frame and use that frame as a sandbox.
|
93
|
+
def evaluate(val)
|
94
|
+
val.strip!
|
95
|
+
@config.pre_processing[:default].each {|p| val = p.call val }
|
96
|
+
|
97
|
+
@sandbox.evaluate val
|
98
|
+
end
|
99
|
+
|
100
|
+
def shell_command?(res)
|
101
|
+
res.is_a?(Executable) || (res.is_a?(Pipe) && res[:returning] != :ruby)
|
102
|
+
end
|
103
|
+
|
104
|
+
def display(res)
|
105
|
+
# The reason that this is here instead of in #evaluate is that
|
106
|
+
# pipes could, in fact, have NO display output, but that isn't for
|
107
|
+
# #evaluate to decide; rather, it is for #display
|
108
|
+
#
|
109
|
+
# Only pipes and executables are standalone. StringMethods need to be
|
110
|
+
# chained, and proc is also a general Ruby type, so we don't want to
|
111
|
+
# automatically call it.
|
112
|
+
if shell_command?(res) || (res.is_a?(Array) && !res.empty? && res.map {|r| shell_command? r }.all?)
|
113
|
+
|
114
|
+
res = [res] unless res.is_a?(Array)
|
115
|
+
|
116
|
+
res.each do |res|
|
117
|
+
# set up the inputs and outputs
|
118
|
+
res[:set_in, STDIN] unless res[:in]
|
119
|
+
res[:set_out, STDOUT] unless res[:out]
|
120
|
+
res[:set_err, STDOUT] unless res[:err]
|
121
|
+
|
122
|
+
res[:run]
|
123
|
+
res[:wait] unless res[:bg]
|
124
|
+
end
|
125
|
+
|
126
|
+
else # else it's a standard ruby type (or a pipe returning as such)
|
127
|
+
|
128
|
+
unless res.is_a? Pipe
|
129
|
+
# RUBY values pass through here
|
130
|
+
txt = @config.post_processing[:color].call res.inspect
|
131
|
+
puts " => #{txt}"
|
132
|
+
else
|
133
|
+
# set up the inputs and outputs
|
134
|
+
res[:set_in, STDIN] unless res[:in]
|
135
|
+
res[:set_out, STDOUT] unless res[:out]
|
136
|
+
res[:set_err, STDOUT] unless res[:err]
|
137
|
+
|
138
|
+
# We have to use #stat here because reopened pipes retain the file
|
139
|
+
# descriptor of the original pipe. Example:
|
140
|
+
# r, w = IO.pipe; r.reopen STDIN; r == STDIN # => false
|
141
|
+
# Thus, we have to use #stat (or, more lamely, #inspect).
|
142
|
+
if res[:out].stat == STDOUT.stat
|
143
|
+
val = res[:raw_run]
|
144
|
+
txt = @config.post_processing[:color].call val.inspect
|
145
|
+
puts " => #{txt}"
|
146
|
+
else
|
147
|
+
res[:run]
|
148
|
+
res[:wait] unless res[:bg]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
# Run all the post_processing stuff
|
155
|
+
# Not sure where I should really put this or what arguments it should have
|
156
|
+
@config.post_processing[:default].each {|b| b.call }
|
157
|
+
end
|
158
|
+
|
159
|
+
def puts(*args)
|
160
|
+
@out.puts *args
|
161
|
+
end
|
162
|
+
|
163
|
+
def print(*args)
|
164
|
+
@out.print *args
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
def proper_string(val)
|
2
|
+
# if there is an unclosed string, close it and run it again.
|
3
|
+
# smart compilers are bad... but this ain't a compiler
|
4
|
+
if (e = syntax_error_for(val)) &&
|
5
|
+
e.message =~ /unterminated string meets end of file/
|
6
|
+
|
7
|
+
if syntax_error_for(val + '\'')
|
8
|
+
unless syntax_error_for(val + '"')
|
9
|
+
val << '"'
|
10
|
+
end
|
11
|
+
else
|
12
|
+
val << '\''
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
val
|
18
|
+
end
|
19
|
+
|
20
|
+
# Is this an incomplete string?
|
21
|
+
# If so, return the string character that it uses.
|
22
|
+
# Else return false.
|
23
|
+
def incomplete_string(string)
|
24
|
+
return '"' unless syntax_error_for(string + '"')
|
25
|
+
return "'" unless syntax_error_for(string + '\'')
|
26
|
+
return false
|
27
|
+
end
|
28
|
+
|
29
|
+
# If the code is syntactically correct, return nil.
|
30
|
+
# Else, return the error.
|
31
|
+
def syntax_error_for(code)
|
32
|
+
eval "return nil\n#{code}"
|
33
|
+
rescue SyntaxError => e
|
34
|
+
e
|
35
|
+
end
|
36
|
+
|
37
|
+
def shellsplit_keep_quotes(line)
|
38
|
+
words = []
|
39
|
+
field = ''
|
40
|
+
line.scan(/\G\s*(?>([^\s\\\'\"]+)|('[^\']*')|("(?:[^\"\\]|\\.)*")|(\\.?)|(\S))(\s|\z)?/m) do
|
41
|
+
|word, sq, dq, esc, garbage, sep|
|
42
|
+
raise ArgumentError, "Unmatched double quote: #{line.inspect}" if garbage
|
43
|
+
field << (word || sq || (dq || esc).gsub(/\\(.)/, '\\1'))
|
44
|
+
if sep
|
45
|
+
words << field
|
46
|
+
field = ''
|
47
|
+
end
|
48
|
+
end
|
49
|
+
words
|
50
|
+
end
|
51
|
+
|
File without changes
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
# The hard part is to not confuse interface with implementation. Tough because
|
4
|
+
# this relies almost entirely on the interface.
|
5
|
+
# Possibly reopen STDOUT so that it writes to two, instead of just redefining
|
6
|
+
# puts/print?
|
7
|
+
class Replay
|
8
|
+
attr_reader :path
|
9
|
+
attr_reader :session
|
10
|
+
|
11
|
+
def initialize(path, session)
|
12
|
+
@path = path
|
13
|
+
@session = session
|
14
|
+
end
|
15
|
+
|
16
|
+
def start!
|
17
|
+
session.out = MultiIO.new STDOUT, StringIO.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def end!
|
21
|
+
multi = session.out
|
22
|
+
session.out = STDOUT
|
23
|
+
stringio = multi.ios[1]
|
24
|
+
@text = stringio.read
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
@text
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
module Builtins
|
34
|
+
|
35
|
+
def record(path="replay.#{DateTime.now.to_s}")
|
36
|
+
@__record__ = Replay.new path, SESSION
|
37
|
+
@__record__.start!
|
38
|
+
end
|
39
|
+
|
40
|
+
def stop
|
41
|
+
@__record__.end!
|
42
|
+
@__record__
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
data/lib/chitin.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
def need(file)
|
2
|
+
require(File.dirname(File.expand_path(__FILE__)) + '/' + file)
|
3
|
+
end
|
4
|
+
|
5
|
+
need 'chitin/version'
|
6
|
+
need 'chitin/support'
|
7
|
+
need 'chitin/file'
|
8
|
+
|
9
|
+
Dir[File.dirname(File.expand_path(__FILE__)) + '/chitin/core_ext/*'].each do |f|
|
10
|
+
require f
|
11
|
+
end
|
12
|
+
|
13
|
+
need 'chitin/commands/runnable'
|
14
|
+
Dir[File.dirname(File.expand_path(__FILE__)) + '/chitin/commands/*'].each do |f|
|
15
|
+
require f
|
16
|
+
end
|
17
|
+
|
18
|
+
need 'chitin/tools/string_methods'
|
19
|
+
|
20
|
+
need 'chitin/sandbox'
|
21
|
+
need 'chitin/session'
|
22
|
+
|
23
|
+
require 'date'
|
24
|
+
require 'fileutils'
|
25
|
+
require 'pp'
|
26
|
+
|