adamwiggins-rush 0.6.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.
Files changed (50) hide show
  1. data/README.rdoc +87 -0
  2. data/Rakefile +61 -0
  3. data/VERSION +1 -0
  4. data/bin/rush +13 -0
  5. data/bin/rushd +7 -0
  6. data/lib/rush/access.rb +130 -0
  7. data/lib/rush/array_ext.rb +19 -0
  8. data/lib/rush/box.rb +112 -0
  9. data/lib/rush/commands.rb +55 -0
  10. data/lib/rush/config.rb +154 -0
  11. data/lib/rush/dir.rb +160 -0
  12. data/lib/rush/embeddable_shell.rb +26 -0
  13. data/lib/rush/entry.rb +185 -0
  14. data/lib/rush/exceptions.rb +31 -0
  15. data/lib/rush/file.rb +85 -0
  16. data/lib/rush/find_by.rb +39 -0
  17. data/lib/rush/fixnum_ext.rb +18 -0
  18. data/lib/rush/head_tail.rb +11 -0
  19. data/lib/rush/local.rb +402 -0
  20. data/lib/rush/process.rb +59 -0
  21. data/lib/rush/process_set.rb +62 -0
  22. data/lib/rush/remote.rb +156 -0
  23. data/lib/rush/search_results.rb +58 -0
  24. data/lib/rush/server.rb +117 -0
  25. data/lib/rush/shell.rb +187 -0
  26. data/lib/rush/ssh_tunnel.rb +122 -0
  27. data/lib/rush/string_ext.rb +3 -0
  28. data/lib/rush.rb +87 -0
  29. data/spec/access_spec.rb +134 -0
  30. data/spec/array_ext_spec.rb +15 -0
  31. data/spec/base.rb +24 -0
  32. data/spec/box_spec.rb +64 -0
  33. data/spec/commands_spec.rb +47 -0
  34. data/spec/config_spec.rb +108 -0
  35. data/spec/dir_spec.rb +164 -0
  36. data/spec/embeddable_shell_spec.rb +17 -0
  37. data/spec/entry_spec.rb +133 -0
  38. data/spec/file_spec.rb +83 -0
  39. data/spec/find_by_spec.rb +58 -0
  40. data/spec/fixnum_ext_spec.rb +19 -0
  41. data/spec/local_spec.rb +352 -0
  42. data/spec/process_set_spec.rb +50 -0
  43. data/spec/process_spec.rb +73 -0
  44. data/spec/remote_spec.rb +140 -0
  45. data/spec/rush_spec.rb +28 -0
  46. data/spec/search_results_spec.rb +44 -0
  47. data/spec/shell_spec.rb +23 -0
  48. data/spec/ssh_tunnel_spec.rb +122 -0
  49. data/spec/string_ext_spec.rb +23 -0
  50. metadata +141 -0
data/lib/rush/dir.rb ADDED
@@ -0,0 +1,160 @@
1
+ # A dir is a subclass of Rush::Entry that contains other entries. Also known
2
+ # as a directory or a folder.
3
+ #
4
+ # Dirs can be operated on with Rush::Commands the same as an array of files.
5
+ # They also offer a square bracket accessor which can use globbing to get a
6
+ # list of files.
7
+ #
8
+ # Example:
9
+ #
10
+ # dir = box['/home/adam/']
11
+ # dir['**/*.rb'].line_count
12
+ #
13
+ # In the interactive shell, dir.ls is a useful command.
14
+ class Rush::Dir < Rush::Entry
15
+ def dir?
16
+ true
17
+ end
18
+
19
+ def full_path
20
+ "#{super}/"
21
+ end
22
+
23
+ # Entries contained within this dir - not recursive.
24
+ def contents
25
+ find_by_glob('*')
26
+ end
27
+
28
+ # Files contained in this dir only.
29
+ def files
30
+ contents.select { |entry| !entry.dir? }
31
+ end
32
+
33
+ # Other dirs contained in this dir only.
34
+ def dirs
35
+ contents.select { |entry| entry.dir? }
36
+ end
37
+
38
+ # Access subentries with square brackets, e.g. dir['subdir/file']
39
+ def [](key)
40
+ key = key.to_s
41
+ if key == '**'
42
+ files_flattened
43
+ elsif key.match(/\*/)
44
+ find_by_glob(key)
45
+ else
46
+ find_by_name(key)
47
+ end
48
+ end
49
+ # Slashes work as well, e.g. dir/'subdir/file'
50
+ alias_method :/, :[]
51
+
52
+ def find_by_name(name) # :nodoc:
53
+ Rush::Entry.factory("#{full_path}/#{name}", box)
54
+ end
55
+
56
+ def find_by_glob(glob) # :nodoc:
57
+ connection.index(full_path, glob).map do |fname|
58
+ Rush::Entry.factory("#{full_path}/#{fname}", box)
59
+ end
60
+ end
61
+
62
+ # A list of all the recursively contained entries in flat form.
63
+ def entries_tree
64
+ find_by_glob('**/*')
65
+ end
66
+
67
+ # Recursively contained files.
68
+ def files_flattened
69
+ entries_tree.select { |e| !e.dir? }
70
+ end
71
+
72
+ # Recursively contained dirs.
73
+ def dirs_flattened
74
+ entries_tree.select { |e| e.dir? }
75
+ end
76
+
77
+ # Given a list of flat filenames, product a list of entries under this dir.
78
+ # Mostly for internal use.
79
+ def make_entries(filenames)
80
+ filenames.map do |fname|
81
+ Rush::Entry.factory("#{full_path}/#{fname}")
82
+ end
83
+ end
84
+
85
+ # Create a blank file within this dir.
86
+ def create_file(name)
87
+ file = self[name].create
88
+ file.write('')
89
+ file
90
+ end
91
+
92
+ # Create an empty subdir within this dir.
93
+ def create_dir(name)
94
+ name += '/' unless name.tail(1) == '/'
95
+ self[name].create
96
+ end
97
+
98
+ # Create an instantiated but not yet filesystem-created dir.
99
+ def create
100
+ connection.create_dir(full_path)
101
+ self
102
+ end
103
+
104
+ # Get the total disk usage of the dir and all its contents.
105
+ def size
106
+ connection.size(full_path)
107
+ end
108
+
109
+ # Contained dirs that are not hidden.
110
+ def nonhidden_dirs
111
+ dirs.select do |dir|
112
+ !dir.hidden?
113
+ end
114
+ end
115
+
116
+ # Contained files that are not hidden.
117
+ def nonhidden_files
118
+ files.select do |file|
119
+ !file.hidden?
120
+ end
121
+ end
122
+
123
+ # Run a bash command starting in this directory. Options are the same as Rush::Box#bash.
124
+ def bash(command, options={})
125
+ box.bash "cd #{quoted_path} && #{command}", options
126
+ end
127
+
128
+ # Destroy all of the contents of the directory, leaving it fresh and clean.
129
+ def purge
130
+ connection.purge full_path
131
+ end
132
+
133
+ # Text output of dir listing, equivalent to the regular unix shell's ls command.
134
+ def ls
135
+ out = [ "#{self}" ]
136
+ nonhidden_dirs.each do |dir|
137
+ out << " #{dir.name}/"
138
+ end
139
+ nonhidden_files.each do |file|
140
+ out << " #{file.name}"
141
+ end
142
+ out.join("\n")
143
+ end
144
+
145
+ # Run rake within this dir.
146
+ def rake(*args)
147
+ bash "rake #{args.join(' ')}"
148
+ end
149
+
150
+ # Run git within this dir.
151
+ def git(*args)
152
+ bash "git #{args.join(' ')}"
153
+ end
154
+
155
+ include Rush::Commands
156
+
157
+ def entries
158
+ contents
159
+ end
160
+ end
@@ -0,0 +1,26 @@
1
+ require 'rush/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,185 @@
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
+ # The factory checks to see if the full path has a trailing slash for
17
+ # creating a Rush::Dir rather than the default Rush::File.
18
+ def self.factory(full_path, box=nil)
19
+ if full_path.tail(1) == '/'
20
+ Rush::Dir.new(full_path, box)
21
+ elsif File.directory?(full_path)
22
+ Rush::Dir.new(full_path, box)
23
+ else
24
+ Rush::File.new(full_path, box)
25
+ end
26
+ end
27
+
28
+ def to_s # :nodoc:
29
+ if box.host == 'localhost'
30
+ "#{full_path}"
31
+ else
32
+ inspect
33
+ end
34
+ end
35
+
36
+ def inspect # :nodoc:
37
+ "#{box}:#{full_path}"
38
+ end
39
+
40
+ def connection
41
+ box ? box.connection : Rush::Connection::Local.new
42
+ end
43
+
44
+ # The parent dir. For example, box['/etc/hosts'].parent == box['etc/']
45
+ def parent
46
+ @parent ||= Rush::Dir.new(@path)
47
+ end
48
+
49
+ def full_path
50
+ "#{@path}/#{@name}"
51
+ end
52
+
53
+ def quoted_path
54
+ Rush.quote(full_path)
55
+ end
56
+
57
+ # Return true if the entry currently exists on the filesystem of the box.
58
+ def exists?
59
+ stat
60
+ true
61
+ rescue Rush::DoesNotExist
62
+ false
63
+ end
64
+
65
+ # Timestamp of most recent change to the entry (permissions, contents, etc).
66
+ def changed_at
67
+ stat[:ctime]
68
+ end
69
+
70
+ # Timestamp of last modification of the contents.
71
+ def last_modified
72
+ stat[:mtime]
73
+ end
74
+
75
+ # Timestamp that entry was last accessed (read from or written to).
76
+ def last_accessed
77
+ stat[:atime]
78
+ end
79
+
80
+ # Attempts to rename, copy, or otherwise place an entry into a dir that already contains an entry by that name will fail with this exception.
81
+ class NameAlreadyExists < Exception; end
82
+
83
+ # Do not use rename or duplicate with a slash; use copy_to or move_to instead.
84
+ class NameCannotContainSlash < Exception; end
85
+
86
+ # Rename an entry to another name within the same dir. The object's name
87
+ # will be updated to match the change on the filesystem.
88
+ def rename(new_name)
89
+ connection.rename(@path, @name, new_name)
90
+ @name = new_name
91
+ self
92
+ end
93
+
94
+ # Rename an entry to another name within the same dir. The existing object
95
+ # will not be affected, but a new object representing the newly-created
96
+ # entry will be returned.
97
+ def duplicate(new_name)
98
+ raise Rush::NameCannotContainSlash if new_name.match(/\//)
99
+ new_full_path = "#{@path}/#{new_name}"
100
+ connection.copy(full_path, new_full_path)
101
+ self.class.new(new_full_path, box)
102
+ end
103
+
104
+ # Copy the entry to another dir. Returns an object representing the new
105
+ # copy.
106
+ def copy_to(dir)
107
+ raise Rush::NotADir unless dir.class == Rush::Dir
108
+
109
+ if box == dir.box
110
+ connection.copy(full_path, dir.full_path)
111
+ else
112
+ archive = connection.read_archive(full_path)
113
+ dir.box.connection.write_archive(archive, dir.full_path)
114
+ end
115
+
116
+ new_full_path = "#{dir.full_path}#{name}"
117
+ self.class.new(new_full_path, dir.box)
118
+ end
119
+
120
+ # Move the entry to another dir. The object will be updated to show its new
121
+ # location.
122
+ def move_to(dir)
123
+ moved = copy_to(dir)
124
+ destroy
125
+ mimic(moved)
126
+ end
127
+
128
+ def mimic(from) # :nodoc:
129
+ @box = from.box
130
+ @path = from.path
131
+ @name = from.name
132
+ end
133
+
134
+ # Unix convention considers entries starting with a . to be hidden.
135
+ def hidden?
136
+ name.slice(0, 1) == '.'
137
+ end
138
+
139
+ # Set the access permissions for the entry.
140
+ #
141
+ # Permissions are set by role and permissions combinations which can be specified individually
142
+ # or grouped together. :user_can => :read, :user_can => :write is the same
143
+ # as :user_can => :read_write.
144
+ #
145
+ # You can also insert 'and' if you find it reads better, like :user_and_group_can => :read_and_write.
146
+ #
147
+ # Any permission excluded is set to deny access. The access call does not set partial
148
+ # permissions which combine with the existing state of the entry, like "chmod o+r" would.
149
+ #
150
+ # Examples:
151
+ #
152
+ # file.access = { :user_can => :read_write, :group_other_can => :read }
153
+ # dir.access = { :user => 'adam', :group => 'users', :read_write_execute => :user_group }
154
+ #
155
+ def access=(options)
156
+ connection.set_access(full_path, Rush::Access.parse(options))
157
+ end
158
+
159
+ # Returns a hash with up to nine values, combining user/group/other with read/write/execute.
160
+ # The key is omitted if the value is false.
161
+ #
162
+ # Examples:
163
+ #
164
+ # entry.access # -> { :user_can_read => true, :user_can_write => true, :group_can_read => true }
165
+ # entry.access[:other_can_read] # -> true or nil
166
+ #
167
+ def access
168
+ Rush::Access.new.from_octal(stat[:mode]).display_hash
169
+ end
170
+
171
+ # Destroy the entry. If it is a dir, everything inside it will also be destroyed.
172
+ def destroy
173
+ connection.destroy(full_path)
174
+ end
175
+
176
+ def ==(other) # :nodoc:
177
+ full_path == other.full_path and box == other.box
178
+ end
179
+
180
+ private
181
+
182
+ def stat
183
+ connection.stat(full_path)
184
+ end
185
+ end
@@ -0,0 +1,31 @@
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
+ # There's already an entry by the given name in the given dir.
21
+ class NameAlreadyExists < Exception; end
22
+
23
+ # The name cannot contain a slash; use two operations, rename and then move, instead.
24
+ class NameCannotContainSlash < Exception; end
25
+
26
+ # You cannot move or copy entries to a path that is not a dir (should end with trailing slash).
27
+ class NotADir < Exception; end
28
+
29
+ # A user or permission value specified to set access was not valid.
30
+ class BadAccessSpecifier < Exception; end
31
+ end
data/lib/rush/file.rb ADDED
@@ -0,0 +1,85 @@
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
+ # Create a blank file.
9
+ def create
10
+ write('')
11
+ self
12
+ end
13
+
14
+ # Size in bytes on disk.
15
+ def size
16
+ stat[:size]
17
+ end
18
+
19
+ # Raw contents of the file. For non-text files, you probably want to avoid
20
+ # printing this on the screen.
21
+ def contents
22
+ connection.file_contents(full_path)
23
+ end
24
+
25
+ alias :read :contents
26
+
27
+ # Write to the file, overwriting whatever was already in it.
28
+ #
29
+ # Example: file.write "hello, world\n"
30
+ def write(new_contents)
31
+ connection.write_file(full_path, new_contents)
32
+ end
33
+
34
+ # Append new contents to the end of the file, keeping what was in it.
35
+ def append(contents)
36
+ connection.append_to_file(full_path, contents)
37
+ end
38
+ alias_method :<<, :append
39
+
40
+ # Return an array of lines from the file, similar to stdlib's File#readlines.
41
+ def lines
42
+ contents.split("\n")
43
+ end
44
+
45
+ # Search the file's for a regular expression. Returns nil if no match, or
46
+ # each of the matching lines in its entirety.
47
+ #
48
+ # Example: box['/etc/hosts'].search(/localhost/) # -> [ "127.0.0.1 localhost\n", "::1 localhost\n" ]
49
+ def search(pattern)
50
+ matching_lines = lines.select { |line| line.match(pattern) }
51
+ matching_lines.size == 0 ? nil : matching_lines
52
+ end
53
+
54
+ # Search-and-replace file contents.
55
+ #
56
+ # Example: box['/etc/hosts'].replace_contents!(/localhost/, 'local.host')
57
+ def replace_contents!(pattern, replace_with)
58
+ write contents.gsub(pattern, replace_with)
59
+ end
60
+
61
+ # Return the file's contents, or if it doesn't exist, a blank string.
62
+ def contents_or_blank
63
+ contents
64
+ rescue Rush::DoesNotExist
65
+ ""
66
+ end
67
+
68
+ # Count the number of lines in the file.
69
+ def line_count
70
+ lines.size
71
+ end
72
+
73
+ # Return an array of lines, or an empty array if the file does not exist.
74
+ def lines_or_empty
75
+ lines
76
+ rescue Rush::DoesNotExist
77
+ []
78
+ end
79
+
80
+ include Rush::Commands
81
+
82
+ def entries
83
+ [ self ]
84
+ end
85
+ end
@@ -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
@@ -0,0 +1,11 @@
1
+ # Mixin for array and string for methods I wish they had.
2
+ module Rush::HeadTail
3
+ def head(n)
4
+ slice(0, n)
5
+ end
6
+
7
+ def tail(n)
8
+ n = [ n, length ].min
9
+ slice(-n, n)
10
+ end
11
+ end