aleksi-rush 0.6.6
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +89 -0
- data/Rakefile +59 -0
- data/VERSION +1 -0
- data/bin/rush +13 -0
- data/bin/rushd +7 -0
- data/lib/rush.rb +87 -0
- data/lib/rush/access.rb +130 -0
- data/lib/rush/array_ext.rb +19 -0
- data/lib/rush/box.rb +115 -0
- data/lib/rush/commands.rb +55 -0
- data/lib/rush/config.rb +154 -0
- data/lib/rush/dir.rb +160 -0
- data/lib/rush/embeddable_shell.rb +26 -0
- data/lib/rush/entry.rb +189 -0
- data/lib/rush/exceptions.rb +39 -0
- data/lib/rush/file.rb +85 -0
- data/lib/rush/find_by.rb +39 -0
- data/lib/rush/fixnum_ext.rb +18 -0
- data/lib/rush/head_tail.rb +11 -0
- data/lib/rush/local.rb +398 -0
- data/lib/rush/process.rb +59 -0
- data/lib/rush/process_set.rb +62 -0
- data/lib/rush/remote.rb +158 -0
- data/lib/rush/search_results.rb +58 -0
- data/lib/rush/server.rb +117 -0
- data/lib/rush/shell.rb +187 -0
- data/lib/rush/ssh_tunnel.rb +122 -0
- data/lib/rush/string_ext.rb +3 -0
- data/spec/access_spec.rb +134 -0
- data/spec/array_ext_spec.rb +15 -0
- data/spec/base.rb +24 -0
- data/spec/box_spec.rb +80 -0
- data/spec/commands_spec.rb +47 -0
- data/spec/config_spec.rb +108 -0
- data/spec/dir_spec.rb +164 -0
- data/spec/embeddable_shell_spec.rb +17 -0
- data/spec/entry_spec.rb +133 -0
- data/spec/file_spec.rb +83 -0
- data/spec/find_by_spec.rb +58 -0
- data/spec/fixnum_ext_spec.rb +19 -0
- data/spec/local_spec.rb +364 -0
- data/spec/process_set_spec.rb +50 -0
- data/spec/process_spec.rb +73 -0
- data/spec/remote_spec.rb +140 -0
- data/spec/rush_spec.rb +28 -0
- data/spec/search_results_spec.rb +44 -0
- data/spec/shell_spec.rb +23 -0
- data/spec/ssh_tunnel_spec.rb +122 -0
- data/spec/string_ext_spec.rb +23 -0
- metadata +142 -0
data/lib/rush/shell.rb
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'readline'
|
2
|
+
|
3
|
+
module Rush
|
4
|
+
# Rush::Shell is used to create an interactive shell. It is invoked by the rush binary.
|
5
|
+
class Shell
|
6
|
+
attr_accessor :suppress_output
|
7
|
+
# Set up the user's environment, including a pure binding into which
|
8
|
+
# env.rb and commands.rb are mixed.
|
9
|
+
def initialize
|
10
|
+
root = Rush::Dir.new('/')
|
11
|
+
home = Rush::Dir.new(ENV['HOME']) if ENV['HOME']
|
12
|
+
pwd = Rush::Dir.new(ENV['PWD']) if ENV['PWD']
|
13
|
+
|
14
|
+
@config = Rush::Config.new
|
15
|
+
|
16
|
+
@config.load_history.each do |item|
|
17
|
+
Readline::HISTORY.push(item)
|
18
|
+
end
|
19
|
+
|
20
|
+
Readline.basic_word_break_characters = ""
|
21
|
+
Readline.completion_append_character = nil
|
22
|
+
Readline.completion_proc = completion_proc
|
23
|
+
|
24
|
+
@box = Rush::Box.new
|
25
|
+
@pure_binding = @box.instance_eval "binding"
|
26
|
+
$last_res = nil
|
27
|
+
|
28
|
+
eval @config.load_env, @pure_binding
|
29
|
+
|
30
|
+
commands = @config.load_commands
|
31
|
+
Rush::Dir.class_eval commands
|
32
|
+
Array.class_eval commands
|
33
|
+
end
|
34
|
+
|
35
|
+
# Run a single command.
|
36
|
+
def execute(cmd)
|
37
|
+
res = eval(cmd, @pure_binding)
|
38
|
+
$last_res = res
|
39
|
+
eval("_ = $last_res", @pure_binding)
|
40
|
+
print_result res
|
41
|
+
rescue Rush::Exception => e
|
42
|
+
puts "Exception #{e.class} -> #{e.message}"
|
43
|
+
rescue ::Exception => e
|
44
|
+
puts "Exception #{e.class} -> #{e.message}"
|
45
|
+
e.backtrace.each do |t|
|
46
|
+
puts " #{::File.expand_path(t)}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Run the interactive shell using readline.
|
51
|
+
def run
|
52
|
+
loop do
|
53
|
+
cmd = Readline.readline('rush> ')
|
54
|
+
|
55
|
+
finish if cmd.nil? or cmd == 'exit'
|
56
|
+
next if cmd == ""
|
57
|
+
Readline::HISTORY.push(cmd)
|
58
|
+
|
59
|
+
execute(cmd)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Save history to ~/.rush/history when the shell exists.
|
64
|
+
def finish
|
65
|
+
@config.save_history(Readline::HISTORY.to_a)
|
66
|
+
puts
|
67
|
+
exit
|
68
|
+
end
|
69
|
+
|
70
|
+
# Nice printing of different return types, particularly Rush::SearchResults.
|
71
|
+
def print_result(res)
|
72
|
+
return if self.suppress_output
|
73
|
+
if res.kind_of? String
|
74
|
+
puts res
|
75
|
+
elsif res.kind_of? Rush::SearchResults
|
76
|
+
widest = res.entries.map { |k| k.full_path.length }.max
|
77
|
+
res.entries_with_lines.each do |entry, lines|
|
78
|
+
print entry.full_path
|
79
|
+
print ' ' * (widest - entry.full_path.length + 2)
|
80
|
+
print "=> "
|
81
|
+
print res.colorize(lines.first.strip.head(30))
|
82
|
+
print "..." if lines.first.strip.length > 30
|
83
|
+
if lines.size > 1
|
84
|
+
print " (plus #{lines.size - 1} more matches)"
|
85
|
+
end
|
86
|
+
print "\n"
|
87
|
+
end
|
88
|
+
puts "#{res.entries.size} matching files with #{res.lines.size} matching lines"
|
89
|
+
elsif res.respond_to? :each
|
90
|
+
counts = {}
|
91
|
+
res.each do |item|
|
92
|
+
puts item
|
93
|
+
counts[item.class] ||= 0
|
94
|
+
counts[item.class] += 1
|
95
|
+
end
|
96
|
+
if counts == {}
|
97
|
+
puts "=> (empty set)"
|
98
|
+
else
|
99
|
+
count_s = counts.map do |klass, count|
|
100
|
+
"#{count} x #{klass}"
|
101
|
+
end.join(', ')
|
102
|
+
puts "=> #{count_s}"
|
103
|
+
end
|
104
|
+
else
|
105
|
+
puts "=> #{res.inspect}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def path_parts(input) # :nodoc:
|
110
|
+
case input
|
111
|
+
when /((?:@{1,2}|\$|)\w+(?:\[[^\]]+\])*)([\[\/])(['"])([^\3]*)$/
|
112
|
+
$~.to_a.slice(1, 4).push($~.pre_match)
|
113
|
+
when /((?:@{1,2}|\$|)\w+(?:\[[^\]]+\])*)(\.)(\w*)$/
|
114
|
+
$~.to_a.slice(1, 3).push($~.pre_match)
|
115
|
+
when /((?:@{1,2}|\$|)\w+)$/
|
116
|
+
$~.to_a.slice(1, 1).push(nil).push($~.pre_match)
|
117
|
+
else
|
118
|
+
[ nil, nil, nil ]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def complete_method(receiver, dot, partial_name, pre)
|
123
|
+
path = eval("#{receiver}.full_path", @pure_binding) rescue nil
|
124
|
+
box = eval("#{receiver}.box", @pure_binding) rescue nil
|
125
|
+
if path and box
|
126
|
+
(box[path].methods - Object.methods).select do |e|
|
127
|
+
e.match(/^#{Regexp.escape(partial_name)}/)
|
128
|
+
end.map do |e|
|
129
|
+
(pre || '') + receiver + dot + e
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def complete_path(possible_var, accessor, quote, partial_path, pre) # :nodoc:
|
135
|
+
original_var, fixed_path = possible_var, ''
|
136
|
+
if /^(.+\/)([^\/]*)$/ === partial_path
|
137
|
+
fixed_path, partial_path = $~.captures
|
138
|
+
possible_var += "['#{fixed_path}']"
|
139
|
+
end
|
140
|
+
full_path = eval("#{possible_var}.full_path", @pure_binding) rescue nil
|
141
|
+
box = eval("#{possible_var}.box", @pure_binding) rescue nil
|
142
|
+
if full_path and box
|
143
|
+
Rush::Dir.new(full_path, box).entries.select do |e|
|
144
|
+
e.name.match(/^#{Regexp.escape(partial_path)}/)
|
145
|
+
end.map do |e|
|
146
|
+
(pre || '') + original_var + accessor + quote + fixed_path + e.name + (e.dir? ? "/" : "")
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def complete_variable(partial_name, pre)
|
152
|
+
lvars = eval('local_variables', @pure_binding)
|
153
|
+
gvars = eval('global_variables', @pure_binding)
|
154
|
+
ivars = eval('instance_variables', @pure_binding)
|
155
|
+
(lvars + gvars + ivars).select do |e|
|
156
|
+
e.match(/^#{Regexp.escape(partial_name)}/)
|
157
|
+
end.map do |e|
|
158
|
+
(pre || '') + e
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Try to do tab completion on dir square brackets and slash accessors.
|
163
|
+
#
|
164
|
+
# Example:
|
165
|
+
#
|
166
|
+
# dir['subd # presing tab here will produce dir['subdir/ if subdir exists
|
167
|
+
# dir/'subd # presing tab here will produce dir/'subdir/ if subdir exists
|
168
|
+
#
|
169
|
+
# This isn't that cool yet, because it can't do multiple levels of subdirs.
|
170
|
+
# It does work remotely, though, which is pretty sweet.
|
171
|
+
def completion_proc
|
172
|
+
proc do |input|
|
173
|
+
receiver, accessor, *rest = path_parts(input)
|
174
|
+
if receiver
|
175
|
+
case accessor
|
176
|
+
when /^[\[\/]$/
|
177
|
+
complete_path(receiver, accessor, *rest)
|
178
|
+
when /^\.$/
|
179
|
+
complete_method(receiver, accessor, *rest)
|
180
|
+
when nil
|
181
|
+
complete_variable(receiver, *rest)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# Internal class for managing an ssh tunnel, across which relatively insecure
|
2
|
+
# HTTP commands can be sent by Rush::Connection::Remote.
|
3
|
+
class Rush::SshTunnel
|
4
|
+
def initialize(real_host)
|
5
|
+
@real_host = real_host
|
6
|
+
end
|
7
|
+
|
8
|
+
def host
|
9
|
+
'localhost'
|
10
|
+
end
|
11
|
+
|
12
|
+
def port
|
13
|
+
@port
|
14
|
+
end
|
15
|
+
|
16
|
+
def ensure_tunnel(options={})
|
17
|
+
return if @port and tunnel_alive?
|
18
|
+
|
19
|
+
@port = config.tunnels[@real_host]
|
20
|
+
|
21
|
+
if !@port or !tunnel_alive?
|
22
|
+
setup_everything(options)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def setup_everything(options={})
|
27
|
+
display "Connecting to #{@real_host}..."
|
28
|
+
push_credentials
|
29
|
+
launch_rushd
|
30
|
+
establish_tunnel(options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def push_credentials
|
34
|
+
display "Pushing credentials"
|
35
|
+
config.ensure_credentials_exist
|
36
|
+
ssh_append_to_credentials(config.credentials_file.contents.strip)
|
37
|
+
end
|
38
|
+
|
39
|
+
def ssh_append_to_credentials(string)
|
40
|
+
# the following horror is exactly why rush is needed
|
41
|
+
passwords_file = "~/.rush/passwords"
|
42
|
+
string = "'#{string}'"
|
43
|
+
ssh "M=`grep #{string} #{passwords_file} 2>/dev/null | wc -l`; if [ $M = 0 ]; then mkdir -p .rush; chmod 700 .rush; echo #{string} >> #{passwords_file}; chmod 600 #{passwords_file}; fi"
|
44
|
+
end
|
45
|
+
|
46
|
+
def launch_rushd
|
47
|
+
display "Launching rushd"
|
48
|
+
ssh("if [ `ps aux | grep rushd | grep -v grep | wc -l` -ge 1 ]; then exit; fi; rushd > /dev/null 2>&1 &")
|
49
|
+
end
|
50
|
+
|
51
|
+
def establish_tunnel(options={})
|
52
|
+
display "Establishing ssh tunnel"
|
53
|
+
@port = next_available_port
|
54
|
+
|
55
|
+
make_ssh_tunnel(options)
|
56
|
+
|
57
|
+
tunnels = config.tunnels
|
58
|
+
tunnels[@real_host] = @port
|
59
|
+
config.save_tunnels tunnels
|
60
|
+
|
61
|
+
sleep 0.5
|
62
|
+
end
|
63
|
+
|
64
|
+
def tunnel_options
|
65
|
+
{
|
66
|
+
:local_port => @port,
|
67
|
+
:remote_port => Rush::Config::DefaultPort,
|
68
|
+
:ssh_host => @real_host,
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
def tunnel_alive?
|
73
|
+
`#{tunnel_count_command}`.to_i > 0
|
74
|
+
end
|
75
|
+
|
76
|
+
def tunnel_count_command
|
77
|
+
"ps x | grep '#{ssh_tunnel_command_without_stall}' | grep -v grep | wc -l"
|
78
|
+
end
|
79
|
+
|
80
|
+
class SshFailed < Exception; end
|
81
|
+
class NoPortSelectedYet < Exception; end
|
82
|
+
|
83
|
+
def ssh(command)
|
84
|
+
raise SshFailed unless system("ssh #{@real_host} '#{command}'")
|
85
|
+
end
|
86
|
+
|
87
|
+
def make_ssh_tunnel(options={})
|
88
|
+
raise SshFailed unless system(ssh_tunnel_command(options))
|
89
|
+
end
|
90
|
+
|
91
|
+
def ssh_tunnel_command_without_stall
|
92
|
+
options = tunnel_options
|
93
|
+
raise NoPortSelectedYet unless options[:local_port]
|
94
|
+
"ssh -f -L #{options[:local_port]}:127.0.0.1:#{options[:remote_port]} #{options[:ssh_host]}"
|
95
|
+
end
|
96
|
+
|
97
|
+
def ssh_stall_command(options={})
|
98
|
+
if options[:timeout] == :infinite
|
99
|
+
"while [ 1 ]; do sleep 1000; done"
|
100
|
+
elsif options[:timeout].to_i > 10
|
101
|
+
"sleep #{options[:timeout].to_i}"
|
102
|
+
else
|
103
|
+
"sleep 9000"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def ssh_tunnel_command(options={})
|
108
|
+
ssh_tunnel_command_without_stall + ' "' + ssh_stall_command(options) + '"'
|
109
|
+
end
|
110
|
+
|
111
|
+
def next_available_port
|
112
|
+
(config.tunnels.values.max || Rush::Config::DefaultPort) + 1
|
113
|
+
end
|
114
|
+
|
115
|
+
def config
|
116
|
+
@config ||= Rush::Config.new
|
117
|
+
end
|
118
|
+
|
119
|
+
def display(msg)
|
120
|
+
puts msg
|
121
|
+
end
|
122
|
+
end
|
data/spec/access_spec.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base'
|
2
|
+
|
3
|
+
describe Rush::Access do
|
4
|
+
before do
|
5
|
+
@access = Rush::Access.new
|
6
|
+
end
|
7
|
+
|
8
|
+
it "has roles: user, group, other" do
|
9
|
+
@access.class.roles == %w(user group other)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "has permissions: read, write, execute" do
|
13
|
+
@access.class.permissions == %w(read write execute)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "gets parts from a one-part symbol like :user" do
|
17
|
+
@access.parts_from(:user).should == %w(user)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "gets parts from a two-part symbol like :read_write" do
|
21
|
+
@access.parts_from(:read_write).should == %w(read write)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "allows use of 'and' in multipart symbols, like :user_and_group" do
|
25
|
+
@access.parts_from(:user_and_group).should == %w(user group)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "extract_list verifies that all the parts among the valid choices" do
|
29
|
+
@access.should_receive(:parts_from).with(:red_green).and_return(%w(red green))
|
30
|
+
@access.extract_list('type', :red_green, %w(red blue green)).should == %w(red green)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "extract_list raises a BadAccessSpecifier when there is part not in the list of choices" do
|
34
|
+
lambda do
|
35
|
+
@access.extract_list('role', :user_bork, %w(user group))
|
36
|
+
end.should raise_error(Rush::BadAccessSpecifier, "Unrecognized role: bork")
|
37
|
+
end
|
38
|
+
|
39
|
+
it "sets one value in the matrix of permissions and roles" do
|
40
|
+
@access.set_matrix(%w(read), %w(user))
|
41
|
+
@access.user_can_read.should == true
|
42
|
+
end
|
43
|
+
|
44
|
+
it "sets two values in the matrix of permissions and roles" do
|
45
|
+
@access.set_matrix(%w(read), %w(user group))
|
46
|
+
@access.user_can_read.should == true
|
47
|
+
@access.group_can_read.should == true
|
48
|
+
end
|
49
|
+
|
50
|
+
it "sets four values in the matrix of permissions and roles" do
|
51
|
+
@access.set_matrix(%w(read write), %w(user group))
|
52
|
+
@access.user_can_read.should == true
|
53
|
+
@access.group_can_read.should == true
|
54
|
+
@access.user_can_write.should == true
|
55
|
+
@access.group_can_write.should == true
|
56
|
+
end
|
57
|
+
|
58
|
+
it "parse options hash" do
|
59
|
+
@access.parse(:user_can => :read)
|
60
|
+
@access.user_can_read.should == true
|
61
|
+
end
|
62
|
+
|
63
|
+
it "generates octal permissions from its member vars" do
|
64
|
+
@access.user_can_read = true
|
65
|
+
@access.octal_permissions.should == 0400
|
66
|
+
end
|
67
|
+
|
68
|
+
it "generates octal permissions from its member vars" do
|
69
|
+
@access.user_can_read = true
|
70
|
+
@access.user_can_write = true
|
71
|
+
@access.user_can_execute = true
|
72
|
+
@access.group_can_read = true
|
73
|
+
@access.group_can_execute = true
|
74
|
+
@access.octal_permissions.should == 0750
|
75
|
+
end
|
76
|
+
|
77
|
+
it "applies its settings to a file" do
|
78
|
+
file = "/tmp/rush_spec_#{Process.pid}"
|
79
|
+
begin
|
80
|
+
system "rm -rf #{file}; touch #{file}; chmod 770 #{file}"
|
81
|
+
@access.user_can_read = true
|
82
|
+
@access.apply(file)
|
83
|
+
`ls -l #{file}`.should match(/^-r--------/)
|
84
|
+
ensure
|
85
|
+
system "rm -rf #{file}; touch #{file}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
it "serializes itself to a hash" do
|
90
|
+
@access.user_can_read = true
|
91
|
+
@access.to_hash.should == {
|
92
|
+
:user_can_read => 1, :user_can_write => 0, :user_can_execute => 0,
|
93
|
+
:group_can_read => 0, :group_can_write => 0, :group_can_execute => 0,
|
94
|
+
:other_can_read => 0, :other_can_write => 0, :other_can_execute => 0,
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
it "unserializes from a hash" do
|
99
|
+
@access.from_hash(:user_can_read => '1')
|
100
|
+
@access.user_can_read.should == true
|
101
|
+
end
|
102
|
+
|
103
|
+
it "initializes from a serialized hash" do
|
104
|
+
@access.class.should_receive(:new).and_return(@access)
|
105
|
+
@access.class.from_hash(:user_can_read => '1').should == @access
|
106
|
+
@access.user_can_read.should == true
|
107
|
+
end
|
108
|
+
|
109
|
+
it "initializes from a parsed options hash" do
|
110
|
+
@access.class.should_receive(:new).and_return(@access)
|
111
|
+
@access.class.parse(:user_and_group_can => :read).should == @access
|
112
|
+
@access.user_can_read.should == true
|
113
|
+
end
|
114
|
+
|
115
|
+
it "converts and octal integer into an array of integers" do
|
116
|
+
@access.octal_integer_array(0740).should == [ 7, 4, 0 ]
|
117
|
+
end
|
118
|
+
|
119
|
+
it "filters out anything above the top three digits (File.stat returns some extra data there)" do
|
120
|
+
@access.octal_integer_array(0100644).should == [ 6, 4, 4 ]
|
121
|
+
end
|
122
|
+
|
123
|
+
it "taskes permissions from an octal representation" do
|
124
|
+
@access.from_octal(0644)
|
125
|
+
@access.user_can_read.should == true
|
126
|
+
@access.user_can_write.should == true
|
127
|
+
@access.user_can_execute.should == false
|
128
|
+
end
|
129
|
+
|
130
|
+
it "computes a display hash by dropping false keys and converting the 1s to trues" do
|
131
|
+
@access.should_receive(:to_hash).and_return(:red => 1, :green => 0, :blue => 1)
|
132
|
+
@access.display_hash.should == { :red => true, :blue => true }
|
133
|
+
end
|
134
|
+
end
|