chitin 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|