aleksi-rush 0.6.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/README.rdoc +89 -0
  2. data/Rakefile +59 -0
  3. data/VERSION +1 -0
  4. data/bin/rush +13 -0
  5. data/bin/rushd +7 -0
  6. data/lib/rush.rb +87 -0
  7. data/lib/rush/access.rb +130 -0
  8. data/lib/rush/array_ext.rb +19 -0
  9. data/lib/rush/box.rb +115 -0
  10. data/lib/rush/commands.rb +55 -0
  11. data/lib/rush/config.rb +154 -0
  12. data/lib/rush/dir.rb +160 -0
  13. data/lib/rush/embeddable_shell.rb +26 -0
  14. data/lib/rush/entry.rb +189 -0
  15. data/lib/rush/exceptions.rb +39 -0
  16. data/lib/rush/file.rb +85 -0
  17. data/lib/rush/find_by.rb +39 -0
  18. data/lib/rush/fixnum_ext.rb +18 -0
  19. data/lib/rush/head_tail.rb +11 -0
  20. data/lib/rush/local.rb +398 -0
  21. data/lib/rush/process.rb +59 -0
  22. data/lib/rush/process_set.rb +62 -0
  23. data/lib/rush/remote.rb +158 -0
  24. data/lib/rush/search_results.rb +58 -0
  25. data/lib/rush/server.rb +117 -0
  26. data/lib/rush/shell.rb +187 -0
  27. data/lib/rush/ssh_tunnel.rb +122 -0
  28. data/lib/rush/string_ext.rb +3 -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 +80 -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 +364 -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 +142 -0
@@ -0,0 +1,55 @@
1
+ # The commands module contains operations against Rush::File entries, and is
2
+ # mixed in to Rush::Entry and Array. This means you can run these commands against a single
3
+ # file, a dir full of files, or an arbitrary list of files.
4
+ #
5
+ # Examples:
6
+ #
7
+ # box['/etc/hosts'].search /localhost/ # single file
8
+ # box['/etc/'].search /localhost/ # entire directory
9
+ # box['/etc/**/*.conf'].search /localhost/ # arbitrary list
10
+ module Rush::Commands
11
+ # The entries command must return an array of Rush::Entry items. This
12
+ # varies by class that it is mixed in to.
13
+ def entries
14
+ raise "must define me in class mixed in to for command use"
15
+ end
16
+
17
+ # Search file contents for a regular expression. A Rush::SearchResults
18
+ # object is returned.
19
+ def search(pattern)
20
+ results = Rush::SearchResults.new(pattern)
21
+ entries.each do |entry|
22
+ if !entry.dir? and matches = entry.search(pattern)
23
+ results.add(entry, matches)
24
+ end
25
+ end
26
+ results
27
+ end
28
+
29
+ # Search and replace file contents.
30
+ def replace_contents!(pattern, with_text)
31
+ entries.each do |entry|
32
+ entry.replace_contents!(pattern, with_text) unless entry.dir?
33
+ end
34
+ end
35
+
36
+ # Count the number of lines in the contained files.
37
+ def line_count
38
+ entries.inject(0) do |count, entry|
39
+ count += entry.lines.size if !entry.dir?
40
+ count
41
+ end
42
+ end
43
+
44
+ # Invoke vi on one or more files - only works locally.
45
+ def vi(*args)
46
+ names = entries.map { |f| f.quoted_path }.join(' ')
47
+ system "vim #{names} #{args.join(' ')}"
48
+ end
49
+
50
+ # Invoke TextMate on one or more files - only works locally.
51
+ def mate(*args)
52
+ names = entries.map { |f| f.quoted_path }.join(' ')
53
+ system "mate #{names} #{args.join(' ')}"
54
+ end
55
+ end
@@ -0,0 +1,154 @@
1
+ # The config class accesses files in ~/.rush to load and save user preferences.
2
+ class Rush::Config
3
+ DefaultPort = 7770
4
+
5
+ attr_reader :dir
6
+
7
+ # By default, reads from the dir ~/.rush, but an optional parameter allows
8
+ # using another location.
9
+ def initialize(location=nil)
10
+ @dir = Rush::Dir.new(location || "#{ENV['HOME']}/.rush")
11
+ @dir.create
12
+ end
13
+
14
+ # History is a flat file of past commands in the interactive shell,
15
+ # equivalent to .bash_history.
16
+ def history_file
17
+ dir['history']
18
+ end
19
+
20
+ def save_history(array)
21
+ history_file.write(array.join("\n") + "\n")
22
+ end
23
+
24
+ def load_history
25
+ history_file.contents_or_blank.split("\n")
26
+ end
27
+
28
+ # The environment file is executed when the interactive shell starts up.
29
+ # Put aliases and your own functions here; it is the equivalent of .bashrc
30
+ # or .profile.
31
+ #
32
+ # Example ~/.rush/env.rb:
33
+ #
34
+ # server = Rush::Box.new('www@my.server')
35
+ # myproj = home['projects/myproj/']
36
+ def env_file
37
+ dir['env.rb']
38
+ end
39
+
40
+ def load_env
41
+ env_file.contents_or_blank
42
+ end
43
+
44
+ # Commands are mixed in to Array and Rush::Entry, alongside the default
45
+ # commands from Rush::Commands. Any methods here should reference "entries"
46
+ # to get the list of entries to operate on.
47
+ #
48
+ # Example ~/.rush/commands.rb:
49
+ #
50
+ # def destroy_svn(*args)
51
+ # entries.select { |e| e.name == '.svn' }.destroy
52
+ # end
53
+ def commands_file
54
+ dir['commands.rb']
55
+ end
56
+
57
+ def load_commands
58
+ commands_file.contents_or_blank
59
+ end
60
+
61
+ # Passwords contains a list of username:password combinations used for
62
+ # remote access via rushd. You can fill this in manually, or let the remote
63
+ # connection publish it automatically.
64
+ def passwords_file
65
+ dir['passwords']
66
+ end
67
+
68
+ def passwords
69
+ hash = {}
70
+ passwords_file.lines_or_empty.each do |line|
71
+ user, password = line.split(":", 2)
72
+ hash[user] = password
73
+ end
74
+ hash
75
+ end
76
+
77
+ # Credentials is the client-side equivalent of passwords. It contains only
78
+ # one username:password combination that is transmitted to the server when
79
+ # connecting. This is also autogenerated if it does not exist.
80
+ def credentials_file
81
+ dir['credentials']
82
+ end
83
+
84
+ def credentials
85
+ credentials_file.lines.first.split(":", 2)
86
+ end
87
+
88
+ def save_credentials(user, password)
89
+ credentials_file.write("#{user}:#{password}\n")
90
+ end
91
+
92
+ def credentials_user
93
+ credentials[0]
94
+ end
95
+
96
+ def credentials_password
97
+ credentials[1]
98
+ end
99
+
100
+ def ensure_credentials_exist
101
+ generate_credentials if credentials_file.contents_or_blank == ""
102
+ end
103
+
104
+ def generate_credentials
105
+ save_credentials(generate_user, generate_password)
106
+ end
107
+
108
+ def generate_user
109
+ generate_secret(4, 8)
110
+ end
111
+
112
+ def generate_password
113
+ generate_secret(8, 15)
114
+ end
115
+
116
+ def generate_secret(min, max)
117
+ chars = self.secret_characters
118
+ len = rand(max - min + 1) + min
119
+ password = ""
120
+ len.times do |index|
121
+ password += chars[rand(chars.length)]
122
+ end
123
+ password
124
+ end
125
+
126
+ def secret_characters
127
+ [ ('a'..'z'), ('1'..'9') ].inject([]) do |chars, range|
128
+ chars += range.to_a
129
+ end
130
+ end
131
+
132
+ # ~/.rush/tunnels contains a list of previously created ssh tunnels. The
133
+ # format is host:port, where port is the local port that the tunnel is
134
+ # listening on.
135
+ def tunnels_file
136
+ dir['tunnels']
137
+ end
138
+
139
+ def tunnels
140
+ tunnels_file.lines_or_empty.inject({}) do |hash, line|
141
+ host, port = line.split(':', 2)
142
+ hash[host] = port.to_i
143
+ hash
144
+ end
145
+ end
146
+
147
+ def save_tunnels(hash)
148
+ string = ""
149
+ hash.each do |host, port|
150
+ string += "#{host}:#{port}\n"
151
+ end
152
+ tunnels_file.write string
153
+ end
154
+ end
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 any unkown method call against 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,189 @@
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
+ # If +force+ is true, it will overwrite existed entry.
89
+ def rename(new_name, force = false)
90
+ connection.rename(@path, @name, new_name, force)
91
+ @name = new_name
92
+ self
93
+ end
94
+
95
+ # Rename an entry to another name within the same dir. The existing object
96
+ # will not be affected, but a new object representing the newly-created
97
+ # entry will be returned.
98
+ # If +force+ is true, it will overwrite existed entry.
99
+ def duplicate(new_name, force = false)
100
+ raise Rush::NameCannotContainSlash if new_name.match(/\//)
101
+ new_full_path = "#{@path}/#{new_name}"
102
+ connection.copy(full_path, new_full_path, force)
103
+ self.class.new(new_full_path, box)
104
+ end
105
+
106
+ # Copy the entry to another dir. Returns an object representing the new
107
+ # copy.
108
+ # If +force+ is true, it will overwrite existed entry.
109
+ def copy_to(dir, force = false)
110
+ raise Rush::NotADir unless dir.class == Rush::Dir
111
+
112
+ if box == dir.box
113
+ connection.copy(full_path, dir.full_path, force)
114
+ else
115
+ archive = connection.read_archive(full_path)
116
+ dir.box.connection.write_archive(archive, dir.full_path, force)
117
+ end
118
+
119
+ new_full_path = "#{dir.full_path}#{name}"
120
+ self.class.new(new_full_path, dir.box)
121
+ end
122
+
123
+ # Move the entry to another dir. The object will be updated to show its new
124
+ # location.
125
+ # If +force+ is true, it will overwrite existed entry.
126
+ def move_to(dir, force = false)
127
+ moved = copy_to(dir, force)
128
+ destroy
129
+ mimic(moved)
130
+ end
131
+
132
+ def mimic(from) # :nodoc:
133
+ @box = from.box
134
+ @path = from.path
135
+ @name = from.name
136
+ end
137
+
138
+ # Unix convention considers entries starting with a . to be hidden.
139
+ def hidden?
140
+ name.slice(0, 1) == '.'
141
+ end
142
+
143
+ # Set the access permissions for the entry.
144
+ #
145
+ # Permissions are set by role and permissions combinations which can be specified individually
146
+ # or grouped together. :user_can => :read, :user_can => :write is the same
147
+ # as :user_can => :read_write.
148
+ #
149
+ # You can also insert 'and' if you find it reads better, like :user_and_group_can => :read_and_write.
150
+ #
151
+ # Any permission excluded is set to deny access. The access call does not set partial
152
+ # permissions which combine with the existing state of the entry, like "chmod o+r" would.
153
+ #
154
+ # Examples:
155
+ #
156
+ # file.access = { :user_can => :read_write, :group_other_can => :read }
157
+ # dir.access = { :user => 'adam', :group => 'users', :read_write_execute => :user_group }
158
+ #
159
+ def access=(options)
160
+ connection.set_access(full_path, Rush::Access.parse(options))
161
+ end
162
+
163
+ # Returns a hash with up to nine values, combining user/group/other with read/write/execute.
164
+ # The key is omitted if the value is false.
165
+ #
166
+ # Examples:
167
+ #
168
+ # entry.access # -> { :user_can_read => true, :user_can_write => true, :group_can_read => true }
169
+ # entry.access[:other_can_read] # -> true or nil
170
+ #
171
+ def access
172
+ Rush::Access.new.from_octal(stat[:mode]).display_hash
173
+ end
174
+
175
+ # Destroy the entry. If it is a dir, everything inside it will also be destroyed.
176
+ def destroy
177
+ connection.destroy(full_path)
178
+ end
179
+
180
+ def ==(other) # :nodoc:
181
+ full_path == other.full_path and box == other.box
182
+ end
183
+
184
+ private
185
+
186
+ def stat
187
+ connection.stat(full_path)
188
+ end
189
+ end