aleksi-rush 0.6.6

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