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.
@@ -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
+