chitin 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,7 @@
1
+ class IO
2
+ def >(executable)
3
+ executable[:set_in, self]
4
+ executable
5
+ end
6
+ end
7
+
@@ -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
+
@@ -0,0 +1,6 @@
1
+ class Symbol
2
+ def -@
3
+ "-#{self}"
4
+ end
5
+ end
6
+
@@ -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
+
@@ -0,0 +1,10 @@
1
+ def string_method(name)
2
+ Chitin::Builtins::PRIORITY_METHODS << name.to_sym
3
+ end
4
+
5
+ string_method :gsub
6
+ string_method :split
7
+ string_method :size
8
+ string_method :pack
9
+ string_method :unpack
10
+
@@ -0,0 +1,4 @@
1
+ module Chitin
2
+ VERSION = '1.0.1'
3
+ end
4
+
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
+