rush2 0.7.0
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.
- checksums.yaml +7 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +86 -0
- data/README.rdoc +125 -0
- data/Rakefile +57 -0
- data/VERSION +1 -0
- data/bin/rush +13 -0
- data/bin/rushd +7 -0
- data/lib/rush.rb +91 -0
- data/lib/rush/access.rb +121 -0
- data/lib/rush/array_ext.rb +19 -0
- data/lib/rush/box.rb +124 -0
- data/lib/rush/commands.rb +76 -0
- data/lib/rush/config.rb +147 -0
- data/lib/rush/dir.rb +166 -0
- data/lib/rush/embeddable_shell.rb +26 -0
- data/lib/rush/entry.rb +229 -0
- data/lib/rush/exceptions.rb +32 -0
- data/lib/rush/file.rb +94 -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 +377 -0
- data/lib/rush/process.rb +59 -0
- data/lib/rush/process_set.rb +62 -0
- data/lib/rush/remote.rb +33 -0
- data/lib/rush/search_results.rb +71 -0
- data/lib/rush/shell.rb +111 -0
- data/lib/rush/shell/completion.rb +110 -0
- data/lib/rush/string_ext.rb +16 -0
- data/spec/access_spec.rb +134 -0
- data/spec/array_ext_spec.rb +15 -0
- data/spec/base.rb +22 -0
- data/spec/box_spec.rb +76 -0
- data/spec/commands_spec.rb +47 -0
- data/spec/config_spec.rb +108 -0
- data/spec/dir_spec.rb +163 -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 +365 -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 +35 -0
- data/spec/ssh_tunnel_spec.rb +122 -0
- data/spec/string_ext_spec.rb +23 -0
- metadata +209 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative 'shell'
|
2
|
+
|
3
|
+
module Rush
|
4
|
+
# This is a class that can be embedded in other applications
|
5
|
+
# rake tasks, utility scripts, etc
|
6
|
+
#
|
7
|
+
# Delegates unknown method calls to a Rush::Shell instance
|
8
|
+
class EmbeddableShell
|
9
|
+
attr_accessor :shell
|
10
|
+
def initialize(suppress_output = true)
|
11
|
+
self.shell = Rush::Shell.new
|
12
|
+
shell.suppress_output = suppress_output
|
13
|
+
end
|
14
|
+
|
15
|
+
# evalutes and unkown method call agains the rush shell
|
16
|
+
def method_missing(sym, *args, &block)
|
17
|
+
shell.execute sym.to_s
|
18
|
+
$last_res
|
19
|
+
end
|
20
|
+
|
21
|
+
# take a whole block and execute it as if it were inside a shell
|
22
|
+
def execute_in_shell(&block)
|
23
|
+
self.instance_eval(&block)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/rush/entry.rb
ADDED
@@ -0,0 +1,229 @@
|
|
1
|
+
# Rush::Entry is the base class for Rush::File and Rush::Dir. One or more of
|
2
|
+
# these is instantiated whenever you use square brackets to access the
|
3
|
+
# filesystem on a box, as well as any other operation that returns an entry or
|
4
|
+
# list of entries.
|
5
|
+
class Rush::Entry
|
6
|
+
attr_reader :box, :name, :path
|
7
|
+
|
8
|
+
# Initialize with full path to the file or dir, and the box it resides on.
|
9
|
+
def initialize(full_path, box=nil)
|
10
|
+
full_path = ::File.expand_path(full_path, '/')
|
11
|
+
@path = ::File.dirname(full_path)
|
12
|
+
@name = ::File.basename(full_path)
|
13
|
+
@box = box || Rush::Box.new('localhost')
|
14
|
+
end
|
15
|
+
|
16
|
+
def method_missing(meth, *args, &block)
|
17
|
+
if executables.include? meth.to_s
|
18
|
+
open_with meth, *args
|
19
|
+
else
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def executables
|
25
|
+
ENV['PATH'].split(':')
|
26
|
+
.map { |x| Rush::Dir.new(x).entries.map(&:name) }
|
27
|
+
.flatten
|
28
|
+
end
|
29
|
+
|
30
|
+
# The factory checks to see if the full path has a trailing slash for
|
31
|
+
# creating a Rush::Dir rather than the default Rush::File.
|
32
|
+
def self.factory(full_path, box=nil)
|
33
|
+
if full_path.tail(1) == '/'
|
34
|
+
Rush::Dir.new(full_path, box)
|
35
|
+
elsif File.directory?(full_path)
|
36
|
+
Rush::Dir.new(full_path, box)
|
37
|
+
else
|
38
|
+
Rush::File.new(full_path, box)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_s # :nodoc:
|
43
|
+
if box.host == 'localhost'
|
44
|
+
"#{full_path}"
|
45
|
+
else
|
46
|
+
inspect
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def inspect # :nodoc:
|
51
|
+
"#{box}:#{full_path}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def connection
|
55
|
+
box ? box.connection : Rush::Connection::Local.new
|
56
|
+
end
|
57
|
+
|
58
|
+
# The parent dir. For example, box['/etc/hosts'].parent == box['etc/']
|
59
|
+
def parent
|
60
|
+
@parent ||= Rush::Dir.new(@path)
|
61
|
+
end
|
62
|
+
|
63
|
+
def full_path
|
64
|
+
"#{@path}/#{@name}"
|
65
|
+
end
|
66
|
+
|
67
|
+
def quoted_path
|
68
|
+
Rush.quote(full_path)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Return true if the entry currently exists on the filesystem of the box.
|
72
|
+
def exists?
|
73
|
+
stat
|
74
|
+
true
|
75
|
+
rescue Rush::DoesNotExist
|
76
|
+
false
|
77
|
+
end
|
78
|
+
|
79
|
+
# Timestamp of most recent change to the entry (permissions, contents, etc).
|
80
|
+
def changed_at
|
81
|
+
stat[:ctime]
|
82
|
+
end
|
83
|
+
|
84
|
+
# Timestamp of last modification of the contents.
|
85
|
+
def last_modified
|
86
|
+
stat[:mtime]
|
87
|
+
end
|
88
|
+
|
89
|
+
# Timestamp that entry was last accessed (read from or written to).
|
90
|
+
def last_accessed
|
91
|
+
stat[:atime]
|
92
|
+
end
|
93
|
+
|
94
|
+
# Rename an entry to another name within the same dir. The object's name
|
95
|
+
# will be updated to match the change on the filesystem.
|
96
|
+
def rename(new_name)
|
97
|
+
connection.rename(@path, @name, new_name)
|
98
|
+
@name = new_name
|
99
|
+
self
|
100
|
+
end
|
101
|
+
alias_method :mv, :rename
|
102
|
+
|
103
|
+
# Rename an entry to another name within the same dir. The existing object
|
104
|
+
# will not be affected, but a new object representing the newly-created
|
105
|
+
# entry will be returned.
|
106
|
+
def duplicate(new_name)
|
107
|
+
raise Rush::NameCannotContainSlash if new_name.match(/\//)
|
108
|
+
new_full_path = "#{@path}/#{new_name}"
|
109
|
+
connection.copy(full_path, new_full_path)
|
110
|
+
self.class.new(new_full_path, box)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Copy the entry to another dir. Returns an object representing the new
|
114
|
+
# copy.
|
115
|
+
def copy_to(dir)
|
116
|
+
raise Rush::NotADir unless dir.class == Rush::Dir
|
117
|
+
|
118
|
+
if box == dir.box
|
119
|
+
connection.copy(full_path, dir.full_path)
|
120
|
+
else
|
121
|
+
archive = connection.read_archive(full_path)
|
122
|
+
dir.box.connection.write_archive(archive, dir.full_path)
|
123
|
+
end
|
124
|
+
|
125
|
+
new_full_path = "#{dir.full_path}#{name}"
|
126
|
+
self.class.new(new_full_path, dir.box)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Move the entry to another dir. The object will be updated to show its new
|
130
|
+
# location.
|
131
|
+
def move_to(dir)
|
132
|
+
moved = copy_to(dir)
|
133
|
+
destroy
|
134
|
+
mimic(moved)
|
135
|
+
end
|
136
|
+
|
137
|
+
def mimic(from) # :nodoc:
|
138
|
+
@box = from.box
|
139
|
+
@path = from.path
|
140
|
+
@name = from.name
|
141
|
+
end
|
142
|
+
|
143
|
+
# Unix convention considers entries starting with a . to be hidden.
|
144
|
+
def hidden?
|
145
|
+
name.slice(0, 1) == '.'
|
146
|
+
end
|
147
|
+
|
148
|
+
# Set the access permissions for the entry.
|
149
|
+
#
|
150
|
+
# Permissions are set by role and permissions combinations which can be specified individually
|
151
|
+
# or grouped together. :user_can => :read, :user_can => :write is the same
|
152
|
+
# as :user_can => :read_write.
|
153
|
+
#
|
154
|
+
# You can also insert 'and' if you find it reads better, like :user_and_group_can => :read_and_write.
|
155
|
+
#
|
156
|
+
# Any permission excluded is set to deny access. The access call does not set partial
|
157
|
+
# permissions which combine with the existing state of the entry, like "chmod o+r" would.
|
158
|
+
#
|
159
|
+
# Examples:
|
160
|
+
#
|
161
|
+
# file.access = { :user_can => :read_write, :group_other_can => :read }
|
162
|
+
# dir.access = { :user => 'adam', :group => 'users', :read_write_execute => :user_group }
|
163
|
+
#
|
164
|
+
def access=(options)
|
165
|
+
connection.set_access(full_path, Rush::Access.parse(options))
|
166
|
+
end
|
167
|
+
|
168
|
+
# Returns a hash with up to nine values, combining user/group/other with read/write/execute.
|
169
|
+
# The key is omitted if the value is false.
|
170
|
+
#
|
171
|
+
# Examples:
|
172
|
+
#
|
173
|
+
# entry.access # -> { :user_can_read => true, :user_can_write => true, :group_can_read => true }
|
174
|
+
# entry.access[:other_can_read] # -> true or nil
|
175
|
+
#
|
176
|
+
def access
|
177
|
+
Rush::Access.new.from_octal(stat[:mode]).display_hash
|
178
|
+
end
|
179
|
+
|
180
|
+
# Change entry ownership
|
181
|
+
#
|
182
|
+
# Changes owner and group on the named files (in list) to the user user and the group group. user and group may be an ID (Integer/String) or a name (String). If user or group is nil, this method does not change the attribute.
|
183
|
+
#
|
184
|
+
# @param user [string/integer] The user to own the file
|
185
|
+
# @param group [string/integer] The group to own the file
|
186
|
+
# @param options [hash] the options to pass to FileUtils.chown (eg. 'noop', 'verbose' or 'recursive' )
|
187
|
+
#
|
188
|
+
def chown(user = nil, group = nil, options = {})
|
189
|
+
connection.chown(full_path, user, group, options)
|
190
|
+
self
|
191
|
+
end
|
192
|
+
|
193
|
+
# Shortcut to Entry::chown to pass the 'recursive' option by default
|
194
|
+
#
|
195
|
+
def chown_R(user = nil, group = nil, options = {})
|
196
|
+
options[:recursive] = true
|
197
|
+
chown(user, group, options)
|
198
|
+
end
|
199
|
+
|
200
|
+
# Chown in ruby way. Ruby way is creating accessors.
|
201
|
+
def owner
|
202
|
+
stat = ::File.stat(full_path)
|
203
|
+
{ user: Etc.getpwuid(stat.uid).name, group: Etc.getgrgid(stat.gid).name }
|
204
|
+
end
|
205
|
+
|
206
|
+
def owner=(params)
|
207
|
+
case params
|
208
|
+
when Hash then chown(params.delete(:user), params.delete(:group), params)
|
209
|
+
when String then chown(params)
|
210
|
+
when Numeric then chown(Etc.getpwuid(params).name)
|
211
|
+
else raise 'Something wrong with params for chown'
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Destroy the entry. If it is a dir, everything inside it will also be destroyed.
|
216
|
+
def destroy
|
217
|
+
connection.destroy(full_path)
|
218
|
+
end
|
219
|
+
|
220
|
+
def ==(other) # :nodoc:
|
221
|
+
full_path == other.full_path and box == other.box
|
222
|
+
end
|
223
|
+
|
224
|
+
private
|
225
|
+
|
226
|
+
def stat
|
227
|
+
connection.stat(full_path)
|
228
|
+
end
|
229
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Rush
|
2
|
+
# Base class for all rush exceptions.
|
3
|
+
class Exception < ::RuntimeError; end
|
4
|
+
|
5
|
+
# Client was not authorized by remote server; check credentials.
|
6
|
+
class NotAuthorized < Exception; end
|
7
|
+
|
8
|
+
# rushd is not running on the remote box.
|
9
|
+
class RushdNotRunning < Exception; end
|
10
|
+
|
11
|
+
# An unrecognized status code was returned by rushd.
|
12
|
+
class FailedTransmit < Exception; end
|
13
|
+
|
14
|
+
# The entry (file or dir) referenced does not exist. Message is the entry's full path.
|
15
|
+
class DoesNotExist < Exception; end
|
16
|
+
|
17
|
+
# The bash command had a non-zero return value. Message is stderr.
|
18
|
+
class BashFailed < Exception; end
|
19
|
+
|
20
|
+
# Attempts to rename, copy, or otherwise place an entry into a dir that
|
21
|
+
# already contains an entry by that name will fail with this exception.
|
22
|
+
class NameAlreadyExists < Exception; end
|
23
|
+
|
24
|
+
# Do not use rename or duplicate with a slash; use copy_to or move_to instead.
|
25
|
+
class NameCannotContainSlash < Exception; end
|
26
|
+
|
27
|
+
# You cannot move or copy entries to a path that is not a dir (should end with trailing slash).
|
28
|
+
class NotADir < Exception; end
|
29
|
+
|
30
|
+
# A user or permission value specified to set access was not valid.
|
31
|
+
class BadAccessSpecifier < Exception; end
|
32
|
+
end
|
data/lib/rush/file.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# Files are a subclass of Rush::Entry. Most of the file-specific operations
|
2
|
+
# relate to manipulating the file's contents, like search and replace.
|
3
|
+
class Rush::File < Rush::Entry
|
4
|
+
def dir?
|
5
|
+
false
|
6
|
+
end
|
7
|
+
|
8
|
+
def dirname
|
9
|
+
::File.dirname full_path
|
10
|
+
end
|
11
|
+
|
12
|
+
# Create a blank file.
|
13
|
+
def create
|
14
|
+
write('')
|
15
|
+
self
|
16
|
+
end
|
17
|
+
alias_method :touch, :create
|
18
|
+
|
19
|
+
# Size in bytes on disk.
|
20
|
+
def size
|
21
|
+
stat[:size]
|
22
|
+
end
|
23
|
+
|
24
|
+
# Raw contents of the file. For non-text files, you probably want to avoid
|
25
|
+
# printing this on the screen.
|
26
|
+
def contents
|
27
|
+
connection.file_contents(full_path)
|
28
|
+
end
|
29
|
+
|
30
|
+
alias_method :read, :contents
|
31
|
+
alias_method :cat, :contents
|
32
|
+
|
33
|
+
# Write to the file, overwriting whatever was already in it.
|
34
|
+
#
|
35
|
+
# Example: file.write "hello, world\n"
|
36
|
+
def write(new_contents)
|
37
|
+
connection.write_file(full_path, new_contents)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Append new contents to the end of the file, keeping what was in it.
|
41
|
+
def append(contents)
|
42
|
+
connection.append_to_file(full_path, contents)
|
43
|
+
end
|
44
|
+
alias_method :<<, :append
|
45
|
+
|
46
|
+
# Return an array of lines from the file, similar to stdlib's File#readlines.
|
47
|
+
def lines
|
48
|
+
contents.split("\n")
|
49
|
+
end
|
50
|
+
|
51
|
+
# Search the file's for a regular expression. Returns nil if no match, or
|
52
|
+
# each of the matching lines in its entirety.
|
53
|
+
#
|
54
|
+
# Example: box['/etc/hosts'].search(/localhost/) # -> [ "127.0.0.1 localhost\n", "::1 localhost\n" ]
|
55
|
+
def search(pattern)
|
56
|
+
matching_lines = lines.select { |line| line.match(pattern) }
|
57
|
+
matching_lines.size == 0 ? nil : matching_lines
|
58
|
+
end
|
59
|
+
|
60
|
+
# Search-and-replace file contents.
|
61
|
+
#
|
62
|
+
# Example: box['/etc/hosts'].replace_contents!(/localhost/, 'local.host')
|
63
|
+
def replace_contents!(pattern, replace_with)
|
64
|
+
write contents.gsub(pattern, replace_with)
|
65
|
+
end
|
66
|
+
alias_method :gsub, :replace_contents!
|
67
|
+
# Because I like vim.
|
68
|
+
alias_method :s, :replace_contents!
|
69
|
+
|
70
|
+
# Return the file's contents, or if it doesn't exist, a blank string.
|
71
|
+
def contents_or_blank
|
72
|
+
contents
|
73
|
+
rescue Rush::DoesNotExist
|
74
|
+
""
|
75
|
+
end
|
76
|
+
|
77
|
+
# Count the number of lines in the file.
|
78
|
+
def line_count
|
79
|
+
lines.size
|
80
|
+
end
|
81
|
+
|
82
|
+
# Return an array of lines, or an empty array if the file does not exist.
|
83
|
+
def lines_or_empty
|
84
|
+
lines
|
85
|
+
rescue Rush::DoesNotExist
|
86
|
+
[]
|
87
|
+
end
|
88
|
+
|
89
|
+
include Rush::Commands
|
90
|
+
|
91
|
+
def entries
|
92
|
+
[ self ]
|
93
|
+
end
|
94
|
+
end
|
data/lib/rush/find_by.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# Generic find_by (returns first match) and find_all_by (returns all matches)
|
2
|
+
# against arrays.
|
3
|
+
#
|
4
|
+
# Examples:
|
5
|
+
#
|
6
|
+
# processes.find_by_pid(::Process.pid)
|
7
|
+
# processes.find_all_by_cmdline(/mongrel_rails/)
|
8
|
+
#
|
9
|
+
module Rush::FindBy
|
10
|
+
def method_missing(meth, *args)
|
11
|
+
if m = meth.to_s.match(/^find_by_([a-z_]+)$/)
|
12
|
+
find_by(m[1], args.first)
|
13
|
+
elsif m = meth.to_s.match(/^find_all_by_([a-z_]+)$/)
|
14
|
+
find_all_by(m[1], args.first)
|
15
|
+
else
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def find_by(field, arg)
|
21
|
+
detect do |item|
|
22
|
+
item.respond_to?(field) and compare_or_match(item.send(field), arg)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def find_all_by(field, arg)
|
27
|
+
select do |item|
|
28
|
+
item.respond_to?(field) and compare_or_match(item.send(field), arg)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def compare_or_match(value, against)
|
33
|
+
if against.class == Regexp
|
34
|
+
value.match(against) ? true : false
|
35
|
+
else
|
36
|
+
value == against
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Integer extensions for file and dir sizes (returned in bytes).
|
2
|
+
#
|
3
|
+
# Example:
|
4
|
+
#
|
5
|
+
# box['/assets/'].files_flattened.select { |f| f.size > 10.mb }
|
6
|
+
class Fixnum
|
7
|
+
def kb
|
8
|
+
self * 1024
|
9
|
+
end
|
10
|
+
|
11
|
+
def mb
|
12
|
+
kb * 1024
|
13
|
+
end
|
14
|
+
|
15
|
+
def gb
|
16
|
+
mb * 1024
|
17
|
+
end
|
18
|
+
end
|